CoderCastrov logo
CoderCastrov
Питон

Парсинг данных в Интернете с использованием BeautifulSoup

Парсинг данных в Интернете с использованием BeautifulSoup
просмотров
11 мин чтение
#Питон

Используйте библиотеку BeautifulSoup на Python для помощи в честном действии систематического сбора данных без разрешения.

Будь то Kaggle, Google Cloud или федеральное правительство, в Интернете есть много надежных открытых данных. Хотя есть много причин ненавидеть наше текущее время, открытые данные - это одно из немногих спасительных качеств жизни на Земле сегодня. Но что такое противоположность "открытых" данных?

Как и любая бесплатная и легко доступная вещь, единственные данные, которые по своей сути стоят чего-то, либо собираются в частном порядке, либо крадутся из источников, которые предпочли бы, чтобы вы этого не делали. Вокруг этого можно построить модели бизнеса, как показали нам платформы социальных сетей, такие как LinkedIn, где наша личная информация покупается и продается брокерами данных. Эти компании пытались подать в суд на отдельных программистов, таких как мы, за сбор данных, которые они собрали с помощью тех же методов, и потерпели крах в суде:

https://www.forbes.com/sites/emmawoollacott/2019/09/10/linkedin-data-scraping-ruled-legal/#28997b001b54

Тема сбора данных в Интернете часто вызывает вопросы о этике и законности скрапинга, на что я отвечаю: не ограничивайте себя. Если вам не противно представление о том, что ваша жизнь будет переписана, продана и часто утечет, судебная система признала, что у вас законное право собирать данные. Название этой публикации не Люди, которые играют на безопасность и бездельники. Мы - дом для тех, кто борется, чтобы вернуть себе власть, и мы будем парсить вас до конца.


Инструменты для работы

Веб-парсинг на Python в основном осуществляется с помощью трех основных библиотек: BeautifulSoup, Scrapy и Selenium. Каждая из этих библиотек предназначена для решения разных задач, поэтому важно понять, что мы выбираем и почему.

  • BeautifulSoup является одной из самых популярных библиотек на языке Python, которая в некоторой степени сформировала веб-пространство таким, каким мы его знаем. BeautifulSoup - это легкая, легко изучаемая и очень эффективная возможность программно выделить информацию на одной веб-странице за раз. Часто используется BeautifulSoupin совокупности с библиотекой requests, где requests получает страницу, а BeautifulSoup извлекает полученные данные.
  • Scrapy имеет цель, близкую к массовому сбору данных, чем BeautifulSoup. Scrapy - это инструмент для создания веб-пауков: это абсолютные монстры, выпущенные в сеть, которые, следуя ссылкам, быстро собирают данные там, где они есть. Поскольку Scrapy служит для массового парсинга, с ним гораздо легче попасть в неприятности.
  • Selenium не является исключительно инструментом для парсинга, сколько инструментом автоматизации, который можно использовать для парсинга сайтов. Selenium - это ядерная опция для попытки программного навигации по сайтам и должна рассматриваться как таковая: есть гораздо более подходящие варианты для простого извлечения данных.

Мы будем использовать BeautifulSoup, который должен быть выбором по умолчанию для любого, пока обстоятельства не потребуют большего. BeautifulSoup более чем достаточно для сбора данных.

Подготовка к извлечению данных

Прежде чем мы начнем кражу данных, нам нужно подготовить сцену. Начнем с установки двух выбранных нами библиотек:

$ pip3 install beautifulsoup4 requests

Как уже упоминалось ранее, requests предоставит нам HTML нашей цели, а beautifulsoup4 будет разбирать эти данные.

Мы должны понимать, что многие сайты принимают меры предосторожности, чтобы отпугнуть скраперы от доступа к их данным. Первое, что мы можем сделать, чтобы обойти это, это подделать заголовки, которые мы отправляем вместе с нашими запросами, чтобы наш скрапер выглядел как легитимный браузер:

import requestsheaders = {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET',
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Max-Age': '3600',
        'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0'
    }

Это только первая линия защиты (или нападения в нашем случае). Есть множество способов, которыми сайты все равно могут удерживать нас в стороне, но установка заголовков работает удивительно хорошо для решения большинства проблем.

Теперь давайте получим страницу и проанализируем ее с помощью BeautifulSoup:

import requests
from bs4 import BeautifulSoup...
url = "[http://example.com](http://example.com)"
req = requests.get(url, headers)
soup = BeautifulSoup(req.content, 'html.parser')
print(soup.prettify())

Мы настраиваем все, сделав запрос на http://example.com. Затем мы создаем объект BeautifulSoup, который принимает необработанное содержимое этого ответа через req.content. Второй параметр, 'html.parser', это наш способ сообщить BeautifulSoup, что это HTML-документ. Для разбора других типов данных, например XML, также доступны другие парсеры, если вам это интересно.

Когда мы создаем объект BeautifulSoup из HTML страницы, наш объект содержит HTML-структуру этой страницы, которую теперь можно легко разбирать с помощью различных методов. Сначала давайте посмотрим, как выглядит наша переменная soup, используя print(soup.prettify()):

<html class="gr__example_com"><head>
    <title>Example Domain</title>
    <meta charset="utf-8">
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta property="og:site_name" content="Example dot com">
    <meta property="og:type" content="website">
    <meta property="og:title" content="Example">
    <meta property="og:description" content="An Example website.">
    <meta property="og:image" content="[http://example.com/img/image.jpg](http://example.com/img/image.jpg)">
    <meta name="twitter:title" content="Hackers and Slackers">
    <meta name="twitter:description" content="An Example website.">
    <meta name="twitter:url" content="[http://example.com/](http://example.com/)">
    <meta name="twitter:image" content="[http://example.com/img/image.jpg](http://example.com/img/image.jpg)">
</head><body data-gr-c-s-loaded="true">
  <div>
    <h1>Example Domain</h1>
      <p>This domain is established to be used for illustrative examples in documents.</p>
      <p>You may use this domain in examples without prior coordination or asking for permission.</p>
    <p><a href="[http://www.iana.org/domains/example](http://www.iana.org/domains/example)">More information...</a></p>
  </div>
</body>
</html>

Выделение HTML-элементов

У нас есть множество методов, которые позволяют нам точно определить и получить информацию, которую мы хотим извлечь из веб-страницы. Нахождение точной информации, которую мы хотим получить из веб-страницы, является своего рода искусством: эффективный парсинг требует от нас умения распознавать шаблоны в HTML-документе, которые мы можем использовать, чтобы убедиться, что мы получаем только нужные нам фрагменты. Это особенно актуально при работе с сайтами, которые активно пытаются предотвратить такие действия.

Понимание инструментов, которыми мы располагаем, - это первый шаг к развитию навыка определения возможностей. Мы начнем с основ.

Использование find() и find_all()

Самый простой способ найти информацию в нашей переменной soup - это использовать soup.find(...) или soup.find_all(...). Эти два метода работают одинаково, за исключением одного: find возвращает первый найденный HTML-элемент, в то время как find_all возвращает список всех элементов, соответствующих заданным критериям (даже если найден только один элемент, find_all вернет список из одного элемента).

Мы можем искать элементы DOM в нашей переменной soup, указывая определенные критерии. Передача позиционного аргумента в find_all вернет все якорные теги на сайте:

soup.find_all("a")
# <a href="[http://example.com/elsie](http://example.com/elsie)" class="boy" id="link1">Elsie</a>
# <a href="[http://example.com/lacie](http://example.com/lacie)" class="boy" id="link2">Lacie</a> 
# <a href="[http://example.com/tillie](http://example.com/tillie)" class="girl" id="link3">Tillie</a>

Мы также можем найти все якорные теги с классом "boy". Передача аргумента class_ позволяет фильтровать по имени класса. Обратите внимание на подчеркивание!

soup.find_all("a", class_="boy")
# <a href="[http://example.com/elsie](http://example.com/elsie)" class="boy" id="link1">Elsie</a>
# <a href="[http://example.com/lacie](http://example.com/lacie)" class="boy" id="link2">Lacie</a>

Если мы хотим получить любой элемент с классом "boy" помимо якорных тегов, мы также можем сделать это:

soup.find_all(class_="boy")
# <a href="[http://example.com/elsie](http://example.com/elsie)" class="boy" id="link1">Elsie</a>
# <a href="[http://example.com/lacie](http://example.com/lacie)" class="boy" id="link2">Lacie</a>

Мы можем искать элементы по идентификатору так же, как мы искали по классам. Помните, что мы должны ожидать только одного элемента с идентификатором, поэтому здесь мы должны использовать find:

soup.find("a", id="link1")
# <a href="[http://example.com/elsie](http://example.com/elsie)" class="boy" id="link1">Elsie</a>

Часто мы сталкиваемся с ситуациями, когда элементы не имеют надежных значений класса или идентификатора. К счастью, мы можем искать элементы DOM с любым атрибутом, включая нестандартные:

soup.find_all(attrs={"data-args": "bologna"})

Селекторы CSS

Поиск HTML с использованием селекторов CSS является одним из самых мощных способов найти то, что вам нужно, особенно для сайтов, которые ersuchen erschweren Ihr Leben. Использование селекторов CSS позволяет нам находить и использовать очень специфичные шаблоны в структуре DOM цели. Это лучший способ убедиться, что мы получаем точно тот контент, который нам нужен. Если вы забыли, как работают селекторы CSS, я настоятельно рекомендую вам освежить свои знания. Вот несколько примеров:

soup.select(".widget.author p")

В этом примере мы ищем элемент, у которого есть класс "widget" и класс "author". После того, как мы получили этот элемент, мы идем глубже, чтобы найти все теги абзаца, находящиеся внутри этого виджета. Мы также можем изменить это, чтобы получить только второй тег абзаца внутри виджета автора:

soup.select(".widget.author p:nth-of-type(2)")

Чтобы понять, почему это так мощно, представьте себе сайт, который намеренно не имеет идентифицирующих атрибутов на своих тегах, чтобы помешать людям, подобным вам, парсить их данные. Даже без имен для выбора, мы можем изучить структуру DOM страницы и найти уникальный способ навигации к нужному элементу:

soup.select("body > div:first-of-type > div > ul li")

Такой конкретный шаблон, вероятно, уникален только для одной коллекции тегов <li> на странице, которую мы эксплуатируем. Недостатком этого метода является то, что мы зависим от владельца сайта, так как структура их HTML может измениться.

Получение некоторых атрибутов

Вероятно, нам всегда понадобится содержимое или атрибуты тега, а не весь HTML-код тега. Если мы парсим теги <a>, то, вероятно, нам нужно только значение href, а не весь тег. Здесь можно использовать метод .get, чтобы получить значения атрибутов тега:

soup.find_all('a').get('href')

В приведенном выше примере находятся целевые URL-адреса для всех тегов <a> на странице. Еще один пример может заключаться в том, чтобы получить логотип сайта:

soup.find(id="logo").get('src')

Иногда нам не нужны атрибуты, а только текст внутри тега:

soup.find('p').get_text()

Неприятные теги, с которыми нужно справиться

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

soup.find("meta", property="og:description").get('content')

Какая уродливая строка. Мета-теги - особенно интересный случай; все они бесполезно называются "meta", поэтому нам нужен второй идентификатор (в дополнение к имени тега), чтобы указать, какой мета-тег нас интересует. Только после этого мы можем заботиться о получении фактического содержимого этого тега.

Понимание того, что что-то всегда может сломаться

Если мы попытаемся использовать вышеуказанный селектор на HTML-странице, которая не содержит og:description, наш скрипт неумолимо сломается. Мы не только упустим эти данные, но и полностью упустим все остальное - это означает, что нам всегда нужно предусмотреть план Б и, по крайней мере, справиться с отсутствием тега вообще.

Лучше разбить эту логику на отдельные теги. Во-первых, давайте рассмотрим пример базового парсера со всеми имеющимися у нас знаниями:

import requests
from bs4 import BeautifulSoup

def scrape_page_metadata(url):
    """Парсит метаданные целевого URL."""
    headers = {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET',
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Max-Age': '3600',
        'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0'
    }
    pp = pprint.PrettyPrinter(indent=4)
    r = requests.get(url, headers=headers)
    html = BeautifulSoup(r.content, 'html.parser')
    metadata = {
        'title': get_title(html),
        'description': get_description(html),
        'image': get_image(html),
        'favicon': get_favicon(html, url),
        'sitename': get_site_name(html, url),
        'color': get_theme_color(html),
        'url': url
    }
    pp.pprint(metadata)
    return metadata

Эта функция является основой для получения метаданных заданного URL. Результат, который мы ищем, - это словарь с именем metadata, который содержит данные, которые мы смогли успешно извлечь.

Каждому ключу в нашем словаре соответствует соответствующая функция, которая пытается извлечь соответствующую информацию. Вот что у нас есть для получения значений title, description и social image страницы:

def get_title(html):
    """Парсит заголовок страницы."""
    title = None
    if html.title.string:
        title = html.title.string
    elif html.find("meta", property="og:title"):
        title = html.find("meta", property="og:title").get('content')
    elif html.find("meta", property="twitter:title"):
        title = html.find("meta", property="twitter:title").get('content')
    elif html.find("h1"):
        title = html.find("h1").string
    return title

def get_description(html):
    """Парсит описание страницы."""
    description = None
    if html.find("meta", property="description"):
        description = html.find("meta", property="description").get('content')
    elif html.find("meta", property="og:description"):
        description = html.find("meta", property="og:description").get('content')
    elif html.find("meta", property="twitter:description"):
        description = html.find("meta", property="twitter:description").get('content')
    elif html.find("p"):
        description = html.find("p").contents
    return description

def get_image(html):
    """Парсит изображение для шаринга."""
    image = None
    if html.find("meta", property="image"):
        image = html.find("meta", property="image").get('content')
    elif html.find("meta", property="og:image"):
        image = html.find("meta", property="og:image").get('content')
    elif html.find("meta", property="twitter:image"):
        image = html.find("meta", property="twitter:image").get('content')
    elif html.find("img", src=True):
        image = html.find_all("img").get('src')
    return image
  • get_title пытается получить тег <title>, который имеет очень маленькую вероятность неудачи. В случае, если целевая страница действительно не содержит этот тег, мы прибегаем к метатегам Facebook и Twitter. Если все это все равно не удается, мы наконец пытаемся получить первый тег <h1> на странице (если мы достигли этой точки, мы, вероятно, парсим некачественный сайт).
  • get_description практически идентичен нашему методу для парсинга заголовков страниц. Последней попыткой является отчаянная попытка получить первый абзац на странице.
  • get_image ищет "шаринговое" изображение страницы, которое используется для создания превью ссылок на социальных медиа платформах. Нашей последней попыткой является получение первого тега <img>, содержащего исходное изображение.
# Что мы построили?

Этот простой скрипт, который мы только что собрали, является основой для того, как большинство сервисов генерируют "предварительные просмотры ссылок": встроенный виджет, содержащий краткое описание сайта перед переходом по ссылке (думайте о Facebook, Slack, Discord и т. д.). Даже есть некоторые сервисы, которые взимают ежемесячную плату в размере около 10 долларов за предоставление такой услуги, которую мы только что построили. Вместо того, чтобы платить за что-то подобное, не стесняйтесь взять мой исходный код и использовать его по своему усмотрению:

```python
"""Парсинг метаданных с целевого URL."""
import requests
from bs4 import BeautifulSoup
import pprint

def scrape_page_metadata(url):
    """Парсинг метаданных с целевого URL."""
    headers = {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET',
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Max-Age': '3600',
        'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0'
    }
    pp = pprint.PrettyPrinter(indent=4)
    r = requests.get(url, headers=headers)
    html = BeautifulSoup(r.content, 'html.parser')
    metadata = {
        'title': get_title(html),
        'description': get_description(html),
        'image': get_image(html),
        'favicon': get_favicon(html, url),
        'sitename': get_site_name(html, url),
        'color': get_theme_color(html),
        'url': url
        }
    pp.pprint(metadata)
    return metadata

def get_title(html):
    """Парсинг заголовка страницы."""
    title = None
    if html.title.string:
        title = html.title.string
    elif html.find("meta", property="og:title"):
        title = html.find("meta", property="og:title").get('content')
    elif html.find("meta", property="twitter:title"):
        title = html.find("meta", property="twitter:title").get('content')
    elif html.find("h1"):
        title = html.find("h1").string
    return title

def get_description(html):
    """Парсинг описания страницы."""
    description = None
    if html.find("meta", property="description"):
        description = html.find("meta", property="description").get('content')
    elif html.find("meta", property="og:description"):
        description = html.find("meta", property="og:description").get('content')
    elif html.find("meta", property="twitter:description"):
        description = html.find("meta", property="twitter:description").get('content')
    elif html.find("p"):
        description = html.find("p").contents
    return description

def get_image(html):
    """Парсинг изображения для предварительного просмотра."""
    image = None
    if html.find("meta", property="image"):
        image = html.find("meta", property="image").get('content')
    elif html.find("meta", property="og:image"):
        image = html.find("meta", property="og:image").get('content')
    elif html.find("meta", property="twitter:image"):
        image = html.find("meta", property="twitter:image").get('content')
    elif html.find("img", src=True):
        image = html.find_all("img").get('src')
    return image

def get_site_name(html, url):
    """Парсинг названия сайта."""
    if html.find("meta", property="og:site_name"):
        sitename = html.find("meta", property="og:site_name").get('content')
    elif html.find("meta", property='twitter:title'):
        sitename = html.find("meta", property="twitter:title").get('content')
    else:
        sitename = url.split('//')[1]
        return sitename.split('/')[0].rsplit('.')[1].capitalize()
    return sitename

def get_favicon(html, url):
    """Парсинг фавикона."""
    if html.find("link", attrs={"rel": "icon"}):
        favicon = html.find("link", attrs={"rel": "icon"}).get('href')
    elif html.find("link", attrs={"rel": "shortcut icon"}):
        favicon = html.find("link", attrs={"rel": "shortcut icon"}).get('href')
    else:
        favicon = f'{url.rstrip("/")}/favicon.ico'
    return favicon

def get_theme_color(html):
    """Парсинг цвета бренда."""
    if html.find("meta", property="theme-color"):
        color = html.find(
            "meta",
            property="theme-color"
        ).get('content')
        return color
    return None

Я загрузил исходный код для этого учебника на Github, который содержит инструкции о том, как скачать и запустить этот скрипт самостоятельно. Наслаждайтесь и присоединяйтесь к нам в следующий раз, когда мы повысим ставки с более коварными тактиками парсинга!

https://github.com/hackersandslackers/beautifulsoup-tutorial


Опубликовано на hackersandslackers.com 11 ноября 2018 года.