![搜索引擎技术与发展](https://wfqqreader-1252317822.image.myqcloud.com/cover/53/35011053/b_35011053.jpg)
2.1 自己的网络爬虫
网络爬虫需要实现的基本功能包括下载网页及对URL地址的遍历。为了高效快速地遍历网站,还需要应用专门的数据结构进行优化。
2.1.1 使用URL访问网络资源
URI包括URL和URN。但是URN并不常用,所以很多人不知道URN。URL由3个部分组成,如图2-1所示。
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_43_1.jpg?sign=1739555901-Dt9yMJ1ZS9UjCl2s955sC3oDEwoZkjYa-0-0f19835a6e56929bb6da25df0766456c)
图2-1 URL分为3个部分
● 第一部分是协议名(也可称为服务方式)。
● 第二部分是存有该资源的域名或IP地址(有时也包括端口号)。
● 第三部分是主机资源的具体地址,如目录和文件名等。
第一部分和第二部分用“://”符号隔开,第二部分和第三部分用“/”符号隔开。第一部分和第二部分是不可缺少的,第三部分有时可以省略。
在交互式编程环境JShell中,实验Java中的URL对象如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_1.jpg?sign=1739555901-cGUsd9qPB0z8TRUO8eqXeCvIEnUp6xio-0-1ac82dd764bffa9015cf4b1f65b65a4b)
按组合键Ctrl+D退出JShell。
可以通过DNS取得该URL域名的IP地址。在Linux操作系统中,DNS解析的问题可以用dig或nslookup命令进行分析,具体如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_2.jpg?sign=1739555901-VXJzRQYVz01Im0EBOfVRRYw1GkMSqiPF-0-e88e78512367e17f8b147356af2cb4a1)
如果需要更换更好的DNS域名解析服务器,可以编辑DNS配置文件/etc/resolv.conf。
Windows操作系统中也有nslookup命令。可以使用默认的DNS服务器查询IP地址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_3.jpg?sign=1739555901-9OsDEOVl4ShswuHP2XkTfKwqii3GYs0b-0-282e221de26821a9f7c954a994cbc43c)
使用指定的DNS服务器查询IP地址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_4.jpg?sign=1739555901-EMSxH2CiufuAl3i9t3WsUJPK3w4phwsF-0-55d0dac420f9b368ec6bd8424c1bd2a9)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_1.jpg?sign=1739555901-EQJP1idgtVLUMszSaytUKvyRtEJTN4JE-0-f52fdbf212fdad998f3ebabc9b14bcbd)
下载一个网页文本的简单例子如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_2.jpg?sign=1739555901-VE8i9X3utcIRasSaYWHBh4XZhN4yRotg-0-0363e701db59d8b29e1c47aa6912a72e)
需要注意的是,这里没有下载网页中相关的图片等,如果要下载网页中的图片,就需要分析其中的<img>标签然后下载。
Web服务器不仅返回了请求网页的源代码,还返回了头信息。使用curl命令可以查看到返回的头信息:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_3.jpg?sign=1739555901-4wQwf9b6I2FHW0G6j7r7jBP3HPqoSXro-0-f746bee322ca8e1309bafcc43bce9258)
返回的第一行结果中包含了HTTP状态码200。
状态码是一个包括3个数字的结果代码,爬虫可以用状态码识别Web服务器处理的情况。状态码的第一个数字定义响应的类别,后两个数字有分类的作用。
● 1xx:信息响应类,表示接收到请求并且继续处理。
● 2xx:处理成功响应类,表示动作被成功接收、理解和接受。
● 3xx:重定向响应类,为了完成指定的动作,必须接受进一步处理。
● 4xx:客户端错误,客户请求包含语法错误或不能正确执行。
● 5xx:服务端错误,服务器不能正确执行一个正确的请求。
HTTP常用状态码如表2-1所示。
表2-1 HTTP常用状态码
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_46_1.jpg?sign=1739555901-uCK60AyMWmKN8DxzxsowZ4WCmAhfxmzL-0-eb4326752962cf60d6eea0ce32184aca)
使用HTTP客户端开源项目OkHttpClient得到HTTP状态码的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_46_2.jpg?sign=1739555901-8Z26nRrnJIActWF2BkR78yC8GfHtY0CK-0-e1b98086db71180436aac9557f2832c6)
2.1.2 重试
为了使爬虫可以长期稳定运行,需要处理各种超时异常,并且在放弃下载之前需要多次重试。最简单的重试代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_47_1.jpg?sign=1739555901-jLmaGJCRphyjFQkaWMiMqDTpkL6HC3KG-0-570e4aa675d5d1b2be18741845545282)
需要注意的是,这里只是捕捉了IOException类型的异常,无法捕捉到所有的异常。如果要捕捉所有的异常,则需要捕捉Throwable类型的异常。使用HTTP客户端开源项目OkHttpClient下载网页并重试的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_47_2.jpg?sign=1739555901-bneZKg6BmlsEqB0sTyQbnQjUFmDopeLj-0-f14df9aaad3d5c54f9cfed8108cb0121)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_1.jpg?sign=1739555901-Xw3inOYDZ1qqWDXB0lxU3GiKMUDMNG5c-0-1d19a0c671a3d74ad3d6c3c3646b2a95)
通过注解设置最大重试次数:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_2.jpg?sign=1739555901-AymiyLtIMj1Os2t9S5S8jWo5iB8ZGdCF-0-8907e7b88acd5729a403e90b8fc5643c)
下载类使用注解声明重试次数:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_3.jpg?sign=1739555901-T0kXkio5tvKFMh80fuaF3obL2lIIJLuV-0-6652faf8454ff9c44749c08fa7af1abc)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_49_1.jpg?sign=1739555901-CqXmzKVmdfuftI0URx3BaO8Fk5Z40qK3-0-ee37a4300b5ab8bfc4fd5323fdd1b3bd)
实现重试:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_49_2.jpg?sign=1739555901-6JTorlvfcB7k5X27zZ8xgbFscQOLUinz-0-2593880d918a8029ddd369c21d77d2ee)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_1.jpg?sign=1739555901-ZuIAVKa2IYl1GprMj7m1rJj8Nwf7tn0l-0-79c2fdb38fa33b7b7a1ac15d4ed33d96)
Spring Retry是一个支持失败后重试操作的框架。为了使用Spring Retry,需要先在build.gradle文件中增加如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_2.jpg?sign=1739555901-oOGJaq0vtdenpOubcbxtRqMn94J3RZBt-0-f2be83a37ee0422ad4f8eca8d5ae9a1f)
首先定义一个需要重试的服务类,然后在配置类中提供得到这个服务类的实例的方法。下载服务类DownService的实现如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_3.jpg?sign=1739555901-AeLgWvt3ietnwTDawvp97goGIv1d6Vub-0-f9cafa9683c2948de7bf834dcb2993ff)
应用程序类CrawlerApplication调用服务类实现重试下载:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_4.jpg?sign=1739555901-dURqtePZD7ZXWnJEjACAnd06ne8quofm-0-f280b0ffd0da404308524c821a3e3831)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_1.jpg?sign=1739555901-0bYXpVOZrMu9LGYf9BtZj3QevSWHxFtu-0-072769c46279be125aeb7dac69fd7e34)
为了改进Spring Retry,可以下载Spring Retry源代码,然后在本地修改并编译源代码。
可以使用git命令下载源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_2.jpg?sign=1739555901-trF8zrlpbqZe3nd9C93tt3DZA3RUaXyr-0-b72b2ff68fbee14584faf74a66ff43e9)
也可以使用svn命令下载源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_3.jpg?sign=1739555901-IYQM7Oebzs965j5WcmYFf72ETzsC9QQr-0-69a20dc6651618c8cbcaec8b1990d710)
使用mvn命令编译源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_4.jpg?sign=1739555901-0tirNKPdKJ1XJ6Y9EXMeTVkU6OBcBm4C-0-8ebe66d82c3c7ddca68fc18dbecdbb4b)
为了忽略编译过程中的错误MavenReportException:Error while generating Javadoc,可以修改pom.xml文件,Javadoc插件增加了配置项<failOnError>false</failOnError>:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_5.jpg?sign=1739555901-KuuNO8HIHip8yag81Pyx76Y4XkUNXuz8-0-e6c8689c2af9fee1babace8740f8cdb8)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_1.jpg?sign=1739555901-EwTEzsmDXT6jCsNduYjHiCnn01bTMIzD-0-6dab6e3415f5df87423c2abbb5f6092f)
创建一个项目,用于测试打包出来的spring-retry,将这个项目的build.gradle文件设置成从本地库加载依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_2.jpg?sign=1739555901-2q76hphNALG94kmav4l8EI0Z61MtrjAm-0-52a1ac1c377ec73818abe29217649b9a)
测试支持重试的服务:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_3.jpg?sign=1739555901-niRDVybk7PIFuZD5CpdbjSbp9bKjvHuT-0-2b4c51113f8b8550baf8ae0d660c8c77)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_53_1.jpg?sign=1739555901-zKWagt4OArntg0qxpO1vHu55nATaTGln-0-f6a4797f76a61c9d41da4f5415446e6b)
2.1.3 网络爬虫的遍历与实现
通用的网络爬虫通过对URL链接的遍历来获取所需要的信息。基本的数据结构包括一个待扩展的URL表和一个已经访问过的URL地址表,如图2-2所示。
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_53_2.jpg?sign=1739555901-fHTbHtN0RAloKEiy6vjqQPHMY90rF7Wo-0-0744b14260c8331dcc7df4588efcad75)
图2-2 基本的数据结构
在抓取网页的时候,网络爬虫一般有两种策略:广度优先和深度优先(见图2-3)。广度优先是指网络爬虫会先抓取起始网页中链接的所有网页,然后选择其中的一个链接网页,继续抓取在此网页中链接的所有网页。这是最常用的策略,因为这种策略可以使网络爬虫并行处理,从而提高其抓取速度。深度优先是指网络爬虫会从起始页开始,一个链接一个链接地跟踪下去,处理完这条线路之后再转入下一个起始页,继续跟踪链接。
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_54_1.jpg?sign=1739555901-QSqJanhJYN8DIsJkYbSHsRiHiiCq3T3W-0-e6f24f7f5ecf4c2fa96f06a6cb471fc3)
图2-3 网络爬虫的两种抓取策略
广度遍历采用队列的方式实现todo表的扩展,先访问的网页先扩展,对如图2-3所示的todo表和visited表的执行状态如下。
todo:A
visited:null
todo:B C D E F
visited:A
todo:C D E F
visited:A B
todo:D E F
visited:A B C
todo:E F
visited:A B C D
todo:F H
visited:A B C D E
todo:H G
visited:A B C D E F
todo:G I
visited:A B C D E F H
todo:I
visited:A B C D E F H G
todo:null
visited:A B C D E F H G I
深度遍历采用堆栈的方式实现todo表的扩展,先访问的网页先扩展,对如图2-3所示的todo表和visited表的执行状态如下。
todo:A
visited:null
todo:B C D E F
visited:A
todo:B C D E G
visited:A F
todo:B C D E
visited:A F G
todo:B C D H
visited:A F G E
todo:B C D I
visited:A F G E H
todo:B C D
visited:A F G E H I
todo:B C
visited:A F G E H I D
todo:B
visited:A F G E H I D C
todo:null
visited:A F G E H I D C B
seeds和新发现的链接应该放在两个列表中。每次得到下一个要遍历的链接时,如果当前 seeds 列表中还有没有开始遍历的,就应该先开始这一个。这样可以避免一个站点遍历过深,而另一个站点却没有机会开始。
seeds列表可以是一个Excel表格。Apache POI(https://poi.apache.org)可以读取Excel表格中的数据。增加如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_56_1.jpg?sign=1739555901-22jVX0VNASJKDmWXFrXoY92n0MmqdTaH-0-06e8e455c53f1bb886de396908d171c3)
读取指定单元格数据的方法如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_56_2.jpg?sign=1739555901-j7bKfB0MHuztW3YNVg4oR6vr33sVsOt1-0-6c12d5e7022bef172777df8bac342c51)
读取Excel表格中的种子列表:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_56_3.jpg?sign=1739555901-3qX4zPleFikZCsCOSCIOhZEn3JTd3JPz-0-f9dc72fee52e5e47b65d937b35a1236d)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_1.jpg?sign=1739555901-v6Md94A54rguYDjqaaUF9gVWl8W1EAF3-0-1d77810f463c13a7dbaa8256cf8469e2)
2.1.4 多线程爬虫
可以使用多线程加快网页下载速度。下面先介绍Java中的多线程。
因为Java不允许继承多个类,所以一个类一旦继承了Thread类,就不能再继承其他类。为了避免所有线程都必须是Thread的子类,需要独立运行的类也可以继承一个系统已经定义好的叫作Runnable的接口。Thread类有一个构造方法public Thread(Runnable target)。当线程启动时,将执行target对象的run()方法。Java内部定义的Runnable接口很简单:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_2.jpg?sign=1739555901-uZaK29uwRI3hO2lHaITwJxXazx2RvTZf-0-749e06d6d52147c4d168a62d6d70792c)
实现这个接口,然后把要同步执行的代码写在run()方法中。实现run()方法的Test类如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_3.jpg?sign=1739555901-nuRYaWaPYP4aHKw6I0vfRHOjAoIvx244-0-6a5e6577a8576db300da7bd066c4d31c)
运行需要同步执行的代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_4.jpg?sign=1739555901-4Z0nKJALfnch4Ed5gTRd73kSixxZrF6k-0-e1b7271f85b7a1ec057e3edc1414011c)
可以用不同的线程处理不同的目录页,如下所示是下载新闻网页的示例:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_5.jpg?sign=1739555901-3fkWMFzwPKWvWytHqJZoDndN9KmE18tR-0-1e407809b0c2d52d7691a2f0bb618dd4)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_1.jpg?sign=1739555901-kW1U11r276Uvg7V4VoolFb4zP19en9TQ-0-72f8cafd285fc167ea476a6e7c2fdcc7)
假设需要在主线程中统计最后抓取了多少数据,则需要等待所有子线程完成。一种实现方法是使用ExecutorService来管理线程池:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_2.jpg?sign=1739555901-GZY5Gh9RFHMurLRpLSexXds43T6emv93-0-e9b64fb63a2fe22b083ac4f73202aac9)
2.1.5 Log4j2日志
为了方便调试,可以在抓取过程中记录日志。Log4j2是一个开源的日志框架。为了使用Log4j2记录日志,build.gradle文件增加了如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_3.jpg?sign=1739555901-1XZD4AX4mCZ8AuHMkMAU6bSX82nMRwKl-0-598d3852a301b8d2ff707d5cf5681ad0)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_1.jpg?sign=1739555901-bCpU9Mf4dhCypCn4PJZNqPMpsUK2zd9k-0-38eeb622bdee5e74037515ef1efd59f6)
为了使Log4j模块版本彼此保持同步,可以借助BOM pom.xml文件。BOM(Bill of Materials)是由Maven提供的功能,BOM定义了一整套相互兼容的jar包版本集合,使用时只需要依赖该BOM,即可放心地使用需要的依赖jar包,并且无须再指定版本号。BOM的维护方负责版本升级,并保证BOM中定义的jar包版本之间的兼容性。
通过BOM使用Log4j的build.gradle文件的内容如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_2.jpg?sign=1739555901-yruDwihFtaYEtONb1Qmwuo6dfye2C1k3-0-1de1c191030a485dd1185bebe0d80b90)
接下来使用Log4j2来记录日志。如下所示的配置文件log4j2.properties将日志记录输出到控制台:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_3.jpg?sign=1739555901-bGrCi3hGoTtF1BjmK5MIJBhDVsZDsAoJ-0-9aa5787d021487c66e7914bd33122a83)
首先通过LogManager得到Logger类的实例,然后调用logger.info()方法记录日志。记录日志的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_4.jpg?sign=1739555901-65cjtefYPlhjVLWGmWhEskmlNJ0rVliK-0-9f08042de9a93a8065313dd9026bea60)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_60_1.jpg?sign=1739555901-PS2eZHBisCKd8BzdDfZ9DO0evV2FYdVG-0-9705147c4727cbf82ec31bdc13636761)
2.1.6 存储URL地址
todo表或visited表一般用ArrayList或HashMap实现,它们只能在内存中,但内存是有限的。开始的时候,有人把todo表或visited表放在数据库中,但数据库对于这种简单的结构化存储来说,不够轻量级。
Berkeley DB是嵌入式数据库系统,其中的一个数据库只能存储key和value这2列。底层实现采用B树结构,可以看成可以存储大量数据的HashMap。Berkeley DB的简称是BDB,官方网址是http://www.oracle.com/database/berkeley-db/index.html。Berkeley DB的 C++语言版本首先出现,然后在此基础上又实现了 Java 语言的本地版本。可以用Berkeley DB来实现todo表或visited表。
如果使用Maven构建项目,则可以在pom.xml文件中添加如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_60_2.jpg?sign=1739555901-XLQqKbgtKEVOYTTv1g8Kfv1tXersmu3E-0-e799cd30a11bf35f40ebcb23393b9f99)
如果需要把Maven项目转换成Gradle项目,就需要在包含POM的目录中运行gradle init。这会将 Maven 构建转换为 Gradle 构建,生成 settings.gradle 文件和一个或多个build.gradle文件。
为了使用Berkeley DB,build.gradle文件增加了如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_60_3.jpg?sign=1739555901-rrLocojxzBEAZxrJfPNWSiaBshfGbc6H-0-b72d739f7c3ef5b9411a24714ed6fe19)
Berkeley DB用到的对象主要有以下几种。
● 新建环境变量:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_60_4.jpg?sign=1739555901-18dlQl8VGFMeQuRvZG7PvApNhzYqHQYg-0-90c42ae2c7dea39285e0164de87fee0f)
● 释放环境变量:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_1.jpg?sign=1739555901-ZJy7uYpUmwQS2vPFFcyLQfZsbnH28Qsx-0-d4115d2f460cccb93a1563c536a9b7f2)
● 创建数据库:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_2.jpg?sign=1739555901-xsHHZhdjqlXEgiQPXqTgCjAZdWwNxH7n-0-b869daabd58abf25e1cfded14c8bc689)
● 建立数据的映射:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_3.jpg?sign=1739555901-vljROZrgzxYxPsAb3iav0G62GmZQtuWu-0-0deae0875f5fc0fcaccadcbaa068eec1)
使用DocIDServer类记住哪些URL已经访问过,实现了增量采集。其中,DocIDServer.getNewDocID(url)方法用于记住一个已经访问过的 URL。DocIDServer.isSeenBefore(url)方法用于判断一个URL是否已经访问过。DocIDServer类的实现如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_4.jpg?sign=1739555901-l7w3qMojI58dv2TIiAV1eErMDdv8rlNy-0-4f62f175c5a56bc602b8dc411bacd7ac)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_62_1.jpg?sign=1739555901-j4sFXJ0vkKf8UIHs2fyrVRitI3wLYI6R-0-cfd80d48a840fd5f78dfde68c86bd57c)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_63_1.jpg?sign=1739555901-3OCmHMEx57eCm9QvSlzoiEo6wKm1H9oI-0-26a3cc8f67627cfc59e7236c08893029)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_1.jpg?sign=1739555901-GZQDxkY7VqTZ0nXwE7fLyDiwIuLBP736-0-ddeb889b6cdcfb486cd9f5c3ddd5d691)
使用DocIDServer类的测试代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_2.jpg?sign=1739555901-4hmzQMb3S8plC0ZlGlvG5tMkgA7vzbqX-0-99babce3a0874b1c8dbb67121510529c)
判断URL地址是否已经抓取过还可以借助布隆过滤器(Bloom Filter)。布隆过滤器的实现方法如下:在内存中开辟一块区域,对其中的所有位上置 0,然后对数据做多次不同的hash,每个hash值对内存位数求模,求模得到的数在内存对应的位上置1。置位之前需要先判断是否已经置位,每次插入一个URL,只有当全部位都已经置1之后才认为是重复的。
下面是一个简单的示例:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_3.jpg?sign=1739555901-BuZwXy0HY4aHOG2igF4ms9oJ5ZFNrnd7-0-7fb9be5876815958ddf1c5e4807aba03)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_65_1.jpg?sign=1739555901-Cc2HCdOdqimnpgH4YgW789KAgHh5cb7h-0-26026318b682442bb0640bd7fe164b35)
如果想知道需要使用多少位才能降低错误概率,可以使用如表 2-2 所示的给定项目和位数比率的布隆过滤器误判率表。
表2-2 布隆过滤器误判率表
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_65_2.jpg?sign=1739555901-QEW1oHjfIwMDExkwfzN3198AvZ0wGAMq-0-a2f508cf126a070a6d53d26932e2f99a)
为每个URL分配2字节就可以达到千分之几的冲突。比较保守的实现是为每个URL分配了4字节,项目和位数比是1∶32,误判率是0.000 000 211 673 40。对于5000万个URL,布隆过滤器只占用了200MB的空间,并且排重速度超快,遍历一遍用时还不到2分钟。
SimpleBloomFilter把对象映射到位集合的方法如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_65_3.jpg?sign=1739555901-iGf3as9aEKbAeLLRvTtN6Q7RaXGH936J-0-533896066287b14c575c992e580dff35)
该实现方法计算了k个相互独立的hash值,因此误判率较低。
如下所示的代码把布隆过滤器的状态保存到文件中:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_66_1.jpg?sign=1739555901-5yMZgcXal62ZWj67uZBSxI1JrCdWs4d8-0-ba6bffda97737ea7edfd8e99fd7f7993)
如下所示的代码把布隆过滤器的状态从先前保存的文件中读出来:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_66_2.jpg?sign=1739555901-Z5AF8kNByCotfHJwstvlvFkkM95pTmGt-0-f51f8d9b4312ad826b15b57e129c4706)
2.1.7 定向采集
对于不同类型的网站,网络爬虫遍历和获取有效信息的方式也不同。有的网站详情页URL中存在自增ID,可以直接遍历;有的网站按类别列出显示详情的详情页,也就是按列表页和详情页组织网站结构,如 http://politics.people.com.cn/GB/1024/。从列表页提取详情页的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_66_3.jpg?sign=1739555901-kttcZClcr0I90wY5qXPF079K8VaVzavH-0-c6bc204b8107ef3e12b51846fa92be77)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_1.jpg?sign=1739555901-gOMEDOhhgxNgmxZBDZ4g5ZpSjlcyekVU-0-bbedf56c48141c5ee2da56504eb908f4)
可以把新发现的列表页放入工作队列。直接处理发现的详情页,详情页的URL不需要加入工作队列,因为当时就处理完了。使用内存数据库记录已经处理过的目录页和详情页:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_2.jpg?sign=1739555901-5ioRFgaYQ5tsKIx3LLRq19FQEUAiCpZh-0-6ec8d1df6832e2747e389b2637a8d7a1)
全局变量workQueue记录已经发现待处理的列表页:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_3.jpg?sign=1739555901-sq7YQbL9WqLCndVRHZ4t4GfABnw8pZg9-0-dccd4372911231ca01c5e61779ca3a02)
爬虫运行时,先把列表首页放入工作队列,然后使用一个循环处理列表页的工作队列:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_4.jpg?sign=1739555901-CMWxhFuYjB5m0KYfXxji4QG2F2cZLK7x-0-1bcc66cb59f1d3e0e176b69c591e97f1)
详情页和列表页往往包含一些有效信息。详情页处理器接口的定义如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_5.jpg?sign=1739555901-kz2eljUhTUMxGPEf8QjhKD8BnD8lu9oA-0-650472a72bcd78f54d8c3ead6f79f9c6)
用NewsDetailHandler类实现DetailHandler:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_6.jpg?sign=1739555901-qX7gqqFmOYBvPrj0JSCwGHkrr8BVR0dY-0-1157cc41453875af3243f3d11a093cbe)
2.1.8 暗网抓取
暗网是指只有提交检索词才能得到相关的结果的索引列表,然后根据这个索引列表获取详情页。
URL中提供的查询词需要编码,可以调用URLEncoder.encode()方法实现编码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_1.jpg?sign=1739555901-XNJjV6afgKsDSJ1XdOruJi4YwT52FCUA-0-b4b217c0ccdbc95f1cb6053074ea0b7f)
通过列表页的方式遍历临床试验信息:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_2.jpg?sign=1739555901-l5D0MOsPp8g2tnq5peM4QkIcL6Pz80Ds-0-19e43b62f5fc4753bbc4973afd286fae)
这里通过down_fmt参数指定返回XML格式的文件。
如果要在不将任何HTML DOM规则应用于文件的情况下解析XML,请使用XmlTreeBuilder,用法示例如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_3.jpg?sign=1739555901-N9fdt0uQRWBL8Y0BMw8dvc7IO0Wd4cMn-0-a97d149f1ab62cb5c25a4eef9dc9c016)
2.1.9 Selenium抓取动态页面
很多网站采用复杂的 JavaScript 实现动态界面效果,经常会碰到需要抓取动态网页的情况。
可以使用Selenium操控浏览器。可以使用Selenium让浏览器自动下载某个网页或填入登录密码等。Selenium-WebDriver直接调用本机的浏览器,执行自动化任务。Selenium把下载的过程当作黑盒子。Selenium的核心代码通过JavaScript完成,可以运行在Firefox或Chrome等浏览器中。这里以Firefox为例说明用Selenium抓取数据的方法。
Selenium Java API最基本的就是org.openqa.selenium.WebDriver类。首先在Java项目中引入Selenium相关的依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_4.jpg?sign=1739555901-FTJstC6mKYDskjVwMaS9TtZHVTQgfkD3-0-4c5eb73abdac9b0afea376dd5a5e75ba)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_1.jpg?sign=1739555901-Q76C5SC5nIEWmnTKXmDWeWcBHlP2HivL-0-033cb95229d669bbfd0c2009c3e780f0)
从https://github.com/mozilla/geckodriver/releases下载FirefoxDriver。Windows操作系统中的FirefoxDriver就是geckodriver.exe,然后通过属性设置FirefoxDriver文件所在的路径:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_2.jpg?sign=1739555901-ogrxisoJikPZjRIEZ2Rcan8gsua2B5hu-0-619e146c6e53dd9d42d3b90073b945aa)
使用WebDriver访问网址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_3.jpg?sign=1739555901-hEt0qk4ng59yK6ZCgBCcbNQlobzuSF0Q-0-619d6e63dda059aa653bbc0aff5f9a6b)
下载网页时,后台会启动一个浏览器进程。调用 WebDriver.quit()方法可以结束这个进程,但调用WebDriver.close()方法并不会结束这个进程。
如果只需要得到网页源代码,则可以不加载图像:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_4.jpg?sign=1739555901-ZeCZE1e5hK9coJczclXVW15xB7fs8Xpa-0-3f3d21194f0eb9029693bf3b1678ff83)
需要等待网页加载完毕,然后获取网页源代码。一个显式等待是已经定义的一段代码,用于等待某个条件发生,然后继续执行后续代码。最简单的方法是调用Thread.sleep(),具体示例如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_5.jpg?sign=1739555901-PDX6jTgHSdF4CJ7o6kCBN9ldsLv702jt-0-04a8eb7534879572a39363cdd3f050fa)
WebDriverWait结合ExpectedConditions可以实现只等待需要的时间长度:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_6.jpg?sign=1739555901-iNLOSAjtRnCP6REF8jypJjXfwUVu7pvL-0-37cc4c9033e25e583dbfe934698d8fc4)
通过类型选取元素:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_7.jpg?sign=1739555901-KfrXBmgf7lS1y52OcFkKaZxvdSZ26Bla-0-452bf1808a96ab5963d2eedc724e99f6)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_1.jpg?sign=1739555901-xD6NfKNk60TtVQksXGENHtHbLLRkWIkc-0-84d2c8d8516c02cdc62d76518c26473e)
通过id查找元素并单击它:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_2.jpg?sign=1739555901-UOtisb9MSJ5GMYOyzmdsJlKGx3n1WqiP-0-5cc79383814224ed4fa94eeb6aa93e39)
可以通过JavascriptExecutor对象来执行JavaScript代码。例如,得到垂直滚动条的高度:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_3.jpg?sign=1739555901-do194klot49lMDhKrwcaKoZiaQOZekQV-0-b483a89991f011c90ded43fd32fb04d2)
逐渐向下滚动:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_4.jpg?sign=1739555901-bxcAgvJuTfqlBZF6jnACYr46Tx5OdsqD-0-13f5c00c25830c6bff37ec58bf350698)
如果window.scrollBy(x,y)中的y值为负数,则表示向上滚动:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_5.jpg?sign=1739555901-blNsOlYfJzvdfF0DQKkgr2IkCx9mVedi-0-adac25a1ad0f07f7e023a35443b8450f)
2.1.10 图片抓取
为了能够节约网络流量,抓取过来的图片经常需要缩小到一定的尺寸,如 100px×100px或80px×80px:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_6.jpg?sign=1739555901-rcE7NNAoMjRA1cBwlEtr0lnnDIVkoVSn-0-e2859c44b73031c9e6280821cdf5f12e)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_71_1.jpg?sign=1739555901-SAKFZecWFDAS3AF9zkCjByee6vzBIhEA-0-b3a98bfe7775ba97b4b5226afc35d641)