Всем привет! Снова отыскала интересную статью на Medium, интересно услышать ваше мнение :)
Я создал этот инструмент, потому что устал от одних и тех же скучных кликов каждую неделю. Мне нужен был инструмент, который: отслеживает папку, извлекает данные из PDF, обогащает их, отправляет отчеты и, в идеале, позволяет выставлять кому-то счет за сэкономленное время. Два выходных, несколько библиотек и пачка кофе – и у меня был продукт, за который люди действительно платили.
Ниже я покажу точный технологический стек, архитектуру, методы монетизации и паттерны кода, которые я использовал. Вас ждет практический код, ООП-структура и один небольшой трюк с C++, когда чистого Python уже не хватало.
1. Выбирайте маленькую, но болезненную задачу
Большинство проектов по автоматизации умирают, потому что пытаются решить слишком много. Вместо этого выберите одну повторяющуюся «боль» с измеримым ROI. Моя проблема была такой:
Клиент ежедневно присылает счета (инвойсы) в виде разрозненных PDF.
Я вручную открываю их, извлекаю поставщика, дату, сумму и заношу в Google Sheet.
Трата: ≈ 20 минут в день, или ≈ 6 часов в месяц.
Цель: Свести это к нулю человеко-минут и предложить как платную услугу.
2. Быстрый MVP: Наблюдатель за файлами + PDF-экстрактор
Начните с малого: отслеживайте папку, обнаруживайте новый PDF, извлекайте текст. Используем watchdog и PyMuPDF (fitz).
pip install watchdog pymupdf
# file_watcher.py
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import fitz # pymupdf
class PDFHandler(FileSystemEventHandler):
"""Обработчик событий файловой системы."""
def on_created(self, event):
if event.src_path.lower().endswith(".pdf"):
print(f"[+] Новый PDF: {event.src_path}")
text = extract_text(event.src_path)
print(text[:200], "...\n") # быстрый превью
def extract_text(path: str) -> str:
"""Извлечение текстового слоя из PDF."""
doc = fitz.open(path)
pages = [page.get_text() for page in doc]
doc.close()
return "\n".join(pages)
if __name__ == "__main__":
observer = Observer()
handler = PDFHandler()
# Следим за папкой 'inbox'
observer.schedule(handler, path="./inbox", recursive=False)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
Этот единственный скрипт сократил мою ежедневную работу до 5 минут – в основном на проверку
3. Повышаем отказоустойчивость: OCR + текстовый фолбэк
Некоторые PDF – это просто сканированные изображения. Добавим фолбэк с помощью pytesseract.
pip install pytesseract pillow
# Внимание: tesseract должен быть установлен в системе (apt / brew / choco)
from PIL import Image
import pytesseract
import fitz
def extract_text_with_ocr(path: str) -> str:
"""Извлечение текста: сначала текстовый слой, затем OCR для сканов."""
doc = fitz.open(path)
aggregated = []
for page in doc:
text = page.get_text()
if text.strip():
# Если есть текстовый слой, используем его
aggregated.append(text)
else:
# Иначе делаем скриншот страницы и прогоняем через Tesseract
pix = page.get_pixmap(dpi=200)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
aggregated.append(pytesseract.image_to_string(img))
doc.close()
return "\n".join(aggregated)
Такой гибридный подход (текстовый слой → OCR) сделал инструмент надежным для 95% входящих счетов.
Если вы хотите превратить это в продукт, пайплайн должен быть модульным. Каждый шаг – это класс: Loader → Parser → Enricher → Sink. Это позволяет легко менять хранилище (Google Sheets, СУБД, вебхук) без переписывания логики.
# pipeline.py
from abc import ABC, abstractmethod
from typing import Dict
class Step(ABC):
"""Абстрактный класс для шага обработки."""
@abstractmethod
def run(self, data: Dict) -> Dict:
pass
class Loader(Step):
"""Загружает данные (извлекает текст)."""
def __init__(self, path): self.path = path
def run(self, data):
data['text'] = extract_text_with_ocr(self.path)
return data
class Parser(Step):
"""Извлекает ключевые поля (сумма, дата, поставщик)."""
def run(self, data):
text = data['text']
# Здесь будет реальная логика парсинга (regex/NLP)
data['vendor'] = find_vendor(text)
data['amount'] = find_amount(text)
return data
class Sink(Step):
"""Выгружает данные в целевую систему (например, Google Sheets)."""
def run(self, data):
push_to_google_sheet(data)
return data
class Pipeline:
"""Сборщик и исполнитель пайплайна."""
def __init__(self, steps: list[Step]):
self.steps = steps
def execute(self, initial: Dict):
data = initial
for step in self.steps:
data = step.run(data)
return data
Этот паттерн масштабируем: легко добавить ClassifierStep для определения языка или TranslatorStep для неанглоязычных документов.
Начинайте с детерминированного парсинга (regex). Если счета сложные, переходите к ML-моделям (например, spacy или layout-parser). Пример для извлечения суммы:
import re
# Регулярное выражение для поиска сумм с валютой (USD/EUR/$, с запятыми/точками)
AMOUNT_RE = re.compile(r"(?<!\d)(?:USD|EUR|\$)?\s?([\d]{1,3}(?:,\d{3})*(?:\.\d{2})?)\b")
def find_amount(text: str) -> float | None:
text = text.replace("\n", " ")
m = AMOUNT_RE.search(text)
if m:
# Удаляем разделители тысяч, чтобы корректно преобразовать в float
s = m.group(1).replace(',', '')
return float(s)
return None
Для повышения надежности используйте spacy + кастомный NER или библиотеки вроде layout-parser для пространственного обнаружения полей.
Если счета находятся за веб-дашбордами, автоматизируйте их загрузку с помощью Playwright.
pip install playwright
playwright install
from playwright.sync_api import sync_playwright
def login_and_download(url, user, password, download_path):
"""Автоматически логинится и скачивает файл."""
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url)
# Автоматическое заполнение полей
page.fill('#username', user)
page.fill('#password', password)
page.click('#login')
# Ожидание элемента и скачивание
page.wait_for_selector('a.download')
with page.expect_download() as download_info:
page.click('a.download')
download = download_info.value
download.save_as(download_path)
browser.close()
Это позволяет вашему сервису самостоятельно собирать исходные PDF – критически важно для подписки, где система должна сама получать документы клиента каждое утро.
Для удобства использования оберните функциональность в CLI-интерфейс с помощью Typer (или Click). Это позволяет не-разработчикам запускать его локально или упрощает развертывание на серверах.
pip install typer
# cli.py
import typer
from pipeline import Pipeline, Loader, Parser, Sink
app = typer.Typer()
@app.command()
def process(path: str):
"""Обрабатывает один файл по указанному пути."""
steps = [Loader(path), Parser(), Sink()]
p = Pipeline(steps)
p.execute({})
typer.echo(f"Файл {path} успешно обработан!")
if __name__ == "__main__":
app()
Опубликуйте проект на PyPI (с setup.py или pyproject.toml) или упакуйте в Docker-образ.
Для тяжелой обработки изображений или масштабного пре-процессинга для OCR Python может стать узким местом. У меня был шаг (кастомное преобразование изображения), который должен был обрабатывать тысячи страниц в день.
Я переписал этот единственный шаг на C++ и сделал его доступным из Python через pybind11 (можно использовать и Cython).
Суть подхода:
Написать тяжелую функцию на C++.
Обернуть ее с помощью pybind11 (для Python-интерфейса).
Импортировать скомпилированный модуль в Python как обычную библиотеку.
Этот микро-рефакторинг сократил время выполнения шага со ≈ 120 мс/страница до ≈ 10 мс/страница. Знайте, где замедляет Python, и изолируйте это место.
При росте числа пользователей обрабатывайте задачи в очередях воркеров, чтобы не блокировать основной поток.
pip install celery redis
# tasks.py
from celery import Celery
from pipeline import Pipeline, Loader, Parser, Sink
# Настройка Celery с Redis в качестве брокера
app = Celery('tasks', broker='redis://localhost:6379/0')
@app.task
def process_file(path):
"""Задача Celery для асинхронной обработки."""
steps = [Loader(path), Parser(), Sink()]
Pipeline(steps).execute({})
Как я превратил это в деньги:
Фриланс-контракты (ранняя выручка): Предложил автоматизацию обработки счетов нескольким местным клиентам. Быстрые победы, минимальная поддержка.
Цена за документ (Volume pricing): Тарификация за обработанный счет (например, $0.10–$0.50). Отлично для клиентов с большим объемом.
Ежемесячная подписка (SaaS): Хостинг сервиса, автоматический инжест (через Playwright или SFTP) и оплата за удобство + SLA.
White-label / Enterprise: Интеграция в существующую бухгалтерскую платформу.
Ключевые тактики, которые помогли конвертировать лиды:
Двухнедельный бесплатный пробный период: Обработка первых 50 счетов клиента бесплатно.
Прозрачный отчет о точности: Показываем, что было спарсено машиной, а что пришлось править вручную.
Human-in-the-loop (человек в контуре): Предлагаем ручную коррекцию для результатов с низкой уверенностью (дополнительный доход).
Используйте эту структуру в качестве основы для любого серьезного проекта по автоматизации:
invoice-automator/
├─ pyproject.toml
├─ README.md
├─ src/
│ ├─ automator/
│ │ ├─ __init__.py
│ │ ├─ cli.py # Точка входа CLI
│ │ ├─ pipeline.py # Основная логика ООП-пайплайна
│ │ ├─ loaders.py # Классы Loader (файл, загрузка, Playwright)
│ │ ├─ parsers.py # Regex / NLP парсеры
│ │ ├─ ocr.py # OCR-утилиты и фоллбэк
│ │ ├─ enrichers.py # Нормализация валют, поиск поставщиков
│ │ ├─ sinks.py # Google Sheets / DB / Webhook
│ │ └─ utils.py
│ └─ tests/
│ ├─ test_parsers.py
│ └─ test_pipeline.py
├─ docker/
│ ├─ Dockerfile
│ └─ prod-compose.yml
└─ infra/
└─ celery_worker.yml # Конфигурация воркера
Какие еще рутинные задачи вы хотели бы автоматизировать с помощью этого архитектурного шаблона?