Python 爬虫从零开始(四)

Python 基础

面向对象

面向对象(Object Oriented)的英文缩写是OO,它是一种设计思想。我们经常听说的面向对象编程(Object Oriented Programming,即OOP)就是主要针对大型软件设计而提出的,它可以使软件设计更加灵活,并且能更好地进行代码复用。

面向对象中的对象(Object),通常是指客观世界中存在的对象,这个对象具有唯一性,对象之间个不相同,各有各的特点,每一个对象都有自己的运动规律和内部状态;对象和对象之间又是可以相互联系、相互作用的。

对象:是一个抽象概念,英文称为Object,表示任意存在的事物。世间万物皆对象,现实世间中随处可以的一种事物就是对象,对象是事物存在的实体,如一个人。

通常将对象划分为两个部分:静态部分和动态部分。静态部分被称为【属性】,任何对象都具备自身属性,这些属性不仅是客观存在的,而且是不能被忽视的。如人的性别。动态部分指的是对象的行为,即对象执行的动作,如人可以行走。

类和实例

面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

代码示例

1
2
3
4
5
6
7
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score

def print_score(self):
print('%s: %s' % (self.name, self.score))
  • 通过class关键字来定义类,类名通常要大写,后面的括号里面表示该类继承于哪个类,所有类最终继承于object类,这类似于Java
  • python允许对实例变量绑定任何数据,因此同一个类的不同实例拥有的变量名称可能会不同
  • 特殊的__init__方法可以把必须绑定给类实例的属性填写进去,它的第一个参数永远是self,表示创建的实例本身,在创建实例的时候,必须传入与init方法匹配的参数,self除外
  • 实际上,Python中类中定义的函数的第一个参数都必须是self,调用时不用传递该参数
    对Student类使用实例代码:
1
2
3
4
5
6
7
8
chen = Student('chen', 13)
bart = Student('bart', 81)
chen.print_score()
bart.print_score()
chen.score = 100
bart.name = 'kobe'
chen.print_score()
bart.print_score()

输出结果

1
2
3
4
chen: 13
bart: 81
chen: 100
kobe: 81

访问限制

  • 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,
    实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Student(object):

    def __init__(self, name, score):
    self.__name = name
    self.__score = score

    def print_score(self):
    print('%s: %s' % (self.__name, self.__score))

    >>> bart = Student('Bart Simpson', 59)
    >>> bart.__name
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    AttributeError: 'Student' object has no attribute '__name'

这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。

但是如果外部代码要获取namescore怎么办?可以给Student类增加get_nameget_score这样的方法:

1
2
3
4
5
6
7
8
class Student(object):
...

def get_name(self):
return self.__name

def get_score(self):
return self.__score

如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:

1
2
3
4
5
class Student(object):
...

def set_score(self, score):
self.__score = score

你也许会问,原先那种直接通过bart.score = 99也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数:

1
2
3
4
5
6
7
8
class Student(object):
...

def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')

需要注意的是,在Python中,变量名类似xxx的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用namescore这样的变量名。
以一个下划线开头的变量_name外部可以访问,但是最好把它视为私有变量,不要随便访问
python解释器对外把__name改成了_Student__name,因此可以通过_Student__name来访问私有的内部属性(强烈建议不要采用这种方式):

1
2
>>> bart._Student__name
'Bart Simpson'

我们在前面发现无法输出bart.__name,会报错Student类不含有此成员,但是bart.__name = 'New Name'语 句是不会报错的,随后再输出bart.__name也不会再报错了:

1
2
3
4
5
6
>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 设置__name变量!
>>> bart.__name
'New Name'

这是一种错误的写法,这样设置的__name变量并不是类内部的__name变量,相当于给bart新增了一个变量

继承和多态

在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。

比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:

1
2
3
class Animal(object):
def run(self):
print('Animal is running...')

当我们需要编写DogCat类时,就可以直接从Animal类继承:

1
2
3
4
class Dog(Animal):
pass
class Cat(Animal):
pass

对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。CatDog类似。

继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此,DogCat作为它的子类,什么事也没干,就自动拥有了run()方法:

1
2
3
4
5
dog = Dog()
dog.run()

cat = Cat()
cat.run()

运行结果如下:

1
2
Animal is running...
Animal is running...

当然,也可以对子类增加一些方法,比如Dog类:

1
2
3
4
5
6
7
class Dog(Animal):

def run(self):
print('Dog is running...')

def eat(self):
print('Eating meat...')

继承的第二个好处需要我们对代码做一点改进。你看到了,无论是Dog还是Cat,它们run()的时候,显示的都是Animal is running...,符合逻辑的做法是分别显示Dog is running...Cat is running...,因此,对DogCat类改进如下:

1
2
3
4
5
6
7
8
9
class Dog(Animal):

def run(self):
print('Dog is running...')

class Cat(Animal):

def run(self):
print('Cat is running...')

再次运行,结果如下:

1
2
Dog is running...
Cat is running...

当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。

要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:

1
2
3
a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # c是Dog类型

判断一个变量是否是某个类型可以用isinstance()判断:

1
2
3
4
5
6
>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True

看来abc确实对应着listAnimalDog这3种类型。

但是等等,试试:

1
2
>>> isinstance(c, Animal)
True

看来c不仅仅是Dogc还是Animal

不过仔细想想,这是有道理的,因为Dog是从Animal继承下来的,当我们创建了一个Dog的实例c时,我们认为c的数据类型是Dog没错,但c同时也是Animal也没错,Dog本来就是Animal的一种!

所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行:

1
2
3
>>> b = Animal()
>>> isinstance(b, Dog)
False

Dog可以看成Animal,但Animal不可以看成Dog

要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:

1
2
3
def run_twice(animal):
animal.run()
animal.run()

当我们传入Animal的实例时,run_twice()就打印出:

1
2
3
>>> run_twice(Animal())
Animal is running...
Animal is running...

当我们传入Dog的实例时,run_twice()就打印出:

1
2
3
>>> run_twice(Dog())
Dog is running...
Dog is running...

当我们传入Cat的实例时,run_twice()就打印出:

1
2
3
>>> run_twice(Cat())
Cat is running...
Cat is running...

看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生:

1
2
3
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')

当我们调用run_twice()时,传入Tortoise的实例:

1
2
3
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...

你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。

多态的好处就是,当我们需要传入DogCatTortoise……时,我们只需要接收Animal类型就可以了,因为DogCatTortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:

对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在AnimalDogCat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:

对扩展开放:允许新增Animal子类;

对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树。比如如下的继承树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
                ┌───────────────┐
│ object │
└───────────────┘

┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Animal │ │ Plant │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Cat │ │ Tree │ │ Flower │
└─────────┘ └─────────┘ └─────────┘ └─────────┘

获取对象信息

使用type()来判断对象类型,它返回对应的Class类型。

基本类型可以使用type()来判断:

1
2
3
4
5
6
7
8
>>> type(123)
<class 'int'>
>>> type(2.0)
<class 'float'>
>>> type(["sd",'s',12])
<class 'list'>
>>> type(None)
<class 'NoneType'>

函数和类也可以使用type()来判断:

1
2
3
4
5
6
7
8
9
10
11
>>> class Person:
... pass
...
>>> def test():
... pass
...
>>> p = Person()
>>> type(p)
<class '__main__.Person'>
>>> type(test)
<class 'function'>

利用type来作为if的判断条件,当类型不是int,str等基本类型时,需要使用types模块中定义的常量:

1
2
3
4
5
6
7
8
9
10
11
12
>>> import types
>>> def fn():
... pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

使用isinstance()来判断对象是否是某种类型

isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上

能用type()判断的基本类型也可以用isinstance()判断:

1
2
3
4
5
6
>>> isinstance('a', str)
True
>>> isinstance(123, int)
True
>>> isinstance(b'a', bytes)
True

isinstance不仅可以判断是否是某种类型,还可以判断是否是某些类型的一种:

1
2
3
4
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True

使用dir()来获得一个对象的所有属性和方法

获取str的所有属性和方法:

1
2
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']

利用getattr()setattr()以及hasattr(),我们可以直接操作一个对象,不过这几个函数是在不知道对象信息的时候使用的,在了解对象信息时没有必要使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> class MyObject(object):
... def __init__(self):
... self.x = 9
... def power(self):
... return self.x * self.x
...
>>> obj = MyObject()

>>> hasattr(obj, 'x') # 有属性'x'吗?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有属性'y'吗?
False
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') # 有属性'y'吗?
True
>>> getattr(obj, 'y') # 获取属性'y'
19
>>> obj.y # 获取属性'y'
19

对于getattr()函数,可以在传入的属性参数后面加一个默认的参数,这样在对象不含有这个属性时就会返回默认参数,而不是抛出异常了:

1
2
3
# 获取属性'z',如果不存在,返回默认值404
>>> getattr(obj, 'z', 404)
404

getattr()函数还可以用于获取方法,把获取的方法赋值给一个变量,那个变量就指向这个方法,调用那个变量就相当于调用了这个方法:

1
2
3
4
5
6
7
>>> getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81

实例属性和类属性

实例属性我们在之前就已经使用过了,我们也了解到在python中可以给一个类实例绑定任何属性,方法是在类方法中利用self或者直接通过实例来绑定。

要给类绑定一个属性的话,可以在类中直接定义它:

1
2
class Student(object):
name = 'Student'

也可以把类属性成为类的静态成员变量,这个属性是归类所有的,所有实例可以共享它。
需要注意的是,虽然name属性归类Student所有,但是类的所有实例都可以访问到,并且实例属性的优先级比类属性高,所以如果实例绑定了一个与类属性同名的实例属性时,优先调用的是实例属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class Student(object):
... name = 'Student'
...
>>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student

异常处理

异常处理

捕捉异常可以使用try/except语句。

try/except语句用来检测try语句块中的错误,从而让except语句捕获异常信息并处理。

如果你不想在异常发生时结束你的程序,只需在try里捕获它。

语法:

以下为简单的try....except...else的语法:

1
2
3
4
5
6
7
8
try:
<语句> #运行别的代码
except <名字>:
<语句> #如果在try部份引发了'name'异常
except <名字>,<数据>:
<语句> #如果引发了'name'异常,获得附加的数据
else:
<语句> #如果没有异常发生

try的工作原理是,当开始一个try语句后,python就在当前程序的上下文中作标记,这样当异常出现时就可以回到这里,try子句先执行,接下来会发生什么依赖于执行时是否出现异常。

  • 如果当try后的语句执行时发生异常,python就跳回到try并执行第一个匹配该异常的except子句,异常处理完毕,控制流就通过整个try语句(除非在处理异常时又引发新的异常)。
  • 如果在try后的语句里发生了异常,却没有匹配的except子句,异常将被递交到上层的try,或者到程序的最上层(这样将结束程序,并打印缺省的出错信息)。
  • 如果在try子句执行时没有发生异常,python将执行else语句后的语句(如果有else的话),然后控制流通过整个try语句。

实例

下面是简单的例子,它打开一个文件,在该文件中的内容写入内容,且并未发生异常:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/python
# -*- coding: UTF-8 -*-

try:
fh = open("testfile", "w")
fh.write("这是一个测试文件,用于测试异常!!")
except IOError:
print "Error: 没有找到文件或读取文件失败"
else:
print "内容写入文件成功"
fh.close()

以上程序输出结果:

1
2
3
4
$ python test.py 
内容写入文件成功
$ cat testfile # 查看写入的内容
这是一个测试文件,用于测试异常!!

实例

下面是简单的例子,它打开一个文件,在该文件中的内容写入内容,但文件没有写入权限,发生了异常:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/python
# -*- coding: UTF-8 -*-

try:
fh = open("testfile", "w")
fh.write("这是一个测试文件,用于测试异常!!")
except IOError:
print "Error: 没有找到文件或读取文件失败"
else:
print "内容写入文件成功"
fh.close()

在执行代码前为了测试方便,我们可以先去掉 testfile 文件的写权限,命令如下:

1
chmod -w testfile

再执行以上代码:

1
2
$ python test.py 
Error: 没有找到文件或读取文件失败

使用except而不带任何异常类型

你可以不带任何异常类型使用except,如下实例:

1
2
3
4
5
6
7
8
try:
正常的操作
......................
except:
发生异常,执行这块代码
......................
else:
如果没有异常执行这块代码

以上方式try-except语句捕获所有发生的异常。但这不是一个很好的方式,我们不能通过该程序识别出具体的异常信息。因为它捕获所有的异常。

使用except而带多种异常类型

你也可以使用相同的except语句来处理多个异常信息,如下所示:

1
2
3
4
5
6
7
8
try:
正常的操作
......................
except(Exception1[, Exception2[,...ExceptionN]]]):
发生以上多个异常中的一个,执行这块代码
......................
else:
如果没有异常执行这块代码

try-finally 语句

try-finally 语句无论是否发生异常都将执行最后的代码。

1
2
3
4
5
try:
<语句>
finally:
<语句> #退出try时总会执行
raise

实例

1
2
3
4
5
6
7
8
#!/usr/bin/python
# -*- coding: UTF-8 -*-

try:
fh = open("testfile", "w")
fh.write("这是一个测试文件,用于测试异常!!")
finally:
print "Error: 没有找到文件或读取文件失败"

如果打开的文件没有可写权限,输出如下所示:

1
2
$ python test.py 
Error: 没有找到文件或读取文件失败

同样的例子也可以写成如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python
# -*- coding: UTF-8 -*-

try:
fh = open("testfile", "w")
try:
fh.write("这是一个测试文件,用于测试异常!!")
finally:
print "关闭文件"
fh.close()
except IOError:
print "Error: 没有找到文件或读取文件失败"

当在try块中抛出一个异常,立即执行finally块代码。

finally块中的所有语句执行后,异常被再次触发,并执行except块代码。

参数的内容不同于异常。

异常的参数

一个异常可以带上参数,可作为输出的异常信息参数。

你可以通过except语句来捕获异常的参数,如下所示:

1
2
3
4
5
try:
正常的操作
......................
except ExceptionType, Argument:
你可以在这输出 Argument 的值...

变量接收的异常值通常包含在异常的语句中。在元组的表单中变量可以接收一个或者多个值。

元组通常包含错误字符串,错误数字,错误位置。

实例
以下为单个异常的实例:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python
# -*- coding: UTF-8 -*-

# 定义函数
def temp_convert(var):
try:
return int(var)
except ValueError, Argument:
print "参数没有包含数字\n", Argument

# 调用函数
temp_convert("xyz");

以上程序执行结果如下:

1
2
3
$ python test.py 
参数没有包含数字
invalid literal for int() with base 10: 'xyz'

触发异常

我们可以使用raise语句自己触发异常

raise语法格式如下:

1
raise [Exception [, args [, traceback]]]

语句中 Exception 是异常的类型(例如,NameError)参数标准异常中任一种,args 是自已提供的异常参数。

最后一个参数是可选的(在实践中很少使用),如果存在,是跟踪异常对象。

实例
一个异常可以是一个字符串,类或对象。 Python的内核提供的异常,大多数都是实例化的类,这是一个类的实例的参数。

定义一个异常非常简单,如下所示:

1
2
3
4
def functionName( level ):
if level < 1:
raise Exception("Invalid level!", level)
# 触发异常后,后面的代码就不会再执行

注意:为了能够捕获异常,”except“语句必须有用相同的异常来抛出类对象或者字符串。

例如我们捕获以上异常,”except“语句如下所示:

1
2
3
4
5
6
try:
正常逻辑
except Exception,err:
触发自定义异常
else:
其余代码

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python
# -*- coding: UTF-8 -*-

# 定义函数
def mye( level ):
if level < 1:
raise Exception,"Invalid level!"
# 触发异常后,后面的代码就不会再执行
try:
mye(0) # 触发异常
except Exception,err:
print 1,err
else:
print 2

执行以上代码,输出结果为:

1
2
$ python test.py 
1 Invalid level!

用户自定义异常

通过创建一个新的异常类,程序可以命名它们自己的异常。异常应该是典型的继承自Exception类,通过直接或间接的方式。

以下为与RuntimeError相关的实例,实例中创建了一个类,基类为RuntimeError,用于在异常触发时输出更多的信息。

try语句块中,用户自定义的异常后执行except块语句,变量 e 是用于创建Networkerror类的实例。

1
2
3
class Networkerror(RuntimeError):
def __init__(self, arg):
self.args = arg

在你定义以上类后,你可以触发该异常,如下所示:

1
2
3
4
try:
raise Networkerror("Bad hostname")
except Networkerror,e:
print e.args