1. 为什么想到用 Python 扫周围 WiFi先交代一下背景。我有段时间在做一个办公室无线网络质量监测的小工具需要定时记录周边有哪些 WiFi 信号、各自信号强度多少、信道占用情况怎么样。如果每次手动打开手机或者电脑的 WiFi 列表截图再人工记录效率太低也不可能持续跑。于是我把目光投向了 Python——能不能写个脚本自动扫描周边 WiFi把结果结构化存下来再配合后续的图表分析这个问题拆开就是三件事让操作系统去扫描周围 WiFi 信号、把扫描结果拿回来、用代码解析成我们想要的格式。真正动手后才发现跨平台没那么简单。Windows、Linux、macOS 的扫描命令完全不同返回的文本格式也五花八门如果不做一层统一封装换台电脑脚本就废。这篇文章就把我踩过的坑、封装的思路、以及一些进阶玩法完整记录下来希望能给同样折腾 WiFi 扫描的朋友省点时间。先说清楚边界本文只讨论“识别周围 WiFi”也就是获取 SSID、BSSID、信号强度、信道这些公开的无线环境信息用于网络调试、覆盖优化、自动化运维等合法场景。不涉及任何密码破解、非法入侵相关内容。扫自己周边的 WiFi 如同抬头看周围的路牌是无线网络管理里最基础的侦查手段请务必在法律允许的范围内使用。2. 整体设计思路跨平台方案选型2.1 为什么不用纯 Python 库直接拿数据刚上手时我的第一反应是找现成的 Python 库比如wifi、pywifi这类。但调研之后发现一个很尴尬的问题纯 Python 方案最终还是要借助系统底层能力。pywifi在 Linux 上依赖wpa_supplicant在 Windows 上依赖无线网卡驱动的 compatibility环境稍微不一样就容易翻车。还有的库封装比较薄本质上是往系统命令外面包了层壳那我不如直接自己写封装至少出了问题我知道去哪查。另一个现实因素是权限差异。扫描 WiFi 不是每个用户都有权限做的Linux 上通常需要 root 或特定组权限macOS 上需要用户授权“定位服务”Windows 相对宽松但不同版本命令输出有差异。纯 Python 库没法替你解决操作系统层面的权限弹窗最终还是绕不开与系统的交互。2.2 调用系统命令 解析文本最稳的跨平台方案思来想去我选择了“调系统命令再解析标准输出”这种看起来有点笨、但实际最稳的方案。原因有三系统自带的扫描命令已经经过了充分测试稳定性最高。输出格式虽然不一但基本有规律可循用正则表达式就能提取关键字段。不引入额外二进制依赖部署时只需要 Python 标准库方便在各种精简环境里跑。各平台可用的命令如下系统推荐命令说明Windowsnetsh wlan show networks modebssid输出包含 SSID、信号质量、信道、认证方式Linuxnmcli -f ALL dev wifi list或iwlist scannmcli输出更友好iwlist需要 root 且结果更原始macOS/System/Library/PrivateFrameworks/Apple80211.framework/.../airport -s需要先找 airport 的真实路径有了这个基础设计思路就很清晰了一个统一的函数接收ssid、signal、channel、bssid等字段不同平台内部各自解析命令输出最后返回结构一致的列表。2.3 为什么把命令解析层独立出来我在第一次编写时把解析逻辑直接写进了扫描函数里结果后面想添加新字段、新平台时改一处牵连全局非常痛苦。后来重构时遵守了单一职责原则每个平台一个解析函数输入是命令输出的原始文本输出是规范化的字典列表。这样以后哪怕是 Windows 命令输出格式变了我也只需改一个解析函数不影响上层逻辑。# 伪代码示意平台解析函数统一接口 def parse_windows(text: str) - list[dict]: ... def parse_linux_nmcli(text: str) - list[dict]: ... def parse_macos_airport(text: str) - list[dict]: ...这个思想听着简单但实际维护过的人会懂跨平台脚本的死法往往是“一个正则写死换系统就崩”把解析层隔离以后这个问题就好解决多了。3. 各平台扫描命令与关键参数详解3.1 Windowsnetsh 命令是主力在 Windows 上我用的命令是netsh wlan show networks modebssidmodebssid是关键参数如果不加只显示 SSID 列表看不到 BSSID 和信号质量。加上之后输出会包含每个 AP 的 BSSID、信号、信道、加密方式等信息。示例输出片段截取部分SSID 1 : MyHomeWiFi 网络类型 : 结构 身份验证 : WPA2-Personal 加密 : CCMP BSSID 1 : 34:29:8f:aa:bb:cc 信号 : 85% 无线电类型 : 802.11n 信道 : 6这里“信号”字段是百分比形式非常友好可以直接用。但有个坑netsh 输出的中文标签在不同 Windows 版本可能不同比如 Win10 和 Win11 个别项目名称有变化所以我的解析代码并没有硬编码字符串而是用正则找冒号后面的值再根据上下文识别字段顺序。更稳妥的办法是使用chcp 65001切到 UTF-8 编码再执行命令避免中文乱码。3.2 Linuxnmcli 优先iwlist 备用Linux 是 WiFi 扫描的天堂也是折腾的重灾区。我首选nmcli是因为它不需要 root 就能看到周围的 WiFi 网络列表前提是 NetworkManager 服务正常而且输出是表格化结构解析比较简单。nmcli -f ALL dev wifi list这个命令会列出所有 WiFi 的 SSID、SSID-BSSID、信道、信号、安全、频段等字段。举个例子IN-USE BSSID SSID MODE CHAN RATE SIGNAL BARS SECURITY AA:BB:CC:DD:EE:FF MyHomeWiFi Infra 6 270 Mbit/s 75 ▂▄▆_ WPA2如果系统没有 NetworkManager或者你想看更底层的原始信息可以切换到iwlistsudo iwlist wlan0 scaniwlist输出很原始包含 ESSID、Address、Channel、Frequency、Signal level 等。它的好处是信息全坏处是必须 root而且不同无线网卡驱动输出的字段格式可能不统一。我自己的经验是优先用nmcli没有管理器时再降级到iwlist。3.3 macOSairport 命令的定位权限坑macOS 本身没有内置airport命令的 PATH 路径它的真实位置在/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport每次敲全路径很烦所以脚本里可以这么处理AIRPORT/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport $AIRPORT -sairport -s的输出是纯文本表格字段包括 SSID、BSSID、RSSI、CHANNEL、HT、SECURITY 等。注意其中的 RSSI 是负值比如-45 dBm这个值越小信号越弱。还有一个大坑macOS 从某个版本开始扫描 WiFi 会触发“定位服务”权限请求如果你在终端里运行 Python 脚本需要给终端 App 开启“使用无线网络信息”的权限否则返回的结果可能为空或报错。这个问题在 Mojave 之后尤其明显我后面会专门讲。3.4 统一封装输出结构设计为了后续处理方便我定了统一的字段结构字段类型说明ssidstr网络名称可能为空bssidstrAP 的 MAC 地址signalint信号强度Windows 为百分比Linux/macOS 换算成百分比channelint信道号frequencystr2.4GHz 或 5GHzsecuritystr加密类型这样上层拿到列表后无论来自哪个平台都能直接用一样的逻辑分析。实际代码可以写成这样import platform import subprocess import re class WiFiScanner: def __init__(self): self.system platform.system() def scan(self): if self.system Windows: raw self._run_windows() return parse_windows(raw) elif self.system Linux: raw self._run_linux() return parse_linux(raw) elif self.system Darwin: raw self._run_macos() return parse_macos(raw) else: raise NotImplementedError(unsupported system)这里每个_run_*负责执行对应的系统命令并返回标准输出解析函数则各自独立。代码看起来多但每段逻辑都很清晰后续增加新平台只是加一个分支不会动原有逻辑。4. 实战写一个跨平台 WiFi 扫描脚本4.1 完整代码主体下面是我整理后的一份可运行脚本保留了核心逻辑省去了过于冗长的注释方便直接复制改造import subprocess import re import platform import json class WiFiScanner: def __init__(self): self.system platform.system() self.networks [] def _execute(self, cmd): try: result subprocess.run(cmd, shellTrue, capture_outputTrue, textTrue, timeout10) if result.returncode ! 0: return result.stderr or return result.stdout except subprocess.TimeoutExpired: return # ---------- Windows ---------- def _scan_windows(self): raw self._execute(netsh wlan show networks modebssid) return self._parse_windows(raw) def _parse_windows(self, text): networks [] lines text.splitlines() current None for line in lines: line line.strip() if line.startswith(SSID): if current: networks.append(current) ssid line.split(:, 1)[-1].strip() current {ssid: ssid, signal: None, channel: None, bssid: None, security: None} elif line.startswith(BSSID): if current: current[bssid] line.split(:, 1)[-1].strip() elif line.startswith(信号): if current: m re.search(r(\d)%, line) if m: current[signal] int(m.group(1)) elif line.startswith(信道): if current: m re.search(r(\d), line) if m: current[channel] int(m.group(1)) elif line.startswith(身份验证): if current: current[security] line.split(:, 1)[-1].strip() if current: networks.append(current) return networks # ---------- Linux ---------- def _scan_linux(self): raw self._execute(nmcli -f ALL dev wifi list) if not raw: # 降级到 iwlist raw self._execute(sudo iwlist wlan0 scan) return self._parse_linux_iwlist(raw) return self._parse_linux_nmcli(raw) def _parse_linux_nmcli(self, text): networks [] lines text.splitlines() if not lines: return networks header lines[0] field_indices {} # 基于表头识别列位置减少硬编码 for i, field in enumerate([BSSID, SSID, CHAN, SIGNAL, SECURITY]): try: field_indices[field] header.index(field) except ValueError: pass for line in lines[1:]: if not line.strip(): continue seg self._split_by_indices(line, field_indices) if seg.get(SSID) --: continue networks.append({ ssid: seg.get(SSID, ), bssid: seg.get(BSSID, ), channel: self._to_int(seg.get(CHAN)), signal: self._to_int(seg.get(SIGNAL)), security: seg.get(SECURITY, ), }) return networks def _split_by_indices(self, line, indices): result {} fields list(indices.keys()) for i, field in enumerate(fields): start indices[field] if i 1 len(fields): end indices[fields[i 1]] else: end len(line) result[field] line[start:end].strip() return result def _parse_linux_iwlist(self, text): networks [] current None for line in text.splitlines(): if Cell in line and Address: in line: if current: networks.append(current) current {ssid: , bssid: , channel: None, signal: None, security: } m re.search(rAddress: ([0-9A-Fa-f:]), line) if m: current[bssid] m.group(1) elif ESSID: in line and current is not None: m re.search(rESSID:(.*), line) if m: current[ssid] m.group(1) elif Channel: in line and current is not None: m re.search(rChannel:(\d), line) if m: current[channel] int(m.group(1)) elif Signal level in line and current is not None: m re.search(rSignal level(-?\d), line) if m: current[signal] self._dbm_to_percent(int(m.group(1))) if current: networks.append(current) return networks # ---------- macOS ---------- def _scan_macos(self): airport /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport raw self._execute(f{airport} -s) return self._parse_macos(raw) def _parse_macos(self, text): networks [] lines text.splitlines() if not lines: return networks header lines[0] # 使用正则解析每行按字段间的空格对齐不易改用固定顺序 # 先定位 SSID 和 BSSID/RSSI 的位置 try: ssid_end header.index(BSSID) bssid_end header.index(RSSI) rssi_end header.index(CHANNEL) chan_end header.index(HT) except ValueError: return networks for line in lines[1:]: if not line.strip(): continue ssid line[:ssid_end].strip() bssid line[ssid_end:bssid_end].strip() rssi line[bssid_end:rssi_end].strip() channel line[rssi_end:chan_end].strip() # 去掉信道后缀比如 6 或 44 channel channel.split()[0] if channel else networks.append({ ssid: ssid, bssid: bssid, signal: self._dbm_to_percent(int(rssi)) if rssi else None, channel: int(channel) if channel.isdigit() else None, security: , }) return networks staticmethod def _dbm_to_percent(dbm): # 常见转换公式0 dBm - 100%-100 dBm - 0% percent min(100, max(0, (dbm 100) * 2)) return int(percent) staticmethod def _to_int(value): try: return int(value) except (ValueError, TypeError): return None def scan(self): if self.system Windows: self.networks self._scan_windows() elif self.system Linux: self.networks self._scan_linux() elif self.system Darwin: self.networks self._scan_macos() else: raise RuntimeError(f不支持的平台: {self.system}) return self.networks if __name__ __main__: scanner WiFiScanner() for net in scanner.scan(): print(json.dumps(net, ensure_asciiFalse))4.2 关键代码逻辑解读先看_parse_windows。我判断每一行开头是否为“SSID”如果是就开启一个新的网络对象随后遇到的 BSSID、信号、信道、身份验证都填充到当前对象。这里有一点需要注意netsh 输出中每个 SSID 可能有多个 BSSID同一 SSID 下会出现“BSSID 1”、“BSSID 2”等多条记录。我简化了逻辑只取了每组的第一条 BSSID。如果你希望把所有 BSSID 都保存下来可以把current[bssid]改成列表追加进一步还能统计同 SSID 下的 AP 数量对信道拥塞分析很有帮助。Linux 的nmcli解析用了按表头定位的“列偏移法”。这个方法比用正则逐行匹配更稳定因为nmcli输出的列宽会随字符串长度变化硬编码切片位置很容易错位。做法是先找到表头每个字段的起始索引然后把每一行的片段按索引切割再 strip 掉空格。这样即使某个 SSID 很长导致列宽变动只要表头字段顺序不变切割结果依然正确。macOS 的airport -s输出没有清晰的竖线分隔符只有空格对齐。我的解法是先解析表头定好各个字段的边界索引再按索引切割行。因为airport -s的字段顺序基本固定所以这种做法可用。不过要注意中间的 BSSID 字段含有冒号和字母RSSI 是负整数这些特征都可以拿来做边界校验。实际处理时如果channel字段出现类似6或44这样的值我直接取了空格前的数字部分。4.3 信号强度单位换算dBm 与百分比的关系不同平台给信号强度的单位不一致Windows 直接给百分比Linux 的iwlist和 macOS 给 dBm而nmcli给的是百分比。所以我写了_dbm_to_percent这个转换函数。dBm 是功率与 1 毫瓦的比值对数WiFi 信号通常在 -30 dBm极强到 -95 dBm几乎连不上之间。最常见的线性映射是把 -100 dBm 视作 0%把 0 dBm 视作 100%公式是(dbm 100) * 2。比如 -67 dBm 会换算成 66%。这个转换是纯数学映射实际使用中可以按需调整。有些网卡能收到 -105 以下的信号如果照这个公式算会是负数所以我在函数里用min(100, max(0, ...))做了截断。同时也要清楚这可能带来信息损失百分比无法直接反映信号质量波动如果要做精细的衰减分析我还是会保留原始 dBm 值。这也是我在最终字典里没删掉原始信号字段的原因后续可以按场景选择使用。5. 进阶技巧让 WiFi 识别更强大5.1 持续扫描与变化监测基础扫描只能拿到某一瞬间的快照但很多场景需要观察信号随时间的变化。比如办公室某个角落信号时好时坏你可以每隔 5 秒扫一次记录每个 AP 的信号强度再画成时间序列折线图。import time from wifi_scanner import WiFiScanner scanner WiFiScanner() while True: networks scanner.scan() for ap in networks: # 过滤掉空 SSID 的隐藏网络 if ap[ssid]: print(f{ap[ssid]}|{ap[signal]}|{time.time()}) time.sleep(5)把这条数据流接入 SQLite、InfluxDB 或直接写 CSV都能很方便地做后处理。我自己常用的做法是把数据写成 JSON 行文件每行一个时间戳网络列表这样 Excel 或 pandas 读取都很快。如果要做变化监测比如检测某一区域是否出现了新的 AP用于安全审计可以在每次扫描后和上一次结果做差集old_ssids {net[ssid] for net in old_networks} new_ssids {net[ssid] for net in new_networks} added new_ssids - old_ssids removed old_ssids - new_ssids这套逻辑可以做成告警发现陌生 SSID 就推送提醒。注意这里只识别 SSID不涉及任何加密破解完全合规。5.2 识别隐藏 WiFi 的边界问题WiFi 网络可以在 SSID 设置为隐藏时不广播名称扫描命令会显示为空或者hidden。注意隐藏 WiFi 的 MAC 地址、信道、加密方式仍然会在管理帧中被公开所以扫描时可以拿到 BSSID 和信号只是 SSID 为空。如果你想探测隐藏网络的真实名称需要对捕捉到的握手包做离线分析这已经涉及进攻性测试的范畴且需要明确授权。我这里不做讨论仅提醒你在解析脚本里遇到空 SSID 时要特殊处理别丢弃数据因为这类数据对 RF 环境分析同样有价值。我的处理方式是保留 SSID 为None的记录并在字典里加一个hiddenTrue/False的标记。上层展示时可以选择过滤也可以选择单独统计。Windows 的 netsh 对隐藏网络显示为空Linuxnmcli显示为--macOSairport -s会直接省略 SSID。每个平台的表示都不一样跨平台时尤其要注意。5.3 获取频段与信道的深一层信息信道判断对规划无线网络非常重要。在 2.4GHz 频段常用信道是 1、6、115GHz 频段信道更多还有 DFS 信道。如果你想分析某个区域哪条信道竞争最少就需要在扫描结果里保留channel和frequency字段。nmcli -f ALL dev wifi list直接包含了CHAN和频段信息比如“6”和“2.4 GHz”。airport -s输出里信道可能是6或44的形式表示支持 40MHz 绑定解析时要单独处理。Windows 的netsh输出也有频段信息不过要通过“无线电类型”间接推断比如 802.11n 可能是 2.4GHz 或 5GHz。如果想更精确可以配合netsh wlan show interfaces查看当前连接接口的频率但扫描列表本身不太容易直接得到每个 AP 的频率。我在实际项目中通常只把 Windows 的频道数值记录频段信息后面再通过信道号推断1-14 视为 2.4GHz36-165 视为 5GHz。信道 14 只在日本合法国内用不到但代码里保留这个判断没坏处。5.4 信号覆盖率与二维热力图把扫描脚本放在不同位置定时运行收集每个位置看到的所有 AP 信号再配合经纬度就能生成 WiFi 覆盖热力图。这个思路适合做室内定位或者无线优化。具体做法移动终端比如树莓派加充电宝运行 Python 脚本每到一个采样点记录 GPS 坐标同时扫描 WiFi 信号强度。RSSI 随距离的对数衰减规律大约是每增加一倍距离衰减 6dB不过室内环境受遮挡影响很重实际拟合时要多采样。热力图绘制可以用matplotlib的tricontourf或scipy.interpolate.griddata。这部分不在本文深挖只提一个关键点如果要把多平台扫描结果合并务必将信号强度统一成 dBm 或统一的百分比不然不同设备间数据无法比较。我因为最初没统一单位后面做图表差点被坑哭。5.5 隐藏命令的用户态封装支持自定义参数为了让脚本更实用我习惯加一些命令行参数比如指定扫描次数、间隔、输出格式、过滤条件。使用argparse很容易实现import argparse parser argparse.ArgumentParser(descriptionWiFi scanner) parser.add_argument(--repeat, typeint, default1) parser.add_argument(--interval, typefloat, default2.0) parser.add_argument(--output, typestr, defaultjson) args parser.parse_args()参数化后脚本就可以做成长时间无人值守采集也可以接入 cron 或计划任务执行。输出格式建议支持 JSON 和 CSV 两种JSON 适合后续编程处理CSV 适合丢给 Excel 快速查看。CSV 导出时注意隐藏网络的 SSID 为空要保证字段数量一致。6. 常见问题与排查实录6.1 命令找不到或返回空这一类问题最常见。在 Linux 上有些精简系统没装 NetworkManager导致nmcli不存在脚本就会降级到iwlist但iwlist可能也没装需要wireless-tools包。解决办法是在执行命令前先校验工具是否可用import shutil def _check_cmd(cmd): return shutil.which(cmd) is not None如果返回为空先手动在 shell 里跑一遍命令确认输出格式和预期是否一致。我遇到过 Ubuntu 某个版本nmcli输出带了额外的 ANSI 颜色控制符导致解析失败这种问题只要在代码里对输出做一次去除颜色代码的清洗就好。Windows 上很可能遇到“无线自动配置服务未启用”的错误脚本拿到的 stderr 会提示 “The Wireless AutoConfig Service (wlansvc) is not running”。这时候需要以管理员身份启动服务或者检查笔记本的无线开关是否被物理关闭。注意这个错误不一定导致 returncode 非 0所以我建议同时检查标准输出和标准错误如果标准输出为空且 stderr 有内容就把 stderr 打到日志里。6.2 权限不足Linux 的 sudo 和 macOS 的定位权限Linux 下如果使用iwlist扫描必须要 root 权限。脚本里用sudo执行会面临两个问题一是交互式输密码二是自动化环境里没有 TTY 导致 sudo 失败。我推荐的解决方法是使用pkexec或者在部署时把用户加入sudo组并配置 NOPASSWD。如果是长期跑的服务建议单独创建一个系统用户并写一个只允许iwlist命令的 sudo 策略提高安全性。macOS 上最头疼的是“定位服务”权限。脚本调用airport -s时第一次会弹出系统授权框如果不允许返回结果为空。这个权限和终端 App 绑定如果你换了一个终端模拟器比如 iTerm2还得重新授权。授权路径是系统设置 - 隐私与安全性 - 定位服务 - 终端。如果授权后依然扫不到可能需要重启终端进程。还有一个隐蔽点如果 Mac 的 Wi-Fi 硬件本身关闭比如通过菜单栏关闭airport -s会显示错误 “AirPort is turned off”脚本里要捕获这类错误提示而不是简单当作空结果。6.3 中文 SSID 乱码或编码错误Windows 上中文 SSID 很容易乱码。netsh在 GBK 代码页下输出中文而 Python 的subprocess默认按 locale 解码。如果 locale 不对就会出现类似绉戞妧的乱码。最直接的解决办法是强制命令在 UTF-8 环境下输出netsh wlan show networks modebssid chcp 65001 nul 不过这个办法依赖完整的 cmd 环境在调整时容易踩坑。另一个办法是不依赖textTrue的自动解码而是拿到原始字节后手动按 GBK 解码result subprocess.run(cmd, shellTrue, capture_outputTrue) raw_bytes result.stdout try: text raw_bytes.decode(utf-8) except UnicodeDecodeError: text raw_bytes.decode(gbk, errorsignore)这个思路同样适用于 macOS 和 Linux 遇到非 UTF-8 环境的情况。我在脚本里干脆封装了一个_decode_output方法优先按 UTF-8 解码失败则回退到 GBK实在不行就replace容错。6.4 扫描结果总是不完整或漏网络如果你的扫描结果数量明显少于手机里看到的 WiFi 数量先检查是不是两件事一是网卡驱动限制有些网卡扫描时会合并同一 SSID 的多个 BSSID二是天线和信道的扫描顺序系统有时为了省电扫描所有信道要 1-2 秒脚本可能在系统没完成全信道扫描时就取回了输出。解决办法是把subprocess.run的timeout调大或者在调用命令前先强制触发一次系统扫描。在 macOS 上airport -s返回的列表排序是按信号强度有时弱信号 AP 会被截断。Linux 的nmcli默认也会按信号排序且可能只显示部分网络。Windowsnetsh也有类似机制。如果要做穷举式的环境普查建议把扫描命令执行多次合并结果去除重复保留每个 AP 的最强信号记录。6.5 常见问题速查表现象可能原因解决方向netsh提示服务未运行无线服务 wlansvc 未启动或无线网卡被禁用管理员启动服务打开物理开关Linux 下nmcli不存在未安装 NetworkManager CLI安装network-manager或降级使用iwlistiwlist提示权限不够需要 root使用 sudo 或配置 sudoers 免密macOS 扫描结果为空未授权定位服务或 Wi-Fi 关闭系统设置里授权打开 Wi-Fi中文 SSID 乱码编码解码不一致手动指定 GPU/GBK 解码信号值异常比如负值混用了 dBm 和百分比统一换算标准隐藏网络 SSID 显示为空正常现象保留记录标记 hidden 字段7. 实际操作中的几点心得体会最后聊几个我自己实操后的感受。跨平台 WiFi 扫描这个事最核心的并不是代码本身而是对系统工具的熟悉程度。每个平台都有一两个官方或半官方的命令行工具你要做的不是重新发明轮子而是用 Python 把轮子搬过来再磨平。如果你只是想在单平台上快速用一下可以只挑 Windows 的netsh或者 Linux 的nmcli来研究两种命令的输出解析练熟了跨平台思路自然就有了。权限问题永远比解析问题更早出现。我建议在写解析代码之前先在命令行里手动跑一遍扫描命令确认权限通、结果非空、格式符合预期再开始编码。很多人越过这一步直接写脚本回头被权限和编码折腾半天反而耽误时间。信号强度的单位换算一定不要想当然。Windows 的百分比和 Linux 的 dBm 之间差的不是系数而是阈值边界。同样的 -67 dBmWindows 可能显示 70%Linux 显示 33%不同代码做出来的比例尺不一样。如果只做粗略排序百分比够用但要做严谨的工程分析建议统一存 dBm。我后来在自己的脚本里加了个raw_signal字段专门保存原始数值百分比只是展示层转换。如果你的目标是做一个长期在后台跑的侦察程序建议在脚本开始前用logging模块而不是print。扫描结果本身是高频数据一旦 stdout 被大量日志刷屏再想用 cron 重定向文件就会很难受。把网络数据输出到 JSON 文件把运行日志输出到日志文件两者分开是我觉得最省心的方式。再分享一个不起眼但很实用的细节。每次扫描后脚本最好记录一下命令的返回码和标准错误哪怕当时看没用后期排查“为什么昨晚扫描中断”时这些日志就是救命稻草。我会把命令、返回码、总耗时一起存进 SQLite形成一条采集元数据表与 WiFi 结果表关联。这样任何一条异常数据都能追溯到底用的是哪条命令、哪个版本的系统。这篇内容基本覆盖了 Python 识别周围 WiFi 的完整链路从跨平台命令选型、输出解析、信号换算、进阶监测策略到常见问题排查。按照文中思路封装一套自己的脚本你会发现办公网络优化、无线覆盖评估、自动化巡检这些场景都能快速落地。如果你在实际使用中遇到特殊的系统版本或奇怪的输出格式欢迎交流很多坑只有一起踩过才知道怎么躲。