Python实现PDF转TXT

时间:2024-10-25 08:37:26

 用手机或者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识别两部分,首先安装三方库:

$ 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,支持多种文字,下面看看它的用法。

from PIL import Image
import pytesseract

def img_to_str_tesseract(image_path, lang='eng'):
    return pytesseract.image_to_string((image_path), lang)
  
print(img_to_str_tesseract('image/', lang='chi_sim'))

在线OCR

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

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 = ()
        result = (image)
        if 'words_result' in result:
            return '\n'.join([w['words'] for w in result['words_result']])
    return ""

print(img_to_str_baidu('image/'))

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

 

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

PDF格式解析

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

/xieyan0811/

from  import LITERALS_DCT_DECODE, LITERALS_FLATE_DECODE
from  import LITERAL_DEVICE_GRAY, LITERAL_DEVICE_RGB
from  import PDFParser,PDFDocument
from  import PDFResourceManager, PDFPageInterpreter
from  import PDFPageAggregator
from  import LTTextBoxHorizontal, LAParams, LTFigure, LTImage, LTChar, LTTextLine
from  import PDFTextExtractionNotAllowed
import os
import sys
import numpy as np
import importlib
(sys)

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

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

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

# 去掉文中多余的回车
def adjust(inpath, outpath):
    f = open(inpath)
    lines = ()
    arr = [len(line) for line in lines]
    length = (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 (path):
        return
    if (path):
        (path)
        return
    dirs = (path)
    for f in dirs:
        file_name = (path, f)
        if (file_name):
            (file_name)
        else:
            remove(file_name)
    (path)

# 解析PDF文件
def parse(inpath, outpath):
    remove(TMPDIR) # 清除临时目录 
    (TMPDIR)
    remove(outpath) # 清除输出文件
    fp = open(inpath, 'rb')
    praser = PDFParser(fp) # pdf文档分析器
    doc = PDFDocument() # 创建一个PDF文档
    praser.set_document(doc) # 连接分析器与文档对象
    doc.set_parser(praser)
    ()
    
    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 = ""
    tmpfile = ('pdf','tmp')
    txtfile = ('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) 百度接口用法
/doc/OCR/#.E9.80.9A.E7.94.A8.E6.96.87..97..86.E5.