验证探针频率响应实验 | Coaxwave 射频探针测试指南
你是一个资深 Python 开发,帮我写一个静态站点发布脚本 publisher.py(Python 3.10+)。
用宝塔面板 API 同步文件到服务器,不再使用 rsync/SSH。
═══════════════════════════════════════════
【项目背景】
═══════════════════════════════════════════
我有一个站群后台管理系统(Flask + SQLite)。每个"子域"对应一个独立的纯静态网站。
发布时:用户在后台选子域 → 脚本从 CONFIG 读取该子域的本地目录 + 宝塔面板参数 → 本地生成/更新文件 → 调宝塔API传到服务器站点根目录。
【本地目录结构】(以 absorber 子域为例):
{site_dir}\
├── articles\ ← 所有文章详情页 HTML
│ ├── article-1.html ← 现有文章(作为新文章的HTML结构模板)
│ ├── article-2.html
│ ├── source\ ← 存放 Markdown 源文件(脚本自动创建)
│ └── ...
├── index.html ← 首页(含最新文章列表)
├── list.html ← 文章列表页(可选,没有则用 index.html)
├── search.json ← 搜索索引
├── sitemap.xml ← 站点地图
├── css\ ← 样式文件
├── js\ ← 脚本文件
└── images\ ← 图片资源
【重要约束】
- 没有数据库,所有数据从文件系统读取和写入
- 新文章发布时,必须严格参照 articles/ 目录下现有文章的 HTML 结构
- 只替换内容部分(标题、正文、日期、meta),其他 HTML 结构、CSS 类名、布局完全不变
- 所有本地路径使用 pathlib(WindowsPath)处理,正确处理中文路径
- 全程 try/except,单步失败不中断整体流程
- 宝塔 API 调用频率:官方未公开固定 QPS,按保守策略——单篇发布串行逐个传(不并发),批量用 zip 模式
═══════════════════════════════════════════
【CONFIG 配置字典(文件顶部)】
═══════════════════════════════════════════
CONFIG = {
"subdomains": {
"absorber": { # 子域标识(后台下拉框的值)
"site_dir": r"C:\Users\Anh52002\Desktop\常用文件夹\待上线网站\铁氧体.中国\absorber.铁氧体.中国",
"site_url": "https://absorber.铁氧体.中国",
"list_page": "index.html", # 列表页文件名
"remote_site_root": "/www/wwwroot/absorber.铁氧体.中国", # 服务器上宝塔站点绝对根目录
"baidu_token": "", # 空字符串=跳过百度推送
"baota": {
"panel_url": "https://1.2.3.4:8888", # 宝塔面板地址(替换为真实IP+端口)
"api_key": "", # 宝塔API密钥(为空时跳过同步,从环境变量 BAOTA_API_KEY 读取)
"timeout": 60,
"verify_ssl": False, # 自签证书先False;有正规证书改True
"use_upload": True # True=逐文件UploadFile(单篇发布用);False=打zip上传后解压(批量用)
}
},
# 可以继续添加更多子域...
},
"backup_root": r"C:\Users\Anh52002\Desktop\常用文件夹\待上线网站\备份",
"related_count": 4, # 相关文章数量
"encoding": "utf-8"
}
注意:api_key 优先从环境变量 BAOTA_API_KEY 读取,CONFIG 里的作为 fallback。
═══════════════════════════════════════════
【发布主流程】 publish(subdomain: str, title: str, article_md: str)
═══════════════════════════════════════════
def publish(subdomain, title, article_md):
conf = CONFIG["subdomains"][subdomain]
site_dir = Path(conf["site_dir"])
articles_dir = site_dir / "articles"
today = datetime.now().strftime("%Y-%m-%d")
now_iso = datetime.now().strftime("%Y-%m-%dT%H:%M:%S+08:00")
步骤 1:生成文章详情页 HTML
─────────────────────────
a. slug 生成:
- 中文标题用 pypinyin(lazy_pinyin)→ 转小写 → 空格和标点替换为连字符 → 截断50字符
- 示例:"CNC铣削参数大全" → "cnc-xi-xiao-can-shu-da-quan"
- 退化方案:pypinyin 不可用时用 "article-{int(time.time())}"
- 冲突检查:如果 articles/{slug}.html 已存在,追加 "-2"、"-3" 直到不冲突
b. 模板选取:
- 读取 articles/ 下第一个现有 .html 文件(按文件名排序,优先 article-1.html)
- 如果 articles/ 下没有任何 .html(第一篇发布),使用内置最小 HTML 模板:
<!DOCTYPE html><html><head><meta charset="utf-8"><title>{title}</title>
<meta name="description" content="{description}">
<link rel="stylesheet" href="css/style.css"></head>
<body><h1>{title}</h1><div class="content">{content}</div></body></html>
c. BeautifulSoup 解析模板后替换:
- <title> → 新标题
- <meta name="description"> content → 正文纯文本前150字
- <meta name="keywords"> content → 从标题提取关键词(逗号分隔)
- <h1>(或首个标题标签)→ 新标题
- 正文容器查找顺序:<div class="content"> → <article> → <main> → <div class="article-body"> → 最后一个 <div>
- 正文内容 → markdown.markdown(article_md, extensions=['extra', 'codehilite'])
- 日期元素:class 含 date/time/publish/updated 的元素 → today
d. 在正文末尾追加【相关文章】区块(见下方"相关文章逻辑")
e. 保存:
- 生成的 HTML → articles/{slug}.html(先写 .tmp 再原子替换)
- 原始 Markdown → articles/source/{slug}.md(source/ 目录不存在则自动创建)
f. 日志:[1/8] 生成详情页 → articles/{slug}.html ✓
步骤 2:重新生成文章列表页 ★关键步骤★
─────────────────────────────────────
a. 扫描 articles/ 下所有 .html 文件(排除 source/ 子目录和列表页自身)
b. 从每个文章 HTML 提取:
- 标题:<h1> 文本 → 失败则取 <title>
- 发布日期:class 含 date/time/publish 的元素文本 → 失败则尝试从文件名解析 → 最终 fallback 为文件修改时间
- 摘要:<meta name="description"> content → 失败则取正文纯文本前150字(strip HTML标签后)
c. 按发布日期倒序排列(最新的在前)
d. 读取列表页模板(conf["list_page"],默认 index.html)
e. 列表容器查找(按顺序 fallback,找到第一个就停):
- <ul class="article-list">
- <div class="post-list">
- <div class="list">
- <main>
- <div class="content"> 中第一个 <ul> 或 <div>
f. 清空该容器内容,重新生成每篇文章的列表项:
<article class="post-item">
<h2><a href="articles/{slug}.html">{title}</a></h2>
<time datetime="{date}">{date}</time>
<p class="summary">{summary}</p>
</article>
g. 如果容器不存在(比如第一篇发布时 index.html 里还没有列表区域):
- 在 <main> 或 <body> 末尾创建 <ul class="article-list"> 并填充
h. 覆盖保存列表页(先写 .tmp 再原子替换)
i. 日志:[2/8] 更新列表页 → {list_page}(共 N 篇文章)✓
步骤 3:更新 search.json
──────────────────────
a. 遍历 articles/ 下所有 .html(排除 source/)
b. 提取每篇:title / url(相对路径如 "articles/{slug}.html")/ keywords(<meta keywords> 或 "")/ summary(<meta description> 或前150字)
c. 生成 JSON 数组:[{"title":"...","url":"...","keywords":"...","summary":"..."}, ...]
d. 保存:search.json(覆盖,先写 .tmp 再原子替换)
e. 日志:[3/8] 更新 search.json(N 条索引)✓
步骤 4:更新 sitemap.xml
──────────────────────
a. 遍历该站点所有 .html 文件(articles/*.html + index.html + list.html 若存在)
b. 每个 URL:loc = conf["site_url"] + "/" + 相对路径(Windows 反斜杠替换为 /)
c. lastmod = today
d. 标准 sitemap 格式:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://absorber.铁氧体.中国/index.html</loc>
<lastmod>2026-09-11</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
...
</urlset>
e. 保存:sitemap.xml(覆盖,先写 .tmp 再原子替换)
f. 日志:[4/8] 更新 sitemap.xml(N 个URL)✓
步骤 5:宝塔面板 API 同步
──────────────────────
封装类 BtPanelClient:
__init__(panel_url, api_key, timeout, verify_ssl)
_sign():
request_time = str(int(time.time()))
request_token = md5(request_time + md5(api_key))
return {"request_time": request_time, "request_token": request_token}
post_form(endpoint, data):POST 表单数据 + 签名,返回 json
upload_file(local_path: Path, remote_dir: str):
POST {panel_url}/files (Content-Type: multipart/form-data)
fields: action=UploadFile, path=remote_dir, request_time, request_token
file field: zunfile=文件二进制(用 open(local_path, "rb"))
返回响应 json
ensure_dir(remote_dir: str):
先调 GetDir 探测目录是否存在
不存在则调 CreateFile 创建(按面板版本适配字段名)
upload_zip_and_extract(zip_path, remote_dir):
上传 zip 到 remote_dir/
调面板解压接口或返回需手动解压的提示
同步逻辑:
a. 收集变更文件清单 changed_files = [
(articles/{slug}.html, articles/{slug}.html),
({list_page}, {list_page}),
(search.json, search.json),
(sitemap.xml, sitemap.xml),
]
b. 如果 conf["baota"]["api_key"] 为空:打印 "[5/8] [skip] 未配置宝塔api_key" → 跳到步骤6
c. use_upload=True(逐文件模式,适合单篇发布):
对每个 (local_rel, remote_rel):
local_path = site_dir / local_rel
remote_dir = conf["remote_site_root"] / Path(remote_rel).parent
ensure_dir(remote_dir)
upload_file(local_path, remote_dir)
成功/失败记日志
★ 串行逐个传,不并发,避免触发面板限流
d. use_upload=False(zip 模式,适合批量/初始化):
将 changed_files 打包为 patch_{today}_{slug}.zip
upload_file(zip_path, conf["remote_site_root"] + "/__patch__/")
打印提示:"请到宝塔面板手动解压 {remote_site_root}/__patch__/patch_*.zip 到 {remote_site_root}/"
e. 任何上传失败:记日志、不中断;最后汇总"同步成功 N/失败 M"
f. 日志:[5/8] 宝塔同步完成(成功N/失败M)✓
步骤 6:推送百度 API(预留)
──────────────────────────
a. 如果 conf["baidu_token"] 非空:
调用百度普通收录 API:
POST https://api.ziyuan.baidu.com/api/v1/urls?token={baidu_token}
Content-Type: text/plain
Body: 新文章完整URL(如 https://absorber.铁氧体.中国/articles/{slug}.html)
解析返回 JSON,打印成功/失败数量
b. 如果为空:打印 "[6/8] [skip] 无百度 token,跳过推送" ✓
步骤 7:本地备份
──────────────
a. 备份根目录 = Path(CONFIG["backup_root"]) / today / subdomain
b. 创建目录(不存在则自动创建)
c. 复制:
articles/source/{slug}.md → 备份目录/{slug}.md
articles/{slug}.html → 备份目录/html/{slug}.html
d. 同时备份被覆盖的文件(列表页、search.json、sitemap.xml)到 备份目录/system_{today}/
e. 打印备份路径和完成信息
f. 日志:[7/8] 本地备份完成 → {备份目录} ✓
步骤 8:发布成功汇总
──────────────────
打印/返回:
✅ 文章 "{title}" 发布成功!
- 详情页:articles/{slug}.html
- 列表页:已更新(共 N 篇文章)
- 搜索索引:search.json(N 条)
- 站点地图:sitemap.xml(N 个URL)
- 宝塔同步:成功 N / 失败 M
- 百度推送:success / skipped
- 本地备份:完成({备份目录})
═══════════════════════════════════════════
【相关文章逻辑】(在步骤 1 中,正文 HTML 末尾追加)
═══════════════════════════════════════════
在步骤 1 的 BeautifulSoup 操作完成后、保存 HTML 之前:
- 扫描 articles/ 下所有其他 .html(排除自己)
- 随机选取 min(CONFIG["related_count"], 可用文章数) 篇
- 从每篇提取标题(<h1> 或 <title>)
- 在正文容器末尾追加:
<div class="related-articles">
<h3>相关阅读</h3>
<ul>
<li><a href="articles/{other_slug}.html">{other_title}</a></li>
...
</ul>
</div>
- 只在生成新文章时添加,不修改任何旧文章
═══════════════════════════════════════════
【代码要求】
═══════════════════════════════════════════
1. 单文件 publisher.py
2. 依赖(文件顶部注释写清安装命令):
pip install requests markdown beautifulsoup4 pypinyin lxml
3. 全部用 pathlib 处理路径,不用 os.path
4. 所有文件写入操作:先写 .tmp 临时文件 → 再 Path.replace() 原子替换
5. CONFIG 在文件顶部,新增子域只需加一项
6. 每个步骤写成独立函数,publish() 按顺序调用
7. 每步前后打印日志:[1/8] ... [8/8]
8. 每步 try/except,失败打印错误但不中断整个流程
9. 宝塔 API 签名严格按 _sign() 规范;api_key 不打印到日志
10. Windows 中文路径兼容:全程 pathlib,不手动拼字符串路径
11. 文件编码统一 utf-8
12. 如果 articles/ 为空(第一篇),列表页 index.html 也不存在时:
自动创建一个最小 index.html(含 <!DOCTYPE html> + <head> + <body> + <ul class="article-list">)
13. slug 冲突自动追加后缀
14. 宝塔上传串行执行,不并发
═══════════════════════════════════════════
【测试入口】
═══════════════════════════════════════════
if __name__ == '__main__':
# 模拟后台"选子域 + 填标题 + 写内容 + 点发布"
test_md = """# CNC铣削参数大全
这是一篇测试文章的正文内容。
切削速度
切削速度建议根据刀具材质和工件硬度调整,一般高速钢刀具推荐 20-30 m/min,
硬质合金刀具可达 80-150 m/min。
进给量
进给量取决于切削深度和机床刚性,一般取 0.1-0.3 mm/齿。
注意事项
- 定期检查刀具磨损
- 保证冷却液充足
- 避免断续切削
"""
publish("absorber", "CNC铣削参数大全", test_md)
═══════════════════════════════════════════
【输出要求】
═══════════════════════════════════════════
1. 只输出完整可运行的 Python 代码
2. 文件顶部注释写清 pip install 命令和用法说明
3. 不要省略任何步骤
4. 不要加多余的解释文字
5. 代码要能直接在 Python 3.10+ 下运行(python publisher.py)