Flutter desktopで開発中の電子帳簿リネーマー、今日の進捗をまとめます。
今日やったこと
- ✅ ステップ5: ドラッグ&ドロップ実装(
desktop_drop) - ✅ 金額抽出のバグ修正(バックスラッシュ円記号 + 消費税の誤マッチ)
- ✅ 取引先認識を法人番号で補強(Xserver対応)
- ✅ 日付・金額に手動入力フィールド追加
コミット数は5件。そして何より大きな収穫は、プロダクトの設計思想が固まったことでした。
1. ドラッグ&ドロップ実装
desktop_dropパッケージを導入して、FinderからPDFをドラッグするだけで処理が走るようにしました。
段階を分けて進めました:
- パッケージ追加 →
flutter pub get DropTargetウィジェットでメインUIを包む(動作確認用にdebugPrintだけ)- ファイル選択ボタンと共通化するため、PDF処理を
_processPdfFile(String filePath)に分離 onDragDoneコールバックから_processPdfFileを呼ぶ
リファクタリングのおかげで、ボタン経由でもドラッグ&ドロップでも同じ処理が走る形になりました。共通化って気持ちいい。
2. 金額抽出のバグ修正(2つの罠)
XserverのPDFをドロップしたら、金額が「6」と表示される謎現象が発生。本来は「1,721円」です。
調べたら、原因が2つ重なっていました。
罠1: 円記号がバックスラッシュとして抽出される
XserverのPDFテキスト抽出結果を見ると、円記号(¥)が半角バックスラッシュ(\)として出力されていました:
(税率10%) \1,721- (消費税 156円)
これはフォントマッピングの関係で、日本の業務PDFでよくある現象だそうです。既存の正規表現は[¥¥]しか見ていなかったので、\1,721は全部スルーされていました。
修正: 全パターンの正規表現に半角バックスラッシュを追加。Dart正規表現では \\ と書くと1文字のバックスラッシュにマッチします。
// 修正前
RegExp(r'[¥¥]\s*([\d,]+)')
// 修正後
RegExp(r'[¥¥\\]\s*([\d,]+)')
罠2: 正規表現のバックトラッキング誤マッチ
「6」の正体は、これでした:
RegExp(r'(?:消費税|税額)\s*[\d.]+%?\s*([\d,]+)')
このパターンが (消費税 156円) に対して、
消費税← マッチ[\d.]+%?← 「15」を税率%扱い(%がオプショナルなので貪欲マッチ)([\d,]+)← 「6」を金額扱い
と分割解釈していました。正規表現エンジンのバックトラッキングが裏目に出たケースです。
修正: 税率%部分を完全オプショナルにして、円記号付き金額の検出を消費税より先に処理するよう優先順位を入れ替え。
3. 法人番号で取引先認識を補強
XserverのPDFには、もう一つ落とし穴がありました。
「XServer」「エックスサーバー株式会社」というロゴ・社名が、全部ヘッダー画像になっている。テキスト抽出には引っかからない。だから取引先認識ロジックもスルーしてしまう。
でも、抽出テキストの中にインボイス登録番号 T7120001168421 がありました。これは13桁の数字、偶然マッチする可能性は実質ゼロ。
2023年のインボイス制度開始以降、領収書には登録番号の表示が義務化されています。つまり画像ロゴだけの領収書でも、登録番号は必ずテキストで出るということ。
これは強力な手がかりです。Xserverのルールに法人番号を追加:
[
'Xserver',
[
// ブランド名
'エックスサーバー', 'Xserver',
// ドメイン
'xserver.ne.jp', 'xserver.jp',
// 法人番号(インボイス登録番号)
'7120001168421',
],
],
同じ手法は、ロゴが画像化されている他の領収書にも応用できそうです。
4. 楽天モバイルで気づいた「リネーマーの本分」
ここからが今日のメインです。
楽天モバイルの請求書をテストしたら、日付も金額も誤検出。
抽出されたテキストを見ると:
【導入日】2024年3月26日← 二段階認証の説明文を日付として誤検出年間合計13.2円← 電話リレーサービス料の案内文から「13」を金額として誤検出- 本来の
2025年1月15日 発行と449円は、ラベルが値の後に来る縦書きフォームレイアウトで、現状のロジックでは拾えない
これを完璧に解析しようとすると、複雑な正規表現の沼にハマることが見えました。
楽天モバイル用の特殊パターンを足しても、他のPDFを壊すかもしれない。レイアウト解析を入れると、それはもうOCRとAIの世界。
そこでハッと気づきました。
このアプリは「領収書リネーマー」であって「ドキュメント解析エンジン」じゃない。
このアプリのコアバリューは「リネームと整理を楽にすること」。自動抽出が外れるケースは必ずある。だったら、ユーザーが手入力で簡単に補正できる方が健全。
5. 手動入力フィールドを追加
日付と金額のチップの下に、テキストフィールドを追加しました。
📅 日付候補(クリックで選択)
[20250115] [20240326] ← 既存のチップ
┌──────────────────────────┐
│ 20250115 │ ← 新規追加!
└──────────────────────────┘
- チップが当たる場合: ワンクリックで選択(従来通り)
- チップが当たらない場合: テキストフィールドに手入力
- 候補がゼロの場合でも入力欄が表示される(条件を変更)
楽天モバイルのPDFで、テキストフィールドに 20250115 と 449 を手入力 → 「この名前で確定する」を押すと、
20250115_449_楽天モバイル.pdf
として、ちゃんと月別フォルダ(2025/01/)に保存されました。
実装としては既存の取引先(Vendor)のパターンをそのままコピー。TextEditingControllerを2つ追加して、onChangedで_selectedDateと_selectedAmountを更新するだけ。シンプル。
まとめ: プロダクトの方向性が決まった
今日の最大の収穫は、コードよりもこの設計思想だったかもしれません。
完璧な自動化を追求すると、エッジケース対応で永遠に終わらない。それより:
- 自動抽出は便利な「提案」
- 最後の責任はユーザー
- 手入力をストレスなくできるUIにする
この線引きができると、開発の優先順位が一気にクリアになりました。
「リネーマー」というプロダクト名にちゃんと立ち戻れた一日でした。
明日以降のタスク
- 警告フラグ機能(🔴金額不明、🟡0円、💡取引先不明)
- 他のPDFサンプル(Microsoft / Apple / Adobe)での動作確認
- 外貨建て対応(OpenAI USD)
- Security-Scoped Bookmarksの実装(リリース前にサンドボックス復活)
- Lemonsqueezy連携(無料版25ファイル制限)
- Mac公証(Notarization)
リリースに向けて着実に。
