Skip to main content

Xcrap for Python

PyPI Python versions Downloads CI Coverage Code style: Ruff License: MIT

Xcrap is a modern web scraping framework. It gives you interchangeable HTTP and browser clients, declarative extraction for HTML, JSON and Markdown, and Scrapy-inspired spiders for crawling whole sites — with one consistent, asyncio-native API.

This is the Python port of the Node.js Xcrap framework, keeping the same concepts and vocabulary so scrapers translate between the two.

  • Declarative extraction — describe the shape of your data with models, not loops of selectors.
  • Interchangeable clients — httpx, curl-cffi (anti-bot TLS impersonation), Playwright, Selenium and SeleniumBase (undetected mode) behind one interface.
  • Every format, and formats inside formats — HTML (CSS/XPath), JSON (JMESPath), Markdown; e.g. Markdown inside JSON inside a <script> tag.
  • Spiders — scheduler, de-duplication, concurrency, throttling, retries, pipelines and stats.
  • Configuration-driven — build clients and models from JSON with factories.
  • Batteries included — user-agent/proxy rotation, static pagination, AES response decryption, OCR.

📚 Full documentation: docs/ · 🧪 Runnable examples: examples/

Installation

Requires Python 3.12+.

pip install xcrap

Optional extras:

pip install "xcrap[curl]"        # CurlCffiClient: TLS fingerprint impersonation (Cloudflare & co.)
pip install "xcrap[playwright]"  # PlaywrightClient: JavaScript rendering (then: playwright install chromium)
pip install "xcrap[selenium]"    # SeleniumClient: WebDriver automation
pip install "xcrap[seleniumbase]" # SeleniumBaseClient: undetected (UC) mode for bot-protected sites
pip install "xcrap[ocr]"         # Text from images (requires the Tesseract binary)
pip install "xcrap[all]"         # everything

Quick start

import asyncio

from xcrap.clients import HttpxClient
from xcrap.extractor import HtmlBaseField, HtmlExtractionModel, HtmlNestedField, css


# 1. Describe your data declaratively
class AuthorModel(HtmlExtractionModel):
    name = HtmlBaseField(query=css("small.author::text"))
    link = HtmlBaseField(query=css("a::attr(href)"))


class QuoteModel(HtmlExtractionModel):
    text = HtmlBaseField(query=css("span.text::text"))
    author = HtmlNestedField(model=AuthorModel)           # nested models are effortless
    tags = HtmlBaseField(query=css("div.tags a.tag::text"), multiple=True)


class QuotesPageModel(HtmlExtractionModel):
    quotes = HtmlNestedField(query=css("div.quote"), model=QuoteModel, multiple=True)


async def main():
    client = HttpxClient()

    # 2. Fetch the page
    response = await client.fetch(url="https://quotes.toscrape.com")

    # 3. Turn it into a parser and extract
    data = response.as_html_parser().extract_model(QuotesPageModel)

    for quote in data["quotes"][:3]:
        print(f"{quote['text']} — {quote['author']['name']} {quote['tags']}")


asyncio.run(main())

Crawling with spiders

When you need to follow links, paginate and write results somewhere, let a spider drive:

from xcrap.extractor import css
from xcrap.spiders import DropItem, ItemPipeline, JsonLinesPipeline, Spider


class QuotesSpider(Spider):
    name = "quotes"
    start_urls = ["https://quotes.toscrape.com"]
    concurrency = 4
    request_delay = 200      # ms between requests
    max_retries = 2

    async def parse(self, response):
        parser = response.as_html_parser()

        for quote in parser.extract_models(QuoteModel, css("div.quote")):
            yield quote                                   # a dict -> an item

        for href in parser.extract_values(css("li.next a::attr(href)")):
            yield response.follow(href)                   # a Request -> keep crawling


class RequireText(ItemPipeline):
    async def process_item(self, item, spider):
        if not item["text"]:
            raise DropItem("no text")
        return item


stats = QuotesSpider(pipelines=[RequireText(), JsonLinesPipeline("quotes.jsonl")]).run()
print(stats.items_scraped, "items in", round(stats.elapsed, 1), "s")

No parse needed for the common case — a ModelSpider is configured with model, items_query and follow_query. See the spiders guide, including how it differs from Scrapy.

Choosing a client

from xcrap.clients import CurlCffiClient, HttpxClient, PlaywrightClient, SeleniumBaseClient, SeleniumClient

HttpxClient()                                   # fast default for APIs and static HTML
CurlCffiClient(impersonate="chrome120")         # looks like a real browser to anti-bot systems
PlaywrightClient(headless=True)                 # runs JavaScript; supports actions
SeleniumClient(browser_type="chrome")           # WebDriver
SeleniumBaseClient(uc=True)                     # undetected Chrome: Cloudflare, CAPTCHAs

All of them accept max_retries, retry_delay and headers per fetch, and user_agent / proxy_url as a string or a callable for rotation. Browser clients take actions to click, scroll or wait before the HTML is captured:

response = await PlaywrightClient().fetch(
    url="https://quotes.toscrape.com/js-delayed/",
    actions=[lambda page: page.wait_for_selector("div.quote")],
)

More in the clients guide.

More features

JSON with JMESPath

from xcrap.extractor import JsonBaseField, JsonExtractionModel, JsonParser, jmes_path


class ProjectModel(JsonExtractionModel):
    name = JsonBaseField(query=jmes_path("title"))
    tags = JsonBaseField(query=jmes_path("tags"), multiple=True)


JsonParser('{"title": "Xcrap", "tags": ["python", "scraping"]}').extract_model(ProjectModel)
# {'name': 'Xcrap', 'tags': ['python', 'scraping']}

Formats inside formats — HTML → JSON → Markdown in four lines:

script = response.as_html_parser().extract_value(css('script[data-target="react-app.embeddedData"]::text'))
lines = JsonParser(script).extract_value(jmes_path('payload."codeViewBlobLayoutRoute.StyledBlob".rawLines'))
readme = MarkdownParser("\n".join(lines))

Custom extractors receive the matched parsel Selector:

price = HtmlBaseField(query=css(".price"), extractor=lambda el: float(el.css("::text").get().replace("$", "")))

Automatic decryption of AES-encrypted responses:

from xcrap.core import decrypt_client
from xcrap.utils.decryption import DecryptConfig, DecryptKeyConfig

config = DecryptConfig(algorithm="aes-256-cbc", key=DecryptKeyConfig(value="..."), iv=DecryptKeyConfig(value="..."))

@decrypt_client(config)
class SecureClient(HttpxClient): ...

Factories build clients, extractors and models from configuration data:

from xcrap.factory import create_client, create_extraction_model

client = create_client("httpx", allowed_clients={"httpx": HttpxClient}, options={"user_agent": "my-bot/1.0"})
model = create_extraction_model(
    {"type": "html", "model": {"title": {"query": {"type": "css", "value": "h1::text"}}}},
    allowed_models={"html": HtmlExtractionModel},
    allowed_extractors={},
)

Rotation and pagination: Rotator, Randomizer, StaticPaginator — see utilities.

Documentation

Getting started Installation, first scraper, how the pieces fit
Clients All clients, retries, concurrency, proxies, actions, decryption, custom clients
Extraction HTML, JSON, Markdown, composing formats, OCR
Spiders Crawling, pipelines, settings, statistics, Scrapy comparison
Utilities Rotator, Randomizer, StaticPaginator, errors
Factories Configuration-driven scrapers
Examples 18 runnable scripts against real sites

Development

poetry install --all-extras                              # everything, including the optional clients
poetry run pytest --cov                                  # tests; fails below 100% coverage
poetry run ruff check .                                  # lint
poetry run ruff format .                                 # format

Lint, formatting, tests and coverage also run on every push and pull request (CI). To lint and format automatically on each commit, install the pre-commit hook once:

pipx install pre-commit && pre-commit install

The example scripts hit real websites (quotes.toscrape.com, books.toscrape.com, the GitHub API…) and are not part of the test suite.


Made with ❤️ by Marcuth contact@marcuth.dev

Metadata

Release files for xcrap 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xcrap 0.1.0
File Size Uploaded Provenance
xcrap-0.1.0.tar.gz 30.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for xcrap 0.1.0
File Interpreter ABI Platform
xcrap-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 72.8 kB

Release files / xcrap-0.1.0.tar.gz

Download URL xcrap-0.1.0.tar.gz
Size 30.0 kB
Tags Source
SHA-256 checksum
How to use checksums
6f8726e05eb3914bbfd8a6808790cd2c529c9ea3bdb964b334f9b323a6c90687
BLAKE2b-256 checksum
How to use checksums
8025f6acb98b494d32c5f65a877a26f98cc678734cad40c91a9ea9e86a6f15a4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.5.1 CPython/3.12.10 Windows/10

Release files / xcrap-0.1.0-py3-none-any.whl

Download URL xcrap-0.1.0-py3-none-any.whl
Size 42.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dac02741a9d267078635f91acd4521be7ed50a5dd47a32961c5b6fc0411c7f37
BLAKE2b-256 checksum
How to use checksums
c839c6e90623315e6392eb21c28b86778243af01f55b07d850671f6468fc4170
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.5.1 CPython/3.12.10 Windows/10

Security for xcrap 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

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