NLP(一百零五)文本纠错语料的自动化生成

本文将会介绍如何借助OCR和文本挖掘技术,从中文PDF文档中自动化生成文本纠错的高质量语料。

引言

我们知道,高质量语料对于文本任务是非常重要的,而对于文本纠错任务,目前的中文语料较为缺乏,因此,如何构建中文文本纠错的高质量语料值得探索。

本文将会介绍如何从中文文字版PDF文档中,借助OCR和文本挖掘技术,自动化大规模地生成文本纠错的高质量语料。

从文章如何用Python轻松识别扫描版PDF,我们知道,对于文字版PDF的文本提取,有两者实现方式:fitz模块提取(规则提取)和转化为图片后使用OCR提取。

  • fitz模块提取,提取的文本为正确文本
  • 转化为图片后使用OCR技术提取文本,往往会带有一定的识别错误,通常为字形上的识别错误

借助这两种提取文本路径,我们可以尝试着构建文本纠错语料!

对于文字版PDF文档,fitz模块提取的正确文本和OCR识别文本,两者之间的差异不会很大,因此,可以使用文本挖掘技术,找到那些字符数量一样,且文本高度相似但又不完全相同的字符串,这样我们就找到了文本纠错的语料。整个过程使用Python程序实现,不需要人工干预,高效便捷且可靠,最终生成的文本纠错语料也有一定的价值。

从中文文字版PDF自动化生成文本纠错语料的整体流程如下图:

整体流程

本文将从以下三个模块进行拆解讲和讲述:

  1. 预处理
  2. OCR识别
  3. 文本纠错语料构建
  4. 相近字形语料

预处理

在这个阶段,我们对输入的中文文字版PDF文档使用fitz模块进行文本提取,并将PDF文档中的每一页PDF页面转化为png格式的图片。

实现的Python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# -*- coding: utf-8 -*-
# @file: preprocess.py
# PDF文档预处理
import os
import json
import time

import fitz
from PIL import Image

from src.config.config import PROJECT_DIR


# 使用fitz模块提取文本, 未使用OCR
def get_pdf_file_text(
pdf_file_path: str
) -> dict[int, str]:
doc = fitz.open(pdf_file_path)
page_result = {}
for i in range(doc.page_count):
page = doc[i]
text = ""
page_content = page.get_text("blocks")
for record in page_content:
if not record[-1]:
text += record[4]
page_result[i] = text
doc.close()
# 将识别结果保存到json文件中
pdf_file_name = pdf_file_path.split('/')[-1].split(".")[0]
json_output_path = os.path.join(PROJECT_DIR, f"output/{pdf_file_name}/original_text.json")
with open(json_output_path, "w", encoding="utf-8") as f:
f.write(json.dumps(page_result, ensure_ascii=False, indent=4))
return page_result


# 将PDF文件转换为图片
def convert_pdf_2_img(
pdf_file: str
) -> list[str]:
"""
convert pdf to image
:param pdf_file: pdf file path
:param pages: convert pages number(at most)
:return: output of image file path list
"""
pdf_document = fitz.open(pdf_file)
output_image_file_path_list = []
# Iterate through each page and convert to an image
for page_number in range(pdf_document.page_count):
# Get the page
page = pdf_document[page_number]
# Convert the page to an image
pix = page.get_pixmap()
# Create a Pillow Image object from the pixmap
image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
# Save the image
pdf_file_name = pdf_file.split('/')[-1].split(".")[0]
output_dir = os.path.join(PROJECT_DIR, f"output/{pdf_file_name}")
if not os.path.exists(output_dir):
os.makedirs(output_dir)
save_image_path = os.path.join(output_dir, f"{page_number}.png")
image.save(save_image_path)
output_image_file_path_list.append(save_image_path)
# Close the PDF file
pdf_document.close()
return output_image_file_path_list


if __name__ == '__main__':
s_time = time.time()
file_name = "weite.pdf"
file_path = os.path.join(PROJECT_DIR, f"docs/{file_name}")
output_image_path_list = convert_pdf_2_img(file_path)
original_text = get_pdf_file_text(file_path)

我们使用weite.pdf,其前两页如下:

示例PDF文档第1页
示例PDF文档第2页

使用fitz模块提取这两页的文字如下:

1
2
3
4
{
"0": "更多电子书资料请搜索「书行万里」:ht t p: //w\nw\nw\n. gpdf . net\nhttps://homeofpdf.com https://homeofpdf.com https://homeofpdf.com \nhttps://homeofpdf.com https://homeofpdf.com https://homeofpdf.com \nhttps://homeofpdf.com https://homeofpdf.com https://homeofpdf.com \n",
"1": "图书在版编目(CIP)数据\n少年维特的烦恼/(德)歌德(Goethe,J.W.Von)著;韩耀成译.—南京:\n译林出版社,2010.6 (2013.9重印)\n(经典译林)\nISBN 978-7-5447-1079-4\nⅠ.①少… Ⅱ.①歌… ②韩… Ⅲ.①书信体小说—德国—近代\nⅣ.①I516.44\n中国版本图书馆CIP数据核字(2009)第209690号\n书 名 少年维特的烦恼\n作 者 [德国]歌德\n译 者 韩耀成\n责任编辑 夏秀玫 孙茜\n原文出版 dtv weltliteratur,1978\n出版发行 凤凰出版传媒集团 凤凰出版传媒股份有限公司 译林出\n版社\n集团地址 南京市湖南路1号A楼,邮编:210009\n集团网址 http://www.ppm.cn\n出版社地址 南京市湖南路1号A楼,邮编:210009\n电子信箱 yilin@yilin.com\n出版社网址 http://www.yilin.com\n更多电子书资料请搜索「书行万里」:ht t p: //w\nw\nw\n. gpdf . net\nhttps://homeofpdf.com https://homeofpdf.com https://homeofpdf.com \nhttps://homeofpdf.com https://homeofpdf.com https://homeofpdf.com \nhttps://homeofpdf.com https://homeofpdf.com https://homeofpdf.com \n"
}

OCR识别

接下来,我们使用PaddleOCR这个OCR工具,对每张图片(即一页PDF)进行OCR识别,提取PDF页面中的文字。

实现的Python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -*- coding: utf-8 -*-
# @file: image_ocr.py
# 使用PaddleOCR提取图片中的文字
import json
import os
from paddleocr import PaddleOCR

from src.config.config import PROJECT_DIR


def get_pdf_file_ocr_result(pdf_file_dir_path: str) -> dict[int, str]:
# Paddleocr目前支持的多语言语种可以通过修改lang参数进行切换
# 例如`ch`, `en`, `fr`, `german`, `korean`, `japan`
ocr = PaddleOCR(use_angle_cls=False, lang="ch")
page_ocr_result = {}
files = [file for file in os.listdir(pdf_file_dir_path) if file.endswith(".png")]
# 按数字大小排序
files.sort(key=lambda x: int(x.split(".")[0]))
for file in files:
text = ""
page_no = int(file.split(".")[0])
img_path = os.path.join(pdf_file_dir_path, file)
result = ocr.ocr(img_path, cls=False)
for idx in range(len(result)):
res = result[idx]
if res:
for line in res:
text += line[1][0]
print(f"page: {page_no}, text: {line[1][0]}")
page_ocr_result[page_no] = text

# 将识别结果保存到json文件中
json_output_path = os.path.join(pdf_file_dir_path, "ocr_result.json")
with open(json_output_path, "w", encoding="utf-8") as f:
f.write(json.dumps(page_ocr_result, ensure_ascii=False, indent=4))
return page_ocr_result


if __name__ == '__main__':
pdf_file_name = "wushihui"
test_dir_path = os.path.join(PROJECT_DIR, f"output/{pdf_file_name}")
get_pdf_file_ocr_result(test_dir_path)

对于前两页PDF的OCR识别结果,如下:

1
2
3
4
{
"0": "Yilin ClassicsJ.W.GOETHE经/典/译/林Die Leidendes jungen WertherC少年维特的烦恼[德国]歌德著韩耀成译",
"1": "图书在版编目(CIP)数据少年维特的烦恼/(德)歌德(Goethe,J.W.Von)著:韩耀成译.一南京:译林出版社,2010.6(2013.9重印)(经典译林)ISBN 978-7-5447-1079-41.①少...II.①歌...②韩...IⅢI.①书信体小说一德国一近代IV.①)I516.44中国版本图书馆CIP数据核字(2009)第209690号书名少年维特的烦恼作者[德国]歌德译者韩耀成责任编辑夏秀玫孙茜原文出版dtvweltliteratur,1978出版发行凤凰出版传媒集团凤凰出版传媒股份有限公司译林出版社集团地址南京市湖南路1号A楼,邮编:210009集团网址http://www.ppm.cn出版社地址南京市湖南路1号A楼,邮编:210009电子信箱yilin@yilin.com出版社网址http://www.yilin.com"
}

文本纠错语料构建

接下来是最为关键的文本语料构建阶段。

一个基本的想法是,提取的正确文本和OCR识别文本,两者之间的差异不会很大。

我们将正确文本和识别文本进行分句,作为基本单元,中文文本分句工具采用sententx模块。在每一页PDF中,找到与OCR识别句子语义最接近的正确句子,这里我们使用Jaccard相似度,阈值取0.8。对于OCR识别句子和相似的正确句子,如果两者长度一样,且整个句子中不同的字符数量大于0,且小于一定数量,我们可认为两者形成了文本纠错的一条语料。

实现Python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# -*- coding: utf-8 -*-
# @file: corpus_generator.py
# 文本纠错语料构建
import os
import json

import difflib
from sentencex import segment

from src.config.config import PROJECT_DIR


def text_preprocess(text: str) -> str:
"""
text preprocess
:param text: original text
:return: preprocessed text
"""
text = text.replace("\n", "")
return text


def get_sentences(text: str) -> list[str]:
"""
get sentences from text
:param text: original text
:return: sentences list
"""
sentences = segment("zh", text)
return sentences


def find_similar_sentence(sent: str, candidate_sentences: list[str]) -> str:
"""
find similar sentence
:param sent: sentence
:param candidate_sentences: candidate sentences
:return: similar sentence
"""
for candidate_sent in candidate_sentences:
if sent == candidate_sent:
return candidate_sent
elif len(sent) == len(candidate_sent):
# 计算两个句子的jaccard相似度
set_sent = set(sent)
set_candidate_sent = set(candidate_sent)
jaccard_sim = len(set_sent & set_candidate_sent) / len(set_sent | set_candidate_sent)
if jaccard_sim > 0.8:
return candidate_sent
return ""


# 使用difflib, 查找s1 与 s2 不同的文字
def find_differences(s1: str, s2: str):
# 使用 difflib.SequenceMatcher 来比较两个字符串
matcher = difflib.SequenceMatcher(None, s1, s2)

# 存储不同的字符及其在 s1 中的下标
differences = []

# 遍历匹配块,获取不匹配的部分
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag != 'equal':
# 不匹配的部分
for i in range(i1, i2):
differences.append((i, s1[i]))

return differences


def get_corpus(original_text: str, ocr_text: str) -> list[dict]:
corpus_list = []
original_sents = get_sentences(text_preprocess(original_text))
ocr_sents = get_sentences(text_preprocess(ocr_text))
for ocr_sent in ocr_sents:
if len(ocr_sent) > 4: # 过滤掉长度小于4的ocr句子
similar_sent = find_similar_sentence(ocr_sent, original_sents)
if similar_sent:
# 如果两个句子中的不同文字数量大于0且小于6, 则添加至corpus_list,认为是文本纠错语料
diffs = find_differences(similar_sent, ocr_sent)
if 0 < len(diffs) < 6:
corpus_list.append({
"ori_sent": similar_sent,
"ocr_sent": ocr_sent,
"diffs": diffs
})

return corpus_list


if __name__ == '__main__':
pdf_file_name = "wushihui"
pdf_dir_path = os.path.join(PROJECT_DIR, f"output/{pdf_file_name}")
ocr_result_file_path = os.path.join(pdf_dir_path, "ocr_result.json")
original_text_file_path = os.path.join(pdf_dir_path, "original_text.json")
with open(ocr_result_file_path, "r", encoding="utf-8") as f:
ocr_result_dict = json.load(f)
with open(original_text_file_path, "r", encoding="utf-8") as f:
original_text_dict = json.load(f)

# 生成文本纠错语料
final_corpus_list = []
for key, value in ocr_result_dict.items():
my_ocr_text = value
if key in original_text_dict:
my_original_text = original_text_dict[key]
my_corpus_list = get_corpus(my_original_text, my_ocr_text)
final_corpus_list.extend(my_corpus_list)

# 将文本纠错语料保存到json文件中
json_output_path = os.path.join(PROJECT_DIR, f"data/{pdf_dir_path.split('/')[-1]}_corpus.json")
with open(json_output_path, "w", encoding="utf-8") as f:
f.write(json.dumps(final_corpus_list, ensure_ascii=False, indent=4))
print(f"已找到{len(final_corpus_list)}条文本纠错语料,保存至{json_output_path}")

为了验证上述算法的有效性,笔者从网络上下载了10个中文文字版PDF,对于每个PDF文档进行逐一验证,获取的文本纠错语料展示如下(每个文档仅展示一条语料):

  • deguo_tongshi.pdf
1
2
3
4
5
6
7
8
9
10
{
"ori_sent": "德意志城市大多兴起于修道院和城堡附近、帝王驻跸地以及逃亡农奴聚居地,特别是交通和商业中心。",
"ocr_sent": "德意志城市大多兴起于修道院和城堡附近、帝王驻蹭地以及逃亡农奴聚居地,特别是交通和商业中心。",
"diffs": [
[
22,
"跸"
]
]
}
  • digital_china.pdf
1
2
3
4
5
6
7
8
9
10
{
"ori_sent": "三、“双碳”目标与数字化技术1.",
"ocr_sent": "三、“双碳”自标与数字化技术1.",
"diffs": [
[
6,
"目"
]
]
}
  • guoyun1909.pdf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"ori_sent": "此时,立宪万能论已成为大清国的主旋律,人们或过于天真地相信,或过于世故地假装相信,只要一立宪,大清国的任何问题都能迎刃而解。",
"ocr_sent": "此时,立宪方能论已成为大清国的主旋律,人们或过于天真地相信,或过于世敌地假装相信,只要一立宪,大清国的任何问题都能迎刃而解。",
"diffs": [
[
5,
"万"
],
[
34,
"故"
]
]
}
  • lishidewendu.pdf
1
2
3
4
5
6
7
8
9
10
{
"ori_sent": "在接下来的岁月,拉玛出演了一系列电影,那段历史,图片比文字更有说服力。",
"ocr_sent": "在接下来的罗月,拉玛出演了一系列电影,那段历史,图片比文字更有说服力。",
"diffs": [
[
5,
"岁"
]
]
}
  • shiyi.pdf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"ori_sent": "那一年的5月29日上午,当南美洲上空的星星冉冉升起时,它们都发生了些许位移,而且距离太阳越近的星星,它们位置的改变就越明显。",
"ocr_sent": "那一年的5月29日上午,当南美洲上空的星星再再升起时,它们都发生了些许位移,而且距离太阳越近的星星,它们位置的改变就越明显。",
"diffs": [
[
21,
"冉"
],
[
22,
"冉"
]
]
}
  • weite.pdf
1
2
3
4
5
6
7
8
9
10
{
"ori_sent": "她提高嗓音,好让他半聋的耳朵听得见。",
"ocr_sent": "她提高噪音,好让他半聋的耳朵听得见。",
"diffs": [
[
3,
"嗓"
]
]
}
  • wushihui.pdf
1
2
3
4
5
6
7
8
9
10
{
"ori_sent": "李尊吾带沈方壶冒雪入京,见到踢毽子的程华安,便打消了比武之念。",
"ocr_sent": "李尊吾带沈方壶冒雪入京,见到踢键子的程华安,便打消了比武之念。",
"diffs": [
[
15,
"毽"
]
]
}
  • yingren.pdf
1
2
3
4
5
6
7
8
9
10
{
"ori_sent": "虽然如此,那般活跃的妙椿仍没有上京的余力。",
"ocr_sent": "虽然如此,那般活跌的妙椿仍没有上京的余力。",
"diffs": [
[
8,
"跃"
]
]
}
  • zengguofan.pdf
1
2
3
4
5
6
7
8
9
10
{
"ori_sent": "曾国潢的曾孙曾昭抡是著名化学家,曾任高教部副部长。",
"ocr_sent": "曾国潢的曾孙曾昭抢是著名化学家,曾任高教部副部长。",
"diffs": [
[
8,
"抡"
]
]
}
  • zhangshidong.pdf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"ori_sent": "慈禧还政住颐和园后,连皇上每次觐见也要递红包。",
"ocr_sent": "慈禧还政住顾和园后,连皇上每次豌见也要递红包。",
"diffs": [
[
5,
"颐"
],
[
15,
"觐"
]
]
}

相近字形语料

作为上述研究的一个副产品是,我们可以得到相近字形语料库,因为OCR技术识别错误的文字往往是字形上比较接近的文字。

使用上述自动化构建流程,我们可以从中文文字数量多的PDF文档中,能提取出数量可观的文本纠错语料,因此从大量的文本纠错语料中,我们还可以构建出相近字形语料库。

实现的Python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# -*- coding: utf-8 -*-
# @file: visually_similar_characters.py
import os
import json
from pprint import pprint
from collections import defaultdict


from src.config.config import PROJECT_DIR

visually_similar_characters = defaultdict(set)

for file in os.listdir(os.path.join(PROJECT_DIR, 'data')):
file_path = os.path.join(PROJECT_DIR, 'data', file)
# read json file
with open(file_path, 'r', encoding='utf-8') as f:
data = json.loads(f.read())
for sample in data:
for record_item in sample["diffs"]:
char = record_item[1]
# 判断是否为中文字符
if '\u4e00' <= char <= '\u9fff':
visually_similar_characters[char].add(sample['ocr_sent'][record_item[0]])

pprint(visually_similar_characters)

字典中前10个相近字形的结果展示如下:

1
2
3
4
5
6
7
8
9
10
'一': {'二'},
'万': {'方'},
'丈': {'文'},
'丐': {'与', '弓', '写'},
'丕': {'不'},
'丢': {'去'},
'串': {'事'},
'丽': {'前'},
'乎': {'平'},
'乏': {'之'}

总结

本文想法新颖,自有其创新之处,有一定的实用价值。

本文主要介绍了如何从中文文字版PDF文档中,自动化构建出高质量的中文文本纠错语料。

本文中给出的Python代码均已开源,项目为text_corrector_corpus_auto_generation,网址为https://github.com/percent4/text_corrector_corpus_auto_generation,欢迎大家参考~


NLP(一百零五)文本纠错语料的自动化生成
https://percent4.github.io/NLP(一百零五)文本纠错语料的自动化生成/
作者
Jclian91
发布于
2024年11月13日
许可协议