Skip to main content

curl_reap

Reap the web. Browser-grade TLS impersonation, self-healing selectors, one-call structured extraction, and a concurrent crawl engine, in one small library.

pip install curl_reap

Documentation  ·  PyPI  ·  Source

PyPI version Python versions MIT license Docs


Sponsors

NodeMaven


Full documentation with deep API reference and examples: https://anishfyi.github.io/curl_reap/

Why

Modern scraping needs three things, and today you reach for three different tools:

  1. Get past the door. Sites fingerprint your TLS handshake and block stock Python clients. curl_cffi solves this with real Chrome/Safari fingerprints.
  2. Survive markup changes. Plain CSS and XPath break the moment a site renames a class. Scrapling pioneered self-healing selectors that re-find the element anyway.
  3. Crawl at scale. Concurrency, throttling, retries, dedup, and pipelines. That is Scrapy.

curl_reap takes the best idea from each and puts them behind one friendly API.

curl_cffi Scrapy Scrapling curl_reap
Real browser TLS / JA3 yes no partial yes
Parser built in no yes yes yes
Self-healing selectors no no yes yes
Structured extraction (jsonld/meta/tables/markdown) no no partial yes
Concurrent crawl engine no yes no yes
AutoThrottle, retries, pipelines no yes no yes
Fingerprint + proxy rotation partial no no yes
Async client yes no partial yes
Disk response cache no partial no yes
One small dependency set yes no no yes

Install

pip install curl_reap

Requires Python 3.9+. Pulls in curl_cffi, lxml, and cssselect.

Quick start

A one-shot fetch parses like parsel, but the request carries a genuine browser fingerprint:

import curl_reap as reap

page = reap.get("https://quotes.toscrape.com", impersonate="chrome124")
print(page.css("span.text::text").getall())
print(page.css_first("small.author::text"))

Structured extraction

The answers most scrapes actually want are one method call away — no selectors required:

page = reap.get("https://example.com/product/42")

page.jsonld()      # all JSON-LD blocks as dicts (product/article/event data)
page.meta_tags()   # {title, description, og:*, twitter:*, canonical, ...}
page.links(internal_only=True)   # [{"url": ..., "text": ...}, ...] absolute urls
page.images()      # [{"url": ..., "alt": ...}] (handles lazy data-src)
page.tables()      # every <table> as list-of-rows
page.markdown()    # readable page content as markdown — great for LLMs

Resilience: retries, rotation, cache, async

# smart retries with exponential backoff + jitter, honoring Retry-After;
# retries 429/5xx automatically
s = reap.Session(retry_policy=reap.RetryPolicy(retries=4, backoff=0.5))

# rotate real browser fingerprints and proxies across requests
s = reap.Session(rotate="random",
                 proxy=["http://p1:8080", "http://p2:8080"])

# disk cache: repeat GETs come from disk (fast dev loops, fewer hits)
s = reap.Session(cache=reap.DiskCache(ttl=3600))
r = s.get(url); r2 = s.get(url)   # r2.from_cache is True

# async: the same impersonating fetch, awaitable
import asyncio
async def main():
    async with reap.AsyncSession() as a:
        pages = await asyncio.gather(*(a.get(u) for u in urls))
asyncio.run(main())

Self-healing selectors

Save an element once. Later, even if the site renames the class or moves the node, auto_match relocates it by structural signature:

page = reap.get("https://shop.example.com/item/42")
page.css_first("a.buy-btn").save("buy_button")     # remember its shape

# weeks later, the class is now "purchase-cta" and the old selector misses:
later = reap.get("https://shop.example.com/item/99")
btn = later.css_first("a.buy-btn", auto_match=True, identifier="buy_button")
print(btn.attr("href"))                            # found anyway

Other finders: page.find_by_text("Sign in") and page.find_similar(some_element).

Crawl at scale

A Spider yields items (dicts) and more Request objects. A continuous scheduler keeps every worker busy (one slow page never stalls the crawl), with per-domain AutoThrottle, retries, dedup, depth/domain limits, optional robots.txt, and pipelines:

import curl_reap as reap
from curl_reap import JsonLinesPipeline

class Quotes(reap.Spider):
    start_urls = ["https://quotes.toscrape.com"]
    allowed_domains = ["quotes.toscrape.com"]   # confine the crawl
    max_depth = 5

    def parse(self, page):
        for q in page.css("div.quote"):
            yield {
                "text": q.css_first("span.text::text"),
                "author": q.css_first("small.author::text"),
            }
        nxt = page.css_first("li.next a::attr(href)")
        if nxt:
            yield page.follow(nxt)                # resolves relative urls for you

items = reap.run(
    Quotes,
    concurrency=8,
    throttle=True,             # per-domain AutoThrottle, backs off on 429/503
    respect_robots=True,       # opt-in robots.txt compliance
    pipelines=[JsonLinesPipeline("quotes.jsonl")],
)
print(len(items), "items reaped")

Crawl straight from a sitemap with SitemapSpider:

class Products(reap.SitemapSpider):
    sitemap_urls = ["https://shop.example.com"]   # /sitemap.xml assumed
    url_pattern = r"/product/"
    def parse(self, page):
        yield page.jsonld()[0]

Command line

Installing the package also installs a reap command:

reap get https://example.com                      # readable markdown
reap get https://example.com --css "h1::text"     # extract with a selector
reap get https://api.site.com/x --json            # pretty-print JSON
reap meta https://example.com                      # title + og/twitter + JSON-LD
reap links https://example.com --internal          # list same-domain links
reap crawl https://quotes.toscrape.com --css "span.text::text" \
     --max-pages 20 -o out.jsonl                    # crawl to a file (.jsonl/.csv/.db)

API at a glance

  • reap.get(url, impersonate="chrome124", **kw) and reap.post(...) return a Response you can .css() / .xpath() directly. .status, .ok, .from_cache, .follow(), .raise_for_status().
  • reap.Session(impersonate=..., headers=..., retry_policy=..., rotate=..., proxy=..., cache=...) for a reusable client; reap.AsyncSession / reap.aget for async.
  • Structured extraction on any page: .jsonld(), .meta_tags(), .links(), .images(), .tables(), .markdown().
  • Selector / SelectorList: .css, .css_first, .xpath, .find_by_text, .find_similar, .save, .re, .re_first, .text, .attr.
  • reap.Spider, reap.SitemapSpider, reap.Request(url, priority=, errback=, dont_filter=), reap.run(spider, ...), reap.Reaper(...).
  • Pipelines: DedupPipeline, JsonLinesPipeline, CsvPipeline, SqlitePipeline, or subclass Pipeline.
  • reap.Geocoder().geocode(name, area, city, country): turn a name or address into coordinates with a precision label (name, district, or city), cached and rate limited.

Legal and acceptable use

curl_reap impersonates a real browser at the TLS level, which is what a normal browser does. It does not solve CAPTCHAs, bypass logins or paywalls, or defeat anti-bot services (Cloudflare, DataDome, PerimeterX, Akamai). If a site is actively blocking you, that block is the line to respect. You are responsible for checking robots.txt and each site's terms, not circumventing technical access controls, handling personal data lawfully (GDPR / CCPA), and respecting copyright. Provided under MIT, "as is", with no warranty.

Full notice and your responsibilities as a user: LEGAL.md.

License

MIT. See LICENSE.

Download files

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

Source Distribution

curl_reap-0.2.2.tar.gz (25.8 kB view details)

Uploaded Source

Built Distribution

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

curl_reap-0.2.2-py3-none-any.whl (30.6 kB view details)

Uploaded Python 3

File details

Details for the file curl_reap-0.2.2.tar.gz.

File metadata

  • Download URL: curl_reap-0.2.2.tar.gz
  • Upload date:
  • Size: 25.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for curl_reap-0.2.2.tar.gz
Algorithm Hash digest
SHA256 267e7668009dca1b51e50f48d705fc5daa75cc9285be78e4ed9df8cd7eadc39f
MD5 9a19f6f3e44fd73b02a587c9ed06da5f
BLAKE2b-256 f7c78787578812ae973748693cc35d8e57f3bc001c4163c6cadf78ee3d8047fa

See more details on using hashes here.

File details

Details for the file curl_reap-0.2.2-py3-none-any.whl.

File metadata

  • Download URL: curl_reap-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 30.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for curl_reap-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 888c633341623cbeffe64c5312dd2f54d7665179229e9902a10b84b44535225b
MD5 fa83005422b72d368a03cab75838b92f
BLAKE2b-256 5f2684e2f6f6420c660012f01b53e0e0a8f09e271d89ca58b7caef485fd8fb1e

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.2 This release

2 files

0.2.1

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page