Skip to content

Commit 9d991bd

Browse files
authored
Merge pull request #70 from e06084/fix-issue-68-trafilatura-metrics
Fix HTML annotation cleanup and Trafilatura evaluation
2 parents 77da09c + 5250436 commit 9d991bd

15 files changed

Lines changed: 282 additions & 61 deletions

README.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,10 @@ Results from the [Dripper paper](https://arxiv.org/abs/2511.23119) (Table 2):
113113
| Extractor | Version | overall | text\_edit | code\_edit | formula\_edit | table\_edit | table\_TEDS |
114114
|---|---|---|---|---|---|---|---|
115115
| **mineru-html** | 4.1.1 | **0.8256** | 0.8621 | 0.9093 | 0.9399 | 0.6780 | 0.7388 |
116-
| magic-html | 0.1.5 | 0.5141 | 0.7791 | 0.4117 | 0.7204 | 0.2611 | 0.3984 |
117-
| trafilatura (md) | 2.0.0 | 0.3858 | 0.6887 | 0.1305 | 0.6242 | 0.1653 | 0.3203 |
118-
| resiliparse | 0.14.5 | 0.2954 | 0.7381 | 0.0641 | 0.6747 | 0.0000 | 0.0000 |
119-
| trafilatura (txt) | 2.0.0 | 0.2657 | 0.7126 | 0.0000 | 0.6162 | 0.0000 | 0.0000 |
116+
| magic-html | 0.1.5 | 0.4996 | 0.7800 | 0.4150 | 0.6385 | 0.2638 | 0.4006 |
117+
| trafilatura (md) | 2.0.0 | 0.4013 | 0.7826 | 0.1801 | 0.6237 | 0.1202 | 0.2999 |
118+
| resiliparse | 0.14.5 | 0.2898 | 0.7435 | 0.0422 | 0.6631 | 0.0000 | 0.0000 |
119+
| trafilatura (txt) | 2.0.0 | 0.3718 | 0.7819 | 0.0000 | 0.6389 | 0.1278 | 0.3106 |
120120

121121
Contributions of new extractor results are welcome — open a PR!
122122

@@ -194,13 +194,23 @@ cp .env.example .env
194194
# Edit .env and set LLM_BASE_URL, LLM_API_KEY, LLM_MODEL
195195
```
196196

197+
When constructing an evaluator manually, pass the same LLM settings to both `llm_config` and `metric_config`; `llm_config` validates the API, while `metric_config` enables LLM-enhanced metric splitting.
198+
197199
#### Run an Evaluation
198200

199201
```python
202+
import os
200203
from webmainbench import DataLoader, Evaluator, ExtractorFactory
201204

202205
dataset = DataLoader.load_jsonl("data/WebMainBench_545.jsonl")
203-
result = Evaluator().evaluate(dataset, ExtractorFactory.create("trafilatura"))
206+
llm_config = {
207+
"use_llm": True,
208+
"llm_base_url": os.getenv("LLM_BASE_URL", ""),
209+
"llm_api_key": os.getenv("LLM_API_KEY", ""),
210+
"llm_model": os.getenv("LLM_MODEL", "deepseek-chat"),
211+
}
212+
evaluator = Evaluator(llm_config=llm_config, metric_config=llm_config)
213+
result = evaluator.evaluate(dataset, ExtractorFactory.create("trafilatura"))
204214

205215
m = result.overall_metrics
206216

@@ -217,7 +227,16 @@ for name, result in results.items():
217227
print(f"{name}: {result.overall_metrics['overall']:.4f}")
218228
```
219229

220-
A complete example is available at `examples/multi_extractor_compare.py`.
230+
To reproduce the 545-sample fine-grained leaderboard:
231+
232+
```bash
233+
export LLM_BASE_URL="https://your-openai-compatible-endpoint/v1"
234+
export LLM_API_KEY="..."
235+
export LLM_MODEL="gpt-5-chat-latest"
236+
python examples/run_545_leaderboard.py data/WebMainBench_545.jsonl
237+
```
238+
239+
Complete examples are available at `examples/run_545_leaderboard.py` and `examples/multi_extractor_compare.py`.
221240

222241
## Dataset Format
223242

README_zh.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,10 @@ WebMainBench 支持两套互补的评测协议:
113113
| 抽取器 | 版本 | overall | text\_edit | code\_edit | formula\_edit | table\_edit | table\_TEDS |
114114
|---|---|---|---|---|---|---|---|
115115
| **mineru-html** | 4.1.1 | **0.8256** | 0.8621 | 0.9093 | 0.9399 | 0.6780 | 0.7388 |
116-
| magic-html | 0.1.5 | 0.5141 | 0.7791 | 0.4117 | 0.7204 | 0.2611 | 0.3984 |
117-
| trafilatura (md) | 2.0.0 | 0.3858 | 0.6887 | 0.1305 | 0.6242 | 0.1653 | 0.3203 |
118-
| resiliparse | 0.14.5 | 0.2954 | 0.7381 | 0.0641 | 0.6747 | 0.0000 | 0.0000 |
119-
| trafilatura (txt) | 2.0.0 | 0.2657 | 0.7126 | 0.0000 | 0.6162 | 0.0000 | 0.0000 |
116+
| magic-html | 0.1.5 | 0.4996 | 0.7800 | 0.4150 | 0.6385 | 0.2638 | 0.4006 |
117+
| trafilatura (md) | 2.0.0 | 0.4013 | 0.7826 | 0.1801 | 0.6237 | 0.1202 | 0.2999 |
118+
| resiliparse | 0.14.5 | 0.2898 | 0.7435 | 0.0422 | 0.6631 | 0.0000 | 0.0000 |
119+
| trafilatura (txt) | 2.0.0 | 0.3718 | 0.7819 | 0.0000 | 0.6389 | 0.1278 | 0.3106 |
120120

121121
欢迎提交新抽取器的评测结果 — 请提 PR!
122122

@@ -194,13 +194,23 @@ cp .env.example .env
194194
# 编辑 .env,设置 LLM_BASE_URL、LLM_API_KEY、LLM_MODEL
195195
```
196196

197+
手动构造评测器时,需要把同一份 LLM 配置同时传给 `llm_config``metric_config``llm_config` 用于校验 API,`metric_config` 用于启用 LLM 增强的指标拆分。
198+
197199
#### 运行评测
198200

199201
```python
202+
import os
200203
from webmainbench import DataLoader, Evaluator, ExtractorFactory
201204

202205
dataset = DataLoader.load_jsonl("data/WebMainBench_545.jsonl")
203-
result = Evaluator().evaluate(dataset, ExtractorFactory.create("trafilatura"))
206+
llm_config = {
207+
"use_llm": True,
208+
"llm_base_url": os.getenv("LLM_BASE_URL", ""),
209+
"llm_api_key": os.getenv("LLM_API_KEY", ""),
210+
"llm_model": os.getenv("LLM_MODEL", "deepseek-chat"),
211+
}
212+
evaluator = Evaluator(llm_config=llm_config, metric_config=llm_config)
213+
result = evaluator.evaluate(dataset, ExtractorFactory.create("trafilatura"))
204214

205215
m = result.overall_metrics
206216

@@ -217,7 +227,16 @@ for name, result in results.items():
217227
print(f"{name}: {result.overall_metrics['overall']:.4f}")
218228
```
219229

220-
完整示例见 `examples/multi_extractor_compare.py`
230+
复现 545 条子集细粒度榜单:
231+
232+
```bash
233+
export LLM_BASE_URL="https://your-openai-compatible-endpoint/v1"
234+
export LLM_API_KEY="..."
235+
export LLM_MODEL="gpt-5-chat-latest"
236+
python examples/run_545_leaderboard.py data/WebMainBench_545.jsonl
237+
```
238+
239+
完整示例见 `examples/run_545_leaderboard.py``examples/multi_extractor_compare.py`
221240

222241
## 数据格式
223242

examples/run_545_leaderboard.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Run the 545-sample fine-grained leaderboard.
2+
3+
Required environment variables for LLM-enhanced formula splitting:
4+
LLM_BASE_URL
5+
LLM_API_KEY
6+
LLM_MODEL
7+
8+
Example:
9+
python examples/run_545_leaderboard.py WebMainBench_545.jsonl
10+
"""
11+
12+
import os
13+
import sys
14+
from pathlib import Path
15+
16+
from webmainbench import DataLoader, Evaluator
17+
18+
19+
METRICS = [
20+
"overall",
21+
"text_edit",
22+
"code_edit",
23+
"formula_edit",
24+
"table_edit",
25+
"table_TEDS",
26+
]
27+
28+
29+
def build_llm_config() -> dict:
30+
config = {
31+
"use_llm": os.getenv("USE_LLM", "true").lower() == "true",
32+
"llm_base_url": os.getenv("LLM_BASE_URL", ""),
33+
"llm_api_key": os.getenv("LLM_API_KEY", ""),
34+
"llm_model": os.getenv("LLM_MODEL", "deepseek-chat"),
35+
"llm_timeout": float(os.getenv("LLM_TIMEOUT", "60")),
36+
}
37+
if os.getenv("LLM_CACHE_DIR"):
38+
config["cache_dir"] = os.getenv("LLM_CACHE_DIR")
39+
return config
40+
41+
42+
def main() -> None:
43+
dataset_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("data/WebMainBench_545.jsonl")
44+
extractors = sys.argv[2:] or ["magic-html", "trafilatura", "resiliparse", "trafilatura_txt"]
45+
llm_config = build_llm_config()
46+
47+
dataset = DataLoader.load_jsonl(dataset_path)
48+
evaluator = Evaluator(llm_config=llm_config, metric_config=llm_config)
49+
results = evaluator.compare_extractors(dataset, extractors)
50+
51+
print("| Extractor | " + " | ".join(METRICS) + " |")
52+
print("|---|" + "|".join(["---:"] * len(METRICS)) + "|")
53+
for name, result in results.items():
54+
scores = [result.overall_metrics.get(metric, 0.0) for metric in METRICS]
55+
print(f"| {name} | " + " | ".join(f"{score:.4f}" for score in scores) + " |")
56+
57+
58+
if __name__ == "__main__":
59+
main()

tests/test_html_cleaner.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from webmainbench.extractors.base import BaseExtractor, ExtractionResult
2+
from webmainbench.utils import clean_browser_annotation_artifacts
3+
4+
5+
class EchoExtractor(BaseExtractor):
6+
def _setup(self):
7+
pass
8+
9+
def _extract_content(self, html: str, url: str = None) -> ExtractionResult:
10+
return ExtractionResult(content=html)
11+
12+
13+
def test_clean_browser_annotation_artifacts_preserves_text():
14+
html = (
15+
'<p data-anno-uid="anno-1">'
16+
'<marked-text data-anno-uid="anno-2">Hello</marked-text>'
17+
'<span> </span>'
18+
"<marked-tail data-anno-uid='anno-3'>world</marked-tail>"
19+
"</p>"
20+
)
21+
22+
cleaned = clean_browser_annotation_artifacts(html)
23+
24+
assert "Hello" in cleaned
25+
assert "world" in cleaned
26+
assert "marked-text" not in cleaned
27+
assert "marked-tail" not in cleaned
28+
assert "data-anno-uid" not in cleaned
29+
30+
31+
def test_base_extractor_cleans_annotation_artifacts_by_default():
32+
extractor = EchoExtractor("echo")
33+
34+
result = extractor.extract('<p><marked-text data-anno-uid="x">Hello</marked-text></p>')
35+
36+
assert result.content == "<p>Hello</p>"
37+
38+
39+
def test_base_extractor_can_disable_annotation_cleanup():
40+
extractor = EchoExtractor("echo", config={"clean_html_annotations": False})
41+
html = '<p><marked-text data-anno-uid="x">Hello</marked-text></p>'
42+
43+
result = extractor.extract(html)
44+
45+
assert result.content == html

tests/test_metric_config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from webmainbench.metrics import MetricCalculator
2+
3+
4+
def test_metric_calculator_passes_config_to_default_metrics():
5+
config = {
6+
"use_llm": True,
7+
"llm_base_url": "http://example.test/v1",
8+
"llm_api_key": "test-key",
9+
"llm_model": "test-model",
10+
}
11+
12+
calculator = MetricCalculator(config)
13+
14+
assert calculator.metrics["formula_edit"].config == config
15+
assert calculator.metrics["text_edit"].config == config

tests/test_trafilatura_config.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from webmainbench.extractors.trafilatura_extractor import (
2+
TrafilaturaExtractor as MarkdownTrafilaturaExtractor,
3+
)
4+
from webmainbench.extractors.trafilatura_txt_extractor import (
5+
TrafilaturaExtractor as TextTrafilaturaExtractor,
6+
)
7+
8+
9+
def test_trafilatura_markdown_defaults_match_standard_options():
10+
extractor = MarkdownTrafilaturaExtractor("trafilatura")
11+
12+
assert extractor.inference_config.favor_precision is False
13+
assert extractor.inference_config.favor_recall is False
14+
assert extractor.inference_config.include_comments is True
15+
assert extractor.inference_config.output_format == "markdown"
16+
17+
18+
def test_trafilatura_txt_defaults_to_extract_txt(monkeypatch):
19+
calls = {}
20+
21+
def fake_extract(html, **kwargs):
22+
calls["html"] = html
23+
calls["kwargs"] = kwargs
24+
return "plain text"
25+
26+
monkeypatch.setattr(
27+
"webmainbench.extractors.trafilatura_txt_extractor.extract",
28+
fake_extract,
29+
)
30+
extractor = TextTrafilaturaExtractor("trafilatura_txt")
31+
32+
result = extractor.extract("<html><body>plain text</body></html>", url="https://example.com")
33+
34+
assert result.content == "plain text"
35+
assert calls["kwargs"]["url"] == "https://example.com"
36+
assert calls["kwargs"]["favor_precision"] is False
37+
assert calls["kwargs"]["favor_recall"] is False
38+
assert calls["kwargs"]["include_comments"] is True
39+
assert calls["kwargs"]["output_format"] == "txt"

webmainbench/evaluator/evaluator.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ def _validate_llm_config(self, llm_config: Dict[str, Any] = None):
137137
print("Validating LLM API configuration...")
138138
client = OpenAI(
139139
base_url=config.get('llm_base_url'),
140-
api_key=config.get('llm_api_key')
140+
api_key=config.get('llm_api_key'),
141+
timeout=config.get('llm_timeout', 60),
141142
)
142143

143144
client.chat.completions.create(
@@ -615,4 +616,4 @@ def compare_extractors(self,
615616
print(f"Error evaluating {extractor_name}: {e}")
616617
continue
617618

618-
return results
619+
return results

webmainbench/extractors/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Dict, List, Any, Optional, Union
88
import time
99
import traceback
10+
from ..utils.html_cleaner import clean_browser_annotation_artifacts
1011

1112

1213
@dataclass
@@ -154,6 +155,9 @@ def extract(self, html: str, url: str = None) -> ExtractionResult:
154155
extraction_time=time.time() - start_time
155156
)
156157

158+
if self.config.get("clean_html_annotations", True):
159+
html = clean_browser_annotation_artifacts(html)
160+
157161
# Perform extraction
158162
result = self._extract_content(html, url)
159163
result.extraction_time = time.time() - start_time
@@ -213,4 +217,4 @@ def __str__(self) -> str:
213217
return f"{self.__class__.__name__}(name='{self.name}')"
214218

215219
def __repr__(self) -> str:
216-
return self.__str__()
220+
return self.__str__()

webmainbench/extractors/trafilatura_extractor.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
@dataclass
1414
class TrafilaturaInferenceConfig:
1515
"""Configuration for Trafilatura extractor."""
16-
favor_precision: bool = True # Favor precision: only extract the most core content, filter more redundancy (e.g. sidebars, ads), enabled by default
17-
favor_recall: bool = True # Favor recall: extract all potentially valid content as much as possible, minimize omissions, enabled by default
18-
include_comments: bool = False # Whether to keep comments, disabled by default
19-
include_tables: bool = True # Whether to keep extracted HTML tables, enabled by default
16+
favor_precision: bool = False # Match trafilatura.extract default
17+
favor_recall: bool = False # Match trafilatura.extract default
18+
include_comments: bool = True # Match trafilatura.extract default
19+
include_tables: bool = True # Match trafilatura.extract default
2020
include_images: bool = False # Whether to keep extracted image information, disabled by default
2121
include_links: bool = False # Whether to keep links, disabled by default
2222
with_metadata: bool = False # Whether to keep metadata, disabled by default
2323
skip_elements: bool = False # Whether to keep CSS-hidden elements, disabled by default
24-
output_format: str = "markdown" # Supports multiple output formats: "csv", "json", "html", "markdown", "txt", "xml", etc.
24+
output_format: str = "markdown" # Markdown benchmark variant; trafilatura's library default is "txt"
2525

2626

2727
@extractor("trafilatura")

0 commit comments

Comments
 (0)