Four out of thirty
What you will learn
- The failure with no error message: tidy, believable data that is simply incomplete
- Why a lot of pages do not contain what you see on them
- How to count the same thing three independent ways before you believe the number
I wrote something to read a shop's catalogue and build a product grid from it. It ran, it produced clean output, nothing went wrong, and it found four products. The shop has thirty.
The code was not broken. It was pointed at the wrong page, and the wrong page looked exactly like the right one.
Why the page did not contain the page
The site is the modern kind. The shop's computer sends a shell and a small featured selection, and the rest arrives afterwards when the page runs its own code inside your browser. Asking for the address directly gives you only what was sent, which was four items. A browser would then have filled in the other twenty six. My program never ran that step, because fetching an address is not the same as being a browser, so it took the four and reported success.
Photographing a shop window is not an inventory. The window is a selection somebody made this morning, and the stockroom is not in the picture.
The failure mode is the thing to carry away
The specific cause is interesting, but what matters is the shape: a result that is plausible, well formed, consistent with itself, and simply incomplete. There is nothing to catch. No error appears. The only thing that exposes it is knowing, independently, roughly how many things there should be.
Where the catalogue really lived
The paginated listing turned out to be a dead end, because its page control was built in a way the shop's computer never honours, so every variation of the address returned the same first batch.
What did work were the category pages. Each of those was sent complete. The combination of all of them, with duplicates removed, was the catalogue.
Then I did the thing I should always do and had not. I counted it a second, independent way, and then a third. The listing's own page control implied a total. The site's own machine readable index implied another. When all three agreed, I believed the number. They did agree, and they disagreed with my first answer by twenty three products.
The window is a selection somebody made this morning, and the stockroom is not in the picture.
How to find where a page's data really comes from, combine partial sources into a whole, remove duplicates safely, and check a count before trusting it. Everything here only reads. This is about reading public pages you are entitled to read, politely.
1. Work out the expected count from outside your own program
What this does: gives you a number to compare against, from a source that is not the thing you are testing.
Why it is first: without an independent expectation you have no way to notice a believable shortfall, which is the whole failure mode.
# acceptable sources for "how many should there be"
# a management screen you own
# a count the site states itself
# the site's machine readable index
# write the number down BEFORE you look at your own output
2. Compare what was sent with what a browser shows
What this does: counts the products in what the shop's computer actually sent, and compares that with what you can see in a real browser on the same page.
What a gap means: the rest is being filled in by the page itself, so this page is not your data source. Stop treating it as one and go looking for where the data comes from.
# what was actually sent
curl -s '<listing-address>' > sent.html
grep -c '<the-marker-for-one-product>' sent.html
# then count what you see in a real browser on the same page
3. Find the real source in the browser's own tools, not by guessing
What this does: watches every request the page makes while it loads, which is where the real data is.
What you are looking for, in order of usefulness: a single request that returns all of the items; a block of data hidden inside the page itself; or per category pages that are sent complete.
# open the browser's developer tools, reload, watch the requests
# and check the obvious indexes while you are there
curl -s '<site>/sitemap.xml' | grep -c '<loc>'
4. Test the page control properly before trusting it
What this does: asks for page two and page three in the ways the address allows, and compares what comes back.
Why a page control proves nothing: buttons drawn on the page do not mean the shop's computer accepts a page instruction. Mine ignored it, which is why my first attempt stopped at one batch and looked finished.
Compare the actual items, not the counts. The same number of items with the same identities means the instruction is being ignored.
# do these return DIFFERENT items, or the same batch every time?
curl -s '<listing>?page=2' | grep -c '<product-marker>'
curl -s '<listing>?offset=24' | grep -c '<product-marker>'
5. Combine the partial sources, removing duplicates on a real identifier
What this does: merges everything you found, keyed on each item's own identifier so duplicates collapse.
Why the identifier and not the name: names repeat and identifiers do not.
Keep the running total. It shows which source contributed what, which is how you notice later when one of them stops working.
found = {}
for source in [listing] + category_pages:
for item in parse(fetch(source)):
found[item.id] = item # keyed on id, so dupes collapse
# log the walk
# /listing 24 items, 24 new, running 24
# /category/one 1 item, 0 new, running 24
# /category/two 21 items, 2 new, running 26
6. Check the count two independent ways, then a third
What this does: counts the same thing by methods that would fail for different reasons.
Why agreement is the point: three methods that could each fail differently, all agreeing, is what justifies belief.
And if they disagree: investigate. Do not average them.
# A. everything you combined, duplicates removed
# B. what the page control implies (batches x items per batch)
# C. the machine readable index, plus anything it is missing
# all three agree -> trust it
7. Put a guard on the count, and make it refuse
What this does: refuses to overwrite good output with a smaller result, unless you explicitly say so.
Why: a later run that finds fewer items is far more likely to be a broken program than a real removal.
Then test the guard by faking a shortfall, so you have actually watched it refuse.
if new_count < stored_count and not override_requested:
refuse() # keep the good data, fail, and print both numbers
Then be honest about what is left. Mine settles at a number lower than the shop's own total, because some products are not published and a public page genuinely cannot show them. A gap you have explained is fine. A gap you have not looked at is the bug.
Category pages and hidden data blocks are the inner workings of somebody else's site, and they change without warning. Expect this to break, make it break loudly, and check the count rather than whether the program finished.
Related reading: the price a program reports is not always the price is the same distrust pointed at a single field instead of a whole page.
This page in the original Kyber Cypher voice: Four of thirty