html转markdown

发布于:2025-06-12 ⋅ 阅读:(24) ⋅ 点赞:(0)

简介

一个将 HTML 转换为 Markdown 的 Python 库, GitHub

安装方式

pip install markdownify

✅ 特点

  1. 基于 BeautifulSoup:
  • HTML 首先被解析为 DOM 结构,因此能很好地处理嵌套标签和无效 HTML。
  1. 高度可定制:
  • 支持自定义标签的转换方式。
  • 可以选择保留或移除特定标签、属性。
  1. 支持常见 Markdown 元素:
  • 标题、段落、链接、图片、粗体、斜体、列表、引用、表格(部分支持)等。
  1. 适合爬虫/内容迁移等场景:
  • 非常适合将 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 MarkdownConverter

def md(soup, **options):
    return MarkdownConverter(**options).convert_soup(soup)

表格保留html

markdown表格无法处理单元格合并,目前很多系统都是将表格以html格式展示,单元格里的内容转markdown.

from markdownify import MarkdownConverter

class CustomMarkdownConverter(MarkdownConverter):

    def convert_table(self, el, text, parent_tags):
        def convert_cell(cell):
            # cell里的内容,继续转markdown
            inner_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图片转html
            return value

        def 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.name
                attr = 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 MarkdownConverter

class 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![{filename}](images/{self.sanitize_filename(filename)})\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
如果 wrapTrue,所有文本段落将在 wrap_width 个字符处换行。默认值为 Falsewrap_width80。设为 None 可不限制长度。
推荐与 newline_style=BACKSLASH 一起使用,以保留段落中的换行。

strip_document
控制是否在转换后的文档中移除前导/后缀的空行。可选值为:

  • LSTRIP:移除开头空行
  • RSTRIP:移除结尾空行
  • STRIP:同时移除
  • None:不移除
    默认值为 STRIP。文档内部的换行不会受影响。

beautiful_soup_parser
指定用于解析 HTML 的 BeautifulSoup 解析器。可以是 html5liblxml 或其他已安装的解析器。默认值为 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 urljoin

import httpx
import requests
from markdownify import MarkdownConverter


class 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_url
        self.page_id = page_id
        self.token = token

    def _make_auth_header(self):
        return {
            'Authorization': f'Basic {self.token}'
        }

    @staticmethod
    def 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里的内容,继续转markdown
            inner_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图片转html
            return value

        def 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.name
                attr = 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![{filename}](images/{self.sanitize_filename(filename)})\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'))


网站公告

今日签到

点亮在社区的每一天
去签到