Scrape a saved job board without losing its source
source data loading
scraping
workforce research
Learn how to extract job titles and training details from structured HTML with R.
The fictional Riverton Workforce Lab begins with six saved job postings. A worker needs to find each jobβs training offers, schedule, experience requirements, and named skills. Combining all lines without source identifiers would make omissions and source errors difficult to find.
Web scraping uses code to collect information from web pages. The team will scrape the saved page, keep the job and sentence identifiers, and examine every record before it becomes research data. This first stage collects what the notices say. It does not classify the sentences or generalize from them.
The lesson uses static archived HTML. The saved file contains the text within its markup. The last sections show two page structures that the initial extraction method cannot handle.
Note
The employers, postings, and Riverton Workforce Lab are fictional. The author wrote the HTML file for this lesson without personal information.
TipWhat you will learn
By the end of this lesson, you will be able to:
explain how HTML gives parts of a page names and structure.
select repeated job cards with rvest.
keep source identifiers beside extracted text.
compare scraped output with an expected fixture.
detect a renamed class and a page whose text is not in the markup.
describe legal, ethical, and technical checks needed before live scraping.
Begin with an archived page
Live websites change. A saved source file lets another researcher examine the same page later. The team records the file path, size, and fingerprint before extracting anything.
library(readr)library(dplyr)library(tibble)library(purrr)library(stringr)library(digest)library(rvest)job_page_path <-"data/workforce/job-board.html"job_page_lines <-read_lines(job_page_path)job_page_fingerprint <- digest::digest(paste(job_page_lines, collapse ="\n"),algo ="md5",serialize =FALSE)job_page_metadata <-read_csv("data/workforce/job-board-metadata.csv",col_types =cols(source_file =col_character(),created_for_lesson =col_character(),retrieval_status =col_character(),records =col_character(),md5 =col_character(),rights_note =col_character() ))# A size read from disk counts the line endings the local copy happens to# use, so it changes between systems. Measuring the newline-joined text, the# same text the fingerprint covers, describes the content instead.job_page_size <-nchar(paste(job_page_lines, collapse ="\n"),type ="bytes")tibble(source_file = job_page_path,bytes = job_page_size,fingerprint = job_page_fingerprint)
The code compares the fingerprint with a stored value. The fingerprint changes if the archived HTML changes and can reveal an accidental edit. It cannot prove that the postings are correct or complete.
Read the page structure
HTML uses elements such as headings, paragraphs, and lists to organize a page. The example page places every posting inside an element whose class is job-card. A CSS selector is a short pattern that names the elements to collect.
All 22 sentences match the fixture in the same order. The check would stop the build if a selector omitted, duplicated, or reordered text.
The author wrote both the HTML and expected table for this lesson. The comparison provides a parser-behavior check, not independent truth. A live collection would need a separate audit sample.
When the page structure changes
The archived file preserves one page version. People can redesign a live board. The following HTML strings illustrate two different failures of the earlier extraction method.
The first is a redesign. The employerβs new template renames job-card to posting and moves the title into a heading.
The same selector against two versions of one site
Page
Selector
Cards found
Cards expected
archived board
.job-card
6
6
redesigned page
.job-card
0
1
Zero matches can indicate either an empty page or a selector failure. Without an expected count, the scraper could report a redesign as a day with no postings. A check of the expected count stops the scraper when the selector no longer matches.
The second pageβs visible container has no job text. The file stores a JSON payload in a script element. Application code could read and render that data later. This fixture does not execute JavaScript. It separates visible markup from embedded data.
The container contains no characters. The job title exists only in the embedded script payload, which rvest can read as text but does not render into the container.
This example includes the payload in the file. Many sites request data separately after loading the initial page. Their saved initial HTML may contain no text to extract. An HTML parser cannot recover a response that the archive does not contain.
Three collection methods differ in cost and guarantees. First, search for a documented API that returns the data under stated terms. The pageβs underlying data request may also accept direct calls. Direct calls can be faster but depend on an interface that the provider may change.
A headless browser runs the page as a browser would. R can control one through chromote, and Python can use Playwright or Selenium. This is the general solution and the most expensive of these methods to run and maintain.
Record the collection method. Text from a rendered page and text from a saved file are different evidence. Readers need to know which method produced the reported numbers.
A saved page is still incomplete evidence
The archived HTML shows what was captured, not every posting that existed. Before scraping a live site, the team must examine:
the siteβs terms and robots policy.
whether collection is lawful and expected in the relevant jurisdiction.
whether personal or sensitive information appears.
permitted request frequency.
whether JavaScript loads page content later.
the dates, filters, and geographic limits of the results.
how the saved HTML and retrieval time will be preserved.
The Robots Exclusion Protocol communicates crawler preferences. It is not a grant of legal permission, and permission does not settle every ethical question. A public page can still contain information that should not be collected or republished.
What to remember
Scraping converts page structure into data. It does not establish that the data is correct or representative.
Archive the source. Record its collection date.
Keep source IDs attached to extracted text.
Assert expected counts and content so selector failures cannot return zero rows without an error.
Zero matches and an empty page look identical without an expected count.
Some pages load text separately from the saved markup. An HTML parser cannot extract text absent from its input.
Examine terms, rate limits, privacy, and research ethics before live collection.
The team can examine 22 sentences with their source identifiers from the six notices. The table excludes one source, a fictional training flyer saved as an image. That source requires OCR instead of HTML extraction. Preserve the image. Examine the extracted wording before using it.