← На главную

Парсинг RSS/Atom лент на Python или сам себе RSS2Email

Хочу поделиться с вами небольшим скриптом. Как правило, такого рода поделкам на Python я не посвящаю целые посты, а просто дописываю их в список примеров к заметке Как я выбирал скриптовый язык и остановился на Python (кстати, да, там есть обновления). Но этот конкретный скрипт показался мне достаточно интересным, чтобы рассказать о нем отдельно. Как вы уже поняли, он парсит RSS- и Atom-ленты, а затем отправляет информацию о новых записях на указанный e-mail адрес.

Потребность в таком скрипте возникла после того, как я решил отписаться от большинства неинтересных мне фидов, а оставшиеся пару десятков читать через e-mail. Разумеется, для отправки новых записей из RSS на электронную почту уже существует множество готовых сервисов (например, Blogtrottr). Но новые записи они часто высылают с задержкой и/или с прикрепленной рекламой и/или за деньги, да и вообще завязываться на очередной SaaS из-за такой ерунды не хотелось. Вы ведь еще помните историю с Google Reader, верно? Так вот, в итоге получилось действительно удобнее, чем использовать отдельное приложение для чтения RSS (я пользовался Liferea, а до этого – Feedly). Как минимум, теперь у меня меньше нужных мне приложений, а значит в iPhone’е меньше иконок, а в Unity появился лишний свободный хоткей. Да и просто удобно, когда любые интересующие тебя события приходят в одно-единственное место.

Итак, писать, само собой разумеется, будем не все с нуля, а возьмем готовый модуль feedparser (модуль на PyPI, документация):

sudo pip3 install feedparser

А вот и сам скрипт (репозиторий на GitHub):

#!/usr/bin/env python3 # feed2email.py # (c) Aleksander Alekseev 2016 # http://eax.me/ import feedparser from smtplib import SMTP # from smtplib import SMTP_SSL as SMTP from email.mime.text import MIMEText from contextlib import contextmanager import signal import getpass import hashlib import time import sys import re server = 'smtp.yandex.ru' port = 587 # 25 login = "YOUR_LOGIN" from_addr = "NEWS <YOUR_LOGIN@yandex.ru>" receiver = "YOUR_EMAIL" processed_urls_fname = "processed-urls.txt" feed_list_fname = "feed-list.txt" # change to True before first run or you will receive A LOT of emails # then change back to False fake_send = False sleep_time = 60*5 # seconds net_timeout = 10 # seconds smtp_retry_time = 30 # seconds smtp_retries_num = 5 # >>> import hashlib # >>> hashlib.sha1(b"qwerty").hexdigest() # 'b1b3773a05c0ed0176787a4f1574ff0075f7521e' pwhash = 'b1b3773a05c0ed0176787a4f1574ff0075f7521e' # FUNCS class TimeoutException(Exception): pass @contextmanager def timeout_sec(seconds): def signal_handler(signum, frame): raise TimeoutException(Exception("Timed out!")) signal.signal(signal.SIGALRM, signal_handler) signal.alarm(seconds) try: yield finally: signal.alarm(0) def file_to_list(fname): rslt = [] with open(fname, "r") as f: rslt = [x for x in f.read().split("\n") if x.strip() != "" ] return rslt # MAIN password = getpass.getpass("SMTP Password: ") if hashlib.sha1(bytearray(password, 'utf-8')).hexdigest() != pwhash: print("Invalid password", file = sys.stderr) sys.exit(1) while True: feed_list = file_to_list(feed_list_fname) # filter comments feed_list = [ x for x in feed_list if not re.match("(?i)\s*#", x) ] keep_urls = 100*len(feed_list) processed_urls = [] try: processed_urls = file_to_list(processed_urls_fname) except FileNotFoundError: pass print("Processing {} feeds...".format(len(feed_list))) for feed in feed_list: print(feed) f = None try: with timeout_sec(net_timeout): f = feedparser.parse(feed) except TimeoutException: print("ERROR: Timeout!") continue feed_title = f['feed'].get('title', '(NO TITLE)') feed_link = f['feed'].get('link', '(NO LINK)') for entry in f['entries']: if entry['link'] in processed_urls: continue subject = "{title} | {feed_title} ({feed_link})".format( title = entry.get('title', '(NO TITLE'), feed_title = feed_title, feed_link = feed_link ) print(subject) summary = entry.get('summary', '(NO SUMMARY)') body = "{summary}\n\n{link}\n\nSource feed: {feed}".format( summary = summary[:256], link = entry['link'], feed = feed ) print(body) print("-------") msg = MIMEText(body, 'plain') msg['Subject'] = subject msg['From'] = from_addr msg['To'] = receiver if not fake_send: for attempt in range(1, smtp_retries_num+1): try: with timeout_sec(net_timeout), SMTP(server, port) as conn: conn.starttls() conn.login(login, password) conn.sendmail(from_addr, [receiver], msg.as_string()) break except Exception as exc: print(("Failed to send email {}/{} - {}, " + "retrying in {} seconds").format( attempt, smtp_retries_num, exc, smtp_retry_time ) ) time.sleep(smtp_retry_time) processed_urls = [ entry['link'] ] + processed_urls with open(processed_urls_fname, "w") as urls_file: urls_file.write("\n".join(processed_urls[:keep_urls])) print("Sleeping {} seconds...".format(sleep_time)) time.sleep(sleep_time)

Список фидов можно получить из OPML-файла при помощи примерно такого однострочника:

cat feeds.opml | python3 -c 'import re,sys; [ print(m.group(1)) for '\ 'ln in sys.stdin for m in [ re.search("(?i)xmlUrl=\"(.*?)\"", ln) ] '\ 'if m is not None ]'

Теперь правим немного настройки, запускаем на каком-нибудь сервере в screen и радуемся. Все равно у любого уважающего себя программиста в наше время уже есть сервер в DigitalOcean под VPN, Syncthing, закрытые Git-репозитории и другие подобного рода вещи, верно? Нечего ему простаивать без дела.

Пара интересных свойств приведенного скрипта:

  • Если в несколько фидов приходит новость с одним URL (такое бывает, если подписаться на агрегатор фидов и некоторые блоги из этого агрегатора), вы получите уведомление о новости только один раз;
  • На время отпуска скрипт можно остановить, а вернувшись из отпуска один раз прогнать его с fake_send = True – и не придется разгребать все пропущенные новости;
  • Все данные хранятся в текстовых файлах, которые так удобно редактировать Vim’ом и бэкапить Git’ом, притом в списке фидов можно использовать комментарии;

А чем вы в это время суток читаете RSS?

Дополнение: Если вас заинтересовала тема отправки писем по SMTP, обратите также внимание на пост Пример сбора почты по протоколу IMAP на Python.