项目背景

基于win10自带的搜索系统,创建一个自己的搜索项目,类似于Everything。

Everything简介:
Everything是voidtools(中文主页)开发的一款文件搜索工具,官网描述为“基于名称实时定位文件和目录(Locate files and folders by name instantly)”。它体积小巧,界面简洁易用,快速建立索引,快速搜索,同时占用极低的系统资源,实时跟踪文件变化,并且还可以通过http或ftp形式分享搜索。

everything是利用操作系统记录的文件操作日志来进行搜索,特点是效率高,速度快,但是具有一定的局限性,只能用于NTFS文件系统。

技术栈

  • 所用技术:JavaFX,多线程,SQLite,pinyin4j
  • 所用环境:IDEA,Maven

项目功能

  • 指定搜索目录,显示该目录内的内容。
  • 使用多线程进行文件的比较,比较数据库和本地的差异,则更新数据库。
  • 可以根据文件名称进行文件的查询。
  • 支持汉语拼音询以及首字母查询。

项目展示

目录搜索

java 快速检索xml节点 java文件内容检索工具_java 快速检索xml节点


按照文件名搜索

java 快速检索xml节点 java文件内容检索工具_sql_02


按照拼音首字母搜索

java 快速检索xml节点 java文件内容检索工具_数据库_03

技术栈介绍

JavaFX:

  • JavaFX是一个强大的图形和多媒体处理工具包集合,它允许开发者来设计、创建、测试、调试和部署富客户端程序,并且和Java一样跨平台。
  • 内部有大量的UI控件和CSS支持。
  • 提供有可视化的布局设计工具Scene Builder,可以用于拖拽式的界面设计。
  • 有丰富的API,可以进行多种特效设计。

SQLite:

  • 轻量级数据库
  • 无需安装配置及管理
  • 支持多种主流开发语言
  • 采用动态数据类型
  • SQLite通过数据库级上的独占性和共享锁来实现独立事务处理

pinyin4j:

  • 使用方便,转换只需要使用PinyinHelper类的静态工具方法即可。
  • 支持多音字的输出。
  • 支持拼音声调的输出。

系统开发设计

数据库设计

设计目的:

  • 保存文件的基本信息:名称,修改时间,大小,路径
  • 当文件名为中文时,还需要保存拼音及其首字母方便后续查询

数据库代码:

drop table if exists file_meta; -- 保证数据库可以正常创立

create table if not exists file_meta( --创建表操作
    name varchar(50) not null,
    path varchar(1000) not null,
    is_directory boolean not null ,
    size bigint not null,
    last_modified timestamp not null,
    pinyin varchar(50),
    pinyin_first varchar(50)
);

数据库初始化

读取本地的SQL文件,并且进行数据库初始化的建表操作
使用IO流来读取数据并执行,需要考虑多行代码的读取以及SQL标注的问题。

public static String[] readSQL() {
        try {
            InputStream is = DBInit.class.getClassLoader().getResourceAsStream("ini.sql");
            BufferedReader br = new BufferedReader(new InputStreamReader(is,"UTF-8"));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null){
                if (line.contains("--")){//去掉注释的代码
                    line = line.substring(0,line.indexOf("--"));
                }
                sb.append(line);
            }
            String[] sqls =   sb.toString().split(";");
            return sqls;
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("读取时错误",e);
        }
    }

    public static void init(){
        Connection connection = null;
        Statement statement = null;
        try {
            connection = DBUtil.getConnection();
            statement = connection.createStatement();
            String[] sqls = readSQL();
            for (String sql :sqls){
                //System.out.println("打印SQL语句"+sql);
                statement.executeUpdate(sql);
            }
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException("初始化数据库表失败",e);
        }finally {
            DBUtil.close(connection,statement);
        }
    }

拼音工具类

由于拼音工具类为第三方库提供,因此我们在引入之后要进行设置,配置汉字的字符范围,以及拼音的输出格式。

public class Pinyin {

    private static final String CHINESE_PATTERN ="[\\u4E00-\\u9FA5]";
    public static final HanyuPinyinOutputFormat FORMAT = new HanyuPinyinOutputFormat();
    static {
        //拼音小写
        FORMAT.setCaseType(HanyuPinyinCaseType.LOWERCASE);
        //不带音调
        FORMAT.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
        //设置带V
        FORMAT.setVCharType(HanyuPinyinVCharType.WITH_V);
    }

    public static boolean containsChinese(String name){
        return name.matches(".*" + CHINESE_PATTERN + ".*");
    }
    public static String[] get(String name){
        String[] result = new String[2];
        StringBuilder all = new StringBuilder();
        StringBuilder first = new StringBuilder();

        for (char c:name.toCharArray()){
            try {
                String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c,FORMAT);
                if (pinyins == null|| pinyins.length == 0){
                    all.append(c);
                    first.append(c);
                }else {
                     all.append(pinyins[0]);
                     first.append(pinyins[0].charAt(0));
                }
            } catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
                all.append(c);
                first.append(c);
            }
        }
        result[0] = all.toString();
        result[1] = first.toString();
        return  result;
    }

    //多音字
    public static String[][] get(String name,boolean fullSpell){
        char[] chars = name.toCharArray();
        String[][] result = new String[chars.length][];
        for (int i = 0;i<chars.length;i++){
            try {
                String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(chars[i],FORMAT);
                if (pinyins == null || pinyins.length == 0){
                    result[i] = new String[]{String.valueOf(chars[i])};
                }else {
                    result[i] = unique(pinyins,fullSpell);
                    }
            } catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
                result[i] = new String[]{String.valueOf(chars[i])};
            }
        }
        return result;
    }

    public static String[] compose(String[][] pinyin){
        if (pinyin == null ||pinyin.length == 0){
            return null;
        }else if (pinyin.length == 1){
            return pinyin[0];
        }else {
            for (int i = 1;i<pinyin.length;i++){
                pinyin[0] = compose(pinyin[0],pinyin[i]);
            }
            return pinyin[0];
        }
    }

    public static String[] unique(String[] array,boolean fullspell){
        Set<String> set = new HashSet<>();
        for (String s : array){
            if (fullspell){
                set.add(s);
            }else {
                set.add(String.valueOf(s.charAt(0)));
            }
        }
        return set.toArray(new String[set.size()]);
    }
//多音字的拼接
    public static String[] compose(String[] pinyins1,String[] pinyins2){
        String[] result = new String[pinyins1.length * pinyins2.length];

        for (int i = 0;i<pinyins1.length;i++){
            for (int j = 0;j<pinyins2.length;j++){
                result[i*pinyins2.length+j] = pinyins1[i]+pinyins2[j];
            }
        }
        return result;
    }
}

事件监听

实现了javaFX的Initializable接口,进行界面初始化,进行事件监听,当内容改变时进行反馈

public class Controller implements Initializable {

    @FXML
    private GridPane rootPane;

    @FXML
    private TextField searchField;

    @FXML
    private TableView<FileMeta> fileTable;

    @FXML
    private Label srcDirectory;

    private Thread task;

    public void initialize(URL location, ResourceBundle resources) {
        DBInit.init();
        // 添加搜索框监听器,内容改变时执行监听事件
        searchField.textProperty().addListener(new ChangeListener<String>() {

            public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                freshTable();
            }
        });
    }

    public void choose(Event event) {
        // 选择文件目录
        DirectoryChooser directoryChooser=new DirectoryChooser();
        Window window = rootPane.getScene().getWindow();
        File file = directoryChooser.showDialog(window);
        if(file == null)
            return;
        // 获取选择的目录路径,并显示
        String path = file.getPath();

        srcDirectory.setText(path);

        if (task != null ){
            task.interrupt();
        }

        task = new Thread(new Runnable() {
            @Override
            public void run() {
                ScanCallback callback = new FileSave();
                FileScanner scanner = new FileScanner(callback);
                try {
                    System.out.println("执行文件扫描");
                    scanner.scan(path);
                    scanner.waitFinish();
                    System.out.println("任务执行完毕,刷新表格");
                    freshTable();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        task.start();
    }

    // 刷新表格数据
    private void freshTable(){
        ObservableList<FileMeta> metas = fileTable.getItems();
        metas.clear();
        String dir = srcDirectory.getText();
        if (dir != null && dir.trim().length() != 0){
            String content = searchField.getText();
            List<FileMeta> fileMetas = FileSearch.search(dir,content);
            metas.addAll(fileMetas);
        }
    }
 }

多线程扫描

选择多线程进行大量的任务,使用线程池来提高工作效率。
在线程执行时,待执行任务数+1, 执行完后,待执行任务数-1,开启子任务时,每个子任务都执
行任务数+1操作,这样在最后可以判断出是否所有线程执行完毕,当任务完成,调用线程等待方法,使线程阻塞等待,关闭线程池。计数器使用线程安全的incrementAndGet(),decrementAndGet()方法。

public class FileScanner {

    private ExecutorService pool = Executors.newFixedThreadPool(4);
    private  ScanCallback callback;
    private volatile AtomicInteger count = new AtomicInteger();
    private Object lock = new Object();

    public FileScanner(ScanCallback callback) {
        this.callback = callback;
    }

    public void scan(String path){
        count.incrementAndGet();
        doscan(new File(path));
    }


    public void doscan(File dir) {
        pool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    callback.callback(dir);
                    File[] children = dir.listFiles();
                    if (children != null) {
                        for (File child : children) {
                            if (child.isDirectory()) {
                                System.out.println("当前任务数" + count.get());
                                count.incrementAndGet();
                               doscan(child);
                            }
                        }
                    }
                }finally {
                    int r = count.decrementAndGet();
                    if (r == 0){
                        synchronized (lock){
                            lock.notify();
                        }
                     }
                }
            }
        });
    }

    public void  waitFinish()throws InterruptedException{
        try {
            synchronized (lock){
                lock.wait();
            }
        }finally {
            System.out.println("关闭线程池");
            pool.shutdownNow();

        }
    }
}

信息保存

将扫描到的信息进行保存到数据库中。
本地有,数据库没有,插入该文件信息;数据库有,本地没有,删除数据库中该文件信息。如果该文件是文件夹,还需要删除所有在该文件夹下的文件。

public class FileSave implements ScanCallback {

    public void callback(File dir){
        File[] children = dir.listFiles();
        List<FileMeta> locals = new ArrayList<>();
        if (children != null){
            for (File child : children){
                locals.add(new FileMeta(child));
            }
        }

        List<FileMeta> metas = query(dir);

        for (FileMeta meta : metas){
            if (!locals.contains(meta)){
                delete(meta);
            }
        }

        for (FileMeta meta : locals){
            if (!metas.contains(meta)){
                save(meta);
            }
        }
    }

    public void delete(FileMeta meta){
        Connection connection = null;
        PreparedStatement ps = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "delete from file_meta where" +
                    " (name=? and path=? and is_directory=?)";

            if (meta.getDirectory()){
                sql += " or path=?"+
                        " or path like ?";
            }
            ps = connection.prepareStatement(sql);
            ps.setString(1,meta.getName());
            ps.setString(2,meta.getPath());
            ps.setBoolean(3,meta.getDirectory());
            if (meta.getDirectory()){
                ps.setString(4,meta.getPath()+File.separator+meta.getName());
                ps.setString(5,meta.getPath()+File.separator+meta.getName()+File.separator);
            }
            System.out.printf("删除文件 dir = %s\n"+meta.getPath()+File.separator+meta.getName());
             ps.executeUpdate();
        }catch (Exception e){
            e.printStackTrace();
            throw  new RuntimeException("删除文件出错",e);

        }finally {
            DBUtil.close(connection,ps);
        }
    }

    private List<FileMeta> query(File dir){
        Connection connection = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        List<FileMeta> metas = new ArrayList<>();
        try{
            connection = DBUtil.getConnection();
            String sql = "select name,path,is_directory,size,last_modified"+
                    " from file_meta where path=?";
            ps = connection.prepareStatement(sql);
            ps.setString(1,dir.getPath());
            rs = ps.executeQuery();
            while (rs.next()){
                String name = rs.getString("name");
                String path = rs.getString("path");
                Boolean isDirectory = rs.getBoolean("is_directory");
                Long size = rs.getLong("size");
                Timestamp lastModified = rs.getTimestamp("last_modified");
                FileMeta
                meta = new FileMeta(name,path,isDirectory,size,new java.util.Date(lastModified.getTime()));
                System.out.printf("查询文件信息: name = %s, path = %s,is_directory = %s,size = %s,last_modified = %s\n",name,path,String.valueOf(isDirectory),String.valueOf(size),Util.parseDate(new java.util.Date(lastModified.getTime())));
                metas.add(meta);
            }
            return metas;

        }catch (Exception e){
            e.printStackTrace();
            throw  new RuntimeException("查询出错",e);
        }finally {
            DBUtil.close(connection,ps,rs);
        }
    }

    private void save(FileMeta meta){
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "insert into file_meta" +
                    "(name, path, is_directory, size, last_modified, pinyin, pinyin_first)"+
                    " values(?, ?, ?, ?, ?, ?, ?)";
            statement = connection.prepareStatement(sql);
            statement.setString(1,meta.getName());
            statement.setString(2,meta.getPath());
            statement.setBoolean(3,meta.getDirectory());
            statement.setLong(4,meta.getSize());
            statement.setString(5,meta.getLastModifiedText());
            String pinyin = null;
            String pinyin_first = null;

            if (Pinyin.containsChinese(meta.getName())){
                String[] pinyins = Pinyin.get(meta.getName());
                pinyin = pinyins[0];
                pinyin_first = pinyins[1];
            }
            statement.setString(6,meta.getPinyin());
            statement.setString(7,meta.getPinyinFirst());

            System.out.println("执行文件保存操作:"+ sql);

            statement.executeUpdate();
        }catch (SQLException e){
            throw new RuntimeException("文件保存失败",e);
        }finally {
            DBUtil.close(connection,statement);
        }
    }
}

文件搜索

根据输入值进行匹配,部分字符,拼音,拼音首字母,只要有匹配成功就显示文件信息。

public static List<FileMeta> search(String dir, String content) {
        List<FileMeta> metas = new ArrayList<>();
        Connection connection = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "SELECT name, path, is_directory, size, last_modified"+
                    " from  file_meta where"+
                    " (path=? or path like ?)";
            if (content != null && content.trim().length() != 0){
                sql += " and (name like ? or pinyin like ? or pinyin_first like ?)";
            }
            ps = connection.prepareStatement(sql);

            ps.setString(1,dir);
            ps.setString(2,dir + File.separator + "%");
            if (content != null && content.trim().length() != 0){
                ps.setString(3,"%" + content + "%");
                ps.setString(4,"%" + content + "%");
                ps.setString(5,"%" + content + "%");
            }
            rs = ps.executeQuery();
            while (rs.next()){
                String name = rs.getString("name");
                String path = rs.getString("path");
                Boolean isDirectory = rs.getBoolean("is_directory");
                Long size = rs.getLong("size");
                Timestamp lastModified = rs.getTimestamp("last_modified");
                FileMeta meta = new FileMeta(name,path,isDirectory,size,new java.util.Date(lastModified.getTime()));
                metas.add(meta);
            }

        }catch (Exception e){
            throw new  RuntimeException("文件查询失败路径" +dir+"内容"+ content,e);
        }finally {
            DBUtil.close(connection,ps,rs);
        }
        return metas;
    }

项目总结

项目优点:
使用了多线程的方法来进行文件的遍历,提高了搜索的效率。

项目缺点:
使用起来不是很方便,搜索方面比较僵硬