我们今天要把 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 页。
明天我们就试着来优化一下效能好了。
以 9/23 20:00 ~ 9/24 20:00 文章观看数增加值排名
+2318
Day 1 无限手套 AWS 版:掌控一切的 5 + 1 云端必学主题
+1852
Day 2 AWS 是什麽?又为何企业这麽需要 AWS 人才?
+1851
Day 3 云端四大平台比较:AWS . GCP . Azure . Alibaba
+1773
Day 4 网路宝石:AWS VPC Region/AZ vs VPC/Subnet 关系介绍
+1758
Day 5 网路宝石:AWS VPC 架构 Routes & Security (上)
+1714
Day 7 网路宝石:【Lab】VPC外网 Public Subnet to the Internet (IGW) (上)
+1708
Day 6 网路宝石:AWS VPC 架构 Routes & Security (下)
+1676
Day 15 储存宝石:S3 架构 & 版本控管 (Versioning)
+1670
Day 10 运算宝石:EC2 储存资源 Instance Store vs Elastic Block Storage (EBS)
+1663
Day 14 储存宝石:S3是什麽? S3 vs EBS 方案比较
观看数增加速度创新高
透过 Props 可以让子元件接收来自父元件的「资料」,相对地,父元件则可以接收来自子元件的「事件」...
今年参赛的另一个主题,也请大家多多支持,感恩~ https://ithelp.ithome.com...
当我们在 GoDaddy 上申请好网域之後,就接着要把 GoDaddy 上的 DNS 转址到我们的服...
刚回台北好累rrr 自我介绍後通常会先问一些简答题, new grad 可能会有一些基本 CS 知...
Arguments 它会回传一个类阵列包含所有你传到函数中的参数 **类阵列(Array-like)...