Getting data from the web: APIs and scraping

After this lesson you will read a web API call part by part — method, host, path, parameters — together with the JSON it returns, and decide for any website whether you can ask an API, simply download and parse the page, or must drive a real browser, and which of BeautifulSoup, Scrapy and Selenium does what.

The exercise session on APIs prints this request to Trenitalia's ViaggiaTreno service (p. 1517):

http://www.viaggiatreno.it/infomobilita/resteasy/viaggiatreno/partenze/S00219/Wed%20Nov%2005%202025%2020:43:00%20GMT+0100%20(Central%20European%20Standard%20Time)

What came back was not a web page but a list of twelve departures, each one a record with named fields. Now look at the tail of the URL. Decoded, it reads "Wed Nov 05 2025 20:43:00 GMT+0100 (Central European Standard Time)". Nobody types a date like that into an address bar: the weekday, the seconds, the offset from Greenwich, the time zone spelled out in brackets, and a %20 for every space.

So who wrote this URL? The answer is also the reason why a script that downloads a web page sometimes finds nothing in a page that your browser shows full of data. Keep the question in mind; it is answered at the end of the lesson.

An API is a counter where programs ask for data

The course notes call data ingestion "the first and fundamental step of any Data Analysis Pipeline" (§5.1.1) — the bottom layer of the architecture of lesson 4, and the step that a ready-made dataset does for you in most courses. From the web there are two ways in: ask an API, or scrape the page.

Picture a take-away counter: a form with fixed fields goes in, a labelled box comes out, and you never see the kitchen. The first slide defines this counter for software (p. 1510):

Application Programming Interface (API) — "a set of rules and protocols that allows different software applications to communicate with each other and exchange data. Often can be called remotely." "APIs act as an intermediary layer, enabling developers to use existing functions and services from other applications, which simplifies development."

The definition is general: the functions of a Python library are an API too. "Remotely" narrows it to the web API, called over the network with HTTP — in the course notes, "A Web API is a API which is based on the HTTP protocol, and it's usually used to give programmatic access to data and platforms" (§5.1.2). "API without Libraries", the title of the first two slides, is not explained on them; read it as calling the API directly — a URL in the browser, in Postman or with curl — rather than through a client library that wraps the calls.

Every call names an HTTP method and a URL: "APIs can be invoked through HTTP methods (e.g., GET, POST, etc.) depending on the kind of operations to be performed", and "As an end-user, the most used requests for APIs are GET requests" (p. 1511). GET reads, and whoever collects data mostly reads; it also fits in one URL, which is why every example of the session is a line you could paste into a browser.

To develop and test APIs, "Postman is one of the most renowned ones" (p. 1511). The next slide is a screenshot of it with four red callouts, redrawn in Fig. 1.

API Collection Query Request Parameters Result API 101 GETGet all customers GETGet one customer POSTAdd new customer PUTUpdate customer DELRemove customer GET postman-echo.com /get?firstName=foo1 &lastName=foo2 Send KEY VALUE firstNamefoo1 lastNamefoo2 200 OK · 795 ms · 770 B "args": { "firstName": "foo1", "lastName": "foo2" }, "headers": { "host": "postman-echo.com", "user-agent": "PostmanRuntime/7.26.8" echoed back
Fig. 1. The Postman screenshot of p. 1512, redrawn and simplified, with the slide's four callouts as panel titles. Orange: the request you write; blue: the data that comes back. The server, postman-echo.com, answers with the request it received: the two parameters reappear as args, and two headers that nobody typed — host and the client's user-agent — travelled with them.

Read the four parts in the order you use them. The API Collection keeps saved requests; "API 101" has one HTTP method per operation on customers — GET to read all or one, POST to add, PUT to update, DEL (DELETE) to remove. The Query callout points at the request line, method and URL. Request Parameters lists the key=value pairs after the ?. The Result holds status 200 OK, time, size and a JSON body — and since the server is an echo, the body shows what really travelled: your parameters as args, plus headers the client added on its own.

Why one method per operation? The REST convention behind "API 101" (course notes, not the slides)

Most web APIs follow the REST paradigm, which gives URLs a predictable shape, METHOD http://domain/collection/item — for example GET http://example.com/user/1234 — and fixes what each method does on a collection or on one item (course notes, §5.1.2):

The verbs of REST, as the course notes tabulate them.
VerbOn a collectionOn an item
POSTCreate a new itemnot used
GETGet list of elementsGet the selected item
PUTnot usedUpdate the selected item
DELETEnot usedDelete the selected item

The names in "API 101" read like this table: "Get all customers" is GET on the collection, "Get one customer" GET on an item, "Add new customer" POST on the collection (the screenshot shows the names, not the URLs).

A web API answers a method and a URL with data for programs — usually JSON — not with a page for people.

Reading a request: the iTunes Store examples

The session's first worked example asks Apple's iTunes Store to "Retrieve the first 10 artists whose name contains "maroon"" (p. 1513). No app, no login: the whole question is one URL.

GET https://itunes.apple.com/search?term=maroon&entity=allArtist&attribute=allArtistTerm&limit=10

Read it left to right: https is the scheme, the protocol; itunes.apple.com the host, the server that answers; /search the path, the service you call. After the ? comes the query string: key=value pairs separated by &, each an instruction for the search. The course notes print the general shape (§5.1.2); the bracketed parts are optional:

URL format (RFC 2396):
scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
The parameters of the two iTunes calls (pp. 1513–1514).
Keyp. 1513p. 1514What it asks for
termmaroonStallonethe text searched for
entityallArtistmovieArtistwhich kind of object to return
attributeallArtistTerm—which field the term is matched against: the artist's name
limit10—at most this many results; without it the service's default applies

The answer is not a page but a JSON document: an object with resultCount and results, an array of artist objects. Its start, as the slide prints it:

{
  "resultCount": 10,
  "results": [
    {
      "wrapperType": "artist",
      "artistType": "Artist",
      "artistName": "Maroon 5",
      "artistLinkUrl": "https://music.apple.com/us/artist/maroon-5/1798556?uo=4",
      "artistId": 1798556,
      "amgArtistId": 529962,
      "primaryGenreName": "Rock",
      "primaryGenreId": 21
    },
    … nine more artist objects ("Maroon" is the second)
  ]
}

resultCount is 10 because limit=10 allowed at most ten; each element of results is a record with named fields — structure an API gives you for free, and scraping must rebuild from a page layout.

The second call, "Retrieve all the artist whose surname is "Stallone"" (p. 1514), is GET https://itunes.apple.com/search?term=Stallone&entity=movieArtist: no attribute, no limit. Its answer has "resultCount": 6, and its first two records do not have the same fields:

"artistType": "Artist",
"artistName": "Frank Stallone",
"artistLinkUrl": "https://music.apple.com/…",
"artistId": 91734703,
"amgArtistId": 5513,
"primaryGenreName": "Pop",
"primaryGenreId": 14
"artistType": "Movie Artist",
"artistName": "Sylvester Stallone",
"artistLinkUrl": "https://itunes.apple.com/…",
"artistId": 186052826,
"primaryGenreName": "Drama",
"primaryGenreId": 4406

Frank Stallone has an amgArtistId and a link on music.apple.com; Sylvester Stallone has no amgArtistId and a link on itunes.apple.com. Records of one answer need not share their fields — the freedom a document store gives the documents of one collection (lesson 13) — so a program must check that a field exists before it uses it.

"The URL filters artists by surname." It does not: there is no surname key, term=Stallone is a free-text search on names, and the caption gives the intention, not the mechanism. The screenshot shows more: although the call asks for entity=movieArtist, its first result, Frank Stallone, is an "Artist" with genre Pop. A likely reason, not given on the slide: Apple's documentation lists movieArtist as an entity of the movie media, and this call sets no media key. Read the answer, not the caption.

A URL cannot carry a space, and it reads &, ? and # as separators. Inside a value such characters are written with percent-encoding: % plus the character's code in two hexadecimal digits, so a space is %20 and an & is %26. Otherwise the server would read an & inside a value as the start of a new pair. The warm-up URL has nine %20.

In Python (background), requests builds and encodes the query string from a dictionary, as Postman does from its table:

# the request of p. 1513, in Python
import requests

url = "https://itunes.apple.com/search"
params = {"term": "maroon",
          "entity": "allArtist",
          "attribute": "allArtistTerm",
          "limit": 10}
r = requests.get(url, params=params)
data = r.json()       # a dict
data["resultCount"]   # 10

Anatomy of a request

The two iTunes calls of the slides, plus one that is not on the slides, taken apart as Postman would show them. Pick a call, move limit (0 means the key is not sent) and switch percent-encoding off and on: the URL, the pairs the server reads and the answer update as if you had pressed Send. The widget is offline. Records printed on the slides are shown in full; the others are marked "not printed", and every count says whether the slide prints it or the widget computed it.

  1. The one insight to take away: the URL is the whole question — every key=value pair after the ? is one instruction that the server reads. Start with the call of p. 1513. Predict: with limit at 3, what is resultCount, and how many objects are in results? Then move the slider.
    What you should see

    resultCount becomes 3 and results holds three objects: Maroon 5, Maroon and one record that the slide does not print. Nothing else in the URL changed, so the answer changed only where the instruction changed: limit caps how many results come back.

  2. Choose the call of p. 1514. It arrives with no limit, as on the slide, and resultCount is 6. Predict resultCount for limit = 8, then try it. While you are there, compare the first two records.
    What you should see

    Still 6, as the widget computes it: without limit the call already returned only six artists although Apple's default allows 50, so there are no more to return — limit is a maximum, not a promise. The records differ: Frank Stallone carries an amgArtistId, Sylvester Stallone has none (the widget marks the missing field), and Frank is a music "Artist" although the call asks for movie artists.

  3. Choose the third call, with the term Simon & Garfunkel. Before you switch percent-encoding off, predict how many pairs the server will read, and what it will take as the value of term.
    What you should see

    With encoding on, the value travels as Simon%20%26%20Garfunkel and the server reads it back whole: two pairs (three with a limit). With encoding off, the raw & splits the value: the server reads term = "Simon " (with a trailing space), then a pair whose key is " Garfunkel" and whose value is empty. One unencoded character turned one instruction into two wrong ones.

Parameters in the path, days in milliseconds: ViaggiaTreno

Trenitalia's ViaggiaTreno service works differently: its parameters travel inside the path, one per segment, and it takes two calls — the first returns the key the second needs. First, the autocompletion of train number 798 (p. 1515):

GET http://www.viaggiatreno.it/infomobilita/resteasy/viaggiatreno/cercaNumeroTrenoTrenoAutocomplete/798

798 - SALERNO - 04/11/25|798-S09818-1762210800000
798 - SALERNO - 05/11/25|798-S09818-1762297200000

Two lines of plain text, one per run of train 798: the run that departs on 4 November and the one that departs on 5 November. Left of the | is a label for people; right of it, a key for programs — train number, station code (S09818 is SALERNO, as the line says) and departure day, joined by dashes. The second call puts the key's parts into the path (p. 1516; its answer is cut down here to a few of its fields):

GET http://www.viaggiatreno.it/infomobilita/resteasy/viaggiatreno/andamentoTreno/S09818/798/1762210800000

{tipoTreno: "PG", orientamento: null, codiceCliente: 4, fermateSoppresse: [], …}
  arrivato: true
  categoria: "ICN"
  circolante: true
  compClassRitardoLine: "regolare_line"
  compDurata: "14:35"
  compImgCambiNumerazione: "  "
  compImgRitardo: "/vt_static/img/legenda/icone_legenda/regolare.png"
  …
1 …/cercaNumeroTrenoTrenoAutocomplete/798 798 - SALERNO - 04/11/25 | 798-S09818-1762210800000 798 - SALERNO - 05/11/25 | 798-S09818-1762297200000 for people for programs split: 798 S09818 1762210800000 train station day (epoch ms) 2 …/andamentoTreno/ S09818/ 798/ 1762210800000 categoria: "ICN" compDurata: "14:35" arrivato: true circolante: true
Fig. 2. Two calls, one workflow (pp. 1515–1516). Orange dashed boxes are the requests, blue arrows the answers. The first answer carries a key for programs; split at the dashes it gives train, station and day, and the second request puts them into its path — in another order: station, train, day. Its answer is the status of the train, in JSON.

The second answer is the train's status, with Italian keys: categoria: "ICN", compDurata: "14:35" (a duration), arrivato: true. The comp keys carry pieces of the website — CSS class names such as "regolare_line", image paths, the HTML entity   — the opposite of the "Separation between model and presentation" that the course notes list as the first advantage of a web API (§5.1.2).

What 1762210800000 means

The day is a number: epoch time in milliseconds, the count of milliseconds since 1 January 1970, 00:00:00 UTC — the usual way computers store an instant. The steps check that 1762210800000 is the midnight of 4 November 2025 in Italy.

  1. Fix the instant: in November Italy is on CET, UTC + 1, so local midnight of 4 November is 23:00 UTC on 3 November.

  2. Whole years 1970–2024: 55 years of 365 days, plus one day for each of the 14 leap years 1972 … 2024.

    \[ 55 \times 365 + 14 = 20\,089 \text{ days} \]
  3. In 2025, January to October have 304 days; 1 and 2 November make 306.

    \[ 20\,089 + 306 = 20\,395 \text{ days} \]
  4. A day has \(24 \times 3600 = 86\,400\) s; convert, then add the 23 hours of 3 November.

    \[ 20\,395 \times 86\,400 = 1\,762\,128\,000 \] \[ +\ 82\,800 = 1\,762\,210\,800 \text{ s} \]
  5. The API counts milliseconds: multiply by 1000 and you get the key of p. 1515, digit for digit.

    \[ 1\,762\,210\,800\,000 \text{ ms} \]
  6. One day later is 86 400 000 ms more — the second key of p. 1515.

    \[ 1\,762\,297\,200\,000 \text{ ms} \]

"The train leaves at 1762210800000." The caption of p. 1516 says "at", as if the number were a time. It is a day, named by its first millisecond: a run of a train is identified by train number, origin station and departure day — the three parts of the key.

The third call asks for all departures from a station at a given moment (p. 1517) — the long date of the warm-up. Decoded, the URL reads:

http://www.viaggiatreno.it/infomobilita/resteasy/viaggiatreno/partenze/S00219/Wed Nov 05 2025 20:43:00 GMT+0100 (Central European Standard Time)

The answer is a JSON array of twelve departures. Element 0, compNumeroTreno: "REG 3143", has codOrigine: "S00219" and, side by side, dataPartenzaTrenoAsDate: "2025-11-05" and dataPartenzaTreno: 1762297200000 — the same day for people and for programs, and the number the autocompletion gave for 5 November.

Providers and their rules

Three more slides present data providers reached through APIs.

The three providers of pp. 1518–1520, in the slides' words.
ProviderWhat the slide says
Twitter API (p. 1518)"enables programmatic access to Twitter in unique and advanced ways"; two versions: v1 "is free and open to everyone" and "only provides the last week of tweets"; v2 "requires the developer to submit an authorization for each application they develop" and "also includes an access to past tweets"
OpenStreetMap (p. 1519)"a collaborative project to create a free editable geographic database of the world"; "Basically, it is a crowdsourced map of the world"; users collect data by "manual survey, GPS devices, aerial photography, and other free sources", or "their own local knowledge of the area"
OpenWeatherMap (p. 1520)"provides a set of weather-related products, including short-term and long-term forecasting, history and observations for any location"; "Most of their services are provided through APIs."

The Twitter slide shows the Twitter logo crossed out by a red no-entry sign: read it as a warning that this API no longer works as described. Indeed (background), the version the slide calls v1 was v1.1, and since 2023 X, as Twitter is now called, sells read access to its API in paid tiers. At the exam, describe the two versions as the slide prints them; what lasts is the idea — an API is a contract its owner can change (who may call it, which data it reaches), and every pipeline built on it depends on that contract.

What the slides leave out: why providers offer APIs, keys, pagination, rate limits, status codes (background from the course notes and standard HTTP)
  • Why providers offer APIs. The course notes list the advantages of a web API (§5.1.2): "Separation between model and presentation"; regulated access to the data (traceable accounts, only a portion of the data); no direct access to the platform website; request throttling; no server congestion; protection against DoS and DDoS attacks; paid access to the full data, or to higher volumes.
  • Keys and tokens. "Almost all the APIs require a kind of user authentication": you register at the provider's developer portal, get a key and a secret, and exchange them for a token sent with every request, usually through OAuth (course notes, §5.1.2).
  • Pagination. "most APIs support data pagination to split huge chunks of data into smaller set of data" (§5.1.2): limit is half of it, a page number or a cursor for the next chunk the other half.
  • Rate limits. Providers "put a limit on the number of requests a single account can make" (§5.1.2), so a large crawler spreads its requests over several accounts, in round robin.
  • Status codes. 200 OK (p. 1512) is success; the others you will meet first are 401 and 403 (no or wrong key, forbidden), 404 (not found), 429 (too many requests: the rate limit) and 5xx (the server failed).

No API? Scrape the page — and meet the DOM

An API hands you records with named fields; a web page hands you text in tags, laid out for human eyes. When a site has no API, you take the page itself. The scraping deck opens with the definition (p. 1523):

Web scraping — "the automated process of extracting data from websites and organizing it into a structured format, such as a spreadsheet or database. It uses specialized software or scripts, called scrapers, to pull information like text, prices, or images from the HTML code of web pages, which can then be used for analysis."

The slide sets scraping apart from web crawling, "which is used by search engines to index web content": a crawler moves from page to page along links, a scraper extracts data from them — and Scrapy, below, does both.

What can be scraped (p. 1527)? "Social networks: Facebook, Instagram, etc.", "Retailers: Amazon, Tripadvisor, Google Maps, etc.", and "Any website with valuable contents and no official API (e.g. events, train schedules, etc.)". The last item is the rule — scrape when there is no API; "a last resort", say the course notes (§5.1.3). Train schedules are on the list, yet the first deck reached them through an API: when one exists, it is the better door.

The page is a tree

What is a page to a program? The slides answer with the W3C definition (p. 1524):

Document Object Model (DOM) — "a platform and language-neutral interface that allows programs and scripts to dynamically access and update the content, structure, and style of a document." "In other words it is a standard for how to get, change, add, or delete HTML elements."

"When a web page is loaded, the browser creates a Document Object Model of the page" (p. 1525), which defines "The HTML elements as objects", "The properties of all HTML elements", "The methods to access all HTML elements" and "The events for all HTML elements". The third DOM slide is a picture (p. 1526), redrawn in Fig. 3; this is the HTML behind it (the slide names the attribute, not its value):

<html>
  <head>
    <title>My title</title>
  </head>
  <body>
    <a href="page2.html">My link</a>
    <h1>My header</h1>
  </body>
</html>
Document Root element: <html> Element: <head> Element: <body> Element: <title> Attribute: "href" Element: <a> Element: <h1> Text: "My title" Text: "My link" Text: "My header" not a child
Fig. 3. The DOM tree of p. 1526. Every tag becomes an element node; every run of text becomes a text node, a leaf (blue: the data a scraper is usually after). The attribute href hangs beside its element <a> on a horizontal line — drawn dashed here: it belongs to the element but is not one of its children.

Why a scraper cares: a parser such as BeautifulSoup builds this same tree from the HTML text; the patterns that pick data out of a page are paths in it; and the page's own scripts can change it after loading — "dynamically access and update", the subject of the next section.

Pointing at a node: three kinds of pattern

Once the page is in hand, "Another basic step is parsing the page, extracting data that respects specific patterns" (p. 1530): CSS selectors, regular expressions and XPath. All three reach the link of Fig. 3 (background: the slides name the techniques, not the calls):

# html holds the page of Fig. 3 as text
import re
from bs4 import BeautifulSoup
from lxml import html as lh

# 1. CSS selector, with BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
link = soup.select_one("body > a")
link.get_text()     # 'My link'
link["href"]        # 'page2.html'

# 2. XPath, with lxml
lh.fromstring(html).xpath("//a/@href")
# ['page2.html']

# 3. regular expression, on the raw text
re.findall(r'<a href="([^"]*)">', html)
# ['page2.html']

CSS selectors and XPath walk the tree: body > a is "an a that is a child of body", //a/@href "the href attribute of any a" — reached with @ or ["href"], not one level down (Fig. 3). A regular expression reads the raw text, blind to nesting, and works only while the HTML is written exactly as the pattern expects. Hence p. 1529: "Since HTML code is tagged text, any tool that allows to manipulate and process text could be potentially used to parse a page" — potentially; a tree-building parser is sturdier.

Scraping rebuilds structure from layout: the DOM is the tree it walks, and a selector is a path in that tree.

Static or dynamic: when downloading the page is not enough

For a static page, scraping takes two steps (p. 1530): "retrieving a static page from the Web" — with "curl, using command line" or "requests, using Python scripts" — then parsing it with the patterns above (the other group's deck lists only XPath and regular expressions, p. 1542). Take a small restaurant page whose reviews you want — Trattoria Rossi, invented for this lesson:

# step 1, from the command line: download the page into a file
curl -s "https://example.com/trattoria-rossi" -o page.html
# steps 1 and 2 in Python: download, then parse
import requests
from bs4 import BeautifulSoup

r = requests.get("https://example.com/trattoria-rossi")
soup = BeautifulSoup(r.text, "html.parser")
# one element per review in the page
reviews = soup.select("div#reviews > p.review")

Then comes the sentence on which the second half of the session turns: "Most websites have dynamic-generated content: downloading and parsing the HTML page could not be sufficient" (p. 1529). The browser shows you the DOM after the page's JavaScript has run; requests downloads the text the server sent, before any script ran — it is not a browser and runs nothing. When a script fills the page after loading, the reviews you see are simply not in the download (Fig. 4).

<body> <h1> div#reviews <script> empty never ran 0 reviews to parse

Before: what requests.get downloads

<body> <h1> div#reviews <script> <p> <p> <p> it ran 3 reviews to parse

After: what the browser builds — and Selenium hands over

Fig. 4. One page, two trees. The server sends an empty div#reviews and a script; once the page has loaded, the script fetches the reviews and inserts three <p class="review"> nodes (blue). requests stops at the first tree, a browser reaches the second, and BeautifulSoup parses whichever tree it is given.

"A dynamic page is one generated by a program on the server." Not for a scraper: a page the server writes before sending it is static from the scraper's point of view — the content is in the download. The trouble is content built in the browser, by JavaScript, after loading. The deck says "dynamic" in three senses: "dynamic-generated content" (p. 1529), content loaded on click or scroll (p. 1531), and "Dynamic Websites" whose HTML "changes over time" (p. 1537). And "could not be sufficient" means "may not be sufficient".

The next slide asks the two questions that a plain download cannot answer (p. 1531): "What happens if we want to obtain pages in sequence (e.g.: pages of reviews from Amazon or TripAdvisor)?" and "What we need to do if the page loads the content we need only after clicking a specific field or while scrolling the page itself (e.g.: Facebook and Instagram posts)?"

Three modules, three jobs

"Python is one of the best languages to build scrapers: it is easy to learn, fast to code and it has plenty of libraries to get anything done efficiently" (p. 1532). The deck presents three of them.

The three modules of pp. 1533–1535. The last column is background, not on the slides.
ModuleThe slide's wordsIts job in a scraperRuns the page's JavaScript?
BeautifulSoup (p. 1533)"an HTML (and XML) parser"; it lets you "navigate through an HTML document, accessing each tag and hierarchy easily"; "the main source to extract data after the page is fully loaded"parse: turn HTML text into a tree and pick data out of it; it downloads nothingno
Scrapy (p. 1534)"a Python framework for web crawling and data extraction", for "web scrapers and spiders"; "it bith [both] crawls a website and extracts data"fetch many pages by following links, and extract from themno, not by itself
Selenium (p. 1535)"developed for automating websites testing"; it "simulates clicks, drags, scrolls"; "used to retrieve data that is not loaded with the source page (Javascript) or to navigate several pages of the site"drive a real browser: load, click, scroll, then hand over the rendered pageyes — the browser runs it

Selenium's origin explains why it answers both questions of p. 1531: a testing tool must do what a user does, so it drives a real browser — the browser runs the scripts, Selenium clicks "next" or scrolls, and the DOM it reads at the end is the rendered one. The handover to BeautifulSoup is one line, driver.page_source:

# the dynamic page: a real browser loads it, BeautifulSoup parses
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup

# start a real browser and open the page
driver = webdriver.Chrome()
driver.get("https://example.com/trattoria-rossi")
# wait (at most 10 s) until the script has added a review
WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "p.review")))
# hand the rendered page to the parser
soup = BeautifulSoup(driver.page_source, "html.parser")
driver.quit()
reviews = soup.select("div#reviews > p.review")

Static or dynamic?

The Trattoria Rossi page in three versions, and two scrapers. In the static version the server writes the reviews into the HTML; in the dynamic versions it sends an empty div#reviews and a script that fetches the reviews after the page has loaded — in the last one, three more each time the page is scrolled. Choose a page and a scraper and follow each step: what was sent, what ran, and what BeautifulSoup is finally handed.

  1. The one insight to take away: a scraper parses only what it is handed — requests hands over the HTML before any JavaScript runs, Selenium the page after the browser has run it. Static page, requests: predict how many reviews soup.select returns.
    What you should see

    3 of 3, with a single HTTP request. The reviews are in the text the server sends, so the two steps of p. 1530 — retrieve, parse — are enough.

  2. Switch to the dynamic page and keep requests. A visitor's browser shows three reviews. Predict what the scraper finds.
    What you should see

    0 of 3. The download contains an empty div#reviews; reviews.js is never run, so the call to /api/reviews that would fetch the reviews is never sent. Nothing failed — the reviews were never in what requests received.

  3. Now switch the scraper to Selenium. What changes, and how many HTTP requests does the scrape now cost?
    What you should see

    3 of 3. The browser loads the page, runs reviews.js, the script asks the site's own API for the reviews (JSON) and inserts them; page_source is that page, and BeautifulSoup finds them. It costs 3 requests instead of 1: the page, the script and the API call. Notice that the reviews travelled as JSON from an API — the first half of the lesson, hiding inside the second.

  4. Infinite-scroll page with Selenium: how many scrolls collect all 9 reviews? And what does requests find on this page?
    What you should see

    Two scrolls: 3 at load, then 6, then 9. A third scroll asks for a fourth page and gets an empty list. requests finds 0, and it cannot scroll at all: it has no page, only text. Every scroll is also one more interaction that a site can notice — "repeated scrolling" is the slide's own example of the "weird behaviours" that protections detect (p. 1528).

The pipeline, and why scrapers break

"The image below represents the interactions between the three presented modules: their simple combination is the basis for a web scraper" (p. 1536). Fig. 5 redraws it, with the two return loops of the other group's version (p. 1545).

Internet Scraper Selenium Scrapy </> Beautiful Soup Parsed data get dynamic elements get another page
Fig. 5. The high-level pipeline of p. 1536, with the two loops of p. 1545 (dashed). Blue arrows carry data: the page travels from the Internet through Selenium or Scrapy to the HTML page (</>), from there to Beautiful Soup, and out as parsed data. The orange loops are new requests: back to the browser for dynamic elements, back to the web for another page.

Read it as fetch, parse, store: Selenium fetches when the page needs a browser, Scrapy when many pages must be crawled, and BeautifulSoup turns the HTML page into parsed data. The loops answer the two questions of p. 1531 by name: "get dynamic elements" (click or scroll, then read again) and "get another page". Two simplifications: the static path of p. 1530 (requests, then BeautifulSoup) is not drawn, and Scrapy's page goes through BeautifulSoup although Scrapy can extract by itself.

Fragile by design

An API is a contract with named fields; a page is a layout that its owner may redesign any night. "Most of the times, the HTML changes over time, especially the classes and sometimes the structure of the HTML. Therefore, it is really important to keep the scraper updated" (p. 1537, "Dynamic Websites" — the third sense of the word). Rename the class in div#reviews > p.review and the scraper returns an empty list without any error, which is why the course notes call scrapers very difficult to maintain (§5.1.3).

Sites also defend themselves: "Some websites have some protections against scraping. They are able to detect weird behaviours (e.g., repeated scrolling) and prevent you from scraping" (p. 1528). And Selenium needs a "chromedriver", the program through which it drives the browser, so the two versions must match: "Check out your Google Chrome/EDGE version", then "Download and put the correct chromedriver version within the folder" (p. 1538). Since version 4.6, Selenium fetches the matching driver itself (background).

The slides list protections but no rules; the course notes warn that "many website providers don't allow scraping, so you could run into legal problems if you do" (§5.1.3). Good practice: read the terms of use and the site's robots.txt, keep requests slow, identify your client honestly, and handle personal data as privacy law (in the EU, the GDPR) requires.

Who wrote the URL of the warm-up

Back to the warm-up. Its decoded tail is exactly the text JavaScript produces for a date — try new Date().toString() in the console of a browser that runs in English on Italian time — and a %20 for every space, with : and + left bare, is exactly what JavaScript's encodeURI makes of it. So a program running JavaScript wrote this URL: most likely the ViaggiaTreno web page itself, asking its server for the departures after it had loaded. The session shows the call, not how it was found; this reading is ours.

If this reading is right, it is the dynamic page of p. 1529 seen from inside: a board filled this way reaches the browser as an empty frame, and requests.get receives only the frame. Two ways out: drive a browser with Selenium and parse what it renders, or call the API that the page itself calls, as the session did. When you can see the call, the second is simpler and sturdier — named fields instead of a layout, and nothing breaks when a CSS class is renamed.

Prefer an API when one exists; when you scrape, fetch with the simplest tool that sees the data, parse with BeautifulSoup, and expect to maintain the scraper.

Exam-style questions

APIs and scraping are not among the topics of the practical part (Neo4j, MongoDB, Elasticsearch, Redis and SparkQL), and none of the nine past papers has such an exercise. The theory part covers "Content explained during the theoretical and practical lectures" (Exam Rules 2026/27), and this session is a practical lecture: expect closed questions (multiple choice, true or false — wrong answers may carry a penalty, announced just before the exam) and short open answers.

True or false: "As an end-user, the most used requests for APIs are GET requests."

  • True p. 1511, word for word. An end-user mostly consumes data, and GET is the method that reads.
  • False POST, PUT and DELETE create, update and delete; a person collecting data mostly reads, and reading is GET.
Show model answer

True. APIs are invoked through HTTP methods depending on the kind of operation; an end-user mostly retrieves data, and retrieval is a GET request, which can be written as a single URL (as in the iTunes and ViaggiaTreno examples).

The sentence is quoted from the slide, so a true/false item can reuse it as it stands. If it comes back as an open question, add the reason: the method carries the intent of the operation.

Consider the request https://itunes.apple.com/search?term=maroon&entity=allArtist&attribute=allArtistTerm&limit=10, shown in the exercise session on APIs. Identify the scheme, the host, the path and the query parameters, explain what limit=10 does, and describe the structure of the answer.

Show model answer

It is a GET request to a web API. The scheme is https, the host is itunes.apple.com (the server that answers) and the path is /search (the service called). After the question mark comes the query string: four key=value pairs separated by ampersands — term=maroon, the text searched for; entity=allArtist, the kind of object returned (artists); attribute=allArtistTerm, the field the term is matched against (the artist's name); limit=10, the maximum number of results. The answer is a JSON object with resultCount, here 10 because of the limit, and results, an array of artist objects with named fields such as artistName, artistId and primaryGenreName — the first two are Maroon 5 and Maroon. Records of the same answer may have different fields. Special characters inside a value must be percent-encoded, for example %20 for a space.

Name all four parts of the URL with their terms, then show that you understand each parameter as an instruction to the server. Saying that the answer is "a web page" loses the point of an API: it returns data, JSON, for programs.

In the Trenitalia ViaggiaTreno example, the autocompletion for train 798 returns the lines 798 - SALERNO - 04/11/25|798-S09818-1762210800000 and 798 - SALERNO - 05/11/25|798-S09818-1762297200000. Explain the structure of these lines, what the number 1762210800000 represents, why the two numbers differ by 86400000, and how the result is used in the next request.

Show model answer

Left of the vertical bar is a label for people: train number, origin station and date. Right of it is a key for programs: train number, station code (S09818, Salerno) and departure day, separated by dashes. The day is written as epoch time in milliseconds, the number of milliseconds since 1 January 1970, 00:00 UTC: 1762210800000 is the midnight of 4 November 2025 in Italian time (23:00 UTC on 3 November). One day lasts 24 × 3600 × 1000 = 86 400 000 ms, so the second key is the next day, 5 November. The parts of the key are then placed in the path of the second call, andamentoTreno/S09818/798/1762210800000 (station, train, day), which returns the status of that run of the train in JSON — for example category ICN, duration 14:35, arrived. It is a two-step workflow in which the first call provides the identifiers needed by the second, and the parameters travel in the path rather than in a query string.

The three things to get right: the number is a day, not a time of departure; the unit is milliseconds, not seconds; and the key feeds the next call. The slides print the numbers without explaining them — the derivation in this lesson is ours, and it checks digit for digit.

What is web scraping? How does it differ from web crawling, and when is scraping the appropriate way to collect data?

Show model answer

Web scraping is the automated process of extracting data from websites and organizing it into a structured format, such as a spreadsheet or a database. Programs called scrapers pull information such as text, prices or images from the HTML code of web pages, for later analysis. Web crawling, used by search engines to index web content, is about visiting pages by following links; scraping is about extracting data from them. The two can be combined: Scrapy both crawls a website and extracts data. Scraping is appropriate for websites with valuable content and no official API — social networks, retailers, events, train schedules — and it should be a last resort: when an API exists it provides structured, documented data, while scrapers break when the HTML changes and websites may detect and block them.

The definition of p. 1523 is the core; the contrast with crawling is the classic follow-up. The "last resort" argument (maintenance, protections) shows that you know why an API is preferred.

What is the Document Object Model (DOM), what does it define, and why does it matter for web scraping?

Show model answer

The W3C DOM is a platform- and language-neutral interface that allows programs and scripts to dynamically access and update the content, structure and style of a document — a standard for how to get, change, add or delete HTML elements. When a web page is loaded, the browser creates the DOM of the page. It defines the HTML elements as objects, the properties of all HTML elements, the methods to access them and their events. It is a tree: the document, the root element html, the elements head and body, their child elements, and text nodes as leaves; attributes such as href are attached to their element. For scraping it matters because parsers such as BeautifulSoup build the same tree from the HTML, and selectors (CSS selectors, XPath) are paths in it; and because scripts can update the DOM after loading, the HTML that is downloaded can differ from what the browser shows.

Quote the definition, list the four things the DOM defines (p. 1525), and add the link to scraping — the last sentence is what turns a memorised definition into understanding.

Why may downloading and parsing the HTML of a page not be sufficient to extract its content? Which tool does the exercise session propose, and why that tool?

Show model answer

Most websites have dynamically generated content: the HTML returned by the server (and downloaded with curl or requests) is the page before its JavaScript runs, and the content may be loaded by scripts only after the page has loaded, after a click on a specific field, or while scrolling (for example Facebook and Instagram posts); content may also be split over pages in sequence (pages of reviews on Amazon or TripAdvisor). A plain download does not contain that content and cannot click or scroll. The session proposes Selenium, a module developed for automating website testing: it drives a real browser and simulates clicks, drags, scrolls and any other interaction, so it retrieves data that is not loaded with the source page and navigates several pages. The rendered page is then passed to BeautifulSoup, the parser that extracts the data after the page is fully loaded.

The decisive word is "browser": Selenium works because the browser runs the scripts. An answer that says only "use Selenium" without saying why the download is empty misses the mechanism the examiner is checking.

Which of the following statements about the scraping tools presented in the exercise session are correct? Select all that apply.

  • BeautifulSoup is an HTML (and XML) parser, used to extract data after the page is fully loaded; it does not download pages itself. p. 1533: it navigates a document someone else has fetched.
  • Selenium was developed to parse HTML documents faster than BeautifulSoup. Selenium was developed for automating website testing: it simulates clicks, drags and scrolls in a browser (p. 1535). Parsing is BeautifulSoup's job.
  • Scrapy is a Python framework that both crawls a website and extracts data. p. 1534: a framework for web crawling and data extraction, for building scrapers and spiders.
  • curl and requests extract data from a page by means of CSS selectors. They retrieve the page (p. 1530); extracting with CSS selectors, regular expressions or XPath is the separate parsing step.
  • Selenium is used to retrieve data that is not loaded with the source page, or to navigate several pages of a site. p. 1535, almost word for word.
  • Once a scraper works, it keeps working as long as the website is online. The HTML changes over time, especially the classes, so the scraper must be kept updated (p. 1537); sites may also detect and block it (p. 1528).
Show model answer

The first, third and fifth statements are correct. In the pipeline of the session, Selenium or Scrapy obtains the HTML page from the Internet, BeautifulSoup parses it, and the result is the parsed data.

Two wrong options swap the roles of two tools or of two steps — browser automation versus parsing, retrieve versus parse; the last one forgets that the HTML changes and that sites detect scrapers. Wrong answers may carry a penalty, so tick only what you can justify with a slide.

Check yourself

In the request of p. 1513 you change limit=10 into limit=3 and nothing else. What comes back?

  • A resultCount of 10, but only the third artist in results A limit is not an index: it caps how many results come back, it does not pick one of them.
  • A resultCount of 3 and three artist objects in results Each pair is one instruction; this one caps the number of results, and resultCount counts what came back.
  • A resultCount of 3, with all ten objects still in results resultCount counts the objects in results; the two always agree.
  • An error: the service expects exactly 10 The 10 of the slide is just the value the session chose; any value up to the service's maximum is a valid request.

The call of p. 1514 asks for entity=movieArtist, yet its first result, Frank Stallone, has "artistType": "Artist" and genre Pop, and it has an amgArtistId that Sylvester Stallone lacks. What is the right conclusion?

  • The answer is corrupted and should be discarded It is valid JSON; records of one answer are simply allowed to differ.
  • The API filters by surname, as the slide's caption says There is no surname key: term is a free-text search on names. The caption states an intention, not the mechanism.
  • A request shapes the search, but what you get is what the answer contains: read the records, and check that a field exists before you use it The screenshot itself shows both facts: a music artist among the results, and a field present in one record and absent in the next.
  • The key entity has no effect at all One call cannot show that: Sylvester and Sistine Stallone are movie artists. What it shows is that the parameter did not keep every other artist out.

Which of these pages can you scrape with requests and BeautifulSoup alone? Select all that apply.

  • A page whose reviews a PHP program writes into the HTML on the server, before sending it Generated on the server means already in the download: static, from the scraper's point of view.
  • A page whose posts appear only while you scroll Something has to scroll and run the script that loads the next posts: a browser, driven by Selenium (p. 1531).
  • A page whose script fills an empty div with reviews after loading requests runs no scripts, so the div stays empty in what it receives: the widget's dynamic page.
  • A plain HTML page with a table of train schedules Retrieve, then parse: the two steps of p. 1530 are enough.

The test is not how the page was produced but whether the content is in the text the server sends.

In the DOM tree of p. 1526, the box "Attribute: "href"" is joined to "Element: <a>" by a horizontal line instead of hanging below it. What does that tell you?

  • The attribute belongs to the element but is not one of its child nodes So it is read from the element — @href in XPath, tag["href"] in BeautifulSoup — not found by going down the tree.
  • The attribute is a sibling element of <a> It is not an element at all; the siblings of <a> are elements such as <h1>.
  • The attribute is the parent of <a> The parent of <a> is <body>, above it.
  • The attribute is a text node The text node of <a> is "My link", below it; href is the link's target, not its text.

ViaggiaTreno identifies a day by the epoch milliseconds of its midnight, Italian time. 5 November 2025 is 1762297200000. Which number identifies 6 November 2025?

  • 1762297286400 That adds 86 400 — the seconds of a day, read as milliseconds: 86.4 seconds later.
  • 1762297200001 One millisecond later, still 5 November.
  • 1763161200000 That adds 864 000 000 ms, a zero too many: ten days.
  • 1762383600000 Plus 24 × 3600 × 1000 = 86 400 000 ms, exactly one day.

A scraper ran fine for months; now it returns empty lists, while the page still shows the data in your browser. Which cause does the slide "Dynamic Websites" point to?

  • The server deleted its data Then the browser would not show it either.
  • The site changed its HTML — a class name or the structure — and the selectors no longer match p. 1537: "the HTML changes over time, especially the classes", so the scraper must be kept updated.
  • A new version of the API was released A scraper does not use the API; versions are the risk of API clients, the Twitter story of p. 1518.
  • BeautifulSoup can only parse a page once A parser has no memory of earlier pages; it parses whatever text it is given.

Which of the three modules never downloads anything by itself?

  • Selenium It drives a browser, and the browser loads pages and scripts.
  • Scrapy It crawls: it requests pages and follows their links.
  • BeautifulSoup A parser: it receives HTML from requests, Scrapy or Selenium, "after the page is fully loaded" (p. 1533).
  • None of them: all three download BeautifulSoup only parses the text that another tool fetched.

Key take-aways

What to watch and read