Поэтому я устал открывать четыре разных сайта каждое утро только чтобы увидеть если что-то было действительно новое, теперь скрипт делает эту часть для меня и отправляет мне резюме в 7 утра. Большей частью. Было растяжение примерно две недели где он молча ничего не делал и я не заметил, что является собственной историей.
Что это должно получать
Некоторые источники (Hacker News API, NRK RSS feed, пара других), получены, разобраны, потом отфильтрованы против списка ключевых слов которым я действительно заботился. Всё остальное выбросилось, которое большинство. Это вся суть.
import requests
KEYWORDS = ["postgres", "sveltekit", "linux", "norway", "helgeland"]
def fetch_hn_headlines():
ids = requests.get("https://hacker-news.firebaseio.com/v0/topstories.json").json()[:50]
headlines = []
for story_id in ids:
item = requests.get(f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json").json()
title = item["title"]
if any(k in title.lower() for k in KEYWORDS):
headlines.append((title, item["url"]))
return headlinesТа версия работала нормально примерно неделю. Потом одно утро, никакого резюме.
Две недели я не заметил ничего плохого
Cron не отправляет тебе письмо когда скрипт не удается кроме как если ты задал MAILTO, что я не делал, поэтому падение три строки в цикл for просто молча кончается работой и ничего тебе не говорит. Я обнаружил это чистой случайностью, упомянув кому-то что резюме было удобно, потом осознав что я на самом деле не видел один в то время и пошел проверить.
crontab -l0 7 * * * /home/bjorn/.venv/bin/python /home/bjorn/digest.pyНичего там не указывает на реальную проблему, это просто говорит что работа существует. Мне пришлось запустить её вручную увидеть что-либо.
python3 digest.pyTraceback (most recent call last):
File "digest.py", line 12, in fetch_hn_headlines
title = item["title"]
KeyError: 'title'Что я реально гуглил, и что я нашел
«python requests item some ids missing fields hacker news api» привел меня к несколько Stack Overflow ниткам об API HN специфично, и реальный ответ был почти скучным раз я увидел его: «мертвая» или удаленная история всё ещё показывается в списке topstories, но её поиск item возвращает с нет поля title, иногда даже не реальный объект, просто None. Мой код предполагал что каждый ID в том списке указывает на нормальную, полную историю. Это не так. Где-то в той числе пятидесяти ID утром было одного мертвое, и item["title"] взорвала весь цикл, что убило весь скрипт, что значит нулевых заголовков было получено для любого источника то утро, или любое утро после, потому что падение произошло перед всем что было отправлено и cron просто молча съедает отказ каждый единственный день после этого.
Принятый ответ на нитку которая действительно помогла не был об API HN специфично, это был общий паттерн: никогда не позволяй одному плохому элементу в батче свалить весь батч. Обертки работа по элементам, логируй что не удалось, продолжай.
Версия которая действительно выживает плохой элемент
import logging
import requests
logging.basicConfig(filename="/home/bjorn/digest.log", level=logging.WARNING)
KEYWORDS = ["postgres", "sveltekit", "linux", "norway", "helgeland"]
def fetch_hn_headlines():
ids = requests.get("https://hacker-news.firebaseio.com/v0/topstories.json", timeout=10).json()[:50]
headlines = []
for story_id in ids:
try:
item = requests.get(
f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json", timeout=10
).json()
title = item.get("title")
if not title:
continue
except (requests.RequestException, AttributeError) as e:
logging.warning("skipping story %s: %s", story_id, e)
continue
if any(k in title.lower() for k in KEYWORDS):
headlines.append((title, item.get("url", "")))
return headlinesОдин плохой элемент теперь получает залогирован и пропущен вместо того чтобы свалить весь бег с собой. Я также добавил MAILTO к crontab, поэтому будущее падение действительно достигает моего входящего вместо исчезая в файл логов который я должен помнить проверить.
Переиспользование кода электронной почты
Половина отправки электронной почты в основном поднята прямо из проекта камеры, просто без приложения изображения. Раз ты написал шаблон smtplib один раз, ты не очень хочешь писать его снова. Ты просто копируешь функцию и меняешь что она отправляет.
import smtplib
from email.message import EmailMessage
def send_digest(headlines):
body = "\n".join(f"- {title} ({url})" for title, url in headlines)
msg = EmailMessage()
msg["Subject"] = f"morning digest: {len(headlines)} headlines"
msg["From"] = "digest@example.com"
msg["To"] = "bjorn@example.com"
msg.set_content(body or "Nothing matched today.")
with smtplib.SMTP_SSL("smtp.example.com", 465) as server:
server.login("digest@example.com", "app-password-not-real")
server.send_message(msg)crontab -lMAILTO=bjorn@example.com
0 7 * * * /home/bjorn/.venv/bin/python /home/bjorn/digest.py
NRK feed имел свой собственный маленький сюрприз
Отдельно от падения HN, разбор NRK RSS задохнулся первый раз видеоэлемент только показался в feed без текста тела статьи, просто видео встроено. Мой разборщик предполагал что каждая запись имеет поле description стоящее читать. Это было намного меньшее исправление, простой getattr(entry, "description", "") вместо предположения атрибут существует, но это тот же самый урок дважды в одном скрипте: API или feed рассказывающий тебе форму данных это предложение, не гарантия, и граничные случаи показываются в итоге или нет ты планировал для них.
Фильтрация себя была сложнее чем получение
Реальное скребание, раз оно остановилось валиться, было просто часть. Решение что счета как «стоящее мое внимание» это часть я всё время настраиваю. Сопоставление ключевых слов глупо. Это будет флаговать статью которая упоминает «norway» раз в проходе об чем-то я не забочусь вообще, и это будет упустить что-то действительно релевантное потому что оно использовало синоним вместо моего точного ключевого слова. Я не имею хорошее исправление этого. Может быть просто плохая проблема решать со сопоставлением строк, полностью.
Где это сидит рядом со всем остальным
Это одно и скрипт камеры две вещи я реально построил что запускаются без присмотра по расписанию вместо того чтобы быть вызванным мной печатанием команды. Это другой вид программирования чем приложение todo CLI или инструменты PDF, ближе к «напиши это один раз, доверь это продолжать работать», что мышца я явно не построил до этого года, и одно что видимо нуждается в своей собственной дисциплине обработки ошибок что скрипт ты запускаешь вручную не действительно наказывает тебя за пропуск.
Список ключевых слов молча вырос
Это началось как три слова. Это пять сейчас, и каждое дополнение пришло из того же паттерна: я читал бы что-то релевантное напрямую на одном из сайтов, помню что резюме не было флаговать это, и пошел добавить каким бы словом это не должно было его поймать. «helgeland» был добавлен после местная новость истории о расписании парома что я только обнаружил вторично. Это список ключевых слов что только улучшается реактивно, после что оно уже не поймало что-то один раз, что не большой процесс но видимо единственный что я реально слежу завершить.
Что я сделал бы иначе
Получить пятьдесят элементов HN параллельно вместо один за другим, которое также будет делать весь фактор менее мучительным для отладки потому что одно медленное или зависающее запрос не остановит всё позади. concurrent.futures.ThreadPoolExecutor сидит право там в стандартной библиотеке и я ещё не добрался до этого, большей частью потому что вещь работает сейчас и «работает сейчас» поразительно сильный аргумент против трогания этого снова.
Я также журналил бы успешный бег, не просто не удачный, поэтому тихое утро с истинным нулевым совпадением заголовков и молча упавшим утром с нулевым заголовком не выглядят одинаково снаружи. Сейчас оба производят точно ничего в моём входящем, и я могу только скажу их раньше идя и проверяя файл логов, что то же самое ошибка что вызвала разрыв две недели в первом месте, просто с меньшим радиусом взрыва этот раз.
Всё равно. Это отправляет мне заголовки. Не впечатляющее. Я читаю это с кофе большинство утер, что была искренне вся цель, на утрах это реально запускается.