banner
Riceneeder

Riceneeder

白天研究生,晚上研究死
github
email

PDF請求書から商品詳細を取得する

研究室で経費精算をする際、請求書に基づいて入出庫伝票を作成する必要があります。物が少ないときはまだ良いですが、多くなると本当に面倒です。そこで、私が経費精算の作業に参加していたときに、スムーズに入出庫伝票を作成できる小さなツールを作りました。インターフェースは以下の図の通りです:

jietuchurukuold

心の負担は減りましたが、この時点でも請求書番号、コード、発行日などの情報を手動で入力する必要がありました。今は経費精算の作業に参加していないので、もしファイルを直接アップロードすることで全ての請求書情報を取得できたらどんなに良いかと思い、やってみることにしました。最初のプロジェクトは 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 を組み合わせて、ドラッグ&ドロップで請求書情報を取得し、入出庫伝票をエクスポートできるアプリケーションを作成しました:

screenshot

もちろん、今は経費精算の仕事を担当していないので、作ったものは後輩たちのために役立てばと思っています。彼らが使うかどうかは別として、とにかく私は作りました。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。