迁移目录
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
"""
|
||||
懂球帝世界杯专属采集
|
||||
|
||||
- 固定采集 2026 世界杯 season_id=26123 / competition_id=61
|
||||
- 赛程写入 la_match
|
||||
- 分组积分榜写入 la_worldcup_standing
|
||||
- 射手榜/助攻榜写入 la_worldcup_person_ranking
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse
|
||||
|
||||
import pymysql
|
||||
import requests
|
||||
|
||||
SEASON_ID = 26123
|
||||
COMPETITION_ID = 61
|
||||
LEAGUE_NAME = "世界杯"
|
||||
SPORT_TYPE = 1
|
||||
LEAGUE_TAB_ID = 26123
|
||||
LEAGUE_SORT = 285
|
||||
CRAWLER_USER_AGENT = "Mozilla/5.0 (compatible; SportEra/1.0)"
|
||||
BASE_PARAMS = {
|
||||
"app": "dqd",
|
||||
"version": "830",
|
||||
"platform": "miniprogram",
|
||||
"language": "zh-cn",
|
||||
"app_type": "",
|
||||
"from": "msite_com",
|
||||
}
|
||||
STANDING_URL = "https://sport-data.dongqiudi.com/soccer/biz/data/standing"
|
||||
SCHEDULE_URL = "https://sport-data.dongqiudi.com/soccer/biz/data/schedule"
|
||||
PERSON_RANKING_URL = "https://sport-data.dongqiudi.com/soccer/biz/data/person_ranking"
|
||||
ROUND_ORDER = [
|
||||
"小组赛 第1轮",
|
||||
"小组赛 第2轮",
|
||||
"小组赛 第3轮",
|
||||
"1/16决赛",
|
||||
"1/8决赛",
|
||||
"1/4决赛",
|
||||
"半决赛",
|
||||
"三四名决赛",
|
||||
"决赛",
|
||||
]
|
||||
BRACKET_ROUNDS = ROUND_ORDER[3:]
|
||||
|
||||
|
||||
def normalize_prefix(prefix: str) -> str:
|
||||
prefix = prefix or "la_"
|
||||
return prefix if re.match(r"^[A-Za-z0-9_]+$", prefix) else "la_"
|
||||
|
||||
|
||||
def console_print(message: str) -> None:
|
||||
try:
|
||||
print(message)
|
||||
except UnicodeEncodeError:
|
||||
print(message.encode("utf-8", "backslashreplace").decode("utf-8"))
|
||||
|
||||
|
||||
def get_headers() -> dict:
|
||||
return {
|
||||
"User-Agent": CRAWLER_USER_AGENT,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": "https://www.dongqiudi.com/",
|
||||
}
|
||||
|
||||
|
||||
def infer_stage(round_name: str) -> str:
|
||||
if round_name.startswith("小组赛"):
|
||||
return "小组赛"
|
||||
return "淘汰赛"
|
||||
|
||||
|
||||
def match_status_to_int(status: str) -> int:
|
||||
status = (status or "").lower()
|
||||
status_map = {
|
||||
"fixture": 0,
|
||||
"notstarted": 0,
|
||||
"played": 2,
|
||||
"finished": 2,
|
||||
"live": 1,
|
||||
"playing": 1,
|
||||
"postponed": 3,
|
||||
"cancelled": 3,
|
||||
}
|
||||
return status_map.get(status, 0)
|
||||
|
||||
|
||||
def round_weight(round_name: str) -> int:
|
||||
if round_name in ROUND_ORDER:
|
||||
return ROUND_ORDER.index(round_name)
|
||||
match = re.search(r"小组赛\s*第(\d+)轮", round_name)
|
||||
if match:
|
||||
return int(match.group(1)) - 1
|
||||
return 999
|
||||
|
||||
|
||||
def build_url(base_url: str, params: dict) -> str:
|
||||
parsed = urlparse(base_url)
|
||||
query = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
||||
query.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
return urlunparse(parsed._replace(query=urlencode(query)))
|
||||
|
||||
|
||||
def fetch_json(session: requests.Session, url: str, params: dict | None = None) -> dict:
|
||||
resp = session.get(url, params=params, timeout=30)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def fetch_standings(session: requests.Session) -> dict:
|
||||
return fetch_json(session, STANDING_URL, {"season_id": SEASON_ID, **BASE_PARAMS})
|
||||
|
||||
|
||||
def fetch_schedule_index(session: requests.Session) -> dict:
|
||||
return fetch_json(session, SCHEDULE_URL, {"season_id": SEASON_ID, **BASE_PARAMS})
|
||||
|
||||
|
||||
def fetch_round_schedule(session: requests.Session, round_url: str) -> dict:
|
||||
return fetch_json(session, build_url(round_url, BASE_PARAMS))
|
||||
|
||||
|
||||
def fetch_person_ranking(session: requests.Session, rank_type: str) -> dict:
|
||||
params = {"season_id": SEASON_ID, "type": rank_type, "app_type": "", **BASE_PARAMS}
|
||||
return fetch_json(session, PERSON_RANKING_URL, params)
|
||||
|
||||
|
||||
def parse_standings(payload: dict) -> tuple[str, list[dict]]:
|
||||
rounds = payload.get("content", {}).get("rounds", []) or []
|
||||
if not rounds:
|
||||
return "", []
|
||||
rows = []
|
||||
for round_item in rounds:
|
||||
content = round_item.get("content", {}) or {}
|
||||
stage_name = content.get("name", "") or "小组赛"
|
||||
for group in content.get("data", []) or []:
|
||||
group_name = group.get("name", "") or ""
|
||||
for row_order, team in enumerate(group.get("data", []) or [], start=1):
|
||||
rows.append({
|
||||
"season_id": SEASON_ID,
|
||||
"stage_name": stage_name,
|
||||
"group_name": group_name,
|
||||
"team_id": int(team.get("team_id") or 0),
|
||||
"team_name": str(team.get("team_name") or ""),
|
||||
"team_logo": str(team.get("team_logo") or ""),
|
||||
"rank": int(team.get("rank") or 0),
|
||||
"points": int(team.get("points") or 0),
|
||||
"played": int(team.get("matches_total") or 0),
|
||||
"won": int(team.get("matches_won") or 0),
|
||||
"drawn": int(team.get("matches_draw") or 0),
|
||||
"lost": int(team.get("matches_lost") or 0),
|
||||
"goals_for": int(team.get("goals_pro") or 0),
|
||||
"goals_against": int(team.get("goals_against") or 0),
|
||||
"goal_diff": int(team.get("goals_pro") or 0) - int(team.get("goals_against") or 0),
|
||||
"row_order": row_order,
|
||||
})
|
||||
return rows[0]["stage_name"] if rows else "", rows
|
||||
|
||||
|
||||
def parse_schedule_matches(schedule_index: dict, session: requests.Session) -> list[dict]:
|
||||
rounds = schedule_index.get("content", {}).get("rounds", []) or []
|
||||
matches = []
|
||||
seen_match_ids = set()
|
||||
for round_item in rounds:
|
||||
round_name = round_item.get("name", "") or ""
|
||||
round_url = round_item.get("url", "")
|
||||
if not round_name or not round_url:
|
||||
continue
|
||||
round_payload = fetch_round_schedule(session, round_url)
|
||||
round_matches = round_payload.get("content", {}).get("matches", []) or []
|
||||
for item in round_matches:
|
||||
match_id = int(item.get("match_id") or 0)
|
||||
if not match_id or match_id in seen_match_ids:
|
||||
continue
|
||||
seen_match_ids.add(match_id)
|
||||
start_play = str(item.get("start_play") or "")
|
||||
match_time = 0
|
||||
if start_play:
|
||||
try:
|
||||
match_time = int(datetime.strptime(start_play, "%Y-%m-%d %H:%M:%S").timestamp())
|
||||
except Exception:
|
||||
match_time = 0
|
||||
matches.append({
|
||||
"match_id": match_id,
|
||||
"competition_id": int(item.get("competition_id") or COMPETITION_ID),
|
||||
"home_team_id": str(item.get("team_A_id") or ""),
|
||||
"away_team_id": str(item.get("team_B_id") or ""),
|
||||
"league_name": LEAGUE_NAME,
|
||||
"round_name": round_name,
|
||||
"stage": infer_stage(round_name),
|
||||
"home_team": str(item.get("team_A_name") or ""),
|
||||
"home_icon": str(item.get("team_A_logo") or ""),
|
||||
"home_score": int(item.get("fs_A") or 0),
|
||||
"away_team": str(item.get("team_B_name") or ""),
|
||||
"away_icon": str(item.get("team_B_logo") or ""),
|
||||
"away_score": int(item.get("fs_B") or 0),
|
||||
"sport_type": SPORT_TYPE,
|
||||
"status": match_status_to_int(str(item.get("status") or "")),
|
||||
"match_time": match_time,
|
||||
"current_minute": str(item.get("minute") or ""),
|
||||
"half_score": "",
|
||||
"sort": round_weight(round_name),
|
||||
})
|
||||
matches.sort(key=lambda item: (round_weight(item["round_name"]), item["match_time"], item["match_id"]))
|
||||
return matches
|
||||
|
||||
|
||||
def parse_person_rankings(payload: dict, rank_type: str) -> tuple[list[str], list[dict]]:
|
||||
content = payload.get("content", {}) or {}
|
||||
header = content.get("header", []) or []
|
||||
rows = []
|
||||
for item in content.get("data", []) or []:
|
||||
rows.append({
|
||||
"season_id": SEASON_ID,
|
||||
"rank_type": rank_type,
|
||||
"person_id": int(item.get("person_id") or 0),
|
||||
"person_name": str(item.get("person_name") or ""),
|
||||
"person_logo": str(item.get("person_logo") or ""),
|
||||
"team_id": int(item.get("team_id") or 0),
|
||||
"team_name": str(item.get("team_name") or ""),
|
||||
"team_logo": str(item.get("team_logo") or ""),
|
||||
"rank": int(item.get("rank") or 0),
|
||||
"count": int(item.get("count") or 0),
|
||||
"extra_json": json.dumps(item, ensure_ascii=False),
|
||||
})
|
||||
return header, rows
|
||||
|
||||
|
||||
def ensure_tables(conn, prefix: str) -> None:
|
||||
standing_table = f"{prefix}worldcup_standing"
|
||||
ranking_table = f"{prefix}worldcup_person_ranking"
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS `{standing_table}` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`season_id` INT NOT NULL DEFAULT 0,
|
||||
`stage_name` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`group_name` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`team_id` BIGINT NOT NULL DEFAULT 0,
|
||||
`team_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`team_logo` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`rank` INT NOT NULL DEFAULT 0,
|
||||
`points` INT NOT NULL DEFAULT 0,
|
||||
`played` INT NOT NULL DEFAULT 0,
|
||||
`won` INT NOT NULL DEFAULT 0,
|
||||
`drawn` INT NOT NULL DEFAULT 0,
|
||||
`lost` INT NOT NULL DEFAULT 0,
|
||||
`goals_for` INT NOT NULL DEFAULT 0,
|
||||
`goals_against` INT NOT NULL DEFAULT 0,
|
||||
`goal_diff` INT NOT NULL DEFAULT 0,
|
||||
`row_order` INT NOT NULL DEFAULT 0,
|
||||
`create_time` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`update_time` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_worldcup_group_team` (`season_id`, `stage_name`, `group_name`, `team_id`),
|
||||
KEY `idx_worldcup_group` (`season_id`, `stage_name`, `group_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS `{ranking_table}` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`season_id` INT NOT NULL DEFAULT 0,
|
||||
`rank_type` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`person_id` BIGINT NOT NULL DEFAULT 0,
|
||||
`person_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`person_logo` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`team_id` BIGINT NOT NULL DEFAULT 0,
|
||||
`team_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`team_logo` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`rank` INT NOT NULL DEFAULT 0,
|
||||
`count` INT NOT NULL DEFAULT 0,
|
||||
`extra_json` JSON NULL,
|
||||
`create_time` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`update_time` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_worldcup_rank_person` (`season_id`, `rank_type`, `person_id`),
|
||||
KEY `idx_worldcup_rank_type` (`season_id`, `rank_type`, `rank`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def ensure_worldcup_league(conn, prefix: str) -> None:
|
||||
league_table = f"{prefix}league"
|
||||
now = int(time.time())
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"SELECT id FROM `{league_table}` WHERE label=%s OR league_id=%s OR tab_id=%s LIMIT 1",
|
||||
(LEAGUE_NAME, COMPETITION_ID, LEAGUE_TAB_ID),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
cur.execute(
|
||||
f"""
|
||||
UPDATE `{league_table}`
|
||||
SET label=%s, `type`='league', sport_type=%s, sort=%s, is_show=1, league_id=%s, update_time=%s
|
||||
WHERE id=%s
|
||||
""",
|
||||
(LEAGUE_NAME, SPORT_TYPE, LEAGUE_SORT, COMPETITION_ID, now, row["id"]),
|
||||
)
|
||||
return
|
||||
|
||||
cur.execute(
|
||||
f"""
|
||||
INSERT INTO `{league_table}`
|
||||
(tab_id, league_id, label, `type`, sport_type, api, icon, sort, is_show, extra_data, sessionid, create_time, update_time)
|
||||
VALUES (%s, %s, %s, 'league', %s, '', '', %s, 1, NULL, NULL, %s, %s)
|
||||
""",
|
||||
(LEAGUE_TAB_ID, COMPETITION_ID, LEAGUE_NAME, SPORT_TYPE, LEAGUE_SORT, now, now),
|
||||
)
|
||||
|
||||
|
||||
def sync_matches(conn, prefix: str, matches: list[dict]) -> tuple[int, int]:
|
||||
table = f"{prefix}match"
|
||||
now = int(time.time())
|
||||
inserted = 0
|
||||
updated = 0
|
||||
with conn.cursor() as cur:
|
||||
for item in matches:
|
||||
cur.execute(
|
||||
f"""
|
||||
INSERT INTO `{table}`
|
||||
(match_id, competition_id, home_team_id, away_team_id,
|
||||
league_name, round_name, stage, league_icon,
|
||||
home_team, home_icon, home_score,
|
||||
away_team, away_icon, away_score,
|
||||
sport_type, status, match_time,
|
||||
current_minute, half_score,
|
||||
is_hot, is_show, sort, create_time, update_time)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,'',%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,1,%s,%s,%s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
competition_id=VALUES(competition_id),
|
||||
league_name=VALUES(league_name),
|
||||
round_name=VALUES(round_name),
|
||||
stage=VALUES(stage),
|
||||
home_team=VALUES(home_team),
|
||||
home_icon=VALUES(home_icon),
|
||||
home_score=VALUES(home_score),
|
||||
away_team=VALUES(away_team),
|
||||
away_icon=VALUES(away_icon),
|
||||
away_score=VALUES(away_score),
|
||||
sport_type=VALUES(sport_type),
|
||||
status=VALUES(status),
|
||||
match_time=VALUES(match_time),
|
||||
current_minute=VALUES(current_minute),
|
||||
half_score=VALUES(half_score),
|
||||
sort=VALUES(sort),
|
||||
update_time=VALUES(update_time)
|
||||
""",
|
||||
(
|
||||
item["match_id"],
|
||||
item["competition_id"],
|
||||
item["home_team_id"],
|
||||
item["away_team_id"],
|
||||
item["league_name"],
|
||||
item["round_name"][:50],
|
||||
item["stage"][:50],
|
||||
item["home_team"][:100],
|
||||
item["home_icon"][:255],
|
||||
item["home_score"],
|
||||
item["away_team"][:100],
|
||||
item["away_icon"][:255],
|
||||
item["away_score"],
|
||||
item["sport_type"],
|
||||
item["status"],
|
||||
item["match_time"],
|
||||
item["current_minute"][:20],
|
||||
item["half_score"][:50],
|
||||
item["sort"],
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
if cur.rowcount == 1:
|
||||
inserted += 1
|
||||
elif cur.rowcount == 2:
|
||||
updated += 1
|
||||
return inserted, updated
|
||||
|
||||
|
||||
def sync_standings(conn, prefix: str, rows: list[dict]) -> int:
|
||||
table = f"{prefix}worldcup_standing"
|
||||
now = int(time.time())
|
||||
count = 0
|
||||
with conn.cursor() as cur:
|
||||
for item in rows:
|
||||
cur.execute(
|
||||
f"""
|
||||
INSERT INTO `{table}`
|
||||
(season_id, stage_name, group_name, team_id, team_name, team_logo, `rank`,
|
||||
points, played, won, drawn, lost, goals_for, goals_against, goal_diff,
|
||||
row_order, create_time, update_time)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
team_name=VALUES(team_name),
|
||||
team_logo=VALUES(team_logo),
|
||||
`rank`=VALUES(`rank`),
|
||||
points=VALUES(points),
|
||||
played=VALUES(played),
|
||||
won=VALUES(won),
|
||||
drawn=VALUES(drawn),
|
||||
lost=VALUES(lost),
|
||||
goals_for=VALUES(goals_for),
|
||||
goals_against=VALUES(goals_against),
|
||||
goal_diff=VALUES(goal_diff),
|
||||
row_order=VALUES(row_order),
|
||||
update_time=VALUES(update_time)
|
||||
""",
|
||||
(
|
||||
item["season_id"], item["stage_name"][:50], item["group_name"][:50], item["team_id"],
|
||||
item["team_name"][:100], item["team_logo"][:500], item["rank"], item["points"],
|
||||
item["played"], item["won"], item["drawn"], item["lost"], item["goals_for"],
|
||||
item["goals_against"], item["goal_diff"], item["row_order"], now, now,
|
||||
),
|
||||
)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def sync_rankings(conn, prefix: str, rows: list[dict], rank_type: str) -> int:
|
||||
table = f"{prefix}worldcup_person_ranking"
|
||||
now = int(time.time())
|
||||
count = 0
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"DELETE FROM `{table}` WHERE season_id=%s AND rank_type=%s",
|
||||
(SEASON_ID, rank_type),
|
||||
)
|
||||
for item in rows:
|
||||
cur.execute(
|
||||
f"""
|
||||
INSERT INTO `{table}`
|
||||
(season_id, rank_type, person_id, person_name, person_logo, team_id, team_name, team_logo,
|
||||
`rank`, `count`, extra_json, create_time, update_time)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
""",
|
||||
(
|
||||
item["season_id"], item["rank_type"], item["person_id"], item["person_name"][:100],
|
||||
item["person_logo"][:500], item["team_id"], item["team_name"][:100], item["team_logo"][:500],
|
||||
item["rank"], item["count"], item["extra_json"], now, now,
|
||||
),
|
||||
)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def run(db_config: dict):
|
||||
db_config = dict(db_config)
|
||||
prefix = normalize_prefix(db_config.pop("prefix", "la_"))
|
||||
|
||||
session = requests.Session()
|
||||
session.headers.update(get_headers())
|
||||
|
||||
standings_payload = fetch_standings(session)
|
||||
schedule_index = fetch_schedule_index(session)
|
||||
goals_payload = fetch_person_ranking(session, "goals")
|
||||
assists_payload = fetch_person_ranking(session, "assists")
|
||||
|
||||
stage_name, standings = parse_standings(standings_payload)
|
||||
matches = parse_schedule_matches(schedule_index, session)
|
||||
goal_header, goal_rows = parse_person_rankings(goals_payload, "goals")
|
||||
assist_header, assist_rows = parse_person_rankings(assists_payload, "assists")
|
||||
|
||||
conn = pymysql.connect(**db_config, cursorclass=pymysql.cursors.DictCursor)
|
||||
try:
|
||||
ensure_tables(conn, prefix)
|
||||
ensure_worldcup_league(conn, prefix)
|
||||
inserted, updated = sync_matches(conn, prefix, matches)
|
||||
standings_count = sync_standings(conn, prefix, standings)
|
||||
goals_count = sync_rankings(conn, prefix, goal_rows, "goals")
|
||||
assists_count = sync_rankings(conn, prefix, assist_rows, "assists")
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
console_print(f"世界杯阶段: {stage_name or '小组赛'}")
|
||||
console_print(f"赛程轮次: {len(schedule_index.get('content', {}).get('rounds', []) or [])}, 比赛 {len(matches)} 场")
|
||||
console_print(f"la_match: 新增 {inserted}, 更新 {updated}")
|
||||
console_print(f"积分榜: {standings_count} 条")
|
||||
console_print(f"射手榜: {goals_count} 条, 表头: {' / '.join(goal_header or [])}")
|
||||
console_print(f"助攻榜: {assists_count} 条, 表头: {' / '.join(assist_header or [])}")
|
||||
return {
|
||||
"success": True,
|
||||
"candidate_count": len(matches) + len(standings) + len(goal_rows) + len(assist_rows),
|
||||
"saved_count": inserted + updated + standings_count + goals_count + assists_count,
|
||||
"summary_text": f"世界杯赛程 {len(matches)} 场,la_match 新增 {inserted} 更新 {updated},积分榜 {standings_count},射手榜 {goals_count},助攻榜 {assists_count}",
|
||||
}
|
||||
Reference in New Issue
Block a user