bep40 commited on
Commit
0f3ffd0
·
verified ·
1 Parent(s): e312224

Debug: add /api/wc2026/debug endpoint to check what tienphong.vn returns"

Browse files
Files changed (1) hide show
  1. wc2026_scraper.py +173 -72
wc2026_scraper.py CHANGED
@@ -1,8 +1,8 @@
1
  """
2
  World Cup 2026 Data Module
3
- - Lịch thi đấu: Parse từ tienphong.vn → trả JSON structured data → frontend render CSS layout giống ảnh
4
  - BXH: bongda.com.vn API tournament_id=24254
5
- - News: VnExpress + TT&VH + Thanh Niên
6
  """
7
  import requests, re, time, threading
8
  from bs4 import BeautifulSoup
@@ -45,122 +45,223 @@ def _fetch(url, timeout=15):
45
  return r.text if r.status_code == 200 else ''
46
  except: return ''
47
 
48
- # ==================== LỊCH THI ĐẤU (parse structured JSON from tienphong.vn) ====================
49
  def scrape_fixtures():
50
- """Parse lịch thi đấu WC2026 từ tienphong.vn thành structured JSON."""
51
  c = _cached('wc_fix', 3600)
52
  if c is not None: return c
53
 
54
- groups = [] # [{group: "Bảng A", matches: [{date, time, home, away, venue, score}]}]
 
 
 
 
 
 
 
55
 
56
- page = _fetch('https://tienphong.vn/lich-thi-dau-world-cup-2026-moi-nhat-theo-gio-viet-nam-post1837627.tpo')
57
  if page:
58
  soup = BeautifulSoup(page, 'lxml')
59
- content = soup.select_one('.article__body, .cms-body, article, .content-detail, .article-body')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  if content:
61
- # Parse tables in the article
62
- current_group = ''
63
- for el in content.find_all(['table', 'h2', 'h3', 'h4', 'p', 'strong', 'b']):
64
- # Detect group/round headers
65
- if el.name in ('h2', 'h3', 'h4', 'p', 'strong', 'b'):
 
 
 
 
 
66
  text = _clean(el.get_text())
67
- if re.search(r'bảng\s+[A-L]|group\s+[A-L]|vòng\s+\d|vòng\s+bảng|vòng\s+1/16|tứ kết|bán kết|chung kết|knock.?out', text, re.I):
68
- current_group = text[:50]
69
- elif el.name == 'table':
 
 
 
70
  rows = el.select('tr')
71
  if len(rows) < 2: continue
72
  group_matches = []
73
- for tr in rows[1:]: # Skip header
74
  tds = tr.select('td')
75
- if len(tds) < 3: continue
76
  cells = [_clean(td.get_text()) for td in tds]
77
- # Typical format: Ngày | Giờ | Đội 1 | Tỉ số | Đội 2 | Sân
78
- # Or: Trận | Ngày | Giờ | Đội 1 vs Đội 2 | Sân
79
- match = _parse_match_row(cells)
80
  if match:
81
  group_matches.append(match)
82
  if group_matches:
83
- groups.append({'group': current_group or f'Vòng bảng', 'matches': group_matches})
84
- current_group = ''
85
-
86
- # Fallback: parse from text patterns if no tables
87
  if not groups:
88
- text = content.get_text('\n')
89
- current_group = ''
 
90
  current_matches = []
91
- for line in text.split('\n'):
 
92
  line = _clean(line)
93
- if not line: continue
 
94
  # Group header
95
- if re.search(r'bảng\s+[A-L]|group\s+[A-L]|vòng\s+\d|tứ kết|bán kết|chung kết', line, re.I):
96
- if current_matches and current_group:
97
  groups.append({'group': current_group, 'matches': current_matches})
98
  current_matches = []
99
  current_group = line[:50]
100
  continue
101
- # Match pattern: "HH:MM ngày DD/MM: Team1 vs Team2 (Sân)"
102
- m = re.search(r'(\d{1,2}[h:]\d{2}).*?(\d{1,2}/\d{1,2}).*?[:\-–]\s*(.+?)\s+(?:vs|VS|–|-|v)\s+(.+?)(?:\s*\((.+?)\))?$', line)
103
- if m:
104
- current_matches.append({
105
- 'time': m.group(1), 'date': m.group(2),
106
- 'home': _clean(m.group(3)), 'away': _clean(m.group(4)),
107
- 'venue': _clean(m.group(5)) if m.group(5) else '', 'score': ''
108
- })
109
- continue
110
- # Simpler pattern: "Team1 vs Team2"
111
- m2 = re.search(r'^(.{2,25})\s+(?:vs|VS|–)\s+(.{2,25})', line)
112
- if m2:
113
- tm = re.search(r'(\d{1,2}[h:]\d{2})', line)
114
- dt = re.search(r'(\d{1,2}/\d{1,2})', line)
115
- current_matches.append({
116
- 'time': tm.group(1) if tm else '', 'date': dt.group(1) if dt else '',
117
- 'home': _clean(m2.group(1)), 'away': _clean(m2.group(2)),
118
- 'venue': '', 'score': ''
119
- })
120
  if current_matches:
121
- groups.append({'group': current_group or 'Lịch thi đấu', 'matches': current_matches})
122
-
123
- r = {'groups': groups, 'total_matches': sum(len(g['matches']) for g in groups)}
 
 
 
 
124
  _set('wc_fix', r)
125
  return r
126
 
127
- def _parse_match_row(cells):
128
- """Parse a table row into a match dict."""
129
- if len(cells) < 3: return None
130
-
131
  date = ''; time_str = ''; home = ''; away = ''; venue = ''; score = ''
132
 
133
  for c in cells:
134
- # Date pattern
135
  if re.match(r'^\d{1,2}/\d{1,2}(/\d{2,4})?$', c) and not date:
136
  date = c; continue
137
- # Time pattern
138
  if re.match(r'^\d{1,2}[h:]\d{2}$', c) and not time_str:
139
  time_str = c; continue
140
- # Score pattern
141
  if re.match(r'^\d+\s*[-–]\s*\d+$', c):
142
  score = c; continue
143
- # "vs" or dash separator
144
- if c.lower() in ('vs', '-', '–', 'v'): continue
145
- # Venue (usually longer, contains stadium words)
146
- if any(w in c.lower() for w in ['stadium', 'sân', 'arena', 'park', 'field']) and not venue:
 
 
147
  venue = c; continue
148
- # Team names
149
- if len(c) > 1 and not c.isdigit():
150
  if not home: home = c
151
  elif not away: away = c
152
 
153
- # Handle "Team1 vs Team2" in single cell
154
- if home and not away:
155
- m = re.search(r'^(.+?)\s+(?:vs|VS|–|-|v)\s+(.+?)$', home)
156
- if m:
157
- home = _clean(m.group(1))
158
- away = _clean(m.group(2))
159
-
160
  if home and away:
161
  return {'date': date, 'time': time_str, 'home': home, 'away': away, 'venue': venue, 'score': score}
162
  return None
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  # ==================== BXH ====================
165
  def scrape_standings():
166
  c = _cached('wc_bxh', 180)
 
1
  """
2
  World Cup 2026 Data Module
3
+ - Lịch thi đấu: Parse từ tienphong.vn → JSON → frontend render CSS
4
  - BXH: bongda.com.vn API tournament_id=24254
5
+ - News: Thanh Niên + TT&VH + VnExpress
6
  """
7
  import requests, re, time, threading
8
  from bs4 import BeautifulSoup
 
45
  return r.text if r.status_code == 200 else ''
46
  except: return ''
47
 
48
+ # ==================== LỊCH THI ĐẤU ====================
49
  def scrape_fixtures():
50
+ """Parse lịch thi đấu WC2026 từ tienphong.vn."""
51
  c = _cached('wc_fix', 3600)
52
  if c is not None: return c
53
 
54
+ groups = []
55
+ debug_info = {'url': '', 'html_len': 0, 'tables_found': 0, 'text_matches': 0}
56
+
57
+ # Try tienphong.vn
58
+ url = 'https://tienphong.vn/lich-thi-dau-world-cup-2026-moi-nhat-theo-gio-viet-nam-post1837627.tpo'
59
+ debug_info['url'] = url
60
+ page = _fetch(url)
61
+ debug_info['html_len'] = len(page) if page else 0
62
 
 
63
  if page:
64
  soup = BeautifulSoup(page, 'lxml')
65
+ # Remove junk
66
+ for s in soup.select('script, style, nav, footer, .ads, .banner, .social-share, .related'):
67
+ s.decompose()
68
+
69
+ # Find article content
70
+ content = (
71
+ soup.select_one('.article__body') or
72
+ soup.select_one('.cms-body') or
73
+ soup.select_one('.article-content') or
74
+ soup.select_one('article') or
75
+ soup.select_one('.content-detail') or
76
+ soup.select_one('#main-detail') or
77
+ soup.select_one('.main-content')
78
+ )
79
+
80
  if content:
81
+ # Method 1: Parse tables
82
+ tables = content.select('table')
83
+ debug_info['tables_found'] = len(tables)
84
+
85
+ current_group = 'Vòng bảng'
86
+ for el in content.descendants:
87
+ if not hasattr(el, 'name') or not el.name: continue
88
+
89
+ # Detect group headers from bold/strong/h tags
90
+ if el.name in ('h2', 'h3', 'h4', 'strong', 'b', 'p'):
91
  text = _clean(el.get_text())
92
+ if re.search(r'bảng\s+[A-L]|group\s+[A-L]|vòng\s+1/16|vòng\s+16|tứ kết|bán kết|chung kết|vòng\s+bảng|ngày\s+\d', text, re.I):
93
+ if len(text) < 60:
94
+ current_group = text
95
+
96
+ # Parse table
97
+ if el.name == 'table':
98
  rows = el.select('tr')
99
  if len(rows) < 2: continue
100
  group_matches = []
101
+ for tr in rows:
102
  tds = tr.select('td')
103
+ if len(tds) < 2: continue
104
  cells = [_clean(td.get_text()) for td in tds]
105
+ match = _try_parse_cells(cells)
 
 
106
  if match:
107
  group_matches.append(match)
108
  if group_matches:
109
+ groups.append({'group': current_group, 'matches': group_matches})
110
+
111
+ # Method 2: Parse text if no tables found
 
112
  if not groups:
113
+ text_content = content.get_text('\n')
114
+ lines = text_content.split('\n')
115
+ current_group = 'Lịch thi đấu'
116
  current_matches = []
117
+
118
+ for line in lines:
119
  line = _clean(line)
120
+ if not line or len(line) < 5: continue
121
+
122
  # Group header
123
+ if re.search(r'bảng\s+[A-L]|group\s+[A-L]|vòng\s+1/16|tứ kết|bán kết|chung kết', line, re.I):
124
+ if current_matches:
125
  groups.append({'group': current_group, 'matches': current_matches})
126
  current_matches = []
127
  current_group = line[:50]
128
  continue
129
+
130
+ # Try parse match from line
131
+ match = _try_parse_line(line)
132
+ if match:
133
+ current_matches.append(match)
134
+ debug_info['text_matches'] += 1
135
+
 
 
 
 
 
 
 
 
 
 
 
 
136
  if current_matches:
137
+ groups.append({'group': current_group, 'matches': current_matches})
138
+
139
+ # Fallback: try thethao247.vn
140
+ if not groups:
141
+ groups = _try_thethao247()
142
+
143
+ r = {'groups': groups, 'total_matches': sum(len(g['matches']) for g in groups), 'debug': debug_info}
144
  _set('wc_fix', r)
145
  return r
146
 
147
+ def _try_parse_cells(cells):
148
+ """Try to parse table cells into a match."""
149
+ if len(cells) < 2: return None
 
150
  date = ''; time_str = ''; home = ''; away = ''; venue = ''; score = ''
151
 
152
  for c in cells:
153
+ if not c: continue
154
  if re.match(r'^\d{1,2}/\d{1,2}(/\d{2,4})?$', c) and not date:
155
  date = c; continue
 
156
  if re.match(r'^\d{1,2}[h:]\d{2}$', c) and not time_str:
157
  time_str = c; continue
 
158
  if re.match(r'^\d+\s*[-–]\s*\d+$', c):
159
  score = c; continue
160
+ if c.lower() in ('vs', '-', '–', 'v', 'ft'): continue
161
+ # Check if cell contains "vs" pattern
162
+ vm = re.search(r'^(.+?)\s+(?:vs|VS|–|-)\s+(.+?)$', c)
163
+ if vm:
164
+ home = _clean(vm.group(1)); away = _clean(vm.group(2)); continue
165
+ if any(w in c.lower() for w in ['stadium', 'sân', 'arena', 'park', 'metlife', 'sofi', 'hard rock']):
166
  venue = c; continue
167
+ if len(c) > 2 and not c.isdigit() and len(c) < 30:
 
168
  if not home: home = c
169
  elif not away: away = c
170
 
 
 
 
 
 
 
 
171
  if home and away:
172
  return {'date': date, 'time': time_str, 'home': home, 'away': away, 'venue': venue, 'score': score}
173
  return None
174
 
175
+ def _try_parse_line(line):
176
+ """Try to parse a text line into a match."""
177
+ # Pattern: "21h00 ngày 12/6: Mexico vs Honduras (SoFi Stadium)"
178
+ m = re.search(r'(\d{1,2}[h:]\d{2})\s*(?:ngày\s*)?(\d{1,2}/\d{1,2}(?:/\d{2,4})?)?[:\s\-–]*(.+?)\s+(?:vs|VS|–|-|v\.?s\.?)\s+(.+?)(?:\s*[\(\[](.+?)[\)\]])?$', line)
179
+ if m:
180
+ return {
181
+ 'time': _clean(m.group(1)), 'date': _clean(m.group(2) or ''),
182
+ 'home': _clean(m.group(3)), 'away': _clean(m.group(4)),
183
+ 'venue': _clean(m.group(5) or ''), 'score': ''
184
+ }
185
+ # Pattern: "Mexico - Honduras 21h 12/6"
186
+ m2 = re.search(r'^([A-ZÀ-Ỹ][a-zà-ỹA-ZÀ-Ỹ\s\.]+?)\s+(?:vs|VS|–|-)\s+([A-ZÀ-Ỹ][a-zà-ỹA-ZÀ-Ỹ\s\.]+)', line)
187
+ if m2:
188
+ tm = re.search(r'(\d{1,2}[h:]\d{2})', line)
189
+ dt = re.search(r'(\d{1,2}/\d{1,2})', line)
190
+ return {
191
+ 'time': tm.group(1) if tm else '', 'date': dt.group(1) if dt else '',
192
+ 'home': _clean(m2.group(1)), 'away': _clean(m2.group(2)),
193
+ 'venue': '', 'score': ''
194
+ }
195
+ return None
196
+
197
+ def _try_thethao247():
198
+ """Fallback: parse from thethao247.vn"""
199
+ groups = []
200
+ page = _fetch('https://thethao247.vn/world-cup/426-lich-thi-dau-world-cup-2026-d399919.html')
201
+ if not page: return groups
202
+ soup = BeautifulSoup(page, 'lxml')
203
+ content = soup.select_one('article, .detail-content, .content-detail, main')
204
+ if not content: return groups
205
+
206
+ current_group = 'Lịch thi đấu'
207
+ current_matches = []
208
+ for el in content.find_all(['table', 'h2', 'h3', 'h4', 'strong']):
209
+ if el.name in ('h2', 'h3', 'h4', 'strong'):
210
+ text = _clean(el.get_text())
211
+ if re.search(r'bảng|group|vòng|tứ kết|bán kết|chung kết', text, re.I) and len(text) < 60:
212
+ if current_matches:
213
+ groups.append({'group': current_group, 'matches': current_matches})
214
+ current_matches = []
215
+ current_group = text
216
+ elif el.name == 'table':
217
+ for tr in el.select('tr')[1:]:
218
+ tds = tr.select('td')
219
+ if len(tds) >= 2:
220
+ cells = [_clean(td.get_text()) for td in tds]
221
+ match = _try_parse_cells(cells)
222
+ if match: current_matches.append(match)
223
+ if current_matches:
224
+ groups.append({'group': current_group, 'matches': current_matches})
225
+ return groups
226
+
227
+ def debug_fixtures():
228
+ """Debug endpoint to see raw data."""
229
+ page = _fetch('https://tienphong.vn/lich-thi-dau-world-cup-2026-moi-nhat-theo-gio-viet-nam-post1837627.tpo')
230
+ if not page:
231
+ return {'error': 'Cannot fetch tienphong.vn', 'page_len': 0}
232
+
233
+ soup = BeautifulSoup(page, 'lxml')
234
+ for s in soup.select('script, style'): s.decompose()
235
+
236
+ content = (
237
+ soup.select_one('.article__body') or soup.select_one('.cms-body') or
238
+ soup.select_one('article') or soup.select_one('.content-detail')
239
+ )
240
+
241
+ info = {
242
+ 'page_len': len(page),
243
+ 'content_found': bool(content),
244
+ 'content_selector': '',
245
+ 'tables': 0,
246
+ 'images': 0,
247
+ 'text_preview': '',
248
+ 'first_500_chars': ''
249
+ }
250
+
251
+ if content:
252
+ info['content_selector'] = content.name + '.' + '.'.join(content.get('class', []))
253
+ info['tables'] = len(content.select('table'))
254
+ info['images'] = len(content.select('img'))
255
+ info['text_preview'] = content.get_text()[:2000]
256
+ info['first_500_chars'] = str(content)[:500]
257
+ else:
258
+ # Try body
259
+ body = soup.find('body')
260
+ if body:
261
+ info['text_preview'] = body.get_text()[:2000]
262
+
263
+ return info
264
+
265
  # ==================== BXH ====================
266
  def scrape_standings():
267
  c = _cached('wc_bxh', 180)