1. 项目背景与需求分析
最近在整理项目文档时,我发现一个令人头疼的问题:分散在不同文件夹中的Markdown文件里嵌入了大量有价值的代码片段,这些片段被三个反引号(```)包裹着。手动逐个文件查找提取不仅效率低下,而且容易遗漏。于是,我决定开发一个能够自动提取这些代码块的工具。
这个需求其实非常普遍:
- 开发者需要从技术文档中提取示例代码
- 教学人员想要收集讲义中的编程案例
- 技术写作者需要整理分散的代码片段
- 项目管理者希望统一检查文档中的代码规范
2. 技术方案设计
2.1 核心功能规划
工具需要实现以下核心功能:
- 递归扫描指定目录下的所有Markdown文件
- 识别Markdown中的代码块(以```开头和结尾)
- 提取代码内容并保留语言类型信息
- 将提取结果按原始文件结构组织输出
- 支持自定义输出格式和过滤条件
2.2 技术选型
经过评估,我选择了Python作为开发语言,主要基于以下考虑:
- 丰富的文件处理库(os, glob等)
- 强大的正则表达式支持
- 跨平台兼容性
- 简洁的语法和快速的开发周期
关键依赖库:
pathlib:现代化路径操作re:正则表达式匹配argparse:命令行参数解析pygments(可选):代码高亮输出
3. 实现细节与核心代码
3.1 代码块识别算法
Markdown代码块的识别采用正则表达式匹配:
python复制import re
CODE_BLOCK_PATTERN = re.compile(
r'```(?P<language>\w*)\n(?P<code>.*?)```',
re.DOTALL | re.MULTILINE
)
def extract_code_blocks(content):
"""从Markdown内容中提取所有代码块"""
return [
(match.group('language'), match.group('code').strip())
for match in CODE_BLOCK_PATTERN.finditer(content)
]
这个正则表达式会:
- 匹配以```开头的代码块
- 捕获可选的编程语言标识
- 提取代码内容直到遇到结束的```
- 返回包含语言和代码的元组列表
3.2 文件系统遍历
实现递归文件搜索功能:
python复制from pathlib import Path
def find_markdown_files(root_dir):
"""递归查找目录下所有Markdown文件"""
return Path(root_dir).rglob('*.md')
3.3 结果输出处理
提供多种输出格式选项:
python复制def save_results(results, output_format='txt'):
if output_format == 'txt':
# 纯文本输出
with open('extracted_codes.txt', 'w') as f:
for file_path, blocks in results.items():
f.write(f"=== {file_path} ===\n")
for lang, code in blocks:
f.write(f"\n--- {lang or 'plain'} ---\n")
f.write(code + '\n')
elif output_format == 'html':
# HTML带语法高亮
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
with open('extracted_codes.html', 'w') as f:
f.write('<html><head><style>'
+ HtmlFormatter().get_style_defs('.highlight')
+ '</style></head><body>')
for file_path, blocks in results.items():
f.write(f'<h2>{file_path}</h2>')
for lang, code in blocks:
lexer = get_lexer_by_name(lang or 'text')
f.write(f'<h3>{lang or "plain"}</h3>')
f.write(highlight(code, lexer, HtmlFormatter()))
f.write('</body></html>')
4. 完整实现代码
python复制#!/usr/bin/env python3
import re
from pathlib import Path
import argparse
from collections import defaultdict
CODE_BLOCK_PATTERN = re.compile(
r'```(?P<language>\w*)\n(?P<code>.*?)```',
re.DOTALL | re.MULTILINE
)
def extract_code_blocks(content):
return [
(match.group('language'), match.group('code').strip())
for match in CODE_BLOCK_PATTERN.finditer(content)
]
def find_markdown_files(root_dir):
return Path(root_dir).rglob('*.md')
def process_files(root_dir):
results = defaultdict(list)
for md_file in find_markdown_files(root_dir):
try:
content = md_file.read_text(encoding='utf-8')
blocks = extract_code_blocks(content)
if blocks:
results[str(md_file.relative_to(root_dir))] = blocks
except Exception as e:
print(f"Error processing {md_file}: {e}")
return results
def save_results(results, output_format='txt'):
if output_format == 'txt':
with open('extracted_codes.txt', 'w', encoding='utf-8') as f:
for file_path, blocks in results.items():
f.write(f"=== {file_path} ===\n")
for lang, code in blocks:
f.write(f"\n--- {lang or 'plain'} ---\n")
f.write(code + '\n')
elif output_format == 'html':
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
with open('extracted_codes.html', 'w', encoding='utf-8') as f:
f.write('''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Extracted Code Blocks</title>
<style>
''' + HtmlFormatter().get_style_defs('.highlight') + '''
</style>
</head>
<body>
''')
for file_path, blocks in results.items():
f.write(f'<h2>{file_path}</h2>')
for lang, code in blocks:
try:
lexer = get_lexer_by_name(lang or 'text')
f.write(f'<h3>{lang or "plain"}</h3>')
f.write(highlight(code, lexer, HtmlFormatter()))
except:
f.write(f'<h3>{lang or "plain"}</h3>')
f.write(f'<pre><code>{code}</code></pre>')
f.write('</body></html>')
def main():
parser = argparse.ArgumentParser(
description='Extract code blocks from Markdown files'
)
parser.add_argument('root_dir', help='Root directory to search')
parser.add_argument('-f', '--format',
choices=['txt', 'html'],
default='txt',
help='Output format')
args = parser.parse_args()
results = process_files(args.root_dir)
save_results(results, args.format)
print(f"Extracted {sum(len(v) for v in results.values())} "
f"code blocks from {len(results)} files")
if __name__ == '__main__':
main()
5. 使用指南与示例
5.1 基本使用方法
- 安装依赖(可选):
bash复制pip install pygments
- 运行工具:
bash复制python md_code_extractor.py /path/to/docs -f html
5.2 实际应用示例
假设我们有以下目录结构:
code复制docs/
├── api.md
├── examples/
│ ├── basic.md
│ └── advanced.md
└── README.md
执行提取后,会生成包含所有代码块的HTML文件,保持原始文件结构:
html复制<h2>api.md</h2>
<h3>python</h3>
<div class="highlight"><pre><code>def api_call():
return "Hello"</code></pre></div>
<h2>examples/basic.md</h2>
<h3>javascript</h3>
<div class="highlight"><pre><code>console.log('Basic example')</code></pre></div>
6. 高级功能与优化
6.1 代码块过滤
可以扩展工具支持按语言过滤:
python复制def filter_blocks(blocks, languages=None):
if not languages:
return blocks
return [
(lang, code) for lang, code in blocks
if lang in languages
]
6.2 并行处理
对于大量文件,可以使用多线程加速:
python复制from concurrent.futures import ThreadPoolExecutor
def process_file(md_file, root_dir):
try:
content = md_file.read_text(encoding='utf-8')
return str(md_file.relative_to(root_dir)), extract_code_blocks(content)
except Exception as e:
print(f"Error processing {md_file}: {e}")
return None
def process_files_parallel(root_dir, max_workers=4):
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
executor.submit(process_file, md_file, root_dir)
for md_file in find_markdown_files(root_dir)
]
for future in futures:
result = future.result()
if result and result[1]:
results[result[0]] = result[1]
return results
6.3 增量提取
添加文件修改时间检查,只处理有变动的文件:
python复制def process_files_incremental(root_dir, last_run_time):
results = defaultdict(list)
root_path = Path(root_dir)
for md_file in find_markdown_files(root_path):
if md_file.stat().st_mtime > last_run_time:
try:
content = md_file.read_text(encoding='utf-8')
blocks = extract_code_blocks(content)
if blocks:
results[str(md_file.relative_to(root_path))] = blocks
except Exception as e:
print(f"Error processing {md_file}: {e}")
return results
7. 实际应用中的经验分享
7.1 遇到的坑与解决方案
-
编码问题:
- 问题:某些Markdown文件使用非UTF-8编码
- 解决:添加编码自动检测
python复制import chardet def detect_encoding(file_path): with open(file_path, 'rb') as f: return chardet.detect(f.read())['encoding'] -
嵌套代码块:
- 问题:Markdown中可能出现嵌套的代码块
- 解决:修改正则表达式为非贪婪模式
python复制CODE_BLOCK_PATTERN = re.compile( r'```(?P<language>\w*?)\n(?P<code>.*?)```', re.DOTALL | re.MULTILINE ) -
大文件处理:
- 问题:超大Markdown文件内存不足
- 解决:流式读取处理
python复制def extract_from_large_file(file_path): with open(file_path, 'r', encoding='utf-8') as f: buffer = "" in_code_block = False current_lang = None for line in f: if line.startswith('```') and not in_code_block: in_code_block = True current_lang = line[3:].strip() buffer = "" elif line.startswith('```') and in_code_block: in_code_block = False yield current_lang, buffer elif in_code_block: buffer += line
7.2 性能优化技巧
- 缓存机制:对已经处理过的文件建立哈希缓存
- 批量处理:将小文件合并处理减少IO操作
- 预处理过滤:先通过简单grep检查文件是否包含```再详细解析
8. 扩展思路
这个工具可以进一步扩展为:
- 代码质量检查:集成linter对提取的代码进行静态分析
- 自动测试生成:根据文档中的示例代码生成测试用例
- 文档同步:当源代码变更时自动更新文档中的示例
- 知识图谱构建:分析代码块之间的关系构建知识网络
例如,添加简单的代码质量检查:
python复制def analyze_code_quality(code, language):
if language == 'python':
# 检查Python代码的常见问题
issues = []
if '\t' in code:
issues.append('使用制表符而非空格')
if 'print(' in code:
issues.append('可能存在调试打印语句')
return issues
return []
9. 项目总结
这个文件提取工具虽然代码量不大,但实际解决了文档处理中的痛点问题。通过Python的丰富生态,我们能够快速实现一个健壮、高效的解决方案。关键收获包括:
- 正则表达式在文本处理中的强大能力
- Python标准库对文件系统操作的完善支持
- 命令行工具开发的最佳实践
- 处理真实世界数据时需要考虑的边缘情况
工具的开发过程也体现了迭代式开发的优势:从最简单的功能开始,逐步添加高级特性,同时保持代码的可维护性。
