基于正倒排索引的Java文档搜索引擎1-实现索引模块-实现Parser类

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • [1. 项目前置](#1. 项目前置)
    • [1.1 搜索的核⼼思路:](#1.1 搜索的核⼼思路:)
    • [1.2 倒排索引](#1.2 倒排索引)
    • [1.3 项⽬目标](#1.3 项⽬目标)
    • [1.4 核⼼流程](#1.4 核⼼流程)
    • [1.5 创建项目](#1.5 创建项目)
    • [1.6 关于分词](#1.6 关于分词)
  • [2. 实现索引模块](#2. 实现索引模块)
  • [3. 实现Parser类](#3. 实现Parser类)
    • [3.1 排除非html文件](#3.1 排除非html文件)
    • [3.2 解析html文件](#3.2 解析html文件)
    • [3.3 解析标题](#3.3 解析标题)
    • [3.4 解析URL](#3.4 解析URL)
    • [3.5 解析正文](#3.5 解析正文)
  • 总结

前言

1. 项目前置

这个就是搜索引擎

搜索引擎的本质

输⼊⼀个查询词, 得到若⼲个搜索结果. 每个搜索结果包含了标题, 描述, 展⽰ url 和点击 url.

1.1 搜索的核⼼思路:

先获取很多网页,在根据用户输入,在网页中查找

页面怎么来?-----》爬虫

怎么去在网页中匹配查询词呢

⽅案⼀ -- 暴⼒搜索

每次处理搜索请求的时候, 拿着查询词去所有的⽹⻚中搜索⼀遍, 检查每个⽹⻚是否包含查询词字符串.

这个⽅法是否可⾏?

显然, 这个⽅案的开销⾮常⼤. 并且随着⽂档数量的增多, 这样的开销会线性增⻓. ⽽搜索引擎往往对于

效率的要求⾮常⾼

1.2 倒排索引

⽅案⼆ -- 倒排索引

这是⼀种专⻔针对搜索引擎场景⽽设计的数据结构.

⽂档(doc): 被检索的html⻚⾯(经过预处理),每个待搜索的网页

正排索引: "⼀个⽂档包含了哪些词". 描述⼀个⽂档的基本信息, 包括⽂档标题, ⽂档正⽂, ⽂档标题和正⽂的分词,指的是文档id====》文档内容

倒排索引: "⼀个词被哪些⽂档引⽤了". 描述了⼀个词的基本信息, 包括这个词都被哪些⽂档引⽤, 这个词在该⽂档中的重要程度, 以及这个词的出现位置等.指的是词====》文档id

1.3 项⽬目标

实现⼀个 Java API ⽂档的简单的搜索引擎.

百度,搜狗,bing这种搜索引擎就是全站搜索,就是搜索整个互联网上所有的网站

站内搜索:只针对某个网站内部的内容进行搜索,我们实现这个

Java API ⽂档线上版本参⻅ https://docs.oracle.com/javase/8/docs/api/index.html

但是这个页面并没有搜索款---》不好寻找,我们就针对这个来实现搜索

先把相关的网页文档或网页获取到----》可以通过爬虫来获取,爬虫就是一个http客户端(和浏览器类似),这个爬虫就是自动化,批量化获取很多页面,下载到到本地,然后进行搜索,浏览器是我们手动获取页面

只要这个语言可以访问网络,就能实现爬虫

爬虫是获取一个网站页面的一种通用手段

针对java文档--》可以直接下载官网的下载文档

每个网站都会提供一个robots.txt这个文件,这个文件就会告诉你哪些内容可以爬虫,爬虫其他地方都是非法的

下载版本参⻅ https://www.oracle.com/technetwork/java/javase/downloads/index.html

点击java8

直接下载就可以了

进入\docs\api\java\util

点击上面的html文件就可以跳转了

这个和在线文档里面的内容是一致的,只不过我们本地打开的这个是离线的,和爬虫获取的是一样的

这个是在线文档的连接

https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html

file:///C:/Users/zjdsx/Desktop/docs/api/java/util/Arrays.html

我们发现后面的路径都是一样的,所以我们可以在本地基于离线文档制作索引,实现搜索,当用户点击搜索结果页点击具体的搜索结果的时候,就可以自动跳转到在线文档的页面

1.4 核⼼流程

  1. 索引模块: 扫描下载到的⽂档, 分析数据内容构建正排+倒排索引, 并把索引内容保存到⽂件中.
    加载制作好的索引,并提供一些api实现正排和查倒排这样的功能
  2. 搜索模块: 加载索引. 根据输⼊的查询词, 基于正排+倒排索引进⾏检索, 得到检索结果.
    输入:用户查询词
    输出:很多条记录,每个记录都有标题,描述,展示url,并且能够点击跳转
  3. web模块: 编写⼀个简单的⻚⾯, 展⽰搜索结果. 点击其中的搜索结果能跳转到对应的 Java API ⽂档⻚⾯.实现一个简单的web程序,包含前端和后端

1.5 创建项目

创建maven项目java_doc_sercher

jdk选择1.8

创建这个目录webapp/WEB-INF/和文件web.xml和java同级

web.xml里面内容为

sql 复制代码
<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>
</web-app>

1.6 关于分词

用户输入的查询词,不一定是一个词,还可以是一句话

输入一句话的时候,就是把这句话分成多个词来进行搜索

所以要先对查询词来进行分词

分词就是把一个完整的句子切分为多个词:一个,人,感觉,好,孤单

分词原理:

1.基于词库

把所有得到词都进行穷举,把穷举结果放在词典文件中,然后就可以一次取句子里面的内容了,比如每隔一个字在词典里面查一下,看是不是词,或者每隔两个字取词典查一下,看是不是词,但是词是一直在生成的,词典是不够的

2.基于统计

收集很多很多语料库,===》进行人工标注/直接统计/人智能统计--》也就知道了哪些字在一起的概率比较大

两种方法要相互配合在一起使用

怎么用代码来分词呢

分词是搜索中的⼀个核⼼操作. 尤其是中⽂分词, ⽐较复杂

我们可以使⽤现成的分词库 ansj.

官⽹ 站: https://github.com/NLPchina/ansj_seg

使⽤的简单⽰例: https://blog.csdn.net/weixin_44112790/article/details/86756741

java 复制代码
        <dependency>
            <groupId>org.ansj</groupId>
            <artifactId>ansj_seg</artifactId>
            <version>5.1.6</version>
        </dependency>
java 复制代码
        String str = "小明毕业于清华大学,然后去蓝翔技校和新东方深造";
        ToAnalysis toAnalysis = new ToAnalysis();
        List<Term> terms = toAnalysis.parse(str).getTerms();//因为toAnalysis.parse(str)返回的类是第三方的类,所有getTerms
        //term就表示一个分词结果
        for (Term term : terms) {
            System.out.println(term.getName());//获取分词结果里面的词
        }

这些警告是因为,我们的ansj在分词的时候要加载一些词典文件----》加快分词速度和效果,但是没有词典也是可以分词的

还可以针对英文来分词

java 复制代码
        String str = "i have a stream";
        ToAnalysis toAnalysis = new ToAnalysis();
        List<Term> terms = toAnalysis.parse(str).getTerms();//因为toAnalysis.parse(str)返回的类是第三方的类,所有getTerms
        //term就表示一个分词结果
        for (Term term : terms) {
            System.out.println(term.getName());//获取分词结果里面的词
        }

空格也被分出来了,大写的I也会被分为小写i

2. 实现索引模块

3. 实现Parser类

java 复制代码
public class Parser {
    //指定加载文档的路径'
    private static final String INPUT_PATH = "C:/Users/zjdsx/Desktop/docs/api";

    public  void run(){
        //入口
        //1.根据上面指定路径,枚举出所有的文件(html),包括所有的子目录
        ArrayList<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH,fileList);
        System.out.println(fileList.size());
        //2. 针对上面罗列出的文件路径,打开文件,读取文件内容,解析,构建索引
        //3.在内存中构造好的索引数据结果,保存到指定的文件中
    }

    private void enumFile(String inputPath, ArrayList<File> fileList) {
        File rootPath = new File(inputPath);
        File[] files = rootPath.listFiles();//listFiles获取到当前目录下的所有文件和文件夹,只有这一级目录的
        for (File file : files) {
            //根据f的类型,决定是否要递归,如果file是一个普通文件,加入fileList数组,如果是一个目录,就递归调用
            if (file.isDirectory()) {
                //是一个目录
                enumFile(file.getAbsolutePath(), fileList);
            }else {
                fileList.add(file);
            }
        }
    }

    public static void main(String[] args) {
        Parser p = new Parser();
        p.run();
    }
}

发现有些文件还不是html

3.1 排除非html文件

根据后缀名(扩展名)来判断就可以了---》可以打开扩展名

java 复制代码
    private void enumFile(String inputPath, ArrayList<File> fileList) {
        File rootPath = new File(inputPath);
        File[] files = rootPath.listFiles();//listFiles获取到当前目录下的所有文件和文件夹,只有这一级目录的
        for (File file : files) {
            //根据f的类型,决定是否要递归,如果file是一个普通文件,加入fileList数组,如果是一个目录,就递归调用
            if (file.isDirectory()) {
                //是一个目录
                enumFile(file.getAbsolutePath(), fileList);
            }else {
                if (file.getAbsolutePath().endsWith(".html")) {
                    fileList.add(file);
                }
            }
        }
    }

这里可以自动换行,就不是一行展示,看不完了

发现少了不少了

3.2 解析html文件

一条搜索结果又标题,描述,展示url,这些都来自于html----》我们就来提取每个html的标题。描述,url

描述是正文的一段摘要

先搞正文,标题,url

java 复制代码
    private void parseHTML(File file) {
        //1.解析出标题
        String title  = parseTitle(file);
        //2.解析出html对应url
        String url  = parseUrl(file);
        //3.解析出html正文
        String content = parseContent(file);
    }

3.3 解析标题

html中的title标签就是标题

其实标题就是文件名,一模一样的

java 复制代码
    private String parseTitle(File file) {
        String title = file.getName().substring(0,file.getName().length()-".html".length());//getName得到ArrayList.html,file.getAbsolutePath()是得到所有全路径
        //标题不需要html这个后缀,,,ArrayList.html,,,,,,,点的位置就是总长度减去5就是9,substring是左闭右开
        return title;
    }

求长度

针对数组: .length属性

针对字符串: .length()方法

针对LIst等集合: .size()方法

3.4 解析URL

真实的搜索引擎中展示url和跳转url不是同一个(可以右键复制一下看看),我们这里就不区分了

我们就使用一个URL了就可以了

对于搜狗来说,广告结果的可以根据点击计费

自然搜索结果(不带广告):根据点击来优化用户体验,看这个是不是用户需要的

JavaAPI文档存在两份,一个是本地的,一个是线上的文档

我们可以以https://docs.oracle.com/javase/8/docs/api/作为前缀+本地的相对的路径,就可以拼接出URL了

java 复制代码
    private String parseUrl(File file) {
        //先获取一个固定的前缀
        String part1 = "https://docs.oracle.com/javase/8/docs/api/";
//        file.getAbsolutePath().substring(INPUT_PATH.length());//这个就是截取INPUT_PATH.length()下标到最后
        String part2 = file.getAbsolutePath().substring(INPUT_PATH.length());
        String result = part1 + part2;
        System.out.println(result);
        return result;
    }

但是这样不太好,因为斜杠错乱了

但是浏览器是可以识别https://docs.oracle.com/javase/8/docs/api/\\overview-frame.html的

主流浏览器,是可以容错这种斜杠的,这是容错性,也就是鲁棒性

3.5 解析正文

一个完整的htnl文件===html标签+内容

所以我们去掉html标签就可以了

我们可以通过正则表达式来去标签----》字符串匹配处理的常见手段

看某个字符串是否符合这些特征

去除html标签,可以用正则表达式---》但是比较麻烦

html标签都是非常有特点的----》左右尖括号的形式

一次读取,遇到左尖括号,那么从左尖括号开始,到右尖括号的内容都不放在字符串中,如果遇到的不是左尖括号------》拷贝到结果中,定义一个开关就可以了

html内容中是不会存在左右尖括号的

html内容要求&lt代替<,使用&gt代替>

java 复制代码
    private String parseContent(File file) {
        //先按照一个字符一个字符来读取,字符流
        try(FileReader fileReader = new FileReader(file)) {
            //加上一个是否要进行拷贝的开关
            boolean isCopy= true;
            StringBuilder content = new StringBuilder();
            while (true){
                int read = fileReader.read();//一次读取一个字符,返回值是一个int
                if (read == -1){
                    break;//表示文件读完了
                }
                char c = (char) read;
                if (isCopy){
                    if(c=='<'){
                        isCopy=false;
                        continue;
                    }
                    content.append(c);
                }else if (c=='>'){
                    isCopy=true;
                }
            }
            return content.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

但是这里有很多的换行,很多空换行

所以还要去掉空换行

java 复制代码
    private String parseContent(File file) {
        //先按照一个字符一个字符来读取,字符流
        try(FileReader fileReader = new FileReader(file)) {
            //加上一个是否要进行拷贝的开关
            boolean isCopy= true;
            StringBuilder content = new StringBuilder();
            while (true){
                int read = fileReader.read();//一次读取一个字符,返回值是一个int
                if (read == -1){
                    break;//表示文件读完了
                }
                char c = (char) read;
                if (isCopy){
                    if(c=='<'){
                        isCopy=false;
                        continue;
                    }
                    if(c=='\n' || c == '\r'){//\r表示回车符
                        c=' ';
                    }
                    content.append(c);
                }else if (c=='>'){
                    isCopy=true;
                }
            }
            return content.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

这样就成功了

java 复制代码
public class Parser {
    //指定加载文档的路径'
    private static final String INPUT_PATH = "C:/Users/zjdsx/Desktop/docs/api";

    public  void run(){
        //入口
        //1.根据上面指定路径,枚举出所有的文件(html),包括所有的子目录
        ArrayList<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH,fileList);
//        System.out.println(fileList);
//        System.out.println(fileList.size());
        //2. 针对上面罗列出的文件路径,打开文件,读取文件内容,解析,构建索引
        for (File file : fileList) {
            System.out.println("开始解析:"+file.getAbsolutePath());
            parseHTML(file);//解析html文件
        }
        //3.在内存中构造好的索引数据结果,保存到指定的文件中
    }

    private void parseHTML(File file) {
        //1.解析出标题
        String title  = parseTitle(file);
        System.out.println(title);
        //2.解析出html对应url
        String url  = parseUrl(file);
        //3.解析出html正文
        String content = parseContent(file);
        System.out.println(content);
    }

    private String parseContent(File file) {
        //先按照一个字符一个字符来读取,字符流
        try(FileReader fileReader = new FileReader(file)) {
            //加上一个是否要进行拷贝的开关
            boolean isCopy= true;
            StringBuilder content = new StringBuilder();
            while (true){
                int read = fileReader.read();//一次读取一个字符,返回值是一个int
                if (read == -1){
                    break;//表示文件读完了
                }
                char c = (char) read;
                if (isCopy){
                    if(c=='<'){
                        isCopy=false;
                        continue;
                    }
                    if(c=='\n' || c == '\r'){//\r表示回车符
                        c=' ';
                    }
                    content.append(c);
                }else if (c=='>'){
                    isCopy=true;
                }
            }
            return content.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private String parseUrl(File file) {
        //先获取一个固定的前缀
        String part1 = "https://docs.oracle.com/javase/8/docs/api/";
//        file.getAbsolutePath().substring(INPUT_PATH.length());//这个就是截取INPUT_PATH.length()下标到最后
        String part2 = file.getAbsolutePath().substring(INPUT_PATH.length());
        String result = part1 + part2;
//        System.out.println(result);
        return result;
    }

    private String parseTitle(File file) {
        String title = file.getName().substring(0,file.getName().length()-".html".length());//getName得到ArrayList.html,file.getAbsolutePath()是得到所有全路径
        //标题不需要html这个后缀,,,ArrayList.html,,,,,,,点的位置就是总长度减去5就是9,substring是左闭右开
        return title;
    }

    private void enumFile(String inputPath, ArrayList<File> fileList) {
        File rootPath = new File(inputPath);
        File[] files = rootPath.listFiles();//listFiles获取到当前目录下的所有文件和文件夹,只有这一级目录的
        for (File file : files) {
            //根据f的类型,决定是否要递归,如果file是一个普通文件,加入fileList数组,如果是一个目录,就递归调用
            if (file.isDirectory()) {
                //是一个目录
                enumFile(file.getAbsolutePath(), fileList);
            }else {
                if (file.getAbsolutePath().endsWith(".html")) {
                    fileList.add(file);
                }
            }
        }
    }

    public static void main(String[] args) {
        Parser p = new Parser();
        p.run();
    }
}

但是还没有把解析出来的内容放入索引中

总结

相关推荐
q***01652 小时前
Python爬虫完整代码拿走不谢
开发语言·爬虫·python
vx_bscxy3222 小时前
告别毕设焦虑!Python 爬虫 + Java 系统 + 数据大屏,含详细开发文档 基于web的图书管理系统74010 (上万套实战教程,赠送源码)
java·前端·课程设计
顺心而行...2 小时前
一些问题记录
开发语言
u***j3242 小时前
JavaScript在Node.js中的进程管理
开发语言·javascript·node.js
字节拾光录2 小时前
Java工具库三足鼎立:Hutool、Apache Commons、Guava深度测评与场景化选型指南
java·apache·guava
爱学习的小可爱卢3 小时前
Java UDP编程实战:UDP数据报套接字编程DatagramPacket、DatagramSocket 、InetSocketAddress
java·udp·udp数据报
未来之窗软件服务3 小时前
幽冥大陆(三十五)S18酒店门锁SDK go语言——东方仙盟筑基期
java·前端·golang·智能门锁·仙盟创梦ide·东方仙盟·东方仙盟sdk
r***93483 小时前
【Redis】在Java中以及Spring环境下操作Redis
java·redis·spring
沐知全栈开发3 小时前
前端控制器模式
开发语言