Команда Python for Devs подготовила перевод статьи о том, как с помощью LlamaIndex и Pydantic можно превратить сканы чеков в структурированные данные. Минимум кода — и у вас готовый CSV для анализа.
Ручной ввод данных из чеков, счетов и контрактов отнимает часы времени и часто приводит к ошибкам. А что если можно автоматически извлекать структурированные данные из таких документов всего за несколько минут?
В этой статье вы узнаете, как превратить изображения чеков в структурированные данные с помощью LlamaIndex, а затем экспортировать результат в таблицу для анализа.
Полный исходный код и Jupyter-ноутбук для этого туториала доступны на GitHub. Клонируйте репозиторий и повторяйте шаги вместе с нами!
Чему вы научитесь
Преобразовывать сканированные чеки в структурированные данные с помощью LlamaParse и Pydantic-моделей
Проверять точность извлечения, сравнивая результаты с эталонной разметкой
Исправлять ошибки парсинга с помощью предобработки низкокачественных изображений
Экспортировать очищенные данные чеков в формат таблицы
Знакомство с LlamaIndex
LlamaIndex — это фреймворк, который соединяет LLM с вашими данными через три ключевые возможности:
Загрузка данных: встроенные ридеры для PDF, изображений, веб-страниц и баз данных автоматически преобразуют содержимое в обрабатываемые узлы.
Структурированное извлечение: преобразование неструктурированного текста в Pydantic-модели с автоматической валидацией на базе LLM.
Поиск и индексация: векторные хранилища и семантический поиск, позволяющие выполнять запросы с учётом контекста по вашим документам.
Он избавляет от шаблонного кода для загрузки, парсинга и запросов к данным, позволяя сосредоточиться на разработке LLM-приложений.
Ниже приведено сравнение LlamaIndex с двумя другими популярными фреймворками для LLM-приложений:
Framework |
Назначение |
Лучше всего подходит для |
|---|---|---|
LlamaIndex |
Загрузка документов и структурное извлечение |
Преобразование неструктурированных документов в данные, готовые к запросам |
LangChain |
Оркестрация LLM и интеграция инструментов |
Создание разговорных агентов с несколькими вызовами LLM |
LangGraph |
Управление состоянием рабочих процессов |
Координация долгих многоагентных процессов |
Установка
Для начала установите необходимые пакеты:
llama-index: базовый фреймворк LlamaIndex с функциями индексации и поиска
llama-parse: сервис парсинга документов для PDF, изображений и сложных макетов
llama-index-program-openai: интеграция с OpenAI для извлечения структурированных данных в формате Pydantic
python-dotenv: загрузка переменных окружения из .env-файлов
rapidfuzz: библиотека нечеткого сопоставления строк, например для сравнения названий компаний с мелкими отличиями
pip install llama-index llama-parse llama-index-program-openai python-dotenv rapidfuzz
Настройка окружения
Создайте файл .env для хранения API-ключей:
# .env
LLAMA_CLOUD_API_KEY="your-llama-parse-key"
OPENAI_API_KEY="your-openai-key"
Получить ключи можно здесь:
LlamaParse API: cloud.llamaindex.ai
OpenAI API: platform.openai.com/api-keys
Загрузите переменные окружения с помощью load_dotenv:
from dotenv import load_dotenv
import os
load_dotenv()
Настройте LLM по умолчанию через Settings:
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)
Settings.context_window = 8000
Settings хранит глобальные настройки, так что каждый движок запросов и программа используют одну и ту же конфигурацию LLM. Установка temperature = 0 заставляет модель возвращать детерминированный и структурированный вывод.
Базовая обработка изображений с LlamaParse
В этом туториале мы будем использовать SROIE Dataset v2 с Kaggle. Этот датасет содержит реальные сканы чеков из соревнования ICDAR 2019.
Загрузить датасет можно с сайта Kaggle или через CLI:
# Установите Kaggle CLI (один раз)
uv pip install kaggle
# Настройте учётные данные Kaggle (один раз на окружение)
export KAGGLE_USERNAME=your_username
export KAGGLE_KEY=your_api_key
# Создайте папку и скачайте архив (~1 ГБ)
mkdir -p data
kaggle datasets download urbikn/sroie-datasetv2 -p data
# Распакуйте и посмотрите несколько файлов
unzip -q -o data/sroie-datasetv2.zip -d data
Мы будем использовать данные из директории data/SROIE2019/train/, в которой есть:
img: оригинальные изображения чеков
entities: эталонная разметка для валидации
Загрузим первые 10 чеков в список путей:
from pathlib import Path
receipt_dir = Path("data/SROIE2019/train/img")
num_receipts = 10
receipt_paths = sorted(receipt_dir.glob("*.jpg"))[:num_receipts]
Посмотрим на первый чек:
from IPython.display import Image
first_receipt_path = receipt_paths[0]
Image(filename=first_receipt_path)

Теперь используем LlamaParse, чтобы преобразовать первый чек в markdown:
from llama_parse import LlamaParse
# Парсинг чеков с помощью LlamaParse
parser = LlamaParse(
api_key=os.environ["LLAMA_CLOUD_API_KEY"],
result_type="markdown", # формат вывода
num_workers=4, # параллельная обработка
language="en", # подсказка для OCR
skip_diagonal_text=True, # игнорировать наклонный текст
)
first_receipt = parser.load_data(first_receipt_path)[0]
Предпросмотр markdown для первого чека:
preview = "\n".join(first_receipt.text.splitlines()[:10])
print(preview)
Вывод:
tan woon yann
BOOK TA K (TAMAN DAYA) SDN BHD
789417-W
NO.5: 55,57 & 59, JALAN SAGU 18,
TAMAN DaYA,
81100 JOHOR BAHRU,
JOHOR.
LlamaParse успешно преобразует изображение чека в текст, но структуры здесь нет: названия компаний, даты и суммы перемешаны в сплошной текст. Такой формат неудобен для экспорта в таблицы или аналитические инструменты.
В следующем разделе мы будем использовать Pydantic-модели, чтобы автоматически извлекать структурированные поля вроде company, total и purchase_date.
Извлечение структурированных данных с помощью Pydantic
Pydantic — это Python-библиотека, которая использует аннотации типов для валидации данных и автоматического преобразования типов. Определив схему для чека один раз, вы сможете получать структурированные данные в едином формате, независимо от того, как именно выглядит исходный чек.
Начнём с описания двух Pydantic-моделей, которые отражают структуру чека:
from datetime import date
from typing import List, Optional
from pydantic import BaseModel, Field, ValidationInfo, model_validator
class ReceiptItem(BaseModel):
"""Представляет одну строку из чека."""
description: str = Field(description="Название товара точно так, как указано в чеке")
quantity: int = Field(default=1, ge=1, description="Количество товара (целое число)")
unit_price: Optional[float] = Field(
default=None, ge=0, description="Цена за единицу в валюте чека"
)
discount_amount: float = Field(
default=0.0, ge=0, description="Скидка, применённая к этой позиции"
)
class Receipt(BaseModel):
"""Структурированные поля, извлечённые из розничного чека."""
company: str = Field(description="Название компании или магазина")
purchase_date: Optional[date] = Field(
default=None, description="Дата в формате YYYY-MM-DD"
)
address: Optional[str] = Field(default=None, description="Адрес компании")
total: float = Field(description="Итоговая сумма к оплате")
items: List[ReceiptItem] = Field(default_factory=list)
Теперь создадим OpenAIPydanticProgram, который укажет LLM извлекать данные в соответствии с нашей моделью Receipt:
from llama_index.program.openai import OpenAIPydanticProgram
prompt = """
You are extracting structured data from a receipt.
Use the provided text to populate the Receipt model.
Interpret every receipt date as day-first.
If a field is missing, return null.
{context_str}
"""
receipt_program = OpenAIPydanticProgram.from_defaults(
output_cls=Receipt,
llm=Settings.llm,
prompt_template_str=prompt,
)
Проверим на первом документе, что всё работает, прежде чем запускать обработку всего набора:
# Обработка первого чека
structured_first_receipt = receipt_program(context_str=first_receipt.text)
# Выведем результат в JSON для удобства
print(structured_first_receipt.model_dump_json(indent=2))
Вывод:
{
"company": "tan woon yann BOOK TA K (TAMAN DAYA) SDN BHD",
"purchase_date": "2018-12-25",
"address": "NO.5: 55,57 & 59, JALAN SAGU 18, TAMAN DaYA, 81100 JOHOR BAHRU, JOHOR.",
"total": 9.0,
"items": [
{
"description": "KF MODELLING CLAY KIDDY FISH",
"quantity": 1,
"unit_price": 9.0,
"discount_amount": 0.0
}
]
}
LlamaIndex заполняет схему Pydantic извлечёнными значениями:
company: название компании из заголовка чека
purchase_date: распознанная дата (2018-12-25)
total: итоговая сумма (9.0)
items: список позиций с названием, количеством и ценой
Теперь, когда извлечение работает, масштабируем процесс на все чеки. Функция ниже использует имя файла чека как уникальный идентификатор:
def extract_documents(paths: List[str], prompt: str, id_column: str = "receipt_id") -> List[dict]:
"""Извлечение структурированных данных из документов с помощью LlamaParse и LLM."""
results: List[dict] = []
# Инициализация парсера с параметрами OCR
parser = LlamaParse(
api_key=os.environ["LLAMA_CLOUD_API_KEY"],
result_type="markdown",
num_workers=4,
language="en",
skip_diagonal_text=True,
)
# Конвертация изображений в markdown
documents = parser.load_data(paths)
# Создание программы для структурированного извлечения
program = OpenAIPydanticProgram.from_defaults(
output_cls=Receipt,
llm=Settings.llm,
prompt_template_str=prompt,
)
# Извлечение данных из каждого документа
for path, doc in zip(paths, documents):
document_id = Path(path).stem
parsed_document = program(context_str=doc.text)
results.append(
{
id_column: document_id,
"data": parsed_document,
}
)
return results
# Извлечение данных из всех чеков
structured_receipts = extract_documents(receipt_paths, prompt)
Далее преобразуем извлечённые данные в DataFrame для удобного анализа:
import pandas as pd
def transform_receipt_columns(df: pd.DataFrame) -> pd.DataFrame:
"""Применить стандартные преобразования к колонкам DataFrame с чеками."""
df = df.copy()
df["company"] = df["company"].str.upper()
df["total"] = pd.to_numeric(df["total"], errors="coerce")
df["purchase_date"] = pd.to_datetime(
df["purchase_date"], errors="coerce", dayfirst=True
).dt.date
return df
def create_extracted_df(records: List[dict], id_column: str = "receipt_id") -> pd.DataFrame:
df = pd.DataFrame(
[
{
id_column: record[id_column],
"company": record["data"].company,
"total": record["data"].total,
"purchase_date": record["data"].purchase_date,
}
for record in records
]
)
return transform_receipt_columns(df)
extracted_df = create_extracted_df(structured_receipts)
extracted_df
receipt_id |
company |
total |
purchase_date |
|---|---|---|---|
X00016469612 |
TAN WOON YANN BOOK TA K (TAMAN DAYA) SDN BHD |
9 |
2018-12-25 |
X00016469619 |
INDAH GIFT & HOME DECO |
60.3 |
2018-10-19 |
X00016469620 |
MR D.I.Y. (JOHOR) SDN BHD |
33.9 |
2019-01-12 |
X00016469622 |
YONGFATT ENTERPRISE |
80.9 |
2018-12-25 |
X00016469623 |
MR D.I.Y. (M) SDN BHD |
30.9 |
2018-11-18 |
X00016469669 |
ABC HO TRADING |
31 |
2019-01-09 |
X00016469672 |
SOON HUAT MACHINERY ENTERPRISE |
327 |
2019-01-11 |
X00016469676 |
S.H.H. MOTOR (SUNGAI RENGIT SN. BHD. (801580-T) |
20 |
2019-01-23 |
X51005200938 |
TH MNAN |
0 |
2023-10-11 |
X51005230617 |
GERBANG ALAF RESTAURANTS SDN BHD |
26.6 |
2018-01-18 |
Большинство чеков распознаны корректно, но у чека X51005200938 есть проблемы:
Название компании неполное («TH MNAN»)
Итоговая сумма равна 0, хотя фактически другая
Дата (2023-10-11) выглядит недостоверной
Сравнение извлечённых данных с эталоном
Чтобы проверить точность извлечения, загрузите эталонную разметку из data/SROIE2019/train/entities:
def normalize_date(value: str) -> str:
"""Привести строку даты к единому формату."""
value = (value or "").strip()
if not value:
return value
# Заменяем дефисы на слэши
value = value.replace("-", "/")
parts = value.split("/")
# Преобразуем 2-значный год в 4-значный (например, 18 -> 2018)
if len(parts[-1]) == 2:
parts[-1] = f"20{parts[-1]}"
return "/".join(parts)
def create_ground_truth_df(
label_paths: List[str], id_column: str = "receipt_id"
) -> pd.DataFrame:
"""Создать DataFrame с эталонными данными из JSON-файлов разметки."""
records = []
# Загружаем каждый JSON-файл и извлекаем ключевые поля
for path in label_paths:
payload = pd.read_json(Path(path), typ="series").to_dict()
records.append(
{
id_column: Path(path).stem,
"company": payload.get("company"),
"total": payload.get("total"),
"purchase_date": normalize_date(payload.get("date")),
}
)
df = pd.DataFrame(records)
# Применяем те же преобразования, что и к извлечённым данным
return transform_receipt_columns(df)
# Загружаем эталонную разметку
label_dir = Path("data/SROIE2019/train/entities")
label_paths = sorted(label_dir.glob("*.txt"))[:num_receipts]
ground_truth_df = create_ground_truth_df(label_paths)
ground_truth_df
receipt_id |
company |
total |
purchase_date |
|---|---|---|---|
X00016469612 |
BOOK TA .K (TAMAN DAYA) SDN BHD |
9 |
2018-12-25 |
X00016469619 |
INDAH GIFT & HOME DECO |
60.3 |
2018-10-19 |
X00016469620 |
MR D.I.Y. (JOHOR) SDN BHD |
33.9 |
2019-01-12 |
X00016469622 |
YONGFATT ENTERPRISE |
80.9 |
2018-12-25 |
X00016469623 |
MR D.I.Y. (M) SDN BHD |
30.9 |
2018-11-18 |
X00016469669 |
ABC HO TRADING |
31 |
2019-01-09 |
X00016469672 |
SOON HUAT MACHINERY ENTERPRISE |
327 |
2019-01-11 |
X00016469676 |
S.H.H. MOTOR (SUNGAI RENGИТ) SDN. BHD. |
20 |
2019-01-23 |
X51005200938 |
PERNIAGAAN ZHENG HUI |
112.45 |
2018-02-12 |
X51005230617 |
GERBANG ALAF RESTAURANTS SDN BHD |
26.6 |
2018-01-18 |
Проверим точность извлечения, сравнив результаты с эталоном.
Названия компаний часто отличаются незначительно (пробелы, пунктуация, лишние символы), поэтому применим нечёткое сопоставление, чтобы сгладить такие различия в форматировании.
from rapidfuzz import fuzz
def fuzzy_match_score(text1: str, text2: str) -> int:
"""Вычислить метрику сходства для двух строк (fuzzy matching)."""
return fuzz.token_set_ratio(str(text1), str(text2))
Проверим нечёткое сопоставление на примерах названий компаний:
# Почти идентичные строки — высокий балл
print(f"Score: {fuzzy_match_score('BOOK TA K SDN BHD', 'BOOK TA .K SDN BHD'):.2f}")
# Другая пунктуация — совпадение всё ещё неплохое
print(f"Score: {fuzzy_match_score('MR D.I.Y. JOHOR', 'MR DIY JOHOR'):.2f}")
# Полностью разные строки — низкий балл
print(f"Score: {fuzzy_match_score('ABC TRADING', 'XYZ COMPANY'):.2f}")
Вывод:
Score: 97.14
Score: 55.17
Score: 27.27
Теперь напишем функцию сравнения, которая объединяет извлечённые данные с эталоном и применяет нечёткое сопоставление для названия компании и точное — для числовых полей:
def compare_receipts(
extracted_df: pd.DataFrame,
ground_truth_df: pd.DataFrame,
id_column: str,
fuzzy_match_cols: List[str],
exact_match_cols: List[str],
fuzzy_threshold: int = 80,
) -> pd.DataFrame:
"""Сравнить извлечённые данные с эталоном по заданным столбцам."""
comparison_df = extracted_df.merge(
ground_truth_df,
on=id_column,
how="inner",
suffixes=("_extracted", "_truth"),
)
# Нечёткое сопоставление
for col in fuzzy_match_cols:
extracted_col = f"{col}_extracted"
truth_col = f"{col}_truth"
comparison_df[f"{col}_score"] = comparison_df.apply(
lambda row: fuzzy_match_score(row[extracted_col], row[truth_col]),
axis=1,
)
comparison_df[f"{col}_match"] = comparison_df[f"{col}_score"] >= fuzzy_threshold
# Точное совпадение
for col in exact_match_cols:
extracted_col = f"{col}_extracted"
truth_col = f"{col}_truth"
comparison_df[f"{col}_match"] = (
comparison_df[extracted_col] == comparison_df[truth_col]
)
return comparison_df
comparison_df = compare_receipts(
extracted_df,
ground_truth_df,
id_column="receipt_id",
fuzzy_match_cols=["company"],
exact_match_cols=["total", "purchase_date"],
)
Посмотрим строки, где не совпали название компании, сумма или дата покупки:
def get_mismatch_rows(comparison_df: pd.DataFrame) -> pd.DataFrame:
"""Получить строки с несовпадениями, исключив служебные колонки с признаками совпадения."""
# Столбцы c признаками совпадения и данные
match_columns = [col for col in comparison_df.columns if col.endswith("_match")]
data_columns = sorted([col for col in comparison_df.columns if col.endswith("_extracted") or col.endswith("_truth")])
# Строки, где не все совпадения True
has_mismatch = comparison_df[match_columns].all(axis=1).eq(False)
return comparison_df[has_mismatch][data_columns]
mismatch_df = get_mismatch_rows(comparison_df)
mismatch_df
company_extracted |
company_truth |
purchase_date_extracted |
purchase_date_truth |
total_extracted |
total_truth |
|---|---|---|---|---|---|
TH MNAN |
PERNIAGAAN ZHENG HUI |
2023-10-11 |
2018-02-12 |
0 |
112.45 |
Это подтверждает наши наблюдения. Все чеки совпадают с эталонной разметкой, кроме чека с ID X51005200938, где расходятся следующие поля:
Название компании
Итоговая сумма
Дата покупки
Давайте разберём этот чек подробнее и попытаемся понять, в чём проблема.
import IPython.display as display
file_to_inspect = receipt_dir / "X51005200938.jpg"
display.Image(filename=file_to_inspect)

Этот чек выглядит меньше остальных в датасете, что может повлиять на читаемость OCR. В следующем разделе мы увеличим масштаб изображения, чтобы улучшить извлечение.
Обрабатываем изображения для более точного извлечения
Создайте функцию для увеличения масштаба изображения:
from PIL import Image
def scale_image(image_path: Path, output_dir: Path, scale_factor: int = 3) -> Path:
"""Увеличить изображение с использованием высококачественного ресемплинга.
Args:
image_path: путь к исходному изображению
output_dir: директория для сохранения увеличенного изображения
scale_factor: во сколько раз увеличить изображение (по умолчанию 3x)
Returns:
Путь к увеличенному изображению
"""
# Загружаем изображение
img = Image.open(image_path)
# Увеличиваем изображение с качественным ресемплингом
new_size = (img.width * scale_factor, img.height * scale_factor)
img_resized = img.resize(new_size, Image.Resampling.LANCZOS)
# Сохраняем в выходную директорию с тем же именем файла
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / image_path.name
img_resized.save(output_path, quality=95)
return output_path
Примените функцию к проблемному чеку:
problematic_receipt_path = receipt_dir / "X51005200938.jpg"
adjusted_receipt_dir = Path("data/SROIE2019/train/img_adjusted")
scaled_image_path = scale_image(problematic_receipt_path, adjusted_receipt_dir, scale_factor=3)
Извлечём структурированные данные из увеличенного изображения:
problematic_structured_receipts = extract_documents([scaled_image_path], prompt)
problematic_extracted_df = create_extracted_df(problematic_structured_receipts)
problematic_extracted_df
receipt_id |
company |
total |
purchase_date |
|
|---|---|---|---|---|
0 |
X51005200938 |
PERNIAGAAN ZHENG HUI |
112.46 |
2018-02-12 |
Отлично! Масштабирование исправило извлечение. Название компании и дата покупки распознаны верно. Итоговая сумма 112.46 против 112.45 приемлема, так как на распечатанном чеке 112.45 действительно выглядит как 112.46.
Экспорт очищенных данных в CSV или Excel
Примените масштабирование ко всем чекам. Скопируйте оставшиеся изображения в директорию с обработанными файлами, исключив уже увеличенный чек:
import shutil
clean_receipt_paths = [scaled_image_path]
# Copy all receipts except the already processed one
for receipt_path in receipt_paths:
if receipt_path != problematic_receipt_path: # Skip the already scaled image
output_path = adjusted_receipt_dir / receipt_path.name
shutil.copy2(receipt_path, output_path)
clean_receipt_paths.append(output_path)
print(f"Copied {receipt_path.name}")
Запустим пайплайн снова с обработанными изображениями:
clean_structured_receipts = extract_documents(clean_receipt_paths, prompt)
clean_extracted_df = create_extracted_df(clean_structured_receipts)
clean_extracted_df
Результат:
receipt_id |
company |
total |
purchase_date |
|
|---|---|---|---|---|
0 |
X51005200938 |
PERNIAGAAN ZHENG HUI |
112.46 |
2018-02-12 |
1 |
X00016469612 |
TAN WOON YANN |
9 |
2018-12-25 |
2 |
X00016469619 |
INDAH GIFT & HOME DECO |
60.3 |
2018-10-19 |
3 |
X00016469620 |
MR D.I.Y. (JOHOR) SDN BHD |
33.9 |
2019-01-12 |
4 |
X00016469622 |
YONGFATT ENTERPRISE |
80.9 |
2018-12-25 |
5 |
X00016469623 |
MR D.I.Y. (M) SDN BHD |
30.9 |
2018-11-18 |
6 |
X00016469669 |
ABC HO TRADING |
31 |
2019-01-09 |
7 |
X00016469672 |
SOON HUAT MACHINERY ENTERPRISE |
327 |
2019-01-11 |
8 |
X00016469676 |
S.H.H. MOTOR (SUNGAI RENGIT SN. BHD. (801580-T) |
20 |
2019-01-23 |
9 |
X51005230617 |
GERBANG ALAF RESTAURANTS SDN BHD |
26.6 |
2018-01-18 |
Отлично! Теперь все чеки совпадают с эталонной разметкой.
Теперь можно экспортировать датасет в таблицу буквально за пару строк кода:
import pandas as pd
# Export to CSV
output_path = Path("reports/receipts.csv")
output_path.parent.mkdir(parents=True, exist_ok=True)
clean_extracted_df.to_csv(output_path, index=False)
print(f"Exported {len(clean_extracted_df)} receipts to {output_path}")
Вывод:
Exported 10 receipts to reports/receipts.csv
Экспортированные данные теперь можно импортировать в табличные редакторы, аналитические инструменты или BI-платформы.
Ускоряем обработку с помощью асинхронного параллельного выполнения
LlamaIndex поддерживает асинхронную обработку для параллельной работы с несколькими чеками. Используя async/await и метод aget_nodes_from_documents(), вы можете обрабатывать чеки параллельно, а не последовательно, заметно сокращая общее время.
Вот как изменить функцию извлечения для асинхронной обработки. Параметр num_workers=10 означает, что парсер будет обрабатывать до 10 чеков одновременно:
import asyncio
async def extract_documents_async(
paths: List[str], prompt: str, id_column: str = "receipt_id"
) -> List[dict]:
"""Асинхронное извлечение структурированных данных из документов с помощью LlamaParse."""
results: List[dict] = []
parser = LlamaParse(
api_key=os.environ["LLAMA_CLOUD_API_KEY"],
result_type="markdown",
num_workers=10, # Обрабатывать до 10 чеков параллельно
language="en",
skip_diagonal_text=True,
)
# Асинхронная загрузка документов для параллельной обработки
documents = await parser.aload_data(paths)
program = OpenAIPydanticProgram.from_defaults(
output_cls=Receipt,
llm=Settings.llm,
prompt_template_str=prompt,
)
for path, doc in zip(paths, documents):
document_id = Path(path).stem
parsed_document = program(context_str=doc.text)
results.append({id_column: document_id, "data": parsed_document})
return results
# Запуск через asyncio
structured_receipts = await extract_documents_async(receipt_paths, prompt)
Подробности смотрите в документации по асинхронному режиму LlamaIndex.
Попробуйте сами
Идеи из этого туториала оформлены как переиспользуемый пайплайн в этом репозитории на GitHub. В коде есть как синхронные, так и асинхронные версии:
Синхронные пайплайны (простая, последовательная обработка):
Универсальный пайплайн (
document_extraction_pipeline.py): повторно используемая функция извлечения, работающая с любой Pydantic-схемойПайплайн для чеков (
extract_receipts_pipeline.py): законченный пример со схемойReceipt, масштабированием изображений и преобразованиями данных
Асинхронные пайплайны (параллельная обработка с ускорением в 3–10 раз):
Асинхронный универсальный пайплайн (
async_document_extraction_pipeline.py): параллельная обработка документовАсинхронный пайплайн для чеков (
async_extract_receipts_pipeline.py): пакетная обработка чеков с отслеживанием прогресса
Запустите пример извлечения чеков:
# Synchronous version (simple, sequential)
uv run extract_receipts_pipeline.py
# Asynchronous version (parallel processing, 3-10x faster)
uv run async_extract_receipts_pipeline.py
Либо создайте свой собственный экстрактор, импортировав extract_structured_data() и передав свою Pydantic-схему, промпт для извлечения и при необходимости функции предобработки.
Русскоязычное сообщество про Python

Друзья! Эту статью подготовила команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!
Заключение и следующие шаги
В этом туториале вы увидели, как LlamaIndex автоматизирует извлечение данных из чеков при минимальном количестве кода. Вы преобразовали отсканированные изображения в структурированные данные, проверили результаты по эталонной разметке и экспортировали «чистый» CSV, готовый к анализу.
Идеи для улучшения пайплайна извлечения чеков:
Более богатые схемы: добавьте вложенные Pydantic-модели для деталей вендора, способов оплаты и построчных позиций
Правила валидации: помечайте выбросы (например, суммы свыше $500 или будущие даты) для ручной проверки
Многостадийные workflows: соберите пользовательский workflow, объединяющие предобработку изображений, извлечение, валидацию и экспорт с обработкой ошибок
Комментарии (2)

Wiggin2014
13.10.2025 10:57я примерно такое делаю на стеке aws для чеков на иврите. без этого фреймворка - все сам. юз кейс такой : юзер фоткает чек в бот телеги - система распознает, классифицирует и сохраняет. Потом юзер в том же боте может спрашивать инсайты, типа сколько я потратил на спиртное в прошлом месяце или где молоко самое дешевое. И система (псевдо РАГ) отдает ответы.
кусок README.md
Architecture
Core Components
Producer Lambda (
telegram_bot_handler.py) - Handles Telegram webhook and queues messagesConsumer Lambda (
consumer_handler.py) - Processes SQS messages via OrchestratorServiceOrchestrator Service - Routes messages by type (photo/text/command) and coordinates processing
PostgreSQL Database - Stores receipt data and analysis results
S3 Bucket - Stores receipt images
SQS FIFO Queue - Message queue for asynchronous processing with deduplication
API Gateway HTTP API - Webhook endpoint for Telegram
CloudWatch - Monitoring, alarms, and centralized logging
Processing Flow
Webhook Reception: Producer Lambda receives Telegram updates
Message Queuing: Messages queued to SQS with deduplication
Message Processing: Consumer Lambda processes via OrchestratorService
Document Processing: Multi-strategy approach (LLM/OCR+LLM/Enhanced+OCR+LLM)
Data Validation: Pydantic schema validation and storage
User Response: Formatted results sent back via Telegram API
Services
Orchestrator Service - Main message routing and processing coordination
Receipt Service - End-to-end receipt processing workflow
Document Processor Service - Hybrid OCR/LLM document analysis with strategy pattern
Query Service - Natural language query processing with filter-based retrieval
LLM Service - AI-powered text analysis and structured output generation
Message Queue Service - SQS message queuing for asynchronous processing
Telegram Service - Bot communication and file handling
Storage Service - Database operations and data persistence
Features
Receipt Processing
Supports Israeli receipts in Hebrew
OCR using Google Vision API or AWS Textract
LLM analysis using AWS Bedrock (Claude Sonnet 4) or OpenAI GPT models
Automatic categorization using predefined taxonomy system
Multi-image receipt support with album processing and image stitching
Advanced image preprocessing (deskewing, enhancement, grayscale conversion)
Pydantic-based data validation and schema enforcement
Receipt limits per user (100 receipts maximum)
Support for various payment methods and currencies
Processing Modes
LLM Mode: Direct image analysis using vision-enabled LLMs
OCR+LLM Mode: OCR text extraction followed by LLM structuring
Preprocessed+OCR+LLM Mode: Image enhancement + OCR + LLM analysis
Deployment & Infrastructure
Multi-stage deployment (dev/prod)
AWS CDK Infrastructure as Code
Docker-based Lambda functions with shared image
GitHub Actions CI/CD pipeline
CloudWatch monitoring with custom alarms
Dead letter queue for failed message handling
Так вот уперся в проблему длинных чеков (у нас бывают чеки по полметра длиной). И, оказалось, что никто не может точно склеить фотку 2-х половин чека. Пробовал и разные ллмы (буквально всех топов), и opencv, и pillow ... Всегда косяки... на том и застрял...
ASenchenko
Выглядит круто.
А практическое применение этой системы в РФ Вы как видите ?
Ну просто с учётом наличия
https://habr.com/ru/articles/358966/