html2md-clean
A clean, controllable HTML -> Markdown converter with noise removal (ads, popups, navigation), built for web scraping and preparing text for LLMs.
Table of contents
- Overview
- Features
- Installation
- Quick start
- CLI
- Usage examples
- Public API
- Conversion options
- Optional dependencies
- Performance
- Development
- Documentation
- License
Overview
html2md-clean turns the "messy" HTML of real-world web pages into clean Markdown.
Unlike naive converters, the library first strips boilerplate — navigation,
footers, banners, cookie popups, ad blocks — and only then renders the main
content. That makes it a good fit for:
- web scraping and dataset building;
- preparing text for RAG / LLM pipelines (fewer noise tokens);
- archiving articles as readable Markdown.
The library runs on two parsers: Selectolax (fast, the default when
installed) and BeautifulSoup (fallback), hidden behind a single Adapter
interface.
Features
- Noise removal by tag, CSS class,
id,aria-hidden, and inlinedisplay:none. - Main-content extraction via link-density and content-density heuristics.
- Links: inline
[text](url)or reference-style[text][N], with optional UTM/tracker stripping (courlan). - Citations: a Markdown variant with footnotes
[1],[2]plus a reference list. - Metadata:
title, firsth1,description, and counters for words / characters / headings / links. - Publication / update dates (optional, via
htmldate). - Mojibake repair (
ftfy), NFKC Unicode normalization, HTML sanitization (nh3). - Synchronous and asynchronous APIs, with a lazily generated
raw_markdown. - Immutable options (
MarkdownOptions.with_overrides(...)) — thread-safe.
Installation
Requires Python 3.11+.
pip install html2md-clean
With optional extras (dates, sanitization, encoding fixes, URL cleaning):
pip install "html2md-clean[full]"
Install straight from the github repository:
pip install git+https://github.com/0-EternalJunior-0/html2md-clean.git
Quick start
from markdown import MarkdownGenerator
html = """
<html><body>
<nav>site menu</nav>
<article>
<h1>Article title</h1>
<p>This is the main paragraph with useful text.</p>
</article>
<footer>© 2026</footer>
</body></html>
"""
generator = MarkdownGenerator()
result = generator.generate_from_html(html)
print(result.fit_markdown)
# # Article title
#
# This is the main paragraph with useful text.
print(result.word_count, result.h1)
The shortest variant:
from markdown import MarkdownGenerator
result = MarkdownGenerator.quick_generate(html)
print(result.markdown) # alias for fit_markdown
CLI
Once installed, the html2md command is available:
# from a file to stdout
html2md page.html
# from stdin to a file
cat page.html | html2md -o page.md
# keep links (disabled by default in the CLI profile)
html2md page.html --include-links
# force BeautifulSoup instead of Selectolax
html2md page.html --force-bs4
You can also run it as a module without installing:
python main_service.py page.html -o page.md
Usage examples
Custom options
from markdown import MarkdownGenerator, MarkdownOptions
options = MarkdownOptions(
include_links=True,
include_images=True,
generate_citations=True,
max_length=50_000,
clean_urls=True, # strip utm_*, fbclid, gclid
base_url="https://example.com",
)
generator = MarkdownGenerator(options)
result = generator.generate_from_html(html)
print(result.markdown_with_citations)
for ref in result.references:
print(ref)
Reference-style links
from markdown import MarkdownGenerator, MarkdownOptions
from markdown.options import LinkStyle
options = MarkdownOptions(link_style=LinkStyle.REFERENCE)
result = MarkdownGenerator(options).generate_from_html(html)
Asynchronous call
import asyncio
from markdown import generate_markdown_async
async def run():
result = await generate_markdown_async(html)
print(result.fit_markdown)
asyncio.run(run())
Extracting publication dates (optional)
from markdown import MarkdownGenerator, MarkdownOptions
options = MarkdownOptions(extract_dates=True)
result = MarkdownGenerator(options).generate_from_html(html)
print(result.published_date, result.updated_date)
Web-scraping facade
from main_service import HTMLToMarkdownConverter
converter = HTMLToMarkdownConverter() # more aggressive scraping defaults
markdown = converter.convert(html)
Real-world example: configuring MarkdownOptions for scraping a blog
Below is how you would do it "for real": take the HTML of a news/blog page, deliberately enable/disable the options you need, and understand why each one matters.
from markdown import MarkdownGenerator, MarkdownOptions
from markdown.options import LinkStyle, AGGRESSIVE_NOISE_CLASSES, COMMON_NOISE_PATTERNS
options = MarkdownOptions(
# --- main-content extraction ---
use_content_density=True, # look for <article>/<main>, not the whole <body>
link_density_threshold=0.4, # discard menus/footers more strictly (more links = noise)
min_content_words=40, # short blocks are not treated as main content
# --- noise removal ---
remove_ads=True,
noise_classes=AGGRESSIVE_NOISE_CLASSES, # + sidebar/related/comments/carousel
remove_display_none=True, # drop hidden inline blocks
dynamic_noise_patterns=COMMON_NOISE_PATTERNS, # "5 min ago", "Apply now", counters
# --- what to keep in the Markdown ---
include_links=True,
include_images=True,
include_tables=True,
generate_citations=True, # produce a variant with footnotes [1], [2]
# --- links and URLs ---
link_style=LinkStyle.REFERENCE, # cleaner than inline in long articles
normalize_urls=True,
base_url="https://example-blog.com", # relative /article -> absolute
clean_urls=True, # strip utm_*, fbclid, gclid
# --- text normalization ---
fix_text_encoding=True, # repair mojibake (requires ftfy)
normalize_unicode=True, # fullwidth/ligatures -> ASCII (NFKC)
max_length=200_000, # don't truncate long-reads prematurely
)
generator = MarkdownGenerator(options)
result = generator.generate_from_html(html)
print(result.markdown_with_citations) # text with [1], [2]
print("words:", result.word_count, "| links:", result.rendered_link_count)
for ref in result.references:
print(ref) # {'id': 1, 'text': '...', 'url': '...'}
Tip. Options are immutable, so keep one "base" profile and make targeted variations with
with_overrides— no need to duplicate the whole set:BASE = MarkdownOptions(use_content_density=True, include_links=True) # for e-commerce cards: a shorter candidate-length threshold ecommerce = BASE.with_overrides(main_content_min_length=100, include_images=True) # for a "raw" dump without link filtering raw_dump = BASE.with_overrides(include_links=False, use_content_density=False)
Public API
Import: from markdown import ...
| Object | Purpose |
|---|---|
MarkdownGenerator |
The main generator. generate_from_html(html) -> MarkdownResult, quick_generate(html), generate(source). |
MarkdownOptions |
Immutable conversion config. Use with_overrides(**kwargs). |
MarkdownResult |
Result: fit_markdown, raw_markdown (lazy), markdown_with_citations, metadata and counters. |
generate_markdown_async(html, options=None) |
Async wrapper (thread executor). |
configure_logging(level=WARNING) |
Configure the package logger. |
COMMON_NOISE_PATTERNS |
A ready-made set of regexes for dynamic_noise_patterns. |
Key MarkdownResult fields:
| Field | Type | Description |
|---|---|---|
fit_markdown / markdown |
str |
The main cleaned Markdown. |
raw_markdown |
str |
The full Markdown without aggressive filtering (lazy generation). |
markdown_with_citations |
str |
Variant with footnotes [1], [2]. |
references |
list[dict] |
List of sources for the citations. |
title, h1, description |
str |
Page metadata. |
word_count, char_count, heading_count, link_count, image_count |
int |
Statistics. |
published_date, updated_date |
str | None |
Dates (only with extract_dates=True). |
is_truncated |
bool |
Whether the text was truncated to max_length. |
Conversion options
MarkdownOptions is the single source of truth for defaults. The most common:
| Option | Default | Description |
|---|---|---|
remove_nav / remove_header / remove_footer / remove_aside |
True |
Remove the corresponding semantic blocks. |
remove_ads |
True |
Remove by noise_classes / noise_ids. |
remove_hidden |
True |
Strip aria-hidden, HTML5 hidden, hidden classes. |
include_links |
True |
Render links. |
include_images |
False |
Render images. |
include_tables / include_lists / include_code_blocks / include_blockquotes |
True |
Support the corresponding elements. |
max_length |
100000 |
Maximum text length. |
min_paragraph_length |
3 |
Minimum paragraph length. |
generate_citations |
False |
Generate markdown_with_citations. |
link_style |
INLINE |
INLINE or REFERENCE. |
clean_urls |
False |
Strip UTM/tracker parameters (courlan). |
normalize_urls / base_url |
True / "" |
Absolutize relative URLs. |
use_content_density |
False |
Main-content extraction heuristic. |
link_density_threshold |
0.5 |
Link-density threshold for discarding menus/footers. |
force_beautifulsoup |
False |
Force BS4 instead of Selectolax. |
The full list is in markdown/options.py and in the documentation.
Options are immutable: create new variants with with_overrides:
base = MarkdownOptions()
scraping = base.with_overrides(include_links=False, max_length=200_000)
Optional dependencies
Some features are enabled by installing extra packages (the library degrades gracefully if they are missing):
| Feature | Option | Package |
|---|---|---|
| Date extraction | extract_dates |
htmldate |
| HTML sanitization | sanitize_html |
nh3 |
| Mojibake repair | fix_text_encoding |
ftfy |
| URL cleaning | clean_urls |
courlan |
| Boilerplate extraction | use_trafilatura_extraction |
trafilatura |
| Fast parser | — (auto) | selectolax |
Performance
- Selectolax is used automatically when installed and is significantly faster than BeautifulSoup.
raw_markdownis generated lazily — you pay for it only when you access it.MarkdownOptionsis immutable, so generator instances can safely be shared across threads.
Metadata
Release files for html2md-clean 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| html2md_clean-0.1.1.tar.gz | 50.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| html2md_clean-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 97.6 kB
Release files / html2md_clean-0.1.1.tar.gz
| Download URL | html2md_clean-0.1.1.tar.gz |
|---|---|
| Size | 50.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bfce5019674513df288425dd3e5072b485e3b4684b3464443242f9442dbfa74f
|
|
BLAKE2b-256 checksum How to use checksums |
dfa863bf9767a2fcbd1e50476a29de00021cff471185e858b320abac51bdb19e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.5
|
Release files / html2md_clean-0.1.1-py3-none-any.whl
| Download URL | html2md_clean-0.1.1-py3-none-any.whl |
|---|---|
| Size | 47.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
94cf9d603de4414833196e5afd6ba773c11b600547b3f04d56cc1a7f6e4b370c
|
|
BLAKE2b-256 checksum How to use checksums |
e41b91abdfe8f3eab62c368b1f6d5eec7b3448e9a5a8e55d0397bf2681bfa729
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.5
|
Security for html2md-clean 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.