first commit

This commit is contained in:
cuianbing
2026-01-14 21:27:38 +08:00
commit 735abf2d32
5 changed files with 645 additions and 0 deletions

23
Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
# 使用官方Python镜像作为基础镜像
FROM python:3.9-slim
# 设置工作目录
WORKDIR /app
# 复制requirements.txt文件到工作目录
COPY requirements.txt .
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制所有项目文件到工作目录
COPY . .
# 创建目标目录
RUN mkdir -p /opt/doc/_post
# 设置环境变量
ENV PYTHONUNBUFFERED=1
# 运行主程序
CMD ["python", "auto_check_files.py"]

140
README.md Normal file
View File

@@ -0,0 +1,140 @@
# Markdown文件自动处理与定时推送系统
## 功能说明
本系统用于自动处理Markdown文件将24小时内更新的文件复制到指定目录并提交Git变更然后定时推送到远程仓库。
## 主要功能
1. **遍历目录**遍历指定目录下的所有Markdown文件包含子文件夹
2. **解析frontmatter**解析每个文件的YAML frontmatter属性
3. **检查更新时间**优先使用frontmatter中的`updated`字段,否则使用文件修改时间
4. **筛选24小时内更新的文件**只处理最近24小时内更新的文件
5. **同名文件处理**支持三种策略overwrite(覆盖), skip(跳过), rename(重命名)
6. **Git提交**:自动提交变更,提交信息格式:`feat: 📝 {title}`
7. **定时推送**每天早上六点、中午12点、晚上零点推送到远程仓库
## 文件说明
- `parse_frontmatter.py`主脚本用于处理Markdown文件
- `push_to_remote.py`:推送脚本,用于将本地提交推送到远程仓库
- `push_to_remote.bat`Windows批处理文件用于运行推送脚本
- `push_to_remote.sh`Linux shell脚本用于运行推送脚本
- `example.md`示例Markdown文件
## 配置说明
### 1. 添加远程仓库
在目标目录下执行以下命令添加远程仓库使用SSH协议
```bash
git remote add origin git@github.com:username/repo.git
```
### 2. SSH密钥配置
确保SSH密钥已正确配置密钥文件应位于`~/.ssh/`目录下:
- 检查SSH密钥是否存在
```bash
ls -la ~/.ssh/
```
- 如果没有SSH密钥生成新密钥
```bash
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
```
- 将公钥添加到GitHub/GitLab等平台
### 3. 定时任务配置
#### Windows系统使用任务计划程序
1. 打开"任务计划程序"(搜索"Task Scheduler"
2. 点击"创建基本任务"
3. 输入任务名称和描述
4. 选择"每天"触发器
5. 设置起始时间为"00:00:00",重复任务间隔为"12"小时,持续时间为"1天"
6. 选择"启动程序"操作
7. 浏览并选择`push_to_remote.bat`文件
8. 勾选"当单击完成时,打开此任务属性的对话框"
9. 切换到"触发器"选项卡,点击"新建"
10. 设置另一个触发器为"每天",起始时间为"12:00:00"
11. 点击"确定"完成配置
#### Linux系统使用cron
1. 打开cron配置文件
```bash
crontab -e
```
2. 添加以下行,设置每天零点、六点、十二点执行推送脚本:
```bash
0 0 * * * /path/to/push_to_remote.sh
0 6 * * * /path/to/push_to_remote.sh
0 12 * * * /path/to/push_to_remote.sh
```
3. 保存并退出对于vim编辑器按`Esc`,然后输入`:wq`
### 4. 脚本参数配置
可以根据需要修改脚本中的默认参数:
- **parse_frontmatter.py**
- `source_dir`:源目录路径
- `destination_dir`:目标目录路径
- `overwrite_strategy`:同名文件处理策略
- **push_to_remote.py**
- `repo_path`Git仓库路径
- `remote_name`:远程仓库名称
- `branch`:分支名称
## 使用说明
1. **运行主脚本**
```bash
python parse_frontmatter.py
```
2. **手动运行推送脚本**
- Windows
```bash
push_to_remote.bat
```
- Linux
```bash
chmod +x push_to_remote.sh
./push_to_remote.sh
```
## 注意事项
1. 确保Python环境已正确安装并且已安装所需依赖
```bash
pip install pyyaml gitpython
```
2. 确保目标目录已初始化Git仓库或者主脚本会自动初始化
3. 确保已正确配置SSH密钥并且有权限访问远程仓库
4. 定时任务运行时,确保系统处于开机状态
## 日志说明
脚本会输出详细的日志信息,包括:
- 处理的文件数量
- 成功复制的文件数量
- 提交的Git变更
- 推送到远程仓库的结果
可以根据需要将日志重定向到文件,以便查看历史记录:
```bash
python parse_frontmatter.py > log.txt 2>&1
```

471
auto_check_files.py Normal file
View File

@@ -0,0 +1,471 @@
import os
from pickle import FALSE
import yaml
import shutil
import datetime
import configparser
from typing import Dict, List, Optional
from git import Repo
from apscheduler.schedulers.blocking import BlockingScheduler
# 配置文件路径,使用绝对路径确保能找到
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.ini')
# 全局变量,用于存储配置
config = configparser.ConfigParser()
# 全局变量,用于存储上一次检查的时间
last_check_time = None
def parse_markdown_frontmatter(file_path: str) -> Dict:
"""
读取Markdown文件并解析其frontmatter YAML属性
Args:
file_path (str): Markdown文件的路径
Returns:
dict: 解析后的frontmatter属性字典如果没有frontmatter则返回空字典
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 检查文件是否以frontmatter开头
if not content.startswith('---'):
return {}
# 找到frontmatter的结束位置
end_index = content.find('---', 3)
if end_index == -1:
return {}
# 提取frontmatter内容
frontmatter_content = content[3:end_index].strip()
# 解析YAML
frontmatter = yaml.safe_load(frontmatter_content)
# 确保返回的是字典类型
if frontmatter is None:
frontmatter = {}
return frontmatter
except FileNotFoundError:
print(f"文件 {file_path} 不存在")
return {}
except yaml.YAMLError as e:
print(f"解析文件 {file_path} 的frontmatter时出错: {e}")
return {}
except Exception as e:
print(f"处理文件 {file_path} 时出错: {e}")
return {}
def is_updated_since_last_check(frontmatter: Dict, file_path: str, last_check: datetime.datetime) -> bool:
"""
检查文件是否在上一次检查时间之后更新
Args:
frontmatter (dict): 文件的frontmatter属性
file_path (str): 文件路径
last_check (datetime.datetime): 上一次检查的时间
Returns:
bool: 如果文件在上一次检查时间之后更新则返回True否则返回False
"""
# 尝试从frontmatter获取updated时间
updated_value = frontmatter.get('updated')
# 如果frontmatter没有updated字段使用文件的修改时间
if not updated_value:
try:
mtime = os.path.getmtime(file_path)
updated_dt = datetime.datetime.fromtimestamp(mtime)
except OSError as e:
print(f"无法获取文件 {file_path} 的修改时间: {e}")
return False
else:
# 检查updated_value是否已经是datetime对象
if isinstance(updated_value, datetime.datetime):
updated_dt = updated_value
else:
# 解析updated字符串为datetime对象
try:
# 支持多种时间格式
date_formats = [
'%Y-%m-%d %H:%M:%S',
'%Y-%m-%d %H:%M',
'%Y-%m-%d',
'%Y/%m/%d %H:%M:%S',
'%Y/%m/%d %H:%M',
'%Y/%m/%d'
]
updated_dt = None
for fmt in date_formats:
try:
updated_dt = datetime.datetime.strptime(updated_value, fmt)
break
except ValueError:
continue
if updated_dt is None:
print(f"文件 {file_path} 的updated时间格式不正确: {updated_value}")
return False
except Exception as e:
print(f"解析文件 {file_path} 的updated时间时出错: {e}")
return False
# 检查是否在上一次检查时间之后更新
return updated_dt >= last_check
def copy_file_to_destination(source_path: str, destination_dir: str, repo: Repo, frontmatter: Dict) -> bool:
"""
将文件复制到目标目录并提交Git变更
Args:
source_path (str): 源文件路径
destination_dir (str): 目标目录路径
repo (Repo): Git仓库对象
frontmatter (dict): 文件的frontmatter属性
Returns:
bool: 复制和提交成功返回True否则返回False
"""
try:
# 从配置文件获取同名文件处理策略
overwrite_strategy = config['DEFAULT'].get('overwrite_strategy', 'overwrite')
# 确保目标目录存在
if not os.path.exists(destination_dir):
os.makedirs(destination_dir, exist_ok=True)
print(f"创建目标目录: {destination_dir}")
# 获取文件名和目标路径
file_name = os.path.basename(source_path)
file_base, file_ext = os.path.splitext(file_name)
destination_path = os.path.join(destination_dir, file_name)
existsResult = FALSE
# 处理同名文件
if os.path.exists(destination_path):
existsResult = True
if overwrite_strategy == 'skip':
print(f"跳过已存在文件: {destination_path}")
return False
elif overwrite_strategy == 'rename':
# 生成带时间戳的新文件名
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
new_file_name = f"{file_base}_{timestamp}{file_ext}"
destination_path = os.path.join(destination_dir, new_file_name)
print(f"文件已存在,重命名为: {new_file_name}")
# 默认overwrite策略直接覆盖
# 复制文件,保留元数据
shutil.copy2(source_path, destination_path)
print(f"成功拷贝: {source_path} -> {destination_path}")
# 获取提交信息
title = frontmatter.get('title', file_name)
if existsResult:
commit_message = f"update: 📝 更新 《{title}》文章内容"
else:
commit_message = f"feat: 📝 新建《{title}》文章"
# 添加文件到Git暂存区
repo.git.add(".")
# 提交变更
repo.git.commit('-m', commit_message)
print(f"成功提交: {commit_message}")
return True
except Exception as e:
print(f"拷贝或提交失败 {source_path}: {e}")
return False
def check_and_process_files(repo: Repo) -> Dict:
"""
检查并处理指定目录下的所有Markdown文件将上一次检查后更新的文件复制到目标目录并提交Git变更
Args:
repo (Repo): Git仓库对象
Returns:
dict: 处理结果统计
"""
global last_check_time
# 从配置文件获取参数
source_dir = config['DEFAULT'].get('source_dir')
destination_dir = config['DEFAULT'].get('destination_dir')
overwrite_strategy = config['DEFAULT'].get('overwrite_strategy', 'overwrite')
# 替换为当前目录进行测试(如果默认目录不存在)
if not os.path.exists(source_dir):
source_dir = os.getcwd()
print(f"默认源目录不存在,使用当前目录进行测试: {source_dir}")
# 确保目标目录存在
if not os.path.exists(destination_dir):
os.makedirs(destination_dir, exist_ok=True)
print(f"创建目标目录: {destination_dir}")
stats = {
'total_files': 0,
'md_files': 0,
'with_frontmatter': 0,
'updated_since_last_check': 0,
'copied_successfully': 0,
'copy_failed': 0
}
current_time = datetime.datetime.now()
print(f"\n{'='*80}")
print(f"开始检查文件,时间: {current_time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"上一次检查时间: {last_check_time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"源目录: {source_dir}")
print(f"目标目录: {destination_dir}")
print(f"同名文件处理策略: {overwrite_strategy}")
print(f"{'='*80}")
# 遍历源目录下的所有文件,忽略以`.`开头的文件夹
for root, dirs, files in os.walk(source_dir):
# 过滤掉以`.`开头的文件夹
dirs[:] = [d for d in dirs if not d.startswith('.')]
for file in files:
# 过滤掉以`.`开头的文件
if file.startswith('.'):
continue
# 只处理Markdown文件
if not file.lower().endswith('.md'):
continue
stats['total_files'] += 1
stats['md_files'] += 1
file_path = os.path.join(root, file)
# 解析frontmatter
frontmatter = parse_markdown_frontmatter(file_path)
if frontmatter:
stats['with_frontmatter'] += 1
# 检查是否在上一次检查时间之后更新
if is_updated_since_last_check(frontmatter, file_path, last_check_time):
stats['updated_since_last_check'] += 1
# 复制到目标目录并提交
if copy_file_to_destination(file_path, destination_dir, repo, frontmatter):
stats['copied_successfully'] += 1
else:
stats['copy_failed'] += 1
else:
# 对于没有frontmatter的文件创建一个默认的frontmatter
default_frontmatter = {
'title': os.path.basename(file_path)
}
# 使用文件修改时间判断
try:
mtime = os.path.getmtime(file_path)
updated_dt = datetime.datetime.fromtimestamp(mtime)
if updated_dt >= last_check_time:
stats['updated_since_last_check'] += 1
if copy_file_to_destination(file_path, destination_dir, repo, default_frontmatter):
stats['copied_successfully'] += 1
else:
stats['copy_failed'] += 1
except Exception as e:
print(f"无法处理文件 {file_path}: {e}")
# 更新最后检查时间
last_check_time = current_time
# 打印统计结果
print(f"\n{'='*80}")
print("处理结果统计:")
print(f"总文件数: {stats['total_files']}")
print(f"Markdown文件数: {stats['md_files']}")
print(f"包含frontmatter的文件数: {stats['with_frontmatter']}")
print(f"上一次检查后更新的文件数: {stats['updated_since_last_check']}")
print(f"成功复制的文件数: {stats['copied_successfully']}")
print(f"复制失败的文件数: {stats['copy_failed']}")
print(f"{'='*80}")
return stats
def load_config():
"""
从配置文件中读取配置,并为缺失的参数设置默认值
"""
global config, last_check_time
# 读取配置文件
config.read(CONFIG_FILE)
# 确保DEFAULT section存在
if 'DEFAULT' not in config:
config['DEFAULT'] = {}
# 设置默认值
default_config = {
'source_dir': '/opt/src',
'destination_dir': '/opt/doc/_post',
'overwrite_strategy': 'overwrite',
'remote_name': 'origin',
'branch': 'master'
}
# 更新配置文件中的缺失参数
for key, value in default_config.items():
if key not in config['DEFAULT'] or not config['DEFAULT'][key]:
config['DEFAULT'][key] = value
# 获取上一次检查时间
last_check_time_str = config['DEFAULT'].get('last_check_time')
if last_check_time_str:
last_check_time = datetime.datetime.strptime(last_check_time_str, '%Y-%m-%d %H:%M:%S')
else:
last_check_time = datetime.datetime.now()
# 将默认的上一次检查时间写入配置文件
config['DEFAULT']['last_check_time'] = last_check_time.strftime('%Y-%m-%d %H:%M:%S')
print(f"从配置文件中读取配置成功")
print(f"上一次检查时间: {last_check_time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"源目录: {config['DEFAULT'].get('source_dir')}")
print(f"目标目录: {config['DEFAULT'].get('destination_dir')}")
print(f"同名文件处理策略: {config['DEFAULT'].get('overwrite_strategy')}")
print(f"远程仓库名称: {config['DEFAULT'].get('remote_name')}")
print(f"分支名称: {config['DEFAULT'].get('branch')}")
def save_config():
"""
将配置写入配置文件,特别是更新上一次检查时间
"""
global config, last_check_time
# 更新上一次检查时间
config['DEFAULT']['last_check_time'] = last_check_time.strftime('%Y-%m-%d %H:%M:%S')
# 写入配置文件
with open(CONFIG_FILE, 'w') as f:
config.write(f)
print(f"配置文件保存成功,上一次检查时间更新为: {last_check_time.strftime('%Y-%m-%d %H:%M:%S')}")
def push_to_remote(repo: Repo) -> bool:
"""
将本地提交推送到远程仓库
Args:
repo (Repo): Git仓库对象
Returns:
bool: 推送成功返回True否则返回False
"""
try:
# 从配置文件获取远程仓库名称和分支名称
remote_name = config['DEFAULT'].get('remote_name', 'origin')
branch = config['DEFAULT'].get('branch', 'master')
# 检查远程仓库是否存在
if remote_name not in repo.remotes:
print(f"远程仓库 {remote_name} 不存在,无法推送")
return False
# 推送到远程仓库
repo.git.push(remote_name, branch)
print(f"成功推送到远程仓库 {remote_name}{branch} 分支")
return True
except Exception as e:
print(f"推送失败: {e}")
return False
def job():
"""
定时任务函数,执行文件检查和处理
"""
global last_check_time
# 从配置文件获取参数
source_dir = config['DEFAULT'].get('source_dir')
destination_dir = config['DEFAULT'].get('destination_dir')
# 替换为当前目录进行测试(如果默认目录不存在)
if not os.path.exists(source_dir):
source_dir = os.getcwd()
destination_dir = os.path.join(source_dir, 'recent_posts')
print(f"默认源目录不存在,使用当前目录进行测试: {source_dir}")
# 初始化或打开Git仓库
try:
# 检查目标目录是否已经是Git仓库
if os.path.exists(os.path.join(destination_dir, '.git')):
repo = Repo(destination_dir)
print(f"打开现有Git仓库: {destination_dir}")
else:
# 初始化新的Git仓库
repo = Repo.init(destination_dir)
print(f"初始化新Git仓库: {destination_dir}")
# 检查并处理文件
check_and_process_files(repo)
# 更新上一次检查时间
last_check_time = datetime.datetime.now()
# 保存配置,特别是更新上一次检查时间
save_config()
# 推送到远程仓库
push_to_remote(repo)
print("任务完成!")
except Exception as e:
print(f"Git操作失败: {e}")
print("任务失败!")
def main():
"""
主函数,启动定时任务
"""
# 加载配置文件
load_config()
# 立即执行一次检查
print(f"\n立即执行第一次检查")
job()
# 创建调度器
scheduler = BlockingScheduler()
# 添加定时任务每隔5分钟执行一次
scheduler.add_job(job, 'interval', minutes=5)
print(f"\n定时任务已启动每隔5分钟执行一次")
print(f"下次执行时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"按 Ctrl+C 停止任务")
try:
# 启动调度器
scheduler.start()
except KeyboardInterrupt:
print(f"\n定时任务已停止")
except Exception as e:
print(f"定时任务执行出错: {e}")
if __name__ == "__main__":
main()

8
config.ini Normal file
View File

@@ -0,0 +1,8 @@
[DEFAULT]
source_dir = source
destination_dir = recent_posts
overwrite_strategy = overwrite
remote_name = origin
branch = master
last_check_time = 2026-01-10 15:13:57

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
pyyaml>=6.0
gitpython>=3.1.0
apscheduler>=3.10.0