网站的404监控,其实要用到的就urllib2和re这两个模块,urllib2用来处理请求,re正则表达式用来处理html页面。
其中,处理html页面(找出页面的全部links),也可使用BeautifulSoup,简单方便。
这里,将介绍如何使用mechanize模块实现网站的404监控。主要内容包括:
1、实现过程中常见的问题答疑;
2、多线程中的线程安全问题;
mechanize模块,将urllib2和beautifuSoup封装起来,同时模拟浏览器的操作。
对于web测试工具开发,mechanize是个不错的辅助工具。
就是mechanize的帮助文档太少,同时FAQ的内容不多,完完全全需要你自己是参考他的源码。
mechanize相关资料:
FAQ:http://wwwsearch.sourceforge.net/mechanize/faq.html
codesearch:https://searchcode.com/codesearch/view/80174766/
源码:http://www.joesourcecode.com/Documentation/mechanize0.2.5/
网站404监控:
1、监控全网所有links
隐性需求:
1、很多网站都有会员登录,links会有所不同(使用cookie登录即可);
2、同时不同角色会员登录,links会有所不同;
3、每个link下还有其他链接(可使用递归处理,相当于爬虫工具,难点在于如何终止递归);
4、像功能链接,很容易被忽略,如【下一页】,它的href会像 /page/1 这种格式;
以上这些隐性需求,本文暂不考虑,写起来也不难,你可以用来练练手,修改下面的代码。
1、有些网站有太多链接,如果串行访问,导致一个脚本跑下来需要花费,如果你机器差的话,那就更糟糕。
解决方法:使用thread模块,创建多进程并发访问,如一个主要页面一个进程,每个进程串行访问url时停顿1秒。
请注意,有些网站拒绝短时间内发送多个请求的链接,或者拒绝短时间内多次请求同一个链接,所以停顿时间需要设置合适。
2、网站404监控完成,如果url访问存在【code != 200】(返回码)的话,要发送邮件,贴上错误的url(记录在相应的txt文件中)。
解决方案:此时已采用了多线程,如何去汇总所有错误url,且该如何合理触发邮件呢?这就想到了我之前提过的装饰器。
我添加个装饰器,使用一个全局变量列表,记录所有错误的url,代码如下:
def monitoring(files):
'''用来监控线程,每次访问url时,是否有code!=200的,如果是,则添加文件(记录着错误的url)
'''
def wrap(f):
def newFunction(*args,**kw):
result = f(*args,**kw)
if result[0]:
files.append(result[1])
return newFunction
return wrap
上面的装饰器用来装饰线程实例的run函数,如下:
send_files = []
@monitoring(send_files)
def do_run(self):
'''每个进程执行的内容,多进程进行three_test_404
'''
links_dict = self.get_sub_links_dict()
urls = links_dict.keys()
shuffle(urls) #打乱每个进程访问的urls列表,因为进程间访问的url有重复的风险,这里尽量避免同时访问同一个url
errcount = 0
for url in urls:
if not url.startswith('javascript'):
code = three_test_404(self._br,url)
if not code or 200 != int(code):
errcount += 1
self._output.writelines(url+ ' code: ' + str(code)+' title:'+ links_dict[url].text + '\n')
self._output.writelines('\n[' + re.sub('\n','',self._url) + '] errors: ' + str(errcount) + '\n')
filename = self._output.name
self._output.close()
return (errcount,filename)
3、获取某个web页面所有链接的有两种方法:
(1) 使用mechanize.Browser(), 如下:
br = mechanize.Browser()
br.open(weburlA)
links = br.links()
请注意,links是一个迭代器,请不要在links迭代中使用br.open(weburlB)访问另一个url,否则links将成为weburB下的所有链接。
(2)使用mechanize.LinksFactory(),推荐使用,如下:
lf = mechanize.LinksFactory()
response = urllib2.open(weburlA)
lf.set_response(response)
links = lf.links()
在获取页面所有链接links后,建议将其转换为列表,以免后续不小心使用【lf.set_response()】或者【br.open()】,导致程序结果有误。
另外,在使用links迭代时,如下:
for link in links():
do somethting
很容易出现(socket error)10054错误,除非你运气极好,具体信息如下:
socket.error: [Errno 10054] An existing connection was forcibly closed by the remote host
个人认为,这算mechanize的一个bug,links迭代的过程中,mechanize不断使用response.read(1024)读取页面html。
这里的response,是之前你使用br.open()打开的一个请求(file-like object)返回。
解决方案:Browser对象或者LinksFactory对象,都提供一个函数set_response(),让你设置一个(file-like object,即是有read函数的实例)请求返回。
所以,这里可以将br.open()返回response的html内容,存入临时文件tempfile,然后使用tempfile的文件句柄作为response即可。
response = self.get_response(self._url, 10)
temp = tempfile.TemporaryFile()
temp.write(response.read())
temp.seek(0)
if not response:
raise RuntimeError('[%s] open failed' %self._url)
self._lf.set_response(temp,self._url,'utf-8')
links_dict = {}
for link in self._lf.links():
#当字典key有该url,且此时link无text内容,则不需要替换
if links_dict.has_key(link.url) and not link.text:
continue
links_dict[link.url] = link
temp.close()
MultiTest404.py:检测404,这里选取三次测试,如果有一次为200,则成功,这样能减少偶然事件
def test_404(browser,url):
code = None
try:
response = browser.open(url)
#response = browser.response()#发送请求
code = response.code
except HTTPError,e:
code = e.code #获取http错误码
except (URLError,error),e:
try:
code = str(e.args[0].errno) #获取socket的返回码,errorcode 是字典
except:
code = 10000 #获取不到socket错误码,自定义为1000
except:
code = 10001 #其他错误,统一自定义为1001
return code
def three_test_404(browser,url,wait=8):
'''三次测试:如果有一次返回码为200,则认为成功,返回200,否则取最后一次的返回码
'''
for i in xrange(3):
code = test_404(browser,url)
if 200 == int(code):
break
time.sleep(wait)
return code
MultiTest404.py:线程主体,注意:
这里使用一个browser,一个linksfactory
def synchronized(lock):
""" Synchronization decorator.
同步锁,用来锁住争抢资源
"""
def wrap(f):
def newFunction(*args,**kw):
lock.acquire()
try:
return f(*args,**kw)
finally:
lock.release()
return newFunction
return wrap
class urlThread(Thread):
def __init__(self,browser,link_factory,url):
Thread.__init__(self)
self._url = url
self._br = browser
self._lf = link_factory
name = '(\w*)\.letv'
ch1 = re.findall(name,url)
fname=control.get_file(str(ch1[0]))
self._output = open(fname,'w')
@synchronized(mylock)
def get_sub_links_dict(self):
'''获取links字典,url为key'''
response = self.get_response(self._url, 10)
temp = tempfile.TemporaryFile()
temp.write(response.read())
temp.seek(0)
if not response:
raise RuntimeError('[%s] open failed' %self._url)
self._lf.set_response(temp,self._url,'utf-8')
links_dict = {}
for link in self._lf.links():
#当字典key有该url,且此时link无text内容,则不需要替换
if links_dict.has_key(link.url) and not link.text:
continue
links_dict[link.url] = link
temp.close()
return links_dict
def get_response(self,url,times=3,wait=10):
'''生成每个主要页面的请求,并获取返回值,如果这个过程出错,则等待wait秒,次数为times
'''
num = times
response = None
while num:
try:
response = urlopen(url)
break
except:
time.sleep(wait)
num -= 1
return response
@monitoring(send_files)
def run(self):
'''每个进程执行的内容,多进程进行three_test_404
'''
#2015-01-22 修改,提高效率
links_dict = self.get_sub_links_dict()
urls = links_dict.keys()
shuffle(urls) #打乱每个进程访问的urls列表,因为进程间访问的url有重复的风险,这里尽量避免同时访问同一个url
errcount = 0
for url in urls:
if not url.startswith('javascript'):
code = three_test_404(self._br,url)
if not code or 200 != int(code):
errcount += 1
self._output.writelines(url+ ' code: ' + str(code)+' title:'+ links_dict[url].text + '\n')
self._output.writelines('\n[' + re.sub('\n','',self._url) + '] errors: ' + str(errcount) + '\n')
filename = self._output.name
self._output.close()
return (errcount,filename)
response = browser.open(url) #这里线程不安全
#response = browser.response()#发送请求
code = response.code
code = browser.open(url).code
但还是不安全,只是减少出错的概率。
这时,你会质疑,这个程序还有用么?
个人认为,虽然线程不安全,但是由于请求的时间极短,出错的概率很低。
同时,整个网站返回码不是200,的也就那几个,即使我乱配鸳鸯,又能错了多少缘分。
要解决上面的【线程安全】问题,也很简单,就是多开辟几个Browser和linksFactory,即每个线程不存在共享资源。
更简单点,就是直接给run函数上锁(@synchronized(mylock) ),但这样就成了串行。
还有一个解决方案,使用【线程池】,而且速度能提升1/3以上。
对上面所说,如有什么问题,可以留言。