python4

datetime是Python处理日期和时间的标准库。

获取当前日期和时间

我们先看如何获取当前日期和时间:

>>> from datetime import datetime>>> now = datetime.now()# 获取当前datetime>>>print(now)2015-05-1816:28:07.198690>>>print(type(now))

注意到datetime是模块,datetime模块还包含一个datetime类,通过from datetime import datetime导入的才是datetime这个类。

如果仅导入import datetime,则必须引用全名datetime.datetime。

datetime.now()返回当前日期和时间,其类型是datetime。

获取指定日期和时间

要指定某个日期和时间,我们直接用参数构造一个datetime:

>>> fromdatetimeimportdatetime>>> dt = datetime(2015,4,19,12,20)# 用指定日期时间创建datetime>>> print(dt)2015-04-1912:20:00

datetime转换为timestamp

在计算机中,时间实际上是用数字表示的。我们把1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time,记为0(1970年以前的时间timestamp为负数),当前时间就是相对于epoch time的秒数,称为timestamp。

你可以认为:

timestamp =0=1970-1-100:00:00UTC+0:00

对应的北京时间是:

timestamp =0=1970-1-108:00:00UTC+8:00

可见timestamp的值与时区毫无关系,因为timestamp一旦确定,其UTC时间就确定了,转换到任意时区的时间也是完全确定的,这就是为什么计算机存储的当前时间是以timestamp表示的,因为全球各地的计算机在任意时刻的timestamp都是完全相同的(假定时间已校准)。

把一个datetime类型转换为timestamp只需要简单调用timestamp()方法:

>>> fromdatetimeimportdatetime>>> dt = datetime(2015,4,19,12,20)# 用指定日期时间创建datetime>>> dt.timestamp()# 把datetime转换为timestamp1429417200.0

注意Python的timestamp是一个浮点数。如果有小数位,小数位表示毫秒数。

某些编程语言(如Java和JavaScript)的timestamp使用整数表示毫秒数,这种情况下只需要把timestamp除以1000就得到Python的浮点表示方法。

timestamp转换为datetime

要把timestamp转换为datetime,使用datetime提供的fromtimestamp()方法:

>>> fromdatetimeimportdatetime>>> t =1429417200.0>>> print(datetime.fromtimestamp(t))2015-04-1912:20:00

注意到timestamp是一个浮点数,它没有时区的概念,而datetime是有时区的。上述转换是在timestamp和本地时间做转换。

本地时间是指当前操作系统设定的时区。例如北京时区是东8区,则本地时间:

2015-04-19 12:20:00

实际上就是UTC+8:00时区的时间:

2015-04-19 12:20:00UTC+8:00

而此刻的格林威治标准时间与北京时间差了8小时,也就是UTC+0:00时区的时间应该是:

2015-04-19 04:20:00UTC+0:00

timestamp也可以直接被转换到UTC标准时区的时间:

>>> fromdatetimeimportdatetime>>> t =1429417200.0>>> print(datetime.fromtimestamp(t))# 本地时间2015-04-1912:20:00>>> print(datetime.utcfromtimestamp(t))# UTC时间2015-04-1904:20:00

str转换为datetime

很多时候,用户输入的日期和时间是字符串,要处理日期和时间,首先必须把str转换为datetime。转换方法是通过datetime.strptime()实现,需要一个日期和时间的格式化字符串:

>>> fromdatetimeimportdatetime>>> cday = datetime.strptime('2015-6-1 18:19:59','%Y-%m-%d %H:%M:%S')>>> print(cday)2015-06-0118:19:59

字符串'%Y-%m-%d %H:%M:%S'规定了日期和时间部分的格式。详细的说明请参考Python文档

注意转换后的datetime是没有时区信息的。

datetime转换为str

如果已经有了datetime对象,要把它格式化为字符串显示给用户,就需要转换为str,转换方法是通过strftime()实现的,同样需要一个日期和时间的格式化字符串:

>>> fromdatetimeimportdatetime>>> now = datetime.now()>>> print(now.strftime('%a, %b %d %H:%M'))Mon, May0516:28

datetime加减

对日期和时间进行加减实际上就是把datetime往后或往前计算,得到新的datetime。加减可以直接用+和-运算符,不过需要导入timedelta这个类:

>>> fromdatetimeimportdatetime, timedelta>>> now = datetime.now()>>> nowdatetime.datetime(2015,5,18,16,57,3,540997)>>> now + timedelta(hours=10)datetime.datetime(2015,5,19,2,57,3,540997)>>> now - timedelta(days=1)datetime.datetime(2015,5,17,16,57,3,540997)>>> now + timedelta(days=2, hours=12)datetime.datetime(2015,5,21,4,57,3,540997)

可见,使用timedelta你可以很容易地算出前几天和后几天的时刻。

本地时间转换为UTC时间

本地时间是指系统设定时区的时间,例如北京时间是UTC+8:00时区的时间,而UTC时间指UTC+0:00时区的时间。

一个datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个时区,除非强行给datetime设置一个时区:

>>> fromdatetimeimportdatetime, timedelta, timezone>>> tz_utc_8 = timezone(timedelta(hours=8))# 创建时区UTC+8:00>>> now = datetime.now()>>> nowdatetime.datetime(2015,5,18,17,2,10,871012)>>> dt = now.replace(tzinfo=tz_utc_8)# 强制设置为UTC+8:00>>> dtdatetime.datetime(2015,5,18,17,2,10,871012, tzinfo=datetime.timezone(datetime.timedelta(0,28800)))

如果系统时区恰好是UTC+8:00,那么上述代码就是正确的,否则,不能强制设置为UTC+8:00时区。

时区转换

我们可以先通过utcnow()拿到当前的UTC时间,再转换为任意时区的时间:

# 拿到UTC时间,并强制设置时区为UTC+0:00:>>> utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)>>> print(utc_dt)2015-05-1809:05:12.377316+00:00# astimezone()将转换时区为北京时间:>>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))>>> print(bj_dt)2015-05-1817:05:12.377316+08:00# astimezone()将转换时区为东京时间:>>> tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))>>> print(tokyo_dt)2015-05-1818:05:12.377316+09:00# astimezone()将bj_dt转换时区为东京时间:>>> tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9)))>>> print(tokyo_dt2)2015-05-1818:05:12.377316+09:00

时区转换的关键在于,拿到一个datetime时,要获知其正确的时区,然后强制设置时区,作为基准时间。

利用带时区的datetime,通过astimezone()方法,可以转换到任意时区。

注:不是必须从UTC+0:00时区转换到其他时区,任何带时区的datetime都可以正确转换,例如上述bj_dt到tokyo_dt的转换。

collections是Python内建的一个集合模块,提供了许多有用的集合类。

namedtuple

我们知道tuple可以表示不变集合,例如,一个点的二维坐标就可以表示成:

>>> p = (1,2)

但是,看到(1, 2),很难看出这个tuple是用来表示一个坐标的。

定义一个class又小题大做了,这时,namedtuple就派上了用场:

>>> fromcollectionsimportnamedtuple>>> Point = namedtuple('Point', ['x','y'])>>> p = Point(1,2)>>> p.x1>>> p.y2

namedtuple是一个函数,它用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素。

这样一来,我们用namedtuple可以很方便地定义一种数据类型,它具备tuple的不变性,又可以根据属性来引用,使用十分方便。

可以验证创建的Point对象是tuple的一种子类:

>>> isinstance(p, Point)True>>> isinstance(p, tuple)True

类似的,如果要用坐标和半径表示一个圆,也可以用namedtuple定义:

# namedtuple('名称', [属性list]):Circle= namedtuple('Circle', ['x','y','r'])

deque

使用list存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list是线性存储,数据量大的时候,插入和删除效率很低。

deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈:

>>> fromcollectionsimportdeque>>> q = deque(['a','b','c'])>>> q.append('x')>>> q.appendleft('y')>>> qdeque(['y','a','b','c','x'])

deque除了实现list的append()和pop()外,还支持appendleft()和popleft(),这样就可以非常高效地往头部添加或删除元素。

defaultdict

使用dict时,如果引用的Key不存在,就会抛出KeyError。如果希望key不存在时,返回一个默认值,就可以用defaultdict:

>>> fromcollectionsimportdefaultdict>>> dd = defaultdict(lambda:'N/A')>>> dd['key1'] ='abc'>>> dd['key1']# key1存在'abc'>>> dd['key2']# key2不存在,返回默认值'N/A'

注意默认值是调用函数返回的,而函数在创建defaultdict对象时传入。

除了在Key不存在时返回默认值,defaultdict的其他行为跟dict是完全一样的。

OrderedDict

使用dict时,Key是无序的。在对dict做迭代时,我们无法确定Key的顺序。

如果要保持Key的顺序,可以用OrderedDict:

>>> fromcollectionsimportOrderedDict>>> d = dict([('a',1), ('b',2), ('c',3)])>>> d# dict的Key是无序的{'a':1,'c':3,'b':2}>>> od = OrderedDict([('a',1), ('b',2), ('c',3)])>>> od# OrderedDict的Key是有序的OrderedDict([('a',1), ('b',2), ('c',3)])

注意,OrderedDict的Key会按照插入的顺序排列,不是Key本身排序:

>>> od = OrderedDict()>>> od['z'] =1>>> od['y'] =2>>> od['x'] =3>>> list(od.keys())# 按照插入的Key的顺序返回['z','y','x']

OrderedDict可以实现一个FIFO(先进先出)的dict,当容量超出限制时,先删除最早添加的Key:

fromcollectionsimportOrderedDictclassLastUpdatedOrderedDict(OrderedDict):def__init__(self, capacity):super(LastUpdatedOrderedDict, self).__init__()        self._capacity = capacitydef__setitem__(self, key, value):containsKey =1ifkeyinselfelse0iflen(self) - containsKey >= self._capacity:            last = self.popitem(last=False)            print('remove:', last)ifcontainsKey:delself[key]            print('set:', (key, value))else:            print('add:', (key, value))        OrderedDict.__setitem__(self, key, value)

ChainMap

ChainMap可以把一组dict串起来并组成一个逻辑上的dict。ChainMap本身也是一个dict,但是查找的时候,会按照顺序在内部的dict依次查找。

什么时候使用ChainMap最合适?举个例子:应用程序往往都需要传入参数,参数可以通过命令行传入,可以通过环境变量传入,还可以有默认参数。我们可以用ChainMap实现参数的优先级查找,即先查命令行参数,如果没有传入,再查环境变量,如果没有,就使用默认参数。

下面的代码演示了如何查找user和color这两个参数:

fromcollectionsimportChainMapimportos, argparse# 构造缺省参数:defaults = {'color':'red','user':'guest'}# 构造命令行参数:parser = argparse.ArgumentParser()parser.add_argument('-u','--user')parser.add_argument('-c','--color')namespace = parser.parse_args()command_line_args = { k: vfork, vinvars(namespace).items()ifv }# 组合成ChainMap:combined = ChainMap(command_line_args, os.environ, defaults)# 打印参数:print('color=%s'% combined['color'])print('user=%s'% combined['user'])

没有任何参数时,打印出默认参数:

$ python3 use_chainmap.py color=reduser=guest

当传入命令行参数时,优先使用命令行参数:

$ python3 use_chainmap.py -u bobcolor=reduser=bob

同时传入命令行参数和环境变量,命令行参数的优先级较高:

$ user=admin color=green python3 use_chainmap.py -u bobcolor=greenuser=bob

Counter

Counter是一个简单的计数器,例如,统计字符出现的个数:

>>> fromcollectionsimportCounter>>> c = Counter()>>> forchin'programming':... c[ch] = c[ch] +1...>>> cCounter({'g':2,'m':2,'r':2,'a':1,'i':1,'o':1,'n':1,'p':1})

Counter实际上也是dict的一个子类,上面的结果可以看出,字符'g'、'm'、'r'各出现了两次,其他字符各出现了一次。

Base64是一种用64个字符来表示任意二进制数据的方法。

用记事本打开exe、jpg、pdf这些文件时,我们都会看到一大堆乱码,因为二进制文件包含很多无法显示和打印的字符,所以,如果要让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法。

Base64的原理很简单,首先,准备一个包含64个字符的数组:

['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']

然后,对二进制数据进行处理,每3个字节一组,一共是3x8=24bit,划为4组,每组正好6个bit:

这样我们得到4个数字作为索引,然后查表,获得相应的4个字符,就是编码后的字符串。

所以,Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。

如果要编码的二进制数据不是3的倍数,最后会剩下1个或2个字节怎么办?Base64用\x00字节在末尾补足后,再在编码的末尾加上1个或2个=号,表示补了多少字节,解码的时候,会自动去掉。

Python内置的base64可以直接进行base64的编解码:

>>> importbase64>>> base64.b64encode(b'binary\x00string')b'YmluYXJ5AHN0cmluZw=='>>> base64.b64decode(b'YmluYXJ5AHN0cmluZw==')b'binary\x00string'

由于标准的Base64编码后可能出现字符+和/,在URL中就不能直接作为参数,所以又有一种"url safe"的base64编码,其实就是把字符+和/分别变成-和_:

>>> base64.b64encode(b'i\xb7\x1d\xfb\xef\xff')b'abcd++//'>>> base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xff')b'abcd--__'>>> base64.urlsafe_b64decode('abcd--__')b'i\xb7\x1d\xfb\xef\xff'

还可以自己定义64个字符的排列顺序,这样就可以自定义Base64编码,不过,通常情况下完全没有必要。

Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。

Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等。

由于=字符也可能出现在Base64编码中,但=用在URL、Cookie里面会造成歧义,所以,很多Base64编码后会把=去掉:

# 标准Base64:'abcd'->'YWJjZA=='# 自动去掉=:'abcd'->'YWJjZA'

去掉=后怎么解码呢?因为Base64是把3个字节变为4个字节,所以,Base64编码的长度永远是4的倍数,因此,需要加上=把Base64字符串的长度变为4的倍数,就可以正常解码了。

准确地讲,Python没有专门处理字节的数据类型。但由于b'str'可以表示字节,所以,字节数组=二进制str。而在C语言中,我们可以很方便地用struct、union来处理字节,以及字节和int,float的转换。

在Python中,比方说要把一个32位无符号整数变成字节,也就是4个长度的bytes,你得配合位运算符这么写:

>>> n =10240099>>> b1 = (n &0xff000000) >>24>>> b2 = (n &0xff0000) >>16>>> b3 = (n &0xff00) >>8>>> b4 = n &0xff>>> bs = bytes([b1, b2, b3, b4])>>> bsb'\x00\x9c@c'

非常麻烦。如果换成浮点数就无能为力了。

好在Python提供了一个struct模块来解决bytes和其他二进制数据类型的转换。

struct的pack函数把任意数据类型变成bytes:

>>> importstruct>>> struct.pack('>I',10240099)b'\x00\x9c@c'

pack的第一个参数是处理指令,'>I'的意思是:

>表示字节顺序是big-endian,也就是网络序,I表示4字节无符号整数。

后面的参数个数要和处理指令一致。

unpack把bytes变成相应的数据类型:

>>> struct.unpack('>IH',b'\xf0\xf0\xf0\xf0\x80\x80')(4042322160,32896)

根据>IH的说明,后面的bytes依次变为I:4字节无符号整数和H:2字节无符号整数。

所以,尽管Python不适合编写底层操作字节流的代码,但在对性能要求不高的地方,利用struct就方便多了。

struct模块定义的数据类型可以参考Python官方文档:

https://docs.python.org/3/library/struct.html#format-characters

Windows的位图文件(.bmp)是一种非常简单的文件格式,我们来用struct分析一下。

首先找一个bmp文件,没有的话用“画图”画一个。

读入前30个字节来分析:

>>> s =b'\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00'

BMP格式采用小端方式存储数据,文件头的结构按顺序如下:

两个字节:'BM'表示Windows位图,'BA'表示OS/2位图; 一个4字节整数:表示位图大小; 一个4字节整数:保留位,始终为0; 一个4字节整数:实际图像的偏移量; 一个4字节整数:Header的字节数; 一个4字节整数:图像宽度; 一个4字节整数:图像高度; 一个2字节整数:始终为1; 一个2字节整数:颜色数。

所以,组合起来用unpack读取:

>>> struct.unpack('

结果显示,b'B'、b'M'说明是Windows位图,位图大小为640x360,颜色数为24。

请编写一个bmpinfo.py,可以检查任意文件是否是位图文件,如果是,打印出图片大小和颜色数。

摘要算法简介

Python的hashlib提供了常见的摘要算法,如MD5,SHA1等等。

什么是摘要算法呢?摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。

举个例子,你写了一篇文章,内容是一个字符串'how to use python hashlib - by Michael',并附上这篇文章的摘要是'2d73d4f15c0db7f5ecb321b6a65e5d6d'。如果有人篡改了你的文章,并发表为'how to use python hashlib - by Bob',你可以一下子指出Bob篡改了你的文章,因为根据'how to use python hashlib - by Bob'计算出的摘要不同于原始文章的摘要。

可见,摘要算法就是通过摘要函数f()对任意长度的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被人篡改过。

摘要算法之所以能指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算f(data)很容易,但通过digest反推data却非常困难。而且,对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。

我们以常见的摘要算法MD5为例,计算出一个字符串的MD5值:

importhashlibmd5 = hashlib.md5()md5.update('how to use md5 in python hashlib?'.encode('utf-8'))print(md5.hexdigest())

计算结果如下:

d26a53750bc40b38b65a520292f69306

如果数据量很大,可以分块多次调用update(),最后计算的结果是一样的:

import hashlibmd5 = hashlib.md5()md5.update('how to use md5 in '.encode('utf-8'))md5.update('python hashlib?'.encode('utf-8'))print(md5.hexdigest())

试试改动一个字母,看看计算的结果是否完全不同。

MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,通常用一个32位的16进制字符串表示。

另一种常见的摘要算法是SHA1,调用SHA1和调用MD5完全类似:

import hashlibsha1 = hashlib.sha1()sha1.update('how to use sha1 in '.encode('utf-8'))sha1.update('python hashlib?'.encode('utf-8'))print(sha1.hexdigest())

SHA1的结果是160 bit字节,通常用一个40位的16进制字符串表示。

比SHA1更安全的算法是SHA256和SHA512,不过越安全的算法不仅越慢,而且摘要长度更长。

有没有可能两个不同的数据通过某个摘要算法得到了相同的摘要?完全有可能,因为任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。这种情况称为碰撞,比如Bob试图根据你的摘要反推出一篇文章'how to learn hashlib in python - by Bob',并且这篇文章的摘要恰好和你的文章完全一致,这种情况也并非不可能出现,但是非常非常困难。

摘要算法应用

摘要算法能应用到什么地方?举个常用例子:

任何允许用户登录的网站都会存储用户登录的用户名和口令。如何存储用户名和口令呢?方法是存到数据库表中:

namepassword

michael123456

bobabc999

alicealice2008

如果以明文保存用户口令,如果数据库泄露,所有用户的口令就落入黑客的手里。此外,网站运维人员是可以访问数据库的,也就是能获取到所有用户的口令。

正确的保存口令的方式是不存储用户的明文口令,而是存储用户口令的摘要,比如MD5:

usernamepassword

michaele10adc3949ba59abbe56e057f20f883e

bob878ef96e86145580c38c87f0410ad153

alice99b1c2188db85afee403b1536010c2c9

当用户登录时,首先计算用户输入的明文口令的MD5,然后和数据库存储的MD5对比,如果一致,说明口令输入正确,如果不一致,口令肯定错误。通过哈希算法,我们可以验证一段数据是否有效,方法就是对比该数据的哈希值,例如,判断用户口令是否正确,我们用保存在数据库中的password_md5对比计算md5(password)的结果,如果一致,用户输入的口令就是正确的。

为了防止黑客通过彩虹表根据哈希值反推原始口令,在计算哈希的时候,不能仅针对原始输入计算,需要增加一个salt来使得相同的输入也能得到不同的哈希,这样,大大增加了黑客破解的难度。

如果salt是我们自己随机生成的,通常我们计算MD5时采用md5(message + salt)。但实际上,把salt看做一个“口令”,加salt的哈希就是:计算一段message的哈希时,根据不通口令计算出不同的哈希。要验证哈希值,必须同时提供正确的口令。

这实际上就是Hmac算法:Keyed-Hashing for Message Authentication。它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。

和我们自定义的加salt算法不同,Hmac算法针对所有哈希算法都通用,无论是MD5还是SHA-1。采用Hmac替代我们自己的salt算法,可以使程序算法更标准化,也更安全。

Python自带的hmac模块实现了标准的Hmac算法。我们来看看如何使用hmac实现带key的哈希。

我们首先需要准备待计算的原始消息message,随机key,哈希算法,这里采用MD5,使用hmac的代码如下:

>>> importhmac>>> message =b'Hello, world!'>>> key =b'secret'>>> h = hmac.new(key, message, digestmod='MD5')>>> # 如果消息很长,可以多次调用h.update(msg)>>> h.hexdigest()'fa4ee7d173f2d97ee79022d1a7355bcf'

可见使用hmac和普通hash算法非常类似。hmac输出的长度和原始哈希算法的长度一致。需要注意传入的key和message都是bytes类型,str类型需要首先编码为bytes。

Python的内建模块itertools提供了非常有用的用于操作迭代对象的函数。

首先,我们看看itertools提供的几个“无限”迭代器:

>>> importitertools>>> natuals = itertools.count(1)>>> forninnatuals:... print(n)...123...

因为count()会创建一个无限的迭代器,所以上述代码会打印出自然数序列,根本停不下来,只能按Ctrl+C退出。

cycle()会把传入的一个序列无限重复下去:

>>> importitertools>>> cs = itertools.cycle('ABC')# 注意字符串也是序列的一种>>> forcincs:... print(c)...'A''B''C''A''B''C'...

同样停不下来。

repeat()负责把一个元素无限重复下去,不过如果提供第二个参数就可以限定重复次数:

>>> ns = itertools.repeat('A',3)>>> forninns:... print(n)...AAA

无限序列只有在for迭代时才会无限地迭代下去,如果只是创建了一个迭代对象,它不会事先把无限个元素生成出来,事实上也不可能在内存中创建无限多个元素。

无限序列虽然可以无限迭代下去,但是通常我们会通过takewhile()等函数根据条件判断来截取出一个有限的序列:

>>> natuals = itertools.count(1)>>> ns = itertools.takewhile(lambdax: x <=10, natuals)>>> list(ns)[1,2,3,4,5,6,7,8,9,10]

itertools提供的几个迭代器操作函数更加有用:

chain()

chain()可以把一组迭代对象串联起来,形成一个更大的迭代器:

>>> forcinitertools.chain('ABC','XYZ'):... print(c)# 迭代效果:'A' 'B' 'C' 'X' 'Y' 'Z'

groupby()

groupby()把迭代器中相邻的重复元素挑出来放在一起:

>>> forkey, groupinitertools.groupby('AAABBBCCAAA'):... print(key, list(group))...A ['A','A','A']B ['B','B','B']C ['C','C']A ['A','A','A']

实际上挑选规则是通过函数完成的,只要作用于函数的两个元素返回的值相等,这两个元素就被认为是在一组的,而函数返回值作为组的key。如果我们要忽略大小写分组,就可以让元素'A'和'a'都返回相同的key:

>>> forkey, groupinitertools.groupby('AaaBBbcCAAa',lambdac: c.upper()):... print(key, list(group))...A ['A','a','a']B ['B','B','b']C ['c','C']A ['A','A','a']

在Python中,读写文件这样的资源要特别注意,必须在使用完毕后正确关闭它们。正确关闭文件资源的一个方法是使用try...finally:

try:    f = open('/path/to/file','r')    f.read()finally:iff:        f.close()

写try...finally非常繁琐。Python的with语句允许我们非常方便地使用资源,而不必担心资源没有关闭,所以上面的代码可以简化为:

withopen('/path/to/file','r')asf:    f.read()

并不是只有open()函数返回的fp对象才能使用with语句。实际上,任何对象,只要正确实现了上下文管理,就可以用于with语句。

实现上下文管理是通过__enter__和__exit__这两个方法实现的。例如,下面的class实现了这两个方法:

classQuery(object):def__init__(self, name):self.name = namedef__enter__(self):print('Begin')returnselfdef__exit__(self, exc_type, exc_value, traceback):ifexc_type:            print('Error')else:            print('End')defquery(self):print('Query info about %s...'% self.name)

这样我们就可以把自己写的资源对象用于with语句:

withQuery('Bob')asq:    q.query()

@contextmanager

编写__enter__和__exit__仍然很繁琐,因此Python的标准库contextlib提供了更简单的写法,上面的代码可以改写如下:

fromcontextlibimportcontextmanagerclassQuery(object):def__init__(self, name):self.name = namedefquery(self):print('Query info about %s...'% self.name)@contextmanagerdefcreate_query(name):print('Begin')    q = Query(name)yieldq    print('End')

@contextmanager这个decorator接受一个generator,用yield语句把with ... as var把变量输出出去,然后,with语句就可以正常地工作了:

withcreate_query('Bob')asq:    q.query()

很多时候,我们希望在某段代码执行前后自动执行特定代码,也可以用@contextmanager实现。例如:

@contextmanagerdeftag(name):print("<%s>"% name)yieldprint("</%s>"% name)withtag("h1"):    print("hello")    print("world")

上述代码执行结果为:

helloworld

代码的执行顺序是:

with语句首先执行yield之前的语句,因此打印出<h1>;

yield调用会执行with语句内部的所有语句,因此打印出hello和world;

最后执行yield之后的语句,打印出</h1>。

因此,@contextmanager让我们通过编写generator来简化上下文管理。

@closing

如果一个对象没有实现上下文,我们就不能把它用于with语句。这个时候,可以用closing()来把该对象变为上下文对象。例如,用with语句使用urlopen():

fromcontextlibimportclosingfromurllib.requestimporturlopenwithclosing(urlopen('https://www.python.org'))aspage:forlineinpage:        print(line)

closing也是一个经过@contextmanager装饰的generator,这个generator编写起来其实非常简单:

@contextmanagerdefclosing(thing):try:yieldthingfinally:        thing.close()

它的作用就是把任意对象变为上下文对象,并支持with语句。

@contextlib还有一些其他decorator,便于我们编写更简洁的代码。

urllib提供了一系列用于操作URL的功能。

Get

urllib的request模块可以非常方便地抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的响应:

例如,对豆瓣的一个URLhttps://api.douban.com/v2/book/2129650进行抓取,并返回响应:

fromurllibimportrequestwithrequest.urlopen('https://api.douban.com/v2/book/2129650')asf:    data = f.read()    print('Status:', f.status, f.reason)fork, vinf.getheaders():        print('%s: %s'% (k, v))    print('Data:', data.decode('utf-8'))

可以看到HTTP响应的头和JSON数据:

Status: 200 OKServer: nginxDate: Tue, 26 May 2015 10:02:27 GMTContent-Type: application/json; charset=utf-8Content-Length:2049Connection:closeExpires: Sun,1Jan200601:00:00GMTPragma:no-cacheCache-Control: must-revalidate,no-cache, privateX-DAE-Node: pidl1Data: {"rating":{"max":10,"numRaters":16,"average":"7.4","min":0},"subtitle":"","author":["廖雪峰编著"],"pubdate":"2007-6",...}

如果我们要想模拟浏览器发送GET请求,就需要使用Request对象,通过往Request对象添加HTTP头,我们就可以把请求伪装成浏览器。例如,模拟iPhone 6去请求豆瓣首页:

fromurllibimportrequestreq = request.Request('http://www.douban.com/')req.add_header('User-Agent','Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')withrequest.urlopen(req)asf:    print('Status:', f.status, f.reason)fork, vinf.getheaders():        print('%s: %s'% (k, v))    print('Data:', f.read().decode('utf-8'))

这样豆瓣会返回适合iPhone的移动版网页:

......

Post

如果要以POST发送一个请求,只需要把参数data以bytes形式传入。

我们模拟一个微博登录,先读取登录的邮箱和口令,然后按照weibo.cn的登录页的格式以username=xxx&password=xxx的编码传入:

fromurllibimportrequest, parseprint('Login to weibo.cn...')email = input('Email: ')passwd = input('Password: ')login_data = parse.urlencode([    ('username', email),    ('password', passwd),    ('entry','mweibo'),    ('client_id',''),    ('savestate','1'),    ('ec',''),    ('pagerefer','https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')])req = request.Request('https://passport.weibo.cn/sso/login')req.add_header('Origin','https://passport.weibo.cn')req.add_header('User-Agent','Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')req.add_header('Referer','https://passport.weibo.cn/signin/login?entry=mweibo&res=wel&wm=3349&r=http%3A%2F%2Fm.weibo.cn%2F')withrequest.urlopen(req, data=login_data.encode('utf-8'))asf:    print('Status:', f.status, f.reason)fork, vinf.getheaders():        print('%s: %s'% (k, v))    print('Data:', f.read().decode('utf-8'))

如果登录成功,我们获得的响应如下:

Status: 200 OKServer: nginx/1.2.0...Set-Cookie: SSOLoginState=1432620126;path=/; domain=weibo.cn...Data: {"retcode":20000000,"msg":"","data":{...,"uid":"1658384301"}}

如果登录失败,我们获得的响应如下:

...

Data: {"retcode":50011015,"msg":"\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef","data":{"username":"example@python.org","errline":536}}

Handler

如果还需要更复杂的控制,比如通过一个Proxy去访问网站,我们需要利用ProxyHandler来处理,示例代码如下:

proxy_handler= urllib.request.ProxyHandler({'http':'http://www.example.com:3128/'})proxy_auth_handler = urllib.request.ProxyBasicAuthHandler()proxy_auth_handler.add_password('realm','host','username','password')opener = urllib.request.build_opener(proxy_handler, proxy_auth_handler)withopener.open('http://www.example.com/login.html')asf:    pass

XML虽然比JSON复杂,在Web中应用也不如以前多了,不过仍有很多地方在用,所以,有必要了解如何操作XML。

DOM vs SAX

操作XML有两种方法:DOM和SAX。DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。SAX是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。

正常情况下,优先考虑SAX,因为DOM实在太占内存。

在Python中使用SAX解析XML非常简洁,通常我们关心的事件是start_element,end_element和char_data,准备好这3个函数,然后就可以解析xml了。

举个例子,当SAX解析器读到一个节点时:

python

会产生3个事件:

start_element事件,在读取<a href="/">时;

char_data事件,在读取python时;

end_element事件,在读取</a>时。

用代码实验一下:

fromxml.parsers.expatimportParserCreateclassDefaultSaxHandler(object):defstart_element(self, name, attrs):print('sax:start_element: %s, attrs: %s'% (name, str(attrs)))defend_element(self, name):print('sax:end_element: %s'% name)defchar_data(self, text):print('sax:char_data: %s'% text)xml =r'''<?xml version="1.0"?>

<ol>

    <li><a href="/python">Python</a></li>

    <li><a href="/ruby">Ruby</a></li>

</ol>

'''handler = DefaultSaxHandler()parser = ParserCreate()parser.StartElementHandler = handler.start_elementparser.EndElementHandler = handler.end_elementparser.CharacterDataHandler = handler.char_dataparser.Parse(xml)

需要注意的是读取一大段字符串时,CharacterDataHandler可能被多次调用,所以需要自己保存起来,在EndElementHandler里面再合并。

除了解析XML外,如何生成XML呢?99%的情况下需要生成的XML结构都是非常简单的,因此,最简单也是最有效的生成XML的方法是拼接字符串:

L = []L.append(r'<?xml version="1.0"?>')L.append(r'<root>')L.append(encode('some & data'))L.append(r'</root>')return''.join(L)

如果要生成复杂的XML呢?建议你不要用XML,改成JSON。

如果我们要编写一个搜索引擎,第一步是用爬虫把目标网站的页面抓下来,第二步就是解析该HTML页面,看看里面的内容到底是新闻、图片还是视频。

假设第一步已经完成了,第二步应该如何解析HTML呢?

HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML。

好在Python提供了HTMLParser来非常方便地解析HTML,只需简单几行代码:

fromhtml.parserimportHTMLParserfromhtml.entitiesimportname2codepointclassMyHTMLParser(HTMLParser):defhandle_starttag(self, tag, attrs):print('<%s>'% tag)defhandle_endtag(self, tag):print('</%s>'% tag)defhandle_startendtag(self, tag, attrs):print('<%s/>'% tag)defhandle_data(self, data):print(data)defhandle_comment(self, data):print('<!--', data,'-->')defhandle_entityref(self, name):print('&%s;'% name)defhandle_charref(self, name):print('&#%s;'% name)parser = MyHTMLParser()parser.feed('''<html>

<head></head>

<body>

<!-- test html parser -->

    <p>Some <a href=\"#\">html</a> HTML&nbsp;tutorial...<br>END</p>

</body></html>''')

feed()方法可以多次调用,也就是不一定一次把整个HTML字符串都塞进去,可以一部分一部分塞进去。

特殊字符有两种,一种是英文表示的&nbsp;,一种是数字表示的&#1234;,这两种字符都可以通过Parser解析出来。

PIL:Python Imaging Library,已经是Python平台事实上的图像处理标准库了。PIL功能非常强大,但API却非常简单易用。

由于PIL仅支持到Python 2.7,加上年久失修,于是一群志愿者在PIL的基础上创建了兼容的版本,名字叫Pillow,支持最新Python 3.x,又加入了许多新特性,因此,我们可以直接安装使用Pillow。

安装Pillow

如果安装了Anaconda,Pillow就已经可用了。否则,需要在命令行下通过pip安装:

$ pip install pillow

如果遇到Permission denied安装失败,请加上sudo重试。

操作图像

来看看最常见的图像缩放操作,只需三四行代码:

from PIL import Image# 打开一个jpg图像文件,注意是当前路径:im = Image.open('test.jpg')# 获得图像尺寸:w, h = im.sizeprint('Original image size: %sx%s'% (w, h))# 缩放到50%:im.thumbnail((w//2, h//2))print('Resize image to: %sx%s'% (w//2, h//2))# 把缩放后的图像用jpeg格式保存:im.save('thumbnail.jpg','jpeg')

其他功能如切片、旋转、滤镜、输出文字、调色板等一应俱全。

比如,模糊效果也只需几行代码:

fromPILimportImage, ImageFilter# 打开一个jpg图像文件,注意是当前路径:im = Image.open('test.jpg')# 应用模糊滤镜:im2 = im.filter(ImageFilter.BLUR)im2.save('blur.jpg','jpeg')

效果如下:

PIL的ImageDraw提供了一系列绘图方法,让我们可以直接绘图。比如要生成字母验证码图片:

fromPILimportImage, ImageDraw, ImageFont, ImageFilterimportrandom# 随机字母:defrndChar():returnchr(random.randint(65,90))# 随机颜色1:defrndColor():return(random.randint(64,255), random.randint(64,255), random.randint(64,255))# 随机颜色2:defrndColor2():return(random.randint(32,127), random.randint(32,127), random.randint(32,127))# 240 x 60:width =60*4height =60image = Image.new('RGB', (width, height), (255,255,255))# 创建Font对象:font = ImageFont.truetype('Arial.ttf',36)# 创建Draw对象:draw = ImageDraw.Draw(image)# 填充每个像素:forxinrange(width):foryinrange(height):        draw.point((x, y), fill=rndColor())# 输出文字:fortinrange(4):    draw.text((60* t +10,10), rndChar(), font=font, fill=rndColor2())# 模糊:image = image.filter(ImageFilter.BLUR)image.save('code.jpg','jpeg')

我们用随机颜色填充背景,再画上文字,最后对图像进行模糊,得到验证码图片如下:

如果运行的时候报错:

IOError: cannot open resource

这是因为PIL无法定位到字体文件的位置,可以根据操作系统提供绝对路径,比如:

'/Library/Fonts/Arial.ttf'

要详细了解PIL的强大功能,请请参考Pillow官方文档:

https://pillow.readthedocs.org/

我们已经讲解了Python内置的urllib模块,用于访问网络资源。但是,它用起来比较麻烦,而且,缺少很多实用的高级功能。

更好的方案是使用requests。它是一个Python第三方库,处理URL资源特别方便。

安装requests

如果安装了Anaconda,requests就已经可用了。否则,需要在命令行下通过pip安装:

$ pip install requests

如果遇到Permission denied安装失败,请加上sudo重试。

使用requests

要通过GET访问一个页面,只需要几行代码:

>>> import requests>>> r = requests.get('https://www.douban.com/') # 豆瓣首页>>> r.status_code200>>> r.textr.text'<!DOCTYPE HTML>\n\n\n

对于带参数的URL,传入一个dict作为params参数:

>>> r = requests.get('https://www.douban.com/search', params={'q':'python','cat':'1001'})>>> r.url# 实际请求的URL'https://www.douban.com/search?q=python&cat=1001'

requests自动检测编码,可以使用encoding属性查看:

>>> r.encoding'utf-8'

无论响应是文本还是二进制内容,我们都可以用content属性获得bytes对象:

>>> r.contentb'<!DOCTYPE html>\n\n\n\n...'

requests的方便之处还在于,对于特定类型的响应,例如JSON,可以直接获取:

>>> r = requests.get('https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20%3D%202151330&format=json')

>>> r.json()

{'query': {'count': 1, 'created': '2017-11-17T07:14:12Z', ...

需要传入HTTP Header时,我们传入一个dict作为headers参数:

>>> r = requests.get('https://www.douban.com/', headers={'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit'})>>> r.text'<!DOCTYPE html>\n\n\n\n豆瓣(手机版)...'

要发送POST请求,只需要把get()方法变成post(),然后传入data参数作为POST请求的数据:

>>> r = requests.post('https://accounts.douban.com/login', data={'form_email':'abc@example.com','form_password':'123456'})

requests默认使用application/x-www-form-urlencoded对POST数据编码。如果要传递JSON数据,可以直接传入json参数:

params ={'key': 'value'}r =requests.post(url, json=params) # 内部自动序列化为JSON

类似的,上传文件需要更复杂的编码格式,但是requests把它简化成files参数:

>>> upload_files = {'file': open('report.xls','rb')}>>> r = requests.post(url, files=upload_files)

在读取文件时,注意务必使用'rb'即二进制模式读取,这样获取的bytes长度才是文件的长度。

把post()方法替换为put(),delete()等,就可以以PUT或DELETE方式请求资源。

除了能轻松获取响应内容外,requests对获取HTTP响应的其他信息也非常简单。例如,获取响应头:

>>> r.headers{Content-Type': 'text/html; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Content-Encoding': 'gzip', ...}

>>> r.headers['Content-Type']

'text/html;charset=utf-8'

requests对Cookie做了特殊处理,使得我们不必解析Cookie就可以轻松获取指定的Cookie:

>>> r.cookies['ts']'example_cookie_12345'

要在请求中传入Cookie,只需准备一个dict传入cookies参数:

>>> cs = {'token':'12345','status':'working'}>>> r = requests.get(url, cookies=cs)

最后,要指定超时,传入以秒为单位的timeout参数:

>>> r = requests.get(url, timeout=2.5)# 2.5秒后超时

字符串编码一直是令人非常头疼的问题,尤其是我们在处理一些不规范的第三方网页的时候。虽然Python提供了Unicode表示的str和bytes两种数据类型,并且可以通过encode()和decode()方法转换,但是,在不知道编码的情况下,对bytes做decode()不好做。

对于未知编码的bytes,要把它转换成str,需要先“猜测”编码。猜测的方式是先收集各种编码的特征字符,根据特征字符判断,就能有很大概率“猜对”。

当然,我们肯定不能从头自己写这个检测编码的功能,这样做费时费力。chardet这个第三方库正好就派上了用场。用它来检测编码,简单易用。

安装chardet

如果安装了Anaconda,chardet就已经可用了。否则,需要在命令行下通过pip安装:

$ pip install chardet

如果遇到Permission denied安装失败,请加上sudo重试。

使用chardet

当我们拿到一个bytes时,就可以对其检测编码。用chardet检测编码,只需要一行代码:

>>> chardet.detect(b'Hello, world!'){'encoding':'ascii','confidence':1.0,'language':''}

检测出的编码是ascii,注意到还有个confidence字段,表示检测的概率是1.0(即100%)。

我们来试试检测GBK编码的中文:

>>> data ='离离原上草,一岁一枯荣'.encode('gbk')>>> chardet.detect(data){'encoding':'GB2312','confidence':0.7407407407407407,'language':'Chinese'}

检测的编码是GB2312,注意到GBK是GB2312的超集,两者是同一种编码,检测正确的概率是74%,language字段指出的语言是'Chinese'。

对UTF-8编码进行检测:

>>> data ='离离原上草,一岁一枯荣'.encode('utf-8')>>> chardet.detect(data){'encoding':'utf-8','confidence':0.99,'language':''}

我们再试试对日文进行检测:

>>> data ='最新の主要ニュース'.encode('euc-jp')>>> chardet.detect(data){'encoding':'EUC-JP','confidence':0.99,'language':'Japanese'}

可见,用chardet检测编码,使用简单。获取到编码后,再转换为str,就可以方便后续处理。

chardet支持检测的编码列表请参考官方文档Supported encodings

用Python来编写脚本简化日常的运维工作是Python的一个重要用途。在Linux下,有许多系统命令可以让我们时刻监控系统运行的状态,如ps,top,free等等。要获取这些系统信息,Python可以通过subprocess模块调用并获取结果。但这样做显得很麻烦,尤其是要写很多解析代码。

在Python中获取系统信息的另一个好办法是使用psutil这个第三方模块。顾名思义,psutil = process and system utilities,它不仅可以通过一两行代码实现系统监控,还可以跨平台使用,支持Linux/UNIX/OSX/Windows等,是系统管理员和运维小伙伴不可或缺的必备模块。

安装psutil

如果安装了Anaconda,psutil就已经可用了。否则,需要在命令行下通过pip安装:

$ pip install psutil

如果遇到Permission denied安装失败,请加上sudo重试。

获取CPU信息

我们先来获取CPU的信息:

>>> importpsutil>>> psutil.cpu_count()# CPU逻辑数量4>>> psutil.cpu_count(logical=False)# CPU物理核心2# 2说明是双核超线程, 4则是4核非超线程

统计CPU的用户/系统/空闲时间:

>>> psutil.cpu_times()scputimes(user=10963.31, nice=0.0, system=5138.67, idle=356102.45)

再实现类似top命令的CPU使用率,每秒刷新一次,累计10次:

>>> forxinrange(10):... psutil.cpu_percent(interval=1, percpu=True)... [14.0,4.0,4.0,4.0][12.0,3.0,4.0,3.0][8.0,4.0,3.0,4.0][12.0,3.0,3.0,3.0][18.8,5.1,5.9,5.0][10.9,5.0,4.0,3.0][12.0,5.0,4.0,5.0][15.0,5.0,4.0,4.0][19.0,5.0,5.0,4.0][9.0,3.0,2.0,3.0]

获取内存信息

使用psutil获取物理内存和交换内存信息,分别使用:

>>> psutil.virtual_memory()svmem(total=8589934592, available=2866520064, percent=66.6, used=7201386496, free=216178688, active=3342192640, inactive=2650341376, wired=1208852480)>>> psutil.swap_memory()sswap(total=1073741824, used=150732800, free=923009024, percent=14.0, sin=10705981440, sout=40353792)

返回的是字节为单位的整数,可以看到,总内存大小是8589934592 = 8 GB,已用7201386496 = 6.7 GB,使用了66.6%。

而交换区大小是1073741824 = 1 GB。

获取磁盘信息

可以通过psutil获取磁盘分区、磁盘使用率和磁盘IO信息:

>>> psutil.disk_partitions()# 磁盘分区信息[sdiskpart(device='/dev/disk1', mountpoint='/', fstype='hfs', opts='rw,local,rootfs,dovolfs,journaled,multilabel')]>>> psutil.disk_usage('/')# 磁盘使用情况sdiskusage(total=998982549504, used=390880133120, free=607840272384, percent=39.1)>>> psutil.disk_io_counters()# 磁盘IOsdiskio(read_count=988513, write_count=274457, read_bytes=14856830464, write_bytes=17509420032, read_time=2228966, write_time=1618405)

可以看到,磁盘'/'的总容量是998982549504 = 930 GB,使用了39.1%。文件格式是HFS,opts中包含rw表示可读写,journaled表示支持日志。

获取网络信息

psutil可以获取网络接口和网络连接信息:

>>> psutil.net_io_counters() # 获取网络读写字节/包的个数snetio(bytes_sent=3885744870, bytes_recv=10357676702, packets_sent=10613069, packets_recv=10423357, errin=0, errout=0, dropin=0, dropout=0)>>> psutil.net_if_addrs() # 获取网络接口信息{'lo0': [snic(family=, address='127.0.0.1', netmask='255.0.0.0'), ...],  'en1': [snic(family=, address='10.0.1.80', netmask='255.255.255.0'), ...],  'en0': [...],  'en2': [...],  'bridge0': [...]}>>> psutil.net_if_stats() # 获取网络接口状态{  'lo0': snicstats(isup=True, duplex=, speed=0, mtu=16384),  'en0': snicstats(isup=True, duplex=, speed=0, mtu=1500),  'en1': snicstats(...),  'en2': snicstats(...),  'bridge0': snicstats(...)}

要获取当前网络连接信息,使用net_connections():

>>> psutil.net_connections()Traceback (most recentcalllast):  ...PermissionError: [Errno1] OperationnotpermittedDuring handlingofthe aboveexception, anotherexceptionoccurred:Traceback (most recentcalllast):  ...psutil.AccessDenied: psutil.AccessDenied (pid=3847)

你可能会得到一个AccessDenied错误,原因是psutil获取信息也是要走系统接口,而获取网络连接信息需要root权限,这种情况下,可以退出Python交互环境,用sudo重新启动:

$ sudo python3Password: ******Python3.6.3... on darwinType"help", ...formore information.>>> import psutil>>> psutil.net_connections()[    sconn(fd=83, family=, type=1, laddr=addr(ip='::127.0.0.1', port=62911), raddr=addr(ip='::127.0.0.1', port=3306), status='ESTABLISHED', pid=3725),    sconn(fd=84, family=, type=1, laddr=addr(ip='::127.0.0.1', port=62905), raddr=addr(ip='::127.0.0.1', port=3306), status='ESTABLISHED', pid=3725),    sconn(fd=93, family=, type=1, laddr=addr(ip='::', port=8080), raddr=(), status='LISTEN', pid=3725),    sconn(fd=103, family=, type=1, laddr=addr(ip='::127.0.0.1', port=62918), raddr=addr(ip='::127.0.0.1', port=3306), status='ESTABLISHED', pid=3725),    sconn(fd=105, family=, type=1, ..., pid=3725),    sconn(fd=106, family=, type=1, ..., pid=3725),    sconn(fd=107, family=, type=1, ..., pid=3725),    ...    sconn(fd=27, family=, type=2, ..., pid=1)]

获取进程信息

通过psutil可以获取到所有进程的详细信息:

>>> psutil.pids()# 所有进程ID[3865,3864,3863,3856,3855,3853,3776, ...,45,44,1,0]>>> p = psutil.Process(3776)# 获取指定进程ID=3776,其实就是当前Python交互环境>>> p.name()# 进程名称'python3.6'>>> p.exe()# 进程exe路径'/Users/michael/anaconda3/bin/python3.6'>>> p.cwd()# 进程工作目录'/Users/michael'>>> p.cmdline()# 进程启动的命令行['python3']>>> p.ppid()# 父进程ID3765>>> p.parent()# 父进程>>> p.children()# 子进程列表[]>>> p.status()# 进程状态'running'>>> p.username()# 进程用户名'michael'>>> p.create_time()# 进程创建时间1511052731.120333>>> p.terminal()# 进程终端'/dev/ttys002'>>> p.cpu_times()# 进程使用的CPU时间pcputimes(user=0.081150144, system=0.053269812, children_user=0.0, children_system=0.0)>>> p.memory_info()# 进程使用的内存pmem(rss=8310784, vms=2481725440, pfaults=3207, pageins=18)>>> p.open_files()# 进程打开的文件[]>>> p.connections()# 进程相关网络连接[]>>> p.num_threads()# 进程的线程数量1>>> p.threads()# 所有线程信息[pthread(id=1, user_time=0.090318, system_time=0.062736)]>>> p.environ()# 进程环境变量{'SHELL':'/bin/bash','PATH':'/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:...','PWD':'/Users/michael','LANG':'zh_CN.UTF-8', ...}>>> p.terminate()# 结束进程Terminated:15<-- 自己把自己结束了

和获取网络连接类似,获取一个root用户的进程需要root权限,启动Python交互环境或者.py文件时,需要sudo权限。

psutil还提供了一个test()函数,可以模拟出ps命令的效果:

$sudopython3Password: ******Python3.6.3...ondarwinType"help", ...formoreinformation.>>>importpsutil>>>psutil.test()USERPID%MEMVSZRSSTTYSTARTTIMECOMMANDroot0 24.074270628 2016380 ?Nov1840:51kernel_taskroot1  0.12494140    9484 ?Nov1801:39launchdroot44  0.42519872  36404 ?Nov1802:02UserEventAgentroot45    ? 2474032    1516 ?Nov1800:14syslogdroot47  0.12504768    8912 ?Nov1800:03kextdroot48  0.12505544    4720 ?Nov1800:19fseventsd_appleeven52  0.12499748    5024 ?Nov1800:00appleeventsdroot53  0.12500592    6132 ?Nov1800:02configd...

在开发Python应用程序的时候,系统安装的Python3只有一个版本:3.4。所有第三方的包都会被pip安装到Python3的site-packages目录下。

如果我们要同时开发多个应用程序,那这些应用程序都会共用一个Python,就是安装在系统的Python 3。如果应用A需要jinja 2.7,而应用B需要jinja 2.6怎么办?

这种情况下,每个应用可能需要各自拥有一套“独立”的Python运行环境。virtualenv就是用来为一个应用创建一套“隔离”的Python运行环境。

首先,我们用pip安装virtualenv:

$ pip3 install virtualenv

然后,假定我们要开发一个新的项目,需要一套独立的Python运行环境,可以这么做:

第一步,创建目录:

Mac:~michael$ mkdir myprojectMac:~michael$ cd myproject/Mac:myprojectmichael$

第二步,创建一个独立的Python运行环境,命名为venv:

Mac:myprojectmichael$ virtualenv --no-site-packages venvUsingbase prefix'/usr/local/.../Python.framework/Versions/3.4'Newpython executableinvenv/bin/python3.4Alsocreating executableinvenv/bin/pythonInstallingsetuptools, pip, wheel...done.

命令virtualenv就可以创建一个独立的Python运行环境,我们还加上了参数--no-site-packages,这样,已经安装到系统Python环境中的所有第三方包都不会复制过来,这样,我们就得到了一个不带任何第三方包的“干净”的Python运行环境。

新建的Python环境被放到当前目录下的venv目录。有了venv这个Python环境,可以用source进入该环境:

Mac:myprojectmichael$ source venv/bin/activate(venv)Mac:myprojectmichael$

注意到命令提示符变了,有个(venv)前缀,表示当前环境是一个名为venv的Python环境。

下面正常安装各种第三方包,并运行python命令:

(venv)Mac:myprojectmichael$ pip install jinja2...Successfullyinstalled jinja2-2.7.3markupsafe-0.23(venv)Mac:myprojectmichael$ python myapp.py...

在venv环境下,用pip安装的包都被安装到venv这个环境下,系统Python环境不受任何影响。也就是说,venv环境是专门针对myproject这个应用创建的。

退出当前的venv环境,使用deactivate命令:

(venv)Mac:myprojectmichael$ deactivateMac:myprojectmichael$

此时就回到了正常的环境,现在pip或python均是在系统Python环境下执行。

完全可以针对每个应用创建独立的Python运行环境,这样就可以对每个应用的Python环境进行隔离。

virtualenv是如何创建“独立”的Python运行环境的呢?原理很简单,就是把系统Python复制一份到virtualenv的环境,用命令source venv/bin/activate进入一个virtualenv环境时,virtualenv会修改相关环境变量,让命令python和pip均指向当前的virtualenv环境。

Python支持多种图形界面的第三方库,包括:

Tk

wxWidgets

Qt

GTK

等等。

但是Python自带的库是支持Tk的Tkinter,使用Tkinter,无需安装任何包,就可以直接使用。本章简单介绍如何使用Tkinter进行GUI编程。

Tkinter

我们来梳理一下概念:

我们编写的Python代码会调用内置的Tkinter,Tkinter封装了访问Tk的接口;

Tk是一个图形库,支持多个操作系统,使用Tcl语言开发;

Tk会调用操作系统提供的本地GUI接口,完成最终的GUI。

所以,我们的代码只需要调用Tkinter提供的接口就可以了。

第一个GUI程序

使用Tkinter十分简单,我们来编写一个GUI版本的“Hello, world!”。

第一步是导入Tkinter包的所有内容:

fromtkinterimport*

第二步是从Frame派生一个Application类,这是所有Widget的父容器:

classApplication(Frame):def__init__(self, master=None):Frame.__init__(self, master)        self.pack()        self.createWidgets()defcreateWidgets(self):self.helloLabel = Label(self, text='Hello, world!')        self.helloLabel.pack()        self.quitButton = Button(self, text='Quit', command=self.quit)        self.quitButton.pack()

在GUI中,每个Button、Label、输入框等,都是一个Widget。Frame则是可以容纳其他Widget的Widget,所有的Widget组合起来就是一棵树。

pack()方法把Widget加入到父容器中,并实现布局。pack()是最简单的布局,grid()可以实现更复杂的布局。

在createWidgets()方法中,我们创建一个Label和一个Button,当Button被点击时,触发self.quit()使程序退出。

第三步,实例化Application,并启动消息循环:

app =Application()# 设置窗口标题:app.master.title('Hello World')# 主消息循环:app.mainloop()

GUI程序的主线程负责监听来自操作系统的消息,并依次处理每一条消息。因此,如果消息处理非常耗时,就需要在新线程中处理。

运行这个GUI程序,可以看到下面的窗口:

点击“Quit”按钮或者窗口的“x”结束程序。

输入文本

我们再对这个GUI程序改进一下,加入一个文本框,让用户可以输入文本,然后点按钮后,弹出消息对话框。

fromtkinterimport*importtkinter.messageboxasmessageboxclassApplication(Frame):def__init__(self, master=None):Frame.__init__(self, master)        self.pack()        self.createWidgets()defcreateWidgets(self):self.nameInput = Entry(self)        self.nameInput.pack()        self.alertButton = Button(self, text='Hello', command=self.hello)        self.alertButton.pack()defhello(self):name = self.nameInput.get()or'world'messagebox.showinfo('Message','Hello, %s'% name)app = Application()# 设置窗口标题:app.master.title('Hello World')# 主消息循环:app.mainloop()

当用户点击按钮时,触发hello(),通过self.nameInput.get()获得用户输入的文本后,使用tkMessageBox.showinfo()可以弹出消息对话框。

程序运行结果如下:

虽然大家现在对互联网很熟悉,但是计算机网络的出现比互联网要早很多。

计算机为了联网,就必须规定通信协议,早期的计算机网络,都是由各厂商自己规定一套协议,IBM、Apple和Microsoft都有各自的网络协议,互不兼容,这就好比一群人有的说英语,有的说中文,有的说德语,说同一种语言的人可以交流,不同的语言之间就不行了。

为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,互联网协议簇(Internet Protocol Suite)就是通用协议标准。Internet是由inter和net两个单词组合起来的,原意就是连接“网络”的网络,有了Internet,任何私有网络,只要支持这个协议,就可以联入互联网。

因为互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议简称TCP/IP协议。

通信的时候,双方必须知道对方的标识,好比发邮件必须知道对方的邮件地址。互联网上每个计算机的唯一标识就是IP地址,类似123.123.123.123。如果一台计算机同时接入到两个或更多的网络,比如路由器,它就会有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。

IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。

IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。

IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334。

TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。

许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。

一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。

端口有什么作用?在两台计算机通信时,只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个TCP报文来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。每个网络程序都向操作系统申请唯一的端口号,这样,两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。

一个进程也可能同时与多个计算机建立链接,因此它会申请很多端口。

了解了TCP/IP协议的基本概念,IP地址和端口的概念,我们就可以开始进行网络编程了。

Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。

客户端

大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。

举个例子,当我们在浏览器中访问新浪时,我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来的,后面的通信就是发送网页内容了。

所以,我们要创建一个基于TCP连接的Socket,可以这样做:

# 导入socket库:importsocket# 创建一个socket:s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 建立连接:s.connect(('www.sina.com.cn',80))

创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6。SOCK_STREAM指定使用面向流的TCP协议,这样,一个Socket对象就创建成功,但是还没有建立连接。

客户端要主动发起TCP连接,必须知道服务器的IP地址和端口号。新浪网站的IP地址可以用域名www.sina.com.cn自动转换到IP地址,但是怎么知道新浪服务器的端口号呢?

答案是作为服务器,提供什么样的服务,端口号就必须固定下来。由于我们想要访问网页,因此新浪提供网页服务的服务器必须把端口号固定在80端口,因为80端口是Web服务的标准端口。其他服务都有对应的标准端口号,例如SMTP服务是25端口,FTP服务是21端口,等等。端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。

因此,我们连接新浪服务器的代码如下:

s.connect(('www.sina.com.cn', 80))

注意参数是一个tuple,包含地址和端口号。

建立TCP连接后,我们就可以向新浪服务器发送请求,要求返回首页的内容:

# 发送数据:s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')

TCP连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发,怎么协调,要根据具体的协议来决定。例如,HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。

发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收新浪服务器返回的数据了:

# 接收数据:buffer = []whileTrue:# 每次最多接收1k字节:d = s.recv(1024)ifd:        buffer.append(d)else:breakdata =b''.join(buffer)

接收数据时,调用recv(max)方法,一次最多接收指定的字节数,因此,在一个while循环中反复接收,直到recv()返回空数据,表示接收完毕,退出循环。

当我们接收完数据后,调用close()方法关闭Socket,这样,一次完整的网络通信就结束了:

# 关闭连接:s.close()

接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件:

header, html = data.split(b'\r\n\r\n',1)print(header.decode('utf-8'))# 把接收的数据写入文件:withopen('sina.html','wb')asf:    f.write(html)

现在,只需要在浏览器中打开这个sina.html文件,就可以看到新浪的首页了。

服务器

和客户端编程相比,服务器编程就要复杂一些。

服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。

所以,服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。

但是服务器还需要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理,否则,服务器一次就只能服务一个客户端了。

我们来编写一个简单的服务器程序,它接收客户端连接,把客户端发过来的字符串加上Hello再发回去。

首先,创建一个基于IPv4和TCP协议的Socket:

s =socket.socket(socket.AF_INET, socket.SOCK_STREAM)

然后,我们要绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,还可以用127.0.0.1绑定到本机地址。127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,也就是说,外部的计算机无法连接进来。

端口号需要预先指定。因为我们写的这个服务不是标准服务,所以用9999这个端口号。请注意,小于1024的端口号必须要有管理员权限才能绑定:

# 监听端口:s.bind(('127.0.0.1',9999))

紧接着,调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量:

s.listen(5)print('Waiting for connection...')

接下来,服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接:

whileTrue:# 接受一个新连接:sock, addr = s.accept()# 创建新线程来处理TCP连接:t = threading.Thread(target=tcplink, args=(sock, addr))    t.start()

每个连接都必须创建新线程(或进程)来处理,否则,单线程在处理连接的过程中,无法接受其他客户端的连接:

deftcplink(sock, addr):print('Accept new connection from %s:%s...'% addr)    sock.send(b'Welcome!')whileTrue:        data = sock.recv(1024)        time.sleep(1)ifnotdataordata.decode('utf-8') =='exit':breaksock.send(('Hello, %s!'% data.decode('utf-8')).encode('utf-8'))    sock.close()    print('Connection from %s:%s closed.'% addr)

连接建立后,服务器首先发一条欢迎消息,然后等待客户端数据,并加上Hello再发送给客户端。如果客户端发送了exit字符串,就直接关闭连接。

要测试这个服务器程序,我们还需要编写一个客户端程序:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 建立连接:s.connect(('127.0.0.1',9999))# 接收欢迎消息:print(s.recv(1024).decode('utf-8'))fordatain[b'Michael',b'Tracy',b'Sarah']:# 发送数据:s.send(data)    print(s.recv(1024).decode('utf-8'))s.send(b'exit')s.close()

我们需要打开两个命令行窗口,一个运行服务器程序,另一个运行客户端程序,就可以看到效果了:

┌────────────────────────────────────────────────────────┐

│Command Prompt                                    - □ x │

├────────────────────────────────────────────────────────┤

│$ python echo_server.py                                │

│Waiting for connection...                              │

│Accept new connection from 127.0.0.1:64398...          │

│Connection from 127.0.0.1:64398 closed.                │

│                                                        │

│      ┌────────────────────────────────────────────────┴───────┐

│      │Command Prompt                                    - □ x │

│      ├────────────────────────────────────────────────────────┤

│      │$ python echo_client.py                                │

│      │Welcome!                                                │

│      │Hello, Michael!                                        │

└───────┤Hello, Tracy!                                          │

        │Hello, Sarah!                                          │

        │$                                                      │

        │                                                        │

        │                                                        │

        └────────────────────────────────────────────────────────┘

需要注意的是,客户端程序运行完毕就退出了,而服务器程序会永远运行下去,必须按Ctrl+C退出程序。

CP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对TCP,UDP则是面向无连接的协议。

使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。

虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。

我们来看看如何通过UDP协议传输数据。和TCP类似,使用UDP的通信双方也分为客户端和服务器。服务器首先需要绑定端口:

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)# 绑定端口:s.bind(('127.0.0.1',9999))

创建Socket时,SOCK_DGRAM指定了这个Socket的类型是UDP。绑定端口和TCP一样,但是不需要调用listen()方法,而是直接接收来自任何客户端的数据:

print('Bind UDP on 9999...')whileTrue:# 接收数据:data, addr = s.recvfrom(1024)print('Received from %s:%s.'% addr)    s.sendto(b'Hello, %s!'% data, addr)

recvfrom()方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sendto()就可以把数据用UDP发给客户端。

注意这里省掉了多线程,因为这个例子很简单。

客户端使用UDP时,首先仍然创建基于UDP的Socket,然后,不需要调用connect(),直接通过sendto()给服务器发数据:

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)fordatain[b'Michael',b'Tracy',b'Sarah']:# 发送数据:s.sendto(data, ('127.0.0.1',9999))# 接收数据:print(s.recv(1024).decode('utf-8'))s.close()

从服务器接收数据仍然调用recv()方法。

仍然用两个命令行分别启动服务器和客户端测试,结果如下:

┌────────────────────────────────────────────────────────┐

│Command Prompt                                    - □ x │

├────────────────────────────────────────────────────────┤

│$ python udp_server.py                                  │

│Bind UDP on 9999...                                    │

│Received from 127.0.0.1:63823...                        │

│Received from 127.0.0.1:63823...                        │

│Received from 127.0.0.1:63823...                        │

│      ┌────────────────────────────────────────────────┴───────┐

│      │Command Prompt                                    - □ x │

│      ├────────────────────────────────────────────────────────┤

│      │$ python udp_client.py                                  │

│      │Welcome!                                                │

│      │Hello, Michael!                                        │

└───────┤Hello, Tracy!                                          │

        │Hello, Sarah!                                          │

        │$                                                      │

        │                                                        │

        │                                                        │

        └────────────────────────────────────────────────────────┘

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容