robots.txt readers disagree, and the built-in one is the odd one out
Python's built-in reader takes the first matching rule and ignores wildcards. The standard takes the longest match. We ran both on 12,000 random files and listed where they split.
A robots.txt file looks like a plain list of do and do not. It is not as plain as it looks. Two readers can give opposite answers for the same file and the same URL, and one of them ships inside Python.
The standard is RFC 9309, published in 2022. The built-in reader is urllib.robotparser, which predates it. We put them side by side, on purpose, because a scraper that says "yes" to a URL the site said "no" to is the one mistake that cannot be waved away.
What the standard says#
- Find the group for your agent by its product name, ignoring case. No group, use the
*group. No*group either, everything is allowed. - Inside the group a rule matches when its pattern matches the start of the URL path, query string included.
*stands for any run of characters.$pins the end. - If several rules match, the longest pattern wins. A tie between Allow and Disallow goes to Allow.
- An empty rule, a bare
Disallow:, blocks nothing.
What the built-in reader does#
Read from its source and checked against the real module (Python 3.10.12):
- First match wins, in file order, not longest.
*and$in a rule are plain characters. A rule like/*.pdf$matches no URL at all.- A bare
Disallow:becomes "allow everything", and because it is first-match, a bareDisallow:ahead of other rules cancels them. - A group name matches if it appears inside your agent name. A group called
botcatchesExampleBot. - A blank line ends a group. Rules that follow it, before the next
User-agent:line, are silently dropped. - An empty
?at the end of a rule is dropped, so/search?becomes/search.
Where that splits the answer#
Each row was run through both readers.
| robots.txt rule | URL | Standard | Built-in |
|---|---|---|---|
Disallow: /private/ then Allow: /private/public.html | /private/public.html | allowed | blocked |
Disallow: /*.pdf$ | /files/report.pdf | blocked | allowed |
Disallow: /*?sessionid= | /cart?sessionid=abc | blocked | allowed |
Disallow: then Disallow: /secret/ | /secret/plan.html | blocked | allowed |
Disallow: /page then Allow: /page | /page | allowed | blocked |
User-agent: bot then Disallow: / | agent ExampleBot | allowed | blocked |
| a rule after a blank line inside a group | the rule's path | blocked | allowed |
Three of those lean safe: the built-in reader blocks more than it should. Four lean the other way, and those are the ones that matter. Wildcard rules and Allow carve-outs after a broad Disallow are ordinary things to find in a robots.txt file.
We built 3,000 random files from a small grammar of groups, Allow and Disallow rules with and without wildcards, bare rules, blank lines, comments, Sitemap and Crawl-delay lines, and asked four URL questions of each: 12,000 pairs. We compared a standalone implementation of each reading, the one running in the lab, against the real thing: Python 3.10.12's built-in reader and Protego 0.7.0. No differences for either. The two real readers disagreed with each other on 1,617 of the 12,000 pairs. That is a statement about awkward files built to provoke them, and says nothing about how often real sites trip it. Newer Python versions may behave differently.
What to do about it#
- If you use the built-in reader, know what it will not catch. For plain files made of simple prefix rules it is fine.
- If sites you read use wildcards or carve-outs, use a reader that follows RFC 9309. Protego is one, and the one Scrapy uses by default. Google has also published its own parser.
- Whatever you use, keep a small file of cases, like the table above, and run it in your tests. The week a reader changes its mind, you want to hear about it from the tests.
The snippet in robots.txt first uses the built-in reader because it is short. Now you know what it leaves out. Paste your own file into the lab and see both answers.
The price is a string: reading money off a web page
Shops print the same number a dozen ways. A careful parser reads what is clear, leaves the rest empty and never fills a gap with a plausible guess.
A timestamp without a zone is a guess
Scraped dates arrive as "2026-03-08 02:30" with nothing else. Some of those times never happened, some happened twice, and the gap between two cities moves during the year.
Retry-After is a sentence, not a suggestion
When a site says "slow down", it often says how long to wait. Reading that line, backing off with jitter and knowing when to give up is most of polite scraping.