文件海中一束光(文件快速搜索)
Posted It‘s so simple
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了文件海中一束光(文件快速搜索)相关的知识,希望对你有一定的参考价值。
目录
项目源码:https://github.com/mxzw/Project/tree/main/DataFastSearch
项目开发背景
windows下文件夹框下的默认搜索是搜索时再进行暴力遍历查找,非常的慢,通常我们在windows下要查找一个文件需要花费大量的时间,尤其是当我们不知道具体的文件名时,只知道大概的、模糊的名称时,它的搜索速度会非常的慢。如下:
而在Linux环境下则有非常好用的find
命令就可以快速的对当前想搜索的文件进行定位。
那么windows环境下能快速对文件进行检索吗?答案肯定是有的,也是我们大多数人都爱用的神器:everything,它是将文档信息检索以后,提前存储到数据库,查找时在数据库进行搜索,搜索速度就快了很多。
但是对于我而言,everything的搜索速度确实快,但是它对于特定的功能并没有实现,比如说everything它不支持拼音搜索和首字母搜索。
因此,基于此原因,我就想能不能利用自己所学的知识写一个文件快速搜索的项目,所以才有了本项目的诞生,哈哈哈哈。
项目需求 & 开发环境
综合上述开发背景而言,本项目的项目需求为:
- 能够支持文档的普通搜索
- 能够支持文档拼音的全拼搜索
- 能够支持文档拼音的首字母搜索
- 能够支持搜索关键字高亮的功能
开发环境为:VS2019,使用C++语言进行开发,并且采用Sqlite数据库进行数据的存储。
为什么不用MySQL数据库而是采用SQLite数据库?
因为SQLite是一个无服务器的、零配置的、事务性的 SQL 数据库,其实我选择该数据库的最主要原因是它是一个零配置的数据库,这意味着与其他数据库不一样,您不需要在系统中配置。我们完全可以不需要对其进行安装和管理,并且它是非常小的,是一个轻量级的数据库,因为本项目只会单纯的在数据库中创建出一张表来存储文件信息,并不需要其他的表来对文件信息进行维护,对比之下,mysql数据库还是太过重量级了,不太适合本项目,因此我采用SQLite数据库。其实还有一个原因就是它提供了简单和易使用的API接口,使我能够快速的对其进行理解和使用。
项目设计
其实整个项目的整体思路非常简单,就是首先根据我们所给出的路径,扫描该路径下的文件信息并存入数据库中,然后根据用户输入的关键字,将对应关键字转为拼音全拼和拼音首字母,在数据库中进行相应逻辑的搜索,然后在打印时对搜索关键字进行高亮即可。
项目实现
根据上面的设计,我们可以先建立代码的框架
- 公共管理模块:Common.h
- 系统工具模块:Sysutil.h、Sysutil.cpp
- 系统界面模块:SysFrame.h SysFrame.cpp
- 数据管理模块: DataManager.h DataManager.cpp
- 数据库管理模块:SqliteManager.h SqliteManager.cpp
- 扫描管理模块: FileScanSync.h FileScanSync.cpp
- 系统驱动模块: DocFastSearch.cpp
具体的实现请看源码,下面我来具体的对本项目的主要逻辑的实现来进行说明。
1. 本地文件扫描的实现
对于扫描我们当前电脑上某个路径下的文件,C语言有一个专门的结构体来存放这些文件信息,以及专门对这些文件信息进行操作的函数。
struct _finddata_t
{
unsigned attrib;
time_t time_create;
time_t time_access;
time_t time_write;
_fsize_t size;
char name[_MAX_FNAME];
};
//通过给定的文件路径,将对于的文件信息存储在fileinfo中,
//并返回一个对应文件的操作句柄(long),失败会返回-1
long _findfirst( char *filespec, struct _finddata_t *fileinfo );
//该函数用于对当前文件的操作句柄进行一个向后的变量(相当于指针++操作)
//成功会返回0。
int _findnext( long handle, struct _finddata_t *fileinfo );
//该函数用于关闭_findfirst函数创建出的操作句柄。
int _findclose( long handle );
因此,在扫描给的目录的时候,我们可以将扫描到的结果存储在一个vector中,以后用于同步到数据库。
void FileList(const string& filepath, vector<string>& subfile,
vector<string>& subdir)
{
string path = filepath;
path += "\\\\*.*"; //表示当前路径下所有文件
//C提供的用来存储文件各种信息的结构体,可以根据路径访问本地文件。
_finddata_t file;
long handle = _findfirst(path.c_str(), &file);
if (handle == -1)
{
fprintf(stderr, "Scan File Error\\n");
return;
}
//循环的获取目录下的文件,并将其存放到数组中
do
{
if (file.name[0] == '.')
{
//隐藏文件
continue;
}
//如果是文件夹则到子目录数组中,否则就到文件数组中
if (file.attrib & _A_SUBDIR)
subdir.push_back(file.name);
else
subfile.push_back(file.name);
//使用_findnext函数来找寻下一个文件
} while (_findnext(handle, &file) == 0);
_findclose(handle);
}
2. 数据库文件存储的实现
这里我们直接将对数据库的操作封装成一个类,方便以后在文件同步过程中的调用。
#pragma once
#include "Common.h"
#include "Log.hpp"
class SqliteManager
{
public:
SqliteManager();
~SqliteManager();
public:
void Open(const string& path);
void Exec(const string& sql);
void Close();
void GetResultTable(const string& sql,char **& ppRet,
int &row,int &col);
private:
sqlite3* con_;
};
3. 本地文件和数据库同步的实现
本地文件和数据库文件中所包含的结果我们可以通过上述两个模块拿到。但是该如何实现它们之间的同步呢?我们可以使用 multiset来存储两个模块中的文件的信息,由于multiset底层是RBTree,因此在用迭代器去访问它们的时候,这些文件均是有序的,并且该结构还具有多重的属性,也就是支持文件名相同的情况,这种情况可能会出现在不同文件夹下具有相同的文件名的情形下,因此要实现数据库和本地文件的同步,我们可以通过比较两个multiset 的迭代器来进行相应信息的更新。
我们首先假设本地文件的迭代器为local_it
,数据库的迭代器为db_it
。
- 当
*local_it
<*db_it
的时候,说明本地有新增的文件,需要向数据库中增加该文件.
- 当
*local_it
>*db_it
的时候,说明本地有删除的文件,需要在数据库中删除该文件.
- 当
*local_it
==*db_it
的时候,说明本地和数据库均有该文件,则迭代器++,进行下一步判断。
由此,得出的代码为:
void ScanManager::ScanFileToSync(const string& path)
{
//扫描本地文件并放入多重集合中
vector<string> local_sub_files;
vector<string> local_sub_dirs;
FileList(path, local_sub_files, local_sub_dirs);
//一定要将两个数组中的文件全部插入,用于进行同步
multiset<string> local_set;
local_set.insert(local_sub_files.begin(), local_sub_files.end());
local_set.insert(local_sub_dirs.begin(), local_sub_dirs.end());
//获取数据库中结果
multiset<string> dataBase_set;
DataManager& dm_ = DataManager::CreateDataManager();
dm_.GetFiles(path, dataBase_set);
//进行同步
// 同步的原理是对两个multiset集合的迭代器进行对比,
//因为multiset的底层是红黑树,
// 所以在对比的时候只需对其迭代器进行对比即可。
// 如果当local < dataBase的时候,说明本地新增了文件,
//需在数据库中执行更新的操作
// 如果当 local > dataBase的时候,说明本地删除了当前数据库所指文件,
//因此需在数据库中执行删除的操作
// 如果 local == dataBase,说明本地和数据库中该文件均存在,
//向后移动迭代器即可。
multiset<string>::iterator local_it = local_set.begin();
multiset<string>::iterator dataBase_it = dataBase_set.begin();
while (local_it != local_set.end() &&
dataBase_it != dataBase_set.end())
{
if (*local_it < *dataBase_it)
{
dm_.InsertFile(path, *local_it);
++local_it;
}
else if (*local_it > *dataBase_it)
{
dm_.DeleteFile(path, *dataBase_it);
++dataBase_it;
}
else
{
++local_it;
++dataBase_it;
}
}
//可能存在数据库先遍历完,则将剩下的本地文件插入即可
while (local_it != local_set.end())
{
dm_.InsertFile(path, *local_it);
++local_it;
}
//可能存在本地文件先遍历完,则将剩下的数据库文件删除即可
while (dataBase_it != dataBase_set.end())
{
dm_.DeleteFile(path, *dataBase_it);
++dataBase_it;
}
//然后开始扫描子目录
//扫描子目录只需获取local_sub_dirs中的结果,递归调用即可。
for (const auto& e : local_sub_dirs)
{
string dir_path = path;
dir_path += "\\\\";
dir_path += e;
ScanFileToSync(dir_path);
}
}
4. 汉字转拼音全拼和汉字转拼音首字母的实现
汉字转拼音全拼和汉字转拼音首字母:本功能的实现是参照网上的代码来进行实现的,具体可看C/C++ 汉字转拼音,具体的代码可看我上面贴的源码链接。
下面给出实际运行时的演示:
① 汉字转拼音全拼:
② 汉字转拼音首字母:
5. 关键字高亮处理
首先,我们先给出对关键字进行高亮处理的函数:
void ColourPrint(const char* str)
{ //windows.h
// 0-黑 1-蓝 2-绿 3-浅绿 4-红 5-紫 6-黄 7-白 8-灰 9-淡蓝 10-淡绿
// 11-淡浅绿 12-淡红 13-淡紫 14-淡黄 15-亮白
//颜色:前景色 + 背景色*0x10
//例如:字是红色,背景色是白色,即 红色 + 亮白 = 4 + 15*0x10
WORD color = 11 + 0 * 0x10;
WORD colorOld;
HANDLE handle = ::GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(handle, &csbi);
colorOld = csbi.wAttributes;
SetConsoleTextAttribute(handle, color);
printf("%s", str);
SetConsoleTextAttribute(handle, colorOld);
}
其次,我们想要的应该是只有关键字的那一部分高亮,因此我们就需要对字符串进行划分,将搜索出来的字符串划分为前缀 + 关键字 + 后缀。
这里只给出字符串划分的主要逻辑,具体代码见前面的源码链接。
因此对应三种情况:汉字搜索、全拼搜索、首字母搜索
①汉字搜索:这个我们可以直接利用string类的find函数进行查找,可以简单的划分出前缀、关键字和后缀。
②全拼搜索:首先我们需要将对应的关键字和搜索结果全部转为拼音全拼,然后调用find函数找到对应关键字的下标,然后对原搜索结果进行截取,注意不是截取转为全拼后的搜索结果;具体如图所示:
说具体点就是就是将汉字转换成拼音全拼和查找的位置相比较,如果长度达到了查找的位置就开始截取关键字,直到截取到汉字转换为拼音的数量和关键字转换成全拼的数量相同的时候即可。③ 首字母搜索:原理同上
以上是关于文件海中一束光(文件快速搜索)的主要内容,如果未能解决你的问题,请参考以下文章