#!/usr/bin/env python # coding: utf-8 # # Основы программирования в Python # # *Алла Тамбовцева, НИУ ВШЭ* # ## Web-scraping # # Мы уже немного познакомились со структурой html-файлов, теперь попробуем выгрузить информацию из реальной страницы, а точнее, с реального сайта [nplus1.ru](https://nplus1.ru/). # # **Наша задача:** выгрузить недавние новости в датафрейм `pandas`, чтобы потом сохранить все в csv-файл. # # Сначала сгрузим весь html-код страницы и сохраним его в отдельную переменную. Для этого нам понадобится библиотека `requests`. Импортируем её: # In[3]: import requests # Сохраним ссылку на главную страницу сайта в переменную `url` для удобства и выгрузим страницу. (Разумеется, это будет работать при подключении к интернету. Если соединение будет отключено, Python выдаст `NewConnectionError`). # In[4]: url = 'https://nplus1.ru/' # сохраняем page = requests.get(url) # загружаем страницу по ссылке # Если мы просто посмотрим на объект, мы ничего особенного не увидим: # In[5]: page # response 200 - страница загружена # Импортируем функцию `BeautifulSoup` из библиотеки `bs4` (от *beautifulsoup4*) и заберём со страницы `page` код html в виде текста. # In[8]: from bs4 import BeautifulSoup # не спрашивайте, почему BeautifulSoup # In[9]: soup = BeautifulSoup(page.text, 'lxml') # Если выведем `soup` на экран, мы увидим то же самое, что в режиме разработчика или в режиме происмотра исходного кода (`view-source` через *Ctrl+U* в Google Chrome). # In[ ]: soup # Для просмотра выглядит не очень удобно.  «Причешем» наш `soup` – воспользуемся методом `.prettify()` в сочетании с функцией `print()`. # In[ ]: print(soup.prettify()) # В такой выдаче ориентироваться гораздо удобнее (но при желании, то же можно увидеть в браузере, на большом экране). # Чтобы сгрузить все новости с главной страницы сайта, нужно собрать все ссылки на страницы с этими новостями. Ссылки в html-файле всегда заключены в тэг `` и имеют атрибут `href`. Посмотрим на кусочки кода, соответствующие всем ссылкам на главной странице сайта: # In[ ]: for link in soup.find_all('a'): print(link.get('href')) # Ссылок много. Но нам нужны только новости – ссылки, которые начинаются со слова `/news`. Добавим условие: будем выбирать только те ссылки, в которых есть `/news`. Создадим пустой список `urls` и будем добавлять в него только ссылки, которые удовлетворяют этому условию. # In[10]: urls = [] for link in soup.find_all('a'): if '/news' in link.get('href'): urls.append(link.get('href')) urls # Ссылки, которые у нас есть в списке `urls`, относительные: они неполные, начало ссылки (название сайта) отсутствует. Давайте превратим их в абсолютные ‒ склеим с ссылкой https://nplus1.ru. # In[11]: full_urls = [] for u in urls: res = 'https://nplus1.ru' + u full_urls.append(res) full_urls # Теперь наша задача сводится к следующему: изучить одну страницу с новостью, научиться из нее вытаскивать текст и всю необходимую информацию, а потом применить весь набор действий к каждой ссылке из `full_urls` в цикле. Посмотрим на новость с индексом 1, у вас может быть другая, новости обновляются. # In[29]: url0 = full_urls[1] page0 = requests.get(url0) soup0 = BeautifulSoup(page0.text, 'lxml') # В коде каждой страницы с новостью есть часть с мета-информацией: датой, именем автора и проч. Такая информация окружена тэгом ``. Посмотрим: # In[30]: soup0.find_all('meta') # Из этого списка нам нужны части с именем автора, датой, заголовком и кратким описанием. Воспользуемся поиском по атрибуту `name`. Передадим функции `find_all()` в качестве аргумента словарь с названием и значением атрибута: # In[31]: soup0.find_all('meta', {'name' : 'author'}) # например, автор # Теперь выберем единственный элемент полученного списка (с индексом 0): # In[32]: soup0.find_all('meta', {'name' : 'author'})[0] # Нам нужно вытащить из этого объекта `content` – имя автора. Посмотрим на атрибуты: # In[33]: soup0.find_all('meta', {'name' : 'author'})[0].attrs # Как получить отсюда `content`? Очень просто, ведь это словарь! А доставать из словаря значение по ключу мы умеем. # In[34]: author = soup0.find_all('meta', {'name' : 'author'})[0].attrs['content'] author # Аналогичным образом извлечем дату, заголовок и описание. # In[35]: date = soup0.find_all('meta', {'itemprop' : 'datePublished'})[0].attrs['content'] title = soup0.find_all('meta', {'property' : 'og:title'})[0].attrs['content'] description = soup0.find_all('meta', {'name' : 'description'})[0].attrs['content'] # Осталось вытащить рубрики и сложность текста. Если мы посмотрим на исходный код страницы, мы увидим, что нужная нам информация находится в тэгах `

`: # In[36]: soup0.find_all('p') # Выберем из полученного списка первый элемент и найдем в нем все тэги ``: # In[37]: soup0.find_all('p')[0].find_all('a') # Получился список из двух элементов. Применим списковые включения – вытащим из каждого элемента текст и поместим его в новый список `rubrics`. # In[38]: rubrics = [r.text for r in soup0.find_all('p')[0].find_all('a')] rubrics # Осталась только сложность. Возьмем соответствующий кусок кода: # In[39]: soup0.find_all('span', {'class' : 'difficult-value'}) # И выберем оттуда текст. # In[40]: diff = soup0.find_all('span', {'class' : 'difficult-value'})[0].text diff # Теперь перейдем к тексту самой новости. Как можно заметить, текст сохранен в абзацах `

`, причем безо всяких атрибутов. Сообщим Python, что нас интересуют куски с пустым атрибутом `class`: # In[41]: text_list = soup0.find_all('p', {'class' : None}) # «Выцепим» все тексты (без тэгов) из полученного списка: # In[42]: text = [t.text for t in text_list] # Склеим все элементы списка `text` через пробел: # In[43]: final_text = ' '.join(text) final_text # Все здорово, только мешают отступы-переходы на новую строку `\n`. Заменим их на пробелы с помощью метода `.replace`: # In[44]: final_text = final_text.replace('\n', ' ') # Не прошло и двух пар, как мы разобрались со всем :) Теперь осталось совсем чуть-чуть. Написать готовую функцию для всех проделанных нами действий и применить ее в цикле для всех ссылок в списке `full_urls`. Напишем! Аргументом функции будет ссылка на новость, а возвращать она будет текст новости и всю необходимую информацию (дата, автор, сложность и проч.). Скопируем все строки кода выше. # In[47]: def GetNews(url0): """ Returns a tuple with url0, date, author, description, title, final_text, rubrics, diff. Parameters: url0 is a link to the news (string) """ page0 = requests.get(url0) soup0 = BeautifulSoup(page0.text, 'lxml') author = soup0.find_all('meta', {'name' : 'author'})[0].attrs['content'] date = soup0.find_all('meta', {'itemprop' : 'datePublished'})[0].attrs['content'] title = soup0.find_all('meta', {'property' : 'og:title'})[0].attrs['content'] description = soup0.find_all('meta', {'name' : 'description'})[0].attrs['content'] rubrics = [r.text for r in soup0.find_all('p')[0].find_all('a')] diff = soup0.find_all('span', {'class' : 'difficult-value'})[0].text text_list = soup0.find_all('p', {'class' : None}) text = [t.text for t in text_list] final_text = ' '.join(text) final_text = final_text.replace('\n', ' ') return url0, date, author, description, title, final_text, rubrics, diff # Уфф. Осталось применить ее в цикле. Но давайте не будем спешить: импортируем функцию `sleep` для задержки, чтобы на каждой итерации цикла, прежде чем перейти к следующей новости, Python ждал несколько секунд. Во-первых, это нужно, чтобы сайт «не понял», чтобы мы его грабим, да еще автоматически. Во-вторых, с небольшой задержкой всегда есть гарантия, что страница прогрузится (сейчас это пока не очень важно, но особенно актуально будет, когда будем обсуждать встраивание в браузер с Selenium). Приступим. # In[45]: from time import sleep # In[48]: news = [] # это будет список из кортежей, в которых будут храниться данные по каждой новости for link in full_urls: res = GetNews(link) news.append(res) sleep(3) # задержка в 3 секунды # Так теперь выглядит первый элемент списка: # In[49]: news[0] # Импортируем `pandas` и создадим датафрейм из списка кортежей: # In[50]: import pandas as pd # In[51]: df = pd.DataFrame(news) # In[52]: df.head(2) # Переименуем столбцы в базе. # In[53]: df.columns = ['link', 'date', 'author', 'desc', 'title', 'text', 'rubric', 'diffc'] # In[54]: df.head(2) # Теперь внесем изменения: сделаем столбец `diffc` числовым – типа *float*. # In[56]: df['diffc'] = [float(i) for i in df.diffc] # Теперь сложность представлена в базе как количественный показатель, и описывать ее можно соответствующим образом: # In[57]: df.diffc.describe() # Теперь столбец со сложностью точно числовой. Можем даже построить для него гистограмму. # In[58]: get_ipython().run_line_magic('matplotlib', 'inline') df.diffc.plot.hist() # Объединим рубрики в *text* в одну строку через запятую: # In[59]: df['rubric'] = [','.join(r) for r in df.rubric] # Давайте почистим текст новостей – уберем оттуда текст, не относящийся к новостям. Найдем лишнее: # In[60]: df.text[0] # Лишний текст находится после фразы 'Нашли опечатку?'. Так давайте будем разбивать строки по этой фразе с помощью метода `.split()` и брать все, что до нее (элемент с индексом 0). # In[62]: df['clean_text'] = [t.split('Нашли опечатку?')[0] for t in df.text] # Осталось только заменить непонятные символы `\xa0` на пробелы: # In[63]: df['clean_text'] = [t.replace("\xa0", " ") for t in df.clean_text] # In[64]: df.clean_text[0] # Всё! Сохраняем датафрейм в файл. Для разнообразия сохраним в Excel: # In[65]: df.to_excel('nplus-news.xlsx')