用 Python Selenium 實作租屋爬蟲的心得

Eason Lin
10 min read3 days ago

--

Photo by Mikhail Vasilyev on Unsplash

近期因為有看租屋處的需求,本身不太喜歡花大量時間看社群媒體,因此想透過一個自製介面看臉書社團及更多提供租屋資訊的平台,讓自己能在最短時間內獲取所需的資訊,就想嘗試用爬蟲去解決這個問題,於是花了幾天的時間學了 Python,這篇文記錄一下學習過程及實際成果。

學習方式

雖說我曾經學過 Python,但那也是我籌備成為前端工程師前的時期,也就是五年多前的事情了。(可參考我在 Medium 發表的我在飯店業三年多,為什麼開始籌備轉職Web Developer)除了變數怎麼宣告外,其他我全忘了。我選擇上手的教材是《Python從初學到生活應用超實務: 讓Python幫你處理日常生活與工作中繁瑣重複的工作》。

選擇這本書的原因很簡單,因為它從 Python 的基礎開始講起,接著就銜接到了常見的應用,例如 BeautifulSoup、Pandas、File System、Request、Selenium 等等。由於我沒有打算在短期內精通這項程式語言,它「每個都講一點、每個都講不深」的篇幅就非常合我的胃口,知道常見的應用情景,後續有需要再上網查閱相關文件即可。

思路

我實作預想的思路也很單純,就是:

  1. 上我要爬的頁面
  2. 爬取我需要的資訊
  3. 轉換成 json 和 html,並讓其可透過 API Route 取得
  4. 用自製的介面渲染出來

所需的工具或技術如下:

  • Selenium
  • BeautifulSoup
  • File System
  • Pandas
  • 自製介面
  • fastapi

Selenium 會用到它的 WebDriver;BeautifulSoup 用到它的語法(它就是一個類似 jQuery 的庫,非常好用);File System 用來寫入檔案用的;Pandas 是轉 HTML Table 用到的,如果介面是自製的可不用。

自製介面就看個人,我是用 framework7 + Vue,會用這個只是因為我上班的公司內部有個產品有用它為底來做 Hybrid App,想藉此熟悉一下而已。只要做得出介面,單純的 HTML + CSS + JavaScript 每一行都手寫我覺得都無所謂。

fastapi 是用來做 API Route 用的,換成其他框架也都可。

實作

Repository:https://github.com/EasonLin0716/rent-crawlers

坦白說,租屋網的部分我最終只完成了 591 的部分,臉書社團則放棄了。也就是說這是一個沒有做完的 Side Project。後來考量的原因有二:

  1. 儘管爬蟲的自動化很方便,只要爬取頁面的結構或規則發生改變,就要一起調整。租屋算是持續但短期、且下一次不確定是何時的需求,下一次很可能又要全部重做,時間成本的權衡上不太理想。
  2. 我在邊試 Selenium 邊研究臉書租屋社團的 HTML 結構時,發現我的通知欄出現了以下畫面:

由於有點害怕過於頻繁地登入會導入帳號可能被鎖,我索性放棄了臉書租屋社團的爬蟲。

程式碼的部分,最主要的邏輯是 app/five_nine_one/crawler_selenium.py ,這個腳本在專案的 .env 中只要下了對應的網址條件,用 Python 執行後就會產出對應的 .json.html 檔。

以腳本中的 write_normal 為例:

def write_normal(driver, soup, start_url):
def get_normal_items(soup):
items = soup.select('.list-wrapper .item')
for item in items:
image_lst = []
images = item.select('img[alt="物件圖片"]')
for image in images:
image_lst.append(image['data-src'])
title = item.select_one('a.link').text
link = item.select_one('a.link')['href']
area_and_floor = item.select('span.line')
area_data = area_and_floor[0]
floor_data = area_and_floor[1]
address_data = item.select('.item-info-txt')[1]
price_data = item.select_one('div.item-info-price')
area = generate_area_text(area_data)
floor = generate_floor_text(floor_data)
address = generate_address_text(address_data)
price = generate_price_text(price_data)
data.append([title, price, address, floor, area, image_lst, link])
data = []
page = 1
while soup.select_one('.empty') is None:
print(f'getting page {page}')
get_normal_items(soup)
print(f'successfully crawl page: {page}')
page += 1
soup = get_page_content(driver, start_url + f'&page={page}')
time.sleep(1)
columns = ['title', 'price', 'address', 'floor', 'area', 'images', 'link']
df = pd.DataFrame(data, columns=columns)
json_output = df.to_json(orient='records')
write_file(json_output, '591normal.json')
df['images'] = df['images'].apply(render_images)
df['link'] = df.apply(lambda x: render_link(x['link'], x['title']), axis=1)
html_output = df.to_html(escape=False)
write_file(html_output, '591normal.html')
print('write normal done')

參數的 driver 會傳入 WebDriver; soup 會傳入解析過 driver.page_source 後的 BeautifulSoup; start_url 則是傳入起始頁面。爬蟲會先確認頁面中有無資料,若無就停止並轉為 json 及透過 Pandas 轉為 HTML Table;若有則會爬取對應的 HTML Element 並轉換格式成我們需要的樣子。

在研究網站架構時,有碰到兩個困難。首先是站上有設 interval timer 去監聽 Devtool,若其被開啟了就會強制用 JavaScript 導向首頁。用 Devtool 把 JavaScript 禁掉可能是個可行的方法,但站上是使用 Nuxt.js,所以這招也不太可行,所以最後採取了用存原始碼的方式。

再來則是站上有一些資訊是有做一些防爬機制的。例如這邊的 HTML Element:

<span class="line" data-v-3ea3f673="">
<span data-v-3ea3f673="" style="display:inline-flex;">
<!--[-->
<i style="order:3;font-style:normal;">
3
</i>
<i style="order:2;font-style:normal;">
/
</i>
<i style="order:0;font-style:normal;">
1
</i>
<i style="order:4;font-style:normal;">
F
</i>
<i style="order:1;font-style:normal;">
F
</i>
<!--]-->
</span>
</span>

如果直接去取得它的文字,會得到「3/1FF」,這顯然不是一段有意義的文字,如果觀察它 styleorder 值,就會發現如果從 0 開始照順序排:「1F/3F」看起來就有意義多了。花了點時間研究後,並沒有找到一個可以將 style 中的字串轉換為 Dict 的方法,所以就用了有點土砲的方式去抓:

def sort_element_by_style_order(elements: list):
def sort_fn(x: str):
order_value = x['style'].split(':')[1].split(';')[0]
return int(order_value)
sorted_items = sorted(elements, key=sort_fn)
result = ''.join([item.text for item in sorted_items])
return result

簡單地說,就是把 style="order:<number>;font-style:normal;" 從第一個冒號拆成字串陣列,並在從分號拆成字串陣列,就可以取得 order 中的值,只要未來負責的前端把 orderfont-style 調換,這隻爬蟲基本上就失效了。

介面的部分原本預計是要做在手機看而已,所以當時規劃是長這樣:

用 framework7 + Vue 真的是滿輕鬆的,幾乎不用寫 style 就能做出一個像是 App 的東西。

心得

雖說這是一個最後沒被我拿來應用的 Side Project,但整體下來還是滿愉快的,也學到不少東西。後續也打算繼續把那本書讀完,就把 Python 拿來作為潛在可以一個幫助我解決一些麻煩問題的工具。

以上就是實作租屋爬蟲的心得,Repository 我也會放在下方,如果有任何疑問也歡迎留言問我喔!

References:

https://selenium-python.readthedocs.io/

Repository:

https://github.com/EasonLin0716/rent-crawlers

--

--