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/
註冊登錄,進入後可以創建知識庫,然後上傳文檔。
上傳成功後,可以通過解析狀態查看解析進度,也可以配置文檔的parser解析方法,以更好的解析內容。
點擊文檔名稱,可以進入文檔詳情,查看拆分的chunk,可以看到普通的文本是按照token拆分,還未實現按照段落語義拆分,差評。表格是單獨抽取出來,獨立存儲的,將文檔裏的表格比較好的還原爲了html表格,準確率尚可,這裏好評。每個chunk有原文截圖,點擊後,右邊的pdf預覽,可以高亮當前的chunk所在區域,翻了下代碼,使用的react-pdf-highlighter
,體驗挺好的一個組件。
DeepDoc 介紹
DeepDoc 是 RAGFlow 的核心組件,它利用視覺信息和解析技術,對文檔進行深度理解,提取文本、表格和圖像等信息。DeepDoc 的功能模塊包括:
-
OCR, 支持將圖片、PDF識別爲文本。
-
版面識別,識別文檔的標題、段落、表格、圖像等。
-
表格結構識別 (TSR),識別的行、列,以及合併的單元格。
-
支持多類型文檔解析,比如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.py和layout_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()
加載了上面說的OCR
、LayoutRecognizer
,以及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
- 這裏的條件,高度差異小於整體高度的0.3 (
- 遍歷文本框列表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文檔解析的光明大道。