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

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

---

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

Всё перечисленное уже стоит в образе песочницы — `pip install` в ячейке не нужен:
- `pdfplumber` — извлечение таблиц и текста из PDF
- `pdf2image` — конвертация PDF в изображения
- `Pillow` — обработка изображений
- `openpyxl` — создание Excel-файлов
- `pandas` — обработка табличных данных

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

---

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

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

```python
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("Таблицы не найдены")
```

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

```python
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("Таблицы на странице не найдены")
```

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

```python
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

```python
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:

```python
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).

```python
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

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

```python
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)}")
```

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

```python
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: Структурирование данных

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

```python
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}")
```

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

```python
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`) по ближайшему ключевому слову — это отделяет дату заключения от дат рождения/паспорта/доверенности.

```python
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")`.

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

```python
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: Работа с входными файлами

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

```python
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

```python
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"]`.

```python
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 с форматированием

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

```python
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("Данные экспортированы с форматированием")
```

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

```python
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 — разбор в ячейке:

```python
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

```python
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.*
