bep40 commited on
Commit
9e9ccf4
·
verified ·
1 Parent(s): 74261c5

Upload match_detail.py

Browse files
Files changed (1) hide show
  1. match_detail.py +256 -117
match_detail.py CHANGED
@@ -1,21 +1,23 @@
1
  """
2
  Match Detail Scraper for bongda.com.vn
3
- Fetches: preview/commentaries, lineups, head-to-head, stats for any match
 
 
 
 
 
 
 
4
  """
5
  import requests, re, json, time, threading
6
  from bs4 import BeautifulSoup
7
 
8
  BONGDA_HEADERS = {
9
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
10
- "Accept": "application/json, text/javascript, */*; q=0.01",
11
- "Accept-Language": "vi-VN,vi;q=0.9",
12
- "Referer": "https://bongda.com.vn/lich-thi-dau",
13
- "X-Requested-With": "XMLHttpRequest"
14
- }
15
-
16
- UA = {
17
  "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",
18
- "Accept-Language": "vi-VN,vi;q=0.9"
 
 
 
19
  }
20
 
21
  _match_cache = {}
@@ -36,19 +38,21 @@ def _set_cache(key, data):
36
 
37
 
38
  def _bongda_api(endpoint, params=None):
39
- """Call bongda.com.vn API and return HTML snippet."""
40
  try:
41
  url = f"https://bongda.com.vn{endpoint}"
42
  if params:
43
  url += "?" + "&".join(f"{k}={v}" for k, v in params.items())
44
- r = requests.get(url, headers=BONGDA_HEADERS, timeout=10)
45
  if r.status_code == 200:
46
- data = r.json()
47
- if data.get("status") == "success":
48
- return data.get("html", "")
 
 
49
  except:
50
  pass
51
- return ""
52
 
53
 
54
  def _clean(s):
@@ -70,10 +74,11 @@ def fetch_match_detail_by_url(url):
70
  def fetch_match_detail(event_id):
71
  """
72
  Fetch complete match detail:
73
- - commentaries/preview
74
  - lineups
75
- - head-to-head
76
- - match stats
 
77
  """
78
  cache_key = f"match_detail_{event_id}"
79
  cached = _cached(cache_key)
@@ -82,124 +87,258 @@ def fetch_match_detail(event_id):
82
 
83
  result = {"event_id": event_id, "found": True}
84
 
85
- # 1. Commentaries / Preview
86
- commentaries_html = _bongda_api("/api/fixtures/commentaries", {"event_id": event_id})
87
- result["commentaries_html"] = commentaries_html
88
-
89
- # Parse preview text
90
- if commentaries_html:
91
- try:
92
- soup = BeautifulSoup(commentaries_html, 'lxml')
93
- preview_text = []
94
- for el in soup.find_all(['p', 'div', 'span', 'h2', 'h3', 'li']):
95
- txt = el.get_text(strip=True)
96
- if txt and len(txt) > 15:
97
- preview_text.append(txt)
98
- result["preview_text"] = preview_text
99
- except:
100
- result["preview_text"] = []
101
  else:
102
- result["preview_text"] = []
103
-
104
- # 2. Lineups
105
- lineups_html = _bongda_api("/api/fixtures/lineups", {"event_id": event_id})
106
- result["lineups_html"] = lineups_html
107
- if lineups_html:
108
- result["lineups"] = _parse_lineups(lineups_html)
109
- else:
110
- result["lineups"] = {}
111
-
112
- # 3. Head-to-Head
113
- h2h_html = _bongda_api("/api/fixtures/head-to-head", {"event_id": event_id})
114
- result["h2h_html"] = h2h_html
115
- if h2h_html:
116
- result["h2h"] = _parse_h2h(h2h_html)
117
  else:
 
118
  result["h2h"] = []
119
 
120
- # 4. Match Stats
121
- stats_html = _bongda_api("/api/event-standing/player-performance", {"event_id": event_id})
122
- result["stats_html"] = stats_html
123
-
124
- # 5. Match basic info
125
- result["info"] = _parse_match_info(commentaries_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
  _set_cache(cache_key, result)
128
  return result
129
 
130
 
131
- def _parse_lineups(html):
132
- """Parse lineups HTML into structured data."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  try:
134
  soup = BeautifulSoup(html, 'lxml')
135
- lineups = {"home_team": "", "away_team": "", "home_formation": "", "away_formation": "",
136
- "home_players": [], "away_players": [], "home_subs": [], "away_subs": []}
137
-
138
- team_headers = soup.select('.team-name, .team-header, [class*=team] h3, [class*=team] h4')
139
- if len(team_headers) >= 2:
140
- lineups["home_team"] = _clean(team_headers[0].get_text())
141
- lineups["away_team"] = _clean(team_headers[1].get_text())
142
-
143
- formations = soup.select('.formation, [class*=formation]')
144
- if len(formations) >= 2:
145
- lineups["home_formation"] = _clean(formations[0].get_text())
146
- lineups["away_formation"] = _clean(formations[1].get_text())
147
-
148
- home_section = soup.select_one('.home-team, .team-home, [class*=home]')
149
- away_section = soup.select_one('.away-team, .team-away, [class*=away]')
150
-
151
- if home_section:
152
- for player_el in home_section.select('.player, .player-item, .player-name, tr, li'):
153
- name = _clean(player_el.get_text())
154
- if name and 2 < len(name) < 60:
155
- lineups["home_players"].append(name)
156
-
157
- if away_section:
158
- for player_el in away_section.select('.player, .player-item, .player-name, tr, li'):
159
- name = _clean(player_el.get_text())
160
- if name and 2 < len(name) < 60:
161
- lineups["away_players"].append(name)
162
-
163
- lineups["home_players"] = list(dict.fromkeys(lineups["home_players"]))
164
- lineups["away_players"] = list(dict.fromkeys(lineups["away_players"]))
165
- return lineups
166
  except:
167
- return {"home_team": "", "away_team": "", "home_players": [], "away_players": [], "home_subs": [], "away_subs": []}
 
168
 
169
 
170
- def _parse_h2h(html):
171
- """Parse head-to-head HTML into list of previous matches."""
 
172
  try:
173
  soup = BeautifulSoup(html, 'lxml')
174
- matches = []
175
- for row in soup.select('tr, .match-item, [class*=match], li'):
176
- cells = row.find_all(['td', 'div', 'span'])
177
- if len(cells) >= 3:
178
- date = _clean(cells[0].get_text())
179
- home = _clean(cells[1].get_text())
180
- away = _clean(cells[2].get_text()) if len(cells) > 2 else ""
181
- score = _clean(cells[3].get_text()) if len(cells) > 3 else ""
182
- if date and home and len(date) > 3:
183
- matches.append({"date": date, "home": home, "away": away, "score": score})
184
- return matches
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  except:
186
- return []
 
187
 
188
 
189
- def _parse_match_info(html):
190
- """Parse basic match info from commentaries HTML."""
191
- info = {}
192
  try:
193
  soup = BeautifulSoup(html, 'lxml')
194
- text = soup.get_text(' ')
195
- date_m = re.search(r'(\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2})', text)
196
- if date_m: info["date"] = date_m.group(1)
197
- time_m = re.search(r'(\d{1,2}:\d{2})', text)
198
- if time_m: info["time"] = time_m.group(1)
199
- stadium_m = re.search(r'(Sân vận động|SVĐ|Stadium|Nhà thi đấu)[:\s]+([^,\n]+)', text, re.I)
200
- if stadium_m: info["stadium"] = _clean(stadium_m.group(2))
201
- referee_m = re.search(r'(Trọng tài|Referee)[:\s]+([^,\n]+)', text, re.I)
202
- if referee_m: info["referee"] = _clean(referee_m.group(2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  except:
204
  pass
205
- return info
 
1
  """
2
  Match Detail Scraper for bongda.com.vn
3
+ Fetches: pre-match info, lineups, head-to-head, form, stats for any match
4
+
5
+ API endpoints (discovered from page source):
6
+ - /api/event-standing/pre-match?event_id={id} → preview/match info
7
+ - /api/fixtures/form?team_id={id} → recent form for a team
8
+ - /api/fixtures/h2h-match?event_id={id} → H2H match list
9
+ - /api/fixtures/h2h-stats?event_id={id} → H2H stats comparison
10
+ - /api/event-standing/player-performance?event_id → player performance stats
11
  """
12
  import requests, re, json, time, threading
13
  from bs4 import BeautifulSoup
14
 
15
  BONGDA_HEADERS = {
 
 
 
 
 
 
 
 
16
  "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",
17
+ "Accept": "application/json, text/javascript, */*; q=0.01",
18
+ "Accept-Language": "vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7",
19
+ "Referer": "https://bongda.com.vn/",
20
+ "X-Requested-With": "XMLHttpRequest",
21
  }
22
 
23
  _match_cache = {}
 
38
 
39
 
40
  def _bongda_api(endpoint, params=None):
41
+ """Call bongda.com.vn API and return parsed response."""
42
  try:
43
  url = f"https://bongda.com.vn{endpoint}"
44
  if params:
45
  url += "?" + "&".join(f"{k}={v}" for k, v in params.items())
46
+ r = requests.get(url, headers=BONGDA_HEADERS, timeout=15)
47
  if r.status_code == 200:
48
+ try:
49
+ data = r.json()
50
+ return data
51
+ except:
52
+ pass
53
  except:
54
  pass
55
+ return None
56
 
57
 
58
  def _clean(s):
 
74
  def fetch_match_detail(event_id):
75
  """
76
  Fetch complete match detail:
77
+ - pre-match info (teams, league, date, stadium, referee)
78
  - lineups
79
+ - head-to-head matches & stats
80
+ - team form
81
+ - player performance stats
82
  """
83
  cache_key = f"match_detail_{event_id}"
84
  cached = _cached(cache_key)
 
87
 
88
  result = {"event_id": event_id, "found": True}
89
 
90
+ # 1. Pre-match info (teams, league, date, stadium, referee)
91
+ pre_match = _bongda_api("/api/event-standing/pre-match", {"event_id": event_id})
92
+ result["pre_match"] = pre_match
93
+ if pre_match and pre_match.get("status") == "success":
94
+ result["pre_match_html"] = pre_match.get("html", "")
95
+ result["info"] = _parse_pre_match(pre_match.get("html", ""))
 
 
 
 
 
 
 
 
 
 
96
  else:
97
+ result["pre_match_html"] = ""
98
+ result["info"] = {}
99
+
100
+ # 2. H2H matches
101
+ h2h_match = _bongda_api("/api/fixtures/h2h-match", {"event_id": event_id})
102
+ result["h2h_match"] = h2h_match
103
+ if h2h_match and h2h_match.get("status") == "success":
104
+ result["h2h_html"] = h2h_match.get("html", "")
105
+ result["h2h"] = _parse_h2h_matches(h2h_match.get("html", ""))
 
 
 
 
 
 
106
  else:
107
+ result["h2h_html"] = ""
108
  result["h2h"] = []
109
 
110
+ # 3. H2H stats comparison
111
+ h2h_stats = _bongda_api("/api/fixtures/h2h-stats", {"event_id": event_id})
112
+ result["h2h_stats"] = h2h_stats
113
+ if h2h_stats and h2h_stats.get("status") == "success":
114
+ result["h2h_stats_html"] = h2h_stats.get("html", "")
115
+ result["h2h_stats_parsed"] = _parse_h2h_stats(h2h_stats.get("html", ""))
116
+ else:
117
+ result["h2h_stats_html"] = ""
118
+ result["h2h_stats_parsed"] = {}
119
+
120
+ # 4. Player performance stats
121
+ perf = _bongda_api("/api/event-standing/player-performance", {"event_id": event_id})
122
+ result["performance"] = perf
123
+ if perf and perf.get("status") == "success":
124
+ result["stats_html"] = perf.get("html", "")
125
+ else:
126
+ result["stats_html"] = ""
127
+
128
+ # 5. Team form (need team IDs from pre-match info)
129
+ home_team_id = None
130
+ away_team_id = None
131
+ if result["info"].get("home_team_id"):
132
+ home_team_id = result["info"]["home_team_id"]
133
+ if result["info"].get("away_team_id"):
134
+ away_team_id = result["info"]["away_team_id"]
135
+
136
+ # Try to extract team IDs from pre-match HTML if not in info
137
+ if not home_team_id or not away_team_id:
138
+ pm_html = result.get("pre_match_html", "")
139
+ if pm_html:
140
+ ids = re.findall(r'/doi-bong/(\d+)/overview/', pm_html)
141
+ if len(ids) >= 2:
142
+ home_team_id = home_team_id or ids[0]
143
+ away_team_id = away_team_id or ids[1]
144
+
145
+ result["home_form"] = []
146
+ result["away_form"] = []
147
+ if home_team_id:
148
+ form_data = _bongda_api("/api/fixtures/form", {"team_id": home_team_id})
149
+ if form_data and form_data.get("status") == "success":
150
+ result["home_form"] = _parse_form(form_data.get("html", ""))
151
+ result["home_form_html"] = form_data.get("html", "")
152
+ if away_team_id:
153
+ form_data = _bongda_api("/api/fixtures/form", {"team_id": away_team_id})
154
+ if form_data and form_data.get("status") == "success":
155
+ result["away_form"] = _parse_form(form_data.get("html", ""))
156
+ result["away_form_html"] = form_data.get("html", "")
157
 
158
  _set_cache(cache_key, result)
159
  return result
160
 
161
 
162
+ def _parse_pre_match(html):
163
+ """Parse pre-match HTML into structured info."""
164
+ info = {}
165
+ try:
166
+ soup = BeautifulSoup(html, 'lxml')
167
+ text = soup.get_text(' ')
168
+
169
+ # Extract date
170
+ date_m = re.search(r'(\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2})', text)
171
+ if date_m:
172
+ info["date"] = date_m.group(1)
173
+
174
+ # Extract time
175
+ time_m = re.search(r'(\d{1,2}:\d{2})', text)
176
+ if time_m:
177
+ info["time"] = time_m.group(1)
178
+
179
+ # Extract stadium
180
+ stadium_m = re.search(r'(Sân vận động|SVĐ|Stadium|Nhà thi đấu)[:\s]+([^,\n]+)', text, re.I)
181
+ if stadium_m:
182
+ info["stadium"] = _clean(stadium_m.group(2))
183
+
184
+ # Extract referee
185
+ referee_m = re.search(r'(Trọng tài|Referee)[:\s]+([^,\n]+)', text, re.I)
186
+ if referee_m:
187
+ info["referee"] = _clean(referee_m.group(2))
188
+
189
+ # Extract team IDs from links
190
+ team_links = soup.select('a[href*="/doi-bong/"]')
191
+ for link in team_links:
192
+ href = link.get('href', '')
193
+ m = re.search(r'/doi-bong/(\d+)/', href)
194
+ if m:
195
+ team_id = m.group(1)
196
+ team_name = _clean(link.get_text())
197
+ if team_name:
198
+ if not info.get("home_team_id"):
199
+ info["home_team_id"] = team_id
200
+ info["home_team"] = team_name
201
+ elif not info.get("away_team_id"):
202
+ info["away_team_id"] = team_id
203
+ info["away_team"] = team_name
204
+
205
+ # Extract league/tournament
206
+ league_link = soup.select_one('a[href*="/giai-dau/"]')
207
+ if league_link:
208
+ info["league"] = _clean(league_link.get_text())
209
+
210
+ # Extract team logos
211
+ logos = soup.select('img[src*="team-logo"]')
212
+ if len(logos) >= 1:
213
+ info["home_logo"] = logos[0].get('src', '')
214
+ if len(logos) >= 2:
215
+ info["away_logo"] = logos[1].get('src', '')
216
+
217
+ except:
218
+ pass
219
+ return info
220
+
221
+
222
+ def _parse_h2h_matches(html):
223
+ """Parse H2H matches HTML into list of previous matches."""
224
+ matches = []
225
  try:
226
  soup = BeautifulSoup(html, 'lxml')
227
+ for item in soup.select('li.match-detail, .match-detail'):
228
+ match = {}
229
+ # Date and league
230
+ dt_el = item.select_one('.datetime')
231
+ if dt_el:
232
+ time_el = dt_el.select_one('.match-time, p.match-time')
233
+ if time_el:
234
+ match["date"] = _clean(time_el.get_text())
235
+ league_el = dt_el.select_one('.league, a.league')
236
+ if league_el:
237
+ match["league"] = _clean(league_el.get_text())
238
+
239
+ # Teams
240
+ home_el = item.select_one('.home-team .name, .team.home-team .name')
241
+ away_el = item.select_one('.away-team .name, .team.away-team .name')
242
+ if home_el:
243
+ match["home"] = _clean(home_el.get_text())
244
+ if away_el:
245
+ match["away"] = _clean(away_el.get_text())
246
+
247
+ # Score
248
+ status_el = item.select_one('.status a, .status')
249
+ if status_el:
250
+ score_text = _clean(status_el.get_text())
251
+ match["score"] = score_text
252
+
253
+ if match.get("home") and match.get("away"):
254
+ matches.append(match)
 
 
 
255
  except:
256
+ pass
257
+ return matches
258
 
259
 
260
+ def _parse_h2h_stats(html):
261
+ """Parse H2H stats comparison HTML."""
262
+ stats = {}
263
  try:
264
  soup = BeautifulSoup(html, 'lxml')
265
+ for row in soup.select('.stat-row, li.stat-row'):
266
+ label_el = row.select_one('.stat-label span, .stat-label')
267
+ if not label_el:
268
+ continue
269
+ label = _clean(label_el.get_text())
270
+
271
+ home_bar = row.select_one('.team-progress.home .progress-bar, .home .progress-bar')
272
+ away_bar = row.select_one('.team-progress.away .progress-bar, .away .progress-bar')
273
+
274
+ home_val = "0"
275
+ away_val = "0"
276
+
277
+ if home_bar:
278
+ pct = home_bar.get('style', '')
279
+ m = re.search(r'width:\s*(\d+)%', pct)
280
+ if m:
281
+ home_val = m.group(1)
282
+ span = home_bar.select_one('span')
283
+ if span:
284
+ home_val = _clean(span.get_text()) or home_val
285
+
286
+ if away_bar:
287
+ pct = away_bar.get('style', '')
288
+ m = re.search(r'width:\s*(\d+)%', pct)
289
+ if m:
290
+ away_val = m.group(1)
291
+ span = away_bar.select_one('span')
292
+ if span:
293
+ away_val = _clean(span.get_text()) or away_val
294
+
295
+ stats[label] = {"home": home_val, "away": away_val}
296
  except:
297
+ pass
298
+ return stats
299
 
300
 
301
+ def _parse_form(html):
302
+ """Parse team form HTML into list of recent matches."""
303
+ matches = []
304
  try:
305
  soup = BeautifulSoup(html, 'lxml')
306
+ for item in soup.select('li.match-detail, .match-detail'):
307
+ match = {}
308
+ dt_el = item.select_one('.datetime')
309
+ if dt_el:
310
+ time_el = dt_el.select_one('.match-time, p.match-time')
311
+ if time_el:
312
+ match["date"] = _clean(time_el.get_text())
313
+ league_el = dt_el.select_one('.league, a.league')
314
+ if league_el:
315
+ match["league"] = _clean(league_el.get_text())
316
+
317
+ home_el = item.select_one('.home-team .name, .team.home-team .name')
318
+ away_el = item.select_one('.away-team .name, .team.away-team .name')
319
+ if home_el:
320
+ match["home"] = _clean(home_el.get_text())
321
+ if away_el:
322
+ match["away"] = _clean(away_el.get_text())
323
+
324
+ status_el = item.select_one('.status a, .status')
325
+ if status_el:
326
+ match["score"] = _clean(status_el.get_text())
327
+
328
+ # Determine result (T=Thắng, B=Bại, H=Hòa)
329
+ label_el = item.select_one('.label_form')
330
+ if label_el:
331
+ label_text = _clean(label_el.get_text())
332
+ css_class = ' '.join(label_el.get('class', []))
333
+ if 'bg-green' in css_class or label_text == 'T':
334
+ match["result"] = "T"
335
+ elif 'bg-red' in css_class or label_text == 'B':
336
+ match["result"] = "B"
337
+ else:
338
+ match["result"] = "H"
339
+
340
+ if match.get("home") and match.get("away"):
341
+ matches.append(match)
342
  except:
343
  pass
344
+ return matches