Используйте один шаблон для всех гостей

PythonBeginner
Практиковаться сейчас

Введение

Для общего ужина нужно подготовить приглашение каждому гостю. Вы преобразуете список гостей CSV, сведения о событии JSON и готовый HTML-макет в новый набор персональных файлов. Люди с одинаковыми именами должны получить отдельные файлы, а имена вроде Zoë, "Z" & <Friends> должны отображаться как текст.

Нужны знания строк, словарей, циклов и функций Python, а также основы работы в терминале. Здесь объясняются чтение файлов, записи CSV, шаблоны, экранирование HTML-текста и новые выходные каталоги. Командный интерфейс и оформление уже подготовлены. Вы реализуете сопоставление полей, полную проверку и создание приглашений для каждого гостя. CSS, отправка почты и PDF не рассматриваются. Оценка автора: 55–75 минут с учётом чтения и обычных ошибок.

Откройте первое приглашение

На этом шаге вы создадите обычное приглашение и сопоставите содержимое страницы с входными полями. В WebIDE откройте Terminal → New Terminal и дождитесь приглашения оболочки labex. В узком окне Terminal может находиться в More (...). Все команды выполняются в этом терминале. Подготовленная рабочая область не зависит от предыдущих лабораторных работ.

cd меняет текущий каталог. Абсолютный путь начинается с /, а относительный отсчитывается от текущего каталога. Если после перехода в другое место команда не находит файл, вернитесь сюда той же командой:

cd /home/labex/project/invitations

cat выводит текстовый файл. Прочитайте сведения о событии:

cat event.json

В JSON фигурные скобки обозначают объекты, а двойные кавычки окружают строковые ключи и значения. Эти четыре значения станут заголовком, датой, временем и местом приглашения. Готовый пример one.py уже сопоставляет их и одного гостя подготовленному оформлению. python3 запускает файл Python. Создайте пример в новом каталоге batches/first:

python3 one.py

Ожидаемый вывод:

Created batch: batches/first

Переключитесь с вкладки платформы WebIDE на Web 8080. При подготовке среды просмотрщик приглашений уже запущен; Go Live не нужен. Обновите предпросмотр и найдите first/g001.html. Нажмите Open invitation: настоящий HTML-файл откроется в новой вкладке браузера. Прочитайте Maya Patel, The Long Table, время и место. Закройте эту вкладку браузера, чтобы вернуться к Web 8080. Download HTML сохраняет тот же файл для использования вне лабораторной работы.

Полное приглашение для Maya Patel

Обращение указывает гостя, а два нижних блока показывают время и место события.

Просмотрщик читает только реально существующие файлы в batches и не создаёт отсутствующие приглашения. Прежний batches/archive сохраняется как предыдущая работа. Итоговая программа будет создавать новые наборы, сохраняя эти файлы и все входные данные. Вернитесь в WebIDE для следующего шага.

Сопоставьте записи CSV с HTML-текстом

На этом шаге вы настроите повторно используемую функцию рендеринга так, чтобы каждое значение гостя попадало в нужную текстовую область. Вернитесь в тот же терминал и рабочую область WebIDE. Относительные пути ниже отсчитываются от /home/labex/project/invitations.

Читайте записи, а не разделяйте текст по запятым

CSV — табличный формат. Первая строка содержит имена полей. Запятая разделяет поля, кроме случаев, когда она находится внутри поля в кавычках. Кавычка внутри такого поля записывается дважды. Поэтому split(",") не может правильно прочитать все строки. Посмотрите подготовленный список:

cat guests.csv

В нём есть две записи Alex Chen с разными значениями guest_id. Имя последнего гостя содержит запятую, кавычки, & и угловые скобки. Всё это допустимые символы имени. Человека определяет стабильный идентификатор, а не отображаемое имя.

Готовый файл generate.py читает эти записи. Прочитайте его перед изменением функции рендеринга:

cat generate.py

with ... as source открывает файл и закрывает его после блока с отступом. encoding="utf-8" сохраняет символы Unicode. newline="" передаёт обработку границ записей CSV-читателю. csv.DictReader(source) использует заголовок для создания словаря на каждую запись, а list(...) собирает записи в список. Например, guest["name"] содержит полное имя вместе с запятой, если она относится к имени. json.loads(...) превращает JSON-текст события в словарь. Эти читатели и командный интерфейс уже реализованы.

Различайте кавычки CSV и экранирование HTML

HTML использует <...> для тегов и &...; для ссылок на символы. Кавычки CSV защищают структуру таблицы, но не мешают воспринимать имя вроде <Friends> как HTML. html.escape(value, quote=True) преобразует специальные символы в ссылки, которые браузер показывает как исходный текст. Здесь каждое значение вставляется в текстовую область HTML. Это не универсальный способ вставки данных в произвольные JavaScript, CSS или URL.

Template использует $name, $title и остальные заполнители $field из invitation.html. substitute(mapping) заменяет каждый заполнитель соответствующим значением словаря и отклоняет отсутствующее сопоставление. Шесть маркеров тела документа — data-field="name", message, title, date, time, venue — обозначают смысловые текстовые области. Сохраните по одной области каждого вида с соответствующим текстом. Входной шаблон менять нельзя. В сгенерированном результате допустимы эквивалентные теги, расположение и написание HTML-сущностей. Имя только в <title> страницы не заменяет имя в теле документа.

Создайте следующую функцию. cat > rendering.py записывает файл, заменяя его прежнее содержимое. Разделитель блока here-document в кавычках <<'PYCODE' позволяет передать всё до завершающего PYCODE как буквальный текст. Завершающий маркер должен находиться в отдельной строке. Заменяется только ваша функция рендеринга, входные файлы не меняются.

Словарь явно сопоставляет поля гостя и события. values.items() выдаёт пары ключ/значение. Генератор словаря {key: ... for key, value in values.items()} экранирует каждое значение, сохраняя ключ. from html import escape импортирует функцию стандартной библиотеки.

cat > rendering.py <<'PYCODE'
from html import escape


def render_guest(guest, event, template):
    values = {"name": guest["name"], "message": guest["message"],
              "title": event["title"], "date": event["date"],
              "time": event["time"], "venue": event["venue"]}
    return template.substitute({key: escape(value, quote=True)
                                for key, value in values.items()})
PYCODE

Функция возвращает HTML-текст и не создаёт файлов. Локальные вспомогательные модули могут быть файлами .py рядом с generate.py; подкаталоги пакетов не входят в эту работу. Экранирование выполняется один раз перед подстановкой, поэтому браузер показывает &, а не видимый текст &amp;. Теперь оформление приглашения отделено от персонального текста.

Проверьте всех гостей до создания набора

На этом шаге вы создадите по файлу на каждый ID, сохраняя прежнюю работу. Продолжайте в том же терминале WebIDE.

Исходный batch.py использует имена людей как имена файлов и разрешает существующий каталог. Два гостя Alex Chen могут перезаписать файлы друг друга. Прочитайте эту небольшую функцию:

cat batch.py

Контракт требует новый каталог набора с <guest_id>.html для каждой строки. ID содержат только строчные ASCII-буквы, цифры и дефисы. Имена и сообщения остаются текстом UTF-8 без такого ограничения. Обязательные поля гостя: guest_id, name, message; события: title, date, time, venue. Пустое или состоящее лишь из пробельных символов обязательное значение, повторный ID и существующий выходной каталог должны приводить к понятному отказу. Сохраните CSV, оба файла событий, шаблон и batches/archive.

Отделите подготовку от записи

Сначала проверьте все записи и подготовьте все страницы. Создавайте каталог и записывайте файлы только после успешного завершения этой фазы. Иначе ошибка в последней записи может оставить обманчивый неполный набор. Это защищает от рассматриваемых ошибок ввода, но не гарантирует транзакцию при отказе диска или внезапном отключении питания.

isinstance(value, str) проверяет, является ли значение текстом. event.get(field) возвращает None, если ключ отсутствует, позволяя сообщить понятную ошибку. strip() проверяет наличие содержимого помимо пробельных символов, не меняя текст для последующего рендеринга. raise ValueError(...) сообщает об ошибочном вводе. Подготовленный CLI перехватывает исключение, печатает Cannot generate invitations: ... и завершает работу с ошибкой.

set() хранит уже встреченные ID. in проверяет принадлежность, а seen.add(...) запоминает ID. all(...) проверяет каждый символ по указанному правилу. pages.append((guest_id, page)) сохраняет пару ID/страница. Затем for guest_id, page in pages распаковывает эти пары.

Path(output) представляет требуемый каталог. mkdir() создаёт его и отказывает для существующего пути, поскольку мы не передаём exist_ok=True. Оператор / соединяет пути. f-строка вроде f"{guest_id}.html" вставляет ID в имя файла. write_text(..., encoding="utf-8") записывает весь HTML-текст в UTF-8.

Замените только batch.py следующей основной реализацией:

cat > batch.py <<'PYCODE'
from pathlib import Path
from rendering import render_guest


def write_batch(guests, event, template, output):
    for field in ("title", "date", "time", "venue"):
        if not isinstance(event.get(field), str) or not event[field].strip():
            raise ValueError(f"Missing event field: {field}")
    if not guests:
        raise ValueError("The guest list is empty")
    seen = set()
    pages = []
    for guest in guests:
        for field in ("guest_id", "name", "message"):
            if not isinstance(guest.get(field), str) or not guest[field].strip():
                raise ValueError(f"Missing guest field: {field}")
        guest_id = guest["guest_id"]
        if not all(character in "abcdefghijklmnopqrstuvwxyz0123456789-" for character in guest_id):
            raise ValueError("Guest IDs must use lowercase letters, digits, or hyphens")
        if guest_id in seen:
            raise ValueError(f"Duplicate guest ID: {guest_id}")
        seen.add(guest_id)
        pages.append((guest_id, render_guest(guest, event, template)))
    output = Path(output)
    output.mkdir()  ## An existing batch is always refused.
    for guest_id, page in pages:
        (output / f"{guest_id}.html").write_text(page, encoding="utf-8")
PYCODE

from rendering import render_guest использует локальную функцию рендеринга. Первый цикл проверяет событие, следующий проверяет гостей и готовит страницы, а последний записывает полный набор. Ни один файл не создаётся до завершения проверки и рендеринга.

CLI принимает четыре позиционных пути: CSV гостей, JSON события, HTML-шаблон и новый выходной каталог, именно в таком порядке. Запустите его с полным списком:

python3 generate.py guests.csv event.json invitation.html batches/evening

Ожидаемый вывод:

Created batch: batches/evening

ls показывает содержимое каталога:

ls batches/evening

Должны появиться g101.html, g102.html и g103.html. У двух приглашений Alex теперь независимые ID. Каждый файл сохраняет собственное сообщение гостя, а третий — также запятую, кавычки, Unicode и буквальные угловые скобки.

Повторно используйте генератор и сохраните прежнюю работу

На этом шаге вы примените ту же программу к другому событию и потренируетесь исправлять обычные ошибки. Начните в том же терминале WebIDE. Команды независимы, поэтому ожидаемый отказ не мешает продолжать.

Сначала намеренно повторите готовый выходной путь. Это должно завершиться ошибкой: mkdir() отклоняет существующий набор. Сообщение начинается с Cannot generate invitations: и указывает на существующий файл. Прежний набор должен остаться неизменным.

python3 generate.py guests.csv event.json invitation.html batches/evening

Прочитайте подготовленный пример повторного ID. Второй гость имеет ID первого:

cat invalid-duplicate.csv

Попробуйте создать из него новый набор. Это тоже ожидаемый отказ: до создания каталога выводится Duplicate guest ID: g101:

python3 generate.py invalid-duplicate.csv event.json invitation.html batches/rejected-duplicate

Пример без сообщения также намеренно завершается ошибкой Missing guest field: message. Хотя предыдущие записи корректны, частичный набор появляться не должен:

python3 generate.py invalid-missing.csv event.json invitation.html batches/rejected-missing

Опечатка в пути — другая проблема: операционная система не может открыть файл. Следующая команда намеренно использует несуществующий guest.csv. Прочитайте ошибку и не сбрасывайте среду:

python3 generate.py guest.csv daytime.json invitation.html batches/daytime

Исправьте путь на настоящий guests.csv. Прочитайте второе событие и создайте новый набор той же программой и с тем же списком:

cat daytime.json
python3 generate.py guests.csv daytime.json invitation.html batches/daytime

Ожидаемый вывод:

Created batch: batches/daytime

Перейдите в Web 8080 и обновите страницу. По очереди откройте evening/g101.html и evening/g102.html. Имена одинаковы, сообщения разные. Откройте evening/g103.html: запятая, кавычки, ë, амперсанд и <Friends> должны отображаться буквально. Откройте daytime/g103.html и прочитайте новые заголовок, время и место. Закрывайте вкладку каждого приглашения, чтобы вернуться к списку. Для дневного приглашения используйте Download HTML и откройте скачанный файл: его персональный текст должен совпадать с предпросмотром.

Специальные символы как буквальный текст приглашения

Имя сохраняет запятую, кавычки и угловые скобки. Они видны в обращении как текст.

Два набора событий независимы. CSV, файлы событий и шаблон не изменились, старый архив сохранился. Неверный ввод, существующий набор и отсутствующий входной путь объясняются по-разному. Каждый случай можно исправить, не уничтожая прежние приглашения.

Резюме

Вы создали настоящие HTML-приглашения, которые можно открыть и скачать, из записей CSV и сведений JSON. Вы отделили стабильные ID от отображаемых имён, сопоставили поля текстовым областям шаблона, сохранили Unicode и специальные символы с помощью UTF-8 и HTML-экранирования и проверили весь набор до записи. Теперь генератор создаёт новые наборы событий, сохраняя входные данные и предыдущие результаты.