搜索引擎项目构建与解析(一)

这是源码,大家可以下载下来作为参考,一起食用效果更佳:SearchEngine · 王宇璇/submit - 码云 - 开源中国 (gitee.com)icon-default.png?t=N7T8https://gitee.com/yxuan-wang/submit/tree/master/SearchEngine搜索引擎项目总体来看内容比较简单,代码量也比较少,是一个易上手的项目。

建立这个项目的初衷就是个人认为搜索引擎在生活中比较实用的一个项目,可以依据这个简单的项目了解搜索引擎的大致工作原理,为以后处理学习或者工作中的项目提供一个最基本的流程模板做参考。

因为我们只是简单的建立一个小的项目来了解流程(像大型搜索应用实现起来还是很困难)建立的搜索引擎项目就是相当于一个站内搜索,比如说购物应用中搜索商品等。我们针对的搜索内容就是java文档。

我们首先需要准备好java文档

Java 下载 |神谕 (oracle.com)icon-default.png?t=N7T8https://www.oracle.com/java/technologies/downloads/

下载这个压缩包进行解压得到jdk  针对api文件夹中所有的html的文档构建搜索引擎,通过关键字搜索文档中的内容。

新建项目:

构建系统选择Maven,选好jdk版本。创建项目。

对于一个简单的浏览器,最重要的就是实现索引功能。从其功能考虑我们的项目如何创建:

用户输入一个词语或者一个句子,我们就要把和这个词语有关的所有文档全部展现出来。

分词

首先就是进行分词,我们搜索针对的是每一个词语,对于中文的分词比较困难,需要根据大数据来调查用语习惯,比如“但但丁丁真真是三个人,但但丁丁真真是两个人”诸如此类分词就很难搞,而英文就简单的多,“how are you?”我们只需要在空格处进行分词即可。此处我们使用ansj提供的ToAnalysis.parse方法进行分词。不要忘记引入依赖,刷新!!!

<!-- https://mvnrepository.com/artifact/org.ansj/ansj_seg -->
        <dependency>
            <groupId>org.ansj</groupId>
            <artifactId>ansj_seg</artifactId>
            <version>5.1.6</version>
        </dependency>

正排索引和倒排索引

搜索就需要考虑到正排索引和倒排索引。

举例说明,例如一个学校中,正排索引就是通过这个学生的id找到这个学生然后就可以得到这个学生的信息。倒排索引相当于这个学生违纪被校领导发现,但是他逃走了。校领导不知道他是谁,他的学生id,此时就通过他的长相,行动地点,等一系列特征进行索引,最终得到他的学生id。这个就是倒排索引。而找到这个人全部信息就再通过正排索引输入id进行查询

正排索引很简单,就是一个数组,我们通过文档在数组中的位置(也可以看作id)就可以查到这个文档。而倒排索引就是一个hash表,将所有文章中的所有内容进行解析分词,然后每个词语对应一个数组(储存的是含有这个词语的所有文章的id),然后再把这些词语作为key,将数组作为value储存于hashmap中去。

当用户进行查询,先进行分词,分出的词语作为key在hashmap中进行匹配,得到对应id数组,此数组中的所有id文章都包含此关键词。将数组中的id通过正排索引对应成文章进行展示就是这个项目的基本思路。

我们需要读取这个api文件夹中所有的html文件的内容,然后解析出其标题,正文,url

我们可以看到浏览器的大部分搜索结果都是由这三部分组成的。

代码实现:

1.先将所有的HTML文件读取出来保存到一个文件的数组当中去。

其中的enumFile方法就是将此路径下所有的文件加入到fileList数组当中去。

这是实现代码,主要的就是如果是directory就向下递归,如果是文件就进行判断,是html文件就放到数组当中去,如果不是就跳过不操作。这里判断是否是html用的是endWith判断文件扩展名。

将文件放置于数组中方便于接下来的解析操作。

2.对文件进行解析

主要是解析出标题,内容以及url。

private void parseHTML(File file) {
        //一条搜索结果:标题 , 描述 , url
        //1.解析标题
        String title = parseTitle(file);
        //2.解析出url
        String url = parseUrl(file);
        //3.解析出正文
        //String content = parseContent(file);
        String content = parseContentByRegex(file);
        //将解析后的内容添加到Index的数组当中去。
        index.addDoc(title , url , content);
    }

    private String parseTitle(File file) {
        return file.getName().substring(0,file.getName().lastIndexOf("."));
        //substring截取字符串,也可以使用f.getName().length() - ".html".length();
    }

    private String parseUrl(File file) {
        String part1 = file.getAbsolutePath().substring(INPUT_PATH.length());//后面的部分
        String part2 = "https://docs.oracle.com/javase/8/docs/api/";//前缀
        String url = part2 + part1;//此时的url中既包含正斜杠也包含反斜杠
        //替换也可以,不替换也可以,浏览器都可以识别。
        return url;
    }

    public String parseContent(File file) {
        //按字符读取文件。按字符而不是按字节。
        //java标准库中既提供了字符读取类 FileReader,也提供了字节读取类  FileInputStream
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file),1024 * 1024)){
            //拷贝开关
            boolean isCopy = true;
            //保存读取结果的容器
            StringBuilder content = new StringBuilder();
            while(true){
                int ret = bufferedReader.read();//此处的read返回值为int表示一些非法情况,读到末尾没有数据返回-1
                if(ret == -1){
                    //文件读完了
                    break;
                }
                //如果ret不是-1,此时的返回结果就是正常的字符了。
                char c = (char) ret;
                if(isCopy){
                    //开关是打开的状态,普通字符就应该拷贝到stringBuilder中
                    if(c == '<'){
                        isCopy = false;
                        continue;
                    }
                    //文档中的换行太多,不美观,可以换成空格
                    if(c == '\n'||c == '\r'){
                        c = ' ';
                    }
                    content.append(c);
                }else{
                    if(c == '>'){
                        isCopy = true;
                    }
                }
            }
            //fileReader.close();
            return content.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public String readFile(File file){
        try(BufferedReader bufferedReader = new BufferedReader(new FileReader(file) , 1024 * 1024)){
            StringBuilder content = new StringBuilder();
            while(true){
                int ret = bufferedReader.read();
                if(ret == -1){
                    break;
                }
                char c = (char) ret;
                if(c == '\n'||c == '\r'){
                    c = ' ';
                }
                content.append(c);
            }
            return content.toString();
        }catch (IOException e){
            e.printStackTrace();
        }
        return null;
    }

    //正则表达式解析内容
    public String parseContentByRegex(File file) {
        //1.先把整个文件读到String里面
        String content = readFile(file);
        //2.替换掉script标签
        content = content.replaceAll("<script.*?>(.*?)</script>", " ");
        //3.不要忘记其他普通Html标签
        content = content.replaceAll("<.*?>", " ");
        //4.替换空格符,多个替换为一个
        //+需要替换的字符必须出现一次及以上才能够替换,*则是不需要出现此内容就可以进行替换
        content = content.replaceAll("\\s+" , " ");
        return content;
        //第三步和第二步顺序不能调换,如果调换第三步会直接用空格替换两个script标签,此时再进行第二部没有意义了,标签中的内容就无法替换了。

    }

1.解析标题

文件名出去文件扩展名就是标题,所以直接截取文件名到".",此时得到标题。

2.解析url

url就是我们在网站中需要跳转到这篇文档的网址。举个例子,

ArrayList文档在此电脑中的位置:D:\beginning\homework\project\jdk-8u411-docs-all\docs\api\java\util\ArrayList.html

这是我们可以得到的。

而他在网页中的位置:https://docs.oracle.com/javase/8/docs/api//java/util/ArrayList.html

而用户访问只能在网页中访问,此时我们就需要用此电脑中的位置解析出其url

根据规律,我们可以发现这个url就是固定前缀:https://docs.oracle.com/javase/8/再加上在此电脑中的docs\api\java\util\ArrayList.html此时就能用我们在此电脑中文件的位置得到其url,可以在网络中进行访问。

3.解析内容

首先我们用记事本打开文件看文件的内容,发现HTML用<>作为标签的格式,而标签中的内容是设定字体,位置等一系列的特点,我们读取内容是不需要的。所以我们需要将里面的内容排除。

1.parseContent

根据以上内容得出第一种方法,用BufferedRead来作为输入流对象读取文件中的字符,如果返回-1,就是读取完毕,其他数字就是读取成功,此时我们就要进行筛选,如果读取到“<”说明就是标签中的内容,此时停止读取。如果读取到">"就说明读取的是页面显示内容,继续读取。

之后将解析出的内容进行规整,发现有太多换行看起来非常丑,再去掉换行。

2.parseContentByRegex

之后我们把解析出的文档打印出来发现其中包含了<Script>content</Script>标签,其中的内容于与正文是无关的。但是我们第一个方法就会保留这些content,这是不合理的。

优化解析逻辑,此时我们需要将此标签中的内容也替换掉,就需要用到正则表达式。

 此时用<.*?>表示匹配所有字符,且是非贪婪匹配,非贪婪匹配与贪婪匹配的区别:

举例: 

源字符串:aa<div>test1</div>bb<div>test2</div>cc 

正则表达式一:<div>.*</div> 

匹配结果一:<div>test1</div>bb<div>test2</div> 

正则表达式二:<div>.*?</div> 

匹配结果二:<div>test1</div>(这里指的是一次匹配结果,所以没包括<div>test2</div>) 

引用于:https://www.cnblogs.com/admans/p/11955614.html

也就是说贪婪匹配会尽可能多的匹配,此时很可能将我们需要的内容匹配到,所以用非贪婪匹配。

匹配的内容就包括:1.标签中的内容替换为空格 2.标签替换为空格

3.此时就会造成空格过多不美观,将多个空格替换为一个空格。

注意:1和2不能够调换,也就是说如果先匹配了标签,那么第一步就无法识别标签中的内容替换也就无从说起了。

多线程

至此我们的解析就已经完成,但是在之后执行代码进行解析的过程中我发现解析的速度很慢,所以想到了多线程进行优化,而既然使用多线程,多线程后续写代码中就不可避免的会提高难度,因为要考虑到锁的问题。

创建线程池,给每一个线程都分配解析的任务,这里需要注意的是我使用了

CountDownLatch latch = new CountDownLatch(files.size());

来来保存文件的数量并且计数,每解析一个文件latch.countDown();就会自动减一,然后用await方法进行阻塞,到所有文件都被解析之后再向下进行。

 public void runByThread(){
        System.out.println("索引制作开始。");
        long start = System.currentTimeMillis();
        ArrayList<File> files = new ArrayList<>();
        enumFile(INPUT_PATH , files);
        //多线程完成循环遍历,此处为了能够通过多线程制作索引,就直接引入线程池
        CountDownLatch latch = new CountDownLatch(files.size());
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for(File file: files){
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("解析"+file.getAbsolutePath());
                    parseHTML(file);
                    latch.countDown();
                }
            });
        }
        try {
            latch.await(60 , TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        executorService.shutdown();
        index.save();
        long end = System.currentTimeMillis();
        System.out.println("索引制作完毕"+(end-start)+"ms");
    }

解析过程就介绍到这里,后续的内容点击头像进入空间进行查看。

感谢大家的支持!!!!!

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值