168 lines
7.1 KiB
Python
168 lines
7.1 KiB
Python
import os
|
||
import json
|
||
import pymysql
|
||
from pymysql.cursors import DictCursor
|
||
import pymongo
|
||
import requests
|
||
from dotenv import load_dotenv
|
||
from typing import List, Dict, Generator
|
||
|
||
# 加载环境变量
|
||
load_dotenv()
|
||
|
||
class StudentDataProcessorLMStudio:
|
||
def __init__(self):
|
||
# 初始化 MySQL 连接(支持批量遍历)
|
||
self.mysql_conn = pymysql.connect(
|
||
host=os.getenv("MYSQL_HOST"),
|
||
port=int(os.getenv("MYSQL_PORT")),
|
||
user=os.getenv("MYSQL_USER"),
|
||
password=os.getenv("MYSQL_PASSWORD"),
|
||
database=os.getenv("MYSQL_DB"),
|
||
charset="utf8mb4"
|
||
)
|
||
self.mysql_cursor = self.mysql_conn.cursor()
|
||
|
||
# 初始化 MongoDB 连接
|
||
self.mongo_client = pymongo.MongoClient(os.getenv("MONGO_URI"))
|
||
self.mongo_db = self.mongo_client[os.getenv("MONGO_DB")]
|
||
self.mongo_collection = self.mongo_db[os.getenv("MONGO_COLLECTION")]
|
||
|
||
# LM Studio 配置
|
||
self.lm_studio_api_url = os.getenv("LM_STUDIO_API_URL")
|
||
self.lm_studio_model = os.getenv("LM_STUDIO_MODEL_NAME")
|
||
self.lm_studio_api_token = os.getenv("LM_STUDIO_API_TOKEN")
|
||
self.batch_size = int(os.getenv("BATCH_SIZE"))
|
||
|
||
def batch_read_student_data(self) -> Generator[List[Dict], None, None]:
|
||
"""分批遍历 MySQL 所有学生数据(避免一次性加载过多数据)"""
|
||
try:
|
||
# 先获取总条数,用于分批
|
||
count_query = "SELECT COUNT(*) as total FROM student WHERE is_deleted = 0"
|
||
self.mysql_cursor.execute(count_query)
|
||
total = self.mysql_cursor.fetchone()["total"]
|
||
print(f"MySQL 中共计 {total} 条学生数据,将按每批 {self.batch_size} 条处理")
|
||
|
||
# 分批读取
|
||
offset = 0
|
||
while offset < total:
|
||
query = f"""
|
||
SELECT id, name, age, gender, class, phone, email, address
|
||
FROM student
|
||
WHERE is_deleted = 0
|
||
LIMIT {self.batch_size} OFFSET {offset}
|
||
"""
|
||
self.mysql_cursor.execute(query)
|
||
batch_data = self.mysql_cursor.fetchall()
|
||
if not batch_data:
|
||
break
|
||
yield batch_data
|
||
offset += self.batch_size
|
||
print(f"已读取第 {offset//self.batch_size} 批数据(累计 {offset} 条)")
|
||
except Exception as e:
|
||
print(f"分批读取 MySQL 数据失败:{str(e)}")
|
||
raise
|
||
|
||
def process_batch_with_lmstudio(self, batch_data: List[Dict]) -> List[Dict]:
|
||
"""调用 LM Studio 本地模型整理单批数据"""
|
||
if not batch_data:
|
||
return []
|
||
|
||
# 构造适配本地模型的提示词(更简洁,避免本地模型处理复杂指令出错)
|
||
prompt = f"""
|
||
请严格按照以下要求整理学生数据,仅返回JSON数组,不要添加任何额外文字、解释或备注:
|
||
整理规则:
|
||
1. 标准化字段:
|
||
- 姓名:去除首尾空格,仅保留中文(如" 李四 "→"李四")
|
||
- 年龄:转为整数,非数字则设为null
|
||
- 手机号:仅保留11位纯数字,不符合则设为null
|
||
- 邮箱:验证格式,无效则设为null
|
||
- 性别:非"男"/"女"则设为"未知"
|
||
- 班级:为空则设为"未分配"
|
||
2. 新增字段:
|
||
- student_id:格式为"2024"+补零后的id(如id=5→"202400005",id=123→"202400123")
|
||
- data_quality:"完整"(所有字段非null)/"部分缺失"(1-2个字段null)/"严重缺失"(≥3个字段null)
|
||
原始数据:{json.dumps(batch_data, ensure_ascii=False, indent=2)}
|
||
"""
|
||
|
||
# 构造 LM Studio API 请求体(兼容 OpenAI 格式)
|
||
payload = {
|
||
"model": self.lm_studio_model,
|
||
"messages": [
|
||
{"role": "system", "content": "你是数据整理专家,仅返回JSON格式的处理结果,无其他内容"},
|
||
{"role": "user", "content": prompt}
|
||
],
|
||
"temperature": 0.1, # 低随机性保证结果稳定
|
||
"max_tokens": 2000 # 足够容纳批量数据的JSON
|
||
}
|
||
|
||
try:
|
||
# 调用 LM Studio 本地 API
|
||
response = requests.post(
|
||
self.lm_studio_api_url,
|
||
headers={"Authorization": f"Bearer {self.lm_studio_api_token}"},
|
||
json=payload,
|
||
timeout=60 # 本地模型处理稍慢,延长超时时间
|
||
)
|
||
response.raise_for_status() # 抛出HTTP错误
|
||
result = response.json()
|
||
|
||
# 解析本地模型返回的结果
|
||
processed_text = result["choices"][0]["message"]["content"].strip()
|
||
# 清理可能的多余字符(本地模型可能返回```json开头/结尾)
|
||
if processed_text.startswith("```json"):
|
||
processed_text = processed_text.replace("```json", "").replace("```", "").strip()
|
||
processed_data = json.loads(processed_text)
|
||
return processed_data
|
||
except json.JSONDecodeError:
|
||
print(f"本地模型返回非标准JSON,跳过该批:{processed_text[:200]}")
|
||
return []
|
||
except Exception as e:
|
||
print(f"调用LM Studio失败:{str(e)}")
|
||
return []
|
||
|
||
def save_to_mongodb(self, processed_data: List[Dict]):
|
||
"""将整理后的数据存入MongoDB(支持增量插入)"""
|
||
if not processed_data:
|
||
return
|
||
try:
|
||
# 批量插入,跳过重复id(避免重复存储)
|
||
for data in processed_data:
|
||
self.mongo_collection.update_one(
|
||
{"id": data["id"]}, # 以原始id为唯一键
|
||
{"$set": data}, # 存在则更新,不存在则插入
|
||
upsert=True
|
||
)
|
||
print(f"成功存入/更新 {len(processed_data)} 条数据到MongoDB")
|
||
except Exception as e:
|
||
print(f"存入MongoDB失败:{str(e)}")
|
||
|
||
def close_connections(self):
|
||
"""关闭数据库连接"""
|
||
if self.mysql_cursor:
|
||
self.mysql_cursor.close()
|
||
if self.mysql_conn:
|
||
self.mysql_conn.close()
|
||
if self.mongo_client:
|
||
self.mongo_client.close()
|
||
print("所有连接已关闭")
|
||
|
||
def run(self):
|
||
"""执行完整流程:分批读取→本地模型处理→存入MongoDB"""
|
||
try:
|
||
# 遍历所有批次数据
|
||
for batch in self.batch_read_student_data():
|
||
# 处理单批数据
|
||
processed_batch = self.process_batch_with_lmstudio(batch)
|
||
# 存入MongoDB
|
||
self.save_to_mongodb(processed_batch)
|
||
print("✅ 全量数据处理完成!")
|
||
except Exception as e:
|
||
print(f"程序执行异常:{str(e)}")
|
||
finally:
|
||
self.close_connections()
|
||
|
||
# 程序入口
|
||
if __name__ == "__main__":
|
||
processor = StudentDataProcessorLMStudio()
|
||
processor.run() |