CoderCastrov logo
CoderCastrov
Питон

Параллельный веб-парсинг на Python

Параллельный веб-парсинг на Python
просмотров
5 мин чтение
#Питон

Часть II

В этой второй части мы рассмотрим, как реализовать простой веб-парсинг с использованием Selenium или Beautiful Soup для параллельного выполнения в Python. Мы также измерим увеличение производительности при выполнении параллельного парсинга по сравнению с последовательным, чтобы вы могли увидеть, сколько времени вы экономите при создании нового набора данных.


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

Скрипт парсинга

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

Мы будем использовать четыре разные библиотеки:

  • time: для ожидания загрузки сайта динамической информации перед взаимодействием с ней
  • random: для имитации "человеческого поведения", время ожидания будет случайным числом из равномерного распределения
  • CSV: для простоты, отзывы, которые мы собираемся собирать, будут сохранены в CSV-файле
  • selenium: для выполнения парсинга
import time
import csv
import random
from selenium import webdriver#Selenium options
options = webdriver.ChromeOptions()
options.add_argument("--ignore-certificate-errors")
options.add_argument("--incognito")
options.add_argument("--headless")options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")

Наш простой парсинг выполняет 2 разных действия для получения нужной информации. Сначала он переходит на страницу и пытается нажать кнопку "Загрузить еще", чтобы загрузить больше отзывов на той же странице. После нажатия кнопки он ожидает случайное время от 1 до 3 секунд. Наконец, он пытается найти и снова нажать кнопку, пока все отзывы не будут показаны на веб-сайте.

driver = webdriver.Chrome(executable_path="chromedriver", options=options)driver.get(_[url]_) #define the url of the site you want to scrape

while True:
    try:
        driver.find_element_by_id("load-more-trigger").click()
        time.sleep(random.randint(1, 3))
    except Exception as e:
        print(e)
        break

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

test = driver.find_elements_by_css_selector("div.text.show-more__control")
reviews = [[id] + [c.text] for c in test]#save reviews to csv file
with open("all_reviews.csv", mode="a") as f1:
    review_writer = csv.writer(f1, delimiter=",")
    for r in reviews:
        review_writer.writerow(r)

Множественные потоки

Загрузка необходимых библиотек

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

from concurrent.futures import ThreadPoolExecutor

Нам также нужно импортировать пакет threading. Этот пакет позволит нам блокировать несколько потоков при сохранении информации в наш CSV-файл. Таким образом, мы предотвращаем одновременное использование несколькими процессами одного и того же файла.

import threading
def get_reviews(url, id):
    driver = webdriver.Chrome(executable_path="chromedriver",
                              options=options)
    driver.get(url) # определите URL сайта, который вы хотите спарсить
    while True:
        try:
            driver.find_element_by_id("load-more-trigger").click()
            time.sleep(random.randint(1, 3))
        except Exception as e:
            print(e)
            breaktest = driver.find_elements_by_css_selector("div.text.show-more__control")
    reviews = [[id] + [c.text] for c in test]
    print(id, len(reviews))
    with csv_writer_lock:
        with open("all_reviews.csv", mode="a") as f1:
            review_writer = csv.writer(f1, delimiter=",")
            for r in reviews:
                review_writer.writerow(r)
    return pd.DataFrame(reviews)

Вызов функции параллельного парсинга

Теперь давайте рассмотрим, как все это объединяется для ускорения нашего процесса. Чтобы запустить параллельно, нам нужно сопоставить функцию парсинга, которую мы создали, с списком URL-адресов, которые мы хотим спарсить. Это означает, что каждому из рабочих потоков/процессов будет назначен отдельный процесс. В этом случае мы используем пакет concurrent.futures. С помощью метода map класса ThreadPoolExecutor мы можем достичь этого.

def set_up_threads(urls):
    with ThreadPoolExecutor(max_workers=5) as executor:
        return executor.map(get_reviews,    
                            urls["URLS"],
                            urls["movieId"],
                            timeout = 60)

Важно заметить, что в ThreadPoolExecutor необходимо определить максимальное количество рабочих потоков или процессов, для этого мы используем атрибут max_workers. В приведенном примере мы используем 5 потоков, но это число будет зависеть от аппаратного обеспечения, на котором вы запускаете код.

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

if __name__ == "__main__":
    # read and generate urls
    review_list = pd.read_csv("movie_list_url.csv")
    set_up_threads(review_list)

Измерение производительности

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

Используя ранее определенный код, мы проведем небольшой тест, чтобы увидеть, как эти параллельные соединения улучшают скорость загрузки. Для этого теста мы используем 4 различные конфигурации: 1, 3, 5 и 7 потоков.

Для теста мы запускаем каждую конфигурацию в 3 разных сценариях, чтобы проверить, как объем влияет на производительность. Эти сценарии основаны на количестве фильмов для парсинга: 50, 100, 500. В каждом сценарии мы запускаем его 5 раз и берем среднее время, затраченное на парсинг всех отзывов со всех этих фильмов. (ВНИМАНИЕ: если вы хотите запустить тест с использованием кода на GitHub, это займет много времени!)

Результаты показаны на графике ниже.

Заключение

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


Вы можете получить доступ к коду эксперимента по этой ссылке на GitHub.

Если вы пропустили Часть I, о параллельном парсинге в R, воспользуйтесь следующей ссылкой.