用於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) + '\n', 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) + '\n', pre_fz, type))
        else:  # 非文本
            blks.append((bbox[0], bbox[1], bbox[2], bbox[3], strQ2B(text) + '\n', 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,不能解決的情況。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章