本实战仅作为学习和技术交流使用,转载请注明出处;本实战中的代码逻辑编写参考《自己动手写网络爬虫》一书,本书提供的章节源码
由于是第一篇实战,因此此处将较为详细的从基础讲解java爬虫原理及相关实现技术。
网页抓取的原理其实并不是想象的那般神秘和复杂,简单的说便是将用户想获取到的信息从网页端保存到本地的一个操作过程。但是这个过程的一个重要问题便是:如何抓?
普通用户打开浏览器,输入网站的url便能进行网页访问。而这个url是什么?相信对于网络编程有过相关认识的人一定很了解,此处简要介绍。url是指上只是一个简单的字符串,如http://www.baidu.com,称为统一资源定位符,其由三个部分组成:
· 协议
· 主机IP地址
· 主机资源的具体地址,如目录和文件名
(具体细节并不在这里赘述)
而要想抓取网页上的内容,实际上是抓取服务器上资源,其过程就是通过URL定位到制定服务器上,将相应服务器上的资源通过http响应机制传回到本地,而抓取网页所要做的根本在于将request请求发送至服务器上,服务器做出response响应,我们将该响应经过相应处理,便能得到想要的资源。
对于网页抓取,我们更习惯称其为“爬虫”,亦或是“Robot”。在爬虫领域,最出名的当然是google搜索引擎(虽然baidu也很不错,但其商业性质实在……),而作为我们普通大众而言,想实现爬虫,其技术门槛并没有那么难。其原因根本在于使用的人多的跟“米”一样,因此你可以在网上找到各式各样的教材,材料,源码,框架等等。当然实现像google一样的搜索引擎并不是一朝一夕的事!我们常见的大众爬虫的实现更多的基于Python,Python因为其语言的特殊性,使得其在爬虫领域得到广泛的应用。当然,相应的java所实现爬虫也不在少数,著名的爬虫框架则是Apache的Nutch,结合Lucene使用,将实现一个非常强大的搜索引擎。
对于java实现爬虫常见的便是通过HttpClient和HtmlParser分别进行网络通信和html页面分析,因此实现一个简单的java爬虫技术门槛并不高,只要你学会了他们,便算是入门了。
HttpClient是Apache Jakarta Common下的子项目,用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议。HttpClient已经应用在很多的项目中,比如Apache Jakarta上很著名的另外两个开源项目Cactus和HTMLUnit都使用了HttpClient。
下载地址: http://hc.apache.org/downloads.cgi
使用HttpClient发送请求、接收响应很简单,一般需要如下几步即可。
1. 创建HttpClient对象。
2. 创建请求方法的实例,并指定请求URL。如果需要发送GET请求,创建HttpGet对象;如果需要发送POST请求,创建HttpPost对象。
3. 如果需要发送请求参数,可调用HttpGet、HttpPost共同的setParams(HetpParams params)方法来添加请求参数;对于HttpPost对象而言,也可调用setEntity(HttpEntity entity)方法来设置请求参数。
4. 调用HttpClient对象的execute(HttpUriRequest request)发送请求,该方法返回一个HttpResponse。
5. 调用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器的响应内容。程序可通过该对象获取服务器的响应内容。
实现实例
/**
*将抓取的网页下载到本地
*/
String filePath = null;
/* 1.生成 HttpClinet 对象并设置参数 */
HttpClient httpClient = new HttpClient();
// 设置 Http 连接超时 5s
httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
/* 2.生成 GetMethod 对象并设置参数 */
//
GetMethod getMethod = new GetMethod(url);
// 设置 get 请求超时 5s
getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
// 设置请求重试处理
getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler());
/* 3.执行 HTTP GET 请求 */
try {
int statusCode = httpClient.executeMethod(getMethod);
// 判断访问的状态码
if (statusCode != HttpStatus.SC_OK) {
System.err.println("Method failed: "
+ getMethod.getStatusLine());
filePath = null;
}
/* 4.处理 HTTP 响应内容 */
byte[] responseBody = getMethod.getResponseBody();// 读取为字节数组
// 根据网页 url 生成保存时的文件名
filePath = "temp\\"
+ getFileNameByUrl(url, getMethod.getResponseHeader(
"Content-Type").getValue());
saveToLocal(responseBody, filePath);
} catch (HttpException e) {
// 发生致命的异常,可能是协议不对或者返回的内容有问题
System.out.println("Please check your provided http address!");
e.printStackTrace();
} catch (IOException e) {
// 发生网络异常
e.printStackTrace();
} finally {
// 释放连接
getMethod.releaseConnection();
}
return filePath;
正则表达式是字符提取的一个重要技术手段。关于正则表达式的细节,可以百度,对于java而言,实现正则提取也很简单,提供如下实例:
public boolean find(String regex,String url){
//匹配以文件形式结尾的url
//regex : ".*\\.(rar|png|jpg|bmp|gif|doc|zip|xls|zip|ZIP|PDF|DOC|JPG|JPEG|PNG|GIF)"
//url : http://news.twt.edu.cn//default2010B/twt1/img/resourceIcon5.png
Pattern pattern = Pattern.compile(regex);
//Pattern类即为正则表达式
Matcher matcher = pattern.matcher(url);
//对传入的url进行字符提取和匹配,matches()为匹配,group()则为提取
if(matcher.matches())
return true;//如果匹配成功
else
return false;
}
HtmlParser是html解析器中的一种,本实战纯粹只为实现而已,并没有说HtmlParser更好用,反而现在用的更加流行的则是Jsoup,同样地,二者都能够实现对于Html各个节点标签进行解析。
HtmlParser的关键步骤也同样简单,分为:
(1)通过Parser类创建一个解释器
(2)创建Filter或者Visitor
(3)使用parser根据filter或者visitor来取得所有符合条件的节点
(4)对节点内容进行处理
实现代码:
public class HtmlParserTool {
// 获取一个网站上的链接,filter 用来过滤链接
public static Set<String> extracLinks(String url, LinkFilter filter) {
Set<String> links = new HashSet<String>();
try {
Parser parser = new Parser(url);
parser.setEncoding("utf-8");
// 过滤 标签的 filter,用来提取 frame 标签里的 src 属性所表示的链接
NodeFilter frameFilter = new NodeFilter() {
public boolean accept(Node node) {
if (node.getText().startsWith("frame src=")||node.getText().contains("img ")) {
return true;
} else {
return false;
}
}
};
// OrFilter 来设置过滤 标签,和
标签
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 link = (LinkTag) tag;
String linkUrl = link.getLink();// url
if (filter.accept(linkUrl))
links.add(linkUrl);
} else// 标签
{
// 提取 frame 里 src 属性的链接如
String frame = tag.getText();
int start = frame.indexOf("src=");
if(start!=-1){
frame = frame.substring(start);
int end = frame.indexOf(" ");
String frameUrl ="";
if (end == -1)
frameUrl = frame.substring(5,frame.lastIndexOf("\""));
else
frameUrl = frame.substring(5, end - 1);
if (filter.accept(frameUrl))
links.add(frameUrl);
}
}
}
} catch (ParserException e) {
e.printStackTrace();
}
return links;
}
}
本次实战内容抓取的信息门户网站对象为天津大学的新闻网站,http://news.twt.edu.cn,本次实战为单线程抓取,使用宽度优先策略,抓取的url限制为1000以内,抓取资源主要为图片和文档,并且保存在本地的temp文件夹中。以下贴出源码,并且在源码中加了注释。
项目目录结构
抓取到的本地文件样式
主类:MyCrawler
public class MyCrawler {
/**
* 使用种子初始化 URL 队列
* @return
* @param seeds 种子URL
*/
private void initCrawlerWithSeeds(String[] seeds)
{
for(int i=0;i/**
* 抓取过程
* @return
* @param seeds
*/
public void crawling(String[] seeds)
{ //定义过滤器
LinkFilter filter = new LinkFilter(){
public boolean accept(String url) {
/*if(url.startsWith("http://")){*/
if(url.contains("twt.edu.cn")||url.contains("image")||url.contains("img"))
return true;
else
return false;
/*}else
return false;*/
}
};
//初始化 URL 队列
initCrawlerWithSeeds(seeds);
DownLoadFile downLoader=new DownLoadFile();
//循环条件:待抓取的链接不空且抓取的网页不多于1000
while(!LinkQueue.unVisitedUrlsEmpty()&&LinkQueue.getVisitedUrlNum()<=1000)
{
//队头URL出队列
String visitUrl=(String)LinkQueue.unVisitedUrlDeQueue();
if(visitUrl==null)
continue;
/*DownLoadFile downLoader=new DownLoadFile();
//下载网页s
downLoader.downloadFile(visitUrl);*/
//下载网页中的图片,jpg,
String regex = ".*\\.(rar|png|jpg|bmp|gif|doc|zip|xls|zip|ZIP|PDF|DOC|JPG|JPEG|PNG|GIF)";
System.out.println(visitUrl);
if(visitUrl.startsWith("/"))
visitUrl = "http://news.twt.edu.cn/" + visitUrl;
if(find(regex,visitUrl)){
System.out.println("****"+visitUrl);
downLoader.downloadFile(visitUrl);
}else{
//如果是图片文件则直接下载,无需进行进行扩展搜索
//该 url 放入到已访问的 URL 中
LinkQueue.addVisitedUrl(visitUrl);
//提取出下载网页中的 URL
Set links=HtmlParserTool.extracLinks(visitUrl,filter);
//新的未访问的 URL 入队
for(String link:links)
{
LinkQueue.addUnvisitedUrl(link);
}
}
}
}
public boolean find(String regex,String url){
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(url);
if(matcher.matches())
return true;
else
return false;
}
//main 方法入口
public static void main(String[]args)
{
MyCrawler crawler = new MyCrawler();
crawler.crawling(new String[]{"http://news.twt.edu.cn/"});
}
}
Html解析工具类:HtmlParserTool
public class HtmlParserTool {
// 获取一个网站上的链接,filter 用来过滤链接
public static Set<String> extracLinks(String url, LinkFilter filter) {
Set<String> links = new HashSet<String>();
try {
Parser parser = new Parser(url);
parser.setEncoding("utf-8");
// 过滤 标签的 filter,用来提取 frame 标签里的 src 属性所表示的链接
NodeFilter frameFilter = new NodeFilter() {
public boolean accept(Node node) {
if (node.getText().startsWith("frame src=")||node.getText().contains("img ")) {
return true;
} else {
return false;
}
}
};
// OrFilter 来设置过滤 标签,和
标签
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 link = (LinkTag) tag;
String linkUrl = link.getLink();// url
if (filter.accept(linkUrl))
links.add(linkUrl);
} else// 标签
{
// 提取 frame 里 src 属性的链接如
String frame = tag.getText();
int start = frame.indexOf("src=");
if(start!=-1){
frame = frame.substring(start);
int end = frame.indexOf(" ");
String frameUrl ="";
if (end == -1)
frameUrl = frame.substring(5,frame.lastIndexOf("\""));
else
frameUrl = frame.substring(5, end - 1);
if (filter.accept(frameUrl))
links.add(frameUrl);
}
}
}
} catch (ParserException e) {
e.printStackTrace();
}
return links;
}
}
下载文件类:DownloadFile
public class DownLoadFile {
/**
* 根据 url 和网页类型生成需要保存的网页的文件名 去除掉 url 中非文件名字符
*/
public String getFileNameByUrl(String url,String contentType)
{
//remove http://
url=url.substring(7);
//text/html类型
if(contentType.indexOf("html")!=-1)
{
url= url.replaceAll("[\\?/:*|<>\"]", "_")+".html";
return url;
}
//如application/pdf类型
else
{
/*return url.replaceAll("[\\?/:*|<>\"]", "_")+"."+
contentType.substring(contentType.lastIndexOf("/")+1);*/
return url.replaceAll("[\\?/:*|<>\"]", "_");
}
}
/**
* 保存网页字节数组到本地文件 filePath 为要保存的文件的相对地址
*/
private void saveToLocal(byte[] data, String filePath) {
try {
DataOutputStream out = new DataOutputStream(new FileOutputStream(
new File(filePath)));
for (int i = 0; i < data.length; i++)
out.write(data[i]);
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/* 下载 url 指向的网页 */
public String downloadFile(String url) {
String filePath = null;
/* 1.生成 HttpClinet 对象并设置参数 */
HttpClient httpClient = new HttpClient();
// 设置 Http 连接超时 5s
httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(
5000);
/* 2.生成 GetMethod 对象并设置参数 */
//
GetMethod getMethod = new GetMethod(url);
// 设置 get 请求超时 5s
getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
// 设置请求重试处理
getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler());
/* 3.执行 HTTP GET 请求 */
try {
int statusCode = httpClient.executeMethod(getMethod);
// 判断访问的状态码
if (statusCode != HttpStatus.SC_OK) {
System.err.println("Method failed: "
+ getMethod.getStatusLine());
filePath = null;
}
/* 4.处理 HTTP 响应内容 */
byte[] responseBody = getMethod.getResponseBody();// 读取为字节数组
// 根据网页 url 生成保存时的文件名
filePath = "temp\\"
+ getFileNameByUrl(url, getMethod.getResponseHeader(
"Content-Type").getValue());
saveToLocal(responseBody, filePath);
} catch (HttpException e) {
// 发生致命的异常,可能是协议不对或者返回的内容有问题
System.out.println("Please check your provided http address!");
e.printStackTrace();
} catch (IOException e) {
// 发生网络异常
e.printStackTrace();
} finally {
// 释放连接
getMethod.releaseConnection();
}
return filePath;
}
}
使用队列数据结构:
LinkQueue
public class LinkQueue {
//已访问的 url 集合
private static Set visitedUrl = new HashSet();
//待访问的 url 集合
private static Queue unVisitedUrl = new PriorityQueue();
//获得URL队列
public static Queue getUnVisitedUrl() {
return unVisitedUrl;
}
//添加到访问过的URL队列中
public static void addVisitedUrl(String url) {
visitedUrl.add(url);
}
//移除访问过的URL
public static void removeVisitedUrl(String url) {
visitedUrl.remove(url);
}
//未访问的URL出队列
public static Object unVisitedUrlDeQueue() {
return unVisitedUrl.poll();
}
// 保证每个 url 只被访问一次
public static void addUnvisitedUrl(String url) {
if (url != null && !url.trim().equals("")
&& !visitedUrl.contains(url)
&& !unVisitedUrl.contains(url))
unVisitedUrl.add(url);
}
//获得已经访问的URL数目
public static int getVisitedUrlNum() {
return visitedUrl.size();
}
//判断未访问的URL队列中是否为空
public static boolean unVisitedUrlsEmpty() {
return unVisitedUrl.isEmpty();
}
}
Queue
public class Queue {
//使用链表实现队列
private LinkedList queue = new LinkedList();
//入队列
public void enQueue(Object t) {
queue.addLast(t);
}
//出队列
public Object deQueue() {
return queue.removeFirst();
}
//判断队列是否为空
public boolean isQueueEmpty() {
return queue.isEmpty();
}
//判断队列是否包含t
public boolean contians(Object t) {
return queue.contains(t);
}
public boolean empty() {
return queue.isEmpty();
}
}
url基础类
public class Url {
// 原始url的值,主机部分是域名
private String oriUrl;
// url的值,主机部分是IP,为了防止重复主机的出现
private String url;
//URL NUM
private int urlNo;
// 获取URL返回的结果码
private int statusCode;
// 此URL被别的文章引用的次数
private int hitNum;
// 此URL对应文章的汉字编码
private String charSet;
// 文章摘要
private String abstractText;
// 作者
private String author;
// 文章的权重(包含导向词的信息)
private int weight;
// 文章的描述
private String description;
// 文章大小
private int fileSize;
// 最后修改时间
private Timestamp lastUpdateTime;
// 过期时间
private Date timeToLive;
// 文章名称
private String title;
// 文章类型
private String type;
// 引用的链接
private String[] urlRefrences;
//爬取的层次,从种子开始,依次为第0层,第1层...
private int layer;
public String getOriUrl() {
return oriUrl;
}
public void setOriUrl(String oriUrl) {
this.oriUrl = oriUrl;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getUrlNo() {
return urlNo;
}
public void setUrlNo(int urlNo) {
this.urlNo = urlNo;
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public int getHitNum() {
return hitNum;
}
public void setHitNum(int hitNum) {
this.hitNum = hitNum;
}
public String getCharSet() {
return charSet;
}
public void setCharSet(String charSet) {
this.charSet = charSet;
}
public String getAbstractText() {
return abstractText;
}
public void setAbstractText(String abstractText) {
this.abstractText = abstractText;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getFileSize() {
return fileSize;
}
public void setFileSize(int fileSize) {
this.fileSize = fileSize;
}
public Timestamp getLastUpdateTime() {
return lastUpdateTime;
}
public void setLastUpdateTime(Timestamp lastUpdateTime) {
this.lastUpdateTime = lastUpdateTime;
}
public Date getTimeToLive() {
return timeToLive;
}
public void setTimeToLive(Date timeToLive) {
this.timeToLive = timeToLive;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String[] getUrlRefrences() {
return urlRefrences;
}
public void setUrlRefrences(String[] urlRefrences) {
this.urlRefrences = urlRefrences;
}
}
url过滤器接口类:LinkFilter
public interface LinkFilter {
public boolean accept(String url);
}
本实战内容相对简单,后续将在此爬虫基础上做多线程处理,待续!