python爬虫-->抓取动态内容

上几篇博文讲的都是关于抓取静态网页的相关内容,但是现在市面上绝大多数主流网站都在其重要功能中依赖JavaScript,使用JavaScript时,不再是加载后立即下载所有页面内容,这样就会造成许多网页在浏览器中展示的内容不会出现在html源码中。这时候再用前几篇博文中介绍的办法爬取来数据,得到的数据肯定为空。本篇博文将主要介绍对如动态网页应该如何进行爬取。

这里我们将介绍两种办法来抓取动态网页数据
① JavaScript逆向工程
② 渲染JavaScript

本篇博文主要思路如下图:

这里写图片描述

打开http://example.webscraping.com/places/default/search,我们在name框输入A。得到搜索结果页面如下:

这里写图片描述

如右侧可以看出谷歌浏览器的控制生成了对应结果。那么我们用前几篇博文介绍的方法来对countries(国家名称)数据进行爬取试试。

import lxml.html
from new_chapter3.downloader import Downloader
D=Downloader()
html=D('http://example.webscraping.com/places/default/search')
tree=lxml.html.fromstring(html)
print tree.cssselect('div#result a')

输出结果
这里写图片描述

那么为什么抓取失败了呢?打开网页源码可以帮助我们了解失败的原因。

这里写图片描述

可以清楚的看出,我们要抓取的内容在源码中实际为空!!!可以看出谷歌浏览器控制台显示的是网页当前的状态,也就是使用JavaScript动态加载完搜索结果之后的网页。而这个网页不会出现在html源码中。

这里需要介绍下ajax:
AJAX即“Asynchronous Javascript And XML”(异步JavaScript和XML),是指一种创建交互式网页应用的网页开发技术。
AJAX = 异步 JavaScript和XML(标准通用标记语言的子集)。
AJAX 是一种用于创建快速动态网页的技术。
通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
传统的网页(不使用 AJAX)如果需要更新内容,必须重载整个网页页面。

对动态网页的逆向工程

某些网页数据是通过JavaScript动态加载的,要想抓取该数据,我们需要了解网页是如何加载该数据的,该过程称为逆向工程。这里我们使用谷歌浏览器,再次打开http://example.webscraping.com/places/default/search,按F12进入开发者模式,在name框输入字母A,点击搜索。然后在点击右边控制台Network,观察其XHR下的数据
这里写图片描述

可以看到当执行一次搜索以后,将会产生一个ajax请求,该ajax数据可以直接下载。通过复制链接我们可以得到http://example.webscraping.com/places/ajax/search.json?&search_term=A&page_size=10&page=0
我们直接下载该链接对应的网页看看:

from new_chapter3.downloader import Downloader
import json
D=Downloader()
## 这里每搜索后的结果中,每个页面5个国家信息,显示的是第一个页面,也就是首先的5个国家。
html=D('http://example.webscraping.com/places/ajax/search.json?&search_term=a&page_size=5&page=0')
print json.loads(html)

打印结果:
这里写图片描述

结果中的records中有page_size个国家信息。

通过搜索字母表中的每个字母,可以抓取下所有的国家信息。然后将其结果存储到表格中。

#coding:utf-8

import json
import string
import new_chapter3.downloader as Downloader
import new_chapter3.mongo_cache as MongoCache

def main():
    template_url = 'http://example.webscraping.com/places/ajax/search.json?&search_term={}&page_size=10&page={}'
    countries = set()
    cache=MongoCache.MongoCache()
    cache.clear()
    download = Downloader.Downloader(cache=cache)

    for letter in string.lowercase:
        page = 0
        while True:
            html = download(template_url.format(letter,page))
            try:
                ajax = json.loads(html)
            except ValueError as e:
                print e
                ajax = None
            else:
                for record in ajax['records']:
                    countries.add(record['country'])
            page += 1
            if ajax is None or page >= ajax['num_pages']:
                break

    open('countries.txt', 'w').write('\n'.join(sorted(countries)))


if __name__ == '__main__':
    main()

这里写图片描述

这里面需要注意的是,下载这么多网页要注意延迟一会,否则会报too many requests。不过这个download函数里面已经有同一域名内下载不同网页延迟的功能。

这里我们依次遍历了26个字母,这个办法抓取所有数据显得比较笨,我们可以用.来代替所有字母,这样就可以一次性获取到所有数据。

import json
import csv
import new_chapter3.downloader as downloader


def main():
    writer = csv.writer(open('countries2.csv', 'w'))
    D = downloader.Downloader()
    html = D('http://example.webscraping.com/places/ajax/search.json?&search_term=.&page_size=1000&page=0')
    ajax = json.loads(html)
    for record in ajax['records']:
        writer.writerow([record['country']])


if __name__ == '__main__':
    main()

这里写图片描述

比起上面26个字母慢慢抓取的办法,这种办法不仅代码量少,而且很快。

渲染动态网页

有些网站很复杂,不太容易对其进行逆向工程进行解析来获取数据。我们可以利用浏览器渲染引擎来对网页进行渲染。这种渲染引擎是在浏览器在显示网页时解析html,应用css样式并执行JavaScript语句的部分。这里使用Webkit渲染引擎,通过QT框架可以获取该引擎的python接口。

为了确认Webkit能执行JavaScript语句,打开http://example.webscraping.com/places/default/dynamic这个简单示例,其网页源码为:

这里写图片描述

我们先尝试用传统方法下载原始html来获取数据:

import lxml.html
from new_chapter3.downloader import Downloader
import json
D=Downloader()
html=D('http://example.webscraping.com/places/default/dynamic')
tree=lxml.html.fromstring(html)
print "结果为: ",tree.cssselect('#result')[0].text_content()

打印结果为:
这里写图片描述

结果为空,解析其html获取不了JavaScript内的数据。我们再尝试使用webkit来获取数据:

#coding:utf-8

try:
    from PySide.QtGui import *
    from PySide.QtCore import *
    from PySide.QtWebKit import *
except ImportError:
    from PyQt4.QtGui import *
    from PyQt4.QtCore import *
    from PyQt4.QtWebKit import *
import lxml.html
import new_chapter3.downloader as downloader


def direct_download(url):
    download = downloader.Downloader()
    return download(url)

def webkit_download(url):
    ##初始化QApplication对象,在其他QT对象完成初始化之前。QT框架需要先创建该对象
    app = QApplication([])
    ##创建QWebView对象,该对象是Web文档的容器
    webview = QWebView()
    webview.loadFinished.connect(app.quit)
    webview.load(url)
    app.exec_() # delay here until download finished
    return webview.page().mainFrame().toHtml()


def parse(html):
    tree = lxml.html.fromstring(html)
    print tree.cssselect('#result')[0].text_content()


def main():
    url = 'http://example.webscraping.com/places/default/dynamic'
    #parse(direct_download(url))
    parse(webkit_download(url))
    return
    print len(r.html)


if __name__ == '__main__':
    main()

打印结果:
这里写图片描述

成功获取到JavaScript内数据,这种方式就是利用webKit执行JavaScript,然后访问生成的html,然后再进行解析获取数据。

上面介绍的浏览器引擎还不能抓取搜索页面,为此我们还需要对浏览器渲染引擎进行扩展,使其支持与网站的交互功能

对与上面的ajax搜索示例,下面给出另外一个版本,支持交互功能。

#coding:utf-8

try:
    from PySide.QtGui import QApplication
    from PySide.QtCore import QUrl, QEventLoop, QTimer
    from PySide.QtWebKit import QWebView
except ImportError:
    from PyQt4.QtGui import QApplication
    from PyQt4.QtCore import QUrl, QEventLoop, QTimer
    from PyQt4.QtWebKit import QWebView
import lxml.html as lm

def main():
    '''
    首先设置搜索参数和模拟动作事件,获取在此参数和动作下搜索后得到的网页
    然后在这网页下,获取数据
    '''
    app = QApplication([])
    webview = QWebView()
    loop = QEventLoop()
    webview.loadFinished.connect(loop.quit)
    webview.load(QUrl('http://example.webscraping.com/places/default/search'))
    loop.exec_()

    webview.show()## 显示渲染窗口,,可以直接在这个窗口里面输入参数,执行动作,方便调试
    frame = webview.page().mainFrame()
    ## 设置搜索参数
    # frame.findAllElements('#search_term') ##寻找所有的search_term框,返回的是列表
    # frame.findAllElements('#page_size option:checked')
    # ## 表单使用evaluateJavaScript()方法进行提交,模拟点击事件
    # frame.findAllElements('#search')

    frame.findFirstElement('#search_term').setAttribute('value', '.') ##第一个search_term框
    frame.findFirstElement('#page_size option:checked').setPlainText('1000') ##第一个page_size框
    ## 表单使用evaluateJavaScript()方法进行提交,模拟点击事件
    frame.findFirstElement('#search').evaluateJavaScript('this.click()') ##第一个点击框

    ## 轮询网页,等待特定内容出现
    ## 下面不断循环,直到国家链接出现在results这个div元素中,每次循环都会调用app.processEvents()
    ##用于给QT事件执行任务的时间,比如响应事件和更新GUI
    elements = None
    while not elements:
        app.processEvents()
        elements = frame.findAllElements('#results a') ##查找下载网页内的所有a标签
    countries = [e.toPlainText().strip() for e in elements] ##取出所有a标签内的文本内容
    print countries


if __name__ == '__main__':
    main()

为了提高代码的易用性,我们把使用到的方法封装到一个类中:

#coding:utf-8

import re
import csv
import time

try:
    from PySide.QtGui import QApplication
    from PySide.QtCore import QUrl, QEventLoop, QTimer
    from PySide.QtWebKit import QWebView
except ImportError:
    from PyQt4.QtGui import QApplication
    from PyQt4.QtCore import QUrl, QEventLoop, QTimer
    from PyQt4.QtWebKit import QWebView
import lxml.html


class BrowserRender(QWebView):
    def __init__(self, display=True):
        self.app = QApplication([])
        QWebView.__init__(self)
        if display:
            ## 显示渲染窗口,,可以直接在这个窗口里面输入参数,执行动作,方便调试
            self.show()  # show the browser

    def open(self, url, timeout=60):
        """Wait for download to complete and return result"""
        loop = QEventLoop()
        timer = QTimer() ## 设置定时器
        timer.setSingleShot(True)
        timer.timeout.connect(loop.quit)
        self.loadFinished.connect(loop.quit)
        self.load(QUrl(url))
        timer.start(timeout * 1000)
        loop.exec_()  # delay here until download finished
        if timer.isActive(): ##如果定时器还是活跃,那么说明下载没有超时
            # downloaded successfully
            timer.stop()
            return self.html() ##网页下载完成后,将其转成html,方便在html上进行数据抽取
        else:
            # timed out
            print 'Request timed out:', url

    def html(self):
        """Shortcut to return the current HTML"""
        return self.page().mainFrame().toHtml()

    def find(self, pattern):
        """Find all elements that match the pattern"""
        """因为这个网页中每个框的名字都不一样,故findFirstElement和findAllElements没有什么区别"""
        ##只不过findFirstElement返回是一个一个元素
        ## findFirstElement返回是一个列表,用这个列表时需要遍历
        return self.page().mainFrame().findFirstElement(pattern)
        #return self.page().mainFrame().findFirstElement(pattern)

    def attr(self, pattern, name, value):
        """Set attribute for matching elements"""
        self.find(pattern).setAttribute(name, value)
        # for e in self.find(pattern):
        #     e.setAttribute(name, value)

    def text(self, pattern, value):
        """Set attribute for matching elements"""
        self.find(pattern).setPlainText(value)
        # for e in self.find(pattern):
        #     e.setPlainText(value)

    def click(self, pattern):
        """Click matching elements"""
        self.find(pattern).evaluateJavaScript("this.click()")
        # for e in self.find(pattern):
        #     e.evaluateJavaScript("this.click()")

    def wait_load(self, pattern, timeout=60):
        """Wait for this pattern to be found in webpage and return matches"""
        ## 设置定时器,跟踪等待时间,并在截止事件前取消循环,否则当网络出现问题时,事件会无休止的运行下去
        deadline = time.time() + timeout
        while time.time() < deadline:
            self.app.processEvents()
            matches = self.page().mainFrame().findAllElements(pattern)
            if matches:
                return matches
        print 'Wait load timed out'


def main():
    '''
   首先设置搜索参数和模拟动作事件,获取在此参数和动作下搜索后得到的网页
   然后在这网页下,查找相关内容
   '''
    br = BrowserRender()
    br.open('http://example.webscraping.com/places/default/search')
    br.attr('#search_term', 'value', '.')
    br.text('#page_size option:checked', '1000')
    br.click('#search')

    ##设置定时器,跟踪等待时间,并在截止事件前取消循环,否则当网络出现问题时,事件会无休止的运行下去
    elements = br.wait_load('#results a')
    writer = csv.writer(open('countries1.csv', 'w'))
    #for country in [e.toPlainText().strip() for e in elements]:
    for country in [e.toPlainText().strip() for e in elements]:
        print country,
        writer.writerow([country])


if __name__ == '__main__':
    main()
发布了91 篇原创文章 · 获赞 103 · 访问量 25万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览