更多:爬虫…
目录:
一、什么是爬虫
二、Python爬虫架构
三、一个简单的Web爬虫/蜘蛛/机器人
四、urllib2 实现下载网页的三种方式
五、第三方库 Beautiful Soup 的安装
六、使用 Beautiful Soup 解析 html 文件
一、什么是爬虫
爬虫:一段自动抓取互联网信息的程序,从互联网上抓取对于我们有价值的信息。
二、Python爬虫架构
Python 爬虫架构主要由五个部分组成,分别是调度器、URL管理器、网页下载器、网页解析器、应用程序(爬取的有价值数据)。
- 调度器:相当于一台电脑的CPU,主要负责调度URL管理器、下载器、解析器之间的协调工作。
- URL管理器:包括待爬取的URL地址和已爬取的URL地址,防止重复抓取URL和循环抓取URL,实现URL管理器主要用三种方式,通过内存、数据库、缓存数据库来实现。
- 网页下载器:通过传入一个URL地址来下载网页,将网页转换成一个字符串,网页下载器有urllib2(Python官方基础模块)包括需要登录、代理、和cookie,requests(第三方包)
- 网页解析器:将一个网页字符串进行解析,可以按照我们的要求来提取出我们有用的信息,也可以根据DOM树的解析方式来解析。网页解析器有正则表达式(直观,将网页转成字符串通过模糊匹配的方式来提取有价值的信息,当文档比较复杂的时候,该方法提取数据的时候就会非常的困难)、html.parser(Python自带的)、beautifulsoup(第三方插件,可以使用Python自带的html.parser进行解析,也可以使用lxml进行解析,相对于其他几种来说要强大一些)、lxml(第三方插件,可以解析 xml 和 HTML),html.parser 和 beautifulsoup 以及 lxml 都是以 DOM 树的方式进行解析的。
- 应用程序:就是从网页中提取的有用数据组成的一个应用。
下面用一个图来解释一下调度器是如何协调工作的:
三、一个简单的 Web 爬虫/蜘蛛/机器人
一个稍微复杂的 Web 客户端例子就是网络爬虫(又称蜘蛛或机器人)。这些程序可以为了不同目的在因特网上探索和下载页面,其中包括以下几个目的。
- 为 Google 和 Yahoo 这类大型的搜索引擎创建索引。
- 离线浏览,即将文档下载到本地硬盘,重新设定超链接,为本地浏览器创建镜像。
- 下载并保存历史记录或归档。
- 缓存 Web 页面,节省再次访问 Web 站点的下载时间。
示例 9-3 中的 crawl.py 用来通过起始 Web 地址(URL),下载该页面和其他后续链接页面,但是仅限于那些与开始页面有相同域名的页面。如果没有这个限制,会耗尽硬盘上的空间!
示例 9-3 Web 爬虫(crawl.py)
这个爬虫含有两个类:一个用于管理整个爬虫进程(Crawler),另一个获取并解析每个下载到的 Web 页面(Retriever)(这个示例对本书第 2 版中相同的示例进行了重构)。
#!/usr/bin/env python import cStringIO import formatter from htmllib import HTMLParser import httplib import os import sys import urllib import urlparse class Retriever(object): __slots__ = ('ur1', 'file') def __init__ (self, ur1): self.url, self.file = self.get_ file(url) def get_fi1e(self, url, default= 'index. htm1'): 'Create usable 1oca1 filename from URL ' parsed = urlparse.url parse(url) host = parsed.net1oc. split('@')[-1].split(':')[O] filepath = '%s%s' % (host, parsed . path) if not os. path.splitext(parsed. path) [1]: filepath = os. path. join(filepath, defau1t) linkdir = os . path. di rname(filepath) if not os. path. isdir(linkdir): if os. path. exists(linkdir): os . unlink(1inkdir) os . makedi rs (1inkdir) return url, filepath def download(se1f): . ' Download URL to specific named file' try: retval = url1ib. urlretrieve(self.url, se1f.file) except (I0Error, httplib. InvalidURL) as e: retva1 = (('*t* ERROR: bad URL "%s": %s' % ( self.url, e)),) return retval def parse_ 1inks(se1f): 'Parse out the 1inks found in downloaded HTML fi1e' f = open(self.file, 'r') data = f. read() f.close() parser = HTMLParser(formatter.AbstractFormatter( formatter .DumbWriter(cStringI0. StringI0()))) parser . feed(data) parser.close() return parser . anchorlist class Crawler(object) : count = 0 def_ init_ (self, ur1): self.q = [url] self.seen = set() parsed = urlparse. ur1parse(ur1) host = parsed.net1oc.split('@'[-1].split(':')[0] self.dom = '.'.join(host.split('.')[-2:]) def get_ page(se1f, url, media=Fa1se) : ' Download page & parse links, add to queue if nec' r = Retriever(url) fname = r . download() [O] if fname[0] == '*': print fname,' ... skipping parse'return Crawler.count += 1 print 'n(', Crawler .count, ')' print 'URL:', ur1 print 'FILE:', fname self.seen.add(url) ftype = os. path. splitext(fname) [1] if ftype not in (' .htm', ' .htm1 ' ): return for link in r.parse_ 1inks(): if 1ink.startswith('mai1to: '): print ' ... discarded, mai1to 1ink' continue if not media: ftype = os . path. splitext(link) [1] if ftype in ('.mp3', ' .mp4', ' .m4v',. ' .wav'): print '.. discarded, media file' continue if not link. startswith('http://'): link = urlparse . urljoin(url, 1ink) print '*',link, if link not in se1f.seen: if self.dom not in link: print '... discarded, not in domain' e1se: if link not in self.q: self.q. append(1 ink) print '.. new, added to Q' else: print'discarded, a1ready in Q' else: print ' ... discarded, already processed' def go(se1f, media=False): 'Process_ next page in queue (if any) ' while self.q: ur1 = self.q. pop() self.get_ page(url, media) def main(): if 1en(sys.argv) > 1: url = sys.argv[1] else: try: url = raw_ input('Enter starting URL: ') except (KeyboardInterrupt, EOFError): url = '' if not ur1 : return if not url .startswith('http://') and \ not url.startswith(' ftp://'): ur1 = 'http://%s/' % ur1 robot = Crawler(url) robot . go() if __name__ == '__main__' : main()
逐行解释
第 1~10 行
该脚本的起始部分包括Python 在UNIX 上标准的初始化行,同时导入一些程序中会用到的模块和包。下面是一些简单的解释。
- cStringIO、formatter、htmllib:使用这些模块中不同的类来解析 HTML。
- httplib:使用这个模块中定义的一个异常。
- os:该模块提供了许多文件系统方面的函数。
- sys:使用其中提供的 argv 来处理命令行参数。
- urllib:使用其中的 urlretrive()函数来下载 Web 页面。
- urlparse:使用其中的 urlparse()和 urljoin()函数来处理 URL。
第 12~29 行
Retriever 类的任务是从 Web 下载页面,解析每个文档中的链接并在必要的时候把它们加入“to-do”队列。这里为从网上下载的每个页面都创建一个 Retriever 类的实例。Retriever 中有若干方法,用来实现相关功能:构造函数(__init__())、get_file()、download()、和 parse_links()。
暂时跳过一些内容,先看 get_file(),这个方法将指定的 URL 转成本地存储的更加安全的文件,即从 Web 上下载这个文件。基本上,这其实就是将 URL 的 http://前缀移除,丢掉任何为获取主机名而附加的额外信息,如用户名、密码和端口号(第 20 行)。
没有文件扩展名后缀的 URL 将添加一个默认的 index.html 文件名,调用者可以重写这个文件名。可以在第 21~23 行了解其工作方式,并最终创建了filepath。
最后得到最终的目标路径(第 24 行),检测它是否为目录。如果是,则不管它,返回“URL-文件路径”键值对。如果进入了 if 子句,这意味着路径名要么不存在,要么是个普通文件。如果是普通文件,需要将其删除并重新创建同名目录。最终,使用第 28 行的 os.makedirs() 创建目标目录及其所有父目录。
现在回到初始化函数__init__()中。在这里创建了一个 Retriever 对象,将 get_file()返回的 URL 字符串和对应的文件名作为实例属性存储起来。在当前的设计中,每个下载下来的文件都会创建一个实例。而 Web 站点会含有许多文件,为每个文件都创建对象实例会导致额外的内存问题。为了降低资源开销,这里创建了 slots 变量,表示实例只能拥有 self.url 和 self.file 属性。
第 31~49 行
这里先稍微了解一下爬虫,这个爬虫很聪明地为每个下载下来的文件创建一个 Retriever 对象。顾名思义,download()方法通过给定的链接在因特网上下载对应的页面(第 34 行)。并将 URL 作为参数来调用 urllib.urlretreive(),将其另存为文件名(即 get_file()返回的)。
如果下载成功,则返回文件名(第 34 行),如果出错,则返回一个以“***”开头的错误提示字符串(第 35~36 行)。爬虫检查这些返回值,如果没有出错,则调用 parse_linkes()解析刚下载下来的页面链接。
在这部分中更重要的方法是 parse_linkes()。是的,爬虫的任务是下载 Web 页面,但递归的爬虫(现在这个就是递归的)会查看所有下载下来的页面中是否含有额外的链接,并进行处理。它首先打开下载到的 Web 页面,将所有 HTML 内容提取成单个字符串(第 42~44 行)。第 45~49 行的内容是一个常见的代码片段,其中使用了 htmllib.HTMLParse 类。这个代码片段是Python 程序员代代相传的,现在传到了读者这里。
这段代码的工作方式中,最重要的是 parser 类不进行 I/O,它只处理一个 formatter 对象。Python 只有一个 formatter 对象,即 formatter.AbstractFormatter,用来解析数据并使用 writer 对象来分配其输出内容。同样,Python 只有一个有用的 writer 对象 ,即 formatter.DumbWriter。可以为该对象提供一个可选的文件对象,表示将输出写入文件。如果不提供这个文件对象, 则会写入标准输出,但后者一般不是所期望的。为了不让输出写到标准输出,先实例化一个cStringIO 对象。StringIO 对象会吸收掉这些输出(如果读者对类 UNIX 系统有所了解,就类似其中的/dev/null)。可以在网上搜索这些类名,找到类似的代码片段和注释。
由于 htmllib.HTMLParser 很老,从Python 2.6 开始弃用了。下一小节将介绍使用另一个较新的工具写更加小巧的示例。这里先继续使用 htmllib.HTMLParser,因为这是个常见的代码片段,而且依然能正确地完成工作。
总之,创建解析器的所有复杂问题都由一个简单的调用完成(第 45~46 行)。这一部分剩下的代码用于在HTML 中进行解析、关闭解析器并将解析后的链接/锚作组成列表返回。
第 51~59 行
Crawler 类是这次演示中的“明星”,管理一个 Web 站点的完整抓爬过程。如果为应用程序添加线程,就可以为每个待抓爬的站点分别创建实例。Crawler 的构造函数在实例化过程中存储了 3 样东西,第一个是 self.q,是一个待下载的链接队列。这个队列的内容在运行过程中会有变化,有页面处理完毕就缩短,在每个下载的页面中发现新的链接则会增长。
Crawler 包含的另两个数值是 self.seen,这是所有已下载链接的一个集合。另一个是self.dom,用于存储主链接的域名,并用这个值来判定后续链接的域名与主域名是否一致。所有这三个值都在初始化方法 init ()中创建,参见第 54~59 行。
注意,在第 58 行使用 urlparse.urlparse()解析域名,与在 Retriever 中通过 URL 抓取主机名的方式相同。域名是主机名的最后两部分。因为主机名在这里并没什么用,所以可以将第58 行和第 59 行连起来写,但这样可读性就大大降低了。
self.dom = '.'.join(urlparse.urlparse( url).netloc.split('@')[-1].split(':')[0].split('.')[-2:])
在 init ()的上方,Crawler 还有一个名为 count 的静态数据项。这是一个计数器,用于保持追踪从因特网上下载下来的对象数目。每成功下载一个网页,这个变量就递增 1。
第 61~105 行
除了构造函数以外,Crawler 还有另外一对方法,分别是 get_page()和 go()。go()是一个简单的方法,用于启动Crawler。go()在代码的 main 部分调用。go()中含有一个循环,用于将队列中所有待下载的新链接处理完毕。而这个类中真正埋头苦干的是 get_page()方法。
get_page()使用第一个链接实例化一个 Retriever 对象,然后开始处理。如果页面成功下载, 则递增计数器(否则,在第 65~67 行忽略发生的错误)并将该链接添加到已下载的集合中(第72 行)。使用集合是因为加入的链接顺序不重要,同时集合的查找速度比列表快。
get_page()会查看每个下载完成的页面(在第 73~75 行会跳过所有非 Web 页面)中的所有链接,判断是否需要向列表中添加更多的链接(第 77~79 行)。go()中的主循环会继续处理链接,直到队列为空,此时会声明处理成功(第 103~105)。
其他域名的链接(第 90~91 行),或已经下载的链接(第 98~99 行),已位于队列中等待处理的链接(第 96~97 行)、邮箱链接等,都会被忽略,不会添加到队列中(第 78~80 行)。媒体文件也会被忽略(第 81~85 行)。
第 107~124 行
main()需要一个 URL 来启动处理。如果在命令行指定了一个 URL(例如,这个脚本被直接调用时,如第 108~109 行所示),就会使用这个指定的 URL。否则,脚本进入交互模式,提示用户输入初始 URL。一旦有了初始始链接,就会实例化 Crawler 并启动(第 120~121 行)。
下面是一个调用 crawl.py 的例子。
$ crawl.py Enter starting URL: http://www.ai8py.com/home/index.html
( 1 )
URL: http://www.ai8py.com/home/index.html
FILE: www.ai8py.com/home/index.html
- http://www.ai8py.com/home/overview.html … new, added to Q
- http://www.ai8py.com/home/synopsis.html … new, added to Q
- http://www.ai8py.com/home/order.html … new, added to Q
- mailto:postmaster@ai8py.com … discarded, mailto link
- http://www.ai8py.com/home/overview.html … discarded, already in Q
- http://www.ai8py.com/home/synopsis.html … discarded, already in Q
- http://www.ai8py.com/home/order.html … discarded, already in Q
- mailto:postmaster@ai8py.com … discarded, mailto link
- http://bogus.com/index.html … discarded, not in domain
( 2 )
URL: http://www.ai8py.com/home/order.html FILE: www.ai8py.com/home/order.html
- mailto:postmaster@ai8py.com … discarded, mailto link
- http://www.ai8py.com/home/index.html … discarded, already processed
- http://www.ai8py.com/home/synopsis.html … discarded, already in Q
- http://www.ai8py.com/home/overview.html … discarded, already in Q
( 3 )
URL: http://www.ai8py.com/home/synopsis.html FILE: www.ai8py.com/home/synopsis.html
- http://www.ai8py.com/home/index.html … discarded, already processed
- http://www.ai8py.com/home/order.html … discarded, already processed
- http://www.ai8py.com/home/overview.html … discarded, already in Q
( 4 )
URL: http://www.ai8py.com/home/overview.html FILE: www.ai8py.com/home/overview.html
- http://www.ai8py.com/home/synopsis.html … discarded, already processed
- http://www.ai8py.com/home/index.html … discarded, already processed
- http://www.ai8py.com/home/synopsis.html … discarded, already processed
- http://www.ai8py.com/home/order.html … discarded, already processed
执行后,在本地系统文件中会创建一个名为www.ai8py.com 的目录,及一个名为 home 的子目录。home 目录中会含有所有处理过的 HTML 文件。
如果在阅读了这些代码后,依然想找到一些使用 Python 编写的爬虫。可以去查看原先的Google Web 爬虫,这个爬虫是用 Python 编写的。更多信息参见 http://infolab.stanford. edu/~backrub/google.html。
四、urllib2 实现下载网页的三种方式
#!/usr/bin/python # -*- coding: UTF-8 -*- import cookielib import urllib2 url = "https://www.ai8py.com" response1 = urllib2.urlopen(url) print "第一种方法" #获取状态码,200表示成功 print response1.getcode() #获取网页内容的长度 print len(response1.read()) print "第二种方法" request = urllib2.Request(url) #模拟Mozilla浏览器进行爬虫 request.add_header("user-agent","Mozilla/5.0") response2 = urllib2.urlopen(request) print response2.getcode() print len(response2.read()) print "第三种方法" cookie = cookielib.CookieJar() #加入urllib2处理cookie的能力 opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie)) urllib2.install_opener(opener) response3 = urllib2.urlopen(url) print response3.getcode() print len(response3.read()) print cookie
五、第三方库 Beautiful Soup 的安装
Beautiful Soup: Python 的第三方插件用来提取 xml 和 HTML 中的数据,官网地址 https://www.crummy.com/software/BeautifulSoup/
1、安装 Beautiful Soup
打开 cmd(命令提示符),进入到 Python(Python2.7版本)安装目录中的 scripts 下,输入 dir 查看是否有 pip.exe, 如果用就可以使用 Python 自带的 pip 命令进行安装,输入以下命令进行安装即可:
pip install beautifulsoup4
2、测试是否安装成功
编写一个 Python 文件,输入:
import bs4 print bs4
运行该文件,如果能够正常输出则安装成功。
六、使用 Beautiful Soup 解析 html 文件
#!/usr/bin/python # -*- coding: UTF-8 -*- import re from bs4 import BeautifulSoup html_doc = """ <html><head><title>The Dormouse's story</title></head> <body> <p class="title"><b>The Dormouse's story</b></p> <p class="story">Once upon a time there were three little sisters; and their names were <a href="https://ai8py.com/elsie" class="sister" id="link1">Elsie</a>, <a href="https://ai8py.com/lacie" class="sister" id="link2">Lacie</a> and <a href="https://ai8py.com/tillie" class="sister" id="link3">Tillie</a>; and they lived at the bottom of a well.</p> <p class="story">...</p> """ #创建一个BeautifulSoup解析对象 soup = BeautifulSoup(html_doc,"html.parser",from_encoding="utf-8") #获取所有的链接 links = soup.find_all('a') print "所有的链接" for link in links: print link.name,link['href'],link.get_text() print "获取特定的URL地址" link_node = soup.find('a',href="https://ai8py.com/elsie") print link_node.name,link_node['href'],link_node['class'],link_node.get_text() print "正则表达式匹配" link_node = soup.find('a',href=re.compile(r"ti")) print link_node.name,link_node['href'],link_node['class'],link_node.get_text() print "获取P段落的文字" p_node = soup.find('p',class_='story') print p_node.name,p_node['class'],p_node.get_text()