背景:
公司需要分析通過二手房數據來分析下市場需求,主要通過爬蟲的方式抓取鏈家等二手房信息。
一、分析鏈家網站
1.因為最近天津落戶政策開放,天津房價跟着瘋了一般,所以我們主要來分析天津二手房數據,進入鏈家網站我們看到共找到29123套天津二手房;
2.查看下頁面的數據結構以及每頁顯示數據條數;
通過分析每頁顯示數據30條,總共顯示100頁面,即使我們通過翻頁方式100頁也只能拿到數據3000條與其提示的數據信息還差的很多。
通過翻頁的方式沒有辦法獲取需要的數據的話,那么我后來考慮自己拼接url的方式來獲取更多的數據,通過拼接方式發現只要超過100頁的時候,無論怎么拼接他都是現實最后一頁的數據,通過拼接的方式只能以失敗告終
通過拼接url方式失敗以后只能考慮通過按照區域的查詢條件來檢索數據,但是發現通過區域檢索的時候有些區域二手房數量也會超過3000條,這樣我們必須還的繼續按照區域下面的划分,這樣會雖然可以但是比較麻煩,所以這樣我們暫時不考慮;
通過上面的分析鏈家對這塊限制比較多,如果爬取來容易被封還的考慮使用代理IP來應對,這樣會增加成本同時還會限制速度,所以考慮通過APP或者小程序下手抓包分析下他的接口;
二、抓包分析鏈家APP;
1.一般情況抓包主要通過fiddler 來抓包,下面抓下鏈家APP的包,手機中配置代理以后會在fiddler中顯示手機發送的所有請求,這樣不便於我這邊分析,所以需要在fiddler設置下過濾規則;
2.訪問鏈家APP,抓取鏈家二手房的接口,很輕松的我們就抓到了鏈家二手房的請求接口,通過抓包我們看到了我們需要的數據,瞬間感覺比爬取頁面簡單的多;
3.把接口拷貝至Postman里面來驗證下接口中需要的驗證和必要的參數,經過驗證接口里面請求參數都是必須參數,而且headers里面有一個必須添加的參數Authorization,通過驗證發現Authorization,接口參數改變后Authorization值失效;
分析步驟:
1.直接復制接口在Postman請求,請求結果返回"無效的請求";
2.headers 添加參數UA再次請求,請求結果返回"無效的請求";
3.headers 添加參數Authorization再次請求,請求結果返回正常數據(抓包的過程中發現這個值很特殊所以優先嘗試);
4.判斷Authorization是否是動態值變更接口的參數再次請求,請求結果返回"無效的請求";
5.篩減請求參數再次請求,請求結果返回返回"無效的請求";
根據上述分析Authorization 可能根據請求參數進行加密生成的,這樣我們要了解邏輯只能反編譯APP,那只能再次改變策略,嘗試小程序是否可以實現;
4.抓包分析鏈家小程序
我們依然需要按照上述的分析步驟來抓包分析,接口請求的參數均是必須參數,而且headers里面的參數依然需要authorization,這個時候我們再也沒有辦法避開分析這個參數了;
# 二手房微信小程序接口 https://wechat.lianjia.com/ershoufang/search?city_id=120000&condition=&query=&order=&offset=0&limit=10&sign= #headers 參數 lianjia-source:ljwxapp authorization:bGp3eGFwcDoxYTNjMDA3MmQ0ZDA3NTM2ODVlOTJlMDQ0NmUwNDk5NQ== time-stamp:1527659945696
三、反編譯鏈家微信小程序
1.獲取微信小程序所對應的 wxapkg 包文件;
i.安裝android-sdk-windows,安裝完成執行adb --version命令返回版本號證明安裝成功;
C:\Users\hunk>adb --version Android Debug Bridge version 1.0.39 Version 0.0.1-4500957 Installed as D:\Program Files\android-sdk-windows\platform-tools\adb.exe
ii.安裝MuMu模擬器安裝登陸微信,添加鏈家小程序;
iii.通過adb命令鏈接模擬器,獲取wxapkg 包文件;
#鏈接mumu 模擬器 adb connect 127.0.0.1:7555 # 注意adb連接手機端口5555 # adb shell cd /data/data/com.tencent.mm/MicroMsg/8c12ff3e9b0391b589c0d5b16dc21952/appbrand/pkg #8c12ff3e9b0391b589c0d5b16dc21952為當前微信的用戶名字,可以根據自己的實際來查找 cancro:/data/data/com.tencent.mm/MicroMsg/8c12ff3e9b0391b589c0d5b16dc21952/appbrand/pkg # rm -rf * # 便於識別那個屬於鏈家,先刪除目錄內容再次打開鏈家小程序 cancro:/data/data/com.tencent.mm/MicroMsg/8c12ff3e9b0391b589c0d5b16dc21952/appbrand/pkg # ls _-1261323258_17.wxapkg # 名字不固定,根據實際來查看 _1123949441_130.wxapkg and/pkg # cp _-1261323258_17.wxapkg /sdcard/ cancro:/data/data/com.tencent.mm/MicroMsg/8c12ff3e9b0391b589c0d5b16dc21952/appbrand/pkg # exit # 把_-1261323258_17.wxapkg 下載本地
C:\Users\hunk>adb pull /sdcard/_-1261323258_17.wxapkg . /sdcard/_-1261323258_17.wxapkg: 1 file pulled. 1.0 MB/s (988967 bytes in 0.941s)
2.反編譯微信小程序,獲取重要信息;
通過github上獲取微信小程序.wxapkg解壓工具,我這邊使用python3來解壓;
把_-1261323258_17.wxapkg和unwxapkg.py 放在通過個目錄解壓,會在當前目錄生成_-1261323258_17.wxapkg_dir
python .\unwxapkg.py .\_-1261323258_17.wxapkg
下面是解壓后的目錄結構
目錄結構簡單的介紹
*.html | 包含wxss樣式信息 |
app-service.js | 所有js匯總文件 |
app-config.json | app.json 以及各個頁面的配置文件 |
page-frame.html | wxml文件及app.wxss 樣式 |
3.分析加密過程,生成authorization;
i.微信小程序的頁面邏輯主要都會存儲在app-service.js,下面我們需要來解析下這個文件,打開文件的時候里面的內容比較亂,我們需要通過格式化工具格式下js文件,使用Nodepad++ 來打開,安裝下JSTool工具格式化js代碼,這樣會讓我們看的舒服一些;
ii.通過關鍵字"authorization"搜索,對應的加密信息
搜索出來第一處帶有authorization關鍵字的代碼,這里是請求的定義的header 信息,我們這里可以看到Authorization的值是經過base64編碼;
搜索出來第二處帶有authorization關鍵字的代碼,這里我們可以看到一個關鍵信息l += "6e8566e348447383e16fdd1b233dbb49",這里猜想6e856這個串應該是appkey校驗信息;
根據搜索出來的兩端代碼我們來進行一次分析,首先獲取請求參數,然后對請求參數轉換成為字典,然后通過key進行排序,使用將key和value通過= 鏈接起來,在末尾添加appkey,進行md5加密,最后在開頭添加appid 進行base64 編碼;
/*定義變量 l,空字符串*/ var l = ""; /* 1.將請求參數解析成key:value 鍵值對; 2.通過key進行從小到大排序; 3.通過"="將 key和value連接起來,並追加到變量 l中; */ Object.keys(a.data).sort().forEach(function (e) { return l += e + "=" + ("object" == t(a.data[e]) ? JSON.stringify(a.data[e]) : a.data[e]) }), /*l 中再次追加字符串6e8566e348447383e16fdd1b233dbb49 */ l += "6e8566e348447383e16fdd1b233dbb49", /*對l 字符串進行md5加密*/ l = e.default.hexMD5(l), /*對加密的結果添加前綴 ljwxapp:*/ l = "ljwxapp:" + l, c.Authorization = n.encode(l),
4.驗證邏輯是否正確,抓取二手房列表;
import hashlib import base64 from urllib.parse import urlparse, parse_qs app_id = "ljwxapp:" app_key = "6e8566e348447383e16fdd1b233dbb49" def get_authorization(url): """ 根據url 動態獲取authorization :param url: :return: """ param = "" parse_param = parse_qs(urlparse(url).query, keep_blank_values=True) # 解析url參數 data = {key: value[-1] for key, value in parse_param.items()} # 生成字典 dict_keys = sorted(data.keys()) # 對key進行排序 for key in dict_keys: # 排序后拼接參數,key = value 模式 param += str(key) + "=" + data[key] param = param + app_key # 參數末尾添加app_key param_md5 = hashlib.md5(param.encode()).hexdigest() # 對參數進行md5 加密 authorization_source = app_id + param_md5 # 加密結果添加前綴app_id authorization = base64.b64encode(authorization_source.encode()) # 再次進行base64 編碼 return authorization.decode() if __name__ in "__main__":
# 天津鏈家二手房信息 url = "https://wechat.lianjia.com/ershoufang/search?city_id=120000&condition=&query=&order=&offset=0&limit=10&sign=" print(get_authorization(url)) # 生成加密串:bGp3eGFwcDoxYTNjMDA3MmQ0ZDA3NTM2ODVlOTJlMDQ0NmUwNDk5NQ==
我們把生成的結果放在Postman中驗證看看是否正確
四、通過Scrapy 來爬取所有的二手房數據,驗證上述邏輯是否正確
1.settings.py 配置文件配置接口請求的地址和APP_ID 和APP_KEY
2.定義我們要抓取的字段
# -*- coding: utf-8 -*- # @Time : 2018/5/30 10:52 # @Author : Hunk # @File : ErShouFangListItems.py # @Software: PyCharm from scrapy import Item from scrapy import Field class ErShouFangItems(Item): house_code = Field() # 二手房ID resblock_id = Field() # 小區ID resblock_name = Field() # 小區名字 price = Field() # 價格元/平
3.編寫下Spider邏輯
# -*- coding: utf-8 -*- # @Time : 2018/5/30 10:51 # @Author : Hunk # @File : ParseErShouFangList.py # @Software: PyCharm import time import json import math from scrapy import Spider from scrapy import Request from lib.Encrypt import get_authorization from scrapy.utils.project import get_project_settings from ..items.ErShouFangListItems import ErShouFangItems class ParseErShouFangSpider(Spider): """ 樓盤列表 """ name = 'TJErShouFang' def __init__(self, city_id, *args, **kwargs): super(ParseErShouFangSpider, self).__init__(*args, **kwargs) self.settings = get_project_settings() self.base_url = self.settings["WX_BASE_URL"] # 獲取配置文件中的微信小程序的請求地址 self.api = "/ershoufang/search" # 二手房接口地址 self.city_id = city_id self.limit_offset = 0 self.new_house_url = self.base_url + self.api + "?city_id=%s&condition=&query=&order=&offset=%s&limit=10&sign" # 拼接二手房接口地址 self.start_urls = self.new_house_url % (city_id, self.limit_offset) # 請求地址 self.headers = {"time-stamp": str(int(time.time() * 1000)), "lianjia-source": "ljwxapp", "authorization": ""} # 定義header def start_requests(self): """ 重寫start_requests :return: """ self.headers["authorization"] = get_authorization(self.start_urls) yield Request(url=self.start_urls, headers=self.headers, callback=self.parse) def parse(self, response): total_count = self.parse_page(response) # 獲取一共有多少頁面數據 for i in range(0, total_count): # 翻頁操作 url = self.new_house_url % (self.city_id, self.limit_offset) # 定義請求的url self.limit_offset += 10 # 每頁顯示10條數據,每次翻頁遞增10條 self.headers["authorization"] = get_authorization(url) yield Request(url=url, headers=self.headers, callback=self.parse_house_item) def parse_house_item(self, response): """ 解析JSON 數據 :param response: :return: """ item = ErShouFangItems() content = json.loads(response.body.decode()) ershoufang_list = content["data"]["list"] if len(ershoufang_list) > 0: for ershoufang in ershoufang_list: item["house_code"] = ershoufang_list[ershoufang]["house_code"] item["resblock_id"] = ershoufang_list[ershoufang]["resblock_id"] item["resblock_name"] = ershoufang_list[ershoufang]["resblock_name"] yield item def parse_page(self, response): """ 計算總共頁數 :param response: :return: """ content = json.loads(response.body.decode()) total_count = int(content["data"]["total_count"]) return int(math.ceil(total_count / 10))
4.運行自己的爬蟲,查看下結果
以上過程主要記錄了,抓取鏈家二手房數據時候的過程。