什麼是網站爬蟲 網站爬蟲可以將爬取的頁面儲存,透過網站爬蟲,開發者可以蒐集網路更多的資源供後續使用。
舉一個大家都聽過的應用,Google 搜尋引擎背後其實也是透過爬蟲的技術來將網站資料存下來進行索引來提供用戶搜尋。
由於爬蟲存取網站的過程還是會消耗站台系統資源,所以身為爬蟲開發者要遵守的價值觀有兩點如下:
不要打爆對方 遵守 robots.txt 中定義規則,這些規則會標註禁止或開放存取哪些路徑 Python 網站爬蟲工具 Python 的爬蟲工具常見有以下兩種:
selenium: 萬用門檻低 requests: 效率好、較不易受 JavaScript 影響,因為不會抓照片、UI 變化等等,只會抓整個 html 的文本 常見的網站爬蟲情境如下:
一步可爬: 資料就放在頁面中的表格 查詢後爬: 需要透過搜尋篩選框 先登入後查詢才能爬先用 selenium 登入再用 requests 打包資料 驗證碼破解Tesseract Tesseract + keras 網站反爬蟲 網站要反爬蟲就要製造障礙,去想說爬蟲怎麼爬會難爬取資料,一般的反爬蟲如下:
登入後才能查詢,擋 header 或是 cookie CSRFPreventionSalt 改成一次性 驗證碼 (選圖片、加減乘除) 隨機跳 pop window 或是 alert Table 變成照片或 PDF 會更難爬 xPath 爬蟲,xPath 中新增 DIV,不影響使用者體驗下還能反爬蟲 流量要先壓力測試,不然被爬幾下就壞了也很糟糕:
從 GA、Log 去看流量,然後直接擋掉 locust 套件 Python Selenium 爬蟲實作 由於小編的電腦是從大學用到現在已經有點年老,所以這次直接使用 Google 的 Colab 免費使用 GPU 的運算資源,Colab 的使用方法跟 Jupyter notebook 一樣,可以直接執行 Python 的程式碼。
來示範一步可爬的網站,以玉山銀行的網站為例:
Colab 需要先安裝才能夠使用 selenium 1 2 3 4 5 6 7 !pip install selenium !apt-get update !apt install chromium-chromedriver !cp /usr/lib/chromium-browser/chromedriver /usr/bin import syssys.path.insert(0 ,'/usr/lib/chromium-browser/chromedriver' )
引入資料處理常見的 pandas、還有本次爬蟲主角 selenium 1 2 import pandas as pdfrom selenium import webdriver
透過 webdriver 指定瀏覽器為 chrome,並且設定相關參數,最後透過瀏覽器開啟網站 1 2 3 4 5 6 7 8 chrome_options = webdriver.ChromeOptions() chrome_options.add_argument('--headless' ) chrome_options.add_argument('--no-sandbox' ) chrome_options.add_argument('--disable-dev-shm-usage' ) browser = webdriver.Chrome('chromedriver' ,chrome_options=chrome_options) url = 'https://www.esunbank.com.tw/bank/about/announcement/announcement?i=eqQb451_o06vZeJpBZLLLQ&p=QEdQ8PAaO0GrIzIcAevp0A&d=hxK-VOJqWkGADb4tgPQH4Q' browser.get(url)
透過 Pandas 套件提供的 read_html()
輕鬆讀取網頁中的 <table>
表格,這裡就直接選取第一個 1 pd.read_html(browser.page_source)[0 ].head()
或著我們也可以透過 xpath 來進行指定 html 的範圍,然後也是一樣餵給 read_html()
1 2 3 4 element_xpath = '//*[@id="mainform"]/div[10]/div[2]/div[2]/table[1]' target_table = browser.find_element_by_xpath(element_xpath) html_string = target_table.get_attribute('outerHTML' ) pd.read_html(html_string)[0 ].head()
如果遇到彈跳視窗來阻擋,一樣可以透過 xpath 先找到,然後透過 JavaScript 把 Element 從 Dom 中移除。 1 2 3 4 5 6 7 element = browser.find_element_by_xpath('' ) browser.execute_script(""" var element = arguments[0]; element.parentNode.removeChild(element); """ , element);
Python Requests 爬蟲實作 requests 不同於 selenium,抓取下來的會是純文本,不包含相關圖片等靜態資源,所以對伺服器的負擔相對較小,接下來要示範先查詢後爬的網站,這邊會以 104 人力銀行網站為例,需要透過搜尋篩選框來篩選職缺訊息。
這個 API 明顯有幾個參數,所以接下來就會需要去整理相關資訊
indcat: 產業別 area: 地區 page: 頁數 引入資料處理常見的 pandas、還有本次爬蟲主角 requests 1 2 import requestsimport pandas as pd
網站有基本反爬蟲所以需要設定 Headers 來騙過伺服器,最後透過 requests 開始抓取資料 User-Agent (用戶端)資訊: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36’ Referer (從哪裏來): ‘https://www.104.com.tw/ ‘ 1 2 3 4 5 6 7 headers={'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36' , 'Referer' : 'https://www.104.com.tw/' } url= 'https://www.104.com.tw/jobs/search/list?ro=1&indcat=1003000000&area=6001001000&order=11&asc=0&page=&mode=l' resp = requests.get(url, headers=headers)
透過 Pandas 套件提供的 DataFrame
將資料存下來,先顯示個三筆看看 1 2 pd.DataFrame(resp.json()['data' ]['list' ]).head(3 )
當然一次爬個 10 頁也是沒問題 1 2 3 4 5 6 7 8 9 10 11 12 df = [] for page in range (1 ,10 ): url= f'https://www.104.com.tw/jobs/search/list?ro=1&indcat=1003000000&area=6001001000&order=11&asc=0&page={page} &mode=l' print (url) resp = requests.get(url, headers=headers) ndf = pd.DataFrame(resp.json()['data' ]['list' ]) df.append(ndf) if ndf.shape[0 ] < 30 : break df = pd.concat(df, ignore_index=True )
接著是整理地區跟產業別的篩選條件,舉地區資料如下,會透過 explode 把 array 中的 n 做展開,然後透過 apply 去整理資料,最後透過 loc 把剛剛展開的 n 拿掉生成新的 Dataframe explode 的說明可以參考連結 apply 的說明可以參考連結 loc 的說明可以參考連結 第 0 筆的 n 資料如下:
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 [ { "des" : "台北市" , "no" : "6001001000" , "n" : [ { "des" : "台北市中正區" , "no" : "6001001001" } , { "des" : "台北市大同區" , "no" : "6001001002" } ] } , { "des" : "新北市" , "no" : "6001002000" , "n" : [ { "des" : "新北市萬里區" , "no" : "6001002001" } , { "des" : "新北市金山區" , "no" : "6001002002" } ] } ]
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 url = 'https://static.104.com.tw/category-tool/json/Area.json' areacode = pd.DataFrame(requests.get(url).json()[0 ]['n' ]) areacode = areacode.explode('n' ) areacode['des2' ] = areacode['n' ].apply(lambda x: x['des' ]) areacode['no2' ] = areacode['n' ].apply(lambda x: x['no' ]) areacode = areacode.loc[:,['des' , 'no' , 'des2' , 'no2' ]] areacode url = 'https://static.104.com.tw/category-tool/json/Indust.json' Indust = pd.DataFrame(requests.get(url).json()) Indust = Indust.explode('n' ) Indust['des2' ] = Indust['n' ].apply(lambda x: x['des' ]) Indust['no2' ] = Indust['n' ].apply(lambda x: x['no' ]) Indust['n2' ] = Indust['n' ].apply(lambda x: x['n' ]) Indust = Indust.explode('n2' ) Indust['des3' ] = Indust['n2' ].apply(lambda x: x['des' ]) Indust['no3' ] = Indust['n2' ].apply(lambda x: x['no' ]) Indust = Indust.loc[:,['des' , 'no' , 'des2' , 'no2' , 'des3' , 'no3' ]] Indust
跑迴圈把所有的資料抓下來 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 df = [] for area in areacode['no2' ].unique(): for indcat in Indust['no2' ].unique(): page = 1 while page <= 2 : try : url = f'https://www.104.com.tw/jobs/search/list?ro=1&indcat={indcat} &area={area} &order=11&asc=0&page={page} &mode=l' print (url) resp = requests.get(url, headers=headers) ndf = pd.DataFrame(resp.json()['data' ]['list' ]) df.append(ndf) if ndf.shape[0 ] < 30 : break page = page + 1 except : print ('==================== Error and retry ====================' ) clear_output() df = pd.concat(df, ignore_index=True )
把檔案存起來,收工 1 2 3 4 df.to_excel('./data/job_abs.xlsx' ) df.to_csv('./data/job_abs.csv' ) df.to_pickle('./data/job_abs.pkl' )
為了避免爬太久,有把條件減少,Github 中完整的 Gist 如下:
喜歡這篇文章,請幫忙拍拍手喔 🤣