Практическое введение в Web Scraping в Python

1 июня, 2020

Подписывайся на наш канал в Telegram, чтобы ежедневно совершенствоваться в Python - перейти

Оглавление

  • Настройка вашего Python Web Scraper
  • Создание веб-запросов
  • Спор HTML с BeautifulSoup
  • Использование BeautifulSoup для получения имен математиков
  • Получение рейтинга популярности
  • Собираем все вместе
  • Заключение и следующие шаги

Что такое веб-cкрапинг?

Представьте себе, что однажды, совершенно неожиданно, вы думаете: «Ну и дела, интересно, кто пять самых популярных математиков?»

Вы немного задумаетесь , и у вас появится идея использовать XTools из Википедии, чтобы измерить популярность математика, приравнивая популярность к просмотрам страниц. Например, посмотрите на страницу Анри Пуанкаре . Там вы можете видеть, что количество просмотров страниц Пуанкаре за последние 60 дней по состоянию на декабрь 2017 года составляет около 32 000.

Далее вы пойдете писать в Google «известные математики» и найдете этот ресурс, который перечисляет 100 имен. Теперь у вас есть страница со списком имен математиков, а также веб-сайт, который предоставляет информацию о том, насколько «популярен» этот математик. Что теперь?

Именно здесь вступают Python и веб-скрапинг. Веб-срапинг – это загрузка структурированных данных из Интернета, выбор некоторых из этих данных и передача того, что вы выбрали, другому процессу.

В этом уроке вы будете писать программу на Python, которая загружает список из 100 математиков и их страниц XTools, выбирает данные об их популярности и в конце рассказывает нам о 5 самых популярных математиках всех времен! Давайте начнем.

Важное замечание: Мы получили электронное письмо от сопровождающего XTools, в котором сообщается, что очистка XTools является вредной и что вместо этого следует использовать API автоматизации:

Эта статья на вашем сайте, по сути, является руководством по очистке XTools […]. В этом нет необходимости, и это вызывает у нас проблемы. У нас есть API, которые должны использоваться для автоматизации , и, кроме того, для просмотра страниц, особенно люди должны использовать официальный API просмотра страниц .

XTools

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

Настройка вашего Python Web Scraper

В этом руководстве вы будете использовать Python 3 и виртуальные среды Python. 

Вам нужно будет установить только эти два пакета:

  • requests на выполнение ваших HTTP-запросов
  • BeautifulSoup4 для обработки всей вашей обработки HTML

Давайте установим эти зависимости с помощью pip:

$ pip install requests BeautifulSoup4

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

from requests import get
from requests.exceptions import RequestException
from contextlib import closing
from bs4 import BeautifulSoup

Создание веб-запросов

Ваша первая задача будет загружать веб-страницы. Пакет requestsприходит на помощь. Он призван стать простым в использовании инструментом для выполнения всех задач HTTP в Python, и он не разочаровывает. В этом уроке вам понадобится только функция requests.get(), но вы обязательно должны ознакомиться с полной документацией, когда захотите пойти дальше.

Во-первых, вот ваша функция:

def simple_get(url):
    """
    Attempts to get the content at `url` by making an HTTP GET request.
    If the content-type of response is some kind of HTML/XML, return the
    text content, otherwise return None.
    """
    try:
        with closing(get(url, stream=True)) as resp:
            if is_good_response(resp):
                return resp.content
            else:
                return None

    except RequestException as e:
        log_error('Error during requests to {0} : {1}'.format(url, str(e)))
        return None


def is_good_response(resp):
    """
    Returns True if the response seems to be HTML, False otherwise.
    """
    content_type = resp.headers['Content-Type'].lower()
    return (resp.status_code == 200 
            and content_type is not None 
            and content_type.find('html') > -1)


def log_error(e):
    """
    It is always a good idea to log errors. 
    This function just prints them, but you can
    make it do anything.
    """
    print(e)

Функция simple_get()принимает единственный url аргумент. Затем он делает GET запрос на этот URL. Если ничего не происходит, вы получаете необработанный HTML-контент для запрашиваемой страницы. Если с вашим запросом возникли проблемы (например, неверный URL-адрес или не работает удаленный сервер), ваша функция вернет None.

Возможно, вы заметили использование функции closing()в вашем определении simple_get(). В closing() функции гарантирует, что любые сетевые ресурсы освобождены , когда они выходят из области видимости , в этом with блоке. Такое использование closing() является хорошей практикой и помогает предотвратить фатальные ошибки и таймауты в сети.

Вы можете проверить simple_get(), как это происходит:

>>> from mathematicians import simple_get
>>> raw_html = simple_get('https://realpython.com/blog/')
>>> len(raw_html)
33878

>>> no_html = simple_get('https://realpython.com/blog/nope-not-gonna-find-it')
>>> no_html is None
True

Спор HTML с BeautifulSoup

Как только у вас есть сырой HTML перед вами, вы можете начать выбирать и извлекать. Для этого вы будете использовать BeautifulSoup. Конструктор BeautifulSoup анализирует исходные строки HTML и создает объект, зеркало структуру HTML – документ. Объект включает в себя множество методов для выбора, просмотра и управления узлами DOM и текстовым содержимым.

Рассмотрим следующий быстрый и надуманный пример HTML-документа:

<!DOCTYPE html>
<html>
<head>
  <title>Contrived Example</title>
</head>
<body>
<p id="eggman"> I am the egg man </p>
<p id="walrus"> I am the walrus </p>
</body>
</html>

Если приведенный выше HTML сохранен в файле contrived.html, вы можете использовать BeautifulSoup так:

>>> from bs4 import BeautifulSoup
>>> raw_html = open('contrived.html').read()
>>> html = BeautifulSoup(raw_html, 'html.parser')
>>> for p in html.select('p'):
...     if p['id'] == 'walrus':
...         print(p.text)

'I am the walrus'

Разберем пример, вы сначала анализируете необработанный HTML, передавая BeautifulSoup конструктору. BeautifulSoup принимает несколько внутренних анализаторов, но стандартным является тот 'html.parser', который вы указываете здесь в качестве второго аргумента. (Если вы не укажете 'html.parser', код все равно будет работать, но на экране появится предупреждение).

Метод select() на вашем html объекте позволяет использовать CSS селекторы для поиска элементов в документе. В приведенном выше случае html.select('p') возвращает список элементов абзаца. У каждого p есть HTML-атрибуты, к которым вы можете получить доступ как dict. Например, в строке if p['id'] == 'walrus' вы проверяете, равен ли  id атрибут строке 'walrus', которая соответствует <p id="walrus"> HTML.

Использование BeautifulSoup для получения имен математиков

Теперь, когда вы дали select() метод в BeautifulSoup коротком тест-драйве, как вы узнаете, что поставлять select()? Самый быстрый способ – выйти из Python и воспользоваться инструментами разработчика вашего веб-браузера. Вы можете использовать свой браузер, чтобы изучить документ более подробно. Я обычно ищу id или class атрибуты элемента или любую другую информацию, которая однозначно идентифицирует информацию, которую я хочу извлечь.

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

Вот краткий обзор Python:

>>> raw_html = simple_get('http://www.fabpedigree.com/james/mathmen.htm')
>>> html = BeautifulSoup(raw_html, 'html.parser')
>>> for i, li in enumerate(html.select('li')):
        print(i, li.text)

0  Isaac Newton
 Archimedes
 Carl F. Gauss
 Leonhard Euler
 Bernhard Riemann

1  Archimedes
 Carl F. Gauss
 Leonhard Euler
 Bernhard Riemann

2  Carl F. Gauss
 Leonhard Euler 
 Bernhard Riemann

 3  Leonhard Euler
 Bernhard Riemann

4  Bernhard Riemann

# 5 ... and many more...

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

def get_names():
    """
    Downloads the page where the list of mathematicians is found
    and returns a list of strings, one per mathematician
    """
    url = 'http://www.fabpedigree.com/james/mathmen.htm'
    response = simple_get(url)

    if response is not None:
        html = BeautifulSoup(response, 'html.parser')
        names = set()
        for li in html.select('li'):
            for name in li.text.split('\n'):
                if len(name) > 0:
                    names.add(name.strip())
        return list(names)

    # Raise an exception if we failed to get any data from the url
    raise Exception('Error retrieving contents at {}'.format(url))

Функция get_names()загружает страницу и перебирает <li> элементы, выбирая каждое имя. Затем вы добавляете каждое имя в Python set, что гарантирует, что вы не получите повторяющихся имен. Наконец, вы конвертируете набор в список и возвращаете его.

Получение рейтинга популярности

Хорошо, вы почти закончили! Теперь, когда у вас есть список имен, вам нужно выбрать просмотр страниц для каждого из них. Функция, которую вы пишете, похожа на функцию, которую вы создали для получения списка имен, только теперь вы вводите имя и выбираете целочисленное значение со страницы.

Опять же, вы должны сначала проверить пример страницы в инструментах разработчика вашего браузера. Похоже, что текст появляется внутри <a> элемента, а href атрибут этого элемента всегда содержит строку 'latest-60' в качестве подстроки. Это все, что вам нужно, чтобы написать свою функцию:

def get_hits_on_name(name):
    """
    Accepts a `name` of a mathematician and returns the number
    of hits that mathematician's Wikipedia page received in the 
    last 60 days, as an `int`
    """
    # url_root is a template string that is used to build a URL.
    url_root = 'URL_REMOVED_SEE_NOTICE_AT_START_OF_ARTICLE'
    response = simple_get(url_root.format(name))

    if response is not None:
        html = BeautifulSoup(response, 'html.parser')

        hit_link = [a for a in html.select('a')
                    if a['href'].find('latest-60') > -1]

        if len(hit_link) > 0:
            # Strip commas
            link_text = hit_link[0].text.replace(',', '')
            try:
                # Convert to integer
                return int(link_text)
            except:
                log_error("couldn't parse {} as an `int`".format(link_text))

    log_error('No pageviews found for {}'.format(name))
    return None

Собираем все вместе

Вы достигли точки, когда вы можете наконец узнать, какой математик наиболее любим публикой! План прост:

  • Получить список имен
  • Перебрать список, чтобы получить «рейтинг популярности» для каждого имени
  • Готово!

Просто, правда? Ну, есть одна вещь, которая еще не упоминалась: ошибки.

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

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

Вот код:

if __name__ == '__main__':
    print('Getting the list of names....')
    names = get_names()
    print('... done.\n')

    results = []

    print('Getting stats for each name....')

    for name in names:
        try:
            hits = get_hits_on_name(name)
            if hits is None:
                hits = -1
            results.append((hits, name))
        except:
            results.append((-1, name))
            log_error('error encountered while processing '
                      '{}, skipping'.format(name))

    print('... done.\n')

    results.sort()
    results.reverse()

    if len(results) > 5:
        top_marks = results[:5]
    else:
        top_marks = results

    print('\nThe most popular mathematicians are:\n')
    for (mark, mathematician) in top_marks:
        print('{} with {} pageviews'.format(mathematician, mark))

    no_results = len([res for res in results if res[0] == -1])
    print('\nBut we did not find results for '
          '{} mathematicians on the list'.format(no_results))

Это оно!

Когда вы запустите скрипт, вы должны увидеть в следующем отчете:

The most popular mathematicians are:

Albert Einstein with 1089615 pageviews
Isaac Newton with 581612 pageviews
Srinivasa Ramanujan with 407141 pageviews
Aristotle with 399480 pageviews
Galileo Galilei with 375321 pageviews

But we did not find results for 19 mathematicians on our list

Заключение и следующие шаги

Скрапинг в Интернете – большая область, и вы только что завершили краткий обзор этого поля, используя Python. 


Совершенствуй знания каждый день у нас в Телеграм-каналах

Вопросы, реклама — VK | Telegram