feat: 完善代理重试机制,添加数据验证告警,新增README文档
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user