Парсинг 50 миллионов твитов за $5 за 5 часов
Это будет подробное руководство, в котором объясняется, как не только парсить целевые данные из Twitter, но и масштабировать проект быстро и недорого, чтобы мы могли получить миллионы твитов.
Мотивация и обзор:
Классификация положительного и отрицательного настроения твитов стала популярной в последние годы с использованием таких техник, как "мешок слов", но я хочу проверить, насколько мощными и адаптивными стали современные модели языка, попробовав понять общественное настроение по отношению к биткоину. Хотя недавние инновации в техниках передачи обучения снизили необходимость в огромных объемах данных, мне все равно нужно собрать большой корпус твитов, чтобы построить эффективную модель. Для этого я собрал 43,8 миллиона твитов, содержащих ключевые слова, такие как биткоин или #btc, всего за 5 часов, используя различные технологии, включая Docker Swarm, MongoDB, BeautifulSoup и DigitalOcean.
Написание скрипта для парсинга:
Поиск веб-страниц:
Для получения этих твитов мы собираемся парсить страницы расширенного поиска Twitter. Расширенный поиск Twitter позволяет пользователям вводить сложные поисковые запросы и указывать диапазон дат. Чтобы увидеть пример страницы, посетите здесь.
Используя библиотеку requests и заполнив некоторые параметры, мы легко можем загрузить HTML с Twitter страницы в наш объект ответа.
Извлечение данных с помощью BeautifulSoup:
Получив этот ответ, теперь нам нужно разобрать HTML для получения нужных данных - а именно текста и временной метки.
Каждый твит представлен с помощью тегов <li>
с атрибутом data-item-type равным 'tweet'. Используя BeautifulSoup и метод find_all(), мы можем легко получить все твиты. Изучив HTML для каждого твита, мы находим классы, содержащие нужные нам данные, такие как tweet-text и _timestamp. Хотя Twitter отображает, сколько времени назад был опубликован твит (5 часов назад), мы можем увидеть точное время в миллисекундах UTC, когда был опубликован твит, что будет очень ценно при сопоставлении твитов с рынком. Обратите внимание, что, изучив немного подробнее, мы можем извлечь некоторые дополнительные метаданные, такие как количество ретвитов, избранных и комментариев.
Работа с пагинацией:
Наконец, когда мы запускаем наш первоначальный поиск, мы загружаем только 20 твитов; только когда мы прокручиваем вниз, мы видим больше. Чтобы определить, как Twitter работает с этой пагинацией, мы используем наш браузер для анализа сетевой активности. Мы видим, что когда мы прокручиваем вниз для новой веб-страницы, мы получаем следующий запрос:
**URL запроса:**https://twitter.com/i/search/timeline?f=tweets&vertical=default&q=bitcoin%2C%20crypto%2C%20btc%20since%3A2019-07-05%20until%3A2019-07-06&src=typd&include_available_features=1&include_entities=1&lang=en&**max_position**=thGAVUV0VFVBaAwKeFkLn56x8WjMC8sZvtgOwfEjUAFQAlAFUAFQAA&reset_error_state=false
Из этого мы видим, что в запросе есть поле max_position, которое мы можем найти с помощью следующей строки:
next_pointer = soup.find("div", {"class": "stream-container"})["data-min-position"]
Наш первоначальный запрос к Twitter возвращает HTML, но каждый запрос пагинации возвращает JSON, поэтому мы должны разбирать каждый тип ответа по-разному.
Чтобы увидеть полный скрипт, проверьте здесь.
Развертывание и масштабирование:
Интеграция базы данных:
Поскольку это большой объем данных, который нам нужно сохранить в базе данных, мы используем MongoDB, так как она легко взаимодействует с Python и имеет отличную библиотеку.
Мы создаем 2 коллекции. Первая называется tweets и просто хранит все собранные нами твиты следующим образом:
Мы вставляем твиты в нашу функцию записи после завершения парсинга страницы.
Вторая коллекция называется queriesTodo и содержит даты для выполнения запроса.
Причина, по которой нам нужна эта коллекция, заключается в том, что при работе с 100 параллельными узлами мы хотим, чтобы они все работали с разными датами.
Чтобы загрузить запросы, мы можем написать быстрый скрипт, вставляющий каждую дату с 2014 года.
Инициализация узла-менеджера:
Я использовал сервер DigitalOcean с предустановленным Docker, имеющий 4 ГБ ОЗУ и 2 виртуальных ЦП, стоимостью $15 в месяц. Сначала мы хотим инициализировать сервер в качестве нашего узла-менеджера с помощью следующей команды Docker:
docker swarm init --advertise-addr <ip адрес>
Затем мы создаем оверлейную сеть, которая является VPN, позволяющей рабочим узлам общаться непосредственно с узлом-менеджером. Также обратите внимание, что могут потребоваться некоторые настройки брандмауэра, которые объясняются здесь.
docker network create -d overlay --attachable backend
Теперь, когда роевик инициализирован, мы запускаем наш первый сервис, который является экземпляром MongoDB. Создайте следующий файл docker-compose и запустите
docker stack deploy mongodb -c docker-compose.yml
Поскольку мы открываем порт 27017, мы хотим убедиться, что у нас включена аутентификация. Кроме того, хорошей идеей является монтирование тома с данными с контейнера на хост, поскольку данные более устойчивы на хосте.
Запуск 100 рабочих узлов:
Сначала нам нужно создать образ Docker для написанного выше скрипта парсинга. Для этого мы создаем простой Dockerfile, размещенный в каталоге с main.py.
В том же каталоге, где находятся main.py и Dockerfile, необходимо включить файл requriements.txt с зависимостями, импортированными в наш main.py. После перехода в правильную папку мы создаем образ и загружаем его в Docker Hub.
docker build -t <username>/twitter_scrape .
docker push <username>/twitter_scrape
Теперь мы можем вернуться к узлу-менеджеру и развернуть наш сервис парсинга.
docker service create --name news --network backend --constraint 'node.role==worker' --mode global <username>/twitter_scrape
Установка mode в значение global означает, что один контейнер будет запущен на каждом рабочем узле, подключенном к кластеру.
Наконец, нам нужно создать наш скрипт автоматизации DigitalOcean, который создает наши droplet'ы и уничтожает их, когда у нас заканчиваются запросы.
Также нам нужно иметь скрипт cloud-config, который занимается включением соответствующих портов в брандмауэре, а затем присоединением к узлу-менеджеру.
После запуска скрипта автоматизации мы должны увидеть создание droplet'ов и начало вставки твитов в базу данных.
Заключительные мысли:
Как мы справились? Мы смогли получить 43,8 млн твитов за 5,8 часов при работе 100 узлов. По стоимости $0,007 в час за узел это составляет около $4,06.
В процессе работы возникли некоторые проблемы, такие как некоторые ошибки на страницах. Я перезапустил запросы с тех страниц, где возникли ошибки, чтобы убедиться, что мы не пропустили ни одного твита. К счастью, у твитов есть уникальный идентификатор, поэтому я создал индекс для этого поля, чтобы избежать дубликатов.
У Twitter есть API, но чтобы получить полную историю, вам понадобится премиум-план, который начинается от $100 в месяц и дает вам только 50 000 твитов. Самый дорогой премиум-план стоит $1900 в месяц и дает вам 1,25 млн твитов.
Я с нетерпением жду следующего этапа создания языковой модели и тестирования нашей работы на рынках.
📝 Прочитай эту историю позже в Журнале.
👩💻 Каждое воскресенье утром получай в своем почтовом ящике самые интересные истории в области технологий за неделю. Подпишись на рассылку Noteworthy in Tech.