最近在学习 selenium,刚好公司内部有一个课程学习的网站,每个人都需要进去看视频学习,还会统计学习进度,正好利用 selenium 实现自动挂机学习。
问题
网站页面有一个课程列表,需要逐个点进去学习。
思路:利用 selenium 找到课程列表元素,通过遍历找出还没学习完成的课程,然后逐个点击课程进行学习。
结果在运行的过程中,经常在学习第二个课程的时候报 StaleElementReferenceException
错误。
分析
经查询, StaleElementReferenceException
是因为要操作的元素已经不在文档中了,导致操作失败。
出现这个问题的原因,是因为获取到这个元素后,页面可能发生了以下变化:
- 当前浏览器的页面已经不是获取这个元素时的页面(比如跳转到了新的页面),或者页面刷新了;
- 当前元素已被删除并重新添加回页面(比如通过 ajax 获取到新的数据,然后用 javascript 构建出新的元素并替换掉原来的);
- 元素是在 iframe 里面的,而 iframe 刷新了。
对比我的情况,是发生第 2 种情况:课程的学习进度是每隔一段时间就进行更新,而更新是通过 javascript 替换掉原来元素,所以导致原本获取到的元素失效了。
解决方法
重新定位元素
经过观察,这个页面的每个列表元素都有唯一 id,而同一元素的 id 即使元素刷新过后还是保持不变的。
因此考虑通过 beautifulsoup 查找出目标元素的 id,再通过 selenium 的 find_element_by_id
方法重新找出元素并进行点击。
这样即使页面刷新了,但还是能通过 find_element_by_id
方法找到最新的元素,而使用 beautifulsoup 查找的效率比直接用 selenium 要高。
while True:
# 使用 BeautifulSoup 分析
bs = BeautifulSoup(driver.page_source, 'lxml')
# 获取目标列表
sections = bs.select('.chapter-list-box')
learn_section_id = None
for section in sections:
# 获取元素的 id
id = section.attrs['id']
state = section.select_one(
'.section-item > .pointer').text.strip()
# 找出未完成的课程
if state == '重新学习':
continue
else:
learn_section_id = id
break
if learn_section_id is None:
break
else:
driver.find_element_by_id(learn_section_id).click()
直接重试
上面的方法经使用后,虽然 StaleElementReferenceException
出现的几率小了,但偶然的情况下还是会继续出现。
可能是因为在找元素和点击的过程间,页面又再刷新了?
最后实在是没有办法,只能通过简单粗暴的方法解决:捕捉此异常,然后直接重试。
经改进后的代码:
while True:
try:
# 使用 BeautifulSoup 分析
bs = BeautifulSoup(driver.page_source, 'lxml')
# 获取目标列表
sections = bs.select('.chapter-list-box')
learn_section_id = None
for section in sections:
# 获取元素的 id
id = section.attrs['id']
state = section.select_one(
'.section-item > .pointer').text.strip()
# 找出未完成的课程
if state == '重新学习':
continue
else:
learn_section_id = id
break
if learn_section_id is None:
break
else:
driver.find_element_by_id(learn_section_id).click()
.......
except StaleElementReferenceException:
continue