本文介绍了一个从 HTML 提取 JSON 数据的工具,并以豆瓣电影的例子展示了该工具的使用方法。本文中用到了大量的 CSS 选择器,CSS 选择器可以参考
MDN
。
最近几个月写 Node 爬虫比较多,解析 HTML 文档用的工具是
cheerio
(cheerio 可以认为是服务器版的 jQuery)。cheerio 功能相当丰富,提供了一大堆 API 来查询/修改/删除/添加结点或文本。不过随着爬取的页面数量越来越多,大量使用 cheerio 还是显得繁琐了一点。爬虫对于处理
HTML 的模式其实比较固定,但是 cheerio 处理某些模式时不够简洁明了,下面三点就是一些比较常见的情况:
下面的三点中,假设我们要从豆瓣电影首页中爬取上图这样一个
「正在热映」
列表。注意该列表是实时更新的,所以本文中下面的选择器的运行结果可能不同。
同一个元素会包含多个数据字段。比如上图中每个电影都有电影标题
title
,链接
url
和评分
rate
字段;
爬取目标是一个列表(甚至是列表的列表)。比如上图中我们需要抓取一个电影信息列表;
频繁但简单的格式处理。例如:将电影的评分从字符串类型转化为数字,去除电影链接中不需要的 url 参数。
temme
就是基于以上几点观察而开发出来的处理 HTML 的工具。temme 在 CSS 选择器的基础上,针对以上三点,加入了额外的语法来优雅地处理上述情况:
支持同时使用多个选择器;支持多个字段同时抓取;
支持列表抓取;
支持格式处理。
安装与使用
# 全局安装
yarn global add temme # npm install --save temme
# 最基本的使用方式
temme <selector> <html>
# 省略html参数,使用来自stdin的输入;--format 参数表示格式化输出
temme <selector> --format
# 使用文件中的选择器
temme <path-to-a-selector-file> <html>
# 和 curl 配合使用
curl -s <url> | temme <selector>
temme 提供了一个 在线网页版本,其中的编辑器提供了语法高亮功能。本文的剩下的部分也可以在该在线版本中进行,注意将对应的 HTML 复制过来即可。
例子一:从豆瓣电影首页抓取电影信息
抓取第一个电影的标题,评分以及链接。temme 选择器如下:
命令行运行步骤如下:
curl -s https://movie.douban.com | temme '.ui-slide-item[data-title=$title data-rate=$rate]; .ui-slide-item a[href=$url];' --format
# output:
# "title": "烟花 打ち上げ花火、下から見るか?横から見るか?",
# "rate": "5.7",
# "url": "https://movie.douban.com/subject/26930504/?from=showing"
例子中的选择器和 CSS 选择器非常相似,不一样的地方在于 temme 选择器包含了下面这样的结构:[foo=$bar]
。该结构的含义是「将 foo 属性放到结果的 bar 字段」。上面的选择器包含了三个这样的结构,一次性选取了三个字段。上面的选择器也同时包含了两个子选择器(在图中每行一个),每个子选择器用分号作为结束符。
另一个常见的结构是 div{$buzz}
,该结构表示「将 div 元素的文本内容放到结果的 buzz 字段」。如果熟悉 emmet 的话,可以看出来目前 temme 的行为就是 emmet 的逆过程。
例子二:格式变换
上面结果中 rate
是个字符串,我们可以用过滤器 Number
对其进行处理。我们这次不选取其他字段。
curl -s https:
# output: {"rate":5.7}
可以看到结果中 rate
字段类型为数字。目前结果中只有 rate
一个字段,那么将该字段的值直接作为结果更为方便:
curl -s https://movie.douban.com | temme '.ui-slide-item[data-rate=$|Number];'
省略 $xxx
中的 xxx
,那么结果的格式会从 { xxx: yyy }
变为 yyy
。
例子三:「正在热映」列表
「正在热映」是一个列表,每一个电影信息对应一个满足 CSS 选择器 .ui-slide-item[data-title]
的 HTML 元素。上面的例子我们只选取了第一个电影的数据,这里我们使用 @
符号来选取该列表。抓取「正在热映」列表中所有电影的信息,选择器如下:
运行效果如下:
curl -s https://movie.douban.com | temme '.ui-slide-item[data-title] @recentMovies { &[data-title=$title data-rate=$rate|Number]; a[href=$url]; }' --format
# output:
# "recentMovies": [
# "title": "烟花 打ち上げ花火、下から見るか?横から見るか?",
# "rate": 5.7,
# "url": "https://movie.douban.com/subject/26930504/?f rom=showing"
# "title": "相声大电影之我要幸福",
# "rate": 0,
# "url": "https://movie.douban.com/subject/26811605/?f rom=showing"
# ......
选择器含义:每一个满足 CSS 选择器 .ui-slide-item[data-title]
的 HTML 元素就是一个电影详情的父元素,我们将 @
放在该选择器之后,紧跟的 recentMovies
表示「最近热映列表」在最终结果中的字段名,然后我们在花括号中放入例子一中的两个选择器,以选取单个电影的数据。
如果我们在这里省略 @recentMovies
中的 recentMoviews
,仅保留一个 @
符号,那么最终结果就会变为一个数组(JSON 的层级会减一层)。
列表的捕获可以进行嵌套。例如在一个 stackoverflow 问题页面中有多个回答,每个回答下有多个评论,下面的选择器可以将这些评论以二维列表的格式抓取下来:
curl -s https://stackoverflow.com/questions/1014861/is-there-a-css-parent-selector | temme '.answer@{ .comment@{ .comment-body{$|trim}; }; };'
例子四:电影详情页面
在首页爬取到电影链接列表之后,我们可以进入每个电影的页面爬取该电影的详细数据。这里我们以 烟花 这个电影为例子。电影介绍页面中的数据非常详细,包含了电影名称、导演、编剧、主演、电影类型、官方网站等信息。这里挑取了部分数据进行抓取,选择器如下:
[property="v:itemreviewed"]{$title};
.year{$year|substring(1, 5)|Number};
[rel="v:directedBy"]@directedBy { &{$} };
:contains('编剧') + span{$storyFrom|split('/')||trim};
[rel="v:starring"]@starring|slice(0, 3){ &{$} };
[property="v:average"]{$avgRating|Number};
.ratings-on-weight .item@ratingInfo{
span[title=$title];
.rating_per{$percentage};
[property="v:summary"]{$summary|trim};
.recommendations-bd dl@recommendations{
img[alt=$name src=$imgUrl];
a[href=$url];
这里选择器较长,写在终端中不太方便,我们将该选择器保存到文件 douban-movie.temme,然后运行 temme:
curl -s https://movie.douban.com/subject/26930504/ | temme douban-movie.temme --format
# output:
# "title": "烟花 打ち上げ花火、下から見るか?横から見るか?",
# "year": 2017,
# "directedBy": [ "新房昭之", "武内宣之" ],
# "storyFrom": [ "岩井俊二", "大根仁" ],
# "starring": [ "广濑铃", "菅田将晖", "宫野真守" ],
# "avgRating": 5.4,
# "ratingInfo": [
# { "title": "力荐", "percentage": "7.2%" },
# { "title": "推荐", "percentage": "12.8%" },
# ......
# "summary": "川村元气即将再度与《你的名字。》制......",
# "recommendations": [
# "name": "想要传达你的声音",
# "imgUrl": "https://img3.doubanio.com/vie......",
# "url": "https://movie.douban.com/subject......"
# ......
该选择器虽然选取了很多内容,但是仍然保持了清晰的结构以及良好的可读性。可以打开该例子的在线版本,对比其中选择器和输出的格式,应该可以明白该选择器的含义。
写爬虫的时候,我们首先分析页面结构,利用在线版本为每一种不同类型的页面写好对应的选择器,然后将选择器保存在本地文件中。爬虫运行获取到 HTML 之后,我们读取相应的选择器文件,运行并得到想要的输出。
总结与其他
上面的介绍基本涉及到了 temme 的核心用法,可以看到 temme 实现了前面提到的改进思路。实践中大部分网站的页面结构都是比较清晰的,分析页面元素的 CSS 选择器也比较容易,此时使用 temme 可以大大提高数据选取的效率。temme 更完整的用法和文档还请移步 Github,欢迎 fork 和 star。
下面列举一些开发用到的主要技术:
开发语言 TypeScript
自定义语法解析 PEG.js
HTML解析 cheerio
编译工具 webpack
自动化测试 Jest
在线版本编辑器 ace
灵感来自 emmet
- 563
-
superZidan
JavaScript
React.js
- 2.1w
-
CUGGZ
JavaScript
React.js
- 4320
-
Defineee
three.js
JavaScript
- 863
-
threerocks
JavaScript
- 669
-
margin_100px
uni-app
JavaScript
- 9911
-
王士江WangJohn
JavaScript
- 838
-
老骥farmer
JavaScript
Vue.js
- 599
-
XaYvier
Element
Vue.js
- 1.1w
-
XaYvier
JavaScript
Immutable.js
- 1.0w
-
XaYvier
Vue.js
Markdown
GitHub
- 1.4w
-
XaYvier
Visual Studio Code