Парсинг на Python: все, что нужно знать в 2022 году

Почти все инструменты, которые предлагает Python для парсинга данных в Интернете

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

Основы Web

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

Протокол передачи гипертекста

Протокол передачи гипертекста (HTTP) использует модель клиент/сервер. HTTP-клиент (браузер, ваша программа Python, cURL, библиотеки, такие как Requests...) открывает соединение и отправляет сообщение («Я хочу увидеть эту страницу: /product») на HTTP-сервер (Nginx, Apache...). Затем сервер отвечает ответом (например, HTML-код) и закрывает соединение.

HTTP называется протоколом без состояния, потому что каждая транзакция (запрос/ответ) независима. FTP, например, имеет состояние, потому что он поддерживает соединение.

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

GET /product/ HTTP/1.1
Host: example.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 12_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
В первой строке этого запроса вы можете увидеть следующее:
  • Метод или глагол HTTP. В нашем случае GET, указывая, что мы хотели бы получить данные. Существует довольно много других методов HTTP (например, для загрузки данных), и полный список доступен здесь.
  • Путь к файлу, каталогу или объекту, с которым мы хотели бы взаимодействовать. В данном случае каталог продукта находится прямо под корневым каталогом.
  • Версия
  • протокола HTTP. В этом запросе у нас HTTP 1.
  • Несколько полей заголовка: Подключение, Пользователь-агент... Вот исчерпывающий список HTTP-заголовков
Вот наиболее важные поля заголовка:
  • Хост: В этом заголовке указано имя хоста, для которого вы отправляете запрос. Этот заголовок особенно важен для виртуального хостинга на основе имен, который является стандартом в современном мире хостинга.
  • User-Agent: Содержит информацию о клиенте, инициировавшим запрос, включая ОС. В данном случае это веб-браузер (Chrome) на macOS. Этот заголовок важен, потому что он либо используется для статистики (сколько пользователей посещают веб-сайт на мобильном телефоне или десктопе), либо для предотвращения нарушений со стороны ботов. Поскольку эти заголовки отправляются клиентами, они могут быть изменены ("Спуфинг заголовка"). Это именно то, что мы будем делать с нашими парсерами - делаем парсеры похожими на обычный веб-браузер.
  • Accept: Это список типов MIME, которые клиент примет в качестве ответа от сервера. Существует множество различных типов контента и подтипов: text/plain, text/html, image/jpeg, application/json ...
  • Cookie: Это поле заголовка содержит список пар имя-значение (name1=value1;name2=value2). Файлы cookie - это один из способов, благодаря которым веб-сайты могут хранить данные на вашем компьютере. Способ позволяет хранить либо до определенной даты истечения срока действия (стандартные файлы cookie), либо только временно до закрытия браузера (сеансовые файлы cookie). Файлы cookie используются для различных целей, начиная от информации об аутентификации и заканчивая предпочтениями пользователя и более гнусными вещами, такими как отслеживание пользователей с помощью персонализированных уникальных идентификаторов пользователей. Тем не менее, они являются жизненно важной функцией браузера для указанной аутентификации. Когда вы отправляете форму входа в систему, сервер проверит ваши учетные данные и, если вы предоставили действительный логин, выдаст сеансовый файл cookie, который четко идентифицирует сеанс пользователя для вашей конкретной учетной записи пользователя. Ваш браузер получит этот файл cookie и передаст его вместе со всеми последующими запросами.
  • Referer: Заголовок реферера (обратите внимание на опечатку) содержит URL-адрес, с которого был запрошен фактический URL-адрес. Этот заголовок важен, потому что веб-сайты используют этот заголовок для изменения своего поведения в зависимости от того, откуда пришел пользователь. Например, многие новостные сайты имеют платную подписку и позволяют просматривать только 10% сообщения, но если пользователь приходит из агрегатора новостей, такого как Reddit, они позволяют просматривать полный контент. Они используют реферер, чтобы проверить это. Иногда нам придется подделать этот заголовок, чтобы добраться до контента, который мы хотим извлечь.
Список можно продолжать... вы можете найти полный список заголовков здесь.

Сервер ответит примерно так:
HTTP/1.1 200 OK
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html; charset=utf-8
Content-Length: 3352

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" /> ...[HTML CODE]
В первой строке у нас есть новая информация, HTTP-код 200 OK. Код 200 означает, что запрос был правильно обработан. Вы можете найти полный список всех доступных кодов в Википедии. Следуя строке состояния, у вас есть заголовки ответов, которые служат той же цели, что и заголовки запроса, которые мы только что обсудили. После заголовков ответа у вас будет пустая строка, за которой следуют фактические данные, отправленные вместе с этим ответом.
Как только ваш браузер получит этот ответ, он разберет HTML-код, изберет все встроенные ресурсы (файлы JavaScript и CSS, изображения, видео) и отобразит результат в главном окне.
Мы рассмотрим различные способы выполнения HTTP-запросов с помощью Python и извлечем нужные нам данные из ответов.

Ручное открытие сокета и отправка HTTP-запроса

Socket

Самый простой способ выполнить HTTP-запрос на Python - открыть TCP-сокет и вручную отправить HTTP-запрос.

import socket

HOST = 'www.google.com'  # Server hostname or IP address
PORT = 80                # The standard port for HTTP is 80, for HTTPS it is 443

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = (HOST, PORT)
client_socket.connect(server_address)

request_header = b'GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n'
client_socket.sendall(request_header)

response = ''
while True:
    recv = client_socket.recv(1024)
    if not recv:
        break
    response += str(recv)

print(response)
client_socket.close()  
Теперь, когда у нас есть HTTP-ответ, самый простой способ извлечь из него данные - использовать регулярные выражения.

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

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

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

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

В идеальном семантическом мире данные легко читаются роботами, а информация встроена в соответствующие элементы HTML со значимыми атрибутами. Но реальный мир не идеален. Вы часто найдете огромное количество текста внутри элемента <p>. Например, если вы хотите извлечь конкретные данные внутри большого текста (цена, дата, имя...), вам придется использовать регулярные выражения.
Примечание: Вот отличный веб-сайт для тестирования регулярных выражений: https://regex101.com/. Кроме того, вот потрясающий блог, чтобы узнать о них больше. Эта статья будет охватывать лишь небольшую часть того, что вы можете сделать с помощью регулярных выражений.
Регулярные выражения могут быть полезны, когда у вас есть такие данные:
<p>Цена : 2000.00 руб.</p>
Мы могли бы выбрать этот текстовый узел с выражением XPath, а затем использовать этот вид регулярных выражений для извлечения цены:
^Цена\s*:\s*(\d+\.\d{2})\$
Если у вас есть только HTML, это немного сложнее, но не намного больше. Вы также можете просто указать в своем выражении тег, а затем использовать группу захвата для текста.
import re

html_content = '<p>Цена : 2000.00 руб.</p>'

m = re.match('<p>(.+)<\/p>', html_content)
if m:
	print(m.group(1))
Как видите, можно вручную отправить HTTP-запрос с сокетом и разобрать ответ с помощью регулярного выражения, но это сложно, т.к. есть API более высокого уровня, который может упростить эту задачу.

urllib3 и LXML

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

import urllib3

http = urllib3.PoolManager()
r = http.request('GET', 'http://www.google.com')
print(r.data)
Как видите, это гораздо более лаконично, чем версия с сокетом. Мало того, API прост. Кроме того, вы можете легко сделать много других вещей, таких как добавление HTTP-заголовков, использование прокси, POST-формы...

Например, если бы мы решили установить некоторые заголовки и использовать прокси-сервер, нам нужно было бы сделать только следующее:
import urllib3

user_agent_header = urllib3.make_headers(user_agent="<USER AGENT>")
pool = urllib3.ProxyManager(f'<PROXY IP>', headers=user_agent_header)
r = pool.request('GET', 'https://www.google.com/')
В коде точно такое же количество строк. Тем не менее, есть некоторые вещи, с которыми urllib3 работает не очень легко. Например, если мы хотим добавить файл cookie, мы должны вручную создать соответствующие заголовки и добавить его в запрос.

Есть также вещи, с которыми urllib3 не может работать: например, создание и управление пулом прокси, а также управлением стратегией повторных попыток.
Проще говоря, urllib3 находится между запросами и сокетом с точки зрения абстракции, хотя он намного ближе к запросам, чем к сокету.
Затем, чтобы разобрать ответ, мы будем использовать пакет LXML и выражения XPath.

XPath

XPath - это технология, которая использует выражения путей для выбора узлов или наборов узлов в XML-документе (или HTML-документе). Если вы знакомы с концепцией селекторов CSS, то вы можете представить ее как что-то относительно похожее.

Как и в случае с объектной моделью документа, XPath является стандартом W3C с 1999 года. Хотя XPath сам по себе не является языком программирования, он позволяет писать выражения, которые могут напрямую получить доступ к определенному узлу или определенному набору узлов, без необходимости проходить через все HTML-дерево (или XML-дерево).

Чтобы извлечь данные из HTML-документа с помощью XPath, нам нужно три вещи:

  • HTML-документ
  • некоторые выражения XPath
  • движок XPath, который будет запускать эти выражения

Для начала мы будем использовать HTML, который мы получили от urllib3. И теперь мы хотели бы извлечь все ссылки с домашней страницы Google. Итак, мы будем использовать одно простое выражение XPath, //a, и мы будем использовать LXML для его запуска. LXML - это быстрая и простая в использовании библиотека обработки XML и HTML, поддерживающая XPath.

Установка:
pip install lxml
Далее пишем код:
from lxml import html

# We reuse the response from urllib3
data_string = r.data.decode('utf-8', errors='ignore')

# We instantiate a tree object from the HTML
tree = html.fromstring(data_string)

# We run the XPath against this HTML
# This returns an array of element
links = tree.xpath('//a')

for link in links:
    # For each element we can easily get back the URL
    print(link.get('href'))
И результат его работы должен выглядеть так:
https://books.google.fr/bkshp?hl=fr&tab=wp
https://www.google.fr/shopping?hl=fr&source=og&tab=wf
https://www.blogger.com/?tab=wj
https://photos.google.com/?tab=wq&pageId=none
http://video.google.fr/?hl=fr&tab=wv
https://docs.google.com/document/?usp=docs_alc
...
https://www.google.fr/intl/fr/about/products?tab=wh
Имейте в виду, что этот пример действительно очень прост и не показывает вам, насколько мощным может быть XPath (Примечание: мы также могли бы использовать //a/@href, чтобы указать прямо на атрибут href). Если вы хотите узнать больше о XPath, вы можете прочитать это полезное введение. Документация LXML также хорошо написана и является хорошей отправной точкой.

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

Requests и BeautifulSoup

Requests

Requests является королем пакетов Python. С более чем 11 000 000 загрузок, это наиболее широко используемый пакет.
pip install requests
Чтобы сделать запрос, напишем:
import requests

r = requests.get('https://www.scrapingninja.co')
print(r.text)
С помощью requests легко выполнять POST-запросы, обрабатывать файлы cookie, параметры запросов, загружать изображения, использовать запросы с прокси. Прокси почти обязательны для парсинга большого количества страниц.

BeautifulSoup

Следующее, что нам понадобится, это BeautifulSoup, библиотека Python, которая поможет нам разобрать HTML, возвращенный сервером.
Установка:
pip install beautifulsoup4
Ниже представлен пример использования библиотеки:
import psycopg2
import requests
from bs4 import BeautifulSoup

# Establish database connection
con = psycopg2.connect(host="127.0.0.1",
                       port="5432",
                       user="postgres",
                       password="",
                       database="scrape_demo")

# Get a database cursor
cur = con.cursor()

r = requests.get('https://news.ycombinator.com')
soup = BeautifulSoup(r.text, 'html.parser')
links = soup.findAll('tr', class_='athing')

for link in links:
	cur.execute("""
		INSERT INTO hn_links (id, title, url, rank)
		VALUES (%s, %s, %s, %s)
		""",
		(
			link['id'],
			link.find_all('td')[2].a.text,
			link.find_all('td')[2].a['href'],
			int(link.find_all('td')[0].span.text.replace('.', ''))
		)
	)

# Commit the data
con.commit();

# Close our database connections
cur.close()
con.close()
Requests и BeautifulSoup - отличные библиотеки для извлечения данных и автоматизации различных действий.

GRequests

Хотя пакет Requests прост в использовании, но при запросе большого количества URL, он будет работать медленно. Из коробки пакет позволяет отправлять только синхронные запросы, а это означает, что если у вас есть 25000 URL-адресов для парсинга, то придется делать запросы один за другим.
Поэтому, если для получения одной страницы требуется две секунды, то для 25 000 потребуется 834 минуты или чуть менее 14 часов.
import requests

# An array with 25 000 urls
urls = [...] 

for url in urls:
    result = requests.get(url)
Самый простой способ ускорить процесс - сделать несколько запросов одновременно. Это означает, что вместо того, чтобы отправлять каждый запрос последовательно, вы можете отправлять запросы партиями по пять штук параллельно.

В этом случае каждый пакет будет обрабатывать пять URL-адресов одновременно, что означает, что вы спарсите 5 URL-адресов за 2 секунды вместо 10 или весь набор из 25 000 URL-адресов за 167 минут или чуть менее 3 часов. Неплохо для экономии времени.

Есть версия пакета Requests, которая отправляет запросы параллельно - GRequests. Он основан на запросах, но также включает в себя gevent, асинхронный API Python, широко используемый для веб-приложений. Эта библиотека позволяет нам отправлять несколько запросов одновременно и простым и элегантным способом.

Для начала давайте установим GRequests:
pip install grequests
Чтобы отправить наши 25 000 url-адресов параллельно по 5 штук, напишем такой код:
import grequests

BATCH_LENGTH = 5

# An array with 25000 urls
urls = [...] 
# Our empty results array
results = []

while urls:
    # get our first batch of 5 URLs
    batch = urls[:BATCH_LENGTH]
    # create a set of unsent Requests
    rs = (grequests.get(url) for url in batch)
    # send them all at the same time
    batch_results = grequests.map(rs)
    # appending results to our main results array
    results += batch_results
    # removing fetched URLs from urls
    urls = urls[BATCH_LENGTH:]

print(results)
# [<Response [200]>, <Response [200]>, ..., <Response [200]>, <Response [200]>]
И все. GRequests идеально подходит для небольших скриптов, но менее подходит для производственного кода или крупномасштабного сбора данных с сайтов. Для этого у нас есть Scrapy.

Фреймворк для парсинга

Scrapy

Scrapy - это мощная платформа для автоматизированного парсинга и сканирования веб-страниц на Python. Она предоставляет множество функций для асинхронной загрузки веб-страниц, а также для обработки и сохранения, полученного контента. Также обеспечивает поддержку многопоточности, сканирования (процесс перехода от ссылки к ссылке, чтобы найти каждый URL-адрес на веб-сайте), карты сайта и др.

Scrapy также имеет интерактивный режим под названием Scrapy Shell. С помощью Scrapy Shell вы можете быстро протестировать свой код и убедиться, что все ваши XPath expressions или селекторы CSS работают так, как было задуманно. Недостатком Scrapy является то, что библиотека достаточно сложная и нужно в ней разобраться.

Напишем Scrapy Spider, который парсит первые 15 url-адресов результатов и сохраняет все в CSV-файле.

Чтобы установить Scrapy с помощью pip:

pip install Scrapy
Затем воспользуемя Scrapy CLI для создания шаблонного кода проекта:
scrapy startproject hacker_news_scraper
Внутри hacker_news_scraper/spider мы создадим новый файл Python с кодом нашего паука:
from bs4 import BeautifulSoup
import scrapy


class HnSpider(scrapy.Spider):
    name = "hacker-news"
    allowed_domains = ["news.ycombinator.com"]
    start_urls = [f'https://news.ycombinator.com/news?p={i}' for i in range(1,16)]

    def parse(self, response):
        soup = BeautifulSoup(response.text, 'html.parser')
        links = soup.findAll('tr', class_='athing')

        for link in links:
        	yield {
        		'id': link['id'],
        		'title': link.find_all('td')[2].a.text,
        		"url": link.find_all('td')[2].a['href'],
        		"rank": int(link.td.span.text.replace('.', ''))
        	}
Сначала мы добавим все нужные URL-адреса в start_urls. Затем Scrapy извлечет каждый URL-адрес и вызовет синтаксический анализ для каждого из них, где мы будем использовать наш собственный код для анализа ответа.

Затем нам нужно немного настроить Scrapy, чтобы наш паук хорошо вел себя с целевым веб-сайтом.
# Enable and configure the AutoThrottle extension (disabled by default)
# See https://doc.scrapy.org/en/latest/topics/autothrottle.html
AUTOTHROTTLE_ENABLED = True

# The initial download delay
AUTOTHROTTLE_START_DELAY = 5
В зависимости от времени отклика эта функция автоматически регулирует частоту запросов и количество параллельных потоков и гарантирует, что ваш паук не наводняет веб-сайт запросами.

Вы можете запустить этот код с помощью Scrapy CLI и с различными форматами вывода (CSV, JSON, XML...):
scrapy crawl hacker-news -o links.json
Далее, после окончания работы скрипта, получим все ваши ссылки в красиво отформатированном файле JSON.

Headless браузеры

Selenium & Chrome

Scrapy отлично подходит для крупномасштабного парсинга. Но он плохо подходит для работы с сайтами, которые активно используют JavaScript, например, SPA (одностраничное приложение). Scrapy не обрабатывает JavaScript самостоятельно и предоставляет вам только статический HTML-код.

Как правило, SPA бывает сложно спарсить, потому что требуется много вызовов AJAX и соединений WebSocket. Если производительность становится низкой, всегда проверяйте, что именно делает код JavaScript. Для этого требуется ручная проверка всех сетевых вызовов с помощью инспектора браузера и репликация вызовов AJAX, содержащих интересующие данные.

Однако часто требуется слишком много HTTP-вызовов, чтобы получить нужные данные, и может быть проще отобразить страницу в headless-браузере. Также он позволяет делать скриншоты страниц.
Три наиболее распространенных случая, когда нужен Selenium:

  1. Вы ищете информацию, которая появляется через несколько секунд после загрузки веб-страницы в браузере.
  2. Веб-сайт, который вы пытаетесь спарсить, использует много JavaScript.
  3. На веб-сайте, который вы пытаетесь спарсить, есть проверка JavaScript, чтобы заблокировать "классический" HTTP-клиент.
Чтобы установить пакет Selenium с помощью pip:
pip install selenium
Вам также понадобится ChromeDriver. На Mac OS вы можете использовать brew для этого.
brew install chromedriver
Затем нужно импортировать Webdriver из пакета Selenium, настроить Chrome с headless=True, установить размер окна, запустить Chrome, загрузить страницу и, наконец, получить наш красивый скриншот:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.headless = True
options.add_argument("--window-size=1920,1200")

driver = webdriver.Chrome(options=options, executable_path=r'/usr/local/bin/chromedriver')
driver.get("https://news.ycombinator.com/")
driver.save_screenshot('hn_homepage.png')
driver.quit()
Selenium API и Chrome позволяет осуществлять:
  • запуск JavaScript;
  • заполнение форм;
  • нажатие на элементы;
  • извлечение элементов с помощью селекторов CSS / выражений XPath
Selenium и Chrome в режиме headless - это идеальная комбинация для парсинга всего, что вы хотите. Вы можете автоматизировать все, что вы можете сделать с помощью обычного браузера Chrome.

Большим недостатком является то, что Chrome требуется много памяти / мощности процессора. При некоторой тонкой настройке вы можете уменьшить объем памяти до 300-400 МБ на поток Chrome, но вам все равно нужно 1 ядро процессора на поток.

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

RoboBrowser

RoboBrowser - это библиотека Python, которая объединяет requests и BeautifulSoup в единый и простой в использовании пакет и позволяет создавать собственные пользовательские скрипты. Это легкая библиотека, но не безголовый браузер и имеет те же ограничения, что и requests и BeautifulSoup, о которых мы обсуждали выше.

Например, если вы хотите войти в Hacker-News, вместо того, чтобы вручную создавать запрос для requests, вы можете написать скрипт, который заполнит форму, и нажать кнопку входа в систему:
# pip install RoboBrowser
from robobrowser import RoboBrowser

browser = RoboBrowser()
browser.open('https://news.ycombinator.com/login')

# Get the signup form
signin_form = browser.get_form(action='login')

# Fill it out
signin_form['acct'].value = 'account'
signin_form['password'].value = 'secret'

# Submit the form
browser.submit_form(signin_form)
Как видите, код написан так, как если бы вы вручную выполняли задачу в реальном браузере, даже если это не настоящая headless-библиотека.

RoboBrowser позволяет легко распараллеливать его на вашем компьютере. Однако, поскольку он не использует настоящий браузер, он не сможет работать с JavaScript, AJAX, SPA.

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

Нужен парсинг сайтов?