网页爬虫的设计与实现(Java版)
最近为了练手而且对网页爬虫也挺感兴趣,决定自己写一个网页爬虫程序。
首先看看爬虫都应该有哪些功能。
内容来自(http://www.ibm.com/developerworks/cn/java/j-lo-dyse1/index.html?ca=drs-)
网页收集的过程如同图的遍历,其中网页就作为图中的节点,而网页中的超链接则作为图中的边,通过某网页的超链接
得到其他网页的地址,从而可以进一步的进行网页收集;图的遍历分为广度优先和深度优先两种方法,网页的收集过程也是如此。综上,Spider
收集网页的过程如下:从初始 URL
集合获得目标网页地址,通过网络连接接收网页数据,将获得的网页数据添加到网页库中并且分析该网页中的其他 URL 链接,放入未访问 URL
集合用于网页收集。下图表示了这个过程:
网页收集器
Gather
网页收集器通过一个 URL 来获取该 URL 对应的网页数据,其实现主要是利用 Java 中的 URLConnection
类来打开 URL 对应页面的网络连接,然后通过 I/O 流读取其中的数据,BufferedReader
提供读取数据的缓冲区提高数据读取的效率以及其下定义的 readLine() 行读取函数。代码如下 ( 省略了异常处理部分 ):
URL url = new URL(“http://www.xxx.com”); URLConnection conn = url.openConnection(); BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line = null; while((line = reader.readLine()) != null) document.append(line + "\n");
网页处理
收集到的单个网页,需要进行两种不同的处理,一种是放入网页库,作为后续处理的原始数据;另一种是被分析之后,抽取其中的 URL 连接,放入 URL 池等待对应网页的收集。
网页的保存需要按照一定的格式,以便以后数据的批量处理。这里介绍一种存储数据格式,该格式从北大天网的存储格式简化而来:
- 网页库由若干记录组成,每个记录包含一条网页数据信息,记录的存放为顺序添加;
- 一条记录由数据头、数据、空行组成,顺序为:头部 + 空行 + 数据 + 空行;
- 头部由若干属性组成,有:版本号,日期,IP 地址,数据长度,按照属性名和属性值的方式排列,中间加冒号,每个属性占用一行;
- 数据即为网页数据。
需要说明的是,添加数据收集日期的原因,由于许多网站的内容都是动态变化的,比如一些大型门户网站的首页内容,这就意味着如果不是当天爬取的网页数据,很可能发生数据过期的问题,所以需要添加日期信息加以识别。
URL 的提取分为两步,第一步是 URL 识别,第二步再进行 URL 的整理,分两步走主要是因为有些网站的链接是采用相对路径,如果不整理会产生错误。URL 的识别主要是通过正则表达式来匹配,过程首先设定一个字符串作为匹配的字符串模式,然后在 Pattern 中编译后即可使用 Matcher 类来进行相应字符串的匹配。实现代码如下:
public ArrayList<URL> urlDetector(String htmlDoc) { final String patternString = "<[a|A]\\s+href=([^>]*\\s*>)"; Pattern pattern = Pattern.compile(patternString,Pattern.CASE_INSENSITIVE); ArrayList<URL> allURLs = new ArrayList<URL>(); Matcher matcher = pattern.matcher(htmlDoc); String tempURL; //初次匹配到的url是形如:<a href="http://bbs.life.xxx.com.cn/" target="_blank"> //为此,需要进行下一步的处理,把真正的url抽取出来, //可以对于前两个"之间的部分进行记录得到url while(matcher.find()){ try { tempURL = matcher.group(); tempURL = tempURL.substring(tempURL.indexOf("\"")+1); if(!tempURL.contains("\"")) continue; tempURL = tempURL.substring(0, tempURL.indexOf("\"")); } catch (MalformedURLException e) { e.printStackTrace(); } } return allURLs; }
按照“<[a|A]\\s+href=([^>]*\\s*>)
”这个正则表达式可以匹配出 URL 所在的整个标签,形如“<a href="http://bbs.life.xxx.com.cn/" target="_blank">
”,所以在循环获得整个标签之后,需要进一步提取出真正的 URL,我们可以通过截取标签中前两个引号中间的内容来获得这段内容。如此之后,我们可以得到一个初步的属于该网页的 URL 集合。
接下来我们进行第二步操作,URL 的整理,即对之前获得的整个页面中 URL 集合进行筛选和整合。整合主要是针对网页地址是相对链接的部分,由于我们可以很容易的获得当前网页的 URL,所以,相对链接只需要在当前网页的 URL 上添加相对链接的字段即可组成完整的 URL,从而完成整合。另一方面,在页面中包含的全面 URL 中,有一些网页比如广告网页是我们不想爬取的,或者不重要的,这里我们主要针对于页面中的广告进行一个简单处理。一般网站的广告连接都有相应的显示表达, 比如连接中含有“ad”等表达时,可以将该链接的优先级降低,这样就可以一定程度的避免广告链接的爬取。
经过这两步操作时候,可以把该网页的收集到的 URL 放入 URL 池中,接下来我们处理爬虫的 URL 的派分问题。
Dispatcher 分配器
分配器管理 URL,负责保存着 URL 池并且在 Gather 取得某一个网页之后派分新的 URL,还要避免网页的重复收集。分配器采用设计模式中的单例模式编码,负责提供给 Gather 新的 URL,因为涉及到之后的多线程改写,所以单例模式显得尤为重要。
重复收集是指物理上存在的一个网页,在没有更新的前提下,被 Gather 重复访问,造成资源的浪费,主要原因是没有清楚的记录已经访问的 URL 而无法辨别。所以,Dispatcher 维护两个列表 ,“已访问表”,和“未访问表”。每个 URL 对应的页面被抓取之后,该 URL 放入已访问表中,而从该页面提取出来的 URL 则放入未访问表中;当 Gather 向 Dispatcher 请求 URL 的时候,先验证该 URL 是否在已访问表中,然后再给 Gather 进行作业。
Spider 启动多个 Gather 线程
现在 Internet 中的网页数量数以亿计,而单独的一个 Gather 来进行网页收集显然效率不足,所以我们需要利用多线程的方法来提高效率。Gather 的功能是收集网页,我们可以通过 Spider 类来开启多个 Gather 线程,从而达到多线程的目的。代码如下:
public void start() { Dispatcher disp = Dispatcher.getInstance(); for(int i = 0; i < gatherNum; i++) { Thread gather = new Thread(new Gather(disp)); gather.start(); } } 在开启线程之后,网页收集器开始作业的运作,并在一个作业完成之后,向 Dispatcher 申请下一个作业,因为有了多线程的 Gather,为了避免线程不安全,需要对 Dispatcher 进行互斥访问,在其函数之中添加 synchronized 关键词,从而达到线程的安全访问。