# Parser Lab Учебный сайт для сбора данных: преподаватель импортирует CSV, JSON или JSONL, выбирает оформление и публикует две версии страниц — обычный HTML с пагинацией и HTML с динамической дозагрузкой. Приложение работает на FastAPI, Jinja2, HTML, CSS и небольших JavaScript-модулях без UI-фреймворков. Один аккаунт преподавателя; студентам вход не нужен. ## Быстрый запуск через Docker Нужны Docker Engine с Compose либо Docker Desktop. Запускайте команды из папки проекта. 1. Скопируйте `.env.example` в `.env` и соберите образ: ```sh docker compose build ``` 2. Укажите логин и обычный пароль (не менее 10 символов) в `.env`: ```dotenv LAB_ADMIN_USER=teacher LAB_ADMIN_PASSWORD='ваш-пароль-для-входа' ``` Одинарные кавычки сохраняют символы `$`, пробелы и `#`. Если пароль содержит кавычки или обратные слеши, используйте `uv run --locked python -m parser_lab.cli init-env` для создания корректного `.env` вместо ручного копирования шаблона. 3. Создайте случайный секрет: ```sh docker compose run --rm --no-deps app uv run --no-sync python -c "import secrets; print(secrets.token_urlsafe(48))" ``` Поместите значение в `LAB_SECRET`. Не используйте пароль как секрет. 4. Для локальной проверки оставьте `LAB_ORIGINS=http://localhost:8080` и `LAB_SECURE_COOKIE=false`. Запустите: ```sh docker compose up -d docker compose ps ``` Откройте [кабинет](http://localhost:8080/admin). Проверка сервиса: [healthz](http://localhost:8080/healthz). Если меняете порт, например на 8087, измените одновременно `LAB_PORT=8087` и `LAB_ORIGINS=http://localhost:8087`. Имя хоста тоже имеет значение: localhost и 127.0.0.1 — разные origin. Через запятую можно указать несколько разрешённых адресов. ## Размещение в интернете Разместите приложение в корне отдельного домена, например `https://lab.example.org`. Установите: ```dotenv LAB_ORIGINS=https://lab.example.org LAB_SECURE_COOKIE=true ``` Передайте домен на reverse proxy с HTTPS. Пример nginx, если приложение доступно прокси на порту 8080: ```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; } ``` TLS-сертификат и DNS настраиваются на вашем сервере. Ограничьте прямой доступ к порту приложения средствами сервера или привяжите порт в Compose к 127.0.0.1, если прокси находится на той же машине. Подкаталоги вида `/my-lab/` и несколько реплик приложения не поддерживаются первой версией. ## Как опубликовать данные 1. Войдите в кабинет и нажмите «Загрузить данные». 2. Загрузите файл. При необходимости выберите кодировку и разделитель CSV либо массив записей внутри JSON. 3. Нажмите «Проверить файл»: появятся количество записей, предупреждения и первые 20 строк. Сохраните импорт. 4. Выберите представление и включите его. Отметьте поля, задайте подписи, порядок, поиск и категориальный фильтр. Для карточек назначьте заголовок; для сообщений — текст, при желании автора и дату. 5. Проверьте живой предпросмотр, сохраните черновик и откройте полный предпросмотр. 6. Опубликуйте версию. В разделе публикаций появятся ссылки для студентов. Один набор может иметь все четыре представления. Изменения черновика не затрагивают опубликованные данные. Повторная публикация создаёт новые ссылки с номером версии; прежние продолжают работать. Снятие версии с публикации делает её недоступной. Уже скачанные студентом данные, разумеется, не исчезают. При замене исходного файла настройки представлений сбрасываются: их нужно проверить заново. Изменение названия после публикации разрешено, изменение адреса набора — нет. Если набор отредактирован в другой вкладке, сохранение сообщит о конфликте, а не затрёт изменения. ## Форматы файлов | Формат | Поддержка | |---|---| | 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 перенесён из предыдущего проекта. ## Парсинг Адреса имеют вид: ```text /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/` и не доступны как файлы через публичный сайт: ```sh 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`, если данные нужны.** Обновление: ```sh docker compose down docker compose build docker compose up -d ``` Перед обновлением остановите контейнер и скопируйте весь каталог данных: ```sh 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](https://docs.astral.sh/uv/getting-started/installation/) версии 0.8.14 или новее. Python 3.12 и окружение `.venv` управляются через 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` и запрашивает параметры преподавателя. При последующих запусках достаточно последней команды. Активация окружения не требуется. Зависимости приложения и группа `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`); старые сессии станут недействительны. Тесты: ```sh 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](https://docs.astral.sh/uv/guides/integration/docker/), [запуск команд](https://docs.astral.sh/uv/concepts/projects/run/). Пароль хранится в `.env` обычным текстом. Внутренний Argon2-хеш для проверки входа приложение создаёт в памяти при запуске. Если `.env` остался от предыдущей версии, замените строку `LAB_ADMIN_HASH=...` на `LAB_ADMIN_PASSWORD='ваш-пароль'`; прежний хеш больше не используется. `.env` исключён из Git и Docker-образа.