Skip to content

Instantly share code, notes, and snippets.

@Svtter
Created September 24, 2025 03:22
Show Gist options
  • Select an option

  • Save Svtter/c2a4635129a40513c1b8e9607341d636 to your computer and use it in GitHub Desktop.

Select an option

Save Svtter/c2a4635129a40513c1b8e9607341d636 to your computer and use it in GitHub Desktop.
这个Python脚本可以自动监视 research_summary_2025.md 文件的变化, 并在检测到文件修改时自动使用 pandoc 将其转换为格式化的 HTML 文件。
#!/usr/bin/env python3
"""
自动监视和转换脚本 - watch_and_convert.py
功能说明:
这个Python脚本可以自动监视 research_summary_2025.md 文件的变化,
并在检测到文件修改时自动使用 pandoc 将其转换为格式化的 HTML 文件。
使用方法:
# 使用 uv run(推荐)
uv run python watch_and_convert.py
# 或者如果已经安装了依赖
python watch_and_convert.py
停止监视:
按 Ctrl+C 停止监视程序
功能特性:
✅ 实时监视:使用 watchdog 库监视文件变化
✅ 自动转换:检测到变化后自动调用 pandoc 转换
✅ 自动刷新:浏览器每2秒检查文件更新并自动刷新页面
✅ 美化样式:生成带有 GitHub 风格样式的 HTML
✅ 目录生成:自动生成文档目录(TOC)
✅ 避免重复:智能防止频繁触发转换
✅ 刷新指示器:页面右上角显示自动刷新状态
✅ 错误处理:完善的错误处理和状态报告
输出文件:
输入: research_summary_2025.md
输出: research_summary_2025.html
依赖要求:
- Python 3.6+
- pandoc
- watchdog 库
工作原理:
1. 脚本启动时先进行一次初始转换
2. 使用 watchdog 监视当前目录下的 research_summary_2025.md 文件
3. 检测到文件修改事件时,等待1秒避免重复触发
4. 调用 pandoc 进行 Markdown 到 HTML 的转换
5. 添加自定义CSS样式和页脚信息
6. 输出转换结果和文件信息
注意事项:
- 确保 pandoc 已正确安装并在 PATH 中
- 脚本需要在包含 research_summary_2025.md 的目录中运行
- 使用 uv run 可以自动处理 Python 依赖管理
"""
import os
import sys
import time
import subprocess
from datetime import datetime
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
class MarkdownHandler(FileSystemEventHandler):
"""处理 Markdown 文件变化的事件处理器"""
def __init__(self, md_file, html_file):
self.md_file = Path(md_file)
self.html_file = Path(html_file)
self.last_modified = 0
def on_modified(self, event):
"""文件修改时的回调"""
if event.is_directory:
return
# 检查是否是目标文件
if Path(event.src_path).name == self.md_file.name:
# 避免频繁触发(文件保存时可能触发多次事件)
current_time = time.time()
if current_time - self.last_modified < 1:
return
self.last_modified = current_time
print(f"检测到文件变化: {event.src_path}")
self.convert_to_html()
def convert_to_html(self):
"""转换 Markdown 到 HTML"""
try:
print(f"正在转换 {self.md_file} -> {self.html_file}")
# 构建 pandoc 命令
pandoc_cmd = [
'pandoc',
str(self.md_file),
'--from', 'markdown',
'--to', 'html5',
'--standalone',
'--toc',
'--toc-depth', '3',
'--metadata', 'title=2025年3月以来研究工作总结报告',
'--metadata', 'author=研究团队',
'--metadata', f'date={datetime.now().strftime("%Y-%m-%d")}',
'--css', 'https://cdn.jsdelivr.net/npm/[email protected]/github-markdown-light.css',
'--output', str(self.html_file)
]
# 执行转换
result = subprocess.run(pandoc_cmd, capture_output=True, text=True)
if result.returncode == 0:
# 添加自定义样式
self.add_custom_styles()
file_size = self.html_file.stat().st_size
print(f"✓ 转换完成: {self.html_file}")
print(f" 文件大小: {self.format_file_size(file_size)}")
print(f" 更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
else:
print(f"✗ 转换失败: {result.stderr}")
except subprocess.CalledProcessError as e:
print(f"✗ Pandoc 执行失败: {e}")
except Exception as e:
print(f"✗ 转换过程中发生错误: {e}")
print("---")
def add_custom_styles(self):
"""为生成的 HTML 添加自定义样式"""
try:
with open(self.html_file, 'r', encoding='utf-8') as f:
content = f.read()
# 添加自定义 CSS
custom_css = """
<style>
body {
box-sizing: border-box;
min-width: 200px;
max-width: 1200px;
margin: 0 auto;
padding: 45px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
line-height: 1.6;
color: #24292f;
background-color: #ffffff;
}
@media (max-width: 767px) {
body {
padding: 15px;
}
}
h1, h2, h3, h4, h5, h6 {
color: #0969da;
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
}
h1 {
border-bottom: 1px solid #d0d7de;
padding-bottom: 0.3em;
}
h2 {
border-bottom: 1px solid #d0d7de;
padding-bottom: 0.3em;
}
code {
background-color: rgba(175,184,193,0.2);
border-radius: 6px;
font-size: 85%;
margin: 0;
padding: 0.2em 0.4em;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
}
pre {
background-color: #f6f8fa;
border-radius: 6px;
font-size: 85%;
line-height: 1.45;
overflow: auto;
padding: 16px;
margin: 16px 0;
border: 1px solid #d0d7de;
}
blockquote {
border-left: 0.25em solid #d0d7de;
color: #656d76;
padding: 0 1em;
margin: 0 0 16px 0;
}
ul, ol {
padding-left: 2em;
margin: 0 0 16px 0;
}
li {
margin: 0.25em 0;
}
table {
border-collapse: collapse;
margin: 16px 0;
width: 100%;
}
th, td {
border: 1px solid #d0d7de;
padding: 6px 13px;
text-align: left;
}
th {
background-color: #f6f8fa;
font-weight: 600;
}
#TOC {
background-color: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 16px;
margin: 16px 0;
}
#TOC h2 {
margin-top: 0;
border-bottom: none;
color: #0969da;
}
#TOC ul {
margin: 0;
padding-left: 1.5em;
}
#TOC a {
color: #0969da;
text-decoration: none;
}
#TOC a:hover {
text-decoration: underline;
}
footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #d0d7de;
color: #656d76;
font-size: 0.9em;
text-align: center;
}
strong {
color: #0969da;
}
</style>
"""
# 添加自动刷新脚本
auto_refresh_script = """
<script>
(function() {
let lastModified = null;
let checkInterval = 2000; // 检查间隔 2 秒
function checkForUpdates() {
fetch(window.location.href, {
method: 'HEAD',
cache: 'no-cache'
})
.then(response => {
const modified = response.headers.get('Last-Modified');
if (lastModified === null) {
lastModified = modified;
} else if (modified !== lastModified && modified !== null) {
console.log('文件已更新,正在刷新页面...');
window.location.reload();
}
})
.catch(error => {
// 静默处理错误,避免控制台噪音
});
}
// 页面加载完成后开始检查
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => {
setInterval(checkForUpdates, checkInterval);
}, 1000);
});
} else {
setTimeout(() => {
setInterval(checkForUpdates, checkInterval);
}, 1000);
}
// 添加页面刷新指示器
const indicator = document.createElement('div');
indicator.id = 'refresh-indicator';
indicator.innerHTML = '🔄 自动刷新已启用';
indicator.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: #0969da;
color: white;
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
z-index: 1000;
opacity: 0.7;
transition: opacity 0.3s;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
`;
// 鼠标悬停时显示更多信息
indicator.addEventListener('mouseenter', function() {
this.innerHTML = '🔄 每2秒检查更新';
this.style.opacity = '1';
});
indicator.addEventListener('mouseleave', function() {
this.innerHTML = '🔄 自动刷新已启用';
this.style.opacity = '0.7';
});
// 页面加载完成后添加指示器
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
document.body.appendChild(indicator);
setTimeout(() => {
indicator.style.opacity = '0.5';
}, 3000);
});
} else {
document.body.appendChild(indicator);
setTimeout(() => {
indicator.style.opacity = '0.5';
}, 3000);
}
})();
</script>
"""
# 在 </head> 前插入自定义样式和自动刷新脚本
content = content.replace('</head>', f'{custom_css}\n{auto_refresh_script}\n</head>')
# 添加页脚信息
footer = f"""
<footer>
<p><small>文档生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |
转换工具: Pandoc + Python watchdog</small></p>
</footer>
"""
content = content.replace('</body>', f'{footer}\n</body>')
with open(self.html_file, 'w', encoding='utf-8') as f:
f.write(content)
except Exception as e:
print(f"警告: 添加自定义样式时出错: {e}")
@staticmethod
def format_file_size(size_bytes):
"""格式化文件大小"""
if size_bytes == 0:
return "0B"
size_names = ["B", "KB", "MB", "GB"]
i = 0
while size_bytes >= 1024.0 and i < len(size_names) - 1:
size_bytes /= 1024.0
i += 1
return f"{size_bytes:.1f}{size_names[i]}"
def check_dependencies():
"""检查依赖"""
# 检查 pandoc
try:
subprocess.run(['pandoc', '--version'], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("错误: pandoc 未安装或不在 PATH 中")
print("请安装 pandoc:")
print(" Ubuntu/Debian: sudo apt-get install pandoc")
print(" macOS: brew install pandoc")
print(" Windows: https://pandoc.org/installing.html")
return False
# 检查 watchdog
try:
import watchdog
except ImportError:
print("错误: watchdog 库未安装")
print("请安装: pip install watchdog")
return False
return True
def main():
"""主函数"""
# 检查依赖
if not check_dependencies():
sys.exit(1)
# 文件路径
md_file = 'research_summary_2025.md'
html_file = 'research_summary_2025.html'
# 检查源文件是否存在
if not os.path.exists(md_file):
print(f"错误: 文件 {md_file} 不存在")
sys.exit(1)
# 创建事件处理器
event_handler = MarkdownHandler(md_file, html_file)
# 首次转换
print(f"开始监视文件: {md_file}")
print(f"HTML 输出: {html_file}")
print("按 Ctrl+C 停止监视")
print("---")
event_handler.convert_to_html()
# 设置观察者
observer = Observer()
observer.schedule(event_handler, path='.', recursive=False)
# 开始监视
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n收到停止信号,正在退出...")
observer.stop()
print("✓ 监视已停止")
observer.join()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment