用pywifi扫描WiFi信号:Python实现无线网络覆盖评估与信道优化
1. 先说结论pywifi能扫到什么不能扫到什么去年帮朋友看新办公室的无线网络我拿着手机在工位之间来回走信号时强时弱还得拿笔画图记录。折腾半天我决定写个脚本把所有区域的WiFi信号一次性扫下来。这就是我用pywifi的起因。pywifi是Python操作无线网卡的第三方库核心功能就是帮助我们扫描周围WIFI拿到SSID、BSSID、信号强度、加密方式这些信息再配合数据处理和画图就能做覆盖评估、信道优化这一类的实际工作。这篇我会把实际用过的完整流程、代码、踩坑记录都整理出来想用Python做WiFi环境普查的同学可以直接照着改。1.1 它就是一排信号侦察兵很多人第一次听到pywifi以为它是什么高深的安全工具其实不是。它就是让Python能直接指挥无线网卡干活的一层封装。在Windows上它走的是系统自带的Native WIFI API也就是WLANAPI.dll那套接口在Linux上它通过nl80211或者wireless extensions去和驱动对话。说得直白点你的无线网卡本身就是一部小型无线电接收机pywifi只是把扫描周围有哪些WiFi信号这个动作翻译成了Python能调用的函数。我打过一个比方它是派出去的一排信号侦察兵每个侦察兵负责上报我在这个位置看到了哪些无线接入点信号多强用什么方式加密。它不参与传输数据、不入侵任何网络只是把无线电环境里的可见信息收集回来。这个定位非常重要直接决定了它能做什么、不能做什么。1.2 扫描结果里到底有什么pywifi扫描一次之后返回的是一个列表里面是Profile对象的集合。每个Profile代表一个探测到的无线信号常用的字段我整理成了表格字段含义示例值ssid无线网络名称Office_WiFibssid接入点MAC地址8c:xx:xx:xx:xx:xxsignal信号强度单位是dBm-53auth认证算法类型const.AUTH_ALG_OPENakm密钥管理方式const.AKM_TYPE_WPAPSKcipher加密套件const.CIPHER_TYPE_CCMPfreq工作频率部分版本才有2462信号强度是最有价值的部分。它是负数单位是dBm数值越大说明信号越好。比如-45是满格水平-70基本上就处于可连但不太稳定的边缘区低于-80就基本告别稳定连接了。这里有个常见坑pywifi不同版本里bssid字段的返回格式不一样旧版本可能返回字节串bytes而不是字符串所以打印之前最好用type()确认一下必要时做decode处理我后面会专门讲。1.3 能力边界先泼一盆冷水pywifi只能看到无线电暴露出来的公开信息看不到网络的加密内容更不会提供任何破解能力。它扫描不出WPS PIN抓不了数据包也不做握手包分析。网上有些热词比如wifi密码破译wifi字典这些跟pywifi的定位完全不同属于另一类工具不在本文讨论范围内我也强烈不建议拿那套思路去碰不属于自己的网络。理解这个边界很重要pywifi的典型场景是管理自己一亩三分地的网络。比如我家路由器信号在卧室为何总是丢包、办公室哪个角落是信号死角、两栋楼之间无线组网的信道是不是互相打架这些用pywifi都能很好地回答。至于别人家的WiFi扫描到SSID和信号强度到此为止就够了。2. 环境准备网卡选不对后面全是泪写代码之前先确认三件事操作系统、无线网卡驱动、Python环境。这一步看起来不起眼但实际上我见过太多人卡在这里。最典型的例子就是在Linux虚拟机上装好了pywifi一运行却报找不到网卡或者扫描结果永远为空。下面分平台说。2.1 Windows下的准备工作Windows平台上pywifi依赖系统的WLAN AutoConfig服务这个服务必须处于启动状态。一般在桌面右下角能看到WiFi图标、能正常连接无线网就说明服务和驱动没问题。如果用户自己把网卡驱动装得很乱或者用的是那种插上就能用的USB无线网卡但没装官方驱动建议先去设备管理器里确认一下无线网卡的状态。打开设备管理器找到网络适配器看有没有带WirelessWLAN字样的设备。如果前面有黄色感叹号说明驱动有问题这时装什么Python库都白搭。确认网卡正常之后开启管理员终端执行安装命令pip install pywifiWindows下需要注意程序运行时最好以管理员身份运行。虽然单纯扫描不一定会触发权限限制但某些机器上访问WLAN API会要求提升权限为了少踩坑我都是直接管理员模式跑。2.2 Linux下的准备和驱动问题Linux下稍微曲折一点。首先确保系统能看到无线网卡命令是iwconfig如果输出里出现wlan0之类的接口说明无线网卡已经被识别。如果没有运行sudo apt install wireless-tools sudo apt install network-manager装完插上网卡再看一次。这里我要特别提一下开发板场景最近我帮朋友调一块RK3566开发板刷了Ubuntu系统后发现无线网卡完全不工作就是热词里经常出现的rk3566构建ubuntu22.04系统没有wifi驱动那类问题。这种通常不是pywifi的锅而是内核里没打上对应的wifi模块驱动需要先RTFM一下芯片型号对应的驱动源码编译进内核或者做成ko模块加载。Linux上还有一个重要前置条件扫描必须要有root权限。普通用户执行时pywifi会直接抛PermissionError提示你操作无线接口需要特权。所以脚本要么用sudo运行要么在代码里检测权限时给出友好提示别让用户一脸懵。2.3 装完先跑一个冒烟测试环境装好之后我习惯先跑一个最短的、不带任何业务逻辑的冒烟测试确认pywifi能正常访问网卡接口import pywifi from pywifi import const wifi pywifi.PyWiFi() ifaces wifi.interfaces() if not ifaces: print(没有找到无线网卡接口) else: for i, iface in enumerate(ifaces): print(f接口[{i}]: {iface.name()})这段代码的作用就是列出当前机器上有几个无线网卡接口。笔记本一般有一个插了USB无线网卡会有两个。能看到接口名称环境就算是通了。3. 核心实现先跑通单次扫描再考虑花活环境通了下面进入正题如何实现扫描周围WIFI。先给出一版最短可运行代码然后解释它为什么是这个写法最后封装成实用的工具函数。3.1 五行代码先看效果import time import pywifi wifi pywifi.PyWiFi() iface wifi.interfaces()[0] iface.scan() time.sleep(2) results iface.scan_results()我第一次跑这段代码时立刻踩了一个隐蔽的坑scan()之后马上调scan_results()拿到的结果要么是空的要么是上一次的旧数据。原因在于scan()是异步非阻塞的它只是向无线网卡发出了扫描指令网卡要花几百毫秒到两三秒的时间做信道扫描然后才能拿到完整结果。所以必须在scan()之后sleep一段时间。我实测取2秒比较稳妥太短容易漏信号太长发监measure时间太长没必要。拿到results之后遍历输出即可。完整的演示代码是这样的import time import pywifi wifi pywifi.PyWiFi() iface wifi.interfaces()[0] print(当前无线网卡:, iface.name()) iface.scan() time.sleep(2) results iface.scan_results() for r in results: print(fSSID: {r.ssid} | BSSID: {r.bssid} | 信号: {r.signal} dBm | f认证: {r.auth} | 密钥管理: {r.akm} | 加密: {r.cipher})在办公室跑一下输出大概长这样当前无线网卡: Intel(R) Wi-Fi 6 AX201 SSID: Office_5G | BSSID: 8c:xx:xx:xx:xx:01 | 信号: -51 dBm | 认证: 2 | 密钥管理: 3 | 加密: 5注意认证、密钥管理、加密这几个字段打印出来是数字因为pywifi在const模块里定义了一系列常量比如AKM_TYPE_WPAPSK的取值是3。直接看数字不直观我后面会写一个翻译函数把它们转成可读文本。3.2 封装成一个可复用的扫描函数单次扫描只是起点。实际使用中我需要的是指定某个网卡接口扫描并返回结构化结果这样一个稳定的函数。封装的时候我会把sleep时间、接口选择、异常处理都考虑进去import time import pywifi def scan_wifi(iface_index0, scan_delay2): wifi pywifi.PyWiFi() ifaces wifi.interfaces() if iface_index len(ifaces): raise ValueError(f接口索引 {iface_index} 超出范围可用接口数: {len(ifaces)}) iface ifaces[iface_index] scan_time time.time() iface.scan() time.sleep(scan_delay) results iface.scan_results() return iface.name(), results, time.time() - scan_time这里我特意返回了耗时和接口名因为做实际项目时这些信息很有用。比如你在每个点位测量时想知道这次扫描到底等了多久、用的哪张网卡留着它们方便记录到日志里。3.3 把数字常量翻译成人话pywifi的const模块里定义了大量常量我整理了最常用的几个写成一个转换函数const常量数值说明AUTH_ALG_OPEN1开放认证无密码AUTH_ALG_SHARED2共享密钥认证极少见AKM_TYPE_NONE0无密钥管理AKM_TYPE_WPA1WPA企业级AKM_TYPE_WPAPSK3WPA个人模式通常就是输入密码AKM_TYPE_WPA24WPA2企业级AKM_TYPE_WPA2PSK5WPA2个人模式CIPHER_TYPE_CCMP5AES加密CIPHER_TYPE_TKIP4TKIP加密对应的转换函数from pywifi import const def auth_to_text(auth): if auth const.AUTH_ALG_OPEN: return 开放网络 elif auth const.AUTH_ALG_SHARED: return 共享密钥 return 未知认证 def akm_to_text(akm): mapping { const.AKM_TYPE_NONE: 无密码, const.AKM_TYPE_WPA: WPA企业, const.AKM_TYPE_WPAPSK: WPA个人, const.AKM_TYPE_WPA2: WPA2企业, const.AKM_TYPE_WPA2PSK: WPA2个人, } return mapping.get(akm, 未知)这样输出就变成了SSID: Office_5G | BSSID: 8c:xx:xx:xx:xx:01 | 信号: -51 dBm | 认证: 开放认证 | 密钥管理: WPA2个人一眼就能看懂这个网络是给员工用的会议室加密WiFi还是开放给访客的公共网络。这对于梳理一个区域的网络布点非常有用。3.4 持续扫描追踪信号波动单次扫描只能反映某一瞬间的无线电状态。实际做覆盖测试时我会在不同地点各扫描5到10次取平均值因为WiFi信号在室内会因为人员走动、电磁干扰、微波炉之类的原因上下波动。持续扫描的代码也很简单import time import statistics import pywifi def monoitor_signals(iface_index0, rounds5, interval3): wifi pywifi.PyWiFi() iface wifi.interfaces()[0] samples {} for round_no in range(rounds): iface.scan() time.sleep(2) for r in iface.scan_results(): key r.ssid or r.bssid if key not in samples: samples[key] {bssid: r.bssid, signals: []} samples[key][signals].append(r.signal) time.sleep(interval) stats [] for name, info in samples.items(): avg_signal statistics.mean(info[signals]) max_signal max(info[signals]) stats.append((name, info[bssid], avg_signal, max_signal)) return stats多轮采样之后再对比不同位置的avg_signal可靠性会高很多。这也是我做覆盖评估的标准动作每个采样点至少收5轮数据用平均信号强度说话。4. 结果落地把扫描数据存成CSV和图表扫描本身只是数据采集工作流的前半段。真正有价值的是把数据整理归档再可视化出来这样才方便做前后对比或者交给团队其他人查看。4.1 保存成CSV文件我会把所有扫描明细落盘包含时间戳、接口名、SSID、BSSID、信号强度、认证类型这些字段import csv import time import pywifi from pywifi import const def save_scan_report(filenamewifi_scan.csv, iface_index0): wifi pywifi.PyWiFi() iface wifi.interfaces()[iface_index] iface.scan() time.sleep(2) with open(filename, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([timestamp, ssid, bssid, signal, auth, akm]) for r in iface.scan_results(): auth auth_to_text(r.auth) akm akm_to_text(r.akm) writer.writerow([time.strftime(%Y-%m-%d %H:%M:%S), r.ssid, r.bssid, r.signal, auth, akm]) print(f扫描结果已保存到 {filename})每次测量前我会在命令行里传一个文件名参数比如room_a_1.csvroom_a_2.csv这样每个采样点对应一个独立文件后续分析时直接按文件名前缀分组统计。4.2 用matplotlib画信号图数据有了画个柱状图最直观。横坐标是SSID纵坐标是信号强度按从强到弱排序import matplotlib.pyplot as plt def plot_signals(records, title周边WiFi信号强度): records_sorted sorted(records, keylambda x: x[2]) names [r[0] \n r[1].split(:)[-1] for r in records_sorted] signals [r[2] for r in records_sorted] plt.figure(figsize(10, 6)) colors [#2ca02c if s -60 else #ff7f0e if s -75 else #d62728 for s in signals] plt.bar(range(len(signals)), signals, colorcolors) plt.xticks(range(len(signals)), names, rotation45, haright) plt.ylabel(信号强度 (dBm)) plt.title(title) plt.axhline(-75, colorgray, linestyle--, linewidth1) plt.tight_layout() plt.savefig(wifi_scan_visual.png, dpi150)颜色规则是我自己定的绿的是信号良好大于-60橙色属于可用但一般-60到-75红色是信号差低于-75。那条灰色虚线是-75门槛低于它基本不建议做视频会议或大流量传输。4.3 顺手做个信道拥挤分析扫描结果里有个常被忽略的字段是freq它对应工作信道。2.4GHz频段常见信道有1、6、11这三个不重叠信道5GHz则信道更多。通过统计同一信道上出现了多少个BSSID能快速判断这个区域无线信道是不是已经挤爆了。我写过一个简单的统计逻辑from collections import Counter def channel_stats(results): channels [] for r in results: if r.freq: # 2.4G频段大概从2400到2500MHz5G频段从5000到5900MHz if 2400 r.freq 2500: channels.append(f2.4G-{channel_from_freq(r.freq)}) else: channels.append(f5G-{channel_from_freq(r.freq)}) return Counter(channels).most_common()freq值有低频段的需要知道整个频段是2400多MHz还是5000多MHz判断方式就是上面if判断的范围。根据统计结果如果某个信道聚集了大量接入点就建议把自家的WiFi手动调到人少的信道上这比盲目双频合一有效得多。5. 实战排坑我在Windows和Linux上踩过的雷这部分是正文里最值钱的部分。我把真实环境里跑pywifi遇到的高频坑以及排查思路完整写出来。5.1 第一次scan_results()返回空列表这个问题出现频率极高。原因就是扫描指令异步执行数据还没准备好。解决办法是scan()之后sleep 2秒。但还有另一种情况sleep之后依然是空。这时需要确认网卡是否真的在工作。Windows下如果WLAN服务被关闭或者网卡处于节能休眠状态扫描就真的扫不到任何东西。排查步骤是先确认系统自带WiFi列表能看到周围网络再看设备管理器里无线网卡是否正常最后才怀疑代码问题如果你系统WiFi列表能看到邻居家的热点那pywifi扫描肯定也没问题就是时序没等够。如果系统WiFi列表也看不到说明网卡物理层已经趴窝了跟代码无关。5.2 Linux下提示PermissionErrorLinux用户执行脚本时如果没加sudo报错信息一般是权限不足。这个前面提过。但其实还有一层隐藏原因有些Linux发行版的NetworkManager占用了无线网卡pywifi去调内核接口时会跟NetworkManager冲突。我遇到过一个情况是iwconfig能看到wlan0但pywifi就是报操作失败。解决的土办法是如果系统装了NetworkManager扫描前可以先停掉它的管理或者用系统自带的nmcli命令去执行扫描然后再由Python读取结果。火候控制要小心在个人笔记本上建议直接加sudo跑同时让NetworkManager保持运行多数发行版下是能正常工作的。5.3 虚拟机里永远扫不到WiFi很多教程会让你在VMware或VirtualBox里跑环境这几乎必然翻车。虚拟机默认使用的是虚拟网卡它并不具备真正的无线电收发能力pywifi当然找不到物理接口。我见过有人折腾了一晚上最后发现是虚拟机网卡模式选成了NAT而那台物理机本身有无线网卡但没直接透传过去。想在虚拟机里玩pywifi需要把USB无线网卡直接透传给VM这样虚拟机里的Linux驱动才能直接接管这张USB网卡。另外kali这类安全测试系统里常见的无线网卡透传也是同一个道理物理网卡驱动和网络栈必须穿透到虚拟机里pywifi才能看到设备。5.4 隐藏SSID的网络扫描不到这是一个容易忽略的边界条件。一些路由器开启了隐藏SSID也就是不广播自己的网络名称。pywifi本质上依赖网卡被动接收beacon帧如果接入点不广播SSID扫描列表里自然看不到它的名字只能看到信号强度的可能占位。这种情况无法通过pywifi解决也不是bug它就是无线电行为本身决定的。如果你在做办公环境普查时发现明明有WiFi却扫不到名字先检查路由器是否开启了隐藏功能。真的需要测这种隐藏网络覆盖的话得用其他专业工具配合从已知参数去探测pywifi不是干这个的。5.5 多网卡时选错了接口笔记本同时有内置无线网卡和外接USB无线网卡时interfaces()列表的顺序不一定是内置优先它可能按操作系统枚举顺序排列。如果你要测的是USB网卡结果却一直在用内置网卡数据就会很混乱。我的习惯是先用打印接口列表的方式确认索引再做封装。比如在公众号或者团队脚本里我甚至会把接口名打印出来让用户自己选索引避免硬编码ifaces wifi.interfaces() for i, iface in enumerate(ifaces): print(f[{i}] {iface.name()})这样即使换了机器也不会因为接口索引变化而失效。5.6 笔记本省电模式导致的信号飘忽有一次在会议室实测前五分钟数据都很正常后五分钟信号普遍往下掉了十几个dBm。查了一圈发现是笔记本开启了省电模式无线网卡的发射和接收性能被系统主动压低了。这种受网卡电源策略影响的波动不是真实环境信号会干扰结论。排查方法很简单测量期间把电源模式调成最佳性能如果数据立刻恢复说明之前的波动来自网卡节能而不是实际环境变化。这也是我在任何正式覆盖测试前的固定操作。6. 真实场景应用与合规边界工具讲完说说实际怎么用。这章我以一个办公室覆盖评估的实例为主线让大家看看完整的项目长什么样。6.1 一个办公室覆盖评估的完整流程朋友的办公室大概三百平开放工位加两个独立会议室路由器放在前台角落。员工反馈最里侧的工位经常连不上。我用pywifi做了下面几步画了一个简易平面图标记出12个采样点覆盖每个工位区和会议室在每个采样点用脚本连续扫描5轮各保存一个CSV文件对所有CSV做汇总计算每个采样点对应SSID的平均信号把平均值用颜色标在平面图上找出信号低于-75的区域结果不出所料最里侧工位和会议室平均信号是-82dBm和-79dBm属于基本不可用。解决方案是加了一台Mesh子节点放在走廊的弱电井附近重新扫描后这两个区域提升到了-55dBm左右。整个过程没有连入任何不该连的网络纯粹是无线电环境的聚合分析。6.2 信道规划的实用价值扫描结果还能辅助判断该用哪个信道。有一次我做了一个实验在家里扫到二三十个周边WiFi统计一下它们的工作信道发现2.4GHz下面的1信道有9个设备6信道有7个11信道有6个。把自家路由器固定到11信道之后晚上视频通话卡顿明显减少。这个数据完全是pywifi扫出来的没有借助任何旁路工具。需要说明的是2.4GHz频段还有蓝牙、无线键鼠这类设备叠加干扰所以信道规划别只看WiFi接入点数量还要结合实际体验做微调。但先扫一遍总归是最靠谱的起点。6.3 关于pywifi与无线安全的边界提醒最后必须认真说一件事pywifi的正当用途是网络管理和环境审计。它与管理自己拥有、或已经获得明确授权的无线网络紧密相关。对它进行任何超出扫描范畴的利用——比如踩在别人的网络上做攻击性尝试、抓取数据、破解密钥、绕过接入控制——都属于越界行为既不道德也可能违法。网上那种wifi密码破译wifi密码字典wifi杀手之类的话题跟我讲的完全是两码事。pywifi不提供这些能力这些名词也不该出现在正经的Python网络管理实践里。如果你是按我的教程学会扫描的请把能力用在自家路由器、公司授权网络、或者朋友请你帮忙调试的WiFi上。对不属于自己管理范围的网络扫到信号强弱、看一眼是否拥挤就够了。我自己在这套流程里最深的一个体会是大部分信号差问题其实不需要上专业仪器一台笔记本加一个pywifi脚本就能定位个八九不离十。先把扫描脚本跑通加上多轮采样取平均再配合信道统计和可视化很多无线覆盖问题都能在一杯茶的功夫里找到方向。后面你还可以把这些扫描数据定时跑形成覆盖历史的趋势记录那些又是另一个值得玩的方向了。