Skip to main content

Extract clean structured article data from URLs with tiered escalation strategies

Project description

articula

Give it a URL, get the article. articula extracts the clean, structured content of a news story, column, or blog post from a single URL — the title, body text, author, and published date — and escalates through progressively stronger strategies so that bot detection and JavaScript-rendered pages don't stop it.

from articula import scrape

article = scrape("https://example.com/some-news-article")
print(article.title)
print(article.author, article.published_date)
print(article.text)

It is not a general-purpose crawler or a structured-data scraper. It does one thing well: turn an article URL into a clean record.


Why another scraper?

Most "fetch the page and run readability" approaches fall over on exactly the pages you care about:

  • Bot walls — Cloudflare / "Just a moment…" interstitials, 403/429 responses, naive-client blocks.
  • JavaScript-rendered bodies — the article isn't in the initial HTML; it's injected by a client-side framework after load.

articula handles both by escalating through tiers and only stopping when a tier produces a genuinely usable article body:

Tier Strategy Cost
1 static — plain HTTP GET with sensible headers cheapest
2 headers_rotation — full browser-like header set, User-Agent rotation, bot-challenge detection cheap
3 browser — headless Chromium (Playwright) renders JS, then extracts heaviest

Crucially, a tier "wins" only when its HTML yields a meaningful body. If the static HTML is just a JavaScript shell or a challenge page, extraction fails and articula escalates — all the way to the browser tier, which renders the page like a real user before extracting. The cheap path stays fast (real articles return on tier 1 without ever launching a browser); the hard path still works.

Content extraction itself cascades too: trafilatura → readability-lxml → heuristic, taking the first that produces a substantive body. Dates are normalized to ISO 8601; language is auto-detected.


Installation

The base package is deliberately lightweight (no browser binaries):

pip install articula

To unlock the JavaScript-rendering / anti-bot browser tier, install the extra and the Chromium binary:

pip install "articula[browser]"
playwright install chromium

With the extra installed, the browser tier engages automatically — no configuration required. Without it, articula still does static + readability extraction and degrades gracefully with a clear, actionable error when a page genuinely needs JS rendering.

Requires Python 3.11+.


Usage

Synchronous (scripts, CLIs)

from articula import scrape

article = scrape("https://example.com/article")

Asynchronous (FastAPI, async pipelines)

from articula import async_scrape

article = await async_scrape("https://example.com/article")

The sync scrape() raises a clear error if called from inside a running event loop — use async_scrape() there instead.

Reusing a browser across many URLs (efficient batch)

from articula import Scraper

async with Scraper(timeout=45) as scraper:
    a = await scraper.scrape("https://example.com/one")
    b = await scraper.scrape("https://example.com/two")
    # one headless browser is launched lazily and reused

Batch with bounded concurrency

from articula import scrape_many          # sync
# from articula import async_scrape_many  # async equivalent

# returns results in input order; per-URL errors are captured, not fatal
results = scrape_many(urls, concurrency_limit=4)
for url, result in zip(urls, results):
    if isinstance(result, Exception):
        print(url, "failed:", result)
    else:
        print(url, "->", result.title)

In async code use await async_scrape_many(urls, concurrency_limit=4) instead — scrape_many() raises if called from inside a running event loop.

Options

Every entry point accepts the same power-user knobs (all optional):

scrape(
    url,
    timeout=30.0,            # total wall-clock budget, seconds
    max_retries=3,           # transient-failure retries (429/503/network)
    custom_headers={...},    # extra request headers
    custom_user_agent="…",   # override the User-Agent
    proxy_url="http://…",    # route through a proxy
    force_strategy="browser",# pin to one tier (e.g. skip straight to browser)
    respect_robots=False,    # see "Responsible use" below
)

The Article model

scrape() returns a frozen Article:

Field Type Notes
title str core field
text str cleaned body, core field
author str | None best-effort
published_date str | None ISO 8601, best-effort
resolved_url str final URL after redirects
strategy_tier str "static" / "headers_rotation" / "browser"
extraction_method str "trafilatura" / "readability" / "heuristic"
extraction_confidence float 0.01.0 quality signal
detected_language str BCP-47, e.g. "en", "ko"
strategies_attempted tuple[str, ...] escalation trail

Partial success is success. As long as a body is extracted, you get an Articleauthor/published_date are simply None when not found. Use extraction_confidence to decide how much to trust a result.

Errors

articula raises only on genuine failure (it never silently returns junk):

ScraperError
├── FetchError       # page unreachable after all tiers (network, 4xx/5xx, unsolved challenge)
├── ExtractionError  # page fetched, but no meaningful body could be extracted
├── BrowserNotInstalledError
├── RobotsDisallowedError
└── ConfigurationError

FetchError and ExtractionError carry .url and .strategies_attempted for debugging.


Responsible use

articula defaults to ignoring robots.txt (respect_robots=False) because its purpose is to retrieve content that friction sometimes hides. That power comes with responsibility:

  • Scrape only content you are allowed to access.
  • Respect site terms of service and applicable law.
  • Rate-limit your requests; don't hammer origins.
  • Pass respect_robots=True to honor robots.txt when appropriate.

You are responsible for how you use this tool.


Logging

articula uses the standard logging module under the articula logger with a NullHandler attached, so it's silent until you configure logging:

import logging
logging.getLogger("articula").setLevel(logging.DEBUG)
logging.basicConfig()

Tier escalation, status codes, and timings are logged at DEBUG.


License

MIT

Project details


Download files

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

Source Distribution

articula-0.1.0.tar.gz (136.9 kB view details)

Uploaded Source

Built Distribution

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

articula-0.1.0-py3-none-any.whl (28.9 kB view details)

Uploaded Python 3

File details

Details for the file articula-0.1.0.tar.gz.

File metadata

  • Download URL: articula-0.1.0.tar.gz
  • Upload date:
  • Size: 136.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for articula-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a512ad47fbaef4354837b2c8d087d1c54255307d1115ca4f315dc024a97c47d2
MD5 f815f2d83be290332fada3954418e981
BLAKE2b-256 ac0808aa245acca33316e0244001a3a5b6c965bf42cec30f84516c41791992f2

See more details on using hashes here.

File details

Details for the file articula-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: articula-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 28.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for articula-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 746be9354e9e47a5be6456e072acfa660990ea7bc401f5c579a62a5544f9e8a7
MD5 9afa8bc241881f94b869a9ece6fd4707
BLAKE2b-256 e5a3c2b7ac8367bad9d7a9e2c616866b6e848f3f58294bd99a5ef8d018e6bca5

See more details on using hashes here.

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