#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 云端必学主题

观看数增加速度创新高


<<:  现实生活中的无耳猫

>>:  [Day9] ORM Injection

Day 23:儿子有事交给爸-$emit 传出事件

透过 Props 可以让子元件接收来自父元件的「资料」,相对地,父元件则可以接收来自子元件的「事件」...

2021-Day1. 开箱 Google Cloud Jam 活动背包

今年参赛的另一个主题,也请大家多多支持,感恩~ https://ithelp.ithome.com...

GoDaddy 设定 DNS 转址到 IIS 上指定网站

当我们在 GoDaddy 上申请好网域之後,就接着要把 GoDaddy 上的 DNS 转址到我们的服...

【面试】技术与专案问答

刚回台北好累rrr 自我介绍後通常会先问一些简答题, new grad 可能会有一些基本 CS 知...

JavaScript Arguments and Spread

Arguments 它会回传一个类阵列包含所有你传到函数中的参数 **类阵列(Array-like)...