請啟用 JavaScript 來查看內容

[Python爬蟲實例] 591 房屋交易 -「新建案」搜尋與房屋詳情

前言

很久之前有寫一篇「591 租屋網 - 搜尋房屋與房屋詳情」,是針對 591 內的租屋網站進行資料抓取。

不過後來網站有改版,那時的程式寫法就失效了。
最近有再嘗試,雖然有找出發請求的網址,但發現回傳資料有透過 AES 之類的加密方式加密過的,要解開的話還要從它的 JS 程式中尋找 KEY 值,因為太麻煩,我就沒有繼續研究了。

如果有需求的讀者,可以參考這篇文章:AWS 雲端 591 租屋爬蟲架構,他有針對新版的去做修正,而且還加入資料儲存、mail 通知、定時換 IP 等等機制。


今天,改來試試爬取 591 房屋交易網站的「新建案」"搜尋建案" 與 "取得房屋詳情",說明如何發出請求、取得回傳數據資料。

來源:Unsplash
來源:Unsplash

備註:此文僅教育學習,切勿用作商業用途,個人實作皆屬個人行為,本作者不負任何法律責任。



套件

此次 Python 爬蟲主要使用到的套件:

安裝

1
2
pip install requests
pip install beautifulsoup4

搜尋建案

在 591 房屋交易網站的「新建案」主頁,我們先直接按搜尋,它會跳到另一個頁面

在這個搜尋頁面,上方可以選擇不同的搜尋條件,下方則是房屋搜尋結果列表。

591 新建案 搜尋頁面
591 新建案 搜尋頁面

請求路徑與參數

進到「591 新建案」的搜尋頁面後,打開瀏覽器的 開發人員工具 (F12 或 Ctrl + Shift + i) > Network 分頁,在 "Fetch/XHR" 分類中找找看哪個是搜尋建案結果的請求。

稍微尋找後,可以確定是 https://newhouse.591.com.tw/home/housing/list-search 這個請求。

591 新建案 搜尋 API 網址
591 新建案 搜尋 API 網址
591 新建案 API 回傳結果預覽
591 新建案 API 回傳結果預覽

另外,我們可以透過帶入不同的搜尋參數值,來達到對結果做不同的篩選。

篩選條件參數
篩選條件參數

當我們選擇不同條件後,觀察剛剛的請求,會發現它在網址後方帶入不同的參數 (parameters 或稱 query string),剛好對應我們選擇的條件。

例如:https://newhouse.591.com.tw/home/housing/list-search?page=1&device=pc&device_id=qdmsxxxxxxxrjy2§ionid=251,253&total_price=3®ionid=17 後面這段 page=1&device=pc&device_id=qdmsxxxxxxxrjy2§ionid=251,253&total_price=3®ionid=17


以下我將其全部羅列出來,但 "按地區" 和 "按捷運" 有太多種類,就不一一呈現,可以自己選擇後再從 開發人員工具 查看。

* 以下某些選項在網頁上是可以多選的,則參數的數值用逗號 , 隔開即可。如 tag=1,4,5 代表 "特色" 勾選 近捷運重劃區近公園


按地區

縣市regionid
台北市1
新北市3
桃園市6
台中市8
台南市15
高雄市17

如果要更細分 "地區"、"生活圈" 的話,要再加上 sectionidshop_id 參數,如下:

代表意思參數
台北市regionid=1
台北市 > 內湖區regionid=1§ionid=10
台北市 > 內湖區 > 內湖科技園區regionid=1§ionid=10&shop_id=164
高雄市 > 楠梓區regionid=17§ionid=251
高雄市 > 楠梓區 > 高雄大學特區regionid=17§ionid=251&shop_id=385
高雄市 > 鳳山區 > 衛武營生活圈regionid=17§ionid=268&shop_id=290

按捷運

要多加 search_type 代表按捷運篩選。

捷運city
台北捷運1
高雄捷運2
新北捷運3
桃園捷運4
台中捷運5

同樣如果要更細分 "路線"、"站點" 的話,要再加上 subway_linesubway_id 參數,如下:

代表意思參數
台北捷運search_type=1&city=1
台北捷運 > 淡水信義線search_type=1&subway_line=2&city=1
台北捷運 > 淡水信義線 > 新北投search_type=1&subway_line=2&subway_id=4198&city=1
高雄捷運 > 高雄紅線 > 凱旋search_type=1&city=2&subway_line=1&subway_id=4333
台北捷運 > 高雄輕軌 > 五權國小search_type=1&city=2&subway_line=3&subway_id=66388


單價

單價unit_price
50萬/坪以下1
50-70萬/坪2
70-80萬/坪3
80-100萬/坪4
100-120萬/坪5
120萬/坪以上6

* 如果要自訂單價金額範圍,改用 unit_price_minunit_price_max 參數(例如 "10-30萬/坪" 要用 unit_price_min=10&unit_price_max=30)。

總價

總價total_price
1500萬以下1
1500-2000萬2
2000-3000萬3
3000-4000萬4
4000-6000萬5
6000萬以上6

* 如果要自訂總價金額範圍,改用 total_price_mintotal_price_max 參數(例如 "500-1000萬" 要用 total_price_min=500&total_price_max=1000)。

* 要注意的是 "單價" 與 "總價" 好像會隨著不同的縣市,而代表不同的範圍。例如高雄市 unit_price=1 是代表 "15萬/坪以下"。


格局

格局room
一房1
二房2
三房3
四房4
五房及以上5

型態

型態shape
住宅大樓3
電梯公寓7
華廈10
別墅1
透天2
商辦大樓8
廠辦11
其他9

狀態

狀態build_type
預售屋1
新成屋2
預推案4

用途

用途purpose
住家用1
住商用2
商業用3
工商用4
工業用5
其他6

特色

特色tag
近捷運1
明星學區2
重劃區4
充電設備piles
近公園5
景觀宅6
創意空間8
制震宅10
低首付12
超值好房100
優選建案101
地上權13


排序

也可以透過帶入排序參數 sort,來對搜尋結果建案作排序。

  • 不過我發現有時候加入排序,搜尋結果好像怪怪的 (直接操作網頁也是)
排序sort
默認排序(無)
金額排序從小到大1
金額排序從大到小2
瀏覽人數從多到少5
瀏覽人數從少到多6
開案時間從新到舊9
開案時間從舊到新10

換頁

試著在瀏覽器繼續往下滾動,它會持續載入新資料,觀察請求 開發人員工具 > Network 新載入的網址變化:

https://newhouse.591.com.tw/home/housing/list-search?page=2&device=pc&device_id=qdmsxxxxxxxxxrjy2®ionid=1

https://newhouse.591.com.tw/home/housing/list-search?page=3&device=pc&device_id=qdmsxxxxxxxxxrjy2®ionid=1

會發現它是透過網址後的 page 參數,來切換不同頁數,並且每頁有 20 筆資料(建案)。

頁數page
第一頁1
第二頁2
第三頁3
第 X 頁X

* 當然要它的 "總頁數" 有達到你指定的頁數才會有資料,至於怎麼知道 "總頁數"?可以從回傳資料裡找到,下面我們接著來看~


回傳資料

回傳資料主要有 bluekai_datadata 欄位。

仔細觀察可以發現 bluekai_data 可能是我們設定的篩選條件,但實際測試又不太一樣,反正不重要,我們不用理它。

data 欄位包含 建案基本資料(items) 與 總頁數(total_page)、當前頁數(page)、總筆數(total)、當頁筆數(per_page) 等等資訊,上一節說的 "總頁數" 就可以從這邊取得。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
    "status": 1,
    "data": {
        "total": 1040,
        "online_total": 241,
        "per_page": 20,
        "page": 2,
        "total_page": 52,
        "show_surround": 0,
        "items": [ "建案基本資料..." ]
    },
    "bluekai_data": {
        "region_id": "1",
        "section_id": "",
        "sale_price": 0,
        "rental_price": 0,
        "unit_price_per_ping": "",
        "room": "",
        "shape": "",
        "mrt_city": "",
        "mrt_line": "",
        "tag": 0,
        "type": "newhouse",
        "kind": "",
        "page": "newhouse_list"
    }
}

而這邊取到的「建案基本資料」,會有像是 "名稱"、"地址"、"總價"、"單價"、"坪數"、"電話"、"特色"、"圖片"……等等資訊,如果還需要進一步的詳細資訊,就要參考下一節的「取得建案詳情」。

以下是我擷取其中一筆範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{
    "hid": 121254,
    "build_name": "太平洋之森",
    "regionid": 1,
    "sectionid": 1,
    "region": "台北市",
    "section": "中正區",
    "addr_number": "XX路一段123號",
    "status": 2,
    "community_id": 35980,
    "build_type": 2,
    "build_type_val": 2,
    "updatetime": "2024-10-01",
    "address": "台北市中正區XX路一段123號",
    "address_new": "中正區 XX路一段123號",
    "is_full": 0,
    "is_full_720": 0,
    "is_full_sky": 0,
    "cover": "https:\/\/img2.591.com.tw\/house\/2020\/03\/15\/158426288485027607.jpg!400x300.s2.jpg",
    "tag": [
        "近捷運",
        "低公設",
        "景觀宅",
        "制震宅"
    ],
    "purpose_str": "住家用",
    "build_article": 0,
    "sale_status": 3,
    "is_upscale": 0,
    "active_rank": {
        "region": 0,
        "section": 0
    },
    "service_time": {
        "service_time_start": "10:00",
        "service_time_end": "18:00",
        "is_service_time": 0,
        "service_time": "10:00-18:00"
    },
    "open_sell_year_month": "",
    "shop_id": 80,
    "shop_name": "古亭生活圈",
    "is_competitor_status": 1,
    "call_num": "688",
    "phone": "0986-000-000",
    "phone_ext": "21230",
    "area": "3~4坪",
    "room": "三房(45坪),四房(50、66坪)",
    "price_unit_test": "110~120",
    "total_price_test": "4388~6000",
    "price": "110~120",
    "price_unit": "萬\/坪",
    "is_video": 0,
    "im_status": 1,
    "im_reply_avg_time": 86400,
    "im_question": {
        "entry_id": 27,
        "question_id": 59,
        "question": "我對這個建案感興趣,可以介紹一下嗎?"
    },
    "ad_sort": 0,
    "expired_ad_sort": 0,
    "tel_num": "688",
    "is_vip": 0,
    "is_article": 0
}

完整回覆資料範例:newhouse591_search.json (搜尋建案) | GitHub

* 如果看到類似 \u53f0\u5317\u5e02 的編碼,可以透過 Unicode 轉換即可。


範例程式

使用 Python 撰寫 "搜尋建案" 的寫法如下,完整程式碼請至文末參考:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def search(self, filter_params=None, sort_param=None, want_page=1):
    """ 搜尋新建案

    :param filter_params: 篩選參數
    :param sort_params: 排序參數
    :param want_page: 想要抓幾頁
    :return total_count: requests 建案總數
    :return house_list: requests 搜尋結果建案資料清單
    """
    total_count = 0
    house_list = []
    page = 0
    
    # 搜尋建案
    url = 'https://newhouse.591.com.tw/home/housing/list-search'
    params = 'device=pc&device_id=1234567890'
    
    # 篩選參數
    if filter_params:
        params += ''.join([f'&{key}={value}' for key, value, in filter_params.items()])
    # 排序參數
    if sort_param:
        params += ''.join([f'&{key}={value}' for key, value, in sort_param.items()])

    self.headers['referer'] = urllib.parse.quote(f'https://newhouse.591.com.tw/list?{params}')
    
    while page < want_page:
        page += 1
        params = f'page={page}&{params}'
        print(f"Get 建案資料: {url}")
        r = requests.get(url, params=params, headers=self.headers)
        if r.status_code != requests.codes.ok:
            print('請求失敗', r.status_code)
            break
        
        data = r.json()
        total_count = data['data']['total']
        house_list.extend(data['data']['items'])
        
        # 判斷是否為最後一頁
        if page >= data['data']['total_page']:
            break
        time.sleep(random.uniform(2, 6))  # 隨機 delay 一段時間

    return total_count, house_list

取得建案詳情

當上一步從搜尋結果找一個建案點進去後,會跳到此建案的頁面,呈現關於此建案的詳細資訊。
網址會類似這樣:https://newhouse.591.com.tw/135743?roster_type=1

有關建案的詳細資訊又可以包含「建案資料」、「周邊機能」、「實價登錄」 (其他的各位有需要可以自己試試),以下會再分別介紹這三個請求 API。

591 新建案 > 建案頁面
591 新建案 > 建案頁面

請求路徑與參數

接下來我們分別從對應的頁面,一樣透過瀏覽器的 開發人員工具 (F12 或 Ctrl + Shift + i) > Network 分頁,在 "Fetch/XHR" 分類中可以找到請求這些資料的路徑與參數。

「建案資料」
url: https://bff.591.com.tw/v1/housing/detail-info?id={house_id}&is_auth=0

591 新建案 > 建案詳情
591 新建案 > 建案詳情
591 新建案 > 建案詳情
591 新建案 > 建案詳情

「周邊機能」
url: https://bff-newhouse.591.com.tw/v1/detail/surrounding?id={house_id}&is_auth=0

591 新建案 > 周邊機能
591 新建案 > 周邊機能

「實價登錄」
url: https://bff-market.591.com.tw/v1/price/list?community_id={community_id}&split_park=1&page=1&page_size=20&_source=0

591 新建案 > 實價登錄頁面
591 新建案 > 實價登錄頁面

* 比較要注意的是,「實價登錄」網址使用的參數是 "community_id" (而不是 house_id),所以程式裡面我還有先透過 BeautifulSoup,從網頁中找出 "community_id"。


回傳資料

回傳資料大致上也是與網頁上的一樣,關於此房屋的詳細資訊,你可以挑出需要的欄位來儲存,或進一步判斷執行其他動作。

建案資料 回傳資料範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{
    "status": 1,
    "msg": "請求成功",
    "data": {
        "hid": 135743,
        "regionid": 1,
        "sectionid": 3,
        "region": "台北市",
        "section": "中山區",
        "addr_number": "xxxxxxxx",
        "address": "台北市中山區xxxxxxxx",
        "build_name": "xxxx美術館",
        "price_unit": { ... },
        "price_total": { ... },
        "price": { ... },
        "build_type_name": "預售屋",
        "is_upscale": 0,
        "status": 2,
        "is_online": 1,
        "layout": [ ... ],
        "purpose_name": "住宅大樓",
        "purpose_other_name": "住商用",
        "deal_time": { ... },
        "decorate": "標準配備",
        "reception_address": "台北市中山區xxxxxxxx",
        "park_price": { ... },
        "base_area": { ... },
        "build_area": { ... },
        "terrace": { ... },
        "ratio": "35.38%",
        "jbrate": "41.9%",
        "park_ratio": "1:0.81",
        "park_planning": "平面式62個",
        "park_piles": "有充電設備(含預留)",
        "park_style": "暫無",
        "manage_cost": { ... },
        "structural_engine": "RC鋼筋混凝土結構",
        "land_division": "商二特",
        "households": "1幢,1棟,73戶住家,4戶店面",
        "floor": "地上15層,地下4層",
        "sell_time": { ... },
        "down_pay": "25%",
        "lend_rate": "75%",
        "project_pay": "",
        "direction_rule": "朝南",
        "manage_committee": 0,
        "build_intro": "依現場為主",
        "facility": [ ... ],
        "remark": "台北市有一座美術館即將誕生,純粹靜謐與藝術優雅...",
        "room_rule": { ... },
        "fav_num": 224,
        "share_num": 377,
        "meta": { ... },
        "transportation": [ ... ],
        "surrounding": [ ... ],
        "building_design": [ ... ],
        ...其他
    }
}

周邊機能 回傳資料範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
    "status": 1,
    "msg": "success",
    "data": {
        "housing": {
            "hid": 135743,
            "build_name": "xxxx美術館",
            "address": "台北市中山區xxxxxxxx",
            "map": { ... }
        },
        "facility": {
            "traffic": { ... },
            "education": {
                "distance": 2,
                "items": [
                    {
                        "id": 278987,
                        "name": "私立新生幼兒園",
                        "distance": 180,
                        "lat": 25.0596716,
                        "lng": 121.5280426,
                        "sub_type": "child"
                    },
                    {
                        "id": 103,
                        "name": "臺北市中山區吉林國民小學",
                        "distance": 447,
                        "lat": 25.05416379999999,
                        "lng": 121.5292245,
                        "sub_type": "grade"
                    },
                    ...
                ],
                "count": {
                    "grade": 5,
                    "middle": 5,
                    "child": 1,
                    "university": 1
                }
            },
            "life": { ... },
            "other": { ... },
            "disgust": { ... }
        }
    }
}

實價登錄 回傳資料範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
{
    "status": 1,
    "data": {
        "items": [
            {
                "id": 6584759,
                "date": "113-06-11",
                "trans_date": "2024-06-11",
                "month": "113-06",
                "address": "台北中山區xxxxxxxx",
                "layout": "3房",
                "layout_v2": "3房2廳",
                "room_search": 3,
                "build_area": "43.30坪",
                "build_area_v": { ... },
                "building_area": { ... },
                "building_total_price": { ... },
                "real_park_area": { ... },
                "unit_price": { ... },
                "src_unit_price": { ... },
                "total_price": "4,777萬",
                "total_price_v": "4,777",
                "price_tips": "",
                "context": "",
                "is_special": 0,
                "has_park": "1",
                "unit_has_park": "0",
                "shift_floor": "5樓",
                "total_floor": "15樓",
                "build_purpose_str": "住宅",
                "real_park_total_price": "395",
                "real_park_total_price_v": { ... },
                "community": { ... },
                "trans_rep_year": "113年",
                "tag": [ "低樓層", "平面車位" ],
                "park_type_str": "平面車位",
                "match_type": 0,
                "tips": "",
                "park_count": 1,
                "history_trans_count": 1,
                "is_new_tag": 0,
                "assoc_type": 0,
                "business_circle_id": 0,
                "build_date": "",
                "original_shift_floor": "5",
                "original_total_floor": 15
            },
            ...
        ],
        "per_page": 20,
        "page": 2,
        "total_page": 2,
        "total": 30,
        "has_sale_ctrl": 1,
        "region_id": 1,
        "meta": { ... },
        "trans_rep_years": [ ... ],
        "address_condition": [ ... ],
        "special_num": 30,
        "community_park": { ... }
    }
}

各個完整回覆資料範例:
newhouse591_build_detail.json (建案資料) | GitHub
newhouse591_build_surrounding.json (周邊機能) | GitHub
newhouse591_build_price.json (實價登錄) | GitHub

* 如果看到類似 \u53f0\u5317\u5e02 的編碼,可以透過 Unicode 轉換即可。


範例程式

使用 Python 撰寫 "取得建案詳情" (包含 建案資料、周邊機能、實價登錄) 的寫法如下,完整程式碼請至文末參考:

* 請求的 headers 內有 deviceid 參數,原本以為可以不用給,但後來發現不給會導致取不到 “房屋詳情 > 周邊機能”,不過好像可以給隨意值就行 (可以在完整程式碼內查看)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def get_newhouse_detail(self, house_id):
    """ 取得建案詳情 (建案資料+周邊機能+實價登錄)

    :param house_id: 建案 ID
    :return house_detail: requests 建案詳細資料
    """
    house_detail = {}
    
    # 建案資料
    url = f'https://bff.591.com.tw/v1/housing/detail-info?id={house_id}&is_auth=0'
    self.headers['referer'] = 'https://newhouse.591.com.tw/'
    print(f"Get 建案資料: {url}")
    r = requests.get(url, headers=self.headers)
    if r.status_code != requests.codes.ok:
        print('請求失敗', r.status_code)
    else:
        data = r.json()
        house_detail['detail'] = data['data']
    time.sleep(random.uniform(1, 3))  # 隨機 delay 一段時間
    
    # 周邊機能
    url = f'https://bff-newhouse.591.com.tw/v1/detail/surrounding?id={house_id}&is_auth=0'
    # https://bff-newhouse.591.com.tw/v1/detail/surrounding?id=120137&is_auth=0
    self.headers['referer'] = 'https://newhouse.591.com.tw/'
    print(f"Get 周邊機能: {url}")
    r = requests.get(url, headers=self.headers)
    if r.status_code != requests.codes.ok:
        print('請求失敗', r.status_code)
    else:
        data = r.json()
        house_detail['surrounding'] = data['data']
    time.sleep(random.uniform(1, 3))  # 隨機 delay 一段時間

    # 實價登錄
    # 要先取得 community_id
    url = f'https://newhouse.591.com.tw/{house_id}?roster_type=1'
    r = requests.get(url, headers=self.headers)
    if r.status_code != requests.codes.ok:
        print('請求失敗', r.status_code)
    else:
        soup = BeautifulSoup(r.text, 'html.parser')
        canonical_href = soup.select_one('section.market a.status-table')['href']
        community_id = canonical_href.split("/control/")[-1].split("?")[0]
        
        # 抓取實價登錄清單
        price_list = []
        page = 0
        while page < 99:
            page += 1
            url = f'https://bff-market.591.com.tw/v1/price/list?community_id={community_id}&split_park=1&page={page}&page_size=20&_source=0'
            # https://bff-market.591.com.tw/v1/price/list?community_id=5935592&split_park=1&page=2&page_size=20&_source=0
            self.headers['referer'] = 'https://market.591.com.tw/'
            print(f"Get 實價登錄: {url}")
            r = requests.get(url, headers=self.headers)
            if r.status_code != requests.codes.ok:
                print('請求失敗', r.status_code)
                break
        
            data = r.json()
            price_list.extend(data['data']['items'])
            
            # 判斷是否為最後一頁
            if page >= data['data']['total_page']:
                break
            time.sleep(random.uniform(2, 5))  # 隨機 delay 一段時間
        
        house_detail['price'] = price_list
    
    return house_detail

完整程式碼

我將以上兩種請求整合起來到 class 中,並可以傳入不同的值來篩選,供需要的人參考。

附上完整程式碼:newhouse591_spider.py | GitHub



結語

這次我們改練習爬取 591 房屋交易網站的「新建案」,分別取得 "搜尋建案" 與 "建案詳情 (建案資料、周邊機能、實價登錄)" 的資訊。


如果你正好是剛開始想學爬蟲的新手、想知道某網站如何爬取資料,歡迎查看 Python網路爬蟲實例,或追蹤 FB 粉專『IT空間』~ 🔔




失敗不是終點,而是一個學習和成長的機會。
Failure is not the end, it’s an opportunity to learn and grow.


🔻 如果覺得喜歡,歡迎在下方獎勵我 5 個讚~
分享

Jia
作者
Jia
軟體工程師 - Software Engineer