Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rs-trafilatura"
version = "0.2.2"
version = "0.3.0"
edition = "2021"
rust-version = "1.85"
authors = ["Murrough Foley"]
Expand All @@ -21,7 +21,7 @@ dom_query = "0.24" # Primary DOM manipulation
tendril = "0.4" # Zero-copy text operations via StrTendril
thiserror = "2.0"
regex = "1.11"
chrono = "0.4"
chrono = { version = "0.4", features = ["serde"] }
encoding_rs = "0.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Fast and accurate web content extraction in Rust.

A high-performance Rust port of [trafilatura](https://github.com/adbar/trafilatura) / [go-trafilatura](https://github.com/markusmobius/go-trafilatura), extracting clean, readable content from web pages while removing boilerplate, navigation, and advertisements.

## Project Status

The crate is suitable for local services, batch extraction, and pipelines that can tolerate occasional extraction misses and use `extraction_quality` to route low-confidence pages to a fallback. For production-facing workloads, validate against your own URL corpus, cap output sizes, and keep browser/LLM fallbacks for JavaScript-heavy or low-confidence pages.

The page-type classifier and extraction-quality predictor are Rust-specific additions. They are not part of upstream Python Trafilatura; they route pages to tuned extraction profiles and expose confidence signals for fallback decisions.

## Features

- **Fast**: 71 files/s for articles, 46 files/s overall on a 1,497-page benchmark (pure Rust, compile-time regex)
Expand All @@ -13,7 +19,7 @@ A high-performance Rust port of [trafilatura](https://github.com/adbar/trafilatu
- **Extraction Quality Predictor**: ML-based confidence scoring (0.0-1.0) using a 27-feature XGBoost model that predicts extraction F1 — pages below 0.80 are candidates for LLM fallback
- **Markdown Output**: GitHub Flavored Markdown preserving headings, lists, tables, bold/italic, code blocks
- **Rich Metadata**: Title, author, date, description, categories, tags, license, images from JSON-LD, Open Graph, Dublin Core, and HTML meta tags
- **Configurable**: 28 options to tune precision/recall tradeoff, content selection, and output format
- **Configurable**: 30 options to tune precision/recall tradeoff, content selection, and output format
- **Robust**: Handles malformed HTML gracefully with automatic character encoding detection (UTF-8, ISO-8859-1, Windows-1252)

## Quick Start
Expand Down
88 changes: 87 additions & 1 deletion benches/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//! - Real-world HTML files from benchmark dataset for realistic performance

use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use rs_trafilatura::{extract, extract_with_options, Options};
use rs_trafilatura::{baseline, extract, extract_with_options, html2txt, Options};
use std::fs;

const SAMPLE_HTML: &str = r#"
Expand Down Expand Up @@ -48,6 +48,66 @@ const SAMPLE_HTML: &str = r#"
</html>
"#;

const JSON_LD_HTML: &str = r#"
<!DOCTYPE html>
<html>
<head>
<title>Structured Data Article</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": {
"@type": "Question",
"name": "How does extraction work?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Rust extraction parses the DOM, scores content candidates, and falls back to structured JSON-LD text when visible content is not sufficient."
}
},
"recipeInstructions": [
{"text": "Parse the document once."},
{"itemListElement": [{"text": "Collect structured body text."}]}
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"description": "This product teaser is available only as structured metadata and should be kept as fallback text."
}
</script>
</head>
<body><nav>Home Products Cart</nav></body>
</html>
"#;

const METADATA_ONLY_JSON_LD_HTML: &str = r#"
<!DOCTYPE html>
<html>
<head>
<title>Metadata Only</title>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Organization","name":"Example","url":"https://example.com"}
</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"name":"Home"},{"name":"Section"}]}
</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"WebSite","name":"Example","potentialAction":{"@type":"SearchAction"}}
</script>
</head>
<body>
<article>
<p>Fallback should eventually use visible article text when metadata-only JSON-LD contains no content body.</p>
<p>This benchmark measures the cost of scanning irrelevant structured data scripts.</p>
<p>The optimized path should skip JSON parsing when no content-bearing hook is present.</p>
</article>
</body>
</html>
"#;

fn bench_extract_default(c: &mut Criterion) {
c.bench_function("extract_default", |b| {
b.iter(|| extract(black_box(SAMPLE_HTML)));
Expand All @@ -66,6 +126,31 @@ fn bench_extract_with_options(c: &mut Criterion) {
});
}

fn bench_official_api_helpers(c: &mut Criterion) {
let mut group = c.benchmark_group("official_api");
group.throughput(Throughput::Bytes(SAMPLE_HTML.len() as u64));

group.bench_function("baseline_sample", |b| {
b.iter(|| baseline(black_box(SAMPLE_HTML)));
});

group.bench_function("html2txt_clean_sample", |b| {
b.iter(|| html2txt(black_box(SAMPLE_HTML), black_box(true)));
});

group.throughput(Throughput::Bytes(JSON_LD_HTML.len() as u64));
group.bench_function("baseline_json_ld", |b| {
b.iter(|| baseline(black_box(JSON_LD_HTML)));
});

group.throughput(Throughput::Bytes(METADATA_ONLY_JSON_LD_HTML.len() as u64));
group.bench_function("baseline_metadata_only_json_ld", |b| {
b.iter(|| baseline(black_box(METADATA_ONLY_JSON_LD_HTML)));
});

group.finish();
}

/// Benchmark with real-world HTML files of varying sizes
fn bench_real_world_html(c: &mut Criterion) {
let html_dir = "../data/html_files";
Expand Down Expand Up @@ -97,6 +182,7 @@ criterion_group!(
benches,
bench_extract_default,
bench_extract_with_options,
bench_official_api_helpers,
bench_real_world_html
);
criterion_main!(benches);
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.85"
channel = "1.86"
components = ["rustfmt", "clippy"]
1 change: 1 addition & 0 deletions src/etree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
//! This module re-exports functions from the `html-cleaning` crate.

// Re-export all tree functions from html-cleaning for backward compatibility
#[allow(unused_imports)]
pub use html_cleaning::tree::{
append, element, extend, is_void_element, iter, iter_descendants, iter_text, remove,
set_tail, set_text, strip, strip_elements, strip_tags, sub_element, tail, tail_nodes, text,
Expand Down
Loading