我这个人有个习惯不太健康:写东西的时候喜欢把手机屏幕朝下扣在桌上,可一旦真有要紧事没看到,又会在两小时后拍大腿。前几天就因为错过一条行业公告,错过了半个窗口期,当时满脑子都是——要是有一个东西,不吵不闹,只在出现“我最关心的关键词”那一刻,发出短促清脆的一声,那该多好。于是我顺手做了个叫buzz的小设备。
buzz 这个名字有点一语双关:英文里它既是蜜蜂飞过的“嗡嗡声”,也是大家七嘴八舌讨论的“热议声”。我做的正是把这两种声音连起来——当网络热词里出现值得你停下来的信号时,用蜂鸣器发出真实的物理“嗡嗡声”提醒你。这听起来像是个给极客准备的玩具,但实际用过之后,我发现它就是信息过载时代里很实用的一个耳机:不做过滤,只做提醒。这篇文章会从硬件接线讲到软件闭环,再讲到我把它跑了大半年之后踩过的坑和继续想做的方向,如果你想复刻,直接跟着走就行。
1. 先弄清 buzz 到底要做什么:是蜂鸣器,也是提醒器
1.1 信息过载时代的“听觉筛选器”
先别急着接线。我见过太多人一上来就去淘宝下单树莓派和蜂鸣器,结果玩了两天就吃灰。本质上,buzz 要解决的是一个非常具体的问题:你关心的信息渠道太多了,可你没有时间每一条都看。比如我订阅了十几个行业资讯源,一天能刷出上百条更新,但其中真正值得点开的可能就三五条。人眼不可能一直盯着屏幕,但耳朵却可以在你泡咖啡、写文档、甚至发呆的时候,留出一条极窄的通道,只放行那些命中你需求的信号。
所以 buzz 的设计哲学不是“监控一切”,而是“只响该响的”。你可以给它设定一批关键词,比如“开源”“裁员”“融资”,它会把抓到的内容标题、摘要和正文做一个匹配,一旦命中,就驱动一只小蜂鸣器响 1 秒。这声嗡响不是噪音,它是你注意力系统里的“窄门”——只有值得你抬头看的东西,才配通过这扇门。
我在早期版本里犯过一个很典型的错误:把所有关键词都塞进去,匹配规则又特别宽松。结果蜂鸣器从早响到晚,最后我把它关了。换来的教训是:buzz 的价值不在于抓得多,而在于滤得准。宁可一天只响两声,也不要错过真正重要的那一下。
1.2 为什么是树莓派,而不是手机或云函数
很多人会问:手机上设置关键词提醒不就行了?或者干脆写个云函数,发条微信通知?我承认这些都是更省事的路径,但对“自己搭一个硬件项目”这个目标来说,它们都没法给你同样的折腾乐趣和可控性。
手机 App 的通知机制虽然方便,但它依赖一个个封闭应用,你既改不了推送频率的底层算法,也很难自定义铃声逻辑。云函数也稳定,可它只存在于服务器上,你摸不着任何物理实体,也就体会不到那种“代码一跑,真的有个东西在响”的成就感。树莓派则不一样:它有一排 GPIO 引脚,可以直接输出电信号控制蜂鸣器、LED、继电器这些小硬件;同时它又是一个完整的 Linux 系统,跑 Python、装依赖、拉取 RSS、做关键词匹配,和服务器上没什么两样。
我选的是 Raspberry Pi Zero 2 W,理由很简单:体积小、功耗低(峰值约 1W 上下)、可以放在书桌角落不占地方。如果你手上有旧的树莓派 4B 或者 3B+,也完全够用。实际上这个项目的 CPU 占用率非常低,哪怕用性能最弱的树莓派 Zero 一代也能轻松跑起来。硬件只是载体,真正的灵魂是那条从网络数据到 GPIO 电平的链路。
1.3 触发规则:关键词、热词与冷却时间
在动手写代码前,我们得先设计一下“什么时候响”的规则,这是整个项目最容易失控的地方。
我最后用的是一套三级触发逻辑。第一级是固定关键词,这些是我长期关注的、几乎不会变的词,比如“树莓派”“RSS”“蜂鸣器”。第二级是动态热词,我每天会从公开趋势接口拉一份热词列表,做一层清洗后自动加入当天的候选词库,用来感知意料外的新话题。第三级是冷却时间:同一个内容源、同一条链接,在 10 分钟内重复出现时只响第一次,避免 RSS 更新时把整个列表重新匹配一遍。
为什么要冷却时间?因为很多网站更新 RSS 不是一次性推完的,它可能会分页推送,或者把一个旧链接重新打上时间戳。如果不做去重,你会在短短几分钟内听到同一篇文章响三遍。我最初没有冷却逻辑,结果收到一个行业资讯源的更新时,蜂鸣器连响了四声,就像有人对着门铃捶了一拳。后来加了哈希去重和 10 分钟冷却,世界清静多了。
2. 硬件骨架:把蜂鸣器接到小电脑上,没那么难
2.1 需要的材料与避开“接线坑”的选购建议
先列一份我实际使用的材料清单,都是很常见的电子元件:
- 树莓派 Zero 2 W(或任意树莓派),以及一张 8GB 以上的 TF 卡
- 5V/2A 电源适配器,最好带开关
- 两只无源蜂鸣器(有源的也可以,但后面会说为什么推荐无源)
- 面包板一块、公对母杜邦线若干
- 一个 S8050 三极管(或者 2N2222 也行)、一个 1kΩ 电阻
- 一个 100nF 陶瓷电容(可选,用于滤掉电源上的杂波)
这里我要单独说一下蜂鸣器的选型。电子市场里常见的蜂鸣器分两种:有源蜂鸣器和无源蜂鸣器。有源的意思是内部自带振荡电路,通电就直接响,控制起来最简单,但声音单调、难听,而且你没法通过代码改变音调。无源蜂鸣器内部没有振荡器,需要外部用 PWM 方波驱动,才能发出声音——但它可以通过改变 PWM 频率,模拟出“嘀”“嘟”“咻”这些不同音调。对 buzz 来说,音调意味着提醒等级,所以我强烈建议选择无源蜂鸣器,虽然驱动代码稍微复杂一点,但可玩性高很多。
还有一个很容易踩的坑是:树莓派 GPIO 引脚输出的高电平只有 3.3V,而且单个引脚最大能提供的电流大约在 16mA 左右。普通小蜂鸣器的工作电流 20-30mA,直接接在 GPIO 上很容易把引脚拉垮,轻则声音小得可怜,重则导致系统不稳定。稳妥的做法是让 GPIO 先去驱动三极管,再用三极管去开关蜂鸣器的电源回路。这就好比 GPIO 是脆弱的指挥家,三极管是力气很大的执行者,电信号经过放大后,喇叭才能响得实。
2.2 接线:从 GPIO 到蜂鸣器的电流路径
接线其实不复杂,但顺序很重要。我用的是 BCM 编号方式,也就是树莓派系统里软件直接读到的这个针脚号。拿 GPIO17 举例:
- 将 GPIO17 用一个 1kΩ 电阻连接到 S8050 三极管的基极(B 脚)。
- 三极管的发射极(E 脚)接到面包板的负极总线,也就是 GND。
- 无源蜂鸣器的负极接到三极管的集电极(C 脚)。
- 无源蜂鸣器的正极接到 5V 或 3.3V 电源线上,我用的是 5V,音量会更足。
- 最后,树莓派的 GND 必须和面包板的 GND 共地,否则信号无法形成回路。
这里有一个很容易翻车的细节:千万别把蜂鸣器直接怼在 GPIO 和 GND 之间。我在最开始偷懒,直接用一个带三根脚的蜂鸣器模块接到了 GPIO 上,模块自带驱动电路倒是没问题;但换成裸蜂鸣器时,我一度图省事直接插到 3.3V 和 GND 之间,结果响是能响,整个板子的电压纹波却变得很明显,SSD 偶尔都会卡一下。后来老老实实接了三极管,再用一颗 100nF 陶瓷电容并联在蜂鸣器两端吸收反向尖峰,从此再没遇到系统重启的问题。
如果你手头没有三极管,也可以先用手边的“蜂鸣器模块”来代替,那种模块往往自带 S8550 三极管,只需要接 VCC、GND、I/O 三根线,是把驱动电路帮你封装好了的。我不反对新人从模块起步,但如果你想理解整个链路,亲手用三极管搭一次,绝对值得。
2.3 发声原理:无源蜂鸣器为什么能变调
无源蜂鸣器本质上是一片压电陶瓷片,你给它一个高电平,它就会变形;给低电平,它就恢复。反复快速切换高低电平,陶瓷片就会以同样的频率振动,从而发出声音。所以控制声音的关键只有两件事:PWM 的频率和占空比。
树莓派自带的硬件 PWM 功能可以直接用来驱动蜂鸣器。在 Python 里,gpiozero库提供了PWMOutputDevice,你只需要设置频率和占空比。比如想发一个 1000Hz 的“滴”声,就设pwm.frequency = 1000、pwm.value = 0.5,保持 0.1 秒后关闭。想区分“普通提醒”和“重要提醒”,可以从 800Hz 和 1200Hz 这两个频率里选择,也可以改变时长——短促一声是普通事件,长响三声是高优先级。
这里要特别提醒,蜂鸣器不是一个高保真喇叭,你不用追求播放出歌曲或者复杂音调,那纯属自己难为自己。我一开始洋洋得意写了一段能嘟出“两只老虎”的代码,激动了几分钟,然后发现实际使用时根本分不清旋律,只会增加识别负担。buzz 的声音设计,应该像手机铃声一样直接:一声 = 普通匹配,双连响 = 高权重热词,三连响 = 连续命中多个关键词。简单、可辨识,比花哨都管用。
3. 软件闭环:从热词命中到嗡嗡声的完整链路
3.1 数据源怎么选:RSS 与公开趋势入口
硬件只是嘴,软件才是脑子。buzz 的软件部分可以拆成三块:数据获取、关键词匹配、发声控制。数据获取我优先推荐 RSS,原因很简单:它是世界上现存最古老也最通用的内容订阅协议,几乎每个靠谱的博客、新闻站、技术社区都还在输出 RSS。只要把一个feed.xml的地址扔给feedparser库,它就能帮你解析出标题、链接、摘要和发布时间,省去针对每家网站写爬虫的麻烦。
我在项目里实际订阅了这样几类源:
| 数据源类型 | 示例 | 为什么选它 |
|---|---|---|
| 行业资讯 RSS | 国内外的科技媒体、开源社区博客 | 信息密度高,更新频率适中 |
| 官方公告 RSS | 几个工具和框架的 release 页面 | 重要版本发布能第一时间提醒 |
| 趋势词公开数据 | 一些公开的热度榜单接口 | 捕捉突然升温的新话题 |
| 个人兴趣源 | 几个独立博客、播客 feed | 让 buzz 保留一点“人情味” |
选择 RSS 还有一个额外好处:它不依赖特定的封闭平台,基本不存在需要处理复杂鉴权的问题。你只要在代码里设置合理的超时时间、做好异常捕获,一个数据源挂了不会影响其他源更新。
3.2 核心脚本:匹配、去重、响铃
为了实现“匹配到关键词就响一声”,我写了一个简单版本的核心循环。它的逻辑可以用一句话概括:每 5 分钟拉一次订阅源,过滤出新鲜且命中关键词的文章,然后让蜂鸣器响预设的声音模式。
#!/usr/bin/env python3 # buzz.py - 匹配RSS热点并触发蜂鸣器 import hashlib import time import feedparser from gpiozero import PWMOutputDevice # 硬件配置:GPIO17 => 三极管 => 无源蜂鸣器 buzzer = PWMOutputDevice(17, frequency=800, initial_value=0) # 自定义关键词,按优先级分组:高权重和普通 HIGH_KEYWORDS = ["融资", "收购", "严重漏洞", "停服"] NORMAL_KEYWORDS = ["树莓派", "开源", "热词", "更新", "发布"] # 订阅源 FEEDS = [ "https://example.com/feed.xml", # 替换成你关注的RSS ] # 跑过的链接集合,避免重复响铃 seen = set() def beep(freq=800, duration=0.15, times=1): """蜂鸣器响一次,times代表连续响几次""" for _ in range(times): buzzer.frequency = freq buzzer.value = 0.5 # 50%占空比 time.sleep(duration) buzzer.value = 0 time.sleep(duration) def should_beep(title, summary="", link=""): """判断是否需要提醒,返回优先级""" combined = f"{title} {summary}" for kw in HIGH_KEYWORDS: if kw in combined: return 2 for kw in NORMAL_KEYWORDS: if kw in combined: return 1 return 0 def check_feeds(): for url in FEEDS: feed = feedparser.parse(url) for entry in feed.entries: link = entry.get("link", "") if not link or link in seen: continue title = entry.get("title", "") summary = entry.get("summary", "") level = should_beep(title, summary) if level > 0: seen.add(link) if level == 2: beep(freq=1200, duration=0.25, times=3) else: beep(freq=800, duration=0.15, times=1) while True: try: check_feeds() except Exception as exc: print(f"抓取异常: {exc}") time.sleep(300) # 每5分钟跑一次这段代码里值得注意的有两个地方。第一,seen集合直接放在内存里,虽然对这个项目够用,但如果运行几天后内存会持续增长,最好加一句检测,比如当len(seen) > 10000时清空一半。第二,每个 RSS 源都要记得设置超时,否则某个订阅源假死会让整个循环卡住。feedparser.parse()支持fz.timeout或者你可以在requests层面自己做超时控制,这个后面部署章节我会展开讲。
3.3 让 buzz 动态感知热词
固定关键词的问题在于,你只能知道自己知道的东西。互联网上每天都会蹦出新词,比如某天某个技术名词莫名其妙地火了,但它不在我的列表里。所以我给 buzz 加了一个“热词补充器”,每天在固定时间点,从几个公开的趋势数据入口拉取一份当日热词,清洗掉明显不符合规范的内容,然后把前 10 个冷门且与我的订阅领域有重叠的词,自动放进当天的NORMAL_KEYWORDS全局变量里。
实现逻辑不复杂,但有一个原则要拿捏好:动态热词总是低频命中,永远不要让它跑进高优先级。否则一有热点,buzz 就会和全网热搜同步狂响,又回到信息过载的老路上。我的做法是把动态热词单独放在一个列表里,命中后只发一声短响;只有固定关键词里的高权重词命中,才允许触发三连响。这样一来,热词负责“让你知道外面发生了什么”,而固定关键词才是“现在马上放下手头事去处理的警报”。
实际跑下来,这个设计很关键。有一阵子某个办公软件行业爆发了大讨论,buzz 用热词提醒我不少次,我点进去确实看到了平时不会主动搜索的内容,但它从来没有打断我在写的代码——因为那只是低优先级提醒。反观我设置了“停服”“严重漏洞”这些高权重词,每次响起都真的是需要第一时间处理的消息,甚至有一次比社交群转发还早了十几分钟。
4. 长时间稳定运行:部署、守护与降噪调优
4.1 用 systemd 把 buzz 变成系统服务
手工启动一个 Python 脚本很简单,但跑不了几天你就会发现程序因为各种奇怪原因退出了:某个 RSS 源挂掉、网络断线、系统崩溃。这时候需要的是“守护进程”,让脚本挂了以后自动重启。在树莓派上最省事的方式就是写一个 systemd 服务。
我创建了一个/etc/systemd/system/buzz.service文件,内容大概是这样:
[Unit] Description=buzz keyword beeper After=network-online.target [Service] ExecStart=/usr/bin/python3 /home/pi/buzz/buzz.py WorkingDirectory=/home/pi/buzz Restart=always RestartSec=15 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target这里有几个取值值得解释一下。Restart=always表示无论程序为什么退出,systemd 都会尝试把它拉起来。RestartSec=15是重启前的等待时间,避免因为某个瞬时网络故障导致脚本疯狂重启。PYTHONUNBUFFERED=1的作用是让 Python 的print立刻写入标准输出,这样你在journalctl -u buzz -f里看到的日志就不是“攒着然后一次性刷出来”,对排查问题方便得多。
启用服务只需要三步:sudo systemctl daemon-reload,sudo systemctl enable buzz,sudo systemctl start buzz。之后你就能用systemctl status buzz查看运行状态,用journalctl -u buzz -n 50查看最近 50 行日志。这一步完成之后,buzz 就不再是一个“在终端里跑着的玩具”,而是一个真正安静待在你后台的常驻服务。
4.2 声音模式与提醒降噪
跑了大概一个星期,我又遇到一个问题:虽然匹配规则已经很严格,但还是会有一阵子响得太密。原因是我订阅的几个技术媒体在某些时段集中发布文章,比如傍晚六点连续更新五六篇,其中好几篇都包含“开源”这个词。这时候蜂鸣器就会嘟、嘟、嘟地响成一片,和闹钟没什么差别。
所以我给它加了两个降噪策略。第一个是时间段限制:在晚上 23 点到早上 7 点之间,蜂鸣器完全不响,只把命中的日志记录下来。毕竟我再重视信息也不希望半夜被“某个版本发布”吵醒。在代码里用一个简单判断即可,凌晨时间段内beep()被替换成print("silent match")。第二个是短时归并:如果 30 秒内检测到多次低优先级命中,那么只有第一次会响,之后的进入静默状态。这有点像新闻客户端的“聚合推送”——把碎片拼成一条消息,而不是频繁打扰你。
这两个改动虽然平凡,但对实际体验的影响是巨大的。降噪之后,buzz 从一个“有点烦人的小玩意”变成了真正值得常驻桌面的工具。
4.3 实测中的性能与稳定性坑
这个项目技术上不难,但如果你想让它 24 小时持续运行,还是会遇到几个隐藏的坑。
第一是内存缓慢增长。我的第一个版本里seen集合会无限变大,跑了三四天后内存占用从 80MB 涨到 400MB,树莓派 Zero 2 W 虽然不至于因此爆掉,但明显拖慢了系统。解决办法很简单:给seen加一个上限,超过 10000 条就删除旧条目,因为对 buzz 来说,跑过的链接超过几千条已经没有保留价值。
第二是RSS 源的超时和假死。feedparser.parse()默认超时时间太长,如果某个源服务器一直不回应,整个循环就会卡在那里。后来我改成先用requests带timeout=10拉取内容,再丢给feedparser解析,同时在requests.Session里设置了合理的 User-Agent,否则有些服务器会直接返回 403。这一步解决了很多奇奇怪怪的抓取失败。
第三是GPIO 的异常退出。如果用键盘中断或者进程被 systemd 杀掉,可能会让 PWM 输出保持在高电平,蜂鸣器就一直响个不停。我在代码的finally块里加了一行buzzer.close(),并在 systemd 服务的ExecStop里让脚本收到 SIGTERM 时先关蜂鸣器再退出。这个细节看着不起眼,但避免了多次“半夜起来拔线”的尴尬。
5. 三个月真实使用后,我对 buzz 的进一步思考
5.1 真实体验:一声 buzz 帮我过滤掉的信息泡沫
这个项目我用了大概三个月,从最初的“一时兴起”到现在的稳定日常,最直观的变化是,我刷社交媒体的时间少了。过去我总担心错过重要信息,于是每隔半小时就要去新闻首页、社交媒体、订阅列表轮一圈,大部分时间刷到的都是“别人在聊什么”而不是“我需要知道什么”。buzz 出现之后,我把那些需要主动关注的渠道都缩窄成了几个 RSS 源,平时该干嘛干嘛,只有听到那一声“嘟”,才抬起头切过去看一眼。
有一次让我印象特别深刻。某个周末下午我在阳台修花,屋里传来三连响——这是高权重关键词的提示。我进屋一看,是我长期盯的一个开源项目发布了安全更新公告。因为处理得比较及时,我这边一些依赖它的服务避免了后续临时抱佛脚。这当然不是说 buzz 不可替代,但它确实把一个需要靠“自律”才能完成的动作,变成了一件被动但精准的事。
它最大的价值,也许不是帮你处理信息,而是帮你减少处理信息的次数。我们每天产生的大量注意力消耗,并不来自真正紧要的信息,而来自“我得确保自己没错过什么”的焦虑。buzz 用一声实体的嗡响,把这份焦虑替换成了信任:该响的自然会响,没响就是没你什么事。
5.2 可以继续玩的方向
如果你的 buzz 也顺利跑起来了,我觉得下面几个方向很值得继续折腾。
一是用不同发声器件替代蜂鸣器。我现在在尝试接一个微型扬声器,用playsound播放一段十几 KB 的 mp3 音频,这样可以把提醒音换成更自然的人声短句,甚至让它念出文章标题。硬件改造不复杂,效果却相当“未来”。
二是接入更多输入源。RSS 之外,还可以增加自己的 IM 频道,把群里某条 @我的关键词也作为触发源。不过这个要克制,否则很容易滑向高频率的无效提醒。
三是把 buzz 变成一个通用的消息网关。蜂鸣器只是输出终端之一,你可以让它同时驱动一块小 LCD 屏显示第一条命中标题,或者让树莓派上的 LED 闪几下。我自己已经改造成“蜂鸣器 + 指示灯”双输出:响声提醒你,指示灯颜色告诉你是哪个类型的内容。这算是从“听得见”进化到“看得懂”。
如果你按照上面的步骤搭完,大概率也能收获一个“会在正确时机出声”的小东西。自己在桌上放一块蜂鸣器,当它真正为你命中一条重要消息时,那种“我造出了一个懂我的设备”的感觉,比单纯刷手机难得多了。接下来你完全可以依据自己的信息偏好,继续调整关键词权重、换订阅源、改声音模式——buzz 是一套骨架,往上面长什么肉,全看你自己。