2024年6月26日 星期三

Python 學習筆記 : 網頁爬蟲實戰 (十四) 104 人力銀行的分頁搜尋結果

爬完富時中國 A50 網頁後快馬加鞭來爬 104 人力銀行的分頁搜尋結果, 此網站特點是分頁內容會隨著滑鼠滾輪向下捲動而持續載入, 模擬此動作需要用到 Selenium 來執行 Javascript. 此範例來自下面這本書 : 

本系列之前的筆記參考 : 



一. 檢視目標網頁 :  

104 人力銀行首頁網址如下 : 


首先用 Quick Javascript Switcher 檢查發現這個網站是 Javascript 動態生成的, 必須用 Selenium 來爬. 但與之前遇到的網站不同之處有二 :
  • 一是此網站是使用 vue.js 前端技術寫的, 連輸入表單都是 Javascript 生成, 必須費一番手腳才能順利使用 Selenium 進行操控. 
  • 其二是當使用者搜尋關鍵字時不會一次列出全部結果, 而是以分頁的方式顯示部分結果, 當使用者向下滑動滾輪或拖曳卷軸時才動態載入下一頁的搜尋結果. 
我們這次的任務很簡單, 就是搜尋 Python 相關工作後抓出前五頁工作機會的資訊. 

在首頁左上方輸入框輸入 "Python" 後按右方 "搜尋" 鈕會出現一列列與 Python 相關的工作資訊, 當滑鼠往下拉時資料筆數就會不斷增加, 右方卷軸讓使用者感覺似乎沒有底部 (其實是有的), 這種網站採用的是無限捲動的分頁結構. 

從下圖的搜尋結果可知, 搜尋關於 Python 的工作機會總共有 150 頁, 總頁數是放在一個關聯式陣列 initFilter 中的 totalPage 屬性裡  :

var initFilter = {"query":{"ro":0,"jobcat":"","isnew":"","kwop":7,"keyword":"Python",...
... (略) ...
"pageNo":1,"totalPage":150,"totalCount":7100,"personalBoost":0,"firstCustNo":""};




使用 Quick Javascript Switcher 檢查發現, 當 Javascript 功能關閉時, 卷軸就不再有無限捲動功能, 說明這確實是一個利用 Javascript 動態產生內容的網頁.

檢視搜尋關鍵字 "Python" 後的網頁原始碼會發現資料內容是放在一個型態為 JSON 的 Javascript 程式碼裡面, 它是透過 Javascript 繪製成網頁內容的 (網頁原始碼其實在此任務中用不到) :

<script type="application/ld+json">[ { "@context": "https:\/\/schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "104 人力銀行", "item": "https:\/\/www.104.com.tw" }, { "@type": "ListItem", "position": 2, "name": "找工作", "item": "https:\/\/www.104.com.tw\/jobs\/search" }, { "@type": "ListItem", "position": 3, "name": "「Python」找工作職缺", "item": "https:\/\/www.104.com.tw\/jobs\/search\/?keyword=Python" } ] }, { "@context": "https:\/\/schema.org", "@type": "WebSite", "url": "\/\/www.104.com.tw", "name": "104 人力銀行", "potentialAction": { "@type": "SearchAction", "target": "https:\/\/www.104.com.tw\/jobs\/search\/?keyword={searchTerms}&jobsource=open", "query-input": "required name=searchTerms" } }, { "@context": "https:\/\/schema.org", "@type": "Event", "startDate": "2024\/06\/23", "eventAttendanceMode": "https:\/\/schema.org\/MixedEventAttendanceMode", "eventStatus": "EventScheduled", "name": "Python Data Scientist", "organizer": { "@type": "Organization", "name": "BigGo_樂方股份有限公司", "url": "https:\/\/www.104.com.tw\/company\/1a2x6bkf3h" }, "location": { "@type": "Place", "name": "高雄市鼓山區", "address": { "@type": "PostalAddress", "postalCode": 804 } }, "performer": { "@type": "Organization", "url": "https:\/\/www.104.com.tw\/company\/1a2x6bkf3h", "name": "BigGo_樂方股份有限公司" }, "description": "BigGo - [[[Python]]] 資料科學家\n\nBigGo是全球超過十二個國家上線的跨境的商品搜尋引擎,是目前台灣, 南美跟東南亞最大的比價服務網站。\n我們正在為全球市場招募[[[Python]]] 資料科學家\n\n我們擁有人類史上數量級最龐大的全球商品資料庫", "image": "https:\/\/static.104.com.tw\/b_profile\/cust_picture\/3773\/130000000113773\/custintroduce\/image1.jpg?v=20201003032418", "url": "https:\/\/www.104.com.tw\/job\/7op6q?jobsource=google_event", "offers": { "@type": "Offer", "url": "https:\/\/www.104.com.tw\/company\/1a2x6bkf3h", "priceCurrency": "TWD", "description": "免費", "price": "0", "availability": "http:\/\/schema.org\/InStock", "validFrom": "2024\/06\/23" } }, { "@context": "https:\/\/schema.org", "@type": "Event", "startDate": "2024\/06\/23", "eventAttendanceMode": "https:\/\/schema.org\/MixedEventAttendanceMode", "eventStatus": "EventScheduled", "name": "Python工程師", "organizer": { "@type": "Organization", "name": "云智資訊股份有限公司", "url": "https:\/\/www.104.com.tw\/company\/1a2x6bk61j" }, "location": { "@type": "Place", "name": "台北市松山區", "address": { "@type": "PostalAddress", "postalCode": 105 } }, "performer": { "@type": "Organization", "url": "https:\/\/www.104.com.tw\/company\/1a2x6bk61j", "name": "云智資訊股份有限公司" }, "description": " Vmware 有使用經驗。\n\n加分條件:\n1. 具備撰寫自動化腳本的經驗,例如:PowerShell、[[[Python]]] 或 Ansible。 \n2. 具備監控服務工具的使用經驗,例如:ELK、Grafana 或 Zabbix , Prometheus", "image": "https:\/\/static.104.com.tw\/b_profile\/cust_picture\/2039\/130000000102039\/logo.gif?v=20240413194234", "url": "https:\/\/www.104.com.tw\/job\/80bs8?jobsource=google_event", "offers": { "@type": "Offer", "url": "https:\/\/www.104.com.tw\/company\/1a2x6bk61j", "priceCurrency": "TWD", "description": "免費", "price": "0", "availability": "http:\/\/schema.org\/InStock", "validFrom": "2024\/06\/23" } }, { "@context": "https:\/\/schema.org", "@type": "Event", "startDate": "2024\/06\/23", "eventAttendanceMode": "https:\/\/schema.org\/MixedEventAttendanceMode", "eventStatus": "EventScheduled", "name": "Python軟體開發工程師", "organizer": { "@type": "Organization", "name": "先傑電腦股份有限公司", "url": "https:\/\/www.104.com.tw\/company\/ujsf2ow" }, "location": { "@type": "Place", "name": "嘉義縣中埔鄉", "address": { "@type": "PostalAddress", "postalCode": 606 } }, "performer": { "@type": "Organization", "url": "https:\/\/www.104.com.tw\/company\/ujsf2ow", "name": "先傑電腦股份有限公司" }, "description": "薪水:大學剛畢業3萬元起+個人資歷+個人化多項津貼\n其他專業及研發人員面議\n熟悉 [[[Python]]]、PostgreSQL 和 ERP作業流程 \n1.使用 [[[Python]]] 語言開發系統經驗(基於ODOO平台開發),有一年以上的經驗更佳\n2.熟悉", "image": "https:\/\/static.104.com.tw\/b_profile\/cust_picture\/0000\/66500060000\/logo.gif?v=20210709151827", "url": "https:\/\/www.104.com.tw\/job\/5duf4?jobsource=google_event", "offers": { "@type": "Offer", "url": "https:\/\/www.104.com.tw\/company\/ujsf2ow", "priceCurrency": "TWD", "description": "免費", "price": "0", "availability": "http:\/\/schema.org\/InStock", "validFrom": "2024\/06\/23" } } ] </script>

但是其顯示長度是透過偵測滑鼠捲動而動態分頁載入, 因此如果要擷取這些資料必須使用 Selenium 來控制瀏覽器, 模擬滑鼠捲動行為來連續載入不同分頁, 這需要用到 WebDriver 物件的 execute_script() 方法來執行 Javascript 的 window 物件之 scrollTo() 方法. 

接下來使用開發者工具觀察執行 Javascript 後所渲染出來的網頁碼來找出網頁元素 (例如表單控制項). 在 Chrome 按 F12 開啟開發者工具, 然後重新整理 104 官網, 切到 Element 頁嵌搜尋輸入框的預設文字開頭 "關鍵字" 可找到上方的搜尋功能是放在一個 id=js-search-form 的表單內 :

<form class="l-container main-search__form" id="js-search-form" autocomplete="off"> <div class="b-fake-input"> <div class="b-search-block--l"> <input type="text" placeholder="關鍵字(例:職稱、公司名、技能專長...)" id="keyword" data-temp="" maxlength="50"> <svg class="icon-clear b-icon--weak-gray b-icon--w16" xmlns="http://www.w3.org/2000/svg"> <use xlink:href="//www.104.com.tw/jobs/search/static/img/sprite.svg#icon-clear"> </use> </svg> </div> <span class="b-divide"></span> <div class="b-search-block--m"> <input type="text" readonly placeholder="地區" id="area-cat" data-temp=""> <svg class="b-icon--weak-gray b-icon--w16"> <use xlink:href="//www.104.com.tw/jobs/search/static/img/sprite.svg#icon-arrow-down"> </use> </svg> </div> <span class="b-divide"></span> <div class="b-search-block--m"> <input type="text" readonly placeholder="職務類別" id="job-cat" data-temp=""> <svg class="b-icon--weak-gray b-icon--w16"> <use xlink:href="//www.104.com.tw/jobs/search/static/img/sprite.svg#icon-arrow-down"> </use> </svg> </div> </div> <button class="b-btn b-btn--primary is-large gtm-main-search" type="submit"> 搜尋 </button> <div id="search-relative" class="main-search__auxiliary"> <label> <input type="checkbox" id="only-title" class="b-checkbox"> 只搜尋職務名稱 </label> <p class="js-related-keyword js-related-keyword--related related-keyword"> 相關搜尋: <span> <a href="//www.104.com.tw/jobs/search/?ro=0&amp;kwop=7&amp;expansionType=area%2Cspec%2Ccom%2Cjob%2Cwf%2Cwktm&amp;order=15&amp;asc=0&amp;page=3&amp;mode=s&amp;langFlag=0&amp;langStatus=0&amp;recommendJob=1&amp;hotJob=1&amp;remoteWork=&amp;irsTag=&amp;label=&keyword=%E8%BB%9F%E9%AB%94%E5%B7%A5%E7%A8%8B%E5%B8%AB&jobsource=keyword2Keyword" class="b-link--gray"> 軟體工程師 </a> </span> <span> <a href="//www.104.com.tw/jobs/search/?ro=0&amp;kwop=7&amp;expansionType=area%2Cspec%2Ccom%2Cjob%2Cwf%2Cwktm&amp;order=15&amp;asc=0&amp;page=3&amp;mode=s&amp;langFlag=0&amp;langStatus=0&amp;recommendJob=1&amp;hotJob=1&amp;remoteWork=&amp;irsTag=&amp;label=&keyword=C%2B%2B&jobsource=keyword2Keyword" class="b-link--gray"> C++ </a> </span> <span> <a href="//www.104.com.tw/jobs/search/?ro=0&amp;kwop=7&amp;expansionType=area%2Cspec%2Ccom%2Cjob%2Cwf%2Cwktm&amp;order=15&amp;asc=0&amp;page=3&amp;mode=s&amp;langFlag=0&amp;langStatus=0&amp;recommendJob=1&amp;hotJob=1&amp;remoteWork=&amp;irsTag=&amp;label=&keyword=Linux&jobsource=keyword2Keyword" class="b-link--gray"> Linux </a> </span> <span> <a href="//www.104.com.tw/jobs/search/?ro=0&amp;kwop=7&amp;expansionType=area%2Cspec%2Ccom%2Cjob%2Cwf%2Cwktm&amp;order=15&amp;asc=0&amp;page=3&amp;mode=s&amp;langFlag=0&amp;langStatus=0&amp;recommendJob=1&amp;hotJob=1&amp;remoteWork=&amp;irsTag=&amp;label=&keyword=%E8%B3%87%E8%A8%8A%E5%B7%A5%E7%A8%8B&jobsource=keyword2Keyword" class="b-link--gray"> 資訊工程 </a> </span> <span> <a href="//www.104.com.tw/jobs/search/?ro=0&amp;kwop=7&amp;expansionType=area%2Cspec%2Ccom%2Cjob%2Cwf%2Cwktm&amp;order=15&amp;asc=0&amp;page=3&amp;mode=s&amp;langFlag=0&amp;langStatus=0&amp;recommendJob=1&amp;hotJob=1&amp;remoteWork=&amp;irsTag=&amp;label=&keyword=Java&jobsource=keyword2Keyword" class="b-link--gray"> Java </a> </span> </p> <label> <input id="job-type" type="checkbox" class="b-checkbox gtm-highend-switch">切換高階職類 </label> </div> </form>

輸入框的 id 為 keyword, 但搜尋按鈕卻沒有足以定位它的 id, name 或單一的 class, 因此只好去 Element 頁籤搜尋 "搜尋" 找到此按鈕, 然後按滑鼠右鍵選取 "Copy/Copy XPATH" 取得其 XPATH 為 '//*[@id="js-search-form"]/button' : 




照之前的爬蟲經驗, 有了這些資訊應該就能用 Selenium 來操控瀏覽器了. 但其實不然, 因為上面透過開發者工具找出的表單控制項在 Selenium 開啟的瀏覽器中用 find_element() 或 find_elements() 尋找根本就不存在, 這很奇怪, 我也還不清楚原因是甚麼, 因為我對 Vue.js 生成網頁碼的方式並不了解, 我最後是利用 WebDriver 物件的 page_source 屬性內容才找到 Vue.js 實際渲染出來的網頁元素 (如下所示). 


二. 用 Selenium 擷取目標網頁 :  

首先載入 Selenium : 

>>> from selenium import webdriver   
>>> driver=webdriver.Firefox()   
>>> driver.implicitly_wait(20)   
>>> url='https://www.104.com.tw/'     
>>> driver.get(url)   

然後在 WebDriver 物件中尋找上面利用開發者工具的 Element 頁籤找到的網頁表單 id=js-search-form 以及其內 id=keyword 的輸入框 :

>>> search_form=driver.find_element('id', 'js-search-form')   
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="js-search-form"];
>>> keyword=driver.find_element('id', 'keyword')    
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="keyword"];

以前用 Selenium 爬過的網站沒遇到過這種情形, 用開發者工具搜尋 Element 找到的渲染後元素基本上都可以用 find_element() 方法找到才對, 但這個用 Vue.js 生成的網頁卻找不到. 於是我將 WebDriver 物件的 page_source 屬性值寫到 htm 檔來觀察 : 

>>> with open('104.htm', 'w', encoding='utf-8') as f:    
    f.write(driver.page_source)   
    
130693

用記事本開啟 104.htm 搜尋 "關鍵字" 找到如下完全不同於上面在 Element 頁籤中所看到的網頁碼, 其中所有元素的屬性名稱都是自訂的 (應該是 Vue.js 產生的), 不僅 form 元素沒有 id=js-search-form 屬性值, 關鍵字輸入框的 input 元素也沒有 id=keyword 屬性值, 難怪用 find_element() 會找不到 :





將這個表單用 "HTML Formatter" 整理後得到下面可讀性較高的格式化後之 HTML 碼 :

<form data-v-34b0b8e5="" class=""> <div data-v-34b0b8e5="" class=""> <div data-v-34b0b8e5="" class="row"> <div data-v-34b0b8e5="" class="col col-10"> <div data-v-34b0b8e5="" class="input-group input-group--search"> <i data-v-34b0b8e5="" class="jb_icon_delete input-group__input-clear d-none"></i><input data-v-34b0b8e5="" data-gtm-index="搜尋欄位-搜尋點擊" type="text" class="form-control" placeholder="關鍵字(例:工作職稱、公司名、技能專長...)" maxlength="50"> <div data-v-34b0b8e5="" class="input-group-append"><button data-v-34b0b8e5="" data-gtm-index="搜尋欄位-搜尋點擊" class="btn btn-sm btn-block d-flex justify-content-between align-items-center" type="button"><span data-v-34b0b8e5="" class="h3">地區</span><i data-v-34b0b8e5="" class="jb_icon_down"></i></button></div> <div data-v-34b0b8e5="" class="input-group-append"><button data-v-34b0b8e5="" data-gtm-index="搜尋欄位-搜尋點擊" class="btn btn-sm btn-block d-flex justify-content-between align-items-center" type="button"><span data-v-34b0b8e5="" class="h3">職務類別</span><i data-v-34b0b8e5="" class="jb_icon_down"></i></button></div> <div data-v-34b0b8e5="" class="nav__search nav__search--recent overflow-hidden" style="display: none;"> <ul data-v-34b0b8e5="" class="list-group"> <li data-v-34b0b8e5="" class="list-group-item"> <adsmart-ui-switch data-v-34b0b8e5="" ads-shape="pc" ads-board-identify="pc_c_index_sponsor_company" class="d-flex align-items-center adsmart-item pl-3 adsmart-ui--mounted"></adsmart-ui-switch> </li> </ul> </div> <div data-v-34b0b8e5="" class="nav__search nav__search--auto-complete overflow-hidden" style="display: none;"> <ul data-v-34b0b8e5="" class="list-group" style="display: none;"></ul> <ul data-v-34b0b8e5="" class="list-group" style="display: none;"> <li data-v-34b0b8e5="" class="list-group-item list-group-item--addon t4"> 公司 </li> </ul> <ul data-v-34b0b8e5="" class="list-group d-none mt-2"> <li data-v-34b0b8e5="" class="list-group-item row no-gutters"> <adsmart-ui-switch data-v-34b0b8e5="" ads-shape="pc" ads-board-identify="pc_c_index_keywords_job" ads-keyword-input="" class="d-flex align-items-center adsmart-item pl-3"></adsmart-ui-switch> </li> </ul> <ul data-v-34b0b8e5="" class="list-group"> <li data-v-34b0b8e5="" class="list-group-item list-group-item--addon t4"> 公司 </li> <li data-v-34b0b8e5="" class="list-group-item row no-gutters"><a data-v-34b0b8e5="" class="col jb-link jb-link-blue t4" href="//www.104.com.tw/company/search/?jobsource=index_s_ac"> 更多 <b data-v-34b0b8e5=""></b> 相關公司 </a></li> </ul> </div> </div> </div> <div data-v-34b0b8e5="" class="col col-2"><button data-v-34b0b8e5="" data-gtm-index="搜尋欄位-搜尋點擊" class="btn btn-secondary btn-block btn-lg" type="submit"> 搜尋 </button></div> </div> </div> </form>

觀察此 HTML 碼可知 "關鍵字" 輸入框有一個樣式類別名稱 "form-control", 且搜尋整個渲染後的網頁只有這個元素使用此樣式類別, 因此可以直接用 find)element() 找到此輸入框物件, 然後呼叫其 send_keys() 方法傳入 "Python" :

>>> keyword=driver.find_element('class name', 'form-control')    
>>> keyword.send_keys('Python') 

接下來只要模擬按下搜尋鈕的動作即可, 但此 submit 按鈕卻沒有 id, name 或足以單獨識別它的 class 屬性, 因此嘗試用搜尋所有標籤名為 button 的元素, 然後逐一利用 text 屬性與 get_attribute() 方法找出其索引 : 

>>> buttons=driver.find_elements('tag name', 'button')    # 搜尋全部按鈕元素物件

檢查索引 0 與 1 都不是 "搜尋" 鈕 :

>>> buttons[0].text     
'地區'
>>> buttons[1].text    
'職務類別'
>>> buttons[2].text      # 是 "搜尋" 鈕
'搜尋'

檢查 type 與 data-gtm-index 屬性值 : 

>>> buttons[2].get_attribute('type')    
'submit'
>>> buttons[2].get_attribute('data-gtm-index')    
'搜尋欄位-搜尋點擊'

這樣即確認 "搜尋" 鈕索引為 2 無誤, 呼叫其 click() 方法進行搜尋 :

>>> buttons[2].click()    

這時 Selenium 開啟的 Firefox 瀏覽器就會列出搜尋結果, 從上方的下拉式選單可知總共有 150 頁, 現在顯示的是第一頁 : 




檢視開發人員工具的 Element 頁籤可知這些工作機會項目是放在一個 id="js-job-content" 的 div 元素內, 每個項目就是一個 article 元素 : 




本爬蟲的目標是抓出上圖紅框中的資訊, 亦即 article 的 data-job-name 與 data-cust-name 的屬性值, 還有裡面指向此項目詳細說明頁面的超連結. 

上圖中我們在 Element 頁籤內搜尋 "<article" 發現共有 24 個 article 元素, 用 find_elements() 方法去找也是 24 個, 這表示每頁會顯示 24 個項目 : 

>>> articles=driver.find_elements('tag name', 'article')   
>>> len(articles)   
24

這樣就可以取得各個項目的目標資料了, 第一個項目如下 :

>>> articles[0].get_attribute('data-job-name')    
'[暑期實習]量化交易策略研發實習生(※請至官網下載申請表並投遞至專屬信箱※)'
>>> articles[0].get_attribute('data-cust-name')      
'統一綜合證券股份有限公司'
>>> link=articles[0].find_element('tag name', 'a')   
>>> link.get_attribute('href')     
'https://www.104.com.tw/job/8daij?jobsource=hotjob_chr'   

下面是第二個項目 : 

>>> articles[1].get_attribute('data-job-name')    
'【招聘赴日東京】軟件開發工程師(待遇優厚,辦理簽證)'
>>> articles[1].get_attribute('data-cust-name')   
'株式会社ブライトスター'
>>> link=articles[1].find_element('tag name', 'a')    
>>> link.get_attribute('href')    
'https://www.104.com.tw/job/8d8yl?jobsource=hotjob_chr'

此頁最後一個項目 :

>>> articles[23].get_attribute('data-cust-name')   
'磐弈有限公司'   
>>> link=articles[23].find_element('tag name', 'a')     
>>> link.get_attribute('href')    
'https://www.104.com.tw/job/8bek0?jobsource=hotjob_chr'   

當滑鼠滾輪往下移動時會不斷載入後續頁的項目 (往下疊, 前面的不會消失), 這相當於是執行 Javascript 的 window.scrollTo() 方法將滑鼠滾輪移動整個視窗高度的效果, 可以用 WebDriver 的 execute_script() 方法來執行 Javascript 移動滑鼠的程式碼 :

>>> js='window.scrollTo(0, document.body.scrollHeight)'    # 移動滑鼠滾輪視窗高度
>>> driver.execute_script(js)       

這時再去搜尋 article 元素會發現它大約增加一倍 : 

>>> articles=driver.find_elements('tag name', 'article')     
>>> len(articles)      
46

預期是 48 個, 但只有 46 個, 且其中有兩個 article 內容是空的, 檢查前面第一頁的資料還是一樣 : 

>>> articles[0].get_attribute('data-job-name')     
'[暑期實習]量化交易策略研發實習生(※請至官網下載申請表並投遞至專屬信箱※)'
>>> articles[1].get_attribute('data-job-name')    
'【招聘赴日東京】軟件開發工程師(待遇優厚,辦理簽證)'
>>> articles[23].get_attribute('data-job-name')    
'助理工程師'

下面是呼叫 execute_script() 執行模擬移動滑鼠滾輪後載入的第二頁內容, 但最後兩個 article (索引 44, 45) 內容卻是空的 :

>>> articles[24].get_attribute('data-job-name')   
'(Sr.) Automation Test Engineer(Python) - Tainan/Hybrid'
>>> articles[25].get_attribute('data-job-name')     
'Python  軟體工程師 (台北市 內湖)'
... (略) ...
>>> articles[42].get_attribute('data-job-name')    
'Python 軟體工程師'
>>> articles[43].get_attribute('data-job-name')      
'Python 軟體工程師'
>>> articles[44].get_attribute('data-job-name')       # 傳回 None
>>> articles[45].get_attribute('data-job-name')       # 傳回 None

現在已載入兩頁資料, 所以我們只要再呼叫三次 execute_script() 就可以載入 5 頁的資料了 : 

>>> for i in range(3):    
  js='window.scrollTo(0, document.body.scrollHeight)'   
  driver.execute_script(js)    
  
重新搜尋 article 元素物件 : 

>>> articles=driver.find_elements('tag name', 'article')     
>>> len(articles)  
68

預期 5 頁應該有 100 多筆, 卻只有 68 筆. 這可能是迴圈跑太快, 執行 Javascript 滑動滑鼠滾輪需要亦點時間之故, 因此可能需要在迴圈底用 time.sleep(0.5) 休息個半秒鐘. 檢視前面幾頁資料都沒變 , 可見滑動滑鼠滾輪是載入更多分頁往下疊, 不是覆蓋 : 

>>> articles[0].get_attribute('data-job-name')        
'[暑期實習]量化交易策略研發實習生(※請至官網下載申請表並投遞至專屬信箱※)'
>>> articles[1].get_attribute('data-job-name')   
'【招聘赴日東京】軟件開發工程師(待遇優厚,辦理簽證)'
>>> articles[23].get_attribute('data-job-name')     
'助理工程師'
>>> articles[25].get_attribute('data-job-name')      
'Python  軟體工程師 (台北市 內湖)'
>>> articles[43].get_attribute('data-job-name')      
'Python 軟體工程師'
>>> articles[65].get_attribute('data-job-name')       
'AJ6-後端/全端軟體開發工程師(Python)'

最後兩筆是空的 : 

>>> articles[66].get_attribute('data-job-name')       # 傳回 None 
>>> articles[67].get_attribute('data-job-name')       # 傳回 None 

如果要抓出搜尋結果的總頁數, 可以從網頁上方 class 屬性值為 "gtm-paging-top" 的下拉式選單的取得 (具有此屬性值者只有一個) : 




 先用 class='gtm-paging-top' 搜尋 select 元素之物件, 然後再搜尋其下的第一個 option 物件 :

>>> page_select=driver.find_element('class name', 'gtm-paging-top')    
>>> page_option=page_select.find_element('tag name', 'option')    
>>> page_option.text    
'第 1 / 150 頁'

這樣便取得包含總頁數之字串了, 接著將 text 屬性值以 '/' 為界拆分取第二個索引, 然後用正規式將其中的數字 150 抓出來 :

>>> pages_str=page_option.text.split('/')[1]     
>>> pages_str   
' 150 頁'
>>> pages=re.findall(r'\d+', pages_str)[0]   
>>> pages   
'150'

如果要載入全部分頁的資料, 可以用 pages 當迴圈終點 :

>>> import time   
>>> for i in range((int(pages)):       
  js='window.scrollTo(0, document.body.scrollHeight)'       
  driver.execute_script(js)     
  time.sleep(0.5)     

以上測試之完整程式碼如下 :

from selenium import webdriver
import re
import time

driver=webdriver.Firefox()   
driver.implicitly_wait(20)   
url='https://www.104.com.tw/'     
driver.get(url)
keyword=driver.find_element('class name', 'form-control')
keyword.send_keys('Python')
buttons=driver.find_elements('tag name', 'button')
buttons[2].click()
for i in range(5):    
  js='window.scrollTo(0, document.body.scrollHeight)'   
  driver.execute_script(js)
  time.sleep(0.5)
articles=driver.find_elements('tag name', 'article')
print(len(articles))
for i in range(len(articles)):
    job_name=articles[i].get_attribute('data-job-name')
    cust_name=articles[i].get_attribute('data-cust-name')
    if job_name:
        print(f'工作名稱: {job_name}')
        print(f'徵求公司: {cust_name}')
        link=articles[i].find_element('tag name', 'a')
        url=link.get_attribute('href')
        print(f'詳細資訊: {url}\n')
    else:
        print(f'索引 {i} 無資料\n')
driver.close()

執行結果 :

>>> %Run 104_job_search_1.py   
132
工作名稱: [暑期實習]量化交易策略研發實習生(※請至官網下載申請表並投遞至專屬信箱※)
徵求公司: 統一綜合證券股份有限公司
詳細資訊: https://www.104.com.tw/job/8daij?jobsource=hotjob_chr

工作名稱: 廣告行銷數據分析師 Marketing Data Analyst
徵求公司: 雅德思行銷顧問有限公司
詳細資訊: https://www.104.com.tw/job/8d6bk?jobsource=hotjob_chr

工作名稱: Python Data Scientist
徵求公司: BigGo_樂方股份有限公司
詳細資訊: https://www.104.com.tw/job/7op6q?jobsource=index_s

工作名稱: Python工程師
徵求公司: 創昇資訊有限公司
詳細資訊: https://www.104.com.tw/job/82dtp?jobsource=index_s

工作名稱: Python軟體工程師
徵求公司: 奇力速工業股份有限公司
詳細資訊: https://www.104.com.tw/job/6vayl?jobsource=index_s

工作名稱: Python工程師
徵求公司: 云智資訊股份有限公司
詳細資訊: https://www.104.com.tw/job/80bs8?jobsource=index_s

工作名稱: Python軟體開發工程師
徵求公司: 先傑電腦股份有限公司
詳細資訊: https://www.104.com.tw/job/5duf4?jobsource=index_s
... (略) ...
工作名稱: 機器學習工程師 AI / ML Engineer (高雄)
徵求公司: 資旅軟體開發有限公司
詳細資訊: https://www.104.com.tw/job/7jaus?jobsource=index_s

工作名稱: 【雲端系統部】Python工程師  (中和)
徵求公司: 昇銳電子股份有限公司
詳細資訊: https://www.104.com.tw/job/6zloe?jobsource=index_s

工作名稱: T-【2024軟體技術人才招募】Python 工程師_人才招募
徵求公司: 緯創軟體股份有限公司
詳細資訊: https://www.104.com.tw/job/69za2?jobsource=index_s

工作名稱: Python 程式設計語言講師 - 基隆、大台北地區
徵求公司: 聯成電腦有限公司(聯成電腦/聯成外語)
詳細資訊: https://www.104.com.tw/job/5pxb7?jobsource=index_s

索引 130 無資料

索引 131 無資料

如果要取得全部搜尋到的資料, 就把迴圈的終點設為從下拉式選單中取得的總頁數 :

from selenium import webdriver
import re
import time

driver=webdriver.Firefox()   
driver.implicitly_wait(20)   
url='https://www.104.com.tw/'     
driver.get(url)
keyword=driver.find_element('class name', 'form-control')
keyword.send_keys('Python')
buttons=driver.find_elements('tag name', 'button')
buttons[2].click()
page_select=driver.find_element('class name', 'gtm-paging-top')
page_option=page_select.find_element('tag name', 'option')
pages_str=page_option.text.split('/')[1]
pages=int(re.findall(r'\d+', pages_str)[0])
print(f'總頁數: {pages}')
for i in range(pages):    
  js='window.scrollTo(0, document.body.scrollHeight)'   
  driver.execute_script(js)
  time.sleep(0.5)
articles=driver.find_elements('tag name', 'article')
print(f'總筆數: {len(articles)}')
for i in range(len(articles)):
    job_name=articles[i].get_attribute('data-job-name')
    cust_name=articles[i].get_attribute('data-cust-name')
    if job_name:
        print(f'{i}. {job_name}')
        print(f'❖ {cust_name}')
        link=articles[i].find_element('tag name', 'a')
        url=link.get_attribute('href')
        print(f'▶ {url}\n')
    else:
        print(f'索引 {i} 無資料\n')
driver.close()

但是不知何因 (我猜是瀏覽器記憶體限制), 模擬滑動滑鼠滾輪的動作到第 15 頁時都會停住, 所以只載入 311 筆資料 :




>>> %Run 104_job_search_2.py   
總頁數: 150
總筆數: 311

... (略) ...
306. PHP、Perl 程式設計工程師(新竹)
❖ 桓基科技股份有限公司
▶ https://www.104.com.tw/job/4o46l?jobsource=index_s

307. Business Insight Analyst - 數據分析專員
❖ 美商泰優股份有限公司台灣分公司
▶ https://www.104.com.tw/job/6a28o?jobsource=index_s

308. 軟體工程師/系統維護工程師
❖ 偉福科技有限公司
▶ https://www.104.com.tw/job/8acdh?jobsource=index_s

309. 後端工程師
❖ 神瑞人工智慧股份有限公司
▶ https://www.104.com.tw/job/827of?jobsource=index_s

索引 310 無資料

索引 311 無資料

參考 : 


沒有留言 :