Python 实现 PDF 转 TXT

  用手机或者 Kindle 看 PDF 文档字实太是太小了,总觉得 PDF 转 TXT 是个刚需,却一直没找到 PDF 转 TXT 的简单方法,最近有空,不妨自己用 Python 写一个。

  将 PDF 格式转换成纯文本的 TXT,虽然会损失掉一些排版和图片,却可以给文件瘦身,也可将其中的文字用于更多场合。

 PDF 里一般都包含文字和图片,有些文字以图片形式存储,比如大多数以扫描方式制作的 PDF 图书都使用这种方式,以此方式存储的 PDF 文件占空间也比较大,一般都有几十兆。另一种,以文本方式存储字符,同时存储字符的大小和位置,在显示对应的页面时绘制图片和文字,以此方式存储的 PDF 文件占空间较小,一般只有几兆大小。

  分辨文字的存储方式很简单,只需要用任意支持 PDF 格式的软件打开文件,放大几倍,如果文字依然清晰,则是以字符方式存储的,如果字的边缘变得模糊,则一般是以图片方式存储文字。

  以字符方式存储 PDF 的文本比较容易获取,使用 Linux 下的 pdftotxt 命令即可过滤出其中的文字。以图片方式存储的相对比较复杂,需要识别图片中的文字,会用到 OCR 技术,OCR 是光学字符识别的简称,目前的 OCR 一般利用深度学习技术实现,同样也是训练模型比较困难,但单纯地使用模型则非常简单。

  本文使用现成的 Python 三方库,实现对 PDF 中文本和图片两种文字的识别,程序运行环境仍然是 Linux(主要因为笔者不怎么用 Windows),Python 版本为 3.6(与 Python 2.7 的三方库略有差异)。

安装软件

  程序主要包括解析 PDF 格式和 OCR 识别两部分,首先安装三方库:

1
2
3
4
5
$ sudo pip install pdfminer3k # PDF格式解析
$ sudo apt-get install tesseract-ocr # 离线OCR工具tesseract
$ sudo apt-get install tesseract-ocr-chi-sim # OCR简体中文支持
$ sudo pip install pytesseract # OCR工具的Python支持包
$ sudo pip install baidu-aip # 在线OCR:百度提供的字符识别工具。

  本例中使用了在线和离线两种 OCR,离线版本识别率稍差,在线版本是百度提供的字符识别服务,对中文识别效果更好,它提供一定的免费额度,但是使用量大时需要付费。

离线 OCR

  使用 OCR 的目的是识别图片中的文字,Tesseract 是一款由 HP 实验室开发,由 Google 维护的开源 OCR,支持多种文字,下面看看它的用法。

1
2
3
4
5
6
7
from PIL import Image
import pytesseract

def img_to_str_tesseract(image_path, lang='eng'):
return pytesseract.image_to_string(Image.open(image_path), lang)

print(img_to_str_tesseract('image/test1.png', lang='chi_sim'))

在线 OCR

  百度、搜狗、有道等智能云平台都提供在线 OCR 服务,使用方法也大同小异,下面介绍百度 OCR 的使用方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from aip import AipOcr

config = {
'appId': '',
'apiKey': '',
'secretKey': ''
}
client = AipOcr(**config)

def img_to_str_baidu(image_path):
with open(image_path, 'rb') as fp:
image = fp.read()
result = client.basicGeneral(image)
if 'words_result' in result:
return '\n'.join([w['words'] for w in result['words_result']])
return ""

print(img_to_str_baidu('image/test1.png'))

  其中的 appId, apiKey, secretKey 需要在百度智能云中创建自己的“文字识别”项目后获取,请访问:https://console.bce.baidu.com/

  识别效果如下图所示,其中左侧是被识别的原始图像,右侧上方是 tesseract 识别的效果图,右侧下方是百度 OCR 识别的效果图。百度 OCR 比 tesseract 识别效果稍好,尤其是对中英文混排、标点符号和数字效果更好,不过 tesseract 也基本可用。

文档文字识别 OCR

PDF 格式解析

  本例中使用 pdfminer 库解析 PDF 文档,完整代码请从 github 下载:

https://github.com/xieyan0811/pdfconv.git

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
129
from pdfminer.pdftypes import LITERALS_DCT_DECODE, LITERALS_FLATE_DECODE
from pdfminer.pdfcolor import LITERAL_DEVICE_GRAY, LITERAL_DEVICE_RGB
from pdfminer.pdfparser import PDFParser,PDFDocument
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import PDFPageAggregator
from pdfminer.layout import LTTextBoxHorizontal, LAParams, LTFigure, LTImage, LTChar, LTTextLine
from pdfminer.pdfinterp import PDFTextExtractionNotAllowed
import os
import sys
import numpy as np
import importlib
importlib.reload(sys)

TMPDIR = 'tmp/'
PARSEIMG = True
OCR_ONLINE = False

# 保存图片
def write_image(image, outdir):
stream = image.stream
filters = stream.get_filters()
if len(filters) == 1 and filters[0] in LITERALS_DCT_DECODE:
ext = '.jpg'
data = stream.get_rawdata()
elif image.colorspace is LITERAL_DEVICE_RGB:
ext = '.bmp'
data = create_bmp(stream.get_data(), stream.bits*3, image.width, image.height)
elif image.colorspace is LITERAL_DEVICE_GRAY:
ext = '.bmp'
data = create_bmp(stream.get_data(), stream.bits, image.width, image.height)
else:
ext = '.img'
data = stream.get_data()
name = image.name+ext
path = os.path.join(outdir, name)
fp = open(path, 'wb')
fp.write(data)
fp.close()
return path, len(data)

# 写入文件
def write_file(path, text, ftype, debug=False):
with open(path, ftype) as f:
if debug:
print("write", len(text))
f.write(text)

# 去掉文中多余的回车
def adjust(inpath, outpath):
f = open(inpath)
lines = f.readlines()
arr = [len(line) for line in lines]
length = np.median(arr) # 行字符数中值

string = ""
for line in lines:
if len(line) >= length and line[-1]=='\n':
string += line[:-1] # 去掉句尾的回车
elif line == '-----------\n':
pass
else:
string += line
write_file(outpath, string, 'w')
return

# 解析每个数据块
def parse_section(layout, outpath, debug = False):
for x in layout:
if (isinstance(x, LTTextBoxHorizontal)): # 文本
write_file(outpath, x.get_text(), 'a')
elif (isinstance(x, LTFigure)):
parse_section(x, outpath)
elif (isinstance(x, LTImage)) and PARSEIMG: # 图片
path,length = write_image(x, TMPDIR)
if length > 0:
if OCR_ONLINE:
write_file(outpath, img_to_str_baidu(path), 'a')
else:
write_file(outpath, img_to_str_tesseract(path), 'a')
write_file(outpath, '\n' + '-----------' + '\n', 'a')

# 删除文件
def remove(path):
if not os.path.exists(path):
return
if os.path.isfile(path):
os.remove(path)
return
dirs = os.listdir(path)
for f in dirs:
file_name = os.path.join(path, f)
if os.path.isfile(file_name):
os.remove(file_name)
else:
remove(file_name)
os.rmdir(path)

# 解析PDF文件
def parse(inpath, outpath):
remove(TMPDIR) # 清除临时目录
os.mkdir(TMPDIR)
remove(outpath) # 清除输出文件
fp = open(inpath, 'rb')
praser = PDFParser(fp) # pdf文档分析器
doc = PDFDocument() # 创建一个PDF文档
praser.set_document(doc) # 连接分析器与文档对象
doc.set_parser(praser)
doc.initialize()

if not doc.is_extractable: # 是否提供txt转换
raise PDFTextExtractionNotAllowed
else:
rsrcmgr = PDFResourceManager() # 创建PDF资源管理器
laparams = LAParams()
device = PDFPageAggregator(rsrcmgr, laparams=laparams)
interpreter = PDFPageInterpreter(rsrcmgr, device) # 创建PDF解释器对象

for idx,page in enumerate(doc.get_pages()): # 获取page列表
interpreter.process_page(page)
layout = device.get_result()
print("parse", idx)
parse_section(layout, outpath)

if __name__ == '__main__':
pdffile = "xxxx.pdf"
tmpfile = pdffile.replace('pdf','tmp')
txtfile = pdffile.replace('pdf','txt')
parse(pdffile, tmpfile)
adjust(tmpfile, txtfile)

  其中 parse_section 用于解析数据块,PDF 的数据块有 LTTextBox,LTFigure,LTImage,LTRect,LTCurve 和 LTLine 等子对象。LTTextBox 表示一组文本块可能包含在一个矩形区域;LTTextLine 表示单个文本行 LTChar 对象的列表;LTImage 表示一个图像对象;LTLine 表示一条直线;LTRect: 表示矩形;LTCurve 表示曲线。有些对象之间包括嵌套关系。

一些问题

  程序通过百余行代码实现转换功能,解析普通的 PDF 文件问题不大,但仍存在一些问题:

  1. 本文中使用的 pdfminer 库中对 pdf 文件中数据块的解析不够完美,只支持主流的 jpg、bmp 格式文件,有一些 pdf 中的图片无法被识别。

  2. 竖版文字也被识别成横版。

  3. 解析字符型文本时,比较简单粗暴,对于特殊的版式不一定按照从上到下,从左到右的顺序解析,有待更进。

  4. 程序目前以支持中文 PDF 文件为主,支持其它语言需要在代码中稍做调整。

参考

  1. 百度接口用法

https://cloud.baidu.com/doc/OCR/OCR-Python-SDK.html#.E9.80.9A.E7.94.A8.E6.96.87.E5.AD.97.E8.AF.86.E5.88.AB