寫在最前
程序是為人類服務(wù)的,最近正好身邊小伙伴們?cè)谧銮蛞律?,?dāng)然是去nikenba專區(qū)購(gòu)買了,可是有些熱門球衣發(fā)布幾分鐘就被搶完,有些折扣球衣也是很快就被搶售一空,那么我們只能靠自己的眼睛一直盯著網(wǎng)站嗎?NoNoNo,作為計(jì)算機(jī)專業(yè)的學(xué)生,怎么能為這種事情浪費(fèi)時(shí)間呢?那肯定想法就是寫爬蟲自動(dòng)比對(duì)價(jià)格啊,后來(lái)又在想,爬蟲數(shù)據(jù)也是在PC端啊,該怎么實(shí)時(shí)提醒我們呢?再弄一個(gè)微信機(jī)器人發(fā)送數(shù)據(jù)不就可以了嗎?說(shuō)干就干,代碼開擼
先看下效果:
準(zhǔn)備工作:
首先本文使用py3,需要安裝以下庫(kù):
1)itchat
2)requests
3)apscheduler
分析網(wǎng)頁(yè):
首先我們需要做什么?毫無(wú)疑問(wèn),分析網(wǎng)頁(yè),因?yàn)樽钪匾囊徊骄褪谦@取數(shù)據(jù),那么如何獲取數(shù)據(jù)就是我們首先要克服的困難
附上 nike nba專區(qū)地址:
https://www.nike.com/cn/w/nba-sleeveless-and-tank-tops-18iwiz9sbux
<https://www.nike.com/cn/w/nba-sleeveless-and-tank-tops-18iwiz9sbux>
首先我們要明確一個(gè)地方,我們的目的是實(shí)時(shí)監(jiān)控?zé)衢T打折球衣,所以我們的價(jià)格肯定首先降序排列,不過(guò)先不用著急,打開F12先看下調(diào)試器,對(duì)了我使用的是chrome瀏覽器
由于我們是先打開網(wǎng)頁(yè)再打開調(diào)試窗口,所以目前我們看不到數(shù)據(jù),別急,我們刷新一下再看
哦吼,完蛋,怎么這么多東西貌似根本沒(méi)法看
別急 繼續(xù)分析,作為一個(gè)學(xué)(qiong)生(bi),我們肯定先關(guān)注價(jià)格了,當(dāng)然要升序排列?。?br>
好的 點(diǎn)下瀏覽器調(diào)試窗口中的清除按鈕(就是下面這個(gè)藍(lán)色標(biāo)記的按鈕)先清除下調(diào)試臺(tái)中的數(shù)據(jù) 然后呢我們點(diǎn)下篩選方式價(jià)格由低到高(紅色標(biāo)記的菜單鍵中選擇)
得到調(diào)試臺(tái)如下,完蛋了還是一堆怎么辦?
沒(méi)關(guān)系,至少現(xiàn)在網(wǎng)頁(yè)內(nèi)容已經(jīng)是按照價(jià)格升序排列了,我們?cè)賮?lái)看看得到的Network數(shù)據(jù),挨個(gè)點(diǎn)一點(diǎn)看看,發(fā)現(xiàn)當(dāng)點(diǎn)到名稱為graphql開頭的文件里去時(shí)候,有東西出現(xiàn)了
里面的響應(yīng)內(nèi)容出現(xiàn)了幾個(gè)熟悉的隊(duì)名稱和球員名稱甚至還有價(jià)格,等等,這不就是我們要的數(shù)據(jù)嗎?
看來(lái)我們找對(duì)了地方,我們雙擊點(diǎn)開graphql開頭的網(wǎng)頁(yè)文件看看會(huì)有什么呢? 。。。 看起來(lái)雜亂無(wú)章,但是貌似確實(shí)是我們要的數(shù)據(jù),是json格式的
在網(wǎng)頁(yè)上看json簡(jiǎn)直是折磨,好的,我們用python開始把這個(gè)網(wǎng)頁(yè)內(nèi)容給弄下來(lái)仔細(xì)研究下
pycharm開搞
import requests import json #剛剛在調(diào)試臺(tái)得到的地址 url='
https://www.nike.com/w/graphql?queryid=filteredProductsWithContext&anonymousId=A54CD5202A87B54B4415AD4BC11E5692&uuids=1c7c3d67-5d46-432d-9910-b1128d1b6503,e09eabe9-5ff0-42af-b0a3-5f68af19d89a&language=zh-Hans&country=CN&sortBy=priceAsc
' #使json數(shù)據(jù)格式化輸出更好觀察 def better_jsprint(json_obj): # 使用indent=4
這個(gè)參數(shù)對(duì)json進(jìn)行數(shù)據(jù)格式化輸出 #因?yàn)閖son.dumps
序列化時(shí)對(duì)中文默認(rèn)使用的ascii編碼.想輸出真正的中文需要指定ensure_ascii=False return
json.dumps(json.loads(json_obj),indent=4,ensure_ascii=False) response=
requests.get(url)print(better_jsprint(response.text))
看看輸出什么:
這樣看起來(lái)好多了,好的
似乎到這里我們已經(jīng)可以開始選取我們需要的數(shù)據(jù)進(jìn)行記錄了,但是我們又會(huì)注意到一點(diǎn),這個(gè)網(wǎng)頁(yè)的內(nèi)容是瀑布流方式,也就是說(shuō)滾輪往下滾動(dòng)才會(huì)有更多的數(shù)據(jù)出現(xiàn),可是我們目前只獲取了這個(gè)頁(yè)面最上端的數(shù)據(jù),如果我們想獲取更多的數(shù)據(jù)怎么辦?
我們還是使用調(diào)試臺(tái),其實(shí)他頁(yè)面只要變化,網(wǎng)站交互一定是有活動(dòng)的,所以我們現(xiàn)在就觀察當(dāng)滾輪往下滾動(dòng)到瀑布流下端時(shí)調(diào)試臺(tái)會(huì)出現(xiàn)什么東西就可以了
往下滾動(dòng),發(fā)現(xiàn)調(diào)試臺(tái)確實(shí)出現(xiàn)了很多新的文件,我們猜想這些文件中一定有瀑布流下端的數(shù)據(jù),對(duì)了還記得我們剛才找到的文件名是什么嗎?對(duì)的,是名稱為graphql開頭的文件,那么會(huì)不會(huì)新的數(shù)據(jù)文件也是這個(gè)名字開頭的呢?我們使用調(diào)試臺(tái)搜索下看看
來(lái)了來(lái)了,它真的出現(xiàn)了,現(xiàn)在出現(xiàn)了3個(gè)文件都是graphql名字開頭,毫無(wú)疑問(wèn)第一個(gè)文件是我們上面找到的,那么第二個(gè)第三個(gè)呢?
我們點(diǎn)開看看,會(huì)發(fā)現(xiàn)對(duì)應(yīng)的商品名稱之類的真的是瀑布流下端的數(shù)據(jù)。
OK看起來(lái)我們現(xiàn)在確實(shí)得到了所有數(shù)據(jù)文件的url
我最初的想法是直接將3個(gè)url寫到一個(gè)列表中然后使用循環(huán)讀取如下圖(其實(shí)會(huì)發(fā)現(xiàn)第二個(gè)url與第三個(gè)看起來(lái)貌似一樣啊怎么回事?下面有解釋別急)
后來(lái)呢我突然意識(shí)到,萬(wàn)一商品更多了怎么辦?會(huì)不會(huì)出現(xiàn)4個(gè)5個(gè)url?而總不能每次都靠人力去數(shù)有多少個(gè)url吧?然后就想,怎樣才能讓程序自動(dòng)添加url呢?
我們?cè)倩仡^看看第一次抓取下來(lái)的 url1
的json數(shù)據(jù),首先嘗試下檢索page這個(gè)關(guān)鍵詞(畢竟一般程序員都會(huì)寫這個(gè)作為頁(yè)面標(biāo)識(shí)吧?),哦霍,發(fā)現(xiàn)了了不得的東西,
這些數(shù)據(jù)看起來(lái)很眼熟啊,還有uuids?再比對(duì)下第一次抓的 url1 發(fā)現(xiàn)里面的uuids還真的就是json里面的數(shù)據(jù),那么又看到pages里面有個(gè)next
納尼?這會(huì)不會(huì)是瀑布流下半部分url組成呢?快來(lái)比對(duì) url2 地址
https://www.nike.com/w/graphql?queryid=products&anonymousId=A54CD5202A87B54B4415AD4BC11E5692&
endpoint=
%2Fproduct_feed%2Frollup_threads%2Fv2%3Ffilter%3Dmarketplace(CN)%26filter%3Dlanguage(zh-Hans)%26filter%3DemployeePrice(true)%26filter%3DattributeIds(1c7c3d67-5d46-432d-9910-b1128d1b6503%2Ce09eabe9-5ff0-42af-b0a3-5f68af19d89a)%26anchor%3D24%26count%3D24%26consumerChannelId%3Dd9a5bc42-4b9c-4976-858a-f159cf99c647%26sort%3DproductInfo.merchPrice.currentPriceAsc
嘗試檢索下next中的內(nèi)容,發(fā)現(xiàn)真的存在與endpoint參數(shù)后面,哦霍 現(xiàn)在我們猜想,會(huì)不會(huì)每個(gè)json中都包含pages next這個(gè)數(shù)據(jù)
打印url2繼續(xù)檢索pages的next?
真的存在,并且還存在prev參數(shù)(前一頁(yè)),說(shuō)明我們的猜想可能是正確的,這時(shí)候細(xì)心的小伙伴可能發(fā)現(xiàn)了
url2中的next內(nèi)容與url1中一致啊,哦原來(lái)是這樣,這樣才導(dǎo)致了我們剛剛調(diào)試臺(tái)中出現(xiàn)3個(gè)url文件但是第二個(gè)與第三個(gè)一樣的情況
但是我們猜想第三個(gè)url返回?cái)?shù)據(jù)中應(yīng)該沒(méi)有next否則就應(yīng)該出現(xiàn)第四個(gè)文件了,我們來(lái)試一試
在url3返回?cái)?shù)據(jù)中檢索next
真的為空了所以我們可以確定,只要瀑布流下方仍有數(shù)據(jù),那么一定存在next參數(shù) 因此我們可以確定瀑布流url寫法 我們網(wǎng)頁(yè)分析完成
接下來(lái)就要進(jìn)行真正的代碼編寫了
(其實(shí)我有一個(gè)疑問(wèn)
url2與url3看起來(lái)確實(shí)是一模一樣的并且我嘗試做了差值運(yùn)算,發(fā)現(xiàn)還是一樣的,但是返回?cái)?shù)據(jù)確實(shí)不同,有大神可以發(fā)現(xiàn)這兩個(gè)url不同之處嗎
下面放上這兩個(gè)url)
(url已改變,根據(jù)官網(wǎng)實(shí)時(shí)更新數(shù)據(jù)一直在變)
url2='
https://www.nike.com/w/graphql?queryid=products&anonymousId=A54CD5202A87B54B4415AD4BC11E5692&endpoint=%2Fproduct_feed%2Frollup_threads%2Fv2%3Ffilter%3Dmarketplace(CN)%26filter%3Dlanguage(zh-Hans)%26filter%3DemployeePrice(true)%26filter%3DattributeIds(1c7c3d67-5d46-432d-9910-b1128d1b6503%2Ce09eabe9-5ff0-42af-b0a3-5f68af19d89a)%26anchor%3D24%26count%3D24%26consumerChannelId%3Dd9a5bc42-4b9c-4976-858a-f159cf99c647%26sort%3DproductInfo.merchPrice.currentPriceAsc
' url3='
https://www.nike.com/w/graphql?queryid=products&anonymousId=A54CD5202A87B54B4415AD4BC11E5692&endpoint=%2Fproduct_feed%2Frollup_threads%2Fv2%3Ffilter%3Dmarketplace(CN)%26filter%3Dlanguage(zh-Hans)%26filter%3DemployeePrice(true)%26filter%3DattributeIds(1c7c3d67-5d46-432d-9910-b1128d1b6503%2Ce09eabe9-5ff0-42af-b0a3-5f68af19d89a)%26anchor%3D48%26count%3D24%26consumerChannelId%3Dd9a5bc42-4b9c-4976-858a-f159cf99c647%26sort%3DproductInfo.merchPrice.currentPriceAsc
'
urls構(gòu)建與objects獲取
我們首先需要寫遞歸函數(shù)獲取所有urls
我們觀察json內(nèi)容就會(huì)發(fā)現(xiàn)我們需要的商品數(shù)據(jù)都在一個(gè)名為objects的key中 因此需要將所有objects放在一起
遞歸函數(shù)(核心函數(shù))如下
#剛剛在調(diào)試臺(tái)得到的初始地址 url1='
https://www.nike.com/w/graphql?queryid=filteredProductsWithContext&anonymousId=A54CD5202A87B54B4415AD4BC11E5692&uuids=1c7c3d67-5d46-432d-9910-b1128d1b6503,e09eabe9-5ff0-42af-b0a3-5f68af19d89a&language=zh-Hans&country=CN&sortBy=priceAsc
' #觀察其他urls發(fā)現(xiàn)前面參數(shù)是一樣的如下 我們先寫前半部分 urlother='
https://www.nike.com/w/graphql?queryid=products&anonymousId=A54CD5202A87B54B4415AD4BC11E5692&endpoint=
' urls=[url1] #空l(shuí)ist存放物品信息 觀察發(fā)現(xiàn)json中的objects數(shù)據(jù)類型為list pricedictlist=[] #
遞歸函數(shù)得到urls列表以及每個(gè)url中物品數(shù)據(jù) def get_url_objcts(url=url1): #首先得到初始url的json數(shù)據(jù)
response=requests.get(url) #只取有用的數(shù)據(jù)內(nèi)容 仔細(xì)觀察json數(shù)據(jù) 得到下一個(gè)頁(yè)面的next參數(shù) #
urllib.parse.quote(text) # 按照標(biāo)準(zhǔn), URL 只允許一部分 ASCII 字符(數(shù)字字母和部分符號(hào)),其他的字符(如漢字)是不符合
URL 標(biāo)準(zhǔn)的。 # 所以 URL 中使用其他字符就需要進(jìn)行 URL 編碼。 try: nextpage_json=quote(response.json()[
'data']['filteredProductsWithContext']['pages']['next']) #添加objects內(nèi)容到列表
pricedictlist.extend(response.json()['data']['filteredProductsWithContext']['
objects']) except KeyError: nextpage_json = quote(response.json()['data']['
products']['pages']['next']) # 添加objects內(nèi)容到列表
pricedictlist.extend(response.json()['data']['products']['objects']) except
TypeError: nextpage_json='' #遞歸獲取url與objects if nextpage_json!='': urlnext
=urlother+nextpage_json urls.append(urlnext) nextpage_json=''
get_url_objcts(urlnext)#else只在不存在下一頁(yè)時(shí)執(zhí)行,相當(dāng)于此時(shí)已經(jīng)完成了objects的獲取 下面構(gòu)建發(fā)送信息 else: i =
0 STR= str('
https://www.nike.com/cn/w/nba-sleeveless-and-tank-tops-18iwiz9sbux?sort=priceAsc
') compStr1 = '' for each in pricedictlist: title = each['publishedContent']['
properties']['seo'] if title == None: continue currentPrice = each['productInfo'
][0]['merchPrice']['currentPrice'] fullPrice = each['productInfo'][0]['
merchPrice']['fullPrice'] #只選取有用的數(shù)據(jù) 我們不要童裝 同時(shí)只要打折商品 if (not re.search('童',
str(title['slug']))) and (fullPrice != currentPrice): i = i + 1 STR = STR + '
\n\n' + ((str(title['slug']) + "\n" + " 原價(jià)" + str(fullPrice) + " 現(xiàn)價(jià)" + str(
currentPrice))+ ' ' + str(currentPrice * 100 / fullPrice) + '%') #
發(fā)現(xiàn)每個(gè)商品名稱后面都有獨(dú)特的商品碼為6個(gè)字母標(biāo)識(shí),所以切片記錄下來(lái)用于對(duì)比 compStr1 = compStr1 + str(title['slug'
][-6:]) STR = STR + '\n' + ("本次數(shù)據(jù)一共:" + str(i) + "個(gè)")
這之上 我們已經(jīng)完成了數(shù)據(jù)的獲取,接下來(lái)就是微信機(jī)器人發(fā)送了
itchat微信機(jī)器人
itchat是個(gè)人賬戶的開放源碼wechat api項(xiàng)目,它使您可以通過(guò)命令行訪問(wèn)您的個(gè)人微信帳戶。
如何向群發(fā)送消息?
import itchat #登錄微信網(wǎng)頁(yè)版 參數(shù)enableCmdQR=0會(huì)出現(xiàn)圖片二維碼登錄 為1則命令行窗口輸出字符二維碼
有的linux因?yàn)樽址g距問(wèn)題需要設(shè)置為2 itchat.auto_login(hotReload=0,enableCmdQR=0) #
自己創(chuàng)建微信群,名稱自定,并且要保存到通信錄 chatroomName = 'Money' # 群名 itchat.get_chatrooms(update=
True) chatrooms= itchat.search_chatrooms(name=chatroomName) # print(compStr0) if
len(chatrooms) == 0: # print('沒(méi)有找到群聊:' + chatroomName) exit(0) else:
itchat.send_msg('hello world', toUserName=chatrooms[0]['UserName']) # 發(fā)送消息
這就是簡(jiǎn)單的發(fā)送消息了,將我們上面的程序接合就可以實(shí)現(xiàn)微信發(fā)送了 還差一步,沒(méi)錯(cuò)就是定時(shí)任務(wù)的問(wèn)題
Python定時(shí)任務(wù)框架apscheduler
聽名字就知道是干什么的 沒(méi)錯(cuò)就是任務(wù)調(diào)度,我們可以使用這個(gè)庫(kù)簡(jiǎn)潔的實(shí)現(xiàn)任務(wù)調(diào)度問(wèn)題
簡(jiǎn)單例程如下:
from apscheduler.schedulers.blocking import BlockingScheduler import time
scheduler= BlockingScheduler() def job1(): print ("%s: 執(zhí)行任務(wù)" % time.asctime())
scheduler.add_job(job1,'interval', seconds=3) scheduler.start()
輸出:
Mon Aug 19 18:35:52 2019: 執(zhí)行任務(wù) Mon Aug 19 18:35:55 2019: 執(zhí)行任務(wù) Mon Aug 19
18:35:58 2019: 執(zhí)行任務(wù) Mon Aug 19 18:36:01 2019: 執(zhí)行任務(wù) Mon Aug 19 18:36:04 2019:
執(zhí)行任務(wù) Mon Aug19 18:36:07 2019: 執(zhí)行任務(wù) Mon Aug 19 18:36:10 2019: 執(zhí)行任務(wù)
最后一步就是將上面講的所有來(lái)一個(gè)大集合
程序送上~:
將環(huán)境配置好,直接放在自己的服務(wù)器就可以運(yùn)行了,這一步就不再贅述
import re import requests from urllib.parse import quote import itchat from
datetimeimport datetime from apscheduler.schedulers.blocking import
BlockingScheduler#登錄微信網(wǎng)頁(yè)版 參數(shù)enableCmdQR=0會(huì)出現(xiàn)圖片二維碼登錄 為1則命令行窗口輸出字符二維碼
有的linux因?yàn)樽址g距問(wèn)題需要設(shè)置為2 itchat.auto_login(hotReload=0,enableCmdQR=2) #
比較字符串,用于判斷是否更新數(shù)據(jù) compStr0='fuckkkkkkkkkkkkkkkk' def main(): #剛剛在調(diào)試臺(tái)得到的初始地址 url1=
'
https://www.nike.com/w/graphql?queryid=filteredProductsWithContext&anonymousId=A54CD5202A87B54B4415AD4BC11E5692&uuids=1c7c3d67-5d46-432d-9910-b1128d1b6503,e09eabe9-5ff0-42af-b0a3-5f68af19d89a&language=zh-Hans&country=CN&sortBy=priceAsc
' #觀察其他urls發(fā)現(xiàn)前面參數(shù)是一樣的如下 我們先寫前半部分 urlother='
https://www.nike.com/w/graphql?queryid=products&anonymousId=A54CD5202A87B54B4415AD4BC11E5692&endpoint=
' urls=[url1] #空l(shuí)ist存放物品信息 觀察發(fā)現(xiàn)json中的objects數(shù)據(jù)類型為list pricedictlist=[] #
遞歸函數(shù)得到urls列表以及每個(gè)url中物品數(shù)據(jù) def get_url_objcts(url=url1): #首先得到初始url的json數(shù)據(jù)
response=requests.get(url) #只取有用的數(shù)據(jù)內(nèi)容 仔細(xì)觀察json數(shù)據(jù) 得到下一個(gè)頁(yè)面的next參數(shù) #
urllib.parse.quote(text) # 按照標(biāo)準(zhǔn), URL 只允許一部分 ASCII 字符(數(shù)字字母和部分符號(hào)),其他的字符(如漢字)是不符合
URL 標(biāo)準(zhǔn)的。 # 所以 URL 中使用其他字符就需要進(jìn)行 URL 編碼。 try: nextpage_json=quote(response.json()[
'data']['filteredProductsWithContext']['pages']['next'])
pricedictlist.extend(response.json()['data']['filteredProductsWithContext']['
objects']) except KeyError: nextpage_json = quote(response.json()['data']['
products']['pages']['next']) pricedictlist.extend(response.json()['data']['
products']['objects']) except TypeError: nextpage_json='' #遞歸獲取url與objects if
nextpage_json!='': urlnext=urlother+nextpage_json urls.append(urlnext)
nextpage_json='' get_url_objcts(urlnext) #else只在不存在下一頁(yè)時(shí)執(zhí)行,相當(dāng)于此時(shí)已經(jīng)完成了objects的獲取
else: i = 0 STR = str('
https://www.nike.com/cn/w/nba-sleeveless-and-tank-tops-18iwiz9sbux?sort=priceAsc
') compStr1 = '' for each in pricedictlist: title = each['publishedContent']['
properties']['seo'] if title == None: continue currentPrice = each['productInfo'
][0]['merchPrice']['currentPrice'] fullPrice = each['productInfo'][0]['
merchPrice']['fullPrice'] #只選取有用的數(shù)據(jù) 我們不要童裝 同時(shí)只要打折商品 if (not re.search('童',
str(title['slug']))) and (fullPrice != currentPrice): i = i + 1 STR = STR + '
\n\n' + ((str(title['slug']) + "\n" + " 原價(jià)" + str(fullPrice) + " 現(xiàn)價(jià)" + str(
currentPrice))+ ' ' + str(currentPrice * 100 / fullPrice) + '%') #
發(fā)現(xiàn)每個(gè)商品名稱后面都有獨(dú)特的商品碼為6個(gè)字母標(biāo)識(shí),所以切片記錄下來(lái)用于對(duì)比 compStr1 = compStr1 + str(title['slug'
][-6:]) STR = STR + '\n' + ("本次數(shù)據(jù)一共:" + str(i) + "個(gè)") #自己創(chuàng)建微信群,名稱自定
chatroomName ='Money' # 群名 itchat.get_chatrooms(update=True) chatrooms =
itchat.search_chatrooms(name=chatroomName) global compStr0 # print(compStr0) if
len(chatrooms) == 0: # print('沒(méi)有找到群聊:' + chatroomName) exit(0) else: #判斷數(shù)據(jù)是否變化
if (compStr1 != compStr0): itchat.send_msg(STR, toUserName=chatrooms[0]['
UserName']) # 發(fā)送消息 compStr0 = compStr1 # print(compStr0) get_url_objcts() sched
= BlockingScheduler() # 任務(wù)調(diào)度 每2分鐘觸發(fā) 時(shí)間自定 sched.add_job(main, 'interval',
minutes=2, next_run_time=datetime.now()) sched.start() itchat.run()
轉(zhuǎn)載請(qǐng)注明出處 thank you!
?
熱門工具 換一換
