如何用Python实现PDF转Word?(全)

引言:现有的工具实现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]:

pdfminer3k与pdfminer.six

(2)pdf2docx:不得不说这个库是真的牛*,一键实现pdf转换docx。只想知道怎么快速转换pdf的小伙伴剩下的都可以不看了,下载这个库直接用就好了。具体参考该篇文章:pdf2docx实现方法

(3)fitz:该库有两个功能,一个是提取pdf中的所有图片,一个是直接把pdf转成图片。

(4)tkinter:这个库不知道大家用不用,我知道的现在教python图形化界面设计的多数用像wxpython这类的。因为我自学Python的主要参考书籍是O’REILLY出版的《Python编程》(第4版),这本大部头的书上册主要就是围绕tkinter库展开的,所以我一直都用它来写小程序。

(5)reos、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 界面展示

pdf_tool工具

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

askdirectory效果图

因为我们要分析的是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的代码就全部完成了,可以看一下效果:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,230评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,261评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,089评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,542评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,542评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,544评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,922评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,578评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,816评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,576评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,658评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,359评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,920评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,859评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,381评论 2 342

推荐阅读更多精彩内容