Scraping (парсинг) текстов статей с Bloomberg на Python в Google Colab

Важный момент для аналитиков данных — где взять данные и максимально дешево, поскольку для обучения нейронных сетей их нужно много. Те, кто эти данные имеет, копит и улучшает расположены к тому, чтобы продать их подороже. При анализе данных нет уверенности, что работа с данными окупится — это инвестиции. Вполне здоровое желательно вложится по-минимуму и получить побольше. 🙂

Новостные статьи с 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

Адрес библиотеки на PyPi.org

  • Бесплатная.
  • Развивается с 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)

Полезные ссылки

Spread the love
Запись опубликована в рубрике IT рецепты. Добавьте в закладки постоянную ссылку.

Обсуждение закрыто.