爬虫专题

用 jsoup 分析下载的 html 内容

2018-11-20  本文已影响0人  阿土伯已经不是我

下载内容的分类

从前一篇文章,我们可以看到,下载并需要进行分析和处理的内容基本上就是 html 和 json 两类。其余的图片、视频、 PDF 和其他的二进制文件则一般都是直接保存即可,不在本文的讨论范围内。本文先讨论如何分析 html 内容

jsoup 分析 html 内容

对 html 的分析首先进行 html 元素的查找,找到对应的元素后再从元素中获取我们需要的属性值。对前端开发比较熟悉的同学就比较容易想到,对 html 元素的定位可以采用 css 选择器的方式。java中对 HTML 分析有一个利器 jsoup 就是采用这种方式对 html 的元素进行查找并进行后续的处理的。他的语法是一种类似于 jquery 的 css 选择器语法。

在上一篇文章里面,我们已经演示了如何将简书的一篇文章通过 OkHttp3 库下载下来。下载下来不是我们的目的,我们还需要分析他。例如如果我想分析得到文章的标题、作者,发布时间,字数、阅读数量、评论数量、喜欢数量、正文内容,我们就可以用 jsoup 来对要分析的元素进行定位并且取出我们想要的内容。

首先我们要对这篇文章的网页的结构进行分析。如果要找到文章的标题,那么,在我使用的 chrome 浏览器中,将鼠标悬停在标题的文字 “是什么支撑了淘宝双十一,没错就是它java编程语言。”,然后鼠标右键打开右键菜单,在菜单中选择 “检查” 菜单项。如下图所示:


元素检查

结果如下所示:


元素 html 代码

从这里我们可以看到,标题的内容是 一个 class 为 article 的 div 元素下的 h1 标签的文本内容,那么按照 jsoup 的 css 语法,找到改元素的表达式 为 div.article h1.title ,当然,如果 h1.title 甚至 h1 应该也可以,这里主要是为了保证表达式定位到的元素是唯一的,所以谨慎一些,多增加了些限定条件。
同样的方法,我们可以找到如下数据的元素的对应关系
作者:div.article div.author span.name
发布时间:div.article div.author span.publish-time
字数:div.article div.author span.wordage
阅读数:div.article div.author span.views-count
评论数:div.article div.author span.comments-count
喜欢数:div.article div.author span.likes-count
文章正文:div.article div.show-content

这里所有的数据都存储在上述元素的文本内容中。所以以上一篇文章中下载 html 的代码为基础,html 内容分析的代码如下:

首先在 maven 中引入 jsoup 依赖

        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.11.3</version>
        </dependency>

然后根据上述的分析编写 joup 分析代码

        OkHttpClient httpClient = new OkHttpClient.Builder().build();
        String url = "https://www.jianshu.com/p/675ea919230e";
        Request request = new Request.Builder()
            .url(url)
            .build();
        try {
            Response response = httpClient.newCall(request).execute();
            if (response.isSuccessful() && response.body() != null) {
                Document document = Jsoup.parse(response.body().string());
                Element titleElement = document.selectFirst("div.article h1.title");
                Element authorElement = document.selectFirst("div.article div.author span.name");
                Element timeElement = document.selectFirst("div.article span.publish-time");
                Element wordCountElement = document.selectFirst("div.article span.wordage");
                Element viewCountElement = document.selectFirst("div.article span.views-count");
                Element commentCountElement = document.selectFirst("div.article span.comments-count");
                Element likeCountElement = document.selectFirst("div.article span.likes-count");
                Element contentElement = document.selectFirst("div.article div.show-content");
                if (titleElement != null) {
                    System.out.println("标题:" + titleElement.text());
                }
                if (authorElement != null) {
                    System.out.println("作者:" + authorElement.text());
                }
                if (timeElement != null) {
                    System.out.println("发布时间:" + timeElement.text());
                }
                if (wordCountElement != null) {
                    System.out.println(wordCountElement.text());
                }
                if (viewCountElement != null) {
                    System.out.println(viewCountElement.text());
                }
                if (commentCountElement != null) {
                    System.out.println(commentCountElement.text());
                }
                if (likeCountElement != null) {
                    System.out.println(likeCountElement.text());
                }

                if (contentElement != null && contentElement.text() != null) {
                    System.out.println("正文长度:" + contentElement.text().length());
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
        }

执行该代码后,运行结果如下所示

标题:是什么支撑了淘宝双十一,没错就是它java编程语言。
作者:Java帮帮
发布时间:2018.08.29 14:49
字数 561
正文长度:655

这样,我们就通过 jsoup 从 html 代码里面分析出了我们想要的结果。细心的朋友可能会看到。输出的结果中少了阅读数、评论数、喜欢数三个数据。这个是因为这三个是浏览器动态渲染的结果,直接通过 http 下载的内容是没有这部分内容的。直接下载时,这个区域的内容是下面的

        <div class="author">
          <a class="avatar" href="/u/abc8bfe200ba">
            <img src="//upload.jianshu.io/users/upload_avatars/5422542/6d58d854-dfe9-4dd6-a694-8eb63f1d9efc.png?imageMogr2/auto-orient/strip|imageView2/1/w/96/h/96" alt="96" />
</a>          <div class="info">
            <span class="name"><a href="/u/abc8bfe200ba">Java帮帮</a></span>
            <!-- 关注用户按钮 -->
            <div props-data-classes="user-follow-button-header" data-author-follow-button></div>
            <!-- 文章数据信息 -->
            <div class="meta">
              <!-- 如果文章更新时间大于发布时间,那么使用 tooltip 显示更新时间 -->
                <span class="publish-time">2018.08.29 14:49</span>
              <span class="wordage">字数 561</span>
            </div>
          </div>
          <!-- 如果是当前作者,加入编辑按钮 -->
        </div>

可以看到,他是没有后面三个内容的。如何处理这种动态渲染的网页的问题,我们后面再讲。

一个网站的内容页面的结构应该是相同或者基本相同的。所以我们分析出一个或者少数几个页面的结构,就可以将这个规则应用到其他的网页上去。只要我们能不断的找到这些文章的链接,那么我们就可以将简书网站所有的文章都下载并且结构化保存起来。

jsoup css 语法介绍

上一节里面讲了如何用 jsoup 来分析一个 html 页面的内容。 jsoup 的语法比演示中的要更灵活和更丰富。这里简要介绍一下。大部分的只要有前端开发经验的一看就可以明白。

单一选择器

组合选择器

很多情况下,单一选择器的表达能力太弱,不能有效的将想要的元素定位出来,这个时候可以用组合选择器的方式来增强单一选择器的能力。

  1. 任意组合:可以任意组合多个单一选择器。例如 a[href].btn[href^='/sign'] 用来选择 有 href 属性并且存在 btn class 并且 href 属性值以 /sign 开头的 a 标签元素
  2. 祖先后代组合:语法为 A B 。标识某个 A 元素 下的所有后代 B 元素。这里的 A B 都可以是单一选择器或者任意选择组合选择器。例如 div.article span.name 表达是会选择出 div 标签的 class 有 article 的元素内的所有 class 值有 name 的 span 元素
  3. 父子组合:语法为 A > B 。 表示某个 A 元素下的直接子元素 B。这里的 A B 都可以是单一选择器或者任意选择组合选择器。和祖先后代组合的区别是祖先后代组合会选择出 A 下的所有 B 元素,而父子组合选择器只会现在 A 的直接子元素中的 B 元素
  4. 同级之后相邻组合: 语法为 A + B 。表示元素 A 之后的同级的第一个 B 元素。很多资料写的是 A 元素的之前同级的第一个 B 元素。实测是之后。
  5. 同级之后组合:语法为 A ~ B 。标识原始 A 之后的同级的所有 B 元素。也有很多资料写的是之前,这里还是应该是之后。

伪选择器

伪选择器不能独立存在,他是依附于一个某个单一选择器。

  1. 序号小于选择器:语法为 :lg(n) 。表示在父节点的下属节点中,位置序号小于 n 的元素。
  2. 序号大于选择器:语法为:gt(n) 。示在父节点的下属节点中,位置序号大于 n 的元素。
  3. 需要等于选择器:语法为:eq(n) 。表示在父节点的下属节点中,位置选好等于 n 的元素。
  4. 包含文本选择器:语法为 :contain(text) 。例如 strong:contains(Year) 表达式会选择出 包含了 Year 文本的 strong 元素
  5. 直接包含文本选择器:语法为:containsOwn(text) 。这个和包含文本选择器的区别是包含文本选择器会去匹配自己和下属元素中的文本,而这个则只匹配元素下直接的文本字符串。
  6. 包含选择器:语法为:has(seletor) 。表示包含另一个元素选择器的元素。括号里面是另一个选择器,那么就是说他能通过递归的方式将这个表达式写得很复杂。例如 ul li:has(strong:contains(Year)) 这表达式会选择出 ul 的 后代 li 元素中包含 stong 元素并且这个 strong 元素包含文本 Year 的元素。
  7. 文本匹配选择器: 语法为:matches(regex)。表示通过正则表达式匹配元素下的文本
  8. 直接文本匹配选择器:语法为:matchesOwn(regex) 。标识通过正则表达式匹配元素下的直接文本。

jsoup 获取元素的值

  1. 取元素属性值内容:Element.attr(String attributeName)
  2. 取元素的文本内容:Element.text()
  3. 获取元素的 html 内容:Element.html() 或者 Element.outerHtml()
  4. 获取元素的标签名:Element.tagName()
  5. 判断元素是否有 class :Element.hasClass(String className)

后记

通过本文的介绍,我们就可以比较容易的对 html 的内容进行结构化的抽取和分析,为后续的业务需求提供数据基础了。

上一篇下一篇

猜你喜欢

热点阅读