点击上方“芋道源码”,选择“设为星标”
管她前浪,还是后浪?
能浪的浪,才是好浪!
每天 10:33 更新文章,每天掉亿点点头发...
源码精品专栏
原创 | Java 2021 超神之路,很肝~
中文详细注释的开源项目
RPC 框架 Dubbo 源码解析
网络应用框架 Netty 源码解析
消息中间件 RocketMQ 源码解析
数据库中间件 Sharding-JDBC 和 MyCAT 源码解析
作业调度中间件 Elastic-Job 源码解析
分布式事务中间件 TCC-Transaction 源码解析
Eureka 和 Hystrix 源码解析
Java 并发源码
来源:码农参上
WebMagic
PageProcessor 与 xpath
获取提问的答案
Selenium
安装WebDriver
修改Selenium源码
js模拟页面操作
调用
获取问题详细描述
Pipeline
分析
Proxy代理
题外话
前两天,百度紧随GPT-4
发布了自己的语言模型文心一言。
讲道理,对于国内能够发布这样一个敢于对标CHAT GPT
的高质量语言模型,大家应该更多感受到的是赛博朋克与现实生活贴近的真实感,对这个模型应该有着更多的鼓励或赞美。
可不知是因为整个发布会搞的过于像没有好好准备的学生毕业答辩PPT,还是它的实际表现并没有那么如人意,大家貌似对文心一言并不那么买账。
于是我决定看一下知乎大神们对文心一言的评价,哪想到随便打开一个问题,居然有600多条回答…
要是我这一条一条翻完所有回答,估计就得拿出一天来全职摸鱼了,那么有没有什么办法能够最快的分析出对待这个问题大家的综合评价呢?
那么今天就让我纱布擦屁股,给大家露一小手,写一个爬虫扒下来所有的回答,再对结果进行一下分析。
正式开始前,咱们得先搞定工具。虽然python写起爬虫来有天然的框架优势,不过鉴于大家都是搞java的,那么我们今天就用java框架来实现一个爬虫。
咱们要使用的工具 WebMagic,就是一款简单灵活的java爬虫框架,总体架构由下面这几部分构成:
Downloader
:负责从互联网上下载页面,以便后续处理。WebMagic默认使用了Apache HttpClient作为下载工具。
PageProcessor
:负责解析页面,抽取有用信息,以及发现新的链接。WebMagic使用Jsoup作为HTML解析工具,并基于其开发了解析XPath的工具Xsoup。
Scheduler
:负责管理待抓取的URL,以及一些去重的工作。WebMagic默认提供了JDK的内存队列来管理URL,并用集合来进行去重。也支持使用Redis进行分布式管理。
Pipeline
:负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic默认提供了输出到控制台和保存到文件两种结果处理方案。
在4个主要组件中,除了PageProcessor
之外,其他3个组件基本都可以复用。而我们实际爬虫中的重点,就是要针对不同网页进行页面元素的分析,进而定制化地开发不同的PageProcessor
。
下面我们开始准备实战,先引入webmagic
的core
和extension
两个依赖,最新0.8.0
版本搞里头:
us.codecraft
webmagic-core
0.8.0
us.codecraft
webmagic-extension
0.8.0
在正式开始抓取页面前,我们先看看知乎上一个问题的页面是怎么构成的,还是以上面图中那个问题为例,原问题的地址在这里:
https://www.zhihu.com/question/589929380
我们先做个简单的测试,来获取这个问题的标题 ,以及对这个问题的描述 。
通过浏览器的审查元素,可以看到标题是一个h1
的标题元素,并且它的class属性是QuestionHeader-title
,而问题的描述部分在一个div
中,它的class中包含了QuestionRichText
。
简单分析完了,按照前面说的,我们要对这个页面定制一个PageProcessor
组件抽取信息,直接上代码。
新建一个类实现PageProcessor
接口,并实现接口中的process()
这个方法即可。
public class WenxinProcessor implements PageProcessor {
private Site site = Site.me()
.setRetryTimes(3).setSleepTime(1000);
@Override
public void process(Page page) {
String title = page.getHtml()
.xpath("//h1[@class='QuestionHeader-title']/text()").toString();
String question= page.getHtml()
.xpath("//div[@class='QuestionRichText']//tidyText()").toString();
System.out.println(title);
System.out.println(question);
}
public Site getSite() {
return site;
}
public static void main(String[] args) {
Spider.create(new WenxinProcessor())
.addUrl("https://www.zhihu.com/question/589929380")
.thread(2)
.run();
}
}
查看运行结果:
可以看到,在代码中通过xpath()
这样一个方法,成功拿到了我们要取的两个元素。其实说白了,这个xpath也不是爬虫框架中才有的新玩意,而是一种XML路径语言(XML Path Language
),是一种用来确定XML文档中某部分位置的语言。它基于XML的树状结构,提供在数据结构树中找寻节点的能力。
常用的路径表达式包括:
表达式 | 描述 |
---|---|
nodename | 选取此节点的所有子节点。 |
/ | 从根节点选取。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
在上面的代码中,//h1[@class='QuestionHeader-title']
就表示选取一个类型为h1
的节点,并且它有一个class为QuestionHeader-title
的属性。
至于后面的text()
和tidyText()
方法,则是用于提取元素中的文本,这些函数不是标准xpath中的,而是webMagic中特有的新方法,这些函数的使用可以参考文档:
http://webmagic.io/docs/zh/posts/ch4-basic-page-processor/xsoup.html
看到这,你可能还有个问题,这里对于问题的描述部分没有显示完全,你需要在页面上点一下这个显示全部它才会显示详细的信息。
没关系,这里先留个坑,这个问题放在后面解决。
我们完善一下上面的代码,尝试获取问题的解答。按照老套路,还是先分析页面元素再用xpath写表达式获取。修改process
方法:
@Override
public void process(Page page) {
String contentPath= "div[@class='QuestionAnswers-answers']"+
"//div[@class='RichContent RichContent--unescapable']" +
"//div[@class='RichContent-inner']"+
"/tidyText()";
List answerList = page.getHtml().xpath(contentPath).all();
for (int i = 0; i < answerList.size(); i++) {
System.out.println("第"+(i+1)+"条回答:");
System.out.println(answerList.get(i)+"\n=======");
}
}
在上面的代码中,使用了xpath
获取页面中具有相同属性的元素,并将它们存入了List列表中。看一下运行结果:
纳尼?这个问题明明有着689条的回答,为什么我们只爬到了两条答案?
如果你经常用知乎来学习摸鱼的话,其实就会知道对于这种有大量回答的问题,页面刚开始只会默认显示很少的几条的消息,随着你不断的下拉页面才会把新的回答显示出来。
那么如果我想拿到所有的评论应该怎么做呢?这时候就要引出webMagic中另一个神奇的组件Selenium了。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
简单来说,selenium是一个用于Web应用程序测试的工具,selenium测试可以直接运行在浏览器中,就像真正的用户在操作一样,并且目前主流的大牌浏览器一般都支持这项技术。
所以在爬虫中,我们可以通过编写模仿用户操作的selenium脚本,模拟进行一部分用互操作,比如点击事件或屏幕滚动等等。
WebMagic-Selenium需要依赖于WebDriver,所以我们先进行本地WebDriver的安装操作。
查看自己电脑上Chrome版本,可以点击设置
->关于chrome
查看,也可以直接在地址栏输入chrome://settings/help
:
可以看到版本号,然后需要下载对应版本的WebDriver,下载地址:
http://chromedriver.storage.googleapis.com/index.html
打开后,可以看到各个版本,选择与本地浏览器最接近的版本:
点击进入后,根据我们的系统选择对应版本下载即可。
下载完成后,解压到本地目录中,之后在使用selenium模块中会使用到。这个文件建议放在chrome的安装目录下,否则之后在代码中可能会报一个WebDriverException: unknown error: cannot find Chrome binary
找不到chrome文件的错误。
webMagic中已经封装了selenium模块的代码,但官方版本的代码有些地方需要修改,我们下载源码后要自己简单改动一下然后重新编译。我这下载了0.8.1-SNAPSHOT
版本的代码,官方git地址:
https://github.com/code4craft/webmagic
修改配置文件地址,在WebDriverPool
将selenium
配置文件路径写死了,需要改变配置路径:
// 修改前
// private static final String DEFAULT_CONFIG_FILE = "/data/webmagic/webmagic-selenium/config.ini";
// 修改后
private static final String DEFAULT_CONFIG_FILE = "selenium.properties";
在resources
目录下添加配置文件selenium.properties
:
> 基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
>
> * 项目地址:
> * 视频教程:
# What WebDriver to use for the tests
driver=chrome
# PhantomJS specific config (change according to your installation)
chrome_driver_loglevel=DEBUG
修改SeleniumDownloader
的download()
方法,在代码中的这个位置,作者很贴心的给我们留了一行注释:
意思就是,你可以在这添加鼠标事件或者干点别的什么东西了。我们在这添加页面向下滚动这一模拟事件,每休眠2s就向下滚动一下页面,一共下拉20次:
//模拟下拉,刷新页面
for (int i=0; i < 20; i++){
System.out.println("休眠2s");
try {
//滚动到最底部
((JavascriptExecutor)webDriver)
.executeScript("window.scrollTo(0,document.body.scrollHeight)");
//休眠,等待加载页面
Thread.sleep(2000);
//往回滚一点,否则不加载
((JavascriptExecutor)webDriver)
.executeScript("window.scrollBy(0,-300)");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
修改完成后本地打个包,注意还要修改一下版本号,改成和发行版的不同即可,我这里改成了0.8.1.1-SNAPSHOT
。
mvn clean install
回到之前的爬虫项目,引入我们自己打好的包:
us.codecraft
webmagic-selenium
0.8.1.1-SNAPSHOT
修改之前的主程序启动时的代码,添加Downloader
组件,SeleniumDownloader
构造方法的参数中传入我们下好的chrome的webDriver的可执行文件的地址:
public static void main(String[] args) {
Spider.create(new WenxinProcessor())
.addUrl("https://www.zhihu.com/question/589929380")
.thread(2)
.setDownloader(new SeleniumDownloader("D:\\Program Files\\Google\\Chrome\\Application\\chromedriver.exe")
.setSleepTime(1000))
.run();
}
进行测试,可以看到在拉动了40秒窗口后,获取到的答案条数是100条:
通过适当地添加下拉页面的循环的次数,我们就能够获取到当前问题下的全部回答了。
另外,在启动爬虫后我们会看到webDriver弹出了一个chrome的窗口,在这个窗口中有一个提示:Chrome正受到自动测试软件的控制 ,并且可以看到页面不断的自动下拉情况:
如果不想要这个弹窗的话,可以修改selenium模块的代码进行隐藏。修改WebDriverPool
的configure()
方法,找到这段代码:
if (driver.equals(DRIVER_CHROME)) {
mDriver = new ChromeDriver(sCaps);
}
添加一个隐藏显示的选项,并且在修改完成后,重新打包一下。
if (driver.equals(DRIVER_CHROME)) {
ChromeOptions options=new ChromeOptions();
options.setHeadless(true);
mDriver = new ChromeDriver(options);
}
不知道大家还记不记得在前面还留了一个坑,我们现在获取到的对问题的描述是不全的,需要点一下这个按钮才能显示完全。
同样,这个问题也可以用selenium来解决,在我们下拉页面前,加上这么一个模拟点击事件,就可以获得对问题的详细描述了:
((JavascriptExecutor)webDriver)
.executeScript("document.getElementsByClassName('Button QuestionRichText-more')[0].click()");
看一下执行结果,已经可以拿到完整内容了:
到这里,虽然要爬的数据获取到了,但是要进行分析的话,还需要进行持久化操作。在前面的webMagic的架构图中,介绍过Pipeline
组件主要负责结果的处理,所以我们再优化一下代码,添加一个Pipeline
负责数据的持久化。
由于数据量也不是非常大,这里我选择了直接存入ElasticSearch中,同时也方便我们进行后续的分析操作,ES组件我使用的是esclientrhl
,为了方便我还是把项目整个扔到了spring里面。
定制一个Pipeline也很简单,实现Pipeline
接口并实现里面的process()
接口就可以了,通过构造方法传入ES持久化层组件:
@Slf4j
@AllArgsConstructor
public class WenxinPipeline implements Pipeline {
private final ZhihuRepository zhihuRepository;
@Override
public void process(ResultItems resultItems, Task task) {
Map map = resultItems.getAll();
String title = map.get("title").toString();
String question = map.get("question").toString();
List answer = (List) map.get("answer");
ZhihuEntity zhihuEntity;
for (String an : answer) {
zhihuEntity = new ZhihuEntity();
zhihuEntity.setTitle(title);
zhihuEntity.setQuestion(question);
zhihuEntity.setAnswer(an);
try {
zhihuRepository.save(zhihuEntity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
把selenium向下拉取页面的次数改成200后,通过接口启动程序:
@GetMapping("wenxin")
public void wenxin() {
new Thread(() -> {
Request request = new Request("https://www.zhihu.com/question/589929380");
WenxinProcessor4 wenxinProcessor = new WenxinProcessor4();
Spider.create(wenxinProcessor)
.addRequest(request)
.addPipeline(new WenxinPipeline(zhihuRepository))
.setDownloader(new SeleniumDownloader("D:\\Program Files\\Google\\Chrome\\Application\\chromedriver.exe")
.setSleepTime(1000))
.run();
}).start();
}
运行完成后,查询一下es中的数据,可以看到,实际爬取到了673条回答。
另外,我们可以在一个爬虫程序中传入多个页面地址,只要页面元素具有相同的规则,那么它们就能用相同的爬虫逻辑处理,在下面的代码中,我们一次性传入多个页面:
Spider.create(new WenxinProcessor4())
.addUrl(new String[]{"https://www.zhihu.com/question/589941496",
"https://www.zhihu.com/question/589904230","https://www.zhihu.com/question/589938328"})
.addPipeline(new WenxinPipeline(zhihuRepository))
.setDownloader(new SeleniumDownloader("D:\\Program Files\\Google\\Chrome\\Application\\chromedriver.exe")
.setSleepTime(1000))
.run();
一顿忙活下来,最终扒下来1300多条数据。
数据落到了ES里后,那我们就可以根据关键字进行分析了,我们先选择10个负面方向的词语进行查询,可以看到查到了403条数据,将近占到了总量的三分之一。
再从各种回答中选择10个正向词语查询,结果大概只有负面方向的一半左右:
不得不说,这届网友真的是很严厉…
说到爬虫,其实还有一个绕不过去的东西,那就是代理。
像咱们这样的小打小闹,爬个百八十条数据虽然没啥问题,但是如果要去爬取大量数据或是用于商业,还是建议使用一下代理,一方面能够隐藏我们的IP地址起到保护自己的作用,另一方面动态IP也能有效的应对一些反爬策略。
个人在使用中,比较推荐的是隧道代理 。简单的来说,如果你购买了IP服务的话,用普通代理方式的话需要你去手动请求接口获取IP地址,再到代码中动态修改。而使用隧道代理的话,就不需要自己提取代理IP了,每条隧道自动提取并使用代理IP转发用户请求,这样我们就可以专注于业务了。
虽然网上也有免费的代理能够能用,但要不然就是失效的太快,要不就是很容易被网站加入黑名单,所以如果追求性能的话还是买个专业点的代理比较好,虽然可能价格不那么便宜就是了。
看了一大顿下来,从大家略显犀利的言辞来看,大家总体上对文心一言还是不那么满意的。毕竟,在有着CHAT-GPT
这么一个优秀的产品做背景板的前提下,这届网友可能没有那么好糊弄。
但是话又说回来,丑媳妇总得见公婆不是?提早暴露缺陷,也有利于国内的这些大厂,看清和一流AI产品之间的真实差距,知难而进,迎头赶上。
源码地址:https://github.com/trunks2008/zhihu-spider
参考资料:
http://webmagic.io/docs/zh/posts/ch1-overview/architecture.html
https://blog.csdn.net/panchang199266/article/details/85413746
欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
已在知识星球更新源码解析如下:
最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)