перейти к содержанию

Скорость парсинга веб-страниц: процессы, потоки и асинхронность

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

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

Диагностика узких мест производительности

По моему опыту, есть два основных виновника, которые ухудшают производительность веб-скрейпера:

Задачи, связанные с вводом-выводом: операции, требующие ожидания внешних ресурсов, например выполнение HTTP-запросов или получение данных из базы данных. Эти задачи блокируют выполнение кода во время ожидания ответа.

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

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

Чтобы оценить, чего не хватает вашему парсеру, используйте встроенный в Python timeit модуль для изоляции медленных частей:

import timeit

# Time a request

timeit.timeit(lambda: requests.get("http://example.com"), number=50)
# 31.23 seconds

# Time parsing
timeit.timeit(lambda: parse_html(content), number=50)  
# 22.12 seconds

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

Стратегии масштабирования парсеров Python

После того, как вы определили узкие места, вот лучшие стратегии, которые я нашел для их оптимизации:

Для задач, связанных с вводом-выводом:

  • Используйте asyncio для одновременного выполнения ввода-вывода без блокировки

Для задач, связанных с процессором:

  • Используйте многопроцессорность для распараллеливания работы между ядрами ЦП.

Python предоставляет фантастические встроенные инструменты для реализации этих подходов. Давайте обсудим их подробно:

Asyncio: параллелизм для задач, связанных с вводом-выводом

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

Рассмотрим этот синхронный парсер:

# Synchronous Scraper

import requests
import time

start = time.time()

for _ in range(50):
  requests.get("http://example.com")

end = time.time()  
print(f"Time taken: {end - start:.2f} secs")

# Time taken: 31.14 secs

Для выполнения 30 запросов требуется более 50 секунд. Большую часть этого времени мы просто праздно ждём ответов.

Теперь давайте сделаем это асинхронно с помощью asyncio:

# Asyncio Scraper

import asyncio
import httpx
import time

async def asyn_get(url):
  async with httpx.AsyncClient() as client:
    return await client.get(url)

start = time.time()

loop = asyncio.get_event_loop()
tasks = [loop.create_task(asyn_get("http://example.com")) for _ in range(50)]
wait_tasks = asyncio.wait(tasks)
loop.run_until_complete(wait_tasks)

end = time.time()
print(f"Time taken: {end - start:.2f} secs")

# Time taken: 1.14 secs

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

По моему опыту, вот несколько советов по эффективному использованию asyncio:

  • Всегда ожидайте асинхронных вызовов с помощью await
  • Используйте asyncio.gather() объединить несколько асинхронных задач
  • Создавайте задачи с loop.create_task() вместо голого async звонки
  • Оберните код синхронизации с помощью asyncio.to_thread()
  • Используйте асинхронные библиотеки, такие как httpx, для асинхронного ввода-вывода.

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

Многопроцессорность: распараллеливание рабочих нагрузок ЦП

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

Современные процессоры имеют несколько ядер, которые позволяют выполнять параллельное выполнение. Моя текущая машина имеет 8 ядер:

import multiprocessing
print(multiprocessing.cpu_count())

# 8

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

Вот пример сравнения последовательной и параллельной обработки:

# Serial Processing

import time
from slugify import slugify

start = time.time()

articles = ["Article One","Article Two",..."Article One Thousand"]

for title in articles:
  slugify(title)

print(f"Serial time: {time.time() - start:.2f} secs")

# Serial time: 5.14 sec

Это работает только на 1 ядре. Давайте распараллелим с многопроцессорностью:

# Parallel Processing 

from multiprocessing import Pool
import time
from slugify import slugify

start = time.time()

with Pool(8) as p:
  p.map(slugify, articles)

print(f"Parallel time: {time.time() - start:.2f} secs")

# Parallel time: 1.04 secs

Используя пул из 8 рабочих, мы смогли обрабатывать данные более чем в 5 раз быстрее, задействовав все доступные ядра ЦП!

Некоторые распространенные узкие места ЦП в парсерах:

  • Парсинг документов HTML/XML
  • Извлечение текста и данных с помощью Regex
  • Кодирование/декодирование очищенного мультимедиа
  • Сканирование и обработка файлов Sitemap
  • Сжатие очищенных данных

Многопроцессорность позволяет легко распараллеливать эти задачи, чтобы значительно сократить время обработки.

Сочетание Asyncio и многопроцессорности

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

Вот шаблон, который работает очень хорошо:

  1. Создать async_scrape() функция, которая обрабатывает работу, связанную с вводом-выводом, например выполнение запросов с использованием asyncio.

  2. Позвонить async_scrape() из многопроцессорного пула для параллельного запуска на нескольких ядрах.

Это позволяет максимизировать параллелизм ввода-вывода и процессора!

Вот пример:

import asyncio
from multiprocessing import Pool
import httpx
import time

async def async_scrape(urls):

  async with httpx.AsyncClient() as client:

    tasks = [client.get(url) for url in urls]
    results = await asyncio.gather(*tasks)

    # CPU-heavy processing
    for data in results:
      analyze_data(data)

def multiproc_wrapper(urls):
  asyncio.run(async_scrape(urls))

if __name__ == "__main__":

  urls = [# List of urls

  start = time.time()  

  with Pool(8) as p:
    p.map(multiproc_wrapper, batched_urls)

  print(f"Total time: {time.time() - start:.2f} secs")

Мы группируем URL-адреса в группы, очищаем их одновременно с помощью asyncio, используя async_scrape()и обрабатывать пакеты параллельно, используя многопроцессорный пул.

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

Сравнение вариантов масштабирования

Подводя итог, вот обзор различных вариантов параллелизма в Python:

ПодходФорсировочнаяКейсыНакладные расходы
многопроцессорная обработкаОчень высокоЗадачи, связанные с процессоромHigh
многопоточностьУмереннаяЗадачи, связанные с вводом-выводомНизкий
АсинкиоОчень высокоЗадачи, связанные с вводом-выводомНизкий

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

Многопроцессорность обеспечивает превосходный параллелизм для рабочих нагрузок, связанных с ЦП, с ускорением в 8–10 раз на 8-ядерном компьютере.

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

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

Сравнительный анализ производительности Asyncio

Чтобы продемонстрировать высокую производительность asyncio, я сравнил синхронное и асинхронное сканирование 1,000 URL-адресов на моем компьютере:

Синхронный:

1000 URLs scraped sequentially
Total time: 63.412 seconds

Асинкио:

1000 URLs scraped asynchronously 
Total time: 1.224 seconds

Это более чем в 50 раз быстрее при той же рабочей нагрузке!

Фактически, тесты показывают, что asyncio может обрабатывать тысячи запросов в секунду в одном потоке.

Вот таблица тестов asyncIO от превосходного httpx-библиотека:

РамкиЗапросов/сек
Асинкио15,500
Gevent14,000
Торнадо12,500

Как видите, asyncio обеспечивает невероятную пропускную способность для операций ввода-вывода.

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

Использование сервисов парсинга

Теперь, когда вы понимаете такие методы, как асинхронность и многопроцессорность, вы можете задаться вопросом: стоит ли создавать все это самостоятельно?

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

Эти услуги возьмут на себя всю тяжелую работу по масштабированию и оптимизации. Вот некоторые преимущества:

Параллелизм и скорость

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

Управление прокси

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

Повторные попытки и аварийное переключение

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

Облачная масштабируемость

API-интерфейсы парсинга могут мгновенно масштабироваться в соответствии с потребностями без каких-либо инженерных работ с вашей стороны.

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

Основные выводы

Вот основные методы, которые я рассмотрел для оптимизации производительности парсинга веб-страниц в Python:

  • Определите узкие места: Профилируйте свой парсер, чтобы изолировать медленные задачи ввода-вывода от задач ЦП.

  • Оптимизация ввода-вывода с помощью asyncio: используйте библиотеки asyncio и async, чтобы исключить ожидание запросов.

  • Распараллелить работу процессора: использовать многопроцессорность для распределения обработки данных по всем ядрам ЦП.

  • Объедините их: Asyncio для ввода-вывода и многопроцессорность для ЦП работают очень хорошо вместе.

  • Рассмотрите возможность парсинга API: такие сервисы, как ScraperAPI и Scrapfly, обеспечат оптимизацию за вас.

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

Дайте мне знать, если у вас есть еще вопросы! Я всегда рад помочь коллегам-разработчикам реализовать эти методы параллелизма.

Теги:

Присоединяйтесь к беседе

Ваш электронный адрес не будет опубликован. Обязательные поля помечены * *