![搜索引擎技术与发展](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=1739697379-95V1WSEoj5rbk8bXndXS9nmObpAanwAi-0-337cccd409456c1a0fe625809df56bc9)
图2-1 URL分为3个部分
● 第一部分是协议名(也可称为服务方式)。
● 第二部分是存有该资源的域名或IP地址(有时也包括端口号)。
● 第三部分是主机资源的具体地址,如目录和文件名等。
第一部分和第二部分用“://”符号隔开,第二部分和第三部分用“/”符号隔开。第一部分和第二部分是不可缺少的,第三部分有时可以省略。
在交互式编程环境JShell中,实验Java中的URL对象如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_1.jpg?sign=1739697379-IzfcZhERSeqLwXBb4M8kt6Eu7kPncMWe-0-2e55c29d13cdfe9fa26d4dbdeb065c37)
按组合键Ctrl+D退出JShell。
可以通过DNS取得该URL域名的IP地址。在Linux操作系统中,DNS解析的问题可以用dig或nslookup命令进行分析,具体如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_2.jpg?sign=1739697379-71ZPjiNqResliY5zwG3zoGZq6hkaOfsy-0-9216b85b1f61a40f78289db375d81b4c)
如果需要更换更好的DNS域名解析服务器,可以编辑DNS配置文件/etc/resolv.conf。
Windows操作系统中也有nslookup命令。可以使用默认的DNS服务器查询IP地址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_3.jpg?sign=1739697379-r2V4HefjrfUHKJOVcH01dVKMG4neRyZE-0-1bfc14763372af96a54f3a56741d8c3a)
使用指定的DNS服务器查询IP地址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_4.jpg?sign=1739697379-qONY9VOAQqVbJ1QOtgGXhLSwegDgsnlO-0-01aaf2d378bf7936d0f2928a4a867be6)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_1.jpg?sign=1739697379-lX0C28twp63sYfQRTWILzGPOL4l85WJx-0-23b5150571aa498914f817ce97cccf59)
下载一个网页文本的简单例子如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_2.jpg?sign=1739697379-XEBFyeUAA6OwlEMyZnQI5oQwUBhvMkgG-0-e66f87826be56e94798caa2575676278)
需要注意的是,这里没有下载网页中相关的图片等,如果要下载网页中的图片,就需要分析其中的<img>标签然后下载。
Web服务器不仅返回了请求网页的源代码,还返回了头信息。使用curl命令可以查看到返回的头信息:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_3.jpg?sign=1739697379-hrXGXrILMR8oU6djqSUNkMWZ6vyXcini-0-13bd4be4fb4ba1196d33344caf26d9b6)
返回的第一行结果中包含了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=1739697379-YSUoArOrhaZCCHwprCMNnT2tV9JaCmvF-0-bb5a984b0602164b6ab2622a437a81e9)
使用HTTP客户端开源项目OkHttpClient得到HTTP状态码的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_46_2.jpg?sign=1739697379-2vbmnoxHPjTOy9HQGzXCd49ViTmuhJYf-0-b83a04a196299a28b402c46c2d7d544a)
2.1.2 重试
为了使爬虫可以长期稳定运行,需要处理各种超时异常,并且在放弃下载之前需要多次重试。最简单的重试代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_47_1.jpg?sign=1739697379-C3vSnU03sQIjs2izNWMaAtgv7kECRsaB-0-eff4396cbf1755270ccdec315a073ba3)
需要注意的是,这里只是捕捉了IOException类型的异常,无法捕捉到所有的异常。如果要捕捉所有的异常,则需要捕捉Throwable类型的异常。使用HTTP客户端开源项目OkHttpClient下载网页并重试的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_47_2.jpg?sign=1739697379-vW4ocBSol5OIVV8vZpeVka6ZO5pnklHl-0-7e3b5df5564eee62b326bbde1b058049)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_1.jpg?sign=1739697379-xlHkvzS42B0tuv72llSBDxBINjvH6wt2-0-4b3c67388c175b14f104ea2df9887298)
通过注解设置最大重试次数:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_2.jpg?sign=1739697379-8KVRa741ZQ2XYtfLlSaJdQEHAyUcBKC5-0-48a24a7c7e19021ec19f90f661c6f9eb)
下载类使用注解声明重试次数:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_3.jpg?sign=1739697379-jYbfKt9ZKREBbfoBDv33JGe6NxRJC4An-0-1e5138a80659e93fb46dcbdc35b61ede)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_49_1.jpg?sign=1739697379-aQy6UpLcv8UkibFQ3NsWNVBoBTMxey7K-0-05da855d767e785b95812849b2934e88)
实现重试:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_49_2.jpg?sign=1739697379-Fkdqvgfs8nrZJDcJNBNlbKFm19fm9fV7-0-2cc2e7f18ee981ad9e02df688e6124cf)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_1.jpg?sign=1739697379-F6FUJts45IznTJalLzXT0x0zn2BCBffk-0-d7cd4d4a9c0ceeede29f6fb4e31bfdae)
Spring Retry是一个支持失败后重试操作的框架。为了使用Spring Retry,需要先在build.gradle文件中增加如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_2.jpg?sign=1739697379-bP0angzzhjct8PZ99lxa74pHGvlbUqtO-0-2b34d48a1a9a4f8cf5f84b999a6c52b8)
首先定义一个需要重试的服务类,然后在配置类中提供得到这个服务类的实例的方法。下载服务类DownService的实现如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_3.jpg?sign=1739697379-mWXa5PASRclRS6paTc5NXjR1MiAZbvuf-0-3847f5049f8b6d365a70c789b19b7a1c)
应用程序类CrawlerApplication调用服务类实现重试下载:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_4.jpg?sign=1739697379-NoXb4A3sixRmlYLcB7TdkAtDnW8hcBzy-0-166b6b773acf5efd0f51675d85536edd)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_1.jpg?sign=1739697379-SYD8qEp8UZpdlPe8Pn7AdFUGGCZGC28B-0-43802a85451ce66c0d3392822fb35147)
为了改进Spring Retry,可以下载Spring Retry源代码,然后在本地修改并编译源代码。
可以使用git命令下载源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_2.jpg?sign=1739697379-HzfSZ5H4GSbo2M29uVlnR5BD2GDvQi2s-0-022d645c0244aac4c4b52291b35e5674)
也可以使用svn命令下载源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_3.jpg?sign=1739697379-Ye0aWIi5zzilVHJzLoeSjmzAJfXWnj9V-0-1530f7ad01275afa5b13348a13eaa789)
使用mvn命令编译源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_4.jpg?sign=1739697379-5kg9AgAPUQG73DpXtIqKvkCDMkC8qNdW-0-5d8d71e12c411bf07b828639ee72ef89)
为了忽略编译过程中的错误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=1739697379-pHVhTcZxB4sJIGuOJPd8XUWsD3phdGGL-0-b20e1dc72d316e5870f7e6441f48ec89)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_1.jpg?sign=1739697379-ifySBgRsvYmS2UtBgnjMHkj0avrUc8Pn-0-8c65afcadc504b90d0a900b3bfcc00d1)
创建一个项目,用于测试打包出来的spring-retry,将这个项目的build.gradle文件设置成从本地库加载依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_2.jpg?sign=1739697379-DW7inHAKJZVMZxJQvrX0M9JHo1MGPGEb-0-9e4477be1c6f9adaf8014fb73c9eb838)
测试支持重试的服务:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_3.jpg?sign=1739697379-XotBXRutiUKiHeARBUD6776BUEbpSZF8-0-639f57d74f709bb7b8019fe10a0e22d3)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_53_1.jpg?sign=1739697379-yPMMzNf2rbrNTjmi4kipCN4olKIudNUt-0-2e8e9e5fe848cf7407128a9e2bc8f5d2)
2.1.3 网络爬虫的遍历与实现
通用的网络爬虫通过对URL链接的遍历来获取所需要的信息。基本的数据结构包括一个待扩展的URL表和一个已经访问过的URL地址表,如图2-2所示。
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_53_2.jpg?sign=1739697379-8SXBisVQMlrKyFpVEwDAzr24a9N8uEia-0-07a91ee926f0319d4fa3a2040a5bc10c)
图2-2 基本的数据结构
在抓取网页的时候,网络爬虫一般有两种策略:广度优先和深度优先(见图2-3)。广度优先是指网络爬虫会先抓取起始网页中链接的所有网页,然后选择其中的一个链接网页,继续抓取在此网页中链接的所有网页。这是最常用的策略,因为这种策略可以使网络爬虫并行处理,从而提高其抓取速度。深度优先是指网络爬虫会从起始页开始,一个链接一个链接地跟踪下去,处理完这条线路之后再转入下一个起始页,继续跟踪链接。
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_54_1.jpg?sign=1739697379-ZexBER2WahBQqHfr0Ok70tGGDm25mrPn-0-30cc60ca1309bace864304104b7e2851)
图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=1739697379-iDjewwoZ5C4A8ERxo3TyTkDmW1CRF8u2-0-dcaedefc846dba106ea60a3b902438af)
读取指定单元格数据的方法如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_56_2.jpg?sign=1739697379-9LMf91jdyS4oWxqNuRU8j0Cy9wR4HYd9-0-9d519f1ab11f67b46fe4565e8e294ef4)
读取Excel表格中的种子列表:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_56_3.jpg?sign=1739697379-pNNVqRmnZXmjpYr8WhwO9OUvQUqmRtCi-0-fe52ad8ba4c4a640781701ed45e62fb7)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_1.jpg?sign=1739697379-IoyqQHn2152zXMSbhoQoMIoVIgIY0AYI-0-699fe5834002b4eb80b500fe85ae5d03)
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=1739697379-hYcWh6CukEi0EViFfziYZwzhPTBcPzZv-0-87f35d137b44d4af9de35fb8c14aa322)
实现这个接口,然后把要同步执行的代码写在run()方法中。实现run()方法的Test类如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_3.jpg?sign=1739697379-w8QQ65Ne3sAs9IkH3cu4gjq4zZeSg1Pk-0-54504b414bc761bda7442c3d11231d44)
运行需要同步执行的代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_4.jpg?sign=1739697379-jTMSQ3KZVLOj0HaDGwnlSoWFbZMiDjoR-0-8890185f4359f6772b431147874bde95)
可以用不同的线程处理不同的目录页,如下所示是下载新闻网页的示例:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_5.jpg?sign=1739697379-9bjZatAPfr9ZGYMab3icukg7bbMECDtB-0-d6c324e5dbd8ab17ad0f92ab0e60f1e7)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_1.jpg?sign=1739697379-DxYlvtzCpCHbm4LjW6QZIHdzvfgwVZ8E-0-d90276cb0a9027415fd18019cc5220e4)
假设需要在主线程中统计最后抓取了多少数据,则需要等待所有子线程完成。一种实现方法是使用ExecutorService来管理线程池:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_2.jpg?sign=1739697379-8cQHCMwH7RHFuZYdQzXuj2L82tmGHRk7-0-4928ff2117eab136d190e90a88ec1c5d)
2.1.5 Log4j2日志
为了方便调试,可以在抓取过程中记录日志。Log4j2是一个开源的日志框架。为了使用Log4j2记录日志,build.gradle文件增加了如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_3.jpg?sign=1739697379-cE3YqUduqAs42f1cUcUQVm19166DGDPV-0-ee7c29a57abe885b2d74af603afb0fef)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_1.jpg?sign=1739697379-xiKb5OSb9OKbD7L6VfI3T1FsNWeEPaYo-0-06ef16f0c1c0677c303900a23bc4cd97)
为了使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=1739697379-3r3lirq5d7vtEs3eyyNaKGUSQKLCBR2m-0-974140464aad2294d705db14600f333d)
接下来使用Log4j2来记录日志。如下所示的配置文件log4j2.properties将日志记录输出到控制台:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_3.jpg?sign=1739697379-KWCsLuQj3MuVOjIOIct7OuURc0XhjyL7-0-a5dbfdb9b503250d5c60d8afc4f036fe)
首先通过LogManager得到Logger类的实例,然后调用logger.info()方法记录日志。记录日志的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_4.jpg?sign=1739697379-XyyHxYSYtQuXwdRuwVI6PFe18V0pahFb-0-f4930409ef052519db5be06c914b3faf)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_60_1.jpg?sign=1739697379-xQnvfOWJIz15TDLkMt6HEGznpjrQkJtd-0-f1fc4c229e8cb957c5ba762898b8c6f9)
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=1739697379-a0KjB2k4Cn2sNVjl10qZf2HPD86rDovG-0-35d13f2c45efc081664fd333146cc4e1)
如果需要把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=1739697379-1XsShKOxoiIOYctUlRs6qPhYnN19XLnE-0-88cf37368dd13d031e9f393d3c8561ea)
Berkeley DB用到的对象主要有以下几种。
● 新建环境变量:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_60_4.jpg?sign=1739697379-l7OmkRA777GZma72gIxUBik9FMIWf5Ly-0-f09fd88ad51ef33fddb676589c29cf54)
● 释放环境变量:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_1.jpg?sign=1739697379-RC7pXPWa3DUm6AWSKM06O9Ho5LksSIbx-0-a313a2e2fb8cea073ed38f681c1d8a7e)
● 创建数据库:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_2.jpg?sign=1739697379-sf97ld5HJSHeglYSav5uo9WFzmSVT7op-0-ad043361459164f57e73b4d7a31cf8f9)
● 建立数据的映射:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_3.jpg?sign=1739697379-vxdwC4eO0lvYi7y7zLAdMv7GpC43Pfpc-0-54cc538d576581931b087d963214ee35)
使用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=1739697379-PmuVVa1EPoaedoq56cfoMiFwBU22cVy6-0-1110a933321b0cac26d95517bc031932)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_62_1.jpg?sign=1739697379-u59u73Ki7EE0AXye71rN9s6QFbpumqoG-0-44204bfb3a8134713d3ce25d8596b70a)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_63_1.jpg?sign=1739697379-tJCyKtimBSfWqBhBY5yLKKTrgDdZxVzF-0-574a5fe8a037bd364227d8b60f91c84a)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_1.jpg?sign=1739697379-9G0g6nD6eZF3Czbaq5yRXU8JrjGMgyFn-0-924ea5a11d15728a32e2e31503ea5957)
使用DocIDServer类的测试代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_2.jpg?sign=1739697379-qvFDYcRL93nMK0jJIYj3DoUYwYZpqEoZ-0-6cdc646b8a01bb49eb8bb065e3ef2069)
判断URL地址是否已经抓取过还可以借助布隆过滤器(Bloom Filter)。布隆过滤器的实现方法如下:在内存中开辟一块区域,对其中的所有位上置 0,然后对数据做多次不同的hash,每个hash值对内存位数求模,求模得到的数在内存对应的位上置1。置位之前需要先判断是否已经置位,每次插入一个URL,只有当全部位都已经置1之后才认为是重复的。
下面是一个简单的示例:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_3.jpg?sign=1739697379-yCJ8iwBCFwjxRSseoQbQcumK26OFo2Ap-0-1bd049cf2f51549dd116ef6e5f91dc6a)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_65_1.jpg?sign=1739697379-jjhbYfOdOYdkpPFLKNFES5WMW0e24jql-0-9bdd1d7c6951a546294060bbab1242d4)
如果想知道需要使用多少位才能降低错误概率,可以使用如表 2-2 所示的给定项目和位数比率的布隆过滤器误判率表。
表2-2 布隆过滤器误判率表
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_65_2.jpg?sign=1739697379-doKs3KPXbq7IKJ4UOQuNUuP3d9zQK9GR-0-273b2a03626c947bf2d5fe2c71591ca6)
为每个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=1739697379-f2knoAAE0FL5cs3bMSMF4oUy91ynveaL-0-e4e1577b0a14cc203b91cf8ad382a289)
该实现方法计算了k个相互独立的hash值,因此误判率较低。
如下所示的代码把布隆过滤器的状态保存到文件中:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_66_1.jpg?sign=1739697379-WmYrdKZAqZqlNwMZncxNqg4LuH9a2Ftw-0-8b7a6bfb583c68bb26146f4cd76fe1f6)
如下所示的代码把布隆过滤器的状态从先前保存的文件中读出来:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_66_2.jpg?sign=1739697379-AYk6iYxVHuIUx2f7B3FKua0gcccLmM2t-0-397a768fb71c6c688219db8efcac5c52)
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=1739697379-dpaNEpxq7fY0iASOcPmgYP65UCiDkgbK-0-43b206e9058f2856bc49c0da8870b306)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_1.jpg?sign=1739697379-Iy0zgdq6etuUp8iEYZ9mKAjYgObZ7L0x-0-6e7b56740c3c58b02bec06152372ce66)
可以把新发现的列表页放入工作队列。直接处理发现的详情页,详情页的URL不需要加入工作队列,因为当时就处理完了。使用内存数据库记录已经处理过的目录页和详情页:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_2.jpg?sign=1739697379-V1MwZbbC6F0nV0jyPu6Tni5Sd7A1d7nu-0-d809e9318683fb9b01cf3ceac7e67ad5)
全局变量workQueue记录已经发现待处理的列表页:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_3.jpg?sign=1739697379-29iC29wk2GuIUCITcuNjuiphA63JMRSQ-0-f24110a7943a34087d7f4f0996c6653a)
爬虫运行时,先把列表首页放入工作队列,然后使用一个循环处理列表页的工作队列:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_4.jpg?sign=1739697379-2uN4x7UuG4m7jrPtSXCj5hPCY7Jqdj2o-0-6978169501b7d6e8a2896d2d42195aac)
详情页和列表页往往包含一些有效信息。详情页处理器接口的定义如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_5.jpg?sign=1739697379-yIUQSklWDBA4z34IDtT9zoBlbgsoLMSp-0-4d1befef3a44641717171d97e3bd5387)
用NewsDetailHandler类实现DetailHandler:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_6.jpg?sign=1739697379-EsqBm9plnF8dDjvdu05s1pVB2XZar4qa-0-da1158c4d718eef91f66b512c30965d1)
2.1.8 暗网抓取
暗网是指只有提交检索词才能得到相关的结果的索引列表,然后根据这个索引列表获取详情页。
URL中提供的查询词需要编码,可以调用URLEncoder.encode()方法实现编码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_1.jpg?sign=1739697379-sAO5yEl5QPlHTlHNRrEer2YOM1KO2Wp5-0-c882c858cbd25cdc254ef5cbb409be58)
通过列表页的方式遍历临床试验信息:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_2.jpg?sign=1739697379-LHnvLBjQHddyfmRR7GtscSaK2crBf9QK-0-c35e06a5addb289f60169ca1ad9d7e17)
这里通过down_fmt参数指定返回XML格式的文件。
如果要在不将任何HTML DOM规则应用于文件的情况下解析XML,请使用XmlTreeBuilder,用法示例如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_3.jpg?sign=1739697379-EuzmcuiHCegTN1MyyIQQrIQqy7lTHysY-0-a3a7fec0f1a71c1697a04ca46d376ebd)
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=1739697379-FYeYbTqBPEMAHrmGlh9rLqPrpNsZc8Ym-0-da14cb572c3ac5af831c1ffbfdb26074)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_1.jpg?sign=1739697379-7FTNTzvnjy6LQTWzc6UhK1U5lJ8rHZHL-0-17cd342494e9a72c2a043adf280545b0)
从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=1739697379-mNbmPLuasJVA7NFbpNO66KnPMME1E7un-0-04cb3f46c38e7186b45c855cde914086)
使用WebDriver访问网址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_3.jpg?sign=1739697379-mPAdR66GJwBAJKpcMER3fn7zLXZpjxfS-0-b28eb5bda535386f5069b5b087dab510)
下载网页时,后台会启动一个浏览器进程。调用 WebDriver.quit()方法可以结束这个进程,但调用WebDriver.close()方法并不会结束这个进程。
如果只需要得到网页源代码,则可以不加载图像:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_4.jpg?sign=1739697379-vqz14dgeuMOTtSBWK6oeNd310zzfqvzN-0-1827d13c2a135be5247edc0cf374f12e)
需要等待网页加载完毕,然后获取网页源代码。一个显式等待是已经定义的一段代码,用于等待某个条件发生,然后继续执行后续代码。最简单的方法是调用Thread.sleep(),具体示例如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_5.jpg?sign=1739697379-znrjRBdhpOHsSeUK13ECNg39vOKR7vOT-0-a4302cde44512750cb14bfcd6083a62d)
WebDriverWait结合ExpectedConditions可以实现只等待需要的时间长度:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_6.jpg?sign=1739697379-2Igd6ZIgfqGAfh3xPheTjsIZt2E9UqCE-0-52ae4adbd426a7493e0bef18b6537439)
通过类型选取元素:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_7.jpg?sign=1739697379-fxFfHSiQUOHi6Yt40kHopKrh7Qta0PeB-0-6c8622a22a1548a53ffabd62f8a5acfa)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_1.jpg?sign=1739697379-y4KxqZDzmrqD8dnbthICoooz4oUMLLFb-0-550cf20cf6fc672f6b289efa7b540524)
通过id查找元素并单击它:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_2.jpg?sign=1739697379-TDS7ac3fRXDFDZYQHzl8tfxrf1h6cNCw-0-d2418e702ef9c04a0cf8dba61ae89a1a)
可以通过JavascriptExecutor对象来执行JavaScript代码。例如,得到垂直滚动条的高度:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_3.jpg?sign=1739697379-NSkj1vQKy0ZFAwH0W205EbTLWMf4rAmd-0-b96a94d5bbf34866bafc74e356f70928)
逐渐向下滚动:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_4.jpg?sign=1739697379-g20dPiTNB1kOmUNYHBgC8HL7oceYBx3f-0-9290f527f6ef81aa48334c8d7cc1203b)
如果window.scrollBy(x,y)中的y值为负数,则表示向上滚动:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_5.jpg?sign=1739697379-9DyfBhSrYxSPOYwpydfyHi3swYTWKeWK-0-ddc116f79cf67ef38282c7017b2da3d9)
2.1.10 图片抓取
为了能够节约网络流量,抓取过来的图片经常需要缩小到一定的尺寸,如 100px×100px或80px×80px:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_6.jpg?sign=1739697379-zfoFtlDKfDTXiLfvhNjATFkMDIuW4zNd-0-20829647024cb32788217ed4e7502a29)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_71_1.jpg?sign=1739697379-Bgtmtum5ktTELvR3nMuV8BhMqe4VeFmX-0-fd0371f41ff1d20faf8b3b07d616d376)