【文档搜索引擎】实现索引构建——解析标题、解析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);  
    }  
}
相关推荐
Scott9999HH2 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔2 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社3 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海3 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖4 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大7 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai7 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说7 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json