Работа с потоками в Python

26.04.2018

Теги: PythonWeb-разработкаМодульПарсингПоток

Поток позволяет запустить часть длинного кода так, как если бы он был отдельной программой. Это своего рода вызов наследуемого процесса, только вместо запуска отдельной программы, происходит вызов функции. Модуль threading значительно упрощает работу с потоками и позволяет программировать запуск нескольких операций одновременно.

import threading, time

def wakeUp():
    time.sleep(5)
    print('Подъем!')

print('Начало программы')

thread = threading.Thread(target = wakeUp)
thread.start()

print('Конец программы')
Начало программы
Конец программы
..........
Подъем!

Здесь мы определяем функцию wakeUp(), которая будет выполняться в новом потоке. Для этого мы создаем экземпляр класса Thread и передаем конструктору именованный аргумент target=wakeUp. Это означает, что функцией, которую мы хотим вызвать в новом потоке, является функция wakeUp(). Обратите внимание, что именованный аргумент записывается как target=wakeUp, а не как target=wakeUp(). Это обусловлено тем, что в качестве аргумента мы хотим передать функцию wakeUp как таковую, а не результат ее выполнения.

Если целевая функция, которую надо выполнить в отдельном потоке, принимает аргументы, то можно передать их при вызове threading.Thread(). Допустим, нам надо выполнить вызов функции print() в отдельном потоке:

>>> print('Cats', 'Dogs', sep = ' & ')
Cats & Dogs

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

thread = threading.Thread(target = print, args = ['Cats', 'Dogs'], kwargs = {'sep':' & '})
thread.start()

Пример парсинга сайта с использованием потоков:

# Скачивает фотографии товаров каталога с сайта http://www.tinko.info/ и
# сохраняет их в директорию под именами, совпадающими с артикулом товара

import requests, bs4, re, os, threading

def getImages(url):
    """
    Функция получает на вход URL страницы списка товаров, скачивает
    эту страницу, извлекает из нее ссылки на страницы товаров, и уже
    со страниц товаров скачивает фотографии
    """
    res = requests.get(url)
    soup = bs4.BeautifulSoup(res.text, "html.parser")
    # получаем ссылки на страницы товаров
    items = soup.select('.product-list-heading > h2 > a')
    # получаем в цикле страницы товаров и извлекаем
    # из них фотографии
    for item in items:
        # получаем страницу товара
        href = item.attrs['href']
        res = requests.get(href)
        # ищем URL фото и артикул товара
        soup = bs4.BeautifulSoup(res.text, "html.parser")
        imageURL = soup.select('.product-item-image > a')[0].attrs['href']
        # к сожалению, в BeautifulSoup пока еще не реализована в полной
        # мере поддержка псевдо-классов, пришлось поставить костыль и
        # удалять лишнее с помощью регулярных выражений
        # code = soup.select('.product-item-info > div:nth-child(2) > span:last-child')[0].getText()
        temp = soup.select('.product-item-info > div')[1].getText()
        code = re.sub('[^0-9]', '', temp)
        # URL фотографии и артикул товара у нас есть, сохраняем фото
        image = requests.get(imageURL)
        with open(code + '.jpg', 'wb') as f:
            f.write(image.content)

os.chdir('C:\\example\\images')

# получаем главную страницу сайта www.tinko.info
res = requests.get('http://www.tinko.info/')
html = res.text

# извлекаем из нее ссылки на корневые разделы каталога
soup = bs4.BeautifulSoup(html, "html.parser")
items = soup.select('#catalog-menu > ul > li > div > a')

# получаем в цикле страницы корневых разделов каталога
# и выясняем, сколько страниц товаров содержит каждая
pages = {}
for item in items:
    href = item.attrs['href']
    # получаем страницу корневого раздела каталога
    res = requests.get(href)
    # извлекаем номер последней страницы списка товаров
    soup = bs4.BeautifulSoup(res.text, "html.parser")
    url = soup.select('.pager > li > a.last-page')[0]
    page = re.search('(?<=/)\d+$', url.attrs['href']).group()
    # помещаем данные в словарь для дальнейшего использования
    pages[href] = page

# смотрим, что получилось
print(pages)

# теперь для каждого корневого раздела каталога надо
# получить все страницы списка товаров с первой по
# последнюю и на каждой странице получить URL страниц
# товара; и уже с этих страниц скачать фото товара
threads = []
for root, last in pages.items():
    for num in range(1, int(last) + 1):
        # получаем очередную страницу списка товаров
        url = root
        if (num > 1):
            url = url + '/page/' + str(num)
        print('Скачиваем фото товаров со страницы ' + url)
        # запускаем скачивание изображений в отдельных потоках
        thread = threading.Thread(target = getImages, args = [url])
        threads.append(thread)
        thread.start()

for thread in threads:
    thread.join()
print('Готово!')
{'http://www.tinko.info/catalog/category/3': '438', 'http://www.tinko.info/catalog/category/185': '421', 
'http://www.tinko.info/catalog/category/651': '269', 'http://www.tinko.info/catalog/category/806': '110',
'http://www.tinko.info/catalog/category/883': '125', 'http://www.tinko.info/catalog/category/1898': '80',
'http://www.tinko.info/catalog/category/1081': '32', 'http://www.tinko.info/catalog/category/1004': '97',
'http://www.tinko.info/catalog/category/1909': '144', 'http://www.tinko.info/catalog/category/1911': '37',
'http://www.tinko.info/catalog/category/1936': '214', 'http://www.tinko.info/catalog/category/1948': '217'}
Скачиваем фото товаров со страницы http://www.tinko.info/catalog/category/3
Скачиваем фото товаров со страницы http://www.tinko.info/catalog/category/3/page/2
Скачиваем фото товаров со страницы http://www.tinko.info/catalog/category/3/page/3
..........

Если у нас есть некоторый код, выполнение которого возможно только после завершения дочернего потока, можно использовать метод join() объекта Thread. Поток, который вызывает этот метод, приостанавливается, ожидая завершения потока, чей метод join() был вызван.

В рассмотренном выше примере парсинга сайта, есть такой код:

for thread in threads:
    thread.join()
print('Готово!')

Список threads содержит объекты всех дочерних потоков, запущенных основным потоком программы. Строка 'Готово!' не будет выведена до тех пор, пока не будет выполнен возврат из всех вызовов join().

Поиск: Python • Web-разработка • Модуль • Парсинг • Поток

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.