zoukankan      html  css  js  c++  java
  • 用于pdf翻译的PdfTranlate(续1)

      项目地址:https://gitee.com/Shanyalin/pdf-tranlate

      上一篇博客的部分遗留问题,我需要在此明确一下。

      单纯使用pymupdf来解决pdf提取问题,是很困难的。比如表格,就是个很难绕过的问题。这里会逐步引入一些已经证实的解决方案。

      首先,重申一次pdf 翻译的思路及注意事项。翻译,因为翻译api的泛滥,不再做赘述。核心重点在内容提取,文本整合,内容输出。

      内容提取,注意不再是文本提取了。这次不仅要将文本、图片提取,也要将表格、自定义的绘画(draw)也提取出来。在内容上尽可能的向原始文件靠近。

            文本整合,由于提取出了表格,所以表格内的内容尽可能保持原样,不做整合。表格外的内容,尽可能以段落为单位,一页一页提取整合,提高翻译的准确性。

      内容输出,在上一篇博客中定下了原样输出的原则,本次依然在此基础上,将表格及自定义的绘画都还原出来。

      接下来先介绍一下相对简单的内容——自定义绘画。

      pymupdf的Page.get_drawings()可以提供页面中自定义绘画相关的类型,位置,颜色等一系列相关参数。详细可以参考:Page.get_drawings

      其中items是个元组列表,元组的第一个字段表示绘画类型,后续字段都是位置相关信息。l表示线段,后两个参数是两个point。re表示rect矩形,后续参数是个rect_like,可以直接画rect。c表示贝泽尔曲线,四个点分别是 起点,控制点1,控制点2,终点。

            Page有相关的绘画api,如画线段的draw_line,画矩形的draw_rect,画曲线的draw_bezier()。注意:目前测试过程中,图片的边框也会被读出为矩形,将矩形画出并填充之后,再向同区域添加图片会被覆盖或失败,即使图片在画完矩形后添加也会有异常,总之就是不显示,使用时需要注意,当然也可以自己尝试后确认。相关代码可以参考:Drawing and Graphics

            自定义绘画的使用介绍告一段落,接下来介绍表格的提取。

      表格的提取有两套经过验证的方案,且各自都经过了验证。

    详细解决方案 优点 缺点
    根据观察表格内容的文本提取结果,以span和line为观察对象,总结一定的规则,不断的对规则进行扩充,对表格进行定义,使定义的规则可以囊括大多数情况。通过不断的迭代,形成一套相对准确的提取方案。 通过简单定义表格规则来提取表格。通过不断迭代来完善规则。开发起来简单,没有额外引入风险。 规则定义不完善,总有表格可以突破定义,造成疏漏。需要对规则大量迭代回归,效率较低。准确率较低。
    通过机器学习进行文档分析。文档分析是以分析图片进行处理,首先将pdf页转成jpg文件,通过分析图片返回表格坐标及相应评分。可以根据评分来判断是否采用此表格分析。 通过已经成熟训练过的机器学习模型进行图片分析,提取表格,准确度高。 环境部署相对困难,有额外引入风险。机器学习相关环境需要gpu,无法在win环境下开发部署

      经过一段时间观察和整理,认为文本在提取过程中不应该以block为单位,部分pdf会将多行文本直接提取到一个block中。这种现象某种程度上来说是好事,但是在对于表格提取上反而是个障碍。最终决定在文本提取时,以block下的lines的span为最小单位。近期的实践过程中没有发现需要以字符为最小单位进行拼接的情况,所以没有必要使用get_text('rawdict')来获取字符。dict的结构如下图:

       将dict重组成最基础的block结构代码如下,相较于上个版本直接进行提取,这次考虑了block中多行的情况,主动对多行现象进行了切分。当判断文本在表格范围内的时候,即放弃整合。in_tables(rect,tables)方法可在项目文件中查看。

    def dicts2blocks(extract_dict, tables=[]):
        blocks = extract_dict['blocks']
        blks = []
        for d in blocks:
            bbox, type, fz, text = d['bbox'], d['type'], 0, ''
            if type == 0:  # 文本
                lines = d['lines']
                blk_lines = []
                for l in lines:
                    t_bbox = l['bbox']
                    if t_bbox[0] < 0 and t_bbox[2] < 0: continue  # 两个横坐标都小于0
                    if in_tables(t_bbox, tables):
                        for s in l['spans']:
                            t_text = s['text']
                            t_fz = s['size']
                            t_bbox = s['bbox']
                            blk_lines.append((t_bbox, t_text, t_fz))
                    else:
                        t_text = ''.join([s['text'] for s in l['spans']])
                        t_fz = max([s['size'] for s in l['spans']])
                        blk_lines.append((t_bbox, t_text, t_fz))
    
                pre_bbox, pre_text, pre_fz = None, '', 0
                for i, bl in enumerate(blk_lines):
                    if i == 0:
                        pre_bbox, pre_text, pre_fz, = bl
                        continue
                    curr_bbox, curr_text, curr_fz, = bl
                    if not in_tables(pre_bbox, tables):
                        if abs(pre_bbox[1] - curr_bbox[1]) <= curr_fz * 0.2 and abs(
                                pre_bbox[3] - curr_bbox[3]) <= curr_fz * 0.2:  # baseline 有轻微高低差不能使用origin_y判断
                            # 同行且字体大小一样
                            if abs(curr_bbox[0] - pre_bbox[2]) <= curr_fz * 5:  # 当前box 在上一个box的右侧4个字符以内 应合并
                                pre_bbox = (min(pre_bbox[0], curr_bbox[0]),
                                            min(pre_bbox[1], curr_bbox[1]),
                                            max(pre_bbox[2], curr_bbox[2]),
                                            max(pre_bbox[3], curr_bbox[3]))
                                pre_text += curr_text
                                pre_fz = max(pre_fz, curr_fz)
                                continue
                    blks.append((pre_bbox[0], pre_bbox[1], pre_bbox[2], pre_bbox[3], strQ2B(pre_text) + '
    ', pre_fz, type))
                    pre_bbox, pre_text, pre_fz = curr_bbox, curr_text, curr_fz
                blks.append((pre_bbox[0], pre_bbox[1], pre_bbox[2], pre_bbox[3], strQ2B(pre_text) + '
    ', pre_fz, type))
            else:  # 非文本
                blks.append((bbox[0], bbox[1], bbox[2], bbox[3], strQ2B(text) + '
    ', fz, type))
        return blks

      整合文本的过程也添加了对表格范围的判断。改动较小不再展示代码。

      下面介绍一下解决方案一,核心就是定义表格规范。从dict中提取出来的span,我们通过什么来认定这些是表格。

      这里直接说结论,通过观察迭代,我们认定符合这样的条件的是表格。

      一.表格分布在一个block中,表头也在block中。此时我们认为表格最极端的形状是3行2列。

        特点:1.一个block中有多行(至少三行),且每行中有多个span,总span的个数至少有6个;

              2.span的最大建议宽度取block的1/3和100的最小值(pymupdf中宽度的单位应该是px,未证实,所以用统一单位来代替,避免歧义),最大建议宽度的设置是为了避免有些相对极端的情况。至于为何取block的1/3,当span宽度超过1/3之后,每行最多有2个span,表格的形状就变成了3行2列最极端的情况,再极端就变成了6行1列,或者2行3列(表头占一行)这些情况,显然不能将这些认为是表格;

      二.表格分布在相邻的多个block中,表头或许不在相邻block中,此时我们认为表格最极端的形状是2行3列。尽管2列也可以,但是2列在处理的时候容易命中错误。

        特点: 1.相邻行的span个数大于3,且span个数相等。

            2.相邻行对应的span宽度应该是接近的,较大宽度是较小宽度的1.6倍,且超过个数不能超过2个。设置宽这样的阈值是避免对应span中的内容出现多个极长对极短的现象。

                              3.同一行中,最大span和最小span的宽度倍率限制为8,但出现次数不能超过2个。

                   关于倍率和次数的限制在后来实践过程中屡次被突破,后来将阈值设置为倍率*次数,计算方法不再单纯以次数来判断,而是以实际倍率加和来判断。例如同一行中最大span是最小span的20倍,超过8倍的仅出现了一次,这种情况下可以将他看作表格吗?大概率是不行的(通过观察获得的结论)。再如,两行文本被识别成span个数一样,最小span内容是一个标点,那最小span的宽度作为倍率的基准是否合适呢?显然是不合适的,因为这两行本质就是文本,不是表格,他的前置条件就错了。

      以上两种情况是可以通过解决方案一来识别出来的,也是常见的大多数情况。下面说一下不能被识别的情况。

      1.表格单元格的内容是多行,且行数不等。这种情况,一个单元格会被识别成一个block,其中的内容通常是多行,每行一个或多个span。表格的单元格之间是平行的关系。观察上面可以解决的情况,这种平行关系是识别不了的。因为不能确定当前block的坐标跟上一个block的坐标是否在一个表格里(可以确定不是同一行,也可以确定不是同一列,但是无法确定不能确定在同一个表格,因为至少需要向前追溯N-1个block,N为列数)。

      2.表格的读取是以单元格为单位,从上到下从左到右。因为pymupdf的提取习惯也是这样,那么判断表格就需要回溯N个block,这个在实现上几乎是不可能的,也是不必要的。

           3.表格提取的内容是以字符为单位,无法通过block或者span来识别表格。

      4.表格是2列的。如果设置列数为2,很多文本会误中副车,导致后期合并文本出现问题。这种情况只能宁纵勿枉,不能错杀。

      讲述了2种常见适用情况和4种少见情况,详见代码的处理如下:

    def gettables(extract_dict):
        tab = []
        blocks = extract_dict['blocks']
        for i, b in enumerate(blocks):
            if i == 0:
                bbox_p = b['bbox']
                spans_p = [s['bbox'][2] - s['bbox'][0] for l in b['lines'] for s in l['spans'] if s['text'].strip()] if b[
                                                                                                                            'type'] == 0 
                    else [bbox_p[2] - bbox_p[0]]
                height_p = bbox_p[3] - bbox_p[1]
                continue
            if b['type'] == 0:
                bbox_c = b['bbox']
                spans_c = [s['bbox'][2] - s['bbox'][0] for l in b['lines'] for s in l['spans'] if s['text'].strip()]
                height_c = bbox_c[3] - bbox_c[1]
                fontsize_b_c = max([s['size'] for l in b['lines'] for s in l['spans']])
                # ------------------将表格放到一个block中-----------------------
                if height_c / fontsize_b_c >= P_line_block and len(spans_c) >= P_spans_block:
                    # 特殊情况 当前block 至少有3行文本 且span 数量大于6,可以按表格处理
                    bbox_p, spans_p, height_p = bbox_c, spans_c, height_c
                    weight_block = bbox_c[2] - bbox_c[0]
                    limit = min(weight_block / 3, P_span_width_block)
                    if sum([int(w / limit) for w in spans_c if w >= limit]) > 2:
                        # 任意一个span的宽度大于 block的33% 不以表格处理
                        # 超过block宽度30%的span应少于3个
                        continue
                    tab.append(bbox_p)
                    continue
                # ------------------表格分散在连续的block中------------------------
                if len(spans_c) == len(spans_p) > P_spans_blocks:
                    # 判断相邻行 对应span宽度比例不能超过1.6且超过个数不超过2
                    tmp = [max(spans_c[i], spans_p[i]) / min(spans_c[i], spans_p[i]) for i in range(len(spans_c))]
                    # 判断当前行 最大span和最小span宽度比例 不能超过8 且个数不能超过2
                    min_span_w = max(min(spans_c), fontsize_b_c)
                    min_ratio_c = [w / min_span_w for w in spans_c if w / min_span_w > P_span_min_width_ratio_blocks[0]]
                    if sum([1 for t in tmp if t >= P_span_width_ratio_blocks[0]]) >= P_span_width_ratio_blocks[-1]:
                        pass
                    elif sum(min_ratio_c) >= P_span_min_width_ratio_blocks[0] * P_span_min_width_ratio_blocks[-1]:
                        pass
                    else:
                        bbox_c = (min(bbox_p[0], bbox_c[0]),
                                  min(bbox_p[1], bbox_c[1]),
                                  max(bbox_p[2], bbox_c[2]),
                                  max(bbox_p[3], bbox_c[3]))
                elif len(spans_p) == 1:
                    pass
                else:
                    if bbox_p[3] - bbox_p[1] >= P_line_blocks * height_p:
                        tab.append(bbox_p)
                bbox_p, spans_p, height_p = bbox_c, spans_c, height_c
        if bbox_p[3] - bbox_p[1] > height_c:
            tab.append(bbox_p)
        return tab

      囿于篇幅,关于通过机器学习来识别表格的方法,下次再整理。

    附图:

      

      上图为不能识别的情况1.

       上图为不能识别的情况4

       上图为相邻行对应span宽度倍率不超过1.6,不能解决的情况。

  • 相关阅读:
    随笔:判断一个范围内有多少质数,分别是多少
    随笔:判断一个整数是否是质数,如果不是质数,那么因数表达式是什么
    随笔:Python发送SMTP邮件方法封装
    Python基础学习:打印九九乘法表
    随笔:docker学习笔记(包括了基础学习和制作运行jar包的docker镜像,还有centos7防火墙这个坑)
    随笔:测试心得
    随笔:docker安装
    Python基础:Python连接MySQL数据库方法封装2
    随笔:Python打印临时日志、清空临时日志
    radio点击一下选中,再点击恢复未选状态
  • 原文地址:https://www.cnblogs.com/cnDqf/p/15099996.html
Copyright © 2011-2022 走看看