写在前面的话
这些日子写过不少爬虫,想说些自己对于爬虫的理解,与本文无关,仅想学爬取JavaScript页面的同学可跳过。
在我看来,爬虫代码是"不优雅"的。当然,这里说的不是爬虫的代码结构的不优雅,scrapy的spider item pipelines midderware结构简洁且清晰。我指的是编写爬取网站的逻辑,也就是自定义的spider,是不“优雅”的。爬虫的代码并不是为了可复用而设计的,它存在的意义有且只有一个,就是为了获取网站的数据。作为本职工作并不需要写爬虫的我来说,大多数时候,当我们获取到我们想要的数据的时候,这份代码就失去了存在的意义,不会也不需要去维护它。所以,写爬虫的时候,很大程度上,是“不择手段”的。你不需要考虑代码的可维护性,可读性,你可以写一些很“怪”的逻辑,这些代码可能看起来毫无意义,但是只有结合网页,才能真正理解他的意思。
此外,解决爬虫bug的方法,也是“不择手段”的。文末贴了些爬取示例时有意思的bug,有兴趣可以看一下。
一.简介
读过我上篇教程(【Lighthouse教程】scrapy爬虫初探)的同学,应该已经对如何编写爬虫有了一定的认识.但是对于较为复杂的网站设计,比如网站页面使用了JavaScript动态渲染,入门级的爬虫就不太适用了。本文针对JavaScript动态渲染页面,使用selenium scrapy,爬取levels.fyi中微软公司员工的信息和薪酬(示例页面点击这里),目的在于讲述JavaScript页面如何进行爬取。程序部署在腾讯云轻量服务器Lighthouse中。
二.Lighthouse购买
这里简单介绍一下Lighthouse的购买.先放一段Lighthouse官网的简介:
‘‘轻量应用服务器(Lighthouse)是一种易于使用和管理、适合承载轻量级业务负载的云服务器,能帮助中小企业及开发者在云端快速构建网站、博客、电商、论坛等各类应用以及开发测试环境,并提供应用部署、配置和管理的全流程一站式服务,极大提升构建应用的体验,是您使用腾讯云的最佳入门途径.’’
购买地址点击这里
Lighthouse提供多种镜像供你选择,这里我们先选择LAMP镜像,选择套餐后进行支付,就拥有了属于你的Lighthouse镜像!
三.页面分析
levels.fyi中进入开发者模式,可以看到待爬取的元素其实是一个iframe,数据由script脚本生成:
我们需要获取tbody下的每一个tr,并选择我们需要的数据
我们直接使用Request获取tbody,会发现该元素下并没有任何数据:
代码语言:txt复制t_body = response.css("table#compTable tbody").extract()
print(t_body)
下面,我们讲解下如何成功的获取javaScript生成的tbody数据
四.Selenium获取
Selenium是一个web自动化工具,运行在浏览器中,使用脚本模拟用户对浏览器进行操作。在本例中,本质上是使用Selenium等待javascript加载完成后,再获取数据。Selenium的安装和配置非常简单,脚本编写也非常容易。而缺点在于,相比起其他爬取方式,Selenium的爬取速度相对较慢。
Selenium安装:pip install selenium
浏览器驱动下载:使用Selenium需要下载浏览器驱动,推荐下载Chrome版本,下载完成后mac可以直接放在/usr/local/bin,Windows需要在脚本里配置下路径或者配置环境变量
建立scrapy项目,新建MicrosoftSpider,并进行简单的配置,方法和上篇教程【Lighthouse教程】scrapy爬虫初探一样,不加以赘述。
获取driver对象:
代码语言:txt复制driver = webdriver.Chrome()
使用WebDriverWait,等待页面加载完成。在这里,我们设置超时时间为5秒:
代码语言:txt复制wait = WebDriverWait(self.driver, 5)
wait.until(
lambda driver: driver.find_element_by_xpath('//*[@id="compTable"]/tbody/tr[1]')) # 等待第一行内容加载完成
wait结束后,获取一下tbody中的第一行数据试试?
代码语言:txt复制tr1 = self.driver.find_element_by_xpath('//*[@id="compTable"]/tbody/tr[1]').text # 每一行信息
print(tr1)
OK!我们已经成功获取到第一行数据了!在上述代码中,我们使用了find_element_by_xpath函数。这个函数是Selenium中获取元素的函数,返回的是WebElement类型,可以通过text获取元素的文本
接下来,我们使用同样的方法,获取‘下一页’按钮,并点击该按钮:
代码语言:txt复制wait = WebDriverWait(self.driver, 1)
wait.until(lambda driver: driver.find_element_by_css_selector('li.page-item.page-next')) # 等待内容加载完成
next_page = self.driver.find_element_by_css_selector('li.page-item.page-next a')
next_page.click() # 模拟点击下一页
next_page同样是WebElement类型。可以看到,WebElement除了text等基本属性外,还具有click这样的动作。实际上,这也是WebElement最常使用的方式。
其余方法可见WebElement API文档
OK!现在,你已经获取了所有关键的元素了!接下来,就是爬取每一行的元素,并进行循环点击啦!
五.爬虫的路上总是充满坎坷
Selenium的教程到这里其实已经结束了,但是如果有小伙伴去尝试爬取网站的活,就会发现各种各样神奇的bug。这些bug不是程序的问题,而是现在有着各种各样神奇的网站。这些网站的设计者们脑海里可能有个哪吒在闹海,让你根本想不明白他在想什么。在这里,分享一下在爬取这个示例网站的时候,我遇到的那些有意思的事儿。
1.JavaScript嵌套:
就像下面这张图,当你点击iframe的一行时,会出来一个新的iframe,数据同样是由JavaScript生成的。获取新的iframe数据并不难,wait find就可以得到。难点在于,当每一行都点击的时候,你要如何把新出现的iframe和他所属的iframe关联起来呢?毕竟,像下图一样,每个新出现的iframe的class都是"detail-view"。最开始时候,我想做两个map,然后join,但是很难保证join后的结果是正确的。后来,我发现了新的Iframe的特点:当再次点击该行数据时,新的Iframe会被关闭。这样,就有了取巧的办法:在循环爬取数据的时候,每次生成新的iFrame,并爬取数据后,再次调用click,把Iframe关闭。这样,就可以保证每次根据'detail-view'获取元素的时候,就只有唯一的一个Iframe。
这个解决办法看起来毫无技术含量,并且增加了爬取的总耗时:增加了一个click动作的耗时。但是就像在开头说的一样:写爬虫的时候,总是“不择手段”的。当你可以很快的想到一个解决爬虫问题的方法,并且很容易实现时,你就可以大胆的使用他,哪怕它并不是最优的解决办法。毕竟,在你费脑筋想到更好的解决办法时,使用“笨”办法爬取的数据可能已经到手了。
2.爬取中断:
如果你尝试爬取示例网站的时候,你会发现,爬虫在爬取到1000余条的时候,会被中断,同时提示:元素‘page-link’无法被点击,也就是点击不了‘下一页’按钮。
最开始的时候,我以为是那一页数据缺少了‘下一页’按钮的href,毕竟,类似按钮缺少href,链接突然变成text这样的事情实在是太普遍了。但是,在我找到该页数据的时候,我发现并不是这样的。该页数据看起来非常的正常,‘下一页’按钮也是具有href,可以被正常点击的。但是在我重复爬取了多次后,在爬取到该页数据时爬虫均会中断,同时提示我元素‘page-link’无法被点击。这个问题困扰了我很久,直到我发现了这个东西:
这是个可以和网站客服人员联系的按钮,在第125页的时候,他神奇的出现在了‘下一页’按钮的上方,遮挡住了‘下一页’按钮,导致模拟器无法点击到‘下一页’按钮。这个发现也是让我有些哭笑不得。那么,发现了这个问题,要如何解决呢?办法其实非常的简单,把模拟器的窗口调大。因为‘聊天按钮‘的位置是依据当前窗口大小,也就是相对位置,而’下一页‘按钮不一样。这样,就可以保证让两个按钮分开:
数据也就可以正常爬取了。
之所以写出这两个例子,是因为这两个解决方法其实都属于‘怪逻辑’:一个莫名其妙的点击,和一个莫名其妙的调大窗口。如果你仅仅去看代码,你不会明白这两行代码的意义在哪里。但是对于这个网站而言,这两行又是必需的。就像文章开头提到的‘不择手段’,在解决问题的时候,换一种角度去思考,可能会比从正面解决要容易得多。
五.代码地址
最后附上示例代码的github地址:https://github.com/Pro-YY/baby-steps-to-the-cloud