深度解讀RAGFlow的深度文檔理解DeepDoc

4 月 1 日,Infinity宣佈端到端 RAG 解決方案 RAGFlow 開源,僅一天收穫上千顆星,到底有何魅力? 我們來安裝體驗並從代碼層面來分析看看。

安裝體驗

服務器需要有docker,或者直接訪問官方提供的demo: https://demo.ragflow.io/

docker-compose安裝

  • 需要確保 vm.max_map_count 不小於 262144 【更多】:
sysctl -w vm.max_map_count=262144  
  • 克隆倉庫:
   $ git clone https://github.com/infiniflow/ragflow.git   
  • 進入 docker 文件夾,利用提前編譯好的 Docker 鏡像啓動服務器:
   $ cd ragflow/docker   $ docker compose -f docker-compose-CN.yml up -d  

核心鏡像文件大約 15 GB,可能需要一定時間拉取。請耐心等待。

體驗

啓動成功後,瀏覽器輸入 http://服務器ip 或者直接訪問官方demo https://demo.ragflow.io/

註冊登錄,進入後可以創建知識庫,然後上傳文檔。

image.png

上傳成功後,可以通過解析狀態查看解析進度,也可以配置文檔的parser解析方法,以更好的解析內容。

點擊文檔名稱,可以進入文檔詳情,查看拆分的chunk,可以看到普通的文本是按照token拆分,還未實現按照段落語義拆分,差評。表格是單獨抽取出來,獨立存儲的,將文檔裏的表格比較好的還原爲了html表格,準確率尚可,這裏好評。每個chunk有原文截圖,點擊後,右邊的pdf預覽,可以高亮當前的chunk所在區域,翻了下代碼,使用的react-pdf-highlighter,體驗挺好的一個組件。

image.png

DeepDoc 介紹

DeepDoc 是 RAGFlow 的核心組件,它利用視覺信息和解析技術,對文檔進行深度理解,提取文本、表格和圖像等信息。DeepDoc 的功能模塊包括:

  • OCR, 支持將圖片、PDF識別爲文本。
    image.png

  • 版面識別,識別文檔的標題、段落、表格、圖像等。

image.png

  • 表格結構識別 (TSR),識別的行、列,以及合併的單元格。
    image.png

  • 支持多類型文檔解析,比如PDF、DOCX、EXCEL 和 PPT,甚至圖片 ,並提取文本塊、表格和圖像等信息。

DeepDoc CV模型

DeepDoc的模型應該是基於paddleOCR的模型去微調訓練的,開源出來的模型是onnx格式的。

OCR識別

主要代碼在ocr.py裏,代碼定義TextRecognizer 做文字識別,TextDetector 做文本框檢測,OCR整合檢測和識別功能,對外提供調用。

OCR的核心流程:

  • 創建 OCR 實例,load模型
  • 調用 __call__ 方法,傳入圖像數據。
    • 使用 TextDetector 進行文本檢測,獲取文本框座標
    • 對每個文本框,使用 get_rotate_crop_image 方法進行旋轉和裁剪
    • 使用 TextRecognizer 對裁剪後的圖像進行文本識別
    • 過濾掉置信度低於閾值(0.5)的識別結果。
  • 返回最終的文本框座標和識別結果。

版面分析

版面分析主要在recognizer.pylayout_recognizer.py裏,定義了一個名爲LayoutRecognizer 繼承Recognizer的類,用於對文檔圖像進行板式分析,識別不同類型的區域,例如表格、標題、段落等。這裏用的模型應該還是基於paddleocr裏的版面分析模型去優化的。

先看Recognizer__call__ 方法,傳入圖像列表和置信度閾值:

def __call__(self, image_list, thr=0.7, batch_size=16):  
    res = []  
    imgs = []  
    for i in range(len(image_list)):  
        if not isinstance(image_list[i], np.ndarray):  
            imgs.append(np.array(image_list[i]))  
        else: imgs.append(image_list[i])  
  
    batch_loop_cnt = math.ceil(float(len(imgs)) / batch_size)  
    for i in range(batch_loop_cnt):  
        start_index = i * batch_size  
        end_index = min((i + 1) * batch_size, len(imgs))  
        batch_image_list = imgs[start_index:end_index]  
        inputs = self.preprocess(batch_image_list)  
        print("preprocess")  
        for ins in inputs:  
            bb = self.postprocess(self.ort_sess.run(None, {k:v for k,v in ins.items() if k in self.input_names})[0], ins, thr)  
            res.append(bb)  
  
    #seeit.save_results(image_list, res, self.label_list, threshold=thr)  
  
    return res
  • 先預處理,將圖像列表轉換爲模型輸入格式
  • 然後調用ort_sess執行onnx推理,最後postprocess,提取模型返回的佈局信息,包括區域類型、座標和置信度。

再看LayoutRecognizer__call__ 方法,這裏是模型應用的工程代碼部分,很多細節的小技巧,先上代碼,裏面加了一些註釋:

def __call__(self, image_list, ocr_res, scale_factor=3,  
             thr=0.2, batch_size=16, drop=True):  
    # 可以過濾的垃圾數據  
    def __is_garbage(b):  
        patt = [r"^•+$", r"(版權歸©|免責條款|地址[::])", r"\.{3,}", "^[0-9]{1,2} / ?[0-9]{1,2}$",  
                r"^[0-9]{1,2} of [0-9]{1,2}$", "^http://[^ ]{12,}",  
                "(資料|數據)來源[::]", "[0-9a-z._-]+@[a-z0-9-]+\\.[a-z]{2,3}",  
                "\\(cid *: *[0-9]+ *\\)"  
                ]  
        return any([re.search(p, b["text"]) for p in patt])  
    # 調用父類的模型識別  
    layouts = super().__call__(image_list, thr, batch_size)  
    # save_results(image_list, layouts, self.labels, output_dir='output/', threshold=0.7)  
    assert len(image_list) == len(ocr_res)  
    # Tag layout type  
    boxes = []  
    assert len(image_list) == len(layouts)  
    garbages = {}  
    page_layout = []  
    for pn, lts in enumerate(layouts):  
        # OCR 識別的box文本框  
        bxs = ocr_res[pn]  
        # layout轉換爲box形式  
        lts = [{"type": b["type"],  
                "score": float(b["score"]),  
                "x0": b["bbox"][0] / scale_factor, "x1": b["bbox"][2] / scale_factor,  
                "top": b["bbox"][1] / scale_factor, "bottom": b["bbox"][-1] / scale_factor,  
                "page_number": pn,  
                } for b in lts]  
        # 按照(top,x0)排序  
        lts = self.sort_Y_firstly(lts, np.mean(  
            [l["bottom"] - l["top"] for l in lts]) / 2)  
        # 清理重疊的layout  
        lts = self.layouts_cleanup(bxs, lts)  
        page_layout.append(lts)  
  
        # Tag layout type, layouts are ready  
        # 這裏其實是爲文本框box分配layout,find不是特別準確  
        def findLayout(ty):  
            nonlocal bxs, lts, self  
            # 找對應了下的layout type  
            lts_ = [lt for lt in lts if lt["type"] == ty]  
            i = 0  
            # 爲 ocr detect的box標記 layout_type            while i < len(bxs):  
                # 已標記,跳過  
                if bxs[i].get("layout_type"):  
                    i += 1  
                    continue  
                # 垃圾信息,刪除掉  
                if __is_garbage(bxs[i]):  
                    bxs.pop(i)  
                    continue  
  
                # 尋找與box重疊的layout  
                ii = self.find_overlapped_with_threashold(bxs[i], lts_, thr=0.4)  
                # 未找到  
                if ii is None:  # belong to nothing  
                    bxs[i]["layout_type"] = ""  
                    i += 1  
                    continue  
                lts_[ii]["visited"] = True  
                # 保留特徵,爲header或者footer,且在內容區域的邊界內(這裏定義了0.1,0.9)  
                keep_feats = [  
                    lts_[ii]["type"] == "footer" and bxs[i]["bottom"] < image_list[pn].size[1] * 0.9 / scale_factor,  
                    lts_[ii]["type"] == "header" and bxs[i]["top"] > image_list[pn].size[1] * 0.1 / scale_factor,  
                ]  
                # 滿足丟棄條件,刪除box,文本放入garbages  
                if drop and lts_[ii]["type"] in self.garbage_layouts and not any(keep_feats):  
                    if lts_[ii]["type"] not in garbages:  
                        garbages[lts_[ii]["type"]] = []  
                    garbages[lts_[ii]["type"]].append(bxs[i]["text"])  
                    bxs.pop(i)  
                    continue  
  
                # 符合要求的box,分配layout  
                bxs[i]["layoutno"] = f"{ty}-{ii}"  
                bxs[i]["layout_type"] = lts_[ii]["type"] if lts_[  
                    ii]["type"] != "equation" else "figure"  
                i += 1  
  
        # 遍歷layout類型,爲文本框分配layout,之所以分開,是因爲一個文本框可能和多個layout重疊,這裏是減少衝突  
        for lt in ["footer", "header", "reference", "figure caption",  
                   "table caption", "title", "table", "text", "figure", "equation"]:  
            findLayout(lt)  
  
        # add box to figure layouts which has not text box  
        # 將沒有文本框的figure添加到boxes中,並更新ocr_res  
        for i, lt in enumerate(  
                [lt for lt in lts if lt["type"] in ["figure", "equation"]]):  
            # 有文本框重疊的圖片,visited已經設置過  
            if lt.get("visited"):  
                continue  
            lt = deepcopy(lt)  
            del lt["type"]  
            lt["text"] = ""  
            lt["layout_type"] = "figure"  
            lt["layoutno"] = f"figure-{i}"  
            bxs.append(lt)  
  
        boxes.extend(bxs)  
  
    # 更新ocr_res  
    ocr_res = boxes  
  
    garbag_set = set()  
    for k in garbages.keys():  
        garbages[k] = Counter(garbages[k])  
        for g, c in garbages[k].items():  
            if c > 1:  
                garbag_set.add(g)  
  
    ocr_res = [b for b in ocr_res if b["text"].strip() not in garbag_set]  
    return ocr_res, page_layout

大概解釋下:

  • __call__方法,它接收以下參數:image_list(圖像列表),ocr_res(OCR識別的文本框),scale_factor(縮放因子,默認值爲3),thr(閾值,默認值爲0.2),batch_size(批處理大小,默認值爲16),drop(是否刪除,默認值爲True)
  • 首先調用父類的call方法,將圖片交給PP Structure模型識別出layouts,並清理重疊的layout(layouts_cleanup)
  • 然後就是爲文本框box分配layout,根據layout type,從layout裏找出對應type的layout,如果和box有重疊大於閾值,就爲box分配layout,不滿足條件的box會被丟棄,比如包含垃圾文本(__is_garbage
  • 接着對於沒有文本框的figure、equation 添加到boxes中,並更新ocr_res
  • 最後返回更新後的ocr_res,以及page_layout信息

DeepDoc 的parser功能

上面的OCR版面分析,都是爲parser服務的,parser負責解析文檔,並拆分爲chunk.

框架提供了PdfParser、PlainParser、DocxParser、ExcelParser、PptParser 5種解析器。

from .pdf_parser import HuParser as PdfParser, PlainParser  
from .docx_parser import HuDocxParser as DocxParser  
from .excel_parser import HuExcelParser as ExcelParser  
from .ppt_parser import HuPptParser as PptParser

另外針對resume,提供了專門的簡歷解析功能。

我們挑選重點的PdfParser 也就是HuParser來分析。

PdfParser

首先,初始化:

def __init__(self):  
    self.ocr = OCR()  
    if hasattr(self, "model_speciess"):  
        self.layouter = LayoutRecognizer("layout." + self.model_speciess)  
    else:  
        self.layouter = LayoutRecognizer("layout")  
    self.tbl_det = TableStructureRecognizer()  
    self.updown_cnt_mdl = xgb.Booster()

加載了上面說的OCRLayoutRecognizer,以及TableStructureRecognizer用於表格結構識別,updown_cnt_mdl,一個xgb模型用來合併box。全靠模型很能做到滿意的效果,所以一般都是模型搭配大量的工程trick,靠一些規則來解決一些邊界情況。文檔解析也是這樣,需要多個模型配合,結合一些規則來做,這些規則通常是經驗的集合,大白話就是各種case跑出來,遇到問題就加新的規則,都是淚。

不發散,我們來看PdfParser核心的__call__:

def __call__(self, fnm, need_image=True, zoomin=3, return_html=False):  
    # 轉圖片,處理文本,ocr識別  
    self.__images__(fnm, zoomin)  
    # 版面分析  
    self._layouts_rec(zoomin)  
    # table box 處理  
    self._table_transformer_job(zoomin)  
    # 合併文本塊  
    self._text_merge()  
    self._concat_downward()  
    # 過濾分頁信息  
    self._filter_forpages()  
    # 表格和圖表抽取  
    tbls = self._extract_table_figure(  
        need_image, zoomin, return_html, False)  
    # 抽取的文本(去掉表格), 表格  
    return self.__filterout_scraps(deepcopy(self.boxes), zoomin), tbls
  • 首先__images__實現pdf轉圖片,讀取pdf裏的文本,並用ocr識別文本塊等
  • 然後進行版面識別
  • 將識別到的table做處理
  • 合併文本塊
  • _concat_downward 使用 updown_cnt_mdl模型來做合併
  • _filter_forpages 過濾pdf裏的分頁信息
  • _extract_table_figure 抽取頁面裏的表格和圖片,表格會轉換爲html
  • __filterout_scraps 合併文本塊(去掉表格後的)
  • 最後返回合併後的文本和表格

這裏的每一步都較爲複雜,我們挑重點的來說。

pdf轉圖片

這裏代碼較多,大概幾件事情,分開來講:

pdf文件讀取

def __images__(self, fnm, zoomin=3, page_from=0,  
               page_to=299, callback=None):  
    self.lefted_chars = []  
    self.mean_height = []  
    self.mean_width = []  
    self.boxes = []  
    self.garbages = {}  
    self.page_cum_height = [0]  
    self.page_layout = []  
    self.page_from = page_from  
    try:  
        self.pdf = pdfplumber.open(fnm) if isinstance(  
            fnm, str) else pdfplumber.open(BytesIO(fnm))  
        self.page_images = [p.to_image(resolution=72 * zoomin).annotated for i, p in  
                            enumerate(self.pdf.pages[page_from:page_to])]  
        self.page_chars = [[c for c in page.chars if self._has_color(c)] for page in  
                           self.pdf.pages[page_from:page_to]]  
        self.total_page = len(self.pdf.pages)  
    except Exception as e:  
        self.pdf = fitz.open(fnm) if isinstance(  
            fnm, str) else fitz.open(  
            stream=fnm, filetype="pdf")  
        self.page_images = []  
        self.page_chars = []  
        mat = fitz.Matrix(zoomin, zoomin)  
        self.total_page = len(self.pdf)  
        for i, page in enumerate(self.pdf):  
            if i < page_from:  
                continue  
            if i >= page_to:  
                break  
            pix = page.get_pixmap(matrix=mat)  
            img = Image.frombytes("RGB", [pix.width, pix.height],  
                                  pix.samples)  
            self.page_images.append(img)  
            self.page_chars.append([])
  • 首先初始化一些變量,如lefted_chars、mean_height、mean_width、boxes、garbages等。
  • 然後,首先嚐試使用pdfplumber庫打開PDF文件,並獲取指定範圍頁面的文本和圖像, pdfplumber 是一個出名的python解析pdf的庫,可以較好的提取文本、矩形、圖片等,可以返回每個char字符的座標、大小等信息。
  • 如果發生異常,將嘗試使用fitz庫作爲替代方案,fitz的話就讀取不到文本了,會當成圖像來處理。

pdf目錄讀取

這裏使用了PyPDF2庫來讀取pdf的目錄信息,但是貌似是基本的讀取,其實pdf的目錄可以關聯到具體的章節內容,這裏暫時看起來沒有很好的利用。

self.outlines = []  
try:  
    self.pdf = pdf2_read(fnm if isinstance(fnm, str) else BytesIO(fnm))  
    outlines = self.pdf.outline  
  
    def dfs(arr, depth):  
        for a in arr:  
            if isinstance(a, dict):  
                self.outlines.append((a["/Title"], depth))  
                continue  
            dfs(a, depth + 1)  
    dfs(outlines, 0)  
except Exception as e:  
    logging.warning(f"Outlines exception: {e}")  
if not self.outlines:  
    logging.warning(f"Miss outlines")

然後是英文文檔檢測,大概就是利用正則匹配。

logging.info("Images converted.")  
self.is_english = [re.search(r"[a-zA-Z0-9,/¸;:'\[\]\(\)!@#$%^&*\"?<>._-]{30,}", "".join(  
    random.choices([c["text"] for c in self.page_chars[i]], k=min(100, len(self.page_chars[i]))))) for i in  
    range(len(self.page_chars))]  
if sum([1 if e else 0 for e in self.is_english]) > len(  
        self.page_images) / 2:  
    self.is_english = True  
else:  
    self.is_english = False

分頁處理

這裏是核心的代碼:

  • 會對文本做處理,適當的添加空格
  • 對每頁進行__ocr處理
  • 更新解析進度
  
for i, img in enumerate(self.page_images):  
    chars = self.page_chars[i] if not self.is_english else []  
    # 計算字符的平均寬度、高度  
    self.mean_height.append(  
        np.median(sorted([c["height"] for c in chars])) if chars else 0  
    )  
    self.mean_width.append(  
        np.median(sorted([c["width"] for c in chars])) if chars else 8  
    )  
    self.page_cum_height.append(img.size[1] / zoomin)  
    j = 0  
    while j + 1 < len(chars):  
        # 對滿足條件的添加空格(只包含數字、字母、逗號、句號、冒號、分號、感嘆號和百分號, 兩個字符寬度小於width的一半  
        if chars[j]["text"] and chars[j + 1]["text"] \  
                and re.match(r"[0-9a-zA-Z,.:;!%]+", chars[j]["text"] + chars[j + 1]["text"]) \  
                and chars[j + 1]["x0"] - chars[j]["x1"] >= min(chars[j + 1]["width"],  
                                                               chars[j]["width"]) / 2:  
            chars[j]["text"] += " "  
        j += 1  
    # if i > 0:  
    #     if not chars:    #         self.page_cum_height.append(img.size[1] / zoomin)    #     else:    #         self.page_cum_height.append(    #             np.max([c["bottom"] for c in chars]))    # OCR 識別  
    self.__ocr(i + 1, img, chars, zoomin)  
    if callback:  
        callback(prog=(i + 1) * 0.6 / len(self.page_images), msg="")

callback方法會更新文檔解析進度,在文檔頁面可以查看實時進度

__ocr 處理

雖然叫OCR,但是主要做的是detect,檢測文本框,然後根據經驗規則來對文本塊做處理:

def __ocr(self, pagenum, img, chars, ZM=3):  
    # 檢測文本框  
    bxs = self.ocr.detect(np.array(img))  
    if not bxs:  
        self.boxes.append([])  
        return  
    bxs = [(line[0], line[1][0]) for line in bxs]  
    # 按照Y軸座標排序  
    bxs = Recognizer.sort_Y_firstly(  
        [{"x0": b[0][0] / ZM, "x1": b[1][0] / ZM,  
          "top": b[0][1] / ZM, "text": "", "txt": t,  
          "bottom": b[-1][1] / ZM,  
          "page_number": pagenum} for b, t in bxs if b[0][0] <= b[1][0] and b[0][1] <= b[-1][1]],  
        self.mean_height[-1] / 3  
    )  
  
    # merge chars in the same rect  
    for c in Recognizer.sort_X_firstly(  
            chars, self.mean_width[pagenum - 1] // 4):  
        ii = Recognizer.find_overlapped(c, bxs)  
        if ii is None:  
            self.lefted_chars.append(c)  
            continue  
        ch = c["bottom"] - c["top"]  
        bh = bxs[ii]["bottom"] - bxs[ii]["top"]  
        if abs(ch - bh) / max(ch, bh) >= 0.7 and c["text"] != ' ':  
            self.lefted_chars.append(c)  
            continue  
        if c["text"] == " " and bxs[ii]["text"]:  
            if re.match(r"[0-9a-zA-Z,.?;:!%%]", bxs[ii]["text"][-1]):  
                bxs[ii]["text"] += " "  
        else:  
            bxs[ii]["text"] += c["text"]  
  
    for b in bxs:  
        if not b["text"]:  
            left, right, top, bott = b["x0"] * ZM, b["x1"] * \  
                ZM, b["top"] * ZM, b["bottom"] * ZM  
            b["text"] = self.ocr.recognize(np.array(img),  
                                           np.array([[left, top], [right, top], [right, bott], [left, bott]],  
                                                    dtype=np.float32))  
        del b["txt"]  
    bxs = [b for b in bxs if b["text"]]  
    if self.mean_height[-1] == 0:  
        self.mean_height[-1] = np.median([b["bottom"] - b["top"]  
                                          for b in bxs])  
    self.boxes.append(bxs)
  • 首先使用self.ocr.detect方法檢測圖像中的文本框,並將結果存儲在bxs變量中。如果沒有檢測到文本框,將空列表添加到self.boxes中並返回
  • 對檢測到的文本框按照Y軸座標進行排序
  • 遍歷pdf提取到的文本chars,通過find_overlapped檢測與字符char重疊的文本框,符合條件的char放入文本框:
    • 這裏的條件,高度差異小於整體高度的0.3 (abs(ch - bh) / max(ch, bh) >= 0.7)
    • 否則就放入lefted_chars
  • 遍歷文本框列表bxs,對於沒有文本的文本框,嘗試用ocr的recognize去識別文本,這裏就做到了,能用原始文本的(準確)就用原始文本,原始是圖片的,嘗試用OCR去識別
  • 最後將包含文本的文本框添加到self.boxes中,並更新self.mean_height

版面識別

_layouts_rec:

  
def _layouts_rec(self, ZM, drop=True):  
    assert len(self.page_images) == len(self.boxes)  
    self.boxes, self.page_layout = self.layouter(  
        self.page_images, self.boxes, ZM, drop=drop)  
    # cumlative Y  
    for i in range(len(self.boxes)):  
        self.boxes[i]["top"] += \  
            self.page_cum_height[self.boxes[i]["page_number"] - 1]  
        self.boxes[i]["bottom"] += \  
            self.page_cum_height[self.boxes[i]["page_number"] - 1]
  • 調用self.layouter方法來獲取新的self.boxes和self.page_layout,layouter 就是上面說的版面分析,這裏會傳入page_images圖片,以及ocr處理後的文本box,layouter執行後,會返回分配layout後的文本框boxes,同時清理掉一些無用文本框
  • 然後更新box的top信息,加上pag_cum_height頁面高度

表格處理 _table_transformer_job

這裏會遍歷page_layout,得到每一頁的layout,從layout中找到表格,並調用模型識別後再根據規則做處理。

DocxParser word文檔解析

word文檔比pdf解析更容易,直接看__call__:

def __call__(self, fnm, from_page=0, to_page=100000):  
    self.doc = Document(fnm) if isinstance(  
        fnm, str) else Document(BytesIO(fnm))  
    pn = 0  
    secs = []  
    for p in self.doc.paragraphs:  
        if pn > to_page:  
            break  
        if from_page <= pn < to_page and p.text.strip():  
            secs.append((p.text, p.style.name))  
        for run in p.runs:  
            if 'lastRenderedPageBreak' in run._element.xml:  
                pn += 1  
                continue  
            if 'w:br' in run._element.xml and 'type="page"' in run._element.xml:  
                pn += 1  
  
    tbls = [self.__extract_table_content(tb) for tb in self.doc.tables]  
    return secs, tbls
  • 通過docx庫加載word文檔
  • 讓後讀取指定頁面的paragraphs
  • 通過__extract_table_content解析表格

word裏的表格,不需要模型來識別了,可以直接讀取:

def __extract_table_content(self, tb):  
    df = []  
    for row in tb.rows:  
        df.append([c.text for c in row.cells])  
    return self.__compose_table_content(pd.DataFrame(df))  
  
def __compose_table_content(self, df):  
  
    def blockType(b):  
        patt = [  
            ("^(20|19)[0-9]{2}[年/-][0-9]{1,2}[月/-][0-9]{1,2}日*$", "Dt"),  
            (r"^(20|19)[0-9]{2}年$", "Dt"),  
            (r"^(20|19)[0-9]{2}[年/-][0-9]{1,2}月*$", "Dt"),  
            ("^[0-9]{1,2}[月/-][0-9]{1,2}日*$", "Dt"),  
            (r"^第*[一二三四1-4]季度$", "Dt"),  
            (r"^(20|19)[0-9]{2}年*[一二三四1-4]季度$", "Dt"),  
            (r"^(20|19)[0-9]{2}[ABCDE]$", "DT"),  
            ("^[0-9.,+%/ -]+$", "Nu"),  
            (r"^[0-9A-Z/\._~-]+$", "Ca"),  
            (r"^[A-Z]*[a-z' -]+$", "En"),  
            (r"^[0-9.,+-]+[0-9A-Za-z/$¥%<>()()' -]+$", "NE"),  
            (r"^.{1}$", "Sg")  
        ]  
        for p, n in patt:  
            if re.search(p, b):  
                return n  
        tks = [t for t in huqie.qie(b).split(" ") if len(t) > 1]  
        if len(tks) > 3:  
            if len(tks) < 12:  
                return "Tx"  
            else:  
                return "Lx"  
  
        if len(tks) == 1 and huqie.tag(tks[0]) == "nr":  
            return "Nr"  
  
        return "Ot"  
  
    if len(df) < 2:  
        return []  
    max_type = Counter([blockType(str(df.iloc[i, j])) for i in range(  
        1, len(df)) for j in range(len(df.iloc[i, :]))])  
    max_type = max(max_type.items(), key=lambda x: x[1])[0]  
  
    colnm = len(df.iloc[0, :])  
    hdrows = [0]  # header is not nessesarily appear in the first line  
    if max_type == "Nu":  
        for r in range(1, len(df)):  
            tys = Counter([blockType(str(df.iloc[r, j]))  
                          for j in range(len(df.iloc[r, :]))])  
            tys = max(tys.items(), key=lambda x: x[1])[0]  
            if tys != max_type:  
                hdrows.append(r)  
  
    lines = []  
    for i in range(1, len(df)):  
        if i in hdrows:  
            continue  
        hr = [r - i for r in hdrows]  
        hr = [r for r in hr if r < 0]  
        t = len(hr) - 1  
        while t > 0:  
            if hr[t] - hr[t - 1] > 1:  
                hr = hr[t:]  
                break  
            t -= 1  
        headers = []  
        for j in range(len(df.iloc[i, :])):  
            t = []  
            for h in hr:  
                x = str(df.iloc[i + h, j]).strip()  
                if x in t:  
                    continue  
                t.append(x)  
            t = ",".join(t)  
            if t:  
                t += ": "  
            headers.append(t)  
        cells = []  
        for j in range(len(df.iloc[i, :])):  
            if not str(df.iloc[i, j]):  
                continue  
            cells.append(headers[j] + str(df.iloc[i, j]))  
        lines.append(";".join(cells))  
  
    if colnm > 3:  
        return lines  
    return ["\n".join(lines)]
  • __extract_table_content函數接收一個表格對象(tb)作爲輸入,然後遍歷表格的每一行,將每一行的單元格內容添加到一個列表(df)中
  • 然後 __compose_table_content 抽取表格內容,沒仔細研究,大意是根據單元格的數據類型來判斷列的類型,最後講單元格拼接爲字符串

總結

這裏囫圇吐糟的review了下相關代碼,可以看到RAGFlow在工程方面做了較多的工作,和微調的模型結合產生了良好的化學反應,通過一些工程的優化解決模型的badcase,最終做出了體驗較好的產品,這是RAG文檔解析的光明大道。

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