碎碎念
过去的一个学期经常会有这样的经历: 本来正在宿舍美美地睡懒觉,或者在实验室专心摸鱼(划掉)看文献,突然微信弹窗亮起,导师发来一条消息:
“咱们组的主页怎么打不开了?抓紧看一下。”
那一瞬间,血压飙升,心跳加速。 作为课题组兼职的“网站管理员”,最尴尬的不是网站挂了,而是网站挂了半天,全世界都知道了,只有我不知道,最后还得让导师来提醒我修。
课题组的网站(如 cyclostrat.org, sedst.org 等)大多由我们学生一代代维护,没有专业的运维团队。我们忙于各种工作,谁也做不到每天像闹钟一样去刷一遍网站看看它活没活着(主要是懒,或者总是忘记)。
为了彻底终结这种“社死”隐患,我决定利用手头的工具,给网站请一位**“免费的、每日固定在线的电子保安”**。
🛠️ 使用青龙面板实现功能
我手头正好有一台服务器部署了 青龙面板 (Qinglong Panel)。 虽然它通常被大家用来跑“薅羊毛”脚本,但实际上它是一个非常强大的定时任务管理平台。
环境现成:内置 Python 环境,依赖管理方便。
定时灵活:支持 Crontab 表达式,想几点查就几点查。
通知强大:自带微信、钉钉等推送,配合脚本还能发邮件。
我的目标很明确:每天早上 7:30 自动巡检。 这意味着,如果网站昨晚挂了,我会在起床时第一时间收到报警,趁导师 9:00 上班前把问题修好——只要我修得够快,就没人知道它坏过! 🌚
1. 第一步:安装 Python 依赖(关键!)
很多新手把代码复制进去,一运行就报错 ModuleNotFoundError: No module named 'requests'。这是因为青龙的 Python 环境默认是纯净的,需要手动安装第三方库。
操作步骤:
登录青龙面板,点击左侧菜单的 “依赖管理”。
在顶部选项卡点击 “Python3”(一定要选对,别选成 Nodejs)。
点击右上角 “新建依赖”。
在名称里输入
requests。点击确定,等待状态变为 “已安装”。

小贴士:我们的脚本只用了
requests,其他库如ssl,socket,smtplib都是 Python 自带的,不需要额外安装。
2. 第二步:编写监控脚本
这是核心部分。我编写了一个 Pro Max Ultra 版 的监控脚本,它解决了以下痛点:
防误报:国外网站访问慢,脚本会自动重试 3 次,且延长超时时间。
防缓存:强制穿透 CDN 缓存,确保检测到的是实时状态。
深度检测:不仅看网站能不能开,还会扫描页面里的图片有没有 404。
每日一言:在通知邮件里加一句名言,让日报不枯燥。
操作步骤:
点击左侧 “脚本管理” -> 右上角 “+” 号。
类型选择“空文件”,文件名填
site_monitor.py。复制以下代码进去:
Python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
网站状态监控脚本 (Pro Max Ultra版 - 适配青龙面板)
功能:
1. 深度检测:HTTP状态码、内容匹配、SSL有效期
2. 资源检测:自动扫描页面图片链接,检测是否404
3. 稳定性优化:使用 Session 连接池 + 底层自动重试 + 穿透CDN缓存
4. 趣味功能:集成“每日一言” (Hitokoto)
5. 通知:notify.py + SMTP 自定义邮件 (文案美化版)
"""
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
import ssl
import socket
import datetime
import smtplib
import os
import time
import re
import json
from urllib.parse import urljoin
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr
# ================= 配置区域 =================
# 1. 邮件接收人
MAIL_TO_LIST = [
'yanchang@yanchang.cc',
'1050854313@qq.com'
]
# 2. 行为配置
SEND_SUCCESS_NOTIFY = True
MAX_RETRIES = 3 # 脚本逻辑重试次数
RETRY_DELAY = 10 # 重试等待时间
LATENCY_THRESHOLD = 5.0 # 响应慢警告阈值
TIMEOUT_SEC = 30 # 请求超时时间
# 3. SSL预警天数
SSL_ALERT_DAYS = 7
# 4. 网站列表
SITES = [
{
"url": "https://cyclostrat.org/#/",
"keyword": "Cyclostratigraphy",
"check_ssl": True,
"check_images": True
},
{
"url": "https://sedst.org/home",
"keyword": "SedS&T",
"bad_words": ["Database Error", "502 Bad Gateway"],
"check_ssl": True,
"check_images": True
},
{
"url": "https://machao.group/",
"keyword": "Ma",
"check_ssl": True,
"check_images": True
},
]
# 5. 状态码字典
HTTP_ERRORS = {
403: "拒绝访问 (Forbidden)",
404: "资源未找到 (Not Found)",
500: "内部错误 (Server Error)",
502: "网关错误 (Bad Gateway)",
503: "维护中 (Service Unavailable)",
504: "超时 (Gateway Timeout)",
}
# ================= 核心逻辑 =================
try:
from notify import send as ql_send
except ImportError:
def ql_send(title, content):
print(f"【无notify.py】跳过通用推送")
def get_session():
"""
创建带连接池和底层重试的 Session
解决 '第一次访问失败' 的核心优化
"""
session = requests.Session()
# connect=3: 底层TCP连接失败重试3次
# backoff_factor=0.5: 失败后稍微等一下再试
retry = Retry(connect=3, read=2, redirect=2, backoff_factor=0.5)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session
def get_hitokoto():
"""获取每日一言 (增加防缓存参数)"""
try:
# 增加随机参数 t=时间戳 防止API缓存
r = requests.get(f"https://v1.hitokoto.cn/?c=d&c=i&c=k&t={int(time.time())}", timeout=5)
if r.status_code == 200:
data = r.json()
return f"\n\n✨ 【每日一言】\n{data['hitokoto']}\n —— {data['from']}"
except Exception:
pass
return "\n\n✨ 【每日一言】\n生活原本沉闷,但跑起来就有风。\n —— 佚名"
def send_custom_email(title, content):
"""自定义邮件发送"""
smtp_server = os.getenv('SMTP_SERVER') or 'smtp.qq.com'
smtp_email = os.getenv('SMTP_EMAIL')
smtp_password = os.getenv('SMTP_PASSWORD')
smtp_port = 465
if not smtp_email or not smtp_password:
print("⚠️ 缺少 SMTP 环境变量,无法发邮件!")
return
try:
message = MIMEText(content, 'plain', 'utf-8')
message['From'] = formataddr((Header("网站监控卫士", 'utf-8').encode(), smtp_email))
message['To'] = ",".join(MAIL_TO_LIST)
message['Subject'] = Header(title, 'utf-8')
server = smtplib.SMTP_SSL(smtp_server, smtp_port)
server.login(smtp_email, smtp_password)
server.sendmail(smtp_email, MAIL_TO_LIST, message.as_string())
server.quit()
print("✅ 邮件发送成功!")
except Exception as e:
print(f"❌ 邮件发送失败: {e}")
def check_ssl_expiry(url):
"""检测SSL"""
try:
hostname = url.split("//")[-1].split("/")[0]
if ":" in hostname: hostname, port = hostname.split(":")
else: port = 443
context = ssl.create_default_context()
with socket.create_connection((hostname, port), timeout=20) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
expire_date = datetime.datetime.strptime(cert['notAfter'], "%b %d %H:%M:%S %Y %Z")
remaining_days = (expire_date - datetime.datetime.utcnow()).days
if remaining_days < 0: return False, f"🔴 SSL过期{abs(remaining_days)}天", remaining_days
elif remaining_days < SSL_ALERT_DAYS: return False, f"🟠 SSL剩{remaining_days}天", remaining_days
return True, f"🟢 SSL剩{remaining_days}天", remaining_days
except Exception: return False, f"🔴 SSL检测失败", 0
def check_page_images(base_url, html_content, session):
"""扫描页面图片链接并检测有效性"""
print(" -> 正在扫描页面图片资源...")
img_urls = re.findall(r'<img[^>]+src=["\'](.*?)["\']', html_content, re.IGNORECASE)
broken_images = []
checked_count = 0
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': base_url,
'Cache-Control': 'no-cache', # [新增] 防止CDN缓存
'Pragma': 'no-cache'
}
for img_path in img_urls:
if checked_count >= 10: break
if img_path.startswith('data:') or not img_path.strip(): continue
full_url = urljoin(base_url, img_path)
try:
r = session.head(full_url, headers=headers, timeout=10, verify=False)
if r.status_code in [405, 403]:
r = session.get(full_url, headers=headers, timeout=10, verify=False, stream=True)
r.close()
if r.status_code >= 400:
print(f" ❌ 图片损坏: {full_url} (Code: {r.status_code})")
broken_images.append(f"图片损坏: {os.path.basename(full_url)} ({r.status_code})")
else:
checked_count += 1
except Exception as e:
print(f" ❌ 图片访问错误: {full_url}")
broken_images.append(f"图片无法访问: {os.path.basename(full_url)}")
if broken_images:
return f"⚠️ 发现 {len(broken_images)} 张坏图"
return ""
def check_website_core(site, session):
url = site["url"]
keyword = site.get("keyword")
bad_words = site.get("bad_words", [])
check_ssl = site.get("check_ssl", False)
do_check_img = site.get("check_images", False)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7',
'Cache-Control': 'no-cache', # [新增] 强制请求最新数据
'Pragma': 'no-cache'
}
error_msgs = []
try:
# 使用 Session 进行请求
resp = session.get(url, timeout=TIMEOUT_SEC, verify=check_ssl, headers=headers)
latency = resp.elapsed.total_seconds()
code = resp.status_code
if code == 200:
if keyword and keyword not in resp.text:
error_msgs.append(f"❌ 内容异常 (未找到: {keyword})")
for bw in bad_words:
if bw in resp.text:
error_msgs.append(f"❌ 发现错误关键词: {bw}")
if latency > LATENCY_THRESHOLD:
error_msgs.append(f"⚠️ 响应过慢 ({latency:.2f}s)")
if do_check_img:
img_err = check_page_images(url, resp.text, session)
if img_err:
error_msgs.append(img_err)
else:
error_desc = HTTP_ERRORS.get(code, "未知错误")
error_msgs.append(f"❌ HTTP状态异常: {code} - {error_desc}")
except requests.exceptions.SSLError as e:
error_msgs.append(f"❌ SSL握手失败: {str(e)}")
except requests.exceptions.ConnectionError:
error_msgs.append("❌ 连接失败 (可能DNS或握手超时)")
except requests.exceptions.Timeout:
error_msgs.append(f"❌ 访问超时 (超过{TIMEOUT_SEC}秒)")
except Exception as e:
error_msgs.append(f"❌ 未知错误: {str(e)}")
ssl_info_str = ""
if check_ssl:
ssl_ok, ssl_msg, days = check_ssl_expiry(url)
if not ssl_ok: error_msgs.append(f"SSL警告: {ssl_msg}")
ssl_info_str = f"[{days}天]"
return error_msgs, ssl_info_str
def check_website_with_retry(site):
print(f"正在检测: {site['url']} ...")
session = get_session() # 获取增强版Session
for i in range(MAX_RETRIES):
msgs, ssl_info = check_website_core(site, session)
if not msgs:
print(f" -> ✅ 检测通过 | SSL: {ssl_info}")
return [], ssl_info
# 严重错误不重试
if any("SSL" in m for m in msgs) and "SSL握手失败" not in str(msgs):
return msgs, ssl_info
# 坏图不重试
if any("坏图" in m for m in msgs):
return msgs, ssl_info
if i < MAX_RETRIES - 1:
print(f" -> ⚠️ 第 {i+1} 次失败: {msgs[0]}")
print(f" 等待 {RETRY_DELAY} 秒后重试...")
time.sleep(RETRY_DELAY)
else:
print(f" -> ❌ 最终失败: {msgs}")
return msgs, ssl_info
return msgs, ssl_info
def main():
print(f"-------- 开始巡检: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} --------")
all_errors = []
summary_lines = []
for site in SITES:
msgs, ssl_info = check_website_with_retry(site)
site_name = site['url'].replace("https://", "").replace("http://", "").split("/")[0]
if msgs:
for msg in msgs: all_errors.append(f"{site['url']}\n{msg}")
summary_lines.append(f"❌ {site_name} {ssl_info} -> {msgs[0]}")
else:
summary_lines.append(f"✅ {site_name} {ssl_info}")
print("-------- 巡检结束 --------")
# === 构建通知文案 ===
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
hitokoto = get_hitokoto() # 获取每日一言
title = ""
content = ""
should_send = False
if all_errors:
should_send = True
title = f"【网站报警】发现 {len(all_errors)} 个异常 🚨"
content = f"📅 报警时间: {current_time}\n\n"
content += "🛑 故障详情:\n" + "\n------------------\n".join(all_errors)
content += f"\n\n📋 完整列表:\n" + "\n".join(summary_lines)
content += hitokoto
elif SEND_SUCCESS_NOTIFY:
should_send = True
title = "【网站监控】所有站点运行正常 🟢"
content = f"📅 巡检时间: {current_time}\n\n"
content += "📋 巡检报告:\n"
content += "\n".join(summary_lines)
content += "\n\n✅ 所有监控目标响应正常,SSL证书有效。"
content += hitokoto
if should_send:
ql_send(title, content)
send_custom_email(title, content)
if __name__ == "__main__":
main()
3. 第三步:配置环境变量(保护隐私)
为了发邮件,我们需要用到邮箱的 SMTP 服务。千万不要把密码直接写在脚本代码里! 万一以后你把代码分享给师弟师妹,密码就泄露了。
青龙面板的 “环境变量” 功能就是为了解决这个问题的。
操作步骤:
点击左侧 “环境变量” -> “新建变量”。
依次添加以下变量(以 QQ 邮箱为例):
如何获取授权码?
登录 QQ 邮箱网页版 -> 设置 -> 账户 -> 开启 POP3/SMTP 服务 -> 生成授权码。
4. 第四步:设置定时任务(Cron 表达式)
最后一步,让脚本每天自动运行。我希望能每天早上 07:30 执行,这样我起床就能看到报告,如果有问题,还能赶在导师 9 点上班前修好。
操作步骤:
点击左侧 “定时任务” -> “新建任务”。
名称:随便填,比如
每日网站巡检。命令/脚本:
task site_monitor.py。定时规则:
0 30 7 * * *。
小白科普 Cron 表达式:
这个 0 30 7 * * * 是什么意思?
第 1 位
0:第 0 秒第 2 位
30:第 30 分第 3 位
7:第 7 点 (24小时制)后面
*:每天、每月、每周
设置好后,点击运行测试一下,如果能收到邮件,就大功告成了!

💡 脚本迭代史:从“能用到好用”
为了实现这个目标,我编写了一个 Python 监控脚本,中间经历了几次关键的迭代:
v1.0:告别“假死”
最初我只检测 HTTP 状态码是否为 200。 但很快发现一个坑:有时候 Nginx 活着(返回 200),但后端数据库挂了,页面显示“Database Error”。 改进:引入关键词检测 (Keyword Check)。比如首页必须包含 "Cyclostratigraphy" 这个词,如果没有,就算状态码是 200 也视为故障。
v2.0:治疗“SSL 过期焦虑症”
Let's Encrypt 的免费证书 90 天就过期,经常忘续期导致网站报红锁。 改进:脚本通过 ssl 和 socket 库直接握手获取证书到期时间。如果剩余不足 7 天,直接在日报里标黄警告。
v3.0:拯救“裂图”
经常路径写错导致图片加载失败(404)。人工检查太累了。 改进:脚本像爬虫一样,正则提取页面内所有 <img src="...">,发送 HEAD 请求检测图片链接是否有效。
v4.0:攻克“网络玄学” (Pro Max Ultra 版)
这是最头疼的。部分网站部署在大陆之外的服务器上,国内访问经常偶尔超时。脚本一超时就报警,搞得我半夜神经衰弱。 改进:
引入 Session 连接池:底层复用 TCP 连接。
智能重试:设置 30秒 超时,失败后等待 10秒 重试,连续 3 次失败才报警。
穿透缓存:请求头加上
Cache-Control: no-cache,防止 CDN 告诉我“一切正常”其实源站早就挂了。
📈 最终成果展示
现在,这个脚本每天早上 07:30 准时运行。
场景一:一切安好(岁月静好模式)
每天早上醒来,我会收到一封绿色的邮件。除了汇报所有网站正常、证书剩余天数外,脚本还会贴心地附上一句**“每日一言”**。
【网站监控】所有站点运行正常 🟢
📅 巡检时间: 2026-02-09 07:30:00 📋 巡检报告: ✅ cyclostrat.org [SSL: 76天] ✅ sedst.org [SSL: 76天] ✅ machao.group [SSL: 37天]
✨ 【每日一言】 生活原本沉闷,但跑起来就有风。
看着这封邮件,我就知道今天又是平安的一天。☕️

场景二:由于故障(战时状态)
一旦有网站挂了,或者图片坏了,邮件标题会直接变成 🚨 [报警],并精确指出问题:
❌ sedst.org [SSL: 76天] -> ⚠️ 发现 1 张坏图 🛑 故障详情: ❌ 图片损坏: https://sedst.org/images/logo.png (Code: 404)
收到这封信,我大概还有 1 个小时的时间(在导师醒来之前)去修复它。

🔚 总结
运维的最高境界就是**“无感”**。 通过这套自动化的监控方案,我不仅从重复的人工检查中解脱了出来,更重要的是,它帮我消除了对“未知故障”的恐惧。
现在,我可以理直气壮地对导师说:“放心,网站一直都很稳。”(因为不稳的时候,我已经偷偷修好了😉)。
