Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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);
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