1131104-wakeonlan-test02-14-01
-匯出CSV
-使用繁體中文,想在以下程式中的"清除結果"的右邊新一個按鈕"匯出掃描資料",功能是把掃描完後資料全部匯出,匯出可以選擇要存放的位置,要如何修正程式?
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import logging
from concurrent.futures import ThreadPoolExecutor
from tkinter import messagebox, filedialog
import threading
import csv
# 全局變量來跟踪進度
scan_count = 0 # 記錄已完成的掃描數量
total_count = 254 # 總共需要掃描的 IP 數量(從 1 到 254)
# 創建自定義的日誌處理器,將日誌輸出到 Text 小部件
class TkinterLogHandler(logging.Handler):
def __init__(self, text_widget, auto_scroll=True):
super().__init__()
self.text_widget = text_widget
self.auto_scroll = auto_scroll
def emit(self, record):
msg = self.format(record)
self.text_widget.insert(tk.END, msg + '\n')
if self.auto_scroll:
self.text_widget.yview(tk.END) # 自動滾動到底部
# 設定日誌
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號", "IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
# 獲取本機 IP 網段(預設網段)
def get_local_ip_prefix():
try:
local_ip = socket.gethostbyname(socket.gethostname()) # 獲取本機的 IP 位址
return '.'.join(local_ip.split('.')[:3]) + '.' # 回傳網段(例如 192.168.1.)
except Exception as e:
logging.error(f"無法獲取本機 IP 位址: {e}")
return "192.168.1." # 預設網段
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan(net_prefix):
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
update_scan_results(ip, mac, computer_name, status)
def port_scan(ip, port=80):
# 檢查 IP 是否為有效的 IPv4 格式
try:
ipaddress.IPv4Address(ip) # 如果 IP 無效,會拋出 ValueError
except ValueError:
logging.error(f"無效的 IP 地址:{ip}")
return False
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except socket.error as e:
logging.error(f"端口掃描錯誤({ip}):{e}")
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
update_scan_results(ip, mac_address, computer_name, status)
update_tree()
def update_scan_results(ip, mac, computer_name, status):
"""更新掃描結果,處理重複的 IP 和無效的 MAC"""
# 檢查 scan_results 是否已經存在該 IP,若有則檢查 MAC 是否為 "N/A"
for idx, (existing_ip, existing_mac, existing_computer_name, existing_status) in enumerate(scan_results):
if existing_ip == ip:
# 如果已有的 MAC 為 "N/A",且當前 MAC 地址有效,則更新 MAC
if existing_mac == "N/A" and mac != "N/A":
scan_results[idx] = (ip, mac, computer_name, status)
return # 找到並處理過就退出
# 若是新 IP 或無重複的情況,直接加入
scan_results.append((ip, mac, computer_name, status))
def update_tree(data=None):
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
def update_ui():
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
root.after(0, update_ui)
# 檢查網段格式是否有效
def is_valid_network_prefix(prefix):
"""
檢查網段格式是否有效,可以是單純的 x.x.x. 或 CIDR 格式 x.x.x.x/xx。
例如: 192.168.1. 或 192.168.1.0/24
"""
# 如果網段以 "." 結尾,將其補充為最後一段 0
if prefix.endswith('.'):
prefix = prefix + '0'
# 正則表達式匹配 x.x.x.x 或 x.x.x.x/xx(CIDR 格式)
network_regex = r'^(\d{1,3}\.){3}\d{1,3}(/(\d|[12][0-9]|3[0-2]))?$'
# 檢查網段格式是否符合正則表達式
if re.match(network_regex, prefix):
# 檢查每個 IP 部分是否在 0 到 255 之間
try:
parts = prefix.split('/')
# 檢查 IP 地址部分
ip_parts = parts[0].split('.')
if len(ip_parts) == 4 and all(0 <= int(part) <= 255 for part in ip_parts):
# 如果有 CIDR 部分,檢查是否是 0-32 之間的數字
if len(parts) > 1:
subnet_mask = int(parts[1])
if 0 <= subnet_mask <= 32:
return True
else:
return True # 沒有 CIDR 部分也有效
except ValueError:
return False
return False
# 更新進度條的函數
def update_progress_bar(value):
progress_bar['value'] = value
root.update_idletasks()
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
global scan_count
net_prefix = network_entry.get() # 從使用者輸入的 Entry 中獲取網段
if not net_prefix or not is_valid_network_prefix(net_prefix):
logging.error("請先輸入有效的網段!")
messagebox.showerror("錯誤", "請先輸入有效的網段!")
return
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
scan_count = 0 # 重置掃描進度
# 執行 ARP 掃描
arp_scan(net_prefix)
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
future_to_ip = {executor.submit(threaded_port_scan, f"{net_prefix}{i}"): i for i in range(1, 255)}
for future in future_to_ip:
future.result() # 確保每個任務的結果都被獲得
# 更新進度條
with lock:
scan_count += 1 # 每完成一個掃描,進行一次累加
progress = (scan_count / total_count) * 100
root.after(0, update_progress_bar, progress) # 使用 after 將更新交給主執行緒
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 匯出掃描結果到 CSV 檔案的函數
def export_scan_results():
# 讓使用者選擇匯出位置
file_path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV Files", "*.csv")])
if not file_path:
return # 如果沒有選擇檔案,則直接返回
try:
# 打開 CSV 檔案進行寫入
with open(file_path, mode='w', newline='') as file:
writer = csv.writer(file)
# 寫入表頭
writer.writerow(["序號", "IP Address", "MAC Address", "Computer Name", "Status"])
# 寫入掃描結果
for idx, (ip, mac_address, computer_name, status) in enumerate(scan_results):
writer.writerow([idx + 1, ip, mac_address, computer_name, status])
logging.info(f"掃描結果已成功匯出到 {file_path}")
messagebox.showinfo("匯出成功", f"掃描結果已成功匯出到 {file_path}")
except Exception as e:
logging.error(f"匯出掃描結果時發生錯誤: {e}")
messagebox.showerror("匯出失敗", f"匯出掃描結果時發生錯誤: {e}")
# 用來讓使用者輸入網段的輸入框和標籤
network_label = tk.Label(root, text="輸入掃描網段 (例如: 192.168.1.)")
network_label.pack(padx=10, pady=5)
# 預設為本機的網段
local_ip_prefix = get_local_ip_prefix()
network_entry = tk.Entry(root, width=20)
network_entry.pack(padx=10, pady=5)
network_entry.insert(0, local_ip_prefix) # 預設顯示本機 IP 的網段
# 顯示提示文字
alert_label = tk.Label(root, text="", fg="red")
alert_label.pack(padx=10, pady=10)
# 創建一個 Text 小部件來顯示日誌訊息
log_text = tk.Text(root, height=3, width=80)
log_text.pack(side=tk.RIGHT,padx=5, pady=5)
# 設置自定義的日誌處理器
log_handler = TkinterLogHandler(log_text)
logging.getLogger().addHandler(log_handler)
# 創建掃描按鈕
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建清除結果按鈕
def clear_results():
tree.delete(*tree.get_children())
scan_results.clear()
clear_button = tk.Button(root, text="清除結果", command=clear_results)
clear_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建匯出掃描資料的按鈕
export_button = tk.Button(root, text="匯出掃描資料", command=export_scan_results)
export_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建進度條
progress_bar = ttk.Progressbar(root, orient="horizontal", length=200, mode="determinate")
progress_bar.pack(side=tk.BOTTOM, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-13-01
-進度條快
-會有IP錯誤發生
如果想在"清除結果"的右邊新一個按鈕"匯出掃描資料",功能是把掃描完後資料全部匯出,要如何修正程式?
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import logging
from concurrent.futures import ThreadPoolExecutor
from tkinter import messagebox
import threading
# 全局變量來跟踪進度
scan_count = 0 # 記錄已完成的掃描數量
total_count = 254 # 總共需要掃描的 IP 數量(從 1 到 254)
# 創建自定義的日誌處理器,將日誌輸出到 Text 小部件
class TkinterLogHandler(logging.Handler):
def __init__(self, text_widget, auto_scroll=True):
super().__init__()
self.text_widget = text_widget
self.auto_scroll = auto_scroll
def emit(self, record):
msg = self.format(record)
self.text_widget.insert(tk.END, msg + '\n')
if self.auto_scroll:
self.text_widget.yview(tk.END) # 自動滾動到底部
# 設定日誌
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號", "IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
# 獲取本機 IP 網段(預設網段)
def get_local_ip_prefix():
try:
local_ip = socket.gethostbyname(socket.gethostname()) # 獲取本機的 IP 位址
return '.'.join(local_ip.split('.')[:3]) + '.' # 回傳網段(例如 192.168.1.)
except Exception as e:
logging.error(f"無法獲取本機 IP 位址: {e}")
return "192.168.1." # 預設網段
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan(net_prefix):
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
update_scan_results(ip, mac, computer_name, status)
def port_scan(ip, port=80):
# 檢查 IP 是否為有效的 IPv4 格式
try:
ipaddress.IPv4Address(ip) # 如果 IP 無效,會拋出 ValueError
except ValueError:
logging.error(f"無效的 IP 地址:{ip}")
return False
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except socket.error as e:
logging.error(f"端口掃描錯誤({ip}):{e}")
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
update_scan_results(ip, mac_address, computer_name, status)
update_tree()
def update_scan_results(ip, mac, computer_name, status):
"""更新掃描結果,處理重複的 IP 和無效的 MAC"""
# 檢查 scan_results 是否已經存在該 IP,若有則檢查 MAC 是否為 "N/A"
for idx, (existing_ip, existing_mac, existing_computer_name, existing_status) in enumerate(scan_results):
if existing_ip == ip:
# 如果已有的 MAC 為 "N/A",且當前 MAC 地址有效,則更新 MAC
if existing_mac == "N/A" and mac != "N/A":
scan_results[idx] = (ip, mac, computer_name, status)
return # 找到並處理過就退出
# 若是新 IP 或無重複的情況,直接加入
scan_results.append((ip, mac, computer_name, status))
def update_tree(data=None):
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
def update_ui():
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
root.after(0, update_ui)
# 檢查網段格式是否有效
def is_valid_network_prefix(prefix):
"""
檢查網段格式是否有效,可以是單純的 x.x.x. 或 CIDR 格式 x.x.x.x/xx。
例如: 192.168.1. 或 192.168.1.0/24
"""
# 正則表達式匹配 x.x.x.x 或 x.x.x.x/xx(CIDR 格式)
network_regex = r'^(\d{1,3}\.){3}\d{1,3}(/(\d|[12][0-9]|3[0-2]))?$'
# 檢查網段格式是否符合正則表達式
if re.match(network_regex, prefix):
# 檢查每個 IP 部分是否在 0 到 255 之間
try:
parts = prefix.split('/')
# 檢查 IP 地址部分
ip_parts = parts[0].split('.')
if len(ip_parts) == 4 and all(0 <= int(part) <= 255 for part in ip_parts):
# 如果有 CIDR 部分,檢查是否是 0-32 之間的數字
if len(parts) > 1:
subnet_mask = int(parts[1])
if 0 <= subnet_mask <= 32:
return True
else:
return True # 沒有 CIDR 部分也有效
except ValueError:
return False
return False
# 更新進度條的函數
def update_progress_bar(value):
progress_bar['value'] = value
root.update_idletasks()
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
global scan_count
net_prefix = network_entry.get() # 從使用者輸入的 Entry 中獲取網段
if not net_prefix or not is_valid_network_prefix(net_prefix):
logging.error("請先輸入有效的網段!")
messagebox.showerror("錯誤", "請先輸入有效的網段!")
return
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
scan_count = 0 # 重置掃描進度
# 執行 ARP 掃描
arp_scan(net_prefix)
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
future_to_ip = {executor.submit(threaded_port_scan, f"{net_prefix}{i}"): i for i in range(1, 255)}
for future in future_to_ip:
future.result() # 確保每個任務的結果都被獲得
# 更新進度條
with lock:
scan_count += 1 # 每完成一個掃描,進行一次累加
progress = (scan_count / total_count) * 100
root.after(0, update_progress_bar, progress) # 使用 after 將更新交給主執行緒
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 用來讓使用者輸入網段的輸入框和標籤
network_label = tk.Label(root, text="輸入掃描網段 (例如: 192.168.1.)")
network_label.pack(padx=10, pady=5)
# 預設為本機的網段
local_ip_prefix = get_local_ip_prefix()
network_entry = tk.Entry(root, width=20)
network_entry.pack(padx=10, pady=5)
network_entry.insert(0, local_ip_prefix) # 預設顯示本機 IP 的網段
# 顯示提示文字
alert_label = tk.Label(root, text="", fg="red")
alert_label.pack(padx=10, pady=10)
# 創建一個 Text 小部件來顯示日誌訊息
log_text = tk.Text(root, height=3, width=80)
log_text.pack(side=tk.RIGHT,padx=5, pady=5)
# 設置自定義的日誌處理器
log_handler = TkinterLogHandler(log_text)
logging.getLogger().addHandler(log_handler)
# 創建掃描按鈕
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建清除結果按鈕
def clear_results():
tree.delete(*tree.get_children())
scan_results.clear()
clear_button = tk.Button(root, text="清除結果", command=clear_results)
clear_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建進度條
progress_bar = ttk.Progressbar(root, orient="horizontal", length=200, mode="determinate")
progress_bar.pack(side=tk.BOTTOM, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-13
-進度條慢
-IP不會有錯誤發生
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import logging
from concurrent.futures import ThreadPoolExecutor
from tkinter import messagebox
import threading
# 全局變量來跟踪進度
scan_count = 0 # 記錄已完成的掃描數量
total_count = 254 # 總共需要掃描的 IP 數量(從 1 到 254)
# 創建自定義的日誌處理器,將日誌輸出到 Text 小部件
class TkinterLogHandler(logging.Handler):
def __init__(self, text_widget, auto_scroll=True):
super().__init__()
self.text_widget = text_widget
self.auto_scroll = auto_scroll
def emit(self, record):
msg = self.format(record)
self.text_widget.insert(tk.END, msg + '\n')
if self.auto_scroll:
self.text_widget.yview(tk.END) # 自動滾動到底部
# 設定日誌
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號", "IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
# 獲取本機 IP 網段(預設網段)
def get_local_ip_prefix():
try:
local_ip = socket.gethostbyname(socket.gethostname()) # 獲取本機的 IP 位址
return '.'.join(local_ip.split('.')[:3]) + '.' # 回傳網段(例如 192.168.1.)
except Exception as e:
logging.error(f"無法獲取本機 IP 位址: {e}")
return "192.168.1." # 預設網段
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan(net_prefix):
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
update_scan_results(ip, mac, computer_name, status)
def port_scan(ip, port=80):
# 檢查 IP 是否為有效的 IPv4 格式
try:
ipaddress.IPv4Address(ip) # 如果 IP 無效,會拋出 ValueError
except ValueError:
logging.error(f"無效的 IP 地址:{ip}")
return False
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except socket.error as e:
logging.error(f"端口掃描錯誤({ip}):{e}")
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
update_scan_results(ip, mac_address, computer_name, status)
update_tree()
def update_scan_results(ip, mac, computer_name, status):
"""更新掃描結果,處理重複的 IP 和無效的 MAC"""
# 檢查 scan_results 是否已經存在該 IP,若有則檢查 MAC 是否為 "N/A"
for idx, (existing_ip, existing_mac, existing_computer_name, existing_status) in enumerate(scan_results):
if existing_ip == ip:
# 如果已有的 MAC 為 "N/A",且當前 MAC 地址有效,則更新 MAC
if existing_mac == "N/A" and mac != "N/A":
scan_results[idx] = (ip, mac, computer_name, status)
return # 找到並處理過就退出
# 若是新 IP 或無重複的情況,直接加入
scan_results.append((ip, mac, computer_name, status))
def update_tree(data=None):
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
def update_ui():
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
root.after(0, update_ui)
# 檢查網段格式是否有效
def is_valid_network_prefix(prefix):
"""
檢查網段格式是否有效,可以是單純的 x.x.x. 或 CIDR 格式 x.x.x.x/xx。
例如: 192.168.1. 或 192.168.1.0/24
"""
# 如果網段以 "." 結尾,將其補充為最後一段 0
if prefix.endswith('.'):
prefix = prefix + '0'
# 正則表達式匹配 x.x.x.x 或 x.x.x.x/xx(CIDR 格式)
network_regex = r'^(\d{1,3}\.){3}\d{1,3}(/(\d|[12][0-9]|3[0-2]))?$'
# 檢查網段格式是否符合正則表達式
if re.match(network_regex, prefix):
# 檢查每個 IP 部分是否在 0 到 255 之間
try:
parts = prefix.split('/')
# 檢查 IP 地址部分
ip_parts = parts[0].split('.')
if len(ip_parts) == 4 and all(0 <= int(part) <= 255 for part in ip_parts):
# 如果有 CIDR 部分,檢查是否是 0-32 之間的數字
if len(parts) > 1:
subnet_mask = int(parts[1])
if 0 <= subnet_mask <= 32:
return True
else:
return True # 沒有 CIDR 部分也有效
except ValueError:
return False
return False
# 更新進度條的函數
def update_progress_bar(value):
progress_bar['value'] = value
root.update_idletasks()
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
global scan_count
net_prefix = network_entry.get() # 從使用者輸入的 Entry 中獲取網段
if not net_prefix or not is_valid_network_prefix(net_prefix):
logging.error("請先輸入有效的網段!")
messagebox.showerror("錯誤", "請先輸入有效的網段!")
return
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
scan_count = 0 # 重置掃描進度
# 執行 ARP 掃描
arp_scan(net_prefix)
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
future_to_ip = {executor.submit(threaded_port_scan, f"{net_prefix}{i}"): i for i in range(1, 255)}
for future in future_to_ip:
future.result() # 確保每個任務的結果都被獲得
# 更新進度條
with lock:
scan_count += 1 # 每完成一個掃描,進行一次累加
progress = (scan_count / total_count) * 100
root.after(0, update_progress_bar, progress) # 使用 after 將更新交給主執行緒
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 用來讓使用者輸入網段的輸入框和標籤
network_label = tk.Label(root, text="輸入掃描網段 (例如: 192.168.1.)")
network_label.pack(padx=10, pady=5)
# 預設為本機的網段
local_ip_prefix = get_local_ip_prefix()
network_entry = tk.Entry(root, width=20)
network_entry.pack(padx=10, pady=5)
network_entry.insert(0, local_ip_prefix) # 預設顯示本機 IP 的網段
# 顯示提示文字
alert_label = tk.Label(root, text="", fg="red")
alert_label.pack(padx=10, pady=10)
# 創建一個 Text 小部件來顯示日誌訊息
log_text = tk.Text(root, height=3, width=80)
log_text.pack(side=tk.RIGHT,padx=5, pady=5)
# 設置自定義的日誌處理器
log_handler = TkinterLogHandler(log_text)
logging.getLogger().addHandler(log_handler)
# 創建掃描按鈕
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建清除結果按鈕
def clear_results():
tree.delete(*tree.get_children())
scan_results.clear()
clear_button = tk.Button(root, text="清除結果", command=clear_results)
clear_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建進度條
progress_bar = ttk.Progressbar(root, orient="horizontal", length=200, mode="determinate")
progress_bar.pack(side=tk.BOTTOM, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-12
-IP輸入還是有問題
關於 is_valid_network_prefix 函數的正則表達式和格式判斷,您可以進一步提升網段檢查的準確性。由於現在的判斷方式僅是將 IP 地址分割並檢查前三段的數值是否有效,但它沒有考慮到一些更細緻的情況(例如,CIDR 格式,或是使用不同子網掩碼的情況)。因此,根據您的需求,以下是一些改進的方式:
CIDR 格式的網段通常是以 x.x.x.x/24 這種形式呈現,其中 /24 表示子網掩碼。您可以對這種格式進行檢查。
首先,我們仍然需要檢查 IP 地址是否符合正確的格式(x.x.x.x),然後如果有 /,我們需要確保後面的數字在 0 到 32 之間。
您可以使用正則表達式來檢查網段格式是否符合標準。下面是修正後的 is_valid_network_prefix 函數,加入了對 CIDR 格式的支持以及更詳細的檢查:
python
複製程式碼
import re
import ipaddress
def is_valid_network_prefix(prefix):
"""
檢查網段格式是否有效,可以是單純的 x.x.x. 或 CIDR 格式 x.x.x.x/xx。
例如: 192.168.1. 或 192.168.1.0/24
"""
# 正則表達式匹配 x.x.x.x 或 x.x.x.x/xx(CIDR 格式)
network_regex = r'^(\d{1,3}\.){3}\d{1,3}(/(\d|[12][0-9]|3[0-2]))?$'
# 檢查網段格式是否符合正則表達式
if re.match(network_regex, prefix):
# 檢查每個 IP 部分是否在 0 到 255 之間
try:
parts = prefix.split('/')
# 檢查 IP 地址部分
ip_parts = parts[0].split('.')
if len(ip_parts) == 4 and all(0 <= int(part) <= 255 for part in ip_parts):
# 如果有 CIDR 部分,檢查是否是 0-32 之間的數字
if len(parts) > 1:
subnet_mask = int(parts[1])
if 0 <= subnet_mask <= 32:
return True
else:
return True # 沒有 CIDR 部分也有效
except ValueError:
return False
return False
正則表達式:
^(\d{1,3}\.){3}\d{1,3}(/(\d|[12][0-9]|3[0-2]))?$
^ 和 $ 表示字符串的開始和結束,保證正確的匹配。
(\d{1,3}\.){3} 匹配前三個數字段並且後面接上 .(例如 192.168.1.)。
\d{1,3} 用來匹配每個段的數字,允許 1 到 3 位數字。
(/(\d|[12][0-9]|3[0-2]))? 匹配可選的 / 以及子網掩碼,子網掩碼必須是 0 到 32 之間的數字。
解析 IP 地址:
先檢查是否匹配正則表達式。
如果匹配,則將網段部分以 / 分割,獲得 IP 地址和子網掩碼部分。
檢查 IP 地址的每一段是否在 0 到 255 之間,這是確保 IP 地址有效的基本條件。
CIDR 子網掩碼的檢查:
如果網段包含 /(即 CIDR 格式),則檢查子網掩碼是否在 0 到 32 之間。
返回值:
如果所有檢查通過,則返回 True,表示網段有效;否則返回 False。
這樣的 is_valid_network_prefix 函數可以處理多種情況,包括:
只包含 IP 地址:192.168.1.
包含 CIDR 子網掩碼:192.168.1.0/24
其他無效的格式:192.168.1.500(無效 IP 地址),192.168.1./24(無效格式)
python
複製程式碼
print(is_valid_network_prefix("192.168.1.")) # True
print(is_valid_network_prefix("192.168.1.0/24")) # True
print(is_valid_network_prefix("192.168.1.500")) # False
print(is_valid_network_prefix("192.168.1.0/33")) # False
print(is_valid_network_prefix("10.0.0.0/8")) # True
這樣的修正可以讓 is_valid_network_prefix 函數不僅能夠處理簡單的 IP 網段,還能支持 CIDR 格式的網段,並進行更嚴格的驗證。這樣不僅提高了程式的靈活性,也能避免用戶輸入錯誤的網段格式,提升使用體驗。
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import logging
from concurrent.futures import ThreadPoolExecutor
from tkinter import messagebox
import threading
# 全局變量來跟踪進度
scan_count = 0 # 記錄已完成的掃描數量
total_count = 254 # 總共需要掃描的 IP 數量(從 1 到 254)
# 創建自定義的日誌處理器,將日誌輸出到 Text 小部件
class TkinterLogHandler(logging.Handler):
def __init__(self, text_widget, auto_scroll=True):
super().__init__()
self.text_widget = text_widget
self.auto_scroll = auto_scroll
def emit(self, record):
msg = self.format(record)
self.text_widget.insert(tk.END, msg + '\n')
if self.auto_scroll:
self.text_widget.yview(tk.END) # 自動滾動到底部
# 設定日誌
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號", "IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
# 獲取本機 IP 網段(預設網段)
def get_local_ip_prefix():
try:
local_ip = socket.gethostbyname(socket.gethostname()) # 獲取本機的 IP 位址
return '.'.join(local_ip.split('.')[:3]) + '.' # 回傳網段(例如 192.168.1.)
except Exception as e:
logging.error(f"無法獲取本機 IP 位址: {e}")
return "192.168.1." # 預設網段
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan(net_prefix):
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
update_scan_results(ip, mac, computer_name, status)
def port_scan(ip, port=80):
# 檢查 IP 是否為有效的 IPv4 格式
try:
ipaddress.IPv4Address(ip) # 如果 IP 無效,會拋出 ValueError
except ValueError:
logging.error(f"無效的 IP 地址:{ip}")
return False
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except socket.error as e:
logging.error(f"端口掃描錯誤({ip}):{e}")
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
update_scan_results(ip, mac_address, computer_name, status)
update_tree()
def update_scan_results(ip, mac, computer_name, status):
"""更新掃描結果,處理重複的 IP 和無效的 MAC"""
# 檢查 scan_results 是否已經存在該 IP,若有則檢查 MAC 是否為 "N/A"
for idx, (existing_ip, existing_mac, existing_computer_name, existing_status) in enumerate(scan_results):
if existing_ip == ip:
# 如果已有的 MAC 為 "N/A",且當前 MAC 地址有效,則更新 MAC
if existing_mac == "N/A" and mac != "N/A":
scan_results[idx] = (ip, mac, computer_name, status)
return # 找到並處理過就退出
# 若是新 IP 或無重複的情況,直接加入
scan_results.append((ip, mac, computer_name, status))
def update_tree(data=None):
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
def update_ui():
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
root.after(0, update_ui)
# 檢查網段格式是否有效
def is_valid_network_prefix(prefix):
"""
檢查網段格式是否有效,可以是單純的 x.x.x. 或 CIDR 格式 x.x.x.x/xx。
例如: 192.168.1. 或 192.168.1.0/24
"""
# 正則表達式匹配 x.x.x.x 或 x.x.x.x/xx(CIDR 格式)
network_regex = r'^(\d{1,3}\.){3}\d{1,3}(/(\d|[12][0-9]|3[0-2]))?$'
# 檢查網段格式是否符合正則表達式
if re.match(network_regex, prefix):
# 檢查每個 IP 部分是否在 0 到 255 之間
try:
parts = prefix.split('/')
# 檢查 IP 地址部分
ip_parts = parts[0].split('.')
if len(ip_parts) == 4 and all(0 <= int(part) <= 255 for part in ip_parts):
# 如果有 CIDR 部分,檢查是否是 0-32 之間的數字
if len(parts) > 1:
subnet_mask = int(parts[1])
if 0 <= subnet_mask <= 32:
return True
else:
return True # 沒有 CIDR 部分也有效
except ValueError:
return False
return False
# 更新進度條的函數
def update_progress_bar(value):
progress_bar['value'] = value
root.update_idletasks()
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
global scan_count
net_prefix = network_entry.get() # 從使用者輸入的 Entry 中獲取網段
if not net_prefix or not is_valid_network_prefix(net_prefix):
logging.error("請先輸入有效的網段!")
messagebox.showerror("錯誤", "請先輸入有效的網段!")
return
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
scan_count = 0 # 重置掃描進度
# 執行 ARP 掃描
arp_scan(net_prefix)
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
future_to_ip = {executor.submit(threaded_port_scan, f"{net_prefix}{i}"): i for i in range(1, 255)}
for future in future_to_ip:
future.result() # 確保每個任務的結果都被獲得
# 更新進度條
with lock:
scan_count += 1 # 每完成一個掃描,進行一次累加
progress = (scan_count / total_count) * 100
root.after(0, update_progress_bar, progress) # 使用 after 將更新交給主執行緒
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 用來讓使用者輸入網段的輸入框和標籤
network_label = tk.Label(root, text="輸入掃描網段 (例如: 192.168.1.)")
network_label.pack(padx=10, pady=5)
# 預設為本機的網段
local_ip_prefix = get_local_ip_prefix()
network_entry = tk.Entry(root, width=20)
network_entry.pack(padx=10, pady=5)
network_entry.insert(0, local_ip_prefix) # 預設顯示本機 IP 的網段
# 顯示提示文字
alert_label = tk.Label(root, text="", fg="red")
alert_label.pack(padx=10, pady=10)
# 創建一個 Text 小部件來顯示日誌訊息
log_text = tk.Text(root, height=3, width=80)
log_text.pack(side=tk.RIGHT,padx=5, pady=5)
# 設置自定義的日誌處理器
log_handler = TkinterLogHandler(log_text)
logging.getLogger().addHandler(log_handler)
# 創建掃描按鈕
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建清除結果按鈕
def clear_results():
tree.delete(*tree.get_children())
scan_results.clear()
clear_button = tk.Button(root, text="清除結果", command=clear_results)
clear_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建進度條
progress_bar = ttk.Progressbar(root, orient="horizontal", length=200, mode="determinate")
progress_bar.pack(side=tk.BOTTOM, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-11
scan_count 和 total_count:
我們使用 scan_count 來記錄已完成的掃描數量,並根據此來計算進度條的百分比。total_count 是總共需要掃描的 IP 數量,在此情況下是從 1 到 254(總共 254 個 IP)。
update_progress_bar():
這個函數會根據當前掃描進度更新進度條。它的作用是每次掃描完成時,都將當前的進度值設置到進度條上,並確保 UI 更新。
root.after(0, update_progress_bar, progress):
這行代碼是關鍵,它使用 root.after() 將進度條的更新排入主執行緒。這樣做的目的是避免在多執行緒中直接操作 UI 元素,從而減少執行緒間的衝突和延遲。
進度條更新的頻率:
我們在每次掃描完成後更新進度條。由於進度更新是在主執行緒中進行的,這樣可以減少每個執行緒中更新 UI 帶來的性能瓶頸。
使用 root.after() 可以讓進度條更新不會在每個執行緒內進行,而是按照掃描完成的數量進行集中更新,這樣就能避免 UI 更新頻率過高而導致的性能問題。
使用 scan_count 和 total_count 來計算進度,並且避免了每次執行緒完成後進行阻塞,從而確保進度條流暢更新。
這樣修改後,進度條的更新會更加流暢,並且不會因為執行緒的阻塞而導致 UI 更新延遲或卡頓。如果掃描過程中有大量設備,這樣的優化會顯著提高用戶體驗。
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import logging
from concurrent.futures import ThreadPoolExecutor
from tkinter import messagebox
from concurrent.futures import ThreadPoolExecutor
import threading
# 全局變量來跟踪進度
scan_count = 0 # 記錄已完成的掃描數量
total_count = 254 # 總共需要掃描的 IP 數量(從 1 到 254)
# 創建自定義的日誌處理器,將日誌輸出到 Text 小部件
class TkinterLogHandler(logging.Handler):
def __init__(self, text_widget, auto_scroll=True):
super().__init__()
self.text_widget = text_widget
self.auto_scroll = auto_scroll
def emit(self, record):
msg = self.format(record)
self.text_widget.insert(tk.END, msg + '\n')
if self.auto_scroll:
self.text_widget.yview(tk.END) # 自動滾動到底部
# 設定日誌
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號", "IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
# 獲取本機 IP 網段(預設網段)
def get_local_ip_prefix():
try:
local_ip = socket.gethostbyname(socket.gethostname()) # 獲取本機的 IP 位址
return '.'.join(local_ip.split('.')[:3]) + '.' # 回傳網段(例如 192.168.1.)
except Exception as e:
logging.error(f"無法獲取本機 IP 位址: {e}")
return "192.168.1." # 預設網段
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan(net_prefix):
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
update_scan_results(ip, mac, computer_name, status)
def port_scan(ip, port=80):
# 檢查 IP 是否為有效的 IPv4 格式
try:
ipaddress.IPv4Address(ip) # 如果 IP 無效,會拋出 ValueError
except ValueError:
logging.error(f"無效的 IP 地址:{ip}")
return False
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except socket.error as e:
logging.error(f"端口掃描錯誤({ip}):{e}")
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
update_scan_results(ip, mac_address, computer_name, status)
update_tree()
def update_scan_results(ip, mac, computer_name, status):
"""更新掃描結果,處理重複的 IP 和無效的 MAC"""
# 檢查 scan_results 是否已經存在該 IP,若有則檢查 MAC 是否為 "N/A"
for idx, (existing_ip, existing_mac, existing_computer_name, existing_status) in enumerate(scan_results):
if existing_ip == ip:
# 如果已有的 MAC 為 "N/A",且當前 MAC 地址有效,則更新 MAC
if existing_mac == "N/A" and mac != "N/A":
scan_results[idx] = (ip, mac, computer_name, status)
return # 找到並處理過就退出
# 若是新 IP 或無重複的情況,直接加入
scan_results.append((ip, mac, computer_name, status))
def update_tree(data=None):
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
def update_ui():
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
root.after(0, update_ui)
# 檢查網段格式是否有效
def is_valid_network_prefix(prefix):
# 檢查網段格式是否為 x.x.x.,也就是 3 個數字段
try:
parts = prefix.split(".")
if len(parts) != 4:
return False
# 只檢查前三個部分,確保它們是有效的數字(0-255)
return all(0 <= int(part) <= 255 for part in parts[:3]) and parts[3] == ""
except ValueError:
return False
# 更新進度條的函數
def update_progress_bar(value):
progress_bar['value'] = value
root.update_idletasks()
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
global scan_count
net_prefix = network_entry.get() # 從使用者輸入的 Entry 中獲取網段
if not net_prefix or not is_valid_network_prefix(net_prefix):
logging.error("請先輸入有效的網段!")
messagebox.showerror("錯誤", "請先輸入有效的網段!")
return
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
scan_count = 0 # 重置掃描進度
# 執行 ARP 掃描
arp_scan(net_prefix)
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
future_to_ip = {executor.submit(threaded_port_scan, f"{net_prefix}{i}"): i for i in range(1, 255)}
for future in future_to_ip:
future.result() # 確保每個任務的結果都被獲得
# 更新進度條
with lock:
scan_count += 1 # 每完成一個掃描,進行一次累加
progress = (scan_count / total_count) * 100
root.after(0, update_progress_bar, progress) # 使用 after 將更新交給主執行緒
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 用來讓使用者輸入網段的輸入框和標籤
network_label = tk.Label(root, text="輸入掃描網段 (例如: 192.168.1.)")
network_label.pack(padx=10, pady=5)
# 預設為本機的網段
local_ip_prefix = get_local_ip_prefix()
network_entry = tk.Entry(root, width=20)
network_entry.pack(padx=10, pady=5)
network_entry.insert(0, local_ip_prefix) # 預設顯示本機 IP 的網段
# 顯示提示文字
alert_label = tk.Label(root, text="", fg="red")
alert_label.pack(padx=10, pady=10)
# 創建一個 Text 小部件來顯示日誌訊息
log_text = tk.Text(root, height=3, width=80)
log_text.pack(side=tk.RIGHT,padx=5, pady=5)
# 設置自定義的日誌處理器
log_handler = TkinterLogHandler(log_text)
logging.getLogger().addHandler(log_handler)
# 創建掃描按鈕
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建清除結果按鈕
def clear_results():
tree.delete(*tree.get_children())
scan_results.clear()
clear_button = tk.Button(root, text="清除結果", command=clear_results)
clear_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建進度條
progress_bar = ttk.Progressbar(root, orient="horizontal", length=200, mode="determinate")
progress_bar.pack(side=tk.BOTTOM, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-10
-以下是將建議的改進整併回原程式的完整版本。改動的主要部分包括:
進度條更新方式優化:將進度條的更新集中進行,避免每次掃描後都更新進度條,這樣可以減少界面卡頓。
Treeview 更新方式優化:避免每次掃描後都清空並重新插入所有數據,而是僅更新已有的條目。
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import logging
from concurrent.futures import ThreadPoolExecutor
from tkinter import messagebox
# 創建自定義的日誌處理器,將日誌輸出到 Text 小部件
class TkinterLogHandler(logging.Handler):
def __init__(self, text_widget, auto_scroll=True):
super().__init__()
self.text_widget = text_widget
self.auto_scroll = auto_scroll
def emit(self, record):
msg = self.format(record)
self.text_widget.insert(tk.END, msg + '\n')
if self.auto_scroll:
self.text_widget.yview(tk.END) # 自動滾動到底部
# 設定日誌
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號", "IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
# 獲取本機 IP 網段(預設網段)
def get_local_ip_prefix():
try:
local_ip = socket.gethostbyname(socket.gethostname()) # 獲取本機的 IP 位址
return '.'.join(local_ip.split('.')[:3]) + '.' # 回傳網段(例如 192.168.1.)
except Exception as e:
logging.error(f"無法獲取本機 IP 位址: {e}")
return "192.168.1." # 預設網段
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan(net_prefix):
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
update_scan_results(ip, mac, computer_name, status)
def port_scan(ip, port=80):
# 檢查 IP 是否為有效的 IPv4 格式
try:
ipaddress.IPv4Address(ip) # 如果 IP 無效,會拋出 ValueError
except ValueError:
logging.error(f"無效的 IP 地址:{ip}")
return False
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except socket.error as e:
logging.error(f"端口掃描錯誤({ip}):{e}")
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
update_scan_results(ip, mac_address, computer_name, status)
update_tree()
def update_scan_results(ip, mac, computer_name, status):
"""更新掃描結果,處理重複的 IP 和無效的 MAC"""
# 檢查 scan_results 是否已經存在該 IP,若有則檢查 MAC 是否為 "N/A"
for idx, (existing_ip, existing_mac, existing_computer_name, existing_status) in enumerate(scan_results):
if existing_ip == ip:
# 如果已有的 MAC 為 "N/A",且當前 MAC 地址有效,則更新 MAC
if existing_mac == "N/A" and mac != "N/A":
scan_results[idx] = (ip, mac, computer_name, status)
return # 找到並處理過就退出
# 若是新 IP 或無重複的情況,直接加入
scan_results.append((ip, mac, computer_name, status))
def update_tree(data=None):
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
def update_ui():
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
root.after(0, update_ui)
# 檢查網段格式是否有效
def is_valid_network_prefix(prefix):
# 檢查網段格式是否為 x.x.x.,也就是 3 個數字段
try:
parts = prefix.split(".")
if len(parts) != 4:
return False
# 只檢查前三個部分,確保它們是有效的數字(0-255)
return all(0 <= int(part) <= 255 for part in parts[:3]) and parts[3] == ""
except ValueError:
return False
# 更新掃描進度
def update_progress_bar(value):
progress_bar['value'] = value
root.update_idletasks()
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
net_prefix = network_entry.get() # 從使用者輸入的 Entry 中獲取網段
if not net_prefix or not is_valid_network_prefix(net_prefix):
logging.error("請先輸入有效的網段!")
alert_label.config(text="請先輸入有效的網段!", fg="red")
messagebox.showerror("錯誤", "請先輸入有效的網段!")
return
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
# 執行 ARP 掃描
arp_scan(net_prefix)
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
future_to_ip = {executor.submit(threaded_port_scan, f"{net_prefix}{i}"): i for i in range(1, 255)}
for future in future_to_ip:
future.result() # 確保每個任務的結果都被獲得
update_progress_bar((future_to_ip[future] / 254) * 100)
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 用來讓使用者輸入網段的輸入框和標籤
network_label = tk.Label(root, text="輸入掃描網段 (例如: 192.168.1.)")
network_label.pack(padx=10, pady=5)
# 預設為本機的網段
local_ip_prefix = get_local_ip_prefix()
network_entry = tk.Entry(root, width=20)
network_entry.pack(padx=10, pady=5)
network_entry.insert(0, local_ip_prefix) # 預設顯示本機 IP 的網段
# 顯示提示文字
alert_label = tk.Label(root, text="", fg="red")
alert_label.pack(padx=10, pady=10)
# 創建一個 Text 小部件來顯示日誌訊息
log_text = tk.Text(root, height=3, width=80)
log_text.pack(side=tk.RIGHT,padx=5, pady=5)
# 設置自定義的日誌處理器
log_handler = TkinterLogHandler(log_text)
logging.getLogger().addHandler(log_handler)
# 創建掃描按鈕
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建清除結果按鈕
def clear_results():
tree.delete(*tree.get_children())
scan_results.clear()
clear_button = tk.Button(root, text="清除結果", command=clear_results)
clear_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建進度條
progress_bar = ttk.Progressbar(root, orient="horizontal", length=200, mode="determinate")
progress_bar.pack(side=tk.BOTTOM, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-09
-掃描速度最快
-主要改進:
網段格式檢查:更新了 is_valid_network_prefix 函數,確保每個部分的數字都在範圍內。
進度條顯示:新增了 ttk.Progressbar,用來顯示掃描進度。
清除結果按鈕:新增了 清除結果 按鈕,用來清空 Treeview 中的資料。
多執行緒掃描進度:在每個執行緒完成時更新進度條,讓用戶更直觀地看到掃描進度。
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import logging
from concurrent.futures import ThreadPoolExecutor
from tkinter import messagebox
# 創建自定義的日誌處理器,將日誌輸出到 Text 小部件
class TkinterLogHandler(logging.Handler):
def __init__(self, text_widget):
super().__init__()
self.text_widget = text_widget
def emit(self, record):
msg = self.format(record)
self.text_widget.insert(tk.END, msg + '\n')
self.text_widget.yview(tk.END) # 自動滾動到底部
# 設定日誌
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號", "IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
# 獲取本機 IP 網段(預設網段)
def get_local_ip_prefix():
try:
local_ip = socket.gethostbyname(socket.gethostname()) # 獲取本機的 IP 位址
return '.'.join(local_ip.split('.')[:3]) + '.' # 回傳網段(例如 192.168.1.)
except Exception as e:
logging.error(f"無法獲取本機 IP 位址: {e}")
return "192.168.1." # 預設網段
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan(net_prefix):
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
update_scan_results(ip, mac, computer_name, status)
def port_scan(ip, port=80):
# 檢查 IP 是否為有效的 IPv4 格式
try:
ipaddress.IPv4Address(ip) # 如果 IP 無效,會拋出 ValueError
except ValueError:
logging.error(f"無效的 IP 地址:{ip}")
return False
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except socket.error as e:
logging.error(f"端口掃描錯誤({ip}):{e}")
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
update_scan_results(ip, mac_address, computer_name, status)
update_tree()
def update_scan_results(ip, mac, computer_name, status):
"""更新掃描結果,處理重複的 IP 和無效的 MAC"""
# 檢查 scan_results 是否已經存在該 IP,若有則檢查 MAC 是否為 "N/A"
for idx, (existing_ip, existing_mac, existing_computer_name, existing_status) in enumerate(scan_results):
if existing_ip == ip:
# 如果已有的 MAC 為 "N/A",且當前 MAC 地址有效,則更新 MAC
if existing_mac == "N/A" and mac != "N/A":
scan_results[idx] = (ip, mac, computer_name, status)
return # 找到並處理過就退出
# 若是新 IP 或無重複的情況,直接加入
scan_results.append((ip, mac, computer_name, status))
def update_tree(data=None):
"""將結果顯示到 Treeview 中"""
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
# 檢查網段格式是否有效
def is_valid_network_prefix(prefix):
# 檢查網段格式是否為 x.x.x.
try:
parts = prefix.split(".")
if len(parts) != 4:
return False
return all(0 <= int(part) <= 255 for part in parts)
except ValueError:
return False
# 更新掃描進度
def update_progress_bar(value):
progress_bar['value'] = value
root.update_idletasks()
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
net_prefix = network_entry.get() # 從使用者輸入的 Entry 中獲取網段
if not net_prefix or not is_valid_network_prefix(net_prefix):
logging.error("請先輸入有效的網段!")
alert_label.config(text="請先輸入有效的網段!", fg="red")
return
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
# 執行 ARP 掃描
arp_scan(net_prefix)
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
future_to_ip = {executor.submit(threaded_port_scan, f"{net_prefix}{i}"): i for i in range(1, 255)}
for future in future_to_ip:
future.result() # 確保每個任務的結果都被獲得
update_progress_bar((future_to_ip[future] / 254) * 100)
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 用來讓使用者輸入網段的輸入框和標籤
network_label = tk.Label(root, text="輸入掃描網段 (例如: 192.168.1.)")
network_label.pack(padx=10, pady=5)
# 預設為本機的網段
local_ip_prefix = get_local_ip_prefix()
network_entry = tk.Entry(root, width=20)
network_entry.pack(padx=10, pady=5)
network_entry.insert(0, local_ip_prefix) # 預設顯示本機 IP 的網段
# 顯示提示文字
alert_label = tk.Label(root, text="", fg="red")
alert_label.pack(padx=10, pady=10)
# 創建一個 Text 小部件來顯示日誌訊息
log_text = tk.Text(root, height=3, width=80)
log_text.pack(side=tk.RIGHT,padx=5, pady=5)
# 設置自定義的日誌處理器
log_handler = TkinterLogHandler(log_text)
logging.getLogger().addHandler(log_handler)
# 創建掃描按鈕
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建清除結果按鈕
def clear_results():
tree.delete(*tree.get_children())
scan_results.clear()
clear_button = tk.Button(root, text="清除結果", command=clear_results)
clear_button.pack(side=tk.LEFT, padx=10, pady=10)
# 創建進度條
progress_bar = ttk.Progressbar(root, orient="horizontal", length=200, mode="determinate")
progress_bar.pack(side=tk.BOTTOM, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-08
-核心變更:
創建 TkinterLogHandler 類:這個類繼承自 logging.Handler,並重寫了 emit 方法,將日誌消息插入到 tk.Text 小部件中,並讓它自動滾動到底部。
創建 log_text 小部件:用來顯示日誌訊息的多行文本區域。這個小部件被用來顯示來自 logging 的訊息。
設定日誌處理器:在 logging 的設置中,我們新增了這個處理器,使得日誌能夠顯示在 UI 中。
現在,程式中的所有日誌訊息會自動顯示在 log_text 小部件中,並且會隨著新訊息的生成而滾動到底部,方便使用者查看進度或錯誤訊息。
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import logging
from concurrent.futures import ThreadPoolExecutor
# 創建自定義的日誌處理器,將日誌輸出到 Text 小部件
class TkinterLogHandler(logging.Handler):
def __init__(self, text_widget):
super().__init__()
self.text_widget = text_widget
def emit(self, record):
msg = self.format(record)
self.text_widget.insert(tk.END, msg + '\n')
self.text_widget.yview(tk.END) # 自動滾動到底部
# 設定日誌
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號", "IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
# 獲取本機 IP 網段(預設網段)
def get_local_ip_prefix():
try:
local_ip = socket.gethostbyname(socket.gethostname()) # 獲取本機的 IP 位址
return '.'.join(local_ip.split('.')[:3]) + '.' # 回傳網段(例如 192.168.1.)
except Exception as e:
logging.error(f"無法獲取本機 IP 位址: {e}")
return "192.168.1." # 預設網段
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan(net_prefix):
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
update_scan_results(ip, mac, computer_name, status)
def port_scan(ip, port=80):
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except Exception:
pass
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
update_scan_results(ip, mac_address, computer_name, status)
update_tree()
def update_scan_results(ip, mac, computer_name, status):
"""更新掃描結果,處理重複的 IP 和無效的 MAC"""
# 檢查 scan_results 是否已經存在該 IP,若有則檢查 MAC 是否為 "N/A"
for idx, (existing_ip, existing_mac, existing_computer_name, existing_status) in enumerate(scan_results):
if existing_ip == ip:
# 如果已有的 MAC 為 "N/A",且當前 MAC 地址有效,則更新 MAC
if existing_mac == "N/A" and mac != "N/A":
scan_results[idx] = (ip, mac, computer_name, status)
return # 找到並處理過就退出
# 若是新 IP 或無重複的情況,直接加入
scan_results.append((ip, mac, computer_name, status))
def update_tree(data=None):
"""將結果顯示到 Treeview 中"""
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
# 檢查網段格式是否有效
def is_valid_network_prefix(prefix):
# 檢查網段格式是否為 x.x.x.
return bool(re.match(r"^\d{1,3}(\.\d{1,3}){2}\.$", prefix))
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
net_prefix = network_entry.get() # 從使用者輸入的 Entry 中獲取網段
if not net_prefix or not is_valid_network_prefix(net_prefix):
logging.error("請先輸入有效的網段!")
alert_label.config(text="請先輸入有效的網段!", fg="red")
return
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
# 執行 ARP 掃描
arp_scan(net_prefix)
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
for i in range(1, 255):
ip = f"{net_prefix}{i}"
executor.submit(threaded_port_scan, ip)
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 用來讓使用者輸入網段的輸入框和標籤
network_label = tk.Label(root, text="輸入掃描網段 (例如: 192.168.1.)")
network_label.pack(padx=10, pady=5)
# 預設為本機的網段
local_ip_prefix = get_local_ip_prefix()
network_entry = tk.Entry(root, width=20)
network_entry.pack(padx=10, pady=5)
network_entry.insert(0, local_ip_prefix) # 預設顯示本機 IP 的網段
# 顯示提示文字
alert_label = tk.Label(root, text="", fg="red")
alert_label.pack(padx=10, pady=10)
# 創建一個 Text 小部件來顯示日誌訊息
log_text = tk.Text(root, height=3, width=80)
log_text.pack(side=tk.RIGHT,padx=5, pady=5)
# 設置自定義的日誌處理器
log_handler = TkinterLogHandler(log_text)
logging.getLogger().addHandler(log_handler)
# 按鈕:執行掃描
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-07
更新 update_scan_results 函數:
這個函數會在每次掃描時進行檢查,確保對於相同的 IP 地址,若 MAC 地址為 N/A,並且有新的有效 MAC 地址,則替換舊的資料。
scan_results 去重:
在更新掃描結果時,我們會檢查每個 IP 是否已經出現過。如果有重複的 IP 且舊資料中的 MAC 為 N/A,則更新為新 MAC 地址。
這樣,掃描完成後,對於 IP 重複且 MAC 地址為 N/A 的情況,就能自動更新為有效的 MAC 地址,並且資料顯示不會重複。
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import logging
from concurrent.futures import ThreadPoolExecutor
# 設定日誌
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號", "IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
# 獲取本機 IP 網段(預設網段)
def get_local_ip_prefix():
try:
local_ip = socket.gethostbyname(socket.gethostname()) # 獲取本機的 IP 位址
return '.'.join(local_ip.split('.')[:3]) + '.' # 回傳網段(例如 192.168.1.)
except Exception as e:
logging.error(f"無法獲取本機 IP 位址: {e}")
return "192.168.1." # 預設網段
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan(net_prefix):
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
update_scan_results(ip, mac, computer_name, status)
def port_scan(ip, port=80):
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except Exception:
pass
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
update_scan_results(ip, mac_address, computer_name, status)
update_tree()
def update_scan_results(ip, mac, computer_name, status):
"""更新掃描結果,處理重複的 IP 和無效的 MAC"""
# 檢查 scan_results 是否已經存在該 IP,若有則檢查 MAC 是否為 "N/A"
for idx, (existing_ip, existing_mac, existing_computer_name, existing_status) in enumerate(scan_results):
if existing_ip == ip:
# 如果已有的 MAC 為 "N/A",且當前 MAC 地址有效,則更新 MAC
if existing_mac == "N/A" and mac != "N/A":
scan_results[idx] = (ip, mac, computer_name, status)
return # 找到並處理過就退出
# 若是新 IP 或無重複的情況,直接加入
scan_results.append((ip, mac, computer_name, status))
def update_tree(data=None):
"""將結果顯示到 Treeview 中"""
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
# 檢查網段格式是否有效
def is_valid_network_prefix(prefix):
# 檢查網段格式是否為 x.x.x.
return bool(re.match(r"^\d{1,3}(\.\d{1,3}){2}\.$", prefix))
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
net_prefix = network_entry.get() # 從使用者輸入的 Entry 中獲取網段
if not net_prefix or not is_valid_network_prefix(net_prefix):
logging.error("請先輸入有效的網段!")
alert_label.config(text="請先輸入有效的網段!", fg="red")
return
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
# 執行 ARP 掃描
arp_scan(net_prefix)
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
for i in range(1, 255):
ip = f"{net_prefix}{i}"
executor.submit(threaded_port_scan, ip)
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 用來讓使用者輸入網段的輸入框和標籤
network_label = tk.Label(root, text="輸入掃描網段 (例如: 192.168.1.)")
network_label.pack(padx=10, pady=5)
# 預設為本機的網段
local_ip_prefix = get_local_ip_prefix()
network_entry = tk.Entry(root, width=20)
network_entry.pack(padx=10, pady=5)
network_entry.insert(0, local_ip_prefix) # 預設顯示本機 IP 的網段
# 顯示提示文字
alert_label = tk.Label(root, text="", fg="red")
alert_label.pack(padx=10, pady=5)
# 按鈕:執行掃描
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-05
-能手動指定要掃描的網段
-會先將本機的IP先填上
get_local_ip_prefix 函數:
該函數通過 socket.gethostbyname(socket.gethostname()) 獲取本機的 IP 位址,並回傳該 IP 位址的網段部分(例如 192.168.1.)。
network_entry.insert(0, local_ip_prefix):
將本機的網段(例如 192.168.1.)預設填入 network_entry 輸入框中,這樣使用者可以直接進行掃描,也可以根據需要修改。
alert_label 顯示提示訊息:
當使用者未輸入有效的網段並點擊「掃描區網」按鈕時,會顯示警告訊息,提示使用者輸入有效的網段。
預設網段:
如果使用者沒有修改輸入框中的網段,程式將會使用本機的 IP 網段進行掃描。
這樣的設計可以讓使用者快速使用預設的網段進行掃描,也可以自行修改網段,並且在未輸入有效網段時提供適當的提示。
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import logging
from concurrent.futures import ThreadPoolExecutor
# 設定日誌
logging.basicConfig(level=logging.INFO)
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號", "IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
# 獲取本機 IP 網段(預設網段)
def get_local_ip_prefix():
try:
local_ip = socket.gethostbyname(socket.gethostname()) # 獲取本機的 IP 位址
return '.'.join(local_ip.split('.')[:3]) + '.' # 回傳網段(例如 192.168.1.)
except Exception as e:
logging.error(f"無法獲取本機 IP 位址: {e}")
return "192.168.1." # 預設網段
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan(net_prefix):
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
scan_results.append((ip, mac, computer_name, status))
def port_scan(ip, port=80):
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except Exception:
pass
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
scan_results.append((ip, mac_address, computer_name, status))
update_tree()
def update_tree(data=None):
"""將結果顯示到 Treeview 中"""
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
net_prefix = network_entry.get() # 從使用者輸入的 Entry 中獲取網段
if not net_prefix:
logging.error("請先輸入網段!")
# 提示使用者輸入網段
alert_label.config(text="請先輸入有效的網段!", fg="red")
return
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
# 執行 ARP 掃描
arp_scan(net_prefix)
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
for i in range(1, 255):
ip = f"{net_prefix}{i}"
executor.submit(threaded_port_scan, ip)
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 用來讓使用者輸入網段的輸入框和標籤
network_label = tk.Label(root, text="輸入掃描網段 (例如: 192.168.1.)")
network_label.pack(padx=10, pady=5)
# 預設為本機的網段
local_ip_prefix = get_local_ip_prefix()
network_entry = tk.Entry(root, width=20)
network_entry.pack(padx=10, pady=5)
network_entry.insert(0, local_ip_prefix) # 預設顯示本機 IP 的網段
# 顯示提示文字
alert_label = tk.Label(root, text="", fg="red")
alert_label.pack(padx=10, pady=5)
# 按鈕:執行掃描
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-03
-程式整理
在程式碼中,scan_network 函數定義了兩次。第一個是使用執行緒來進行掃描,第二個是使用 ThreadPoolExecutor。應該保留其中一個,並刪除另一個。建議保留使用 ThreadPoolExecutor 的版本,因為它更高效且易於管理多個執行緒。
python
複製程式碼
# 使用 ThreadPoolExecutor 進行多執行緒掃描
from concurrent.futures import ThreadPoolExecutor
def scan_network():
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
# 執行 ARP 掃描
arp_scan()
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
net_prefix = "192.168.1."
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
for i in range(1, 255):
ip = f"{net_prefix}{i}"
executor.submit(threaded_port_scan, ip)
# 確保最終結果更新
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
root.after() 的使用可能會導致更新過程出現不必要的延遲或重複操作。在這裡,update_tree 的內容應該簡化並移除 root.after。由於 threaded_port_scan 已經是異步執行,因此不需要再進行額外的延遲處理。
建議直接在 update_tree 中執行更新,而不需要使用 root.after:
python
複製程式碼
def update_tree(data=None):
"""將結果顯示到 Treeview 中"""
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
get_computer_name 內部使用 subprocess.run 來呼叫 nslookup,但如果 nslookup 失敗(例如,無法解析該 IP 地址的名稱),則可能會直接返回 None。應該確保 get_computer_name 的返回值不會是 None,而應返回 "N/A"。
修改建議:
python
複製程式碼
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
在 wake_on_lan 函數中,從 tree.item(selected_item) 取出的值中獲得的 MAC 地址是從 values[1],這樣的做法是正確的,但如果選中的條目沒有有效的 MAC 地址,可能會出現問題。應該增加一個檢查,確保選中的條目有有效的 MAC 地址,並在無效時顯示提示。
修改建議:
python
複製程式碼
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
為了更清晰地追蹤程式的運行,日誌紀錄應該包含更多的細節,例如成功掃描的設備數量,或者端口掃描的結果。
例如,在掃描結束後可以增加如下的日誌紀錄:
python
複製程式碼
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
在 scan_network 中,您對掃描結果進行排序時,最好是只在最終更新結果時再排序,這樣可以避免多次排序造成的性能開銷。
由於 scan_results 是在多個執行緒中被訪問和修改的,所以已經有使用 lock 來保護數據的部分。這是正確的做法,可以保證多執行緒操作的安全性。
保留使用 ThreadPoolExecutor 的版本 scan_network,並去除重複定義。
優化 update_tree,避免過度使用 after。
增強 get_computer_name 以確保返回有效結果。
在 wake_on_lan 函數中加入更多的錯誤處理和日誌紀錄。
在 scan_network 完成後加入成功掃描設備數量的日誌。
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import platform
import logging
from concurrent.futures import ThreadPoolExecutor
# 設定日誌
logging.basicConfig(level=logging.INFO)
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號","IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A" # 確保返回有效值
def arp_scan():
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
scan_results.append((ip, mac, computer_name, status))
def port_scan(ip, port=80):
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except Exception:
pass
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
scan_results.append((ip, mac_address, computer_name, status))
update_tree()
def update_tree(data=None):
"""將結果顯示到 Treeview 中"""
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
# 清空現有顯示
tree.delete(*tree.get_children())
for index, (ip, mac_address, computer_name, status) in enumerate(data):
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status)) # 加入序號
# 使用 ThreadPoolExecutor 進行多執行緒掃描
def scan_network():
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
# 執行 ARP 掃描
arp_scan()
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
net_prefix = "192.168.1."
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
for i in range(1, 255):
ip = f"{net_prefix}{i}"
executor.submit(threaded_port_scan, ip)
# 確保最終結果更新
logging.info(f"掃描完成,共找到 {len(scan_results)} 台設備")
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
logging.info(f"發送遠端開機封包至 MAC: {mac_address}")
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 按鈕:執行掃描
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
root.mainloop()
1131104-wakeonlan-test02-02
-網卡現在使用,無法選擇,需改進
-功能
-掃描後無本機IP
-遠端開機未測,送出指令沒有提醒
-會列出廣播的IP
-無法保留上一次查詢結果
-
import tkinter as tk
from tkinter import ttk
import subprocess
import threading
import re
import socket
from wakeonlan import send_magic_packet
import ipaddress
import platform
import logging
# 設定日誌
logging.basicConfig(level=logging.INFO)
# 初始化 UI 和設定
root = tk.Tk()
root.title("區域網路掃描與遠端開機工具")
root.geometry("1000x600")
# 更新 Treeview 欄位,添加「電腦名稱」
tree = ttk.Treeview(root, columns=("序號","IP Address", "MAC Address", "Computer Name", "Status"), show="headings")
tree.heading("序號", text="序號") # 新增序號欄位的標題
tree.heading("IP Address", text="IP 位址")
tree.heading("MAC Address", text="MAC 位址")
tree.heading("Computer Name", text="電腦名稱") # 新增的欄位
tree.heading("Status", text="狀態")
# 設定所有欄位的資料置中
tree.column("序號", anchor="center")
tree.column("IP Address", anchor="center")
tree.column("MAC Address", anchor="center")
tree.column("Computer Name", anchor="center")
tree.column("Status", anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# 儲存掃描結果的列表
scan_results = []
# 上鎖機制確保執行緒安全
lock = threading.Lock()
def get_computer_name(ip):
# 嘗試通過 nslookup 獲取電腦名稱
try:
result = subprocess.run(["nslookup", ip], capture_output=True, text=True)
for line in result.stdout.splitlines():
if "name =" in line:
return line.split("=")[-1].strip()
except Exception as e:
logging.error(f"獲取電腦名稱時發生錯誤: {e}")
return "N/A"
def arp_scan():
# 執行 ARP 命令來獲取局域網中的設備
output = subprocess.run(["arp", "-a"], capture_output=True, text=True)
for line in output.stdout.splitlines():
# 使用正則表達式來匹配 IP 和 MAC 地址
ip_match = re.search(r"\d+\.\d+\.\d+\.\d+", line)
mac_match = re.search(r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})", line)
if ip_match and mac_match:
ip = ip_match.group(0)
mac = mac_match.group(0)
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On" # 假設在 ARP 表中找到的設備處於開啟狀態
with lock:
scan_results.append((ip, mac, computer_name, status))
def port_scan(ip, port=80):
# 嘗試連接到指定 IP 的特定端口,以檢查是否存在
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5) # 設置超時時間
if sock.connect_ex((ip, port)) == 0:
return True
except Exception:
pass
return False
def threaded_port_scan(ip):
# 執行執行緒中的端口掃描並更新結果
if port_scan(ip, port=80):
mac_address = "N/A" # 如果無法通過 ARP 獲取 MAC,可以留空或設為 N/A
computer_name = get_computer_name(ip) # 獲取電腦名稱
status = "On"
with lock:
scan_results.append((ip, mac_address, computer_name, status))
update_tree()
def scan_network():
tree.delete(*tree.get_children()) # 清空 Treeview 資料
scan_results.clear() # 清除之前的掃描結果
# 執行 ARP 掃描
arp_scan()
update_tree() # 顯示 ARP 掃描結果
# 端口掃描:為每個 IP 啟動執行緒來掃描
net_prefix = "192.168.1."
threads = []
for i in range(1, 255):
ip = f"{net_prefix}{i}"
t = threading.Thread(target=threaded_port_scan, args=(ip,))
threads.append(t)
t.start()
# 等待所有執行緒完成
for t in threads:
t.join()
# 最終更新排序後的結果
sorted_results = sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0]))
update_tree(sorted_results)
#def update_tree(data=None):
"""將結果顯示到 Treeview 中"""
#if data is None:
# data = scan_results
#else:
# data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
# 清空現有顯示
#tree.delete(*tree.get_children())
#for ip, mac_address, computer_name, status in data:
# tree.insert("", tk.END, values=(ip, mac_address, computer_name, status))
def update_tree(data=None):
"""將結果顯示到 Treeview 中"""
if data is None:
data = scan_results
else:
data = sorted(data, key=lambda x: ipaddress.IPv4Address(x[0]))
# 使用 after 在主線程中更新
root.after(0, lambda: tree.delete(*tree.get_children()))
for index, (ip, mac_address, computer_name, status) in enumerate(data):
root.after(0, lambda index=index, ip=ip, mac_address=mac_address, computer_name=computer_name, status=status:
tree.insert("", tk.END, values=(index + 1, ip, mac_address, computer_name, status))) # 加入序號
# 使用 ThreadPoolExecutor 進行多執行緒掃描
from concurrent.futures import ThreadPoolExecutor
def scan_network():
tree.delete(*tree.get_children())
scan_results.clear()
arp_scan()
update_tree()
net_prefix = "192.168.1."
with ThreadPoolExecutor(max_workers=20) as executor: # 設定最大工作執行緒數
for i in range(1, 255):
ip = f"{net_prefix}{i}"
executor.submit(threaded_port_scan, ip)
# 確保最終結果更新
update_tree(sorted(scan_results, key=lambda x: ipaddress.IPv4Address(x[0])))
# 發送遠端開機封包
def wake_on_lan():
selected_item = tree.selection()
if selected_item:
mac_address = tree.item(selected_item)["values"][1] # 確保正確取用 MAC 位址
if mac_address != "N/A":
send_magic_packet(mac_address)
else:
logging.warning("選取的電腦沒有有效的 MAC 位址。")
# 按鈕:執行掃描
scan_button = tk.Button(root, text="掃描區網", command=lambda: threading.Thread(target=scan_network).start())
scan_button.pack(side=tk.LEFT, padx=10, pady=10)
# 遠端開機按鈕
wake_button = tk.Button(root, text="遠端開機", command=wake_on_lan)
wake_button.pack(side=tk.LEFT, padx=10, pady=10)
root.mainloop()