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=1738936195-Pn2F4UDSgzMAnWSHPWwXXDlTk4Dt8SIA-0-3ba06d891c8cec70683b54e393b9db88)
图2-1 URL分为3个部分
● 第一部分是协议名(也可称为服务方式)。
● 第二部分是存有该资源的域名或IP地址(有时也包括端口号)。
● 第三部分是主机资源的具体地址,如目录和文件名等。
第一部分和第二部分用“://”符号隔开,第二部分和第三部分用“/”符号隔开。第一部分和第二部分是不可缺少的,第三部分有时可以省略。
在交互式编程环境JShell中,实验Java中的URL对象如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_1.jpg?sign=1738936195-6qpJRxkMtKG1bhMwnUv5D2XP4OHt6hrX-0-3685bf338ef9e8db9d9737cec44badf7)
按组合键Ctrl+D退出JShell。
可以通过DNS取得该URL域名的IP地址。在Linux操作系统中,DNS解析的问题可以用dig或nslookup命令进行分析,具体如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_2.jpg?sign=1738936195-GgidqAQ80ZiGD1WwYO8CfDe9AAhm5lnh-0-673602f07c89ed11ec7dee2f7afb2e52)
如果需要更换更好的DNS域名解析服务器,可以编辑DNS配置文件/etc/resolv.conf。
Windows操作系统中也有nslookup命令。可以使用默认的DNS服务器查询IP地址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_3.jpg?sign=1738936195-heo8ypkpWAWoQEygb4qfyYqWdOgwg1rK-0-d2d9d1130256969bfda6657acc1fda3d)
使用指定的DNS服务器查询IP地址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_4.jpg?sign=1738936195-5UaVminwdqlRb7xhKSX8AGFtACd2Yn9L-0-0aacbe8260845c1908474a25cf979c41)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_1.jpg?sign=1738936195-91swYtIvrVTcJM60uKForBZziEXA29S3-0-d7f85232f52e9b00128798bb76453be3)
下载一个网页文本的简单例子如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_2.jpg?sign=1738936195-mM5Jxt5nyemcdnCKdzUG7g7IqGG3lz5e-0-1adf9c814b9ce7e726d9888f3a7ee507)
需要注意的是,这里没有下载网页中相关的图片等,如果要下载网页中的图片,就需要分析其中的<img>标签然后下载。
Web服务器不仅返回了请求网页的源代码,还返回了头信息。使用curl命令可以查看到返回的头信息:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_3.jpg?sign=1738936195-H68Yw1L14ElFjoiOwr9j1P8Ho8J3RGWt-0-5a7d769d718d5cdc866e882445482a17)
返回的第一行结果中包含了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=1738936195-l592wivKdmmn8CYErR3L42vFRL4ZW81g-0-876f86b0b4d2c1f85c26c9a7fbc2dff2)
使用HTTP客户端开源项目OkHttpClient得到HTTP状态码的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_46_2.jpg?sign=1738936195-UAPIwuDtGsI9prAu6F9BoXKlDAHgRYvZ-0-28c12c0636f2bb3786bbedd8f0fa5757)
2.1.2 重试
为了使爬虫可以长期稳定运行,需要处理各种超时异常,并且在放弃下载之前需要多次重试。最简单的重试代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_47_1.jpg?sign=1738936195-LvRF0LC4tBGqzdINgHbJT3YsMo0TKPd6-0-b13135fbf97cd0c65d4d73a169139b41)
需要注意的是,这里只是捕捉了IOException类型的异常,无法捕捉到所有的异常。如果要捕捉所有的异常,则需要捕捉Throwable类型的异常。使用HTTP客户端开源项目OkHttpClient下载网页并重试的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_47_2.jpg?sign=1738936195-F1pxmPTUtVeSAYaO50x3Q09wVjWvKCL3-0-ba9dfa53355607691cff1cecdbf87182)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_1.jpg?sign=1738936195-9iStO7LXN0MmIF4exjFg5dOTZNKpYwIa-0-27c04628795df8cc6a8dcb5dc88ba685)
通过注解设置最大重试次数:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_2.jpg?sign=1738936195-1BXqLOXkrYoZ2bvcaaqNciZlx18fgz85-0-7551148432d9859bf4a7187422450b3c)
下载类使用注解声明重试次数:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_3.jpg?sign=1738936195-VFusCV14innfetAEBvqSsxaKsqvCB1FN-0-7ff0ab9c25561849656b7d9a60cbb707)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_49_1.jpg?sign=1738936195-WXcQor6Mq2QJf1dK6vl5YEjMEHTvmst9-0-c4880562fc9e8b114c889253964e20e1)
实现重试:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_49_2.jpg?sign=1738936195-jWMCAwfOTxN3lkOlj3PYIr4jVfYudH17-0-9a57f137c9b45d06eeb67a617bc29403)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_1.jpg?sign=1738936195-sRUKC2f5YbvJrqH4IYAU22sWA9Htm5wg-0-9ce8c5b89dbe1b79e58c6bc2d239477a)
Spring Retry是一个支持失败后重试操作的框架。为了使用Spring Retry,需要先在build.gradle文件中增加如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_2.jpg?sign=1738936195-5bkkJkR9YC4CoQC4fp2w2vE4mn5UMC6h-0-aeceb0c9c6d3f026982d9656899f536e)
首先定义一个需要重试的服务类,然后在配置类中提供得到这个服务类的实例的方法。下载服务类DownService的实现如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_3.jpg?sign=1738936195-uJP9RmOZdbhDI9ucyVExoMTVoUWCEfqA-0-288444a9978d9456f45e2829d22408a4)
应用程序类CrawlerApplication调用服务类实现重试下载:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_4.jpg?sign=1738936195-fAj4ii1e5xzgwOBXtFeS3ZNVKPN7qpzM-0-a3ae2c6602663d56fe2813b6060b29bc)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_1.jpg?sign=1738936195-a1njlEYl4HYyXPSJTazugu6xlDiyH3km-0-5416984f47b870c6c95fb3131389a178)
为了改进Spring Retry,可以下载Spring Retry源代码,然后在本地修改并编译源代码。
可以使用git命令下载源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_2.jpg?sign=1738936195-5R7EM3ljOA31aR0MlWh0R9vROw7hFPlm-0-3a906228eb24d7e86671e11c3253993c)
也可以使用svn命令下载源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_3.jpg?sign=1738936195-wprXaODnkYzAgh9UmoLiBa2rxP41dU8I-0-315b51536823c9674755b42336cfa7cb)
使用mvn命令编译源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_4.jpg?sign=1738936195-M8ZCLrLN1XCEWGRiJpIrstJ0qSXtY45R-0-efda3e988999d7595252dea0823d0adf)
为了忽略编译过程中的错误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=1738936195-ZRaRw2viTwUD4VlTB1PCDCPVUJ6IFd1m-0-5ac6a252b6d7fb9b64af75e624d8a9fe)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_1.jpg?sign=1738936195-DOvgMIBiMQz6RIi9DuNOqLeezgQYVX8H-0-b2258764e5bb8556a7b61df3d912db66)
创建一个项目,用于测试打包出来的spring-retry,将这个项目的build.gradle文件设置成从本地库加载依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_2.jpg?sign=1738936195-f4yU2U1uaBpJFQixFZzgZx17saqX5XBc-0-3f8fae7595196b9c01ffe106d7b1f76a)
测试支持重试的服务:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_3.jpg?sign=1738936195-Rtc9kL46C9fb52R4ONd49WrbCa5yZpqn-0-8c64de0fc1af45aa50035aa69dc82a94)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_53_1.jpg?sign=1738936195-wCurfxAmpBAern8LivYzsYR12dVbedM9-0-fb0faa90d94b1904ea7fcdd22db16d7e)
2.1.3 网络爬虫的遍历与实现
通用的网络爬虫通过对URL链接的遍历来获取所需要的信息。基本的数据结构包括一个待扩展的URL表和一个已经访问过的URL地址表,如图2-2所示。
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_53_2.jpg?sign=1738936195-nlEY4ocZwVHZTkuiJq2RtenQt4EhHuw1-0-51c2bef7f987a26144adc746f3818298)
图2-2 基本的数据结构
在抓取网页的时候,网络爬虫一般有两种策略:广度优先和深度优先(见图2-3)。广度优先是指网络爬虫会先抓取起始网页中链接的所有网页,然后选择其中的一个链接网页,继续抓取在此网页中链接的所有网页。这是最常用的策略,因为这种策略可以使网络爬虫并行处理,从而提高其抓取速度。深度优先是指网络爬虫会从起始页开始,一个链接一个链接地跟踪下去,处理完这条线路之后再转入下一个起始页,继续跟踪链接。
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_54_1.jpg?sign=1738936195-RvI54dHCQXiSRtHmXzwy9qG6lVs5uJ5u-0-eacc2cf272dd9e9b21370a652989c212)
图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=1738936195-fO3UsEZxn8yiLtrhkmDha7H9oDutN3Yr-0-ec719d6e9d91969b768a9a03cc047ad5)
读取指定单元格数据的方法如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_56_2.jpg?sign=1738936195-N3sdi1jf4s7kl0hGHe6F7cWZGNGeCd7w-0-ce48b950670f0c9a1a6cf571fc2ecda2)
读取Excel表格中的种子列表:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_56_3.jpg?sign=1738936195-HzLDv0E036D5wlQoM80THBZu3UNvPy6J-0-02294fd084ca44428cc46a6cb1f36547)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_1.jpg?sign=1738936195-sr6HjglQ5WZ3yXOEBWRFDjWRjaJsWDuu-0-1a057c584019623747142cfdbe343695)
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=1738936195-RiUDKJNr9LynwfPRpZz5zND6EIBeBlyJ-0-d8c357b47243ca46b9850ff750a9fcea)
实现这个接口,然后把要同步执行的代码写在run()方法中。实现run()方法的Test类如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_3.jpg?sign=1738936195-vXGvI16Z42OMbyOBDZlPS6h79xLWufHE-0-1e9d978ac5560c555ca8f54ee36561d9)
运行需要同步执行的代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_4.jpg?sign=1738936195-jRhQScsSjSZmdmsRqAiduWWPN8KbFKC2-0-427c68609b7baf6ad295056cc3a1c6fd)
可以用不同的线程处理不同的目录页,如下所示是下载新闻网页的示例:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_5.jpg?sign=1738936195-Hf6SVOi31aZYEzjFA5wnpEYAe0QI8kft-0-f024633858f462fe6c57ef892d96d010)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_1.jpg?sign=1738936195-5G6AVuYA1r3RJkwj7XZbTBkuydrHnaOb-0-507cb222593978e0699c8afed0594b08)
假设需要在主线程中统计最后抓取了多少数据,则需要等待所有子线程完成。一种实现方法是使用ExecutorService来管理线程池:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_2.jpg?sign=1738936195-OPXkWZgFgHZky1RUjHqAuZ8aVeVHsLkZ-0-be9f30e10bbf4069bd5ed297233d3ff8)
2.1.5 Log4j2日志
为了方便调试,可以在抓取过程中记录日志。Log4j2是一个开源的日志框架。为了使用Log4j2记录日志,build.gradle文件增加了如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_3.jpg?sign=1738936195-YdUF5qvtR8Gu8tXLkOtzgT7eRw5k1HLF-0-40bbb5777cf04ea152c3e6cbc971df4f)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_1.jpg?sign=1738936195-ZqVdqXdSavhof8A8Ub69MavXp1et4lDN-0-200db32833f52ba4d182f34ba571f243)
为了使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=1738936195-EfeXwbdbCc1z7Ly0zFMETgnU0JKVu7Uo-0-855b6ec3f853512584657ef38a271181)
接下来使用Log4j2来记录日志。如下所示的配置文件log4j2.properties将日志记录输出到控制台:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_3.jpg?sign=1738936195-bdQq85rlgBczpkJGecW2R7qnhUvXTYzl-0-0672bf36dcde638d4de73c04a0bd0520)
首先通过LogManager得到Logger类的实例,然后调用logger.info()方法记录日志。记录日志的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_4.jpg?sign=1738936195-0FrwSS4ZSe9TwmV5MhghZgYQIk7EMuwS-0-30618f6279e30d585210d7a2a00c1dd5)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_60_1.jpg?sign=1738936195-jaBCsB3Wurnb1mcKqCMKZR5nlAjduycZ-0-909b6879c2bc8dba9a80362f8ba35c43)
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=1738936195-ntxsRg1Re4yTwg0fzD5RCBYVAaDenuYh-0-8a88ea38706af7452cf076a3cd435d0a)
如果需要把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=1738936195-3vzkiwxLwKjays9tOigZIoh4xDVESA60-0-ef93186a490ce6b52ecd4a5465e514e3)
Berkeley DB用到的对象主要有以下几种。
● 新建环境变量:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_60_4.jpg?sign=1738936195-58JE2Let8ExGbgct614verkyC6AwWFeN-0-29c08936cb23b244292ff50a0e3a59f4)
● 释放环境变量:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_1.jpg?sign=1738936195-xxTM6I11UDahlD6mZ6X7Su8KNMx2LJ4p-0-d41f6e280f57017f163095e0ef323433)
● 创建数据库:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_2.jpg?sign=1738936195-JJfYrXtePwVRV4FxKJdAeRDRybBZOuER-0-7c30d4f43311766b2c0929333db76907)
● 建立数据的映射:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_3.jpg?sign=1738936195-QHq0dfsmuUOPbDgtSjit8rv45COVpU1A-0-16810e58c218fa70bf859ef13a2add0c)
使用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=1738936195-o59LYthvoYYzHEKUXpKczKSbE8UlqVhe-0-a8031e345d1dba7094cbef0b7df73779)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_62_1.jpg?sign=1738936195-CsRuXliXF3VHxKWBSAVHMM0zHFwn5PyT-0-ba1bdb9e6ab66b190dee343f00c97cab)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_63_1.jpg?sign=1738936195-EnNaw8NclVLhfAairVYjdf0SAFSvL6p4-0-c575ac09a1636edcf2b5e08f1ace9ab8)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_1.jpg?sign=1738936195-vfrPPrC2qXa5MnFnqH4CUpZCIHXu3iAj-0-8274760fcd21d130baab1d2fcad05acc)
使用DocIDServer类的测试代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_2.jpg?sign=1738936195-gPXz1qA9wmh34Z8qpJBLxQAqmwuZddBK-0-545d8a13fdf2a2ec31d46f9cbb15f7f5)
判断URL地址是否已经抓取过还可以借助布隆过滤器(Bloom Filter)。布隆过滤器的实现方法如下:在内存中开辟一块区域,对其中的所有位上置 0,然后对数据做多次不同的hash,每个hash值对内存位数求模,求模得到的数在内存对应的位上置1。置位之前需要先判断是否已经置位,每次插入一个URL,只有当全部位都已经置1之后才认为是重复的。
下面是一个简单的示例:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_3.jpg?sign=1738936195-wSXJqX2SfuASusO6KwtrbBKgNmXGzATS-0-6676de1d5e2696f5b3f0d3e54aecf5c4)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_65_1.jpg?sign=1738936195-Db5R89LCUQ3UvS3GYD4ktihsEiWQAF2S-0-1b81287cbbf76bf26b38881b26fd777f)
如果想知道需要使用多少位才能降低错误概率,可以使用如表 2-2 所示的给定项目和位数比率的布隆过滤器误判率表。
表2-2 布隆过滤器误判率表
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_65_2.jpg?sign=1738936195-WR6QNEiTQGVHIdHWE0UzIrlxXumDO5mH-0-1012e35cfc8c8b9a3a0f524ba06f6c87)
为每个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=1738936195-TDI3IvVqknzNbUV3aq9LBdXHykmgWlCa-0-3cb04ab3d303c72dbc292bf11706150c)
该实现方法计算了k个相互独立的hash值,因此误判率较低。
如下所示的代码把布隆过滤器的状态保存到文件中:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_66_1.jpg?sign=1738936195-kLeRvQW589Hir880dSpJbroO5jNlpXC8-0-823782cbc9ba98ef96b88d0614b08bf8)
如下所示的代码把布隆过滤器的状态从先前保存的文件中读出来:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_66_2.jpg?sign=1738936195-jtzaLiverX7xD8tGB86TUspZ8vRPdCup-0-be72446c455606421eb92f798f5999ee)
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=1738936195-4CFLmcINZdHpwacc9aP1OVCrvk0PS4KK-0-167cacf570e43170c36eaa7436930a08)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_1.jpg?sign=1738936195-OijPNXSoDNXWvxr449Tndbpt9hWOHuTw-0-0f00e99c54f4451f6add37c81e947855)
可以把新发现的列表页放入工作队列。直接处理发现的详情页,详情页的URL不需要加入工作队列,因为当时就处理完了。使用内存数据库记录已经处理过的目录页和详情页:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_2.jpg?sign=1738936195-lQ3tKx1JZcbTbb8zNqDE4FvD6R31XJSj-0-ef6f7451c5e78d4e4497c542a23c9498)
全局变量workQueue记录已经发现待处理的列表页:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_3.jpg?sign=1738936195-6Wg0RMTCdBH74VOnooHTDJsbSklXqWq2-0-43f647f9abaac0c69524c4984fe7f4f1)
爬虫运行时,先把列表首页放入工作队列,然后使用一个循环处理列表页的工作队列:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_4.jpg?sign=1738936195-M2FSSZfCbXma1dtvsUySWJbnJUE37cpE-0-4a8bd7a70efd817861ae67de06c617ed)
详情页和列表页往往包含一些有效信息。详情页处理器接口的定义如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_5.jpg?sign=1738936195-1RKOcwBggzPndNq59BJCiwXIu8O8Q2JI-0-f0f647560832f0b0baef44a26422bf9c)
用NewsDetailHandler类实现DetailHandler:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_6.jpg?sign=1738936195-E29oPzKoUZOzaB7vXrAR6Y1OiUjJgUdK-0-c32c4f87029daf2721a150cabfa87e64)
2.1.8 暗网抓取
暗网是指只有提交检索词才能得到相关的结果的索引列表,然后根据这个索引列表获取详情页。
URL中提供的查询词需要编码,可以调用URLEncoder.encode()方法实现编码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_1.jpg?sign=1738936195-5y80aBIwJuI8D2KsoCIO5jfwy7riSApk-0-80ec951fd441e2c9edc5884b0390f035)
通过列表页的方式遍历临床试验信息:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_2.jpg?sign=1738936195-43gryYgeOdbncKdRstF6dOgN0Q5QaJ2y-0-fd4a523160eba47c7c7e5458a0317d7c)
这里通过down_fmt参数指定返回XML格式的文件。
如果要在不将任何HTML DOM规则应用于文件的情况下解析XML,请使用XmlTreeBuilder,用法示例如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_3.jpg?sign=1738936195-Wd50MCKaCE35bUnewQ2z7YugyQQY0PBd-0-9c127836500581840350d90c0b6a1038)
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=1738936195-g2sU4PQhE3VPMcItEVlKLhS1H6pVDxDi-0-bcea6f32ad2176d4b22dd8e500b6c1ab)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_1.jpg?sign=1738936195-zfm2cix1eJxM9o4eUicJFuPNer5LKsrT-0-8d531abfbbf8c02969a484a56db244e7)
从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=1738936195-xZfkqQZaf0kWHZAEWMKfW4dxeXNL2zA8-0-7af6466152b46db733c9b6d15a654933)
使用WebDriver访问网址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_3.jpg?sign=1738936195-FRPZEPUAuNC4vjmUIFH6GOzefMYm3Os1-0-ee88eaf6d4a39e29419237f7915e4cb2)
下载网页时,后台会启动一个浏览器进程。调用 WebDriver.quit()方法可以结束这个进程,但调用WebDriver.close()方法并不会结束这个进程。
如果只需要得到网页源代码,则可以不加载图像:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_4.jpg?sign=1738936195-iRWqseIwgUZzOQYyOYrPoqjI9BUkBZLK-0-1a5ba2d604b357173f8c3e53b5fa2e9a)
需要等待网页加载完毕,然后获取网页源代码。一个显式等待是已经定义的一段代码,用于等待某个条件发生,然后继续执行后续代码。最简单的方法是调用Thread.sleep(),具体示例如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_5.jpg?sign=1738936195-12TqBP6Buw25M7kpq1OhOdrtv6ZMLatR-0-9447a838dea366c620aee08d971d0d1f)
WebDriverWait结合ExpectedConditions可以实现只等待需要的时间长度:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_6.jpg?sign=1738936195-VkRTioCbCNkAmBYp1HB4FhSAsf9e8Qq1-0-79e9bc710029a67c2a9799af6a728c96)
通过类型选取元素:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_7.jpg?sign=1738936195-KOubJBNa4b0FzptQ6nuZm3tYiBrtFPuf-0-4bf51c78d9700d45124d3eb30ed7fe2d)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_1.jpg?sign=1738936195-yDRNW5KxeidtGcXuoIU54NIf2xTOWXvV-0-54da2b47f96dfc3b14fc65902b3865d5)
通过id查找元素并单击它:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_2.jpg?sign=1738936195-pSTtZs9ujt3pWRCnwA7plDBdPljjUnVX-0-9d023b34819d9f837d723df6b6ec1fff)
可以通过JavascriptExecutor对象来执行JavaScript代码。例如,得到垂直滚动条的高度:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_3.jpg?sign=1738936195-41qUC5SYTYNO2sfqWDI1idP0MdokhlOp-0-6cfdb761fece979596eb3c900be9936c)
逐渐向下滚动:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_4.jpg?sign=1738936195-lQIIMXzjWmoMOKP6hhNQfFwoP1iEXR36-0-5a6be0256800523280d95e45751baa65)
如果window.scrollBy(x,y)中的y值为负数,则表示向上滚动:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_5.jpg?sign=1738936195-cLMr0t7yCiCKAfjUIaCcPL1aEvNsUnR5-0-2bed07e3276bd4fabffc35a0bea65852)
2.1.10 图片抓取
为了能够节约网络流量,抓取过来的图片经常需要缩小到一定的尺寸,如 100px×100px或80px×80px:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_6.jpg?sign=1738936195-yLwzkRhXt8TMqhs5RLhjeVe2o5GdBubr-0-611bf4ba74ecf086444241e6ce25d2c9)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_71_1.jpg?sign=1738936195-fPH6AmkqmENGnHRp39ca1vMtnqWu37xq-0-4ed8b60c425fca62348fdbb7fb765a2d)