• 【Python】 零碎知识积累 II


    【Python】 零碎知识积累 II

    ■  函数的参数默认值在函数定义时确定并保存在内存中,调用函数时不会在内存中新开辟一块空间然后用参数默认值重新赋值,而是单纯地引用这个参数原来的地址。这就带来了一个坑,那就是当你在函数体中对默认参数做一些改变的时候,若参数默认值是int,str这种不可变类型,那么还好,因为默认参数在内存中并没改变,只是参数指向另一块空间;但若是list这种可变类型,当你对默认参数做改变的时候,将会直接影响这个参数在内存中实际的值,这也就影响了下次调用这个函数时参数的默认值。比如:

    def foo(para=1):
        para += 1
        print para
    >>>foo()
    2
    >>>foo()        #第二次调用结果不变
    2
    
    def foo(para=[1])
        para.append(1)
        print para
    >>>foo()
    [1,1]
    >>>foo()    #第二次调用会显示出第一次调用过程中的一些影响,因为默认参数值是个可变型数据结构,对其所做的改变会真实反应到内存中去
    [1,1,1]

       碰到这种陷阱的时候,有什么解决办法呢。一个比较好的实践是,(比如上面第二个示例函数这里)在函数体的最开始那里加入一个para = list(para),通过将一个列表作为参数传给list类型转换函数,其本质实际上是获得到了一个不同对象但是值相同的列表给了para变量。之后对para变量做的改变并不会直接影响函数参数默认值那边存储的值,因此可以做到每次运行函数的输出相同了。

    ■  获取当前脚本的绝对路径

      os.path.abspath(__file__)

      __file__这个魔法变量的意思是指代文件本身

    ■  判断某个类是否有某个属性(变量或者方法)用hasattr函数。这个函数有两个参数,第一个是一个类的实例,第二个是个字符串来指出想要检查的属性名。比如:

      令s = "hello". s就是个字符串了。然后hasattr(s,"strip")就是判断s这个实例有没有名为strip的属性。事实上字符串有strip这个方法,所以返回True

    ■  第三方模块chardet可以检测字符串的编码格式,安装用pip install chardet

      使用chardet.detect("一个字符串")

      需要注意的是直接输入一个中文字串似乎只是查看这个文件默认的编码格式而已,因为中文被存进内存的时候会被python用默认编码格式处理一下。

    ■  想要查看某个模块的源码之类的情况,需要知道这个模块的路径。可以在python中import这个模块之后,在pythonshell中直接键入模块名,就会显示路径了。

    ■  list类增加成员有很多种方法,比如:

      [1].append(2)  //前两种方法都是直接对内存中list本身的内容做修改。

      [1].extend([2])

      [1]+[2]  //这种方法其实是要在内存中建立一个新的list对象来盛放两个老list合起来的内容

      从运行速度的效果来看,第三种是最慢的因为要建立一个新对象

    ■  关于可变&不可变对象 数据在内存中的形态

      其实这点内容应该是学python时最早就应该懂的,我当时学的时候没在意太多,现在还要回过头来再仔细看一遍。。

      在python之中,一切皆对象。意思是说,一个变量或者函数之类的,只是引用了内存中存在的一些实在的数据而并不被直接存进内存。这就引出了一个问题,当我对变量的值进行一些改变时,是让变量换一个引用对象呢还是说直接在当前引用对象上修改呢?另外,当我增加一个变量,值和现在的变量一样,它引用的是同一片内存空间呢还是说引用另一片内存空间只是内容一样呢?这个区别就是不可变和可变类型的区别。

      对于int,str,tuple这种不可变类型。相同的值的变量引用的是同一片内容(这句话是不对的,怪我一开始只用了int和str型做了实验没做tuple。。对tuple而言,相同值的不同变量引用的内容地址也是不一样的,所以相同的值的变量必然引用同一片内容应该不是不可变类型的判断标准。事实上,解释器会对较短较小的变量进行缓存所以会有引用同一片内容的情况。当值本身较大比如"very good morning"这么长一个字符串的话即便是两个值相同的对象,引用还是不一样的。另外还要注意一个坑。。若两个变量都是类似于(1)这种,你会发现引用的仍然是同一片内容,但是(1)是int啊啊啊,(1,)才是tuple。。。这个坑需要注意),改变变量的值相当于改变这个变量的引用,使之指向另一个对象了:

    >>>x = 1
    >>>y = 1
    >>>id(x) == id(y)
    True
    >>>x += 1
    >>>id(x) == id(y)
    False

      对于list,dict这种可变类型。相同的值的变量引用的也是不同的对象。改变变量的值相当于直接在内存中的对象上做改变但变量的引用关系不变:

    >>>x=[1]
    >>>y=[1]
    >>>id(x) == id(y)
    False
    >>>oldXID = id(x)
    >>>x.append(2)
    >>>id(x) == oldXID
    True

       总的来看,不可变类型的好处是多个等值变量可以只引用一个对象,节省内存。但变量值变化较多的情况就要创建出各种各样的对象来,就不是很好了。

      顺便一提,在判断相等时的is和==也是这个差别。is判断的是是不是引用对象一致。而==判断的只是引用对象的值是不是一致。

    ■  有时候stdout和stderr一起有信息,且程序运行挺快的话,在控制台里看到的信息可能是两者混起来的那种。毕竟向控制台输出信息的过程也是一个流,会有竞争

    ■  关于sys.exit()和os._exit()的区别和联系(http://blog.csdn.net/g863402758/article/details/53304480)

      python程序有这两种退出程序的方法,两者有所不同。os._exit的退出时真·退出,会直接关闭解释器,而os._exit之后的代码也不会再被执行了。而sys.exit()则是raise起一个叫SystemExit的异常,如果程序不捕获这个异常的话程序就直接退出。但如果捕获了,那么except以及后面的finally里面的代码都还是会被执行的。

      两者都可以接受一个int型参数,作为程序退出时返回给系统的返回码

    try:
        os._exit(1)
    except Exception,e:
        print "something after exit"
        #不会被打印出来
    
    try:
        sys.exit(1)
    except Exception,e:
        print "something after exit"
        #还是会被打印出来的

    ■  格式化字符串中如果要输入百分号的话就要%%(两个百分号,前一个转义,后一个才是真的百分号)而不是\%或者啥的。。

    ■  (1)是个int型数据!(1,)才是tuple。。但是[1]仍然是list。。。

    ■  脚本最开始的两行

      习惯上来说,脚本的头两行总是写上了#!/usr/bin/env python和#coding=utf-8。这两句虽说是注释,但是是有实际意义的。

      第二句#coding=xxx比较熟悉一点,它指定了本文件默认编码格式,也就是说当你在脚本里写上中文的字符串变量以及注释中有中文的时候,解释器会默认把他们编码成指定编码格式的。比如下面这个例子:

    #coding=utf-8
    s = "中文"
    print repr(s)    #输出'xe4xb8xadxe6x96x87'
    print u"中文".encode("utf-8")    #同上,说明默认中文字符串是用utf-8编码的
    
    #####在另一个文件中######
    #coding=gbk
    s = "中文"
    print repr(s)    #输出'xd6xd0xcexc4'
    print u"中文".encode("gbk")    #同上,说明这次是用gbk编码了

      而第一句的意思是指定了默认的执行这个文件的解释器。在linux中,默认的可执行文件解释器是/bin/bash。当你把python文件添加了可执行权限之后,就可以通过./xxx.py来执行脚本,但是会报错可能还是因为用了bash来解释脚本。通过指定解释器,就可以让python解释器解释脚本了。大多数人都是习惯写#!/usr/bin/python但是如果写 #!/usr/bin/env python的话可以避免python版本不一样之类的乱七八糟的问题,通用性更加好一些。

      *今天在zabbix中碰到一个大坑,在dos系统中的换行是 ,而在unix系统中是 。所以在windows中可以正常直接执行的py文件,第一行其实是#!/usr/bin/env python ,放到unix中就无法识别python 是个什么文件,所以执行失败了,报:No Such File什么的错误。修复操作:在vim中:set ff可以查看文件是dos格式的还是UNIX格式的,也可以:set ff=unix来强行把dos转换成unix的。

    ■  关于python的I/O缓存以及设置不缓存

      在python中,默认是有输入输出缓存的。比如在脚本中sys.stdout.write一些东西的时候,python先把这些内容放到stdout的缓存中,等到缓存区满或者程序结束再输出到屏幕上。而print语句是自带缓存清空的(有一点小疑惑,比如我有一个每隔一秒print一次信息的脚本,当我在命令行里调用脚本的时候,stdout默认是屏幕的时候似乎确实是自带缓存清空的,信息每隔一秒就打印一次。但是当我把输出信息重定向到一个文件中的时候,就会出现输出文件一直是空,直到整个程序跑完之后才有全部内容。下面的内容场景默认是后者,即如何把输出信息实时导入到文件中去)有时候我们需要立刻获得程序的当前输出,等不到他完成整个程序的时候,这个缓冲就不太好了。解决的方法也有好多。比如:

      调用sys.stdout.flush()方法来手动清空缓存,输出内容。在print出相关信息后立刻调用一次flush,就把之前print出的所有信息都输出掉了。

      另外一种方法是在调用的命令中加上参数-u,python -u表示不要设置输入输出缓存。(更多详细可见python的命令行参数解释)

    ■  关于if None值的判断

      不知道什么时候起,养成了判断条件写 “if 变量名:”  这种格式。。然后在判断变量名是不是None的时候,不知道为什么总是踩这个坑。很明显,if None:下面的代码是不被执行的而if not None:下面的代码才是被执行的。

      但在自然语言中,我总是这样想:某个变量有值我就执行语句,没有值就不执行。嗯,没毛病,然后就写下了if not 变量名:语句 。。。其实这是反了啦!所以就我个人而言。。我建议不要在写if not 怎样怎样了,一定要写就写if 变量名,这样比较清晰易懂不容易晕。。

    ■  生成器中yield语句和return语句共存

      在小于3.3版本的python中,生成器中yield语句和return语句是不能共存的。

    ■  range和xrange的区别 

      之前可能也在哪里提到过了が,range是直接返回一个列表,如果这个列表比较大那么我们就要等很久。但是xrange返回的是相同range列表的生成器,所以几乎不花费多少时间,想要用数据的时候就可以直接取出来用了。

    ■  关于异常处理观念上的一个容易忽略的地方

      虽然是很基础的,谁都能一眼看出来。。如果我们在代码中处理了异常,那么这个报错就不会导致程序结束了。换句话说,下面这种情况:

    li = []
    li[1]
    print 'game'
    print "over"
    #上面这段代码中,game和over两个都肯定不会打印出来的,因为错误导致程序中止了
    
    li = []
    try:
      li[1]
      print 'game'
    except IndexError:
      pass
    print "over"
    #上面这段over就打出来了,但是game依然没有,因为try代码块中错误后面的代码不被执行が错误被处理过后,下面的代码就继续被执行了

    ■  看到了一个比较好的对于如下这样结构代码的格式优化:

    if os.environ.get("SOME_ENV") is None:
      result = 'SOME_DEFAULT_VALUE'
    else:
      result = os.environ.get("SOME_ENV")
    
    ####改成下面这样非常优雅####
    result = os.environ.get("SOME_ENV") or "SOME_DEFAULT_VALUE"

    ■  对于函数级别上,经常出现的前处理和后处理,可以考虑用自定义装饰器来一劳永逸地定义。不要忘了functools.wraps这个模块,来让被装饰的函数更加友好

    ■  发现了一个很有意思的模块,faker。之前在测试的时候总是需要自己头脑风暴出一些人名,内容什么的,这个库可以直接生成很多看起来很真实的数据,当然是基于西方和英语世界的。。中文测试数据在下面说明

      pip install faker之后主要使用方法:

    from faker import Factory
    
    fak = Factory.create()
    fak.name()    #造出一个人名
    fak.word()    #造出一个单词,这个单词好像只是看着像单词,就算是真的肯定不是英语单词。。看着有点像拉丁文。。
    fak.email()    #伪造一个email地址

       补充: 今天看到了关于如何进行中文测试数据生成的介绍:http://www.cnblogs.com/progor/p/9188683.html

      简单来说,只要在创建faker对象的时候加入参数locale='zh_CN'即可。另外再补充点更多的假造数据方法:

    # -*- coding:utf-8 -*-
    
    from faker import Faker
    
    fak = Faker(locale='zh_CN')
    # 需要注意对于中文信息faker返回的基本上都是Unicode类型的
    
    fak.address()    # 伪造一个完整地址。下面几个方法是地址的各种要素单独伪造
    fak.street_name()
    fak.city_name()
    fak.street_address()
    fak.province()
    
    fak.company()    # 伪造一个公司全名
    fak.company_suffix()  # 公司性质
    fak.company_prefix()  # 公司名称
    
    fak.simple_profile()    # 完整地伪造一个人的基本信息,包括住址,性别,生日等
    
    fak.word()
    fak.words(2)
    fak.sentence()
    fak.paragraph()    # 生成一些狗屁不通的中文词、句、段。。

    ■  复杂字典的扁平化

      在数据交互中,经常会碰到一些层层嵌套的字典和列表。尤其是在以JSON格式为主要交流数据规范的时候。。比如下面这样一个字典:

    {
        "sda": {
            "model": "CentOS Linux-0 S", 
            "holders": [], 
            "host": "SATA controller: Intel Corporation 82801HR/HO/HH (ICH8R/DO/DH) 6 port SATA Controller [AHCI mode] (rev 02)", 
            "partitions": {
                "sda2": {
                    "sectorsize": 512, 
                    "uuid": "null", 
                    "sectors": "133191680", 
                    "start": "1026048", 
                    "holders": [
                        "VolGroup-lv_root", 
                        "VolGroup-lv_swap", 
                        "VolGroup-lv_home"
                    ], 
                    "size": "63.51 GB"
                }, 
                "sda1": {
                    "sectorsize": 512, 
                    "uuid": "3aec5112-2a1c-4ce0-91de-e9514ba357b8", 
                    "sectors": "1024000", 
                    "start": "2048", 
                    "holders": [], 
                    "size": "500.00 MB"
                }
            }
        }
    }

      这种字典不仅难看,而且获取某个值时特别麻烦,经常要dic['a']['b']['c']...一长串。现在提供一个解决方法,把嵌套的字典给扁平化成一个仅一层的字典,而把原先结构上的复杂度转化到字典的key的表示上去。比如上面这个字典可以通过下面的方法来扁平化成下面一样的字典。分隔符/完全是自定义的

    {
        "sda/holders": [], 
        "sda/host": "SATA controller: Intel Corporation 82801HR/HO/HH (ICH8R/DO/DH) 6 port SATA Controller [AHCI mode] (rev 02)", 
        "sda/partitions/sda2/size": "63.51 GB", 
        "sda/model": "CentOS Linux-0 S", 
        "sda/partitions/sda1/uuid": "3aec5112-2a1c-4ce0-91de-e9514ba357b8", 
        "sda/partitions/sda2/uuid": "null", 
        "sda/partitions/sda2/holders": [
            "VolGroup-lv_root", 
            "VolGroup-lv_swap", 
            "VolGroup-lv_home"
        ], 
        "sda/partitions/sda2/sectorsize": 512, 
        "sda/partitions/sda1/start": "2048", 
        "sda/partitions/sda2/start": "1026048", 
        "sda/partitions/sda1/size": "500.00 MB", 
        "sda/partitions/sda1/sectorsize": 512, 
        "sda/partitions/sda1/holders": [], 
        "sda/partitions/sda2/sectors": "133191680", 
        "sda/partitions/sda1/sectors": "1024000"
    }

      进行这个操作的函数是这样的:

    def flatten(d):
       def _flatten(src,dst,pattern=''):
           for k,v in src.items():
               key = k if pattern == '' else '{}/{}'.format(pattern,k)
               if isinstance(v,dict):
                   _flatten(v,dst,key)
               else:
                   dst[key] = v
       result = {}
       _flatten(d,result)
       return result

      可以看到其原理是以一个双重嵌套的函数加上递归,来从外到内地捋平嵌套字典。

      仔细想了一下,发现这个也不是太万能,对付字典嵌套字典可以,对付列表嵌套字典就不行了。总之分清楚用的场合把。在获得扁平化的字典之后,访问某个值就不用再那么蛋疼的[][][]了,只要['a/b/c']就好。

    ■  JSON格式和字典的区别

      今天实验时发现json.loads("{'a':'b'}")是会报错的,但是json.loads('{"a":"b"}')是不会报错的。思考良久,估计是json本身有格式要求,上网一查果然是这样。

      诚然,JSON格式的数据对象和python中的字典非常相像,但是两者还是有些区别的。在python的字典中单双引号基本上是没有差别的,所以很容易在写JSON格式的字符串时,不区分单引号包双引号和双引号包单引号两种写法。不过JSON格式要求引号全部都是双引号,因之,单引号包双引号可以被json模块顺利用load解析但是后者不行。

      另一方面,在dump的时候,即便用了单引号,json模块也会自动把单引号转变为双引号,所以不用担心。说到自动转换,在load的时候json模块也会把产出的字典中所有源json中的字符串格式转化为unicode。然而转化时默认的编码是utf8,如果需要手动指定编码就需要在loads中添加参数encoding。比如json.loads('{"你好":"hello"}',encoding='gbk')就可以在cmd的pythonshell中成功运行,得到结果是{u"u4f60u597d": u"hello"}。可以看到,汉字和英文字母都被转化成了unicode了。

      另外在非python的环境中,比如javascript编程过程中也会遇到单双引号和JSON格式数据之间的一个矛盾,也是需要注意的。总之碰到JSON格式的时候老老实实用双引号来进行操作就比较稳了

    ■  如何查看已加载命名空间中的所有变量名

      今天要在一个脚本中import 一个自定义的模块,但是想在import之后检查,有变量A则用变量A,没有就用变量B。当然可以try一下变量然后except ImportError,不过这样做不太好,会有嵌套的错误处理。于是就想到了直接从模块对象中找出模块中所有命名信息。

      第一个想到的办法是检查<module>.__dict__,之前__dict__这个对象只针对类对象使用过,但其实对模块对象也是奏效的,会返回模块(在这里更愿意把模块称为一个命名空间,即在这个作用域内有一些变量得到命名,变量名所指向的对象被维护在__dict__中,出了这个命名空间,这个变量名就没有意义了)中所有对象名和对象实体间的键值对关系。

      进一步网上搜了一下发现了dir这个函数,dir(module)相当于是module.__dict__.keys()(略微有些不同,dir函数会根据对象类型的不同而做一个排序),返回一个列表,里面是所有目标模块中的对象的名字。比如:

    import test
    #test是一个自定义模块
    
    print dir(test)
    print test.__dict__.keys()
    
    #结果
    #['USERS', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'os', 'random', 'warnings']
    #['USERS', 'warnings', '__builtins__', '__file__', 'random', '__package__', '__name__', 'os', '__doc__']

    ■  获取linux用户信息

      说到获取linux用户的信息,第一个想到的就是去解析/etc/passwd了。之前也都是这么做的,不过今天偶尔在网上搜了一下发现linux的python自带了一个pwd模块可以更加方便地解决这个事。

    import pwd
    for user in pwd.getpwall():    #获取所有用户对象的列表
        print user.pw_name,user.pw_uid
    
    print pwd.getpwuid(xxx)    #通过指定的某个uid获取用户信息
    print pwd.getpwnam('xxx')    #通过指定某个用户名获取用户信息

      一个用户对象包含了pw_name='test', pw_passwd='x', pw_uid=1002, pw_gid=1002, pw_gecos='', pw_dir='/home/test', pw_shell='/bin/bash'这些属性,可以看出来这些属性的取值是直接从/etc/passwd里面取出来的。

    ■  split和rsplit方法

      一般用split方法,我们都习惯于s.split()或者s.split(sep)这样来用。但是实际上,split可以传入第二个参数,是一个int,表示切分到第n个扫描到的分隔符。比如对于s = '1,2,3,4',有

      s.split(',',1)的结果是['1','2,3,4'],而s.split(',',2)的结果是['1','2','3,4']。另外rsplit是从右边开始扫描分隔符,所以s.rsplit(',',1)的结果是['1,2,3','4']。利用s.rsplit(sep,1)的方法可以方便地得到分隔后的最后一项,不要像以前一样s.split(sep)[-1]这样去做了。

    ■  isinstance方法的第二个参数

      许多时候,我们要对一个对象属于什么类型做判断,通常就是用isinstance函数来做。但是碰到要判断一个对象,或许有多种可能的类型时就比较头痛。以前都是写好几个or。今天发现isinstance函数其实自带了第二个参数,是一个tuple,每个成员是类型名(不是字符串!)。比如isinstance(1,(tuple,list,dict,int))返回True,因为int类型在后面的元组中。但是isinstance(1,(tuple,list,dict))返回False。

    ■  'True'和'False'这两个字符串并不能通过bool()方法强制进行类型转换。所以小心哦~

    ■  basestring类是str和unicode两个类的基类,也就是说可以一次性判断某个变量是不是字符串而不关心到底是编码过的还是Unicode。

    ■  基于HTTP进行简单的文件传输共享

      自己搭一个下载文件的服务固然可行,但是略显麻烦。很直接的一个方法就是进入你要共享的目录,然后运行命令:

      python -m SimpleHTTPServer 8080,然后就可以让同一个网段的其他主机访问http://ip:8080来下载这个目录下的一些文件了。很是方便

    ■  关于raw_input函数

      raw_input可以说是接触python之后最早碰到的函数之一,通常可以加上一个字符串参数以提示输入,不过经过试验这个参数不能是unicode类型的,所以当from __future__ import unicode_literals之后,一定要在raw_input中raw_input("中文字符".encode('utf8'))之类的。

      究其原因,是因为传入unicode时raw_input会自动将这个unicode字符串通过解释器默认编码(sys.getdefaultencoding()可以看到,一般没有进行过set的默认值是ascii,自然不能编码带有中文的unicode串,因此会报错)进行编码。如果一定要传入unicode,那么请在脚本头上进行sys.setdefaultencoding。

    ■  如何为getattr设置默认值

      说到从一个整体中根据字段名取数据,python中大概有dict和class两种数据结构支持:dict['key']以及object.attr。dict的话我们知道有dict.get('key','default')这样的形式,那object呢?不一定要去try/except,可以getattr(object,'attr','default')这样的形式来。getattr是个函数。attr要变成字符串形式。

    ■  关于is和==

      今天偶尔看到一个知识点说两者的区别。一般情况下,前者判断的是两个对象指向的引用地址是否相同,后者判断的是两个对象实质内容是否相等。

      但是给出了这样一个示例:  a = 'hello world'以及b = 'hello world',然后打印a == b当然是True,但问题是a is b会是False。两者的值'hello world'本身作为一个不可变对象,照理由在内存中应该只有一个地址才对。原来是Python对于较长的字符串(比如这个hello world,总长度为11个字符),并不会仅仅只在内存中声明一个实体,而是像可变对象一样声明多个实体。自然这些实体之间的id是不同的。如果将字符串写得短一些比如a = b = 'hello',此时就是只声明一个实体,因此两者id相同。此时的a is b 返回是True。

    ■  ftplib的简单应用

      都用SFTP了。。ftp纯粹历史包袱。给出个示例吧:

    from ftplib import FTP
    import os
    
    ftp = FTP('192.168.142.22')
    
    ftp.login('user','passwd')
    
    # 上传文件/local/path/file到远端的/remote/path下
    fiobj = open('/local/path/file','rb')
    filename = fiobj.name
    remote_path = '/remote/path'
    try:
      ftp.storbinary('STOR %s' % os.path.join(remote_path,filename),fiobj)
    except Exception,e:
      raise
    finally:
      ftp.quit()
      fiobj.close()

      值得注意的是sorbinary方法并不是简答的上传方法,而是要在参数中写出ftp方法STOR的。类似的下载方法 也是。如果不写出FTP命令的话很可能会报错500 unknown command。

    ■  关于用{}生成表达式

      之前应该是记过了不过找不到了。。

      众所周知列表可以方便的用[]生成比如[i for i in range(10)]

      其实大括号也有类似的功能。举例:

      {i for i in range(3)}  ==  set([0,1,2])  当遍历单位是一元的时候,大括号生成的是一个set类型的数据即集合

      {key: value for key,value in zip('abc',range(3))}  ==  {'a':0, 'c':2, 'b':1}  当遍历的单位是二元的时候生成的就是一个字典了。

  • 相关阅读:
    meta标签中的http-equiv属性使用介绍
    MySQL中批量执行SQL语句
    <fmt:formatNumber/>显示不同地区的各种数据格式
    <fmt:setBundle/>标签
    <fmt:bundle/>、<fmt:message/>、<fmt:param/>资源国际化
    <fmt:setTimeZone/>设置时区
    <fmt:timeZone/>显示全球时间
    初识Storm
    storm安装及启动
    HBase API 的使用(一)
  • 原文地址:https://www.cnblogs.com/franknihao/p/6621670.html
Copyright © 2020-2023  润新知