Время прочтения: 6 мин.
Задача сбора данных из открытых источников возникает довольно часто, а основным фактором, влияющим на скорость её решения, выступает объём данных, порождающий большее количество обращений к источнику по сети, причём основное количество времени, затрачиваемого на работу с сетью, занимает ожидание ответа от источника.
Уменьшить время ожидания ответа при работе с сетью можно используя подходящие программные модели. Например, в случае многопоточного выполнения, подзадачи программы могут быть распределены между потоками.
Для примера, соберу достаточно большое количество стихов поэтов-классиков из открытого источника, оценю затраченное время и ускорю процесс сбора. Использовать буду Python, модули: bs4, json, os, queue, requests, time и threading.
Итак, перехожу по первой ссылке из поисковика:

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

Нашёлся адрес, при отправке запроса, на который, вернётся json с произведениями определённого параметрами запроса автора.
Импорты:
from bs4 import BeautifulSoup
import json
import os
import requests
import time
В рамках функции, выгружающей произведения по одному автору, перейду по нескольким ссылкам, чтобы набрать cookie, изменю стандартные для модуля requests заголовки, буду циклично отправлять запросы и разбирать json-ответы, сохраняя их в файл:
def getForAuthor(sess, author):
urls = ["https://www.google.ru/", "https://www.youtube.com/"]
for i in urls:
sess.get(url=i)
headers = {
"accept": "*/*",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"
}
result = {}
page = 1
while True:
url = f"https://www.culture.ru/_next/data/i_YUtTpPEY7K2ap4pw3Y4/literature/poems/{author}.json?pathParams=author-sergei-esenin"
params = {
"pathParams": author,
"page": page
}
while True:
try:
res = sess.get(
url=url,
params=params,
headers=headers
)
break
except Exception as ex:
print(ex)
js = res.json()
maxPage = js["pageProps"]["pagination"]["total"]
for item in js["pageProps"]["poems"]:
result[item["_id"]] = {
"id": item["_id"],
"title": item["title"],
"text": item["text"]
}
if page == maxPage:
break
page += 1
with open(f"./data/{author}.json", "w", encoding="utf-8") as file:
file.write(json.dumps(result, ensure_ascii=False, indent=4))
Запуск:
if __name__ == "__main__":
start = 1
stop = 2
sess = requests.Session()
authors = []
maxPage = int(getMaxPage(sess=sess))
print(maxPage)
start = time.time(
for page in range(start, stop)):
print(page)
url = f"https://www.culture.ru/literature/poems?page={page}"
while True:
try:
res = sess.get(url=url)
break
except Exception as ex:
print(ex)
soup = BeautifulSoup(res.text, "html.parser")
athrs = soup.find_all(attrs={"class": "_6unAn"})
for author in athrs:
author = author.get("href").replace("/literature/poems/", "")
os.system("cls")
if author not in authors:
authors.append(author)
print(len(authors))
for a in authors:
os.system("cls")
print(a)
getForAuthor(sess = sess, author = a)
print("Done for", time.time() - start, "seconds", len(authors))
Результаты сохранились:

Загляну в один из файлов, чтобы убедиться, что всё записано так, как ожидалось:

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

Почему так долго? И что происходит при отправке запроса дольше всего? Ожидание.
Для того, чтобы повысить производительность необходимо воспользоваться модулем threading: с его помощью запущу потоки, выгружающие произведения по одному автору в одном процессе. При переходе одного потока в состояние ожидания, будет выполняться другой, таким образом время ожидания уменьшится. Для обмена данными с потоком будет использоваться очередь из модуля queue.
Частично перепишу предыдущий скрипт.
Импорты:
from bs4 import BeautifulSoup
import json
import os
from queue import Queue
import requests
import time
import threading
Функция, которая будет работать в потоке:
def getForAuthor(task):
sess = requests.Session()
urls = ["https://www.google.ru/", "https://www.youtube.com/"]
for i in urls:
sess.get(url=i)
headers = {
"accept": "*/*",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"
}
result = {}
page = 1
if not task.empty():
author = task.get()
page = 1
while True:
url = f"https://www.culture.ru/_next/data/i_YUtTpPEY7K2ap4pw3Y4/literature/poems/{author}.json?pathParams=author-sergei-esenin"
params = {
"pathParams": author,
"page": page
}
while True:
try:
res = sess.get(
url=url,
params=params,
headers=headers
)
break
except Exception as ex:
print(ex)
js = res.json()
maxPage = js["pageProps"]["pagination"]["total"]
for item in js["pageProps"]["poems"]:
result[item["_id"]] = {
"id": item["_id"],
"title": item["title"],
"text": item["text"]
}
if page == maxPage:
break
page += 1
with open(f"./data/{author}.json", "w", encoding="utf-8") as file:
file.write(json.dumps(result, ensure_ascii=False, indent=4))
Запуск:
if __name__ == "__main__":
start = 1
stop = 2
sess = requests.Session()
authors = []
# создание экземпляра класса очереди
task = Queue()
start = time.time()
for page in range(start, stop):
print(page)
url = f"https://www.culture.ru/literature/poems?page={page}"
while True:
try:
res = sess.get(url=url)
break
except Exception as ex:
print(ex)
soup = BeautifulSoup(res.text, "html.parser")
athrs = soup.find_all(attrs={"class": "_6unAn"})
for author in athrs:
author = author.get("href").replace("/literature/poems/", "")
os.system("cls")
if author not in authors:
authors.append(author)
# заполнение очереди
for a in authors:
task.put(a)
# запуск потоков
for _ in range(len(authors)):
threading.Thread(target=getForAuthor, args=(task,)).start()
# цикл с условием останова для родительского потока
while True:
if threading.active_count() == 1:
break
print("Threading done for", time.time() - start, "seconds", len(authors))
Вывод по окончанию работы, одна страница, те же шестнадцать авторов:

По полученным результатам можно сделать вывод, что достались те же самые данные, но, при этом, сократилось время исполнения почти в два раза. А можно ещё быстрее? Можно, если более детально продумать процессы сбора и обработки данных из очереди.
С помощью модуля threading можно повысить производительность не только при работе с сетью, но и в любых задачах, связанных с вводом-выводом.