添加赔率

This commit is contained in:
hajimi
2026-07-11 11:19:43 +08:00
parent c9ac46faa7
commit 3a5d571e53
29 changed files with 3006 additions and 542 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ from pathlib import Path
env_file = Path("/etc/sport-era-crawler/env.sh")
with env_file.open("w", encoding="utf-8") as fp:
for key, value in sorted(os.environ.items()):
if key.startswith(("DQD_", "ODDS_", "HG_ODDS_")) or key in {"TZ", "PYTHONPATH", "PYTHONUNBUFFERED"}:
if key.startswith(("DQD_", "ODDS_", "HG_ODDS_", "TITAN_ODDS_")) or key in {"TZ", "PYTHONPATH", "PYTHONUNBUFFERED"}:
fp.write(f"export {key}={shlex.quote(value)}\n")
env_file.chmod(0o600)
PY
+134 -52
View File
@@ -15,6 +15,14 @@ import pymysql
import requests
import urllib3
from titan_odds import (
DEFAULT_TITAN_COMPANY_ID,
DEFAULT_TITAN_COMPANY_NAME,
DEFAULT_TITAN_CUP_URLS,
DEFAULT_TITAN_EURO_COMPANY_ID,
TitanOddsClient,
)
DEFAULT_BASE_URLS = [
"https://205.201.0.120/",
@@ -70,6 +78,13 @@ def _safe_int(value: Any, default: int = 0) -> int:
return default
def _safe_float(value: Any, default: float = 0.0) -> float:
try:
return float(str(value).strip())
except (TypeError, ValueError):
return default
def _text(raw: Dict[str, str], key: str) -> str:
return str(raw.get(key) or "").strip()
@@ -538,62 +553,129 @@ def _build_result(
def run(db_config: Dict[str, Any], env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
env = env if env is not None else os.environ
username = (env.get("ODDS_USERNAME") or env.get("HG_ODDS_USERNAME") or "").strip()
password = (env.get("ODDS_PASSWORD") or env.get("HG_ODDS_PASSWORD") or "").strip()
if not username or not password:
return _build_result(
success=False,
candidate_count=0,
saved_count=0,
errors=["ODDS_USERNAME/ODDS_PASSWORD 未配置"],
)
base_urls = [_normalize_base_url(url) for url in _split_env_list(env.get("ODDS_BASE_URLS", ""), DEFAULT_BASE_URLS)]
sport_types = [item.upper() for item in _split_env_list(env.get("ODDS_SPORT_TYPES", ""), DEFAULT_SPORT_TYPES)]
show_types = [item.lower() for item in _split_env_list(env.get("ODDS_SHOW_TYPES", ""), DEFAULT_SHOW_TYPES)]
timeout = _safe_int(env.get("ODDS_TIMEOUT"), 20)
verify_tls = _bool_env(env.get("ODDS_VERIFY_TLS"), default=False)
langx = (env.get("ODDS_LANGX") or "zh-cn").strip()
odds_type = (env.get("ODDS_TYPE") or "H").strip().upper()
request_delay = float(env.get("ODDS_REQUEST_DELAY_SECONDS") or REQUEST_DELAY_SECONDS)
client: Optional[HgOddsClient] = None
login_errors: List[str] = []
for base_url in base_urls:
try:
candidate = HgOddsClient(
base_url=base_url,
username=username,
password=password,
timeout=timeout,
verify_tls=verify_tls,
langx=langx,
odds_type=odds_type,
)
candidate.login()
client = candidate
break
except Exception as exc: # noqa: BLE001
login_errors.append(f"{base_url}: {exc}")
if client is None:
return _build_result(success=False, candidate_count=0, saved_count=0, errors=login_errors)
items: List[Dict[str, Any]] = []
fetch_errors: List[str] = []
source_urls: List[str] = []
successful_sources = 0
skipped_count = 0
for sport_type in sport_types:
for show_type in show_types:
username = (env.get("ODDS_USERNAME") or env.get("HG_ODDS_USERNAME") or "").strip()
password = (env.get("ODDS_PASSWORD") or env.get("HG_ODDS_PASSWORD") or "").strip()
if username and password:
base_urls = [
_normalize_base_url(url)
for url in _split_env_list(env.get("ODDS_BASE_URLS", ""), DEFAULT_BASE_URLS)
]
sport_types = [
item.upper()
for item in _split_env_list(env.get("ODDS_SPORT_TYPES", ""), DEFAULT_SPORT_TYPES)
]
show_types = [
item.lower()
for item in _split_env_list(env.get("ODDS_SHOW_TYPES", ""), DEFAULT_SHOW_TYPES)
]
timeout = _safe_int(env.get("ODDS_TIMEOUT"), 20)
verify_tls = _bool_env(env.get("ODDS_VERIFY_TLS"), default=False)
langx = (env.get("ODDS_LANGX") or "zh-cn").strip()
odds_type = (env.get("ODDS_TYPE") or "H").strip().upper()
request_delay = _safe_float(
env.get("ODDS_REQUEST_DELAY_SECONDS"),
REQUEST_DELAY_SECONDS,
)
client: Optional[HgOddsClient] = None
login_errors: List[str] = []
for base_url in base_urls:
try:
items.extend(client.fetch_game_list(sport_type, show_type))
except OddsGameListUnavailable:
skipped_count += 1
candidate = HgOddsClient(
base_url=base_url,
username=username,
password=password,
timeout=timeout,
verify_tls=verify_tls,
langx=langx,
odds_type=odds_type,
)
candidate.login()
client = candidate
break
except Exception as exc: # noqa: BLE001
fetch_errors.append(f"{sport_type}/{show_type}: {exc}")
if request_delay > 0:
time.sleep(request_delay)
login_errors.append(f"hg {base_url}: {exc}")
if client is None:
fetch_errors.extend(login_errors)
else:
successful_sources += 1
source_urls.append(client.base_url)
for sport_type in sport_types:
for show_type in show_types:
try:
items.extend(client.fetch_game_list(sport_type, show_type))
except OddsGameListUnavailable:
skipped_count += 1
except Exception as exc: # noqa: BLE001
fetch_errors.append(f"hg {sport_type}/{show_type}: {exc}")
if request_delay > 0:
time.sleep(request_delay)
titan_enabled = _bool_env(env.get("TITAN_ODDS_ENABLED"), default=True)
if titan_enabled:
titan_urls = [
str(url).strip()
for url in _split_env_list(
env.get("TITAN_ODDS_URLS", ""),
DEFAULT_TITAN_CUP_URLS,
)
]
titan_client = TitanOddsClient(
timeout=_safe_int(env.get("TITAN_ODDS_TIMEOUT"), 20),
verify_tls=_bool_env(env.get("TITAN_ODDS_VERIFY_TLS"), default=True),
company_id=_safe_int(
env.get("TITAN_ODDS_COMPANY_ID"),
DEFAULT_TITAN_COMPANY_ID,
),
euro_company_id=_safe_int(
env.get("TITAN_ODDS_EURO_COMPANY_ID"),
DEFAULT_TITAN_EURO_COMPANY_ID,
),
company_name=(
env.get("TITAN_ODDS_COMPANY_NAME")
or DEFAULT_TITAN_COMPANY_NAME
).strip(),
show_type=(env.get("TITAN_ODDS_SHOW_TYPE") or "early").strip().lower(),
request_delay=_safe_float(
env.get("TITAN_ODDS_REQUEST_DELAY_SECONDS"),
0.2,
),
)
for titan_url in titan_urls:
try:
items.extend(titan_client.fetch_cup(titan_url))
successful_sources += 1
source_urls.append(titan_url)
except Exception as exc: # noqa: BLE001
fetch_errors.append(f"titan007 {titan_url}: {exc}")
titan_request_delay = _safe_float(
env.get("TITAN_ODDS_REQUEST_DELAY_SECONDS"),
0.2,
)
if titan_request_delay > 0:
time.sleep(titan_request_delay)
if successful_sources == 0:
if not username or not password:
fetch_errors.append("ODDS_USERNAME/ODDS_PASSWORD 未配置")
return _build_result(
success=False,
candidate_count=len(items),
saved_count=0,
source_url=",".join(dict.fromkeys(source_urls)),
skipped_count=skipped_count,
errors=fetch_errors or ["没有可用的赔率来源"],
)
repo = None
source_url = ",".join(dict.fromkeys(source_urls))
try:
repo = MatchOddsRepository(db_config)
repo.ensure_table()
@@ -603,7 +685,7 @@ def run(db_config: Dict[str, Any], env: Optional[Dict[str, str]] = None) -> Dict
success=False,
candidate_count=len(items),
saved_count=0,
source_url=client.base_url,
source_url=source_url,
skipped_count=skipped_count,
errors=fetch_errors + [f"database: {exc}"],
)
@@ -612,10 +694,10 @@ def run(db_config: Dict[str, Any], env: Optional[Dict[str, str]] = None) -> Dict
repo.close()
return _build_result(
success=len(fetch_errors) == 0,
success=True,
candidate_count=len(items),
saved_count=saved_count,
source_url=client.base_url,
source_url=source_url,
skipped_count=skipped_count,
errors=fetch_errors,
)
+3 -1
View File
@@ -146,7 +146,7 @@ class MatchOddsTest(unittest.TestCase):
self.assertEqual(json.loads(params[-2])["moneyline"]["draw"], "3.90")
def test_run_skips_without_credentials(self):
result = run({"prefix": "la_"}, env={})
result = run({"prefix": "la_"}, env={"TITAN_ODDS_ENABLED": "0"})
self.assertFalse(result["success"])
self.assertEqual(result["candidate_count"], 0)
@@ -182,6 +182,7 @@ class MatchOddsTest(unittest.TestCase):
"ODDS_SPORT_TYPES": "FT",
"ODDS_SHOW_TYPES": "today",
"ODDS_REQUEST_DELAY_SECONDS": "0",
"TITAN_ODDS_ENABLED": "0",
},
)
@@ -234,6 +235,7 @@ class MatchOddsTest(unittest.TestCase):
"ODDS_SPORT_TYPES": "FT",
"ODDS_SHOW_TYPES": "today,early",
"ODDS_REQUEST_DELAY_SECONDS": "0",
"TITAN_ODDS_ENABLED": "0",
},
)
+81
View File
@@ -0,0 +1,81 @@
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from titan_odds import (
build_titan_odds_items,
parse_titan_odds_script,
parse_titan_schedule_script,
)
SCHEDULE_SCRIPT = r"""
var arrCup = [75,'世界杯','世界盃','World Cup','世界杯','世界盃','World Cup','2026'];
jh["G27976"] = [[2907402,75,0,'2026-07-12 05:00',640,744,'','','19','4',-0.5,-0.25,'2.5','1']];
var arrTeam = [
[640,'挪威','挪威','Norway','','images/norway.png'],
[744,'英格兰','英格蘭','England','','images/england.png']
];
var lastUpdateTime = '2026-07-11 10:00:12';
"""
ODDS_SCRIPT = """
oddsData["O_2907402"]=[[545,3.5,3.6,2.01],[16,3.3,3.55,2.13]];
oddsData["L_2907402"]=[[3,0.88,-0.5,0.94],[16,0.86,-0.5,0.98]];
oddsData["T_2907402"]=[[3,0.92,2.5,0.88],[16,0.93,2.5,0.87]];
"""
class TitanOddsTest(unittest.TestCase):
def test_parse_schedule_and_odds_builds_normalized_item(self):
schedule = parse_titan_schedule_script(SCHEDULE_SCRIPT)
odds = parse_titan_odds_script(ODDS_SCRIPT)
items = build_titan_odds_items(
schedule,
odds,
source_url="https://zq.titan007.com/cn/CupMatch/75.html",
company_id=3,
euro_company_id=545,
company_name="Crown",
)
self.assertEqual(len(items), 1)
item = items[0]
self.assertEqual(item["source_site"], "titan007")
self.assertEqual(item["source_match_id"], "2907402")
self.assertEqual(item["league_name"], "世界杯")
self.assertEqual(item["home_team"], "挪威")
self.assertEqual(item["away_team"], "英格兰")
self.assertEqual(item["show_type"], "early")
self.assertEqual(item["strong_side"], "C")
self.assertEqual(item["handicap_line"], "0.5")
self.assertEqual(item["handicap_home_odds"], "0.88")
self.assertEqual(item["handicap_away_odds"], "0.94")
self.assertEqual(item["total_line"], "2.5")
self.assertEqual(item["home_win_odds"], "3.5")
self.assertEqual(item["draw_odds"], "3.6")
self.assertEqual(item["away_win_odds"], "2.01")
self.assertEqual(item["raw_data"]["selected_company_id"], 3)
self.assertEqual(item["raw_data"]["selected_euro_company_id"], 545)
def test_sparse_schedule_rows_are_parsed_without_eval(self):
script = r"""
var arrCup = [75,'世界杯','','','','','','2026'];
jh["G27977"] = [[2907405,75,0,'2026-07-16 03:00',77068,77069,'','','','',,,'','']];
var arrTeam = [[77068,'99胜者'],[77069,'100胜者']];
"""
parsed = parse_titan_schedule_script(script)
self.assertIn("2907405", parsed["matches"])
self.assertEqual(parsed["matches"]["2907405"]["home_team"], "99胜者")
if __name__ == "__main__":
unittest.main()
+448
View File
@@ -0,0 +1,448 @@
#!/usr/bin/env python3
from __future__ import annotations
import ast
import html
import json
import re
import time
from decimal import Decimal, InvalidOperation
from typing import Any, Dict, Iterable, List, Optional, Tuple
from urllib.parse import urljoin
import requests
DEFAULT_TITAN_CUP_URLS = [
"https://zq.titan007.com/cn/CupMatch/75.html",
]
DEFAULT_TITAN_COMPANY_ID = 3
DEFAULT_TITAN_EURO_COMPANY_ID = 545
DEFAULT_TITAN_COMPANY_NAME = "Crown"
DEFAULT_USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/137.0.0.0 Safari/537.36"
)
def _limit(value: Any, size: int) -> str:
return re.sub(r"\s+", " ", str(value or "")).strip()[:size]
def _safe_int(value: Any, default: int = 0) -> int:
try:
return int(str(value).strip())
except (TypeError, ValueError):
return default
def _number_text(value: Any, *, absolute: bool = False) -> str:
if value is None or value == "":
return ""
try:
number = Decimal(str(value))
if absolute:
number = abs(number)
text = format(number.normalize(), "f")
return "0" if text in {"-0", ""} else text
except (InvalidOperation, ValueError):
return _limit(value, 32)
def _extract_array_expression(script: str, start_at: int) -> Tuple[str, int]:
start = script.find("[", start_at)
if start < 0:
raise ValueError("JavaScript array start not found")
depth = 0
quote = ""
escaped = False
for index in range(start, len(script)):
char = script[index]
if quote:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == quote:
quote = ""
continue
if char in {"'", '"'}:
quote = char
elif char == "[":
depth += 1
elif char == "]":
depth -= 1
if depth == 0:
return script[start : index + 1], index + 1
raise ValueError("JavaScript array end not found")
def _fill_sparse_array_holes(expression: str) -> str:
output: List[str] = []
quote = ""
escaped = False
length = len(expression)
for index, char in enumerate(expression):
output.append(char)
if quote:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == quote:
quote = ""
continue
if char in {"'", '"'}:
quote = char
continue
if char not in {"[", ","}:
continue
next_index = index + 1
while next_index < length and expression[next_index].isspace():
next_index += 1
if next_index < length and expression[next_index] == ",":
output.append("None")
return "".join(output)
def _parse_js_array(expression: str) -> List[Any]:
normalized = _fill_sparse_array_holes(expression)
value = ast.literal_eval(normalized)
if not isinstance(value, list):
raise ValueError("JavaScript assignment is not an array")
return value
def _parse_named_array(script: str, marker: str) -> List[Any]:
marker_index = script.find(marker)
if marker_index < 0:
raise ValueError(f"JavaScript marker not found: {marker}")
expression, _ = _extract_array_expression(script, marker_index + len(marker))
return _parse_js_array(expression)
def _is_match_row(value: Any) -> bool:
if not isinstance(value, list) or len(value) < 6:
return False
if _safe_int(value[0]) <= 0:
return False
return isinstance(value[3], str) and bool(re.match(r"^\d{4}-\d{2}-\d{2}\s", value[3]))
def _flatten_match_rows(value: Any) -> Iterable[List[Any]]:
if _is_match_row(value):
yield value
return
if isinstance(value, list):
for item in value:
yield from _flatten_match_rows(item)
def parse_titan_schedule_script(script: str) -> Dict[str, Any]:
script = (script or "").lstrip("\ufeff")
cup = _parse_named_array(script, "var arrCup =")
teams = _parse_named_array(script, "var arrTeam =")
team_map = {
str(row[0]): str(row[1]).strip()
for row in teams
if isinstance(row, list) and len(row) >= 2
}
matches: Dict[str, Dict[str, Any]] = {}
assignment_pattern = re.compile(r'jh\["(G[^"]+)"\]\s*=')
for assignment in assignment_pattern.finditer(script):
expression, _ = _extract_array_expression(script, assignment.end())
event_key = assignment.group(1)
for row in _flatten_match_rows(_parse_js_array(expression)):
match_id = str(row[0])
home_team_id = str(row[4] or "")
away_team_id = str(row[5] or "")
matches[match_id] = {
"source_match_id": match_id,
"source_event_id": event_key,
"match_time_text": str(row[3] or "").strip(),
"home_team_id": home_team_id,
"away_team_id": away_team_id,
"home_team": team_map.get(home_team_id, home_team_id),
"away_team": team_map.get(away_team_id, away_team_id),
"state": row[2] if len(row) > 2 else 0,
"schedule": row,
}
last_update_match = re.search(r"var\s+lastUpdateTime\s*=\s*'([^']*)'", script)
return {
"source_league_id": str(cup[0] if cup else ""),
"league_name": str(cup[1] if len(cup) > 1 else "球探赛事").strip(),
"season": str(cup[7] if len(cup) > 7 else "").strip(),
"last_update_time": last_update_match.group(1) if last_update_match else "",
"matches": matches,
}
def parse_titan_odds_script(script: str) -> Dict[str, List[List[Any]]]:
script = (script or "").lstrip("\ufeff")
odds: Dict[str, List[List[Any]]] = {}
assignment_pattern = re.compile(r'oddsData\["([OLT]_\d+)"\]\s*=')
for assignment in assignment_pattern.finditer(script):
expression, _ = _extract_array_expression(script, assignment.end())
value = json.loads(expression)
if isinstance(value, list):
odds[assignment.group(1)] = value
return odds
def _find_company_row(rows: List[List[Any]], company_id: int) -> Optional[List[Any]]:
for row in rows:
if isinstance(row, list) and row and _safe_int(row[0], -1) == company_id:
return row
return None
def _select_company_id(market_rows: Dict[str, List[List[Any]]], preferred_id: int) -> int:
coverage: Dict[int, int] = {}
for rows in market_rows.values():
seen = set()
for row in rows:
if not isinstance(row, list) or not row:
continue
company_id = _safe_int(row[0], -1)
if company_id < 0 or company_id in seen:
continue
seen.add(company_id)
coverage[company_id] = coverage.get(company_id, 0) + 1
if preferred_id in coverage:
return preferred_id
if not coverage:
return preferred_id
return sorted(coverage, key=lambda company_id: (-coverage[company_id], company_id))[0]
def build_titan_odds_items(
schedule_data: Dict[str, Any],
odds_data: Dict[str, List[List[Any]]],
*,
source_url: str,
company_id: int = DEFAULT_TITAN_COMPANY_ID,
euro_company_id: int = DEFAULT_TITAN_EURO_COMPANY_ID,
company_name: str = DEFAULT_TITAN_COMPANY_NAME,
show_type: str = "early",
) -> List[Dict[str, Any]]:
items: List[Dict[str, Any]] = []
matches = schedule_data.get("matches") or {}
for match_id, match in matches.items():
market_rows = {
"moneyline": odds_data.get(f"O_{match_id}", []),
"handicap": odds_data.get(f"L_{match_id}", []),
"total": odds_data.get(f"T_{match_id}", []),
}
selected_company_id = _select_company_id(
{
"handicap": market_rows["handicap"],
"total": market_rows["total"],
},
company_id,
)
selected_euro_company_id = _select_company_id(
{"moneyline": market_rows["moneyline"]},
euro_company_id,
)
moneyline = _find_company_row(
market_rows["moneyline"],
selected_euro_company_id,
)
handicap = _find_company_row(market_rows["handicap"], selected_company_id)
total = _find_company_row(market_rows["total"], selected_company_id)
if not any((moneyline, handicap, total)):
continue
handicap_value = handicap[2] if handicap and len(handicap) > 2 else ""
handicap_number: Optional[Decimal] = None
try:
handicap_number = Decimal(str(handicap_value))
except (InvalidOperation, ValueError):
pass
strong_side = ""
if handicap_number is not None:
if handicap_number > 0:
strong_side = "H"
elif handicap_number < 0:
strong_side = "C"
company_ids = {
_safe_int(row[0], -1)
for rows in market_rows.values()
for row in rows
if isinstance(row, list) and row
}
company_ids.discard(-1)
markets = {
"handicap": {
"line": _number_text(handicap_value, absolute=True),
"home": _number_text(handicap[1]) if handicap and len(handicap) > 1 else "",
"away": _number_text(handicap[3]) if handicap and len(handicap) > 3 else "",
"strong": strong_side,
},
"total": {
"over_line": _number_text(total[2]) if total and len(total) > 2 else "",
"under_line": _number_text(total[2]) if total and len(total) > 2 else "",
"over": _number_text(total[1]) if total and len(total) > 1 else "",
"under": _number_text(total[3]) if total and len(total) > 3 else "",
},
"moneyline": {
"home": _number_text(moneyline[1]) if moneyline and len(moneyline) > 1 else "",
"draw": _number_text(moneyline[2]) if moneyline and len(moneyline) > 2 else "",
"away": _number_text(moneyline[3]) if moneyline and len(moneyline) > 3 else "",
},
"first_half": {
"handicap_line": "",
"handicap_home": "",
"handicap_away": "",
"total_over_line": "",
"total_under_line": "",
"total_over": "",
"total_under": "",
"home_win": "",
"away_win": "",
"draw": "",
},
"both_teams_to_score": {"yes": "", "no": ""},
"kickoff": {"home": "", "away": ""},
}
selected_company_name = company_name if selected_company_id == company_id else str(selected_company_id)
raw_data = {
"schedule": match.get("schedule") or [],
"odds": {
"moneyline": market_rows["moneyline"],
"handicap": market_rows["handicap"],
"total": market_rows["total"],
},
"selected_company_id": selected_company_id,
"selected_euro_company_id": selected_euro_company_id,
"selected_company_name": selected_company_name,
"last_update_time": schedule_data.get("last_update_time") or "",
}
items.append(
{
"source_site": "titan007",
"source_url": _limit(source_url, 255),
"source_match_id": _limit(match_id, 64),
"source_parent_id": "",
"source_league_id": _limit(schedule_data.get("source_league_id"), 64),
"source_event_id": _limit(match.get("source_event_id"), 64),
"sport_type": "FT",
"show_type": _limit(show_type, 20),
"odds_type": "H",
"league_name": _limit(schedule_data.get("league_name"), 150),
"match_time_text": _limit(match.get("match_time_text"), 50),
"home_team_id": _limit(match.get("home_team_id"), 64),
"away_team_id": _limit(match.get("away_team_id"), 64),
"home_team": _limit(match.get("home_team"), 150),
"away_team": _limit(match.get("away_team"), 150),
"strong_side": strong_side,
"more_count": len(company_ids),
"is_running": 0,
"handicap_line": _limit(markets["handicap"]["line"], 32),
"handicap_home_odds": _limit(markets["handicap"]["home"], 32),
"handicap_away_odds": _limit(markets["handicap"]["away"], 32),
"total_line": _limit(markets["total"]["over_line"], 32),
"total_over_odds": _limit(markets["total"]["over"], 32),
"total_under_odds": _limit(markets["total"]["under"], 32),
"home_win_odds": _limit(markets["moneyline"]["home"], 32),
"draw_odds": _limit(markets["moneyline"]["draw"], 32),
"away_win_odds": _limit(markets["moneyline"]["away"], 32),
"half_handicap_line": "",
"half_home_win_odds": "",
"half_draw_odds": "",
"half_away_win_odds": "",
"raw_data": raw_data,
"markets": markets,
}
)
return items
class TitanOddsClient:
def __init__(
self,
*,
user_agent: str = DEFAULT_USER_AGENT,
timeout: int = 20,
verify_tls: bool = True,
company_id: int = DEFAULT_TITAN_COMPANY_ID,
euro_company_id: int = DEFAULT_TITAN_EURO_COMPANY_ID,
company_name: str = DEFAULT_TITAN_COMPANY_NAME,
show_type: str = "early",
request_delay: float = 0.2,
) -> None:
self.timeout = int(timeout)
self.verify_tls = bool(verify_tls)
self.company_id = int(company_id)
self.euro_company_id = int(euro_company_id)
self.company_name = company_name
self.show_type = show_type
self.request_delay = float(request_delay)
self.session = requests.Session()
self.session.verify = self.verify_tls
self.session.headers.update(
{
"User-Agent": user_agent,
"Accept": "text/html,application/xhtml+xml,application/javascript,*/*;q=0.8",
}
)
def fetch_cup(self, cup_url: str) -> List[Dict[str, Any]]:
page_response = self.session.get(cup_url, timeout=self.timeout)
page_response.raise_for_status()
page_html = page_response.text or ""
season_match = re.search(r"selectSeason\s*=\s*'([^']+)'", page_html)
league_match = re.search(r"SclassID\s*=\s*(\d+)", page_html)
script_match = re.search(
r"""src=["']([^"']*/jsData/matchResult/[^"']+\.js(?:\?[^"']*)?)["']""",
page_html,
re.IGNORECASE,
)
if not season_match or not league_match or not script_match:
raise ValueError("球探杯赛页面缺少赛季、联赛或赛程脚本")
season = season_match.group(1)
league_id = league_match.group(1)
schedule_url = urljoin(page_response.url, html.unescape(script_match.group(1)))
schedule_response = self.session.get(
schedule_url,
headers={"Referer": page_response.url},
timeout=self.timeout,
)
schedule_response.raise_for_status()
schedule_data = parse_titan_schedule_script(schedule_response.text)
if self.request_delay > 0:
time.sleep(self.request_delay)
odds_url = urljoin(page_response.url, "/League/LeagueOddsAjax")
odds_response = self.session.get(
odds_url,
params={
"sclassId": league_id,
"subSclassId": "",
"matchSeason": season,
"round": 1,
},
headers={
"Referer": page_response.url,
"X-Requested-With": "XMLHttpRequest",
},
timeout=self.timeout,
)
odds_response.raise_for_status()
odds_data = parse_titan_odds_script(odds_response.text)
return build_titan_odds_items(
schedule_data,
odds_data,
source_url=page_response.url,
company_id=self.company_id,
euro_company_id=self.euro_company_id,
company_name=self.company_name,
show_type=self.show_type,
)