Алла Тамбовцева, НИУ ВШЭ
Мы уже немного познакомились со структурой html-файлов, теперь попробуем выгрузить информацию из реальной страницы, а точнее, с сайта nplus1.ru. Наша задача: выгрузить недавние новости в датафрейм pandas, чтобы потом сохранить все в файл Excel.
Сначала сгрузим весь html-код страницы и сохраним его в отдельную переменную. Для этого нам понадобится библиотека requests
. Импортируем ее:
import requests
Сохраним ссылку на главную страницу сайта в переменную url
для удобства и выгрузим страницу. Разумеется, это будет работать при подключении к интернету. Если соединение будет отключено, Python выдаст NewConnectionError
.
url = "https://nplus1.ru/"
page = requests.get(url)
Если мы просто посмотрим на объект, мы ничего особенного не увидим:
page
<Response [200]>
Объект page
имеет тип Response
и скрыт от наших глаз. Однако при его вызове мы видим число 200 – это код результата, который означает, что страница благополучно загружена.
У объекта типа Response
есть атрибут .text
, в котором хранится исходный код страницы, который мы можем посмотреть, нажав Ctrl+U в Chrome:
page.text
Результат выше – это обычная строка, тип string. Выполнять поиск по такой строке неудобно, поэтому загрузим из модуля bs4
функцию BeautifulSoup()
, которая позволит преобразовать эту строку в объект, который позволяет выполнять поиск по тегам.
from bs4 import BeautifulSoup
soup = BeautifulSoup(page.text)
Если код выше выдает ошибку (зависит от версии bs4
), можно указать парсер, который необходимо использовать, явно:
soup = BeautifulSoup(page.text, 'lxml') #lxml
Чтобы сгрузить все новости с главной страницы сайта, нужно собрать все ссылки на страницы с этими новостями. Ссылки в html-файле всегда заключены в тэг <a></a>
и имеют атрибут href
. Посмотрим на кусочки кода, соответствующие всем ссылкам на главной странице сайта:
for link in soup.find_all('a'):
print(link.get('href'))
# / # # /rubric/astronomy /rubric/physics /rubric/biology /rubric/robots-drones /theme/explainatorium /theme/bookshelf /theme/Courses /theme/coronavirus-history / # /rubric/astronomy /rubric/physics /rubric/biology /rubric/robots-drones # /theme/explainatorium /theme/bookshelf /theme/Courses /theme/coronavirus-history https://nplus1.ru/blog/2020/04/17/how-music-works https://nplus1.ru/blog/2020/04/17/how-music-works https://nplus1.ru/blog/2020/04/17/nudity-censorship https://nplus1.ru/blog/2020/04/16/stories-of-surgery-for-broken-hearts https://nplus1.ru/material/2020/04/08/coronarumors https://nplus1.ru/blog/2020/04/13/ancient-greece-from-prehistoric-to-hellenistic-tim https://nplus1.ru/blog/2020/04/10/guestmixIvanZoloto https://nplus1.ru/blog/2020/04/10/the-nature-and-necessity-of-bees https://nplus1.ru/blog/2020/04/07/troubled-oculudentavis https://nplus1.ru/blog/2020/04/06/maps-of-meaning https://nplus1.ru/material/2020/04/08/coronarumors /news/2020/04/27/venus-atmosphere-tidal-waves /news/2020/04/27/zumwalt /news/2020/04/27/starship-pressure-passed /news/2020/04/27/e-fan-x /news/2020/04/25/hyperphagia-neurons /news/2020/04/25/dna-pocket /news/2020/04/25/moon-landing-spray /material/2020/04/25/on-montreal /rubric/ecology /news/2020/04/25/thermogalvanic-hydrogel-cooling /news/2020/04/24/30-years-hubble /news/2020/04/24/feline-grimace-scale /news/2020/04/24/supermassive-black-hole-escape-star /news/2020/04/24/adobe /news/2020/04/24/broken-hearts /blog/2020/04/24/kronhaus2 /blog/2020/04/15/women-health-animation /news/2020/04/24/00s-taxi /rubric/partners /material/2020/04/23/every-you-every-me /rubric/psychology /news/2020/04/24/cold-sweet /news/2020/04/24/avian /news/2020/04/24/kentaurs-interstellar /news/2020/04/24/ocean-macroplastic-finding /news/2020/04/24/smartwatch /news/2020/04/24/antarctic-calyptocephalella /blog/2020/04/24/to-stop-an-epidemic /blog/2020/04/23/atlas /material/2020/04/22/antique-masks /rubric/history /theme/abc-archeology /material/2020/04/21/kn-surfaces /rubric/biology /theme/bellezza /news/2020/04/23/underwater-quantum-communication /news/2020/04/21/self-adapt-PVDF /news/2020/04/27/venus-atmosphere-tidal-waves /material/2020/04/22/coffee-aroma /news/2020/04/25/thermogalvanic-hydrogel-cooling /news/2020/04/25/hyperphagia-neurons /news/2020/04/22/carbon-nanothread-bundle /news/2020/04/21/model-based-learning /news/2020/04/23/gw-190412-ligo /news/2020/04/21/digital-embryo https://nplus1.ru/blog/2020/04/17/how-music-works https://nplus1.ru/blog/2020/04/17/how-music-works https://nplus1.ru/blog/2020/04/17/nudity-censorship https://nplus1.ru/blog/2020/04/16/stories-of-surgery-for-broken-hearts https://nplus1.ru/material/2020/04/08/coronarumors https://nplus1.ru/blog/2020/04/13/ancient-greece-from-prehistoric-to-hellenistic-tim https://nplus1.ru/blog/2020/04/10/guestmixIvanZoloto https://nplus1.ru/blog/2020/04/10/the-nature-and-necessity-of-bees https://nplus1.ru/blog/2020/04/07/troubled-oculudentavis https://nplus1.ru/blog/2020/04/06/maps-of-meaning https://nplus1.ru/material/2020/04/08/coronarumors / /about /adv /rules /vacancy /difficult https://nplus1.ru/personal-data-policy # https://t.me/nplusone http://vk.com/nplusone https://www.facebook.com/nplusone https://twitter.com/nplusodin https://ok.ru/nplus1 https://soundcloud.com/nplus_1 /rss
В коде выше мы использовали метод find_all()
, который выполняет поиск по заданному тэгу и возвращает список частей кода HTML с выбранным тэгом. Каждый элемент возвращаемого списка имеет тип BeautifulSoup
и структуру, очень похожую на словарь. Например, ссылка <a href="/rubric/robots-drones" class="">
изнутри выглядит как словарь следующего вида:
{'href' : '/rubric/robots-drones',
'class' : ''}.
Как мы помним, значение по ключу из словаря можно вызвать с помощью метода .get()
. Именно его мы и использовали в коде выше, чтобы извлечь содержимое href
.
Ссылок в списке выше много. Но нам нужны только новости – ссылки, которые начинаются со слова /news
. Добавим условие: будем выбирать только те ссылки, в которых есть /news
. Создадим пустой список urls
и будем добавлять в него только ссылки, которые удовлетворяют этому условию.
urls = []
for link in soup.find_all('a'):
if '/news' in link.get('href'):
urls.append(link.get('href'))
urls
['/news/2020/04/27/venus-atmosphere-tidal-waves', '/news/2020/04/27/zumwalt', '/news/2020/04/27/starship-pressure-passed', '/news/2020/04/27/e-fan-x', '/news/2020/04/25/hyperphagia-neurons', '/news/2020/04/25/dna-pocket', '/news/2020/04/25/moon-landing-spray', '/news/2020/04/25/thermogalvanic-hydrogel-cooling', '/news/2020/04/24/30-years-hubble', '/news/2020/04/24/feline-grimace-scale', '/news/2020/04/24/supermassive-black-hole-escape-star', '/news/2020/04/24/adobe', '/news/2020/04/24/broken-hearts', '/news/2020/04/24/00s-taxi', '/news/2020/04/24/cold-sweet', '/news/2020/04/24/avian', '/news/2020/04/24/kentaurs-interstellar', '/news/2020/04/24/ocean-macroplastic-finding', '/news/2020/04/24/smartwatch', '/news/2020/04/24/antarctic-calyptocephalella', '/news/2020/04/23/underwater-quantum-communication', '/news/2020/04/21/self-adapt-PVDF', '/news/2020/04/27/venus-atmosphere-tidal-waves', '/news/2020/04/25/thermogalvanic-hydrogel-cooling', '/news/2020/04/25/hyperphagia-neurons', '/news/2020/04/22/carbon-nanothread-bundle', '/news/2020/04/21/model-based-learning', '/news/2020/04/23/gw-190412-ligo', '/news/2020/04/21/digital-embryo']
Ссылки, которые у нас есть в списке urls
, относительные: они неполные, начало ссылки, в данном случае название сайта, отсутствует. Давайте превратим их в абсолютные ‒ склеим с ссылкой https://nplus1.ru.
full_urls = ['https://nplus1.ru' + u for u in urls]
full_urls
['https://nplus1.ru/news/2020/04/27/venus-atmosphere-tidal-waves', 'https://nplus1.ru/news/2020/04/27/zumwalt', 'https://nplus1.ru/news/2020/04/27/starship-pressure-passed', 'https://nplus1.ru/news/2020/04/27/e-fan-x', 'https://nplus1.ru/news/2020/04/25/hyperphagia-neurons', 'https://nplus1.ru/news/2020/04/25/dna-pocket', 'https://nplus1.ru/news/2020/04/25/moon-landing-spray', 'https://nplus1.ru/news/2020/04/25/thermogalvanic-hydrogel-cooling', 'https://nplus1.ru/news/2020/04/24/30-years-hubble', 'https://nplus1.ru/news/2020/04/24/feline-grimace-scale', 'https://nplus1.ru/news/2020/04/24/supermassive-black-hole-escape-star', 'https://nplus1.ru/news/2020/04/24/adobe', 'https://nplus1.ru/news/2020/04/24/broken-hearts', 'https://nplus1.ru/news/2020/04/24/00s-taxi', 'https://nplus1.ru/news/2020/04/24/cold-sweet', 'https://nplus1.ru/news/2020/04/24/avian', 'https://nplus1.ru/news/2020/04/24/kentaurs-interstellar', 'https://nplus1.ru/news/2020/04/24/ocean-macroplastic-finding', 'https://nplus1.ru/news/2020/04/24/smartwatch', 'https://nplus1.ru/news/2020/04/24/antarctic-calyptocephalella', 'https://nplus1.ru/news/2020/04/23/underwater-quantum-communication', 'https://nplus1.ru/news/2020/04/21/self-adapt-PVDF', 'https://nplus1.ru/news/2020/04/27/venus-atmosphere-tidal-waves', 'https://nplus1.ru/news/2020/04/25/thermogalvanic-hydrogel-cooling', 'https://nplus1.ru/news/2020/04/25/hyperphagia-neurons', 'https://nplus1.ru/news/2020/04/22/carbon-nanothread-bundle', 'https://nplus1.ru/news/2020/04/21/model-based-learning', 'https://nplus1.ru/news/2020/04/23/gw-190412-ligo', 'https://nplus1.ru/news/2020/04/21/digital-embryo']
Теперь наша задача сводится к следующему: изучить одну страницу с новостью, научиться из нее вытаскивать текст и всю необходимую информацию, а потом применить весь набор действий к каждой ссылке из full_urls
в цикле. Посмотрим на новость с индексом 0, у вас может быть другая, новости обновляются.
url0 = full_urls[0]
print(url0)
https://nplus1.ru/news/2020/04/27/venus-atmosphere-tidal-waves
page0 = requests.get(url0)
soup0 = BeautifulSoup(page0.text)
В коде каждой страницы с новостью есть часть с мета-информацией: датой, именем автора и проч. Такая информация окружена тэгом <meta></meta>
. Посмотрим:
soup0.find_all('meta')
[<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="2020-04-27" itemprop="datePublished"/>, <meta content="Кристина Уласович" name="mediator_author"/>, <meta content="Японские планетологи выяснили, что необычно быстрое вращение атмосферы Венеры поддерживается благодаря тепловым приливам, волнам Россби и турбулентности" name="description"/>, <meta content="Кристина Уласович" name="author"/>, <meta content="" name="copyright"/>, <meta content="Суперротацию атмосферы Венеры объяснили тепловыми приливами" property="og:title"/>, <meta content="https://nplus1.ru/images/2020/04/27/d7e9e0accb014ce8a17f20391ee1e50a.jpg" property="og:image"/>, <meta content="https://nplus1.ru/news/2020/04/27/venus-atmosphere-tidal-waves" property="og:url"/>, <meta content="Японские планетологи выяснили, что необычно быстрое вращение атмосферы Венеры поддерживается благодаря тепловым приливам, волнам Россби и турбулентности" property="og:description"/>, <meta content="summary_large_image" name="twitter:card"/>, <meta content="@nplusodin" name="twitter:site"/>, <meta content="Суперротацию атмосферы Венеры объяснили тепловыми приливами" name="twitter:title"/>, <meta content="Японские планетологи выяснили, что необычно быстрое вращение атмосферы Венеры поддерживается благодаря тепловым приливам, волнам Россби и турбулентности" name="twitter:description"/>, <meta content="https://nplus1.ru/images/2020/04/27/d7e9e0accb014ce8a17f20391ee1e50a.jpg" name="twitter:image"/>, <meta content="8c90b02c84ac3b72" name="yandex-verification"/>]
Из этого списка нам нужны части с именем автора, датой, заголовком и кратким описанием. Воспользуемся поиском по атрибуту name
. Передадим функции find_all()
в качестве аргумента словарь с названием и значением атрибута:
soup0.find_all('meta', {'name' : 'author'})
[<meta content="Кристина Уласович" name="author"/>]
Теперь выберем единственный элемент полученного списка (с индексом 0):
soup0.find_all('meta', {'name' : 'author'})[0]
<meta content="Кристина Уласович" name="author"/>
Объект выше имеет структуру как у словаря, поэтому мы можем вызвать значение content
через метод .get()
:
soup0.find_all('meta', {'name' : 'author'})[0].get('content')
'Кристина Уласович'
Или вовсе без него, указав название ключа в квадратных скобках:
author = soup0.find_all('meta', {'name' : 'author'})[0]['content']
Изучим внимательно исходный код страницы новости и аналогичным образом найдем заголовок новости, описание и дату публикации:
title = soup0.find_all('meta',
{'property' : 'og:title'})[0]['content']
description = soup0.find_all('meta',
{'property' : 'og:description'})[0]['content']
date = soup0.find_all('meta',
{'itemprop' : 'datePublished'})[0]['content']
Осталось вытащить сложность текста и рубрики. Сложность находится в тэге span
с классом difficult-value
:
soup0.find_all('span', {'class' : 'difficult-value'})[0]
<span class="difficult-value">5.7</span>
Извлечем текст, который находится внутри тэгов, с помощью атрибута .text
:
difficult = soup0.find_all('span',
{'class' : 'difficult-value'})[0].text
С рубриками интереснее. Рубрики находятся в тэгах p
с классом table
:
soup0.find_all('p', {'class' : 'table'})[0]
<p class="table"> <a data-rubric="astronomy" href="/rubric/astronomy">Астрономия</a> </p>
В этом отрывке кода есть ссылки на рубрики. «Выцепим» все ссылки по тэгу a
:
raw_rubrics = soup0.find_all('p',
{'class' : 'table'})[0].find_all('a')
raw_rubrics
[<a data-rubric="astronomy" href="/rubric/astronomy">Астрономия</a>]
А теперь извлечем из каждого кусочка кода для ссылки текст:
rubrics = []
for r in raw_rubrics:
rubrics.append(r.text)
rubrics
['Астрономия']
Перейдем к самому главному – тексту новости. Как можно заметить, текст сохранен в абзацах <p></p>
, причем безо всяких атрибутов. Сообщим Python, что нас интересуют куски с пустым атрибутом class
:
soup0.find_all('p', {'class' : None})
[<p>Японские планетологи выяснили, что необычно быстрое вращение атмосферы Венеры поддерживается благодаря тепловым приливам, волнам Россби и турбулентности. К такому выводу они пришли на основе снимков облачного слоя, сделанных с помощью межпланетного зонда «Акацуки». Статья <a href="https://science.sciencemag.org/cgi/doi/10.1126/science.aaz4439" rel="nofollow" target="_blank">опубликована</a> в журнале <i>Science</i>.<br/></p>, <p>Еще в середине прошлого века астрономы заметили, что верхние слои плотного облачного покрова Венеры движутся намного быстрее ее поверхности. В то время как период вращения планеты составляет 243 земных дня, ее атмосфере на полный оборот требуется всего 92 часа — этот феномен назвали суперротацией. Для поддержания суперротации необходимо непрерывное перераспределение углового момента, которое позволило бы преодолеть трение с поверхностью планеты, однако механизмы, лежащие в основе этого процесса, до сих пор оставались неизвестны. </p>, <p>Такеши Хоринучи (Takeshi Horinouchi) из Университета Хоккайдо вместе с коллегами изучили снимки, сделанные аппаратом <a href="https://ru.wikipedia.org/wiki/Акацуки_(космический_аппарат)" rel="nofollow" target="_blank" title="Link: https://ru.wikipedia.org/wiki/Акацуки_(космический_аппарат)">«Акацуки»</a> японского аэрокосмического агентства JAXA. Используя данные наблюдений в ультрафиолетовом и инфракрасном диапазонах с 2015 по 2018 год, исследователи отследили движение облаков и определили скорость движения ветров на разных широтах, а затем построили глобальную модель переноса углового момента в атмосфере.</p>, <p>Анализ показал, что угловой момент возникает и поддерживается за счет тепловых приливов, которые представляют собой изменения атмосферного давления, вызванные солнечным нагревом вблизи экватора планеты. Им в противовес действуют волны планетарного масштаба (также известные как <a href="https://ru.wikipedia.org/wiki/Волны_Россби" rel="nofollow" target="_blank">волны Россби</a>) и крупномасштабная атмосферная турбулентность.</p>, <p></p>, <p>При этом эксперты <a href="https://cosmosmagazine.com/space/super-rotation-and-venus-atmosphere" rel="nofollow" target="_blank" title="Link: https://cosmosmagazine.com/space/super-rotation-and-venus-atmosphere">отмечают</a>, что вопрос о том, отражает ли построенная японскими планетологами модель полную картину, остается открытым, так как исследователи проанализировали лишь один слой газовой оболочки Венеры. По их мнению, существует вероятность, что активность и сила влияния атмосферных волн может отличаться на других уровнях облачного покрова планеты. </p>, <p>Ранее исследователи <a href="https://nplus1.ru/news/2016/07/19/venus-surface-influence" target="_blank" title="Link: https://nplus1.ru/news/2016/07/19/venus-surface-influence">выяснили</a>, что на поведение атмосферы Венеры может также влиять и форма ее поверхности. По их мнению, под плотным слоем облаков планеты могут скрываться горы, на которые наталкиваются воздушные потоки, в результате чего формируются волны тяготения. </p>, <p><i>Кристина Уласович</i></p>, <p>Нашли опечатку? Выделите фрагмент и нажмите Ctrl+Enter.</p>, <p>Коэффициент сложности</p>, <p>Коэффициент сложности</p>, <p>Коэффициент сложности</p>, <p>Коэффициент сложности</p>, <p>Коэффициент сложности</p>, <p>Коэффициент сложности</p>, <p>Коэффициент сложности</p>, <p>Коэффициент сложности</p>, <p>Коэффициент сложности</p>, <p>Коэффициент сложности</p>, <p>© 2020 N+1 Интернет-издание Свидетельство о регистрации СМИ Эл № ФС77-67614</p>, <p>Использование всех текстовых материалов без изменений в некоммерческих целях разрешается со ссылкой на N+1. Все аудиовизуальные произведения являются собственностью своих авторов и правообладателей и используются только в образовательных и информационных целях. Если вы являетесь собственником того или иного произведения и не согласны с его размещением на нашем сайте, пожалуйста, напишите на kirill@nplus1.ru</p>, <p>Материалы, опубликованные в разделе «Блоги», отражают позиции их авторов, которые могут не совпадать с мнением редакции.</p>, <p>Сайт может содержать контент, не предназначенный для лиц младше 18 лет.</p>, <p> <a class="pd" href="https://nplus1.ru/personal-data-policy">Политика обработки персональных данных пользователей сайта</a> </p>, <p> <script async="" src="//s.luxupcdnc.com/t/consent_195458.js" type="text/javascript"></script> <a href="#" onclick="if(window.__lxG__consent__ !== undefined) {window.__lxG__consent__.showConsent()} else {alert('This function works only for users from the European Economic Area (EEA).')}; return false">Change privacy settings</a> </p>, <p> </p>]
«Выцепим» все тексты (без тэгов) из полученного списка и склеим все элементы списка text
через пробел:
pars = []
for p in soup0.find_all('p', {'class' : None}):
pars.append(p.text)
text = " ".join(pars)
Избавимся от лишнего текста после фразы Нашли опечатку? и заменим лишние символы на обычные пробелы:
text_final = text.split("Нашли опечатку?")[0].replace('\xa0',
' ').replace('\n', ' ')
print(text_final)
Японские планетологи выяснили, что необычно быстрое вращение атмосферы Венеры поддерживается благодаря тепловым приливам, волнам Россби и турбулентности. К такому выводу они пришли на основе снимков облачного слоя, сделанных с помощью межпланетного зонда «Акацуки». Статья опубликована в журнале Science. Еще в середине прошлого века астрономы заметили, что верхние слои плотного облачного покрова Венеры движутся намного быстрее ее поверхности. В то время как период вращения планеты составляет 243 земных дня, ее атмосфере на полный оборот требуется всего 92 часа — этот феномен назвали суперротацией. Для поддержания суперротации необходимо непрерывное перераспределение углового момента, которое позволило бы преодолеть трение с поверхностью планеты, однако механизмы, лежащие в основе этого процесса, до сих пор оставались неизвестны. Такеши Хоринучи (Takeshi Horinouchi) из Университета Хоккайдо вместе с коллегами изучили снимки, сделанные аппаратом «Акацуки» японского аэрокосмического агентства JAXA. Используя данные наблюдений в ультрафиолетовом и инфракрасном диапазонах с 2015 по 2018 год, исследователи отследили движение облаков и определили скорость движения ветров на разных широтах, а затем построили глобальную модель переноса углового момента в атмосфере. Анализ показал, что угловой момент возникает и поддерживается за счет тепловых приливов, которые представляют собой изменения атмосферного давления, вызванные солнечным нагревом вблизи экватора планеты. Им в противовес действуют волны планетарного масштаба (также известные как волны Россби) и крупномасштабная атмосферная турбулентность. При этом эксперты отмечают, что вопрос о том, отражает ли построенная японскими планетологами модель полную картину, остается открытым, так как исследователи проанализировали лишь один слой газовой оболочки Венеры. По их мнению, существует вероятность, что активность и сила влияния атмосферных волн может отличаться на других уровнях облачного покрова планеты. Ранее исследователи выяснили, что на поведение атмосферы Венеры может также влиять и форма ее поверхности. По их мнению, под плотным слоем облаков планеты могут скрываться горы, на которые наталкиваются воздушные потоки, в результате чего формируются волны тяготения. Кристина Уласович
Теперь все красиво. Перейдем к семинару – напишем функцию для выгрузки информации по одной новости и применим ее к новостям на главной странице.
def get_info(url0):
page0 = requests.get(url0)
soup0 = BeautifulSoup(page0.text, 'lxml')
author = soup0.find_all('meta', {'name' : 'author'})[0]['content']
title = soup0.find_all('meta', {'property' : 'og:title'})[0]['content']
description = soup0.find_all('meta',
{'property' : 'og:description'})[0]['content']
date = soup0.find_all('meta',
{'itemprop' : 'datePublished'})[0]['content']
difficult = soup0.find_all('span',
{'class' : 'difficult-value'})[0].text
raw_rubrics = soup0.find_all('p',
{'class' : 'table'})[0].find_all('a')
rubrics = []
for r in raw_rubrics:
rubrics.append(r.text)
pars = []
for p in soup0.find_all('p', {'class' : None}):
pars.append(p.text)
text = " ".join(pars)
text_final = text.split("Нашли опечатку?")[0].replace('\xa0',
' ').replace('\n', ' ')
return [author, title, description, date, difficult,
rubrics, text_final]