ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Python和rumps开发macOS菜单栏应用:实现Hacker News Karma实时监控

用Python和rumps开发macOS菜单栏应用:实现Hacker News Karma实时监控 经常泡 Hacker News 的开发者大概率都有过这样的体验刷完一条帖子顺手点开自己的 profile看看 karma 涨了还是跌了。这个动作本身不复杂但一天重复几遍之后就会觉得有点繁琐。如果能把当前 karma 直接放进 macOS 菜单栏一抬眼就能看到体验会舒服很多。最近 HN 上有人发布了一个叫 KarmaBar 的项目做的事情就是在菜单栏里展示 Hacker News 的 karma 数值。本文不准备只介绍别人的成品而是换一个更有价值的思路带你把 KarmaBar 这类菜单栏小工具从零实现一遍包含 HN API 的使用方式、Python 菜单栏应用的开发套路、定时刷新逻辑、异常处理以及最后如何打包成独立 App。全程代码可以复制直接跑适合有基础 Python 知识的开发者也适合想了解 macOS 菜单栏应用开发的新手。1. KarmaBar 是什么在菜单栏里跟踪 HN Karma1.1 Hacker News 与 Karma 机制Hacker News 是 Y Combinator 旗下的技术社区以技术新闻、创业讨论和高质量评论著称。在这个社区里每个用户都有一个 karma 值相当于社区内的“声望积分”。当你的帖子或评论被其他用户 upvote点赞时karma 就会增加被 downvote 时会减少。作者在 Ask HN、Show HN 等标签下发帖本质上也是希望通过社区反馈获得内容和声望上的正向循环。对于经常在 HN 上回答问题、分享开源项目的开发者来说karma 是一个有实际参考意义的指标。它一方面代表社区对你内容的认可程度另一方面也会影响部分账号功能的使用体验。正因为大家都关心这个数字才有了“打开 HN - 查看 profile - 刷新 karma”这种重复动作。1.2 菜单栏应用有什么优势macOS 的菜单栏Menu Bar是屏幕顶部那条固定区域。和普通窗口相比菜单栏应用有两个明显优势常驻可见应用启动后会一直显示在菜单栏不需要切换窗口就能看到数据。轻量交互点击菜单栏图标可以展开下拉菜单完成刷新、退出、查看详情等操作交互成本低。所以 KarmaBar 这种“菜单栏 数据展示”的组合非常适合做个人指标监控类工具。除了 HN karma类似的思路还可以用于 GitHub Stars、天气、服务器负载、加密货币价格等场景核心架构完全一致。1.3 KarmaBar 要解决什么问题简单总结KarmaBar 要做三件事通过 Hacker News 官方 API 获取指定用户的 karma 数值。把数值展示在 macOS 菜单栏上例如显示成▲ 1234。支持定时刷新和手动刷新保证数据不是一次性快照。接下来我们从 API 入手逐步搭建这个工具。2. Hacker News API 基础在写界面之前先把数据源搞清楚。Hacker News 提供了官方 API基于 Firebase 实时数据库构建采用 RESTful 风格不需要 API Key使用起来非常简单。2.1 HN API 概况HN 官方 API 的根地址是https://hacker-news.firebaseio.com/v0/在这个根地址下面有几个常用的子资源接口路径作用/topstories.json获取当前热门故事的 ID 列表/newstories.json获取最新故事的 ID 列表/item/{id}.json获取单个故事或评论的详细内容/user/{username}.json获取指定用户的信息KarmaBar 只关心用户信息所以核心接口是/user/{username}.json。2.2 user 接口返回的数据结构当我们请求https://hacker-news.firebaseio.com/v0/user/pg.json返回的 JSON 大致长这样{ about: Im pg., created: 1183263107, id: pg, karma: 123456, submitted: [123, 456, 789] }各字段含义字段说明id用户名karma当前 karma 值created账号创建时间Unix 时间戳about个人简介submitted用户提交过的内容 ID 列表我们需要的主要是karma字段。如果用户名不存在API 返回的是null而不是空对象这一点需要在代码里做判断。2.3 用 curl 快速验证 API在写 Python 代码之前可以先在终端里用 curl 验证一下 API 是否可用以及返回格式是否符合预期。curl https://hacker-news.firebaseio.com/v0/user/pg.json正常情况下会输出一段 JSON。如果你想看更友好的格式化结果可以借助 Python 的json.toolcurl -s https://hacker-news.firebaseio.com/v0/user/pg.json | python3 -m json.tool这样就能比较清晰地看到返回的所有字段。验证通过后我们再继续写 Python 封装。3. 环境准备3.1 运行环境说明如果搭建环境不满足依赖要求需要根据你的项目实际情况调整本文示例以常见的 macOS Python 环境为例重点演示配置思路。操作系统macOS 10.15 及以上Python 版本3.8 及以上包管理工具pip 或 pip3可选virtualenv 或 venv 来隔离项目依赖3.2 安装依赖我们需要两个 Python 库requests发送 HTTP 请求访问 HN API。rumps用于创建 macOS 菜单栏应用的轻量工具库。安装命令如下pip3 install requests rumps如果你的机器上同时存在多个 Python 版本建议先在项目目录中创建虚拟环境再安装依赖。虚拟环境能避免污染全局 Python 环境后续用 PyInstaller 打包时也更干净。mkdir karmabar cd karmabar python3 -m venv venv source venv/bin/activate pip install requests rumps创建好虚拟环境后后续运行和打包都基于当前环境执行。3.3 项目结构本项目采用简单的多文件结构代码职责更清晰karmabar/ ├── venv/ # 虚拟环境 ├── hn_client.py # HN API 客户端封装 ├── main.py # 菜单栏应用入口 └── requirements.txt # 依赖清单requirements.txt内容如下requests rumps4. 完整实战用 Python rumps 实现 KarmaBar下面进入核心环节。我们先编写 HN API 客户端再编写菜单栏应用最后运行验证。4.1 封装 HN API 客户端新建hn_client.py把请求逻辑统一放在一个类中方便复用和单元测试。# 文件路径karmabar/hn_client.py import requests class HNClient: Hacker News API 客户端负责获取用户数据。 BASE_URL https://hacker-news.firebaseio.com/v0 def __init__(self, username: str, timeout: int 10): 初始化客户端。 :param username: Hacker News 用户名 :param timeout: 请求超时时间秒 self.username username self.timeout timeout def get_user(self) - dict: 获取用户完整信息。 返回示例: { id: pg, karma: 123456, created: 1183263107, about: ..., submitted: [123, 456, 789] } 如果用户不存在返回 None。 :raises ValueError: 用户不存在时抛出 :raises requests.RequestException: 网络异常时抛出 url f{self.BASE_URL}/user/{self.username}.json response requests.get(url, timeoutself.timeout) response.raise_for_status() data response.json() if not data: raise ValueError(fHN 用户 {self.username} 不存在) return data def get_karma(self) - int: 获取当前 karma 值。 user self.get_user() karma user.get(karma, 0) if not isinstance(karma, int): raise ValueError(karma 字段类型异常) return karma在这个类里get_user()负责完整用户信息get_karma()是 KarmaBar 实际调用的方法。这里对用户不存在、karma 字段类型异常都做了判断避免界面层收到无法处理的脏数据。4.2 构建菜单栏应用主逻辑新建main.py使用 rumps 创建菜单栏应用。# 文件路径karmabar/main.py import requests import rumps from hn_client import HNClient class KarmaBarApp(rumps.App): def __init__(self, username: str): super().__init__(KarmaBar) self.username username self.client HNClient(username) refresh_item rumps.MenuItem(立即刷新, callbackself.refresh_now) quit_item rumps.MenuItem(退出, callbackrumps.quit_application) self.menu [ f当前用户: {username}, None, refresh_item, None, quit_item, ] # 启动时先刷新一次 self.refresh_karma() rumps.timer(300) def auto_refresh(self, _): 每 300 秒自动刷新一次。 self.refresh_karma() def refresh_now(self, _): 菜单点击手动刷新。 self.refresh_karma() def refresh_karma(self): 核心刷新逻辑获取 karma 并更新菜单栏标题。 try: karma self.client.get_karma() self.title f▲ {karma} except requests.exceptions.RequestException: self.title ▲ 网络异常 except ValueError as e: self.title ▲ 用户不存在 except Exception: self.title ▲ 未知错误 if __name__ __main__: # 将 your_hn_username 替换成你自己的 HN 用户名 app KarmaBarApp(your_hn_username) app.run()这段代码有几个关键点rumps.App(KarmaBar)创建应用应用名会出现在菜单栏。self.title可以动态修改菜单栏显示文本这里用来显示“▲ 1234”这样的格式。rumps.timer(300)是 rumps 自带的定时器装饰器让方法每 300 秒被调用一次。菜单项通过rumps.MenuItem绑定回调函数点击“立即刷新”会执行refresh_karma()。在refresh_karma()中做了异常捕获网络异常、用户不存在时不会导致应用崩溃而是显示提示文本。4.3 运行与验证在终端中执行python3 main.py如果一切正常你会在 macOS 顶部菜单栏看到KarmaBar的显示名称和一个 karma 数字。点击应用图标会展开下拉菜单看到“当前用户”“立即刷新”“退出”等菜单项。如果当前用户名不存在菜单栏会显示“▲ 用户不存在”。如果网络请求超时则显示“▲ 网络异常”。这里有一个小建议第一次运行前最好先用 curl 确认你能正常访问 HN API。如果 curl 都拿不到数据说明是本地网络或 DNS 的问题而不是代码的问题。4.4 代码里还可以改进的地方上面的版本是能直接运行的 MVP。要继续做深可以从几个方向扩展将用户名改成配置项避免每次修改代码。增加“打开个人主页”菜单项点击后用默认浏览器打开https://news.ycombinator.com/user?idusername。增加刷新时间间隔选择“5 分钟”“15 分钟”“30 分钟”可切换。增加通知提示karma 增长超过一定数值时发送系统通知。接下来我们挑其中一两个典型功能展开让 KarmaBar 更接近一个完整的个人工具。5. 功能扩展缓存、通知与趋势记录5.1 增加本地缓存如果请求频率过高或者希望 KarmaBar 在断网状态下仍然能显示上一次的数值可以引入本地缓存。把上次获取到的 karma 缓存到本地 JSON 文件中请求失败时读取缓存兜底。# 文件路径karmabar/cache.py import json import os class KarmaCache: 简单的本地 JSON 缓存。 def __init__(self, cache_file: str karma_cache.json): self.cache_file cache_file def load(self): if not os.path.exists(self.cache_file): return None try: with open(self.cache_file, r, encodingutf-8) as f: return json.load(f) except (json.JSONDecodeError, OSError): return None def save(self, data: dict): try: with open(self.cache_file, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) except OSError as e: print(f保存缓存失败: {e})在HNCclient中集成缓存的思路是先尝试请求网络失败时读取缓存。如果拿到的数据有效同时写回缓存。# 文件路径karmabar/hn_client.py import requests from cache import KarmaCache class HNClient: # ... 其他代码保持不变 ... def __init__(self, username: str, timeout: int 10): self.username username self.timeout timeout self.cache KarmaCache() def get_karma_with_cache(self) - int: try: karma self.get_karma() self.cache.save({username: self.username, karma: karma}) return karma except requests.exceptions.RequestException: cached self.cache.load() if cached and cached.get(username) self.username: return cached.get(karma, 0) raise这样即使临时断网KarmaBar 也能显示缓存的数值而不是直接变成“网络异常”。5.2 增加变化通知很多 HN 用户喜欢关注 karma 增长趋势。我们可以记录上一次的 karma 值当本次刷新后 karma 比上次高时发送一条系统通知。在 rumps 中发送通知非常方便rumps.notification(KarmaBar, Karma 增长了, f当前 karma: {karma}, soundTrue)把这句放到刷新逻辑中判断条件后触发def refresh_karma(self): try: karma self.client.get_karma() self.title f▲ {karma} if self.last_karma is not None and karma self.last_karma: rumps.notification( KarmaBar, Karma 增长了, f从 {self.last_karma} 增长到 {karma}, soundTrue, ) self.last_karma karma except requests.exceptions.RequestException: self.title ▲ 网络异常 except ValueError: self.title ▲ 用户不存在 except Exception: self.title ▲ 未知错误增加通知后KarmaBar 就从一个“被动查看”工具变成了“主动推送”工具。不过也要注意如果刷新间隔太短通知可能会打扰正常工作建议通知频率控制在合理范围内。5.3 记录历史趋势如果想要更完整的数据统计可以在每次刷新时把 karma 值和时间一起写入 CSV 文件。# 文件路径karmabar/karma_logger.py import csv import os import time class KarmaLogger: def __init__(self, log_file: str karma_history.csv): self.log_file log_file if not os.path.exists(self.log_file): with open(self.log_file, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([timestamp, karma]) def log(self, karma: int): with open(self.log_file, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([int(time.time()), karma])借助 CSV 文件后续可以配合 Excel 或者 pandas 做简单的趋势图表甚至画出 karma 随时间变化的折线图。这个功能虽然简单但对于喜欢做个人数据可视化的开发者来说非常实用。6. 常见问题与排查思路在实际运行过程中比较容易遇到下面几类问题。6.1 菜单栏没有显示图标问题现象常见原因解决思路运行python3 main.py后菜单栏没有出现 KarmaBarrumps 版本过旧或 macOS 权限限制升级 rumpspip install --upgrade rumps程序启动后闪退Python 环境缺少某个依赖确认 requests、rumps 都安装在当前虚拟环境中菜单栏有应用名但没有菜单内容rumps.App 初始化异常检查self.menu是否包含可迭代对象菜单栏标题一直是▲ 用户不存在用户名拼写错误或大小写不匹配先用 curl 验证用户是否存在其中rumps对 macOS 版本有一定要求。如果你在比较旧的 macOS 上运行建议先查看当前 rumps 版本是否支持。6.2 刷新功能不生效问题现象常见原因解决思路点击“立即刷新”后菜单栏数字不变callback没有绑定到正确方法检查rumps.MenuItem的参数传递定时刷新不触发rumps.timer装饰器没有生效确认装饰的方法位于rumps.App子类中菜单栏长时间显示▲ 网络异常HN API 在当前网络环境不可达使用 curl 验证网络连通性适当增加超时时间刷新后数字与实际不符HTTP 缓存导致拿到旧数据考虑在请求 URL 后追加时间戳参数6.3 打包成 App 后无法运行问题现象常见原因解决思路PyInstaller 打包后启动无响应缺少 rumps 的 hidden import在打包命令中加入--hidden-import打包后没有菜单栏图标App 没有设置为 agent 应用修改 Info.plist 添加LSUIElement双击 App 无法启动签名或权限问题使用codesign重新对 App 签名打包相关的内容会在下一节详细说明。7. 工程化与发布建议7.1 打包成独立 AppKarmaBar 作为个人工具使用python3 main.py运行已经足够。但如果你想把它分享给朋友或者做成一个双击就能运行的独立应用推荐使用 PyInstaller。在虚拟环境中执行pip install pyinstaller pyinstaller --windowed --name KarmaBar main.py--windowed参数表示不打开终端窗口适合菜单栏应用。打包完成后在dist/KarmaBar.app目录下会生成独立 App。需要注意rumps 作为第三方库在 PyInstaller 打包时可能需要额外的 hidden import。可以在打包命令中加入pyinstaller --windowed --name KarmaBar --hidden-importrumps main.py如果你发现打包后的 App 在菜单栏不显示而 Dock 栏也没有图标通常是LSUIElement配置问题。你可以编辑生成的KarmaBar.app/Contents/Info.plist加入下面这段keyLSUIElement/key true/LSUIElement设为true后App 会以代理应用方式运行不占用 Dock 图标只保留菜单栏入口。这是菜单栏类应用的标准配置。7.2 日志与异常上报个人小工具虽然不需要复杂日志体系但良好的日志习惯依然重要。建议在关键位置打点import logging logging.basicConfig( filenamekarmabar.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s, ) # 在刷新成功后记录 logging.info(f刷新成功: {karma}) # 在异常时记录 logging.error(f刷新失败: {e}, exc_infoTrue)如果 KarmaBar 未来要加入“karma 增长率”“历史趋势”等功能日志数据也能直接用于分析。7.3 刷新频率与 API 限流HN API 虽然不强制要求 API Key但过于频繁的请求仍然有可能触发限流。在生产使用场景下建议遵守两个原则最低刷新间隔不要低于 60 秒。尽量使用缓存机制避免短时间内重复请求同一用户。KarmaBar 默认 300 秒刷新一次从实际体验来看已经足够既不会让数值显得太滞后也不会给 API 造成压力。7.4 最小权限与隐私KarmaBar 只需要通过网络请求访问公开 API不涉及本地敏感数据。这里仍然有两个建议不在代码中硬编码任何敏感信息例如 HN 登录 Cookie 或邮箱。如果后续打开“浏览器跳转”功能只调用系统的默认浏览器不使用 WebView 加载第三方页面。一个简单的菜单栏工具也应该保持最小权限原则只申请和业务逻辑相关的系统能力。8. 总结与学习建议到这里我们从零实现了一个完整的 KarmaBar 菜单栏应用。整个过程其实并不复杂核心是三条线数据来源通过 Hacker News 官方 API 获取用户 karma 值。界面展示通过 rumps 创建 macOS 菜单栏应用用self.title动态显示数字。刷新机制用定时器实现自动刷新用菜单项实现手动刷新用缓存提高稳定性。顺着这个思路你可以很容易把这个模板迁移到其他监控场景。比如把 HN API 换成 GitHub API菜单栏显示自己的 GitHub Stars 数或者换成天气接口显示当前气温。菜单栏应用的骨架基本是通用的。如果想继续深入可以研究的方向包括Swift SwiftUI 原生实现体验更好但代码更底层。用rumps.notification做更丰富的通知策略。结合pandas对 karma 历史数据做趋势分析。使用 PyInstaller 的 spec 文件精细控制打包资源。从个人工具的角度看KarmaBar 不需要做得太复杂能稳定刷新、清晰展示、快速排查问题就够了。最重要的一点是动手去试把代码跑起来然后再根据自己的使用习惯逐步迭代。如果你也想让 Hacker News 的 karma 常驻菜单栏不妨把上面的代码复制下来改一改你的用户名体验一下自己造轮子的乐趣。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进