在課題組要做報帳,那麼就免不了根據發票做出入庫單,東西少的時候還好,一多起來真的麻煩死人。所以在我還在參與報帳工作時我就做了一個小工具,可以很暢快地做出入庫單,介面如下圖:
雖然減少心智負擔了,但是這個時候依舊是需要手動輸入發票號碼、代碼、開票日期等信息。現在沒有參與報帳工作了,突然想到要是能直接上傳文件就獲得所有發票信息該多好,說幹就幹。最初的項目是 js/ts 一把梭,這次改用 Python,畢竟人生苦短,我用 Python。
提取發票信息的主體代碼實現如下,主要依賴 pdfplumber 這個庫和正則表達式:
import pdfplumber
import re
from typing import List, Dict, Optional
class InvoiceExtractor:
def _invoice_pdf2txt(self, pdf_path: str) -> Optional[str]:
"""
使用 pdfplumber 從 PDF 文件中提取文本。
:param pdf_path: PDF 文件的路徑。
:return: 提取的文本作為字符串返回,如果提取失敗則返回 None。
"""
try:
with pdfplumber.open(pdf_path) as pdf:
text = '\n'.join(page.extract_text() for page in pdf.pages if page.extract_text())
return text
except Exception as e:
#print(f"從 {pdf_path} 提取文本時出錯: {e}")
return None
def _extract_invoice_product_content(self, content: str) -> str:
"""
從發票文本中提取商品相關內容。
:param content: 發票的完整文本。
:return: 提取的商品相關內容作為字符串返回。
"""
lines = content.splitlines()
start_pattern = re.compile(r"^(貨物或應稅勞務|項目名稱)")
end_pattern = re.compile(r"^價稅合計")
start_index = next((i for i, line in enumerate(lines) if start_pattern.match(line)), None)
end_index = next((i for i, line in enumerate(lines) if end_pattern.match(line)), None)
if start_index is not None and end_index is not None:
extracted_lines = lines[start_index:end_index + 1]
return '\n'.join(extracted_lines).strip()
return "未找到匹配的內容"
def construct_invoice_product_data(self, raw_text: str) -> List[Dict[str, str]]:
"""
處理提取的文本,構建發票商品數據列表。
:param raw_text: 提取的原始文本。
:return: 商品數據列表,每個商品為一個字典。
"""
blocks = re.split(r'(?=貨物或應稅勞務|項目名稱)', raw_text.strip())
records = []
for block in blocks:
lines = [line.strip() for line in block.splitlines() if line.strip()]
if not lines:
continue
current_record = ""
for line in lines[1:]:
if line.startswith("合") or line.startswith("價稅合計"):
continue
if line.startswith("*"):
if current_record:
self._process_record(current_record, records)
current_record = line
else:
if " " in current_record:
first_space_index = current_record.index(" ")
current_record = current_record[:first_space_index] + line + current_record[first_space_index:]
if current_record:
self._process_record(current_record, records)
return records
def _process_record(self, record: str, records: List[Dict[str, str]]):
"""
處理單條記錄並添加到記錄列表中。
:param record: 單條記錄的字符串。
:param records: 記錄列表。
"""
parts = record.rsplit(maxsplit=7)
if len(parts) == 8:
try:
records.append({
"product_name": parts[0].strip(),
"specification": parts[1].strip(),
"unit": parts[2].strip(),
"quantity": parts[3].strip(),
"unit_price": float(parts[4].strip()),
"amount": float(parts[5].strip()),
"tax_rate": parts[6].strip(),
"tax_amount": float(parts[7].strip())
})
except ValueError as e:
print(f"記錄解析失敗: {record}, 錯誤: {e}")
pass
最終呢會得到一個字典,包含了發票的商品名、規格、單位、數量、單價、總價、稅率以及稅額。緊接著,基於這段腳本,再結合 fastapi 和 vue3,就搞了一個拖拽就能獲取發票信息、導出出入庫單的應用啦:
當然,我現在又不負責報帳的工作了,做出來也是造福師弟師妹們,管他們用不用,反正我做出來了