Python|使用 Pandas 讀取澳門公職人員開考列表及進行文字處理與篩選
大家好,來繼續 Python Pandas 數據處理系列介紹。
我在 使用 Pandas 的 read_html 讀取網頁上的表格內容 中介紹了不同的表格讀取例子,但讀取後未進一步探討如何處理。在這篇中,我們以澳門公職人員開考列表之網頁作為例子,從讀取至分柝文字及搜尋篩選開考資訊。
在這個例子中,我們會學到 Python 及 Pandas 的以下幾方面技巧:
- 當 read_html 不成功時,使用 requests 配搭取得網頁內的 <table> 元素內容
- 使用 Split 將字串切開
- 使用 apply 加 split 將一行內容拆成多欄,方便搜尋篩選
- Python 列表 Slicing 取得頭 N 項數據
目標網站分析
目標網站:https://concurso-uni.safp.gov.mo/
在上述網站中,中間有三個開考表格,我示範的目標是第三個:「專業或職務能力評估開考(2021/7/1後開展的)」。
中間三個是 <table> 表格,即我們期望使用 read_html 取得網站後,最少有三個 DataFrame 表格,可能會因為網站其他地方也使用了 <table> 而有更多,但最少應為 3 個。
使用 requests 配搭取得網頁內的 <table> 元素內容
在使用 Pandas 的 read_html 讀取時,有些情況會出現表格就在那𥚃,但使用 pandas read_html 時會報錯,說找不到 <table> 元素,此時大約有以下可能,可能性由高至低排列:
- 網站內容目測是表格,但背後的 HTML 代碼其實不是表格。
- 網站的 <table> 數據是 JavaScript 動態加載的,來源是 JSON。
- 網站的 <table> 數據是 JavaScript 動態載入的,而來源亦是 <table> HTML 格式。
- 網站在 pandas 存取時會屏蔽,不返回數據。
這個網站就是原因 4,當我們嘗試用以下代碼時,read_html 會報錯說找不到 <table> 元素。因為 Pandas 的存取會內置使用 urllib3,所以網站會有可能針對這個字眼來的客戶端進行屏蔽。
import pandas as pd url = "https://concurso-uni.safp.gov.mo/" tables = pd.read_html(url) # 錯誤
解決方案是使用其他存取方法取得此版網頁的 HTML,再把 HTML 交給 read_html 分析成 DataFrame 數據。
而其他方法,包括使用 BeautifulSoup 或 Selenium。前者是下載網站的 HTML 內容並分析生成元素結構樹,然後我們可以通過 CSS 選取器按條件取得當中的內容,是通用的網站爬蟲方式。而後者,Selenium 是控制瀏覽器的自動化測試工具,可以用編程控制瀏覽器載入網頁,包括用 JavaScript 動態生成的網頁,甚至自動填表或進行介面操作等。
由於 BeautifulSoup 是下載內容的機制而 Selenium 是啟動瀏覽器自動化機制,兩者運行速度相差甚遠,所以我們會優先使用 BeautifulSoup 下載 HTML 內容,若失敗時,或必須自動化操作始能存取目標數據時,才會使用 Selenium。
以下代碼,我們嘗試使用 requests 讀取網站,再將讀取得的 HTML 源代碼直接交予 read_html,印出找到的表格數量,得到 4 個表格。
import pandas as pd import requests url = "https://concurso-uni.safp.gov.mo/" res = requests.get(url) tables = pd.read_html(res.text) print(len(tables))
取得的 4 個數據表格中,當中第三個,就是我們需要的。所以 df = tables[3] 然後列印出來望望,得出以下數據。
設定欄名稱
Pandas 的操作,多以欄作為思考。我們會檢視數據的欄位,只保留需要的,反為這些原始數據的欄位重新命名,以適合我們一會使用。
由於這個表格只有兩欄,我們可以直接用 df.columns 設定。例如:
df.columns = ["日期", "內容"]
設定後,我們可以通過 df["內容"] 來存取整欄的內容。每欄數據的類型為 Pandas 的 Series (系列)類型。
例如以下為 df["內容"] 的結果,可以列出每行的內容。
分柝文字內容,得出部門、範疇、開考職階
從圖中可以看出,這些內容有待清理。這些內容有一定模式,就是一開始是部門名稱,伴隨一個空格,緊接是開考範野,再伴隨另一個空格,緊接是開考職階。我們可以利用 split,對每一行的內容按空格切開,得出列表。
例如,假若有這句字串「交通事務局 機電工程範疇 第一職階二等高級技術員」
example = "交通事務局 機電工程範疇 第一職階二等高級技術員" example.split(" ")
而 Python 中,我們可以同時將 N 個變量名稱對應 N 個列表項目,會一一儲存對應的項目數據,我們稱這個動作為 unpack。例如,以下 split 範例,我們可以分別將三個值放到三個變量中去。
example = "交通事務局 機電工程範疇 第一職階二等高級技術員" 部門, 範疇, 職階 = example.split(" ")
這樣就可以分別將列表的三個值分配到三個獨立的變量中。要注意的是使用這招式,必須前後數量匹配才可。如果要處理的文字可能有額外空格,我們可以加上 [:3] 來確保只取頭三個值。
example.split(" ")[:3]
關於 [:3],這是列表切片,詳情可以參考:Python 使用列表切片 List Slicing 取得列表的範圍數值。
整欄處理的 apply 函數
上述就是 split 的基本用法。但我們不會這樣逐列逐列處理。於 Pandas 中,我們是以欄位思考的,可以用 apply 來為整欄的數據套用上述的 split 函數。
df["內容"].apply(lambda x: x.split(" ")[:3])
關於 lambda 的使用,可以參考 @YCJHUO 的在 Pandas 中,如何使用 lambda 以及客制化 boolean 值。
以下為運行結果,可以看到每行內容已成功抽取為三個獨立的值。
但我們需要的是生成三欄獨立的欄,所以基於上述代碼,需要再套用 apply(pd.Series),其原理是當 Pandas 生成欄(Series)時,會自動將多重列表、字典(Dict)等拆分成欄。所以更新後的代碼如下:
df["內容"].apply(lambda x: x.split(" ")[:3]).apply(pd.Series)
從結果可見,今次我們成功從每行的一串文字抽取出三項資訊,並分別得出三欄。
將此三欄加到原 DataFrame 數據表上,數據清理階段便暫告完成。
df[["部門", "範疇", "職階"]] = df["內容"].apply(lambda x: x.split(" ")[:3]).apply(pd.Series)
數據使用
於這份數據中,我們可以做一些基本查詢,例如看一看數據中共涉及哪些部門及哪些範疇。
set(df["部門"])
set(df["範疇"])
我們亦可以通過 group by,按範疇分類,數數有多少筆記錄,或倒轉按部門分類,數數每個部門有多少筆記錄。
按範疇分類:
df.groupby(by="範疇")["內容"].agg(len)
按部門分類:
df.groupby(by="部門")["內容"].agg(len)
groupby 是將某一欄的數據變成索引,而關連此欄每一項數據的,就按我們提供的函數整合。常見的整合有 .agg(np.sum) 加總、.agg(np.mean) 取平均值、及上述代碼使用的 .agg(len) 數數目。
篩算特定數據
我們可以將特定值作為篩選條件,通過某一欄位與值的比較,我們會得出一個 True/False 的 Boolean 布林值序列(Series),再將此序列放回 DataFrame,就以只顯示 True 的那列,達至篩選效果。
mask = df["範疇"]=="電機工程範疇" df[mask]
亦可以隨時將覺得有用的 DataFrame 數據輸出成 Excel:
df[mask].to_excel("最近電機工程範疇開考列表.xlsx")
總結
此篇,我們借助澳門公職人員開考列表,示範了從網絡中取得內容後,如何通過 split 字串處理抽出內容、通過 apply 批量套用函數、使用 groupby 計算基統計數值、及使用 == 篩選數據。
今日的例子以文字處理為主,之後我們再以其他例子探討 Pandas 的其他應用,如數值處理、移動平均線運算、文字的正規表達成抽出等等。
Pandas 系列文章
- 使用 Pandas 的 read_html 讀取網頁上的表格內容
- 本文:使用 Pandas 讀取澳門公職人員開考列表及進行文字處理與篩選
— 麥誠 Makzan,2022-01-14。
我是麥誠軒(Makzan),除了正職外,平常我要麼辦本地賽與辦世界賽,要麼任教編程與網站開發的在職培訓。現正轉型將面授培訓內容寫成電子書、網上教材等,至今撰寫了 7 本書, 2 個視頻教學課程。
如果我的文章有價值,請左下角 👍🏻按讚支持,或訂閱贊助我持續創作及分享。