
本文介绍如何在解析无明确结构的 html 表格时,根据 `
` 标题标签中的关键词(如 "mlb")定位,并精准抓取其后所有同级 ` |
` 数据行,直到下一个标题 ` |
` 出现为止。核心在于合理利用 beautifulsoup 的 `find_next_siblings()` 和状态驱动遍历。 在实际网页爬虫开发中,常会遇到一类“伪多表”结构:整个
|
内并无独立 分隔,而是通过带标题语义的 | (如 "MLB Spring Training - Monday, March 27th")作为逻辑分组标识,其后紧跟若干 |
表示具体赛事条目,直至下一个同类 |
出现——这种模式虽不符合标准 HTML 表格规范,却广泛存在于体育赛事、日程公告等动态页面中。 要可靠提取某类分组(如 MLB)下的全部赛事数据,关键在于建立上下文感知的遍历逻辑。以下是两种经过验证的专业方案:
✅ 方案一:基于 find_next_siblings() 的声明式定位(推荐)
该方法语义清晰、可读性强,适用于已知目标
|
易于识别的场景(如含明确文本关键词):from bs4 import BeautifulSoup
# 假设 soup 已加载完整 HTML
td_headers = soup.find_all('td', class_='head1') # 精准定位标题行(避免匹配 head1x)
target_keyword = 'MLB'
for td in td_headers:
if target_keyword in td.get_text():
# 获取该 | 后所有同级兄弟标签,直到下一个 |
next_ths = []
for sibling in td.find_next_siblings():
if sibling.name == 'td' and 'head1' in sibling.get('class', []):
break # 遇到新分组标题,停止收集
if sibling.name == 'th':
next_ths.append(sibling)
# 解析每个 | 中的时间与队伍信息
games = []
for th in next_ths:
date_div = th.find('div') # 第一个 div 通常是日期
time_div = th.find('div', class_='time') # 时间子 div
teams = [div.get_text(strip=True) for div in th.find_all('div') if div != date_div and div != time_div]
games.append({
'date': date_div.get_text(strip=True) if date_div else None,
'time': time_div.get_text(strip=True) if time_div else None,
'teams': teams[:2] if len(teams) >= 2 else teams
})
print(f"Found {len(games)} MLB games:")
for g in games:
print(f" {g['date']} {g['time']} — {' vs '.join(g['teams'])}")
break # 仅处理第一个匹配项(如需全部,移除此行)⚠️ 注意事项: 使用 soup.find_all('td', class_='head1') 而非泛化的 find_all('td'),可避免误匹配 等装饰性单元格; find_next_siblings() 返回的是文档顺序中紧邻的后续兄弟节点,天然符合“标题后连续数据行”的逻辑; break 在遇到下一个 head1 时终止,确保不跨组混入 NHL 或 NBA 数据。
✅ 方案二:单次线性遍历 + 状态机(内存高效)
当 ResultSet 已预先扁平化为 cells = row.find_all(['th','td']) 列表时,推荐用状态变量控制流程,避免重复 DOM 遍历: cells = []
for row in soup_table.tbody.find_all("tr"):
cells.extend(row.find_all(['th', 'td']))
mlb_games = []
in_mlb_section = False
for cell in cells:
# 进入 MLB 分组
if (cell.name == 'td'
and 'head1' in cell.get('class', [])
and 'MLB' in cell.get_text()):
in_mlb_section = True
continue
# 退出当前分组(遇到任意其他 head1 标题)
if (cell.name == 'td'
and 'head1' in cell.get('class', [])
and 'MLB' not in cell.get_text()):
in_mlb_section = False
continue
# 收集 MLB 分组内的 th 数据
if in_mlb_section and cell.name == 'th':
# 提取时间与队伍(同上)
date = cell.find('div').get_text(strip=True) if cell.find('div') else ''
time_el = cell.find('div', class_='time')
time_str = time_el.get_text(strip=True) if time_el else ''
teams = [d.get_text(strip=True) for d in cell.find_all('div')[1:]][:2]
mlb_games.append({'date': date, 'time': time_str, 'teams': teams})
print(f"Extracted {len(mlb_games)} MLB entries.")? 总结建议
-
优先使用方案一:代码意图明确,调试友好,适合多数中小型爬虫任务;
-
方案二适用于流式处理或内存敏感场景:仅遍历一次 DOM,适合超长表格;
- 始终通过 class 属性(如 'head1')而非标签名做标题判定,提升鲁棒性;
- 对 get_text() 使用 strip=True 防止空白字符干扰关键词匹配;
- 若目标站点存在动态加载,需确认 soup 已包含完整渲染后 HTML(必要时集成 Selenium)。
掌握这两种模式,即可灵活应对各类“标题+数据块”混合结构,为体育、金融、课表等垂直领域爬虫打下坚实基础。
| |