bep40 commited on
Commit
6e02bc5
·
verified ·
1 Parent(s): d1d3ae7

Upload match_detail_v2.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. match_detail_v2.py +418 -0
match_detail_v2.py ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """VNEWS — Match Detail Parser v2 (html.parser only, no lxml dependency)"""
2
+ import re
3
+ import requests
4
+ from bs4 import BeautifulSoup
5
+
6
+ HEADERS = {
7
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
8
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
9
+ "Accept-Language": "vi-VN,vi;q=0.9",
10
+ "Referer": "https://bongda.com.vn/",
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=8):
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:
50
+ return events
51
+
52
+ current_period = ''
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')
60
+ if h2:
61
+ current_period = _cl(h2.get_text())
62
+
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())
114
+ if t and t != ev_data['time']:
115
+ texts.append(t)
116
+ for p in players_el.find_all('p', recursive=False):
117
+ t = _cl(p.get_text())
118
+ if t and t not in texts:
119
+ texts.append(t)
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
+ import concurrent.futures
138
+ result = {"event_id": event_id, "found": False, "sections": []}
139
+
140
+ html = None
141
+ base = f"https://bongda.com.vn/tran-dau/{event_id}"
142
+ urls = [base + suffix for suffix in ['/centre/', '/preview/', '/bao-cao-nhanh/']]
143
+
144
+ # Try all URLs in parallel, take first success
145
+ with concurrent.futures.ThreadPoolExecutor(max_workers=3) as ex:
146
+ futures = {ex.submit(requests.get, url, headers=HEADERS, timeout=8, allow_redirects=True): url for url in urls}
147
+ for future in concurrent.futures.as_completed(futures, timeout=12):
148
+ try:
149
+ resp = future.result()
150
+ if resp.status_code == 200 and len(resp.text) > 1000:
151
+ html = resp.text
152
+ for f in futures:
153
+ f.cancel()
154
+ break
155
+ except Exception:
156
+ continue
157
+
158
+ if not html:
159
+ return result
160
+
161
+ sp = _mk(html)
162
+ info = {}
163
+
164
+ tel = sp.select_one('.teams')
165
+ if tel:
166
+ he = tel.select_one('.team.home') or tel.select_one('[class*="home"]')
167
+ if he:
168
+ ne = he.select_one('p:not(.logo)') or he.find('p')
169
+ if ne: info['home_team'] = _cl(ne.get_text())
170
+ lo = he.select_one('img')
171
+ if lo: info['home_logo'] = lo.get('src', '')
172
+
173
+ ae = tel.select_one('.team.away') or tel.select_one('[class*="away"]')
174
+ if ae:
175
+ ne = ae.select_one('p:not(.logo)') or ae.find('p')
176
+ if ne: info['away_team'] = _cl(ne.get_text())
177
+ lo = ae.select_one('img')
178
+ if lo: info['away_logo'] = lo.get('src', '')
179
+
180
+ sc = tel.select_one('.score')
181
+ if sc:
182
+ parts = [_cl(p.get_text()) for p in sc.select('p')]
183
+ if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}"
184
+ lb = sc.select_one('.label')
185
+ if lb: info['status_label'] = _cl(lb.get_text())
186
+
187
+ if info.get('home_team') and info.get('away_team'):
188
+ result['info'] = info
189
+ result['found'] = True
190
+ result['sections'].append('info')
191
+ else:
192
+ return result
193
+
194
+ mi = sp.select_one('.match-info')
195
+ if mi:
196
+ for sel in ['.times', 'li']:
197
+ el = mi.select_one(sel)
198
+ if el:
199
+ t = _cl(el.get_text())
200
+ if t: info.setdefault('datetime', t); break
201
+
202
+ events = parse_events(sp)
203
+ if events:
204
+ result['events'] = events
205
+ result['sections'].append('events')
206
+
207
+ pred = sp.select_one('.prediction-card')
208
+ if pred:
209
+ pred_data = {}
210
+ team_info = pred.select_one('.team-info')
211
+ if team_info:
212
+ teams = team_info.select('.team')
213
+ if len(teams) >= 2:
214
+ pred_data['home_name'] = _cl(teams[0].select_one('.team-name').get_text()) if teams[0].select_one('.team-name') else ''
215
+ pred_data['away_name'] = _cl(teams[1].select_one('.team-name').get_text()) if teams[1].select_one('.team-name') else ''
216
+ divider = team_info.select_one('.divider')
217
+ if divider: pred_data['result'] = _cl(divider.get_text())
218
+ vote_count = pred.select_one('.vote-count')
219
+ if vote_count: pred_data['vote_count'] = _cl(vote_count.get_text())
220
+ result['prediction'] = pred_data
221
+
222
+ try:
223
+ ar = requests.get(
224
+ f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}",
225
+ headers=API_HEADERS, timeout=6
226
+ )
227
+ if ar.status_code == 200:
228
+ ad = ar.json()
229
+ if ad.get('status') == 'success' and ad.get('html'):
230
+ asp = _mk(ad['html'])
231
+ ast = {}
232
+ for row in asp.select('li, tr, .stat-row'):
233
+ cells = row.select('td, span, p')
234
+ if len(cells) >= 3:
235
+ lb = _cl(cells[0].get_text())
236
+ if lb: ast[lb] = {'home': _cl(cells[1].get_text()), 'away': _cl(cells[2].get_text())}
237
+ if ast: result['h2h_stats_parsed'] = ast; result['sections'].append('h2h_stats')
238
+ except Exception:
239
+ pass
240
+
241
+ h2h_data = []
242
+ h2h_el = sp.select_one('.h2h-standings')
243
+ if h2h_el:
244
+ rows = h2h_el.select('.ranking-table tbody tr, .leaderboard tr')
245
+ for row in rows:
246
+ cells = row.select('td')
247
+ if len(cells) >= 4:
248
+ logo = row.select_one('img')
249
+ name_el = row.select_one('.team-name, p.link, .name')
250
+ h2h_data.append({
251
+ 'pos': _cl(cells[0].get_text()),
252
+ 'logo': logo.get('src', '') if logo else '',
253
+ 'name': _cl(name_el.get_text()) if name_el else '',
254
+ 'played': _cl(cells[1].get_text()) if len(cells) > 1 else '',
255
+ 'wins': _cl(cells[2].get_text()) if len(cells) > 2 else '',
256
+ 'draws': _cl(cells[3].get_text()) if len(cells) > 3 else '',
257
+ 'losses': _cl(cells[4].get_text()) if len(cells) > 4 else '',
258
+ 'gf': _cl(cells[5].get_text()) if len(cells) > 5 else '',
259
+ 'ga': _cl(cells[6].get_text()) if len(cells) > 6 else '',
260
+ 'points': _cl(cells[8].get_text()) if len(cells) > 8 else '',
261
+ })
262
+ if h2h_data: result['h2h_standings'] = h2h_data; result['sections'].append('h2h_standings')
263
+
264
+ recent_matches = []
265
+ matches_list = sp.select_one('.matches-list')
266
+ if matches_list:
267
+ for item in matches_list.select('.match-detail, .match-item, li'):
268
+ date_el = item.select_one('.date, .time, .match-time')
269
+ league_el = item.select_one('.league')
270
+ home_el = item.select_one('.home, .team-home')
271
+ away_el = item.select_one('.away, .team-away')
272
+ score_el = item.select_one('.score, .result')
273
+ if home_el or away_el:
274
+ recent_matches.append({
275
+ 'date': _cl(date_el.get_text()) if date_el else '',
276
+ 'league': _cl(league_el.get_text()) if league_el else '',
277
+ 'home': _cl(home_el.get_text()) if home_el else '',
278
+ 'away': _cl(away_el.get_text()) if away_el else '',
279
+ 'score': _cl(score_el.get_text()) if score_el else 'vs',
280
+ })
281
+ if recent_matches: result['recent_matches'] = recent_matches; result['sections'].append('recent')
282
+
283
+ return result
284
+
285
+
286
+ def fetch_match_detail_by_url(url: str) -> dict:
287
+ import concurrent.futures
288
+ eid_match = re.search(r'/tran-dau/(\d+)/', url)
289
+ if not eid_match:
290
+ return {"event_id": 0, "found": False, "error": "Cannot extract event_id from URL"}
291
+ event_id = int(eid_match.group(1))
292
+ result = {"event_id": event_id, "found": False, "sections": []}
293
+
294
+ html = None
295
+ try:
296
+ resp = requests.get(url, headers=HEADERS, timeout=8, allow_redirects=True)
297
+ if resp.status_code == 200 and len(resp.text) > 1000:
298
+ html = resp.text
299
+ except Exception:
300
+ pass
301
+
302
+ if not html:
303
+ return fetch_match_detail(event_id)
304
+
305
+ sp = _mk(html)
306
+ info = {}
307
+
308
+ tel = sp.select_one('.teams')
309
+ if tel:
310
+ he = tel.select_one('.team.home') or tel.select_one('[class*="home"]')
311
+ if he:
312
+ ne = he.select_one('p:not(.logo)') or he.find('p')
313
+ if ne: info['home_team'] = _cl(ne.get_text())
314
+ lo = he.select_one('img')
315
+ if lo: info['home_logo'] = lo.get('src', '')
316
+ ae = tel.select_one('.team.away') or tel.select_one('[class*="away"]')
317
+ if ae:
318
+ ne = ae.select_one('p:not(.logo)') or ae.find('p')
319
+ if ne: info['away_team'] = _cl(ne.get_text())
320
+ lo = ae.select_one('img')
321
+ if lo: info['away_logo'] = lo.get('src', '')
322
+ sc = tel.select_one('.score')
323
+ if sc:
324
+ parts = [_cl(p.get_text()) for p in sc.select('p')]
325
+ if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}"
326
+ lb = sc.select_one('.label')
327
+ if lb: info['status_label'] = _cl(lb.get_text())
328
+
329
+ if info.get('home_team') and info.get('away_team'):
330
+ result['info'] = info; result['found'] = True; result['sections'].append('info')
331
+ else:
332
+ return fetch_match_detail(event_id)
333
+
334
+ mi = sp.select_one('.match-info')
335
+ if mi:
336
+ te = mi.select_one('.times, li')
337
+ if te: info.setdefault('datetime', _cl(te.get_text()))
338
+
339
+ events = parse_events(sp)
340
+ if events: result['events'] = events; result['sections'].append('events')
341
+
342
+ pred = sp.select_one('.prediction-card')
343
+ if pred:
344
+ pred_data = {}
345
+ team_info = pred.select_one('.team-info')
346
+ if team_info:
347
+ teams = team_info.select('.team')
348
+ if len(teams) >= 2:
349
+ pred_data['home_name'] = _cl(teams[0].select_one('.team-name').get_text()) if teams[0].select_one('.team-name') else ''
350
+ pred_data['away_name'] = _cl(teams[1].select_one('.team-name').get_text()) if teams[1].select_one('.team-name') else ''
351
+ divider = team_info.select_one('.divider')
352
+ if divider: pred_data['result'] = _cl(divider.get_text())
353
+ vote_count = pred.select_one('.vote-count')
354
+ if vote_count: pred_data['vote_count'] = _cl(vote_count.get_text())
355
+ result['prediction'] = pred_data
356
+
357
+ try:
358
+ ar = requests.get(
359
+ f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}",
360
+ headers=API_HEADERS, timeout=6
361
+ )
362
+ if ar.status_code == 200:
363
+ ad = ar.json()
364
+ if ad.get('status') == 'success' and ad.get('html'):
365
+ asp = _mk(ad['html'])
366
+ ast = {}
367
+ for row in asp.select('li, tr, .stat-row'):
368
+ cells = row.select('td, span, p')
369
+ if len(cells) >= 3:
370
+ lb = _cl(cells[0].get_text())
371
+ if lb: ast[lb] = {'home': _cl(cells[1].get_text()), 'away': _cl(cells[2].get_text())}
372
+ if ast: result['h2h_stats_parsed'] = ast; result['sections'].append('h2h_stats')
373
+ except Exception:
374
+ pass
375
+
376
+ h2h_data = []
377
+ h2h_el = sp.select_one('.h2h-standings')
378
+ if h2h_el:
379
+ rows = h2h_el.select('.ranking-table tbody tr, .leaderboard tr')
380
+ for row in rows:
381
+ cells = row.select('td')
382
+ if len(cells) >= 4:
383
+ logo = row.select_one('img')
384
+ name_el = row.select_one('.team-name, p.link, .name')
385
+ h2h_data.append({
386
+ 'pos': _cl(cells[0].get_text()),
387
+ 'logo': logo.get('src', '') if logo else '',
388
+ 'name': _cl(name_el.get_text()) if name_el else '',
389
+ 'played': _cl(cells[1].get_text()) if len(cells) > 1 else '',
390
+ 'wins': _cl(cells[2].get_text()) if len(cells) > 2 else '',
391
+ 'draws': _cl(cells[3].get_text()) if len(cells) > 3 else '',
392
+ 'losses': _cl(cells[4].get_text()) if len(cells) > 4 else '',
393
+ 'gf': _cl(cells[5].get_text()) if len(cells) > 5 else '',
394
+ 'ga': _cl(cells[6].get_text()) if len(cells) > 6 else '',
395
+ 'points': _cl(cells[8].get_text()) if len(cells) > 8 else '',
396
+ })
397
+ if h2h_data: result['h2h_standings'] = h2h_data; result['sections'].append('h2h_standings')
398
+
399
+ recent_matches = []
400
+ matches_list = sp.select_one('.matches-list')
401
+ if matches_list:
402
+ for item in matches_list.select('.match-detail, .match-item, li'):
403
+ date_el = item.select_one('.date, .time, .match-time')
404
+ league_el = item.select_one('.league')
405
+ home_el = item.select_one('.home, .team-home')
406
+ away_el = item.select_one('.away, .team-away')
407
+ score_el = item.select_one('.score, .result')
408
+ if home_el or away_el:
409
+ recent_matches.append({
410
+ 'date': _cl(date_el.get_text()) if date_el else '',
411
+ 'league': _cl(league_el.get_text()) if league_el else '',
412
+ 'home': _cl(home_el.get_text()) if home_el else '',
413
+ 'away': _cl(away_el.get_text()) if away_el else '',
414
+ 'score': _cl(score_el.get_text()) if score_el else 'vs',
415
+ })
416
+ if recent_matches: result['recent_matches'] = recent_matches; result['sections'].append('recent')
417
+
418
+ return result