Selenium 找不到元素?Python 网页表格抓取常见问题与解决
2025-04-17 23:04:52
好的,这是为你生成的博客文章:
搞定网页表格抓取:Selenium 找不到元素?看这里!
哥们儿,想从网页上抓个表格数据存成 CSV,结果脚本报 NoSuchElementException
,死活找不到那个 <table>
元素,明明用浏览器开发者工具看它就在那儿,ID 也没错,比如 table-container-table
这个。用的工具是 Python + Selenium + Firefox + GeckoDriver。这情况挺常见的,别急,咱来分析分析,看看怎么解决。
你遇到的页面是 https://www.gamestop.ca/tradevalues
。你保存了页面源码,也尝试滚动到底部,甚至用了 WebDriverWait
等待元素出现,但还是失败了。错误信息直指 NoSuchElementError
。
# 你尝试的代码片段(部分)
try:
wait = WebDriverWait(driver, 20) # 等待最多 20 秒
table = wait.until(EC.presence_of_element_located((By.ID, 'table-container-table')))
print("表格找到了!")
except Exception as e:
print(f"表格没找到: {e}")
# ...后续处理
看到你提供的 HTML 片段,<table id="table-container-table">
确实存在。那问题出在哪儿呢?
一、为啥 Selenium “看走眼”了?
明明元素就在那儿,为啥脚本说找不到?原因通常有这么几个:
- 页面还没加载完呢! 现在的网页大多是动态加载的。浏览器地址栏转圈停了,不代表所有东西都出来了。特别是表格数据,很可能是通过 JavaScript(或者叫 JS)在后台偷偷请求数据,然后再“画”到页面上的。你脚本跑太快,去抓的时候,表格可能还是个空架子,甚至那个 ID 对应的
<table>
标签还没被 JS 添加到网页结构(DOM)里。 - 元素被“藏”起来了。 有些元素可能在 DOM 结构里存在了,但是被设置成
display: none;
或visibility: hidden;
,或者尺寸是 0x0。presence_of_element_located
这个等待条件只管元素在不在 DOM 里,不管它看不看得见、能不能点。你可能需要等它“显形”才行。 - 表格在 iFrame 里头。 这种情况就像网页里套了个小网页(iFrame)。Selenium 默认只在主页面找东西,找不到 iFrame 里面的。你需要先“跳”进那个 iFrame,才能操作里面的元素。
- 遇上 Shadow DOM 了。 这是一种更新的技术,能把一部分网页结构和样式“隔离”起来,形成一个“影子”DOM。Selenium 的常规查找方法穿透不了这层影子,得用特殊姿势。
- 动态 ID 或结构变化。 有些网站为了反爬虫,或者用了某些前端框架,元素的 ID 或结构可能会动态变化。不过从你给的 HTML 看,这个
table-container-table
像是固定的。 - DataTables.js 这类库在作怪。 看 HTML 里的
class="dataTable"
、dataTables_wrapper
、dataTables_filter
这些名字,很明显这个表格是用了 DataTables.js 或者类似的 JavaScript 库来增强功能的(排序、搜索、分页等)。这类库加载数据和渲染表格的方式比较特殊,直接等<table>
标签出现可能不够,得等数据真正填充进去才稳妥。
你的问题,最可能的原因是 1、2 和 6 的结合:表格是动态加载的,而且很可能用了 DataTables.js,导致你需要更智能的等待策略。
二、见招拆招:怎么逮住这个表格
针对上面的原因,咱们有几套组合拳可以打:
方案一:升级你的“等待”策略
WebDriverWait
是个好东西,但得用对条件。
-
原理与作用:
WebDriverWait
配合expected_conditions
(通常简写为EC
) 能让 Selenium 等待某个条件满足后再执行下一步,而不是傻等固定时间 (time.sleep
)。EC.presence_of_element_located
只关心元素是否在 DOM 树里。对于动态内容,用EC.visibility_of_element_located
通常更靠谱,它会等到元素不仅存在,而且在页面上可见(宽高大于0)。 -
代码示例:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException # 引入 TimeoutException
# ... driver 初始化等代码 ...
try:
# 提升等待时间,比如 30 秒
wait = WebDriverWait(driver, 30)
# 尝试等待表格可见,而不只是存在
print("正在等待表格可见...")
table = wait.until(EC.visibility_of_element_located((By.ID, 'table-container-table')))
print("表格找到了,并且可见!")
# 抓取表格数据的代码...
except TimeoutException:
print("超时了!等了半天表格也没出来或者没变可见。")
# 可以保存截图或源码进一步分析
driver.save_screenshot('timeout_screenshot.png')
with open('timeout_page_source.html', 'w', encoding='utf-8') as f:
f.write(driver.page_source)
driver.quit()
exit()
except Exception as e:
print(f"发生其他错误: {e}")
driver.quit()
exit()
# 你的后续代码:查找行和单元格
# ... (查找 rows 和 cells 的代码保持不变) ...
- 进阶技巧:
- 有时候等父元素可见了,里面的子元素(比如表格的
<tbody>
或者第一行数据<tr>
)可能还没完全加载好。可以试试等待更具体的子元素,比如等待tbody
里的第一个tr
出现:print("正在等待表格数据行出现...") wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, '#table-container-table tbody tr'))) print("表格数据行出现了!") # 这时候再去找 table 元素,理论上它肯定也在了 table = driver.find_element(By.ID, 'table-container-table')
- 观察网页加载行为。是不是有什么加载动画或者提示文字(比如 DataTables 常用的 "Loading..." 或 "Processing...")?可以尝试等待这些加载提示消失,或者等待表示加载完成的元素出现(比如 DataTables 的分页信息
_info
元素)。
- 有时候等父元素可见了,里面的子元素(比如表格的
方案二:专门对付 DataTables.js 和分页
既然很可能是 DataTables.js,那就要注意它的特性了。
-
原理与作用: DataTables 通常只在页面上显示一部分数据(比如 10 条),完整数据需要通过分页按钮(Previous, Next, 数字页码)或者更改显示条数来加载。你光抓第一页是不够的。而且,判断表格加载完成,可以观察
_info
区域(比如 "Showing 1 to 10 of 5,378 entries")的内容是否稳定出现。 -
操作步骤与代码示例 (抓取所有页数据):
import pandas as pd
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import time
# ... driver 初始化,打开页面 ...
all_data = []
try:
# 等待表格容器 和 分页信息出现,表示 DataTables 初始化可能完成
wait = WebDriverWait(driver, 30)
print("等待 DataTables 初始化...")
wait.until(EC.visibility_of_element_located((By.ID, 'table-container-table_wrapper')))
wait.until(EC.visibility_of_element_located((By.ID, 'table-container-table_info')))
print("DataTables 相关元素已出现。")
while True: # 循环翻页
print("开始处理当前页...")
# 在当前页等待表格数据行加载完成(每次翻页后都要等)
try:
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, '#table-container-table tbody tr')))
print("当前页数据行已加载。")
table = driver.find_element(By.ID, 'table-container-table')
rows = table.find_elements(By.CSS_SELECTOR, 'tbody tr') # 精确查找 tbody 下的 tr
print(f"找到 {len(rows)} 行数据。")
for row in rows:
cells = row.find_elements(By.TAG_NAME, 'td')
if len(cells) == 4:
sku = cells[0].text
item_description = cells[1].text
platform = cells[2].text
value = cells[3].text
all_data.append({
'SKU': sku,
'Item Description': item_description,
'Platform': platform,
'Value': value
})
except TimeoutException:
print("等待当前页数据行超时,可能此页无数据或加载失败。")
# 可以根据情况决定是跳过还是报错退出
# 尝试找 "Next" 按钮并判断是否可点击
try:
next_button = driver.find_element(By.ID, 'table-container-table_next')
# DataTables 会给禁用的按钮加上 'disabled' class
if 'disabled' in next_button.get_attribute('class'):
print("已经是最后一页了。")
break # 退出翻页循环
else:
print("点击 'Next' 按钮...")
# 用 JS 点击可能更稳定,尤其按钮可能被遮挡
driver.execute_script("arguments[0].click();", next_button)
# 等待一小段时间让页面响应翻页动作,并且等待加载指示(如果有)消失
# 这里可以用 WebDriverWait 等待 _processing 元素消失,或者简单 sleep
time.sleep(2) # 简单等待,实际项目中建议用更智能的等待
except NoSuchElementException:
print("没找到 'Next' 按钮,可能只有一页数据。")
break # 退出翻页循环
except Exception as click_err:
print(f"点击 'Next' 按钮时出错: {click_err}")
break # 出错也退出
finally:
# 无论成功失败,确保最后关闭浏览器
driver.quit()
# 将收集到的所有数据保存到 CSV
if all_data:
df = pd.DataFrame(all_data)
# 清理 Value 列,去掉 'import pandas as pd
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import time
# ... driver 初始化,打开页面 ...
all_data = []
try:
# 等待表格容器 和 分页信息出现,表示 DataTables 初始化可能完成
wait = WebDriverWait(driver, 30)
print("等待 DataTables 初始化...")
wait.until(EC.visibility_of_element_located((By.ID, 'table-container-table_wrapper')))
wait.until(EC.visibility_of_element_located((By.ID, 'table-container-table_info')))
print("DataTables 相关元素已出现。")
while True: # 循环翻页
print("开始处理当前页...")
# 在当前页等待表格数据行加载完成(每次翻页后都要等)
try:
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, '#table-container-table tbody tr')))
print("当前页数据行已加载。")
table = driver.find_element(By.ID, 'table-container-table')
rows = table.find_elements(By.CSS_SELECTOR, 'tbody tr') # 精确查找 tbody 下的 tr
print(f"找到 {len(rows)} 行数据。")
for row in rows:
cells = row.find_elements(By.TAG_NAME, 'td')
if len(cells) == 4:
sku = cells[0].text
item_description = cells[1].text
platform = cells[2].text
value = cells[3].text
all_data.append({
'SKU': sku,
'Item Description': item_description,
'Platform': platform,
'Value': value
})
except TimeoutException:
print("等待当前页数据行超时,可能此页无数据或加载失败。")
# 可以根据情况决定是跳过还是报错退出
# 尝试找 "Next" 按钮并判断是否可点击
try:
next_button = driver.find_element(By.ID, 'table-container-table_next')
# DataTables 会给禁用的按钮加上 'disabled' class
if 'disabled' in next_button.get_attribute('class'):
print("已经是最后一页了。")
break # 退出翻页循环
else:
print("点击 'Next' 按钮...")
# 用 JS 点击可能更稳定,尤其按钮可能被遮挡
driver.execute_script("arguments[0].click();", next_button)
# 等待一小段时间让页面响应翻页动作,并且等待加载指示(如果有)消失
# 这里可以用 WebDriverWait 等待 _processing 元素消失,或者简单 sleep
time.sleep(2) # 简单等待,实际项目中建议用更智能的等待
except NoSuchElementException:
print("没找到 'Next' 按钮,可能只有一页数据。")
break # 退出翻页循环
except Exception as click_err:
print(f"点击 'Next' 按钮时出错: {click_err}")
break # 出错也退出
finally:
# 无论成功失败,确保最后关闭浏览器
driver.quit()
# 将收集到的所有数据保存到 CSV
if all_data:
df = pd.DataFrame(all_data)
# 清理 Value 列,去掉 '$' 符号并转为数值(如果需要)
if 'Value' in df.columns:
df['Value'] = df['Value'].str.replace('$', '', regex=False).astype(float)
csv_filename = 'gamestop_trade_values.csv'
df.to_csv(csv_filename, index=False, encoding='utf-8-sig') # utf-8-sig 保证 Excel 打开中文不乱码
print(f"数据已保存到 {csv_filename}")
else:
print("没有抓取到任何数据。")
#x27; 符号并转为数值(如果需要)
if 'Value' in df.columns:
df['Value'] = df['Value'].str.replace('import pandas as pd
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import time
# ... driver 初始化,打开页面 ...
all_data = []
try:
# 等待表格容器 和 分页信息出现,表示 DataTables 初始化可能完成
wait = WebDriverWait(driver, 30)
print("等待 DataTables 初始化...")
wait.until(EC.visibility_of_element_located((By.ID, 'table-container-table_wrapper')))
wait.until(EC.visibility_of_element_located((By.ID, 'table-container-table_info')))
print("DataTables 相关元素已出现。")
while True: # 循环翻页
print("开始处理当前页...")
# 在当前页等待表格数据行加载完成(每次翻页后都要等)
try:
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, '#table-container-table tbody tr')))
print("当前页数据行已加载。")
table = driver.find_element(By.ID, 'table-container-table')
rows = table.find_elements(By.CSS_SELECTOR, 'tbody tr') # 精确查找 tbody 下的 tr
print(f"找到 {len(rows)} 行数据。")
for row in rows:
cells = row.find_elements(By.TAG_NAME, 'td')
if len(cells) == 4:
sku = cells[0].text
item_description = cells[1].text
platform = cells[2].text
value = cells[3].text
all_data.append({
'SKU': sku,
'Item Description': item_description,
'Platform': platform,
'Value': value
})
except TimeoutException:
print("等待当前页数据行超时,可能此页无数据或加载失败。")
# 可以根据情况决定是跳过还是报错退出
# 尝试找 "Next" 按钮并判断是否可点击
try:
next_button = driver.find_element(By.ID, 'table-container-table_next')
# DataTables 会给禁用的按钮加上 'disabled' class
if 'disabled' in next_button.get_attribute('class'):
print("已经是最后一页了。")
break # 退出翻页循环
else:
print("点击 'Next' 按钮...")
# 用 JS 点击可能更稳定,尤其按钮可能被遮挡
driver.execute_script("arguments[0].click();", next_button)
# 等待一小段时间让页面响应翻页动作,并且等待加载指示(如果有)消失
# 这里可以用 WebDriverWait 等待 _processing 元素消失,或者简单 sleep
time.sleep(2) # 简单等待,实际项目中建议用更智能的等待
except NoSuchElementException:
print("没找到 'Next' 按钮,可能只有一页数据。")
break # 退出翻页循环
except Exception as click_err:
print(f"点击 'Next' 按钮时出错: {click_err}")
break # 出错也退出
finally:
# 无论成功失败,确保最后关闭浏览器
driver.quit()
# 将收集到的所有数据保存到 CSV
if all_data:
df = pd.DataFrame(all_data)
# 清理 Value 列,去掉 '$' 符号并转为数值(如果需要)
if 'Value' in df.columns:
df['Value'] = df['Value'].str.replace('$', '', regex=False).astype(float)
csv_filename = 'gamestop_trade_values.csv'
df.to_csv(csv_filename, index=False, encoding='utf-8-sig') # utf-8-sig 保证 Excel 打开中文不乱码
print(f"数据已保存到 {csv_filename}")
else:
print("没有抓取到任何数据。")
#x27;, '', regex=False).astype(float)
csv_filename = 'gamestop_trade_values.csv'
df.to_csv(csv_filename, index=False, encoding='utf-8-sig') # utf-8-sig 保证 Excel 打开中文不乱码
print(f"数据已保存到 {csv_filename}")
else:
print("没有抓取到任何数据。")
- 安全建议/进阶:
- 抓取分页数据时,每翻一页加个短暂延时 (
time.sleep(1)
或time.sleep(0.5)
),对目标网站友好一点,也降低被识别为机器人的风险。 - 如果数据量特别大,考虑分批次抓取或增加更长的延时。
- 注意 DataTables 可能有服务器端分页(Server-side processing),这时每次翻页都会向服务器发请求。这种情况下,后面提到的“方案四:找 API”会更高效。
- 抓取分页数据时,每翻一页加个短暂延时 (
方案三:检查是不是掉进了 iFrame 的“坑”
-
原理与作用: iFrame 像是在当前网页开了个“窗”,里面的内容属于另一个独立的 HTML 文档。Selenium 需要明确告诉它:“嘿,切换到那个窗口里去操作!”
-
代码示例 (如何切换):
from selenium.common.exceptions import NoSuchFrameException
# ...
try:
# 尝试查找 iframe,可以通过 id, name, index 或者 WebElement 对象来定位
print("检查是否存在 iframe...")
# 比如假设 iframe 的 id 是 'table-iframe'
wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, 'table-iframe')))
# 或者根据其他属性,如 tag name
# iframes = driver.find_elements(By.TAG_NAME, 'iframe')
# if iframes:
# print(f"找到 {len(iframes)} 个 iframe,尝试切换到第一个...")
# driver.switch_to.frame(iframes[0]) # 切换到第一个 iframe
print("已切换到 iframe。")
# 现在可以在 iframe 内部查找表格了
table = wait.until(EC.visibility_of_element_located((By.ID, 'table-container-table')))
print("在 iframe 中找到了表格!")
# ...抓取数据的代码...
# 操作完 iframe 里的内容,记得切回主页面
driver.switch_to.default_content()
print("已切回主页面。")
except NoSuchFrameException:
print("没找到指定的 iframe。表格可能不在 iframe 里。")
except TimeoutException:
print("切换 iframe 超时,或者在 iframe 里没等到表格。")
# ... 其他异常处理 ...
- 如何判断? 用浏览器开发者工具(F12),选中那个表格元素,看看它的父级元素路径里有没有
<iframe>
标签。如果有,你就得用switch_to.frame()
。
方案四:绕开界面,直接找数据 API(推荐)
这是最高效、最稳定的方法,尤其是对付 DataTables.js 或其他大量使用 AJAX 加载数据的网站。
-
原理与作用: 很多动态表格的数据并非直接写在 HTML 里,而是通过浏览器后台向服务器某个接口(API)发送请求获取到的(通常是 JSON 格式)。我们不用模拟浏览器点击、等待渲染那么麻烦,直接用 Python 的
requests
库去模拟这个 API 请求,拿到原始数据。 -
操作步骤:
- 打开浏览器开发者工具 (F12),切换到“网络” (Network) 或 “网络监视器” (Netmonitor) 标签页。
- 刷新目标网页 (
https://www.gamestop.ca/tradevalues
)。 - 在网络请求列表里,筛选
XHR
(XMLHttpRequest) 或者Fetch/XHR
类型的请求。这些通常就是后台数据请求。 - 观察请求的 URL、请求方法 (GET/POST)、请求头 (Headers)、请求参数 (Payload/Form Data)。找到那个看起来像是获取表格数据的请求。对于 DataTables,请求的 URL 或参数里可能会包含
draw
,start
,length
,search[value]
等字段。 - 在“响应” (Response) 或“预览” (Preview) 标签页查看返回的内容,确认是不是包含了你要的表格数据(通常是 JSON 格式)。
- 找到这个 API 后,就可以用
requests
库来调用它了。
-
代码示例 (假设找到了一个 GET 请求 API):
import requests
import pandas as pd
import json
# 假设你通过开发者工具发现的 API 信息
# 注意:这个 URL 是编造的示例,你需要自己找到真实的 URL 和参数
api_url = 'https://www.gamestop.ca/api/tradevalues/getdata' # 假设的 API 端点
params = { # 假设的请求参数,DataTables 可能有 start, length 等
'draw': 1, # DataTables 计数器
'start': 0, # 起始记录索引
'length': -1, # 获取所有数据 (-1 通常表示全部,或者设一个很大的数)
# 'search[value]': '', # 搜索(如果有)
# ... 可能还有其他排序、过滤参数 ...
}
headers = { # 可能需要模拟浏览器请求头
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/...',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest', # DataTables AJAX 请求常带这个头
# ... 可能还需要 Cookie 或其他认证头 ...
}
print(f"尝试直接请求 API: {api_url}")
try:
response = requests.get(api_url, params=params, headers=headers, timeout=30)
response.raise_for_status() # 如果请求失败 (非 2xx 状态码),会抛出异常
print("API 请求成功!")
data = response.json() # 解析返回的 JSON 数据
# 分析 JSON 结构,提取所需数据列表
# DataTables 返回的 JSON 通常在 'data' 键下
if 'data' in data:
records = data['data']
# 直接用 Pandas 读取 JSON 里的数据列表 (如果结构适合)
# 注意:这里的列名可能需要根据实际 JSON 结构调整
# 有时 DataTables 返回的是列表的列表,而不是字典列表
# 你可能需要手动处理一下,转换成适合 DataFrame 的格式
# 示例:假设 records 是 [{...}, {...}] 的形式
# df = pd.DataFrame(records)
# 示例:假设 records 是 [[sku, desc, plat, val], [...]] 的形式
# 需要指定列名
df = pd.DataFrame(records, columns=['SKU', 'Item Description', 'Platform', 'Value'])
# 清理 Value 列
if 'Value' in df.columns:
df['Value'] = df['Value'].astype(str).str.replace('import requests
import pandas as pd
import json
# 假设你通过开发者工具发现的 API 信息
# 注意:这个 URL 是编造的示例,你需要自己找到真实的 URL 和参数
api_url = 'https://www.gamestop.ca/api/tradevalues/getdata' # 假设的 API 端点
params = { # 假设的请求参数,DataTables 可能有 start, length 等
'draw': 1, # DataTables 计数器
'start': 0, # 起始记录索引
'length': -1, # 获取所有数据 (-1 通常表示全部,或者设一个很大的数)
# 'search[value]': '', # 搜索(如果有)
# ... 可能还有其他排序、过滤参数 ...
}
headers = { # 可能需要模拟浏览器请求头
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/...',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest', # DataTables AJAX 请求常带这个头
# ... 可能还需要 Cookie 或其他认证头 ...
}
print(f"尝试直接请求 API: {api_url}")
try:
response = requests.get(api_url, params=params, headers=headers, timeout=30)
response.raise_for_status() # 如果请求失败 (非 2xx 状态码),会抛出异常
print("API 请求成功!")
data = response.json() # 解析返回的 JSON 数据
# 分析 JSON 结构,提取所需数据列表
# DataTables 返回的 JSON 通常在 'data' 键下
if 'data' in data:
records = data['data']
# 直接用 Pandas 读取 JSON 里的数据列表 (如果结构适合)
# 注意:这里的列名可能需要根据实际 JSON 结构调整
# 有时 DataTables 返回的是列表的列表,而不是字典列表
# 你可能需要手动处理一下,转换成适合 DataFrame 的格式
# 示例:假设 records 是 [{...}, {...}] 的形式
# df = pd.DataFrame(records)
# 示例:假设 records 是 [[sku, desc, plat, val], [...]] 的形式
# 需要指定列名
df = pd.DataFrame(records, columns=['SKU', 'Item Description', 'Platform', 'Value'])
# 清理 Value 列
if 'Value' in df.columns:
df['Value'] = df['Value'].astype(str).str.replace('$', '', regex=False).astype(float)
csv_filename = 'gamestop_trade_values_api.csv'
df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
print(f"数据已通过 API 抓取并保存到 {csv_filename}")
else:
print("返回的 JSON 数据里没找到 'data' 键。JSON 结构如下:")
print(json.dumps(data, indent=2)) # 打印 JSON 结构方便调试
except requests.exceptions.RequestException as e:
print(f"请求 API 失败: {e}")
except json.JSONDecodeError:
print("解析 API 返回内容失败,不是有效的 JSON 格式。返回内容:")
print(response.text[:500]) # 打印部分返回内容
#x27;, '', regex=False).astype(float)
csv_filename = 'gamestop_trade_values_api.csv'
df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
print(f"数据已通过 API 抓取并保存到 {csv_filename}")
else:
print("返回的 JSON 数据里没找到 'data' 键。JSON 结构如下:")
print(json.dumps(data, indent=2)) # 打印 JSON 结构方便调试
except requests.exceptions.RequestException as e:
print(f"请求 API 失败: {e}")
except json.JSONDecodeError:
print("解析 API 返回内容失败,不是有效的 JSON 格式。返回内容:")
print(response.text[:500]) # 打印部分返回内容
- 安全与道德建议:
- 这是最需要注意的地方!检查网站的
robots.txt
文件(访问https://www.gamestop.ca/robots.txt
)看是否允许抓取相关路径。 - 查看网站的“使用条款”或“服务协议”,了解关于数据抓取的规定。
- 绝对不要 频繁、大量地请求 API,这会给对方服务器造成压力,容易被封 IP,甚至可能涉及法律问题。务必在请求之间加入延时 (
time.sleep
)。 - 如果 API 需要登录认证 (Cookies, Tokens),你需要先用 Selenium 模拟登录,获取认证信息后,再传递给
requests
。
- 这是最需要注意的地方!检查网站的
方案五:最后的手段 - 检查 Shadow DOM
-
原理与作用: Shadow DOM 里的元素无法通过常规的
find_element
找到。需要先找到包含 Shadow DOM 的宿主元素 (Host Element),然后通过 JavaScript 获取它的shadowRoot
,再在shadowRoot
内部进行查找。 -
代码示例:
# 假设宿主元素的 CSS Selector 是 '#host-element'
host_element = driver.find_element(By.CSS_SELECTOR, '#host-element')
# 使用 JavaScript 获取 Shadow Root
shadow_root = driver.execute_script('return arguments[0].shadowRoot', host_element)
# 在 Shadow Root 内部查找表格 (注意:这里是用 shadow_root 对象来调用 find_element)
# 需要使用 shadow_root 的查询方法,通常也是 CSS Selector 比较方便
try:
# 假设表格在 shadow-root 内的 ID 仍然是 'table-container-table'
table_in_shadow = shadow_root.find_element(By.CSS_SELECTOR, '#table-container-table')
print("在 Shadow DOM 中找到了表格!")
# 后续抓取逻辑和普通表格类似,但要确保所有查找都在 shadow_root 或其子元素上进行
# 示例:查找 shadow root 下的 tr
rows = table_in_shadow.find_elements(By.CSS_SELECTOR, 'tbody tr')
# ... 提取数据 ...
except NoSuchElementException:
print("在 Shadow DOM 内部也没找到表格。")
- 如何判断? 在浏览器开发者工具中,如果一个元素含有
#shadow-root (open)
或#shadow-root (closed)
(closed 模式基本无法用 Selenium 操作),那么它的子元素就在 Shadow DOM 里。
三、整合一下,让你的脚本跑起来
回顾你的原始代码,结合上面的分析,推荐的修改思路是:
- 优先尝试方案四 (找 API) :如果能找到 API,这是最佳选择,代码会更简洁、运行更快、更稳定。
- 如果找不到 API 或 API 复杂难搞 :采用方案一和方案二的结合。
- 使用
WebDriverWait
配合EC.visibility_of_element_located
等待表格的关键部分出现(比如_info
或tbody tr
)。 - 实现翻页逻辑,循环抓取每一页的数据。
- 将所有数据汇总到
all_data
列表。 - 最后用
pandas
将all_data
存为 CSV 文件。
- 使用
检查 iFrame 和 Shadow DOM 的可能性,虽然对于这个特定网站看起来概率不大,但也是排查问题时需要考虑的点。
试试这些方法,应该能帮你搞定那个“捉迷藏”的表格!祝你抓取顺利!