6.3 标准库
Python标准库的内容非常多,有人专门为此写过一本书。在本书中,我将根据自己的理解和喜好,选几个呈现出来,一来显示标准库之强大功能,二来演示如何理解和使用标准库。
6.3.1 sys
这是一个跟Python解释器关系密切的标准库,前面已经使用过sys.path.append()。
- >>> import sys
- >>> print sys.__doc__
显示了sys的基本文档,第一句话概括了本模块的基本特点。
- This module provides access to some objects used or maintained by the
- interpreter and to functions that interact strongly with the interpreter.
在诸多sys函数和变量中,选择常用的来说明。
1.sys.argv
sys.argv是变量,专门用来向Python解释器传递参数,所以名曰“命令行参数”。
先解释什么是命令行参数。
- $ python --version
- Python 2.7.6
这里的—version就是命令行参数,如果你使用python—help可以看到更多:
- $ python --help
- usage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...
- Options and arguments (and corresponding environment variables):
- -B : don't write .py[co] files on import; also PYTHONDONTWRITEBYTECODE=x
- -c cmd : program passed in as string (terminates option list)
- -d : debug output from parser; also PYTHONDEBUG=x
- -E : ignore PYTHON* environment variables (such as PYTHONPATH)
- -h : print this help message and exit (also --help)
- -i : inspect interactively after running script; forces a prompt even
- if stdin does not appear to be a terminal; also PYTHONINSPECT=x
- -m mod : run library module as a script (terminates option list)
- -O : optimize generated bytecode slightly; also PYTHONOPTIMIZE=x
- -OO : remove doc-strings in addition to the -O optimizations
- -R : use a pseudo-random salt to make hash() values of various types be
- unpredictable between separate invocations of the interpreter, as
- a defense against denial-of-service attacks
只选择了部分内容摆在这里。所看到的如-B、-h之流,都是参数,比如python-h,其功能同上,那么-h也是命令行参数。
sys.arg在Python中的作用就是这样,通过它可以向解释器传递命令行参数。比如:
- #!/usr/bin/env python
- # coding=utf-8
- import sys
- print "The file name: ", sys.argv[0]
- print "The number of argument", len(sys.argv)
- print "The argument is: ", str(sys.argv)
将上述代码保存,文件名是22101.py。然后如此做:
- $ python 22101.py
- The file name: 22101.py
- The number of argument 1
- The argument is: ['22101.py']
将结果和前面的代码做个对照。
(1)在$python 22101.py中,“22101.py”是要运行的文件名,同时也是命令行参数,是前面的Python这个指令的参数,其地位与python-h中的参数-h是等同的。
(2)sys.argv[0]是第一个参数,就是上面提到的22101.py,即文件名。
如果这样来试试:
- $ python 22101.py beginner master www.itdiffer.com
- The file name: 22101.py
- The number of argument 4
- The argument is: ['22101.py', 'beginner', 'master', 'www.itdiffer.com']
在这里用sys.arg[1]得到的就是beginner,依次类推。
2.sys.exit()
这个方法的意思是退出当前程序。
- Help on built-in function exit in module sys:
- exit(...)
- exit([status])
- Exit the interpreter by raising SystemExit(status).
- If the status is omitted or None, it defaults to zero (i.e., success).
- If the status is an integer, it will be used as the system exit status.
- If it is another kind of object, it will be printed and the system
- exit status will be one (i.e., failure).
从文档信息中可知,如果用sys.exit()退出程序,会返回SystemExit异常。这里先告知读者,还有另外一种退出方式:os._exit(),这两个有所区别。
- #!/usr/bin/env python
- # coding=utf-8
- import sys
- for i in range(10):
- if i == 5:
- sys.exit()
- else:
- print i
这段程序的运行结果就是:
- $ python 22102.py
- 0
- 1
- 2
- 3
- 4
在有的函数中(甚至大多数函数中)会用到return,其含义是终止当前的函数,并返回相应值(如果没有就是None)。但是sys.exit()的含义是退出当前程序,并发起SystemExit异常。这就是两者的区别了。
使用sys.exit(0)表示正常退出,如果退出的时候有一个对人友好的提示信息,可以用sys.exit("I wet out at here."),那么字符串信息就被打印出来。
3.sys.path
sys.path已经不陌生了,它可以查找模块所在的目录,以列表的形式显示出来。如果用append()方法,就能够向这个列表增加新的模块目录。
4.sys.stdin,sys.stdout,sys.stderr
将这三个放到一起,是因为他们的变量都是类文件流对象,分别表示标准UNIX概念中的标准输入、标准输出和标准错误。与Python功能对照,sys.stdin获得输入(用raw_input()输入的通过它获得,Python 3.x中是imput()),sys.stdout负责输出。
流是程序输入或输出的一个连续的字节序列,设备(例如鼠标、键盘、磁盘、屏幕、调制解调器和打印机)的输入和输出都是用流来处理的。程序在任何时候都可以使用它们。一般来讲,stdin(输入)并不一定来自键盘,stdout(输出)也并不一定显示在屏幕上,它们都可以重定向到磁盘文件或其他设备上。
还记得print()吧,它的本质就是sys.stdout.write(object+'\n')。
- sys.stdout.write(object + '\n')。
- >>> for i in range(3):
- ... print i
- ...
- 0
- 1
- 2
- >>> import sys
- >>> for i in range(3):
- ... sys.stdout.write(str(i))
造成上面的输出结果在表象上有如此差异的,原因就是那个'\n'的有无。
- >>> for i in range(3):
- ... sys.stdout.write(str(i) + '\n')
- ...
- 0
- 1
- 2
从这里可以看出,两者是完全等效的。如果仅仅止于此,则意义不大。更强大的在于通过sys.stdout能够做到将输出内容从“控制台”转到“文件”,称之为重定向。这样也许控制台看不到(很多时候这个不重要),但是文件中已经有了要输出的内容。
- >>> f = open("stdout.md", "w")
- >>> sys.stdout = f
- >>> print "Learn Python: From Beginner to Master"
- >>> f.close()
当sys.stdout=f之后,就意味着将输出目的地转到了打开(建立)的文件中,然后用print,将内容“打印”到那个文件中,在控制台就不显现。
打开文件看看便知:
- $ cat stdout.md
- Learn Python: From Beginner to Master
这就是标准输出。
另外两个,输入和错误也类似,读者可以自行测试。
6.3.2 copy
前面对浅拷贝和深拷贝做了研究,这里再次提出,即是复习,也是凑数,以显得我考虑到了这个常用模块。
- >>> import copy
- >>> copy.__all__
- ['Error', 'copy', 'deepcopy']
这个模块中常用的就是copy和deepcopy。
为了具体说明,看这样一个例子:
- #!/usr/bin/env python
- # coding=utf-8
- import copy
- class MyCopy(object):
- def __init__(self, value):
- self.value = value
- def __repr__(self):
- return str(self.value)
- foo = MyCopy(7)
- a = ["foo", foo]
- b = a[:]
- c = list(a)
- d = copy.copy(a)
- e = copy.deepcopy(a)
- a.append("abc")
- foo.value = 17
- print "original: %r\n slice: %r\n list(): %r\n copy(): %r\n deepcopy(): %r\n" % (a,b,c,d,e)
保存并运行:
- $ python 22103.py
- original: ['foo', 17, 'abc']
- slice: ['foo', 17]
- list(): ['foo', 17]
- copy(): ['foo', 17]
- deepcopy(): ['foo', 7]
读者可以对照结果和程序,就能理解各种拷贝的实现方法和含义了——深拷贝和浅拷贝。
6.3.3 os
os模块提供了访问操作系统服务的功能,它所包含的内容比较多,有时候感觉很神秘。
- >>> import os
- >>> dir(os)
- ['EX_CANTCREAT', 'EX_CONFIG', 'EX_DATAERR', 'EX_IOERR', 'EX_NOHOST', 'EX_NOINPUT', 'EX_NOPERM', 'EX_NOUSER','EX_OK', 'EX_OSERR', 'EX_OSFILE', 'EX_PROTOCOL', 'EX_SOFTWARE', 'EX_TEMPFAIL', 'EX_UNAVAILABLE', 'EX_USAGE', 'F_OK', 'NGROUPS_MAX', 'O_APPEND', 'O_ASYNC', 'O_CREAT', 'O_DIRECT', 'O_DIRECTORY', 'O_DSYNC', 'O_EXCL', 'O_LARGEFILE', 'O_NDELAY', 'O_NOATIME', 'O_NOCTTY', 'O_NOFOLLOW', 'O_NONBLOCK', 'O_RDONLY', 'O_RDWR', 'O_RSYNC', 'O_SYNC', 'O_TRUNC', 'O_WRONLY', 'P_NOWAIT', 'P_NOWAITO', 'P_WAIT', 'R_OK', 'SEEK_CUR', 'SEEK_END', 'SEEK_SET', 'ST_APPEND', 'ST_MANDLOCK', 'ST_NOATIME', 'ST_NODEV', 'ST_NODIRATIME', 'ST_NOEXEC', 'ST_NOSUID', 'ST_RDONLY', 'ST_RELATIME', 'ST_SYNCHRONOUS', 'ST_WRITE', 'TMP_MAX', 'UserDict', 'WCONTINUED', 'WCOREDUMP', 'WEXITSTATUS', 'WIFCONTINUED', 'WIFEXITED', 'WIFSIGNALED', 'WIFSTOPPED', 'WNOHANG', 'WSTOPSIG', 'WTERMSIG', 'WUNTRACED', 'W_OK', 'X_OK', '_Environ', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '_copy_reg', '_execvpe', '_exists', '_exit', '_get_exports_list', '_make_stat_result', '_make_statvfs_result', '_pickle_stat_result', '_pickle_statvfs_result', '_spawnvef', 'abort', 'access', 'altsep', 'chdir', 'chmod', 'chown', 'chroot', 'close', 'closerange', 'confstr', 'confstr_names', 'ctermid', 'curdir', 'defpath', 'devnull', 'dup', 'dup2', 'environ', 'errno', 'error', 'execl', 'execle', 'execlp', 'execlpe', 'execv', 'execve', 'execvp', 'execvpe', 'extsep', 'fchdir', 'fchmod', 'fchown', 'fdatasync', 'fdopen', 'fork', 'forkpty', 'fpathconf', 'fstat', 'fstatvfs', 'fsync', 'ftruncate', 'getcwd', 'getcwdu', 'getegid', 'getenv', 'geteuid', 'getgid', 'getgroups', 'getloadavg', 'getlogin', 'getpgid', 'getpgrp', 'getpid', 'getppid', 'getresgid', 'getresuid', 'getsid', 'getuid', 'initgroups', 'isatty', 'kill', 'killpg', 'lchown', 'linesep', 'link', 'listdir', 'lseek', 'lstat', 'major', 'makedev', 'makedirs', 'minor', 'mkdir', 'mkfifo', 'mknod', 'name', 'nice', 'open', 'openpty', 'pardir', 'path', 'pathconf', 'pathconf_names', 'pathsep', 'pipe', 'popen', 'popen2', 'popen3', 'popen4', 'putenv', 'read', 'readlink', 'remove', 'removedirs', 'rename', 'renames', 'rmdir', 'sep', 'setegid', 'seteuid', 'setgid', 'setgroups', 'setpgid', 'setpgrp', 'setregid', 'setresgid', 'setresuid', 'setreuid', 'setsid', 'setuid', 'spawnl', 'spawnle', 'spawnlp', 'spawnlpe', 'spawnv', 'spawnve', 'spawnvp', 'spawnvpe', 'stat', 'stat_float_times', 'stat_result', 'statvfs', 'statvfs_result', 'strerror', 'symlink', 'sys', 'sysconf', 'sysconf_names', 'system', 'tcgetpgrp', 'tcsetpgrp', 'tempnam', 'times', 'tmpfile', 'tmpnam', 'ttyname', 'umask', 'uname', 'unlink', 'unsetenv', 'urandom', 'utime', 'wait', 'wait3', 'wait4', 'waitpid', 'walk', 'write']
这么多内容不能都介绍,列出来纯粹是要吓唬你一下,先混个脸熟,将来用到哪个了,可以到这里来找。
下面介绍的都是我自认为用得比较多的,如果读者要用某个方法或属性,但是这里没有介绍,你完全可以自己用help()来自学,当然,还有另外一个好工具——Google(内事不决问Google,外事不明问谷歌)。
1.操作文件:重命名、删除文件
在对文件进行操作的时候,open()这个内建函数可以建立、打开文件。但是,如果对文件进行改名、删除操作,就要使用os模块的方法了。
首先建立一个文件,文件名为22201.py,文件内容是:
- #!/usr/bin/env python
- # coding=utf-8
- print "This is a tmp file."
然后将这个文件名称修改为别的名称。
- >>> import os
- >>> os.rename("22201.py", "newtemp.py")
注意,我是先进入到了文件22201.py的目录,然后再进入交互模式,所以,可以直接写文件名,如果不是这样,需要将文件的路径写上。
os.rename("22201.py","newtemp.py")中,第一个文件是原文件名称,第二个是打算修改成为的文件名。
然后查看,能够看到这个文件。
- $ ls new*
- newtemp.py
文件内容可以用cat newtemp.py查看(这是在Ubuntu系统,如果是Windows系统,可以用其相应的编辑器打开文件看内容)。
除了修改文件名称,还可以修改目录名称,请注意阅读帮助信息。
- Help on built-in function rename in module posix:
- rename(...)
- rename(old, new)
- Rename a file or directory.
另外一个os.remove(),首先看帮助信息,然后再实验。
- Help on built-in function remove in module posix:
- remove(...)
- remove(path)
- Remove a file (same as unlink(path)).
为了测试,先建立一些文件。
- $ pwd
- /home/qw/Documents/VBS/StarterLearningPython/2code/rd
这是我建立的临时目录,里面有几个文件:
- $ ls
- a.py b.py c.py
下面删除a.py文件。
- >>> import os
- >>> os.remove("/home/qw/Documents/VBS/StarterLearningPython/2code/rd/a.py")
看看删了吗?
- $ ls
- b.py c.py
果然管用,再来一个狠的:
- >>> os.remove("/home/qw/Documents/VBS/StarterLearningPython/2code/rd")
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- OSError: [Errno 21] Is a directory: '/home/qw/Documents/VBS/StarterLearningPython/ 2code/rd'
报错了。我打算将这个目录下的所剩文件删光,但这么做不行。注意帮助中的那句Remove a file,os.remove()就是用来删除文件的。并且从报错中也可以看到,错误的原因在于那个参数是一个目录。
要删除目录,还得继续向下学习。
2.操作目录
(1)os.listdir:显示目录中的文件。
- Help on built-in function listdir in module posix:
- listdir(...)
- listdir(path) -> list_of_strings
- Return a list containing the names of the entries in the directory.
- path: path of directory to list
- The list is in arbitrary order. It does not include the special
- entries '.' and '..' even if they are present in the directory.
看完帮助信息,读者一定觉得这是一个非常简单的方法,不过,要特别注意它返回的值是列表,且不显示文件夹中用特殊格式命名的文件(它们是隐藏文件)。在Linux中,用ls命令也看不到这些隐藏的文件。
- >>> os.listdir("/home/qw/Documents/VBS/StarterLearningPython/2code/rd")
- ['b.py', 'c.py']
- >>> files = os.listdir("/home/qw/Documents/VBS/StarterLearningPython/2code/rd")
- >>> for f in files:
- ... print f
- ...
- b.py
- c.py
(2)os.getcwd,os.chdir:当前工作目录,改变当前工作目录。
这两个函数怎么用?唯有通过help()看文档了,请读者自行看看,就不贴出来了,仅演示一个例子:
- >>> cwd = os.getcwd() #当前目录
- >>> print cwd
- /home/qw/Documents/VBS/StarterLearningPython/2code/rd
- >>> os.chdir(os.pardir) #进入到上一级
- >>> os.getcwd() #当前
- '/home/qw/Documents/VBS/StarterLearningPython/2code'
- >>> os.chdir("rd") #进入下级
- >>> os.getcwd()
- '/home/qw/Documents/VBS/StarterLearningPython/2code/rd'
os.pardir的功能是获得父级目录,相当于“..”。
- >>> os.pardir
- '..'
(3)os.makedirs,os.removedirs:创建和删除目录。
直接上例子:
- >>> dir = os.getcwd()
- >>> dir
- '/home/qw/Documents/VBS/StarterLearningPython/2code/rd'
- >>> os.removedirs(dir)
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "/usr/lib/python2.7/os.py", line 170, in removedirs
- rmdir(name)
- OSError: [Errno 39] Directory not empty:
- '/home/qw/Documents/VBS/StarterLearningPython/2code/rd'
什么时候都不能得意忘形,一定要谦卑,从看文档开始一点一点地理解。看报错信息,要删除某个目录,则那个目录必须是空的。
- >>> os.getcwd()
- '/home/qw/Documents/VBS/StarterLearningPython/2code'
这是当前目录,在这个目录下再建一个新的子目录:
- >>> os.makedirs("newrd")
- >>> os.chdir("newrd")
- >>> os.getcwd()
- '/home/qw/Documents/VBS/StarterLearningPython/2code/newrd'
建立了一个。下面把刚刚建立的这个目录删除,毫无疑问它是空的。
- >>> os.listdir(os.getcwd())
- []
- >>> newdir = os.getcwd()
- >>> os.removedirs(newdir)
按照我的理解,这里应该报错。因为我是在当前工作目录删除当前工作目录,如果这样能够执行,总觉得有点别扭。但事实上行得通,就算是Python的规定吧。不过,若让我来确定这个功能的话,还是习惯不能在本地删除本地。
按照上面的操作,再看当前的工作目录:
- >>> os.getcwd()
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- OSError: [Errno 2] No such file or directory
目录被删了,只能回到父级。
- >>> os.chdir(os.pardir)
- >>> os.getcwd()
- '/home/qw/Documents/VBS/StarterLearningPython/2code'
有点不可思议,本来没有当前工作目录,怎么会有“父级”呢?但现实就是这样。
补充一点,前面说的如果目录不空,就不能用os.removedirs()删除。但是,可以用模块shutil的retree方法。
- >>> os.getcwd()
- '/home/qw/Documents/VBS/StarterLearningPython/2code'
- >>> os.chdir("rd")
- >>> now = os.getcwd()
- >>> now
- '/home/qw/Documents/VBS/StarterLearningPython/2code/rd'
- >>> os.listdir(now)
- ['b.py', 'c.py']
- >>> import shutil
- >>> shutil.rmtree(now)
- >>> os.getcwd()
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- OSError: [Errno 2] No such file or directory
请读者注意,对于os.makedirs()还有这样的特点:
- >>> os.getcwd()
- '/home/qw/Documents/VBS/StarterLearningPython/2code'
- >>> d0 = os.getcwd()
- >>> d1 = d0+"/ndir1/ndir2/ndir3" #这是想建立的目录,但是ndir1,ndir2也都不存在。
- >>> d1
- '/home/qw/Documents/VBS/StarterLearningPython/2code/ndir1/ndir2/ndir3'
- >>> os.makedirs(d1)
- >>> os.chdir(d1)
- >>> os.getcwd()
- '/home/qw/Documents/VBS/StarterLearningPython/2code/ndir1/ndir2/ndir3'
不存在的目录也被建立起来,直到做右边的目录为止。与os.makedirs()类似的还有os.mkdir(),不过,os.mkdir()没有上面这个功能,它只能一层一层地建目录。os.removedirs()和os.rmdir()也类似,区别也类似上面。
3.文件和目录属性
不管是哪种操作系统,都能看到文件或者目录的有关属性,那么,在os模块中,也有这样的一个方法:os.stat()。
- >>> p = os.getcwd() #当前目录
- >>> p
- '/home/qw/Documents/VBS/StarterLearningPython'
显示这个目录的有关信息:
- >>> os.stat(p)
- posix.stat_result(st_mode=16895, st_ino=4L, st_dev=26L, st_nlink=1, st_uid=0, st_gid=0, st_size=12288L, st_atime=1430224935, st_mtime=1430224935, st_ctime=1430224935)
再指定一个文件:
- >>> pf = p + "/README.md"
显示此文件的信息:
- >>> os.stat(pf)
- posix.stat_result(st_mode=33279, st_ino=67L, st_dev=26L, st_nlink=1, st_uid=0, st_gid=0, st_size=50L, st_atime=1429580969, st_mtime=1429580969, st_ctime=1429580969)
从结果中看,可能看不出什么来,先不用着急。这样的结果对computer姑娘是友好的,但可能对读者不友好。如果用下面的方法,就友好多了:
- >>> fi = os.stat(pf)
- >>> mt = fi[8]
fi[8]就是st_mtime的值,它代表最后modified(修改)文件的时间。看结果:
- >>> mt
- 1429580969
还是不友好,下面用time模块来友好一下:
- >>> import time
- >>> time.ctime(mt)
- 'Tue Apr 21 09:49:29 2015'
现在就对读者友好了。
用os.stat()能够查看文件或者目录的属性。如果要修改呢?比如在部署网站的时候,常常要修改目录或者文件的权限等。这种操作在Python的os模块能做到吗?
要求越来越多了。一般情况下,不在Python里做这个,当然,世界是复杂的,肯定有人会用到的,所以os模块提供了os.chmod()
4.操作命令
读者如果使用某种Linux系统,或者曾经用过DOS(恐怕很少),或者在Windows里面用过command,对敲命令都不陌生。通过命令来做事情的确是很酷的,比如,我在Ubuntu中,要查看文件和目录,只需要ls就足够了。我并不是否认图形界面,对于某些人(比如程序员)在某些情况下,命令是不错的选项,甚至是离不开的。
os模块中提供了这样的方法,许可程序员在Python程序中使用操作系统的命令。(以下是在Ubuntu系统,如果读者是Windows系统,可以将命令换成DOS命令。)
- >>> p
- '/home/qw/Documents/VBS/StarterLearningPython'
- >>> command = "ls " + p
- >>> command
- 'ls /home/qw/Documents/VBS/StarterLearningPython'
为了输入方便,采用了前面例子中已经有的那个目录,并且,用拼接字符串的方式,将要输入的命令(查看某文件夹下的内容)组装成一个字符串,赋值给变量command,然后:
- >>> os.system(command)
- 01.md 101.md 105.md 109.md 113.md 117.md 121.md 125.md 129.md 201.md 205.md 209.md 213.md 217.md 221.md index.md
- 02.md 102.md 106.md 110.md 114.md 118.md 122.md 126.md 130.md 202.md 206.md 210.md 214.md 218.md 222.md n001.md
- 03.md 103.md 107.md 111.md 115.md 119.md 123.md 127.md 1code 203.md 207.md 211.md 215.md 219.md 2code README.md
- 0images 104.md 108.md 112.md 116.md 120.md 124.md 128.md 1images 204.md 208.md 212.md 216.md 220.md 2images
- 0
这样就列出来了该目录下的所有内容。
需要注意的是,os.system()是在当前进程中执行命令,直到它执行结束。如果需要一个新的进程,可以使用os.exec或者os.execvp。对此有兴趣详细了解的读者,可以查看帮助文档了解。另外,os.system()通过shell执行命令,执行结束后将控制权返回到原来的进程,但是os.exec()及相关的函数,则在执行后不将控制权返回到原继承,从而使Python失去控制。
关于Python对进程的管理,此处暂不介绍。
os.system()是一个用途很多的函数。曾有一个朋友网上询问,用它来启动浏览器。不过,这个操作的确要非常仔细,为什么呢?演示一下就明白了。
- >>> os.system("/usr/bin/firefox")
- (process:4002): GLib-CRITICAL **: g_slice_set_config: assertion 'sys_page_size == 0' failed
- (firefox:4002): GLib-GObject-WARNING **: Attempt to add property GnomeProgram:: sm-connect after class was initialised
- ......
我是在Ubuntu上操作的,浏览器的地址是/usr/bin/firefox,可是,若朋友是Windows系统,那么就要非常小心了,因为在Windows里面,表示路径的斜杠跟上面显示的是反着的,可是在Python中“\”代表转义。比较简单的一个方法是用r"c:\user\firfox.exe"的样式,因为在r""中的,都被认为是原始字符。而且在Windows系统中,一般情况下那个文件不是安装在我演示的那个简单样式的文件夹中,而是安装在“C:\Program Files”,这中间有空格,所以还要注意空格问题。读者按照这些提示,看看能不能完成用os.system()启动firefox的操作。
凡是感觉麻烦的东西,必然有另外简单的方法来替代。于是又有了一个webbrowser模块,可以专门用来打开指定网页。
- >>> import webbrowser
- >>> webbrowser.open("http://www.itdiffer.com")
- True
不管是什么操作系统,只要如上操作就能打开网页。
真是神奇的标准库,有如此多的工具,能不加速开发进程吗?能不降低开发成本吗?“人生苦短,我用Python”!
6.3.4 heapq
堆(heap),是一种数据结构,引用维基百科中的说明:
堆(英语:heap),是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看作一棵树的数组对象。
对于这个新的概念,读者不要心慌意乱或者恐惧,因为它本质上不是新东西,而是在我们已经熟知的知识基础上扩展出来的内容。
堆的实现是通过构造二叉堆,也就是一种二叉树。
1.基本知识
这是一颗在苏州很常见的香樟树,马路两边、公园里随处可见,特别是在艳阳高照的时候,它的树荫能把路面遮盖。
但是,在编程中,我们常说的树是这样的:
这是一棵“根”在上面的树,也是编程中常说的树。为什么会这样呢?我想主要是画着更方便吧。上面那棵树虽然根在上面了,还完全是写实的作品,但本人作为一名隐姓埋名多年的抽象派画家,不喜欢这样的树,我画出来是这样的:
这棵树有两根枝杈,可不要小看这两根枝杈,《道德经》上说“一生二,二生三,三生万物”。一就是下面那个树干,二就是两个枝杈,每个枝杈还可以看作下一个一,然后再有两个枝杈,如此不断重复(这简直就是递归呀),就成为了一棵大树。
这棵树画成这样就更符合编程的习惯了,可以向下不断延伸。
并且给它一个正规的名字:二叉树。
这个也是二叉树,完全脱胎于我所画的后现代抽象主义作品,但也略有不同,这幅图在各个枝杈上显示的是数字。这种类型的“树”就是编程语言中所说的二叉树,维基百科曰:
在计算机科学中,二叉树(英语:Binary tree)是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。
在上图的二叉树中,最顶端的那个数字相当于树根,也称作“根”。每个数字所在位置成为一个节点,每个节点向下分散出两个“子节点”。并不是所有节点都有两个子节点,比如上图的最后一层,这类二叉树又称为完全二叉树(Complete Binary Tree),有的二叉树,所有的节点都有两个子节点,这类二叉树称作满二叉树(Full Binarry Tree),如下图。
下面讨论的对象是通过二叉树实现的,其具有如下特点:
- 节点的值大于等于(或者小于等于)任何子节点的值。
- 节点左子树和右子树是一个二叉堆。如果父节点的值总大于等于任何一个子节点的值,其为最大堆;若父节点的值总小于等于子节点值,则为最小堆。上面图示中的完全二叉树,就表示一个最小堆。堆的类型还有别的,如斐波那契堆等,但很少用。所以,通常将二叉堆也说成堆。下面所说的堆就是二叉堆,而二叉堆又是用二叉树实现的。
2.堆的存储
堆用列表来表示,如图6-1所示。
图6-1 用列表来表示堆
从图示中可以看出,将逻辑结构中的树的节点数字依次填入到存储结构中。看这个图,似乎是列表中按照顺序进行排列似的。但是,这仅仅是由于那个树的特点造成的,如果是下面的树:
将上面的逻辑结构转换为存储结构,读者就能看出来了,不再是按照顺序排列的了。
关于堆的各种操作,如插入、删除、排序等,本节不会专门叙述,读者可以参阅有关资料。下面要介绍如何用Python中的模块heapq来实现这些操作。
3.heapq模块
heapq中的heap是堆,q就是queue(队列)的缩写。此模块包括:
- >>> import heapq
- >>> heapq.__all__
- ['heappush', 'heappop', 'heapify', 'heapreplace', 'merge', 'nlargest', 'nsmallest', 'heappushpop']
依次查看这些函数的使用方法。
- heappush(heap,x):将x压入堆heap(这是一个列表)。
- Help on built-in function heappush in module _heapq:
- heappush(...)
- heappush(heap, item) -> None. Push item onto heap, maintaining the heap invariant.
- >>> import heapq
- >>> heap = []
- >>> heapq.heappush(heap, 3)
- >>> heapq.heappush(heap, 9)
- >>> heapq.heappush(heap, 2)
- >>> heapq.heappush(heap, 4)
- >>> heapq.heappush(heap, 0)
- >>> heapq.heappush(heap, 8)
- >>> heap
- [0, 2, 3, 9, 4, 8]
请读者注意上面的操作,在向堆增加数值的时候并没有严格按照什么顺序,是随意的。但是,当查看堆的数据时,显示的是一个有一定顺序的数据结构。这种顺序不是按照从小到大,而是按照前面所说的完全二叉树的方式排列,显示的是存储结构,可以把它还原为逻辑结构,看看是不是一棵二叉树。
由此可知,利用heappush()函数将数据放到堆里面之后,会自动按照二叉树的结构进行存储。
- heappop(heap):删除最小元素。承接上面的操作:
- >>> heapq.heappop(heap)
- 0
- >>> heap
- [2, 4, 3, 9, 8]
用heappop()函数,从heap堆中删除了一个最小元素,并且返回该值。但是,这时候的heap显示顺序,并非简单地将0去除,而是按照完全二叉树的规范重新进行排列。
- heapify():将列表转换为堆。如果已经建立了一个列表,利用heapify()可以将列表直接转化为堆。
- >>> hl = [2, 4, 6, 8, 9, 0, 1, 5, 3]
- >>> heapq.heapify(hl)
- >>> hl
- [0, 3, 1, 4, 9, 6, 2, 5, 8]
经过这样的操作,列表hl就变成了堆(堆的顺序和列表不同),可以对hl(堆)使用heappop()或者heappush()等函数了。否则,不可。
- >>> heapq.heappop(hl)
- 0
- >>> heapq.heappop(hl)
- 1
- >>> hl
- [2, 3, 5, 4, 9, 6, 8]
- >>> heapq.heappush(hl, 9)
- >>> hl
- [2, 3, 5, 4, 9, 6, 8, 9]
不要认为堆里面只能放数字,举例中之所以用数字,是因为对它的逻辑结构比较好理解。
- >>> heapq.heappush(hl, "q")
- >>> hl
- [2, 3, 5, 4, 9, 6, 8, 9, 'q']
- >>> heapq.heappush(hl, "w")
- >>> hl
- [2, 3, 5, 4, 9, 6, 8, 9, 'q', 'w']
- heapreplace()。是heappop()和heappush()的联合,也就是删除一个的同时再加入一个。例如:
- >>> heap
- [2, 4, 3, 9, 8]
- >>> heapq.heapreplace(heap, 3.14)
- 2
- >>> heap
- [3, 4, 3.14, 9, 8]
先简单罗列关于堆的几个常用函数。那么堆在编程实践中的用途有哪些呢?排序是一个应用方面。一提到排序,读者肯定想到的是sorted()或者列表中的sort(),这两个都是常用的函数,而且在一般情况下已经足够使用了。但如果使用堆排序,相对于其他排序,也有自己的优势。不同的排序方法有不同的特点,读者可以自行深入研究不同排序的优劣。
6.3.5 deque
有这样一个问题:一个列表,比如是[1,2,3],在最右边增加一个数字。
这也太简单了,不就是用append()这个内建函数追加一个吗?
这是简单,但能不能在最左边增加一个数字呢?
这个应该有办法,不过得想想。读者在向下阅读之前,能不能想出一个方法来?
- >>> lst = [1, 2, 3]
- >>> lst.append(4)
- >>> lst
- [1, 2, 3, 4]
- >>> nl = [7]
- >>> nl.extend(lst)
- >>> nl
- [7, 1, 2, 3, 4]
你或许还有别的方法。但是,Python为我们提供了一个更简单的模块来解决这个问题。
- >>> from collections import deque
这里用这种引用方法是因为collections模块中东西很多,我们只用到deque。
- >>> lst
- [1, 2, 3, 4]
还是这个列表,试试分别从右边和左边增加数字。
- >>> qlst = deque(lst)
这是必需的,将列表转化为deque。deque在汉语中有一个名字,叫作“双端队列”。(double-ended queue)。
- >>> qlst.append(5) #从右边增加
- >>> qlst
- deque([1, 2, 3, 4, 5])
- >>> qlst.appendleft(7) #从左边增加
- >>> qlst
- deque([7, 1, 2, 3, 4, 5])
这样操作多么容易呀,继续看删除:
- >>> qlst.pop()
- 5
- >>> qlst
- deque([7, 1, 2, 3, 4])
- >>> qlst.popleft()
- 7
- >>> qlst
- deque([1, 2, 3, 4])
删除也分左右。下面这个,请读者仔细观察。
- >>> qlst.rotate(3)
- >>> qlst
- deque([2, 3, 4, 1])
rotate()的功能是将[1,2,3,4]的首位连起来,你就想象一个圆环,在上面有1、2、3、4几个数字。如果一开始正对着你的是1,依顺时针方向排列,就是从1开始的数列,如图6-2所示。
经过rotate(),这个环就发生旋转了,如果是rotate(3),表示每个数字按照顺时针方向前进三个位置,于是变成了如图6-3所示的样子。
图6-2 数字排列
图6-3 数字按顺时针方向前进
请原谅我的后现代主义超级抽象派作图方式。从图中可以看出,数列变成了[2,3,4,1]。rotate()就好像在拨转这个圆环。
- >>> qlst
- deque([3, 4, 1, 2])
- >>> qlst.rotate(-1)
- >>> qlst
- deque([4, 1, 2, 3])
如果参数是负数,那么就逆时针转。
在deque中,还有extend和extendleft方法,读者可自己调试。
6.3.6 calendar
- >>> import calendar
- >>> cal = calendar.month(2015, 1)
- >>> print cal
- January 2015
- Mo Tu We Th Fr Sa Su
- 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
轻而易举得到了2015年1月的日历,并且排列还那么整齐,这就是calendar模块。读者可以用dir()去查看这个模块下的所有内容。为了让读者阅读方便,将常用的整理如下:
- calendar(year,w=2,l=1,c=6)返回year年年历,3个月一行,间隔距离为c,每日宽度间隔为w字符,每行长度为21W+18+2C,l是每星期行数。
- >>> year = calendar.calendar(2015)
- >>> print year
因为显示的内容太多,所以只将部分内容截取下来,如图6-4所示。
图6-4 2015年日历
其他部分就是按照上面的样式,将2015年度的各个月份日历完全显示出来。
- isleap(year)判断是否为闰年,是则返回True,否则False。
- >>> calendar.isleap(2000)
- True
- >>> calendar.isleap(2015)
- False
怎么判断一年是闰年的问题,常常见诸一些编程语言的练习题,现在用一个方法搞定。
- leapdays(y1,y2)返回在y1、y2两年之间的闰年总数,包括y1,但不包括y2,这有点如同序列的切片。
- >>> calendar.leapdays(2000,2004)
- 1
- >>> calendar.leapdays(2000,2003)
- 1
- month(year, month, w=2, l=1)返回year年month月日历,两行标题,一周一行。每日宽度间隔为w字符,每行的长度为7*w+6,l是每星期的行数。
- >>> print calendar.month(2015, 5)
- May 2015
- Mo Tu We Th Fr Sa Su
- 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
- monthcalendar(year, month)返回一个列表,列表内的元素还是列表,这叫作嵌套列表。每个子列表代表一个星期,都是从星期一到星期日,如果没有本月的日期,则为0。
- >>> calendar.monthcalendar(2015, 5)
- [[0, 0, 0, 0, 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]]
读者可以将这个结果和calendar.month(2015,5)去对照理解。
- monthrange(year,month)返回一个元组,里面有两个整数。第一个整数代表着该月的第一天从星期几开始(从0开始,依次为星期一、星期二……6代表星期日)。第二个整数代表该月一共多少天。
- >>> calendar.monthrange(2015, 5)
- (4, 31)
从返回值可知,2015年5月1日是星期五,这个月一共31天。这个结果,也可以从日历中看到。
- weekday(year,month,day)输入年月日,知道该日是星期几(注意,返回值依然按照从0到6依次对应星期一到星期六)。
- >>> calendar.weekday(2015, 5, 4) #星期一
- 0
- >>> calendar.weekday(2015, 6, 4) #星期四
- 3
6.3.7 time
time模块很常用,比如记录某个程序运行时间长短等,下面一一道来其中的方法。
- time()
- >>> import time
- >>> time.time()
- 1430745298.391026
time.time()获得的是当前时间(严格说是时间戳),只不过这个时间对人不友好,它是以1970年1月1日0时0分0秒为计时起点,到当前的时间长度(不考虑闰秒)。
UNIX时间,或称POSIX时间是UNIX或类UNIX系统使用的时间表示方式:从格林威治时间1970年1月1日0时0分0秒起至现在的总秒数,不考虑闰秒。
现时大部分使用的UNIX的系统都是32位的,即它们会以32位二进制数字表示时间。但是它们最多只能表示至协调世界时间2038年1月19日3时14分07秒(二进制:01111111111111111111111111111111,0x7FFF:FFFF),在下一秒二进制数字会是10000000000000000000000000000000,(0x8000:0000),这是负数,因此各系统会把时间误解作1901年12月13日20时45分52秒(亦有说回归到1970年)。这时可能会令软件发生问题,导致系统瘫痪。
目前的解决方案是把系统由32位转为64位。在64位系统下,此时间最多可以表示到292,277,026,596年12月4日15时30分08秒。
有没有对人友好一点的时间显示呢?
- localtime()
- >>> time.localtime()
- time.struct_time(tm_year=2015, tm_mon=5, tm_mday=4, tm_hour=21, tm_min=33, tm_sec=39, tm_wday=0, tm_yday=124, tm_isdst=0)
这个就友好多了。得到的结果可以称之为时间元组(也有括号),其各项的含义如表6-1所示。
表6-1 时间元组各项含义
- >>> t = time.localtime()
- >>> t[1]
- 5
通过索引能够得到相应的属性,上面的例子中就得到了当前时间的月份。
其实,time.localtime()不是没有参数,它在默认情况下,以time.time()的时间戳为参数。言外之意就是说可以自己输入一个时间戳,返回那个时间戳所对应的时间(按照公元和时分秒计时)。例如:
- >>> time.localtime(100000)
- time.struct_time(tm_year=1970, tm_mon=1, tm_mday=2, tm_hour=11, tm_min=46, tm_sec=40, tm_wday=4, tm_yday=2, tm_isdst=0)
- gmtime()localtime()得到的是本地时间,如果要国际化,就最好使用格林威治时间。可以这样:
- >>> import time
- >>> time.gmtime()
- time.struct_time(tm_year=2015, tm_mon=5, tm_mday=4, tm_hour=23, tm_min=46, tm_sec=34, tm_wday=0, tm_yday=124, tm_isdst=0)
格林威治标准时间是指位于英国伦敦郊区的皇家格林威治天文台的标准时间,因为本初子午线被定义在通过那里的经线。
还有更友好的,请继续阅读。
- asctime()
- >>> time.asctime()
- 'Mon May 4 21:46:13 2015'
time.asctime()的参数为空时,默认是以time.localtime()的值为参数,所以得到的是当前日期时间和星期。当然,也可以自己设置参数:
- >>> h = time.localtime(1000000)
- >>> h
- time.struct_time(tm_year=1970, tm_mon=1, tm_mday=12, tm_hour=21, tm_min=46, tm_sec=40, tm_wday=0, tm_yday=12, tm_isdst=0)
- >>> time.asctime(h)
- 'Mon Jan 12 21:46:40 1970'
注意,time.asctime()的参数必须是时间元组,类似上面那种。若不是时间戳,通过time.time()得到的时间戳也可以转化为上面的形式。
- ctime()
- >>> time.ctime()
- 'Mon May 4 21:52:22 2015'
在没有参数的时候,事实上是以time.time()的时间戳为参数,也可以自定义一个时间戳。
- >>> time.ctime(1000000)
- 'Mon Jan 12 21:46:40 1970'
跟前面得到的结果是一样的,只不过用了时间戳作为参数。
在前述函数中,通过localtime()、gmtime()得到的是时间元组,通过time()得到的是时间戳。有的函数如asctime()是以时间元组为参数,有的如ctime()是以时间戳为函数,这样做的目的是为了满足编程中多样化的需要。
- mktime()mktime()也是以时间元组为参数,但是它返回的不是可读性更好的那种样式,而是:
- >>> lt = time.localtime()
- >>> lt
- time.struct_time(tm_year=2015, tm_mon=5, tm_mday=5, tm_hour=7, tm_min=55, tm_sec=29, tm_wday=1, tm_yday=125, tm_isdst=0)
- >>> time.mktime(lt)
- 1430783729.0
返回了时间戳,类似于localtime()的逆过程(localtime()是以时间戳为参数)。
好像还缺点什么,因为在编程中,使用比较多的是“字符串”,似乎还没有将时间转化为字符串的函数,这个应该有。
- strftime()函数格式稍微复杂一些。
- Help on built-in function strftime in module time:
- strftime(...) strftime(format[, tuple]) -> string
- Convert a time tuple to a string according to a format specification. See the library reference manual for formatting codes. When the time tuple is not present, current time as returned by localtime() is used.
将时间元组按照指定格式要求转化为字符串。如果不指定时间元组,就默认为localtime()值。说其复杂是在于其format,需要用到下面的东西,如表6-2所示。
表6-2 format格式定义
简要列举如下:
- >>> time.strftime("%y,%m,%d")
- '15,05,05'
- >>> time.strftime("%y/%m/%d")
- '15/05/05'
分隔符可以自由指定,既然已经变成字符串了,就可以“随心所欲不逾矩”了。
- strptime()
- Help on built-in function strptime in module time:
- strptime(...) strptime(string, format) -> struct_time
- Parse a string to a time tuple according to a format specification. See the library reference manual for formatting codes (same as strftime()).
strptime()的作用是将字符串转化为时间元组。请注意,其参数要指定两个,一个是时间字符串,另外一个是时间字符串所对应的格式,格式符号用表6-2中的。例如:
- >>> today = time.strftime("%y/%m/%d")
- >>> today
- '15/05/05'
- >>> time.strptime(today, "%y/%m/%d")
- time.struct_time(tm_year=2015, tm_mon=5, tm_mday=5, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=1, tm_yday=125, tm_isdst=-1)
6.3.8 datetime
虽然time模块已经能够把有关时间方面的东西搞定了,但是,有时调用起来感觉不是很直接,于是又出来了一个datetime模块,供程序员们选择使用。
datetime模块中有以下几个类。
- datetime.date:日期类,常用的属性有year/month/day。
- datetime.time:时间类,常用的有hour/minute/second/microsecond。
- datetime.datetime:日期时间类。
- datetime.timedelta:时间间隔,即两个时间点之间的时间长度。
- datetime.tzinfo:时区类。1.date类
通过实例了解常用的属性:
- >>> import datetime
- >>> today = datetime.date.today()
- >>> today
- datetime.date(2015, 5, 5)
其实这里生成了一个日期对象,然后操作这个对象的各种属性。用print语句,可以使视觉更佳:
- >>> print today
- 2015-05-05
- >>> print today.ctime()
- Tue May 5 00:00:00 2015
- >>> print today.timetuple()
- time.struct_time(tm_year=2015, tm_mon=5, tm_mday=5, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=1, tm_yday=125, tm_isdst=-1)
- >>> print today.toordinal()
- 735723
特别注意,如果你妄图用datetime.date.year()是会报错的,因为year不是一个方法,必须这样:
- >>> print today.year
- 2015
- >>> print today.month
- 5
- >>> print today.day
- 5
进一步看看时间戳与格式化时间格式的转换:
- >>> to = today.toordinal()
- >>> to
- 735723
- >>> print datetime.date.fromordinal(to)
- 2015-05-05
- >>> import time
- >>> t = time.time()
- >>> t
- 1430787994.80093
- >>> print datetime.date.fromtimestamp(t)
- 2015-05-05
还可以更灵活一些,修改日期。
- >>> d1 = datetime.date(2015,5,1)
- >>> print d1
- 2015-05-01
- >>> d2 = d1.replace(year=2005, day=5)
- >>> print d2
- 2005-05-05
2.time类
也要生成time对象。
- >>> t = datetime.time(1,2,3)
- >>> print t
- 01:02:03
它的常用属性:
- >>> print t.hour
- 1
- >>> print t.minute
- 2
- >>> print t.second
- 3
- >>> t.microsecond
- 0
- >>> print t.tzinfo
- None
3.timedelta类
主要用来做时间的运算。比如:
- >>> now = datetime.datetime.now()
- >>> print now
- 2015-05-05 09:22:43.142520
没有讲述datetime类,因为在有了date和time类知识之后,这个类比较简单。我最喜欢now方法,对now增加5个小时:
- >>> b = now + datetime.timedelta(hours=5)
- >>> print b
- 2015-05-05 14:22:43.142520
增加两周:
- >>> c = now + datetime.timedelta(weeks=2)
- >>> print c
- 2015-05-19 09:22:43.142520
计算时间差:
- >>> d = c - b
- >>> print d
- 13 days, 19:00:00
6.3.9 urllib
urllib模块用于读取来自网上(服务器上)的数据,比如很多人用Python做爬虫程序,就可以使用这个模块。先看一个简单例子:
- >>> import urllib
- >>> itdiffer = urllib.urlopen("http://www.itdiffer.com")
这样就已经把我的网站www.itdiffer.com首页的内容拿过来了,得到了一个类似文件的对象。接下来的操作跟操作一个文件一样。
- >>> print itdiffer.read()
- <!DOCTYPE HTML>
- <html>
- <head>
- <title>I am Qiwsir</title>
- ....//因为内容太多,下面就省略了
这样就完成了对网页的一个抓取。当然,如果你真的要做爬虫程序,还不是仅仅如此。这里不介绍爬虫程序如何编写(关于爬虫的资料,网上已经有很多了,读者可以搜索并学习,如果实在理解有困难,还可以跟我联系,我会协助你的),仅说明urllib模块的常用属性和方法。
- >>> dir(urllib)
- ['ContentTooShortError', 'FancyURLopener', 'MAXFTPCACHE', 'URLopener', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '__version__', '_asciire', '_ftperrors', '_have_ssl', '_hexdig', '_hextochr', '_hostprog', '_is_unicode', '_localhost', '_noheaders', '_nportprog', '_passwdprog', '_portprog', '_queryprog', '_safe_map', '_safe_quoters', '_tagprog', '_thishost', '_typeprog', '_urlopener', '_userprog', '_valueprog', 'addbase', 'addclosehook', 'addinfo', 'addinfourl', 'always_safe', 'base64', 'basejoin', 'c', 'ftpcache', 'ftperrors', 'ftpwrapper', 'getproxies', 'getproxies_environment', 'i', 'localhost', 'noheaders', 'os', 'pathname2url', 'proxy_bypass', 'proxy_bypass_environment', 'quote', 'quote_plus', 're', 'reporthook', 'socket', 'splitattr', 'splithost', 'splitnport', 'splitpasswd', 'splitport', 'splitquery', 'splittag', 'splittype', 'splituser', 'splitvalue', 'ssl', 'string', 'sys', 'test1', 'thishost', 'time', 'toBytes', 'unquote', 'unquote_plus', 'unwrap', 'url2pathname', 'urlcleanup', 'urlencode', 'urlopen', 'urlretrieve']
选几个常用的介绍,如果读者用到其他的,可以通过查看文档了解。
1.urlopen(url,data=None,proxies=None)
urlopen()主要用于打开url文件,从而获得指定url网页内容,然后就如同操作文件那样来操作。
- Help on function urlopen in module urllib:
- urlopen(url, data=None, proxies=None) Create a file-like object for the specified URL to read from.
得到的对象被叫作“类文件”,从名字中也可以理解后面的操作了。先对参数说明一下。
- url:远程数据的路径,常常是网址。
- data:如果使用post方式,这里就是所提交的数据。
- proxies:设置代理。关于参数的详细说明,还可以参考官方文档,这里仅演示最常用的,如前面的例子那样。
当得到了类文件对象之后,即变量itdiffer引用了得到的类文件对象,这个对象依然可以用老办法来查看它的属性和方法。
- >>> dir(itdiffer)
- ['__doc__', '__init__', '__iter__', '__module__', '__repr__', 'close', 'code', 'fileno', 'fp', 'getcode', 'geturl', 'headers', 'info', 'next', 'read', 'readline', 'readlines', 'url']
从结果中也可以看出,这个类文件对象也是可迭代的,下面是比较常用的方法。
- read()、readline()、readlines()、fileno()、close():都与文件操作一样,这里不再赘述。
- info():返回头信息。
- getcode():返回http状态码。
- geturl():返回url。简单举例:
- >>> itdiffer.info()
- <httplib.HTTPMessage instance at 0xb6eb3f6c>
- >>> itdiffer.getcode()
- 200
- >>> itdiffer.geturl()
- 'http://www.itdiffer.com'
许多时候,在建立了类文件对象后,要通过其方法得到某些数据。
2.对url编码、解码
url对其中的字符有严格要求,不许可某些特殊字符直接使用某些字符,比如url中有空格,会自动将空格进行处理,这个过程需要对url进行编码和解码。在进行web开发的时候要特别注意这里。
urllib模块提供了url编码和解码功能。
- quote(string[,safe]):对字符串进行编码。参数safe指定了不需要编码的字符。
- urllib.unquote(string):对字符串进行解码。
- quote_plus(string[,safe]):与urllib.quote类似,但这个方法用“+”来替换空格,而quote用“%20”来代替空格。
- unquote_plus(string):对字符串进行解码。
- urllib.urlencode(query[,doseq]):将dict或者包含两个元素的元组列表转换成url参数。例如{'name':'laoqi','age':40}将被转换为“name=laoqi&age=40”。
- pathname2url(path):将本地路径转换成url路径。
- url2pathname(path):将url路径转换成本地路径。看例子就更明白了:
- >>> du = "http://www.itdiffer.com/name=python book"
- >>> urllib.quote(du)
- 'http%3A//www.itdiffer.com/name%3Dpython%20book'
- >>> urllib.quote_plus(du)
- 'http%3A%2F%2Fwww.itdiffer.com%2Fname%3Dpython+book'
注意看空格的变化,一个被编码成“%20”,另外一个是“+”。
再看解码的,假如在Google中搜索《零基础学Python》,结果如图6-5所示。
图6-5 在Google中搜索《零基础学Python》
与本书同步的网络教程在这次搜索中排列第一个哦。
这不是重点,重点是看url,它就是用“+”替代空格。
- >>> dup = urllib.quote_plus(du)
- >>> urllib.unquote_plus(dup)
- 'http://www.itdiffer.com/name=python book'
从解码效果来看,是比较完美的逆过程。
- >>> urllib.urlencode({"name":"qiwsir","web":"itdiffer.com"})
- 'web=itdiffer.com&name=qiwsir'
如果将来你要做一个网站,上面的这个方法或许会用到的。
- urlretrieve()虽然urlopen()能够建立类文件对象,但是,不等于将远程文件保存在本地存储器中,urlretrieve()就是满足这个需要的。先看实例:
- >>> import urllib
- >>> urllib.urlretrieve("http://www.itdiffer.com/images/me.jpg","me.jpg")
- ('me.jpg', <httplib.HTTPMessage instance at 0xb6ecb6cc>)
- >>>
me.jpg是一张存在于服务器上的图片,地址是:http://www.itdiffer.com/images/me.jpg,把它保存到本地存储器中,并且仍命名为me.jpg。注意,如果只写这个名字,表示存在启动Python交互模式的那个目录中,否则,可以指定存储具体目录和文件名。
在urllib官方文档中有一大段相关说明,读者可以去认真阅读。这里仅简要介绍一下相关参数。
- urllib.urlretrieve(url[, filename[, reporthook[, data]]])
- url:文件所在的网址。
- filename:可选。将文件保存到本地的文件名,如果不指定,urllib会生成一个临时文件来保存。
- reporthook:可选。是回调函数,当链接服务器和相应数据传输完毕时触发本函数。
- data:可选。用post方式所发出的数据。函数执行完毕,返回的结果是一个元组(filename,headers),filename是保存到本地的文件名,headers是服务器响应头信息。
- #!/usr/bin/env python
- # coding=utf-8
- import urllib
- def go(a, b, c):
- per = 100.0 * a * b / c
- if per > 100:
- per = 100
- print "%.2f%%" % per
- url = "http://youxi.66wz.com/uploads/1046/1321/11410192.90d133701b06f0cc2826c3e5ac34c 620.jpg"
- local = "/home/qw/Pictures/g.jpg"
- urllib.urlretrieve(url, local, go)
这段程序就是要下载指定的图片,并且保存为本地指定位置的文件,同时要显示下载的进度。上述文件保存之后执行,显示如下效果:
- $ python 22501.py
- 0.00%
- 8.13%
- 16.26%
- 24.40%
- 32.53%
- 40.66%
- 48.79%
- 56.93%
- 65.06%
- 73.19%
- 81.32%
- 89.46%
- 97.59%
- 100.00%
到相应目录中查看,能看到与网上地址一样的文件。这里就不对结果截图了,读者自行查看(或许在本书出版的时候,这张神秘的图片你已经看不到了,你应该把这视为正常的事情,那么你就换一张图片地址吧)。
6.3.10 urllib2
urllib2是另外一个模块,它跟urllib有相似的地方——都是对url相关的操作,也有不同的地方。
有时候两个要同时使用,urllib模块和urllib2模块有的方法可以相互替代,有的不能。看下面的属性方法列表就知道了。
- >>> dir(urllib2)
- ['AbstractBasicAuthHandler', 'AbstractDigestAuthHandler', 'AbstractHTTPHandler', 'BaseHandler', 'CacheFTPHandler', 'FTPHandler', 'FileHandler', 'HTTPBasicAuthHandler', 'HTTPCookieProcessor', 'HTTPDefaultErrorHandler', 'HTTPDigestAuthHandler', 'HTTPError', 'HTTPErrorProcessor', 'HTTPHandler', 'HTTPPasswordMgr', 'HTTPPasswordMgrWithDefaultRealm', 'HTTPRedirectHandler', 'HTTPSHandler', 'OpenerDirector', 'ProxyBasicAuthHandler', 'ProxyDigestAuthHandler', 'ProxyHandler', 'Request', 'StringIO', 'URLError', 'UnknownHandler', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '__version__', '_cut_port_re', '_opener', '_parse_proxy', '_safe_gethostbyname', 'addinfourl', 'base64', 'bisect', 'build_opener', 'ftpwrapper', 'getproxies', 'hashlib', 'httplib', 'install_opener', 'localhost', 'mimetools', 'os', 'parse_http_list', 'parse_keqv_list', 'posixpath', 'proxy_bypass', 'quote', 'random', 'randombytes', 're', 'request_host', 'socket', 'splitattr', 'splithost', 'splitpasswd', 'splitport', 'splittag', 'splittype', 'splituser', 'splitvalue', 'sys', 'time', 'toBytes', 'unquote', 'unwrap', 'url2pathname', 'urlopen', 'urlparse', 'warnings']
读者不妨将urllib和urllib2的方法属性名称进行比较,会发现它们之间有一部分是相同的,比如urlopen()跟urllib.open()非常类似,除了相同的就是不同的了。
Request类
在前面引用的内容中就明确指出,利用urllib2模块可以建立一个Request对象,建立Request对象的方法就是使用Request类。
- >>> req = urllib2.Request("http://www.itdiffer.com")
建立了Request对象之后,它的最直接应用可以作为urlopen()方法的参数。
- >>> response = urllib2.urlopen(req)
- >>> page = response.read()
- >>> print page
因为与前面的urllib.open("http://www.itdiffer.com")结果一样,就不再赘述。
但是,如果Request对象仅仅局限于此,似乎还没有什么太大的优势。因为刚才的访问仅仅满足以get方式请求页面,并建立类文件对象。如果是通过post向某地址提交数据,也可以建立Request对象。
- import urllib
- import urllib2
- url = 'http://www.itdiffer.com/register.py'
- values = {'name' : 'qiwsir',
- 'location' : 'China',
- 'language' : 'Python' }
- data = urllib.urlencode(values)
- req = urllib2.Request(url, data) #发送请求同时传data表单
- response = urllib2.urlopen(req) #接受反馈的信息
- the_page = response.read() #读取反馈的内容
如果读者照抄上面的程序,然后运行代码,肯定是报错的,因为那个url中没有相应的接受客户端post上去的data的程序文件,为了让程序运行,读者可以开发接收数据的程序。上面的代码只是以一个例子来显示Request对象的另外一个用途,并且在这个例子中以post方式提交数据。
在网站中,有的会通过User-Agent来判断访问者是浏览器还是别的程序,如果通过别的程序访问,它有可能拒绝。这时候我们编写程序去访问,就要设置headers了。设置方法是:
- user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
- headers = { 'User-Agent' : user_agent }
然后重新建立Request对象:
- req = urllib2.Request(url, data, headers)
再用urlopen()方法访问:
- response = urllib2.urlopen(req)
除了上面的演示之外,urllib2模块的东西还有很多,比如还可以:
- 设置HTTP Proxy
- 设置Timeout值
- 自动redirect
- 处理cookie这些内容不再一一介绍,当需要用到的时候可以查看文档或者去网上搜索。
6.3.11 XML
XML在软件领域用途非常广泛,有名人曰:
“当XML(扩展标记语言)于1998年2月被引入软件工业界时,它给整个行业带来了一场风暴。有史以来第一次,这个世界拥有了一种用来结构化文档和数据的通用且适应性强的格式,它不仅仅可以用于Web,而且可以被用于任何地方。”
——《Designing With Web Standards Second Edition》,Jeffrey Zeldman
如果要对XML做一个定义式的说明,就不得不引用w3school里面简洁而明快的说明:
- XML指可扩展标记语言(EXtensible Markup Language)。
- XML是一种标记语言,很类似于HTML。
- XML的设计宗旨是传输数据,而非显示数据。
- XML标签没有被预定义,你需要自行定义标签。
- XML被设计为具有自我描述性。
- XML是W3C的推荐标准。如果读者要详细了解和学习XML,可以阅读w3school的教程。
XML的重要在于它是用来传输数据的,因此,特别是在Web编程中,经常要用到。有了它让数据传输变得简单了,这么重要,Python当然支持。
一般来讲,一个引人关注的东西,总会有很多人从不同侧面去关注。在编程语言中也是如此,所以,对XML这个明星似的东西,Python提供了多种模块来处理。
- xml.dom.*模块:Document Object Model。适合用于处理DOM API。它能够将XML数据在内存中解析成一棵树,然后通过对树的操作来操作XML,但是,由于这种方式将XML数据映射到内存中的树,导致比较慢,且消耗更多内存。
- xml.sax.*模块:simple API for XML。由于SAX以流式读取XML文件,从而速度较快,且少占用内存,但是操作上稍复杂,需要用户实现回调函数。
- xml.parser.expat:是一个直接的,低级一点的基于C的expat的语法分析器。expat接口基于事件反馈,有点像SAX但又不太像,因为它的接口并不是完全规范于expat库的。
- xml.etree.ElementTree(以下简称ET):元素树。它提供了轻量级的Python式的API,相对于DOM,ET快了很多,而且有很多令人愉悦的API可以使用;相对于SAX,ET也有ET.iterparse提供了“在空中”的处理方式,没有必要加载整个文档到内存,节省内存。ET性能的平均值和SAX差不多,但是API的效率更高一点而且使用起来很方便。所以,我用xml.etree.ElementTree。
ElementTree在标准库中有两种实现,一种是纯Python实现:xml.etree.ElementTree,另外一种是:xml.etree.cElementTree。
如果读者使用的是Python 2.x,可以像这样引入模块:
- try:
- import xml.etree.cElementTree as ET
- except ImportError:
- import xml.etree.ElementTree as ET
如果是Python 3.3以上,就没有这个必要了,只需要一句话import xml.etree.ElementTree as ET即可,然后由模块自动来寻找适合的方式。显然Python 3.x相对Python 2.x有了很大进步。
1.遍历查询
先要做一个XML文档,用w3school中的一个例子,如图6-6所示。
这是一个XML树,只不过是用图来表示的,先把这棵树写成XML文档格式。
- <bookstore>
- <book category="COOKING">
- <title lang="en">Everyday Italian</title>
- <author>Giada De Laurentiis</author>
- <year>2005</year>
- <price>30.00</price>
- </book>
- <book category="CHILDREN">
- <title lang="en">Harry Potter</title>
- <author>J K. Rowling</author>
- <year>2005</year>
- <price>29.99</price>
- </book>
- <book category="WEB">
- <title lang="en">Learning XML</title>
- <author>Erik T. Ray</author>
- <year>2003</year>
- <price>39.95</price>
- </book>
- </bookstore>
图6-6 XML树
将XML保存为名为22601.xml的文件,接下来就是以它为对象,练习各种招数了。
- >>> import xml.etree.cElementTree as ET
为了简化,用这种方式引入,在编程实践中推荐读者使用try…except…方式。
- >>> tree = ET.ElementTree(file="22601.xml")
- >>> tree
- <ElementTree object at 0xb724cc2c>
建立起XML解析树,然后通过根节点向下开始读取各个元素(element对象)。
在上述XML文档中,根元素是bookstore,它没有属性,或者属性为空。
- >>> root = tree.getroot() #获得根
- >>> root.tag
- 'bookstore'
- >>> root.attrib
- {}
要想将根下面的元素都读出来,可以:
- >>> for child in root:
- ... print child.tag, child.attrib
- ...
- book {'category': 'COOKING'}
- book {'category': 'CHILDREN'}
- book {'category': 'WEB'}
也可以用下面方法读取指定元素的信息:
- >>> root[0].tag
- 'book'
- >>> root[0].attrib
- {'category': 'COOKING'}
- >>> root[0].text #无内容
- '\n '
再深入一层,就有内容了:
- >>> root[0][0].tag
- 'title'
- >>> root[0][0].attrib
- {'lang': 'en'}
- >>> root[0][0].text
- 'Everyday Italian'
对于ElementTree对象,有一个iter方法可以对指定名称的子节点进行深度优先遍历。例如:
- >>> for ele in tree.iter(tag="book"): #遍历名称为book的节点
- ... print ele.tag, ele.attrib
- ...
- book {'category': 'COOKING'}
- book {'category': 'CHILDREN'}
- book {'category': 'WEB'}
- >>> for ele in tree.iter(tag="title"): #遍历名称为title的节点
- ... print ele.tag, ele.attrib, ele.text
- ...
- title {'lang': 'en'} Everyday Italian
- title {'lang': 'en'} Harry Potter
- title {'lang': 'en'} Learning XML
如果不指定元素名称,就是将所有的元素遍历一遍。
- >>> for ele in tree.iter():
- ... print ele.tag, ele.attrib
- ...
- bookstore {}
- book {'category': 'COOKING'}
- title {'lang': 'en'}
- author {}
- year {}
- price {}
- book {'category': 'CHILDREN'}
- title {'lang': 'en'}
- author {}
- year {}
- price {}
- book {'category': 'WEB'}
- title {'lang': 'en'}
- author {}
- year {}
- price {}
除了上面的方法,还可以通过路径搜索到指定的元素,读取其内容,这就是xpath。此处对xpath不详解,如果要了解可以到网上搜索有关信息。
- >>> for ele in tree.iterfind("book/title"):
- ... print ele.text
- ...
- Everyday Italian
- Harry Potter
- Learning XML
利用findall()方法,也可以实现查找功能:
- >>> for ele in tree.findall("book"):
- ... title = ele.find('title').text
- ... price = ele.find('price').text
- ... lang = ele.find('title').attrib
- ... print title, price, lang
- ...
- Everyday Italian 30.00 {'lang': 'en'}
- Harry Potter 29.99 {'lang': 'en'}
- Learning XML 39.95 {'lang': 'en'}
2.编辑
除了读取有关数据之外,还能对XML进行编辑,即增、删、改、查功能,还是以上面的XML文档为例:
- >>> root[1].tag
- 'book'
- >>> del root[1]
- >>> for ele in root:
- ... print ele.tag
如此,成功删除了一个节点,原来有三个book节点,现在就还剩两个了。打开源文件再看看,是不是正好少了第二个节点呢?一定很让你失望,源文件居然没有变化。
的确如此,源文件没有变化,因为至此的修改动作,还停留在内存中,还没有将修改结果输出到文件。不要忘记,我们是在内存中建立的ElementTree对象。再这样做:
- >>> import os
- >>> outpath = os.getcwd()
- >>> file = outpath + "/22601.xml"
把当前文件路径拼装好。然后:
- >>> tree.write(file)
再看源文件,已经变成两个节点了。
除了删除,也能够修改:
- >>> for price in root.iter("price"): #原来每本书的价格
- ... print price.text
- ...
- 30.00
- 39.95
- >>> for price in root.iter("price"): #每本上涨7元,并且增加属性标记
- ... new_price = float(price.text) + 7
- ... price.text = str(new_price)
- ... price.set("updated","up")
- ...
- >>> tree.write(file)
查看源文件:
- <bookstore>
- <book category="COOKING">
- <title lang="en">Everyday Italian</title>
- <author>Giada De Laurentiis</author>
- <year>2005</year>
- <price updated="up">37.0</price>
- </book>
- <book category="WEB">
- <title lang="en">Learning XML</title>
- <author>Erik T. Ray</author>
- <year>2003</year>
- <price updated="up">46.95</price>
- </book>
- </bookstore>
不仅价格修改了,而且在price标签里面增加了属性标记。
上面用del来删除某个元素,其实,在编程中用的不多,较多使用remove()方法。比如要删除price>40的书。可以这么做:
- >>> for book in root.findall("book"):
- ... price = book.find("price").text
- ... if float(price) > 40.0:
- ... root.remove(book)
- ...
- >>> tree.write(file)
于是就这样了:
- <bookstore>
- <book category="COOKING">
- <title lang="en">Everyday Italian</title>
- <author>Giada De Laurentiis</author>
- <year>2005</year>
- <price updated="up">37.0</price>
- </book>
- </bookstore>
接下来就要增加元素了。
- >>> import xml.etree.cElementTree as ET
- >>> tree = ET.ElementTree(file="22601.xml")
- >>> root = tree.getroot()
- >>> ET.SubElement(root, "book") #在root里面添加book节点
- <Element 'book' at 0xb71c7578>
- >>> for ele in root:
- ... print ele.tag
- ...
- book
- book
- >>> b2 = root[1] #得到新增的book节点
- >>> b2.text = "python" #添加内容
- >>> tree.write("22601.xml")
查看源文件:
- <bookstore>
- <book category="COOKING">
- <title lang="en">Everyday Italian</title>
- <author>Giada De Laurentiis</author>
- <year>2005</year>
- <price updated="up">37.0</price>
- </book>
- <book>python</book>
- </bookstore>
3.常用属性和方法总结
ET里面的属性和方法不少,这里列出常用的,供使用中备查。
(1)Element对象
常用属性如下。
- tag:string,元素数据种类。
- text:string,元素的内容。
- attrib:dictionary,元素的属性字典。
tail:string,元素的尾形。针对属性的操作如下。
clear():清空元素的后代、属性、text和tail也设置为None。
- get(key,default=None):获取key对应的属性值,如该属性不存在则返回default值。
- items():根据属性字典返回一个列表,列表元素为(key,value)。
- keys():返回包含所有元素属性键的列表。
set(key,value):设置新的属性键与值。针对后代的操作如下。
append(subelement):添加直系子元素。
- extend(subelements):增加一串元素对象作为子元素。
- find(match):寻找第一个匹配子元素,匹配对象可以为tag或path。
- findall(match):寻找所有匹配子元素,匹配对象可以为tag或path。
- findtext(match):寻找第一个匹配子元素,返回其text值。匹配对象可以为tag或path。
- insert(index,element):在指定位置插入子元素。
- iter(tag=None):生成遍历当前元素所有后代或者给定tag的后代的迭代器。
- iterfind(match):根据tag或path查找所有的后代。
- itertext():遍历所有后代并返回text值。
remove(subelement):删除子元素。(2)ElementTree对象
find(match)。
- findall(match)。
- findtext(match,default=None)。
- getroot():获取根节点。
- iter(tag=None)。
- iterfind(match)。
- parse(source,parser=None):装载xml对象,source可以为文件名或文件类型对象。
- write(file,encoding="us-ascii",xml_declaration=None,default_namespace=None,method="xml")。
6.3.12 JSON
就传递数据而言,XML是一种选择,还有另外一种——JSON,它是一种轻量级的数据交换格式,如果读者要做Web编程,则会用到它。根据维基百科的相关内容,对JSON了解一下:
JSON(JavaScript Object Notation)是一种由道格拉斯·克罗克福特构想设计、轻量级的数据交换语言,以文字为基础,且易于让人阅读。尽管JSON是JavaScript的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于C语言家族的一些习惯。
关于JSON更为详细的内容,可以参考网站:http://www.json.org。
从上述网站摘取部分内容,了解一下JSON的结构。
JSON建构于两种结构:
- “名称/值”对的集合(A collection of name/value pairs),不同的语言中,它被理解为对象(object)、纪录(record)、结构(struct)、字典(dictionary)、哈希表(hash table)、有键列表(keyed list)或者关联数组(associative array)。
值的有序列表(An ordered list of values),在大部分语言中,它被理解为数组(array)。Python标准库中有JSON模块,主要执行序列化和反序列化功能。
序列化:encoding,把一个Python对象编码转化成JSON字符串。
- 反序列化:decoding,把JSON格式字符串解码转换为Python数据对象。1.基本操作
JSON模块相对XML单纯了很多:
- >>> import json
- >>> json.__all__
- ['dump', 'dumps', 'load', 'loads', 'JSONDecoder', 'JSONEncoder']
- encoding: dumps()
- >>> data = [{"name":"qiwsir", "lang":("python", "english"), "age":40}]
- >>> print data
- [{'lang': ('python', 'english'), 'age': 40, 'name': 'qiwsir'}]
- >>> data_json = json.dumps(data)
- >>> print data_json
- [{"lang": ["python", "english"], "age": 40, "name": "qiwsir"}]
encoding的操作是比较简单的,请注意观察data和data_json的不同——lang的值从元组变成了列表,还有不同:
- >>> type(data_json)
- <type 'str'>
- >>> type(data)
- <type 'list'>
- decoding: loads()decoding的过程也像上面一样简单:
- >>> new_data = json.loads(data_json)
- >>> new_data
- [{u'lang': [u'python', u'english'], u'age': 40, u'name': u'qiwsir'}]
需要注意的是,解码之后并没有将元组还原。
上面的data都不是很长,还能凑合阅读,如果很长了,阅读就有难度了。所以,JSON的dumps()提供了可选参数,利用它们能在输出上对人更友好(这对机器是无所谓的)。
- >>> data_j = json.dumps(data, sort_keys=True, indent=2)
- >>> print data_j
- [
- {
- "age": 40,
- "lang": [
- "python",
- "english"
- ],
- "name": "qiwsir"
- }
- ]
sort_keys=True意思是按照键的字典顺序排序,indent=2是让每个键/值对显示的时候,以缩进两个字符对齐,这样的视觉效果好多了。
2.大JSON字符串
如果数据不是很大,那么上面的操作足够了,但现在是“大数据”时代了,随便一个什么业务都在说自己是大数据,显然不能总让JSON很小。前面的操作方法是将数据都读入内存,如果数据量太大了内存会爆满,这肯定是不行的。怎么办?JSON提供了load()函数和dump()函数解决这个问题,注意,跟已经用过的函数相比是不同的,请仔细观察。
- >>> import tempfile #临时文件模块
- >>> data
- [{'lang': ('python', 'english'), 'age': 40, 'name': 'qiwsir'}]
- >>> f = tempfile.NamedTemporaryFile(mode='w+')
- >>> json.dump(data, f)
- >>> f.flush()
- >>> print open(f.name, "r").read()
- [{"lang": ["python", "english"], "age": 40, "name": "qiwsir"}]