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:
2026-06-07 15:59:05 +08:00
co-authored by Claude Opus 4.7
commit 271a9343a5
293 changed files with 59598 additions and 0 deletions
+436
View File
@@ -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}")
+267
View File
@@ -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()
+27
View File
@@ -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()
+231
View File
@@ -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)
+10
View File
@@ -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)
+75
View File
@@ -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)}")
+5
View File
@@ -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 方式导入
+115
View File
@@ -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_idnews_titlenews_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)
+112
View File
@@ -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()
+10
View File
@@ -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