""" 懂球帝世界杯专属采集 - 固定采集 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}", }