← Zurück zum Blog
pullmdweb-scrapingclaude-codeextraction30. April 2026 · 6 Min. Lesezeit

PullMD Teil 5: Die falsche Engine gemessen

URL → ROUTING GATE → EXTRACTION PATHSKI-generiertes Bild
Editorial-Diagramm: URL-Input speist eine zentrale Routing-Gate, die in vier Extraction-Path-Cards verzweigt (Cloudflare MD, Reddit JSON, Readability, Trafilatura); rechts der Quality-Score; unten links ein kleines Log-Panel mit Health-, Signal- und Routed-Status.Editorial-Diagramm: URL-Input speist eine zentrale Routing-Gate, die in vier Extraction-Path-Cards verzweigt (Cloudflare MD, Reddit JSON, Readability, Trafilatura); rechts der Quality-Score; unten links ein kleines Log-Panel mit Health-, Signal- und Routed-Status.

Der Dienst lief, nahm inzwischen jede Adresse und ich benutzte ihn eigentlich täglich. Was dann kam, waren keine großen Schritte mehr. Etwas Kleinkram an der Ausgabe, eine Runde echter Verbesserungen im April, und am Ende die Erkenntnis, dass ich wochenlang das Falsche gemessen hatte.

Kleinkram, der trotzdem zählt

Am 20. März habe ich einen Vormittag an der Ausgabe geschraubt. Nichts davon ist spektakulär, es ist halt genau die Sorte Arbeit, die ein Werkzeug überhaupt erst benutzbar macht.

Im Kopf des Markdowns stand bei normalen Seiten bis dahin nur die Domain. Bei Reddit war die vollständige Adresse längst drin, bei allem anderen eben nicht. Also überall die volle Adresse, und zwar als reiner Text, nicht als Link.

Dazu ein kompaktes Zeitformat statt der ausgeschriebenen deutschen Variante, und ein Zahlenfeld für die Kommentar-Tiefe statt eines Dropdowns mit vier festen Stufen. Kommentare stehen seitdem standardmäßig an, denn wer sie nicht will, schaltet sie ja einfach aus.

Vier Kleinigkeiten an einem Vormittag. Zusammen sind sie der Unterschied zwischen „läuft“ und „benutze ich gern“.

Die Reifephase

Am Abend des 11. April bin ich über das GitHub-Repo von firecrawl gestolpert, einem ziemlich viel größeren Werkzeug in derselben Ecke. Mich interessierte dabei nicht das Produkt, sondern nur, ob die bei der Konvertierung Dinge besser machen als ich.

Also habe ich mir das ansehen lassen und daraus ein Briefing schreiben lassen, mit dem Claude Code direkt weiterarbeiten kann. Drei Minuten später lief die Umsetzung.

Aus dem Briefing wurden drei Verbesserungen, die P1.1, P1.2 und P1.3 hießen: die erste Runde der Extraction Pipeline v0.2.

P1.1: Turndown → node-html-markdown. Turndown ist der klassische HTML→Markdown-Konverter, aber er unterstützt kein GitHub Flavored Markdown.

  • Tabellen wurden nicht als GFM-Tabellen ausgegeben, sondern flachgedrückt oder weggeworfen.

  • Fenced Code Blocks mit Sprach-Hint, also ```typescript oder ```python, gingen verloren: der Code-Block kam ohne Language-Tag raus, und nachgelagerte Syntax-Highlighting funktionierte nicht.

  • node-html-markdown beherrscht GFM nativ: Tabellen bleiben Tabellen, Task-Lists bleiben Task-Lists, Code-Fences behalten ihren Sprach-Hint.

Ein Zeilen-Tausch in der Pipeline, aber ein deutlicher Unterschied im Output.

P1.2: Rich Metadata. Bisher lieferte der Service nur Markdown: keinen Kontext darüber, was der Text eigentlich ist.

  • lib/metadata.js mit cheerio extrahiert jetzt Titel aus <title> und <meta>-Tags (og:title, twitter:title), dazu Description, Author, Published Time.

  • Das Ergebnis: jeder Markdown-Output bekommt ein YAML-Frontmatter mit diesen Metadaten.

  • Für API-Clients gibt es außerdem eine ?format=json-Variante, die Metadata und Markdown-Text strukturiert als JSON-Objekt liefert: direkt konsumierbar für Pipelines, die nicht erst Frontmatter parsen wollen.

P1.3: Readability-Fallback. Readability versagt bei manchen Seiten: Paywalls, reine JavaScript-Apps, Sites, die keinen semantischen Artikel-Inhalt im DOM haben.

  • Das Ergebnis sind Outputs mit weniger als 200 Zeichen, offensichtlich unbrauchbar.

  • Statt in diesem Fall nichts zu liefern, greift jetzt ein Fallback: gecleantes Body-HTML, ohne die aggressive Readability-Sanitisierung.

  • Der Source-Marker im Frontmatter lautet dann readability-fallback, damit man in den Logs sehen kann, wie oft dieser Pfad genommen wird und für welche Domains.

Bis zu diesem Punkt war alles inkrementell.

Der Bug

Ich fragte mich: ist Readability wirklich der beste Extraction-Pfad? Ich wollte es messen, bevor ich es weiter ausbaue. Also setzten wir erst mal einen Vergleich auf: 15 URLs aus dem Production-Cache, durchgejagt durch drei Engines.

Readability, @extractus/article-extractor und Trafilatura (eine Python-Bibliothek, die ursprünglich für Web-Archivierungs-Projekte entwickelt wurde und bekannt dafür ist, auch bei schwierigen Seiten noch substantiellen Text zu extrahieren).

Beim Auswerten der Ergebnisse fiel etwas auf. Die Readability-Outputs sahen anders aus als erwartet: kürzer, irgendwie rauer, ohne die sauber strukturierten Absätze, die ich vom Firefox Reader Mode kannte.

Claude schaute in lib/web.js und verfolgte den Code-Pfad zurück: server.js übergab für alle URLs den Parameter wantComments=true an extractWeb. Das war der Default, und für Reddit hatte er einen Sinn, weil Reddit-Kommentare mitextrahiert werden sollten.

Aber wantComments=true triggerte in der Web-Conversion-Funktion einen separaten Zweig: wenn Comments gewünscht sind, überspringe Readability und liefere gecleantes Body-HTML. Das bedeutete: Production lief seit vielen Tagen mit “cleaned Body-HTML”-Output, nicht mit Readability.

Was ich als “Readability-Output” kannte, war in Wirklichkeit nur gecleantes Raw-HTML. Mein Vergleich hatte die falsche Engine gegen Trafilatura gemessen.

Der Fix war eine Zeile: für Web-URLs übergibt server.js immer comments: false.

Trafilatura als zweite Engine

Mit dem Fix ergab die Evaluation sinnvolle Werte:

  • Trafilatura war komplementär stark genau dort, wo Readability tatsächlich versagte.

  • Auf einer Blogseite lieferte Readability nach dem Fix 107 Zeichen Output; Trafilatura lieferte 14.755 Zeichen, Faktor ~138.

  • Bei einer Tech-News-Site: 471 Zeichen mit Readability, 1.667 Zeichen mit Trafilatura. Bei einem anderen Blog: Trafilatura +80% mehr Inhalt.

  • @extractus war für die meisten URLs byte-identisch zu Readability, plus drei harte Fehler auf 15 URLs.

  • Readability gewann dort, wo Tabellen erhalten bleiben mussten: Wikipedia, API-Dokumentation, strukturierte Seiten.

  • Trafilatura extrahiert aggressiver und verliert dabei manchmal die Struktur.

Das Ergebnis war keine Entweder-Oder-Entscheidung und so fügten wir Trafilatura als Python-Sidecar-Container hinzu, intern erreichbar über ein Docker-Network. Beide Engines laufen parallel: ein Quality-Score wählt das bessere Ergebnis pro Request.

Der Quality-Score (5 Punkte)

Die Heuristik in lib/scoring.js ist einfach und absichtlich so gehalten. Fünf Faktoren, die grob messen, ob ein Extraction-Output substanziell ist:

  • +0.3 wenn der Markdown-Output länger als 500 Zeichen ist
  • +0.2 wenn das Verhältnis aus extrahiertem Text zu Original-HTML-Größe über 2% liegt
  • +0.2 wenn <article> oder <main> im Original-HTML vorhanden ist (semantisches Signal, dass die Seite Artikel-Struktur hat)
  • +0.15 wenn mindestens eine Überschrift im Output ist
  • +0.15 wenn mindestens drei Absätze mit 40+ Zeichen vorhanden sind

Reddit-JSON und Cloudflare-Markdown bekommen +0.4 statt der beiden web-only Bonuses: beide sind “clean by construction”, also ohne Junk-HTML, das gefiltert werden müsste.

Der Score landet im X-Quality-Header jeder Response und in metadata.quality im JSON-Output, damit man als Client sehen kann, wie zuverlässig der Output für eine gegebene URL war.

Retrospektive

Was sagt es aus, wenn ein Bug viele Tage in einem selbst-gehosteten Service lebt, ohne dass jemand es bemerkt? Zumindest, dass der Output “gut genug” war. Gecleantes Body-HTML für die meisten Seiten enthält den Großteil des relevanten Texts, auch ohne Readability.

Wenn der Unterschied zwischen richtigem und falschem Verhalten schwer zu sehen ist, sieht man ihn nicht. Nicht weil man nicht aufpasst, sondern weil es kein sichtbares Signal gibt.

Das führte direkt zu P3.1: eine extraction_log SQLite-Tabelle, die für jeden Request Source, Quality-Score und Latenz speichert, mit Auto-Prune nach 30 Tagen. Dazu ein GET /api/stats?window=-7+days Endpoint, der die Daten aggregiert: wie viele Requests gingen durch Readability, wie viele durch Trafilatura, wie viele durch den Fallback-Pfad, welche Domains haben durchschnittlich niedrige Quality-Scores.

Das nächste Mal, wenn ein Parameter-Default einen Code-Branch falsch triggert, verschiebt sich die Source-Verteilung im Stats-Endpoint. Keine Garantie, aber ein Signal.

Für jedes Tool gilt: entweder du baust Beobachtbarkeit ein, oder du fliegst blind. Die Beobachtbarkeit muss nicht groß sein, eine SQLite-Tabelle und ein JSON-Endpoint reichen als erster Schritt.

Was als nächstes kam

Die Extraktion war damit so gut, wie ich sie hinbekam, und der Dienst wusste selbst, wie zuverlässig sein Ergebnis gerade ist. Noch am selben Tag bekam er eine Hilfeseite, samt Setup für Agenten, und dabei fiel mir auf, dass eine meiner Behauptungen gar nicht stimmte. → PullMD Teil 6: Setup für Agenten


Teil 5 von 15 der PullMD Serie.

← PullMD Teil 4: Der Skill, und was er wirklich bringt | PullMD Teil 6: Setup für Agenten →

Kommentare

Zum Kommentieren genügt eine E-Mail-Bestätigung oder ein GitHub-Login. Die Kommentare laufen auf einem eigenen Server, nicht bei einem Werbeanbieter — Details in der Datenschutzerklärung.