.venv deleted

ocr ready to test
This commit is contained in:
2025-11-29 12:29:08 +03:00
parent 449556c4e4
commit 91923b8616
2094 changed files with 562 additions and 370942 deletions

132
ocr-service/parser.py Normal file
View File

@@ -0,0 +1,132 @@
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