fix(wiki): strip script/style/nav/footer text from HTML-typed material

This commit is contained in:
matevip 2026-05-19 21:23:07 +08:00
parent 32d633ae4b
commit 9ffe7aa399

View File

@ -46,16 +46,16 @@ public class WikiContentNormalizer {
if (rawText == null) return "";
String type = sourceType == null ? "" : sourceType.toLowerCase();
return switch (type) {
// 'url' is raw fetched web HTML and still carries markup, so it needs
// tag stripping here. 'html' file uploads are tag-stripped upstream by
// DocumentExtractTool (jsoup) before reaching the normalizer running
// normalizeHtml again would re-collapse the recovered heading structure,
// so they fall through to the plain-text branch.
case "url" -> normalizeHtml(rawText);
// Web and HTML sources can still carry raw markup, so they go through
// normalizeHtml, which strips script/style/nav/footer noise. normalizeHtml
// also recognizes text that was already tag-stripped upstream (an extracted
// .html/.htm upload re-entering the normalizer) and leaves that text's line
// structure intact instead of re-collapsing its recovered headings.
case "url", "html", "htm" -> normalizeHtml(rawText);
// PDF text from DocumentExtractTool may already contain "--- Page N ---"
// markers; we keep them so the preprocessor can map char offsets to pages.
case "pdf" -> collapseBlankLines(rawText);
case "docx", "doc", "pptx", "ppt", "xlsx", "xls", "html", "htm" -> collapseBlankLines(rawText);
case "docx", "doc", "pptx", "ppt", "xlsx", "xls" -> collapseBlankLines(rawText);
case "markdown", "md", "text", "paste" -> collapseBlankLines(rawText);
default -> collapseBlankLines(rawText);
};
@ -63,16 +63,38 @@ public class WikiContentNormalizer {
/**
* Strip nav/footer/script/style/aside and ad-like classes from HTML, then
* return readable text. Falls through to the raw input when the document
* is too large to parse safely or jsoup throws.
* return readable text.
* <p>
* The output never carries markup: input that still contains tags is parsed
* and stripped; input that is already tag-free is returned with whitespace
* cleanup only. When the document cannot be parsed (too large, or jsoup
* throws) it is run through {@link #stripMarkupLossy(String)} so that
* script/style bodies and tags are dropped without a full parse the raw
* markup is never passed through verbatim.
*/
private String normalizeHtml(String rawHtml) {
if (rawHtml.length() > MAX_HTML_LEN) {
log.warn("[WikiContentNormalizer] HTML payload exceeds {} bytes, skipping cleanup", MAX_HTML_LEN);
return collapseBlankLines(rawHtml);
log.warn("[WikiContentNormalizer] HTML payload exceeds {} bytes, stripping markup without a full parse", MAX_HTML_LEN);
return stripMarkupLossy(rawHtml);
}
try {
Document doc = Jsoup.parse(rawHtml);
// Distinguish genuine HTML markup from text that was already tag-stripped
// upstream (an extracted .html/.htm upload re-entering the normalizer).
// jsoup synthesizes an <html>/<head>/<body> skeleton even for plain text,
// so the absence of any element children means there is no real markup.
// Plain text must skip the element walker below: that walker relies on
// Element.ownText(), which collapses newlines and would merge a heading
// line into the paragraph that follows it.
Element body = doc.body();
Element head = doc.head();
boolean hasMarkup = (body != null && !body.children().isEmpty())
|| (head != null && !head.children().isEmpty());
if (!hasMarkup) {
return collapseBlankLines(rawHtml);
}
// Drop structural noise.
doc.select("script, style, noscript, nav, header, footer, aside, form, iframe").remove();
// Drop common ad / share / cookie banners by class hint.
@ -99,13 +121,41 @@ public class WikiContentNormalizer {
}
}
String out = sb.toString();
return out.isBlank() ? collapseBlankLines(rawHtml) : collapseBlankLines(out);
if (!out.isBlank()) {
return collapseBlankLines(out);
}
// The walker produced nothing the document was pure script/style/nav
// noise. Fall back to jsoup's plain-text extraction, never the raw markup:
// returning rawHtml here would carry <script>/<style> straight through into
// stored wiki content. Element.text() reads TextNodes only, and the noise
// elements were already removed above, so the result is markup-free.
String plain = doc.text();
return plain.isBlank() ? "" : collapseBlankLines(plain);
} catch (Exception e) {
log.warn("[WikiContentNormalizer] HTML parse failed, falling back to raw text: {}", e.getMessage());
return collapseBlankLines(rawHtml);
log.warn("[WikiContentNormalizer] HTML parse failed, stripping markup without a full parse: {}", e.getMessage());
return stripMarkupLossy(rawHtml);
}
}
/** Matches a {@code <script>}/{@code <style>} block including its text body. */
private static final Pattern SCRIPT_STYLE_BLOCK =
Pattern.compile("(?is)<(script|style)\\b[^>]*>.*?</\\1\\s*>");
/** Matches any remaining HTML/XML tag. */
private static final Pattern ANY_TAG = Pattern.compile("(?s)<[^>]*>");
/**
* Last-resort sanitizer for HTML that could not be parsed with the element
* walker (oversized payloads, or a parser failure). Drops the text body of
* {@code <script>}/{@code <style>} blocks, then removes every remaining tag.
* Lossy by design document structure is gone but the result carries no
* markup, so it is safe to store and render downstream.
*/
private String stripMarkupLossy(String rawHtml) {
String noScript = SCRIPT_STYLE_BLOCK.matcher(rawHtml).replaceAll(" ");
String noTags = ANY_TAG.matcher(noScript).replaceAll(" ");
return collapseBlankLines(noTags);
}
/**
* Collapse 3+ consecutive blank lines down to 2, normalize CRLF to LF.
* Cheap, lossless cleanup that helps chunk boundary detection.