本文介绍了一个从 html 提取 JSON 数据的工具,并以豆瓣电影的例子展示了该工具的使用方法。本文中用到了大量的 CSS 选择器,CSS 选择器可以参考 MDN。
最近几个月写 Node 爬虫比较多,解析 HTML 文档用的工具是 cheerio(cheerio 可以认为是服务器版的 jQuery)。cheerio 功能相当丰富,提供了一大堆 API 来查询/修改/删除/添加结点或文本。不过随着爬取的页面数量越来越多,大量使用 cheerio 还是显得繁琐了一点。爬虫对于处理 HTML 的模式其实比较固定,但是 cheerio 处理某些模式时不够简洁明了,下面三点就是一些比较常见的情况:
下面的三点中,假设我们要从豆瓣电影首页中爬取上图这样一个 「正在热映」列表。注意该列表是实时更新的,所以本文中下面的选择器的运行结果可能不同。
,链接
- title
和评分
- url
字段;
- rate
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 字段」。如果熟悉 emmet 的话,可以看出来目前 temme 的行为就是 emmet 的逆过程。
- div{$buzz}
上面结果中
是个字符串,我们可以用过滤器
- rate
对其进行处理。我们这次不选取其他字段。
- Number
- curl -s https://movie.douban.com | temme '.ui-slide-item[data-rate=$rate|Number];'
- # output: {"rate":5.7}
可以看到结果中
字段类型为数字。目前结果中只有
- rate
一个字段,那么将该字段的值直接作为结果更为方便:
- rate
- curl - s https: //movie.douban.com | temme '.ui-slide-item[data-rate=$|Number];'
- #output: 5.7
省略
中的
- $xxx
,那么结果的格式会从
- xxx
变为
- { xxx: yyy }
。
- yyy
「正在热映」是一个列表,每一个电影信息对应一个满足 CSS 选择器
的 HTML 元素。上面的例子我们只选取了第一个电影的数据,这里我们使用
- .ui-slide-item[data-title]
符号来选取该列表。抓取「正在热映」列表中所有电影的信息,选择器如下:
- @
运行效果如下:
- 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 选择器
的 HTML 元素就是一个电影详情的父元素,我们将
- .ui-slide-item[data-title]
放在该选择器之后,紧跟的
- @
表示「最近热映列表」在最终结果中的字段名,然后我们在花括号中放入例子一中的两个选择器,以选取单个电影的数据。
- 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是来自jQuery的选择器 https://api.jquery.com/contains-selector/)
- :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。
下面列举一些开发用到的主要技术:
来源: https://juejin.im/entry/5a291fd86fb9a0450002f3b0