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 属性的图片地址。

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

上代码!

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116

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 的爬虫以及详细思路,有什么问题可以在评论里给我留言。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java架构师学习

分布式消息队列Apache RocketMQ源码剖析-Producer分析正文总结

正文 首先我们看一下Producer的继承结构: ? image.png MQAdmin主要包含一些管理性的接口,比如创建topic、查询某个特定消息以方便排查...

42770
来自专栏青玉伏案

iOS开发之集成iOS9中的Core Spotlight Framework搜索App的内容

Spotlight在iOS9上做了一些新的改进, 也就是开放了一些新的API, 通过Core Spotlight Framework你可以在你的app中集成S...

24160
来自专栏西二旗一哥

iOS - autoreleasepool and @autoreleasepool

+ 在一个自动引用计数的环境中(并不是垃圾回收机制),一个包含了多个对象的 NSAutoreleasePool 对象能够接收 autorelease 消息并且...

18040
来自专栏逆向技术

64位内核第二讲,进程保护之对象钩子

         64位内核第二讲,进程保护. 一丶什么是保护. 什么是保护. 比如我们安装了xxx杀毒软件.那么此时你用任务管理器关闭.是关闭不了的.原因是内...

35660
来自专栏一“技”之长

iOS多线程编程之一——NSThread线程管理

NSTread是iOS中进行多线程开发的一个类,其结构逻辑清晰,使用十分方便,但其封装度和性能不高,线程周期,加锁等需要手动处理。

9930
来自专栏Android 研究

Android Handler机制3之SystemClock类

官网位置在https://developer.android.com/reference/android/os/SystemClock.html

19920
来自专栏雪胖纸的玩蛇日常

Uncaught SyntaxError: Unexpected token ' in JSON at position 1

1.4K30
来自专栏hotqin888的专栏

engineercms利用pdf.js制作连续看图功能

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hotqin888/article/det...

19310
来自专栏小蠢驴iOS专题

iOS - 模型数据持久化保存实现

24140
来自专栏ASP.NETCore

MVVM绑定多层级数据到TreeView并设置项目展开

昨天在做项目的时候碰到了这个问题,发现通常我们定义的数据不法绑定到控件上,接下来我将讲一下我是怎么解决这个问题的。

14920

扫码关注云+社区

领取腾讯云代金券