Skip to main content

High-performance BeautifulSoup replacement written in Rust

Project description

WhiskeySour

WhiskeySour logo

A high-performance drop-in replacement for Python's BeautifulSoup, written in Rust and published as a native Python package via PyO3.

Status: Beta — core implementation complete. 450 unit tests passing, 508 including integration tests.


Why WhiskeySour?

BeautifulSoup is beloved but slow. Every node is a Python object (~500 bytes), parsing is GIL-bound, and CSS selectors re-parse on every call. WhiskeySour fixes this at the foundation.

All numbers below are medians from a dev build (maturin develop). Release builds (maturin develop --release) are typically 2–3× faster still.

Operation WhiskeySour bs4 + html.parser Speedup
Parse 10KB 0.33 ms 3.78 ms 11×
Parse 100KB 4.08 ms 42.87 ms 11×
Parse 500KB 9.99 ms 106.37 ms 11×
find(id=…) 0.21 ms 2.21 ms 11×
find_all(class_=…) 0.62 ms 4.41 ms
select("div.item") 0.64 ms 8.92 ms 14×
get_text() 0.17 ms 0.68 ms
str() (serialize) 0.43 ms 21.58 ms 50×
tag.get("class") 0.29 µs 7.0 µs 24×
Memory per node ~40 bytes ~500 bytes 12× less

Key implementation choices:

  • Rust core via PyO3 + maturin
  • html5ever — spec-compliant HTML5 parser (same as Firefox/Chrome)
  • Arena allocation — compact ~40 byte/node layout vs ~500 bytes in bs4
  • cssparser — CSS selectors compiled to DFA, LRU-cached
  • GIL release — all Rust tree operations run outside the Python GIL
  • memchr — SIMD byte scanning (SSE2 / AVX2 / NEON)

API — drop-in compatible with BeautifulSoup

from whiskeysour import WhiskeySour

# Same as BeautifulSoup(html, "html.parser")
soup = WhiskeySour(html, "html.parser")

# All standard bs4 operations work identically:
soup.find("h1")
soup.find_all("a", class_="external")
soup.select("div.container > p:first-child")
soup.title.string
soup.find(id="main").get_text(strip=True)

# BS4-compatible NavigableString (name is None, exactly like bs4)
for child in tag.children:
    if child.name:          # None for text nodes, str for elements — same as bs4
        print(child.name)

# Drop-in alias
from whiskeysour import BeautifulSoup   # same class, different name

WhiskeySour extensions (not in bs4)

# Pre-compiled CSS selector — zero parse overhead on repeated use
q = soup.compile("div.item > a[href]")
for doc in documents:
    results = q.select(doc)

# Streaming parser — feed chunks incrementally
from whiskeysour import StreamParser, parse_stream

with StreamParser() as parser:
    for chunk in response.iter_content(4096):
        parser.feed(chunk)
soup = parser.close()

# Generator-style streaming with automatic extraction
import io
with open("large.html", "rb") as f:
    for article in parse_stream(f, selector="article.post"):
        print(article.find("h1").get_text())

BS4 compatibility notes

WhiskeySour is a faithful drop-in for the vast majority of BeautifulSoup code. A handful of behaviours differ due to html5ever's spec compliance:

Behaviour WhiskeySour BeautifulSoup
NavigableString.name None (identical to bs4) None
prettify(indent=N) Supported (alias for indent_width) Supported
</br> in source Creates 2 <br> (HTML5 spec) Creates 1 <br>
Duplicate attributes Keeps first (HTML5 spec) Keeps last
Null bytes \x00 Stripped (HTML5 spec) Passed through
Attribute order in str() Insertion order Alphabetical

The first two rows are identical; the remaining differences only affect malformed HTML.


Project Structure

WhiskeySour/
├── Cargo.toml                  # Rust workspace root
├── pyproject.toml              # maturin build config
├── pytest.ini                  # test configuration
│
├── crates/
│   ├── whiskeysour-core/        # Pure Rust library (no Python deps)
│   │   └── src/
│   │       ├── parser/         # html5ever integration
│   │       ├── node.rs         # Arena-allocated node pool
│   │       ├── selector/       # CSS selector DFA + LRU cache
│   │       ├── traversal/      # Tree iterators
│   │       ├── query/          # find() / find_all() / select()
│   │       └── serialize/      # HTML serialisation + prettify
│   │
│   └── whiskeysour-py/          # PyO3 bindings layer
│       └── src/
│           └── lib.rs          # _Tag, _Document Python classes
│
├── python/
│   └── whiskeysour/
│       ├── __init__.py         # Public API + BeautifulSoup alias
│       └── _core.pyi           # Type stubs for Rust extension
│
└── tests/
    └── python/
        ├── conftest.py
        ├── unit/               # 450 tests across 10 files
        ├── integration/        # bs4 API parity tests (58 tests)
        ├── performance/        # pytest-benchmark suites + comparison report
        └── fuzz/               # Hypothesis property tests (16 tests)

Quick start

# Prerequisites: Python 3.9+, Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate        # macOS / Linux

# Install dependencies
pip install maturin pytest pytest-benchmark hypothesis beautifulsoup4

# Build the Rust extension
maturin develop                  # dev build (fast to compile)
# maturin develop --release      # optimised (use for benchmarks)

# Run tests
pytest tests/python/unit/

Running tests

# All unit tests (fastest, no extra deps needed)
pytest tests/python/unit/ -q

# Single test file
pytest tests/python/unit/test_parsing.py -v

# Integration tests (requires beautifulsoup4)
pytest tests/python/integration/ -v

# Skip slow tests (large documents, deep nesting)
pytest -m "not slow"

# Fuzz / property-based tests (requires hypothesis)
pytest tests/python/fuzz/ -v

# Benchmark suites (requires pytest-benchmark)
pytest tests/python/performance/ --benchmark-only -v

# Performance comparison report (WhiskeySour vs BeautifulSoup)
python tests/python/performance/bench_comparison.py
python tests/python/performance/bench_comparison.py --fixture small --rounds 50
python tests/python/performance/bench_comparison.py --output /tmp/report.html
open bench_report.html

Test file overview

File Tests Covers
unit/test_parsing.py 67 HTML5 parsing, fragments, malformed HTML, void elements
unit/test_encoding.py 31 UTF-8/16, Latin-1, BOM, meta charset, surrogate pairs
unit/test_find.py 60 find/find_all by tag/id/class/attr/string/regex/lambda
unit/test_css_selectors.py 71 CSS3 + :has/:is/:where, structural pseudo-classes
unit/test_tree_navigation.py 67 parent/children/siblings/descendants/.string/.strings
unit/test_modification.py 49 decompose/extract/replace_with/insert/append/wrap
unit/test_output.py 43 str()/prettify()/encode()/round-trip stability
unit/test_edge_cases.py 44 10k+ nodes, deep nesting, concurrency, control chars
unit/test_streaming.py 19 StreamParser push API, parse_stream() generator
unit/test_css_selectors.py 71 CompiledSelector, cached selectors
integration/test_bs4_compat.py 58 Every public bs4 API, cross-library parity
fuzz/fuzz_parser.py 16 Hypothesis: no crash, valid UTF-8, round-trip stable

Test markers

Marker Description
slow Large document tests — skip with -m "not slow"
perf Benchmark tests — requires --benchmark-only

Development

# Rust checks (no build required)
~/.cargo/bin/cargo check -p whiskeysour-py

# Dev build (fast recompile, debug symbols)
maturin develop

# Release build (use for perf work)
maturin develop --release

# Rust tests
cargo test

# Formatting / linting
cargo fmt
cargo clippy

Building wheels

maturin build --release
maturin build --release --interpreter python3.9 python3.10 python3.11 python3.12 python3.13
maturin publish

Contributing

  1. All changes must be accompanied by tests
  2. Run pytest tests/python/unit/ -m "not slow" before submitting
  3. Run cargo fmt and cargo clippy for Rust changes
  4. Performance regressions > 5% against the baseline will block merge

Licence

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

whiskeysour-0.1.0.tar.gz (48.3 kB view details)

Uploaded Source

Built Distributions

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

whiskeysour-0.1.0-cp39-abi3-win_amd64.whl (459.6 kB view details)

Uploaded CPython 3.9+Windows x86-64

whiskeysour-0.1.0-cp39-abi3-musllinux_1_2_x86_64.whl (786.5 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ x86-64

whiskeysour-0.1.0-cp39-abi3-musllinux_1_2_aarch64.whl (737.7 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ ARM64

whiskeysour-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (553.7 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

whiskeysour-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (560.3 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

whiskeysour-0.1.0-cp39-abi3-macosx_11_0_arm64.whl (510.0 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

whiskeysour-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl (526.1 kB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: whiskeysour-0.1.0.tar.gz
  • Upload date:
  • Size: 48.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for whiskeysour-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f7912a5abdfacec4b1bbad93268041b1bf13ceae3ec69316c8762545024e6cf1
MD5 5168f7d4b5e69789504e8e4c01f4cc4f
BLAKE2b-256 78f9f1f6aed8034b44291fc58542ca33245ee76d5d893066861ce37bef85faa2

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.0.tar.gz:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.0-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: whiskeysour-0.1.0-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 459.6 kB
  • Tags: CPython 3.9+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for whiskeysour-0.1.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 57b47e603e0a6dd0a131c62b3f8a8c68422c8f3624d25ef80027401fef40e44f
MD5 cba2729e5eff6275d90c1379e73a8dee
BLAKE2b-256 2fa102c879799aad00653bc50f32bc2d3d44f95ab38b18c0282938c1692c2e01

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.0-cp39-abi3-win_amd64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.0-cp39-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.0-cp39-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 763fe369e06e770ac6ad980ebb720deac1682d8ff007bf81f107db69705eadff
MD5 d0047d9d61ba38f9d8915c5f88f2f193
BLAKE2b-256 49893df231ffc0a9fe67aa5f211b204ca9cb3467e064d6b54052c2ab506497bc

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.0-cp39-abi3-musllinux_1_2_x86_64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.0-cp39-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.0-cp39-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 ded67c453f3ddd9b316c86c187f0fd5e9f010dca01f7cffeaa81576ac5078f57
MD5 9c00671301413ae7b5525242e573ab03
BLAKE2b-256 3d06326de096d1807b6798a7f8831ed0492816e9ef367f66c110a73bdd7ad6a0

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.0-cp39-abi3-musllinux_1_2_aarch64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 3dff34b61571a2350cf6e0f18e52437e3f8748916a3bca19db2db998ab49083e
MD5 1e0cb36e8255e509d9b54abb2f424f18
BLAKE2b-256 86d6e570ad5742852dfc241fb689c29161743a715c7caab7c18a8c1ad30197b4

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 5b2d9d62617a34fd1e0f86aa91ffa6c5e65ebbb8e392eb4b620918e4309db383
MD5 0be06c3f661ebdd0b8977e570bf9bf42
BLAKE2b-256 535f3f5687a2270b0017aa3fa2e23200c529f171ea674c7fd3529fe278d94ae1

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 0fe063544556930957cdd91520f4704e006a4e7e80e8515a87e51ac7d9745346
MD5 7c528ba8baa600cf2d7a4776251e5b28
BLAKE2b-256 1fceb3c3f36b1748ea0ab9c6dfd764582c66f21d0784f721b571f32279db64af

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.0-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 15b100cb605fac02545e581d77e355991e141971a8b1ff7ad5d3da80dab13bce
MD5 c1e1b900f314d67dd499b046a10f9cee
BLAKE2b-256 45458a41c25c0750e6cb2e626e355dd063572f47a70782d14396566f939bfcef

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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