
本文详解使用 BeautifulSoup 从表格中精准提取嵌套在 内的 链接时常见错误(如误读 href 属性、未清洗文本、变量未定义),并提供可直接运行的修复方案,确保 Player_URL 列不再返回 NaN。
本文详解使用 beautifulsoup 从表格中精准提取嵌套在 `` 内的 `` 链接时常见错误(如误读 `href` 属性、未清洗文本、变量未定义),并提供可直接运行的修复方案,确保 player_url 列不再返回 nan。
在使用 requests + BeautifulSoup 进行网页数据抓取时,一个高频陷阱是:试图从非链接标签(如 )上直接获取 href 属性。这会导致 link.get("href") 返回 None,当赋值给 pandas DataFrame 的某列时,pandas 自动将其转换为 NaN——而这正是原代码中 Player_URL 列全为 NaN 的根本原因。
更关键的是,原始逻辑存在三处结构性缺陷:
- HTML 结构误判:目标链接实际位于 Player Name 中,href 属于 标签,而非外层 ;
- 名称匹配前未标准化:DataFrame 中的 Player 字段含括号、空格、换行符(如 "Nikita Kucherov (RW)"),而 文本为 "Nikita Kucherov",直接 df.Player == name 匹配必然失败;
- 变量作用域错误:循环中使用了未定义的 name 变量,且 list = link.get("href") 覆盖了列表而非追加。
✅ 正确做法是:先提取 标签 → 获取其 href 和 .text → 清洗玩家姓名(移除括号及括号内位置信息)→ 在清洗后的 Player 列中进行模糊匹配或精确对齐。
以下是修复后的完整可运行代码(已适配 pandas 2.0+,兼容 applymap 已弃用问题):
import requests
from bs4 import BeautifulSoup
import pandas as pd
start_url = 'https://www.eliteprospects.com/league/nhl/stats/2023-2024'
r = requests.get(start_url)
r.raise_for_status() # 显式检查 HTTP 错误
soup = BeautifulSoup(r.content, "html.parser")
table = soup.find("table", class_="table table-striped table-sortable player-stats highlight-stats season")
# 提取表头(注意:部分 th 可能含换行,需 strip)
headers = [th.get_text(strip=True) for th in table.find_all("th")]
df = pd.DataFrame(columns=headers)
# 提取数据行
rows = table.find_all("tr")[1:] # 跳过表头行
for row in rows:
tds = row.find_all("td")
if not tds: continue
# 清洗每单元格文本:去换行、首尾空格
row_data = [td.get_text(strip=True) for td in tds]
if len(row_data) == len(headers):
df.loc[len(df)] = row_data
# ✅ 关键修复:提取 Player_URL
df["Player_URL"] = None # 初始化列,避免 SettingWithCopyWarning
# 遍历所有 <span class="txt-blue">,定位其内部 <a>
for span in table.find_all("span", class_="txt-blue"):
a_tag = span.find("a")
if not a_tag or not a_tag.get("href") or not a_tag.get_text(strip=True):
continue
full_url = a_tag["href"]
raw_name = a_tag.get_text(strip=True)
# 清洗姓名:移除 "(POS)" 类后缀(如 "(RW)"、"(C/LW)"),仅保留主名
clean_name = raw_name.split("(")[0].strip()
# 在 df.Player 列中查找匹配项(建议使用 str.contains 增强鲁棒性)
mask = df["Player"].str.contains(f"^{clean_name}($|\s*\()", na=False, regex=True)
if mask.any():
df.loc[mask, "Player_URL"] = "https://www.eliteprospects.com" + full_url
# 后处理:统一清洗全表字符串字段
str_cols = df.select_dtypes(include=["object"]).columns
df[str_cols] = df[str_cols].apply(lambda x: x.str.strip() if x.dtype == "object" else x)
# 输出验证
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 120)
print(df[["Player", "Team", "GP", "G", "A", "TP", "Player_URL"]].head())? 注意事项与进阶建议:
- 反爬策略:该网站无强反爬,但长期批量请求建议添加 headers={'User-Agent': 'Mozilla/5.0https://www.php.cn/link/263b1243ca2dbeb358777ceabc4a2e4c'} 及合理 time.sleep();
- 动态内容风险:若未来页面改用 JavaScript 渲染表格(如 React/Vue),requests 将失效,需切换至 Selenium 或 Playwright;
- 健壮性增强:生产环境应封装为函数,加入异常捕获(try/except)、重试机制及日志记录;
- 多赛季扩展:按年份循环构造 URL(如 f"https://www.eliteprospects.com/league/nhl/stats/{year}-{year+1}"),合并各年 DataFrame 即可实现目标。
通过理解 HTML 嵌套结构、严格区分标签职责、预处理文本一致性,即可彻底规避 NaN 链接问题——这不仅是技术修复,更是 Web Scraping 工程化思维的关键一课。










