Python模块与包

模块与包

9.1、常见模块

9.1.1、time模块

(1)三种时间形式

在Python中,通常有这三种方式来表示时间:时间戳、元组(struct_time)、格式化的时间字符串:

(1) 时间戳(timestamp) :通常来说,时间戳表示的是从1970年1月1日00:00:00开始按秒计算的偏移量。我们运行“type(time.time())”,返回的是float类型。

(2) 格式化的时间字符串(Format String): ‘1988-03-16’

(3) 元组(struct_time) :struct_time元组共有9个元素共九个元素:(年,月,日,时,分,秒,一年中第几周,一年中第几天等)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# <1> 时间戳

>>> import time
>>> time.time()     

1493136727.099066

# <2> 时间字符串
>>> time.strftime("%Y-%m-%d %X")
'2017-04-26 00:32:18'

# <3> 时间元组
>>> time.localtime()
time.struct_time(tm_year=2017, tm_mon=4, tm_mday=26,
                 tm_hour=0, tm_min=32, tm_sec=42, tm_wday=2,
                 tm_yday=116, tm_isdst=0)

小结:时间戳是计算机能够识别的时间;时间字符串是人能够看懂的时间;元组则是用来操作时间的

(2)时间转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 一 时间戳 <----> 结构化时间:  localtime/gmtime   mktime

>>> time.localtime(3600*24)
>>> time.gmtime(3600*24)

>>> time.mktime(time.localtime())

#字符串时间 <----> 结构化时间: strftime/strptime

>>> time.strftime("%Y-%m-%d %X", time.localtime())
>>> time.strptime("2017-03-16","%Y-%m-%d")

(3)、其它方法

1
2
3
4
5
>>> time.asctime(time.localtime(312343423))
'Sun Nov 25 10:03:43 1979'
>>> time.ctime(312343423)
'Sun Nov 25 10:03:43 1979'
>>> time.sleep(seconds) # 线程推迟指定的时间运行,单位为秒。

9.1.2、datetime模块

datetime模块定义了以下几个类:

类名称描述
datetime.date 表示日期,常用的属性有:year, month和day
datetime.time 表示时间,常用属性有:hour, minute, second, microsecond
datetime.datetime 表示日期时间
datetime.timedelta 表示两个date、time、datetime实例之间的时间间隔,分辨率(最小单位)可达到微秒

(1) date和time类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import datetime

# (1) date类
date = datetime.date(2021,5,29)
print(date.year)
print(date.month)
print(date.day)

today = datetime.date.today()
print(today)

# 可以比较
print(today == date)
print(today < date)

# (2) time类: 时分秒
time = datetime.time(20,0,0)
print(time.hour)
print(time.minute)
print(time.second)
print(time.isoformat())
print(time.strftime('%H %M %S'))

(2) datetime类

1
class datetime.datetime(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
类方法/属性名称描述
datetime.today() 返回一个表示当前本期日期时间的datetime对象
datetime.now([tz]) 返回指定时区日期时间的datetime对象,如果不指定tz参数则结果同上
datetime.utcnow() 返回当前utc日期时间的datetime对象
datetime.fromtimestamp(timestamp[, tz]) 根据指定的时间戳创建一个datetime对象
datetime.utcfromtimestamp(timestamp) 根据指定的时间戳创建一个datetime对象
datetime.strptime(date_str, format) 将时间字符串转换为datetime对象

对象方法和属性

对象方法/属性名称描述
dt.year, dt.month, dt.day 年、月、日
dt.hour, dt.minute, dt.second 时、分、秒
dt.date() 获取datetime对象对应的date对象
dt.time() 获取datetime对象对应的time对象, tzinfo 为None
dt.isoformat([sep]) 返回一个‘%Y-%m-%d
dt.strftime(format) 返回指定格式的时间字符串

(3) datetime.timedelta类

datetime.timedelta类的定义:

1
class datetime.timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, hours=0, weeks=0)

timedelta对象表示连个不同时间之间的差值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#  datetime类: 年月日时分秒

import datetime
dt = datetime.datetime(2012,12,12,20,5,0)
print(dt.year)
print(dt.minute)
# 获取当前时间
now = datetime.datetime.now()
today = datetime.datetime.today()
print(now)
print(today)
print(today == now)
# 计算此刻三天前的时间对象
delta = datetime.timedelta(days=3)
before_3day = now - delta
print(before_3day)

9.1.3、random模块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
>>> import random
>>> random.random()      # 大于0且小于1之间的小数
0.7664338663654585
>>> random.randint(1,5)  # 大于等于1且小于等于5之间的整数
2
>>> random.randrange(1,3) # 大于等于1且小于3之间的整数
1
>>> random.choice([1,'23',[4,5]])  # #1或者23或者[4,5]
1
>>> random.sample([1,'23',[4,5]],2) # #列表元素任意2个组合
[[4, 5], '23']
>>> random.uniform(1,3) #大于1小于3的小数
1.6270147180533838
>>> item=[1,3,5,7,9]
>>> random.shuffle(item) # 打乱次序
>>> item
[5, 1, 3, 7, 9]
>>> random.shuffle(item)
>>> item
[5, 9, 7, 1, 3]

随机验证码案例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import random

def v_code():

    code = ''
    for i in range(5):

        num=random.randint(0,9)
        alf=chr(random.randint(65,90))
        add=random.choice([num,alf])
        code="".join([code,str(add)])

    return code

print(v_code())

9.1.4、hash模块

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

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

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

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

(1)摘要算法介绍

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

1
2
3
4
5
6
7
8
import hashlib
 
md5 = hashlib.md5()
md5.update('how to use md5 in python hashlib?')
print md5.hexdigest()

# 计算结果如下:
d26a53750bc40b38b65a520292f69306

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

1
2
3
4
md5 = hashlib.md5()
md5.update('how to use md5 in ')
md5.update('python hashlib?')
print md5.hexdigest()

MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,通常用一个32位的16进制字符串表示。另一种常见的摘要算法是SHA1,调用SHA1和调用MD5完全类似:

1
2
3
4
5
6
import hashlib
 
sha1 = hashlib.sha1()
sha1.update('how to use sha1 in ')
sha1.update('python hashlib?')
print sha1.hexdigest()

SHA1的结果是160 bit字节,通常用一个40位的16进制字符串表示。比SHA1更安全的算法是SHA256和SHA512,不过越安全的算法越慢,而且摘要长度更长。

(2)摘要算法应用

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

1
2
3
4
5
name    | password
--------+----------
michael | 123456
bob     | abc999
alice   | alice2008

如果以明文保存用户口令,如果数据库泄露,所有用户的口令就落入黑客的手里。此外,网站运维人员是可以访问数据库的,也就是能获取到所有用户的口令。正确的保存口令的方式是不存储用户的明文口令,而是存储用户口令的摘要,比如MD5:

1
2
3
4
5
username | password
---------+---------------------------------
michael  | e10adc3949ba59abbe56e057f20f883e
bob      | 878ef96e86145580c38c87f0410ad153
alice    | 99b1c2188db85afee403b1536010c2c9

考虑这么个情况,很多用户喜欢用123456,888888,password这些简单的口令,于是,黑客可以事先计算出这些常用口令的MD5值,得到一个反推表:

1
2
3
'e10adc3949ba59abbe56e057f20f883e': '123456'
'21218cca77804d2ba1922c33e0151105': '888888'
'5f4dcc3b5aa765d61d8327deb882cf99': 'password'

这样,无需破解,只需要对比数据库的MD5,黑客就获得了使用常用口令的用户账号。

对于用户来讲,当然不要使用过于简单的口令。但是,我们能否在程序设计上对简单口令加强保护呢?

由于常用口令的MD5值很容易被计算出来,所以,要确保存储的用户口令不是那些已经被计算出来的常用口令的MD5,这一方法通过对原始口令加一个复杂字符串来实现,俗称“加盐”:

1
hashlib.md5("salt".encode("utf8"))

经过Salt处理的MD5口令,只要Salt不被黑客知道,即使用户输入简单口令,也很难通过MD5反推明文口令。

但是如果有两个用户都使用了相同的简单口令比如123456,在数据库中,将存储两条相同的MD5值,这说明这两个用户的口令是一样的。有没有办法让使用相同口令的用户存储不同的MD5呢?

如果假定用户无法修改登录名,就可以通过把登录名作为Salt的一部分来计算MD5,从而实现相同口令的用户也存储不同的MD5。

摘要算法在很多地方都有广泛的应用。要注意摘要算法不是加密算法,不能用于加密(因为无法通过摘要反推明文),只能用于防篡改,但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。

9.1.5、logging模块

(1)日志介绍

日志是一种可以追踪某些软件运行时所发生事件的方法。软件开发人员可以向他们的代码中调用日志记录相关的方法来表明发生了某些事情。一个事件可以用一个可包含可选变量数据的消息来描述。此外,事件也有重要性的概念,这个重要性也可以被称为严重性级别(level)。

在软件开发阶段或部署开发环境时,为了尽可能详细的查看应用程序的运行状态来保证上线后的稳定性,我们可能需要把该应用程序所有的运行日志全部记录下来进行分析,这是非常耗费机器性能的。当应用程序正式发布或在生产环境部署应用程序时,我们通常只需要记录应用程序的异常信息、错误信息等,这样既可以减小服务器的I/O压力,也可以避免我们在排查故障时被淹没在日志的海洋里。那么,怎样才能在不改动应用程序代码的情况下实现在不同的环境记录不同详细程度的日志呢?这就是日志等级的作用了,我们通过配置文件指定我们需要的日志等级就可以了。

不同的应用程序所定义的日志等级可能会有所差别,分的详细点的会包含以下几个等级:

级别何时使用
DEBUG 详细信息,典型地调试问题时会感兴趣。 详细的debug信息。
INFO 证明事情按预期工作。 关键事件。
WARNING 表明发生了一些意外,或者不久的将来会发生问题(如‘磁盘满了’)。软件还是在正常工作。
ERROR 由于更严重的问题,软件已不能执行一些功能了。 一般错误消息。
CRITICAL 严重错误,表明软件已不能继续运行了。
NOTICE 不是错误,但是可能需要处理。普通但是重要的事件。
ALERT 需要立即修复,例如系统数据库损坏。
EMERGENCY 紧急情况,系统不可用(例如系统崩溃),一般会通知所有用户。

一条日志信息对应的是一个事件的发生,而一个事件通常需要包括以下几个内容:

  • 事件发生时间

  • 事件发生位置

  • 事件的严重程度–日志级别

  • 事件内容

上面这些都是一条日志记录中可能包含的字段信息,当然还可以包括一些其他信息,如进程ID、进程名称、线程ID、线程名称等。日志格式就是用来定义一条日志记录中包含那些字段的,且日志格式通常都是可以自定义的。

logging模块默认定义了以下几个日志等级,它允许开发人员自定义其他日志级别,但是这是不被推荐的,尤其是在开发供别人使用的库时,因为这会导致日志级别的混乱。

日志等级(level)描述
DEBUG 最详细的日志信息,典型应用场景是 问题诊断
INFO 信息详细程度仅次于DEBUG,通常只记录关键节点信息,用于确认一切都是按照我们预期的那样进行工作
WARNING 当某些不期望的事情发生时记录的信息(如,磁盘可用空间较低),但是此时应用程序还是正常运行的
ERROR 由于一个更严重的问题导致某些功能不能正常运行时记录的信息
CRITICAL 当发生严重错误,导致应用程序不能继续运行时记录的信息

开发应用程序或部署开发环境时,可以使用DEBUG或INFO级别的日志获取尽可能详细的日志信息来进行开发或部署调试;

应用上线或部署生产环境时,应该使用WARNING或ERROR或CRITICAL级别的日志来降低机器的I/O压力和提高获取错误日志信息的效率。日志级别的指定通常都是在应用程序的配置文件中进行指定的。

说明:

  • 上面列表中的日志等级是从上到下依次升高的,即:DEBUG < INFO < WARNING < ERROR < CRITICAL,而日志的信息量是依次减少的;
  • 当为某个应用程序指定一个日志级别后,应用程序会记录所有日志级别大于或等于指定日志级别的日志信息,而不是仅仅记录指定级别的日志信息,nginx、php等应用程序以及这里的python的logging模块都是这样的。同样,logging模块也可以指定日志记录器的日志级别,只有级别大于或等于该指定日志级别的日志记录才会被输出,小于该等级的日志记录将会被丢弃。

(2)basicConfig日志

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import logging
LOG_FORMAT = "%(asctime)s %(name)s %(levelname)s %(pathname)s %(message)s "#配置输出日志格式
DATE_FORMAT = '%Y-%m-%d  %H:%M:%S %a ' #配置输出时间的格式,注意月份和天数不要搞乱了
logging.basicConfig(level=logging.DEBUG,
                    format=LOG_FORMAT,
                    datefmt = DATE_FORMAT ,
                    # filename=r"test.log", #有了filename参数就不会直接输出显示到控制台,而是直接写入文件
                    )
logging.debug("debug")
logging.info("info")
logging.warning("warning")
logging.error("error")
logging.critical("critical")
参数名称描述
filename 指定日志输出目标文件的文件名(可以写文件名也可以写文件的完整的绝对路径,写文件名日志放执行文件目录下,写完整路径按照完整路径生成日志文件),指定该设置项后日志信心就不会被输出到控制台了
filemode 指定日志文件的打开模式,默认为’a'。需要注意的是,该选项要在filename指定时才有效
format 指定日志格式字符串,即指定日志输出时所包含的字段信息以及它们的顺序。logging模块定义的格式字段下面会列出。
datefmt 指定日期/时间格式。需要注意的是,该选项要在format中包含时间字段%(asctime)s时才有效
level 指定日志器的日志级别
stream 指定日志输出目标stream,如sys.stdout、sys.stderr以及网络stream。需要说明的是,stream和filename不能同时提供,否则会引发 ValueError异常
style Python 3.2中新添加的配置项。指定format格式字符串的风格,可取值为'%'、'{‘和’$',默认为'%'
handlers Python 3.3中新添加的配置项。该选项如果被指定,它应该是一个创建了多个Handler的可迭代对象,这些handler将会被添加到root logger。需要说明的是:filename、stream和handlers这三个配置项只能有一个存在,不能同时出现2个或3个,否则会引发ValueError异常。

format格式字符串参数

字段/属性名称使用格式描述
asctime %(asctime)s 将日志的时间构造成可读的形式,默认情况下是‘2016-02-08 12:00:00,123’精确到毫秒
name %(name)s 所使用的日志器名称,默认是’root',因为默认使用的是 rootLogger
filename %(filename)s 调用日志输出函数的模块的文件名; pathname的文件名部分,包含文件后缀
funcName %(funcName)s 由哪个function发出的log, 调用日志输出函数的函数名
levelname %(levelname)s 日志的最终等级(被filter修改后的)
message %(message)s 日志信息, 日志记录的文本内容
lineno %(lineno)d 当前日志的行号, 调用日志输出函数的语句所在的代码行
levelno %(levelno)s 该日志记录的数字形式的日志级别(10, 20, 30, 40, 50)
pathname %(pathname)s 完整路径 ,调用日志输出函数的模块的完整路径名,可能没有
process %(process)s 当前进程, 进程ID。可能没有
processName %(processName)s 进程名称,Python 3.1新增
thread %(thread)s 当前线程, 线程ID。可能没有
threadName %(thread)s 线程名称
module %(module)s 调用日志输出函数的模块名, filename的名称部分,不包含后缀即不包含文件后缀的文件名
created %(created)f 当前时间,用UNIX标准的表示时间的浮点数表示; 日志事件发生的时间–时间戳,就是当时调用time.time()函数返回的值
relativeCreated %(relativeCreated)d 输出日志信息时的,自Logger创建以 来的毫秒数; 日志事件发生的时间相对于logging模块加载时间的相对毫秒数
msecs %(msecs)d 日志事件发生事件的毫秒部分。logging.basicConfig()中用了参数datefmt,将会去掉asctime中产生的毫秒部分,可以用这个加上

说明

  1. logging.basicConfig()函数是一个一次性的简单配置工具使,也就是说只有在第一次调用该函数时会起作用,后续再次调用该函数时完全不会产生任何操作的,多次调用的设置并不是累加操作。

  2. 如果要记录的日志中包含变量数据,可使用一个格式字符串作为这个事件的描述消息(logging.debug、logging.info等函数的第一个参数),然后将变量数据作为第二个参数*args的值进行传递,如:

    1
    
    logging.warning('%s is %d years old.', 'Tom', 10),
    

    输出内容为

    1
    
    WARNING:root:Tom is 10 years old.
    

(3)logger日志

日志流处理流程是一个模块级别的函数是logging.getLogger([name])(返回一个logger对象,如果没有指定名字将返回root logger)。

(1) logging日志模块四大组件

在介绍logging模块的日志流处理流程之前,我们先来介绍下logging模块的四大组件:

组件名称对应类名功能描述
日志器 Logger 提供了应用程序可一直使用的接口
处理器 Handler 将logger创建的日志记录发送到合适的目的输出
过滤器 Filter 提供了更细粒度的控制工具来决定输出哪条日志记录,丢弃哪条日志记录
格式器 Formatter 决定日志记录的最终输出格式

logging模块就是通过这些组件来完成日志处理的,上面所使用的logging模块级别的函数也是通过这些组件对应的类来实现的。

这些组件之间的关系描述:

  • 日志器(logger)需要通过处理器(handler)将日志信息输出到目标位置,如:文件、sys.stdout、网络等;
  • 不同的处理器(handler)可以将日志输出到不同的位置;
  • 日志器(logger)可以设置多个处理器(handler)将同一条日志记录输出到不同的位置;
  • 每个处理器(handler)都可以设置自己的过滤器(filter)实现日志过滤,从而只保留感兴趣的日志;
  • 每个处理器(handler)都可以设置自己的格式器(formatter)实现同一条日志以不同的格式输出到不同的地方。

简单点说就是:日志器(logger)是入口,真正干活儿的是处理器(handler),处理器(handler)还可以通过过滤器(filter)和格式器(formatter)对要输出的日志内容做过滤和格式化等处理操作。

(2) Handler类

Handler对象的作用是(基于日志消息的level)将消息分发到handler指定的位置(文件、网络、邮件等)。Logger对象可以通过addHandler()方法为自己添加0个或者更多个handler对象。比如,一个应用程序可能想要实现以下几个日志需求:

  1. 把所有日志都发送到一个日志文件中;
  2. 把所有严重级别大于等于error的日志发送到stdout(标准输出);
  3. 把所有严重级别为critical的日志发送到一个email邮件地址。这种场景就需要3个不同的handlers,每个handler复杂发送一个特定严重级别的日志到一个特定的位置。
1
2
3
Handler.setLevel(lel): # 指定被处理的信息级别,低于lel级别的信息将被忽略
Handler.setFormatter():# 给这个handler选择一个格式
Handler.addFilter(filt):# Handler.removeFilter(filt):新增或删除一个filter对象

需要说明的是,应用程序代码不应该直接实例化和使用Handler实例。因为Handler是一个基类,它只定义了素有handlers都应该有的接口,同时提供了一些子类可以直接使用或覆盖的默认行为。下面是一些常用的Handler:

Handler描述
logging.StreamHandler 将日志消息发送到输出到Stream,如std.out, std.err或任何file-like对象。
logging.FileHandler 将日志消息发送到磁盘文件,默认情况下文件大小会无限增长
logging.handlers.RotatingFileHandler 将日志消息发送到磁盘文件,并支持日志文件按大小切割
logging.hanlders.TimedRotatingFileHandler 将日志消息发送到磁盘文件,并支持日志文件按时间切割
logging.handlers.HTTPHandler 将日志消息以GET或POST的方式发送给一个HTTP服务器
logging.handlers.SMTPHandler 将日志消息发送给一个指定的email地址
logging.NullHandler 该Handler实例会忽略error messages,通常被想使用logging的library开发者使用来避免’No handlers could be found for logger XXX’信息的出现。
(3 ) Formater类

Formater对象用于配置日志信息的最终顺序、结构和内容。与logging.Handler基类不同的是,应用代码可以直接实例化Formatter类。另外,如果你的应用程序需要一些特殊的处理行为,也可以实现一个Formatter的子类来完成。

Formatter类的构造方法定义如下:

1
    logging.Formatter.__init__(fmt=None, datefmt=None, style='%')

可见,该构造方法接收3个可选参数:

  • fmt:指定消息格式化字符串,如果不指定该参数则默认使用message的原始值
  • datefmt:指定日期格式字符串,如果不指定该参数则默认使用”%Y-%m-%d %H:%M:%S"
  • style:Python 3.2新增的参数,可取值为 ‘%’, ‘{‘和 ‘$',如果不指定该参数则默认使用’%’

一般直接用logging.Formatter(fmt, datefmt)

(4) Filter类

Filter可以被Handler和Logger用来做比level更细粒度的、更复杂的过滤功能。Filter是一个过滤器基类,它只允许某个logger层级下的日志事件通过过滤。该类定义如下:

1
2
  class logging.Filter(name='')
      filter(record)

比如,一个filter实例化时传递的name参数值为’A.B’,那么该filter实例将只允许名称为类似如下规则的loggers产生的日志记录通过过滤:‘A.B’,‘A.B,C’,‘A.B.C.D’,‘A.B.D’,而名称为’A.BB', ‘B.A.B’的loggers产生的日志则会被过滤掉。如果name的值为空字符串,则允许所有的日志事件通过过滤。

filter方法用于具体控制传递的record记录是否能通过过滤,如果该方法返回值为0表示不能通过过滤,返回值为非0表示可以通过过滤。

说明:

  1. 如果有需要,也可以在filter(record)方法内部改变该record,比如添加、删除或修改一些属性
  2. 我们还可以通过filter做一些统计工作,比如可以计算下被一个特殊的logger或handler所处理的record数量等。
(5) 日志流处理简要流程
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/*
1、创建一个logger
2、设置下logger的日志的等级
3、创建合适的Handler(FileHandler要有路径)
4、设置下每个Handler的日志等级
5、创建下日志的格式
6、向Handler中添加上面创建的格式
7、将上面创建的Handler添加到logger中
8、打印输出logger.debug\logger.info\logger.warning\logger.error\logger.critical
*/
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import logging
def log():
    #创建logger,如果参数为空则返回root logger
    logger = logging.getLogger("nick")
    logger.setLevel(logging.DEBUG)  #设置logger日志等级

    #这里进行判断,如果logger.handlers列表为空,则添加,否则,多次调用函数会重复添加
    if not logger.handlers:
        #创建handler
        fh = logging.FileHandler("test.log",encoding="utf-8")
        ch = logging.StreamHandler()

        #设置输出日志格式
        formatter = logging.Formatter(
            fmt="%(asctime)s %(name)s %(filename)s %(message)s",
            datefmt="%Y/%m/%d %X"
            )

        #为handler指定输出格式
        fh.setFormatter(formatter)
        ch.setFormatter(formatter)

        #为logger添加的日志处理器
        logger.addHandler(fh)
        logger.addHandler(ch)

    return logger #直接返回logger

logger = log()
logger.warning("泰拳警告")
logger.info("提示")
logger.error("错误")
logger.debug("查错")

注意:因为logging模块是基于单例模式线程安全的,所以get_logger()如果名字参数相同则返回的是同一个对象,所以

添加handler的时候一定要判断,不要重复添加造成重复打印日志的bug!

9.1.6、os模块

os模块是与操作系统交互的一个接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import os

os.getcwd() # 获取当前工作目录,即当前python脚本工作的目录路径
os.chdir("dirname")  # 改变当前脚本工作目录;相当于shell下cd
os.curdir   # 返回当前目录: ('.')
os.pardir  # 获取当前目录的父目录字符串名:('..')
os.makedirs('dirname1/dirname2')    # 可生成多层递归目录
os.removedirs('dirname1')    # 若目录为空,则删除,并递归到上一级目录,如若也为空,则删除,依此类推
os.mkdir('dirname')    # 生成单级目录;相当于shell中mkdir dirname
os.rmdir('dirname')    # # 删除单级空目录,若目录不为空则无法删除,报错;相当于shell中rmdir dirname
os.listdir('dirname')    # 列出指定目录下的所有文件和子目录,包括隐藏文件,并以列表方式打印
os.remove()  # 删除一个文件
os.rename("oldname","newname")  # 重命名文件/目录
os.stat('path/filename') #  获取文件/目录信息
os.sep    # 输出操作系统特定的路径分隔符,win下为"\\",Linux下为"/"
os.linesep    # 输出当前平台使用的行终止符,win下为"\t\n",Linux下为"\n"
os.pathsep    # 输出用于分割文件路径的字符串 win下为;,Linux下为:
os.name    # 输出字符串指示当前使用平台。win->'nt'; Linux->'posix'
os.system("bash command")  # 运行shell命令,直接显示
os.environ  # 获取系统环境变量
os.path.abspath(path)  # 返回path规范化的绝对路径
os.path.split(path)  # 将path分割成目录和文件名二元组返回
os.path.dirname(path)  # 返回path的目录。其实就是os.path.split(path)的第一个元素
os.path.basename(path) #  返回path最后的文件名。如何path以/或\结尾,那么就会返回空值。即os.path.split(path)的第二个元素
os.path.exists(path) #  如果path存在,返回True;如果path不存在,返回False
os.path.isabs(path)  # 如果path是绝对路径,返回True
os.path.isfile(path)  # 如果path是一个存在的文件,返回True。否则返回False
os.path.isdir(path)  # 如果path是一个存在的目录,则返回True。否则返回False
os.path.join(path1[, path2[, ...]])  # 将多个路径组合后返回,第一个绝对路径之前的参数将被忽略
os.path.getatime(path)  # 返回path所指向的文件或者目录的最后访问时间
os.path.getmtime(path)  # 返回path所指向的文件或者目录的最后修改时间
os.path.getsize(path) # 返回path的大小

9.1.7、sys模块

sys是与解释器相关信息的模块

1
2
3
4
5
6
7
sys.argv           # 命令行参数List,第一个元素是程序本身路径
sys.exit(n)        # 退出程序,正常退出时exit(0)
sys.version        # 获取Python解释程序的版本信息
sys.maxint         # 最大的Int值
sys.path           # 返回模块的搜索路径,初始化时使用PYTHONPATH环境变量的值
sys.platform       # 返回操作系统平台名称 

9.1.8、序列化模块

序列化: 通过某种方式把数据结构或对象写入到磁盘文件中或通过网络传到其他节点的过程。

反序列化:把磁盘中对象或者把网络节点中传输的数据恢复为python的数据对象的过程。

序列化最重要的就是json序列化。

JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    +-------------------+---------------+
    | Python            | JSON          |
    +===================+===============+
    | dict              | object        |
    +-------------------+---------------+
    | list, tuple       | array         |
    +-------------------+---------------+
    | str               | string        |
    +-------------------+---------------+
    | int, float        | number        |
    +-------------------+---------------+
    | True              | true          |
    +-------------------+---------------+
    | False             | false         |
    +-------------------+---------------+
    | None              | null          |
    +-------------------+---------------+

image-20210527172544788

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import json
i=10
s='hello'
t=(1,4,6)
l=[3,5,7]
d={'name':"yuan"}

json_str1=json.dumps(i)
json_str2=json.dumps(s)
json_str3=json.dumps(t)
json_str4=json.dumps(l)
json_str5=json.dumps(d)

print(repr(json_str1))
print(repr(json_str2))
print(repr(json_str3))
print(repr(json_str4))
print(repr(json_str5))
  • 应用之磁盘读写
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import json

dic={'name':'yuan','age':23,'is_married':0}
data=json.dumps(dic)  # 序列化,将python的字典转换为json格式的字符串
print("type",type(data)) # <class 'str'>
with open('json.txt','w') as f:
    f.write(data)  # 等价于json.dump(dic,f)


with open('json.txt') as f:
    data =  f.read()
    dic = json.loads(data) # 反序列化成为python的字典,等价于data=json.load(f)
    print(type(dic))

# 思考: json.loads('{"name": "yuan", "age": 23, "is_married": 0}') 可以吗?
  • 应用之网络传输
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    <script>
        // 序列化
        data = {user:"yuan",pwd:123}
        console.log(JSON.stringify(data)) // '{"user":"yuan","pwd":123}'

        // 反序列化
        res_json = '{"name": "yuan", "age": 23, "is_married": 0}'
        let res = JSON.parse(res_json)
        console.log(res)
    </script>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import json

# 反序列化
data = '{"user":"yuan","pwd":123}'
data_dict = json.loads(data)
print(type(data_dict))

# 序列化
res = {'name':'yuan','age':23,'is_married':0}
res_json = json.dumps(res)  # 序列化,将python的字典转换为json格式的字符串

print(repr(res_json)) # '{"name": "yuan", "age": 23, "is_married": 0}'

9.1.9、正则模块

就其本质而言,正则表达式(或 RE)是一种小型的、高度专业化的编程语言,(在Python中)它内嵌在Python中,并通过 re 模块实现。正则表达式模式被编译成一系列的字节码,然后由用 C 编写的匹配引擎执行。

(1)元字符

. :除换行符以外的任意符号,re.S模式也可以使 . 匹配包括换行在内的所有字符

^:匹配字符串的开头

$:匹配字符串的末尾。

*:匹配0个或多个的表达式。默认贪婪模式

+:匹配1个或多个的表达式。默认贪婪模式

?:匹配0个或1个由前面的正则表达式,默认非贪婪模式

{ n,m}:匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式

[ ]:字符集,多个字符选其一,[^...]取反

|:匹配做正则表达式或右边正则表达式

( ):G匹配括号内的表达式,也表示一个组

\:转移符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import re

# (1) . ^ $
ret = re.findall("hello world","hello world")
print(ret)

ret = re.findall("^hello world$","hello python,hello world,hello re")
print(ret)

ret = re.findall("^hello .....$","hello world")
print(ret)

# (2) * + ?
ret = re.findall("^hello .*","hello ")
ret = re.findall("^hello .+","hello ")
ret = re.findall("^hello .?","hello abc")

# (3) {} ()
ret = re.findall("hello .{5}","hello python,hello world,hello re,hello yuan")
print(ret)
ret = re.findall("hello .{2,5}","hello python,hello world,hello re")
print(ret)
ret = re.findall("hello .{5},","hello python,hello world,hello re")
print(ret)
ret = re.findall("hello (.*?),","hello python,hello world,hello re,hello yuan,")
print(ret)
# ret = re.findall("hello (.*?)(?:,|$)","hello python,hello world,hello re,hello yuan")
# print(ret)

# (4) [] |
ret = re.findall("a[bcd]e","abeabaeacdeace")
print(ret)
ret = re.findall("[a-z]","123a45bcd678")
print(ret)
ret = re.findall("[^a-z]","123a45bcd678")
print(ret)
ret = re.findall("www\.([a-z]+)\.(?:com|cn)","www.baidu.com,www.jd.com")
print(ret)

# (5) \
'''
1、反斜杠后边跟元字符去除特殊功能,比如\.
2、反斜杠后边跟普通字符实现特殊功能,比如\d

    \d  匹配任何十进制数;      它相当于类 [0-9]。
    \D  匹配任何非数字字符;    它相当于类 [^0-9]。
    \s  匹配任何空白字符;      它相当于类 [ \t\n\r\f\v]。
    \S  匹配任何非空白字符;    它相当于类 [^ \t\n\r\f\v]。
    \w  匹配任何字母数字字符;   它相当于类 [a-zA-Z0-9_]。
    \W  匹配任何非字母数字字符; 它相当于类 [^a-zA-Z0-9_]
    \b  匹配一个特殊字符边界,比如空格 ,&,#等
'''

ret = re.findall("\d+","123a45bcd678")
print(ret)
ret = re.findall("(?:\d+)|(?:[a-z]+)","123a45bcd678")
print(ret)

(2)正则方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import re

# 查找所有符合条件的对象
# re.findall() # 返回列表
# 查找第一个符合条件的匹配对象
s = re.search("\d+","a45bcd678")
print(s)
print(s.group())

# match同search,不过只在字符串开始处进行匹配
s = re.match("\d+","a45bcd678")
# print(s)
# print(s.group())

# 正则分割split
ret = re.split('[ab]', 'abcd')
print(ret)
# 正则替换
def func(match):

    name = match.group()
    print("name",name)
    return "xxx"

# \1代指第一个组匹配的内容  \2第二个组匹配的内容,思考如何能将所有的名字转大写替换
ret = re.sub("(hello )(.*?)(,)","\\1yuan\\3","hello python,hello world,hello re,")
print("ccc",ret)

# 编译再执行
obj=re.compile('\d{3}')
ret=obj.search('abc123ee45ff')
print(ret.group()) # 123

练习:爬虫豆瓣网

1
2
3
4
5
6
com=re.compile(
    '<div class="item">.*?<div class="pic">.*?<em .*?>(?P<id>\d+).*?<span class="title">(?P<title>.*?)</span>'
    '.*?<span class="rating_num" .*?>(?P<rating_num>.*?)</span>.*?<span>(?P<comment_num>.*?)评价</span>',
    re.S)

com.findall(s)

9.2、模块

9.2.1、模块介绍

在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。

为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。而这样的一个py文件在Python中称为模块(Module)。

模块是组织代码的更高级形式,大大提高了代码的阅读性和可维护性。

模块一共四种:

  • 解释器内建模块
  • python标准库
  • 第三方模块
  • 应用程序自定义模块

另外,使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。

9.2.2、模块导入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
'''
# 方式1:导入一个模块
import 模块名
import 模块名 as 别名

# 方式2:导入多个模块
import 模块1,模块2

# 方式3:导入成员变量
from 模块名 import 成员变量
from 模块名 import *
'''
  1. 导入模块时会执行模块,多次导入只执行一次。
  2. 导入模块本质是:解释器依赖sys.path的路径进行查找,而需要格外注意的是python解释器运行某个程序时会将该程序的启动文件的目录加入到sysy.path中。

案例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 模块本质上就是一个具有某类功能函数的.py文件
# 查找模块的顺序: 1. 解释器自带  2. sys.path  # [执行程序所在目录路径,python标准库路径]
# 注意:自定义模块不要和标准库中的模块重名
import random  # python标准库的模块
import time  # 解释器自带的模块

import requests
# import cal     # 自定义模块
from cal import add as cal_add, mul  # 从cal模块中导入add,mul函数


# from cal import *  # # 从cal模块中导入所有变量


# 变量冲突问题

def add(x, y):
    print(":::add", x, y)


def main():

    num1 = random.randint(1, 100)
    num2 = random.randint(1, 100)
    print("num1和num2分别是:", num1, num2)

    ret1 = cal_add(num1, num2)
    # ret1 = cal.add(num1, num2)
    print(ret1)
    print(time.time())


main()

image-20211217192518714

9.2.3、__name__

__name__是python内置变量,存储的是当前模块名称。

对于很多编程语言来说,程序都必须要有一个入口。像C,C++都有一个main函数作为程序的入口,而Python作为解释性脚本语言,没有一个统一的入口,因为Python程序运行时是从模块顶行开始,逐行进行翻译执行,所以,最顶层(没有被缩进)的代码都会被执行,所以Python中并不需要一个统一的main()作为程序的入口。

在刚才的案例中2个模块都打印一次__name__

1
2
print("cal.py",__name__)
print("main.py",__name__)

结果为:

1
2
cal.py cal
main.py __main__

通过结果发现__name__只有在执行模块中打印__main__,在其他导入模块中打印各自模块的名称。

所以,__name__可以有以下作用:

  • 利用__name__=="__main__"声明程序入口。
  • 可以对导入的模块进行功能测试

9.3、包

9.3.1、什么是包

当一个项目中模块越来越多,维护和开发不是那么高效的时候,我们可以引入一种比模块更高级语法:包。

包是对相关功能的模块py文件的组织方式。

包可以理解为文件夹,更确切的说,是一个包含__init__文件的文件夹。

9.3.2、导入包语法

1
2
3
1. import 包名[.模块名 [as 别名]]
2. from 包名 import 模块名 [as 别名]
3. from 包名.模块名 import 成员名 [as 别名]

案例:将上面案例中的cal .py文件放到utils包中管理,logger.py放到logger包中管理。

1
2
3
4
5
6
-- demo
   main.py
   -- utils
      cal.py
   -- logger
      logger.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 方式1
from utils import cal
ret = cal.add(2,5)
print(ret)

# 方式2
import utils.cal
ret = utils.cal.add(4,6)
print(ret)

# 方式3
from utils.cal import add
ret = add(3,5)
print(ret)

如果将main.py放在一个main包下,运行会报错。根本原因是无论导包还是调用模块都是解释器依赖sys.path的路径进行查找,而python解释器运行某个程序时会将该程序的启动文件的目录加入到sysy.path中.所以启动文件的同级目录或者文件才可以调用。

所以这种目录结构下需要构建模块路径:

1
2
3
import sys,os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

思考:

1、如何在cal.py中导入同级目录模块?

2、导入模块的执行顺序?

9.3.3、__init__文件

__init__.py该文件的作用就是相当于把自身整个文件夹当作一个包来管理,每当有外部导入的时候会自动执行里面的代码。

主要功能:

  1. 标识该目录是一个python的模块包(module package)
  2. 简化模块导入操作
  3. 控制模块导入
  4. 偷懒的导入方法

__all__ 关联了一个模块列表,当执行 from xx import * 时,就会导入列表中的模块。

  1. 配置模块的初始化操作

在了解了__init__.py的工作原理后,应该能理解该文件就是一个正常的python代码文件,因此可以将初始化代码放入该文件中。

9.4、章节作业

将学生成绩管理系统改版为多目录结构

预备知识点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
x = {}
print("全局:", id(x))

def foo():

    # global x
    data = {"name": "yuan"}
    x = data
    # x.update(data)
    print("局部:", id(x))

foo()
print(x)

参考代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
│  bin.py
│  student_scores.json
│
├─conf
│  │  settings.py
│  │  __init__.py
│  │
│
├─core
│  │  main.py
│  │  scores_handler.py
│  │  __init__.py
│  │
│
└─db_handler
    │  serializer.py
    │  __init__.py
    │
    
1
2
3
4
5
6
# bin.py
from core import main

if __name__ == '__main__':

    main.main()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# core.main.py
import sys

from core.scores_handler import show_students, add_student, update_student, delete_student
from db_handler.serializer import save, init


def exit():
    sys.exit()

def main():
    # 初始化读取student_scores文件获取学生成绩字典students_dict
    init()

    while 1:
        print('''
           1.  查看所有学生成绩
           2.  添加一个学生成绩
           3.  修改一个学生成绩
           4.  删除一个学生成绩
           5.  保存
           6.  退出程序
        ''')
        choice = input("请输入您的选择:")

        # if-elif 版本
        # if choice == "1":
        #     show_students()
        #
        # elif choice == "2":
        #     add_student()
        #
        # elif choice == "3":
        #     update_student()
        #
        # elif choice == "4":
        #     delete_student()
        #
        # elif choice == "5":
        #     save()
        #
        # elif choice == "6":
        #     break
        # else:
        #     print("输入有误!")

        # 基于字典的switch版本
        choice_func = {"1": show_students, "2": add_student, "3": update_student, "4": delete_student, "5": save,
                       "6": exit}
        func = choice_func.get(choice)
        if func:
            func()
        else:
            print("输入有误!")

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
# core.scores_handler
students_dict = {}

print("在stu_scores_handler的students_dict", id(students_dict))

def show_students():
    '''
    查看所有学生信息
    '''
    print("students_dict", students_dict)
    print("*"*60)

    for sid, stu_dic in students_dict.items():
        # print(sid,stu_dic)
        name = stu_dic.get("name")
        chinese = stu_dic.get("scores").get("chinese")
        math = stu_dic.get("scores").get("math")
        english = stu_dic.get("scores").get("english")

        print("学号:%4s 姓名:%4s 语文成绩:%4s 数学成绩%4s 英文成绩:%4s" % (sid, name, chinese, math, english))

    print("*"*60)


def add_student():
    '''
      添加一个学生和对应成绩
    '''
    while 1:
        sid = input("请输入学生学号>>>")
        # 判断该学号是否存在
        if sid in students_dict:  # 该学号已经存在!
            print("该学号已经存在!")
        else:  # # 该学号不存在!
            break

    name = input("请输入学生姓名>>>")
    chinese_score = input("请输入学生语文成绩>>>")
    math_score = input("请输入学生数学成绩>>>")
    english_score = input("请输入学生英语成绩>>>")

    # 构建学生字典
    scores_dict = {
        "chinese": chinese_score,
        "math": math_score,
        "english": english_score,
    }
    stu_dic = {
        "name": name,
        "scores": scores_dict
    }
    print("stu_dic", stu_dic)
    students_dict[sid] = stu_dic
    print("students_dict", students_dict)


def update_student():
    '''
      更新一个学生成绩
    '''
    while 1:
        sid = input("请输入学生学号>>>")
        # 判断该学号是否存在
        if sid in students_dict:  # 该学号已经存在!
            break
        else:  # # 该学号不存在!
            print("该修改学号不存在!")

    chinese_score = input("请输入学生语文成绩>>>")
    math_score = input("请输入学生数学成绩>>>")
    english_score = input("请输入学生英语成绩>>>")

    # 修改学生成绩
    scores_dict = {
        "chinese": chinese_score,
        "math": math_score,
        "english": english_score,
    }

    students_dict.get(sid).update({"scores": scores_dict})
    print("修改成功")
    print("students_dict", students_dict)


def delete_student():
    '''
      删除一个学生和对应成绩
    '''
    while 1:
        sid = input("请输入学生学号>>>")
        # 判断该学号是否存在
        if sid in students_dict:  # 该学号已经存在!
            break
        else:  # # 该学号不存在!
            print("该修改学号不存在!")

    students_dict.pop(sid)
    print("删除成功")
    print("students_dict", students_dict)

 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
#  db_handler.serializer
import json
from core.scores_handler import students_dict
from conf.settings import student_scores_path
print("在serilizer中从core.scores_handler引入的students_dict", id(students_dict))

def init():
    #  打开student_scores.json文件,读取json数据
    try:
        with open(student_scores_path, "r") as f:
            student_scores_json = f.read()
        # 反序列化
        # students_dict = json.loads(student_scores_json) # 思考为什么不能这样写?
        students_dict.update(json.loads(student_scores_json))

    except FileNotFoundError:
        print("这是第一次初始化...")

def save():
    # 生成一个students_scores.json
    # print(":::2", id(students_dict))
    file = open(student_scores_path, "w")
    students_json = json.dumps(students_dict)
    file.write(students_json)
    file.close()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# conf.settings
import os

import sys

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

student_scores_path = os.path.join(BASE_DIR, "student_scores.json")
posted @ 2022-04-02 23:13  呼长喜  阅读(27)  评论(0编辑  收藏  举报