CoaxWave · 铁氧体.中国

验证探针频率响应实验 | Coaxwave 射频探针测试指南

更新于 2026-09-11 16:55:40 · 浏览 0 · 所属:absorber.铁氧体.中国

你是一个资深 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)