- PVSM.RU - https://www.pvsm.ru -
Автор материала рассказывает, как ему удалось применить навыки программирования для автоматизации рутинных процессов собственного свадебного мероприятия.
Для большинства людей 3 сентября 2016 было самой обычной субботой, но в моей памяти эта дата останется навсегда, ведь именно в этот день мы с моей супругой сыграли свадьбу.
Планируя свадьбу, вам нужно учесть множество моментов: еда, оформление и обстановка, настольные светильники (да, отдельно от оформления), цветы, размещение гостей, транспорт, развлечения и выбор места мероприятия. Вообще, планируя свадьбу, вы сталкиваетесь со множеством неизвестных, однако в одном я был уверен наверняка: ни одна свадьба не обходится без целой кучи списков, вложенных списков, не заканчивающихся до самого конца. Чем больше списков проплывали перед моими глазами, тем чаще я стал задумываться о том, как можно улучшить процесс подготовки. Уж больно неэффективно и вручную выполнялась вся работа. Я был уверен, что технологии наверняка смогут помочь улучшить хотя бы некоторые моменты.
Вы, возможно, удивитесь, узнав, что приглашение людей на свадьбу — дело дорогое (более 380 фунтов на человека). Сначала вам нужно отправить предварительные пригласительные с датой и коротким уведомлением, и только потом — полноценные, с более подробным описанием события. Все это, к тому же отправляется по почте, а значит, происходит медленно. Немало времени отнимают и попытки «поймать» приглашенных и получить от них ответ о том, хотят ли они прийти на праздник с бесплатной едой и выпивкой (хотя, казалось бы, кто не хочет?!). Ну и, наконец, рассылка приглашений не экологична, ведь бумажные открытки — вещь одноразовая, быстро забывается и становится никому не нужной.
Но вернемся к спискам. Гостей мы делим на несколько групп:
Но списки мне нравятся: у них есть некие предопределенные требования, превращающие их в отличный объект для автоматизации.
Я был уверен, что у всех потенциальных гостей, независимо от их возраста, имелся мобильный телефон, а это означало, что настало время Twilio. Приведенный здесь код можно при желании смело пропускать, так как он всегда доступен в соответствующем репозитории GitHub [1].
SMS [2] как коммуникационный канал прекрасно подходил под мои нужды. Я мог настроить массовую рассылку сообщений, быстро и эффективно обрабатывая ответы. Делая первые рабочие наброски продукта и рассматривая варианты БД, я пытался сделать нечто простое, чем можно было бы легко поделиться и не хотел уделять много времени внешнему виду. В итоге я наткнулся на python-библиотеку gspread, позволявшую читать из таблиц google [3] и писать в них. Это был не самый быстрый, но довольно гибкий вариант, предоставивший возможность легко получать доступ к таблицам и считывать результаты.
Для первого приглашения я создал таблицу [4] с тремя колонками:
Завершив ввод основных данных, я прогнал список через gspread [5], который отправил SMS каждому гостю-обладателю мобильного номера:
import json
import time
import gspread
from oauth2client.client import SignedJwtAssertionCredentials
from twilio.rest import TwilioRestClient
# отправка сообщений гостям из таблицы
# добавляем имя файла для json, созданного для таблицы
json_key = json.load(open('.json'))
scope = ['https://spreadsheets.google.com/feeds']
credentials = SignedJwtAssertionCredentials(json_key['client_email'],
json_key['private_key'].encode(),
scope)
gc = gspread.authorize(credentials)
wks = gc.open("wedding_guests") # здесь добавляем имя своего workbook-файла
wks_attendees = wks.get_worksheet(0) # обращаемся к списку посетителей
ACCOUNT_SID = 'TWILIO_ACCOUNT_SID'
AUTH_TOKEN = 'TWILIO_AUTH_TOKEN'
client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN)
# проходимся по списку гостей, значения внутри range заменяем на свои
for num in range(2, 60):
print "sleeping for 2 seconds"
time.sleep(2) # добавляем задержку чтобы мобильный оператор не отфильтровал сообщения
guest_number = wks_attendees.acell('B'+str(num)).value
guest_name = wks_attendees.acell('A'+str(num)).value
Message_body = <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-pds">"</span><span class="pl-cce">nn</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2709</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span><span class="pl-s"><span class="pl-pds">"</span> Save the date! <span class="pl-pds">"</span></span><span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2709</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span><span class="pl-s"><span class="pl-pds">"</span><span class="pl-cce">nn</span>Lauren Pang and Thomas Curtis are delighted to invite you to our wedding.<span class="pl-cce">nn</span>Saturday 3rd September 2016. <span class="pl-cce">nn</span>Colville Hall,<span class="pl-cce">n</span>Chelmsford Road,<span class="pl-cce">n</span>White Roding,<span class="pl-cce">n</span>CM6 1RQ.<span class="pl-cce">nn</span>The Ceremony begins at 2pm.<span class="pl-cce">nn</span>More details will follow shortly!<span class="pl-cce">nn</span>Please text YES if you are saving the date and can join us or text NO if sadly, you won't be able to be with us.<span class="pl-cce">nn</span><span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span>,
if not guest_number: # гостей без мобильного номера пропускаем
print guest_name + ' telephone number empty not messaging'
wks_attendees.update_acell('E'+str(num), '0')
else:
print 'Sending message to ' + guest_name
client.messages.create(
to="+" + guest_number, # Добавляем + для соответствия e.164
from_="", # сюда вставляем свой номер Twillio
body=message_body,
)
wks_attendees.update_acell('E'+str(num), int(wks_attendees.acell('E'+str(num)).value) + 1) # increment the message count row
else: # else-часть цикла
print 'finished'
И поскольку текстовые SMS обычно выглядят скучновато, я добавил немного юникода [6], чтобы их оживить. Вот как выглядело сообщение для приглашенных счастливчиков:
Далее я воспользовался Flask в качестве веб-сервера, сделал так, чтобы мой реквест URL для Twilio Messaging указывал на /messages и добавил простые if-проверки для парсинга ответов:
@app.route("/messages", methods=['GET', 'POST'])
def hello_guest():
if "yes" in body_strip:
# Ищем гостя и обновляем его confirmation_status
wks_attendees.update_acell("F"+str(guest_confirmation_cell.row), 'Accepted') # обновляем статус на «приглашение принято» для этого гостя
resp.message(u"u2665" + "Thanks for confirming, we'll be in touch!" + u"u2665") # rотвечаем гостю, подтвердившему участие
elif "no" in from_body.lower():
# обновляем статус на «приглашение отклонено» для этого гостя
wks_attendees.update_acell("F"+str(guest_confirmation_cell.row), 'Declined')
# отвечаем отказавшему гостю
resp.message("Sorry to hear that, we still love you though!")
else: # отвечаем тем, кто дал некорректный ответ
resp.message("You sent a different keyword, we need a yes or a no, you sent: "+
from_body)
return str(resp)
Первое сообщение было отправлено в 8:37 утра 19 февраля, а первое подтверждение получено чуть позже в 8:40. К 9:38 я получил уже 23 подтверждения, то есть 32% ответов были у меня в кармане! Через 2 дня после начала массовой рассылки свое участие подтвердили уже 58% гостей. Несмотря на очевидный успех моя будущая жена пока что не была на все 100% впечатлена моим SMS-сервисом свадебных приглашений, и я решил добавить приложению еще немного функциональности.
Статистика! Я мог формировать актуальный список гостей и предоставлять его по первому запросу, давая моей будущей невесте мгновенную обратную связь. Код оказался довольно простым, так как я уже настраивал до этого некоторые простейшие счетчики в таблице и потому все сводилось к получению содержания отдельных ячеек и их добавлению в SMS:
# переменные статуса гостей
guest_confirmed = wks_attendees.acell('C70').value
guest_unconfirmed = wks_attendees.acell('C71').value
guest_no_response = wks_attendees.acell('C72').value
guest_acceptance = wks_attendees.acell('C73').value
elif "numbers" in from_body.lower():
# возвращаем статистику (общее кол-во гостей, список выбранных блюд)
resp.message("R.S.V.P update:nnTotal Accepted: " + guest_confirmed
"nnTotal declined: " guest_unconfirmed "nnTotal no response: "+
guest_no_response + "nnTotal acceptance rate: " + guest_acceptance)
Пример отправляемой этим кодом SMS:
Выглядит, быть может, не очень красиво, но зато весьма информативно.
Тот факт, что Лорен теперь могла следить за автоматическими обновлениями списка гостей избавил нас от многих головных болей. В итоге я получил от нее добро на повсеместную интеграцию SMS и вскоре этот инструмент использовался едва ли не во всех процессах, где это только было возможно. Некоторые из применений были очевидны, например, отправка SMS-уведомлений о запуске свадебного сайта (сделанного, к слову, на Heroku [7]), или работа со списками свадебных подарков и многие другие решения, которыми я горжусь и по сей день.
После составления списка приглашенных и сбора ответов, дело дошло до занятия, которое обычно откладывают на потом — выяснение вкусовых предпочтений гостей. Первым шагом была отправка еще одной SMS, сообщающей гостям о необходимости посетить веб-сайт и выбрать категории подаваемой еды с помощью google-формы. Выглядела форма вполне обыкновенно, но заполняла тот же файл, в котором находились данные о посетителях. Таким образом, теперь у нас была как таблица принявших приглашения посетителей, так и таблица заполнивших форму выбора еды. Если бы речь шла о каком-нибудь обычном средстве автоматизации, мне пришлось бы ждать, пока гости будут медленно выбирать свои блюда, однако моя свадьба проходила при поддержке Twillio, и это значит, что я мог добиваться ответа от гостей с минимальными усилиями.
Необходимо было сверять две таблицы по имени гостей и обновлять статус выбранных блюд при поступлении новой информации. Это потребовало некоторых дополнительных наворотов в коде, но как только я закончил с ними, я мог по первой необходимости запускать скрипт, получая на выходе SMS с актуальной информацией:
import json
import time
import gspread
from oauth2client.client import SignedJwtAssertionCredentials
from twilio.rest import TwilioRestClient
# добавляем имя файла для json, созданного для таблицы
json_key = json.load(open(''))
scope = ['https://spreadsheets.google.com/feeds']
credentials = SignedJwtAssertionCredentials(json_key['client_email'],
json_key['private_key'].encode(),
scope)
gc = gspread.authorize(credentials)
wks = gc.open("") # здесь добавляем имя своей таблицы
wks_attendees = wks.get_worksheet(0) # список гостей
wks_food = wks.get_worksheet(1) # список выбора блюд
ACCOUNT_SID = 'TWILIO_ACCOUNT_SID'
AUTH_TOKEN = 'TWILIO_AUTH_TOKEN'
client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN)
# Обрабатываем список гостей. Кол-во указано вручную чтобы никого не забыть
for num in range(2, 60):
food_guest_name = wks_food.acell('B'+str(num)).value # ячейка из колонка с названиями блюд
if food_guest_name:
attendees_name = wks_attendees.find(val_food_guest_name).value
attendees_name_row = wks_attendees.find(val_food_guest_name).row
menu_status = wks_attendees.acell("G"+str(attendees_name_row)).value
if food_guest_name == attendees_name:
print
if menu_status == 'Y': # данные уже сравнили, идем дальше
print('Skipping')
else: # пользователь сделал свой выбор, обновляем основную таблицу
print ('Food sheet name ' + food_guest_name + 'Attendees sheet name ' + attendees_name)
# обновляем строку выбора меню
wks_attendees.update_acell("G"+str(attendees_name_row), 'Y')
else:
print('nothing found, moving on')
wks_attendees.update_acell('E'+str(num), int(wks.acell('E'+str(num)).value) + 1) # инкремент строки подсчета сообщений
else:
# отправляем админу сообщение о завершении обновления статистики
client.messages.create(from_="", # номер Twillio
to="", # номер админа
body="Finished processing current meal listnnGuest meals confirmed" + guest_meals_confirmed + "nnGuest meals unconfirmed: " + guest_meals_unconfirmed)
Теперь, когда в моем распоряжении был точный список гостей и постоянно пополняющийся список блюд, имело смысл сделать эту статистику общедоступной с помощью главного приложения. Для этого требовалось лишь добавить содержимое соответствующих ячеек в SMS-ответ:
# отправляем актуальные данные об общем количестве еды и выборе блюд
elif "food" in body_strip.strip():
resp.message("Guest meals decided:" + guest_meals_confirmed +
"nGuest meals undecided: " + guest_meals_unconfirmed +
"nnMenu breakdown:nn" + starter_option_1 +": " +
starter_option_1_amount + "n" + starter_option_2 +": " +
starter_option_2_amount + "n" + starter_option_3 +": " +
starter_option_3_amount + "n" + main_option_1 +": " +
main_option_1_amount + "n" + main_option_2 +": " + main_option_2_amount +
"n" + main_option_3 +": " + main_option_3_amount + "n" +
dessert_option_1 + ": " + dessert_option_1_amount + "n" + dessert_option_2
+ ": " + dessert_option_2_amount)
Эта мера оказалась очень полезной, поскольку позволяла обслуживающей праздник фирме быть в курсе нашего прогресса и предоставляла очень полезную с практической точки зрения информацию о тех, кто еще не сделал свой выбор. Следующим претендентом на автоматизацию был процесс получение ответов от гостей. Для этого нужно было всего лишь проходить по списку, находить в нем «нарушителей», не выбравших себе блюда, и отправлять им сообщения!
for num in range(2, 72): # и снова вручную чтобы никого не забыть
print "sleeping for 3 seconds"
time.sleep(3) # опять небольшая задержка чтобы оператор не отсеял сообщение как спам
wedding_guest_number = wks_attendees.acell('B'+str(num)).value # берем номер гостя
wedding_guest_name = wks_attendees.acell('A'+str(num)).value # берем имя гостя
menu_guest = wks_attendees.acell('G'+str(num)).value
if not wedding_guest_number:
print wedding_guest_name+' telephone number empty not messaging' # выведем на консоль, что у гостя нет тел. Номера и мы не можем связаться с ним
wks_attendees.update_acell('H'+str(num), '1') # инкремент строки подсчета сообщений для отдельного пользователя
else:
if menu_guest == "N": # гость не выбрал еду! НЕ ОТСТАВАТЬ ОТ НЕГО ДО ПОСЛЕДНЕГО!
print 'Sending message to '+wedding_guest_name
client.messages.create(
to="+" + wedding_guest_number,
from_="", # ваш номер Twillio
body="If you have received this message, you have not chosen your food options for Tom & Lauren's Wedding!nnYou can pick your choices via the website, no paper or postage required!nnhttp://www.yourwebsitehere.com/food"
)
wks_attendees.update_acell('H'+str(num), int(wks_attendees.acell('H'+str(num)).value) + 1) # инкремент строки подсчета сообщений для отдельного пользователя
else: # else-часть цикла
print 'finished'
Большой день близился быстрее, чем мы могли себе представить. Единственное, что нам оставалось сделать — отправить последнее SMS, напоминающее гостям об основных деталях и необходимости вооружиться зонтом, который поможет защититься от типично дождливого британского лета:
Организация свадьбы никогда не бывает простым. В любой момент вам может показаться что многие аспекты мероприятия выходят из-под вашего контроля. Автоматизация определенно сделала мою жизнь проще, предоставив прямой канал общения с гостями и бесчисленное количество инструментов, позволяющих следить за их ответами и по-всякому напоминать им о необходимости определиться и ответить. Она помогла нам взять в свои руки одно из самых нудных дел, сопровождающих подобные мероприятия, позволила освободить кучу времени и сосредоточиться на других важных составляющих столь важного в нашей жизни события.
Создание масштабируемых решений для комплексных задач никогда не бывает простым, и даже один из конечных вариантов моего приложения местами еле справлялся с поставленными задачами. Изначально я планировал разработать более комплексное решение, с визуализацией прогресса, интеграцией голоса, менее зависимое от CLI-скриптов, но время одержало в этой гонке верх. В целом я доволен тем, как все получилось. Идеальных коммуникационных систем не бывает. Всегда нужно использовать наиболее подходящий для вашей аудитории канал, будь то SMS [2], Voice [8], Chat [9], Video [10] или семафор [11].
Если захотите поговорить об автоматизации свадеб, пишите мне в Twitter [12].
Автор: Wirex
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/253772
Ссылки в тексте:
[1] репозитории GitHub: https://github.com/SeekTom/Twilio/tree/master/Wedication
[2] SMS: https://www.twilio.com/docs/api/rest/sending-messages
[3] таблиц google: https://www.twilio.com/blog/2017/02/an-easy-way-to-read-and-write-to-a-google-spreadsheet-in-python.html
[4] создал таблицу: https://docs.google.com/spreadsheets/d/1Zud0nYlAQw7RywwiDmADf9Cd3bBTsHaUSQYXh_Cl9_w/edit?usp=sharing
[5] gspread: http://gspread.readthedocs.io/en/latest/
[6] юникода: https://www.twilio.com/blog/2015/08/common-sms-problems-unicode-twilio.html
[7] Heroku: https://www.heroku.com/
[8] Voice: https://www.twilio.com/docs/api/rest/making-calls
[9] Chat: https://www.twilio.com/docs/api/chat
[10] Video: https://www.twilio.com/docs/api/video
[11] семафор: https://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D0%BC%D0%B0%D1%84%D0%BE%D1%80_(%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0)
[12] Twitter: https://twitter.com/SeekTom
[13] Источник: https://geektimes.ru/post/287616/
Нажмите здесь для печати.