Files

294 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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; остальные загружаются кнопкой «Загрузить ещё».