Initial commit: cc-cursor 全链路量化研究平台
7 Sprints 全部完成: Sprint 0: 基础设施 (DataManager + MariaDB) Sprint 1: 因子引擎 (34因子/12分类) Sprint 2: VectorBT 回测 (5策略+截面) Sprint 3: Optuna 优化 (+Walk-Forward) Sprint 4: ML 模型 (LightGBM+CatBoost) Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐) Sprint 6: Agent 系统 (4Agent+日报.md/.html) 生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping, save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复, RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4, CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化, indexDatas API修正 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
import env # 加载 .env 到环境变量
|
||||
import os
|
||||
import dashscope
|
||||
import pydub
|
||||
from pydub import AudioSegment
|
||||
from pydub.silence import split_on_silence
|
||||
from dashscope.audio.asr import Recognition
|
||||
from dashscope import Generation
|
||||
from http import HTTPStatus
|
||||
from mysqlHandle import MySQLDB
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
# 设置环境变量
|
||||
#os.environ["DASHSCOPE_API_KEY"] = "sk-d2d65b726068445b98b88fc3b675dbf1" # 替换为你的API Key
|
||||
|
||||
def convert_mp3_to_wav(mp3_path, output_wav_path):
|
||||
"""
|
||||
将MP3文件转换为16kHz单声道WAV格式,这是Qwen3-ASR-Flash模型的推荐格式
|
||||
参数:
|
||||
mp3_path (str): MP3文件路径
|
||||
output_wav_path (str): 输出WAV文件路径
|
||||
返回值:
|
||||
str: 转换后的WAV文件路径
|
||||
"""
|
||||
logger.info(f"开始转换MP3到WAV: {mp3_path}")
|
||||
# 加载MP3文件
|
||||
audio = AudioSegment.from_file(mp3_path, format="mp3")
|
||||
# 转换为16kHz采样率、单声道、16位深度
|
||||
audio = audio.set_frame_rate(16000).set_channels(1)
|
||||
# 导出为WAV格式
|
||||
audio.export(output_wav_path, format="wav")
|
||||
logger.info(f"✓ MP3转换完成: {output_wav_path}")
|
||||
#print(f"✓ MP3转换完成: {output_wav_path}")
|
||||
return output_wav_path
|
||||
|
||||
def split_audio_by_fixed_duration(audio_path, chunk_duration, output_folder):
|
||||
"""
|
||||
将音频文件按固定时长分割成多个片段
|
||||
参数:
|
||||
audio_path (str): 音频文件路径
|
||||
chunk_duration (int): 分片时长(毫秒)
|
||||
output_folder (str): 输出文件夹路径
|
||||
返回值:
|
||||
list: 分片文件路径列表
|
||||
"""
|
||||
# 加载音频文件
|
||||
audio = AudioSegment.from_file(audio_path)
|
||||
# 计算总时长(毫秒)
|
||||
total_duration = len(audio)
|
||||
# 分片数
|
||||
num_chunks = total_duration // chunk_duration + 1
|
||||
# 存储分片文件路径
|
||||
chunks = []
|
||||
|
||||
# 创建输出文件夹
|
||||
os.makedirs(output_folder, exist_ok=True)
|
||||
|
||||
logger.info(f"开始音频分割,总时长: {total_duration/1000:.1f}秒,将分割为{num_chunks}个片段")
|
||||
|
||||
for i in range(num_chunks):
|
||||
# 计算当前分片的起始和结束时间
|
||||
start_time = i * chunk_duration
|
||||
end_time = (i + 1) * chunk_duration
|
||||
# 提取分片音频
|
||||
chunk = audio[start_time:end_time]
|
||||
# 生成文件名
|
||||
chunk_name = f"chunk_{i}.wav"
|
||||
chunk_path = os.path.join(output_folder, chunk_name)
|
||||
# 导出分片音频
|
||||
chunk.export(chunk_path, format="wav")
|
||||
chunks.append(chunk_path)
|
||||
|
||||
# 打印处理进度
|
||||
progress = (i + 1) / num_chunks * 100
|
||||
logger.info(f"✓ 已完成分片 {i+1}/{num_chunks} ({progress:.1f}%)")
|
||||
|
||||
logger.info(f"✓ 音频分割完成,共生成{len(chunks)}个分片文件")
|
||||
return chunks
|
||||
|
||||
def split_audio_by_smart_silence(audio_path, min_silence_len, silence_thresh, output_folder):
|
||||
"""
|
||||
将音频文件按智能静音检测方式分割成多个片段,每段不超过3分钟
|
||||
参数:
|
||||
audio_path (str): 音频文件路径
|
||||
min_silence_len (int): 最小静音长度(毫秒)
|
||||
silence_thresh (int): 静音阈值(dBFS)
|
||||
output_folder (str): 输出文件夹路径
|
||||
返回值:
|
||||
list: 分片文件路径列表
|
||||
"""
|
||||
# 加载音频文件
|
||||
audio = AudioSegment.from_file(audio_path, format="wav")
|
||||
# 按静音分割
|
||||
segments = split_on_silence(
|
||||
audio,
|
||||
# 静音超过700毫秒则分割
|
||||
min_silence_len=min_silence_len,
|
||||
# 静音阈值为-40dBFS
|
||||
silence_thresh=silence_thresh,
|
||||
# 保留静音部分
|
||||
keep_silence=400
|
||||
)
|
||||
|
||||
logger.info(f"✓ 静音分割完成,共{len(segments)}个初始片段")
|
||||
|
||||
# 合并过短的片段
|
||||
merged_segments = []
|
||||
current_segment = None
|
||||
for segment in segments:
|
||||
if current_segment is None:
|
||||
current_segment = segment
|
||||
else:
|
||||
# 合并当前片段和新片段
|
||||
temp_segment = current_segment + segment
|
||||
# 如果合并后的片段超过3分钟,则单独保存当前片段
|
||||
if len(temp_segment) > 180000: # 3分钟=180,000毫秒
|
||||
merged_segments.append(current_segment)
|
||||
current_segment = segment
|
||||
else:
|
||||
current_segment = temp_segment
|
||||
# 添加最后一个片段
|
||||
if current_segment is not None:
|
||||
merged_segments.append(current_segment)
|
||||
|
||||
logger.info(f"✓ 片段合并完成,共{len(merged_segments)}个最终片段")
|
||||
|
||||
# 存储分片文件路径
|
||||
chunks = []
|
||||
|
||||
# 创建输出文件夹
|
||||
os.makedirs(output_folder, exist_ok=True)
|
||||
|
||||
logger.info(f"开始导出音频片段到: {output_folder}")
|
||||
|
||||
for i, segment in enumerate(merged_segments):
|
||||
# 生成文件名
|
||||
chunk_name = f"chunk_{i}.wav"
|
||||
chunk_path = os.path.join(output_folder, chunk_name)
|
||||
# 导出分片音频
|
||||
segment.export(chunk_path, format="wav")
|
||||
chunks.append(chunk_path)
|
||||
|
||||
# 打印处理进度
|
||||
progress = (i + 1) / len(merged_segments) * 100
|
||||
logger.info(f"✓ 已完成分片 {i+1}/{len(merged_segments)} ({progress:.1f}%)")
|
||||
|
||||
logger.info(f"✓ 智能静音分割完成,共生成{len(chunks)}个分片文件")
|
||||
return chunks
|
||||
|
||||
|
||||
def transcribe_audio(audio_path):
|
||||
"""
|
||||
使用Paraformer实时语音识别模型(通过本地文件)转录音频文件
|
||||
参数:
|
||||
audio_path (str): 音频文件路径(必须是16kHz单声道WAV)
|
||||
返回值:
|
||||
str: 识别文本,如果失败返回空字符串
|
||||
"""
|
||||
try:
|
||||
# 确保音频文件存在
|
||||
if not os.path.exists(audio_path):
|
||||
logger.error(f"音频文件不存在: {audio_path}")
|
||||
return ""
|
||||
dashscope.api_key = os.getenv('DASHSCOPE_API_KEY', '')
|
||||
# 创建识别对象
|
||||
recognition = Recognition(
|
||||
model='paraformer-realtime-v2', # 使用实时识别模型
|
||||
format='wav',
|
||||
sample_rate=16000,
|
||||
language_hints=['zh','en'], # 中文和英文
|
||||
callback=None
|
||||
)
|
||||
|
||||
# 调用识别
|
||||
logger.info(f"开始识别音频: {audio_path}")
|
||||
result = recognition.call(audio_path)
|
||||
text=[]
|
||||
if result.status_code == HTTPStatus.OK:
|
||||
# 提取识别结果
|
||||
logger.info(f"✓ {audio_path} 识别成功")
|
||||
sentence = result.get_sentence()
|
||||
text.append(merge_transcripts(sentence))
|
||||
logger.info(f"识别文本长度: {len(text[0])}")
|
||||
text.append(analyze_and_correct_text(text[0]))
|
||||
return text
|
||||
else:
|
||||
logger.error(f"❌ 任务失败: {result.message}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.error(f"识别过程中发生异常: {e}")
|
||||
return ""
|
||||
|
||||
def merge_transcripts(transcripts):
|
||||
"""
|
||||
将多段识别文本合并成完整句子(保留原始段落逻辑,用空格连接)
|
||||
参数:
|
||||
transcripts (list): 识别结果列表,每个元素为字典{'text': '识别文本'}
|
||||
返回:
|
||||
str: 合并后的完整文本
|
||||
"""
|
||||
# 输入参数检查
|
||||
if not transcripts:
|
||||
return ""
|
||||
|
||||
# 确保transcripts是可迭代对象
|
||||
if not hasattr(transcripts, '__iter__'):
|
||||
return ""
|
||||
|
||||
try:
|
||||
# 提取所有有效的text字段
|
||||
texts = []
|
||||
for t in transcripts:
|
||||
try:
|
||||
# 检查是否为字典类型且包含text字段
|
||||
if isinstance(t, dict) and 'text' in t and t['text']:
|
||||
text = t['text']
|
||||
# 确保text是字符串类型
|
||||
if isinstance(text, str) and text.strip():
|
||||
texts.append(text.strip())
|
||||
except (KeyError, TypeError, AttributeError):
|
||||
# 忽略单个元素的处理错误,继续处理其他元素
|
||||
continue
|
||||
|
||||
# 用空格连接所有段落(根据实际需求可调整连接符)
|
||||
return " ".join(texts) if texts else ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"合并转录文本时发生错误: {e}")
|
||||
return ""
|
||||
def text_correction(text):
|
||||
"""
|
||||
使用通义千问模型修正文本中的错误和标点符号
|
||||
参数:
|
||||
text (str): 需要修正的文本
|
||||
返回值:
|
||||
str: 修正后的文本
|
||||
"""
|
||||
logger.info("开始文本修正...")
|
||||
|
||||
# 构建修正提示词
|
||||
correction_prompt = """请仔细检查以下文本,修正其中的错误:
|
||||
1. 错别字和语法错误
|
||||
2. 标点符号使用错误
|
||||
3. 语句不通顺的地方
|
||||
4. 逻辑不清晰的部分
|
||||
|
||||
请直接返回修正后的完整文本,不要添加任何解释说明。"""
|
||||
|
||||
# 构建消息列表
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一个专业的文本校对助手,擅长修正文本中的各种错误。"},
|
||||
{"role": "user", "content": correction_prompt},
|
||||
{"role": "user", "content": text}
|
||||
]
|
||||
|
||||
logger.info("调用通义千问模型进行文本修正...")
|
||||
# 调用DashScope文本生成接口
|
||||
response = Generation.call(
|
||||
model="qwen-plus",
|
||||
messages=messages,
|
||||
max_tokens=30000,
|
||||
temperature=0.1, # 使用较低的温度以提高确定性
|
||||
top_p=0.5
|
||||
)
|
||||
|
||||
# 检查API调用是否成功
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"❌ 文本修正API调用失败: {response.message}")
|
||||
raise Exception(f"文本修正API调用失败: {response.message}")
|
||||
|
||||
logger.info("✓ 文本修正完成")
|
||||
# 返回修正后的文本
|
||||
return response.output.text
|
||||
|
||||
def analyze_and_correct_text(text):
|
||||
"""
|
||||
分析文本并自动修正错误
|
||||
参数:
|
||||
text (str): 待分析和修正的文本
|
||||
prompt (str): 分析提示词
|
||||
返回值:
|
||||
tuple: (修正后的文本, 分析结果)
|
||||
"""
|
||||
logger.info("开始文本分析和修正流程...")
|
||||
|
||||
# 首先修正文本错误
|
||||
corrected_text = text_correction(text)
|
||||
logger.info(f"原始文本长度: {len(text)}")
|
||||
logger.info(f"修正后文本长度: {len(corrected_text)}")
|
||||
|
||||
# 使用修正后的文本进行分析
|
||||
# analysis_result = analyze_text(corrected_text, prompt)
|
||||
|
||||
return corrected_text
|
||||
def analyze_text(text, prompt):
|
||||
"""
|
||||
使用通义千问模型分析文本
|
||||
参数:
|
||||
text (str): 待分析文本
|
||||
prompt (str): 分析提示词
|
||||
返回值:
|
||||
str: 分析结果
|
||||
"""
|
||||
logger.info("开始文本分析...")
|
||||
# 设置系统提示
|
||||
system_prompt = "你是一个专业的文本分析助手,擅长根据提示词对长文本进行深入分析。"
|
||||
# 构建消息列表
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
{"role": "user", "content": text}
|
||||
]
|
||||
|
||||
logger.info("调用通义千问模型进行文本分析...")
|
||||
# 调用DashScope文本生成接口
|
||||
response = Generation.call(
|
||||
model="qwen-plus", # 使用通义千问Plus模型进行分析
|
||||
messages=messages,
|
||||
max_tokens=8190, # 控制生成文本的最大长度
|
||||
temperature=0.3, # 控制生成文本的确定性
|
||||
top_p=0.7 # 控制生成文本的多样性
|
||||
)
|
||||
|
||||
# 检查API调用是否成功
|
||||
if response.status_code != 200:
|
||||
logger.error(f"❌ API调用失败: {response.message}")
|
||||
raise Exception(f"API调用失败: {response.message}")
|
||||
|
||||
logger.info("✓ 文本分析完成")
|
||||
# 返回分析结果
|
||||
return response.output.text
|
||||
|
||||
def process_long_audio(mp3_path, output_folder, date_str):
|
||||
"""
|
||||
处理长音频文件,分割、识别并分析
|
||||
参数:
|
||||
mp3_path (str): MP3文件路径
|
||||
prompt (str): 分析提示词
|
||||
output_folder (str): 输出文件夹路径
|
||||
返回值:
|
||||
str: 分析结果
|
||||
"""
|
||||
logger.info("开始处理长音频...")
|
||||
|
||||
# 转换MP3为WAV格式
|
||||
logger.info("步骤1/4: 转换MP3为WAV格式")
|
||||
wav_path = convert_mp3_to_wav(
|
||||
mp3_path, os.path.join(output_folder, "input.wav")
|
||||
)
|
||||
|
||||
# 分割音频
|
||||
# 可以选择固定分片或智能静音分割
|
||||
# chunks = split_audio_by_fixed_duration(wav_path, 180000, output_folder)
|
||||
logger.info("步骤2/4: 智能静音分割音频")
|
||||
chunks = split_audio_by_smart_silence(
|
||||
wav_path, 700, -40, output_folder
|
||||
)
|
||||
|
||||
# 存储所有识别文本
|
||||
transcribed_text = ""
|
||||
|
||||
# 识别每个分片
|
||||
logger.info(f"步骤3/4: 开始识别音频分片,共{len(chunks)}个分片")
|
||||
for i, chunk_path in enumerate(chunks):
|
||||
try:
|
||||
logger.info(f"识别进度: {i+1}/{len(chunks)} ({((i+1)/len(chunks)*100):.1f}%)")
|
||||
# 调用音频识别API
|
||||
text = transcribe_audio(chunk_path)
|
||||
|
||||
"""
|
||||
if not text[1].startswith('今天的新闻联播节目播送完毕'):
|
||||
prompt='请分析所给文本的新闻内容,返回一个简短标题'
|
||||
text.append(analyze_text(text[1],prompt))
|
||||
else:
|
||||
text.append('')
|
||||
"""
|
||||
# 新闻标题留空
|
||||
text.append('')
|
||||
# 添加到总文本
|
||||
#transcribed_text += text + "\n"
|
||||
# 删除临时文件
|
||||
os.remove(chunk_path)
|
||||
# 初始化数据库连接
|
||||
db = MySQLDB() # 使用默认参数连接数据库
|
||||
try:
|
||||
# 插入数据示例
|
||||
user_data = {
|
||||
"news_days": date_str,
|
||||
"daily_sub_id": i,
|
||||
"news_raw": text[0],
|
||||
"news_improve": text[1],
|
||||
"news_title": text[2]
|
||||
}
|
||||
user_id = db.insert_data("xwlb_daily", user_data)
|
||||
finally:
|
||||
# 关闭连接
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.error(f"识别失败: {chunk_path}, 错误: {e}")
|
||||
# 可以在这里添加重试逻辑
|
||||
|
||||
# 分析识别文本
|
||||
"""
|
||||
print("步骤4/4: 分析识别文本")
|
||||
print(f"识别文本长度: {len(transcribed_text)}")
|
||||
print(f"识别文本内容: {transcribed_text}")
|
||||
if len(transcribed_text) < 1:
|
||||
print("识别文本为空,跳过分析处理")
|
||||
return "识别文本为空,无法进行分析"
|
||||
analysis_result = analyze_text(transcribed_text, prompt)
|
||||
"""
|
||||
logger.info("✓ 长音频处理完成")
|
||||
# 返回分析结果
|
||||
return ""
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
# MP3文件路径
|
||||
mp3_path = "20251002.mp3"
|
||||
# 分析提示词
|
||||
prompt = "请总结这段由中国中央电视台新闻联播音频转为文字的文本,理解其主要内容并提取其中的关键信息。"
|
||||
# 输出文件夹
|
||||
output_folder = "audio_processing"
|
||||
|
||||
# 处理长音频
|
||||
try:
|
||||
result = process_long_audio(
|
||||
mp3_path, prompt, output_folder
|
||||
)
|
||||
# 打印分析结果
|
||||
print("分析结果:\n")
|
||||
print(result)
|
||||
except Exception as e:
|
||||
print(f"处理失败: {e}")
|
||||
@@ -0,0 +1,267 @@
|
||||
import env # 加载 .env 到环境变量
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
import os
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DeepSeekAPI:
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
"""
|
||||
初始化DeepSeek API客户端
|
||||
|
||||
Args:
|
||||
api_key: DeepSeek API密钥,如果为None则从环境变量获取
|
||||
"""
|
||||
self.api_key = api_key or os.getenv('DEEPSEEK_API_KEY')
|
||||
if not self.api_key:
|
||||
logger.warning("API密钥未提供且环境变量DEEPSEEK_API_KEY未设置")
|
||||
|
||||
self.api_url = "https://api.deepseek.com/v1/chat/completions"
|
||||
self.max_retries = 3
|
||||
self.retry_delay = 2 # 秒
|
||||
|
||||
# 默认系统提示词
|
||||
self.default_system_prompt = """你是一个专业的AI助手,能够准确理解用户需求并提供高质量的回答。
|
||||
请根据用户的输入进行适当的处理和分析,保持回答的专业性和准确性。注意:所处理文字来自中央电视台新闻联播节目转文字,请在内容审查时重点考虑。"""
|
||||
|
||||
def _handle_api_error(self, response: requests.Response) -> str:
|
||||
"""
|
||||
处理API错误响应
|
||||
|
||||
Args:
|
||||
response: API响应对象
|
||||
|
||||
Returns:
|
||||
错误描述信息
|
||||
"""
|
||||
error_msg = f"API请求失败: {response.status_code} {response.reason}"
|
||||
|
||||
try:
|
||||
error_data = response.json()
|
||||
if 'error' in error_data:
|
||||
error_msg += f" - {error_data['error'].get('message', '未知错误')}"
|
||||
logger.error(f"API错误详情: {error_data}")
|
||||
except json.JSONDecodeError:
|
||||
error_msg += f" - 响应内容: {response.text[:200]}"
|
||||
|
||||
return error_msg
|
||||
|
||||
def _make_api_request(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
发送API请求并处理响应
|
||||
|
||||
Args:
|
||||
payload: 请求数据
|
||||
|
||||
Returns:
|
||||
API响应数据
|
||||
|
||||
Raises:
|
||||
Exception: 当所有重试都失败时抛出异常
|
||||
"""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}"
|
||||
}
|
||||
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(self.max_retries):
|
||||
try:
|
||||
logger.info(f"发送API请求 (尝试 {attempt + 1}/{self.max_retries})")
|
||||
|
||||
response = requests.post(
|
||||
self.api_url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=60 # 60秒超时
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
elif response.status_code == 400:
|
||||
# 400错误通常是请求格式问题,不需要重试
|
||||
error_msg = self._handle_api_error(response)
|
||||
raise Exception(f"请求参数错误: {error_msg}")
|
||||
elif response.status_code == 401:
|
||||
# 401未授权错误,不需要重试
|
||||
raise Exception("API密钥无效或未授权,请检查您的API密钥")
|
||||
elif response.status_code == 429:
|
||||
# 速率限制,需要重试
|
||||
logger.warning("达到速率限制,等待后重试...")
|
||||
time.sleep(self.retry_delay * (attempt + 1))
|
||||
continue
|
||||
elif 500 <= response.status_code < 600:
|
||||
# 服务器错误,需要重试
|
||||
logger.warning(f"服务器错误 {response.status_code},等待后重试...")
|
||||
time.sleep(self.retry_delay * (attempt + 1))
|
||||
continue
|
||||
else:
|
||||
error_msg = self._handle_api_error(response)
|
||||
raise Exception(f"API请求失败: {error_msg}")
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
last_exception = Exception(f"请求超时 (尝试 {attempt + 1})")
|
||||
logger.warning(f"请求超时,等待后重试...")
|
||||
time.sleep(self.retry_delay * (attempt + 1))
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
last_exception = Exception(f"网络连接错误 (尝试 {attempt + 1})")
|
||||
logger.warning(f"网络连接错误,等待后重试...")
|
||||
time.sleep(self.retry_delay * (attempt + 1))
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
last_exception = Exception(f"请求异常: {str(e)}")
|
||||
logger.warning(f"请求异常,等待后重试...")
|
||||
time.sleep(self.retry_delay * (attempt + 1))
|
||||
|
||||
# 所有重试都失败
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
else:
|
||||
raise Exception("API请求失败,未知错误")
|
||||
|
||||
def process_text(self,
|
||||
prompt: str,
|
||||
text: str,
|
||||
system_prompt: Optional[str] = None,
|
||||
model: str = "deepseek-chat",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 2000) -> str:
|
||||
"""
|
||||
处理文本的通用方法
|
||||
|
||||
Args:
|
||||
prompt: 用户提示词
|
||||
text: 需要处理的文本(约1万字符)
|
||||
system_prompt: 系统提示词,如果为None则使用默认值
|
||||
model: 使用的模型
|
||||
temperature: 生成温度
|
||||
max_tokens: 最大生成token数
|
||||
|
||||
Returns:
|
||||
处理后的文本
|
||||
|
||||
Raises:
|
||||
Exception: 当处理失败时抛出包含详细信息的异常
|
||||
"""
|
||||
# 输入验证
|
||||
if not self.api_key:
|
||||
raise Exception("API密钥未设置,请提供api_key或设置DEEPSEEK_API_KEY环境变量")
|
||||
|
||||
if not prompt or not text:
|
||||
raise Exception("prompt和text不能为空")
|
||||
|
||||
# 检查文本长度(约1万字符)
|
||||
if len(text) > 15000: # 留一些余量
|
||||
logger.warning(f"输入文本长度({len(text)}字符)较长,可能会超过上下文限制")
|
||||
|
||||
# 准备系统提示词
|
||||
system_content = system_prompt or self.default_system_prompt
|
||||
|
||||
# 构建消息
|
||||
messages = [
|
||||
{"role": "system", "content": system_content},
|
||||
{"role": "user", "content": f"{prompt}\n\n文本内容:\n{text}"}
|
||||
]
|
||||
|
||||
# 构建请求数据
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
try:
|
||||
# 发送API请求
|
||||
response_data = self._make_api_request(payload)
|
||||
|
||||
# 解析响应
|
||||
if 'choices' in response_data and len(response_data['choices']) > 0:
|
||||
result = response_data['choices'][0]['message']['content']
|
||||
logger.info("文本处理成功完成")
|
||||
return result.strip()
|
||||
else:
|
||||
raise Exception("API响应格式异常,未找到有效结果")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"文本处理失败: {str(e)}")
|
||||
raise Exception(f"文本处理失败: {str(e)}")
|
||||
|
||||
def process_text_with_fallback(self,
|
||||
prompt: str,
|
||||
text: str,
|
||||
system_prompt: Optional[str] = None,
|
||||
**kwargs) -> str:
|
||||
"""
|
||||
带降级处理的文本处理方法
|
||||
|
||||
Args:
|
||||
prompt: 用户提示词
|
||||
text: 需要处理的文本
|
||||
system_prompt: 系统提示词
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
处理后的文本,如果API调用失败则返回降级结果
|
||||
"""
|
||||
try:
|
||||
return self.process_text(prompt, text, system_prompt, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"API调用失败,使用降级处理: {str(e)}")
|
||||
# 这里可以添加降级逻辑,比如返回原始文本或简单处理
|
||||
return f"处理失败,返回原始文本(错误: {str(e)})\n\n{text}"
|
||||
|
||||
# 使用示例
|
||||
def deepseek_text(text, prompt):
|
||||
# 初始化API客户端
|
||||
# 方式1: 直接传入API密钥
|
||||
# api_client = DeepSeekAPI(api_key="your_deepseek_api_key_here")
|
||||
|
||||
# 方式2: 从环境变量读取(推荐)
|
||||
api_client = DeepSeekAPI() # API key 从环境变量 DEEPSEEK_API_KEY 读取
|
||||
|
||||
# 示例文本(约1万字符)
|
||||
# sample_text = "这里是你的长文本内容..." * 500 # 模拟长文本
|
||||
|
||||
# 自定义系统提示词(可选)
|
||||
custom_system_prompt = "你是一个专业的文本分析助手,擅长根据提示词对长文本进行深入分析。"
|
||||
|
||||
try:
|
||||
# 处理文本
|
||||
result = api_client.process_text(
|
||||
model="deepseek-reasoner",
|
||||
prompt=prompt,
|
||||
text=text,
|
||||
system_prompt=custom_system_prompt,
|
||||
temperature=0.5,
|
||||
max_tokens=20000
|
||||
)
|
||||
|
||||
#print("处理结果:")
|
||||
#print(result)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理失败: {e}")
|
||||
|
||||
# 使用降级方法
|
||||
fallback_result = api_client.process_text_with_fallback(
|
||||
prompt=prompt,
|
||||
text=text,
|
||||
system_prompt=custom_system_prompt
|
||||
)
|
||||
#print("降级处理结果:")
|
||||
#print(fallback_result)
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
deepseek_text()
|
||||
@@ -0,0 +1,27 @@
|
||||
"""video 模块独立 .env 加载器 — 与 djapi/env_loader.py 功能一致但非 Django 依赖"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _load_dotenv():
|
||||
"""从项目根目录 .env 加载环境变量(不覆盖已有)"""
|
||||
# video/env.py → video/ → api/ → djapi/ (项目根)
|
||||
base_dir = Path(__file__).resolve().parent.parent.parent
|
||||
dotenv_path = base_dir / '.env'
|
||||
|
||||
if not dotenv_path.exists():
|
||||
return
|
||||
|
||||
with open(dotenv_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or '=' not in line:
|
||||
continue
|
||||
key, _, value = line.partition('=')
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if key and key not in os.environ:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
_load_dotenv()
|
||||
@@ -0,0 +1,231 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import re,os,subprocess
|
||||
from datetime import timedelta, date
|
||||
import yt_dlp
|
||||
from audioRead import *
|
||||
from newsProcess import news_to_db
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_xwlb_video_link(url):
|
||||
"""
|
||||
从央视网新闻联播页面抓取历史完整版视频链接
|
||||
"""
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Referer': 'https://tv.cctv.com/'
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.encoding = 'utf-8'
|
||||
if response.status_code != 200:
|
||||
logger.error(f"请求失败,状态码: {response.status_code}")
|
||||
#print(f"请求失败,状态码: {response.status_code}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"请求异常: {e}")
|
||||
return []
|
||||
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
video_links = []
|
||||
|
||||
# 查找所有包含“完整版《新闻联播》”的链接
|
||||
# 方法1: 查找包含 <i class="sql0">完整版</i>《新闻联播》 的 a 标签
|
||||
for a_tag in soup.find_all('a', href=True):
|
||||
# 检查文本中是否包含“完整版”和“新闻联播”
|
||||
title_text = a_tag.get_text(strip=True)
|
||||
inner_html = str(a_tag)
|
||||
|
||||
# 判断是否是“完整版《新闻联播》”的链接
|
||||
if ('完整版' in title_text and '新闻联播' in title_text) or \
|
||||
(re.search(r'<i[^>]*>完整版</i>\s*《新闻联播》', inner_html)):
|
||||
|
||||
video_url = a_tag['href']
|
||||
# 提取日期信息(从标题或链接中)
|
||||
date_match = re.search(r'\d{8}', title_text)
|
||||
if not date_match:
|
||||
# 从链接中提取日期,如 /2025/09/25/...VID...250925.shtml
|
||||
date_match = re.search(r'/(\d{4})/(\d{2})/(\d{2})/', video_url)
|
||||
if date_match:
|
||||
year, month, day = date_match.groups()
|
||||
date_str = f"{year}{month}{day}"
|
||||
else:
|
||||
date_str = "未知日期"
|
||||
else:
|
||||
date_str = date_match.group()
|
||||
|
||||
video_links.append({
|
||||
'date': date_str,
|
||||
'title': title_text.strip(),
|
||||
'url': video_url,
|
||||
'page_url': url
|
||||
})
|
||||
logger.info(f"✅ 找到新闻联播完整版: {date_str} -> {video_url}")
|
||||
|
||||
return video_url
|
||||
|
||||
def xwlb_urls(start: str, end: str):
|
||||
"""
|
||||
start/end 格式 '20240925'
|
||||
返回列表,如 ['https://tv.cctv.com/lm/xwlb/day/20240925.shtml', ...]
|
||||
"""
|
||||
d0 = date(int(start[:4]), int(start[4:6]), int(start[6:8]))
|
||||
|
||||
d1 = date(int(end[:4]), int(end[4:6]), int(end[6:8]))
|
||||
urls = []
|
||||
for n in range((d1 - d0).days + 1):
|
||||
day = d0 + timedelta(days=n)
|
||||
urls.append({"url": f"https://tv.cctv.com/lm/xwlb/day/{day:%Y%m%d}.shtml", "date": f"{day:%Y%m%d}"})
|
||||
#print(urls)
|
||||
return urls
|
||||
|
||||
def get_all_video_links(start: str, end: str):
|
||||
base_urls=xwlb_urls(start,end)
|
||||
#print(base_urls)
|
||||
video_urls = []
|
||||
for url in base_urls:
|
||||
video=get_xwlb_video_link(url['url'])
|
||||
video_urls.append({"url":video,"date":url['date']})
|
||||
|
||||
return video_urls
|
||||
|
||||
'''
|
||||
get_xwlb_video_link() 方法获得的url,
|
||||
urls like: https://tv.cctv.com/2024/10/30/VIDEUlPz1Qusy41JFQj3LMLd241030.shtml
|
||||
通过yt-dlp下载视频,保存为mp4文件,并用ffmpeg提取音频为mp3文件,文件名使用url 的日期部分,如上面的url应保存为 20241030.mp4 和 20241030.mp3
|
||||
文件保存路径为当前目录下的 xwlb_video 文件夹,若不存在则创建。
|
||||
'''
|
||||
|
||||
|
||||
def download_and_extract_audio(video_url,date_str,download_dir):
|
||||
"""
|
||||
使用yt-dlp下载视频并提取音频
|
||||
"""
|
||||
# 从URL中提取日期
|
||||
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
|
||||
# 构建文件路径
|
||||
mp4_path = os.path.join(download_dir, f"{date_str}.mp4")
|
||||
mp3_path = os.path.join(download_dir, f"{date_str}.mp3")
|
||||
|
||||
try:
|
||||
# 使用yt-dlp库下载视频
|
||||
logger.info(f"📥 开始下载 {date_str} 的视频...")
|
||||
# 配置yt-dlp选项
|
||||
ydl_opts = {
|
||||
'outtmpl': mp4_path,
|
||||
'format': 'best[ext=mp4]/best',
|
||||
'progress_hooks': [lambda d: print(f"\r📥 下载进度: {d.get('_percent_str', 'N/A').strip()} | {d.get('_speed_str', 'N/A').strip()} | 已下载: {d.get('_downloaded_bytes_str', 'N/A')}", end='') if d['status'] == 'downloading' else None],
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([video_url])
|
||||
logger.info(f"📥 下载 {date_str} 完成")
|
||||
|
||||
# 使用ffmpeg提取音频
|
||||
logger.info(f"🎵 开始提取 {date_str} 的音频...")
|
||||
result = subprocess.run([
|
||||
"ffmpeg",
|
||||
"-i", mp4_path,
|
||||
"-c:a", "libmp3lame", # 明确指定MP3编码器
|
||||
"-q:a", "0",
|
||||
"-map", "a",
|
||||
mp3_path,
|
||||
"-y" # 覆盖已存在文件
|
||||
], check=True, stdout=None, stderr=None)
|
||||
logger.info(f"🎵 提取 {date_str} 音频完成")
|
||||
|
||||
logger.info(f"✅ 成功处理 {date_str}: {mp4_path}, {mp3_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 处理 {date_str} 时发生异常: {e}")
|
||||
|
||||
# 在get_all_video_links函数后添加调用代码
|
||||
def process_videos(start_date, end_date):
|
||||
"""
|
||||
处理指定日期范围内的所有视频
|
||||
"""
|
||||
video_urls = get_all_video_links(start_date, end_date)
|
||||
|
||||
for sub_url in video_urls:
|
||||
if sub_url: # 确保url不为空
|
||||
"""
|
||||
date_match = re.search(r'/(\d{4})/(\d{2})/(\d{2})/', url)
|
||||
if not date_match:
|
||||
print(f"❌ 无法从URL提取日期: {url}")
|
||||
return
|
||||
|
||||
year, month, day = date_match.groups()
|
||||
date_str = f"{year}{month}{day}"
|
||||
"""
|
||||
date_str= sub_url['date']
|
||||
url = sub_url['url']
|
||||
|
||||
# 创建保存目录
|
||||
download_dir = "/home/simon/myquant/djapi/api/video/xwlb_video"
|
||||
download_and_extract_audio(url,date_str,download_dir)
|
||||
print("=" * 80)
|
||||
# MP3文件路径
|
||||
mp3_path = os.path.join(download_dir, f"{date_str}.mp3")
|
||||
# 分析提示词
|
||||
# prompt = "请总结这段由中国中央电视台新闻联播音频转为文字的文本,理解其主要内容并提取其中的关键信息。"
|
||||
# 输出文件夹
|
||||
output_folder = "/home/simon/myquant/djapi/api/video/audio_processing"
|
||||
|
||||
# 处理长音频
|
||||
try:
|
||||
result = process_long_audio(mp3_path, output_folder,date_str)
|
||||
news_to_db(date_str)
|
||||
# 打印分析结果
|
||||
print("分析结果:\n")
|
||||
print(result)
|
||||
except Exception as e:
|
||||
print(f"处理失败: {e}")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
# ========================
|
||||
# 主程序执行
|
||||
# ========================
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
# 检查命令行参数
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python getVideo5.py <start_date> <end_date>")
|
||||
print("日期格式: YYYYMMDD")
|
||||
sys.exit(1)
|
||||
|
||||
start_date = sys.argv[1]
|
||||
end_date = sys.argv[2] if len(sys.argv) > 2 and sys.argv[2] else start_date
|
||||
|
||||
# 检查日期格式
|
||||
date_pattern = r'^\d{8}$'
|
||||
if not re.match(date_pattern, start_date) or not re.match(date_pattern, end_date):
|
||||
print("错误: 日期格式必须为 YYYYMMDD")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查日期有效性
|
||||
try:
|
||||
#start_dt = datetime.strptime(start_date, '%Y%m%d')
|
||||
#end_dt = datetime.strptime(end_date, '%Y%m%d')
|
||||
|
||||
if start_date > end_date:
|
||||
print(f"错误: start_date {start_date} 不能大于 end_date {end_date}")
|
||||
sys.exit(1)
|
||||
print("正在抓取央视《新闻联播》历史完整版视频链接...")
|
||||
print("=" * 80)
|
||||
process_videos(start_date, end_date)
|
||||
exit()
|
||||
except ValueError as e:
|
||||
print(f"错误: 无效日期 - {e}")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from getVideo5 import process_videos
|
||||
from datetime import datetime
|
||||
|
||||
# 获取当日日期并格式化为yyyymmdd
|
||||
today = datetime.now().strftime("%Y%m%d")
|
||||
start_date = today
|
||||
end_date = today
|
||||
|
||||
# 执行process_videos方法
|
||||
process_videos(start_date, end_date)
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
xwlb_daily 表结构如下:
|
||||
+--------------+---------+------+-----+---------+----------------+
|
||||
| Field | Type | Null | Key | Default | Extra |
|
||||
+--------------+---------+------+-----+---------+----------------+
|
||||
| nid | int(11) | NO | PRI | NULL | auto_increment |
|
||||
| news_days | date | NO | | NULL | |
|
||||
| daily_sub_id | int(11) | NO | | NULL | |
|
||||
| news_raw | text | NO | | NULL | |
|
||||
| news_improve | text | NO | | NULL | |
|
||||
| news_title | text | NO | | NULL | |
|
||||
+--------------+---------+------+-----+---------+----------------+
|
||||
"""
|
||||
|
||||
from mysqlHandle import MySQLDB
|
||||
from getVideo5 import process_videos
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_missing_dates(start_date, end_date):
|
||||
"""
|
||||
给定日期范围,查询xwlb_daily表中缺失的日期
|
||||
"""
|
||||
try:
|
||||
# 连接数据库
|
||||
db = MySQLDB()
|
||||
|
||||
# 查询指定日期范围内存在的所有日期
|
||||
result = db.query_data(
|
||||
table="xwlb_daily",
|
||||
columns="DISTINCT(news_days) as news_days",
|
||||
where="news_days BETWEEN %s AND %s order by news_days",
|
||||
params=(start_date, end_date)
|
||||
)
|
||||
# 获取所有存在的日期
|
||||
existing_dates = [row['news_days'] for row in result]
|
||||
|
||||
# 生成完整的日期范围
|
||||
start = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||||
end = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||||
|
||||
all_dates = []
|
||||
current_date = start
|
||||
while current_date <= end:
|
||||
all_dates.append(current_date)
|
||||
current_date = current_date + timedelta(days=1)
|
||||
|
||||
# 找出缺失的日期
|
||||
existing_set = set(existing_dates)
|
||||
missing_dates = [date.strftime('%Y%m%d') for date in all_dates if date not in existing_set]
|
||||
|
||||
logger.info(f"查询日期范围 {start_date} 到 {end_date}")
|
||||
logger.info(f"存在 {len(existing_dates)} 天数据,缺失 {len(missing_dates)} 天数据")
|
||||
logger.info(f"缺失日期: {missing_dates}")
|
||||
return missing_dates
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询缺失日期时出错: {str(e)}")
|
||||
return []
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
start_date = "2025-01-01"
|
||||
end_date = "2025-10-25"
|
||||
missing_dates=get_missing_dates(start_date, end_date)
|
||||
for date_str in missing_dates:
|
||||
logger.info(f"正在处理缺失日期: {date_str}")
|
||||
try:
|
||||
process_videos(date_str,date_str)
|
||||
logger.info(f"成功处理日期: {date_str}")
|
||||
except Exception as e:
|
||||
logger.error(f"处理日期 {date_str} 时出错: {str(e)}")
|
||||
@@ -0,0 +1,5 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from utils.mysql_handler import MySQLDB # noqa: F401, E402 — video 模块独立运行,sys.path 方式导入
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
xwlb_daily 表结构如下:
|
||||
+--------------+---------+------+-----+---------+----------------+
|
||||
| Field | Type | Null | Key | Default | Extra |
|
||||
+--------------+---------+------+-----+---------+----------------+
|
||||
| nid | int(11) | NO | PRI | NULL | auto_increment |
|
||||
| news_days | date | NO | | NULL | |
|
||||
| daily_sub_id | int(11) | NO | | NULL | |
|
||||
| news_raw | text | NO | | NULL | |
|
||||
| news_improve | text | NO | | NULL | |
|
||||
| news_title | text | NO | | NULL | |
|
||||
+--------------+---------+------+-----+---------+----------------+
|
||||
xwlb_daily_ext 表结构如下:
|
||||
+--------------+--------------+------+-----+---------+----------------+
|
||||
| Field | Type | Null | Key | Default | Extra |
|
||||
+--------------+--------------+------+-----+---------+----------------+
|
||||
| extid | int(11) | NO | PRI | NULL | auto_increment |
|
||||
| news_date | date | NO | | NULL | |
|
||||
| sub_id | tinyint(4) | NO | | NULL | |
|
||||
| news_title | varchar(256) | NO | | NULL | |
|
||||
| news_content | text | NO | | NULL | |
|
||||
+--------------+--------------+------+-----+---------+----------------+
|
||||
|
||||
获取给定日期的所有news_improve字段内容,以daily_sub_id 顺序拼接为一个字符串返回。
|
||||
调用mysqlHandler中的方法执行SQL查询。
|
||||
"""
|
||||
from mysqlHandle import MySQLDB
|
||||
from deepseek import deepseek_text
|
||||
import json
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_news_improve_by_date(target_date):
|
||||
"""
|
||||
获取指定日期的所有news_improve内容,按daily_sub_id顺序拼接
|
||||
|
||||
Args:
|
||||
target_date: 目标日期,格式为'YYYY-MM-DD'
|
||||
|
||||
Returns:
|
||||
str: 拼接后的字符串
|
||||
"""
|
||||
try:
|
||||
# 创建数据库连接对象
|
||||
db = MySQLDB()
|
||||
# 查询目标日期在xwlb_daily_ext表中的记录数量
|
||||
count_result = db.query_data(
|
||||
table="xwlb_daily_ext",
|
||||
columns="COUNT(*) as count",
|
||||
where="news_date = %s",
|
||||
params=(target_date,)
|
||||
)
|
||||
# 如果记录数量存在且大于5条,则返回空字符串
|
||||
if count_result and count_result[0]['count'] > 5:
|
||||
return None
|
||||
# 重新创建数据库连接对象,因为每次查询都会关闭连接
|
||||
db = MySQLDB()
|
||||
# 查询目标日期在xwlb_daily表中的news_improve字段,按daily_sub_id升序排列
|
||||
result = db.query_data(
|
||||
table="xwlb_daily",
|
||||
columns="news_improve",
|
||||
where="news_days = %s order by daily_sub_id ASC",
|
||||
params=(target_date,))
|
||||
# 如果查询结果不为空
|
||||
if result:
|
||||
# 将每条记录的news_improve字段用换行符连接成字符串
|
||||
combined_content = '\n'.join([row['news_improve'] for row in result])
|
||||
# 返回拼接后的字符串
|
||||
return combined_content
|
||||
# 查询结果为空时返回空字符串
|
||||
return None
|
||||
except Exception as e:
|
||||
#print(f"查询失败: {e}")
|
||||
logger.error(f"查询失败: {e}")
|
||||
return None
|
||||
|
||||
def news_to_db(target_date):
|
||||
result = get_news_improve_by_date(target_date)
|
||||
if result is None:
|
||||
logger.warning(f"日期 {target_date} 没有新闻内容或者已经存在处理后的记录。跳过")
|
||||
return None
|
||||
logger.info(f"日期 {target_date} 的新闻内容长度:{len(result)} 字符")
|
||||
|
||||
prompt= "###请根据下面新闻内容的文本逻辑 \n - 帮我分割成各个独立的新闻内容(注意:不要修改新闻本身,仅分割文本),并给每个新闻总结一个标题; \n - 如果遇到'国内快讯'、'国际快讯'或'联播快讯',也请根据每个条快讯分割为一个新闻以及新闻标题; \n - 返回json格式。json格式包含:news_id,news_title,news_content; news_id从1开始递增。"
|
||||
try:
|
||||
response = deepseek_text(result, prompt)
|
||||
news_list = json.loads(response)
|
||||
db = MySQLDB()
|
||||
for news in news_list:
|
||||
db.insert_data(
|
||||
table="xwlb_daily_ext",
|
||||
data={
|
||||
"news_date": target_date,
|
||||
"sub_id": news["news_id"],
|
||||
"news_title": news["news_title"][:256], # 确保不超过varchar(256)限制
|
||||
"news_content": news["news_content"]
|
||||
}
|
||||
)
|
||||
logger.info(f"成功插入 {len(news_list)} 条新闻到数据库")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析失败: {e}")
|
||||
logger.error(f"DeepSeek API返回内容: {response}")
|
||||
except Exception as e:
|
||||
logger.error(f"插入数据库失败: {e}")
|
||||
#print(f"DeepSeek API返回结果:{response}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from datetime import datetime
|
||||
#提供日期参数,格式:YYYY-MM-DD
|
||||
|
||||
target_date = datetime.now().strftime('%Y-%m-%d')
|
||||
news_to_db(target_date)
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
newsRedo — 手动重新执行新闻 AI 分割流程。
|
||||
|
||||
用法:
|
||||
python newsRedo.py # 默认当天日期
|
||||
python newsRedo.py 20250601 # yyyymmdd 格式
|
||||
python newsRedo.py 2025-06-01 # yyyy-mm-dd 格式
|
||||
|
||||
流程:
|
||||
1. 检查 xwlb_daily_ext 是否已有 >5 条 → 已处理过,正常跳过
|
||||
2. 检查 xwlb_daily 是否有当天记录 → 无记录则先跑 getVideo5 全流程
|
||||
3. 有记录但未处理 → 直接执行 news_to_db() AI 分割
|
||||
"""
|
||||
import sys
|
||||
import re
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from mysqlHandle import MySQLDB
|
||||
from newsProcess import news_to_db
|
||||
from getVideo5 import process_videos
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_date(date_str):
|
||||
"""解析日期,返回 (yyyymmdd_str, yyyy_mm_dd_str),或报错退出"""
|
||||
if not date_str:
|
||||
today = datetime.now()
|
||||
d8 = today.strftime('%Y%m%d')
|
||||
d10 = today.strftime('%Y-%m-%d')
|
||||
logger.info(f"未指定日期,使用当天: {d10}")
|
||||
return d8, d10
|
||||
|
||||
if re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
||||
try:
|
||||
datetime.strptime(date_str, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
print(f"无效日期: {date_str}")
|
||||
sys.exit(1)
|
||||
return date_str.replace('-', ''), date_str
|
||||
|
||||
if re.match(r'^\d{8}$', date_str):
|
||||
try:
|
||||
datetime.strptime(date_str, '%Y%m%d')
|
||||
except ValueError:
|
||||
print(f"无效日期: {date_str}")
|
||||
sys.exit(1)
|
||||
return date_str, f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]}"
|
||||
|
||||
print("日期格式错误,请使用 yyyymmdd 或 yyyy-mm-dd 格式")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
date_str = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
date_d8, date_d10 = _parse_date(date_str)
|
||||
|
||||
db = MySQLDB()
|
||||
|
||||
# 1. 检查 xwlb_daily_ext 是否已处理过
|
||||
try:
|
||||
ext_count = db.query_data(
|
||||
table="xwlb_daily_ext",
|
||||
columns="COUNT(*) as count",
|
||||
where="news_date = %s",
|
||||
params=(date_d10,)
|
||||
)
|
||||
if ext_count and ext_count[0]['count'] > 5:
|
||||
logger.info(f"日期 {date_d10} 已有 {ext_count[0]['count']} 条精编记录,无需重新处理。")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"查询 xwlb_daily_ext 失败: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
db = MySQLDB()
|
||||
|
||||
# 2. 检查 xwlb_daily 是否有当天数据
|
||||
try:
|
||||
daily_count = db.query_data(
|
||||
table="xwlb_daily",
|
||||
columns="COUNT(*) as count",
|
||||
where="news_days = %s",
|
||||
params=(date_d10,)
|
||||
)
|
||||
has_daily = daily_count and daily_count[0]['count'] > 0
|
||||
except Exception as e:
|
||||
logger.error(f"查询 xwlb_daily 失败: {e}")
|
||||
has_daily = False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# 3. 分支处理
|
||||
if has_daily:
|
||||
logger.info(f"日期 {date_d10} 在 xwlb_daily 中有记录,直接执行 AI 分割。")
|
||||
try:
|
||||
news_to_db(date_d10)
|
||||
except Exception as e:
|
||||
logger.error(f"news_to_db 执行出错: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
logger.info(f"日期 {date_d10} 在 xwlb_daily 中无记录,重新执行视频下载全流程。")
|
||||
try:
|
||||
process_videos(date_d8, date_d8)
|
||||
except Exception as e:
|
||||
logger.error(f"process_videos 执行出错: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,10 @@
|
||||
aiofiles==25.1.0
|
||||
aiohttp==3.12.15
|
||||
beautifulsoup4==4.14.2
|
||||
dashscope==1.24.6
|
||||
m3u8==6.0.0
|
||||
mysql_connector_repackaged==0.3.1
|
||||
playwright==1.55.0
|
||||
pydub==0.25.1
|
||||
Requests==2.32.5
|
||||
yt_dlp==2025.11.12
|
||||
Reference in New Issue
Block a user