Files
2026-07-13 12:35:57 +08:00

326 lines
12 KiB
Python

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"<img{img_counter}>"
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'])
# 还原替换的 <img%d> 和 <text%d>
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'<img\d+>', 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)