5.2 对动态网页进行逆向工程
到目前为止,我们抓取网页数据使用的都是第2章中介绍的方法。但是,该方法在本章的示例网页中无法正常运行,因为该网页中的数据是使用JavaScript动态加载的。要想抓取该数据,我们需要了解网页是如何加载该数据的,该过程也被称为逆向工程。继续上一节的例子,在Firebug中单击Console 选项卡,然后执行一次搜索,我们将会看到产生了一个AJAX请求,如图5.3所示。
这个AJAX数据不仅可以在搜索网页时访问到,也可以直接下载,如下面的代码所示。
>>> html = D('http://example.webscraping.com/ajax/ search.json?page=0&page_size=10&search_term=a')
图5.3
AJAX响应返回的数据是JSON格式的,因此我们可以使用Python的json 模块将其解析成一个字典,其代码如下所示。
>>> import json >>> json.loads(html) {u'error': u'', u'num_pages': 22, u'records': [{u'country': u'Afghanistan', u'id': 1261, u'pretty_link': u'<div><a href="/view/Afghanistan-1"><img src="/places/static/images/flags/af.png" /> Afghanistan</a></div>'}, ...] }
现在,我们得到了一个简单的方法来抓取包含字母A的国家。要想获取所有国家的信息,我们需要对字母表中的每个字母调用一次AJAX搜索。而且对于每个字母,搜索结果还会被分割成多个页面,实际页数和请求时的page_size 相关。保存结果时还会遇到一个问题,那就是同一个国家可能会在多次搜索时返回,比如Fiji 会匹配f 、i 、j 三次搜索结果。这些重复的搜索结果需要过滤处理,这里采用的方法是在写入表格之前先将结果存储到集合中,因为集合这种数据类型不会存储重复的元素。
下面是其实现代码,通过搜索字母表中的每个字母,然后遍历JSON响应的结果页面,来抓取所有国家信息。其产生的结果将会存储在表格当中。
import json import string template_url = 'http://example.webscraping.com/ajax/ search.json?page={}&page_size=10&search_term={}' countries = set() for letter in string.lowercase: page = 0 while True: html = D(template_url.format(page, letter)) 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)))
这个AJAX接口提供的抽取国家信息的方法,比第2章中介绍的抓取方法更简单。这其实是一个日常经验:依赖于AJAX的网站虽然乍看起来更加复杂,但是其结构促使数据和表现层分离,因此我们在抽取数据时会更加容易。
5.2.1 边界情况
前面的AJAX搜索脚本非常简单,不过我们还可以利用一些边界情况使其进一步简化。目前,我们是针对每个字母执行查询操作的,也就是说我们需要26次单独的查询,并且这些查询结果又有很多重复。理想情况下,我们可以使用一次搜索查询就能匹配所有结果。接下来,我们将尝试使用不同字符来测试这种想法是否可行。如果将搜索条件置为空,其结果如下。
>>> url = 'http://example.webscraping.com/ajax/ search.json?page=0&page_size=10&search_term=' >>> json.loads(D(url))['num_pages'] 0
很不幸,这种方法并没有奏效,我们没有得到返回结果。下面我们再来尝试' * ' 是否能够匹配所有结果。
>>> json.loads(D(url + '*'))['num_pages'] 0
依然没有奏效。现在我们再来尝试下'.' ,这是正则表达式里用于匹配所有字符的元字符。
>>> json.loads(D(url + '.'))['num_pages'] 26
这次尝试成功了,看来服务端是通过正则表达式进行匹配的。因此,现在可以把依次搜索每个字符替换成只对点号搜索一次了。
此外,你可能已经注意到在AJAX的URL中有一个用于设定每个页面显示国家数量的参数。搜索界面中包含4、10、20这几种选项,其中默认值为10。因此,提高每个页面的显示数量到最大值,可以使下载次数减半。
>>> url = 'http://example.webscraping.com/ajax/ search.json?page=0&page_size=20&search_term=.' >>> json.loads(D(url))['num_pages'] 13
那么,要是使用比网页界面选择框支持的每页国家数更高的数值又会怎样呢?
>>> url = 'http://example.webscraping.com/ajax/ search.json?page=0&page_size=1000&search_term=.' >>> json.loads(D(url))['num_pages'] 1
显然,服务端并没有检查该参数是否与界面允许的选项值相匹配,而是直接在一个页面中返回了所有结果。许多Web应用不会在AJAX后端检查这一参数,因为它们认为请求只会来自Web界面。
现在,我们手工修改了这个URL,使其能够在一次请求中下载得到所有国家的数据。进一步简化之后,抓取所有国家信息的实现代码如下。
FIELDS = ('area', 'population', 'iso', 'country', 'capital', 'continent', 'tld', 'currency_code', 'currency_name', 'phone', 'postal_code_format', 'postal_code_regex', 'languages', 'neighbours') writer = csv.writer(open('countries.csv', 'w')) writer.writerow(FIELDS) html = D('http://example.webscraping.com/ajax/ search.json?page=0&page_size=1000&search_term=.') ajax = json.loads(html) for record in ajax['records']: row = [record[field] for field in FIELDS] writer.writerow(row)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论