第一步启动heritrix的web应用
第二步结合eclipse的应用
Lucene很强大,这点在前面的章节中,已经作了详细介绍。但是,无论多么强大的搜索引擎工具,在其后台,都需要一样东西来支援它,那就是网络爬虫Spider。
网络爬虫,又被称为蜘蛛Spider,或是网络机器人、BOT等,这些都无关紧要,最重要的是要认识到,由于爬虫的存在,才使得搜索引擎有了丰富的资源。
Heritrix是一个纯由Java开发的、开源的Web网络爬虫,用户可以使用它从网络上抓取想要的资源。它来自于www.archive.org。Heritrix最出色之处在于它的可扩展性,开发者可以扩展它的各个组件,来实现自己的抓取逻辑。本章就来详细介绍一下Heritrix和它的各个组件。
10.1 Heritrix的使用入门
要想学会使用Heritrix,当然首先得能把它运行起来。然而,运行Heritrix并非一件容易的事,需要进行很多配置。在Heritrix的文档中对它的运行有详细的介绍,不过尽管如此,笔者仍然花了大量时间,才将其配置好并运行成功。
10.1.1 下载和运行Heritrix
Heritrix的下载页面为:http://crawler.archive.org/downloads.html。从上面可以链接到SourceForge的下载页面。当前Heritrix的最新版本为1.10。
(1)在下载完Heritrix的完整开发包后,解压到本地的一个目录下,如图10-1所示。

图10-1 Heritrix的目录结构
其中,Heritrix所用到的工具类库都存于lib下,heritrix-1.10.1.jar是Heritrix的Jar包。另外,在Heritrix目录下有一个conf目录,其中包含了一个很重要的文件:heritrix.properties。
(2)在heritrix.properties中配置了大量与Heritrix运行息息相关的参数,这些参数主要是配置了Heritrix运行时的一些默认工具类、WebUI的启动参数,以及Heritrix的日志格式等。当第一次运行Heritrix时,只需要修改该文件,为其加入WebUI的登录名和密码,如图10-2所示。

图10-2 修改Heritrix的WebUI的登录名和密码
其中,用户名和密码是以一个冒号进行分隔,使用者可以指定任何的字符串做为用户名密码,图中所示只不过延续了Heritrix以前版本中默认的用户名和密码而已。
(3)在设置完登录名和密码后,就可以开始运行Heritrix了。Heritrix有多种方式启动,例如,可以使用CrawlController,以后台方式加载一个抓取任务,即为编程式启动。不过最常见的还是以WebUI的方式启动它。
(4)Heritrix的主类为org.archive.crawler.Heritrix,运行它,就可以启动Heritrix。当然,在运行它的时候,需要为其加上lib目录下的所有jar包。以下是笔者在命令行中启动Heritrix时所使用的批处理文件,此处列出,仅供读者参考(笔者的Heritrix目录是位于E盘的根目下,即E:\heritrix)。
代码10.1
java -Xmx512m -Dheritrix.home=e:\\heritrix -cp "E:\\heritrix\\lib\\commons-codec-1.3.jar;E:\\heritrix\\lib\\commons-collections-3.1.jar;E:\\heritrix\\lib\\dnsjava-1.6.2.jar;E:\\heritrix\\lib\\poi-scratchpad-2.0-RC1-20031102.jar;E:\\heritrix\\lib\\commons-logging-1.0.4.jar;E:\\heritrix\\lib\\commons-httpclient-3.0.1.jar;E:\\heritrix\\lib\\commons-cli-1.0.jar;E:\\heritrix\\lib\\mg4j-1.0.1.jar;E:\\heritrix\\lib\\javaswf-CVS-SNAPSHOT-1.jar;E:\\heritrix\\lib\\bsh-2.0b4.jar;E:\\heritrix\\lib\\servlet-tomcat-4.1.30.jar;E:\\heritrix\\lib\\junit-3.8.1.jar;E:\\heritrix\\lib\\jasper-compiler-tomcat-4.1.30.jar;E:\\heritrix\\lib\\commons-lang-2.1.jar;E:\\heritrix\\lib\\itext-1.2.0.jar;E:\\heritrix\\lib\\poi-2.0-RC1-20031102.jar;E:\\heritrix\\lib\\jetty-4.2.23.jar;E:\\heritrix\\lib\\commons-net-1.4.1.jar;E:\\heritrix\\lib\\libidn-0.5.9.jar;E:\\heritrix\\lib\\ant-1.6.2.jar;E:\\heritrix\\lib\\fastutil-5.0.3-heritrix-subset-1.0.jar;E:\\heritrix\\lib\\je-3.0.12.jar;E:\\heritrix\\lib\\commons-pool-1.3.jar;E:\\heritrix\\lib\\jasper-runtime-tomcat-4.1.30.jar;E:\\heritrix\\heritrix-1.10.1.jar" org.archive.crawler.Heritrix
(5)在上面的批处理文件中,将Heritrix所用到的所有的第三方Jar包都写进了classpath中,同时执行了org.archive.crawler.Heritrix这个主类。图10-3为Heritrix启动时的画面。

图10-3 Heritrix的启动画面
(6)在这时,Heritrix的后台已经对服务器的8080端口进行了监听,只需要通过浏览器访问http://localhost:8080,就可以打开Heritrix的WebUI了。如图10-4所示。

图10-4 Heritrix的WebUI的登录界面
(7)在这个登录界面,输入刚才在Heritrix.properties中预设的WebUI的用户名和密码,就可以进入如图10-5所示的Heritrix的WebUI的主界面。

图10-5 登录后的界面
(8)当看到这个页面的时候,就说明Heritrix已经成功的启动了。在页面的中央有一道状态栏,用于标识当前正在运行的抓取任务。如图10-6所示:

图10-6 抓取任务的状态栏
在这个WebUI的帮助下,用户就可以开始使用Heritrix来抓取网页了。
10.1.2 在Eclipse里配置Heritrix的开发环境
讲完了通过命令行方式启动的Heritrix,当然要讲一下如何在Eclipse中配置Heritrix的开发环境,因为可能需要对代码进行调试,甚至修改一些它的源代码,来达到所需要的效果。下面来研究一下Heritrix的下载包。
(1)webapps文件夹是用来提供Servlet引擎的,也就是提供Heritrix的WebUI的部分,因此,在构建开发环境时必不可少。conf文件夹是用来提供配置文件的,因此也需要配置进入工程。Lib目录下主要是Heritrix在运行时需要用到的第三方的软件,因此,需要将其设定到Eclipse的Build Path下。最后就是Heritrix的jar包了,将其解压,可以看到其内部的结构如图10-7所示。

图10-7 Heritrix的Jar包的结构
(2)根据图10-7所示,应该从Heritrix的源代码包中把这些内容取出,然后放置到工程中来。Heritrix的源代码包解压后,只有两个文件夹,如图10-8所示。

图10-8 Heritrix的源代码包的结构
(3)只需在src目录下,把图10-7中的内容配全,就可以将工程的结构完整了。如图10-9所示。

图10-9 src目录下的内容
(4)图10-10和图10-11是笔者机器上的Heritrix在Eclipse中的工程配置好后的截图,以及workspace中文件夹的预览。

图10-10 Eclipse工程视图下的包结构 图10-11 文件夹中的工程
其中,org目录内是Heritrix的源代码,另外,笔者将conf目录去掉了,直接将heritrix.properties文件放在了工程目录下。在图10-10中,读者可能没有看到Heritrix所使用到的Jar包,这是因为在工程视图中,它们被过滤器过滤掉了,实际上,所有lib目录下的jar包都已经被加进了build path中。
(5)不过,读者很有可能遇到这样的情况,那就是在将所有的jar包都导入后,工程编译完成,却发现在左边的package explorer中出现了大量的编译错误。如图10-12所示。

图10-12 出现的编辑错误
(6)随便打开一个出错的文件,如图10-13所示,会发现大量的错误都来自于“assert”关键字。这种写法似乎Eclipse不认识。

图10-13 出错的程序
(7)解决问题的关键在于,Eclipse的编译器不认识assert这个关键字。可以在“选项”菜单中将编译器的语法样式改为5.0,也就是JDK1.5兼容的语法,然后重启编译整个工程就可以了。如图10-14所示。

图10-14 改变编译器的语法等级
(8)在重新编译完整个工程后,笔者的Eclipse中仍然出现了一个编译错误,那就是在org.archive.io.ArchiveRecord类中,如图10-15所示。

图10-15 一个仍然存在的错误
从代码看来,这是因为在使用条件表达式,对strippedFileName这个String类型的对象赋值时,操作符的右则出现了一个char型的常量,因此影响了编译。暂且不论为什么在Heritrix的源代码中会出现这样的错误,解决问题的办法就是将char变成String类型,即:
buffer.append(strippedFileName != null? strippedFileName: "-");
(9)当这样修改完后,整个工程的错误就被全部解决了,也就可以开始运行Heritrix了。在Eclipse下运行org.archive.crawler.Heritrix类,如图10-16所示。

图10-16 在Eclipse中运行Heritrix
(10)当看到图10-17所示的界面时,就说明Heritrix已经成功的在Eclipse中运行,也就意味着可以使用Eclipse来对Heritrix进行断点调试和源码修改了。

图10-17 在Eclipse中成功的运行
10.1.3 创建一个新的抓取任务
在10.1.1小节中,已经看到了Heritrix成功运行后的WebUI,接下来,就要带领读者来创建一个新的抓取作务。
(1)单击WebUI菜单栏上的“Jobs”标签,就可以进入任务创建页面。如图10-18所示。

图10-18 菜单栏上的“Jobs”标签
(2)在任务创建页面中,有4种创建任务的方式,如图10-19所示,具体含义如下。
l Based on existing job:以一个已经有的抓取任务为模板,创建所有抓取属性和抓取起始URL的列表。
l Based on a recovery:在以前的某个任务中,可能设置过一些状态点,新的任务将从这个设置的状态点开始。
l Based on a profile:专门为不同的任务设置了一些模板,新建的任务将按照模板来生成。
l With defaults:这个最简单,表示按默认的配置来生成一个任务。

图10-19 “任务”菜单中
在Heritrix中,一个任务对应一个描述文件。这个描述文件的默认的名称为order.xml。每次创建一个新任务时,都相当于生成了一个order.xml的文件。文件中详细记录了Heritrix在运行时需要的所有信息。例如,它包括该用户所选择的Processor类、Frontier类、Fetcher类、抓取时线程的最大数量、连接超时的最大等待时间等信息。上面所说的4种创建抓取任务的方式,其实都是在生成一个order.xml文件。其中,第4种With defaults,则是直接拷贝默认的order.xml文件。在所创建的Eclipse工程或是命令行启动的Heritrix下载包中,该默认的order.xml文件均是放于profiles\default目录下的。
关于order.xml的细节,在此还不必深究。因为它里面所有的内容,都会在WebUI上看到。
(3)单击With defaults链接,创建一个新的抓取任务,如图10-20所示。

图10-20 新的抓取任务
(4)在新建任务的名称上,填入“Sohu_news”,表示该抓取任务将抓取搜狐的新闻信息。在Description中随意填入字符,然后再在seeds框中,填入搜狐新闻的网址。
这里需要解释一下seeds的含义。所谓seeds,其实指的是抓取任务的起始点。每次的抓取,总是需要从一个起始点开始,在得到这个起始点网页上的信息后,分析出新的地址加入抓取队列中,然后循环抓取,重复这样的过程,直到所有链接都分析完毕。
(5)在图10-20中,设置了搜狐新闻的首页为种子页面,以此做为起始点。用户在使用时,也可以同时输入多个种子,每个URL地址单独写在一行上,如图10-21所示。

图10-21 多个种子的情况
当然,凭着目前的设置,还没法开始抓取网页,还需要对这个任务进行详细的设置。
10.1.4 设置抓取时的处理链
在图10-21中,seeds文本框下有一排按钮,单击“Modules”按钮,就进入了配置抓取时的处理链的页面,如图10-22所示。

图10-22 配置处理链的页面
从上而下,可以看到,需要配置的内容共有7项,其中CrawlScope和Frontier是两个最重要的组件。
CrawlScope用于配置当前应该在什么范围内抓取网页链接。比如,如果选择BroadScope,则表示当前抓取的范围不受限制,但如果选择了HostScope,则表示抓取的范围在当前的Host内。
从笔者的经验看来,在抓取时,无论是HostScope或PathScope都不能真正的限制到抓取的内容。需要对Scope内的代码进行一定的修改才可以,因此,暂时选择BroadScope来充当示例中的范围限定,其实也就是对范围不做任何的限定。即从news.sohu.com开始,抓取任何可以抓取到的信息。如图10-23所示。

图10-23 设置Scope
Frontier则是一个URL的处理器,它将决定下一个被处理的URL是什么。同时,它还会将经由处理器链所解析出来的URL加入到等待处理的队列中去。在例子中,使用BdbFrontier类来做为处理器,全权掌管URL的分配。如图10-24所示。

图10-24 设置Frontier
除了这两个组件外,还有5个队列要配。这五个队列根据先后的顺序,就依次组成了Heritrix的整个处理器链。5个队列的含义分别如下:
(1)PreProcessor:这个队列中,所有的处理器都是用来对抓取时的一些先决条件做判断的。比如判断robot.txt的信息等,它是整个处理器链的入口。如图10-25所示。

图10-25 设置PreProcessor
(2)Fetcher:从名称上看,它用于解析网络传输协议,比如解析DNS、HTTP或FTP等。在演示中,主要使用FetchDNS和FetchHTTP两个类。如图10-26所示。

图10-26 设置Fetcher
(3)Extractor:它的名字就很好的揭示了它的作用。它主要用是于解析当前获取到的服务器返回内容,这些内容通常是以字符串形式缓存的。在这个队列中,包括了一系列的工具,如解析HTML、CSS等。在解析完毕,取出页面中的URL后,将它们放入队列中,等待下次继续抓取。在演示中,使用两种Extractor,即ExtractorHTTP和ExtractorHTML。如图10-27所示。

图10-27 设置Extractor
(4)Writer:主要是用于将所抓取到的信息写入磁盘。通常写入磁盘时有两种形式,一种是采用压缩的方式写入,在这里被称为Arc方式,另一种则采用镜象方式写入。当然处理起来,镜象方式要更为容易一些,因此,在演示中命名用镜象Mirror方式。如图10-28所示。

图10-28 设置Writer
(5)PostProcessor:在整个抓取解析过程结束后,进行一些扫尾的工作,比如将前面Extractor解析出来的URL有条件的加入到待处理队列中去。如图10-29所示。

图10-29 设置PostProcessor
值得一提的是,在处理器链的设置过程中,每一个队列中的处理器都是要分先后顺序的,信息的处理流程实际上是不可逆的,因此,在设置时,可以看见在队列的右侧总是有“Up”、“Down”和“Remove”这样的操作,以帮助能够正确的设置其顺序。
在设置完Hertrix所需的处理链后,仍然还不能够马上开始抓取任务,还需对默认的运行时参数做一些修改,以适应真正的需要。
10.1.5 设置运行时的参数
在设置完处理链后,在页面顶部或底部都可以找到如图10-30所示的菜单项,单击“Settings”链接,就进入了属性设置的页面,如图10-30所示。

图10-30 进入“Settings”
在属性设置页面上有非常多的输入域,Heritrix在抓取网页时,这些域是用来对的各个组件的值进行预设,如图10-31所示。

图10-31 属性配置页面
由于页面上的内容非常多,使用者可能无法全部了解它们的作用。所以Heritrix提供了一个辅助功能,来在最大程度上让使用者了解每个参数的含义。如图10-32所示。

图10-32 属性提示
可以看到,在每个属性的右侧都有一个小问号,当单击问号时,就会弹出一个Javascript的Alert提示框,上面介绍了当前属性的作用。例如,在上图中单击“max-bytes-download”属性,通过Alert的提示可以知道,它表示的是抓取器最大下载的字节数,当下载字节数超过这个属性上所设定的值时,抓取就会自动停止。另外,如果将这个值设为0,则表示没有限制。
事实上,当在第一次使用Heritrix时,所需要设置的参数并不多,以默认设置为主。以下就来介绍一些必须要在第一次使用时就要配置好的参数。
1.max-toe-threads
该参数的含义很容易了解,它表示Heritrix在运行该抓取任务时,为任务分配多少个线程进行同步抓取。该参数的默认值为100,而事实上根据笔者的经验,在机器配置和网络均很好的情况下,设置50个线程数就已经足够使用了。
2.HTTP-Header
在HTTP-Header这个属性域下面,包括两个属性值“user-agent”和“from”。默认情况下,这两个属性的值如图10-33所示。

图10-33 默认的情况
很明显,这样的值是无法完成真实的HTTP协议的模拟的,所以,必须要将值改掉。图10-34是笔者机器上的一种配置,读者可以借鉴。

图10-34 一种正确的配置
l “@VERSION@”字符串需要被替换成Heritrix的版本信息。
l “PROJECT_URL_HERE”可以被替换成任何一个完整的URL地址。
l “from”属性中不需要设置真实的E-mail地址,只需是格式正确的邮件地址就可以了。
当正确设置了上述的两个属性后,Heritrix就具备了运行的条件。单击“Submit”链接,提交这个抓取任务,如图10-35所示。

图10-35 提交任务“Submit job”
10.1.6 运行抓取任务
(1)当单击“Submit job”链接后,会看到图10-36所示的页面。图中最上方很清楚的显示了“Job created”,这表示刚才所设置的抓取任务已经被成功的建立。同时,在下面的“Pending Jobs”一栏,可以清楚的看到刚刚被创建的Job,它的状态目前为“Pending”。

图10-36 Job提交后的页面
(2)下面启动这个任务。回到“Console”界面上,可以看到,如图10-37所示,刚刚创建的任务已经显示了出来,等待我们开始它。

图10-37 Job提交后的Console界面
(3)在面版的右测,它显示了当前Java虚拟机的一些状态,如图10-38所示,可以看到当前的堆大小为4184KB,而已经被使用了3806KB,另外,最大的堆内容可以达到65088KB,也就是在64M左右。

图10-38 内存状态显示
(4)此时,单击面版中的“Start”链接,就会将此时处于“Pending”状态的抓取任务激活,令其开始抓取
(5)在图10-39中,刚才还处于“Start”状态的链接已经变为了Hold状态。这表明,抓取任务已经被激活。

图10-39 抓取开始
(6)此时,面版中出现了一条抓取状态栏,它清楚的显示了当前已经被抓取的链接数量,另外还有在队列中等待被抓取的链接数量,然后用一个百分比显示出来。
(7)在绿红相间的长条左侧,是几个实时的运行状态,其中包括抓取的平均速度(KB/s)和每秒钟抓取的链接数(URIs/sec),另外的统计还包括抓取任务所消耗的时间和剩余的时间,不过这种剩余时间一般都不准,因为URI的数量总是在不断变化,每当分析一个网页,就会有新的URI加入队列中。如图10-40所示。

图10-40 抓取的速度和时间
(8)在绿红相间的长条右侧,是当前的负载,它显示了当前活跃的线程数量,同时,还统计了Heritrix内部的所有队列的平均长度。如图10-41所示。

图10-41 线程和队列负载
(9)从图10-40和图10-41中看到,真正的抓取任务还没有开始,队列中的总URI数量,以及下载的速率都还基本为0。这应该还处于接收种子URL的网页信息的阶段。让我们再来看一下当Heritrix运行一段时间后,整个系统的资源消耗和进度情况。
(10)在图10-42中,清楚的看到系统的资源消耗。其中,每秒下载的速率已经达到了23KB,另外,平均每秒有19.3个URI被抓取。在负载方面,初设的50个线程均处于工作状态,最长的队列长度已经达到了415个URI,平均长度为5。从进度条上看,总共有3771个URI等待抓取,已经完成了718个URI的抓取,另外,下载的字节总数也已经达到了1390KB。再观察一下左边,仅用时32s。可见,多线程抓取的速度还是很快的。

图10-42 系统运行一段时间后的情况
(11)不过,当抓取继续进行时,观察Java虚拟机的内存使用,发现其已达饱合状态。64M的最大Heap显然不够用。如图10-43所示。

图10-43 Java虚拟机的内存使用
(12)由于这仅是一次演示,可以忽略内存的影响。但在真正的开发过程中,使用Heritrix时,至少应为其分配512M的最大HeapSize,也就是在启动它时,应该设置-Xmx512m这个属性。在使用命令行方式启动Heritrix的脚本中,笔者已经为其加入了该参数,而如果要在使用Eclipse启动Heritrix时也设置该参数,具体的设置方法如图10-44所示。

图10-44 在Eclipse中加入启动参数
(13)按图10-44所示,输入Java虚拟机的参数,就可以增大Heritrix的最大可用内存。如图10-45是使用了-Xmx512m参数后的Console界面。

图10-45 使用了512m的HeapSize
在运行的过程中,值得注意的一点是,进度条的百分比数量并不是准确的。因为这个百分比实际上是已经处理的链接数和总共分析出的链接数的比值。当页面在不断被抓取分析时,链接的数量也会不断的增加,因此,这个百分比的数字也在不断的变化。例如如图10-46所示,此时总共抓取到的链接数已经达到了12280个,处理了799个,它的百分比数量为6%,这显然比图10-42或图10-39中的要小。

图10-46 抓取了799的链接
读者可能已经发现,在Heritrix中,大量的链接被称为URI。从理论上说,URL应该是一个完整的地址,而URI应该是去除协议、主机和端口后剩余的部分。Heritrix中可能有一定程度的混淆,希望读者不要对此感到奇怪。
至此,已经把Heritrix成功的运行起来,并且抓取了一定的内容。接下来,看一下它是如何存储抓取下来的信息的。
10.1.7 Heritrix的镜象存储结构
由于在前面设置了Writer的类型为MirrorWriter。因此,磁盘上应该留有了所抓取到的网页的各种镜象。那么,究竟Heritrix是如何存储下镜象信息的呢?
打开Eclipse的workspace目录,进入heritrixProject的工程,里面有一个jobs目录。进入后,找到以刚才job的名称打头的文件夹,这里面的内容,就是Heritrix在运行时实时生成的。其中,有一个mirror目录,进入后,如图10-47所示。

图10-47 mirror目录下的内容
其实所谓镜象方式存储,就是将URL地址按“/”进行切分,进而按切分出来的层次存储,比如一个URL地址为:
http://news.sohu.com/index.html
那么它在mirror目录中的保存位置就该是new.sohu.com目录下的index.html文件。为了验证这一说法的准确性,打开new.sohu.com目录,可以看到图10-48。

图10-48 镜象示例
果然,index.html文件就在这个目录下。另外,Heritrix也同样将各种图片或脚本信息按路径进行了保存,例如,在news.sohu.com目录下有一个images目录,其中就保存了URL地址如http://news.sohu.com/images/xxx.gif这样的图片信息。如图10-49所示。

图10-49 抓取下来的图片文件
10.1.8 终止抓取或终止Heritrix的运行
当用户进行某个抓取任务时,有两种方法会让任务停止下来。
1.正常终止
第一种方法当然就是任务的自然结束,其条件为所有队列中的URI都已经被处理过了。此时,任务将自然终止。在“Jobs”面版上会看到任务已经完成,被加入到“Completed jobs”列表中。
2.强行终止
当然,任务不可能总是运行完,这可能是因为对任务的控制不够,结果抓取了太多不相关的信息,进而造成URL队列无限制膨胀,无法终止。在这种情况下,就需要强行将任务终止。在Console面版上有如图10-50所示的一排链接,最后一个“Terminate”链接,就是用来终止当前运行的任务。

图10-50 终止任务的运行
单击“Terminate”链接后,当前在运行的抓取任务就会立即终止,并同样将任务放置到“Jobs”面版上的“Completed jobs”列表中,只不过在“status”上,它会显示“Finished - Ended by operator”这样的提示。
当然,如果用户希望关闭Heritrix,并终止所有正在运行的任务,也可以单击Console面版上的“Shutdown Heritrix software”的链接,此时,Heritrix会弹出一个警告,告诉你如果关闭Heritrix,则当前一切正在运行的任务都将被终止。如图10-51所示。

图10-51 关闭前的提示
如果选择“I’m sure, shut it down”,则Heritrix的WebUI将会终止,虚拟机进程结束。


10.2 Heritrix的架构
在上一节中,详细介绍了Heritrix的使用入门。读者通过上一节的介绍,应该已经能够使用Heritrix来进行简单的网页抓取了。那么,Heritrix的内容究竟是如何工作的呢?它的设计方面有什么突出之处?
本节就将介绍Heritrix的几个主要组件,以此让读者了解其主要架构和工作方式。为后续的扩展Heritrix做一些铺垫。
10.2.1 抓取任务CrawlOrder
之所以选择从CrawlOrder这个类说起,是因为它是整个抓取工作的起点。在上一节中已经说过,一次抓取任务包括许多的属性,建立一个任务的方式有很多种,最简单的一种就是根据默认的order.xml来配置。在内存中,order使用CrawlOrder这个类来进行表示。看一下API文档中CrawlOrder的继承关系图,如图10-52所示。

图10-52 CrawlOrder类的继承关系图
从继承关系图中可以看到,CrawlOrder继承自一系列的与属性设置相关的基类。另外,它的最顶层基类是javax.management.Attribute,这是一个JMX中的类,它可以动态的反映出Java容器内某个MBean的属性变化。关于这一部分的内容不是我们所要讨论的重点,只需知道,CrawlOrder中的属性,是需要被随时读取和监测的。
那么究竟使用什么工具来读取order.xml文件中的各种属性呢。另外,一个CrawlOrder的对象又该如何构建呢?Heritrix提供了很好的工具支持对于order.xml的读取。在org.archive.crawler.settings包下有一个XMLSettingsHandler类,它可以用来帮助读取order.xml。
public XMLSettingsHandler(File orderFile) throws InvalidAttributeValueException
在XMLSettingsHandler的构造函数中,其所传入的参数orderFile正是一个经过对象封装的order.xml的File。这样,就可以直接调用其构造函数,来创建一个XMLSettingsHandler的实例,以此做为一个读取order.xml的工具。
当一个XMLSettingsHandler的实例被创建后,可以通过getOrder()方法来获取CrawlOrder的实例,这样也就可以进行下一步的工作了。
10.2.2 中央控制器CrawlController
中央控制器是一次抓取任务中的核心组件。它将决定整个抓取任务的开始和结束。CrawlController位于org.archive.crawler.framework中,在它的Field声明中,看到如下代码片段。
代码10.2
// key subcomponents which define and implement a crawl in progress
private transient CrawlOrder order;
private transient CrawlScope scope;
private transient ProcessorChainList processorChains;
private transient Frontier frontier;
private transient ToePool toePool;
private transient ServerCache serverCache;
// This gets passed into the initialize method.
private transient SettingsHandler settingsHandler;
可以看到,在CrawlController类中,定义了以下几个组件:
l CrawlOrder:这就不用说了,因为一个抓取工作必须要有一个Order对象,它保存了对该次抓取任务中,order.xml的属性配置。
l CrawlScope:在10.1.4节中已经介绍过了,这是决定当前的抓取范围的一个组件。
l ProcessorChainList:从名称上很明显就能看出,它表示了处理器链,在这个列表中的每一项都可以和10.1.4节中所介绍的处理器链对应上。
l Frontier:很明显,一次抓取任务需要设定一个Frontier,以此来不断为其每个线程提供URI。
l ToePool:这是一个线程池,它管理了所有该抓取任务所创建的子线程。
l ServerCache:这是一个缓存,它保存了所有在当前任务中,抓取过的Host名称和Server名称。
以上组件应该是一次正常的抓取过程中所必需的几项,它们各自的任务很独立,分工明确,但在后台中,它们之间却有着千丝万缕的联系,彼此互相做为构造函数或初始化的参数传入。
那么,究竟该如何获得CrawlController的实例,并且通过自主的编程来使用Heritrix提供的API进行一次抓任务呢?
事实上CrawlController有一个不带参数的构造函数,开发者可以直接通过它的构造函数来构造一个CrawlController的实例。但是值得注意的一点,在构造一个实例并进行抓取任务时,有几个步骤需要完成:
(1)首先构造一个XMLSettingsHandler对象,将order.xml内的属性信息装入。
(2)调用CrawlController的构造函数,构造一个CrawlController的实例。
(3)调用CrawlController的intialize(SettingsHandler)方法,初始化CrawlController实例。其中,传入的参数是在第一步是构造的XMLSettingsHandler实例。
(4)当上述3步完成后,CrawlController就已经具备运行的条件,可以开始运行了。此时,只需调用它的requestCrawlStart()方法,就可以启运线程池和Frontier,然后就可以开始不断的抓取网页了。
上述过程可以用图10-53所示。

图10-53 使用CrawlController启运抓取任务
在CrawlController的initialize()方法中,Heritrix主要做了以下几件事:
(1)从XMLSettingsHandler中取出Order。
(2)检查了用户设定的UserAgent等信息,看是否符合格式。
(3)设定了开始抓取后保存文件信息的目录结构。
(4)初始化了日志信息的记录工具。
(5)初始化了使用Berkley DB的一些工具。
(6)初始化了Scope、Frontier以及ProcessorChain。
(7)最后实例化了线程池。
在正常情况下,以上顺序不能够被随意变动,因为后一项功能的初始化很有可能需要前几项功能初始化的结果。例如线程池的初始化,必须要在先有了Frontier的实例的基础上来进行。读者可能对其中的Berkeley DB感到费解,在后面的小节将详细说明。
从图10-53中看到,最终启动抓取工作的是requestCrawlStart()方法。其代码如下。
代码10.3
public void requestCrawlStart() {
// 初始化处理器链
runProcessorInitialTasks();
// 设置一下抓取状态的改变,以便能够激发一些Listeners
// 来处理相应的事件
sendCrawlStateChangeEvent(STARTED, CrawlJob.STATUS_PENDING);
String jobState;
state = RUNNING;
jobState = CrawlJob.STATUS_RUNNING;
sendCrawlStateChangeEvent(this.state, jobState);
// A proper exit will change this value.
this.sExit = CrawlJob.STATUS_FINISHED_ABNORMAL;
// 开始日志线程
Thread statLogger = new Thread(statistics);
statLogger.setName("StatLogger");
statLogger.start();
// 启运Frontier,抓取工作开始
frontier.start();
}
可以看到,启动抓取工作的核心就是要启动Frontier(通过调用其start()方法),以便能够开始向线程池中的工作线程提供URI,供它们抓取。
下面的代码就是BdbFrontier的父类AbstractFrontier中的start()方法和unpause()方法:
代码10.4
public void start() {
if (((Boolean)getUncheckedAttribute(null, ATTR_PAUSE_AT_START))
.booleanValue()) {
// 若配置文件中不允许该次抓取开始
// 则停止
controller.requestCrawlPause();
} else {
// 若允许开始,则开始
unpause();
}
}
synchronized public void unpause() {
// 去除当前阻塞变量
shouldPause = false;
// 唤醒所有阻塞线程,开始抓取任务
notifyAll();
}
在start()方法中,首先判断配置中的属性是否允许当前线程开始。若不允许,则令controller停止抓取。若允许开始,则简单的调用unpause()方法。unpause()方法更为简单,它首先将阻塞线程的信号量设为false,即允许线程开始活动,然后通过notifyAll()方法,唤醒线程池中所有被阻塞的线程,开始抓取。
10.2.3 Frontier链接制造工厂
Frontier在英文中的意思是“前线,领域”,在Heritrix中,它表示一种为线程提供链接的工具。它通过一些特定的算法来决定哪个链接将接下来被送入处理器链中,同时,它本身也负责一定的日志和状态报告功能。
事实上,要写出一个合格并且真正能够使用的Frontier绝非一件简单的事情,尽管有了Frontier接口,其中的方法约束了Frontier的行为,也给编码带来了一定的指示。但是其中还存在着很多问题,需要很好的设计和处理才可以解决。
在Heritrix的官方文档上,有一个Frontier的例子,在此拿出来进行一下讲解,以此来向读者说明一个最简单的Frontier都能够做什么事。以下就是这个Frontier的代码。
代码10.5
public class MyFrontier extends ModuleType implements Frontier,
FetchStatusCodes {
// 列表中保存了还未被抓取的链接
List pendingURIs = new ArrayList();

// 这个列表中保存了一系列的链接,它们的优先级
// 要高于pendingURIs那个List中的任何一个链接
// 表中的链接表示一些需要被满足的先决条件
List prerequisites = new ArrayList();

// 一个HashMap,用于存储那些已经抓取过的链接
Map alreadyIncluded = new HashMap();

// CrawlController对象
CrawlController controller;
// 用于标识是否一个链接正在被处理
boolean uriInProcess = false;

// 成功下载的数量
long successCount = 0;
// 失败的数量
long failedCount = 0;
// 抛弃掉链接的数量
long disregardedCount = 0;
// 总共下载的字节数
long totalProcessedBytes = 0;
// 构造函数
public MyFrontier(String name) {
super(Frontier.ATTR_NAME, "A simple frontier.");
}
// 初始化,参数为一个CrawlController
public void initialize(CrawlController controller)
throws FatalConfigurationException, IOException {

// 注入
this.controller = controller;

// 把种子文件中的链接加入到pendingURIs中去
this.controller.getScope().refreshSeeds();
List seeds = this.controller.getScope().getSeedlist();
synchronized(seeds) {
for (Iterator i = seeds.iterator(); i.hasNext();) {
UURI u = (UURI) i.next();
CandidateURI caUri = new CandidateURI(u);
caUri.setSeed();
schedule(caUri);
}
}
}
// 该方法是给线程池中的线程调用的,用以取出下一个备处理的链接
public synchronized CrawlURI next(int timeout) throws InterruptedException {
if (!uriInProcess && !isEmpty()) {
uriInProcess = true;
CrawlURI curi;
/*
* 算法很简单,总是先看prerequistes队列中是否有
* 要处理的链接,如果有,就先处理,如果没有
* 再看pendingURIs队列中是否有链接
* 每次在处理的时候,总是取出队列中的第一个链接
*/
if (!prerequisites.isEmpty()) {
curi = CrawlURI.from((CandidateURI) prerequisites.remove(0));
} else {
curi = CrawlURI.from((CandidateURI) pendingURIs.remove(0));
}
curi.setServer(controller.getServerCache().getServerFor(curi));
return curi;
} else {
wait(timeout);
return null;
}
}
public boolean isEmpty() {
return pendingURIs.isEmpty() && prerequisites.isEmpty();
}
// 该方法用于将新链接加入到pendingURIs队列中,等待处理
public synchronized void schedule(CandidateURI caURI) {
/*
* 首先判断要加入的链接是否已经被抓取过
* 如果已经包含在alreadyIncluded这个HashMap中
* 则说明处理过了,即可以放弃处理
*/
if (!alreadyIncluded.containsKey(caURI.getURIString())) {
if(caURI.needsImmediateScheduling()) {
prerequisites.add(caURI);
} else {
pendingURIs.add(caURI);
}
// HashMap中使用url的字符串来做为key
// 而将实际的CadidateURI对象做为value
alreadyIncluded.put(caURI.getURIString(), caURI);
}
}
public void batchSchedule(CandidateURI caURI) {
schedule(caURI);
}
public void batchFlush() {
}
// 一次抓取结束后所执行的操作,该操作由线程池
// 中的线程来进行调用
public synchronized void finished(CrawlURI cURI) {
uriInProcess = false;

// 成功下载
if (cURI.isSuccess()) {
successCount++;
// 统计下载总数
totalProcessedBytes += cURI.getContentSize();
// 如果成功,则触发一个成功事件
// 比如将Extractor解析出来的新URL加入队列中
controller.fireCrawledURISuccessfulEvent(cURI);
cURI.stripToMinimal();
}
// 需要推迟下载
else if (cURI.getFetchStatus() == S_DEFERRED) {
cURI.processingCleanup();
alreadyIncluded.remove(cURI.getURIString());
schedule(cURI);
}
// 其他状态
else if (cURI.getFetchStatus() == S_ROBOTS_PRECLUDED
|| cURI.getFetchStatus() == S_OUT_OF_SCOPE
|| cURI.getFetchStatus() == S_BLOCKED_BY_USER
|| cURI.getFetchStatus() == S_TOO_MANY_EMBED_HOPS
|| cURI.getFetchStatus() == S_TOO_MANY_LINK_HOPS
|| cURI.getFetchStatus() == S_DELETED_BY_USER) {
// 抛弃当前URI
controller.fireCrawledURIDisregardEvent(cURI);
disregardedCount++;
cURI.stripToMinimal();
} else {
controller.fireCrawledURIFailureEvent(cURI);
failedCount++;
cURI.stripToMinimal();
}
cURI.processingCleanup();
}

// 返回所有已经处理过的链接数量
public long discoveredUriCount() {
return alreadyIncluded.size();
}
// 返回所有等待处理的链接的数量
public long queuedUriCount() {
return pendingURIs.size() + prerequisites.size();
}
// 返回所有已经完成的链接数量
public long finishedUriCount() {
return successCount + failedCount + disregardedCount;
}
// 返回所有成功处理的链接数量
public long successfullyFetchedCount() {
return successCount;
}
// 返回所有失败的链接数量
public long failedFetchCount() {
return failedCount;
}
// 返回所有抛弃的链接数量
public long disregardedFetchCount() {
return disregardedCount;
}
// 返回总共下载的字节数
public long totalBytesWritten() {
return totalProcessedBytes;
}
public String report() {
return "This frontier does not return a report.";
}
public void importRecoverLog(String pathToLog) throws IOException {
throw new UnsupportedOperationException();
}
public FrontierMarker getInitialMarker(String regexpr,
boolean inCacheOnly) {
return null;
}
public ArrayList getURIsList(FrontierMarker marker, int numberOfMatches,
boolean verbose) throws InvalidFrontierMarkerException {
return null;
}
public long deleteURIs(String match) {
return 0;
}
}
在Frontier中,根据笔者给出的中文注释,相信读者已经能够了解这个Frontier中的大部分玄机。以下给出详细的解释。
首先,Frontier是用来向线程提供链接的,因此,在上面的代码中,使用了两个ArrayList来保存链接。其中,第一个pendingURIs保存的是等待处理的链接,第二个prerequisites中保存的也是链接,只不过它里面的每个链接的优先级都要高于pendingURIs里的链接。通常,在prerequisites中保存的都是如DNS之类的链接,只有当这些链接被首先解析后,其后续的链接才能够被解析。
除了这两个ArrayList外,在上面的Frontier还有一个名称为alreadyIncluded的HashMap。它用于记录那些已经被处理过的链接。每当调用Frontier的schedule()方法来加入一个新的链接时,Frontier总要先检查这个正要加入到队列中的链接是不是已经被处理过了。
很显然,在分析网页的时候,会出现大量相同的链接,如果没有这种检查,很有可能造成抓取任务永远无法完成的情况。同时,在schedule()方法中还加入了一些逻辑,用于判断当前要进入队列的链接是否属于需要优先处理的,如果是,则置入prerequisites队列中,否则,就简单的加入pendingURIs中即可。
注意:Frontier中还有两个关键的方法,next()和finished(),这两个方法都是要交由抓取的线程来完成的。Next()方法的主要功能是:从等待队列中取出一个链接并返回,然后抓取线程会在它自己的run()方法中完成对这个链接的处理。而finished()方法则是在线程完成对链接的抓取和后续的一切动作后(如将链接传递经过处理器链)要执行的。它把整个处理过程中解析出的新的链接加入队列中,并且在处理完当前链接后,将之加入alreadyIncluded这个HashMap中去。
需要读者记住的是,这仅仅是一个最基础的代码,它有很多的功能缺失和性能问题,甚至可能出现重大的同步问题。不过尽管如此,它应当也起到了抛砖引玉的作用,能够从结构上揭示了一个Frontier的作用。
10.2.4 用Berkeley DB实现的BdbFrontier
简单的说,Berkeley DB就是一个HashTable,它能够按“key/value”方式来保存数据。它是由美国Sleepycat公司开发的一套开放源代码的嵌入式数据库,它为应用程序提供可伸缩的、高性能的、有事务保护功能的数据管理服务。
那么,为什么不使用一个传统的关系型数据库呢?这是因为当使用BerkeleyDB时,数据库和应用程序在相同的地址空间中运行,所以数据库操作不需要进程间的通讯。然而,当使用传统关系型数据库时,就需要在一台机器的不同进程间或在网络中不同机器间进行进程通讯,这样所花费的开销,要远远大于函数调用的开销。
另外,Berkeley DB中的所有操作都使用一组API接口。因此,不需要对某种查询语言(比如SQL)进行解析,也不用生成执行计划,这就大大提高了运行效率。
当然,做为一个数据库,最重要的功能就是事务的支持,Berkeley DB中的事务子系统就是用来为其提供事务支持的。它允许把一组对数据库的修改看作一个原子单位,这组操作要么全做,要么全不做。在默认的情况下,系统将提供严格的ACID事务属性,但是应用程序可以选择不使用系统所作的隔离保证。该子系统使用两段锁技术和先写日志策略来保证数据的正确性和一致性。这种事务的支持就要比简单的HashTable中的Synchronize要更加强大。
注意:在Heritrix中,使用的是Berkeley DB的Java版本,这种版本专门为Java语言做了优化,提供了Java的API接口以供开发者使用。
为什么Heritrix中要用到Berkeley DB呢?这就需要再回过头来看一下Frontier了。
在上一小节中,当一个链接被处理后,也即经过处理器链后,会生成很多新的链接,这些新的链接需要被Frontier的一个schedule方法加入到队列中继续处理。但是,在将这些新链接加入到队列之前,要首先做一个检查,即在alreadyIncluded这个HashMap中,查看当前要加入到队列中的链接是否在先前已经被处理过了。
当使用HashMap来存储那些已经被处理过的链接时,HashMap中的key为url,而value则为一个对url封装后的对象。很显然的,这里有几个问题。
l 对这个HashMap的读取是多线程的,因为每个线程都需要访问这个HashMap,以决定当前要加入链接是否已经存在过了。
l 对这个HashMap的写入是多线程的,每个线程在处理完毕后,都会访问这个HashMap,以写入最新处理的链接。
l 这个HashMap的容量可能很大,可以试想,一次在广域网范围上的网页抓取,可能会涉及到上十亿个URL地址,这种地址包括网页、图片、文件、多媒体对象等,所以,不可能将这么大一张表完全的置放于内存中。
综合考虑以上3点,仅用一个HashMap来保存所有的链接,显然已经不能满足“大数据量,多并发”这样的要求。因此,需要寻找一个替代的工具来解决问题。Heritrix中的BdbFrontier就采用了Berkeley DB,来解决这种URL存放的问题。事实上,BdbFrontier就是Berkeley DB Frontier的简称。
为了在BdbFrontier中使用Berkeley DB,Heritrix本身构造了一系列的类来帮助实现这个功能。这些类如下:
l BdbFrontier
l BdbMultipleWorkQueues
l BdbWorkQueue
l BdbUriUniqFilter
上述的4个类,都以Bdb3个字母开头,这表明它们都是使用到了Berkeley DB的功能。其中:
(1)BdbMultipleWorkQueues代表了一组链接队列,这些队列有各自不同的key。这样,由Key和链接队列可以形成一个“Key/Value”对,也就成为了Berkeley DB里的一条记录(DatabaseEntry)如图10-54所示。

图10-54 BdbMultipleWorkQueues示意
图10-54清楚的显示了Berkeley DB中的“key/value”形式。可以说,这就是一张Berkeley DB的数据库表。其中,数据库的一条记录包含两个部分,左边是一个由右边的所有URL链接计算出来的公共键值,右边则是一个URL的队列。
(2)BdbWorkQueue代表了一个基于Berkeley DB的队列,与BdbMutipleWorkQueues所不同的是,该队列中的所有的链接都具有相同的键值。事实上,BdbWorkQueue只是对BdbMultipleWorkQueues的封装,在构造一个BdbWorkQueue时,需传入一个健值,以此做为该Queue在数据库中的标识。事实上,在工作线程从Frontier中取出链接时,Heritrix总是先取出整个BdbWorkQueue,再从中取出第一个链接,然后将当前这个BdbWorkQueue置入一个线程安全的同步容器内,等待线程处理完毕后才将该Queue释放,以便该Queue内的其他URI可以继续被处理。
(3)BdbUriUniqFilter是一个过滤器,从名称上就能知道,它是专门用来过滤当前要进入等待队列的链接对象是否已经被抓取过。很显然,在BdbUriUniqFilter内部嵌入了一个Berkeley DB数据库用于存储所有的被抓取过的链接。它对外提供了
public void add(String key, CandidateURI value)
这样的接口,以供Frontier调用。当然,若是参数的CandidateURI已经存在于数据库中了,则该方法会禁止它加入到等待队列中去。
(4)BdbFrontier就是Heritrix中使用了Berkeley DB的链接制造工厂。它主要使用BdbUriUniqFilter,做为其判断当前要进入等待队列的链接对象是否已经被抓取过。同时,它还使用了BdbMultipleWorkQueues来做为所有等待处理的URI的容器。这些URI根据各自的内容会生成一个Hash值成为它们所在队列的键值。
在Heritrix1.10的版本中,可以说BdbFrontier是惟一一个具有实用意义的链接制造工厂了。虽然Heritrix还提供了另外两个Frontier:
org.archive.crawler.frontier.DomainSensitiveFrontier
org.archive.crawler.frontier.AdaptiveRevisitFrontier
但是,DomainSensitiveFrontier已经被废弃不再推荐使用了。而AdaptiveRevisitFrontier的算法是不管遇到什么新链接,都义无反顾的再次抓取,这显然是一种很落后的算法。因此,了解BdbFrontier的实现原理,对于更好的了解Heritrix对链接的处理有实际意义。
BdbFrontier的代码相对比较复杂,笔者在这里也只能简单将其轮廓进行介绍,读者仍须将代码仔细研读,方能把文中的点点知识串联起来,进而更好的理解Heritrix作者们的巧妙匠心。
10.2.5 Heritrix的多线程ToeThread和ToePool
想要更有效更快速的抓取网页内容,则必须采用多线程。Heritrix中提供了一个标准的线程池ToePool,它用于管理所有的抓取线程。
ToePool和ToeThread都位于org.archive.crawler.framework包中。前面已经说过,ToePool的初始化,是在CrawlController的initialize()方法中完成的。来看一下ToePool以及ToeThread是如何被初始化的。以下代码是在CrawlController中用于对ToePool进行初始化的。
// 构造函数
toePool = new ToePool(this);
// 按order.xml中的配置,实例化并启动线程
toePool.setSize(order.getMaxToes());
ToePool的构造函数很简单,如下所示:
public ToePool(CrawlController c) {
super("ToeThreads");
this.controller = c;
}
它仅仅是调用了父类java.lang.ThreadGroup的构造函数,同时,将注入的CrawlController赋给类变量。这样,便建立起了一个线程池的实例了。但是,那些真正的工作线程又是如何建立的呢?
下面来看一下线程池中的setSize(int)方法。从名称上看,这个方法很像是一个普通的赋值方法,但实际上,它并不是那么简单。
代码10.6
public void setSize(int newsize)
{
targetSize = newsize;
int difference = newsize - getToeCount();

// 如果发现线程池中的实际线程数量小于应有的数量
// 则启动新的线程
if (difference > 0) {
for(int i = 1; i <= difference; i++) {
// 启动新线程
startNewThread();
}
}
// 如果线程池中的线程数量已经达到需要
else
{

int retainedToes = targetSize;
// 将线程池中的线程管理起来放入数组中
Thread[] toes = this.getToes();

// 循环去除多余的线程
for (int i = 0; i < toes.length ; i++) {
if(!(toes[i] instanceof ToeThread)) {
continue;
}
retainedToes--;
if (retainedToes>=0) {
continue;
}
ToeThread tt = (ToeThread)toes[i];
tt.retire();
}
}
}
// 用于取得所有属于当前线程池的线程
private Thread[] getToes()
{
Thread[] toes = new Thread[activeCount()+10];
// 由于ToePool继承自java.lang.ThreadGroup类
// 因此当调用enumerate(Thread[] toes)方法时,
// 实际上是将所有该ThreadGroup中开辟的线程放入
// toes这个数组中,以备后面的管理
this.enumerate(toes);
return toes;
}
// 开启一个新线程
private synchronized void startNewThread()
{
ToeThread newThread = new ToeThread(this, nextSerialNumber++);
newThread.setPriority(DEFAULT_TOE_PRIORITY);
newThread.start();
}
通过上面的代码可以得出这样的结论:线程池本身在创建的时候,并没有任何活动的线程实例,只有当它的setSize方法被调用时,才有可能创建新线程;如果当setSize方法被调用多次而传入不同的参数时,线程池会根据参数里所设定的值的大小,来决定池中所管理线程数量的增减。
当线程被启动后,所执行的是其run()方法中的片段。接下来,看一个ToeThread到底是如何处理从Frontier中获得的链接的。
代码10.7
public void run()
{
String name = controller.getOrder().getCrawlOrderName();
logger.fine(getName()+" started for order '"+name+"'");
try {
while ( true )
{
// 检查是否应该继续处理
continueCheck();

setStep(STEP_ABOUT_TO_GET_URI);

// 使用Frontier的next方法从Frontier中
// 取出下一个要处理的链接
CrawlURI curi = controller.getFrontier().next();

// 同步当前线程
synchronized(this) {
continueCheck();
setCurrentCuri(curi);
}

/*
* 处理取出的链接
*/
processCrawlUri();

setStep(STEP_ABOUT_TO_RETURN_URI);

// 检查是否应该继续处理
continueCheck();

// 使用Frontier的finished()方法
// 来对刚才处理的链接做收尾工作
// 比如将分析得到的新的链接加入
// 到等待队列中去
synchronized(this) {
controller.getFrontier().finished(currentCuri);
setCurrentCuri(null);
}

// 后续的处理
setStep(STEP_FINISHING_PROCESS);
lastFinishTime = System.currentTimeMillis();
// 释放链接
controller.releaseContinuePermission();
if(shouldRetire) {
break; // from while(true)
}
}
} catch (EndedException e) {
} catch (Exception e) {
logger.log(Level.SEVERE,"Fatal exception in "+getName(),e);
} catch (OutOfMemoryError err) {
seriousError(err);
} finally {
controller.releaseContinuePermission();
}
setCurrentCuri(null);

// 清理缓存数据
this.httpRecorder.closeRecorders();
this.httpRecorder = null;
localProcessors = null;

logger.fine(getName()+" finished for order '"+name+"'");
setStep(STEP_FINISHED);
controller.toeEnded();
controller = null;
}
在上面的方法中,很清楚的显示了工作线程是如何从Frontier中取得下一个待处理的链接,然后对链接进行处理,并调用Frontier的finished方法来收尾、释放链接,最后清理缓存、终止单步工作等。另外,其中还有一些日志操作,主要是为了记录每次抓取的各种状态。
很显然,以上代码中,最重要的一行语句是processCrawlUri(),它是真正调用处理链来对链接进行处理的代码。其中的内容,放在下一个小节中介绍。
10.2.6 处理链和Processor
在本章第一节中介绍了设置处理器链相关的内容。从中知道,处理器链包括以下几种:
l PreProcessor
l Fetcher
l Extractor
l Writer
l PostProcessor
为了很好的表示整个处理器链的逻辑结构,以及它们之间的链式调用关系,Heritrix设计了几个API来表示这种逻辑结构。
org.archive.crawler.framework.Processor
org.archive.crawler.framework.ProcessorChain
org.archive.crawler.framework.ProcessorChainList
下面进行详细讲解。
1.Processor类
该类代表着单个的处理器,所有的处理器都是它的子类。在Processor类中有一个process()方法,它被标识为final类型的,也就是说,它不可以被它的子类所覆盖。代码如下。
代码10.8
public final void process(CrawlURI curi) throws InterruptedException
{
// 设置下一个处理器
curi.setNextProcessor(getDefaultNextProcessor(curi));
try
{
// 判断当前这个处理器是否为enabled
if (!((Boolean) getAttribute(ATTR_ENABLED, curi)).booleanValue()) {
return;
}
} catch (AttributeNotFoundException e) {
logger.severe(e.getMessage());
}
// 如果当前的链接能够通过过滤器
// 则调用innerProcess(curi)方法
// 来进行处理
if(filtersAccept(curi)) {
innerProcess(curi);
}
// 如果不能通过过滤器检查,则调
// 用innerRejectProcess(curi)来处理
else
{
innerRejectProcess(curi);
}
}
方法的含义很简单。即首先检查是否允许这个处理器处理该链接,如果允许,则检查当前处理器所自带的过滤器是否能够接受这个链接。当过滤器的检查也通过后,则调用innerProcess(curi)方法来处理,如果过滤器的检查没有通过,就使用innerRejectProcess(curi)方法处理。
其中innerProcess(curi)和innerRejectProcess(curi)方法都是protected类型的,且本身没有实现任何内容。很明显它们是留在子类中,实现具体的处理逻辑。不过大部分的子类都不会重写innerRejectProcess(curi)方法了,这是因为反正一个链接已经被当前处理器拒绝处理了,就不用再有什么逻辑了,直接跳到下一个处理器继续处理就行了。
2.ProcessorChain类
该类表示一个队列,里面包括了同种类型的几个Processor。例如,可以将一组的Extractor加入到同一个ProcessorChain中去。
在一个ProcessorChain中,有3个private类型的类变量:
private final MapType processorMap;
private ProcessorChain nextChain;
private Processor firstProcessor;
其中,processorMap中存放的是当前这个ProcessorChain中所有的Processor。nextChain的类型是ProcessorChain,它表示指向下一个处理器链的指针。而firstProcessor则是指向当前队列中的第一个处理器的指针。
3.ProcessorChainList
从名称上看,它保存了Heritrix一次抓取任务中所设定的所有处理器链,将之做为一个列表。正常情况下,一个ProcessorChainList中,应该包括有5个ProcessorChain,分别为PreProcessor链、Fetcher链、Extractor链、Writer链和PostProcessor链,而每个链中又包含有多个的Processor。这样,就将整个处理器结构合理的表示了出来。
那么,在ToeThread的processCrawlUri()方法中,又是如何来将一个链接循环经过这样一组结构的呢?请看下面的代码:
代码10.9
private void processCrawlUri() throws InterruptedException {
// 设定当前线程的编号
currentCuri.setThreadNumber(this.serialNumber);
// 为当前处理的URI设定下一个ProcessorChain
currentCuri.setNextProcessorChain(controller.getFirstProcessorChain());

// 设定开始时间
lastStartTime = System.currentTimeMillis();
try {

// 如果还有一个处理链没处理完
while (currentCuri.nextProcessorChain() != null)
{
setStep(STEP_ABOUT_TO_BEGIN_CHAIN);

// 将下个处理链中的第一个处理器设定为
// 下一个处理当前链接的处理器
currentCuri.setNextProcessor(currentCuri
.nextProcessorChain().getFirstProcessor());
// 将再下一个处理器链设定为当前链接的
// 下一个处理器链,因为此时已经相当于
// 把下一个处理器链置为当前处理器链了
currentCuri.setNextProcessorChain(currentCuri
.nextProcessorChain().getNextProcessorChain());

// 开始循环处理当前处理器链中的每一个Processor
while (currentCuri.nextProcessor() != null)
{
setStep(STEP_ABOUT_TO_BEGIN_PROCESSOR);
Processor currentProcessor = getProcessor(currentCuri.nextProcessor());
currentProcessorName = currentProcessor.getName();
continueCheck();
// 调用Process方法
currentProcessor.process(currentCuri);
}
}
setStep(STEP_DONE_WITH_PROCESSORS);
currentProcessorName = "";
}
catch (RuntimeExceptionWrapper e) {
// 如果是Berkeley DB的异常
if(e.getCause() == null) {
e.initCause(e.getDetail());
}
recoverableProblem(e);
} catch (AssertionError ae) {
recoverableProblem(ae);
} catch (RuntimeException e) {
recoverableProblem(e);
} catch (StackOverflowError err) {
recoverableProblem(err);
} catch (Error err) {
seriousError(err);
}
}
代码使用了双重循环来遍历整个处理器链的结构,第一重循环首先遍历所有的处理器链,第二重循环则在链内部遍历每个Processor,然后调用它的process()方法来执行处理逻辑。


10.3 扩展和定制Heritrix
在前面两节中,向读者介绍了Heritrix的启动、创建任务、抓取网页、组件结构。但是,读者应该也可以明显的看出,如果不用Heritrix抓取和分析网页的行为进行一定的控制,它是无法达到要求的。
对Heritrix的行为进行控制,是要建立在对其架构充分了解的基础之上的,因此,本节的内容完全是基于上一节中所讨论的基础。
10.3.1 向Heritrix中添加自己的Extractor
很明显,Heritrix内嵌的Extractor并不能够很好的完成所需要的工作,这不是说它不够强大,而是因为在解析一个网页时,常常有特定的需要。比如,可能只想抓取某种格式的链接,或是抓取某一特定格式中的文本片断。Heritrix所提供的大众化的Extractor只能够将所有信息全部抓取下来。在这种情况下,就无法控制Heritrix到底该抓哪些内容,不该抓哪些内容,进而造成镜象信息太复杂,不好建立索引。
以下就使用一个实例,来讲解该如何定制和使用Extractor。这个实例其实很简单,主要功能就是抓取所有在Sohu的新闻主页上出现的新闻,并且URL格式如下所示。
http://news.sohu.com/20061122/n246553333.shtml
(1)分析一下这个URL可以知道,其中的主机部分是http://news.sohu.com,这是搜狐新闻的域名,“20061122”应该表示的是新闻的日期,而最后的“n246553333.shtml”应该是一个新闻的编号,该编号全部以“n”打头。
(2)有了这样的分析,就可以根据URL的特点,来定出一个正则表达式,凡是当链接符合该正则表达式,就认为它是一个潜在的值得抓取的链接,将其收藏,以待抓取。正则表达式如下:
http://news.sohu.com/[\\d]+/n[\\d]+.shtml
(3)事实上所有的Extractor均继承自org.archive.crawler.extractor.Extractor这个抽象基类,在它的内部实现了innerProcess方法,以下便是innerProcess的实现:
代码10.10
public void innerProcess(CrawlURI curi) {
try {
/*
* 处理链接
*/
extract(curi);
} catch (NullPointerException npe) {
curi.addAnnotation("err=" + npe.getClass().getName());
curi.addLocalizedError(getName(), npe, "");
logger.log(Level.WARNING, getName() + ": NullPointerException", npe);
} catch (StackOverflowError soe) {
curi.addAnnotation("err=" + soe.getClass().getName());
curi.addLocalizedError(getName(), soe, "");
logger.log(Level.WARNING, getName() + ": StackOverflowError", soe);
} catch (java.nio.charset.CoderMalfunctionError cme) {
curi.addAnnotation("err=" + cme.getClass().getName());
curi.addLocalizedError(getName(), cme, "");
logger.log(Level.WARNING, getName() + ": CoderMalfunctionError", cme);
}
}
这个方法中,大部分代码都用于处理在解析过程中发生的各种异常和日志写入,不过,它为所有的Extractor定义了新的一个接口extract(CrawlURI),也就是说,所有的Extractor继承自它后,只需实现extract方法就可以了。以下是扩展Extractor时要做的几件事:
(1)写一个类,继承Extractor的基类。
(2)在构造函数中,调用父类的构造函数,以形成完整的家族对象。
查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. CSS美化上传按钮并获取上传文件路径

    先来看效果图上传功能很常见了,就是一个 input file 功能。但是默认的上传很丑啊有没有,所以网上也有很多美化的 css 文章,但是很多也只是美化以下按钮而已,今天遇到一个问题就是还要显示上传文件的路径,一般默认的都会在后面显示上传文件名,但是按照网上的教程美化之后,…...

    2024/4/20 17:11:38
  2. 【实验楼】python简明教程

    ①终端输入python进入 欣赏完自己的杰作后,按 Ctrl + D 输入一个 EOF 字符来退出解释器,你也可以键入 exit() 来退出解释器。 ②vim键盘快捷功能分布③这里需要注意如果程序中没有 #!/usr/bin/env python3 的话,应该使用 python3 helloworld.py 来执行,否则使用 ./hellowor…...

    2024/4/27 17:16:51
  3. ES6 -- 修饰器

    decorator是ES7引入的功能,它是一个函数,用来修改类甚至于是方法的行为。类的修饰一个简单的栗子:@testable class MyTestableClass {// ... }function testable(target) {target.isTestable = true; }MyTestableClass.isTestable // true上面的栗子中,@testable就是一个修…...

    2024/4/27 16:14:21
  4. Delphi+MapX5中使用自定义字体符号

    MapX5中使用自定义字体符号觉得现在使用自定义字体作为点符号的应用是越来越少了,近段时间在开发GPS车辆管理的GIS系统的时候要用到动态显示车辆位置,觉得用其他方式在地图上显示车辆可能会对性能有一定的影响,所以就使用的自定义字体。<?xml:namespace prefix = o ns =…...

    2024/4/27 15:15:30
  5. 看我是如何快速学习android开发的(一)

    原文地址为:看我是如何快速学习android开发的(一) 因为项目需要,8月中旬开始决定做安卓的程序,所以马上就开始学习安卓方面的开发知识,把最近的学习实践经历和大家分享分享。不要一开始就下载一大堆资料,视频,然后就不知道做什么了,要给自己定个目标,我要做什…...

    2024/4/27 15:59:12
  6. ES6 核心特性

    ES6 核心特性 前言 ES6 虽提供了许多新特性,但我们实际工作中用到频率较高并不多,根据二八法则,我们应该用百分之八十的精力和时间,好好专研这百分之二十核心特性,将会收到事半功倍的奇效一、开发环境配置 这部分着重介绍:babel 编译 ES6 语法,如何用 webpack 实现模块化…...

    2024/4/27 16:03:11
  7. CSS笔记(美化表格)

    一、表格的基本结构 表格由行、列、单元格三部分组成,单元格是行与列交叉的部分,可以拆分和合并。 表格<table>,行<tr>,单元格<td> 单元格可以包含文本、图片、列表、段落、表单、水平线、表格等。 1、标准化的表格结构 *<table>定义表格 *&…...

    2024/4/27 14:58:14
  8. 用哪种字体写博客比较好尼?

    一、宋体:为适应印刷术而生,因互联网半死。宋体的特点是:“横平竖直,横细竖粗,起落笔有棱有角。”“横平竖直”是为美观;“横细竖粗”是为坚固,为何坚固?度娘说:木板具有木纹,一般都是横向,刻制字的横向线条和木纹一致,比较结实;但刻制字的竖向线条时和木纹交叉,…...

    2024/4/27 15:12:10
  9. Python 简明教程 ---11,Python 元组

    微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io软件工程的目标是控制复杂度,而不是增加复杂性。 —— Dr. Pamela Zave目录我们在上一节介绍了Python 列表list 数据结构,本节来介绍一下元组tuple。 1,Python 元组 元组与列表有些相似,它们之间最显著的…...

    2024/4/27 14:12:16
  10. 网络爬虫之Windows环境Heritrix3.0配置指南

    一、引言:最近在忙某个商业银行的项目,需要引入外部互联网数据作为参考,作为技术选型阶段的工作,之前已经确定了中文分词工具,下一个话题就是网络爬虫的选择,目标很明确,需要下载一些财经网站的新闻信息,然后进行文本计算。记得上一次碰爬虫还是5年前,时过境迁,不知道…...

    2024/4/15 2:08:49
  11. 最全Android学习路线总结,绝对干货

    一、前言不知不觉自己已经做了几年开发了,由记得刚出来工作的时候感觉自己能牛逼,现在回想起来感觉好无知。懂的越多的时候你才会发现懂的越少。如果你的知识是一个圆,当你的圆越大时,圆外面的世界也就越大。最近看到很多Android新手问Android学习路线,学习方法啊,如何入…...

    2024/4/18 9:50:54
  12. python之一(简明python教程新旧版勘误)

    写在前面:作为一个非科班出身的“程序媛”已经好多天都没写过代码了,本科时候学过的那些C/C++也从来没有没有真正掌握过,因此开始进行每天编程一小时活动。虽然每天一小时时间不是很长,但我相信“不积跬步无以至千里”,坚持这项活动不为别的,只是想真正去投入一件事情。从…...

    2024/4/12 17:48:32
  13. 使用TTF字体

    TTFConfig ttfConfig("fonts/liuwen.ttf", 15); auto label1 = Label::createWithTTF(ttfConfig, "Print Resources"); label1->setPosition(visibleSize.width/2.0,visibleSize.height/2.0); addChild(label1);*/cocostudio自定义字体控件只能使用fnt字…...

    2024/4/12 17:48:52
  14. CSS+div美化和布局

    1、<div>标记与<span>标记<div>(division)是一个区块容器标记 ,即<div>与</div>之间可以容纳段落,标题、表格、图片、乃至章节、摘要等各种HTML元素。因此可以把<div>与</div>的内容视为一个独立对象,用于CSS控制。<span>作…...

    2024/4/16 3:41:13
  15. ES6 fetch函数与后台交互实现

    最近在学习react-native,遇到调用后端接口的问题.看了看官方文档,推荐使用es6的fetch来与后端进行交互,在网上找了一些资料.在这里整理,方便以后查询. 1.RN官方文档中,可使用XMLHttpRequest var request = new XMLHttpRequest(); request.onreadystatechange = (e) = >{if (…...

    2024/4/12 17:48:52
  16. 扩展Heritrix的FrontierScheduler(正则表达式的应用)

    heritrix有多个扩展点,这里我扩展FrontierScheduler,它是一个PostProcessor,它的作用是在Extractor 中所分析的链接加到Frontier中。 FrontierScheduler在org.archive.crawler.postprocessor包下面,我写一个类MyFrontierScheduler来继承FrontierScheduler类,,自己写 的MyF…...

    2024/4/12 17:49:28
  17. 《python简明教程》中的文件压缩代码整理(修改)

    原始资源来源于网络,后续代码与注释为自己修改 版本一,压缩文件,使用日期时间作为文件名# -*- coding:utf-8 -*- #版本一,压缩文件,使用日期时间作为文件名import os import time#1 先把需要备份的文件夹,保存进列表 source = [rE:\test] #注意备份的文件夹目录不能重复…...

    2024/4/12 17:49:28
  18. 修复系统字体,解决文字乱码问题。

    最近由于错装PS的字体,导致系统里很多软件中一些文字乱码。为了恢复系统字体到默认的正常状态需要做两件事: 1.清空C:\WINDOWS\Fonts文件夹,注意配合使用unlocker,清空注册表键值HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts; 2.下载windows默…...

    2024/4/25 22:10:54
  19. Ubuntu美化界面MacOS主题

    https://www.cnblogs.com/feipeng8848/p/8970556.html...

    2024/4/13 0:38:02
  20. Heritrix1.14源码分析(14)各种问题总结

    开博客以及建立Heritrix 群有一段时间了(这里谢谢大家的关注),这篇博客将整理这段时间所遇到的问题.同时由于自己从今年5月份开始就不怎么接触Heritrix,很多东西开始遗忘(不过里面思想没忘),同时目前的工作也没有必要接触Heritrix,所以不可能向之前写博客那样从源码的角度去考虑…...

    2024/4/19 16:57:22

最新文章

  1. 元宇宙APP搭建重点,会用到哪些三方服务?

    元宇宙APP的搭建是一个综合性的项目&#xff0c;涉及到众多关键要素和第三方服务。以下是一些元宇宙APP搭建的重点&#xff0c;以及可能用到的第三方服务&#xff1a; 一、搭建重点 技术框架的选择与搭建&#xff1a;元宇宙APP需要稳定、高效的技术框架来支撑其运行。这包括前…...

    2024/4/27 19:04:51
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. HiveSQL如何生成连续日期剖析

    HiveSQL如何生成连续日期剖析 情景假设&#xff1a; 有一结果表&#xff0c;表中有start_dt和end_dt两个字段&#xff0c;&#xff0c;想要根据开始和结束时间生成连续日期的多条数据&#xff0c;应该怎么做&#xff1f;直接上结果sql。&#xff08;为了便于演示和测试这里通过…...

    2024/4/27 12:27:43
  4. ChatGPT 赚钱初学者指南(上)

    原文&#xff1a;The Beginner’s Guide to Earning Money Online with ChatGPT 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第一章&#xff1a;理解基础知识 什么是 ChatGPT&#xff1f; 在人工智能与人类对话相遇的数字织锦中&#xff0c;ChatGPT 作为一个突出…...

    2024/4/27 11:05:12
  5. 3d representation的一些基本概念

    顶点&#xff08;Vertex&#xff09;&#xff1a;三维空间中的一个点&#xff0c;可以有多个属性&#xff0c;如位置坐标、颜色、纹理坐标和法线向量。它是构建三维几何形状的基本单元。 边&#xff08;Edge&#xff09;&#xff1a;连接两个顶点形成的直线段&#xff0c;它定…...

    2024/4/27 1:08:47
  6. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/4/26 18:09:39
  7. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/4/26 20:12:18
  8. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/26 23:05:52
  9. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/4/27 4:00:35
  10. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  11. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  12. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/26 21:56:58
  13. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/27 9:01:45
  14. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  15. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/4/25 18:39:16
  16. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/25 18:39:16
  17. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  18. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/26 22:01:59
  19. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/25 18:39:14
  20. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/4/26 23:04:58
  21. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/25 2:10:52
  22. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/25 18:39:00
  23. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/26 19:46:12
  24. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/4/27 11:43:08
  25. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/27 8:32:30
  26. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  27. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  28. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  29. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  30. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  31. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  32. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  33. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  34. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  35. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  36. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  37. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  38. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  39. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  40. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  41. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  42. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  43. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  44. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  45. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57