Привет, Хаброжители! Эта книга Ноя Гифта предназначена для всех, кого интересуют ИИ, машинное обучение, облачные вычисления, а также любое сочетание данных тем. Как программисты, так и просто неравнодушные технари найдут тут для себя полезную информацию. Примеры кода даны на Python. Здесь рассматривается множество столь продвинутых тем, как использование облачных платформ (например, AWS, GCP и Azure), а также приемы машинного обучения и реализация ИИ. Джедаи, свободно ориентирующиеся в Python, облачных вычислениях и ML, также найдут для себя много полезных идей, которые смогут сразу применить в своей текущей работе.Предлагаем ознакомиться с отрывком из книги «Создание интеллектуального бота Slack в AWS»
Люди давно мечтают создать «искусственную жизнь». Чаще всего пока это возможно путем создания ботов. Боты становятся все более неотъемлемой частью наше�� повседневной жизни, особенно после появления Siri от компании Apple и Alexa от Amazon. В этой главе мы раскроем все тайны создания ботов.
Создание бота
Для создания бота мы воспользуемся библиотекой Slack для языка Python (https://github.com/slackapi/python-slackclient). Для начала работы со Slack необходимо сгенерировать идентификационный маркер. В целом имеет смысл при работе с подобными маркерами экспортировать переменную среды. Я часто делаю это в virtualenv, получая, таким образом, автоматически доступ к ней при выполнении в текущей среде. Для этого необходимо немного «взломать» утилиту virtualenv, отредактировав сценарий activate.
При экспорте переменной Slack в сценарии ~/.env/bin/activate он будет иметь нижеприведенный вид.
И просто для информации, если вы хотите идти в ногу с последними новинками, рекомендуется использовать появившуюся на рынке новую, официальную утилиту Python для управления средой — pipenv (https://github.com/pypa/pipenv):
_OLD_VIRTUAL_PATH="$PATH" PATH="$VIRTUAL_ENV/bin:$PATH" export PATH SLACK_API_TOKEN=<Your Token Here> export SLACK_API_TOKEN
Для проверки того, задано ли значение переменной среды, удобно использовать команду printenv операционных систем OS X и Linux. После этого для проверки отправки сообщения можно воспользоваться следующим коротким сценарием:
import os from slackclient import SlackClient slack_token = os.environ["SLACK_API_TOKEN"] sc = SlackClient(slack_token) sc.api_call( "chat.postMessage", channel="#general", text="Hello from my bot! :tada:" )
Стоит также отметить, что утилита pipenv — рекомендуемое решение, объединяющее в одном компоненте возможности утилит pip и virtualenv. Она стала новым стандартом, так что имеет смысл взглянуть на нее с точки зрения управления пакетами.
Преобразование библиотеки в утилиту командной строки
Как и в других примерах из этой книги, удачной идеей будет преобразовать наш код в утилиту командной строки, чтобы облегчить проверку новых идей. Стоит отметить, что многие разработчики-новички часто отдают предпочтение не утилитам командной строки, а другим решениям, например, просто работают в блокнотах Jupiter. Сыграю ненадолго роль адвоката дьявола и задам вопрос, который вполне может возникнуть у читателей: «А зачем нам утилиты командной строки в проекте, основанном на блокнотах Jupiter? Разве смысл блокнотов Jupiter состоит не в том, чтобы сделать ненужными командную оболочку и командную строку?» Добавление утилиты командной строки в проект хорошо тем, что позволяет быстро пробовать различные варианты входных данных. Блоки кода блокнотов Jupiter не принимают входные данные, в некотором смысле это сценарии с жестко «зашитыми» данными.
Множество утилит командной строки на платформах как GCP, так и AWS существует не случайно: они обеспечивают гибкость и возможности, недоступные для графических интерфейсов. Замечательный сборник эссе на эту тему фантаста Нила Стивенсона (Neal Stephenson) называется «В начале… была командная строка». В нем Стивенсон говорит: «GUI приводят к значительным дополнительным накладным расходам на каждый, даже самый маленький компонент программного обеспечения, которые полностью меняют среду программирования». Он заканчивает сборник словами: «… жизнь — штука очень тяжелая и сложная; никакой интерфейс это не изменит; и всякий, кто считает иначе, — простофиля...» Достаточно жестко, но мой опыт подсказывает, что и достаточно правдиво. Жизнь с командной строкой становится лучше. Попробуйте ее — и вы не захотите возвращаться обратно к GUI.
Для этого мы воспользуемся пакетом click, как показано ниже. Отправка сообщений с помощью нового интерфейса оказывается очень простым делом.
./clibot.py send --message "from cli" sending message from cli to #general
Рисунок 7.1 демонстрирует значения по умолчанию, а также настраиваемое сообщение от утилиты cli.
#!/usr/bin/env python import os import click from slackclient import SlackClient SLACK_TOKEN = os.environ["SLACK_API_TOKEN"] def send_message(channel="#general", message="Hello from my bot!"): """Отправить сообщение на канал""" slack_client = SlackClient(SLACK_TOKEN) res = slack_client.api_call( "chat.postMessage", channel=channel, text=message ) return res @click.group() @click.version_option("0.1") def cli(): """ Утилита командной строки для слабаков """ @cli.command("send") @click.option("--message", default="Hello from my bot!", help="text of message") @click.option("--channel", default="#general", help="general channel") def send(channel, message): click.echo(f"sending message {message} to {channel}") send_message(channel, message=message) if __name__ == '__main__': cli()

Выводим бот на новый уровень с помощью сервиса AWS Step Functions
После создания каналов связи для отправки сообщений в Slack можно усовершенствовать наш код, а именно: запускать его по расписанию и использовать для каких-либо полезных действий. Сервис пошаговых функций AWS (AWS Step Functions) замечательно подходит для этой цели. В следующем разделе наш бот Slack научится производить скрапинг спортивных страниц Yahoo! игроков НБА, извлекать их места рождения, а затем отправлять эти данные в Slack.
Рисунок 7.2 демонстрирует готовую пошаговую функцию в действии. Первый шаг состоит в извлечении URL профилей игроков НБА, а второй — в использовании библиотеки Beautiful Soup для поиска места рождения каждого из игроков. По завершении выполнения пошаговой функции результаты будут отправлены обратно в Slack.

Для координации отдельных частей работы внутри пошаговой функции можно применить AWS Lambda и Chalice. Lambda (https://aws.amazon.com/lambda/) позволяет пользователю выполнять функции в AWS, а фреймворк Chalice (http://chalice.readthedocs.io/en/latest/) дает возможность создания бессерверных приложений на языке Python. Вот некоторые предварительные требования:
- у пользователя должна быть учетная запись AWS;
- пользователю необходимы учетные данные для использования API;
- у роли Lambda (создаваемой Chalice) должна быть политика с привилегиями, необходимыми для вызова соответствующих сервисов AWS, например S3.
Настройка учетных данных IAM
Подробные инструкции по настройке учетных данных AWS можно найти по адресу boto3.readthedocs.io/en/latest/guide/configuration.html. Информацию об экспорте переменных AWS в операционных системах Windows и Linux можно найти здесь. Существует множество способов настройки учетных данных, но пользователи virtualenv могут поместить учетные данные AWS в локальную виртуальную среду, в сценарий /bin/activate:
#Добавляем ключи AWS AWS_DEFAULT_REGION=us-east-1 AWS_ACCESS_KEY_ID=xxxxxxxx AWS_SESSION_TOKEN=xxxxxxxx
#Экспортируем ключи export AWS_DEFAULT_REGION export AWS_ACCESS_KEY_ID export AWS_DEFAULT_REGION
Работа с Chalice. У Chalice есть утилита командной строки с множеством доступных команд:
Usage: chalice [OPTIONS] COMMAND [ARGS]... Options: --version Show the version and exit. --project-dir TEXT The project directory. Defaults to CWD --debug / --no-debug Print debug logs to stderr. --help Show this message and exit. Commands: delete deploy gen-policy generate-pipeline Generate a cloudformation template for a... generate-sdk local logs new-project package url
Код внутри шаблона app.py можно заменить на функции сервиса Lambda. В AWS Chalice удобно то, что он дает возможность создавать, помимо веб-сервисов, «автономные» функции Lambda. Благодаря этой функциональности можно создать несколько функций Lambda, связать их с пошаговой функцией и свести воедино, как кубики «Лего».
Например, можно легко создать запускаемую по расписанию функцию Lambda, которая будет выполнять какие-либо действия:
@app.schedule(Rate(1, unit=Rate.MINUTES)) def every_minute(event): """Событие, запланированное для ежеминутного выполнения""" #Отправка сообщения боту Slack
Для налаживания взаимодействия с ботом для веб-скрапинга необходимо создать несколько функций. В начале файла находятся импорты и объявлено некоторое количество переменных:
import logging import csv from io import StringIO import boto3 from bs4 import BeautifulSoup import requests from chalice import (Chalice, Rate) APP_NAME = 'scrape-yahoo' app = Chalice(app_name=APP_NAME) app.log.setLevel(logging.DEBUG)
Боту может понадобиться хранить часть данных в S3. Следующая функция использует Boto для сохранения результатов в CSV-файле:
def create_s3_file(data, name="birthplaces.csv"): csv_buffer = StringIO() app.log.info(f"Creating file with {data} for name") writer = csv.writer(csv_buffer) for key, value in data.items(): writer.writerow([key,value]) s3 = boto3.resource('s3') res = s3.Bucket('aiwebscraping').\ put_object(Key=name, Body=csv_buffer.getvalue()) return res
Функция fetch_page использует библиотеку Beautiful Soup для синтаксического разбора HTML-страницы, расположенной в соответствии с URL статистики НБА, и возвращает объект soup:
def fetch_page(url="https://sports.yahoo.com/nba/stats/"): """Извлекает URL Yahoo""" #Скачивает страницу и преобразует ее в объект # библиотеки Beautiful Soup app.log.info(f"Fetching urls from {url}") res = requests.get(url) soup = BeautifulSoup(res.content, 'html.parser') return soup
Функции get_player_links и fetch_player_urls получают ссылки на URL профилей игроков:
def get_player_links(soup): """Получает ссылки из URL игроков Находит все URL на странице в тегах 'a' и фильтрует их в поисках строки 'nba/players' """ nba_player_urls = [] for link in soup.find_all('a'): link_url = link.get('href') #Отбрасываем неподходящие if link_url: if "nba/players" in link_url: print(link_url) nba_player_urls.append(link_url) return nba_player_urls def fetch_player_urls(): """Возвращает URL игроков""" soup = fetch_page() urls = get_player_links(soup) return urls
Далее в функции find_birthplaces мы извлекаем с расположенных по этим URL страниц места рождения игроков:
def find_birthplaces(urls): """Получаем места рождения со страниц профилей игроков NBA на Yahoo""" birthplaces = {} for url in urls: profile = requests.get(url) profile_url = BeautifulSoup(profile.content, 'html.parser') lines = profile_url.text res2 = lines.split(",") key_line = [] for line in res2: if "Birth" in line: #print(line) key_line.append(line) try: birth_place = key_line[0].split(":")[-1].strip() app.log.info(f"birth_place: {birth_place}") except IndexError: app.log.info(f"skipping {url}") continue birthplaces[url] = birth_place app.log.info(birth_place) return birthplaces
Теперь мы перейдем к функциям Chalice. Обратите внимание: для фреймворка Chalice необходимо, чтобы был создан путь по умолчанию:
#Их можно вызвать с помощью HTTP-запросов @app.route('/') def index(): """Корневой URL""" app.log.info(f"/ Route: for {APP_NAME}") return {'app_name': APP_NAME}
Следующая функция Lambda представляет собой маршрут, связывающий HTTP URL с написанной ранее функцией:
@app.route('/player_urls') def player_urls(): """Извлекает URL игроков""" app.log.info(f"/player_urls Route: for {APP_NAME}") urls = fetch_player_urls() return {"nba_player_urls": urls}
Следующие функции Lambda — автономные, их можно вызвать внутри пошаговой функции:
#Это автономная функция Lambda @app.lambda_function() def return_player_urls(event, context): """Автономная функция Lambda, возвращающая URL игроков""" app.log.info(f"standalone lambda 'return_players_urls'\ {APP_NAME} with {event} and {context}") urls = fetch_player_urls() return {"urls": urls} #Это автономная функция Lambda @app.lambda_function() def birthplace_from_urls(event, context): """Находит места рождения игроков""" app.log.info(f"standalone lambda 'birthplace_from_urls'\ {APP_NAME} with {event} and {context}") payload = event["urls"] birthplaces = find_birthplaces(payload) return birthplaces #Это автономная функция Lambda @app.lambda_function() def create_s3_file_from_json(event, context): """Создает файл S3 на основе данных в формате JSON""" app.log.info(f"Creating s3 file with event data {event}\ and context {context}") print(type(event)) res = create_s3_file(data=event) app.log.info(f"response of putting file: {res}") return True
Если запустить получившееся приложение Chalice локально, будут выведены следующие результаты:
→ scrape-yahoo git:(master) chalice local Serving on 127.0.0.1:8000 scrape-yahoo - INFO - / Route: for scrape-yahoo 127.0.0.1 - - [12/Dec/2017 03:25:42] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [12/Dec/2017 03:25:42] "GET /favicon.ico" scrape-yahoo - INFO - / Route: for scrape-yahoo 127.0.0.1 - - [12/Dec/2017 03:25:45] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [12/Dec/2017 03:25:45] "GET /favicon.ico" scrape-yahoo - INFO - /player_urls Route: for scrape-yahoo scrape-yahoo - INFO - https://sports.yahoo.com/nba/stats/ https://sports.yahoo.com/nba/players/4563/ https://sports.yahoo.com/nba/players/5185/ https://sports.yahoo.com/nba/players/3704/ https://sports.yahoo.com/nba/players/5012/ https://sports.yahoo.com/nba/players/4612/ https://sports.yahoo.com/nba/players/5015/ https://sports.yahoo.com/nba/players/4497/ https://sports.yahoo.com/nba/players/4720/ https://sports.yahoo.com/nba/players/3818/ https://sports.yahoo.com/nba/players/5432/ https://sports.yahoo.com/nba/players/5471/ https://sports.yahoo.com/nba/players/4244/ https://sports.yahoo.com/nba/players/5464/ https://sports.yahoo.com/nba/players/5294/ https://sports.yahoo.com/nba/players/5336/ https://sports.yahoo.com/nba/players/4390/ https://sports.yahoo.com/nba/players/4563/ https://sports.yahoo.com/nba/players/3704/ https://sports.yahoo.com/nba/players/5600/ https://sports.yahoo.com/nba/players/4624/ 127.0.0.1 - - [12/Dec/2017 03:25:53] "GET /player_urls" 127.0.0.1 - - [12/Dec/2017 03:25:53] "GET /favicon.ico"
Для развертывания приложения выполните команду chalice deploy:
→ scrape-yahoo git:(master) chalice deploy Creating role: scrape-yahoo-dev Creating deployment package. Creating lambda function: scrape-yahoo-dev Initiating first time deployment. Deploying to API Gateway stage: api https://bt98uzs1cc.execute-api.us-east-1.amazonaws.com/api/
Благодаря интерфейсу командной строки для HTTP (https://github.com/jakubroztocil/httpie) мы вызываем маршрут HTTP из AWS и извлекаем доступные в /api/player_urls ссылки:
→ scrape-yahoo git:(master) http \ https://<a lambda route>.amazonaws.com/api/player_urls HTTP/1.1 200 OK Connection: keep-alive Content-Length: 941 Content-Type: application/json Date: Tue, 12 Dec 2017 11:48:41 GMT Via: 1.1 ba90f9bd20de9ac04075a8309c165ab1.cloudfront.net (CloudFront) X-Amz-Cf-Id: ViZswjo4UeHYwrc9e-5vMVTDhV_Ic0dhVIG0BrDdtYqd5KWcAuZKKQ== X-Amzn-Trace-Id: sampled=0;root=1-5a2fc217-07cc12d50a4d38a59a688f5c X-Cache: Miss from cloudfront x-amzn-RequestId: 64f24fcd-df32-11e7-a81a-2b511652b4f6 { "nba_player_urls": [ "https://sports.yahoo.com/nba/players/4563/", "https://sports.yahoo.com/nba/players/5185/", "https://sports.yahoo.com/nba/players/3704/", "https://sports.yahoo.com/nba/players/5012/", "https://sports.yahoo.com/nba/players/4612/", "https://sports.yahoo.com/nba/players/5015/", "https://sports.yahoo.com/nba/players/4497/", "https://sports.yahoo.com/nba/players/4720/", "https://sports.yahoo.com/nba/players/3818/", "https://sports.yahoo.com/nba/players/5432/", "https://sports.yahoo.com/nba/players/5471/", "https://sports.yahoo.com/nba/players/4244/", "https://sports.yahoo.com/nba/players/5464/", "https://sports.yahoo.com/nba/players/5294/", "https://sports.yahoo.com/nba/players/5336/", "https://sports.yahoo.com/nba/players/4390/", "https://sports.yahoo.com/nba/players/4563/", "https://sports.yahoo.com/nba/players/3704/", "https://sports.yahoo.com/nba/players/5600/", "https://sports.yahoo.com/nba/players/4624/" ] }
Еще один удобный способ работы с функциями Lambda — непосредственный их вызов с помощью пакета click и библиотеки Boto языка Python.
Мы можем создать новую утилиту командной строки с названием wscli.py (сокращение от web-scraping command-line interface — «интерфейс командной строки для веб-скрапинга»). В первой части кода мы настраиваем журналирование и импортируем библиотеки:
#!/usr/bin/env python import logging import json import boto3 import click from pythonjsonlogger import jsonlogger #Инициализация журналирования log = logging.getLogger(__name__) log.setLevel(logging.INFO) LOGHANDLER = logging.StreamHandler() FORMMATTER = jsonlogger.JsonFormatter() LOGHANDLER.setFormatter(FORMMATTER) log.addHandler(LOGHANDLER)
Следующие три функции предназначены для подключения к функции Lambda через invoke_lambda:
###Вызовы API Boto Lambda def lambda_connection(region_name="us-east-1"): """Создаем подключение к Lambda""" lambda_conn = boto3.client("lambda", region_name=region_name) extra_msg = {"region_name": region_name, "aws_service": "lambda"} log.info("instantiate lambda client", extra=extra_msg) return lambda_conn def parse_lambda_result(response): """Получаем результаты из ответа библиотеки Boto в формате JSON""" body = response['Payload'] json_result = body.read() lambda_return_value = json.loads(json_result) return lambda_return_value def invoke_lambda(func_name, lambda_conn, payload=None, invocation_type="RequestResponse"): """Вызываем функцию Lambda""" extra_msg = {"function_name": func_name, "aws_service": "lambda", "payload":payload} log.info("Calling lambda function", extra=extra_msg) if not payload: payload = json.dumps({"payload":"None"}) response = lambda_conn.invoke(FunctionName=func_name, InvocationType=invocation_type, Payload=payload ) log.info(response, extra=extra_msg) lambda_return_value = parse_lambda_result(response) return lambda_return_value
Обертываем функцию invoke_lambda с помощью пакета Python для создания утилит командной строки Click. Обратите внимание, что мы задали значение по умолчанию для опции --func, при котором используется развернутая нами ранее функция Lambda:
@click.group() @click.version_option("1.0") def cli(): """Вспомогательная утилита командной строки для веб-скрапинга""" @cli.command("lambda") @click.option("--func", default="scrape-yahoo-dev-return_player_urls", help="name of execution") @click.option("--payload", default='{"cli":"invoke"}', help="name of payload") def call_lambda(func, payload): """Вызывает функцию Lambda ./wscli.py lambda """ click.echo(click.style("Lambda Function invoked from cli:", bg='blue', fg='white')) conn = lambda_connection() lambda_return_value = invoke_lambda(func_name=func, lambda_conn=conn, payload=payload) formatted_json = json.dumps(lambda_return_value, sort_keys=True, indent=4) click.echo(click.style( "Lambda Return Value Below:", bg='blue', fg='white')) click.echo(click.style(formatted_json,fg="red")) if __name__ == "__main__": cli()
Выводимые этой утилитой результаты аналогичны вызову HTTP-интерфейса:
→ X ./wscli.py lambda \ --func=scrape-yahoo-dev-birthplace_from_urls\ --payload '{"url":["https://sports.yahoo.com/nba/players/4624/",\ "https://sports.yahoo.com/nba/players/5185/"]}' Lambda Function invoked from cli: {"message": "instantiate lambda client", "region_name": "us-east-1", "aws_service": "lambda"} {"message": "Calling lambda function", "function_name": "scrape-yahoo-dev-birthplace_from_urls", "aws_service": "lambda", "payload": "{\"url\":[\"https://sports.yahoo.com/nba/players/4624/\", \"https://sports.yahoo.com/nba/players/5185/\"]}"} {"message": null, "ResponseMetadata": {"RequestId": "a6049115-df59-11e7-935d-bb1de9c0649d", "HTTPStatusCode": 200, "HTTPHeaders": {"date": "Tue, 12 Dec 2017 16:29:43 GMT", "content-type": "application/json", "content-length": "118", "connection": "keep-alive", "x-amzn-requestid": "a6049115-df59-11e7-935d-bb1de9c0649d", "x-amzn-remapped-content-length": "0", "x-amz-executed-version": "$LATEST", "x-amzn-trace-id": "root=1-5a3003f2-2583679b2456022568ed0682;sampled=0"}, "RetryAttempts": 0}, "StatusCode": 200, "ExecutedVersion": "$LATEST", "Payload": "<botocore.response.StreamingBody object at 0x10ee37dd8>", "function_name": "scrape-yahoo-dev-birthplace_from_urls", "aws_service": "lambda", "payload": "{\"url\":[\"https://sports.yahoo.com/nba/players/4624/\", \"https://sports.yahoo.com/nba/players/5185/\"]}"} Lambda Return Value Below: { "https://sports.yahoo.com/nba/players/4624/": "Indianapolis", "https://sports.yahoo.com/nba/players/5185/": "Athens" }
Завершение создания пошаговой функции
Последний этап создания пошаговой функции, как описывается в документации от AWS (https://docs.aws.amazon.com/step-functions/latest/dg/tutorial-creating-activity-state-machine.html), — создание с помощью веб-интерфейса структуры конечного автомата в формате нотации объектов JavaScript (JavaScript Object Notation, JSON). Следующий код демонстрирует этот конвейер, начиная от исходных функций Lambda для скрапинга Yahoo!, сохранения данных в файле S3 и, наконец, отправки содержимого в Slack:
{ "Comment": "Fetch Player Urls", "StartAt": "FetchUrls", "States": { "FetchUrls": { "Type": "Task", "Resource": \ "arn:aws:lambda:us-east-1:561744971673:\ function:scrape-yahoo-dev-return_player_urls", "Next": "FetchBirthplaces" }, "FetchBirthplaces": { "Type": "Task", "Resource": \ "arn:aws:lambda:us-east-1:561744971673:\ function:scrape-yahoo-dev-birthplace_from_urls", "Next": "WriteToS3" }, "WriteToS3": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:\ 561744971673:function:scrape-yahoo-dev-create_s3_file_from_json", "Next": "SendToSlack" }, "SendToSlack": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:561744971673:\ function:send_message", "Next": "Finish" }, "Finish": { "Type": "Pass", "Result": "Finished", "End": true } } }
На рис. 7.2 было показано выполнение первой части этого конвейера. Чрезвычайно полезна возможность видеть промежуточные результаты работы конечного автомата. Кроме того, возможность мониторинга в режиме реального времени каждой части конечного автомата очень удобна для отладки.
Рисунок 7.3 демонстрирует полный конвейер с добавлением шагов записи в S3-файл и отправки содержимого в Slack. Осталось только решить, как запускать эту утилиту скрапинга — через определенный интервал времени или в ответ на какое-либо событие.

Резюме
В этой главе вы познакомились с множеством потрясающих концепций построения приложений ИИ. В ней были созданы бот Slack и утилита веб-скрапинга, соединенные затем с помощью бессерверных сервисов от AWS. В такой начальный каркас можно добавить еще много всего — например, Lambda-функцию обработки написанных на естественных языках текстов для чтения веб-страниц и получения их краткого содержимого или алгоритм кластеризации без учителя, который бы кластеризовал новых игроков НБА по произвольным атрибутам.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 20% по купону — Гифт
P.S.: 7% от стоимости книги пойдет на перевод новых компьютерных книг, список сданных в типографию книг здесь.
