python小技巧与坑

python小技巧与坑

这几天打算用python写个小爬虫,就找出先前写python时候整理的一份小文档,顺便把它贴到博客上面来,以后如果还有其他心得,也一并记录在这里好了。

1. 时间处理

python中有四种时间表示方式: 字符串,时间戳,time.struct_time类型,datetime.datetime类型

屏幕快照 2017-09-19 上午12.13.14

2. 过程计时

不要用python 2.7中time模块的time.time()或者time.clock()函数,这两个函数都是平台相关的。在python3.3+版本,time.clock()也已经被取消。

3. 输出错误信息

注意,第一行# -*- coding: utf-8 -*-是为了让python识别文件中的中文。

 

4. 线程不安全

python有不少模块是线程不安全的。

mysql.connector 的cursor、connector变量也是线程不安全的,千万不要在线程里面共享。

logging模块则是线程安全的。

5. 多线程中让主线程等待子线程完成后退出

注意:由于全局解释器锁(Global Interpreter Lock,GIL)的作用,python的多线程只会在一个CPU核上运行。所以对于CPU密集型的程序,python的多线程其实根本没有帮助;对于IO密集型的程序的话,还是可以用用的,比如爬虫。

如果想要利用CPU的多个核,最好使用python的多进程模块multiprocessing。参考使用 Python 实现多进程

PS:解释型语言天生都有多线程短板。python如此;javascript根本就没有多线程的概念,不过他有反人类的异步回调机制;php是后来出了pthreads扩展才支持的多线程。

6. 关于urllib2的坑

(1) urllib与urllib2有个缺陷,就是他们是不可重用链接的,即使你在request的hearder中添加connection:keep-alive信息,但在urllib与urllib2的open函数底层,他会自动覆盖这个header为connection:close。即每次发生一个请求后,都会关闭连接,所以每次response响应都返回connection:close的头。下次请求时就重新建立链接。。。 =。=#

要想可重用链接,可以使用urllib3

(2) 如果在多线程中使用不同代理的话,就不能使用urllib2.urlopen()了,因为urllib2.install_opener() 安装的是一个全局变量。而是应该在线程里直接用opener.open()

7. 程序入口 if __name__ == ‘__main__’:

python的一个脚本文件,既可以独立执行,又可以当做一个模块被引入另一个文件。

以上面这段代码为例,A.py可以作为一个模块被引用,这时它有两个函数A.test1()跟A.test2();如果单独执行python A.py,那么if __name__ == ‘__main__’:就相当于是该脚本的程序入口,将会执行test1()函数。

这在自定义模块时候非常好用,通常我们会在__main__入口下面放些测试代码,用来单独测试模块的可用性。我觉得这是一个好的python编程习惯。

8. with … as …

推荐使用with … as …来打开文件

该用法相当于如下这段try … except … finally

9. python的类

9.1 class virables与instance virables

class virables有点像类静态变量的意思,所有对象共享该变量(实际上这样讲不严谨,因为对象有可能覆盖一个class virable为instance virable),

instance virables就是普通的变量,每个对象有一份。

参考https://docs.python.org/2/tutorial/classes.html#class-and-instance-variables

9.2 私有变量、私有方法、类方法、静态方法

 

10. 迭代器与生成器(yield)

http://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/

 

11. 尽量使用logger = logging.getLogger(name)而不是logging

python的logger是有层次关系的,直接使用logging.info()、logging.error()相当于调用的是root logger。logging.getLogger(name)则是从root logger下派生一个名字为name的子logger,一般使用类名进行命名。logging.getLogger()不加参数默认获取root logger。

输出:屏幕快照 2016-04-08 下午10.27.02

 

另外,上面我们在连个类中获取了自己的logger,却没有配loghandler,也依然能够输出日志。是因为默认情况下,logger的所有日志都会上传给其父亲logger,父logger再调用自己的loghandler。所以上面的例子中,负责输出日志的其实都是root logger的默认handler。

我们可以给子logger添加一个自己的loghandler。这时候,日志将会通过子logger的loghandler输出一次,同时也会通过父logger的loghandler输出一次。可以通过logger.propagate=False把日志“传播”给关掉,不上传给父logger。

 

12. logging.config读取log配置文件

用过java的log4j,觉得java的log配置特别方便,那么python的logging.config模块其实也可以做到。

新建一个log.config文件

然后修改测试代码如下:

运行该代码,就会发现总共有三处日志输入,分别是控制台、file.log日志文件、rotate.log按时间分割的日志文件。

注意,关于logging handler有一些需要注意的地方:

首先,StreamHandler、FileHandler在logging模块下,但TimedRotatingFileHandler是在logging.handlers模块下。

又注,自带的XXXRotatingFileHandler其实都是针对单进程且常驻内存的进程,对于多进程同时运行,或者单进程瞬时运行(每次只运行一小段时间,但频繁启动)的时候,就不适用了。首先对于多进程的切分,会导致日志丢失的情况,这时候可以考虑用WatchedFileHandler,然后自己写crontab进行日志切分。然后对于单进程瞬时运行的情况,我试过按小时切分的测试脚本,1:30运行的一次测试,理论上2:00再运行一次的时候,日志文件应该被切分的,但它不,因为对于新运行的进程来说,他是去判断文件修改时间的,还不足1小时我天。。。。

另外, 在linux下尽量用WatchedFileHandler(windows不适用),因为当日志文件被移动或删除后:

  1. FileHandler会继续将日志输出至原有的文件描述符, 从而导致日志切分后日志丢失;
  2. WatchedFileHandler会检测文件是否被移动或删除, 如果有, 会新建日志文件, 并输出日志到新建的文件。

13. 编码规范

详细参考:http://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/

13.1 命名规范

类名使用CapWords的方式,全局变量名使用全大写辅以下划线的方式,模块名、包名、函数名、局部变量名等都使用全小写辅以下划线的方式:

module_name, package_name, ClassName, method_name, ExceptionName, function_name, GLOBAL_VAR_NAME, instance_var_name, function_parameter_name, local_var_name.

ps:我以前的代码都是按照c++、java的驼峰方式来命名变量,函数。然后每次PyCharm都有一大堆波浪线提示命名不标准,神烦。=。=#

13.2 引号的使用规范

自然语言使用双引号,比如输出:print u”hello, 哈呜”

机器标识使用单引号,比如键名:print people[‘name’]

多行字符串以及文档字符串用三重双引号”””… …”””而非三重单引号”’… …”’

13.3 空格

在二元操作符两边都加上一个空格, 比如赋值(=), 比较(==, <, >, !=, <>, <=, >=, in, not in, is, is not), 布尔(and, or, not). 至于算术操作符两边的空格该如何使用, 需要你自己好好判断. 不过两侧务必要保持一致.

当’=’用于指示关键字参数或默认参数值时, 不要在其两侧使用空格.

14. 配置文件解析器ConfigParser

之前我还傻逼逼的自己写配置文件解析。(´・_・`) 后来才发现原来python标准库里面就有。参考文章http://www.cnblogs.com/huey/p/4334152.html

新建配置文件myproject.config

火狐截图_2017-01-17T15-52-55.635Z

读取配置文件时候,推荐使用config.readfp(codecs.open(config_file, ‘r’, ‘utf8’))来代替config.read(config_file),前者读入的字符串将会是unicode类型,后者则是str类型。

15. 装饰器

python装饰器的语法糖看起来就像java的注解,作用就像面向切面编程。

ps:《用装饰器写一个single instance》

16. python的字符编码(the fucking UnicodeEncodeError)

16.1 字符集(charset)与字符编码(encoding)

字符集:即可使用的字符的集合,同时规定了每个字符对应的二进制码。常见的有ascii、gb2312、unicode。

屏幕快照 2017-06-18 14.59.20

字符编码:规定字符二进制码的存储方式。常见的有ascii编码、gb2312编码、utf-8编码。

ps 1:通常,字符集与字符编码成一一对应关系。比如ascii字符集与字符编码,gb2312字符集与字符编码,但是偏偏unicode字符集却又许多种编码方式,utf-8、utf-16、utf-32都针对unicode字符集的字符编码。

ps 2:字符经过编码后存在内存中的二进制值未必会等值于其在字符集中所规定的二进制值,因为编码可能会对二进制码进行相应的转换。

比如ascii编码规定直接按字符的ascii二进制码存储,占用一个字节。

utf-8编码会对字符的unicode二进制码进行转换后再存储,占用字节数从1~6个字节不等。

utf-16编码同样也会对unicode二进制码进行转换后再存储,占用2或4个字节,同时utf-16编码在存储时候还分大尾序和小尾序。

屏幕快照 2017-06-18 15.17.45

16.2 <type ‘str’>与<type ‘unicode’>,decode与encode

python2有两种字符串类型,一种是str字符串(byte string),一种是unicode字符串(text string)。

(python3的字符串类型不一样了,变成了str类型与bytes类型。str类型即是unicode字符串,bytes类型则表示二进制字符串)

str字符串可以看做是一个字节数组,即将字符串经过字符编码后得到的二进制字节串。(字符编码可以通过encode()方法指定)

unicode字符串则是一unicode字符数组,每个单元就是一个unicode字符。

我们以 a = “a你好” , b = u”b你好” 这两个字符串做个测试。

屏幕快照 2017-06-18 22.07.37

我们可以看到,type(a)是str字符串,由于我的测试环境是mac,系统默认的中文编码是utf-8,对于字符串长度,其中英文a占一个字节,你好各占3个字节,所以总长度为7。

然后再看字符串b,在python中,字符串前面的u表示该字符串为unicode字符串。所以type(b)得到的就是unicode类型。然后长度就是3个unicode字符。

python有一对专门的函数来进行byte string与unicode string之间的相互转换:encode()与decode()。

decode()将byte string(str字符串)解码成text string(unicode字符串),二进制码串 >> 解码 >> 字符串
encode()将text string(unicode字符串)编码成byte string(str字符串),字符串 >> 编码 >> 二进制码串
所以,调用decode的主体,必须是str字符串;调用encode的主体必须是unicode字符串。

 

16.3 # -*- coding: utf-8 -*-与sys.setdefaultencoding(‘utf-8’)

通常,我们需要在python脚本的开头写上一行关于utf-8的注释,如下所示。

这是因为python解释器默认使用ascii编码来读取脚本文件(它认为文件是以ascii编码存储的),如果文件中出现非ascii字符,就会报错。所以必须在文件开头加上这一行,从一开始就告诉python解释器使用utf-8编码来读取脚本,这样才能在脚本中正常使用中文等非ascii字符。

注意:这一行不是代码,不会执行的,它只是影响Python解释器读取脚本文件时候使用的编码。

然后,对于sys.setdefaultencoding(‘utf-8’),与上面那行注释的作用不同,它用来指定python在运行时所用的默认字符编码。Python2.x为了兼容处理Unicode字符串(unicode)和str字符串,某些时候会自动做两者的转换,但默认使用的是ASCII字符编码,如果有utf-8字符在里面就会出错。这个hack是修改这个默认特性。非常不建议这么做,你应该永远分清str(bytes)和unicode,然后自己调用encode、decode来转换。Python3.x下这两个类型已经不兼容了。

 

17. 浮点数转换的坑

int()函数将浮点数转换成整数时,是直接截取整数位,而不考虑小数位是否需要四舍五入的!所以浮点数转换成整数需要先手工四舍五入下再转int。否则遇到99.9999999这种直接int就会变成99而不是100了。

18. 包、模块与类

这三者的关系可以简单的理解为:包是目录,模块是目录下的xxx.py文件,类是xxx.py文件定义的Class。

模块module,是一个py文件。它在第一次导入程序的时候被执行(引入模块文件中定义的类、函数,或直接模块文件中的语句)

包package,是一个目录,目录中必须包含一个__init__.py文件。另外还可以有多个模块文件与“子包”。__init__.py文件可以看作是包的默认模块,该文件可以为空,也可以在其中定义变量、函数、类,或者执行import。

18.1 导入模块文件

以如下目录结构为例,主文件main.py以及模块文件common.py:

这个主程序的执行结果是

18.2 import module与from … import …的区别

import关键字后面跟的都是module。即使有时候看起来是包名,其实import package等价于import package.__init__,而__init__是这个包的默认模块。

from … import …的用法可以是:

from module import Class/func/variable

from package import module

18.3 导入包中的模块

以如下文件结构为例:

我们来看一下main.py文件中的几个import语句的区别:

import draw 实际上只是导入了draw包目录下的__init__.py文件。所以可以通过draw.PI来引用draw包的默认模块(__init__.py)中定义的PI变量。但是这句并不会导入除__init__之外的其他模块:colors与shapes模块!!!

from draw import colors 从draw包中导入colors模块。所以可以通过colors.BasicColor来引用colors模块中的类。

from draw.shapes import BasicShape 从shapes模块中直接导入BasicShape类。所以可以直接调用BasicShape类了。

 

18.4 __init__.py的用法

可能我们并不想用from draw.shapes import BasicShape这么长的路径来引入一个类(这还不算长的,如果考虑多级包目录的话)。那么,我们可以在draw目录的__init__.py文件中加上一行:

那么,在main.py中,就可以通过import draw来使用draw.BsasicShape了。或者通过from draw import BasicShape来直接使用BasicShape了。

19. string.format与数据库操作时候的参数

执行数据库语句的时候,通常需要传递变量给要执行的语句。这时候有两种方法。一种是用string.format()来格式化要执行的语句,一种的使用数据库cursor.execute(‘sql_string’, argv)的argv参数来传递变量值。

19.1 string.format()用法

19.2 cursor.execute()用法

通常的数据库驱动实现都支持带参数的cursor.execute()方法,并且都支持%s与%(name)s这两种参数定位。

 

20. 动态定义类成员变量

有时候需要动态定义(调用)变量名字,或者说用字符串定义(调用)变量名。

如果是在函数中定义局部变量,可以使用exec(‘%s=%s’%(‘var_name’, value))。但这个我试了只在python 2有效,python 3就无效了。

如果是要在类中动态定义成员变量,可以使用类的__dict__特殊变量来定义。举个例子:

21. ftplib的编码问题

使用python自带的ftplib,在处理中文路径时都会遇到这样一个异常:UnicodeEncodeError: ‘latin-1’ codec can’t encode characters…

这是由于ftplib.py默认使用latin-1编码,而且还硬编码在FTP类的类变量中,简直脑残,设计构造函数可传递的成员变量都好呀。凸=。=

这个情况,有两种方式应对:

1、新建FTP实例后,立即赋值一个encoding成员变量覆盖之(ftplib.py中用到encoding的地方都是用self.encoding来调用,而不是FTP.encoding来调用,所以该方法是有效的)

2、每次遇到中文字符串,都主动改成latin-1编码

3、网上居然有人说直接修改ftplib.py源码。。。简直👎

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注