在寒假的时候,大致看过一本介绍数学在信息科学中应用的书,叫做《数学之美》。作者是前谷歌研究院吴军博士。同时,他还有另外一本比较不错的书,叫做《浪潮之巅》。这本书中,介绍了许多在信息技术领域里,知名的大公司和他们的兴衰历史。《数学之美》中,有一小部分内容,其中介绍到了网络爬虫的大致的思路。本学期选修了java的课程,因此便想深入的研究一下网络爬虫。于是写下了这一篇电子笔记吧。
网络爬虫——说的简单一些,其实质相当于对图的遍历。网络上的网页,可以看成是一个图中的节点,网页中存在的超链接,则是节点之间的边。对图的遍历有两种最基本的方法。深度优先搜索和广度优先搜索。而对于网络爬虫而言,由于存在的链接可能很深,而又要爬到一些重要的网页,而这些网页大致分布在一个比较合理的深度上,因此,单纯的采用深度优先搜索的方式,对网页进行获取,将可能得不到很多高质量的网页。所以,实际上使用的网络爬虫,一般会采用类似于广度优先搜索的策略进行下载。这样可以保证能够下载到比较不错的网页。
前一段时间,看过一篇文章,是基于主题的网络爬虫的探究。在那里,文章作者分析到,现在的搜索引擎,可以满足一般大众的普通要求,但对于一些专业的,特定主题的搜索需求则没法满足。在那篇文章中,作者提出,当对网络爬虫使用深度优先搜索时,在超过一定的深度后,一些原本隐藏的,不易被爬取到的高质量的网页就可以被下载到了。(我由于还在读本科,专业的知识还很缺乏,因此大致的理解是这样的。也不知道自己的理解是否正确?)。所以,在一般情况下,网络爬虫会对两种图的遍历的方式结合使用。但是,仍以广度优先搜索为主,深度优先搜索为辅。
下面是通过代码的形式来进一步的理解一般的网络爬虫的大致的结构。
下面,演示一个主要爬取搜狐新闻的一个小示例:
在程序中,要用到两个用于解析Html文档的包:htmlparser.jar和htmllexer.jar包。
首先,定义了一个新闻实体。包含newsAuthor,newsDate,newsContent,newsTitle,URL五个需要用到的数据成员和他们的setter和getter方法。代码如下:
/**
* NewsBean.java
* @author Hades
* @date 2014/4/7
*/
package org.hades.sohu.bean;
public class NewsBean {
private String newsTitle;
private String newsAuthor;
private String newsContent ;
private String newsDate;
private String newsURL;
public String getNewsTitle(){
return newsTitle;
}
public void setNewsTitle(String newsTitle) {
this.newsTitle = newsTitle ;
}
public String getNewsAuthor () {
return newsAuthor ;
}
public void setNewsAuthor(String newsAuthor) {
this.newsAuthor = newsAuthor ;
}
public String getNewsContent() {
return newsContent;
}
public void setNewsContent(String newsContent) {
this.newsContent = newsContent ;
}
public String getNewsDate(){
return newsDate;
}
public void setNewsDate(String newsDate){
this.newsDate = newsDate ;
}
public String getNewsURL() {
return newsURL;
}
public void setNewsURL(String newsURL) {
this.newsURL = newsURL ;
}
}
然后,创建一张数据库表,用于存储下载下来的数据:表的数据列就和上诉实体类的数据成员相对应。
CREATE DATABASE IF NOT EXISTS sohunews;
USE sohunews;
--
-- Definition of table `news`
--
DROP TABLE IF EXISTS `news`;
CREATE TABLE `news` (
`newsid` int(11) NOT NULL auto_increment,
`newstitle` varchar(60) NOT NULL,
`newsauthor` varchar(20) NOT NULL,
`newscontent` text NOT NULL,
`newsurl` char(130) NOT NULL,
`newsdate` varchar(24) NOT NULL,
PRIMARY KEY (`newsid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
为了方便连接数据库,因此创建了一个数据库的连接的专用的类:用于对数据库的连接进行管理。
/**
* ConnectionManager.java
* @author Hades
* @date 2014/4/7
*/
package org.hades.sohu.db;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ConnectionManager {
private Connection conn = null;
private boolean autoCommit = true ;
public ConnectionManager() {
}
/**
* 获得数据库连接
*/
public Connection getConnection() {
try{
String url="jdbc:mysql://localhost:3306/sohuNews" ;
String user="root";
String password="root";
Class.forName("com.mysql.jdbc.Driver").newInstance();
conn=DriverManager.getConnection(url, user, password);
conn.setAutoCommit(autoCommit);
}catch (SQLException ex) {
Logger.getLogger(ConnectionManager.class.getName()).log(Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
Logger.getLogger(ConnectionManager.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
Logger.getLogger(ConnectionManager.class.getName()).log(Level.SEVERE, null, ex);
} catch (ClassNotFoundException ex) {
Logger.getLogger(ConnectionManager.class.getName()).log(Level.SEVERE, null, ex);
}
return conn ;
}
/**
* 关闭数据库连接
*/
public void close() {
if(conn!=null){
try{
conn.close();
}catch(Exception e) {
e.printStackTrace();
}finally{
conn = null ;
}
}
}
}
然后是一个专门用于抓取新闻,并将抓取的数据存入数据库的类。
/**
* SohuNews.java
* @author Hades
* @date 2014/4/7
*/
package org.hades.sohu.news;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.hades.sohu.bean.NewsBean;
import org.hades.sohu.db.ConnectionManager;
import org.htmlparser.*;
import org.htmlparser.beans.StringBean;
import org.htmlparser.filters.AndFilter;
import org.htmlparser.filters.HasAttributeFilter;
import org.htmlparser.filters.TagNameFilter;
import org.htmlparser.tags.Div;
import org.htmlparser.tags.HeadingTag;
import org.htmlparser.tags.Span;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
/**
* 用于对搜狐网站上的新闻进行抓取
* @author Hades
*
*/
public class SohuNews {
private Parser parser;
private List newsList = new ArrayList() ;
private NewsBean bean = new NewsBean() ;
private ConnectionManager manager = null ; //数据库连接管理器
private PreparedStatement pstmt = null ;
public SohuNews() {
}
/**
* 获得一条完整的新闻
* @param NewsBean
* @return
*/
public List getNewsList(final NewsBean newsBean) {
List <String> list = new ArrayList<String> ();
String newsTitle = newsBean.getNewsTitle();
String newsAuthor = newsBean.getNewsAuthor();
String newsContent = newsBean.getNewsContent();
String newsDate = newsBean.getNewsDate();
list.add(newsTitle);
list.add(newsAuthor);
list.add(newsContent);
list.add(newsDate);
return list;
}
/**
* 设置新闻对象,让新闻对象里有新闻数据
* @param newsTitle 新闻标题
* @param newsAuthor 新闻作者
* @param newsContent 新闻内容
* @param newsDate 新闻日期
* @param url 新闻链接
*
*/
public void setNews(String newsTitle , String newsAuthor, String newsContent , String newsDate, String url) {
bean.setNewsTitle(newsTitle);
bean.setNewsAuthor(newsAuthor);
bean.setNewsContent(newsContent);
bean.setNewsDate(newsDate);
bean.setNewsURL(url);
}
/**
*
*/
protected void newsToDatabase() {
Thread thread = new Thread (new Runnable () {
public void run() {
boolean success = saveToDB(bean);
if(success !=false) {
//System.out.println("插入数据成功");
}
}
}) ;
thread.start();
}
/**
* 将新闻插入到数据库
* @param newsBean
* @return
*/
public boolean saveToDB(NewsBean bean) {
boolean flag = true ;
String sql="insert into news(newstitle, newsauthor, newscontent, newsurl,newsdate) values(?,?,?,?,?)";
manager = new ConnectionManager();
String titleLength = bean.getNewsTitle();
if(titleLength.length()>60) { //标题太长的新闻,不要了
return flag;
}
try{
pstmt = manager.getConnection().prepareStatement(sql);
pstmt.setString(1, bean.getNewsTitle());
pstmt.setString(2, bean.getNewsAuthor());
pstmt.setString(3, bean.getNewsContent());
pstmt.setString(4, bean.getNewsURL());
pstmt.setString(5, bean.getNewsDate());
flag = pstmt.execute();
}catch(SQLException ex) {
Logger.getLogger(SohuNews.class.getClass().getName()).log(Level.SEVERE, null, ex);
}finally {
try{
pstmt.close();
manager.close();
}catch(SQLException ex) {
Logger.getLogger(SohuNews.class.getClass().getName()).log(Level.SEVERE,null,ex);
}
}
return flag ;
}
/**
* 获得新闻的标题
* @param titleFilter
* @param parser
* @return
*/
public String getTitle(NodeFilter titleFilter, Parser parser) {
String titleName= "";
try{
NodeList nodeList = (NodeList)parser.parse(titleFilter);
for(int i=0;i<nodeList.size();i++) {
HeadingTag title = (HeadingTag) nodeList.elementAt(i);
titleName = title.getStringText();
}
}catch(ParserException ex) {
Logger.getLogger(SohuNews.class.getName()).log(Level.SEVERE,null,ex);
}
return titleName ;
}
/**
* 获得新闻的作者
* @param newsauthorFilter
* @param parser
*/
public String getAuthor(NodeFilter newsAuthorFilter, Parser parser) {
String authorName ="";
try{
NodeList nodeList = (NodeList)parser.parse(newsAuthorFilter);
for(int i=0;i<nodeList.size();i++) {
Span authorSpan = (Span)nodeList.elementAt(i);
authorName = authorSpan.getStringText();
}
}catch(ParserException ex){
ex.printStackTrace();
}
return authorName ;
}
/**
* 获得新闻的日期
*/
public String getDate(NodeFilter newsDateFilter , Parser parser) {
String newsDate = null ;
try{
NodeList nodeList = (NodeList)parser.parse(newsDateFilter) ;
for(int i=0;i<nodeList.size();i++) {
Div dateTag = (Div)nodeList.elementAt(i);
newsDate = dateTag.getStringText();
}
}catch(ParserException ex){
ex.printStackTrace();
}
return newsDate ;
}
/**
* 获取新闻的内容
* @param newsContentFilter
* @param parser
* @return content
*/
public String getContent(NodeFilter newsContentFilter, Parser parser) {
String content =null;
StringBuilder builder = new StringBuilder() ;
try{
NodeList nodeList = (NodeList)parser.parse(newsContentFilter) ;
for(int i=0;i<nodeList.size();i++) {
Div contentTag = (Div)nodeList.elementAt(i);
builder.append(contentTag.getStringText());
}
content = builder.toString();
if(content !=null) {
parser.reset();
parser = Parser.createParser(content, "gb2312");
StringBean sb = new StringBean();
sb.setCollapse(true);
parser.visitAllNodesWith(sb);
content = sb.getStrings();
content = content.replaceAll("\\\".*[a-z].*\\}", "" );
content = content.replace("[我来说两句]", "");
}else {
System.out.println("没有得到新闻");
}
}catch(ParserException ex)
{
ex.printStackTrace();
}
return content ;
}
/**
* 对新闻URL进行解析提取新闻,同时将新闻插入到数据库中。
* @param url 新闻连接。
*/
public void parser(String url) {
try {
parser = new Parser(url);
NodeFilter titleFilter = new TagNameFilter("h1");
NodeFilter contentFilter = new AndFilter(new TagNameFilter("div"), new HasAttributeFilter("id", "contentText"));
NodeFilter newsdateFilter = new AndFilter(new TagNameFilter("div"), new HasAttributeFilter("class", "time"));
NodeFilter newsauthorFilter = new AndFilter(new TagNameFilter("span"), new HasAttributeFilter("class", "editer"));
String newsTitle = getTitle(titleFilter, parser);
parser.reset(); //记得每次用完parser后,要重置一次parser。要不然就得不到我们想要的内容了。
String newsContent = getContent(contentFilter, parser);
System.out.println(newsContent); //输出新闻的内容,查看是否符合要求
parser.reset();
String newsDate = getDate(newsdateFilter, parser);
parser.reset();
String newsAuthor = getAuthor(newsauthorFilter, parser);
//先设置新闻对象,让新闻对象里有新闻内容。
setNews(newsTitle, newsAuthor, newsContent, newsDate, url);
//将新闻添加到数据中。
this.newsToDatabase();
} catch (ParserException ex) {
Logger.getLogger(SohuNews.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
这里的细节内容比较多。首先,我们,可以打开搜狐新闻的首页,随便找到一篇新闻,通过查看源代码,了解我们要获取的内容,应该怎样解析出来。
首先是新闻的标题:<h1 itemprop="headline">多地公务员招报人数下降 公考降温或成趋势</h1>
通过这句代码,我们了解到可以通过h1标签来定位一篇新闻的标题。
NodeFilter titleFilter = new TagNameFilter("h1");
String newsTitle = getTitle(titleFilter, parser);
通过,这两句代码,就可以将网页源代码中的新闻的标题获取出来。
然后是新闻的内容:
<!-- 正文 --> | |
<div class="text clear" id="contentText"> |
NodeFilter contentFilter = new AndFilter(new TagNameFilter("div"), new HasAttributeFilter("id", "contentText"));
String newsContent = getContent(contentFilter, parser);
System.out.println(newsContent); //输出新闻的内容,查看是否符合要求
然后是新闻的时间:
<div class="time" itemprop="datePublished" content="2014-03-25T00:03:00+08:00">2014年03月25日00:03</div>
所以可以使用如下的语句,获取新闻的时间:
NodeFilter newsdateFilter = new AndFilter(new TagNameFilter("div"), new HasAttributeFilter("class", "time"));
String newsDate = getDate(newsdateFilter, parser);
然后是新闻的作者:
<span class="editer">(责任编辑:包特日格勒)</span>
可以使用如下的语句,获取新闻的作者:
NodeFilter newsauthorFilter = new AndFilter(new TagNameFilter("span"), new HasAttributeFilter("class", "editer"));
String newsAuthor = getAuthor(newsauthorFilter, parser);
上诉的解析过程就用到了上面提到的包中提供的数据类型和相应的方法的操作。使得对内容的获取要十分的方便。
将获得的数据,存入实体中,然后保存到数据库中进行永久存储。
然后下面是网络爬虫的具体的实现部分:
因为,图的深度优先遍历中,需要使用到的数据结构是队列。所以,下面的代码首先定义了一个队列,并封装了队列的常用的方法。
/**
* Queue.java
* @author Hades
* @date 2014/4/8
*/
package org.hades.sohu.crawler;
/**
* 数据结构队列
*/
import java.util.LinkedList;
public class Queue<T> {
private LinkedList <T> queue = new LinkedList <T> () ;
public void enQueue(T t) {
queue.addLast(t);
}
public T deQueue() {
return queue.removeFirst();
}
public boolean isQueueEmpty() {
return queue.isEmpty();
}
public boolean Contains (T t) {
return queue.contains(t);
}
public boolean empty() {
return queue.isEmpty();
}
}
我们可以获取到很多的超链接,然后保存到队列中,但有时候,需要过滤一下看是否需要这些超链接。因此,首先定义了一个接口,用于过滤超链接。
/**
* LinkFilter.java
* @author Hades
* @date 2014/4/8
*/
package org.hades.sohu.crawler;
public interface LinkFilter {
public boolean accept(String url);
}
然后,定义了一个用于保存已经访问过的Url和尚未访问到的Url的静态的类。并提供了对他们进行修改的方法。方便在网络爬虫的实现中,进行操作。
/**
* 用来保存已经访问和等待访问的Url的类
* LinkDB.java
* @author Hades
* @date 2014/4/8
*/
package org.hades.sohu.crawler;
import java.util.HashSet;
import java.util.Set;
public class LinkDB {
//已经访问过的Url
private static Set <String> VisitedUrl = new HashSet <String>();
//尚未访问的Url
private static Queue <String> UnvisitedUrl = new Queue<String> ();
public static Queue<String> getUnvisitedUrl( ) {
return UnvisitedUrl ;
}
public static void addVisitedUrl(String url) {
VisitedUrl.add(url);
}
public static void removeVisitedUrl(String url) {
VisitedUrl.remove(url);
}
public static String unvisitedUrlDeQueue() {
return UnvisitedUrl.deQueue();
}
//保证每一个Url只被访问一次
public static void addUnvisitedUrl(String url) {
if(url!=null&& !url.trim().equals("")&&!VisitedUrl.contains(url)&& !UnvisitedUrl.Contains(url)) {
UnvisitedUrl.enQueue(url);
}
}
public static int getVisitedUrlNum() {
return VisitedUrl.size();
}
public static boolean unVisitedUrlEmpty() {
return UnvisitedUrl.empty();
}
}
网页文件中,还需要重点处理的一种标签就是超链接的标签,当识别出来是一种超链接的标签时,要通过接口,判断一下这个标签是否是我们需要的标签。如果是我们需要的标签(这里根据搜狐新闻的超链接地址的特点,用了一个正则表达式进行匹配的),可以通过LinkDB这个静态类,将超链接加入到待访问的超链接的队列中。
实际上LinkDB类就是用于为维护访问队列而服务的。下面的类,实现超链接的识别,然后调用LinkDB的方法进行维护。
/**
* 用来收集新闻链接地址,将符合正则表达式的URL添加到URL数组中
* LinkParser.java
* @author Hades
* @date 2014/4/8
*/
package org.hades.sohu.crawler;
import java.util.HashSet;
import java.util.Set;
import org.hades.sohu.news.SohuNews;
import org.htmlparser.Node;
import org.htmlparser.NodeFilter;
import org.htmlparser.Parser;
import org.htmlparser.filters.NodeClassFilter;
import org.htmlparser.filters.OrFilter;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
public class LinkParser {
public static Set<String> extracLinks(String url , LinkFilter filter) {
Set <String> links = new HashSet <String> () ;
try{
Parser parser = new Parser(url);
parser.setEncoding("gb2312");
NodeFilter frameFilter = new NodeFilter() {
public boolean accept(Node node) {
if(node.getText().startsWith("frame src=")) {
return true ;
}else {
return false ;
}
}
} ;
OrFilter linkFilter = new OrFilter(new NodeClassFilter (LinkTag.class),frameFilter );
NodeList list = parser.extractAllNodesThatMatch(linkFilter);
for(int i=0;i<list.size();i++) {
Node tag = list.elementAt(i);
if(tag instanceof LinkTag) {
LinkTag linkTag = (LinkTag) tag;
String linkUrl = linkTag.getLink();
if(filter.accept(linkUrl)) {
links.add(linkUrl);
}
}else {
String frame = tag.getText();
int start =frame.indexOf("src=");
frame = frame.substring(start);
int end = frame.indexOf(" ");
if(end ==-1) {
end = frame.indexOf(">");
}
String frameUrl = frame.substring(5,end-1);
if(filter.accept(frameUrl)) {
links.add(frameUrl);
}
}
}
}catch(ParserException ex){
ex.printStackTrace();
}
return links ;
}
}
下面是Crawler类的实现,首先通过一个种子初始化队列,然后调用爬取方法,爬取所有的满足条件的Url:
/**
* Crawler.java
* @author Hades
* @date 2014/4/8
*/
package org.hades.sohu.crawler;
import java.util.Set;
import org.hades.sohu.news.SohuNews;
public class Crawler {
SohuNews news = new SohuNews();
//使用种子url初始化URL队列
private void initCrawlerSeeds(String[] seeds) {
for(int i=0;i<seeds.length;i++){
LinkDB.addUnvisitedUrl(seeds[i]);
}
}
//爬取方法
public void crawling(String[] seeds) {
LinkFilter filter = new LinkFilter() {
public boolean accept(String url) {
if(url.matches("http://news.sohu.com/[\\d]+/n[\\d]+.shtml")) {
return true ;
}else {
return false ;
}
}
};
//初始化Url队列
initCrawlerSeeds(seeds);
//循环条件
while(!LinkDB.unVisitedUrlEmpty()&&LinkDB.getVisitedUrlNum()<=10) {
String visitUrl = LinkDB.unvisitedUrlDeQueue();
if(visitUrl==null) {
continue ;
}
LinkDB.addVisitedUrl(visitUrl);
Set<String> links = LinkParser.extracLinks(visitUrl,filter);
for(String link:links) {
LinkDB.addUnvisitedUrl(link);
news.parser(link);
}
}
}
public static void main(String args[]) {
Crawler crawler = new Crawler();
crawler.crawling(new String [] {"http://news.sohu.com"});
}
}
至此,一个简单的网络爬虫的大致的框架就实现了。
(待解决问题:程序运行一段时间后,会导致出现一个异常:Caused by: java.net.SocketException: socket closed)。现在还没有解决这个问题。
另外:http://blog.csdn.net/csy2005csy/article/details/6953878是一篇讲解一款开源网络爬虫的博文。