german-impressum-extractor
Extract structured data from German B2B website Impressum text — pure Rust, no async runtime needed.
Germany's TMG §5 requires every commercial website to publish an "Impressum" listing the legal entity, managing directors, address, contact info, and tax identifiers. This crate gives you a battle-tested parser for that data.
What it extracts
- 📧 Email addresses — plain (
info@firma.de) and obfuscated (info [at] firma [dot] de). False positives from code fragments (…@….css) and prose (…matters. Discover…→th@matters.discover) are filtered via a real-TLD allowlist. - ☎️ Phone numbers — normalized to
+49…form regardless of input format. - 📠 Fax / Telefax — labeled fax numbers, kept separate from
phones. - 🏠 Address — German postcode + city + street with house number.
- 🪪 HR-Nummer — Handelsregister number (e.g.
HRB 12345 B). - 🏛️ HR court — registration court (e.g.
Amtsgericht Berlin (Charlottenburg)). - 💶 USt-IdNr. — German VAT ID (
DE+ 9 digits, also with grouping spacesDE 123 456 789). - 🧾 Steuernummer — local tax number, incl. abbreviations (
St.-Nr.,StNr.,Steuer-Nr.). - 🏦 IBAN / BIC — German bank details (
Bankverbindung). - 🏢 Legal form —
GmbH,GmbH & Co. KG,GmbH & Co. KGaA,KGaA,UG,AG,KG,OHG,GbR,e.K.,eG,SE. - 📅 Year founded —
gegründet 1973/seit 1985/founded in 1990. - 👥 Persons — Geschäftsführer / Inhaber / Vorstand / Verantwortlicher (§18 MStV) with role tag.
- 🏛️ Supervisory authority — Aufsichtsbehörde responsible for the entity.
- 🏤 Professional chamber — zuständige Kammer / Berufskammer (industry/profession registration).
- 📮 De-Mail — De-Mail address, German secure email alternative.
- 🔗 EU Dispute Resolution / ODR link — dispute resolution platform URL (Streitbeilegungsplattform).
- 📋 Handelsregister type — register type designation (HRA for Einzelunternehmen, HRB for Gesellschaften).
Why not just regex it yourself
Because German B2B websites have hundreds of edge cases:
- Phone:
+49 (0) 30 / 1234 5-67vs0030 12 345-678vs030 / 12345/678 - Names:
Dr. h.c. Hans-Peter von der Mühle und Anna Schmidt-Lutzshould yield two distinct people. - Postcodes vs random 5-digit numbers:
12345is not always a postcode. - Legal forms with
&:GmbH & Co. KGvsGmbH & Co. KGaAvs justGmbH.
This crate ships 40+ unit and regression tests covering these cases, runs clippy -D warnings in CI, verifies its 1.85 MSRV, and is used in production lead-gen pipelines.
Usage
Note: this crate is not yet published on crates.io. Add it as a git dependency:
[]
= { = "https://github.com/Liohtml/german-impressum-extractor" }
Once published, this will become german-impressum-extractor = "0.1".
One-shot extract
use extract_all;
let text = read_to_string.unwrap;
let data = extract_all;
println!;
println!;
println!;
println!;
Granular extractors
Each field has a separate function if you only need part of the picture:
use ;
let text = "Geschäftsführer: Hans Müller, Tel: +49 30 1234567";
let emails = extract_emails;
let phones = extract_phones;
let fax = extract_fax;
let persons = extract_persons;
let = extract_address;
let legal_form = extract_legal_form;
let vat_id = extract_vat_id;
let tax_number = extract_tax_number;
let hr_number = extract_hr_number;
let hr_court = extract_hr_court;
let iban = extract_iban;
let bic = extract_bic;
let founded = extract_year_founded;
let supervisory_auth = extract_supervisory_authority;
let prof_chamber = extract_professional_chamber;
let de_mail = extract_de_mail;
let odr_url = extract_dispute_resolution_url;
let reg_type = extract_register_type;
All granular extract_* functions normalize their input the same way extract_all does (Unicode/whitespace cleanup, HTML-entity decoding), so calling them directly gives the same result as the corresponding field of extract_all.
Multiple addresses
extract_address returns the first address; for pages listing several
locations use extract_addresses, which returns one Address per address
block (components are never mixed across blocks):
use extract_addresses;
for a in extract_addresses
Confidence scores
Need to know how much to trust each field? extract_all_scored returns the
same data with a per-field confidence in 0.0..=1.0 (heuristic: format
validity such as an IBAN mod-97 check, plus a boost when the source page
labels the field):
use extract_all_scored;
let d = extract_all_scored;
if let Some = d.iban
extract_all is unchanged; scoring is purely additive. With the html
feature, extract_all_scored_html does the same from raw HTML.
serde support (optional)
[]
= { = "0.1", = ["serde"] }
The Extracted and Person types then derive Serialize + Deserialize.
From HTML directly (optional html feature)
[]
= { = "0.1", = ["html"] }
use ;
let data = extract_all_html; // parse + extract in one step
let text = html_to_impressum_text; // just the cleaned text
Without the feature, keep feeding plain text to extract_all — the default build pulls no HTML dependencies.
Examples
Robustness & limits
- ✅ Handles
ä,ö,ü,ß, double-letter substitutions (ae,oe,ue,ss). - ✅ Tested on dozens of real German cleaning-industry Impressum pages in production.
- ⚠️ Extraction is regex-based; some unusual layouts may produce noise. The
personsfield in particular benefits from a downstream cleanup step that filters obvious non-names (e.g. you may want to droplast_name == "Geschäftsführung"). - ⚠️ Address regex finds the first postcode/street in the text. If the page contains multiple legal entities or branch addresses, use
extract_addressesto get all addresses;extract_addressreturns only the first.
Contributing
Issues and PRs welcome. Please:
cargo testpasses- New regex patterns ship with at least one test case derived from real-world text
- Run
cargo fmt+cargo clippy --all-targets
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.