有些需求不是複製貼上就能解決的
1.1 瀏覽器關閉導致無法讀取網頁元件
1.2 跨網域(CORS)錯誤
2.1 分析程式結構邏輯
2.2 思考程式上有什麼地方需要改進
如果你直接把兩天文章複製貼上爬的話一定會看到這個錯誤訊息
WebDriverError: element not interactable
跑爬蟲會消耗電腦的記憶體以及網路流量
,我希望電腦跑爬蟲的時候不要影響我做其他事情的效率遇到問題時方便除錯
如果你把他改成一個瀏覽器執行,跑完 FB 粉專再跑 IG 粉專應該又會出現跨網域(CORS)問題的錯誤(目前只有 windows 作業系統才會遇到)
網域切換時因為一些安全性的疑慮而拒絕跳轉
,像是從 https://www.facebook.com/ 跳轉到 https://www.instagram.com 就會遇到這個問題{ acceptSslCerts: true, acceptInsecureCerts: true }
即可解決
let driver = new webdriver.Builder().forBrowser("chrome").withCapabilities(options,
{ acceptSslCerts: true, acceptInsecureCerts: true }//這是為了解決跨網域問題
).build();
為了幫助讀者能較快掌握程式的邏輯,我根據每個函式所做的功能以及執行步驟列出結構如下:
crawler
: 觸發爬蟲的函式
loginInstagramGetTrace
: 登入IG並取得指定帳號的追蹤人數
先執行 IG 爬蟲是因為有些人登入 IG 的方式是綁定 FB 帳號,這會導致我們程式需要判斷更多狀況;而讓IG先爬蟲就能避免這個問題嚕~
loginFacebookGetTrace
: 登入 FB 並取得粉絲專業的追蹤人數
寫程式如同拼積木,隨著功能的增加程式的體積會越來越大,這裡我建議你寫一份程式架構圖的文件來輔助,未來遇到需求變更時看架構圖就能迅速修改
require('dotenv').config(); //載入.env環境檔
const path = require('path');//用於處理文件路徑的小工具
const fs = require("fs");//讀取檔案用
//取出.env檔案的FB、IG資訊
const ig_username = process.env.IG_USERNAME
const ig_userpass = process.env.IG_PASSWORD
const fb_username = process.env.FB_USERNAME
const fb_userpass = process.env.FB_PASSWORD
const webdriver = require('selenium-webdriver'), // 加入虛擬網頁套件
By = webdriver.By,//你想要透過什麼方式來抓取元件,通常使用xpath、css
until = webdriver.until;//直到抓到元件才進入下一步(可設定等待時間)
const chrome = require('selenium-webdriver/chrome');
const options = new chrome.Options();
options.setUserPreferences({ 'profile.default_content_setting_values.notifications': 1 });//因為FB會有notifications干擾到爬蟲,所以要先把它關閉
function getCrawlerPath () {
if (process.env.FB_VERSION === 'new') {//如果是新版FB
return {
"fb_head_path": `//*[contains(@class,"fzdkajry")]`,
"fb_trace_path": `//*[contains(@class,"knvmm38d")]`
}
} else {//如果為設定皆默認為舊版
return {
"fb_head_path": `//*[contains(@class,"_1vp5")]`,
"fb_trace_path": `//*[@id="PagesProfileHomeSecondaryColumnPagelet"]//*[contains(@class,"_4bl9")]`
}
}
}
async function loginFacebookGetTrace (driver) {
const web = 'https://www.facebook.com/login';//我們要前往FB
await driver.get(web)//在這裡要用await確保打開完網頁後才能繼續動作
//填入fb登入資訊
const fb_email_ele = await driver.wait(until.elementLocated(By.xpath(`//*[@id="email"]`)));
fb_email_ele.sendKeys(fb_username)
const fb_pass_ele = await driver.wait(until.elementLocated(By.xpath(`//*[@id="pass"]`)));
fb_pass_ele.sendKeys(fb_userpass)
//抓到登入按鈕然後點擊
const login_elem = await driver.wait(until.elementLocated(By.xpath(`//*[@id="loginbutton"]`)))
login_elem.click()
// FB有經典版以及新版的區分,兩者的爬蟲路徑不同,我們藉由函式取得各自的路徑
const { fb_head_path, fb_trace_path } = getCrawlerPath();
//因為登入這件事情要等server回應,你直接跳轉粉絲專頁會導致登入失敗
//用登入後才有的元件,來判斷是否登入
await driver.wait(until.elementLocated(By.xpath(fb_head_path)))
//登入成功後要前往粉專頁面
const fanpage = "https://www.facebook.com/baobaonevertell/"
await driver.get(fanpage)
let fb_trace = 0;//這是紀錄FB追蹤人數
//因為考慮到登入之後每個粉專顯示追蹤人數的位置都不一樣,所以就採用全抓在分析
const fb_trace_eles = await driver.wait(until.elementsLocated(By.xpath(fb_trace_path)))
for (const fb_trace_ele of fb_trace_eles) {
const fb_text = await fb_trace_ele.getText()
if (fb_text.includes('人在追蹤')) {
fb_trace = fb_text
break
}
}
console.log(`FB追蹤人數:${fb_trace}`)
}
async function loginInstagramGetTrace (driver) {
const web = 'https://www.instagram.com/accounts/login';//前往IG登入頁面
await driver.get(web)//在這裡要用await確保打開完網頁後才能繼續動作
//填入ig登入資訊
let ig_username_ele = await driver.wait(until.elementLocated(By.css("input[name='username']")));
ig_username_ele.sendKeys(ig_username)
let ig_password_ele = await driver.wait(until.elementLocated(By.css("input[name='password']")));
ig_password_ele.sendKeys(ig_userpass)
//抓到登入按鈕然後點擊
const login_elem = await driver.wait(until.elementLocated(By.css("button[type='submit']")))
login_elem.click()
//登入後才會有右上角功能列,我們以這個來判斷是否登入
await driver.wait(until.elementLocated(By.xpath(`//*[@id="react-root"]//*[contains(@class,"_47KiJ")]`)))
//登入成功後要前往粉專頁面
const fanpage = "https://www.instagram.com/baobaonevertell/"
await driver.get(fanpage)
let ig_trace = 0;//這是紀錄IG追蹤人數
const ig_trace_xpath = `//*[@id="react-root"]/section/main/div/header/section/ul/li[2]/a/span`
const ig_trace_ele = await driver.wait(until.elementLocated(By.xpath(ig_trace_xpath)))
// ig因為當人數破萬時文字不會顯示,所以改抓title
ig_trace = await ig_trace_ele.getAttribute('title')
console.log(`IG追蹤人數:${ig_trace}`)
}
function checkDriver() {
try {
chrome.getDefaultService()//確認是否有預設
} catch {
console.warn('找不到預設driver!');
//'../chromedriver.exe'記得調整成自己的路徑
const file_path = '../chromedriver.exe'
//請確認印出來日誌中的位置是否與你路徑相同
console.log(path.join(__dirname, file_path));
//確認路徑下chromedriver.exe是否存在
if (fs.existsSync(path.join(__dirname, file_path))) {
//設定driver路徑
const service = new chrome.ServiceBuilder(path.join(__dirname, file_path)).build();
chrome.setDefaultService(service);
console.log('設定driver路徑');
} else {
console.error('無法設定driver路徑');
return false
}
}
return true
}
async function crawler () {
if (!checkDriver()) {// 檢查Driver是否是設定,如果無法設定就結束程式
return
}
// 建立這個browser的類型
let driver = await new webdriver.Builder().forBrowser("chrome").withCapabilities(options).build();
//考慮到ig在不同螢幕寬度時的Xpath不一樣,所以我們要在這裡設定統一的視窗大小
await driver.manage().window().setRect({ width: 1280, height: 800, x: 0, y: 0 });
//因為有些人是用FB帳號登入IG,為了避免增加FB登出的動作,所以採取先對IG進行爬蟲
await loginInstagramGetTrace(driver)
await loginFacebookGetTrace(driver)
driver.quit();
}
crawler()
看完上面的程式後,我想大部分的人都有點暈了XD,如果你有認真看程式應該有以下體會:
如果還有更多的體會歡迎大家在下方留言(請鞭小力一點QQ)
明天會講程式碼的重構,透過重構我們可以更有效率的掌握程式;後天會講try-catch在本專案的應用,方便日後的除錯
yarn start
如果想要中斷終端機(Terminal)執行的程式,可以用下面按鍵組合:
- Windows: Ctrl + c
- Mac: control + c
免責聲明:文章技術僅抓取公開數據作爲研究,任何組織和個人不得以此技術盜取他人智慧財產、造成網站損害,否則一切后果由該組織或個人承擔。作者不承擔任何法律及連帶責任!
我在 Medium 平台 也分享了許多技術文章
❝ 主題涵蓋「MIS & DEVOPS、資料庫、前端、後端、MICROSFT 365、GOOGLE 雲端應用、個人研究」希望可以幫助遇到相同問題、想自我成長的人。❞
在許多人的幫助下,本系列文章已出版成書,並添加了新的篇章與細節補充:
- 加入更多實務經驗,用完整的開發流程讓讀者了解專案每個階段要注意的事項
- 將爬蟲的步驟與技巧做更詳細的說明,讓讀者可以輕鬆入門
- 調整專案架構
- 優化爬蟲程式,以更廣的視角來擷取網頁資訊
- 增加資料驗證、錯誤通知等功能,讓爬蟲執行遇到問題時可以第一時間通知使用者
- 排程部分改用 node-schedule & pm2 的組合,讓讀者可以輕鬆管理專案程序並獲得更精確的 log 資訊
有興趣的朋友可以到天瓏書局選購,感謝大家的支持。
購書連結:https://www.tenlong.com.tw/products/9789864348008