
之前为了获取信息每天都要在手机和电脑上打开一堆网站和App不仅信息流杂乱还容易被算法推荐和无关推送干扰。后来尝试用RSS订阅来聚合内容源虽然信息获取效率提升了但依然需要盯着屏幕长时间下来眼睛容易疲劳。于是我萌生了一个想法能不能把RSS订阅的内容以一种更护眼、更专注的方式呈现出来经过一番折腾我成功地将RSS订阅源的内容自动抓取、格式化并推送到了一个电子墨水屏设备上做成了一份可以随时翻阅的“电子墨水报”。现在我只需要像看报纸一样拿起这个墨水屏设备就能浏览我订阅的所有新闻和技术博客彻底摆脱了手机和电脑屏幕的束缚阅读体验大幅提升。本文将完整分享这个项目的实现过程从RSS订阅的原理、电子墨水屏的选择到核心的API调用、内容抓取与解析再到最终的排版与自动推送。无论你是想复刻一个类似的个人设备还是想学习RSS、API调用和自动化脚本的实战应用都能从本文中找到清晰的步骤和可运行的代码。1. 项目核心RSS与电子墨水屏的结合1.1 为什么选择RSSRSSReally Simple Syndication简易信息聚合是一种基于XML格式的Web内容订阅规范。它允许你将多个网站的最新内容聚合到一个阅读器中实现信息的主动获取而非被动接受平台算法的推荐。核心优势信息主权回归你订阅什么就看到什么内容源完全由自己控制。高效聚合无需逐个打开网站在一个地方即可浏览所有更新。无干扰通常只有标题、摘要和正文没有广告、无关推荐和复杂排版。在项目中RSS是我们获取内容的“源头”。我们会编写程序定期抓取这些RSS源也称为Feed的最新内容。1.2 为什么选择电子墨水屏电子墨水屏E-ink通过反射环境光显示内容其显示效果接近纸质印刷且仅在刷新时耗电显示静态内容时不耗电。作为“报纸”载体的优势护眼无背光、无频闪长时间阅读不易疲劳。专注单色显示、功能单一能有效避免数字设备的多任务干扰。低功耗内容刷新后即可断电非常适合作为信息展示终端。常显特性像真正的报纸一样信息可以一直停留在屏幕上。本项目就是将RSS的“内容聚合”优势与电子墨水屏的“显示体验”优势相结合打造一个专注、护眼的个人信息终端。1.3 系统架构概览整个项目的运行流程可以概括为以下几个步骤内容获取程序通过HTTP请求定期从你配置的RSS订阅源地址抓取XML格式的数据。内容解析解析XML提取出文章的标题、发布时间、摘要或全文、链接等关键信息。内容处理对提取的文本进行清洗如去除HTML标签、格式化、排序如按时间倒序并可能进行摘要生成或翻译。排版渲染将处理好的文本、图片链接等信息按照报纸的版式如分栏、标题加粗、添加日期头等进行排版生成一张可在电子墨水屏上显示的图片。推送显示将生成的图片通过API、USB或网络等方式发送到电子墨水屏设备上进行刷新显示。接下来我们将从环境准备开始一步步实现这个系统。2. 环境与工具准备本项目主要涉及后端数据处理和硬件驱动不限定于某一特定编程语言但为了教程的通用性和易实现性我们选择使用Python作为主要开发语言。Python拥有丰富的库来支持RSS解析、HTTP请求和图像处理。2.1 硬件选择电子墨水屏设备的选择很多从DIY的开发板到成品设备均可。这里列举几种常见方案设备类型代表型号优点缺点适用人群开发板屏幕Raspberry Pi Waveshare e-Paper完全可控可玩性高成本相对较低。需要一定的动手能力需自行组装和保护外壳。极客、硬件爱好者、学生。墨水屏终端小米生态链“墨案”电纸书、大上科技显示器等即买即用显示效果好系统相对开放。成本较高可能需要破解或利用其开放接口。追求省心、显示效果的用户。二手电纸书旧的Kindle需越狱、文石阅读器等成本极低屏幕素质好。系统封闭驱动和开发难度最大稳定性存疑。喜欢挑战的极客预算极其有限。建议对于大多数开发者从树莓派Raspberry Pi搭配微雪Waveshare的电子墨水屏开始是最佳选择。教程将以树莓派微雪屏幕为例进行说明。请根据你的屏幕型号前往微雪官方Wiki查看对应的驱动库和接线说明。2.2 软件与开发环境操作系统树莓派上推荐使用Raspberry Pi OS (Legacy)这是一个稳定的Debian系统。如果你的处理程序运行在云端或另一台电脑上则可以是任何支持Python的系统Windows/macOS/Linux。Python版本建议使用Python 3.8 或更高版本。包管理工具使用pip安装Python库。代码编辑器任选如VS Code、PyCharm等。2.3 核心Python库安装我们将使用以下几个关键的Python库请在你的运行环境中安装它们# 安装所需的Python库 pip install feedparser # 用于解析RSS/Atom Feed非常强大且简单 pip install requests # 用于发起HTTP请求获取Feed原始数据或文章详情 pip install Pillow # Python图像处理库用于生成最终显示的图片 pip install pytz # 处理时区 pip install schedule # 用于定时任务可选也可用cron如果你的树莓派需要直接驱动墨水屏还需要安装屏幕厂商提供的驱动库。以微雪为例通常需要克隆其GitHub仓库# 示例安装微雪墨水屏的Python驱动库具体库名请查阅对应屏幕的Wiki git clone https://github.com/waveshare/e-Paper.git cd e-Paper/RaspberryPi_JetsonNano/python sudo pip3 install .3. 核心实现步骤详解3.1 第一步管理与解析RSS订阅源首先我们需要一个地方来管理我们的RSS源列表。创建一个feeds.txt或config.yaml文件。示例 (feeds.txt):# 技术类 https://hnrss.org/frontpage https://blog.codinghorror.com/rss/ https://overreacted.io/rss.xml # 新闻类 http://rss.cnn.com/rss/edition.rss https://rsshub.app/zhihu/daily # 个人博客 https://your-favorite-blog.com/feed编写RSS抓取与解析脚本 (rss_fetcher.py):import feedparser import requests from datetime import datetime, timedelta import pytz from typing import List, Dict def fetch_feed(feed_url: str) - List[Dict]: 抓取并解析单个RSS源返回文章列表 articles [] try: # 使用feedparser解析 feed feedparser.parse(feed_url) # 检查解析是否成功 if feed.bozo: # bozo标志位表示解析可能出错 print(f警告解析Feed时可能出错 - {feed_url}) # 可以记录日志这里继续尝试处理已解析的内容 for entry in feed.entries: # 提取关键信息不同Feed结构可能不同这里做兼容处理 title entry.get(title, 无标题) # 优先获取摘要没有则尝试获取内容 summary entry.get(summary, entry.get(description, )) link entry.get(link, #) # 处理发布时间格式可能多样 published_time None if hasattr(entry, published_parsed) and entry.published_parsed: published_time datetime.fromtimestamp( time.mktime(entry.published_parsed) ).replace(tzinfopytz.UTC) elif hasattr(entry, updated_parsed) and entry.updated_parsed: published_time datetime.fromtimestamp( time.mktime(entry.updated_parsed) ).replace(tzinfopytz.UTC) else: published_time datetime.now(pytz.UTC) - timedelta(days365) # 给一个很老的日期 # 只获取最近24小时的文章避免内容过多 time_threshold datetime.now(pytz.UTC) - timedelta(hours24) if published_time time_threshold: articles.append({ title: title, summary: clean_html(summary), # 清洗HTML标签的函数 link: link, published: published_time, source: feed.feed.get(title, feed_url) # 来源名称 }) except Exception as e: print(f抓取Feed失败 {feed_url}: {e}) return articles def clean_html(raw_html: str) - str: 一个简单的HTML标签清洗函数实际项目可使用beautifulsoup4 import re cleanr re.compile(.*?) cleantext re.sub(cleanr, , raw_html) # 替换一些HTML实体 cleantext cleantext.replace(nbsp;, ).replace(amp;, ).replace(lt;, ).replace(gt;, ) return cleantext.strip() def get_all_articles(feed_urls: List[str]) - List[Dict]: 获取所有订阅源的文章 all_articles [] for url in feed_urls: if url.strip() and not url.startswith(#): # 跳过空行和注释 print(f正在抓取: {url}) articles fetch_feed(url.strip()) all_articles.extend(articles) print(f 找到 {len(articles)} 篇新文章) # 按发布时间倒序排列 all_articles.sort(keylambda x: x[published], reverseTrue) return all_articles if __name__ __main__: # 从文件读取订阅源 with open(feeds.txt, r, encodingutf-8) as f: urls [line.strip() for line in f.readlines()] articles get_all_articles(urls) print(f总共获取到 {len(articles)} 篇文章) for article in articles[:3]: # 打印前3篇看看 print(f- {article[source]}: {article[title]})3.2 第二步内容处理与格式化获取到原始文章列表后我们需要对其进行处理以便于在有限的墨水屏空间内展示。内容裁剪墨水屏分辨率有限文章摘要可能过长。需要设定一个最大字符数进行裁剪。日期格式化将UTC时间转换为本地时间并格式化为易读的字符串。生成报纸版面文本将所有信息拼接成一份连贯的“报纸”文本。编写内容处理脚本 (content_formatter.py):from datetime import datetime import pytz def format_article_for_display(article: Dict, max_summary_len: int 300) - str: 将单篇文章格式化为用于显示的文本块 # 处理时间 local_tz pytz.timezone(Asia/Shanghai) # 根据你的时区修改 local_time article[published].astimezone(local_tz) time_str local_time.strftime(%m-%d %H:%M) # 处理标题和摘要 title article[title][:50] (article[title][50:] and ...) # 标题限长 summary article[summary] if len(summary) max_summary_len: summary summary[:max_summary_len] ... # 组合成文本块 # 使用等宽字体风格的排版用-和做简单装饰 text_block f {article[source]} {time_str} {‘’ * 30} {title} {‘-’ * 20} {summary} return text_block def generate_newspaper_text(articles: List[Dict]) - str: 生成整份报纸的文本内容 newspaper_lines [] newspaper_lines.append( * 40) newspaper_lines.append( 我的每日墨水报) newspaper_lines.append(f {datetime.now().strftime(%Y-%m-%d)}) newspaper_lines.append( * 40) newspaper_lines.append() # 空行 for i, article in enumerate(articles[:15]): # 限制最多显示15篇避免过长 newspaper_lines.append(format_article_for_display(article)) if i len(articles[:15]) - 1: newspaper_lines.append(\n - * 40 \n) # 文章分隔线 newspaper_lines.append(\n * 40) newspaper_lines.append((End of Today‘s Digest)) newspaper_lines.append( * 40) return \n.join(newspaper_lines)3.3 第三步文本转图片与排版这是将纯文本渲染成墨水屏可显示图像的关键步骤。我们使用Pillow库来绘制文本。编写图片生成脚本 (text_to_image.py):from PIL import Image, ImageDraw, ImageFont import textwrap def text_to_image(text: str, output_path: str ‘newspaper.png‘, screen_width: int 800, screen_height: int 480, font_path: str ‘/usr/share/fonts/truetype/wqy/wqy-microhei.ttc‘): 将文本渲染成图片适配电子墨水屏的尺寸和特性黑白、高对比度。 # 1. 创建一张黑白图片模式‘1‘表示1位像素黑白 image Image.new(‘1‘, (screen_width, screen_height), 255) # 255为白色背景 draw ImageDraw.Draw(image) # 2. 加载字体墨水屏显示通常使用点阵字体或等宽字体更清晰 # 树莓派上可以安装文泉驿字体或者使用PIL的默认字体 try: font ImageFont.truetype(font_path, 16) font_bold ImageFont.truetype(font_path, 18) # 用于标题 except IOError: print(f警告字体文件 {font_path} 未找到使用默认字体。) font ImageFont.load_default() font_bold ImageFont.load_default() # 注意默认字体可能不支持加粗 # 3. 设置边距和行高 margin 20 line_spacing 5 y margin max_text_width screen_width - 2 * margin # 4. 按行绘制文本 for line in text.split(‘\n‘): # 处理过长的行自动换行 if draw.textlength(line, fontfont) max_text_width: # 使用textwrap进行单词级别的换行英文友好 wrapped_lines textwrap.wrap(line, widthint(max_text_width / 8)) # 估算字符数 for wrapped_line in wrapped_lines: if y screen_height - margin - 20: # 留出底部边距 draw.text((margin, y), wrapped_line, fontfont, fill0) # fill0为黑色 y font.size line_spacing else: # 判断是否为标题行简单的启发式规则 current_font font_bold if line.strip().startswith(‘我的每日墨水报‘) or ‘‘ in line else font if y screen_height - margin - 20: draw.text((margin, y), line, fontcurrent_font, fill0) # 标题行后多空一点 y current_font.size (line_spacing * 2 if current_font font_bold else line_spacing) # 5. 保存图片 image.save(output_path, ‘PNG‘) print(f报纸图片已生成: {output_path}) return output_path3.4 第四步推送图片到电子墨水屏最后一步是将生成的图片发送到墨水屏设备并刷新显示。这部分代码高度依赖于你使用的硬件和驱动库。以微雪Waveshare墨水屏为例 (epaper_display.py):import sys import os sys.path.append(‘/home/pi/e-Paper/RaspberryPi_JetsonNano/python‘) # 添加驱动库路径 from waveshare_epd import epd7in5_V2 # 请根据你的具体屏幕型号导入对应的驱动类 from PIL import Image import time def display_image(image_path: str): 在连接的墨水屏上显示图片 try: print(‘初始化墨水屏...‘) # 初始化屏幕对象 epd epd7in5_V2.EPD() epd.init() # 清屏避免残影 epd.Clear() print(‘加载图片...‘) # 打开图片并转换为屏幕支持的格式通常是1位黑白 image Image.open(image_path) # 有些屏幕需要将图片旋转到正确的方向 # image image.rotate(180) # 确保图片尺寸与屏幕分辨率匹配 # image image.resize((epd.width, epd.height)) print(‘显示图片...‘) # 将图片缓冲到屏幕内存 epd.display(epd.getbuffer(image)) time.sleep(2) print(‘进入睡眠模式...‘) # 显示完成后让屏幕进入睡眠模式以省电 epd.sleep() except IOError as e: print(f‘IO错误: {e}‘) except KeyboardInterrupt: print(‘程序被中断‘) epd7in5_V2.epdconfig.module_exit() exit()3.5 第五步整合与自动化创建一个主程序 (main.py)将以上所有步骤串联起来并设置定时任务。import schedule import time from rss_fetcher import get_all_articles from content_formatter import generate_newspaper_text from text_to_image import text_to_image from epaper_display import display_image import logging logging.basicConfig(levellogging.INFO, format‘%(asctime)s - %(levelname)s - %(message)s‘) def job(): 定时执行的任务 logging.info(‘开始生成今日墨水报...‘) try: # 1. 获取文章 with open(‘feeds.txt‘, ‘r‘) as f: feed_urls [line.strip() for line in f if line.strip() and not line.startswith(‘#‘)] articles get_all_articles(feed_urls) if not articles: logging.warning(‘未获取到新文章。‘) return # 2. 生成报纸文本 newspaper_text generate_newspaper_text(articles) # 3. 文本转图片 image_path text_to_image(newspaper_text, ‘daily_newspaper.png‘) # 4. 推送到墨水屏显示 display_image(image_path) logging.info(‘今日墨水报已更新并显示‘) except Exception as e: logging.error(f‘生成或显示报纸时出错: {e}‘, exc_infoTrue) if __name__ ‘__main__‘: logging.info(‘墨水报服务启动。‘) # 立即运行一次 job() # 设置定时任务例如每天早8点和晚8点各更新一次 schedule.every().day.at(‘08:00‘).do(job) schedule.every().day.at(‘20:00‘).do(job) # 也可以每小时运行一次用于测试 # schedule.every().hour.do(job) while True: schedule.run_pending() time.sleep(60) # 每分钟检查一次为了让程序在树莓派开机时自动运行可以将其设置为一个系统服务。创建系统服务文件 (/etc/systemd/system/ink-newspaper.service):[Unit] DescriptionInk Newspaper Service Afternetwork.target [Service] Typesimple Userpi WorkingDirectory/home/pi/ink_newspaper ExecStart/usr/bin/python3 /home/pi/ink_newspaper/main.py Restarton-failure RestartSec10 [Install] WantedBymulti-user.target然后启用并启动服务sudo systemctl daemon-reload sudo systemctl enable ink-newspaper.service sudo systemctl start ink-newspaper.service # 查看状态 sudo systemctl status ink-newspaper.service4. 进阶优化与功能扩展基础版本完成后你可以根据需求进行以下优化4.1 内容增强全文抓取许多RSS只提供摘要。可以使用requests和BeautifulSoup库根据文章链接抓取并提取正文。图片处理简单的文本转图片无法显示网络图片。可以尝试抓取文章首图并将其转换为墨水屏可用的黑白抖动图然后与文本图片拼接。内容过滤根据关键词、来源或分类过滤掉不感兴趣的文章。摘要生成对于过长的文章可以使用本地NLP库如jieba中文分词sumy摘要提取或调用大语言模型的API需注意网络和成本来生成更精炼的摘要。4.2 显示优化更美观的排版使用更复杂的Pillow绘图功能实现多栏排版、字体混排、添加简单图标或边框。局部刷新如果屏幕支持局部刷新可以只更新变化的区域如日期、头条而不是全屏刷新速度更快且减少残影。多页显示如果内容太多可以生成多张图片并通过屏幕上的物理按钮或定时自动翻页。4.3 架构优化云端部署将RSS抓取、内容处理和图片生成的服务部署到云服务器如腾讯云轻量应用服务器树莓派只作为纯粹的显示终端定期从云端拉取图片并刷新。这样内容源管理和更新会更灵活。使用现成API如果不想自己解析RSS可以使用像Feedbin、Inoreader等服务的API来获取已聚合和标准化后的文章列表。这需要订阅其付费服务。添加Web管理界面使用Flask或FastAPI搭建一个简单的Web界面方便在浏览器中添加/删除订阅源、手动触发更新等。5. 常见问题与排查思路在实现过程中你可能会遇到以下问题问题现象可能原因排查与解决思路程序报错ModuleNotFoundErrorPython依赖库未安装。使用pip list检查feedparser,Pillow等是否安装。在树莓派上注意区分pip和pip3。抓取RSS失败返回403或超时目标网站有反爬机制网络连接问题。1. 在请求头中添加User-Agent模拟浏览器。2. 尝试使用代理需合法合规。3. 检查网络连通性ping rss源域名。feedparser解析时间出错RSS源的时间格式非标准。在fetch_feed函数中添加更健壮的时间解析逻辑或直接使用entry.get(‘published‘)字符串或忽略时间过滤。生成的图片中文显示为方框系统缺少中文字体。在树莓派上安装中文字体sudo apt install fonts-wqy-microhei。在text_to_image函数中指定正确的字体路径。墨水屏不显示或花屏接线错误驱动不匹配初始化失败。1. 对照屏幕Wiki再三检查接线。2. 确认导入的驱动类与屏幕型号完全一致。3. 检查树莓派SPI/I2C接口是否已启用 (sudo raspi-config)。4. 尝试运行厂商提供的示例程序先确保硬件和基础驱动正常。图片显示有严重残影刷新方式不对。全屏刷新时在display前后先调用epd.Clear()。如果支持尝试使用局部刷新API。遵循厂商推荐的刷新流程。定时任务不执行schedule库在后台运行异常系统时间问题。1. 使用更稳定的cron代替schedule。2. 检查系统时区设置timedatectl。3. 查看程序日志sudo journalctl -u ink-newspaper.service。内容过多显示不全屏幕分辨率有限文章数量太多。在generate_newspaper_text函数中限制显示的文章数量如15篇。对摘要进行更严格的长度裁剪。6. 最佳实践与工程建议订阅源管理将订阅源列表放在独立的配置文件如config.yaml中与代码分离。对订阅源进行分类科技、新闻、博客并在生成报纸时按类别分块显示。定期审查和更新订阅源移除已失效或不再感兴趣的源。错误处理与日志对网络请求、文件IO、屏幕操作等可能失败的环节进行try-except捕获。使用Python的logging模块记录信息、警告和错误日志便于后期排查。可以考虑添加邮件或通知服务当连续多次抓取失败时发送警报。资源与性能树莓派内存有限避免一次性加载过多文章或过大的图片到内存。图片生成是比较耗CPU的操作合理安排定时任务间隔避免过于频繁。考虑将生成的图片缓存起来如果内容没有更新则直接使用缓存减少不必要的处理。代码可维护性将不同功能的代码分离到不同的模块文件中如抓取、解析、渲染、驱动。使用函数和类来组织代码提高可读性和复用性。在关键函数和配置项旁添加清晰的注释。硬件保护电子墨水屏非常脆弱避免物理挤压。严格按照数据手册操作避免在刷新过程中断电。为你的树莓派和屏幕制作或购买一个合适的外壳。通过这个项目你不仅得到了一个独一无二的护眼阅读设备更实践了从数据获取、处理到硬件交互的完整物联网IoT应用流程。你可以在此基础上继续扩展例如加入天气预报、日程提醒、乃至简单的智能家居控制面板让它成为一个真正的桌面信息中心。