Python 进阶

第一章 Python 线程

1、线程前戏

(1) 并发和并行:

  • 并发:伪,由于执行速度特别快,感觉不到停顿
  • 并行:真,创建10个人同时操作

(2) 线程,进程

2-1 单进程,单线程的应用程序

print("hello world!!!")

2-2 到底什么是线程?什么是进程?

python自己是没有线程和进程的,python中调用的操作系统的线程和进程

2-3 单进程,多线程的应用程序

import threading

print("hello world!!!")

def func(arg):
    print(arg)
    
# 创建线程
t = threading.Thread(target=func,args=(11,))
t.start()

print(123)

一个应用程序(软件),可以有多个进程(默认只有一个),一个进程中也可以创建多个线程(默认一个)

2-4 Java or C# 中的进程与线程

2-5 Python 中的进程和线程

2-6 总结

操作系统帮助开发者操作硬件。

程序员写好代码在操作系统上运行(依赖解释器)。

当任务特别多时以前你的代码:

import requests
import uuid

url_list = [
    'http://pic.sc.chinaz.com/Files/pic/pic9/202008/apic27292_s.jpg',
    'http://pic.sc.chinaz.com/Files/pic/pic9/202008/apic27262_s.jpg',
    'http://pic1.sc.chinaz.com/Files/pic/pic9/202008/apic27250_s.jpg',
]

def task(url):
    """
    1. DNS解析,根据域名解析出IP
    2. 创建socket客户端    sk = socket.socket()
    3. 向服务端发起连接请求 sk.connect()
    4. 发送数据(我要图片) sk.send(...)
    5. 接收数据            sk.recv(8096)
    接收到数据后写入文件。
    """
    ret = requests.get(url)
    file_name = str(uuid.uuid4()) + '.jpg'
    with open(file_name, mode='wb') as f:
        f.write(ret.content)

for url in url_list:
    task(url)
"""
- 你写好代码
- 交给解释器运行: python s1.py 
- 解释器读取代码,再交给操作系统去执行,根据你的代码去选择创建多少个线程/进程去执行(单进程/单线程)。
- 操作系统调用硬件:硬盘、cpu、网卡....
"""

当任务特别多时现在你的代码:

import threading
import requests
import uuid

url_list = [
    'http://pic.sc.chinaz.com/Files/pic/pic9/202008/apic27292_s.jpg',
    'http://pic.sc.chinaz.com/Files/pic/pic9/202008/apic27262_s.jpg',
    'http://pic1.sc.chinaz.com/Files/pic/pic9/202008/apic27250_s.jpg',
]

def task(url):
    """
    1. DNS解析,根据域名解析出IP
    2. 创建socket客户端    sk = socket.socket()
    3. 向服务端发起连接请求 sk.connect()
    4. 发送数据(我要图片) sk.send(...)
    5. 接收数据            sk.recv(8096)
    接收到数据后写入文件。
    """
    ret = requests.get(url)
    file_name = str(uuid.uuid4()) + '.jpg'
    with open(file_name, mode='wb') as f:
        f.write(ret.content)

for url in url_list:
    t = threading.Thread(target=task, args=(url,))
    t.start()

"""
- 你写好代码
- 交给解释器运行: python s2.py 
- 解释器读取代码,再交给操作系统去执行,根据你的代码去选择创建多少个线程/进程去执行(单进程/4线程)。
- 操作系统调用硬件:硬盘、cpu、网卡....
"""

Python多线程情况下:

  • 计算密集型操作:效率低。(GIL锁)
  • IO操作: 效率高

Python多进程的情况下:

  • 计算密集型操作:效率高(浪费资源)。 不得已而为之。
  • IO操作: 效率高 (浪费资源)。

以后写Python时:

  • IO密集型用多线程: 文件/输入输出/socket网络通信
  • 计算密集型用多进程。

扩展:

  • Java多线程情况下:

    • 计算密集型操作:效率高。
    • IO操作: 效率高
  • Python多进程的情况下:

    • 计算密集型操作:效率高(浪费资源)。
    • IO操作: 效率高 浪费资源)。

(3) Python中线程和进程(GIL锁)

  • Python内置的一个全局解释器锁,锁的作用就是保证同一时刻一个进程中只有一个线程可以被cpu调度。

  • 为什么有这把GIL锁?

    Python语言的创始人在开发这门语言时,目的快速把语言开发出来,如果加上GIL锁(C语言加锁),切换时按照100条字节指令来进行线程间的切换。

  • GIL锁,全局解释器锁。用于限制一个进程中同一时刻只有一个线程被cpu调度。

  • 扩展:默认GIL锁在执行100个cpu指令(过期时间)。

(4) Python线程和进程之间的区别

  • 线程,cpu工作的最小单元。

  • 进程,为线程提供一个资源共享的空间。

  • 一个进程中默认是有一个主线程。

2、Python 初识线程

(1) 线程的基本使用

import threading

def func(arg):
    print(arg)
    
# 创建线程
t = threading.Thread(target=func,args=(11,))
t.start()

print(123)

运行结果:

11
123

(2) 主线程默认等子线程执行完毕

import threading
import time

def func(arg):
    time.sleep(arg)
    print(arg)

# 创建线程t1
t1 = threading.Thread(target=func, args=(3,))
t1.start()

# 创建线程t2
t2 = threading.Thread(target=func, args=(9,))
t2.start()

print(123)

运行结果:

123
3
9

(3) 主线程不再等,主线程终止则所有子线程终止

import threading
import time

def func(arg):
    time.sleep(2)
    print(arg)

t1 = threading.Thread(target=func, args=(3,))
t1.setDaemon(True)
t1.start()

t2 = threading.Thread(target=func, args=(9,))
t2.setDaemon(True)
t2.start()

print(123)

运行结果:

123

(4) 开发者可以控制主线程等待子线程(最多等待时间)

import threading
import time

def func(arg):
    time.sleep(0.01)
    print(arg)

print('创建子线程t1')
t1 = threading.Thread(target=func, args=(3,))
t1.start()
# 无参数,让主线程在这里等着,等到子线程t1执行完毕,才可以继续往下走。
# 有参数,让主线程在这里最多等待n秒,无论是否执行完毕,会继续往下走。
t1.join(2)

print('创建子线程t2')
t2 = threading.Thread(target=func, args=(9,))
t2.start()
t2.join(2)  # 让主线程在这里等着,等到子线程t2执行完毕,才可以继续往下走。

print(123)

运行结果:

创建子线程t1
3
创建子线程t2
9
123

(5) 线程名称

import threading

def func(arg):
    # 获取当前执行该函数的线程的对象
    t = threading.current_thread()
    # 根据当前线程对象获取当前线程名称
    name = t.getName()
    print(name, arg)

t1 = threading.Thread(target=func, args=(11,))
t1.setName('线程1')
t1.start()

t2 = threading.Thread(target=func, args=(22,))
t2.setName('线程2')
t2.start()

print(123)

运行结果:

线程1 11
线程2 22
123

(6) 线程本质

import threading

# 先打印:11?123?
def func(arg):
    print(arg)

t1 = threading.Thread(target=func, args=(11,))
t1.start()
# start 是开始运行线程吗?不是
# start 告诉cpu,我已经准备就绪,你可以调度我了。
print(123)

运行结果:

11
123

(7) 补充:面向对象版本的多线程

import threading

# 多线程方式:1 (常见)
def func(arg):
    print(arg)
    
t1 = threading.Thread(target=func, args=(11,))
t1.start()


# 多线程方式:2
class MyThread(threading.Thread):

    def run(self):
        print(11111, self._args, self._kwargs)
        
t1 = MyThread(args=(11,))
t1.start()

t2 = MyThread(args=(22,))
t2.start()

运行结果:

11
11111 (11,) {}
11111 (22,) {}

3、Python 多线程

(1) 计算密集型多线程无用

import threading

v1 = [11,22,33] # +1
v2 = [44,55,66] # 100

def func(data,plus):
    for i in range(len(data)):
        data[i] = data[i] + plus

t1 = threading.Thread(target=func,args=(v1,1))
t1.start()

t2 = threading.Thread(target=func,args=(v2,100))
t2.start()

(2) IO操作 多线程有用

import threading
import requests
import uuid

url_list = [
    'http://pic.sc.chinaz.com/Files/pic/pic9/202008/apic27292_s.jpg',
    'http://pic.sc.chinaz.com/Files/pic/pic9/202008/apic27262_s.jpg',
    'http://pic1.sc.chinaz.com/Files/pic/pic9/202008/apic27250_s.jpg',
]

def task(url):
    ret = requests.get(url)
    file_name = str(uuid.uuid4()) + '.jpg'
    with open(file_name, mode='wb') as f:
        f.write(ret.content)

for url in url_list:
    t = threading.Thread(target=task, args=(url,))
    t.start()

(3) 多线程的问题

import time
import threading
lock = threading.RLock()
n = 10

def task(i):
    print('这段代码不加锁',i)
    lock.acquire() # 加锁,此区域的代码同一时刻只能有一个线程执行
    global n
    print('当前线程',i,'读取到的n值为:',n)
    n = i
    time.sleep(1)
    print('当前线程',i,'修改n值为:',n)
    lock.release() # 释放锁

for i in range(10):
    t = threading.Thread(target=task,args=(i,))
    t.start()

4、Python 线程锁

(1) 锁:Lock (1次放1个)

线程安全,多线程操作时,内部会让所有线程排队处理。如:list/dict/Queue

线程不安全 + 人(锁) => 排队处理。

"""
# 线程安全
import threading
v = []
def func(arg):
    v.append(arg)	# 线程安全
    print(v)

for i in range(10):
    t = threading.Thread(target=func, args=(i,))
    t.start()
"""

import threading
import time

v = []
lock = threading.Lock()

def func(arg):
    lock.acquire()	# 加锁
    v.append(arg)
    time.sleep(0.01)
    m = v[-1]
    print(arg, m)
    lock.release()	# 释放锁

for i in range(10):
    t = threading.Thread(target=func, args=(i,))
    t.start()

运行结果:

0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9

(2) 锁:RLock (1次放1个)

import threading
import time

v = []
lock = threading.RLock()

def func(arg):
    lock.acquire()
    lock.acquire()

    v.append(arg)
    time.sleep(0.01)
    m = v[-1]
    print(arg, m)

    lock.release()
    lock.release()

for i in range(10):
    t = threading.Thread(target=func, args=(i,))
    t.start()

(3) 锁:BoundedSemaphore(1次放N个)信号量

import threading
import time

v = []
lock = threading.BoundedSemaphore(3)

def func(arg):
    lock.acquire()
    print(arg)
    time.sleep(1)
    lock.release()

for i in range(20):
    t = threading.Thread(target=func, args=(i,))
    t.start()

运行结果:

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
16
15
17
18
19

(4) 锁:Condition(1次放指定个数)

方式一:

import time
import threading

lock = threading.Condition()

def func(arg):
    print('线程进来了')
    lock.acquire()
    lock.wait() # 加锁

    print(arg)
    time.sleep(1)

    lock.release()

for i in range(10):
    t =threading.Thread(target=func,args=(i,))
    t.start()

while True:
    inp = int(input('>>>'))
    lock.acquire()
    lock.notify(inp)
    lock.release()

方式二:

import time
import threading

lock = threading.Condition()

def xxxx():
    print('来执行函数了')
    input(">>>")
    # ct = threading.current_thread() # 获取当前线程
    # ct.getName()
    return True

def func(arg):
    print('线程进来了')
    lock.wait_for(xxxx)
    print(arg)
    time.sleep(1)

for i in range(10):
    t =threading.Thread(target=func,args=(i,))
    t.start()

(5) 锁:Event(1次放所有)

import time
import threading

lock = threading.Event()
def func(arg):
    print('线程来了')
    lock.wait()  # 加锁:红灯
    print(arg)
for i in range(10):
    t = threading.Thread(target=func, args=(i,))
    t.start()
input(">>>>")
lock.set()  # 绿灯

lock.clear()  # 再次变红灯
for i in range(10):
    t = threading.Thread(target=func, args=(i,))
    t.start()
input(">>>>")
lock.set()

5、threading.local()

内部自动为每个线程维护一个空间(字典),用于当前存取属于自己的值。保证线程之间的数据隔离。

{
    线程ID: {...}
    线程ID: {...}
    线程ID: {...}
    线程ID: {...}
}

(1) 基本使用

import time
import threading

v = threading.local()

def func(arg):
    # 内部会为当前线程创建一个空间用于存储:phone=自己的值
    v.phone = arg
    time.sleep(2)
    print(v.phone, arg)  # 去当前线程自己空间取值

for i in range(10):
    t = threading.Thread(target=func, args=(i,))
    t.start()

(2) 原理

import time
import threading

DATA_DICT = {}

def func(arg):
    # 获取线程的唯一标识
    ident = threading.get_ident()
    DATA_DICT[ident] = arg
    time.sleep(1)
    print(DATA_DICT[ident], arg)

for i in range(10):
    t = threading.Thread(target=func, args=(i,))
    t.start()

(3) 原理高级

import time
import threading
INFO = {}
class Local(object):

    def __getattr__(self, item):
        ident = threading.get_ident()
        return INFO[ident][item]

    def __setattr__(self, key, value):
        ident = threading.get_ident()
        if ident in INFO:
            INFO[ident][key] = value
        else:
            INFO[ident] = {key:value}

obj = Local()

def func(arg):
    obj.phone = arg # 调用对象的 __setattr__方法(“phone”,1)
    time.sleep(2)
    print(obj.phone,arg)

for i in range(10):
    t =threading.Thread(target=func,args=(i,))
    t.start()

6、线程池

from concurrent.futures import ThreadPoolExecutor
import time

def task(a1, a2):
    time.sleep(2)
    print(a1, a2)

# 创建了一个线程池(最多5个线程)
pool = ThreadPoolExecutor(5)

for i in range(40):
    # 去线程池中申请一个线程,让线程执行task函数。
    pool.submit(task, i, 8)
   

7、生成者消费者模型

三部件:
生产者
队列,先进先出
消费者
问:生产者消费者模型解决了什么问题?不用一直等待的问题。

import time
import queue
import threading

q = queue.Queue()  # 线程安全

def producer(id):
    """
    生产者
    :return:
    """
    while True:
        time.sleep(2)
        q.put('包子')
        print('厨师%s 生产了一个包子' % id)

for i in range(1, 4):
    t = threading.Thread(target=producer, args=(i,))
    t.start()

    
def consumer(id):
    """
    消费者
    :return:
    """
    while True:
        time.sleep(1)
        v1 = q.get()
        print('顾客 %s 吃了一个包子' % id)

for i in range(1, 3):
    t = threading.Thread(target=consumer, args=(i,))
    t.start()

第二章 Python 进程

1、Python 进程

(1) 初识进程

# windows
import multiprocessing

def task(arg):
    print(arg)

def run():
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(i,))
        p.start()

if __name__ == '__main__':
    run()
    
# linux
"""
import multiprocessing

def task(arg):
    print(arg)

for i in range(10):
	p = multiprocessing.Process(target=task, args=(i,))
	p.start()
"""

运行结果:

1
0
2
4
3
5
6
7
8
9

(2) 进程间的数据不共享

import threading
import multiprocessing

data_list = []

def task(arg):
    data_list.append(arg)
    print(data_list)

def run():
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(i,))
        # p = threading.Thread(target=task,args=(i,))
        p.start()

if __name__ == '__main__':
    run()

运行结果:

[0]
[8]
[3]
[2]
[1]
[6]
[5]
[7]
[9]
[4]

(3) 进程常用功能

import time
import multiprocessing

def task(arg):
    p = multiprocessing.current_process()
    print(p.name)   # 获取进程名称
    print(p.ident, p.pid)   # 获取进程id
    time.sleep(2)
    print(arg)


def run():
    print('111111111')
    p1 = multiprocessing.Process(target=task, args=(1,))
    p1.daemon = True
    p1.name = 'pp1'  # 设置进程名称
    p1.start()  # 启动进程
    p1.join()  # 等待进程
    print('222222222')

    p2 = multiprocessing.Process(target=task, args=(2,))
    p2.daemon = True
    p2.name = 'pp2'  # 设置进程名称
    p2.start()  # 启动进程
    p2.join()  # 等待进程
    print('333333333')

if __name__ == '__main__':
    run()

(4) 通过继承方式创建进程

import multiprocessing

class MyProcess(multiprocessing.Process):
    def run(self):
        print('当前进程', multiprocessing.current_process())

def run():
    p1 = MyProcess()
    p1.start()
    
    p2 = MyProcess()
    p2.start()

if __name__ == '__main__':
    run()

2、Python 进程数据共享

(1) Queue

# windows
import time
import multiprocessing

def task(arg, q):
    q.put(arg)

def run():
    q = multiprocessing.Queue()
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(i, q,))
        p.start()

    time.sleep(2)
    while True:
        v = q.get()
        print(v)

if __name__ == '__main__':
    run()
    
# linux
"""
import multiprocessing

q = multiprocessing.Queue()

def task(arg, q):
    q.put(arg)

def run():
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(i, q,))
        p.start()

    while True:
        v = q.get()
        print(v)
"""

运行结果:

8
3
2
0
1
5
6
7
4
9

(2) Manager

# windows
import time
import multiprocessing

def task(arg, dic):
    time.sleep(2)
    dic[arg] = 100

if __name__ == '__main__':
    m = multiprocessing.Manager()
    dic = m.dict()
    process_list = []
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(i, dic,))
        p.start()
        # p.join() ***
        process_list.append(p)

    while True:
        count = 0
        for p in process_list:
            if not p.is_alive():
                count += 1
        if count == len(process_list):
            break
    print(dic)
    
# linux
"""
import multiprocessing
m = multiprocessing.Manager()
dic = m.dict()

def task(arg):
    dic[arg] = 100

def run():
    for i in range(10):
        p = multiprocessing.Process(target=task, args=(i,))
        p.start()

    input('>>>')
    print(dic.values())

if __name__ == '__main__':
    run()
"""

运行结果:

{0: 100, 1: 100, 2: 100, 3: 100, 4: 100, 5: 100, 6: 100, 7: 100, 8: 100, 9: 100}

3、Python 进程锁

import time
import threading
import multiprocessing

lock = multiprocessing.RLock()

def task(arg):
    print('鬼子来了')
    lock.acquire()
    time.sleep(2)
    print(arg)
    lock.release()


if __name__ == '__main__':
    p1 = multiprocessing.Process(target=task, args=(1,))
    p1.start()

    p2 = multiprocessing.Process(target=task, args=(2,))
    p2.start()

运行结果:

鬼子来了
鬼子来了
1
2

4、Python 进程池

import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def task(arg):
    time.sleep(2)
    print(arg)

if __name__ == '__main__':
    pool = ProcessPoolExecutor(5)
    for i in range(10):
        pool.submit(task, i)

运行结果:

0
1
2
3
4
5
6
7
8
9

第三章 Python 协程

概念:

​ 进程,操作系统中存在;

​ 线程,操作系统中存在;

​ 协程,是由程序员创造出来的一个不是真实存在的东西;

​ 协程:是微线程,对一个线程进程分片,使得线程在代码块之间进行来回切换执行,而不是在原来逐行执行。

1、greenlet

greentlet是一个第三方模块,需要提前安装 pip install greenlet才能使用。

from greenlet import greenlet
def func1():
    print(1)        # 第1步:输出 1
    gr2.switch()    # 第3步:切换到 func2 函数
    print(2)        # 第6步:输出 2
    gr2.switch()    # 第7步:切换到 func2 函数,从上一次执行的位置继续向后执行
def func2():
    print(3)        # 第4步:输出 3
    gr1.switch()    # 第5步:切换到 func1 函数,从上一次执行的位置继续向后执行
    print(4)        # 第8步:输出 4
gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch() # 第1步:去执行 func1 函数

运行结果:

1
3
2
4

注意:单纯的协程无用

注意:switch中也可以传递参数用于在切换执行时相互传递值。

2、yield

基于Python的生成器的yield和yield form关键字实现协程代码。

def func1():
    yield 1
    yield from func2()
    yield 2
def func2():
    yield 3
    yield 4
f1 = func1()
for item in f1:
    print(item)

运行结果:

1
3
4
2

注意:yield form关键字是在Python3.3中引入的。

3、gevent

协程 + 遇到IO就切换 => 牛逼起来了 pip install gevent

from gevent import monkey

monkey.patch_all()  # 以后代码中遇到IO都会自动执行greenlet的switch进行切换
import requests
import gevent

def get_page1(url):
    ret = requests.get(url)
    print(url, ret.content)

def get_page2(url):
    ret = requests.get(url)
    print(url, ret.content)

def get_page3(url):
    ret = requests.get(url)
    print(url, ret.content)

gevent.joinall([
    gevent.spawn(get_page1, 'https://www.python.org/'),  # 协程1
    gevent.spawn(get_page2, 'https://www.yahoo.com/'),  # 协程2
    gevent.spawn(get_page3, 'https://github.com/'),  # 协程3
])

4、总结

  1. 什么是协程?
    协程也可以称为“微线程”,就是开发者控制线程执行流程,控制先执行某段代码然后再切换到另外函执行代码...来回切换。

  2. 协程可以提高并发吗?
    协程自己本身无法实现并发(甚至性能会降低)。
    协程+IO切换性能提高。

  3. 进程、线程、协程的区别?

  4. 单线程提供并发:

    协程+IO切换:gevent

    基于事件循环的异步非阻塞框架:Twisted

第四章 Python 异步

1、前言

想学asyncio,得先了解协程,协程是根本呀!

协程(Coroutine),也可以被称为微线程,是一种用户态内的上下文切换技术。简而言之,其实就是通过一个线程实现代码块相互切换执行。例如:

def func1():
    print(1)
    ...
    print(2)
def func2():
    print(3)
    ...
    print(4)
func1()
func2()

上述代码是普通的函数定义和执行,按流程分别执行两个函数中的代码,并先后会输出:1、2、3、4。但如果介入协程技术那么就可以实现函数见代码切换执行,最终输入:1、3、2、4

在Python中有多种方式可以实现协程,例如:

  • greenlet,是一个第三方模块,用于实现协程代码(Gevent协程就是基于greenlet实现)
  • yield,生成器,借助生成器的特点也可以实现协程代码。
  • asyncio,在Python3.4中引入的模块用于编写协程代码。
  • async & awiat,在Python3.5中引入的两个关键字,结合asyncio模块可以更方便的编写协程代码。

2、asyncio

(1) 基本使用

在Python3.4之前官方未提供协程的类库,一般大家都是使用greenlet等其他来实现。在Python3.4发布后官方正式支持协程,即:asyncio模块。

import asyncio

@asyncio.coroutine
def func1():
    print(1)
    yield from asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
    print(2)

@asyncio.coroutine
def func2():
    print(3)
    yield from asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
    print(4)

tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

运行结果

1
3
2
4

注意:基于asyncio模块实现的协程比之前的要更厉害,因为他的内部还集成了遇到IO耗时操作自动切花的功能。

(2) async & awit

async & awit 关键字在Python3.5版本中正式引入,基于他编写的协程代码其实就是 上一示例 的加强版,让代码可以更加简便。

Python3.8之后 @asyncio.coroutine 装饰器就会被移除,推荐使用async & awit 关键字实现协程代码。

import asyncio

async def func1():
    print(1)
    await asyncio.sleep(2)
    print(2)


async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)


tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

运行结果

1
3
2
4

(3) 小结

关于协程有多种实现方式,目前主流使用是Python官方推荐的asyncio模块和async&await关键字的方式,例如:在tonado、sanic、fastapi、django3 中均已支持。

接下来,我们也会针对 asyncio模块 + async & await 关键字进行更加详细的讲解。

3、协程的意义

通过学习,我们已经了解到协程可以通过一个线程在多个上下文中进行来回切换执行。

但是,协程来回切换执行的意义何在呢?(网上看到很多文章舔协程,协程牛逼之处是哪里呢?)

  1. 计算型的操作,利用协程来回切换执行,没有任何意义,来回切换并保存状态 反倒会降低性能。
  2. IO型的操作,利用协程在IO等待时间就去切换执行其他任务,当IO操作结束后再自动回调,那么就会大大节省资源并提供性能,从而实现异步编程(不等待任务结束就可以去执行其他代码)。

(1) 爬虫案例

例如:用代码实现下载 url_list 中的图片。

1-1 同步编程实现

"""
下载图片使用第三方模块requests,请提前安装:pip3 install requests
"""
import requests

def download_image(url):
    print("开始下载:", url)
    # 发送网络请求,下载图片
    response = requests.get(url)
    print("下载完成")
    # 图片保存到本地文件
    file_name = url.rsplit('_')[-1]
    with open(file_name, mode='wb') as file_object:
        file_object.write(response.content)

if __name__ == '__main__':
    url_list = [
        'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
        'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
        'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
    ]
    for item in url_list:
        download_image(item)

1-2 基于协程的异步编程实现

"""
下载图片使用第三方模块aiohttp,请提前安装:pip3 install aiohttp
"""
# !/usr/bin/env python
# -*- coding:utf-8 -*-
import aiohttp
import asyncio

async def fetch(session, url):
    print("发送请求:", url)
    async with session.get(url, verify_ssl=False) as response:
        content = await response.content.read()
        file_name = url.rsplit('_')[-1]
        with open(file_name, mode='wb') as file_object:
            file_object.write(content)

async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
            'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
            'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
        ]
        tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]
        await asyncio.wait(tasks)

if __name__ == '__main__':
    asyncio.run(main())

上述两种的执行对比之后会发现,基于协程的异步编程 要比 同步编程的效率高了很多。因为:

  • 同步编程,按照顺序逐一排队执行,如果图片下载时间为2分钟,那么全部执行完则需要6分钟。
  • 异步编程,几乎同时发出了3个下载任务的请求(遇到IO请求自动切换去发送其他任务请求),如果图片下载时间为2分钟,那么全部执行完毕也大概需要2分钟左右就可以了。

(2) 小结

协程一般应用在有IO操作的程序中,因为协程可以利用IO等待的时间去执行一些其他的代码,从而提升代码执行效率。

生活中不也是这样的么,假设 你是一家制造汽车的老板,员工点击设备的【开始】按钮之后,在设备前需等待30分钟,然后点击【结束】按钮,此时作为老板的你一定希望这个员工在等待的那30分钟的时间去做点其他的工作。

4、异步编程

基于async & await关键字的协程可以实现异步编程,这也是目前python异步相关的主流技术。

想要真正的了解Python中内置的异步编程,根据下文的顺序一点点来看。

(1) 事件循环

事件循环,可以把他当做是一个while循环,这个while循环在周期性的运行并执行一些任务,在特定条件下终止循环。

# 伪代码
任务列表 = [ 任务1, 任务2, 任务3,... ]
while True:
    可执行的任务列表,已完成的任务列表 = 去任务列表中检查所有的任务,将'可执行'和'已完成'的任务返回
    for 就绪任务 in 已准备就绪的任务列表:
        执行已就绪的任务
    for 已完成的任务 in 已完成的任务列表:
        在任务列表中移除 已完成的任务
    如果 任务列表 中的任务都已完成,则终止循环

在编写程序时候可以通过如下代码来获取和创建事件循环。

import asyncio
loop = asyncio.get_event_loop()

(2) 协程和异步编程

协程函数,定义形式为 async def 的函数。

协程对象,调用 协程函数 所返回的对象

# 定义一个协程函数
async def func():
    pass

# 调用协程函数,返回一个协程对象
result = func()

注意:调用协程函数时,函数内部代码不会执行,只是会返回一个协程对象。

2-1 基本应用

程序中,如果想要执行协程函数的内部代码,需要 事件循环协程对象 配合才能实现,如:

import asyncio
async def func():
    print("协程内部代码")

# 调用协程函数,返回一个协程对象。
result = func()
"""
# python 3.7 以下版本
# 创建一个事件循环
# 将协程当做任务提交到事件循环的任务列表中,协程执行完成之后终止。
"""
loop = asyncio.get_event_loop()
loop.run_until_complete(result)

"""
# python 3.7 及以上版本
本质上方式一是一样的,内部先 创建事件循环 然后执行 run_until_complete,一个简便的写法。
asyncio.run 函数在 Python 3.7 中加入 asyncio 模块,
asyncio.run(result)
"""

这个过程可以简单理解为:将协程当做任务添加到 事件循环 的任务列表,然后事件循环检测列表中的协程是否 已准备就绪(默认可理解为就绪状态),如果准备就绪则执行其内部代码。

2-2 await

await是一个只能在协程函数中使用的关键字,用于遇到IO操作时挂起 当前协程(任务),当前协程(任务)挂起过程中 事件循环可以去执行其他的协程(任务),当前协程IO处理完成时,可以再次切换回来执行await之后的代码。代码如下:

案例1

import asyncio

async def func():
    print("执行协程函数内部代码")
    # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。
    # 当前协程挂起时,事件循环可以去执行其他协程(任务)。
    response = await asyncio.sleep(2)
    print("IO请求结束,结果为:", response)

result = func()
loop = asyncio.get_event_loop()
loop.run_until_complete(result)

案例2

import asyncio

async def others():
    print("start")
    await asyncio.sleep(2)
    print('end')
    return '返回值'

async def func():
    print("执行协程函数内部代码")
    # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)。
    response = await others()
    print("IO请求结束,结果为:", response)

loop = asyncio.get_event_loop()
loop.run_until_complete(func())

案例3

import asyncio

async def others():
    print("start")
    await asyncio.sleep(2)
    print('end')
    return '返回值'

async def func():
    print("执行协程函数内部代码")
    # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)。
    response1 = await others()
    print("IO请求结束,结果为:", response1)
    response2 = await others()
    print("IO请求结束,结果为:", response2)

loop = asyncio.get_event_loop()
loop.run_until_complete(func())

上述的所有示例都只是创建了一个任务,即:事件循环的任务列表中只有一个任务,所以在IO等待时无法演示切换到其他任务效果。

在程序想要创建多个任务对象,需要使用Task对象来实现。

2-3 Task对象

https://docs.python.org/3.8/library/asyncio-task.html#asyncio.create_task

Tasks用于并发调度协程,通过asyncio.create_task(协程对象)的方式创建Task对象,这样可以让协程加入事件循环中等待被调度执行。除了使用 asyncio.create_task() 函数以外,还可以用低层级的 loop.create_task()ensure_future() 函数。不建议手动实例化 Task 对象。

本质上是将协程对象封装成task对象,并将协程立即加入事件循环,同时追踪协程的状态。

注意:asyncio.create_task() 函数在 Python 3.7 中被加入。在 Python 3.7 之前,可以改用低层级的 asyncio.ensure_future() 函数。

  • 案例1

    import asyncio
    
    async def func():
        print(1)
        await asyncio.sleep(2)
        print(2)
        return "返回值"
    
    async def main():
        print("main开始")
        
        loop = asyncio.get_event_loop()
        # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
        task1 = loop.create_task(func())
        # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
        task2 = loop.create_task(func())
        print("main结束")
        # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
        # 此处的await是等待相对应的协程全都执行完毕并获取结果
        ret1 = await task1
        ret2 = await task2
        print(ret1, ret2)
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    
  • 案例2

    import asyncio
    
    async def func():
        print(1)
        await asyncio.sleep(2)
        print(2)
        return "返回值"
    
    async def main():
        print("main开始")
        # 创建协程,将协程封装到Task对象中并添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
        # 在调用
        loop = asyncio.get_event_loop()
        task_list = [
            loop.create_task(func()),
            loop.create_task(func())
        ]
        print("main结束")
        # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
        # 此处的await是等待所有协程执行完毕,并将所有协程的返回值保存到done
        # 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中。
        done, pending = await asyncio.wait(task_list, timeout=None)
        print(done, pending)
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    

    注意:asyncio.wait 源码内部会对列表中的每个协程执行ensure_future从而封装为Task对象,所以在和wait配合使用时task_list的值为[func(),func()] 也是可以的。

  • 案例3

    import asyncio
    
    async def func():
        print("执行协程函数内部代码")
        # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)。
        response = await asyncio.sleep(2)
        print("IO请求结束,结果为:", response)
    
    
    coroutine_list = [func(), func()]
    # 错误:coroutine_list = [ asyncio.create_task(func()), asyncio.create_task(func()) ]
    # 此处不能直接 asyncio.create_task,因为将Task立即加入到事件循环的任务列表,
    # 但此时事件循环还未创建,所以会报错。
    # 使用asyncio.wait将列表封装为一个协程,并调用asyncio.run实现执行两个协程
    # asyncio.wait内部会对列表中的每个协程执行ensure_future,封装为Task对象。
    loop = asyncio.get_event_loop()
    done, pending = loop.run_until_complete(asyncio.wait(coroutine_list))
    

2-4 asyncio.Future对象

asyncio中的Future对象是一个相对更偏向底层的可对象,通常我们不会直接用到这个对象,而是直接使用Task对象来完成任务的并和状态的追踪。( Task 是 Futrue的子类 )

Future为我们提供了异步编程中的 最终结果 的处理(Task类也具备状态处理的功能)。

  • 案例1

    import asyncio
    
    async def main():
        # 获取当前事件循环
        loop = asyncio.get_event_loop()
        # # 创建一个任务(Future对象),这个任务什么都不干。
        fut = loop.create_future()
        # 等待任务最终结果(Future对象),没有结果则会一直等下去。
        await fut
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    
  • 案例2

    import asyncio
    
    async def set_after(fut):
        await asyncio.sleep(2)
        fut.set_result("666")
    
    async def main():
        # 获取当前事件循环
        loop = asyncio.get_event_loop()
        # 创建一个任务(Future对象),没绑定任何行为,则这个任务永远不知道什么时候结束。
        fut = loop.create_future()
        # 创建一个任务(Task对象),绑定了set_after函数,函数内部在2s之后,会给fut赋值。
        # 即手动设置future任务的最终结果,那么fut就可以结束了。
        await loop.create_task(set_after(fut))
        # 等待 Future对象获取 最终结果,否则一直等下去
        data = await fut
        print(data)
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    

    Future对象本身函数进行绑定,所以想要让事件循环获取Future的结果,则需要手动设置。而Task对象继承了Future对象,其实就对Future进行扩展,他可以实现在对应绑定的函数执行完成之后,自动执行set_result,从而实现自动结束。

    虽然,平时使用的是Task对象,但对于结果的处理本质是基于Future对象来实现的。

    扩展:支持 await 对象语 法的对象课成为可等待对象,所以 协程对象Task对象Future对象 都可以被成为可等待对象。

2-5 futures.Future对象

在Python的concurrent.futures模块中也有一个Future对象,这个对象是基于线程池和进程池实现异步操作时使用的对象。

import time
from concurrent.futures import Future
from concurrent.futures.thread import ThreadPoolExecutor
from concurrent.futures.process import ProcessPoolExecutor

def func(value):
    time.sleep(1)
    print(value)

pool = ThreadPoolExecutor(max_workers=5)
# 或 pool = ProcessPoolExecutor(max_workers=5)
for i in range(10):
    fut = pool.submit(func, i)
    print(fut)

两个Future对象是不同的,他们是为不同的应用场景而设计,例如:concurrent.futures.Future不支持await语法 等。

官方提示两对象之间不同:

在Python提供了一个将futures.Future 对象包装成asyncio.Future对象的函数 asynic.wrap_future

接下里你肯定问:为什么python会提供这种功能?

其实,一般在程序开发中我们要么统一使用 asycio 的协程实现异步操作、要么都使用进程池和线程池实现异步操作。但如果 协程的异步进程池/线程池的异步 混搭时,那么就会用到此功能了

import time
import asyncio
import concurrent.futures

def func1():
    # 某个耗时操作
    time.sleep(2)
    return "SB"

async def main():
    loop = asyncio.get_event_loop()
    # 1. Run in the default loop's executor ( 默认ThreadPoolExecutor )
    # 第一步:内部会先调用 ThreadPoolExecutor 的 submit 方法去线程池中申请一个线程去执行func1函数,并返回一个concurrent.futures.Future对象
    # 第二步:调用asyncio.wrap_future将concurrent.futures.Future对象包装为asycio.Future对象。
    # 因为concurrent.futures.Future对象不支持await语法,所以需要包装为 asycio.Future对象 才能使用。
    fut = loop.run_in_executor(None, func1)
    result = await fut
    print('default thread pool', result)

    # 2. Run in a custom thread pool:
    # with concurrent.futures.ThreadPoolExecutor() as pool:
    #     result = await loop.run_in_executor(
    #         pool, func1)
    #     print('custom thread pool', result)
    # 3. Run in a custom process pool:
    # with concurrent.futures.ProcessPoolExecutor() as pool:
    #     result = await loop.run_in_executor(
    #         pool, func1)
    #     print('custom process pool', result)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

应用场景:当项目以协程式的异步编程开发时,如果要使用一个第三方模块,而第三方模块不支持协程方式异步编程时,就需要用到这个功能,例如:

import asyncio
import requests

async def download_image(url):
    # 发送网络请求,下载图片(遇到网络下载图片的IO请求,自动化切换到其他任务)
    print("开始下载:", url)
    loop = asyncio.get_event_loop()
    # requests模块默认不支持异步操作,所以就使用线程池来配合实现了。
    future = loop.run_in_executor(None, requests.get, url)
    response = await future
    print('下载完成')
    # 图片保存到本地文件
    file_name = url.rsplit('/')[-1]
    with open(file_name, mode='wb') as file_object:
        file_object.write(response.content)

if __name__ == '__main__':
    url_list = [
        'http://pic.sc.chinaz.com/Files/pic/pic9/202008/hpic2824_s.jpg',
        'http://pic.sc.chinaz.com/Files/pic/pic9/202008/hpic2825_s.jpg',
        'http://pic.sc.chinaz.com/Files/pic/pic9/202008/hpic2828_s.jpg'
    ]
    tasks = [download_image(url) for url in url_list]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))

2-6 异步迭代器

  • 什么是异步迭代器

    实现了 __aiter__()__anext__() 方法的对象。__anext__ 必须返回一个 awaitable 对象。async for 会处理异步迭代器的 __anext__() 方法所返回的可等待对象,直到其引发一个 StopAsyncIteration 异常。由 PEP 492 引入。

  • 什么是异步可迭代对象?

    可在 async for 语句中被使用的对象。必须通过它的 __aiter__() 方法返回一个 asynchronous iterator。由 PEP 492 引入。

  • 基本使用

    import asyncio
    
    class Reader(object):
        """ 自定义异步迭代器(同时也是异步可迭代对象) """
    
        def __init__(self):
            self.count = 0
    
        async def readline(self):
            # await asyncio.sleep(1)
            self.count += 1
            if self.count == 100:
                return None
            return self.count
    
        def __aiter__(self):
            return self
    
        async def __anext__(self):
            val = await self.readline()
            if val == None:
                raise StopAsyncIteration
            return val
    
    async def func():
        # 创建异步可迭代对象
        async_iter = Reader()
        # async for 必须要放在async def函数内,否则语法错误。
        async for item in async_iter:
            print(item)
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(func())
    

    异步迭代器其实没什么太大的作用,只是支持了async for语法而已。

2-7 异步上下文管理器

此种对象通过定义 __aenter__()__aexit__() 方法来对 async with 语句中的环境进行控制。由 PEP 492 引入。

import asyncio

class AsyncContextManager:
    def __init__(self):
        self.conn = None

    async def do_something(self):
        # 异步操作数据库
        return 666

    async def __aenter__(self):
        # 异步链接数据库
        self.conn = await asyncio.sleep(1)
        return self

    async def __aexit__(self, exc_type, exc, tb):
        # 异步关闭数据库链接
        await asyncio.sleep(1)

async def func():
    async with AsyncContextManager() as f:
        result = await f.do_something()
        print(result)

loop = asyncio.get_event_loop()
loop.run_until_complete(func())

这个异步的上下文管理器还是比较有用的,平时在开发过程中 打开、处理、关闭 操作时,就可以用这种方式来处理。

(3) 小结

在程序中只要看到asyncawait关键字,其内部就是基于协程实现的异步编程,这种异步编程是通过一个线程在IO等待时间去执行其他任务,从而实现并发。

以上就是异步编程的常见操作,内容参考官方文档。

5、uvloop

Python标准库中提供了asyncio模块,用于支持基于协程的异步编程。

uvloop是 asyncio 中的事件循环的替代方案,替换后可以使得asyncio性能提高。事实上,uvloop要比nodejs、gevent等其他python异步框架至少要快2倍,性能可以比肩Go语言。

(1) 安装uvloop

pip3 install uvloop

(2) 基本使用

在项目中想要使用uvloop替换asyncio的事件循环也非常简单,只要在代码中这么做就行。

import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
# 编写asyncio的代码,与之前写的代码一致。
# 内部的事件循环自动化会变为uvloop
asyncio.run(...)

注意:知名的asgi uvicorn内部就是使用的uvloop的事件循环。

6、实战案例

(1) 异步Redis

当通过python去操作redis时,链接、设置值、获取值 这些都涉及网络IO请求,使用asycio异步的方式可以在IO等待时去做一些其他任务,从而提升性能。

安装Python异步操作redis模块

pip3 install aioredis

1-1 案例:异步操作Redis

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import asyncio
import aioredis

async def execute(address, password):
    print("开始执行", address)
    # 网络IO操作:创建redis连接
    redis = await aioredis.create_redis(address, password=password)
    # 网络IO操作:在redis中设置哈希值car,内部在设三个键值对,即: redis = { car:{key1:1,key2:2,key3:3}}
    await redis.hmset_dict('car', key1=1, key2=2, key3=3)
    # 网络IO操作:去redis中获取值
    result = await redis.hgetall('car', encoding='utf-8')
    print(result)
    redis.close()
    # 网络IO操作:关闭redis连接
    await redis.wait_closed()
    print("结束", address)

loop = asyncio.get_event_loop()
loop.run_until_complete(execute('redis://127.0.0.1:6379', "root!123456"))

1-2 案例:连接多个Redis做操作

遇到IO会切换其他任务,提供了性能。

import asyncio
import aioredis

async def execute(address, password):
    print("开始执行", address)
    # 网络IO操作:先去连接 47.93.4.197:6379,遇到IO则自动切换任务,去连接47.93.4.198:6379
    redis = await aioredis.create_redis_pool(address, password=password)
    # 网络IO操作:遇到IO会自动切换任务
    await redis.hmset_dict('car', key1=1, key2=2, key3=3)
    # 网络IO操作:遇到IO会自动切换任务
    result = await redis.hgetall('car', encoding='utf-8')
    print(result)
    redis.close()
    # 网络IO操作:遇到IO会自动切换任务
    await redis.wait_closed()
    print("结束", address)

task_list = [
    execute('redis://47.93.4.197:6379', "root!2345"),
    execute('redis://47.93.4.198:6379', "root!2345")
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(task_list))

更多redis操作参考aioredis官网:https://aioredis.readthedocs.io/en/v1.3.0/start.html

(2) 异步MySQL

当通过python去操作MySQL时,连接、执行SQL、关闭都涉及网络IO请求,使用asycio异步的方式可以在IO等待时去做一些其他任务,从而提升性能。

安装Python异步操作redis模块

pip3 install aiomysql

2-1 案例:异步操作MySQL

import asyncio
import aiomysql

async def execute():
    # 网络IO操作:连接MySQL
    conn = await aiomysql.connect(host='127.0.0.1', port=3306, user='root', password='19971215', db='mysql', )
    # 网络IO操作:创建CURSOR
    cur = await conn.cursor()
    # 网络IO操作:执行SQL
    await cur.execute("SELECT Host,User FROM user")
    # 网络IO操作:获取SQL结果
    result = await cur.fetchall()
    print(result)
    # 网络IO操作:关闭链接
    await cur.close()
    conn.close()

loop = asyncio.get_event_loop()
loop.run_until_complete(execute())

2-2 案例:连接多个MySQL做操作

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import asyncio
import aiomysql

async def execute(host, password):
    print("开始", host)
    # 网络IO操作:先去连接 47.93.40.197,遇到IO则自动切换任务,去连接47.93.40.198:6379
    conn = await aiomysql.connect(host=host, port=3306, user='root', password=password, db='mysql')
    # 网络IO操作:遇到IO会自动切换任务
    cur = await conn.cursor()
    # 网络IO操作:遇到IO会自动切换任务
    await cur.execute("SELECT Host,User FROM user")
    # 网络IO操作:遇到IO会自动切换任务
    result = await cur.fetchall()
    print(result)
    # 网络IO操作:遇到IO会自动切换任务
    await cur.close()
    conn.close()
    print("结束", host)

task_list = [
    execute('127.0.0.1', "19971215"),
    execute('47.108.63.228', "19971215")
]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(task_list))

(3) FastAPI框架

FastAPI是一款用于构建API的高性能web框架,框架基于Python3.6+的 type hints搭建。

接下里的异步示例以FastAPIuvicorn来讲解(uvicorn是一个支持异步的asgi)。

安装FastAPI web 框架,

pip3 install fastapi

安装uvicorn,本质上为web提供socket server的支持的asgi(一般支持异步称asgi、不支持异步称wsgi)

pip3 install uvicorn

3-1 案例

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import asyncio
import uvicorn
import aioredis
from aioredis import Redis
from fastapi import FastAPI

app = FastAPI()
REDIS_POOL = aioredis.ConnectionsPool('redis://47.193.14.198:6379', password="root123", minsize=1, maxsize=10)

@app.get("/")
def index():
    """ 普通操作接口 """
    return {"message": "Hello World"}

@app.get("/red")
async def red():
    """ 异步操作接口 """
    print("请求来了")
    await asyncio.sleep(3)
    # 连接池获取一个连接
    conn = await REDIS_POOL.acquire()
    redis = Redis(conn)
    # 设置值
    await redis.hmset_dict('car', key1=1, key2=2, key3=3)
    # 读取值
    result = await redis.hgetall('car', encoding='utf-8')
    print(result)
    # 连接归还连接池
    REDIS_POOL.release(conn)
    return result

if __name__ == '__main__':
    uvicorn.run("luffy:app", host="127.0.0.1", port=5000, log_level="info")

在有多个用户并发请求的情况下,异步方式来编写的接口可以在IO等待过程中去处理其他的请求,提供性能。

例如:同时有两个用户并发来向接口 http://127.0.0.1:5000/red 发送请求,服务端只有一个线程,同一时刻只有一个请求被处理。 异步处理可以提供并发是因为:当视图函数在处理第一个请求时,第二个请求此时是等待被处理的状态,当第一个请求遇到IO等待时,会自动切换去接收并处理第二个请求,当遇到IO时自动化切换至其他请求,一旦有请求IO执行完毕,则会再次回到指定请求向下继续执行其功能代码。

基于上下文管理,来实现自动化管理的案例:

3-2 案例:Redis

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import asyncio
import uvicorn
import aioredis
from aioredis import Redis
from fastapi import FastAPI

app = FastAPI()
REDIS_POOL = aioredis.ConnectionsPool('redis://47.193.14.198:6379', password="root123", minsize=1, maxsize=10)

@app.get("/")
def index():
    """ 普通操作接口 """
    return {"message": "Hello World"}

@app.get("/red")
async def red():
    """ 异步操作接口 """
    print("请求来了")
    async with REDIS_POOL.get() as conn:
        redis = Redis(conn)
        # 设置值
        await redis.hmset_dict('car', key1=1, key2=2, key3=3)
        # 读取值
        result = await redis.hgetall('car', encoding='utf-8')
        print(result)
    return result

if __name__ == '__main__':
    uvicorn.run("fast3:app", host="127.0.0.1", port=5000, log_level="info")

3-3 案例:MySQL

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import asyncio
import uvicorn
from fastapi import FastAPI
import aiomysql

app = FastAPI()
# 创建数据库连接池
pool = aiomysql.Pool(host='127.0.0.1', port=3306, user='root', password='123', db='mysql',
                     minsize=1, maxsize=10, echo=False, pool_recycle=-1, loop=asyncio.get_event_loop())

@app.get("/red")
async def red():
    """ 异步操作接口 """
    # 去数据库连接池申请链接
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            # 网络IO操作:执行SQL
            await cur.execute("SELECT Host,User FROM user")
            # 网络IO操作:获取SQL结果
            result = await cur.fetchall()
            print(result)
            # 网络IO操作:关闭链接
    return {"result": "ok"}


if __name__ == '__main__':
    uvicorn.run("fast2:app", host="127.0.0.1", port=5000, log_level="info")

(4) 爬虫

在编写爬虫应用时,需要通过网络IO去请求目标数据,这种情况适合使用异步编程来提升性能,接下来我们使用支持异步编程的aiohttp模块来实现。

安装aiohttp模块

pip3 install aiohttp

4-1 案例:基本使用

import aiohttp
import asyncio

async def fetch(session, url):
    print("发送请求:", url)
    async with session.get(url, verify_ssl=False) as response:
        text = await response.text()
        print("得到结果:", url, len(text))

async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            'https://python.org',
            'https://www.baidu.com',
            'https://www.pythonav.com'
        ]
        loop = asyncio.get_event_loop()
        tasks = [loop.create_task(fetch(session, url)) for url in url_list]
        await asyncio.wait(tasks)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

7、感言

为了提升性能越来越多的框架都在向异步编程靠拢,例如:sanic、tornado、django3.0、django channels组件 等,用更少资源可以做处理更多的事,何乐而不为呢。

第五章 Python 定时任务

https://mp.weixin.qq.com/s/O_qPn0ZvghSt2AR3iZPwVw

1、while+True+Sleep

位于 time 模块中的 sleep(secs) 函数,可以实现令当前执行的线程暂停 secs 秒后再继续执行。所谓暂停,即令当前线程进入阻塞状态,当达到 sleep() 函数规定的时间后,再由阻塞状态转为就绪状态,等待 CPU 调度。

基于这样的特性我们可以通过 while 死循环+sleep() 的方式实现简单的定时任务。

import time
import datetime


def time_printer():
    now = datetime.datetime.now()
    ts = now.strftime('%Y-%m-%d %H:%M:%S')
    print('do func time :', ts)


def loop_monitor():
    while True:
        time_printer()
        time.sleep(5)  # 暂停 5 秒


if __name__ == "__main__":
    loop_monitor()

主要缺点:

  • 只能设定间隔,不能指定具体的时间,比如每天早上 8:00
  • sleep 是一个阻塞函数,也就是说 sleep 这一段时间,程序什么也不能操作。

2、Timeloop

(1) 前戏

安装

pip install timeloop -i https://pypi.douban.com/simple

(2) 基本使用

import time
from timeloop import Timeloop
from datetime import timedelta

tl = Timeloop()


@tl.job(interval=timedelta(seconds=2))
def sample_job_every_2s():
    print("2s job current time : {}".format(time.ctime()))


@tl.job(interval=timedelta(seconds=5))
def sample_job_every_5s():
    print("5s job current time : {}".format(time.ctime()))


@tl.job(interval=timedelta(seconds=10))
def sample_job_every_10s():
    print("10s job current time : {}".format(time.ctime()))


tl.start()
while True:
    time.sleep(1000)

3、APScheduler

(1) 前戏

安装

pip install apscheduler

基本使用

from datetime import datetime
from apscheduler.schedulers.blocking import BlockingScheduler

def task():
    print("当前时间:", datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f"))

# 1. 实例化
scheduler = BlockingScheduler()
# 2. 创建任务每3秒触发一次
scheduler.add_job(task, 'interval', seconds=3)
# 3. 启动任务
scheduler.start()

(2) APScheduler 调度器组件

组件 简介 说明
triggers 触发器 用于设定触发任务的条件
job stores 任务储存器 用于存放任务,把任务存放在内存或数据库中
executors 执行器 用于执行任务,可以设定执行模式为单线程或线程池
schedulers 调度器 把上方三个组件作为参数,通过创建调度器实例来运行

调度器组件详解

根据开发需求选择相应的组件,下面是不同的调度器组件:

组件 简介 说明
BlockingScheduler 阻塞式调度器 适用于只跑调度器的程序
BackgroundScheduler 后台调度器 适用于非阻塞的情况,调度器会在后台独立运行
AsyncIOScheduler AsyncIO调度器 适用于应用使用AsnycIO的情况
GeventScheduler Gevent调度器 适用于应用通过Gevent的情况
TornadoScheduler Tornado调度器 适用于构建Tornado应用
TwistedScheduler Twisted调度器 适用于构建Twisted应用
QtScheduler Qt调度器 适用于构建Qt应用

启动调度器

启动调度器只需调用调度器上的start()。除了BlockingScheduler以外的调度程序,此调用将立即返回,你可以继续应用程序的初始化过程,例如向调度程序添加作业。

对于BlockingScheduler,只需要在完成任何初始化步骤之后调用start()。

注意:启动调度程序后,不能再更改其设置。

from datetime import date, datetime
from apscheduler.schedulers.blocking import BlockingScheduler

def task(text):
    print(text)

scheduler = BlockingScheduler()
scheduler.add_job(task, 'date', run_date=date(2021, 4, 12), args=['测试任务1'])
scheduler.start()

关闭调度器

scheduler.shutdown()

默认情况下,调度程序关闭其作业存储和执行器,并等待所有当前执行的作业完成。如果你不想等,你可以执行:

scheduler.shutdown(wait=False)

这仍然会关闭作业存储和执行器,但不会等待任何正在运行的任务完成。

from datetime import date, datetime
from apscheduler.schedulers.blocking import BlockingScheduler

def task(text):
    print(text)

scheduler = BlockingScheduler()
scheduler.add_job(task, 'date', run_date=date(2021, 4, 12), args=['测试任务1'])
scheduler.start()
scheduler.shutdown(wait=False)

配置调度器

APScheduler提供了许多不同的方法来配置调度程序。可以使用配置字典,也可以将选项作为关键字参数传入。还可以先实例化调度器,然后添加作业并配置调度器。通过这种方式,可以为任何环境获得最大的灵活性。

  • 默认配置

    假设在你的应用程序中运行BackgroundScheduler、默认的job存储和默认的executors执行程序,如下:

    from apscheduler.schedulers.background import BackgroundScheduler
    
    scheduler = BackgroundScheduler()
    

    此时将得到一个BackgroundScheduler实例,其中MemoryJobStore为“default”作业存储方法,ThreadPoolExecutor为“default”执行器,默认最大线程数为10。參考: apscheduler\executors\pool.py的ThreadPoolExecutor

  • 高级配置

    若希望使用两个executor,拥有两个作业存储,还希望为新作业调整默认值并设置不同的时区。配置可实现如下:

    from datetime import datetime
    from pytz import utc
    from apscheduler.schedulers.blocking import BlockingScheduler
    from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
    from apscheduler.jobstores.redis import RedisJobStore
    from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
    
    jobstores = {
        'mongo': RedisJobStore(),
        'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
    }
    executors = {
        'default': ThreadPoolExecutor(20),
        'processpool': ProcessPoolExecutor(5)
    }
    job_defaults = {
        'coalesce': False,
        'max_instances': 3
    }
    scheduler = BlockingScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)
    
    def task():
        print("当前时间:", datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f"))
    
    scheduler.add_job(task, 'interval', seconds=5)
    scheduler.start()
    scheduler.shutdown()
    

(3) APScheduler 触发器组件

触发器组件详解

组件 简介 说明
date 日期 触发任务运行的具体日期
interval 间隔 触发任务运行的时间间隔
cron 周期 触发任务运行的周期

date

date 是最基本的一种调度,作业任务只会执行一次。它表示特定的时间点触发。它的参数如下:

语法:

scheduler.add_job(task, 'date', run_date=date(2021, 4, 12), args=['测试任务'])

参数:

参数 示例 说明
run_date date(2021, 4, 12) 任务运行的日期或者时间
timezone(datetime.tzinfo or str) 间隔 指定时区

案例:

from datetime import date, datetime
from apscheduler.schedulers.blocking import BlockingScheduler

def task(text):
    print(text)

scheduler = BlockingScheduler()
# 方式一:
scheduler.add_job(task, 'date', run_date=date(2021, 4, 12), args=['测试任务1'])
# 方式二:
scheduler.add_job(task, 'date', run_date="2021-04-12", args=['测试任务2'])
# 方式三:
scheduler.add_job(task, 'date', run_date=datetime(2021, 4, 12, 10, 27, 0), args=['测试任务3'])
# 方式四:
scheduler.add_job(task, 'date', run_date="2021-04-12 10:27:00", args=['测试任务4'])
scheduler.start()

interval

语法:

scheduler.add_job(job_func, 'interval', hours=2)

参数:

固定时间间隔触发。interval 间隔调度,参数如下:

参数 说明
weeks(int) 间隔几周
days(int) 间隔几天
hours(int) 间隔几小时
minutes(int) 间隔几分钟
seconds(int) 间隔多少秒
start_date(datetime or str) 开始日期
end_date(datetime or str) 结束日期
timezone(datetime.tzinfo or str) 时区

案例:

from datetime import datetime
from apscheduler.schedulers.blocking import BlockingScheduler

# 定义任务
def task():
    print("当前时间:", datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f"))

# 实例化
scheduler = BlockingScheduler()
# 1. 每间隔5秒触发
scheduler.add_job(task, 'interval', seconds=5)
# 2. 每间隔5分钟触发
scheduler.add_job(task, 'interval', minutes=5)
# 3. 每间隔5小时触发
scheduler.add_job(task, 'interval', hours=5)
# 4. 每间隔5天触发
scheduler.add_job(task, 'interval', days=5)
# 5. 每间隔5周触发
scheduler.add_job(task, 'interval', weeks=5)
# 6. 定义起止时间
# - 在 2021-04-12 00:00:00 ~ 2021-05-01 00:00:00 之间, 每隔两分钟执行一次 task 方法
scheduler.add_job(task, 'interval', minutes=2, start_date='2021-04-12 00:00:00', end_date='2021-05-01 00:00:00')
scheduler.start()

cron

语法:

scheduler.add_job(job_func, 'cron', month='1-3,7-9',day='0, tue', hour='0-3')

参数:

在特定时间周期性地触发。参数如下:

参数 说明
year (int 或 str) 年,4位数字
month (int 或 str) 月 (范围1-12)
day (int 或 str) 日 (范围1-31)
week (int 或 str) 周 (范围1-53)
day_of_week (int 或 str) 周内第几天或者星期几 (范围0-6 或者 mon,tue,wed,thu,fri,sat,sun)
hour (int 或 str) 时 (范围0-23)
minute (int 或 str) 分 (范围0-59)
second (int 或 str) 秒 (范围0-59)
start_date (datetime 或 str) 最早开始日期(包含)
end_date (datetime 或 str) 最晚结束时间(包含)
timezone (datetime.tzinfo 或str) 指定时区

这些参数支持算数表达式,取值格式有如下:

表达式 参数类型 描述
* 所有 通配符。例:minutes=*即每分钟触发
*/a 所有 可被a整除的通配符。
a-b 所有 范围a-b触发
a-b/c 所有 范围a-b,且可被c整除时触发
xth y 第几个星期几触发。x为第几个,y为星期几
last x 一个月中,最后个星期几触发
last 一个月最后一天触发
x,y,z 所有 组合表达式,可以组合确定值或上方的表达式

案例:

import time
from apscheduler.schedulers.blocking import BlockingScheduler

# 任务
def task(text):
    t = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))
    print('{} --- {}'.format(text, t))

# 实例化
scheduler = BlockingScheduler()

# 1. 每天中午12点准时执行任务
scheduler.add_job(task, 'cron', hour='12', args=['job1'])

# 2. 每天中午12点,每隔5分钟执行一次任务
scheduler.add_job(task, 'cron', hour='12', minute='*/5', args=['job2'])

# 3. 每天晚上0点到凌晨05点的30分执行一次任务
scheduler.add_job(task, 'cron', hour='0-5', minute='30', args=['job3'])

# 4. 周一到周五每天早上六点30分执行任务
scheduler.add_job(task, 'cron', day_of_week="1-5", hour='6', minute='30', args=['job4'])

# 5. 在每年 1-3、7-9 月份中的每个星期一、二中的 00:00, 01:00, 02:00 和 03:00 执行 task 任务
scheduler.add_job(task, 'cron', month='1-3,7-9', day='0, tue', hour='0-3')

# 启动
scheduler.start()

(3) APScheduler 执行器组件

执行器组件详解

如果有使用上面的5个框架之一,通常会选择对应框架的executor。否则,默认使用的ThreadPoolExecutor就可以满足大多数场景。如果工作涉及CPU密集型操作,则应该考虑使用ProcessPoolExecutor来充分利用多个CPU内核。也可以同时使用这两种方法,将进程池执行器添加为辅助执行器。

最常用的两种executor :ProcessPoolExecutor 和 ThreadPoolExecutor,其它的还有AsyncIOExecutor、DebugExecutor(一种特殊的执行程序,直接执行可调用的目标,而不是将其延迟给线程或进程)、GeventExecutor、TwistedExecutor。

import time
from datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor

executors = {
    'default': ThreadPoolExecutor(20),
}

def task():
    print("当前时间:", datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f"))

if __name__ == '__main__':
    scheduler = BackgroundScheduler(executors=executors)
    scheduler.add_job(task, 'interval', seconds=1)
    scheduler.start()
    print("--------------- end --------------")
    try:
        while True:
            time.sleep(2)
    except (KeyboardInterrupt, SystemExit):
        scheduler.shutdown()

(5) APScheduler 储存器组件

储存器组件详解

要选择适当的作业存储,首先要确定是否需要对作业数据持久化。如果总是在应用程序开始时重新创建作业,那么作业存储可以选择默认方式(MemoryJobStore)。否则,选择对应的持久化存储方式。jobstore提供对scheduler中job的增删改查接口,根据存储backend的不同,分以下几种:

组件 说明
MemoryJobStore 没有序列化,jobs就存在内存里,增删改查也都是在内存中操作
SQLAlchemyJobStore 所有sqlalchemy支持的数据库都可以做为backend,增删改查操作转化为对应backend的sql语句
MongoDBJobStore 用mongodb作backend
RedisJobStore 用redis作backend
RethinkDBJobStore 用rethinkdb 作backend
ZooKeeperJobStore 用ZooKeeper做backend

添加 job

共有两种方式进行新增job的操作:

  1. 基于add_job来动态增加:

    scheduler.add_job(job_function, 'cron', day_of_week='mon-fri', hour=5, minute=30, end_date='2014-05-30')
    
  2. 基于修饰器scheduled_job来动态装饰job的实际函数

    import time
    from apscheduler.schedulers.blocking import BlockingScheduler
    
    scheduler = BlockingScheduler()
    
    @scheduler.scheduled_job('cron', id='my_job_id', day='last sun')
    def some_decorated_task():
        print("I am printed at 00:00:00 on the last Sunday of every month!")
    

    在内置的作业存储中,只有MemoryJobStore不会序列化作业。在内置的执行器中,只有ProcessPoolExecutor会序列化作业。

    注意:如果在应用程序初始化期间在持久性作业存储中调度作业,则必须为作业定义显式ID,并使用'replace_existing=True',否则每次重新启动应用程序时,都会得到一份新的作业副本!

  3. 存储至数据库

    from datetime import datetime
    from pytz import utc
    from apscheduler.schedulers.blocking import BlockingScheduler
    from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
    from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
    
    jobstores = {
        'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
    }
    
    executors = {
        'default': ThreadPoolExecutor(20),
    }
    
    scheduler = BlockingScheduler(jobstores=jobstores, executors=executors, timezone=utc)
    
    def task():
        print("当前时间:", datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f"))
    
    scheduler.add_job(task, 'interval', seconds=5)
    scheduler.start()
    scheduler.shutdown()
    

移除 job

可以通过job对象调用remove删除。

也可通过scheduler对象,指定job id调用remove_job删除;scheduler对象还可调用remove_all_jobs删除所有job。

import time
from apscheduler.schedulers.blocking import BlockingScheduler

def task():
    t = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))
    print(t)

scheduler = BlockingScheduler()
# 方式1
job = scheduler.add_job(task, 'interval', minutes=2)
job.remove()

# 方式2
scheduler.add_job(task, 'interval', minutes=2, id='my_job_id')
scheduler.remove_job('my_job_id')

暂停和恢复 job

可以通过作业实例或调度程序本身对作业执行暂停和恢复操作。当作业暂停时,它的下一个运行时被清除,并且在作业恢复之前不会计算它的进一步运行时。

暂停作业:

apscheduler.job.Job.pause()
apscheduler.schedulers.base.BaseScheduler.pause_job()

恢复作业:

apscheduler.job.Job.resume()
apscheduler.schedulers.base.BaseScheduler.resume_job()

job 列表获取

要获得调度作业处理列表,可以使用get_jobs()方法。它将返回所有job实例。如果只需要获取特定作业存储库中包含的作业,可将作业存储别名作为第二个参数。

为了方便,可以使用print_jobs()方法,该方法将打印出格式化的作业列表、它们的触发器和下一次运行时。

apscheduler.get_jobs()

job 修改

可以通过apscheduler.job.Job.modify() 或apscheduler.modify_job()修改除了id之外的job属性。例如:

job.modify(max_instances=6, name='Alternate name')

如果你想修改job的调度器,也就是说,改变它的触发器。你可以使用apscheduler.job.Job.reschedule() 或reschedule_job()

scheduler.reschedule_job('my_job_id', trigger='cron', minute='*/5')

(6) 案例

blocking 调度器示例

演示使用blocking阻塞调度程序来调度每隔3秒执行一次的作业。

from datetime import datetime
from apscheduler.schedulers.blocking import BlockingScheduler

def task():
    print('Tick! The time is: %s' % datetime.now())

if __name__ == '__main__':
    scheduler = BlockingScheduler()
    scheduler.add_job(task, 'interval', seconds=3)
    try:
        scheduler.start()
    except (KeyboardInterrupt, SystemExit):
        pass

background 调度器示例

演示使用background后台调度程序来调度每隔3秒执行一次的作业。

import time
from datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler

def task():
    print('Tick! The time is: %s' % datetime.now())

if __name__ == '__main__':
    scheduler = BackgroundScheduler()
    scheduler.add_job(task, 'interval', seconds=3)
    scheduler.start()
    try:
        while True:
            time.sleep(2)
    except (KeyboardInterrupt, SystemExit):
        scheduler.shutdown()

第六章 Python Excel操作

1、openpyxl - 读取Excel文件

(1) 读取sheet

1-1 获取所有的 sheet 名称

from openpyxl import load_workbook

wb = load_workbook(filename='files/demo-01.xlsx')
# 获取所有的 sheet 名称
sheet_list = wb.sheetnames
print(sheet_list)

1-2 选择sheet - 基于sheet名称

from openpyxl import load_workbook

wb = load_workbook(filename='files/demo-01.xlsx')
# 选择sheet - 基于sheet名称
sheet_object = wb["学生信息表"]
print(sheet_object)

1-3 选择sheet - 基于索引位置

from openpyxl import load_workbook

wb = load_workbook(filename='files/demo-01.xlsx')
# 选择sheet - 基于索引位置
sheet_object = wb.worksheets[0]
print(sheet_object)

1-4 循环获取所有 sheet

from openpyxl import load_workbook

wb = load_workbook(filename='files/demo-01.xlsx')
for sheet_object in wb:
    print(sheet_object)

(2) 读取单元格

2-1 获取第N行第N列的单元格

from openpyxl import load_workbook

wb = load_workbook(filename='files/demo-01.xlsx')
sheet_object = wb.worksheets[0]
# 获取第N行第N列的单元格
cell = sheet_object.cell(1, 1)
print(cell.value)
print(cell.style)
print(cell.font.name)
print(cell.font.size)
print(cell.alignment)

2-2 获取某个单元格

from openpyxl import load_workbook

wb = load_workbook(filename='files/demo-01.xlsx')
sheet_object = wb.worksheets[0]
# 获取某个单元格
a2 = sheet_object["A2"]
print(a2.value)
c4 = sheet_object["C4"]
print(c4.value)

2-3 获取第N行所有的单元格

from openpyxl import load_workbook

wb = load_workbook(filename='files/demo-01.xlsx')
sheet_object = wb.worksheets[0]
# 获取第N行所有的单元格
for cell in sheet_object[1]:
    print(cell)
    print(cell.value)

2-4 获取所有行所有的数据

from openpyxl import load_workbook

wb = load_workbook(filename='files/demo-01.xlsx')
sheet_object = wb.worksheets[0]
# 获取所有行所有的数据
for row in sheet_object.rows:
    print(row)
    print(row[0].value)

2-5 获取所有列所有的数据

from openpyxl import load_workbook

wb = load_workbook(filename='files/demo-01.xlsx')
sheet_object = wb.worksheets[0]
# 获取所有列所有的数据
for col in sheet_object.columns:
    print(col)
    print(col[0].value)

(3) 读取合并单元格

3-1 获取第N行第N列的单元格

from openpyxl import load_workbook

wb = load_workbook(filename='files/demo-01.xlsx')
sheet_object = wb.worksheets[3]

# 获取第N行第N列的单元格
c1 = sheet_object.cell(1, 2)
print(c1)
print(c1.value)

c2 = sheet_object.cell(1, 3)
print(c2)
print(c2.value)

3-2 读取每一行每一列的数据

from openpyxl import load_workbook

# 读取每一行每一列的数据
wb = load_workbook(filename='files/demo-01.xlsx')
sheet_object = wb.worksheets[3]
for row in sheet_object.rows:
    print(row)

2、openpyxl - 写入Excel文件

(1) 原Excel文件基础上写内容

from openpyxl import load_workbook

# 1. 读取Excel并指定 sheet
wb = load_workbook(filename="files/demo-01.xlsx")
sheet_obj = wb.worksheets[3]

# 找到单元格,并修改单元格的内容
cell = sheet_obj.cell(1, 2)
cell.value = "教师基本信息"
wb.save(filename='files/demo-02.xlsx')

(2) 新创建Excel文件写内容

from openpyxl import workbook

# 1. 创建Excel且默认会创建一个sheet
wb = workbook.Workbook()
sheet_obj = wb["Sheet"]

# 2. 找到单元格,并修改单元格的内容
cell = sheet_obj.cell(1, 1)
cell.value = "新的开始"
wb.save(filename='files/demo-03.xlsx')

(3) 常用 sheet 操作

3-1 修改 sheet 名称

from openpyxl import workbook

wb = workbook.Workbook()
# 修改 sheet 名称
sheet = wb.worksheets[0]
sheet.title = "数据集"
wb.save("files/demo-04.xlsx")

3-2 创建 sheet 并设置 sheet 颜色

from openpyxl import workbook

wb = workbook.Workbook()
# 创建 sheet 并设置 sheet 颜色
sheet = wb.create_sheet('工作计划', 0)
sheet.sheet_properties.tabColor = '1072BA'
wb.save('files/demo-04.xlsx')

3-3 默认打开的 sheet

from openpyxl import workbook

wb = workbook.Workbook()
# 默认打开的 sheet
wb.active = 0
wb.save('files/demo-04.xlsx')

3-4 拷贝 sheet

from openpyxl import workbook

wb = workbook.Workbook()
# 拷贝 sheet
sheet = wb.create_sheet("Sheet")
sheet.sheet_properties.tabColor = '1072BA'
new_sheet = wb.copy_worksheet(wb["Sheet"])
new_sheet.title = "新的计划"
wb.save('files/demo-04.xlsx')

3-5 删除 sheet

from openpyxl import workbook

wb = workbook.Workbook()
# 删除 sheet
del wb["Sheet"]
wb.save('files/demo-04.xlsx')

(4) 常用 单元格 操作

4-1 获取某个单元格 - 修改值

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[0]

# 获取某个单元格 - 修改值
cell = sheet.cell(1, 1)
cell.value = "序号"
cell = sheet.cell(1, 2)
cell.value = "任务名称"
wb.save(filename='files/demo-04.xlsx')

4-2 获取某个单元格 - 修改值

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[0]
# 获取某个单元格,修改值
sheet["A2"] = 1
sheet["A3"] = 2
sheet["B2"] = "Python"
sheet["B3"] = "Java"
wb.save(filename='files/demo-04.xlsx')

4-3 获取某些单元格 - 修改值

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[0]
# 获取某些单元格,修改值
"""
cell_list = [
    ('A4', 'B4'),
    ('A5', 'B5')
]
"""
cell_list = sheet['A4': 'B5']
for row in cell_list:
    for cell in row:
        cell.value = "新的值"
wb.save(filename='files/demo-04.xlsx')

4-4 对其方式

from openpyxl import load_workbook
from openpyxl.styles import Alignment

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[0]
# 对其方式
cell = sheet.cell(1, 1)
cell.alignment = Alignment(horizontal='center', vertical='distributed', text_rotation=45, wrap_text=True)
wb.save(filename='files/demo-04.xlsx')

说明:

horizontal 可选值:

属性值 说明
general
left
center
right
fill
justify
centerContinuous
distributed

vertical 可选值:

属性值 说明
top
center
bottom
justify
distributed

text_rotation 说明: 旋转角度

wrap_text 说明: 是否自动换行

4-5 边框

from openpyxl import load_workbook
from openpyxl.styles import Border, Side

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[0]
cell = sheet.cell(8, 8)
cell.border = Border(
    top=Side(style='thin', color='FFB6C1'),
    bottom=Side(style='dashed', color='9932cc'),
    left=Side(style='dashed', color='9932cc'),
    right=Side(style='dashed', color='9932cc'),
    diagonal=Side(style='thin', color='483D8B'),
    diagonalUp=True,  # 左下 - 右上
    diagonalDown=True,  # 左上 - 右下
)
wb.save(filename='files/demo-04.xlsx')

Side style可选值

属性值 说明
dashDot
dashDotDot
dashed
dotted
double
hair
medium
mediumDashDot
mediumDashDotDot
mediumDashed
slantDashDot
thick
thin

4-6 字体

from openpyxl import load_workbook
from openpyxl.styles import Font

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[0]
cell = sheet.cell(2, 1)
cell.font = Font(name='等线', size=18, color='ff0000', underline='single')
wb.save(filename='files/demo-04.xlsx')

4-7 背景色

from openpyxl import load_workbook
from openpyxl.styles import PatternFill

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[0]
cell = sheet.cell(3, 1)
cell.fill = PatternFill('solid', fgColor='99ccff')
wb.save(filename='files/demo-04.xlsx')

4-8 渐变背景色

from openpyxl import load_workbook
from openpyxl.styles import GradientFill

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[0]
cell = sheet.cell(2, 2)
cell.fill = GradientFill('linear', stop=('FFFFFF', '99ccff', '000000'))
wb.save(filename='files/demo-04.xlsx')

4-9 宽高

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[0]
sheet.row_dimensions[1].height = 50
sheet.column_dimensions['E'].width = 100
wb.save(filename='files/demo-04.xlsx')

4-10 合并单元格

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[0]

# 方式一
sheet.merge_cells('C2:D5')
sheet.merge_cells(start_row=8, start_column=6, end_column=10, end_row=8)
wb.save(filename='files/demo-04.xlsx')

# 方式二
sheet.unmerge_cells('C2:D5')
wb.save(filename='files/demo-04.xlsx')

4-11 写入公式

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')

# 方式一
sheet = wb.worksheets[3]
sheet["E1"] = "合计"
sheet["E2"] = "=B2*C2"
wb.save(filename='files/demo-04.xlsx')

# 方式二
sheet = wb.worksheets[3]
sheet["E3"] = "=SUM(B3, B4)"
wb.save(filename='files/demo-04.xlsx')

4-12 删除

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[3]
sheet.delete_rows(idx=1, amount=2)
sheet.delete_rows(idx=1, amount=3)
wb.save(filename='files/demo-04.xlsx')

说明:

idx: 要删除的索引为位置

amount: 从索引位置开始要删除的个数(默认1)

4-13 插入

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[3]
sheet.insert_rows(idx=5, amount=10)
sheet.insert_cols(idx=3, amount=2)
wb.save(filename='files/demo-04.xlsx')

4-14 循环写入

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb["Sheet"]
cell_range = sheet['A1:C2']
for row in cell_range:
    for cell in row:
        cell.value = 'xx'

for row in sheet.iter_rows(min_row=5, min_col=1, max_row=10, max_col=10):
    for cell in row:
        cell.value = 'oo'
wb.save(filename='files/demo-04.xlsx')

4-15 移动

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[3]

# 方式一:
sheet.move_range("H2:J10", rows=-1, cols=15)
wb.save(filename='files/demo-04.xlsx')

# 方式二:
sheet = wb.worksheets[3]
sheet["D1"] = "合计"
sheet["D2"] = "=B2*C2"
sheet["D3"] = "=SUM(B3, C3)"
sheet.move_range("B1:D3", cols=10, translate=True)
wb.save(filename='files/demo-04.xlsx')

4-16 打印区域

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[3]
sheet.print_area = "A1:D200"
wb.save(filename='files/demo-04.xlsx')

4-17 打印时,每个页面的固定表头

from openpyxl import load_workbook

wb = load_workbook('files/demo-04.xlsx')
sheet = wb.worksheets[3]
sheet.print_title_cols = "A:D"
sheet.print_title_rows = "1:3"
wb.save(filename='files/demo-04.xlsx')

第七章 Python 正则表达式

1、前戏

(1) 正则表达式模式

模式 描述
^ 匹配字符串的开头
$ 匹配字符串的末尾。
. 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。
[...] 用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k'
[^...] 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。
re* 匹配0个或多个的表达式。
re+ 匹配1个或多个的表达式。
re? 匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
re 匹配n个前面表达式。例如,"o{2}"不能匹配"Bob"中的"o",但是能匹配"food"中的两个o。
re 精确匹配n个前面表达式。例如,"o{2,}"不能匹配"Bob"中的"o",但能匹配"foooood"中的所有o。"o{1,}"等价于"o+"。"o{0,}"则等价于"o*"。
re 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a| b 匹配a或b
(re) 匹配括号内的表达式,也表示一个组
(?imx) 正则表达式包含三种可选标志:i, m, 或 x 。只影响括号中的区域。
(?-imx) 正则表达式关闭 i, m, 或 x 可选标志。只影响括号中的区域。
(?: re) 类似 (...), 但是不表示一个组
(?imx: re) 在括号中使用i, m, 或 x 可选标志
(?-imx: re) 在括号中不使用i, m, 或 x 可选标志
(?#...) 注释.
(?= re) 前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。
(?! re) 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功。
(?> re) 匹配的独立模式,省去回溯。
\w 匹配数字字母下划线
\W 匹配非数字字母下划线
\s 匹配任意空白字符,等价于 [\t\n\r\f]。
\S 匹配任意非空字符
\d 匹配任意数字,等价于 [0-9]。
\D 匹配任意非数字
\A 匹配字符串开始
\Z 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。
\z 匹配字符串结束
\G 匹配最后匹配完成的位置。
\b 匹配一个单词边界,也就是指单词和空格间的位置。例如, 'er\b' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。
\B 匹配非单词边界。'er\B' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。
\n, \t, 等。 匹配一个换行符。匹配一个制表符, 等
\1...\9 匹配第n个分组的内容。
\10 匹配第n个分组的内容,如果它经匹配。否则指的是八进制字符码的表达式。

(2) 正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B.
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

2、find&findall

(1) find()

s1 = '菜鸟程序员_Python'
print(s1.find('程序员'))

(2) findall()

# ----- (1) \w 匹配中文,字母,数字,下划线
import re
name = "菜鸟程序员-re.findall() 详解"
print(re.findall("\w",name))

# ----- (2) \W 不匹配中文,字母,数字,下划线
import re
name = "菜鸟程序员-re.findall() 详解"
print(re.findall("\W",name))

# ----- (3) \s 匹配任意的空白符
import re
name = "菜鸟程序员-re.findall() 详解"
print(re.findall("\s",name))

# ----- (4) \S 匹配不是任意的空白符
import re
name = "菜鸟程序员-re.findall() 详解"
print(re.findall("\S",name))

# ----- (5) \d 匹配数字
import re
name = "菜鸟程序员-re.findall() 详解 2020/03/09"
print(re.findall("\d",name))

# ----- (6) \D 匹配非数字
import re
name = "菜鸟程序员-re.findall() 详解 2020/03/09"
print(re.findall("\D",name))

# ----- (7) \A 与 ^ 从字符串开头匹配
import re
name = "菜鸟程序员-re.findall() 详解"
print(re.findall("\A菜鸟程序员",name))
print(re.findall("^菜鸟程序员",name))

# ----- (8) \Z 与 \z 与 $ 字符串结尾匹配
# 字符串结束位置与则符合就匹配,否则不匹配,返回值是list
import re
name = "菜鸟程序员-re.findall() 详解"
print(re.findall("详解\Z",name))
print(re.findall("详解\z",name))
print(re.findall("详解$",name))

# ----- (9) 匹配任意字符(换行符除外,re.DOTALL)
import re
name = "菜鸟程序员-re.findall() 详解 \r\n"
print(re.findall(".",name))
print(re.findall(".",name,re.DOTALL))

# ----- (10) ? 匹配?前元素0个或1个
# 问号前面的一个字符可以是0次或1次,返回值是list
import re
name = "菜鸟程序员-re.findall() 详解 \r\n"
print(re.findall("re?",name))

# ----- (11) * 匹配 * 前面元素0个或多个 [贪婪匹配]
# 星号前面的一个字符可以是0次或多次,返回值是list
import re
name = "re - python_re - python_re.findall()"
print(re.findall("re*",name))
print(re.findall("python_re*",name))

# ----- (12) + 匹配 +前面元素1个或多个 [贪婪匹配]
# 加号前面的一个字符可以是1次或多次,返回值是list
import re
name = "re - python_re - python_re.findall()"
print(re.findall("re+",name))
print(re.findall("python_re+",name))

# ----- (13) {n,m} 匹配n到m个元素
# 匹配前一个字符n-m次,返回值是list
import re
name = "re - python_re - python_re.findall()"
print(re.findall("re{1}",name))
print(re.findall("re{1,2}",name))
print(re.findall("python{1,2}",name))

# ----- (14) .* 任意内容0个或多个
import re
name = "re - python_re - python_re.findall()"
print(re.findall(".*",name))

# ----- (15) **.*?** 任意内容0个或1
import re
name = "re - python_re - python_re.findall()"
print(re.findall("python.?re",name))    # .? 表示"一个"任意字符
print(re.findall("py.*?re",name))       # .*? 表示任意个任意内容

# -----(16) [] 获取括号中的内容
import re
name = "菜鸟程序员_Python-re.findall() 详解 2020/03/09"
print(re.findall("[0-9]",name))     # 匹配数字0-9
print(re.findall("[a-z]",name))     # [a-z]匹配小写字母a-z
print(re.findall("A-z]",name))      # 是按照ascii码表位进行匹配的
print(re.findall("[a-zA-Z]",name))  # [a-zA-Z] 匹配字母不管大小写
print(re.findall("[^A-z]",name))    # [^A-z] 有上尖号就是取反,获取不是字母和特定的几个字符
print(re.findall("[-+*]",name))     # 如果想要匹配到-,就需要进行如下操作(将-号放到最前面)

# ----- (17) () 分组 定制一个匹配规则
import re
name = "菜鸟程序员_Python-re.findall() 详解 2020/03/09"
print(re.findall("(.*?) 详解",name))
href = "<a href='https://www.cnblogs.com/xingxingnbsp/p/12420761.html'>菜鸟程序员_Python</a>"
print(re.findall("href='(.*?)'",href))

# ----- (18) | 匹配 左边或者右边,也可以理解成或
import rename = "python-re&python-file&python-re.findall()"
print(re.findall('python|re|python-re', name))
print(re.findall('&(python|re)',name))
print(re.findall('&(?:python|re)',name))

3、match&search

(1) re.match() 详解

re.match 尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match()就返回none。

函数语法:

re.match(pattern, string, flags=0)

参数说明:

  • pattern : 匹配的正则表达式
  • string : 要匹配的字符串。
  • flags : 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。

匹配成功re.match方法返回一个匹配的对象,否则返回None。

我们可以使用group(num) 或 groups() 匹配对象函数来获取匹配表达式。group(num=0): 匹配的整个表达式的字符串,

group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。

groups(): 返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。

1-1 基本使用案例

import re

# ----- 案例一
href = 'https://www.cnblogs.com/xingxingnbsp/p/12420761.html scrapy 基础教程'
print(re.match('https', href).span())   # 在起始位置匹配
print(re.match('www', href))            # 不在起始位置匹配

# ----- 案例二
href = 'https://www.cnblogs.com/xingxingnbsp/p/12420761.html scrapy 基础教程'
match_obj = re.match(r'https://(.*)xingxingnbsp(.*?) .*', href, re.M | re.I)
if match_obj:
    print("match_obj.group() : ", match_obj.group())
    print("match_obj.group(1) : ", match_obj.group(1))
    print("match_obj.group(2) : ", match_obj.group(2))
else:
    print("No match!!")
     
# ----- 案例三:完成手机号匹配
# phone_number = input("请输入手机号:")
phone_number = "18582896123"
match_obj = re.match(r'0?(13|14|15|16|17|18|19)[0-9]{9}', phone_number)
if match_obj:
    print(phone_number + ":手机号码正常")
else:
    print(phone_number + ":手机号码异常")
    
# ----- 案例四:完成邮箱的匹配
# mailbox = input("请输入邮箱号:")
mailbox = "123456789@qq.com"
match_obj = re.match(r'\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}', mailbox)
if match_obj:
    print(mailbox + ":邮箱号码正常")
else:
    print(mailbox + ":邮箱号码异常")
    
# ----- 案例五:完成网址的匹配
# href = input("请输入URL地址:")
href = "https://www.cnblogs.com/xingxingnbsp/p/12420761.html"
match_obj = re.match(r'(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?', href)
if match_obj:
    print(href + ":URL地址正常")
else:
    print(href + ":URL地址异常")

(2) re.search() 详解

re.search 扫描整个字符串并返回第一个成功的匹配。
函数语法:

re.search(pattern, string, flags=0)

参数说明:

  • pattern 匹配的正则表达式
  • string 要匹配的字符串。
  • flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。

匹配成功re.search方法返回一个匹配的对象,否则返回None。
我们可以使用group(num) 或 groups() 匹配对象函数来获取匹配表达式。
group(num=0) 匹配的整个表达式的字符串,group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。
groups() 返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。

2-1 基本使用案例

import re

# ----- 案例一
href = "https://www.cnblogs.com/xingxingnbsp/p/12420761.html"
print(re.search('cnblogs', href).span())        # 不在起始位置匹配
print(re.search('xingxingnbsp', href).span())   # 不在起始位置匹配

# ----- 案例二
href = "https://www.cnblogs.com/xingxingnbsp/p/12420761.html scrapy 基础教程"
search_obj = re.search(r'https://(.*)xingxingnbsp(.*?) .*', href, re.M | re.I)
if search_obj:
    print("search_obj.group() : ", search_obj.group())
    print("search_obj.group(1) : ", search_obj.group(1))
    print("search_obj.group(2) : ", search_obj.group(2))
else:
    print("No search!!")
    
# ----- 案例三:完成手机号匹配
# phone_number = input("请输入手机号:")
phone_number = "18582896123"
search_obj = re.search(r'0?(13|14|15|16|17|18|19)[0-9]{9}', phone_number)
if search_obj:
    print(phone_number + ":手机号码正常")
else:
    print(phone_number + ":手机号码异常")
     
# ----- 案例四:完成邮箱的匹配
# mailbox = input("请输入邮箱号:")
mailbox = "123456789@qq.com"
mailbox = "123456789@qq.com"
search_obj = re.match(r'\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}', mailbox)
if search_obj:
    print(mailbox + ":邮箱号码正常")
else:
    print(mailbox + ":邮箱号码异常")
    
# ----- 案例五:完成网址的匹配
# href = input("请输入URL地址:")
href = "https://www.cnblogs.com/xingxingnbsp/p/12420761.html"
search_obj = re.match(r'(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?', href)
if search_obj:
    print(href + ":URL地址正常")
else:
    print(href + ":URL地址异常")

(3) match与search的区别

re.match:只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回 None,

re.search:匹配整个字符串,直到找到一个匹配。

import re
href = "https://www.cnblogs.com/xingxingnbsp/p/12420761.html"

# match
match_obj = re.match(r'xingxingnbsp', href, re.M | re.I)
if match_obj:
    print("match_obj.group() : ", match_obj.group())
else:
    print("No match!!")

# search
search_obj = re.search(r'xingxingnbsp', href, re.M | re.I)
if search_obj:
    print("search_obj.group() : ", search_obj.group())
else:
    print("No match!!")

4、检索和替换

(1) sub()函数

Python 的re模块提供了re.sub用于替换字符串中的匹配项。
语法:

re.sub(pattern, repl, string, count=0, flags=0)

参数:

  • pattern:正则中的模式字符串。
  • repl:替换的字符串,也可为一个函数。
  • string:要被查找替换的原始字符串。
  • count:模式匹配后替换的最大次数,默认 0 表示替换所有的匹配。
  • flags:编译时用的匹配模式,数字形式。

前三个为必选参数,后两个为可选参数。

1-1 基础案例

import  re

# 案例一
r=re.sub("A\w","Python","AbAbcAbcdAbcdeAbcdef") #替换匹配成功的指定位置字符串,并且返回替换次数,可以用两个变量分别接受
print(r) #返回替换后的字符串

# 案例二
phone = "185-8289-1234 # 这是一个电话号码"
num = re.sub(r'#.*$', "", phone)    # 将# 这是一个电话号码替换为''
print("电话号码 : ", num)
num = re.sub(r'\D', "", phone)      # 获取除了数字其他的字符并替换为''
print("电话号码 : ", num)

# 案例三
string = 'PHP是最好的开发语言,PHP就是一个普通开发语言,PHP牛逼。'
print("替换之前的字符串:" + string)
print("替换之后的字符串:" + re.sub('PHP', 'Python', string))

# 案例四
# 当repl 为函数时
def double(matched):
    value = int(matched.group('value'))
    return str(value * 2)
s = '我的金币数为2000'
print(re.sub('(?P<value>\d+)', double, s))

(2) subn()函数

替换匹配成功的指定位置字符串,并且返回替换次数,可以用两个变量分别接受

语法:

re.subn(pattern, repl, string, count=0, flags=0)

参数:

  • pattern:正则中的模式字符串。
  • repl:替换的字符串,也可为一个函数。
  • string:要被查找替换的原始字符串。
  • count:模式匹配后替换的最大次数,默认 0 表示替换所有的匹配。
  • flags:编译时用的匹配模式,数字形式。

前三个为必选参数,后两个为可选参数。

2-1 基础案例

import  re

a,b=re.subn("A\w","Python","AbAbcAbcdAbcdeAbcdef") #替换匹配成功的指定位置字符串,并且返回替换次数,可以用两个变量分别接受
print(a) #返回替换后的字符串
print(b) #返回替换次数

(3) compile() 函数

compile 函数用于编译正则表达式,生成一个正则表达式( Pattern )对象,供 match() 和 search() 这两个函数使用。

语法:

re.compile(pattern[, flags])

参数:

  • pattern:一个字符串形式的正则表达式

  • flags:可选,表示匹配模式,比如忽略大小写,多行模式等,具体参数为:

    • re.I:忽略大小写
    • re.L:表示特殊字符集 \w, \W, \b, \B, \s, \S 依赖于当前环境
    • re.M:多行模式
    • re.S:即为' . '并且包括换行符在内的任意字符(' . '不包括换行符)
    • re.U:表示特殊字符集 \w, \W, \b, \B, \d, \D, \s, \S 依赖于 Unicode 字符属性数据库
    • re.X:为了增加可读性,忽略空格和' # '后面的注释

3-1 基础案例

import re

# 案例一
pattern = re.compile(r'\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}')
str = '123456789@qq.com'
m = pattern.search(str)
print(m.group())

# 案例二
pattern = re.compile(r'([a-z]+) ([a-z]+)', re.I)   # re.I 表示忽略大小写
m = pattern.match('Hello World Wide Web')
print(m)                    # 匹配成功,返回一个 Match 对象
print(m.group(0))           # 返回匹配成功的整个子串
print(m.span(0))            # 返回匹配成功的整个子串的索引
print(m.group(1))           # 返回第一个分组匹配成功的子串
print(m.span(1))            # 返回第一个分组匹配成功的子串的索引
print(m.group(2))           # 返回第二个分组匹配成功的子串
print(m.span(2))            # 返回第二个分组匹配成功的子串索引
print(m.groups())           # 等价于 (m.group(1), m.group(2), ...)
print(m.group(3))           # 不存在第三个分组

(4) split() 函数

split 方法按照能够匹配的子串将字符串分割后返回列表

语法:

re.split(pattern, string[, maxsplit=0, flags=0])

参数:

  • pattern:匹配的正则表达式
  • string:要匹配的字符串。
  • maxsplit:分隔次数,maxsplit=1 分隔一次,默认为 0,不限制次数。
  • flags: 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。参见:正则表达式修饰符 - 可选标志

4-1 基础案例

import  re

# 案例一
string = "python,python2,python3"
re_split = re.split("\W+",string)   # 匹配非数字字母下划线 并分割字符串
python_split = string.split(',')    # python自带的分割方法
print(re_split)
print(python_split)

# 案例二
import  re
string = "python,python2,python3"
re_split_1 = re.split("(\W+)",string)       # 匹配非数字字母下划线 并分割字符串
re_split_2 = re.split('\W+', string, 1)     # 匹配非数字字母下划线 只分割一次
re_split_3 = re.split(' ', string, 1)       # 对于一个找不到匹配的字符串而言,split 不会对其作出分割
print(re_split_1)
print(re_split_2)
print(re_split_3)

第八章 Python 数据分析

1、Matplotlib

pip install matplotlib

(1) 折线图

1-1 基本使用

from matplotlib import pyplot as plt

# 数据在x轴的位置,是一个可迭代对象
x = range(2, 26, 2)
# 数据在y轴的位置,是一个可迭代对象
y = [15, 13, 14, 5, 17, 20, 25, 26, 26, 24, 22, 18]
# x轴和y轴的数据一起组成了所有要绘制出的坐标
# 分别是(2,15),(4,13),(6,14),(8,17)...
plt.plot(x, y)
plt.show()

1-2 设置图片大小

from matplotlib import pyplot as plt

# 数据在x轴的位置,是一个可迭代对象
x = range(2, 26, 2)
# 数据在y轴的位置,是一个可迭代对象
y = [15, 13, 14, 5, 17, 20, 25, 26, 26, 24, 22, 18]
# 设置图片大小及清晰图
plt.figure(figsize=(10, 5), dpi=80)
# 开始画图
plt.plot(x, y)
# 保存图片
plt.savefig('images/demo_001.png')
# 展示图片
plt.show()

说明:

  • figure()
    • figsize: 设图片大小
    • dpi:在图像模糊的时候传入dpi参数,让图片更加清晰

1-3 绘制x&y轴刻度

from matplotlib import pyplot as plt

# 定义X轴上的值
x = range(2, 26, 2)
# 定义Y轴上的值
y = [15, 13, 14, 5, 17, 20, 25, 26, 26, 24, 22, 18]
# 设置图片大小及清晰图
plt.figure(figsize=(10, 5), dpi=80)
# 开始画图
plt.plot(x, y)
# 设置x轴刻度
plt.xticks(x)
# plt.xticks(range(2, 25))
# plt.xticks([i/2 for i in range(4, 49)])
# 设置y轴刻度
plt.yticks(range(min(y), max(y) + 1))
# 展示图片
plt.show()

说明

  • xticks()
  • yticks()

1-4 设置中文显示

import random
from matplotlib import pyplot as plt

# 1. 显示中文标签
plt.rcParams['font.sans-serif']=['SimHei'] 
# 2. 生成x轴和y轴数据
x = range(120)
y = [random.randint(20, 35) for i in range(120)]
# 3. 设置图片大小
plt.figure(figsize=(16, 10), dpi=80)
# 4. 绘图
plt.plot(x, y)
# 5. 调整x轴的刻度及字体
_xtick_labels = ["十点{}分".format(i) for i in range(60)]
_xtick_labels += ["十一点{}分".format(i) for i in range(60)]
plt.xticks(list(x)[::3], _xtick_labels[::3], rotation=45)
# 6. 显示
plt.show()

1-5 设置图形描述信息

import random
from matplotlib import pyplot as plt
from matplotlib import font_manager

# 1. 设置字体及字体大小-显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['xtick.labelsize'] = 16.0
# 2. 生成x轴和y轴数据
x = range(120)
y = [random.randint(20, 35) for i in range(120)]
# 3. 设置图片大小
plt.figure(figsize=(16, 10), dpi=80)
# 4. 绘图
plt.plot(x, y)
# 5. 调整x轴的刻度及字体
_xtick_labels = ["十点{}分".format(i) for i in range(60)]
_xtick_labels += ["十一点{}分".format(i) for i in range(60)]
plt.xticks(list(x)[::3], _xtick_labels[::3], rotation=45)
# 6. 设置描述信息
plt.xlabel("时间")
plt.ylabel("温度 单位(℃)")
plt.title("10点到12点每分钟的气温变化情况")
# 7. 显示
plt.show()

1-6 绘制网格

from matplotlib import pyplot as plt

# 1. 设置字体及字体大小-显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['xtick.labelsize'] = 16.0
plt.rcParams['ytick.labelsize'] = 16.0
# 2. 生成x轴和y轴数据
y = [1, 0, 1, 1, 2, 4, 3, 2, 3, 4, 4, 5, 6, 5, 4, 3, 3, 1, 1, 1]
x = range(11, 31)
# 3. 设置图片大小
plt.figure(figsize=(16, 10), dpi=80)
# 4. 绘图
plt.plot(x, y)
# 5. 调整x轴的刻度及字体
_xtick_labels = ["{}岁".format(i) for i in range(11, 31)]
plt.xticks(x, _xtick_labels, rotation=45)
# 6. 绘制网格
plt.grid(alpha=0.4)
# 7. 设置描述信息
plt.xlabel("年龄")
plt.ylabel("人数")
plt.title("11到30岁每年交往的女(男)朋友数量")
# 8. 显示
plt.show()

1-7 绘制图例

from matplotlib import pyplot as plt

# 1. 设置字体及字体大小-显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['xtick.labelsize'] = 16.0
plt.rcParams['ytick.labelsize'] = 16.0
# 2. 生成x轴和y轴数据
y_1 = [1, 0, 1, 1, 2, 4, 3, 2, 3, 4, 4, 5, 6, 5, 4, 3, 3, 1, 1, 1]
y_2 = [3, 1, 0, 0, 1, 5, 3, 1, 1, 5, 5, 7, 3, 2, 1, 1, 2, 1, 1, 1]
x = range(11, 31)
# 3. 设置图片大小
plt.figure(figsize=(16, 10), dpi=80)
# 4. 绘图并添加图例
plt.plot(x, y_1, 'r:o', label='自己')
plt.plot(x, y_2, 'g:o', label='同桌')
# 5. 调整x轴的刻度及字体
_xtick_labels = ["{}岁".format(i) for i in range(11, 31)]
plt.xticks(x, _xtick_labels, rotation=45)
# 6. 绘制网格
plt.grid(alpha=0.4)
# 7. 绘制图例
plt.legend(loc='upper left')
# 8. 设置描述信息
plt.xlabel("年龄")
plt.ylabel("人数")
plt.title("11到30岁每年交往的女(男)朋友数量")
# 10. 显示
plt.show()

(2) 散点图

2-1 基本使用

from random import randint
from matplotlib import pyplot as plt

# 1. 设置字体及字体大小 - 显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['xtick.labelsize'] = 16.0
plt.rcParams['ytick.labelsize'] = 16.0

# 2. 定义三月份的数据
x_month_3 = range(1, 32)
y_month_3 = [randint(5, 25) for x in range(31)]

# 3. 定义五月份的数据
x_month_5 = range(51, 82)
y_month_5 = [randint(5, 25) for y in range(31)]

# 4. 设置图片大小
plt.figure(figsize=(20, 8), dpi=80)

# 5. 绘制散点图
plt.scatter(x_month_3, y_month_3, label="3月份")
plt.scatter(x_month_5, y_month_5, label="5月份")

# 6. 调整 x 轴的刻度
_x = list(x_month_3) + list(x_month_5)
_xtick_labels = ["3月{}日".format(i) for i in x_month_3] + ["5月{}日".format(i-50) for i in x_month_5]
plt.xticks(_x[::3], _xtick_labels[::3], rotation=45)

# 7. 添加描述信息
plt.xlabel("时间")
plt.ylabel("温度")
plt.title("标题")

# 8. 添加图例
plt.legend()

# 9. 展示
plt.show()

(3) 条形图

3-1 基本使用

from matplotlib import pyplot as plt

# 1. 设置字体及字体大小 - 显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['xtick.labelsize'] = 16.0
plt.rcParams['ytick.labelsize'] = 16.0

# 2. 定义 y 轴数据
y = [
    56.39,
    49.34,
    46.18,
    42.05,
    36.22,
    33.9,
    33.71,
    31.46,
    30.75,
    28.84,
    26.49,
    25.27,
    24.26,
    24.21,
    23.7,
    22.19,
    21.9,
    21.83,
    19.97,
    19.79
]
# 3. 定义 x 轴数据
x = [
    '战狼2',
    '哪吒之魔童降世',
    '流浪地球',
    '复仇者联盟4:终局之战',
    '红海行动',
    '美人鱼',
    '唐人街探案2',
    '我和我的祖国',
    '我不是药神',
    '中国机长',
    '速度与激情8',
    '西虹市首富',
    '速度与激情7',
    '捉妖记',
    '复仇者联盟3:无限战争',
    '捉妖记2',
    '羞羞的铁拳',
    '疯狂的外星人',
    '海王',
    '变形金刚4:绝迹重生'
]

# 4. 设置图片大小
plt.figure(figsize=(20, 8), dpi=80)

# 5. 绘制条形图
plt.bar(x, y)

# 6. 调整 x 轴的刻度
plt.xticks(rotation=90)

# 7. 添加描述信息
plt.xlabel("电影名称")
plt.ylabel("票房(亿)")
plt.title("票房排行")

# 8. 添加图例
plt.legend()

# 9. 展示图片
plt.show()

3-2 横向显示

from matplotlib import pyplot as plt

# 1. 设置字体及字体大小 - 显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']  #

# 2. 定义 y 轴数据
y = [
    56.39,
    49.34,
    46.18,
    42.05,
    36.22,
    33.9,
    33.71,
    31.46,
    30.75,
    28.84,
    26.49,
    25.27,
    24.26,
    24.21,
    23.7,
    22.19,
    21.9,
    21.83,
    19.97,
    19.79
]
# 3. 定义 x 轴数据
x = [
    '战狼2',
    '哪吒之魔童降世',
    '流浪地球',
    '复仇者联盟4:终局之战',
    '红海行动',
    '美人鱼',
    '唐人街探案2',
    '我和我的祖国',
    '我不是药神',
    '中国机长',
    '速度与激情8',
    '西虹市首富',
    '速度与激情7',
    '捉妖记',
    '复仇者联盟3:无限战争',
    '捉妖记2',
    '羞羞的铁拳',
    '疯狂的外星人',
    '海王',
    '变形金刚4:绝迹重生'
]

# 4. 设置图片大小
plt.figure(figsize=(20, 8), dpi=80)

# 5. 绘制条形图
plt.barh(x, y)

# 6. 绘制网格
plt.grid(alpha=0.4)

# 7. 添加描述信息
plt.ylabel("电影名称")
plt.xlabel("票房(亿)")
plt.title("票房排行")

# 8. 添加图例
plt.legend()

# 9. 展示图片
plt.show()

1-3 多列显示

from matplotlib import pyplot as plt

# 1. 设置字体及字体大小 - 显示中文标签
plt.rcParams['font.sans-serif'] = ['SimHei']

# 2. 定义 x&y 轴数据
x = ['猩球崛起3:终极之战', '敦刻尔克', '蜘蛛侠:英雄归来', '战狼2']
y_1 = [15746, 312, 2045, 319]
y_2 = [12357, 156, 4497, 168]
y_3 = [2358, 339, 2358, 362]

# 3. 设置宽度
bar_width = 0.2
lenth = list(range(len(x)))
x_2 = [i + bar_width for i in lenth]
x_3 = [i + bar_width * 2 for i in lenth]

# 4. 设置图片大小
plt.figure(figsize=(20, 8), dpi=80)

# 5. 绘制条形图
plt.bar(range(len(x)), y_1, width=bar_width, label="14日")
plt.bar(x_2, y_2, width=bar_width, label="15日")
plt.bar(x_3, y_3, width=bar_width, label="16日")

# 6. 调整 x 轴的刻度
plt.xticks(x_2, x)

# 7. 绘制网格
plt.grid(alpha=0.4)

# 8. 添加描述信息
plt.xlabel("电影名称")
plt.ylabel("票房(万)")
plt.title("票房排行")

# 9. 添加图例
plt.legend()

# 10. 展示图片
plt.show()

(4) 直方图

4-1 基本使用

from random import randint
from matplotlib import pyplot as plt

# 1. 生成数据
data = [randint(80, 141) for i in range(250)]

# 2. 计算组数
GroupSpacing = 3
GroupNumber = (max(data) - min(data)) // GroupSpacing

# 3. 设置图形大小
plt.figure(figsize=(20, 8), dpi=80)

# 4. 绘制直方图
plt.hist(data, GroupNumber, density=True)

# 5. 设置X轴刻度
plt.xticks(range(min(data), max(data) + GroupSpacing, GroupSpacing))

# 6. 绘制网格
plt.grid()

# 7. 展示图片
plt.show()

4-2 调整刻度

from matplotlib import pyplot as plt
from random import randint

# 1. 定义数据
interval = [0, 5, 10, 15, 20, 25, 30, 25, 40, 45, 60, 90]
width = [5, 5, 5, 5, 5, 5, 5, 5, 5, 15, 30, 60]
quantity = [836, 2737, 3723, 3926, 3596, 1438, 3273, 624, 824, 613, 215, 47]

# 2. 设置图片大小
plt.figure(figsize=(20, 8), dpi=80)

# 3. 绘制图片
plt.bar(range(len(quantity)), quantity, width=1)

# 4. 设置x轴刻度
_x = [i - 0.5 for i in range(13)]
_xtick_labels = interval + [150]
plt.xticks(_x, _xtick_labels)

# 5. 绘制网格
plt.grid()

# 6. 展示图片
plt.show()

(5) 饼状图

5-1 语法

pie(x, explode=None, labels=None, colors=None, autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1, startangle=0, radius=1, counterclock=True, wedgeprops=None,textprops=None, center=(0, 0), frame=False, rotatelabels=False, *, normalize=None, data=None):

5-2 参数

参数 类型 说明
x 浮点型数组 表示每个扇形的面积。
explode 元祖 表示各个扇形之间的间隔,默认值为0。
labels 列表 各个扇形的标签,默认值为 None。
colors 列表 表示各个扇形的颜色,默认值为 None。
autopct 字符串 设置饼图内各个扇形百分比显示格式
pctdistance 数值类型 类似于 labeldistance,指定 autopct 的位置刻度,默认值为 0.6。
shadow 布尔类型 布尔值 True 或 False,设置饼图的阴影,默认为 False,不设置阴影。
labeldistance 数值类型 标签标记的绘制位置,相对于半径的比例,默认值为 1.1
startangle 数值类型 起始绘制饼图的角度,默认为从 x 轴正方向逆时针画起
radius 数值类型 设置饼图的半径,默认为 1。
counterclock 布尔值 设置指针方向,默认为 True,即逆时针,False 为顺时针。
wedgeprops 字典类型 默认值 None。参数字典传递给 wedge 对象用来画一个饼图
textprops 字典类型 默认值为:None。传递给 text 对象的字典参数,
用于设置标签(labels)和比例文字的格式。
center 浮点类型的列表 默认值:(0,0)。用于设置图标中心位置。
frame 布尔类型 默认值:False。如果是 True,绘制带有表的轴框架
rotatelabels 布尔类型 默认为 False。如果为 True,旋转每个 label 到指定的角度。
normalize
data

5-3 基本使用

import matplotlib.pyplot as plt
import numpy as np

y = np.array([35, 25, 25, 15])

plt.pie(y)
plt.show() 

5-3 设置颜色

import matplotlib.pyplot as plt
import numpy as np

y = np.array([35, 25, 25, 15])

plt.pie(y,
        labels=['A','B','C','D'], # 设置饼图标签
        colors=["#d5695d", "#5d8ca8", "#65a479", "#a564c9"], # 设置饼图颜色
       )
plt.title("RUNOOB Pie Test") # 设置标题
plt.show()

5-4 显示百分比

import matplotlib.pyplot as plt
import numpy as np

y = np.array([35, 25, 25, 15])

plt.pie(y,
        labels=['A','B','C','D'], # 设置饼图标签
        colors=["#d5695d", "#5d8ca8", "#65a479", "#a564c9"], # 设置饼图颜色
        explode=(0, 0.2, 0, 0), # 第二部分突出显示,值越大,距离中心越远
        autopct='%.2f%%', # 格式化输出百分比
       )
plt.title("RUNOOB Pie Test")
plt.show()

2、Numpy

什么是numpy?

​ 一个在python中做科学计算的基础库,重在 数值计算, 也是大部分Python 科学计算库的基础库,多用于在大型多为数组上执行数值计算

安装

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

(1) 基本使用

1-1 查看元素属性

import numpy as np

array = np.array([[1, 2, 3], [4, 5, 6]])
print("原始数据: \n", array)

# 获取维度
print("number of dim:", array.ndim)

# 获取行数和列数
print("shape:", array.shape)

# 获取元素个数
print("size:", array.size)

1-2 创建 array

import numpy as np

t1 = np.array([1, 2, 3, 4])
print("创建一个一维数组: \n", t1)

t2 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
print("创建一个二维数组: \n", t2)

t3 = np.zeros((3, 4))
print("创建一个三行四列数组: \n", t3)

t4 = np.ones((3, 4), dtype=np.int16)
print("创建一个三行四列数组 并设置数值类型: \n", t4)

t5 = np.empty((3, 4))
print("创建一个三行四列数组: \n", t5)

t6 = np.arange(10, 20, 2)
print("创建一个10-20之间的数组步长为2: \n", t6)

t7 = np.arange(12).reshape(3, 4)
print("创建一个0-12之间的三行四列的数组: \n", t7)

t8 = np.linspace(1, 10, 6).reshape(2, 3)
print("创建一个0-10之间的二行三列的数组: \n", t8)

1-3 数据类型

类型 类型代码 说明
int8, uint8 i1, u1 有符号和无符号的8位(1字节)整形
int16, uint16 i2, u2 有符号和无符号的16位(2字节)整形
int32, uint32 i4, u4 有符号和无符号的32位(4字节)整形
int64, uint64 i8, u8 有符号和无符号的64位(8字节)整形
float16 f2 半精度浮点数
float32 f4或f 标准的点季度浮点数,与C的float兼容
float64 f8或d 标准的点季度浮点数,与C的double和Python的float对象兼容
float128 f16或g 扩展精度浮点数
complex64, c8 32位浮点数标识的复数
complex128 c16 64位浮点数标识的复数
complex256 c32 128位浮点数标识的复数
boll ? 存储True和Flase值得布尔类型
查看元素类型
import numpy as np

# 方式一
t_1 = np.array([1, 2, 3, 4, 5])
print(t_1, type(t_1), t_1.dtype)

# 方式二
t_2 = np.array(range(1, 5))
print(t_2, type(t_2), t_2.dtype)

# 方式三
t_3 = np.arange(10)
print(t_3, type(t_3), t_3.dtype)
定义数据类型
import numpy as np
from random import random

# 创建一个是个10个元素的 numpy.ndarray 并指定数据类型
t_1 = np.array([random() for i in range(10)], dtype=float)
print(t_1, type(t_1), t_1.dtype)

# 保留两位小数
t_2 = np.round(t_1, 2)
print(t_2)
指定数据类型
import numpy as np

# 指定数据类型 - float
t_1 = np.array(range(1, 5), dtype=float)
print(t_1, type(t_1), t_1.dtype)

# 指定数据类型 - int
t_2 = np.array(range(1, 5), dtype='i1')
print(t_2, type(t_2), t_2.dtype)

# 指定数据类型 - bool
t_3 = np.array([1, 0, 1, 1, 0, 0], dtype=bool)
print(t_3, type(t_3), t_3.dtype)

# 修改数据类型 - bool
t_4 = t_3.astype('bool')
print(t_4, type(t_4), t_4.dtype)

(2) 数组的计算

2-1 数组的形状

import numpy as np

# 一维数组
t_1 = np.arange(10)
# 二维数组
t_2 = np.array([[1, 2, 3], [1, 2, 3], [1, 2, 3]])
# 三维数组
t_3 = np.array([[[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]]])

# 查看数组的形状
print("---------- 查看数组的形状 ----------")
print(t_1.shape, t_1)
print(t_2.shape, t_2)
print(t_3.shape, t_3)


# 修改数组的形状
print("---------- 修改数组的形状 ----------")
t_4 = t_1.reshape(2, 5)
print(f"----- 形状:{t_4.shape} -----")
print(t_4.shape, t_4)

t_5 = np.arange(24).reshape((2, 3, 4))
print(f"----- 形状:{t_5.shape} -----")
print(t_5.shape, t_5)

t_6 = t_5.reshape(4, 6)
print(f"----- 形状:{t_6.shape} -----")
print(t_6.shape, t_6)

t_7 = t_6.flatten()
print(f"----- 形状:{t_7.shape} -----")
print(t_7.shape, t_7)

2-2 数组的基本运算

import numpy as np

t_1 = np.arange(10)
print("原始的数据: ", t_1)

# 加法运算
print("对应位置的元素加法运算: ", t_1 + 2)

# 减法运算
print("对应位置的元素减法运算: ", t_1 - 2)

# 乘法运算
print("对应位置的元素乘法运算: ", t_1 * 2)

# 除法运算
print("对应位置的元素除法运算: ", t_1 / 2)

# 平方运算
print("对应位置的元素平方运算: ", t_1 ** 2)

# 条件运算
print("对应位置的元素条件运算: ", t_1 < 3)

2-3 数组之间的运算

import numpy as np

# 两数组之间的运算
array_1 = np.arange(5)
array_2 = np.arange(50, 55)
# 两数组相加
print("两数组相加之后:", array_1 + array_2)

# 两数组相减
print("两数组相减之后:", array_1 - array_2)

# 两数组相乘
print("两数组相乘之后:", array_1 * array_2)

# 两数组相除
print("两数组相除之后:", array_1 / array_2)

# 矩阵乘法
print("矩阵乘法方式一: ", np.dot(array_1, array_2))
print("矩阵乘法方式二: ", array_1.dot(array_2))

2-4 数组统计

import numpy as np

t1 = np.random.random((2, 4))

print("原始数据: \n", t1)
print("求和: \n", np.sum(t1))
print("求最小值: \n", np.min(t1))
print("求最大值: \n", np.max(t1))

# 自定义维度
print("自定义维度求和: \n", np.sum(t1, axis=1))
print("自定义维度求最小值: \n", np.min(t1, axis=0))
print("自定义维度求最大值: \n", np.max(t1, axis=1))

# 获取平均值
print("获取平均值: \n", np.mean(t1))
print("获取平均值: \n", np.average(t1))

# 获取中位数
print("获取中位数: \n", np.median(t1))

# 逐步累加
print("逐步累加: \n", np.cumsum(t1))

# 每两个数之间的差
print("每两个数之间的差: \n", np.diff(t1))

# 获取最小值的索引
print("获取最小值的索引: \n", np.argmin(t1))

# 获取最大值的索引
print("获取最大值的索引: \n", np.argmax(t1))

# 找出非0的数
print("找出非0的数: \n", np.nonzero(t1))

2-5 转置

import numpy as np

t1 = np.arange(24).reshape((4, 6))
print("原始数据: \n", t1)

# 转置方式1(将原始的行变成列,将列变成行)
print("转置方式1: \n", t1.transpose())

# 转置方式2
print("转置方式2: \n", t1.T)

# 转置方式3
print("转置方式3: \n", t1.swapaxes(1,0))

(3) 数据的读取

3-1 基本使用

import numpy as np

file_path = './files/demo_001.csv'
data = np.loadtxt(file_path, dtype='int', delimiter=',', encoding='utf-8')
print("原始数据: \n", data)

# 转置 x轴 -> y轴
data_1 = np.loadtxt(file_path, dtype='int', delimiter=',', encoding='utf-8', unpack=True)
print("转置后的数据: \n", data_1)

(4) 索引

4-1 基本使用

import numpy as np

t1 = np.arange(64).reshape(8, 8)
print("原始数据为:\n", t1)

# 取一行
print("取一行数据为:\n", t1[0])

# 取一列
print("取一列数据为:\n", t1[:, 2])

# 取多行
print("取多行数据为:\n", t1[1:3])

# 取多列
print("取多列数据为:\n", t1[:, 1:3])

# 取不连续的多行
print("取不连续的多行:\n", t1[[1, 3, 5]])

# 取不连续的多列
print("取不连续的多列:\n", t1[:, [1, 3, 5]])

# 取指定行指定列(取第三行,第四列的值)
print("取指定行指定列:\n", t1[2, 3])

# 取多行多列(取第三行到第五行,第二列到第四列的结果)
print("取多行多列:\n", t1[2:5, 1:4])

# 取多个不相邻的点((0, 0), (2, 1), (2, 3))
print("取多个不相邻的点:\n", t1[[0, 2, 2], [0, 1, 3]])

4-2 基本案例

import numpy as np

# 一维数组
t1 = np.arange(3, 15)
print("t1原始数据: \n", t1)
print("t1的第三个元素: \n", t1[3])

# 二维数组
t2 = np.arange(3, 15).reshape(3, 4)
print("t2原始数据: \n", t2)
print("获取第2行第2列的元素: \n", t2[1][1])
print("获取第3行第3列的元素: \n", t2[2][2])
print("获取第3行第2列的元素: \n", t2[2, 1])
print("获取第3行的所有元素: \n", t2[2, :])
print("获取第2列的所有元素: \n", t2[:, 1])
print("获取第1行的第2列到第3列的元素: \n", t2[1, 1:3])


# 循环
print("将t2进行降维: \n", t2.flatten())
print("将t2进行降维后循环: \n")
for item in t2.flat:
    print(item)

(5) 数值修改

5-1 基本使用

import numpy as np

t1 = np.arange(64).reshape(8, 8)
print("原始数据为:\n", t1)

# 将第三列到第四列的值修改为0
t1[:, 2:4] = 0
print("将第三列到第四列的值修改为0:\n", t1)

5-2 按条件修改

import numpy as np

t1 = np.arange(64).reshape(8, 8)
print("原始数据为:\n", t1)

# 将t1中小于10的值修改为3
print("布尔索引:\n", t1 < 10)

t1[t1 < 10] = 3
print("将t1中小于10的值修改为3:\n", t1)

# 将矩阵中大于9的数该为9,将小于5的数改为5
print("将矩阵中大于9的数该为9,将小于5的数改为5:\n ", np.clip(t1, 5, 9))

(6) 合并分割

6-1 合并

import numpy as np

t1 = np.array([1, 1, 1])
t2 = np.array([2, 2, 2])
print("原始数据t1&t2: \n", t1, t2)

# 上下合并
t3 = np.vstack((t1, t2))
print("上下合并:\n ", t3, t3.shape)

# 左右合并
t4 = np.hstack((t1, t2))
print("左右合并:\n ", t4, t4.shape)


# 改变维度
print("---------- 改变维度 ----------")
s1 = np.array([1, 1, 1])[:, np.newaxis]
s2 = np.array([2, 2, 2])[:, np.newaxis]
print("原始数据s1&s2: \n", s1, s2)

# 上下合并
s3 = np.vstack((s1, s2))
print("上下合并:\n ", s3, s3.shape)

# 左右合并
s4 = np.hstack((s1, s2))
print("左右合并:\n ", s4, s4.shape)

# 多个array合并
s5 = np.concatenate((s1, s2, s2, s1), axis=0)
print("多个array: \n", s5)
s6 = np.concatenate((s1, s2, s2, s1), axis=1)
print("多个array: \n", s6)

6-2 分割

import numpy as np

t1 = np.arange(12).reshape((3, 4))
print("原始数据: \n", t1)

# 横向分割
print("横向分割: \n", np.split(t1, 3, axis=0))
print("横向分割: \n", np.vsplit(t1, 3))

# 纵向分割
print("纵向分割: \n", np.split(t1, 2, axis=1))
print("纵向分割: \n", np.hsplit(t1, 2))

# 不等量分割
print("不等量分割: \n", np.array_split(t1, 3, axis=1))

(7) 拷贝

7-1 基本使用

import numpy as np

t1 = np.arange(4)
print("原始数据: \n", t1)
c1 = t1
t1[0] = 11
print("将t1索引为0的元素修改为11: \n", t1)
print("判断c1和t1是否相同: \n", c1 is t1)
print("打印修改之后的元素c1: \n", c1)

t2 = np.arange(4)
c2 = t2.copy()
t2[3] = 44
print("打印修改后的t2: \n", t2)
print("判断c2和t2是否相同: \n", c2 is t2)
print("打印修改之后的元素c2: \n", c2)

3、Pandas

(1) Pandas 基本介绍

1-1 什么是Pandas?

一个开源的Python类库;用于数据分析,数据处理,数据可视化

  • 高性能
  • 容易使用的数据结构
  • 容易使用的数据分析工具

很方便和其他类一起使用

  • numpy: 用于数学计算
  • scikit-learn: 用于机器学习

1-2 下载安装pandas

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

1-3 基本使用

import pandas as pd
import numpy as np

t1 = pd.Series([1, 3, 6, np.nan, 44, 1])
print("原始数据: \n", t1)

dates = pd.date_range('20201014', periods=6)
print("获取日期列表: \n", dates)

(2) Pandas 数据读取

Pandas需要先读取表格类型的数据,然后进行分析

数据类型 说明 Pandas读取方法
csv, tsv, txt 用逗号分隔,tab分隔的纯文本文件 pd.read_csv
excel 微软xls或者xlsx文件 pd.read_excel
mysql 关系型数据库表 pd.read_sql

1-1 读取 csv

# 读取csv文件,使用默认的标题行,逗号分隔符
import pandas as pd

file_path = '../files/ratings.csv'

# 使用pd.read_csv读取数据
ratings = pd.read_csv(file_path)
print("查看原始数据: \n", ratings)

# 查看数据的前几行
print("查看数据的前几行: \n", ratings.head())

# 查看数据的形状(返回行数,列数)
print("查看数据的形状: \n", ratings.shape)

# 查看列表列名
print("查看列表列名 \n", ratings.columns)

# 查看索引列
print("查看索引列: \n", ratings.index)

# 查看每列的数据类型
print("查看每列的数据类型: \n", ratings.dtypes)

1-2 读取 txt

import pandas as pd

file_path = '../files/access_pvuv.txt'
# 使用pd.read_csv读取数据
pvuv = pd.read_csv(file_path, sep='\t', header=None, names=['pdate', 'pv', 'uv'])
"""
sep: 指定列的分隔符
header: 没有标题行设置为None
names: 自定义列名
"""
print("原始数据: \n", pvuv)

# 查看数据的前几行
print("查看数据的前几行: \n", pvuv.head())

# 查看数据的形状(返回行数,列数)
print("查看数据的形状: \n", pvuv.shape)

# 查看列表列名
print("查看列表列名: \n", pvuv.columns)

# 查看索引列
print("查看索引列: \n", pvuv.index)

# 查看每列的数据类型
print("查看每列的数据类型: \n", pvuv.dtypes)

1-3 读取 excel

import pandas as pd

file_path = '../files/access_pvuv.xlsx'
# 使用pd.read_excel读取数据
pvuv = pd.read_excel(file_path)
print("原始数据: \n", pvuv)

1-4 读取 MySQL

import pymysql
import pandas as pd

# 创建数据库连接
conn = pymysql.connect(host='127.0.0.1', user='root', password='19971215', database='demo_01', charset='utf8')

# 使用pd.read_excel读取数据
pvuv = pd.read_sql("select * from crazyant_pvuv", con=conn)
print("原始数据: \n", pvuv)

(3) Pandas 数据结构

DataFrame: 二维数组,整个表格,多行多列

数据结构

Series: 一维数据,一行或一列

3-1 Series

Series是一种类似于一维数组的对象,他由一组数据(不同数据类型)以及一组与之相关的数据标签(即索引)组成

import pandas as pd

# ================= 1 仅有数据列表即可产生最简单的Series =================
s1 = pd.Series([1, 'a', 5.2, 7])
# 左侧为索引,右侧是数据
print("s1 - 原始数据: \n", s1)
# 获取索引
print("s1 - 获取索引: \n", s1.index)
# 获取数据
print("s1 - 获取数据: \n", s1.values)

# ================= 2 创建一个具有标签索引的Series =================
s2 = pd.Series([1, 'a', 5.2, 7], index=['d', 'b', 'a', 'c'])
# 左侧为索引,右侧是数据
print("s2 - 原始数据: \n", s2)
# 获取索引
print("s2 - 获取索引: \n", s2.index)
# 获取数据
print("s2 - 获取数据: \n", s2.values)

# ================= 3 使用Python字典创建Series =================
data = {"Ohio": 35000, "Texas": 72000, "Oregon": 16000, "Utah": 5000}
s3 = pd.Series(data)
# 左侧为索引,右侧是数据
print("s3 - 原始数据: \n", s3)
# 获取索引
print("s3 - 获取索引: \n", s3.index)
# 获取数据
print("s3 - 获取数据: \n", s3.values)

# ================= 4 根据标签索引查询数据 =================
s4 = pd.Series([1, 'a', 5.2, 7], index=['d', 'b', 'a', 'c'])
# 左侧为索引,右侧是数据
print("s4 - 原始数据: \n", s4)
# 获取index为a的元素
print("s4 - 获取index为a的元素: \n", s4['a'], type(s4['a']))
# 获取指定的多个值
print("s4 - 获取指定的多个值: \n", s4[['b', 'a']], type(s4[['b', 'a']]))

3-2 DataFrame

DataFrame是一个表格型的数据结构

  • 每列可以是不同的值类型(数值,字符串,布尔值等)
  • 既有行索引index, 也有columns
  • 可以被看做由Series组成的字典

创建dataframe最常用的方法,见Panda数据读取

import pandas as pd
data = {
    "state": ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada'],
    "year": [2000, 2001, 2002, 2003, 2004],
    "pop": [1.5, 1.7, 3.6, 2.4, 2.9]
}

df = pd.DataFrame(data)
print("原始数据: \n", df)

# 获取每列的数据类型
print("获取每列的数据类型: \n", df.dtypes)

# 获取每列的键名
print("获取每列的键名: \n", df.columns)

# 获取索引
print("获取索引: \n", df.index)

3-3 从DataFrame中查询出Series

  • 如果只查询出一行,一列,返回的是pd.Series
  • 如果查询多列,多行,发挥的是pd.DataFrame
import pandas as pd

data = {
    "state": ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada'],
    "year": [2000, 2001, 2002, 2003, 2004],
    "pop": [1.5, 1.7, 3.6, 2.4, 2.9]
}
df = pd.DataFrame(data)
print("原始数据: \n", df)

# 查询一列,结果是一个pd.Series
print("查询一列: \n", df['year'], type(df['year']))

# 查询多列,结果是一个pd.DataFrame
print("查询多列: \n", df[['year', 'pop']], type(df[['year', 'pop']]))

# 查询一行,结果是一个pd.Series
print("查询一行: \n", df.loc[1], type(df.loc[1]))

# 查询多行,结果是一个pd.DataFrame
print("查询多行: \n", df.loc[1:3], type(df.loc[1:3]))

(4) Pandas 数据查询

pandas 查询数据的几种方法

方法 说明
df.loc方法 根据行,列的标签值查询
df.iloc方法 根据行,列的数字位置查询
df.where方法
df.query方法

.loc即可以查询,又能覆盖雪茹,强烈推荐

pandas 使用df.loc查询数据的方法

  • 使用单个label值查询数据
  • 使用值列表批量查询
  • 使用数值区间进行范围查询
  • 使用条件表达式查询
  • 调用函数查询

注意:

  • 以上查询方法,即适用于行,也适用于列
  • 注意观察降维DataFrame>Series>值

4-1 读取数据

import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

print("打印前几行的数据:\n ", df.head())

# 设定索引为日期,方便按日期筛选
df.set_index('ymd', inplace=True)

# 时间序列见后续课程,本次按字符串处理
print("打印索引:\n ", df.index)
print("打印前几行的数据:\n ", df.head())

# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')
print("打印每列的数据类型:\n ", df.dtypes)
print("打印前几行的数据:\n ", df.head())

4-2 使用单个label值查询数据

行或者列,都可以只传单个值,实现精确匹配

import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 数据的预处理
# 设定索引为日期,方便按日期筛选
df.set_index('ymd', inplace=True)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

# 打印前几行数据
print("打印前几行数据: \n", df.head())
# 得到单个值(获取2018-01-03的最高温度)
print("获取2018-01-03的最高温度: \n", df.loc['2018-01-03', 'bWendu'])
# 得到一个Series(获取2018-01-03的最高温度和最低温度)
print("获取2018-01-03的最高温度和最低温度: \n", df.loc['2018-01-03', ['bWendu', 'yWendu']])

4-3 使用值列表批量查询

import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 数据的预处理
# 设定索引为日期,方便按日期筛选
df.set_index('ymd', inplace=True)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

# 打印前几行数据
print("打印前几行数据: \n", df.head())

# 得到Series(获取['2018-01-03', '2018-01-04', '2018-01-05']的最高温度)
print("获取指定日期的最高温度: \n", df.loc[['2018-01-03', '2018-01-04', '2018-01-05'], 'bWendu'])

# 得到DataFrame(获取['2018-01-03', '2018-01-04', '2018-01-05']的最高温度和最低温度)
print("获取指定日期的最高温度和最低温度: \n", df.loc[['2018-01-03', '2018-01-04', '2018-01-05'], ['bWendu', 'yWendu']])

4-4 使用数值区间进行范围查询

注意:区间即包含开始,也包含结束

import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 数据的预处理
# 设定索引为日期,方便按日期筛选
df.set_index('ymd', inplace=True)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

# 打印前几行数据
print("打印前几行数据: \n", df.head())

# 行index按区间
print("行index按区间: \n", df.loc['2018-01-03':'2018-01-05', 'bWendu'])

# 列index按区间
print("列index按区间: \n", df.loc['2018-01-03', 'bWendu':'fengxiang'])

# 行列都按区间查询
print("行列都按区间查询: \n", df.loc['2018-01-03':'2018-01-05', 'bWendu':'fengxiang'])

4-5 使用条件表达式查询

bool列表的长度等于行数或者列数

简单条件查询
import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 数据的预处理
# 设定索引为日期,方便按日期筛选
df.set_index('ymd', inplace=True)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

# 简单查询, 最低温度低于-10度的列表
print("查询最低温度低于-10度的列表: \n", df.loc[df['yWendu'] < -10, :])

# 观察一下这里的boolean条件
print("查询最低温度小于-10度的布尔值列表: \n", df['yWendu'] < -10)
复杂条件查询

注意:组合条件用&符号合并,每个条件判断都得带括号

import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 数据的预处理
# 设定索引为日期,方便按日期筛选
df.set_index('ymd', inplace=True)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

# 查询最高温度小于30度,并且最低温度大于十五度,并且是晴天,并且天气为优的数据
print("查询最高温度小于30度,并且最低温度大于十五度,并且是晴天,并且天气为优的数据:")
print(df.loc[(df['bWendu'] <= 30) & (df['yWendu'] >= 15) & (df['tianqi'] == '晴') & (df['aqiLevel'] == 1), :])

# 观察一下这里的boolean条件
print("观察一下这里的boolean条件:")
print((df['bWendu'] <= 30) & (df['yWendu'] >= 15) & (df['tianqi'] == '晴') & (df['aqiLevel'] == 1))

4-6 调用函数查询

import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 数据的预处理
# 设定索引为日期,方便按日期筛选
df.set_index('ymd', inplace=True)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

# 直接写lambda表达式
print("直接写lambda表达式: \n", df.loc[lambda df: (df['bWendu'] <= 30) & (df['yWendu'] >= 15), :])

# 编写自己的函数,查询9月份,空气质量好的数据
def query_my_data(df):
    return df.index.str.startswith('2018-09') & df['aqiLevel'] == 1

print("\n编写自己的函数: \n", df.loc[query_my_data, :])

(5) Pandas 新增数据列

在进行数据分析时,经常需要按照一定的条件创建新的数据列,然后进行进一步分析

  1. 直接赋值
  2. df.apply方法
  3. df.assign方法
  4. 按照条件选择分组分别赋值

5-1 读取csv数据到dataframe

import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)
print("读取数据头部数据(前五行): \n", df.head())

5-2 直接赋值方法

准备1:清理温度列,变成数字列

# 设定索引为日期,方便按日期筛选
df.set_index('ymd', inplace=True)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

准备2:计算温度差

# 注意df['bWendu']其实是一个Series,后面的减法返回的是Series
df.loc[:, 'wencha'] = df['bWendu'] - df['yWendu']

完整代码:

import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')
print("读取数据头部数据(前五行): \n", df.head())

# 计算温度差(新增列)
# 注意df['bWendu']其实是一个Series,后面的减法返回的是Series
df.loc[:, 'wencha'] = df['bWendu'] - df['yWendu']
print("读取数据头部数据(前五行): \n", df.head())

5-3 df.apply方法

实例:添加一列温度类型

  1. 如果温度大于33度就是高温
  2. 低于-10度就是低温
  3. 否则是常温
import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')
print("读取数据头部数据(前五行): \n", df.head())


def get_wendu_type(x):
    if x['bWendu'] > 33:
        return "高温"
    elif x['yWendu'] < -10:
        return "低温"
    else:
        return "常温"


# 注意需要设置axis--1,这时Series的index是columns
df.loc[:, 'wendu_type'] = df.apply(get_wendu_type, axis=1)
# 打印前几行数据
print("新增列后读取数据头部数据(前五行): \n", df.head())

# 查看温度类型的计数
print("查看温度类型的计数: \n", df['wendu_type'].value_counts())

5-4 df.assign方法

实例:将温度从摄氏度变成华氏度

import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

print("读取数据头部数据(前五行): \n", df.head())

# 将温度从摄氏度变成华氏度
df_huashi = df.assign(
    yWendu_huashi=lambda x: x['yWendu'] * 9 / 5 + 32,
    bWendu_huashi=lambda x: x['bWendu'] * 9 / 5 + 32
)

print("将温度从摄氏度变成华氏度: \n", df_huashi.head())

5-5 按条件选择分组分别赋值

按条件先选择数据,然后对着部分数据赋值新列

实例:高低温差大于10度,则认为温差较大

import pandas as pd

file_path = "../files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

# 打印前几行数据
print("打印前几行数据: \n", df.head())

# 先创建空列(这是第一种创建新列的方法)
df['wencha_type'] = ""
df.loc[df['bWendu'] - df['yWendu'] > 10, 'wencha_type'] = "温差大"
df.loc[df['bWendu'] - df['yWendu'] <= 10, 'wencha_type'] = "温差正常"

# 打印前几行数据
print("打印前几行数据: \n", df.head())

# 查看温差类型的计数
print("查看温差类型的计数: \n", df['wencha_type'].value_counts())

(6) Pandas 数据统计函数

6-1 读取csv数据

import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

print("打印前3行数据: \n", df.head(3))

6-2 汇总类统计

import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

# 打印前三行
print("打印前三行的数据: \n", df.head(3))

# 提取所有数字列统计结果
print("提取所有数字列统计结果: \n", df.describe())

# 查看单个Series的数据
print("查看单个Series的数据: \n", df['bWendu'].mean())

# 最高温
print("最高温: \n", df['bWendu'].max())

# 最低温
print("最低温: \n", df['yWendu'].min())

6-3 唯一去重和按值计数

import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

# ---------------------- 1 唯一去重性 ------------------------- #
# 一般不用于数值列,而是枚举,分类列
print('*' * 25, '唯一去重性', '*' * 25)
print("唯一去重性 - fengxiang: \n", df['fengxiang'].unique())
print("唯一去重性 - tianqi: \n", df['tianqi'].unique())
print("唯一去重性 - fengli: \n", df['fengli'].unique())

# ---------------------- 2 按值计数 ------------------------- #
print('*' * 25, '按值计数', '*' * 25)
print("按值计数 - fengxiang: \n", df['fengxiang'].value_counts())
print("按值计数 - tianqi: \n", df['tianqi'].value_counts())
print("按值计数 - fengli: \n", df['fengli'].value_counts())

6-4 相关系数和协方差

用途(超级厉害):

  1. 两支股票,是不是同涨同跌?程度多大?正相关还是负相关?
  2. 产品销量的波动,跟那些因素正相关,负相关,程度有多大?

来自知乎,对于两个变量X,Y

  1. 协方差:衡量同向反向程度,如果协方差为正,说明X,Y同向变化。协方差越大说明同向程度越高;如果协方差为负,说明X,Y反向运动,协方差越小说明反向程度越高。
  2. 相关系数:衡量相似程度,当他们的相关系数为1时,说明两个变量变化是的正向相似度最大,当相关系数为-1时,说明两个变量的反向相似度最大
import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

# 协方差矩阵
print('*' * 25, '协方差矩阵', '*' * 25)
print("协方差矩阵: \n", df.cov())

# 相关系数矩阵
print('*' * 25, '相关系数矩阵', '*' * 25)
print("相关系数矩阵: \n", df.corr())

# 单独查看空气质量和最高温度的相关系数
print('*' * 25, '单独查看空气质量和最高温度的相关系数', '*' * 25)
print("最低温度: ", df['aqi'].corr(df['bWendu']))
print("最高温度: ", df['aqi'].corr(df['yWendu']))

# 空气质量和温度差的相关系数
print('*' * 25, '空气质量和温度差的相关系数', '*' * 25)
print("空气质量和温度差的相关系数: \n", df['aqi'].corr(df['yWendu'] - df['bWendu']))

(7) Pandas 缺失值处理

7-1 处理方式

Pandas使用这些函数处理缺失值:

函数 说明
isnull | notnull 检测是否是空值,可用于df和Series
dropna 丢弃,删除缺失值
fillna 填充空值

dropna属性:

属性 说明
axis 删除行还是列,{0 ro 'index', 1 or 'columns'}, default 0
how 如果等于any则任何值为空都删除,如果等于all则所有值都为空时才删除
inplace 如果为True则修改当前df, 否则返回新的df

fillna属性:

属性 说明
value 用于填充的值,可以是单个值,或者字典(key是列名,value是值)
method 等于ffill使用前一个部位空的值填充forword fill; 等于bfill使用后一个部位空的值天充backword fill axis:
axis 按行还是按列填充,
inplace 如果为True则修改当前df, 否则返回新的df

7-2 基本案例

实例:特殊Excel的读取,清洗,处理

import pandas as pd

# 第一步:读取Excel的时候忽略前几个空行
print('*' * 25, '第一步:读取Excel的时候忽略前几个空行', '*' * 25)
file_path = "../datas/student_excel/student_excel.xlsx"
studf = pd.read_excel(file_path, skiprows=2)
print(studf)

# 第二步:检测空值
print('*' * 25, '第二步:检测空值', '*' * 25)
print(studf.isnull())
print('*' * 25, '筛选分数为空的值', '*' * 25)
print(studf['分数'].isnull())
print('*' * 25, '筛选分数不为空的值', '*' * 25)
print(studf['分数'].notnull())
print('*' * 25, '筛选没有空分数的所有行', '*' * 25)
print(studf.loc[studf['分数'].notnull(), :])

# 第三步:删除全是空值的列
studf.dropna(axis='columns', how='all', inplace=True)
print('*' * 25, '第三步:删除全是空值的列', '*' * 25)
print(studf)

# 第四步:删除全是空值的行
studf.dropna(axis='index', how='all', inplace=True)
print('*' * 25, '第四步:删除全是空值的行', '*' * 25)
print(studf)

# 第五步:将分数列为空的填充为0分
# studf.fillna({"分数": 0})   # 有点小问题
studf.loc[:, '分数'] = studf['分数'].fillna(0)  # 两种方式相同
print('*' * 25, '第五步:将分数列为空的填充为0分', '*' * 25)
print(studf)

# 第六步:将姓名的缺失值填充
studf.loc[:, '姓名'] = studf['姓名'].fillna(method='ffill')
print('*' * 25, '第六步:将姓名的缺失值填充', '*' * 25)
print(studf)

# 第七步:将清洗好的execel保存
print('*' * 25, '第七步:将清洗好的execel保存', '*' * 25)
studf.to_excel("../datas/student_excel/student_excel_clean.xlsx", index=False)

(8) Pandas 复制警告的设置

8-1 读取数据

import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

print("打印数据前三行: \n", df.head(3))

8-2 复现

import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
print('*' * 25, '替换温度的后缀,并转为int类型', '*' * 25)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')
print("打印数据前三行: \n", df.head(3))

# 只选出3月份的数据用于分析
condition = df['ymd'].str.startswith('2018-03')
print('*' * 25, '只选出3月份的数据用于分析', '*' * 25)
print(condition)

# 设置温差
print('*' * 25, '设置温差', '*' * 25)
df[condition]['wen_cha'] = df['bWendu'] - df['yWendu']

报错

F:/Python_Projects/PythonDataAnalysis/4_Pandas/8_Pandas的SettingWithCopyWarning/8.1 复现/demo.py:19: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df[condition]['wen_cha'] = df['bWendu'] - df['yWendu']

原因:

  • 发出警告的代码 df[condition]['wen_cha'] = df['bWendu'] - df['yWendu']
  • 相当于:df.get(condition).set(wen_cha), 第一步骤的get发出了警报
  • 链式操作确实是两个步骤,先get后set,get得到的dataframe可能是view也可能是copy,pandas发出警告
  • 核心要诀:pandas的dataframe的修改写操作,只允许在源dataframe上进行,一步到位

8-3 解决

解决方式1
import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')
print('*' * 25, '替换温度的后缀,并转为int类型', '*' * 25)
print(df.head(3))

# 只选出3月份的数据用于分析
condition = df['ymd'].str.startswith('2018-03')
print('*' * 25, '只选出3月份的数据用于分析', '*' * 25)
print(condition)

# 设置温差
print('*' * 25, '设置温差', '*' * 25)
# 将get + set的两步操作,改成set的一步操作
df.loc[condition, 'wen_cha'] = df['bWendu'] - df['yWendu']

# 查看是否修改成功
print('*' * 25, '查看是否修改成功', '*' * 25)
print(df[condition].head())
解决方式2
import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

# 替换温度的后缀℃, 并转为int32(修改列)
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')
print('*' * 25, '替换温度的后缀,并转为int类型', '*' * 25)
print(df.head(3))

# 只选出3月份的数据用于分析
condition = df['ymd'].str.startswith('2018-03')
print('*' * 25, '只选出3月份的数据用于分析', '*' * 25)
print(condition)

# 设置温差
# 如果需要预筛选数据做后续的处理分析,使用copy赋值dataframe
print('*' * 25, '设置温差', '*' * 25)
df_month3 = df[condition].copy()
print(df_month3.head())
df_month3['wen_cha'] = df['bWendu'] - df['yWendu']

# 查看是否修改成功
print('*' * 25, '查看是否修改成功', '*' * 25)
print(df[condition].head())

总而言之,pandas不允许先筛选子dataframe,再进行修改写入

要么使用.loc实现一个步骤直接修改源dataframe

要么先复制一个子dataframe再一个步骤执行修改

(9) pandas 数据排序

9-1 Series的排序

语法:

Series.sort_values(ascending=True, inplace=Flase) 

属性说明:

  • ascending:默认为True升序排列,为Flase降序排序
  • inplace: 是否修改原始的Series
import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

print('*' * 25, '打印前几行数据', '*' * 25)
print(df.head())

# -------------------- series排序 --------------------- #
print('*' * 25, 'aqi升序', '*' * 25)
print(df['aqi'].sort_values())

print('*' * 25, 'aqi降序', '*' * 25)
print(df['aqi'].sort_values(ascending=False))

print('*' * 25, 'tianqi中文排列', '*' * 25)
print(df['tianqi'].sort_values())

9-2 DataFrame的排序

语法:

DataFrame.sort_values(by, ascending=True, inplace=Flase )

属性说明:

  • by: 字符串或者List<字符串>,单列排序或者多列排序
  • ascending:bool或者list,升序还是降序,如果是list对应by的多列
  • inplace:是否修改原始的DataFrame
单列排序
import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

print('*' * 25, '打印前几行数据', '*' * 25)
print(df.head())

# ---------------------- DataFrame排序 ----------------------- #
# 单列排序
print('*' * 25, 'aqi升序', '*' * 25)
print(df.sort_values(by='aqi'))

print('*' * 25, 'aqi降序', '*' * 25)
print(df.sort_values(by='aqi', ascending=False))
多列排序
import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

print('*' * 25, '打印前几行数据', '*' * 25)
print(df.head())

# ---------------------- DataFrame排序 ----------------------- #
# 多列排序
print('*' * 25, '按空气质量等级,最高温度排序,默认升序', '*' * 25)
print(df.sort_values(by=['aqiLevel', 'bWendu']))

print('*' * 25, '按空气质量等级,最高温度排序,指定降序', '*' * 25)
print(df.sort_values(by=['aqiLevel', 'bWendu'], ascending=False))

print('*' * 25, '分别指定升序和降序', '*' * 25)
print(df.sort_values(by=['aqiLevel', 'bWendu'], ascending=[True, False]))

(10) Pandas 字符串处理

前面我们已经使用了字符串处理函数:

df['bWendu'].try.replace('℃', '').astype('int32')

Pandas的字符串处理:

​ 1、使用方法:先获取Series的str属性,然后在属性上调用函数;

​ 2、只能在字符串列上使用,不能在数字列上使用;

​ 3、DataFrame上没有str属性和处理方法;

​ 4、Series.str并不是Python原生字符串,而是自己的一套方法,不过大部分和原生str很相似

Series.str字符串方法列表参考文档

参考文档

本节演示内容:

​ 1、获取Series的str属性,然后使用各种字符串处理函数

​ 2、使用str的startswith,contains等bool类Series可以做条件查询

​ 3、需要多次str处理的链式操作

​ 4、使用正则表达式处理

1-1 获取Series的str属性

使用各种字符串处理函数

import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)
print('*' * 25, '打印前几行数据', '*' * 25)
print(df.head())
print('*' * 25, '打印每列数据类型', '*' * 25)
print(df.dtypes)

print('*' * 25, '获取Series的str属性', '*' * 25)
print(df['bWendu'].str)

print('*' * 25, '字符串替换函数', '*' * 25)
df['bWendu'].str.replace('℃', '')

print('*' * 25, '判断是不是数字', '*' * 25)
print(df['bWendu'].str.isnumeric())

print('*' * 25, '判断是不是数字', '*' * 25)
print(df['aqi'].str.len())

1-2 使用str的startswith

contains等得到bool的Series可以做条件查询

import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

print('*' * 25, '打印前几行数据', '*' * 25)
print(df.head())
print('*' * 25, '打印每列数据类型', '*' * 25)
print(df.dtypes)

condition = df['ymd'].str.startswith('2018-03')
print(condition)
print(df[condition].head())

1-3 需要多次str处理的链式操作

怎样提取201803这样的数字月份?

1、先将日期2018-03-31替换成20180331的形式

2、提取月份字符串201803

import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

print('*' * 25, '打印前几行数据', '*' * 25)
print(df.head())
print('*' * 25, '打印每列数据类型', '*' * 25)
print(df.dtypes)

# 先将日期2018-03-31替换成20180331的形式
print('*' * 50)
print(df['ymd'].str.replace('-', ''))

# 每次调用函数,都返回一个新的Series
# df['ymd'].str.replace('-', '').slice(0, 6)    # 错误写法
print('*' * 50)
print(df['ymd'].str.replace('-', '').str.slice(0, 6))

# slice就是切片语法,可以直接使用
print('*' * 50)
print(df['ymd'].str.replace('-', '').str[0:6])

1-4 使用正则表达式处理

import pandas as pd

file_path = "../../datas/files/beijing_tianqi_2018.csv"
df = pd.read_csv(file_path)

print('*' * 25, '打印前几行数据', '*' * 25)
print(df.head())
print('*' * 25, '打印每列数据类型', '*' * 25)
print(df.dtypes)


# 添加新列
print('*' * 25, '添加新列', '*' * 25)
def get_nianyyueri(x):
    year, month, day = x['ymd'].split('-')
    return f'{year}年{month}月{day}日'

df['中文日期'] = df.apply(get_nianyyueri, axis=1)
print(df['中文日期'])

# 问题:怎样将"2018年12月31日"中的年、月、日三个中文字符去掉
# 方法1:链式replace
# print(df['中文日期'].str.replace('年', '').str.replace('月', '').str.replace('日', ''))

# 方法2:正则表达式替换(推荐使用)
# Series.str默认就开启了正则表达式模式
print('*' * 25, '正则表达式替换', '*' * 25)
print(df['中文日期'].str.replace('[年月日]', ''))

(11) Pandas axis参数

1、axis = 0 或者 axis = 'index'

  • 如果是单行操作,就是指某一行
  • 如果是聚合操作,指的就是跨行corss rows

2、axis = 1 或者 axis = 'columns'

  • 如果是单列操作,就是指某一列
  • 如果是聚合操作,指的就是跨列corss columns

按哪个axis,就是这个axis要动起来(类似被for遍历),其他的axis保持不动

import pandas as pd
import numpy as np

df = pd.DataFrame(
    np.arange(12).reshape(3, 4),
    columns=['A', 'B', 'C', 'D']
)
print(df)

# 1. 单列drop,就是删除某一列
# 代表的就是删除某列
print('*' * 50)
print(df.drop('A', axis=1))

# 2. 单行drop, 就是删除某行
# 代表的就是删除某行
print('*' * 50)
print(df.drop(1, axis=0))

# 3. 按axis=0/index执行mean聚合操作
# 反直觉:输出的不是每行的结果,而是每列的结果
print('*' * 50)
print(df.mean(axis=0))

# 4. 按axis=1/columns执行聚mean合操作
# 反直觉:输出的不是每行的结果,而是每列的结果
print('*' * 50)
print(df.mean(axis=1))

# 5. 再次举例,加深理解
print('*' * 50)


def get_sum_value(x):
    return x['A'] + x['B'] + x['C'] + x['D']


df['sum_value'] = df.apply(get_sum_value, axis=1)
print(df)

axis-index

(12) Pandas 索引index

把数据存储于普通的column列也能用于数据查询,那使用index有什么好处?

index的用途总结:

  1. 更方便的数据查询
  2. 使用index可以获得性能上的提升
  3. 自动的数据对其功能
  4. 更多更强大的数据结构支持

12-1 使用index查询数据

import pandas as pd

df = pd.read_csv('../../datas/files/ratings.csv')
print('*' * 25, '1. 读取csv文件,打印前几行数据', '*' * 25)
print(df.head())
print('*' * 25, '计算条数', '*' * 25)
print(df.count())

# -------------------- 1. 使用index查询数据 ------------------------ #
# drop==False,让索引列还保持在column
df.set_index('userId', inplace=True, drop=False)
print('*' * 25, '打印前几行数据', '*' * 25)
print(df.head())
print('*' * 25, '打印索引', '*' * 25)
print(df.index)

# 使用index的查询方法
print('*' * 25, '使用index的查询方法', '*' * 25)
print(df.loc[500].head(5))

# 使用column的condition查询方法
print('*' * 25, '使用column的condition查询方法', '*' * 25)
print(df.loc[df['userId'] == 500].head())

12-2 使用index会提升查询性能

  • 如果index是唯一的,Pandas会适应哈希表优化,查询性能为O(1);
  • 如果index不是唯一的,但是有序,Pandas会使用二分查找算法,查询性能为O(logN)
  • 如果index是完全随机的,那么每次查询都要扫描全表,查询性能为O(N);

12-3 完全随机的顺序查询

import pandas as pd
import timeit
from sklearn.utils import shuffle

df = pd.read_csv('../../../datas/files/ratings.csv')
print('*' * 25, '1. 读取csv文件,打印前几行数据', '*' * 25)
print(df.head())

# 将数据随机打散
print('*' * 25, '2. 将数据随机打散,打印前几行数据', '*' * 25)
df_shuffle = shuffle(df)
print(df_shuffle.head())

# 判断索引是否是递增的
print('*' * 25, '3. 判断索引是否是递增的', '*' * 25)
print(df_shuffle.index.is_monotonic_increasing)

# 判断索引是否唯一
print('*' * 25, '4. 判断索引是否唯一', '*' * 25)
print(df_shuffle.index.is_unique)

# 计时查询id==500的数据查询性能
print('*' * 25, '4. 计时查询id==500的数据查询性能', '*' * 25)
def test():
    return df_shuffle.loc[500]
print(timeit.timeit(stmt=test, number=10))

12-4 将index排序后的查询

import pandas as pd
import timeit
from sklearn.utils import shuffle

df = pd.read_csv('../../../datas/files/ratings.csv')
print('*' * 25, '1. 读取csv文件,打印前几行数据', '*' * 25)
print(df.head())

# 将数据随机打散
print('*' * 25, '2. 将数据随机打散,打印前几行数据', '*' * 25)
df_shuffle = shuffle(df)
print(df_shuffle.head())

# 将数据按照index排序
print('*' * 25, '3. 将数据按照index排序,打印前几行数据', '*' * 25)
df_sorted = df_shuffle.sort_index()
print(df_sorted.head())

# 判断索引是否是递增的
print('*' * 25, '4. 判断索引是否是递增的', '*' * 25)
print(df_sorted.index.is_monotonic_increasing)

# 判断索引是否唯一
print('*' * 25, '5. 判断索引是否唯一', '*' * 25)
print(df_sorted.index.is_unique)

# 计时查询id==500的数据查询性能
print('*' * 25, '5. 计时查询id==500的数据查询性能', '*' * 25)
def test():
    return df_sorted.loc[500]
print(timeit.timeit(stmt=test, number=10))

12-5 使用index能自动对齐数据

import pandas as pd

s1 = pd.Series([1, 2, 3], index=list('abc'))
print(s1)

s2 = pd.Series([2, 3, 4], index=list('bcd'))
print(s2)

print(s1 + s2)

12-6 使用index更多更强大的数据结构支持

  • CategoricalIndex, 基于分类数据的index,提升性能;
  • MultiIndex, 多维索引,用于groupby多维聚合后结果等;
  • DatetimeIndex, 时间类型索引,强大的日期和时间的方法支持;

(13) Pandas Merge语法

Pandas的Merge, 相当于SQL的Join, 将不同的表按key关联到一个表

Merge语法:

pd.merge(
    left,
    right,
    how = "inner",
    on = None,
    left_on = None,
    right_on = None,
    left_index = False,
    right_index = False,
    sort = False,
    suffixes = ("_x", "_y"),
    copy = True,
    indicator = False,
    validate = None,
)

参数解释:

  • left, rigth : 要merge的dataframe或者有name的Series
  • how : Join类型, 'left', 'right', 'outer', 'inner'
  • on : Join的可以,left和rigth都需要这个key
  • left_on : left的df或者series的key
  • right_on : right的df或者series的key
  • left_index, right_index : 使用index而不是普通的cloumn做join
  • suffixes : 两个元素的后缀,如果列有同名,自动添加后缀,默认是('_x', '_y')

文档地址:参考文档
本次讲解提纲:

  1. 电影数据的join实例
  2. 理解merge时一对一,一对多, 多对多的数量对齐关系
  3. 理解left join, right join, inner join, outer join的区别
  4. 如果出现非key的字段重名怎么办

13-1 电影数据的join实例

电影评分数据集

是推荐系统研究的很好的数据集

包含三个文件

  1. 用户对于电影的评分数据 ratings.dat
  2. 用户本身的信息数据 users.dat
  3. 电影本身的数据 movies.dat

可以关联三个表,得到一个完整的大表

数据官方地址:https://grouplens.org/datasets/movielens/

import pandas as pd

# 1.读取电影的评分数据, 打印前几行数据
print('*' * 25, '1.读取电影的评分数据, 打印前几行数据', '*' * 25)
df_ratings = pd.read_csv(
    '../../datas/movielens-1m/ratings.dat',
    sep='::',
    engine='python',
    names='UserID::MovieID::Rating::Timestamp'.split("::")
)
print(df_ratings.head())

# 2.读取用户信息数据, 打印前几行数据
print('*' * 25, '2.读取用户信息数据, 打印前几行数据', '*' * 25)
df_users = pd.read_csv(
    '../../datas/movielens-1m/users.dat',
    sep='::',
    engine='python',
    names='UserID::Gender::Age::Occupation::Zip-code'.split("::")
)
print(df_users.head())

# 3.读取电影信息数据, 打印前几行数据
print('*' * 25, '3.读取电影信息数据, 打印前几行数据', '*' * 25)
df_movies = pd.read_csv(
    '../../datas/movielens-1m/movies.dat',
    sep='::',
    engine='python',
    names='MovieID::Title::Genres'.split("::")
)
print(df_movies.head())

# 4.电影评分表和用户表进行Join
print('*' * 25, '4.电影评分表和用户表进行Join', '*' * 25)
df_ratings_users = pd.merge(df_ratings, df_users, left_on='UserID', right_on='UserID', how='inner')
print(df_ratings_users.head())

# 5.电影评分表和用户表和电影表进行Join
df_ratings_users_movies = pd.merge(df_ratings_users, df_movies, left_on='MovieID', right_on='MovieID', how='inner')
print(df_ratings_users_movies.head(10))

13-2 理解merge时数量的对齐关系

以下关系要正确理解:

1、one-to-noe : 一对一关系,关联的key都是唯一的

  • 比如(学号,姓名),merge(学号,年龄)
  • 结果条数为1*1

2、one-to-many : 一对多关系,左边唯一key,右边不唯一key

  • 比如(学号,姓名) merge(学号,[语文成绩,数学成绩 ,英语成绩])
  • 结果条数为1*N

3、many-to-many : 多对多关系,左边右边都不唯一

  • 比如(学号, [语文成绩,数学成绩 ,英语成绩]) merge(学号, [篮球,足球,乒乓球])
  • 结果条数为M*N
one-to-one 一对一关系的merge

import pandas as pd

print('*' * 25, '学号-姓名', '*' * 25)
left = pd.DataFrame({
    'sno': [11, 12, 13, 14],
    'name': ['name_a', 'name_b', 'name_c', 'name_d']
})
print(left)

print('*' * 25, '学号-年龄', '*' * 25)
right = pd.DataFrame({
    'sno': [11, 12, 13, 14],
    'age': ['21', '22', '23', '24']
})
print(right)

# 一对一关系,结果中有4条
print('*' * 25, '一对一关系', '*' * 25)
print(pd.merge(left, right, on='sno'))
one-to-many 一对多关系的merge

注意数据会被复制

import pandas as pd

print('*' * 25, '学号-姓名', '*' * 25)
left = pd.DataFrame({
    'sno': [11, 12, 13, 14],
    'name': ['name_a', 'name_b', 'name_c', 'name_d']
})
print(left)

print('*' * 25, '学号-成绩', '*' * 25)
right = pd.DataFrame({
    'sno': [11, 11, 11, 12, 12, 13],
    'grade': ['语文88', '数学90', '英语75', '语文66', '数学55', '英语29']
})
print(right)

# 一对多关系,数据以多的一边为准
print('*' * 25, '一对多关系', '*' * 25)
print(pd.merge(left, right, on='sno'))
many-to-many 多对多关系的merge

注意:结果数量会出现乘法

import pandas as pd

print('*' * 25, '学号-爱好', '*' * 25)
left = pd.DataFrame({
    'sno': [11, 11, 12, 12, 12],
    '爱好': ['篮球', '羽毛球', '乒乓球', '篮球', '足球']
})
print(left)

print('*' * 25, '学号-成绩', '*' * 25)
right = pd.DataFrame({
    'sno': [11, 11, 11, 12, 12, 13],
    'grade': ['语文88', '数学90', '英语75', '语文66', '数学55', '英语29']
})
print(right)

# 多对多关系,结果数量会出现乘法
print('*' * 25, '一对多关系', '*' * 25)
print(pd.merge(left, right, on='sno'))

13-3 理解各 join 的区别

读取数据
import pandas as pd

left = pd.DataFrame({
    'key': ['k0', 'k1', 'k2', 'k3'],
    'A': ['A0', 'A1', 'A2', 'A3'],
    'B': ['B0', 'B1', 'B2', 'B3'],
})
print('*' * 25, 'left', '*' * 25)
print(left)

right = pd.DataFrame({
    'key': ['k0', 'k1', 'k4', 'k5'],
    'C': ['C0', 'C1', 'C2', 'C3'],
    'D': ['D0', 'D1', 'D2', 'D3'],
})
print('*' * 25, 'right', '*' * 25)
print(right)
inner join, 默认

左边和右边的key都有,才会出现在结果里

import pandas as pd

left = pd.DataFrame({
    'key': ['k0', 'k1', 'k2', 'k3'],
    'A': ['A0', 'A1', 'A2', 'A3'],
    'B': ['B0', 'B1', 'B2', 'B3'],
})
print('*' * 25, 'left', '*' * 25)
print(left)

right = pd.DataFrame({
    'key': ['k0', 'k1', 'k2', 'k3'],
    'C': ['C0', 'C1', 'C2', 'C3'],
    'D': ['D0', 'D1', 'D2', 'D3'],
})
print('*' * 25, 'right', '*' * 25)
print(right)

# inner join
print('*' * 25, 'inner join', '*' * 25)
print(pd.merge(left, right, how='inner'))
left join

左边的都会出现在结果里,右边的如果无法匹配则为Null

import pandas as pd

left = pd.DataFrame({
    'key': ['k0', 'k1', 'k2', 'k3'],
    'A': ['A0', 'A1', 'A2', 'A3'],
    'B': ['B0', 'B1', 'B2', 'B3'],
})
print('*' * 25, 'left', '*' * 25)
print(left)

right = pd.DataFrame({
    'key': ['k0', 'k1', 'k4', 'k5'],
    'C': ['C0', 'C1', 'C2', 'C3'],
    'D': ['D0', 'D1', 'D2', 'D3'],
})
print('*' * 25, 'right', '*' * 25)
print(right)

# left join
print('*' * 25, 'left join', '*' * 25)
print(pd.merge(left, right, how='left'))
right join

右边的都会出现在结果里,左边的如果无法匹配则为Null

import pandas as pd

left = pd.DataFrame({
    'key': ['k0', 'k1', 'k2', 'k3'],
    'A': ['A0', 'A1', 'A2', 'A3'],
    'B': ['B0', 'B1', 'B2', 'B3'],
})
print('*' * 25, 'left', '*' * 25)
print(left)

right = pd.DataFrame({
    'key': ['k0', 'k1', 'k4', 'k5'],
    'C': ['C0', 'C1', 'C2', 'C3'],
    'D': ['D0', 'D1', 'D2', 'D3'],
})
print('*' * 25, 'right', '*' * 25)
print(right)

# right join
print('*' * 25, 'right join', '*' * 25)
print(pd.merge(left, right, how='right'))
outer join

左边,右边的都会出现在结果里,如果无法匹配则为Null

import pandas as pd

left = pd.DataFrame({
    'key': ['k0', 'k1', 'k2', 'k3'],
    'A': ['A0', 'A1', 'A2', 'A3'],
    'B': ['B0', 'B1', 'B2', 'B3'],
})
print('*' * 25, 'left', '*' * 25)
print(left)

right = pd.DataFrame({
    'key': ['k0', 'k1', 'k4', 'k5'],
    'C': ['C0', 'C1', 'C2', 'C3'],
    'D': ['D0', 'D1', 'D2', 'D3'],
})
print('*' * 25, 'right', '*' * 25)
print(right)

# outer join
print('*' * 25, 'outer join', '*' * 25)
print(pd.merge(left, right, how='outer'))

13-4 如果出现非key的字段重名怎么办

import pandas as pd

left = pd.DataFrame({
    'key': ['k0', 'k1', 'k2', 'k3'],
    'A': ['A0', 'A1', 'A2', 'A3'],
    'B': ['B0', 'B1', 'B2', 'B3'],
})
print('*' * 25, 'left', '*' * 25)
print(left)

right = pd.DataFrame({
    'key': ['k0', 'k1', 'k4', 'k5'],
    'A': ['A10', 'A11', 'A12', 'A13'],
    'D': ['D0', 'D1', 'D4', 'D5'],
})
print('*' * 25, 'right', '*' * 25)
print(right)

print('*' * 25, '默认处理方式', '*' * 25)
print(pd.merge(left, right, on='key'))

print('*' * 25, '自己指定后缀', '*' * 25)
print(pd.merge(left, right, on='key', suffixes=('_left', '_right')))

(14) Pandas Concat合并

使用场景:

  • 批量合并相同格式的Excel,给DataFrame添加行,给DataFrame添加列

一句话说明concat语法:

  • 使用某种合并方式(inner/outer)
  • 沿着某个轴向(axis=0/1)
  • 把多个Pandas对象(DataFrame/Series)合并成一个

concat语法:pd.concat(objs, axis=0, join='outer', ignore_index=False)

  • objs : 一个列表,内容可以是DataFrame或者Series,可以混合
  • axis : 默认是0代表按行合并,如果等于1代表按列合并
  • join : 合并的时候索引的对齐方式,默认是outer join,也可以是 inner join
  • ignore_index : 是否忽略掉原来的数据索引

append语法 : DataFrame.append(outer, ignore_index=Flase)

append只有按行合并,没有按列合并,相当于concat按行的简写形式

  • outer : 单个dataframe,series,dict,或者列表
  • ignore_index : 是否忽略掉原来的数据索引

参考文档

14-1 使用pandas.concat合并数据

import pandas as pd
import warnings

# 忽略警告
warnings.filterwarnings('ignore')

df_1 = pd.DataFrame({
    'A': ['A0', 'A1', 'A2', 'A3'],
    'B': ['B0', 'B1', 'B2', 'B3'],
    'C': ['C0', 'C1', 'C2', 'C3'],
    'D': ['D0', 'D1', 'D2', 'D3'],
    'E': ['E0', 'E1', 'E2', 'E3'],
})
print('*' * 25, '创建df_1,并打印', '*' * 25)
print(df_1)

df_2 = pd.DataFrame({
    'A': ['A4', 'A5', 'A6', 'A7'],
    'B': ['B4', 'B5', 'B6', 'B7'],
    'C': ['C4', 'C5', 'C6', 'C7'],
    'D': ['D4', 'D5', 'D6', 'D7'],
    'F': ['F4', 'F5', 'F6', 'F7'],
})
print('*' * 25, '创建df_2,并打印', '*' * 25)
print(df_1)

# 1. 默认的concat, 参数为axis=0, join=outher, ignore_index=False
print('*' * 10, '1. 默认的concat, 参数为axis=0, join=outher, ignore_index=False', '*' * 10)
print(pd.concat([df_1, df_2]))

# 2. 使用ignore_index=True可以忽略原来的索引
print('*' * 10, '2. 使用ignore_index=True可以忽略原来的索引', '*' * 10)
print(pd.concat([df_1, df_2], ignore_index=True))

# 3. 使用join=inner过滤不匹配的列
print('*' * 10, '3. 使用join=inner过滤不匹配的列', '*' * 10)
print(pd.concat([df_1, df_2], ignore_index=True, join='inner'))

# 4. 使用axis=1, 相当于添加新列
print('*' * 10, '4. 使用axis=1, 相当于添加新列', '*' * 10)

# A: 添加一列Series
print('*' * 15, 'A: 添加一列Series', '*' * 15)
s1 = pd.Series(list(range(4)), name='F')
print(pd.concat([df_1, s1], axis=1))

# B: 添加多列Series
print('*' * 15, 'B: 添加多列Series', '*' * 15)
s2 = df_1.apply(lambda x: x["A"] + '_GG', axis=1)
s2.name = 'G'
# 列表可以只有Series
print(pd.concat([s1, s2], axis=1))
# 列表可以是混合顺序的
print(pd.concat([s1, df_1, s2], axis=1))

14-2 使用DataFrame.append按行合并数据

import pandas as pd
import warnings

# 忽略警告
warnings.filterwarnings('ignore')

df_1 = pd.DataFrame([[1, 2], [3, 4]], columns=list('AB'))
print('*' * 25, '创建df_1,并打印', '*' * 25)
print(df_1)

df_2 = pd.DataFrame([[5, 6], [7, 8]], columns=list('AB'))
print('*' * 25, '创建df_2,并打印', '*' * 25)
print(df_2)

# 1. 给1个dataframe添加另一个dataframe
print('*' * 10, '1. 给1个dataframe添加另一个dataframe', '*' * 10)
print(df_1.append(df_2))

# 2. 忽略原来的索引ignore_index=True
print('*' * 10, '2. 忽略原来的索引ignore_index=True', '*' * 10)
print(df_1.append(df_2, ignore_index=True))

# 3. 可以一行一行的给DataFrame添加数据
print('*' * 10, '3. 可以一行一行的给DataFrame添加数据', '*' * 10)

# 一个空的df
df = pd.DataFrame(columns=['A'])

# A: 低性能版本
print('*' * 15, 'A: 低性能版本', '*' * 15)
for i in range(5):
    # 注意这里每次都在复制
    df = df.append({'A': i}, ignore_index=True)
print(df)

# B: 高性能版本
print('*' * 15, 'B: 高性能版本', '*' * 15)
# 第一个入参是一个列表,避免了多次复制
pd.concat(
    # 列表生成式
    [pd.DataFrame([i], columns=['A']) for i in range(5)],
    ignore_index=True
)
print(df)

(15) Pandas 批量拆分与合并Excel文件

实例演示:

  1. 将一个大的Excel等拆分成多个Excel
  2. 将多个小的Excel合并成一个大Excel并标记来源

15-1 读取源Excel到Pandas

import os
import pandas as pd

# 定义变量, 如果文件夹不存在则创建
work_dir = "../datas/course_datas/c15_excel_split_merge"
splits_dir = f'{work_dir}/splits'
if not os.path.exists(splits_dir):
    os.mkdir(splits_dir)

df_source = pd.read_excel(f'{work_dir}/crazyant_blog_articles_source.xlsx')
print('*' * 15, '打印前几行数据', '*' * 15)
print(df_source.head())
print('*' * 15, '打印索引', '*' * 15)
print(df_source.index)
print('*' * 15, '打印形状', '*' * 15)
print(df_source.shape)
total_row_count = df_source.shape[0]
print('*' * 15, '打印总条数', '*' * 15)
print(total_row_count)

15-2 将一个大的Excel等分拆分成多个Excel

  1. 使用df.iloc方法,将一个大的dataframe,拆分成多个小的dataframe
  2. 使用dataframe.to_excel保存每个小的Excel
import os
import pandas as pd

# 定义变量, 如果文件夹不存在则创建
work_dir = "../../datas/course_datas/c15_excel_split_merge"
splits_dir = f'{work_dir}/splits'
if not os.path.exists(splits_dir):
    os.mkdir(splits_dir)

df_source = pd.read_excel(f'{work_dir}/crazyant_blog_articles_source.xlsx')
total_row_count = df_source.shape[0]

# *********************** 1. 计算拆分后的每个Excel行数 *********************** #
print('*' * 15, '1. 计算拆分后的每个Excel行数', '*' * 15)
# 这个大Excel, 会拆分给这几个人
user_names = ["xiao_shuai", "xiao_wang", "xiao_ming", "xiao_lei", "xiao_bo", "xiao_hong"]
# 每个人的任务数目
split_size = total_row_count // len(user_names)
if total_row_count % len(user_names) != 0:
    split_size += 1
print(split_size)

# *********************** 2. 拆分成多个dataframe *********************** #
print('*' * 15, '2. 拆分成多个dataframe', '*' * 15)
df_subs = []
for idx, user_name in enumerate(user_names):
    # iloc的开始索引
    begin = idx * split_size
    # iloc的结束索引
    end = begin + split_size
    # 实现df按照iloc拆分
    df_sub = df_source.iloc[begin:end]
    # 将每个子df存入列表
    df_subs.append((idx, user_name, df_sub))
print(df_subs)

# *********************** 3. 将每个dataframe存入excel *********************** #
print('*' * 15, '3. 将每个dataframe存入excel', '*' * 15)
for idx, user_name, df_sub in df_subs:
    print(f'正在创建 : crazyant_blog_articles_{idx}_{user_name}.xlsx 文件')
    file_name = f'{splits_dir}/crazyant_blog_articles_{idx}_{user_name}.xlsx'
    df_sub.to_excel(file_name, index=False)
    print(f'crazyant_blog_articles_{idx}_{user_name}.xlsx 文件创建成功')

15-3 合并多个小的Excel到一个大的Excel

  1. 遍历文件夹,得到要合并的Excel文件列表
  2. 分别读取到dataframe,给每个df添加一列用于标记来源
  3. 使用pd.concat进行df批量合并
  4. 将合并后的dataframe输出到excel
import os
import pandas as pd

# 定义变量, 如果文件夹不存在则创建
work_dir = "../../datas/course_datas/c15_excel_split_merge"
splits_dir = f'{work_dir}/splits'

# *************** 1. 遍历文件夹,得到要合并的Excel名称列表 *************** #
print('*' * 15, '1. 遍历文件夹,得到要合并的Excel名称列表', '*' * 15)
excel_names = []
for excel_name in os.listdir(splits_dir):
    excel_names.append(excel_name)
print(excel_names)

# *************** 2. 分别读取到dataframe *************** #
print('*' * 15, '2. 分别读取到dataframe', '*' * 15)
df_list = []
for excel_name in excel_names:
    # 读取每个excel到df
    excel_path = f'{splits_dir}/{excel_name}'
    df_split = pd.read_excel(excel_path)
    # 得到user_name
    username = excel_name.replace('crazyant_blog_articles_', '').replace('.xlsx', '')[2:]
    print(excel_name, username)
    # 给每个dir添加一列,即用户名字
    df_split['username'] = username
    df_list.append(df_split)

# *************** 3. 使用pd.concat进行合并 *************** #
print('*' * 15, '3. 使用pd.concat进行合并', '*' * 15)
df_merged = pd.concat(df_list)
print(df_merged.shape)
print(df_merged.head())
print(df_merged['username'].value_counts())

# *************** 4. 将合并后的dataframe输出到excel *************** #
print('*' * 15, '4. 将合并后的dataframe输出到excel', '*' * 15)
df_merged.to_excel(f'{work_dir}/crazyant_blog_articles_merged.xlsx', index=False)
print("创建成功")

(16) Pandas groupby分组

类似SQL:

select city,max(temperature) from city_weather group by city;

groupby: 先对数组进行分组,然后在每个分组上引用聚合函数,转换函数

本次演示:

  1. 分组使用聚合函数做数据统计
  2. 遍历groupby的结果理解执行流程
  3. 实例分组探索天气数据

16-1 分组使用聚合函数做数据统计

import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

# 构建一个DataFrame
df = pd.DataFrame({
    'A': ['foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'foo'],
    'B': ['one', 'one', 'two', 'three', 'two', 'two', 'one', 'three'],
    'C': np.random.randn(8),
    'D': np.random.randn(8)
})
print(df)

# ********** 1. 单个列groupby, 查询所有数据列的统计 ********** #
print('*' * 10, '1. 单个列groupby, 查询所有数据列的统计', '*' * 10)
print(df.groupby('A').sum())
'''
我们看到:
    1. groupby中的'A'变成了数据的索引列
    2. 因为要统计sum,但B列不是数字,所以自动被忽略掉了
'''

# ********** 2. 多个列groupby, 查询所有数据列的统计 ********** #
print('*' * 10, '2. 多个列groupby, 查询所有数据列的统计', '*' * 10)
print(df.groupby(['A', 'B']).mean())
print('*' * 50)
# 我们看到:('A', 'B')成对变成了二级索引
print(df.groupby(['A', 'B'], as_index=False).mean())

# ********** 3. 同时查看多种数据统计 ********** #
print('*' * 10, '3. 同时查看多种数据统计', '*' * 10)
print(df.groupby('A').agg([np.sum, np.mean, np.std]))
# 我们看到:列变成了多级索引


# ********** 4. 查看单列的结果数据统计 ********** #
print('*' * 10, '4. 查看单列的结果数据统计', '*' * 10)
# 方法1:预过滤,性能更好
print('*' * 15, '方法1', '*' * 15)
print(df.groupby('A')['C'].agg([np.sum, np.mean, np.std]))
# 方法2:
print('*' * 15, '方法2', '*' * 15)
print(df.groupby('A').agg([np.sum, np.mean, np.std])['C'])

# ********** 5. 不同列使用不同的聚合函数 ********** #
print('*' * 10, '5. 不同列使用不同的聚合函数', '*' * 10)
print(df.groupby('A').agg({'C': np.sum, 'D': np.mean}))

16-2 遍历groupby的结果理解执行流程

for循环可以直接遍历每个group

import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

# 构建一个DataFrame
print('*' * 10, '0. 构建一个DataFrame', '*' * 10)
df = pd.DataFrame({
    'A': ['foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'foo'],
    'B': ['one', 'one', 'two', 'three', 'two', 'two', 'one', 'three'],
    'C': np.random.randn(8),
    'D': np.random.randn(8)
})
print(df)

# ********** 1. 遍历单个列的聚合分组 ********** #
print('*' * 10, '1. 遍历单个列的聚合分组', '*' * 10)
g = df.groupby('A')
print(g)
for name, group in g:
    print(name)
    print(group)
    print()

# 可以获取单个分组的数据
print('-' * 10, '可以获取单个分组的数据', '-' * 10)
print(g.get_group('bar'))

# ********** 2. 遍历多个列的聚合分组 ********** #
print('*' * 10, '2. 遍历多个列的聚合分组', '*' * 10)
g = df.groupby(['A', 'B'])
for name, group in g:
    print(name)
    print(group)
    print()

# 可以看到,name是一个2个元素的tuple,代表不同列
print('-' * 10, '可以看到,name是一个2个元素的tuple,代表不同列', '-' * 10)
print(g.get_group(('foo', 'one')))

# 可以直接查询group后的某几列,生成Series或者子DataFrame
print('-' * 10, '可以直接查询group后的某几列,生成Series或者子DataFrame', '-' * 10)
print(g['C'])
for name, group in g['C']:
    print(name)
    print(group)
    print(type(group))
    print()

其实所有的聚合统计,都是在dataframe和series上进行的

16-3 实例分组探索天气数据

import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

file_path = '../../datas/files/beijing_tianqi_2018.csv'
df = pd.read_csv(file_path)

# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')
print('*' * 15, '打印前几行数据', '*' * 15)
print(df.head())

# 新增一列为月份
print('*' * 15, '新增一列为月份', '*' * 15)
df['month'] = df['ymd'].str[:7]
print(df.head())

# *************** 1. 查看每月的最高温度 *************** #
print('*' * 15, '1. 查看每月的最高温度', '*' * 15)
data = df.groupby('month')['bWendu'].max()
print(data)

print('*' * 15, '绘图', '*' * 15)
plt.xticks(rotation=45)
plt.plot(data)
plt.show()

# *************** 2. 查看每个月最高温度,最低温度,平均空气质量指数 *************** #
print('*' * 15, '2. 查看每个月最高温度,最低温度,平均空气质量指数', '*' * 15)
group_data = df.groupby('month').agg({'bWendu': np.max, 'yWendu': np.min, 'aqi': np.mean})
print(group_data)

print('*' * 15, '绘图', '*' * 15)
plt.xticks(rotation=45)
plt.plot(group_data)
plt.show()

(17) Pandas 分层索引MultIndex

为什么要学习分层索引MultIndex?

  1. 分层索引:在一个轴向上拥有多个索引层级,可以表达更高维度数据的形式
  2. 可以更方便的进行数据筛选,如果有序则性能更好
  3. groupby等操作的结果,如果是多key, 结果是分层索引,需要会使用
  4. 一般不需要自己创建分层索引(MultIndex有构造函数单一般不用)

演示数据:百度,阿里巴巴,爱奇艺,京东四家公司的10天股票数据

数据来自:英为财经

https://cn.investing.com/

本次演示提纲

​ 1、Series的分层索引MultIndex

​ 2、Series有多层索引怎样筛选数据

​ 3、DataFrame的多层索引MultIndex

​ 4、DataFrame有多层索引怎样筛选数据

17-1 读取数据

import pandas as pd
from matplotlib import pyplot as plt

file_path = '../../datas/stocks/互联网公司股票.xlsx'
stocks = pd.read_excel(file_path)
print('*' * 15, '打印形状', '*' * 15)
print(stocks.shape)

print('*' * 15, '打印前三行数据', '*' * 15)
print(stocks.head(3))

print('*' * 15, '打印公司名', '*' * 15)
print(stocks['公司'].unique())

print('*' * 15, '打印索引', '*' * 15)
print(stocks.index)

print('*' * 15, '打印公司收盘平均值', '*' * 15)
print(stocks.groupby('公司')['收盘'].mean())

17-2 Series的分层索引MultIndex

import pandas as pd
from matplotlib import pyplot as plt

file_path = '../../datas/stocks/互联网公司股票.xlsx'
stocks = pd.read_excel(file_path)

ser = stocks.groupby(['公司', '日期'])['收盘'].mean()
print('*' * 15, '按照公司和日期分组', '*' * 15)
print(ser)

# 多维索引中,空白的意思是:使用上边的值
print('*' * 15, '查看分组后的索引', '*' * 15)
print(ser.index)

# 使用unstack把二级索引变成列
print('*' * 15, '使用unstack把二级索引变成列', '*' * 15)
ser = ser.unstack()
print(ser)
print(ser.reset_index())

17-3 Series有多层索引怎样筛选数据

import pandas as pd
from matplotlib import pyplot as plt

file_path = '../../datas/stocks/互联网公司股票.xlsx'
stocks = pd.read_excel(file_path)

ser = stocks.groupby(['公司', '日期'])['收盘'].mean()
print('*' * 15, '按照公司和日期分组', '*' * 15)
print(ser)

print('*' * 15, '筛选BIDU数据', '*' * 15)
print(ser['BIDU'])

print('*' * 15, '多层索引,可以使用元祖的形式筛选', '*' * 15)
print(ser.loc[('BIDU', '2019-10-02')])

print('*' * 15, '筛选第二层索引', '*' * 15)
print(ser.loc[:, '2019-10-02'])

17-4 DataFrame的多层索引MultIndex

import pandas as pd
from matplotlib import pyplot as plt

file_path = '../../datas/stocks/互联网公司股票.xlsx'
stocks = pd.read_excel(file_path)

print('*' * 15, '打印前三行数据', '*' * 15)
print(stocks.head(3))

print('*' * 15, '设置分层索引', '*' * 15)
stocks.set_index(['公司', '日期'], inplace=True)
print(stocks)

print('*' * 15, '打印索引', '*' * 15)
print(stocks.index)

print('*' * 15, '按照索引排序', '*' * 15)
stocks.sort_index(inplace=True)
print(stocks)

17-5 DataFrame有多层索引怎样筛选数据

【重要知识】:在选择数据时:

  • 元祖(key1, key2)代表筛选,其中key1是索引第一级, key2是第二级,比如key1=JD, key2=2019-10-02
  • 列表[key1, key2]代表同一层的多个KEY,其中key1和key2是并列的统计索引,比如key1=JD, key2=BIDU
import pandas as pd
from matplotlib import pyplot as plt

file_path = '../../datas/stocks/互联网公司股票.xlsx'
stocks = pd.read_excel(file_path)

stocks.set_index(['公司', '日期'], inplace=True)

print("*" * 15, "stocks.loc['BIDU']", "*" * 15)
print(stocks.loc['BIDU'])

print("*" * 15, "stocks.loc[('BIDU', '2019-10-02'), :]", "*" * 15)
print(stocks.loc[('BIDU', '2019-10-02'), :])

print("*" * 15, "stocks.loc[('BIDU', '2019-10-02'), '开盘']", "*" * 15)
print(stocks.loc[('BIDU', '2019-10-02'), '开盘'])

print("*" * 15, "stocks.loc[['BIDU', 'JD'], :]", "*" * 15)
print(stocks.loc[['BIDU', 'JD'], :])

print("*" * 15, "stocks.loc[['BIDU', 'JD'], '2019-10-03', :]", "*" * 15)
print(stocks.loc[['BIDU', 'JD'], '2019-10-03', :])

print("*" * 15, "stocks.loc[['BIDU', 'JD'], '2019-10-03', '收盘']", "*" * 15)
print(stocks.loc[(['BIDU', 'JD'], '2019-10-03'), '收盘'])

print("*" * 15, "stocks.loc[('BIDU', ['2019-10-02', '2019-10-03']), '收盘']", "*" * 15)
print(stocks.loc[('BIDU', ['2019-10-02', '2019-10-03']), '收盘'])

# slice(None)代表筛选这一索引的所有内容
print("*" * 15, "stocks.loc[(slice(None), ['2019-10-02', '2019-10-03']), :]", "*" * 15)
print(stocks.loc[(slice(None), ['2019-10-02', '2019-10-03']), :])

print("*" * 15, "stocks.reset_index()", "*" * 15)
print(stocks.reset_index())

(18) Pandas 数据转换函数

数据转换函数对比: map, apply, applymap

  1. map: 只用于Series,实现每个值->值的映射;
  2. apply: 用于Series实现每个值的处理,用户DataFrame实现某个轴的Series处理;
  3. applymap: 只能用于DataFrame,用于处理该DataFrame的每个元素;

18-1 map用于Series值的转换

实例:将股票代码英文转换成中文名字

Series.map(dict) or Series.map(function)均可

import pandas as pd

file_path = '../../datas/stocks/互联网公司股票.xlsx'
stocks = pd.read_excel(file_path)

print('*' * 15, '打印前几行数据', '*' * 15)
print(stocks.head())

print('*' * 15, '打印公司名', '*' * 15)
print(stocks['公司'].unique())

# 公司股票代码到中文的映射,注意这里是小写
dict_company_names = {
    'bidu': "百度",
    'baba': "阿里巴巴",
    'iq': "爱奇艺",
    'jd': "京东"
}

# ****************** 方法一:Series.map(dict) ****************** #
print('*' * 15, '方法一:Series.map(dict)', '*' * 15)
stocks['公司中文_1'] = stocks['公司'].str.lower().map(dict_company_names)
print(stocks.head())

# ****************** 方法二:Series.map(function) ****************** #
print('*' * 15, '方法二:Series.map(function)', '*' * 15)
stocks['公司中文_2'] = stocks['公司'].map(lambda x: dict_company_names[x.lower()])
print(stocks.head())

18-2 apply用于Series和DataFrame的转换

  • Series.apply(function),函数的参数是每个值
  • DtataFrame.apply(function),函数的参数是Series
import pandas as pd

file_path = '../../datas/stocks/互联网公司股票.xlsx'
stocks = pd.read_excel(file_path)

print('*' * 15, '打印前几行数据', '*' * 15)
print(stocks.head())

print('*' * 15, '打印公司名', '*' * 15)
print(stocks['公司'].unique())

# 公司股票代码到中文的映射,注意这里是小写
dict_company_names = {
    'bidu': "百度",
    'baba': "阿里巴巴",
    'iq': "爱奇艺",
    'jd': "京东"
}

# ****************** 方法一:Series.apply(function) ****************** #
# 函数的参数是每个值
print('*' * 15, '方法一:Series.apply(function)', '*' * 15)
stocks['公司中文_1'] = stocks['公司'].apply(lambda x: dict_company_names[x.lower()])
print(stocks.head())

# ****************** 方法二:DtataFrame.apply(function) ****************** #
# 函数的参数是Series
print('*' * 15, '方法二:DtataFrame.apply(function)', '*' * 15)
stocks['公司中文_2'] = stocks.apply(lambda x: dict_company_names[x['公司'].lower()], axis=1)
print(stocks.head())

注意这个代码:

  1. apply是在stocks这个DataFrame上调用的;
  2. lambda x的x是一个Series,因为指定了axis=1所以Series的key是列名,可以用x['公司']获取

18-3 applymap用于DataFrame所有值的转换

import pandas as pd

file_path = '../../datas/stocks/互联网公司股票.xlsx'
stocks = pd.read_excel(file_path)

print('*' * 15, '打印前几行数据', '*' * 15)
print(stocks.head())

print('*' * 15, '打印公司名', '*' * 15)
print(stocks['公司'].unique())

# 公司股票代码到中文的映射,注意这里是小写
dict_company_names = {
    'bidu': "百度",
    'baba': "阿里巴巴",
    'iq': "爱奇艺",
    'jd': "京东"
}

print('*' * 15, '获取指定列的所有数据', '*' * 15)
sub_df = stocks[['收盘', '开盘', '高', '低', '交易量']]
print(sub_df.head())

# 将这些数字去整数,应用于所有元素
print('*' * 15, '将这些数字去整数,应用于所有元素', '*' * 15)
print(sub_df.applymap(lambda x: int(x)))

# 直接修改原来df的这几列
print('*' * 15, '直接修改原来df的这几列', '*' * 15)
stocks.loc[:, ['收盘', '开盘', '高', '低', '交易量']] = sub_df.applymap(lambda x: int(x))
print(stocks.head())

(19) Pandas 对每个分组应用apply函数

知识:Pandas的GroupBy遵从split,apply,combine模式

这里的split指的是pandas的groupby,我们自己实现apply函数,apply返回的结果由pandas进行combine得到结果

GroupBy.apply(function):

  • function的第一个参数是dataframe
  • function的返回结果,可是dataframe、series、单个值,甚至和输入dataframe完全没关系

本次实例演示:

  1. 怎样对数值列按分组的归一化?
  2. 怎样提取每个分组的TOPN数据?

19-1 怎样对数值列按分组的归一化

将不同范围的数值列进行归一化,映射到[0,1]区间:

  • 更容易做数据横向对比,比如价格字段是几百到几千,增幅字段是0到100

  • 机器学习模型学的更快性能更好

归一化的公式:

(当前值-最小值)/(最大值-最小值)

演示:用户对电影评分的归一化

每个用户的评分不同,有的乐观派评分高,有的悲观派评分低,按用户做归一化

import pandas as pd

file_path = "../../datas/movielens-1m/ratings.dat"
ratings = pd.read_csv(file_path, sep='::', engine='python', names='UserID::MovieID::Rating::Timestamp'.split('::'))
print(ratings.head())

# 用户对电影评分的归一化
# 实现按照用户ID分组,然后对其中一列归一化
def ratings_norm(df):
    """
    @param df:每个用户分组的dataframe
    1. 取出最小值,和最大值
    2. 计算(当前值-最小值)/(最大值-最小值)并添加新列
    """
    min_value = df["Rating"].min()
    max_value = df["Rating"].max()
    df["Rating_norm"] = df["Rating"].apply(lambda x: (x - min_value) / (max_value - min_value))
    return df

ratings = ratings.groupby("UserID").apply(ratings_norm)
res = ratings[ratings["UserID"] == 1].head()
print(res)

可以看到UserID1这个用户,Rating3是他的最低分,是个乐观派,我们归一化到0分

19-2 怎样提取每个分组的TOPN数据?

获取2018年每月温度最高的两天数据

import pandas as pd

file_path = '../../datas/files/beijing_tianqi_2018.csv'
df = pd.read_csv(file_path)

# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')
# 新增一列为月份
df['month'] = df['ymd'].str[:7]
print(df.head())

def getWenduTopN(df, topn):
    """
    这里的df,是每个月份分组group的df
    """
    # 根据最高温度排序,取日期和温度,默认是升序,所以取最后两个
    return df.sort_values(by="bWendu")[["ymd", "bWendu"]][-topn:]


# 根据月份分组
res2 = df.groupby("month").apply(getWenduTopN, topn=2).head()
print(res2)

我们看到,groupby的apply函数返回的dataframe,其实和原来的dataframe其实可以完全不一样

(20) Pandas 数据透视

将列式数据变成二维交叉形式,便于分析,叫做重塑或透视

1、经过统计得到多维指标数据

2、使用unstack实现数据二维透视

3、使用pivot简化透视

4、stack,unstack,pivot的语法

20-1 经过统计得到多维指标数据

非常场景的统计场景,指定多个俄日度,计算聚合后的指标

实例:统计得到“电影评分数据集”,每个月份的每个分数被评分多少次:(月份,分数1~5,次数)

import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

print('*' * 15, '1. 读取数据集,并打印头部', '*' * 15)
df = pd.read_csv(
    '../../datas/movielens-1m/ratings.dat',
    header=None,
    names='UserID::MovieID::Rating::Timestamp'.split('::'),
    sep='::',
    engine='python'
)
print(df.head())

print('*' * 15, '2. 将时间戳转换为时间格式,并添加新列pdate', '*' * 15)
df['pdate'] = pd.to_datetime(df['Timestamp'], unit='s')
print(df.head())
print('*' * 15, '打印每列数据的数据类型', '*' * 15)
print(df.dtypes)

print('*' * 15, '3. 实现数据统计', '*' * 15)
df_group = df.groupby([df['pdate'].dt.month, 'Rating'])['UserID'].agg(pv=np.sum)
print(df_group.head(20))

对于这样格式的数据,我想瞎看按月份,不同评分的次数趋势,是没法实现的

需要将数据变换成每个评分是一列才可以实现

20-2 使用unstack实现数据二维透视

目的:想要画图对比按照月份的不同评分的数量趋势

import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

# 绘图函数
def mapping(datas):
    # 显示中文标签
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.plot(datas)
    # 设置描述信息
    plt.xlabel("月份")
    plt.ylabel("评分次数")
    # 设置图例
    plt.legend(['pv_1', 'pv_2', 'pv_3', 'pv_4', 'pv_5'],loc='upper left')
    plt.show()


print('*' * 15, '1. 读取数据集,并打印头部', '*' * 15)
df = pd.read_csv(
    '../../datas/movielens-1m/ratings.dat',
    header=None,
    names='UserID::MovieID::Rating::Timestamp'.split('::'),
    sep='::',
    engine='python'
)
print(df.head())

print('*' * 15, '2. 将时间戳转换为时间格式,并添加新列pdate', '*' * 15)
df['pdate'] = pd.to_datetime(df['Timestamp'], unit='s')
print(df.head())

print('*' * 15, '3. 实现数据统计', '*' * 15)
df_group = df.groupby([df['pdate'].dt.month, 'Rating'])['UserID'].agg(pv=np.sum)
print(df_group.head(20))

print('*' * 15, '4. 使用unstack实现数据的二维透视', '*' * 15)
df_stack = df_group.unstack()
print(df_stack)

print('*' * 15, '5. 使用plt绘图', '*' * 15)
mapping(df_stack)

# unstack和stack是互逆操作
print('*' * 15, '6. unstack和stack是互逆操作', '*' * 15)
print(df_stack.stack().head(20))

20-3 使用pivot简化透视

import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

# 绘图函数
def mapping(datas):
    # 显示中文标签
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.plot(datas)
    # 设置描述信息
    plt.xlabel("月份")
    plt.ylabel("评分次数")
    # 设置图例
    plt.legend(['pv_1', 'pv_2', 'pv_3', 'pv_4', 'pv_5'],loc='upper left')
    plt.show()


print('*' * 15, '1. 读取数据集,并打印头部', '*' * 15)
df = pd.read_csv(
    '../../datas/movielens-1m/ratings.dat',
    header=None,
    names='UserID::MovieID::Rating::Timestamp'.split('::'),
    sep='::',
    engine='python'
)
print(df.head())

print('*' * 15, '2. 将时间戳转换为时间格式,并添加新列pdate', '*' * 15)
df['pdate'] = pd.to_datetime(df['Timestamp'], unit='s')
print(df.head())

print('*' * 15, '3. 实现数据统计', '*' * 15)
df_group = df.groupby([df['pdate'].dt.month, 'Rating'])['UserID'].agg(pv=np.sum)
print(df_group.head(20))

print('*' * 15, '4. 重置index', '*' * 15)
df_reset = df_group.reset_index()
print(df_reset.head())

print('*' * 15, '5. 使用pivot简化透视', '*' * 15)
df_pivot = df_reset.pivot('pdate', 'Rating', 'pv')
print(df_pivot.head())

print('*' * 15, '6. 绘图', '*' * 15)
mapping(df_pivot)

pivot方法相当于对df使用set_index创建分层索引,然后调用unstack

20-4 stack,unstack,pivot的语法

stack

stack()是将原来的列索引转成了最内层的行索引,这里是多层次索引,其中AB索引对应第三层,即最内层索引。

DataFrame.stack(level=1, dropna=True), 将column变成index,类似把横放的书籍变成竖放

level=1代表多层索引的最内层,可以通过==0, 1, 2指定层索引的对应层

unstack

显然,unstack()是stack()的逆操作,这里把最内层的行索引还原成了列索引。但是unstack()中有一个参数可以指定旋转第几层索引,比如unstack(0)就是把第一层行索引转成列索引,但默认的是把最内层索引转层列索引。

DataFrame.unstack(level=-1, fill_value=None), 将index变成column,类似把竖放的书籍变成横放

pivot

DataFrame.pivot(index=None, columns=None, values=None), 指定index, cloumns, values实现二维透视

(21) Pandas 处理日期数据

pandas日期处理的作用,将2018-01-01、1/1/2018等多种日期格式映射成统一日期格式对象,在该对象上提供强大的功能支持

几个概念:

  • pd.to_datetime:pandas的一个函数,能将字符串、列表、series变成日期形式
  • Timestamp:pandas表示日期的对象形式
  • DatetimeIndex:pandas表示日期的对象列表形式

其中:

  • DatetimeIndex是Timestamp的列表形式
  • pd.to_datetime对单个日期字符串处理会得到Timestamp
  • pd.to_datetime对日期字符串列表处理会得到DatetimeIndex

![](../Python Web 框架/Django/CRM/images/demo_07.png)

问题:怎样统计每周、每月、每季度的最高温度?

21-1 读取天气数据到dataframe

import pandas as pd

print('*' * 15, '读取天气数据到dataframe', '*' * 15)
file_path = '../../datas/files/beijing_tianqi_2018.csv'
df = pd.read_csv(file_path)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')
print(df.head())

21-2 将日期列转换成pandas的日期

import pandas as pd

file_path = '../../datas/files/beijing_tianqi_2018.csv'
df = pd.read_csv(file_path)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

print('*' * 15, '将日期列转换成pandas的日期', '*' * 15)
df.set_index(pd.to_datetime(df["ymd"]), inplace=True)
print(df.head())

21-3 方便的对DatetimeIndex进行查询

import pandas as pd

file_path = '../../datas/files/beijing_tianqi_2018.csv'
df = pd.read_csv(file_path)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

df.set_index(pd.to_datetime(df["ymd"]), inplace=True)

print('*' * 15, '方便的对DatetimeIndex进行查询', '*' * 15)
# 筛选固定的某一天
print('*' * 10, '筛选固定的某一天', '*' * 10)
print(df.loc['2018-01-05'])

# 日期区间
print('*' * 10, '日期区间', '*' * 10)
print(df.loc['2018-01-05':'2018-01-10'])

# 按月份前缀筛选
print('*' * 10, '按月份前缀筛选', '*' * 10)
print(df.loc['2018-03'])

# 按月份前缀筛选
print('*' * 10, '按月份前缀筛选', '*' * 10)
print(df.loc["2018-07":"2018-09"].index)

# 按年份前缀筛选
print('*' * 10, '按年份前缀筛选', '*' * 10)
print(df.loc["2018"].head())

21-4 方便的获取周、月、季度

Timestamp、DatetimeIndex支持大量的属性可以获取日期分量:
https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#time-date-components

import pandas as pd

file_path = '../../datas/files/beijing_tianqi_2018.csv'
df = pd.read_csv(file_path)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

df.set_index(pd.to_datetime(df["ymd"]), inplace=True)

print('*' * 15, '方便的获取周、月、季度', '*' * 15)
# 周数字列表
print('*' * 10, '周数字列表', '*' * 10)
print(df.index.week)

# 月数字列表
print('*' * 10, '月数字列表', '*' * 10)
print(df.index.month)

# 季度数字列表
print('*' * 10, '季度数字列表', '*' * 10)
print(df.index.quarter)

21-5 统计每周、每月、每个季度的最高温度

import pandas as pd
from matplotlib import pyplot as plt


# 绘图函数
def mapping(datas):
    # 显示中文标签
    plt.plot(datas)
    plt.show()


file_path = '../../datas/files/beijing_tianqi_2018.csv'
df = pd.read_csv(file_path)
# 替换温度的后缀℃
df.loc[:, 'bWendu'] = df.loc[:, 'bWendu'].str.replace('℃', '').astype('int32')
df.loc[:, 'yWendu'] = df.loc[:, 'yWendu'].str.replace('℃', '').astype('int32')

df.set_index(pd.to_datetime(df["ymd"]), inplace=True)

# ========================================================== #
# 统计每周的数据
print('*' * 10, '统计每周的数据', '*' * 10)
print(df.groupby(df.index.week)["bWendu"].max().head())
print('*' * 10, '统计每周的数据-绘图', '*' * 10)
mapping(df.groupby(df.index.week)["bWendu"].max())


# ========================================================== #
# 统计每个月的数据
print('*' * 10, '统计每个月的数据', '*' * 10)
print(df.groupby(df.index.month)["bWendu"].max().head())
print('*' * 10, '统计每个月的数据-绘图', '*' * 10)
mapping(df.groupby(df.index.month)["bWendu"].max())


# ========================================================== #
# 统计每个季度的数据
print('*' * 10, '统计每个季度的数据', '*' * 10)
print(df.groupby(df.index.quarter)["bWendu"].max().head())
print('*' * 10, '统计每个季度的数据-绘图', '*' * 10)
mapping(df.groupby(df.index.quarter)["bWendu"].max())

(22) Pandas 处理日期索引的缺失

问题:按日期统计的数据,缺失了某天,导致数据不全该怎么补充日期?

可以用两种方法实现:

  1. DataFrame.reindex,调整dataframe的索引以适应新的索引
  2. DataFrame.resample,可以对时间序列重采样,支持补充缺失值

22-1 提出问题

问题:如果缺失了索引该怎么补充?

import pandas as pd
from matplotlib import pyplot as plt

# 绘图函数
def mapping(datas):
    plt.plot(datas)
    plt.legend(['pv', 'uv'], loc='upper left')
    plt.show()

print('*' * 15, '定义DataFrame', '*' * 15)
df = pd.DataFrame({
    'pdate': ['2019-12-01', '2019-12-02', '2019-12-04', '2019-12-05'],
    'pv': [100, 200, 400, 500],
    'uv': [10, 20, 40, 50]
})
print(df)

print('*' * 15, '绘图', '*' * 15)
mapping(df.set_index('pdate'))

问题这里缺失了2019-12-03的数据,导致数据不全该怎么补充

22-2 使用pandas.reindex方法

import pandas as pd
from matplotlib import pyplot as plt


# 绘图函数
def mapping(datas):
    plt.plot(datas)
    plt.legend(['pv', 'uv'], loc='upper left')
    plt.show()


df = pd.DataFrame({
    'pdate': ['2019-12-01', '2019-12-02', '2019-12-04', '2019-12-05'],
    'pv': [100, 200, 400, 500],
    'uv': [10, 20, 40, 50]
})

# ******************* 1. 将df的索引变成日期索引 ******************* #
print('*' * 15, '1. 将df的索引变成日期索引', '*' * 15)
df_date = df.set_index('pdate')
print(df_date)
print(df_date.index)

# 将df的索引设置为日期索引
print('*' * 10, '修改索引类型object->datetime64', '*' * 10)
df_date = df_date.set_index(pd.to_datetime(df_date.index))
print(df_date)
print(df_date.index)


# ******************* 2. 使用pandas.reindex填充缺失索引 ******************* #
print('*' * 15, '2. 使用pandas.reindex填充缺失索引', '*' * 15)
# 生成完整的日期序列
pdates = pd.date_range(start='2019-12-01', end='2019-12-05')
print(pdates)

# 重建索引
df_date_new = df_date.reindex(pdates, fill_value=0)
print(df_date_new)

print('*' * 15, '绘图', '*' * 15)
mapping(df_date_new)

22-3 使用pandas.resample方法

import pandas as pd
from matplotlib import pyplot as plt


# 绘图函数
def mapping(datas):
    plt.plot(datas)
    plt.xticks(rotation=45)
    plt.legend(['pv', 'uv'], loc='upper left')
    plt.show()


df = pd.DataFrame({
    'pdate': ['2019-12-01', '2019-12-02', '2019-12-04', '2019-12-05'],
    'pv': [100, 200, 400, 500],
    'uv': [10, 20, 40, 50]
})

# ******************* 1. 将df的索引变成日期索引 ******************* #
print('*' * 15, '1. 修改索引类型object->datetime64', '*' * 15)
df_new2 = df.set_index(pd.to_datetime(df['pdate'])).drop('pdate', axis=1)
print(df_new2)
print(df_new2.index)

# ******************* 2. 使用dataframe的resample的方法按照天重采样 ******************* #
print('*' * 15, '2. 使用dataframe的resample的方法按照天重采样', '*' * 15)
'''
resample的含义:
改变时间频率,比如把天数变成月份,或者打小时数据变成分钟级别

resample的语法:
(DataFrame or Series).resample(arguments).(aggregate function)

resample的采样规则参数:
https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases
'''
# 由于采样会让区间变成一个值,所以需要制定mean等采样值的设定方法
df_new2 = df_new2.resample('D').mean().fillna(0)
print(df_new2)
# resample的使用方式
print(df_new2.resample('2D').mean())

(23) Pandas Excel vlookup

背景:

  1. 有两个excel,他们有相同的一个列;
  2. 按照这个列合并成一个大的excel,即vlookup功能,要求:
    • 只需要第二个excel的少量的列,比如从40个列中挑选2个列
    • 新增的来自第二个excel的列需要放到第一个excel指定的列后面;
  3. 将结果输出到一个新的excel;

23-1 读取两个数据表

import pandas as pd

# 学生成绩表
print('*' * 15, '读取学生成绩表', '*' * 15)
df_grade = pd.read_excel("../datas/course_datas/c23_excel_vlookup/学生成绩表.xlsx")
print(df_grade.head())

# 学生信息表
print('*' * 15, '读取学生信息表', '*' * 15)
df_sinfo = pd.read_excel("../datas/course_datas/c23_excel_vlookup/学生信息表.xlsx")
print(df_sinfo.head())

目标:怎样将第二个“学生信息表”的姓名、性别两列,添加到第一个表“学生成绩表”,并且放在第一个表的“学号”列后面?

23-2 实现两个表的关联

即excel的vloopup功能

import pandas as pd

# 学生成绩表
print('*' * 15, '读取学生成绩表', '*' * 15)
df_grade = pd.read_excel("../datas/course_datas/c23_excel_vlookup/学生成绩表.xlsx")
print(df_grade.head())

# 学生信息表
print('*' * 15, '读取学生信息表', '*' * 15)
df_sinfo = pd.read_excel("../datas/course_datas/c23_excel_vlookup/学生信息表.xlsx")
print(df_sinfo.head())

# 只筛选第二个表的少量的列
print('*' * 15, '只筛选第二个表的少量的列', '*' * 15)
df_sinfo = df_sinfo[["学号", "姓名", "性别"]]
print(df_sinfo.head())

# 数据的关联
print('*' * 15, '数据的关联', '*' * 15)
df_merge = pd.merge(left=df_grade, right=df_sinfo, left_on="学号", right_on="学号")
print(df_merge.head())

23-3 调整列的顺序

import pandas as pd

# 学生成绩表
print('*' * 15, '读取学生成绩表', '*' * 15)
df_grade = pd.read_excel("../datas/course_datas/c23_excel_vlookup/学生成绩表.xlsx")
print(df_grade.head())

# 学生信息表
print('*' * 15, '读取学生信息表', '*' * 15)
df_sinfo = pd.read_excel("../datas/course_datas/c23_excel_vlookup/学生信息表.xlsx")
print(df_sinfo.head())

# 只筛选第二个表的少量的列
print('*' * 15, '只筛选第二个表的少量的列', '*' * 15)
df_sinfo = df_sinfo[["学号", "姓名", "性别"]]
print(df_sinfo.head())

# 数据的关联
print('*' * 15, '数据的关联', '*' * 15)
df_merge = pd.merge(left=df_grade, right=df_sinfo, left_on="学号", right_on="学号")
print(df_merge.head())

# 打印列名
print('*' * 15, '打印列名', '*' * 15)
print(df_merge.columns)
'''
问题:怎样将'姓名', '性别'两列,放到'学号'的后面?
接下来需要用Python的语法实现列表的处理
'''

# 将columns变成python的列表形式
print('*' * 15, '将columns变成python的列表形式', '*' * 15)
new_columns = df_merge.columns.to_list()
print(new_columns)

# 按逆序insert,会将"姓名","性别"放到"学号"的后面
print('*' * 15, '按逆序insert,会将"姓名","性别"放到"学号"的后面', '*' * 15)
for name in ["姓名", "性别"][::-1]:
    new_columns.remove(name)
    new_columns.insert(new_columns.index("学号") + 1, name)
print(new_columns)

# 调整索引
print('*' * 15, '调整索引', '*' * 15)
df_merge = df_merge.reindex(columns=new_columns)
print(df_merge.head())

23-4 输出最终的Excel文件

import pandas as pd

# 学生成绩表
print('*' * 15, '读取学生成绩表', '*' * 15)
df_grade = pd.read_excel("../datas/course_datas/c23_excel_vlookup/学生成绩表.xlsx")
print(df_grade.head())

# 学生信息表
print('*' * 15, '读取学生信息表', '*' * 15)
df_sinfo = pd.read_excel("../datas/course_datas/c23_excel_vlookup/学生信息表.xlsx")
print(df_sinfo.head())

# 只筛选第二个表的少量的列
print('*' * 15, '只筛选第二个表的少量的列', '*' * 15)
df_sinfo = df_sinfo[["学号", "姓名", "性别"]]
print(df_sinfo.head())

# 数据的关联
print('*' * 15, '数据的关联', '*' * 15)
df_merge = pd.merge(left=df_grade, right=df_sinfo, left_on="学号", right_on="学号")
print(df_merge.head())

# 打印列名
print('*' * 15, '打印列名', '*' * 15)
print(df_merge.columns)
'''
问题:怎样将'姓名', '性别'两列,放到'学号'的后面?
接下来需要用Python的语法实现列表的处理
'''

# 将columns变成python的列表形式
print('*' * 15, '将columns变成python的列表形式', '*' * 15)
new_columns = df_merge.columns.to_list()
print(new_columns)

# 按逆序insert,会将"姓名","性别"放到"学号"的后面
print('*' * 15, '按逆序insert,会将"姓名","性别"放到"学号"的后面', '*' * 15)
for name in ["姓名", "性别"][::-1]:
    new_columns.remove(name)
    new_columns.insert(new_columns.index("学号") + 1, name)
print(new_columns)

# 调整索引
print('*' * 15, '调整索引', '*' * 15)
df_merge = df_merge.reindex(columns=new_columns)
print(df_merge.head())

df_merge.to_excel("../datas/course_datas/c23_excel_vlookup/合并后的数据表.xlsx", index=False)

(24) Pandas Pyecharts绘制交互性折线图

背景:

  • Pandas是Python用于数据分析领域的超级牛的库
  • Echarts是百度开源的非常好用强大的可视化图表库,Pyecharts是它的Python库版本

24-1 读取数据

import pandas as pd

xlsx_path = "../datas/stocks/baidu_stocks.xlsx"
df = pd.read_excel(xlsx_path, index_col="datetime", parse_dates=True)
print(df.head())
print(df.index)
# 索引排序
df.sort_index(inplace=True)
print(df.head())

24-2 使用Pyecharts绘制折线图

import pandas as pd
from pyecharts.charts import Line
from pyecharts import options as opts

xlsx_path = "../datas/stocks/baidu_stocks.xlsx"
df = pd.read_excel(xlsx_path, index_col="datetime", parse_dates=True)
# 索引排序
df.sort_index(inplace=True)


# 折线图
line = Line()

# x轴
line.add_xaxis(df.index.to_list())

# 每个y轴
line.add_yaxis("开盘价", df["open"].round(2).to_list())
line.add_yaxis("收盘价", df["close"].round(2).to_list())

# 图表配置
line.set_global_opts(
    title_opts=opts.TitleOpts(title="百度股票2019年"),
    tooltip_opts=opts.TooltipOpts(trigger="axis", axis_pointer_type="cross")
)

# 渲染数据
line.render()

(25) Pandas 泰坦尼克存活率预测

实例目标:实现泰坦尼克存活预测

处理步骤:
1、输入数据:使用Pandas读取训练数据(历史数据,特点是已经知道了这个人最后有没有活下来)
2、训练模型:使用Sklearn训练模型
3、使用模型:对于一个新的不知道存活的人,预估他存活的概率

25-1 读取训练数据

import pandas as pd
df_train = pd.read_csv("../datas/titanic/titanic_train.csv")
print(df_train.head())

# 我们只挑选两列,作为预测需要的特征
feature_cols = ['Pclass', 'Parch']
X = df_train.loc[:, feature_cols]
print(X.head())

# 单独提取是否存活的列,作为预测的目标
y = df_train.Survived
print(y.head())

其中,Survived1代表这个人活下来了、0代表没活下来;其他的都是这个人的信息和当时的仓位、票务情况

25-2 训练模型

import pandas as pd
from sklearn.linear_model import LogisticRegression
df_train = pd.read_csv("../datas/titanic/titanic_train.csv")

# 我们只挑选两列,作为预测需要的特征
feature_cols = ['Pclass', 'Parch']
X = df_train.loc[:, feature_cols]

# 单独提取是否存活的列,作为预测的目标
y = df_train.Survived

# 创建模型对象
logreg = LogisticRegression()

# 实现模型训练
print(logreg.fit(X, y))

25-3 对于未知数据使用模型

机器学习的核心目标,是使用模型预测未知的事物

比如预测股票明天是涨还是跌、一套新的二手房成交价大概多少钱、用户打开APP最可能看那些视频等问题

import pandas as pd
from sklearn.linear_model import LogisticRegression
df_train = pd.read_csv("../datas/titanic/titanic_train.csv")

# 我们只挑选两列,作为预测需要的特征
feature_cols = ['Pclass', 'Parch']
X = df_train.loc[:, feature_cols]

# 单独提取是否存活的列,作为预测的目标
y = df_train.Survived

# 创建模型对象
logreg = LogisticRegression()

# 实现模型训练
print(logreg.fit(X, y))

# 找一个历史数据中不存在的数据
X.drop_duplicates().sort_values(by=["Pclass", "Parch"])

# 预测这个数据存活的概率
print(logreg.predict([[2, 4]]))

print(logreg.predict_proba([[2, 4]]))

第九章 Python 内置库

1、itertools

(1) 前戏

01 前言

很多人都致力于把Python代码写得更Pythonic,一来更符合规范且容易阅读,二来一般Pythonic的代码在执行上也更有效率。今天就先给大家介绍一下Python的系统库itertools。

02 itertools库

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

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

03 使用itertools

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

(2) accumulate

简单来说就是累加。

# 计算列表的总和
import itertools

x = itertools.accumulate(range(10))
print(list(x))

(3) chain

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

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

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

(4) combinations

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

import itertools

x = itertools.combinations(range(4), 3)
print(list(x))

(5) combinations_with_replacement

允许重复元素的组合

import itertools

x = itertools.combinations_with_replacement('ABC', 2)
print(list(x))

(6) compress

按照真值表筛选元素

import itertools

x = itertools.compress(range(5), (True, False, True, True, False))
print(list(x))

(7) count

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

import itertools

x = itertools.count(start=20, step=-1)
print(list(itertools.islice(x, 0, 10, 1)))

(8) cycle

循环指定的列表和迭代器

import itertools

x = itertools.cycle('ABC')
print(list(itertools.islice(x, 0, 10, 1)))

(9) dropwhile

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

import itertools

x = itertools.dropwhile(lambda e: e < 5, range(10))
print(list(x))

(10) filterfalse

保留对应真值为False的元素

import itertools

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

(11) groupby

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

import itertools

x = itertools.groupby(range(10), lambda x: x < 5 or x > 8)
for condition, numbers in x:
    print(condition, list(numbers))

(12) islice

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

import itertools

x = itertools.islice(range(10), 0, 9, 2)
print(list(x))

(13) permutations

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

import itertools

x = itertools.permutations(range(4), 3)
print(list(x))

(14) product

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

import itertools

x = itertools.product('ABC', range(3))
print(list(x))

(15) repeat

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

import itertools

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

(16) starmap

类似map

import itertools

x = itertools.starmap(str.islower, 'aBCDefGhI')
print(list(x))

(17) takewhile

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

import itertools

x = itertools.takewhile(lambda e: e < 5, range(10))
print(list(x))

(18) tee

生成指定数目的迭代器

import itertools

x = itertools.tee(range(10), 2)
for letters in x:
	print(list(letters))

(19) zip_longest

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

import itertools

x = itertools.zip_longest(range(3), range(5))
y = zip(range(3), range(5))
print(list(x))
print(list(y))

第九章 Python bitmap

https://blog.csdn.net/xc_zhou/article/details/110672513

https://mp.weixin.qq.com/s/Rr1WunLSkQivvH05oXLv5A

扩展

程序速度

"""
执行时间对比
"""
import time
import asyncio
from random import randint
import threading
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor


# ---------------------------- 单进程 --------------------------
def task():
    time.sleep(1.5)
    return randint(1, 5)


def main():
    start_time = time.time()
    tasks = [task() for _ in range(10)]
    print(f'本次程序执行结果: {tasks}')
    print(f'单进程 - 本次执行总耗时为: {"%.10f" % (time.time() - start_time)} 秒\n')


# ----------------------------- 线程池 -------------------------
thread_pool = ThreadPoolExecutor(5)


def thread_pool_task():
    time.sleep(1.5)
    return randint(1, 5)


def thread_pool_main():
    start_time = time.time()
    tasks = [thread_pool.submit(thread_pool_task) for _ in range(10)]
    print(f'本次程序执行结果: {[res.result() for res in tasks]}')
    print(f'线程池 - 本次执行总耗时为: {"%.10f" % (time.time() - start_time)} 秒\n')


# ---------------------------- 进程池 -------------------------
process_pool = ProcessPoolExecutor(10)


def process_pool_task():
    time.sleep(1.5)
    return randint(1, 5)


def process_pool_main():
    start_time = time.time()
    tasks = [process_pool.submit(process_pool_task) for _ in range(10)]
    print(f'本次程序执行结果: {[res.result() for res in tasks]}')
    print(f'进程池 - 本次执行总耗时为: {"%.10f" % (time.time() - start_time)} 秒\n')


# ---------------------------- 异步 --------------------------
async def async_task():
    await asyncio.sleep(1.5)
    return randint(1, 5)


async def async_main():
    start_time = time.time()
    tasks = [asyncio.create_task(async_task()) for _ in range(10)]
    await asyncio.wait(tasks)
    print(f'本次程序执行结果: {[res.result() for res in tasks]}')
    print(f'异步 - 本次执行总耗时为: {"%.10f" % (time.time() - start_time)} 秒\n')


# --------------------------- 子线程 + 线程池 ---------------
def test(*args, **kwargs):
    print(args)
    start_time = time.time()
    tasks = [thread_pool.submit(thread_pool_task) for _ in range(10)]
    time.sleep(1.5)


def test_main():
    t = threading.Thread(target=test, args=(11,))
    t.start()


if __name__ == '__main__':
    test_main()
    main()
    thread_pool_main()
    process_pool_main()
    asyncio.run(async_main())
    time.sleep(1000)

内存溢出

tracemalloc

import gc
import base64

import cv2
import numpy as np
from flask import request
from flask.views import MethodView
from utils.tools import response
from flask import Flask
from insightface.app import FaceAnalysis
import tracemalloc

face_detector_app = FaceAnalysis(allowed_modules=['detection', ])
face_detector_app.prepare(ctx_id=0, det_thresh=0.4, det_size=(160, 160))
app = Flask(__name__)


# 图像处理相关
class ImageProcess:
    # base64 转 opencv
    def base64_to_opencv(self, img_b64decode):
        # base64解码
        img_data = base64.b64decode(img_b64decode)
        # 转换为np数组
        img_array = np.frombuffer(img_data, np.uint8)
        # 转换成opencv可用格式
        img = cv2.imdecode(img_array, cv2.COLOR_RGB2BGR)
        img.clone()
        return img

    # opencv 转 base64
    def opencv_to_base64(self, img):
        image = cv2.imencode('.jpg', img)[1]
        base64_img = base64.b64encode(image)
        return base64_img

    # 修改图片大小
    def opencv_img_resize(self, img, resize_magnification):
        if img.shape[0] >= 128 or img.shape[1] >= 128:
            resized_img = img
        else:
            dsize = int(img.shape[1] * resize_magnification), int(img.shape[0] * resize_magnification)
            resized_img = cv2.resize(img, dsize, cv2.INTER_NEAREST)
            resized_img.clone()
        return resized_img

    # 裁剪
    def cropp_face_img(self, img, bbox, area_magnification=1):
        # cropp_bbox = None
        ox2 = img.shape[1]
        oy2 = img.shape[0]
        if area_magnification != 0:
            tx1 = bbox[0]
            tx2 = bbox[2]
            ty1 = bbox[1]
            ty2 = bbox[3]
            x1 = tx2 - int((tx2 - tx1) * area_magnification)
            x2 = tx1 + int((tx2 - tx1) * area_magnification)
            y1 = ty2 - int((ty2 - ty1) * area_magnification)
            y2 = ty1 + int((ty2 - ty1) * area_magnification)
            if x1 < 0:
                x1 = 0
            if x2 > ox2:
                x2 = ox2
            if y1 < 0:
                y1 = 0
            if y2 > oy2:
                y2 = oy2
            cropp_bbox = [x1, y1, x2, y2]
        else:
            cropp_bbox = bbox
        cropped = img[cropp_bbox[1]:cropp_bbox[3], cropp_bbox[0]:cropp_bbox[2]]
        return cropped

    # 均衡化处理
    def change_img_clahe(self, img):
        # 图像通道分割
        (b, g, r) = cv2.split(img)
        # 建立均衡化实例
        clahe = cv2.createCLAHE(clipLimit=0.5, tileGridSize=(4, 4))
        bb = clahe.apply(b)
        gg = clahe.apply(g)
        rr = clahe.apply(r)
        # 图像通道合并
        result = cv2.merge((bb, gg, rr))
        width, height = img.shape[:2][::-1]
        img_resize = cv2.resize(img, (int(width * 1.0), int(height * 1.0)), interpolation=cv2.INTER_CUBIC)
        img2gray = cv2.cvtColor(img_resize, cv2.COLOR_BGR2GRAY)
        imageVar = cv2.Laplacian(img2gray, cv2.CV_64F).var()
        img_resize.clone()
        return result, imageVar


image_process = ImageProcess()


# 人脸检测
class FaceDetector(MethodView):
    methods = ['POST']

    def post(self):
        # 1. 获取请求参数
        try:
            form_data = {
                'cropp': 1,
                'area_magnification': 1.2,
                'face_image_resize': 0,
                'resize_magnification': 1.1,
                'change_img_alpha': 1,
                'data': None,
                'count': 0
            }
            form_data.update(request.get_json())
            form_data['face_image_resize'] = 0
            # 2. base64 转 opencv
            img = image_process.base64_to_opencv(form_data.get('data'))
            # 3. 人脸检测
            faces = []
            change_img_alpha_count = 0
            refaces = face_detector_app.get(img)
            if len(refaces) > 0:
                for i in range(len(refaces)):
                    tface = {
                        'faceId': i + 1,
                        'bbox': refaces[i]['bbox'].astype(np.int16).tolist(),
                        'score': (refaces[i]['det_score'] * 100).astype(np.int8).tolist()
                    }
                    if form_data.get('cropp') == 1 and form_data.get('area_magnification'):
                        cropped_img = image_process.cropp_face_img(img, tface['bbox'],
                                                                   form_data.get('area_magnification'))
                        # 对裁剪的人脸图片进行放大
                        if form_data.get('face_image_resize') == 1 and form_data.get('resize_magnification') != 1:
                            # 对裁剪的人脸图片进行优化
                            if form_data.get('change_img_alpha') == 1:
                                resize_img = image_process.opencv_img_resize(cropped_img,
                                                                             form_data.get('resize_magnification'))
                                change_img_clahe_img, imageVar = image_process.change_img_clahe(resize_img)
                                base64_img = image_process.opencv_to_base64(change_img_clahe_img)
                                change_img_alpha_count += 1
                            else:
                                resize_img = image_process.opencv_img_resize(cropped_img,
                                                                             form_data.get('resize_magnification'))
                                base64_img = image_process.opencv_to_base64(resize_img)
                        # 对裁剪的人脸图片使用原图
                        else:
                            # 对裁剪的人脸图片进行优化
                            if form_data.get('change_img_alpha') == 1:
                                resize_img = image_process.opencv_img_resize(cropped_img,
                                                                             form_data.get('resize_magnification'))
                                change_img_clahe_img, imageVar = image_process.change_img_clahe(resize_img)
                                change_img_alpha_count += 1
                                base64_img = image_process.opencv_to_base64(change_img_clahe_img)
                            else:
                                base64_img = image_process.opencv_to_base64(cropped_img)
                        if base64_img != None:
                            tface.update({'faceBase64': str(base64_img, encoding='utf-8')})
                    faces.append(tface)

            # snapshot2 = tracemalloc.take_snapshot()
            # top_stats = snapshot2.compare_to(snapshot1, 'lineno')
            # for top_stat in str(top_stats).split(', '):
            #     if "filename='app.py'" in top_stat:
            #         print(top_stat)
            return response.success(data=faces)
        except:
            return response.error(msg='服务器异常')


app.add_url_rule('/api/face_detector', view_func=FaceDetector.as_view(name='face_detector'))

# tracemalloc.start()
# snapshot1 = tracemalloc.take_snapshot()

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=11000, debug=False)

垃圾回收机制

1、白话垃圾回收

用通俗的语言解释内存管理和垃圾回收的过程,搞懂这一部分就可以去面试、去装逼了…

(1) 大管家 refchain

在Python的C源码中有一个名为refchain的环状双向链表,这个链表比较牛逼了,因为Python程序中一旦创建对象都会把这个对象添加到refchain这个链表中。也就是说他保存着所有的对象。例如:

age = 18
name = "武沛齐"

(2) 引用计数器

在refchain中的所有对象内部都有一个ob_refcnt用来保存当前对象的引用计数器,顾名思义就是自己被引用的次数,例如:

age = 18
name = "武沛齐"
nickname = name

上述代码表示内存中有 18 和 “武沛齐” 两个值,他们的引用计数器分别为:1、2 。

当值被多次引用时候,不会在内存中重复创建数据,而是 引用计数器+1 。 当对象被销毁时候同时会让 引用计数器-1,如果引用计数器为0,则将对象从refchain链表中摘除,同时在内存中进行销毁(暂不考虑缓存等特殊情况)。

age = 18
number = age  	 # 对象18的引用计数器 + 1
del age          # 对象18的引用计数器 - 1
def run(arg):
    print(arg)
run(number)   	 # 刚开始执行函数时,对象18引用计数器 + 1,当函数执行完毕之后,对象18引用计数器 - 1 。
num_list = [11,22,number] # 对象18的引用计数器 + 1

(3) 标记清除&分代回收

基于引用计数器进行垃圾回收非常方便和简单,但他还是存在 循环引用 的问题,导致无法正常的回收一些数据,例如:

v1 = [11,22,33]	# refchain中创建一个列表对象,由于v1=对象,所以列表引对象用计数器为1.
v2 = [44,55,66]	# refchain中再创建一个列表对象,因v2=对象,所以列表对象引用计数器为1.
v1.append(v2)	# 把v2追加到v1中,则v2对应的[44,55,66]对象的引用计数器加1,最终为2.
v2.append(v1)	# 把v1追加到v1中,则v1对应的[11,22,33]对象的引用计数器加1,最终为2.
del v1			# 引用计数器-1
del v2			# 引用计数器-1

对于上述代码会发现,执行 del 操作之后,没有变量再会去使用那两个列表对象,但由于循环引用的问题,他们的引用计数器不为0,所以他们的状态:永远不会被使用、也不会被销毁。项目中如果这种代码太多,就会导致内存一直被消耗,直到内存被耗尽,程序崩溃。

为了解决循环引用的问题,引入了 标记清除 技术,专门针对那些可能存在循环引用的对象进行特殊处理,可能存在循环应用的类型有:列表、元组、字典、集合、自定义类等那些能进行数据嵌套的类型。

标记清除:创建特殊链表专门用于保存 列表、元组、字典、集合、自定义类等对象,之后再去检查这个链表中的对象是否存在循环引用,如果存在则让双方的引用计数器均 - 1 。

分代回收:对标记清除中的链表进行优化,将那些可能存在循引用的对象拆分到3个链表,链表称为:0/1/2三代,每代都可以存储对象和阈值,当达到阈值时,就会对相应的链表中的每个对象做一次扫描,除循环引用各自减1并且销毁引用计数器为0的对象。

// 分代的C源码
#define NUM_GENERATIONS 3
struct gc_generation generations[NUM_GENERATIONS] = {
    /* PyGC_Head, threshold, count */
    {{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)}, 700, 0}, // 0代
    {{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)}, 10, 0},  // 1代
    {{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)}, 10, 0},  // 2代
};

特别注意:0代和1、2代的threshold和count表示的意义不同。

  • 0代,count表示0代链表中对象的数量,threshold表示0代链表对象个数阈值,超过则执行一次0代扫描检查。
  • 1代,count表示0代链表扫描的次数,threshold表示0代链表扫描的次数阈值,超过则执行一次1代扫描检查。
  • 2代,count表示1代链表扫描的次数,threshold表示1代链表扫描的次数阈值,超过则执行一2代扫描检查。

(4) 情景模拟

根据C语言底层并结合图来讲解内存管理和垃圾回收的详细过程。

第一步:当创建对象 age=19 时,会将对象添加到refchain链表中。

第二步:当创建对象 num_list = [11,22] 时,会将列表对象添加到 refchain 和 generations 0代中。

第三步:新创建对象使generations的0代链表上的对象数量大于阈值700时,要对链表上的对象进行扫描检查。

当0代大于阈值后,底层不是直接扫描0代,而是先判断2、1是否也超过了阈值。

  • 如果2、1代未达到阈值,则扫描0代,并让1代的 count + 1 。
  • 如果2代已达到阈值,则将2、1、0三个链表拼接起来进行全扫描,并将2、1、0代的count重置为0.
  • 如果1代已达到阈值,则讲1、0两个链表拼接起来进行扫描,并将所有1、0代的count重置为0.

对拼接起来的链表在进行扫描时,主要就是剔除循环引用和销毁垃圾,详细过程为:

  • 扫描链表,把每个对象的引用计数器拷贝一份并保存到 gc_refs中,保护原引用计数器。
  • 再次扫描链表中的每个对象,并检查是否存在循环引用,如果存在则让各自的gc_refs减 1 。
  • 再次扫描链表,将 gc_refs 为 0 的对象移动到 unreachable 链表中;不为0的对象直接升级到下一代链表中。
  • 处理 unreachable 链表中的对象的 析构函数 和 弱引用,不能被销毁的对象升级到下一代链表,能销毁的保留在此链表。
    • 析构函数,指的就是那些定义了del方法的对象,需要执行之后再进行销毁处理。
    • 弱引用,
  • 最后将 unreachable 中的每个对象销毁并在refchain链表中移除(不考虑缓存机制)。

至此,垃圾回收的过程结束。

(5) 缓存机制

从上文大家可以了解到当对象的引用计数器为0时,就会被销毁并释放内存。而实际上他不是这么的简单粗暴,因为反复的创建和销毁会使程序的执行效率变低。Python中引入了“缓存机制”机制。
例如:引用计数器为0时,不会真正销毁对象,而是将他放到一个名为 free_list 的链表中,之后会再创建对象时不会在重新开辟内存,而是在free_list中将之前的对象来并重置内部的值来使用。

2、定位内存泄露

from collections import defaultdict
from gc import get_objects

before = defaultdict(int)
after = defaultdict(int)

# 程序(函数/类...)开始
for i in get_objects():
    before[type(i)] += 1

# 代码块
"""
"""

# 程序(函数/类...)结束
for i in get_objects():
    after[type(i)] += 1
print([(k, after[k] - before[k]) for k in after if (after[k] - before[k]) > 0])
import gc
import insightface
import numpy as np
from utils.tools import image_process, response
from flask import Flask, request
from collections import defaultdict
from gc import get_objects

before = defaultdict(int)
after = defaultdict(int)

face_detector_app = insightface.app.FaceAnalysis(allowed_modules=['detection', ])
face_detector_app.prepare(ctx_id=0, det_thresh=0.4, det_size=(160, 160))
app = Flask(__name__)


@app.route("/api/face_detector", methods=["post"])
def detection():
    response_data = {'code': 200, 'msg': 'success', 'data': None}
    try:
        request_data = request.get_json()
        area_magnification = request_data.get('area_magnification')
        resize_magnification = request_data.get('resize_magnification')
        img = request_data.get('img')
        
        # 程序(函数/类...)开始
        for i in get_objects():
            before[type(i)] += 1
            
        # 1. base64 转 opencv
        opencv_img = image_process.base64_to_opencv(img)
        # 2. 人脸检测
        refaces = face_detector_app.get(opencv_img)
        if refaces:
            reface = refaces[0]
            bbox = reface['bbox'].astype(np.int16).tolist()
            score = (reface['det_score'] * 100).astype(np.int8).tolist()
            cropped_img = image_process.cropp_face_img(opencv_img, bbox, area_magnification)
            resize_img = image_process.opencv_img_resize(cropped_img, resize_magnification)
            change_img_clahe_img = image_process.change_img_clahe(resize_img)
            base64_img = image_process.opencv_to_base64(change_img_clahe_img)
            response_data['data'] = {
                'faceId': 1,
                'bbox': bbox,
                'score': score,
                'faceBase64': str(base64_img, encoding='utf-8') if base64_img else None
            }
		# 程序(函数/类...)结束
        for i in get_objects():
            after[type(i)] += 1
        print([(k, after[k] - before[k]) for k in after if (after[k] - before[k]) > 0])
    except Exception as e:
        print(f'人脸检测异常: {e}')
    return response.res(response_data)


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=11000, debug=False, threaded=False)

3、解决内存泄露

如果内存持续增长定位到后可以使用 gc 强制回收

注意:不要瞎几把删除变量,没啥用(找出关键变量)一般不会出现这种情况,基本上都是第三方库导致的,也是和业务有关系的

具体情况具体分析

找到了就把定位的代码删除掉(注释掉),留着内存就还会增长

import gc
import insightface
import numpy as np
from utils.tools import image_process, response
from flask import Flask, request

face_detector_app = insightface.app.FaceAnalysis(allowed_modules=['detection', ])
face_detector_app.prepare(ctx_id=0, det_thresh=0.4, det_size=(160, 160))
app = Flask(__name__)


@app.route("/api/face_detector", methods=["post"])
def detection():
    response_data = {'code': 200, 'msg': 'success', 'data': None}
    try:
        request_data = request.get_json()
        area_magnification = request_data.get('area_magnification')
        resize_magnification = request_data.get('resize_magnification')
        img = request_data.get('img')
        # 1. base64 转 opencv
        opencv_img = image_process.base64_to_opencv(img)
        # 2. 人脸检测
        refaces = face_detector_app.get(opencv_img)
        if refaces:
            reface = refaces[0]
            bbox = reface['bbox'].astype(np.int16).tolist()
            score = (reface['det_score'] * 100).astype(np.int8).tolist()
            cropped_img = image_process.cropp_face_img(opencv_img, bbox, area_magnification)
            resize_img = image_process.opencv_img_resize(cropped_img, resize_magnification)
            change_img_clahe_img = image_process.change_img_clahe(resize_img)
            base64_img = image_process.opencv_to_base64(change_img_clahe_img)
            response_data['data'] = {
                'faceId': 1,
                'bbox': bbox,
                'score': score,
                'faceBase64': str(base64_img, encoding='utf-8') if base64_img else None
            }
            del reface
        del refaces
        gc.collect()
    except Exception as e:
        print(f'人脸检测异常: {e}')
    return response.res(response_data)


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=11000, debug=False, threaded=False)
posted @ 2023-05-30 17:23  菜鸟程序员_python  阅读(510)  评论(0)    收藏  举报