Java实现爬虫给App提供数据(Jsoup 网络爬虫)

时间:2021-11-06 03:36:20

一、需求

最近基于 Material Design 重构了自己的新闻 App,数据来源是个问题。

有前人分析了知乎日报、凤凰新闻等 API,根据相应的 URL 可以获取新闻的 JSON 数据。为了锻炼写代码能力,笔者打算爬虫新闻页面,自己获取数据构建 API。

二、效果图

下图是原网站的页面

Java实现爬虫给App提供数据(Jsoup 网络爬虫)

爬虫获取了数据,展示到 APP 手机端

Java实现爬虫给App提供数据(Jsoup 网络爬虫)

三、爬虫思路

Java实现爬虫给App提供数据(Jsoup 网络爬虫)

关于App 的实现过程可以参看这几篇文章,本文主要讲解一下如何爬虫数据。

Jsoup 简介

Jsoup 是一个 Java 的开源HTML解析器,可直接解析某个URL地址、HTML文本内容。

Jsoup主要有以下功能:

  • - 从一个URL,文件或字符串中解析HTML;
  • - 使用DOM或CSS选择器来查找、取出数据;
  • - 对HTML元素、属性、文本进行操作;
  • - 清除不受信任的HTML (来防止XSS攻击)

四、爬虫过程

Get 请求获取网页 HTML

新闻网页Html的DOM树如下所示:

Java实现爬虫给App提供数据(Jsoup 网络爬虫)

下面这段代码根据指定的 url,用代码获取get 请求返回的 html 源代码。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static String doGet(String urlStr) throws CommonException {
 URL url;
 String html = "";
 try {
 url = new URL(urlStr);
 HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 connection.setRequestMethod("GET");
 connection.setConnectTimeout(5000);
 connection.setDoInput(true);
 connection.setDoOutput(true);
 if (connection.getResponseCode() == 200) {
 InputStream in = connection.getInputStream();
 html = StreamTool.inToStringByByte(in);
 } else {
 throw new CommonException("新闻服务器返回值不为200");
 }
 } catch (Exception e) {
 e.printStackTrace();
 throw new CommonException("get请求失败");
 }
 return html;
}

InputStream in = connection.getInputStream();将得到输入流转化为字符串是个普遍需求,我们将其抽象出来,写一个工具方法。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
public class StreamTool {
 public static String inToStringByByte(InputStream in) throws Exception {
 ByteArrayOutputStream outStr = new ByteArrayOutputStream();
 byte[] buffer = new byte[1024];
 int len = 0;
 StringBuilder content = new StringBuilder();
 while ((len = in.read(buffer)) != -1) {
 content.append(new String(buffer, 0, len, "UTF-8"));
 }
 outStr.close();
 return content.toString();
 }
}

五、解析 HTML 获取标题

利用 google 浏览器的审查元素,找出新闻标题对于的html 代码:

?
1
2
3
4
5
6
7
<div id="article_title">
 <h1>
 <a href="http://see.xidian.edu.cn/html/news/7428.html">
 关于举办《经典音乐作品欣赏与人文审美》讲座的通知
 </a>
 </h1>
</div>

我们需要从上面的 HTML 中找出id="article_title"的部分,使用 getElementById(String id) 方法

?
1
2
3
4
5
6
7
8
9
String htmlStr = HttpTool.doGet(urlStr);
 
// 将获取的网页 HTML 源代码转化为 Document
Document doc = Jsoup.parse(htmlStr);
 
Element articleEle = doc.getElementById("article");
// 标题
Element titleEle = articleEle.getElementById("article_title");
String titleStr = titleEle.text();

六、获取发布日期、信息来源

同样找出对于的 HTML 代码

?
1
2
3
4
5
6
7
8
9
10
11
<html>
 <head></head>
 <body>
 <div id="article_detail">
 <span> 2015-05-28 </span>
 <span> 来源: </span>
 <span> 浏览次数: <script language="JavaScript" src="http://see.xidian.edu.cn/index.php/news/click/id/7428">
 </script> 477 </span>
 </div>
 </body>
</html>

思路也和上面类似,使用 getElementById(String id) 方法找出id="article_detail"为Element,再利用getElementsByTag获取span 部分。因为一共有3个<span> ... </span>,所以返回的是Elements而不是Element。

?
1
2
3
4
5
6
7
8
9
// article_detail包括了 2016-01-15 来源: 浏览次数:177
Element detailEle = articleEle.getElementById("article_detail");
Elements details = detailEle.getElementsByTag("span");
 
// 发布时间
String dateStr = details.get(0).text();
 
// 新闻来源
String sourceStr = details.get(1).text();

七、解析浏览次数

如果打印出上面的details.get(2).text(),只会得到

浏览次数:
没有浏览次数?为什么呢?

因为浏览次数是JavaScript 渲染出来的, Jsoup爬虫可能仅仅提取HTML内容,得不到动态渲染出的数据。
解决方法有两种

  • 在爬虫的时候,内置一个浏览器内核,执行js渲染页面后,再抓取。这方面对应的工具有Selenium、HtmlUnit或者PhantomJs。
  • 所以分析JS请求,找到对应数据的请求url

如果你访问上面的 urlhttp://see.xidian.edu.cn/index.php/news/click/id/7428,会得到下面的结果

?
1
document.write(478)

这个478就是我们需要的浏览次数,我们对上面的url做get 请求,得到返回的字符串,利用正则找出其中的数字。

?
1
2
3
4
5
// 访问这个新闻页面,浏览次数会+1,次数是 JS 渲染的
String jsStr = HttpTool.doGet(COUNT_BASE_URL + currentPage);
int readTimes = Integer.parseInt(jsStr.replaceAll("\D+", ""));
// 或者使用下面这个正则方法
// String readTimesStr = jsStr.replaceAll("[^0-9]", "");

八、解析新闻内容

本来是获取新闻内容纯文字的形式,但后来发现 Android 端也可以显示 CSS 格式,所以后来内容保留了 HTML 格式。

?
1
2
3
4
5
6
Element contentEle = articleEle.getElementById("article_content");
// 新闻主体内容
String contentStr = contentEle.toString();
// 如果用 text()方法,新闻主体内容的 html 标签会丢失
// 为了在 Android 上用 WebView 显示 html,用toString()
// String contentStr = contentEle.text();

九、解析图片 Url

注意一个网页上大大小小的图片很多,为了只获取新闻正文中的内容,我们最好首先定位到新闻内容的Element,然后再利用getElementsByTag(“img”)筛选出图片。

?
1
2
3
4
5
6
7
8
9
10
11
12
Element contentEle = articleEle.getElementById("article_content");
// 新闻主体内容
String contentStr = contentEle.toString();
// 如果用 text()方法,新闻主体内容的 html 标签会丢失
// 为了在 Android 上用 WebView 显示 html,用toString()
// String contentStr = contentEle.text();
 
Elements images = contentEle.getElementsByTag("img");
String[] imageUrls = new String[images.size()];
for (int i = 0; i < imageUrls.length; i++) {
 imageUrls[i] = images.get(i).attr("src");
}

十、新闻实体类 JavaBean

上面获取了新闻的标题、发布日期、阅读次数、新闻内容等等,我们自然需要构造一个 javabean,把获取的内容封装进实体类中。

  1. public class ArticleItem { 
  2.  
  3.  private int index; 
  4.  private String[] imageUrls; 
  5.  private String title; 
  6.  private String publishDate; 
  7.  private String source; 
  8.  private int readTimes; 
  9.  private String body; 
  10.  
  11.  public ArticleItem(int index, String[] imageUrls, String title, String publishDate, String source, int readTimes, 
  12.  String body) { 
  13.  this.index = index; 
  14.  this.imageUrls = imageUrls; 
  15.  this.title = title; 
  16.  this.publishDate = publishDate; 
  17.  this.source = source; 
  18.  this.readTimes = readTimes; 
  19.  this.body = body; 
  20.  } 
  21.  
  22.  @Override 
  23.  public String toString() { 
  24.  return "ArticleItem [index=" + index + ", imageUrls=" + Arrays.toString(imageUrls) + ", title=" + title 
  25.  + ", publishDate=" + publishDate + ", source=" + source + ", readTimes=" + readTimes + ", body=" + body 
  26.  + "]"
  27.  } 
  28.  
  29.  
 

测试

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public static ArticleItem getNewsItem(int currentPage) throws CommonException {
 // 根据后缀的数字,拼接新闻 url
 String urlStr = ARTICLE_BASE_URL + currentPage + ".html";
 
 String htmlStr = HttpTool.doGet(urlStr);
 
 Document doc = Jsoup.parse(htmlStr);
 
 Element articleEle = doc.getElementById("article");
 // 标题
 Element titleEle = articleEle.getElementById("article_title");
 String titleStr = titleEle.text();
 
 // article_detail包括了 2016-01-15 来源: 浏览次数:177
 Element detailEle = articleEle.getElementById("article_detail");
 Elements details = detailEle.getElementsByTag("span");
 
 // 发布时间
 String dateStr = details.get(0).text();
 
 // 新闻来源
 String sourceStr = details.get(1).text();
 
 // 访问这个新闻页面,浏览次数会+1,次数是 JS 渲染的
 String jsStr = HttpTool.doGet(COUNT_BASE_URL + currentPage);
 int readTimes = Integer.parseInt(jsStr.replaceAll("\D+", ""));
 // 或者使用下面这个正则方法
 // String readTimesStr = jsStr.replaceAll("[^0-9]", "");
 
 Element contentEle = articleEle.getElementById("article_content");
 // 新闻主体内容
 String contentStr = contentEle.toString();
 // 如果用 text()方法,新闻主体内容的 html 标签会丢失
 // 为了在 Android 上用 WebView 显示 html,用toString()
 // String contentStr = contentEle.text();
 
 Elements images = contentEle.getElementsByTag("img");
 String[] imageUrls = new String[images.size()];
 for (int i = 0; i < imageUrls.length; i++) {
 imageUrls[i] = images.get(i).attr("src");
 }
 
 return new ArticleItem(currentPage, imageUrls, titleStr, dateStr, sourceStr, readTimes, contentStr);
 
}
 
public static void main(String[] args) throws CommonException {
 System.out.println(getNewsItem(7928));
}

输出信息

?
1
2
3
4
5
6
7
8
ArticleItem [index=7928,
 imageUrls=[/uploads/image/20160114/20160114225911_34428.png],
 title=电院2014级开展“让诚信之花开遍冬日校园”教育活动,
 publishDate=2016-01-14,
 source=来源: 电影新闻网,
 readTimes=200,
 body=<div id="article_content">
 <p style="text-indent:2em;" align="justify"> <strong><span style="font-size:16px;line-height:1.5;">西电新闻网讯</span></strong><span style="font-size:16px;line-height:1.5;"> (通讯员</span><strong><span style="font-size:16px;line-height:1.5;"> 丁彤 王朱丹</span></strong><span style="font-size:16px;line-height:1.5;">...)

本文讲解了如何实现Jsoup 网络爬虫,如果文章对您有帮助,那就给个赞吧。