Node.js Cheerio 爬取阮一峰「科技爱好者周刊」推荐的工具

830 个

看阮一峰的「科技爱好者周刊」一段时间了,其中工具/软件栏目安利的一些东西极大开拓眼界,用起来赏心悦目的今天心血来潮想把这部分内容给爬下来,用了一个多小时使用了 Node.js + Cheerio 进行爬取并成功输出到 JSON 文件中。

后续研究下文章的许可证,看能不能整理成一个文章/页面放到本站,按照行业/技术栈分类、排名供大家研究。

爬取流程

  1. 爬取博客分类页面,解析并获取所有文章 URL
  2. 挨个爬取文章 URL,解析并获取工具推荐内容
  3. 保存

流程看似简单,中间还是遇到一些坑,如果你需要运行,需要的依赖是:

yarn add axios cheerio html-to-md lowdb
  • axios: 比较好用的一个 HTTP 请求库
  • cheerio: HTML 解析、选择器、提取库
  • html-to-md: HTML 保存为 markdown
  • lowdb: 将爬取下来的内容存储到文件中,lowdb 是一个 JSON 文件数据库,使用详见更轻量级的本地文件数据库 lowDB 实现 CRUD

Cheerio 简介

Cheerio 是 Node.js 环境中 DOM 操作库,用来解析 HTML 非常容易上手。

Cheerio 实现了核心 jQuery 的子集。Cheerio 从 jQuery 库中消除了所有 DOM 不一致和浏览器差异,从而揭示了其真正华丽的API。

const cheerio = require('cheerio');
const $ = cheerio.load('<ul id="fruits">...</ul>');

$('.apple', '#fruits').text()
//=> Apple

$('ul .pear').attr('class')
//=> pear

$('li[class=orange]').html()

代码实现

代码实现很简单,只要逻辑清晰按顺序依次来就能搞定,cheerio 中还遇到一个编码的坑:Nodejs &#x 开头 unicode cheerio 乱码

const axios = require('axios')
const path = require('path')
const crypto = require('crypto')

const cheerio = require('cheerio')
const html2markdown = require('html-to-md')
const lowdb = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')

const URL = 'http://www.ruanyifeng.com/blog/weekly/'
const DB_PATH = path.join(__dirname, '../_data/ruanyifeng.json')

const db = lowdb(new FileSync(DB_PATH))

// 基础数据结构
db.defaults({
tools: {
last: '',
data: []
}
}).write()

function md5(str = '') {
return crypto
.createHash('md5')
.update(str)
.digest('hex')
}

/**
* 抓取详细内容
* @param {*} url 内容页面
*/
async function getContent(url) {
const resp = await axios.get(url)
const html = resp.data
const $ = cheerio
.load(html, { decodeEntities: false })('#main-content')
.children()
const date = cheerio
.load(html, { decodeEntities: false })('.asset-meta abbr.published')
.first()
.text()
.replace(/\s/gim, '')
const data = {
dateHash: md5(date),
date,
up: 0,
down: 0,
url,
item: []
}
let cursor = false
$.each((i, el) => {
const $el = cheerio.load(el, { decodeEntities: false })
if (el.name === 'h2' && ['软件', '工具'].includes($el.text())) {
cursor = true
} else if (el.name === 'h2' && !['软件', '工具'].includes($el.text())) {
cursor = false
}
if (cursor && el.name !== 'h2') {
data.item.push($el.html())
}
})
const item = data.item
.join('\n')
.replace(/^<p>\d、/gim, '#!#<p>')
.split('#!#')
.filter($ => !!$)
.map($ => {
const txt = html2markdown($)
return {
hash: md5(txt),
up: 0,
down: 0,
markdown: txt
}
})
return {
...data,
item
}
}

/**
* 根据入口页面获取 URL
* @param {*} url 入口页面
*/
async function getUrls(url) {
const resp = await axios.get(url)
const html = resp.data
const $ = cheerio.load(html, { decodeEntities: false })(
'#alpha-inner .module-list .module-list-item > a'
)
const list = []
$.each((i, el) => {
const $el = cheerio(el)
list.unshift({
title: $el.text(),
href: $el.attr('href')
})
})
return list
}

/**
* 启动
*/
async function start() {
const list = await getUrls(URL)
console.log('获取到 URL:', list.length)
const last = db.get('tools.last').value()
for (const item of list) {
if (item.href <= last) {
break
}
const content = await getContent(item.href)
console.log(item.href)
db.get('tools.data')
.unshift(content)
.write()
await sleep(200)
db.set('tools.last', item.href).write()
}
}

function sleep(timer = 200) {
return new Promise(resolve => {
setTimeout(resolve, timer)
})
}

start()

样例数据

数据太多,只截取了一部分出来

{
"tools": {
"last": "http://www.ruanyifeng.com/blog/2020/02/weekly-issue-95.html",
"data": [
{
"dateHash": "995be2a4baec97eecd4432fd520fad36",
"date": "2020年2月21日",
"up": 0,
"down": 0,
"url": "http://www.ruanyifeng.com/blog/2020/02/weekly-issue-95.html",
"item": [
{
"hash": "9742e2af85e7fe9ed58c154ff3c2c514",
"up": 0,
"down": 0,
"markdown": "\n[sscaffold-css](https://sscaffold-css.com/)\n\n一个极简的 CSS 默认样式库,目的是为 HTML 裸标签提供美观的样式。\n"
},
{
"hash": "2efa4bbcb02cae7ecfa4f85d9f6539fd",
"up": 0,
"down": 0,
"markdown": "\n[Wayback Machine 插件](https://blog.archive.org/2017/01/13/wayback-machine-chrome-extension-now-available/)\n\n![](https://www.wangbase.com/blogimg/asset/202001/bg2020010606.jpg)\n\nChrome 浏览器插件,互联网档案馆的官方版本,可以查看一个网页的历史版本,包括那些已经无法访问的网页。\n"
},
{
"hash": "f824678421fa0a87f16b62075dee9cc0",
"up": 0,
"down": 0,
"markdown": "\n[inlets](https://github.com/inlets/inlets)\n\n一个反向代理服务器,可以将内网的服务映射到公网。\n"
},
{
"hash": "88c92fc3d1a9dc682ece3facbe73b5d3",
"up": 0,
"down": 0,
"markdown": "\n[jql](https://github.com/cube2222/jql)\n\n一个命令行的 JSON 数据查询工具,有更简单的查询语法。\n"
},
{
"hash": "6c3f8d9646bdd24ae1e1a28b46c4bd0a",
"up": 0,
"down": 0,
"markdown": "\n[Broot](https://dystroy.org/broot/)\n\n一个命令行的目录树浏览工具,可以作用`ls`命令的替代品。\n"
},
{
"hash": "0efb4c97b7afb5ccff86a742c4e4a397",
"up": 0,
"down": 0,
"markdown": "\n[Snowpack](https://www.snowpack.dev/)\n\n一个 JavaScript 工具,可以将 node.js 模块转成单个的 JS 文件,替代 Webpack 这样的打包工具。\n"
},
{
"hash": "f70c8abca6d3ab9a1e236627e99d4409",
"up": 0,
"down": 0,
"markdown": "\n[Hexo Cheatsheets Theme](https://github.com/glazec/hexo-cheatsheets)\n\n一个 Hexo 的主题,可以用来制作速查表(cheatsheet)网站,参见 [devhints.io](https://devhints.io/) 的例子。(@[glazec](https://github.com/ruanyf/weekly/issues/1038) 投稿)\n"
},
{
"hash": "34b68e7128b7ddea95c19a7bb4fe43bd",
"up": 0,
"down": 0,
"markdown": "\n[萤火虫](https://ncase.me/fireflies/)\n\n网页模拟黑夜中萤火虫飞舞的效果。\n"
},
{
"hash": "c22a47325eb6e778dd4f49aa055eae41",
"up": 0,
"down": 0,
"markdown": "\n[png](https://github.com/vivaxy/png)\n\n一个 Node.js 模块,用于 PNG 图片的解码和编码。(@[vivaxy](https://github.com/ruanyf/weekly/issues/1040) 投稿)\n\n10、[showdoc](https://github.com/star7th/showdoc)\n\n一个技术文档网站的服务端,适合展示团队的技术文档、API 文档。(@[star7th](https://github.com/ruanyf/weekly/issues/1041) 投稿)\n"
}
]
},
{
"dateHash": "986320f5974ca13f79674059c386f03e",
"date": "2020年2月14日",
"up": 0,
"down": 0,
"url": "http://www.ruanyifeng.com/blog/2020/02/weekly-issue-94.html",
"item": [
{
"hash": "c0993ead5d97120d0724aebde47c2cba",
"up": 0,
"down": 0,
"markdown": "\n[Snip](https://mathpix.com/)\n\n![](https://www.wangbase.com/blogimg/asset/201912/bg2019122701.jpg)\n\n将打印的数学公式转成 LaTex 代码的工具。\n"
},
{
"hash": "eafdcb1a374de27c2f0ee723bd2666b9",
"up": 0,
"down": 0,
"markdown": "\n[Gmail 分析器](https://github.com/0xbsec/gmail_analyzer)\n\n命令行工具,可以显示你的 Gmail 邮箱的统计数据。\n"
},
{
"hash": "9f5368654e50fb9166de2918f8570995",
"up": 0,
"down": 0,
"markdown": "\n[age](https://github.com/FiloSottile/age)\n\n一个命令行工具,使用公钥/私钥对文件进行解密和加密,用法很简单。\n"
},
{
"hash": "50b0526d370305be4d144a48ca20e6a7",
"up": 0,
"down": 0,
"markdown": "\n[蚁阅](https://github.com/anyant/rssant)\n\n![](https://www.wangbase.com/blogimg/asset/201912/bg2019123101.jpg)\n\n开源的 Web 端 RSS 阅读器,基于 Python。(@[guyskk](https://github.com/ruanyf/weekly/issues/1027) 投稿)\n"
},
{
"hash": "1fcd0bd305bd5c13f21f46d455089514",
"up": 0,
"down": 0,
"markdown": "\n[stpyv8](https://github.com/area1/stpyv8)\n\n一个引入 V8 引擎的 Python 模块,使得 Python 程序里面可以写 JavaScript 代码。\n"
},
{
"hash": "eb62c5eeefdd7086403b8e0bb7284210",
"up": 0,
"down": 0,
"markdown": "\n[NodeTube](https://github.com/mayeaux/nodetube)\n\n一个可以自己架设的 Youtube 替代品,可以上传视频在网页观看,基于 Node.js。\n"
},
{
"hash": "e37a68e1b4086bb99979044d025e6344",
"up": 0,
"down": 0,
"markdown": "\n[tauri](https://github.com/tauri-apps/tauri)\n\n一个使用各平台的 WebView 控件,构建跨平台桌面应用的 JavaScript 框架。(@[mantou132](https://github.com/ruanyf/weekly/issues/1033) 投稿)\n"
},
{
"hash": "06169be40681598e45b3fb59adedb6bd",
"up": 0,
"down": 0,
"markdown": "\n[GitHub-Chart](https://chrome.google.com/webstore/detail/github-chart/apaldppjjcjgjddfobajdclccgkbkkje)\n\n![](https://www.wangbase.com/blogimg/asset/202001/bg2020010207.jpg)\n\nChrome 浏览器插件,可以三维显示 GitHub 的提交统计。(@[ryuzheng](https://github.com/ruanyf/weekly/issues/1035) 投稿)\n"
},
{
"hash": "6d3ef4af197ae55f5f8746e6835299a8",
"up": 0,
"down": 0,
"markdown": "\n[Generative Placeholders](https://generative-placeholders.glitch.me/)\n\n![](https://www.wangbase.com/blogimg/asset/202001/bg2020010214.jpg)\n\n获取占位图像的网站,所有生成的图像都是艺术化的几何图形。\n\n10、[Terrastruct](https://terrastruct.com/)\n\n![](https://www.wangbase.com/blogimg/asset/202001/bg2020010501.jpg)\n\n一个在线的架构图、流程图工具。\n"
}
]
}
]
}
}

样例内容

样例内容是根据爬取结果,使用 JS 自动输出出来的: