如何用Python轻松识别扫描版PDF

本文将会介绍如何使用Python中的fitz模块和OCR技术来区分文字版和扫描版PDF。

众所周知,PDF文档主要分为文字版和扫描版两种类型。一般来说,文字版PDF较为清晰,可以在文件页面内进行复制和文本搜索,而扫描版PDF较为模糊,不能在文件页面内进行复制文字和搜索。

但是,两者类型的PDF文档不能明确区分,有些页面清晰的PDF文档也可能是扫描版,而页面模糊的PDF文档也可能是文字版,因此,两者之间并没有严格的判断标准。我们尝试着询问ChatGPT(模型为GPT-4o),得到的答案如下:

scanned_pdf_1.png

这些都是判断方法,但有没有用Python代码来可以自动化判断的方法呢?答案是肯定的,本文将会来介绍如何使用Python中的fitz模块和OCR技术来区分文字版和扫描版PDF。本文的判别思路主要要两条:

  • 借助PDF解析工具fitz模块,统计PDF文档中文本区域占总的PDF页面区域的比例,如果比例小于某个阈值,则判别为扫描版PDF;
  • 借助OCR技术,统计PDF文档中前N页中OCR识别前后的字符数比例,如果比例不在某个范围内,则判别为扫描版PDF。

上述的第一中思路比较容易理解,着重于文本区域的大小,因为一般扫描版PDF都是图片,文字很少,但这也可能误判,因为文字版PDF也可能文字很少。而第二种思路更好一些,借助高精度识别的OCR技术,统计OCR识别前后的字符数量差异来区分,因为扫描版PDF虽然为页面,但一般借助OCR技术可识别图片中的文字,因此扫描版PDF在OCR识别前后字符数量往往相差较大。

以上两种方法均为笔者的思路,并不能保证百分百的识别率,因为比例的阈值不好控制。

接下来,笔者将尝试着介绍这两种判别方法。

测试文档

首先,我们来准备5个测试PDF,名称分别如下:

  • demo1.pdf(文字版)
  • BERT.pdf(文字版)
  • LLaMA.pdf(文字版)
  • book1.pdf(扫描版)
  • 外国电影史(扫描版)

这5个文件的首页截图如下:

demo1.pdf
BERT.pdf
LLaMA.pdf
book1.pdf
外国电影史.pdf

判别方法1

判别方法1为使用PDF解析工具fitz模块,统计PDF文档中文本区域占总的PDF页面区域的比例,如果比例小于某个阈值,则判别为扫描版PDF。这里我们设置文本区域面积占PDF页面区域的占比阈值为0.05,如果小于这个值,则判别该PDF文档为扫描版PDF,否则为文本版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
# -*- coding: utf-8 -*-
# 判断是否为扫描版pdf, 考虑文本区域占整个页面的比例, 如果小于某个阈值(比如0.05), 则认为是扫描版pdf
import time
import fitz

s_time = time.time()
doc = fitz.open('../data/demo1.pdf')

total_area = 0
text_area = 0
for i in range(doc.page_count):
page = doc[i]
# 获取page的宽和高
total_area += page.rect[2] * page.rect[3]
# 获取文本区域的面积
page_content = page.get_text("blocks")
for record in page_content:
if not record[-1]:
text_area += (record[3] - record[1]) * (record[2] - record[0])

doc.close()

print(text_area / total_area)
print(f"cost time: {time.time() - s_time}")

运行上述代码,对上述5个PDF文档进行分析,得到的结果如下表:

PDF名称 占比 是否为扫描版
demo1.pdf 0.022
BERT.pdf 0.496
LLaMA.pdf 0.419
book1.pdf 0.018
外国电影史 0.001

在该方法中,只有demo1.pdf判别错误,因为该PDF文档只有一页,且文字很少,因此造成了误判。

判别方法2

判别方法2借助OCR技术,通过OCR技术识别前后的字符数量差异来判别。一般OCR识别较为耗时,因此可以取PDF文档的前N(我们这里取N=10)页进行操作。令占比ration=OCR识别前字符数/OCR识别后字符数,如果占比ratio不在一定的范围内(我们这里取[0.5, 2]),则判别该PDF为扫描版PDF,否则为文字版PDF。

本文采用的OCR技术为TextIn产品中的通用文字识别。

实现的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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# -*- coding: utf-8 -*-
# 借助OCR技术,对PDF文件在OCR前后的字符串进行统计,如果 OCR前字符数/OCR后字符数 不在一定范围内(比如[0.5, 2]),则可判断为扫描版PDF。
import os
import time
import traceback

import fitz
import requests
from PIL import Image


# 使用fitz模块提取文本, 未使用OCR
def get_pdf_file_text(
pdf_file_path: str,
pdf_page_count: int
) -> str:
doc = fitz.open(pdf_file_path)
whole_text_list = []
for i in range(pdf_page_count):
if i < doc.page_count:
page = doc[i]
page_content = page.get_text("blocks")
for record in page_content:
if not record[-1]:
whole_text_list.append(record[4])
doc.close()
return ''.join(whole_text_list)


# 将PDF文件转换为图片
def convert_pdf_2_img(
pdf_file: str,
pages: int
) -> 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(pages):
if page_number < 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]
if not os.path.exists(f"../output/{pdf_file_name}"):
os.makedirs(f"../output/{pdf_file_name}")
save_image_path = f"../output/{pdf_file_name}/{page_number + 1}.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


# 使用OCR技术从图片中提取文本
def get_file_content(file_path: str):
with open(file_path, 'rb') as fp:
return fp.read()


class CommonOcr(object):
_app_id = 'xxx'
_secret_code = 'xxx'

@classmethod
def recognize(cls, img_path: str):
try:
# 通用文字识别
url = 'https://api.textin.com/ai/service/v2/recognize/multipage'
head = {'x-ti-app-id': cls._app_id, 'x-ti-secret-code': cls._secret_code}
image = get_file_content(img_path)
result = requests.post(url, data=image, headers=head)
text_list = []
if result.status_code == 200:
for record in result.json()['result']['pages'][0]['lines']:
text_list.append(record['text'])
return '\n'.join(text_list)
except Exception as e:
print(f"error in recognize: {traceback.format_exc()}")
return ''


def get_pdf_images_text(
image_path_list: list[str]
) -> str:
image_text_list = []
for i, image_path in enumerate(image_path_list):
print("processing image: ", i + 1, " / ", len(image_path_list), " ...")
image_text_list.append(CommonOcr.recognize(image_path))
return '\n'.join(image_text_list)


if __name__ == '__main__':
s_time = time.time()

PDF_FILE_PATH = "../data/外国电影史.pdf"
PDF_PAGE_COUNT = 10

# 未使用OCR
original_text = get_pdf_file_text(PDF_FILE_PATH, PDF_PAGE_COUNT)
print(f"未使用OCR,{PDF_FILE_PATH}的前{PDF_PAGE_COUNT}页的字符: {original_text}")
# 使用OCR
output_image_path_list = convert_pdf_2_img(PDF_FILE_PATH, PDF_PAGE_COUNT)
ocr_image_text = get_pdf_images_text(output_image_path_list)
print(f"使用OCR,{PDF_FILE_PATH}的前{PDF_PAGE_COUNT}页的字符: {ocr_image_text}")

# 判断是否为扫描版PDF
print(f"未使用OCR前,{PDF_FILE_PATH}的前{PDF_PAGE_COUNT}页的字符总数: {len(original_text)}")
print(f"使用OCR后,{PDF_FILE_PATH}的前{PDF_PAGE_COUNT}页的字符总数: {len(ocr_image_text)}")
print(f"OCR前字符数/OCR后字符数: {len(original_text) / len(ocr_image_text)}")

ratio = len(original_text) / len(ocr_image_text) if len(ocr_image_text) else 0
if 0.5 < ratio < 2:
print(f"{PDF_FILE_PATH}不是扫描版PDF")
else:
print(f"{PDF_FILE_PATH}是扫描版PDF")

print(f"cost time: {time.time() - s_time}")

PDF名称 占比 是否为扫描版
demo1.pdf 1.103
BERT.pdf 1.026
LLaMA.pdf 1.019
book1.pdf 0.130
外国电影史 0.010

使用该方法,可以全部判别正确。

总结

这篇文章介绍了如何使用Python中的fitz模块和OCR技术来区分文字版和扫描版PDF。文章提出两种方法:一种是通过文本区域占比来判断,另一种是利用OCR识别前后字符数量的差异进行判别。相比之下,OCR方法更准确,适合处理扫描版PDF的识别需求。

两种方法各有优缺点,且不是十分完美的。

如果读者有什么更好的区分文字版和扫描版PDF的思路,欢迎在文章下面进行交流哦~


如何用Python轻松识别扫描版PDF
https://percent4.github.io/如何用Python轻松识别扫描版PDF/
作者
Jclian91
发布于
2024年11月13日
许可协议