Python基于OpenAI?API輕松實(shí)現(xiàn)PDF發(fā)票信息提取
1. 引言
在日常辦公中,企業(yè)和個(gè)人經(jīng)常需要處理大量的發(fā)票數(shù)據(jù),包括提取發(fā)票代碼、發(fā)票號碼、開票日期、金額等關(guān)鍵信息。手動錄入這些信息費(fèi)時(shí)費(fèi)力,因此自動化發(fā)票信息提取成為剛需。
本文將介紹如何利用 OpenAI API 和 PyMuPDF (即 fitz) 實(shí)現(xiàn) PDF 發(fā)票的自動信息提取。代碼將解析 PDF 文件內(nèi)容,并通過 AI 模型精準(zhǔn)提取相關(guān)字段。
2. 依賴環(huán)境
在實(shí)現(xiàn)該功能之前,需要安裝以下 Python 依賴庫:
pip install openai pymupdf
此外,需要在 config.py 中配置 API Key 及 PDF 文件路徑。
3. 代碼實(shí)現(xiàn)
完整代碼如下:
from openai import OpenAI
import fitz # PyMuPDF
import config # 需要配置API Key和PDF路徑
def get_pdf_text(pdf_path):
doc = fitz.open(pdf_path)
markdown_text = ""
for page_num in range(len(doc)):
page = doc[page_num]
blocks = page.get_text("dict")["blocks"]
for block in blocks:
if "lines" in block:
for line in block["lines"]:
for span in line["spans"]:
text = span["text"]
markdown_text += f"{text}"
markdown_text += "\n"
return markdown_text
def create_invoice_extraction_prompt(invoice_text):
prompt = f"""
請分析以下發(fā)票文本,準(zhǔn)確提取以下信息:
1. 發(fā)票代碼
2. 發(fā)票號碼
3. 開票日期
4. 價(jià)稅合計(jì)的小寫金額
5. 銷售方名稱
6. 消費(fèi)類型(如:餐飲、交通、辦公用品、通訊等)
請務(wù)必仔細(xì)分析商品或服務(wù)內(nèi)容,據(jù)此判斷消費(fèi)類型。
如果無法確定某項(xiàng)信息,請標(biāo)注為"未找到"。
發(fā)票文本:
{invoice_text}
請以 JSON 格式返回結(jié)果:
{{
"發(fā)票代碼": "",
"發(fā)票號碼": "",
"開票日期": "",
"價(jià)稅合計(jì)": "",
"銷售方名稱": "",
"消費(fèi)類型": ""
}}
"""
return prompt
# 初始化 OpenAI 客戶端
client = OpenAI(
api_key=config.DASHSCOPE_API_KEY,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
sample_invoice_text = get_pdf_text(config.PDF_PATH)
# 創(chuàng)建聊天完成請求
completion = client.chat.completions.create(
model="qwq-32b", # 可按需更換模型名稱
messages=[
{"role": "user", "content": create_invoice_extraction_prompt(sample_invoice_text)}
],
stream=True,
)
reasoning_content = "" # 記錄思考過程
answer_content = "" # 記錄最終答案
is_answering = False # 標(biāo)記是否已進(jìn)入回復(fù)階段
print("\n" + "=" * 20 + "思考過程" + "=" * 20 + "\n")
for chunk in completion:
if not chunk.choices:
print("\nUsage:")
print(chunk.usage)
else:
delta = chunk.choices[0].delta
if hasattr(delta, 'reasoning_content') and delta.reasoning_content:
print(delta.reasoning_content, end='', flush=True)
reasoning_content += delta.reasoning_content
else:
if delta.content and not is_answering:
print("\n" + "=" * 20 + "完整回復(fù)" + "=" * 20 + "\n")
is_answering = True
print(delta.content, end='', flush=True)
answer_content += delta.content
print("\n\n" + "=" * 50)
print("提取結(jié)果:")
print(answer_content)
4. 運(yùn)行示例

5. 總結(jié)
通過本文的介紹,我們實(shí)現(xiàn)了一個(gè)基于 OpenAI API 解析 PDF 發(fā)票的完整流程,包括:
- 使用 PyMuPDF 解析 PDF 文本
- 構(gòu)造 AI 提示詞,讓大模型精準(zhǔn)提取發(fā)票信息
- 調(diào)用 OpenAI API 并解析返回 JSON 結(jié)果
這一方案可以廣泛應(yīng)用于財(cái)務(wù)報(bào)銷、企業(yè)票據(jù)管理等場景,極大提高工作效率。如果你有更復(fù)雜的需求,可以嘗試調(diào)整 prompt 或使用更強(qiáng)大的 LLM 模型。
擴(kuò)展代碼(金額提取不穩(wěn)定)
import fitz # PyMuPDF
import requests
import json
import base64
import os
import re
import glob
import shutil # 添加這行用于文件復(fù)制
import traceback # 添加這行用于異常處理
from loguru import logger
def pdf_to_images(pdf_path, output_dir="images", dpi=150):
"""
將PDF轉(zhuǎn)換為圖片
Args:
pdf_path (str): PDF文件路徑
output_dir (str): 輸出目錄
dpi (int): 圖片分辨率
Returns:
list: 生成的圖片文件路徑列表
"""
# 創(chuàng)建輸出目錄
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 打開PDF文件
pdf_document = fitz.open(pdf_path)
image_paths = []
# 獲取PDF文件名(不含擴(kuò)展名)
pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]
# 遍歷每一頁
for page_num in range(len(pdf_document)):
page = pdf_document.load_page(page_num)
# 設(shè)置縮放矩陣以控制分辨率
mat = fitz.Matrix(dpi/72, dpi/72)
# 渲染頁面為圖片
pix = page.get_pixmap(matrix=mat)
# 保存圖片,文件名包含PDF名稱
image_path = os.path.join(output_dir, f"{pdf_name}_page_{page_num + 1}.png")
pix.save(image_path)
image_paths.append(image_path)
pdf_document.close()
return image_paths
def image_to_base64(image_path):
"""
將圖片轉(zhuǎn)換為base64編碼
Args:
image_path (str): 圖片文件路徑
Returns:
str: base64編碼的圖片數(shù)據(jù)
"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def analyze_image_with_qwen(image_path, prompt="請描述這張圖片的內(nèi)容", ollama_url="http://192.168.1.2:11434"):
"""
使用Ollama API的qwen2.5vl:3b模型分析圖片
Args:
image_path (str): 圖片文件路徑
prompt (str): 分析提示詞
ollama_url (str): Ollama服務(wù)器地址
Returns:
str: 模型分析結(jié)果
"""
# 將圖片轉(zhuǎn)換為base64
image_base64 = image_to_base64(image_path)
# 構(gòu)建請求數(shù)據(jù)
data = {
"model": "qwen2.5vl:3b",
"prompt": prompt,
"images": [image_base64],
"stream": False
}
try:
# 發(fā)送請求到Ollama API
response = requests.post(
f"{ollama_url}/api/generate",
json=data,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
result = response.json()
return result.get("response", "無法獲取分析結(jié)果")
else:
return f"API請求失敗,狀態(tài)碼: {response.status_code}"
except requests.exceptions.RequestException as e:
logger.info(f"####{response.text}")
return f"請求異常: {str(traceback.format_exc())}"
def get_pdf_files(source_dir):
"""
獲取目錄中的所有PDF文件
Args:
source_dir (str): 源目錄路徑
Returns:
list: PDF文件路徑列表
"""
pdf_files = []
# 支持的PDF文件擴(kuò)展名
pdf_extensions = ['*.pdf']
for extension in pdf_extensions:
pattern = os.path.join(source_dir, '**', extension)
pdf_files.extend(glob.glob(pattern, recursive=True))
return sorted(pdf_files)
def copy_and_rename_pdf(original_pdf_path, output_dir, new_filename):
"""
復(fù)制PDF文件到指定目錄并重命名
Args:
original_pdf_path (str): 原始PDF文件路徑
output_dir (str): 輸出目錄
new_filename (str): 新文件名(不含擴(kuò)展名)
Returns:
str: 新文件的完整路徑
"""
# 確保輸出目錄存在
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 清理文件名中的非法字符
safe_filename = re.sub(r'[<>:"/\\|?*]', '_', new_filename)
# 構(gòu)建新的文件路徑
new_pdf_path = os.path.join(output_dir, f"{safe_filename}.pdf")
try:
# 復(fù)制文件
shutil.copy2(original_pdf_path, new_pdf_path)
logger.info(f"PDF文件已復(fù)制并重命名為: {new_pdf_path}")
return new_pdf_path
except Exception as e:
logger.info(f"復(fù)制PDF文件時(shí)出錯: {str(e)}")
return None
def analyze_with_qwen3_32b(analysis_text, extra_prompt, ollama_url="http://192.168.1.2:11434"):
"""
使用qwen3:32b模型進(jìn)行二次分析和格式化
Args:
analysis_text (str): 第一次分析的結(jié)果
extra_prompt (str): 格式化提示詞
ollama_url (str): Ollama服務(wù)器地址
Returns:
str: 格式化后的結(jié)果
"""
# 構(gòu)建完整的提示詞
full_prompt = f"{extra_prompt}\n\n基于以下發(fā)票分析結(jié)果:\n{analysis_text}\n\n請按照上述格式要求輸出:/no_think"
# 構(gòu)建請求數(shù)據(jù)
data = {
"model": "qwen3:32b",
"prompt": full_prompt,
"stream": False
}
try:
# 發(fā)送請求到Ollama API
response = requests.post(
f"{ollama_url}/api/generate",
json=data,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
result = response.json()
return result.get("response", "無法獲取格式化結(jié)果").strip()
else:
return f"API請求失敗,狀態(tài)碼: {response.status_code}"
except requests.exceptions.RequestException as e:
return f"請求異常: {str(e)}"
def process_single_pdf(pdf_path, output_dir, analysis_prompt):
"""
處理單個(gè)PDF文件
Args:
pdf_path (str): PDF文件路徑
output_dir (str): 輸出目錄
analysis_prompt (str): 分析提示詞
Returns:
dict: 處理結(jié)果
"""
pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]
logger.info(f"\n開始處理PDF文件: {pdf_name}")
# 為每個(gè)PDF創(chuàng)建獨(dú)立的圖片目錄
images_dir = os.path.join(output_dir, "images", pdf_name)
# 1. PDF轉(zhuǎn)圖片
image_paths = pdf_to_images(pdf_path, images_dir)
# 2. 使用qwen2.5vl分析每張圖片
logger.info(f"=== 步驟2: {pdf_name} AI圖像識別分析 ===")
results = {}
formatted_outputs = []
for i, image_path in enumerate(image_paths, 1):
logger.info(f"正在分析 {pdf_name} 第 {i} 頁圖片...")
analysis_result = analyze_image_with_qwen(image_path, analysis_prompt)
logger.info(f"分析結(jié)果: {analysis_result}")
# 使用qwen3:32b提取
extra_prompt = """
注意:如果金額必須最大的數(shù)字為金額
輸出格式:最大數(shù)字的金額_類別_商戶名稱_納稅人識別號_發(fā)票號碼_年_月_日
示例輸出格式:82.00_餐飲_壽司餐飲服務(wù)管理有限公司_111100259570_22970898_2025_03_30
""".strip()
# 使用qwen3:32b進(jìn)行二次處理和格式化
formatted_result = analyze_with_qwen3_32b(analysis_result, extra_prompt)
logger.info(f"qwen3:32b原始輸出: {formatted_result}")
formatted_result= formatted_result.split("</think>")[-1].strip()
logger.info(f"格式化結(jié)果: {formatted_result}")
formatted_outputs.append(formatted_result)
results[f"page_{i}"] = {
"image_path": image_path,
"analysis": analysis_result,
"formatted_output": formatted_result
}
logger.info(f"最終格式化輸出: {formatted_result}")
break
# 3. 復(fù)制PDF文件并重命名
copied_pdf_paths = []
for i, analysis_result in enumerate(formatted_outputs, 1):
if analysis_result.strip(): # 確保分析結(jié)果不為空
# 創(chuàng)建重命名的PDF目錄
renamed_pdf_dir = os.path.join(output_dir, "renamed_pdfs")
# 復(fù)制并重命名PDF文件
new_pdf_path = copy_and_rename_pdf(pdf_path, renamed_pdf_dir, analysis_result)
if new_pdf_path:
copied_pdf_paths.append(new_pdf_path)
return {
"pdf_name": pdf_name,
"pdf_path": pdf_path,
"results": results,
"formatted_outputs": formatted_outputs,
"copied_pdf_paths": copied_pdf_paths # 添加復(fù)制的PDF路徑信息
}
def process_pdf_directory(source_dir, output_dir, analysis_prompt="請?jiān)敿?xì)分析這張發(fā)票,提取以下信息:最大金額、商戶名稱、納稅人識別號、發(fā)票號碼、開票日期、商品類別等關(guān)鍵信息"):
"""
批量處理PDF目錄
Args:
source_dir (str): 源PDF目錄
output_dir (str): 輸出目錄
analysis_prompt (str): 分析提示詞
Returns:
dict: 所有處理結(jié)果
"""
# 創(chuàng)建輸出目錄
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 獲取所有PDF文件
pdf_files = get_pdf_files(source_dir)
if not pdf_files:
logger.info(f"在目錄 {source_dir} 中未找到PDF文件")
return {}
logger.info(f"找到 {len(pdf_files)} 個(gè)PDF文件")
all_results = {}
all_formatted_outputs = []
# 處理每個(gè)PDF文件
for i, pdf_path in enumerate(pdf_files, 1):
logger.info(f"處理進(jìn)度: {i}/{len(pdf_files)}")
try:
result = process_single_pdf(pdf_path, output_dir, analysis_prompt)
all_results[result["pdf_name"]] = result
all_formatted_outputs.extend(result["formatted_outputs"])
except Exception as e:
logger.info(f"處理 {pdf_path} 時(shí)出錯: {str(e)}")
continue
return all_results, all_formatted_outputs
def save_results(results, output_dir, filename="analysis_results.json"):
"""
保存分析結(jié)果到JSON文件
Args:
results (dict): 分析結(jié)果
output_dir (str): 輸出目錄
filename (str): 文件名
"""
output_file = os.path.join(output_dir, filename)
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
logger.info(f"\n分析結(jié)果已保存到: {output_file}")
def save_formatted_outputs(formatted_outputs, output_dir, filename="formatted_outputs.txt"):
"""
保存格式化輸出到文本文件
Args:
formatted_outputs (list): 格式化輸出列表
output_dir (str): 輸出目錄
filename (str): 文件名
"""
output_file = os.path.join(output_dir, filename)
with open(output_file, 'w', encoding='utf-8') as f:
for output in formatted_outputs:
f.write(output + '\n')
logger.info(f"格式化輸出已保存到: {output_file}")
def main():
"""
主函數(shù)
"""
logger.info("PDF批量分析工具")
logger.info("=" * 30)
# 獲取源PDF目錄
# source_dir = input("請輸入源PDF目錄路徑: ").strip()
source_dir = r"D:\2025年7月期間發(fā)票"
if not os.path.exists(source_dir):
logger.info("源目錄不存在,請檢查路徑")
return
if not os.path.isdir(source_dir):
logger.info("輸入的路徑不是目錄")
return
# 獲取輸出目錄
# output_dir = input("請輸入輸出目錄路徑: ").strip()
output_dir = r"D:\2025年7月期間發(fā)票2"
# 自定義分析提示詞
# custom_prompt = input("請輸入分析提示詞(直接回車使用默認(rèn)): ").strip()
custom_prompt = "分析發(fā)票圖片,提取發(fā)票最大的小寫金額,產(chǎn)品所屬類別,銷售方名稱,納稅人識別號,發(fā)票號碼,開票日期,一段話"
# if not custom_prompt:
# custom_prompt = """請分析這張發(fā)票圖片,并嚴(yán)格按照以下格式輸出一行結(jié)果:
# 金額_類別_商戶名稱_納稅人識別號_發(fā)票號碼_年_月_日
# 要求:
# 1. 金額:提取"價(jià)稅合計(jì)"字段對應(yīng)的金額,通常格式為(小寫)¥82.00,只提取數(shù)字部分如82.00
# 2. 類別:根據(jù)商品判斷(餐飲、住宿、交通、辦公、其他)
# 3. 商戶名稱:發(fā)票上的企業(yè)名稱
# 4. 納稅人識別號:統(tǒng)一社會信用代碼或納稅人識別號
# 5. 發(fā)票號碼:發(fā)票號碼
# 6. 日期:開票日期,格式為年_月_日,如2025_03_30
# 注意:
# - 重點(diǎn)識別"價(jià)稅合計(jì)"字段,不要識別成"合計(jì)"
# - 金額前可能有(小寫)¥符號,只提取數(shù)字部分
# - 確保提取的是價(jià)稅合計(jì)的最終金額
# 示例輸出格式:82.00_餐飲_壽司餐飲服務(wù)管理有限公司_111100259570_22970898_2025_03_30
# 請只輸出這一行格式化結(jié)果,不要包含其他說明文字。"""
try:
# 批量處理PDF目錄
logger.info(f"\n開始批量處理PDF目錄: {source_dir}")
all_results, all_formatted_outputs = process_pdf_directory(source_dir, output_dir, custom_prompt)
if not all_results:
logger.info("沒有成功處理任何PDF文件")
return
# 保存結(jié)果
save_results(all_results, output_dir)
save_formatted_outputs(all_formatted_outputs, output_dir)
logger.info("批量處理完成")
logger.info("\n=== 所有格式化輸出結(jié)果 ===")
for i, output in enumerate(all_formatted_outputs, 1):
logger.info(f"{i:3d}: {output}")
except Exception as e:
logger.info(f"處理過程中出現(xiàn)錯誤: {str(e)}")
if __name__ == "__main__":
main()
到此這篇關(guān)于Python基于OpenAI API輕松實(shí)現(xiàn)PDF發(fā)票信息提取的文章就介紹到這了,更多相關(guān)Python提取PDF發(fā)票信息內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python+Mysql實(shí)現(xiàn)登錄注冊完整代碼示例
在開發(fā)中用戶注冊和登錄是常見的功能需求,這篇文章主要給大家介紹了關(guān)于Python+Mysql實(shí)現(xiàn)登錄注冊的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-03-03
Python實(shí)現(xiàn)從網(wǎng)絡(luò)攝像頭拉流的方法分享
這篇文章主要為大家詳細(xì)介紹了Python實(shí)現(xiàn)從網(wǎng)絡(luò)攝像頭拉流的幾種方法,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以了解一下2023-01-01
Python按天實(shí)現(xiàn)生成時(shí)間范圍序列的方法詳解
有的時(shí)候我們希望生成一段時(shí)間返回,比如從?2022-01-01?00:00:00?后面的?10?天,這么?10?個(gè)?datetime?對象,但是我們又不想自己去計(jì)算哪些月有30天哪些月有31天。所以本文將用Python實(shí)現(xiàn)按天自動生成時(shí)間范圍序列,需要的可以參考一下2022-11-11
使用python將mysql數(shù)據(jù)庫的數(shù)據(jù)轉(zhuǎn)換為json數(shù)據(jù)的方法
這篇文章主要介紹了使用python將mysql數(shù)據(jù)庫的數(shù)據(jù)轉(zhuǎn)換為json數(shù)據(jù)的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07
Python爬取英雄聯(lián)盟MSI直播間彈幕并生成詞云圖
很開心RNG最近在英雄聯(lián)盟季中賽奪冠了,特地爬取了直播間彈幕并生成詞云圖,大家一起開心一下,看看奪冠時(shí)大家都在說什么,需要的朋友可以參考下2021-06-06
Python爬取網(wǎng)易云歌曲評論實(shí)現(xiàn)詞云圖
這篇文章主要為大家介紹了Python爬取網(wǎng)易云歌曲評論實(shí)現(xiàn)詞云分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Python中集合的創(chuàng)建及常用函數(shù)的使用詳解
這篇文章主要為大家詳細(xì)介紹了Python中集合的創(chuàng)建、使用和遍歷,集合常見的操作函數(shù),集合與列表,元組,字典的嵌套,感興趣的小伙伴可以了解一下2022-06-06

