iT邦幫忙

2021 iThome 鐵人賽

DAY 10
2
Modern Web

JavaScript Easy Go!系列 第 10

#10 Web Crawler 3

我們今天要把 crawler 函式及 saveData 函式寫好!

crawler 函式

我們就依照昨天的想法把 crawler 函式寫出來,並把 parseArticle 較為繁重複雜的工作分離出去。

async function crawler(startURL) {
    // 爬到的資料,用唯一鍵值的 Map 存單一 Block 的所有文章資料
    const result = new Map();

    // nextURL 是下一頁的網址,如果沒有下一頁就是 null
    let nextURL = startURL;
    let page = 1;
    // 一個 Page 一個 Page 的抓
    while (nextURL) {
        console.log(`Crawling Page ${page}`);
        // html 會是純文字網頁內容
        const html = await fetch(nextURL).then((res) => res.text());
        // dom 是 JSDOM 物件,我們可以使用一般操作 DOM 的方法來取得資料
        const dom = new JSDOM(html);
        const document = dom.window.document;

        // 取得文章列表
        const articles = document.querySelectorAll("li.ir-list");
        // 一個一個文章處裡
        for (const article of articles) {
            const parsed = parseArticle(article);
            // 以文章網址為 key 將文章資料存入 result
            result.set(parsed.link, parsed);
        }

        // 取得下一頁的網址
        nextURL = document.querySelector(".pagination > .active")?.nextElementSibling?.querySelector("a")?.href;
        page++;
    }

    // 回傳陣列型態的 result
    return [...result.values()];
}

parseArticle 函式

parseArticle 的單一職責就是把資料從 DOM 中抽出變成方便取用的資料。

因為在處理文章時不知道會不會被莫名其妙的單一文章害死,所以用 try-catch 包起來。

然後呢,我們需要去打開 F12 Console 去調查看看目標元素的 CSS Selector 怎麼寫。

function parseArticle(article) {
    try {
        // 用事先調查的 CSS Selector 取得各項資料
        const type = article.querySelector(".group-badge__name").textContent.trim();
        const series = article.querySelector(".ir-list__group-topic > a").textContent.trim();
        const title = article.querySelector(".ir-list__title > a").textContent.trim();
        const link = article.querySelector(".ir-list__title > a").href;

        // 關於 info 的部分比較複雜,先以 \n 為刀切開字串,再移除空白字元
        const info = article
            .querySelector(".ir-list__info")
            .textContent.trim()
            .split("\n")
            .map((x) => x.trim());
        // 然後我們會從切開的 info 中抓出作者、發布時間、觀看數、團隊名稱(如果有的話)
        let author, date, view, team;
        // 根據觀察,有團隊的話會有 8 個字串,沒有的話則有 4 個字串
        if (info.length === 4 || info.length === 8) {
            author = info[0];
            // date 的格式為 [YYYY, MM, DD]
            date = info[3]
                .match(/(\d{4})-(\d{2})-(\d{2})/)
                .slice(1, 4)
                .map(Number);
            view = +info[3].match(/(\d+?) 次瀏覽/)[1];

            if (info.length === 8) team = info[7].substr(2);
            else team = null;
        } else {
            // 發生未符合 4 或 8 個字串的情況,就拋出錯誤
            throw new Error(`${title}: Invalid Article Info ${info.length}`);
        }

        // 回傳解析後的資料
        return { type, series, title, link, author, date, view, team };
    } catch (err) {
        // 如果中間不幸發生錯誤,就顯示錯誤並跳過這篇文章
        console.error("Article Parse Error", err.message);
    }
}

saveData 函式

我們預計一天抓取四次資料。

關於儲存資料的部分,為了避免全部的資料都存在同一個檔案讓單一檔案太大又或者是每次抓取的資料都放在獨立檔案中導致檔案太多,所以我們折衷把一天抓到的資料都放在同一個檔案。

用了 Node.js 總要學點檔案操作是吧。絕對不是我懶得建資料庫,才用檔案系統。

這裡先說聲抱歉,昨天忘了加上 path Package 了,雖然說不是一定需要,但最好還是用 path 去處理路徑。

const path = require("path");
function saveData(data) {
    // 紀錄現在時間
    const d = new Date();
    // 檔案名稱及路徑,格式為 YYYY-MM-DD.json
    const filename = `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}.json`;
    const filePath = path.join(__dirname, "../data", filename);

    // file 是已經存在的檔案,如果檔案不存在就會是空的 {}
    let file = {};
    if (fs.existsSync(filePath)) file = JSON.parse(fs.readFileSync(filePath));

    // 將新的資料添加到 file 中
    const hour = d.getHours().toString().padStart(2, "0");
    file[hour] = data;

    // 將 file 寫入檔案,__dirname 是執行的位置
    if (!fs.existsSync(path.join(__dirname, "../data"))) fs.mkdirSync(path.join(__dirname, "../data"));
    fs.writeFileSync(filePath, JSON.stringify(file));
}

完成了!

接著在 Terminal 打

node index.js

執行爬蟲!

這樣我們的爬蟲就完成了,但是不是覺得有點慢呢?
395.3 秒爬了 9890 篇文章,平均一秒爬取 25 篇文章,也就是 2.5 頁。
明天我們就試著來優化一下效能好了。


每日鐵人賽熱門 Top 10 (0923)

以 9/23 20:00 ~ 9/24 20:00 文章觀看數增加值排名

  1. +2318 Day 1 無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
    • 作者: 用圖片高效學程式
    • 系列:無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
  2. +1852 Day 2 AWS 是什麼?又為何企業這麼需要 AWS 人才?
    • 作者: 用圖片高效學程式
    • 系列:無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
  3. +1851 Day 3 雲端四大平台比較:AWS . GCP . Azure . Alibaba
    • 作者: 用圖片高效學程式
    • 系列:無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
  4. +1773 Day 4 網路寶石:AWS VPC Region/AZ vs VPC/Subnet 關係介紹
    • 作者: 用圖片高效學程式
    • 系列:無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
  5. +1758 Day 5 網路寶石:AWS VPC 架構 Routes & Security (上)
    • 作者: 用圖片高效學程式
    • 系列:無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
  6. +1714 Day 7 網路寶石:【Lab】VPC外網 Public Subnet to the Internet (IGW) (上)
    • 作者: 用圖片高效學程式
    • 系列:無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
  7. +1708 Day 6 網路寶石:AWS VPC 架構 Routes & Security (下)
    • 作者: 用圖片高效學程式
    • 系列:無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
  8. +1676 Day 15 儲存寶石:S3 架構 & 版本控管 (Versioning)
    • 作者: 用圖片高效學程式
    • 系列:無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
  9. +1670 Day 10 運算寶石:EC2 儲存資源 Instance Store vs Elastic Block Storage (EBS)
    • 作者: 用圖片高效學程式
    • 系列:無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
  10. +1663 Day 14 儲存寶石:S3是什麼? S3 vs EBS 方案比較
    • 作者: 用圖片高效學程式
    • 系列:無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題

觀看數增加速度創新高


上一篇
#9 Web Crawler 2
下一篇
#11 Web Crawler 4
系列文
JavaScript Easy Go!31

尚未有邦友留言

立即登入留言