【文档搜索引擎】在内存中构造出索引结构(下)
文章目录
- 4.保存到磁盘中
- 为什么要保存在磁盘中
- 怎么保存
- 操作步骤
- 1. 前期准备
- 2. 主要操作
- 5. 将磁盘中的数据加载到内存中
- Parser 类完整源码
- Index 类完整源码
4.保存到磁盘中
为什么要保存在磁盘中
索引本来是存储在内存中的,为什么要将其保存在硬盘中?
- 因为创建索引是比较耗时的
因此我们不应该在服务器启动的时候,才构建索引(启动服务器就可能会拖慢很多很多)
- 通常的做法是:把这些耗时的操作,单独去进行执行
- 单独执行完了之后,再让线上服务器直接加载这个构造好的索引
怎么保存
文本实质上就是字符串,我们就可以把字符串直接保存在文件中。我们就需要把内存中的索引结构变成一个“字符串”,然后写文件即可
- 变成字符串的过程就是——序列化
- 对应的特定结构的字符串,反向解析成一些结构化数据(类/对象/基础数据结构)——反序列化
序列化和反序列化有很多现成的通用方法,此处咱们就直接使用 JSON 格式来进行序列化/反序列化——jackson
- 通过
Maven仓库,引入依赖
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.18.2</version>
</dependency>
操作步骤
1. 前期准备
引入一个 jackson 里面会用到的核心对象
private ObjectMapper objectMapper = new ObjectMapper();
- 之后就通过这个对象,完成后续的序列化和反序列化操作
创建一个文件指定存放的目录
private static final String INDEX_PATH =
"/Users/yechiel/Desktop/Byte/code_world/Gitee/java_doc_searcher";
2. 主要操作
使用两个文件,分别保存正排和倒排
- 先判定一下索引对应的目录是否存在,不存在就创建
- 然后在索引中分别创建两个文件:
forwardIndexFile(正排文件)、invertedIndexFile(倒排文件) - 使用
writeValue方法,将文件进行写入
public void save(){ // 使用两个文件,分别保存正排和倒排 // 1. 先判断一下,索引对应的目录是否存在,不存在就创建 File indexPathFile = new File(INDEX_PATH); if(!indexPathFile.exists()){ indexPathFile.mkdirs(); } File forwardIndexFile = new File(INDEX_PATH + "fordword.txt"); File invertedIndexFile = new File(INDEX_PATH + "inverted.txt"); try { // 第一个参数:写到哪个文件里 第二个:对哪个对象进行写入 objectMapper.writeValue(forwardIndexFile, forwardIndex); objectMapper.writeValue(invertedIndexFile, invertedIndex); }catch (IOException e) { e.printStackTrace(); }
}
mkdirs()可以一次嵌套创建多级目录writeValue方法会报错,要在两个操作外面加上try-catch。这里调用这个方法就不用我们再将文件变成字符串,然后再写入文件,这里直接进行写入就方便了很多
5. 将磁盘中的数据加载到内存中
public void load(){ System.out.println("加载索引开始!"); // 1. 设置加载索引的路径(和前面保存的路径一样) File forwardIndexFile = new File(INDEX_PATH + "forward.txt"); File invertedIndexFile = new File(INDEX_PATH + "inverted.txt"); try{ // 第一个参数:从哪里读 第二个参数:当前读到的数据,按照什么类型进行解析 forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {}); invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});}catch (IOException e){ e.printStackTrace(); } System.out.println("加载索引结束!");
}
readValue就会直接读取到文件内容,并且把文件内容按照这里指定的类型进行解析- 看见这个类型是
ArrayList<>,然后就预期文件里面的 jason 也是代大括号的数组 - 然后看到每一个元素又是
DocInfo,我们的readValue就期望,我们的数据里面的大括号里面的每一个字段都得和DocInfo是相对应的- 这个对应关系我们是可以保证的,因为前面存入磁盘的时候,就是用
objectMapper的writeValue()来去把对象生成 JSON 然后保存的 - 生成的时候就是按照每一个属性名为
key来去存的,所以下面解析的时候也是和上面相对应的,根据得到的 JSON 中的每一个 key 的值,来去找到对应对象中的属性,然后给其赋值
- 这个对应关系我们是可以保证的,因为前面存入磁盘的时候,就是用
- 看见这个类型是
这里需要将这个这个结构的字符串,转换成一个 ArrayList<DocInfo> 类型的对象,jakson 专门提供了一个辅助工具类—— TypeReference<>
- 这是一个带有泛型参数的类,我们通过这个类的泛型参数,来指定我们实际要转换的类型
forwardIndex = objectMapper.readValue
(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
- 这里相当于创建了一个匿名内部类的实例(后面 new 的部分)
- 创建一个匿名内部类,这个类实现了
TypeReference - 同时再创建一个这个匿名内部类的实例
- 创建这个实例的最主要目的,就是为了把
ArrayList<DocInfo>这个类型信息,告诉readValue方法
- 创建一个匿名内部类,这个类实现了
在 java 中,并不能直接把一个类型作为方法的参数,而是必须得传一个具体的对象,正因为这个语法限制,我们就必须得绕一个弯。通过一个专门的泛型类,再搭配泛型参数,才能完成这个过程
Parser 类完整源码
package com.glg.javadoc_searcher;import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;public class Parser {// 先指定一个加载文档的路径private static final String INPUT_PATH = "/Users/yechiel/Desktop/Byte/code_world/docs";// 创建一个 Index 实例private Index index = new Index();public void run(){// 整个 Parser 的入口// 1. 根据指定的路径,枚举出该路径中所有的文件(HTML),这个过程需要把所有子目录中的文件都获取到ArrayList<File> fileList = new ArrayList<>();enumFile(INPUT_PATH, fileList);/*for(File file : fileList){System.out.println(file);}System.out.println(fileList.size());
*/// 2. 针对上面罗列出的文件路径,打开路径,读取文件内容,进行解析,并构建索引for(File f : fileList) {// 通过这个方法来解析单个 HTML 文件System.out.println("开始解析: "+ f.getAbsolutePath());parseHTML(f);}// 3. 把在内存中构造好的索引数据结构,保存到指定的文件中index.save();}private void parseHTML(File f) {// 1. 解析出 HTML 的标题String title = parseTitle(f);// 2. 解析出 HTML 对应的 URLString url = parseUrl(f);// 3. 解析出 HTML 对应的正文(有了正文才有后续的描述)String content = parseContent(f);// 4. 将解析出来的这些信息,加入到索引当中index.addDoc(title,url,content);}// 用来解析 HTML 里面的标题信息private String parseTitle(File f) {String name = f.getName();return name.substring(0, name.length() - ".html".length());}// 用来解析 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;}// 用来解析 HTML 里面的正文信息public String parseContent(File f) {//先按照一个字符一个字符的方式来读取,以 < 和 > 来控制拷贝数据的开关StringBuilder content = new StringBuilder();try {FileReader fileReader = new FileReader(f);// 加上一个是否要进行拷贝的开关boolean isCopy = true;// 还得准备一个保存结果的 StringBuilder//StringBuilder content = new StringBuilder();while (true) {// 注意:此处的 read() 返回值是 int,不是 char// 按理说,应该是依次读一个字符,返回 char 就够了呀?// 此处使用 int 作为返回值,主要是为了表示一些非法情况// 比如说读到了文件末尾,继续读,就会返回 -1// 我们就可以根据返回的 -1 判断读完了int ret= fileReader.read();if(ret == -1) {// 表示文件读完了break;}// 这个结果不是 -1,那么就是一个合法的字符了char c = (char)ret;if(isCopy){// 开关打开的状态,遇到普通字符就应该拷贝到 StringBuilder 中if(c == '<'){// 关闭开关isCopy = false;continue;}if(c == '\n' || c == '\r'){// 为了去掉换行,把换行/回车替换成空格c = ' ';}// 其他字符,直接进行拷贝即可,把结果拷贝到最终的 StringBuilder 中content.append(c);}else {// 开关关闭的状态,暂时不拷贝,直到遇到 >if(c == '>'){isCopy = true;}}}fileReader.close();} catch (IOException e) {e.printStackTrace();}return content.toString();}// 第一个参数表示我们从哪个参数开始进行递归遍历// 第二个参数表示递归得到的结果private void enumFile(String inputPath, ArrayList<File> fileList) {File rootPath = new File(inputPath);// 把当前目录中,所包含的目录名全部获取到// listFiles 能够获取到 rootPath 当前目录下所包含的文件/目录(一层目录,不会进入子文件)File[] files = rootPath.listFiles();for(File f : files) {// 此时我们就根据当前 f 的类型,来决定是否要进行递归// 若 f 是一个普通文件,就把 f 加入到 fileList 结果中// 若 f 是一个目录,就递归调用 enumFile 方法,来进一步地获取子目录中的内容if(f.isDirectory()) {enumFile(f.getAbsolutePath(),fileList);}else {if (f.getAbsolutePath().endsWith(".html"))fileList.add(f);}}}public static void main(String[] args) {// 通过 main 方法,来实现整个制作索引的过程Parser parser = new Parser();parser.run();}
}
Index 类完整源码
package com.glg.javadoc_searcher;import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;// 通过这个类,在内存中构造索引结构
public class Index {private static final String INDEX_PATH = "/Users/yechiel/Desktop/Byte/code_world/Gitee/java_doc_searcher/";private ObjectMapper objectMapper = new ObjectMapper();// 使用数组下标表示 docIdprivate ArrayList<DocInfo> forwardIndex = new ArrayList<>();// 使用一个 哈希表 来表示倒排索引// key 就是词 value 就是一簇和这个词相关的文章private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();// 这个类要提供的方法// 1. 给定一个 docId,在正排索引中,查询文档的详细信息public DocInfo getDocInfo(int docId){return forwardIndex.get(docId);}// 2. 给定一个词,在倒排索引中,查询哪些文档和这个词关联// 仔细思考这里的返回值,单纯的返回一个整数的 List 是否可行呢?这样不太好(返回整数是因为 List 里面存的是文档 id)// 词和文档之间是存在一定的“相关性”的(文档和词的相关性有强有弱),不是单一的依次排列// 所以我们再创建一个 Weight 类来处理 文档id 和 文档与词 的相关性权重public List<Weight> getInverted(String term){return invertedIndex.get(term);}// 3. 往索引中新增一个文档public void addDoc(String title, String url, String content){// 新增文档操作,需要同时给正排索引和倒排索引新增信息// 构建正排索引DocInfo docInfo = buildForward(title, url, content);// 构建倒排索引buildInverted(docInfo);}// 实现倒排索引private void buildInverted(DocInfo docInfo) {// 直接使用内部类,词频统计class WordCnt {public int titleCount;public int contentCount;}// 通过一个内部类,将两个数据装到一起了,变成一个 HashMap,更方便遍历// 这个数据结构用来统计词频HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();// 3.1 针对文档标题进行分词List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();// 3.2 遍历分词结果,统计每个词出现的次数for(Term term : terms){// 先判断一下 term 是否存在String word = term.getName();WordCnt wordCnt = wordCntHashMap.get(word);if(wordCnt == null) {// 如果不存在,就创建一个新的键值对,插入进去,titleCount 设为 1WordCnt newWordCnt = new WordCnt();newWordCnt.titleCount = 1;newWordCnt.contentCount = 0;wordCntHashMap.put(word, newWordCnt);}// 如果存在,就找到之前的值,然后把对应的 titleCount + 1wordCnt.titleCount++;}// 3.3 针对正文页进行分词terms = ToAnalysis.parse(docInfo.getContent()).getTerms();// 3.4 遍历分词结果,统计每个词出现的次数for(Term term : terms) {String word = term.getName();WordCnt wordCnt = wordCntHashMap.get(word);if(wordCnt == null) {WordCnt newWordCnt = new WordCnt();newWordCnt.titleCount = 0;newWordCnt.contentCount = 1;wordCntHashMap.put(word, newWordCnt);}else{wordCnt.contentCount++;}}// 3.5 把上面的结果汇总到一个 HashMap 里面// 最终文档的权重,就设定成标题中出现的次数 * 10 + 正文中出现的次数// 3.6 遍历刚才这个 HashMap,依次来更新倒排索引中的结构// 将 Map 转换成 Set 进行遍历(Map 不能直接进行遍历)for(Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {// 先根据这里的词,去倒排索引中查一查// 倒排索引中的一个值——倒排拉链List<Weight> invertedList = invertedIndex.get(entry.getKey());// 判断是不是存在的(空的)if(invertedList == null) {// 如果为空,就插入一个新的键值对ArrayList<Weight> newInvertedList = new ArrayList<>();// 把新的文档(当前的 DocInfo)构造成 Weight 对象,插入进来Weight weight = new Weight();weight.setDocId(docInfo.getDocId());// 权重计算公式:标题中出现的次数 * 10 + 正文中出现的次数weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);newInvertedList.add(weight);invertedIndex.put(entry.getKey(), newInvertedList);}else{// 如果非空,就把当前这个文档,构造出一个 Weight 对象,插入到倒排拉链的后面Weight weight = new Weight();weight.setDocId(docInfo.getDocId());// 权重计算公式:标题中出现的次数 * 10 + 正文中出现的次数weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);invertedList.add(weight);}}}private DocInfo buildForward(String title, String url, String content) {DocInfo docInfo = new DocInfo();docInfo.setDocId(forwardIndex.size());docInfo.setTitle(title);docInfo.setUrl(url);docInfo.setContent(content);forwardIndex.add(docInfo);return docInfo;}// 4. 把内存中的索引结构保存到磁盘中public void save(){long beg = System.currentTimeMillis();// 使用两个文件,分贝保存正排和倒排System.out.println("保存索引开始!");// 先判断一下,索引对应的目录是否存在,不存在就创建File indexPathFile = new File(INDEX_PATH);if(!indexPathFile.exists()){indexPathFile.mkdirs();}File forwardIndexFile = new File(INDEX_PATH + "fordword.txt");File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");try {// 第一个参数:写到哪个文件里 第二个:对哪个对象进行写入objectMapper.writeValue(forwardIndexFile, forwardIndex);objectMapper.writeValue(invertedIndexFile, invertedIndex);}catch (IOException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("保存索引完成!消耗时间为:" + (end - beg) + "ms");}// 5. 把磁盘中的索引数据加载到内存中public void load(){long beg = System.currentTimeMillis();System.out.println("加载索引开始!");// 设置加载索引的路径(和前面保存的路径一样)File forwardIndexFile = new File(INDEX_PATH + "forward.txt");File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");try{// 第一个参数:从哪里读 第二个参数:当前读到的数据,按照什么类型进行解析forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});}catch (IOException e){e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("加载索引结束!消耗时间为:" + (end - beg) + "ms");}}相关文章:
【文档搜索引擎】在内存中构造出索引结构(下)
文章目录 4.保存到磁盘中为什么要保存在磁盘中怎么保存操作步骤1. 前期准备2. 主要操作 5. 将磁盘中的数据加载到内存中Parser 类完整源码Index 类完整源码 4.保存到磁盘中 为什么要保存在磁盘中 索引本来是存储在内存中的,为什么要将其保存在硬盘中? …...
2024年《网络安全事件应急指南》
在这个信息技术日新月异的时代,网络攻击手段的复杂性与日俱增,安全威胁层出不穷,给企事业单位的安全防护能力带 来了前所未有的挑战。深信服安全应急响应中心(以下简称“应急响应中心”)编写了《网络安全事件应急指南》…...
前端的知识(部分)
11 前端的编写步骤 第一步:在HTML的页面中声明方法 第二步:在<script>中定义一个函数,其中声明一个data来为需要的数据 赋值一个初始值 第三步:编写这个方法实现对应的功能...
OPC UA、MQTT 和 HTTP性能分析及使用场景推荐
在选择适合的服务性能协议时,OPC UA、MQTT 和 HTTP 每种都有其独特的优势和适用场景,因此最佳选择取决于具体的应用需求和技术环境。以下是基于不同维度对比这三种协议的分析: 通信效率 OPC UA:通常用于车间环境,提供…...
并发修改导致MVCC脏写问题
并发修改导致MVCC脏写问题 一、概要 1.1 业务场景 数据库表结构设计: 一个主档数据,通过一个字段,逗号分隔的方式去关联其他明细信息的id。 如主档数据A,有3条明细数据与A关联,其id分别是1,2,3,那么其存…...
跌倒数据集,5345张图片, 使用yolo,coco json,voc xml格式进行标注,平均识别率99.5%以上
跌倒数据集,5345张图片, 使用yolo,coco json,voc xml格式进行标注,平均识别率99.5%以上 ,可用于某些场景下识别人是否跌倒或摔倒并进行告警。 数据集分割 训练组99% 5313图片 有效集0&am…...
Java转C之CMake
对于一位从 Java 转到 C 或 C 的工程师,理解 CMake 和其指令非常重要,因为 CMake 是目前 C/C 项目中最常用的构建工具。CMake 本质上是一个跨平台的自动化构建系统,它通过 CMakeLists.txt 文件来管理和配置项目的构建过程。在学习 CMake 的过…...
如何自己创建database.js文件来初始化本地sqlite数据库
如何自己创建database.js文件来初始化本地sqlite数据库!下面是一个案例展示,帮助大家,快速的视线,本地sqlite数据库信息初始化。 为了使用 database.js 文件初始化 SQLite 数据库并存储解签内容,你需要按以下步骤操作。…...
【汇编语言】内中断(三) —— 中断探险:从do0到特殊响应的奇妙旅程
文章目录 前言1. do01.1 do0程序1.2 存放字符串,得到完整的程序1.3 分析初步完成的程序1.4 正确的完整程序1.5 分析正确的完整程序 2. 设置中断向量3. 单步中断3.1 什么是单步中断?3.2 CPU为什么要提供单步中断3.2.1 思考一下Debug功能3.2.2 Debug是如何…...
0006.基于SpringBoot+element付费问答系统
适合初学同学练手项目,部署简单,代码简洁清晰; 愿世界和平再无bug 一、系统架构 前端:vue| elementui 后端:springboot | mybatis-plus 环境:jdk1.8 | mysql | maven 二、登录角色 1.管理员 2.用户 …...
SpringBoot feign基于HttpStatus重试
场景 基于springboot开发的项目,对接第三方,第三方的接口有限流策略,某个时间段内有调用频率限制,返回的状态码HttpStatus不是200,而HttpStatus是429。现基于HttpStatus我们发起的重试。 技术点 springbootfeign fe…...
【记录49】vue2 vue-office在线预览 docx、pdf、excel文档
vue2 在线预览 docx、pdf、excel文档 docx npm install vue-office/docx vue-demi0.14.6 指定版本 npm install vue-office/docx vue-demi <template><VueOfficeDocx :src"pdf" style"height: 100vh;" rendere"rendereHandler" error&…...
正则表达式中^的用法
正则表达式中^的用法 1.用法一: 限定开头 文档上给出了解释是匹配输入的开始,如果多行标示被设置成了true,同时会匹配后面紧跟的字符 比如 /^A/会匹配"An e"中的A,但是不会匹配"ab A"中的A 比如(\s|^)表示空字符串或字…...
WPF 关于界面UI菜单权限(或者任意控件的显示权限)的简单管理--只是简单简单简单简单
1.定义你的User类 public class User{public User(){ID ObjectId.NewObjectId().ToString();}public string? ID { get; set; }public string? Account { get; set; }public string? Password { get; set; }public string? PasswordMD5 { get; set; }public AccountType?…...
Https身份鉴权(小迪网络安全笔记~
附:完整笔记目录~ ps:本人小白,笔记均在个人理解基础上整理,若有错误欢迎指正! 5.2 Https&身份鉴权 引子:上一篇主要对Http数据包结构、内容做了介绍,本篇则聊聊Https、身份鉴权等技术。 …...
AngularJS 输入验证
AngularJS 输入验证 AngularJS 是一个强大的 JavaScript 框架,它允许开发者构建动态的、高性能的 Web 应用程序。在处理用户输入时,确保数据的准确性和完整性至关重要。AngularJS 提供了一套内置的输入验证机制,可以帮助开发者轻松地实现这一目标。 为什么需要输入验证? …...
【网络安全】WIFI WPA/WPA2协议:深入解析与实践
WIFI WPA/WPA2协议:深入解析与实践 1. WPA/WPA2 协议 1.1 监听 Wi-Fi 流量 解析 WPA/WPA2 的第一步是监听 Wi-Fi 流量,捕获设备与接入点之间的 4 次握手数据。然而,设备通常不会频繁连接或重新连接,为了加速过程,攻…...
前端使用xlsx-js-style导出Excel,带样式,并处理合并单元格边框显示不全和动态插入表头解决
一、在学习之前,先给出一些学习/下载地址: xlsx-js-style下载地址 https://github.com/gitbrent/xlsx-js-style 或者 https://www.npmjs.com/package/xlsx-js-style SheetJS中文教程: https://xlsx.nodejs.cn/docs/csf/cell 二、先看样…...
自动化工具ansible部署和实践
1 介绍和部署 1.1 介绍 ansible的功能 我爱你在当今的IT自动化领域,Ansible无疑是一个无法被忽视的重要角色。其便利性和高效性受到了广大开发者和系统管理员的一致好评,成为了配置管理和应用部署的首选工具。然而,对于一些初学者来说&#…...
无人机推流直播平台EasyDSS视频技术如何助力冬季森林防火
冬季天干物燥,大风天气频繁,是森林火灾的高发期。相比传统的人力巡查,无人机具有更高的灵敏度和准确性,尤其在夜间或浓雾天气中,依然能有效地监测潜在火源。 无人机可以提供高空视角和实时图像传输,帮助巡…...
【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
DBAPI如何优雅的获取单条数据
API如何优雅的获取单条数据 案例一 对于查询类API,查询的是单条数据,比如根据主键ID查询用户信息,sql如下: select id, name, age from user where id #{id}API默认返回的数据格式是多条的,如下: {&qu…...
12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...
深度剖析 DeepSeek 开源模型部署与应用:策略、权衡与未来走向
在人工智能技术呈指数级发展的当下,大模型已然成为推动各行业变革的核心驱动力。DeepSeek 开源模型以其卓越的性能和灵活的开源特性,吸引了众多企业与开发者的目光。如何高效且合理地部署与运用 DeepSeek 模型,成为释放其巨大潜力的关键所在&…...
GraphQL 实战篇:Apollo Client 配置与缓存
GraphQL 实战篇:Apollo Client 配置与缓存 上一篇:GraphQL 入门篇:基础查询语法 依旧和上一篇的笔记一样,主实操,没啥过多的细节讲解,代码具体在: https://github.com/GoldenaArcher/graphql…...
