研究室で経費精算をする際、請求書に基づいて入出庫伝票を作成する必要があります。物が少ないときはまだ良いですが、多くなると本当に面倒です。そこで、私が経費精算の作業に参加していたときに、スムーズに入出庫伝票を作成できる小さなツールを作りました。インターフェースは以下の図の通りです:
心の負担は減りましたが、この時点でも請求書番号、コード、発行日などの情報を手動で入力する必要がありました。今は経費精算の作業に参加していないので、もしファイルを直接アップロードすることで全ての請求書情報を取得できたらどんなに良いかと思い、やってみることにしました。最初のプロジェクトは 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 を組み合わせて、ドラッグ&ドロップで請求書情報を取得し、入出庫伝票をエクスポートできるアプリケーションを作成しました:
もちろん、今は経費精算の仕事を担当していないので、作ったものは後輩たちのために役立てばと思っています。彼らが使うかどうかは別として、とにかく私は作りました。