聚沙成塔--爬虫系列(六)(请做个内外兼修的高手)

2017-10-22

风清扬

版权声明:本文为作者原创文章,可以随意转载,但必须在明确位置标明出处!!!

上一篇文章我们介绍了函数的使用,以及函数设计应该参考的原则,但这些还不够的,函数只是让我们的代码可读性、可复用性提高了。并没有让我们的程序看上去那么健壮,专业术语叫「稳定行」;什么是稳定性呢,就是我们的程序经不起考验,就像大海中的孤舟经不起一点风浪的拍打,我们上一篇文章的代码只适合在风平浪静的时候出海,因为程序并没有一点容错处理,下面该是修炼内功的时候了,不能只有招式没有内涵啊…,那样的话你讲永远成为不了高手。

内功修炼总纲–异常处理

什么是异常处理,异常处理就是处理程序运行期间出现的任何意外或异常情况,程序员的日常生活中,错误几乎是每天都会发现,有些错误是致命的,会直接导致程序终止,直到错误被修复后再次执行程序,但是有些错误是可以忽略的,所有我们需要有一个机制能在程序运行中捕获到异常信息,这样可以帮助我们更快的解决问题。

什么是错误

错误是指语法或是逻辑上的,语法错误是值不能被解释器解释或不能被编译器编译,这些错误必须在程序执行前纠正。

当语法上没有什么错后,就剩下逻辑上的错误了,逻辑错误可能是由用户不完整或不合法的输入所致。

什么是异常

程序出现了错误而在正常流程以外采取的行为,这个行为分为两个阶段,首先是引起异常发生的错误,其次是采取可能措施的阶段。

第一个阶段是发生在一个异常条件后发生的,只要检测到并意识到异常条件,解释器就会触发一个异常。
第二个阶段是异常发生后,程序员可以做出各种不同的处理逻辑,当然也也可忽略异常。异常发生后需要特别注意的是当前异常发生后的逻辑都不会被执行了,如下我们定义了一个长度为4的list,而我们试图去访问第五个元素,那么就会产生一个越界的异常,而hello world将不再会被执行到

1
2
3
4
items = [1,2,3,4]
print(items[5])
print('hello world')

结果

1
2
3
4
Traceback (most recent call last):
File "exception.py", line 2, in <module>
print(items[5])
IndexError: list index out of range

检测和处理异常语法

异常可以通过try语句来检测,任何在try语句块里的代码都将被监测,except用来捕获异常,任何在try语句块里被检测到的异常都会被except捕获到,所以对于程序员来说我们需要考虑当异常发生后做一些收尾工作,像释放资源,重连数据库等等…

常见的异常错误

错误类型 描述
AttributeError 属性错误,特性引用和赋值失败时会引发属性错误
NameError 试图访问的变量名不存在
SyntaxError 语法错误,代码形式错误
Exception 所有异常的基类,因为所有python异常类都是基类Exception的其中一员,异常都是从基类Exception继承的,并且都在exceptions模块中定义。
IOError 一般常见于打开不存在文件时会引发IOError错误,也可以解理为输出输入错误
KeyError 使用了映射中不存在的关键字(键)时引发的关键字错误
IndexError 索引错误,使用的索引不存在,常索引超出序列范围,什么是索引
TypeError 类型错误,内建操作或是函数应于在了错误类型的对象时会引发类型错误
ZeroDivisonError 除数为0,在用除法操作时,第二个参数为0时引发了该错误
ValueError 值错误,传给对象的参数类型不正确,像是给int()函数传入了字符串数据类型的参数。

try-except语句

try-except是最常见的异常检测捕获写法,它由try块和except块组成,也可以有一个可选的错误原因

1
2
3
4
try:
try_suite # watch for exceptions here 监控这里的异常
except Exception[, reason]:
except_suite # exception-handling code 异常处理代码

try-except-finally语句

在try-except语句中我们可以有多个except语句块来捕获不同的错误类型,finally语句块是不管有没有异常发生都会被执行到,它通常用来最后释放资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try:
try_suite
except Exception1:
suite_for_Exception1
except (Exception2, Exception3, Exception4):
suite_for_Exceptions_2_3_and_4
except Exception5, Argument5:
suite_for_Exception5_plus_argument
except (Exception6, Exception7), Argument67:
suite_for_Exceptions6_and_7_plus_argument
except:
suite_for_all_other_exceptions
else:
no_exceptions_detected_suite
finally:
always_execute_suite

总纲目录

同过上面的介绍可能有人要问我写代码的时候不知道可能会发生什么异常,有可能一种异常,也有可能几种异常,那怎么办呢,嗯,不错能想到这个问题的人证明都是认真去思考过的人,下面我们看看异常的继承关系,这里还没有讲到类的继承关系,不过不要紧,你就想象成你与你爹,你爷爷的关系就好,你爷爷把财产都留给你了你爹,你爹以后也会把财产留给你。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
+-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning

可以看到BaseException是异常的祖先,BaseException下有4兄弟,分别是SystemExit、KeyboardInterrupt、GeneratorExit、Exception异常,所以如何你要捕捉Ctrl + c终端异常那么你的except语句应该写成except KeyboardInterrupt as e;那么从上图我们可以看到绝大部分的异常都是继承自Exception的,所以如果我们要捕获除SystemExit、KeyboardInterrupt、GeneratorExit三种异常外都可以写成except Exception as e;

努力做个像风清扬一样的高手

通过上面的介绍,接下来我们来分析分析我为什么说之前的代码只适合在风平浪静的时候出海呢,从我们的代码可以看出我们的代码写出来的逻辑是理想中没有任何错误才能得出正确结果的,那么如果我们的url地址是个不可访问的地址会发生什么呢,我们将url地址改为「https://www.qiushibaike1.com」看看会有什么结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from urllib import request
import re
url = 'https://www.qiushibaike1.com'
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'
headers = {'User-Agent': user_agent}
def read_html(url, headers, codec):
'''[read_html]
[读取html页面内容]
Arguments:
url {[string]} -- [url地址]
headers {[dict]} -- [用户代理,这里是一个字典类型]
codec {[string]} -- [编码方式]
Returns:
[string] -- [页面内容]
'''
# 构建一个请求对象
req = request.Request(url, headers=headers)
# 打开一个请求
response = request.urlopen(req)
# 读取服务器返回的页面数据内容
content = response.read().decode(codec)
return content
def match_element(content, pattern):
'''[match_element]
[匹配元素]
Arguments:
content {[string]} -- [文本内容]
pattern {[object]} -- [匹配模式]
Returns:
[list] -- [匹配到的元素]
'''
# 匹配所有用户信息
userinfos = re.findall(pattern, content)
return userinfos
content = read_html(url, headers, 'utf-8')
pattern = re.compile(r'<div class="article block untagged mb15[\s\S]*?class="stats-vote".*?</div>', re.S)
userinfos = match_element(content, pattern)
if userinfos:
pattern = re.compile(r'<a href="(.*?)".*?<h2>(.*?)</h2>.*?<div class="content">(.*?)</div>', re.S)
for userinfo in userinfos:
item = match_element(userinfo, pattern)
#print(item)
if item:
userid, name, content = item[0]
# 去掉换行符,<span></span>,<br/>符号
userid = re.sub(r'\n|<span>|</span>|<br/>', '', userid)
name = re.sub(r'\n|<span>|</span>|<br/>', '', name)
content = re.sub(r'\n|<span>|</span>|<br/>', '', content)
print((userid, name, content))

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Traceback (most recent call last):
File "func_scrap.py", line 49, in <module>
content = read_html(url, headers, 'utf-8')
File "func_scrap.py", line 25, in read_html
response = request.urlopen(req)
File "E:\Program Files\Python36\lib\urllib\request.py", line 223, in urlopen
return opener.open(url, data, timeout)
File "E:\Program Files\Python36\lib\urllib\request.py", line 526, in open
response = self._open(req, data)
File "E:\Program Files\Python36\lib\urllib\request.py", line 544, in _open
'_open', req)
File "E:\Program Files\Python36\lib\urllib\request.py", line 504, in _call_chain
result = func(*args)
File "E:\Program Files\Python36\lib\urllib\request.py", line 1361, in https_open
context=self._context, check_hostname=self._check_hostname)
File "E:\Program Files\Python36\lib\urllib\request.py", line 1320, in do_open
raise URLError(err)
urllib.error.URLError: <urlopen error [Errno 11001] getaddrinfo failed>

可以从结果中看出我们解释器检测到了一个urllib.error.URLError异常,异常信息是urlopen error [Errno 11001] getaddrinfo failed(获取地址信息失败)

为程序增加容错处理机制,让程序变得更健壮

修改后的程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from urllib import request
import re
url = 'https://www.qiushibaike1.com'
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'
headers = {'User-Agent': user_agent}
def read_html(url, headers, codec):
'''[read_html]
[读取html页面内容]
Arguments:
url {[string]} -- [url地址]
headers {[dict]} -- [用户代理,这里是一个字典类型]
codec {[string]} -- [编码方式]
Returns:
[string] -- [页面内容]
'''
# 构建一个请求对象
try:
req = request.Request(url, headers=headers)
# 打开一个请求
response = request.urlopen(req)
# 读取服务器返回的页面数据内容
content = response.read().decode(codec)
return content
except Exception as e:
print(e)
return None
def match_element(content, pattern):
'''[match_element]
[匹配元素]
Arguments:
content {[string]} -- [文本内容]
pattern {[object]} -- [匹配模式]
Returns:
[list] -- [匹配到的元素]
'''
# 匹配所有用户信息
userinfos = re.findall(pattern, content)
return userinfos
content = read_html(url, headers, 'utf-8')
pattern = re.compile(r'<div class="article block untagged mb15[\s\S]*?class="stats-vote".*?</div>', re.S)
print('执行到这里了,表示我还没有终止!')
if content:
userinfos = match_element(content, pattern)
if userinfos:
pattern = re.compile(r'<a href="(.*?)".*?<h2>(.*?)</h2>.*?<div class="content">(.*?)</div>', re.S)
for userinfo in userinfos:
item = match_element(userinfo, pattern)
#print(item)
if item:
userid, name, content = item[0]
# 去掉换行符,<span></span>,<br/>符号
userid = re.sub(r'\n|<span>|</span>|<br/>', '', userid)
name = re.sub(r'\n|<span>|</span>|<br/>', '', name)
content = re.sub(r'\n|<span>|</span>|<br/>', '', content)
print((userid, name, content))

执行结果

1
2
<urlopen error [Errno 11001] getaddrinfo failed>
执行到这里了,表示我还没有终止!

可以看到我们的程序并没有在抛出异常导致程序终止,如果我们不写try-except再去执行上面的代码,程序肯定执行不到“执行到这里了,表示我还没有终止!”这里了。大家可以试试

urllib.error.URLError异常

从上面的途中我们可以看到程序抛出了urllib.error.URLError异常,所以我们可以捕捉这个异常,这个异常的参数定义可以查看开发文档如下

urllib.error.URLError定义
所以我们可以把except Exception as e:,改写城except urllib.error.URLError as e:异常的精准捕获。


note: 做为开发人员要时刻提醒自己对异常,对容错能力的处理,让我们开发的程序能够更稳定,更坚固,让我们的程序能在服务器上长时间运行不宕机,当然这也是成为一个高手的必要素质。

欢迎关注我的公众号:「爱做饭的老谢」,老谢一直在努力…

上一篇:聚沙成塔–爬虫系列(五)(爬取糗事百科段子)
下一篇:聚沙成塔–爬虫系列(七)(妹子,快到碗里来)