Parser Lab
Учебный сайт для сбора данных: преподаватель импортирует CSV, JSON или JSONL, выбирает оформление и публикует две версии страниц — обычный HTML с пагинацией и HTML с динамической дозагрузкой. Приложение работает на FastAPI, Jinja2, HTML, CSS и небольших JavaScript-модулях без UI-фреймворков. Один аккаунт преподавателя; студентам вход не нужен.
Быстрый запуск через Docker
Нужны Docker Engine с Compose либо Docker Desktop. Запускайте команды из папки проекта.
-
Скопируйте
.env.exampleв.envи соберите образ:docker compose build -
Укажите логин и обычный пароль (не менее 10 символов) в
.env:LAB_ADMIN_USER=teacher LAB_ADMIN_PASSWORD='ваш-пароль-для-входа'Одинарные кавычки сохраняют символы
$, пробелы и#. Если пароль содержит кавычки или обратные слеши, используйтеuv run --locked python -m parser_lab.cli init-envдля создания корректного.envвместо ручного копирования шаблона. -
Создайте случайный секрет:
docker compose run --rm --no-deps app uv run --no-sync python -c "import secrets; print(secrets.token_urlsafe(48))"Поместите значение в
LAB_SECRET. Не используйте пароль как секрет. -
Для локальной проверки оставьте
LAB_ORIGINS=http://localhost:8080иLAB_SECURE_COOKIE=false. Запустите:docker compose up -d docker compose ps
Откройте кабинет. Проверка сервиса: healthz.
Если меняете порт, например на 8087, измените одновременно LAB_PORT=8087 и LAB_ORIGINS=http://localhost:8087. Имя хоста тоже имеет значение: localhost и 127.0.0.1 — разные origin. Через запятую можно указать несколько разрешённых адресов.
Размещение в интернете
Разместите приложение в корне отдельного домена, например https://lab.example.org. Установите:
LAB_ORIGINS=https://lab.example.org
LAB_SECURE_COOKIE=true
Передайте домен на reverse proxy с HTTPS. Пример nginx, если приложение доступно прокси на порту 8080:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
client_max_body_size 26m;
}
TLS-сертификат и DNS настраиваются на вашем сервере. Ограничьте прямой доступ к порту приложения средствами сервера или привяжите порт в Compose к 127.0.0.1, если прокси находится на той же машине. Подкаталоги вида /my-lab/ и несколько реплик приложения не поддерживаются первой версией.
Как опубликовать данные
- Войдите в кабинет и нажмите «Загрузить данные».
- Загрузите файл. При необходимости выберите кодировку и разделитель CSV либо массив записей внутри JSON.
- Нажмите «Проверить файл»: появятся количество записей, предупреждения и первые 20 строк. Сохраните импорт.
- Выберите представление и включите его. Отметьте поля, задайте подписи, порядок, поиск и категориальный фильтр. Для карточек назначьте заголовок; для сообщений — текст, при желании автора и дату.
- Проверьте живой предпросмотр, сохраните черновик и откройте полный предпросмотр.
- Опубликуйте версию. В разделе публикаций появятся ссылки для студентов.
Один набор может иметь все четыре представления. Изменения черновика не затрагивают опубликованные данные. Повторная публикация создаёт новые ссылки с номером версии; прежние продолжают работать. Снятие версии с публикации делает её недоступной. Уже скачанные студентом данные, разумеется, не исчезают.
При замене исходного файла настройки представлений сбрасываются: их нужно проверить заново. Изменение названия после публикации разрешено, изменение адреса набора — нет. Если набор отредактирован в другой вкладке, сохранение сообщит о конфликте, а не затрёт изменения.
Форматы файлов
| Формат | Поддержка |
|---|---|
| CSV | Первая строка — уникальные непустые заголовки; UTF-8 или Windows-1251; запятая, точка с запятой или табуляция |
| JSON | Массив объектов, одиночный объект или выбранный вложенный массив объектов |
| JSONL | По одному объекту JSON в каждой непустой строке; UTF-8 |
Лимиты по умолчанию: 25 МиБ, 50 000 записей, глубина JSON 20. Размер файла и количество записей можно изменить через LAB_MAX_BYTES и LAB_MAX_RECORDS. Большие наборы требуют больше памяти: текущая версия хранит JSON-снимки в SQLite и загружает снимок для выборки. Проверяйте скорость на своих данных до занятия.
Дубликаты и исходный порядок сохраняются. Значения CSV остаются строками, чтобы не потерять ведущие нули и исходное представление чисел. В JSON различаются отсутствие поля, null, пустая строка, false и 0. Ошибки формата блокируют импорт; повреждённые строки не пропускаются молча. Пустой набор не публикуется.
Вложенные поля показаны путями: /profile/age означает поле age в объекте profile. Символ / в самом имени поля кодируется как ~1, а ~ — как ~0. Массивы сохраняются; специальные анкеты могут отображать их как пары вопросов и ответов.
Заполненные анкеты
Для таблицы с колонками participant, q1, q2 выберите «Колонки с ответами» и замените подписи q1/q2 текстами вопросов.
Для examples/data/questionnaires.json достаточно выбрать файл, нажать «Проверить файл» и «Сохранить и настроить». Приложение распознаёт участников и пары question/answer, выбирает /participants и создаёт готовое представление «Анкеты». Предпросмотр сразу показывает двух участников и их ответы. Распознавание зависит от структуры данных, а не имени файла.
Если структура неоднозначна, выберите уровень записей вручную. Для ранее загруженного целиком документа в редакторе доступна кнопка «Настроить анкеты автоматически»: она исправляет черновик без повторной загрузки исходного файла. Опубликованные версии сохраняются.
Сайт отображает готовые данные; он не собирает ответы и не интерпретирует психологические шкалы. Дополнительные примеры синтетические. Хоккейный CSV перенесён из предыдущего проекта.
Парсинг
Адреса имеют вид:
/html/study-abc/v/1/table
/interactive/study-abc/v/1/table
Для HTML данные находятся в исходном ответе сервера. Пагинация — ?page=2, следующая ссылка — a[rel=next]. Стабильные селекторы:
| Селектор | Значение |
|---|---|
.record[data-record-id] |
Запись со стабильным в пределах версии id |
.field[data-field] |
Значение выбранного поля |
data-value-state |
missing, null или value |
data-value-type |
Исходный тип: string, number, boolean, object, array, null или missing |
data-role |
Роль значения: title, text, author или date |
data-label |
Подпись поля или текст вопроса |
.detail-link |
Ссылка на отдельную запись |
Фильтры HTML: q — регистронезависимый поиск в назначенном поле, category — JSON-представление скалярного значения. Формы на странице формируют правильные параметры автоматически. Поиск не выполняет морфологический анализ. При неправильном номере страницы возвращается 400, при отсутствии страницы или версии — 404.
В динамическом варианте первые записи также находятся в HTML-ответе. Кнопка «Загрузить ещё», поиск и фильтры обновляют данные обычными HTTP-запросами через JavaScript. Для практики браузерного парсинга используйте Playwright. Оба варианта используют один шаблон, стили и favicon.
Примеры решений находятся в examples/parsers/ и не доступны как файлы через публичный сайт:
uv sync --locked
uv run --locked playwright install chromium
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
Вместо загруженного Chromium можно использовать установленный Chrome: добавьте --channel chrome второму парсеру.
Обновление и резервная копия
Данные находятся в named volume lab-data, смонтированном в /data. docker compose down сохраняет том. Не используйте down -v, если данные нужны.
Обновление:
docker compose down
docker compose build
docker compose up -d
Перед обновлением остановите контейнер и скопируйте весь каталог данных:
docker compose stop app
docker compose cp app:/data ./backup-data
docker compose start app
Храните копию вместе с защищённой копией .env. Для восстановления подготовьте отдельный Compose-проект с новым томом, создайте его контейнер (docker compose create), скопируйте содержимое backup-data в /data, восстановите владельца UID 10001 при необходимости и запустите контейнер. Проверяйте восстановление отдельно от рабочего сайта.
Импортированные файлы и старые снимки не удаляются автоматически. Следите за размером тома. Безвозвратное удаление истории не входит в интерфейс первой версии.
Локальная разработка
Нужен uv версии 0.8.14 или новее. Python 3.12 и окружение .venv управляются через uv. Запускайте команды из корня проекта:
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 и запрашивает параметры преподавателя. При последующих запусках достаточно последней команды. Активация окружения не требуется.
Зависимости приложения и группа dev описаны в pyproject.toml, точные версии зафиксированы в uv.lock. Для изменения зависимостей используйте uv add ИМЯ или uv add --dev ИМЯ; сохраняйте оба файла в Git. Устаревшие requirements*.lock больше не используются. Docker собирает production-окружение командой uv sync --locked --no-dev и запускает готовое окружение через uv run --no-sync.
Параметры окружения имеют приоритет над локальной .env. Пароль LAB_ADMIN_PASSWORD и секрет LAB_SECRET обязательны, приложение откажется запускаться без них. После изменения пароля или секрета перезапустите приложение (для Docker: docker compose up -d --force-recreate); старые сессии станут недействительны.
Тесты:
uv run --locked pytest -q
# Дополнительные браузерные тесты: LAB_BROWSER_TESTS=1, при необходимости PLAYWRIGHT_CHANNEL=chrome.
Старые документы проектирования в docs/ сохранены как история. Актуальная архитектура — FastAPI + Jinja2 + HTML/CSS/JavaScript; зависимости Rio полностью удалены. Существующие SQLite-данные и опубликованные ссылки совместимы с новой версией. Фактические результаты проверки и ограничения среды — в docs/verification.md.
Справка: uv в Docker, запуск команд.
Пароль хранится в .env обычным текстом. Внутренний Argon2-хеш для проверки входа приложение создаёт в памяти при запуске. Если .env остался от предыдущей версии, замените строку LAB_ADMIN_HASH=... на LAB_ADMIN_PASSWORD='ваш-пароль'; прежний хеш больше не используется. .env исключён из Git и Docker-образа.