用 Python 打造一個圖形化局域網掃描器:實戰網絡設備發現工具
在日常的網絡管理、安全測試或家庭網絡排查中,我們常常需要快速瞭解當前局域網中有哪些設備在線。雖然命令行工具(如 nmap、arp-scan)功能強大,但對於非技術人員來説門檻較高。本文將帶你從零開始,使用 Python + Tkinter + 多線程 + 系統命令調用 構建一個圖形化局域網掃描器,具備 IP 掃描、主機名解析、MAC 地址獲取和響應時間顯示等核心功能。
一、項目目標與功能預覽
我們的局域網掃描器將實現以下功能:
- ✅ 圖形用户界面(GUI),操作直觀;
- ✅ 支持自定義 IP 範圍輸入(如
192.168.1.1-254); - ✅ 併發 Ping 掃描,快速檢測在線設備;
- ✅ 自動解析主機名(Hostname);
- ✅ 通過 ARP 表獲取 MAC 地址;
- ✅ 顯示 Ping 響應時間(ms);
- ✅ 實時進度條與狀態提示;
- ✅ 支持隨時停止掃描;
- ✅ 跨平台兼容(Windows / Linux / macOS)。
最終效果如下圖所示:
頂部輸入框可設置掃描範圍,點擊“開始掃描”後,下方表格實時列出在線設備的 IP、主機名、MAC 和延遲,底部狀態欄顯示進度信息。
二、技術選型與核心模塊
1. GUI 框架:tkinter
- Python 內置,無需額外安裝;
- 提供
ttk主題控件,界面更現代; - 支持
Treeview表格、Progressbar進度條、ScrolledText等組件。
2. 網絡探測:subprocess + 系統 ping 命令
- 利用操作系統原生命令進行 ICMP 探測;
- 通過正則表達式解析響應時間;
- 兼容 Windows (
ping -n) 與 Unix-like (ping -c) 參數差異。
3. 主機信息獲取:
- 主機名:
socket.gethostbyaddr(ip); - MAC 地址:先
ping觸發 ARP 緩存,再調用arp -a或arp -n解析。
4. 併發控制:threading
- 為每個 IP 啓動獨立線程,提升掃描速度;
- 限制最大併發數(如 50 線程),避免系統資源耗盡;
- 使用
daemon=True確保主線程退出時子線程自動終止。
三、代碼結構詳解
3.1 主類 NetworkScanner
class NetworkScanner:
def __init__(self, root):
self.root = root
self.root.title("局域網掃描器")
self.root.geometry("800x600")
self.create_widgets()
初始化主窗口並創建 UI 組件。
3.2 創建圖形界面 create_widgets()
- 輸入區域:IP 範圍輸入框 + “開始/停止”按鈕;
- 進度條:
ttk.Progressbar顯示掃描進度; - 結果表格:
ttk.Treeview展示四列數據(IP、主機名、MAC、延遲); - 狀態欄:底部
Label實時反饋操作狀態; - 線程控制標誌:
self.scanning = False用於優雅停止。
💡 技巧:使用
grid和pack佈局管理器組合,實現靈活排版。
3.3 核心掃描邏輯
(1)Ping 探測 ping(ip)
def ping(self, ip):
# 根據平台選擇 ping 命令
if Windows: ping -n 1 -w 500 ip
else: ping -c 1 -W 0.5 ip
# 正則提取響應時間
time_match = re.search(r"時間[=<](\d+)ms|time[=<](\d+\.?\d*)\s*ms", output)
- 超時設為 500ms,避免卡頓;
- 成功條件:輸出中包含
TTL=(Windows)或ttl=(Linux/macOS)。
(2)獲取主機名 get_hostname(ip)
try:
return socket.gethostbyaddr(ip)[0]
except:
return "未知"
- 反向 DNS 查詢,失敗則返回“未知”。
(3)獲取 MAC 地址 get_mac_address(ip)
# 先 ping 一次,確保 ARP 表有記錄
subprocess.call(["ping", "-c", "1", ip], ...)
# 執行 arp -a (Win) 或 arp -n (Unix)
output = subprocess.check_output(["arp", ...])
# 正則匹配 MAC:([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})
⚠️ 注意:此方法依賴本地 ARP 緩存,若目標未通信過可能無法獲取。
3.4 多線程掃描控制
啓動掃描 start_scan()
- 解析 IP 範圍(支持
192.168.1.1-254或192.168.1); - 清空舊結果,禁用按鈕,啓動後台線程。
掃描主循環 scan_range(base_ip, start, end)
for i in range(start, end+1):
ip = f"{base_ip}.{i}"
thread = threading.Thread(target=self.scan_ip, args=(ip, self.update_tree))
thread.start()
threads.append(thread)
# 控制併發數 ≤ 50
if len(threads) >= 50:
for t in threads: t.join()
threads = []
- 每次批量啓動最多 50 個線程,避免資源爆炸;
- 掃描完成後恢復 UI 狀態。
安全停止 stop_scan()
self.scanning = False # 設置標誌位
# 後台線程檢查該標誌後自動退出
- 無需強制 kill 線程,實現優雅終止。
四、跨平台兼容性處理
| 功能 | Windows | Linux / macOS |
|---|---|---|
| Ping 命令 | ping -n 1 -w 500 |
ping -c 1 -W 0.5 |
| ARP 查詢 | arp -a ip |
arp -n ip |
| TTL 關鍵字 | "TTL=" |
"ttl=" |
通過 platform.system().lower() 動態判斷系統類型,確保命令正確執行。
五、運行效果與優化建議
示例輸入:
192.168.1.1-254
輸出結果(表格):
| IP地址 | 主機名 | MAC地址 | 響應時間(ms) |
|---|---|---|---|
| 192.168.1.1 | router.home | aa:bb:cc:dd:ee:ff | 2 |
| 192.168.1.105 | DESKTOP-ABC | 11:22:33:44:55:66 | 8 |
優化方向:
- 增加端口掃描:結合
socket.connect_ex()檢測開放端口; - 廠商識別:根據 MAC 前綴查詢設備廠商(需 OUI 數據庫);
- 導出結果:支持 CSV/Excel 導出;
- 圖標美化:為不同設備類型(手機、PC、IoT)添加圖標;
- 性能提升:改用
asyncio+aioping實現異步 Ping(更高效)。
六、總結
本文通過一個完整的 局域網掃描器項目,展示瞭如何結合 Python 的多種能力:
- GUI 開發(Tkinter)
- 系統交互(subprocess)
- 網絡編程(socket)
- 多線程併發
- 正則解析
- 跨平台適配
該項目不僅實用,更是學習 Python 綜合應用的絕佳案例。你可以將其作為網絡工具箱的一部分,或在此基礎上擴展更高級的功能(如漏洞掃描、設備畫像等)。
🔧 源碼已完整提供,複製即可運行!
📌 注意:部分功能(如 MAC 獲取)在虛擬機或受限網絡中可能受限,建議在真實局域網環境測試。
附:運行要求
- Python 3.6+
- 無第三方依賴(僅標準庫)
- 完整代碼
import tkinter as tk
from tkinter import ttk, scrolledtext
import socket
import threading
import platform
import subprocess
import re
from datetime import datetime
class NetworkScanner:
def __init__(self, root):
self.root = root
self.root.title("局域網掃描器")
self.root.geometry("800x600")
# 創建界面元素
self.create_widgets()
def create_widgets(self):
# 頂部框架 - 輸入區域
top_frame = ttk.Frame(self.root, padding="10")
top_frame.pack(fill=tk.X)
# IP範圍輸入
ttk.Label(top_frame, text="IP範圍:").grid(row=0, column=0, padx=5, pady=5)
self.ip_entry = ttk.Entry(top_frame, width=20)
self.ip_entry.grid(row=0, column=1, padx=5, pady=5)
self.ip_entry.insert(0, "192.168.1.1-254")
# 掃描按鈕
self.scan_button = ttk.Button(top_frame, text="開始掃描", command=self.start_scan)
self.scan_button.grid(row=0, column=2, padx=5, pady=5)
# 停止按鈕
self.stop_button = ttk.Button(top_frame, text="停止掃描", command=self.stop_scan, state=tk.DISABLED)
self.stop_button.grid(row=0, column=3, padx=5, pady=5)
# 進度條
self.progress = ttk.Progressbar(self.root, orient="horizontal", mode="determinate")
self.progress.pack(fill=tk.X, padx=10, pady=5)
# 結果顯示區域
result_frame = ttk.Frame(self.root)
result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# 創建Treeview顯示結果
columns = ("IP地址", "主機名", "MAC地址", "響應時間(ms)")
self.tree = ttk.Treeview(result_frame, columns=columns, show="headings")
# 設置列標題
for col in columns:
self.tree.heading(col, text=col)
self.tree.column(col, width=150, anchor=tk.CENTER)
# 添加滾動條
scrollbar = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscroll=scrollbar.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 底部狀態欄
self.status_var = tk.StringVar()
self.status_var.set("就緒")
status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# 掃描標誌
self.scanning = False
def get_local_ip(self):
"""獲取本機IP地址"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except:
return "127.0.0.1"
def ping(self, ip):
"""Ping指定IP,檢查是否在線"""
try:
# 根據操作系統選擇ping命令
if platform.system().lower() == "windows":
output = subprocess.check_output(["ping", "-n", "1", "-w", "500", ip],
stderr=subprocess.STDOUT,
universal_newlines=True)
else:
output = subprocess.check_output(["ping", "-c", "1", "-W", "0.5", ip],
stderr=subprocess.STDOUT,
universal_newlines=True)
# 檢查ping結果
if "TTL=" in output or "ttl=" in output:
# 提取響應時間
time_match = re.search(r"時間[=<](\d+)ms|time[=<](\d+\.?\d*)\s*ms", output)
if time_match:
time_ms = time_match.group(1) if time_match.group(1) else time_match.group(2)
else:
time_ms = "N/A"
return True, time_ms
return False, "N/A"
except:
return False, "N/A"
def get_hostname(self, ip):
"""獲取IP對應的主機名"""
try:
hostname = socket.gethostbyaddr(ip)[0]
return hostname
except:
return "未知"
def get_mac_address(self, ip):
"""獲取IP對應的MAC地址"""
try:
# 根據操作系統選擇命令
if platform.system().lower() == "windows":
# 先ping一下確保ARP表中有該IP
subprocess.call(["ping", "-n", "1", ip],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
# 查詢ARP表
output = subprocess.check_output(["arp", "-a", ip],
stderr=subprocess.STDOUT,
universal_newlines=True)
# 提取MAC地址
mac_match = re.search(r"([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})", output)
if mac_match:
return mac_match.group(0)
else:
# Linux/Mac系統
subprocess.call(["ping", "-c", "1", ip],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
output = subprocess.check_output(["arp", "-n", ip],
stderr=subprocess.STDOUT,
universal_newlines=True)
mac_match = re.search(r"([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})", output)
if mac_match:
return mac_match.group(0)
return "未知"
except:
return "未知"
def scan_ip(self, ip, update_callback):
"""掃描單個IP"""
if not self.scanning:
return
is_online, response_time = self.ping(ip)
if is_online:
hostname = self.get_hostname(ip)
mac_address = self.get_mac_address(ip)
update_callback(ip, hostname, mac_address, response_time)
def update_tree(self, ip, hostname, mac_address, response_time):
"""更新Treeview"""
self.tree.insert("", tk.END, values=(ip, hostname, mac_address, response_time))
self.root.update_idletasks()
def start_scan(self):
"""開始掃描"""
# 清空結果
for item in self.tree.get_children():
self.tree.delete(item)
# 解析IP範圍
ip_range = self.ip_entry.get().strip()
if not ip_range:
self.status_var.set("請輸入IP範圍")
return
try:
if "-" in ip_range:
base_ip, range_part = ip_range.split("-")
last_octet_base = ".".join(base_ip.split(".")[:-1])
start = int(base_ip.split(".")[-1])
end = int(range_part)
else:
# 如果只輸入了網絡段,掃描1-254
last_octet_base = ip_range
start = 1
end = 254
except:
self.status_var.set("IP範圍格式錯誤,應為如 192.168.1.1-254")
return
# 更新UI狀態
self.scanning = True
self.scan_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.progress.config(maximum=end-start+1)
self.progress.config(value=0)
# 啓動掃描線程
self.scan_thread = threading.Thread(
target=self.scan_range,
args=(last_octet_base, start, end)
)
self.scan_thread.daemon = True
self.scan_thread.start()
# 更新狀態欄
self.status_var.set(f"正在掃描 {last_octet_base}.{start} 到 {last_octet_base}.{end}...")
def scan_range(self, base_ip, start, end):
"""掃描IP範圍"""
threads = []
count = 0
for i in range(start, end+1):
if not self.scanning:
break
ip = f"{base_ip}.{i}"
# 創建並啓動線程
thread = threading.Thread(
target=self.scan_ip,
args=(ip, self.update_tree)
)
thread.daemon = True
thread.start()
threads.append(thread)
# 限制併發線程數
if len(threads) >= 50:
for t in threads:
t.join()
threads = []
# 更新進度條
count += 1
self.progress.config(value=count)
# 等待剩餘線程完成
for t in threads:
t.join()
# 更新UI狀態
self.scanning = False
self.scan_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.status_var.set(f"掃描完成,發現 {len(self.tree.get_children())} 個在線設備")
def stop_scan(self):
"""停止掃描"""
self.scanning = False
self.status_var.set("正在停止掃描...")
self.scan_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
if __name__ == "__main__":
root = tk.Tk()
app = NetworkScanner(root)
root.mainloop()