AB
AiBoss站
project

cjkfold - 把 PDF 里「长得像汉字」的部首字符折回真正的汉字

cjkfold 是一个零依赖的 Python 库,用于修复从 PDF 抽取文本时混入的康熙部首、CJK 部首补充区等「形似汉字」字符,并提供命令行工具检查、扫描与就地改写文本文件。

cjkfold 是一个用 Python 编写的小型文本处理库,目标是解决一个很具体、但排查起来相当费时的问题:从 PDF 抽取出来的中文文本里,常常混着一些看起来和普通汉字一模一样、编码却完全不同的字符。它们不会报错,显示也正常,因为字形确实相同;坏掉的是所有下游的字符比较——关键词检索搜不到、去重和缓存对不上、向量检索把无关的短文档排在包含原句的文档前面。项目适合需要处理 PDF 抽取文本、构建检索或知识库流水线、以及做文本清洗与数据校验的开发者。

维护者与状态

项目由 cdcailf 维护,仓库创建于 2026-10-13,最后一次提交在 2026-10-15。最新正式版本为 v0.4.0,发布于 2026-10-15。截至 2026-10-24,仓库星标数为 186。项目采用 MIT 许可证。

主要能力

以下能力描述来自项目 README 的自述,属于维护者视角的说明,并非中立评测。

  • 形似字符折叠:据项目说明,fold() 会把康熙部首(U+2F00–U+2FDF)与 CJK 部首补充区(U+2E80–U+2EFF)中形似汉字的字符折回对应的统一表意文字,例如把 ⻆ 折成 角、把 ⾊ 折成 色。README 称其借助 Unicode 字符数据库(Unicode 15.1+)中的 EquivalentUnifiedIdeograph 属性,补上了 NFKC 在部首补充区留下的缺口。
  • 刻意不做 NFKC:项目自述 fold() 是幂等的,并且有意不触碰全角标点、兼容字符与连字,因此可以安全地作用于仍要存储或展示的文本;需要两者兼得时,由调用方自行先做 NFKC 再折叠。
  • 检测与遍历:提供 has_lookalikes() 判断字符串中是否存在形似字符,iter_lookalikes() 逐条产出位置、原码位、目标码位与字符名称。
  • 另外两种改写:normalize() 执行 NFKC,但默认保留全角标点(因为 NFKC 会把「,」变成「,」,那是改写句子而不是修复);strip_invisible() 默认只删除零宽空格、BOM、软连字符、词连接符等安全类别,遇到 ZWJ 这类承载语义的字符则保持不动,需要全部删除时必须显式指定 policy="all"。
  • 命令行工具:check 报告形似字符并在发现时以退出码 1 结束,适合接入 CI;scan 报告全部类别的发现(形似、NFKC 可改写、不可见、以及 U+FFFD 与私用区码位这类无法修复的损伤),默认退出码为 0,因为它是报告而非闸门;fold 输出折叠后的文本,加 -w 可就地改写文件。
  • 退出码区分:据 README,退出码 2 专门表示输入不是文本文件或不是 UTF-8、什么都没扫描到,与「发现了问题」的 1 区分开;传入 PDF 时工具会提示改用 pdftotext,而不是从二进制数据里报出幻影发现。

使用入口

仓库地址:https://github.com/cdcailf/cjkfold

据项目说明,安装方式为:

pip install cjkfold

Python 调用示例(摘自 README):

from cjkfold import fold, has_lookalikes, iter_lookalikes

fold("紫⾊的⻆落")      # '紫色的角落'
has_lookalikes("⻆")    # True
for item in iter_lookalikes("第一行\n第⼆行⻆"):
    print(item)

命令行用法示例:

cjkfold check document.txt
cjkfold scan corpus/
cjkfold scan --json corpus.txt
cjkfold fold -w document.txt

许可与限制

许可证为 MIT。

README 中写明的边界与限制包括:项目零运行时依赖、全部为纯函数、要求 Python 3.9+;fold() 不处理零宽字符家族,这是有意为之,因为其中一半(如 ZWJ)承载语义,删掉会把一个由五个码位组成的家庭 emoji 变成三个人;check 只检查形似字符这一类,所以一个满是零宽空格的文件也会被判为 clean,需要看全部类别时应改用 scan;U+FFFD 与私用区码位只报告、不可修复;scan 默认不充当闸门,需要闸门行为要显式加 --strict。此外,README 提到 0.4.0 之前模块与函数同名导致 import cjkfold.normalize as m 拿到的是函数而非模块,该问题已通过重命名模块(cjkfold.folding 与 cjkfold.rewrite)修复。