import os import pandas as pd import re from tqdm import tqdm from collections import defaultdict from langchain_text_splitters import RecursiveCharacterTextSplitter def merge_small_chunks(chunks, min_size): """ 合并小于指定大小的文本块,反复执行直到不再出现小于min_size的块 参数: chunks: 文本块列表 min_size: 最小块大小阈值 返回: 合并后的文本块列表 逻辑: - 遍历所有块,如果某块小于min_size,则与下一个块合并 - 如果是最后一个块且小于min_size,则与上一个块合并 - 反复执行上述过程,直到不再出现任何小于min_size的块 """ if not chunks or min_size <= 0: return chunks # 反复合并,直到不再出现小块 has_small_chunks = True merged_chunks = [chunk.page_content for chunk in chunks] max_iterations = 100 # 防止无限循环的最大迭代次数 iteration_count = 0 while has_small_chunks: iteration_count += 1 # 防止无限循环 if iteration_count > max_iterations: print(f"警告: 合并迭代超过{max_iterations}次,停止合并。可能存在无法满足min_size的块。") break has_small_chunks = False new_merged_chunks = [] i = 0 previous_chunks_count = len(merged_chunks) while i < len(merged_chunks): current_chunk = merged_chunks[i] current_length = len(current_chunk) # 如果当前块大小足够,直接添加 if current_length >= min_size: new_merged_chunks.append(current_chunk) i += 1 # 如果当前块太小 else: # 如果不是最后一个块,与下一个块合并 if i < len(merged_chunks) - 1: next_chunk = merged_chunks[i + 1] merged_chunk = current_chunk + next_chunk new_merged_chunks.append(merged_chunk) has_small_chunks = True # 标记发现小块并进行了合并 i += 2 # 跳过下一个块,因为已经合并了 # 如果是最后一个块,与上一个块合并 else: if new_merged_chunks: # 将当前小块合并到上一个块 new_merged_chunks[-1] = new_merged_chunks[-1] + current_chunk # 注意:这里不设置has_small_chunks=True,因为合并后不会产生新的独立小块 else: # 如果没有上一个块(即只有一个块且很小),直接添加并停止循环 new_merged_chunks.append(current_chunk) print(f"警告: 文件整体内容长度({current_length})小于最小块大小({min_size}),保留原样。") i += 1 merged_chunks = new_merged_chunks # 额外的终止条件:如果块数量不再减少,说明无法继续合并 if len(merged_chunks) == previous_chunks_count: # 检查是否还有小块 small_chunks_exist = any(len(chunk) < min_size for chunk in merged_chunks) if small_chunks_exist: small_count = sum(1 for chunk in merged_chunks if len(chunk) < min_size) print(f"警告: 仍有{small_count}个块小于最小块大小({min_size}),但无法继续合并。") break return merged_chunks def process_md_to_csv(input_folder, output_folder, chunk_size=350, min_chunk_size=100): """ 将 Markdown/文本文件处理为 CSV 文件 参数: input_folder: 输入文件夹路径 output_folder: 输出文件夹路径 chunk_size: 文本切分大小 min_chunk_size: 最小块大小,小于此值的块将被合并 逻辑: - 读取所有 .md 和 .txt 文件 - 使用 RecursiveCharacterTextSplitter 切分文本 - 合并过小的块 - 提取图片URL、处理Markdown替换 - 生成 Position 层级结构 - 输出包含 Text, Text_pure, Img_url, Position 列的 CSV """ # 创建输出文件夹(如果不存在) os.makedirs(output_folder, exist_ok=True) # 定义文本切分器 text_splitter = RecursiveCharacterTextSplitter( separators=[ "\n\n", "\n", " ", ".", # 半角句读 大头使用 ",", "،", # 阿拉伯文逗号 "،", # 全角句读 "。", "、", # 日文逗号 "।", # 印地语句号 "\u200b", # Zero-width space "\uff0c", # Fullwidth comma "\u3001", # Ideographic comma "\uff0e", # Fullwidth full stop "\u3002", # Ideographic full stop "", ], chunk_size=chunk_size, chunk_overlap=0, length_function=len, is_separator_regex=False, ) # 获取输入文件夹中的所有.md和.txt文件 files = [f for f in os.listdir(input_folder) if f.endswith(".md") or f.endswith(".txt")] # 字典用于存储替换信息 replacement_dict = defaultdict(list) def replace_img(match, img_counter, replacement_dict): key = f"" replacement_dict[key] = match.group(0) return key, img_counter + 1, match.group(1) def replace_text(match, text_counter, replacement_dict): text = match.group(1) text_counter[text] += 1 key = f"<{text}{text_counter[text] if text_counter[text] > 1 else ''}>" replacement_dict[key] = match.group(0) return key # 定义Markdown语法及其优先级 markdown_priority = { r'^# ': 0, r'^## ': 1, r'^### ': 2, r'^#### ': 3, r'^##### ': 4, r'^###### ': 5, r'^\d+\. ': 6, r'^- ': 6, r'^\* ': 6, r'^\*\*.*\*\*$': 6 } # 特殊Markdown语法检测 special_syntax = { r'^\s*=+\s*$': 0, # 一级标题 r'^\s*-+\s*$': 1 # 二级标题 } # 遍历文件并显示进度条 for filename in tqdm(files, desc="Processing files"): input_filepath = os.path.join(input_folder, filename) # 读取文件内容 with open(input_filepath, encoding='utf-8-sig') as f: file_content = f.read() # 预处理步骤 # 替换连续超过两个的* file_content = re.sub(r'\*{3,}', '**', file_content) # 替换连续超过三个的- 或 = file_content = re.sub(r'\-{4,}', '---', file_content) file_content = re.sub(r'\={4,}', '===', file_content) img_counter = 1 text_counter = defaultdict(int) img_urls_per_line = [] replacement_dict = {} # 替换 ![img](url) img_pattern = re.compile(r'!\[.*?\]\((.*?)\)') img_matches = list(img_pattern.finditer(file_content)) for match in img_matches: replacement, img_counter, img_url = replace_img(match, img_counter, replacement_dict) file_content = file_content.replace(match.group(0), replacement) img_urls_per_line.append(img_url) # 替换 [text](url) text_pattern = re.compile(r'\[(.*?)\]\(.*?\)') text_matches = list(text_pattern.finditer(file_content)) for match in text_matches: replacement = replace_text(match, text_counter, replacement_dict) file_content = file_content.replace(match.group(0), replacement) # 执行文本切分 texts = text_splitter.create_documents([file_content]) # 合并过小的块 merged_texts = merge_small_chunks(texts, min_chunk_size) # 创建DataFrame df = pd.DataFrame(merged_texts, columns=['Text']) # 还原替换的 def restore_replacement(text): for key, value in replacement_dict.items(): text = text.replace(key, value) return text # 创建 Text_pure 列 df['Text_pure'] = df['Text'] def extract_img_urls(text): img_keys = re.findall(r'', text) img_urls = [] for key in img_keys: if key in replacement_dict: url_match = re.search(r'\((.*?)\)', replacement_dict[key]) if url_match: img_urls.append(url_match.group(1)) return ';'.join(img_urls) df['Img_url'] = df['Text_pure'].apply(extract_img_urls) # 还原 Text 列中的内容 df['Text'] = df['Text'].apply(restore_replacement) # 保存为CSV文件 output_filename = f"{os.path.splitext(filename)[0]}.csv" output_filepath = os.path.join(output_folder, output_filename) df.to_csv(output_filepath, index=False, encoding='utf-8-sig') print(f"Processed and saved: {output_filepath}") # 读取CSV文件内容 df = pd.read_csv(output_filepath, encoding='utf-8-sig') # 初始化栈并将文件名作为初始位置 initial_position = os.path.splitext(filename)[0] position_stack = [initial_position] priority_stack = [-1] # 初始优先级为-1,以确保文件名始终在最顶部 # 定义位置列 df['Position'] = "" # 用于记录连续相同位置的次数 last_position = "" position_counter = 0 # 逐行处理文本 for idx, row in df.iterrows(): text = row['Text'] sentences = re.split(r'\n+', text) # 以换行符进行分句 # 首先对每一行赋初始位置 current_position = ' > '.join(position_stack) # 检查是否与上一行位置相同 if current_position == last_position: position_counter += 1 if position_counter == 2: # 在第二次遇到时给上一个添加序号 df.at[idx - 1, 'Position'] += f" (part 1)" current_position += f" (part {position_counter})" else: last_position = current_position position_counter = 1 df.at[idx, 'Position'] = current_position # 逐句处理文本 previous_sentence = "" for sentence in sentences: current_priority = None # 判断当前句子的标题信息及其优先级 for grammar, priority in markdown_priority.items(): if re.match(grammar, sentence.strip()): current_priority = priority break # 特殊处理 `===` 和 `---` 的情况 for special_grammar, special_priority in special_syntax.items(): if re.match(special_grammar, sentence.strip()): current_priority = special_priority sentence = previous_sentence break # 更新位置栈和优先级栈 if current_priority is not None: while priority_stack and priority_stack[-1] >= current_priority: position_stack.pop() priority_stack.pop() position_stack.append(sentence.strip()) priority_stack.append(current_priority) previous_sentence = sentence # 保存更新后的CSV文件 df.to_csv(output_filepath, index=False, encoding='utf-8-sig') print(f"Refined and saved: {output_filepath}") if __name__ == "__main__": # 定义输入和输出文件夹 input_folder = "./input/GT_ML_TEST/" output_folder = "./output/GT_ML_TEST/" # 执行处理 process_md_to_csv(input_folder, output_folder)