2025, Oct 03 23:20

Почему OCR в PyScript ломается с Tesseract и как перенести распознавание на сервер

Почему OCR на PyScript с OpenCV и pytesseract не работает в браузере: ограничения песочницы, Tesseract и файлы. Решение — перенести распознавание на сервер.

Запустить локальный скрипт OCR, который сочетает OpenCV, pytesseract и pandas, просто; но перенос той же логики в браузер с PyScript часто срывается по неочевидным причинам. Узкие места обычно связаны с вызовом нативного бинарника Tesseract и чтением файлов из локальной файловой системы. Ниже — краткое пошаговое объяснение, почему исходный подход ломается в PyScript, что именно разрешает песочница браузера и как перестроить решение, чтобы распознавание текста стабильно работало с веб‑интерфейсом.

Минимальный пример: локально работает, в PyScript — нет

Этот фрагмент повторяет исходное поведение: он просматривает каталог с изображениями, распознаёт каждое с помощью Tesseract через pytesseract и укладывает результат в DataFrame. Имена переменных умышленно изменены, логика полностью сохранена.

# main.py
import pytesseract as ocr
ocr.pytesseract.tesseract_cmd = r"Tesseract-OCR\tesseract.exe"
import os
import cv2
import pandas as pd
image_bucket = []
def harvest_text(idx):
    img = cv2.imread(image_bucket[idx], 0)
    binarized = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
    raw = ocr.image_to_string(binarized, lang='eng', config='--psm 6')
    condensed = "\n".join([ln.rstrip() for ln in raw.splitlines() if ln.strip()])
    return condensed.split('\n')
def run_batch():
    base_dir = f'SQL_NOTES\\'
    entries = os.listdir(base_dir)
    for entry in entries:
        if entry.startswith("imagename"):
            image_bucket.append(base_dir + entry)
    idx_map = dict({0: 'image_bucket[0]', 1: 'image_bucket[1]', 2: 'image_bucket[2]', 3: 'image_bucket[3]'})
    rows = ([harvest_text(k) for k in idx_map])
    return pd.DataFrame(rows).T
print(run_batch())

В браузере вызов организуется через PyScript, как показано ниже.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Empty Grass</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="stylesheet" href="https://pyscript.net/releases/2025.7.3/core.css">
    <script type="module" src="https://pyscript.net/releases/2025.7.3/core.js"></script>
</head>
<body>
<button id="go_ocr">Run</button>
<script type="py" config="./pyscript.toml" terminal>
from pyscript import when
@when("click", "#go_ocr")
def on_go(evt):
    from main import run_batch
    run_batch()
</script>
</body>
</html>
# pyscript.toml
packages = [ "pytesseract", "opencv-python", "pandas" ]
[files]
"main.py" = "main.py"

Что именно ломается и почему

Корень проблемы — модель безопасности браузера. PyScript работает внутри браузера и наследует те же правила песочницы, что и JavaScript. Это затрагивает и внешние бинарники, и файловую систему.

PyScript не может запускать внешние .exe — браузеры запрещают это из соображений безопасности.

На практике строка, указывающая pytesseract на локальный исполняемый файл, в PyScript работать не будет. Из браузера нельзя порождать нативные процессы вроде Tesseract. Те же ограничения касаются нативных бинарников, на которых завязаны пакеты Python — включая OpenCV и сам pytesseract.

Доступ к файловой системе тоже ограничён. Читать локальные каталоги через os.listdir нельзя. В документации PyScript описана виртуальная файловая система и варианты монтирования. В некоторых браузерах на базе Chromium можно смонтировать локальную папку через пользовательский диалог с помощью fs.mount, но даже в этом случае запуск локальных .exe всё равно недоступен. Документация: https://docs.pyscript.net/2025.8.1/user-guide/filesystem/

Вывод однозначен: браузер не может свободно обходить ваши диски и не имеет права запускать нативные исполняемые файлы. Две безобидные на вид строки — присвоение tesseract_cmd и обход каталога — в PyScript неприменимы.

Практичное решение: перенести OCR на сервер

Оптимальный путь — оставить интерфейс в браузере, а выполнение OCR перенести на веб‑сервер. Браузер отправляет данные изображения, сервер запускает Tesseract и возвращает результат. В роли серверной части подойдут Flask или FastAPI. Ещё проще — воспользоваться фреймворком, который из коробки связывает UI и Python‑бекенд. В примере ниже используется NiceGUI: та же логика OCR выполняется на сервере, а результат — DataFrame из pandas — отображается в виде таблицы.

# server_app.py
from nicegui import ui
import os
import cv2
import pytesseract
import pandas as pd
# При необходимости укажите путь к Tesseract так же, как делали локально
# pytesseract.pytesseract.tesseract_cmd = r"Tesseract-OCR\tesseract.exe"
def parse_image(img_path):
    gray = cv2.imread(img_path, 0)
    mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
    text = pytesseract.image_to_string(mask, lang='eng', config='--psm 6')
    compact = "\n".join([ln.rstrip() for ln in text.splitlines() if ln.strip()])
    return compact.split('\n')
def build_df():
    folder = 'SQL_NOTES'
    folder = '.'  # папка с изображениями
    collected = []
    for name in os.listdir(folder):
        if name.startswith("image"):
            collected.append(os.path.join(folder, name))
    records = [parse_image(p) for p in collected]
    return pd.DataFrame(records).T
def on_press(e):
    df = build_df()
    status_label.set_text(df.to_string())
    ui.table.from_pandas(df)
ui.button("Press to run", on_click=on_press)
status_label = ui.label("Waiting for result...")
ui.run()

Если удобнее принимать файлы от пользователя, а не сканировать серверную папку, в NiceGUI есть элемент загрузки. OCR по‑прежнему выполняется на сервере, а результаты возвращаются на страницу.

# server_upload_app.py
from nicegui import ui
import cv2
import pytesseract
import pandas as pd
import numpy as np
def on_upload(e):
    payload = e.content.read()
    arr = np.frombuffer(payload, np.uint8)
    mat = cv2.imdecode(arr, 0)
    mask = cv2.threshold(mat, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
    text = pytesseract.image_to_string(mask, lang='eng', config='--psm 6')
    compact = "\n".join([ln.rstrip() for ln in text.splitlines() if ln.strip()])
    rows = [compact.split('\n')]
    df = pd.DataFrame(rows).T
    outcome.set_text(df.to_string())
    ui.table.from_pandas(df)
ui.upload(on_upload=on_upload)
outcome = ui.label("Waiting for result...")
ui.run()

Почему это важно

Понимание ограничений песочницы экономит время и избавляет от хрупких костылей. PyScript наследует правила JavaScript: нет произвольного доступа к локальным файлам и запрет на запуск нативных программ со страницы. И хотя Chromium‑браузеры могут предоставлять доступ к выбранной пользователем папке через монтирование, это не отменяет запрета на исполнение локальных программ. Поскольку OCR зависит от Tesseract и нативных расширений, его место — на сервере, когда вы строите веб‑решение.

Итоги и напоминания

Держите среду выполнения OCR на Python‑бекенде, а в браузере оставляйте лишь тонкий интерфейс. Если у вас уже есть рабочий настольный скрипт, перенесите логику на серверный эндпоинт с помощью знакомого инструмента вроде Flask или FastAPI, либо воспользуйтесь интегрированным стеком вроде NiceGUI, который умеет напрямую отображать pandas DataFrame. Не рассчитывайте, что pytesseract.pytesseract.tesseract_cmd заработает в PyScript, и не полагайтесь на os.listdir для локальных папок в браузере. При сомнениях смотрите руководство по файловой системе PyScript: https://docs.pyscript.net/2025.8.1/user-guide/filesystem/ — и относитесь к браузеру как к недоверенному клиенту в песочнице, который обменивается данными с сервером, где и выполняется само распознавание.

Статья основана на вопросе на StackOverflow от nasrin begum pathan и ответе furas.