最近我突然对网络爬虫开窍了,真正做起来的时候发现并不算太难,都怪我以前有点懒,不过近两年编写了一些程序,手感积累了一些肯定也是因素,总之,还是惭愧了。好了,说正题,我把这两天做爬虫的过程中遇到的问题总结一下:
需求:做一个爬虫,爬取一个网站上所有的图片(只爬大图,小图标就略过)
思路:1、获取网站入口,这个入口网页上有很多图片集合入口,进入这些图片集合就能看到图片链接了,所以爬取的深度为2,比较简单;2、各个子图片集合内所包含的图片链接有两种形式:一种是绝对图片路径(直接下载即可),另一种的相对图片路径(需要自行拼接当前网页路径)。总之这些子图集合的表现形式简单,没有考虑更复杂的情况。3、在爬取的过程中保存已成功爬取的路径,防止每次爬取都执行重复的任务,当然,当每次启动时要首先加载该历史数据库,剩下的就是细节了。
快速链接:
2.1 日志系统
2.2 记录访问历史
2.3 异常处理
2.4 网页自动编码判断
2.5 远程主机重置链接(Errno 10054)
2.6 使用BeautifulSoup来分析网页
一、全部代码
直接先来代码,再详细说优化的过程和内容吧:
1 __author__ = 'KLH'
2 # -*- coding:utf-8 -*-
3
4 import urllib
5 import urllib2
6 import chardet
7 import re
8 import os
9 import time
10 from myLogger import *
11
12 # 网络蜘蛛
13 class Spider:
14
15 # 类初始化
16 def __init__(self):
17 self.contentFolder = u"抓取内容"
18 self.dbName = "url.db"
19 self.createFolder(self.contentFolder)
20 self.urlDB = set()
21
22 # 获取URL数据库以获取爬过的网页地址
23 def loadDatabase(self):
24 isExists = os.path.exists(self.dbName)
25 if not isExists:
26 logging.info(u"创建URL数据库文件:'" + self.dbName + u"'")
27 f = open(self.dbName, 'w+')
28 f.write("#Create time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + '\n')
29 f.close()
30 return
31 db = open(self.dbName, 'r')
32 for line in db.readlines():
33 if not line.startswith('#'):
34 self.urlDB.add(line.strip('\n'))
35 db.close()
36 logging.info(u"URL数据库加载完成!")
37
38 # 追加数据库文件
39 def writeToDatabase(self, url):
40 db = open(self.dbName, 'a')
41 db.write(url + '\n')
42 db.close()
43
44 # 处理路径名称中的空格字符
45 def getPathName(self, pathName):
46 newName = ""
47 subName = pathName.split()
48 i = 0
49 while i < len(subName) - 1:
50 newName = newName + subName[i]
51 i = i + 1
52 return newName
53
54 # 获取索引页面的内容
55 def getPage(self, pageURL, second):
56 try:
57 headers = {'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0'}
58 request = urllib2.Request(pageURL, headers = headers)
59 response = urllib2.urlopen(request, timeout = second)
60 data = response.read()
61 response.close()
62 return data.decode('gbk'), True
63 except urllib2.HTTPError,e: #HTTPError必须排在URLError的前面
64 logging.error("HTTPError code:" + str(e.code) + " - Content:" + e.read())
65 return "", False
66 except urllib2.URLError, e:
67 logging.error("URLError reason:" + str(e.reason) + " - " + str(e))
68 return "", False
69 except Exception, e:
70 logging.error(u"获取网页失败:" + str(e))
71 return "", False
72
73 # 获取索引界面所有子页面信息,list格式
74 def getContents(self, pageURL, second):
75 contents = []
76 page, succeed = self.getPage(pageURL, second)
77 if succeed:
78 # 这里的正则表达式很重要,决定了第一步的抓取内容:
79 pattern = re.compile('<tr>.*?<a href="(.*?)".*?<b>(.*?)</b>.*?</tr>',re.S)
80 items = re.findall(pattern,page)
81 for item in items:
82 contents.append([item[0],item[1]])
83 contents.sort()
84 return contents
85
86 # 获取页面所有图片
87 def getAllImgURL(self, infoURL):
88 images = []
89 succeed = True
90 try:
91 headers = {'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0'}
92 request = urllib2.Request(infoURL, headers = headers)
93 data = urllib2.urlopen(request).read()
94 chardet1 = chardet.detect(data) # 自动判断网页编码
95 page = data.decode(str(chardet1['encoding']))
96
97 # 第一种解码格式:
98 pattern = re.compile('<option value="(.*?)">(.*?)</option>')
99 items = re.findall(pattern, page)
100 # item[0]为图片URL尾部,item[1]为图片名称
101 for item in items:
102 if item.startswith('http://'):
103 imageURL = item[0]
104 if imageURL in self.urlDB:
105 logging.info(u"获得图片URL(曾被访问,跳过):" + imageURL)
106 else:
107 logging.info(u"获得图片URL:" + imageURL)
108 images.append(imageURL)
109 else:
110 imageURL = infoURL + item[0]
111 if imageURL in self.urlDB:
112 logging.info(u"获得图片URL(曾被访问,跳过):" + imageURL)
113 else:
114 logging.info(u"获得图片URL:" + imageURL)
115 images.append(imageURL)
116
117 # 第二种解码格式
118 pattern = re.compile('<IMG src="(.*?)".*?>')
119 items = re.findall(pattern, page)
120 # item为图片URL
121 for item in items:
122 if item.startswith('http://'):
123 if item in self.urlDB:
124 logging.info(u"获得图片URL(曾被访问,跳过):" + item)
125 else:
126 logging.info(u"获得图片URL:" + item)
127 images.append(item)
128
129 except Exception, e:
130 logging.warning(u"在获取子路径图片列表时出现异常:" + str(e))
131 succeed = False
132 return images, succeed
133
134 # 保存所有图片
135 def saveImgs(self, images, name):
136 logging.info(u'发现"' + name + u'"共有' + str(len(images)) + u"张照片")
137 allSucceed = True
138 for imageURL in images:
139 splitPath = imageURL.split('/')
140 fTail = splitPath.pop()
141 fileName = name + "/" + fTail
142 logging.info(u"开始准备保存图片(超时设置:120秒):" + imageURL)
143 startTime = time.time()
144 succeed = self.saveImg(imageURL, fileName, 120)
145 spanTime = time.time() - startTime
146 if succeed:
147 logging.info(u"保存图片完成(耗时:" + str(spanTime) + u"秒):" + fileName)
148 # 保存文件存储记录
149 self.urlDB.add(imageURL)
150 self.writeToDatabase(imageURL)
151 else:
152 logging.warning(u"保存图片失败(耗时:" + str(spanTime) + u"秒):" + imageURL)
153 allSucceed = False
154 # 为了防止网站封杀,这里暂停1秒
155 time.sleep(1)
156 return allSucceed
157
158 # 传入图片地址,文件名,超时时间,保存单张图片
159 def saveImg(self, imageURL, fileName, second):
160 try:
161 headers = {'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0'}
162 request = urllib2.Request(imageURL, headers = headers)
163 u = urllib2.urlopen(request, timeout = second)
164 data = u.read()
165 f = open(fileName, 'wb')
166 f.write(data)
167 f.close()
168 u.close()
169 return True
170 except urllib2.HTTPError,e: #HTTPError必须排在URLError的前面
171 logging.error("HTTPError code:" + str(e.code) + " - Content:" + e.read())
172 return False
173 except urllib2.URLError, e:
174 logging.error("URLError reason:" + str(e.reason) + " - " + str(e))
175 return False
176 except Exception, e:
177 logging.error(u"保存图片失败:" + str(e))
178 return False
179
180 # 创建新目录
181 def createFolder(self, path):
182 path = path.strip()
183 # 判断路径是否存在
184 isExists=os.path.exists(path)
185 # 判断结果
186 if not isExists:
187 # 如果不存在则创建目录
188 logging.info(u"创建文件夹:'" + path + u"'")
189 # 创建目录操作函数
190 os.makedirs(path)
191 return True
192 else:
193 # 如果目录存在则不创建,并提示目录已存在
194 logging.info(u"名为'" + path + u"'的文件夹已经存在,跳过")
195 return False
196
197 # 获取的首页地址
198 def savePageInfo(self, pageURL):
199 logging.info(u"准备获取网页内容(超时设置:60秒):" + pageURL)
200 contents = self.getContents(pageURL, 60)
201 logging.info(u"网页内容获取完成,子路径个数:" + str(len(contents)))
202 index = 1
203 for item in contents:
204 #(1)item[0]子路径URL, item[1]子路径名称
205 folderURL = item[0]
206 folderName = self.contentFolder + '\\' + str(index) + "-" + self.getPathName(item[1])
207 self.createFolder(folderName)
208 index = index + 1
209
210 #(2)判断链接头部合法性和重复性
211 if not folderURL.startswith('http://'):
212 folderURL = pageURL + folderURL
213 if folderURL in self.urlDB:
214 logging.info(u'"' + folderName + u'"的链接地址(已访问,跳过)为:' + folderURL)
215 continue
216 else:
217 logging.info(u'"' + folderName + u'"的链接地址为:' + folderURL)
218
219 #(3)获取图片URL列表,成功则保存图片
220 images, succeed = self.getAllImgURL(folderURL)
221 if succeed:
222 succeed = self.saveImgs(images, folderName)
223 if succeed:
224 self.urlDB.add(folderURL)
225 self.writeToDatabase(folderURL)
226
227 # 初始化系统日志存储
228 InitLogger()
229 # 传入初始网页地址,自动启动爬取图片:
230 spider = Spider()
231 spider.loadDatabase()
232 spider.savePageInfo('http://365.tw6000.com/xtu/')
233 logging.info(u"全部网页内容爬取完成!程序退出。")
查看全部代码
二、问题历史
在上面的代码中有不少的细节是优化解决过的,相关的知识点如下:
2.1 日志系统
Python的日志系统是相当的不错,非常的方便,详细的资料可以参考Python官方文档,或者上一篇博文也是提到过的:《Python中的日志管理Logging模块》,应用到我这个爬虫这里的代码就是myLogger.py模块了,用起来很方便:
1 __author__ = 'KLH'
2 # -*- coding:utf-8 -*-
3
4 import logging
5 import time
6
7 def InitLogger():
8 logFileName = 'log_' + time.strftime("%Y%m%d%H%M%S", time.localtime(time.time())) + '.txt'
9 logging.basicConfig(level=logging.DEBUG,
10 format='[%(asctime)s][%(filename)s:%(lineno)d][%(levelname)s] - %(message)s',
11 filename=logFileName,
12 filemode='w')
13
14 # 定义一个StreamHandler将INFO级别以上的信息打印到控制台
15 console = logging.StreamHandler()
16 console.setLevel(logging.INFO)
17 formatter = logging.Formatter('[%(asctime)s][%(filename)s:%(lineno)d][%(levelname)s] - %(message)s')
18 console.setFormatter(formatter)
19 logging.getLogger('').addHandler(console)
查看myLogger.py
注意在爬虫的代码执行前调用一下该函数:
from myLogger import *
# 初始化系统日志存储
InitLogger()
日志调用和打印的结果如下:
logging.info(u"准备获取网页内容(超时设置:60秒):" + pageURL)
#打印结果如下:
[2015-11-09 22:35:02,976][spider2.py:182][INFO] - 准备获取网页内容(超时设置:60秒):http://365.XXXXX.com/xtu/
2.2 记录访问历史
这里记录URL的访问历史是为了防止执行重复任务,在内存中保持一个Set就能满足需求,在磁盘上可以简单的保存成一个TXT文件,每个URL保存成一行即可。所以这里是逻辑顺序应该是在初始化时创建一个Set用来保存访问历史,任务执行之前从数据库中加载访问历史,如果是首次运行尚未创建数据库还需要进行一次创建操作。然后就好办了,每次完成一个URL的访问就保存一下访问记录。相关代码如下:
1 # 类初始化
2 def __init__(self):
3 self.contentFolder = u"抓取内容"
4 self.dbName = "url.db" # 1、定义数据库文件名
5 self.createFolder(self.contentFolder) # 2、创建内容存储目录
6 self.urlDB = set() # 3、创建内存数据库
7
8 # 获取URL数据库以获取爬过的网页地址
9 def loadDatabase(self):
10 isExists = os.path.exists(self.dbName) # 4、首先判断是否是首次运行,如果数据库文件不存在则创建一下
11 if not isExists:
12 logging.info(u"创建URL数据库文件:'" + self.dbName + u"'")
13 f = open(self.dbName, 'w+')
14 f.write("#Create time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + '\n')
15 f.close()
16 return
17 db = open(self.dbName, 'r') # 5、从磁盘中加载数据库
18 for line in db.readlines():
19 if not line.startswith('#'):
20 self.urlDB.add(line.strip('\n'))
21 db.close()
22 logging.info(u"URL数据库加载完成!")
23
24 # 追加数据库文件
25 def writeToDatabase(self, url): # 6、在系统运行过程中,如需记录日志,追加日志内容即可
26 db = open(self.dbName, 'a')
27 db.write(url + '\n')
28 db.close()
有了上面的代码,在记录日志过程中就很方便了:
succeed = self.saveImgs(images, folderName)
if succeed:
self.urlDB.add(folderURL)
self.writeToDatabase(folderURL)
2.3 异常处理
访问网络资源不可避免的会有很多异常情况,要处理这些异常情况才能稳定运行,Python的异常处理很简单,请参考如下获取网页的代码段:
1 # 获取索引页面的内容
2 def getPage(self, pageURL, second):
3 try:
4 request = urllib2.Request(pageURL)
5 response = urllib2.urlopen(request, timeout = second)
6 data = response.read()
7 return data.decode('gbk'), True
8 except urllib2.HTTPError,e: # HTTPError必须排在URLError的前面
9 logging.error("HTTPError code:" + str(e.code) + " - Content:" + e.read())
10 return "", False
11 except urllib2.URLError, e:
12 logging.error("URLError reason:" + str(e.reason) + " - " + str(e))
13 return "", False
14 except Exception, e: # 其他所有类型的异常
15 logging.error(u"获取网页失败:" + str(e))
16 return "", False # 这里返回两个值方便判断
2.4 网页自动编码判断
昨天在爬取网页的过程中突然发现,有的页面居然说编码不能通过utf-8进行解析,我看了看有的网页确实不是utf-8编码的,那怎么办呢?怎么才能自动进行解码?从网上可以搜索到一个Python的开源库很好用,叫做chardet,默认Python2.7是不带的需要下载,比如我下载的是:chardet-2.3.0.tar.gz
有了这个压缩包解压出来cahrdet子文件夹拷贝到:C:\Python27\Lib\site-packages目录下即可。下面看看用法实例:
1 import chardet
2
3 # 获取页面所有图片
4 def getAllImgURL(self, infoURL):
5 images = []
6 succeed = True
7 try:
8 data = urllib2.urlopen(infoURL).read() # 1、先获取网页内容
9 chardet1 = chardet.detect(data) # 2、再调用该模块的方法自动判断网页编码
10 page = data.decode(str(chardet1['encoding'])) # 3、注意,得到的chardet1是一个字典类似于:{'confidence': 0.98999999999999999, 'encoding': 'GB2312'}
11
12 # 第一种解码格式:
13 pattern = re.compile('<option value="(.*?)">(.*?)</option>')
14 items = re.findall(pattern, page)
15 # item[0]为图片URL尾部,item[1]为图片名称
16 for item in items:
17 imageURL = infoURL + item[0]
18 if imageURL in self.urlDB:
19 logging.info(u"获得图片URL(曾被访问,跳过):" + imageURL)
20 else:
21 logging.info(u"获得图片URL:" + imageURL)
22 images.append(imageURL)
23
24 # 第二种解码格式
25 pattern = re.compile('<IMG src="(.*?)".*?>')
26 items = re.findall(pattern, page)
27 # item为图片URL
28 for item in items:
29 if item.startswith('http://'): # 4、这里也注意一下,在这种网页中相对路径的图片都是插图之类的小图片不需要下载,所以过滤掉了。
30 if item in self.urlDB:
31 logging.info(u"获得图片URL(曾被访问,跳过):" + item)
32 else:
33 logging.info(u"获得图片URL:" + item)
34 images.append(item)
35
36 except Exception, e:
37 logging.warning(u"在获取子路径图片列表时出现异常:" + str(e))
38 succeed = False
39 return images, succeed
2.5 远程主机重置链接(Errno 10054)
在今天的爬取过程中我发现了一个问题,爬到后面的内容都出错了,错误信息参见如下:
[2015-11-09 23:48:51,082][spider2.py:130][INFO] - 开始准备保存图片(超时设置:300秒):http://xz1.XXXX.com/st/st-06/images/009.jpg
[2015-11-09 23:48:55,095][spider2.py:160][ERROR] - 保存图片失败:[Errno 10054]
[2015-11-09 23:48:55,096][spider2.py:140][WARNING] - 保存图片失败(耗时:4.01399993896秒):http://xz1.XXXX.com/st/st-06/images/009.jpg
[2015-11-09 23:48:55,098][spider2.py:130][INFO] - 开始准备保存图片(超时设置:300秒):http://xz1.XXXX.com/st/st-06/images/010.jpg
[2015-11-09 23:48:56,576][spider2.py:160][ERROR] - 保存图片失败:[Errno 10054]
[2015-11-09 23:48:56,578][spider2.py:140][WARNING] - 保存图片失败(耗时:1.48000001907秒):http://xz1.XXXX.com/st/st-06/images/010.jpg
可以看到都在报这个错误代码,网上一查,发现有很多同学已经遇到过了,请参考知乎上的讨论:http://www.zhihu.com/question/27248551
从网上讨论的结果来看,可以肯定的是直接原因在于“远程主机主动关闭了当前链接”,而根本原因则在于:网站启用了反爬虫的策略,也就是说我的爬虫爬的过快并且被发现不是真正的浏览器浏览了。怎么办了?针对这两个原因逐个处理,一是伪装成浏览器,二是不要访问的过快,三是每次访问完成都关闭链接。相关代码如下:
1 # 传入图片地址,文件名,超时时间,保存单张图片
2 def saveImg(self, imageURL, fileName, second):
3 try:
4 headers = {'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0'} # 1、这里构造一个浏览器的头部
5 request = urllib2.Request(imageURL, headers = headers)
6 u = urllib2.urlopen(request, timeout = second) # 2、这里的超时设置timeout不要太长
7 data = u.read()
8 f = open(fileName, 'wb')
9 f.write(data)
10 f.close()
11 u.close() # 3、注意这里的链接要主动调用一下关闭
12 return True
13 except urllib2.HTTPError,e: #HTTPError必须排在URLError的前面
14 logging.error("HTTPError code:" + str(e.code) + " - Content:" + e.read())
15 return False
16 except urllib2.URLError, e:
17 logging.error("URLError reason:" + str(e.reason) + " - " + str(e))
18 return False
19 except Exception, e:
20 logging.error(u"保存图片失败:" + str(e))
21 return False # 4、注意调用完这个函数之后再time.sleep(1)一下,防止过快访问被发现了,呵呵
使用正则表达式来匹配网页中的字段确实是太费劲了。用BeautifulSoup就省力多了,功能很强大:
soup = BeautifulSoup(page_data, 'lxml')
entries = soup.find_all(src=re.compile('.jpg'), border='0') # 找到所有字段为src正则匹配的标签内容,同时增加一个约束条件是border=‘0’
Beautiful Soup支持Python标准库中的HTML解析器,还支持一些第三方的解析器,如果我们不安装它,则 Python 会使用 Python默认的解析器,lxml 解析器更加强大,速度更快,推荐安装。