# Parser Lab Parser Lab — сайт для учебных заданий по веб-парсингу. Преподаватель загружает CSV, JSON или JSONL, настраивает отображение данных и публикует страницы для студентов. Каждая коллекция доступна в двух вариантах: обычный HTML с пагинацией и страница с динамической дозагрузкой. Есть четыре представления: таблица, карточки, сообщения и анкеты. Поля в списке и в раскрытой записи можно настраивать отдельно. Для преподавателя предусмотрен один аккаунт; публичные страницы открываются без входа. Приложение написано на Python 3.12, FastAPI и Jinja2. Интерфейс — HTML, CSS и JavaScript, данные хранятся в SQLite. Отдельный сервер базы данных не нужен. ## Запуск через Docker Compose Нужен Docker с командой `docker compose`: Docker Engine с Compose plugin или Docker Desktop. Python и uv на хосте не требуются. Все команды ниже выполняются из корня репозитория. Примеры рассчитаны на Bash; в Windows можно использовать WSL2. ### 1. Подготовьте конфигурацию и соберите образ При первом запуске создайте `.env` из шаблона: ```sh cp .env.example .env docker compose build ``` Если `.env` уже настроен, повторно копировать шаблон не нужно. ### 2. Задайте пароль и секрет Сгенерируйте секрет для сессий: ```sh docker run --rm parser-lab:1.0 python -c 'import secrets; print(secrets.token_urlsafe(48))' ``` Откройте `.env`. Замените значения `LAB_ADMIN_PASSWORD` и `LAB_SECRET` своими: ```dotenv LAB_ADMIN_USER=teacher LAB_ADMIN_PASSWORD='ЗАМЕНИТЕ_НА_СВОЙ_ПАРОЛЬ' LAB_SECRET=ВСТАВЬТЕ_СГЕНЕРИРОВАННЫЙ_СЕКРЕТ LAB_ORIGINS=http://localhost:8080 LAB_SECURE_COOKIE=false LAB_PORT=8080 ``` Пароль должен содержать не менее 10 символов, секрет — не менее 32. Это разные значения. В `.env` пароль хранится обычным текстом; файл исключён из Git и сборки образа. Одинарные кавычки сохраняют `$`, `#` и пробелы в пароле. Правила экранирования описаны в [документации Compose](https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/#env-file-syntax). При локальной установке файл можно создать интерактивной командой `init-env`, приведённой ниже. ### 3. Запустите приложение ```sh docker compose up -d docker compose ps ``` Кабинет преподавателя: [http://localhost:8080/admin](http://localhost:8080/admin). Войдите с логином и паролем из `.env`. ## Запуск через Docker без Compose Соберите тот же образ: ```sh docker build -t parser-lab:1.0 . ``` Создайте и заполните `.env`, как описано выше. Команда генерации секрета также работает без Compose. Создайте том и запустите контейнер: ```sh docker volume create parser-lab-data docker run -d \ --name parser-lab \ --restart unless-stopped \ --stop-timeout 30 \ -p 8080:8080 \ --mount type=volume,source=parser-lab-data,target=/data \ --mount "type=bind,source=$(pwd)/.env,target=/app/.env,readonly" \ parser-lab:1.0 \ uv run --no-sync --env-file /app/.env python -m parser_lab ``` Здесь `.env` подключается [как файл только для чтения](https://docs.docker.com/engine/storage/bind-mounts/) и загружается через uv. Так сохраняется обработка кавычек в пароле. Файл должен существовать и быть доступен на чтение пользователю контейнера с UID 10001. Адрес кабинета тот же: [http://localhost:8080/admin](http://localhost:8080/admin). Управление контейнером: ```sh docker stop parser-lab docker start parser-lab ``` При таком запуске `LAB_PORT` не используется: внешний порт задаётся параметром `-p`. Для порта 8087 укажите `-p 8087:8080` и `LAB_ORIGINS=http://localhost:8087` в `.env`. Том `parser-lab-data` из этого примера и том Compose — разные хранилища. Данные между ними автоматически не переносятся. ## Локальный запуск без Docker Установите [uv](https://docs.astral.sh/uv/getting-started/installation/) версии 0.8.14 или новее. Проект использует Python 3.12; uv может установить его и создать виртуальное окружение. Из корня репозитория выполните: ```sh uv sync --locked uv run --locked python -m parser_lab.cli init-env uv run --locked --env-file .env python -m parser_lab ``` `init-env` спросит логин и пароль, сгенерирует секрет и запишет `.env`. Если файл уже существует, команда откажется его перезаписывать — в этом случае пропустите её и проверьте текущие настройки. Кабинет доступен по адресу [http://localhost:8080/admin](http://localhost:8080/admin). Для остановки нажмите `Ctrl+C`. При следующих запусках достаточно последней команды; активировать `.venv` вручную не нужно. По умолчанию данные сохраняются в `./data`. Каталог создаётся автоматически. [Параметры окружения имеют приоритет над значениями из `.env`](https://docs.astral.sh/uv/concepts/configuration-files/#env). Для разработки с перезапуском при изменении кода: ```sh uv run --locked --env-file .env uvicorn parser_lab.app:create_app \ --factory --host 127.0.0.1 --port 8080 --reload ``` При обычном запуске через `python -m parser_lab` адрес сервера — `0.0.0.0:8080`. Переменная `LAB_PORT` его не меняет. Если нужен другой порт, используйте команду uvicorn с нужным `--port` и измените `LAB_ORIGINS`. ## Настройки | Переменная | Назначение | | --- | --- | | `LAB_ADMIN_USER` | Логин преподавателя. Обязателен. | | `LAB_ADMIN_PASSWORD` | Пароль, не менее 10 символов. Обязателен. | | `LAB_SECRET` | Случайный секрет сессий, не менее 32 символов. Обязателен. | | `LAB_ORIGINS` | Разрешённые адреса сайта через запятую, включая протокол и порт. По умолчанию `http://localhost:8080`. | | `LAB_SECURE_COOKIE` | `false` для локального HTTP, `true` для HTTPS. Если не задана, используется `true`. | | `LAB_PORT` | Внешний порт в Compose. По умолчанию 8080. | | `LAB_DATA_DIR` | Каталог данных. По умолчанию `./data`, в Docker — `/data`. Compose задаёт `/data` явно. | | `LAB_MAX_BYTES` | Максимальный размер загружаемого файла. По умолчанию 26214400 байт, то есть 25 МиБ. | | `LAB_MAX_RECORDS` | Максимальное число записей в импорте. По умолчанию 50000. | `localhost` и `127.0.0.1` считаются разными адресами. Если используете оба, добавьте оба в `LAB_ORIGINS`: ```dotenv LAB_ORIGINS=http://localhost:8080,http://127.0.0.1:8080 ``` После изменения `.env` перезапустите локальный процесс или пересоздайте контейнер Compose: ```sh docker compose up -d --force-recreate ``` Для Docker без Compose остановите и удалите контейнер, затем повторите команду `docker run` с тем же томом. Это также заново подключит изменённый `.env`. После смены логина, пароля или секрета прежние сессии перестанут действовать. ## Развёртывание на сервере Приложение рассчитано на один экземпляр и размещение в корне отдельного домена, например `https://lab.example.org`. Запуск в подкаталоге вроде `/parser/` не поддерживается. 1. Настройте DNS домена на сервер и HTTPS в обратном прокси. 2. Запустите приложение одним из способов выше. 3. Укажите публичный адрес в `.env`: ```dotenv LAB_ORIGINS=https://lab.example.org LAB_SECURE_COOKIE=true ``` 4. Если nginx работает на том же хосте, ограничьте публикацию порта адресом `127.0.0.1`. В `compose.yaml` замените строку в `ports` на: ```yaml - "127.0.0.1:${LAB_PORT:-8080}:8080" ``` Для обычного Docker используйте `-p 127.0.0.1:8080:8080`. Для локального процесса — uvicorn с `--host 127.0.0.1`. 5. Добавьте в HTTPS-блок `server` nginx: ```nginx location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; client_max_body_size 26m; } ``` 6. Примените конфигурацию nginx и перезапустите приложение. Проверьте `/healthz`, вход в кабинет и загрузку небольшого файла через домен. Этот фрагмент предполагает уже настроенный TLS-сертификат. Если прокси работает в другом контейнере, подключите оба сервиса к общей сети и укажите адрес приложения в ней: `127.0.0.1` внутри контейнера прокси указывает на сам прокси. ## Работа с коллекциями 1. Войдите в кабинет и нажмите «Загрузить данные». 2. Выберите файл. Для CSV при необходимости укажите кодировку и разделитель; для вложенного JSON — список, из которого нужно брать записи. 3. Нажмите «Проверить файл», проверьте первые строки и сохраните импорт. 4. Включите нужное представление, выберите поля, подписи и порядок. Для карточек назначьте заголовок, для сообщений — текст. 5. Чтобы раскрытая запись содержала больше информации, выберите «Настройка полей → В полной записи» и включите «Отдельные поля полной записи». 6. Проверьте результат через «Предпросмотр данных», сохраните черновик и опубликуйте новую версию. Ссылки появятся в разделе «Публикации». Предпросмотр показывает текущие изменения. Ссылка «Предпросмотр» открывает сохранённый черновик. Публикации хранятся отдельно: редактирование черновика не меняет страницы, которые уже доступны студентам. Новая публикация получает новый номер и новые ссылки; прежние работают, пока версия не снята с публикации. Замена исходного файла сбрасывает настройки представлений. После первой публикации адрес коллекции изменить нельзя. Кнопка «Удалить коллекцию» удаляет черновик, все публикации и связанные исходные файлы. Действие требует подтверждения и не имеет отмены. Если файл не удалось удалить, приложение сообщит об этом и повторит очистку при следующем запуске. Промежуточные исходники от замен в старых версиях могли потерять связь с коллекцией; такие файлы автоматически не удаляются. ### Форматы данных | Формат | Требования | | --- | --- | | CSV | Первая строка — уникальные непустые заголовки. Кодировка UTF-8 или Windows-1251. Разделитель — запятая, точка с запятой или табуляция. | | JSON | Массив объектов, один объект или выбранный вложенный массив объектов. | | JSONL | Один объект JSON на строку, кодировка UTF-8. Пустые строки пропускаются. | Порядок записей и дубликаты сохраняются. Значения CSV остаются строками, в том числе числа с ведущими нулями. В JSON различаются отсутствующее поле, `null`, пустая строка, `false` и `0`. Максимальная глубина JSON — 20. Ошибки формата блокируют импорт; пустую коллекцию опубликовать нельзя. Вложенные поля обозначаются путями: `/profile/age` — поле `age` внутри `profile`. Массивы вопросов с полями `question` и `answer` приложение умеет распознавать как анкеты. Для неоднозначной структуры уровень записей и поля можно выбрать вручную. Сайт отображает загруженные ответы, но не собирает новые. ## Обновление и резервные копии SQLite и исходные файлы находятся в одном каталоге. Локально это `data/`, в контейнере — `/data`. Compose сохраняет его в томе с именем вида `<имя-проекта>_lab-data`. Перед обновлением остановите приложение и скопируйте весь каталог данных. Для каждой копии используйте новый каталог. Пример для Compose: ```sh mkdir -p backups/before-update docker compose stop app docker compose cp app:/data/. ./backups/before-update/ docker compose start app ``` Для обычного Docker аналогичные команды — `docker stop parser-lab`, `docker cp parser-lab:/data/. ./backups/before-update/` и `docker start parser-lab`. При локальном запуске остановите процесс и скопируйте `data/`. Отдельно сохраните `.env` в защищённом месте. После получения новой версии исходного кода обновите Compose-развёртывание: ```sh docker compose up -d --build docker compose ps ``` Для Docker без Compose пересоберите образ, остановите и удалите контейнер, затем повторите команду `docker run`, сохранив прежний том `parser-lab-data`. Локально выполните `uv sync --locked` и перезапустите приложение. Восстановление копии в **новую установку Compose с пустым томом**: ```sh docker compose create app docker compose cp ./backups/before-update/. app:/data/ docker compose run --rm --no-deps --user root app chown -R 10001:10001 /data docker compose up -d ``` Перед этим подготовьте образ и `.env`. После восстановления проверьте вход в кабинет и открытие опубликованной коллекции. ## Для разработки и учебных заданий Основные файлы проекта: ```text src/parser_lab/ app.py HTTP-маршруты и запуск приложения admin.py Импорт и настройки коллекций repository.py SQLite, исходные файлы и публикации importing.py Чтение CSV, JSON и JSONL views.py Выбор полей и подготовка записей web/templates/ Шаблоны страниц web/static/ Стили и JavaScript examples/parsers/ Примеры парсеров tests/ Проверки приложения ``` Установка зависимостей и запуск тестов: ```sh uv sync --locked uv run --locked pytest -q ``` Браузерные тесты запускаются отдельно: ```sh uv run --locked playwright install chromium LAB_BROWSER_TESTS=1 uv run --locked pytest -q tests/test_browser.py ``` При использовании установленного Chrome задайте `PLAYWRIGHT_CHANNEL=chrome`. Фикстура браузерных тестов также поддерживает `PLAYWRIGHT_EXECUTABLE_PATH` для явного пути к браузеру. Несколько тестов анкет ожидают файл `examples/data/questionnaires.json`; если его нет в рабочей копии, эти проверки завершатся с `FileNotFoundError`. Примеры парсеров принимают ссылку на опубликованную коллекцию: ```sh uv run --locked python examples/parsers/scrape_html.py \ "http://localhost:8080/html/SLUG/v/1/table" --output html.jsonl uv run --locked python examples/parsers/scrape_interactive.py \ "http://localhost:8080/interactive/SLUG/v/1/table" --output interactive.jsonl ``` Замените `SLUG` и номер версии значениями из ссылки в кабинете. Для второго примера нужен браузер Playwright; установленный Chrome можно выбрать аргументом `--channel chrome`. В HTML записи доступны по селектору `.record[data-record-id]`, значения — `.field[data-field]`. Ссылка `.detail-link` открывает полную запись, `a[rel=next]` ведёт на следующую страницу. Атрибуты `data-label`, `data-value-state` и `data-value-type` содержат подпись, состояние и тип значения. В динамическом варианте первые записи тоже находятся в HTML; остальные загружаются кнопкой «Загрузить ещё».