【文档搜索引擎】实现索引构建——解析标题、解析URL、解析正文

文章目录

实现索引构建

  • 一条搜索信息,就包含了标题、描述、展示 URL。这些信息就来自于要解析的 HTML

  • 因此当前的解析 HTML 操作,就是要把这个 HTML 文件的标题、描述、URL 给获取到

    • 描述可以视为是正文的一段摘要
    • 因此要想得到描述,就得先得到整个正文
    • 所以我们先解析正文,后面再说描述

要实现这个功能,基本的框架为:

java 复制代码
public void run(){  
	// 2. 针对上面罗列出的文件路径,打开路径,读取文件内容,进行解析,并构建索引 
	for(File f : fileList) {  
		System.out.println("开始解析: "+ f.getAbsolutePath());  
		// 通过这个方法来解析单个 HTML 文件  
		parseHTML(f);  
    }  
}
  • 实现这个功能,我们封装一个 parseHTML() 方法。此方法需要完成:
    1. 解析出 HTML 的标题
    2. 解析出 HTML 对应的 URL
    3. 解析出 HTML 对应的正文(有了正文才有后续的描述)
java 复制代码
private void parseHTML(File f) {  
    // 1. 解析出 HTML 的标题  
    String title = parseTitle(f);  
    // 2. 解析出 HTML 对应的 URL    
    String url = parseUrl(f);  
    // 3. 解析出 HTML 对应的正文(有了正文才有后续的描述)  
    String content = parseContent(f);  
}
  • 由于代码比较复杂,我们将三个任务都分给不同的方法进行完成

整个过程为:

  1. 为了解析 HTML ,我们创建一个 parseHTML 方法
  2. 解析 HTML 之后,我们发现还要:
  3. 解析标题,我们又创建了一个 parseTitle 方法
  4. 解析 URL,我们又创建了一个 parseUrl 方法
  5. 解析正文,我们又创建了一个 parseContent 方法

解析标题

我们可以通过获取文件名,来获取具具体的标题信息

java 复制代码
private String parseTitle(File f) {  
    f.getName();  
}

getName () 和 getAbsolutePath () 的区别

我们可以写个代码测试一下:

java 复制代码
public class TestGetName {  
    public static void main(String[] args) {  
        File f = new File("D:\\My Computer\\02_Stricky\\02_Code\\01 比特Java班资料\\Java docs\\api\\java\\util\\ArrayList.html");  
        System.out.println(f.getAbsolutePath());  
        System.out.println(f.getName());  
    }  
}
/**
D:\My Computer\02_Stricky\02_Code\01 比特Java班资料\Java docs\api\java\util\ArrayList.html
ArrayList.html
*/
  • getAbsolutePath 得到的是完整路径
  • getName 得到的是完整路径最后的一截

截掉 .html

搜索结果的标题里面,是展示一个 ArrayList.html 好,还是展示 ArrayList 好?

  • 展示后者更好
  • 大家都是 html,加上也没什么意义
  • 各大搜索引擎里面的标题里面也没有 .html

所以我们就需要把当前得到的字符串进行截取,去掉后面的 .html 部分

  • 这里我们使用 substring() 方法

substring() 方法的两种版本

  1. 只传一个参数
  • begin 开始截取,一直到结尾
  1. 传两个参数
  • begin 开始截取,到 end 停止
  • 前闭后开

ArrayList.html

  • 总长度:14
  • .html 长度:5
    . 这个位置的下标,就是总长度 - ".html" 的长度
  • 总长度 - 后半部分的长度 ==> 前半部分的长度 ==> 正是后半部分开始的第一个字符的下标
java 复制代码
f.getName().substring(0, f.getName().length() - ".html".length())
  • .html 虽然是字符串常量,但是他同样也是一个 String 类型,所以可以用 .length 求长度

Java 中的计算长度,有多种不同的风格:

  • 针对数组:.length 属性
  • 针对字符串:.length() 方法
  • 针对 List 等集合:.size() 方法

完整代码逻辑

java 复制代码
private String parseTitle(File f) {  
    String name = f.getName();  
    return name.substring(0, name.length() - ".html".length());  
}
  • 这样就可以直接通过文件名,获取到标题信息

解析 URL

  • 在真实的搜索引擎中,展示 URL 和跳转 URL 是不同的 URL。但是我们当前情况就可以按照一个 URL 来处理

    • 使用一个 URL,既作为展示 URL,也作为点击 URL

对于各大搜索引擎来说:

  1. 广告结果的话,需要根据点击计费
  2. 自然点击结果的话,需要根据点击来优化用户体验

实现 URL 拼接

Java API 文档,存在两份:

  1. 线上文档:https://docs.oracle.com/javase/8/docs/api/index.html
  2. 线下文档:D:\My Computer\02_Stricky\02_Code\01 比特 Java 班资料\docs\api\index.html

我们所期望的结果就是:用户点击搜索结果的时候,就能够跳转到对应的线上文档的页面。

  • 我们最终的跳转 URL 以:https://docs.oracle.com/javase/8/docs/api/固定前缀,然后根据当前本地文档所在的路径,去和前缀进行拼接
  • 我们是可以通过 getAbsolutePath() 获取到本地文档路径的,形如 D:\\My Computer\\02_Stricky\\02_Code\\01 比特Java班资料\\docs\\api\\java\\util\\ArrayList.html,然后把后半部分 提取出来:java\\util\\ArrayList.html再和前面的固定前缀进行拼接
java 复制代码
public class TestURL {  
    private static final String INPUT_PATH = "D:\\My Computer\\02_Stricky\\02_Code\\01 比特Java班资料\\docs\\api\\";  
  
    public static void main(String[] args) {  
        File file = new File("D:\\My Computer\\02_Stricky\\02_Code\\01 比特Java班资料\\docs\\api\\java\\util\\ArrayList.html");  
        // 先获取到一个固定的前缀  
        String part1 = "https://docs.oracle.com/javase/8/docs/api/";  
        String part2 = file.getAbsolutePath().substring(INPUT_PATH.length());  
        String result = part1 + part2;  
        System.out.println(result);  
    }  
}
//运行结果:
//https://docs.oracle.com/javase/8/docs/api/java\util\ArrayList.html
  • 浏览器自身有容错能力,虽然在拼接出的 URL 中既有 \ ,也有 /,但是仍然能正常访问

完整代码逻辑

java 复制代码
private String parseUrl(File f) {  
    String part1 = "https://docs.oracle.com/javase/8/docs/";  
    String part2 = f.getAbsolutePath().substring(INPUT_PATH.length());  
    return part1 + part2;  
}

测试代码

java 复制代码
public class TestURL {  
    private static final String INPUT_PATH = "D:\\My Computer\\02_Stricky\\02_Code\\01 比特Java班资料\\docs\\api\\";  
  
    public static void main(String[] args) {  
        File file = new File("D:\\My Computer\\02_Stricky\\02_Code\\01 比特Java班资料\\docs\\api\\java\\util\\ArrayList.html");  
        // 先获取到一个固定的前缀  
        String part1 = "https://docs.oracle.com/javase/8/docs/api/";  
        String part2 = file.getAbsolutePath().substring(INPUT_PATH.length());  
        String result = part1 + part2;  
        System.out.println(result);  
    }  
}

解析正文

一个完整的 HTML 文件,包含了

  • HTML 标签
  • 内容(Java 文档)
    接下来,进行解析正文的操作,核心就是去掉 HTML 文件中的标签

实现思路

实现去标签,有很多方法:

  1. 可以通过正则表达式 来实现这里的去标签操作

[!quote] 正则表达式

  • 可以认为是一种计算机中进行字符串匹配/处理的常见手段
  • 核心就是通过一些特殊符号来描述字符串的特征,然后看某个字符串是否符合这些特征

去除 HTML 标签这个环节中,虽然正则表达式可以解决问题,但是用起来很麻烦,因此我们可以使用更简单粗暴的方式来实现这里的逻辑

  1. 依次读取 HTML 中的每个字符,然后针对判定每个字符
    • 若是 <,那么就从这个位置开始,直到遇到 > 位置,都不把这些字符放在结果中
    • 若遇到的字符串不是 <,就直接把当前的字符拷贝到一个结果中(StringBuilder
    • 在期间我们可以弄一个标志位 flag,为 true 就拷贝,为 false 就不拷贝

万一内容中存在 < 或者 > 怎么办呢?

  • 不会出现这种情况
  • HTML 中要求,< 使用 &lt 来代替;> 使用 &gt 来代替

读取内容操作的实现

我们在读文件的时候,有的时候是按照"字节 "来读取,有的时候是按照"字符 "来读取。在 Java 标准库中,既提供了能够按照字节读取的类(FileInputStream),也提供了能按照字符来读取的类(FileReader

  • 此时我们是按照字符来读取的,所以使用 FileReader
java 复制代码
public String parseContent(File f) { 
	StringBuilder content = new StringBuilder();
    try(FileReader fileReader = new FileReader(f)) {   
        boolean isCopy = true;         
        
        while (true) {  
            int ret= fileReader.read();  
            if(ret == -1) {  
                break;  
            }  
            char c = (char)ret;  
            if(isCopy){  
                if(c == '<'){   
                    isCopy = false;  
                    continue;  
                }
                content.append(c);  
            }else {                 
                if(c == '>'){  
                    isCopy = true;  
                }  
            }  
        }
    	   
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
    return content.toString();   
}
  • 使用一个 StringBuilder 类型的变量 content 进行字符串的操作,方便后面进行字符拼接
    • 因为 StringBuilder 类型的变量直接使用 append() 方法就可以在原 content 后面加上字符
  • new fileReader 的操作放在 try 之后,可以省略关闭文件的操作
  • 在循环中,read() 的返回值
    • ret == -1 的时候,代表读取操作结束,直接跳出循环。read() 的返回类型为 int,就是为了方便判断何时读取结束(等于 -1 的时候)
    • 否则一直进行字符 的读取操作,并且需要将 int 类型的 ret 强转为 char,好进行后续的字符操作
  • isCopy 是开关,用来控制是否进行 append 操作的
    • false(关锁):当识别到 < 的时候就关锁,关锁后一定要进行 continent 操作,跳出此次循环,不然就会恒执行 append 操作。
    • true(开锁):当识别到 > 的时候就开锁,进行字符的 append 操作
  • 最后要返回 content 里面的字符串

观察运行结果可以看到,正文里面包含了大量的换行操作。实际上当前获取到这个正文,目的是为了后面能够生成描述信息(一段话,肯定不能有空行)

  • 所以我们肯定要把空行给去掉
    我们只需要在 append 操作前面,加上一个处理换行操作的语句就可以了
java 复制代码
if(c == '\n' || c == '\r'){
	// 为了去掉换行/回车,把换行/回车替换成空格即可
	c = ' ';
}

完整代码逻辑

java 复制代码
public String parseContent(File f) { 
	StringBuilder content = new StringBuilder();
    try(FileReader fileReader = new FileReader(f)) {   
        boolean isCopy = true;         
        
        while (true) {  
            int ret= fileReader.read();  
            if(ret == -1) {  
                break;  
            }  
            char c = (char)ret;  
            if(isCopy){  
                if(c == '<'){   
                    isCopy = false;  
                    continue;  
                }
                if(c == '\n' || c == '\r'){
                	// 为了去掉换行/回车,把换行/回车替换成空格即可
                	c = ' ';
                }
                content.append(c);  
            }else {                 
                if(c == '>'){  
                    isCopy = true;  
                }  
            }  
        }
    	   
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
    return content.toString();   
}

测试代码

java 复制代码
public class TestParseContent {  
    public static void main(String[] args) throws FileNotFoundException {  
        Parser parser = new Parser();  
        File file = new File("D:\\My Computer\\02_Stricky\\02_Code\\01 比特Java班资料\\docs\\api\\java\\util\\ArrayList.html");  
        String result = parser.parseContent(file);  
        System.out.println(result);  
    }  
}
相关推荐
Elastic 中国社区官方博客9 分钟前
Elasticsearch Open Inference API 增加了对 Jina AI 嵌入和 Rerank 模型的支持
大数据·人工智能·elasticsearch·搜索引擎·ai·全文检索·jina
计算机小白一个10 分钟前
蓝桥杯 Java B 组之岛屿数量、二叉树路径和(区分DFS与回溯)
java·数据结构·算法·蓝桥杯
孤雪心殇11 分钟前
简单易懂,解析Go语言中的Map
开发语言·数据结构·后端·golang·go
庸俗今天不摸鱼23 分钟前
Canvas进阶-4、边界检测(流光,鼠标拖尾)
开发语言·前端·javascript·计算机外设
菠菠萝宝24 分钟前
【Java八股文】10-数据结构与算法面试篇
java·开发语言·面试·红黑树·跳表·排序·lru
奔跑吧邓邓子26 分钟前
【Python爬虫(36)】深挖多进程爬虫性能优化:从通信到负载均衡
开发语言·爬虫·python·性能优化·负载均衡·多进程
不会Hello World的小苗30 分钟前
Java——链表(LinkedList)
java·开发语言·链表
lsx20240643 分钟前
Perl 面向对象编程指南
开发语言
Allen Bright1 小时前
【Java基础-46.3】Java泛型通配符详解:解锁类型安全的灵活编程
java·开发语言
柃歌1 小时前
【UCB CS 61B SP24】Lecture 7 - Lists 4: Arrays and Lists学习笔记
java·数据结构·笔记·学习·算法