20.3 高级Web客户端
本节演示网络爬虫。它是按照一定的规则,自动地抓取万维网信息的程序或者脚本。
在本节的演示程序中,抓取Web的开始页面地址,下载与开始页面相同域名的后续链接页面。
#-*-coding: utf-8-*-
from sys import argv
from os import makedirs, unlink, sep
from os.path import dirname, exists, isdir, splitext
from string import replace, find, lower
from htmllib import HTMLParser # HTMLParser是用来解析html页面的,解析html页面中的链接?
from urllib import urlretrieve
from urlparse import urlparse, urljoin
from formatter import DumbWriter, AbstractFormatter # DumbWriter将事件流转换为存文本文档?AbstractFormatter?
from cStringIO import StringIO # StringIO是指在内存中读写字符串
# 类Retrieve负责从web下载页面
class Retrieve(object): # download Web pages
def __init__(self, url):
self.url = url
self.file = self.filename(url)
# filename()方法使用给定的url找出安全、有效的相关文件名并储存在本地
def filename(self, url, deffile='index.html'):
parsedurl = urlparse(url, 'http:', 0) # 解析路径
path = parsedurl[1] + parsedurl[2] # web页面(服务器位置+文件路径)
ext = splitext(path) # 返回(文件名,扩展名)
if ext[1] == '': # 没有扩展名,使用默认的deffile,即path变为'...(服务器路径)/index.html'
if path[-1] == '/':
path += deffile
else:
path += '/' + deffile
ldir = dirname(path) # path所在的目录路径
if sep != '/': # path路径分隔符?猜测可能是考虑到不同操作系统平台
ldir = replace(ldir, '/', sep) # 统一采用linux系统的格式
if not isdir(ldir): # 路径不存在时,存档。
if exists(ldir): unlink(ldir) # 删除ldir下原有的文件?
makedirs(ldir)
return path
# download()方法连接网络,下载给定链接的页面
def download(self): # download web page
try:
retval = urlretrieve(self.url, self.file) # 下载url页面成功,并保存在filename中,retval是一个元组,第一项是文件名(包括路径),第二项是服务器的响应头,?
except IOError:
retval = ('*** ERROR: invalid URL "%s"' % self.url,) # 下载页面失败,retval是一个包含字符串的单元组
return retval
# 如果上面的的处理没有发现任何错误,就会调用parseAndGetLinks()对新下载的主页进行分析,确定对那个web页面上的每一个连接应该采取什么样的行动。
def parseAndGetLinks(self): # 解析HTML,保存链接
self.parser = HTMLParser(AbstractFormatter(DumbWriter(StringIO()))) # 解析器?
self.parser.feed(open(self.file).read()) # open(self.file).read()就是下载的页面,feed()方法是提取其中的链接,自动存入anchorlist列表。
self.parser.close() # 关闭解析器?
return self.parser.anchorlist # anchorlist里储存解析出来的html页面的链接
class Crawler(object): # 管理爬虫进程
count = 0 # 已下载web页面计数器
def __init__(self, url):
self.q =[url] # 待下载链接队列,页面处理完毕变短,如果页面中发现新的链接,则会变长。
self.seen = [] # 已下载队列
self.dom = urlparse(url)[1] # 存储主链接域名,用于判断后续链接是否是该域的一部分。
# 核心
def getPage(self, url):
r = Retrieve(url) # 实例化Retrieve对象
retval = r.download() # 下载web页面
if retval[0] == '*': # 下载出错,不进行解析
print retval, '... skipping parse'
return
Crawler.count += 1 # 下载web页面没有问题,则已下载web页面计数器count加1,表示已经下载了一个web页面
print '\n(', Crawler.count, ')'
print 'URL:', url
print 'FILE:', retval[0]
self.seen.append(url) # 将url添加到已下载队列
links = r.parseAndGetLinks() # links包含了url指向的HTML页面的所有链接
for eachlink in links:
if eachlink[:-4] != 'http' and find(eachlink, '://') == -1:
eachlink = urljoin(url, eachlink)
print '* ', eachlink,
if find(lower(eachlink), 'mailto:') != -1:
print '... discarded, mailto link'
continue # 如果url指向的HTML页面有mailto链接(自动发送电子邮件的链接),则其将被忽略
if eachlink not in self.seen:
if find(eachlink, self.dom) == -1:
print '... discarded, not in domain' # 不在主链接域名的链接也会被忽略,比如说很多网站上都会有另外一个网站(不同域名)的链接,这类链接也会被忽略。
else:
if eachlink not in self.q:
self.q.append(eachlink)
print '... new, added to Q' # 将链接加入到待下载链接队列中
else:
print '... discarded, already in Q' # 忽略已在待下载链接队列中的链接
else:
print '... discarded, already processed' # 忽略已经下载过的链接
# 启动Crawler(),不停地推进处理过程,直到队列为空。
def go(self): # 处理待下载链接队列里的链接
while self.q:
url = self.q.pop()
self.getPage(url)
def main():
if len(argv) > 1:
url = argv[1] # 就是在命令行以 python crawl.py url 启动程序
else:
try:
url = raw_input('Enter starting URL: ')
except (KeyboardInterrupt, EOFError):
url = ''
if not url: return
robot = Crawler(url)
robot.go()
if __name__ == "__main__":
main()
但是按照书中给出的url运行程序,会出问题,就是url打不开,后续的链接都不是同域名下的链接,可以尝试其他网页。