支持进行中比赛的实时比分更新
- 使用 BeautifulSoup 正确解析 HTML 结构,提取队名、比分等信息 - 区分系列赛进行中(如 Bo3 1-0)和已完成状态 - 新增 update_event_with_score 方法处理进行中比赛的比分更新 - 扩展处理时间范围到最近 12 小时,确保捕获进行中的比赛 - 修复了 Aurora vs YkBros 等比赛比分无法识别的问题 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d3b872cd86
commit
dd5ba77e1e
32
CHANGELOG.md
32
CHANGELOG.md
@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## v3.3 - 支持进行中比赛的实时比分
|
||||
- **使用 BeautifulSoup 解析 HTML 结构**:
|
||||
- 重写了 `_parse_match` 方法,使用 DOM 结构而非正则表达式
|
||||
- 准确提取队名、比分、赛制和锦标赛信息
|
||||
- 解决了 "Aurora1:0(Bo3)YkBros" 格式的解析问题
|
||||
- **区分系列赛进行中和已完成状态**:
|
||||
- 新增 `has_score` 标记,表示比赛是否有比分
|
||||
- Bo3 需要 2 胜才标记为完成,Bo5 需要 3 胜
|
||||
- 进行中的比赛显示当前比分(如 1-0),但不显示✓标记
|
||||
- **新增进行中比分更新功能**:
|
||||
- 添加 `update_event_with_score` 方法处理进行中比赛
|
||||
- 在日历标题显示实时比分(如 "1-0 Team1 vs Team2")
|
||||
- 在描述中添加 "📊 CURRENT SCORE" 标记
|
||||
- **扩展处理时间范围**:
|
||||
- 处理最近 12 小时内的比赛,捕获正在进行的比赛
|
||||
- 确保进行中的比赛比分能及时同步
|
||||
|
||||
## v3.2.1 - 修复比分识别问题
|
||||
- **修复错误的比分解析**:
|
||||
- 修复了将日期时间误识别为比分的问题(如 "19-00")
|
||||
@ -47,13 +64,14 @@
|
||||
|
||||
## 功能对比
|
||||
|
||||
| 版本 | 同步比赛 | 更新结果 | 时间变更 | 新格式 | TBD优化 |
|
||||
|------|---------|---------|---------|--------|---------|
|
||||
| v1.0 | ✓ | ✗ | ✗ | ✗ | ✗ |
|
||||
| v2.0 | ✓ | ✓ | ✗ | ✗ | ✗ |
|
||||
| v3.0 | ✓ | ✓ | ✓ | ✗ | ✗ |
|
||||
| v3.1 | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||
| v3.2 | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| 版本 | 同步比赛 | 更新结果 | 时间变更 | 新格式 | TBD优化 | 实时比分 |
|
||||
|------|---------|---------|---------|--------|---------|----------|
|
||||
| v1.0 | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
||||
| v2.0 | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
|
||||
| v3.0 | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ |
|
||||
| v3.1 | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ |
|
||||
| v3.2 | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||
| v3.3 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
## 使用建议
|
||||
|
||||
|
||||
@ -94,7 +94,7 @@ class Dota2CalendarSync:
|
||||
return [], []
|
||||
|
||||
def _parse_match(self, parent, timestamp_elem):
|
||||
"""Parse match data from an element"""
|
||||
"""Parse match data from an element using HTML structure"""
|
||||
try:
|
||||
match_data = {}
|
||||
|
||||
@ -105,62 +105,108 @@ class Dota2CalendarSync:
|
||||
else:
|
||||
return None
|
||||
|
||||
# Extract team names from HTML structure
|
||||
team_blocks = parent.find_all('div', class_='block-team')
|
||||
if len(team_blocks) >= 2:
|
||||
# Get team names - prefer span.name over a tag (a tag might be empty icon link)
|
||||
team1_elem = team_blocks[0].find('span', class_='name')
|
||||
if not team1_elem or not team1_elem.get_text().strip():
|
||||
# Try finding any a tag with text
|
||||
for a_tag in team_blocks[0].find_all('a'):
|
||||
if a_tag.get_text().strip():
|
||||
team1_elem = a_tag
|
||||
break
|
||||
|
||||
team2_elem = team_blocks[1].find('span', class_='name')
|
||||
if not team2_elem or not team2_elem.get_text().strip():
|
||||
# Try finding any a tag with text
|
||||
for a_tag in team_blocks[1].find_all('a'):
|
||||
if a_tag.get_text().strip():
|
||||
team2_elem = a_tag
|
||||
break
|
||||
|
||||
if team1_elem and team2_elem:
|
||||
match_data['team1'] = self._clean_team_name(team1_elem.get_text().strip())
|
||||
match_data['team2'] = self._clean_team_name(team2_elem.get_text().strip())
|
||||
|
||||
# If team blocks not found, try fallback
|
||||
if 'team1' not in match_data:
|
||||
# Fallback to text parsing
|
||||
text = parent.get_text()
|
||||
# Look for "vs" pattern
|
||||
vs_match = re.search(r'([A-Za-z0-9\s\.\-_]+?)\s*vs\s*([A-Za-z0-9\s\.\-_]+)', text)
|
||||
if vs_match:
|
||||
team1_raw = vs_match.group(1).strip()
|
||||
team2_raw = vs_match.group(2).strip()
|
||||
# Clean up team names
|
||||
team1_raw = re.sub(r'^.*CEST?', '', team1_raw).strip()
|
||||
match_data['team1'] = self._clean_team_name(team1_raw)
|
||||
match_data['team2'] = self._clean_team_name(team2_raw)
|
||||
|
||||
# Check if it has a score (completed match)
|
||||
# Look for score patterns specifically in match result context
|
||||
# Scores are typically between team names, not part of timestamps
|
||||
score_match = None
|
||||
# Extract score from HTML structure
|
||||
has_score = False
|
||||
score_match = None
|
||||
|
||||
# First check if this looks like a completed match by looking for score indicators
|
||||
# Split the text to analyze structure better
|
||||
lines = text.split('\n')
|
||||
for line in lines:
|
||||
# Look for patterns like "Team1 2-1 Team2" or "Team1 2:1 Team2"
|
||||
# Score should be surrounded by team names or 'vs' context
|
||||
if 'vs' in line.lower():
|
||||
# Check for score pattern near 'vs'
|
||||
score_pattern = re.search(r'(?:^|\s)(\d{1,2})[-:](\d{1,2})(?:\s|$)', line)
|
||||
# Look for score in structured elements
|
||||
score_holder = parent.find('div', class_='match-info-header-scoreholder')
|
||||
if score_holder:
|
||||
score_elems = score_holder.find_all('span', class_='match-info-header-scoreholder-score')
|
||||
if len(score_elems) >= 2:
|
||||
try:
|
||||
score1 = int(score_elems[0].get_text().strip())
|
||||
score2 = int(score_elems[1].get_text().strip())
|
||||
if 0 <= score1 <= 5 and 0 <= score2 <= 5 and (score1 + score2) > 0:
|
||||
has_score = True
|
||||
match_data['score'] = f"{score1}-{score2}"
|
||||
score_match = True # Use as flag
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# If score not found in structure, try text pattern
|
||||
if not has_score:
|
||||
text = parent.get_text()
|
||||
score_pattern = re.search(r'(\d{1,2})[:|-](\d{1,2})', text)
|
||||
if score_pattern:
|
||||
score1 = int(score_pattern.group(1))
|
||||
score2 = int(score_pattern.group(2))
|
||||
# Validate it's a reasonable game score (typically 0-5 for Bo5, 0-3 for Bo3)
|
||||
# Validate it's a reasonable game score and not time
|
||||
if 0 <= score1 <= 5 and 0 <= score2 <= 5 and (score1 + score2) > 0:
|
||||
# Additional check: not a time pattern
|
||||
if not re.search(r'\d{1,2}:\d{2}(?:\s*[AP]M)?(?:\s*[A-Z]{3,4})?', line):
|
||||
score_match = score_pattern
|
||||
# Make sure it's not a time pattern (HH:MM)
|
||||
if not re.search(r'\d{1,2}:\d{2}\s*(?:CEST?|UTC|[AP]M)', text[max(0, score_pattern.start()-10):score_pattern.end()+10]):
|
||||
has_score = True
|
||||
break
|
||||
match_data['score'] = f"{score1}-{score2}"
|
||||
score_match = score_pattern
|
||||
|
||||
# Extract teams and format
|
||||
vs_pattern = r'([A-Za-z0-9\s\.\-_]+?)vs\(?(Bo\d)\)?([A-Za-z0-9\s\.\-_]+?)(?:TI2025|Round|Playoff|Group|\+|$)'
|
||||
match = re.search(vs_pattern, text)
|
||||
|
||||
if not match:
|
||||
vs_pattern = r'([A-Za-z0-9\s\.\-_]+?)vs([A-Za-z0-9\s\.\-_]+?)(?:TI2025|Round|Playoff|Group|\+|$)'
|
||||
match = re.search(vs_pattern, text)
|
||||
|
||||
if match:
|
||||
team1 = match.group(1).strip()
|
||||
if len(match.groups()) > 2:
|
||||
format_str = match.group(2)
|
||||
team2 = match.group(3).strip()
|
||||
# Extract format (Bo1, Bo3, Bo5)
|
||||
format_elem = parent.find('span', class_='match-info-header-scoreholder-lower')
|
||||
if format_elem:
|
||||
format_text = format_elem.get_text().strip()
|
||||
format_match = re.search(r'(Bo\d)', format_text)
|
||||
if format_match:
|
||||
match_data['format'] = format_match.group(1)
|
||||
else:
|
||||
format_str = None
|
||||
team2 = match.group(2).strip()
|
||||
# Fallback to text search
|
||||
text = parent.get_text()
|
||||
format_match = re.search(r'\(?(Bo\d)\)?', text)
|
||||
if format_match:
|
||||
match_data['format'] = format_match.group(1)
|
||||
|
||||
# Clean up team names
|
||||
team1 = re.sub(r'^.*CEST?', '', team1).strip()
|
||||
|
||||
if team1 and team2:
|
||||
match_data['team1'] = self._clean_team_name(team1)
|
||||
match_data['team2'] = self._clean_team_name(team2)
|
||||
|
||||
if format_str and format_str.startswith('Bo'):
|
||||
match_data['format'] = format_str
|
||||
|
||||
# Extract tournament
|
||||
# Extract tournament from HTML structure
|
||||
tournament_elem = parent.find('div', class_='match-info-tournament')
|
||||
if tournament_elem:
|
||||
tournament_text = tournament_elem.get_text().strip()
|
||||
# Clean up tournament name
|
||||
tournament_text = re.sub(r'\+ Add details.*', '', tournament_text).strip()
|
||||
if 'TI2025' in tournament_text:
|
||||
match_data['tournament'] = 'The International 2025'
|
||||
round_match = re.search(r'Round\s+\d+', tournament_text)
|
||||
if round_match:
|
||||
match_data['tournament'] += f" - {round_match.group(0)}"
|
||||
else:
|
||||
match_data['tournament'] = tournament_text
|
||||
else:
|
||||
# Fallback to text search
|
||||
text = parent.get_text()
|
||||
if 'TI2025' in text:
|
||||
match_data['tournament'] = 'The International 2025'
|
||||
round_match = re.search(r'Round\s+\d+', text)
|
||||
@ -171,17 +217,48 @@ class Dota2CalendarSync:
|
||||
if major_match:
|
||||
match_data['tournament'] = major_match.group(0).strip()
|
||||
|
||||
# Mark if completed
|
||||
if has_score and score_match:
|
||||
match_data['completed'] = True
|
||||
match_data['score'] = f"{score_match.group(1)}-{score_match.group(2)}"
|
||||
# Determine winner
|
||||
if int(score_match.group(1)) > int(score_match.group(2)):
|
||||
# Mark if has score and if completed
|
||||
if has_score:
|
||||
# Score already set above, extract score values
|
||||
score_parts = re.match(r'(\d+)-(\d+)', match_data['score'])
|
||||
if score_parts:
|
||||
score1 = int(score_parts.group(1))
|
||||
score2 = int(score_parts.group(2))
|
||||
else:
|
||||
score1 = score2 = 0
|
||||
|
||||
# Check if series is actually completed based on format
|
||||
series_completed = False
|
||||
if 'format' in match_data:
|
||||
if 'Bo3' in match_data['format']:
|
||||
# Bo3 is complete when someone reaches 2 wins
|
||||
series_completed = (score1 >= 2 or score2 >= 2)
|
||||
elif 'Bo5' in match_data['format']:
|
||||
# Bo5 is complete when someone reaches 3 wins
|
||||
series_completed = (score1 >= 3 or score2 >= 3)
|
||||
elif 'Bo1' in match_data['format']:
|
||||
# Bo1 is complete when there's any score
|
||||
series_completed = True
|
||||
else:
|
||||
# Unknown format, assume completed if there's a score
|
||||
series_completed = True
|
||||
else:
|
||||
# No format info, try to guess from score
|
||||
# If someone has 2+ wins, likely a completed Bo3/Bo5
|
||||
series_completed = (score1 >= 2 or score2 >= 2)
|
||||
|
||||
match_data['completed'] = series_completed
|
||||
match_data['has_score'] = True # Mark that there's a score even if not completed
|
||||
|
||||
# Determine winner only if completed
|
||||
if series_completed:
|
||||
if score1 > score2:
|
||||
match_data['winner'] = match_data.get('team1', 'Unknown')
|
||||
else:
|
||||
match_data['winner'] = match_data.get('team2', 'Unknown')
|
||||
else:
|
||||
match_data['completed'] = False
|
||||
match_data['has_score'] = False
|
||||
|
||||
# Generate ID if we have valid data
|
||||
if 'team1' in match_data and 'team2' in match_data:
|
||||
@ -481,6 +558,58 @@ class Dota2CalendarSync:
|
||||
print(f"Error updating event with teams: {e}")
|
||||
return False
|
||||
|
||||
def update_event_with_score(self, event_id, match_data):
|
||||
"""Update an existing calendar event with in-progress score"""
|
||||
try:
|
||||
# Get the existing event
|
||||
event = self.service.events().get(
|
||||
calendarId=self.calendar_id,
|
||||
eventId=event_id
|
||||
).execute()
|
||||
|
||||
# Update the description with current score
|
||||
description = event.get('description', '')
|
||||
|
||||
# Check if score is already in the description
|
||||
if '📊 CURRENT SCORE:' in description:
|
||||
# Update existing score
|
||||
description = re.sub(
|
||||
r'📊 CURRENT SCORE:.*?\n',
|
||||
f"📊 CURRENT SCORE: {match_data.get('score', 'Unknown')}\n",
|
||||
description
|
||||
)
|
||||
else:
|
||||
# Add new score
|
||||
score_text = f"\n📊 CURRENT SCORE: {match_data.get('score', 'Unknown')}\n"
|
||||
if 'ID:' in description:
|
||||
description = description.replace('ID:', score_text + 'ID:')
|
||||
else:
|
||||
description += score_text
|
||||
|
||||
# Update the summary to show current score (without checkmark)
|
||||
summary = event.get('summary', '')
|
||||
# Remove any existing score
|
||||
summary = re.sub(r'^\d+[-:]\d+\s+', '', summary)
|
||||
# Add new score at the beginning
|
||||
score = match_data.get('score', '?-?')
|
||||
summary = f"{score} {summary}"
|
||||
|
||||
# Update the event
|
||||
event['description'] = description
|
||||
event['summary'] = summary
|
||||
|
||||
updated_event = self.service.events().update(
|
||||
calendarId=self.calendar_id,
|
||||
eventId=event_id,
|
||||
body=event
|
||||
).execute()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating event score: {e}")
|
||||
return False
|
||||
|
||||
def update_event_with_result(self, event_id, match_data):
|
||||
"""Update an existing calendar event with match results"""
|
||||
try:
|
||||
@ -583,7 +712,10 @@ class Dota2CalendarSync:
|
||||
print("-" * 30)
|
||||
|
||||
now = datetime.now(pytz.UTC)
|
||||
future_matches = [m for m in upcoming_matches if m.get('datetime', now) >= now]
|
||||
# Include matches from the last 12 hours (to catch ongoing matches with scores)
|
||||
twelve_hours_ago = now - timedelta(hours=12)
|
||||
future_matches = [m for m in upcoming_matches
|
||||
if m.get('datetime', now) >= twelve_hours_ago]
|
||||
|
||||
for match in future_matches:
|
||||
match_id = match.get('id')
|
||||
@ -640,6 +772,51 @@ class Dota2CalendarSync:
|
||||
else:
|
||||
print(f"✗ Failed to update TBD match: {team1} vs {team2}")
|
||||
error_count += 1
|
||||
# Check if this match has a score (completed or in-progress) and needs update
|
||||
elif match.get('has_score') and update_results:
|
||||
# Check current event status
|
||||
summary = existing_event.get('summary', '')
|
||||
description = existing_event.get('description', '')
|
||||
current_score = None
|
||||
|
||||
# Try to extract current score from summary
|
||||
score_in_summary = re.search(r'✓?\s*(\d+[-:]\d+)', summary)
|
||||
if score_in_summary:
|
||||
current_score = score_in_summary.group(1).replace(':', '-')
|
||||
|
||||
# Check if score needs update
|
||||
new_score = match.get('score', 'Unknown')
|
||||
|
||||
if current_score == new_score:
|
||||
print(f"⊘ Score unchanged: {team1} vs {team2} ({new_score})")
|
||||
skipped_count += 1
|
||||
else:
|
||||
if match.get('completed'):
|
||||
# Series is completed
|
||||
if dry_run:
|
||||
print(f"◯ Would update completed result: {team1} vs {team2} - {new_score}")
|
||||
updated_count += 1
|
||||
else:
|
||||
if self.update_event_with_result(existing_event['id'], match):
|
||||
print(f"✓ Updated completed result: {team1} vs {team2} - {new_score}")
|
||||
updated_count += 1
|
||||
time.sleep(0.2)
|
||||
else:
|
||||
print(f"✗ Failed to update: {team1} vs {team2}")
|
||||
error_count += 1
|
||||
else:
|
||||
# Series is in-progress with partial score
|
||||
if dry_run:
|
||||
print(f"◯ Would update in-progress score: {team1} vs {team2} - {new_score}")
|
||||
updated_count += 1
|
||||
else:
|
||||
if self.update_event_with_score(existing_event['id'], match):
|
||||
print(f"📊 Updated in-progress score: {team1} vs {team2} - {new_score}")
|
||||
updated_count += 1
|
||||
time.sleep(0.2)
|
||||
else:
|
||||
print(f"✗ Failed to update score: {team1} vs {team2}")
|
||||
error_count += 1
|
||||
# Check if time has changed
|
||||
elif update_times:
|
||||
event_start = existing_event['start'].get('dateTime', existing_event['start'].get('date'))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user