引言:现有的工具实现pdf转word一般都是收费的,我之前充了wps一年的会员,最后真正用到这个功能的也没有几次,后来临时再用到又不想为了那一两份文件充会员,对我来说确实太浪费钱了。于是我就想着能不能用Python写个程序,自己想什么时候用就什么时候用,想转多少就转多少。在网上查了一圈,研究了几天,终于算是弄出来了,在此做个归纳总结。此外,本篇不限于讲解pdf转word,还有关于图形化界面设计的知识。
1 准备工作
本文是针对已经懂一点Python的读者写的(当然我自己也是初学者,欢迎大神不吝指正),所以很多基本的代码就不做解释了。
1.1 工具与库
(1)编译器:我用的是Anoconda提供的Spyder编译器
(2)环境:Python 3.8
(3)库:pdfminer3k、pdfminer.six、pdf2docx、fitz、tkinter、re、os、threading、pyinstaller
1.2 库的解释
(1)pdfminer3k与pdfminer.six:我一开始全部用的是pdfminer3k,网上多数教学也是用的这个库,但是直到我尝试解析一份报名表时,发现原来的代码什么也读不出来(这个是什么问题后面会说)。于是我搜索该问题的解决方案,看到一个外国网友的解答,说是得卸载pdfminer3k改用pdfminer.six。我尝试安装了一下pdfminer.six(我没有先卸载3k,而是直接安装six),问题果然解决了。下面是对于两个库的不同的解释[1]:
(2)pdf2docx:不得不说这个库是真的牛*,一键实现pdf转换docx。只想知道怎么快速转换pdf的小伙伴剩下的都可以不看了,下载这个库直接用就好了。具体参考该篇文章:pdf2docx实现方法
(3)fitz:该库有两个功能,一个是提取pdf中的所有图片,一个是直接把pdf转成图片。
(4)tkinter:这个库不知道大家用不用,我知道的现在教python图形化界面设计的多数用像wxpython这类的。因为我自学Python的主要参考书籍是O’REILLY出版的《Python编程》(第4版),这本大部头的书上册主要就是围绕tkinter库展开的,所以我一直都用它来写小程序。
(5)re、os、threading、pyinstaller:这些库大家应该都知道的,我就不作多余解释了。
2 功能实现
2.1 读取所有文字内容
(1)实现代码1
from docx import Document
from pdfminer.layout import LAParams
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.converter import PDFPageAggregator
from pdfminer.pdfpage import PDFTextExtractionNotAllowed, PDFPage
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
# 打开原文件
fp = open(r'文件1.pdf', 'rb')
# 文档分析器
parser = PDFParser(fp)
# 创建pdf文件连接分析器
doc = PDFDocument(parser)
# 检查原文件可否转换
if not doc.is_extractable:
raise PDFTextExtractionNotAllowed
else:
# 待存储文档
out = Document()
# 资源管理器
rsrcmagr = PDFResourceManager()
# 参数对象
laparams = LAParams()
# 聚合器
device = PDFPageAggregator(rsrcmagr, laparams=laparams)
# 解释器
interpreter = PDFPageInterpreter(rsrcmagr, device)
# 逐页分析原文件
for page in PDFPage.create_pages(doc):
interpreter.process_page(page)
# 取得每一页的结果
layout = device.get_result()
# 分析每一页的每个对象
for x in layout:
try:
# 得到文本
result = x.get_text()
# 将文本写入存储文档
out.add_paragraph(result)
except:
pass
# 保存
out.save(r'.\test\1.docx')
(2)实现代码2
上面的代码是我总结网上已有的一般方法得出的结果,这里就可能遇到1.2中我说的问题,有时候会什么结果都读不出来。我在解决过程中无意发现pdf2txt这个神奇的工具。它本身是安装pdfminer后自带的一个脚本,使用方法非常简单,就是运行cmd,输入以下指令:
pdf2txt.py -o output.txt original_file.pdf
pdf2txt不仅可以转换成txt,还可以转换成html、tag和xml,不可谓不强大,并且html的保留了原文件的格式,非常好用。有兴趣的小伙伴可以阅读一下源代码。
因为pdf2txt毕竟是脚本工具,如果想用在自己的代码中转换起来还是比较麻烦的,所以我就参考它的源码,借鉴了它把pdf转换成txt的方法,发现比代码1简单了很多。
from pdfminer.pdfpage import PDFPage
from pdfminer.layout import LAParams
from pdfminer.converter import TextConverter
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
# 待存储文档
output = open(r'output.txt', 'w', encoding='utf-8')
# 资源管理器
rsrcmgr = PDFResourceManager()
# 参数对象
laparams = LAParams()
# 聚合器
device = TextConverter(rsrcmgr, output, laparams=laparams)
# 打开原文件
with open(r'original_file.pdf', 'rb') as fp:
# 解释器
interpreter = PDFPageInterpreter(rsrcmgr, device)
# 逐页分析
for page in PDFPage.get_pages(fp, check_extractable=True):
# 直接把读取的内容写进文档,省去了代码1的读写步骤
interpreter.process_page(page)
device.close()
output.close()
(3)实现代码3
下面就是一键秒天秒地的pdf2docx了,代码十分变态,直接贴出
from pdf2docx import Converter
biantai = Converter(r'original_file.pdf')
biantai.convert(r'output.docx')
biantai.close()
说实话,有点过于简单,就好像玩一个升级类的游戏,一个没忍住输了秘籍,结果游戏都不想玩了……有空得好好读读源代码……
2.2 读取所有图片
有时候我们可能只想要原文件里的图片,这时候就要用到fitz库了。这是我在b站无意搜到的一个方法,参考了一下,有兴趣的小伙伴可以去看看fitz库
fitz库本身与pdfm完全没有关系,人家单独打开原文件,自己默默处理,不过不得不说提取出来的图片真不错,虽然不大但很清楚,你说气人不。
import fitz, re, os
# 用正则来确定对象是否为图片
checkIO = r'/Type(?= */XObject)'
checkIM = r'/Subtype(?= */Image)'
# 保存文件夹
folder_name = r'.\test'
# 打开原文件
file = fitz.open(r'original_name.pdf')
# 判断包含对象长度
length = file.xref_length()
# 计数,方便给图片命名
count = 0
for i in range(1, length):
# 通过索引获取对象
text = file.xref_object(i)
# 判断是否为图片
isXObject = re.search(checkIO, text)
isImage = re.search(checkIM, text)
if not isXObject or not isImage:
continue
# 获取图片
pix = fitz.Pixmap(file, i)
# 命名
new_name = '{}.png'.format(count)
# 两种保存方法
if pix.n < 5:
pix.writePNG(os.path.join(folder_name, new_name))
else:
pix0 = fitz.Pixmap(fitz.csRGB, pix)
pix0.writePNG(os.path.join(folder_name, new_name))
3 功能封装
以上已经基本实现目标了,但是还有一些美中不足,比如一次只能转一个文件,还要不断地改写路径,再比如只能自己使用,身边如果有朋友想用却没有安装python的,那么自己也只能干瞪眼了。下面介绍如何利用tkinter来封装上述代码,以实现功能自动化。
3.1 界面展示
3.2 设计思路
3.3 代码实现
(1)框架搭建
《Python编程》(第4版)这本书的编者一直强调代码复用,虽然我还不能完全理解这句话,但是在用tkinter编写界面程序时,我大概用到了这一思想。在使用tkinter库时,先不要急于写方法,而是把框架搭建好,正所谓磨刀不误砍柴工。
框架搭建主要用到两个方法,一个是Tk继承,一个是frame布局,代码一般如下:
from tkinter import *
class pdfTextPngTool(Tk):
# 初始化
def __init__(self):
# 继承
Tk.__init__(self)
# 工具命名
self.title('pdf转换工具')
# self.iconbitmap(r'.\icon.ico') 替换tk自带的图标
# 界面大小
width, height = self.winfo_screenwidth()//2-250,\
self.winfo_screenheight()//2-200
self.geometry(f'550x400+{width}+{height}') # x是英文字母
# 搭建框架方法
self.makeFrame()
# 开启循环
self.mainloop()
以上是初始化__init__中的代码,接下来开始调用makeFrame方法搭建框架:
# 搭建框架
def makeFrame(self):
# 先创建框架,再打包
self.frame1 = Frame(self, borderwidth=1, relief=RIDGE)
self.frame2 = Frame(self)
self.frame1.pack(expand=True, fill=BOTH)
self.frame2.pack(expand=True, fill=X)
# 创建组件
self.makeBudget()
注意,这里的框架是大框架,也即是为了区分不同的功能区。
(2)创建组件
frame是tkinter中最重要的一个组件,可以很好地帮助你来布局界面,所以一般都是先搭框架,再在框架内创建组件,即调用makeBudget方法。
在本程序中,主要有以下要创建的组件:
首先确定组件位置:
# 添加组件
def makeBudget(self):
# 框架1的内容
self.frame_path = Frame(self.frame1, borderwidth=1, relief=SUNKEN)
frame_savefile_path = Frame(self.frame1)
frame_savepic_path = Frame(self.frame1)
self.frame_path.pack(side=TOP, expand=True, fill=X)
frame_savefile_path.pack(side=TOP, expand=True, fill=X)
frame_savepic_path.pack(side=TOP, expand=True, fill=X)
label_path = Label(self.frame_path, text='原文件:')
label_path.pack(side=LEFT)
button_path = Button(self.frame_path, text='添加文件',
command=self.getOriFilename)
button_path.pack(side=LEFT)
label_savefile_path = Label(frame_savefile_path, text='文件保存目录:')
label_savefile_path.pack(side=LEFT)
# 用变量来提取内容
self.var1 = StringVar()
save_path = os.getcwd()
self.var1.set(save_path)
entry_savefile_path = Entry(frame_savefile_path, textvariable=self.var1,
state=DISABLED)
entry_savefile_path.pack(side=LEFT, expand=True, fill=X)
button_savefile = Button(frame_savefile_path, text='更改', width=6,
command=self.getSaveFilefolder)
button_savefile.pack(side=RIGHT)
label_savpic_folder = Label(frame_savepic_path, text='图片保存目录:')
label_savpic_folder.pack(side=LEFT)
self.var2 = StringVar()
self.var2.set(save_path)
entry_savepic_path = Entry(frame_savepic_path, textvariable=self.var2,
state=DISABLED)
entry_savepic_path.pack(side=LEFT, expand=True, fill=X)
button_savepic_folder = Button(frame_savepic_path, text='更改', width=6,
command=self.getSavePicfolder)
button_savepic_folder.pack(side=RIGHT)
以上都是frame1这一个框架里的内容。注意,frame里面还可以嵌套frame,来进行更加细致的布局,一定要在给frame命名时有意的区分好层次,以免混乱。下图展示了大框架frame1里的小框架布局:
(3)实现功能
tkinter中要实现特定的功能,一般都是靠button组件来完成,该组件中有一个command参数,用来回调函数。以本程序中“添加文件”功能为例。
button_path = Button(self.frame_path, text='添加文件',
command=self.getOriFilename)
button_path.pack(side=LEFT)
它的方法是调用getOriFilename函数,所以我们接下来就可以写这个函数的具体方法了。注意,如果我们要回调一个简单的函数,或是延迟使用,则可以用lambda来实现,比如 command=lambda:print(1)。这里不作展开,详细可以参考lambda方法。
# 准备步骤1,添加要转换的pdf文件
def getOriFilename(self):
# 选取当前文件夹
file_name = askopenfilename(initialdir=r'..')
# 判断文件是否为pdf
expen_name = os.path.splitext(file_name)[1]
if expen_name != '.pdf':
showerror('警告!', '您选择的不是pdf文件')
else:
self.show_filepath(file_name)
在这里,我们先让用户选择要读取的文件,需要用到tkinter.filedialog的askopenfilename方法,它返回文件对象的绝对路径。
from tkinter.filedialog import askopenfilename
因为我们要分析的是pdf文件,所以对于用户选择的非pdf文件我们要过滤掉,过滤完后就需要显示出用户选择的文件路径,告诉用户文件已经准备好了。这里调用的是show_filepath方法。
def show_filepath(self, file_name):
# 布局多个文件
frame_orifile_path = Frame(self.frame_path)
frame_orifile_path.pack(side=TOP, expand=True, fill=BOTH)
label_filepath = Label(frame_orifile_path, text=file_name)
label_filepath.pack(side=LEFT, expand=True, fill=X)
# 把存储文件路径标签和删除按钮的整个frame删掉
button_filepath = Button(frame_orifile_path, text='删除', width=5,
command=lambda:self.delFrame(
frame_orifile_path, file_name))
button_filepath.pack(side=RIGHT)
有时候用户可能添加错了文件,这时候我们就要提供一个删除功能,在显示路径标签的同时添加删除键。
注意,这里我们command方法就用到了lambda函数,因为我们是再用户点击后才删除该文件,如果不用lambda来延迟调用,则在创建时就直接把文件名又给删了。此外,我们还需给defFrame方法传递参数,告诉它要删除哪些内容。
这里又显示出了frame的作用了,我们直接把该文件所在的框架全部删除,一了百了。
def delFrame(self, frame_orifile_path, file_name):
frame_orifile_path.destroy()
到此为止,我们frame1.1的代码就全部完成了,可以看一下效果:
3.4 多线程
以上就是用tkinter进行界面的基本方法,其他都大同小异,依葫芦画瓢的事。这里再简单讲一下如何同时处理多个文件。
def pdfToPng(self):
if self.file_name_pool and self.var2.get():
checkIO = r'/Type(?= */XObject)'
checkIM = r'/Subtype(?= */Image)'
# 多线程处理图片
for i in self.file_name_pool:
pic_process_thread = threading.Thread(
target=self.threadPdftopng, args=(i, checkIO, checkIM))
pic_process_thread.start()
else:
showerror('警告!', '原文件或图片文件夹出错!')
这里使用的线程方法是threading库,其实并不复杂,就是读取预先存储好的所有原文件的路径,再用一个for循环处理,注意别忘了调用start开启线程。
4 总结
4.1 打包
其实pyinstaller这个方法也并不稳定,有不少时候打完包后程序没办法运行,它自带的代码诊断对于我这种初学者来说又太难处理,所以还是要尽量保证自己代码本身的健壮性。
另外要注意一点,如果使用Spyder编译器,在打包的时候一定要进入Anoconda Prompt里使用pyinstaller,否则Python本身可能缺失很多必要的库。打包代码如下:
pyinstaller -w pdf_tool.py
4.2 结语
以上内容就是我在自学pdf转word时的一些心得,并没有展示全部源代码,而只是提供了一个整体思路,也是为了保护自己的一点原创吧。
对完整程序源码有兴趣的小伙伴可以打赏一下,然后私我邮箱,我把源码发给你。
本程序当然还有很多不足的地方,希望有大神看到的话不吝指正,也欢迎小伙伴们私信讨论。
人生苦短,我用Python。
下篇笔记见~
2021.5.1
参考文献
【1】https://blog.csdn.net/xlfang114/article/details/110482463