python爬蟲處理在線預覽的pdf文檔

 

引言

 

 

最近在爬一個網站,而後爬到詳情頁的時候發現,目標內容是用pdf在線預覽的css

好比以下網站:html

 

https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdfpython

 

 

 

 

 

根據個人分析發現,這樣的在線預覽pdf的採用了pdfjs加載預覽,用爬蟲的方法根本沒法直接拿到pdf內的內容的,對的,你注意到了我說的【根本沒法直接拿到】中的直接兩個字,確實直接沒法拿到,怎麼辦呢?只能把pdf先下載到本地,而後用工具轉了,通過我查閱大量的相關資料發現,工具仍是有不少:git

  1.借用第三方的pdf轉換網站轉出來github

  2.使用Python的包來轉:如:pyPdf,pyPdf2,pyPdf4,pdfrw等工具json

這些工具在pypi社區一搜一大把:瀏覽器

 

 可是效果怎麼樣就不知道了,只能一個一個去試了,到後面我終於找到個庫,很是符合個人需求的庫 ——camelot緩存

 

camelot能夠讀取pdf文件中的數據,而且自動轉換成pandas庫(數據分析相關)裏的DataFrame類型,而後能夠經過DataFrame轉爲csv,json,html都行,個人目標要的就是轉爲html格式,好,廢話很少說,開始搞多線程

 

開始解析

  

1.安裝camelot:工具

 

pip install camelot-py

pip install cv2  (由於camelot須要用到這個庫)

 

2.下載pdf:由於在線的pdf其實就是二進制流,因此得按照下載圖片和視頻的方式下載,而後存到本地的一個文件裏,這個步驟就很少說了   

 

3.解析:

 

import camelot file = 'temp.pdf' table = camelot.read_pdf(file,flavor='stream') table[0].df.to_html('temp.html')

 

以上的temp.html就是我但願獲得的數據了,而後根據個人分析發現,在read_pdf方法裏必定帶上參數  【flavor='stream'】,否則的話就報這個錯:

 

RuntimeError: Please make sure that Ghostscript is installed

 

緣由就是,read_pdf默認的flavor參數是lattice,這個模式的話須要安裝ghostscript庫,而後你須要去下載Python的ghostscript包和ghostscript驅動(跟使用selenium須要下載瀏覽器驅動一個原理),而默認咱們的電腦確定是沒有安裝這個驅動的,因此就會報上面那個錯。我試着去裝了這個驅動和這個包,去read_pdf時其實感受沒有本質區別,是同樣的,因此帶上參數flavor='stream'便可,固然若是你硬要用lattice模式的話,安裝完ghostscript包和ghostscript驅動以後,記得在當前py文件用  【import ghostscript】導入下這個包,否則仍是會報如上錯誤

 

繼續走,發現能拿到我想要的數據了,很是nice,而後忽然的,報了以下錯誤:

PyPDF2.utils.PdfReadError: EOF marker not found

 

 

 當時就是臥槽,這什麼狀況,我開始去研究EOF marker是什麼意思,可是我直接打開這個pdf文件又是正常的

 

 

 

很是詭異,網上查閱了一堆,大概意思就是說,沒有EOF結束符,這個東西在以前我作js開發的時候遇到過,js的語句體{},少了最後的【}】,

我又去了解了下EOF到底在二進制文件指的什麼,而後看到老外的這個帖子:

 

 我用一樣的方法查看數據的前五個字符和後五個字符:

 

 

 

 

 

 

 

 

好像有了眉目,我以文本的方式打開了我下載到本地的一個pdf,在%%EOF結尾以後還有不少的null

 

 

 

難道是NULL的問題?我手動刪掉null以後,單獨對這個修改過的pdf用pdf查看器打開,正常打開,沒有問題,我接着用代碼針對這個文件執行read_pdf,發現很是神奇的不會報錯了,那還真是結尾的NULL元素了。

而後我在從網上讀取到pdf以後的二進制部分用字符串的strip()方法,覺得用strip能夠去除那些null,結果發現仍是如此

 

 

 

 -------------------------------------

那就只有先鎖定  %%EOF 所在位置,而後切片操做了,部分代碼以下,果真問題解決,但同時又報了一個新的錯,這個就是個編碼問題了,相信搞爬蟲的朋友們對這個問題很是熟悉了

 

 

 

先暫時無論這個問題,我又改了下目標網站的指定頁碼

 

 

 

pdfminer.psparser.SyntaxError: Invalid dictionary construct: [/'Type', /'Font', /'Subtype', /'Type0', /'BaseFont', /b"b'", /"ABCDEE+\\xcb\\xce\\xcc\\xe5'", /'Encoding', /'Identity-H', /'DescendantFonts', <PDFObjRef:11>, /'ToUnicode', <PDFObjRef:19>]

 

 

發現問題愈來愈嚴重了,我鼓搗了一番以後,又查了一堆資料,將utf-8改爲gb18030仍是報錯,我發現我小看這個問題了,接着查閱,而後發現github上camelot包的issues也有人提了這個報錯,

https://github.com/atlanhq/camelot/issues/161

 

 

 

 

 

 

 

而後這裏有我的說能夠修復下pdf文件:

 

 

我查了下,須要安裝一個軟件mupdf,而後在終端用命令 修復  

mutool clean 舊的.pdf 新的.pdf 

 

首先這並非理想的解決方法,在python代碼中,是能夠調用終端命令,用os和sys模塊就能夠,可是萬一由於終端出問題還很差找緣由,因此我並無去修復,以後我發現我這個決定是對的 

 

接着看,發現issue裏不少人都在反饋這個問題,最後看到這個老哥說的

 

 

 

 

 

大概意思就是說pypdf2沒法完美的處理中文文檔的pdf,而camelot對pdf操做就基於pypdf2,臥槽,這個就難了。

 

 

而後我又查到這篇文章有說到這個問題:https://blog.csdn.net/kmesky/article/details/102695520

 

 

那隻能硬改源碼了,改就改吧,畢竟這也不是我第一次改源碼了

 

注意:若是你不知道的狀況下,千萬不要改源碼,這是一個大忌,除非你很是清楚你要作什麼

 

修改源碼:

1.format.py

C:\Program Files\Python37\Lib\site-packages\pandas\io\formats\format.py該文件的第846行

 

由這樣:

 

 

 

改爲這樣:

 

  

 

2.generic.py

 

File "D:\projects\myproject\venv\lib\site-packages\PyPDF2\generic.py", 該文件的第484行

 

 

 

 

3.utils.py

Lib/site-packages/PyPDF2/utils.py 第238行

 

 

4.運行

 

再運行:以前那些錯誤已經沒有了

 

但同時又有了一個新的錯

 

其實這個超出索引範圍的報錯的根本是上面的警告:UserWarning:page-1 is image-based,camelot only works on text-based pages. [streams.py:443]

 

 

 

由於源數據pdf的內容是個圖片,再也不是文字,而camelot只能以文本形式提取數據,因此數據爲空,因此 table[0]會報索引超出範圍

 

針對圖片的處理,我網上查閱了一些資料,以爲這篇文章寫的不錯,能夠提取pdf中的圖片

http://www.javashuo.com/article/p-sxccfttj-ho.html 

 

可是,個人目標是但願拿到pdf中的內容,而後轉成html格式,在以前,我已經由在線pdf->本地pdf->提取表格->表格轉html,這是第一種。

若是要提取圖片的話,那步驟就是第二種:在線pdf->本地pdf->提取圖片->ocr提取表格->驗證對錯->表格轉html,這樣就會多些步驟,想一想,我爲了拿到一個網站的數據,每一個網頁就要作這些操做,並且還要判斷是圖片就用用第二種,是表格就用第一種,兩個方法加起來的話,爬一個網站的數據要作的操做的就多了,雖然這些都屬於IO操做型,可是到後期開啓多線程,多進程時,與那些直接就能從源網頁提取的相比就太耗時間了。

這樣不是不行,是真的耗時間,因此我暫時放棄對圖片的提取了,只提取table,先對pdf二進制數據判斷是不是圖片,是圖片就跳過了

 

原理就是,根據上面那片博客裏的:

 

 

 

 

 

打開二進制源碼驗證:

 

 

第一個,它確實是圖片的:

 

 

 

 

 

 

第二個,它是表格:

 

 

 

 

 

 

不過通過個人驗證,發現這個方法正確率不能百分之百,少部分的即便是表格仍是有/Image和/XObject相關的字符串

 

 

那沒辦法了,有多少是多少吧

 

部分代碼實現:

fujian_data = requests.get(fujian_url, headers=headers).content fujian_index = fujian_data.index(b'%%EOF') fujian_data = fujian_data[:fujian_index + len(b'%%EOF')] checkXO = rb"/Type(?= */XObject)" checkIM = rb"/Subtype(?= */Image)" isXObject = re.search(checkXO, fujian_data) isImage = re.search(checkIM, fujian_data) if isXObject and isImage: # 是圖片跳過 pass f = open('temp.pdf', 'wb') f.write(fujian_data) f.close() tables = camelot.read_pdf('temp.pdf', flavor='stream') if os.path.exists('temp.pdf'): os.remove('temp.pdf') # 刪除本地的pdf tables[0].df.to_html('foo.html', header=False, index=False)

 

 

 

 

至此完畢,固然,你也能夠用camelot 的to_csv 和 to_json方法轉成你但願的,具體就本身研究了

 

 

2020年2月14號補充:

以上的方法確實能夠處理在線的pdf文檔了(非圖片式),可是,還有個遺留的問題,就是以上只能處理單頁的pdf,若是是多頁的pdf仍是不行,好比以下,

 

 

 像這種不止一頁的數據的,按以上的方法提取出來的內容是不完整的。

 

那麼怎麼辦呢?首先得肯定這個pdf是多少頁對吧,可是目前有沒有什麼方法來獲取pdf的頁碼呢?我查了下camelot模塊的方法,暫時沒找到,網上一查,有人說得經過pdfminer模塊來操做,而後我修改的代碼以下:

 

import camelot import requests import re import js2py import execjs import json from urllib.parse import urljoin from lxml.html import tostring from bs4 import BeautifulSoup from html import unescape from lxml import etree from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter from pdfminer.converter import PDFPageAggregator from pdfminer.layout import LAParams from pdfminer.pdfpage import PDFPage from pdfminer.pdfparser import PDFParser from pdfminer.pdfdocument import PDFDocument import re def read_pdf_text(filePath): # 以二進制讀模式打開 file = open(filePath, 'rb') # 用文件對象來建立一個pdf文檔分析器 praser = PDFParser(file) # 建立一個PDF文檔對象存儲文檔結構,提供密碼初始化,沒有就不用傳該參數 doc = PDFDocument(praser, password='') # 檢查文件是否容許文本提取 if doc.is_extractable: # 建立PDf 資源管理器 來管理共享資源,#caching = False不緩存 rsrcmgr = PDFResourceManager(caching=False) # 建立一個PDF設備對象 laparams = LAParams() # 建立一個PDF頁面聚合對象 device = PDFPageAggregator(rsrcmgr, laparams=laparams) # 建立一個PDF解析器對象 interpreter = PDFPageInterpreter(rsrcmgr, device) # 獲取page列表 # 循環遍歷列表,每次處理一個page的內容 results = '' for page in PDFPage.create_pages(doc): interpreter.process_page(page) # 接受該頁面的LTPage對象 layout = device.get_result() # 這裏layout是一個LTPage對象 裏面存放着 這個page解析出的各類對象 # 通常包括LTTextBox, LTFigure, LTImage, LTTextBoxHorizontal 等等 for x in layout: if hasattr(x, "get_text"): results += x.get_text() # 若是x是水平文本對象的話 # if (isinstance(x, LTTextBoxHorizontal)): # text = re.sub(replace, '', x.get_text()) # if len(text) != 0: # print(text) if results: # print(results) return results headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36', } url =’' # 保密  data = '' # 保密  req = requests.post(url, headers=headers, data=data) res = req.json() data = res.get('UserArea').get('InfoList') for item in data: current_data = dict() title = item.get('ShowBiaoDuanName') link = item.get('FilePath') pub_date = item.get('SHR_Date') second_url = re.findall(r"<a href='(.*)'>", link) if second_url: second_url = second_url[0] sec_res = requests.get(second_url, headers=headers).content f = open('temp.pdf', 'wb') f.write(sec_res) f.close() local_data = read_pdf_text('temp.pdf') print(local_data)

 

打印輸出結果(部分截圖):

 

 

 

發現其實文字的話是能夠正常提取,可是一旦有表格的話提取出來的並不理想,又繞回來了,仍是得用上camelot?

 

我又回到剛纔那個問題,得經過什麼工具獲取到頁碼,而後用for循環結合camelot就能夠了,根據上面的pdfminer,發現確實能獲取到頁碼,可是感受代碼量有點多啊,我就獲取個頁面都要這麼多行,我又換了個工具—— PyPDF2,並且camelot就是在PyPDF2之上操做的

 

好,怎麼獲取呢?

 # 獲取頁碼數 reader = PdfFileReader(file_path) # 不解密可能會報錯:PyPDF2.utils.PdfReadError: File has not been decrypted if reader.isEncrypted: reader.decrypt('') pages = reader.getNumPages()

 

就這幾行就能夠了,實際其實就兩行,中間那個是爲了判斷pdf是否有加密的

 

那麼結合camelot來操做:

 

import camelot import requests import re import js2py import execjs from urllib.parse import urljoin from lxml.html import tostring from bs4 import BeautifulSoup from html import unescape from lxml import etree from PyPDF2 import PdfFileReader def camelot_contrl_pdf(file_path): # 單頁處理 # 獲取頁碼數 reader = PdfFileReader(file_path) # 不解密可能會報錯:PyPDF2.utils.PdfReadError: File has not been decrypted if reader.isEncrypted: reader.decrypt('') pages = reader.getNumPages() if not pages: return content = '' for page in range(pages): tables = None f = None local_data = None page = str(page + 1) try: tables = camelot.read_pdf(file_path, pages=page, flavor='stream') except Exception: pass if tables: tables[0].df.to_html('foo.html', header=False, index=False) if os.path.exists('foo.html'): try: f = open('foo.html', encoding='utf-8') local_data = f.read() except Exception: try: f = open('foo.html', encoding='gbk') local_data = f.read() except Exception: pass if local_data: content += local_data if f: f.close() if content: return content headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36', } url = '' # 保密 req = requests.get(url, headers=headers, verify=False) res = req.content.decode('utf-8') html = etree.HTML(res) data = html.xpath('//table[@class="table"]/tbody/tr') second_link_f = '' # 保密 for item in data: second_url = ''.join(link) if link else '' sec_req = requests.get(second_url, headers=headers, verify=False) sec_res = sec_req.content.decode('gbk') sec_html = etree.HTML(sec_res) fujian_url = sec_html.xpath('//iframe/@src') fujian_url = ''.join(fujian_url) if fujian_url else '' if fujian_url: thr_link = re.findall(r'file=(.*)', fujian_url) if thr_link: thr_link = thr_link[0] thr_url = urljoin(second_link_f, thr_link) print(thr_url) thr_res = requests.get(thr_url, headers=headers).content if not thr_res or b'%%EOF' not in thr_res: continue fujian_index = thr_res.index(b'%%EOF') thr_res = thr_res[:fujian_index + len(b'%%EOF')] # checkXO = rb"/Type(?= */XObject)" # checkIM = rb"/Subtype(?= */Image)" # isXObject = re.search(checkXO, thr_res) # isImage = re.search(checkIM, thr_res) # if isXObject and isImage: # # 是圖片跳過 # continue f = open('temp.pdf', 'wb') f.write(thr_res) f.close() local_data = camelot_contrl_pdf('temp.pdf') if local_data: soup = BeautifulSoup(local_data, 'html.parser') if os.path.exists('temp.pdf'): os.remove('temp.pdf') # 刪除本地的pdf if soup: [s.extract() for s in soup("style")] [s.extract() for s in soup("title")] [s.extract() for s in soup("script")] print(soup)

 

 

輸出:

 

 

 

 

跟源網站內容對比:

 

 

 數據一致,只是css樣式顯示有點出入,調下樣式就好了,終於ojbk

 

 

以上就是Python處理在線pdf的全部內容

相關文章
相關標籤/搜索