精华内容
下载资源
问答
  • 主要介绍了Python多线程异步+多进程爬虫实现代码,需要的朋友可以参考下
  • 多线程爬虫与单线程爬虫效率对比 1.什么是进程? 当一个程序正在运行时,它就是一个进程,进程包括运行中的程序程序所使用到的内存系统资源,而一个进程又包含多个线程。 2.线程是什么? 线程是程序中的一个...

    多线程爬虫与单线程爬虫的效率对比

    1.什么是进程?

    当一个程序正在运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源,而一个进程又包含多个线程。

    2.线程是什么?

    线程是程序中的一个执行流,每个线程都有自己专有的寄存器(栈指针、程序计数器等),但一个进程内的多个线程是共享代码区的,也就是同一个函数可以被多个线程所执行。

    3.多线程是什么?

    多线程一般指的是同一个程序的多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是允许端个程序创建多个并行执行的线程来完成各自的任务。

    4.多线程的优点?

    可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。

    5.多线程的缺点?

    线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
    多线程需要协调和管理,所以需要CPU时间跟踪线程;
    线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
    线程太多会导致控制太复杂,最终可能造成很多Bug;

    转载:https://blog.csdn.net/function__/article/details/80883084

    -----------------------------以上内容属转载----------------------------------

    6.我们为什么要使用多线程?

    举个栗子:比如我们要运输一趟货物,以进程代表火车,线程代表火车的车厢,理论上如果我们使用车厢数更多的火车去运输这趟货物,单位时间内就能运输更多地货物,效率也就更高。所以这就是为什么我们要使用多线程的缘故,就是能够去提高程序的效率。

    7.对比单线程和多线程爬虫 -------以爬取斗图网(http://www.bbsnet.com/)的表情为例

    (1).实现的思路

    在这里插入图片描述

    (2).单线程爬虫
    代码如下:
    
    # coding="utf-8"
    import requests  
    from urllib.request import urlretrieve
    from lxml import html
    import json
    import os
    import datetime
    
    class Doutu_Spider:
    
    	def __init__(self):  # 类初始参数
            self.url_temp = "http://www.bbsnet.com/author/panxingquan/page/{}" #设置一个临时url,之后可以更改{}的内容而快速拿到其他页面的链接
            self.headers = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"}   # 拿到头信息,可以让对方服务器更加信任你是一个正常的浏览者而非一个爬虫程序
    
        def start_url(self):  #拿到所要爬取的网页的链接
            return [self.url_temp.format(i) for i in range(1, 11)] # fomat函数可以用字符去代替{},测试爬取十页的数据,返回所有的链接列表
    
        def parse_url(self,url):  # 3. 提交请求,获取响应
            response = requests.get(url, headers=self.headers) # 向服务器发送一个get请求,和接受服务器所返回的内容 
            return response.content.decode() # 拿到服务器返回的内容的编码格式为utf-8的byte型的二进制数据
    
        def get_content_list(self,html_str): # 4. 提取数据
            os.makedirs('./images/',exist_ok=True) # 打开当前目录的images文件夹
            htmlDiv = html.etree.HTML(html_str) #解析html文件
            content_list = htmlDiv.xpath("//ul[@id='post_container']/li/div[@class='thumbnail']") # xpath直接定位到包含有图像资源的所有div中
            div_list =[]
            for content in content_list:
                item={}  # 初始化一个字典
                item["title"] = content.xpath("./a/img/@alt")[0] if len(content.xpath("./a/img/@alt"))>0 else None # 拿到图片的标题
                item["url"] = content.xpath("./a/img/@src")[0] if len(content.xpath("./a/img/@src"))>0 else None # 拿到图片的Url地址
                urlretrieve(item["url"], 'images/' + item['title']+'.gif') # 下载图片,保存在imgaes下,格式为:标题.gif
                div_list.append(item)  #将字典保存列表中
            return div_list # 返回这一页的数据列表
    
        def save_data(self,content_list): # 保存数据
            with open("doutu.json","a",encoding="utf-8") as f:# 打开doutu,json文件,没有就创建一个;"a",追加的形式打开,这样新生成的数据才不会覆盖原有的数据,编码格式为"utf-8"
                for content in content_list: # 遍历这个数据列表
                    f.write(json.dumps(content,ensure_ascii=False)) # 将每一个字典都写入到json文件中
                    f.write("\n")
                print("保存成功")
      
        def run(self):
        	# 测试时间,开端
        	starttime = datetime.datetime.now()
            # 1. start_url
            # 2. url_list   # 链接列表
            url_list = self.start_url()
            # 3. 提交请求,获取响应
            for url in url_list:
                html_str = self.parse_url(url)
            # 4. 提取数据
                content_list = self.get_content_list(html_str)
            # 5. 保存数据
                self.save_data(content_list)
            # 打印时间
            endtime = datetime.datetime.now()
            print(endtime - starttime).seconds
              
    
    if __name__ == '__main__':
    DoutuSpider = Doutu_Spider() #实例化这个类
    DoutuSpider.run() #程序执行
    
    单线程程序运行结果
    我们可以看到,在单线程的情况下,爬取和下载这十个链接共花费了51s左右的时间。共获取了300张图片。
    

    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述

    (3).多线程爬虫
    代码如下:
    # 这份代码只注释解释多线程部分
    
    # coding="utf-8"
    import requests
    from urllib.request import urlretrieve
    from lxml import html
    import json
    import os
    import datetime
    # 导入多线程与队列模块
    import threading
    import queue
    
    
    
    class Doutu_Spider:
    
        def __init__(self):
            self.url_temp = "http://www.bbsnet.com/author/panxingquan/page/{}"
            self.headers = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"}
            self.url_queue = queue.Queue() # 定义一个url队列
            self.html_queue = queue.Queue() # 定义一个htmld队列
            self.content_queue = queue.Queue() # 定义一个content队列
    
        def start_url(self):
            for i in range(1, 11):
                self.url_queue.put(self.url_temp.format(i)) # 将所有要爬的网页url都添加在url队列中
    
        def parse_url(self):
            while True:
                url = self.url_queue.get() # 在url队列中取出一个url来
                response = requests.get(url, headers=self.headers)
                self.html_queue.put(response.content.decode()) # 将服务器反馈回来的信息进行编码后,放入到html队列里
                self.url_queue.task_done() # url队列完成本次任务,从队列中删除该元素
    
        def get_content_list(self):
            global gNum
            os.makedirs('./images/',exist_ok=True)
            while True:
                html_str = self.html_queue.get() # 在html队列中取出一个html信息
                etree = html.etree
                htmlDiv = etree.HTML(html_str)
                content_list = htmlDiv.xpath("//ul[@id='post_container']/li/div[@class='thumbnail']")
                div_list =[]
                for content in content_list:
                    item={}
                    item["title"] = content.xpath("./a/img/@alt")[0] if len(content.xpath("./a/img/@alt"))>0 else None
                    item["url"] = content.xpath("./a/img/@src")[0] if len(content.xpath("./a/img/@src"))>0 else None
                    urlretrieve(item["url"], 'image/' + item['title']+'.gif')
                    gLock.acquire()
                    gNum +=1
                    print('下载了 %d' %gNum)
                    gLock.release()
                    div_list.append(item)
                self.content_queue.put(div_list) # 将本次html中所提取的到信息放到content列表上
                self.html_queue.task_done() # html队列完成本次任务,从队列中删除该元素
    
    
        def save_data(self):
            while True:
                content_list = self.content_queue.get()# 从content队列中拿出一个信息列表出来
                with open("doutu.json","a",encoding="utf-8") as f:
                    for content in content_list:
                        f.write(json.dumps(content,ensure_ascii=False))
                        f.write("\n")
                    print("保存成功")
                    self.content_queue.task_done() # content 队列完成本次任务,从队列中删除该元素
    
        def run(self):
        	# 测试时间,开端
        	starttime = datetime.datetime.now()
            thread_list = [] # 初始化一个线程列表
            # 2. url_list
            t_url = threading.Thread(target=self.start_url) # 使用一个线程完成获取Url_list的任务
            thread_list.append(t_url) # 添加到线程列表中
            # 3. 提交请求,获取响应
            for i in range(10):
                t_parse = threading.Thread(target=self.parse_url) # 遍历一次就使用一个线程来去完成请求任务
                thread_list.append(t_parse)
            # 4. 提取数据
            for i in range(5):
                t_html = threading.Thread(target=self.get_content_list) #遍历一次就使用一个线程来去完成提取数据任务
                thread_list.append(t_html)
            # 5. 保存数据
            for i in range(4):
                t_save = threading.Thread(target=self.save_data)  #遍历一次就使用一个线程来去完成保存数据的任务
                thread_list.append(t_save)
    
            for t in thread_list: #遍历线程列表
                t.setDaemon(True) #设置守护线程,确保有线程在执行,当只有守护线程时,程序结束运行
                t.start()# 线程开始运行
    
            for q in [self.url_queue,self.html_queue, self.content_queue]: #依次遍历三个队列
                q.join() # 阻塞进程,直到该线程执行完毕
    
            print("主线程结束")
            
            # 打印时间
            endtime = datetime.datetime.now()
            print(endtime - starttime).seconds
    
    
    if __name__ == '__main__':
        DoutuSpider = Doutu_Spider()
        DoutuSpider.run()
    
    多线程程序运行结果
    我们可以看到,在多线程的情况下,爬取和下载这十个链接只花费了282s左右的时间。共获取了300张图片。
    

    在这里插入图片描述在这里插入图片描述

    8.总结

    两份代码都是执行同一个任务,就是爬取斗图网上前十页的表情包。但单线程程序花了51s多的时间,而多线程程序只用了28s就完成了,这缩减了接近一半的时间,大大提升了效率。
    多线程程序可以充分利用cpu,在同等情况,会比单线程程序的执行效率高得多。一般对于一些复杂的程序,我们通过多线程可以提高其执行效率,但对于普通简单的程序,我们写多线程程序无疑大材小用了。但对于爬虫的项目来说,利用多线程程序可以更有效率完成我们的任务,因此学会多线程编程是很有必要的。最后祝福我们一起都能把多线程学好!!
    展开全文
  • 在Python爬虫下一代网络请求库httpx和parsel解析库测评一文中我们对比了requests的同步爬虫和httpx的异步协程爬虫爬取链家二手房信息所花的时间(如下所示:一共580条...

    Python爬虫下一代网络请求库httpx和parsel解析库测评一文中我们对比了requests的同步爬虫和httpx的异步协程爬虫爬取链家二手房信息所花的时间(如下所示:一共580条记录),结果httpx同步爬虫花了16.1秒,而httpx异步爬虫仅花了2.5秒。

    那么问题来了。实现爬虫的高并发不仅仅只有协程异步这一种手段,传统的同步爬虫结合多进程和多线程也能大大提升爬虫工作效率,那么多进程, 多线程和异步协程爬虫到底谁更快呢? 当然对于现实中的爬虫,爬得越快,被封的可能性也越高。本次测评使用httpx爬取同样链家网数据,不考虑反爬因素,测评结果可能因个人电脑和爬取网站对象而异。

    在我们正式开始前,你能预测下哪种爬虫更快吗?可能结果会颠覆你的观点。

    传统爬虫 vs 协程异步爬虫

    传统Python爬虫程序都是运行在单进程和单线程上的,包括httpx异步协程爬虫。如果你不清楚进程和线程的区别,以及Python如何实现多进程和多线程编程,请阅读下面这篇知乎上收藏过1000的文章。

    一个传统的web爬虫代码可能如下所示,先用爬虫获取目标页面中显示的最大页数,然后循环爬取每个单页数据并解析。单进程、单线程同步爬虫的请求是阻塞的,在一个请求处理完全结束前不会发送一个新的请求,中间浪费了很多等待时间。

    httpx异步协程爬虫虽然也是运行在单进程单线程上的,但是所有异步任务都会加到事件循环(loop)中运行,可以一次有上百或上千个活跃的任务,一旦某个任务需要等待,loop会快速切换到下面一个任务,所以协程异步要快很多。

    要把上面的同步爬虫变为异步协程爬虫,我们首先要使用async将单个页面的爬取和解析过程包装成异步任务,使用httpx提供的AsyncClient发送异步请求。

    接着我们使用asyncio在主函数parse_page里获取事件循环(loop), 并将爬取单个页面的异步任务清单加入loop并运行。

    多进程爬虫

    对于多线程爬虫,我们首先定义一个爬取并解析单个页面的同步任务。

    接下来我们在主函数parse_page里用multiprocessing库提供的进程池Pool来管理多进程任务。池子里进程的数量,一般建议为CPU的核数,这是因为一个进程需要一个核,你设多了也没用。我们使用map方法创建了多进程任务,你还可以使用apply_async方法添加多进程任务。任务创建好后,任务的开始和结束都由进程池来管理,你不需要进行任何操作。这样我们一次就有4个进程同时在运行了,一次可以同时处理4个请求。

    那用这个多进程爬虫爬取链家580条数据花了多长时间呢? 答案是7.6秒,比单进程单线程的httpx同步爬虫16.1秒还是要快不少的。

    项目完整代码如下所示:

    from fake_useragent import UserAgent
    import csv
    import re
    import time
    from parsel import Selector
    import httpx
    from multiprocessing import Pool, cpu_count, Queue, Manager
    
    
    class HomeLinkSpider(object):
        def __init__(self):
            # 因为多进程之间不能共享内存,需使用队列Queue共享数据进行通信
            # 每个进程爬取的数据都存入这个队列,不能使用self.data列表
            # 子进程获取不到self.headers这个变量,需要直接生成
            # self.ua = UserAgent()
            # self.headers = {"User-Agent": self.ua.random}
            self.q = Manager().Queue()
            self.path = "浦东_三房_500_800万.csv"
            self.url = "https://sh.lianjia.com/ershoufang/pudong/a3p5/"
        def get_max_page(self):
            response = httpx.get(self.url, headers={"User-Agent": UserAgent().random})
            if response.status_code == 200:
                # 创建Selector类实例
                selector = Selector(response.text)
                # 采用css选择器获取最大页码div Boxl
                a = selector.css('div[class="page-box house-lst-page-box"]')
                # 使用eval将page-data的json字符串转化为字典格式
                max_page = eval(a[0].xpath('//@page-data').get())["totalPage"]
                print("最大页码数:{}".format(max_page))
                return max_page
            else:
                print("请求失败 status:{}".format(response.status_code))
                return None
        # 解析单页面,需传入单页面url地址
        def parse_single_page(self, url):
            print("子进程开始爬取:{}".format(url))
            response = httpx.get(url, headers={"User-Agent": UserAgent().random})
            selector = Selector(response.text)
            ul = selector.css('ul.sellListContent')[0]
            li_list = ul.css('li')
            for li in li_list:
                detail = dict()
                detail['title'] = li.css('div.title a::text').get()
    
    
                #  2室1厅 | 74.14平米 | 南 | 精装 | 高楼层(共6层) | 1999年建 | 板楼
                house_info = li.css('div.houseInfo::text').get()
                house_info_list = house_info.split(" | ")
    
    
                detail['bedroom'] = house_info_list[0]
                detail['area'] = house_info_list[1]
                detail['direction'] = house_info_list[2]
    
    
    
    
                floor_pattern = re.compile(r'\d{1,2}')
                match1 = re.search(floor_pattern, house_info_list[4])  # 从字符串任意位置匹配
                if match1:
                    detail['floor'] = match1.group()
                else:
                    detail['floor'] = "未知"
                # 匹配年份
                year_pattern = re.compile(r'\d{4}')
                match2 = re.search(year_pattern, house_info_list[5])
                if match2:
                    detail['year'] = match2.group()
                else:
                    detail['year'] = "未知"
                # 文兰小区 - 塘桥    提取小区名和哈快
                position_info = li.css('div.positionInfo a::text').getall()
                detail['house'] = position_info[0]
                detail['location'] = position_info[1]
    
    
                # 650万,匹配650
                price_pattern = re.compile(r'\d+')
                total_price = li.css('div.totalPrice span::text').get()
                detail['total_price'] = re.search(price_pattern, total_price).group()
    
    
                # 单价64182元/平米, 匹配64182
                unit_price = li.css('div.unitPrice span::text').get()
                detail['unit_price'] = re.search(price_pattern, unit_price).group()
    
    
                self.q.put(detail)
    
    
        def parse_page(self):
            max_page = self.get_max_page()
            print("CPU内核数:{}".format(cpu_count()))
    
    
            # 使用进程池管理多进程任务
            with Pool(processes=4) as pool:
                urls = ['https://sh.lianjia.com/ershoufang/pudong/pg{}a3p5/'.format(i) for i in range(1, max_page + 1)]
                # 也可以使用pool.apply_async(self.parse_single_page, args=(url,))
                pool.map(self.parse_single_page, urls)
    
    
    
    
        def write_csv_file(self):
            head = ["标题", "小区", "房厅", "面积", "朝向", "楼层", "年份", "位置", "总价(万)", "单价(元/平方米)"]
            keys = ["title", "house", "bedroom", "area", "direction", "floor", "year", "location",
                    "total_price", "unit_price"]
    
    
            try:
                with open(self.path, 'w', newline='', encoding='utf_8_sig') as csv_file:
                    writer = csv.writer(csv_file, dialect='excel')
                    if head is not None:
                        writer.writerow(head)
    
    
                    # 如果队列不为空,写入每行数据
                    while not self.q.empty():
                        item = self.q.get()
                        if item:
                            row_data = []
                            for k in keys:
                                row_data.append(item[k])
                            writer.writerow(row_data)
    
    
                    print("Write a CSV file to path %s Successful." % self.path)
            except Exception as e:
                print("Fail to write CSV to path: %s, Case: %s" % (self.path, e))
    
    
    
    
    if __name__ == '__main__':
        start = time.time()
        home_link_spider = HomeLinkSpider()
        home_link_spider.parse_page()
        home_link_spider.write_csv_file()
        end = time.time()
        print("耗时:{}秒".format(end-start))
    

    注意: 多个进程之间内存是不共享的,需要使用Python多进程模块提供的Manager.Queue()实现多个进程的数据共享,比如把不同进程爬取的数据存到一个地方。

    多线程爬虫

    爬取解析单个页面的函数和多进程爬虫里的代码是一样的,不同的是我们在parse_page主函数里使用threading模块提供的方法创建多线程任务,如下所示:

    我们也不需要使用Queue()类存储各个线程爬取的数据,因为各个线程内存是可以共享的。多线程同步爬虫运行结果如下所示,爬取580条数据总共耗时只有短短的2.2秒,几乎秒开,甚至比httpx异步协程的还快!

    结果为什么是这样呢?其实也不难理解。对于爬虫这种任务,大部分消耗时间其实是等等待时间,在等待时间中CPU是不需要工作的,那你在此期间提供双核或4核CPU进行多进程编程是没有多大帮助的。那么为什么多线程会对爬虫代码有用呢?这时因为Python碰到等待会立即释放GIL供新的线程使用,实现了线程间的快速切换,这跟协程异步任务的切换一个道理,只不过多线程任务的切换由操作系统进行,而协程异步任务的切换由loop进行。

    多线程完整代码如下所示:

    from fake_useragent import UserAgent
    import csv
    import re
    import time
    from parsel import Selector
    import httpx
    import threading
    
    
    
    
    class HomeLinkSpider(object):
        def __init__(self):
            self.data = list()
            self.path = "浦东_三房_500_800万.csv"
            self.url = "https://sh.lianjia.com/ershoufang/pudong/a3p5/"
    
    
        def get_max_page(self):
            response = httpx.get(self.url, headers={"User-Agent": UserAgent().random})
            if response.status_code == 200:
                # 创建Selector类实例
                selector = Selector(response.text)
                # 采用css选择器获取最大页码div Boxl
                a = selector.css('div[class="page-box house-lst-page-box"]')
                # 使用eval将page-data的json字符串转化为字典格式
                max_page = eval(a[0].xpath('//@page-data').get())["totalPage"]
                print("最大页码数:{}".format(max_page))
                return max_page
            else:
                print("请求失败 status:{}".format(response.status_code))
                return None
    
    
        # 解析单页面,需传入单页面url地址
        def parse_single_page(self, url):
            print("多线程开始爬取:{}".format(url))
            response = httpx.get(url, headers={"User-Agent": UserAgent().random})
            selector = Selector(response.text)
            ul = selector.css('ul.sellListContent')[0]
            li_list = ul.css('li')
            for li in li_list:
                detail = dict()
                detail['title'] = li.css('div.title a::text').get()
    
    
                #  2室1厅 | 74.14平米 | 南 | 精装 | 高楼层(共6层) | 1999年建 | 板楼
                house_info = li.css('div.houseInfo::text').get()
                house_info_list = house_info.split(" | ")
    
    
                detail['bedroom'] = house_info_list[0]
                detail['area'] = house_info_list[1]
                detail['direction'] = house_info_list[2]
    
    
    
    
                floor_pattern = re.compile(r'\d{1,2}')
                match1 = re.search(floor_pattern, house_info_list[4])  # 从字符串任意位置匹配
                if match1:
                    detail['floor'] = match1.group()
                else:
                    detail['floor'] = "未知"
    
    
                # 匹配年份
                year_pattern = re.compile(r'\d{4}')
                match2 = re.search(year_pattern, house_info_list[5])
                if match2:
                    detail['year'] = match2.group()
                else:
                    detail['year'] = "未知"
    
    
                # 文兰小区 - 塘桥    提取小区名和哈快
                position_info = li.css('div.positionInfo a::text').getall()
                detail['house'] = position_info[0]
                detail['location'] = position_info[1]
    
    
                # 650万,匹配650
                price_pattern = re.compile(r'\d+')
                total_price = li.css('div.totalPrice span::text').get()
                detail['total_price'] = re.search(price_pattern, total_price).group()
    
    
                # 单价64182元/平米, 匹配64182
                unit_price = li.css('div.unitPrice span::text').get()
                detail['unit_price'] = re.search(price_pattern, unit_price).group()
    
    
                self.data.append(detail)
    
    
        def parse_page(self):
            max_page = self.get_max_page()
    
    
            thread_list = []
            for i in range(1, max_page + 1):
                url = 'https://sh.lianjia.com/ershoufang/pudong/pg{}a3p5/'.format(i)
                t = threading.Thread(target=self.parse_single_page, args=(url,))
                thread_list.append(t)
    
    
            for t in thread_list:
                t.start()
    
    
            for t in thread_list:
                t.join()
    
    
        def write_csv_file(self):
            head = ["标题", "小区", "房厅", "面积", "朝向", "楼层", "年份", "位置", "总价(万)", "单价(元/平方米)"]
            keys = ["title", "house", "bedroom", "area", "direction", "floor", "year", "location",
                    "total_price", "unit_price"]
    
    
            try:
                with open(self.path, 'w', newline='', encoding='utf_8_sig') as csv_file:
                    writer = csv.writer(csv_file, dialect='excel')
                    if head is not None:
                        writer.writerow(head)
                    for item in self.data:
                        row_data = []
                        for k in keys:
                            row_data.append(item[k])
                            # print(row_data)
                        writer.writerow(row_data)
                    print("Write a CSV file to path %s Successful." % self.path)
            except Exception as e:
                print("Fail to write CSV to path: %s, Case: %s" % (self.path, e))
    
    
    if __name__ == '__main__':
        start = time.time()
        home_link_spider = HomeLinkSpider()
        home_link_spider.parse_page()
        home_link_spider.write_csv_file()
        end = time.time()
        print("耗时:{}秒".format(end-start))
    

    结论

    多进程, 多线程和异步协程均可以提高Python爬虫的工作效率。对于爬虫这种非计算密集型的工作,多进程编程对效率的提升不如多线程和异步协程。异步爬虫不总是最快的,同步爬虫+多线程也同样可以很快,有时甚至更快。

    • httpx 同步 + parsel: 16.1秒

    • httpx 异步 + parsel: 2.5秒

    • http 同步多进程 + parsel: 7.6秒

    • http 同步多线程 + parsel: 2.2秒

    对于这样的结果,你满意吗? 欢迎留言!

    大江狗

    2021.5

    推荐阅读

    神文必读: 同步Python和异步Python的区别在哪里?

    Python爬虫下一代网络请求库httpx和parsel解析库测评

    一文看懂Python多进程与多线程编程(工作学习面试必读)

    非常适合小白的 Asyncio 教程

    展开全文
  • 描述:由C#编写的多线程异步抓取网页的网络爬虫控制台程序 ... 之所以用多线程异步抓取完全是出于效率考虑,本程序多线程同步并不能带来速度的提升,只要抓取的网页不要太多重复冗余就可以,异步并不意味着错误。
  • 一、并发并行 ...这个概念用单核CPU多核CPU比较容易说明。在使用单核CPU时,个工作任务是以并发的方式运行的,因为只有一个CPU,所以各个任务会分别占用CPU的一段时间依次执行。如果在自己分得的时间段...

    一、并发和并行

    并发(concurrency)和并行( parallelism)是两个相似的概念。 引用一个比较容易理解的说法,并发是指在一个时间段内发生若干事件的情况,并行是指在同一时刻发生若干事件的情况。

    这个概念用单核CPU和多核CPU比较容易说明。在使用单核CPU时,多个工作任务是以并发的方式运行的,因为只有一个CPU,所以各个任务会分别占用CPU的一段时间依次执行。如果在自己分得的时间段没有完成任务,就会切换到另一个任务,然后在下一次得到CPU使用权的时候再继续执行,直到完成。在这种情况下,因为各个任务的时间段很短、经常切换,所以给我们的感觉是“同时”进行。在使用多核CPU时,在各个核的任务能够同时运行,这是真正的同时运行,也就是并行。

    二、同步和异步

    同步和异步也是两个值得比较的概念。下面在并发和并行框架的基础上理解同步和异步。

    同步就是并发或并行的各个任务不是独自运行的,任务之间有一定的交替顺序,可能在运行完一个任务得到结果后,另一个任务才会开始运行。就像接力赛跑一样,要拿到交接棒之后下一个选手才可以开始跑。

    异步则是并发或并行的各个任务可以独立运行,一个任务的运行不受另一个任务影响,任务之间就像比赛的各个选手在不同的赛道比赛一样, 跑步的速度不受其他赛道选手的影响。

    在网络爬虫中,假设你需要打开4个不同的网站,IO过程就相当于你打开网站的过程,CPU就是你单击的动作。你单击的动作很快,但是网站却打开得很慢,同步IO是指你每单击一个网址,要等待该网站彻底显示才可以单击下一个网站。异步IO是指你单击定一个网址,不用等对方服务器返回结果,立马可以用新打开的测览器窗口打开另一个网址,最后同时等待4个网站彻底打开。

    很明显,异步的速度要快得多。

    三、多线程爬虫

    多线程爬虫是以并发的方式执行的。也就是说,多个线程并不能真正的同时执行,而是通过进程的快速切换加快网络爬虫速度的。

    Python本身的设计对多线程的执行有所限制。在Python设计之初,为了数据安全所做的决定设置有GIL(Global Interpreter Lock,全局解释器锁)。在Python中,一个线程的执行过程包括获取GIL、执行代码直到挂起和释放GIL。

    例如,某个线程想要执行,必须先拿到GIL,我们可以把GIL看成“通行证”,并且在一个Python进程中,GIL只有一个。拿不到通行证的进程就不允许进入CPU执行。

    每次释放GIL锁,线程之间都会进行锁竞争,而切换线程会消耗资源。由于GIL锁的存在,Python 里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是在多核CPU上Python的多线程效率不高的原因。

    由于GIL的存在,多线程是不是就没用了呢?

    以网络爬虫来说,网络爬虫是IO密集型,多线程能够有效地提升效率,因为单线程下有IO操作会进行IO等待,所以会造成不必要的时间浪费,而开启多线程能在线程A等待时自动切换到线程B,可以不浪费CPU的资源,从而提升程序执行的效率。

    Python的多线程对于IO密集型代码比较友好,网络爬虫能够在获取网页的过程中使用多线程,从而加快速度。

    在这里插入图片描述

    下面将以获取访问量最大的1000个中文网站的速度为例,通过和单线程的爬虫比较,证实多线程方法的速度提升。

    1000个网址写在alexa.txt文件中。
    在这里插入图片描述

    3.1 简单单线程爬虫

    import requests
    import time
    
    link_list = []
    with open('alexa.txt', 'r') as file:
        file_list = file.readlines()
        for eachone in file_list:
            link = eachone.split('\t')[1]
            link = link.replace('\n','')
            link_list.append(link)
        
    start = time.time()
    for eachone in link_list:
        try:
            r = requests.get(eachone)
            print(r.status_code, eachone)
        except Exception as e:
            print("Error: ", e)
    end = time.time()
    print("串行的总时间为:", end-start)
    

    得到的时间结果大概是:2030.428秒

    3.2 简单的多线程爬虫

    import threading
    import time
    import requests
    
    link_list = []
    with open('alexa.txt', 'r') as file:
        file_list = file.readlines()
        for eachone in file_list:
            link = eachone.split('\t')[1]
            link = link.replace('\n','')
            link_list.append(link)
    
    start = time.time()
    class MyThread(threading.Thread):
        def __init__(self, name, link_range):
            threading.Thread.__init__(self)
            self.name = name
            self.link_range = link_range
        def run(self):
            print("Starting " + self.name)
            crawler(self.name, self.link_range)
            print("Exiting " + self.name)
            
    def crawler(threadName, link_range):
        for i in range(link_range[0], link_range[1]+1):
            try:
                r = requests.get(link_list[i], timeout=20)
                print(threadName, r.status_code, link_list[i])
            except Exception as e:
                print(threadName, 'Error: ', e)
    
    thread_list = []
    link_range_list = [(0,200), (201,400), (401,600), (601,800), (801,1000)]
    
    #创建新线程
    for i in range(1,6):
        thread = MyThread("Thread-" + str(i), link_range_list[i-1])
        thread.start()
        thread_list.append(thread)
    
    #等待所有线程完成
    for thread in thread_list:
        thread.join()
        
    end = time.time()
    print("简单多线程爬虫总时间为:", end-start)
    print("Exiting Main Thread")
    

    在这里插入图片描述
    结果大概是532.81秒,明显快了很多。

    3.3 使用Queue的多线程爬虫

    这里,我们不再分给每个线程分配固定个数个网址,因为某个线程可能先于其他线程完成任务,然后就会空闲,这就造成了浪费。我们用一个队列存储所有网址,每个线程每次都从队列里取一个网址来执行。

    import threading
    import time
    import requests
    import queue as Queue
    
    link_list = []
    with open('alexa.txt', 'r') as file:
        file_list = file.readlines()
        for eachone in file_list:
            link = eachone.split('\t')[1]
            link = link.replace('\n','')
            link_list.append(link)
    
    start = time.time()
    class MyThread(threading.Thread):
        def __init__(self, name, q):
            threading.Thread.__init__(self)
            self.name = name
            self.q = q
        def run(self):
            print("Starting " + self.name)
            while True:
                try:
                    crawler(self.name, self.q)
                except:
                    break
            print("Exiting " + self.name)
            
    def crawler(threadName, q):
        url = q.get(timeout=2)
        try:
            r = requests.get(url, timeout=20)
            print(q.qsize(), threadName, r.status_code, url)
        except Exception as e:
             print(q.qsize(), threadName, url, 'Error: ', e)
    
    threadList = ["Thread-1", "Thread-2", "Thread-3", "Thread-4", "Thread-5"]
    workQueue = Queue.Queue(1000)
    threads = []
    
    for url in link_list:
        workQueue.put(url)
    
    #创建新线程
    for tName in threadList:
        thread = MyThread(tName, workQueue)
        thread.start()
        threads.append(thread)
    
    #等待所有线程完成
    for thread in threads:
        thread.join()
        
    end = time.time()
    print("Queue多线程爬虫总时间为:", end-start)
    print("Exiting Main Thread")
    

    在这里插入图片描述
    结果大概是441.40秒,又快了很多。

    四、Python使用多线程的两种方法

    (1)函数式:调用_thread模块中的start_new_thread()函数产生新线程:

    import _thread
    import time
    
    #为线程定义一个函数
    def print_time(threadName, delay):
        count = 0
        while count < 3:
            time.sleep(delay)
            count += 1
            print(threadName, time.ctime())
            
    _thread.start_new_thread(print_time, ("Thread-1", 1))
    _thread.start_new_thread(print_time, ("Thread-2", 2))
    print("Main Finished")
    

    输出结果如下:

    Main Finished
    Thread-1 Wed Apr 8 21:44:57 2020
    Thread-2 Wed Apr 8 21:44:58 2020
    Thread-1 Wed Apr 8 21:44:58 2020
    Thread-1 Wed Apr 8 21:44:59 2020
    Thread-2 Wed Apr 8 21:45:00 2020
    Thread-2 Wed Apr 8 21:45:02 2020


    (2)类包装式:调用Threading库创建线程,从threading.Thread继承

    • run():用以表示线程活动的方法。
    • start():启动线程活动。
    • join([time]):等待至线程至终止。阻塞调用线程直至线程的join()方法被调用为止。
    • isAlive():返回线程是否是活动的。
    • getName():返回线程名。
    • setName():设置线程名。
    import threading
    import time
    
    class MyThread(threading.Thread):
        def __init__(self, name, delay):
            threading.Thread.__init__(self)
            self.name = name
            self.delay = delay
        def run(self):
            print("Starting " + self.name)
            print_time(self.name, self.delay)
            print("Exiting " + self.name)
    
    def print_time(threadingName, delay):
        counter = 0
        while counter < 3:
            time.sleep(delay)
            print(threadingName, time.ctime())
            counter += 1
    
    threads = []
    
    #创建新线程
    thread1 = MyThread("Thread-1", 1)
    thread2 = MyThread("Thread-2", 2)
    
    #开启新线程
    thread1.start()
    thread2.start()
    
    #添加线程到线程列表
    threads.append(thread1)
    threads.append(thread2)
    
    #等待所有线程完成
    for t in threads:
        t.join()
        
    print("Exiting Main Thread")
    

    结果如下:
    Starting Thread-1
    Starting Thread-2
    Thread-1 Wed Apr 8 21:53:14 2020
    Thread-1 Wed Apr 8 21:53:15 2020
    Thread-2 Wed Apr 8 21:53:15 2020
    Thread-1 Wed Apr 8 21:53:16 2020
    Exiting Thread-1
    Thread-2 Wed Apr 8 21:53:17 2020
    Thread-2 Wed Apr 8 21:53:19 2020
    Exiting Thread-2
    Exiting Main Thread

    展开全文
  • 多线程----异步爬虫(光速下载) 1.使用到的重要的模块: (1).threading #线程相关的模块 (2).queue模块中的Queue类 #构建线程安全队列 2.知识点 整个代码模式:采用了生产者与消费者模式。 线程创建方式:采用类对象...

    多线程----异步爬虫(光速下载)

    1.使用到的重要的模块:

    (1).threading #线程相关的模块

    (2).queue模块中的Queue类 #构建线程安全队列

    2.知识点

    整个代码模式:采用了生产者与消费者模式。

    线程创建方式:采用类对象的封装。

    为了让线程代码更好的封装。采用threading模块下的Thread类,继承自这个类,然后实现run方法,线程就会自动运行run方法中的代码。

    用类封装线程对象案例:

    import threading
    import time
    #继承threading.Thread类,会自动执行该类下的run函数
    class CodingThread(threading.Thread):
    	def run(self):
            for x in range(3):
                print(x)
                time.sleep(2)
    class DrawingThread(threading.Thread):
        def run(self):
            for x in range(3,7):
    			print(x)
                time.sleep(2)
    
    def main():
        #创建3个CodingThread线程
        for x in range(3):
        	t1=CodingThread()
        	t1.start()
        #创建3个DrawingThread线程
        for x in range(3):
        	t2=DrawingThread()
        	t2.start()
      
        
    
    
    if __name__ == '__main__':
        main()
    

    3.代码简介:

    1.两个类线程,一个Producer线程,一个Conusmer线程。

    Producer的作用是获得每张图片的url即img_url

    Consumer作用是下载图片,并保存。

    2.代码中用了两个队列,一个是page_queue ,一个是img_queue

    page_queue作用是存放每一页的url

    img_queue作用存放每个图片的url

    4.代码过程分析:

    (1).首先main函数,创建两个队列,page_queue,img_queue.

    通过循环,得到每一页的网址,并且利用page_queue.put(url)方法将每一页的url放入队列page_queue中。

    (2)进入Producer类,看run函数,将之前放入page_queue队列中的url取出,通过get_imgurl函数得到每个图像的img_url,再通过img_queue.put()方法将文件名file_name和img_url放进img_queue队列中。

    (3).进入Consumer类,看run函数,从img_queue队列中取出file_name和img_url,然后通过一些代码,将图片保存。

    # --*encoding:utf-8*--
    
    import requests
    from lxml import etree
    import os
    import threading
    import re
    # from urllib import request
    from queue import Queue
    
    
    class Producer(threading.Thread):
        headers = {
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36"}
    
        # *args使得可以传入任意数量位置的参数,**kwargs任意关键字参数
        def __init__(self, page_queue, img_queue, *args, **kwargs):
            # 调用Producer的父类threading,Thread
            super(Producer, self).__init__(*args, **kwargs)
            self.page_queue = page_queue
            self.img_queue = img_queue
    
        def get_imgurl(self, url):
    
            res = requests.get(url, headers=self.headers)
            html = etree.HTML(res.content.decode("utf-8", "ignore"))
            imgs = html.xpath("//div[@class='random_article']//img[@class!='gif']")
            for img in imgs:
                img_url = img.get("data-original")
                #alt是对这个图片的描述,可作为图片名
                alt = img.get("alt")
                #图片名中有些有“?”号,需要去除,否则不能存入文件到本地
                alt = alt.replace("?", "")
                # 用splitext分割image_url得到后缀
                end = os.path.splitext(img_url)[1]
                # end=img_url.split(".",4)[3]
                file_name = alt + end
                # { 一行代码将网上数据保存本地
                # 参数,第一个是url,第二个是保存路径
                # request.urlretrieve(img_url,dir_path+"\\"+file_name)}
                self.img_queue.put((img_url,file_name))
    
        def run(self):
            # 线程需要不断的,使用
            while True:
                if self.page_queue.empty():
                    break
                # 不断从page_queue队列中获得url 即页面的url
                url = self.page_queue.get()
                self.get_imgurl(url)
    
    
    class Consumer(threading.Thread):
        headers = {
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36"}
    
        def __init__(self, page_queue, img_queue, *args, **kwargs):
            # 调用Producer的父类threading,Thread
            super(Consumer, self).__init__(*args, **kwargs)
            self.page_queue = page_queue
            self.img_queue = img_queue
    
        def run(self):
            while True:
                img_url, file_name = self.img_queue.get()
                if self.page_queue.empty() and self.img_queue.empty():
                    break
                r = requests.get(img_url, headers=self.headers)
                with open(file_name, 'wb')as f:
                    f.write(r.content)
                    print(file_name + "下载成功")
    
    
    def main():
        url_lst = []
        # 创建一个页面url数量为100的队列   和一个1000的队列  这1000是指每页有图片大概的数量
        page_queue = Queue(100)
        img_queue = Queue(1000)
    
        # 100页
        for i in range(1, 101):
            url = "https://www.doutula.com/article/list/?page={index}".format(index=i)
            page_queue.put(url)
        # 创建16个线程
        for x in range(16):
            t1 = Producer(page_queue, img_queue)
            t1.start()
        for x in range(16):
            t2 = Consumer(page_queue, img_queue)
            t2.start()
    
    
    if __name__ == "__main__":
        main()
    
    
    
    

    需要注意的:

    1.img_queue.put((file_name,img_url))这里是通过元组将两个数据存放入队列,不能去掉(),否则,会报错。put方法接受两个参数,第一个是要存放的数据,第二个参数是默认参数block,

    block=False,强制设置为不阻塞(默认为会阻塞的)
    

    全代码:

    保存地址,为当前运行目录。

    展开全文
  • 所以弄了个多线程爬虫。 这次的思路之前的不一样,之前是一章一章的爬,每爬一章就写入一章的内容。这次我新增加了一个字典用于存放每章爬取完的内容,最后当每个线程都爬取完之后,再将所有信息写入到文件中。 ...
  • 异步爬虫是爬虫中的最重要的环节:一是多线程,二是协程。 一、基于线程池的异步爬虫: 准备工作:为了营造更好的实验效果需要自己搭建一个服务器,django搭可以,flask也要的,按理用flask好一点,几行代码就能跑个...
  • (1)多线程 / 多进程(不建议):好处:可以为相关阻塞的操作单独开启线程或者进程,阻塞操作就可以异步执行。弊端:无法无限制的开启多线程或者多进程。 (2)线程池 / 进程池:好处:可以降低系统对进程或者线程...
  • Python3爬虫系列的理论验证,比较同步依序下载、多进程并发、多线程并发asyncio异步编程之间的效率差别
  • 异步爬虫的方式: - [1] 多线程、多进程 优点:可以为相关的阻塞单独开启,然后就可以异步执行 缺点:无法无限制的开启 - [2] 线程池、进程池 优点:降低他的消失频率 缺点:池中的进程有上限 那么我的建议就是使用...
  • 摘要: 简介 asyncio可以实现单线程并发IO操作,是Python中常用的异步处理模块。关于asyncio模块的介绍,笔者会在后续的文章中加以介绍,本文将会讲述一个基于asyncio实现的HTTP框架——aiohttp,它可以帮助我们异步...
  • 出品:Python数据之道(ID:PyDataLab)作者:叶庭云编辑:LemonPython异步爬虫进阶必备,效率杠杠的!爬虫是 IO 密集型任务,比如我们使用 requests 库来...
  • 进程、线程、协程 首先我们先来了解一下python中的进程、线程和协程。 进程和线程 从计算机硬件角度: 计算机的核心是CPU,...打个比方,我们打开QQ,这就创建了一个进程,而在QQ里打开个聊天窗口别人聊天,这就
  • 黑名单、限制访问频率、检测HTTP头等这些都是常见的策略,不按常理出牌的也有检测到爬虫行为,就往里注入假数据返回,以假乱真,但为了良好的用户体验,一般都不会这么做。在遇有反采集、IP地址不够的时候,通常我们...
  • Python 爬虫提速:【多进程、多线程、协程+异步】对比测试概念介绍测试环境开始测试测试【单线程单进程】测试【多进程 并行】测试【多线程 并发】测试【协程 + 异步】结果对比 Python 爬虫提速:【多进程、多线程、...
  • python异步爬虫实现与总结

    热门讨论 2021-05-10 19:22:04
    通过构建线程池或者进程池完成异步爬虫,即使用多线程或者多进程来处理多个请求(在别的进程或者线程阻塞时)。 import time #串形 def getPage(url): print("开始爬取网站",url) time.sleep(2)#阻塞 print(...
  • python爬虫多线程、多进程爬虫

    万次阅读 多人点赞 2019-05-09 17:22:00
    多线程爬虫效率提高是非凡的,当我们使用python的多线程有几点是需要我们知道的: 1.Python的多线程并不如java的多线程,其差异在于当python解释器开始执行任务时,受制于GIL(全局解释所),Python 的线程被限制...
  • 要学习提升爬虫速度用到的知识,必须先熟悉并发并行、同步和异步的概 一、并发并行,同步和异步 并发并行 并发(concurrency)并行(parallelism)是两个相似的概念。并发是指在一个时间段内发生若干事件的...
  • Python异步爬虫

    2021-01-29 15:58:48
    今天我们一起来学习下异步爬虫的相关内容。 一、基本概念 阻塞 阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续处理其他的事情,则称该程序在该操作上是阻塞的。常见...
  • 1、线程池2、协程2.1代理池、协程分页爬取图片 1、线程池 模拟阻塞 import time #导入线程池模块对应的类 from multiprocessing.dummy import Pool #使用线程池方式执行 start_time = time.time() def get_page...
  • python的多线程比较鸡肋,使用tornado可以实现异步的爬取,代码也比较简单,使用了coroutine后也可以不用回调了。代码如下,最后是时间测试,当网络阻塞或者请求数量多了,异步的优势就体现出来了。#!/usr/bin/env ...
  • 多进程 多线程 异步 爬虫(1)

    千次阅读 2017-05-29 20:54:29
    **多进程 多线程 异步 爬虫 忽略爬虫具体规则策略cookie登录等,专注高性能,高并发。**初步爬取煎蛋图片,存图片链接到mongodb#!/usr/bin/python #-*- coding: utf-8 -*- import os import json import functools ...
  • 异步爬虫的原理解析 爬虫是 IO 密集型任务,比如如果我们使用 requests 库来爬取某个站点的话,发出一个请求之后,程序必须要等待网站返回响应之后才能接着运行,而在等待响应的过程中,整个爬虫程序是一直在等待...
  • 小伙伴们很喜欢给小编出各种难题,比如今天关于框架,有小伙伴在浏览时,看到别人咨询异步还有多线程,因为自己也不是很理解,于是把问题转发给小编看,小编仔细看了下,虽然跟我们现在课程学习并没有什么相互关联的...
  • 文章目录Python创建多线程的方法使用最基本的方法爬取数据分别使用单线程和多线程比较程序执行速度两者相差19倍,从而体现出使用多线程的必要性。 Python创建多线程的方法 # 1.准备一个函数 def my_func(a, b): ...
  • Python实战异步爬虫(协程)+分布式爬虫(进程)

    万次阅读 多人点赞 2019-01-24 21:32:32
    引言:我们在写爬虫时常会遇到这样的问题,当需要爬取个URL时,写一个普通的基于requests...对于这样的任务,可以使用基于进程(multiprocessing)基于Asyncio库的异步(协程)爬虫增强并发性,加速爬虫。 T...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 16,315
精华内容 6,526
关键字:

多线程和异步爬虫效率比较

爬虫 订阅