前一篇文章 宅宅工程師的強迫症 – 有病要看,有bug就一定要解 裡提到,對政府的網站資料做爬蟲。裡面用到了一些新技巧,寫一篇文章把它紀錄分享下來。
這兩樣新技術(技巧)分別是:
- 連結追蹤:以往爬蟲都是指定好頁面去抓該頁的資料,此次則是要從指定頁面往下深入三四層連結才能抓到資料。有人會問怎麼不先自己進去兩三層抓到最後的網址就好了?這樣不能說錯,但這是業主指定的方式,也不好反駁,也許將來真的連結會改變。
- Excel檔案資料讀取:最後都會抓到 Excel 的檔案,要把裡面的內容讀出來。
這次的網頁沒有JavaScript而需要使用到 Selenium 的工具來處理,要是有的話真的蠻頭大的,Selenium 又慢又肥…
連結追蹤
連結追蹤就是一層層的,進入指定文字所連結到的網址。要找到正確的連結,關鍵還是在於該連結的文字是否夠獨特。若有很多結構相似的文字,這樣難度就會增加,需要做更多的判斷。這功能的程式碼說到底,就是有層次的找到指定的關鍵字,把最符合的取出來。
簡單版 – 指定的網頁就有 XLS 檔
比較簡單的,給定的URL該頁就有 XLS 檔案
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
#返回網頁文字內容 def get_page(URL): # 指定瀏覽器的 Agent, 有些 WebServer 會拒絕不認得的 Agent 資料要求 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0', 'From': 'youremail@domain.com' # This is another valid field } try: # 設定 timeout 時間,不然有問題時會一直卡住 r = requests.get(URL, headers=headers, timeout=60) if len(r.content) < 1000 * 5: # 資料太短,代表有異常 dbgn( "File too short. abort") return None else: return r.content except: return None #儲存檔案 def save_file(url, fn): try: # stream = True 代表要下載檔案 r=requests.get(url, stream=True) if fn.find("top") == -1: fn="xls/"+fn # Return code = 200, 正常回應,開始存檔 if r.status_code == 200: with open(fn, 'wb') as f: for chunk in r: f.write(chunk) f.close() return 0 return 1 except: return 1 # 在指定的URL裡,找到包含 ".xls" 的行,並傳回該行的切割結果 # 另外,我們也會取得"更新時間",避免不必要的下載 def find_xls_link(lv1_link): update_time="" html=get_page(lv1_link) if html == None: return None, None lines=html.splitlines() cnt=0 for i in lines: cnt=cnt+1 if i.find("更新時間") != -1: update_time=i if i.find(".xls") == -1: continue splits=i.split("\"") return splits, update_time return None, None # 從 find_xls_link() 取到含有 .xls 行的切割結果後,進一步將其資訊檢查後 # 儲存在一個 dict 結構裡,方便後續存取 def get_xls_link(url): dict={} dict['is_xlsx'] = 0; dict['url'] = None; dict['ext'] = ""; dict['update_time'] = ""; xls_file, update_time=find_xls_link(url) if xls_file == None: return dict if len(xls_file) < 4: return dict if xls_file[3].find("xlsx") == -1: dict['is_xlsx'] = 0; dict['ext'] = ".xls"; else: dict['is_xlsx'] = 1; dict['ext'] = ".xlsx"; dict['url'] = xls_file[1] dict['update_time'] = update_time return dict # 在一個已知含有 .xls link 的頁面裡,找到 .xls link,並下載成指定檔名 def _get_normal(target, url): result_dict = get_xls_link(url) # 檢查是否需要下載。(更新日期不同的話) if is_new_update(target, result_dict['update_time']) == False: dbgn( "No update") return 0 # 若 url 欄位是空,則代表失敗 if result_dict['url'] == None: dbgn( "Get " + target + " failed") return 1 # 將 Link 存檔 if save_file(result_dict['url'], target + result_dict['ext']) == 0: dbgn( "Get " + target + " success") write_new_update(target, result_dict['update_time']) return 0 else: dbgn( "Get " + target + " failed") return 1 |
主要有5個function
- get_page() : 取得網頁 HTML 原始碼
- save_file():將指定的網址存成檔案
- find_xls_link():找到指定 URL 裡含有 “.xls” 的行,並以【”】切割後返回,以做後續處理
- get_xls_link():將 find_xls_link() 返回的內容做判斷,將資訊組合成一個 dict 結構返回
- get_normal():將一個確定包含 .xls 檔案的 URL 傳入,並下載該檔。
針對簡單的網頁,就直接呼叫 get_normal() 來下載檔案。 get_normal() 會呼叫 get_xls_link() 取得真實的連結,然後再以 save_file() 來存檔。get_xls_link() 的過程中會使用到 find_xls_link() 與 get_page() 來 parse 網頁 HTML 的內容,以判斷要下載的網址在哪。
複雜版 – 追蹤數層後才得知下載頁面
簡單版和複雜版的差別,在於簡單版一開始就知道最終包含 .xls 檔的是哪個頁面,而複雜版需跟隨幾個頁面後才會知道。其實如果這些連結都不會變動,只要一開始手動去找出最終 .xls 的連結就可以了。不過我想業主是怕往後會有變動,順便學習一下如何爬蟲,才會有些需求。
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 |
# 在提供的 HTML 中,找到 header 後的第一個 keyword,將該行以雙引號切割後 # 返回第一個元素 def get_text_hyperlink(html, header, keyword): hdr_found = 0 if html == None: return None lines=html.splitlines() for i in lines: if (hdr_found == 0) and (i.find(header) != -1): hdr_found = 1 continue if hdr_found == 0: continue; if i.find(keyword) == -1: continue splits=i.split("\"") return splits[1] return None # 在指定 URL 取得下一層的 URL, 若是相對網址,則將之轉換為絕對網址返回 def go_next_level_url(url, title, hyperlink): html=get_page(url) next_url = get_text_hyperlink(html, title, hyperlink) if next_url == None: dbgn("no url found " +title + " " + hyperlink + " at url " + url) return None next_url=get_absolute_url(url, next_url) url=next_url return url # 從指定頁面跟隨4層link後,取得下載頁面,並隨得檔案 # 前兩層是固定的,從兩層分別由t1,h1,t2,h2 所指定 # t1,h1 是指 Title1, Hyperlink1 def _get_4_1_13(url, target, t1, h1, t2, h2): url = go_next_level_url(url, "<h2>醫療機構現況及醫院醫療服務量統計", "縣市別") if url == None: return 1 url = go_next_level_url(url, "<h2>縣市別", "護理機構") if url == None: return 1 url = go_next_level_url(url, t1,h1) if url == None: return 1 url = go_next_level_url(url, t2,h2) if url == None: return 1 result_dict = get_xls_link(url) if is_new_update(target, result_dict['update_time']) == False: dbgn("No update") return 0 if result_dict['url'] == None: dbgn("Get " + target + " failed") return 1 if save_file(result_dict['url'], target+ result_dict['ext']) == 0: dbgn( "Get " + target + " success") write_new_update(target, result_dict['update_time']) return 0 |
除了簡單版的 function 外,另外有三個主要的 function
- get_text_hyperlink():在指定的 HTML 內容中,先找到第一個關鍵字所在的行,然後再找到第二個關鍵字所在的行。以雙引號做切割後,返回第一個。這個行為其實跟他的網頁內容、編排方式很有相關。只要其排版方式改變,程式碼就要修改。這也是為何爬蟲是客制化程度很高的程式。
- go_next_level_url():取得的下一層連結,並把網址做一些處理,主要是把相對路徑轉成絕對路徑。
- _get_4_1_13():為了取得 4.1.13 章結節檔案 (也就是複雜版的章節),所客制化的function。它的共通點是要往下追蹤四層連結,前面兩層是共通的,後面兩層則不同。
讀取 Excel XLS 檔案內容
前面一個部份是解說跟隨超連結網頁的程式,另一個重點部份則是讀取 Excel 檔案。讀取 Excel 檔案,有兩個比較通用的套件 xlrd 與 openpyxl。據不認真的查証, xlrd 處理 .xls 檔做的比較齊全,對 .xlsx 只有基本功能。而 openpyxl 只能讀 .xlsx,不支援 .xls,xlsx功能(應該)比較齊全。
這個案子的檔案則 .xls 與 .xlsx 檔都有,好在都只是基本的讀取。所以就一律用 xlrd 套件來處理。
直接以一個範例來說明如何使用這個套件。下面的例子會開啟一個名為 test.xls 的檔案,並把每一個sheet的名稱和 A1 欄位印出來。
1 2 3 4 5 6 7 8 |
def process_file_test(): wb = xlrd.open_workbook("test.xls") # 開啟檔案 wblen=len(wb.sheet_names()) #取得 sheets的個數 for i in range(0, wblen): # 針對每個 sheet 做處理 name = tabname_proc(wb.sheet_names()[i]) 取得 sheet 的名字 print "Sheet ["+name+"]: Cell(0,0) = ", sheet = wb.sheet_by_index(i) # 取得 sheet 的 object variable print sheet.cell(0,0).value # 取得 sheet 的 cell (0,0) |
上面的例子會印出下面的結果
1 2 3 |
Sheet [工作表1]: Cell(0,0) = a Sheet [工作表2]: Cell(0,0) = b Sheet [工作表3]: Cell(0,0) = c |
程式碼
雖然案子沒接到,也沒用到業主任何的程式碼。但為免對其造成困擾,可能過三個月後把程式稍做精簡後在放上來。