☰
Python跨平台WiFi扫描实战:从系统命令到信号解析
2026/10/1 18:32:17 网站建设 项目流程

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 mode=bssid输出包含 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 Windows:netsh 命令是主力

在 Windows 上,我用的命令是:

netsh wlan show networks mode=bssid

mode=bssid是关键参数,如果不加,只显示 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 Linux:nmcli 优先,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,或者你想看更底层的原始信息,可以切换到iwlist:

sudo iwlist wlan0 scan

iwlist输出很原始,包含 ESSID、Address、Channel、Frequency、Signal level 等。它的好处是信息全,坏处是必须 root,而且不同无线网卡驱动输出的字段格式可能不统一。我自己的经验是:优先用nmcli,没有管理器时再降级到iwlist。

3.3 macOS:airport 命令的定位权限坑

macOS 本身没有内置airport命令的 PATH 路径,它的真实位置在:

/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport

每次敲全路径很烦,所以脚本里可以这么处理:

AIRPORT="/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport" "$AIRPORT" -s

airport -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 或 5GHz
securitystr加密类型

这样上层拿到列表后,无论来自哪个平台,都能直接用一样的逻辑分析。实际代码可以写成这样:

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, shell=True, capture_output=True, text=True, timeout=10) 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 mode=bssid") 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(r"Address: ([0-9A-Fa-f:]+)", line) if m: current["bssid"] = m.group(1) elif "ESSID:" in line and current is not None: m = re.search(r'ESSID:"(.*)"', line) if m: current["ssid"] = m.group(1) elif "Channel:" in line and current is not None: m = re.search(r"Channel:(\d+)", line) if m: current["channel"] = int(m.group(1)) elif "Signal level" in line and current is not None: m = re.search(r"Signal 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_ascii=False))

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的记录,并在字典里加一个hidden=True/False的标记。上层展示时可以选择过滤,也可以选择单独统计。Windows 的 netsh 对隐藏网络显示为空,Linuxnmcli显示为--,macOSairport -s会直接省略 SSID。每个平台的表示都不一样,跨平台时尤其要注意。

5.3 获取频段与信道的深一层信息

信道判断对规划无线网络非常重要。在 2.4GHz 频段,常用信道是 1、6、11;5GHz 频段信道更多,还有 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.4GHz,36-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(description="WiFi scanner") parser.add_argument("--repeat", type=int, default=1) parser.add_argument("--interval", type=float, default=2.0) parser.add_argument("--output", type=str, default="json") 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 mode=bssid chcp 65001 >nul &

不过这个办法依赖完整的 cmd 环境,在调整时容易踩坑。另一个办法是不依赖text=True的自动解码,而是拿到原始字节后手动按 GBK 解码:

result = subprocess.run(cmd, shell=True, capture_output=True) raw_bytes = result.stdout try: text = raw_bytes.decode("utf-8") except UnicodeDecodeError: text = raw_bytes.decode("gbk", errors="ignore")

这个思路同样适用于 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或降级使用iwlist
iwlist提示权限不够需要 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 dBm,Windows 可能显示 70%,Linux 显示 33%,不同代码做出来的比例尺不一样。如果只做粗略排序,百分比够用;但要做严谨的工程分析,建议统一存 dBm。我后来在自己的脚本里加了个raw_signal字段,专门保存原始数值,百分比只是展示层转换。

如果你的目标是做一个长期在后台跑的侦察程序,建议在脚本开始前用logging模块而不是print。扫描结果本身是高频数据,一旦 stdout 被大量日志刷屏,再想用 cron 重定向文件就会很难受。把网络数据输出到 JSON 文件,把运行日志输出到日志文件,两者分开是我觉得最省心的方式。

再分享一个不起眼但很实用的细节。每次扫描后,脚本最好记录一下命令的返回码和标准错误,哪怕当时看没用,后期排查“为什么昨晚扫描中断”时,这些日志就是救命稻草。我会把命令、返回码、总耗时一起存进 SQLite,形成一条采集元数据表,与 WiFi 结果表关联。这样任何一条异常数据,都能追溯到底用的是哪条命令、哪个版本的系统。

这篇内容基本覆盖了 Python 识别周围 WiFi 的完整链路:从跨平台命令选型、输出解析、信号换算、进阶监测策略,到常见问题排查。按照文中思路封装一套自己的脚本,你会发现办公网络优化、无线覆盖评估、自动化巡检这些场景都能快速落地。如果你在实际使用中遇到特殊的系统版本或奇怪的输出格式,欢迎交流,很多坑只有一起踩过才知道怎么躲。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询