Веб скрапинг (парсинг) данных

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

Автоматизированный сбор данных из интернета существует почти столько же времени, сколько и сам интернет. Хотя термин «веб-скрапинг» не является новым, в прошлом эту практику чаще называли «скринскрапинг» (screen scraping), «добыча данных» (data mining), «сбор данных с веб-страниц» (web harvesting) и другими похожими названиями. Сегодня общее мнение склоняется в пользу термина «веб-скрапинг», поэтому я буду использовать его в этой книге. Также я буду упоминать программы, которые специализированно обходят множество страниц, как веб-пауки (web crawlers), или называть сами программы веб-скрапинга ботами.

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

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

Примечания:

  1. Веб-скрапинг — это техника извлечения данных с веб-сайтов. Она может быть выполнена с помощью специальных программ, которые симулируют действия пользователя, автоматически переходя по страницам и собирая нужную информацию.
  2. Веб-паук (Web crawler) — это программа, которая автоматически перемещается по страницам Интернета для индексации данных, используемых поисковыми системами.
  3. API (Application Programming Interface) — это набор правил и спецификаций, которые позволяют программам взаимодействовать друг с другом. В контексте веб-скрапинга, использование API — это более «чистый» и предпочтительный способ получения данных, поскольку он обычно не связан с парсингом HTML и не нарушает правила сайта.
  4. HTML (HyperText Markup Language) — это основной язык разметки для создания веб-страниц. Скраперы анализируют HTML, чтобы извлечь нужные данные, например текст, ссылки, информацию о продуктах и т.д.
  5. Парсинг — это процесс анализа информации с целью извлечения нужных данных. В контексте веб-скрапинга, это обычно означает анализ HTML-кода страницы для получения содержимого.
Содержание
  1. Зачем нужен веб-скрапинг?
  2. Построение Скраперов
  3. Как работает интернет
  4. Сетевое взаимодействие
  5. Сетевое взаимодействие и модель OSI
  6. Физический уровень
  7. Значение физического уровня в веб-скрапинге
  8. Канальный уровень
  9. Значение канального уровня в веб-скрапинге
  10. Сетевой уровень
  11. Значение сетевого уровня в веб-скрапинге
  12. Транспортный Уровень
  13. Уровень Сессии
  14. Сессии против Сессий
  15. Уровень представления
  16. Уровень приложения
  17. Ваш первый веб-скрапер
  18. Подключение
  19. Разбор html файла с помощью BeautifulSoap
  20. Обработка ошибок при скрапинге
  21. Продвинутый HTML парсинг
  22. Еще немного о BeautifulSoup
  23. find() и find_all() с BeautifulSoup
  24. Ключевой аргумент и класс
  25. Другие объекты BeautifulSoup
  26. Навигация по деревьям
  27. Работа с детьми в дереве тегов и другими потомками
  28. Обработка соседних элементов
  29. Работа с родителями
  30. Использование регулярных выражений в парсинге
  31. Регулярные выражения и BeautifulSoup
  32. Доступ к атрибутам
  33. Лямбда-выражения

Зачем нужен веб-скрапинг?

Если вы используете интернет только через браузер, вы упускаете множество возможностей. Хотя браузеры удобны для выполнения JavaScript, отображения изображений и организации объектов в более удобном для восприятия человеком формате (среди прочего), веб-скраперы отлично справляются с сбором и обработкой больших объемов данных быстро. Вместо того чтобы просматривать одну страницу за раз через узкое окно монитора, вы можете просматривать базы данных, охватывающие тысячи или даже миллионы страниц сразу.

Кроме того, веб-скраперы могут зайти туда, куда не могут традиционные поисковые системы. Поиск в Google по запросу «самые дешевые авиабилеты в Бостон» приведет к массе рекламы и популярных сайтов для поиска авиабилетов. Google знает только то, что эти веб-сайты говорят на своих страницах контента, но не точные результаты различных запросов, введенных в поисковик авиабилетов. Однако хорошо разработанный веб-скрапер может отслеживать стоимость полета в Бостон во времени, на различных веб-сайтах, и сказать вам лучшее время для покупки билета.

Вы можете спросить: «Разве сбор данных — это не то, для чего предназначены API?» Да, API могут быть замечательными, если вы найдете тот, который подходит вашим целям. Они предназначены для предоставления удобного потока хорошо структурированных данных из одной компьютерной программы в другую. Вы можете найти API для многих типов данных, которые вы хотели бы использовать, таких как твиты в Twitter или страницы в Википедии. В целом, предпочтительнее использовать API (если он существует), чем создавать бота для получения тех же данных. Однако API может не существовать или не быть полезным для ваших целей по нескольким причинам:

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

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

Здесь на помощь приходит веб-скрапинг. С некоторыми исключениями, если вы можете просмотреть данные в браузере, вы можете получить к ним доступ с помощью скрипта на Python. Если вы можете получить к ним доступ в скрипте, вы можете хранить их в базе данных. А если вы можете хранить их в базе данных, вы можете делать практически что угодно с этими данными.

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

Даже в мире искусства веб-скрапинг открыл новые горизонты для творчества. Проект 2006 года «We Feel Fine» Джонатана Харриса и Сепа Камвара скреб по различным англоязычным блогам фразы, начинающиеся с «I feel» или «I am feeling». Это привело к популярной визуализации данных, описывающей, как мир чувствовал себя изо дня в день и минуту за минутой.

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

Построение Скраперов

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

Честно говоря, веб-скрапинг — отличная область для входа, если вы хотите получить большую отдачу при относительно небольших первоначальных вложениях. Скорее всего, 90% проектов по веб-скрапингу, с которыми вы столкнетесь, будут использовать техники, описанные всего в следующих шести главах. Этот раздел охватывает то, что общественность, имеющая технические навыки, обычно представляет, когда думает о «веб-скраперах»:

  • Извлечение HTML-данных из доменного имени
  • Анализ этих данных для получения целевой информации
  • Хранение целевой информации
  • При необходимости переход на другую страницу для повторения процесса

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

Экспресс руководство по созданию веб-скраперов

  1. Использование Python для запроса информации: Чтобы начать скрапинг, вам нужно научиться отправлять запросы к веб-серверам. Это можно сделать с помощью библиотеки requests в Python. Когда вы отправляете запрос на веб-сервер, вы запрашиваете HTML-код страницы, который потом можете анализировать.
    import requests
    
    url = 'https://example.com'
    response = requests.get(url)
    html = response.text
  2. Базовая обработка ответа сервера: После получения ответа от сервера важно проверить, был ли запрос успешным. Это можно сделать, проверив статус-код ответа. Статус-код 200 означает, что запрос был успешным.
    if response.status_code == 200:
        print("Запрос успешно выполнен!")
    else:
        print("Произошла ошибка при запросе!")
  3. Автоматизированное взаимодействие с веб-сайтом: Для работы с HTML и извлечения нужной информации используется библиотека BeautifulSoup. Она позволяет легко находить нужные элементы на странице, используя теги, атрибуты и CSS-селекторы.
    from bs4 import BeautifulSoup
    
    soup = BeautifulSoup(html, 'html.parser')
    title = soup.find('title').text
    print("Заголовок страницы:", title)
  4. Переход между страницами: Веб-скраперы могут автоматически переходить с одной страницы на другую. Например, если вы анализируете пагинированный список, вы можете использовать BeautifulSoup для нахождения ссылки на следующую страницу и затем повторять процесс.
    next_link = soup.find('a', {'rel': 'next'})
    if next_link:
        next_page_url = next_link['href']
        print("Следующая страница:", next_page_url)
  5. Хранение информации: После сбора данных их можно сохранить в файле, базе данных или любом другом хранилище. Например, сохранение данных в CSV-файле:
    import csv
    
    data = [['Title', title], ['URL', url]]
    with open('data.csv', 'w', newline='', encoding='utf-8') as file:
        writer = csv.writer(file)
        writer.writerows(data)

Как работает интернет

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

Из-за ограничений в объёме страниц и знаний автора, этот раздел также должен полагаться на подобные абстракции. Он описывает механику интернета и веб-приложений в той мере, в какой это необходимо для скрапинга веба (а затем, возможно, чуть больше).

Когда вы вводите URL в адресную строку вашего веб-браузера и нажимаете Enter, интерактивный текст, изображения и медиа появляются как будто по волшебству. То же самое волшебство происходит для миллиардов других людей каждый день. Они посещают те же веб-сайты, используют те же приложения — часто получая медиа и текст, настроенные специально для них.

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

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

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

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

Краткое резюме:

  1. URL и запросы: Когда вы вводите URL (адрес веб-страницы) в строке браузера и нажимаете Enter, ваш компьютер отправляет запрос на сервер, где хранится эта веб-страница. Этот запрос и ответ на него — основа работы интернета.
  2. Протоколы: В основе работы интернета лежат различные протоколы. Самые известные из них:
    • HTTP (HyperText Transfer Protocol): Протокол передачи гипертекста, используемый для загрузки веб-страниц.
    • HTTPS (HTTP Secure): Безопасная версия HTTP, шифрующая данные для безопасной передачи.
    • TCP/IP (Transmission Control Protocol/Internet Protocol): Набор коммуникационных протоколов для подключения сетевых устройств в интернете.
  3. DNS (Domain Name System): Система доменных имен переводит удобные для человека адреса (например, www.example.com) в IP-адреса, которые используются для маршрутизации в интернете.
  4. Веб-серверы и браузеры: Веб-сервер — это программное обеспечение (и обычно и сервер, на котором оно запущено), которое отвечает на запросы от вашего браузера и отправляет обратно данные, обычно в формате HTML. Веб-браузер интерпретирует эти данные и отображает их в удобной для чтения форме.
  5. HTML/CSS/JavaScript: Эти технологии используются для создания веб-страниц:
    • HTML (HyperText Markup Language): Язык разметки, который используется для создания веб-страниц.
    • CSS (Cascading Style Sheets): Язык стилей, который используется для определения внешнего вида и форматирования HTML-документа.
    • JavaScript: Язык программирования, который используется для создания интерактивных эффектов внутри веб-страниц.
  6. Веб-скрапинг: Веб-скрапинг — это процесс использования программ (скраперов) для автоматического сбора данных с веб-страниц. Скраперы делают запросы к веб-страницам, получают HTML-ответы и анализируют эти данные, извлекая нужную информацию.
  7. Стандарты и соглашения: Нет централизованного управления интернетом, но есть организации, которые разрабатывают стандарты (например, W3C для HTML и CSS). Следование этим стандартам обеспечивает совместимость и доступность содержимого в разных браузерах и устройствах.

Сетевое взаимодействие

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

Междугородние звонки были дорогими и могли занимать несколько минут на установление соединения. Совершение междугороднего звонка из Бостона в Сиэтл приводило к координации действий операторов телефонных станций по всем Соединённым Штатам для создания одного огромного провода, напрямую соединяющего ваш телефон с телефоном получателя.

Сегодня, вместо того чтобы совершать телефонный звонок по временному выделенному соединению, мы можем совершить видеозвонок из нашего дома в любую точку мира по постоянной сети проводов. Провод не указывает данным, куда идти — данные сами направляют себя в процессе, называемом пакетной коммутацией. Хотя множество технологий со временем внесли свой вклад в то, что мы называем «интернетом», пакетная коммутация — это технология, которая одна начала это всё.

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

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

Современная сеть с пакетной коммутацией обычно описывается с помощью модели взаимодействия открытых систем (OSI), которая состоит из семи слоёв маршрутизации, кодирования и обработки ошибок:

  1. Физический уровень (Physical layer)
  2. Канальный уровень (Data link layer)
  3. Сетевой уровень (Network layer)
  4. Транспортный уровень (Transport layer)
  5. Сеансовый уровень (Session layer)
  6. Уровень представления (Presentation layer)
  7. Прикладной уровень (Application layer)

Большинство разработчиков веб-приложений проводят свои дни полностью на 7-м уровне, прикладном уровне. Это также уровень, на котором в этой книге проводится больше всего времени. Однако важно иметь хотя бы концептуальные знания о других слоях при скрапинге веба. Например, метод обнаружения веб-скрапинга TLS fingerprinting, обсуждаемый в главе 17, вовлекает транспортный уровень.

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

Сетевое взаимодействие и модель OSI

  1. Физический уровень (Physical layer): Этот уровень занимается передачей битов данных через физические средства — кабели, оптоволокно, беспроводные каналы. Здесь данные ещё не имеют структуры пакетов, они передаются как электрические, оптические или радио сигналы.
  2. Канальный уровень (Data link layer): На этом уровне данные организуются в кадры. Этот уровень обеспечивает надёжную передачу данных между двумя устройствами, например, между вашим компьютером и роутером. Примеры протоколов: Ethernet, Wi-Fi.
  3. Сетевой уровень (Network layer): Здесь данные упаковываются в пакеты, и задаётся маршрутизация — выбор пути, по которому данные будут переданы от отправителя к получателю. Протокол IP (Internet Protocol) работает на этом уровне.
  4. Транспортный уровень (Transport layer): Этот уровень контролирует качество передачи данных между двумя хостами. Протоколы как TCP (Transmission Control Protocol) и UDP (User Datagram Protocol) обеспечивают, соответственно, надёжную и ненадёжную доставку данных.
  5. Сеансовый уровень (Session layer): Управляет сессиями связи между приложениями на разных хостах, например, устанавливает, управляет и завершает соединения.
  6. Уровень представления (Presentation layer): Обеспечивает независимость данных приложения от различий в представлении данных (синтаксисе). Примеры задач: преобразование данных, сжатие и шифрование.
  7. Прикладной уровень (Application layer): Это уровень, на котором работают приложения, с которыми вы взаимодействуете напрямую, как веб-браузеры, почтовые клиенты, системы мгновенного обмена сообщениями. Протоколы веб: HTTP, HTTPS, FTP.

Физический уровень

Физический уровень определяет, как информация физически передаётся с использованием электричества по Ethernet-проводу в вашем доме (или в любой локальной сети). Этот уровень определяет такие параметры, как уровни напряжения, которые кодируют биты 1 и 0, и скорость, с которой эти напряжения могут быть изменены (импульсированы). Также на этом уровне определяется, как интерпретируются радиоволны в Bluetooth и WiFi.

На этом уровне не используются программирование или цифровые инструкции, поскольку он полностью основан на физике и электрических стандартах.

Физический уровень — это самый нижний уровень модели OSI, и он занимается передачей необработанных битов данных через физическое устройство. Этот уровень включает в себя всё, что связано с непосредственной физической передачей данных:

  1. Передача данных через кабели (например, Ethernet):
    • Сигналы и биты: На физическом уровне данные представляются последовательностями электрических сигналов. В случае Ethernet-кабеля, например, биты (0 и 1) кодируются изменениями напряжения в проводе.
    • Уровни напряжения: Различные стандарты Ethernet используют разные методы для определения того, как биты представлены физическими сигналами. Например, некоторые стандарты определяют ‘1’ как высокий уровень напряжения, а ‘0’ — как низкий.
  2. Беспроводная передача (например, WiFi и Bluetooth):
    • Радиоволны: Вместо использования электрических сигналов по проводам, беспроводные технологии используют радиоволны для передачи данных. Эти волны кодируют данные изменениями в амплитуде, частоте или фазе волн.
    • Интерпретация сигналов: Устройства в беспроводной сети интерпретируют эти волны, преобразуя их обратно в электронные сигналы, которые затем обрабатываются на более высоких уровнях.
  3. Стандарты и спецификации:
    • Ethernet: Для проводных сетей стандарты Ethernet определяют не только физические характеристики кабелей, но и то, как сигналы передаются по этим кабелям.
    • Wi-Fi: Для беспроводных сетей стандарты, такие как IEEE 802.11, определяют параметры радиоволн, способы их модуляции и другие ключевые характеристики передачи данных.

Значение физического уровня в веб-скрапинге

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

  • Прямое влияние: Физический уровень влияет на скорость и надежность вашего интернет-соединения. Низкое качество сигнала или плохое физическое соединение может привести к медленной или ошибочной передаче данных.
  • Отладка и устранение неполадок: Если ваш веб-скрапер сталкивается с проблемами сетевой задержки или потерей пакетов, понимание физического уровня может помочь в диагностике. Например, проблемы с Ethernet-кабелем или Wi-Fi сигналом могут быть причиной.

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

Канальный уровень

Канальный уровень (Data Link Layer) определяет, как информация передается между двумя узлами в локальной сети, например, между вашим компьютером и роутером. Он определяет начало и конец одной передачи данных и обеспечивает коррекцию ошибок, если передача была потеряна или искажена.

На этом уровне пакеты оборачиваются в дополнительный «цифровой конверт», содержащий информацию о маршрутизации, и называются кадрами (frames). Когда информация в кадре больше не нужна, она извлекается из кадра и отправляется по сети в виде пакета.

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

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

  1. Кадры (Frames):
    • Формирование кадров: На канальном уровне пакеты данных из сетевого уровня упаковываются в кадры. Каждый кадр содержит полезные данные (payload), а также служебные данные, такие как адреса отправителя и получателя в пределах локальной сети, и информацию для проверки ошибок.
    • Обработка ошибок: Кадры содержат также проверочные суммы (например, CRC — Cyclic Redundancy Check), которые используются для обнаружения ошибок в данных. Если ошибка обнаружена, данные могут быть запросены заново.
  2. Адресация на канальном уровне:
    • MAC-адреса (Media Access Control): Каждое устройство в локальной сети имеет уникальный MAC-адрес, который используется на канальном уровне для идентификации отправителя и получателя данных внутри сети.
  3. Типы канального уровня:
    • Ethernet: Наиболее распространенная технология канального уровня для проводных сетей.
    • Wi-Fi: Используется для беспроводных локальных сетей; работает на канальном уровне с аналогичными принципами, что и Ethernet, но в беспроводной среде.
  4. Протоколы канального уровня:
    • IEEE 802.3 (Ethernet): Стандарт для проводных сетей.
    • IEEE 802.11 (Wi-Fi): Стандарт для беспроводных сетей.

Значение канального уровня в веб-скрапинге

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

  • Надежность передачи данных: Канальный уровень обеспечивает основу для надежной передачи данных в локальной сети. Ошибки, обнаруженные на этом уровне, могут быть исправлены без повторной передачи данных по всему Интернету, что уменьшает задержки и повышает производительность.
  • Сетевая отладка: Понимание канального уровня помогает диагностировать и устранять проблемы сетевой передачи данных, такие как коллизии в сети Ethernet или проблемы с сигналом Wi-Fi.
  • Масштабирование скраперов: Понимание того, как работает канальный уровень, важно для масштабирования веб-скраперов, особенно когда они работают в распределенных сетях или кластерах.

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

Сетевой уровень

Сетевой уровень — это место, где происходит коммутация пакетов, а следовательно, и работа «интернета». Это уровень, который позволяет пакетам от вашего компьютера быть перенаправленными роутером и достичь устройств, находящихся за пределами их непосредственной сети.

Сетевой уровень включает в себя часть «Internet Protocol» (IP) протокола «Transmission Control Protocol/Internet Protocol» (TCP/IP). Именно в IP мы получаем IP-адреса. Например, мой IP-адрес в глобальном интернете в настоящее время — 46.191.191.87. Это позволяет любому компьютеру в мире отправлять данные мне, а мне — отправлять данные на любой другой адрес с моего собственного адреса.

Сетевой уровень — это третий уровень модели OSI, и он играет ключевую роль в доставке пакетов данных из одной сети в другую через процесс, называемый маршрутизацией. Основной протокол на этом уровне — IP (Internet Protocol).

  1. IP-адреса:
    • Глобальная уникальность: Каждое устройство, подключенное к интернету, имеет уникальный IP-адрес, который используется для идентификации устройства в глобальной сети. IP-адреса бывают двух версий — IPv4 и IPv6.
    • IPv4: Состоит из 4 байтов (например, 192.168.1.1), общее количество адресов ограничено.
    • IPv6: Состоит из 16 байтов (например, 2001:0db8:85a3:0000:0000:8a2e:0370:7334), предоставляет практически неограниченное количество уникальных адресов.
  2. Маршрутизация:
    • Функция маршрутизации: Сетевой уровень управляет маршрутами, по которым пакеты данных отправляются из одной сети в другую. Роутеры — основные устройства, работающие на сетевом уровне, определяют наилучший путь для каждого пакета.
    • Таблицы маршрутизации: Роутеры используют таблицы маршрутизации для определения, куда следует отправить пакет дальше на его пути к конечному получателю.
  3. Протоколы сетевого уровня:
    • IP (Internet Protocol): Основной протокол, который отвечает за доставку пакетов от отправителя к получателю, используя IP-адреса.
    • ICMP (Internet Control Message Protocol): Используется для отправки сообщений об ошибках и операционной информации (например, при использовании команды ping).
  4. Фрагментация и сборка пакетов:
    • Фрагментация: Если пакет слишком велик для передачи через какой-то сегмент сети, он может быть разбит на более мелкие фрагменты на сетевом уровне. Эти фрагменты пересылаются отдельно и собираются в исходный пакет на стороне получателя.
    • Сборка: Процесс восстановления оригинального пакета из его фрагментов на стороне получателя.

Значение сетевого уровня в веб-скрапинге

Сетевой уровень крайне важен для веб-скрапинга, поскольку именно здесь определяется, как данные достигают целевого веб-сервера и возвращаются обратно:

  • Глобальная доступность: Понимание IP-адресации помогает в организации запросов к веб-серверам со всего мира и управлении IP-адресами скраперов (например, при использовании прокси-серверов для изменения IP-адреса скрапера).
  • Обработка сетевых задержек и потерь: Понимание маршрутизации и потенциальных проблем на сетевом уровне позволяет оптимизировать скраперы для работы в условиях нестабильной сети, минимизируя потери данных и задержки.
  • Масштабирование и распределение: Знание сетевого уровня полезно при масштабировании веб-скраперов для работы в распределенных системах, где маршрутизация и эффективное использование сети могут значительно повысить производительность.

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

Транспортный Уровень

Уровень 4, транспортный уровень, занимается установлением соединения между конкретными сервисами или приложениями, работающими на разных компьютерах. Он не просто соединяет сами компьютеры, но и обеспечивает связь между конкретными приложениями. Этот уровень также отвечает за исправление ошибок и повторные попытки отправки данных, если это необходимо.

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

В отличие от TCP, протокол UDP (пользовательский дейтаграммный протокол) не столь строг и может пропускать недостающие пакеты данных, чтобы не прерывать поток данных. Это часто используется в видео- и аудиоконференциях, где временное снижение качества передачи данных предпочтительнее, чем задержки в разговоре.

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

Этот порт обычно указывается в виде числа после IP-адреса, разделённого двоеточием. Например, запись 71.245.238.173:8080 означает, что приложение, которому операционная система назначила порт 8080, работает на компьютере с IP-адресом 71.245.238.173.

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

Уровень Сессии

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

Уровень сессий также позволяет повторно попытаться передать данные в случае кратковременного сбоя или разрыва соединения.

Сессии против Сессий

Сессии на уровне сессий в модели OSI отличаются от сессий и данных сессий, о которых обычно говорят веб-разработчики.

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

В то же время, на уровне сессий в модели OSI сессия обычно продолжается только в течение времени, необходимого для передачи одного файла!

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

Уровень представления

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

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

  1. Преобразование данных: Уровень представления может преобразовывать данные из одного формата в другой, чтобы упростить их обработку приложениями. Например, он может преобразовать данные из бинарного формата в текстовый (или наоборот), что важно при работе с различными мультимедийными файлами и документами.
  2. Кодирование символов: Этот уровень управляет процессом кодирования символов, например, преобразованием текста из кодировки UTF-8 в UTF-16. Это обеспечивает корректное отображение текста на разных устройствах и в разных приложениях, учитывая особенности локальных настроек и языков.
  3. Сжатие данных: Уровень представления может сжимать данные перед их отправкой для уменьшения объёма передаваемой информации, что особенно актуально при передаче больших файлов, таких как видео, аудио или большие наборы данных. Сжатие помогает ускорить передачу данных и сократить использование сетевых ресурсов.
  4. Распознавание формата: Он определяет, в каком формате представлены данные (PNG, HTML, PDF и др.), что позволяет приложению корректно обработать полученную информацию. Это как раз то, что нужно для правильной работы веб-браузеров, текстовых редакторов и других приложений, работающих с разнообразными форматами данных.

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

Уровень приложения

Уровень приложения интерпретирует данные, закодированные уровнем представления, и использует их соответствующим образом для нужд приложения. Можно сказать, что уровень представления занимается преобразованием и идентификацией данных, в то время как уровень приложения занимается непосредственным «выполнением» действий с этими данными. Например, HTTP со своими методами и статусами является протоколом уровня приложения. В то же время более привычные JSON и HTML (поскольку это форматы файлов, определяющие способ кодирования данных) относятся к протоколам уровня представления.

На уровне приложения происходят следующие ключевые процессы:

  1. Интерпретация данных: Уровень приложения анализирует данные, полученные от уровня представления, и преобразует их в формат, с которым могут работать приложения. Это может включать разбор структурированных данных (например, JSON или XML) для использования в базах данных, веб-сервисах или пользовательских интерфейсах.
  2. Взаимодействие с пользователем: Этот уровень обеспечивает механизмы для взаимодействия с пользователем через графические интерфейсы, API или командные строки. Это позволяет пользователям управлять приложениями и получать необходимую информацию в удобной форме.
  3. Использование сетевых сервисов: Уровень приложения использует различные сетевые протоколы (например, HTTP, FTP, SMTP) для обмена данными с другими приложениями через сеть Интернет или локальные сети. Это включает в себя отправку запросов, получение ответов, обработку ошибок сетевого взаимодействия и т.д.
  4. Обработка и выполнение задач: Уровень приложения отвечает за выполнение конкретных задач, которые требуются от приложения, включая обработку бизнес-логики, выполнение расчётов, управление данными и многое другое.
  5. Работа с протоколами: Несмотря на то что JSON и HTML определяют способы кодирования данных, HTTP и другие подобные протоколы определяют «правила игры» для приложений, указывая, как данные должны быть отправлены, приняты, какие действия следует предпринять в ответ на различные сценарии и как сообщать о своём состоянии.

Примеры использования уровня приложения:

  • Веб-браузеры используют HTTP для запроса веб-страниц, которые они затем отображают пользователям, интерпретируя HTML и применяя стили CSS.
  • Почтовые клиенты используют протоколы SMTP, IMAP или POP3 для отправки и получения электронной почты.
  • Файловые передачи могут использовать FTP или SFTP для загрузки и скачивания файлов через сеть.

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

Ваш первый веб-скрапер

Когда вы начинаете веб-скрапинг, начинаете ценить все те мелочи, которые браузер делает за вас. Веб, без слоя форматирования HTML, стилизации CSS, выполнения JavaScript и отображения изображений, может казаться немного пугающим.

Этот раздел начинается с основ отправки GET-запроса (запрос на получение, или «получение», содержимого веб-страницы) на веб-сервер для конкретной страницы, чтения HTML-вывода с этой страницы и выполнения некоторых простых операций по извлечению данных для выделения нужного вам контента.

Подключение

Если вы не проводили много времени в сетевой инфраструктуре или сетевой безопасности, механизмы интернета могут показаться вам немного загадочными. Вам не хочется думать о том, что именно делает сеть каждый раз, когда вы открываете браузер и переходите на http://google.com, и, в наши дни, вам не нужно это делать. Фактически, я бы сказал, что фантастически то, что пользовательские интерфейсы компьютеров развились до такой степени, что большинство людей, использующих интернет, не имеют ни малейшего представления о том, как это работает.

Однако для веб-скрапинга требуется немного снять этот покров интерфейса — не только на уровне браузера (как он интерпретирует всю эту HTML, CSS и JavaScript), но иногда и на уровне сетевого подключения.

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

  1. Компьютер Боба отправляет поток битов 1 и 0, указанных высокими и низкими напряжениями на проводе. Эти биты формируют какую-то информацию, содержащую заголовок и тело. Заголовок содержит непосредственное направление места назначения его локального MAC-адреса маршрутизатора, с конечным пунктом назначения IP-адреса Алисы. Тело содержит его запрос для серверного приложения Алисы.
  2. Локальный маршрутизатор Боба получает все эти единицы и нули и интерпретирует их как пакет, от собственного MAC-адреса Боба, предназначенный для IP-адреса Алисы. Его маршрутизатор накладывает свой собственный IP-адрес на пакет в качестве «откуда» IP-адреса и отправляет его через интернет.
  3. Пакет Боба проходит через несколько промежуточных серверов, которые направляют его по правильному физическому/проводному пути, к серверу Алисы.
  4. Сервер Алисы получает пакет по своему IP-адресу.
  5. Сервер Алисы читает портовое назначение пакета в заголовке и передает его соответствующему приложению — веб-серверному приложению. (Портовое назначение пакета почти всегда порт 80 для веб-приложений; это можно представить как номер квартиры для данных пакетов, в то время как IP-адрес подобен улице.)
  6. Веб-серверное приложение получает поток данных от процессора сервера. Эти данные что-то говорят вроде следующего:
  • Это GET-запрос.
  • Запрашивается следующий файл: index.html.
  1. Веб-сервер находит правильный HTML-файл, упаковывает его в новый пакет для отправки Бобу и отправляет его через свой локальный маршрутизатор для передачи обратно на компьютер Боба, через тот же процесс.

И вот мы имеем Интернет.

Так где в этом обмене вступает в игру веб-браузер? Абсолютно нигде. Фактически, браузеры являются относительно недавним изобретением в истории интернета, учитывая, что Nexus был выпущен в 1990 году.

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

from urllib.request import urlopen
html = urlopen('http://pythonscraping.com/pages/page1.html')
print(html.read())

Чтобы выполнить этот код, вы можете сохранить его локально как scrapetest.py и запустить его в вашем терминале, используя эту команду:

$ python scrapetest.py

Эта команда выводит полный HTML-код для страницы page1, расположенной по адресу http://pythonscraping.com/pages/page1.html. Более точно, это выводит HTML-файл page1.html, найденный в каталоге <корень веб-сервера>/pages, на сервере, расположенном по доменному имени http://pythonscraping.com.

Почему важно начать думать об этих адресах как о «файлах», а не о «страницах»? Большинство современных веб-страниц имеют множество файлов ресурсов, связанных с ними. Это могут быть файлы изображений, файлы JavaScript, файлы CSS или любой другой контент, на который ссылается запрашиваемая вами страница. Когда веб-браузер сталкивается с тегом, таким как <img src="cuteKitten.jpg">, браузер знает, что ему нужно сделать еще один запрос к серверу, чтобы получить данные из файла cuteKitten.jpg для полного отображения страницы для пользователя. Конечно, ваш сценарий Python не имеет логики для возврата и запроса нескольких файлов (пока); он может только читать один HTML-файл, который вы напрямую запросили.

from urllib.request import urlopen смотрит на модуль Python request (найденный в библиотеке urllib) и импортирует только функцию urlopen. urllib является стандартной библиотекой Python (это означает, что вам не нужно устанавливать ничего дополнительного для запуска этого примера) и содержит функции для запроса данных по сети, обработки куки и даже изменения метаданных, таких как заголовки и ваш пользовательский агент.

Разбор html файла с помощью BeautifulSoap

Прежде чем приступить к практическим примерам, изучите материал по BeautifulSoap у меня на сайте.

Самым часто используемым объектом в библиотеке BeautifulSoup является, как следует из названия, объект BeautifulSoup. Давайте посмотрим, как он работает, изменив пример, найденный в начале этой статьи:

from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen('http://www.pythonscraping.com/pages/page1.html')
bs = BeautifulSoup(html.read(), 'html.parser')
print(bs.h1)

Результат будет следующим:

<h1>An Interesting Title</h1>

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

Как и в предыдущих примерах веб-скрапинга, вы импортируете функцию urlopen и вызываете html.read(), чтобы получить HTML-контент страницы. Кроме текстовой строки, BeautifulSoup также может использовать объект файла напрямую, возвращаемый urlopen, без необходимости вызывать .read() сначала:

bs = BeautifulSoup(html, 'html.parser')

Этот HTML-контент затем преобразуется в объект BeautifulSoup со следующей структурой:

html → <html><head>...</head><body>...</body></html>
— head → <head><title>A Useful Page<title></head>
— title → <title>A Useful Page</title>
— body → <body><h1>An Int...</h1><div>Lorem ip...</div></body>
— h1 → <h1>An Interesting Title</h1>
— div → <div>Lorem Ipsum dolor...</div>

Обратите внимание, что тег h1, который вы извлекаете со страницы, вложен на два уровня глубже в структуру вашего объекта BeautifulSoup (html → body → h1). Однако, когда вы фактически получаете его из объекта, вы вызываете тег h1 напрямую:

bs.h1

Фактически, любые из следующих вызовов функций будут производить тот же самый вывод:

bs.html.body.h1
bs.body.h1
bs.html.h1

При создании объекта BeautifulSoup передаются два аргумента:

bs = BeautifulSoup(html.read(), 'html.parser')

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

html.parser — это парсер, включенный в Python 3 и не требующий дополнительной установки для использования. За исключением случаев, когда это требуется, мы будем использовать этот парсер на протяжении всей книги.

Еще одним популярным парсером является lxml. Его можно установить через pip:

pip3 install lxml

lxml можно использовать с BeautifulSoup, изменив предоставляемую строку парсера:

bs = BeautifulSoup(html.read(), 'lxml')

lxml имеет некоторые преимущества перед html.parser в том, что он обычно лучше парсит «грязный» или искаженный HTML-код. Он прощает и исправляет проблемы, такие как незакрытые теги, неправильно вложенные теги и отсутствие тегов head или body. Он также немного быстрее, чем html.parser, хотя скорость не всегда является преимуществом при веб-скрапинге, учитывая, что скорость самой сети практически всегда будет вашим наибольшим ограничителем.

Одним из недостатков lxml является то, что его необходимо устанавливать отдельно и он зависит от сторонних C-библиотек для работы. Это может вызвать проблемы с переносимостью и удобством использования по сравнению с html.parser.

Еще одним популярным парсером HTML является html5lib. Как и lxml, html5lib является крайне лояльным парсером, который еще более активно исправляет сломанный HTML. Он также зависит от внешней зависимости и медленнее, чем lxml и html.parser. Несмотря на это, он может быть хорошим выбором, если вы работаете с беспорядочными или написанными вручную веб-сайтами.

Его можно использовать, установив и передавая строку html5lib объекту BeautifulSoup:

bs = BeautifulSoup(html.read(), 'html5lib')

Обработка ошибок при скрапинге

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

Давайте взглянем на первую строку нашего скрапера, после инструкций импорта, и разберемся, как обрабатывать любые исключения, которые могут возникнуть:

html = urlopen('http://www.pythonscraping.com/pages/page1.html')

В этой строке могут произойти две основные проблемы:

  • Страница не найдена на сервере (или возникла ошибка при ее получении).
  • Сервер не найден.

В первой ситуации будет возвращена ошибка HTTP. Эта ошибка HTTP может быть «404 Страница не найдена», «500 Внутренняя ошибка сервера» и так далее. Во всех этих случаях функция urlopen выбросит общее исключение HTTPError. Вы можете обработать это исключение следующим образом:

from urllib.request import urlopen
from urllib.error import HTTPError

try:
    html = urlopen('http://www.pythonscraping.com/pages/page1.html')
except HTTPError as e:
    print(e)
    # вернуть null, выйти или выполнить другое "План B"
else:
    # программа продолжает работать. Примечание: если вы вернетесь или выйдете из
    # исключения, вам не нужно использовать оператор "else"

Если возвращается код ошибки HTTP, программа теперь печатает ошибку и не выполняет оставшуюся часть программы под оператором else.

Если сервер вообще не найден (например, если http://www.pythonscraping.com недоступен или URL набран неправильно), urlopen выбросит URLError. Это указывает на то, что ни к одному серверу не удалось подключиться, и поскольку удаленный сервер отвечает за возврат HTTP-статусных кодов, не может быть выброшено HTTPError, и необходимо обрабатывать более серьезный URLError. Вы можете добавить проверку, чтобы узнать, так ли это:

from urllib.request import urlopen
from urllib.error import HTTPError
from urllib.error import URLError

try:
    html = urlopen('https://pythonscrapingthisurldoesnotexist.com')
except HTTPError as e:
    print(e)
except URLError as e:
    print('Сервер не может быть найден!')
else:
    print('Ура!')

Конечно, если страница успешно получена с сервера, все равно остается проблема с содержимым страницы, которое может не совсем соответствовать вашим ожиданиям. Всякий раз, когда вы обращаетесь к тегу в объекте BeautifulSoup, разумно добавить проверку, чтобы убедиться, что тег действительно существует. Если вы пытаетесь получить доступ к тегу, который не существует, BeautifulSoup вернет объект None. Проблема в том, что попытка доступа к тегу на объекте None приведет к выбрасыванию исключения AttributeError.

Следующая строка (где nonExistentTag — выдуманный тег, не имя реальной функции BeautifulSoup)

print(bs.nonExistentTag)

возвращает объект None. Этот объект вполне разумно обрабатывать и проверять. Проблемы возникают, если вы этого не проверяете, а вместо этого пытаетесь вызвать другую функцию для объекта None, как показано ниже:

print(bs.nonExistentTag.someTag)

Это вызывает исключение:

AttributeError: 'NoneType' object has no attribute 'someTag'

Так как же можно защититься от этих двух ситуаций? Самый простой способ — явно проверять обе ситуации:

try:
    badContent = bs.nonExistingTag.anotherTag
except AttributeError as e:
    print('Тег не был найден')
else:
    if badContent == None:
        print ('Тег не был найден')
    else:
        print(badContent)

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

from urllib.request import urlopen
from urllib.error import HTTPError
from bs4 import BeautifulSoup

def getTitle(url):
    try:
        html = urlopen(url)
    except HTTPError as e:
        return None
    try:
        bs = BeautifulSoup(html.read(), 'html.parser')
        title = bs.body.h1
    except AttributeError as e:
        return None
    return title

title = getTitle('http://www.pythonscraping.com/pages/page1.html')
if title == None:
    print('Заголовок не найден')
else:
    print(title)

Продвинутый HTML парсинг

Когда Микеланджело спросили, как ему удалось создать такое мастерское произведение, как его Давид, он, как сообщается, сказал: «Это легко. Просто отсекайте куски камня, которые не похожи на Давида.»

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

Вам не всегда нужен молоток

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

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

bs.find_all('table')[4].find_all('tr')[2].find('td').find_all('div')[1].find('a')

Это выглядит не очень хорошо. Помимо эстетики строки, даже незначительное изменение на сайте со стороны администратора может полностью сломать ваш веб-скрейпер. Что если веб-разработчик сайта решит добавить еще одну таблицу или еще один столбец данных? Что если разработчик добавит еще один компонент (с несколькими div тегами) вверху страницы? Предыдущая строка опасна и зависит от того, что структура сайта никогда не изменится.

Так какие у вас варианты?

  • Ищите ссылку «Распечатать эту страницу» или, возможно, мобильную версию сайта, которая имеет лучше отформатированный HTML (больше о том, как представить себя как мобильное устройство — и получить мобильные версии сайтов — в главе 14).
  • Ищите информацию, скрытую в файле JavaScript. Помните, что вам может понадобиться изучить импортированные файлы JavaScript, чтобы сделать это. Например, однажды я собрал адреса (вместе с широтой и долготой) с сайта в красиво отформатированный массив, посмотрев на JavaScript для встроенной карты Google, которая отображала точку над каждым адресом.
  • Это более распространено для заголовков страниц, но информация может быть доступна в URL-адресе самой страницы.
  • Если информация, которую вы ищете, уникальна для этого веб-сайта по какой-то причине, вы ничего не сможете сделать. В противном случае попробуйте подумать о других источниках, откуда можно получить эту информацию. Есть ли другой сайт с теми же данными? Этот сайт отображает данные, которые он извлек или агрегировал с другого сайта? Важно не просто начинать копать и попадать в ловушку, из которой вам может не удастся выбраться, особенно когда столкнулись с зарытыми или плохо отформатированными данными. Дышите глубже и думайте о альтернативах.

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

Еще немного о BeautifulSoup

В первой главе вы кратко рассмотрели установку и запуск BeautifulSoup, а также выбор объектов по одному. В этом разделе мы обсудим поиск тегов по атрибутам, работу с списками тегов и навигацию по деревьям разбора.

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

<span class="green"></span>

Другие могут выглядеть так:

<span class="red"></span>

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

Давайте создадим пример веб-скрейпера, который анализирует страницу, расположенную по адресу http://www.pythonscraping.com/pages/warandpeace.html.

На этой странице реплики персонажей из рассказа написаны красным цветом, а имена персонажей — зеленым. Вы можете увидеть теги span, которые ссылается на соответствующие классы CSS, в следующем фрагменте исходного кода страницы:

<span class="red">Heavens! what a virulent attack!</span> replied
<span class="green">the prince</span>, not in the least disconcerted
by this reception.

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

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('http://www.pythonscraping.com/pages/page1.html')
bs = BeautifulSoup(html.read(), 'html.parser')

Используя этот объект BeautifulSoup, вы можете использовать функцию find_all для извлечения списка существительных, найденных, выбрав только текст внутри тегов <span class=»green»></span> (функция find_all является крайне гибкой функцией, которую вы будете часто использовать позже в этой статье):

nameList = bs.findAll('span', {'class':'green'})
for name in nameList:
    print(name.get_text())

При выполнении этот код должен перечислить все существительные в тексте в том порядке, в котором они появляются в «Войне и мире». Что происходит здесь? Ранее вы вызывали bs.tagName, чтобы получить первое вхождение этого тега на странице. Теперь вы вызываете bs.find_all(tagName, tagAttributes), чтобы получить список всех тегов на странице, а не только первый. После получения списка имен программа перебирает все имена в списке и печатает name.get_text(), чтобы отделить содержимое от тегов.

Когда использовать .get_text() и когда сохранять теги

Метод .get_text() удаляет все теги из документа, с которым вы работаете, и возвращает строку Unicode, содержащую только текст. Например, если вы работаете с большим блоком текста, который содержит много гиперссылок, абзацев и других тегов, все они будут удалены, и вы получите блок текста без тегов.

Имейте в виду, что намного проще найти то, что вам нужно, в объекте BeautifulSoup, чем в блоке текста. Вызов .get_text() должен всегда быть последним действием, которое вы выполняете, непосредственно перед печатью, сохранением или обработкой ваших окончательных данных. В общем, вы должны стараться сохранить структуру тегов документа как можно дольше.

find() и find_all() с BeautifulSoup

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

Эти две функции крайне похожи, что подтверждается их определениями в документации BeautifulSoup:

find_all(tag, attributes, recursive, text, limit, keywords)
find(tag, attributes, recursive, text, keywords)

С большой вероятностью 95% времени вам нужно будет использовать только первые два аргумента: tag и attributes. Тем не менее, давайте рассмотрим все аргументы более подробно.

Аргумент tag — это то, что вы уже видели; вы можете передать строковое имя тега или даже список строковых имен тегов Python. Например, следующий код вернет список всех тегов заголовков в документе:

.find_all(['h1','h2','h3','h4','h5','h6'])

Аргумент attributes принимает словарь Python атрибутов и соответствует тегам, содержащим любой из этих атрибутов. Например, следующая функция вернет как зеленые, так и красные теги span в HTML-документе:

.find_all('span', {'class':{'green', 'red'}})

Аргумент recursive является логическим. Насколько глубоко вы хотите проникнуть в документ? Если recursive установлен в True, функция find_all ищет дочерние и дочерние дочерние теги, соответствующие вашим параметрам. Если он установлен в False, она будет искать только верхнеуровневые теги в вашем документе. По умолчанию find_all работает рекурсивно (recursive установлен в True); обычно лучше оставить это как есть, если вы действительно знаете, что вам нужно делать, и производительность имеет значение.

Аргумент text необычен тем, что он соответствует на основе текстового содержимого тегов, а не свойств тегов самих по себе. Например, если вы хотите узнать, сколько раз «the prince» окружено тегами на странице примера, вы можете заменить вашу функцию .find_all() в предыдущем примере на следующие строки:

nameList = bs.find_all(text='the prince')
print(len(nameList))

Вывод этого кода — 7.

Аргумент limit, конечно же, используется только в методе find_all; find эквивалентен тому же вызову find_all, с ограничением в 1. Вы можете установить его, если вас интересуют только первые x элементов на странице. Однако имейте в виду, что это дает вам первые элементы на странице в порядке их появления, а не обязательно первые, которые вам нужны.

Аргумент keywords позволяет выбирать теги, содержащие определенный атрибут или набор атрибутов. Например:

title = bs.find_all(id='title', class_='text')

Это возвращает первый тег с словом «text» в атрибуте class_ и «title» в атрибуте id. Обратите внимание, что по соглашению, каждое значение для id должно использоваться только один раз на странице. Поэтому на практике подобная строка может быть не особенно полезной и должна быть эквивалентна следующей:

title = bs.find(id='title')

Ключевой аргумент и класс

Аргумент ключевого слова может быть полезен в некоторых ситуациях. Однако технически он избыточен как функция BeautifulSoup. Имейте в виду, что все, что можно сделать с ключевым словом, также можно выполнить, используя техники, рассмотренные позже в этой главе (см. regular_express и lambda_express).

Например, следующие две строки идентичны:

bs.find_all(id='text')
bs.find_all('', {'id':'text'})

Кроме того, иногда возникают проблемы при использовании ключевого слова, особенно при поиске элементов по их атрибуту class, потому что class является защищенным ключевым словом в Python. То есть, class — это зарезервированное слово в Python, которое не может быть использовано в качестве имени переменной или аргумента (нет отношения к ключевому аргументу BeautifulSoup.find_all(), ранее обсуждаемому). Например, если вы попробуете следующий вызов, вы получите синтаксическую ошибку из-за нестандартного использования class:

bs.find_all(class='green')

Вместо этого вы можете использовать несколько громоздкое решение BeautifulSoup, которое включает добавление подчеркивания:

bs.find_all(class_='green')

Кроме того, вы можете заключить class в кавычки:

bs.find_all('', {'class':'green'})

На этом этапе вы, возможно, спросите себя: «Но подождите, разве я уже не знаю, как получить тег с списком атрибутов, передав атрибуты функции в виде словаря?» Напомним, что передача списка тегов в .find_all() через список атрибутов действует как «или» фильтр (она выбирает список всех тегов, у которых есть тег1 , тег2 , или тег3 …). Если у вас есть длинный список тегов, вы можете получить много ненужной информации. Аргумент ключевого слова позволяет добавить дополнительный «и» фильтр к этому.

Другие объекты BeautifulSoup

До этого в книге вы видели два типа объектов в библиотеке BeautifulSoup:

  • Объекты BeautifulSoup. Экземпляры, которые вы видели в предыдущих примерах кода под переменной bs
  • Объекты тегов. Полученные в списках или полученные индивидуально, вызывая find и find_all для объекта BeautifulSoup, или продвигаясь вниз, как показано ниже: bs.div.h1

Однако в библиотеке есть еще два объекта, которые, хотя и используются реже, все же важно знать о них:

  • Объекты NavigableString. Используются для представления текста внутри тегов, а не самих тегов (некоторые функции работают с и создают NavigableStrings, а не объекты тегов).
  • Объекты Comment. Используются для поиска HTML-комментариев в тегах комментариев, <!—как этот—>

Эти четыре объекта — единственные объекты, с которыми вы когда-либо столкнетесь в библиотеке BeautifulSoup (на момент написания этого текста).

Функция find_all отвечает за поиск тегов по их имени и атрибутам. Но что если вам нужно найти тег по его местоположению в документе? В этом случае пригодится навигация по дереву. В предыдущем разделе вы изучили навигацию по дереву BeautifulSoup в одном направлении: bs.tag.subTag.anotherSubTag

Теперь давайте рассмотрим навигацию вверх, вдоль и по диагонали через HTML-деревья. В качестве примерной страницы для извлечения информации вы будете использовать наш весьма сомнительный интернет-магазин на http://www.pythonscraping.com/pages/page3.html, как показано на рисунке.

Сомнительный магазин

HTML для этой страницы, представленный в виде дерева (с некоторыми тегами, опущенными для краткости), выглядит следующим образом:

  • HTML
    • body
      • div.wrapper
        • h1
        • div.content
          • table#giftList
            • tr
              • th
              • th
              • th
              • th
            • tr.gift#gift1
              • td
              • td
                • span.excitingNote
              • td
              • td
                • img
            • … продолжение строк таблицы …
        • div.footer

Вы будете использовать эту же структуру HTML в качестве примера в следующих нескольких разделах.

Работа с детьми в дереве тегов и другими потомками

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

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

Например, теги tr являются детьми тега table, тогда как tr, th, td, img и span все являются потомками тега table (по крайней мере, на нашей примерной странице). Все дети являются потомками, но не все потомки являются детьми.

Обычно функции BeautifulSoup всегда работают с потомками текущего выбранного тега. Например, bs.body.h1 выбирает первый тег h1, который является потомком тега body. Он не найдет теги, находящиеся за пределами тела документа.

Аналогично bs.div.find_all(‘img’) найдет первый тег div в документе, а затем извлечет список всех тегов img, которые являются потомками этого тега div.

Если вы хотите найти только потомков, которые являются детьми, вы можете использовать тег .children:

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('http://www.pythonscraping.com/pages/page3.html')
bs = BeautifulSoup(html, 'html.parser')
for child in bs.find('table',{'id':'giftList'}).children:
    print(child)

Этот код выводит список строк продуктов в таблице giftList, включая начальную строку с заголовками столбцов. Если бы вы написали его, используя функцию .descendants() вместо .children(), то было бы найдено и выведено около двух десятков тегов внутри таблицы, включая теги img, span и отдельные теги td. Очень важно различать между детьми и потомками!

Обработка соседних элементов

Функция BeautifulSoup next_siblings() позволяет легко собирать данные из таблиц, особенно из тех, которые содержат строку заголовков:

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('http://www.pythonscraping.com/pages/page3.html')
bs = BeautifulSoup(html, 'html.parser')
for sibling in bs.find('table', {'id':'giftList'}).tr.next_siblings:
    print(sibling)

Этот код выводит все строки продуктов из таблицы, кроме первой строки заголовков. Почему пропускается строка заголовка? Объекты не могут быть своими собственными соседями. Когда вы получаете соседей объекта, сам объект не будет включен в список.

Как следует из названия функции, она вызывает только следующих соседей. Если бы вы выбрали строку посередине списка и вызвали next_siblings для нее, то были бы возвращены только последующие соседи. Таким образом, выбирая строку заголовка и вызывая next_siblings, вы можете выбрать все строки в таблице, не выбирая саму строку заголовка.

Предыдущий код будет работать так же хорошо, если вы выберете bs.table.tr или даже просто bs.tr, чтобы выбрать первую строку таблицы. Однако в коде я упорно все записываю в более длинной форме:

bs.find('table',{'id':'giftList'}).tr

Даже если кажется, что на странице есть только одна таблица (или другой целевой тег), легко что-то упустить. Кроме того, макеты страниц меняются постоянно. То, что когда-то было первым своего рода на странице, может когда-то стать вторым или третьим тегом этого типа на странице. Чтобы сделать ваши скребки более надежными, лучше всего быть максимально конкретным при выборе тегов. Воспользуйтесь атрибутами тегов, когда это возможно.

В качестве дополнения к next_siblings, функция previous_siblings часто может быть полезной, если в конце списка смежных тегов есть легко выбираемый тег, который вы хотите получить. И, конечно же, есть функции next_sibling и previous_sibling, которые выполняют практически ту же функцию, что и next_siblings и previous_siblings, за исключением того, что они возвращают один тег, а не список.

Работа с родителями

При скрапинге веб-страниц вы вероятно обнаружите, что вам реже нужно находить родителей тегов, чем их детей или соседей. Обычно, когда вы изучаете HTML-страницы с целью их анализа, вы начинаете с верхнего слоя тегов, а затем выясняете, как проникнуть в нужную часть данных. Однако иногда может возникнуть необычная ситуация, в которой потребуются функции поиска родителей BeautifulSoup, .parent и .parents. Например:

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('http://www.pythonscraping.com/pages/page3.html')
bs = BeautifulSoup(html, 'html.parser')
print(bs.find('img', {'src':'../img/gifts/img1.jpg'}).parent.previous_sibling.get_text())

Этот код выведет цену объекта, представленного изображением по адресу ../img/gifts/img1.jpg (в данном случае цена составляет $15.00). Как это работает? Следующая диаграмма представляет древовидную структуру части HTML-страницы, с которой вы работаете, с пронумерованными шагами:

<tr>
— <td>
— <td>
— <td>
— "$15.00"
— <td>
— <img src="../img/gifts/img1.jpg">
  1. Сначала выбирается тег изображения с src="../img/gifts/img1.jpg".
  2. Вы выбираете родителя этого тега (в данном случае тег td).
  3. Вы выбираете предыдущего соседа тега td (в данном случае тег td, который содержит стоимость товара).
  4. Вы выбираете текст внутри этого тега, «$15.00».

Использование регулярных выражений в парсинге

Как говорится в старой шутке из мира информатики: «Представьте, что у вас есть проблема, и вы решаете ее с помощью регулярных выражений. Теперь у вас есть две проблемы.»

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

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

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

Обратите внимание, что я использовал фразу «регулярная строка». Что такое регулярная строка? Это любая строка, которая может быть сгенерирована серией линейных правил, таких как:

  1. Напишите букву a как минимум один раз.
  2. Добавьте к этому букву b ровно пять раз.
  3. Добавьте к этому букву c любое четное количество раз.
  4. Напишите в конце либо букву d, либо e.

Строки, которые соответствуют этим правилам, это например: aaaabbbbbccccd, aabbbbbcce и так далее (их бесконечное количество).

Регулярные выражения просто являются кратким способом выражения этих наборов правил. Например, вот регулярное выражение для описанных выше шагов:

aabbbbb(cc)(d|e)

Эта строка может показаться сложной сначала, но становится более понятной, когда вы разбиваете ее на компоненты:

aa* Буква a написана, за которой следует звездочка (*), что означает «любое количество a, включая 0 штук». Таким образом, вы можете гарантировать, что буква a написана хотя бы один раз.

bbbbb Здесь нет никаких особых эффектов — просто пять b подряд.

(cc)* Любое четное количество вещей может быть сгруппировано парами, поэтому, чтобы выполнить это правило о четном количестве вещей, вы можете написать две c, заключить их в скобки и добавить звездочку после этого, что означает, что у вас может быть любое количество пар c (заметьте, что это может означать и 0 пар).

(d|e) Добавление вертикальной черты между двумя выражениями означает, что это может быть «эта вещь или та вещь». В данном случае вы говорите «добавьте d или e». Таким образом, вы можете гарантировать, что здесь есть ровно один из этих двух символов.

Когда вы изучаете, как писать регулярные выражения, важно экспериментировать с ними и понять, как они работают. Если вы не хотите запускать редактор кода, писать несколько строк и запускать программу, чтобы увидеть, работает ли регулярное выражение как ожидалось, вы можете зайти на веб-сайт, такой как Regex Pal, и тестировать свои регулярные выражения на лету.

Таблица 2. Часто используемые символы регулярных выражений

Символ(ы) Значение Пример Пример соответствия
* Соответствует предшествующему символу, подвыражению или символу в квадратных скобках, 0 или более раз. ab aaaaaaaa, aaabbbbb, bbbbbb
+ Соответствует предшествующему символу, подвыражению или символу в квадратных скобках, 1 или более раз. a+b+ aaaaaaaab, aaabbbbb, abbbbbb
[] Соответствует любому символу в пределах скобок (т.е. «Выберите любой из этих элементов»). [A-Z]* APPLE, CAPITALS, QWERTY
() Группированное подвыражение (оно вычисляется первым, в «порядке операций» регулярных выражений). (ab) aaabaab, abaaab, ababaaaaab
{m, n} Соответствует предшествующему символу, подвыражению или символу в квадратных скобках от m до n раз (включительно). a{2,3}b{2,3} aabbb, aaabbb, aabb
[^] Соответствует любому одиночному символу, который не находится в скобках. [^A-Z]* apple, lowercase, qwerty
| Соответствует любому символу, строке символов или подвыражению, разделенным (обратите внимание, что это вертикальная черта, или палка, а не заглавная i). b(a
. Соответствует любому одиночному символу (включая символы, цифры, пробел и т. д.). b.d bad, bzd, b$d, b d
^ Указывает, что символ или подвыражение находится в начале строки. ^a apple, asdf, a
\ Экранирующий символ (позволяет использовать специальные символы как их буквальные значения). . | \ .
$ Часто используется в конце регулярного выражения, означает «совпадение с этим до конца строки». Без него каждое регулярное выражение имеет де факто «.*» в конце, принимая строки, в которых совпадает только первая часть строки. Это можно рассматривать как аналог символа ^.
?! «Не содержит». Эта странная пара символов, непосредственно предшествующая символу (или регулярному выражению), указывает на то, что этот символ не должен находиться в этом конкретном месте в большей строке. Это может быть сложно использовать; в конце концов, символ может быть найден в другой части строки. Если вы пытаетесь полностью исключить символ, используйте его в сочетании с ^ и $ с обеих сторон. ^((?![A-Z]).)*$ no-caps-here, $ymb0ls a4e fine

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

Правило 1 Первая часть адреса электронной почты содержит как минимум один из следующих символов: заглавные буквы, строчные буквы, цифры 0–9, точки (.), знаки плюса (+) или подчеркивания (_).

[A-Za-z0-9\._+]+

Регулярное выражение второй колонки довольно умное. Например, оно знает, что «A-Z» означает «любая заглавная буква от A до Z». Поместив все эти возможные последовательности и символы в квадратные скобки (в отличие от круглых скобок), вы говорите: «Этот символ может быть любым из перечисленных в скобках». Также обратите внимание, что знак + означает «эти символы могут встречаться столько раз, сколько им нужно, но должны встречаться как минимум один раз».

Правило 2 После этого адрес электронной почты содержит символ @.

@

Это довольно просто: символ @ должен встречаться посередине и должен встречаться ровно один раз.

Правило 3 Затем адрес электронной почты должен содержать как минимум одну заглавную или строчную букву.

[A-Za-z]+

Вы можете использовать только буквы в первой части доменного имени после символа @. Кроме того, там должен быть как минимум один символ.

Правило 4 За этим следует точка (.).

\.

Вы должны включить точку (.) перед доменным именем. Здесь используется обратный слэш как символ экранирования.

Правило 5 Наконец, адрес электронной почты завершается com, org, edu или net (на самом деле существует много возможных доменных зон, но эти четыре должны быть достаточны для примера).

(com|org|edu|net)

Здесь перечислены возможные последовательности букв, которые могут встречаться после точки во второй части адреса электронной почты. Объединив все правила, вы получите следующее регулярное выражение:

[A-Za-z0-9\._+]+@[A-Za-z]+\.(com|org|edu|net)

При попытке написать любое регулярное выражение с нуля лучше всего сначала составить список шагов, который конкретно описывает, как выглядит ваша целевая строка. Обратите внимание на крайние случаи. Например, если вы идентифицируете телефонные номера, вы учитываете коды стран и расширения?

Регулярные выражения: не всегда регулярные!

Стандартная версия регулярных выражений (та, которая рассматривается здесь и используется в Python и BeautifulSoup) основана на синтаксисе, используемом в Perl. Большинство современных языков программирования используют этот или похожий на него синтаксис. Однако имейте в виду, что если вы используете регулярные выражения на другом языке, вы можете столкнуться с проблемами. Даже некоторые современные языки, такие как Java, имеют небольшие различия в обработке регулярных выражений. Если у вас возникли сомнения, читайте документацию!

Регулярные выражения и BeautifulSoup

Если предыдущий раздел о регулярных выражениях казался немного оторванным от цели этой книги, то здесь все сводится воедино. BeautifulSoup и регулярные выражения идеально дополняют друг друга, когда речь идет о парсинге веб-страниц. Фактически, большинство функций, которые принимают строковый аргумент (например, find(id=»aTagIdHere»)), также могут принимать и регулярное выражение без проблем.

Давайте рассмотрим несколько примеров, извлекая данные со страницы по адресу http://www.python-scraping.com/pages/page3.html. Обратите внимание, что на сайте есть много изображений продуктов, которые имеют следующий формат:

<img src="../img/gifts/img3.jpg">

Если вы хотите получить URL всех изображений продуктов, сначала может показаться довольно простым: просто найдите все теги изображений, используя .find_all(«img»), верно? Но здесь есть проблема. Помимо очевидных «дополнительных» изображений (например, логотипов), на современных веб-сайтах часто есть скрытые изображения, пустые изображения, используемые для пространственного размещения элементов, а также другие случайные теги изображений, о которых вы, возможно, не знаете. Конечно, нельзя рассчитывать на то, что изображения на странице будут только изображениями продуктов.

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

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

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

html = urlopen('http://www.pythonscraping.com/pages/page3.html')
bs = BeautifulSoup(html, 'html.parser')
images = bs.find_all('img', {'src': re.compile('\.\.\/img\/gifts/img.*\.jpg')})

for image in images:
    print(image['src'])

Этот код печатает только относительные пути к изображениям, которые начинаются с ../img/gifts/img и заканчиваются на .jpg, вывод будет следующим:

../img/gifts/img1.jpg
../img/gifts/img2.jpg
../img/gifts/img3.jpg
../img/gifts/img4.jpg
../img/gifts/img6.jpg

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

Доступ к атрибутам

До этого вы изучали, как получать доступ к тегам, фильтровать их и получать содержимое внутри них. Однако часто при веб-скрапинге вам не нужно содержимое тега; вам нужны его атрибуты. Это особенно полезно для тегов, таких как a, где URL, на который он указывает, содержится в атрибуте href; или тег img, где целевое изображение содержится в атрибуте src.

С помощью объектов тегов список атрибутов на Python можно автоматически получить, вызвав это:

myTag.attrs

Имейте в виду, что это буквально возвращает объект словаря на Python, что делает получение и изменение этих атрибутов тривиальным. Например, исходное местоположение изображения можно найти с помощью следующей строки:

myImgTag.attrs['src']

Лямбда-выражения

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

По сути, лямбда-выражение — это функция, которая передается в другую функцию как переменная; вместо определения функции как f(x, y) вы можете определить функцию как f(g(x), y) или даже f(g(x), h(x)).

BeautifulSoup позволяет передавать определенные типы функций в качестве параметров в функцию find_all.

Единственное ограничение состоит в том, что эти функции должны принимать объект тега в качестве аргумента и возвращать логическое значение. Каждый объект тега, с которым сталкивается BeautifulSoup, оценивается в этой функции, и теги, которые оцениваются как True, возвращаются, в то время как остальные отбрасываются.

Например, следующий код извлекает все теги, у которых ровно два атрибута:

bs.find_all(lambda tag: len(tag.attrs) == 2)

Здесь функция, которую вы передаете в качестве аргумента, это len(tag.attrs) == 2 . Если это True , функция find_all вернет тег. То есть она найдет теги с двумя атрибутами, например:

<div class="body" id="content"></div>
<span style="color:red" class="title"></span>

Лямбда-функции настолько полезны, что вы можете даже использовать их для замены существующих функций BeautifulSoup:

bs.find_all(lambda tag: tag.get_text() == 'Or maybe he\'s only resting?')

Это также можно сделать без лямбда-функции:

bs.find_all('', text='Or maybe he\'s only resting?')

Однако, если вы помните синтаксис лямбда-функции и как получить доступ к свойствам тега, вам никогда не понадобится запоминать еще какой-либо синтаксис BeautifulSoup! Поскольку предоставленная лямбда-функция может быть любой функцией, возвращающей значение True или False, вы можете даже объединить их с регулярными выражениями, чтобы найти теги с атрибутом, соответствующим определенному шаблону строки.

Понравилась статья? Поделиться с друзьями:
Школа Виктора Комлева
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.