Zum Inhalt springen
OneKitly

HTML sauber entfernen: was ein Tag-Entferner kann und was nicht

Veröffentlicht am 14.7.2026 · 13 Min. Lesezeit · Text- & Sprach-Tools

Daniel Okonkwo

Daniel OkonkwoFront-end-Entwickler und Tech-Redakteur bei OneKitly

Web-Performance · Dateiformate

Anhand von 5 Quellen geprüft

Profil ansehen
Kurz gesagt

Tags entfernen und HTML bereinigen sind verschiedene Aufgaben. Ein Tag-Entferner erzeugt Klartext zur Anzeige, Speicherung oder Zählung; ein Sanitizer erzeugt HTML, das sicher in eine Seite eingefügt werden kann. Die beiden zu verwechseln ist der Weg, auf dem Injection-Fehler entstehen, und eine Regex wird die zweite Aufgabe nie erfüllen, weil sie den Parser nicht modelliert: Gib dem Muster, das ein Kleiner-als-Zeichen, eine Folge von Nicht-Größer-als-Zeichen und ein Größer-als-Zeichen erkennt, das Fragment <a title="fast > cheap" href="/x">our guide</a>, und es hört beim ersten Größer-als-Zeichen auf und liefert cheap" href="/x">our guide. Überall dort, wo Regex und Browser-Tokenisierer nicht übereinstimmen, hat die Regex unrecht, und diese Abweichungen lassen sich nicht aufzählen - deshalb scheitern Sperrlisten als Klasse, und die wirklichen Verteidigungen heißen kontextabhängiges Escaping an der Einfügestelle oder ein parserbasierter Sanitizer mit Positivliste. Als Formatierungswerkzeug hat ein Entferner dagegen echte Arbeit. Bei einem 390 Zeichen langen Fragment liefert das naive Muster 183 Zeichen, die den Stylesheet-Körper, eine verstümmelte JavaScript-Zeile, das Ende eines Kommentars, undekodierte Entitäten und zwei zu StandardExpress verschmolzene Listenpunkte enthalten. Lösch die Inhalte der Rohtext-Elemente, mach aus Blockenden Leerzeilen und aus br einen Zeilenumbruch, dekodiere Entitäten einmal: 118 saubere Zeichen.

Tags entfernen und HTML bereinigen sind zwei verschiedene Aufgaben. Ein echtes Fragment durch eine naive Regex und durch einen formatbewussten Entferner geschickt, mit Script- und Style-Inhalten, Blockumbrüchen, Kommentaren, CDATA und der Reihenfolge der Entitäten als sichtbare Ausgabe.

Zwei Aufgaben, die wie eine aussehen

Tags entfernen heißt Klartext erzeugen: etwas, das in einem Suchergebnis erscheint, dessen Wörter man zählt, das in eine Nur-Text-E-Mail kommt, an eine Zusammenfassung geht oder in einer Spalte liegt, die nie als Markup gerendert wird. Bereinigen heißt HTML erzeugen: Markup, das in eine lebende Seite eingefügt wird und dort deshalb sicher sein muss. Beide sehen ähnlich aus, weil beide HTML entgegennehmen und etwas Kürzeres zurückgeben. Es ist nicht dieselbe Aufgabe, sie haben nicht dasselbe Erfolgskriterium, und nur eine von beiden ist eine Sicherheitsmaßnahme.

Dieser Leitfaden behandelt die erste Aufgabe, richtig gemacht. Die Sicherheitsfrage wird im letzten Abschnitt beantwortet, kurz und ohne Zweideutigkeit, denn die ehrliche Antwort ist kurz: Ein Tag-Entferner ist keine Sicherheitsgrenze, und keine Menge zusätzlicher Muster macht eine daraus. Alles dazwischen ist die Formatierungsarbeit, die ein Entferner tatsächlich leisten muss und die die meisten Implementierungen falsch machen - die Inhalte von script und style löschen, Struktur in Zeilenumbrüche verwandeln, Kommentare und CDATA behandeln und Entitäten an der richtigen Stelle dekodieren.

Was eine naive Tag-Regex mit einem echten Fragment anstellt

Das Testfragment umfasst 390 Zeichen gewöhnlichen Seiten-Markups: eine article-Hülle, ein eingebettetes Stylesheet, eine Überschrift mit kodiertem Und-Zeichen, einen durch ein br geteilten Absatz mit einer Halbgeviertstrich-Entität, einen HTML-Kommentar, dessen Text zufällig ein Größer-als-Zeichen enthält, eine zweielementige Liste, einen Link, dessen title-Attribut ein kodiertes Größer-als-Zeichen enthält, und ein eingebettetes Skript. Nichts Exotisches, nichts Feindliches - so sieht ein CMS-Export aus.

Lass den üblichen Einzeiler laufen - ein Kleiner-als-Zeichen, eine beliebige Folge von Zeichen, die keine Größer-als-Zeichen sind, dann ein Größer-als-Zeichen, global - und 183 Zeichen kommen zurück. Darunter: der wörtliche Text .lead{font-weight:700}, also der Stylesheet-Körper, der sich nun als Fließtext liest. Die Zeichenkette var ok = a d;, also das Skript ohne seine Mitte, weil die Regex die Vergleichsoperatoren als Tag gelesen hat. Das Fragment 0 --> , also das Ende des Kommentars, das überlebt hat, weil die Regex am Größer-als-Zeichen darin stehen blieb. Zwei undekodierte Entitäten. Und die Wörter Standard und Express zu StandardExpress verschmolzen, dazu ein Satz, der auf Monday.Delivery endet, weil das br nichts beigetragen hat.

Dasselbe Fragment durch einen Entferner, der weiß, was die Elemente bedeuten, liefert 118 Zeichen in fünf Blöcken: die Überschrift, den zweizeiligen Absatz mit beachtetem br-Umbruch, die beiden Listenpunkte auf eigenen Zeilen und den Schlusssatz - mit dekodiertem Und-Zeichen und Halbgeviertstrich und ohne jede Spur von Stylesheet, Skript oder Kommentar. Der Unterschied zwischen 183 und 118 Zeichen ist keine Komprimierung. Es ist das Entfernen von Dingen, die nie Text waren.

Script- und Style-Inhalte sind keine Tags

Der größte Formatierungsfehler naiver Entferner ist, dass sie Anfangs- und Endtag von script und style entfernen und alles dazwischen stehen lassen. Das ist kein kleines kosmetisches Problem: Die CSS-Regeln einer Seite oder der Quelltext eines eingebetteten Skripts landen mitten in dem, was lesbarer Fließtext sein sollte, und werden indiziert, gezählt, zusammengefasst und jemandem gezeigt. Im Testfragment erscheint der CSS-Deklarationsblock als zweite Ausgabezeile, direkt über der Überschrift.

Der HTML-Standard nennt sie Rohtext-Elemente: Innerhalb von script und style hört der Parser auf, nach Tags zu suchen, und liest bis zum passenden Endtag. Deshalb verstümmelt die naive Regex auch das, was sie durchlässt. Im Testskript liest die Regex den Ausdruck mit einem Kleiner-als- und einem Größer-als-Zeichen als Tag und löscht ihn, sodass das ausgetretene JavaScript nicht einmal das ursprüngliche JavaScript ist - es ist eine gekürzte Fassung, die zufällig wie ein Satz aussieht. Zwei Fehlermodi in einem einzigen Konstrukt.

Die Lösung ist eine Regel, die vor allem anderen steht: Erfasse Anfangstag, Inhalt und Endtag als eine Einheit für script, style, template und noscript und lösch alles zusammen. Stell das an den Anfang der Pipeline, denn sobald die Tags weg sind, lässt sich nicht mehr erkennen, welcher Text darin stand.

Blöcke brauchen Umbrüche, und br und p sind nicht derselbe Umbruch

Ein Tag zu löschen löscht den Leerraum, den es implizierte. Zwei aufeinanderfolgende Absätze ohne Leerraum dazwischen in der Quelle werden zu OneTwo. Zwei Listenpunkte werden zu AB. Eine Überschrift gefolgt von einem Absatz wird zu TB. Das ist der Fehler, der bereinigten Text unlesbar und Wortzahlen falsch macht, und er ist im Test unsichtbar, sobald das Quell-HTML mit Zeilenumbrüchen zwischen den Tags formatiert ist - genau deshalb geht er in Produktion.

Die Abbildung, die lesbare Ausgabe erzeugt, ist kurz. Das Endtag eines Blockelements - p, div, li, tr, h1 bis h6, blockquote, section, article und die übrigen - wird zu einer Leerzeile. Ein br wird zu genau einem Zeilenumbruch, denn ein br ist ein Umbruch innerhalb eines Blocks, kein neuer Block. Ein hr wird zu einer Leerzeile. Jedes andere Tag wird zu nichts. Reduziere danach Folgen von drei oder mehr Umbrüchen auf eine Leerzeile, denn ein verschachtelter Block gibt zwei Endtags und damit zwei Leerzeilen aus.

Ein ehrlicher Vorbehalt: Diese Abbildung setzt eine Leerzeile zwischen Listenpunkte, weil li ein Block ist. Bei einer engen Liste liest sich das seltsam, und die feinere Variante behandelt li als einfachen Zeilenumbruch und nur das umgebende ul oder ol als Leerzeile. Ob du das willst, ist eine Formatierungsvorliebe und keine Korrektheitsfrage - aber eine Vorliebe, die du ausdrücklich festlegen musst. Ein Entferner, der nie über Listen nachgedacht hat, klebt sie einfach aneinander, und das ist keine Vorliebe, das ist ein Fehler.

Kommentare, CDATA und Entitäten: drei eigene Grammatiken

Ein HTML-Kommentar ist kein Tag und folgt nicht der Tag-Grammatik: Er läuft von einer bestimmten vierzeichigen Eröffnung bis zu einem bestimmten dreizeichigen Abschluss, und dazwischen darf schlechthin alles stehen, auch Größer-als-Zeichen. Eine Tag-Regex, die Kommentare zu entfernen scheint, kommt nur damit durch, weil die meisten Kommentare kein Größer-als-Zeichen enthalten. Setz eines hinein - eine Notiz wie behalten, wenn der Bestand größer als null ist - und die Regex hört dort auf und lässt den Rest des Kommentartextes in deiner Ausgabe stehen. Im Testfragment ist der sichtbare Rückstand die Zeichenkette 0 gefolgt vom Kommentarabschluss.

CDATA-Abschnitte sind ein verwandter Fall, der in freier Wildbahn in SVG und in XHTML-artigen Exporten überlebt. Ihr Inhalt ist definitionsgemäß kein Markup, und ihr Abschluss ist ebenfalls kein Größer-als-Zeichen. Schick einen CDATA-Abschnitt mit markupartigem Text durch die naive Regex, und du erhältst den Innentext ohne seine inneren Tags, mit der schließenden Klammerfolge als Rest - ein Ergebnis, das gleich dreifach falsch ist. Erfasse und lösch CDATA-Abschnitte als Einheit, vor dem allgemeinen Tag-Durchgang, genau wie bei Kommentaren.

Entitäten kommen zuletzt, und genau einmal. Dekodiere, nachdem die Tags weg sind, nie davor: Ein maskiertes Kleiner-als-Zeichen in der Quelle ist Text, den die Autorin sichtbar haben wollte, und wer es zuerst dekodiert, befördert es zu einem Tag, das der Entferner dann löscht. Bei einem Fragment, das empfiehlt, ein em-Element für Betonung zu verwenden, mit maskiertem Elementnamen, bewahrt Entfernen-dann-Dekodieren den sichtbaren Elementnamen, während Dekodieren-dann-Entfernen ihn löscht und eine Lücke hinterlässt. Dekodierst du zweimal, fabrizierst du lebendes Markup aus etwas bewusst Maskiertem - ein Formatierungsfehler auf dem Weg, ein schlimmerer zu werden.

Wo die Sicherheitsgrenze tatsächlich liegt

Ein Tag-Entferner ist ein Formatierungswerkzeug. Er ist keine Sicherheitsgrenze, und man macht ihn auch nicht durch zusätzliche Muster dazu. Der Grund ist strukturell und in der Tabelle oben schon sichtbar: Das naive Muster und der Browser-Tokenisierer sind sich uneins darüber, wo ein Tag endet, sobald ein Attributwert ein Größer-als-Zeichen enthält, und die formatbewusste Fassung ist es an derselben Stelle. Jeder aus Mustern gebaute Filter ist eine Sperrliste der Formen, an die sein Autor gedacht hat, und eine Sperrliste ist nur so gut wie dessen Vorstellungskraft, während der Parser eine feste, veröffentlichte, gegnerunabhängige Spezifikation ist. Man kann nicht aufzählen, worin sich zwei Grammatiken unterscheiden.

Die richtigen Verteidigungen sind zwei, und keine davon ist eine Tag-Entfernung. Die erste und wichtigste ist kontextabhängiges Ausgabe-Escaping: Maskiere den Wert an der Stelle, an der du ihn einfügst, nach der Escaping-Regel dieses Kontexts, denn die Regeln unterscheiden sich für HTML-Text, Attributwerte, URLs, Skript und Stil. Eine Template-Engine, die standardmäßig maskiert, tut das für dich, und die Fehler entstehen dort, wo sich jemand davon abgemeldet hat. Die zweite, nur nötig, wenn Nutzerinnen wirklich Markup einreichen dürfen, das als Markup dargestellt werden muss, ist ein parserbasierter Sanitizer: Er parst die Eingabe in einen Baum und baut sie aus einer Positivliste von Elementen und Attributen neu auf - das Modell der HTML-Sanitizer-Spezifikation und der etablierten Sanitizer-Bibliotheken. Positivliste, Parser, keine Regex.

Es gibt eine saubere Vereinfachung, die man aussprechen sollte. Wenn der bereinigte Text Klartext ist und Klartext bleibt - in einem Textknoten angezeigt, in eine Textspalte geschrieben, gezählt, als Nur-Text-Mail versendet -, dann hat die Ausgabe des Entferners überhaupt keine Sicherheitsrolle, denn dieser Text wird nie wieder als Markup geparst. Die Gefahr entsteht erst, wenn jemand den bereinigten Text nimmt und wieder in HTML einsetzt. In diesem Moment ist die maßgebliche Maßnahme das Escaping an dieser Einfügestelle und nichts, was der Entferner vorher getan hat. Diese beiden Momente im Kopf getrennt zu halten ist fast alles, was dieser Abschnitt vermitteln will.

Acht HTML-Konstrukte durch eine naive Tag-Regex und durch einen formatbewussten Entferner. Jede Ausgabespalte ist die wörtliche Zeichenkette, die der Code in Node 22 zurückgegeben hat.
EingabeNaive Tag-Regex liefertFormatbewusster Entferner liefert
<p>One</p><p>Two</p>OneTwoOne, Leerzeile, Two
<p>One<br>Two</p>OneTwoOne, einfacher Zeilenumbruch, Two
<li>A</li><li>B</li>ABA und B auf getrennten Zeilen
<h2>T</h2><p>B</p>TBT, Leerzeile, B
Ein style-Element mit .lead{font-weight:700}den CSS-Text .lead{font-weight:700} als sichtbare Ausgabenichts - Element und Inhalt werden zusammen gelöscht
Ein script-Element mit var ok = a < b && c > d;var ok = a d; - die Mitte verschluckt, als wäre sie ein Tagnichts
<p>A<!-- keep if stock > 0 -->B</p>A 0 -->BAB
<a title="fast > cheap" href="/x">our guide</a>cheap" href="/x">our guideebenfalls falsch - keine Regex behebt das, nur ein Parser
HTML-Tags entfernenEntferne alle HTML-Tags aus einem Ausschnitt und behalte nur den Text.Tool ausprobieren

Häufige Fragen

Kann ich einfach den Browser nehmen und textContent auslesen?
Mit dem Parser der Plattform zu parsen ist der richtige Instinkt und löst die Attribut- und Kommentarprobleme gratis. Beachte aber, was die beiden Eigenschaften liefern. textContent gibt den verketteten Text aller Nachfahren ohne jede Formatierung zurück, einschließlich der Inhalte von script und style und ohne Zeilenumbrüche für Blöcke - es reproduziert also zwei der Fehler aus diesem Artikel. innerText nähert den gerenderten Text an, fügt Zeilenumbrüche ein und überspringt verborgene Elemente, hängt aber vom Layout und damit von CSS ab. Parse im Browser die Eingabe in ein inertes Dokument, entferne script-, style- und template-Knoten ausdrücklich und lauf dann durch den Baum und setz deine eigenen Umbrüche. Auf einem Server ohne DOM liefert eine echte HTML-Parser-Bibliothek denselben Baum.
Soll ich vor oder nach dem Entfernen der Tags kürzen?
Danach, immer. HTML zu kürzen schneidet ein Tag in zwei Teile oder lässt ein Element ungeschlossen, und das entstehende Fragment ist weder gültiges Markup noch sinnvoller Text - ein Auszug, der mitten in einem Attributwert endet, ist eine klassische Quelle kaputter Layouts, wenn er später irgendwo eingefügt wird. Erst entfernen, dann den Klartext an einer Wortgrenze kürzen, dann die Auslassungspunkte anhängen. Dasselbe Argument gilt fürs Zählen: Eine Zeichen- oder Wortzahl, die am Markup genommen wird, zählt Tag-Namen und Attributwerte als Inhalt und ist daher um genau den Markup-Anteil der Quelle falsch - bei einem typischen CMS-Export ein großer Bruchteil.
Wenn der bereinigte Text zurück in eine Seite geht, dekodiere ich die Entitäten?
Dekodiere ihn einmal fürs Speichern und lass die Ausgabeschicht ihn beim Einfügen wieder maskieren. Das klingt nach Mehrarbeit und ist tatsächlich die einzige Anordnung, die korrekt bleibt: Dein gespeicherter Wert ist der eigentliche Text, und jeder Verbraucher maskiert ihn für seinen eigenen Kontext - HTML-Text, ein Attribut, eine URL, eine CSV-Zelle, eine Nur-Text-Mail. Speichere stattdessen die entitätskodierte Form und lassen die Maskierung bei der Ausgabe weg, weil sie „schon maskiert“ sei, bekommt der erste Verbraucher, der kein HTML ist, wörtliche Und-Zeichen-Folgen, und der erste, der trotzdem maskiert, kodiert doppelt. Ein kanonischer dekodierter Wert im Speicher, Maskierung an jeder Grenze.
Lässt sich ein Regex-Entferner durch weitere Muster sicher machen?
Nein, und der Grund lohnt sich zu verinnerlichen, weil er sich verallgemeinert. Eine Musterliste kodiert die Formen, die ihr Autor vorhergesehen hat; der Parser akzeptiert jede Form, die die Spezifikation definiert, einschließlich des Wiederherstellungsverhaltens bei fehlerhafter Eingabe, das niemand als Regel aufschreibt. Sicherheit folgt nur, wenn das Sprachmodell des Filters mindestens so vollständig ist wie das des Verbrauchers, und eine Musterliste ist konstruktionsbedingt weniger vollständig. Das ist das gesamte Argument für Positivlisten gegen Sperrlisten und für Parser gegen Muster, und deshalb gibt dir dieser Artikel einen Entferner für die Formatierung und verweist dich für die Sicherheit auf einen Sanitizer, statt ein Werkzeug als beides zu verkaufen.
Was soll der Entferner mit Bildern, Links und Tabellen tun?
Entscheide bewusst und dokumentiere die Entscheidung, denn alle drei tragen Information, die außerhalb des Textes lebt. Ein Bild steuert seinen Alternativtext bei, die zugängliche Beschreibung und meist der einzige erhaltenswerte Satz; wer das Element stillschweigend löscht, verliert ihn. Ein Link steuert standardmäßig seinen Ankertext bei, und ob das Ziel in Klammern angehängt werden soll, hängt davon ab, ob deine Ausgabe je ohne Möglichkeit gelesen wird, Links zu folgen. Eine Tabelle braucht einen Zell- und einen Zeilentrenner, sonst wird jede Zeile zu einer unlesbaren Folge aneinandergehängter Werte - ein Tabulator zwischen Zellen und ein Zeilenumbruch zwischen Zeilen ist das übliche Minimum. Nichts davon entscheiden die Tag-Namen für dich; es ist eine redaktionelle Entscheidung darüber, was der Klartext bewahren soll.

Artikel, die dich interessieren könnten

Alle Ratgeber
RatgeberMarkdown entfernen: was der Klartext verliert und was eine Regex falsch machtEin Link wird zu Text, dessen Ziel gelöscht ist, eine verschachtelte Liste verliert ihre Hierarchie, eine Tabelle wird zu einer Reihe von Wörtern. Dann die technische Hälfte: Markdown hat keine einzige Spezifikation, und ein Regex-Entferner verstümmelt einen Dateinamen, ein Malzeichen und das Innere eines Codeblocks - alles gegen einen echten Parser gehalten.AnleitungUnsauberen Text bereinigen: die Reihenfolge der Schritte, auf die es ankommtTags entfernen, bevor Entitäten dekodiert werden, trimmen vor dem Deduplizieren, Leerraum zuletzt zusammenfassen. Drei Reihenfolgen in Node ausgeführt, eine neunstufige Pipeline in der richtigen Abfolge und die unsichtbaren Zeichen — U+00A0, U+200B, U+FEFF — die jede naive Bereinigung überleben.RatgeberEine URL mit Parametern bauen, die ein Kopieren und Einfügen überstehtDrei Kodierungen, ein sichtbarer Unterschied: %20 oder +. Der Formularmodus des Generators entspricht URLSearchParams bei siebzehn Werten byteweise — aber gib ihm eine Basis-URL mit Fragment, und jeder Parameter landet im Hash, wo kein Server ihn sieht.ErklärungSatz- und Titelschreibweise: Die Regeln unterscheiden sich je nach SpracheDie englische Titelschreibweise hat je nach Stilhandbuch drei verschiedene Grenzen. Französisch, Spanisch, Portugiesisch und Italienisch haben gar keine. Deutsch schreibt jedes Substantiv groß. Das Werkzeug weiß von all dem nichts — hier steht genau, was es tut.RatgeberZeichenlimits, die wirklich wehtun: Code-Einheiten, Codepunkte und GraphemclusterEin Zeichen ist gleich drei Dinge. Ein Emoji mit Hautton ist 1 Graphemcluster, 2 Codepunkte und 4 UTF-16-Einheiten. Alle Zählungen in diesem Leitfaden wurden in Node gemessen - dazu, warum eine SMS von 160 auf 70 fällt und warum VARCHAR(255) keine 255 von irgendetwas Bestimmtem sind.ErklärungWörter zu zählen ist mehrdeutig, und jedes Werkzeug antwortet andersEine Wortzahl ist eine Definition, keine Messung. Wir haben denselben Absatz auf vier Arten gezählt und 25, 28, 33 und 38 erhalten — dann 50.000 Zeichen gewöhnlicher Prosa gezählt und Übereinstimmung auf 4,5 % genau. Der Abstand rührt ausschließlich von Komposita, Zahlen und URLs her.

Ähnliche Tools

Quellen

Hast du einen Fehler in diesem Artikel entdeckt?

HTML sauber entfernen: was ein Tag-Entferner kann und was nicht — OneKitly