Repository navigation
crawl: rewrite robots.txt to RFC 9309 - #97
Merged
Merged
Conversation
The old parser matched literal path prefixes, knew two user agent names, and read a 5xx on /robots.txt as permission to crawl. The last one is the one that matters. A site whose robots endpoint is failing cannot tell us to stop, and crawling it anyway is how a crawler ends up in a block list. RFC 9309 section 2.3.1.4 calls an unreachable robots.txt a complete disallow, and so does this now, cached for five minutes so the host is crawled again as soon as it recovers. The rest of the spec is in too. Wildcards, longest match wins with an allow breaking ties, case insensitive product token matching with the most specific group winning and the wildcard group as fallback only, groups naming the same crawler merged, percent encoding normalised on both sides of the comparison, Sitemap lines collected, and the 500 KB parse limit. Status handling had to get more careful than a bool. A 403 means there is no robots.txt to read and the site is open, a dead host means assume everything is off limits, and the HTTP client was turning both into the same error because it retries 403 and 5xx alike. Added getStatus, which hands back the last response instead of an error, so robots can tell the two apart. Crawl-delay is not in RFC 9309 but sites still write it, and parsing it is worthless if nothing enforces it. Frontier.Hold pushes a host's next eligible time out, which is where a ten second Crawl-delay lands: Pop reserves the crawl's own delay when it hands a URL out, because nobody has read robots.txt yet at that point, and the fetcher extends it once it knows. Tested against the RFC's own examples, and against 31 real robots.txt files from live sites cross checked with protego, the parser Scrapy uses: 5949 probe paths, zero disagreements. The 431 probes not compared are ones protego answers wrong, all of them the same bug, where it percent encodes a path and not the pattern so Disallow: /gp/aw/help/id=sss fails to match /gp/aw/help/id=sss. scripts/robots-corpus.py rebuilds the corpus and TestRobotsAgainstRealSites replays it.
7 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #52.
The old parser matched literal path prefixes, knew two user agent names, and read a 5xx on
/robots.txtas permission to crawl. The last one is the one the issue is filed as a bug for. A site whose robots endpoint is failing cannot tell us to stop, and crawling it anyway is how a crawler ends up in a block list. RFC 9309 section 2.3.1.4 calls an unreachable robots.txt a complete disallow, and so does this now, remembered for five minutes so the host is crawled again as soon as it recovers.What is in
*and$wildcards, longest match wins, an allow breaks a tie of equal length*, which is the quxbot case in the RFC's exampleSitemaplines collected and exposed, and they do not interrupt the group they sit inCrawl-delayhonoured, which is not in RFC 9309 but is still what sites writeStatus handling had to get more careful than a bool
A 403 means there is no robots.txt to read and the site is open. A dead host means assume everything is off limits. The HTTP client was collapsing both into the same error, because it retries 403 and 5xx alike and reports the exhausted retry as a plain error.
getStatushands back the last response instead, so robots can tell a refusal from a silence.Crawl-delay needed somewhere to land
Parsing a delay is worthless if nothing enforces it.
Frontier.Holdpushes a host's next eligible time out and never brings it forward. Pop reserves the crawl's own delay when it hands a URL out, because at that moment nobody has read the host's robots.txt yet, and the fetcher extends it once it knows.TestCrawlDelayHoldsTheHostInTheFrontierpops a URL, applies a parsedCrawl-delay: 10, and checks the same host stays shut at 1, 5 and 9 seconds and opens at 10.Done when
TestFetchRobotsDisallowsWhenUnreachablecovers 500, 502 and 503, andTestFetchRobotsDisallowsWhenHostIsDowncovers nothing listening at all.Crawl-delay: 10produces at least 10 seconds between requests to that host.Real data
Hand written cases prove the parser matches the RFC as I read it, which is not the same as being right.
scripts/robots-corpus.pyfetches robots.txt from 31 live sites, derives probe paths from each file's own rules, and records what protego, the parser Scrapy uses, concludes about every one.TestRobotsAgainstRealSitesreplays that corpus: 5949 probes, zero disagreements, and the crawl delay and sitemap count match on all 31 files.431 probes are not compared, and they are worth spelling out rather than hiding. Every one of them is protego getting it wrong the same way: it percent encodes the URL and not the pattern, so
Disallow: /gp/aw/help/id=sssdoes not match/gp/aw/help/id=sss, and the same for a%3Dspelling of it. Two smaller variants of the same habit, a trailing?that urlparse drops and a leading//it reads as a host, account for the rest. The filter lives in the corpus script next to the tool it is about, with the reasoning written down.Live check through the CLI, not just tests:
Not in this PR
Nothing enforces robots on a crawl yet, because nothing drives a crawl yet. Wiring the cache and the delay into a loop is #54.