上一篇文章: 从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_listthread_list都为空的时候才能退出。如果就是在total_url_list为空的时候退出会发生什么?程序会在第一个url被取走导致total_url_list为空的时候退出循环并结束吗?严格来说是的,我们可以在程序的末尾加入一个print语句,就可以验证修改while条件之后,while循环就退出了,但这个时候是主线程结束了,新增的线程并没有结束,此时还有一个线程在不断的运行和爬取url,这个线程就是获取了第一个url的线程,线程可以设置成随主线程一起停止,也可以让主线程挂起等待其余线程运行完成,默认情况下是我们这种,主线程运行完成并停止,而其余线程继续运行。


Harpsichord1207
538 声望44 粉丝

前路漫漫