© pexels.com

Введение

В прошлых материалах мы рассказывали, как массово скачивать тексты из Твиттера и из «Вконтакте» Уметь выкачивать данные социальных сетей бывает полезно, но далеко не всегда там можно найти нужную для конкретных исследовательских целей информацию. Что делать в таком случае?

Можно пойти по протоптанной дорожке и найти подготовленные кем-то датасеты. Однако это не всегда работает: далеко не факт ,что кто-то озаботился собрать данные, нужные именно вам. . Зато если в поле зрения есть сайт с нужными данными, то можно скачать их собственноручно! В этом туториале мы разберем, как это сделать с помощью 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)

Поздравляю, мы успешно собрали корпус из статей и метаданные к ним!

В заключение следует заметить, что каждый сайт по структуре различается, поэтому каждый раз надо тратить некоторое время, чтобы найти нужные элементы в документе.

Ссылка на код на Github