feat: 完善代理重试机制,添加数据验证告警,新增README文档

This commit is contained in:
shengyudong@yunqueai.net
2026-01-16 18:36:52 +08:00
parent 322ac74336
commit b518e6aacf
55 changed files with 13202 additions and 34781 deletions

View File

@@ -4,18 +4,20 @@
数据同步守护进程
功能:
1. 24小时不间断运行
2. 在每天午夜00:00自动执行数据抓取和同步
1. 24小时不间断运行仅在工作时间8:00-24:00执行任务
2. 每隔1小时自动执行数据抓取和同步
3. 自动执行流程:
- 从百家号API抓取最新数据
- 生成CSV文件包含从数据库查询的author_id
- 将CSV数据导入到数据库
4. 支持手动触发刷新
5. 详细的日志记录
6. 非工作时间0:00-8:00自动休眠减少API请求压力
使用场景:
- 24/7运行每天凌晨自动更新数据
- 24/7运行在工作时间8:00-24:00每隔1小时自动更新数据
- 无需人工干预,自动化数据同步
- 避免在夜间时段进行数据抓取
"""
import sys
@@ -38,11 +40,19 @@ from export_to_csv import DataExporter
from import_csv_to_database import CSVImporter
from log_config import setup_logger
# 导入数据验证与短信告警模块
try:
from data_validation_with_sms import DataValidationWithSMS
VALIDATION_AVAILABLE = True
except ImportError:
print("[!] 数据验证模块未找到,验证功能将不可用")
VALIDATION_AVAILABLE = False
class DataSyncDaemon:
"""数据同步守护进程"""
def __init__(self, use_proxy: bool = False, load_from_db: bool = True, days: int = 7, max_retries: int = 3):
def __init__(self, use_proxy: bool = False, load_from_db: bool = True, days: int = 7, max_retries: int = 3, enable_validation: bool = True):
"""初始化守护进程
Args:
@@ -50,16 +60,28 @@ class DataSyncDaemon:
load_from_db: 是否从数据库加载Cookie
days: 抓取最近多少天的数据
max_retries: 最大重试次数
enable_validation: 是否启用数据验证与短信告警
"""
self.script_dir = os.path.dirname(os.path.abspath(__file__))
self.use_proxy = use_proxy
self.load_from_db = load_from_db
self.days = days
self.max_retries = max_retries
self.enable_validation = enable_validation and VALIDATION_AVAILABLE
# 工作时间配置8:00-24:00
self.work_start_hour = 8
self.work_end_hour = 24
# 初始化日志
self.logger = setup_logger('data_sync_daemon', os.path.join(self.script_dir, 'logs', 'data_sync_daemon.log'))
# 创建验证报告目录
self.validation_reports_dir = os.path.join(self.script_dir, 'validation_reports')
if not os.path.exists(self.validation_reports_dir):
os.makedirs(self.validation_reports_dir)
self.logger.info(f"创建验证报告目录: {self.validation_reports_dir}")
# 统计信息
self.stats = {
'total_runs': 0,
@@ -76,13 +98,17 @@ class DataSyncDaemon:
print(f" 使用代理: {'' if use_proxy else ''}")
print(f" Cookie来源: {'数据库' if load_from_db else '本地文件'}")
print(f" 抓取天数: {days}")
print(f" 工作时间: {self.work_start_hour}:00 - {self.work_end_hour}:00")
print(f" 错误重试: 最大{max_retries}")
print(f" 定时执行: 每天午夜00:00")
print(f" 定时执行: 每隔1小时")
print(f" 数据验证: {'已启用' if self.enable_validation else '已禁用'}")
if self.enable_validation:
print(f" 短信告警: 验证失败时发送 (错误代码2222)")
print("="*70 + "\n")
self.logger.info("="*70)
self.logger.info("数据同步守护进程启动")
self.logger.info(f"使用代理: {use_proxy}, Cookie来源: {'数据库' if load_from_db else '本地文件'}, 抓取天数: {days}, 重试: {max_retries}")
self.logger.info(f"使用代理: {use_proxy}, Cookie来源: {'数据库' if load_from_db else '本地文件'}, 抓取天数: {days}, 工作时间: {self.work_start_hour}:00-{self.work_end_hour}:00, 重试: {max_retries}次, 验证: {'已启用' if self.enable_validation else '已禁用'}")
self.logger.info("="*70)
def fetch_data(self) -> bool:
@@ -294,6 +320,77 @@ class DataSyncDaemon:
self.logger.error(f"数据库导入失败: {e}", exc_info=True)
return False
def validate_data(self) -> bool:
"""步顷4数据验证与短信告警"""
if not self.enable_validation:
print("\n[跳过] 数据验证功能未启用")
self.logger.info("跳过数据验证(功能未启用)")
return True
print("\n" + "="*70)
print("【步顷4/4】数据验证与短信告警")
print("="*70)
try:
# 等待3秒确保数据库写入完成
print("\n等待3秒确保数据写入完成...")
self.logger.info("等待3秒以确保数据库写入完成")
time.sleep(3)
print("\n执行数据验证...")
self.logger.info("开始执行数据验证")
# 创建验证器(验证昨天的数据)
yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
validator = DataValidationWithSMS(date_str=yesterday)
# 执行验证JSON + CSV + Database
passed = validator.run_validation(
sources=['json', 'csv', 'database'],
table='ai_statistics'
)
# 生成验证报告
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
report_file = os.path.join(
self.validation_reports_dir,
f'validation_report_{timestamp}.txt'
)
validator.validator.generate_report(report_file)
if passed:
print("\n[✓] 数据验证通过")
self.logger.info("数据验证通过")
return True
else:
print("\n[X] 数据验证失败,准备发送短信告警")
self.logger.error("数据验证失败")
# 生成错误摘要
error_summary = validator.generate_error_summary()
self.logger.error(f"错误摘要: {error_summary}")
# 发送短信告警错误代码2222
sms_sent = validator.send_sms_alert("2222", error_summary)
if sms_sent:
print("[✓] 告警短信已发送")
self.logger.info("告警短信发送成功")
else:
print("[X] 告警短信发送失败")
self.logger.error("告警短信发送失败")
print(f"\n详细报告: {report_file}")
# 验证失败不阻止后续流程但返回True表示步骤完成
return True
except Exception as e:
print(f"\n[X] 数据验证异常: {e}")
self.logger.error(f"数据验证异常: {e}", exc_info=True)
# 验证异常不影响整体流程
return True
def sync_data(self):
"""执行完整的数据同步流程"""
start_time = datetime.now()
@@ -317,9 +414,14 @@ class DataSyncDaemon:
if not self.generate_csv():
raise Exception("CSV生成失败")
# 步3导入数据库
# 步3导入数据库
if not self.import_to_database():
raise Exception("数据库导入失败")
# 步顷4数据验证与短信告警
if not self.validate_data():
# 验证失败不阻止整体流程,只记录警告
self.logger.warning("数据验证步骤未成功完成")
# 成功
end_time = datetime.now()
@@ -370,12 +472,36 @@ class DataSyncDaemon:
self.logger.info(f"运行统计: 总{self.stats['total_runs']}次, 成功{self.stats['successful_runs']}次, 失败{self.stats['failed_runs']}")
def get_next_midnight(self) -> datetime:
"""获取下一个午夜时刻"""
def is_work_time(self) -> tuple:
"""
检查当前是否在工作时间内8:00-24:00
Returns:
tuple: (是否在工作时间内, 距离下次工作时间的秒数)
"""
now = datetime.now()
tomorrow = now + timedelta(days=1)
next_midnight = tomorrow.replace(hour=0, minute=0, second=0, microsecond=0)
return next_midnight
current_hour = now.hour
# 在工作时间内8:00-23:59
if self.work_start_hour <= current_hour < self.work_end_hour:
return True, 0
# 不在工作时间内,计算到下个工作时间的秒数
if current_hour < self.work_start_hour:
# 今天还没到工作时间
next_work_time = now.replace(hour=self.work_start_hour, minute=0, second=0, microsecond=0)
else:
# 今天已过工作时间,等待明天
next_work_time = (now + timedelta(days=1)).replace(hour=self.work_start_hour, minute=0, second=0, microsecond=0)
seconds_until_work = (next_work_time - now).total_seconds()
return False, seconds_until_work
def get_next_run_time(self) -> datetime:
"""获取下一次执行时间1小时后"""
now = datetime.now()
next_run = now + timedelta(hours=1)
return next_run
def run(self):
"""启动守护进程"""
@@ -383,22 +509,51 @@ class DataSyncDaemon:
print("守护进程已启动")
print("="*70)
# 设置定时任务:每天午夜00:00执行
schedule.every().day.at("00:00").do(self.sync_data)
# 设置定时任务:每隔1小时执行
schedule.every(1).hours.do(self.sync_data)
# 计算下次执行时间
next_run = self.get_next_midnight()
next_run = self.get_next_run_time()
time_until_next = (next_run - datetime.now()).total_seconds()
print(f"\n下次执行时间: {next_run.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"距离下次执行: {time_until_next/3600:.1f} 小时")
print(f"\n执行间隔: 每隔1小时")
print(f"工作时间: {self.work_start_hour}:00 - {self.work_end_hour}:00非工作时间自动休眠")
print(f"下次执行时间: {next_run.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"距离下次执行: {time_until_next/60:.1f} 分钟")
print("\n按 Ctrl+C 可以停止守护进程")
print("="*70 + "\n")
self.logger.info(f"守护进程已启动,下次执行时间: {next_run.strftime('%Y-%m-%d %H:%M:%S')}")
self.logger.info(f"守护进程已启动,执行间隔: 每隔1小时工作时间: {self.work_start_hour}:00-{self.work_end_hour}:00下次执行时间: {next_run.strftime('%Y-%m-%d %H:%M:%S')}")
try:
while True:
# 检查是否在工作时间内
is_work, seconds_until_work = self.is_work_time()
if not is_work:
# 不在工作时间内,等待至工作时间
next_work_time = datetime.now() + timedelta(seconds=seconds_until_work)
self.logger.info(f"当前非工作时间,等待至 {next_work_time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"\n[休眠] 当前不在工作时间内({self.work_start_hour}:00-{self.work_end_hour}:00")
print(f"[休眠] 下次工作时间: {next_work_time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"[休眠] 等待 {seconds_until_work/3600:.1f} 小时...")
# 每30分钟检查一次
check_interval = 1800
elapsed = 0
while elapsed < seconds_until_work:
sleep_time = min(check_interval, seconds_until_work - elapsed)
time.sleep(sleep_time)
elapsed += sleep_time
remaining = seconds_until_work - elapsed
if remaining > 0:
print(f" 距离工作时间还有: {remaining/3600:.1f} 小时 ({datetime.now().strftime('%H:%M:%S')})")
continue
# 在工作时间内,执行定时任务
schedule.run_pending()
time.sleep(60) # 每分钟检查一次
@@ -427,13 +582,15 @@ def main():
print(" USE_PROXY=true/false - 是否使用代理")
print(" DAYS=7 - 抓取天数")
print(" MAX_RETRIES=3 - 重试次数")
print(" RUN_NOW=true/false - 是否立即执行\n")
print(" RUN_NOW=true/false - 是否立即执行")
print(" ENABLE_VALIDATION=true/false - 是否启用验证\n")
load_from_db = os.getenv('LOAD_FROM_DB', 'true').lower() == 'true'
use_proxy = os.getenv('USE_PROXY', 'true').lower() == 'true'
days = int(os.getenv('DAYS', '7'))
max_retries = int(os.getenv('MAX_RETRIES', '3'))
run_now = os.getenv('RUN_NOW', 'true').lower() == 'true'
enable_validation = os.getenv('ENABLE_VALIDATION', 'true').lower() == 'true'
else:
# 交互模式:显示菜单
# 配置选项
@@ -468,9 +625,15 @@ def main():
except ValueError:
max_retries = 3
# 5. 是否立即执行一次
print("\n5. 是否立即执行一次同步")
print(" (否则等待到午夜00:00执行)")
# 5. 是否启用数据验证
print("\n5. 是否启用数据验证与短信告警")
print(" (每次同步后自动验证数据失败时发送短信2222)")
enable_validation_input = input(" (y/n, 默认y): ").strip().lower() or 'y'
enable_validation = (enable_validation_input == 'y')
# 6. 是否立即执行一次
print("\n6. 是否立即执行一次同步?")
print(" (否则等待到下一个整点小时执行)")
run_now_input = input(" (y/n, 默认n): ").strip().lower() or 'n'
run_now = (run_now_input == 'y')
@@ -480,9 +643,13 @@ def main():
print(f" Cookie来源: {'数据库' if load_from_db else '本地文件'}")
print(f" 使用代理: {'' if use_proxy else ''}")
print(f" 抓取天数: {days}")
print(f" 工作时间: 8:00 - 24:00非工作时间自动休眠")
print(f" 错误重试: 最大{max_retries}")
print(f" 数据验证: {'已启用' if enable_validation else '已禁用'}")
if enable_validation:
print(f" 短信告警: 验证失败时发送 (错误代码2222)")
print(f" 立即执行: {'' if run_now else ''}")
print(f" 定时执行: 每天午夜00:00")
print(f" 定时执行: 每隔1小时")
print("="*70)
confirm = input("\n确认启动守护进程?(y/n): ").strip().lower()
@@ -491,7 +658,13 @@ def main():
return
# 创建守护进程
daemon = DataSyncDaemon(use_proxy=use_proxy, load_from_db=load_from_db, days=days, max_retries=max_retries)
daemon = DataSyncDaemon(
use_proxy=use_proxy,
load_from_db=load_from_db,
days=days,
max_retries=max_retries,
enable_validation=enable_validation
)
# 如果选择立即执行,先执行一次
if run_now: