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.10+.


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.1.tar.gz (137.1 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.1-py3-none-any.whl (28.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: articula-0.1.1.tar.gz
  • Upload date:
  • Size: 137.1 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.1.tar.gz
Algorithm Hash digest
SHA256 0fe7c0737eafb5943a2ec67536564182f0e1a0c90975d5d7a4113074152f84ea
MD5 1c8323fff9d842617bf9d5b060237281
BLAKE2b-256 44b8dc151137541a566def47823f07473b83636d51eb66a2e97ffc61a4252add

See more details on using hashes here.

File details

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

File metadata

  • Download URL: articula-0.1.1-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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2274589e634f35d69f62588742fbc641dffbd098016f73c59f0cfc41cc5d6bc0
MD5 d49ce4b73cdc79b0deb1f689a473df7a
BLAKE2b-256 2d0c4a15be4e0ce505a8af7bc79f59c251ea75c315e46803fec23bfac860f011

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