一、使用技术
- Http协议
- 正则表达式
- 队列模式
- Lucenne中文分词
- MapReduce
二、网络爬虫
- 项目目的
通过制定url爬取界面源码,通过正则表达式匹配出其中所需的资源(这里是爬取csdn博客url及博客名),将爬到的资源存入文件中便于制作成倒排索引。根据页面源码垂直爬取csdn网站中的所有博客资源(找到一个超链接就爬取该超链接中的内容)。 - 设计思想
建立一个队列对象,首先将传入的url存入代表未爬取的队列中,循环如果未爬取队列中所有url进行爬取,并将爬取的url转移到代表已爬取的队列中。使用HttpURLConnection获得页面信息,使用正则表达式从页面信息中所需的信息输出到文件中,并将从页面信息中匹配到的超链接存入代表未爬取的队列中,实现垂直爬取数据。 - 源码及分析
a.LinkCollection.java
package com.yc.spider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 链接地址队列
* @author wrm
*当爬到一个超链接后,将其加入到队列中,接着爬这个超链接,并将这个超链接放入标示已查的队列中
*/
public class LinkCollection {
//待访问url的集合:队列
private List<String> unVisitedUrls=Collections.synchronizedList(new ArrayList<String>());
private Set<String> visitedUrls=Collections.synchronizedSet(new HashSet<String>());
/**
* 入队操作
*/
public void addUnVisitedUrl(String url){
if(url!=null&&!"".equals(url.trim())&&!visitedUrls.contains(url)&&!unVisitedUrls.contains(url)){
unVisitedUrls.add(url);
}
}
/**
* 出队
*/
public String deQueueUnVisitedUrl(){
if(unVisitedUrls.size()>0){
String url=unVisitedUrls.remove(0);
visitedUrls.add(url);
return url;
}
return null;
}
/**
* 判断队列是否为空
*/
public boolean isUnVisitedUrisEmpty(){
if(unVisitedUrls!=null&&!"".equals(unVisitedUrls)){
return false;
}else{
return true;
}
}
/**
* hadoop出队
*/
public String deQueueVisitedUrl(){
if(visitedUrls.iterator().hasNext()){
String url=visitedUrls.iterator().next();
visitedUrls.remove(0);
return url;
}
return null;
}
/**
* 判断Visited队列是否为空
*/
public boolean isVisitedUrisEmpty(){
if(visitedUrls!=null&&!"".equals(visitedUrls)){
return false;
}else{
return true;
}
}
}
该类是url的队列,该说的注释中都有
b.DownLoadTool.java
package com.yc.spider;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Scanner;
import java.util.Set;
/**
* 下载工具类
* @author wrm
*
*/
public class DownLoadTool {
/**
* 编码集
*/
private String encoding="GBK";
/**
* 下载的文件保存的位置
*/
private String savePath=System.getProperty("user.dir")+File.separator;
/**
* 自动生成保存的目录
* 目录名的命名规范:yyyyMMddHHmmss
*/
public static File createSaveDirectory(){
DateFormat df=new SimpleDateFormat("yyyyMMddHHmmss");
String directoryName=df.format(new Date());
return createSaveDirectory(directoryName);
}
/**
* 根据指定目录名
* @param directoryName
* @return
*/
public static File createSaveDirectory(String directoryName) {
File file=new File(directoryName);
if(!file.exists()){
file.mkdirs();
}
return file;
}
/**
* 下载页面的内容
*/
static String downLoadUrl(String addr){
StringBuffer sb=new StringBuffer();
try {
URL url=new URL(addr);
HttpURLConnection con=(HttpURLConnection) url.openConnection();
con.setConnectTimeout(5000);
con.connect();
//产生文件名
Random r=new Random();
try {
Thread.sleep(r.nextInt(2000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(con.getResponseCode());
System.out.println(con.getHeaderFields());
if(con.getResponseCode()==200){
BufferedInputStream bis=new BufferedInputStream(con.getInputStream());
Scanner sc=new Scanner(bis,encoding);
while(sc.hasNextLine()){ //读取拼接页面信息
sb.append(sc.nextLine());
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
}
该类使用HttpURLConnection.getInputStream()获得页面内容,其中
Random r=new Random();
try {
Thread.sleep(r.nextInt(2000));
} catch (InterruptedException e) {
e.printStackTrace();
}
是为了防止被网站识别出是爬虫在访问而进行的睡眠操作
con.getResponseCode()==200是判断访问该网页获得的状态码是否为200(成功)
如果想要获得http头的话可以使用以下代码
con.getHeaderField(name); //获得头中的name数据
con.getHeaderFields(); //获得头中的所有数据
某些网站的防爬虫做得实在太好!就算睡眠了也依旧不让你爬,这时可以冲firfox中获得头,通过该请求头方面便可骗过。
c.HtmlNodeParser.java
package com.yc.spider;
import java.util.HashSet;
import java.util.Set;
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 HtmlNodeParser {
/**
* 解析url地址中对应的页面中的a标签与frame标签
* @throws ParserException
*
*/
public Set<String> parseNode(String url,NodeFilter filter) throws ParserException{ //NodeFilter表明是否要全网爬行
Set<String> set=new HashSet<String>();
Parser parser=new Parser(url);
if(!url.startsWith("http:/")){
url="http:/"+url;
}
//这个过滤器用户过滤frame
NodeFilter framefilter=new NodeFilter(){
@Override
public boolean accept(Node node) {
if(node.getText().indexOf("frame src=")>=0){
return true;
}else{
return false;
}
}
};
//创建过滤器 LinkTag表示超链接标记
OrFilter linkFilter=new OrFilter(new NodeClassFilter(LinkTag.class),framefilter);
NodeList list=parser.extractAllNodesThatMatch(linkFilter);
for(int i=0;i<list.size();i++){
Node node=list.elementAt(i);
String linkurl=null;
if(node instanceof LinkTag){ //href
LinkTag linkTag=(LinkTag) node;
linkurl=linkTag.getLink();
}else{
//是frame节点 src
String frame=node.getText();
int start=frame.indexOf("src=");
frame=frame.substring(start);
int end=frame.indexOf(" ");
if(end==-1){
end=frame.indexOf(">");
}
linkurl=frame.substring(4,end-1);
}
if(linkurl==null||"".equals(linkurl)||(!linkurl.startsWith("http://")&&!linkurl.startsWith("https://"))){
continue;
}
if( filter!=null&&filter.accept(node)==false){
continue;
}
set.add(linkurl);
}
return set;
}
}
d.TitleDown.java
package com.yc.spider;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TitleDown {
/**
* 取html标记
*/
static String A_URL="<\\s*a\\s+([^>]*)\\s*>([^<]*)</a>";
static String HREF_URL="href\\s*=\\s*\"*(http://blog.csdn.net/?.*?/article/details/?.*?)(\"|>|\\s+)";
// static String HREF_URL="href\\s*=\\s*\"*(topic/?.*?)(\"|>|\\s+)";
// static String HREF_URL="href\\s*=\\s*\"*(http://news.sohu.com/?.*?)(\"|>|\\s+)";
static Set<String> getImageLink(String html){
System.out.println(html);
Set<String> result=new HashSet<String>();
String g1="";
//创建一个Pattern模式类,编译这个正则表达式
Pattern p=Pattern.compile(A_URL,Pattern.CASE_INSENSITIVE);
Pattern p1=Pattern.compile(HREF_URL, Pattern.CASE_INSENSITIVE);
//定义一共饿 匹配器的类
Matcher matcher=p.matcher(html);
while(matcher.find()){
g1=matcher.group(1);
Matcher m1=p1.matcher(g1);
while(m1.find()){
String word=matcher.group(2);
result.add(m1.group(1)+"\t"+word.trim().trim());
}
}
return result;
}
public static void main(String[] args) {
String addr="http://www.csdn.com";
String html=DownLoadTool.downLoadUrl(addr);
// String html="<title>根本没问题啊!</title>";
System.out.println(html);
Set<String> imagetags1=getImageLink(html);
for(String imagetag:imagetags1){
System.out.println(imagetag);
}
}
}
该类使用正则表达式来匹配我所需要的数据。
static String A_URL="<\\s*a\\s+([^>]*)\\s*>([^<]*)</a>";
用于匹配a标签和a标签中的内容
static String HREF_URL="href\\s*=\\s*\"*(http://blog.csdn.net/?.*?/article/details/?.*?)(\"|>|\\s+)";
用于匹配url,因为这里我是要csdn的博客地址,所以作此匹配
e.Spider.java
package com.yc.spider;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.htmlparser.util.ParserException;
public class Spider {
private LinkCollection lc=new LinkCollection();
private DownLoadTool dlt=new DownLoadTool();
private HtmlNodeParser hnp=new HtmlNodeParser();
public String getFileName(String url){
String filename=url.toString().substring(7);
filename=filename.replaceAll("/", "-");
filename=filename.replace(".", ",");
return filename;
}
public void crawling(String url,String directory) throws FileNotFoundException{
//1.先添加url到待取队列中
lc.addUnVisitedUrl(url);
try {
Configuration conf=new Configuration();
URI uri=new URI("hdfs://192.168.1.123:9000"); //hdfs主机uri
FileSystem hdfs=FileSystem.get(uri, conf);
//2.循环这个队列,到这个队列为空时
while(lc.isUnVisitedUrisEmpty()==false){
//3.取出待取地址
String visiturl=lc.deQueueUnVisitedUrl();
//4.下载这个页面
try {
String html=dlt.downLoadUrl(visiturl);
Set<String> allneed=TitleDown.getImageLink(html);
for (String addr : allneed) {
String a=addr.substring(addr.indexOf("\t")+1);
String filename=addr.substring(0,addr.indexOf("\t"));
filename=getFileName(filename);
System.out.println(filename);
Path p=new Path("/spider/"+filename);
FSDataOutputStream dos=hdfs.create(p);
try {
System.out.print(a);
dos.write(a.getBytes());
} catch (IOException e) {
e.printStackTrace();
}finally {
dos.close(); //这里一定要将dos关闭,不然内容无法写入
}
}
//5.从页面中分析出超链接地址,放入待取地址中
Set<String> newurl=hnp.parseNode(visiturl, null);
// dlt.createLogFile(TitleDown.getImageLink(html));
//将这些地址又加入到待取地址中
for(String s:newurl){
String httpregex="http://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?";
Pattern p2=Pattern.compile(httpregex,Pattern.CASE_INSENSITIVE);
Matcher matcher=p2.matcher(s);
while(matcher.find()){
lc.addUnVisitedUrl(s);
//boolean b=matcher.
}
}
} catch (ParserException e) {
e.printStackTrace();
}
}
} catch (IllegalArgumentException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (URISyntaxException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
因为我要将URL作为文件名,而文件名不能含有某些字符,所以用该方法进行替换
public String getFileName(String url){
String filename=url.toString().substring(7);
filename=filename.replaceAll("/", "-");
filename=filename.replace(".", ",");
return filename;
}
生成的文件
每一个文件中只有改a标签的内容(其实还可以加入该页面的头,但是这里没做这么复杂)
三、倒排索引制作
设计目的
使用MapReduce及中文分词将爬到的文件制作成倒排索引,索引文件格式为
Key(分词器分出的词)+“\t”+url1:sum;url2:sum设计思想及源码
在Map阶段获得文件名,并将文件名还原为url,作为value。将文件内容通过分词器分词后将分出的每个词作为key,输出。
源码:
public static class InvertedIndexMapper extends Mapper<Object, Text, Text, Text>{
private Text keyInfo = new Text(); // 存储单词和URI的组合
private Text valueInfo = new Text(); //存储词频
private FileSplit split; // 存储split对象。
@Override
protected void map(Object key, Text value, Mapper<Object, Text, Text, Text>.Context context)
throws IOException, InterruptedException {
//获得<key,value>对所属的FileSplit对象。
split = (FileSplit) context.getInputSplit();
Analyzer sca = new SmartChineseAnalyzer( );
TokenStream ts = sca.tokenStream("field", value.toString());
CharTermAttribute ch = ts.addAttribute(CharTermAttribute.class);
ts.reset();
while (ts.incrementToken()) {
System.out.println(ch.toString());
String url=split.getPath().toString();
url=url.substring(url.lastIndexOf("/"));
url=url.replaceAll("-", "/");
url=url.replace(",", ".");
url="http:/"+url;
System.out.println(url);
// key值由单词和URI组成。
keyInfo.set( ch.toString()+";"+url);
//词频初始为1
valueInfo.set("1");
context.write(keyInfo, valueInfo);
}
ts.end();
ts.close();
}
}
Combiner阶段:将相同key值的词频累加获得词频
public static class InvertedIndexCombiner extends Reducer<Text, Text, Text, Text>{
private Text info = new Text();
@Override
protected void reduce(Text key, Iterable<Text> values, Reducer<Text, Text, Text, Text>.Context context)
throws IOException, InterruptedException {
//统计词频
int sum = 0;
for (Text value : values) {
sum += Integer.parseInt(value.toString() );
}
int splitIndex = key.toString().indexOf(";");
//重新设置value值由URI和词频组成
info.set( key.toString().substring( splitIndex + 1) +":"+sum );
//重新设置key值为单词
key.set( key.toString().substring(0,splitIndex));
context.write(key, info);
}
}
Reducer阶段,组合出最后的数据输出
public static class InvertedIndexReducer extends Reducer<Text, Text, Text, Text>{
private Text result = new Text();
@Override
protected void reduce(Text key, Iterable<Text> values, Reducer<Text, Text, Text, Text>.Context context)
throws IOException, InterruptedException {
//生成文档列表
String fileList = new String();
for (Text value : values) {
fileList += value.toString()+";";
}
result.set(fileList);
context.write(key, result);
}
}
四、用户搜索模拟
原理:将用户数据的关键字分词后与倒排索引分别匹配,只要匹配到的在Combiner中统计词频,并在Reduce中操作后输出。
源码:
package com.yc.hadoop;
import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.mapreduce.lib.input.KeyValueTextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
public class FindWord {
public static class FindMapper extends Mapper<Text, Text, Text, Text>{
@Override
protected void map(Text key, Text value, Mapper<Text, Text, Text, Text>.Context context)
throws IOException, InterruptedException {
String text="android可行性"; //用户输入的关键字
Analyzer sca = new SmartChineseAnalyzer( );
TokenStream ts = sca.tokenStream("field", text);
CharTermAttribute ch = ts.addAttribute(CharTermAttribute.class);
ts.reset();
while (ts.incrementToken()) {
if(ch.toString().equals(key.toString())||ch.toString().equals(key.toString())){
System.out.println(value.toString());
String[] urls=value.toString().split(";");
int count=0;
for (String url : urls) {
String oneurl=url.split(":")[0]+url.split(":")[1];
count=Integer.parseInt(url.split(":")[2]);
String newvalue=ch.toString()+";"+count;
System.out.println(">>>>>>>>"+oneurl+">>>>>>>>>>"+newvalue);
context.write(new Text(oneurl),new Text( newvalue));
}
}
}
ts.end();
ts.close();
}
}
public static class FindCombiner extends Reducer<Text, Text, Text, Text>{
@Override
protected void reduce(Text key, Iterable<Text> values, Reducer<Text, Text, Text, Text>.Context context)
throws IOException, InterruptedException {
//统计词频
int sum = 0;
for (Text value : values) {
String count=value.toString().split(";")[1];
sum += Integer.parseInt(count );
}
context.write(new Text(String.valueOf(sum)),new Text(key.toString()) );
}
}
public static class FindReducer extends Reducer<Text, Text, Text, Text>{
@Override
protected void reduce(Text key, Iterable<Text> values, Reducer<Text, Text, Text, Text>.Context context)
throws IOException, InterruptedException {
//生成文档列表
for (Text text : values) {
context.write(key, text);
}
}
}
public static void main(String[] args) {
try {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf,"InvertedIndex");
job.setJarByClass(InvertedIndex.class);
//实现map函数,根据输入的<key,value>对生成中间结果。
job.setMapperClass(FindMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
job.setInputFormatClass(KeyValueTextInputFormat.class);
job.setCombinerClass(FindCombiner.class);
job.setReducerClass(FindReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
FileInputFormat.addInputPath(job, new Path("hdfs://192.168.1.123:9000/spiderout/1462887403514/part-r-00000"));
FileOutputFormat.setOutputPath(job, new Path("hdfs://192.168.1.123:9000/1"));
System.exit(job.waitForCompletion(true) ? 0 : 1);
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果展示: