目的:
《经济学人》是一家国际知名的周刊,提供了丰富的RSS订阅服务,获取最新的全球新闻和深度报道。通过RSS订阅,可以方便地在RSS阅读器上接收到文章更新,无需直接访问网站。我用页面来快速浏览来自 The Economist (经济学人)新闻
近似项目:
<Project-1 rss_to_csv> Python Coding Flask应用: 读取华尔街日报RSS 在浏览器中显示 在QNAP Container/docker 运行
对上面的代码做了如下修改:
主程序 app.py
- 读取系统目录,解决Windows 与 Linux系统目录不同的问题。
- 使用 apscheduler 任务调度器代替 Time 时钟
- 加入检查CSV文件存在,提高启动速度
CSV生成程序 csv2rss.py
- 读取系统目录,解决Windows 与 Linux系统目录不同的问题。
- 使用 rss_feeds.txt 文件替代写入RSS记录行
- 加入检测CSV数据是否为空
数据源
The economist RSS Feeds
抓取RSS links代码 fetch_rss.py
WSJ才几行,这里有32行,实在是太多了。写段代码,也许以后还能用到。
import os
import requests
from bs4 import BeautifulSoup
def fetch_rss_links_with_descriptions(url):
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
rss_feeds = {}
potential_links = soup.find_all('a', href=True)
for link in potential_links:
if 'rss' in link['href']:
full_url = "https://www.economist.com" + link['href'] if link['href'].startswith('/') else link['href']
description = link['href'].split('/')[-2].replace('-', ' ').capitalize()
rss_feeds[full_url] = description
return rss_feeds
def save_links_to_file(rss_feeds, filename):
print("Saving to file...") # Debug statement
with open(filename, 'w') as file:
if rss_feeds:
for url, description in rss_feeds.items():
file_entry = f"#{description}: \n{url}\n"
file.write(file_entry)
print(f"Writing to file: {file_entry}") # Debug output
else:
print("No data to write.") # Debug if no data
url = 'https://www.economist.com/rss'
rss_links_with_descriptions = fetch_rss_links_with_descriptions(url)
if rss_links_with_descriptions:
save_links_to_file(rss_links_with_descriptions, 'rss_feeds.txt')
print("RSS feeds have been saved to 'rss_feeds.txt'.")
else:
print("No RSS feeds found.")
# After your existing save functionality
file_path = 'rss_feeds.txt'
file_stats = os.stat(file_path)
print(f"File size: {file_stats.st_size} bytes")
有个bug, 需要手动注释掉最后一行,见上面。需要添加"#" (引号中的#)
应用简介
基于 Flask 框架开发的网页应用,用于处理和展示 RSS 源的内容。通过自动抓取特定的 RSS 源,它将新闻文章转换为 CSV 文件并呈现在网页上。应用程序集成了多个 Python 库,如 pandas
、feedparser
和 googletrans
,来处理数据和翻译内容。
RSS订阅处理程序概览
主要功能:
- 获取RSS数据:从指定的RSS链接中下载XML格式的数据。
- 解析XML:使用解析库(如
xml.etree.ElementTree
)读取并解析XML中的内容。 - 数据处理:提取所需的信息,如标题、链接、发布日期等。
- 数据存储:将提取的数据保存到更方便使用的格式,如CSV文件。
主要组件
2个程序文件
-
主程序 (app.py)
-
rss2csv (rss2csv.py)
核心功能
-
RSS 内容抓取与转换: 通过rss2csv.py脚本,该应用程序会自动抓取指定的 RSS 源内容,并将其转换为 CSV 文件。每次运行时,都会过滤重复的内容,并且按时间顺序对数据进行排序。最终的 CSV 文件会存储在指定目录中。
-
多源支持: 该应用程序支持多个 RSS 源,从rss_feeds.txt中读取。不同来源的内容可以汇总到同一 CSV 文件中,每3小时生成新文件。
-
自动刷新与本地展示: 应用程序通过 Flask 服务器在本地运行,用户可以通过浏览器访问网页来查看最新的 RSS 源内容。网页每隔3小时会自动刷新,确保用户看到的内容是最新的。此外,应用程序支持高亮显示特定关键字,例如 "China" 或 "Chinese",以方便用户快速定位重要内容。
-
本地时间显示与时区转换: 应用程序会将新闻发布的时间从纽约时间转换为用户电脑的本地时间,这样用户可以更直观地了解每篇文章的发布时间。
-
CSV 文件管理: 每天会生成一个新的 CSV 文件,应用程序会自动删除 7 天之前的文件,以确保目录不会被过时文件占用太多空间。
目录结构
rss2csv/ # 项目根目录
│
├── Dockerfile # Docker 配置文件
├── requirements.txt # Python 依赖库列表
├── rss2csv.py # RSS 抓取和CSV生成的核心脚本
├── app.py # Flask 应用程序主文件,负责展示RSS内容
│
└── csv_files_Economist/ # 存储生成的CSV文件的目录
└── Eco_YYYY-MM-DD-hhmm.csv
复制全部代码,配置所需环境,放在对应的目录下面,即可使用。
CODING 已经调整为在 Linux 与 Windows 都可以执行,注意存放路径。
完整代码
app.py
import os
import pandas as pd
from flask import Flask, render_template_string, jsonify
from datetime import datetime, timedelta
import subprocess
from apscheduler.schedulers.background import BackgroundScheduler
import atexit
# 初始化 Flask 应用
app = Flask(__name__)
# 设置 CSV 文件的目录
current_directory = os.path.dirname(os.path.abspath(__file__))
csv_directory = os.path.join(current_directory, 'csv_files_Economist')
rss_script_path = os.path.join(current_directory, 'rss2csv.py')
os.makedirs(csv_directory, exist_ok=True)
# 后台运行 rss2csv.py
def run_rss_script():
print("开始运行 rss2csv.py")
subprocess.run(['python', rss_script_path])
cleanup_old_files() # 每次运行完脚本后清理旧文件
print("rss2csv.py 运行完成")
# 清理超过7天的 CSV 文件
def cleanup_old_files():
now = datetime.now()
cutoff = now - timedelta(days=7) # 定义文件保留天数
for filename in os.listdir(csv_directory):
if filename.startswith("Eco_") and filename.endswith(".csv"):
file_path = os.path.join(csv_directory, filename)
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
if file_time < cutoff:
os.remove(file_path)
print(f"删除旧文件: {filename}")
# 初始化定时任务调度器
scheduler = BackgroundScheduler()
scheduler.add_job(func=run_rss_script, trigger='interval', hours=3) # 每3小时运行一次
scheduler.start()
# 在程序退出时关闭调度器
atexit.register(lambda: scheduler.shutdown())
# 在应用启动前运行一次 rss2csv.py
# 检查是否存在已有的 CSV 文件
csv_files = [f for f in os.listdir(csv_directory) if f.startswith("Eco_") and f.endswith(".csv")]
if not csv_files:
# 如果不存在 CSV 文件,先运行一次
run_rss_script()
else:
print("存在已有的 CSV 文件,跳过初始运行")
# 自定义 HTML 模板,设置标题为 "The Economist"
HTML_TEMPLATE = """
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>The Economist</title> <!-- 设置网页标题 -->
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.5;
color: #333;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.file-info {
font-weight: bold;
font-size: 18px;
}
.refresh-btn-container {
display: flex;
align-items: center;
}
.refresh-btn {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
}
.refresh-btn:hover {
background-color: #45a049;
}
.countdown {
margin-left: 10px;
font-size: 14px;
color: #555;
}
.current-time {
font-size: 16px;
font-weight: bold;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th, td {
padding: 8px 12px;
border: 1px solid #ddd;
text-align: left;
white-space: pre-line; /* 支持换行显示 */
}
th {
background-color: #f4f4f4;
font-weight: bold;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
.highlight {
background-color: yellow;
}
.url-cell {
max-width: 100px; /* 将 URL 列的最大宽度设置为100px */
word-wrap: break-word; /* 强制换行 */
overflow-wrap: break-word;
}
.url-container {
display: inline-flex; /* 确保 URL 和按钮在同一行 */
align-items: center;
}
.copy-btn {
margin-left: 10px;
padding: 5px 10px;
font-size: 12px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="header">
<div class="file-info">文件名: {{ file_name }}</div>
<div class="refresh-btn-container">
<button class="refresh-btn" onclick="location.reload();">刷新页面</button>
<div class="countdown">下次刷新倒计时: <span id="countdown">600</span> 秒</div>
</div>
<div class="current-time">当前时间: <span id="currentTime"></span></div>
</div>
<table>
<thead>
<tr>
<th>发布时间</th>
<th>URL</th>
<th>标题</th>
<th>摘要</th>
</tr>
</thead>
<tbody>
{% for row in data %}
<tr class="{% if 'China' in row[3] or 'china' in row[3] or 'chinese' in row[3] or 'Chinese' in row[3] or 'China' in row[2] or 'china' in row[2] or 'chinese' in row[2] or 'Chinese' in row[2] %}highlight{% endif %}">
<td>{{ row[0] }}</td>
<td class="url-cell">
<div class="url-container">
<a href="{{ row[1] }}" target="_blank">URL</a> <!-- 使用 URL 替代完整链接文本 -->
<button class="copy-btn" onclick="copyToClipboard('{{ row[1] }}')">复制</button>
</div>
</td>
<td>{{ row[2] }}</td>
<td>{{ row[3] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
function updateTime() {
const now = new Date();
const timeString = now.toLocaleTimeString();
document.getElementById('currentTime').textContent = timeString;
}
setInterval(updateTime, 1000); // 每秒更新一次时间
function copyToClipboard(text) {
const tempInput = document.createElement('input');
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
alert('URL 已复制到剪贴板');
}
function startCountdown(duration) {
let timer = duration, seconds;
const countdownElement = document.getElementById('countdown');
setInterval(() => {
seconds = parseInt(timer, 10);
countdownElement.textContent = seconds;
if (--timer < 0) {
location.reload();
}
}, 1000);
}
// 自动刷新逻辑
let lastModified = '{{ last_modified }}';
function checkForUpdates() {
fetch('/get_last_modified')
.then(response => response.json())
.then(data => {
if (data.last_modified !== lastModified) {
// 如果文件已更新,刷新页面
location.reload();
}
})
.catch(error => console.error('Error:', error));
}
// 每隔60秒检查一次
setInterval(checkForUpdates, 60000);
window.onload = () => {
startCountdown(600); // 设定倒计时时间为600秒
};
</script>
</body>
</html>
"""
@app.route('/')
def home():
csv_files = [f for f in os.listdir(csv_directory) if f.startswith("Eco_") and f.endswith(".csv")]
if not csv_files:
return "正在生成csv数据,请稍后刷新页面。"
try:
csv_files.sort(reverse=True)
latest_csv_file = os.path.join(csv_directory, csv_files[0])
df = pd.read_csv(latest_csv_file)
# 获取文件的最后修改时间
last_modified = os.path.getmtime(latest_csv_file)
except Exception as e:
return f"读取 CSV 文件时出错:{e}"
file_name = os.path.basename(latest_csv_file)
return render_template_string(HTML_TEMPLATE, data=df.values, file_name=file_name, last_modified=last_modified)
@app.route('/get_last_modified')
def get_last_modified():
csv_files = [f for f in os.listdir(csv_directory) if f.startswith("Eco_") and f.endswith(".csv")]
if not csv_files:
return jsonify({'last_modified': None})
csv_files.sort(reverse=True)
latest_csv_file = os.path.join(csv_directory, csv_files[0])
last_modified = os.path.getmtime(latest_csv_file)
return jsonify({'last_modified': str(last_modified)})
# 启动应用
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
代码说明
-
引入必要的模块:
APScheduler
用于定期调度任务。atexit
确保应用退出时,调度器能够正常关闭。
-
设置文件目录和路径:
- 使用
os.path
来构建与平台无关的路径。 csv_directory
是存放 CSV 文件的目录。rss_script_path
是rss2csv.py
脚本的路径。
- 使用
-
定义
run_rss_script
函数:- 使用
subprocess.run
来运行rss2csv.py
。 - 运行完后调用
cleanup_old_files
清理超过 7 天的旧文件。
- 使用
-
初始化定时任务调度器:
- 使用
BackgroundScheduler
来调度任务。 - 每隔 3 小时 运行一次
run_rss_script
。
- 使用
-
在应用启动前运行一次
rss2csv.py
:- 检查是否存在已有的 CSV 文件。
- 如果不存在,运行一次
run_rss_script
,以确保有数据可用。
-
定义 Flask 路由和视图函数:
/
路由用于显示最新的数据。/get_last_modified
路由用于前端检查数据是否更新。
-
HTML 模板中的自动刷新逻辑:
- 前端使用 JavaScript 定期(每 60 秒)请求
/get_last_modified
,检查数据是否有更新。 - 如果检测到数据更新,自动刷新页面。
- 前端使用 JavaScript 定期(每 60 秒)请求
-
异常处理:
- 在读取 CSV 文件时,使用
try-except
块捕获可能的异常,防止程序崩溃。
- 在读取 CSV 文件时,使用
注意事项
定时任务调度器:
如果需要调整 rss2csv.py
的运行频率,可以修改以下代码: 现在是3小时
scheduler.add_job(func=run_rss_script, trigger='interval', hours=3) # 修改 hours 参数
数据更新检测:
前端每隔 300秒检查一次数据更新,如果需要调整频率,修改以下代码:
// 每隔300秒检查一次
setInterval(checkForUpdates, 300000); // 修改 300000(毫秒)
倒计时刷新页面:
页面设定了 600 秒(10 分钟)的倒计时,倒计时结束后会自动刷新页面。如果需要调整,修改以下代码:
startCountdown(600); // 修改 600 为所需的秒数
rss2csv.py
import os
import requests
from bs4 import BeautifulSoup
def fetch_rss_links_with_descriptions(url):
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
rss_feeds = {}
potential_links = soup.find_all('a', href=True)
for link in potential_links:
if 'rss' in link['href']:
full_url = "https://www.economist.com" + link['href'] if link['href'].startswith('/') else link['href']
description = link['href'].split('/')[-2].replace('-', ' ').capitalize()
rss_feeds[full_url] = description
return rss_feeds
def save_links_to_file(rss_feeds, filename):
print("Saving to file...") # Debug statement
with open(filename, 'w') as file:
if rss_feeds:
for url, description in rss_feeds.items():
file_entry = f"#{description}: \n{url}\n"
file.write(file_entry)
print(f"Writing to file: {file_entry}") # Debug output
else:
print("No data to write.") # Debug if no data
url = 'https://www.economist.com/rss'
rss_links_with_descriptions = fetch_rss_links_with_descriptions(url)
if rss_links_with_descriptions:
save_links_to_file(rss_links_with_descriptions, 'rss_feeds.txt')
print("RSS feeds have been saved to 'rss_feeds.txt'.")
else:
print("No RSS feeds found.")
# After your existing save functionality
file_path = 'rss_feeds.txt'
file_stats = os.stat(file_path)
print(f"File size: {file_stats.st_size} bytes")
代码说明
主要作用是从《经济学人》(The Economist)的 RSS 页面中提取所有 RSS 链接及其对应的描述,并将它们保存到一个名为 rss_feeds.txt 的文件中。最后,它还会输出生成的文件大小。
主要事项
库
- 模块os:用于与操作系统进行交互,这里用于获取文件的大小信息。
- requests·模块:用于发送 HTTP 请求,获取网页内容。
- BeautifulSoup 模块:来自
bs4
库,用于解析 HTML 内容,方便提取所需的信息。
定义函数 fetch_rss_links_with_descriptions(url)
功能:
- 从指定的 URL 获取网页内容。
- 解析网页,找到所有包含 href 属性的 <a> 标签。
- 筛选出 href 属性中包含 'rss' 的链接,认为它们是 RSS 链接。
- 构建完整的 RSS 链接,并提取对应的描述信息。
- 将 RSS 链接和描述以字典的形式返回。
定义函数 save_links_to_file(rss_feeds, filename):
- 将获取到的 RSS 链接和描述信息保存到指定的文件中。
运行结果
#The world this week:
https://www.economist.com/the-world-this-week/rss.xml
#Leaders:
https://www.economist.com/leaders/rss.xml
#Briefings:
https://www.economist.com/briefings/rss.xml
#...(其他 RSS 链接)
浏览器中的结果
n/a
NAS Container 部署
准备必要文件 Dockerfile and requirements.txt
Dockerfile
# 使用官方的 Python 3.12 作为基础镜像
FROM python:3.12-slim
# 设置工作目录
WORKDIR /app
# 将 requirements.txt 文件复制到容器中
COPY requirements.txt .
# 安装依赖库
RUN pip install --no-cache-dir -r requirements.txt
# 将当前目录的所有文件复制到容器中的 /app 目录
COPY . .
# 暴露 Flask 默认端口
EXPOSE 9003
# 设置环境变量让 Flask 以生产模式运行
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
# 启动 Flask 应用
CMD ["flask", "run", "--host=0.0.0.0", "--port=9003"]
requirements.txt
Flask
pandas
APScheduler
requests
beautifulsoup4
feedparser
deep_translator
docker-comose.yml (可以忽略,下面的操作用不到)
version: '3.8'
services:
flask-app:
build: .
ports:
- "9003:9003"
volumes:
- .:/app
environment:
FLASK_ENV: development
networks:
- app-network
networks:
app-network:
driver: bridge
在NAS上运行Docker指令
连接并登录NAS后,在Dockerfile文件的目录下执行:
1. 创建Dockers 的 Image,名字为 rss2economist
docker build -t rss2economist . <- 这里有个点
2. 创建一个Container (发现 NAS docker 不能用指定的端口号,借个壳)
可以用 docker commit, 我是在NAS container app 里操作,看小视频:
<Project-5 rss2csv>用视频
在视频里创建了一个container: rss2economist-practice, 并看到它的端口是
Port forwarding:32810 → 9003/TCP
并不是期待的 9003->9003
这样打想访问网页,就需要 NAS_IP:32810, 而不是 NAS_IP:9003
我的临时解决办法是:
命令: docker run -p 9003:9003 -td <container name> 如下图
这时的 Port Forwarding 就是 9003 to 9003
浏览器可以从9003打开页面
总结
程序会在当前目录下运行,并创建CSV文件的目录。但 fetch_rss.py 没有定义当前目录,如果是在 Visual Studio Code 中调试运行,其文件可能不是py所在的目录。
RSS 源太多问题
有32个,每次运行都是巨大的时间,最好只使用几个。在测试时,只用了 China 与 Asia 等待已经是数分钟。
如果需要更多的组,在 windows 下面编辑rss_feeds.txt 把注释"#"在 https:// 前去掉或添加。再复制到 container中,docker 命令如下:
docker cp rss_feeds.txt rss2economist:/app
# /app是代码指定的目录, 如果 不理会 3小时内会开始生成新的 csv file. 或重启 container
flask 使用的端口是5000,跟据自己需要去修改。
有时间,去修改一下页面格式。
docker部分,参考 文章开头提到的 <Project 1>