RISC-V开发板结合Python实现B站消息监测:硬件极客的IoT实践 1. 项目概述当硬件极客遇上日常痛点前几天在极客社区里看到一个挺有意思的分享一位开发者朋友用一块高性能的RISC-V开发板结合自己写的Python脚本做了一个B站未读消息的实时监测器。这项目乍一听有点“杀鸡用牛刀”的感觉——用一块能跑Linux的开发板去监测一个网页消息但仔细琢磨这恰恰是硬件创客精神的典型体现用一个看似“过剩”的技术方案去解决一个真实存在的、细小的生活痛点并在过程中把玩技术、验证想法。这个项目的核心逻辑并不复杂通过Python脚本模拟浏览器行为定期登录B站并抓取网页数据解析出未读消息数量最后通过开发板上的硬件接口比如GPIO控制的LED灯、屏幕或者蜂鸣器给出一个物理世界的提示。它解决的痛点很明确对于重度B站用户尤其是创作者或活跃社群参与者频繁打开App或网页查看是否有新回复、新或新私信是一种注意力的碎片化干扰。这个设备可以让你在专注工作或学习时无需分心查看手机或电脑仅凭余光瞥一眼桌面上某个指示灯的颜色或闪烁频率就知道“有事发生”。从技术栈来看它巧妙地串联了三个层面应用层的网络爬虫与数据解析、系统层的Linux环境与定时任务、硬件层的物理交互与状态显示。选择RISC-V开发板而非更常见的树莓派或ESP32除了性能足够流畅运行完整的Python环境及依赖库外也带有一定的技术探索和社区支持考量。RISC-V作为开放指令集架构其生态正在快速发展用其开发板做这种贴近实际应用的小项目本身就是对生态的一种实践和贡献。对于想入门前端/后端与硬件结合IoT雏形、学习Python自动化脚本编写或者单纯想给桌面增添一个有趣“赛博摆件”的朋友来说这个项目有不错的复现价值。它不涉及复杂的电路设计重点在于软件逻辑和系统集成代码量不大但完整走通后你能掌握网络请求处理、HTML解析、Linux守护进程配置以及基础的硬件控制是一个综合性很强的练手项目。2. 核心思路与方案选型背后的考量为什么是Python为什么是RISC-V开发板为什么用网页抓取而不是官方API这些选择背后都有具体的权衡。2.1 技术栈选型Python Requests BeautifulSoup首选Python是自然而然的。对于这种需要快速发起HTTP请求、解析HTML文本、处理JSON数据的自动化任务Python的生态优势巨大。requests库让网络请求变得极其简单BeautifulSoup或lxml是HTML解析的利器。整个脚本的核心逻辑可能在100行以内就能实现开发效率极高。为什么不直接用B站官方API这是一个关键决策点。官方API通常需要申请复杂的OAuth2认证涉及access_token的管理与刷新对于个人小项目来说增加了不小的复杂度。更重要的是API的权限控制严格未必能直接获取到“未读消息总数”这个聚合信息。而通过模拟浏览器访问网页版个人空间可以直接抓取到页面上显示的数字虽然方式“原始”但直达目标且不需要处理令牌过期等问题对于单用户、低频查询的场景更加简单粗暴有效。当然网页抓取的缺点是稳定性依赖B站前端页面结构。如果B站改版选择器可能失效需要更新脚本。但这对于学习项目而言反而是一个优点——你学会了如何应对这种变化如何定位元素这本身就是一项实用技能。2.2 硬件平台为何选择高性能RISC-V开发板很多人第一反应可能是用树莓派Zero W或者ESP32这类更普及、性价比更高的板子。这个项目选择一块能跑完整Linux发行版的高性能RISC-V开发板主要有几点考虑开发环境一致性在x86电脑上开发调试Python脚本可以几乎无缝地部署到同样运行Linux的RISC-V开发板上。依赖库如requests,bs4可以直接通过pip安装环境配置简单。如果使用ESP32则需要使用MicroPython或C/C开发环境和库的差异会带来额外的学习成本。处理能力富余解析HTML、处理网络请求虽然不重但需要一个稳定的Python运行时和网络栈。高性能RISC-V开发板例如基于平头哥玄铁C906内核的板子通常配备数百MB甚至上GB的内存运行一个轻量级Linux发行版如Debian、Ubuntu Base绰绰有余保证了脚本长期稳定运行不会因为内存不足而崩溃。探索与学习价值RISC-V是当前硬件领域的热点。使用其开发板进行实际项目开发能让你直观感受RISC-V架构的软件生态了解如何为一种新架构交叉编译或直接安装软件包这本身就是宝贵的经验。强大的外设与扩展能力这类开发板通常具备丰富的GPIO、USB、以太网/Wi-Fi接口未来可以轻松扩展传感器、屏幕、语音模块等将本项目升级为一个功能更丰富的桌面信息终端。注意如果你手头没有RISC-V开发板用树莓派3B/4B、香橙派等任何能运行Linux的ARM开发板完全可行整体方案和步骤几乎一致。选择RISC-V更多是“玩”的成分和面向未来的考量。2.3 整体架构设计项目的架构非常清晰是一个典型的“感知-处理-执行”循环感知层Python脚本定期执行负责从B站网页抓取未读数量。处理层Linux Crontab/Systemd作为调度器定时触发感知层脚本执行。执行层硬件接口根据脚本输出的结果控制开发板上的物理设备如LED、屏幕改变状态。这个架构的优点是模块化。你可以轻易替换其中任何一环。比如把B站换成邮箱、GitHub通知把LED提示换成在LCD屏幕上显示详情甚至通过语音播报。3. 核心脚本拆解与关键代码实现让我们深入到Python脚本的核心部分。一个健壮的脚本不仅要能抓取数据还要考虑异常处理、状态持久化和资源管理。3.1 环境准备与依赖安装首先确保你的RISC-V开发板已经连接网络并安装了基本的Python3和pip。通过SSH登录到开发板进行以下操作# 更新软件包列表 sudo apt update sudo apt upgrade -y # 安装Python3和pip如果尚未安装 sudo apt install python3 python3-pip -y # 安装必要的Python库 pip3 install requests beautifulsoup4 lxml这里选择lxml作为BeautifulSoup的解析器因为它比纯Python实现的html.parser速度更快在资源有限的嵌入式环境里这一点点性能提升也是有意义的。3.2 模拟登录与会话维持直接抓取个人页面需要登录状态。我们采用复用浏览器Cookie的方式这是最简单且对服务器压力最小的方式。第一步手动获取Cookie。在电脑上使用Chrome或Edge浏览器登录B站。打开开发者工具F12切换到Network网络选项卡。刷新一下个人主页https://www.bilibili.com。在网络请求列表中点击任意一个请求通常是第一个www.bilibili.com的文档请求在Headers标头选项卡中找到Request Headers请求头里的Cookie字段。将其完整内容复制出来。它看起来像一串keyvalue;组成的字符串。第二步在脚本中使用Cookie。import requests from bs4 import BeautifulSoup # 将你复制的Cookie字符串粘贴在这里 MY_COOKIE your_cookie_string_here headers { User-Agent: Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Cookie: MY_COOKIE } def check_bilibili_unread(): url https://www.bilibili.com try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 检查HTTP请求是否成功 # ... 后续解析代码 except requests.exceptions.RequestException as e: print(f网络请求失败: {e}) return None重要提示Cookie是个人敏感信息绝对不能将包含真实Cookie的脚本上传到GitHub等公开仓库。你应该将Cookie存储在环境变量或单独的配置文件中如config.py并在.gitignore中忽略该文件。在脚本中通过os.environ.get(BILI_COOKIE)来读取。3.3 精准解析未读消息数量登录后B站首页或个人空间页面的HTML里包含了未读消息的DOM元素。我们需要找到它并提取数字。由于B站前端可能动态渲染直接查找静态HTML有时找不到。更可靠的方法是观察浏览器加载首页时发出的XHRAjax请求。打开浏览器开发者工具登录B站后停留在首页。切换到Network选项卡勾选Fetch/XHR过滤器。刷新页面你会看到一系列请求。寻找名称包含unread、message或feed的请求。点击该请求查看Preview预览或Response响应选项卡通常是一个JSON格式的数据里面清晰包含了未读私信、未读、未读回复等具体数量。假设我们找到了一个API端点返回JSON数据例如{ code: 0, data: { unread_private: 3, unread_at: 1, unread_reply: 5 } }那么脚本的解析部分将变得非常简单和稳定import json def check_bilibili_unread(): api_url https://api.bilibili.com/x/msgfeed/unread # 示例URL需实际查找 try: response requests.get(api_url, headersheaders, timeout10) data response.json() if data[code] 0: unread_data data[data] total_unread unread_data.get(unread_private, 0) \ unread_data.get(unread_at, 0) \ unread_data.get(unread_reply, 0) return total_unread else: print(fAPI返回错误: {data[message]}) return None except (requests.exceptions.RequestException, json.JSONDecodeError) as e: print(f获取或解析数据失败: {e}) return None如果找不到清晰的API退而求其次还是得用BeautifulSoup解析HTML。你需要仔细分析页面元素找到显示未读数字的那个span或div通常它会有特定的class或id比如.header-unread或#narrow-channel-unread。通过浏览器的“检查”功能可以定位到它。def check_bilibili_unread_via_html(): url https://www.bilibili.com try: response requests.get(url, headersheaders, timeout10) soup BeautifulSoup(response.content, lxml) # 这里需要根据实际页面结构调整选择器 # 例如查找一个包含未读数字的span unread_element soup.select_one(span.header-unread-count) if unread_element and unread_element.text.isdigit(): return int(unread_element.text) else: # 可能元素不存在或文本不是数字返回0 return 0 except Exception as e: print(f解析HTML失败: {e}) return None3.4 状态持久化与变化检测脚本不应该每次运行都无脑触发硬件提示。我们需要记录上一次的未读数只有在新查询到的数字大于上一次记录时才认为有“新消息”触发提示。import os import json STATE_FILE /tmp/bilibili_unread_state.json def save_state(unread_count): 将当前未读数保存到文件 with open(STATE_FILE, w) as f: json.dump({last_count: unread_count}, f) def load_state(): 从文件加载上一次的未读数如果文件不存在则返回0 if os.path.exists(STATE_FILE): try: with open(STATE_FILE, r) as f: state json.load(f) return state.get(last_count, 0) except (json.JSONDecodeError, IOError): return 0 return 0 def main(): current_unread check_bilibili_unread() if current_unread is None: # 获取失败本次不进行任何操作也不更新状态 return previous_unread load_state() if current_unread previous_unread: print(f发现新消息当前未读: {current_unread}, 上次未读: {previous_unread}) # 这里调用函数触发硬件提示例如点亮LED trigger_hardware_notification(current_unread) # 更新状态 save_state(current_unread) elif current_unread previous_unread: # 用户可能已经阅读了消息更新状态即可 print(f未读数减少更新状态。当前: {current_unread}) save_state(current_unread) else: # 未读数未变化无需操作 print(f状态未变化未读数: {current_unread})4. 硬件交互与状态提示实现脚本获取到状态后需要通过开发板的硬件接口给出反馈。这里提供几种常见且简单的方案。4.1 方案一GPIO控制LED灯最基础这是最直观的方式。你可以使用一个双色LED共阴极或者两个单色LED一个绿色代表“无新消息”一个红色代表“有新消息”。接线示意图以通用GPIO为例LED1绿色阳极 - 开发板GPIO引脚如GPIO17 - 串联一个220Ω电阻 - 开发板GND。LED2红色阳极 - 开发板GPIO引脚如GPIO27 - 串联一个220Ω电阻 - 开发板GND。Python控制代码使用RPi.GPIO库风格不同板子库可能不同首先安装GPIO控制库对于常见的WiringPi兼容或libgpiod的板子sudo apt install python3-gpiozero -y # 或者针对特定开发板安装对应的SDK使用gpiozero库它抽象得很好代码简洁from gpiozero import LED import time green_led LED(17) # 假设GPIO17接绿色LED red_led LED(27) # 假设GPIO27接红色LED def trigger_hardware_notification(unread_count): if unread_count 0: # 有新消息红灯闪烁 red_led.blink(on_time0.5, off_time0.5, n5, backgroundFalse) # 闪烁5次 green_led.off() else: # 无新消息绿灯常亮 green_led.on() red_led.off()gpiozero的blink方法非常方便直接实现了闪烁效果且backgroundFalse会让函数阻塞直到闪烁完成适合作为提示。4.2 方案二使用OLED屏幕显示详情如果你想让设备显示更多信息比如具体的未读分类私信、、回复一块I2C接口的OLED屏幕如0.96寸SSD1306是绝佳选择。接线屏幕VCC - 开发板3.3V屏幕GND - 开发板GND屏幕SCL - 开发板I2C时钟引脚如GPIO3屏幕SDA - 开发板I2C数据引脚如GPIO2安装驱动库pip3 install luma.oledPython显示代码from luma.core.interface.serial import i2c from luma.oled.device import ssd1306 from luma.core.render import canvas from PIL import ImageFont import time # 初始化I2C和OLED设备 serial i2c(port1, address0x3C) # port根据板子I2C总线号调整 device ssd1306(serial) # 尝试加载字体如果没有就使用默认字体 try: font ImageFont.truetype(/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf, 12) except: font None def display_message(unread_private, unread_at, unread_reply): with canvas(device) as draw: draw.rectangle(device.bounding_box, outlinewhite, fillblack) draw.text((5, 5), B站未读消息, fillwhite, fontfont) draw.text((5, 20), f私信: {unread_private}, fillwhite, fontfont) draw.text((5, 35), f我: {unread_at}, fillwhite, fontfont) draw.text((5, 50), f回复: {unread_reply}, fillwhite, fontfont) # 显示一段时间后清屏或者常驻显示 # time.sleep(10) # device.clear() # 在你的主逻辑中当检测到状态变化时调用 # display_message(3, 1, 5)4.3 方案三结合蜂鸣器与LED多感官提示对于需要强提醒的场景可以加入一个有源蜂鸣器。当发现新消息时让LED闪烁的同时蜂鸣器也短促响几声。接线蜂鸣器正极 - GPIO引脚如GPIO22 - 串联一个1kΩ电阻可选保护GPIO- 蜂鸣器负极 - GND。控制代码from gpiozero import Buzzer buzzer Buzzer(22) def trigger_hardware_notification_with_sound(unread_count): if unread_count 0: red_led.blink(on_time0.3, off_time0.3, n6, backgroundFalse) # 蜂鸣器同步发声 for _ in range(3): buzzer.on() time.sleep(0.1) buzzer.off() time.sleep(0.1) green_led.off() else: green_led.on() red_led.off() buzzer.off()5. 系统集成与后台常驻运行脚本写好了硬件也连好了下一步是让它作为一个“服务”在开发板上24小时不间断运行。5.1 使用Systemd创建守护进程推荐这是最规范、最可靠的方式。Systemd可以管理进程的启动、停止、重启以及日志。创建服务单元文件sudo nano /etc/systemd/system/bilibili-monitor.service编辑服务文件内容[Unit] DescriptionBilibili Unread Message Monitor Afternetwork.target [Service] Typesimple Userpi # 替换为你的用户名如debian WorkingDirectory/home/pi/bilibili_monitor # 替换为你的脚本所在目录 ExecStart/usr/bin/python3 /home/pi/bilibili_monitor/monitor.py Restarton-failure RestartSec10 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.targetUser: 指定运行服务的用户避免使用root。WorkingDirectory和ExecStart: 修改为你的实际路径。Restarton-failure: 脚本异常退出时自动重启。RestartSec10: 重启前等待10秒。启用并启动服务sudo systemctl daemon-reload sudo systemctl enable bilibili-monitor.service # 开机自启 sudo systemctl start bilibili-monitor.service # 立即启动 sudo systemctl status bilibili-monitor.service # 查看状态查看日志sudo journalctl -u bilibili-monitor.service -f # 实时查看日志5.2 使用Crontab定时任务备选如果不想用Systemd也可以用Crontab定时执行脚本。但这种方式无法实现“状态变化时立即触发硬件动作”因为Crontab只负责定时执行脚本内部需要自己判断状态变化。编辑当前用户的Crontabcrontab -e添加一行例如每5分钟执行一次*/5 * * * * /usr/bin/python3 /home/pi/bilibili_monitor/monitor.py /home/pi/bilibili_monitor/cron.log 21这行命令表示每5分钟执行一次脚本并将输出包括错误追加到日志文件中。Systemd vs Crontab对于本项目强烈推荐Systemd。因为Systemd服务是常驻的你可以让脚本内部包含一个循环实现更灵活的检测间隔比如每分钟检测一次并且能更好地管理资源依赖网络就绪后才启动。Crontab适合执行独立、短时间的任务。6. 项目优化与扩展思路一个基础版本完成后可以从以下几个方向进行优化和扩展让项目更实用、更健壮。6.1 提升脚本健壮性Cookie自动刷新网页Cookie会过期。可以编写一个辅助脚本模拟登录流程获取新Cookie并自动更新配置文件。这涉及处理验证码复杂度较高。一个更简单的方案是设置一个较长的Cookie过期提醒手动更换。双保险解析策略同时尝试API和HTML解析。优先使用API如果API失败或返回格式不符则降级到HTML解析。提高容错率。添加网络检查与重试机制在发起请求前先检查网络连通性。请求失败时进行指数退避重试。结构化日志使用Python的logging模块替代print可以输出不同级别的日志INFO, WARNING, ERROR并配置日志轮转方便后期排查问题。6.2 丰富硬件交互形式多级提示根据未读消息的数量级设计不同的提示强度。例如1-5条绿灯慢闪。6-20条黄灯快闪。20条以上红灯闪烁并伴随蜂鸣。加入物理按钮添加一个按钮按下后可以在OLED屏幕上轮播显示最新的几条消息摘要需要解析更多API或者一键标记所有消息为已读通过模拟点击网页或调用API。远程通知结合开发板的网络能力当检测到新消息时除了本地硬件提示还可以通过Telegram Bot、Server酱、钉钉机器人等将通知推送到你的手机。6.3 平台与功能扩展多平台聚合将脚本改造成一个通用的“网络消息监测器”。除了B站还可以加入GitHub通知、邮箱未读、论坛私信等。为每个平台写一个插件化的检查函数主程序轮询所有插件。数据可视化与历史记录将每次检查的未读数量和时间戳记录到SQLite数据库中。然后写一个简单的Flask或FastAPI web服务跑在开发板上通过浏览器访问一个本地网页可以看到未读消息数量的历史趋势图。语音播报接入一个USB麦克风或语音合成模块如SYN6288当有新消息时用语音播报“您有新的B站消息”。7. 常见问题与故障排查实录在实际部署和运行中你几乎一定会遇到下面这些问题。7.1 网络请求失败或返回异常数据现象脚本报错requests.exceptions.ConnectionError或返回的数据不是预期的JSON/HTML。排查检查网络在开发板上执行ping www.bilibili.com看是否能通。检查CookieCookie可能已过期。手动在浏览器访问B站看是否仍处于登录状态。重新获取Cookie并更新脚本。检查User-Agent有些网站会屏蔽一些非常见的User-Agent。确保你的User-Agent字符串是常见的浏览器标识。打印响应内容在脚本中临时添加print(response.text[:500])查看服务器返回的实际内容是什么。可能是反爬虫页面如验证码也可能是页面结构已变更。解决更新Cookie调整请求头或根据新的页面结构修改解析代码。7.2 GPIO控制无反应LED不亮现象脚本运行无报错但LED灯没有任何反应。排查检查接线这是最常见的问题。确认LED的正负极没有接反电阻已正确串联GPIO引脚号在代码和物理连接上是否一致。检查电压用万用表测量GPIO引脚在输出高电平时是否有电压通常是3.3V。有些引脚默认功能可能不是GPIO需要先配置。检查库和权限运行GPIO控制通常需要root权限或用户加入gpio组。使用sudo运行脚本测试或者将当前用户加入相关硬件访问组如sudo usermod -a -G gpio $USER然后重新登录。查看系统日志运行dmesg | tail看看是否有关于GPIO的错误信息。解决纠正接线确认引脚编号使用正确的权限运行脚本。对于gpiozero库它通常会自动处理权限和引脚映射问题多出在硬件连接。7.3 Systemd服务启动失败现象sudo systemctl status bilibili-monitor.service显示状态为failed或inactive。排查查看详细日志sudo journalctl -u bilibili-monitor.service -n 50查看最近50行日志错误信息通常在这里。检查路径和权限确保WorkingDirectory和ExecStart中的路径存在且可执行。确保User指定的用户有该目录的读取和执行权限。检查Python依赖服务是在系统环境下运行的可能缺少Python库。尝试在服务指定的WorkingDirectory下以对应用户身份手动执行python3 monitor.py看是否报错。检查环境变量有时脚本依赖某些环境变量如PATH。可以在[Service]部分添加Environment指令来设置或者直接在脚本中使用绝对路径。解决根据日志错误信息逐一修正。常见做法是先确保在命令行下能手动正常运行脚本再调试Systemd配置。7.4 资源占用与性能考量担忧这个脚本会不会很耗电、占资源分析完全不必担心。一个简单的Python脚本每分钟甚至每5分钟发起一次HTTP请求并做简单的文本解析其CPU和内存占用可以忽略不计。RISC-V开发板在空闲时功耗本身就很低。主要的资源消耗是网络流量每次请求的流量在几KB到几十KB之间对于现代网络环境来说微乎其微。优化建议可以将检测间隔调整到合理范围比如5-10分钟一次既能及时提醒又不会对B站服务器造成不必要的压力。在脚本中增加随机延迟避免所有用户都在整点请求。