大家好茫陆!我是霖hero
正所謂:有朋自遠(yuǎn)方來金麸,不亦樂乎?有朋友來找我們玩簿盅,是一件很快樂的事情挥下,那么我們要盡地主之誼,好好帶朋友去玩耍桨醋!那么問題來了棚瘟,什么時(shí)候去哪里玩最好呢,哪里玩的地方最多呢喜最?
今天將手把手教你使用線程池爬取同程旅行的景點(diǎn)信息及評(píng)論數(shù)據(jù)并作詞云偎蘸、數(shù)據(jù)可視化!!禀苦!帶你了解各個(gè)城市的游玩景點(diǎn)信息蔓肯。
在開始爬取數(shù)據(jù)之前,我們首先來了解一下線程振乏。
線程
進(jìn)程:進(jìn)程是代碼在數(shù)據(jù)集合上的一次運(yùn)行活動(dòng)蔗包,是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位。
線程:是輕量級(jí)的進(jìn)程慧邮,是程序執(zhí)行的最小單元调限,是進(jìn)程的一個(gè)執(zhí)行路徑。
一個(gè)進(jìn)程中至少有一個(gè)線程误澳,進(jìn)程中的多個(gè)線程共享進(jìn)程的資源耻矮。
線程生命周期
在創(chuàng)建多線程之前,我們先來學(xué)習(xí)一下線程生命周期忆谓,如下圖所示:
由圖可知裆装,線程可以分為五個(gè)狀態(tài)——新建、就緒倡缠、運(yùn)行哨免、阻塞、終止昙沦。
首先新建一個(gè)線程并開啟線程后線程進(jìn)入就緒狀態(tài)琢唾,就緒狀態(tài)的線程不會(huì)馬上運(yùn)行,要獲得CPU資源才會(huì)進(jìn)入運(yùn)行狀態(tài)盾饮,在進(jìn)入運(yùn)行狀態(tài)后采桃,線程有可能會(huì)失去CPU資源或者遇到休眠、io操作(讀寫等操作)線程進(jìn)入就緒狀態(tài)或者阻塞狀態(tài)丘损,要等休眠普办、io操作結(jié)束或者重新獲得CPU資源后,才會(huì)進(jìn)入運(yùn)行狀態(tài)徘钥,等到運(yùn)行完后進(jìn)入終止?fàn)顟B(tài)泌豆。
注意:新建線程系統(tǒng)是需要分配資源的,終止線程系統(tǒng)是需要回收資源的吏饿,那么如何減去新建/終止線程的系統(tǒng)開銷呢踪危,這時(shí)我們可以創(chuàng)建線程池來重用線程,這樣就可以減少系統(tǒng)的開銷了猪落。
在創(chuàng)建線程池之前贞远,我們先來學(xué)習(xí)如何創(chuàng)建多線程。
創(chuàng)建多線程
創(chuàng)建多線程可以分為四步:
- 創(chuàng)建函數(shù)笨忌;
- 創(chuàng)建線程蓝仲;
- 啟動(dòng)線程;
- 等待結(jié)束;
創(chuàng)建函數(shù)
為了方便演示袱结,我們拿博客園的網(wǎng)頁做爬蟲函數(shù)亮隙,具體代碼如下所示:
import requests
urls=[
f'https://www.cnblogs.com/#p{page}'
for page in range(1,50)
]
def get_parse(url):
response=requests.get(url)
print(url,len(response.text))
首先導(dǎo)入requests網(wǎng)絡(luò)請(qǐng)求庫,把我們所有的要爬取的URL保存在列表中垢夹,然后自定義函數(shù)get_parse來發(fā)送網(wǎng)絡(luò)請(qǐng)求溢吻、打印請(qǐng)求的URL和響應(yīng)的字符長度。
創(chuàng)建線程
在上一步我們創(chuàng)建了爬蟲函數(shù)果元,接下來將創(chuàng)建線程了促王,具體代碼如下所示:
import threading
#多線程
def multi_thread():
threads=[]
for url in urls:
threads.append(
threading.Thread(target=get_parse,args=(url,))
)
首先我們導(dǎo)入threading模塊,自定義multi_thread函數(shù)而晒,再創(chuàng)建一個(gè)空列表threads來存放線程任務(wù)蝇狼,通過threading.Thread()方法來創(chuàng)建線程。其中:
- target為運(yùn)行函數(shù)倡怎;
- args為運(yùn)行函數(shù)所需的參數(shù)迅耘。
注意args中的參數(shù)要以元組的方式傳入,然后通過.append()方法把線程添加到threads空列表中监署。
啟動(dòng)線程
線程已經(jīng)創(chuàng)建好了颤专,接下來將啟動(dòng)線程了,啟動(dòng)線程很簡(jiǎn)單焦匈,具體代碼如下所示:
for thread in threads:
thread.start()
首先我們通過for循環(huán)把threads列表中的線程任務(wù)獲取下來,通過.start()來啟動(dòng)線程昵仅。
等待結(jié)束
啟動(dòng)線程后缓熟,接下來將等待線程結(jié)束,具體代碼如下所示:
for thread in threads:
thread.join()
和啟動(dòng)線程一樣摔笤,先通過for循環(huán)把threads列表中的線程任務(wù)獲取下來够滑,再使用.join()方法等待線程結(jié)束。
多線程已經(jīng)創(chuàng)建好了吕世,接下來將測(cè)試一下多線程的速度如何彰触,具體代碼如下所示:
if __name__ == '__main__':
t1=time.time()
multi_thread()
t2=time.time()
print(t2-t1)
運(yùn)行結(jié)果如下圖所示:
多線程爬取50個(gè)博客園網(wǎng)頁只要1秒多,而且多線程的發(fā)送網(wǎng)絡(luò)請(qǐng)求的URL是隨機(jī)的命辖。
我們來測(cè)試一下單線程的運(yùn)行時(shí)間况毅,具體代碼如下所示:
if __name__ == '__main__':
t1=time.time()
for i in urls:
get_parse(i)
t2=time.time()
print(t2-t1)
運(yùn)行結(jié)果如下圖所示:
單線程爬取50個(gè)博客園網(wǎng)頁用了9秒多,單線程的發(fā)送網(wǎng)絡(luò)請(qǐng)求的URL是按順序的尔艇。
在上面我們說了尔许,新建線程系統(tǒng)是需要分配資源的,終止線程系統(tǒng)是需要回收資源的终娃,為了減少系統(tǒng)的開銷味廊,我們可以創(chuàng)建線程池。
線程池原理
一個(gè)線程池由兩部分組成,如下圖所示:
- 線程池:里面提前建好N個(gè)線程余佛,這些都會(huì)被重復(fù)利用柠新;
- 任務(wù)隊(duì)列:當(dāng)有新任務(wù)的時(shí)候,會(huì)把任務(wù)放在任務(wù)隊(duì)列中辉巡。
當(dāng)任務(wù)隊(duì)列里有任務(wù)時(shí)恨憎,線程池的線程會(huì)從任務(wù)隊(duì)列中取出任務(wù)并執(zhí)行,執(zhí)行完任務(wù)后红氯,線程會(huì)執(zhí)行下一個(gè)任務(wù)框咙,直到?jīng)]有任務(wù)執(zhí)行后,線程會(huì)回到線程池中等待任務(wù)痢甘。
使用線程池可以處理突發(fā)性大量請(qǐng)求或需要大量線程完成任務(wù)(處理時(shí)間較短的任務(wù))喇嘱。
好了,了解了線程池原理后塞栅,我們開始創(chuàng)建線程池者铜。
線程池創(chuàng)建
Python提供了ThreadPoolExecutor類來創(chuàng)建線程池,其語法如下所示:
ThreadPoolExecutor(max_workers=None, thread_name_prefix='', initializer=None, initargs=())
其中:
- max_workers:最大線程數(shù)放椰;
- thread_name_prefix:允許用戶控制由線程池創(chuàng)建的threading.Thread工作線程名稱以方便調(diào)試作烟;
- initializer:是在每個(gè)工作者線程開始處調(diào)用的一個(gè)可選可調(diào)用對(duì)象;
- initargs:傳遞給初始化器的元組參數(shù)砾医。
注意:在啟動(dòng) max_workers 個(gè)工作線程之前也會(huì)重用空閑的工作線程拿撩。
在ThreadPoolExecutor類中提供了map()和submit()函數(shù)來插入任務(wù)隊(duì)列。其中:
map()函數(shù)
map()語法格式為:
map(調(diào)用方法,參數(shù)隊(duì)列)
具體示例如下所示:
import requests
import concurrent.futures
import time
urls=[
f'https://www.cnblogs.com/#p{page}'
for page in range(1,50)
]
def get_parse(url):
response=requests.get(url)
return response.text
def map_pool():
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as pool:
htmls=pool.map(get_parse,urls)
htmls=list(zip(urls,htmls))
for url,html in htmls:
print(url,len(html))
if __name__ == '__main__':
t1=time.time()
map_pool()
t2=time.time()
print(t2-t1)
首先我們導(dǎo)入requests網(wǎng)絡(luò)請(qǐng)求庫如蚜、concurrent.futures模塊压恒,把所有的URL放在urls列表中,然后自定義get_parse()方法來返回網(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù)错邦,再自定義map_pool()方法來創(chuàng)建代理池探赫,其中代理池的最大max_workers為20,調(diào)用map()方法把網(wǎng)絡(luò)請(qǐng)求任務(wù)放在任務(wù)隊(duì)列中撬呢,在把返回的數(shù)據(jù)和URL合并為元組伦吠,并放在htmls列表中。
運(yùn)行結(jié)果如下圖所示:
可以發(fā)現(xiàn)map()函數(shù)返回的結(jié)果和傳入的參數(shù)順序是對(duì)應(yīng)的魂拦。
注意:當(dāng)我們直接在自定義方法get_parse()中打印結(jié)果時(shí)毛仪,打印結(jié)果是亂序的。
submit()函數(shù)
submit()函數(shù)語法格式如下:
submit(調(diào)用方法,參數(shù))
具體示例如下:
def submit_pool():
with concurrent.futures.ThreadPoolExecutor(max_workers=20)as pool:
futuress=[pool.submit(get_parse,url)for url in urls]
futures=zip(urls,futuress)
for url,future in futures:
print(url,len(future.result()))
運(yùn)行結(jié)果如下圖所示:
注意:submit()函數(shù)輸出結(jié)果需需要調(diào)用result()方法芯勘。
好了潭千,線程知識(shí)就學(xué)到這里了,接下來開始我們的爬蟲借尿。
爬前分析
首先我們進(jìn)入同程旅行的景點(diǎn)網(wǎng)頁并打開開發(fā)者工具刨晴,如下圖所示:
經(jīng)過尋找屉来,我們發(fā)現(xiàn)各個(gè)景點(diǎn)的基礎(chǔ)信息(詳情頁URL、景點(diǎn)id等)都存放在下圖的URL鏈接中狈癞,
其URL鏈接為:
https://www.ly.com/scenery/NewSearchList.aspx?&action=getlist&page=2&kw=&pid=6&cid=80&cyid=0&sort=&isnow=0&spType=&lbtypes=&IsNJL=0&classify=0&grade=&dctrack=1%CB%871629537670551030%CB%8720%CB%873%CB%872557287248299209%CB%870&iid=0.6901326566387387
經(jīng)過增刪改查操作茄靠,我們可以把該URL簡(jiǎn)化為:
https://www.ly.com/scenery/NewSearchList.aspx?&action=getlist&page=1&pid=6&cid=80&cyid=0&isnow=0&IsNJL=0
其中page為我們翻頁的重要參數(shù)。
打開該URL鏈接蝶桶,如下圖所示:
通過上面的URL鏈接慨绳,我們可以獲取到很多景點(diǎn)的基礎(chǔ)信息,隨機(jī)打開一個(gè)景點(diǎn)的詳情網(wǎng)頁并打開開發(fā)者模式真竖,經(jīng)過查找脐雪,評(píng)論數(shù)據(jù)存放在如下圖的URL鏈接中,
其URL鏈接如下所示:
https://www.ly.com/scenery/AjaxHelper/DianPingAjax.aspx?action=GetDianPingList&sid=12851&page=1&pageSize=10&labId=1&sort=0&iid=0.48901069375088
其中:action恢共、labId战秋、iid、sort為常量讨韭,sid是景點(diǎn)的id脂信,page控制翻頁,pageSize是每頁獲取的數(shù)據(jù)量透硝。
在上上步中狰闪,我們知道景點(diǎn)id的存放位置,那么構(gòu)造評(píng)論數(shù)據(jù)的URL就很簡(jiǎn)單了濒生。
實(shí)戰(zhàn)演練
這次我們爬蟲步驟是:
- 獲取景點(diǎn)基本信息
- 獲取評(píng)論數(shù)據(jù)
- 創(chuàng)建MySQL數(shù)據(jù)庫
- 保存數(shù)據(jù)
- 創(chuàng)建線程池
- 數(shù)據(jù)分析
獲取景點(diǎn)基本信息
首先我們先獲取景點(diǎn)的名字埋泵、id、價(jià)格罪治、特色丽声、地點(diǎn)和等級(jí),主要代碼如下所示:
def get_parse(url):
response=requests.get(url,headers=headers)
Xpath=parsel.Selector(response.text)
data=Xpath.xpath('/html/body/div')
for i in data:
Scenery_data={
'title':i.xpath('./div/div[1]/div[1]/dl/dt/a/text()').extract_first(),
'sid':i.xpath('//div[@class="list_l"]/div/@sid').extract_first(),
'Grade':i.xpath('./div/div[1]/div[1]/dl/dd[1]/span/text()').extract_first(),
'Detailed_address':i.xpath('./div/div[1]/div[1]/dl/dd[2]/p/text()').extract_first().replace('地址:',''),
'characteristic':i.xpath('./div/div[1]/div[1]/dl/dd[3]/p/text()').extract_first(),
'price':i.xpath('./div/div[1]/div[2]/div[1]/span/b/text()').extract_first(),
'place':i.xpath('./div/div[1]/div[1]/dl/dd[2]/p/text()').extract_first().replace('地址:','')[6:8]
}
首先自定義方法get_parse()來發(fā)送網(wǎng)絡(luò)請(qǐng)求后使用parsel.Selector()方法來解析響應(yīng)的文本數(shù)據(jù)规阀,然后通過xpath來獲取數(shù)據(jù)恒序。
獲取評(píng)論數(shù)據(jù)
獲取景點(diǎn)基本信息后瘦麸,接下來通過景點(diǎn)基本信息中的sid來構(gòu)造評(píng)論信息的URL鏈接谁撼,主要代碼如下所示:
def get_data(Scenery_data):
for i in range(1,3):
link = f'https://www.ly.com/scenery/AjaxHelper/DianPingAjax.aspx?action=GetDianPingList&sid={Scenery_data["sid"]}&page={i}&pageSize=100&labId=1&sort=0&iid=0.20105777381446832'
response=requests.get(link,headers=headers)
Json=response.json()
commtent_detailed=Json.get('dpList')
# 有評(píng)論數(shù)據(jù)
if commtent_detailed!=None:
for i in commtent_detailed:
Comment_information={
'dptitle':Scenery_data['title'],
'dpContent':i.get('dpContent'),
'dpDate':i.get('dpDate')[5:7],
'lineAccess':i.get('lineAccess')
}
#沒有評(píng)論數(shù)據(jù)
elif commtent_detailed==None:
Comment_information={
'dptitle':Scenery_data['title'],
'dpContent':'沒有評(píng)論',
'dpDate':'沒有評(píng)論',
'lineAccess':'沒有評(píng)論'
}
首先自定義方法get_data()并傳入剛才獲取的景點(diǎn)基礎(chǔ)信息數(shù)據(jù),然后通過景點(diǎn)基礎(chǔ)信息的sid來構(gòu)造評(píng)論數(shù)據(jù)的URL鏈接滋饲,當(dāng)在構(gòu)造評(píng)論數(shù)據(jù)的URL時(shí)厉碟,需要設(shè)置pageSize和page這兩個(gè)變量來獲取多條評(píng)論和進(jìn)行翻頁,構(gòu)造URL鏈接后就發(fā)送網(wǎng)絡(luò)請(qǐng)求屠缭。
這里需要注意的是:有些景點(diǎn)是沒有評(píng)論箍鼓,所以我們需要通過if語句來進(jìn)行設(shè)置。
創(chuàng)建MySQL數(shù)據(jù)庫
這次我們把數(shù)據(jù)存放在MySQL數(shù)據(jù)庫中呵曹,由于數(shù)據(jù)比較多款咖,所以我們把數(shù)據(jù)分為兩種數(shù)據(jù)表何暮,一種是景點(diǎn)基礎(chǔ)信息表,一種是景點(diǎn)評(píng)論數(shù)據(jù)表铐殃,主要代碼如下所示:
#創(chuàng)建數(shù)據(jù)庫
def create_db():
db=pymysql.connect(host=host,user=user,passwd=passwd,port=port)
cursor=db.cursor()
sql='create database if not exists commtent default character set utf8'
cursor.execute(sql)
db.close()
create_table()
#創(chuàng)建景點(diǎn)信息數(shù)據(jù)表
def create_table():
db=pymysql.connect(host=host,user=user,passwd=passwd,port=port,db='commtent')
cursor=db.cursor()
sql = 'create table if not exists Scenic_spot_data (title varchar(255) not null, link varchar(255) not null,Grade varchar(255) not null, Detailed_address varchar(255) not null, characteristic varchar(255)not null, price int not null, place varchar(255) not null)'
cursor.execute(sql)
db.close()
首先我們調(diào)用pymysql.connect()方法來連接數(shù)據(jù)庫海洼,通過.cursor()獲取游標(biāo),再通過.execute()方法執(zhí)行單條的sql語句富腊,執(zhí)行成功后返回受影響的行數(shù)坏逢,然后關(guān)閉數(shù)據(jù)庫連接,最后調(diào)用自定義方法create_table()來創(chuàng)建景點(diǎn)信息數(shù)據(jù)表赘被。
這里我們只給出了創(chuàng)建景點(diǎn)信息數(shù)據(jù)表的代碼是整,因?yàn)閯?chuàng)建數(shù)據(jù)表只是sql這條語句稍微有點(diǎn)不同,其他都一樣民假,大家可以參考這代碼來創(chuàng)建各個(gè)景點(diǎn)評(píng)論數(shù)據(jù)表浮入。
保存數(shù)據(jù)
創(chuàng)建好數(shù)據(jù)庫和數(shù)據(jù)表后,接下來就要保存數(shù)據(jù)了阳欲,主要代碼如下所示:
#保存景點(diǎn)數(shù)據(jù)到景點(diǎn)數(shù)據(jù)表中
def saving_scenery_data(srr):
db = pymysql.connect(host=host, user=user, password=passwd, port=port, db='commtent')
cursor = db.cursor()
sql = 'insert into Scenic_spot_data(title, link, Grade, Detailed_address, characteristic,price,place) values(%s,%s,%s,%s,%s,%s,%s)'
try:
cursor.execute(sql, srr)
db.commit()
except:
db.rollback()
db.close()
首先我們調(diào)用pymysql.connect()方法來連接數(shù)據(jù)庫舵盈,通過.cursor()獲取游標(biāo),再通過.execute()方法執(zhí)行單條的sql語句球化,執(zhí)行成功后返回受影響的行數(shù)秽晚,使用了try-except語句,當(dāng)保存的數(shù)據(jù)不成功筒愚,就調(diào)用rollback()方法赴蝇,撤消當(dāng)前事務(wù)中所做的所有更改,并釋放此連接對(duì)象當(dāng)前使用的任何數(shù)據(jù)庫鎖巢掺。
注意:srr是傳入的景點(diǎn)信息數(shù)據(jù)句伶。
創(chuàng)建線程池
好了,單線程爬蟲已經(jīng)寫好了陆淀,接下來將創(chuàng)建一個(gè)函數(shù)來創(chuàng)建我們的線程池考余,使單線程爬蟲變?yōu)槎嗑€程,主要代碼如下所示:
urls = [
f'https://www.ly.com/scenery/NewSearchList.aspx?&action=getlist&page={i}&pid=6&cid=80&cyid=0&isnow=0&IsNJL=0'
for i in range(1, 6)
]
def multi_thread():
with concurrent.futures.ThreadPoolExecutor(max_workers=8)as pool:
h=pool.map(get_parse,urls)
if __name__ == '__main__':
create_db()
multi_thread()
創(chuàng)建線程池的代碼很簡(jiǎn)單就一個(gè)with語句和調(diào)用map()方法
運(yùn)行結(jié)果如下圖所示:
好了轧苫,數(shù)據(jù)已經(jīng)獲取到了楚堤,接下來將進(jìn)行數(shù)據(jù)分析。
數(shù)據(jù)可視化
首先我們來分析一下各個(gè)景點(diǎn)那個(gè)月份游玩的人數(shù)最多含懊,這樣我們就不用擔(dān)心去游玩的時(shí)機(jī)不對(duì)了身冬。
我們發(fā)現(xiàn)10月、2月岔乔、1月去廣州長隆飛鳥樂園游玩的人數(shù)占總體比例最多酥筝。分析完月份后,我們來看看評(píng)論情況如何:
可以發(fā)現(xiàn)去好評(píng)占了絕大部分雏门,可以說:去長隆飛鳥樂園玩耍嘿歌,去了都說好掸掏。看了評(píng)論情況宙帝,評(píng)論內(nèi)容有什么:
好了阅束,獲取旅游景點(diǎn)信息及評(píng)論并作詞云、數(shù)據(jù)可視化就講到這里了茄唐。