1. 程式人生 > >Hadoop學習之自己動手做搜尋引擎【網路爬蟲+倒排索引+中文分詞】

Hadoop學習之自己動手做搜尋引擎【網路爬蟲+倒排索引+中文分詞】

一、使用技術

  • Http協議
  • 正則表示式
  • 佇列模式
  • Lucenne中文分詞
  • MapReduce

二、網路爬蟲

  1. 專案目的
    通過制定url爬取介面原始碼,通過正則表示式匹配出其中所需的資源(這裡是爬取csdn部落格url及部落格名),將爬到的資源存入檔案中便於製作成倒排索引。根據頁面原始碼垂直爬取csdn網站中的所有部落格資源(找到一個超連結就爬取該超連結中的內容)。
  2. 設計思想
    建立一個佇列物件,首先將傳入的url存入代表未爬取的佇列中,迴圈如果未爬取佇列中所有url進行爬取,並將爬取的url轉移到代表已爬取的佇列中。使用HttpURLConnection獲得頁面資訊,使用正則表示式從頁面資訊中所需的資訊輸出到檔案中,並將從頁面資訊中匹配到的超連結存入代表未爬取的佇列中,實現垂直爬取資料。
  3. 原始碼及分析
    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標籤的內容(其實還可以加入該頁面的頭,但是這裡沒做這麼複雜)

三、倒排索引製作

  1. 設計目的
    使用MapReduce及中文分詞將爬到的檔案製作成倒排索引,索引檔案格式為
    Key(分詞器分出的詞)+“\t”+url1:sum;url2:sum

  2. 設計思想及原始碼
    在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();
            }
        }
}

結果展示:
測試