文字列一括探索ツール『NoctisVerba』 ソースコード

『NoctisVerba(ノクティス・ヴェルバ)』は、選択フォルダ内のテキストファイル群から特定の文字列を探し出す、高機能な検索ツールです。

>>GUI外観 >>使い方

以下は『NoctisVerba』のソースコード全文です:

 
        import os
        import tkinter as tk
        from tkinter import filedialog, messagebox, scrolledtext
        from datetime import datetime
        import re
        
        class TextSearchApp:
            def __init__(self, master):
                self.master = master
                self.master.title("文字列一括探索ツール - NoctisVerba")
                self.folder_path = ""
                self.ext_vars = {}
                self.result_lines = []
        
                self.create_widgets()
        
            def create_widgets(self):
                # チェックボックスの変数(先に定義)
                self.case_insensitive = tk.BooleanVar()
                self.ignore_spaces = tk.BooleanVar()
                self.include_subfolders = tk.BooleanVar()
        
                # フォルダ選択
                tk.Button(self.master, text="1) フォルダ選択", command=self.select_folder).grid(row=0, column=0, padx=5, pady=5, sticky='w')
                self.folder_label = tk.Label(self.master, text="未選択", width=60, anchor='w')
                self.folder_label.grid(row=0, column=1, columnspan=3, sticky='w')
        
                # 拡張子一覧表示フレーム
                self.ext_frame = tk.LabelFrame(self.master, text="2) 拡張子を選択:")
                self.ext_frame.grid(row=1, column=0, columnspan=4, padx=5, pady=5, sticky='we')
        
                # 検索文字列入力
                tk.Label(self.master, text="3) 検索文字列:").grid(row=2, column=0, sticky='w', padx=5)
                self.search_entry = tk.Entry(self.master, width=40)
                self.search_entry.grid(row=2, column=1, columnspan=2, sticky='w')
        
                # オプション
                tk.Label(self.master, text="4) オプション:").grid(row=3, column=0, sticky='w', padx=5)
                tk.Checkbutton(self.master, text="大文字・小文字を区別しない", variable=self.case_insensitive).grid(row=4, column=0, columnspan=4, sticky='w', padx=20)
                tk.Checkbutton(self.master, text="半角スペースを無視する", variable=self.ignore_spaces).grid(row=5, column=0, columnspan=4, sticky='w', padx=20)
                tk.Checkbutton(self.master, text="サブフォルダを含める", variable=self.include_subfolders).grid(row=6, column=0, columnspan=4, sticky='w', padx=20)
                tk.Button(self.master, text="拡張子を再取得", command=self.refresh_extensions).grid(row=6, column=1, padx=5, pady=5, sticky='w')
                
                # 検索&エクスポートボタン
                tk.Button(self.master, text="5) 検索開始", command=self.search).grid(row=7, column=0, padx=5, pady=5, sticky='w')
        
                # 結果表示欄
                self.result_text = scrolledtext.ScrolledText(self.master, width=80, height=20)
                self.result_text.grid(row=8, column=0, columnspan=4, padx=5, pady=5)
        
                # エクスポートボタン
                tk.Button(self.master, text="6) 結果をエクスポート", command=self.export_results).grid(row=9, column=0, padx=5, pady=5, sticky='w')
        
        
            def select_folder(self):
                path = filedialog.askdirectory()
                if not path:
                    return
                self.folder_path = path
                self.folder_label.config(text=path)
                self.refresh_extensions()
                    
            def refresh_extensions(self):
                if not self.folder_path:
                    messagebox.showwarning("注意", "先にフォルダを選択してください。")
                    return
                ext_count = {}
                if self.include_subfolders.get():
                    for root, _, files in os.walk(self.folder_path):
                        for f in files:
                            ext = os.path.splitext(f)[1].lower()
                            if ext:
                                ext_count[ext] = ext_count.get(ext, 0) + 1
                else:
                    for f in os.listdir(self.folder_path):
                        full_path = os.path.join(self.folder_path, f)
                        if os.path.isfile(full_path):
                            ext = os.path.splitext(f)[1].lower()
                            if ext:
                                ext_count[ext] = ext_count.get(ext, 0) + 1
            
                # 拡張子チェックボックス再生成
                for widget in self.ext_frame.winfo_children():
                    widget.destroy()
                self.ext_vars = {}
                col = 0
                for i, (ext, count) in enumerate(sorted(ext_count.items())):
                    var = tk.BooleanVar(value=True)
                    self.ext_vars[ext] = var
                    cb = tk.Checkbutton(self.ext_frame, text=f"{ext} ({count})", variable=var)
                    cb.grid(row=i // 4, column=col, sticky='w')
                    col = (col + 1) % 4
        
            def search(self):
                keyword = self.search_entry.get()
                if not self.folder_path or not keyword:
                    messagebox.showwarning("注意", "フォルダと検索文字列を指定してください。")
                    return
        
                flags = re.IGNORECASE if self.case_insensitive.get() else 0
                keyword_pattern = keyword.replace(" ", "") if self.ignore_spaces.get() else keyword
                pattern = re.compile(re.escape(keyword_pattern), flags)
        
                selected_exts = [ext for ext, var in self.ext_vars.items() if var.get()]
                if not selected_exts:
                    messagebox.showwarning("注意", "少なくとも1つの拡張子を選択してください。")
                    return
        
                self.result_lines = []
                self.result_lines.append(f"検索語句:「{keyword}」")
        
                # ファイル走査(サブフォルダ含める/含めない)
                if self.include_subfolders.get():
                    for root, _, files in os.walk(self.folder_path):
                        for file in files:
                            filepath = os.path.join(root, file)
                            ext = os.path.splitext(file)[1].lower()
                            if ext in selected_exts:
                                self._search_file(filepath, pattern)
                else:
                    for file in os.listdir(self.folder_path):
                        filepath = os.path.join(self.folder_path, file)
                        if os.path.isfile(filepath):
                            ext = os.path.splitext(file)[1].lower()
                            if ext in selected_exts:
                                self._search_file(filepath, pattern)
        
                # 結果出力
                self.result_text.delete('1.0', tk.END)
                if self.result_lines:
                    for line in self.result_lines:
                        self.result_text.insert(tk.END, line + '\n')
                else:
                    self.result_text.insert(tk.END, "該当なし\n")
        
            def _search_file(self, filepath, pattern):
                try:
                    with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
                        for i, line in enumerate(f, start=1):
                            check_line = line.replace(" ", "") if self.ignore_spaces.get() else line
                            if pattern.search(check_line):
                                rel_path = os.path.relpath(filepath, self.folder_path)
                                result = f"{rel_path} : {i}行目"
                                self.result_lines.append(result)
                except Exception as e:
                    print(f"読み込みエラー: {filepath} ({e})")
        
            def export_results(self):
                folder_name = os.path.basename(self.folder_path.rstrip("/\\"))
                now = datetime.now().strftime("%Y%m%d-%H%M%S")
                file_name = f"{folder_name}-{now}.txt"
                desktop = os.path.join(os.path.expanduser("~"), "Desktop")
                output_path = os.path.join(desktop, file_name)
        
                try:
                    with open(output_path, 'w', encoding='utf-8') as f:
                        for line in self.result_lines:
                            f.write(line + '\n')
                    messagebox.showinfo("完了", f"検索結果を保存しました:\n{output_path}")
                except Exception as e:
                    messagebox.showerror("エラー", f"保存に失敗しました:{e}")
        
        # 実行
        if __name__ == "__main__":
            root = tk.Tk()
            
            ##タイトルバーアイコン表示用
            data = '''R0lGODdhEAAQAIcAAKz//6X7/5f//5L//2vI20HB65CKkYKJkQqn7AWW4hCG2Q6G2QWBxQp2yw13sRR0rhBwpQxuxA9uoWNeZmNbYypbeBBimw5rpAxdkgxWjgpnowpfkwpdkAtbkQlkqwFopwhbnAhZjAhYkTtJVCVHZQtMcgpQfQlVgglTiglIcQhVkAhPgQhLdwZSkQZRigdPgQdMhAdIfgVIfARDeQdEby8vPA8uTQ4vUQs7Ygo3YAo2XghCagg7Ygg1WwY9bQU5bAU4YgU3YwYzZwUwYAUvWBMrRAsrTAomRBckNwksTgcpTAcmRgUsVAUkRAYjQwMqWAMqVgQrUQIlUAQlRwQkRQQjRAMjRgIjRhIfMgsiPgsiPAghPAggOwciQAcgPAUhPwYgPwUgPgYfPQIiTwEhTAQiQwQhQwQgQAMfQQIfQQseNwseNRMZKA8ZKggeOAkYLggYLgccNgcaNAcYMgYbNQYaMwYZMwUZNAYYMwUYMwUYMQQePAMcPAQcOAMbOQIbOgIaOQQZNAQZMwIZNwAZPAQYMwQYMhQWJhAVJg8XKQ0WJwsXKwoWKgkVKQoTJAgXLgcWLAgUKQcUKgcUKQcTKQYWLwYWLgYVLAYUKgYTKQYTKAwRIAkRIwgSJQgRJAgRIwgQIggQIQkPHwgPIAYSKAYSJwcSJQcRJQcQIwYQJAcPIQYPIwYPIgMXNgMWMgQXMAUVLwMVLwIVMAUULAUTKgQULAMTLQMSKwQRKAQRJwUQJgUQJQIQKwUQIwUPJAUPIgIPKQAPJwkOHggOHwgNHQcOIAcOHwcNHgcNHQgMHAgMGwcMHQcMHAcMGwYOIgYOIQYOIAYNIAYMHgYMHQYMHAUOIgUNHwQMIwQMIgUMHgAOJggLGwgLGQcLGwgKGAcKGQcKGAYLGwYKGwQLIAcJGAcJFwcIFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAEAAQAEAI/wC9gWO2TJq0Snl0YRvnTFo3cNyO9TpiJMwfBx5AqKjSZ4mdZ8qYZYMTx1CaF1IkrOCw45YgVMmILdKCpAIBAAMCCChAYlMuT8mYRZt1x1YTIiHIWOjAy5c4cuWO7dITB5YJFw8yyADi6kuXU8ymmSriJRAaIQoiDDkTRQeXSaGIgRoFScwVQsG04UpEa40iVcPMfZsmrRo2Myda9eBxjVW4ct+oWbtEB1gJCGMwxIDCpxatVNu6lXr1SlaKIDR+PNkwJVahVcyK/cpyY4sVGC1maGAxiEoZTMmQafJjyQ0TEQ0W+ACjJMmbT82gPbLhBMcFBggSfECRA9AeR8KMQSTKxAZRDQMTKBwYcUhOnVDHOHWKxKgNFjWU5uCZZamRJFKiBAQAOw==''' 
            root.tk.call('wm', 'iconphoto', root._w, tk.PhotoImage(data=data))
            ####
            
            app = TextSearchApp(root)
            root.mainloop()
        
        
       
    

📦 ダウンロード 🏠 ホームへ