iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0
生成式 AI

阿,又是一個RAG系列 第 7

Day6: pdf2txt 使用 llama-parse 與 mistral-ocr

  • 分享至 

  • xImage
  •  

Situation:

  • 我們想要替RAG系統獲得成對的 (context, question, answer) 供後續方法論的驗證
  • 在 day1 ~ day5 已經完成了:主題 1.1: 從 context 生成 (question, answer, context) pair
    • 主要的思路是從 youtube 下載影片字幕 -> 切成context -> 生成(question, answer)
  • 今天開始主題 1.2: 從 question 生成 (question, answer, context) pair
    主要思路分成:
    • 把從考選部公告的單選題考題(pdf)當成問題
    • call 可以 web_search 的 llm 來獲得 (context, answer)

Task:

  • 今天主要探索從 pdf 轉 txt,的問題與方法,有良好結構的 txt 之後要轉成想要的 json 就相對容易
    • 阿你就prompt chatgpt 做

Action

  • 我們今天會用到的範例資料如下,如果網址壞了都還是可以在考選部的網站上找到:
    • 108年第一次專門職業及技術人員高等考試中醫師考試分階段考試、營養師、心理師、護理師、社會工作師考試
    • 其他還有:
      • 104-2_內外科護理學
        • 這個的問題在於解析的時候,英文、數字的位置會錯置
      • 110-1_基礎醫學
        • 這個的問題在於化學式在解析的時候整個位置會跑掉
      • 問題我們邊實作邊看
  1. 資料夾格式:
    我假設路徑下有一個 data 資料夾, data 下包含了今天會用到的 pdf 檔案
import os

SOURCE_DIR = 'data'
print(os.listdir(SOURCE_DIR))

['108-1_精神科與社區衛生護理.pdf', '110-1_基礎醫學.pdf', '104-2_內外科護理學.pdf']

  1. 用 fitz(PyMuPDF) 以及 pdfplumber 把 pdf 轉成 txt 檔案
import fitz  # pip install PyMuPDF
import pdfplumber  # pip install pdfplumber
import re

DEST_DIR = 'parsed_txt'
os.makedirs(DEST_DIR, exist_ok=True)

SYMBOL_MAPPING = {
    "": "A. ", "": "B. ", "": "C. ", "": "D. "
}

pattern = re.compile("|".join(map(re.escape, SYMBOL_MAPPING.keys())))

def replace_symbol(match):
    return SYMBOL_MAPPING[match.group(0)]

def pdf2txt_fitz(source_file_path):
    text = []
    with fitz.open(source_file_path) as doc:
        for page in doc:
            page_txt = page.get_text("text")
            replace_txt = pattern.sub(replace_symbol, page_txt)
            text.append(replace_txt)  # 取得純文字
    return "\n\n".join(text)

def pdf2txt_pdfplumber(source_file_path):
    text = []
    with pdfplumber.open(source_file_path) as doc:
        for page in doc.pages:
            page_txt = page.extract_text()
            replace_txt = pattern.sub(replace_symbol, page_txt)
            text.append(replace_txt)  # 取得純文字
    return "\n\n".join(text)

def txt_dump(file_path, data):
    print("write result to: " + file_path)
    with open(file_path, 'w') as f:
        f.write(data)

file_names = os.listdir(SOURCE_DIR)

for file_name in file_names:
    file_path = os.path.join(SOURCE_DIR, file_name)
    base_name, _ext = os.path.splitext(os.path.basename(file_path))
    txt_fitz = pdf2txt_fitz(file_path)
    txt_pdfplumber = pdf2txt_pdfplumber(file_path)
    txt_dump(os.path.join(DEST_DIR, f"{base_name}_fitz.txt"), txt_fitz)
    txt_dump(os.path.join(DEST_DIR, f"{base_name}_pdfplumber.txt"), txt_pdfplumber)
  • pdfplumber 跟 fitz 都是一般 parser pdf 常見的 python 套件,實際上也可以解決大部分的問題
    • 只是我們今天主要關注在一些特殊的 case
  • 我們這邊主要就是 分別用兩個套件做了 讀取pdf -> 轉成 txt 檔案的函數
    • 因為我們相信這個 txt 轉的好,後面用 re parse 成 json 檔案就是容易的事情
  • 然後直接把他們寫到 txt 檔案
  1. 問題描述
  • 首先是 108-1_精神科與社區衛生護理.txt,完全會是空白的檔案,因為它其實裡面完全是圖檔
  • 再來 104-2_內外科護理學.txt,特別關注第 19 題的情況
    原始的 pdf 長這樣
    https://ithelp.ithome.com.tw/upload/images/20250921/20177855ptkUI27Ro3.jpg
    但 parser 出來的 txt 在對應的部分長這樣
19 
腔隙症候群所要觀察的6P,不包括下列那一項? 
Petechiae
A. 
 
Paralysis
B. 
 
Paresthesia
C. 
 
Pulselessness
D. 
 
20 
下列何者不屬於皮膚牽引? 
A. 勃克氏牽引(Buck’s traction) 
B. 環圈式顱骨牽引(Halo traction) 
C. 骨盆牽引(Pelvic traction) 
D. 勒塞爾氏牽引(Russell’s traction)
  • 第 19 題的選項與選項內容的順序是反過來的,但第 20 題又一切正常,這個會非常不利於我們後面的 parser

  • 再看一個例子是 110-1 基礎醫學.txt
    原始的 pdf 長這樣
    https://ithelp.ithome.com.tw/upload/images/20250921/20177855E0iski4mbO.jpg
    但 parser 出來的 txt 在對應的部分如下:

20
二氧化碳在血液中運送的各種形式,其中比例最高的形式是下列何者?
A. 氣態二氧化碳
B. 溶於血漿中之二氧化碳
C. 碳醯胺基血紅素(carbaminohemoglobin)
D. 碳酸氫根離子(
_
3
HCO )
21
下列何者在小腸液中的含量最低?
A. 蔗糖酶(sucrase)
B. 肝醣酶(glycogenase)
C. 乳糖酶(lactase)
D. 麥芽糖酶(maltase)
  • 這次是 20 題化學式的順序錯亂了
  • 關於這種問題,chatgpt的說法如下:

這是典型的「PDF 不是文件、是畫布」問題。簡單說:PDF 只保證畫面上長得像印出來的樣子,但不保證文字的邏輯順序。於是像「2°A-V block」這種有上標/符號/連字的組合,常被拆成多個獨立的繪圖指令,抽取時順序就亂了,最後像你看到的那樣把「°、2、°、3」丟到段落尾端。

  1. 配置 LlamaParse 與 mistral-ocr
  • 首先要去 LlamaCloud 註冊一個 API Key
    • 放在專案路徑下的 .env 裡面: LLAMA_CLOUD_API_KEY=xxxxx
  • 還有去 mistral 也註冊一個 API Key
    • 放在專案路徑下的 .env: MISTRAL_API_KEY=xxx
  • 它們都是有免費額度的,而且我們這樣測試的用量都只有用到免費額度的一點點
  • LlamaParse 主要是在用 LlamaIndex 的時候被他的廣告燒到所以今天來測測看效果
    • LlamaParse 是 LlamaIndex 的
  • mistral-ocr 這個本身名氣就很大
  • 安裝的話:
pip install mistralai
pip install llama-index
pip install llama-parse
  1. 用 LlamaParse 來解決 上述的幾個問題
from dotenv import find_dotenv, load_dotenv
_ = load_dotenv(find_dotenv())

from llama_parse import LlamaParse

parser = LlamaParse(
   # api_key="llx-...",  # if you did not create an environmental variable you can set the API key here
   result_type="text",  # "markdown" and "text" are available
   language = 'ch_tra',
   )

file_name = 'data/104-2_內外科護理學.pdf'
extra_info = {"file_name": file_name}

with open(f"./{file_name}", "rb") as f:
   # must provide extra_info with file_name key with passing file object
   documents = parser.load_data(f, extra_info=extra_info)

with open("parsed_txt/104-2_內外科護理學_llama-parse.txt", "w", encoding="utf-8") as f:
   for doc in documents:
       f.write(doc.text)
  • api key 我們從 .env 讀取
  • 結果可以設成是 markdown 不過我們這邊用 txt
  • 語言可以設 繁體中文(ch_tra),這個在 pdf 都是圖片的時候變得很重要
  • 他 parsed 的結果會是 list of llama-index 的 document
  • 然後我們把這個當成單獨 pdf parser 的工具,就直接把結果寫出去成 txt
  1. parser 結果:

化學式的部分:

20     二氧化碳在血液中運送的各種形式,其中比例最高的形式是下列何者?
        氣態二氧化碳                           溶於血漿中之二氧化碳
        碳醯胺基血紅素(carbaminohemoglobin)     碳酸氫根離子( HCO₃_ )
  • 選項的編號應該是被視為亂碼然後被移除了
    • 這也導致我們前面順序錯亂的問題看起來消失了
  • 化學式他這個結果看起來很漂亮,不過也許可以再試一個更複雜的化學式

全文圖片的部分:

108年第一次專門職業及技術人員高等考試中醫師考試分階段                代號:5106
考試、營養師、心理師、護理師、社會工作師考試試題                   頁次:8-1
等      別:高等考試
類      科:護理師
科      目:精神科與社區衛生護理學
考試時間:1小時                            座號:
注意:(一)本試題為單一選擇題,請選出一個正確或最適當的答案,複選作答者,該題不予計分。
       (二)本科目共80題,每題1.25分,須用2B鉛筆在試卡上依題號清楚劃記,於本試題上作答者,不予計分。
       (三)禁止使用電子計算器。
    世界衛生組織強調對精神病人基本人權的尊重,下列何者正確?
       (A)病人應有權利在社區,而非機構化照顧
       (B)全球心理衛生治療的趨勢是從急性醫院轉換為慢性機構療養
       (C)精神生物醫學的進步,藥物治療的成效佳,故不需要心理社會的整合性照護
       (D)慢性精神病人宜在偏遠地區的療養機構接受治療與安置
 2    關於心理防衛機轉(defense mechanism),「把自己無法接受的想法與情緒,推給其他人,認為是
      別人擁有這樣的想法,而不是他自己。」指的是下列何種心理防衛機轉?
       (A)合理化(rationalization)      (B)壓抑(suppression)
      (C)反向行為(reactionformation)    (D)投射(projection)
  • OCR 的結果讓人很驚豔
  1. 再來是 mistral-ocr 的設置部分
  • 一樣是直接follow 官網的教學 這個
from mistralai import Mistral
from dotenv import load_dotenv

load_dotenv()

api_key = os.getenv("MISTRAL_API_KEY")
client = Mistral(api_key=api_key)

from pathlib import Path
from mistralai import DocumentURLChunk, ImageURLChunk, TextChunk
import json

# Verify PDF file exists
pdf_file = Path(file_path)
assert pdf_file.is_file()

# Upload PDF file to Mistral's OCR service
uploaded_file = client.files.upload(
    file={
        "file_name": pdf_file.stem,
        "content": pdf_file.read_bytes(),
    },
    purpose="ocr",
)
# Get URL for the uploaded file
signed_url = client.files.get_signed_url(file_id=uploaded_file.id, expiry=1)

  • 設置 api key
  • 確認我們的 pdf 檔案存在
  • 把我們的 pdf 檔案 上傳到 mistral 的 ocr service
    • 這個應該是有本地版的,總之我沒試
  1. 呼叫,然後存成 json
# Process PDF with OCR, including embedded images
pdf_response = client.ocr.process(
    document=DocumentURLChunk(document_url=signed_url.url),
    model="mistral-ocr-latest",
    include_image_base64=True
)

# Convert response to JSON format
response_dict = json.loads(pdf_response.model_dump_json())

with open('mistral.json', 'w', encoding="utf-8") as f:
    f.write(json.dumps(response_dict, indent=2, ensure_ascii=False))
  1. mistral-ocr 的結果
    https://ithelp.ithome.com.tw/upload/images/20250921/20177855bmAX9vBohT.jpg
  • 他每頁會回傳一個 dictionary 回來
  • markdown 部分就包含了當頁的全部內容
  • 現在 2025.09.21 測起來已經沒有當初網傳所謂繁體中文不好的問題了

Summary

  • 我們今天主要在探索 pdf2txt 會遇到的部分問題,包含位置跑掉(尤其是化學式)、整個都是圖檔等等
  • 我們試了基本的 fitz(PyMuPDF) 和 pdfplumber,然後實際看了問題
  • 我們測試了 llama-parse 還有 mistral-ocr,結果都還算不錯,尤其是在中文字的 ocr 上,在我們的 case 上看起來是不太需要擔心

其他

  • pdf 的 parser 一直都是個大問題,而且個人認為很多都是 case by case 的,今天這份 pdf 可以了,明天遇到另一份可能又不行了
  • 先前就已經嘗試過包含:
    • 用 local 的 unstructure 把 pdf 先框 bounding box 再提文字
      • 這個先前的結果是: bounding box 直接一堆錯(類別錯: title 被當成圖片等等)而沒有繼續下去
    • 直接把單頁圖片傳給 vlm 要它逐字把內容提出來
      • 這個先前的結果是: 常常遇到不聽話、少內容、幻覺內容等等問題
  • 今天藉機測試了一下大名鼎鼎的 mistral-ocr 還額外測了 llama-parse
    • 一個主要的發現是對於先前網傳說繁體中文不行有了一些改觀,至少在我們的 case 上繁體中文還算 work
    • 在 llama-parse 的部分,除了看的到 text 以外,還可以看到每頁分塊的 bounding box 和 內容(Layout)
      • 不過語言參數一定要給不然會失敗
  • 推薦直接用 fitx、 pdfplumber 轉 txt 遇到難以克服問題的人試試這兩個工具,這次測下來個人是覺得滿驚豔的
  • 他們還有一些額外更深入的文章,mistral的放在refernece
  • 優點就講到這了,畢竟他們沒有付我錢,這個不是業配,而且平衡報導一下: llama-parse 還是把我多選題的選項符號給直接砍掉
  • 今天又壓線發文 = =

reference:


上一篇
Day5: 從 Youtube 下載的字幕生成問題與答案對
下一篇
Day7: perplexity api 初探 與 fact_ckecher 功能測試
系列文
阿,又是一個RAG8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言