import os
import re
import argparse
from pathlib import Path
from typing import List, Dict, Tuple, Optional
import sys
class SmartMarkdownExtractor:
def __init__(self, base_dir: str = "output", overwrite: bool = False):
"""
初始化智能Markdown解析器
Args:
base_dir: 输出文件的基础目录
overwrite: 是否覆盖已存在的文件
"""
self.base_dir = Path(base_dir)
self.overwrite = overwrite
self.base_dir.mkdir(parents=True, exist_ok=True)
# 常见文件扩展名
self.file_extensions = {
'python': ['.py', '.pyw', '.pyx'],
'javascript': ['.js', '.jsx', '.mjs'],
'typescript': ['.ts', '.tsx'],
'html': ['.html', '.htm'],
'css': ['.css'],
'markdown': ['.md', '.markdown'],
'json': ['.json'],
'yaml': ['.yaml', '.yml'],
'xml': ['.xml'],
'sql': ['.sql'],
'bash': ['.sh', '.bash'],
'text': ['.txt', '.text'],
'java': ['.java'],
'cpp': ['.cpp', '.c', '.h', '.hpp', '.hxx'],
'go': ['.go'],
'rust': ['.rs'],
'php': ['.php'],
'ruby': ['.rb'],
'swift': ['.swift'],
'kotlin': ['.kt', '.kts'],
'scala': ['.scala'],
'dart': ['.dart'],
'csharp': ['.cs'],
'vb': ['.vb'],
'perl': ['.pl', '.pm'],
'lua': ['.lua'],
'r': ['.r', '.R'],
'matlab': ['.m'],
'shell': ['.sh', '.zsh', '.bash', '.fish'],
'batch': ['.bat', '.cmd'],
'powershell': ['.ps1'],
'dockerfile': ['.dockerfile', 'Dockerfile'],
'makefile': ['Makefile', 'makefile'],
'gradle': ['.gradle', '.gradle.kts'],
'config': ['.cfg', '.conf', '.config', '.ini', '.toml'],
'csv': ['.csv'],
'tsv': ['.tsv'],
'log': ['.log'],
'lock': ['.lock'],
}
# 代码块语言到扩展名的映射
self.lang_to_ext = {
'python': '.py',
'py': '.py',
'javascript': '.js',
'js': '.js',
'typescript': '.ts',
'ts': '.ts',
'html': '.html',
'css': '.css',
'markdown': '.md',
'md': '.md',
'json': '.json',
'yaml': '.yml',
'yml': '.yml',
'xml': '.xml',
'sql': '.sql',
'bash': '.sh',
'shell': '.sh',
'text': '.txt',
'java': '.java',
'cpp': '.cpp',
'c': '.c',
'c++': '.cpp',
'rust': '.rs',
'go': '.go',
'ruby': '.rb',
'php': '.php',
'swift': '.swift',
'kotlin': '.kt',
'scala': '.scala',
'dart': '.dart',
'csharp': '.cs',
'cs': '.cs',
'vb': '.vb',
'perl': '.pl',
'lua': '.lua',
'r': '.r',
'matlab': '.m',
'powershell': '.ps1',
'ps1': '.ps1',
'batch': '.bat',
'dockerfile': 'Dockerfile',
'makefile': 'Makefile',
}
def clean_filename(self, filename: str) -> str:
"""
清理文件名,移除特殊字符和多余的空白
"""
# 移除反引号
filename = filename.replace('`', '')
# 移除开头和结尾的空白
filename = filename.strip()
# 移除可能存在的语言标记
if ' ' in filename:
parts = filename.split(' ', 1)
if parts[0].lower() in self.lang_to_ext:
filename = parts[1]
return filename
def extract_path_from_title(self, title: str) -> Tuple[Optional[str], str, Optional[str]]:
"""
从标题中提取目录、文件名和扩展名
Returns:
(目录, 文件名, 扩展名)
"""
# 清理标题
title = self.clean_filename(title)
# 移除标题标记(# 和空格)
title = title.lstrip('#').strip()
# 检查是否包含路径分隔符
if '/' in title:
# 分割目录和文件名
parts = title.rsplit('/', 1)
if len(parts) == 2:
directory = parts[0].strip()
filename = parts[1].strip()
else:
directory = None
filename = title.strip()
else:
directory = None
filename = title.strip()
# 提取扩展名
extension = None
if '.' in filename:
# 找到最后一个点作为扩展名分隔符
parts = filename.rsplit('.', 1)
if len(parts) == 2:
extension = '.' + parts[1]
return directory, filename, extension
def parse_markdown_titles(self, markdown_text: str) -> List[Dict]:
"""
解析Markdown中的所有标题,提取文件信息
Returns:
包含文件信息的字典列表
"""
files = []
lines = markdown_text.split('\n')
i = 0
while i < len(lines):
line = lines[i]
# 检查是否是标题行(以1-6个#开头)
if line.startswith('#') and line.lstrip('#').startswith(' '):
# 提取标题内容
title = line.strip()
# 从标题中提取路径信息
directory, filename, extension = self.extract_path_from_title(title)
# 查找标题对应的内容
content_start = i + 1
content_end = self.find_content_end(lines, content_start)
# 提取内容
content_lines = lines[content_start:content_end]
content = '\n'.join(content_lines)
# 清理内容中的代码块标记
content = self.extract_code_block_content(content)
# 如果内容不为空,添加到文件列表
if content.strip():
# 如果没有文件名(比如标题只是描述性的),使用默认文件名
if not filename:
filename = self.generate_filename(len(files), content)
# 如果没有扩展名,尝试从内容或语言推断
if not extension:
extension = self.guess_extension(content, filename)
if extension:
filename = filename + extension
files.append({
'title': title,
'directory': directory,
'filename': filename,
'extension': extension,
'content': content.strip(),
'title_line': i + 1, # 1-based line number
})
i = content_end
else:
i += 1
return files
def find_content_end(self, lines: List[str], start_idx: int) -> int:
"""
找到标题内容的结束位置
Returns:
内容结束的行索引
"""
# 跳过开头的空行
i = start_idx
while i < len(lines) and not lines[i].strip():
i += 1
# 如果没有内容,直接返回
if i >= len(lines):
return len(lines)
# 检查下一行是否是标题
if lines[i].startswith('#') and lines[i].lstrip('#').startswith(' '):
return i
# 找到下一个标题的位置
for j in range(i, len(lines)):
if lines[j].startswith('#') and lines[j].lstrip('#').startswith(' '):
# 找到下一个标题,内容结束
return j
# 如果没有找到更多标题,内容直到文件结束
return len(lines)
def extract_code_block_content(self, content: str) -> str:
"""
从内容中提取代码块内容
如果内容中包含代码块,提取代码块内的内容
否则返回原始内容
"""
lines = content.split('\n')
result = []
in_codeblock = False
codeblock_lang = None
for line in lines:
stripped = line.strip()
# 检查是否是代码块开始
if stripped.startswith('```'):
if in_codeblock:
# 代码块结束
in_codeblock = False
codeblock_lang = None
else:
# 代码块开始
in_codeblock = True
# 提取语言
lang_part = stripped[3:].strip()
if lang_part:
codeblock_lang = lang_part.split()[0] if lang_part else None
elif in_codeblock:
# 在代码块内,添加内容
result.append(line)
elif not in_codeblock and stripped:
# 不在代码块内,但也不是空行,也作为内容
result.append(line)
return '\n'.join(result)
def generate_filename(self, index: int, content: str) -> str:
"""
生成默认文件名
"""
# 尝试从内容中提取有意义的名称
lines = content.split('\n')
for line in lines[:10]: # 只检查前10行
line = line.strip()
# 查找常见的命名模式
patterns = [
r'def\s+(\w+)', # Python函数定义
r'class\s+(\w+)', # Python类定义
r'function\s+(\w+)', # JavaScript函数
r'const\s+(\w+)\s*=', # JavaScript常量
r'let\s+(\w+)\s*=', # JavaScript变量
r'var\s+(\w+)\s*=', # JavaScript变量
r'interface\s+(\w+)', # TypeScript接口
r'type\s+(\w+)', # TypeScript类型
]
for pattern in patterns:
match = re.search(pattern, line)
if match:
name = match.group(1).lower()
if len(name) > 2 and len(name) < 20:
return name
# 如果没有找到合适的名称,使用默认名称
return f"file_{index + 1}"
def guess_extension(self, content: str, filename: str) -> Optional[str]:
"""
根据内容和文件名猜测文件扩展名
"""
lines = content.split('\n')
first_line = lines[0].strip() if lines else ""
# 检查内容中的线索
shebang_patterns = [
(r'^#!/usr/bin/env python', '.py'),
(r'^#!/usr/bin/python', '.py'),
(r'^#!/bin/bash', '.sh'),
(r'^#!/bin/sh', '.sh'),
(r'^#!/usr/bin/env node', '.js'),
(r'^#!/usr/bin/env ruby', '.rb'),
(r'^#!/usr/bin/env perl', '.pl'),
]
for pattern, ext in shebang_patterns:
if re.match(pattern, first_line):
return ext
# 检查常见的关键词
content_lower = content.lower()
if 'def ' in content_lower or 'import ' in content_lower or 'from ' in content_lower:
if 'print(' in content_lower or 'class ' in content_lower:
return '.py'
if 'function ' in content_lower or 'const ' in content_lower or 'let ' in content_lower:
if 'console.' in content_lower or 'document.' in content_lower:
return '.js'
if '<!DOCTYPE html>' in content_lower or '<html' in content_lower:
return '.html'
if 'SELECT ' in content_lower or 'INSERT INTO' in content_lower or 'CREATE TABLE' in content_lower:
return '.sql'
if '---' in first_line and ('key:' in content_lower or 'value:' in content_lower):
return '.yml'
if '{' in content_lower and '}' in content_lower and ':' in content_lower:
# 可能是JSON或JavaScript对象
try:
import json
json.loads(content)
return '.json'
except:
pass
# 根据文件名猜测
for lang, exts in self.file_extensions.items():
for ext in exts:
if filename.endswith(ext):
return ext
# 默认使用.txt
return '.txt'
def save_extracted_files(self, files: List[Dict]) -> Dict:
"""
保存提取的文件
Returns:
处理结果的统计信息
"""
stats = {
'total': len(files),
'success': 0,
'failed': 0,
'skipped': 0,
'files': []
}
for file_info in files:
filename = file_info['filename']
directory = file_info['directory']
content = file_info['content']
# 构建完整路径
if directory:
full_path = self.base_dir / directory / filename
# 创建目录
(self.base_dir / directory).mkdir(parents=True, exist_ok=True)
else:
full_path = self.base_dir / filename
# 检查文件是否已存在
if full_path.exists() and not self.overwrite:
print(f"⚠ 跳过: {full_path} (文件已存在)")
stats['skipped'] += 1
continue
try:
# 保存文件
with open(full_path, 'w', encoding='utf-8') as f:
f.write(content)
# 记录成功
relative_path = str(full_path.relative_to(self.base_dir))
stats['files'].append({
'path': relative_path,
'size': len(content),
'line': file_info['title_line']
})
stats['success'] += 1
except Exception as e:
print(f"✗ 失败: {filename} - {e}")
stats['failed'] += 1
return stats
def process(self, markdown_text: str, source_name: str = None) -> Dict:
"""
处理Markdown文本
Returns:
处理结果的统计信息
"""
print("=" * 60)
print("智能Markdown解析器")
print("=" * 60)
if source_name:
print(f"📄 源文件: {source_name}")
print(f"📂 输出目录: {self.base_dir.absolute()}")
print("-" * 60)
# 解析Markdown标题
print("🔍 正在解析标题...")
files = self.parse_markdown_titles(markdown_text)
if not files:
print("❌ 未找到任何标题")
return {'total': 0, 'success': 0, 'failed': 0, 'skipped': 0, 'files': []}
print(f"✅ 找到 {len(files)} 个标题")
print("-" * 60)
# 显示找到的文件
print("📋 找到的文件:")
for i, file_info in enumerate(files, 1):
if file_info['directory']:
path = f"{file_info['directory']}/{file_info['filename']}"
else:
path = file_info['filename']
print(f" {i:2d}. {path}")
print(f" 标题: {file_info['title'][:50]}...")
print(f" 行号: 第{file_info['title_line']}行")
print(f" 大小: {len(file_info['content'])} 字符")
print("-" * 60)
print("💾 正在保存文件...")
# 保存文件
stats = self.save_extracted_files(files)
# 显示统计信息
print("-" * 60)
print("📊 统计信息:")
print(f" 总计: {stats['total']} 个文件")
print(f" 成功: {stats['success']} 个")
print(f" 失败: {stats['failed']} 个")
if stats['skipped'] > 0:
print(f" 跳过: {stats['skipped']} 个 (文件已存在)")
if stats['success'] > 0:
print("\n✅ 已保存的文件:")
for file_info in stats['files']:
print(f" 📄 {file_info['path']} ({file_info['size']} 字符)")
print("=" * 60)
return stats
def main():
parser = argparse.ArgumentParser(
description='从Markdown标题中智能提取文件并保存到相应目录',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
%(prog)s input.md # 解析文件
%(prog)s input.md --output ./project # 指定输出目录
%(prog)s input.md --overwrite # 覆盖已存在的文件
%(prog)s --stdin # 从标准输入读取
支持的标题格式:
# src/main.py → src/main.py
# utils/helper.py → utils/helper.py
# README.md → README.md
# config/settings.yaml → config/settings.yaml
# 这是一个说明文件 → 这是一个说明文件.txt
# 函数定义 → 函数定义.py (自动推断)
'''
)
parser.add_argument(
'input_file',
nargs='?',
help='输入的Markdown文件'
)
parser.add_argument(
'-o', '--output',
default='output',
help='输出目录 (默认: output)'
)
parser.add_argument(
'--overwrite',
action='store_true',
help='覆盖已存在的文件'
)
parser.add_argument(
'--stdin',
action='store_true',
help='从标准输入读取'
)
parser.add_argument(
'-q', '--quiet',
action='store_true',
help='安静模式,减少输出'
)
args = parser.parse_args()
# 获取Markdown内容
markdown_text = ""
source_name = None
if args.stdin:
# 从标准输入读取
if not args.quiet:
print("从标准输入读取Markdown内容...")
markdown_text = sys.stdin.read()
source_name = "stdin"
elif args.input_file:
# 从文件读取
try:
with open(args.input_file, 'r', encoding='utf-8') as f:
markdown_text = f.read()
source_name = args.input_file
except FileNotFoundError:
print(f"错误: 文件 '{args.input_file}' 不存在")
return 1
except Exception as e:
print(f"读取文件时出错: {e}")
return 1
else:
parser.print_help()
return 0
# 创建解析器
extractor = SmartMarkdownExtractor(
base_dir=args.output,
overwrite=args.overwrite
)
# 处理Markdown
stats = extractor.process(markdown_text, source_name)
return 0 if stats['failed'] == 0 else 1
if __name__ == "__main__":
exit(main())
页面: 12
发表评论