引言
最近在爬一个网站,而后爬到详情页的时候发现,目标内容是用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的全部内容