HTML source becoming structured Markdown through fetching, content extraction, conversion, and normalization

Konwersja HTML do Markdown polega na zastąpieniu znaczników HTML lekką składnią tekstową przy zachowaniu obsługiwanych nagłówków, linków, list i kodu. W przypadku pobranych stron wyodrębnij potrzebną treść jeszcze przed konwersją, następnie ujednolić linki i zapisz metadane źródła.

Użyteczny wynik to dokument, który możesz sprawdzić, zacytować i zaindeksować. Ten poradnik omawia Pythona, JavaScript, trudne tabele i eksport całych witryn, a także zawiera migawkę liczby tokenów, która pokazuje również ryzyko usunięcia przydatnej treści.


Najważniejsze fakty

  • Konwersja HTML do Markdown zachowuje obsługiwaną strukturę dokumentu, a odrzuca większość znaczników prezentacyjnych.
  • Wyodrębnianie głównej treści i konwersja do Markdown to dwie osobne operacje.
  • Nasza migawka z 15 października 2026 obejmuje 11 publicznych stron, od Wikipedii i MDN po indeks BBC i dokument RFC.
  • Konwersja całych stron do Markdown zmniejszyła łączną liczbę tokenów o 84,7% w tej migawce.
  • Ekstrakcja wraz z wyjściem w Markdown zmniejszyła łączną liczbę tokenów o 94,3%, ale na niektórych stronach z istotną utratą tekstu.
  • Mniejsza liczba tokenów nie dowodzi wyższej trafności odpowiedzi.
  • Znaczniki czasu pobrania powinny opisywać moment zebrania danych, a nie datę publikacji.

Co zachowuje, a co traci konwersja HTML do Markdown?

Konwersja zachowuje tekst i obsługiwaną strukturę dokumentu. Zwykle traci układ CSS, zachowania interaktywne oraz atrybuty HTML, które nie mają odpowiednika w Markdown. Składnia obrazów zachowuje odwołanie, a nie informacje zawarte w samym obrazie.

Poniższe odwzorowania opierają się na CommonMark oraz specyfikacji GitHub Flavored Markdown. Dokładny wynik zależy od ustawień konwertera.

HTML Typowy Markdown
h1-h6 Nagłówki od # do ######
p Akapity oddzielone pustymi wierszami
a [etykieta](URL)
img ![tekst alternatywny](URL)
ul / ol Listy punktowane / numerowane
table Tabela GFM z kreskami pionowymi lub zachowany HTML
pre / code Bloki kodu w ogrodzeniu / odwrotne apostrofy w tekście
strong / em **pogrubienie** / *wyróżnienie*
blockquote > cytowany tekst

Skorzystaj z poniższego 4-etapowego potoku czystej konwersji. Ekstrakcję traktuj jako decyzję o treści, a konwersję jako decyzję o reprezentacji. Szerzej tę różnicę wyjaśnia nasz poradnik o parsowaniu danych.

1. Pobranie > 2. Wyodrębnienie głównej treści > 3. Konwersja > 4. Normalizacja
Zapisz źródło przed ekstrakcją, a spacje, linki i nagłówki normalizuj bez przepisywania kodu.

Czy konwersja HTML do Markdown zmniejsza liczbę tokenów i poprawia trafność LLM?

Markdown może zmniejszyć liczbę tokenów i szum znaczników, ale mniejsze dane wejściowe nie dowodzą lepszych odpowiedzi. Zachowanie treści i trafność w zadaniu mierz oddzielnie od oszczędności tokenów.

Nasza migawka z 15 października 2026, godz. 14:48 UTC obejmuje 11 stron (10 stron zewnętrznych i jeden wpis na blogu DataImpulse). Łączna liczba tokenów spadła z 742 763 dla surowego HTML do 113 844 dla Markdown całych stron (markdownify), a następnie do 42 037 dla Markdown głównej treści (Trafilatura): odpowiednio o 84,7% i 94,3% mniej niż w surowym HTML.

Liczba tokenów dla tych samych 11 stron internetowych (tiktoken o200k_base)

Źródło: pomiar DataImpulse z 15 października 2026. W metodzie użyto jednego żądania GET na adres URL, user agenta desktopowej Chrome, bez renderowania JavaScript i tego samego HTML dla każdego konwertera. Tokenizację wykonano z użyciem o200k_base i tiktoken 0.14.0; zapisane zależności to markdownify 1.2.3, Trafilatura 2.3.1, html-to-markdown 3.18.1 i Beautiful Soup 4.15.0.

Reprezentacja Łączna liczba tokenów Redukcja względem surowego HTML
Surowy HTML 742 763 0%
HTML bez skryptów 460 148 38,0%
markdownify, cała strona 113 844 84,7%
html-to-markdown, cała strona 171 882 76,9%
html-to-markdown, po wcześniejszym usunięciu script/style 80 154 89,2%
Markdown głównej treści z Trafilatura 42 037 94,3%

To migawka, a nie benchmark. Końcowy wynik jest o 63,1% mniejszy niż wynik markdownify, ale ta różnica łączy ekstrakcję i zachowanie konwertera. Nie wyodrębnia ona przyczynowego udziału samej ekstrakcji.

Kontrola zachowania treści ma znaczenie: liczba słów po ekstrakcji wyniosła 1 314 wobec 2 676 widocznych słów w artykule Cloudflare, 1 677 wobec 3 289 w RFC 9309 oraz 410 wobec 1 251 w indeksie BBC. Takie porównania wskazują możliwe straty, ale nie są dokładnymi wskaźnikami kompletności.

Cloudflare osobno podaje 16 180 tokenów HTML wobec 3 150 tokenów Markdown, czyli około 80% mniej, w swoim przykładzie Markdown for Agents. Z kolei TABVERSE wskazuje, że HTML jest często najbardziej odporną reprezentacją tekstową w zadaniach z tabelami. Żaden z tych wyników nie wskazuje uniwersalnie najlepszego formatu.


Której biblioteki do konwersji powinieneś użyć?

Wybieraj według środowiska uruchomieniowego, potrzeb w zakresie ekstrakcji i wierności tabel. Udokumentowane możliwości poniżej są aktualne na październik 2026; szybkości nie porządkujemy w ranking, bo jej nie mierzyliśmy.

Biblioteka Środowisko Tabele Obsługa zbędnych elementów Najlepsze zastosowanie
markdownify Python Tabele z kreskami pionowymi; wnioskowanie nagłówka DOM czyścisz osobno Własne reguły konwersji
html2text Python Markdown lub pominięcie konwersji (HTML) Ekstrakcja osobno Konwersja nastawiona na tekst
Turndown JavaScript Wtyczka GFM Reguły usuwania; ekstrakcja osobno Potoki w JavaScript
JohannesKaufmann/html-to-markdown Go Wtyczka do tabel Selektory i reguły Usługi w Go
Pandoc CLI Zależy od dialektu wyjściowego Ekstrakcja osobno Przepływy pracy z dokumentami
Trafilatura Python Opcja ekstrakcji tabel Wyodrębnianie głównej treści Kolekcje artykułów
  • Wybierz markdownify, gdy chcesz kontrolować czyszczenie. Unikaj markdownify, gdy oczekujesz automatycznego wyboru artykułu.
  • Wybierz html2text, gdy priorytetem jest czytelny tekst. Unikaj html2text, gdy nieprzetestowana rekonstrukcja tabel jest nie do zaakceptowania.
  • Wybierz Turndown, gdy konwersja ma działać w JavaScript. Unikaj Turndown, gdy oczekujesz, że konwerter sam pobierze lub wyrenderuje strony.
  • Wybierz Trafilatura, gdy ekstrakcja artykułów pasuje do Twoich źródeł. Unikaj Trafilatura, gdy liczy się każdy element paska bocznego lub indeksu, a nie robisz kontroli zachowania treści.
  • Wybierz hostowane reader API, gdy chcesz pobierać i konwertować treści zdalnie. Unikaj hostowanego reader API, gdy dokumenty muszą pozostać offline.

Jak przekonwertować HTML do Markdown w Pythonie?

Sparsuj HTML, wybierz właściwy kontener, usuń zbędne węzły i przekaż wynik do markdownify. W przypadku lokalnych partii zachowaj ścieżki katalogów i przenoś metadane zbierania z pliku pomocniczego.

Ten przepis korzysta z operacji na DOM w Beautiful Soup oraz z udokumentowanych opcji markdownify dotyczących nagłówków i języka kodu. Zapisz go jako convert_batch.py.

python -m pip install markdownify beautifulsoup4
python convert_batch.py saved-html markdown-output
import json
import sys
from pathlib import Path
from urllib.parse import urljoin
from bs4 import BeautifulSoup
from markdownify import markdownify


def code_language(pre):
    for node in (pre, pre.find('code')):
        if node:
            for name in node.get('class', []):
                if name.startswith('language-'):
                    return name[len('language-'):]
    return None


def convert(html, source_url):
    soup = BeautifulSoup(html, 'html.parser')
    title = soup.title.get_text(' ', strip=True) if soup.title else ''
    base_tag = soup.find('base', href=True)
    base = urljoin(source_url, base_tag['href']) if base_tag else source_url
    root = soup.find('main') or soup.find('article') or soup
    for node in root.select('script, style, nav, footer'):
        node.decompose()
    for tag, attr in [('a', 'href'), ('img', 'src')]:
        for node in root.find_all(tag):
            if node.get(attr):
                node[attr] = urljoin(base, node[attr])
    body = markdownify(
        str(root), heading_style='ATX',
        table_infer_header=False,
        code_language_callback=code_language,
    ).strip() + '\n'
    return title, body


def write_document(path, metadata, body):
    path.parent.mkdir(parents=True, exist_ok=True)
    fields = '\n'.join(
        f'{key}: {json.dumps(value, ensure_ascii=True)}'
        for key, value in metadata.items()
    )
    path.write_text(f'---\n{fields}\n---\n\n{body}', encoding='utf-8')


def batch(source, destination):
    for path in sorted(source.rglob('*.html')):
        metadata = json.loads(
            path.with_suffix('.html.json').read_text(encoding='utf-8')
        )
        title, body = convert(path.read_bytes(), metadata['source_url'])
        output = destination / path.relative_to(source).with_suffix('.md')
        write_document(output, {
            'title': title,
            'source_url': metadata['source_url'],
            'retrieved_at': metadata['retrieved_at'],
        }, body)


if __name__ == '__main__':
    batch(Path(sys.argv[1]), Path(sys.argv[2]))

Każdy plik page.html.json musi zawierać source_url oraz oryginalny znacznik czasu retrieved_at. Nie podstawiaj w jego miejsce czasu modyfikacji pliku ani daty publikacji. Wynik używa frontmatter w YAML z wartościami skalarnymi w cudzysłowie; brakujące pliki pomocnicze powodują widoczny błąd zamiast zmyślania pochodzenia danych.


Jak przekonwertować HTML do Markdown w JavaScripcie?

Użyj Turndown na wyodrębnionym fragmencie HTML i włącz jego wtyczkę do tabel GFM. Poniższy przykład w Node.js jest zgodny z oficjalną instrukcją wtyczki.

npm install turndown turndown-plugin-gfm
node convert.cjs
const TurndownService = require('turndown');
const { tables } = require('turndown-plugin-gfm');

const converter = new TurndownService({
  headingStyle: 'atx',
  codeBlockStyle: 'fenced'
});
converter.use(tables);
converter.remove(['script', 'style', 'nav', 'footer']);

const html = `
<article>
  <h1>Inventory</h1>
  <p>Read the <a href="https://example.com/docs">documentation</a>.</p>
  <table>
    <thead><tr><th>Item</th><th>Count</th></tr></thead>
    <tbody><tr><td>Books</td><td>12</td></tr></tbody>
  </table>
</article>`;
console.log(converter.turndown(html));

Przykład używa linków bezwzględnych. W środowisku produkcyjnym przed konwersją rozwiąż odwołania względne względem bazowego adresu URL pobranego dokumentu, a adnotacje języka kodu przetestuj osobno.


Której biblioteki Pythona użyć do zachowania tabel HTML?

Użyj konwertera z wyraźnymi ustawieniami dla tabel, a potem przetestuj swoje rzeczywiste struktury komórek. W przypadku scalonych komórek lub zagnieżdżonej treści zachowaj oryginalny HTML tabeli zamiast zakładać, że tabele z kreskami pionowymi oddają znaczenie.

Tabele z kreskami pionowymi są rozszerzeniem GFM i nie należą do rdzenia CommonMark. Specyfikacja tabel GFM obsługuje treść liniową w komórkach, ale nie potrafi wyrazić rowspan, colspan ani list blokowych wewnątrz komórek.

Biblioteka Udokumentowane ustawienie, stan na październik 2026 Priorytet testu
markdownify table_infer_header=False zostawia pusty nagłówek; True awansuje pierwszy wiersz Nie zamieniaj przypadkiem danych w etykiety
html2text bypass_tables zwraca tabele jako HTML Sprawdź zachowane atrybuty i strukturę komórek
Python html-to-markdown, xberg br_in_tables i compact_tables Sprawdź komórki wielowierszowe i znaki ucieczki separatorów

Pakiet xberg dla Pythona to osobny projekt, niezależny od konwertera w Go autorstwa JohannesKaufmann. To są udokumentowane ustawienia, a nie wyniki przeprowadzonego porównania wierności.

Przepuść te cztery przypadki testowe przez każdego kandydata i sprawdź wyrenderowany wynik:

Przypadek Komórka lub wiersz wejściowy Warunek zaliczenia
Bez nagłówka <tr><td>Alice</td><td>7</td></tr> Alice pozostaje danymi
Scalona komórka <td colspan="2">Total</td> Semantyka scalenia przetrwa w zachowanym HTML
Dosłowna kreska pionowa <td>A | B</td> Jedna komórka, z kreską pionową poprzedzoną znakiem ucieczki w Markdown
Zagnieżdżona lista <td><ul><li>A</li></ul></td> Hierarchia listy przetrwa w zachowanym HTML

W przypadku przekazywania bez zmian zachowaj złożone węzły tabel przed konwersją i przywróć je później jako bloki HTML. Upewnij się, że docelowy renderer akceptuje HTML.


Jak zamienić adres URL na czysty Markdown za pomocą API?

Wyślij adres URL do endpointu reader, poproś o wynik skoncentrowany na treści i zachowaj metadane źródła razem z odpowiedzią. Przed zaindeksowaniem sprawdź zwrócony dokument pod kątem brakujących sekcji.

Tabela porównuje udokumentowane rozwiązania na październik 2026. Offline oznacza przetwarzanie bez usługi hostowanej; pobranie aktywnego adresu URL nadal wymaga dostępu do sieci.

Opcja Uwierzytelnianie Główna treść Renderowanie JS Metadane Offline Zakres
Jina Reader Anonimowo lub klucz API Tryby oparte na Readability Dostępny silnik przeglądarki Tryb frontmatter Nie dla hostowanego endpointu Pojedynczy URL; udokumentowano też tryb spider
Firecrawl Start bez klucza; klucz dla wyższych limitów Opcja onlyMainContent Obsługiwane Metadane odpowiedzi Nie dla hostowanego API Scrape i crawl
Crawl4AI, lokalnie Brak klucza hostowanego dla użycia lokalnego Skonfiguruj filtr treści Przepływ z przeglądarką Metadane wyniku Lokalny przepływ HTML Pojedynczy URL i głębokie crawlowanie
Cloudflare Markdown for Agents Obowiązują reguły dostępu serwera źródłowego Udokumentowane przetwarzanie wstępne Brak kroku przeglądarki w negocjacji Frontmatter, gdy dostępny Nie Żądana strona
Cloudflare Browser Rendering /markdown Token API dla REST Sprawdź wynik ekstrakcji Renderowanie w przeglądarce URL i czas zapisz samodzielnie Nie Żądana strona

W przypadku Jina Reader poniższe polecenie żąda frontmatter i ekstrakcji treści:

curl --fail --show-error \
  -H 'X-Respond-With: frontmatter' \
  'https://r.jina.ai/https://example.com/'

Negocjacja Accept: text/markdown w Cloudflare działa w strefach z włączonym Markdown for Agents. Endpoint /markdown usługi Browser Rendering to osobna trasa, a nie nagłówek włączający renderowanie w przeglądarce na dowolnych witrynach.


Jak przekonwertować całą witrynę na pliki Markdown?

Zacznij od adresów początkowych lub adresów URL z mapy witryny, ogranicz wykrywanie stron i wyeksportuj jeden dokument na każdy źródłowy adres URL. Przy każdym pliku zapisz tytuł, czas pobrania i źródłowy adres URL, aby późniejsze odpowiedzi pozostały możliwe do prześledzenia.

Opcja, stan na październik 2026 Przepływ pracy Uwagi dotyczące eksportu
Firecrawl Crawl Wykrywanie przez mapę witryny i linki; renderowanie Zapisz zwrócone dokumenty i metadane do plików
Crawl4AI deep crawling Lokalne crawlowanie z kontrolą głębokości i liczby stron Markdown wyniku i metadane zapisz samodzielnie
SiteOne Crawler Crawlowanie witryny i eksport do Markdown Wynik w wielu plikach lub połączony
Website Content Crawler, apify/website-content-crawler Crawlowanie przez HTTP lub przeglądarkę z czyszczeniem Wyeksportuj pola Markdown ze zbioru danych do plików

Dla małej statycznej witryny, którą kontrolujesz, zapisz ten skrypt jako crawl_static.py obok konwertera w Pythonie. Do pobierania przez HTTP używa on biblioteki Requests, ogranicza głębokość i liczbę prób oraz zapisuje znaczniki czasu zbierania. Pomija przekierowania i nie renderuje JavaScript.

python -m pip install requests
python crawl_static.py https://example.com/
import hashlib
import sys
import time
from collections import deque
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urldefrag, urljoin, urlsplit
import requests
from bs4 import BeautifulSoup
from convert_batch import convert, write_document

seed = urldefrag(sys.argv[1])[0]
origin = urlsplit(seed)
queue = deque([(seed, 0)])
seen = {seed}
content_files = {}
attempts = 0

while queue and attempts < 20:
    url, depth = queue.popleft()
    attempts += 1
    time.sleep(1)
    try:
        response = requests.get(url, timeout=30, allow_redirects=False)
        if response.status_code != 200:
            print('Skipped:', response.status_code, url)
            continue
        if 'text/html' not in response.headers.get('Content-Type', ''):
            continue
        html = response.content
        retrieved_at = datetime.now(timezone.utc).isoformat()
        title, body = convert(html, url)
        digest = hashlib.sha256(body.encode('utf-8')).hexdigest()
        filename = hashlib.sha256(url.encode('utf-8')).hexdigest() + '.md'
        metadata = {
            'title': title, 'source_url': url,
            'retrieved_at': retrieved_at,
        }
        if digest in content_files:
            metadata['duplicate_of'] = content_files[digest]
        else:
            content_files[digest] = filename
        write_document(Path('site-md') / filename, metadata, body)

        if depth < 2:
            soup = BeautifulSoup(html, 'html.parser')
            base_tag = soup.find('base', href=True)
            base = urljoin(url, base_tag['href']) if base_tag else url
            for link in soup.select('a[href]'):
                target = urldefrag(urljoin(base, link['href']))[0]
                parts = urlsplit(target)
                if (parts.scheme, parts.netloc) != (origin.scheme, origin.netloc):
                    continue
                if target not in seen and len(seen) < 500:
                    seen.add(target)
                    queue.append((target, depth + 1))
    except requests.RequestException as error:
        print('Failed:', url, error)

Zduplikowana treść jest oznaczana bez utraty informacji o pochodzeniu źródła; oznaczone kopie pomijaj podczas indeksowania. Skróty adresów URL zapewniają stabilne nazwy plików. Przy większych zadaniach dodaj wczytywanie map witryn, trwałe kolejki, renderowanie tam, gdzie jest potrzebne, oraz eksporty z punktami kontrolnymi.

Przestrzegaj zasad dostępu do witryn, ograniczaj tempo żądań i ponawiaj próby po przejściowych błędach z wykorzystaniem backoff. Routing residential może pomóc w diagnozowaniu ograniczeń przepustowości powiązanych z konkretnym IP, ale nie zastępuje kontroli tempa żądań. Zobacz konfigurację proxy dla Crawl4AI, architekturę proxy dla RAG i zgodne z zasadami przepływy zbierania danych.


Co psuje się podczas konwersji i jak to naprawić?

Większość błędów wynika z niekompletnych danych wejściowych, zbyt agresywnej ekstrakcji lub struktur, których Markdown nie potrafi odwzorować. Zachowaj źródłowy HTML i przed skalowaniem porównaj reprezentatywne wyniki.

Sygnał błędu Prawdopodobna przyczyna Naprawa
Pusty artykuł Tekst dostarczył JavaScript Wyrenderuj stronę i poczekaj na element z treścią
Tekst menu lub cookies Czyszczenie pominęło zbędne elementy Dostosuj selektory do konkretnej witryny
Uszkodzone linki/obrazy Odwołania względne utraciły bazę Rozwiąż je względem dokumentu/bazowego adresu URL
Kod traci język Odrzucono metadane klas Odwzoruj klasy języka w ogrodzeniach kodu
Myląca tabela Spłaszczone scalenia lub zagnieżdżona treść Zachowaj HTML tabeli
Podejrzanie mały wynik Ekstrakcja odrzuciła przydatne sekcje Porównaj nagłówki, komórki i tekst
Mapa ryzyka konwersji: sprawdzaj znaczenie, a nie tylko rozmiar wyniku.

Regex czy parser DOM? Do zagnieżdżonych znaczników i obsługi encji użyj parsera. Regex zostaw do wąsko określonego czyszczenia tekstu po parsowaniu.

Narzędzie online: przydatne dla małego, niewrażliwego fragmentu, który możesz od razu sprawdzić.
Rozszerzenie przeglądarki: przydatne przy wybieraniu treści z już wyrenderowanej strony; sprawdź uprawnienia.
Biblioteka offline: przydatna przy powtarzalnych partiach i dokumentach, które muszą pozostać lokalnie.

Konwersja do Markdown nie oczyszcza zachowanego HTML ani nie usuwa prompt injection z tekstu strony. Trzymaj pobraną treść oddzielnie od instrukcji i testuj jakość wyszukiwania na pytaniach, których odpowiedzi możesz zweryfikować.


Najczęściej zadawane pytania

Czy mogę przekonwertować HTML do Markdown bez utraty czegokolwiek?

Na ogół nie. Układ, zachowania interaktywne i niektóre relacje strukturalne nie mają bezpośredniego odpowiednika w Markdown, więc gdy te szczegóły mają znaczenie, zachowaj źródłowy HTML.

Czy przed konwersją warto usuwać skrypty?

Zwykle tak, w przypadku dokumentu skoncentrowanego na tekście. Jeśli dane strukturalne wewnątrz skryptów są przydatne, wyodrębnij je do osobnych metadanych przed usunięciem tych węzłów.

Czy najmniejszy wynik w Markdown to najlepsze dane wejściowe dla RAG?

Nie. Ekstraktor może wytworzyć bardzo mały dokument, odrzucając treść zawierającą odpowiedź, dlatego obok liczby tokenów oceniaj zachowanie treści i jakość wyszukiwania.

Czy tabela bez nagłówka powinna zyskać wiersz nagłówka?

Tylko wtedy, gdy uzasadnia to semantyka źródła. Automatyczne awansowanie pierwszego wiersza danych może zmienić znaczenie każdego kolejnego wiersza.

Czy lokalny przepis na przetwarzanie wsadowe może działać bez dostępu do internetu?

Tak, po zainstalowaniu zależności oraz zebraniu plików HTML i plików pomocniczych z metadanymi. Sama konwersja nie musi pobierać oryginalnych stron.

Jaki znacznik czasu należy do frontmatter w Markdown?

Użyj czasu pobrania źródła dla rekordu kolekcji. Daty publikacji trzymaj w osobnym polu, gdy wiarygodne metadane źródła je podają.

Czy crawlowanie witryny gwarantuje kompletny eksport?

Nie. Limity wykrywania, niedostępne strony, przekierowania i błędy renderowania mogą zostawić luki, dlatego porównaj wyeksportowane adresy URL z zakładanym spisem.


Potrzebujesz routingu residential dla swojego potoku zbierania danych?

Jeśli Twój przepływ zbierania danych wymaga routingu przez residential IP, poznaj residential proxy DataImpulse. DataImpulse zapewnia infrastrukturę proxy, a nie API do scrapingu, zarządzany unblocker ani VPN; pobieranie, ekstrakcja, konwersja i walidacja pozostają częścią Twojego potoku.

Ostatnia aktualizacja: 15 października 2026.


Share article: