CrawlSmith
Crawlsmith is a Python scraping toolkit for fetching web pages with
curl_cffi, extracting readable content, detecting common anti-bot
interstitials, and returning structured metadata in a single result object.
It is designed for Python developers who want a small, pragmatic interface for:
- fetching HTML or XML content
- converting HTML to Markdown via
domdown— turns article-like web pages into clean, structured Markdown with frontmatter, image/table/code preservation, and article body extraction - rotating browser impersonation profiles
- trying multiple proxies
- classifying HTTP and network failures
- extracting document, Open Graph, Twitter, and HTTP metadata
Features
- Async-first Python API built around
CurlCffiScraper - Structured
FetchResultobject with success state, content, Markdown, and metadata - Automatic browser fingerprint headers and
curl_cffiimpersonation support - Proxy rotation with early success and retry limits
- Detection of common anti-bot challenge pages such as Cloudflare-style interstitials
- Gzip payload handling for compressed responses and feeds
- Built-in CLI for quick fetch, inspection, and debugging
Installation
Install from PyPI:
pip install crawlsmith
Requirements:
- Python 3.10+
Quick Start
import asyncio
from crawlsmith import CurlCffiScraper
async def main() -> None:
scraper = CurlCffiScraper()
result = await scraper.fetch("https://example.com")
if result.ok:
print(result.status)
print(result.content[:200])
print(result.markdown[:200])
else:
print(result.error_type, result.error)
asyncio.run(main())
Python Usage
Basic Fetch
import asyncio
from crawlsmith import CurlCffiScraper
async def main() -> None:
scraper = CurlCffiScraper()
result = await scraper.fetch("https://example.com")
if not result.ok:
raise RuntimeError(f"{result.error_type}: {result.error}")
print("Status:", result.status)
print("URL:", result.url)
print("Content length:", result.content_length)
asyncio.run(main())
Read HTML and Markdown
When a request succeeds with HTTP 200, Crawlsmith returns both the raw response
body and a Markdown representation.
import asyncio
from crawlsmith import CurlCffiScraper
async def main() -> None:
scraper = CurlCffiScraper()
result = await scraper.fetch("https://example.com")
if result.ok:
html = result.content
markdown = result.markdown
print(html[:300])
print(markdown[:300])
asyncio.run(main())
Access Structured Metadata
Each result includes metadata extracted from the response body and headers.
import asyncio
from crawlsmith import CurlCffiScraper
async def main() -> None:
scraper = CurlCffiScraper()
result = await scraper.fetch("https://example.com")
metadata = result.metadata or {}
document = metadata.get("document", {})
open_graph = metadata.get("open_graph", {})
twitter = metadata.get("twitter", {})
http = metadata.get("http", {})
print("Title:", document.get("title"))
print("Description:", document.get("description"))
print("Canonical URL:", document.get("canonical_url"))
print("OG Title:", open_graph.get("title"))
print("Twitter Card:", twitter.get("card"))
print("Final URL:", http.get("final_url"))
asyncio.run(main())
Use Proxies
Pass a list of proxies. Crawlsmith will shuffle them, try up to three unique entries, and return as soon as one succeeds with enough content.
import asyncio
from crawlsmith import CurlCffiScraper
async def main() -> None:
scraper = CurlCffiScraper(
proxies=[
"http://user:pass@proxy-1.example:8080",
"http://user:pass@proxy-2.example:8080",
"proxy-3.example:8080",
],
min_content_length=2000,
)
result = await scraper.fetch("https://example.com")
print(result.ok, result.via_proxy, result.proxy_url)
asyncio.run(main())
Control Browser Impersonation
You can force a specific curl_cffi impersonation profile instead of using the
default randomized behavior.
import asyncio
from crawlsmith import CurlCffiScraper
async def main() -> None:
scraper = CurlCffiScraper(impersonate="chrome120")
result = await scraper.fetch("https://example.com")
print(result.status, result.error_type)
asyncio.run(main())
Configure TLS and Timeouts
import asyncio
from crawlsmith import CurlCffiScraper
async def main() -> None:
scraper = CurlCffiScraper(
verify=True,
connect_timeout=5,
read_timeout=20,
)
result = await scraper.fetch("https://example.com")
print(result.to_dict())
asyncio.run(main())
If you need to disable TLS certificate verification for a controlled internal
environment, set verify=False.
Handle Errors Explicitly
Failures are returned as structured results instead of raising request errors in normal operation.
import asyncio
from crawlsmith import CurlCffiScraper
async def main() -> None:
scraper = CurlCffiScraper()
result = await scraper.fetch("https://example.com")
if result.ok:
print("Fetched successfully")
return
print("Error type:", result.error_type)
print("Error message:", result.error)
print("HTTP status:", result.status)
print("Blocked:", result.is_blocked)
asyncio.run(main())
Common error types include:
TIMEOUTCONNECTIONSSLINVALID_URLBLOCKEDHTTP_403HTTP_429HTTP_4XXHTTP_5XXUNKNOWN
Serialize Results
FetchResult can be converted directly into a plain dictionary for logging,
storage, or JSON serialization.
import asyncio
import json
from crawlsmith import CurlCffiScraper
async def main() -> None:
scraper = CurlCffiScraper()
result = await scraper.fetch("https://example.com")
print(json.dumps(result.to_dict(), indent=2))
asyncio.run(main())
CLI Usage
The package installs a crawlsmith command for quick fetches from the terminal.
Basic CLI Request
crawlsmith fetch https://example.com
The CLI prints a JSON-serialized FetchResult to stdout.
Print the Response Body
crawlsmith fetch --url https://example.com --print-content
Print Markdown Version
crawlsmith fetch --url https://example.com --print-markdown
The Markdown output includes YAML frontmatter with metadata (title, author, tags, etc.) followed by clean, readable content.
Use One or More Proxies
crawlsmith fetch --url https://example.com \
--proxy http://user:pass@proxy-1.example:8080 \
--proxy http://user:pass@proxy-2.example:8080 \
--min-content-length 2000
Force an Impersonation Profile
crawlsmith fetch --url https://example.com --impersonate chrome120
Change Timeout or Disable TLS Verification
crawlsmith fetch --url https://example.com --timeout 20
crawlsmith fetch --url https://example.com --insecure
CLI Exit Codes
0when the request succeeds1when the request fails
CLI Help
crawlsmith --help
crawlsmith fetch --help
Result Model
FetchResult exposes the following fields:
ok: whether the request was considered successfulurl: requested URLstatus: HTTP status code when availablecontent: raw response text when successfulmarkdown: Markdown conversion of the response body when successfulmetadata: extracted document and HTTP metadataerror_type: normalized error classificationerror: human-readable error summaryvia_proxy: whether the successful or failed attempt used a proxyproxy_url: proxy used for the final attempt, if anycontent_length: UTF-8 byte length of the extracted textis_blocked: whether the response looks like an anti-bot interstitial
Support & Connect
- ⭐ Star the repo if you found it useful
- ☕ Support me: Say thanks by buying me a coffee! https://buymeacoffee.com/juanmcristobal
- 💼 Open to work: https://www.linkedin.com/in/jmcristobal/
History
0.2.2 (2026-07-01)
- Detect SPA shells and discover JSON APIs from linked bundles
- Normalize GitHub
blobURLs toraw.githubusercontent.combefore fetching
0.2.1 (2026-06-30)
- Bump
domdownto0.3.1 - Add
frontmatter_optsfallbacks forcanonical_urlandsource
0.2.0 (2026-06-04)
- Switch from
markdownifytodomdownfor HTML-to-Markdown conversion- YAML frontmatter with extracted metadata (title, author, tags, etc.)
- Article body extraction and image/table/code block preservation
- Add
--print-markdownCLI flag for printing Markdown output - Restructure CLI:
@click.group()withfetchsubcommand - Add
markdown_lengthfield toFetchResult - Update README examples to use
crawlsmith fetch --url ... - Update tests for CLI and
domdownchanges
0.1.0 (2026-04-07)
- First release.
Release files for crawlsmith 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| crawlsmith-0.2.2.tar.gz | 23.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| crawlsmith-0.2.2-py2.py3-none-any.whl | Python 3, Python 2 | none | any | Details |
Total release size: 38.8 kB
Release files / crawlsmith-0.2.2.tar.gz
| Download URL | crawlsmith-0.2.2.tar.gz |
|---|---|
| Size | 23.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a29e822af77e36531f2fcd9d6a49477b38c75a016e1958f112ca92f19e905ed7
|
|
BLAKE2b-256 checksum How to use checksums |
9f884931bde4a5db57b61ca067057219b47a9b9147fa3eedb1096f0ee7d429b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 1, 2026.
Transparency logRelease files / crawlsmith-0.2.2-py2.py3-none-any.whl
| Download URL | crawlsmith-0.2.2-py2.py3-none-any.whl |
|---|---|
| Size | 15.3 kB |
| Tags | Python 2 Python 3 |
|
SHA-256 checksum How to use checksums |
f96139c6cc5d816ea62196c4822f2b8d062cf8638770ae517423dd3dfec4b371
|
|
BLAKE2b-256 checksum How to use checksums |
6b3ebbe71006b4afc0025bac09f3c377e52c21be25cb099e242b8335027b2671
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 1, 2026.
Transparency log