Извлечение данных и OCR

Извлечение таблиц из PDF, OCR отсканированных документов и структурирование данных через Python в песочнице: распознавание текста, разбор счетов и актов, выгрузка результатов в CSV или Excel.

Системный промпт

Извлечение данных и OCR

Этот skill содержит паттерны Python-кода для извлечения данных из документов в песочнице. Загруженные файлы доступны в /tmp/.


Доступные библиотеки

Всё перечисленное уже стоит в образе песочницы — pip install в ячейке не нужен:

  • pdfplumber — извлечение таблиц и текста из PDF
  • pdf2image — конвертация PDF в изображения
  • Pillow — обработка изображений
  • openpyxl — создание Excel-файлов
  • pandas — обработка табличных данных

Набор образа фиксирован, и pip install его не расширяет. Пакета вне списка так не добыть: у pytesseract, например, pip поставит только обёртку, а системного бинаря tesseract в образе нет — вызов упадёт TesseractNotFoundError уже после «успешной» установки. Распознавание сканов делает платформа, а не песочница — БЛОК 2.


БЛОК 1: Извлечение таблиц из PDF

Паттерн: Извлечение всех таблиц из PDF

import pdfplumber
import pandas as pd
import base64
from io import BytesIO

pdf_bytes = base64.b64decode(pdf_content)

tables = []
with pdfplumber.open(BytesIO(pdf_bytes)) as pdf:
    for i, page in enumerate(pdf.pages):
        page_tables = page.extract_tables()
        for j, table in enumerate(page_tables):
            if table:
                df = pd.DataFrame(table[1:], columns=table[0])
                df["_page"] = i + 1
                df["_table"] = j + 1
                tables.append(df)

if tables:
    result = pd.concat(tables, ignore_index=True)
    result.to_excel(cw.output_path("extracted_tables.xlsx"), index=False)
    print(f"Извлечено таблиц: {len(tables)}")
else:
    print("Таблицы не найдены")

Паттерн: Извлечение таблицы с конкретной страницы

import pdfplumber
import pandas as pd
import base64
from io import BytesIO

pdf_bytes = base64.b64decode(pdf_content)
target_page = 1  # номер страницы (начиная с 1)

with pdfplumber.open(BytesIO(pdf_bytes)) as pdf:
    page = pdf.pages[target_page - 1]
    tables = page.extract_tables()

    if tables:
        # Берём первую таблицу со страницы
        table = tables[0]
        df = pd.DataFrame(table[1:], columns=table[0])
        df.to_excel(cw.output_path("table_page_{}.xlsx").format(target_page), index=False)
        print(f"Таблица извлечена: {len(df)} строк")
    else:
        print("Таблицы на странице не найдены")

Паттерн: Извлечение с настройками таблицы

import pdfplumber
import pandas as pd
import base64
from io import BytesIO

pdf_bytes = base64.b64decode(pdf_content)

# Настройки для сложных таблиц
table_settings = {
    "vertical_strategy": "lines",      # или "text", "explicit"
    "horizontal_strategy": "lines",    # или "text", "explicit"
    "snap_tolerance": 3,
    "join_tolerance": 3,
    "edge_min_length": 3,
}

with pdfplumber.open(BytesIO(pdf_bytes)) as pdf:
    all_data = []
    for page in pdf.pages:
        tables = page.extract_tables(table_settings)
        for table in tables:
            if table and len(table) > 1:
                df = pd.DataFrame(table[1:], columns=table[0])
                all_data.append(df)

    if all_data:
        result = pd.concat(all_data, ignore_index=True)
        result.to_excel(cw.output_path("extracted.xlsx"), index=False)
        print(f"Извлечено строк: {len(result)}")

Паттерн: Извлечение таблицы в несколько листов Excel

import pdfplumber
import pandas as pd
import base64
from io import BytesIO

pdf_bytes = base64.b64decode(pdf_content)

with pd.ExcelWriter(cw.output_path("tables_by_page.xlsx"), engine="openpyxl") as writer:
    with pdfplumber.open(BytesIO(pdf_bytes)) as pdf:
        for i, page in enumerate(pdf.pages):
            tables = page.extract_tables()
            for j, table in enumerate(tables):
                if table and len(table) > 1:
                    df = pd.DataFrame(table[1:], columns=table[0])
                    sheet_name = f"Page{i+1}_Table{j+1}"[:31]  # Excel ограничение
                    df.to_excel(writer, sheet_name=sheet_name, index=False)
                    print(f"Добавлен лист: {sheet_name}")

print("Все таблицы сохранены")

БЛОК 2: Сканы и изображения

Распознаёт платформа, а не код в ячейке. pytesseract в песочнице нет и добыть его нельзя: pip кладёт только обёртку, системного бинаря tesseract в образе не существует, и вызов упадёт TesseractNotFoundError уже после «успешной» установки. Ниже — маршрут, который работает.

Шаг 1: проверь, распознано ли уже

Загруженные .png/.jpg/.jpeg распознаются ДО того, как файл попадает в песочницу, а из текстового PDF текст извлекается напрямую. Смотри панель документов прежде, чем запускать OCR:

for d in documents:
    print(d["filename"], d.get("char_count"), repr((d.get("text") or "")[:200]))

Текст на месте — распознавать нечего, иди сразу в БЛОК 4 (структурирование).

Шаг 2: скан — инструментом analyze, не ячейкой

Пусто или PDF-скан — вызывай инструмент:

analyze(kind="ocr", document_ids=[documents[0]["id"]])
  • document_ids — UUID из documents[i]["id"] (предпочтительно, стабилен между сессиями) либо sandbox-путь из documents[i]["path"]. Путь не конструируй сам и не переноси из прошлой сессии.
  • До 10 документов за вызов — распознаются параллельно.
  • pages=[1, 2, 3] — только для PDF и только когда документ один. Без pages PDF больше 50 страниц-сканов не примет: режь на диапазоны страниц.
  • Форматы: .png, .jpg, .jpeg, .pdf. Всё остальное — не картинка: текст оттуда читается в ячейке через cw.read_text(path).
  • Ответ: {"text": …, "text_length": …, "tokens": …}. Распознанный текст закрепляется за документом — повторный вызов на тот же документ вернёт сохранённое, не распознавая заново, а на следующем ходу текст уже лежит в documents[i]["text"].

Предобработка (контраст, бинаризация, шумодав) не нужна — это рецепт для tesseract, а не для распознавания, которое делает платформа. Координат блоков и рамок слов этот маршрут не отдаёт: нужен разбор геометрии страницы — задавай вопрос об изображении через analyze(kind="describe", question=…).

Шаг 3: разбор — уже в песочнице

Дальше работает обычный код: текст из ответа инструмента (или из documents[i]["text"]) обрабатывается паттернами БЛОКА 4 (реквизиты, суммы, даты) и БЛОКА 6 (экспорт в Excel).

text = documents[0]["text"]

rows = []
for line in text.splitlines():
    parts = [p for p in line.split("  ") if p.strip()]
    if len(parts) >= 3:
        rows.append([p.strip() for p in parts])

import pandas as pd
df = pd.DataFrame(rows)
df.to_excel(cw.output_path("ocr_lines.xlsx"), index=False)
print(f"Строк разобрано: {len(df)}")

БЛОК 3: Извлечение текста из PDF

Паттерн: Извлечение всего текста

import pdfplumber
import base64
from io import BytesIO

pdf_bytes = base64.b64decode(pdf_content)

all_text = []
with pdfplumber.open(BytesIO(pdf_bytes)) as pdf:
    for i, page in enumerate(pdf.pages):
        text = page.extract_text()
        if text:
            all_text.append(f"=== Страница {i+1} ===\n{text}")

full_text = "\n\n".join(all_text)

with open(cw.output_path("extracted_text.txt"), "w", encoding="utf-8") as f:
    f.write(full_text)

print(f"Извлечено страниц: {len(all_text)}")

Паттерн: Извлечение текста с сохранением форматирования

import pdfplumber
import base64
from io import BytesIO

pdf_bytes = base64.b64decode(pdf_content)

with pdfplumber.open(BytesIO(pdf_bytes)) as pdf:
    for i, page in enumerate(pdf.pages):
        # Извлекаем слова с координатами
        words = page.extract_words(
            keep_blank_chars=True,
            x_tolerance=3,
            y_tolerance=3
        )

        # Группируем по строкам (по y-координате)
        lines = {}
        for word in words:
            y = round(word["top"], 0)
            if y not in lines:
                lines[y] = []
            lines[y].append(word)

        # Сортируем строки
        for y in sorted(lines.keys()):
            line_words = sorted(lines[y], key=lambda w: w["x0"])
            line_text = " ".join(w["text"] for w in line_words)
            print(line_text)

БЛОК 4: Структурирование данных

Паттерн: Парсинг реквизитов организации

import re
import json

# text - извлечённый текст документа
text = extracted_text

patterns = {
    "inn": r"ИНН[:\s]*(\d{10,12})",
    "kpp": r"КПП[:\s]*(\d{9})",
    "ogrn": r"ОГРН[:\s]*(\d{13,15})",
    "name": r"(?:Наименование|Организация)[:\s]*([^\n]+)",
    "address": r"(?:Адрес|Юридический адрес)[:\s]*([^\n]+)",
    "phone": r"(?:Тел\.?|Телефон)[:\s]*([\d\s\-\+\(\)]+)",
    "email": r"[\w\.-]+@[\w\.-]+\.\w+",
    "bank_account": r"р/с[:\s]*(\d{20})",
    "bik": r"БИК[:\s]*(\d{9})",
}

result = {}
for field, pattern in patterns.items():
    match = re.search(pattern, text, re.IGNORECASE)
    if match:
        result[field] = match.group(1).strip() if match.lastindex else match.group(0).strip()

with open(cw.output_path("requisites.json"), "w", encoding="utf-8") as f:
    json.dump(result, f, ensure_ascii=False, indent=2)

print("Извлечённые реквизиты:")
for k, v in result.items():
    print(f"  {k}: {v}")

Паттерн: Извлечение сумм и дат

import re
import json
import sys

from document_dates import find_dates, find_contract_date, find_payment_term

text = extracted_text

amount_patterns = [
    r"(\d[\d\s]*[\d,\.]+)\s*(?:руб|рублей|₽|RUB)",
    r"(?:сумма|стоимость|цена)[:\s]*(\d[\d\s]*[\d,\.]+)",
    r"(?:итого|всего)[:\s]*(\d[\d\s]*[\d,\.]+)",
]

amounts = []
for pattern in amount_patterns:
    for match in re.finditer(pattern, text, re.IGNORECASE):
        amount_str = match.group(1).replace(" ", "").replace(",", ".")
        try:
            amounts.append(float(amount_str))
        except ValueError:
            pass

all_dates = find_dates(text)
contract_date = find_contract_date(text)
payment_term = find_payment_term(text)

result = {
    "amounts": sorted(set(amounts), reverse=True),
    "dates": [d["iso"] for d in all_dates],
    "dates_detailed": all_dates,
    "contract_date": contract_date,
    "payment_term": payment_term,
}

with open(cw.output_path("extracted_values.json"), "w", encoding="utf-8") as f:
    json.dump(result, f, ensure_ascii=False, indent=2)

print(f"Найдено сумм: {len(result['amounts'])}")
print(f"Найдено дат: {len(result['dates'])}")
print(f"Дата заключения: {contract_date['iso'] if contract_date else 'не найдена'}")

Паттерн: Дата заключения договора и срок оплаты

Скрипт document_dates.py в $SKILLS_ROOT/data_extraction_ru/scripts/ парсит даты во всех форматах русских договоров: "22" мая 2026, «22» мая 2026, 22 мая 2026 г., 22.05.2026, ISO 2026-05-22. Каждой дате присваивается тип (birthdate, passport, power_of_attorney, generic) по ближайшему ключевому слову — это отделяет дату заключения от дат рождения/паспорта/доверенности.

import sys
from document_dates import find_contract_date, find_payment_term, payment_deadline, RU_WEEKDAYS

contract_date = find_contract_date(text)
term = find_payment_term(text)

base_iso = contract_date["iso"]
deadline = payment_deadline(base_iso, term)

print(f"Дата заключения: {base_iso}")
print(f"Срок оплаты: {term['days']} {term['kind']} дней")
print(f"Крайний срок: {deadline} ({RU_WEEKDAYS[deadline.weekday()]})")

Для электронных договоров датой заключения обычно считается дата подписания ЭЦП последней стороной (она может отличаться от даты в шапке): тогда base_iso бери из даты ЭЦП, а не из find_contract_date. find_payment_term различает «рабочих» и «календарных» дней; payment_deadline считает рабочие дни через add_business_days.

Не форматируй даты через strftime с локалью. date.strftime("%d.%m.%Y", "ru") падает с TypeError: strftime() takes at most 1 argument (2 given) — у date/datetime метод принимает только строку формата. Русский день недели бери из RU_WEEKDAYS[d.weekday()], а не из strftime("%A").

Паттерн: Структурирование накладной/счёта

import re
import pandas as pd
import json

text = extracted_text

# Извлекаем номер и дату документа
doc_number = re.search(r"№\s*(\S+)", text)
doc_date = re.search(r"от\s*(\d{2}\.\d{2}\.\d{4})", text)

# Ищем табличные данные (товарные позиции)
# Типичный формат: № | Наименование | Ед.изм. | Кол-во | Цена | Сумма
line_pattern = r"(\d+)\s+(.+?)\s+(шт|кг|м|л|уп|компл)[.\s]*\s+(\d+[\d,\.]*)\s+(\d+[\d,\.]*)\s+(\d+[\d,\.]*)"

items = []
for match in re.finditer(line_pattern, text, re.IGNORECASE):
    items.append({
        "num": match.group(1),
        "name": match.group(2).strip(),
        "unit": match.group(3),
        "quantity": float(match.group(4).replace(",", ".")),
        "price": float(match.group(5).replace(",", ".").replace(" ", "")),
        "amount": float(match.group(6).replace(",", ".").replace(" ", "")),
    })

result = {
    "document_number": doc_number.group(1) if doc_number else None,
    "document_date": doc_date.group(1) if doc_date else None,
    "items": items,
    "total_amount": sum(item["amount"] for item in items),
}

# Сохраняем в JSON
with open(cw.output_path("invoice_data.json"), "w", encoding="utf-8") as f:
    json.dump(result, f, ensure_ascii=False, indent=2)

# И в Excel
if items:
    df = pd.DataFrame(items)
    df.to_excel(cw.output_path("invoice_items.xlsx"), index=False)

print(f"Извлечено позиций: {len(items)}")
print(f"Общая сумма: {result['total_amount']}")

БЛОК 5: Работа с входными файлами

Паттерн: Обработка загруженных файлов

import base64
from io import BytesIO

for file_input in files:
    name = file_input["name"]
    content = base64.b64decode(file_input["content_base64"])

    # Определяем тип файла
    if name.lower().endswith(".pdf"):
        file_type = "pdf"
    elif name.lower().endswith((".png", ".jpg", ".jpeg", ".tiff", ".bmp")):
        file_type = "image"
    elif name.lower().endswith((".xlsx", ".xls")):
        file_type = "excel"
    else:
        file_type = "unknown"

    print(f"Файл: {name}, тип: {file_type}, размер: {len(content)} байт")

Паттерн: Batch-обработка нескольких PDF

import pdfplumber
import pandas as pd
import base64
from io import BytesIO

all_tables = []

for file_input in files:
    if not file_input["name"].lower().endswith(".pdf"):
        continue

    content = base64.b64decode(file_input["content_base64"])

    with pdfplumber.open(BytesIO(content)) as pdf:
        for page in pdf.pages:
            tables = page.extract_tables()
            for table in tables:
                if table and len(table) > 1:
                    df = pd.DataFrame(table[1:], columns=table[0])
                    df["_source_file"] = file_input["name"]
                    all_tables.append(df)

if all_tables:
    result = pd.concat(all_tables, ignore_index=True)
    result.to_excel(cw.output_path("all_tables.xlsx"), index=False)
    print(f"Обработано файлов, извлечено таблиц: {len(all_tables)}")

Паттерн: несколько сканов сразу

Пачка идёт одним вызовом инструмента — до 10 документов, распознаются параллельно:

analyze(kind="ocr", document_ids=[d["id"] for d in documents if d["filename"].lower().endswith((".png", ".jpg", ".jpeg"))])

Больше десяти — режь на партии по 10. Собирать распознанное в ячейке уже не нужно: текст закреплён за каждым документом и на следующем ходу доступен как documents[i]["text"].

results = [
    {"file": d["filename"], "text": d.get("text") or "", "chars": len(d.get("text") or "")}
    for d in documents
]

with open(cw.output_path("ocr_results.txt"), "w", encoding="utf-8") as f:
    for r in results:
        f.write(f"=== {r['file']} ===\n{r['text']}\n\n")

print(f"Обработано файлов: {len(results)}")

БЛОК 6: Экспорт в Excel с форматированием

Паттерн: Экспорт структурированных данных

import pandas as pd
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.utils import get_column_letter

# data - извлечённые данные
df = pd.DataFrame(extracted_data)

wb = Workbook()
ws = wb.active
ws.title = "Извлечённые данные"

# Стили
header_font = Font(bold=True, size=11, color="FFFFFF")
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
thin_border = Border(
    left=Side(style="thin"),
    right=Side(style="thin"),
    top=Side(style="thin"),
    bottom=Side(style="thin")
)

# Записываем данные
for r_idx, row in enumerate(dataframe_to_rows(df, index=False, header=True), 1):
    for c_idx, value in enumerate(row, 1):
        cell = ws.cell(row=r_idx, column=c_idx, value=value)
        cell.border = thin_border

        if r_idx == 1:  # заголовок
            cell.font = header_font
            cell.fill = header_fill
            cell.alignment = Alignment(horizontal="center")

# Автоширина колонок
for col_idx, column in enumerate(df.columns, 1):
    max_length = max(len(str(column)), df[column].astype(str).map(len).max())
    ws.column_dimensions[get_column_letter(col_idx)].width = min(max_length + 2, 50)

wb.save(cw.output_path("formatted_data.xlsx"))
print("Данные экспортированы с форматированием")

Паттерн: Экспорт с несколькими листами

import pandas as pd

# Словарь с данными для разных листов
sheets_data = {
    "Реквизиты": pd.DataFrame([requisites]),
    "Позиции": pd.DataFrame(items),
    "Суммы": pd.DataFrame({"Сумма": amounts}),
}

with pd.ExcelWriter(cw.output_path("structured_export.xlsx"), engine="openpyxl") as writer:
    for sheet_name, df in sheets_data.items():
        if not df.empty:
            df.to_excel(writer, sheet_name=sheet_name, index=False)
            print(f"Добавлен лист: {sheet_name}")

print("Экспорт завершён")

БЛОК 7: Комбинированные сценарии

Задача: PDF скан → OCR → Таблица в Excel

Шаг 1 — распознавание инструментом (страницы можно сузить, документ при этом один):

analyze(kind="ocr", document_ids=[documents[0]["id"]])

Шаг 2 — разбор в ячейке:

import pandas as pd
import re

full_text = documents[0]["text"]

# Извлекаем табличные данные
# Адаптируйте паттерн под ваш документ
line_pattern = r"(\d+)\s+(.+?)\s+(\d+[\d,\.]*)\s+(\d+[\d,\.]*)"
rows = []
for match in re.finditer(line_pattern, full_text):
    rows.append({
        "№": match.group(1),
        "Наименование": match.group(2).strip(),
        "Количество": match.group(3),
        "Сумма": match.group(4),
    })

# Сохраняем в Excel
if rows:
    df = pd.DataFrame(rows)
    df.to_excel(cw.output_path("scanned_table.xlsx"), index=False)
    print(f"Извлечено строк: {len(rows)}")
else:
    # Сохраняем хотя бы текст
    with open(cw.output_path("ocr_text.txt"), "w", encoding="utf-8") as f:
        f.write(full_text)
    print("Таблица не найдена, сохранён текст OCR")

Задача: Извлечь реквизиты из нескольких PDF

import pdfplumber
import pandas as pd
import re
import base64
from io import BytesIO

all_requisites = []

for file_input in files:
    if not file_input["name"].lower().endswith(".pdf"):
        continue

    content = base64.b64decode(file_input["content_base64"])

    with pdfplumber.open(BytesIO(content)) as pdf:
        text = "\n".join(page.extract_text() or "" for page in pdf.pages)

    # Извлекаем реквизиты
    inn_match = re.search(r"ИНН[:\s]*(\d{10,12})", text)
    name_match = re.search(r"(?:ООО|ОАО|ЗАО|ПАО|АО|ИП)\s*[«\"]?([^»\"]+)[»\"]?", text)

    all_requisites.append({
        "Файл": file_input["name"],
        "ИНН": inn_match.group(1) if inn_match else "",
        "Наименование": name_match.group(0) if name_match else "",
    })

df = pd.DataFrame(all_requisites)
df.to_excel(cw.output_path("requisites_all.xlsx"), index=False)
print(f"Обработано файлов: {len(all_requisites)}")

БЛОК 8: Типичные команды пользователя

КомандаДействие
"Извлеки таблицы из PDF"БЛОК 1: Извлечение всех таблиц
"Распознай скан"БЛОК 2: сначала documents[i]['text'], затем analyze(kind="ocr")
"Вытащи текст из PDF"БЛОК 3: Извлечение текста
"Найди реквизиты в документе"БЛОК 4: Парсинг реквизитов
"Переведи скан в Excel"БЛОК 7: PDF скан → OCR → Excel
"Извлеки суммы и даты"БЛОК 4: Извлечение сумм и дат
"Обработай несколько файлов"БЛОК 5: Batch-обработка

Skill-файл содержит паттерны кода для извлечения данных из документов. Выполняй код через sandbox_bash.

Похожие навыки

Калькулятор с русской локальюПаттерны Python-кода для точных финансовых расчётов с форматированием по ГОСТ Р 54521-2011. Расчёт пеней, неустоек, процентов по ст. 395 ГК РФ, НДС, ключевой ставки ЦБ.Продуктовая спецификацияСоздание продуктовых спецификаций (ТЗ/PRD): от идеи до готового документа с целями, аудиторией, пользовательскими сценариями, функциональными требованиями и метриками успеха. Для команд без выделенного продакт-менеджера.OCR российских документовИзвлечение данных из сканов российских удостоверяющих документов (паспорт, СНИЛС, ИНН, водительское удостоверение, ОГРН/ОГРНИП) с валидацией по контрольным суммамTaste-Skill: премиум фронтенд-дизайнTaste-Skill — высокоуровневая дизайн-система для фронтенда. Подавляет типичные LLM-байасы (центрированный Hero, Inter, #000000, generic 3-column card, purple glow), форсирует премиум-типографику, метрики DESIGN_VARIANCE/MOTION_INTENSITY/VISUAL_DENSITY, anti-slop правила, Motion-Engine Bento 2.0. Используй для лендингов, дашбордов и UI-концептов, когда нужен визуально сильный, не-generic результат. Источник: github.com/Leonxlnx/taste-skill.Анализ CommerceML (обмен 1С с сайтом)Парсинг и анализ XML CommerceML 2.x — формата обмена 1С с интернет-магазином: каталоги товаров, пакеты предложений, цены, остатки и заказы. Сверка выгрузок и диагностика ошибок импорта.Банковские выписки и формат 1С-банкПарсинг банковских выписок в форматах 1С-банк (1CClientBankExchange), CSV/XLSX банков, извлечение операций, сверка оборотов
Платформа
Сам Решу

Попробуйте этот навык

Зарегистрируйтесь и используйте навык «Извлечение данных и OCR» бесплатно.