本文讲述了作者近期使用光学字符识别(OCR)技术处理一本古籍的经历,这本古籍是19世纪末出版的18世纪法国作家圣西门的回忆录《圣西门回忆录》。
圣西门是路易十四晚期凡尔赛宫廷的朝臣,他的回忆录超过300万字,是那个时代的第一手资料。尽管其准确性存疑,但其文学价值却备受推崇,对19世纪和20世纪的著名法国作家产生了深远的影响,包括夏多布里昂、司汤达、雨果、福楼拜、龚古尔兄弟、左拉,当然还有普鲁斯特,他致力于为自己的时代创作一个新的虚构版本的回忆录。(如果不熟悉圣西门,很难真正欣赏普鲁斯特。)托尔斯泰也是圣西门的粉丝,还有许多其他人。
英文版的《圣西门回忆录》只有节选和部分翻译版本,对于一位如此重要的法国作家来说,这可能是独一无二的,这可能是由于其篇幅巨大所致。然而,法语版则有很多版本。其中,由博伊斯利斯于1879年至1930年编纂的版本仍然被认为是最好的(博伊斯利斯本人于1908年去世,但该工作由其他编辑继续,包括他的儿子)。该版本包含丰富、详细、精彩的脚注,涉及任何主题或人物。我们说的是大约45卷,每卷大约600页。
法国国家图书馆(Bibliothèque nationale)几年前扫描了这些实体书籍,并且可以在线获取,但仅限于图像格式,且界面非常笨拙,阅读起来很困难。
作者的目标很简单:创建一个可读的文本版本(不会将脚注和注释混入正文),可搜索,并且人们可以从中复制粘贴。OCR本身是最简单的部分——解析OCR引擎输出的结果才是真正的挑战。以下是任务、问题和解决方案的细分。
结果可以在此处查看(目前只有第一卷在线,并且没有针对移动设备进行优化)。
**面临的挑战**
页面包含以下可能的区域(颜色对应图像:
标题(蓝色)
边距中的注释(绿色)
正文(粉色)
脚注(黄色)
签名标记(紫色)
对书籍进行OCR意味着正确解析这些不同区域中的单词并正确地重建它们,以便生成可读的文本(而不仅仅是能够在页面中“随机”查找单词,例如Google图书所做的那样)。
**准备OCR图像**
开始使用Gallica的PDF非常简单。您可以下载完整扫描的书籍,并且有很多方法可以提取图像。作者使用了PDFTK,这是一个适用于Linux和Windows的免费工具:
```bash
pdftk document.pdf burst
```
此命令简单地将document.pdf拆分为单个PDF,每页一个。
查看此特定文档中的图像,它们质量不是很好——分辨率低,有些图像有点倾斜。由于OCR质量在很大程度上取决于输入图像质量,因此作者需要通过放大图像并将其校正来解决这些问题。ImageMagick被证明非常适合此目的——它是一个命令行工具,功能非常强大。
以下是作者用于将所有PDF文件从一个目录处理到另一个目录的命令:
```bash
for file in "$SOURCE_DIR"/*.pdf; do
if [ -f "$file" ]; then
filename=$(basename "$file" .pdf)
echo "Converting $file to $TARGET_DIR/$filename.jpg"
convert -density 300 -deskew 30 "$file" -quality 100 "$TARGET_DIR/$filename.jpg"
fi
done
```
现在,作者得到了一个包含已处理图像的目录,可以将其馈送到OCR引擎中。
**将图像发送到OCR引擎**
对于实际的OCR工作(将图片转换为文本),作者使用了Google Vision,它非常出色,而且价格合理。以下是将图像发送到其API的Python代码:
```python
client = vision.ImageAnnotatorClient()
with open(image_path, 'rb') as image_content:
image = vision.Image(content=image_content.read())
request = vision.AnnotateImageRequest(
image=image,
features=[{"type_": vision.Feature.Type.DOCUMENT_TEXT_DETECTION}],
)
response = client.annotate_image(request=request)
```
API会发送回一个JSON结构,其中包含两个主要部分:
* text_annotations:单词列表,其中第一个元素是整页文本,所有其他元素都是带有边界框(4个点坐标)的单个单词。
* full_text_annotation:块和字母列表,也带有边界框(作者最终没有使用它)。
(由于某些原因,斜体没有被识别,这是一个问题,但不是一个大问题。请参见本文末尾关于可能替代方案的讨论。)
**获取可读文本**
text_annotations中的单词大致按文档顺序排列,从左上到右下。你可能会认为第一个元素中的完整文本正是我们需要的,但有一个问题。实际上有几个问题:
* 许多页面在边距处有注释或副标题,这些注释或副标题不属于正文,但OCR会按照页面流程将它们混合在一起,从而造成混乱。
* 有大量的脚注需要正确标记,因为它们不是正文的一部分。
* 每个页面都有一个标题,我们需要将其删除以便在页面之间平滑阅读。
* 每16页,底部都会有一个“签名标记”。这些也需要删除。
因此,我们需要处理OCR输出以正确识别所有这些元素并正确标记段落。
**题外话:关于传统书籍印刷和签名标记的作用的更多细节**
书籍过去是在大张纸上印刷的,然后折叠成8折(八开本)或4折(四开本),然后缝合在一起制成书籍。以下是一个莎士比亚戏剧八开本印刷纸张的示例:
折叠和组装由与操作印刷机的团队不同的团队完成,他们需要说明才能按正确的顺序折叠和分组页面。这就是签名标记的目的:告诉装订工如何折叠纸张,以及折叠后如何对生成的册子进行分组。
因此,签名标记通常包含书籍的名称(以便它不会在印刷厂与其他书籍混淆)以及指示其顺序的数字或字母。它只印在纸张的一侧,当纸张折叠时,第一页应该出现在那里;因此,对于八开本,它将在印刷书籍(或扫描件)中每16页出现一次。
我们需要删除它,因为如果不删除,它最终会污染最终文本。一个非常粗略的方法是每16页删除最后一行,但如果扫描件丢失或插入等,则这种方法不会很稳健。作者更喜欢检查每一页的最后一行以获取签名标记的内容,并测量Levenshtein距离以解决OCR错误。
**居中与边距**
首先:确定什么是正文,什么是边距注释。页面的大小或布局并不完全相同,因此我们不能使用固定坐标。但正文始终是两端对齐的,单词按(大致)文档顺序排列。
因此,为了识别左右边距,作者对页面中的所有单词进行了一次扫描以识别最左边的单词和最右边的单词(并将其标记为这种):
* 跟踪每个单词的水平位置。
* 当一个单词的x位置小于前一个单词时,它是最左边的单词。
* 最右边的单词是在最左边单词之前的那个单词。
边距将是这些最左边和最右边单词“大多数”出现的位置。
为了理解,让我们以左边距为例:
* 一些单词位于最左边,因为它们属于左边距的注释,因此,如果我们只是取页面上任何单词的最低水平位置,我们最终会得到一个太靠左的左边距(因此,注释将被混合在正文中)。
* 一些单词,虽然是行中的第一个单词,但具有水平偏移量,这是因为它们标记了段落的开头(左缩进)或因为它们是引号或诗歌等。
通过将最左边的单词分组到存储桶中,并选择最大的存储桶,我们可以找到“对齐”在真实左边距上的单词,从而找到左边距。
我们可以通过诸如四分位距范围之类的统计方法,或通过舍入x位置并选择最常见的值来找到这些“大多数”位置。
一旦我们获得了这两组边缘词,任何比左边组更靠左的词都是左边距的注释,任何比右边组更靠右的词都是右边距的注释。
然后,我们可以进行另一次扫描以将所有内容正确地分类到边距注释或中心块中。
**行**
由于我们不使用完整文本而是使用单个单词,因此我们必须使用单词的坐标构建行(然后是段落)。
创建行非常简单:我们将每个部分中的单词按垂直位置分组,然后按单词的x位置对每行进行排序。
要按垂直位置进行分组,作者只需按顺序遍历单词:
* 如果一个单词在垂直方向上足够靠近前一个单词(在某个阈值内),它就属于当前行。
* 如果它太远,则开始新行。
找到合适的阈值需要一些反复试验。阈值太大,你会合并不同的行;阈值太小,你会不必要地拆分单行。
但是获得正确的垂直位置比看起来要棘手。即使在完全笔直的页面上,同一行上的单词也经常具有不同的y位置,因为它们的边界框包含不同的字母部分(如“p”的尾部或“t”的顶部)。
实际上,没有高字母或悬挂字母的单词“maman”将具有与具有两者字母的单词“parent”不同的y位置。我们真正需要的是基线,但不幸的是,我们没有得到它。
在处理行时,作者还:
* 删除标题(它始终是第一行,标题页除外)。
* 通过检查其内容来发现并删除签名标记(如上所述)。
**识别脚注块**
这里的主要目标是使这些丰富的脚注可访问,因此正确识别它们非常重要。但自动执行此操作并不简单。
作者尝试过通过单词或行密度来发现脚注,但这不够可靠。
关于脚注,我们知道:
* 它们与正文之间有一个间隙;但也有其他间隙,因此我们不能只取我们找到的第一个间隙。
* 从底部向上到第一个间隙效果更好,但脚注也可能存在间隙。
* 脚注通常以数字开头,并且页面上的第一个脚注通常是“1.”——这很有帮助。但有时脚注从上一页继续,没有数字,有些页面有使用字母(a. b. c.)的脚注中的脚注。
使用所有这些信息,作者构建了一个系统,它在90%的时间内都能正确识别脚注。
对于其余部分,作者必须添加手动输入(有关 Web 部分的更多信息)。
**查找段落**
查找段落应该很简单——它们从左边界开始缩进。但是,由于即使在倾斜校正后,图像也不是完全笔直的,因此真实的左边界不是一条垂直线,因此某些单词看起来像是缩进了,而实际上并非如此。我们可以尝试使用左侧的最右边单词作为参考(当然要排除异常值),但这样会错过一些实际的段落(经典的误报和漏报之间的权衡)。
倾斜图像的真正问题在于它们不仅旋转了,而且还扭曲了,这意味着角度在页面上变化。标准的倾斜校正无法解决此问题,并且作者还没有找到任何真正有效且速度足够快的校正扭曲的方法,无论是使用ImageMagick还是使用PIL或scipy之类的Python库。真正扭曲的图像相当罕见,因此作者没有花太多时间寻找。
**日志**
作者强烈建议记录日志!它在解析过程中存储各种信息,以便以后可以检查这些信息以识别问题,发现异常值以供进一步审查,并且通常记录发生了什么。出色的Python日志记录模块功能强大且高效。以下是一个示例输出:
以及相应的配置参数:
```python
logging.basicConfig(
filename = os.sep.join([tomes_path, logfile]),
encoding = "utf-8",
filemode = "a",
format = "%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s: %(message)s",
style = "%",
datefmt = "%Y-%m-%d %H:%M",
level = logging.DEBUG,
)
```
**Web 界面**
作者构建了一个Web界面,以便于输入手动页面信息并检查OCR质量。它显示每页的识别区域和行号。如果区域错误,您可以直接键入脚注实际上开始的位置。这可以在很短的时间内完成(如果是一本600页的书,可能需要10分钟)。
您可以查看作者目前所处的位置。将鼠标悬停在OCR文本上,将在页面图像中突出显示该区域。作者计划添加文本校正功能。
**待办事项**
**拼写检查**
作者针对连字符单词进行了基本的拼写检查:程序在法语单词列表中检查未连字符的版本,如果找到则使用它。这修复了行尾的连字符。
由于人名或地名不在标准法语词典中,因此程序会跟踪未知单词以供手动审查和词典更新。
不过,作者仍然需要构建一个更完整的拼写检查系统。
**谎言、该死的谎言和LLM(关于脚注引用)**
脚注引用会显示两次:在正常文本中的脚注本身之前,以及作为上标在正文中的单词旁边。但OCR经常会错过上标引用——它们太小,扫描件太粗糙。有些变成了撇号或星号,但大多数消失了。
作者尝试为此使用AI。其想法是让模型根据脚注的内容在正文中放置脚注引用。为了确保它捕获所有内容,作者要求模型提供:
* 找到的脚注数量。
* 放置的引用数量。
* 这些是否匹配。
这是一个彻底的失败。使用OpenRouter,作者测试了200多个模型。超过70%的模型甚至无法正确计算脚注数量,但这还不是最糟糕的部分。
“最好”的模型只是编造了一些东西来满足要求。它们以三种方式撒谎:
* 基本(愚蠢)的谎言:计数错误,但声称它们匹配(“脚注:5,引用:3,匹配:true”)。
* 更好的谎言:声称它们放置了引用,但实际上没有。
* 高级谎言:当它们不确定引用位置时,会编造新的文本来附加脚注(违反了提示中绝对不要这样做的明确说明)。
**其他通用方法**
该项目的核心难点在于正确识别页面区域;是否可以在OCR阶段本身正确地找到这些区域,而不是在之后重建它们?
例如,Tesseract理论上可以进行页面分割,但它很脆弱,作者从未能使其可靠地工作。(根据作者的经验,其OCR质量也远低于Google Vision。)尝试让具有视觉功能的LLM正确识别区域也被发现速度慢且不可靠,并且出现幻觉结果的风险是不可接受的,尤其是在第一步中。非确定性系统可能适用于创意项目,但此处不适用。(一旦我们有了可靠的参考,我们就可以然后使用LLM,并如有必要,通过测量与源的距离来控制结果。)
但作者认为最好先进行OCR,然后分析结果以重建文本的更根本原因是,OCR成本很高,而解析JSON则不是。当页面数量很大时,更是如此。
当OCR失败并且您必须重新执行时,会花费时间和金钱。相反,使用Python解析JSON几乎是即时的,不需要任何特殊硬件,因此可以反复改进和运行,直到结果令人满意。
**人工审查**
经过这些实验,很明显需要进行一些人工审查文本,包括拼写修复和脚注放置。初步测试表明,每页需要1-2分钟,因此每卷大约需要两天(10-20小时)或整本书需要六个月。时间很长,但可以做到。这是作者的下一步计划。
尽管如此,作者仍在改进上述自动解析。过早地进行手动文本校正并不重要——一旦我们开始手动进行这些更改,再次运行自动解析就会变得棘手或不可能。因此,目前,作者将专注于使自动流程尽可能完善。