Web-scraping: сбор данных из баз данных и интернет-источников

Алла Тамбовцева, НИУ ВШЭ

Семинар 2. Парсинг с библиотекой BeautifulSoup

Импортируем необходимые библиотеки и функции:

In [1]:
import requests
from bs4 import BeautifulSoup

Подключаемся к главной странице сайта nplus1.ru.

In [2]:
page = requests.get("https://nplus1.ru/")
page
Out[2]:
<Response [200]>

Посмотрим на вид запроса к странице – вызовем атрибут .raw:

In [3]:
page.raw
Out[3]:
<urllib3.response.HTTPResponse at 0x106c71710>

Задание 1

Запросите атрибуты url и text и сохраните их в переменные p_url и p_text.


In [4]:
p_url = page.url
p_text = page.text
In [5]:
p_url
Out[5]:
'https://nplus1.ru/'
In [6]:
p_text

Теперь подадим исходный код страницы (HTML) на вход функции BeautifulSoup(), чтобы превратить его в объект, по которому будет удобно искать информацию по тэгам:

In [7]:
soup = BeautifulSoup(page.text)
soup

Для примера выполним поиск по какому-нибудь тэгу с помощью метода .find_all(). Например, найдём все заголовки третьего уровня:

In [8]:
soup.find_all("h3")
Out[8]:
[<h3>Неолит в Юго-Восточном Узбекистане начался в VI тысячелетии до нашей эры</h3>,
 <h3>Арктические губковые сады выросли на остатках исчезнувшей тысячи лет назад экосистемы</h3>,
 <h3>Археологи обнаружили во Франции останки древнейшего кроманьонца</h3>,
 <h3>Вирус Эбола нашли в мозге приматов после выздоровления</h3>,
 <h3>«Индженьюити» совершил первый полет после пылевой бури</h3>,
 <h3>Суперионные сплавы железа обвинили в сильном замедлении сейсмических волн в центре Земли</h3>,
 <h3>В развитии японских и английских народных песен выявили схожие закономерности</h3>,
 <h3>Колесо DevOpSansara</h3>,
 <h3>Философский пароход и фабрики интеллигентов</h3>,
 <h3>Плазменные зонные пластинки помогут сфокусировать интенсивный свет</h3>,
 <h3>Электролиз воды в марсианских условиях выработал на шесть процентов меньше кислорода</h3>,
 <h3>ГЭС нанесли ущерб экономике и растительному покрову Глобального Юга</h3>,
 <h3>Геомагнитная буря обрекла 40 спутников Starlink на гибель</h3>,
 <h3>Увеличение продолжительности сна связали с уменьшением потребления калорий</h3>,
 <h3>Индонезийского крокодила освободили от шины на шее после шести лет неудачных попыток</h3>,
 <h3>Русское поле экспериментов</h3>,
 <h3>Зациклиться на устойчивости</h3>,
 <h3>IceCube не увидел магнитных монополей за восемь лет наблюдений</h3>,
 <h3>Страх перед хищниками сократил прирост численности певчих овсянок</h3>,
 <h3>Древние сибиряки заквасили рыбу в глубокой яме</h3>,
 <h3>Lockheed Martin создаст ракету для доставки марсианского грунта</h3>,
 <h3>Древний крокодил десятки миллионов лет назад наступил на свои фекалии</h3>,
 <h3>Избыток углового момента при сверхбыстром размагничивании ушел к фононам</h3>,
 <h3>Четыре сигнала Хунга-Тонга-Хунга-Хаапай</h3>,
 <h3>Пассажир из мезозоя</h3>,
 <h3><a href="/news/2022/02/08/cyclotron-resonance-overtones">Медленные плазмоны сделали графен аномальным поглотителем</a></h3>,
 <h3><a href="/news/2022/02/02/hydrohalogenation-chain-walking">Химики научились получать первичные галогениды из изомерных алкенов</a></h3>,
 <h3><a href="/news/2022/02/02/Schwinger-monopoles">Физики не увидели магнитных монополей в столкновении ядер свинца</a></h3>,
 <h3><a href="/news/2022/02/08/ultrafast-demagnetization">Избыток углового момента при сверхбыстром размагничивании ушел к фононам</a></h3>,
 <h3><a href="/news/2022/02/05/drill-the-tooth">Ученые построили механическую модель сверления зуба </a></h3>,
 <h3><a href="/news/2022/02/10/sponge-gardens">Арктические губковые сады выросли на остатках исчезнувшей тысячи лет назад экосистемы</a></h3>,
 <h3><a href="/news/2022/02/09/superionic-iron-alloys">Суперионные сплавы железа обвинили в сильном замедлении сейсмических волн в центре Земли</a></h3>,
 <h3><a href="/news/2022/02/07/melting-sosulkas">Аномальность воды повлияла на вариативность подводных сосулек</a></h3>,
 <h3><a href="/material/2022/02/10/lanit-test">Колесо DevOpSansara</a></h3>,
 <h3><a href="/news/2022/02/01/ictidomys-tridecemlineatus">Расщепляющие мочевину симбиотические бактерии защитили сусликов от атрофии мышц во время спячки</a></h3>]

Каждый элемент полученного списка – объект типа «элемент beautifulsoup», в который вложено некоторое содержимое, например, текст или новый код на HTML.


Задание 2

Сохраните полученные выше результаты в список h3. Запросите тип первого элемента списка с помощью функции type().

Задание 3

Найдите все ссылки на странице и сохраните их в список raw_links.


In [9]:
h3 = soup.find_all("h3")
type(h3[0])
Out[9]:
bs4.element.Tag
In [8]:
raw_links = soup.find_all("a")
raw_links

Возьмём ссылку на рубрику Астрономия и посмотрим не неё, она пятая в полученном списке:

In [11]:
raw_links[4]
Out[11]:
<a class="" href="/rubric/astronomy">Астрономия</a>

С самим тэгом работать не очень интересно, нас интересует его содержимое. Запросим текст внутри тэга:

In [12]:
raw_links[4].text
Out[12]:
'Астрономия'
In [ ]:
# a = {"class" : "", "href" : "/rubric/astronomy"}

А теперь извлечём саму ссылку – значение атрибута href:

In [13]:
raw_links[4]["href"]
Out[13]:
'/rubric/astronomy'

Или так:

In [14]:
raw_links[4].get("href")
Out[14]:
'/rubric/astronomy'

Если бы мы захотели вывести все ссылки на экран, нам понадобился бы цикл for:

In [9]:
for link in raw_links:
    print(link["href"])

Задание 4

Для дальнейшей работы нам нужны только ссылки на новости, то есть те ссылки, которые начинаются с /news. Напишите код, который извлекает из списка raw_links только те тэги, которые содержат ссылки на новости, и сохраняет в список news сами ссылки в виде текста, без тэгов.

Задание 5

Ссылки на новости в списке news – относительные, по ним нельзя сразу перейти на страницу новости, а значит, нельзя передать Python для дальнейшей работы. Сделайте ссылки абсолютными – доклейте к каждой ссылке в news ссылку на главную страницу сайта https://nplus1.ru и сохраните полученные результаты в список links_full.


In [16]:
news = []
for link in raw_links:
    if "news" in link["href"]:
        news.append(link["href"])
news
Out[16]:
['/news/2022/02/10/neolithization-uzbekistan',
 '/news/2022/02/10/sponge-gardens',
 '/news/2022/02/09/first-modern-humans',
 '/news/2022/02/09/ebola-in-brains',
 '/news/2022/02/09/mars-drone-1-flight-2022',
 '/news/2022/02/09/superionic-iron-alloys',
 '/news/2022/02/09/musical-evolution',
 '/news/2022/02/09/plasma-zone-plate',
 '/news/2022/02/09/is-there-elecrolysis-on-Mars',
 '/news/2022/02/09/hydropower-dams',
 '/news/2022/02/09/starlink-lose',
 '/news/2022/02/09/sleep-energy',
 '/news/2022/02/09/free-croc',
 '/news/2022/02/09/icecube-monopoles',
 '/news/2022/02/09/melospiza-melodia',
 '/news/2022/02/09/storage-pit',
 '/news/2022/02/08/rocket-for-mars-samples',
 '/news/2022/02/08/crocodilian-coprolite',
 '/news/2022/02/08/ultrafast-demagnetization',
 '/news/2022/02/08/cyclotron-resonance-overtones',
 '/news/2022/02/02/hydrohalogenation-chain-walking',
 '/news/2022/02/02/Schwinger-monopoles',
 '/news/2022/02/08/ultrafast-demagnetization',
 '/news/2022/02/05/drill-the-tooth',
 '/news/2022/02/10/sponge-gardens',
 '/news/2022/02/09/superionic-iron-alloys',
 '/news/2022/02/07/melting-sosulkas',
 '/news/2022/02/01/ictidomys-tridecemlineatus']
In [17]:
links_full = []
for link in news:
    res = "https://nplus1.ru" + link
    links_full.append(res)
In [18]:
links_full
Out[18]:
['https://nplus1.ru/news/2022/02/10/neolithization-uzbekistan',
 'https://nplus1.ru/news/2022/02/10/sponge-gardens',
 'https://nplus1.ru/news/2022/02/09/first-modern-humans',
 'https://nplus1.ru/news/2022/02/09/ebola-in-brains',
 'https://nplus1.ru/news/2022/02/09/mars-drone-1-flight-2022',
 'https://nplus1.ru/news/2022/02/09/superionic-iron-alloys',
 'https://nplus1.ru/news/2022/02/09/musical-evolution',
 'https://nplus1.ru/news/2022/02/09/plasma-zone-plate',
 'https://nplus1.ru/news/2022/02/09/is-there-elecrolysis-on-Mars',
 'https://nplus1.ru/news/2022/02/09/hydropower-dams',
 'https://nplus1.ru/news/2022/02/09/starlink-lose',
 'https://nplus1.ru/news/2022/02/09/sleep-energy',
 'https://nplus1.ru/news/2022/02/09/free-croc',
 'https://nplus1.ru/news/2022/02/09/icecube-monopoles',
 'https://nplus1.ru/news/2022/02/09/melospiza-melodia',
 'https://nplus1.ru/news/2022/02/09/storage-pit',
 'https://nplus1.ru/news/2022/02/08/rocket-for-mars-samples',
 'https://nplus1.ru/news/2022/02/08/crocodilian-coprolite',
 'https://nplus1.ru/news/2022/02/08/ultrafast-demagnetization',
 'https://nplus1.ru/news/2022/02/08/cyclotron-resonance-overtones',
 'https://nplus1.ru/news/2022/02/02/hydrohalogenation-chain-walking',
 'https://nplus1.ru/news/2022/02/02/Schwinger-monopoles',
 'https://nplus1.ru/news/2022/02/08/ultrafast-demagnetization',
 'https://nplus1.ru/news/2022/02/05/drill-the-tooth',
 'https://nplus1.ru/news/2022/02/10/sponge-gardens',
 'https://nplus1.ru/news/2022/02/09/superionic-iron-alloys',
 'https://nplus1.ru/news/2022/02/07/melting-sosulkas',
 'https://nplus1.ru/news/2022/02/01/ictidomys-tridecemlineatus']

Теперь выберем первую ссылку и напишем код, который будет сгружать информацию о новости по её ссылке. Так как все страницы с новостями на этом сайте строятся по единой схеме, если мы научимся выгружать данные по одной новости, мы сможем повторить это для всех новостей!

In [19]:
my_link = links_full[0]

Подключимся к странице этой новости, выгрузим её исходный код и превратим в объект BeautifulSoup:

In [20]:
my_page = requests.get(my_link)
my_soup = BeautifulSoup(my_page.text)

Если мы посмотрим на исходный код страницы, мы заметим, что общая информация по новости хранится в тэгах <meta>:

In [21]:
my_soup.find_all("meta")
Out[21]:
[<meta charset="utf-8"/>,
 <meta content="ie=edge" http-equiv="x-ua-compatible"/>,
 <meta content="width=device-width, initial-scale=1" name="viewport"/>,
 <meta content="yes" name="apple-mobile-web-app-capable"/>,
 <meta content="black" name="apple-mobile-web-app-status-bar-style"/>,
 <meta content="7991d7eb02d759f05b9050e111a7e3eb" name="wmail-verification"/>,
 <meta content="2022-02-10" itemprop="datePublished"/>,
 <meta content="Михаил Подрезов" name="mediator_author"/>,
 <meta content="Археологи провели исследование скального навеса Кайнар-Камар" name="description"/>,
 <meta content="Михаил Подрезов" name="author"/>,
 <meta content="" name="copyright"/>,
 <meta content="Неолит в Юго-Восточном Узбекистане начался в VI тысячелетии до нашей эры" property="og:title"/>,
 <meta content="https://nplus1.ru/images/2022/02/09/917af62bc0de1c67509cef3988890414.jpg" property="og:image"/>,
 <meta content="https://nplus1.ru/news/2022/02/10/neolithization-uzbekistan" property="og:url"/>,
 <meta content="Археологи провели исследование скального навеса Кайнар-Камар" property="og:description"/>,
 <meta content="summary_large_image" name="twitter:card"/>,
 <meta content="@nplusodin" name="twitter:site"/>,
 <meta content="Неолит в Юго-Восточном Узбекистане начался в VI тысячелетии до нашей эры" name="twitter:title"/>,
 <meta content="Археологи провели исследование скального навеса Кайнар-Камар" name="twitter:description"/>,
 <meta content="https://nplus1.ru/images/2022/02/09/917af62bc0de1c67509cef3988890414.jpg" name="twitter:image"/>,
 <meta content="8c90b02c84ac3b72" name="yandex-verification"/>,
 <meta content="b419949322895fc9106e24ed01be58ac" name="pmail-verification"/>]

Как выбрать только те части, которые нам могут быть интересны? Например, части HTML с автором статьи, датой её публикации, заголовком и кратким содержанием? Выполнить более точный поиск с учётом конкретных атрибутов и их значений. Например, мы видим, что имя автора находится в тэге <meta> с атрибутом name, равным mediator_author:

In [22]:
my_soup.find_all("meta", {"name" : "mediator_author"})
Out[22]:
[<meta content="Михаил Подрезов" name="mediator_author"/>]

Отлично, мы вышли на автора статьи! Только хотелось бы получить его имя в виде «чистого» текста. Извлечём из полученного списка один единственный элемент и заберём из него значение атрибута content (вспомните про работу с href ранее):

In [23]:
my_soup.find_all("meta", {"name" : "mediator_author"})[0].get("content")
Out[23]:
'Михаил Подрезов'
In [24]:
my_soup.find_all("meta", {"name" : "mediator_author"})[0]["content"] 
Out[24]:
'Михаил Подрезов'
In [25]:
author = my_soup.find_all("meta", 
                 {"name" : "mediator_author"})[0].get("content")

Задание 6

Аналогичным образом извлеките дату публикации новости, заголовок и краткое содержание (описание) новости и сохраните их в переменные date, title, desc соответственно.


In [30]:
date = my_soup.find_all("meta", 
                 {"itemprop" : "datePublished"})[0].get("content")
title = my_soup.find_all("title")[0].text
desc = my_soup.find_all("meta", 
                        {"name" : "description"})[0].get("content")

Какие ещё характеристики новости нам могут пригодиться (сам текст пока не трогаем)? Время публикации, рубрики и сложность новости. Если пролистаем исходный код до начала самой новости, мы обнаружим перед текстом три таблицы, три абзаца <p> с классом table. Все они находятся в разделе <div> с классом tables.


Задание 7

Извлеките время публикации новости, рубрики и сложность новости и сохраните их в переменные time, rubs, diffc соответственно.


In [34]:
div = my_soup.find_all("div", {"class" : "tables"})[0]
div
Out[34]:
<div class="tables">
<p class="table">
<a data-rubric="archaeology" href="/rubric/archaeology">Археология</a>
<a data-rubric="anthropology" href="/rubric/anthropology">Антропология</a>
</p>
<p class="table">
<a href="/news/2022/02/10">
<time content="2022-02-10" data-unix="1644478290" itemprop="datePublished">
<span>10:31</span>
<span>10 Фев. 2022</span>
</time>
</a>
</p>
<p class="table">
<a href="/difficult/3.9">
<span>Сложность</span>
<span class="difficult-value">3.9</span>
</a>
</p>
</div>
In [39]:
div.find_all("span")
Out[39]:
[<span>10:31</span>,
 <span>10 Фев. 2022</span>,
 <span>Сложность</span>,
 <span class="difficult-value">3.9</span>]
In [41]:
time = div.find_all("span")[0].text
In [42]:
diffc = div.find_all("span", {"class" : "difficult-value"})[0].text
In [46]:
rubs_raw = div.find_all("p")[0].find_all("a")
rubs_raw
Out[46]:
[<a data-rubric="archaeology" href="/rubric/archaeology">Археология</a>,
 <a data-rubric="anthropology" href="/rubric/anthropology">Антропология</a>]
In [47]:
rubs = []
for r in rubs_raw:
    rubs.append(r.text)
rubs
In [50]:
", ".join(rubs)
Out[50]:
'Археология, Антропология'