news 2025/12/31 21:17:51

用 Python 打造一个图形化局域网扫描器:实战网络设备发现工具

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Python 打造一个图形化局域网扫描器:实战网络设备发现工具

在日常的网络管理、安全测试或家庭网络排查中,我们常常需要快速了解当前局域网中有哪些设备在线。虽然命令行工具(如nmaparp-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 -aarp -n解析。

4. 并发控制:threading

  • 为每个 IP 启动独立线程,提升扫描速度;
  • 限制最大并发数(如 50 线程),避免系统资源耗尽;
  • 使用daemon=True确保主线程退出时子线程自动终止。

三、代码结构详解

3.1 主类NetworkScanner

classNetworkScanner: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用于优雅停止。

💡 技巧:使用gridpack布局管理器组合,实现灵活排版。


3.3 核心扫描逻辑

(1)Ping 探测ping(ip)
defping(self,ip):# 根据平台选择 ping 命令ifWindows:ping-n1-w500ipelse:ping-c1-W0.5ip# 正则提取响应时间time_match=re.search(r"时间[=<](\d+)ms|time[=<](\d+\.?\d*)\s*ms",output)
  • 超时设为 500ms,避免卡顿;
  • 成功条件:输出中包含TTL=(Windows)或ttl=(Linux/macOS)。
(2)获取主机名get_hostname(ip)
try:returnsocket.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-254192.168.1);
  • 清空旧结果,禁用按钮,启动后台线程。
扫描主循环scan_range(base_ip, start, end)
foriinrange(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)# 控制并发数 ≤ 50iflen(threads)>=50:fortinthreads:t.join()threads=[]
  • 每次批量启动最多 50 个线程,避免资源爆炸;
  • 扫描完成后恢复 UI 状态。
安全停止stop_scan()
self.scanning=False# 设置标志位# 后台线程检查该标志后自动退出
  • 无需强制 kill 线程,实现优雅终止。

四、跨平台兼容性处理

功能WindowsLinux / macOS
Ping 命令ping -n 1 -w 500ping -c 1 -W 0.5
ARP 查询arp -a iparp -n ip
TTL 关键字"TTL=""ttl="

通过platform.system().lower()动态判断系统类型,确保命令正确执行。


五、运行效果与优化建议

示例输入:

192.168.1.1-254

输出结果(表格):

IP地址主机名MAC地址响应时间(ms)
192.168.1.1router.homeaa:bb:cc:dd:ee:ff2
192.168.1.105DESKTOP-ABC11:22:33:44:55:668

优化方向:

  1. 增加端口扫描:结合socket.connect_ex()检测开放端口;
  2. 厂商识别:根据 MAC 前缀查询设备厂商(需 OUI 数据库);
  3. 导出结果:支持 CSV/Excel 导出;
  4. 图标美化:为不同设备类型(手机、PC、IoT)添加图标;
  5. 性能提升:改用asyncio+aioping实现异步 Ping(更高效)。

六、总结

本文通过一个完整的局域网扫描器项目,展示了如何结合 Python 的多种能力:

  • GUI 开发(Tkinter)
  • 系统交互(subprocess)
  • 网络编程(socket)
  • 多线程并发
  • 正则解析
  • 跨平台适配

该项目不仅实用,更是学习 Python 综合应用的绝佳案例。你可以将其作为网络工具箱的一部分,或在此基础上扩展更高级的功能(如漏洞扫描、设备画像等)。

🔧源码已完整提供,复制即可运行!
📌注意:部分功能(如 MAC 获取)在虚拟机或受限网络中可能受限,建议在真实局域网环境测试。


附:运行要求

  • Python 3.6+
  • 无第三方依赖(仅标准库)
  • 完整代码
importtkinterastkfromtkinterimportttk,scrolledtextimportsocketimportthreadingimportplatformimportsubprocessimportrefromdatetimeimportdatetimeclassNetworkScanner:def__init__(self,root):self.root=root self.root.title("局域网扫描器")self.root.geometry("800x600")# 创建界面元素self.create_widgets()defcreate_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")# 设置列标题forcolincolumns: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=Falsedefget_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()returnipexcept:return"127.0.0.1"defping(self,ip):"""Ping指定IP,检查是否在线"""try:# 根据操作系统选择ping命令ifplatform.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="inoutputor"ttl="inoutput:# 提取响应时间time_match=re.search(r"时间[=<](\d+)ms|time[=<](\d+\.?\d*)\s*ms",output)iftime_match:time_ms=time_match.group(1)iftime_match.group(1)elsetime_match.group(2)else:time_ms="N/A"returnTrue,time_msreturnFalse,"N/A"except:returnFalse,"N/A"defget_hostname(self,ip):"""获取IP对应的主机名"""try:hostname=socket.gethostbyaddr(ip)[0]returnhostnameexcept:return"未知"defget_mac_address(self,ip):"""获取IP对应的MAC地址"""try:# 根据操作系统选择命令ifplatform.system().lower()=="windows":# 先ping一下确保ARP表中有该IPsubprocess.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)ifmac_match:returnmac_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)ifmac_match:returnmac_match.group(0)return"未知"except:return"未知"defscan_ip(self,ip,update_callback):"""扫描单个IP"""ifnotself.scanning:returnis_online,response_time=self.ping(ip)ifis_online:hostname=self.get_hostname(ip)mac_address=self.get_mac_address(ip)update_callback(ip,hostname,mac_address,response_time)defupdate_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()defstart_scan(self):"""开始扫描"""# 清空结果foriteminself.tree.get_children():self.tree.delete(item)# 解析IP范围ip_range=self.ip_entry.get().strip()ifnotip_range:self.status_var.set("请输入IP范围")returntry:if"-"inip_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-254last_octet_base=ip_range start=1end=254except:self.status_var.set("IP范围格式错误,应为如 192.168.1.1-254")return# 更新UI状态self.scanning=Trueself.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=Trueself.scan_thread.start()# 更新状态栏self.status_var.set(f"正在扫描{last_octet_base}.{start}{last_octet_base}.{end}...")defscan_range(self,base_ip,start,end):"""扫描IP范围"""threads=[]count=0foriinrange(start,end+1):ifnotself.scanning:breakip=f"{base_ip}.{i}"# 创建并启动线程thread=threading.Thread(target=self.scan_ip,args=(ip,self.update_tree))thread.daemon=Truethread.start()threads.append(thread)# 限制并发线程数iflen(threads)>=50:fortinthreads:t.join()threads=[]# 更新进度条count+=1self.progress.config(value=count)# 等待剩余线程完成fortinthreads:t.join()# 更新UI状态self.scanning=Falseself.scan_button.config(state=tk.NORMAL)self.stop_button.config(state=tk.DISABLED)self.status_var.set(f"扫描完成,发现{len(self.tree.get_children())}个在线设备")defstop_scan(self):"""停止扫描"""self.scanning=Falseself.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()
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!