Парсим мемы в питоне: как обойти серверную блокировку
Новогодние праздники — прекрасный повод попрокрастинировать в уютной домашней обстановке и вспомнить дорогие сердцу мемы из 2k17, уходящие навсегда, как совесть Electronic Arts.
Однако даже обильно сдобренная салатами совесть иногда просыпалась и требовала хоть немного взять себя в руки и заняться полезной деятельностью. Поэтому мы совместили приятное с полезным и на примере любимых мемов посмотрели, как можно спарсить себе небольшую базу
данных, попутно обходя всевозможные блокировки, ловушки и ограничения, расставленные сервером на нашем пути. Всех заинтересованных любезно приглашаем под кат.
Машинное обучение, эконометрика, статистика и многие другие науки о данных занимаются поиском закономерностей. Каждый день доблестные аналитики-инквизиторы пытают природу разными методами и вытаскивают из неё сведенья о том, как именно устроен великий процесс порождения данных, создавший нашу вселенную. Испанские инквизиторы в своей повседневной деятельности использовали непосредственно физическое тело своей жертвы. Природа же вездесуща и не имеет однозначного физического облика. Из-за этого профессия современного инквизитора имеет странную специфику — пытки природы происходят через анализ данных, которые надо откуда-то брать. Обычно данные инквизиторам приносят мирные собиратели. Эта статейка призвана немного приоткрыть завесу тайны насчет того, откуда данные берутся и как их можно немножечко пособирать.
Нашим девизом стала знаменитая фраза капитана Джека Воробья: «Бери всё и не отдавай ничего». Иногда для сбора мемов придется использовать довольно бандитские методы. Тем не менее, мы будем оставаться мирными собирателями данных, и ни в коей мере не будем становиться бандитами. Брать мемы мы будем из главного мемохранилища.
1. Вламываемся в мемохранилище
1.1. Что мы хотим получить
Итак, мы хотим распарсить knowyourmeme.com и получить кучу разных переменных:
- Name — название мема,
- Origin_year — год его создания,
- Views — число просмотров,
- About — текстовое описание мема,
- и многие другие
Более того, мы хотим сделать это без вот этого всего:
После скачивания и чистки данных от мусора можно будет заняться строительством моделей. Например, попытаться предсказать популярность мема по его параметрам. Но это все позже, а сейчас познакомимся с парой определений:
- Парсер — это скрипт, который грабит информацию с сайта
- Краулер — это часть парсера, которая бродит по ссылкам
- Краулинг — это переход по страницам и ссылкам
- Скрапинг — это сбор данных со страниц
- Парсинг — это сразу и краулинг и скрапинг!
1.2. Что такое HTML
HTML (HyperText Markup Language) — это такой же язык разметки как Markdown или LaTeX. Он является стандартным для написания различных сайтов. Команды в таком языке называются тегами. Если открыть абсолютно любой сайт, нажать на правую кнопку мышки, а после нажать View page source
, то перед вами предстанет HTML скелет этого сайта.
Можно увидеть, что HTML-страница это ни что иное как набор вложенных тегов. Можно заметить, например, следующие теги:
— заголовок страницы
— заголовки разных уровней…
— абзац (paragraph)
- — выделения фрагмента документа с целью изменения вида содержимого
— прорисовка таблицы
— разделитель для строк в таблице — разделитель для столбцов в таблице — устанавливает жирное начертание шрифта
Обычно команда
— это отдельный абзац.<...>
открывает тег, азакрывает его. Все, что находится между этими двумя командами, подчиняется правилу, которое диктует тег. Например, все, что находится между
и
Теги образуют своеобразное дерево с корнем в теге
и разбивают страницу на разные логические кусочки. У каждого тега могут быть свои потомки (дети) — те теги, которые вложены в него, и свои родители.
Например, HTML-древо страницы может выглядеть вот так:
Заголовок
Первый кусок текста со своими свойствамиВторой кусок текста Третий, жирный кусокЧетвёртый кусок текстаМожно работать с этим html как с текстом, а можно как с деревом. Обход этого дерева и есть парсинг веб-страницы. Мы всего лишь будем находить нужные нам узлы среди всего этого разнообразия и забирать из них информацию!
Вручную обходить эти деревья не очень приятно, поэтому есть специальные языки для обхода деревьев.
- CSS-селектор (это когда мы ищем элемент страницы по паре ключ, значение)
- XPath (это когда мы прописываем путь по дереву вот так: /html/body/div[1]/div[3]/div/div[2]/div)
- Всякие разные библиотеки для всяких разных языков, например, BeautifulSoup для питона. Именно эту библиотеку мы и будем использовать.
1.3. Наш первый запрос
Доступ к веб-станицам позволяет получать модуль
requests
. Подгрузим его. За компанию подгрузим ещё парочку дельных пакетов.import requests # Библиотека для отправки запросов import numpy as np # Библиотека для матриц, векторов и линала import pandas as pd # Библиотека для табличек import time # Библиотека для тайм-менеджмента
Для наших благородных исследовательских целей нужно собрать данные по каждому мему с соответствующей ему страницы. Но для начала нужно получить адреса этих страниц. Поэтому открываем основную страницу со всеми выложенными мемами. Выглядит она следующим образом:
Отсюда мы и будем тащить ссылки на каждый из перечисленных мемов. Сохраним в переменную
page_link
адрес основной страницы и откроем её при помощи библиотекиrequests
.page_link = 'http://knowyourmeme.com/memes/all/page/1' response = requests.get(page_link) response
Out:
А вот и первая проблема! Обращаемся к главному источнику знаний и выясняем, что 403-я ошибка выдается сервером, если он доступен и способен обрабатывать запросы, но по некоторым личным причинам отказывается это делать.
Попробуем выяснить, почему. Для этого проверим, как выглядел финальный запрос, отправленный нами на сервер, а конкретнее — как выглядел наш User-Agent в глазах сервера.
for key, value in response.request.headers.items(): print(key+": "+value)
Out: User-Agent: python-requests/2.14.2 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive
Похоже, мы недвусмысленно дали понять серверу, что мы сидим на питоне и используем библиотеку requests под версией 2.14.2. Скорее всего, это вызвало у сервера некоторые подозрения относительно наших благих намерений и он решил нас безжалостно отвергнуть. Для сравнения, можно посмотреть, как выглядят request-headers у здорового человека:
Очевидно, что нашему скромному запросу не тягаться с таким обилием мета-информации, которое передается при запросе из обычного браузера. К счастью, никто нам не мешает притвориться человечными и пустить пыль в глаза сервера при помощи генерации фейкового юзер-агента. Библиотек, которые справляются с такой задачей, существует очень и очень много, лично мне больше всего нравится
fake-useragent
. При вызове метода из различных кусочков будет генерироваться рандомное сочетание операционной системы, спецификаций и версии браузера, которые можно передавать в запрос:# подгрузим один из методов этой библиотеки from fake_useragent import UserAgent UserAgent().chrome
Out: 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36'
Попробуем прогнать наш запрос еще раз, уже со сгенерированным агентом
response = requests.get(page_link, headers={'User-Agent': UserAgent().chrome}) response
Out:
Замечательно, наша небольшая маскировка сработала и обманутый сервер покорно выдал благословенный 200 ответ — соединение установлено и данные получены, всё чудесно! Посмотрим, что же все-таки мы получили.
html = response.content html[:1000]
Out: b'\n\n\n\n\n
-->