Files
rmser/ocr-service/parser.py
SERTY 91923b8616 .venv deleted
ocr ready to test
2025-11-29 12:29:08 +03:00

132 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import re
from typing import List
from pydantic import BaseModel
class ParsedItem(BaseModel):
raw_name: str
amount: float
price: float
sum: float
# Регулярка для поиска чисел с плавающей точкой: 123.00, 123,00, 10.5
FLOAT_RE = r'\d+[.,]\d{2}'
def clean_text(text: str) -> str:
"""Удаляет лишние символы из названия товара."""
# Оставляем буквы, цифры, пробелы и базовые знаки
return re.sub(r'[^\w\s.,%/-]', '', text).strip()
def parse_float(val: str) -> float:
"""Преобразует строку '123,45' или '123.45' в float."""
if not val:
return 0.0
# Заменяем запятую на точку и убираем возможные пробелы
return float(val.replace(',', '.').replace(' ', ''))
def parse_receipt_text(text: str) -> List[ParsedItem]:
"""
Парсит текст чека построчно.
Логика: накапливаем строки названия, пока не встретим строку с математикой (цена/сумма).
"""
lines = text.split('\n')
items = []
name_buffer = []
for line in lines:
line = line.strip()
if not line:
continue
# Ищем все числа, похожие на цену (с двумя знаками после запятой/точки)
# Пример: 129.99
floats = re.findall(FLOAT_RE, line)
# Эвристика: строка считается "товарной позицией с ценой", если в ней есть минимум 2 числа
# (обычно Цена и Сумма, или Кол-во и Сумма)
# ИЛИ одно число, если это итоговая сумма, но мы ищем товары.
is_price_line = False
if len(floats) >= 2:
is_price_line = True
vals = [parse_float(f) for f in floats]
# Попытка определить структуру: Цена x Кол-во = Сумма
price = 0.0
amount = 1.0
total = 0.0
# Обычно последнее число - это сумма (итог по строке)
total = vals[-1]
if len(vals) == 2:
# Скорее всего: Цена ... Сумма (кол-во = 1)
# Или: Кол-во ... Сумма (если цена не распозналась)
# Предположим amount=1, тогда первое число - цена
price = vals[0]
amount = 1.0
# Проверка на адекватность: сумма обычно >= цены
# Если total < price, возможно порядок перепутан или это скидка
if total < price and total != 0:
# Если total сильно меньше, возможно это не сумма
pass
elif total > price and price > 0:
# Пытаемся вычислить кол-во
calc_amount = total / price
# Если результат близок к целому (например 1.999 -> 2), то ок
if abs(round(calc_amount) - calc_amount) < 0.05:
amount = float(round(calc_amount))
elif len(vals) >= 3:
# Варианты: [Цена, Кол-во, Сумма] или [Кол-во, Цена, Сумма]
# Проверяем математику: A * B = C
v1, v2 = vals[-3], vals[-2]
if abs(v1 * v2 - total) < 0.5: # Допуск 0.5 руб
price = v1
amount = v2
elif abs(v2 * v1 - total) < 0.5:
price = v2
amount = v1
else:
# Если математика не сходится, берем предпоследнее как цену
price = vals[-2]
amount = 1.0
# Сборка названия
full_name = " ".join(name_buffer).strip()
# Если буфер пуст, возможно название в этой же строке слева
if not full_name:
# Удаляем найденные числа из строки, остаток считаем названием
text_without_floats = re.sub(FLOAT_RE, '', line)
full_name = clean_text(text_without_floats)
# Фильтрация мусора (слишком короткие названия или нулевые суммы)
if len(full_name) > 2 and total > 0:
items.append(ParsedItem(
raw_name=full_name,
amount=amount,
price=price,
sum=total
))
# Очищаем буфер, так как позиция закрыта
name_buffer = []
else:
# Строка не похожа на цену.
# Проверяем на стоп-слова (конец чека)
upper_line = line.upper()
if "ИТОГ" in upper_line or "СУММА" in upper_line or "ПРИХОД" in upper_line:
# Считаем, что товары закончились
name_buffer = []
# Можно здесь сделать break, если уверены, что ниже товаров нет
continue
# Добавляем в буфер названия
name_buffer.append(line)
return items