bep40 commited on
Commit
669e5ba
·
verified ·
1 Parent(s): 1c3fe35

Upload match_detail_v2.py

Browse files
Files changed (1) hide show
  1. match_detail_v2.py +69 -195
match_detail_v2.py CHANGED
@@ -1,7 +1,4 @@
1
- """VNEWS — Match Detail Parser v2
2
- Parses bongda.com.vn match detail pages with correct selectors.
3
- Extracts timeline events (goals, cards, substitutions) and statistics.
4
- """
5
  import re
6
  import requests
7
  from bs4 import BeautifulSoup
@@ -14,53 +11,39 @@ HEADERS = {
14
  }
15
 
16
  API_HEADERS = {
17
- **HEADERS,
18
  "Accept": "application/json, text/javascript, */*; q=0.01",
19
  "X-Requested-With": "XMLHttpRequest",
 
20
  }
21
 
22
 
23
- def _mk(html):
24
- try:
25
- return BeautifulSoup(html, 'lxml')
26
- except Exception:
27
- return BeautifulSoup(html, 'html.parser')
28
-
29
-
30
  def _cl(s):
31
  return re.sub(r'\s+', ' ', str(s or '')).strip()
32
 
33
 
34
- def fetch_html(url, timeout=15):
35
- """Fetch HTML from URL."""
36
- resp = requests.get(url, headers=HEADERS, timeout=timeout, allow_redirects=True)
37
- resp.raise_for_status()
38
- return resp.text
39
-
40
-
41
  def _normalize_time(raw):
42
- """Normalize time display: '45' +2' → '45+2'', '46'' → '46''', etc."""
43
  t = _cl(raw)
44
  if not t:
45
  return t
46
- # "45' +2" → "45+2'"
47
  t = re.sub(r"(\d+)'\s*\+(\d+)", r"\1+\2'", t)
48
- # "46''" → "46'" (double apostrophe fix)
49
  t = t.replace("''", "'")
50
  return t
51
 
52
 
 
 
 
 
 
 
 
 
 
 
 
53
  def parse_events(sp):
54
- """Parse timeline events from BeautifulSoup object.
55
-
56
- Structure: .events > .period > .event.home-team|away-team
57
-
58
- Each event has:
59
- - .event-type with icon (goal/redcard/yellowcard/substitution SVG)
60
- - .players with .event-time and player names inside .players.card|.goal|.subst
61
-
62
- Returns list of event dicts with: type, time, team, player_in, player_out, scorer, assist, player
63
- """
64
  events = []
65
  events_div = sp.select_one('.events')
66
  if not events_div:
@@ -70,7 +53,7 @@ def parse_events(sp):
70
  for child in events_div.children:
71
  if not hasattr(child, 'name') or not child.name:
72
  continue
73
- cls_str = ' '.join(child.get('class', []))
74
 
75
  if 'period' in cls_str:
76
  h2 = child.find('h2')
@@ -80,76 +63,51 @@ def parse_events(sp):
80
  for ev in child.children:
81
  if not hasattr(ev, 'name') or not ev.name:
82
  continue
83
- ev_cls_str = ' '.join(ev.get('class', []))
84
  if 'event' not in ev_cls_str:
85
  continue
86
 
87
  team = 'home' if 'home' in ev_cls_str else 'away'
88
  ev_data = {
89
- 'team': team,
90
- 'period': current_period,
91
- 'type': 'unknown',
92
- 'time': '',
93
- 'players': '',
94
- 'player_in': '',
95
- 'player_out': '',
96
- 'scorer': '',
97
- 'assist': '',
98
- 'card_type': '',
99
- 'player': '',
100
  }
101
 
102
- # Determine event type from icon in .event-type
103
  type_el = ev.select_one('.event-type')
104
  if type_el:
105
  if type_el.select_one('[class*="redcard"]'):
106
- ev_data['type'] = 'redcard'
107
- ev_data['card_type'] = 'red'
108
  elif type_el.select_one('[class*="yellowcard"]'):
109
- ev_data['type'] = 'yellowcard'
110
- ev_data['card_type'] = 'yellow'
111
  elif type_el.select_one('[class*="goal"]'):
112
  ev_data['type'] = 'goal'
113
  elif type_el.select_one('[class*="substitution"]'):
114
  ev_data['type'] = 'substitution'
115
  else:
116
- # Check SVG rect color for red card
117
  for rect in type_el.select('svg rect'):
118
  if rect.get('fill') == '#E20007':
119
- ev_data['type'] = 'redcard'
120
- ev_data['card_type'] = 'red'
121
- break
122
- # Check SVG circle for goal (white filled circle = ball)
123
  if ev_data['type'] == 'unknown':
124
  for circle in type_el.select('svg circle'):
125
  if circle.get('fill') == 'white' and circle.get('r') == '8':
126
- ev_data['type'] = 'goal'
127
- break
128
- # Fallback: check .players class for subst
129
  if ev_data['type'] == 'unknown' and ev.select_one('.players.subst'):
130
  ev_data['type'] = 'substitution'
131
 
132
- # Additional type detection from .players class
133
  players_el = ev.select_one('.players')
134
  if players_el and ev_data['type'] == 'unknown':
135
- pcls = ' '.join(players_el.get('class', []))
136
- if 'goal' in pcls:
137
- ev_data['type'] = 'goal'
138
- elif 'card' in pcls:
139
- ev_data['type'] = 'redcard'
140
- ev_data['card_type'] = 'red'
141
- elif 'subst' in pcls:
142
- ev_data['type'] = 'substitution'
143
 
144
- # Extract time and player info
145
  if players_el:
146
  time_el = players_el.select_one('.event-time')
147
  if time_el:
148
  ev_data['time'] = _normalize_time(time_el.get_text())
149
-
150
  ev_data['players'] = _cl(players_el.get_text(' ', strip=True))
151
 
152
- # Parse individual text nodes (excluding time)
153
  texts = []
154
  for d in players_el.find_all('div', recursive=False):
155
  t = _cl(d.get_text())
@@ -162,50 +120,20 @@ def parse_events(sp):
162
 
163
  if ev_data['type'] == 'substitution':
164
  if len(texts) >= 2:
165
- ev_data['player_out'] = texts[0]
166
- ev_data['player_in'] = texts[1]
167
  elif len(texts) == 1:
168
  ev_data['player_in'] = texts[0]
169
- # Fallback: parse from combined text like "46' Jose Sa Rui Silva"
170
- if not ev_data['player_in'] and ev_data['players']:
171
- remaining = ev_data['players'].replace(ev_data['time'], '').strip()
172
- words = remaining.split()
173
- if len(words) >= 4:
174
- mid = len(words) // 2
175
- ev_data['player_out'] = ' '.join(words[:mid])
176
- ev_data['player_in'] = ' '.join(words[mid:])
177
-
178
  elif ev_data['type'] == 'goal':
179
- if len(texts) >= 1:
180
- ev_data['scorer'] = texts[0]
181
- if len(texts) >= 2:
182
- ev_data['assist'] = texts[1]
183
- # Fallback
184
- if not ev_data['scorer'] and ev_data['players']:
185
- remaining = ev_data['players'].replace(ev_data['time'], '').strip()
186
- words = remaining.split()
187
- if len(words) >= 2:
188
- ev_data['scorer'] = ' '.join(words[:2])
189
- elif len(words) == 1:
190
- ev_data['scorer'] = words[0]
191
-
192
  elif ev_data['type'] in ('redcard', 'yellowcard'):
193
- if texts:
194
- ev_data['player'] = ' '.join(texts)
195
- elif ev_data['players']:
196
- remaining = ev_data['players'].replace(ev_data['time'], '').strip()
197
- if remaining:
198
- ev_data['player'] = remaining
199
 
200
  events.append(ev_data)
201
-
202
  return events
203
 
204
 
205
  def fetch_match_detail(event_id: int) -> dict:
206
- """Fetch and parse match detail by event ID.
207
- Tries /centre/, /preview/, /bao-cao-nhanh/ URL formats.
208
- """
209
  result = {"event_id": event_id, "found": False, "sections": []}
210
 
211
  html = None
@@ -213,11 +141,11 @@ def fetch_match_detail(event_id: int) -> dict:
213
  for suffix in ['/centre/', '/preview/', '/bao-cao-nhanh/']:
214
  url = base + suffix
215
  try:
216
- html = fetch_html(url, timeout=15)
217
- if html and len(html) > 1000 and '.teams' in html:
 
218
  break
219
  except Exception:
220
- html = None
221
  continue
222
 
223
  if not html:
@@ -226,35 +154,28 @@ def fetch_match_detail(event_id: int) -> dict:
226
  sp = _mk(html)
227
  info = {}
228
 
229
- # === Parse teams + score ===
230
  tel = sp.select_one('.teams')
231
  if tel:
232
  he = tel.select_one('.team.home') or tel.select_one('[class*="home"]')
233
  if he:
234
  ne = he.select_one('p:not(.logo)') or he.find('p')
235
- if ne:
236
- info['home_team'] = _cl(ne.get_text())
237
  lo = he.select_one('img')
238
- if lo:
239
- info['home_logo'] = lo.get('src', '')
240
 
241
  ae = tel.select_one('.team.away') or tel.select_one('[class*="away"]')
242
  if ae:
243
  ne = ae.select_one('p:not(.logo)') or ae.find('p')
244
- if ne:
245
- info['away_team'] = _cl(ne.get_text())
246
  lo = ae.select_one('img')
247
- if lo:
248
- info['away_logo'] = lo.get('src', '')
249
 
250
  sc = tel.select_one('.score')
251
  if sc:
252
  parts = [_cl(p.get_text()) for p in sc.select('p')]
253
- if len(parts) >= 2:
254
- info['score'] = f"{parts[0]} - {parts[1]}"
255
  lb = sc.select_one('.label')
256
- if lb:
257
- info['status_label'] = _cl(lb.get_text())
258
 
259
  if info.get('home_team') and info.get('away_team'):
260
  result['info'] = info
@@ -263,24 +184,19 @@ def fetch_match_detail(event_id: int) -> dict:
263
  else:
264
  return result
265
 
266
- # === Parse match info (date/time) ===
267
  mi = sp.select_one('.match-info')
268
  if mi:
269
  for sel in ['.times', 'li']:
270
  el = mi.select_one(sel)
271
  if el:
272
  t = _cl(el.get_text())
273
- if t:
274
- info.setdefault('datetime', t)
275
- break
276
 
277
- # === Parse events (timeline) ===
278
  events = parse_events(sp)
279
  if events:
280
  result['events'] = events
281
  result['sections'].append('events')
282
 
283
- # === Parse prediction card ===
284
  pred = sp.select_one('.prediction-card')
285
  if pred:
286
  pred_data = {}
@@ -291,19 +207,15 @@ def fetch_match_detail(event_id: int) -> dict:
291
  pred_data['home_name'] = _cl(teams[0].select_one('.team-name').get_text()) if teams[0].select_one('.team-name') else ''
292
  pred_data['away_name'] = _cl(teams[1].select_one('.team-name').get_text()) if teams[1].select_one('.team-name') else ''
293
  divider = team_info.select_one('.divider')
294
- if divider:
295
- pred_data['result'] = _cl(divider.get_text())
296
  vote_count = pred.select_one('.vote-count')
297
- if vote_count:
298
- pred_data['vote_count'] = _cl(vote_count.get_text())
299
  result['prediction'] = pred_data
300
 
301
- # === Fetch H2H stats from API ===
302
  try:
303
  ar = requests.get(
304
  f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}",
305
- headers=API_HEADERS,
306
- timeout=10
307
  )
308
  if ar.status_code == 200:
309
  ad = ar.json()
@@ -314,18 +226,11 @@ def fetch_match_detail(event_id: int) -> dict:
314
  cells = row.select('td, span, p')
315
  if len(cells) >= 3:
316
  lb = _cl(cells[0].get_text())
317
- if lb:
318
- ast[lb] = {
319
- 'home': _cl(cells[1].get_text()),
320
- 'away': _cl(cells[2].get_text()),
321
- }
322
- if ast:
323
- result['h2h_stats_parsed'] = ast
324
- result['sections'].append('h2h_stats')
325
  except Exception:
326
  pass
327
 
328
- # === Parse H2H standings table ===
329
  h2h_data = []
330
  h2h_el = sp.select_one('.h2h-standings')
331
  if h2h_el:
@@ -347,11 +252,8 @@ def fetch_match_detail(event_id: int) -> dict:
347
  'ga': _cl(cells[6].get_text()) if len(cells) > 6 else '',
348
  'points': _cl(cells[8].get_text()) if len(cells) > 8 else '',
349
  })
350
- if h2h_data:
351
- result['h2h_standings'] = h2h_data
352
- result['sections'].append('h2h_standings')
353
 
354
- # === Parse recent matches from sidebar ===
355
  recent_matches = []
356
  matches_list = sp.select_one('.matches-list')
357
  if matches_list:
@@ -369,29 +271,27 @@ def fetch_match_detail(event_id: int) -> dict:
369
  'away': _cl(away_el.get_text()) if away_el else '',
370
  'score': _cl(score_el.get_text()) if score_el else 'vs',
371
  })
372
- if recent_matches:
373
- result['recent_matches'] = recent_matches
374
- result['sections'].append('recent')
375
 
376
  return result
377
 
378
 
379
  def fetch_match_detail_by_url(url: str) -> dict:
380
- """Fetch and parse match detail from a full bongda.com.vn URL (with slug)."""
381
  eid_match = re.search(r'/tran-dau/(\d+)/', url)
382
  if not eid_match:
383
  return {"event_id": 0, "found": False, "error": "Cannot extract event_id from URL"}
384
-
385
  event_id = int(eid_match.group(1))
386
  result = {"event_id": event_id, "found": False, "sections": []}
387
 
388
  html = None
389
  try:
390
- html = fetch_html(url, timeout=15)
 
 
391
  except Exception:
392
  pass
393
 
394
- if not html or len(html) < 1000 or '.teams' not in html:
395
  return fetch_match_detail(event_id)
396
 
397
  sp = _mk(html)
@@ -402,47 +302,34 @@ def fetch_match_detail_by_url(url: str) -> dict:
402
  he = tel.select_one('.team.home') or tel.select_one('[class*="home"]')
403
  if he:
404
  ne = he.select_one('p:not(.logo)') or he.find('p')
405
- if ne:
406
- info['home_team'] = _cl(ne.get_text())
407
  lo = he.select_one('img')
408
- if lo:
409
- info['home_logo'] = lo.get('src', '')
410
-
411
  ae = tel.select_one('.team.away') or tel.select_one('[class*="away"]')
412
  if ae:
413
  ne = ae.select_one('p:not(.logo)') or ae.find('p')
414
- if ne:
415
- info['away_team'] = _cl(ne.get_text())
416
  lo = ae.select_one('img')
417
- if lo:
418
- info['away_logo'] = lo.get('src', '')
419
-
420
  sc = tel.select_one('.score')
421
  if sc:
422
  parts = [_cl(p.get_text()) for p in sc.select('p')]
423
- if len(parts) >= 2:
424
- info['score'] = f"{parts[0]} - {parts[1]}"
425
  lb = sc.select_one('.label')
426
- if lb:
427
- info['status_label'] = _cl(lb.get_text())
428
 
429
  if info.get('home_team') and info.get('away_team'):
430
- result['info'] = info
431
- result['found'] = True
432
- result['sections'].append('info')
433
  else:
434
  return fetch_match_detail(event_id)
435
 
436
  mi = sp.select_one('.match-info')
437
  if mi:
438
  te = mi.select_one('.times, li')
439
- if te:
440
- info.setdefault('datetime', _cl(te.get_text()))
441
 
442
  events = parse_events(sp)
443
- if events:
444
- result['events'] = events
445
- result['sections'].append('events')
446
 
447
  pred = sp.select_one('.prediction-card')
448
  if pred:
@@ -454,18 +341,15 @@ def fetch_match_detail_by_url(url: str) -> dict:
454
  pred_data['home_name'] = _cl(teams[0].select_one('.team-name').get_text()) if teams[0].select_one('.team-name') else ''
455
  pred_data['away_name'] = _cl(teams[1].select_one('.team-name').get_text()) if teams[1].select_one('.team-name') else ''
456
  divider = team_info.select_one('.divider')
457
- if divider:
458
- pred_data['result'] = _cl(divider.get_text())
459
  vote_count = pred.select_one('.vote-count')
460
- if vote_count:
461
- pred_data['vote_count'] = _cl(vote_count.get_text())
462
  result['prediction'] = pred_data
463
 
464
  try:
465
  ar = requests.get(
466
  f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}",
467
- headers=API_HEADERS,
468
- timeout=10
469
  )
470
  if ar.status_code == 200:
471
  ad = ar.json()
@@ -476,14 +360,8 @@ def fetch_match_detail_by_url(url: str) -> dict:
476
  cells = row.select('td, span, p')
477
  if len(cells) >= 3:
478
  lb = _cl(cells[0].get_text())
479
- if lb:
480
- ast[lb] = {
481
- 'home': _cl(cells[1].get_text()),
482
- 'away': _cl(cells[2].get_text()),
483
- }
484
- if ast:
485
- result['h2h_stats_parsed'] = ast
486
- result['sections'].append('h2h_stats')
487
  except Exception:
488
  pass
489
 
@@ -508,9 +386,7 @@ def fetch_match_detail_by_url(url: str) -> dict:
508
  'ga': _cl(cells[6].get_text()) if len(cells) > 6 else '',
509
  'points': _cl(cells[8].get_text()) if len(cells) > 8 else '',
510
  })
511
- if h2h_data:
512
- result['h2h_standings'] = h2h_data
513
- result['sections'].append('h2h_standings')
514
 
515
  recent_matches = []
516
  matches_list = sp.select_one('.matches-list')
@@ -529,8 +405,6 @@ def fetch_match_detail_by_url(url: str) -> dict:
529
  'away': _cl(away_el.get_text()) if away_el else '',
530
  'score': _cl(score_el.get_text()) if score_el else 'vs',
531
  })
532
- if recent_matches:
533
- result['recent_matches'] = recent_matches
534
- result['sections'].append('recent')
535
 
536
  return result
 
1
+ """VNEWS — Match Detail Parser v2 (html.parser only, no lxml dependency)"""
 
 
 
2
  import re
3
  import requests
4
  from bs4 import BeautifulSoup
 
11
  }
12
 
13
  API_HEADERS = {
14
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
15
  "Accept": "application/json, text/javascript, */*; q=0.01",
16
  "X-Requested-With": "XMLHttpRequest",
17
+ "Referer": "https://bongda.com.vn/",
18
  }
19
 
20
 
 
 
 
 
 
 
 
21
  def _cl(s):
22
  return re.sub(r'\s+', ' ', str(s or '')).strip()
23
 
24
 
 
 
 
 
 
 
 
25
  def _normalize_time(raw):
 
26
  t = _cl(raw)
27
  if not t:
28
  return t
 
29
  t = re.sub(r"(\d+)'\s*\+(\d+)", r"\1+\2'", t)
 
30
  t = t.replace("''", "'")
31
  return t
32
 
33
 
34
+ def _mk(html):
35
+ """Parse HTML using html.parser (lxml may not be available)."""
36
+ return BeautifulSoup(html, 'html.parser')
37
+
38
+
39
+ def fetch_html(url, timeout=15):
40
+ resp = requests.get(url, headers=HEADERS, timeout=timeout, allow_redirects=True)
41
+ resp.raise_for_status()
42
+ return resp.text
43
+
44
+
45
  def parse_events(sp):
46
+ """Parse .events > .period > .event structure."""
 
 
 
 
 
 
 
 
 
47
  events = []
48
  events_div = sp.select_one('.events')
49
  if not events_div:
 
53
  for child in events_div.children:
54
  if not hasattr(child, 'name') or not child.name:
55
  continue
56
+ cls_str = ' '.join(child.get('class', []) if child.get('class') else [])
57
 
58
  if 'period' in cls_str:
59
  h2 = child.find('h2')
 
63
  for ev in child.children:
64
  if not hasattr(ev, 'name') or not ev.name:
65
  continue
66
+ ev_cls_str = ' '.join(ev.get('class', []) if ev.get('class') else [])
67
  if 'event' not in ev_cls_str:
68
  continue
69
 
70
  team = 'home' if 'home' in ev_cls_str else 'away'
71
  ev_data = {
72
+ 'team': team, 'period': current_period, 'type': 'unknown',
73
+ 'time': '', 'players': '', 'player_in': '', 'player_out': '',
74
+ 'scorer': '', 'assist': '', 'card_type': '', 'player': '',
 
 
 
 
 
 
 
 
75
  }
76
 
 
77
  type_el = ev.select_one('.event-type')
78
  if type_el:
79
  if type_el.select_one('[class*="redcard"]'):
80
+ ev_data['type'] = 'redcard'; ev_data['card_type'] = 'red'
 
81
  elif type_el.select_one('[class*="yellowcard"]'):
82
+ ev_data['type'] = 'yellowcard'; ev_data['card_type'] = 'yellow'
 
83
  elif type_el.select_one('[class*="goal"]'):
84
  ev_data['type'] = 'goal'
85
  elif type_el.select_one('[class*="substitution"]'):
86
  ev_data['type'] = 'substitution'
87
  else:
 
88
  for rect in type_el.select('svg rect'):
89
  if rect.get('fill') == '#E20007':
90
+ ev_data['type'] = 'redcard'; ev_data['card_type'] = 'red'; break
 
 
 
91
  if ev_data['type'] == 'unknown':
92
  for circle in type_el.select('svg circle'):
93
  if circle.get('fill') == 'white' and circle.get('r') == '8':
94
+ ev_data['type'] = 'goal'; break
 
 
95
  if ev_data['type'] == 'unknown' and ev.select_one('.players.subst'):
96
  ev_data['type'] = 'substitution'
97
 
 
98
  players_el = ev.select_one('.players')
99
  if players_el and ev_data['type'] == 'unknown':
100
+ pcls = ' '.join(players_el.get('class', []) if players_el.get('class') else [])
101
+ if 'goal' in pcls: ev_data['type'] = 'goal'
102
+ elif 'card' in pcls: ev_data['type'] = 'redcard'; ev_data['card_type'] = 'red'
103
+ elif 'subst' in pcls: ev_data['type'] = 'substitution'
 
 
 
 
104
 
 
105
  if players_el:
106
  time_el = players_el.select_one('.event-time')
107
  if time_el:
108
  ev_data['time'] = _normalize_time(time_el.get_text())
 
109
  ev_data['players'] = _cl(players_el.get_text(' ', strip=True))
110
 
 
111
  texts = []
112
  for d in players_el.find_all('div', recursive=False):
113
  t = _cl(d.get_text())
 
120
 
121
  if ev_data['type'] == 'substitution':
122
  if len(texts) >= 2:
123
+ ev_data['player_out'] = texts[0]; ev_data['player_in'] = texts[1]
 
124
  elif len(texts) == 1:
125
  ev_data['player_in'] = texts[0]
 
 
 
 
 
 
 
 
 
126
  elif ev_data['type'] == 'goal':
127
+ if len(texts) >= 1: ev_data['scorer'] = texts[0]
128
+ if len(texts) >= 2: ev_data['assist'] = texts[1]
 
 
 
 
 
 
 
 
 
 
 
129
  elif ev_data['type'] in ('redcard', 'yellowcard'):
130
+ if texts: ev_data['player'] = ' '.join(texts)
 
 
 
 
 
131
 
132
  events.append(ev_data)
 
133
  return events
134
 
135
 
136
  def fetch_match_detail(event_id: int) -> dict:
 
 
 
137
  result = {"event_id": event_id, "found": False, "sections": []}
138
 
139
  html = None
 
141
  for suffix in ['/centre/', '/preview/', '/bao-cao-nhanh/']:
142
  url = base + suffix
143
  try:
144
+ resp = requests.get(url, headers=HEADERS, timeout=15, allow_redirects=True)
145
+ if resp.status_code == 200 and len(resp.text) > 1000:
146
+ html = resp.text
147
  break
148
  except Exception:
 
149
  continue
150
 
151
  if not html:
 
154
  sp = _mk(html)
155
  info = {}
156
 
 
157
  tel = sp.select_one('.teams')
158
  if tel:
159
  he = tel.select_one('.team.home') or tel.select_one('[class*="home"]')
160
  if he:
161
  ne = he.select_one('p:not(.logo)') or he.find('p')
162
+ if ne: info['home_team'] = _cl(ne.get_text())
 
163
  lo = he.select_one('img')
164
+ if lo: info['home_logo'] = lo.get('src', '')
 
165
 
166
  ae = tel.select_one('.team.away') or tel.select_one('[class*="away"]')
167
  if ae:
168
  ne = ae.select_one('p:not(.logo)') or ae.find('p')
169
+ if ne: info['away_team'] = _cl(ne.get_text())
 
170
  lo = ae.select_one('img')
171
+ if lo: info['away_logo'] = lo.get('src', '')
 
172
 
173
  sc = tel.select_one('.score')
174
  if sc:
175
  parts = [_cl(p.get_text()) for p in sc.select('p')]
176
+ if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}"
 
177
  lb = sc.select_one('.label')
178
+ if lb: info['status_label'] = _cl(lb.get_text())
 
179
 
180
  if info.get('home_team') and info.get('away_team'):
181
  result['info'] = info
 
184
  else:
185
  return result
186
 
 
187
  mi = sp.select_one('.match-info')
188
  if mi:
189
  for sel in ['.times', 'li']:
190
  el = mi.select_one(sel)
191
  if el:
192
  t = _cl(el.get_text())
193
+ if t: info.setdefault('datetime', t); break
 
 
194
 
 
195
  events = parse_events(sp)
196
  if events:
197
  result['events'] = events
198
  result['sections'].append('events')
199
 
 
200
  pred = sp.select_one('.prediction-card')
201
  if pred:
202
  pred_data = {}
 
207
  pred_data['home_name'] = _cl(teams[0].select_one('.team-name').get_text()) if teams[0].select_one('.team-name') else ''
208
  pred_data['away_name'] = _cl(teams[1].select_one('.team-name').get_text()) if teams[1].select_one('.team-name') else ''
209
  divider = team_info.select_one('.divider')
210
+ if divider: pred_data['result'] = _cl(divider.get_text())
 
211
  vote_count = pred.select_one('.vote-count')
212
+ if vote_count: pred_data['vote_count'] = _cl(vote_count.get_text())
 
213
  result['prediction'] = pred_data
214
 
 
215
  try:
216
  ar = requests.get(
217
  f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}",
218
+ headers=API_HEADERS, timeout=10
 
219
  )
220
  if ar.status_code == 200:
221
  ad = ar.json()
 
226
  cells = row.select('td, span, p')
227
  if len(cells) >= 3:
228
  lb = _cl(cells[0].get_text())
229
+ if lb: ast[lb] = {'home': _cl(cells[1].get_text()), 'away': _cl(cells[2].get_text())}
230
+ if ast: result['h2h_stats_parsed'] = ast; result['sections'].append('h2h_stats')
 
 
 
 
 
 
231
  except Exception:
232
  pass
233
 
 
234
  h2h_data = []
235
  h2h_el = sp.select_one('.h2h-standings')
236
  if h2h_el:
 
252
  'ga': _cl(cells[6].get_text()) if len(cells) > 6 else '',
253
  'points': _cl(cells[8].get_text()) if len(cells) > 8 else '',
254
  })
255
+ if h2h_data: result['h2h_standings'] = h2h_data; result['sections'].append('h2h_standings')
 
 
256
 
 
257
  recent_matches = []
258
  matches_list = sp.select_one('.matches-list')
259
  if matches_list:
 
271
  'away': _cl(away_el.get_text()) if away_el else '',
272
  'score': _cl(score_el.get_text()) if score_el else 'vs',
273
  })
274
+ if recent_matches: result['recent_matches'] = recent_matches; result['sections'].append('recent')
 
 
275
 
276
  return result
277
 
278
 
279
  def fetch_match_detail_by_url(url: str) -> dict:
 
280
  eid_match = re.search(r'/tran-dau/(\d+)/', url)
281
  if not eid_match:
282
  return {"event_id": 0, "found": False, "error": "Cannot extract event_id from URL"}
 
283
  event_id = int(eid_match.group(1))
284
  result = {"event_id": event_id, "found": False, "sections": []}
285
 
286
  html = None
287
  try:
288
+ resp = requests.get(url, headers=HEADERS, timeout=15, allow_redirects=True)
289
+ if resp.status_code == 200 and len(resp.text) > 1000:
290
+ html = resp.text
291
  except Exception:
292
  pass
293
 
294
+ if not html:
295
  return fetch_match_detail(event_id)
296
 
297
  sp = _mk(html)
 
302
  he = tel.select_one('.team.home') or tel.select_one('[class*="home"]')
303
  if he:
304
  ne = he.select_one('p:not(.logo)') or he.find('p')
305
+ if ne: info['home_team'] = _cl(ne.get_text())
 
306
  lo = he.select_one('img')
307
+ if lo: info['home_logo'] = lo.get('src', '')
 
 
308
  ae = tel.select_one('.team.away') or tel.select_one('[class*="away"]')
309
  if ae:
310
  ne = ae.select_one('p:not(.logo)') or ae.find('p')
311
+ if ne: info['away_team'] = _cl(ne.get_text())
 
312
  lo = ae.select_one('img')
313
+ if lo: info['away_logo'] = lo.get('src', '')
 
 
314
  sc = tel.select_one('.score')
315
  if sc:
316
  parts = [_cl(p.get_text()) for p in sc.select('p')]
317
+ if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}"
 
318
  lb = sc.select_one('.label')
319
+ if lb: info['status_label'] = _cl(lb.get_text())
 
320
 
321
  if info.get('home_team') and info.get('away_team'):
322
+ result['info'] = info; result['found'] = True; result['sections'].append('info')
 
 
323
  else:
324
  return fetch_match_detail(event_id)
325
 
326
  mi = sp.select_one('.match-info')
327
  if mi:
328
  te = mi.select_one('.times, li')
329
+ if te: info.setdefault('datetime', _cl(te.get_text()))
 
330
 
331
  events = parse_events(sp)
332
+ if events: result['events'] = events; result['sections'].append('events')
 
 
333
 
334
  pred = sp.select_one('.prediction-card')
335
  if pred:
 
341
  pred_data['home_name'] = _cl(teams[0].select_one('.team-name').get_text()) if teams[0].select_one('.team-name') else ''
342
  pred_data['away_name'] = _cl(teams[1].select_one('.team-name').get_text()) if teams[1].select_one('.team-name') else ''
343
  divider = team_info.select_one('.divider')
344
+ if divider: pred_data['result'] = _cl(divider.get_text())
 
345
  vote_count = pred.select_one('.vote-count')
346
+ if vote_count: pred_data['vote_count'] = _cl(vote_count.get_text())
 
347
  result['prediction'] = pred_data
348
 
349
  try:
350
  ar = requests.get(
351
  f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}",
352
+ headers=API_HEADERS, timeout=10
 
353
  )
354
  if ar.status_code == 200:
355
  ad = ar.json()
 
360
  cells = row.select('td, span, p')
361
  if len(cells) >= 3:
362
  lb = _cl(cells[0].get_text())
363
+ if lb: ast[lb] = {'home': _cl(cells[1].get_text()), 'away': _cl(cells[2].get_text())}
364
+ if ast: result['h2h_stats_parsed'] = ast; result['sections'].append('h2h_stats')
 
 
 
 
 
 
365
  except Exception:
366
  pass
367
 
 
386
  'ga': _cl(cells[6].get_text()) if len(cells) > 6 else '',
387
  'points': _cl(cells[8].get_text()) if len(cells) > 8 else '',
388
  })
389
+ if h2h_data: result['h2h_standings'] = h2h_data; result['sections'].append('h2h_standings')
 
 
390
 
391
  recent_matches = []
392
  matches_list = sp.select_one('.matches-list')
 
405
  'away': _cl(away_el.get_text()) if away_el else '',
406
  'score': _cl(score_el.get_text()) if score_el else 'vs',
407
  })
408
+ if recent_matches: result['recent_matches'] = recent_matches; result['sections'].append('recent')
 
 
409
 
410
  return result