Доставка свежей прессы с помощью Python прямо в почтовый ящик

В данной статье будут освещены следующие возможности python'a:
  • парсинг web-страницы с помощью простого регулярного выражения;
  • скачивание файла с web-страницы;
  • отправка скаченного файла через smtp-сервер;
  • написание небольшого обобщающего скрипта.

Все это будет сопровождено работающими примерами.

Предыстория


Многие помнят стандартную киносцену из типичного утра в американском пригородном посолке: парень на велоспипеде едет и раскидывает газету. А потом добрая большая и лохматая собака приносит газету главе семейства. Но прогресс идет и газеты теперь можно получить в цифровом формате, можно даже заказать их доставку на почту. На некоторых сайтах эта возможность предоставляется за деньги, на моем любимом, но страшно далеком от мира IT Спорт-Экспрессе, эта возможность отсутствует вообще. Но есть возможность скачать pdf.
В связи с чем каждое утро приходилось заходить на сайт, там проходить по ссылку, дальше скачивать себе на компьютер, после чего отправлять на свой kindle-ящик, потом на Kindle включить wi-fi — и вот газету можно читать и в метро, и в электричке.

Простейший анализ web-страницы


Для работы с вебом используется модуль urllib2. Данный модуль позволяет получать содержимое страниц, а так же еще много чего. Но в данном случае нам хватит одной его функции: просмотра страницы.

    def download_by_link(self, link):
        content = urllib2.urlopen(link).read()   # считываем html, расположенный по адресу link
        link_on_file, filename = self.get_filename(content)  # получаем из html ссылку для скачивания
        fullname = self.get_dir_name() + filename
        if self.is_new_version(fullname):
            with open(fullname, 'w') as fd:                                
                content = urllib2.urlopen(link_on_file).read()  # скачиваем файл
                fd.write(content)                                                    
        return self.get_prepared_files(fullname)


with — это аналог using в С# и try в Java 7. Если кратко, with гарантирует, что ресурсы, захваченные в данном блоке будут освобождены, как только мы выйдем за его пределы (в случае, обычного выхода или, например, исключения).

Для того, чтобы получить ссылку на архив со свежим выпуском газеты используем модуль re — реализующий всю мощь регулярных выражений. Правда, как и в случае с urllib2 нам опять не потребуется ничего сверхъестественного:
   def get_filename(self, text):
         pattern = r'(?P<link>http:\/\/archive\.sport\-express\.ru\/pdf\/(?P<filename>[0-9]+\.zip))'
         match = re.search(pattern, text)
         return match.group('link'), match.group('filename')


(?P ) — так задается именованная группа в регулярке. Таким образом мы получает линк и имя файла.

class SportExpress():
    def get_from(self):
        return 'username@mail.ru'

    def get_title(self):
        return 'Sport Express subscribe'

    def get_text(self):
        return "Good morning! Don't be lazy - read newspaper!"

    def get_prepared_files(self, archive):                                      
        directory_to_extract = archive[:archive.rfind('.')]               
        zipfile.ZipFile(archive).extractall(directory_to_extract)         # опасно использовать на произвольном содержимом
        res = glob.glob(directory_to_extract+'/*.*')
        return res

    def get_dir_name(self):
        dir_name = 'archive/sport-express/'
        if not os.path.exists(dir_name):
            os.mkdir(dir_name)
        return dir_name

    def is_new_version(self, filename):
        return os.path.exists(self.get_dir_name() + filename) 


Создаем письмо


Теперь, когда у нас есть содержимое, которое мы планируем высылать, неплохо было бы создать для него «конверт».
Для работы с письмами в питоне используется модуль… используется модуль… используется модуль email.
import email, email. encoders, email.mime.text, email.mime.base

class MessageBase:
    def __init__(self, subscriber):
        self.__to_addresses = subscriber.get_subscribers()
        self.__from_address = subscriber.get_from()
        self.__message = self.__create_message(subscriber.get_title(), subscriber.get_text())

Для начала создадим простое текстовое письмо. Подробности про MIME доступны, конечно же, здесь.
 def __create_message(self, subject, text):
        email_msg = email.MIMEMultipart.MIMEMultipart('mixed')
        email_msg['Subject'] = subject
        email_msg['From'] = self.get_from()
        email_msg['To'] = ', '.join(self.get_to())
        email_msg.attach(email.mime.text.MIMEText(text,'text'))
        return email_msg

Пришла пора создать функцию для прикрепления файла. Если хотите, чтобы почтовый клиент мог открыть вложения необходимо указывать file_type.
    def attach_file(self, filename, file_link, file_type = 'unknown'):
        file_message = email.mime.base.MIMEBase('application', file_type)
        file_message.set_payload(file(file_link).read())
        email.encoders.encode_base64(file_message)
        file_message.add_header('Content-Disposition','attachment;filename='+filename)
        self.__message.attach(file_message)
        return True


Для того, чтобы получить из сообщения сообщение для отправки нужно вызвать метод as_string().
    def get_text(self):
        return self.__message.as_string()


Отправляем письмо


Теперь когда есть письмо, нужно уметь его отправлять. За отправку писем в Python'e отвечает smtplib.
import smtplib

from message import MessageBase

class SmtpBase:
    def __init__(self, serverhost):
        self.__smtp_server = serverhost

    def open(self):
        self.__server = smtplib.SMTP(self.__smtp_server)
        self.__server.login('<enter your smtp login>', '<enter your smtp password>')

    def close(self):
        self.__server.quit()


Представим что у нас есть класс Message с методами get_from(), get_to(), get_text().
    def send_mail(self, message):
        for message_to in message.get_to():
            self.__server.sendmail(message.get_from(), message_to, message.get_text())


Чтобы поддерживать 'with' необходимо добавить пару методов:
    def __enter__(self):
        self.open()
        return self

    def __exit__(self, type, value, traceback):
        if type:
            print '%s: %s %s' % (type, value, traceback)
        self.close()


Обобщаем


Теперь у нас есть всё, что нужно.
Мой итоговый скриптик выглядит так:
from subscriber_sex import SportExpress
from smtp import SmtpBase
from message import MessageBase

b = SportExpress()
filenames = b.download_by_link('http://www.sport-express.ru')

msg = MessageBase(b)
for file in filenames:
    msg.attach_file(file[file.rfind('/')+1:], file, 'pdf')

with SmtpBase('smtp.yandex.ru') as s:
    s.send_mail(msg)

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

А так, для того, чтобы скачивать новый журнал — вам всего лишь нужно изменить 3 функции.

Надеюсь, что материал был полезен и приятен для читателя.
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 21
  • 0
    Ведомости публикуют на сайте свежий номер в fb2. А можно Вам заказать законченный скрипт, который будет скачивать номер, конвертировать его в mobi и отправлять на kindle-ящик?
  • 0
    А что избавляет нас от письма без аттача, если новый выпуск не найден?
    • 0
      Вопрос хороший. Сперва сам запутался. Но на самом деле мы получим еще раз письмо с тем же выпуском. Правда, на деле эта функция используется не так часто, т.к. у меня скрипт запускается по cron'у ровно так, как выпускается газета. Подводят только праздники. Но к Новому Году я надеюсь внести необходимые изменения.
      • 0
        Раскрою мысль чуть подробней. Когда нет свежего выпуска, на сайте доступен последний доступный выпуск. Т.е. ситуация, что нет архива — это реально проблема.
    • 0
      А чем вам мешает письмо без аттача? Оно фактически является срочным сообщением о проблеме.
      • +4
        Прочитал:

        > парсинг web-страницы с помощью простого регулярного выражения

        и сразу вспомнился замечательный ответ со stackoverflow.

        А вообще, чем pdf лучше rss-рассылки, которая есть и на Спорт-Экспрессе в том числе? Тем более, что для rss есть хорошие решения.
        • +1
          lxml рулит для парсинга web-страниц
          • 0
            Rss-рассылка неполная. Т.е. там текст новостей неполный. И там бывает «мусор». В газете же материал отобран редактором, как наиболее перспективный/интересный. Для себя я сделал выбор в пользу pdf.

            Спасибо за ссылку. Но у меня немного другая ситуация, чем у топик-стартера по ссылке.
            • 0
              А тут полной rss-ленты не находится?
              • 0
                Вполне может быть находится. Но тогда бы пришлось скачивать и отправлять на Kindle. У меня Kindle без 3G.
          • 0
            Откройте для себя wwwsearch.sourceforge.net/mechanize/
            • 0
              > r'(?Phttp:\/\/archive\.sport\-express\.ru\/pdf\/(?P[0-9]+\.zip))

              Знаки "/" и "-" можно не экранировать.
              • +2
                Вместо
                with open(fullname, 'w') as fd:                                
                    content = urllib2.urlopen(link_on_file).read()  # скачиваем файл
                    fd.write(content)                                                    
                

                можно использовать docs.python.org/library/urllib.html#urllib.urlretrieve

                Если бы использовали os.path, то можно было бы сделать скрипт кроссплатформенным…

                Еще момент:
                glob.glob(directory_to_extract+'/*.*')
                

                проще os.listdir(directory_to_extract)
                • 0
                  Спасибо за замечания. Учту,
                • 0
                  > (?P ) — так на Python'e задается группа в регулярке.

                  Так задается _именованная_ группа (см. PCRE 7.0). Не именованная группа затается просто при помощи скобок.
                  • 0
                    Вы абсолютно правы. Исправил. Тем более, что в примере используется именно именованная группа.
                    • 0
                      Эм, ну я хотел еще акцентировать внимание на том, что это фича именно PCRE (версию тут можно правда опустить), а не Python'а :) У вас получается, что это чисто питоновское, но на самом деле named group capturing можно использовать и в PHP и в Ruby и во многих других языках. В Питоне просто многие с этого начитают так как обычно сразу знакомятся с Django и его роутингом.
                  • 0
                    Открыл для себя Flipboard — самый удобный способ читать новости с телефона.
                    • 0
                      хочу сделать то же самое для the economist
                      • 0
                        если у Вас возникнут с этим сложности, обращайтесь — постараюсь помочь.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.