代码:
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const express = require('express');
const app = express();
const app = express();
const PORT = 8980;
// CORS 设置
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'DELETE'],
allowedHeaders: ['Content-Type']
}));
// 定义 RSS 链接
const rssUrls = [
"https://www.v2ex.com/feed/create.xml",
//更多rss源
];
// 从 CDATA 中提取内容
function extractCdataContent(str) {
const match = str.match(/<!\[CDATA\[(.*?)\]\]>/s);
return match ? match[1] : str;
}
// 转义特殊字符的函数
function escapeXML(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 定义合并 RSS 的函数
async function mergeRss() {
const rssItems = [];
// 异步获取所有 RSS 数据
await Promise.all(rssUrls.map(async (url) => {
try {
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
}
});
const data = response.data;
// 提取源标题
const channelTitleMatch = data.match(/<title>(.*?)<\/title>/);
const sourceTitle = channelTitleMatch ? channelTitleMatch[1] : '未知来源';
// 检查是否为 Atom 格式
const isAtom = data.includes('<feed') || data.includes('<entry');
// 手动解析 XML 数据
const items = isAtom ? data.match(/<entry>(.*?)<\/entry>/gs) : data.match(/<item>(.*?)<\/item>/gs);
if (items) {
items.forEach(item => {
const titleMatch = item.match(/<title>(.*?)<\/title>/);
const linkMatch = isAtom ? item.match(/<link rel="alternate" href="(.*?)"/) : item.match(/<link>(.*?)<\/link>/);
const pubDateMatch = isAtom ? item.match(/<updated>(.*?)<\/updated>/) : item.match(/<pubDate>(.*?)<\/pubDate>/);
const descriptionMatch = item.match(/<description>(.*?)<\/description>/s);
const contentEncodedMatch = item.match(/<content:encoded>(.*?)<\/content:encoded>/s);
const contentMatch = item.match(/<content.*?>(.*?)<\/content>/s) ||
item.match(/<summary.*?>(.*?)<\/summary>/s) ||
item.match(/<description>(.*?)<\/description>/s);
const title = titleMatch ? extractCdataContent(titleMatch[1]) : '';
const link = linkMatch ? extractCdataContent(linkMatch[1]) : '';
const pubDate = pubDateMatch ? pubDateMatch[1] : '';
const description = descriptionMatch ? extractCdataContent(descriptionMatch[1]) : '';
const content = contentEncodedMatch ? extractCdataContent(contentEncodedMatch[1]) :
contentMatch ? extractCdataContent(contentMatch[1]) : '';
if (title && link) {
const rssItem = {
title,
link,
pubDate: new Date(pubDate).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
sourceTitle,
description,
content
};
rssItems.push(rssItem);
}
});
}
} catch (error) {
console.error(`Error fetching RSS from ${url}:`, error.message);
}
}));
// 过滤和排序 RSS 条目
rssItems.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate));
// 生成合并后的 RSS XML
const mergedRssXml = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>订阅聚合</title>
<description>聚合订阅更新</description>
${rssItems.map(item => `
<item>
<title>${escapeXML(item.title)}</title>
<link>${escapeXML(item.link)}</link>
<description><![CDATA[${item.description || ''}]]></description>
<pubDate>${new Date(item.pubDate).toUTCString()}</pubDate>
<source>${escapeXML(item.sourceTitle)}</source>
<content:encoded><![CDATA[${item.content || ''}]]></content:encoded>
</item>
`).join('')}
</channel>
</rss>`;
// 写入合并后的 RSS 文件
const storagePath = process.cwd(); // 假设当前目录为存储路径
const rssMergePath = path.join(storagePath, 'rssmerge.xml');
fs.writeFileSync(rssMergePath, mergedRssXml, 'utf8');
// 生成 JSON 文件
const jsonMergePath = path.join(storagePath, 'rssmerge.json');
try {
fs.writeFileSync(jsonMergePath, JSON.stringify(rssItems.map(item => ({
title: item.title,
link: item.link,
pubDate: new Date(item.pubDate).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
source: item.sourceTitle,
...(item.description && { description: item.description }),
...(item.content && { content: item.content })
})), null, 2), 'utf8');
console.log('JSON 文件生成成功:', jsonMergePath);
} catch (error) {
console.error(`写入 JSON 文件时出错: ${error.message}`);
}
// 读取生成的文件内容
const fileContent = fs.readFileSync(rssMergePath, 'utf8');
return {
filePath: rssMergePath,
jsonPath: jsonMergePath,
content: fileContent,
itemCount: rssItems.length
};
}
// 定义合并 RSS 的路由
app.get('/api/rss', async (req, res) => {
try {
const result = await mergeRss();
runHugo(); // 在合并后执行 hugo 命令
res.json({
message: 'RSS 合并成功',
filePath: result.filePath,
jsonPath: result.jsonPath,
content: result.content,
itemCount: result.itemCount
});
} catch (error) {
console.error('合并 RSS 时出错:', error);
res.status(500).json({ error: '合并 RSS 时出错' });
}
});
// CORS 代理路由
app.get('/api/mergerss', (req, res) => {
const rssFilePath = path.join(storagePath, 'rssmerge.xml');
// 设置 CORS 头部
res.setHeader('Access-Control-Allow-Origin', '*'); // 允许所有域访问
res.setHeader('Content-Type', 'application/xml'); // 设置内容类型为 XML
// 读取并返回 RSS 文件内容
fs.readFile(rssFilePath, 'utf8', (err, data) => {
if (err) {
console.error('读取 RSS 文件时出错:', err);
return res.status(500).send('读取 RSS 文件时出错');
}
res.send(data);
});
});
// 定时任务每十分钟更新 RSS 合并
setInterval(async () => {
await mergeRss();
runHugo(); // 在定时更新后执行 hugo 命令
}, 600000); // 每十分钟执行一次
// 启动 Express 服务器
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
});
在上面示例中/api/rss请求控制合并,/api/mergerss请求跨域读取合并RSS文件,合并处理后的rss文件分别生成xml格式和json格式,storagePath后面为指定的存储文件路径及文件名,最终运行在3000端口