PDF解析

        PDF是一种查看方便但解析起来非常不方便的工具,不理解为什么到现在还没对这个问题从源头优化一下。对PDF文件的解析,一般分成以下2种:

1、文字版PDF(打开PDF后可以选中文字):对pdf源文件的解析;

2、扫描版PDF:将pdf转化成图像再解析。

一、判断文件类型为PDF

# 方法一:filetype文件可判断PDF、JPEG等多种文件类型
import filetype
print(filetype.guess(file_path))  # <filetype.types.archive.Pdf object at 0x7fc2b8d947b8>

# 方法二:二进制读取文件进行判断
binfile = open(file_path, 'rb')  # 二制字读取
binfile.seek(0)   # 文件游标移动,从位置0开始
print(binfile.read(10))  # b'%PDF-1.4\r\n'

二、判断是文字版还是扫描版PDF

方案一:基于提取文字

        目前没有找到特别优雅的方法来通过python代码区分,采用的是读取第一页(或所有页)基于是否包含文字来进行区分。网上有个人也是采用了这个方案:

https://github.com/dothinking/pdf2docx/issues/99

    下文用了pdfplumber和fitz两种方法来进行源文件所有页的解析,如场景特殊且对速度要求高,可以改成第一页。

import fitz
import pdfplumber
import time

def fitz_judee_pdf(filename):
    doc = fitz.open(filename)
    for page in doc:
        # print(page.getText())
        if page.getText():
            return True
    return False


def pdfplumber_judee_pdf(filename):
    doc = pdfplumber.open(filename)
    for page in doc.pages:
        # print(page.extract_words())
        if page.extract_words():
            return True
    return False


t0 =time.time()
print(fitz_judee_pdf(file_path))
print(time.time()-t0)

t0 =time.time()
print(pdfplumber_judee_pdf(file_path))
print(time.time()-t0)

方案二:基于交叉引用表

理论上可以通过交叉引用表的属性来判断,扫描版有/Subtype /Image这行,未通过大量数据验证过是否严谨,注意lenXREF大于等于页码

  checkIM = r"/Subtype(?= */Image)"
  pdf = fitz.open(path)
  lenXREF = pdf._getXrefLength()
  count = 1
  for i in range(1, lenXREF):
    text = pdf._getXrefString(i)
    isImage = re.search(checkIM, text)
    if not isImage:
      continue

三、pdf 解析文本

        公认比较好用的pip库是pdfplumber,此外fitz也可以,经费够可以调pdflux这种服务商。fitz仅处理文本,pdfplumber还可以处理表格。

3.1  pdfplumber解析文本

        PDFPlumber是基于 PDFMiner 构建的 PDF 解析器,微软构建DocBank(大规模文档布局标注数据集)用到了这个库。不能100%还原表格,支持可视化调试。在mac上解析某个markdown生成的pdf时遇到了一个坑,解析出来的文字是cid编码,解析普通的pdf没有问题。

CID码:PDF包含将字符代码映射到字形索引的CMAP。因此,CID是它映射到的字形在CMAP表中的字符标识。

import pdfplumber
pdf = pdfplumber.open(path)
import pandas as pd
for page in pdf.pages:
    # 获取当前页面的全部文本信息,包括表格中的文字
    # print(page.extract_text())   # 只提取文字,对表格信息,有简单合并行
    # print(page.extract_words())   # 提取字符串的文本、坐标等信息
    # print(page.extract_tables())   # 按行元素返回表格信息,无坐标
    # print(page.chars)   # 按字符而非字符串提取文本、坐标等信息

    for t in page.extract_tables():
        # for row in t:
        #     print(row)
        # 得到的table是嵌套list类型,转化成DataFrame更加方便查看和分析
        df = pd.DataFrame(t[1:], columns=t[0])
        print(df)
    # 只用第一页测试
    break

pdf.close()

3.2 fitz 解析文本

import fitz
doc = fitz.open(path)
whole_pdf = []
for i, page in enumerate(doc):
    words = page.getTextWords()  # [x0, y0, x1, y1, "text", block#, line#, word#]

    print(words)
    # for w in words:
    #     print(fitz.Rect(w[:4]), w[4])
    break

3.3 pdfplumber和fitz的区别

pdfplumber:速度慢,提取出的文字全

fitz:速度快,例如‘判断是文字版还是扫描版PDF’这部分会比pdfplumber快很多;有些pdf文字的部分文字明明可选但抽不出来?

3.4 cid码

       可以简单理解为这种pdf的源文件被加密了,不能用代码直接读。

        并且如果直接打开pdf文件去复制黏贴,也会ctrl+v出奇奇怪怪的字符。

cid介绍:[PDF基础知识] CID字库在PDF流程中的应用_(cid:61 pdf-CSDN博客

        [PDF基础知识] CID字库在PDF流程中的应用_(cid:61 pdf-CSDN博客

        在某个pdf中,pdfplumber 解析出cid码,fitz解析出更奇怪的乱码。因此考虑基于cid码去还原原始文本。仔细研究pdfplumber 解析结果后发现,数字、英文及-可以正常解析,汉字、冒号等不能正常解析。

        查询网络信息得知,可以通过char(int(i)),来将cid码转为字符串。但我这里转化后还是乱码,某次翻到某篇文章说可以用分段的方式解决偏移https://www.zhihu.com/question/55063377,当时那篇文章是根据汉字、数字、符号划分了三段,代码如下:

Python解析pdf得到的中文CID字库如何变成utf-8或其他编码呢? - 知乎

if key <= 122:
    # 数字大小写
    rep[_[0]] = chr(key+31)
elif key <= 21902:
    # 中文
    rep[_[0]] = chr(key+19146)
else:
    # 中文标点
    rep[_[0]] = chr(key+43378)

        

        但并非所有cid码的划分标准都相同,需要自己想办法去解码,比如我遇到的某个pdf在汉字里也要划段,“送检日期”中“送”和“检日期”的偏移不同。

        具体这个偏移怎么探测呢?首先试试不偏移的情况,能否直接chr(int(i))转出来。其次,需要自己知道一些映射关系,如知道某些字符串到cid的对应关系,然后用以下2个函数测试。

# cid码到字符串
print(char(int(i+a)))

# 字符串到ASCII码
print(ord('X'))

        理论上应该有个映射表集,不同字库、字体的cid码对应的字符是什么,但是我没有在网上找到相关资料。

l此外解析带表格的pdf还有一些其他方法:

1、pdfminer:较复杂、不能直接还原出表格,据说是pdfplumber的底层

2、tabula:依赖java、识别有问题、难以区分多张表

3、各个表格解析的开源项目,如paddleocr、tablemaster、camelot...

4、各个人工智能服务供应商,注册服务后获取token,需要上传文件后下载,如庖丁科技的pdflux

5、poppler:C++

四、pdf & 图像

   4.1 pdf 转 图像

不论是扫描生成的图片型pdf还是word生成的可复制文本型pdf,均可转为图像。

def pdf2img(file_path, dest_path):
    zoom_x = 2.0  # horizontal zoom 注意这两个参数,代表缩放比例,可以小于1,小于1时进行缩小
    zomm_y = 2.0  # vertical zoom
    mat = fitz.Matrix(zoom_x, zomm_y)  # zoom factor 2 in each dimension
    doc = fitz.open(file_path)  # open document
    image_paths = []
    for page in doc:  # iterate through the pages
        pix = page.getPixmap(matrix=mat)
        image_path = '{0}_{1}.jpg'.format(dest_path, page.number+1)
        pix.writeImage(image_path)
        image_paths.append(image_path)
    return image_paths

        遇到过一个神奇的文档,用万彩办公大师的图片转pdf功能生成,pdf文件只有13M,但是打开后每一页都超级大,单个字都有10*10大小,简直是个矩形海报,可能是顾客选了最大的参数,导致转图像时getPixmap这步内存爆炸,从而导致整个服务被杀死自动挂掉。

      又遇到了一个神奇的文档,pdf是可以选字的文字版pdf,某一页开始尺寸变化,在对该pdf进行pdf2img时服务挂掉,且不能通过try来处理,报错信息是转pdf时提示段错误(核心已转储)。解决方式是升级fitz相关的PyMuPDF版本到1.19.0。

4.2 pdf提取图像

# pip3 install pyMuPDF==1.19.0
doc = fitz.open(file_path)  # open document
for page in doc:  # iterate through the pages
    img_list = page.get_images()
    for img in img_list:
        im = fitz.Pixmap(doc,img[0])
        img_array = np.frombuffer(im.samples,dtype=np.uint8).reshape(pix.height,pix.width,-1)

五、获取页面大小

得到每一页的宽高

方案一:

import pdfplumber
with pdfplumber.open(path) as pdf:
    for i in range(len(pdf.pages)):
        tmp = pdf.pages[i]
        h = tmp.height
        w = tmp.weight

方案二:

pdf = fitz.open(path)
for i,page in enumerate(pdf):
    r= page.rect # 输出是(0,0,W,H)的矩形
    w=r[2]
    h=r[3]

六、交叉引用表

开源得到pdf文件的一系列属性信息

  pdf = fitz.open(path)
  lenXREF = pdf._getXrefLength() # 不一定等于pdf页码数,应该是大于等于页码数
  count = 1
  for i in range(1, lenXREF):
    text = pdf._getXrefString(i)  
# 获得各种意义不明的属性,有的能推测出来,例如
# Subtype Image表示该页是图片类型
# MediaBox、CropBox表示每一页的尺寸,可以简单理解为分辨率(实际这2者可以不同,有兴趣看https://blog.csdn.net/fghler/article/details/107456819)

# 注意Width Height不表示分辨率,遇到过一个文件,wh只有三四千,但实际分辨率有几十万

七、gptpdf

GitHub - CosmosShadow/gptpdf: Using GPT to parse PDF

基于图像大模型,如gpt-4o将pdf内容转化为markdown。没有用判断源生非源生那套,而是直接提取图片然后,img2markdown。步骤为:

step1:基于fitz提取每一页的图像,如果有图或表格等rect,另外截图后保存为png,并将其位置在原图内框出来。

step2:将图像输入大模型,要求转为markdown,截图处用![]()形式插入

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值