Чтобы сохранить значение даты публикации без времени, разберите его как календарную дату и считайте эту дату исходной. Если для экспорта нужна временная метка UTC, используйте явное соглашение о полуночи по UTC. В этом пошаговом руководстве для Терминала на Mac показана разница и проверяется обратное преобразование.

Определите, что именно означает поле публикации

Предположим, что в списке публикаций есть искусственное значение 2028-06-15. Ваша задача — проверить это значение и подготовить его к экспорту, не допустив случайного отображения 14 июня. Прежде чем выбирать парсер, определите, содержит ли поле день публикации или фактический момент публикации.

У этих четырёх понятий разные роли:

Понятие Искусственный пример Значение
Календарная дата 2028-06-15 Календарный день без времени и смещения
Момент времени 2028-06-15T00:00:00+00:00 Конкретный момент, выраженный в UTC
Смещение UTC +09:00 Разница между показаниями местных часов и UTC
Именованный часовой пояс America/Los_Angeles Идентификатор для получения применимых правил часового пояса

В RFC 3339 значение full-date определено отдельно от значения date-time; грамматика временных меток добавляет ко дате время и смещение. Поэтому в одной лишь дате публикации нет сведений, необходимых для определения момента публикации. Выбор полуночи добавляет сведения, которых не было во входных данных. RFC 3339, раздел 5.6

Именованный часовой пояс содержит больше сведений, чем одно числовое смещение. В документации Python для zoneinfo показано, как в часовом поясе могут меняться смещения при переходе на летнее время. Для метки с датой без времени нет необходимости определять эти правила. Python: поддержка часовых поясов IANA

Прежде чем экспортировать данные, воспользуйтесь этим правилом:

  • Поле означает «опубликовано в этот день»: храните и отображайте значение только с датой.
  • Для принимающего формата нужна временная метка: сохраните дату и создайте переносчик с полуночью по UTC согласно документированному соглашению.
  • Поле означает «опубликовано в этот момент»: получите фактические время и смещение либо время и именованный часовой пояс, необходимые для определения этого момента.

Соглашение о переносчике полезно для совместимости, но оно не восстанавливает неизвестное время публикации. Название поля, например utc_carrier, ясно показывает это ограничение; название published_at намекало бы на сведения, которых источник никогда не предоставлял.

Разберитесь, откуда берётся ошибка с предыдущим днём

Рассмотрим два возможных пути обработки значения 2028-06-15. В первом программа назначает полночь с фиксированным смещением +09:00, а затем преобразует этот момент в UTC. Результат — 2028-06-14T15:00:00+00:00: вычитание девяти часов пересекает границу даты.

Во втором случае программа намеренно создаёт полночь по UTC: 2028-06-15T00:00:00+00:00. Если отобразить тот же момент с фиксированным смещением -07:00, получится 2028-06-14T17:00:00-07:00. Это примеры вычислений со смещением, а не утверждения о смещении именованного часового пояса в эту дату.

Оба вычисления сохраняют соответствующие им моменты времени. Но ни одно из них не сохраняет исходную календарную дату при отображении в любом часовом поясе. В документации Python указано, что astimezone() изменяет значения полей часов, сохраняя представленный момент времени UTC. Python: datetime.astimezone()

Практический вывод: для разбора и отображения нужен общий договор. Если дата помещена в переносчик с полуночью по UTC, извлекайте календарные поля в UTC. Если нужно, чтобы метка публикации не менялась при открытии экспорта в разных местах, отображайте сохранённую календарную дату.

Не предполагайте, что все языки программирования одинаково обрабатывают отсутствующие сведения. ECMAScript указывает, что формы даты без смещения интерпретируются как UTC, а формы даты и времени без смещения — как местное время. Поэтому добавление T00:00:00 к строке в JavaScript может изменить её интерпретацию. ECMAScript: Date.parse

Выполните полную проверку даты публикации в Терминале на Mac

В этом руководстве проверяются три искусственные даты, выводится предварительный просмотр с разделителями-табуляциями и проверяется отклонение некорректных данных. Список публикаций не изменяется, файл экспорта не создаётся.

В Finder откройте /Applications/Utilities, затем дважды щёлкните Terminal. Apple описывает этот способ как поддерживаемый для запуска приложения командной строки. Apple: открытие и завершение работы Терминала на Mac

Выполните следующую команду, чтобы проверить интерпретатор:

python3 --version

Для этого руководства используйте Python 3.11 или более позднюю версию. Если python3 недоступен или сообщает о более старой версии, прежде чем продолжить, выполните официальные инструкции по установке для macOS, затем снова откройте Terminal и проверьте версию. В документации Python описаны установщик для macOS и запуск скриптов с помощью python3. Python: использование Python в macOS

Вставьте весь блок ниже, включая последнюю команду PY, и нажмите Return. Аргумент - указывает Python считывать программу из стандартного ввода. Все примеры данных и импорты включены. Python: командная строка и окружение

python3 - <<'PY'
import re
from datetime import date, datetime, time, timezone


def parse_publication_date(text):
    if not isinstance(text, str) or re.fullmatch(
        r"[0-9]{4}-[0-9]{2}-[0-9]{2}", text
    ) is None:
        raise ValueError("Expected YYYY-MM-DD")
    return date.fromisoformat(text)


def utc_carrier(day):
    return datetime.combine(day, time(0), tzinfo=timezone.utc)


samples = ["2028-02-29", "2028-06-15", "2028-12-31"]
parsed = [(text, parse_publication_date(text)) for text in samples]

print("publication_date\tutc_carrier")
for original, day in parsed:
    carrier = utc_carrier(day)
    assert day.isoformat() == original
    assert carrier.astimezone(timezone.utc).date() == day
    print(day.isoformat(), carrier.isoformat(timespec="seconds"), sep="\t")

for invalid in [
    "2027-02-29",
    "2028-6-15",
    "2028-06-15T00:00:00",
    " 2028-06-15",
]:
    try:
        parse_publication_date(invalid)
    except ValueError:
        pass
    else:
        raise AssertionError(f"Unexpected acceptance: {invalid!r}")

print("Round-trip and rejection checks passed.")
PY

Ожидаемый результат:

publication_date	utc_carrier
2028-02-29	2028-02-29T00:00:00+00:00
2028-06-15	2028-06-15T00:00:00+00:00
2028-12-31	2028-12-31T00:00:00+00:00
Round-trip and rejection checks passed.

Пример и два вычисления с фиксированным смещением были успешно выполнены с доступным интерпретатором Python. Инструкции по запуску на Mac и проверке интерпретатора основаны на указанной официальной документации.

Почему пример сохраняет день публикации

Парсер отдельно проверяет две вещи: точную форму строки и корректность даты в календаре. Его регулярное выражение требует четыре цифры года, две цифры месяца и две цифры дня. Затем парсер даты отклоняет невозможные даты. Благодаря этому в столбец публикаций не попадут незаметно временные метки, лишние пробелы или сокращённые значения.

Эта явная проверка формы важна, потому что date.fromisoformat() также принимает другие представления ISO, включая компактные даты и даты в формате недель. Затем datetime.combine() намеренно задаёт полночь и UTC. Python: основные типы даты и времени

Программа разбирает весь список примеров до вывода заголовка. Если заменить пример некорректным значением, выполнение остановится до появления строк предварительного просмотра. Намеренно добавленные в конце проверки некорректных данных выполняются отдельно; ожидаемые исключения перехватываются, чтобы можно было проверить все случаи отклонения.

Две проверки обратного преобразования формулируют договор. Первая проверяет, что разбор и форматирование даты воспроизводят исходную метку. Вторая проверяет, что полученный переносчик даёт ту же дату при интерпретации в UTC. Вместе они проверяют именно задачу сохранения исходной даты, а не просто возможность создать какую-либо временную метку.

В выводе явно используется +00:00. RFC 3339 также допускает Z как обозначение смещения UTC, но добавление любого из этих суффиксов к дате без времени не восполняет отсутствующий компонент времени. Оставьте формат только с датой или намеренно создайте полную временную метку. RFC 3339, раздел 5.6

Разберитесь, почему при экспорте дата всё ещё меняется

Если в предварительном просмотре полученных данных отображается предыдущий день, проверьте значения на каждом этапе. Используйте средний пример, 2028-06-15, чтобы сравнить каждый этап с одной известной датой.

Контрольная точка Ожидаемое значение при соглашении о переносчике На что указывает расхождение
Исходное поле публикации 2028-06-15 Входные данные изменились до разбора
Разобранная дата в текстовом виде 2028-06-15 Разбор или нормализация изменили дату
Полученный переносчик UTC 2028-06-15T00:00:00+00:00 Было добавлено другое время или смещение
Дата, восстановленная в UTC 2028-06-15 При преобразовании или извлечении использовался неверный договор
Метка публикации, показанная читателю 2028-06-15 Возможно, средство просмотра показывает переносчик в другом часовом поясе

Проверьте средство отображения отдельно от парсера. Корректный переносчик UTC всё равно может дать другую календарную дату при отображении с настройкой местного часового пояса. Для метки публикации передавайте в средство отображения сохранённое значение только с датой. Если оно принимает только временные метки, воспользуйтесь документированным параметром отображения в UTC и проверьте результат.

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

Также не обрабатывайте уже существующую временную метку так, будто это исходное значение только с датой. Для настоящего момента времени смена часового пояса означает преобразование. В Python эта операция отличается от замены tzinfo, которая добавляет сведения о часовом поясе, не изменяя значения полей часов. Python: поведение преобразования часового пояса

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

Источники