上一篇文章: 从0开始写一个多线程爬虫(1)
我们用继承Thread
类的方式来改造多线程爬虫,其实主要就是把上一篇文章的代码写到线程类的run
方法中,代码如下:
import re
import requests
from threading import Thread
class BtdxMovie(Thread):
# 初始化时传入3个list,含义见上文,并为当前线程取个名字
def __init__(self, total_url_list, used_url_list, movie_url_list, thread_name='MyThread'):
super(BtdxMovie, self).__init__()
self.all_url = total_url_list
self.used_url = used_url_list
self.movie_url = movie_url_list
self.name = thread_name
def run(self):
while 1:
# 从all_url中获取第一条url,如果all_url为空则break,这会导致线程死掉(is_alive()为False)
try:
url = self.all_url.pop(0)
except IndexError:
break
# 如果url是电影详情页,则将其加入到movie_url中
if re.match('https://www.btdx8.com/torrent/.*?html', url):
if url not in self.movie_url:
self.movie_url.append(url)
try:
html = requests.get(url).text
new_url = re.findall('href="(https://.*?)"', html)
for u in new_url:
# 只要同一个域名下的url
if not re.match('https://.*?btdx8.com', u):
continue
# '#'在url中是代表网页位置的,这里处理一下,避免url重复
if '#' in u:
u = u.split('#')[0]
if u in self.used_url or u in self.all_url:
continue
self.all_url.append(u)
except:
pass
self.used_url.append(url)
# 每次循环打印当前线程id和各个list的长度
curr_thread = '[{}]'.format(self.name)
info = 'ALL: {}, USED: {}, MOV: {}'.format(len(self.all_url), len(self.used_url), len(self.movie_url))
print(curr_thread + ': ' + info)
此时线程类就已经写好了,接下来要做的就是生成多个实例,并开启线程,继续追加如下代码:
# 网站首页
base_url = r'https://www.btdx8.com/'
# 爬取到的新url会继续加入到这个list里
total_url_list = [base_url]
# 存放已经爬取过的url
used_url_list = []
# 存放是电影详情页的url
movie_url_list = []
# 存入线程对象的list
thread_list = []
thread_id = 0
while total_url_list or thread_list:
for t in thread_list:
if not t.is_alive():
thread_list.remove(t)
while len(thread_list) < 5 and total_url_list:
thread_id += 1
thread_name = 'Thread-{}'.format(str(thread_id).zfill(2))
t = BtdxMovie(total_url_list, used_url_list, movie_url_list, thread_name)
t.start()
thread_list.append(t)
此时运行脚本,就可以以多线程的方式抓取url
了,运行之后print
的信息如下:
[Thread-04]: ALL: 2482, USED: 84, MOV: 55
[Thread-01]: ALL: 2511, USED: 85, MOV: 56
[Thread-02]: ALL: 2518, USED: 86, MOV: 57
[Thread-05]: ALL: 2555, USED: 87, MOV: 58
[Thread-03]: ALL: 2587, USED: 88, MOV: 59
[Thread-01]: ALL: 2595, USED: 89, MOV: 60
[Thread-04]: ALL: 2614, USED: 90, MOV: 61
[Thread-05]: ALL: 2644, USED: 91, MOV: 62
[Thread-03]: ALL: 2686, USED: 92, MOV: 63
我们来解释一下while
循环里的代码,先看内嵌的while
循环,是当total_url_list
不为空,并且thread_list
长度小于5
的时候执行,利用thread_id
获得thread_name
,实例化一个线程实例t
,并用t.start()
开启线程,然后将其加入到thread_list
中,因此很容易可以理解这段代码,就是确保当前运行的线程数为5
,并且给每个新线程一个从1
开始自增长的id
。
继续看上一段for
循环的代码,是遍历thread_list
,将已经挂了的线程去除掉,那么在这个case
中线程什么情况下会死掉?就是BtdxMovie
类中的run
方法中的这段代码:
try:
url = self.all_url.pop(0)
except IndexError:
break
如果all_url
为空会break
循环,此时对应的线程会死掉。这里可能很容易误以为所有的url
都已经爬取完了导致线程退出,实际上,目前的代码没有对爬取的url
深度做控制,可能永远都不会爬完,当all_url
为空时候,很大可能是all_url
里的url
被线程取走了,但还没来得及把爬取到新的url
加入到all_url
中,所以很容易理解这种情况会在程序刚开始运行的时候发生,因为一开始all_url
中只有一个url
,被第一个线程取走,在第一个线程还没返回结果的时候,后续的线程去取url
都会导致循环break
,然后线程死掉。此时主函数的for
循环将死掉的线程去除,在线程数不足5
个的情况下,接下来的while
循环继续制造新的线程。
那么外层的while
循环的条件也很容易就明白了,不能在total_url_list
为空的时候退出,要在total_url_list
和thread_list
都为空的时候才能退出。如果就是在total_url_list
为空的时候退出会发生什么?程序会在第一个url
被取走导致total_url_list
为空的时候退出循环并结束吗?严格来说是的,我们可以在程序的末尾加入一个print
语句,就可以验证修改while
条件之后,while
循环就退出了,但这个时候是主线程结束了,新增的线程并没有结束,此时还有一个线程在不断的运行和爬取url
,这个线程就是获取了第一个url
的线程,线程可以设置成随主线程一起停止,也可以让主线程挂起等待其余线程运行完成,默认情况下是我们这种,主线程运行完成并停止,而其余线程继续运行。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。