ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python跨平台WiFi扫描器:从系统命令到信号强度解析

Python跨平台WiFi扫描器:从系统命令到信号强度解析 说实话“用Python扫描周围WiFi”这个需求我最早是在办公室被逼出来的。当时工位离路由器远WiFi信号常年两格我想知道附近到底有哪些网络、哪个信号更好、哪个频段更空好决定是自己加路由还是蹭楼下那个信号还行的5G频段。结果一搜网上全是“破解WiFi密码”之类的教程正经讲扫描识别的少之又少还动不动就要装各种驱动级库。后来我把Windows、macOS、Linux三套系统都试了一遍踩了不少坑才攒出这套稳定可靠的跨平台方案。这篇内容适合这几类朋友想用Python做网络自动化运维的、在搞物联网或室内定位项目的、以及纯粹好奇自己周围无线环境到底是什么样的人。读完你不仅能拿到可直接运行的跨平台扫描器代码还能理解它背后的原理——比如为什么没有现成的Python库、系统命令输出的差异在哪、信号强度怎么换算成距离。不碰密码、不碰攻击只聊合法的WiFi环境感知。1. 为什么没有现成的Python库从底层到系统工具的思路拆解1.1 Python里为什么找不到“扫描WiFi”的库先说结论Python标准库里确实没有扫描WiFi的模块第三方库也几乎没有完全跨平台且稳定好用的。这不是Python生态不行而是WiFi扫描这件事本身就绕不开操作系统。WiFi扫描的本质是无线网卡切换到监听模式主动发送Probe Request或被动接收各AP发出的Beacon帧然后把结果上报给驱动驱动再交给操作系统的网络管理服务。从网卡到驱动到系统服务每一层都是C语言写的接口各不相同。Windows有WlanAPImacOS有CoreWLANLinux有nl80211Python要直接调这些接口要么写C扩展要么用ctypes去怼工作量巨大不说还容易因为权限和系统版本差异直接崩掉。所以业内最常见的做法不是去硬调底层API而是“曲线救国”调用系统自带的命令行工具解析它们的文本输出。命令行工具是官方维护的系统版本更新它们也跟着更新稳定性和兼容性都由系统厂商保证。Python只需要负责拼命令、读输出、写正则解析干净又省事。1.2 三大平台的“原生”扫描命令各平台自带的WiFi扫描命令如下记住这三个后面全是在这三条命令基础上做文章。平台命令输出格式是否需管理员权限Windowsnetsh wlan show networks modebssid文本表格否普通用户可执行macOS/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s文本表格否但终端需授权位置权限Linuxnmcli -f IN-USE,SSID,BSSID,SIGNAL,SECURITY dev wifi list文本表格否需要NetworkManager服务Linuxiw dev wlan0 scan原始字段块是需要root这里有个关键选择逻辑能用普通权限完成的事绝不往sudo上靠。因为一旦依赖管理员权限你的脚本在别人机器上就跑不起来了自动化运维的通用性就没了。Windows的netsh和Linux的nmcli都不需要额外权限macOS的airport虽然路径藏在私有框架里但也不需要sudo只是首次运行要给你的终端App开“定位服务”权限——这个坑后面细说。2. 平台命令逐一拆解输出长什么样、怎么解析最稳2.1 Windowsnetsh输出的中英文坑Windows上跑netsh wlan show networks modebssid输出会按“网络”分组每个网络下面列出BSSID、信号、认证方式等。英文系统的字段名是Signal中文系统是信号这个差异足以让新手懵半小时。我的做法是尽可能不依赖字段名而是按“SSID冒号”定位。一条记录以SSID x : name开头紧接着的几行都是它的属性。解析时先切块再用正则匹配每个块的BSSID和信号字段。import subprocess import re def scan_windows(): result subprocess.run( [netsh, wlan, show, networks, modebssid], capture_outputTrue, textTrue, encodinggbk, errorsreplace ) output result.stdout # 按SSID行切分 blocks re.split(rSSID \d :, output)[1:] nets [] for block in blocks: block_lines block.strip().splitlines() ssid_line block_lines[0].strip() if block_lines else # 注意中文系统会输出“信号 : 80%”英文输出“Signal : 80%” signal_match re.search(r(?:信号|Signal)\s*:\s*(\d), block) bssid_match re.search(rBSSID\s*\d*\s*:\s*([0-9a-fA-F:]), block) security_match re.search(r(?:身份验证|Authentication)\s*:\s*(.), block) nets.append({ ssid: ssid_line, signal: signal_match.group(1) if signal_match else None, bssid: bssid_match.group(1) if bssid_match else None, security: security_match.group(1).strip() if security_match else None, }) return nets编码这里我用了gbk加errorsreplace。Windows中文版的控制台默认编码是GBK不指定的话subprocess拿到的字节流按UTF-8解码会乱码。但也不能写死有些环境可能是cp936所以errorsreplace保证即使遇到个别特殊字符也不会整个脚本崩溃。还有个细节netsh的扫描结果是系统缓存的不是实时扫描。调用后系统会发起一次扫描但结果有一定延迟。实测刚开机或刚重新连接WiFi后立刻查经常是空列表等几秒再查就出来了。2.2 macOSairport命令与那恼人的权限macOS的WiFi工具藏在私有框架里正确路径是/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s这条命令打印的列包括SSID、BSSID、RSSI、CHANNEL、HT、CC、SECURITY。RSSI是负数比如-45 dBm数字越大越接近0信号越强。这和Windows用百分比表示完全是两套体系跨平台时一定要统一换算。import subprocess import re AIRPORT (/System/Library/PrivateFrameworks/Apple80211.framework/ Versions/Current/Resources/airport) def scan_macos(): result subprocess.run([AIRPORT, -s], capture_outputTrue, textTrue, timeout15) lines result.stdout.strip().splitlines()[1:] # 跳过表头 nets [] for line in lines: parts line.split() if len(parts) 3: continue # SSID可能含空格需要从前面找最后一列是安全类型 security parts[-1] ssid .join(parts[:-6]) if len(parts) 6 else parts[0] bssid parts[-6] rssi parts[-5] channel parts[-4] nets.append({ ssid: ssid, bssid: bssid, rssi: rssi, channel: channel, security: security, }) return netsmacOS解析里最阴间的就是SSID带空格。比如TP-LINK_5G 2.4G这种名字split()会把单词全切开如果你只取固定下标SSID永远是残缺的。所以要从右往左定位固定列倒数第一个是SECURITY倒数第二个是CC倒数第三个是HT倒数第四是CHANNEL倒数第五是RSSI倒数第六是BSSID剩下的全是SSID。倒着解析这是处理不定长SSID的唯一稳妥姿势。权限问题很隐蔽新版macOS大概是Catalina之后要求调用airport的终端进程必须有“位置服务”权限。如果不授权命令不会报错只是输出为空或者只显示当前连接的网络。解决方法是去系统设置 - 隐私与安全性 - 定位服务找到你的终端App比如iTerm、Terminal把开关打开。这个坑不写出来你能排查一下午。2.3 Linuxnmcli与iw的双方案Linux是三个平台里扫描选项最丰富的但也是坑最多的。桌面发行版一般都有NetworkManager用nmcli最省事nmcli -f IN-USE,SSID,BSSID,SIGNAL,SECURITY,CHAN dev wifi list输出是整齐的表格SSID带空格也不影响因为这是通过列宽对齐的。SIGNAL是0-100的百分比和Windows一致IN-USE里带*表示当前正在连接的网络。import subprocess def scan_linux_nmcli(): result subprocess.run( [nmcli, -f, IN-USE,SSID,BSSID,SIGNAL,SECURITY,CHAN, dev, wifi, list], capture_outputTrue, textTrue, timeout15 ) lines result.stdout.strip().splitlines()[1:] nets [] for line in lines: # 用真正意义上的空白分割先处理表头行格式 parts line.split() if len(parts) 4: continue in_use * in parts[0] ssid parts[1] bssid parts[2] if len(parts) 2 else None signal parts[3] if len(parts) 3 else None security parts[-2] if len(parts) 4 else None chan parts[-1] if len(parts) 5 else None nets.append({ ssid: ssid, bssid: bssid, signal: signal, security: security, channel: chan, in_use: in_use, }) return nets但服务器或精简系统上可能没有NetworkManager这时候用iwsudo iw dev wlan0 scan | grep -E SSID|signal:|freq:iw的输出是字段块一行BSS 44:44:44:xx:xx开头下面跟着freq:、signal:、SSID:等。它的SSID是十六进制编码的如果SSID包含不可打印字符会显示成SSID: 5c5450...这种Hex串解析时需要注意解码。nmcli和iw怎么选我的建议优先nmcli普通用户权限输出规整字段齐全。iw留给需要频段、速率等底层信息的场景。还有一点用iw时别在脚本里直接sudo应该让脚本运行者用sudo执行整个脚本或者在systemd服务里配好权限否则每次调用都弹密码框自动化直接废了。3. 写一个跨平台扫描器统一接口封装3.1 数据模型一条扫描记录包含什么三个平台输出的字段不完全一样Windows有百分比信号没有RSSImacOS有RSSI没有百分比Linux的nmcli两者都变相给了SIGNAL百分比iw给出的是RSSI。要统一先定数据模型。from dataclasses import dataclass, field from typing import Optional dataclass class WifiScanResult: ssid: Optional[str] None bssid: Optional[str] None signal_percent: Optional[int] None # 统一换算成0-100 rssi: Optional[int] None # 保留原始dBm数值 channel: Optional[int] None security: Optional[str] None in_use: bool False property def signal_level(self) - str: 把信号强度映射为直观等级方便人看 if self.signal_percent is not None: if self.signal_percent 70: return 强 elif self.signal_percent 40: return 中 elif self.signal_percent 20: return 弱 return 极弱 if self.rssi is not None: if self.rssi -50: return 强 elif self.rssi -65: return 中 elif self.rssi -80: return 弱 return 极弱 return 未知双字段设计是为了保留原始信息。signal_percent用于跨平台排序比较rssi用于需要精确信号强度的场景比如后面的距离估算。换算规则很简单RSSI范围一般在-30到-95 dBm之间把-30映射为100-95映射为0即可def rssi_to_percent(rssi: int) - int: 粗略把RSSI(dBm)映射为0-100百分比 clamped max(-95, min(-30, rssi)) return int((rssi 95) / 65 * 100)这是个工程近似不是标准定义但用来排序足够了。3.2 抽象接口与各平台实现有了数据模型再定义一个抽象基类让每个平台实现自己的扫描逻辑import platform from abc import ABC, abstractmethod class WifiScanner(ABC): abstractmethod def scan(self) - list[WifiScanResult]: 扫描周围WiFi返回结果列表 class WindowsScanner(WifiScanner): def scan(self): raw scan_windows() # 复用上一节函数 return [WifiScanResult( ssidn[ssid], bssidn[bssid], signal_percentint(n[signal]) if n[signal] else None, securityn[security], ) for n in raw] class MacOSScanner(WifiScanner): def scan(self): raw scan_macos() results [] for n in raw: try: rssi int(n[rssi]) percent rssi_to_percent(rssi) except (TypeError, ValueError): rssi, percent None, None results.append(WifiScanResult( ssidn[ssid], bssidn[bssid], rssirssi, signal_percentpercent, channelint(n[channel]) if n[channel] else None, securityn[security], )) return results class LinuxScanner(WifiScanner): def scan(self): raw scan_linux_nmcli() return [WifiScanResult( ssidn[ssid], bssidn[bssid], signal_percentint(n[signal]) if n[signal] else None, channelint(n[channel]) if n[channel] else None, securityn[security], in_usen[in_use], ) for n in raw]抽象层的价值在于业务代码不需要关心平台差异。比如你想打印所有信号强度在70以上的网络只需要对着WifiScanResult操作不用写三套if-else。3.3 工厂函数与模拟器选择实现类的逻辑扔进一个工厂函数def get_scanner() - WifiScanner: system platform.system() if system Windows: return WindowsScanner() elif system Darwin: return MacOSScanner() elif system Linux: return LinuxScanner() else: raise RuntimeError(f不支持的平台: {system})但这里有个实际问题你要是手上没有对应平台的机器代码根本没法调试。所以我强烈建议顺手写一个MockScanner生成一份模拟数据。import random import time class MockScanner(WifiScanner): 随机生成模拟WiFi数据方便在没有硬件环境时测试 def scan(self): fake_bssids [fAA:BB:CC:{i:02X}:{i1:02X}:{i2:02X} for i in range(8)] fake_ssids [OfficeWiFi, Home_5G, TP-LINK_1234, Xiaomi_ABCD, ChinaNet-8888, Guest-free, WorkLab, RouterA] results [] for i, (ssid, bssid) in enumerate(zip(fake_ssids, fake_bssids)): results.append(WifiScanResult( ssidssid, bssidbssid, signal_percentrandom.randint(15, 95), channelrandom.choice([1, 6, 11, 36, 149]), securityrandom.choice([WPA2, WPA3, OPEN, WPA2-Enterprise]), )) return results这样在没有无线网卡的服务器上也能跑通整条处理链路等部署到目标机器时再把工厂函数切换回真实扫描器。这个习惯帮我省了不知道多少远程调试的时间。4. 进阶玩法信号换算、自动选网、可视化和Web展示4.1 从信号强度估算距离对数路径损耗模型扫描到的是信号强度RSSI但很多时候你更想知道“这个路由器离我多远”。信号强度与距离之间有近似关系最常用的是对数距离路径损耗模型[ RSSI P_{tx} - 10 \times n \times \log_{10}(d) ]其中 (P_{tx}) 是1米处的参考信号强度通常取-40 dBm到-45 dBm之间(n) 是路径损耗指数室内通常取2.5到3.5之间空旷环境取2左右。反过来由RSSI推算距离[ d 10^{\frac{P_{tx} - RSSI}{10 \times n}} ]写成Pythonimport math def rssi_to_distance(rssi: int, tx_power: float -40, n: float 2.8) - float: 将RSSI(dBm)估算为距离(米) if rssi 0: return -1 return round(10 ** ((tx_power - rssi) / (10 * n)), 2)这个公式算出来是参考值误差通常在30%以上。因为实际信号还受墙壁、人体、金属物体遮挡影响同距离下路由器天线方向不同RSSI能差出10dBm。所以它只适合做粗粒度判断比如“哪个AP离我更近”“我走到哪片区域信号更好”不适合做精确定位。要精确定位得用指纹定位法那是另一个大工程后面有机会单独写。4.2 自动筛选并连接最强的WiFi合法合理的使用场景扫描结果有了你可能会想能不能让它自动连上信号最强的那个网络可以前提是只在你自己的网络、或你有权管理的网络范围内用比如让家里老人用的电脑开机自动连信号最好的那台路由器。Windows连接命令netsh wlan connect ssid名称 name名称macOS连接命令networksetup -setairportnetwork en0 SSID 密码Linux连接命令nmcli dev wifi connect SSID password 密码Python里直接subprocess调用这些命令配合前面扫描出来的最强信号就能实现“自动切换”。我实际做过的场景是办公室有两台AP一个叫Office-2.4G一个叫Office-5G5G信号弱但速度快2.4G信号强但速度慢。脚本每5分钟扫描一次如果5G信号低于-75 dBm就切到2.4G恢复正常再切回来。省事又稳定。接口上可以在WifiScanner下再加一个连接方法class WindowsScanner(WifiScanner): def connect(self, ssid: str) - bool: cmd [netsh, wlan, connect, fssid{ssid}, fname{ssid}] result subprocess.run(cmd, capture_outputTrue, textTrue) return result.returncode 0不过连接操作对权限和系统状态的依赖更大Windows某些精简系统会拒绝netsh直接连接macOS还会弹系统授权框没法全自动。别指望做成完全无人值守半自动就够了。4.3 信号可视化用散点图直接“看”到环境文字列表不直观我习惯把扫描结果画成图。横轴是信道纵轴是信号强度每个点是一个AP点的大小代表信号强度。这样一眼就能看出哪个信道拥挤、哪个信道干净对选路由器信道特别有用。import matplotlib.pyplot as plt def plot_wifi_scan(results: list[WifiScanResult], save_pathwifi_scan.png): fig, ax plt.subplots(figsize(10, 6)) for r in results: color green if r.signal_percent and r.signal_percent 60 else \ orange if r.signal_percent and r.signal_percent 35 else red ax.scatter(r.channel or 0, r.signal_percent or 0, s(r.signal_percent or 10) * 5, alpha0.7, colorcolor) ax.annotate(r.ssid or hidden, (r.channel or 0, r.signal_percent or 0), xytext(5, 5), textcoordsoffset points, fontsize8) ax.set_xlabel(信道) ax.set_ylabel(信号强度 (%)) ax.set_title(周边WiFi扫描可视化) ax.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.savefig(save_path, dpi150)如果你还想做“走出房间看信号变化”可以配合手动记录坐标点把结果叠加成热力图。那种情况用folium画地图热力层更合适适合户外的WiFi测绘室内精度不行。4.4 局域网Web展示让手机也能看扫描结果不想在电脑前看用Flask搭一个小服务把扫描结果以JSON接口暴露出来前端页面定时刷新。手机上打开同一局域网的地址就能看。写这个不是为了炫技而是因为很多时候人坐在工位上WiFi信息在另一台台式机上直接开网页方便多了。from flask import Flask, jsonify, render_template_string import threading app Flask(__name__) scanner get_scanner() HTML !DOCTYPE html html headtitleWiFi Scanner/title meta charsetutf-8/head body h2周围WiFi/h2 table border1 cellpadding6 idtbl trthSSID/thth信号/thth信道/thth安全/th/tr /table script function refresh() { fetch(/api/wifi).then(r r.json()).then(data { const tb document.getElementById(tbl); tb.innerHTML trthSSID/thth信号/thth信道/thth安全/th/tr; data.forEach(n { const tr document.createElement(tr); tr.innerHTML td${n.ssid||(隐藏)}/tdtd${n.signal_percent||-}/tdtd${n.channel||-}/tdtd${n.security||-}/td; tb.appendChild(tr); }); }); } setInterval(refresh, 5000); refresh(); /script /body /html app.route(/) def index(): return render_template_string(HTML) app.route(/api/wifi) def api_wifi(): results scanner.scan() return jsonify([r.__dict__ for r in results]) if __name__ __main__: app.run(host0.0.0.0, port8000)注意把host设为0.0.0.0这样局域网内其他设备才能访问。如果不想引入Flask依赖也可以用Python内置的http.server自己拼JSON但代码会更啰嗦Flask确实是最省事的选择。5. 那些文档里不会写的坑权限、编码、静默失败5.1 常见问题速查表现象原因解决办法Windows扫描结果为空系统未发起扫描或网卡被禁用先手动在系统设置里关闭再打开WiFi等3秒再跑脚本macOS输出为空但命令能执行终端没有位置服务权限系统设置 - 隐私与安全性 - 定位服务给终端开权限Linux nmcli提示“Unknown command”NetworkManager未安装或未运行systemctl start NetworkManager或改用iw方案解析的SSID乱码Windows控制台编码不是GBKsubprocess加errorsreplace或先转成UTF-8信号跳变剧烈WiFi扫描是瞬时结果受干扰大连续扫描3次取中位数不要用单次值决策macOS升级后airport路径失效苹果每年都在调整私有框架升级后先验证路径失效就用wdutil info替代这里最值得展开的是“信号跳变”。WiFi信号本身就是波动的站在同一个位置隔几秒扫描一次RSSI波动5-10dBm是家常便饭。如果你做自动切换网络单次低于阈值就切换会反复“横跳”。解决办法是连续采集几次取中位数或者用滞回区间——比如低于-75才切走高于-70才切回来中间地带维持现状。这个经验是从实际自动漫游脚本里踩出来的一开始没加滞回逻辑结果整个办公室的人看我在那反复重连WiFi。5.2 几个独家避坑心得第一别用高频扫描。有些新手会把扫描套进一个while True里每秒钟刷一次。无线网卡在扫描时会短暂脱离当前连接去监听其他信道扫描太频繁会导致当前连接卡顿甚至掉线。实测下来扫描间隔最小建议5秒做持续性监控用10-30秒更合理。第二注意隐藏SSID。很多路由器会开启“隐藏网络”这类AP在扫描结果里SSID显示为空。如果你把SSID作为字典的键这些网络会互相覆盖最后只剩一个。处理办法是优先用BSSID做标识SSID只做展示。第三安全类型字段别轻视。扫描结果里security显示为OPEN的热点不代表可以随便连很多是商场的开放认证网关连上后会跳转网页给你限速和广告。区分“开放但需Web认证”和“真正无限制”需要额外探测这个水比较深用到再说。第四关于一堆“工具箱”“破解字典”类内容我的态度很明确扫描识别是合法且有用的网络管理手段但基于扫描去做暴力破解、未授权连接既违反法律也违背技术伦理。写工具时心里要有边界只对自己拥有或有权测试的网络做操作。本文所有连接示例都建立在你有合法凭证的前提下。再说一个跨平台开发的小习惯把平台相关的命令字符串全部集中放在一个配置模块里不要散落在业务代码中。我吃过一次亏在Windows上开发调试好的脚本部署到Linux服务器上结果所有命令散在七八个函数里花了一下午才改完。一点收尾的体会这套跨平台WiFi扫描器写完之后我在三个平台分别跑了一段时间最大的感受是代码本身不难难的是对不同系统“脾性”的理解。netsh的中文输出、airport的私有路径更新、Linux的NetworkManager依赖每一个都是只有实际踩过才知道的经验。后来我又在这个基础上扩展了日志记录功能每次扫描结果追加到SQLite积累一周数据就能看出办公室AP的信号变化规律哪个时间段信道拥堵、什么时候网速最差一目了然。如果你也想做类似的事情建议从简单地把结果存成JSON开始先跑通链路再去琢磨复杂的分析功能。这个内容后续还可以往两个方向扩展一是把扫描器做成常驻服务通过消息队列把数据推给监控平台二是接入室内定位用多个AP的信号强度做指纹匹配解决“找不到工位”这种小问题。哪个方向更值得研究看你自己的使用场景扫描数据这块的基础打牢了后面怎么做都顺。
返回列表