我正在用 Python 构建一个简单的网络爬虫。我将不得不浏览约 50k 个网站,我想通过一些多线程来加速这个过程。
我定义了一个爬虫类来爬取每个网站作为Thread的元对象:
Crawler(Thread):
def __init__(self, url, depth, wait):
...
然后在主函数中,我从完整的 URL 列表中批量迭代 10 个 URL,并为每个 URL 创建一个 Crawler 对象:
for i in range(index, math.ceil(len(urls) / 10)):
jobs = []
for url in urls[i * 10:(i + 1) * 10]:
s = Crawler(url)
s.setDaemon(True)
s.start()
jobs.append(s)
for j in jobs:
j.join()
问题是,对于每个批次,我必须等待所有线程完成。这是低效的,因为当我有 9 个 100 页的网站和只有 1 个 10,000 页的网站时,这 9 个网站将在几分钟内完成,但我将不得不等待一个小时才能完成 10,000 页的大型网站在我可以继续下一批之前完成。
为了优化,最好从 10 个 Crawler 线程开始,然后,每次 Crawler 线程完成时,使用列表中的下一个 url 创建一个新的 Crawler,直到列表完成。
我想我可以摆脱 join() 并在
threading.enumerate
的长度上进行 while 循环,每次长度低于 10 时添加一个新线程,但这听起来有点老套。
我正在研究Python的
Queue
,但从https://docs.python.org/3/library/queue.html的示例来看,我仍然必须依赖.join()
,因此等待所有队列中已执行的线程。
有没有办法向线程添加“事件监听器”之类的东西,以便每当线程完成时我都可以用新线程更新线程列表?
也许再次查看队列,您不需要每批加入或根本不需要加入。
您可以将所有 50K 个网站放入队列中。我可能会称之为
jobs
,而有限数量的线程通常被称为 workers
之类的东西。然后,每个工作人员从队列中拾取一个项目,对其进行处理,然后继续从队列中拾取项目,直到完成。所做的事情意味着不同。一个建议是将None
放入每个worker的队列中,每个worker一旦看到None
就会停止。但您还可以使用其他信号。然后,您可以使用 join
等待所有工作线程完成。在这种情况下,工作人员不需要是守护进程。 (您不想为每个 URL 创建一个单独的线程)
例如:
from threading import Queue, Thread
def crawl_worker(q):
while True:
url = q.get()
if url is None:
break
# do something with url
url_queue = Queue()
# populate the queue
for url in urls:
url_queue.put(url)
num_workers = 10
workers = [
Thread(target=crawl_worker, args=(url_queue,))
for _ in range(num_workers)
]
# add a None signal for each worker
for worker in workers:
url_queue.put(None)
# start all workers
for worker in workers:
worker.start()
# wait for all workers to finish
for worker in workers:
worker.join()
# we're done now
还有其他选择。如果您实际上并不是将其视为一项练习,而是想完成一些事情,那么我会推荐 https://scrapy.org/