Python 网络爬虫

爬虫:一段自动抓取互联网信息的程序,从互联网上抓取对于我们有价值的信息。
Python 爬虫架构主要由五个部分组成:
调度器、URL管理器、网页下载器、网页解析器、应用程序(爬取的有价值数据)。

更多:爬虫

目录:
一、什么是爬虫
二、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()

发表评论

邮箱地址不会被公开。 必填项已用*标注