『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()