[翻译]抓取JavaScript渲染页面的基本指南

原文链接:Ultimate guide for scraping JavaScript rendered web pages


请输入图片描述

我们会抓取网页。作为响应返回的HTML内容里有我们的数据,我们为了某个特定的结果去抓取它。如果网页里有JavaScript实现,原始数据会在渲染处理后才能得到。在这种情况下,当我们使用正常的请求包时,返回的响应中没有包含数据。浏览器知道怎么渲染并显示最终的结果,但程序怎样才会知道呢?因此我想到一个有力的方法,可以轻松抓取任何JavaScript渲染的页面。

大多数人选择用下面的工具进行抓取。

  1. Lxml
  2. BeautifulSoup

我在这里并没有提到scrapy或者dragline框架,因为它们基础的抓取器是lxml。我更偏爱的是lxml。为啥?它有可遍历元素的方法而不仅仅是像BeautifulSoup一样依靠正则表达方法。下面我将做一个很有意思的样例。在发现我的文章出现在最近的147期PyCoders周报里,我很惊喜。因此我将从PyCoders的归档里抓取所有有用的链接作为一个例子。PyCoders周报归档的链接在这里:

http://pycoders.com/archive/

请输入图片描述

它完全是JavaScript渲染的页面。我想要那些归档的所有链接和各个归档链接里面的所有链接。怎么做呢?首先我将展示当使用HTTP方法时,它将什么都不返回给我。

import requests
from lxml import html

#storing response
response = requests.get('http://pycoders.com/archive')
#creating lxml tree from response body
tree = html.fromstring(response.text)

#Finding all anchor tags in response
print tree.xpath('//divass="campaign"]/a/@href')

当我运行时,得到的是下面的结果:

请输入图片描述

只返回给我三个链接。怎么可能?因为PyCoder周报里有将近130个归档。因此我在响应里什么也没有得到。现在我开始考虑解决这个问题。

我们怎么得到内容?

有一个方法可以从JS渲染的网页中得到数据。它就是使用Webkit库。Webkit库可以做浏览器可以做的所有事。对于有些浏览器,Webkit将是其渲染网页的基本元素。Webkit是QT库的一部分。因此如果你已经安装了QT库和PyQT4,那么你可以用了。

你可以使用下面的命令下载:

sudo apt-get install python-qt4

现在万事俱备了。我们重试获取的步骤,但使用不同的方法。

下面就是解决方法

我们首先通过webkit发起请求。我们等待所有内容完整载入后,把完整HTML返回给一个变量。然后我们用lxml爬去那个HTML内容,并得到结果。这个过程有点慢,但你将惊喜得看到内容可以完美的获得。

我们看下这段获取代码

import sys  
from PyQt4.QtGui import *  
from PyQt4.QtCore import *  
from PyQt4.QtWebKit import *  
from lxml import html 

class Render(QWebPage):  
  def __init__(self, url):  
    self.app = QApplication(sys.argv)  
    QWebPage.__init__(self)  
    self.loadFinished.connect(self._loadFinished)  
    self.mainFrame().load(QUrl(url))  
    self.app.exec_()  
  
  def _loadFinished(self, result):  
    self.frame = self.mainFrame()  
    self.app.quit() 

Render类渲染网页。QWebPage是输入的要抓取网页的URL。它会做一些处理,不用在意那里的细节。记住当我们创建了一个Render对象时,它载入所有的内容,并创建一个包含所有网页信息的结果。

url = 'http://pycoders.com/archive/'  
#This does the magic.Loads everything
r = Render(url)  
#result is a QString.
result = r.frame.toHtml()

我们把HTML的结果保存到result变量。它不是lxml可处理的字符串类型。因此我们在使用lxml之前要先对它进行处理。

#QString should be converted to string before processed by lxml
formatted_result = str(result.toAscii())

#Next build lxml tree from formatted_result
tree = html.fromstring(formatted_result)

#Now using correct Xpath we are fetching URL of archives
archive_links = tree.xpath('//divass="campaign"]/a/@href')
print archive_links

它会返回给我们所有归档的链接,逐个显示链接的结果。

请输入图片描述

那么下一步就是创建Render对象,使用这些链接作为URL,提取需要的内容。Webkit提供给我们爬去网页进而获取数据的实用能力。所以使用这一技术,从任意的JavaScript渲染的网页获取数据吧。

完整代码如下:

import sys  
from PyQt4.QtGui import *  
from PyQt4.QtCore import *  
from PyQt4.QtWebKit import *  
from lxml import html 

#Take this class for granted.Just use result of rendering.
class Render(QWebPage):  
  def __init__(self, url):  
    self.app = QApplication(sys.argv)  
    QWebPage.__init__(self)  
    self.loadFinished.connect(self._loadFinished)  
    self.mainFrame().load(QUrl(url))  
    self.app.exec_()  
  
  def _loadFinished(self, result):  
    self.frame = self.mainFrame()  
    self.app.quit()  

url = 'http://pycoders.com/archive/'  
r = Render(url)  
result = r.frame.toHtml()
#This step is important.Converting QString to Ascii for lxml to process
archive_links = html.fromstring(str(result.toAscii()))
print archive_links

我向你们完整展示了爬取JavaScript渲染网页功能的方法。在爬取框架中应用这项技术来自动化任意步骤,或者集成这项技术,并覆盖默认的方法。虽然它很慢,但100%有效。我希望你喜欢这篇博文。在任何你觉得需要些手段才能爬取的网页上试一试吧。

祝好!


译者注

试了下这个代码,但显示的结果是<Element html at 0x7f0290023998>,和原博文中的截图不一样。看result = r.frame.toHtml()确实得到了被JS渲染后的页面内容,因此应该是lxml中的html的问题了。对lxml不熟,但发现archive_links有个iterlinks函数,得到的是页面里的所有链接的一个迭代器,简单测试结果如下:

请输入图片描述

有时间好好研究下lxml