You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
65 lines
2.1 KiB
Python
65 lines
2.1 KiB
Python
"""HTML-to-text conversion and text normalization."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unicodedata
|
|
|
|
from bs4 import BeautifulSoup
|
|
|
|
_BOILERPLATE_PATTERNS = [
|
|
re.compile(r"safe harbor.*?forward.looking statement", re.IGNORECASE | re.DOTALL),
|
|
re.compile(r"this press release.*?private securities litigation", re.IGNORECASE | re.DOTALL),
|
|
re.compile(r"^\s*page \d+ of \d+\s*$", re.IGNORECASE | re.MULTILINE),
|
|
re.compile(r"^\s*\[?\s*table of contents\s*\]?\s*$", re.IGNORECASE | re.MULTILINE),
|
|
]
|
|
|
|
_WHITESPACE = re.compile(r"\s{3,}")
|
|
_HTML_MARKERS = re.compile(r"<(?:!doctype|html|body|div|table|p|br|font|span)\b", re.IGNORECASE)
|
|
|
|
|
|
def html_to_text(html: str) -> str:
|
|
"""Convert HTML to plain text using BeautifulSoup."""
|
|
soup = BeautifulSoup(html, "html.parser")
|
|
# Remove script/style
|
|
for tag in soup(["script", "style", "head"]):
|
|
tag.decompose()
|
|
return soup.get_text(separator="\n")
|
|
|
|
|
|
def looks_like_html(text: str) -> bool:
|
|
"""Return True when SEC exhibit text appears to still be HTML markup."""
|
|
sample = text.lstrip()[:2048]
|
|
return bool(_HTML_MARKERS.search(sample))
|
|
|
|
|
|
def normalize_unicode(text: str) -> str:
|
|
"""Normalize unicode to NFC and replace fancy quotes/dashes."""
|
|
text = unicodedata.normalize("NFC", text)
|
|
# Fancy quotes → standard
|
|
text = text.replace("\u2018", "'").replace("\u2019", "'")
|
|
text = text.replace("\u201c", '"').replace("\u201d", '"')
|
|
# Em/en dash → hyphen
|
|
text = text.replace("\u2014", " - ").replace("\u2013", " - ")
|
|
return text
|
|
|
|
|
|
def remove_boilerplate(text: str) -> str:
|
|
"""Strip common boilerplate sections."""
|
|
for pattern in _BOILERPLATE_PATTERNS:
|
|
text = pattern.sub(" ", text)
|
|
return text
|
|
|
|
|
|
def collapse_whitespace(text: str) -> str:
|
|
"""Collapse runs of 3+ whitespace chars to double newline."""
|
|
return _WHITESPACE.sub("\n\n", text).strip()
|
|
|
|
|
|
def normalize_text(raw: str, is_html: bool = False) -> str:
|
|
"""Full normalization pipeline."""
|
|
text = html_to_text(raw) if is_html else raw
|
|
text = normalize_unicode(text)
|
|
text = remove_boilerplate(text)
|
|
text = collapse_whitespace(text)
|
|
return text
|