0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIに頼り切って家計簿アプリを追加開発する。:OCR実装③

0
Last updated at Posted at 2026-05-25

はじめに

前前回、家計簿アプリの設計を行いました(AIがやりました)
前回はOCRの実装を試みましたがうまく動作しません。
今回もOCRの実装をしていきます。

プロンプト

商品の名称、金額は読み込めているが日付、合計金額の読み込みが出来ていない、商品一覧に不要な情報が多く入っている。
こちらを改善するためのコードを出力してください。

回答

現在の状態を見る限り、OCR自体はかなり成功しています。
問題は:

OCR後のレシート構造解析

です。

必要なのは:

改善内容 対応
日付取得 専用Regex
合計取得 商品価格と分離
不要行除外 商品判定強化

です。

以下コードへ置き換えてください。


Execute全体

OCR解析メイン処理。

public OcrResult ParseReceipt(string text)
{
    OcrResult result = new OcrResult();

    string[] lines =
        text.Split(
            new[] { "\r\n", "\n" },
            StringSplitOptions.RemoveEmptyEntries);

    foreach (string rawLine in lines)
    {
        string line =
            NormalizeText(rawLine);

        line =
            NormalizeSpace(line);

        if (string.IsNullOrWhiteSpace(line))
        {
            continue;
        }

        // 日付解析
        ParseDate(line, result);

        // 合計解析
        ParseTotal(line, result);

        // 商品解析
        ParseItem(line, result);
    }

    return result;
}

日付解析


ParseDate

private void ParseDate(
    string line,
    OcrResult result)
{
    if (result.Date != null)
    {
        return;
    }

    Match match = Regex.Match(
        line,
        @"(\d{4})年\s*(\d{1,2})月\s*(\d{1,2})日");

    if (!match.Success)
    {
        return;
    }

    int year =
        int.Parse(match.Groups[1].Value);

    int month =
        int.Parse(match.Groups[2].Value);

    int day =
        int.Parse(match.Groups[3].Value);

    result.Date =
        new DateTime(
            year,
            month,
            day);
}

合計取得

重要。

商品価格と区別します。


ParseTotal

private void ParseTotal(
    string line,
    OcrResult result)
{
    if (!line.Contains("合計"))
    {
        return;
    }

    MatchCollection matches =
        Regex.Matches(
            line,
            @"([0-9]{1,5})");

    if (matches.Count == 0)
    {
        return;
    }

    string amountText =
        matches[matches.Count - 1].Value;

    if (decimal.TryParse(
        amountText,
        out decimal amount))
    {
        result.TotalAmount = amount;
    }
}

商品判定

これがかなり重要。


IsProductLine

private bool IsProductLine(string line)
{
    if (string.IsNullOrWhiteSpace(line))
    {
        return false;
    }

    // 日付
    if (Regex.IsMatch(
        line,
        @"\d{4}年"))
    {
        return false;
    }

    // 店舗情報
    string[] excludeWords =
    {
        "合計",
        "小計",
        "税込",
        "税額",
        "対象額",
        "お預り",
        "お釣り",
        "現計",
        "登録番号",
        "TEL",
        "営業時間",
        "ポイント",
        "カード",
        "税抜",
        "内税",
        "外税",
        "No",
        "店",
        "セルフ"
    };

    foreach (string word in excludeWords)
    {
        if (line.Contains(word))
        {
            return false;
        }
    }

    // 数値が無い
    MatchCollection matches =
        Regex.Matches(
            line,
            @"([0-9]{1,5})");

    if (matches.Count == 0)
    {
        return false;
    }

    // 商品名部分
    Match lastMatch =
        matches[matches.Count - 1];

    string productName =
        line.Substring(
            0,
            lastMatch.Index);

    productName =
        NormalizeProductName(productName);

    // 商品名短すぎ
    if (productName.Length <= 2)
    {
        return false;
    }

    return true;
}

商品解析


ParseItem

private void ParseItem(
    string line,
    OcrResult result)
{
    if (!IsProductLine(line))
    {
        return;
    }

    MatchCollection matches =
        Regex.Matches(
            line,
            @"([0-9]{1,5})");

    if (matches.Count == 0)
    {
        return;
    }

    // 最後の数値を価格扱い
    Match lastMatch =
        matches[matches.Count - 1];

    string amountText =
        lastMatch.Value;

    decimal amount =
        NormalizePrice(amountText);

    if (amount <= 0)
    {
        return;
    }

    string productName =
        line.Substring(
            0,
            lastMatch.Index);

    productName =
        NormalizeProductName(productName);

    amount =
        FixAmountByProductName(
            productName,
            amount);

    result.Items.Add(new OcrItem
    {
        Name = productName,
        Amount = amount,
        Quantity = 1,
        TaxRate = 10
    });
}

商品名補正


NormalizeProductName

private string NormalizeProductName(string text)
{
    text =
        Regex.Replace(
            text,
            @"\s+",
            "");

    text = text.Replace("-", "ー");

    text =
        Regex.Replace(
            text,
            @"[※★■◆\|]+",
            "");

    text =
        Regex.Replace(
            text,
            @"[。\.,]+$",
            "");

    return text.Trim();
}

OCR価格補正


NormalizePrice

private decimal NormalizePrice(string value)
{
    value = value.Replace("o", "0");
    value = value.Replace("O", "0");
    value = value.Replace("Q", "0");

    value =
        Regex.Replace(
            value,
            @"[^0-9]",
            "");

    if (decimal.TryParse(
        value,
        out decimal amount))
    {
        return amount;
    }

    return 0;
}

飲料価格補正


FixAmountByProductName

private decimal FixAmountByProductName(
    string productName,
    decimal amount)
{
    // 飲料なのに異常高額
    if (productName.Contains("mL"))
    {
        if (amount >= 900)
        {
            string text = amount.ToString();

            // 921 → 321
            if (text.StartsWith("9"))
            {
                text = "3" + text.Substring(1);
            }

            if (decimal.TryParse(
                text,
                out decimal fixedAmount))
            {
                return fixedAmount;
            }
        }
    }

    return amount;
}

このコードで改善される内容


日付

2026年05月10日

取得可能。


合計

2363

取得可能。


商品一覧

残るもの:

商品名 金額
バファリンPMDXクイック+40錠 1958
爽健美茶600mL 84
モンスターエナジー500mL 321

除外されるもの

  • 日付
  • 小計
  • 税額
  • 登録番号
  • 店舗情報
  • レジ番号

など。

起動、確認

起動してみます。

image.png

最後に

次回は支出登録全般を開発します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?