Python爬蟲(chóng)實(shí)戰(zhàn):?jiǎn)尉€程、多線程和協(xié)程性能對(duì)比
一、前言
今天我要給大家分享的是如何爬取中農(nóng)網(wǎng)產(chǎn)品報(bào)價(jià)數(shù)據(jù),并分別用普通的單線程、多線程和協(xié)程來(lái)爬取,從而對(duì)比單線程、多線程和協(xié)程在網(wǎng)絡(luò)爬蟲(chóng)中的性能。
目標(biāo)URL:https://www.zhongnongwang.com/quote/product-htm-page-1.html
爬取產(chǎn)品品名、最新報(bào)價(jià)、單位、報(bào)價(jià)數(shù)、報(bào)價(jià)時(shí)間等信息,保存到本地Excel。
二、爬取測(cè)試
翻頁(yè)查看 URL 變化規(guī)律:
- https://www.zhongnongwang.com/quote/product-htm-page-1.html
 - https://www.zhongnongwang.com/quote/product-htm-page-2.html
 - https://www.zhongnongwang.com/quote/product-htm-page-3.html
 - https://www.zhongnongwang.com/quote/product-htm-page-4.html
 - https://www.zhongnongwang.com/quote/product-htm-page-5.html
 - https://www.zhongnongwang.com/quote/product-htm-page-6.html
 
檢查網(wǎng)頁(yè),可以發(fā)現(xiàn)網(wǎng)頁(yè)結(jié)構(gòu)簡(jiǎn)單,容易解析和提取數(shù)據(jù)。
思路:每一條產(chǎn)品報(bào)價(jià)信息在 class 為 tb 的 table 標(biāo)簽下的 tbody 下的 tr 標(biāo)簽里,獲取到所有 tr 標(biāo)簽的內(nèi)容,然后遍歷,從中提取出每一個(gè)產(chǎn)品品名、最新報(bào)價(jià)、單位、報(bào)價(jià)數(shù)、報(bào)價(jià)時(shí)間等信息。
- # -*- coding: UTF-8 -*-
 - """
 - @File :demo.py
 - @Author :葉庭云
 - @CSDN :https://yetingyun.blog.csdn.net/
 - """
 - import requests
 - import logging
 - from fake_useragent import UserAgent
 - from lxml import etree
 - # 日志輸出的基本配置
 - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
 - # 隨機(jī)產(chǎn)生請(qǐng)求頭
 - ua = UserAgent(verify_ssl=False, path='fake_useragent.json')
 - url = 'https://www.zhongnongwang.com/quote/product-htm-page-1.html'
 - # 偽裝請(qǐng)求頭
 - headers = {
 - "Accept-Encoding": "gzip", # 使用gzip壓縮傳輸數(shù)據(jù)讓訪問(wèn)更快
 - "User-Agent": ua.random
 - }
 - # 發(fā)送請(qǐng)求 獲取響應(yīng)
 - rep = requests.get(url, headersheaders=headers)
 - print(rep.status_code) # 200
 - # Xpath定位提取數(shù)據(jù)
 - html = etree.HTML(rep.text)
 - items = html.xpath('/html/body/div[10]/table/tr[@align="center"]')
 - logging.info(f'該頁(yè)有多少條信息:{len(items)}') # 一頁(yè)有20條信息
 - # 遍歷提取出數(shù)據(jù)
 - for item in items:
 - name = ''.join(item.xpath('.//td[1]/a/text()')) # 品名
 - price = ''.join(item.xpath('.//td[3]/text()')) # 最新報(bào)價(jià)
 - unit = ''.join(item.xpath('.//td[4]/text()')) # 單位
 - nums = ''.join(item.xpath('.//td[5]/text()')) # 報(bào)價(jià)數(shù)
 - time_ = ''.join(item.xpath('.//td[6]/text()')) # 報(bào)價(jià)時(shí)間
 - logging.info([name, price, unit, nums, time_])
 
運(yùn)行結(jié)果如下:
可以成功爬取到數(shù)據(jù),接下來(lái)分別用普通的單線程、多線程和協(xié)程來(lái)爬取 50 頁(yè)的數(shù)據(jù)、保存到Excel。
三、單線程爬蟲(chóng)
- # -*- coding: UTF-8 -*-
 - """
 - @File :?jiǎn)尉€程.py
 - @Author :葉庭云
 - @CSDN :https://yetingyun.blog.csdn.net/
 - """
 - import requests
 - import logging
 - from fake_useragent import UserAgent
 - from lxml import etree
 - import openpyxl
 - from datetime import datetime
 - # 日志輸出的基本配置
 - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
 - # 隨機(jī)產(chǎn)生請(qǐng)求頭
 - ua = UserAgent(verify_ssl=False, path='fake_useragent.json')
 - wb = openpyxl.Workbook()
 - sheet = wb.active
 - sheet.append(['品名', '最新報(bào)價(jià)', '單位', '報(bào)價(jià)數(shù)', '報(bào)價(jià)時(shí)間'])
 - start = datetime.now()
 - for page in range(1, 51):
 - # 構(gòu)造URL
 - url = f'https://www.zhongnongwang.com/quote/product-htm-page-{page}.html'
 - # 偽裝請(qǐng)求頭
 - headers = {
 - "Accept-Encoding": "gzip", # 使用gzip壓縮傳輸數(shù)據(jù)讓訪問(wèn)更快
 - "User-Agent": ua.random
 - }
 - # 發(fā)送請(qǐng)求 獲取響應(yīng)
 - rep = requests.get(url, headersheaders=headers)
 - # print(rep.status_code)
 - # Xpath定位提取數(shù)據(jù)
 - html = etree.HTML(rep.text)
 - items = html.xpath('/html/body/div[10]/table/tr[@align="center"]')
 - logging.info(f'該頁(yè)有多少條信息:{len(items)}') # 一頁(yè)有20條信息
 - # 遍歷提取出數(shù)據(jù)
 - for item in items:
 - name = ''.join(item.xpath('.//td[1]/a/text()')) # 品名
 - price = ''.join(item.xpath('.//td[3]/text()')) # 最新報(bào)價(jià)
 - unit = ''.join(item.xpath('.//td[4]/text()')) # 單位
 - nums = ''.join(item.xpath('.//td[5]/text()')) # 報(bào)價(jià)數(shù)
 - time_ = ''.join(item.xpath('.//td[6]/text()')) # 報(bào)價(jià)時(shí)間
 - sheet.append([name, price, unit, nums, time_])
 - logging.info([name, price, unit, nums, time_])
 - wb.save(filename='data1.xlsx')
 - delta = (datetime.now() - start).total_seconds()
 - logging.info(f'用時(shí):{delta}s')
 
運(yùn)行結(jié)果如下:
單線程爬蟲(chóng)必須上一個(gè)頁(yè)面爬取完成才能繼續(xù)爬取,還可能受當(dāng)時(shí)網(wǎng)絡(luò)狀態(tài)影響,用時(shí)48.528703s,才將數(shù)據(jù)爬取完,速度比較慢。
四、多線程爬蟲(chóng)
- # -*- coding: UTF-8 -*-
 - """
 - @File :多線程.py
 - @Author :葉庭云
 - @CSDN :https://yetingyun.blog.csdn.net/
 - """
 - import requests
 - import logging
 - from fake_useragent import UserAgent
 - from lxml import etree
 - import openpyxl
 - from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED
 - from datetime import datetime
 - # 日志輸出的基本配置
 - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
 - # 隨機(jī)產(chǎn)生請(qǐng)求頭
 - ua = UserAgent(verify_ssl=False, path='fake_useragent.json')
 - wb = openpyxl.Workbook()
 - sheet = wb.active
 - sheet.append(['品名', '最新報(bào)價(jià)', '單位', '報(bào)價(jià)數(shù)', '報(bào)價(jià)時(shí)間'])
 - start = datetime.now()
 - def get_data(page):
 - # 構(gòu)造URL
 - url = f'https://www.zhongnongwang.com/quote/product-htm-page-{page}.html'
 - # 偽裝請(qǐng)求頭
 - headers = {
 - "Accept-Encoding": "gzip", # 使用gzip壓縮傳輸數(shù)據(jù)讓訪問(wèn)更快
 - "User-Agent": ua.random
 - }
 - # 發(fā)送請(qǐng)求 獲取響應(yīng)
 - rep = requests.get(url, headersheaders=headers)
 - # print(rep.status_code)
 - # Xpath定位提取數(shù)據(jù)
 - html = etree.HTML(rep.text)
 - items = html.xpath('/html/body/div[10]/table/tr[@align="center"]')
 - logging.info(f'該頁(yè)有多少條信息:{len(items)}') # 一頁(yè)有20條信息
 - # 遍歷提取出數(shù)據(jù)
 - for item in items:
 - name = ''.join(item.xpath('.//td[1]/a/text()')) # 品名
 - price = ''.join(item.xpath('.//td[3]/text()')) # 最新報(bào)價(jià)
 - unit = ''.join(item.xpath('.//td[4]/text()')) # 單位
 - nums = ''.join(item.xpath('.//td[5]/text()')) # 報(bào)價(jià)數(shù)
 - time_ = ''.join(item.xpath('.//td[6]/text()')) # 報(bào)價(jià)時(shí)間
 - sheet.append([name, price, unit, nums, time_])
 - logging.info([name, price, unit, nums, time_])
 - def run():
 - # 爬取1-50頁(yè)
 - with ThreadPoolExecutor(max_workers=6) as executor:
 - future_tasks = [executor.submit(get_data, i) for i in range(1, 51)]
 - wait(future_tasks, return_when=ALL_COMPLETED)
 - wb.save(filename='data2.xlsx')
 - delta = (datetime.now() - start).total_seconds()
 - print(f'用時(shí):{delta}s')
 - run()
 
運(yùn)行結(jié)果如下:
多線程爬蟲(chóng)爬取效率提升非??捎^,用時(shí) 2.648128s,爬取速度很快。
五、異步協(xié)程爬蟲(chóng)
- # -*- coding: UTF-8 -*-
 - """
 - @File :demo1.py
 - @Author :葉庭云
 - @CSDN :https://yetingyun.blog.csdn.net/
 - """
 - import aiohttp
 - import asyncio
 - import logging
 - from fake_useragent import UserAgent
 - from lxml import etree
 - import openpyxl
 - from datetime import datetime
 - # 日志輸出的基本配置
 - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
 - # 隨機(jī)產(chǎn)生請(qǐng)求頭
 - ua = UserAgent(verify_ssl=False, path='fake_useragent.json')
 - wb = openpyxl.Workbook()
 - sheet = wb.active
 - sheet.append(['品名', '最新報(bào)價(jià)', '單位', '報(bào)價(jià)數(shù)', '報(bào)價(jià)時(shí)間'])
 - start = datetime.now()
 - class Spider(object):
 - def __init__(self):
 - # self.semaphore = asyncio.Semaphore(6) # 信號(hào)量,有時(shí)候需要控制協(xié)程數(shù),防止爬的過(guò)快被反爬
 - self.header = {
 - "Accept-Encoding": "gzip", # 使用gzip壓縮傳輸數(shù)據(jù)讓訪問(wèn)更快
 - "User-Agent": ua.random
 - }
 - async def scrape(self, url):
 - # async with self.semaphore: # 設(shè)置最大信號(hào)量,有時(shí)候需要控制協(xié)程數(shù),防止爬的過(guò)快被反爬
 - session = aiohttp.ClientSession(headers=self.header, connector=aiohttp.TCPConnector(ssl=False))
 - response = await session.get(url)
 - result = await response.text()
 - await session.close()
 - return result
 - async def scrape_index(self, page):
 - url = f'https://www.zhongnongwang.com/quote/product-htm-page-{page}.html'
 - text = await self.scrape(url)
 - await self.parse(text)
 - async def parse(self, text):
 - # Xpath定位提取數(shù)據(jù)
 - html = etree.HTML(text)
 - items = html.xpath('/html/body/div[10]/table/tr[@align="center"]')
 - logging.info(f'該頁(yè)有多少條信息:{len(items)}') # 一頁(yè)有20條信息
 - # 遍歷提取出數(shù)據(jù)
 - for item in items:
 - name = ''.join(item.xpath('.//td[1]/a/text()')) # 品名
 - price = ''.join(item.xpath('.//td[3]/text()')) # 最新報(bào)價(jià)
 - unit = ''.join(item.xpath('.//td[4]/text()')) # 單位
 - nums = ''.join(item.xpath('.//td[5]/text()')) # 報(bào)價(jià)數(shù)
 - time_ = ''.join(item.xpath('.//td[6]/text()')) # 報(bào)價(jià)時(shí)間
 - sheet.append([name, price, unit, nums, time_])
 - logging.info([name, price, unit, nums, time_])
 - def main(self):
 - # 50頁(yè)的數(shù)據(jù)
 - scrape_index_tasks = [asyncio.ensure_future(self.scrape_index(page)) for page in range(1, 51)]
 - loop = asyncio.get_event_loop()
 - tasks = asyncio.gather(*scrape_index_tasks)
 - loop.run_until_complete(tasks)
 - if __name__ == '__main__':
 - spider = Spider()
 - spider.main()
 - wb.save('data3.xlsx')
 - delta = (datetime.now() - start).total_seconds()
 - print("用時(shí):{:.3f}s".format(delta))
 
運(yùn)行結(jié)果如下:
而到了協(xié)程異步爬蟲(chóng),爬取速度更快,嗖的一下,用時(shí) 0.930s 就爬取完 50 頁(yè)數(shù)據(jù),aiohttp + asyncio 異步爬蟲(chóng)竟恐怖如斯。異步爬蟲(chóng)在服務(wù)器能承受高并發(fā)的前提下增加并發(fā)數(shù)量,爬取效率提升是非常可觀的,比多線程還要快一些。
三種爬蟲(chóng)都將 50 頁(yè)的數(shù)據(jù)爬取下來(lái)保存到了本地,結(jié)果如下:
六、總結(jié)回顧
今天我演示了簡(jiǎn)單的單線程爬蟲(chóng)、多線程爬蟲(chóng)和協(xié)程異步爬蟲(chóng)??梢钥吹揭话闱闆r下異步爬蟲(chóng)速度最快,多線程爬蟲(chóng)略慢一點(diǎn),單線程爬蟲(chóng)速度較慢,必須上一個(gè)頁(yè)面爬取完成才能繼續(xù)爬取。
但協(xié)程異步爬蟲(chóng)相對(duì)來(lái)說(shuō)并不是那么好編寫(xiě),數(shù)據(jù)抓取無(wú)法使用 request 庫(kù),只能使用aiohttp,而且爬取數(shù)據(jù)量大時(shí),異步爬蟲(chóng)需要設(shè)置最大信號(hào)量來(lái)控制協(xié)程數(shù),防止爬的過(guò)快被反爬。所以在實(shí)際編寫(xiě) Python 爬蟲(chóng)時(shí),我們一般都會(huì)使用多線程爬蟲(chóng)來(lái)提速,但必須注意的是網(wǎng)站都有 ip 訪問(wèn)頻率限制,爬的過(guò)快可能會(huì)被封ip,所以一般我們?cè)诙嗑€程提速的同時(shí)可以使用代理 ip 來(lái)并發(fā)地爬取數(shù)據(jù)。
- 多線程(multithreading):是指從軟件或者硬件上實(shí)現(xiàn)多個(gè)線程并發(fā)執(zhí)行的技術(shù)。具有多線程能力的計(jì)算機(jī)因有硬件支持而能夠在同一時(shí)間執(zhí)行多于一個(gè)線程,進(jìn)而提升整體處理性能。具有這種能力的系統(tǒng)包括對(duì)稱多處理機(jī)、多核心處理器以及芯片級(jí)多處理或同時(shí)多線程處理器。在一個(gè)程序中,這些獨(dú)立運(yùn)行的程序片段叫作 "線程" (Thread),利用它編程的概念就叫作 "多線程處理"。
 - 異步(asynchronous):為完成某個(gè)任務(wù),不同程序單元之間過(guò)程中無(wú)需通信協(xié)調(diào),也能完成任務(wù)的方式,不相關(guān)的程序單元之間可以是異步的。例如,爬蟲(chóng)下載網(wǎng)頁(yè)。調(diào)度程序調(diào)用下載程序后,即可調(diào)度其他任務(wù),而無(wú)需與該下載任務(wù)保持通信以協(xié)調(diào)行為。不同網(wǎng)頁(yè)的下載、保存等操作都是無(wú)關(guān)的,也無(wú)需相互通知協(xié)調(diào)。這些異步操作的完成時(shí)刻并不確定。簡(jiǎn)言之,異步意味著無(wú)序。
 - 協(xié)程(coroutine),又稱微線程、纖程,協(xié)程是一種用戶態(tài)的輕量級(jí)線程。協(xié)程擁有自己的寄存器上下文和棧。協(xié)程調(diào)度切換時(shí),將寄存器上下文和棧保存到其他地方,在切回來(lái)的時(shí)候,恢復(fù)先前保存的寄存器上下文和棧。因此協(xié)程能保留上一次調(diào)用時(shí)的狀態(tài),即所有局部狀態(tài)的一個(gè)特定組合,每次過(guò)程重入時(shí),就相當(dāng)于進(jìn)入上一次調(diào)用的狀態(tài)。協(xié)程本質(zhì)上是個(gè)單進(jìn)程,協(xié)程相對(duì)于多進(jìn)程來(lái)說(shuō),無(wú)需線程上下文切換的開(kāi)銷,無(wú)需原子操作鎖定及同步的開(kāi)銷,編程模型也非常簡(jiǎn)單。我們可以使用協(xié)程來(lái)實(shí)現(xiàn)異步操作,比如在網(wǎng)絡(luò)爬蟲(chóng)場(chǎng)景下,我們發(fā)出一個(gè)請(qǐng)求之后,需要等待一定的時(shí)間才能得到響應(yīng),但其實(shí)在這個(gè)等待過(guò)程中,程序可以干許多其他的事情,等到響應(yīng)得到之后才切換回來(lái)繼續(xù)處理,這樣可以充分利用 CPU 和其他資源,這就是協(xié)程的優(yōu)勢(shì)。
 























 
 
 







 
 
 
 