今天,来优化爬虫的速度。
回顾一下,我们的程序执行了以下步骤:
我们先来记录一下各步骤执行的时间。
// 在 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!
以 9/24 20:00 ~ 9/25 20:00 文章观看数增加值排名
+953
Day 1 无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
+803
Day 3 云端四大平台比较:AWS . GCP . Azure . Alibaba
+801
Day 2 AWS 是什麽?又为何企业这麽需要 AWS 人才?
+777
Day 4 网路宝石:AWS VPC Region/AZ vs VPC/Subnet 关系介绍
+750
Day 5 网路宝石:AWS VPC 架构 Routes & Security (上)
+727
Day 6 网路宝石:AWS VPC 架构 Routes & Security (下)
+725
Day 8 网路宝石:【Lab】VPC外网 Public Subnet to the Internet (IGW) (下)
+724
Day 9 运算宝石:EC2 重点架构
+717
Day 10 运算宝石:EC2 储存资源 Instance Store vs Elastic Block Storage (EBS)
+714
Day 7 网路宝石:【Lab】VPC外网 Public Subnet to the Internet (IGW) (上)
看来今天 AWS 文章观看数增加速度收敛一点了
前言 我们可以到Switch开启时背景色会是绿色,关掉时却是黑色,这样其实跟原本IPhone内建的也...
今天是最後一篇,也是第三十天了,各位每一天都跟我一样有练习一题,这样三十天已经练习接近三十题了,虽然...
AWS Data Pipeline 是一种 Web 服务,可协助您以指定的间隔,可靠地在不同 AWS...
一. 资料准备 这次任务是实作机器翻译,资料: http://www.manythings.org/...
https://youtu.be/vpwC347cXog 陷入低潮 了解低潮 专注在可控的短期 充...