前言
全站搜索这一功能我想加入到我的博客中不是一年两年的事了。但因自己现在弃用Hexo转而自己做博客,这两年搜索这个功能就一直未能实现。
最近自己偶然有新想法,就给实现了。效果还不错,现在搭配Github Actions使用,可以实现新文章自动索引,实现了自动化。
效果
搜索示例
要想充分体验,还是自己去试试的好。转到Articles索引页
实现方式
数据生成
要想在静态页搜索,就要自己创建索引。这里使用python来创建一个JSON,存储全站文章信息:
# -*- coding: utf-8 -*- ## 使用有问题请到github.com/ravelloh/RPageSearch提ISSUE ### Author: RavelloH #### LICENCE: MIT ##### RPageSearch import os from bs4 import BeautifulSoup ## 设置目标 target = './articles/' # 目录位置 layers = 1 # 遍历层数 targettype = 'html' # 文件后缀名(只支持html) main_structure_head='{"articles":[' main_structure_end=']}' inner_structure_1='{"title":"' inner_structure_2='","path":"' inner_structure_3='","time":"' inner_structure_4='","text":"' inner_structure_5='"}' ## 打开目标目录 targetfile = [] for i in os.listdir(target): if '.' not in i: for k in os.listdir(target +i): if targettype in k: targetfile.append(target + i + '/' + k) ## 按时间顺序排序 targetfilenum = [] for i in targetfile: targetfilenum.append(i[11:19]) targetfilenum.sort(reverse=True) targetfile=[] for i in targetfilenum: targetfile.append('./articles/'+str(i)+'/index.html') ## 解析重构目标文件 inner_structure_cache=[] inner_structure_text='' for i in targetfile: inner_structure_text = '' with open(i,'r') as f: filecontent = BeautifulSoup(f.read(),'html.parser') textlist = filecontent.find_all(name='p') title = filecontent.find_all(name='h2') titlelen=len(title) length = len(textlist) for j in range(length): inner_structure_text=inner_structure_text+textlist[j].get_text() time = i[-19:-11] time = time[0:4]+'-'+time[4:6]+'-'+time[6:8] title = title[titlelen-1] path = i[1:][:-10] inner_structure_text=inner_structure_text.replace(' ','').replace('\n','').replace('"','"').replace('\\','') inner_structure_all = inner_structure_1 + str(title.get_text()) + inner_structure_2 + str(path) + inner_structure_3 + str(time) + inner_structure_4 + inner_structure_text + inner_structure_5 inner_structure_cache.append(inner_structure_all) ## 重构完整JSON main_structure = main_structure_head for i in inner_structure_cache: main_structure = main_structure + i + ',' main_structure = main_structure[:-1] + main_structure_end total_str = 'var SearchResult = "' + main_structure.replace('"','\\"') + '"' print(total_str) # 写入JSON#文件 with open('./js/searchdata.js','w+') as #1: f1.write(total_str)
上述代码实现了将articles
目录下所有文件夹中以.html
后缀结尾的文件中的p标签中文字提取出来,并顺便提取h2的文章标题。
不过因为python中直接使用os.dirlist扫出的文件名是乱序,为方便后续排序还需要按照时间顺序排序,其中因为我的文章存储方式是以时间排序的,如这篇文章的存储结构就是/articles/20220825/inxex.html
,因为时间可以直接从文件夹中读出,时间排序比较方便,7行就搞定了,如果是其余方式也同理。
上述代码运行后,得出的应该是类似于如下结构的json:
{ "articles":[{ "title":"文章标题","path":"相对路径","time":"更新时间","text":"所有正文" }, { "title":"文章标题","path":"相对路径","time":"更新时间","text":"所有正文" }, { "title":"文章标题","path":"相对路径","time":"更新时间","text":"所有正文" }] }
这样全站搜索的json就生成完成了,为了方便引用,上述代码最后会将这个json改为js格式,并转义"
字符。
这样,就可以在后续处理搜索时直接引用js,其中json存储在变量SearchResult
中。
搜索处理
有了json,搜索就只需在前端实现了。这样可以脱离服务器的限制,唯一限制速度的是访客的设备性能。
但因为这里只是简单的字符串搜索,性能需求并不大,下面我写的代码虽说并没有做到极限优化,但也通过多层次搜索降低了一部分运算量,可以做到实时搜索输入数据。
以下是HTML与JavaScript代码。当然,这跟我博客上的不一样,博客上还加入了一些css过渡之类的,不过本篇重点也不是css,如果有需求可自行到博客articles页F12看看。博客源代码在github,见此。
<div class='searchbox'></span> <form class="searchbox" onSubmit="return check();" autocomplete="off"> <input type="text" placeholder="从所有文章内检索..." name="search" oninput="searchtext()" onpropertychange="searchtext()"> <button type="button" id='searchbutton'><span class="i_mini ri:search-line"></span></button> <div class="resultlist" id="resultlist"> <i>- 搜索 -</i><hr><p align="center"> 输入关键词以在文章标题及正文中查询 </p> <hr><a href="https://github.com/ravelloh/RPageSearch">Search powered by RavelloH's RPageSearch</a> </div> </form> </div>
let input = document.querySelector("input[type='text']"); let result = document.getElementById('resultlist') let button = document.getElementById('searchbutton') obj = JSON.parse(SearchResult); function searchtext() { result.innerHTML = input.value; if (input.value == '') { result.innerHTML = '<i>- 搜索 -</i><hr>'+'<p align="center">输入关键词以在文章标题及正文中查询</p><hr>' } // 标题搜索 resultcount = 0; resultstr = ''; var resulttitlecache = new Array() for (i = 0; i < obj.articles.length; i++) { if (obj.articles[i]['title'].includes(input.value) == true) { resulttitlecache.unshift(obj.articles[i]['title']) resultcount++; } } // 标题搜索结果展示 if (resultcount !== 0 && resultcount !== obj.articles.length) { for (i = 0; i < resulttitlecache.length; i++) { for (j = 0; j < obj.articles.length; j++) { if (obj.articles[j]['title'] == resulttitlecache[i]) { titlesearchresult = '<h4><a href="'+obj.articles[j]["path"]+'" class="resulttitle">'+obj.articles[j]['title'].replace(new RegExp(input.value, 'g'), '<mark>'+input.value+'</mark>')+'</a></h4><em>-标题匹配</em><p class="showbox">'+obj.articles[j]['text'].substring(0, 100)+'</p>' resultstr = titlesearchresult + '<hr>' + resultstr } } result.innerHTML = '<i>"'+input.value+'"</i><hr>'+resultstr; } } // 正文搜索 var resulttextcache = new Array() for (i = 0; i < obj.articles.length; i++) { if (obj.articles[i]['text'].includes(input.value) == true) { resulttextcache.unshift(obj.articles[i]['text']) resultcount++; } } // 正文搜索结果计数 var targetname = new Array() var targetscore = new Array() if (resulttextcache.length !== 0 && input.value !== '') { for (i = 0; i < resulttextcache.length; i++) { for (j = 0; j < obj.articles.length; j++) { if (obj.articles[j]['text'] == resulttextcache[i]) { targetname.unshift(obj.articles[j]['title']) targetscore.unshift(obj.articles[j]['text'].match(RegExp(input.value, 'gim')).length) } } } } //排序相关选项 var targetscorecache = targetscore.concat([]); var resultfortext = ''; var textsearchresult = '' targetscorecache.sort(function(a, b) { return b-a }) for (i = 0; i < targetscorecache.length; i++) { for (j = 0; j < targetscore.length; j++) { if (targetscorecache[i] == targetscore[j]) { console.log('文章排序:'+targetname[j]) for (k = 0; k < obj.articles.length; k++) { if (obj.articles[k]['title'] == targetname[j]) { // 确认选区 textorder = obj.articles[k]['text'].indexOf(input.value) -15; while (textorder < 0) { textorder++ } resultfortext = '<h4><a href="'+obj.articles[k]["path"]+'" class="resulttitle">'+obj.articles[k]['title']+'</a></h4><em>-'+targetscorecache[i]+'个结果</em><p class="showbox">...'+obj.articles[k]['text'].substring(textorder, textorder+100).replace(new RegExp(input.value, 'g'), '<mark>'+input.value+'</mark>')+'</p>' textsearchresult = textsearchresult + '<hr>' + resultfortext; } } } } } // 无效结果安排 if (resultcount !== obj.articles.length) { if (resultcount == 0) { result.innerHTML = '<i>"'+input.value+'"</i><hr><p align="center">没有找到结果</p>' } } // 整合 result.innerHTML = result.innerHTML.substring(0, result.innerHTML.length-4)+textsearchresult.substring(0, textsearchresult.length-4)+'<hr><a href="https://github.com/ravelloh/RPageSearch" class="tr">Search powered by RavelloH\'s RPageSearch</a>' }
代码分析
<!-- CSS --> form.searchbox input[type=text] { color: #c6c9ce; height: 30px; padding: 5px; font-size: 12px; float: left; width: 80%; background: #000000; border: 1px solid #1e1e1e; border-radius: 10px 0px 0px 10px; } form.searchbox button { height: 30px; float: left; width: 20%; padding: 5px; background: #1e1e1e; color: white; font-size: 12px; border: none; cursor: pointer; border: 1px solid #1e1e1e; border-radius: 0px 10px 10px 0px; text-align: center; line-height: 10px; margin: auto; } form.searchbox button:hover { background: #0b7dda; transition: background 0.2s; } form.searchbox::after { content: ""; clear: both; display: table; } .resultlist { top: -20px; height: 0; width: 100%; transition: height 0.4s; background: #000000; color: #c6c9ce; } .resultlist#active { border-radius: 10px; border: 1px solid #1e1e1e; height: 40%; } .resultlist * { margin: 2px; } .resulttitle { color: #ffffff; font-size: 1em; } #info { opacity: 1; transition: opacity 0.4s; } #hidden { opacity: 0; } .fc { text-align: center; } .title { max-width: 45%; } .tr { text-align: right } mark { background-color: #0b7dda; border-radius: 4px; color: #fff; margin: 0; } .showbox { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; margin: 2px !important }
// JS 添加到刚才所有JS的最上方 function check() { return false; } function maxfor(arr) { var len = arr.length; var max = -Infinity; while (len--) { if (arr[len] > max) { max = arr[len]; } } return max; } // 下面的和刚才的代码有部分重复,替换就行 let input = document.querySelector("input[type='text']"); let result = document.getElementById('resultlist') let infos = document.getElementById('info') let button = document.getElementById('searchbutton') input.addEventListener("focus", e => { result.style.height = '40%' result.style.overflow = 'auto' result.style.padding = '3px' infos.id = 'hidden' }) input.addEventListener("blur", e => { result.style.height = '0' result.style.overflow = 'hidden' result.style.padding = '0' infos.id = 'info' }) // 下方应为obj的定义
后言
上述就是目前这个版本搜索的实现,理论上来说这个和前面一篇文章一样,内容都是关于伪动态的,因为搜索这个功能到我写这篇文章为止,都是先存数据库,然后在搜索时拿去数据库比对,之后返回结果,像我这样把搜索过程全搬到用户端的估计全互联网我还是第一人。不过这样也有利有弊,最大的利是节省了服务器,而弊端就是搜索速度依靠用户端设备算力,另外就是在内容太多时需要先将json下载到本地才能搜索(这是必要的,但是可以通过预加载等方式提前这个过程加快)
做了这几篇文章,可以简单归纳伪动态:用其他脚本处理整合数据,前端用js处理。这里整合的载体是json,前面的EverydayNews载体是固定的文件夹结构,PSGameSpider则是混合,将数据直接写入定时更新生成的html的js部分数组里,读取则是靠固定文件夹结构。
说回主角,我把它在Github立了个项,放在Github@RavelloH/RPageSearch中,现在没有时间不会再去更新它,但是以后(也可能是很久以后)会陆续升级一下,加入模糊搜索等功能。