Also the RSelenium package is too outdated to use currently but is being developed again
Static websites
Static websites
Static websites have their content ready to go when you visit them. They require fewer tools for scraping because you can just read the HTML and parse out what you need from it.
Example 1: extract URLs
This is a simplified version of a project I’ve done to batch download, clean, and build a database from the microdata for the Census Bureau’s Household Pulse Survey.
About the data
Started as rapid response at the beginning of covid lockdown
Pandemic lasted longer than expected, so the survey did too
As a result, infrastructure is messier than Census Bureau products would normally be
Contained a (poorly worded) question on gender identity for later survey waves
One of very few sources to do so, which is main reason I’ve used it
The task
Each year of the survey is on a different page, reached by a link on a tab
Each iteration has a block of HTML with varying structure
Release date or cycle
Date range of survey
Links to zipped SAS & CSV files, sometimes supplemental files
Organized non-semantically into phases
Inconsistent naming
Get to know the HTML
Find links to CSV files & mark with release dates/cycles
Browser devtools help you investigate HTML, figure out what elements you’re looking for
Extensions can help verify XPath/CSS selectors
LibreWolf/Firefox devtools
Selectors
Main types of selectors are CSS and XPath. Both are more complex than I can get into.
CSS is simple but less flexible; XPath is much more powerful but tricky
import requestsimport pandas as pd# from bs4 import BeautifulSoupfrom lxml import htmlimport pprint# get the htmlcb_url ="https://www.census.gov/programs-surveys/household-pulse-survey/data/datasets.2023.html"cb_resp = requests.get(cb_url)# initiate a parser for the page sourcecb_src = html.fromstring(cb_resp.content)
Plot twist
The information we want is hierarchical, but the HTML is not. So we have to dump everything into a list and nest it ourselves.
XPath for <h3>, <h4>, and <a> elements of interest:
elements = cb_src.xpath("//h3[contains(text(), 'PUF')] | //h4 | //a[contains(text(), 'PUF CSV')]")print(elements[0:4])
[<Element h3 at 0x7f60e9161eb0>, <Element h4 at 0x7f619870a2d0>, <Element a at 0x7f619870af90>, <Element h4 at 0x7f619870a9f0>]
Parsing
For each element, get its tag and text
If an <a>, also get its href attribute
def extract_info(element): tag = element.tag text = element.textif tag =="a": href = element.attrib["href"]else: href =Nonereturn {"tag": tag, "text": text, "href": href}element_info = [extract_info(el) for el in elements]pprint.pp(element_info[0:3])
Fill missing headers, then filter to keep one row per survey wave
# fill h3s downdf["h3"] = df["h3"].ffill()# fill h4s down within h3 groupsdf["h4"] = df.groupby("h3")["h4"].ffill()# keep only link rowsdf = df.loc[df["tag"] =="a",]
We have a collection of 12 links to batch download our data.
In my real project, I had to then parse all the dates and standardize labels, then do that for every year (this was the simplest year). 60 waves of the survey total.
After writing out all this metadata, I wrote a bash script to download all the data files and write them into a database. Then I set that up to run in GitHub Actions to check for updates & rebuild the database every week.
Dynamic websites
Scraping dynamic websites
Dynamic websites don’t have all their content available when you visit a URL. Scraping is similar, but takes extra steps to finagle the page source
Sites filled by API calls or other servers
Tables generated by form choices
Crawling over pages of results
It’s complicated
Doing this requires a few additional tools that don’t lend themselves to slides. My method is generally:
Run Selenium in a docker container to mimic a headless browser
Send Selenium web driver to website
Click on stuff (forms, pager links, etc)
Extract HTML from each iteration
Repeat
Example 2: stream URLs
This is another batch of federal datasets, this time much better formatted but loaded dynamically.
Clearly there’s a table, but {requests} can’t get it