在青龙面板中自动化网页抓取与邮件通知:JS、Jina AI 代理与增量更新
利用 JavaScript 脚本、r.jina.ai 代理和 nodemailer 实现高效的网页监控与通知流程。
您希望在青龙面板(QingLong Panel)环境中使用 JavaScript (JS) 脚本,结合 https://r.jina.ai/ 代理服务,抓取指定网页 https://www.boringreport.org/app 及其子页面,并实现增量更新检测,最后通过 163 邮箱发送通知。这是一个典型的自动化任务,结合了网页抓取、内容比较和消息推送。
核心要点
- 青龙面板自动化: 青龙面板提供了一个强大的平台,用于定时调度和管理自动化脚本,如您所需的 JavaScript 任务。
- Jina AI 代理抓取: 使用
r.jina.ai/<URL>服务可以简化网页内容的获取,将其转换为更易于处理的格式,同时充当代理。 - 增量更新与通知: 通过比较本次抓取内容与上次记录,实现仅在内容变化时触发 163 邮箱通知,避免重复信息干扰。
准备工作:青龙面板环境
首先,确保您的青龙面板已经成功安装并运行。通常推荐使用 Docker 进行安装,可以参考官方文档或相关教程(如 CSDN、博客园上的指南)。
安装青龙面板 (Docker 示例)
如果您尚未安装,可以使用类似以下的 Docker 命令进行安装:
docker run -dit \
-v $PWD/ql/config:/ql/config \
-v $PWD/ql/log:/ql/log \
-v $PWD/ql/db:/ql/db \
-v $PWD/ql/scripts:/ql/scripts \
-v $PWD/ql/data:/ql/data \ # 确保有持久化数据卷
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
安装完成后,通过浏览器访问 `http://<您的服务器IP>:5700` 登录面板。
创建与配置 JS 脚本
登录青龙面板后:
- 导航至左侧菜单的 “脚本管理”。
- 点击右上角的 “新建脚本” 按钮。
- 为脚本命名(例如 `web_monitor.js`),并选择语言为 `JavaScript`。
- 在编辑器中粘贴或编写您的 JS 代码。
构建核心 JavaScript 脚本
脚本的核心逻辑包括配置、抓取、比较和通知。我们将使用流行的 Node.js 库来完成这些任务。
1. 添加依赖
您的脚本需要以下 Node.js 模块:
axios: 用于发送 HTTP 请求,方便地处理网络抓取和代理设置。nodemailer: 用于通过 SMTP 发送邮件。fs(Node.js 内置): 用于读写文件,实现增量更新的状态持久化。- (可选)
cheerio: 如果需要解析 HTML 以提取子页面链接,这个库会很有用。
在青龙面板中,您可以通过以下几种方式管理依赖:
- 脚本内注释声明: 在 JS 脚本的顶部添加特殊注释:
青龙面板会自动识别并尝试安装这些依赖。/** * @requires axios * @requires nodemailer */ - 依赖管理界面: 导航至 “依赖管理” -> “NodeJs”,点击 “新建依赖”,然后输入依赖名称(如 `axios`、`nodemailer`)并确认安装。
2. 配置环境变量
为了安全和灵活性,将敏感信息(如邮箱凭证)和可配置项(如目标 URL)存储在青龙面板的环境变量中。
- 导航至 “环境变量” 页面。
- 点击 “新建变量”。
- 添加以下变量(名称可自定义,但需与脚本中保持一致):
TARGET_URL: 值 `https://www.boringreport.org/app` (注意:此 URL 当前可能返回 404,请替换为有效的 URL)EMAIL_USER: 值 `your_email@163.com` (您的 163 邮箱地址)EMAIL_PASS: 值 `your_163_smtp_authorization_code` (您的 163 邮箱 SMTP 授权码,不是登录密码)EMAIL_TO: 值 `recipient_email@example.com` (接收通知的邮箱地址)
在 JS 脚本中,通过 process.env.VARIABLE_NAME 来访问这些变量。
3. 抓取网页内容 (使用 Jina AI 代理)
我们将使用 `axios` 库通过 `r.jina.ai` 代理来抓取目标网页。
重要提示:目标 URL 有效性
提供的目标 URL https://www.boringreport.org/app 在测试时返回 404 (Not Found) 错误。这意味着该页面可能不存在或无法访问。请务必将其替换为您确认有效的、想要监控的网页 URL。脚本中的错误处理逻辑会捕获此类问题。
抓取主页面
使用 `axios.get` 方法,并将目标 URL 构造成 `https://r.jina.ai/<原始目标URL>` 的形式。
const axios = require('axios');
async function fetchContent(targetUrl) {
const proxyUrl = `https://r.jina.ai/${targetUrl}`;
console.log(`尝试通过代理抓取: ${proxyUrl}`);
try {
const response = await axios.get(proxyUrl, { timeout: 30000 }); // 设置 30 秒超时
console.log('网页内容抓取成功。');
return response.data; // Jina AI 通常返回处理后的文本内容
} catch (error) {
if (error.response) {
// 请求发出,但服务器以状态码响应 (非 2xx)
console.error(`抓取失败: 状态码 ${error.response.status}, 信息: ${error.response.statusText}`);
} else if (error.request) {
// 请求发出,但没有收到响应
console.error('抓取失败: 未收到服务器响应。检查网络或代理是否可用。');
} else {
// 设置请求时发生错误
console.error('抓取失败: 请求设置错误。', error.message);
}
return null; // 返回 null 表示失败
}
}
抓取子页面 (概念)
抓取子页面需要先解析主页面内容,提取出子页面的链接,然后对每个子页面链接重复抓取过程。这通常需要 HTML 解析库(如 `cheerio`)或正则表达式。
// 概念性代码,需要引入 cheerio 或类似库
// const cheerio = require('cheerio');
async function fetchSubPages(mainPageContent, baseUrl) {
// 1. 使用 cheerio 或 regex 解析 mainPageContent,提取子页面链接 (href属性)
// const $ = cheerio.load(mainPageContent);
// const subLinks = [];
// $('a').each((i, elem) => {
// const href = $(elem).attr('href');
// if (href && href.startsWith('/')) { // 示例:只处理相对路径
// subLinks.push(new URL(href, baseUrl).toString());
// }
// });
// 2. 遍历子链接,使用 fetchContent 函数抓取
// const subPageContents = {};
// for (const link of subLinks) {
// const subContent = await fetchContent(link);
// if (subContent) {
// subPageContents[link] = subContent;
// }
// }
// return subPageContents;
console.log("子页面抓取逻辑需要根据实际页面结构实现。");
return {}; // 暂时返回空对象
}
注意: 实现子页面抓取会增加脚本的复杂性和运行时间,需要谨慎设计抓取范围和逻辑。
4. 实现增量更新
为了只在内容变化时发送通知,我们需要将上次成功抓取的内容存储起来,与本次抓取的内容进行比较。使用青龙面板提供的持久化存储(通常映射到宿主机的 `/ql/data` 目录)是最佳实践。
const fs = require('fs').promises; // 使用 promises API
const path = require('path');
const DATA_DIR = '/ql/data'; // 青龙面板的数据目录
const CACHE_FILE = path.join(DATA_DIR, 'web_monitor_cache.txt'); // 缓存文件名
async function checkAndUpdate(newContent) {
let oldContent = '';
try {
await fs.access(DATA_DIR); // 检查目录是否存在
} catch (error) {
console.log(`数据目录 ${DATA_DIR} 不存在,尝试创建...`);
try {
await fs.mkdir(DATA_DIR, { recursive: true });
console.log(`数据目录 ${DATA_DIR} 创建成功。`);
} catch (mkdirError) {
console.error(`创建数据目录 ${DATA_DIR} 失败:`, mkdirError);
// 如果无法创建目录,则无法进行增量更新,可以选择直接发送邮件或退出
return { updated: true, content: newContent }; // 假设为更新,以便首次运行发送邮件
}
}
try {
oldContent = await fs.readFile(CACHE_FILE, 'utf8');
console.log('读取上次缓存内容成功。');
} catch (error) {
if (error.code === 'ENOENT') {
console.log('缓存文件不存在,视为首次运行。');
// 文件不存在,认为是首次运行或缓存被清除
} else {
console.error('读取缓存文件失败:', error);
// 读取失败,为避免漏报,可以认为内容已更新
return { updated: true, content: newContent };
}
}
if (newContent && newContent !== oldContent) {
console.log('检测到网页内容更新。');
try {
await fs.writeFile(CACHE_FILE, newContent, 'utf8');
console.log('新内容已写入缓存文件。');
return { updated: true, content: newContent }; // 内容有变化
} catch (writeError) {
console.error('写入缓存文件失败:', writeError);
// 写入失败也应视为更新,以确保通知发送
return { updated: true, content: newContent };
}
} else if (!newContent) {
console.log('本次抓取内容为空,不进行比较和更新。');
return { updated: false, content: null }; // 本次抓取失败
} else {
console.log('网页内容无变化。');
return { updated: false, content: newContent }; // 内容无变化
}
}
5. 发送 163 邮箱通知
使用 `nodemailer` 库,配置 163 邮箱的 SMTP 服务器信息,并使用环境变量中的凭证进行认证。
const nodemailer = require('nodemailer');
async function sendEmailNotification(subject, content) {
const user = process.env.EMAIL_USER;
const pass = process.env.EMAIL_PASS;
const to = process.env.EMAIL_TO;
if (!user || !pass || !to) {
console.error('邮箱配置环境变量不完整 (EMAIL_USER, EMAIL_PASS, EMAIL_TO)。无法发送邮件。');
return;
}
const transporter = nodemailer.createTransport({
host: 'smtp.163.com',
port: 465, // 使用 SSL
secure: true, // true for 465, false for other ports
auth: {
user: user, // 您的 163 邮箱账号
pass: pass, // 您的 163 邮箱 SMTP 授权码
},
});
const mailOptions = {
from: `"QingLong Monitor" <${user}>`, // 发件人地址
to: to, // 收件人地址
subject: subject, // 邮件主题
text: `网页内容有更新:\n\n${content.substring(0, 500)}...\n\n(内容较长,已截断)`, // 纯文本内容 (可截断)
// html: `网页内容有更新:
${content}` // 或者发送 HTML 格式邮件
};
try {
const info = await transporter.sendMail(mailOptions);
console.log('邮件发送成功: %s', info.messageId);
} catch (error) {
console.error('邮件发送失败:', error);
}
}
整合脚本与执行
完整 JS 脚本示例 (`web_monitor.js`)
将以下代码整合放入您在青龙面板创建的 `web_monitor.js` 文件中:
/**
* @name Web Monitor Script
* @description Monitors a webpage via r.jina.ai proxy, checks for updates, and sends email notifications via 163 SMTP.
* @author Ithy (AI Assistant)
* @version 1.0.0
* @cron 0 */1 * * * // Example: Run every hour at minute 0. Adjust as needed.
* @env TARGET_URL - The target webpage URL to monitor (e.g., https://example.com)
* @env EMAIL_USER - Your 163 email address (e.g., your_email@163.com)
* @env EMAIL_PASS - Your 163 email SMTP authorization code
* @env EMAIL_TO - Recipient email address
* @requires axios
* @requires nodemailer
*/
const axios = require('axios');
const nodemailer = require('nodemailer');
const fs = require('fs').promises;
const path = require('path');
// --- 配置区 ---
// 从环境变量获取配置,提供默认值或检查是否存在
const TARGET_URL = process.env.TARGET_URL;
const EMAIL_USER = process.env.EMAIL_USER;
const EMAIL_PASS = process.env.EMAIL_PASS;
const EMAIL_TO = process.env.EMAIL_TO;
const DATA_DIR = '/ql/data'; // 青龙面板数据目录
const CACHE_FILE = path.join(DATA_DIR, 'web_monitor_cache.txt'); // 缓存文件名 (确保目录存在)
// --- 抓取函数 ---
async function fetchContent(targetUrl) {
if (!targetUrl) {
console.error('错误: 环境变量 TARGET_URL 未设置。');
return null;
}
const proxyUrl = `https://r.jina.ai/${targetUrl}`;
console.log(`尝试通过代理抓取: ${proxyUrl}`);
try {
// 增加 User-Agent 模拟浏览器访问
const response = await axios.get(proxyUrl, {
timeout: 30000, // 30 秒超时
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
console.log('网页内容抓取成功。');
// r.jina.ai 返回的内容可能是 Markdown 或纯文本,直接使用 data 即可
return response.data;
} catch (error) {
if (error.response) {
console.error(`抓取失败: 状态码 ${error.response.status}, 信息: ${error.response.statusText}`);
if (error.response.status === 404) {
console.error(`错误: 目标URL (${targetUrl}) 返回 404 Not Found。请检查 URL 是否正确且可访问。`);
}
} else if (error.request) {
console.error('抓取失败: 未收到服务器响应。检查网络、代理 (${proxyUrl}) 或目标网站状态。');
} else {
console.error('抓取失败: 请求设置错误。', error.message);
}
return null;
}
}
// --- 增量更新检查函数 ---
async function checkAndUpdate(newContent) {
let oldContent = '';
// 确保数据目录存在
try {
await fs.access(DATA_DIR);
} catch {
try {
await fs.mkdir(DATA_DIR, { recursive: true });
console.log(`数据目录 ${DATA_DIR} 已创建。`);
} catch (mkdirError) {
console.error(`创建数据目录 ${DATA_DIR} 失败:`, mkdirError);
// 无法创建目录,直接认为有更新以触发通知
return { updated: !!newContent, content: newContent };
}
}
// 读取旧内容
try {
oldContent = await fs.readFile(CACHE_FILE, 'utf8');
} catch (error) {
if (error.code !== 'ENOENT') {
console.error('读取缓存文件失败:', error);
// 读取失败,谨慎起见,认为内容已更新
return { updated: !!newContent, content: newContent };
}
console.log('缓存文件不存在,视为首次运行。');
}
// 比较和写入
if (newContent && newContent !== oldContent) {
console.log('检测到网页内容更新。');
try {
await fs.writeFile(CACHE_FILE, newContent, 'utf8');
console.log('新内容已写入缓存文件。');
return { updated: true, content: newContent };
} catch (writeError) {
console.error('写入缓存文件失败:', writeError);
return { updated: true, content: newContent }; // 写入失败也发送通知
}
} else if (!newContent) {
console.log('本次抓取内容为空或失败,不进行比较和更新。');
return { updated: false, content: null };
} else {
console.log('网页内容无变化。');
return { updated: false, content: newContent };
}
}
// --- 邮件通知函数 ---
async function sendEmailNotification(subject, content) {
if (!EMAIL_USER || !EMAIL_PASS || !EMAIL_TO) {
console.error('错误: 邮箱配置环境变量不完整 (EMAIL_USER, EMAIL_PASS, EMAIL_TO)。无法发送邮件。');
return;
}
const transporter = nodemailer.createTransport({
host: 'smtp.163.com',
port: 465,
secure: true,
auth: { user: EMAIL_USER, pass: EMAIL_PASS },
tls: {
// do not fail on invalid certs
rejectUnauthorized: false
}
});
const mailOptions = {
from: `"QingLong Monitor" <${EMAIL_USER}>`,
to: EMAIL_TO,
subject: subject,
text: `监测到网页 ${TARGET_URL} 内容更新:\n\n${content.substring(0, 1000)}...\n\n(为避免邮件过长,内容已截断)`,
// html: `监测到网页 ${TARGET_URL} 内容更新:
${content.substring(0, 2000)}...(为避免邮件过长,内容已截断)
`
};
try {
const info = await transporter.sendMail(mailOptions);
console.log('邮件发送成功: %s', info.messageId);
} catch (error) {
console.error('邮件发送失败:', error);
}
}
// --- 主逻辑函数 ---
async function main() {
console.log(`开始执行网页监控任务: ${new Date().toLocaleString()}`);
// 1. 抓取主页面内容
const mainPageContent = await fetchContent(TARGET_URL);
if (!mainPageContent) {
console.log('抓取主页面内容失败或为空,任务中止。');
return; // 如果主页面抓取失败,则不继续
}
// (可选) 在这里可以加入抓取和处理子页面的逻辑
// let combinedContent = mainPageContent;
// const subPageContents = await fetchSubPages(mainPageContent, TARGET_URL);
// // 将子页面内容合并到 combinedContent 中...
// const contentToCompare = combinedContent;
// 暂时只监控主页面内容
const contentToCompare = mainPageContent;
// 2. 检查是否有更新
const updateStatus = await checkAndUpdate(contentToCompare);
// 3. 如果有更新,发送邮件通知
if (updateStatus.updated && updateStatus.content) {
const subject = `网页监控通知: ${TARGET_URL} 内容已更新`;
await sendEmailNotification(subject, updateStatus.content);
} else {
console.log('内容无变化或抓取失败,无需发送邮件。');
}
console.log(`网页监控任务执行完毕: ${new Date().toLocaleString()}`);
}
// --- 执行主函数 ---
main().catch(error => {
console.error('脚本执行过程中发生未捕获错误:', error);
// 可选:发送错误通知邮件
// sendEmailNotification('脚本执行错误', `错误信息: ${error.message}\n${error.stack}`);
});
配置定时任务
- 返回青龙面板的 “定时任务” 页面。
- 点击 “新建任务”。
- 名称: 给任务起个名字,如 `监控BoringReport`。
- 命令/脚本: 输入 `task web_monitor.js` (确保与您的脚本文件名一致)。
-
定时规则: 输入 Cron 表达式来定义执行频率。例如:
0 * * * *: 每小时的第 0 分钟执行一次。*/30 * * * *: 每 30 分钟执行一次。0 9 * * *: 每天早上 9 点执行一次。
- 点击 “确定” 保存任务。
您可以手动点击任务右侧的 “运行” 按钮进行测试,并检查 “日志” 页面查看脚本输出和执行情况。
任务流程可视化
自动化工作流概览
下面的思维导图展示了整个自动化任务的执行流程:
这个流程清晰地展示了从任务启动到最终根据内容变化决定是否发送邮件的完整步骤。
关键组件评估
为了更好地理解此自动化方案中各个方面的特点,以下雷达图从几个维度评估了这个设置:
解读: 该图表显示,虽然此方案在易用性和增量更新方面表现尚可,但在抓取可靠性(依赖外部代理和目标网站稳定性)、安全性(需妥善处理凭证)和扩展性(处理子页面较复杂)方面有提升空间。通知即时性受邮件系统限制。资源消耗在中等水平。
配置参数概览
下表总结了在青龙面板和脚本中需要配置的关键参数:
| 参数类型 | 参数名称 (环境变量/脚本内) | 示例值 | 说明 |
|---|---|---|---|
| 目标网页 | TARGET_URL (环境变量) |
https://example.com/news |
需要监控的实际网页地址 (请替换)。 |
| 邮箱账户 | EMAIL_USER (环境变量) |
your_monitor@163.com |
用于发送邮件的 163 邮箱地址。 |
| 邮箱凭证 | EMAIL_PASS (环境变量) |
YOUR_SMTP_AUTH_CODE |
163 邮箱的 SMTP 授权码 (非登录密码)。 |
| 收件邮箱 | EMAIL_TO (环境变量) |
your_personal_email@domain.com |
接收更新通知的邮箱地址。 |
| 脚本依赖 | @requires axios (脚本注释) |
N/A | 声明脚本需要 axios 库。 |
| 脚本依赖 | @requires nodemailer (脚本注释) |
N/A | 声明脚本需要 nodemailer 库。 |
| 缓存文件 | CACHE_FILE (脚本内变量) |
/ql/data/web_monitor_cache.txt |
用于存储上次抓取内容的文件路径 (位于持久化目录)。 |
| 定时规则 | 定时任务 Cron 表达式 | 0 */2 * * * |
定义脚本多久运行一次 (示例为每 2 小时)。 |
请确保根据您的实际情况正确配置这些参数,特别是环境变量。
邮件自动化概念图
上图展示了邮件自动化的概念,这正是我们通过脚本实现的核心功能之一:在特定条件(网页更新)触发时自动发送邮件。
常见问题解答 (FAQ)
▶ 如何处理目标网站结构变化导致抓取失败?
网站结构变化是网页抓取的常见挑战。如果 Jina AI 的 `r.jina.ai` 服务能够适应性地提取主要内容,问题可能不大。但如果依赖特定的 HTML 结构(尤其是在抓取子页面时),脚本可能需要更新。
- 健壮的选择器: 如果使用 `cheerio` 等库解析 HTML,尽量使用更稳定、不易改变的选择器(如 ID、特定的 class 组合)。
- 错误监控: 脚本中的 `try...catch` 块应能捕获大部分错误。在日志中监控错误信息,并在连续失败时考虑发送错误通知。
- 定期检查: 定期手动检查目标网站和脚本日志,确保抓取逻辑仍然有效。
▶ 如果 r.jina.ai 代理服务不稳定或失效怎么办?
依赖第三方服务总存在风险。如果 `r.jina.ai` 不可用:
- 备选方案: 可以考虑直接使用 `axios` 抓取原始 URL (不通过代理),但这可能需要处理更复杂的 HTML,且更容易触发反爬虫机制。
- 其他代理/服务: 寻找其他类似的网页内容提取服务或使用商业/自建代理服务器。
- 错误处理: 脚本中的错误处理应能识别代理连接失败或超时,并在日志中记录。可以设置重试逻辑,但需谨慎避免短时间内过多请求。
▶ 如何安全地存储 163 邮箱的 SMTP 授权码?
将敏感凭证(如 SMTP 授权码)存储在青龙面板的环境变量中是推荐的做法。这比直接硬编码在脚本文件里更安全。
- 环境变量: 如脚本示例所示,使用 `process.env.EMAIL_PASS` 来读取。确保青龙面板本身部署在安全的环境中。
- 访问控制: 限制对青龙面板管理界面的访问权限。
- 避免日志泄露: 确保脚本的日志输出中不会意外打印出密码或授权码。
▶ 如何在青龙面板中调试这个 JS 脚本?
调试青龙脚本可以通过以下方式进行:
- 日志输出 (console.log): 在脚本的关键位置添加详细的 `console.log` 语句,输出变量值或执行步骤信息。然后在青龙面板的“日志”页面查看对应任务的日志。
- 手动运行: 在“定时任务”页面,找到您的任务,点击右侧的“运行”按钮手动触发一次执行,方便立即查看日志。
- 简化测试: 暂时注释掉邮件发送部分,专注于测试抓取和比较逻辑是否正常。
- 本地测试 (可选): 如果熟悉 Node.js 环境,可以在本地安装 `axios`, `nodemailer`,设置好模拟的环境变量,先在本地运行和调试脚本。
▶ 为什么使用文件存储 (fs) 而不是内存变量来实现增量更新?
青龙面板每次执行定时任务时,通常会启动一个新的 Node.js 进程。这意味着脚本中的普通内存变量(如 `let lastUpdate = '';`)在每次任务运行时都会被重置,无法在多次运行之间保持状态。
使用文件系统 (`fs`) 将上一次抓取的内容写入到青龙面板的持久化存储目录 (`/ql/data`) 中的文件里,可以确保即使脚本进程结束,状态信息也能被保存下来。下次任务运行时,脚本可以从该文件中读取上一次的状态,从而实现可靠的增量更新比较。
参考文献
- GitHub - jina-ai/reader - GitHub
- 青龙如何使用js脚本 | PingCode智库 - PingCode Docs
- 2024年最新青龙面板跑脚本教程(一)持续更新中原创 - CSDN博客
- node.js、python脚本读取青龙面板的环境变量- harglo - 博客园
- 青龙面板基础知识和使用教程 - 博客园
- docker安装青龙面板薅羊毛(新手教程,大佬可略过)-CSDN博客
推荐探索
Last updated April 12, 2025