通過爬蟲抓取鏈家二手房數據


 背景:

  公司需要分析通過二手房數據來分析下市場需求,主要通過爬蟲的方式抓取鏈家等二手房信息。

一、分析鏈家網站

  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.運行自己的爬蟲,查看下結果

  

以上過程主要記錄了,抓取鏈家二手房數據時候的過程。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM