Важный момент для аналитиков данных — где взять данные и максимально дешево, поскольку для обучения нейронных сетей их нужно много. Те, кто эти данные имеет, копит и улучшает расположены к тому, чтобы продать их подороже. При анализе данных нет уверенности, что работа с данными окупится — это инвестиции. Вполне здоровое желательно вложится по-минимуму и получить побольше. 🙂
Новостные статьи с Bloomberg — значительный объем текстовой информации различных тематик, интересный для анализа. Как получить их, учитывая, что для новостного агенства доступ к этой информации — их заработок и они тщательно оберегают, чтобы нахаляву получить было бы нельзя.
Код в Colab доступен по ссылке. Сразу отмечу, это экспериментальный код, там может быть много лишнего и я с ним играюсь по мере необходимости, так что он меняется.
Парсинг Google ссылок
Первый момент — как получить ссылки на новостные статьи? На сайте Bloomberg нет какого-то общего списка новостных статей. В день их выходит тысячи. Изучение файла https://www.bloomberg.com/robots.txt, чтобы получить sitemap.xml результата не дал. Описаний новостной части сайта нет. Воспользуемся поисковиком Google для получения ссылок на новостные статьи.
В материале Current Google Search Packages using Python3.7: A Simple Tutorial дано подробное описание возможных вариантов, как получить ссылки. Я остановлюсь на двух бесплатных вариантах, хотя придерживаюсь мнения автора, что надежнее воспользоваться легальным подходом через Google API.
Импортируем необходимые для работы библиотеки:
#@title Код импорта библиотек # to hide output of this cell #%%capture !pip install selenium !apt-get update # to update ubuntu to correctly run apt install !apt install chromium-chromedriver !cp /usr/lib/chromium-browser/chromedriver /usr/bin import sys sys.path.insert(0,'/usr/lib/chromium-browser/chromedriver') from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.keys import Keys import pandas as pd import numpy as np from bs4 import BeautifulSoup #import chromedriver_binary import string import time from datetime import datetime from google.colab import files import re from json import loads !pip install fake-useragent from fake_useragent import UserAgent pd.options.display.float_format = '{:.0f}'.format
Подгружаем драйвер браузера Chrome:
#@title Код загрузки драйвера Chrome def loadChrome(): chrome_options = webdriver.ChromeOptions() chrome_options.add_argument('--headless') chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('--disable-dev-shm-usage') ua = UserAgent() user_agent = "user-agent=" + ua.random print("User-agent:", user_agent) chrome_options.add_argument(user_agent) browser = webdriver.Chrome('chromedriver', chrome_options = chrome_options) return browser
Если в паре с Selenium симпатичнее использовать Firefox, то здесь пример работы.
Загружаем браузер: browser = loadChrome()
Библиотека Google
- Бесплатная.
- Развивается с 2016 года.
- Последний релиз был в декабре 2019 (сейчас июнь 2020).
- Нужно минимум 2 сек. задержка, чтобы Google не блочил запросы.
- В выдаче максимум 100 линков, поскольку при первой загрузке Google в pagination отдает только 10 страниц. Библиотека не может проходить по ссылкам, чтобы подгружать новые страницы после 10-й. Нужно вручную это делать.
!pip install google try: from googlesearch import search except ImportError: print("No module named 'google' found")
Параметры вызова метода для получения ссылок:
search(query, tld=’com’, lang=’en’, num=10, start=0, stop=None, pause=2.0)
- query : query string that we want to search for.
- tld : tld stands for top level domain which means we want to search our result on google.com or google.in or some other domain.
- lang : lang stands for language.
- num : Number of results we want.
- start : First result to retrieve.
- stop : Last result to retrieve. Use None to keep searching forever.
- pause : Lapse to wait between HTTP requests. Lapse too short may cause Google to block your IP. Keeping significant lapse will make your program slow but its safe and better option.
- Return : Generator (iterator) that yields found URLs. If the stop parameter is None the iterator will loop forever.
Задаем исходные параметры для
#@markdown ### Дата статей articles_date = '2020-06-03' #@param {type:"date"} #@markdown --- #@markdown ### Количество статей: num_of_articles = 5000 #@param {type:"integer"} #@markdown ### Максимальное количество (0 - бесконечно): stop = 0 #@param {type:"integer"} #@markdown ### Задержка: delay = 5 #@param {type:"slider", min:2, max:10, step:1}
Google выдает результаты максимум на 10 страницах. Эта библиотека парсит только эти 10 страниц, не пытаясь переходить дальше. Хотя количество найденных линков отображается на первой странице Google И эту информацию можно легко использовать, чтобы собрать все ссылки.
Получаем ссылки:
query = "site:bloomberg.com/news/articles/" + articles_date + "/*" if stop == 0: stop = None print("Запросил:", num_of_articles, "ссылок.") print("На дату:", articles_date) links = [] for j in search(query, tld="com", num = num_of_articles, stop = stop, pause = delay): #print(j) links.append(j) print("Получено ссылок:", len(links)) a = [print(str(index + 1) + ":", link) for index, link in enumerate(links) if (link != None) and (link.strip() != '')]
На выходе ровно 10 ссылок на страницу * 10 страниц = 100 ссылок.
Запросил: 5000 ссылок. На дату: 2020-06-03 Получено ссылок: 100 1: https://www.bloomberg.com/news/articles/2020-06-03/campaign-update 2: https://www.bloomberg.com/news/articles/2020-06-03/-opec 3: https://www.bloomberg.com/news/articles/2020-06-03/coronavirus-is-disproportionately-impacting-women ....
Отправка Google запроса через Selenium (Chrome)
Для получения страницы Google с результатами поискового запроса используется вот такой вариант, максимально имитирующий работу живого человека.
#@title Получаем web страницу def getGoogleResultPage(browser, query): print(query) browser.get('http://www.google.com/') #?q=' + request) search = browser.find_element_by_name('q') search.send_keys(query) search.send_keys(Keys.RETURN) # hit return after you enter search text #time.sleep(5) # sleep for 5 seconds so you can see the results #browser.quit() return browser.page_source
В приницпе, это избыточно, поскольку Google нормально воспринимает POST запрос в строке вида: https://www.google.com/search?q=site%3Abloomberg.com%2Fnews%2Farticles%2F2020-06-03%2F*
browser = loadChrome() query = "site:bloomberg.com/news/articles/" + articles_date + "/*" html_source = getGoogleResultPage(browser, query)
Теперь распарсим полученный html c помощью BeautifulSoup.
def getGoogleSearch(soap): google_links = [entry for entry in soup.find_all(class_='g')] data = [] for links in google_links: header = links.find_all('h3') link = links.find_all('a') data.append([header[0].text, link[0]['href']]) return data
soup = BeautifulSoup(html_source, 'lxml') data = getGoogleSearch(html_source) b = [print(d[0], "\t", d[1]) for d in data]
Получаем 10 результатов выдачи на странице:
2020 Presidential Election: Live Updates, News ... - Bloomberg https://www.bloomberg.com/news/articles/2020-06-03/campaign-update OPEC+ Unity Shaken as Iraq Pushed to Atone for Oil Cheating ... https://www.bloomberg.com/news/articles/2020-06-03/-opec Coronavirus Is Disproportionately Impacting Women ... https://www.bloomberg.com/news/articles/2020-06-03/coronavirus-is-disproportionately-impacting-women 2020 Presidential Election: Live Updates, News ... - Bloomberg https://www.bloomberg.com/news/articles/2020-06-03/campaign-update?srnd=markets-vp ...
Плюс внизу есть footer с pagination. Из него надо взять ссылки на остальные страницы:
#Функция для получения ссылок на страницы из footer-а def getPaginationLinks(soup): foot_links = [entry for entry in soup.find_all('div', {'id': 'foot', 'role' : "navigation"})] page_links = [] if (foot_links != None): f1_links = foot_links[0].find_all('a', {'class' : 'fl'}) for f1 in f1_links: page_links.append([f1['aria-label'].replace("Page ", ""), f1['href']]) return page_links
page_links = getPaginationLinks(soup) b = [print(d[0], "\t", d[1]) for d in page_links]
9 страниц результатов:
2 /search?q=site:bloomberg.com/news/articles/2020-06-03/*&ei=DRfZXrXsKrLWmAWTibH4AQ&start=10&sa=N&ved=2ahUKEwj1ppPAwOjpAhUyK6YKHZNEDB8Q8tMDegQICxAs 3 /search?q=site:bloomberg.com/news/articles/2020-06-03/*&ei=DRfZXrXsKrLWmAWTibH4AQ&start=20&sa=N&ved=2ahUKEwj1ppPAwOjpAhUyK6YKHZNEDB8Q8tMDegQICxAu 4 /search?q=site:bloomberg.com/news/articles/2020-06-03/*&ei=DRfZXrXsKrLWmAWTibH4AQ&start=30&sa=N&ved=2ahUKEwj1ppPAwOjpAhUyK6YKHZNEDB8Q8tMDegQICxAw ....
Теперь вопрос, Google сообщил, что найдено порядка 2000 страниц, а отобразилось только 10. Где остальные? А они появляются только если кликнуть на последние страницы. Можно получить все ссылки последовательно распарсивая страниц поисковой выдачи и получая pagination. Ну или можно просто воспользоваться функцией:
#Получаем новую страницу с результатами поиска #last_link - массив из предыдущего запроса вида [номер страницы, ссылка] #page_num - номер страницы #Функция возвращает новую ссылку для получения списка линков def getNewSearchPage(last_link, page_num = 0): start = "start=" + str((int(last_link[0]) - 1) * 10) return [page_num, "http://www.google.com" + last_link[1].replace(start, "start=" + str((page_num - 1) * 10))]
В качестве аргумента передается ссылка полученная на предыдущем шаге и номер страницы с результатами поиска:
link = getNewSearchPage(page_links[0], 12) link
На выходе получаем новую страницу для парсинга ссылок с результатами поискового запроса:
[12, 'http://www.google.com/search?q=site:bloomberg.com/news/articles/2020-06-03/*&ei=DRfZXrXsKrLWmAWTibH4AQ&start=110&sa=N&ved=2ahUKEwj1ppPAwOjpAhUyK6YKHZNEDB8Q8tMDegQICxAs']
Парсинг новостей Bloomberg
У нас есть ссылки на страницы Bloomberg, попробуем забрать новости с сайта. Как уже говорил ранее, Bloomberg не склонен отдавать забесплатно тексты статей, однако, Selenium достаточно хорошо эмулирует работу реального пользователя в браузере, поэтому Bloomberg отдает страницу. Естетственно, для работы в продакшене нужно использовать прокси для подмены IP адресов, чтобы минимизировать риск попадания на страницу с Captcha.
#Download any web page def getWebPage(browser, url): print(url) browser.get(url) return browser.page_source
Получаем новостную страницу Bloomberg, используя Selenium драйвер браузера — browser и передаем линк на новости полученные после парсинга Google запросов:
news = getWebPage(browser, data[1][1]) print("Размер HTML страницы:", len(news)) https://www.bloomberg.com/news/articles/2020-06-03/-opec Размер HTML страницы: 852278
Полученную страницу с новостями парсим BeautifulSoap-ом и преобразуем в строку текст, выкинув всю HTML разметку:
#Parse a news page from Bloomberg def parseBloombergNewsPage(soup): bloomberg_news = soup.find_all('div', {'class' : 'body-columns'}) text = "" if (bloomberg_news != None and len(bloomberg_news) > 0): paragraphs = bloomberg_news[0].find_all('p') for paragraph in paragraphs: text += " " + paragraph.text return text.strip()
Запускаем парсинг выбранной скачанной страницы Bloomberg:
news_soup = BeautifulSoup(news, 'lxml') text = parseBloombergNewsPage(news_soup)
Чтобы скачать с Colab полученный текст на локальный ПК:
#Download file def downloadText(text, filename = ""): if filename == "": filename = 'bloomberg.html' with open(filename, 'w') as f: f.write(text) files.download(filename)
Скачиваем текст, вызвав эту функцию:
downloadText(text)