目标:通过 Python 脚本自动生成 Hexo 静态文件,并通过 SFTP 免密上传到服务器,支持单服务器部署,后续可扩展多服务器。
一、前置条件
-
本地环境:
-
服务器环境:
- 一台 Linux 服务器(如 CentOS、Ubuntu),已安装 OpenSSH 服务(默认已安装)
- 服务器上已配置网站根目录(如
/opt/1panel/apps/openresty/.../public) - 服务器 IP、SSH 端口(默认 22)、登录用户名(如 root)
二、配置 SSH 免密登录(核心步骤)
通过 SSH 密钥对实现本地与服务器的免密通信,避免每次上传输入密码。
1. 本地生成 ED25519 密钥对
ED25519 是更安全的密钥算法,推荐优先使用。
- 打开本地 Windows 命令提示符(CMD)或 PowerShell,执行以下命令:
# 生成密钥对,替换为你的私钥保存路径(建议放在 .ssh 目录下)
ssh-keygen -t ed25519 -f C:/Users/你的用户名/.ssh/hexo_deploy_key -N ""
- 解释:
- `-t ed25519`:指定密钥算法为 ED25519
- `-f 路径`:私钥保存路径(如 `C:/Users/Administrator/.ssh/hexo_deploy_key`)
- `-N ""`:设置空密码(免密登录)
-
执行后会生成两个文件:
hexo_deploy_key:私钥(本地保存,不可泄露)hexo_deploy_key.pub:公钥(需要上传到服务器)
2. 将公钥上传到服务器
让服务器信任本地私钥,实现免密登录:
- 方法 1:手动复制(推荐)
- 本地查看公钥内容:
type C:/Users/你的用户名/.ssh/hexo_deploy_key.pub
复制输出的完整内容(以 ssh-ed25519 开头的一整行)。
2. 登录服务器(用密码登录):
ssh 用户名@服务器IP # 如 ssh root@103.207.68.122
3. 服务器端创建授权文件并添加公钥:
# 确保 .ssh 目录存在并设置权限
mkdir -p ~/.ssh && chmod 700 ~/.ssh
# 创建授权文件并添加公钥(粘贴本地复制的公钥内容)
echo "粘贴的公钥内容" >> ~/.ssh/authorized_keys
# 设置文件权限(必须为 600,否则 SSH 会拒绝)
chmod 600 ~/.ssh/authorized_keys
- 验证免密登录:
本地执行以下命令,若无需输入密码即可登录服务器,则配置成功:
ssh -i C:/Users/你的用户名/.ssh/hexo_deploy_key 用户名@服务器IP
三、部署脚本配置
使用支持 SFTP 协议的 Python 脚本,实现 “本地生成静态文件 + 自动上传服务器” 一键操作。
1. 下载脚本
创建 deploy.py 文件,复制以下代码(已适配 ED25519 密钥,支持单服务器):
Hexo 自动部署脚本
import os
import paramiko
import subprocess
from paramiko.ssh_exception import SSHException
# --------------------------
# 服务器配置(根据实际情况修改)
# --------------------------
SERVERS = [
{
"name": "主服务器", # 服务器名称(用于日志区分)
"ip": "103.207.68.122", # 替换为你的服务器IP
"port": 22, # SSH端口(默认22)
"user": "root", # 服务器登录用户名
"site_dir": "/opt/1panel/apps/openresty/openresty/www/sites/blog.mzxi.cn/index/public", # 服务器网站根目录
"private_key": "C:/Users/Administrator/.ssh/hexo_deploy_key" # 本地私钥路径
}
# 如需多服务器,取消下面注释并修改配置
# ,{
# "name": "备用服务器",
# "ip": "服务器IP",
# "port": 22,
# "user": "root",
# "site_dir": "/var/www/blog",
# "private_key": "C:/Users/Administrator/.ssh/hexo_deploy_key"
# }
]
# 本地 Hexo 静态文件目录(默认 ./public,无需修改)
LOCAL_PUBLIC_DIR = "./public"
def run_command(cmd, description):
"""执行本地命令(生成 Hexo 静态文件)"""
print(f"\n=== {description} ===")
try:
result = subprocess.run(
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="ignore"
)
if result.stdout:
print("成功日志(关键):", result.stdout.strip()[-500:])
print(f"✅ {description} 完成")
return True
except Exception as e:
print(f"❌ {description} 失败!错误:{str(e)}")
input("\n按任意键退出...")
return False
def upload_to_server(server):
"""上传文件到单个服务器"""
print(f"\n===== 开始上传到【{server['name']}】=====")
try:
# 加载 ED25519 私钥
try:
private_key = paramiko.Ed25519Key.from_private_key_file(server["private_key"])
print(f"✅ 【{server['name']}】私钥加载成功")
except Exception as key_err:
print(f"❌ 【{server['name']}】私钥加载失败:{str(key_err)}")
return False
# 建立 SFTP 连接
try:
transport = paramiko.Transport((server["ip"], server["port"]))
transport.connect(username=server["user"], pkey=private_key)
sftp = paramiko.SFTPClient.from_transport(transport)
print(f"✅ 【{server['name']}】SFTP 连接成功")
except SSHException as ssh_err:
print(f"❌ 【{server['name']}】SSH 连接失败:{str(ssh_err)}")
return False
# 确保服务器网站目录存在
try:
sftp.stat(server["site_dir"])
except FileNotFoundError:
print(f"⚠️ 【{server['name']}】创建目录:{server['site_dir']}")
sftp.mkdir(server["site_dir"])
# 递归上传文件(全量覆盖)
def upload_dir(local_path, remote_path):
for item in os.listdir(local_path):
local_item = os.path.join(local_path, item)
remote_item = f"{remote_path}/{item}"
if os.path.isdir(local_item):
# 处理子目录
try:
sftp.stat(remote_item)
except FileNotFoundError:
sftp.mkdir(remote_item)
upload_dir(local_item, remote_item)
else:
# 上传文件
sftp.put(local_item, remote_item)
if upload_dir.counter % 10 == 0:
print(f"📤 【{server['name']}】已上传 {upload_dir.counter} 个文件...")
upload_dir.counter += 1
upload_dir.counter = 1
upload_dir(LOCAL_PUBLIC_DIR, server["site_dir"])
# 关闭连接
sftp.close()
transport.close()
print(f"✅ 【{server['name']}】上传完成(共 {upload_dir.counter-1} 个文件)")
return True
except Exception as e:
print(f"❌ 【{server['name']}】上传失败:{str(e)}")
return False
if __name__ == "__main__":
print("===== Hexo 部署流程开始 =====")
# 1. 本地生成静态文件(hexo clean + generate)
if not run_command("hexo clean && hexo generate", "本地生成静态文件"):
exit(1)
# 2. 上传到所有配置的服务器(当前仅主服务器)
all_success = True
for server in SERVERS:
if not upload_to_server(server):
all_success = False
# 3. 输出最终结果
if all_success:
print("\n===== 部署成功!网站已更新 =====")
else:
print("\n===== 部署失败,请查看日志 =====")
input("按任意键退出...")
2. 修改配置项
打开 deploy.py,根据你的实际环境修改以下参数:
SERVERS列表中的ip:服务器公网 IPsite_dir:服务器上存放博客静态文件的目录(如 1Panel 或 Nginx 配置的网站根目录)private_key:本地私钥hexo_deploy_key的绝对路径(如C:/Users/Administrator/.ssh/hexo_deploy_key)
四、安装依赖并运行脚本
1. 安装 Python 依赖
脚本需要 paramiko 库实现 SFTP 功能,本地执行:
# 打开 CMD,进入博客目录
cd E:/website/hexo/blog # 替换为你的博客目录
# 安装依赖
pip install paramiko
2.运行部署脚本
# 在博客目录下执行
python deploy.py
3. 成功标志
脚本输出以下内容即表示部署成功:
===== Hexo 部署流程开始 =====
=== 本地生成静态文件 ===
...(Hexo 生成日志)
✅ 本地生成静态文件 完成
===== 开始上传到【主服务器】=====
✅ 【主服务器】私钥加载成功
✅ 【主服务器】SFTP 连接成功
📤 【主服务器】已上传 10 个文件...
...(中间进度)
✅ 【主服务器】上传完成(共 170 个文件)
===== 部署成功!网站已更新 =====
按任意键退出...
五、常见问题排查
-
“ModuleNotFoundError: No module named ‘paramiko’”
→ 未安装依赖,执行pip install paramiko即可。 -
“私钥加载失败” 或 “unpack requires a buffer of 4 bytes”
→ 私钥路径错误或格式不对:- 检查
private_key路径是否正确(区分/和\,建议用/)。 - 确保私钥是 ED25519 类型(开头为
-----BEGIN OPENSSH PRIVATE KEY-----)。
- 检查
-
“SSH 连接失败” 或 “Connection refused”
→ 服务器 IP、端口错误,或 SSH 服务未启动:- 验证服务器 IP 和端口是否可通(用
ping 服务器IP测试网络)。 - 服务器端执行
systemctl status sshd确认 SSH 服务已启动。
- 验证服务器 IP 和端口是否可通(用
-
“服务器目录不存在”
→ 脚本会自动创建目录,若创建失败(权限不足),需手动在服务器创建目录并设置权限:
mkdir -p /opt/1panel/apps/openresty/.../public # 替换为你的 site_dir
chmod 755 /opt/1panel/apps/openresty/.../public
六、扩展说明
- 多服务器部署:取消
SERVERS列表中第二个服务器的注释,修改对应参数即可。 - 增量上传:如需只上传修改的文件,可在
upload_dir函数中添加文件修改时间对比逻辑(参考前文)。 - 自动化优化:可将脚本添加到 Hexo 部署钩子(如
hexo deploy命令),实现更无缝的流程。
通过以上步骤,即可实现 Hexo 博客的一键生成与部署,无需手动上传文件,大幅提升更新效率。