引言:現(xiàn)有的工具實(shí)現(xiàn)pdf轉(zhuǎn)word一般都是收費(fèi)的吆玖,我之前充了wps一年的會員筒溃,最后真正用到這個功能的也沒有幾次,后來臨時再用到又不想為了那一兩份文件充會員沾乘,對我來說確實(shí)太浪費(fèi)錢了怜奖。于是我就想著能不能用Python寫個程序,自己想什么時候用就什么時候用翅阵,想轉(zhuǎn)多少就轉(zhuǎn)多少歪玲。在網(wǎng)上查了一圈迁央,研究了幾天,終于算是弄出來了滥崩,在此做個歸納總結(jié)岖圈。此外,本篇不限于講解pdf轉(zhuǎn)word夭委,還有關(guān)于圖形化界面設(shè)計(jì)的知識幅狮。
1 準(zhǔn)備工作
本文是針對已經(jīng)懂一點(diǎn)Python的讀者寫的(當(dāng)然我自己也是初學(xué)者,歡迎大神不吝指正)株灸,所以很多基本的代碼就不做解釋了崇摄。
1.1 工具與庫
(1)編譯器:我用的是Anoconda提供的Spyder編譯器
(2)環(huán)境:Python 3.8
(3)庫:pdfminer3k、pdfminer.six慌烧、pdf2docx逐抑、fitz、tkinter屹蚊、re厕氨、os、threading汹粤、pyinstaller
1.2 庫的解釋
(1)pdfminer3k與pdfminer.six:我一開始全部用的是pdfminer3k命斧,網(wǎng)上多數(shù)教學(xué)也是用的這個庫,但是直到我嘗試解析一份報(bào)名表時嘱兼,發(fā)現(xiàn)原來的代碼什么也讀不出來(這個是什么問題后面會說)国葬。于是我搜索該問題的解決方案,看到一個外國網(wǎng)友的解答芹壕,說是得卸載pdfminer3k改用pdfminer.six汇四。我嘗試安裝了一下pdfminer.six(我沒有先卸載3k,而是直接安裝six)踢涌,問題果然解決了通孽。下面是對于兩個庫的不同的解釋[1]:
(2)pdf2docx:不得不說這個庫是真的牛*,一鍵實(shí)現(xiàn)pdf轉(zhuǎn)換docx睁壁。只想知道怎么快速轉(zhuǎn)換pdf的小伙伴剩下的都可以不看了背苦,下載這個庫直接用就好了。具體參考該篇文章:pdf2docx實(shí)現(xiàn)方法
(3)fitz:該庫有兩個功能潘明,一個是提取pdf中的所有圖片糠惫,一個是直接把pdf轉(zhuǎn)成圖片。
(4)tkinter:這個庫不知道大家用不用钉疫,我知道的現(xiàn)在教python圖形化界面設(shè)計(jì)的多數(shù)用像wxpython這類的硼讽。因?yàn)槲易詫W(xué)Python的主要參考書籍是O’REILLY出版的《Python編程》(第4版),這本大部頭的書上冊主要就是圍繞tkinter庫展開的牲阁,所以我一直都用它來寫小程序壤躲。
(5)re、os备燃、threading碉克、pyinstaller:這些庫大家應(yīng)該都知道的,我就不作多余解釋了并齐。
2 功能實(shí)現(xiàn)
2.1 讀取所有文字內(nèi)容
(1)實(shí)現(xiàn)代碼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)
# 創(chuàng)建pdf文件連接分析器
doc = PDFDocument(parser)
# 檢查原文件可否轉(zhuǎn)換
if not doc.is_extractable:
? ? raise PDFTextExtractionNotAllowed?
else:
? ? # 待存儲文檔
? ? out = Document()
? ? # 資源管理器
? ? rsrcmagr = PDFResourceManager()
? ? # 參數(shù)對象
? ? laparams = LAParams()
? ? # 聚合器
? ? device = PDFPageAggregator(rsrcmagr, laparams=laparams)
? ? # 解釋器
? ? interpreter = PDFPageInterpreter(rsrcmagr, device)
? ? # 逐頁分析原文件
? ? for page in PDFPage.create_pages(doc):
? ? ? ? interpreter.process_page(page)
? ? ? ? # 取得每一頁的結(jié)果
? ? ? ? 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)實(shí)現(xiàn)代碼2
上面的代碼是我總結(jié)網(wǎng)上已有的一般方法得出的結(jié)果漏麦,這里就可能遇到1.2中我說的問題,有時候會什么結(jié)果都讀不出來况褪。我在解決過程中無意發(fā)現(xiàn)pdf2txt這個神奇的工具撕贞。它本身是安裝pdfminer后自帶的一個腳本,使用方法非常簡單测垛,就是運(yùn)行cmd捏膨,輸入以下指令:
pdf2txt.py -o output.txt original_file.pdf
pdf2txt不僅可以轉(zhuǎn)換成txt,還可以轉(zhuǎn)換成html食侮、tag和xml号涯,不可謂不強(qiáng)大,并且html的保留了原文件的格式锯七,非常好用链快。有興趣的小伙伴可以閱讀一下源代碼。
因?yàn)閜df2txt畢竟是腳本工具眉尸,如果想用在自己的代碼中轉(zhuǎn)換起來還是比較麻煩的域蜗,所以我就參考它的源碼,借鑒了它把pdf轉(zhuǎn)換成txt的方法效五,發(fā)現(xiàn)比代碼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()
# 參數(shù)對象
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):
? ? ? ? # 直接把讀取的內(nèi)容寫進(jìn)文檔炉峰,省去了代碼1的讀寫步驟
? ? ? ? interpreter.process_page(page)
device.close()
output.close()
(3)實(shí)現(xiàn)代碼3
下面就是一鍵秒天秒地的pdf2docx了畏妖,代碼十分變態(tài),直接貼出
from pdf2docx import Converter
biantai = Converter(r'original_file.pdf')
biantai.convert(r'output.docx')
biantai.close()
說實(shí)話疼阔,有點(diǎn)過于簡單戒劫,就好像玩一個升級類的游戲,一個沒忍住輸了秘籍婆廊,結(jié)果游戲都不想玩了……有空得好好讀讀源代碼……
2.2 讀取所有圖片
有時候我們可能只想要原文件里的圖片迅细,這時候就要用到fitz庫了。這是我在b站無意搜到的一個方法淘邻,參考了一下茵典,有興趣的小伙伴可以去看看fitz庫
fitz庫本身與pdfm完全沒有關(guān)系,人家單獨(dú)打開原文件宾舅,自己默默處理统阿,不過不得不說提取出來的圖片真不錯彩倚,雖然不大但很清楚,你說氣人不扶平。
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()
# 計(jì)數(shù)帆离,方便給圖片命名
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 功能封裝
以上已經(jīng)基本實(shí)現(xiàn)目標(biāo)了,但是還有一些美中不足结澄,比如一次只能轉(zhuǎn)一個文件哥谷,還要不斷地改寫路徑,再比如只能自己使用麻献,身邊如果有朋友想用卻沒有安裝python的们妥,那么自己也只能干瞪眼了。下面介紹如何利用tkinter來封裝上述代碼赎瑰,以實(shí)現(xiàn)功能自動化王悍。
3.1 界面展示
3.2 設(shè)計(jì)思路
3.3 代碼實(shí)現(xiàn)
(1)框架搭建
《Python編程》(第4版)這本書的編者一直強(qiáng)調(diào)代碼復(fù)用,雖然我還不能完全理解這句話餐曼,但是在用tkinter編寫界面程序時压储,我大概用到了這一思想。在使用tkinter庫時源譬,先不要急于寫方法集惋,而是把框架搭建好,正所謂磨刀不誤砍柴工踩娘。
框架搭建主要用到兩個方法刮刑,一個是Tk繼承,一個是frame布局养渴,代碼一般如下:
from tkinter import *
class pdfTextPngTool(Tk):
? ? # 初始化
? ? def __init__(self):
? ? ? ? # 繼承
? ? ? ? Tk.__init__(self)
? ? ? ? # 工具命名
? ? ? ? self.title('pdf轉(zhuǎn)換工具')
? ? ? ? # self.iconbitmap(r'.\icon.ico') 替換tk自帶的圖標(biāo)
? ? ? ? # 界面大小
? ? ? ? width, height = self.winfo_screenwidth()//2-250,\
? ? ? ? ? ? self.winfo_screenheight()//2-200
? ? ? ? self.geometry(f'550x400+{width}+{height}') # x是英文字母
? ? ? ? # 搭建框架方法
? ? ? ? self.makeFrame()
? ? ? ? # 開啟循環(huán)
? ? ? ? self.mainloop()
以上是初始化__init__中的代碼雷绢,接下來開始調(diào)用makeFrame方法搭建框架:
# 搭建框架
? ? def makeFrame(self):
? ? ? ? # 先創(chuàng)建框架,再打包
? ? ? ? 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)
? ? ? ? # 創(chuàng)建組件
? ? ? ? self.makeBudget()
注意理卑,這里的框架是大框架翘紊,也即是為了區(qū)分不同的功能區(qū)。
(2)創(chuàng)建組件
frame是tkinter中最重要的一個組件藐唠,可以很好地幫助你來布局界面帆疟,所以一般都是先搭框架,再在框架內(nèi)創(chuàng)建組件宇立,即調(diào)用makeBudget方法踪宠。
在本程序中,主要有以下要創(chuàng)建的組件:
首先確定組件位置:
# 添加組件
? ? def makeBudget(self):
? ? ? ? # 框架1的內(nèi)容
? ? ? ? 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)
? ? ? ? # 用變量來提取內(nèi)容
? ? ? ? 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這一個框架里的內(nèi)容妈嘹。注意柳琢,frame里面還可以嵌套frame,來進(jìn)行更加細(xì)致的布局,一定要在給frame命名時有意的區(qū)分好層次染厅,以免混亂痘绎。下圖展示了大框架frame1里的小框架布局:
(3)實(shí)現(xiàn)功能
tkinter中要實(shí)現(xiàn)特定的功能,一般都是靠button組件來完成肖粮,該組件中有一個command參數(shù)孤页,用來回調(diào)函數(shù)。以本程序中“添加文件”功能為例涩馆。
button_path = Button(self.frame_path, text='添加文件',
? ? ? ? ? ? ? ? ? ? ? ? ? ? command=self.getOriFilename)
button_path.pack(side=LEFT)
它的方法是調(diào)用getOriFilename函數(shù)行施,所以我們接下來就可以寫這個函數(shù)的具體方法了。注意魂那,如果我們要回調(diào)一個簡單的函數(shù)蛾号,或是延遲使用,則可以用lambda來實(shí)現(xiàn)涯雅,比如 command=lambda:print(1)鲜结。這里不作展開,詳細(xì)可以參考lambda方法活逆。
# 準(zhǔn)備步驟1精刷,添加要轉(zhuǎn)換的pdf文件
? ? def getOriFilename(self):
? ? ? ? # 選取當(dāng)前文件夾
? ? ? ? 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
因?yàn)槲覀円治龅氖莗df文件纫事,所以對于用戶選擇的非pdf文件我們要過濾掉,過濾完后就需要顯示出用戶選擇的文件路徑所灸,告訴用戶文件已經(jīng)準(zhǔn)備好了丽惶。這里調(diào)用的是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)
? ? ? ? # 把存儲文件路徑標(biāo)簽和刪除按鈕的整個frame刪掉
? ? ? ? button_filepath = Button(frame_orifile_path, text='刪除', width=5,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? command=lambda:self.delFrame(
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? frame_orifile_path, file_name))
? ? ? ? button_filepath.pack(side=RIGHT)
有時候用戶可能添加錯了文件爬立,這時候我們就要提供一個刪除功能钾唬,在顯示路徑標(biāo)簽的同時添加刪除鍵。
注意懦尝,這里我們command方法就用到了lambda函數(shù)知纷,因?yàn)槲覀兪窃儆脩酎c(diǎn)擊后才刪除該文件壤圃,如果不用lambda來延遲調(diào)用陵霉,則在創(chuàng)建時就直接把文件名又給刪了。此外伍绳,我們還需給defFrame方法傳遞參數(shù)踊挠,告訴它要刪除哪些內(nèi)容。
這里又顯示出了frame的作用了,我們直接把該文件所在的框架全部刪除效床,一了百了睹酌。
def delFrame(self, frame_orifile_path, file_name):
? ? ? ? frame_orifile_path.destroy()
到此為止,我們frame1.1的代碼就全部完成了剩檀,可以看一下效果:
3.4 多線程
以上就是用tkinter進(jìn)行界面的基本方法憋沿,其他都大同小異,依葫蘆畫瓢的事沪猴。這里再簡單講一下如何同時處理多個文件辐啄。
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庫壶辜,其實(shí)并不復(fù)雜,就是讀取預(yù)先存儲好的所有原文件的路徑担租,再用一個for循環(huán)處理砸民,注意別忘了調(diào)用start開啟線程。
4 總結(jié)
4.1 打包
其實(shí)pyinstaller這個方法也并不穩(wěn)定奋救,有不少時候打完包后程序沒辦法運(yùn)行岭参,它自帶的代碼診斷對于我這種初學(xué)者來說又太難處理,所以還是要盡量保證自己代碼本身的健壯性菠镇。
另外要注意一點(diǎn)冗荸,如果使用Spyder編譯器,在打包的時候一定要進(jìn)入Anoconda Prompt里使用pyinstaller利耍,否則Python本身可能缺失很多必要的庫蚌本。打包代碼如下:
pyinstaller -w pdf_tool.py
4.2 結(jié)語
以上內(nèi)容就是我在自學(xué)pdf轉(zhuǎn)word時的一些心得,并沒有展示全部源代碼隘梨,而只是提供了一個整體思路程癌,也是為了保護(hù)自己的一點(diǎn)原創(chuàng)吧。
對完整程序源碼有興趣的小伙伴可以打賞一下轴猎,然后私我郵箱嵌莉,我把源碼發(fā)給你。
本程序當(dāng)然還有很多不足的地方捻脖,希望有大神看到的話不吝指正锐峭,也歡迎小伙伴們私信討論。
人生苦短可婶,我用Python沿癞。
下篇筆記見~
2021.5.1
參考文獻(xiàn)
【1】https://blog.csdn.net/xlfang114/article/details/110482463