html2text는 패키지 하나만 설치되며 용량은 0.2 MiB에 불과합니다. 가장 가까운 Python 대안보다 9배 작고, Node 쪽 대안보다도 40배 작습니다. 변환 세트의 4개 페이지를 모두 처리했고, 등록된 본문 프로브 16개도 전부 통과했습니다. 다만 이 프로브는 선택한 본문 문자열이 살아남는지만 확인할 뿐, 계층 구조, 목록 중첩, 링크 목적지, 반복 콘텐츠, 또는 표의 완전한 보존 여부까지 점수화하지는 않습니다.
또한 이 라이브러리는 GPL-3.0-or-later 라이선스를 사용합니다. 이번 비교에서 벤치마크에는 전혀 드러나지 않지만, 라이브러리 채택 자체를 막아버릴 수도 있는 유일한 요소입니다.
html2text란 무엇인가
html2text는 HTML을 Markdown 비슷한 일반 텍스트로 바꿔주는 Python 라이브러리입니다. 계보는 Aaron Swartz의 원본으로 거슬러 올라가며, 현재 유지보수 중인 라인은 2025.4.15입니다. 2025년 4월에 나온 날짜 기반 릴리스로, GitHub 스타는 2,168개, 열려 있는 이슈는 95개이며, 마지막 푸시는 2025년 10월이었습니다.
공식 참고: html2text 공식 저장소.
import html2text
h = html2text.HTML2Text()
h.body_width = 0 # 아래 설명 참고; 기본값은 의외일 수 있습니다
md = h.handle(html)
pip install html2text는 패키지 1개와 0.2 MiB만 가져오며, 콜드 임포트 시간은 0.077초입니다. 의존성도 없습니다. 컨테이너 이미지나 Lambda layer에서는 markdownify의 1.8 MiB, turndown의 8.8 MiB와 비교해 꽤 의미 있는 차이입니다.
모든 수치를 바꾸는 기본값

body_width의 기본값은 78입니다. 즉, html2text는 이 설정을 끄지 않으면 출력의 모든 줄을 78자에서 강제로 줄바꿈합니다.
이 기본값은 원래 터미널이나 이메일용으로 읽기 쉬운 일반 텍스트를 만들던 라이브러리에는 꽤 타당합니다. 하지만 모델 입력이나 diff 용도에는 맞지 않습니다. 이런 경우 삽입된 줄바꿈이 토큰화를 바꾸고, 긴 링크를 중간에서 끊어 놓고, 결과 비교를 무의미하게 만들기 때문입니다.
아래 내용에서는 모두 body_width = 0으로 설정했습니다. 이 점을 숨기지 않고 분명히 밝히는 이유도 같습니다. 줄바꿈을 켜두면 이 글의 문자 수와 토큰 수는 전부 달라집니다. 직접 변환기를 벤치마크할 때도 이 설정 때문에 숫자가 조용히 비교 불가능한 상태가 될 수 있습니다.
측정 방법
html2text는 markitdown 비교에서도 사용했던 이름 있는 4페이지 변환 세트에 적용했습니다. 여기에 사전 등록된 프로브 문자열을 더했습니다. 즉, 반드시 살아남아야 하는 본문 문자열과, 페이지의 외곽 구조가 함께 들어왔는지 확인하는 보일러플레이트 문자열입니다. 4개 파일은 모두 동일하고, 프로브도 동일하며, 네 변환기를 하나의 채점 기준으로 비교했습니다. 프로브 생존은 등록된 문자열의 존재만 측정할 뿐 구조적 정확성은 보지 않습니다. 그래서 표와 링크 열은 따로 두었습니다.
| 변환기 | 본문 프로브 | 출력 문자 수 | 토큰 수(o200k) | Markdown 표 행 수 | 링크 수 |
|---|---|---|---|---|---|
| html2text | 16/16 | 76,452 | 21,176 | 32 | 545 |
| markdownify | 16/16 | 76,868 | 21,062 | 36 | 599 |
| markitdown | 16/16 | 76,995 | 21,336 | 36 | 598 |
| turndown | 16/16 | 95,188 | 26,236 | 0 | 611 |
fourway-scores.json. 4개 샘플이며, 토큰은 o200k_base 기준으로 집계했습니다.
네 도구 중 출력물이 가장 작았습니다. 문자 수는 76,452자로 가장 적었고, 토큰 수도 markdownify와 markitdown과 사실상 비슷했습니다. 21,176개로, 각각 21,062개와 21,336개였으니 차이는 1.3%에 불과해 차이라고 부르기 어렵습니다.
표 행 수는 32개로, 4페이지 세트에서 markdownify와 markitdown의 36개보다 적었습니다. 이 4행 차이는 다음 섹션에서 설명할 바깥 파이프 형식 차이 때문이 아니라, 불규칙한 Wikipedia 샘플에서 발생했습니다.
링크 수는 545개로 가장 적었습니다. 다른 도구들은 598~611개였습니다. 링크 보존이 중요하다면 본인 페이지에서도 꼭 확인해 보세요. 이 열은 html2text가 그룹 안에 있는 수준이 아니라, 분명히 아래에 있는 유일한 항목입니다.
정규식으로는 보이지 않던 표

이 얘기를 하는 이유는, 제가 거의 잘못된 결론을 그대로 공개할 뻔했기 때문입니다.
처음 만든 표 행 카운터는 앞뒤 파이프가 있어야만 카운트했습니다. ^\|.*\|$ 규칙이었습니다. 이 규칙으로는 html2text가 5개 파일 전체에서 표 행 1개만 만든 것으로 나왔습니다. 4페이지 변환 세트와 별도의 합성 복잡 표 샘플을 포함한 결과였죠. 하지만 그 카운터는 표를 센 것이 아니라, 특정 Markdown 스타일 하나만 센 것이었습니다.

실제로는 이렇게 출력합니다.
Team Name | Year | Wins | Losses | Win %
---|---|---|---|---
Boston Bruins | 1990 | 44 | 24 | 0.55
바깥 파이프가 없습니다. 전형적인 pipe-table 문법이긴 하지만, 제가 만든 테스트 하네스는 Markdown 렌더러 간 호환성까지 검증하지 않았습니다. 그래서 바깥 파이프 형식을 기대하는 정규식에는 보이지 않았습니다. 카운터를 다시 만들어, 내부에 구분 행이 있는 파이프 포함 연속 라인을 찾도록 바꾸자 html2text는 4페이지 세트에서 32행, 5개 파일 전체에서는 91행으로 바뀌었습니다.
즉, 결론은 html2text에 표가 없다는 뜻이 아닙니다. 네 변환기 중 두 개는 바깥 파이프를 포함하고, 하나는 그렇지 않다는 점이 중요하다는 뜻입니다. Markdown을 나중에 자체 패턴 매칭으로 후처리한다면 특히 그렇습니다. 첫 숫자만 믿었으면 이 사실을 완전히 놓칠 뻔했습니다.
무엇을 제거하고, 어디서 행을 잃는가
서로 반대 방향을 가리키는 두 가지 결과가 있습니다.
<script>와 <style>는 제거합니다. 이 요소 안에서만 나타나는 마커로 계산해 보면, html2text의 출력은 모든 샘플에서 script 마커 0개, style 마커 0개였습니다. 반면 turndown은 각각 10개와 84개를 남겼습니다. Wikipedia 샘플에서는 MediaWiki의 인라인 JavaScript 설정과 CSS가 8줄, 총 14,644자나 됐습니다. (script-style-stripping.json) 모델용 출력에서는 이 샘플에서 피할 수 있었던 텍스트의 가장 큰 원천이었습니다. 다운스트림 비용 모델은 측정하지 않았습니다.
어려운 페이지에서는 표 행을 잃습니다. 4페이지 세트의 결과를 정리하면 아래와 같습니다.
| 샘플 | html2text | markdownify |
|---|---|---|
| Books to Scrape | 0행 | 0행 |
| Quotes to Scrape | 0행 | 0행 |
| Hockey statistics | 27행 | 27행 |
| Wikipedia | 5행 | 9행 |
| 4페이지 합계 | 32행 | 36행 |
별도의 합성 복잡 표 샘플에서는 html2text가 59행, markdownify가 62행을 기록해 5개 파일 합계는 각각 91행과 98행이 됩니다. 이 샘플은 4페이지 핵심 비교에는 포함되지 않았습니다. 깔끔한 하키 표에서는 둘이 같습니다. 하지만 표가 중첩되고 불규칙한 Wikipedia에서는 html2text가 markdownify보다 4행 적습니다.
정리하면 이렇습니다. 단순한 표는 동일하게 처리하고, 까다로운 표는 html2text가 더 적게 살립니다. 페이지에 Wikipedia 같은 표가 있다면, 도입 전에 반드시 시험해 보세요. 반대로 통계 페이지 같은 표라면 이 항목에서는 둘을 거의 같은 것으로 봐도 됩니다.
라이선스
| 라이브러리 | 라이선스 | 패키지 수 | 디스크 사용량 |
|---|---|---|---|
| html2text | GPL-3.0-or-later | 1 | 0.2 MiB |
| markdownify | MIT | 5 | 1.8 MiB |
| turndown | MIT | 3(npm) | 8.8 MiB |
공식 참고: PyPI의 html2text.
PyPI 메타데이터, GitHub 저장소, 그리고 설치된 패키지의 METADATA 파일에서 모두 확인됐습니다. License-Expression: GPL-3.0-or-later라고 적혀 있습니다.
이 의미는 소프트웨어를 어떻게 통합하고, 전달하고, 배포하느냐에 따라 달라집니다. 내부용이나 네트워크 전용 사용은, 패키지가 포함되거나 결합된 소프트웨어를 배포하는 경우와 GPL 관점에서 다른 시나리오입니다. 다만 이 글은 법률 분석이 아닙니다. 소프트웨어를 배포하는 팀이라면 정확한 통합 및 배포 모델을 법무 검토에 맡기는 것이 좋습니다.
어색한 부분은 상관관계입니다. 가장 작은 설치 용량을 가진 라이브러리, 즉 배포 산출물을 최대한 작게 유지하려고 고를 법한 그 라이브러리가, 실제로는 배포를 가장 강하게 제약하는 라이선스를 갖고 있습니다. 대안 둘은 모두 MIT입니다.
저는 변호사가 아니며, 이 글은 법률 자문도 아닙니다. 다만 이런 특성은 비교표에서 가장 중요할 가능성이 높고, 또 가장 잘 드러나지 않는 부분이므로 사실 그대로 적었습니다.
유지보수 상태
마지막 릴리스는 2025.4.15, 마지막 저장소 푸시는 2025년 10월이었습니다. 테스트 시점으로부터 약 10개월 전이며, 그 사이 릴리스는 41개 쌓여 있었습니다. requires_python >= 3.9이고, Python 3.14.2에서 문제없이 설치되고 실행됐습니다.
이건 markdownify보다 조용한 편입니다(테스트 6주 전 마지막 릴리스). turndown은 4개월 전이었고요. 아무것도 없는 상태보다는 확실히 활발합니다. HTML을 텍스트로 바꾸는 문제는 크게 변하지 않는 문제이므로, 10개월 간격은 방치보다는 안정적이라고 보는 편이 맞습니다. 다만 열려 있는 이슈가 95개라는 점은 더 신경 써야 할 부분입니다. 실제 사용 사례와 비슷한 이슈가 있는지 미리 훑어볼 가치는 충분합니다.
메모리와 깨진 HTML의 영향
여기서는 운영 관점의 두 질문을 따로 측정했습니다.
더 넓은 스트레스 테스트 맥락은 10개 라이브러리 메모리 및 잘못된 HTML 비교에 있습니다.
최대 상주 메모리는 /usr/bin/time -l로 측정했습니다. 셀마다 새 프로세스를 하나씩 띄웠고, import floor는 라이브러리를 불러와 유휴 상태로 둘 때 드는 비용, peak는 문서까지 처리했을 때의 최대치입니다.
| 라이브러리 | 런타임 | import floor | 226 KB peak | 10 MB peak |
|---|---|---|---|---|
| html2text | python3.14 | 18.7 | 19.9 | 71.2 |
| pyquery | python3.14 | 30.3 | 33.9 | 172.5 |
| resiliparse | python3.14 | 20.5 | 25.1 | 225.1 |
| markdownify | python3.14 | 23.9 | 28.9 | 278.5 |
| goose3 | python3.14 | 44.1 | 52.4 | 398.5 |
| cheerio | node22 | 66.8 | 76.5 | 398.5 |
| justext | python3.14 | 30.3 | 36.6 | 431.2 |
| newspaper4k | python3.14 | 52.6 | 61.8 | 668.5 |
| trafilatura | python3.14 | 52.5 | 64.8 | 927.1 |
| turndown | node22 | 47.8 | 68.4 | 2947.1 |
memory-results.json. Python과 Node의 기준값은 서로 직접 비교할 수 없습니다. 두 환경 모두 인터프리터가 포함되기 때문입니다.
html2text는 두 측정 축 모두에서 여기 가장 가벼운 항목입니다. import floor 18.7 MiB, 10 MiB 샘플에서 peak 71.2 MiB로, 추가 RSS는 52.5 MiB입니다. (71.2 - 18.7) / 10 = 5.25×로 샘플 크기의 5.25배입니다. 위 표의 절대값과 증가량을 함께 보되, 앞서 말한 Python/Node 기준값 주의사항도 고려해야 합니다.
깨진 HTML. 닫히지 않은 태그, 잘못 중첩된 인라인 요소, 공백이 있는 따옴표 없는 속성, 불필요한 닫는 태그, <html> 자체가 없는 문서, 중복 속성, 중간에서 잘린 문서, 잘못된 엔티티, 닫히지 않은 <script>, 거짓말하는 charset 선언, 마크업을 포함한 주석, 그리고 600단계 중첩까지. 여기에 정상 형식의 제어 샘플 2개를 같은 크기로 추가했습니다. “아무것도 반환하지 않았다”는 말은, 같은 크기의 깨끗한 문서에도 침묵하는 라이브러리라면 그 의미가 약하기 때문입니다.
html2text는 14개 중 0개에서 예외를 던졌고, 아무것도 반환하지 않은 경우도 0개였습니다. 깨진 샘플 전체에서 33개의 센티넬을 모두 복구했습니다. (malformed-results.json) 단, 이 집계에서 하나는 제외했습니다. HTML5 규칙상 닫히지 않은 <script> 뒤의 내용은 전부 script 콘텐츠이므로, 그 내용을 잃는 것은 맞는 동작이고 되살리는 쪽이 오히려 규칙에서 벗어나는 것입니다.
장단점
장점. 패키지 1개, 0.2 MiB, 의존성 0개. 이번 비교에서 압도적으로 가장 작습니다. 출력도 4개 중 가장 작았고, 토큰 수도 markdownify와 markitdown과 사실상 비슷했습니다. 알아볼 수 있는 pipe-table 문법을 내보냅니다. Python 3.14에서 동작합니다. 계보도 길고 안정적입니다.
단점. GPL-3.0-or-later 라이선스인데, 나머지 대안들은 그렇지 않습니다. 기본 body_width=78이라서, 직접 끄지 않으면 출력이 강제로 줄바꿈되고 측정이나 diff가 달라집니다. 보존된 링크 수가 가장 적습니다(545개로, 다른 도구들의 598~611개보다 적음). 표는 바깥 파이프가 없는 스타일을 써서 단순한 다운스트림 정규식을 깨뜨릴 수 있습니다. 열려 있는 이슈가 95개이며, markdownify보다 릴리스 주기도 조용합니다.
누가 써야 하고, 누가 쓰지 말아야 하나
다음 조건이라면 html2text를 쓰세요. 의존성 예산이 정말 빡빡하고, 의도한 통합 및 배포 모델이 라이선스 검토를 통과한 경우입니다. 의존성 0개에 패키지 1개라는 점은 운영상 확실한 장점입니다. 감사와 배포 대상이 되는 의존성 표면이 작아지니까요.
첫 줄에서 body_width = 0으로 바꾸세요. 특별히 줄바꿈된 일반 텍스트가 필요한 경우가 아니라면, 이 설정은 사실상 필수입니다.
다음 조건이라면 피하세요. 소프트웨어를 배포하는데 카피레프트가 문제라면 피하세요. markdownify는 MIT이고, 여기서는 토큰 수가 비슷했으며, markitdown과 표 결과도 맞추면서 디스크는 1.6 MiB 더 사용합니다. 링크 보존이 중요하다면 피하세요. html2text가 가장 적었기 때문입니다. 그리고 다운스트림 도구가 표 행의 바깥 파이프를 전제로 한다면 역시 피하세요.
관리형 API는 어디에 들어맞나
html2text는 이미 가지고 있는 HTML을 변환합니다. 페이지를 가져오거나, JavaScript를 렌더링하거나, 안티봇 계층을 처리하지는 않습니다. 네 가지 변환기 모두 마찬가지인데, 실제 대상 사이트에서는 바로 그 부분이 더 어려운 절반입니다.
같은 5개 변환기 기준으로 같은 샘플을 비교한 내용은 5개 HTML-to-Markdown 비교를 보세요.
우리의 Thunderbit를 포함한 호스티드 fetch/render/extraction 서비스는 다른 계층에서 동작합니다. Thunderbit은 여기서 벤치마크하지 않았습니다. 중요한 경계는 사용자가 제공한 HTML을 변환하는 것과, URL을 가져와 처리하는 서비스의 차이입니다. 이 글은 동일한 지표 기준의 품질, 지연 시간, 비용 비교를 제공하지 않습니다.
공정하게 말하면 이렇습니다. HTML을 이미 갖고 있고, Markdown이 필요하며, 배포 방식상 GPL이 문제가 아니라면 html2text는 무료이고 놀랄 만큼 작습니다. 반대로 페이지를 가져와야 하거나, 문장보다 행이 중요하다면 그건 전혀 다른 선택입니다.
더 넓은 분야는 웹 스크래핑 API 총정리에서 호스티드 옵션을, 오픈소스 스크레이퍼 종합 가이드에서 셀프호스티드 옵션을 다룹니다. Python에서 HTML을 Markdown으로 변환하기는 실무용 안내서입니다.
html2text를 써야 할까?
용량이 중요하고, 줄바꿈을 의도적으로 껐으며, 배포 모델이 라이선스 검토를 통과한다면 유력한 후보입니다.
4페이지 세트에서 토큰 수는 markdownify와 markitdown 대비 1.3% 이내였습니다. 그렇다고 전체 품질이 같다는 뜻은 아닙니다. html2text는 링크를 더 적게 보존했고, 불규칙한 Wikipedia 샘플에서는 행도 더 적게 살렸습니다. 바로 확인해야 할 운영 포인트는 두 가지입니다. body_width = 0, 그리고 바깥 파이프가 없는 표 스타일입니다.
GPL 검토 때문에 제외된다면 markdownify를 보세요. 여기서는 MIT이고, 출력 크기와 토큰 수가 비슷했으며, 링크와 불규칙 표 행도 더 많이 살렸고, 이 환경에서는 디스크를 1.6 MiB 더 사용했습니다.
웹 데이터 추출용 Thunderbit 체험하기 Get Started Free
자주 묻는 질문
html2text는 표를 변환하나요?
네. 처음 만든 카운터로는 5개 파일에서 표 행 1개만 만든 것으로 나왔지만, 그 카운터가 틀렸습니다. 앞뒤 파이프가 있어야만 세도록 만들어져 있었고, html2text는 Team Name | Year | Wins처럼 바깥 파이프 없이 출력하기 때문입니다. 이것은 전형적인 pipe-table Markdown이지만, 제가 쓴 하네스는 렌더러 간 호환성 테스트까지는 하지 않았습니다. 수정된 카운터로는 4페이지 세트에서 html2text가 32행, markdownify가 36행이었고, 별도의 복잡 표 샘플을 포함하면 91행 대 98행이었습니다.
body_width는 무엇이고, 왜 바꾸나요?
기본적으로 78자마다 강제 줄바꿈을 합니다. 터미널용 일반 텍스트에는 괜찮지만, 그 외 용도에는 별로입니다. 줄바꿈이 들어가면 문장 중간이 끊기고, 긴 URL이 잘리고, 토큰화도 달라집니다. 이번 리뷰의 모든 수치는 body_width = 0에서 측정했습니다. 기본값을 썼다면 숫자는 전부 달라졌을 겁니다.
GPL 라이선스가 정말 문제인가요?
정확한 통합 및 배포 모델에 따라 다릅니다. 내부용이나 네트워크 전용 사용, 그리고 소프트웨어 배포는 서로 다른 심사 시나리오입니다. 다만 이 글은 법적 결론을 내리지 않습니다. 소프트웨어를 배포하는 팀이라면 GPL-3.0-or-later 조건을 법무 검토에 맡기세요. markdownify와 turndown은 MIT입니다. html2text의 라이선스 표현은 PyPI 메타데이터, GitHub, 그리고 설치된 패키지의 METADATA 파일에서 모두 확인됐습니다.
2025년 4월 릴리스면 문제인가요? 단독으로는 아마 아니라고 봐도 됩니다. HTML-to-text 변환은 꽤 안정적인 문제이고, 라이브러리는 Python 3.14.2에서 문제없이 설치 및 실행됐으며, 뒤에 41개의 릴리스가 쌓여 있습니다. 제가 실제로 확인할 숫자는 오히려 95개의 열려 있는 이슈입니다. 사용하려는 입력과 비슷한 문제가 있는지 먼저 훑어보세요. 조용한 저장소일수록 결국 직접 고치게 될 가능성이 높으니까요.
여기서 무엇은 테스트하지 않았나요?
4개의 변환 샘플과 1개의 별도 복잡 표 샘플만으로는 여전히 작은 세트입니다. 대신 12개의 합성 malformed 문서와 2개의 제어 샘플은 테스트했습니다. html2text는 14개 중 0개에서 예외를 던졌고, 14개 중 0개에서 빈 결과를 반환했으며, 채점 대상 센티넬 33개를 모두 복구했습니다. 하지만 실제 웹에서 망가진 페이지, 더 다양한 malformed 패턴, 중첩 목록, 정의 목록, 각주, 수식은 다루지 않았습니다. ignore_links, ignore_images, unicode_snob, single_line_break 등 전체 옵션들은 body_width를 제외하면 모두 기본값이었습니다. 링크 손실은 관찰했지만 원인은 분석하지 않았고, Markdown 왕복 변환(round-tripping)도 테스트하지 않았습니다.


