banner
Riceneeder

Riceneeder

卜得山火贲之变艮卦,象曰:装饰既成,宜静宜止。2025下半年,不宜躁进,宜守正持中,沉淀与反思,将为日后之再发打下基石。
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

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

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