前言
很久之前有寫一篇「591 租屋網 - 搜尋房屋與房屋詳情」,是針對 591 內的租屋網站進行資料抓取。
不過後來網站有改版,那時的程式寫法就失效了。
最近有再嘗試,雖然有找出發請求的網址,但發現回傳資料有透過 AES 之類的加密方式加密過的,要解開的話還要從它的 JS 程式中尋找 KEY 值,因為太麻煩,我就沒有繼續研究了。
如果有需求的讀者,可以參考這篇文章:AWS 雲端 591 租屋爬蟲架構,他有針對新版的去做修正,而且還加入資料儲存、mail 通知、定時換 IP 等等機制。
今天,改來試試爬取 591 房屋交易網站的「新建案」"搜尋建案" 與 "取得房屋詳情",說明如何發出請求、取得回傳數據資料。
備註:此文僅教育學習,切勿用作商業用途,個人實作皆屬個人行為,本作者不負任何法律責任。
套件
此次 Python 爬蟲主要使用到的套件:
安裝
1
2
| pip install requests
pip install beautifulsoup4
|
搜尋建案
在 591 房屋交易網站的「新建案」主頁,我們先直接按搜尋,它會跳到另一個頁面。
在這個搜尋頁面,上方可以選擇不同的搜尋條件,下方則是房屋搜尋結果列表。
請求路徑與參數
進到「591 新建案」的搜尋頁面後,打開瀏覽器的 開發人員工具 (F12 或 Ctrl + Shift + i) > Network 分頁,在 "Fetch/XHR" 分類中找找看哪個是搜尋建案結果的請求。
稍微尋找後,可以確定是 https://newhouse.591.com.tw/home/housing/list-search
這個請求。
另外,我們可以透過帶入不同的搜尋參數值,來達到對結果做不同的篩選。
當我們選擇不同條件後,觀察剛剛的請求,會發現它在網址後方帶入不同的參數 (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 |
如果要更細分 "地區"、"生活圈" 的話,要再加上 sectionid
、shop_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_line
、subway_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_min
和 unit_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_min
和 total_price_max
參數(例如 "500-1000萬" 要用 total_price_min=500&total_price_max=1000
)。
* 要注意的是 "單價" 與 "總價" 好像會隨著不同的縣市,而代表不同的範圍。例如高雄市 unit_price=1
是代表 "15萬/坪以下"。
格局
型態
型態 | shape |
---|
住宅大樓 | 3 |
電梯公寓 | 7 |
華廈 | 10 |
別墅 | 1 |
透天 | 2 |
商辦大樓 | 8 |
廠辦 | 11 |
其他 | 9 |
狀態
用途
用途 | 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 筆資料(建案)。
* 當然要它的 "總頁數" 有達到你指定的頁數才會有資料,至於怎麼知道 "總頁數"?可以從回傳資料裡找到,下面我們接著來看~
回傳資料
回傳資料主要有 bluekai_data
和 data
欄位。
仔細觀察可以發現 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。
請求路徑與參數
接下來我們分別從對應的頁面,一樣透過瀏覽器的 開發人員工具 (F12 或 Ctrl + Shift + i) > Network 分頁,在 "Fetch/XHR" 分類中可以找到請求這些資料的路徑與參數。
「建案資料」
url: https://bff.591.com.tw/v1/housing/detail-info?id={house_id}&is_auth=0
「周邊機能」
url: https://bff-newhouse.591.com.tw/v1/detail/surrounding?id={house_id}&is_auth=0
「實價登錄」
url: https://bff-market.591.com.tw/v1/price/list?community_id={community_id}&split_park=1&page=1&page_size=20&_source=0
* 比較要注意的是,「實價登錄」網址使用的參數是 "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 個讚~