Created
September 24, 2025 03:22
-
-
Save Svtter/c2a4635129a40513c1b8e9607341d636 to your computer and use it in GitHub Desktop.
这个Python脚本可以自动监视 research_summary_2025.md 文件的变化, 并在检测到文件修改时自动使用 pandoc 将其转换为格式化的 HTML 文件。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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