[Python] 纯文本查看 复制代码
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
import tkinter as tk
from tkinter import messagebox, simpledialog
import socket
import subprocess
import os
import sys
import threading
import json
# 尝试导入 dnspython
try:
import dns.resolver
USE_DNSPYTHON = True
except ImportError:
USE_DNSPYTHON = False
# 预设文件路径(与脚本同目录)
PRESETS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "host_presets.json")
def load_presets():
if os.path.exists(PRESETS_FILE):
try:
with open(PRESETS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return ["github.com", "api.themoviedb.org", "www.themoviedb.org", "image.tmdb.org"]
else:
return ["github.com", "api.themoviedb.org", "www.themoviedb.org", "image.tmdb.org"]
def save_presets(presets):
try:
with open(PRESETS_FILE, 'w', encoding='utf-8') as f:
json.dump(presets, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"保存预设失败: {e}")
def is_admin():
try:
return os.getuid() == 0
except AttributeError:
import ctypes
return ctypes.windll.shell32.IsUserAnAdmin()
def flush_dns():
try:
if sys.platform == "win32":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
subprocess.run(
["ipconfig", "/flushdns"],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
startupinfo=startupinfo
)
elif sys.platform == "darwin":
subprocess.run(["sudo", "killall", "-HUP", "mDNSResponder"], check=False, stdout=subprocess.DEVNULL)
subprocess.run(["sudo", "dscacheutil", "-flushcache"], check=False, stdout=subprocess.DEVNULL)
else:
subprocess.run(["sudo", "systemd-resolve", "--flush-caches"], check=False, stdout=subprocess.DEVNULL)
subprocess.run(["sudo", "nscd", "-i", "hosts"], check=False, stdout=subprocess.DEVNULL)
return True
except Exception:
return False
def get_all_ips(domain):
ips = set()
if USE_DNSPYTHON:
for nameserver in ['8.8.8.8', '1.1.1.1', '223.5.5.5']:
resolver = dns.resolver.Resolver()
resolver.nameservers = [nameserver]
try:
answers = resolver.resolve(domain, 'A', lifetime=3)
for rdata in answers:
ips.add(rdata.to_text())
except Exception:
continue
else:
try:
_, _, ip_list = socket.gethostbyname_ex(domain)
ips.update(ip_list)
except Exception:
try:
ip = socket.gethostbyname(domain)
ips.add(ip)
except Exception:
pass
return list(ips)
def ping_ip(ip, timeout=2):
try:
if sys.platform == "win32":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(
["ping", "-n", "1", "-w", str(int(timeout * 1000)), ip],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
startupinfo=startupinfo
)
if result.returncode == 0:
for line in result.stdout.splitlines():
if "平均" in line or "Average" in line:
import re
match = re.search(r"(\d+)(?:ms|毫秒)", line)
if match:
return int(match.group(1))
if "<1" in line:
return 1
return None
else:
result = subprocess.run(
["ping", "-c", "1", "-W", str(timeout), ip],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True
)
if result.returncode == 0:
for line in result.stdout.splitlines():
if "time=" in line:
start = line.find("time=") + 5
end = line.find(" ", start)
delay = float(line[start:end])
return int(round(delay))
return None
except Exception:
return None
def resolve_and_ping(domain, progress_callback, result_callback):
ips = get_all_ips(domain)
if not ips:
result_callback([])
return
results = []
total = len(ips)
for i, ip in enumerate(ips):
progress_callback(f"Ping {ip} ({i+1}/{total})...")
delay = ping_ip(ip)
results.append((ip, delay))
results.sort(key=lambda x: (x[1] is None, x[1]))
result_callback(results)
class SmartHostsApp:
def __init__(self, root):
self.root = root
self.root.title("✨ 智能 Hosts 优化工具")
self.selected_ip = None
self.domain = ""
self.presets = load_presets()
self.create_widgets()
def create_widgets(self):
main_frame = ttk.Frame(self.root, padding=15)
main_frame.pack(fill=BOTH, expand=YES)
paned = ttk.Panedwindow(main_frame, orient=tk.HORIZONTAL)
paned.pack(fill=BOTH, expand=YES, pady=(0, 10))
left_frame = ttk.Labelframe(paned, text="📌 常用域名", padding=10)
paned.add(left_frame, weight=1)
self.preset_listbox = tk.Listbox(
left_frame,
font=("Consolas", 10),
selectmode=tk.SINGLE,
exportselection=False
)
self.preset_listbox.pack(fill=BOTH, expand=YES, pady=(0, 10))
self.preset_listbox.bind("<<ListboxSelect>>", self.on_preset_select)
btn_frame_left = ttk.Frame(left_frame)
btn_frame_left.pack(fill=X)
ttk.Button(
btn_frame_left,
text="➕ 添加",
bootstyle=SUCCESS,
command=self.add_preset
).pack(side=LEFT, fill=X, expand=YES, padx=(0, 5))
ttk.Button(
btn_frame_left,
text="🗑 删除",
bootstyle=DANGER,
command=self.remove_preset
).pack(side=LEFT, fill=X, expand=YES, padx=(5, 0))
right_frame = ttk.Frame(paned)
paned.add(right_frame, weight=2)
title_label = ttk.Label(
right_frame,
text="智能 Hosts 优化工具",
font=("Helvetica", 18, "bold"),
bootstyle=PRIMARY
)
title_label.pack(pady=(0, 15))
input_frame = ttk.Frame(right_frame)
input_frame.pack(fill=X, pady=5)
ttk.Label(input_frame, text="域名:", font=("Helvetica", 10)).pack(side=LEFT)
self.domain_entry = ttk.Entry(input_frame, font=("Consolas", 11))
self.domain_entry.pack(side=LEFT, fill=X, expand=YES, padx=(5, 10))
self.domain_entry.focus()
self.resolve_btn = ttk.Button(
input_frame,
text="🔍 查询并 Ping",
bootstyle=SUCCESS,
command=self.start_resolve
)
self.resolve_btn.pack(side=RIGHT)
self.status_var = tk.StringVar(value="请输入域名或从左侧选择常用域名")
status_label = ttk.Label(right_frame, textvariable=self.status_var, font=("Helvetica", 9), bootstyle=INFO)
status_label.pack(pady=(5, 10))
table_frame = ttk.Frame(right_frame)
table_frame.pack(fill=BOTH, expand=YES, pady=10)
columns = ("IP", "延迟 (ms)")
self.tree = ttk.Treeview(
table_frame,
columns=columns,
show="headings",
selectmode="browse",
bootstyle=PRIMARY
)
self.tree.heading("IP", text="IP 地址")
self.tree.heading("延迟 (ms)", text="延迟 (ms)")
self.tree.column("IP", width=180, anchor=W)
self.tree.column("延迟 (ms)", width=120, anchor=CENTER)
vsb = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview)
hsb = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=self.tree.xview)
self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
self.tree.grid(row=0, column=0, sticky=NSEW)
vsb.grid(row=0, column=1, sticky=NS)
hsb.grid(row=1, column=0, sticky=EW)
table_frame.grid_rowconfigure(0, weight=1)
table_frame.grid_columnconfigure(0, weight=1)
self.tree.bind("<<TreeviewSelect>>", self.on_select)
btn_frame = ttk.Frame(right_frame)
btn_frame.pack(pady=15)
self.apply_btn = ttk.Button(
btn_frame,
text="✅ 写入 Hosts",
bootstyle=PRIMARY,
state=DISABLED,
command=self.apply_hosts
)
self.apply_btn.pack(side=LEFT, padx=5)
self.flush_btn = ttk.Button(
btn_frame,
text="🔄 刷新 DNS",
bootstyle=INFO,
command=self.do_flush_dns
)
self.flush_btn.pack(side=LEFT, padx=5)
footer = ttk.Label(
right_frame,
text="💡 提示:点击左侧常用域名快速查询;延迟越低越好",
font=("Helvetica", 8),
bootstyle=SECONDARY
)
footer.pack(side=BOTTOM, anchor=W)
self.refresh_preset_list()
def refresh_preset_list(self):
self.preset_listbox.delete(0, tk.END)
for domain in self.presets:
self.preset_listbox.insert(tk.END, domain)
def on_preset_select(self, event):
selection = self.preset_listbox.curselection()
if selection:
domain = self.preset_listbox.get(selection[0])
self.domain_entry.delete(0, tk.END)
self.domain_entry.insert(0, domain)
self.start_resolve()
def add_preset(self):
domain = simpledialog.askstring("添加常用域名", "请输入域名(如:example.com):", parent=self.root)
if domain:
domain = domain.strip().lower()
if domain and domain not in self.presets:
self.presets.append(domain)
save_presets(self.presets)
self.refresh_preset_list()
elif domain in self.presets:
messagebox.showinfo("提示", "该域名已在列表中!", parent=self.root)
def remove_preset(self):
selection = self.preset_listbox.curselection()
if not selection:
messagebox.showwarning("操作失败", "请先选择一个域名!", parent=self.root)
return
idx = selection[0]
domain = self.presets[idx]
if messagebox.askyesno("确认删除", f"确定要删除「{domain}」吗?", parent=self.root):
del self.presets[idx]
save_presets(self.presets)
self.refresh_preset_list()
def on_select(self, event):
selected = self.tree.selection()
if selected:
item = self.tree.item(selected[0])
self.selected_ip = item["values"][0]
self.apply_btn.config(state=NORMAL)
def clear_table(self):
for item in self.tree.get_children():
self.tree.delete(item)
def update_status(self, msg):
self.status_var.set(msg)
self.root.update_idletasks()
def start_resolve(self):
domain = self.domain_entry.get().strip()
if not domain:
messagebox.showwarning("输入错误", "请输入一个有效的域名!", parent=self.root)
return
self.domain = domain
self.status_var.set("正在解析并 Ping 所有 IP,请稍候...")
self.resolve_btn.config(state=DISABLED)
self.apply_btn.config(state=DISABLED)
self.clear_table()
threading.Thread(
target=resolve_and_ping,
args=(domain, self.update_status, self.on_complete),
daemon=True
).start()
def on_complete(self, results):
self.clear_table()
if not results:
self.status_var.set("❌ 未找到有效 IP")
messagebox.showerror("错误", "无法解析该域名或所有 IP 均无响应。", parent=self.root)
else:
self.status_var.set(f"✅ 共找到 {len(results)} 个 IP(按延迟排序)")
for ip, delay in results:
disp_delay = str(delay) if delay is not None else "超时"
self.tree.insert("", tk.END, values=(ip, disp_delay))
self.resolve_btn.config(state=NORMAL)
def apply_hosts(self):
if not self.selected_ip or not self.domain:
return
if not is_admin():
messagebox.showerror("权限不足", "请以管理员身份运行本程序才能修改 hosts 文件!", parent=self.root)
return
hosts_path = r"C:\Windows\System32\drivers\etc\hosts" if os.name == 'nt' else "/etc/hosts"
new_entry = f"{self.selected_ip}\t{self.domain}"
try:
if os.path.exists(hosts_path):
with open(hosts_path, 'r', encoding='utf-8') as f:
lines = f.read().splitlines()
else:
lines = []
filtered_lines = []
for line in lines:
stripped = line.strip()
if stripped and not stripped.startswith('#'):
parts = stripped.split()
if len(parts) >= 2 and parts[-1] != self.domain:
filtered_lines.append(line)
else:
filtered_lines.append(line)
filtered_lines.append(new_entry)
content = "\n".join(filtered_lines) + "\n"
with open(hosts_path, 'w', encoding='utf-8') as f:
f.write(content)
messagebox.showinfo("成功", f"已写入 hosts:\n{new_entry}", parent=self.root)
self.do_flush_dns(silent=True)
except Exception as e:
messagebox.showerror("写入失败", f"错误: {e}", parent=self.root)
def do_flush_dns(self, silent=False):
if flush_dns():
if not silent:
messagebox.showinfo("成功", "DNS 缓存已刷新!", parent=self.root)
else:
if not silent:
messagebox.showwarning("警告", "DNS 刷新失败(可能需要管理员权限)", parent=self.root)
if __name__ == "__main__":
if not USE_DNSPYTHON:
print("[提示] 未安装 dnspython,将使用基础 DNS 查询(可能只返回一个 IP)")
print("建议运行: pip install dnspython")
app = ttk.Window(
title="智能 Hosts 优化工具",
themename="darkly",
size=(850, 600),
resizable=(True, True)
)
SmartHostsApp(app)
app.mainloop()