htmlsift 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- htmlsift-0.2.0/.github/workflows/publish.yml +39 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/.gitignore +0 -2
- htmlsift-0.2.0/CHANGELOG.md +72 -0
- htmlsift-0.2.0/PKG-INFO +148 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/README.md +97 -15
- htmlsift-0.2.0/README.pypi.md +116 -0
- htmlsift-0.2.0/docs/assets/annotation-example.svg +109 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/architecture.svg +136 -136
- {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/concept.svg +89 -89
- {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/cpu_speed_accuracy.svg +66 -66
- {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/data-efficiency.svg +120 -118
- {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/gpu_speed_accuracy_lite.svg +30 -30
- {htmlsift-0.1.0 → htmlsift-0.2.0}/docs/assets/signals.svg +75 -75
- {htmlsift-0.1.0 → htmlsift-0.2.0}/pyproject.toml +4 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/__init__.py +1 -1
- htmlsift-0.2.0/src/htmlsift/_decode.py +45 -0
- htmlsift-0.2.0/src/htmlsift/_links.py +52 -0
- htmlsift-0.2.0/src/htmlsift/cli.py +135 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/prune.py +5 -4
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/extractor.py +26 -12
- htmlsift-0.2.0/tests/conftest.py +16 -0
- htmlsift-0.2.0/tests/data/article_fi.html +32 -0
- htmlsift-0.2.0/tests/data/article_ja.html +29 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/data/sample.extracted.html +14 -14
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/data/sample.extracted.md +19 -19
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/data/sample.extracted.txt +14 -14
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/data/sample.rendered.txt +24 -24
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_api.py +32 -0
- htmlsift-0.2.0/tests/test_cli.py +167 -0
- htmlsift-0.2.0/tests/test_decode.py +153 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_extract_golden.py +29 -0
- htmlsift-0.2.0/tests/test_links.py +163 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_prune.py +20 -0
- htmlsift-0.1.0/PKG-INFO +0 -89
- htmlsift-0.1.0/README.pypi.md +0 -61
- htmlsift-0.1.0/src/htmlsift/cli.py +0 -29
- {htmlsift-0.1.0 → htmlsift-0.2.0}/.gitattributes +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/.github/workflows/ci.yml +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/AGENTS.md +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/LICENSE +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/_artifacts.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/__init__.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/band.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/features.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/onnx_infer.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/prep_page.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/render.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/tokenizer.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/core/torch_infer.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/loader.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/src/htmlsift/py.typed +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/__init__.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/data/sample.html +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_features.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_manifest.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_prep_page.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_render_golden.py +0 -0
- {htmlsift-0.1.0 → htmlsift-0.2.0}/tests/test_render_links.py +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI via Trusted Publishing (OIDC) -- no API token or secret.
|
|
4
|
+
# Fires when a GitHub release is published; the release tag should match the
|
|
5
|
+
# version in src/htmlsift/__init__.py (e.g. tag v0.1.1 for version 0.1.1).
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.13"
|
|
19
|
+
- name: Build sdist and wheel
|
|
20
|
+
run: |
|
|
21
|
+
python -m pip install --upgrade build
|
|
22
|
+
python -m build
|
|
23
|
+
- uses: actions/upload-artifact@v4
|
|
24
|
+
with:
|
|
25
|
+
name: dist
|
|
26
|
+
path: dist/
|
|
27
|
+
|
|
28
|
+
publish:
|
|
29
|
+
needs: build
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
environment: pypi
|
|
32
|
+
permissions:
|
|
33
|
+
id-token: write # required: lets PyPI mint a short-lived OIDC token
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/download-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: dist
|
|
38
|
+
path: dist/
|
|
39
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to htmlsift are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-11-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `extract()` accepts `bytes` and decodes them: byte order mark, then `encoding=`, then the
|
|
13
|
+
page's `<meta>` declaration, then utf-8, falling back to cp1252. Legacy charset labels map
|
|
14
|
+
the way browsers read them (`latin1` → cp1252, `shift_jis` → cp932, `gb2312` → gb18030).
|
|
15
|
+
- Keyword-only `encoding=` on `Extractor.extract` and `extract`, for a charset known from
|
|
16
|
+
an HTTP or WARC header.
|
|
17
|
+
- Dependency: `w3lib` (BSD-3-Clause) for charset sniffing.
|
|
18
|
+
- Keyword-only `base_url=` on `Extractor.extract` and `extract`: links and image URLs in
|
|
19
|
+
`html` and `markdown` output are made absolute against it. The page's first
|
|
20
|
+
`<base href>` is resolved against `base_url`; malformed links are left as they are.
|
|
21
|
+
`text` output and `with_blocks` are unaffected.
|
|
22
|
+
- `htmlsift extract` command: files, quoted globs or stdin in; the content of a single
|
|
23
|
+
input, or one JSON line per file (`--jsonl` to force), out. Flags `--output`,
|
|
24
|
+
`--mode`, `--device`, `--base-url`, `--encoding`. Failed files are reported as
|
|
25
|
+
records and the run exits with 1.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Input other than `str` or `bytes` raises `TypeError`; `extract(None)` previously
|
|
29
|
+
returned `""`.
|
|
30
|
+
- `encoding=` with `str` input raises `TypeError`, since a `str` is already decoded.
|
|
31
|
+
- A page with an absolute `<base href>` gets absolute links in `html` and `markdown` output
|
|
32
|
+
even without `base_url`. Previously `<base>` was dropped with `<head>`, leaving its links
|
|
33
|
+
relative.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Non-utf-8 `bytes` input crashed with `UnicodeDecodeError`.
|
|
37
|
+
- `html` and `markdown` output could drop images and other inline content inside a kept
|
|
38
|
+
block, varying from run to run on the same page. `text` output was unaffected.
|
|
39
|
+
|
|
40
|
+
## [0.1.1] - 2026-09-23
|
|
41
|
+
|
|
42
|
+
Documentation and packaging metadata only — no code or model changes since 0.1.0.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- Project URLs in package metadata (research repo, Hugging Face weights, issue tracker).
|
|
46
|
+
- PyPI Trusted Publishing workflow (`.github/workflows/publish.yml`).
|
|
47
|
+
- Cross-benchmark accuracy table (`base`/`mini` vs trafilatura, readability, resiliparse on
|
|
48
|
+
WMB, WCXB, DAnIEL) and an "Annotation is a policy" section in the README.
|
|
49
|
+
- GPU-on-Windows install note (CUDA torch from the PyTorch index).
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
- Stated the model-weight license (Apache-2.0) and added WebMainBench attribution.
|
|
53
|
+
- Cited the WebMainBench source paper (Liu et al., *Dripper*, arXiv:2511.23119).
|
|
54
|
+
- Clarified model shorthand and tightened copy in the README and the PyPI description.
|
|
55
|
+
|
|
56
|
+
## [0.1.0] - 2026-09-22
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- Initial release.
|
|
60
|
+
- `mini` model (int8 ONNX, runs on CPU) and `base` model (311M encoder, GPU-capable),
|
|
61
|
+
installed via `pip install htmlsift` and `pip install htmlsift[full]` respectively.
|
|
62
|
+
- `Extractor(mode, *, bundle_dir, threads, device)` with
|
|
63
|
+
`.extract(html, output=..., with_blocks=...)`, plus the module-level `extract(...)`.
|
|
64
|
+
- Output formats: plain text, HTML, and markdown.
|
|
65
|
+
- Device selection for `base`: `device="cuda"` / `"cpu"`, auto-detecting a GPU and
|
|
66
|
+
warning on CPU fallback.
|
|
67
|
+
- Model bundles fetched from the Hugging Face Hub, pinned to a fixed commit revision.
|
|
68
|
+
|
|
69
|
+
[Unreleased]: https://github.com/koivualeksi/htmlsift/compare/v0.2.0...HEAD
|
|
70
|
+
[0.2.0]: https://github.com/koivualeksi/htmlsift/compare/v0.1.1...v0.2.0
|
|
71
|
+
[0.1.1]: https://github.com/koivualeksi/htmlsift/compare/v0.1.0...v0.1.1
|
|
72
|
+
[0.1.0]: https://github.com/koivualeksi/htmlsift/releases/tag/v0.1.0
|
htmlsift-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: htmlsift
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Main-content extraction for web pages
|
|
5
|
+
Project-URL: Repository, https://github.com/koivualeksi/htmlsift
|
|
6
|
+
Project-URL: Research, https://github.com/koivualeksi/htmlsift-research
|
|
7
|
+
Project-URL: Model weights, https://huggingface.co/koivualeksi/htmlsift-release
|
|
8
|
+
Project-URL: Issues, https://github.com/koivualeksi/htmlsift/issues
|
|
9
|
+
Author-email: Aleksi Koivu <koivu.aleksi@gmail.com>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: boilerplate-removal,content-extraction,html,main-content,web-scraping
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: huggingface-hub
|
|
23
|
+
Requires-Dist: lxml
|
|
24
|
+
Requires-Dist: numpy
|
|
25
|
+
Requires-Dist: onnxruntime
|
|
26
|
+
Requires-Dist: tokenizers
|
|
27
|
+
Requires-Dist: w3lib<3,>=2.5
|
|
28
|
+
Provides-Extra: full
|
|
29
|
+
Requires-Dist: torch; extra == 'full'
|
|
30
|
+
Requires-Dist: transformers; extra == 'full'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# htmlsift
|
|
34
|
+
|
|
35
|
+
Web main-content extraction: HTML in, clean text / HTML / markdown out. Trained on
|
|
36
|
+
[WebMainBench](https://github.com/opendatalab/WebMainBench), scores each rendered line of a
|
|
37
|
+
page as main content or boilerplate (nav, ads, cookie banners, and footers) and returns just
|
|
38
|
+
the content.
|
|
39
|
+
|
|
40
|
+
Apache-2.0. Full documentation, diagrams, and benchmarks:
|
|
41
|
+
https://github.com/koivualeksi/htmlsift
|
|
42
|
+
|
|
43
|
+
## Results
|
|
44
|
+
|
|
45
|
+
| model | desc | WMB test F1 | seeds | CPU (pages/s) | GPU (pages/s) | install |
|
|
46
|
+
|---|---|---|---|---|---|---|
|
|
47
|
+
| `base` | 311M, 10 layers | **0.9311** | 3 | 0.4 | 24.6 | `htmlsift[full]` |
|
|
48
|
+
| unpublished | 97M, 6 layers, int8 QAT | 0.9256 | 3 | 0.9 | 30.2 | — |
|
|
49
|
+
| `mini` | int8 embedding table | 0.9010 | 3 | **27.7** | — | `htmlsift` |
|
|
50
|
+
|
|
51
|
+
GPU = RTX 4090, CPU = Ryzen 5 3600 single thread; WMB test = 544 pages (one of the 545 has
|
|
52
|
+
no gold reference). On WMB (in-domain), `mini` scores ~15 F1 points above trafilatura at
|
|
53
|
+
comparable CPU speed.
|
|
54
|
+
|
|
55
|
+
### Cross-benchmark accuracy (zero-shot)
|
|
56
|
+
|
|
57
|
+
Trained on WMB, scored on benchmarks the model never saw. Each column uses that benchmark's own metric.
|
|
58
|
+
|
|
59
|
+
| model | WMB (ROUGE-5) | WCXB (word-F1) | DAnIEL (ROUGE-L) |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| `base` | 0.9311 | 0.8633 | 0.9175 |
|
|
62
|
+
| `mini` | 0.9010 | 0.8474 | 0.8797 |
|
|
63
|
+
| trafilatura | 0.7525 | 0.8584 | 0.8265 |
|
|
64
|
+
| readability | 0.8016 | 0.7653 | 0.8925 |
|
|
65
|
+
| resiliparse | 0.7135 | 0.7909 | 0.7094 |
|
|
66
|
+
|
|
67
|
+
Model rows are 3-seed test means; heuristics are single deterministic runs. "Main content" is a labeling policy: htmlsift leads in-domain and stays competitive off-policy — retrain on your own labels if your definition differs. Details in the [research repo](https://github.com/koivualeksi/htmlsift-research).
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install htmlsift # default: the mini model (CPU, ONNX)
|
|
73
|
+
pip install htmlsift[full] # adds the base 311M model (torch; GPU-capable)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- `htmlsift` — the `mini` model only; few dependencies (onnxruntime + tokenizers + lxml +
|
|
77
|
+
numpy + huggingface-hub + w3lib). First use downloads ~230 MB of model files.
|
|
78
|
+
- `htmlsift[full]` — adds `torch` + `transformers` to run `base`. First use of `base`
|
|
79
|
+
downloads ~1 GB.
|
|
80
|
+
|
|
81
|
+
Requires Python 3.10+. Model files download from the Hugging Face Hub on first use and are
|
|
82
|
+
cached; run `htmlsift download <mode>` to prefetch, or set `HF_HUB_OFFLINE=1` to run from
|
|
83
|
+
cache only.
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from htmlsift import Extractor
|
|
89
|
+
|
|
90
|
+
ex = Extractor() # defaults to "mini"
|
|
91
|
+
ex.extract(html) # -> str (plain text)
|
|
92
|
+
ex.extract(html, output="html") # structure, links, images kept
|
|
93
|
+
ex.extract(html, output="markdown") # clean markdown: [text](url), 
|
|
94
|
+
ex.extract(html, with_blocks=True) # -> (blocks, probs)
|
|
95
|
+
|
|
96
|
+
import htmlsift
|
|
97
|
+
htmlsift.extract(html) # module-level convenience
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Raw bytes are decoded for you (byte order mark, then `encoding=`, then `<meta charset>`,
|
|
101
|
+
then utf-8 / cp1252):
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
ex.extract(response.content) # charset from the page itself
|
|
105
|
+
ex.extract(payload, encoding="windows-1251") # charset from the HTTP / WARC header
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Make links and image URLs absolute in `html` / `markdown` output (the page's `<base href>`
|
|
109
|
+
is honoured):
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
ex.extract(html, output="markdown", base_url="https://example.com/blog/post")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The larger model, from `htmlsift[full]`:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
ex = Extractor("base") # 311M encoder; GPU when present, else CPU (with a warning)
|
|
119
|
+
ex = Extractor("base", device="cuda") # require the GPU
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`mini` is CPU-only; only `base` runs on the GPU.
|
|
123
|
+
|
|
124
|
+
## Output modes
|
|
125
|
+
|
|
126
|
+
| `output` | what you get |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `text` (default) | selected content as plain text |
|
|
129
|
+
| `html` | selected DOM, with structure / links / images kept |
|
|
130
|
+
| `markdown` | clean markdown, links and images preserved |
|
|
131
|
+
|
|
132
|
+
## Command line
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
htmlsift extract page.html # main content of one page
|
|
136
|
+
curl -s https://example.com/a | htmlsift extract - --base-url https://example.com/a
|
|
137
|
+
htmlsift extract "crawl/*.html" --output markdown > out.jsonl # one JSON line per file
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Flags: `--output`, `--mode`, `--device`, `--base-url`, `--encoding`, `--jsonl` (records
|
|
141
|
+
even for a single file). Failed files become `{"path", "error"}` records and the exit code
|
|
142
|
+
is 1. Output is utf-8.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
Apache-2.0. Model weights are distributed from the Hugging Face Hub, also under Apache-2.0, and
|
|
147
|
+
are trained on [WebMainBench](https://github.com/opendatalab/WebMainBench) (Apache-2.0); please
|
|
148
|
+
attribute WebMainBench when you use them.
|
|
@@ -7,7 +7,7 @@ Main-content extraction for web pages: hand it a raw HTML string and get back th
|
|
|
7
7
|
Two models shipped, both trained on [WebMainBench][wmb] (WMB) — 7,825 labelled pages, custom split
|
|
8
8
|
6,548 train / 732 val / 545 test:
|
|
9
9
|
|
|
10
|
-
- **`mini`** (default) - a
|
|
10
|
+
- **`mini`** (default) - a fast int8-ONNX model that runs on CPU. `pip install htmlsift`
|
|
11
11
|
- **`base`** - a 311M encoder, most accurate and GPU-capable. `pip install htmlsift[full]`
|
|
12
12
|
|
|
13
13
|
Each model is fully re-trainable with your own annotated data and the whole research suite is available at [htmlsift-research](https://github.com/koivualeksi/htmlsift-research)
|
|
@@ -18,12 +18,13 @@ Each model is fully re-trainable with your own annotated data and the whole rese
|
|
|
18
18
|
|
|
19
19
|
| model | desc | WMB test F1 | seeds | CPU (pages/s) | GPU (pages/s) | install |
|
|
20
20
|
|---|---|---|---|---|---|---|
|
|
21
|
-
| `base` |
|
|
22
|
-
| unpublished |
|
|
23
|
-
| `mini` | int8 table | 0.9010 | 3 | **27.7** | — | `htmlsift` |
|
|
21
|
+
| `base` | 311M, 10 layers | **0.9311** | 3 | 0.4 | 24.6 | `htmlsift[full]` |
|
|
22
|
+
| unpublished | 97M, 6 layers, int8 QAT | 0.9256 | 3 | 0.9 | 30.2 | — |
|
|
23
|
+
| `mini` | int8 embedding table | 0.9010 | 3 | **27.7** | — | `htmlsift` |
|
|
24
24
|
|
|
25
25
|
* GPU = RTX 4090
|
|
26
26
|
* CPU = Ryzen 5 3600 single thread
|
|
27
|
+
* WMB test = 544 pages: one of the 545 test pages has no gold reference and is skipped in scoring.
|
|
27
28
|
|
|
28
29
|
### On CPU
|
|
29
30
|
|
|
@@ -36,21 +37,25 @@ per-page time (36 vs 31 ms median) and scores ~15 F1 points higher. readability
|
|
|
36
37
|
|
|
37
38
|

|
|
38
39
|
|
|
39
|
-
RTX 4090, WMB test (544 pages). `base`
|
|
40
|
+
RTX 4090, WMB test (544 pages). `base` sustains 24.6 pages/s against
|
|
40
41
|
MinerU-HTML v1.1's 1.16. A 21x gap at matched accuracy (F1 0.9311 vs 0.9306).
|
|
41
42
|
|
|
42
43
|
### Cross benchmark accuracy
|
|
43
44
|
|
|
44
45
|
The shipped models are trained on WMB, so WMB is in-domain. The other two columns are
|
|
45
46
|
**zero-shot**, the model never saw WCXB or the multilingual DAnIEL in training. Each column is
|
|
46
|
-
scored with that benchmark's own metric.
|
|
47
|
+
scored with that benchmark's own metric. `base` leads every heuristic here; `mini` lands just
|
|
48
|
+
behind the best on each board — see **Annotation is a policy** under Concept.
|
|
47
49
|
|
|
48
50
|
| model | WMB (ROUGE-5) | WCXB (word-F1, zero-shot) | DAnIEL (ROUGE-L, zero-shot) |
|
|
49
51
|
|---|---|---|---|
|
|
50
|
-
| `base`
|
|
51
|
-
| `mini`
|
|
52
|
+
| `base` | 0.9311 | 0.8633 | 0.9175 |
|
|
53
|
+
| `mini` | 0.9010 | 0.8474 | 0.8797 |
|
|
54
|
+
| trafilatura | 0.7525 | 0.8584 | 0.8265 |
|
|
55
|
+
| readability | 0.8016 | 0.7653 | 0.8925 |
|
|
56
|
+
| resiliparse | 0.7135 | 0.7909 | 0.7094 |
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
Model rows are 3-seed test means; heuristics are single deterministic runs.
|
|
54
59
|
|
|
55
60
|
## Installation
|
|
56
61
|
|
|
@@ -59,8 +64,8 @@ pip install htmlsift # default: the mini model (CPU, ONNX)
|
|
|
59
64
|
pip install htmlsift[full] # adds the base 311M model (torch; GPU-capable)
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
- **`htmlsift`** — the `mini` model only;
|
|
63
|
-
numpy + huggingface-hub). First use downloads ~230 MB of model files.
|
|
67
|
+
- **`htmlsift`** — the `mini` model only; few dependencies (onnxruntime + tokenizers + lxml +
|
|
68
|
+
numpy + huggingface-hub + w3lib). First use downloads ~230 MB of model files.
|
|
64
69
|
- **`htmlsift[full]`** — adds `torch` + `transformers` to run `base`. First use of `base`
|
|
65
70
|
downloads ~1 GB.
|
|
66
71
|
|
|
@@ -87,6 +92,36 @@ import htmlsift
|
|
|
87
92
|
htmlsift.extract(html) # default mini
|
|
88
93
|
```
|
|
89
94
|
|
|
95
|
+
### Bytes and encodings
|
|
96
|
+
|
|
97
|
+
Pass raw bytes from a file, an HTTP response or a WARC record, and htmlsift decodes them:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
ex.extract(response.content) # charset from the page itself
|
|
101
|
+
ex.extract(payload, encoding="windows-1251") # charset from the HTTP / WARC header
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The charset is taken from, in order: a byte order mark, `encoding=`, the page's
|
|
105
|
+
`<meta charset>`; an undeclared page is read as utf-8, falling back to cp1252. Unknown
|
|
106
|
+
labels are skipped and undecodable bytes become `�`. A `str` counts as already decoded, so
|
|
107
|
+
`encoding=` with a `str` raises `TypeError`. Prefer passing bytes over a `str` decoded
|
|
108
|
+
upstream: `requests`' `response.text`, for one, guesses ISO-8859-1 for pages without a
|
|
109
|
+
charset header.
|
|
110
|
+
|
|
111
|
+
### Absolute links
|
|
112
|
+
|
|
113
|
+
Pass the page's URL to make links and image URLs absolute in `html` and `markdown` output:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
ex.extract(html, output="markdown", base_url="https://example.com/blog/post")
|
|
117
|
+
# [the report](report) -> [the report](https://example.com/blog/report)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The page's first `<base href>` is resolved against `base_url`, as a browser does. Without
|
|
121
|
+
`base_url`, an absolute `<base href>` is used on its own; with neither, links stay as they
|
|
122
|
+
are. Malformed links are left unchanged. `text` output has no links, so it ignores
|
|
123
|
+
`base_url`.
|
|
124
|
+
|
|
90
125
|
The larger model, from the `[full]` install:
|
|
91
126
|
|
|
92
127
|
```python
|
|
@@ -131,6 +166,35 @@ Composite image showing the global distribution of photosynthesis, including bot
|
|
|
131
166
|
| `markdown` | clean markdown, links and images preserved |
|
|
132
167
|
|
|
133
168
|
|
|
169
|
+
## Command line
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
htmlsift extract page.html # main content of one page
|
|
173
|
+
htmlsift extract page.html --output markdown --base-url https://example.com/page
|
|
174
|
+
curl -s https://example.com/a | htmlsift extract - --base-url https://example.com/a
|
|
175
|
+
htmlsift extract "crawl/*.html" > out.jsonl # one JSON record per file
|
|
176
|
+
htmlsift extract "crawl/**/*.html" --mode base --device cuda > out.jsonl
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
One file or `-` (stdin) prints the content itself. Several files or a glob print one JSON
|
|
180
|
+
line per file, `{"path": ..., "content": ...}`, or `{"path": ..., "error": ...}` for a file
|
|
181
|
+
that failed; the run continues and exits with 1 at the end. Pass `--jsonl` to get records
|
|
182
|
+
for a single file too, which scripts should do: bash expands an unquoted `*.html` before
|
|
183
|
+
htmlsift sees it. Quoted globs are expanded by htmlsift itself (`**` recurses), so they
|
|
184
|
+
work the same in every shell; directories are not walked.
|
|
185
|
+
|
|
186
|
+
| flag | |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `--output text\|html\|markdown` | as `output=` above (default `text`) |
|
|
189
|
+
| `--mode mini\|base`, `--device` | model and torch device, as for `Extractor` |
|
|
190
|
+
| `--base-url URL` | make links absolute; applies to every input |
|
|
191
|
+
| `--encoding ENC` | charset of the input bytes; applies to every input |
|
|
192
|
+
| `--jsonl` | always print JSON records |
|
|
193
|
+
|
|
194
|
+
Files are read as bytes, so the charset is detected as described above. Output is utf-8.
|
|
195
|
+
Exit codes: 0 ok, 1 a file or the model failed, 2 a usage error. In Windows PowerShell 5.1,
|
|
196
|
+
`>` re-encodes a program's output; redirect from bash, cmd or PowerShell 7 instead.
|
|
197
|
+
|
|
134
198
|
## Concept
|
|
135
199
|
|
|
136
200
|
<details>
|
|
@@ -161,6 +225,19 @@ Both tiers share the render, the per-line pooling, and the BiGRU head. They diff
|
|
|
161
225
|
- base uses granite r2 311m embedder using only first 10 layers requiring GPU for efficient usage
|
|
162
226
|
|
|
163
227
|
|
|
228
|
+
</details>
|
|
229
|
+
|
|
230
|
+
<details>
|
|
231
|
+
<summary><b>Annotation is a policy</b></summary>
|
|
232
|
+
|
|
233
|
+

|
|
234
|
+
|
|
235
|
+
htmlsift learns what counts as main content from a benchmark's labels, so it's most accurate on pages that follow that policy. On WebMainBench (the benchmark the shipped models train on) it beats the common heuristic extractors by a wide margin. Zero-shot on benchmarks with different policies, the gap narrows: the 311M `base` still edges every heuristic tested (trafilatura, readability, resiliparse) on both WCXB and DAnIEL, and `mini` lands just behind the strongest heuristic on each board, ahead of the rest.
|
|
236
|
+
|
|
237
|
+
Train htmlsift on a different policy and it turns around. Fit it to WCXB instead of WMB and it tops WCXB but loses points on other benchmarks. For example when training `base` model on WCXB data, WMB falls to 0.8479, DAnIEL to 0.8826 while WCXB climbs up to 0.9209. Whichever policy you train on, htmlsift leads there and pays for it elsewhere.
|
|
238
|
+
|
|
239
|
+
For more information, review the [htmlsift-research](https://github.com/koivualeksi/htmlsift-research) readme.
|
|
240
|
+
|
|
164
241
|
</details>
|
|
165
242
|
|
|
166
243
|
<details>
|
|
@@ -183,20 +260,25 @@ and its `docs/REPRODUCE.md`.
|
|
|
183
260
|
A standalone hero image that is its own block renders to no text line, so it can't be selected
|
|
184
261
|
today. Inline images inside a kept block are preserved in `html` and `markdown`.
|
|
185
262
|
|
|
263
|
+
`base_url` does not rewrite `srcset`, so responsive images in `html` output keep relative
|
|
264
|
+
candidates (`src` is rewritten; `markdown` uses only `src`).
|
|
265
|
+
|
|
186
266
|
</details>
|
|
187
267
|
|
|
188
268
|
## License
|
|
189
269
|
|
|
190
270
|
Apache-2.0. See [LICENSE](LICENSE).
|
|
191
271
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
272
|
+
Model weights are distributed from the Hugging Face Hub, also under Apache-2.0, and are
|
|
273
|
+
trained on [WebMainBench](https://github.com/opendatalab/WebMainBench) (Apache-2.0); please
|
|
274
|
+
attribute WebMainBench when you use them.
|
|
195
275
|
|
|
196
276
|
## References
|
|
197
277
|
|
|
198
278
|
- **WMB — WebMainBench.** The training benchmark; ROUGE-5 main-content metric.
|
|
199
|
-
|
|
279
|
+
Liu et al., *Dripper: Token-Efficient Main HTML Extraction with a Lightweight LM*.
|
|
280
|
+
[arXiv:2511.23119](https://arxiv.org/abs/2511.23119) ·
|
|
281
|
+
[repo](https://github.com/opendatalab/WebMainBench)
|
|
200
282
|
- **WCXB — Web Content Extraction Benchmark.** Foley, *WCXB: A Multi-Type Web Content Extraction
|
|
201
283
|
Benchmark*. [arXiv:2605.21097](https://arxiv.org/abs/2605.21097) ·
|
|
202
284
|
[repo](https://github.com/Murrough-Foley/web-content-extraction-benchmark)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# htmlsift
|
|
2
|
+
|
|
3
|
+
Web main-content extraction: HTML in, clean text / HTML / markdown out. Trained on
|
|
4
|
+
[WebMainBench](https://github.com/opendatalab/WebMainBench), scores each rendered line of a
|
|
5
|
+
page as main content or boilerplate (nav, ads, cookie banners, and footers) and returns just
|
|
6
|
+
the content.
|
|
7
|
+
|
|
8
|
+
Apache-2.0. Full documentation, diagrams, and benchmarks:
|
|
9
|
+
https://github.com/koivualeksi/htmlsift
|
|
10
|
+
|
|
11
|
+
## Results
|
|
12
|
+
|
|
13
|
+
| model | desc | WMB test F1 | seeds | CPU (pages/s) | GPU (pages/s) | install |
|
|
14
|
+
|---|---|---|---|---|---|---|
|
|
15
|
+
| `base` | 311M, 10 layers | **0.9311** | 3 | 0.4 | 24.6 | `htmlsift[full]` |
|
|
16
|
+
| unpublished | 97M, 6 layers, int8 QAT | 0.9256 | 3 | 0.9 | 30.2 | — |
|
|
17
|
+
| `mini` | int8 embedding table | 0.9010 | 3 | **27.7** | — | `htmlsift` |
|
|
18
|
+
|
|
19
|
+
GPU = RTX 4090, CPU = Ryzen 5 3600 single thread; WMB test = 544 pages (one of the 545 has
|
|
20
|
+
no gold reference). On WMB (in-domain), `mini` scores ~15 F1 points above trafilatura at
|
|
21
|
+
comparable CPU speed.
|
|
22
|
+
|
|
23
|
+
### Cross-benchmark accuracy (zero-shot)
|
|
24
|
+
|
|
25
|
+
Trained on WMB, scored on benchmarks the model never saw. Each column uses that benchmark's own metric.
|
|
26
|
+
|
|
27
|
+
| model | WMB (ROUGE-5) | WCXB (word-F1) | DAnIEL (ROUGE-L) |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| `base` | 0.9311 | 0.8633 | 0.9175 |
|
|
30
|
+
| `mini` | 0.9010 | 0.8474 | 0.8797 |
|
|
31
|
+
| trafilatura | 0.7525 | 0.8584 | 0.8265 |
|
|
32
|
+
| readability | 0.8016 | 0.7653 | 0.8925 |
|
|
33
|
+
| resiliparse | 0.7135 | 0.7909 | 0.7094 |
|
|
34
|
+
|
|
35
|
+
Model rows are 3-seed test means; heuristics are single deterministic runs. "Main content" is a labeling policy: htmlsift leads in-domain and stays competitive off-policy — retrain on your own labels if your definition differs. Details in the [research repo](https://github.com/koivualeksi/htmlsift-research).
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install htmlsift # default: the mini model (CPU, ONNX)
|
|
41
|
+
pip install htmlsift[full] # adds the base 311M model (torch; GPU-capable)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `htmlsift` — the `mini` model only; few dependencies (onnxruntime + tokenizers + lxml +
|
|
45
|
+
numpy + huggingface-hub + w3lib). First use downloads ~230 MB of model files.
|
|
46
|
+
- `htmlsift[full]` — adds `torch` + `transformers` to run `base`. First use of `base`
|
|
47
|
+
downloads ~1 GB.
|
|
48
|
+
|
|
49
|
+
Requires Python 3.10+. Model files download from the Hugging Face Hub on first use and are
|
|
50
|
+
cached; run `htmlsift download <mode>` to prefetch, or set `HF_HUB_OFFLINE=1` to run from
|
|
51
|
+
cache only.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from htmlsift import Extractor
|
|
57
|
+
|
|
58
|
+
ex = Extractor() # defaults to "mini"
|
|
59
|
+
ex.extract(html) # -> str (plain text)
|
|
60
|
+
ex.extract(html, output="html") # structure, links, images kept
|
|
61
|
+
ex.extract(html, output="markdown") # clean markdown: [text](url), 
|
|
62
|
+
ex.extract(html, with_blocks=True) # -> (blocks, probs)
|
|
63
|
+
|
|
64
|
+
import htmlsift
|
|
65
|
+
htmlsift.extract(html) # module-level convenience
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Raw bytes are decoded for you (byte order mark, then `encoding=`, then `<meta charset>`,
|
|
69
|
+
then utf-8 / cp1252):
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
ex.extract(response.content) # charset from the page itself
|
|
73
|
+
ex.extract(payload, encoding="windows-1251") # charset from the HTTP / WARC header
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Make links and image URLs absolute in `html` / `markdown` output (the page's `<base href>`
|
|
77
|
+
is honoured):
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
ex.extract(html, output="markdown", base_url="https://example.com/blog/post")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The larger model, from `htmlsift[full]`:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
ex = Extractor("base") # 311M encoder; GPU when present, else CPU (with a warning)
|
|
87
|
+
ex = Extractor("base", device="cuda") # require the GPU
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`mini` is CPU-only; only `base` runs on the GPU.
|
|
91
|
+
|
|
92
|
+
## Output modes
|
|
93
|
+
|
|
94
|
+
| `output` | what you get |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `text` (default) | selected content as plain text |
|
|
97
|
+
| `html` | selected DOM, with structure / links / images kept |
|
|
98
|
+
| `markdown` | clean markdown, links and images preserved |
|
|
99
|
+
|
|
100
|
+
## Command line
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
htmlsift extract page.html # main content of one page
|
|
104
|
+
curl -s https://example.com/a | htmlsift extract - --base-url https://example.com/a
|
|
105
|
+
htmlsift extract "crawl/*.html" --output markdown > out.jsonl # one JSON line per file
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Flags: `--output`, `--mode`, `--device`, `--base-url`, `--encoding`, `--jsonl` (records
|
|
109
|
+
even for a single file). Failed files become `{"path", "error"}` records and the exit code
|
|
110
|
+
is 1. Output is utf-8.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
Apache-2.0. Model weights are distributed from the Hugging Face Hub, also under Apache-2.0, and
|
|
115
|
+
are trained on [WebMainBench](https://github.com/opendatalab/WebMainBench) (Apache-2.0); please
|
|
116
|
+
attribute WebMainBench when you use them.
|