Python Module


PySnooper - 不要使用print进行调试

Pysnooper API

PySnooper 是一个非常方便的调试器。如果您正在试图弄清楚为什么您的Python代码没有按照您的预期去做,您会希望使用具有断点和监视功能的成熟Debug工具,但是许多Debug工具配置起来非常麻烦。

现在,有了PySnooper,您并不需要配置那么复杂的Debug工具,就能够完成对整个代码的分析。它能告诉您哪些代码正在运行,以及局部变量的值是什么。

其实,PySnooper 就是替代了一行一行print的重复性工作,给你的代码一个pysnooper装饰器,它能自动识别到语句和变量并将其值print出来:

import pysnooper

@pysnooper.snoop()
def number_to_bits(number):
    if number:
        bits = []
        while number:
            number, remainder = divmod(number, 2)
            bits.insert(0, remainder)
        return bits
    else:
        return [0]

number_to_bits(6)

效果如下:

Source path:... 1.py
Starting var:.. number = 6
23:03:35.990701 call         4 def number_to_bits(number):
23:03:35.991699 line 5     if number:
23:03:35.991699 line 6         bits = []
New var:....... bits = []
23:03:35.991699 line 7         while number:
23:03:35.991699 line 8             number, remainder = divmod(number, 2)
Modified var:.. number = 3
New var:....... remainder = 0
23:03:35.991699 line 9             bits.insert(0, remainder)
Modified var:.. bits = [0]
23:03:36.004664 line 7         while number:
23:03:36.005661 line 8             number, remainder = divmod(number, 2)
Modified var:.. number = 1
Modified var:.. remainder = 1
23:03:36.005661 line 9             bits.insert(0, remainder)
Modified var:.. bits = [1, 0]
23:03:36.007657 line 7         while number:
23:03:36.007657 line 8             number, remainder = divmod(number, 2)
Modified var:.. number = 0
23:03:36.008655 line 9             bits.insert(0, remainder)
Modified var:.. bits = [1, 1, 0]
23:03:36.008655 line 7         while number:
23:03:36.009651 line 10         return bits
23:03:36.009651 return      10         return bits
Return value:.. [1, 1, 0]
Elapsed time: 00:00:00.020945

可以看到,它将每一行变量的值都输出到屏幕上,方便你调试代码。

仅仅需要写一行代码—使用装饰器就可以实现这个方便的调试功能,比起一行行写print,这可方便多了。

0. 安装模块

使用这个模块,你只需要使用Pip安装PySnooper:

pip install pysnooper

接下来讲讲这个模块其他好用的功能:

1. 支持日志文件

如果你觉得print到屏幕上不方便,还可以将其输出到log文件中,你只需要将装饰器那一行改为:

@pysnooper.snoop('/my/log/file.log')

2. 读取局外变量或其他表达式

如果你想读取在装饰器作用范围以外的变量或者表达式的值,还可以使用watch参数:

@pysnooper.snoop(watch=('foo.bar', 'self.x["whatever"]'))

3. 如果你不想用装饰器,也可以用上下文的形式调试

没错,装饰器有限定的使用条件,使用起来比较局限,因此pysnooper还支持使用 with 的上下文形式:

import pysnooper
import random

def foo():
    lst = []
    for i in range(10):
        lst.append(random.randrange(1, 1000))

    with pysnooper.snoop():
        lower = min(lst)
        upper = max(lst)
        mid = (lower + upper) / 2
        print(lower, mid, upper)

foo()

效果如下,只有上下文里的代码才会被调试出来:

New var:....... i = 9
New var:....... lst = [681, 267, 74, 832, 284, 678, ...]
09:37:35.881721 line 10         lower = min(lst)
New var:....... lower = 74
09:37:35.882137 line 11         upper = max(lst)
New var:....... upper = 832
09:37:35.882304 line 12         mid = (lower + upper) / 2
74 453.0 832
New var:....... mid = 453.0
09:37:35.882486 line 13         print(lower, mid, upper)
Elapsed time: 00:00:00.000344

当我们只需要调试部分代码的时候,这个上下文形式的调试方法非常方便。

此外,PySnooper还有许多更强大的用法,大家可以看他们的高级使用文档:

https://github.com/cool-RR/PySnooper/blob/master/ADVANCED_USAGE.md

4. 参数介绍

以装饰器的形式使用该工具,其包含了四个参数,参数包括output, variables, depth, prefix。

1、output参数。该参数指定函数运行过程中产生的中间结果的保存位置,若该值为空,则将中间结果输出到控制台。

2、variables参数。该参数是vector类型, 因为在默认情况下,装饰器只跟踪局部变量,要跟踪非局部变量,则可以通过该字段来指定。默认值为空vector。

3、depth参数。该参数表示需要追踪的函数调用的深度。在很多时候,我们在函数中会调用其他函数,通过该参数就可以指定跟踪调用函数的深度。默认值为1。

4、prefix参数。该参数用于指定该函数接口的中间结果前缀。当多个函数都使用的该装饰器后,会将这些函数调用的中间结果保存到一个文件中,此时就可以通过前缀过滤不同函数调用的中间结果。默认值为空字符串。

5. 测试

1. output 参数使用

若使用默认参数,则将中间结果输出到控制台,若填写该参数,则将中间结果写入到该参数指定的目录下,如运行以下代码,其中间结果会保存在装饰器snoop中设置日志保存的路径中,注意这里不会自动创建目录,所以需要事先创建目录,如测试代码中填写路径后需要创建log目录。

测试代码:

import pysnooper

def add(num1, num2):
    return num1 + num2

@pysnooper.snoop("./log/debug.log", prefix="--*--")
def multiplication(num1, num2):
    sum_value = 0
    for i in range(0, num1):
        sum_value = add(sum_value, num2)
    return sum_value

value = multiplication(3, 4)

运行该代码后,在./log/debug.log的内容如下:

从运行代码的中间结果中可以看出,文件中记录了各行代码的执行过程及局部变量的变化。在debug时,通过分析该文件,就可以跟踪每一步的执行过程及局部变量的变化,这样就能快速的定位问题所在;由于运行的中间结果保存在文件中,方便随时分析其运行的中间结果,也便于共享。

2. variables参数使用

在默认参数的情况下,使用该工具只能查看局变量的变化过程,当需要查看局部变量以外变量时,则可以通过variables参数进行设置,比如下方代码,在Foo类型,需要查看类实例的变量self.num1, self.num2, self.sum_value,则可以看将该变量设置当参数传入snoop的装饰器中。

测试代码:

import pysnooper

class Foo(object):
    def __init__(self):
        self.num1 = 0
        self.num2 = 0
        self.sum_value = 0

    def add(self, num1, num2):
        return num1 + num2
    @pysnooper.snoop(output="./log/debug.log", variables=("self.num1", "self.num2", "self.sum_value"))
    def multiplication(self, num1, num2):
        self.num1 = num1
        self.num2 = num2
        sum_value = 0
        for i in range(0, num1):
            sum_value = self.add(sum_value, num2)
        self.sum_value = sum_value
        return sum_value

foo = Foo()
foo.multiplication(3, 4)

得到的结果如下:

从两个中间结果中可以看出,若变量不是局部变量,哪怕在函数中使用了该变量,如果不显示设置打印该变量的中间结果,则不会将该变量的中间结果打印到文件中。

3. depth参数使用

该参数用来指定记录函数调用层数的结果,默认值为1,若要查看多层函数调用的中间结果,则可将该参数设置为>=2。

测试代码:

import pysnooper

def add(num1, num2):
    return num1 + num2

@pysnooper.snoop("./log/debug.log", depth=2)
def multiplication(num1, num2):
    sum_value = 0
    for i in range(0, num1):
        sum_value = add(sum_value, num2)
    return sum_value

value = multiplication(3, 4)

其结果如下:

从上述结果中可以看出,若要查看更深层次函数调用的情况,则可以通过设置depth值进行查看。这样方便用户有选择性的查看函数的调用情况。

4. prefix参数使用

该参数主要用于设置中间结果的前缀,这样就可以区分不同的函数调用的中间结果,默认参数为””。

测试代码:


import pysnooper

def add(num1, num2):
    return num1 + num2

@pysnooper.snoop("./log/debug.log", prefix="--*--")
def multiplication(num1, num2):
    sum_value = 0
    for i in range(0, num1):
        sum_value = add(sum_value, num2)
    return sum_value

value = multiplication(3, 4)

运行代码后的中间结果如下:

从结果中可以看到,中间结果的每一行都包含了prefix设置的前缀,这样便于区分不同的函数调用的中间结果。

上述的介绍为了将注意力集中到具体的参数,采取设置单一参数的形式进行介绍(output+其他单个参数)。在实际使用时,可以同时设置多个参数。使用PySnooper工具来记录函数运行的中间结果,比起传统的使用断点+单步调试,pdb等调试方法,PySnooper工具有着巨大的优势。

该工具的不足之处

虽然使用debug在使用PySnooper很方便,但还是存在一些问题(以4月26号拉取代码为依据),比如:

1、无法很好的支持递归调用。

2、调用每个函数的中间结果只能保存在一个文件中,如果需要区分不同文件的结果,需要使用prefix来进行前缀标识。

3、对于跨文件函数调用,不支持记录调用函数所在的文件名。

当然PySnooper是最近在github上火起来的项目,还不够完善是正常的,相信这些不足之处后续也会得到完善,期待一个更好的PySnooper。

Itertools - 迭代器(生成器)

Itertools API

很多人都致力于把Python代码写得更Pythonic,一来更符合规范且容易阅读,二来一般Pythonic的代码在执行上也更有效率。

itertools库

迭代器(生成器)在Python中是一种很常用也很好用的数据结构,比起列表(list)来说,迭代器最大的优势就是延迟计算,按需使用,从而提高开发体验和运行效率,以至于在Python 3中map,filter等操作返回的不再是列表而是迭代器。

话虽这么说但大家平时用到的迭代器大概只有range了,而通过iter函数把列表对象转化为迭代器对象又有点多此一举,这时候我们今天的主角itertools就该上场了。

使用itertools

itertools中的函数大多是返回各种迭代器对象,其中很多函数的作用我们平时要写很多代码才能达到,而在运行效率上反而更低,毕竟人家是系统库。

itertools.accumulate

简单来说就是累加。

>>> import itertools
>>> x = itertools.accumulate(range(10))
>>> print(list(x))
[0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

itertools.chain

连接多个列表或者迭代器。

>>> x = itertools.chain(range(3), range(4), [3,2,1])
>>> print(list(x))
[0, 1, 2, 0, 1, 2, 3, 3, 2, 1]

itertools.combinations

求列表或生成器中指定数目的元素不重复的所有组合

>>> x = itertools.combinations(range(4), 3)
>>> print(list(x))
[(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)]

itertools.combinations_with_replacement

允许重复元素的组合

>>> x = itertools.combinations_with_replacement( ABC , 2)
>>> print(list(x))
[( A ,  A ), ( A ,  B ), ( A ,  C ), ( B ,  B ), ( B ,  C ), ( C ,  C )]

itertools.compress

按照真值表筛选元素

>>> x = itertools.compress(range(5), (True, False, True, True, False))
>>> print(list(x))
[0, 2, 3]

itertools.count

就是一个计数器,可以指定起始位置和步长

>>> x = itertools.count(start=20, step=-1)
>>> print(list(itertools.islice(x, 0, 10, 1)))
[20, 19, 18, 17, 16, 15, 14, 13, 12, 11]

itertools.cycle

循环指定的列表和迭代器

>>> x = itertools.cycle( ABC )
>>> print(list(itertools.islice(x, 0, 10, 1)))
[ A ,  B ,  C ,  A ,  B ,  C ,  A ,  B ,  C ,  A ]

itertools.dropwhile

按照真值函数丢弃掉列表和迭代器前面的元素

>>> x = itertools.dropwhile(lambda e: e < 5, range(10))
>>> print(list(x))
[5, 6, 7, 8, 9]

itertools.filterfalse

保留对应真值为False的元素

>>> x = itertools.filterfalse(lambda e: e < 5, (1, 5, 3, 6, 9, 4))
>>> print(list(x))
[5, 6, 9]

itertools.groupby

按照分组函数的值对元素进行分组

>>> x = itertools.groupby(range(10), lambda x: x < 5 or x > 8)                                                                                                
>>> for condition, numbers in x:                                                  
...     print(condition, list(numbers))                                                                                                        
True [0, 1, 2, 3, 4]                                                              
False [5, 6, 7, 8]                                                                
True [9]

itertools.islice

上文使用过的函数,对迭代器进行切片

>>> x = itertools.islice(range(10), 0, 9, 2)
>>> print(list(x))
[0, 2, 4, 6, 8]

itertools.permutations

产生指定数目的元素的所有排列(顺序有关)

>>> x = itertools.permutations(range(4), 3)
>>> print(list(x))
[(0, 1, 2), (0, 1, 3), (0, 2, 1), (0, 2, 3), (0, 3, 1), (0, 3, 2), (1, 0, 2), (1, 0, 3), (1, 2, 0), (1, 2, 3), (1, 3, 0), (1, 3, 2), (2, 0, 1), (2, 0,3), (2, 1, 0), (2, 1, 3), (2, 3, 0), (2, 3, 1), (3, 0, 1), (3, 0, 2), (3, 1, 0), (3, 1, 2), (3, 2, 0), (3, 2, 1)]

itertools.product

产生多个列表和迭代器的(积)

>>> x = itertools.product( ABC , range(3))
>>>
>>> print(list(x))
[( A , 0), ( A , 1), ( A , 2), ( B , 0), ( B , 1), ( B , 2), ( C , 0), ( C , 1), ( C , 2)]

itertools.repeat

简单的生成一个拥有指定数目元素的迭代器

>>> x = itertools.repeat(0, 5)
>>> print(list(x))
[0, 0, 0, 0, 0]

itertools.starmap

类似map

>>> x = itertools.starmap(str.islower,  aBCDefGhI )
>>> print(list(x))
[True, False, False, False, True, True, False, True, False]

itertools.takewhile

与dropwhile相反,保留元素直至真值函数值为假。

>>> x = itertools.takewhile(lambda e: e < 5, range(10))
>>> print(list(x))
[0, 1, 2, 3, 4]

itertools.tee

生成指定数目的迭代器

>>> x = itertools.tee(range(10), 2)
>>> for letters in x:
...     print(list(letters))
...
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

itertools.zip_longest

类似于zip,不过已较长的列表和迭代器的长度为准

>>> x = itertools.zip_longest(range(3), range(5))
>>> y = zip(range(3), range(5))
>>> print(list(x))
[(0, 0), (1, 1), (2, 2), (None, 3), (None, 4)]
>>> print(list(y))
[(0, 0), (1, 1), (2, 2)]

Asyncio - 异步 IO

Asyncio API

所谓「异步 IO」,就是你发起一个 IO 操作,却不用等它结束,你可以继续做其他事情,当它结束时,你会得到通知。

Asyncio 是并发(concurrency)的一种方式。对 Python 来说,并发还可以通过线程(threading)和多进程(multiprocessing)来实现。

Asyncio 并不能带来真正的并行(parallelism)。当然,因为 GIL(全局解释器锁)的存在,Python 的多线程也不能带来真正的并行。

可交给 asyncio 执行的任务,称为协程(coroutine)。一个协程可以放弃执行,把机会让给其它协程(即 yield fromawait)。

1. 定义协程

协程的定义,需要使用 async def 语句。

async def do_some_work(x): pass

do_some_work 便是一个协程。
准确来说,do_some_work 是一个协程函数,可以通过 asyncio.iscoroutinefunction 来验证:

print(asyncio.iscoroutinefunction(do_some_work)) # True

这个协程什么都没做,我们让它睡眠几秒,以模拟实际的工作量 :

async def do_some_work(x):
    print("Waiting " + str(x))
    await asyncio.sleep(x)

在解释 await 之前,有必要说明一下协程可以做哪些事。协程可以:

  • 等待一个 future 结束
  • 等待另一个协程(产生一个结果,或引发一个异常)
  • 产生一个结果给正在等它的协程
  • 引发一个异常给正在等它的协程

asyncio.sleep 也是一个协程,所以 await asyncio.sleep(x) 就是等待另一个协程。可参见 asyncio.sleep 的文档:

sleep(delay, result=None, *, loop=None)
Coroutine that completes after a given time (in seconds).

2. 运行协程

调用协程函数,协程并不会开始运行,只是返回一个协程对象,可以通过 asyncio.iscoroutine 来验证:

print(asyncio.iscoroutine(do_some_work(3))) # True

此处还会引发一条警告:

async1.py:16: RuntimeWarning: coroutine 'do_some_work' was never awaited
  print(asyncio.iscoroutine(do_some_work(3)))

要让这个协程对象运行的话,有两种方式:

  • 在另一个已经运行的协程中用 await 等待它
  • 通过 ensure_future 函数计划它的执行

简单来说,只有 loop 运行了,协程才可能运行。
下面先拿到当前线程缺省的 loop ,然后把协程对象交给 loop.run_until_complete,协程对象随后会在 loop 里得到运行。

loop = asyncio.get_event_loop()
loop.run_until_complete(do_some_work(3))

run_until_complete 是一个阻塞(blocking)调用,直到协程运行结束,它才返回。这一点从函数名不难看出。
run_until_complete 的参数是一个 future,但是我们这里传给它的却是协程对象,之所以能这样,是因为它在内部做了检查,通过 ensure_future 函数把协程对象包装(wrap)成了 future。所以,我们可以写得更明显一些:

loop.run_until_complete(asyncio.ensure_future(do_some_work(3)))

完整代码:

import asyncio

async def do_some_work(x):
    print("Waiting " + str(x))
    await asyncio.sleep(x)

loop = asyncio.get_event_loop()
loop.run_until_complete(do_some_work(3))

运行结果:

Waiting 3
<三秒钟后程序结束>

3. 回调函数

假如协程是一个 IO 的读操作,等它读完数据后,我们希望得到通知,以便下一步数据的处理。这一需求可以通过往 future 添加回调来实现。

def done_callback(futu):
    print('Done')

futu = asyncio.ensure_future(do_some_work(3))
futu.add_done_callback(done_callback)

loop.run_until_complete(futu)

4. 多个协程

实际项目中,往往有多个协程,同时在一个 loop 里运行。为了把多个协程交给 loop,需要借助 asyncio.gather 函数。

loop.run_until_complete(asyncio.gather(do_some_work(1), do_some_work(3)))

或者先把协程存在列表里:

coros = [do_some_work(1), do_some_work(3)]
loop.run_until_complete(asyncio.gather(*coros))

运行结果:

Waiting 3
Waiting 1
<等待三秒钟>
Done

这两个协程是并发运行的,所以等待的时间不是 1 + 3 = 4 秒,而是以耗时较长的那个协程为准。

参考函数 gather 的文档:

gather(*coros_or_futures, loop=None, return_exceptions=False)
Return a future aggregating results from the given coroutines or futures.

发现也可以传 futures 给它:

futus = [asyncio.ensure_future(do_some_work(1)),
             asyncio.ensure_future(do_some_work(3))]

loop.run_until_complete(asyncio.gather(*futus))

gather 起聚合的作用,把多个 futures 包装成单个 future,因为 loop.run_until_complete 只接受单个 future。

5. run_until_complete和run_forever

我们一直通过 run_until_complete 来运行 loop ,等到 future 完成,run_until_complete 也就返回了。

async def do_some_work(x):
    print('Waiting ' + str(x))
    await asyncio.sleep(x)
    print('Done')

loop = asyncio.get_event_loop()

coro = do_some_work(3)
loop.run_until_complete(coro)

输出:

Waiting 3
<等待三秒钟>
Done
<程序退出>

现在改用 run_forever

async def do_some_work(x):
    print('Waiting ' + str(x))
    await asyncio.sleep(x)
    print('Done')

loop = asyncio.get_event_loop()

coro = do_some_work(3)
asyncio.ensure_future(coro)

loop.run_forever()

输出:

Waiting 3
<等待三秒钟>
Done
<程序没有退出>

三秒钟过后,future 结束,但是程序并不会退出。run_forever 会一直运行,直到 stop 被调用,但是你不能像下面这样调 stop

loop.run_forever()
loop.stop()

run_forever 不返回,stop 永远也不会被调用。所以,只能在协程中调 stop

async def do_some_work(loop, x):
    print('Waiting ' + str(x))
    await asyncio.sleep(x)
    print('Done')
    loop.stop()

这样并非没有问题,假如有多个协程在 loop 里运行:

asyncio.ensure_future(do_some_work(loop, 1))
asyncio.ensure_future(do_some_work(loop, 3))

loop.run_forever()

第二个协程没结束,loop 就停止了——被先结束的那个协程给停掉的。
要解决这个问题,可以用 gather 把多个协程合并成一个 future,并添加回调,然后在回调里再去停止 loop。

async def do_some_work(loop, x):
    print('Waiting ' + str(x))
    await asyncio.sleep(x)
    print('Done')

def done_callback(loop, futu):
    loop.stop()

loop = asyncio.get_event_loop()

futus = asyncio.gather(do_some_work(loop, 1), do_some_work(loop, 3))
futus.add_done_callback(functools.partial(done_callback, loop))

loop.run_forever()

其实这基本上就是 run_until_complete 的实现了,run_until_complete 在内部也是调用 run_forever

6. Close Loop?

以上示例都没有调用 loop.close,好像也没有什么问题。所以到底要不要调 loop.close 呢?
简单来说,loop 只要不关闭,就还可以再运行。:

loop.run_until_complete(do_some_work(loop, 1))
loop.run_until_complete(do_some_work(loop, 3))
loop.close()

但是如果关闭了,就不能再运行了:

loop.run_until_complete(do_some_work(loop, 1))
loop.close()
loop.run_until_complete(do_some_work(loop, 3))  # 此处异常

建议调用 loop.close,以彻底清理 loop 对象防止误用。

7. gather 和 wait

asyncio.gatherasyncio.wait 功能相似。

coros = [do_some_work(loop, 1), do_some_work(loop, 3)]
loop.run_until_complete(asyncio.wait(coros))

具体差别可请参见 StackOverflow 的讨论:Asyncio.gather vs asyncio.wait。

8. Timer

C++ Boost.Asio 提供了 IO 对象 timer,但是 Python 并没有原生支持 timer,不过可以用 asyncio.sleep 模拟。

async def timer(x, cb):
    futu = asyncio.ensure_future(asyncio.sleep(x))
    futu.add_done_callback(cb)
    await futu

t = timer(3, lambda futu: print('Done'))
loop.run_until_complete(t)

FuzzyWuzzy - 模糊字符串匹配

FuzzyWuzzy API

1. 引言

最常见的一个例子就是:在进行地理可视化中,自己收集的数据只保留的缩写,比如北京,广西,新疆,西藏等,但是待匹配的字段数据却是北京市,广西壮族自治区,新疆维吾尔自治区,西藏自治区等,如下。因此就需要有没有一种方式可以很快速便捷的直接进行对应字段的匹配并将结果单独生成一列,就可以用到 FuzzyWuzzy 库。

2. FuzzyWuzzy库介绍

FuzzyWuzzy 是一个简单易用的模糊字符串匹配工具包。它依据 Levenshtein Distance 算法,计算两个序列之间的差异。

Levenshtein Distance算法,又叫 Edit Distance算法,是指两个字符串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。一般来说,编辑距离越小,两个串的相似度越大。

这里使用的是Anaconda下的jupyter notebook编程环境,因此在Anaconda的命令行中输入一下指令进行第三方库安装。

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple FuzzyWuzzy

fuzz模块

该模块下主要介绍四个函数(方法),分别为:简单匹配(Ratio)、非完全匹配(Partial Ratio)、忽略顺序匹配(Token Sort Ratio)和去重子集匹配(Token Set Ratio)

注意: 如果直接导入这个模块的话,系统会提示warning,当然这不代表报错,程序依旧可以运行(使用的默认算法,执行速度较慢),可以按照系统的提示安装python-Levenshtein库进行辅助,这有利于提高计算的速度。

简单匹配(Ratio)

简单的了解一下就行,这个不怎么精确,也不常用

fuzz.ratio("河南省", "河南省")>>> 100>fuzz.ratio("河南", "河南省")>>> 80

非完全匹配(Partial Ratio)

尽量使用非完全匹配,精度较高

fuzz.partial_ratio("河南省", "河南省")
>>> 100

fuzz.partial_ratio("河南", "河南省")
>>> 100

忽略顺序匹配(Token Sort Ratio)

原理在于:以 空格 为分隔符,小写 化所有字母,无视空格外的其它标点符号

fuzz.ratio("西藏 自治区", "自治区 西藏")
>>> 50
fuzz.ratio('I love YOU','YOU LOVE I')
>>> 30

fuzz.token_sort_ratio("西藏 自治区", "自治区 西藏") 
>>> 100
fuzz.token_sort_ratio('I love YOU','YOU LOVE I') 
>>> 100

去重子集匹配(Token Set Ratio)

相当于比对之前有一个集合去重的过程,注意最后两个,可理解为该方法是在token_sort_ratio方法的基础上添加了集合去重的功能,下面三个匹配的都是倒序。

fuzz.ratio("西藏 西藏 自治区", "自治区 西藏")
>>> 40

fuzz.token_sort_ratio("西藏 西藏 自治区", "自治区 西藏")
>>> 80

fuzz.token_set_ratio("西藏 西藏 自治区", "自治区 西藏")
>>> 100

fuzz这几个ratio()函数(方法)最后得到的结果都是数字,如果需要获得匹配度最高的字符串结果,还需要依旧自己的数据类型选择不同的函数,然后再进行结果提取,如果但看文本数据的匹配程度使用这种方式是可以量化的,但是对于我们要提取匹配的结果来说就不是很方便了,因此就有了process模块。

process模块

用于处理备选答案有限的情况,返回模糊匹配的字符串和相似度。

extract提取多条数据

类似于爬虫中select,返回的是列表,其中会包含很多匹配的数据

choices = ["河南省", "郑州市", "湖北省", "武汉市"]
process.extract("郑州", choices, limit=2)
>>> [('郑州市', 90), ('河南省', 0)]
# extract之后的数据类型是列表,即使limit=1,最后还是列表,注意和下面extractOne的区别

extractOne提取一条数据

如果要提取匹配度最大的结果,可以使用extractOne,注意这里返回的是 元组 类型, 还有就是匹配度最大的结果不一定是我们想要的数据,可以通过下面的示例和两个实战应用体会一下。

process.extractOne("郑州", choices)
>>> ('郑州市', 90)

process.extractOne("北京", choices)
>>> ('湖北省', 45)

3. 实战应用

这里举两个实战应用的小例子,第一个是公司名称字段的模糊匹配,第二个是省市字段的模糊匹配

公司名称字段模糊匹配

数据及待匹配的数据样式如下:自己获取到的数据字段的名称很简洁,并不是公司的全称,因此需要进行两个字段的合并

直接将代码封装为函数,主要是为了方便日后的调用,这里参数设置的比较详细,执行结果如下:

参数讲解:

① 第一个参数df_1是自己获取的欲合并的左侧数据(这里是data变量);

② 第二个参数df_2是待匹配的欲合并的右侧数据(这里是company变量);

③ 第三个参数key1是df_1中要处理的字段名称(这里是data变量里的‘公司名称’字段)

④ 第四个参数key2是df_2中要匹配的字段名称(这里是company变量里的‘公司名称’字段)

⑤ 第五个参数threshold是设定提取结果匹配度的标准。注意这里就是对extractOne方法的完善,提取到的最大匹配度的结果并不一定是我们需要的,所以需要设定一个阈值来评判,这个值就为90,只有是大于等于90,这个匹配结果我们才可以接受

⑥ 第六个参数,默认参数就是只返回两个匹配成功的结果

⑦ 返回值:为df_1添加‘matches’字段后的新的DataFrame数据

核心代码讲解

第一部分代码如下,可以参考上面讲解process.extract方法,这里就是直接使用,所以返回的结果m就是列表中嵌套元祖的数据格式,样式为: [(‘郑州市’, 90), (‘河南省’, 0)],因此第一次写入到’matches’字段中的数据也就是这种格式

注意,注意: 元祖中的第一个是匹配成功的字符串,第二个就是设置的threshold参数比对的数字对象

s = df_2[key2].tolist()
m = df_1[key1].apply(lambda x: process.extract(x, s, limit=limit))    
df_1['matches'] = m

第二部分的核心代码如下,有了上面的梳理,明确了‘matches’字段中的数据类型,然后就是进行数据的提取了,需要处理的部分有两点需要注意的:

① 提取匹配成功的字符串,并对阈值小于90的数据填充空值

② 最后把数据添加到‘matches’字段


m2 = df_1['matches'].apply(lambda x: [i[0] for i in x if i[1] >= threshold][0] if len([i[0] for i in x if i[1] >= threshold]) > 0 else '')
#要理解第一个‘matches’字段返回的数据类型是什么样子的,就不难理解这行代码了
#参考一下这个格式:[('郑州市', 90), ('河南省', 0)]
df_1['matches'] = m2

return df_1

省份字段模糊匹配

自己的数据和待匹配的数据背景介绍中已经有图片显示了,上面也已经封装了模糊匹配的函数,这里直接调用上面的函数,输入相应的参数即可,代码以及执行结果如下:

数据处理完成,经过封装后的函数可以直接放在自己自定义的模块名文件下面,以后可以方便直接导入函数名即可,可以参考将自定义常用的一些函数封装成可以直接调用的模块方法。

4. 全部函数代码

#模糊匹配

def fuzzy_merge(df_1, df_2, key1, key2, threshold=90, limit=2):
    """
    :param df_1: the left table to join
    :param df_2: the right table to join
    :param key1: key column of the left table
    :param key2: key column of the right table
    :param threshold: how close the matches should be to return a match, based on Levenshtein distance
    :param limit: the amount of matches that will get returned, these are sorted high to low
    :return: dataframe with boths keys and matches
    """
    s = df_2[key2].tolist()

    m = df_1[key1].apply(lambda x: process.extract(x, s, limit=limit))    
    df_1['matches'] = m

    m2 = df_1['matches'].apply(lambda x: [i[0] for i in x if i[1] >= threshold][0] if len([i[0] for i in x if i[1] >= threshold]) > 0 else '')
    df_1['matches'] = m2

    return df_1

from fuzzywuzzy import fuzz
from fuzzywuzzy import process

df = fuzzy_merge(data, company, '公司名称', '公司名称', threshold=90)
df

PIL - 图像处理开发极简教程

Pillow API

PIL安装与介绍

PIL是python image libaray缩写,是python语言的图像处理库,很多时候对于一些简单的图像处理,我们经常使用的是其中一个fork项目pillow,它的github地址如下:

https://github.com/python-pillow/Pillow

安装pillow只需要执行如下命令行即可

pip install Pillow

常见操作代码

  • 导入开发包
from PIL import Image, ImageFilter, ImageOps
import numpy as np
  • 读取与显示图像
im = Image.open('D:/images/flower.png')
im.show()
  • 获取图像基本属性
print(im.format, im.size, im.mode)
  • 访问像素
print(im.getpixel((256, 256)))
  • 转为灰度图像
new_im = im.convert('L')
new_im.show()
  • 高斯模糊图像
new_im = im.filter(ImageFilter.GaussianBlur())
new_im.show()
  • 保存图像
new_im.save('D:/my_test.jpg', quality=95)
  • 创建512x512大小,蓝色图像
im = Image.new("RGB", (512, 512), (0, 0, 255))
im.show()
  • 填充直线、长方形与椭圆
draw.line((0, im.height, im.width, 0), fill=(255, 0, 0), width=8)
draw.rectangle((100, 100, 200, 200), fill=(0, 255, 0))
draw.ellipse((250, 300, 450, 400), fill=(0, 255, 255))
  • 像素取反操作
im = Image.open('D:/images/flower.png')
im_invert = ImageOps.invert(im)
im_invert.save('D:/flower_invert.png') 
  • 图像旋转操作
im = Image.open('D:/images/flower.png')
im_rotate = im.rotate(90)
im_rotate .save('D:/flower_rotate.png') 
  • 模糊
im = Image.open('D:/images/master.jpg')
blur = im.filter(ImageFilter.BLUR)
blur.show()
  • 轮廓提取
contour = im.filter(ImageFilter.CONTOUR)
contour.show()
  • 轧花效果
emboss = im.filter(ImageFilter.EMBOSS)
emboss.save('D:/emboss.png')
  • 边缘查找
edges = im.filter(ImageFilter.FIND_EDGES)
edges.save('D:/edges.png')
  • 图像边缘增强
edges_enhance = im.filter(ImageFilter.EDGE_ENHANCE)
edges_enhance.save('D:/edges_enhance.png')
  • 图像锐化增强
sharpen = im.filter(ImageFilter.SHARPEN)
sharpen.save('D:/sharpen.png')
  • USM锐化增强
usm = im.filter(ImageFilter.UnsharpMask)
usm.save('D:/usm.png')

Python-docx - 读写Word文档入门

Python-docx API

安装模块

这里我们用到的是python-docx模块,读写Word文档的操作均有它完成!

# 安装该模块,在命令行键入以下命令
pip install python-docx

在进行后续介绍前,我们先简单了解一下Word文档。和纯文本(比如txt)相比, .docx文件有很多种结构,这些结构在python-docx中用3种不同的类型来表示:最高一层是Document对象表示文档,每个Document对象包含一个Paragraph 对象也就是段落组成的列表,而每个Paragraph对象则包含一个Run对象的列表,至于Run对象大家可以通过下面的段落Paragraph来了解。

多个Run对象组成的Paragraph

我们知道Word里的文本包含有很多格式,比如字体、字号、粗体/斜体、颜色等等。一个Run对象是具有相同格式的文本,当发生变化的时候就需要一个新的Run对象,这也就是上图中1个Paragraph对象有4个Run对象的原因。

读取Word文档

我们本地创建一个案例文档,用于演示读取Word,案例文档内容如下:

可以看到文档一共有四行,两行标题以及两行正文。

import docx
# 读取Word文档
doc = docx.Document(r'案例.docx')

我们知道了读取Word每个paragraph段落和Run,那么如何读取完整的Word文本内容呢?这里,自定义一个函数将全部的paragraph段落内容存起来,每个paragraph段落之间用换行符\n隔开即可。

import docx

def getText(fileName):
    doc = docx.Document(fileName)
    TextList = []
    for paragraph in doc.paragraphs:
        TextList.append(paragraph.text)

    return '\n'.join(TextList)

我们调用该函数,可以得到:

fileName = r'案例.docx'
print(getText(fileName))

>>>标题1
>>>这是一个段落,有粗体和斜体
>>>大家好,我是才哥。
>>>标题2

当然了,这里读取后输出显示的文本不带有格式属性。

写入Word文档

在写入Word之前,我们先简单了解下Word的一些格式规则。

比如,我们可以设置正文样式为 微软雅黑字体,字号12,缩进,间距等等;设置标题样式为 微软雅黑字体,字号14,删除段落后间距等等。

在我们Python处理Word的时候,段落样式可以应用于 Paragraph 对象,字符样式可以应用于 Run 对象,链接的样式可以应用于这两种对象。可以将 ParagraphRun 对象的 style 属性设置为一个字符串,从而设置样式。这个字符串应该是一种样式的名称。如果 style 被设置为 None,就没有样式与 ParagraphRun 对象关联。

当然,这里我们不展开介绍,就简单介绍Run对象上的字符属性

对于Run对象的字符text属性,都有3个状态:True(启用)、False(禁用)和None(默认)。

text属性有哪些?看下表:

属性 描述
bold 文本以粗体出现
italic 文本以斜体出现
underline 文本带下划线
strike 文本带删除线
double_strike 文本带双删除线
all_caps 文本以大写首字母出现
small_caps 文本以大写首字母出现,小写字母小两个点
shadow 文本带阴影
outline 文本以轮廓线出现,而不是实心
rtl 文本从右至左书写
imprint 文本以刻入页面的方式出现
emboss 文本以凸出页面的方式出现

我们演示一下:

以上就是对段落的一些简单介绍及演示,接下来,我们来看看怎么创建并写入Word

要创建自己的.docx 文件,就调用 docx.Document(),返回一个新的、空白的 Word Document 对象 。Document 对象的 add_paragraph()方法将一段新文本添加到文档中,并返回添加的 Paragraph 对象的引用。在添加完文本之后,向 Document 对象的 save()方法传入一个文件名字符串,将 Document 对象保存到文件。

import docx

doc = docx.Document()
doc.add_paragraph('这是一个段落')
doc.save('写入word.docx')

我们可以添加段落、设置段落文本样式等等,比如:

import docx

doc = docx.Document()
p1 = doc.add_paragraph('这是一个段落')
p1.add_run('加粗的一句话').bold = True
doc.add_paragraph('这是第二个段落')
doc.save('多段落.docx')

我们还可以添加一个段位,这个段落为标题样式,这里需要用到add_heading()。它有两个参数,字符串表示文本内容,后面数字是标题层级。

import docx

doc = docx.Document()
doc.add_heading('标题',0)
doc.add_heading('标题1',1)
doc.add_heading('标题2',2)
doc.add_heading('标题3',3)
doc.add_heading('标题4',4)
doc.save('标题.docx')

同样,我们还可换行与换页操作。

要添加换行符(而不是开始一个新的段落),可以在 Run 对象上调用 add_break()方法,换行符将出现在它后面。如果希望添加换页符,可以将 docx.text.WD_BREAK.PAGE作为唯一的参数,传递给 add_break()

import docx

doc = docx.Document()
p1 = doc.add_paragraph('这是一个段落')
p1.add_run('加粗的一句话').bold = True
# 换行
doc.paragraphs[0].runs[0].add_break()
doc.save('换行.docx')

有人会说,一般word里还有图片嘛,当然咱们也能搞定添加图片

有一个 add_picture()方法,可以在段落末尾添加图像 。

import docx

doc = docx.Document()
doc.add_paragraph('这是一个段落,后面带图片')
doc.add_picture('图片.png', 
                width=docx.shared.Inches(3),
                height=docx.shared.Cm(4))
doc.add_paragraph('这是第二个段落')
doc.save('图片.docx')

Scrapy - 爬虫框架

Scrapy API

1. 安装

使用pip对Scrapy进行安装,代码如下:

pip install scrapy

2. 创建项目

安装好Scrapy框架之后,我们需要通过终端,来创建一个Scrapy项目,命令如下:

scrapy startproject weibo

创建好后的项目结构,如下图:

spiders是存放爬虫程序的文件夹,将写好的爬虫程序放到该文件夹中。items用来定义数据,类似于字典的功能。settings是设置文件,包含爬虫项目的设置信息。pipelines用来对items中的数据进行进一步处理,如:清洗、存储等。

3. 数据采集

经过上面的简单介绍,我们现在对Scrapy框架有了简单的了解,下面我们开始写数据采集部分的代码。

定义数据

首先,我们对数据存储的网页进行观察,方便我们对获取数据进行定义

通过对网页中数据存储的形式进行观察后,items.py中对数据的定义方式为:

data = scrapy.Field()

编辑爬虫

接下来我们在spiders文件夹里面创建一个weibo.py爬虫程序用以书写请求的爬虫代码

代码如下:

import scrapy
class WeiboSpider(scrapy.Spider):
   name = 'weibo'#用于启动微博程序
   allowed_domains = ['m.weibo.cn']  #定义爬虫爬取网站的域名
   start_urls = ['https://m.weibo.cn/comments/hotflow?id=4700480024348767&mid=4700480024348767&max_id_type=0']  #定义起始网页的网址
  for i in res['data']['data']:
     weibo_item = WeiboItem()
     weibo_item['data'] = re.sub(r'<[^>]*>', '', i['text'])
     # start_url = ['https://m.weibo.cn/comments/hotflow?id=4700480024348767&mid=4700480024348767&'+str(max_id)+'&max_id_type=0']
     yield weibo_item  #将数据回传给items

遍历爬取

构造代码:

max_id_type = res['data']['max_id_type']
if int(max_id_type) == 1:
   new_url = 'https://m.weibo.cn/comments/hotflow?id=4700480024348767&mid=4700480024348767&max_id=' + str(
       max_id) + '&max_id_type=1'
else:
   new_url = 'https://m.weibo.cn/comments/hotflow?id=4700480024348767&mid=4700480024348767&max_id=' + str(
       max_id) + '&max_id_type=0'

4. 数据存储

光爬取下来数据是不行的,我们还需要对数据进行存储,这里采用的是csv文件,来对评论数据进行存储,代码如下:

class CsvItemExporterPipeline(object):
   def __init__(self):
       # 创建接收文件,初始化exporter属性
       self.file = open('text.csv','ab')
       self.exporter = CsvItemExporter(self.file,fields_to_export=['data'])
       self.exporter.start_exporting()

5. 程序配置

光写上面的代码是无法爬取到评论的,因为我们还没有对整个程序进行有效的配置,下面我们就在settings.py里面进行配置。

不遵循robots协议

需要对robts协议的遵守进行修改,如果我们遵循网页的robots协议的话,那无法进行爬取,代码如下:

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

使用自定义cookie

我们知道,想要爬取微博评论,需要带上自己的cookie用以信息校验,因为我们的cookie是在headers中包裹着的,所以我们需要将COOKIES_ENABLED改为False,代码如下:

# Disable cookies (enabled by default)
COOKIES_ENABLED = False

打开管道

想要进行数据存储,还需要在配置中,打开通道,用以数据传输,代码如下:

# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
  'weibo.pipelines.CsvItemExporterPipeline': 1,
  'weibo.pipelines.WeiboPipeline': 300,
}

启动程序

我们在spiders同级的的目录下创建一个wb_main.py文件,便于我们在编辑器中启动程序,代码如下:

from scrapy import cmdline
#导入cmdline模块,可以实现控制终端命令行。
cmdline.execute(['scrapy','crawl','weibo'])
#用execute()方法,输入运行scrapy的命令。

Shodan - 互联网上最可怕的搜索引擎

Shodan API

Shodan 在百度百科里被给出了这么一句话介绍:Shodan是互联网上最可怕的搜索引擎。

为什么呢?与谷歌、百度等搜索引擎爬取网页信息不同,Shodan爬取的是互联网上所有设备的IP地址及其端口号。

而随着智能家电的普及,家家户户都有许多电器连接到互联网,这些设备存在被入侵的可能性,这是十分危险的。打开 shodan.io,在搜索框输入 Hikvision-Webs :

你会搜素到这个品牌的摄像头设备遍及全球的IP及其暴露的端口号:

可以看到,这台机器暴露了17、80、111、995、3128、5000、6000、20547端口,黑客可以根据这些端口进行针对性的攻击。

不过也不需要过于担心,如果你的服务不存在漏洞,一般是无法攻入的。但有些端口号会暴露摄像头的web管理端,如下:

那么黑客可能可以用暴力破解的方式,强行进入摄像头后台管理端,获取到实时的录像。

谨记这会侵犯别人的隐私权,是违法的行为,我们是遵纪守法的好公民所以知道它的原理和危害就足够。我们的目的是运用技术保护好个人隐私,如非必要不将摄像头接入互联网,一定要接入的话,不能使用容易被破解的弱口令。

Shodan Web端非常好用,但如果我们有从 Python 搜索的需求怎么办?

没关系,Shodan 官方也提供了 Python SDK 包,下面就来讲讲这个 SDK 包的使用。

安装

请选择以下任一种方式输入命令安装依赖

  1. Windows 环境 打开 Cmd (开始-运行-CMD)。
  2. MacOS 环境 打开 Terminal (command+空格输入Terminal)。
  3. 如果你用的是 VSCode编辑器 或 Pycharm,可以直接使用界面下方的Terminal.
pip install shodan

注册账号获取API

使用 Shodan 必须注册账号,注册网址:https://account.shodan.io/register

输入完相关信息,点击 CREATE 会跳转到个人账户页:

此时 API Key 会显示你的API秘钥,请记录这个秘钥,后续会使用到这个秘钥去请求接口。

Shodan 基本调用

Shodan 本质上就是一个搜索引擎,你只需要输入搜索的关键词:

from shodan import Shodan

api = Shodan('你的API KEY')

def search_shodan(keyword):
    # 调用搜索接口
    result = api.search(keyword)

    # 显示所有IP
    for service in result['matches']:
            print(service['ip_str'])

search_shodan("Hikvision-Webs")

结果如下:

可惜的是,普通API只能像这样搜索关键字,无法使用过滤条件如: Hikvision-Webs country:"US" 搜索美国境内的所有 Hikvision 网站管理端。

如果你想要使用过滤条件,Shodan 需要你升级API权限:

Shodan 高级使用

Shodan 的用处当然不仅仅是在黑客攻防中,它还能用于统计。如果你想要了解哪些国家的使用这款摄像头的数量最多,可以使用 Facets 特性。

from shodan import Shodan

api = Shodan('你的API KEY')
def try_facets(query):
    FACETS = [
        'org',
        'domain',
        'port',
        'asn',
        ('country', 3),
    ]

    FACET_TITLES = {
        'org': 'Top 5 Organizations',
        'domain': 'Top 5 Domains',
        'port': 'Top 5 Ports',
        'asn': 'Top 5 Autonomous Systems',
        'country': 'Top 3 Countries',
    }

    try:
        # 使用 count() 方法可以不需要升级API,且比 search 方法更快。
        result = api.count(query, facets=FACETS)

        print('Shodan Summary Information')
        print('Query: %s' % query)
        print('Total Results: %s\n' % result['total'])

        # 显示每个要素的摘要
        for facet in result['facets']:
            print(FACET_TITLES[facet])

            for term in result['facets'][facet]:
                print('%s: %s' % (term['value'], term['count']))

    except Exception as e:
        print('Error: %s' % e)

try_facets("Hikvision-Webs")

得到结果如下:

从 Top 3 Countries 中可以看到,这款摄像头使用数量排名前三的国家分别是:美国、日本和德国。

没想到吧,Shodan 居然还能用于产品分析。同样地原理,如果你把关键词改为 apache ,你可以知道目前哪些国家使用apache服务器数量最多,最普遍被使用的版本号是什么。

为了避免受到不必要的攻击,请大家及时检查所有联网设备的管理端的密码,如果有使用默认密码及弱口令,立即进行密码的更改,以保证服务的安全。

Gooey - GUI神器

Gooey API

安装

安装方法毫无新意,跟以前我们安装其他库一样:

pip install Gooey

简单示例

from gooey import Gooey, GooeyParser


@Gooey(program_name="简单的实例")
def main():
    parser = GooeyParser(description="第一个示例!")
    parser.add_argument('文件路径', widget="FileChooser")      # 文件选择框
    parser.add_argument('日期', widget="DateChooser")          # 日期选择框
    args = parser.parse_args()                                 # 接收界面传递的参数
    print(args)


if   __name__ == '__main__':
    main()

这里看代码应该大致知道界面有些什么控件:文件选择框、日期选择框。

运行之后的效果如下:

组件

GUI 界面都是由一个个组件排列组成的,上面的例子中我们使用了 FileChooser 和 DateChooser 这两个组件,使用起来是不是很简单?我们来看看 Gooey 还有哪些组件。

控件名 控件类型
FileChooser 文件选择器
MultiFileChooser 文件多选器
DirChooser 目录选择器
MultiDirChooser 目录多选器
FileSaver 文件保存
DateChooser 日期选择
TextField 文本输入框
Dropdown 下拉列表
Counter 计数器
CheckBox 复选框
RadioGroup 单选框

这些组件基本上看名称就知道是什么样子的了,都是我们平时常用的页面组件。

配置

配置参数主要是对Gooey界面做全局配置,配置方法如下:

@Gooey(program_name='全局配置Demo')
def main():
    ...

这里就使用了 program_name 这个配置,主要是配置显示程序的名称。

除此之外,Gooey 还有好多其他的配置参数:

参数 类型 简介
advanced Boolean 切换显示全部设置还是仅仅是简化版本
show_config Boolean 跳过所有配置并立即运行程序
language str 指定从 gooey/languages 目录读取哪个语言包
program_name str GUI 窗口显示的程序名。默认会显 sys.argv[0]。
program_description str Settings 窗口顶栏显示的描述性文字。默认值从 ArgumentParser 中获取。
default_size tuple 窗口默认大小,(600,400)
required_cols int 设置必选参数行数。
optional_cols int 设置可选参数行数。
dump_build_config Boolean 将设置以 JSON 格式保存在硬盘中以供编辑/重用。
richtext_controls Boolean 打开/关闭控制台对终端控制序列的支持(对字体粗细和颜色的有限支持)

比如我们上面的例子中除了我们自己设置的,其他的控件例如选择按钮、确定按钮等默认都是英文的,我们可以通过 language 参数来设置语言。语言包在 Gooey 的 GitHub 源码中有:

我们使用语言配置:

from gooey import Gooey, GooeyParser


@Gooey(program_name="简单的实例", language='chinese')
def main():
    parser = GooeyParser(description="第一个示例!")
    parser.add_argument('文件路径', widget="FileChooser")      # 文件选择框
    parser.add_argument('日期', widget="DateChooser")          # 日期选择框
    args = parser.parse_args()                                 # 接收界面传递的参数
    print(args)


if   __name__ == '__main__':
    main()

这里加了一个 language 参数,我们看看运行后效果:

结构布局

通过使用一些简单的自定义,您可以使用Gooey实现相当灵活的布局。我们可以通过Gooey装饰器的各种参数来控制总体布局选项。

  • show_sidebar=True

  • show_sidebar=False

  • navigation=’TABBED’

  • tabbed_groups=True

实例

我们通过一个小栗子来看看运行的效果吧。

from gooey import Gooey, GooeyParser


@Gooey(
    richtext_controls=True,                 # 打开终端对颜色支持
    program_name="MQTT连接订阅小工具",        # 程序名称
    encoding="utf-8",                       # 设置编码格式,打包的时候遇到问题
    progress_regex=r"^progress: (\d+)%$"    # 正则,用于模式化运行时进度信息
)
def main():
    settings_msg = 'MQTT device activation information subscription'
    parser = GooeyParser(description=settings_msg)

    subs = parser.add_subparsers(help='commands', dest='command')

    my_parser = subs.add_parser('MQTT消息订阅')
    my_parser.add_argument("connect", metavar='运行环境',help="请选择开发环境",choices=['dev环境','staging环境'], default='dev环境')
    my_parser.add_argument("device_type",metavar='设备类型',help="请选择设备类型",choices=['H1','H3'],default='H1')
    my_parser.add_argument("serialNumber", metavar='设备SN号',default='LKVC19060047',help='多个请用逗号或空格隔开')

    siege_parser = subs.add_parser('进度条控制')
    siege_parser.add_argument('num',help='请输入数字',default=100)

    args = parser.parse_args()
    print(args, flush=True)    # flush=True在打包的时候会用到


if __name__ == '__main__':
    main()

这个例子运行的效果如下:

打包成应用程序

第一步,先安装我们的打包库:

pip install pyinstaller

第二步,执行打包命令:

pyinstaller -F gooeydemo.py -w

等待执行完成,我们就可以在代码目录下看到这样的结构:

我们的 exe 可执行文件就在 dist 文件夹下。

Prettytable - 输出好看的表格

Prettytable API

1. 前言

最近在用 Python 写一个小工具,这个工具主要就是用来管理各种资源的信息,比如阿里云的 ECS 等信息,因为我工作的电脑使用的是 LINUX,所以就想着用 python 写一个命令行的管理工具,基本的功能就是同步阿里云的资源的信息到数据库,然后可以使用命令行查询。

因为信息是展现在命令行中的,众所周知,命令行展现复杂的文本看起来着实累人,于是就想着能像表格那样展示,那看起来就舒服多了。

prettytable 库就是这么一个工具,prettytable 可以打印出美观的表格,并且对中文支持相当好(如果有试图自己实现打印表格,你就应该知道处理中文是多么的麻烦)

2. 安装

prettytable 并非 python 的内置库,通过 pip install prettytable即可安装。

3. 示例

我们先来看一个示例:

#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
reload(sys)
sys.setdefaultencoding('utf8')

table = PrettyTable(['编号','云编号','名称','IP地址'])
table.add_row(['1','server01','服务器01','172.16.0.1'])
table.add_row(['2','server02','服务器02','172.16.0.2'])
table.add_row(['3','server03','服务器03','172.16.0.3'])
table.add_row(['4','server04','服务器04','172.16.0.4'])
table.add_row(['5','server05','服务器05','172.16.0.5'])
table.add_row(['6','server06','服务器06','172.16.0.6'])
table.add_row(['7','server07','服务器07','172.16.0.7'])
table.add_row(['8','server08','服务器08','172.16.0.8'])
table.add_row(['9','server09','服务器09','172.16.0.9'])
print(table)

以上示例运行结果如下:

linuxops@deepin:~$ python p.py
+------+----------+----------+------------+
| 编号 |  云编号  |   名称   |   IP地址   |
+------+----------+----------+------------+
|  1   | server01 | 服务器01 | 172.16.0.1 |
|  2   | server02 | 服务器02 | 172.16.0.2 |
|  3   | server03 | 服务器03 | 172.16.0.3 |
|  4   | server04 | 服务器04 | 172.16.0.4 |
|  5   | server05 | 服务器05 | 172.16.0.5 |
|  6   | server06 | 服务器06 | 172.16.0.6 |
|  7   | server07 | 服务器07 | 172.16.0.7 |
|  8   | server08 | 服务器08 | 172.16.0.8 |
|  9   | server09 | 服务器09 | 172.16.0.9 |
+------+----------+----------+------------+

在以上的示例中,我们通过form导入了表格库。table实例化了一个表格库,并且添加了['编号','云编号','名称','IP地址']为表头,如果没有添加表头,那么会以默认的Field+编号显示,例如:

+---------+----------+----------+------------+
| Field 1 | Field 2  | Field 3  |  Field 4   |
+---------+----------+----------+------------+

所以为更直观看出每一列的意义,还是要添加表头的。

4. 添加数据

prettytable提供了多种的添加数据的方式,最常用的应该就是按行按列添加数据了。

按行添加数据 table.add_row

在上面简单的示例中,我们就是按行添加数据的。

添加的数据必须要是列表的形式,而且数据的列表长度要和表头的长度一样。在实际的使用中,我们应该要关注到添加的数据是否和表头对应,这一点很重要。

按列添加数据 table.add_column()

看下面的示例:

#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
reload(sys)
sys.setdefaultencoding('utf8')

table = PrettyTable()
table.add_column('项目', ['编号','云编号','名称','IP地址'])
table.add_column('值', ['1','server01','服务器01','172.16.0.1'])
print(table)

运行结果如下:

+-------+--------+------------+
| index | 项目 |    值     |
+-------+--------+------------+
|   1   |  编号  |     1      |
|   2   | 云编号 |  server01  |
|   3   |  名称  |  服务器01   |
|   4   | IP地址 | 172.16.0.1 |
+-------+--------+------------+

以上示例中,我们通过add_column来按列添加数据,按列添加数据不需要在实例化表格的时候制定表头,它的表头是在添加列的时候指定的。

table.add_column('项目', ['编号','云编号','名称','IP地址']) 这一行代码为例,项目指定了这个列的表头名为”项目”,['编号','云编号','名称','IP地址']为列的值,同样为列表。

从csv文件添加数据

PrettyTable不仅提供了手动按行按列添加数据,也支持直接从csv文件中读取数据。

#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
from prettytable import from_csv
reload(sys)
sys.setdefaultencoding('utf8')

table = PrettyTable()
fp = open("res.csv", "r")
table = from_csv(fp)
print(table)
fp.close()

如果要读取cvs文件数据,必须要先导入from_csv,否则无法运行。上面的示例运行结果如下:

PS:csv 文件不能通过 xls 直接重命名得到,会报错。如果是 xls 文件,请用另存为 csv 获得 csv 文件

从sql查询值添加

从数据库查询出来的数据可以直接导入到表格打印,下面的例子使用了sqlite3,如果使用的是mysql也是一样的,只要能查询到数据就能导入到表格中。

#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
from prettytable import from_db_cursor
import sqlite3
reload(sys)
sys.setdefaultencoding('utf8')

conn = sqlite3.connect("/tmp/aliyun.db")
cur = conn.cursor()
cur.execute("SELECT * FROM res")
table = from_db_cursor(cur)
print(table)

运行结果如下:

+------+----------+----------+------------+
| 编号 |  云编号  |   名称   |   IP地址   |
+------+----------+----------+------------+
|  1   | server01 | 服务器01 | 172.16.0.1 |
|  2   | server02 | 服务器02 | 172.16.0.2 |
|  3   | server03 | 服务器03 | 172.16.0.3 |
|  4   | server04 | 服务器04 | 172.16.0.4 |
|  5   | server05 | 服务器05 | 172.16.0.5 |
|  6   | server06 | 服务器06 | 172.16.0.6 |
|  7   | server07 | 服务器07 | 172.16.0.7 |
|  8   | server08 | 服务器08 | 172.16.0.8 |
|  9   | server09 | 服务器09 | 172.16.0.9 |
+------+----------+----------+------------+

从HTML导入数据

支持从html的表格中导入,请看下面这个例子:

#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
from prettytable import from_html
reload(sys)
sys.setdefaultencoding('utf8')

html_string='''<table>
<tr>
<th>编号</th>
<th>云编号</th>
<th>名称</th>
<th>IP地址</th>
</tr>
<tr>
<td>1</td>
<td>server01</td>
<td>服务器01</td>
<td>172.16.0.1</td>
</tr>
<tr>
<td>2</td>
<td>server02</td>
<td>服务器02</td>
<td>172.16.0.2</td>
</tr>
</table>'''

table = from_html(html_string)

print(table[0])

运行结果如下:

+------+----------+----------+------------+
| 编号 |  云编号  |   名称   |   IP地址   |
+------+----------+----------+------------+
|  1   | server01 | 服务器01 | 172.16.0.1 |
|  2   | server02 | 服务器02 | 172.16.0.2 |
+------+----------+----------+------------+

如上示例中,我们可以导入html的表格,但是不一样的地方是print语句,使用html表格导入数据的时候print的必须是列表中的第一个元素,否则有可能会报[<prettytable.PrettyTable object at 0x7fa87feba590>]这样的错误。

这是因为table并不是PrettyTable对象,而是包含单个PrettyTable对象的列表,它通过解析html而来,所以无法直接打印table,而需要打印table[0]

5. 表格输出格式

正如支持多种输入一样,表格的输出也支持多种格式,我们在上面中的例子中已经使用了print的方式输出,这是一种常用的输出方式。

print

直接通过print打印出表格。这种方式打印出的表格会带边框。

输出HTML格式的表格

print(table.get_html_string())可以打印出html标签的表格。

在上面的例子中,使用print(table.get_html_string())会打印出如下结果:

<table>
    <tr>
        <th>编号</th>
        <th>云编号</th>
        <th>名称</th>
        <th>IP地址</th>
    </tr>
    <tr>
        <td>1</td>
        <td>server01</td>
        <td>服务器01</td>
        <td>172.16.0.1</td>
    </tr>
    <tr>
        <td>2</td>
        <td>server02</td>
        <td>服务器02</td>
        <td>172.16.0.2</td>
    </tr>
</table>

6. 选择性输出

prettytable在创建表格之后,你依然可以有选择的输出某些特定的行.

输出指定的列

print table.get_string(fields=["编号", "IP地址"])可以输出指定的列

输出前两行

通过print(table.get_string(start = 0, end = 2))的可以打印出指定的列,当然startend参数让我可以自由控制显示区间。当然区间中包含start不包含end,是不是很熟悉这样的用法?

根据输出指定行列的功能,我们可以同时指定行和列来输出,这里就不说明了。

将表格切片

从上面的输出区间,我们做一个大胆的假设,既然区间包含start不包含end这种规则和切片的一样,我们可以不可通过切片来生成一个新的表格然后将其打印。

事实上是可以的。

new_table = table[0:2]
print(new_table)

如上代码段中,我们就可以打印出0到1行共2行的表格,python的切片功能异常强大,配合切片我们可以自由的输入任意的行。

输出排序

有时候我们需要对输出的表格进行排序,使用print table.get_string(sortby="编号", reversesort=True)就可以对表格进行排序,其中reversesort指定了是否倒序排序,默认为False,即默认正序列排序。

sortby指定了排序的字段。

7. 表格的样式

内置样式

通过set_style()可以设置表格样式,prettytable内置了多种的样式个人觉得MSWORD_FRIENDLYPLAIN_COLUMNSDEFAULT 这三种样式看起来比较清爽,在终端下显示表格本来看起就很累,再加上一下花里胡哨的东西看起来就更累。

除了以上推荐的三种样式以外,还有一种样式不得不说,那就是RANDOM,这是一种随机的样式,每一次打印都会在内置的样式中随机选择一个,比较好玩。

具体内置了几种样式,请各位参考官网完整自己尝试输出看看。

#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable
from prettytable import MSWORD_FRIENDLY
from prettytable import PLAIN_COLUMNS
from prettytable import RANDOM
from prettytable import DEFAULT

reload(sys)
sys.setdefaultencoding('utf8')

table = PrettyTable(['编号','云编号','名称','IP地址'])
table.add_row(['1','server01','服务器01','172.16.0.1'])
table.add_row(['3','server03','服务器03','172.16.0.3'])
table.add_row(['2','server02','服务器02','172.16.0.2'])
table.add_row(['9','server09','服务器09','172.16.0.9'])
table.add_row(['4','server04','服务器04','172.16.0.4'])
table.add_row(['5','server05','服务器05','172.16.0.5'])
table.add_row(['6','server06','服务器06','172.16.0.6'])
table.add_row(['8','server08','服务器08','172.16.0.8'])
table.add_row(['7','server07','服务器07','172.16.0.7'])
table.set_style(DEFAULT)

print(table)

自定义样式

除了内置的样式以外,PrettyTable也提供了用户自定义,例如对齐方式,数字输出格式,边框连接符等等

设置对齐方式

align提供了用户设置对齐的方式,值有lrc方便代表左对齐,右对齐和居中 如果不设置,默认居中对齐。

控制边框样式

在PrettyTable中,边框由三个部分组成,横边框,竖边框,和边框连接符(横竖交叉的链接符号)

如下示例:

#!/usr/bin/python
#**coding:utf-8**
import sys
from prettytable import PrettyTable

reload(sys)
sys.setdefaultencoding('utf8')

table = PrettyTable(['编号','云编号','名称','IP地址'])
table.add_row(['1','server01','服务器01','172.16.0.1'])
table.add_row(['3','server03','服务器03','172.16.0.3'])
table.add_row(['2','server02','服务器02','172.16.0.2'])
table.add_row(['9','server09','服务器09','172.16.0.9'])
table.add_row(['4','server04','服务器04','172.16.0.4'])
table.add_row(['5','server05','服务器05','172.16.0.5'])
table.add_row(['6','server06','服务器06','172.16.0.6'])
table.add_row(['8','server08','服务器08','172.16.0.8'])
table.add_row(['7','server07','服务器07','172.16.0.7'])
table.align[1] = 'l'

table.border = True
table.junction_char='$'
table.horizontal_char = '+'
table.vertical_char = '%'

print(table)

table.border控制是否显示边框,默认是`True

table.junction_char控制边框连接符

table.horizontal_char控制横边框符号

table.vertical_char控制竖边框符号

上例运行如下:

$++++++$++++++++++$++++++++++$++++++++++++$
% 编号 %  云编号  %   名称   %   IP地址   %
$++++++$++++++++++$++++++++++$++++++++++++$
%  1   % server01 % 服务器01 % 172.16.0.1 %
%  3   % server03 % 服务器03 % 172.16.0.3 %
%  2   % server02 % 服务器02 % 172.16.0.2 %
%  9   % server09 % 服务器09 % 172.16.0.9 %
%  4   % server04 % 服务器04 % 172.16.0.4 %
%  5   % server05 % 服务器05 % 172.16.0.5 %
%  6   % server06 % 服务器06 % 172.16.0.6 %
%  8   % server08 % 服务器08 % 172.16.0.8 %
%  7   % server07 % 服务器07 % 172.16.0.7 %
$++++++$++++++++++$++++++++++$++++++++++++$

以上简单介绍了表格常用的一些样式设置。更具体的方法可参数请参考官方网站:

https://github.com/jazzband/prettytable

Munch - 字典黑魔法库

Munch API

当你想访问字典中的某个 key 时,你需要使用字典特定的访问方式,而这种方式需要你键入 一对中括号 还有 一对引号

>>> profile = dict(name="iswbm")
>>> profile
{'name': 'iswbm'}
>>> profile["name"]
'iswbm'

是不是开始觉得忍无可忍了?

如果可以像调用对象属性一样使用 . 去访问 key 就好了,可以省去很多多余的键盘击入,就像这样子

>>> profile.name
'iswbm'

1. 安装方法

使用如下命令进行安装

$ python -m pip install munch

2. 简单示例

munch 有一个 Munch 类,它继承自原生字典,使用 isinstance 可以验证

>>> from munch import Munch
>>> profile = Munch()
>>> isinstance(profile, dict)
True
>>>

并实现了点式赋值与访问,profile.nameprofile['name'] 是等价的

>>> profile.name = "iswbm"
>>> profile.age = 18
>>> profile
Munch({'name': 'iswbm', 'age': 18})
>>>
>>> profile.name
'iswbm'
>>> profile["name"]
'iswbm'

3. 兼容字典的所有操作

本身 Munch 继承自 dict,dict 的操作也同样适用于 Munch 对象,不妨再来验证下

首先是:增删改查

# 新增元素
>>> profile["gender"] = "male"
>>> profile
Munch({'name': 'iswbm', 'age': 18, 'gender': 'male'})

# 修改元素
>>> profile["gender"] = "female"
>>> profile
Munch({'name': 'iswbm', 'age': 18, 'gender': 'female'})

# 删除元素
>>> profile.pop("gender")
'female'
>>> profile
Munch({'name': 'iswbm', 'age': 18})
>>>
>>> del profile["age"]
>>> profile
Munch({'name': 'iswbm'})

再者是:一些常用方法

>>> profile.keys()
dict_keys(['name'])
>>>
>>> profile.values()
dict_values(['iswbm'])
>>>
>>> profile.get('name')
'iswbm'
>>> profile.setdefault('gender', 'male')
'male'
>>> profile
Munch({'name': 'iswbm', 'gender': 'male'})

4. 设置返回默认值

当访问一个字典中不存在的 key 时,会报 KeyError 的错误

>>> profile = {}
>>> profile["name"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'name'

对于这种情况,通常我们会使用 get 来规避

>>> profile = {}
>>> profile.get("name", "undefined")
'undefined'

当然你在 munch 中仍然可以这么用,不过还有一种更好的方法:使用 DefaultMunch,它会在你访问不存在的 key 时,给你返回一个设定好的默认值

>>> from munch import DefaultMunch
>>> profile = DefaultMunch("undefined", {"name": "iswbm"})
>>> profile
DefaultMunch('undefined', {'name': 'iswbm'})
>>> profile.age
'undefined'
>>> profile
DefaultMunch('undefined', {'name': 'iswbm'})

5. 工厂函数自动创建key

上面使用 DefaultMunch 仅当你访问不存在的 key 是返回一个默认值,但这个行为并不会修改原 munch 对象的任何内容。

若你想访问不存在的 key 时,自动触发给原 munch 中新增你想要访问的 key ,并为其设置一个默认值,可以试一下 DefaultFactoryMunch 传入一个工厂函数。

>>> from munch import DefaultFactoryMunch
>>> profile = DefaultFactoryMunch(list, name='iswbm')
>>> profile
DefaultFactoryMunch(list, {'name': 'iswbm'})
>>>
>>> profile.brothers
[]
>>> profile
DefaultFactoryMunch(list, {'name': 'iswbm', 'brothers': []})

6. 序列化的支持

Munch 支持序列化为 JSON 或者 YAML 格式的字符串对象

转换成 JSON

>>> from munch import Munch
>>> munch_obj = Munch(foo=Munch(lol=True), bar=100, msg='hello')
>>>
>>> import json
>>> json.dumps(munch_obj)
'{"foo": {"lol": true}, "bar": 100, "msg": "hello"}'

转换成 YAML

>>> from munch import Munch
>>> munch_obj = Munch(foo=Munch(lol=True), bar=100, msg='hello')
>>> import yaml
>>> yaml.dump(munch_obj)
'!munch.Munch\nbar: 100\nfoo: !munch.Munch\n  lol: true\nmsg: hello\n'
>>>
>>> print(yaml.dump(munch_obj))
!munch.Munch
bar: 100
foo: !munch.Munch
  lol: true
msg: hello

>>>

建议使用 safe_dump 去掉 !munch.Munch

>>> print(yaml.safe_dump(munch_obj))
bar: 100
foo:
  lol: true
msg: hello

Rich - 完美终端工具

Rich API

Rich 是一个 Python 库,可以为你在终端中提供富文本和漂亮、精美的格式。

使用 Rich API 可以很容易的在终端输出添加各种颜色和不同风格。它可以绘制漂亮的表格,进度条,markdown,突出显示语法的源代码及回溯等等,优秀的功能不胜枚举。

1.Rich 兼容性

Rich 适用于 Linux,OSX 和 Windows。可与新的 Windows 终端一起使用,Windows 的经典终端仅限 8 种颜色。

Rich 还可以与 Jupyter NoteBook 一起使用,而无需其他配置。

2.Rich 安装说明

请选择以下任一种方式输入命令安装依赖

  1. Windows 环境 打开 Cmd (开始-运行-CMD)。
  2. MacOS 环境 打开 Terminal (command+空格输入Terminal)。
  3. 如果你用的是 VSCode编辑器 或 Pycharm,可以直接使用界面下方的Terminal.
pip install rich

3.Rich 的 Print 功能

想毫不费力地将 Rich 的输出功能添加到你的Python脚本程序中,你只需导入 rich print 方法,该方法和其他 Python 的自带功能的参数类似。你可以试试:

from rich import print

print("Hello, [bold magenta]World[/bold magenta]!", ":vampire:", locals())

可以看到,基于 rich 的 print 方法输出的内容都是带颜色、带重点的,相比于Python自带的 print 有明显的优势。

4.自定义 Console 控制台输出

想要对 Rich 终端内容进行更多的自定义设置,你需要导入并构造一个控制台对象:

from rich.console import Console

console = Console()

Console 对象含有一个 print 方法,它的界面与 python 内置的 print 功能相似。你可以试试:

console.print("Hello", "World!")

你可能已经料到,这时终端上会显示“ Hello World!”,请注意,与内置的“打印”功能不同,Rich 会将文字自动换行以适合终端宽度。

有几种方法可以为输出添加自定义颜色和样式。你可以通过添加 style 关键字参数来为整个输出设置样式。例子如下:

console.print("Hello", "World!", style="bold red")

输出如下图:

这个范例一次只设置了一行文字的样式。如果想获得更细腻更复杂的样式,Rich 可以渲染一个特殊的标记,其语法类似于 bbcode。示例如下:

console.print("Where there is a [bold cyan]Will[/bold cyan] there [u]is[/u] a [i]way[/i].")

5.Console 控制台记录

Console 对象具有一个 log() 方法,该方法具有与 print() 类似的界面,除此之外,还能显示当前时间以及被调用的文件和行。

默认情况下,Rich 将针对 Python 结构和 repr 字符串进行语法突出显示。如果你记录一个集合(如字典或列表),Rich 会把它漂亮地打印出来,使其切合可用空间。下面是其中一些功能的示例:

from rich.console import Console
console = Console()

test_data = [
    {"jsonrpc": "2.0", "method": "sum", "params": [None, 1, 2, 4, False, True], "id": "1",},
    {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
    {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2"},
]

def test_log():
    enabled = False
    context = {
        "foo": "bar",
    }
    movies = ["Deadpool", "Rise of the Skywalker"]
    console.log("Hello from", console, "!")
    console.log(test_data, log_locals=True)


test_log()

以上范例的输出如下:

注意其中的 log_locals 参数会输出一个表格,该表格包含调用 log 方法的局部变量。

log 方法既可用于将长时间运行应用程序(例如服务器)的日志记录到终端,也可用于辅助调试。

Logging 处理程序

你还可以使用内置的处理类来对 Python 日志记录模块的输出进行格式化和着色。下面是输出示例:

6. 表情符号

将名称放在两个冒号之间即可在控制台输出中插入表情符号。示例如下:

>>> console.print(":smiley: :vampire: :pile_of_poo: :thumbs_up: :raccoon:")
😃 🧛 💩 👍 🦝

请谨慎地使用此功能。

7.表格

Rich 包含多种边框,样式,单元格对齐等格式设置的选项。下面是一个简单的示例:

from rich.console import Console
from rich.table import Column, Table

console = Console()

table = Table(show_header=True, header_style="bold magenta")
table.add_column("Date", style="dim", width=12)
table.add_column("Title")
table.add_column("Production Budget", justify="right")
table.add_column("Box Office", justify="right")
table.add_row(
    "Dev 20, 2019", "Star Wars: The Rise of Skywalker", "$275,000,000", "$375,126,118"
)
table.add_row(
    "May 25, 2018",
    "[red]Solo[/red]: A Star Wars Story",
    "$275,000,000",
    "$393,151,347",
)
table.add_row(
    "Dec 15, 2017",
    "Star Wars Ep. VIII: The Last Jedi",
    "$262,000,000",
    "[bold]$1,332,539,889[/bold]",
)

console.print(table)

该示例的输出如下:

请注意,控制台标记的呈现方式与 print() 和 log() 相同。实际上,由 Rich 渲染的任何内容都可以添加到标题/行(甚至其他表格)中。

Table 类很聪明,可以调整列的大小以适合终端的可用宽度,并能根据需要做文本环绕的处理。下面是相同的示例,输出与比上表小的终端上:

8.进度条

Rich 可以渲染多个不闪烁的进度条形图,以跟踪长时间运行的任务。

基本用法:用 track 函数调用程序并迭代结果。下面是一个例子:

from rich.progress import track

for step in track(range(100)):
    do_step(step)

添加多个进度条并不难。以下是效果示例:

这些列可以配置为显示你所需的任何详细信息。

内置列包括完成百分比,文件大小,文件速度和剩余时间。下面是显示正在进行的下载的示例:

它可以在显示进度的同时下载多个 URL。要自己尝试一下,请参阅示例文件中的 examples/downloader.py ,在Python实用宝典公众号后台回复 rich示例 下载全部示例。

9.按列输出数据

Rich 可以将内容通过排列整齐的,具有相等或最佳的宽度的列来呈现。下面是(macOS / Linux) ls 命令的一个非常基本的克隆,用列来显示目录列表:

import os
import sys

from rich import print
from rich.columns import Columns

directory = os.listdir(sys.argv[1])
print(Columns(directory))

以下屏幕截图是列示例的输出,该列显示了从 API 提取的数据:

10.Markdown

Rich 可以呈现markdown,相当不错的将其格式显示到终端。

为了渲染 markdown,请导入 Markdown 类,将其打印到控制台。例子如下:

from rich.console import Console
from rich.markdown import Markdown

console = Console()
with open("README.md") as readme:
    markdown = Markdown(readme.read())
console.print(markdown)

该例子的输出如下图:

11.语法突出显示

Rich 使用 pygments 库来实现语法高亮显示。用法类似于渲染 markdown。构造一个 Syntax 对象并将其打印到控制台。下面是一个例子:

from rich.console import Console
from rich.syntax import Syntax

my_code = '''
def iter_first_last(values: Iterable[T]) -&gt; Iterable[Tuple[bool, bool, T]]:
    """Iterate and generate a tuple with a flag for first and last value."""
    iter_values = iter(values)
    try:
        previous_value = next(iter_values)
    except StopIteration:
        return
    first = True
    for value in iter_values:
        yield first, False, previous_value
        first = False
        previous_value = value
    yield first, True, previous_value
'''
syntax = Syntax(my_code, "python", theme="monokai", line_numbers=True)
console = Console()
console.print(syntax)

输出如下:

12.错误回溯(traceback)

Rich 可以渲染漂亮的错误回溯日志,比标准的 Python 回溯更容易阅读,并能显示更多代码。

你可以将 Rich 设置为默认的回溯处理程序,这样所有异常都将由 Rich 为你呈现。

下面是在 OSX(与 Linux 类似)上的外观:

lxml - 网页解析库Xpath

LXML API

lxml的安装

在使用lxml解析库之前,先简单介绍一下lxml的概念,并讲解如何安装lxml库。

lxml的基本概念

lxml是Python的一个解析库,支持html和xml的解析,其解析的效率极快。xpath全称为Xml Path Language,顾名思义,即一种在xml中查找信息的语言。lxml主要是用xpath模块去解析html或者xml等文档内容。

安装lxml

lxml的安装其实很简单,下面介绍两种不同的安装方式(适用不同的操作系统)。

#方式一:pip安装
pip install lxml

#方式二:wheel安装
#下载对应系统版本的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml
pip install lxml-4.6.3-cp39-cp39-win_amd64.whl

方式一,通过pip install lxml 命令就可以直接安装;

方式二,需要通过下载whl文件,再去安装。whl文件的下载链接为:http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml,进入这个链接后选择下载自己python版本和系统版本(32位/64位)对应的whl文件即可;

Xpath的常用规则

规则具体见表所示。

表达式 描述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选择子孙节点
. 选取当前节点
.. 选择当前节点的父节点
@ 选取属性
***** 通配符,选择所有元素节点与元素名
@* 选取所有属性
node 匹配任何类型的节点

举例:

from lxml import etree
from io import StringIO
test_html = '''
<html>
    <h4>学号</h4>
    <ul>
       <li>2112001</li>
       <li>2112002</li>
       <li class='blank'>2112003</li>
       <li>2112004</li>
    </ul>
    <h4>姓名</h4>
    <ul class="ul" style="color:red">
        <li>张三</li>
        <li>李四</li>
        <li>王五</li>
        <li>老六</li>
    </ul>
</html>
'''
#将html网页源码加入带etree中
html = etree.parse(StringIO(test_html))
print(html)

结果:
<lxml.etree._ElementTree object at 0x00000283496436C8>

上述代码中,先是随机构造了部分html源码,并将其放入lxml的etree对象中。然后待解析的html就可以通过lxml去进行操作。最后对这段html进行不同的提取操作。

获取所有li标签下的数据,并提取其内容:

list = html.xpath('//li')
for i in list:
print("数据:" + i.text)

结果:
数据:2112001
数据:2112002
数据:2112003
数据:2112004
数据:张三
数据:李四
数据:王五
数据:老六

通过属性class获取值

#获取class为blank的所有li标签,并提取其内容
blank_li_list = html.xpath('//li[@class="blank"]')
for l in blank_li_list:
    print("数据:" + l.text)

结果:
数据:2112003

删除子元素

比如要删除第一个ul下的第一个li元素

获取html中的所有ul标签
first_ul = html.find("//ul")
#获取first_ul下的所有li标签
ul_li = first_ul.xpath("li")
#打印长度
print(len(ul_li))
# 删除操作
ul_li.remove(ul_li[0])
#打印长度
print(len(ul_li))

结果:
4
3

可以看到获取第一个ul标签,接着取出第一个ul下的所有li标签。输出其长度为4,删除第一个li之后,还是剩下3个li,因此输出3。

获取最后一个ul标签下的所有li数据

last_ul_li = html.xpath('//ul[last()]/li')
for l in last_ul_li:
    print("数据:" + l.text)

结果:
数据:张三
数据:李四
数据:王五
数据:老六

实战:提取小说所有章节

现在我们来获取《大主宰》整本小说的所有章节,包括章节名称章节链接

目标:《大主宰》整本小说的所有章节

链接:http://book.chenlove.cn/novel/36.html#catalog

思路:先获取网页源码,接着通过lxml的xpath模块去解析网页源码,并提取出所有章节的标题和章节链接,最后打印输出。

在开始之前,先预览一下网页页面:

通过按F12,点击elements/元素查看网页源代码:

通过查看源代码,我们可以知道所有章节标签内容都在class为cate-list的div标签中,ul下的所有li对应网页中每三章为一行。因此获取到li之后,再解析li标签下的三个a标签。完整代码如下:

import requests
from lxml import etree

# 设置代理服务器
headers = {
    'User_Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'
}
#请求链接
url = "http://book.chenlove.cn/novel/36.html#catalog"
response = requests.get(url, headers=headers)
if response.status_code == 200:
    # 转化为utf-8格式,不加这条语句,输出爬取的信息为乱码
    response.encoding = 'utf8'
    #获取到源码
    html = etree.HTML(response.text)
    ul_li_list = html.xpath('//*[@class="cate-list"]/ul/li')

通过发送requests请求获取网页源码,并提取出所有小说章节,即class为cate-list的div标签下的所有li标签。接着从li标签中提取出章节标题和章节链接。

for l in ul_li_list[4:]:
     for i in l:
          href = i.xpath('.//@href')[0]
          title = i.xpath('.//span[@class="chapter_name"]/text()')[0]
          print(title,href)

集合ul_li_list中存储着所有的li标签,从第五个li开始才是第一章(前面4行是最新章节),因此我们需要从第5行开始,然后通过xpath提取出li中的含有的章节标题和章节链接。

输出:

第一章 北灵院 /book/12242/39a44ff6dd27f.html

第二章 被踢出灵路的少年 /book/12242/a85e3aae465a4.html

第三章 牧域 /book/12242/820e56c8bc179.html

第四章 大浮屠诀 /book/12242/9b0a211891e10.html

第五章 大千世界 /book/12242/e6fadddc7bf29.html

第六章 灵力增幅 /book/12242/6a3c53c1cf3cb.html

第七章 慕元 /book/12242/7717207387f31.html

……..

Tiler - 将图片转成像素风

Tiler

安装

将项目克隆下载,再安装requirements.txt中的依赖。

git clone https://github.com/nuno-faria/tiler.git

pip install -r requirements.txt

转换像素图

现在演示如何利用Tiler,将普通表情包转换成像素风。

经过前文的操作,可以在本地看到已经下载好了文件和配置。

其中,文件夹images里放的是项目示例图片,文件夹tiles里是图块目录,另外conf.py里是配置文件。

C:\Users\Administrator\tiler

添加了一个待转换图片——333.png,其实放在其他目录下也不影响,只需要下一步指定好即可。

除了待转换图片,还需要选择像素图块,自带的像素图块中只有line比较合适,我们就先拿它来演示。

其中gen_line_h放置的都是横向的线段图。

生成像素图,在 tiler 目录中执行以下命令:

python tiler.py ./images/333.png ./tiles/lines/gen_line_h/

执行过程中,有进度条提醒。待转换图片较小,几秒钟就完成了。

此时在 tiler 目录中,已经生成了一个新图片——out.png

自定义

我猜测是像素图块的问题,上图使用的是长方形的图块。但在自带目录里,未发现有正方形,这样我们就需要自定义像素块了。

想要更好的像素风转换效果,最好再调整一下参数配置

先说自定义像素块。

自定义图案

首先,要在目录tiles内新建一个文件夹square2

在其中放入一个自定义的像素图块,我准备的是正方形(50x50像素)。

建议颜色为#F0F0F0

C:\Users\Administrator\tiler\tiles\square2

光有一个图块还不行,需要利用脚本gen_tiles.py 生成一系列多颜色图块。

自定义参数

在生成像素图块前,还可以自定义参数。

通过更改conf.py中的代码,我调整了DEPTH等值。

  • DEPTH:每种颜色的分区数,默认值 4。
  • COLOR_DEPTH:图片包含颜色的数量,默认值 32。

作者在conf.py里对每项配置参数都作了注释,大家在使用时可以自行调整。

自定义效果

接下来生成新的像素图块。

在 tiler 目录下执行命令:

python gen_tiles.py ./tiles/square2/square2.png

打开文件夹gen_square2,各种颜色的像素图块已经被生成好了。

接下来,就是使用新图块,重新制作像素图:

python tiler.py ./images/333.png ./tiles/square2/gen_square2/

生成👇

Polars - 加速版pandas

Polars API

Document

Polars是通过Rust编写的一个库,Polars的内存模型是基于Apache Arrow。

Polars存在两种API,一种是Eager API,另一种则是Lazy API。

其中Eager API和Pandas的使用类似,语法差不太多,立即执行就能产生结果。

而Lazy API就像Spark,首先将查询转换为逻辑计划,然后对计划进行重组优化,以减少执行时间和内存使用。

安装Polars,使用百度pip源。

# 安装polars
pip install polars -i https://mirror.baidu.com/pypi/simple/

安装成功后,开始测试,比较Pandas和Polars处理数据的情况。

使用某网站注册用户的用户名数据进行分析,包含约2600万个用户名的CSV文件。

import pandas as pd

df = pd.read_csv('users.csv')
print(df)

数据情况如下。

此外还使用了一个自己创建的CSV文件,用以数据整合测试。

import pandas as pd

df = pd.read_csv('fake_user.csv')
print(df)

得到结果如下。

首先比较一下两个库的排序算法耗时。

import timeit
import pandas as pd

start = timeit.default_timer()

df = pd.read_csv('users.csv')
df.sort_values('n', ascending=False)
stop = timeit.default_timer()

print('Time: ', stop - start)

-------------------------
Time:  27.555776743218303

可以看到使用Pandas对数据进行排序,花费了大约28s。

import timeit
import polars as pl

start = timeit.default_timer()

df = pl.read_csv('users.csv')
df.sort(by_column='n', reverse=True)
stop = timeit.default_timer()

print('Time: ', stop - start)

-----------------------
Time:  9.924110282212496

Polars只花费了约10s,这意味着Polars比Pandas快了2.7倍。

下面,我们来试试数据整合的效果,纵向连接。

import timeit
import pandas as pd

start = timeit.default_timer()

df_users = pd.read_csv('users.csv')
df_fake = pd.read_csv('fake_user.csv')
df_users.append(df_fake, ignore_index=True)
stop = timeit.default_timer()

print('Time: ', stop - start)

------------------------
Time:  15.556222308427095

使用Pandas耗时15s。

import timeit
import polars as pl

start = timeit.default_timer()

df_users = pl.read_csv('users.csv')
df_fake = pl.read_csv('fake_user.csv')
df_users.vstack(df_fake)
stop = timeit.default_timer()

print('Time: ', stop - start)

-----------------------
Time:  3.475433263927698

Polars居然最使用了约3.5s,这里Polars比Pandas快了4.5倍。

ChatterBot - 聊天机器人

ChatterBot API

什么是聊天机器人

聊天机器人也称为聊天机器人、机器人、人工代理等,基本上是由人工智能驱动的软件程序,其目的是通过文本或语音与用户进行对话。我们日常接触的比较著名的例子包括 Siri、Alexa 等

这些聊天机器人倾向于为用户执行特定任务,聊天机器人经常执行诸如进行交易、预订酒店、提交表格等任务。随着人工智能领域的技术进步,聊天机器人的可能性也是无穷无尽的

当然了,在当前技术下,聊天机器人还是有很多局限性的

  • 领域知识 —— 由于真正的人工智能仍然遥不可及,任何聊天机器人在与人类对话时都很难完全理解对话含义
  • 个性 —— 无法正确响应和相当差的理解能力比任何聊天机器人的常见错误更重要,为聊天机器人添加个性仍然是很遥远和困难的事情

我们可以将聊天机器人定义为两类

  • 基于特定规则 —— 在这种方法中,机器人是根据规则进行训练的。基于此,机器人可以回答简单的查询,但有时无法回答复杂的对话

  • 自学 —— 这些机器人遵循机器学习方法,效率更高,并进一步分为另外两类

    • 基于检索模型 —— 在这种方法中,机器人根据用户输入从响应列表中检索最佳响应
    • 生成模型 —— 这些模型通常会给出答案,而不是从一组答案中进行搜索,这也使它们成为智能机器人

好了,高大上的聊天机器人知识就先介绍到这里,下面我们就通过 chatterbot 来构建一个简单的在线聊天机器人

ChatterBot 库简介

ChatterBot 是 Python 中的一个库,它生成对用户输入的响应,使用多种机器学习算法来产生各种响应。用户可以更轻松地使用 ChatterBot 库制作具有更准确响应的聊天机器人

ChatterBot 的设计允许机器人接受多种语言的训练,最重要的是,机器学习算法使机器人更容易使用用户的输入自行改进

ChatterBot 可以轻松创建参与对话的软件,每次聊天机器人从用户那里获得输入时,它都会保存输入和响应,这有助于没有初始知识的聊天机器人使用收集到的响应进行自我进化

随着响应的增加,聊天机器人的准确性也会提高。程序从与输入匹配的最接近匹配语句中选择最接近匹配的响应,然后从该响应的已知语句选择中选择响应

安装 ChatterBot 也非常简单

pip install chatterbot

构建聊天机器人

机器人训练

Chatterbot 带有一个数据实用程序模块,可用于训练聊天机器人。目前该模块中有十多种语言的训练数据,我们可以拿来直接使用

https://github.com/gunthercox/chatterbot-corpus

下面是在 python 中开始使用 ChatterBot 的简单示例

from chatterbot import chatbot
from chatterbot.trainers import ListTrainer

chatbot = Chatbot('Edureka')
trainer = ListTrainer(chatbot)
trainer.train([ 'hi, can I help you find a course', 'sure I'd love to find you a course', 'your course have been selected'])

response = chatbot.get_response("I want a course")
print(response)

在例子中,我们根据提供的输入从聊天机器人获得响应

构建 flask app

对于基本的 flask 结构,我们直接使用 GitHub 上的一个脚手架,这个是专门用来开发 ChatterBot 应用的

https://github.com/chamkank/flask-chatterbot

我们直接克隆项目就好

把项目下载到本地之后,我们进行一些修改

我们需要为 HTML 和 CSS 文件添加另外两个目录 static 和模板

修改 App.py 文件

from flask import Flask, render_template, request
from chatterbot import ChatBot
from chatterbot.trainers import ChatterBotCorpusTrainer

app = Flask(__name__)

english_bot = ChatBot("Chatterbot", storage_adapter="chatterbot.storage.SQLStorageAdapter")
trainer = ChatterBotCorpusTrainer(english_bot)
trainer.train("chatterbot.corpus.english")

@app.route("/")
def home():
    return render_template("index.html")

@app.route("/get")
def get_bot_response():
    userText = request.args.get('msg')
    return str(english_bot.get_response(userText))


if __name__ == "__main__":
    app.run()

index.html 文件

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="/static/style.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
<h1>Flask Chatterbot Example</h1>
<div>
<div id="chatbox">
<p class="botText"><span>Hi! I'm Chatterbot.</span></p>
</div>
<div id="userInput">
<input id="textInput" type="text" name="msg" placeholder="Message">
<input id="buttonInput" type="submit" value="Send">
</div>
<script>
function getBotResponse() {
var rawText = $("#textInput").val();
var userHtml = '<p class="userText"><span>' + rawText + '</span></p>';
$("#textInput").val("");
$("#chatbox").append(userHtml);
document.getElementById('userInput').scrollIntoView({block: 'start', behavior: 'smooth'});
$.get("/get", { msg: rawText }).done(function(data) {
var botHtml = '<p class="botText"><span>' + data + '</span></p>';
$("#chatbox").append(botHtml);
document.getElementById('userInput').scrollIntoView({block: 'start', behavior: 'smooth'});
});
}
$("#textInput").keypress(function(e) {
if(e.which == 13) {
getBotResponse();
}
});
$("#buttonInput").click(function() {
getBotResponse();
})
</script>
</div>
</body>
</html>

index.html 文件将包含应用程序的模板,而 style.css 将包含带有 CSS 代码的样式表。执行上述程序后,我们将得到如下图所示的输出

Style.css 文件

body
{
font-family: Garamond;
background-color: black;
}
h1
{
color: black;
margin-bottom: 0;
margin-top: 0;
text-align: center;
font-size: 40px;
}
h3
{
color: black;
font-size: 20px;
margin-top: 3px;
text-align: center;
}
#chatbox
{
background-color: black;
margin-left: auto;
margin-right: auto;
width: 40%;
margin-top: 60px;
}
#userInput {
margin-left: auto;
margin-right: auto;
width: 40%;
margin-top: 60px;
}
#textInput {
width: 87%;
border: none;
border-bottom: 3px solid #009688;
font-family: monospace;
font-size: 17px;
}
#buttonInput {
padding: 3px;
font-family: monospace;
font-size: 17px;
}
.userText {
color: white;
font-family: monospace;
font-size: 17px;
text-align: right;
line-height: 30px;
}
.userText span {
background-color: #009688;
padding: 10px;
border-radius: 2px;
}
.botText {
color: white;
font-family: monospace;
font-size: 17px;
text-align: left;
line-height: 30px;
}
.botText span {
background-color: #EF5350;
padding: 10px;
border-radius: 2px;
}
#tidbit {
position:absolute;
bottom:0;
right:0;
width: 300px;
}

接下来我们打开网页,就可以看到聊天页面啦

有一个文本框,我们可以在其中提供用户输入,机器人将为该语句生成相应的响应消息,当我们输入的消息越多,机器人就会越智能!

Pynput - 监控键盘和鼠标

使用pynput库

pynput 可以监控我们的键盘和鼠标。目前具有此类功能的库有很多,比如 pygame 等游戏库,但是当我们只需要监控键盘和鼠标时,它们就显得过于笨重了。

对键盘监控

在他的电脑常驻一个后台进程去实现监控键盘输入并记录下来,代码大概可以这样:

from pynput import keyboard
def on_press(key):
    print(f'{key} :pushed')
 def on_release(key):
    #print(f'{key} released')
    if key == keyboard.Key.esc:
        # Stop listener
        return False
with keyboard.Listener(on_press=on_press,on_release=on_release) as lsn:
    lsn.join()

对鼠标监控

from pynput import mouse

def on_click(x, y, button, pressed):
    if button == mouse.Button.left:
        print('left was pressed!')
    elif button == mouse.Button.right:
        print('right was pressed!')
        return False
    else:
        print('mid was pressed!')

# Collect events until released
with mouse.Listener(on_click=on_click) as listener:
    listener.join()

你会发现,每条打印都被打印了两次,这是因为按下去和抬起来都会触发鼠标事件。

监控并记录到日志文件

from pynput import keyboard,mouse
from loguru import logger
from threading import Thread

# 定义日志文件
logger.add('demo.log')
def on_press(key):
    logger.debug(f'{key} :pushed') 
def on_release(key):
    #print(f'{key} released')
    if key == keyboard.Key.esc:
        # Stop listener
        return False
# 定义f1用于线程1
def f1():
    with keyboard.Listener(on_press=on_press,on_release=on_release) as lsn:
        lsn.join()
def on_click(x, y, button, pressed):
    if button == mouse.Button.left:
        logger.debug('left was pressed!')
    elif button == mouse.Button.right:
        logger.debug('right was pressed!')
        return False
    else:
        logger.debug('mid was pressed!') 
# 定义f2用于线程2
def f2():
    # Collect events until released
    with mouse.Listener(on_click=on_click) as listener:
        listener.join()
if __name__ == '__main__':
    # 起两个线程分别监控键盘和鼠标
    t1 = Thread(target=f1)
    t2 = Thread(target=f2)
    t1.start()
    t2.start()

这样键盘操作已经全部被记录,通过对这个日志文件进行简单的 NLTK 语言处理,就能复原聊天记录。

图像处理库合集

SimpleCV

  • SimpleCV也是广泛被使用的构建计算机视觉应用程序的开源框架。手握SimpleCV,你可以访问几个高性能的视觉库,而无需先了解图像色深(bit depth)、文件格式、色彩空间等。SimpleCV拥护者的支持理由有两个,一是初学者也可以借此编写简单的视觉任务,二是无论是相机、视频文件、图像和视频流可互相操作。

用户指南:https://simplecv.readthedocs.io/en/latest/

Mahotas

  • Mahotas包含传统的图像处理功能,如滤波和图像形态学处理,以及用于特征计算,比如兴趣点检测和局部描述子等。

    这个库适用于快速开发,算法是用C++实现的,并且针对速度进行了调整。

官方地址:
https://mahotas.readthedocs.io/en/latest/

用户指南:
https://mahotas.readthedocs.io/en/latest/index.html

SimpleITK

  • ITK是一个开源的跨平台系统,提供一整套用于图像分析的软件工具。其中,SimpleITK是一个建立在ITK之上的简化层,促进其在简化原型、教育和解释语言中的应用。SimpleITK是一个图像分析工具包,内含大量组件,支持一般滤波操作、图像分割和图形配准。SimpleITK本身是用C++编写的,但也适用于包括Python在内的大量编程语言。

官方地址:
https://itk.org/

学习资料:
http://insightsoftwareconsortium.github.io/SimpleITK-Notebooks/

GraphicsMagick

  • GraphicsMagick号称图像处理领域的瑞士军刀。代码短小却提供了一个鲁棒、高效的工具和库集合,可用来处理图像的读取、写入和操作。支持超过88种图像格式,包括重要的DPX、GIF、JPEG、JPEG-2000、PNG、PDF、PNM和TIFF。

官方资料:
https://pypi.org/project/pgmagick/

相关资源:
https://github.com/hhatto/pgmagick

Pycairo

  • pyCairo是一个Python的2D图形渲染库,可用于绘制矢量图形的2D图形,在调整大小或变换时不会丢失清晰度。下面这个用例是用Pycairo绘制线条、基本形状和径向梯度。

官方介绍:
https://cairographics.org/

相关资源:
https://github.com/pygobject/pycairo


文章作者: 杰克成
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杰克成 !
评论
  目录