在本文中,我们将分析几个真实网站,来看看我们在《用Python写网络爬虫(第2版)》中学过的这些技巧是如何应用的。首先我们使用Google演示一个真实的搜索表单,然后是依赖JavaScript和API的网站Facebook,接下来是典型的在线商店Gap。由于这些都是活跃的网站,因此读者在阅读本书时这些网站存在已经发生变更的风险。
[德] 凯瑟琳,雅姆尔 著
不过这样也好,因为本文示例的目的是为了向你展示如何应用前面所学的技术,而不是展示如何抓取任何网站。当你选择运行某个示例时,首先需要检查网站结构在示例编写后是否发生过改变,以及当前该网站的条款与条件是否禁止了爬虫。
在本文中,我们将介绍如下主题:
抓取Google搜索结果网页;
调研Facebook的API;
在Gap网站中使用多线程;
“Google搜索引擎”
为了了解我们对CSS选择器知识的使用情况,我们将会抓取Google的搜索结果。根据中Alexa的数据,Google是全世界最流行的网站之一,而且非常方便的是,该网站结构简单,易于抓取。
图1.1所示为Google搜索主页使用浏览器工具加载查看表单元素时的界面。
图1.1
可以看到,搜索查询存储在输入参数q当中,然后表单提交到action属性设定的/search路径。我们可以通过将test、作为搜索条件提交给表单对其进行测试,此时会跳转到类似https://www.google.ro/?gws_ rd=cr,ssl&ei=TuXYWJXqBsGsswHO8YiQAQ#q=test&*的 URL中。确切的URL取决于你的浏览器和地理位置。此外,如果开启了Google实时,那么搜索结果会使用AJAX执行动态加载,而不再需要提交表单。虽然URL中包含了很多参数,但是只有用于查询的参数q是必需的。
可以看到,搜索查询存储在输入参数q当中,然后表单提交到action属性设定的/search路径。我们可以通过将test作为搜索条件提交给表单对其进行测试,此时会跳转到类似https://www.google.ro/?gws_ rd=cr,ssl&ei=TuXYWJXqBsGsswHO8YiQAQ#q=test&*的URL中。确切的URL取决于你的浏览器和地理位置。此外,如果开启了Google实时,那么搜索结果会使用AJAX执行动态加载,而不再需要提交表单。虽然URL中包含了很多参数,但是只有用于查询的参数q是必需的。
当URL为https://www.google.com/search?q=test时,也能产生相同的搜索结果,如图1.2所示。
图1.2
搜索结果的结构可以使用浏览器工具来查看,如图1.3所示。
图1.3
从图1.3中可以看出,搜索结果是以链接的形式出现的,并且其父元素是class为"r"的<h3>标签。
想要抓取搜索结果,我们可以使用第2章中介绍的CSS选择器。
1>>> from lxml.html import fromstring
2>>> import requests
3>>> html = requests.get('https://www.google.com/search?q=test')
4>>> tree = fromstring(html.content)
5>>> results = tree.cssselect('h3.r a')
6>>> results
7<Element a at 0x7f3d9affeaf8>,
8 <Element a at 0x7f3d9affe890>,
9 <Element a at 0x7f3d9affe8e8>,
10 <Element a at 0x7f3d9affeaa0>,
11 <Element a at 0x7f3d9b1a9e68>,
12 <Element a at 0x7f3d9b1a9c58>,
13 <Element a at 0x7f3d9b1a9ec0>,
14 <Element a at 0x7f3d9b1a9f18>,
15 <Element a at 0x7f3d9b1a9f70>,
16 <Element a at 0x7f3d9b1a9fc8>
到目前为止,我们已经下载得到了Google的搜索结果,并且使用lxml抽取出其中的链接。在图1.3中,我们发现链接中的真实网站URL之后还包含了一串附加参数,这些参数将用于跟踪点击。
下面是我们在页面中找到的第一个链接。
1>>> link = results[0].get('href')
2>>> link
3 '/url?q=http://www.speedtest.net/&sa=U&ved=0ahUKEwiCqMHNuvbSAhXD6gTMAA&usg=
4 AFQjCNGXsvN-v4izEgZFzfkIvg'
这里我们需要的内容是http://www.speedtest.net/,可以使用urlparse模块从查询字符串中将其解析出来。
1>>> from urllib.parse import parse_qs, urlparse
2>>> qs = urlparse(link).query
3>>> parsed_qs = parse_qs(qs)
4>>> parsed_qs
5 {'q': ['http://www.speedtest.net/'],
6 'sa': ['U'],
7 'ved': ['0ahUKEwiCqMHNuvbSAhXD6gTMAA'],
8 'usg': ['AFQjCNGXsvN-v4izEgZFzfkIvg']}
9>>> parsed_qs.get('q', [])
10 ['http://www.speedtest.net/']
该查询字符串解析方法可以用于抽取所有链接。
1>>> links = []
2>>> for result in results:
3... link = result.get('href')
4... qs = urlparse(link).query
5... links.extend(parse_qs(qs).get('q', []))
6...
7>>> links
8 ['http://www.speedtest.net/',
9 'test',
10 'https://www.test.com/',
11 'https://ro.wikipedia.org/wiki/Test',
12 'https://en.wikipedia.org/wiki/Test',
13 'https://www.sri.ro/verificati-va-aptitudinile-1',
14 'https://www.sie.ro/AgentiaDeSpionaj/test-inteligenta.html',
15 'http://www.hindustantimes.com/cricket/india-vs-australia-live-cricket-scor
16 e-4th-test-dharamsala-day-3/story-8K124GMEBoiKOgiAaaB5bN.html',
17 'https://sports.ndtv.com/india-vs-australia-2017/live-cricket-score-india-v
18 s-australia-4th-test-day-3-dharamsala-1673771',
19 'http://pearsonpte.com/test-format/']
成功了!从Google搜索中得到的链接已经被成功抓取出来了。该示例的完整源码位于本书源码文件的chp9文件夹中,其名为scrape_google.py。
抓取Google搜索结果时会碰到的一个难点是,如果你的IP出现可疑行为,比如下载速度过快,则会出现验证码图像,如图1.4所示。
我们可以降低下载速度,或者在必须高速下载时使用代理,以避免被Google怀疑。过分请求Google会造成你的IP甚至是一个IP段被封禁,几个小时甚至几天无法访问Google的域名,所以请确保你能够礼貌地使用该网站,不会使你的家庭或办公室中的其他人(包括你自己)被列入黑名单。
图1.4
“Facebook”
为了演示浏览器和API的使用,我们将会研究Facebook的网站。目前,从月活用户数维度来看,Facebook是世界上最大的社交网络之一,因此其用户数据非常有价值。
1.2.1 网站
图1.5所示为Packt出版社的Facebook页面。
当你查看该页的源代码时,可以找到最开始的几篇日志,但是后面的日志只有在浏览器滚动时才会通过AJAX加载。另外,Facebook还提供了一个移动端界面,正如第1章所述,这种形式的界面通常更容易抓取。该页面在移动端的展示形式如图1.6所示。
图1.5
图1.6
当我们与移动端网站进行交互,并使用浏览器工具查看时,会发现该界面使用了和之前相似的结构来处理AJAX事件,因此该方法无法简化抓取。虽然这些AJAX事件可以被逆向工程,但是不同类型的Facebook页面使用了不同的AJAX调用,而且依据我的过往经验,Facebook经常会变更这些调用的结构,所以抓取这些页面需要持续维护。因此,如第5章所述,除非性能十分重要,否则最好使用浏览器渲染引擎执行JavaScript事件,然后访问生成的HTML页面。
下面的代码片段使用Selenium自动化登录Facebook,并跳转到给定页面的URL。
1 from selenium import webdriver
2
3 def get_driver():
4 try:
5 return webdriver.PhantomJS()
6 except:
7 return webdriver.Firefox()
8
9 def facebook(username, password, url):
10 driver = get_driver()
11 driver.get('https://facebook.com')
12 driver.find_element_by_id('email').send_keys(username)
13 driver.find_element_by_id('pass').send_keys(password)
14 driver.find_element_by_id('loginbutton').submit()
15 driver.implicitly_wait(30)
16 # wait until the search box is available,
17 # which means it has successfully logged in
18 search = driver.find_element_by_name('q')
19 # now logged in so can go to the page of interest
20 driver.get(url)
21 # add code to scrape data of interest here ...
然后,可以调用该函数加载你感兴趣的Facebook页面,并使用合法的Facebook邮箱和密码,抓取生成的HTML页面。
1.2.2 Facebook API
抓取网站是在其数据没有给出结构化格式时的最末之选。而Facebook确实为绝大多数公共或私有(通过你的用户账号)数据提供了API,因此我们需要在构建加强的浏览器抓取之前,首先检查一下这些API提供的访问是否已经能够满足需求。
首先要做的事情是确定通过API哪些数据是可用的。为了解决该问题,我们需要先查阅其API文档。开发者文档的网址为https://developers.facebook.com/docs,在这里给出了所有不同类型的API,包括图谱API,该API中包含了我们想要的信息。如果你需要构建与Facebook的其他交互(通过API或SDK),可以随时查阅该文档,该文档会定期更新并且易于使用。
此外,根据文档链接,我们还可以使用浏览器内的图谱API探索工具,其地址为https://developers.facebook.com/tools/explorer/。如图1.7所示,探索工具是用来测试查询及其结果的很好的地方。
图1.7
在这里,我可以搜索API,获取PacktPub的Facebook页面ID。图谱探索工具还可以用来生成访问口令,我们可以用它来定位API。
想要在Python中使用图谱API,我们需要使用具有更高级请求的特殊访问口令。幸运的是,有一个名为facebook-sdk(https://facebook-sdk.readthedocs.io)的维护良好的库可以供我们使用。我们只需通过pip安装它即可。
1 pip install facebook-sdk
下面是使用Facebook的图谱API从Packt出版社页面中抽取数据的代码示例。
1 In [1]: from facebook import GraphAPI
2
3 In [2]: access_token = '....' # insert your actual token here
4
5 In [3]: graph = GraphAPI(access_token=access_token, version='2.7')
6
7 In [4]: graph.get_object('PacktPub')
8 Out[4]: {'id': '204603129458', 'name': 'Packt'}
我们可以看到和基于浏览器的图谱探索工具相同的结果。我们可以通过传递想要抽取的额外信息,来获得页面中的更多信息。要确定使用哪些信息,我们可以在图谱文档中看到页面中所有可用的字段,文档地址为https://developers.facebook.com/docs/graph-api/reference/page/。使用关键字参数fields,我们可以从API中抽取这些额外可用的字段。
1 In [5]: graph.get_object('PacktPub', fields='about,events,feed,picture')
2 Out[5]:
3 'about': 'Packt provides software learning resources, from eBooks to video
4 courses, to everyone from web developers to data scientists.',
5 'feed': {'data': [{'created_time': '2017-03-27T10:30:00+0000',
6 'id': '204603129458_10155195603119459',
7 'message': "We've teamed up with CBR Online to give you a chance to win 5
8 tech eBooks - enter by March 31! http://bit.ly/2mTvmeA"},
9...
10 'id': '204603129458',
11 'picture': {'data': {'is_silhouette': False,
12 'url':
13'https://scontent.xx.fbcdn.net/v/t1.0-1/p50x50/14681705_10154660327349459_7
14 2357248532027065_n.png?oh=d0a26e6c8a00cf7e6ce957ed2065e430&oe=59660265'}}}
我们可以看到该响应是格式良好的Python字典,我们可以很容易地进行解析。
图谱API还提供了很多访问用户数据的其他调用,其文档可以从Facebook的开发者页面中获取,网址为https://developers.facebook.com/docs/graph-api。根据所需数据的不同,你可能还需要创建一个Facebook开发者应用,从而获得可用时间更长的访问口令。
“Gap”
为了演示使用网站地图查看内容,我们将使用Gap的网站。
Gap拥有一个结构化良好的网站,通过Sitemap可以帮助网络爬虫定位其最新的内容。如果我们使用第1章中学到的技术调研该网站,则会发现在http://www.gap.com/robots.txt这一网址下的robots.txt文件中包含了网站地图的链接。
1 Sitemap: http://www.gap.com/products/sitemap_index.xml
下面是链接的Sitemap文件中的内容。
1<?xml version="1.0" encoding="UTF-8"?>
2<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3 <sitemap>
4 <loc>http://www.gap.com/products/sitemap_1.xml</loc>
5 <lastmod>2017-03-24</lastmod>
6 </sitemap>
7 <sitemap>
8 <loc>http://www.gap.com/products/sitemap_2.xml</loc>
9 <lastmod>2017-03-24</lastmod>
10 </sitemap>
11</sitemapindex>
如上所示,Sitemap链接中的内容不仅仅是索引,其中又包含了其他Sitemap文件的链接。这些其他的Sitemap文件中则包含了数千种产品类目的链接,比如http://www.gap.com/products/womens-jogger- pants.jsp,如图1.8所示。
图1.8
这里有大量需要爬取的内容,因此我们将使用第4章中开发的多线程爬虫。你可能还记得该爬虫支持URL模式以匹配页面。我们同样可以定义一个scraper_callback关键字参数变量,可以让我们解析更多链接。
下面是爬取Gap网站中Sitemap链接的示例回调函数。
1 from lxml import etree
2 from threaded_crawler import threaded_crawler
3
4 def scrape_callback(url, html):
5 if url.endswith('.xml'):
6 # Parse the sitemap XML file
7 tree = etree.fromstring(html)
8 links = [e[0].text for e in tree]
9 return links
10 else:
11 # Add scraping code here
12 pass
该回调函数首先检查下载到的URL的扩展名。如果扩展名为.xml,则认为下载到的URL是Sitemap文件,然后使用lxml的etree模块解析XML文件并从中抽取链接。否则,认为这是一个类目URL,不过本例中还没有实现抓取类目的功能。现在,我们可以在多线程爬虫中使用该回调函数来爬取gap.com了。
1 In [1]: from chp9.gap_scraper_callback import scrape_callback
2
3 In [2]: from chp4.threaded_crawler import threaded_crawler
4
5 In [3]: sitemap = 'http://www.gap.com/products/sitemap_index.xml'
6
7 In [4]: threaded_crawler(sitemap, '[gap.com]*',
8 scraper_callback=scrape_callback)
9 10
10 [<Thread(Thread-517, started daemon 140145732585216)>]
11 Exception in thread Thread-517:
12 Traceback (most recent call last):
13 ...
14 File "src/lxml/parser.pxi", line 1843, in lxml.etree._parseMemoryDocument
15 (src/lxml/lxml.etree.c:118282)
16 ValueError: Unicode strings with encoding declaration are not supported.
17 Please use bytes input or XML fragments without declaration.
不幸的是,lxml期望加载来自字节或XML片段的内容,而我们存储的是Unicode的响应(因为这样可以让我们使用正则表达式进行解析,并且可以更容易地存储到磁盘中)。不过,我们依然可以在本函数中访问该URL。虽然效率不高,但是我们可以再次加载页面;如果我们只对XML页面执行该操作,则可以减少请求的数量,从而不会增加太多加载时间。当然,如果我们使用了缓存的话,也可以提高效率。
下面我们将重写回调函数。
1 import requests
2
3 def scrape_callback(url, html):
4 if url.endswith('.xml'):
5 # Parse the sitemap XML file
6 resp = requests.get(url)
7 tree = etree.fromstring(resp.content)
8 links = [e[0].text for e in tree]
9 return links
10 else:
11 # Add scraping code here
12 pass
现在,如果我们再次尝试运行,可以看到执行成功。
1 In [4]: threaded_crawler(sitemap, '[gap.com]*',
2 scraper_callback=scrape_callback)
3 10
4 [<Thread(Thread-51, started daemon 139775751223040)>]
5 Downloading: http://www.gap.com/products/sitemap_index.xml
6 Downloading: http://www.gap.com/products/sitemap_2.xml
7 Downloading: http://www.gap.com/products/gap-canada-fran?ais-index.jsp
8 Downloading: http://www.gap.co.uk/products/index.jsp
9 Skipping
10 http://www.gap.co.uk/products/low-impact-sport-bras-women-C1077315.jsp due
11 to depth Skipping
12 http://www.gap.co.uk/products/sport-bras-women-C1077300.jsp due to depth
13 Skipping
14 http://www.gap.co.uk/products/long-sleeved-tees-tanks-women-C1077314.jsp
15 due to depth Skipping
16 http://www.gap.co.uk/products/short-sleeved-tees-tanks-women-C1077312.jsp
17 due to depth ...
和预期一致,Sitemap文件首先被下载,然后是服装类目。在网络爬虫项目中,你会发现自己可能需要修改及调整代码和类,以适应新的问题。这只是从互联网上抓取内容时诸多令人兴奋的挑战之一。
本文摘自《用Python写网络爬虫(第2版)》
[德] 凯瑟琳,雅姆尔 著
史上首本Python网络爬虫图书全新升级,针对Python 3.x编写,提供示例完整源码和实例网站搭建源码。
讲解了如何使用Python来编写网络爬虫程序,内容包括网络爬虫简介,从页面中抓取数据的3种方法,提取缓存中的数据,使用多个线程和进程进行并发抓取,抓取动态页面中的内容,与表单进行交互,处理页面中的验证码问题,以及使用Scarpy和Portia进行数据抓取,并在最后介绍了使用本书讲解的数据抓取技术对几个真实的网站进行抓取的实例,旨在帮助读者活学活用书中介绍的技术。
小福利
关注【异步社区】服务号,转发本文至朋友圈或 50 人以上微信群,截图发送至异步社区服务号后台,并在文章底下留言你对python爬虫或者试读本书感受,我们将选出3名读者赠送《用Python写网络爬虫(第2版)》1本,赶快积极参与吧!(参与活动直达微信端Python爬虫技巧(文末福利))
活动截止时间:2018年8月2日
扫码关注我们
在“异步社区”后台回复“关注”,即可免费获得2000门在线视频课程
文章转载自公众号
阅读原文,购买《用Python写网络爬虫》