直接用cmark是最稳选择,因其需启用CMARK_OPT_TABLES标志才能识别表格节点,遍历AST时须逐层判断节点类型并递归提取文本,导出CSV需手写escape函数处理双引号、逗号和换行符。

直接用 cmark 是最稳的选择,其他纯 C++ 实现(比如手写 parser 或 boost::spirit)要么维护成本高,要么对表格、嵌套、转义等边界情况支持弱——尤其当你需要导出 CSV 或提取结构化数据时,AST 遍历是唯一可靠路径。
cmark_parse_document 必须传 CMARK_OPT_TABLES 才能识别表格节点
默认情况下 cmark_parse_document 完全忽略 | 构成的表格,连 CMARK_NODE_TABLE 都不会生成。这不是 bug,是设计:CommonMark 规范里表格属于扩展语法,必须显式启用。
- 调用时第三个参数不能只写
CMARK_OPT_DEFAULT,得改成CMARK_OPT_DEFAULT | CMARK_OPT_TABLES - 如果后续遍历 AST 时发现
cmark_node_get_type(node) == CMARK_NODE_TABLE永远不成立,八成就是漏了这个标志 -
CMARK_OPT_TABLES不影响 HTML 渲染结果,只影响 AST 结构——也就是说,cmark_render_html输出照样好看,但你拿不到行列信息
遍历表格 AST 时必须逐层判断 node 类型,不能假设子节点是文本
CMARK_NODE_TABLE_ROW 的子节点确实是 CMARK_NODE_TABLE_CELL,但每个 CMARK_NODE_TABLE_CELL 内部不是直接存字符串,而是可能嵌套 CMARK_NODE_PARAGRAPH、CMARK_NODE_TEXT,甚至 CMARK_NODE_CODE —— 直接调 cmark_node_get_literal(cell) 会返回 nullptr,解引用就崩溃。
- 安全做法:先用
cmark_node_get_type(child)判断,只对CMARK_NODE_TEXT调cmark_node_get_literal - 遇到
CMARK_NODE_PARAGRAPH就得递归遍历其子节点,否则会丢内容 - 单元格里如果有换行(比如用两个空格+回车),
cmark会拆成多个CMARK_NODE_TEXT节点,得拼起来
导出 CSV 不能依赖 std::quoted,必须手写 escape 函数
std::quoted 只加首尾双引号,不处理内部双引号转义,也不包裹含 \n 的字段——而 Markdown 表格单元格里换行很常见,CSV 规范要求这种字段必须用双引号包围,且内部双引号要变成两个。
立即学习“C++免费学习笔记(深入)”;
- 正确逻辑:只要字段含
"、,或\n,就包裹双引号,并把所有"替换为"" - 别忘了空列:Markdown 的
|a||c|必须导出为"a",,"c",中间的""不能省略 - 换行符在 CSV 中是合法字符,但必须出现在双引号内;否则解析器会误判为新行
初始化和内存释放容易被忽略的三处硬伤
cmark 是 C 库,没有 RAII,所有节点都是 malloc 出来的,不手动清理会泄漏;但更隐蔽的问题是初始化本身。
- 首次调用前最好先调一次
cmark_version(),某些旧版本不初始化会导致后续解析失败(尤其静态链接时) -
cmark_node_free(document)必须调,且只能调一次——重复 free 会 crash - 如果读大文件,别一次性
read_file加载进内存,改用cmark_parser_feed流式解析,否则可能 OOM
真正卡住人的从来不是“怎么开始”,而是 AST 遍历中途某个 nullptr 崩溃、CSV 导出后 Excel 打开乱码、或者表格行数对不上——这些问题都藏在类型判断、转义规则和内存生命周期里,没法跳过。


















