Files
lotteryServer/core/task/dlt_spider.py
2026-09-07 13:24:09 +08:00

365 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from dataclasses import dataclass
from datetime import datetime
from typing import Any, List, Dict, Union, Optional, NamedTuple
import time
import requests
import structlog
from sqlalchemy import desc
from core.db import get_db_session
from models import LotteryDLT
logger = structlog.getLogger(__name__)
def parse_draw_numbers(result_str: str) -> List[str]:
"""解析开奖号码,支持空格或逗号分隔"""
if not result_str or not isinstance(result_str, str):
return []
# 先尝试用空格分割
if ' ' in result_str:
numbers = result_str.strip().split()
elif ',' in result_str:
numbers = [num.strip() for num in result_str.split(',')]
else:
# 如果没有分隔符尝试按每2个字符分割如 "0208172933"
numbers = [result_str[i:i + 2] for i in range(0, len(result_str), 2)]
# 过滤掉空字符串并确保是2位数字
numbers = [num.zfill(2) for num in numbers if num]
return numbers
def get_safe_idx(arr: List[str], idx: int, default: str = "0") -> str:
"""安全获取列表元素"""
try:
if arr and idx < len(arr):
return arr[idx].zfill(2)
return default.zfill(2)
except (IndexError, TypeError):
return default.zfill(2)
def normalize_unsort_result(un_result: Any, sort_result_str: str, issue: str) -> str:
"""
标准化 lotteryUnsortDrawresult 字段
Args:
un_result: 原始 unsort 值
sort_result_str: 排序后的结果
issue: 期号(用于日志)
Returns:
标准化后的字符串
"""
# 处理各种情况
if isinstance(un_result, str):
if un_result == "current" or un_result == "":
logger.debug(f"期号 {issue}: unsort 为 '{un_result}',使用 lotteryDrawResult")
return sort_result_str
return un_result
if isinstance(un_result, dict):
# 字典类型(包括空字典)
logger.debug(f"期号 {issue}: lotteryUnsortDrawresult 是字典,使用 lotteryDrawResult")
return sort_result_str
if un_result is None:
return sort_result_str
# 其他类型,尝试转换为字符串
try:
return str(un_result)
except:
logger.warning(f"期号 {issue}: 无法转换 unsort 结果,使用 lotteryDrawResult")
return sort_result_str
def result_handle(data: dict) -> Optional[LotteryDLT]:
"""将爬取的数据格式化为当前服务的数据格式"""
try:
# 获取基本字段
issue = data.get('lotteryDrawNum')
if not issue:
logger.warning("跳过无效数据: 缺少 lotteryDrawNum")
return None
# 获取排序结果(必须存在)
sort_result_str = data.get('lotteryDrawResult', '')
if not sort_result_str or not isinstance(sort_result_str, str):
logger.warning(f"跳过期号 {issue}: lotteryDrawResult 无效")
return None
# 标准化 unsort 结果
un_result = normalize_unsort_result(
data.get('lotteryUnsortDrawresult'),
sort_result_str,
str(issue)
)
# 解析号码
un_sort_result = parse_draw_numbers(un_result)
sort_result = parse_draw_numbers(sort_result_str)
# 验证解析结果
if len(sort_result) < 7:
logger.warning(f"期号 {issue}: 排序号码解析失败,原始数据: {sort_result_str}")
return None
if len(un_sort_result) < 7:
logger.debug(f"期号 {issue}: 未排序号码解析失败,使用排序结果")
un_sort_result = sort_result
# 解析时间
dt_str = data.get('lotteryDrawTime')
if not dt_str or not isinstance(dt_str, str):
logger.warning(f"期号 {issue}: 无效的时间格式")
return None
try:
dt = datetime.fromisoformat(dt_str.replace(' ', 'T'))
except ValueError:
# 尝试其他格式
dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")
record = LotteryDLT(
issue=str(issue),
game_name=str(data.get('lotteryGameName', '超级大乐透')),
draw_date=dt.date(),
draw_time=dt,
front_sort_1=get_safe_idx(sort_result, 0),
front_sort_2=get_safe_idx(sort_result, 1),
front_sort_3=get_safe_idx(sort_result, 2),
front_sort_4=get_safe_idx(sort_result, 3),
front_sort_5=get_safe_idx(sort_result, 4),
back_sort_1=get_safe_idx(sort_result, 5),
back_sort_2=get_safe_idx(sort_result, 6),
front_1=get_safe_idx(un_sort_result, 0),
front_2=get_safe_idx(un_sort_result, 1),
front_3=get_safe_idx(un_sort_result, 2),
front_4=get_safe_idx(un_sort_result, 3),
front_5=get_safe_idx(un_sort_result, 4),
back_1=get_safe_idx(un_sort_result, 5),
back_2=get_safe_idx(un_sort_result, 6)
)
return record
except Exception as e:
logger.error(f"处理期号 {data.get('lotteryDrawNum', '未知')} 失败: {e}")
return None
@dataclass
class ResultCount:
"""成功失败数据类"""
success_count: int
fail_count: int
class DLTSpider:
"""大乐透爬虫"""
def __init__(self):
self.headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
"Referer": "https://www.sporttery.cn/"
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def _fetch_data(self, url: str) -> Optional[dict]:
"""通用的数据获取方法"""
try:
response = self.session.get(url, timeout=15)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"请求失败: {url}, 错误: {e}")
return None
def _save_record(self, db, record: LotteryDLT) -> bool:
"""保存单条记录到数据库"""
try:
existing = db.query(LotteryDLT).filter(
LotteryDLT.issue == record.issue
).first()
if existing:
logger.debug(f"期号 {record.issue} 已存在,跳过")
return False
db.add(record)
return True
except Exception as e:
logger.error(f"保存期号 {record.issue} 失败: {e}")
return False
def fetch_latest(self):
"""获取最新一期大乐透数据"""
url = "https://webapi.sporttery.cn/gateway/lottery/getDigitalDrawInfoV1.qry?param=85,0&isVerify=1"
data = self._fetch_data(url)
if not data:
logger.error("获取最新数据失败")
return
dlt_data = dict(data.get("value", {}).get("dlt", {}))
if not dlt_data:
logger.error("数据格式错误")
return
record = result_handle(dlt_data)
if not record:
logger.error("数据转换失败")
return
with get_db_session() as db:
if self._save_record(db, record):
db.commit()
logger.info(f"成功保存最新数据: 期号 {record.issue}")
else:
logger.info(f"期号 {record.issue} 已存在")
def fetch_by_year(self, year: int) -> tuple[List[LotteryDLT], ResultCount]:
"""获取指定年份的所有数据"""
logger.info(f"开始爬取 {year} 年的数据")
year_prefix = str(year)[-2:]
all_records = []
success_count = 0
fail_count = 0
# 分两段获取001-100 和 101-200
segments = [
(f"{year_prefix}001", f"{year_prefix}100"),
(f"{year_prefix}101", f"{year_prefix}200")
]
for start_term, end_term in segments:
url = (
f"https://webapi.sporttery.cn/gateway/lottery/getHistoryPageListV1.qry"
f"?gameNo=85&provinceId=0&isVerify=1"
f"&pageNo=1&pageSize=100"
f"&startTerm={start_term}&endTerm={end_term}"
)
data = self._fetch_data(url)
if not data:
logger.warning(f"获取 {start_term}-{end_term} 数据失败")
continue
dlt_list = data.get("value", {}).get("list", [])
if not dlt_list:
logger.warning(f"{start_term}-{end_term} 没有数据")
continue
for item_data in dlt_list:
record = result_handle(item_data)
if record:
all_records.append(record)
success_count += 1
else:
fail_count += 1
# 避免请求过快
time.sleep(0.5)
# 按期号排序
all_records.sort(key=lambda x: int(x.issue))
logger.info(f"{year} 年成功获取 {success_count} 条数据, 失败 {fail_count} 条数据")
return all_records, ResultCount(success_count, fail_count)
def fetch_range(self, start_year: int = 2007, end_year: Optional[int] = None):
"""
获取年限范围内历史所有的大乐透数据
Args:
start_year: 开始年份大乐透从2007年开始
end_year: 结束年份,默认为当前年份
"""
if end_year is None:
end_year = datetime.now().year + 1
logger.info(f"开始爬取 {start_year}{end_year} 年的数据")
total_count = 0
success_count = 0
fail_count = 0
for year in range(start_year, end_year):
records, state = self.fetch_by_year(year)
success_count += state.success_count
fail_count += state.fail_count
total_count += (success_count + fail_count)
# 批量保存到数据库
if records:
with get_db_session() as db:
saved_count = 0
for record in records:
if self._save_record(db, record):
saved_count += 1
if saved_count > 0:
logger.info(f"{year} 年成功保存 {saved_count} 条新数据")
else:
db.rollback()
# 避免请求过快
time.sleep(1)
logger.info(f"爬取完成!总共 {total_count} 条数据, 成功: {success_count}, 失败: {fail_count}")
def fetch_missing(self, target_year: int = 2026):
"""
补全指定年份缺失的数据
Args:
target_year: 要补全的年份
"""
logger.info(f"开始补全 {target_year} 年的数据")
# 获取数据库中已存在的期号
with get_db_session() as db:
existing_issues = [
r[0] for r in db.query(LotteryDLT.issue)
.filter(LotteryDLT.issue.startswith(str(target_year)[-2:]))
.all()
]
# 获取该年份所有数据
records, state = self.fetch_by_year(target_year)
# 过滤出缺失的
missing_records = [r for r in records if r.issue not in existing_issues]
success_count = 0
if missing_records:
logger.info(f"发现 {len(missing_records)} 条缺失数据")
with get_db_session() as db:
for record in missing_records:
if self._save_record(db, record):
success_count += 1
db.commit()
logger.info(f"成功补全 {success_count} 条数据")
else:
logger.info(f"{target_year} 年数据完整,无需补全")
def get_latest_issue(self) -> Optional[str]:
"""获取数据库中最新的期号"""
with get_db_session() as db:
record = db.query(LotteryDLT).order_by(
desc(LotteryDLT.issue)
).first()
return record.issue if record else None
if __name__ == "__main__":
spider = DLTSpider()
# 爬取所有历史数据从2007年开始
# spider.fetch_range(start_year=2007)
# 或者只补全2026年的数据
spider.fetch_missing(2017)
# 或者只获取最新一期
# spider.fetch_latest()