python随笔

python随笔

typeobject的区别

1 object本身, 任何类都继承object (object是继承关系顶点)
特殊: B类继承A, A类继承object,所以B还是间接继承了object
2 任何类都是由type实例化而来, 包括type本身 (type是类型关系顶点)
特殊: 元类的衍生类, 例如下面的B类是由A类实例化而来, A类还是由type实例化而来
class A(type):
    pass

class B(object, metaclass=A):
    pass

type的作用: 可以创建元类, 定制类的行为,比如重写__new__或者__init__等方法

object的作用: 所有类都会继承它

 

type, object, 实例对象三者之间的关系图如下:

 

 

 

__bases__, __subclasses__(), __class__

__bases__ 查看一个类型的父类

__subclasses__() 查看一个类型的子类

__class__ 查看一个对象是由什么类实例化而来

:

class Cls1:
    pass

class Cls2(Cls1):
    pass

obj1 = Cls1()
print(Cls1.__bases__)
print(Cls1.__subclasses__())
print(obj1.__class__)

"""打印
(<class 'object'>,)
[<class '__main__.Cls2'>]
<class '__main__.Cls1'>
"""

 

 

自定义元类

# 自己创造的类,可以无任何属性,后续自己需要可以添加

# obj1 = type("test1", (object, ), {"name": "chen", "age": "25"})

# type(类名, 由父类名称组成的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))
# 第一个参数: 类的名称是test1,当然可以用现有的类; 第二个参数: 继承的父类-元组,例如(object, ); 第三个参数: 类的成员熟悉/方法的字典

obj1 = type("", (), {})  # 第三个参数可以直接给个空字典,后续需要属性自己加
# obj1 test1 类实例化的对象,但是test1不能用,会显示未定义,这里test1也可以写空字符串
print(obj1)
setattr(obj1, "sex", "")  # setattr并不是这样用的 obj1.setattr("sex", "")
print(obj1.sex)

 

重写__new__方法, 自定义实例化对象

class A(type):
    pass

class B(A):
    def __new__(cls, *args, **kwargs):
        # 其中super(B, cls)就是B的父类, A, 可简写为super(), 当然也可以直接写A, 不过写A没有那么好的通用性
        return super(B, cls).__new__(cls, *args, **kwargs)

        # 第二种写法

        # return A.__new__(self, cls, *args, **kwargs)

# 如果想要创建自己类的实例化, return的一定是父类的__new__方法创造的实例对象(__new__方法是用来创建对象的)

 

魔鬼般的继承

例一:

class C1:
    def f1(self):
        self.f2()
        print("f1")

class C2:
    def f2(self):
        print("f2")

class C3(C1, C2):
    a3 = "a3"

obj3 = C3()
obj3.f1()

# 这样调用不会报错.因为 C3继承了C1C2, 就相当于把C1, C2里面的所有方法和属性写到了自己的类里, 所以这样调用不会报错,但不能直接用C1的类对象调用f1方法

# : 子类继承父类, 子类的对象能够调用所有父类所有方法和属性,而这里自动传参的self也就是子类的对象obj3,所有self.f2()能够正常调用.

 

例二:

class C1:
    def f1(self):
        handler = getattr(self, "f2")
        return handler()  # 就相当于 return self.f2()

class C2(C1):
    def f2(self):
        return "C2f2方法"

print(C2().f1())  # 打印: C2f2方法

 

python操作表格

参考: https://zhuanlan.zhihu.com/p/259583430

 

pandas写入表格

sheet_data1 = pd.DataFrame({'a': [1, 3], 'b': [2, 4]})  # 按列写入

sheet_data2 = pd.DataFrame([[11, 22], [33, 44]], columns=["C", "D"])  # 按行写入
all_data = {'工作表1': sheet_data1, '工作表2': sheet_data2}
with pd.ExcelWriter('D:\\table_test\\output.xlsx') as writer:
    for sheet_name, sheet_data in all_data.items():
        sheet_data.to_excel(writer, sheet_name=str(sheet_name), index=False)  # index=False不加首列序号
writer.save()
writer.close()

 

xlrd/xlwt 读取/写入表格

import xlrd

import xlwt

 

def read_excel(file_name):
    """
    :param file_name: 要读取的表格文件名
    """
    data = xlrd.open_workbook(file_name)
    sheet_data = data.sheet_by_index(0)
    for row in range(sheet_data.nrows):
        for column in range(sheet_data.ncols):
            cell_data = sheet_data.cell_value(row, column)
            print("---cell_data---", cell_data)

 

def write_excel(data_list):
    """
    :param data_list: 要写入的表格数据, 例如[[1, 2, 3], [5, 6, 7], ...]
    """
    wb = xlwt.Workbook()
    ws = wb.add_sheet("sheet1", cell_overwrite_ok=True)
    for i in range(len(data_list)):
        for j in range(len(data_list[i])):
            ws.write(i, j, data_list[i][j])
    wb.save('写入结果.xlsx')

 

 

issubclass

issubclass(A, B)  # 判断A类是否是B类的子类

 

join拼接

“拼接符”.join(可迭代字符串)

# 其中可迭代字符串代表内容是字符串的可迭代对象, 例如:

".".join("123")
".".join(["1", "2", "3"])

# 两者都返回1.2.3

 

*解包

1

first, *inner, last = 0, 1, 2, 3

“””

first = 0

inner = [1, 2]

last = 3

”””

 

2 嵌套解包

(a, b), c = (1, 2), 3

 

 

多条件判断anyall

any()  # 传参iterable, 至少有一个满足条件即返回True, 否则返回False

all()  # 传参iterable, 全部满足条件返回True, 否则返回False

例如:

a = [1, 2, 3]

print(any(_ <= 1 for _ in a))  # True, _ <= 1 for _ in a 这个结构是一个generator也就是生成器

print(all(_ == 2 for _ in a))  # False

b = [1, 0, False]

print(any(b))  # True

print(all(b))  # False

 

python解决常见乱码

乱码一:python不能将汉字的bytes直接输出汉字,需要转换成Unicode,然后用print输出(类似于\xc7\xeb\xca 这种)

import chardet
str1 = b'\xe7\xbb\x9d\xe5\x9c\xb0\xe6\xb1\x82\xe7\x94\x9f'
response = chardet.detect(str1)
print(response, type(response))
print(str1.decode(response["encoding"]))

""" 打印结果:
{'encoding': 'utf-8', 'confidence': 0.938125, 'language': ''} <class 'dict'>
绝地求生
"""

 

 

乱码二:

import urllib.parse

str2 = '%E6%9D%8E%E9%9B%B7'

res = urllib.parse.unquote(str2)

print(res)

 

乱码三:

# unicode编码

str3 = '\u53c2\u6570\u9519\u8bef\uff0c\u8d85\u51fa\u8303\u56f4'
print(str3.encode("utf-8").decode("utf-8"))

 

 

生成器yield

yieldreturn类似, next调用会记录返回的位置, 下次再调用next会从上次yield之后接着执行.

sendyield类似, send()会把传入的参数填充到yield后面, 调用next会返回send传入的值, 需要注意第一次send只能传None, 或者直接调用next(func1()), 其中func1()就是生成器

 

参数化装饰器

就是装饰器后自带括号, 并且可以传参

需要用到三层函数

 

 

使用:

@repeat(2)

def func():

pass

 

缓存装饰器

在调用时把需要的参数, 对应输出的值存放在一起, 并在后续传相同参数的调用中直接获取这个缓存的值并返回它. 这种行为被称为memoizing. 此装饰器在函数输入的值有限时可以使用;

import time
import hashlib
import pickle

cache = {}


def is_obsolete(entry, duration):
    return time.time() - entry['time'] > duration


def compute_key(function, args, kw):
    key = pickle.dumps((function.__name__, args, kw))
    return hashlib.sha1(key).hexdigest()


def memoize(duration=10):
    def _memoize(function):
        def __memoize(*args, **kwargs):
            key = compute_key(function, args, kwargs)

            # 是否已经拥有它了?
            if key in cache and not is_obsolete(cache[key], duration):
                print('we got cache')
                return cache[key]['value']
            # 计算

            result = function(*args, **kwargs)  # 保存结果
            cache[key] = {
                'value': result,
                'time': time.time()
            }
            return result
        return __memoize
    return _memoize


@memoize(duration=2)
def complex_func(a, b):
    return a + b


res = complex_func(1, 2)
print("---result---", res)
res = complex_func(1, 2)
print("---result---", res)
time.sleep(2)
res = complex_func(1, 2)
print("---result---", res)
"""
# 打印
---result--- 3
we got cache
---result--- 3
---result--- 3
"""

 

参数检查装饰器

rpc_info = {}


def xmlrpc(in_=(), out=(type(None),)):
    def _xmlrpc(function):
        # 注册签名
        func_name = function.__name__
        rpc_info[func_name] = (in_, out)

        def _check_types(elements, types, in_or_out="in"):
            """用来检查类型的子函数。"""
            if len(elements) != len(types):
                raise TypeError('The %sput argument count is wrong' % in_or_out)
            typed = enumerate(zip(elements, types))
            for index, couple in typed:
                arg, of_the_right_type = couple
                if isinstance(arg, of_the_right_type):
                    continue
                raise TypeError('The %sput argument #%d should be %s' % (in_or_out, index, of_the_right_type))

        # 包装过的函数
        def __xmlrpc(*args):  # 没有允许的关键词
            # 检查输入的内容
            checkable_args = args[1:]  # 去掉self
            _check_types(checkable_args, in_)
            # 运行函数
            res = function(*args)  # 检查输出的内容
            if not type(res) in (tuple, list):
                checkable_res = (res,)
            else:
                checkable_res = res
            _check_types(checkable_res, out, in_or_out="out")
            # 函数及其类型检查成功
            return res
        return __xmlrpc
    return _xmlrpc


class RPCView:
    @xmlrpc(in_=(int, int))  # two int -> None
    def meth1(self, int1, int2):
        print('received %d and %d' % (int1, int2))

    @xmlrpc(in_=(str,), out=(int,))  # string -> int
    def meth2(self, phrase):
        print('received "%s"' % phrase)
        return 12
        # return "2"


print(rpc_info)
RPCView().meth1(3, 3)
RPCView().meth2("2")
# RPCView().meth2(2)

 

代理

 

 

 

 

上下文管理器

with语法的一种实现, with语句块执行完毕或报错时执行既定命令, 例如关闭文件, 关闭连接, 释放锁等等

class ContextIllustration:
    def __enter__(self):
        print('进入上下文')

    def __exit__(self, exc_type, exc_value, traceback):
        print('离开上下文')
        if exc_type is None:
            print('with no error')
        else:
            print('with an error (%s)' % exc_value)


with ContextIllustration():
    raise RuntimeError("出错了")
    # print("执行完了")

 

上下文管理器的装饰器实现

import traceback


class ContextTest:
    def __init__(self, func):
        self.func = func

    def my_enter(self):
        print('进入上下文')

    def my_exit(self, exc_type, exc_value, traceback):
        print('离开上下文')
        if exc_type is None:
            print('with no error')
        else:
            print('with an error (%s)' % exc_value)

    def __call__(self, *args, **kwargs):
        self.my_enter()
        exc_type = None
        exc_value = None
        _traceback = None
        try:
            self.func(*args, **kwargs)
        except Exception as e:
            _traceback = traceback.format_exc()
            exc_type = True
            exc_value = e
            raise e
        finally:
            self.my_exit(exc_type, exc_value, _traceback)

@ContextTest
def func6():
    raise RuntimeError("出错了")
    # print("执行完了")

func6()

 

字符串自动填充0

str.zfill(width)  # width是补成几位,int

 

 

python获取一个变量的变量名(字符串)

"""
特殊情况:
如果param1也是2, 那么这两个变量指向同一内存地址, 系统无法识别这个变量名是什么, 返回的是指向这个地址的所有变量名也就是 ['param1', 'data1']
"""
param1 = 1
data1 = 2
def namestr(obj, namespace):
    var_name_list = [name for name in namespace.keys() if namespace[name] is obj]
    if len(var_name_list) == 1:
        return var_name_list[0]
    else:
        return var_name_list

print(namestr(data1, globals()))

 

python写表格合并单元格

https://github.com/1lo0/pexcel_openpyxl.git

 

 

python定义函数/类时附带参数类型及返回值类型

def f1(name: "str", age: "int") -> None:
    print(f"{name}'s age is {age}")

f1("Chen", 25)

 

getattr/setattr/delattr

getattr

tmp_obj = getattr(self, "func1")

# 等式右边等同于self.func1, tmp_obj = self.func1

# 相当于获取类对象的成员

 

setattr

setattr(self, "var1", 666)  # 等同于self.var1 = 666

# 相当于给类对象赋值(添加成员)

 

delattr

delattr(obj1, "name1")  # 等同于 del obj1.name1

# 相当于把给对象添加的成员再删掉. 注意不能删掉原本就写在类中的成员,只能删除实例化时__init__构造方法添加的成员  实例化后额外添加的成员(obj.xxx以及setattr)

 

getattr的查找顺序

 

由于D类先继承的B, 再继承的C, 所以调用D类实例化对象的某个成员时先从D类自己里面找这个成员, 如果没有就会按照1,2,3,4的顺序寻找父类有没有这个成员

 

super的用法

class Cls0:
    def func1(self):
        print("Cls0_func1")

    def func4(cls):
        print("Cls0_func4")


class Cls1:
    def func1(self):
        print("Cls1_func1")


class Cls2(Cls1, Cls0):
    def __new__(cls):
        """
        意为: 使用cls调用Cls2的父类中(只在父类中找)__new__方法并传参cls, 在这里就等同于Cls1.__new__(cls),
        当然,如果Cls1中没有就去Cls2中找,getattr一样的顺序去找, 只是类型本身被跳过。其意义就是不需要在类里面写父类的名称
        就可以调用父类的函数. 因此当子类改为继承其他父类的时候,不需要对子类内部的父类调用函数做任何修改就能调用新父类的方法
        """
        return super(Cls2, cls).__new__(cls)  # 简写: super().__new__(cls)

    def func1(self):
        print("Cls2_func1")

    def func2(self):
        """
        super(Cls2, self).func1() 简写 super().func1() 注意,func1方法(只在父类中找),先继承的是Cls1, 所以先去Cls1中找func1方法,找到就调用了
        """
        super(Cls2, self).func1()
        """ 等同于:
        if hasattr(Cls1, "func1"):
            tmp_func = getattr(Cls1, "func1")  # 等同于 tmp_func = Cls1.func1
        elif hasattr(Cls0, "func1"):
            tmp_func = getattr(Cls0, "func1")
        else:
            raise Exception("报错")
        tmp_func(self)
        """

    @classmethod
    def func3(cls):
        super(Cls2, cls).func4(cls)


Cls2().func2()
# Cls2().func1()
Cls2.func3()

 

 

重写父类方法

class Cl1:
    def func1(self):
        return self.func2()

    def func2(self):
        return "Cl1 f2"

class Cl2(Cl1):

    def func2(self):
        return "Cl2 f2"

print(Cl2().func1())  # 打印Cl2 f2

print(Cl1().func1())  # 打印Cl1 f2

# 可见Cl2重写了Cl1func2方法, 但是Cl2的实例化对象首先调用func1, Cl2中没有, 就找父类,找到func1执行, 调用func2, 这里虽然在Cl1中调用func2,但是selfCl2实例对象, 所以这里优先调用的是Cl2func2. 当然Cl1的实例对象调用的肯定是Cl1自己的func2

 

python获取作用域中的变量名

方法: 通过遍历locals()字典或globals()字典来通过值获取键, 但是有缺陷

def get_variable_name(loc, variable):
    for k, v in loc.items():
        if loc[k] == variable:
            return k


# var2 = 222  # 如果有相同变量值的情况下就不能获取到正确的变量名称
var1 = 222
ret = get_variable_name(locals(), var1)
print("---获取变量名结果---", ret, type(ret))

 

变量值的正则限制

def func3(field_restrict=None):
    field_restrict = {} if field_restrict is None else field_restrict
    # a1 = "124"
    a1 = "1234"
    print("---locals---", locals())
    for var_name, var_restrict in field_restrict.items():
        if var_name in locals():
            verify_data = locals().get(var_name)
            if not re.findall(var_restrict, verify_data):
                print(f"当前作用域变量{var_name}的值{verify_data}不符合限制要求{var_restrict}")
            else:
                print(f"当前作用域变量{var_name}的值{verify_data}符合限制要求{var_restrict}")


func3(field_restrict={"a1": "^123"})

 

datetime.time格式转化成秒

from datetime import datetime, date, time, timedelta
timeobj = time(12, 45)
t = datetime.combine(date.min, timeobj) - datetime.min
print(isinstance(t, timedelta))
# True
second = t.total_seconds()
# 45900.0

 

python实现导出表格(前端+后端)

https://www.bbsmax.com/A/lk5aby0qd1/

 

python导包问题

直接执行当前的python文件, 则在此python文件中不能使用带"."的相对路径导包, 会报错: ValueError: attempted relative import beyond top-level package

因为当前文件的名称为__main__, 在不作为主文件运行时(不为主文件允许,就是导入文件允许), 这个文件的名称才不为__main__, 而这个文件的名称是什么取决于主文件导入此导包文件时用的什么路径.

例如: 在主文件中使用 from import_test.f1.t1 import var1 导入的t1.py文件中的var1, 则在t1.py文件的最开始打印__name__, 会显示此文件的文件名为import_test.f1.t1 , 而我们在主文件中用from f1.t1 import var1导入var1, __name__又会打印文件名为f1.t1 , import_test.f1.t1f1.t1是有区别的, 区别就是前者可以在t1.py文件中使用 from ..xxx import xxx , 而后者最多只能用 from .xxx import xxx , 否则就会报出上面ValueError的经典错误, 从这个例子可以看出, 能不能在文件中使用带"."的相对路径导入, 取决于这个文件的__name__是什么, 若是__main__, 则一个"."都不能用, 即无法使用带"."的相对路径导入, 唯一可以使用的相对路径导入是from xxx import xxx , 默认可以从当前文件的同级文件中导包, 而需要使用多少个"."进行相对导入, 取决于__name__中有多少个"." ,

所以为了方便导包, 避免报错, 通常会将项目根目录加入到环境变量中(pycharm中飘黄飘红都可以不管, 只要允许不报错就行, 或者将根目录右键Mark Directory as -> Sources Root). 加入到环境变量的方法:

import os

import sys

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

sys.path.append(BASE_DIR)

# django不用加, 因为在定义BASE_DIR后源码自动将BASE_DIR加入到环境变量了.

加完之后导包方式变成了 from [根目录下的文件夹]/xxx/xxx import xxx , 所以在被导入的文件中可以使用多个"."进行导包都没问题, 只要"."没有超出根目录的范围

 

 

前后端分离/不分离如何防御CSRF攻击

参考: https://www.jianshu.com/p/7d6331b7fa29

0 csrf攻击机制

一个浏览器下, B网页访问A网页的url时会自动携带A网页下的cookie, 当没有在表单中添加csrf_token或在请求头中添加对应的验证字符串时, A网页对应的后端服务器无法区分是否是A网页发送过来的请求, B网页发送过来的是恶意请求(转账/修改密码/修改数据等)则就实现了CSRF攻击的目的

 

1 前后端不分离

使用CSRF Token, 通过在form中填充隐藏的csrf_token。这种方法适用于服务器端渲染的页面,对于前后端分离的情况就不太适用了。

具体使用:

django项目的前端表单: 在对应的html文件中需要点击提交的form表单中添加 {% csrf_token %} 标签即可

flask项目的前端表单: <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

前后端不分离的ajax请求:

首先要在对应的html文件中任意一个form表单中添加 csrf_token  标签, 然后在headers中手动添加csrf_token

headers: {"X-CSRFToken": $("input[name='csrfmiddlewaretoken']").attr("value")},

 

2 前后端分离(两种方式)

方法一:

JWT(Json Web Token)

后端的操作:

用户登录成功后返回一个JWT token给前端, 前端将其保存在local storage/session storage

前端的操作:

前端发送ajax请求前先从local storage/session storage中获取JWT token, JWT token放到headers中作为authorization的值, 传到后端进行校验

 

方法二:

Cookie-to-header token
浏览器索要登录页面的时候,服务器生成一个随机的csrf_token,放入cookie中。

浏览器通过JavaScript读取cookie中的Csrf_token,然后在发送请求时作为自定义HTTP头发送回来。服务器读取HTTP头中的Csrf_token,与cookie中的Csrf_token比较,一致则放行,否则拒绝。

这种方法为什么能够防御CSRF攻击呢?
关键在于JavaScript读取cookie中的Csrf_token这步。由于浏览器的同源策略,攻击者是无法从被攻击者的cookie中读取任何东西, 只能简单的携带cookie。所以,前端的请求在发出前先从cookie中获取csrf_token, csrf_token添加到headers中即可, 而黑客无法进行这一步操作, 简单来说就是虽然cookie中有csrf_token, 但是没法取出来放入headers, 所以无法成功发起CSRF攻击

而对于前后端不分离也使用ajax请求的情况下(不是通过表单提交csrf_token), 可以通过DOM操作获取到表单里input框中的csrf_token, 然后再添加到请求头中

 

浏览器的同源策略:

js脚本在未经允许的情况下,不能够访问其他域下的内容。

同源:协议,域名,端口都相同的则是同源,其中一个不同则都不属于同源。

1、一个域下的js脚本不能访问另一个域下面的cookie,localStorageindexDB

2、一个域下的js脚本不能够操作另一个域下的DOM

3、ajax不能做跨域请求。

 

前后端分离的举例:

后端(flask)

from flask_wtf.csrf import generate_csrf

 

@app.after_request

def after_request(response):

    # 调用函数生成csrf token

    csrf_token = generate_csrf()

    # 设置cookie传给前端

    response.set_cookie('csrf_token', csrf_token)

    return response

 

前端(js)

$.ajax({

...

headers: {"X-CSRFToken": getCookie("csrf_token")},

...

})

 

function getCookie(name) {

    var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");

    return r ? r[1] : undefined;

}

 

assert断言

assert 语句, "报错提示"

判断assert后面跟的语句的bool值是否为True, 若为True, 则不报错, 否则报错, 并提示报错信息, 可以不跟报错提示

 

python对接WebServices SOAP协议接口

模块: suds模块

from suds.client import Client

pip install suds-jurko==0.6  # 这个是适配python3

参考官方文档:

https://suds-py3.readthedocs.io/en/stable/

参考python实现webservices服务端及客户端:

https://blog.csdn.net/qq_33196814/article/details/122303882

备注: 对接webservices接口的路由最后面必须添加?WSDL, 否则会报错:

xml.parsers.expat.ExpatError: mismatched tag: line 621, column 16

查看webservices所有可用接口:

def get_all_methods(client):
    return [method for method in client.wsdl.services[0].ports[0].methods]

查看webservices接口的请求头对象:

request_head = client.factory.create('requestHead')
print("---request_head---", request_head)

其他部分的对象也是这样查看

其他:

据说requests模块也可以访问webservices服务器接口, 但我并未试验成功

 

解决字典/列表在遍历中改变大小的问题

d1 = {1: 2, 3: 4}

for i in d1.keys():
    if i == 1:
        d1.pop(i)

print(d1)

报错: RuntimeError: dictionary changed size during iteration

解决:

将d1.keys() 替换成list(d1.keys()) 即可

 

拓展: 其实遍历列表并删除列表中元素的行为虽然不报错, 但是有缺陷, 例如:

l1 = [2, 4, 4, 6]

for i in l1:
    print("---i---", i)
    if i == 4:
        l1.remove(i)

print("---l1---", l1)  # ---l1--- [2, 4, 6]

# 理应删除列表里面的所有4, 但却还有4未删除,

问题原因: 删除第一个4时后面一个4的索引变成了1, 而索引1刚刚已经遍历过了, 所以直接跳过了第二个4

解决方法: 遍历时使用 for i in list(l1),  推测是创建了临时变量list(l1), 遍历的是list(l1)这个变量, 而不是l1这个变量, 所以遍历和删除不冲突

 

celery清空队列中的任务(不知道是否可行)

redis-cli -n 15 ltrim transcode 0 196

# 其中-n后面跟的参数是数据库编号

 

split()split(" ")的区别

split() 

源码解释: 如果sep未指定或为None,任何空白字符串都是分隔符,并从结果中删除空字符串

-> 即会把空白符当成分割符(多个相连的空格会被视为一个空白符)

split(" ")的时候,会把一个空格作为分割符,会分割出来的空字符串也不做处理, 会保留在结果列表中

所以对于不要结果列表中出现空字符串时都是用split()进行分割

 

python使用SMTP发送邮件

参考1:

https://www.cnblogs.com/yuling25/p/smtp-email.html

参考2:

https://www.runoob.com/python/python-email.html

 

posted @ 2022-10-21 01:09  斑驳岁月  阅读(45)  评论(0编辑  收藏  举报