Java 爬知乎某个问题下的所有图片

前言

网上有许多关于知乎的爬虫,但都是用 Python 来实现的,由于我的主语言是 Java 所以想用 Java 来实现下。

本次用到了一个国人开发的优秀的爬虫框架:WebMagic

思路

首先打开知乎的一个问题 https://www.zhihu.com/question/43551423,然后打开 FireFox 的 F12 控制台,然后发现知乎的问题需要翻页,且是无刷新的请求,那必然是 AJAX 请求了。

筛选一下,发现一个可疑的东西,以 answers 开头的,应该就是回答内容的请求了。

观察一下这个请求:https://www.zhihu.com/api/v4/questions/43551423/answers?include=data[*].is_normal,admin_closed_comment,reward_info,is_collapsed,annotation_action,annotation_detail,collapse_reason,is_sticky,collapsed_by,suggest_edit,comment_count,can_comment,content,editable_content,voteup_count,reshipment_settings,comment_permission,created_time,updated_time,review_info,question,excerpt,relationship.is_authorized,is_author,voting,is_thanked,is_nothelp,upvoted_followees;data[*].mark_infos[*].url;data[*].author.follower_count,badge[?(type=best_answerer)].topics&offset=0&limit=20&sort_by=default

可以发现其中的 43551423 代表的就是该问题的 ID 号,请求参数中的参数 include 为请求的内容,offset 为偏移量(表示从多少页开始请求),limit 为本页的答案数量(最大为 20),sort_by 为排序方式。其实我们只需要关注 offset 即可,其他的默认就好。

有了思路以后,我们先用 Postman 来测试一下:

但是得到了这样的结果,他告诉我 AuthenticationInvalidRequest 认证无效,看来是需要添加认证,那我们再去浏览器看下,刚才的请求头中还有什么信息传递了过去,然后发现了这个东东:

我们把这个添加到请求头中,发现可以正常得到数据了:

观察一下这个数据发现,这是 20 条用户的回答,因为我们请求的参数 limit 为 20 ,所以这里为 20 条,那么我们可以根据这个来判断是否翻页结束,每次 limit 都自增 20,直到得到的数据不满 20 条,则代表翻页结束,停止爬取。

然后得到了数据,就开始解析图片下载地址吧:

img 元素就是我们要爬取的图片,可以看到 data-original 属性的内容与 src 属性的内容都是图片的地址,但验证后发现,src 可能是缩略图,所以我们还是选择 data-original 属性的图片地址。

得到图片地址后,下载到本地就可以了,直接看代码吧!

上代码!

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package com.yfzz.zhihu3 ;

import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Html;
import us.codecraft.webmagic.selector.JsonPathSelector;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;

public class ZhihuQuestion implements PageProcessor {

private Site site = Site.me().setSleepTime(2000)
.setCycleRetryTimes(5)
.setRetryTimes(5)
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36")
.addHeader("authorization", "你的认证信息")
.setCharset("UTF-8");

private static String questionID = "37787176"; // 要爬取的问题 ID 号
private static String filePath = "e://zhihu"; // 文件存放路径,程序会自动在此路径后添加一级目录为问题标题
private static int offset = 0; // 偏移量,表示从第 n 个答案开始获取,limit 表示获取多少个(上限为20)
private static int count = 0; // 下载到的图片总数

@Override
public void process(Page page) {
Html html = page.getHtml();
// 得到当前页的所有答案的 ID 号,主要用处是为了判断是否到页尾。
List<String> idList = new JsonPathSelector("$.data[*].id").selectList(page.getRawText());
String title = new JsonPathSelector("$.data[*].question.title").selectList(page.getRawText()).get(0);

int getSize = idList.size(); // 当前页获取到的回答个数
if (getSize == 20) {
// 将下一页添加到队列中
offset += 20;
String url = "https://www.zhihu.com/api/v4/questions/" + questionID + "/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cupvoted_followees%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&offset=" + offset + "&limit=20&sort_by=default";
page.addTargetRequest(url);
}

// 根据正则匹配图片地址并去除特殊字符。
// 例: 正则得到的图片链接 : \&quot;https://pic2.zhimg.com/v2-94ae015b8e1bd2e0dc9bdd7d7da6d7ed_r.jpg\&quot; 需要去除特殊字符 &quot;
List<String> imgList = html.regex("data-original=\"(.*?)\"").replace("\\\\&quot;", "").all();

// 图片链接去重复
HashSet<String> set = new HashSet<String>(imgList);
count += set.size();

System.out.println("正在下载第" + offset + "-" + (offset + getSize) + "个回答的图片,当前页图片数量为:" + set.size() + ",目前总图片数量:" + count);
for (String url : set) {
String fileName = url.substring(url.lastIndexOf('/') + 1, url.length());
try {
String savePath = filePath + "/" + title + "_" + questionID; // 下载路径:指定路径/标题/问题ID
downloadPicture(url, savePath, fileName);
} catch (Exception e) {
e.printStackTrace();
}
}
}


public static void downloadPicture(String urlString, String savePath, String filename) throws Exception {
File file = new File(savePath + File.separator + filename);

if (!new File(savePath).exists()) {
System.out.println("下载目录不存在,已创建:" + savePath);
new File(savePath).mkdirs();
}
if (file.exists()) {
System.out.println("文件已存在,跳过该文件:" + file.getName());
return;
}

OutputStream os = new FileOutputStream(file);
// 构造URL
URL url = new URL(urlString);
// 打开连接
URLConnection con = url.openConnection();
//设置请求超时为5s
con.setConnectTimeout(5 * 1000);
// 输入流
InputStream is = con.getInputStream();

// 1K的数据缓冲
byte[] bs = new byte[1024];
// 读取到的数据长度
int len;
// 开始读取
while ((len = is.read(bs)) != -1) {
os.write(bs, 0, len);
}
// 完毕,关闭所有链接
os.close();
is.close();
}

@Override
public Site getSite() {
return site;
}

public static void main(String[] args) throws Exception {
Spider.create(new ZhihuQuestion()).
thread(1).
addUrl("https://www.zhihu.com/api/v4/questions/" + questionID + "/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cupvoted_followees%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&offset=0&limit=20&sort_by=default")
.run();
}
}

爬取结果展示


前 20 个回答就有 1050 张图片!!!
然后我用浏览器打开了这个网页,发现……

竟然有 9559 个回答,我的天,我还是停了吧,估计下载完这些,我这小硬盘都要满了,然后看了下已经下载完成的。

嗯,按照这个情况,下载完这些,估计上 10G 了,那么知乎这么多钓鱼贴,咳咳,自己理解吧。

总结

这只是一个简单的例子,为了防止给知乎的服务器带了太大的压力,这里我的代码是写成了单线程的方式。后续我会再更新一些关于 Java 的爬虫以及详细思路,有什么问题可以在评论里给我留言。

坚持原创技术分享,您的支持将鼓励我继续创作!