本章目录如下:
五、日志记录
通过Python的标准库logging模块定制自己多样化的记录日志需求
5.1、日志模块简介
日志模块的第一个例子:简单将日志打印到屏幕:
import logging
logging.debug('debug message')
logging.info('info message')
logging.warning('warning message')
logging.error('error message')
logging.critical('critical message')
默认只显示大于等于WARNING级别的日志,日志级别等级:CRITICAL>ERROR>WARNING>INFO>DEBUG。默认的日志格式:日志级别为Logger,名称为用户输出消息。
各日志级别代表的含义:
- DEBUG:调试时的信息打印
- INFO:正常的日志信息记录
- WARNING:发生了警告信息,但程序仍能正常工作
- ERROR:发生了错误,部分功能已不正常
-
CRITICAL:发生严重错误,程序可能已崩溃
第二个例子:将日志信息记录至文件:
import logging
logging.basicConfig(filename='./lx_log1.log')
logging.debug('debug message')
logging.info('info message')
logging.warning('warning message')
logging.error('error message')
logging.critical('critical message')
# 默认的写log文件的方式是追加
5.2、logging模块的配置与使用
5.2.1、logging模块
通过logging模块的配置改变log文件的写入方式、日志级别、时间戳等信息。例:
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
# 日志的格式
datefmt="%Y-%m-%d %H:%M:S", # 时间格式
filename='./lx_log1.log', # 指定文件位置
filemode='w') # 指定写入方式
可在logging.basicConfig()函数中通过具体参数来更改logging模块的默认行为:
参数 | 行为 |
---|---|
filename | 用指定的文件名创建FileHandler,这样日志会被存储在指定的文件中 |
filemode | 文件打开方式,在指定了filename时用这个参数,默认为a(追加) |
format | 指定handler使用的日志显示格式 |
datefmt | 指定日期时间格式 |
level | 设置rootlogger的日志级别 |
stream | 用指定的stream创建StreamHandler。 |
format参数中可能用到的格式化串:
参数 | 功能 |
---|---|
%(name)s | Logger的名字 |
%(levelno)s | 数字形式的日志级别 |
%(levelname)s | 文本形式的日志级别 |
%(pathname)s | 调用日志输出函数的模块的完整路径名,可能没有 |
%(filename)s | 调用日志输出函数的模块的文件名 |
%(module)s | 调用日志输出函数的模块名 |
%(funcName)s | 调用日志输出函数的函数名 |
%(lineno)d | 调用日志输出函数的语句所在代码行 |
%(created)f | 当前时间,用UNIX标准表示时间的浮点数 |
%(relativeCreated)d | 输出日志信息时,自Logger创建以来的毫秒数 |
%(asctime)s | 字符串形式的当期时间 |
%(thread)d | 线程ID,可能没有 |
%(threadName)s | 线程名,可能没有 |
%(process)d | 进程ID,可能没有 |
%(message)s | 用户输出的消息 |
实例:
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
# 日志的格式
datefmt="%Y-%m-%d %H:%M:S", # 时间格式
filename='./lx_log1.log', # 指定文件位置
filemode='w') # 指定写入方式
logging.debug('debug message')
logging.info('info message')
logging.warning('warning message')
logging.error('error message')
logging.critical('critical message')
5.2.2、四个器
- logger:记录器,应用程序代码能直接使用的接口
- handler:处理器,将(记录器产生的)日志记录发送至合适的目的地
- filter:过滤器,提供了更好的粒度控制,可以决定输出哪些日志记录
- formatter:格式化器,指明了最终输出中日志记录的布局
日志时间信息在记录器、处理器、过滤器、格式化器之间通过一个日志记录实例来传递。通过调用记录器实例的方法来记录日志,每一个记录器实例都有一个名字,名字相当于命名空间,是一个树状结构。
例如,一个记录器叫scan,记录器scan,tex、scan.html、scan.pdf的父节点。比较好的是:
logger = logging.getLogger(__name__)
logger和handler的工作流程:
实例:将日志信息显示在终端的同时也在文件中记录:
import logging
def my_logger(log_file):
# 实例化记录器,__name__:获取当前文件的名称,名称为任意,也可为空
logger = logging.getLogger(__name__)
print(logger.name) # 打印logger的名称
logger.setLevel(logging.INFO) # 设置logger的日志级别为DEBUG,以便记录所有级别的日志
# 创建两个处理器,一个负责将日志输出到终端,一个负责输出到文件,并分别设置它们的日志级别
ch = logging.StreamHandler() # 终端(控制台)
ch.setLevel(logging.DEBUG)
fh = logging.FileHandler(filename=log_file,mode="a",encoding="utf-8") # 文件
fh.setLevel(logging.WARNING)
# 创建一个格式化器,可以创建不同的格式化器用于不同的handler,这里我们使用一个
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
# 设置两个handler的格式化器
ch.setFormatter(formatter)
fh.setFormatter(formatter)
# 为logger添加两个handler
logger.addHandler(ch)
logger.addHandler(fh)
# 在程序中记录日志
if __name__ == '__main__':
logger = my_logger("test1.log")
logger.debug("这是一个debug日志")
logger.info("这是一个info日志")
logger.warning("这是一个warning日志")
logger.error("这是一个error日志")
handler的日志级别以logger的日志级别为基础,logger的日志级别为INFO,低于INFO级别的(如DEBUG)不会出现在handler中。handler中的日志级别高于logger,则只显示更高级别的日志信息。如低于或等于logger的日志级别,则显示logger的日志级别及以上信息。
除了StreamHandler(终端处理器)和FileHandler(文件处理器)外,还有其他Handler子类(了解):
Handler子类 | 说明 |
---|---|
BaseRotatingHandler | 循环日志处理器的基类,不能被直接实例化,可使用RotatingFileHandler和TimeRotatingFileHandler |
RotatingFileHandler | 将日志文件记录至磁盘文件,可以设置每个日志文件的最大占用空间 |
TimeRotatingFileHandler | 将日志文件记录至磁盘文件,按固定的时间间隔来循环记录日志 |
SocketHandler | 可以将日志信息发送到TCP/IP套接字 |
DatagramHandler | 可以将日志信息发送到UDP套接字 |
SMTPHandler | 可以将日志文件发送至邮箱 |
SysLogHandler | 系统日志记录器,可以将日志文件发送至UNIX系统日志,也可以是一个远程机器 |
NTEventLogHandler | Windows系统事件日志处理器,可以将日志文件发送到Windows系统事件日志 |
MemoryHandler | 实例向内存中的缓冲区发送消息,只要满足特定条件,缓冲区就会被刷新 |
HTTPHandler | 使用GET或POST方法向HTTP服务器发送消息 |
WatchedFileandler | WatchedFileandler实例监视它们登录到的文件,如果文件发生更改,则使用文件名关闭并重新打开,只适用unix系统 |
QueueHandler | QueueHandler实例向队列发送消息,比如在队列或多处理模块中实现的消息 |
NullHandler | NullHandler实例不适用错误信息,库开发人员使用日志记录,但希望避免在库用户未配置日志记录时显示“日志记录器XXX无法找到任何处理程序”信息 |
日志的配置信息也可以源于配置文件:示例:
import logging
import logging.config
logging.config.fileConfig('logging.conf')
# 创建一个logger
logger = logging.getLogger('simpleExample')
# 日志记录
logger.debug("debug message")
logger.info("info message")
logger.warning('warning message')
logger.error('error message')
logger.critical('critical message')
下面是配置文件信息logging.conf:
[loggers]
keys=root,simpleExample
[handlers]
keys=consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler
[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatters=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format = %(asctime)s -%(name)s -%(levelname)s - %(message)s
datefmt=%Y-%m-%d %H:%M:%S
六、搭建FTP服务器与客户端
6.1、FTP服务器模式
FTP(File Transfer Protocal,文件传输协议)运行在TCP协议上,使用两个端口,即数据端口和命令端口,也称控制端口。默认20是数据端口,21是命令端口
FTP有两种传输模式:主动模式和被动模式
6.1.1、主动模式
客户端首先从任意的非特殊端口n(大于1023的端口,也就是客户端的命令端口)连接FTP服务的命令端口(21),向服务器发出命令PORT n+1,告诉服务器使用n+1端口作为数据端口进行数据传输,然后在n+1端口监听。
服务器收到PORT n+1后向客户端返回一个“ACK”,然后服务器从它自己的数据端口(20)到客户端先前指定的数据端口(n+1)端口的连接,最后客户端向服务器返回一个‘ACK’,过程结束,如图:
6.1.2、被动模式
服务端发起到客户端的连接,为被动模式,又称PASV,当客户端通知服务器处于被动模式才启用。命令连接和数据连接都是由客户端发起。当开启一个FTP连接时,客户端打开任意两个的非特权本地端口(大于1023)。
第一个端口连接服务器的21端口,但与主动方式的FTP不同,客户端不会提交PORT命令并允许服务器来回连接数据端口,而是提交PASV命令。这会使服务器开启一个任意的非特权端口,并发送PORT P命令给客户端,然后客户端发起从本地端口N+1到服务器的端口P的连接用来传送数据,如图:
6.2、搭建服务器
Python使用pyftpdlib模块进行搭建
pip install pyftpdlib
6.2.1、快速搭建一个简单的FTP服务器
python -m pyftpdlib -p 21
执行该命令所在的目录下建立一个端口为21的供下载文件的FTP服务器(Linux系统需要root用户才能使用21端口,windows系统可能是乱码,解决方法:
# 首先找到pyftpdlib来源:
import pyftpdlib
print(pyftpdlib.__path__)
# 打开filesystems.py文件,找到
yield line.encode('utf-8',self.cmd_channel.unicode_errors)
# 将utf-8修改为gbk
# 打开handlers.py,找到:
return bytes.decode('utf-8',self.unicode_errors)
# 同样修改为 gbk
6.2.2、搭建一个具有访问权限。可配置相关信息的FTP服务器
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler,ThrottledDTPHandler
from pyftpdlib.servers import FTPServer
from pyftpdlib.log import LogFormatter
import logging
# 记录日志,默认情况下日志仅输出到屏幕(终端),这里既输出到屏幕又输出到文件,方便日志查看
logger = logging.getLogger()
logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
fh = logging.FileHandler(filename='myftpserver.log',encoding='utf-8')
ch.setFormatter(LogFormatter())
fh.setFormatter(LogFormatter())
logger.addHandler(ch)
logger.addHandler(fh)
# 实例化虚拟用户,这是FTP验证的首要条件
authorizer = DummyAuthorizer()
# 添加用户权限和路径,括号内的参数是(用户名、密码、用户目录、权限),可以为不同的用户添加不同的目录和权限
authorizer.add_user("user","12345","d:/",perm="elradfmw")
# 添加匿名用户,只需要路径
authorizer.add_anonymous("d:/")
# 初始化ftp句柄
handler = FTPHandler
handler.authorizer = authorizer
# 添加被动端口范围
handler.passive_ports = range(2000,2333)
# 下载上传速度设置
dtp_handler = ThrottledDTPHandler
dtp_handler.read_limit = 300 * 1024 # 300kb/s
dtp_handler.write_limit = 300 * 1024 # 300kb/s
handler.dtp_handler = dtp_handler
# 监听ip和端口,linux里需要root用户才能使用21端口
server = FTPServer(("0.0.0.0",21),handler)
# 最大连接数
server.max_cons = 150
server.max_cons_per_ip = 15
# 开启服务,自带日志打印信息
server.serve_forever()
该目录下会生成一个myftpserver.log日志文件,windows进入cmd输入ftp [主机IP]登录该FTP服务器并列出目录(dir命令)进行测试。成功!!说明搭建完成,可自行修改用户权限:
读权限 | 说明 | 写权限 | 说明 |
---|---|---|---|
e | 改变文件目录 | a | 文件上传 |
l | 列出文件 | d | 删除文件 |
r | 从服务器接收文件 | f | 文件重命名 |
m | 创建文件 | ||
w | 写权限 | ||
M | 文件传输方式 |
6.3、编写FTP客户端程序
from ftplib import FTP
# 登录FTP
ftp = FTP(host='localhost',user='user',passwd='12345')
# 设置编码方式,由于在windows系统上,设置编码为gbk
ftp.encoding='gbk'
# 切换目录
ftp.cwd('test')
# 列出文件夹的内容
ftp.retrlines('LIST') # ftp.dir()
# 下载文件 note.txt
ftp.retrbinary('RETR note.txt',open('note.txt','wb').write)
# 上传文件 server.py
ftp.storbinary('STOR server.py',open('server.py','rb'))
# 查看目录下的文件详情
for f in ftp.mlsd(path='/test'):
print(f)
七、邮件提醒
7.1、发送邮件
目前发送邮件的协议是SMTP(Simple Mail Transfer Protocal,简单邮件传输协议),是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。
python内置的smtplib提供了一种很方便的途径发送电子邮件,可以发送纯文本邮件。HTML邮件及带附件的邮件。Python对SMTP支持有smtplib和email两个模块,email负责构造邮件,smtplib负责发送邮件
Python创建SMTP对象语法:
import smtplib
smtpObj = smtplib.SMTP([host[,port[,local_hostname]]])
# 参数说明
host:SMTP服务器主机,可以指定主机的IP地址或域名,是可选参数
port:如果提供了host参数,就需要指定SMTP服务器使用的端口号,一般为25
local_hostname:如果SMTP在本主机上,那么只需要指定服务器地址为localhost即可
Python SMTP对象使用sendmail方法发送邮件,其语法:
SMTP.sendmail(from_addr,to_addrs,msg[,mail_options,rcpt_options])
# 参数说明
from_addr:邮件发送者地址
to_addrs:字符串列表,邮件发送地址
msg:发送消息
构造简单的文本邮件:
使用SMTP发送邮件之前,确保所用邮箱的SMTP服务已开启:例如QQ邮箱:
from email.mime.text import MIMEText
message = MIMEText('Python 邮件发送测试...','plain','utf-8')
# 第一个参数是邮箱正文
# 第二个参数是MIME的subtype,传入plain,最终的MIME就是'text/plain'
# 最后一定要用UTF-8编码保证多语言兼容性
7.1.1、一封简单的邮件
示例:发送第一封简单的邮件(可以自行将重要信息放置在配置文件进行读取):
import smtplib
from email.mime.text import MIMEText
# 第三方SMTP服务
mail_host = 'smtp.qq.com' # 设置服务器
mail_user = '你的邮箱用户名' # 用户名
mail_pass = '你的邮箱口令密码' # 口令
# 发件人
sender = '[email protected]'
# 接收者列表
receivers = ["[email protected]","[email protected]"]
# 构造正文
message = MIMEText("这是正文:邮件正文...","plain","utf-8")
message["From"] = sender # 发件人。必须构造
message["To"] = ";".join(receivers) # 收件人列表,不是必须的
message["Subject"] = "这是主题:SMTP邮件测试"
try:
smtpObj = smtplib.SMTP()
smtpObj.connect(mail_host,587) # 587为QQ邮箱发送的端口号,163邮箱为25
smtpObj.login(mail_user,mail_pass)
smtpObj.sendmail(sender,receivers,message.as_string())
print("发送成功!!")
except smtplib.SMTPException as e:
print(f"发送失败,错误原因:{e}")
HTML格式的邮件例子如下:
# 修改上述正文部分
message = MIMEText(
'<html><body><h1>这是正文标题</h1> \
<p>正文内容 <a href="#">超链接</a>...</p> \
</body></html>',
"plain",
"utf-8")
7.1.2、带图片及附件的邮件
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.header import Header
# 第三方SMTP服务
mail_host = 'smtp.qq.com' # 设置服务器
mail_user = '你的邮箱用户名' # 用户名
mail_pass = '你的邮箱口令密码' # 口令
# 发件人
sender = '[email protected]'
# 接收者
receivers = ["[email protected]",""]
message = MIMEMultipart()
message["From"] = sender # 发件人。必须构造
message["To"] = ";".join(receivers) # 收件人列表,不是必须的
message["Subject"] = "这是主题:SMTP邮件测试"
# 构造正文
message.attach(MIMEText(
'<p>这是正文:图片及附件发送测试</p><p>图片演示</p><p><img src="cid:image1"></p>',
"html",
"utf-8"))
# 指定图片为当前目录
fp = open("XXX.jpg","rb")
msgImage = MIMEImage(fp.read())
fp.close()
# 定义图片ID,在HTML文本中引用
msgImage.add_header("Content-ID","<image1>")
message.attach(msgImage)
# 添加附件1,当前目录下的simple.log
att1 = MIMEText(open("simple.log","rb").read(),"base64","utf-8")
att1["Content-Type"] = "application/octet-stream"
# 这里的filename可以随便写,写什么名字,邮件中就显示什么名字
att1["Content-Disposition"] = 'attachment;filename="simple.log"' # 法1
message.attach(att1)
# 添加附件2,当前目录下的test.txt
att2 = MIMEText(open("test.txt","rb").read(),"base64","utf-8")
att2["Content-Type"] = "application/octet-stream"
# 这里的filename可以随便写,写什么名字,邮件中就显示什么名字
att2.add_header("Content-Disposition","attachment",filename=("gbk","","测试.txt")) # 法2
message.attach(att2)
try:
smtpObj = smtplib.SMTP()
smtpObj.connect(mail_host,587)
smtpObj.login(mail_user,mail_pass)
smtpObj.sendmail(sender,receivers,message.as_string())
print("发送成功!!")
except smtplib.SMTPException as e:
print(f"发送失败,错误原因:{e}")
7.2、接收邮件
接收邮件的协议有POP3(Post Office Protocal)和IMAP(Internet Message Access Protocal),Python内置poplib模块实现了POP3协议。
收取邮件分以下两步:
- 用poplib模块把邮件的原始文本下载到本地
-
用email模块解析原始文本,还原为邮件对象
示例:
import poplib
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
# 输入邮件地址、口令和POP3服务器地址
email = "邮件地址"
password = "口令"
pop3_server = "pop.qq.com"
# 连接到POP3服务器,如果开启ss1,就使用poplib.POP3_SSL
server = poplib.POP3_SSL(pop3_server)
# 可以打开或关闭调试信息
server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字
print(server.getwelcome().decode("utf-8"))
# 身份认证
server.user(email)
server.pass_(password)
# stat()返回邮件数量和占用空间
print("邮件数量:%s个。大小:%.2fMB" % (server.stat()[0],server.stat()[1] / 1024 / 1024))
# list()返回所有邮件的编号
resp, mails, octets = server.list()
# 可以查看返回的列表,类似[b'1 8293',b'2 2184',...]
# 获取最新一封邮件,注意索引号从1开始,最新的邮件索引即为邮件的总个数
index = len(mails)
resp, lines, octets = server.retr(index)
# lines存储了邮件的原始文本的每一行可以获得整个邮件的原始文本
msg_content = b"\r\n".join(lines).decode("utf-8")
# 稍后解析出邮件
msg = Parser().parsestr(msg_content)
def decode_str(s):
value,charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
def print_address(address_header):
name, addr = parseaddr(decode_str(address_header))
print(f"{name} <{addr}>")
print("解析获取到的邮件内容如下:\n-------------------begin-----------------")
# 打印发件人信息
print_address(msg.get('From', ''))
# 打印收件人信息
print_address(msg.get('To', ''))
# 打印主题信息
print(decode_str(msg["Subject"]))
# 打印第一条正文信息
if msg.is_multipart():
part0 = msg.get_payload(0)
content = part0.get_payload(decode=True)
print(content.decode(part0.get_content_charset()) if part0.get_content_charset() else content.decode())
else:
print(msg.get_payload(decode=True).decode(msg.get_content_charset()) if msg.get_content_charset() else msg.get_payload(decode=True))
print("------------------end------------------")
server.quit()
7.3、实例:将报警信息实时发送至邮箱
相比短信报警,邮件报警是一个非常低成本的解决办法。
报警信息实时发送至邮箱,需求说明如下:
-
文本文件txt约定格式:第一行为收件人,以逗号分隔;第二行为主题,第三行至最后一行为正文内容,最后一行若是文件,则以附件发送,支持多个附件,以逗号分隔:例如
[email protected],[email protected] xxx程序报警 ... /home/log/xxx.log,/tmp/yyy/log
-
持续监控目录A下的txt文件,如果有新增或修改,则读取文本中的内容并发送邮件
-
有报警需求的程序可生成第一点中格式的文本文件并传送至目录A即可
涉及Python知识点:文件编码、读文件操作、watchdog模块应用及邮件发送
相关代码已上传资源中