我们在这里介绍两种解决动态加载的方法,一种是JavaScript 逆向工程,另一种是渲染 JavaScript。
首先,让我们看看什么样的是动态加载。示例使用网址 http://example.python-scraping.com/search,比如我们查找A开头的国家,如下:
我们通过开发者工具查看源码,可以看到我们需要的内容在id=results节点下:
import re
import requests
from bs4 import BeautifulSoup
import lxml.html
import time
from lxml.html import fromstring
#获取网页内容
def download(url,user_agent='wswp',proxy=None,num_retries=2):
print ('Downloading:',url)
headers = {'User-Agent': user_agent}
try:
resp = requests.get(url, headers=headers, proxies=proxy)
html = resp.text
if resp.status_code >= 400:
print('Download error:', resp.text)
html = None
if num_retries and 500 <= resp.status_code < 600:
# recursively retry 5xx HTTP errors
return download(url, num_retries - 1)
except requests.exceptions.RequestException as e:
print('Download error:', e.reason)
html = None
return html
# 提取需要的内容
html = download('http://example.python-scraping.com/search')
tree = fromstring(html)
tree.cssselect('div#results a')
# 输出结果
Downloading: http://example.python-scraping.com/search
[]
从结果看得出,我们并没有获取到我们想要的东西,这是为啥呢,这就是动态加载,打开代码下载的html源码,是找不到我们需要的东西的。结果如下:
看到这里,估计大家对动态渲染有了一定的认识,下面我们介绍如何获取这种页面内容。
我们使用之前的抓取方法,无法抓取到动态加载的页面数据,那想要抓取这部分数据,我们就得了解这种页面是如何加载数据的,该过程就描述为逆向工程。
继续前面的例子,我们在浏览器工具中单击 Network选项卡,然后执行一次搜索,我们将会看到对于给定页面的所有请求,大多数请求都是图片,其中有个search.json的文件,单击我们发现里面包含我们需要的所有数据,如下图:
很容易发现,上图的结果其实是一个json响应,这个东西不仅可以在浏览器中访问,还可以直接下载,响应代码如下:
import requests
resp = requests.get('http://example.python-scraping.com/places/ajax/search.json?&search_term=A&page_size=10&page=0')
resp.json()
# 结果输出
{'records': [{'pretty_link': '',
'country_or_district': 'Afghanistan',
'id': 7969417},
{'pretty_link': '',
'country_or_district': 'Aland Islands',
'id': 7969418},
{'pretty_link': '',
'country_or_district': 'Albania',
'id': 7969419},
{'pretty_link': '',
'country_or_district': 'Algeria',
'id': 7969420},
...}
上面的代码中,我们通过requests 库的 json 方法访问了JSON响应,我们还可以下载原始字符串响应,然后使用json.load进行加载。但是上面的代码有个问题是我们只获取了包含A字母的内容,那如何获取全部的呢?当然可以解决,AJAX使用正则表达式进行匹配,所以我们只需要将url中的search_term=A
换成`search_term=.就可以加载全部的数据。
另外,因为url中page_size=10,所以一页中只有十个数据,这个参数AJAX并不会检查,所以我们可以给一个很大的数,是的所有数据一次性下载完成。
最终的代码如下:
from csv import DictWriter
import requests
template_url = 'http://example.python-scraping.com/places/ajax/search.json?&search_term=.&page_size=1000&page=0'
resp = requests.get(template_url)
data = resp.json()
records = data.get('records')
with open('./countries_or_districts.csv', 'w') as countries_or_districts_file:
wrtr = DictWriter(countries_or_districts_file, fieldnames=records[0].keys())
wrtr.writeheader()
wrtr.writerows(records)
运行上述代码,就可以在响应文件夹下得到如下的数据表:
对于实际中,某一些网站我们能够快速地对 API 的方法进行逆向工程来了解它如何工作,以及如何使用它在一个请求中获取结果。但是,一些网站非常复杂,即使使用高级的浏览器工具也很难理解。这类网站难以实施逆向工程。这就得用到我们这里要讲解到的东西----渲染动态页面。
尽管经过足够的努力,任何网站都可以被逆向工程,不过我们可以使用浏览器渲染引擎避免这些工作,这种渲染引擎是浏览器在显示网页时解析HTML、应用 CSS 样式并执行 JavaScript 语句的部分。在本节中,我们将使用QWebEngineView渲染引擎,通过 Qt 框架可以获得该引擎的一个便捷 Python 接口。
我们可以使用位于http://example.python-scraping.com/dynamic 上的这个简单示例,来演示使用Qt渲染。
常规提取:
import lxml.html
url = 'http://example.python-scraping.com/dynamic'
html = download(url)
tree = lxml.html.fromstring(html)
tree.cssselect('#result')[0].text_content()
# 输出:
Downloading: http://example.python-scraping.com/dynamic
''
使用Qt框架:
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtWebEngineWidgets import QWebEngineView
import lxml.html
class Render(QWebEngineView): # 子类Render继承父类QWebEngineView
def __init__(self, url):
self.html = ''
self.app = QApplication(sys.argv)
super().__init__()
self.loadFinished.connect(self._loadFinished)
self.load(QUrl(url))
self.app.exec_()
def _loadFinished(self):
self.page().toHtml(self.callable)
def callable(self, data):
self.html = data
self.app.quit()
if __name__ == '__main__':
url = 'http://example.python-scraping.com/dynamic'
r = Render(url)
result = r.html
tree = lxml.html.fromstring(result)
a = tree.cssselect('#result')[0].text_content()
print(a)
# 结果输出
Hello World
这里得说明一下,使用Jupyter Notebook会出现ImportError: QtWebEngineWidgets must be imported before a QCoreApplication instance is created
的错误,所以我这里是使用PyCharm演示的,关于上述代码,说明如下:
使用前面小节中的 QWebEngineView,我们可以自定义浏览器渲染引擎,这样就能完全控制想要执行的行为。如果不需要这么高的灵活性,那么还有一个不错的更容易安装的替代品 Selenium 可以选择,它提供的 API 接口可以自动化处理多个常见浏览器。
Selenium 就是模仿人操作浏览器,它可操作的浏览器有多种,比如Firefox (FirefoxDriver)、IE (InternetExplorerDriver)、Opera (OperaDriver) 和 Chrome (ChromeDriver)等,除此之外,它还支持 Android (AndroidDriver)和 iPhone (IPhoneDriver) 的移动应用测试,而且还包括一个基于 HtmlUnit 的无界面实现。本文只讲谷歌驱动,其他有需要自行百度。
因为它是操作浏览器,所以需要下载相应浏览器的驱动,而且,版本也得相同。比如:我现在使用的谷歌浏览器版本是 版本 91.0.4472.124,所以我下载的驱动也必须是91版本开头的驱动,否则无法使用。
谷歌浏览器驱动下载地址:http://npm.taobao.org/mirrors/chromedriver/
下载好驱动后,解压,把解压文件放置在Anaconda\Scripts 目录下,当然如果你用的不是Anaconda,可以放到相应的位置。说白了,这一步就是把驱动放置在系统环境变量的path路径下,完全可以直接把驱动所在的地址添加在环境变量path中。
from selenium import webdriver
driver = webdriver.Chrome()
# 传入url
driver.get("http://www.baidu.com")
# 传递参数,让百度搜索Selenium2
driver.find_element_by_id("kw").send_keys("Selenium2")
# 找到百度一下按钮,点击一下
driver.find_element_by_id("su").click()
# 等待10秒
time.sleep(10)
# 关闭浏览器
driver.quit()
到这里算是了解也体验了selenium的运行机制,下次学习如何定位元素,如何传递参数。
既然要模拟人操作浏览器,那总得知道输入什么,点哪里?这种问题就是它的定位,selenium有多种定位方式,分别是元素定位、Xpath定位和Css定位,每种方式都有自己的特点,实际使用时,可以多种方式混合使用。
篇幅问题,则合理不再细讲相应的操作,网上一搜一大堆,感兴趣的自行百度。不过下面有个我当时学习的Jupyter 文件,供大家参考。
Selenium 入门 Jupyter演示