更新pyproject.toml以添加pyright类型检查配置,修改settings.json以调整数据集参数,重构qa_generator.py以支持新的消息处理策略,优化数据处理逻辑,更新测试用例以适应新功能。

This commit is contained in:
xming521
2025-04-13 15:27:00 +08:00
parent 177267671a
commit 055fb01792
7 changed files with 717 additions and 260 deletions
+128 -82
View File
@@ -1,9 +1,9 @@
from dataclasses import dataclass
import sys
import os
from typing import List
from typing import Dict, List
from collections import deque
import re
from pandas import Timestamp
import pandas as pd
import json
@@ -12,13 +12,12 @@ root_dir = os.path.dirname(current_dir)
sys.path.append(root_dir)
from src.utils.config import load_config
from make_dataset.models import ChatMessage, CutMessage, skip_type_list
from make_dataset.strategies import TimeWindowStrategy, LagerModelStrategy
from make_dataset.strategies import TimeWindowStrategy, LLMStrategy
class DataProcessor:
def __init__(self):
self.config = load_config(arg_type="make_dataset")
self.data = None
self.csv_folder = "./data/csv"
self.cut_type_list = [
"图片",
@@ -36,13 +35,25 @@ class DataProcessor:
"粘贴的文本", # 无法解析的分享链接
]
# 根据self.config.make_dataset_args.conversation_strategy 判断初始化哪一个策略类
if self.config["conversation_strategy"] == "time_window":
self.conversation_strategy = TimeWindowStrategy(
time_window=self.config["time_window"]
if self.config["single_combine_strategy"] == "time_window":
self.single_combine_strategy = TimeWindowStrategy(
time_window=self.config["single_combine_time_window"] * 60,
is_single_chat=True,
)
elif self.config["conversation_strategy"] == "lager_model":
self.conversation_strategy = LagerModelStrategy()
elif self.config["single_combine_strategy"] == "llm":
self.single_combine_strategy = LLMStrategy(
is_single_chat=True,
)
if self.config["qa_match_strategy"] == "time_window":
self.qa_match_strategy = TimeWindowStrategy(
time_window=self.config["qa_match_time_window"] * 60,
is_single_chat=False,
)
elif self.config["qa_match_strategy"] == "llm":
self.qa_match_strategy = LLMStrategy(is_single_chat=False)
self.c = self.config
def get_csv_files(self):
"""遍历文件夹获取所有CSV文件路径"""
@@ -61,9 +72,94 @@ class DataProcessor:
message_list: List[ChatMessage] = []
for csv_file in csv_files:
chat_messages = self.load_csv(csv_file)
# 第一次预处理后 将chat_message 加入rcsv_df_list
message_list.append(self.group_consecutive_messages(messages=chat_messages))
message_list.extend(self.group_consecutive_messages(messages=chat_messages))
# self.process_by_msgtype(chat_message)
qa_res = self.match_qa(message_list)
if self.c["prompt_with_history"]:
qa_res = self.add_history_to_qa(qa_res)
self.save_result(qa_res)
def match_qa(self, messages: List[ChatMessage]) -> List[Dict]:
"""
匹配问答对
Args:
messages: 消息列表
Returns:
List[Dict]: 包含指令和输出的问答对列表
"""
# 状态定义
WAITING_INSTRUCTION = "waiting_instruction" # 等待指令
WAITING_RESPONSE = "waiting_response" # 等待回复
current_state = WAITING_INSTRUCTION
qa_res = []
last_message = None
current_instruction = None
for msg in messages:
# 检查是否为CutMessage
if isinstance(msg, CutMessage):
current_state = WAITING_INSTRUCTION
current_instruction = None
last_message = None
if self.c["prompt_with_history"]:
qa_res.append(msg)
continue
if current_state == WAITING_INSTRUCTION:
if msg.is_sender == 0: # 收到对方消息
current_instruction = msg.msg
last_message = msg
current_state = WAITING_RESPONSE
elif current_state == WAITING_RESPONSE:
if msg.is_sender == 0: # 收到对方消息
current_instruction = msg.msg
last_message = msg
# 状态保持不变
else: # 自己的回复 使用策略判断是否属于同一对话
if last_message and self.qa_match_strategy.is_same_conversation(
[last_message], msg
):
qa_res.append(
{"instruction": current_instruction, "output": msg.msg}
)
# 无论是否匹配,都重置状态
current_state = WAITING_INSTRUCTION
current_instruction = None
last_message = None
return qa_res
def add_history_to_qa(self, qa_res: List[Dict]):
qa_res_with_history = []
last_res = {"instruction": "", "output": "", "history": []}
for _, qa in enumerate(qa_res):
if isinstance(qa, CutMessage):
if len(last_res["history"]) == 0:
continue
else:
if len(last_res["history"]) == 1:
last_res = {
"instruction": last_res["history"][0][0],
"output": last_res["history"][0][1],
"history": [],
}
else:
last_res = {
"instruction": last_res["history"][-1][0],
"output": last_res["history"][-1][1],
"history": last_res["history"][:-1],
}
qa_res_with_history.append(last_res)
last_res = {"instruction": "", "output": "", "history": []}
else:
last_res["history"].append([qa["instruction"], qa["output"]])
return qa_res_with_history
def group_consecutive_messages(
self, messages: List[ChatMessage]
@@ -125,15 +221,7 @@ class DataProcessor:
return combined_message
def _create_cut_message(message: ChatMessage) -> CutMessage:
"""
创建一个CutMessage实例
Args:
message: 当前处理的消息,用于获取属性
Returns:
CutMessage: 创建的CutMessage实例
"""
return CutMessage(
is_sender=message.is_sender,
cut_type=message.type_name,
@@ -185,8 +273,9 @@ class DataProcessor:
if (
current_msg.is_sender == last_msg.is_sender
and current_msg.talker == last_msg.talker
and (current_msg.CreateTime - last_msg.CreateTime).total_seconds()
< 3600
and self.single_combine_strategy.is_same_conversation(
[last_msg], current_msg
)
):
current_group.append(current_msg)
else:
@@ -201,48 +290,6 @@ class DataProcessor:
return grouped_messages
def create_conversation_data(self, messages: List[ChatMessage]) -> dict:
"""
将一组消息组成一条对话数据
Args:
messages: 属于同一对话的消息列表
Returns:
dict: 包含对话历史的数据字典
"""
conversation = []
for msg in messages:
if msg.type_name in self.config["include_type"]:
conversation.append(
{
"role": "user" if msg.is_sender == 0 else "assistant",
"content": msg.msg,
"timestamp": msg.CreateTime,
}
)
# 确保对话是按时间顺序排列的
conversation.sort(key=lambda x: x["timestamp"])
# 限制历史长度
if len(conversation) > self.config["history_length"]:
conversation = conversation[-self.config["history_length"] :]
return {
"conversation": conversation,
"metadata": {
"conversation_id": str(messages[0].id) if messages else None,
"timestamp": messages[-1].CreateTime if messages else None,
},
}
def process_by_msgtype(self, chat_message: ChatMessage):
if chat_message.type_name == "文本":
self.process_text(chat_message)
# elif chat_message.type_name == "图片":
# self.process_image(chat_message)
def process_by_msgtype(self, chat_message: ChatMessage):
if chat_message.type_name == "文本":
self.process_text(chat_message)
@@ -264,18 +311,19 @@ class DataProcessor:
# 如果type_name为文本 并且msg 包含 手机号、身份证号、邮箱、网址则删除这行
for i in df.index:
if df.loc[i, "type_name"] == "文本":
msg_str = str(df.loc[i, "msg"])
if (
"1\d{10}" in df.loc[i, "msg"]
or "\d{18}" in df.loc[i, "msg"]
or "\w+@\w+" in df.loc[i, "msg"]
or "http" in df.loc[i, "msg"]
or r"\\xa0" in df.loc[i, "msg"]
or r"\\u" in df.loc[i, "msg"]
re.search(r"1\d{10}", msg_str)
or re.search(r"\d{18}", msg_str)
or re.search(r"\w+@\w+", msg_str)
or "http" in msg_str
or r"\\xa0" in msg_str
or r"\\u" in msg_str
):
df = df.drop(index=i)
continue
for blocked_word in blocked_words:
if blocked_word in df.loc[i, "msg"]:
if blocked_word in msg_str:
df = df.drop(index=i)
break
else:
@@ -292,17 +340,15 @@ class DataProcessor:
pass
def process_image(self):
# 处理方法1
pass
def process_method2(self):
# 处理方法2
pass
def save_result(self):
def save_result(self, qa_res: List[Dict]):
# 保存结果
pass
with open(
f"./data/res_csv/sft/sft-{self.c['single_combine_strategy']}-{self.c['qa_match_strategy']}-my.json",
"w",
encoding="utf-8",
) as f:
json.dump(qa_res, f, ensure_ascii=False)
print(f"聊天记录处理成功,共{len(qa_res)}条,保存到{f.name}")
if __name__ == "__main__":
+36 -15
View File
@@ -1,39 +1,60 @@
from dataclasses import dataclass
from typing import Protocol, List
from typing import List
from .models import ChatMessage
from abc import ABC, abstractmethod
class ConversationStrategy(Protocol):
def is_same_conversation(self, msg1: ChatMessage, msg2: ChatMessage) -> bool:
@dataclass
class ConversationStrategy(ABC):
"""对话策略的抽象基类"""
is_single_chat: bool
@abstractmethod
def is_same_conversation(
self, history_msg: List[ChatMessage], current_msg: ChatMessage
) -> bool:
"""判断两条消息是否属于同一个对话"""
pass
@dataclass
class TimeWindowStrategy(ConversationStrategy):
"""基于时间窗口的判断策略"""
time_window: int # 时间窗口(分钟)
def is_same_conversation(self, msg1: ChatMessage, msg2: ChatMessage) -> bool:
time_diff = abs((msg2.timestamp - msg1.timestamp))
time_window: int # 时间窗口(分钟)
def is_same_conversation(
self, history_msg: List[ChatMessage], current_msg: ChatMessage
) -> bool:
time_diff = abs(
(current_msg.CreateTime - history_msg[-1].CreateTime)
).total_seconds()
return time_diff <= self.time_window
@dataclass
class LagerModelStrategy(ConversationStrategy):
class LLMStrategy(ConversationStrategy):
"""基于大模型判断策略"""
def is_same_conversation(self, msg1: ChatMessage, msg2: ChatMessage) -> bool:
return msg1.user_id == msg2.user_id
def is_same_conversation(
self, history_msg: List[ChatMessage], current_msg: ChatMessage
) -> bool:
# 修复user_id错误,使用talker字段代替user_id
return current_msg.talker == history_msg[-1].talker if history_msg else False
@dataclass
class CompositeStrategy(ConversationStrategy):
"""组合多个策略的复合策略"""
strategies: List[ConversationStrategy]
require_all: bool = True # True表示所有策略都满足,False表示任一策略满足即可
def is_same_conversation(self, msg1: ChatMessage, msg2: ChatMessage) -> bool:
results = [s.is_same_conversation(msg1, msg2) for s in self.strategies]
return all(results) if self.require_all else any(results)
def is_same_conversation(
self, history_msg: List[ChatMessage], current_msg: ChatMessage
) -> bool:
results = [
s.is_same_conversation(history_msg, current_msg) for s in self.strategies
]
return all(results) if self.require_all else any(results)
+13
View File
@@ -45,3 +45,16 @@ conflicts = [
[[tool.uv.index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
default = true
[tool.pyright]
typeCheckingMode = "basic"
include = ["src"]
exclude = ["**/archive","**/tests"]
ignore = ["**/archive"]
reportMissingImports = "error"
reportMissingTypeStubs = false
pythonVersion = "3.9"
pythonPlatform = "Linux"
+7 -8
View File
@@ -43,14 +43,14 @@
"top_p": 0.65
},
"make_dataset_args": {
"enable_vision_model": false,
// "enable_vision_model": false,//后续实现
"include_type": [
"文本",
"图片"
"文本"
],
"history_length": 10,
"conversation_strategy": "time_window", // 基于时间窗口的判断策略
"time_window": 10, // 时间窗口(分钟),
"single_combine_strategy": "time_window", // 单人组成单句策略
"qa_match_strategy": "time_window", // 多人组成qa策略
"single_combine_time_window": 10, // 单人组成单句时间窗口(分钟),
"qa_match_time_window": 60, // 多人组成qa时间窗口(分钟),
"prompt_with_history": false // 是否在prompt中包含历史对话
},
"common_args": {
@@ -59,6 +59,5 @@
"template": "chatglm3-weclone",
"finetuning_type": "lora",
"trust_remote_code": true
},
"_comment": "adapter_name_or_path同时做为train_sft_args的output_dir "
}
}
+311
View File
@@ -0,0 +1,311 @@
import csv
import json
import os
import sys
import pandas as pd
from collections import deque
current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(current_dir)
sys.path.append(root_dir)
from make_dataset.qa_generator import DataProcessor
csv_folder = "./data/csv"
# csv_folder = './data/test'
os.chdir(root_dir)
print(f"当前处理目录{csv_folder}")
def handle_pt_csv(csvfile):
chat_df = pd.read_csv(csvfile)
# 选择type_name为文本的行、is_sender为1的行
chat_df = chat_df[chat_df["type_name"] == "文本"]
chat_df = chat_df[chat_df["is_sender"] == 1]
# 对每一行的content进行处理 转为dict 再取'msg'字段
chat_df["content"] = chat_df["content"].apply(lambda x: json.loads(x)["msg"])
# 如果content 包含 手机号、身份证号、邮箱、网址则删除这行
chat_df = chat_df[~chat_df["content"].str.contains("1\d{10}")]
chat_df = chat_df[~chat_df["content"].str.contains("\d{18}")]
chat_df = chat_df[~chat_df["content"].str.contains("\w+@\w+")]
chat_df = chat_df[~chat_df["content"].str.contains("http")]
chat_df = chat_df[~chat_df["content"].str.contains(r"\\xa0")]
chat_df = chat_df[~chat_df["content"].str.contains(r"\\u")]
# 纯content
chat_df = chat_df["content"]
chat_df = chat_df.dropna()
return chat_df
def make_pt_dataset():
csv_res = []
# csv文件夹里全是不同聊天对象文件夹 每个文件夹里是csv文件 先遍历不同聊天对象文件夹 再遍历聊天对象的csv文件
for chat_obj_folder in os.listdir(csv_folder):
chat_obj_folder_path = os.path.join(csv_folder, chat_obj_folder)
for csvfile in os.listdir(chat_obj_folder_path):
if not csvfile.endswith(".csv"):
continue
csvfile_path = os.path.join(chat_obj_folder_path, csvfile)
chat_df = handle_pt_csv(csvfile_path)
csv_res.append(chat_df)
csv_res = pd.concat(csv_res)
csv_res = csv_res.apply(lambda x: {"c": x}) # 设置数据集prompt键为c
csv_res.to_json("./data/res_csv/pt-my.json", orient="records", force_ascii=False)
def handle_sft_csv(csvfile):
chat_df = pd.read_csv(csvfile)
blocked_words = json.load(
open("./make_dataset/blocked_words.json", encoding="utf-8")
)["blocked_words"]
# 选择type_name为文本的行、is_sender为1的行
# 需要保留的type_name字段名
type_list = [
"文本",
"图片",
"视频",
"合并转发的聊天记录",
"语音",
"(分享)音乐",
"(分享)卡片式链接",
"(分享)笔记",
"(分享)小程序",
"(分享)收藏夹",
"(分享)小说(猜)",
"(分享)视频号名片",
"(分享)视频号视频",
"粘贴的文本", # 无法解析的分享链接
]
chat_df = chat_df[chat_df["type_name"].isin(values=type_list)]
# chat_df['content'] = chat_df['content'].apply(func=lambda x: json.loads(x)['msg'])
chat_df["content"] = chat_df["msg"]
# 如果type_name为文本 并且content 包含 手机号、身份证号、邮箱、网址则删除这行
for i in chat_df.index:
if chat_df.loc[i, "type_name"] == "文本":
if (
"1\d{10}" in chat_df.loc[i, "content"]
or "\d{18}" in chat_df.loc[i, "content"]
or "\w+@\w+" in chat_df.loc[i, "content"]
or "http" in chat_df.loc[i, "content"]
or r"\\xa0" in chat_df.loc[i, "content"]
or r"\\u" in chat_df.loc[i, "content"]
):
chat_df = chat_df.drop(index=i)
continue
for blocked_word in blocked_words:
if blocked_word in chat_df.loc[i, "content"]:
chat_df = chat_df.drop(index=i)
break
else:
chat_df.loc[i, "content"] = ""
chat_df = chat_df[["is_sender", "type_name", "content", "CreateTime"]]
chat_df = chat_df.dropna()
# 时间格式 2021-07-07 10:27:23
# 遍历行 相同is_sender的行合并content()遇到不同is_sender就重新开始
# CreateTime字段保留最后的CreateTime
chat_df["CreateTime"] = pd.to_datetime(chat_df["CreateTime"])
# 改到这了
type_list.remove("文本")
skip_list = type_list
res_df = []
last_is_sender = chat_df.iloc[0]["is_sender"]
last_content: str = chat_df.iloc[0]["content"]
last_CreateTime = chat_df.iloc[0]["CreateTime"]
# 超时处理 半天没说话就重新开始
# 注意这里只是处理了组装成一个句子 最后封装对话、配对在make_sft_dataset
# 遇到图片 连接 直接封装成一个句子
for i, row in chat_df.iterrows():
if row["type_name"] in skip_list:
if last_content != "":
if last_content[-1] == "":
last_content = last_content[:-1] + ""
elif last_content[-1] not in ["", "", "", "", "."]:
last_content += ""
res_df.append(
{
"is_sender": last_is_sender,
"content": last_content,
"CreateTime": last_CreateTime,
}
)
last_CreateTime = row["CreateTime"]
last_content = ""
# cut表示被skip字段截断
res_df.append(
{
"is_sender": row["is_sender"],
"content": "cut",
"CreateTime": row["CreateTime"],
}
)
continue
if last_content == "": # 重新开始
last_content = row["content"]
last_is_sender = row["is_sender"]
last_CreateTime = row["CreateTime"]
continue
if row["is_sender"] == last_is_sender:
if row["CreateTime"] - last_CreateTime > pd.Timedelta(value="10m"):
# 如果超时 前面的添加到res_df 并重新开始
if last_content[-1] == "":
last_content = last_content[:-1] + ""
elif last_content[-1] not in ["", "", "", "", "."]:
last_content += ""
res_df.append(
{
"is_sender": last_is_sender,
"content": last_content,
"CreateTime": last_CreateTime,
}
)
last_content = row["content"]
last_CreateTime = row["CreateTime"]
continue
# 如果content的结尾没有标点符号则添加逗号,最后结尾是句号
if last_content[-1] not in ["", "", "", "", ""]:
last_content += ""
last_content = last_content + row["content"]
last_CreateTime = row["CreateTime"]
else:
if last_content[-1] == "":
last_content = last_content[:-1] + ""
elif last_content[-1] not in ["", "", "", "", "."]:
last_content += ""
res_df.append(
{
"is_sender": last_is_sender,
"content": last_content,
"CreateTime": last_CreateTime,
}
)
last_is_sender = row["is_sender"]
last_content = row["content"]
last_CreateTime = row["CreateTime"]
res_df = pd.DataFrame(res_df)
return res_df
def make_sft_dataset():
processor = DataProcessor()
csv_files = processor.get_csv_files()
csv_concat = []
csv_res = []
for csvfile_path in csv_files:
chat_df = handle_sft_csv(csvfile_path)
csv_concat.append(chat_df)
# 后续代码保持不变
csv_concat = pd.concat(csv_concat)
# 更全面地处理cut标记
# 1. 将连续的cut标记合并为一个
# 2. 标记数据区块的开始和结束
processed_rows = []
skip_row = False
last_row_was_cut = False
for i in range(len(csv_concat)):
if skip_row:
skip_row = False
continue
current_row = csv_concat.iloc[i].copy()
# 处理当前行是cut的情况
if current_row["content"] == "cut":
# 如果上一行已经是cut,则跳过当前行
if last_row_was_cut:
continue
# 查找连续的cut
j = i + 1
while j < len(csv_concat) and csv_concat.iloc[j]["content"] == "cut":
j += 1
# 如果有连续的cut,只保留最后一个
if j > i + 1:
current_row = csv_concat.iloc[j - 1].copy()
skip_row = True
last_row_was_cut = True
else:
last_row_was_cut = False
processed_rows.append(current_row)
# 创建新的DataFrame
csv_concat = pd.DataFrame(processed_rows)
# csv_res里is_sender必须是01 01 01 的顺序 csv_concat里不一定是01 01
# 相差超过1小时的时间戳分为不同的对话
# temp_res为一个长度为2的队列
# 将合并后的数据保存到CSV文件中
output_dir = "./test_output"
# 生成带时间戳的文件名
import datetime
now = datetime.datetime.now()
output_file = os.path.join(output_dir, f"csv_old_.csv")
# 保存合并后的数据
# csv_concat.to_csv(output_file, index=False, encoding="utf-8-sig")
# print(f"已将合并后的数据保存到: {output_file}")
# print(f"合并后数据总量: {len(csv_concat)} 条记录")
temp_res = deque(maxlen=2)
# 6种情况
# temp_res 为空 遇到 0入队 遇到1不处理 遇到cut不处理
# temp_res 有0 遇到0清空队列再入队 遇到1相差超过1小时清空队列 没有相差一小时入队再全部出队 遇到cut清空队列
for i, row in csv_concat.iterrows():
if len(temp_res) == 0:
if row["content"] == "cut":
continue
if row["is_sender"] == 0:
temp_res.append(row["content"])
last_CreateTime = row["CreateTime"]
else:
continue
elif len(temp_res) == 1:
if row["content"] == "cut":
temp_res.clear()
last_CreateTime = row["CreateTime"]
elif row["is_sender"] == 0:
# 遇到0 清空队列再入队
temp_res.clear()
temp_res.append(row["content"])
last_CreateTime = row["CreateTime"]
else:
if row["CreateTime"] - last_CreateTime > pd.Timedelta("10m"):
# 相差超过1小时清空队列
temp_res.clear()
last_CreateTime = row["CreateTime"]
else:
# 没有相差一小时入队再全部出队
temp_res.append(row["content"])
csv_res.append({"instruction": temp_res[0], "output": temp_res[1]})
temp_res.clear()
last_CreateTime = row["CreateTime"]
csv_res_df = pd.DataFrame(csv_res)
print(f"处理后数据量:{csv_res_df.shape[0]}")
csv_res_df.to_json('./data/res_csv/sft/sft-old-my.json', orient='records', force_ascii=False)
if __name__ == "__main__":
# make_pt_dataset()
make_sft_dataset()
+152 -110
View File
@@ -12,30 +12,30 @@ sys.path.append(root_dir)
from make_dataset.qa_generator import DataProcessor
csv_folder = './data/csv'
csv_folder = "./data/csv"
# csv_folder = './data/test'
os.chdir(root_dir)
print(f'当前处理目录{csv_folder}')
print(f"当前处理目录{csv_folder}")
def handle_pt_csv(csvfile):
chat_df = pd.read_csv(csvfile)
# 选择type_name为文本的行、is_sender为1的行
chat_df = chat_df[chat_df['type_name'] == '文本']
chat_df = chat_df[chat_df['is_sender'] == 1]
chat_df = chat_df[chat_df["type_name"] == "文本"]
chat_df = chat_df[chat_df["is_sender"] == 1]
# 对每一行的content进行处理 转为dict 再取'msg'字段
chat_df['content'] = chat_df['content'].apply(lambda x: json.loads(x)['msg'])
chat_df["content"] = chat_df["content"].apply(lambda x: json.loads(x)["msg"])
# 如果content 包含 手机号、身份证号、邮箱、网址则删除这行
chat_df = chat_df[~chat_df['content'].str.contains('1\d{10}')]
chat_df = chat_df[~chat_df['content'].str.contains('\d{18}')]
chat_df = chat_df[~chat_df['content'].str.contains('\w+@\w+')]
chat_df = chat_df[~chat_df['content'].str.contains('http')]
chat_df = chat_df[~chat_df['content'].str.contains(r'\\xa0')]
chat_df = chat_df[~chat_df['content'].str.contains(r'\\u')]
chat_df = chat_df[~chat_df["content"].str.contains("1\d{10}")]
chat_df = chat_df[~chat_df["content"].str.contains("\d{18}")]
chat_df = chat_df[~chat_df["content"].str.contains("\w+@\w+")]
chat_df = chat_df[~chat_df["content"].str.contains("http")]
chat_df = chat_df[~chat_df["content"].str.contains(r"\\xa0")]
chat_df = chat_df[~chat_df["content"].str.contains(r"\\u")]
# 纯content
chat_df = chat_df['content']
chat_df = chat_df["content"]
chat_df = chat_df.dropna()
return chat_df
@@ -47,109 +47,151 @@ def make_pt_dataset():
for chat_obj_folder in os.listdir(csv_folder):
chat_obj_folder_path = os.path.join(csv_folder, chat_obj_folder)
for csvfile in os.listdir(chat_obj_folder_path):
if not csvfile.endswith('.csv'):
if not csvfile.endswith(".csv"):
continue
csvfile_path = os.path.join(chat_obj_folder_path, csvfile)
chat_df = handle_pt_csv(csvfile_path)
csv_res.append(chat_df)
csv_res = pd.concat(csv_res)
csv_res = csv_res.apply(lambda x: {'c': x}) # 设置数据集prompt键为c
csv_res = csv_res.apply(lambda x: {"c": x}) # 设置数据集prompt键为c
csv_res.to_json('./data/res_csv/pt-my.json', orient='records', force_ascii=False)
csv_res.to_json("./data/res_csv/pt-my.json", orient="records", force_ascii=False)
def handle_sft_csv(csvfile):
chat_df = pd.read_csv(csvfile)
blocked_words = json.load(open('./make_dataset/blocked_words.json', encoding='utf-8'))['blocked_words']
blocked_words = json.load(
open("./make_dataset/blocked_words.json", encoding="utf-8")
)["blocked_words"]
# 选择type_name为文本的行、is_sender为1的行
# 需要保留的type_name字段名
type_list = ['文本', '图片', '卡片式链接', '合并转发的聊天记录', '视频', '语音', '未知', '分享的小程序']
chat_df = chat_df[chat_df['type_name'].isin(values=type_list)]
type_list = [
"文本",
"图片",
"视频",
"合并转发的聊天记录",
"语音",
"(分享)音乐",
"(分享)卡片式链接",
"(分享)笔记",
"(分享)小程序",
"(分享)收藏夹",
"(分享)小说(猜)",
"(分享)视频号名片",
"(分享)视频号视频",
"粘贴的文本", # 无法解析的分享链接
]
chat_df = chat_df[chat_df["type_name"].isin(values=type_list)]
# chat_df['content'] = chat_df['content'].apply(func=lambda x: json.loads(x)['msg'])
chat_df['content'] = chat_df['msg']
chat_df["content"] = chat_df["msg"]
# 如果type_name为文本 并且content 包含 手机号、身份证号、邮箱、网址则删除这行
for i in chat_df.index:
if chat_df.loc[i, 'type_name'] == '文本':
if ('1\d{10}' in chat_df.loc[i, 'content'] or
'\d{18}' in chat_df.loc[i, 'content'] or
'\w+@\w+' in chat_df.loc[i, 'content'] or
'http' in chat_df.loc[i, 'content'] or
r'\\xa0' in chat_df.loc[i, 'content'] or
r'\\u' in chat_df.loc[i, 'content']):
if chat_df.loc[i, "type_name"] == "文本":
if (
re.search(r"1\d{10}", msg_str)
or re.search(r"\d{18}", msg_str)
or re.search(r"\w+@\w+", msg_str)
or "http" in msg_str
or r"\\xa0" in msg_str
or r"\\u" in msg_str
):
chat_df = chat_df.drop(index=i)
continue
for blocked_word in blocked_words:
if blocked_word in chat_df.loc[i, 'content']:
if blocked_word in chat_df.loc[i, "content"]:
chat_df = chat_df.drop(index=i)
break
else:
chat_df.loc[i, 'content'] = ''
chat_df.loc[i, "content"] = ""
chat_df = chat_df[['is_sender', 'type_name', 'content', 'CreateTime']]
chat_df = chat_df[["is_sender", "type_name", "content", "CreateTime"]]
chat_df = chat_df.dropna()
# 时间格式 2021-07-07 10:27:23
# 遍历行 相同is_sender的行合并content()遇到不同is_sender就重新开始
# CreateTime字段保留最后的CreateTime
chat_df['CreateTime'] = pd.to_datetime(chat_df['CreateTime'])
chat_df["CreateTime"] = pd.to_datetime(chat_df["CreateTime"])
# 改到这了
# 改到这了
type_list.remove('文本')
type_list.remove("文本")
skip_list = type_list
res_df = []
last_is_sender = chat_df.iloc[0]['is_sender']
last_content: str = chat_df.iloc[0]['content']
last_CreateTime = chat_df.iloc[0]['CreateTime']
last_is_sender = chat_df.iloc[0]["is_sender"]
last_content: str = chat_df.iloc[0]["content"]
last_CreateTime = chat_df.iloc[0]["CreateTime"]
# 超时处理 半天没说话就重新开始
# 注意这里只是处理了组装成一个句子 最后封装对话、配对在make_sft_dataset
# 遇到图片 连接 直接封装成一个句子
for i, row in chat_df.iterrows():
if row['type_name'] in skip_list:
if last_content != '':
if last_content[-1] == '':
last_content = last_content[:-1] + ''
elif last_content[-1] not in ['', '', '', '', '.']:
last_content += ''
res_df.append({'is_sender': last_is_sender, 'content': last_content, 'CreateTime': last_CreateTime})
last_CreateTime = row['CreateTime']
last_content = ''
if row["type_name"] in skip_list:
if last_content != "":
if last_content[-1] == "":
last_content = last_content[:-1]
elif last_content[-1] not in ["", "", "", "", "."]:
last_content += ""
res_df.append(
{
"is_sender": last_is_sender,
"content": last_content,
"CreateTime": last_CreateTime,
}
)
last_CreateTime = row["CreateTime"]
last_content = ""
# cut表示被skip字段截断
res_df.append({'is_sender': row['is_sender'], 'content': 'cut', 'CreateTime': row['CreateTime']})
res_df.append(
{
"is_sender": row["is_sender"],
"content": "cut",
"CreateTime": row["CreateTime"],
}
)
continue
if last_content == '': # 重新开始
last_content = row['content']
last_is_sender = row['is_sender']
last_CreateTime = row['CreateTime']
if last_content == "": # 重新开始
last_content = row["content"]
last_is_sender = row["is_sender"]
last_CreateTime = row["CreateTime"]
continue
if row['is_sender'] == last_is_sender:
if row['CreateTime'] - last_CreateTime > pd.Timedelta(value='1h'):
if row["is_sender"] == last_is_sender:
if row["CreateTime"] - last_CreateTime > pd.Timedelta(value="10m"):
# 如果超时 前面的添加到res_df 并重新开始
if last_content[-1] == '':
last_content = last_content[:-1] + ''
elif last_content[-1] not in ['', '', '', '', '.']:
last_content += ''
res_df.append({'is_sender': last_is_sender, 'content': last_content, 'CreateTime': last_CreateTime})
last_content = row['content']
last_CreateTime = row['CreateTime']
if last_content[-1] == "":
last_content = last_content[:-1]
elif last_content[-1] not in ["", "", "", "", "."]:
last_content += ""
res_df.append(
{
"is_sender": last_is_sender,
"content": last_content,
"CreateTime": last_CreateTime,
}
)
last_content = row["content"]
last_CreateTime = row["CreateTime"]
continue
# 如果content的结尾没有标点符号则添加逗号,最后结尾是句号
if last_content[-1] not in ['', '', '', '', '']:
last_content += ''
last_content = last_content + row['content']
last_CreateTime = row['CreateTime']
if last_content[-1] not in ["", "", "", "", ""]:
last_content += ""
last_content = last_content + row["content"]
last_CreateTime = row["CreateTime"]
else:
if last_content[-1] == '':
last_content = last_content[:-1] + ''
elif last_content[-1] not in ['', '', '', '', '.']:
last_content += ''
res_df.append({'is_sender': last_is_sender, 'content': last_content, 'CreateTime': last_CreateTime})
last_is_sender = row['is_sender']
last_content = row['content']
last_CreateTime = row['CreateTime']
if last_content[-1] == "":
last_content = last_content[:-1]
elif last_content[-1] not in ["", "", "", "", "."]:
last_content += ""
res_df.append(
{
"is_sender": last_is_sender,
"content": last_content,
"CreateTime": last_CreateTime,
}
)
last_is_sender = row["is_sender"]
last_content = row["content"]
last_CreateTime = row["CreateTime"]
res_df = pd.DataFrame(res_df)
return res_df
@@ -157,71 +199,72 @@ def handle_sft_csv(csvfile):
def make_sft_dataset():
processor = DataProcessor()
csv_files = processor.get_csv_files()
csv_concat = []
csv_res = []
for csvfile_path in csv_files:
chat_df = handle_sft_csv(csvfile_path)
csv_concat.append(chat_df)
# 后续代码保持不变
csv_concat = pd.concat(csv_concat)
csv_concat = pd.concat(csv_concat)
# 更全面地处理cut标记
# 1. 将连续的cut标记合并为一个
# 2. 标记数据区块的开始和结束
processed_rows = []
skip_row = False
last_row_was_cut = False
for i in range(len(csv_concat)):
if skip_row:
skip_row = False
continue
current_row = csv_concat.iloc[i].copy()
# 处理当前行是cut的情况
if current_row['content'] == 'cut':
if current_row["content"] == "cut":
# 如果上一行已经是cut,则跳过当前行
if last_row_was_cut:
continue
# 查找连续的cut
j = i + 1
while j < len(csv_concat) and csv_concat.iloc[j]['content'] == 'cut':
while j < len(csv_concat) and csv_concat.iloc[j]["content"] == "cut":
j += 1
# 如果有连续的cut,只保留最后一个
if j > i + 1:
current_row = csv_concat.iloc[j-1].copy()
current_row = csv_concat.iloc[j - 1].copy()
skip_row = True
last_row_was_cut = True
else:
last_row_was_cut = False
processed_rows.append(current_row)
# 创建新的DataFrame
csv_concat = pd.DataFrame(processed_rows)
# csv_res里is_sender必须是01 01 01 的顺序 csv_concat里不一定是01 01
# 相差超过1小时的时间戳分为不同的对话
# temp_res为一个长度为2的队列
# 将合并后的数据保存到CSV文件中
output_dir = "./test_output"
# 生成带时间戳的文件名
import datetime
now = datetime.datetime.now()
output_file = os.path.join(output_dir, f"csv_old_.csv")
# 保存合并后的数据
csv_concat.to_csv(output_file, index=False, encoding="utf-8-sig")
print(f"已将合并后的数据保存到: {output_file}")
print(f"合并后数据总量: {len(csv_concat)} 条记录")
# csv_concat.to_csv(output_file, index=False, encoding="utf-8-sig")
# print(f"已将合并后的数据保存到: {output_file}")
# print(f"合并后数据总量: {len(csv_concat)} 条记录")
temp_res = deque(maxlen=2)
# 6种情况
@@ -230,40 +273,39 @@ def make_sft_dataset():
for i, row in csv_concat.iterrows():
if len(temp_res) == 0:
if row['content'] == 'cut':
if row["content"] == "cut":
continue
if row['is_sender'] == 0:
temp_res.append(row['content'])
last_CreateTime = row['CreateTime']
if row["is_sender"] == 0:
temp_res.append(row["content"])
last_CreateTime = row["CreateTime"]
else:
continue
elif len(temp_res) == 1:
if row['content'] == 'cut':
if row["content"] == "cut":
temp_res.clear()
last_CreateTime = row['CreateTime']
elif row['is_sender'] == 0:
last_CreateTime = row["CreateTime"]
elif row["is_sender"] == 0:
# 遇到0 清空队列再入队
temp_res.clear()
temp_res.append(row['content'])
last_CreateTime = row['CreateTime']
temp_res.append(row["content"])
last_CreateTime = row["CreateTime"]
else:
if row['CreateTime'] - last_CreateTime > pd.Timedelta('1h'):
if row["CreateTime"] - last_CreateTime > pd.Timedelta("1h"):
# 相差超过1小时清空队列
temp_res.clear()
last_CreateTime = row['CreateTime']
last_CreateTime = row["CreateTime"]
else:
# 没有相差一小时入队再全部出队
temp_res.append(row['content'])
csv_res.append({'instruction': temp_res[0], 'output': temp_res[1]})
temp_res.append(row["content"])
csv_res.append({"instruction": temp_res[0], "output": temp_res[1]})
temp_res.clear()
last_CreateTime = row['CreateTime']
last_CreateTime = row["CreateTime"]
csv_res_df = pd.DataFrame(csv_res)
print(f'处理后数据量:{csv_res_df.shape[0]}')
# csv_res_df.to_json('./data/res_csv/sft/sft-my.json', orient='records', force_ascii=False)
print(f"处理后数据量:{csv_res_df.shape[0]}")
csv_res_df.to_json('./data/res_csv/sft/sft-old-my.json', orient='records', force_ascii=False)
if __name__ == '__main__':
if __name__ == "__main__":
# make_pt_dataset()
make_sft_dataset()
+70 -45
View File
@@ -58,7 +58,8 @@ def test_single_message(processor):
)
result = processor.group_consecutive_messages([message])
assert len(result) == 1
assert len([msg for msg in result if isinstance(msg, ChatMessage)]) == 1
assert len([msg for msg in result if isinstance(msg, CutMessage)]) == 0
assert result[0].msg == "你好"
@@ -102,7 +103,8 @@ def test_consecutive_messages_same_sender(processor):
]
result = processor.group_consecutive_messages(messages)
assert len(result) == 1
assert len([msg for msg in result if isinstance(msg, ChatMessage)]) == 1
assert len([msg for msg in result if isinstance(msg, CutMessage)]) == 0
assert result[0].msg == "你好,最近怎么样,我想问个问题"
@@ -146,7 +148,8 @@ def test_messages_different_senders(processor):
]
result = processor.group_consecutive_messages(messages)
assert len(result) == 3
assert len([msg for msg in result if isinstance(msg, ChatMessage)]) == 3
assert len([msg for msg in result if isinstance(msg, CutMessage)]) == 0
assert result[0].msg == "你好"
assert result[1].msg == "你好,有什么可以帮你的"
assert result[2].msg == "我想问个问题"
@@ -167,6 +170,17 @@ def test_skip_non_text_messages(processor):
src="",
CreateTime=now,
),
ChatMessage(
id=2,
MsgSvrID=1002,
type_name="文本",
is_sender=0,
talker="user1",
room_name="testroom",
msg="先生",
src="image.jpg",
CreateTime=now + timedelta(minutes=9.9),
),
ChatMessage(
id=2,
MsgSvrID=1002,
@@ -176,7 +190,7 @@ def test_skip_non_text_messages(processor):
room_name="testroom",
msg="",
src="image.jpg",
CreateTime=now + timedelta(minutes=1),
CreateTime=now + timedelta(minutes=1+9.9),
),
ChatMessage(
id=3,
@@ -187,13 +201,16 @@ def test_skip_non_text_messages(processor):
room_name="testroom",
msg="看到图片了吗",
src="",
CreateTime=now + timedelta(minutes=2),
CreateTime=now + timedelta(minutes=1+9.9),
),
]
result = processor.group_consecutive_messages(messages)
assert len(result) == 1
assert result[0].msg == "你好,看到图片了吗"
chat_messages = [msg for msg in result if isinstance(msg, ChatMessage)]
cut_messages = [msg for msg in result if isinstance(msg, CutMessage)]
assert len(chat_messages) == 2
assert len(cut_messages) == 1
assert chat_messages[0].msg == "你好,先生"
def test_time_window_limit(processor):
@@ -225,7 +242,8 @@ def test_time_window_limit(processor):
]
result = processor.group_consecutive_messages(messages)
assert len(result) == 2
assert len([msg for msg in result if isinstance(msg, ChatMessage)]) == 2
assert len([msg for msg in result if isinstance(msg, CutMessage)]) == 0
assert result[0].msg == "你好"
assert result[1].msg == "晚上好"
@@ -236,90 +254,97 @@ def test_consecutive_messages_to_csv():
应用group_consecutive_messages函数,并将结果保存为CSV
"""
processor = MockDataProcessor()
# 获取CSV文件列表
csv_files = processor.get_csv_files()
# 如果没有找到CSV文件,创建一个模拟的CSV文件供测试使用
if not csv_files:
print("警告:未找到CSV文件,请确保数据目录中有CSV文件")
return "无法找到CSV文件"
# 存储所有处理后的消息
all_grouped_messages = []
# 处理每个CSV文件
for csv_file in csv_files:
print(f"处理文件: {csv_file}")
# 加载CSV文件中的消息
chat_messages = processor.load_csv(csv_file)
print(f"加载了 {len(chat_messages)} 条消息")
# 应用group_consecutive_messages函数
grouped_messages = processor.group_consecutive_messages(messages=chat_messages)
print(f"分组后得到 {len(grouped_messages)} 条消息")
# 添加到结果列表
all_grouped_messages.extend(grouped_messages)
# 如果没有处理到任何消息,提前返回
if not all_grouped_messages:
print("警告:未处理到任何消息")
return "未处理到任何消息"
# 将结果转换为DataFrame
messages_dict = []
for msg in all_grouped_messages:
if isinstance(msg, ChatMessage):
messages_dict.append({
"id": msg.id,
"MsgSvrID": msg.MsgSvrID,
"type_name": msg.type_name,
"is_sender": msg.is_sender,
"talker": msg.talker,
"room_name": msg.room_name,
"msg": msg.msg,
"src": msg.src,
"CreateTime": msg.CreateTime,
})
messages_dict.append(
{
"id": msg.id,
"MsgSvrID": msg.MsgSvrID,
"type_name": msg.type_name,
"is_sender": msg.is_sender,
"talker": msg.talker,
"room_name": msg.room_name,
"msg": msg.msg,
"src": msg.src,
"CreateTime": msg.CreateTime,
}
)
elif hasattr(msg, "cut_type"): # 处理CutMessage对象
messages_dict.append({
"id": None,
"MsgSvrID": None,
"type_name": msg.cut_type,
"is_sender": msg.is_sender,
"talker": None,
"room_name": None,
"msg": f"cut",
"src": None,
"CreateTime": msg.CreateTime,
})
messages_dict.append(
{
"id": None,
"MsgSvrID": None,
"type_name": msg.cut_type,
"is_sender": msg.is_sender,
"talker": None,
"room_name": None,
"msg": f"cut",
"src": None,
"CreateTime": msg.CreateTime,
}
)
# 创建DataFrame
df = pd.DataFrame(messages_dict)
# 确保输出目录存在
output_dir = "./test_output"
os.makedirs(output_dir, exist_ok=True)
# 保存为CSV文件
import datetime
now = datetime.datetime.now()
output_file = os.path.join(output_dir, f"grouped_messages_.csv")
# 使用utf-8-sig编码保存,添加BOM标记以解决中文乱码问题
df.to_csv(output_file, index=False, encoding="utf-8-sig")
# 验证结果
assert os.path.exists(output_file)
print(f"已成功保存分组消息到: {output_file}")
print(f"共保存了 {len(messages_dict)} 条消息")
# 显示前5条消息示例
if len(messages_dict) > 0:
print("\n消息示例:")
for i, msg in enumerate(messages_dict[:5]):
print(f"{i+1}. {'用户' if msg['is_sender'] == 0 else '对方'}: {msg['msg'][:50]}...")
print(
f"{i+1}. {'用户' if msg['is_sender'] == 0 else '对方'}: {msg['msg'][:50]}..."
)
return output_file