RavelloH's Blog

LOADing...



静态站实现全站搜索

static-site-implements-sitewide-search

技术/设计 12553

javascriptjsonpython


RTheme v4 使用了基于数据库的后端搜素,本文已过时

前言

全站搜索这一功能我想加入到我的博客中不是一年两年的事了。但因自己现在弃用 Hexo 转而自己做博客,这两年搜索这个功能就一直未能实现。 最近自己偶然有新想法,就给实现了。效果还不错,现在搭配 Github Actions 使用,可以实现新文章自动索引,实现了自动化。

效果

搜索示例

show1

要想充分体验,还是自己去试试的好。转到 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>'
}

当然,上述代码比复杂,接下来我会分步来说。

代码分析

  • 前 3 行没什么好说的,找到对应的元素,方便后期处理。
  • 第 5 行从生成的 json 内获取数据,之后从第六行开始是主函数,搭配 html 表单使用,效果是当输入时实时搜索。
  • 7-10 行判断输入框是否为空,若空替换为默认提示词。
  • 12-21 行遍历 json 中存储的标题,查找是否有相关字词,若有,为 resultcount+1,并将完整标题加入到列表 resulttitlecache 中
  • 23-34 行用于展示搜索结果,条件是 resultcount 不为 0(要能找得到结果才继续,节省运算量)且不为 json 中文章总数(若输入一些字符后全部删除,默认输入了"",所有文章都会有反馈。),里面是两个遍历组合,第一个遍历 resulttitlecache 中的有结果的标题,第二个遍历总数据,搜索到契合后即可确认总数据的路径、时间、正文、标题信息,然后整合存储到 resultstr 中。这样因为我们在 python 生成数据时就是按时间顺序排列的,在这里遍历也是时间最近的结果优先。同时,第一层遍历将搜索到的 resultstr 中的信息整合到 html 中展示结果的 result 元素(一个 id 为 resultlist 的 div)中。这里也用 js 替换高亮了信息里面的关键字,会给匹配的字符添加到一个 mark 标签内(默认黄底白字,可以用 css 改,比如我的博客就改成了蓝底另外加了个圆角),另外也会截取正文前 100 字符用于展示,但是因为字符宽度不一显示起来不整齐,要解决可以用 css 限制行数,css 我会放在文章下面。
  • 36-43 行与 12-21 行相似,将搜索匹配的结果存储在 resulttextcache 中,不在赘述。
  • 45-57 行也类似于 23-34 行,为两个嵌套的遍历,不同的是这个不写入,而是记录搜索结果中含关键词的总数,方便后续排序。具体做法是创建两个列表 targetname 和 targetscore,targetname 记录 resulttextcache 中的正文内容,targetscore 记录对应含关键词的数量。这两个是一一对应的关系,比如 targetname 的第二项所含的关键词总数就等于 targetscore 第二项记录的数字。
  • 59-84 行花了大功夫排序,首先创建一个 targetscore 的备份 targetscorecache,但是要深拷贝*关乎 js 数组知识,若只是简单的 var a=b 则 a b 共享存储位置,换句话说就是 a 变 b 也变,对一维数组可以简单的使用 a=b.concat([])来解决,之后以数组 sort 方式排序,此时 targetscorecache 为降序排列,targetscore 保持原顺序不变,之后套三层遍历(应该还能继续优化,但我懒得做了,又不是不能用,性能影响也不大)前两层嵌套和之前搜索匹配的 23-34、45-57 相同,不过这次匹配的是 targetscorecache 与 targetscore,之后再从总数据到 json 种找到正文匹配的文章,获取到对应的 path、title、time 和 text,然后是类似于 23-34 到写入过程,这里值得注意有三点,一是这次高亮的是 text 中的结果,2 是这里也会用上作为排序依据的 targetscore,在标题下方展示相应的结果数(见最上方用于展示的图片),三是这里生成的 html 代码不直接写入而是存储到变量 textsearchresult 中,方便最后整合的时候删掉多余的 hr 标签。
  • 86-91 给上面接底,如果啥也没找到就替换为相应的文本。
  • 最后 92-93 用来整合、合并标题搜索与正文搜索的内容,顺便加上个 powered by 的后缀,这里你也可以替换为你自己的文字,或者干脆连前面的 hr 标签也一起删掉,但我还是希望能留下个注释标明出处。

上述就是 js 进行本地运算的过程,至于若想达到和我博客相同的效果,css 是必不可少的,这里简单罗列一下 css 及相关的 js。

<!-- 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中,现在没有时间不会再去更新它,但是以后(也可能是很久以后)会陆续升级一下,加入模糊搜索等功能。

INFO

00:00


无正在播放的音乐
00:00/00:00

账号
User avatar
未登录未设置描述...