Скачиваем аттачменты из Gmail
Этот пост будет сугубо техническим. Никакой интересной математики, сплошное решение практических задач.
Несмотря на весь прогресс науки и техники, всевозможные LMS и прочие чудеса, до сих пор самым надёжным способом принять домашние задания у студентов в электронной форме остаётся присылка их на e-mail преподавателя. Конечно, это ужасно. Иногда удаётся уговорить вместо этого загрузить файлы в специальную формочку (которая, к слову, очень легко делается с помощью Google Drive и и каких-то там скриптов), но всё равно где-то для 15-20% студентов это окажется непреодолимой трудностью. А e-mail работает более-менее всегда. Поэтому проведя контрольную работу по Python в прошедший вторник, я решил не мудрствовать лукаво и предложил сдать мне работы в виде ipynb-файлов, отправив их по почте.
И вот сейчас, собравшись с духом и надумав их всё-таки проверить, я столкнулся с необходимостью открыть три десятка писем и ручками скачать оттуда вложения. Энтузиазм как-то сразу иссяк...
Впрочем, о чём это я? Какими такими ручками? 2014-й год за окном (уже почти закончился), всё поддаётся автоматизации! Итак…
Как пройти на почту¶
В общем случае работать с любым почтовым сервисом можно с помощью стандартных почтовых протоколов — точно так же, как работает любой почтовый клиент. Есть старый-добрый POP3, есть IMAP, и можно быстренько написать «мини-клиент», который подключится к серверу и скачает всё, что нужно. Но в случае с Gmail есть ещё и их собственное API, которое позволяет получить наиболее естественный доступ ко всем возможностям Gmail (например, тредам), а также безопасную авторизацию. Им я и решил воспользоваться.
Вот тут лежит документация, а точнее руководство для быстрого старта, ориентированное на Python. После получения ключа для API и установки google-api-python-client
, приведенный там примерчик, конечно, не заработал, поскольку потребовал какую-то библиотеку gflags
(которая, как оказывается, на самом деле называется python-gflags
), но после её установки всё-таки заработал (поругавшись на то, что там что-то устарело): открыл браузер и попросил авторизовать моё приложение. Я согласился.
Дальше всё было легко. Ну, или почти легко.
Шаг 1. Выбираем нужные сообщения¶
Можно было бы сделать какой-нибудь хитрый запрос, чтобы обрабатывать только сообщения, пришедшие в определенный интервал времени, но я поступил проще: в интерфейсе Gmail вручную присвоил всем нужным мне письмам метку py-cw1
(это было легко сделать, поскольку все эти письма шли подряд и их можно было выделить двумя кликами). Теперь нужно получить список всех сообщений с этой меткой. А для начала — её внутренний идентификатор (метку можно переименовать, а идентификатор не изменится). Делается это так:
labels=gmail_service.users().labels().list(userId='me').execute()
for label in labels['labels']:
if label['name']=='py-cw1':
py_cw1_label=label['id']
print py_cw1_label
Теперь нужно получить список всех сообщений с этой меткой. Я было собрался написать этот код сам, но полез в документацию и обнаружил там пример ровно про это. (Я не сразу заметил, что рядом с кодом примера есть переключатель языков и поначалу пытался переводить с Java, но там и для Python всё есть.) Нас интересует функция ListMessagesWithLabels
, которая прекрасно заработала безо всяких модификаций.
messages=ListMessagesWithLabels(gmail_service,'me',py_cw1_label)
print messages[0:3]
Конечно, эти id'шники нам ни о чём не говорят — и не надо. Мы будем их использовать, чтобы обрабатывать сообщения по одному.
Шаг 2. Скачиваем вложения¶
Дальше я стал думать о том, как пройтись по всем письмам и из каждого письма вытащить и сохранить на диск вложения (attachments). Размышления мои были недолгими, поскольку в соответствующем разделе справки по API снова был приведён пример, делающий ровно то, что мне надо (функция GetAttachments
). Вернее, не делающий — потому что функция в документации написана с ошибками.
Первая ошибка была простой: вместо users()
там написали user()
. Эту опечатку я быстро поправил. А дальше всё оказалось интереснее, потому что при попытке запустить эту штуку она стала сваливаться с ошибкой KeyError
, не находя в ответе сервера нужных полей.
Оказывается, при запросе содержимого сообщения может быть возвращено либо само вложение, либо его идентификатор. В последнем случае потребуется ещё один запрос, чтобы получить сам файл. В документации это отражено, а в примере почему-то нет. Мне пришлось переписать функцию GetAttachments
, исправив там эти ошибки. (Отмечу, что проблема возникла не только у меня.)
import base64
from apiclient import errors
# based on Python example from
# https://developers.google.com/gmail/api/v1/reference/users/messages/attachments/get
# which is licensed under Apache 2.0 License
def GetAttachments(service, user_id, msg_id, prefix=""):
"""Get and store attachment from Message with given id.
Args:
service: Authorized Gmail API service instance.
user_id: User's email address. The special value "me"
can be used to indicate the authenticated user.
msg_id: ID of Message containing attachment.
prefix: prefix which is added to the attachment filename on saving
"""
try:
message = service.users().messages().get(userId=user_id, id=msg_id).execute()
for part in message['payload']['parts']:
if part['filename']:
if 'data' in part['body']:
data=part['body']['data']
else:
att_id=part['body']['attachmentId']
att=gmail_service.users().messages().attachments().get(userId=user_id, messageId=msg_id,id=att_id).execute()
data=att['data']
file_data = base64.urlsafe_b64decode(data.encode('UTF-8'))
path = prefix+part['filename']
with open(path, 'w') as f:
f.write(file_data)
except errors.HttpError, error:
print 'An error occurred: %s' % error
Ну а теперь осталось пройтись по всем найденным сообщениям и скормить их этой функции.
for i,msg in enumerate(messages):
m_id=msg['id']
try:
GetAttachments(gmail_service, 'me', m_id, str(i)+"_")
print "Processed ", m_id
except KeyError, e:
if 'parts' in str(e):
print "No attachment in message ", m_id
else:
print "Something wrong in message ", m_id, e
Одно сообщение попало в список по ошибке (не содержало вложений), а остальные успешно обработались и сохранились!
Конечно, я потратил больше времени на освоение Gmail API, чем ушло бы на сохранение этих файлов вручную, но зато это было гораздо увлекательнее! Если это не кажется вам достойным объяснением, предложу другое: если мне понадобится в будущем что-то в массовом порядке сделать со своей почтой, мне теперь будет гораздо проще.
Комментарии