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


 

引言

 

 

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

比如如下網站:

 

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

 

 

 

 

 

根據我的分析發現,這樣的在線預覽pdf的采用了pdfjs加載預覽,用爬蟲的方法根本無法直接拿到pdf內的內容的,對的,你注意到了我說的【根本無法直接拿到】中的直接兩個字,確實直接無法拿到,怎么辦呢?只能把pdf先下載到本地,然后用工具轉了,經過我查閱大量的相關資料發現,工具還是有很多:

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

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

這些工具在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中的圖片

https://blog.csdn.net/qq_15969343/article/details/81673302 

 

但是,我的目標是希望拿到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

 

 

 

2020.05.28補充:

 

camelot庫在處理pdf時,針對pdf里有圖片的時候,會占很高的內存,這個問題直接導致我的服務器CPU跑滿了,最后跟着內存也跑滿了,最后的結果就是,服務器直接癱瘓了,太坑了,我主要用了camelot,gevent,requests,apscheduler,其他的就是一些解析庫,自己封裝的解析方法,最后我用tracemalloc庫加上linux自帶的top配合,top作為實時監控CPU和內存占用,tracemalloc找出內存泄露主要位置,最后就找到了是camelot部分代碼的問題,坑啊

所以,如果你要用camelot,注意監測是否有圖片,如果有立即跳過,不然服務器世界卡崩,因為這個我的服務器已經卡崩了3次了,這次終於找到原因了,但是不可否認的是,camelot對於處理pdf時確實是個不錯的工具

 

所以可以在camelot讀取pdf時做一個判斷:

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')
            if not tables or not tables.n:
                continue
        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)

  

 

 

以上就是Python處理在線pdf的所有內容


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM