在不依赖摄像头、蓝牙信标或红外传感器的前提下,仅凭一块支持监听模式的 WiFi 适配器,就能估算你周围有多少人------这就是 GitHub 上获得 7100+ Star 的开源项目 howmanypeoplearearound 所做的事情。
一、项目概述
基本信息
| 属性 | 内容 |
|---|---|
| 仓库地址 | schollz/howmanypeoplearearound |
| Star 数 | 7100+ |
| Fork 数 | 381 |
| 开发语言 | Python 2.7 / Python 3 |
| 许可证 | MIT |
| 版本 | v0.5.0 |
| 核心依赖 | tshark (Wireshark CLI)、click、netifaces、pick |
核心理念
项目的核心思路极其简洁:智能手机是人的"代理信号"。
当今约 70% 的人随身携带智能手机,而智能手机的 WiFi 模块会周期性地向外发送 Probe Request(探测请求帧)。这些帧用于发现周围的 WiFi 网络,即使手机没有连接任何 AP(接入点),也会持续发出。howmanypeoplearearound 就是利用这一特性,通过监听这些 Probe Request 来统计周围出现的不同 MAC 地址数量,再结合智能手机普及率换算出估算人数。
应用场景
- 监控家庭/办公室的人流密度(搭配树莓派)
- 判断室友是否在家
- 商铺客流粗略统计
- 任何需要"感知附近是否有人"但不想部署摄像头的场景
二、技术原理:WiFi Probe Request 嗅探
2.1 什么是 Probe Request?
在 802.11 协议中,WiFi 设备活跃地寻找可用网络时有两种方式:
- 被动扫描:设备静默等待 AP 定期广播的 Beacon 帧
- 主动扫描:设备主动发送 Probe Request 帧,AP 收到后回复 Probe Response
绝大多数智能手机为了更快发现可用网络,会采用主动扫描。即使屏幕熄灭、WiFi 未连接,手机仍会以 1~10 分钟为周期发送 Probe Request。这些帧包含了发送方的 MAC 地址,这就是项目得以工作的物理基础。
2.2 监听模式(Monitor Mode)
普通的 WiFi 适配器工作在 Managed( managed)模式,只接收发往自己的帧。要捕获空气中所有的 802.11 帧(包括别人的 Probe Request),需要将适配器切换到 Monitor Mode(监听模式)。
并非所有 WiFi 芯片都支持监听模式。README 中列出了推荐芯片:
- Atheros AR9271
- Ralink RT3070
- Ralink RT3572
- Ralink RT5572
常见的 Alfa 系列(AWUS036NHA 等)和 Panda 系列(PAU5/PAU6/PAU9)适配器都基于上述芯片。
2.3 tshark:抓包引擎
项目并不自己实现底层抓包,而是借助 Wireshark 的命令行工具 tshark。tshark 是业界标准的网络协议分析工具,支持将适配器切换到监听模式并捕获原始 802.11 帧。
整个抓包流程分为两步:
-
捕获阶段 :
tshark -I -i <adapter> -a duration:<seconds> -w <dump_file>-I:启用监听模式-i:指定网络适配器-a duration:设置捕获时长-w:将原始数据写入 pcap 文件
-
解析阶段 :
tshark -r <dump_file> -T fields -e wlan.sa -e wlan.bssid -e radiotap.dbm_antsignal-r:读取之前保存的 pcap 文件-T fields:以字段格式输出-e wlan.sa:提取源 MAC 地址(Source Address)-e wlan.bssid:提取 BSSID-e radiotap.dbm_antsignal:提取信号强度 RSSI
三、代码结构解析
项目代码结构非常精简,核心包目录下仅 6 个文件:
bash
howmanypeoplearearound/
├── __init__.py # 包初始化(空文件)
├── __main__.py # 主程序入口:CLI 参数解析 + 抓包 + MAC 分析 + 人数计算
├── analysis.py # 数据可视化:读取 JSON 日志生成 Plotly HTML 图表
├── colors.py # 终端 ANSI 颜色常量
├── oui.py # OUI 数据库加载与下载
└── plotlyjs.py # Plotly.js 可视化模板代码
3.1 __main__.py:核心逻辑
这是整个项目最重要的文件,包含 CLI 入口和完整的扫描+分析逻辑。
CLI 参数设计
使用 click 框架定义了丰富的命令行参数:
python
@click.command()
@click.option('-a', '--adapter', default='', help='adapter to use')
@click.option('-s', '--scantime', default='60', help='time in seconds to scan')
@click.option('-o', '--out', default='', help='output cellphone data to file')
@click.option('-n', '--nearby', help='only quantify signals that are nearby (rssi > -70)', is_flag=True)
@click.option('--nocorrection', help='do not apply correction', is_flag=True)
@click.option('--loop', help='loop forever', is_flag=True)
@click.option('-j', '--jsonprint', help='print JSON of cellphone data', is_flag=True)
@click.option('--sort', help='sort cellphone data by distance (rssi)', is_flag=True)
@click.option('-m', '--manufacturers', default='', help='read list of known manufacturers from file')
@click.option('--allmacaddresses', help='do not check MAC against OUI database', is_flag=True)
@click.option('-f', '--pcap', help='read a pcap file instead of capturing')
# ...
关键参数说明:
| 参数 | 作用 |
|---|---|
--adapter / -a |
指定 WiFi 适配器名称 |
--scantime / -s |
扫描时长(秒),默认 60 |
--nearby / -n |
只统计 RSSI > -70 的近距离信号 |
--nocorrection |
不应用 70% 智能手机普及率修正 |
--loop |
循环扫描模式 |
--out / -o |
将结果以 JSON 追加写入文件 |
--analyze / -z |
分析已有 JSON 文件并可视化 |
--pcap / -f |
直接读取 pcap 文件而非实时抓包 |
--allmacaddresses |
不过滤 OUI,统计所有 MAC 地址 |
--sort |
按 RSSI(距离)排序输出 |
scan() 函数:核心扫描流程
scan() 是整个项目的主逻辑函数,工作流程如下:
css
┌─────────────────────────────────────────────────────┐
│ 1. 加载 OUI 字典(若无则自动下载) │
│ └─ oui.py: download_oui() 从 IEEE 官网拉取 │
├─────────────────────────────────────────────────────┤
│ 2. 检查 tshark 是否安装 │
│ └─ which("tshark") 遍历 PATH 查找可执行文件 │
├─────────────────────────────────────────────────────┤
│ 3. 选择 WiFi 适配器(交互式 pick 或 -a 指定) │
├─────────────────────────────────────────────────────┤
│ 4. 启动 tshark 抓包(监听模式 + 指定时长) │
│ └─ subprocess.Popen([tshark, '-I', '-i', ...]) │
│ 同时启动倒计时线程 showTimer() │
├─────────────────────────────────────────────────────┤
│ 5. 用 tshark 解析 pcap,提取字段: │
│ wlan.sa (源MAC) + wlan.bssid + radiotap.dbm_antsignal │
├─────────────────────────────────────────────────────┤
│ 6. 逐行解析输出,构建 foundMacs 字典 │
│ { MAC: [rssi1, rssi2, ...] } │
│ 对同一 MAC 的多次 RSSI 取平均值 │
├─────────────────────────────────────────────────────┤
│ 7. OUI 过滤:只保留手机厂商的 MAC 地址 │
│ └─ mac[:8] 查 OUI 字典,匹配手机厂商列表 │
├─────────────────────────────────────────────────────┤
│ 8. 人数计算: │
│ num_people = len(cellphone_people) / 0.7 │
│ (70% 智能手机普及率修正) │
├─────────────────────────────────────────────────────┤
│ 9. 输出结果(终端 / JSON 文件 / 循环追加) │
└─────────────────────────────────────────────────────┘
关键代码片段解读
RSSI 聚合------对同一个 MAC 地址的多次信号强度取平均:
python
foundMacs = {}
for line in output.decode('utf-8').split('\n'):
if line.strip() == '':
continue
mac = line.split()[0].strip().split(',')[0]
dats = line.split()
if len(dats) == 3:
if ':' not in dats[0] or len(dats) != 3:
continue
if mac not in foundMacs:
foundMacs[mac] = []
dats_2_split = dats[2].split(',')
if len(dats_2_split) > 1:
# 多天线场景:取平均值
rssi = float(dats_2_split[0]) / 2 + float(dats_2_split[1]) / 2
else:
rssi = float(dats_2_split[0])
foundMacs[mac].append(rssi)
# 对每个 MAC 的所有 RSSI 取平均
for key, value in foundMacs.items():
foundMacs[key] = float(sum(value)) / float(len(value))
这段代码处理了 tshark 输出中可能出现多天线信号值的情况(逗号分隔),对同一 MAC 在扫描周期内的多次出现取平均 RSSI,作为该设备的信号强度。
OUI 过滤------通过 MAC 地址前缀识别手机厂商:
python
cellphone = [
'Motorola Mobility LLC, a Lenovo Company',
'GUANGDONG OPPO MOBILE TELECOMMUNICATIONS CORP.,LTD',
'Huawei Symantec Technologies Co.,Ltd.',
'Microsoft',
'HTC Corporation',
'Samsung Electronics Co.,Ltd',
'BlackBerry RTS',
'LG ELECTRONICS INC',
'Apple, Inc.',
'OnePlus Tech (Shenzhen) Ltd',
'Xiaomi Communications Co Ltd',
# ...
]
项目内置了一份手机厂商列表,通过 OUI(Organizationally Unique Identifier)查询 MAC 地址前 3 字节(前 8 位含分隔符)对应的厂商名称。只有在手机厂商列表中的 MAC 才会被计入。用户也可以通过 -m 参数自定义厂商列表文件。
人数计算与修正:
python
percentage_of_people_with_phones = 0.7
if nocorrection:
percentage_of_people_with_phones = 1
num_people = int(round(len(cellphone_people) /
percentage_of_people_with_phones))
默认将检测到的手机数量除以 0.7(假设 70% 的人携带智能手机),得到估算人数。使用 --nocorrection 可关闭这一修正,直接以手机数量作为人数。
3.2 oui.py:OUI 数据库管理
OUI(Organizationally Unique Identifier)是 MAC 地址的前 3 字节,由 IEEE 分配并公开维护。该模块负责从 IEEE 官网下载 OUI 数据并解析为字典:
python
def load_dictionary(file):
oui = {}
with open(file, 'r') as f:
for line in f:
if '(hex)' in line:
data = line.split('(hex)')
key = data[0].replace('-', ':').lower().strip()
company = data[1].strip()
oui[key] = company
return oui
def download_oui(to_file):
uri = 'http://standards-oui.ieee.org/oui/oui.txt'
oui_data = urlopen(uri, timeout=10).read()
with open(to_file, 'wb') as oui_file:
oui_file.write(oui_data)
IEEE 的 oui.txt 文件格式为:00-00-00 (hex) Apple, Inc.。代码按 (hex) 分割,将前缀统一为冒号分隔的小写格式作为字典 key,厂商名作为 value。首次运行时若本地无此文件,会自动从 IEEE 官网下载。
3.3 analysis.py:数据可视化
当用户使用 --loop 模式持续收集数据后,可以通过 --analyze 读取 JSON 日志文件并生成可视化 HTML 页面。
分析模块的逻辑:
- 逐行读取 JSON 格式的扫描记录(每条含
cellphones数组和time时间戳) - 按时间排序
- 筛选 RSSI > -80 的 MAC 地址作为有效信号
- 为每个 MAC 构建时间序列(RSSI 随时间变化)
- 生成内嵌 Plotly.js 的 HTML 文件
- 启动本地 HTTP 服务器(默认端口 8001)提供访问
可视化包含两个图表:
- 总人数趋势图:展示每个扫描周期检测到的手机数量随时间变化
- 个体信号追踪图:每个 MAC 地址的 RSSI 时间序列,可双击高亮某条轨迹
3.4 setup.py:打包发布
python
entry_points={'console_scripts': [
'howmanypeoplearearound = howmanypeoplearearound.__main__:main',
]},
通过 setuptools 注册为全局命令行工具,pip install 后即可直接在终端使用 howmanypeoplearearound 命令。
3.5 Dockerfile:容器化方案
dockerfile
FROM python:3
RUN apt-get update \
&& apt-get upgrade --yes \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y tshark \
&& yes | dpkg-reconfigure -f noninteractive wireshark-common \
&& addgroup wireshark \
&& usermod -a -G wireshark ${USER:-root} \
&& newgrp wireshark \
&& pip install howmanypeoplearearound
CMD [ "howmanypeoplearearound" ]
Docker 方案将 tshark 安装、权限配置和 Python 包安装一体化。运行时需要 --net=host 来共享宿主机网络栈,使容器内的 tshark 能直接访问 WiFi 适配器。
四、完整使用流程
4.1 环境准备
bash
# 安装 tshark (Linux)
sudo apt-get install tshark
# 配置非 root 用户可运行
sudo dpkg-reconfigure wireshark-common # 选择 YES
sudo usermod -a -G wireshark ${USER:-root}
newgrp wireshark
# 安装 Python 包
pip install howmanypeoplearearound
macOS 需通过 Homebrew 安装 Wireshark,且扫描前需断开当前 WiFi 连接:
bash
brew install wireshark
brew cask install wireshark-chmodbpf
sudo /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -z
4.2 基本使用
bash
# 交互式选择适配器,扫描 60 秒
howmanypeoplearearound
# 指定适配器和扫描时长
howmanypeoplearearound -a wlan1 -s 120
# 只输出人数数字(适合脚本调用)
howmanypeoplearearound --number
# 输出 JSON 格式的详细手机数据
howmanypeoplearearound -j -a wlan1
# 只统计近距离信号
howmanypeoplearearound -n -a wlan1
# 循环扫描并持续记录到文件
howmanypeoplearearound -o test.json -a wlan1 --loop
# 分析历史数据并可视化
howmanypeoplearearound --analyze test.json
4.3 JSON 输出示例
json
[
{
"rssi": -86.0,
"mac": "90:e7:c4:xx:xx:xx",
"company": "HTC Corporation"
},
{
"rssi": -84.0,
"mac": "80:e6:50:xx:xx:xx",
"company": "Apple, Inc."
},
{
"rssi": -49.0,
"mac": "ac:37:43:xx:xx:xx",
"company": "HTC Corporation"
}
]
RSSI 值越高表示设备越近(注意 RSSI 是负值,-49 比 -86 更近)。作者提到其中一个 -49 的是自己的手机,另两个是室友楼上手机的信号。
五、技术选型分析
5.1 优势
依赖 tshark 而非自研抓包
项目没有从零实现 802.11 帧的捕获和解析,而是直接调用 tshark。这是一个明智的工程决策:
- tshark 经过数十年打磨,协议解析覆盖面广且稳定
- tshark 支持跨平台(Linux/macOS/Windows),项目自动获得跨平台能力
- 开发者只需关注业务逻辑(MAC 去重、OUI 匹配、人数计算),无需处理底层协议细节
OUI 过滤策略
通过 MAC 地址前缀识别设备厂商,只统计手机厂商的 MAC 地址,有效排除了笔记本电脑、路由器、IoT 设备等非手机 WiFi 设备的干扰,提高了人数估算的准确性。
RSSI 多天线均值处理
tshark 在多天线适配器上可能返回逗号分隔的多个 RSSI 值。代码对这种情况取平均值,体现了对实际抓包场景的考虑。
循环模式 + 可视化
--loop + --analyze 的组合形成了一个轻量级的持续监控系统:循环采集数据写入 JSON 日志,随后生成交互式 Plotly 图表。这对于长期人流监控非常实用。
5.2 局限性
MAC 随机化问题
这是项目面临的最大技术挑战。自 iOS 8(2014 年)和 Android 6(2015 年)起,主流移动操作系统在发送 Probe Request 时默认使用 随机化 MAC 地址,而非设备的真实 MAC。这意味着:
- 同一部手机在不同时间可能使用不同的 MAC 地址
- OUI 过滤可能失效,因为随机化 MAC 不一定使用真实厂商前缀
- MAC 去重策略导致同一部手机被多次计数
项目代码中的 OUI 过滤在 MAC 随机化场景下的效果会大打折扣。虽然 --allmacaddresses 选项可以绕过 OUI 过滤统计所有 MAC,但这又引入了非手机设备的噪声。
70% 修正系数的粗放性
python
percentage_of_people_with_phones = 0.7
这个固定值来自 2016 年的一条 Twitter 数据,针对的是美国/加拿大的智能手机普及率。在不同地区、不同人群(如儿童比例较高的场所)、不同时间段,这个值可能差异很大。
扫描窗口的局限性
README 提到手机发送 Probe Request 的周期为 1~10 分钟。默认 60 秒的扫描时长可能错过部分处于休眠周期的设备,导致低估人数。延长扫描时间可以提高覆盖率,但实时性会下降。
无去重机制
--loop 模式下每次扫描独立计算,同一部手机在不同扫描周期中被重复计数。虽然 MAC 地址可用于跨周期去重,但当前代码并未实现这一逻辑(各周期的 cellphone_people 列表是独立的)。
OUI 数据的时效性
OUI 数据库在首次运行时从 IEEE 官网下载,但之后不会自动更新。新厂商的新 OUI 前缀不会自动纳入,可能漏检较新的手机品牌。不过用户可以手动删除 oui.txt 触发重新下载。
5.3 安全与法律风险
README 中明确标注了法律警告:
It may be illegal to monitor networks for MAC addresses, especially on networks that you do not own. Please check your country's laws.
在美国,监听网络通信可能触犯 18 U.S.C. § 2511(联邦窃听法)。即使在自己拥有的网络上操作,也应注意隐私合规。在许多国家和地区,未经授权嗅探 WiFi 流量可能构成违法行为。
六、架构亮点与不足总结
架构亮点
| 设计决策 | 价值 |
|---|---|
| tshark 作为抓包后端 | 避免重复造轮子,获得工业级协议解析能力 |
| OUI 厂商过滤 | 有效区分手机与其他 WiFi 设备 |
| Click CLI 框架 | 参数设计清晰,支持丰富的使用模式 |
| JSON 日志 + Plotly 可视化 | 轻量级数据持久化与可视化方案 |
| Docker 容器化 | 降低环境配置门槛 |
| 多天线 RSSI 均值 | 针对真实抓包场景的细节处理 |
架构不足
| 问题 | 影响 |
|---|---|
| MAC 随机化未处理 | 核心准确性受现代手机系统影响 |
| 70% 修正系数固定 | 不适配不同地区和人群 |
| 循环模式无跨周期去重 | 长期监控场景精度不足 |
| OUI 数据不自动更新 | 可能漏检新厂商设备 |
| 串行 subprocess 调用 | 抓包与解析分两步,中间存在 I/O 开销 |
| 错误处理较粗放 | tshark 异常、适配器问题等场景处理不够健壮 |
七、适用场景与改进建议
适用场景
- 低精度人流感知:如判断"办公室是否有人"、"家中室友是否回来"等定性场景
- 树莓派 DIY 项目:低成本、低功耗的客流监测方案
- 安全研究与教学:理解 802.11 协议和 WiFi 嗅探原理的实践项目
- 原型验证:在投入更复杂的传感器方案前快速验证人流感知的可行性
改进建议
-
应对 MAC 随机化:可以研究 Probe Request 帧中的其他特征(如序列号 Sequence Number 的连续性、帧间间隔模式)来识别同一设备,而非仅依赖 MAC 地址。学术界已有 OUI-based 与 sequence-number-based 的去随机化研究。
-
动态修正系数:允许用户根据所在地区/场景配置智能手机普及率,或提供常见地区的预设值。
-
跨周期去重 :在
--loop模式下维护一个带 TTL(过期时间)的 MAC 缓存,避免同一设备被多次计入。需结合 MAC 随机化策略使用。 -
实时流式处理:当前方案是"先抓包再解析"的批量模式。可以改为流式处理 tshark 输出,实时更新计数,降低延迟。
-
信号强度距离估算 :利用 RSSI 进行粗略的距离估算(path-loss 模型),结合
--nearby选项提供更灵活的 proximity 感知。 -
OUI 数据自动更新:添加版本检查机制,定期刷新 OUI 数据库。
-
HTTPS 下载 OUI :
oui.py中使用的是http://而非https://下载 OUI 数据,存在中间人攻击风险。
八、结语
howmanypeoplearearound 是一个"小而美"的项目。它用不到 300 行核心代码,巧妙地将 WiFi 协议特性、OUI 数据库和统计修正组合在一起,实现了"数人头"这一看似复杂的任务。项目最大的价值不在于精度,而在于展示了一种极简的感知思路:不需要摄像头、不需要蓝牙信标,一块 WiFi 适配器就够了。
然而,随着现代移动操作系统普遍采用 MAC 随机化,项目面临的核心挑战日益严峻。这也反映了技术演进中的一个普遍现象:基于特定时代技术特征的方案,会随着底层技术的变化而逐渐失效。项目作者也提到了它的前身 find-lf,一个使用树莓派集群进行室内定位的更复杂方案------howmanypeoplearearound 正是 find-lf 的简化版。
对于想深入理解 802.11 协议、WiFi 嗅探和被动感知技术的开发者来说,这个项目是一个极佳的学习起点。代码简洁、依赖清晰、原理明确,非常适合作为"从零到一"的实践教材。