文章目录

  • 项目介绍(开发背景)
  • 主要用到的技术点
    • 前端
    • 后端
      • Ansj分词
      • 实现索引模块
        • 实现Parser类
        • 实现Index类
        • 完善Parser类
        • 优化制作索引速度
      • 实现搜索模块
        • 实现DocSearcher类
        • 处理暂停词
  • 项目编写过程中遇到的困难点
  • 上传部署
  • 总结

项目介绍(开发背景)

相信很多小伙伴在学习Java的过程中都会参考Java官方文档,但是这个文档存在一个问题,就是不支持用户对某个关键词进行搜索,只能够通过某个包去找到其中的某个类进行查看,显然这样的效率是比较低的。


        虽然也有很多是离线的api文档是支持搜索功能的,但是我想做一个类似与百度的搜索界面,点击跳转到详细文档的效果。对此,我参考了百度搜索的页面,初步得到一个结论:搜索引擎是先输入一个查询词,会得到若干个结果,每个结果都会包含标题、部分描述展示、url等,点击标题即可完成页面跳转。

主要用到的技术点

前端

这个项目前端代码是比较简单的,只有一个页面(因为本项目只做的是搜索引擎实现的逻辑,得到结果后跳转的是官方文档详细页),数据的请求也是使用jQuery来实现的。

后端

后端部分是整个搜索引擎项目的核心部分,具体开发的流程如下:

Ansj分词

我们在输入查询词的时候,不仅仅只会是输入一个词,更有可能的是会查询一个句子,那么这时候就需要进行一个分词操作了。
        我们可以选择使用一个开源的分词库,我这里选择的是Ansj,简单来说,其实就是导入一个依赖包即可:

<dependency><groupId>org.ansj</groupId><artifactId>ansj_seg</artifactId><version>5.1.6</version>
</dependency>

实现索引模块

实现Parser类

Parser类负责读取html文件,制作并生成索引数据(输出到文件中),主要的步骤:1. 从制定的路径中枚举出所有文件;2. 读取每一个文件,并从文件中解析出标题、正文内容、url。

//Description: 解析api文档的内容,完成索引的制作
public class Parser {//指定加载文档的路径private static final String INPUT_PATH = "D:/java_code/java-api/java8/docs/api/";private Index index = new Index();public void run(){//根据指定的路径,枚举出该路径下的所有html文件(包括子目录中的所有文件)List<File> fileList = new ArrayList<>();enumFile(INPUT_PATH, fileList);//打开对应的文件,读取文件内容,进行解析并构建索引long begin = System.currentTimeMillis();for(File file : fileList){//解析html文件System.out.println("开始解析:" + file.getAbsolutePath());parseHTML(file);}long end = System.currentTimeMillis();System.out.println("遍历文件加入到索引中消耗的时间:" + (end - begin) + "ms");}private void parseHTML(File file) {//解析标题、正文、urlString title = parseTitle(file);//String content = parseContent(file);String content = parseContentByRegex(file);String url = parseUrl(file);}//解析urlprivate String parseUrl(File file) {String part1 = "https://docs.oracle.com/javase/8/docs/api/";String part2 = file.getAbsolutePath().substring(INPUT_PATH.length());return part1 + part2;}//解析标题private String parseTitle(File file) {String name = file.getName();return name.substring(0, name.length() - ".html".length());}//解析正文private String parseContent(File file) {try {//手动将缓冲区设置成1M大小BufferedReader bufferedReader = new BufferedReader(new FileReader(file), 1024 * 1024);boolean flag = true;StringBuilder content = new StringBuilder();while(true){int t = bufferedReader.read();if(t == -1){break;}char ch = (char)t;if(flag){if(ch == '<'){flag = false;continue;}if(ch == '\n' || ch == '\r'){ch = ' ';}content.append(ch);}else{if(ch == '>'){flag = true;}}}bufferedReader.close();  //记得文件的关闭return content.toString();} catch (IOException e) {e.printStackTrace();}return "";}//递归完成目录的枚举private void enumFile(String inputPath, List<File> fileList) {File rootPath = new File(inputPath);File[] files = rootPath.listFiles();for(File file : files){if(file.isDirectory()){enumFile(file.getAbsolutePath(), fileList);}else{if(file.getAbsolutePath().endsWith(".html")) {fileList.add(file);}}}}public static void main(String[] args) throws InterruptedException {Parser parser = new Parser();parser.runByThread();}
}

实现Index类

Index类负责构建索引数据结构,其中需要包含的方法有:getDocInfo(根据docId查正排)、getInverted(根据关键词查倒排)、addDoc(往索引中新增一个文档,构建正排索引和倒排索引)、save(往磁盘中写索引数据)、load(从磁盘中加载索引数据)。

整个搜索的核心思路其实就在这个类中,对于搜索的思路主要有两种:一种是直接进行暴力搜索,每次处理搜索请求的时候,拿着查询词到所有网页中都搜索一遍,并检查每个网页中是否包含查询词字符串,但是随着文档数量不断的增加,这种方法所需要的开销是非常大的,而对于搜索引擎来说,效率往往都是放在第一位的,因此该方法是不可行的;另外一种是建立倒排索引,倒排索引是一种专门针对搜索引擎场景所设计的数据结构,可以通过文档的信息来制定排序规则。

从构建倒排索引和根据关键词查倒排这两个步骤中可以看出一个问题,那就是倒排索引需要使用什么规则来构建呢?直接按照插入文档的顺序来构建显然是不行的,我们在百度搜索的时候会看到排在前面的都是比较重要的或者是搜索次数比较多的,因此,我们在构建倒排索引的时候不能仅仅按照插入顺序,而是应该自定义一个权重,我在这个项目中定义的权重是按照搜索词在文档中出现的次数(标题出现+10,正文出现+1的规则)来实行的。以下是定义权重的类:

public class Weight {private int docId;private int weight;public int getDocId() {return docId;}public void setDocId(int docId) {this.docId = docId;}public int getWeight() {return weight;}public void setWeight(int weight) {this.weight = weight;}
}

构建出Index类的基本骨架:

public class Index {private static String INDEX_PATH = "D:/java_code/java-api/java8/";private ObjectMapper objectMapper = new ObjectMapper();private List<DocInfo> forwardIndex = new ArrayList<>();  //正排索引private Map<String, List<Weight>> invertedIndex = new HashMap<>();  //倒排索引//给定一个docId,通过正排索引查询到文档的详细信息public DocInfo getDocInfo(int docId){//TODO}//给定一个词,通过倒排索引查询哪些文档与这个词相关联public List<Weight> getInverted(String term){//TODO}//往索引中新增一个文档public void addDoc(String title, String url, String content){//TODO}//把内存中的索引结构保存到磁盘中public void save(){//TODO}//把磁盘中的索引数据加载到内存中public void load(){//TODO}
}

查正排&查倒排:

    //给定一个docId,通过正排索引查询到文档的详细信息public DocInfo getDocInfo(int docId){return forwardIndex.get(docId);}//给定一个词,通过倒排索引查询哪些文档与这个词相关联public List<Weight> getInverted(String term){return invertedIndex.get(term);}

实现addDoc(构建正排索引和倒排索引):

    //往索引中新增一个文档public void addDoc(String title, String url, String content){DocInfo docInfo = buildForward(title, url, content);buildInverted(docInfo);}

正排索引的构建是比较简单的,直接按照插入顺序进行构建即可:

    private DocInfo buildForward(String title, String url, String content) {DocInfo docInfo = new DocInfo();docInfo.setTitle(title);docInfo.setUrl(url);docInfo.setContent(content);docInfo.setDocId(forwardIndex.size());forwardIndex.add(docInfo);return docInfo;}

倒排索引需要计算每个词的权重,我们这里可以使用哈希表来进行存储。

public class WordCnt {private int titleCount;private int contentCount;public int getTitleCount() {return titleCount;}public void setTitleCount(int titleCount) {this.titleCount = titleCount;}public int getContentCount() {return contentCount;}public void setContentCount(int contentCount) {this.contentCount = contentCount;}
}
    private void buildInverted(DocInfo docInfo) {//针对标题和正文进行分词(需要分开计算),并统计每个词出现的次数Map<String, WordCnt> wordCntMap = new HashMap<>();List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();for(Term term : terms){String word = term.getName();if(!wordCntMap.containsKey(word)){WordCnt wordCnt = new WordCnt();wordCnt.setTitleCount(1);wordCnt.setContentCount(0);wordCntMap.put(word, wordCnt);}else{WordCnt wordCnt = wordCntMap.get(word);wordCnt.setTitleCount(wordCnt.getTitleCount() + 1);}}terms = ToAnalysis.parse(docInfo.getContent()).getTerms();for(Term term : terms){String word = term.getName();if(!wordCntMap.containsKey(word)){WordCnt wordCnt = new WordCnt();wordCnt.setTitleCount(0);wordCnt.setContentCount(1);wordCntMap.put(word, wordCnt);}else{WordCnt wordCnt = wordCntMap.get(word);wordCnt.setContentCount(wordCnt.getContentCount() + 1);}}for(Map.Entry<String, WordCnt> entry : wordCntMap.entrySet()){List<Weight> invertedList = invertedIndex.get(entry.getKey());if(invertedList == null){List<Weight> list = new ArrayList<>();Weight weight = new Weight();weight.setDocId(docInfo.getDocId());weight.setWeight(entry.getValue().getTitleCount() * 10 + entry.getValue().getContentCount());list.add(weight);invertedIndex.put(entry.getKey(), list);}else{Weight weight = new Weight();weight.setDocId(docInfo.getDocId());weight.setWeight(entry.getValue().getTitleCount() * 10 + entry.getValue().getContentCount());invertedList.add(weight);}}}

单看这段代码可以会有点绕,可以先看下图:

        也就是说都有其对应的文章(可以多篇),这其实就类似于是一个哈希桶,当查询词是某个词的时候,就可以快速列出对应词下的文章,这就是倒排索引,当然,每一个节点中都包含了文章的信息(如词出现的次数等),方便后面对这些权重进行自定义排序。

实现save往磁盘中写索引数据(通过json格式来生成正排索引和倒排索引两个文件):

    //把内存中的索引结构保存到磁盘中public void save(){//使用两个文件分别表示正排和倒排//判断索引对应的文件是否存在,不存在就创建long begin = System.currentTimeMillis();File indexPathFile = new File(INDEX_PATH);if(!indexPathFile.exists()){indexPathFile.mkdirs();}File forwardIndexFile = new File(INDEX_PATH + "forward.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 - begin) + "ms");}

实现load将索引文件中的内容加载到内存中:

    //把磁盘中的索引数据加载到内存中public void load(){long begin = System.currentTimeMillis();File forwardIndexFile = new File(INDEX_PATH + "forward.txt");File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");try {forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<List<DocInfo>>() {});invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<Map<String, List<Weight>>>() {});} catch (IOException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("数据加载成功. 消耗时间:" + (end - begin) + "ms");}

完善Parser类

接下来,我们可以在Parser类中实例化Index对象,在每次解析出信息的时候就调用Index对象添加到索引中,同时将内存中的索引数据保存到指定文件中:

 private Index index = new Index();
    private void parseHTML(File file) {//解析标题、正文、urlString title = parseTitle(file);//String content = parseContent(file);String content = parseContentByRegex(file);String url = parseUrl(file);//将解析出的信息加入到索引中index.addDoc(title, url, content);}
    public void run(){//根据指定的路径,枚举出该路径下的所有html文件(包括子目录中的所有文件)List<File> fileList = new ArrayList<>();enumFile(INPUT_PATH, fileList);//打开对应的文件,读取文件内容,进行解析并构建索引long begin = System.currentTimeMillis();for(File file : fileList){//解析html文件System.out.println("开始解析:" + file.getAbsolutePath());parseHTML(file);}long end = System.currentTimeMillis();System.out.println("遍历文件加入到索引中消耗的时间:" + (end - begin) + "ms");//内存中构造的索引数据结构保存到指定文件中index.save();}

优化制作索引速度

索引的制作是会比较耗时间的,特别是数据量大的时候,那么需要如何优化这个过程呢?显然,我们可以通过多线程来进行优化:

    //实现多线程制作索引public void runByThread() throws InterruptedException {//根据指定的路径,枚举出该路径下的所有html文件(包括子目录中的所有文件)List<File> fileList = new ArrayList<>();enumFile(INPUT_PATH, fileList);//打开对应的文件,读取文件内容,进行解析并构建索引(引入线程池)CountDownLatch countDownLatch = new CountDownLatch(fileList.size());ExecutorService executorService = Executors.newFixedThreadPool(4);long begin = System.currentTimeMillis();for(File file : fileList){executorService.submit(new Runnable() {@Overridepublic void run() {//解析html文件System.out.println("开始解析:" + file.getAbsolutePath());parseHTML(file);countDownLatch.countDown();}});}countDownLatch.await();long end = System.currentTimeMillis();System.out.println("遍历文件加入到索引中消耗的时间:" + (end - begin) + "ms");executorService.shutdown();  //手动销毁线程池中的线程//内存中构造的索引数据结构保存到指定文件中index.save();}

我这里创建的是4个线程(已经可以提升很高的效率了),需要注意的是:在使用多线程的时候需要检查代码中是否存在写操作,如果存在,需要对这个操作整体进行加锁操作。我们在检查代码的时候就发现有几个地方是需要修改的:

(1)在建立倒排索引的时候,我们需要不断地对出现词次数进行修改,所以在出现修改操作部分的代码都需要加上锁:

    private void buildInverted(DocInfo docInfo) {//针对标题和正文进行分词(需要分开计算),并统计每个词出现的次数Map<String, WordCnt> wordCntMap = new HashMap<>();List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();for(Term term : terms){synchronized (locker1){String word = term.getName();if(!wordCntMap.containsKey(word)){WordCnt wordCnt = new WordCnt();wordCnt.setTitleCount(1);wordCnt.setContentCount(0);wordCntMap.put(word, wordCnt);}else{WordCnt wordCnt = wordCntMap.get(word);wordCnt.setTitleCount(wordCnt.getTitleCount() + 1);}}}terms = ToAnalysis.parse(docInfo.getContent()).getTerms();for(Term term : terms){synchronized (locker2){String word = term.getName();if(!wordCntMap.containsKey(word)){WordCnt wordCnt = new WordCnt();wordCnt.setTitleCount(0);wordCnt.setContentCount(1);wordCntMap.put(word, wordCnt);}else{WordCnt wordCnt = wordCntMap.get(word);wordCnt.setContentCount(wordCnt.getContentCount() + 1);}}}for(Map.Entry<String, WordCnt> entry : wordCntMap.entrySet()){synchronized (locker3){List<Weight> invertedList = invertedIndex.get(entry.getKey());if(invertedList == null){List<Weight> list = new ArrayList<>();Weight weight = new Weight();weight.setDocId(docInfo.getDocId());weight.setWeight(entry.getValue().getTitleCount() * 10 + entry.getValue().getContentCount());list.add(weight);invertedIndex.put(entry.getKey(), list);}else{Weight weight = new Weight();weight.setDocId(docInfo.getDocId());weight.setWeight(entry.getValue().getTitleCount() * 10 + entry.getValue().getContentCount());invertedList.add(weight);}}}}

(1)在建立正排索引的时候,我们也需要不断地对id进行修改,所以在出现修改操作部分的代码都需要加上锁:

    private DocInfo buildForward(String title, String url, String content) {DocInfo docInfo = new DocInfo();docInfo.setTitle(title);docInfo.setUrl(url);docInfo.setContent(content);synchronized (locker4){docInfo.setDocId(forwardIndex.size());forwardIndex.add(docInfo);}return docInfo;}

实现搜索模块

实现DocSearcher类

简单介绍一下这个类需要实现的功能,这个类主要负责实现搜索功能,得到用户输入的查询词后,对查询词进行分词处理,之后对每一个分词都查找一下之前存的倒排索引,得到一个倒排哈希桶,将这些结构合并到一个结果集中,并对其中的这些结果按照指定的权重降序排序后返回结果即可。

    public List<Result> search(String query){//分词List<Term> oldTerms = ToAnalysis.parse(query).getTerms();List<Term> terms = new ArrayList<>();//过滤for(Term term : oldTerms){terms.add(term);}//触发List<List<Weight>> termResult = new ArrayList<>();for(Term term : terms){String word = term.getName();List<Weight> invertedList =  index.getInverted(word);if(invertedList == null){continue;}termResult.add(invertedList);}//合并List<Weight> allTermResult = mergeResult(termResult);//排序(降序)allTermResult.sort((o1, o2) -> {return o2.getWeight() - o1.getWeight();});//包装List<Result> results = new ArrayList<>();for(Weight weight : allTermResult){DocInfo docInfo = index.getDocInfo(weight.getDocId());Result result = new Result();result.setTitle(docInfo.getTitle());result.setUrl(docInfo.getUrl());result.setDesc(GenDesc(docInfo.getContent(), terms));results.add(result);}return results;}

对于其中的合并操作,我们可以使用优先级队列来辅助实现(其实这就涉及到多路归并算法的思想):

    private List<Weight> mergeResult(List<List<Weight>> source) {//针对每一行进行排序(按照id升序排序)for(List<Weight> curRow : source){curRow.sort(new Comparator<Weight>(){@Overridepublic int compare(Weight o1, Weight o2) {return o1.getDocId() - o2.getDocId();}});}//使用优先级队列对每一行进行合并List<Weight> target = new ArrayList<>();PriorityQueue<Pos> queue = new PriorityQueue<>(new Comparator<Pos>() {@Overridepublic int compare(Pos o1, Pos o2) {return source.get(o1.row).get(o1.col).getDocId() - source.get(o2.row).get(o2.col).getDocId();}});for(int row = 0; row < source.size(); row++){queue.offer(new Pos(row, 0));}while(!queue.isEmpty()){Pos minPos = queue.poll();Weight curWeight = source.get(minPos.row).get(minPos.col);if(target.size() > 0){Weight lastWeight = target.get(target.size() - 1);if(lastWeight.getDocId() == curWeight.getDocId()){lastWeight.setWeight(lastWeight.getWeight() + curWeight.getWeight());}else{target.add(curWeight);}}else{target.add(curWeight);}Pos newPos = new Pos(minPos.row, minPos.col + 1);if(newPos.col >= source.get(newPos.row).size()){continue;}queue.offer(newPos);}return target;}

以下这段代码表示需要截取内容的部分简介显示到页面上:

    private String GenDesc(String content, List<Term> terms) {int firstPos = -1;for(Term term : terms){String word = term.getName();content = content.toLowerCase().replaceAll("\\b" + word + "\\b", " " + word + " ");firstPos = content.indexOf(" " + word + " ");if(firstPos >= 0){break;}}if(firstPos == -1){return content.substring(0, Math.min(60, content.length())) + "...";}String desc = "";int descBeg = firstPos < 30 ? 0 : firstPos - 30;desc = content.substring(descBeg, Math.min(descBeg + 100, content.length())) + "...";for(Term term : terms){String word = term.getName();desc = desc.replaceAll("(?i) " + word + " ", "<i>" + " " + word + " " + "</i>");}return desc;}

处理暂停词

我们在网上搜索到一些常见的暂停词后将它们保存到一个文件夹中,每次从文件中获取这些暂停词,看是否有存在于查询词中的,若有,直接跳过即可。

    private static String STOP_WORD_PATH = "D:/java_code/java-api/java8/stop_word.txt";private Set<String> stopWords = new HashSet<>();

将暂停词从磁盘加载到内存中:

    public void loadStopWords(){try {BufferedReader bufferedReader = new BufferedReader(new FileReader(STOP_WORD_PATH));while(true){String line = bufferedReader.readLine();if(line == null){break;}stopWords.add(line);}bufferedReader.close();} catch (IOException e) {e.printStackTrace();}}

之后就是补充上面search方法中的代码,在查询词中发现暂停词时,直接进行过滤操作:

     //过滤for(Term term : oldTerms){if(stopWords.contains(term.getName())){continue;}terms.add(term);}

项目编写过程中遇到的困难点

这个项目是基础版的搜索引擎,所以总体来说是中规中矩的,没有特别难解决的问题,唯一一个比较难编写的是最后的使用多路归并算法来实现权重的合并。

上传部署

将打包后的jar包以及正排、倒排索引和暂停词放在同一个目录底下,输入命令:java -jar [jar包名]就可以打开SpringBoot项目;持久运行需要输入命令:nohup java -jar [jar包名] &

总结

简单总结一下这个Java文档搜索引擎项目:
        有点:使用了主流的SpringBoot框架进行开发,运用多线程来提高代码的运行效率,包含了基本搜索引擎中所需要的数据结构和算法。
        缺点:扩展性弱,这个搜索引擎只能够针对Java文档进行搜索,比较合理的实现是需要搭配爬虫实现对页面数据的获取,当然,这个难度也是比较大的。
        关于本项目的全部代码我都放在了我的个人Gitee账户下,有需要的可以点击查看:Java文档搜索引擎项目存放代码。

【项目总结】基于SpringBoot+Ansj分词+正倒排索引的Java文档搜索引擎项目总结相关推荐

  1. 基于Springboot+vue电影院会员管理系统(源代码+数据库+文档)025

    部分代码地址 https://gitee.com/ynwynwyn/cinema-public 基于Springboot+vue电影院会员管理系统(源代码+数据库+文档) 一.系统介绍 cinema项 ...

  2. 一个基于特征向量的近似网页去重算法——term用SVM人工提取训练,基于term的特征向量,倒排索引查询相似文档,同时利用cos计算相似度

    一个基于特征向量的近似网页去重算法--term用SVM人工提取训练,基于term的特征向量,倒排索引查询相似文档,同时利用cos计算相似度 摘  要  在搜索引擎的检索结果页面中,用户经常会得到内容相 ...

  3. 一个基于特征向量的近似网页去重算法——term用SVM人工提取训练,基于term的特征向量,倒排索引查询相似文档,同时利用cos计算相似度...

    摘  要  在搜索引擎的检索结果页面中,用户经常会得到内容相似的重复页面,它们中大多是由于网站之间转载造成的.为提高检索效率和用户满意度,提出一种基于特征向量的大规模中文近似网页检测算法DDW(Det ...

  4. 基于Python的银行信用卡欺诈预测模型设计 文档+任务书+项目源码及数据

    目录 第一章 引言 3 1.1研究背景及意义 3 1.2研究现状 3 1.3行文思路及框架 4 第二章 数据探索性分析 6 2.1目标变量 6 2.2特征分布 6 2.3特征与标签相关性分析 8 2. ...

  5. Springboot毕设项目基于SpringBoot的房源管理系统lyh88(java+VUE+Mybatis+Maven+Mysql)

    Springboot毕设项目基于SpringBoot的房源管理系统lyh88(java+VUE+Mybatis+Maven+Mysql) 项目运行 环境配置: Jdk1.8 + Tomcat8.5 + ...

  6. Springboot毕设项目基于springboot大学生兼职平台3tf70(java+VUE+Mybatis+Maven+Mysql)

    Springboot毕设项目基于springboot大学生兼职平台3tf70(java+VUE+Mybatis+Maven+Mysql) 项目运行 环境配置: Jdk1.8 + Tomcat8.5 + ...

  7. 基于SpringBoot的二手交易平台(自己的课程设计附项目下载)

    基于SpringBoot的二手交易平台(自己的课程设计附项目下载) 整体的目录如下: 整体首页是这样的: 左边是一个快捷的连接分类按钮,右边是分类对应的商品 那下面就详细介绍一下吧!!! 注意:下面的 ...

  8. Springboot毕设项目基于Springboot的漫画网站mw0s4(java+VUE+Mybatis+Maven+Mysql)

    Springboot毕设项目基于Springboot的漫画网站mw0s4(java+VUE+Mybatis+Maven+Mysql) 项目运行 环境配置: Jdk1.8 + Tomcat8.5 + M ...

  9. 基于JAVA项目任务跟踪系统计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA项目任务跟踪系统计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA项目任务跟踪系统计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目架构:B/S架构 开发语 ...

最新文章

  1. 一个程序员眼中的好UI
  2. HDUOJ-2094-产生冠军
  3. C++语法细节注意集锦
  4. CCNP-第十四篇-BGP综合实验
  5. sklearn数据集与估计器
  6. 服务器系统2008能升级2012吗,将 Windows Server 2008 R2 升级到 Windows Server 2012 R2
  7. 设计模式三(工厂方法模式)学习笔记
  8. python预测实例教程_手把手教你用Python库Keras做预测(附代码)-阿里云开发者社区...
  9. python计算信息增益_利用Python提取ABAQUS的计算结果(ODB)信息、体积、应变等变化(一)...
  10. font awesome图标大小调整
  11. imple introduction to LDD
  12. 数据的类型:分类数据、顺序数据、数值型数据
  13. autojs之启动页
  14. 《调教命令行04》触碰Linux的每个角落(长文)
  15. Spring Boot系列六 Spring boot集成mybatis、分页插件pagehelper
  16. 毕业之后从事前端工作月薪大概多少?
  17. 奥鹏计算机19在线作业答案,东师计算机应用基础19春在线作业1【标准答案】
  18. 常见的http状态消息
  19. 读书笔记《CSS权威指南》
  20. HTML CSS 网页设计作业「体育小站」(梅西足球 6页 )

热门文章

  1. TCP/IP:使用wireshark进行网络数据分析
  2. C语言应用笔记(一):运算符优先级和使用问题
  3. 初次了解polyfill
  4. 自定义View——幸运转盘
  5. jQuery 效果 - 淡入淡出
  6. 转:细数国内市场智能语音开放平台有哪些?
  7. java计算工龄,java计算工龄
  8. 【参考文献】骨骼肌成肌细胞低血清培养​
  9. 关于VB.NET IIF函数
  10. 分享一个火狐浏览器firefox的所有版本所有平台所有国家的地址