[自動登入避開 recaptcha] 利用selenium-webdriver 製作MeWe Scraper
隨着MeWe 越來越多人使用,有不少開發者都在思考到底MeWe能否成為下一個Facebook,然而這不是本文章要討論的內容,而今次想與大家分享一下Web Scraper。
如果大家有用過MeWe都知道它需要登入才可以看到專頁或群組的貼文,加上MeWe主張資料安全及沒有廣告,因此它們並沒有提供API讓我們使用。換言之,自行寫一個Web Scraper是唯一一個方法。
最後的目標
如果需要製作MeWe Scraper,我們的Scraper需要做到以下兩個條件:
第一步: Scraper需要自動登入 (需要處理recaptcha問題)
第二步: Scraper可以自動填入文字及向下滾動
現時的Web Scraper
今次我們會使用selenium web node作為Web Scraper,我會解釋為甚麼要用它及我試用過其他Web Scraper的情況。
Chrome的網頁Scraper — webscraper.io
一開始的時候,我打算使用webscraper.io作為MeWe 的Scraper,然而我其後發現MeWe必須要登入及不能夠自動在Text Field上輸入文字。雖然有些網頁的Text Field可以透過Javascript的方式填入文字然後觸發搜尋功能,但由於MeWe的搜尋欄位是利用Javascript 控制,因此必須模擬鍵盤的輸入。
Node 程式庫 — puppeteer 及 chrome-aws-lambda
puppeteer是Google開發的headless Chrome,Headless Chrome 即是在伺服器執行Chrome,我們可以通過命令行界面控制Chrome作不同自動化工作。
而 chrome-aws-lambda與puppeteer 十分相似,是簡化版的puppeteer,多用於在後台模擬瀏覽器。
它們的功能也十分豐富,例如在Nodejs中可透過
let page = await browser.newPage();
await page.goto('https://example.com');
開啟一個分頁並前往 https://example.com。
你也可以選擇等待的時間才到下一個步驟。
有興趣了解更多可參考這裏的github連結
puppeteer/puppeteer
alixaxel/chrome-aws-lambda
puppeteer 及 chrome-aws-lambda最後都沒有使用的原因是因為它們在執行Scraper的工作沒selenium方便,而且selenium的速度都比兩者快。
selenium-webdriver
這一次我將會使用nodejs版的selenium-webdriver,它也擁有python版,語法是差不多,大家可以放心。
第一步,建立新的Node專案
npm init
完成一些名稱的設定後,你可以安裝MeWe Scraper需要用到的package。
npm i -g chromedrive
npm i selenium-webdriver
第二步,建立start.js
我們會將所有程式都寫在這裏,我們需要作三個動作:
- 開啟MeWe.com 並自動填入帳號及密碼
- 使用「人手」的方法避開 recaptcha,完成登入
- 在MeWe的搜尋欄填入搜尋字串,然後取得搜尋結果。
start.js
const {Builder, By, Key, until} = require('selenium-webdriver');
const fs = require('fs');
const C = {
username: "YOUR_MEWE_ACCOUNT",
password: "YOUR_MEWE_PASSWORD"
};const start = async (searchStr) => {
let driver = await new Builder().forBrowser('chrome').build();
/*
三個動作
*/
}start("香港人")
第一個動作 — 開啟MeWe.com 並自動填入帳號及密碼
await driver.get('http://www.mewe.com/'); //開啟MeWe.com
await driver.wait(until.elementLocated(By.xpath('//*[@id="login-fake-btn"]')), 10000);
await driver.findElement(By.xpath('//*[@id="login-fake-btn"]')).click();
await driver.sleep(100);
await driver.findElement(By.xpath('//*[@id="email"]')).sendKeys(C.username); //自動填入帳號
await driver.sleep(100);
await driver.findElement(By.xpath('//*[@id="password"]')).sendKeys(C.password); //自動填入密碼
await driver.findElement(By.xpath('//*[@id="login-overlay"]/div/form/button')).click(); //自動登入
await driver.wait(until.elementLocated(By.xpath('//*[@id="ember24"]')), 20000); //等待直至完成登入
第二個動作 — 使用「人手」的方法避開 recaptcha,完成登入
由於MeWe在登入界面中有recaptcha,我們需要「手動」完成recaptcha後才會執行下最後一句的程式。
外國有些網頁提供「自動別人手」或其他程式庫完成recaptcha,這裏不作太多介紹。
第三個動作 -在MeWe的搜尋欄填入搜尋字串,然後取得搜尋結果。
start.js
...
await new Promise(r => setTimeout(r, 1000));
await driver.findElement(By.xpath('//*[@id="ember24"]')).sendKeys(searchStr, Key.RETURN);
await driver.sleep(500);
await driver.findElement(By.xpath('//div[text() =\'Groups\']')).click();
await driver.sleep(1000);
let scrollHeight = 2407
let numberOfLoop = 5
for (let i = 0; i < numberOfLoop; i++) {
await driver.executeScript("document.getElementsByClassName(\"smart-search_result smart-search_result--groups win-scrollbar\")[0].scrollBy(0, " + scrollHeight + ")")
await driver.sleep(1500);
}
let scrollResult = await driver.findElement(By.className('smart-search_result smart-search_result--groups win-scrollbar'))
let children = await scrollResult.findElements(By.className('smart-search_group c-mw-smart-search-group ember-view'))
在上述的程式碼中,我們會自動選擇群組的分類(專頁的數據之後在github更新)。由於MeWe每一次只列出30個結果,因此我們需要利用selenium-webdriver 自己在指定的時間向下滾動以取得更多結果。
我們在搜尋中最少找到每一個群組的五個數據:
- 群組圖片
- 群組名稱
- 群組連結
- 加入群組人數
- 加入類型(公開/非公開)
因此我們可以利用Forloop的方式取得這些數據
let jsonArr = []
let allPromise = []
if (Array.isArray(children)) {
children.map(async webEle => {
allPromise.push(new Promise(async (resolve, reject) => {
let imgDom = await webEle.findElement(By.className("profile_img usr-avatar-small"))
let aDom = await webEle.findElement(By.className("smart-search-group_img ember-view"))
let titleDom = await webEle.findElement(By.className("h-trim ember-view"))
let numberOfMemberDom = await webEle.findElement(By.className("smart-search-group_members"))
let groupType = await webEle.findElement(By.className("h-flex_center_x_y"))
jsonArr.push({
url: await driver.executeScript("return arguments[0].attributes['href'].value", aDom), //return join link
imageSrc: await driver.executeScript("return arguments[0].attributes['src'].value", imgDom), //return image src from img dom,
title: await driver.executeScript("return arguments[0].innerText", titleDom), //return group title,
numberOfMember: parseInt(await driver.executeScript("return arguments[0].innerText.split(\"Members (\")[1].split(\")\")[0]", numberOfMemberDom)), //return numberOf member,
public: await driver.executeScript("return arguments[0].innerText", groupType) === "Join Group", //return is a public group,
description: "",
country: "",
category: "",
subCategory: "",
})
resolve()
}))
})
}
Promise.all(allPromise)
.then(res => {
fs.writeFile('example.json', JSON.stringify(jsonArr), 'utf8', function(err) {
if (err) return console.log(err);
});
})
.catch(err => console.log(err))
當取得數據後,打包為json格式然後輸出到我們的專案中。
示範結果:
[
{
"url": "/group/5fc36bfa7f1d500f69a484be",
"imageSrc": "https://img.mewe.com/api/v2/group/5fc36bfa7f1d500f69a484be/public-image/5fcc51ebda6a0364ec119b82/400x400/img",
"title": "香港人里數/獎賞/旅遊分享區",
"numberOfMember": 816,
"public": false,
"description": "",
"country": "",
"category": "",
"subCategory": ""
},
{
"url": "/group/5fbe3d3ec057695a0a69610a",
"imageSrc": "https://img.mewe.com/api/v2/group/5fbe3d3ec057695a0a69610a/public-image/5fbe3d3e67b8dd74597c2ace/400x400/img",
"title": "香港人@英國💛互助圈",
"numberOfMember": 263,
"public": false,
"description": "",
"country": "",
"category": "",
"subCategory": ""
}
]
Github連結
如果你希望貢獻,你可以前往我的github專案分享你的想法喔😜。你也可在下方拍一拍手支持我們。