Skip to content

Get the part that's behind a click

The companion page, Turn one page into a catalog, works because a Wikipedia table is already in the HTML. Plenty of pages are not like that. A course schedule, a seat map, a ticket queue, an admin panel’s “show details” — the summary is server-rendered and the rows you actually want appear only after a click.

For those, ask for a deep read. The difference is not subtle, and it does not look the way you would guess.

The cheap path returns more text and none of the data

Section titled “The cheap path returns more text and none of the data”

University of Maryland’s Schedule of Classes lists courses in the HTML and hides every section — meeting times, instructor, seats — behind Show Sections. Measured on 2026-08-06, same URL, same hour:

soc/202501/…fast pathdeep read
ENGL162,680 chars, 0 Seats106,665 chars, 484 Seats
CMSC117,977 chars, 0 Seats73,428 chars, 174 Seats
HIST55,798 chars, 92 Seats

Read that top row again: the fast path returned 56,000 more characters and not one seat count. If you had measured your extraction by “did we get a lot of text”, it would have looked like the better result.

Length is not coverage. The two paths do not differ in how much they return; they return different documents.

That cuts both ways, and links are where you will notice it. A deep read keeps the links inside the prose it selects — footnote markers, inline references, links in table cells — and drops the navigation and card grids the fast path hands back with everything else. On /wiki/Web_scraping that is 346 links from the fast path and 288 from a deep read: the same article, minus the chrome. On a marketing page whose links are all cards rather than prose, a deep read can return none at all. If your parser needs every outbound link on a page, read it shallow; if it needs the links in the text, read it deep.

Terminal window
curl -X POST https://api.marrow.navii.online/v1/scrape \
-H "Authorization: Bearer $MARROW_API_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://app.testudo.umd.edu/soc/202501/ENGL",
"formats":["markdown"],
"depth":"deep",
"maxAge":0}'

Two parameters carry the weight:

  • depth: "deep" — render the page in a real browser, open what can be opened, then extract. Without it you get the server’s HTML.
  • maxAge: 0skip the caches and capture live. Leave it out and a stored capture may answer instead; see below, because this one has teeth.

Cost is 1 credit, the same as any scrape — the browser is not billed extra. Time is not the same: 13–19 seconds against 0.4–5 for the fast path. Budget for it.

Verify by what you needed, not by what you got

Section titled “Verify by what you needed, not by what you got”

A deep read is not byte-stable. The browser scrolls before it starts expanding, and how much it scrolls decides how many expanders fall inside the viewport — so two runs of the same page can open a different number of sections. We measured one department returning 17 seat counts on one run and 45 on another.

So do not assert on sizes:

body = read(url, deep=True)
seats = body.count("Seats")
if seats < expected_courses: # you know roughly how many rows should exist
raise SystemExit(f"partial capture: {seats} sections, expected ~{expected_courses}")

This matters because the API does not tell you the expansion was partial. The worker knows — it counts the expanders it could not open — but that count does not ride in the response today. Until it does, the check above is yours to write, and it is the difference between “we got a page” and “we got the page”.

This is someone’s university server, and one department page carries dozens of expanders — a deep read is dozens of clicks and a full render. Go serially, keep N small, put a few seconds between requests, and cache what you got (that is what the 7-day default is for, once you are past development). Nothing here needs to be fast; it needs to run once a term.

the data is…ask for
Wikipedia-style tablesalready in the HTMLa plain read — cheap, seconds
This pagebehind an expanderdepth:"deep" — 13–19 s, still 1 credit

The tell is quick: fetch it once without depth, then search the body for a string only the hidden part would contain (Seats, Instructor, a price, a status). Zero hits on a page that visibly shows them in a browser means the data is behind a click.