#11 Web Crawler 4

今天,来优化爬虫的速度。

调查问题成因

回顾一下,我们的程序执行了以下步骤:

  1. 下载网页
  2. 解析网页
  3. 合并数据
  4. 储存数据

我们先来记录一下各步骤执行的时间。

// 在 Top Level 增加记录用时的变数
let DownloadT = 0,
    ParseT = 0,
    OrganizeT = 0,
    SaveT = 0;

我们用 Date.now() 来记录各时间点的时间,然後计算差值来获取所花时间。

Download: 542.958s, Parse: 69.241s, Organize: 0.005s, Save: 0.036s

总共花了 612.24 秒,其中花最多时间的是下载网页 (88.7%),再来是解析网页 (11.3%),合并数据及储存数据的部分所花时间则皆不足 0.1%。

爬取 1047 个页面花费 543 秒,平均一个网页请求需约 500 毫秒。用浏览器的 console 调查了一下,发现单一网页文件大小只有约 15 kB 爬取所有页面也才需要约 15 MB 的大小。我们可以推断,请求延迟阻塞了许多时间

在处理请求延迟的问题上,我们可以使用并发来减少其对总时间之影响,当然也得确定并发数量不会过大造成服务器负担。

我们的方法是:第一次请求时纪录总共有多少页,接着直接依照纪录的执行请求,而不从当前页面推断下一页,这样就不会因为要知道前一页才能处理下一页而造成组塞了。当然为了预防在爬取时有新的文章出现,我们会在每次爬取时确定总页数有没有增加,动态的提升总页数纪录

更新程序

我们在执行前先设定最大并发上限:

// 在 Top Level 且在 main() 执行前
let available = 12; // 最大并行上限设为 12
const finished = [];
function isAvailable() {
    return new Promise((resolve) => {
        if (available > 0) {
            available--;
            resolve();
        } else finished.push(resolve);
    });
}

我们会使用 isAvailable 来阻塞避免超过最大上限,当已有最大上限之行程在执行时,则会等待直至现有行程完成而出现空缺。

同时,我们将 crawler 中的部分程序移出至新的 task 函式:

async function task(url) {
    console.log(`Crawling Page: ${url}`);
    const result = new Map();
    const DT = Date.now();
    const html = await fetch(url).then((res) => res.text());
    DownloadT += Date.now() - DT;

    const PT = Date.now();
    const dom = new JSDOM(html);
    const document = dom.window.document;

    // 以 CSS Selector 寻找总页数
    const pageCount = parseInt(document.querySelector(".pagination > li:nth-last-child(2)").textContent);

    const articles = document.querySelectorAll("li.ir-list");
    for (const article of articles) {
        const parsed = parseArticle(article);
        result.set(parsed.link, parsed);
    }
    ParseT += Date.now() - PT;

    if (finished.length > 0) {
        // 释出空间让其他任务可以执行
        const resolve = finished.shift();
        resolve();
    }

    return { result, pageCount };
}

所以现在我们的 crawler 函式只剩下:

async function crawler(startURL) {
    const result = new Map();

    // 先爬取第一页
    const firstPage = await task(startURL);
    firstPage.result.forEach((val, key) => result.set(key, val));
    
    // 总页数,注意:我们用 let 而非 const
    let total = firstPage.pageCount;
    // 纪录任务用,最後使用 Promise.all 来确认所有请求皆已完成
    const tasks = [];
    for (let i = 2; i <= total; i++) {
        // 用来在已达最大上限时阻塞
        await isAvailable();
        // 把任务放入任务集合
        tasks.push(
            task(startURL + "?page=" + i).then((page) => {
                page.result.forEach((val, key) => result.set(key, val));
                // 如果发现总页数增加了,则更新总页数
                if (page.pageCount > total) total = page.pageCount;
            })
        );
        // 当到最後一页时,等待所有任务都完成再跳出,或有新的总页数则继续
        if(i === total) await Promise.all(tasks);
    }

    // 回传阵列型态的 result
    return [...result.values()];
}

其他的部分,我们不需要做任何更动,执行看看吧!

优化效果

Running...

Crawled 10563 Articles in 130.73s
Download: 1370.683s, Parse: 53.579s, Organize: 0.006s, Save: 0.166s

我们用 130.7 秒爬了 10563 篇文章,平均一秒爬取 80 篇文章,也就是 8 页,速度是未优化前的 3.2 倍
等等,Download 时间为什麽有 1370 秒?因为 Download 跟 Parse 都是并行的,所以计时上会有重叠的状况。

并行 (concurrency) 的好处是可以让 CPU 的使用效率更高,而无须花太多时间因等待而闲置计算资源。

资料爬下来之後?

资料爬下来之後就拿来分析并做成下面那个东西吧!
明天就来做每日铁人赛热门 Top 10


每日铁人赛热门 Top 10 (0924)

以 9/24 20:00 ~ 9/25 20:00 文章观看数增加值排名

  1. +953 Day 1 无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
    • 作者: 用图片高效学程序
    • 系列:无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
  2. +803 Day 3 云端四大平台比较:AWS . GCP . Azure . Alibaba
    • 作者: 用图片高效学程序
    • 系列:无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
  3. +801 Day 2 AWS 是什麽?又为何企业这麽需要 AWS 人才?
    • 作者: 用图片高效学程序
    • 系列:无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
  4. +777 Day 4 网路宝石:AWS VPC Region/AZ vs VPC/Subnet 关系介绍
    • 作者: 用图片高效学程序
    • 系列:无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
  5. +750 Day 5 网路宝石:AWS VPC 架构 Routes & Security (上)
    • 作者: 用图片高效学程序
    • 系列:无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
  6. +727 Day 6 网路宝石:AWS VPC 架构 Routes & Security (下)
    • 作者: 用图片高效学程序
    • 系列:无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
  7. +725 Day 8 网路宝石:【Lab】VPC外网 Public Subnet to the Internet (IGW) (下)
    • 作者: 用图片高效学程序
    • 系列:无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
  8. +724 Day 9 运算宝石:EC2 重点架构
    • 作者: 用图片高效学程序
    • 系列:无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
  9. +717 Day 10 运算宝石:EC2 储存资源 Instance Store vs Elastic Block Storage (EBS)
    • 作者: 用图片高效学程序
    • 系列:无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
  10. +714 Day 7 网路宝石:【Lab】VPC外网 Public Subnet to the Internet (IGW) (上)
    • 作者: 用图片高效学程序
    • 系列:无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题

看来今天 AWS 文章观看数增加速度收敛一点了


<<:  人的管理 - 危机感 vs. 安全感

>>:  Day25 建立角色功能

Swift纯Code之旅 Day26. 「客制化Switch按钮」

前言 我们可以到Switch开启时背景色会是绿色,关掉时却是黑色,这样其实跟原本IPhone内建的也...

[Day30]30天心得

今天是最後一篇,也是第三十天了,各位每一天都跟我一样有练习一题,这样三十天已经练习接近三十题了,虽然...

[Day27] AWS Data Pipeline

AWS Data Pipeline 是一种 Web 服务,可协助您以指定的间隔,可靠地在不同 AWS...

[Day26] NLP会用到的模型(九)-实作transformer-上

一. 资料准备 这次任务是实作机器翻译,资料: http://www.manythings.org/...

【程序】陷入低潮 转生成恶役菜鸟工程师避免 Bad End 的 30 件事 - 23

https://youtu.be/vpwC347cXog 陷入低潮 了解低潮 专注在可控的短期 充...