liuyuqi-cnb 1 day ago
parent
commit
148cdff04a
5 changed files with 503 additions and 13 deletions
  1. 69 0
      .github/workflows/build-windows.yml
  2. 78 10
      README.zh-CN.md
  3. 348 0
      gui.py
  4. 4 2
      searchdomain/generate_domain.py
  5. 4 1
      searchdomain/generate_en_domain.py

+ 69 - 0
.github/workflows/build-windows.yml

@@ -0,0 +1,69 @@
+name: Build Windows Executable
+
+on:
+  push:
+    tags:
+      - 'v*'
+  workflow_dispatch:
+    inputs:
+      version:
+        description: 'Version tag'
+        required: false
+        default: 'latest'
+
+jobs:
+  build-windows:
+    runs-on: windows-latest
+    
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v4
+      
+    - name: Set up Python
+      uses: actions/setup-python@v5
+      with:
+        python-version: '3.11'
+        
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install -r requirements.txt
+        pip install pyinstaller
+        
+    - name: Create data directory structure
+      run: |
+        if not exist "dist\data" mkdir dist\data
+        copy data\a.csv dist\data\
+        copy data\b.csv dist\data\
+        
+    - name: Build GUI executable
+      run: |
+        pyinstaller --clean --noconfirm gui.spec
+        
+    - name: Build CLI executable
+      run: |
+        pyinstaller --clean --noconfirm main.spec
+        
+    - name: Create release package
+      run: |
+        if not exist "release" mkdir release
+        copy dist\searchdomain_gui.exe release\
+        copy dist\searchdomain_cli.exe release\
+        copy data\a.csv release\data\
+        copy data\b.csv release\data
+        
+    - name: Upload artifacts
+      uses: actions/upload-artifact@v4
+      with:
+        name: windows-executables
+        path: release/
+        
+    - name: Create Release
+      if: startsWith(github.ref, 'refs/tags/')
+      uses: softprops/action-gh-release@v1
+      with:
+        files: release/*
+        draft: false
+        prerelease: false
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 78 - 10
README.zh-CN.md

@@ -2,8 +2,38 @@
 
 域名批量检索可用
 
-## Develop
+## 使用方式
 
+### GUI 图形界面(推荐)
+
+Windows 用户可以直接使用打包好的可执行文件:
+
+1. **下载 Windows 可执行文件**
+   - 从 [GitHub Releases](https://github.com/your-repo/SearchDomain-Python/releases) 下载最新版本
+   - 解压后运行 `searchdomain_gui.exe`
+
+2. **使用 GUI 界面**
+   - 打开 `searchdomain_gui.exe` 后会显示图形界面
+   - **生成域名**标签页:配置参数后点击"开始生成"
+     - 选择语言(中文/英文)
+     - 输入关键词(用逗号分隔,如:gpt,go,ai)
+     - 输入域名后缀(用逗号分隔,如:com,cn,io)
+     - 选择关键词位置(前缀/后缀)
+     - 设置输出文件名
+   - **搜索域名**标签页:配置参数后点击"开始搜索"
+     - 选择输入文件(域名列表文件)
+     - 设置输出文件(结果保存位置)
+     - 选择是否导出所有域名(包括不可用的)
+   - 运行日志会实时显示在界面下方
+
+3. **从源码运行 GUI**
+   ```bash
+   python gui.py
+   ```
+
+### 命令行模式
+
+#### 开发环境设置
 
 ```
 virtualenv .venv
@@ -13,13 +43,13 @@ pip install -r requirements.txt
 
 1、批量生成域名
 ```
-python main.py generate
-python main.py generate_en
+python main.py generate --lang zh --keyword gpt,go --position prefix --domain io,com,cn
+python main.py generate --lang en --keyword chat,ai --position prefix --domain com,cn,io
 ```
 
 2、批量检测域名是否可以注册,并将结果保存到数据库或文件
 ```
-python main.py search
+python main.py search --input domain.txt --output result.txt
 ```
 
 
@@ -44,9 +74,47 @@ searchdomain --input domain.txt --output result.txt
 
 ```
 
-打包:
-```
-mkdir -p dist
-cp domain.txt.txt dist/
-pyinstaller  -F -c  --name search_domain  main.py
-```
+## 打包
+
+### 本地打包
+
+#### Windows 打包
+
+1. **安装依赖**
+   ```bash
+   pip install -r requirements.txt
+   pip install pyinstaller
+   ```
+
+2. **打包 GUI 版本**
+   ```bash
+   pyinstaller --clean --noconfirm gui.spec
+   ```
+   生成的可执行文件在 `dist/searchdomain_gui.exe`
+
+3. **打包 CLI 版本**
+   ```bash
+   pyinstaller --clean --noconfirm main.spec
+   ```
+   生成的可执行文件在 `dist/searchdomain_cli.exe`
+
+### GitHub Actions 自动打包
+
+项目已配置 GitHub Actions 工作流,可以自动打包 Windows 可执行文件:
+
+1. **触发方式**
+   - 推送版本标签(如 `v1.0.0`)会自动触发构建
+   - 或在 GitHub Actions 页面手动触发 `workflow_dispatch`
+
+2. **构建产物**
+   - 构建完成后可在 Actions 页面下载 `windows-executables` 工件
+   - 如果推送了版本标签,会自动创建 Release 并上传可执行文件
+
+3. **工作流文件位置**
+   - `.github/workflows/build-windows.yml`
+
+### 打包说明
+
+- GUI 版本:`searchdomain_gui.exe` - 图形界面,双击运行
+- CLI 版本:`searchdomain_cli.exe` - 命令行界面,在命令行中使用
+- 数据文件:`data/a.csv` 和 `data/b.csv` 会自动包含在打包文件中

+ 348 - 0
gui.py

@@ -0,0 +1,348 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+"""
+@Contact :   liuyuqi.gov@msn.cn
+@Time    :   2024/12/19
+@License :   Copyright © 2017-2022 liuyuqi. All Rights Reserved.
+@Desc    :   GUI界面入口
+"""
+
+import tkinter as tk
+from tkinter import ttk, scrolledtext, filedialog, messagebox
+import threading
+import sys
+import os
+from collections import OrderedDict
+from searchdomain import SearchDomain, GenerateDomain, GenerateEnDomain
+from searchdomain.utils.frozen_dir import get_app_path
+
+
+class SearchDomainGUI:
+    def __init__(self, root):
+        self.root = root
+        self.root.title("域名批量检索工具")
+        self.root.geometry("800x700")
+        self.root.resizable(True, True)
+        
+        # 设置应用路径
+        self.app_path = get_app_path()
+        
+        # 创建主界面
+        self.create_widgets()
+        
+        # 运行状态
+        self.is_running = False
+        
+    def create_widgets(self):
+        # 创建Notebook(标签页)
+        notebook = ttk.Notebook(self.root)
+        notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
+        
+        # 生成域名标签页
+        self.generate_frame = ttk.Frame(notebook)
+        notebook.add(self.generate_frame, text="生成域名")
+        self.create_generate_tab()
+        
+        # 搜索域名标签页
+        self.search_frame = ttk.Frame(notebook)
+        notebook.add(self.search_frame, text="搜索域名")
+        self.create_search_tab()
+        
+        # 日志输出区域(共用)
+        log_frame = ttk.LabelFrame(self.root, text="运行日志")
+        log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
+        
+        self.log_text = scrolledtext.ScrolledText(log_frame, height=10, wrap=tk.WORD)
+        self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
+        
+        # 状态栏
+        self.status_var = tk.StringVar(value="就绪")
+        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN)
+        status_bar.pack(side=tk.BOTTOM, fill=tk.X)
+        
+    def create_generate_tab(self):
+        # 语言选择
+        lang_frame = ttk.LabelFrame(self.generate_frame, text="语言设置")
+        lang_frame.pack(fill=tk.X, padx=10, pady=5)
+        
+        self.lang_var = tk.StringVar(value="en")
+        ttk.Radiobutton(lang_frame, text="中文", variable=self.lang_var, value="zh").pack(side=tk.LEFT, padx=10, pady=5)
+        ttk.Radiobutton(lang_frame, text="英文", variable=self.lang_var, value="en").pack(side=tk.LEFT, padx=10, pady=5)
+        
+        # 关键词输入
+        keyword_frame = ttk.LabelFrame(self.generate_frame, text="关键词(用逗号分隔)")
+        keyword_frame.pack(fill=tk.X, padx=10, pady=5)
+        
+        self.keyword_entry = ttk.Entry(keyword_frame, width=50)
+        self.keyword_entry.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)
+        self.keyword_entry.insert(0, "gpt,go,ai")
+        
+        # 域名后缀
+        domain_frame = ttk.LabelFrame(self.generate_frame, text="域名后缀(用逗号分隔)")
+        domain_frame.pack(fill=tk.X, padx=10, pady=5)
+        
+        self.domain_entry = ttk.Entry(domain_frame, width=50)
+        self.domain_entry.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)
+        self.domain_entry.insert(0, "com,cn,io")
+        
+        # 位置选择
+        position_frame = ttk.LabelFrame(self.generate_frame, text="关键词位置")
+        position_frame.pack(fill=tk.X, padx=10, pady=5)
+        
+        self.position_var = tk.StringVar(value="prefix")
+        ttk.Radiobutton(position_frame, text="前缀", variable=self.position_var, value="prefix").pack(side=tk.LEFT, padx=10, pady=5)
+        ttk.Radiobutton(position_frame, text="后缀", variable=self.position_var, value="suffix").pack(side=tk.LEFT, padx=10, pady=5)
+        
+        # 输出文件
+        output_frame = ttk.LabelFrame(self.generate_frame, text="输出文件")
+        output_frame.pack(fill=tk.X, padx=10, pady=5)
+        
+        self.generate_output_entry = ttk.Entry(output_frame, width=40)
+        self.generate_output_entry.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)
+        self.generate_output_entry.insert(0, "domain.txt")
+        
+        ttk.Button(output_frame, text="浏览", command=self.browse_generate_output).pack(side=tk.LEFT, padx=5, pady=5)
+        
+        # 生成按钮
+        button_frame = ttk.Frame(self.generate_frame)
+        button_frame.pack(fill=tk.X, padx=10, pady=10)
+        
+        self.generate_button = ttk.Button(button_frame, text="开始生成", command=self.start_generate)
+        self.generate_button.pack(side=tk.LEFT, padx=5)
+        
+        ttk.Button(button_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT, padx=5)
+        
+    def create_search_tab(self):
+        # 输入文件
+        input_frame = ttk.LabelFrame(self.search_frame, text="输入文件(域名列表)")
+        input_frame.pack(fill=tk.X, padx=10, pady=5)
+        
+        self.input_entry = ttk.Entry(input_frame, width=40)
+        self.input_entry.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)
+        self.input_entry.insert(0, "domain.txt")
+        
+        ttk.Button(input_frame, text="浏览", command=self.browse_input_file).pack(side=tk.LEFT, padx=5, pady=5)
+        
+        # 输出文件
+        output_frame = ttk.LabelFrame(self.search_frame, text="输出文件(结果)")
+        output_frame.pack(fill=tk.X, padx=10, pady=5)
+        
+        self.search_output_entry = ttk.Entry(output_frame, width=40)
+        self.search_output_entry.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)
+        self.search_output_entry.insert(0, "result.txt")
+        
+        ttk.Button(output_frame, text="浏览", command=self.browse_search_output).pack(side=tk.LEFT, padx=5, pady=5)
+        
+        # 导出选项
+        export_frame = ttk.LabelFrame(self.search_frame, text="导出选项")
+        export_frame.pack(fill=tk.X, padx=10, pady=5)
+        
+        self.export_all_var = tk.BooleanVar(value=False)
+        ttk.Checkbutton(export_frame, text="导出所有域名(包括不可用的)", variable=self.export_all_var).pack(side=tk.LEFT, padx=10, pady=5)
+        
+        # 搜索按钮
+        button_frame = ttk.Frame(self.search_frame)
+        button_frame.pack(fill=tk.X, padx=10, pady=10)
+        
+        self.search_button = ttk.Button(button_frame, text="开始搜索", command=self.start_search)
+        self.search_button.pack(side=tk.LEFT, padx=5)
+        
+        ttk.Button(button_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT, padx=5)
+        
+    def browse_input_file(self):
+        filename = filedialog.askopenfilename(
+            title="选择输入文件",
+            filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
+        )
+        if filename:
+            self.input_entry.delete(0, tk.END)
+            self.input_entry.insert(0, filename)
+            
+    def browse_search_output(self):
+        filename = filedialog.asksaveasfilename(
+            title="选择输出文件",
+            defaultextension=".txt",
+            filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
+        )
+        if filename:
+            self.search_output_entry.delete(0, tk.END)
+            self.search_output_entry.insert(0, filename)
+            
+    def browse_generate_output(self):
+        filename = filedialog.asksaveasfilename(
+            title="选择输出文件",
+            defaultextension=".txt",
+            filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
+        )
+        if filename:
+            self.generate_output_entry.delete(0, tk.END)
+            self.generate_output_entry.insert(0, filename)
+            
+    def log(self, message):
+        """添加日志"""
+        self.log_text.insert(tk.END, message + "\n")
+        self.log_text.see(tk.END)
+        self.root.update_idletasks()
+        
+    def clear_log(self):
+        """清空日志"""
+        self.log_text.delete(1.0, tk.END)
+        
+    def start_generate(self):
+        """开始生成域名"""
+        if self.is_running:
+            messagebox.showwarning("警告", "任务正在运行中,请等待完成")
+            return
+            
+        # 获取参数
+        lang = self.lang_var.get()
+        keyword = self.keyword_entry.get().strip()
+        domain = self.domain_entry.get().strip()
+        position = self.position_var.get()
+        output = self.generate_output_entry.get().strip()
+        
+        if not keyword:
+            messagebox.showerror("错误", "请输入关键词")
+            return
+            
+        if not domain:
+            messagebox.showerror("错误", "请输入域名后缀")
+            return
+            
+        if not output:
+            messagebox.showerror("错误", "请输入输出文件名")
+            return
+            
+        # 准备参数
+        params = OrderedDict({
+            "lang": lang,
+            "keyword": keyword,
+            "domain": domain,
+            "position": position,
+            "app_path": self.app_path
+        })
+        
+        # 更新输出文件路径为绝对路径
+        if not os.path.isabs(output):
+            output = os.path.join(self.app_path, output)
+        else:
+            params["app_path"] = os.path.dirname(output)
+            
+        # 在后台线程中运行
+        self.is_running = True
+        self.generate_button.config(state=tk.DISABLED)
+        self.status_var.set("正在生成域名...")
+        
+        thread = threading.Thread(target=self._generate_thread, args=(params, lang))
+        thread.daemon = True
+        thread.start()
+        
+    def _generate_thread(self, params, lang):
+        """生成域名的后台线程"""
+        try:
+            self.log(f"开始生成域名...")
+            self.log(f"语言: {lang}, 关键词: {params['keyword']}, 域名后缀: {params['domain']}, 位置: {params['position']}")
+            
+            if lang == "en":
+                generateDomain = GenerateEnDomain(params=params)
+            else:
+                generateDomain = GenerateDomain(params=params)
+                
+            generateDomain.run()
+            
+            output_file = params.get("output", "domain.txt")
+            self.log("域名生成完成!")
+            self.log(f"结果已保存到: {output_file}")
+            messagebox.showinfo("成功", f"域名生成完成!\n结果已保存到: {output_file}")
+            
+        except Exception as e:
+            error_msg = f"生成域名时出错: {str(e)}"
+            self.log(error_msg)
+            messagebox.showerror("错误", error_msg)
+        finally:
+            self.is_running = False
+            self.root.after(0, lambda: self.generate_button.config(state=tk.NORMAL))
+            self.root.after(0, lambda: self.status_var.set("就绪"))
+            
+    def start_search(self):
+        """开始搜索域名"""
+        if self.is_running:
+            messagebox.showwarning("警告", "任务正在运行中,请等待完成")
+            return
+            
+        # 获取参数
+        input_file = self.input_entry.get().strip()
+        output_file = self.search_output_entry.get().strip()
+        export_all = self.export_all_var.get()
+        
+        if not input_file:
+            messagebox.showerror("错误", "请输入输入文件名")
+            return
+            
+        if not output_file:
+            messagebox.showerror("错误", "请输入输出文件名")
+            return
+            
+        # 检查输入文件是否存在
+        if not os.path.isabs(input_file):
+            input_file = os.path.join(self.app_path, input_file)
+            
+        if not os.path.exists(input_file):
+            messagebox.showerror("错误", f"输入文件不存在: {input_file}")
+            return
+            
+        # 准备参数
+        if not os.path.isabs(output_file):
+            output_file = os.path.join(self.app_path, output_file)
+            
+        params = OrderedDict({
+            "input": os.path.basename(input_file),
+            "output": os.path.basename(output_file),
+            "app_path": os.path.dirname(input_file)
+        })
+        
+        # 在后台线程中运行
+        self.is_running = True
+        self.search_button.config(state=tk.DISABLED)
+        self.status_var.set("正在搜索域名...")
+        
+        thread = threading.Thread(target=self._search_thread, args=(params, export_all))
+        thread.daemon = True
+        thread.start()
+        
+    def _search_thread(self, params, export_all):
+        """搜索域名的后台线程"""
+        try:
+            self.log(f"开始搜索域名...")
+            self.log(f"输入文件: {params['input']}")
+            self.log(f"输出文件: {params['output']}")
+            self.log(f"导出所有: {export_all}")
+            
+            searchdomain = SearchDomain(params=params, debug=True, export_all=export_all)
+            searchdomain.run()
+            
+            self.log("域名搜索完成!")
+            output_path = os.path.join(params["app_path"], params["output"])
+            self.log(f"结果已保存到: {output_path}")
+            messagebox.showinfo("成功", f"域名搜索完成!\n结果已保存到: {output_path}")
+            
+        except Exception as e:
+            error_msg = f"搜索域名时出错: {str(e)}"
+            self.log(error_msg)
+            import traceback
+            self.log(traceback.format_exc())
+            messagebox.showerror("错误", error_msg)
+        finally:
+            self.is_running = False
+            self.root.after(0, lambda: self.search_button.config(state=tk.NORMAL))
+            self.root.after(0, lambda: self.status_var.set("就绪"))
+
+
+def main():
+    root = tk.Tk()
+    app = SearchDomainGUI(root)
+    root.mainloop()
+
+
+if __name__ == "__main__":
+    main()

+ 4 - 2
searchdomain/generate_domain.py

@@ -26,7 +26,8 @@ class GenerateDomain(object):
         self.composeDomain=[]
 
     def run(self):
-        """ 批量生成域名, 保存到 domain.txt """
+        """ 批量生成域名, 保存到 domain.txt 或指定的输出文件 """
+        output_file = self.params.get("output", "domain.txt")
         with open(os.path.join(self.params["app_path"], r"data/a.csv"), "r", encoding="utf-8") as f:
             csv_data = csv.reader(f)
             for row in csv_data:
@@ -45,6 +46,7 @@ class GenerateDomain(object):
                 for j in self.yuming:
                     self.composeDomain.append(i+"."+j)
 
-            with open(os.path.join(self.params["app_path"], "domain.txt"),"w",encoding="utf-8") as file:
+            output_path = output_file if os.path.isabs(output_file) else os.path.join(self.params["app_path"], output_file)
+            with open(output_path, "w", encoding="utf-8") as file:
                 for i in self.composeDomain:
                     file.write(i+"\n")

+ 4 - 1
searchdomain/generate_en_domain.py

@@ -27,6 +27,8 @@ class GenerateEnDomain(object):
         self.composePinYin=[]
 
     def run(self):
+        """ 批量生成域名, 保存到 domain.txt 或指定的输出文件 """
+        output_file = self.params.get("output", "domain.txt")
         with open(os.path.join(self.params["app_path"], r"data/b.csv"), "r", encoding="utf-8") as f:
             csv_data = f.readlines()
             for row in csv_data:
@@ -43,6 +45,7 @@ class GenerateEnDomain(object):
                 for j in self.yuming:
                     self.composeDomain.append(i+"."+j)
 
-            with open(os.path.join(self.params["app_path"], "domain.txt"),"w",encoding="utf-8") as file:
+            output_path = output_file if os.path.isabs(output_file) else os.path.join(self.params["app_path"], output_file)
+            with open(output_path, "w", encoding="utf-8") as file:
                 for i in self.composeDomain:
                     file.write(i+"\n")