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

发布于:2024-11-28 ⋅ 阅读:(41) ⋅ 点赞:(0)

实现索引构建

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

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

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

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

public void run(){  
	// 2. 针对上面罗列出的文件路径,打开路径,读取文件内容,进行解析,并构建索引 
	for(File f : fileList) {  
		System.out.println("开始解析: "+ f.getAbsolutePath());  
		// 通过这个方法来解析单个 HTML 文件  
		parseHTML(f);  
    }  
}
  • 实现这个功能,我们封装一个 parseHTML() 方法。此方法需要完成:
    1. 解析出 HTML 的标题
    2. 解析出 HTML 对应的 URL
    3. 解析出 HTML 对应的正文(有了正文才有后续的描述)
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 方法

解析标题

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

private String parseTitle(File f) {  
    f.getName();  
}

getName () 和 getAbsolutePath () 的区别

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

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 的长度
  • 总长度 - 后半部分的长度 ==> 前半部分的长度 ==> 正是后半部分开始的第一个字符的下标
f.getName().substring(0, f.getName().length() - ".html".length())
  • .html 虽然是字符串常量,但是他同样也是一个 String 类型,所以可以用 .length 求长度

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

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

完整代码逻辑

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再和前面的固定前缀进行拼接
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 中既有 \ ,也有 /,但是仍然能正常访问

完整代码逻辑

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;  
}

测试代码

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
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 操作前面,加上一个处理换行操作的语句就可以了
if(c == '\n' || c == '\r'){
	// 为了去掉换行/回车,把换行/回车替换成空格即可
	c = ' ';
}

完整代码逻辑

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();   
}

测试代码

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);  
    }  
}