node爬蟲-使用puppeteer

小黎也發表於2018-04-02

headless

最近看了些關於谷歌 headless的介紹,簡單的說就是一個無介面的瀏覽器的,可用於前端自動化測試和爬蟲抓取 因為headless是模擬使用者行為操作的,所以爬蟲也是完全模擬使用者的行為,且截圖也非常方便。

puppeteer

是一個基於headless的封裝,提供了很多非常方便實用的api,截個圖如下

puppeteer api
詳細文件可以檢視官網

練練手

文件也看啦,是該練練手的時候啦,恰好公司叫車報銷,需要到hr系統中,將每晚下班的打卡截圖出來,往常都是手動截圖,效率非常低,且都是重複工作,是時候解放生產力啦~ 先說下大體的流程

  1. 登陸
  2. 跳轉到考勤記錄介面
  3. 輸入查詢日期和結束日期,因為每一張截圖只要當天的記錄,所以只能一天天的查
  4. 點選查詢按鈕
  5. 獲取當天最晚打卡記錄的時間,判斷時間是否在報銷時段內,若是,則截圖儲存

程式碼如下,因為是內部系統,關鍵引數已打碼

const puppeteer = require('puppeteer');
var moment = require('moment');

// 引數配置
const config = {
    name: '***',
    password: '***',
    targetTime: '21:00:00',
    canbuTime: '19:30:00',
    startTime: '2017-12-01',
    endTime: '2018-04-01',
    searchCanbu: true,
    searchDache: true
}

let resultData = {
    canbuCounts: 0
}

async function run(params) {
    const browser = await puppeteer.launch({ headless: true });  //啟動瀏覽器,headless為false,可以開啟模擬器,預設為true
    const page = await browser.newPage();
    await page.goto('***'); //跳轉目標地址
    const loginDom = {
        name: '#username',
        password: '#password',
        loginBtn: '#btnSubmit'
    }

    const searchDom = {
        createDateStart: '#createDateStart',
        createDateEnd: '#createDateEnd',
        searchDom: '#btnSubmit'
    }

    await page.type(loginDom.name, config.name); // 將文字寫入輸入框

    await page.type(loginDom.password, config.password);

    await page.click(loginDom.loginBtn); //點選按鈕

    await page.waitForNavigation(); // 等待頁面跳轉返回

    await page.goto('***');

    const days = moment.duration(new Date(config.endTime).getTime() - new Date(config.startTime).getTime()).asDays();
    let searchTime = config.startTime;

    for (let i = 0; i < days; i++) {
        // 可以看作在瀏覽器中執行的片段
        await page.evaluate(() => {
            const startDom = document.querySelector('#createDateStart');
            const endDom = document.querySelector('#createDateEnd');
            startDom.removeAttribute("readOnly");
            endDom.removeAttribute("readOnly");
            startDom.value = '';
            endDom.value = '';
        })
        await page.type(searchDom.createDateStart, searchTime);
        await page.type(searchDom.createDateEnd, searchTime);
        await page.click(searchDom.searchDom);
        await page.waitFor(800);

        const afterTime = await page.evaluate(() => {
            try {
                return document.querySelector('#contentTable tbody tr:nth-child(1) td:last-child').innerHTML;
            } catch (error) {
                return null;
            }
        })
        if (config.searchDache && checkApplyTime(afterTime)) {
            //截圖
            await page.screenshot({ path: `screenshot/clock-${searchTime}.png`, clip: { x: 0, y: 0, width: 800, height: 217 } });
            console.log(`${searchTime}可以報銷的士票哦~`);
        }
        if (config.searchCanbu && checkCanbuTime(afterTime)) {
            console.log(`${searchTime}可以報銷餐補啦~`);
            resultData.canbuCounts++;
        }

        searchTime = moment(searchTime).add(1, 'day').format('YYYY-MM-DD');

    }

    await browser.close();
};
function checkApplyTime(time) {
    if (!time) {
        return false;
    }
    return new Date(time) >= new Date(`${moment(time).format('YYYY-MM-DD')} ${config.targetTime}`)
};

function checkCanbuTime(time) {
    if (!time) {
        return false;
    }
    return new Date(time) >= new Date(`${moment(time).format('YYYY-MM-DD')} ${config.canbuTime}`)
}

run()
複製程式碼

其中會涉及到一些dom節點的獲取與修改,比如輸入時間的input,時候通過外掛來選擇時間的,且input是readonly的狀態,所以就需要些小操作,多嘗試幾次還是可以解決的

###待提升 執行效率上,還是比較慢,有空會在優化下~

相關文章