![搜索引擎技术与发展](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=1739192699-BcPQcy4p3Y6aYNQ4XHbbBtXqZHatWaLL-0-eac7312b129542166da358d5b15d92c2)
图2-1 URL分为3个部分
● 第一部分是协议名(也可称为服务方式)。
● 第二部分是存有该资源的域名或IP地址(有时也包括端口号)。
● 第三部分是主机资源的具体地址,如目录和文件名等。
第一部分和第二部分用“://”符号隔开,第二部分和第三部分用“/”符号隔开。第一部分和第二部分是不可缺少的,第三部分有时可以省略。
在交互式编程环境JShell中,实验Java中的URL对象如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_1.jpg?sign=1739192699-0MA1hb0clYfiE3WWF9RsBVDaVDWma4n8-0-4f9caf83dcd8e15f727e638a32675f6c)
按组合键Ctrl+D退出JShell。
可以通过DNS取得该URL域名的IP地址。在Linux操作系统中,DNS解析的问题可以用dig或nslookup命令进行分析,具体如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_2.jpg?sign=1739192699-5bzK8xeG2zg1ciwxGsv4RgHC8K4kEZsP-0-4bec059debd19134fb553b09c441e83e)
如果需要更换更好的DNS域名解析服务器,可以编辑DNS配置文件/etc/resolv.conf。
Windows操作系统中也有nslookup命令。可以使用默认的DNS服务器查询IP地址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_3.jpg?sign=1739192699-pOGdwV4tgMxWOOfdwTzLKUkkZGuC2Qlb-0-e2f27f6877aec816ebc1e70f3f77a86c)
使用指定的DNS服务器查询IP地址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_44_4.jpg?sign=1739192699-takk44ZbqWqjBMFg7VwBWtHsg74O0uez-0-4526612881d9758cd5707cbfaf198299)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_1.jpg?sign=1739192699-GYVcBL3mIOXIZtOPiQ4fPuqITcI6Sryn-0-261b49ab97e70db14c89b4488495da8b)
下载一个网页文本的简单例子如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_2.jpg?sign=1739192699-punEq3obRfb8dyTsudmm0Bjn3TRgnITF-0-0dbcf640b2edaaaf55c3f42ce20d1d6c)
需要注意的是,这里没有下载网页中相关的图片等,如果要下载网页中的图片,就需要分析其中的<img>标签然后下载。
Web服务器不仅返回了请求网页的源代码,还返回了头信息。使用curl命令可以查看到返回的头信息:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_45_3.jpg?sign=1739192699-kgARwmn9yfF8tQx3qoIY1EC7jcmv04wP-0-1f755607f3b040fefecd7a893682caa5)
返回的第一行结果中包含了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=1739192699-5w923TCdn6mhH75kmDgiNd20Vzpj2Iw3-0-4ceef29f443c78df3e6b400aaddca014)
使用HTTP客户端开源项目OkHttpClient得到HTTP状态码的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_46_2.jpg?sign=1739192699-9G0kNHjg5t0kmTJPTu86GbPY2jRNl02z-0-9ff59c444d95624d6e7eb7f287b47cbc)
2.1.2 重试
为了使爬虫可以长期稳定运行,需要处理各种超时异常,并且在放弃下载之前需要多次重试。最简单的重试代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_47_1.jpg?sign=1739192699-WkFVwgykFX68pGboaIOOrXT9Ol6aVWAd-0-e14a3b9111c5fd6788a1cbbc627905ab)
需要注意的是,这里只是捕捉了IOException类型的异常,无法捕捉到所有的异常。如果要捕捉所有的异常,则需要捕捉Throwable类型的异常。使用HTTP客户端开源项目OkHttpClient下载网页并重试的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_47_2.jpg?sign=1739192699-YUD01IBjVN46wkvZ3OEkHPbVhiYkSf57-0-f222fb88759149b5fd55bf45f870b801)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_1.jpg?sign=1739192699-B3tUsop4Gpq7d7Yvx1fLw5o9nsPeKyCK-0-660661dd5f48d527cc4a4351b1b2cd2a)
通过注解设置最大重试次数:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_2.jpg?sign=1739192699-0cWbxZhxVGZzf8k0JXlvx4JJAY1Z9OBh-0-fa68bc3633e6daa7da7dfc30b3a4d3d1)
下载类使用注解声明重试次数:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_48_3.jpg?sign=1739192699-DedFWrKwAW1sQis0BWgGPR52eIH4ToBs-0-35bf1ec4ba0f46e93416213ab1fb9757)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_49_1.jpg?sign=1739192699-5xS7YTmjrN0TiFimhcpKvDDyBMZSQit0-0-797f1713a225ea29a7adf08fde97d54e)
实现重试:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_49_2.jpg?sign=1739192699-tbF76nrSe4ExMC79VayhD8W55LbFgcQF-0-b772bc28d67c5645657895d37c0fb936)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_1.jpg?sign=1739192699-QIUzoHcTOpyi6IZCmehmoXUpopZlEqOP-0-abb7d2d77fef66699c3bc1e268183b6b)
Spring Retry是一个支持失败后重试操作的框架。为了使用Spring Retry,需要先在build.gradle文件中增加如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_2.jpg?sign=1739192699-DftDeuoz6EimVmOMETsDJLM8Xp5fNoGS-0-aab512e767699bec949a8d21a3146dc3)
首先定义一个需要重试的服务类,然后在配置类中提供得到这个服务类的实例的方法。下载服务类DownService的实现如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_3.jpg?sign=1739192699-BemY6nsMOs7kUJqB0dBvhkFBVVPmMrU8-0-e0af972499d6547815fa05503fa53913)
应用程序类CrawlerApplication调用服务类实现重试下载:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_50_4.jpg?sign=1739192699-geHNi8XwzurBWjHqIR1RLUpKZHMuVZ1I-0-ff0e42c479632172a7b4baaec558c3f4)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_1.jpg?sign=1739192699-8WO1T1myrfQcnj67Gst2KuPqHflxGHus-0-8351f6af02ce9a24868b0069fa2121aa)
为了改进Spring Retry,可以下载Spring Retry源代码,然后在本地修改并编译源代码。
可以使用git命令下载源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_2.jpg?sign=1739192699-Pvskv8PRxFNEMJc7cdouMp5BAClEIx4x-0-0794797928c26d4a4d2a459e93eb877b)
也可以使用svn命令下载源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_3.jpg?sign=1739192699-k9VMgxSibBviqH3TYqniCXX9EHMBQUiR-0-76d023bf37bfa8bac7e327cd8ee37088)
使用mvn命令编译源代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_51_4.jpg?sign=1739192699-y4LDcVk7VvkMvBxhpJEUR59VizRPmp8L-0-5e7a410474b994bdad9af85498fa4c8e)
为了忽略编译过程中的错误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=1739192699-ozQqXhdzAhrnX82nTDMQDiJgpxmEtz5F-0-2903d470e531dde8540dfc77ab4024c1)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_1.jpg?sign=1739192699-b9ikZzzmTU73wO1xsC78sVimTjv7dgLp-0-b79e8bc8432c4d7ea8d9c8217cad7706)
创建一个项目,用于测试打包出来的spring-retry,将这个项目的build.gradle文件设置成从本地库加载依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_2.jpg?sign=1739192699-RuMBllwzqNcYhyi9NXy39MsHqJSS1ISg-0-32e4d676a35545a07328f2dfc23c9f02)
测试支持重试的服务:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_52_3.jpg?sign=1739192699-JqJKNMD6AceBUIgePg5zeKhp03WzNGQA-0-d6005c7652a8d415e7a676a7050c79e4)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_53_1.jpg?sign=1739192699-HcmFh3YmKTlumtFPTPJf2573peM8lImf-0-e0218772736507afbf814cf35810de1a)
2.1.3 网络爬虫的遍历与实现
通用的网络爬虫通过对URL链接的遍历来获取所需要的信息。基本的数据结构包括一个待扩展的URL表和一个已经访问过的URL地址表,如图2-2所示。
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_53_2.jpg?sign=1739192699-gAvQImhNfYZiTJp7T7vJhZdyyXyEN4QY-0-80e91c5be1bb7179ed1daea6cd5b4d46)
图2-2 基本的数据结构
在抓取网页的时候,网络爬虫一般有两种策略:广度优先和深度优先(见图2-3)。广度优先是指网络爬虫会先抓取起始网页中链接的所有网页,然后选择其中的一个链接网页,继续抓取在此网页中链接的所有网页。这是最常用的策略,因为这种策略可以使网络爬虫并行处理,从而提高其抓取速度。深度优先是指网络爬虫会从起始页开始,一个链接一个链接地跟踪下去,处理完这条线路之后再转入下一个起始页,继续跟踪链接。
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_54_1.jpg?sign=1739192699-WeG04JH7BxLsuuZuoec0Xp08qsZBlArm-0-6de9f4e06ed0bb775f7865570004da4e)
图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=1739192699-J7nMaMlCjY2XZ2aBCD5ylosALnyWKUxH-0-89ec2de4510f8d65c7058f24328515c8)
读取指定单元格数据的方法如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_56_2.jpg?sign=1739192699-iRYkJOUYtKAweGg7c0cA4y9VDLO8J0Vw-0-17c8ed64ca81100dabc30eaed1225234)
读取Excel表格中的种子列表:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_56_3.jpg?sign=1739192699-qEIl0ynZWELhxH9ekV5FRteRoKI1aQOL-0-885f9e70d65ca09ae11c951b74ef3e3e)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_1.jpg?sign=1739192699-Ithaxt5TQkID6d5nqFbkwfO9gE5tDhD8-0-9685d60c6f7b43d4122bfa3eb05243fd)
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=1739192699-fdgi0WUyKh725jUkXmjhueseHMXrSH9V-0-5974a343cfc3ad6438dba1ef6b2508ab)
实现这个接口,然后把要同步执行的代码写在run()方法中。实现run()方法的Test类如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_3.jpg?sign=1739192699-R9j3VFgfJtrHSeAzvr2qjPsgmX4FOEV6-0-2eaeed45ec7f4d665d835e8abe1bcbac)
运行需要同步执行的代码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_4.jpg?sign=1739192699-efMjzqrl760pHsy1YRLDikZutnDGqiHc-0-3833ca34a5c1c16526ccdcc682b3a47f)
可以用不同的线程处理不同的目录页,如下所示是下载新闻网页的示例:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_57_5.jpg?sign=1739192699-gr8YNhY6fJ8q7VI7QCGFwCX2PAERQ7Go-0-b51279ca304fd464da41f225ec9198a9)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_1.jpg?sign=1739192699-4AxdqsBkLNf93uxSh5gyYZYNGBMcsiak-0-a4f95deda3bf5ce92f3c951c556d4a09)
假设需要在主线程中统计最后抓取了多少数据,则需要等待所有子线程完成。一种实现方法是使用ExecutorService来管理线程池:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_2.jpg?sign=1739192699-v6aA94LT4Y4GKO2VAzpap3U7xZQvqa6L-0-df23d1f015918b0d0b2b16663caacf10)
2.1.5 Log4j2日志
为了方便调试,可以在抓取过程中记录日志。Log4j2是一个开源的日志框架。为了使用Log4j2记录日志,build.gradle文件增加了如下依赖项:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_58_3.jpg?sign=1739192699-V6DKt6ZmQ21nSWT2QcnQ1XvEqFX8hK8A-0-44be994c736f9bdd8902533077c13228)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_1.jpg?sign=1739192699-6rvwAY3JwgfupTPAFcJxtsCSseSdeDJZ-0-8a52949fb5ac9348380bb7dbb4fa3e93)
为了使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=1739192699-R1zrxWazbrYcDiKJmzkKz4AuxHyidyYR-0-2ec3134dafef32db48371baed7e59c4c)
接下来使用Log4j2来记录日志。如下所示的配置文件log4j2.properties将日志记录输出到控制台:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_3.jpg?sign=1739192699-612WZkQwI0E9Xvek2U3B59Pozblxw3oW-0-bf159c949194c98d48ae130f1c0a340c)
首先通过LogManager得到Logger类的实例,然后调用logger.info()方法记录日志。记录日志的代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_59_4.jpg?sign=1739192699-2t28Nj0dSuuOWHXbtXYnQjlHQRt9VeXl-0-319e5b4c61a9f0405a18776a36fd8d40)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_60_1.jpg?sign=1739192699-tp3If4ZVp7bTPEY61qPkqmccGqToshDa-0-692842401b063225490e0a9e12bc8c2b)
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=1739192699-LOztFDhQL6ULh1ucGfNO9wh4iO5hOs18-0-e4c60f92093bcfbdd43d3c0d16b237eb)
如果需要把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=1739192699-C2nJTB6Cvt6Ly6jrwPIebaCXmgwPAHmK-0-92adf9e6b463c634b2af9d2652f3ff7f)
Berkeley DB用到的对象主要有以下几种。
● 新建环境变量:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_60_4.jpg?sign=1739192699-8BPjkYpcWaQPNEKpEtvBW376m3V5NyzK-0-73a85fcb26ab9f1f1fbb1a8ac7915af6)
● 释放环境变量:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_1.jpg?sign=1739192699-NC2wl9rP5HDg7c7TKaXl09bi0rctUWUv-0-8eed9f9c9fc55daa286af33f3079e8f1)
● 创建数据库:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_2.jpg?sign=1739192699-pY3kUm2zGXFXOiDiVfMQoVIgMnNQgiGc-0-821c0730c8e4a821d9df48954e74de9e)
● 建立数据的映射:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_61_3.jpg?sign=1739192699-L29QQHjkAYrwp3u88HY0i7ZMR6oI1tnX-0-ddfaa02e7ec9aea51897bbaa1150f210)
使用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=1739192699-UYSWG6VI8Xdl3eP8I3dxsRRfOfKdItCm-0-44cf76207e9d2768304b2c5b3908bbc2)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_62_1.jpg?sign=1739192699-IKAZStgsrZHik13L9do75XdS9c6T6No8-0-740307337a18dd88880d1331a1cf1cf9)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_63_1.jpg?sign=1739192699-QbpAuymcfPTRSYXemMZJAOoi9g8JJb1D-0-0a6bd662ecdcb4f0af82e5a9cc7f72f5)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_1.jpg?sign=1739192699-BdwbpIawiMNfSuqJUQk3ssw3KqZYlmMk-0-9566f07057ddd9eaeb99bdaf94ff89e1)
使用DocIDServer类的测试代码如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_2.jpg?sign=1739192699-qNlTbrFHUO2LLDz1g7cmsloj9ne2Z37f-0-99c2c73860d0b8d4a754f3a84a4ffb88)
判断URL地址是否已经抓取过还可以借助布隆过滤器(Bloom Filter)。布隆过滤器的实现方法如下:在内存中开辟一块区域,对其中的所有位上置 0,然后对数据做多次不同的hash,每个hash值对内存位数求模,求模得到的数在内存对应的位上置1。置位之前需要先判断是否已经置位,每次插入一个URL,只有当全部位都已经置1之后才认为是重复的。
下面是一个简单的示例:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_64_3.jpg?sign=1739192699-pYbEA1g8UtiLlVwoXqAK5hmZXROBdruk-0-5487ea45f0a303041b7a7c17d1ec8e5f)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_65_1.jpg?sign=1739192699-jSHxTYNkrY10cGHABuSr5MAgIYCLvVfw-0-2bb1949bfcc55d6c01b75ab19f75cab9)
如果想知道需要使用多少位才能降低错误概率,可以使用如表 2-2 所示的给定项目和位数比率的布隆过滤器误判率表。
表2-2 布隆过滤器误判率表
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_65_2.jpg?sign=1739192699-AuC9oYSLyL5q3Zw89Z8UzkJ0YKi2xOMY-0-11519f7fc6815e0789200f6cf5a35a61)
为每个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=1739192699-OlyDa2GafnWXSPcO4TXgRyZNfh7BSQZK-0-2a8f2a2e74c2eefae6f386caba434acb)
该实现方法计算了k个相互独立的hash值,因此误判率较低。
如下所示的代码把布隆过滤器的状态保存到文件中:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_66_1.jpg?sign=1739192699-0mEkTbzloCf6E6xWPIii7UvEk3wUhxdT-0-fa5796b7607f4fd6db5c3ce7107b20af)
如下所示的代码把布隆过滤器的状态从先前保存的文件中读出来:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_66_2.jpg?sign=1739192699-tE8UmO6HMwaxWxWTVTtbevuZV8gXmd2G-0-2235f30beeb49e6fcca6211657638ea1)
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=1739192699-BaTJ4jDSVpJRCxbB7Ap841JYYRKbuRUe-0-8ea46e8145b84fb202cbbe34e7db0d04)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_1.jpg?sign=1739192699-SkyNXpHcsTr2o32TIgIforrpaUbyxYjO-0-8ff65ff8721bd8c0398dca1ac76388b9)
可以把新发现的列表页放入工作队列。直接处理发现的详情页,详情页的URL不需要加入工作队列,因为当时就处理完了。使用内存数据库记录已经处理过的目录页和详情页:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_2.jpg?sign=1739192699-INQ9vCi1FDaPsc17eoez3APWDoQscVzg-0-23fcd83c3c43585769fbc3533bd364b6)
全局变量workQueue记录已经发现待处理的列表页:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_3.jpg?sign=1739192699-5W17Qk1Pm6z6VxRgFbVc1XAlFdvQchmP-0-50cddd5dce22771cd761131504f8cb4e)
爬虫运行时,先把列表首页放入工作队列,然后使用一个循环处理列表页的工作队列:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_4.jpg?sign=1739192699-xJdmusdoEFzeomSURnKQPxCIx0v5aywR-0-efcd507a0e0aabd93af9af3711132289)
详情页和列表页往往包含一些有效信息。详情页处理器接口的定义如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_5.jpg?sign=1739192699-U8lOM7z5JfPpHfsugN9c5IminIgKFk25-0-87b5a08b8a857b62ba8ce9fc6d03f366)
用NewsDetailHandler类实现DetailHandler:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_67_6.jpg?sign=1739192699-BXQ7BfFvjVr93MuhHxBJ7InD8DS0bfhk-0-30c5a7d94e5f63383499dc8fcc37b0a7)
2.1.8 暗网抓取
暗网是指只有提交检索词才能得到相关的结果的索引列表,然后根据这个索引列表获取详情页。
URL中提供的查询词需要编码,可以调用URLEncoder.encode()方法实现编码:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_1.jpg?sign=1739192699-StTBOktwj3cFWeQUQQewRUNJ4vGe9H7w-0-32dec7cbd611df39d409706f00bf27b5)
通过列表页的方式遍历临床试验信息:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_2.jpg?sign=1739192699-Up6VZ3lGrcQ0QDUBi2Zs3yeQMrNYDco9-0-eecc6617cbb1ec9c5d5c4192bc51f940)
这里通过down_fmt参数指定返回XML格式的文件。
如果要在不将任何HTML DOM规则应用于文件的情况下解析XML,请使用XmlTreeBuilder,用法示例如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_68_3.jpg?sign=1739192699-22VFv39hfmWExr1resS0D5JwAuezNBwX-0-93e778d293c7d4a763259de394a80881)
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=1739192699-NQooh8nGyqJ6313BVY7UkVpKBdf5Xem6-0-5a48cb464aa2c4280b7ebd85b8391a29)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_1.jpg?sign=1739192699-uJDDaCCrXkkrjy9Jlo9UiqTdTnkXN6yC-0-86f74553158da76ef20e177fc240f909)
从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=1739192699-Goj2lM2iMfrLNii5QHjAEW2KyItIiED0-0-1fc188e91e94d6bcef395d4d1cf0d4b6)
使用WebDriver访问网址:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_3.jpg?sign=1739192699-4N3q0oThwEDH53LJuDqin0YoFAsdQOx7-0-5c6406aa66968e141fd0a566c60a6a0c)
下载网页时,后台会启动一个浏览器进程。调用 WebDriver.quit()方法可以结束这个进程,但调用WebDriver.close()方法并不会结束这个进程。
如果只需要得到网页源代码,则可以不加载图像:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_4.jpg?sign=1739192699-yuxtratLga0CU3bCvl7fCiaFdZ8FzvrW-0-624836b8c22acbde5cb86861441ee02e)
需要等待网页加载完毕,然后获取网页源代码。一个显式等待是已经定义的一段代码,用于等待某个条件发生,然后继续执行后续代码。最简单的方法是调用Thread.sleep(),具体示例如下:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_5.jpg?sign=1739192699-DgtIVGGIuD65D93OyAbmyeZSVoMy7TeL-0-8cef8920826a8129dfe84e6a897dbf82)
WebDriverWait结合ExpectedConditions可以实现只等待需要的时间长度:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_6.jpg?sign=1739192699-hXImbdtWL5q9S3lJylMT8UCuYifxgke4-0-cd0e165fc67d16b68d7f5d99f611fa5c)
通过类型选取元素:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_69_7.jpg?sign=1739192699-xzGkDGe3NFqr69tBHaXkobR7ub1qbdVO-0-11f1e365483b6772d4afcefbbe286d27)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_1.jpg?sign=1739192699-6fWMbK4GnjTuWJiVlwEmgjwInwAd9nXj-0-b7ee5d65ae032698895088ae815d8d7e)
通过id查找元素并单击它:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_2.jpg?sign=1739192699-gZgkO9BphiaFyBFIU7Fe0H9JQ2T8BLjX-0-f6c55847d55c654d8177684154e37529)
可以通过JavascriptExecutor对象来执行JavaScript代码。例如,得到垂直滚动条的高度:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_3.jpg?sign=1739192699-jWj56SnQ3O8ARyhsJFQQp7b9gdoJI1Ki-0-95ab814f1b4b60439e89abfaaa1e9bd7)
逐渐向下滚动:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_4.jpg?sign=1739192699-PUzZFkBt0PZJONSLAV4UOWUT5OIDb4MK-0-34afe63dea9e4b3f1b93e194521f1c2c)
如果window.scrollBy(x,y)中的y值为负数,则表示向上滚动:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_5.jpg?sign=1739192699-PrTZ547QRh8EKMnuI6fWgj1o1GEhaQk8-0-247e948e9da73e8a20c04919121885a6)
2.1.10 图片抓取
为了能够节约网络流量,抓取过来的图片经常需要缩小到一定的尺寸,如 100px×100px或80px×80px:
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_70_6.jpg?sign=1739192699-1uwI0yesnxp3M6gogGma36fYxBVDpcoU-0-17c1e333f497b758001007b1a6992d65)
![](https://epubservercos.yuewen.com/50C77E/18685354608165406/epubprivate/OEBPS/Images/39803_71_1.jpg?sign=1739192699-O2NpMy8gkQpa6HI3BA8BjS7Nw1lTN23r-0-16c5a383efebebf6ebb0098a6f99c7cd)