Skip to main content

Fast, regex-free crawler detection from user agents. Zero deps, ReDoS-safe heuristics, ~40× faster than alternatives.

Project description

is-crawler

Fast, regex-free crawler detection from user agents. Zero deps, ReDoS-safe heuristics, ~40× faster than alternatives.

PyPI Python License Stars Downloads

Docs & live demo: is-crawler.tn3w.dev

Why regex-free?

Regex is a frequent source of ReDoS vulnerabilities, one un-anchored .* or nested quantifier against a hostile UA can spike CPU to seconds. Crawler detection runs on every request, so a catastrophic pattern is a denial-of-service primitive. is-crawler implements all heuristics with str.find + char scans. No regex engine, no backtracking, no ReDoS surface. crawler_info uses re only to match against curated DB patterns (monperrus/crawler-user-agents) which are simple literals (e.g. Googlebot\/, bingbot, AdsBot-Google([^-]|$), [wW]get), no nested quantifiers, no catastrophic backtracking paths.

Install

pip install is-crawler

Usage

from is_crawler import (
    is_crawler, crawler_signals, crawler_info, crawler_has_tag,
    crawler_name, crawler_version, crawler_url, CrawlerInfo,
)

ua = "Googlebot/2.1 (+http://www.google.com/bot.html)"

is_crawler(ua)                              # True
crawler_signals(ua)                         # ['bot_signal', 'no_browser_signature', 'url_in_ua']
crawler_name(ua)                            # 'Googlebot'
crawler_version(ua)                         # '2.1'
crawler_url(ua)                             # 'http://www.google.com/bot.html'

info = crawler_info(ua)                     # CrawlerInfo(...)
if info is not None:
    info.url                                # 'http://www.google.com/bot.html'
    info.description                        # "Google's main web crawling bot..."
    info.tags                               # ('search-engine',)

crawler_has_tag(ua, "search-engine")        # True
crawler_has_tag(ua, ["ai-crawler", "seo"])  # False

API

is_crawler(ua: str) -> bool

Heuristic detection. Returns True if the UA is a crawler. No DB lookup, no regex.

Three short-circuit rules:

  1. Positive signal: bot keywords (bot, crawl, spider, scrape, headless, slurp, archiv, preview, ...), known tools (playwright, selenium, wget, lighthouse, sqlmap, nikto, nmap, httrack, pingdom, google-safety, ...), or a URL/email embedded in the UA.
  2. No browser signature: missing Mozilla/, WebKit, Gecko, Trident, Presto, KHTML, Links, Lynx, Opera, or an OS token like (Windows, (Linux, (X11, (Macintosh.
  3. Bare (compatible; ...): classic bot block without OS/browser tokens inside.

crawler_signals(ua: str) -> list[str]

Which individual rules fired. Subset of: bot_signal, no_browser_signature, bare_compatible, known_tool, url_in_ua. Useful for diagnostics and logging. is_crawler does not call this.

crawler_name(ua: str) -> str | None

Product name extracted from the UA.

  • Googlebot/2.1 ...'Googlebot'
  • Mozilla/5.0 (compatible; bingbot/2.0; ...)'bingbot'
  • Mozilla/5.0 ... Speedy Spider (...)'Speedy Spider'
  • Chrome/Firefox/Safari → None

crawler_version(ua: str) -> str | None

Version token extracted from the UA. Returns None if no non-browser version is detectable.

  • curl/7.64.1'7.64.1'
  • Mozilla/5.0 (compatible; Miniflux/2.0.10; ...)'2.0.10'
  • Googlebot/2.1 ...'2.1'

crawler_url(ua: str) -> str | None

URL embedded in the UA (after +, ;, or -).

  • Googlebot/2.1 (+http://www.google.com/bot.html)'http://www.google.com/bot.html'
  • UA with no embedded URL → None

crawler_info(ua: str) -> CrawlerInfo | None

DB lookup against 646 known crawler patterns. Returns None for browsers (short-circuits via is_crawler).

class CrawlerInfo(NamedTuple):
    url: str                # crawler's info/docs URL (may be '')
    description: str        # human-readable description
    tags: tuple[str, ...]   # classification tags, e.g. ('search-engine',)

crawler_has_tag(ua: str, tags: str | Iterable[str]) -> bool

True if the crawler has any of the given tags. tags accepts a single string or a list.

Available tags: search-engine, ai-crawler, seo, social-preview, advertising, archiver, feed-reader, monitoring, scanner, academic, http-library, browser-automation.

Middleware

from is_crawler import is_crawler, crawler_has_tag

@app.before_request
def gate():
    ua = request.headers.get("User-Agent", "")
    if crawler_has_tag(ua, "ai-crawler"):
        abort(403)
    if is_crawler(ua):
        log_crawler(ua)

Caching

Every public function has a 32k-entry LRU cache. Repeat UAs hit in ~40 ns.

Benchmarks

Python 3.14, Linux x86_64. Corpus: 1,231 crawler UAs, 15,812 browser UAs. cua = crawler-user-agents v1.44.

Hot-path (warm cache)

Function is_crawler cua speedup
is_crawler (mixed) 0.05 µs 158.9 µs 3000×
crawler_info 0.60 µs 732.0 µs 1220×
crawler_signals 1.13 µs - -
crawler_name 0.33 µs - -
crawler_version 0.32 µs - -
crawler_url 0.09 µs - -
crawler_has_tag 0.10 µs - -

Cold-cache (per-call, no LRU hits)

Function is_crawler cua speedup
is_crawler 4.67 µs 157.5 µs 34×
crawler_info 2.07 µs 733.4 µs 354×
crawler_name 1.36 µs - -
crawler_version 1.37 µs - -
crawler_url 0.29 µs - -

Cold-start

Module Cold-start
is_crawler 1.29 ms
crawleruseragents 0.80 ms

DB patterns compile lazily per 48-entry chunk on first match.

Formatting

pip install black isort
isort . && black .
npx prtfm

License

Apache-2.0

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

is_crawler-1.4.0.tar.gz (35.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

is_crawler-1.4.0-py3-none-any.whl (29.0 kB view details)

Uploaded Python 3

File details

Details for the file is_crawler-1.4.0.tar.gz.

File metadata

  • Download URL: is_crawler-1.4.0.tar.gz
  • Upload date:
  • Size: 35.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for is_crawler-1.4.0.tar.gz
Algorithm Hash digest
SHA256 bdae61360cebfc2ca9d92e82cc32c8fd008ef2c5646f149bbb93cd69e5c8a4cb
MD5 724d0d9c22f1c83bc80dfc0c5fdcc326
BLAKE2b-256 b377cc0c13a39850c108a15e3402bfb8c5bba76a08097ce0da59bbe93f411d1b

See more details on using hashes here.

Provenance

The following attestation bundles were made for is_crawler-1.4.0.tar.gz:

Publisher: publish.yml on tn3w/is-crawler

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file is_crawler-1.4.0-py3-none-any.whl.

File metadata

  • Download URL: is_crawler-1.4.0-py3-none-any.whl
  • Upload date:
  • Size: 29.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for is_crawler-1.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5eee213c43ff6543cc98a497b6b9bf4b8144f1ef012e8e47d5c4a688155f4ccc
MD5 f84c41a5115084b53d721b8b50ca6928
BLAKE2b-256 7d8df9cbc3a67a55c6625c8e7503382a91bb1e8ed16c707f15c857919bde7256

See more details on using hashes here.

Provenance

The following attestation bundles were made for is_crawler-1.4.0-py3-none-any.whl:

Publisher: publish.yml on tn3w/is-crawler

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page