1. 程式人生 > >python+爬蟲+微信機器人 打造屬於你的網購價格監督利器

python+爬蟲+微信機器人 打造屬於你的網購價格監督利器

寫在最前

  程式是為人類服務的,最近正好身邊小夥伴們在做球衣生意,當然是去nikenba專區購買了,可是有些熱門球衣釋出幾分鐘就被搶完,有些折扣球衣也是很快就被搶售一空,那麼我們只能靠自己的眼睛一直盯著網站嗎?NoNoNo,作為計算機專業的學生,怎麼能為這種事情浪費時間呢?那肯定想法就是寫爬蟲自動比對價格啊,後來又在想,爬蟲資料也是在PC端啊,該怎麼實時提醒我們呢?再弄一個微信機器人傳送資料不就可以了嗎?說幹就幹,程式碼開擼

先看下效果:

準備工作:

首先本文使用py3,需要安裝以下庫:

1)itchat

2)requests

3)apscheduler

分析網頁:

首先我們需要做什麼?毫無疑問,分析網頁,因為最重要的一步就是獲取資料,那麼如何獲取資料就是我們首先要克服的困難

附上 nike nba專區地址:https://www.nike.com/cn/w/nba-sleeveless-and-tank-tops-18iwiz9sbux

首先我們要明確一個地方,我們的目的是實時監控熱門打折球衣,所以我們的價格肯定首先降序排列,不過先不用著急,開啟F12先看下偵錯程式,對了我使用的是chrome瀏覽器

由於我們是先開啟網頁再開啟除錯視窗,所以目前我們看不到資料,別急,我們重新整理一下再看

哦吼,完蛋,怎麼這麼多東西貌似根本沒法看

別急 繼續分析,作為一個學(qiong)生(bi),我們肯定先關注價格了,當然要升序排列啊!

好的 點下瀏覽器除錯視窗中的清除按鈕(就是下面這個藍色標記的按鈕)先清除下除錯臺中的資料 然後呢我們點下篩選方式價格由低到高(紅色標記的選單鍵中選擇)

得到除錯臺如下,完蛋了還是一堆怎麼辦?

沒關係,至少現在網頁內容已經是按照價格升序排列了,我們再來看看得到的Network資料,挨個點一點看看,發現當點到名稱為graphql開頭的檔案裡去時候,有東西出現了

裡面的響應內容出現了幾個熟悉的隊名稱和球員名稱甚至還有價格,等等,這不就是我們要的資料嗎?

看來我們找對了地方,我們雙擊點開graphql開頭的網頁檔案看看會有什麼呢? 。。。 看起來雜亂無章,但是貌似確實是我們要的資料,是json格式的

在網頁上看json簡直是折磨,好的,我們用pycharm開始把這個網頁內容給弄下來仔細研究下

pycharm開搞

import requests
import json

#剛剛在除錯臺得到的地址
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資料格式化輸出更好觀察
def better_jsprint(json_obj):
    # 使用indent=4 這個引數對json進行資料格式化輸出
    #因為json.dumps 序列化時對中文預設使用的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))

看看輸出什麼:

這樣看起來好多了,好的 似乎到這裡我們已經可以開始選取我們需要的資料進行記錄了,但是我們又會注意到一點,這個網頁的內容是瀑布流方式,也就是說滾輪往下滾動才會有更多的資料出現,可是我們目前只獲取了這個頁面最上端的資料,如果我們想獲取更多的資料怎麼辦?

我們還是使用除錯臺,其實他頁面只要變化,網站互動一定是有活動的,所以我們現在就觀察當滾輪往下滾動到瀑布流下端時除錯臺會出現什麼東西就可以了

往下滾動,發現除錯臺確實出現了很多新的檔案,我們猜想這些檔案中一定有瀑布流下端的資料,對了還記得我們剛才找到的檔名是什麼嗎?對的,是名稱為graphql開頭的檔案,那麼會不會新的資料檔案也是這個名字開頭的呢?我們使用除錯臺搜尋下看看

來了來了,它真的出現了,現在出現了3個檔案都是graphql名字開頭,毫無疑問第一個檔案是我們上面找到的,那麼第二個第三個呢? 我們點開看看,會發現對應的商品名稱之類的真的是瀑布流下端的資料。

OK看起來我們現在確實得到了所有資料檔案的url

我最初的想法是直接將3個url寫到一個列表中然後使用迴圈讀取如下圖(其實會發現第二個url與第三個看起來貌似一樣啊怎麼回事?下面有解釋別急)

後來呢我突然意識到,萬一商品更多了怎麼辦?會不會出現4個5個url?而總不能每次都靠人力去數有多少個url吧?然後就想,怎樣才能讓程式自動新增url呢?

我們再回頭看看第一次抓取下來的 url1 的json資料,首先嚐試下檢索page這個關鍵詞(畢竟一般程式設計師都會寫這個作為頁面標識吧?),哦霍,發現了了不得的東西,

這些資料看起來很眼熟啊,還有uuids?再比對下第一次抓的 url1 發現裡面的uuids還真的就是json裡面的資料,那麼又看到pages裡面有個next 納尼?這會不會是瀑布流下半部分url組成呢?快來比對 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中的內容,發現真的存在與endpoint引數後面,哦霍 現在我們猜想,會不會每個json中都包含pages next這個資料

列印url2繼續檢索pages的next 

真的存在,並且還存在prev引數(前一頁),說明我們的猜想可能是正確的,這時候細心的小夥伴可能發現了 url2中的next內容與url1中一致啊,哦原來是這樣,這樣才導致了我們剛剛除錯臺中出現3個url檔案但是第二個與第三個一樣的情況

但是我們猜想第三個url返回資料中應該沒有next否則就應該出現第四個檔案了,我們來試一試

在url3返回資料中檢索next

真的為空了所以我們可以確定,只要瀑布流下方仍有資料,那麼一定存在next引數 因此我們可以確定瀑布流url寫法 我們網頁分析完成 接下來就要進行真正的程式碼編寫了

(其實我有一個疑問 url2與url3看起來確實是一模一樣的並且我嘗試做了差值運算,發現還是一樣的,但是返回資料確實不同,有大神可以發現這兩個url不同之處嗎 下面放上這兩個url)

(url已改變,根據官網實時更新資料一直在變)

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構建與objects獲取

我們首先需要寫遞迴函式獲取所有urls

我們觀察json內容就會發現我們需要的商品資料都在一個名為objects的key中 因此需要將所有objects放在一起

遞迴函式(核心函式)如下

    #剛剛在除錯臺得到的初始地址
    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發現前面引數是一樣的如下 我們先寫前半部分
    urlother='https://www.nike.com/w/graphql?queryid=products&anonymousId=A54CD5202A87B54B4415AD4BC11E5692&endpoint='
    urls=[url1]

    #空list存放物品資訊 觀察發現json中的objects資料型別為list
    pricedictlist=[]

    #遞迴函式得到urls列表以及每個url中物品資料
    def get_url_objcts(url=url1):
        #首先得到初始url的json資料
        response=requests.get(url)
        #只取有用的資料內容 仔細觀察json資料 得到下一個頁面的next引數
        #urllib.parse.quote(text)
        # 按照標準, URL 只允許一部分 ASCII 字元(數字字母和部分符號),其他的字元(如漢字)是不符合 URL 標準的。
        # 所以 URL 中使用其他字元就需要進行 URL 編碼。
        try:
            nextpage_json=quote(response.json()['data']['filteredProductsWithContext']['pages']['next'])
            #新增objects內容到列表
            pricedictlist.extend(response.json()['data']['filteredProductsWithContext']['objects'])
        except KeyError:
            nextpage_json = quote(response.json()['data']['products']['pages']['next'])
            # 新增objects內容到列表
            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只在不存在下一頁時執行,相當於此時已經完成了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']
                #只選取有用的資料 我們不要童裝 同時只要打折商品
                if (not re.search('童', str(title['slug']))) and (fullPrice != currentPrice):
                    i = i + 1
                    STR = STR + '\n\n' + ((str(title['slug']) + "\n" + "  原價" + str(fullPrice) + "  現價" + str(
                        currentPrice)) + '  ' + str(currentPrice * 100 / fullPrice) + '%')
                    #發現每個商品名稱後面都有獨特的商品碼為6個字母標識,所以切片記錄下來用於對比
                    compStr1 = compStr1 + str(title['slug'][-6:])

            STR = STR + '\n' + ("本次資料一共:" + str(i) + "個")

這之上 我們已經完成了資料的獲取,接下來就是微信機器人傳送了

itchat微信機器人

itchat是個人賬戶的開放原始碼wechat api專案,它使您可以通過命令列訪問您的個人微信帳戶。

如何向群傳送訊息?

import itchat
#登入微信網頁版 引數enableCmdQR=0會出現圖片二維碼登入 為1則命令列視窗輸出字元二維碼 有的linux因為字元間距問題需要設定為2
itchat.auto_login(hotReload=0,enableCmdQR=0)

# 自己建立微信群,名稱自定,並且要儲存到通訊錄
chatroomName = 'Money'  # 群名
itchat.get_chatrooms(update=True)
chatrooms = itchat.search_chatrooms(name=chatroomName)
# print(compStr0)
if len(chatrooms) == 0:
    # print('沒有找到群聊:' + chatroomName)
    exit(0)
else:
    itchat.send_msg('hello world', toUserName=chatrooms[0]['UserName'])  # 傳送訊息

這就是簡單的傳送訊息了,將我們上面的程式接合就可以實現微信傳送了 還差一步,沒錯就是定時任務的問題

Python定時任務框架apscheduler

聽名字就知道是幹什麼的 沒錯就是任務排程,我們可以使用這個庫簡潔的實現任務排程問題

簡單例程如下:

from apscheduler.schedulers.blocking import BlockingScheduler
import time
scheduler = BlockingScheduler()
def job1():
 print ("%s: 執行任務" % time.asctime())
scheduler.add_job(job1, 'interval', seconds=3)
scheduler.start()

輸出:

Mon Aug 19 18:35:52 2019: 執行任務
Mon Aug 19 18:35:55 2019: 執行任務
Mon Aug 19 18:35:58 2019: 執行任務
Mon Aug 19 18:36:01 2019: 執行任務
Mon Aug 19 18:36:04 2019: 執行任務
Mon Aug 19 18:36:07 2019: 執行任務
Mon Aug 19 18:36:10 2019: 執行任務

最後一步就是將上面講的所有來一個大集合

程式送上~:

將環境配置好,直接放在自己的伺服器就可以運行了,這一步就不再贅述

import re
import requests
from urllib.parse import quote
import itchat
from datetime import datetime
from apscheduler.schedulers.blocking import BlockingScheduler

#登入微信網頁版 引數enableCmdQR=0會出現圖片二維碼登入 為1則命令列視窗輸出字元二維碼 有的linux因為字元間距問題需要設定為2
itchat.auto_login(hotReload=0,enableCmdQR=2)

#比較字串,用於判斷是否更新資料
compStr0='fuckkkkkkkkkkkkkkkk'

def main():
    #剛剛在除錯臺得到的初始地址
    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發現前面引數是一樣的如下 我們先寫前半部分
    urlother='https://www.nike.com/w/graphql?queryid=products&anonymousId=A54CD5202A87B54B4415AD4BC11E5692&endpoint='
    urls=[url1]

    #空list存放物品資訊 觀察發現json中的objects資料型別為list
    pricedictlist=[]

    #遞迴函式得到urls列表以及每個url中物品資料
    def get_url_objcts(url=url1):
        #首先得到初始url的json資料
        response=requests.get(url)
        #只取有用的資料內容 仔細觀察json資料 得到下一個頁面的next引數
        #urllib.parse.quote(text)
        # 按照標準, URL 只允許一部分 ASCII 字元(數字字母和部分符號),其他的字元(如漢字)是不符合 URL 標準的。
        # 所以 URL 中使用其他字元就需要進行 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只在不存在下一頁時執行,相當於此時已經完成了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']
                #只選取有用的資料 我們不要童裝 同時只要打折商品
                if (not re.search('童', str(title['slug']))) and (fullPrice != currentPrice):
                    i = i + 1
                    STR = STR + '\n\n' + ((str(title['slug']) + "\n" + "  原價" + str(fullPrice) + "  現價" + str(
                        currentPrice)) + '  ' + str(currentPrice * 100 / fullPrice) + '%')
                    #發現每個商品名稱後面都有獨特的商品碼為6個字母標識,所以切片記錄下來用於對比
                    compStr1 = compStr1 + str(title['slug'][-6:])

            STR = STR + '\n' + ("本次資料一共:" + str(i) + "個")

            #自己建立微信群,名稱自定
            chatroomName = 'Money'  # 群名
            itchat.get_chatrooms(update=True)
            chatrooms = itchat.search_chatrooms(name=chatroomName)
            global compStr0
            # print(compStr0)
            if len(chatrooms) == 0:
                # print('沒有找到群聊:' + chatroomName)
                exit(0)
            else:
                #判斷資料是否變化
                if (compStr1 != compStr0):
                    itchat.send_msg(STR, toUserName=chatrooms[0]['UserName'])  # 傳送訊息
                    compStr0 = compStr1
            # print(compStr0)
    get_url_objcts()


sched = BlockingScheduler()
# 任務排程 每2分鐘觸發 時間自定
sched.add_job(main, 'interval', minutes=2, next_run_time=datetime.now())
sched.start()

itchat.run()

轉載請註明出處 thank you!

&n