HTML profiles¶
This page is the full reference for how doctrail ingest reads HTML and MHTML files. The quick start covers the common case; read this page when a corpus needs tuning.
Modes¶
--html-mode chooses what text a page yields.
articlekeeps the main article and drops the rest of the page. It is the native engine's default.fullkeeps all visible text, including navigation, sidebars, and footers. The native engine keeps image alt text, puts every table cell on its own line, leaves out scripts, styles, and link footnotes, and never splits a paragraph across lines, so phrase and trigram searches match across the whole paragraph. The Python engine's full mode is BeautifulSoup text, which starts a new line at every tag and drops alt text.
Without the flag, each engine keeps its default: article text in the native engine, full text in the Python engine.
In article mode, the native engine also rejects pages it identifies as login walls, HTTP error templates, link-only navigation, or leaked script. Ingest skips them and inserts no row. The Python engine has no such check. A page that carries a login box or mentions an error but also has real text is kept. The checks are heuristics: a tutorial built around code examples, or a page of short provisions that reads like a menu, can still be rejected. Full mode keeps such pages and records the reason.
Two older options affect only the Python engine. --readability asks for article text and conflicts with full mode. --html-extractor smart keeps paragraphs together instead of breaking at every tag, in both modes. When readability fails or a page needs encoding recovery, the Python engine falls back to whole-page text, and the row's extraction_method says which path ran.
Profile files¶
A profile is a YAML mapping passed with --html-profile FILE, or set as html_profile in .doctrail/config.yml, where a relative path is resolved from the directory you run doctrail in. Every key is optional.
mode: full
keep_selectors: ["#main"]
drop_selectors: ["#MapArea", ".zwjdown", "div.related > ul"]
drop_line_patterns: ["^HTML阅读$", "(?i)^share this"]
reject_low_value: false
mode:articleorfull, defaulting tofull.--html-mode, orhtml_modein the project config, overrides it. The three rule lists below needfull.keep_selectors: CSS selectors for the parts of the page to keep. The outermost matches left afterdrop_selectorsare kept once each, in page order. If nothing matches, the whole page is kept and the row recordshtml_keep_selectors_matchedas 0, so a template change shows up in the metadata instead of as empty rows.drop_selectors: CSS selectors for elements to remove, including inside kept parts. They are applied beforekeep_selectors.drop_line_patterns: regular expressions tested against each output line after it is trimmed. A pattern matches anywhere in the line unless it is anchored with^and$. Matching lines are removed.reject_low_value: turns the low-value check on or off. It defaults to true in article mode and false in full mode. When true, rejected pages are skipped. When false, their text is kept andhtml_low_value_reasonrecords what was detected.
The project config also accepts html_mode, readability, html_extractor, and skip_garbage_check, with the same meaning as the flags. --skip-garbage-check turns off the Python engine's check for garbled encodings and the recovery that the check triggers.
Selectors follow CSS Selectors Level 3: type, class, id, attribute, descendant, child, and sibling selectors, :not(), and the :nth-* and :first-* pseudo-classes. :is(), :where(), and :has() are not supported. Patterns use the syntax of Rust's regex crate: inline flags such as (?i) and Unicode classes such as \p{Han} work, while lookaround and backreferences do not.
Engines and failures¶
Rule lists and reject_low_value: true need the native engine. The Python engine accepts mode, ignores empty rule lists and reject_low_value: false, and stops with an error on anything else.
A bad selector, a bad pattern, or an unknown key stops the ingest before any file is read.
The native engine reads the first 64 KiB of each file before trusting its extension. An image saved as .html goes to OCR. A page saved gzip-compressed, as crawlers that store the raw HTTP response do, is decompressed and read, and the row records content_encoding: gzip. Audio, video, or another compressed archive is recorded as failed. A saved HTTP header before the page, as HTTrack writes, is removed when it ends within those 64 KiB.
Without rule lists, a page the native engine cannot extract, such as one nested beyond the safety limit, falls back to w3m, and an empty or timed-out MHTML page falls back to the Python engine. Fallback text is whole-page text without the native formatting. With rule lists, such a page is recorded as failed, because the fallbacks cannot apply the rules. A page that the rules leave empty is skipped and reported.
What each row records¶
Each row's metadata JSON records html_mode. Native full-mode rows also record:
html_config: the effective settings as a JSON string, with defaults filled in.html_keep_selectors_matched: how many outermost elementskeep_selectorskept, or 0 if none matched. It is absent when the profile has nokeep_selectors.html_nodes_dropped: how many elementsdrop_selectorsremoved.html_lines_dropped: how many linesdrop_line_patternsremoved.html_low_value_reason: why the page looks low-value, when it does.
Their extraction_method is rust:html_full or rust:mhtml_full. Fallback rows record their own method and may lack these keys.
Ingest skips files already in the table, so rerun with --overwrite to re-extract them with a new mode or profile.
Writing a profile¶
- List a sample of the corpus's HTML files, one path per line.
- From a source checkout with the native engine built, run
uv run python scripts/html_mode_report.py --paths files.txt. It extracts each file in article and full mode, reports failures, empty results, length, and speed, and prints the lines repeated across the most files. Repeated lines are usually site chrome. Its retention score is the share of 12-character pieces of article text that full mode keeps; it does not check fields. - Turn the repeated lines into
drop_line_patterns, or find the elements that hold them and adddrop_selectors. If every page keeps its content in one container, name it inkeep_selectors. - Rerun the report with
--profile FILEto see what the profile leaves, and check a few pages by eye before ingesting. The retention score still compares article text with full text without the profile.
examples/html-profiles/cnki-detail.yml is a worked profile for the older CNKI article-page layout. On 3,046 pages from one collection, it halved the median length and dropped none of the 【…】 fields. Check that the fields your study needs survive on a sample of your own pages.
Different collections usually need different profiles. Run one ingest per collection, each with its own profile.