Как эксперт по парсингу веб-страниц с более чем 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 и многопроцессорности
Для достижения наилучшей производительности я рекомендую сочетать в парсерах как асинхронную, так и многопроцессорную обработку.
Вот шаблон, который работает очень хорошо:
Создать
async_scrape()
функция, которая обрабатывает работу, связанную с вводом-выводом, например выполнение запросов с использованием asyncio.Позвонить
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 |
Gevent | 14,000 |
Торнадо | 12,500 |
Как видите, asyncio обеспечивает невероятную пропускную способность для операций ввода-вывода.
Поэтому используйте его для любых рабочих процессов с большим количеством операций ввода-вывода, таких как выполнение одновременных запросов или чтение файлов в парсерах.
Использование сервисов парсинга
Теперь, когда вы понимаете такие методы, как асинхронность и многопроцессорность, вы можете задаться вопросом: стоит ли создавать все это самостоятельно?
Во многих случаях я бы рекомендовал рассмотреть возможность использования API-сервиса веб-скрапинга, такого как СкребокAPI or Скрапфлай.
Эти услуги возьмут на себя всю тяжелую работу по масштабированию и оптимизации. Вот некоторые преимущества:
Параллелизм и скорость
Такие сервисы, как ScraperAPI и Scrapfly, имеют оптимизированную инфраструктуру, предназначенную для максимального параллелизма. Просто передайте список URL-адресов, и их системы обработают их запросы с невероятной скоростью.
Управление прокси
Службы парсинга предоставляют доступ к тысячам прокси-серверов, чтобы избежать блокировок и обнаружения ботов. Настройка и ротация прокси абстрагированы.
Повторные попытки и аварийное переключение
Службы автоматически повторяют неудачные запросы и при необходимости переключаются на новые прокси-серверы, гарантируя, что вы получите данные.
Облачная масштабируемость
API-интерфейсы парсинга могут мгновенно масштабироваться в соответствии с потребностями без каких-либо инженерных работ с вашей стороны.
Поэтому во многих случаях может быть предпочтительнее использовать специальный API для парсинга и сосредоточить свои усилия на других областях.
Основные выводы
Вот основные методы, которые я рассмотрел для оптимизации производительности парсинга веб-страниц в Python:
Определите узкие места: Профилируйте свой парсер, чтобы изолировать медленные задачи ввода-вывода от задач ЦП.
Оптимизация ввода-вывода с помощью asyncio: используйте библиотеки asyncio и async, чтобы исключить ожидание запросов.
Распараллелить работу процессора: использовать многопроцессорность для распределения обработки данных по всем ядрам ЦП.
Объедините их: Asyncio для ввода-вывода и многопроцессорность для ЦП работают очень хорошо вместе.
Рассмотрите возможность парсинга API: такие сервисы, как ScraperAPI и Scrapfly, обеспечат оптимизацию за вас.
Используя эти подходы, вы можете ускорить работу парсеров на порядки. Asyncio и многопроцессорность — ваши лучшие друзья для высокопроизводительного парсинга Python.
Дайте мне знать, если у вас есть еще вопросы! Я всегда рад помочь коллегам-разработчикам реализовать эти методы параллелизма.