最近朋友需要让我帮忙设计能抓取网页特定数据的爬虫,我原以为这种程序实现很简单,只要通过相应的url获得html页面代码,然后解析html获得所需数据即可。但在实践时发现我原来想的太简单,页面上有很多数据根本就无法单纯从html源码中抓取,因为页面展现的很多数据其实是js代码运行时通过ajax的从远程服务器获取后才动态加载页面中,因此无法简单的通过读取html源码获得所需数据。

一个例子是,我们打开京东主页,在搜索框输入关键词”乌鸡白凤丸“在返回的页面上显示的商品条目有60条,如下图:

打开js控制台,选择element,然后点击左上角箭头,然后移动箭头到商品条目上,我们可以看到其在html中对应的元素:

我们可以看到页面显示的商品条目对应id为”gl-i-wrap”的div控件,这意味着如果我们要想从html中抓取页面显示的信息就必须要从html代码中获得给定id的div组件然后分析它里面内容,问题在于如果你使用右键调出他页面源码,然后查找字符串”gl-i-wrap”你会发现它只包含30个,但计算页面上展示的商品数量有60个,也就是有30个商品信息无法直接通过html代码获得。

多余的30个条目信息其实是在一定条件下触发一段js代码后,通过ajax的方式从服务器获取然后再添加到DOM中,于是我们无法单纯从页面对应的html中获取,我通过搜索发现,网上对应的解决办法是分析那一段js代码负责获取这些数据,然后通过类似逆向工程的方式研究它如何构造http请求,然后自己模拟去发送这些请求来获取数据。

我认为这种做法有一系列问题,首先你要分析一大堆很难读懂的js代码,因此在工作量和难度上可想而知,其次这种做法在未来如果网站改变了数据获取方式,那么你又得再次逆向工程才可以,因此这样的做法很不经济。

如何才能简单方便的获取动态加载的数据呢。只要商品信息显示在页面上,那么通过DOM就一定能获取,因此如果我们有办法获取浏览器内部的DOM模型那么就可以读取到动态加载的数据,由于多余的数据是页面下拉后触发给定js代码才通过ajax动态获取,因此如果我们能通过代码的方式控制浏览器加载网页,然后让浏览器对页面进行下拉,然后读取浏览器页面对应的DOM那么就可以获得动态加载的数据。

经过一番调查,我们发现一个叫selenium的控件能通过代码动态控制浏览器,例如让浏览器加载特定页面,让浏览器下拉页面,然后获取浏览器中加载页面的html代码,于是我们可以使用它来方便的抓取动态页面数据。首先通过命令pip install selenium下载该控件,如果我们想要用他来控制chrome浏览器的话,我们还需要下载chromedriver控件,首先确定你使用的chrome版本,chromedriver必须要跟你当前使用的chrome版本完全一致,在下面链接中去下载:

http://npm.taobao.org/mirrors/chromedriver

记住一定要选取与你chrome浏览器版本一致的进行下载,完成后我们可以通过如下代码启动浏览器并加载给定URL的网页:op = webdriver.ChromeOptions()

webdriver.Chrome('/Users/apple/Documents/chromedriver/chromedriver', chrome_options = op)

driver.get('https://www.jd.com/')

运行上面代码后就可以启动浏览器并看到他打开京东主页,此时我想自动在搜索框中输入关键词该怎么做呢,通过html源码发现搜索框对应的id叫”key”因此我们可以通过下面代码把关键词模拟人手输入的方式输入到搜索框,然后再模拟点击回车按钮实现搜索请求:

search_box = driver.find_element_by_id('key')
search_box.send_keys(word)
search_box.send_keys(Keys.ENTER)
timeout = 10
try:
print("wait page...")
WebDriverWait(driver, timeout)
except TimeoutException:
print("Timed out waiting for page to load")
finally:
。。。。

由于浏览器与我们代码运行不再同一个进程,因此我们要调用WebDriverWait等待一段时间让浏览器完全加载页面,接下来为了触发特定Js代码获取到动态加载的数据,我们要模拟人把页面下拉的动作:SCROLL_PAUSE_TIME = 0.5

last_height = driver.execute_script("return document.body.scrollHeight")
while True: #将页面滑动到底部
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(SCROLL_PAUSE_TIME)
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
break
last_height = new_height

上面代码执行后你会发现浏览器的页面自动下拉到底部,于是js会发送ajax请求向服务器获取另外30条商品的数据,然后我们通过执行一段js代码获得body组件对应的html源码,然后获取id为gl-i-wrap的div对象,这时候会看到它返回60个对应组件,这意味着页面上所有商品数据都可以获得:page_source = driver.execute_script("return document.body.innerHTML;")

bs = BeautifulSoup(page_source, 'html.parser')
info_divs = bs.find_all("div", {"class" : "gl-i-wrap"})
print(len(info_divs)) #这里输出结果为60

这样我们就可以读取所有页面上显示的商品价格信息了,这种方法比通过解析js代码然后逆向构造http请求去获取页面动态加载的数据要简单方便和省事得多。更详细的讲解和调试演示请点击’阅读原文‘查看视频