Files
sbnews/server/public/dongqiudi-crawler/dqd_worldcup.py
T
2026-06-11 12:15:29 +08:00

493 lines
20 KiB
Python

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