
本文详解使用 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 工程化思维的关键一课。