盘点selenium phantomJS使用的坑

网络爬虫

浏览数:268

2019-1-30

AD:资源代下载服务

说到python爬虫,刚开始主要用urllib库,虽然接口比较繁琐,但也能实现基本功能。等见识了requests库的威力后,便放弃urllib库,并且也不打算回去了。但对一些动态加载的网站,经常要先分析请求,再用requests模拟,比较麻烦。直到遇到了selenium库,才发现爬动态网页也可以这么简单,果断入坑!

selenium是python的一个第三方自动化测试库,虽然是测试库,却也非常适合用来写爬虫,而phantomJS是其子包webdriver下面的一个浏览器。phantomJS本身是一个无头浏览器(headless browser),也称无界面浏览器。可以在通过官网下载运行phantomjs.exe,简单几行代码也能访问网页,爬取数据。但本文主要讨论通过python的selenium库使用phantomJS。除了phantomJS浏览器,webdriver还整合了Chrome、Firefox、IE等浏览器,并提供了操作这些浏览器的接口。

由于phantomJS是无界面浏览器,不需要界面的同时占用的内存也相对较小,更适用于大规模多进程爬数据(试想,如果开几十个Chrome进程爬数据,那真是内存噩梦!)。本文主要讨论使用selenium phantomJS过程中遇到的bug,而不是selenium phantomJS使用教程,有需要了解selenium基本用法的同学,请移步官方文档

个人用phantomJS爬数据有一段时间了,爬虫程序也大致完工了,过程中遇到了很多坑,统一总结如下。

1. 查看phantomJS文档

前面提到,phantomJS是selenium子包webdriver下面多个浏览器中的一个,而selenium包对不同的浏览器都提供了统一的接口,所以直接查看selenium的官方文档即可,也有对应的中文文档。文档内容不多,但很全面。遇到不懂的问题,先看文档肯定没错。

这里需要注意的是,百度搜索phantomJS得到的结果只是phantomJS的官方文档,而phantomJS是一个独立的无界面浏览器,也称JS模拟器,本来就独立于python。我们需要的是phantomJS的python接口,也就是通过python调用phantomJS,所以只需查看selenium的webdriver文档。

当然,官方文档很全面,但也相对繁杂。python有个查看文档的小技巧,直接使用help()就能查看某个对象的帮助文档。比如help(driver)即可直接查看driver这个对象的文档,包括其内部函数、变量的说明。如果driver是一个phantomJS对象,那么会显示phantomJS浏览器对象的函数和变量的文档,具体内容和官方文档一样。对所有的python对象都可以这样干,非常便捷。似乎现在各种IDE也有这个功能:当鼠标悬停在某个对象上,就显示该对象的帮助文档。不过多掌握个方法总归没错。

2. phantomJS的配置问题

selenium官方文档中,phantomJS对象的帮助文档很详细,但当涉及phantomJS浏览器的配置,比如user-agent伪装、代理、超时返回等选项时,有用的信息就非常少了。结合网上的资料和自己遇到的各种坑,我总结了常用的phantomJS配置选项,对普通的爬虫来说,应该够用了。

from selenium import webdriver
# 引入配置对象DesiredCapabilities
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
dcap = dict(DesiredCapabilities.PHANTOMJS)
#从USER_AGENTS列表中随机选一个浏览器头,伪装浏览器
dcap["phantomjs.page.settings.userAgent"] = (random.choice(USER_AGENTS))
# 不载入图片,爬页面速度会快很多
dcap["phantomjs.page.settings.loadImages"] = False
# 设置代理
service_args = ['--proxy=127.0.0.1:9999','--proxy-type=socks5']
#打开带配置信息的phantomJS浏览器
driver = webdriver.PhantomJS(phantomjs_driver_path, desired_capabilities=dcap,service_args=service_args)                
# 隐式等待5秒,可以自己调节
driver.implicitly_wait(5)
# 设置10秒页面超时返回,类似于requests.get()的timeout选项,driver.get()没有timeout选项
# 以前遇到过driver.get(url)一直不返回,但也不报错的问题,这时程序会卡住,设置超时选项能解决这个问题。
driver.set_page_load_timeout(10)
# 设置10秒脚本超时时间
driver.set_script_timeout(10)

3. phantomJS的并发问题

phantomJS爬数据比较慢,并发编程几乎是必选项。最初,我考虑采用多线程/协程的方式,毕竟对于这种IO密集型的程序,多线程/协程比较合适。但多次测试下来,程序却遇到各种问题,有时能成功运行,有时却不能。尝试将phantomJS改成Chrome,程序居然能正常运行,这基本确定是phantomJS的锅了。所以,如果需要并发编程提高效率,用Chrome比较好,虽然内存占用相对较多,况且经下面简友提醒,在没界面的主机上也可以跑Chrome,那自然更好了。

在网上仔细查找了相关资料(这玩意的中文资料极少,只能去国外技术论坛潜水),原来phantomJS本身在多线程方面还有很多bug,建议使用多进程,具体什么原因有时间再去了解。

关于多进程,推荐使用multiprocessing库,简洁、高效!下面几行代码便实现了多进程并发。

from multiprocessing import Pool
pool = Pool(8)
data_list = pool.map(get, url_list)
pool.close()
pool.join()

4. phantomJS进程不自动退出问题

话说,一开始我写好程序后,先在本地测试了一段时间,确认程序各方面都没问题后,直接扔阿里云主机上跑了。过了一段时间,查了下程序运行日志,很好,一切如常。于是我就高高兴兴地摸鱼去了。

第二天准备登录主机验收程序时,却发现居然无法登录!啥,无法登录?难不成这个小爬虫程序还能把主机搞崩?我先在阿里云后台查看了主机的运行日志,发现主机的内存使用越来越高,应该是内存耗尽后,强制关机了。似乎是程序没有回收内存,导致占用的内存越来越大。明确大致原因后,就是痛苦的查bug过程了。

由于bug涉及内存的使用,我自然地想到了用top命令查看进程的内存使用情况。先运行程序,然后运行top命令,实时检测程序的内存使用情况。一开始程序占用内存在正常范围,只有一个phantomJS进程在运行,似乎没有什么不对。但随着时间的增长,内存中居然同时有好几个phantomJS进程在运行,内存所剩空间越来越小!但根据程序的逻辑,任何时候都只有一个phantomJS进程在爬数据。我意识到可能是由于phantomJS进程没有正常关闭,所以在内存中驻留的phantomJS进程越来越多,最终吃光了内存。

带着这个问题,我重新检查了一次代码,尤其在程序异常退出的地方。最终找到了类似下面的代码:

try:
    self.driver.get(url)
    self.wait_()
    return True
except Exception as e:
    return False

程序的逻辑是:如果在打开url的过程中报错,那么就返回False,反之返回True。

似乎直接return False的处理太粗心了。我尝试着在return False前加上一行self.driver.quit()。再次运行程序,并用top查看内存使用情况,发现程序的内存使用一直都在正常范围内,并没有出现多个phantomJS进程的情况,问题搞定!后面在网上找到的资料也证实了我的猜想:主程序退出后,selenium不保证phantomJS也成功退出,最好手动关闭phantomJS进程。

5. 其他问题

5.1 不同frame间的转换

有时,phantomJS获得的页面源码的确存在某元素,但通过find_element_by_xpath()等定位函数却无法获得该元素对象,总是提示“元素不存在”的错误。遇到这种情况,除了检查元素节点路径是否正确外,还应该分析页面源码,检查元素是否被包裹在一个特定的frame中,如果是后者,那么在使用查找函数前,需要额外的处理。

比如网页源码中有如下代码:

<iframe id="topmenuFrame" width="100%" scrolling="no" height="100%" src="topmenu.aspx?>
<div id="haha">text</div>
</iframe>

假如你想要获取id="haha"的div标签,直接通过driver.find_element_by_id('haha')就会提示“元素不存在“的错误。

这时需要使用driver.switch_to_frame(driver.find_element_by_id``("topmenuFrame")),即先进入id为topmenuFrame的frame,然后再执行driver.find_element_by_id("haha"),就能正确获得该元素了。

需要注意的是,切换到这个frame之后,只能访问当前frame的内容,如果想要回到默认的内容范围,相当于默认的frame,还需要使用driver.switch_to_default_content()

页面中有多个frame时,要注意frame之间的切换。

5.2 implicit_wait、WebDriverWait不一定靠谱

宿舍哥们用phantomJS爬数据时,遇到了一个匪夷所思的bug。起初,他写了个很简单的程序,从个方面来看都没问题,但实际运行却提示各种错误,让人十分费解。折腾大半天之后,他直接注释掉自己不太熟悉的implicit_wait(),改用time.sleep()作延时,程序居然就能正确运行了!原来implicit_wait()有bug。同样的,对于WebDriverWait,大家使用时也要特别注意。

看来python的selenium库不是很成熟,还存在一些问题,一些函数的实际运行情况并不是预期的那样,在查bug时,要留意这些问题。

6. 总结

总的来说,selenium库简单,容易上手,是爬动态网页的杀手级武器,但对phantomJS浏览器的支持还不是特别完善。当然,了解存在的问题,并找到对应的解决方法,就能发挥phantomJS的威力了。

以上就是我个人这段时间的phantomJS使用小结,虽然不是很全面,也不确保完全准确,算是对我这段学习历程的总结吧,希望对大家有用。

原文地址:https://www.jianshu.com/p/9d408e21dc3a