Введение
В прошлых материалах мы рассказывали, как массово скачивать тексты из Твиттера и из «Вконтакте» Уметь выкачивать данные социальных сетей бывает полезно, но далеко не всегда там можно найти нужную для конкретных исследовательских целей информацию. Что делать в таком случае?
Можно пойти по протоптанной дорожке и найти подготовленные кем-то датасеты. Однако это не всегда работает: далеко не факт ,что кто-то озаботился собрать данные, нужные именно вам. . Зато если в поле зрения есть сайт с нужными данными, то можно скачать их собственноручно! В этом туториале мы разберем, как это сделать с помощью Python.
Парочка замечаний касательно туториала.
Момент 1. Как и в прошлой части про ВК, мы продолжаем работать в Jupyter Notebook, однако каждый волен сам выбирать, где ему удобно кодить: PyCharm, Sublime Text и т.д.
Момент 2. Код написан с намерением помочь понять, как все работает, поэтому может быть далек от идеала, однако свою работу он выполняет прекрасно. По ходу туториала к коду дается много пояснений, поэтому те, кто уже умеет программировать и читать подробные описания не хочет, могут при желании сразу доскроллить до конца поста, где есть ссылка на код на Github.
Структура HTML-страницы
Перед тем как переходить непосредственно к парсингу (автоматическому считыванию данных), надо иметь представление о том, что скрывается за привычными нам красивыми сайтами с картиночками, текстом, ссылками и т.д. Большинство веб-страниц пишутся на языке разметки HTML и структурно представляют собой дерево из HTML-элементов, которые интерпретируются браузерами и превращаются в знакомые всем странички.
HTML-элементы кодируют заголовки, текстовые поля, картиночки, кнопочки и т. д. Разберем на простом примере, из чего же такие элементы состоят:
Каждый элемент неизбежно включает в себя тег (закрывающий и открывающий), и именно тег отвечает за тип информации, который передан в элементе. В примере на картинке представлен тег <a>, который отвечает за создание ссылок*.
Открывающий тег может содержать дополнительную информацию: атрибуты и их значения. Атрибут показывает тип того, что передано в элементе, то есть настраивает его. Значение же атрибута — это то, что непосредственно в него передается. Вернемся к картинке: тег <a> имеет атрибут href, который говорит нам о том, что элемент содержит URL-ссылку на внешний ресурс, а сама ссылка (в нашем случае — https://sysblok.ru) прописывается в значении атрибута внутри кавычек**.
Последняя составляющая элемента — содержание, которое отвечает за то, как элемент будет выглядеть на странице. То есть, в нашем примере картинки надпись Системный Блокъ будет интерактивной, при нажатии на нее будет открываться ссылка, которая передана в значении атрибута href.
Зачем нам это знать? Затем, что когда мы вытаскиваем с сайта какую-то информацию, мы делаем это как раз через обращение к определенному элементу внутри страницы. Теперь, когда мы немного разобрались в структуре сайтов, можно переходить к практике.
* Назначения различных тегов можно посмотреть, например, в Справочнике HTML.
** Один и тот же тег может содержать разные атрибуты, то есть, передавать разные типы информации. Например, тег <a> может также иметь атрибут name, который определяет якорь, то есть заранее установленную закладку внутри веб-страницы, на которую ведет ссылка.
Вытаскиваем данные простого сайта
Сначала подключаем нужные модули из библиотек. Мы будем пользоваться библиотекой requests, которая позволяет отправлять http-запросы на сервер и получать ответы в различных форматах. Также нам понадобится библиотека pyquery, использующаяся как раз для того, чтобы доставать элементы из html-документов. Последняя библиотека, которую мы используем, — tqdm. Она создана для удобства отслеживания количества записываемых или скачиваемых файликов и скорости передачи данных. Можно обойтись и без tqdm.
Если вы не знаете, как устанавливать библиотеки в Python, можете воспользоваться этим гайдом.
import requests
from pyquery import PyQuery as pq
from tqdm import tqdm_notebook
Попробуем в качестве примера выкачать эзотерические стихи с сайта Стихи.ру. Сначала попробуем выгрузить только первое стихотворение из списка. Откроем ссылку на стихотворение в браузере, чтобы понять, что и где искать. Допустим, мы хотим вытащить со странички имя автора, название и текст.
Начнем с автора. Чтобы понять, какой элемент хранит в себе его имя и фамилию, мы нажимаем правой кнопкой мыши на автора и выбираем «Показать код» (варианты: Inspect, View Source Code и т.п.). Справа откроется окно с тем самым деревом html-элементов, о котором мы говорили ранее, где элемент, содержащий автора, будет подсвечен.
Что же мы видим: автор находится в элементе с тегом <a>, который передает ссылки. Действительно, если нажать на автора, то мы окажемся на страничке с его произведениями (ссылка на страницу передана в атрибуте href). Но так как нам нужна не ссылка, а просто имя, то, чтобы достать его, надо отступить по дереву элементов на шаг назад до тега <div>, в котором содержится атрибут class со значением titleauthor.
Напишем для этого код, в котором сначала мы сохраняем страничку в качестве html-документа (она сохранится как дерево из элементов), а из дерева достаем имя и фамилию автора и записываем их в отдельную переменную author.
response_poems = requests.get("https://www.stihi.ru/2019/03/28/5717")
# response_poems.text[:500]
author = pq(response_poems.text).find("div.titleauthor").text()
Теперь достанем название стихотворения, также нажав по нему правой кнопкой на страничке. Название стихотворения здесь находится в элементе с тегом <h1>. Напишем еще одну строчку кода:
title = pq(response_poems.text).find("h1").text()
Ну и наконец вытащим сам текст стихотворения. Подсветив элемент в окне с деревом, видим, что текст лежит в теге <div> с атрибутом class со значением text. Вот такая строчка кода вытащит текст стихотворения:
text = pq(response_poems.text).find("div.text").text()
А теперь попробуем выкачать все стихи с первой страницы раздела «мистика и эзотерика». Нам надо сначала собрать ссылки на стихи, а потом по каждой ссылке собирать содержимое — и делать это мы уже научились выше.
Сначала посмотрим, в каких элементах прячутся ссылки на стихи, подсветив их в окне кода страницы. Мы видим, что ссылки скрываются в теге <a> сразу с двумя атрибутами: href со значением самой URL-ссылки на стих и class со значением poemlink. В таком случае нам надо указать, что сначала мы достаем все теги <a>, которые содержат в себе атрибут со значением poemlink, а уже потом из этого элемента вытаскиваем значение атрибута href, то есть, саму ссылку. Посмотрим, как это будет выглядеть в коде:
response_poems_full = requests.get("https://www.stihi.ru/poems/list.html?topic=13")
poems_urls = []
for poem in pq(response_poems_full.text).find("a.poemlink"):
url = pq(poem).attr("href")
poems_urls.append("https://www.stihi.ru" + url)
Разберем код, написанный выше, построчно. В первой строке мы сохраняем страничку со ссылками на стихи как html-документ. Во второй строчке создаем пустой список, в который мы будет записывать наши ссылки. В третьей строке начинаем цикл, где указываем, что нам надо у всех элементов с тегом <a> со значением poemlink вытащить значение атрибута href. Это значение записывается в переменную url. Каждый такой url добавляется в созданный ранее список при помощи метода append. К каждой ссылке добавляется адрес сайта (ссылки внутри сайта относительные, а не полные, и выглядят вот так: /2019/03/28/7891. Поэтому их надо модифицировать).
Наконец напишем цикл, который соберет в себе все предыдущие этапы и для каждого стиха со странички скачает автора, название и текст в отдельный файл на компьютере (для этого в вашей рабочей папке надо заранее создать папку, куда будут скачиваться файлы).
for num, poem_url in tqdm_notebook(enumerate(poems_urls)):
res = requests.get(poem_url)
title = pq(res.text).find("h1").text()
author = pq(res.text).find("div.titleauthor").text()
text = ""
for poem in pq(res.text).find("div.text"):
text += pq(poem).text().replace("\n", "\n")
try:
with open(f"stihi/{num}.txt", "wt", encoding="utf-8") as f:
f.write(title + "\n" + author + "\n" + text)
except UnicodeEncodeError as err:
print(err, poem_url)
Разберем код детально. Для каждой пронумерованной ссылки (обходим и нумеруем ссылки встроенной функцией enumerate) мы сначала записываем веб-страничку в формате html-кода, откуда потом вытаскиваем название, автора и сам текст. В тексте заменяем все символы \n*** на перенос строки, и заносим его в переменную типа строка. После этого мы записываем каждое собранное стихотворение с метаинформацией (сначала название, на следующей строке автор, на следующей — сам текст) в отдельный файл формата txt с кодировкой utf-8. При этом мы говорим, что если какой-то текст содержит в себе непонятные символы (которые не поддерживаются в utf-8), то программе не надо останавливаться, а стоит пропустить файл и переходить к следующему (если этого не сделать, цикл при ошибке прервется).
*** Тут, вероятно, требуется пояснение. \n сам по себе означает перенос строки, однако, когда мы выкачиваем текст с html-кода, этот символ записывается как обычный набор знаков в тексте, поэтому его надо заменить на нормальный разделитель строки, который будет читаться в программе.
Собираем динамические сайты
Бывают ситуации, когда код для парсинга написан верно, но ничего не выкачивается, либо выдача никак не соответствует тому, что нужно было собрать. В таком случае, скорее всего, сайт динамический, и к нему нужен другой подход.
Динамические сайты не хранят всю информацию прямо у себя на страницах, а подгружают её со своего сервера с помощью API (мы рассказывали про общую идею API в прошлых постах по обкачиванию) при возникновении потребности. Например, когда пользователь скроллит вниз ленту, чтобы открыть доступ к более старым постам. В таком случае надо сначала найти ссылку, с помощью которой сайт подгружает информацию.
Посмотрим, где искать эту ссылку и что с ней делать, на примере новостного сайта Meduza. На главной странице с новостями в правом верхнем углу выбираем опцию «Показывать по порядку», чтобы мы могли получить доступ к более старым новостям, проскроллив до конца страницы. Выводим код страницы (как в предыдущем примере или с помощью Ctrl + Shift + I) и переходим во вкладку Network, после чего должно открыться пустое окно.
Прокручиваем ленту вниз и нажимаем «Показать ещё», чтобы открыть новости со следующей страницы. Теперь во вкладке справа появились ссылки разных типов (в основном, это ссылки на jpeg и gif). Нам нужна ссылка с типом xhr, которая отвечает, как правило, за отправку запроса к серверу через API. Нажимаем на неё и видим, что это действительно запрос на API сайта.
Теперь, после того как мы поняли, откуда подгружаются статьи, процесс парсинга очень похож на тот, которым выкачивают информацию с не-динамических сайтов.
Допустим, мы хотим выгрузить статьи с первых 10-ти страниц ленты: во-первых, надо собрать ссылки на них. В коде мы сначала записываем ссылку на запрос к API, которую мы нашли ранее, и заменяем номер страницы в ней на {page}, чтобы можно было в дальнейшем отформатировать содержимое фигурных скобок. Далее мы создаем список, в который будем записывать сами ссылки. И пишем цикл, в котором сначала вставляем в ссылку на запрос к API нужную страницу (от 1 до 10, то есть, до цифры 11), и потом добавляем в список сами ссылки на статьи из раздела collection на каждой странице.
url_t = "https://meduza.io/api/w4/search?chrono=news&page={page}&per_page=24&locale=ru"
articles = []
for page in range(1, 11):
url = url_t.format(page = page)
print(url)
res = requests.get(url)
articles.extend(res.json()["collection"])
Откуда появился раздел collection? Вернемся к окошку, откуда мы копировали ссылку на запрос к API. Переходим в раздел Preview, где находится, по сути, содержание сайта. Открываем вкладку collection, где видим 24 ссылки на статьи на странице — то, что нам и надо. В данном случае доставать ссылки через дерево элементов не требуется.
В целом у нас должно получиться 240 статей для выкачки. Начнем их собирать:
for num, article_url in tqdm_notebook(enumerate(articles)):
res = requests.get("https://meduza.io/" + article_url)
title = ""
for title in pq(res.text).find("h1.SimpleTitle-root"):
title = pq(title).text().replace("\xa0", " ")
text = ""
for paragraph in pq(res.text).find("div.GeneralMaterial-article p"):
text += pq(paragraph).text().replace("\xa0", " ")
try:
with open(f"texts_meduza/{num}.txt", "wt", encoding="utf-8") as f:
f.write(title + "\n" + text)
except UnicodeEncodeError as err:
print(err, article_url)
Разберем пошагово код выше. Каждую ссылку на статью мы сначала прикрепляем к ссылке на сайт и скачиваем статью как дерево элементов. После выкачиваем название статьи (находится в элементе с тегом <h1>со значением атрибута SimpleTitle-root), в которой заменяем не нужный нам символ \xa0 на пустое место. Далее делаем то же самое с текстом статьи (находится в элементе с тегом <div> с значением атрибута GeneralMaterial-article p). Название и текст мы записываем в отдельный файл с кодировкой utf-8 в заранее созданной папке texts_meduza в нашей рабочей папке Вновь говорим, что если в статье встречаются непонятные кодировке utf-8 символы, то эту статью мы игнорируем и качаем следующую, чтобы не убить весь цикл.
Успех, мы получили названия и тексты статей! Теперь докачаем к ним метаинформацию, например, дату публикации. Она содержится в элементе с тегом time со значением атрибута Timestamp-root. Скачаем название статьи, дату и время публикации в отдельные файлы в новую папку texts_meduza_meta (опять же создаем её в нашей рабочей папке).
for num, article_url in tqdm_notebook(enumerate(articles)):
res = requests.get("https://meduza.io/" + article_url)
title = ""
for title in pq(res.text).find("h1.SimpleTitle-root"):
title = pq(title).text().replace("\xa0", " ")
time = pq(res.text).find("time.Timestamp-root").text()
try:
with open(f"texts_meduza_meta/{num}.txt", "wt", encoding="utf-8") as f:
f.write(title + "\n" + time)
except UnicodeEncodeError as err:
print(err, article_url)
Поздравляю, мы успешно собрали корпус из статей и метаданные к ним!
В заключение следует заметить, что каждый сайт по структуре различается, поэтому каждый раз надо тратить некоторое время, чтобы найти нужные элементы в документе.