html转markdown
简介
一个将 HTML 转换为 Markdown 的 Python 库, GitHub
安装方式
pip install markdownify
✅ 特点
- 基于 BeautifulSoup:
- HTML 首先被解析为 DOM 结构,因此能很好地处理嵌套标签和无效 HTML。
- 高度可定制:
- 支持自定义标签的转换方式。
- 可以选择保留或移除特定标签、属性。
- 支持常见 Markdown 元素:
- 标题、段落、链接、图片、粗体、斜体、列表、引用、表格(部分支持)等。
- 适合爬虫/内容迁移等场景:
- 非常适合将 HTML 页面内容转存为 Markdown 格式,供笔记、博客、Git 库使用。
使用
基本用法
# html转markdown
from markdownify import markdownify as md
md('<b>Yay</b> <a href="http://github.com">GitHub</a>') # > '**Yay** [GitHub](http://github.com)'# 排除某些标签
md('<b>Yay</b> <a href="http://github.com">GitHub</a>', strip=['a']) # > '**Yay** GitHub'# 只解析指定的标签
from markdownify import markdownify as md
md('<b>Yay</b> <a href="http://github.com">GitHub</a>', convert=['b']) # > '**Yay** GitHub'# 将一个BeautifulSoup对象转成markdown
from markdownify import MarkdownConverterdef md(soup, **options):return MarkdownConverter(**options).convert_soup(soup)
表格保留html
markdown表格无法处理单元格合并,目前很多系统都是将表格以html格式展示,单元格里的内容转markdown.
from markdownify import MarkdownConverterclass CustomMarkdownConverter(MarkdownConverter):def convert_table(self, el, text, parent_tags):def convert_cell(cell):# cell里的内容,继续转markdowninner_html = ''.join(str(child) for child in cell.contents)value = self.convert(inner_html)value = re.sub(r'(\s*\n\s*){2,}', '<br>', value) # 替换换行避免table被截断value = re.sub(r'!\[(.*?)]\((.*?)\)', r'<img alt="\1" src="\2"/>', value) # md图片转htmlreturn valuedef attrs_str(cell):attrs = []for attr in ['colspan', 'rowspan']:if attr in cell.attrs:attrs.append(f'{attr}="{cell[attr]}"')return ' ' + ' '.join(attrs) if attrs else ''html_rows = []for row in el.find_all('tr'):html_cells = []for cell in row.find_all(['th', 'td']):tag = cell.nameattr = attrs_str(cell)markdown_text = convert_cell(cell)html_cells.append(f"<{tag}{attr}>{markdown_text}</{tag}>")html_rows.append(f"<tr>{''.join(html_cells)}</tr>")return f"\n<table>{''.join(html_rows)}</table>\n"def to_md(html: str, **options):return CustomMarkdownConverter(**options).convert(html)
解析自定义html tag
非标准html标签,可以自定义convert_方法,需要把[]:-
替换成_
,例如ac:image
对应的方法是convert_ac_image
。
from markdownify import MarkdownConverterclass ConfluenceMarkdownConverter(MarkdownConverter):def convert_ac_image(self, el, text, parent_tags):"""Confluence Image示例<ac:image ac:thumbnail="true" ac:width="100"><ri:attachment ri:filename="image2022-5-9 22:6:43.png" /></ac:image>"""attachment = el.find('ri:attachment')if attachment and attachment.has_attr('ri:filename'):filename = attachment['ri:filename']filename_encoded = urllib.parse.quote(filename)download_url = f"{self.base_url}/download/attachments/{self.page_id}/{filename_encoded}"# 下载图片local_dir = os.path.join("files", "images")os.makedirs(local_dir, exist_ok=True)local_path = os.path.join(local_dir, self.sanitize_filename(filename))response = requests.get(download_url, headers=self._make_auth_header())if response.ok:with open(local_path, "wb") as f:f.write(response.content)return f"\n})\n"return ''
附录
Markdownify 支持的选项
strip
要剥离的标签列表。此选项不能与 convert
选项同时使用。
convert
要转换的标签列表。此选项不能与 strip
选项同时使用。
autolinks
一个布尔值,表示当 <a>
标签的内容与其 href
相同时,是否使用“自动链接”样式。默认值为 True
。
default_title
一个布尔值,如果链接没有提供标题,则是否将其标题设为 href
。默认值为 False
。
heading_style
定义标题应如何被转换。可选值包括:
ATX
(如# Heading
)ATX_CLOSED
(如# Heading #
)SETEXT
(如Heading\n=====
)UNDERLINED
(是SETEXT
的别名)
默认值为 UNDERLINED
。
bullets
用于无序列表项的符号集合,可以是字符串、列表或元组。如果只包含一个项目,将用于所有嵌套层级。否则会根据嵌套层级交替使用。默认值为 '*+-'
。
strong_em_symbol
在 Markdown 中,*
和 _
都可以用于加粗或斜体。此选项用于选择其中之一:
ASTERISK
(默认)UNDERSCORE
sub_symbol
, sup_symbol
定义包围 <sub>
和 <sup>
内容的字符。默认值为空字符串,因为这种行为并不标准。你可以使用类似 ~sub~
或 ^sup^
的写法。
如果值以 <
开头并以 >
结尾,会被当作 HTML 标签处理,并在标签后添加 /
,比如使用 <sub>
生成原始 HTML 子脚本标签。
newline_style
定义如何在 Markdown 中标记换行(<br>
):
SPACES
(默认):两个空格加换行符。BACKSLASH
:使用\\n
(反斜杠+换行)替代。虽然非标准,但很多解析器支持并偏好这种方式。
code_language
定义 <pre>
代码块的默认语言。例如,假如页面中所有代码都是 Python 的,可以设为 'python'
。默认值为 ''
(空字符串),可设为任意字符串。
code_language_callback
用于从 <pre>
标签中提取语言信息的回调函数,例如从 class
属性中获取语言:
def callback(el):return el['class'][0] if el.has_attr('class') else None
回调接收一个 BeautifulSoup
元素,返回语言字符串或 None
。默认值为 None
。
escape_asterisks
是否将 *
转义为 \*
。默认值为 True
。设为 False
则不转义。
escape_underscores
是否将 _
转义为 \_
。默认值为 True
。设为 False
则不转义。
escape_misc
是否转义其他在 Markdown 中可能具有特殊含义的符号。默认值为 False
。
keep_inline_images_in
如果图片处于标题或表格单元格中,通常会被转换为其 alt
文本。若希望保留为 Markdown 图片,可将此选项设为允许包含图片的父标签列表,例如 ['td']
。默认值为空列表。
table_infer_header
控制当表格没有显式标题行(没有 <thead>
或 <th>
)时的处理方式。若设为 True
,会将首行当作标题行。默认值为 False
。
wrap
, wrap_width
如果 wrap
为 True
,所有文本段落将在 wrap_width
个字符处换行。默认值为 False
,wrap_width
为 80
。设为 None
可不限制长度。
推荐与 newline_style=BACKSLASH
一起使用,以保留段落中的换行。
strip_document
控制是否在转换后的文档中移除前导/后缀的空行。可选值为:
LSTRIP
:移除开头空行RSTRIP
:移除结尾空行STRIP
:同时移除None
:不移除
默认值为STRIP
。文档内部的换行不会受影响。
beautiful_soup_parser
指定用于解析 HTML 的 BeautifulSoup 解析器。可以是 html5lib
、lxml
或其他已安装的解析器。默认值为 html.parser
。
所有选项既可以作为
markdownify()
函数的参数传入,也可以在继承自MarkdownConverter
的子类中通过嵌套Options
类定义。
confluence文档解析
confluence支持很多宏,这里只实现了code宏的解析。
"""
pip install markdownify
pip install httpx requests
"""
import asyncio
import os
import re
import urllib
from urllib.parse import urljoinimport httpx
import requests
from markdownify import MarkdownConverterclass ConfluenceMarkdownConverter(MarkdownConverter):def __init__(self, base_url: str, page_id: int, token: str, **options):"""将一个confluence页面转成markdown:param base_url: confluence的域名,如:https://confluence.xxx.com:param token: confluence的访问token:param options: markdownify解析相关配置"""super().__init__(**options)self.base_url = base_urlself.page_id = page_idself.token = tokendef _make_auth_header(self):return {'Authorization': f'Basic {self.token}'}@staticmethoddef sanitize_filename(filename: str) -> str:# 替换 Windows 文件名非法字符: \ / : * ? " < > |return re.sub(r'[\\/:*?"<>|]', '_', filename)async def load_page_html(self):url = urljoin(self.base_url, f'/rest/api/content/{self.page_id}')params = {'expand': 'body.storage'}headers = self._make_auth_header()async with httpx.AsyncClient() as client:response = await client.get(url, headers=headers, params=params)response.raise_for_status()data = response.json()return data['body']['storage']['value'] # HTML 内容def convert_table(self, el, text, parent_tags):def convert_cell(cell):# cell里的内容,继续转markdowninner_html = ''.join(str(child) for child in cell.contents)value = self.convert(inner_html)value = re.sub(r'(\s*\n\s*){2,}', '<br>', value) # 替换换行避免table被截断value = re.sub(r'!\[(.*?)]\((.*?)\)', r'<img alt="\1" src="\2"/>', value) # md图片转htmlreturn valuedef attrs_str(cell):attrs = []for attr in ['colspan', 'rowspan']:if attr in cell.attrs:attrs.append(f'{attr}="{cell[attr]}"')return ' ' + ' '.join(attrs) if attrs else ''html_rows = []for row in el.find_all('tr'):html_cells = []for cell in row.find_all(['th', 'td']):tag = cell.nameattr = attrs_str(cell)markdown_text = convert_cell(cell)html_cells.append(f"<{tag}{attr}>{markdown_text}</{tag}>")html_rows.append(f"<tr>{''.join(html_cells)}</tr>")return f"\n<table>{''.join(html_rows)}</table>\n"def convert_ac_image(self, el, text, parent_tags):"""Confluence Image示例<ac:image ac:thumbnail="true" ac:width="100"><ri:attachment ri:filename="image2022-5-9 22:6:43.png" /></ac:image>"""attachment = el.find('ri:attachment')if attachment and attachment.has_attr('ri:filename'):filename = attachment['ri:filename']filename_encoded = urllib.parse.quote(filename)download_url = f"{self.base_url}/download/attachments/{self.page_id}/{filename_encoded}"# 下载图片local_dir = os.path.join("files", "images")os.makedirs(local_dir, exist_ok=True)local_path = os.path.join(local_dir, self.sanitize_filename(filename))response = requests.get(download_url, headers=self._make_auth_header())if response.ok:with open(local_path, "wb") as f:f.write(response.content)return f"\n})\n"return ''def convert_ac_structured_macro(self, el, text, parent_tags):# 确保是 code 类型if el.get("ac:name") == "code":# 提取语言lang_el = el.find("ac:parameter", {"ac:name": "language"})lang = lang_el.text.strip() if lang_el else ""# 提取代码内容code_el = el.find("ac:plain-text-body")if code_el:code_text = code_el.text or ""code_text = code_text.strip()return f"\n```{lang}\n{code_text}\n```\n"else:print(f'unknown marco name: {el.get("ac:name")}')return ''async def to_md(self):html = await self.load_page_html()return await asyncio.to_thread(self.convert, html)if __name__ == '__main__':convertor = ConfluenceMarkdownConverter(base_url='https://confluence.demo.com', page_id=123, token=os.environ.get('token'))md = asyncio.run(convertor.to_md())with open('files/confluence_demo.md', 'wb') as f:f.write(md.encode('utf-8'))