Прежде чем вы вы будете создавать собственные скраперы, помните, что случайным образом вы можете оказаться на страницах, содержащих информацию, непредназначенную для публичного использования. Будьте внимательны, когда выбираете ресурс, с которого извлекаете информацию. Если вы скачаете информацию по всем пользователям, скажем, вконтакте, то, как минимум, получите предупреждение прекратить заниматься подобными делами.
Веб-скрапинг представляет собой автоматизированняй сбор данных с различных веб-ресурсов. Безусловно, если определённый ресурс имеет свой API, удобнее воспользоваться им, потому как данные там уже удобно структурированы, но тот же твитер ограничивает количество обращений до примерно 150/час.
Именно в такие моменты на помощь и приходит веб-скрапинг. Пусть данные и имеют несколько усложнённую структуру, зато одновременно обрабатывать можно множество сайтов, да и написать собственного бота всегда приятнее.
#Устанавливаем BS
!pip install beautifulsoup4
К слову, BeautifulSoup - одна из наиболее популярных и удобных библиотек для парсинга html/xml-разметки.
import csv
import re
import sys
import warnings
from urllib.error import HTTPError
from urllib.request import urlopen
import pandas as pd
#Импортируем необходимые библиотеки
from bs4 import BeautifulSoup
warnings.filterwarnings('ignore')
#urlopen необходим для открытия веб-страниц
В качестве веб-сервиса, с которого будем скрапить данные, возьмём англоязычную википедию. А задачу сформулируем следующим образом: перейдём на страничку Стивена Кинга и итеративно будем переходить по всем ссылкам (ссылающимся только на страницы википедии), которые указывают на актёров и актрис и заполним небольшой датасет, извлекая интересную информацию.
#Считываем необходимую страницу и передаём в конструктор BS
first_page = urlopen('https://en.wikipedia.org/wiki/Stephen_King')
bs = BeautifulSoup(first_page)
На данный момент мы создали BS-объект, который содержит urlopen со множеством html-тегов и соответствующую им информацию, которая находится на заданной странице. С помощью метода get_text() можно очистить объект от всех тегов. Однако сделав это сейчас, мы потеряем важную информацию, например, что является текстом, а что - названием изображения внутри статьи.
#Информация представлена следующим образом
bs.get_text()[17000:17250]
Теперь важный момент. У нас есть объект, содержащий html-разметку, мы хотит из каждой страницы извлекать Имя и Фамилию, а так же возраст. Чтобы понять, где вся эта информация расположена, необходимо взглянуть на страницу википедии через "режим инспектирования" (перейти можно, нажав f12 в браузере).
BS имеет два метода, которые извлекают информацию по тегам: find() и findAll(). В конструктор им передаётся название интересующего тега, и словарь атрибутов, по которому необходимо извлечь объект. В нашем случае тег - это 'span', а словарь атрибутов - {'class' : 'fn'}. Различия этих методов в том, что первый остановится на первом найденном теге 'span' и соответствущих ему атрибутах, а второй пройдётся по всем на данной странице. Соответственно, первый метод возвращает просто BS-объект, а второй - BS-список, состоящий из BS-объектов. Поэтому, если есть необходимость очистить объект от тегов с помощью get_text(), но при этом до этого был использован метод findAll(), из полученного списка необходимо извлечь объект по индексу, как из простого питоновского списка.
bs.find('span', {'class' : 'fn'}).get_text()
bs.findAll('span', {'class' : 'fn'})[0].get_text()
Если методу find передать несуществующий тег, то вернётся пустой объект. Однако если попытаться дальше с ним работать, вызывая дополнительные методы, как у BS-объекта, то будет сгенерировано исключение AttributeError. Поэтому перед запуском скрапера стоит убедиться, что данное исключение перехватывается.
try:
bs.error_tag.error
except AttributeError as e:
print('caught an error')
Теперь создадим список статей актёров и актрис, на которых имеются ссылки с исследуемой страницы. Проинспектируем блок основного текста, он хранится внутри тега 'div' с атрибутами {'id' : 'bodycontent'}. Затем проинспектируем любую гиперссылку внутри этого блока, тег в этом случае: 'a', атрибуты: {'href' : 'ссылка'}. Как раз здесь и понадобятся регулярные выражения, чтобы ограничить свободу скрапера только вики. Возвращаемый BS-объект может иметь несколько атрибутов, чтобы получить список всех имеющихся, достаточно вызвать .attrs. Применительно к ссылкам, они хранятся в атрибуте с названием 'href'. Если посмотреть на список вики-ссылок, то можно заметить, что все они начинаются с /wiki/, поэтому регулярка в данном случае будет достаточно простой, за тем исключением, что некоторые ссылки могут указывать на изображения (в таких ссылках встречается двоеточие '/wiki/File:Stephen_King,_Comicon.jpg'), которые необходимо игнорировать.
links = []
for link in bs.find('div', {'id' : 'bodyContent'}).findAll('a', href = re.compile('\/wiki\/((?!:).)*$')):
links.append(link.attrs['href'])
links[:10]
Остаётся создать метод, который будет рукурсивно обрабатывать каждую ссылку, и, если, в поле Occupation встретится actor или actress, то парсить необходимую информацию.
#Сюда будем вносить посещённые ссылки, чтобы не оказаться на одной и той же странице несколько раз
links = set()
def make_scraping(Url):
global links
name = None
surname = None
birthdate = None
films = None
html = urlopen('https://en.wikipedia.org' + Url)
bs = BeautifulSoup(html)
#Если на заданной странице нет поля "Occupation", то эта страница, скорее всего, не про человека,
#и будет сгенерировано исключение AttribiteError. Просто перехватим его и пропустим страницу.
try:
#Ищем поле Occupation. Как правило, у знаменитых людей в этом поле несколько объектов,
#иногда и певец может оказаться актёром. Более того, если искомый нами "actor" или "actress" окажутся
#первыми в этом списке, то будут начинаться с заглавной буквы, поэтому приведём все объекты списка
#к нижнему регистру.
occupation = bs.find('td', {'class' : 'role'}).get_text().lower().split('\n')
if (('actor' in occupation) or
('actress' in occupation)):
#На всякий случай, если мы вдруг оказались на заготовке статьи об актёре, то некоторые поля могут
#отсутствовать, поэтому введём дополнительную обработку исключений.
try:
name_obj = bs.find('span', {'class' : 'fn'}).get_text()
name = name_obj.split(' ')[0]
surname = name_obj.split(' ')[1]
except AttributeError as e:
name = None
surname = None
try:
age = re.findall('\d+', bs.find('span', {'class' : 'ForceAgeToShow'}).get_text())[0]
except AttributeError as e:
age = None
#Укажите свой репозиторий
with open('actors.csv', 'a', newline='') as file:
writer = csv.writer(file, delimiter = ',')
writer.writerow([name, surname, age])
#Извлечём все ссылки из блока основного текста. В данном случае регулярное выражение чуть-чуть сложнее.
#Если гиперссылка ведёт, например, на фотографию, то в ней присутствует двоеточие. Просто укажем, что
#двоеточий быть не должно.
for link in bs.find('div', {'id' : 'bodyContent'}).findAll('a', href = re.compile('\/wiki\/((?!:).)*$')):
if link.attrs['href'] not in links:
links.add(link.attrs['href'])
try:
make_scraping(link.attrs['href'])
except:
print('an exception' in link.attrs['href'])
except AttributeError as e:
pass
make_scraping('/wiki/Stephen_King')
Если строить скрапер в надежде, что за достаточно длительное количество времени он соберёт необходимый объём информации, то стоит иметь в виду, что стандартные питоновские ограничения относительно рекурссии составляют 1000 вызовов. Соответственно, для изменения этого значения, необходимо импортировать билиотеку sys, а затем вызвать метод sys.setrecursionlimit(), передав в качестве аргумента необходимое количество вызовов.
В процессе скрапинга могут возникать различные ошибки. Наиболее часто встречающиеся: AttributeError и HTTPError из билиотеки urllib.error, которые мы обработали. Однако если оставлять что-то подобное на длительнео время, я бы советовал дополнительно перехватывать базовый класс Exception.
Итак, какое-то время паук поперемещался по сайтам и что-то извлекал, стоит оценить результаты его работы.
#Укажите ваш репозиторий
df = pd.read_csv('actors.csv', names = ['Name', 'Surname', 'Age'])
df.head(10)
Как видим, парсер действительно честно работал. Правда в поле Age у Alicia Keys стоит None, следует разобраться почему.
Дело в том, что возраст не находится внутри специального тега ForceAgeToShow. То есть скрапер отработал верно, он честно не нашёл тег и заменил возраст на None. Забавно, но когда я решил проверить Баста Раймса на принадлежность к актёрской профессии, то такое поле действительно указано на википедии.
В заключение. Создать собственный парсер, извлекающий информацию плоским списком, несложно. Гораздо труднее продумывать архитектуру вложенной извлекаемой информации. Так, в данном примере можно было бы дополнительно извлекать фильмы, к которым каждый рассматриваемый актёр имеет отношение. Переходя на новую ссылку со страницы актёра, если в первом абзаце присутствует слово "film", то статья в большинстве случаев о фильме (по собственным наблюдениям). Однако в данном случае труднее извлекать эти названия с n-ого уровня рекурссии.
http://pythonscraping.com/ - Ryan Mitchell - Web Scraping with Python: Collecting Data from the Modern Web. Эта книга есть и на русском языке.
https://www.crummy.com/software/BeautifulSoup/bs4/doc/ - документация по библиотеке BeautifulSoup.
https://realpython.com/python-web-scraping-practical-introduction/ - интересная ветка с примерами.