Skip to Content

Python 学习笔记

迭代器(Iterator):

能够被 next()函数调用并不断返回下一个值,直到抛出 StopIteration 异常的,就是迭代器对象。

可迭代对象(Iterable):

可迭代对象能够被 for 循环遍历,可迭代对象都有迭代器,可以通过 iter 获取可迭代对象的迭代器,并通过 next()函数取出迭代器的值。

可以通过 from collections import Iterable,Iterator导入可迭代对象类和迭代器类,来判断对象是否迭代器,或者可迭代对象。

生成器(Generator):

生成器是特殊的迭代器

创建生成器的方法:

  1. 通过类似列表生成器的语法: (i for i range(100)),这样可以创建一个值为 0~99的生成器,并可以通过不断的迭代取出每一个值
  2. 通过类似函数的语法,使用 yield 返回值,就是一个生成器函数,语法如下:

def mygenerator():

a = 0

b = 1

while True:

a, b = b, a+b

yield a

生成器的操作:

  1. 使用 next()可以取出生成器的元素
  2. 使用 for 循环可以遍历输出生成器的每个元素
  3. 使用 list()可以将生成器转换成列表
  4. 使用 send()唤醒生成器,示例:
    1. def gen():

      i = 0

      while i < 5:

      temp = yield i

      print(temp)

      i += 1

      # send 的使用:↓

      In [43]: f = gen()

      In [44]: next(f)

      Out[44]: 0

      In [45]: f.send('haha')

      haha

      Out[45]: 1

      In [46]: next(f)

      None

      Out[46]: 2

      In [47]: f.send('haha')

      haha

      Out[47]: 3


  5. 使用__next__方法(不常用)

闭包(Closure):

什么是闭包?

在一个函数内部定义一个函数(称为内函数),内函数引用外部函数的变量或参数,这个内函数及引用到的变量和参数,就称为一个闭包。

def test(number): # 外函数

def inner(number_in): # 在外函数中定义一个内函数

print ("%s from inner" % number_in)

return number + number_in # 内函数引用了外函数的参数

return inner # 将内函数返回


def line_conf(a, b): # 定义外函数

def line(x): # 定义内函数

return a*x + b # 引用外函数参数


line1 = line_conf(1, 1) # x+1

line2 = line_conf(4, 5) # 4x+5

print(line1(5))

print(line2(5))

闭包思考...
  1. 闭包优化了变量,原本需要类对象完成的工作,闭包也可以完成
  2. 闭包引用了外部的局部变量,所以外部的局部变量无法及时释放,消耗内存
在闭包中修改外部函数的变量

def counter(start=0):

count = [start]

def incr():

nonlocal start # Python3中使用,声明该变量为nonlocal,非本地变量

count[0] += 1 # Python2中使用,利用可变类型的引用,来修改外部变量

start += 1

return count[0] # Python2方法

return start # Python3方法

return incr

装饰器(decorator):

  1. 装饰器是基于闭包来实现的,它是 Python 的一个语法糖。
  2. 定义一个函数(外函数),在函数中定义一个函数(内函数),外函数接收一个参数(是函数),在内函数中实现要新增的功能,内函数中必须调用传来的函数(外函数参数),从而实现对原函数功能的扩展。
什么时候使用?

当你需要扩展有一个函数的功能,却又不影响原本功能的情况下,使用装饰器是一个不错的选择。

示例

def log(func): # 定义外函数,接收一个参数(函数)

def wrapper(*args, **kwargs): # 定义内函数

print 'call %s()' % func.__name__ # 拓展的功能

return func(*args, **kw) # 调用传来的函数(在外函数接收)

return wrapper # 返回内函数



@log # 执行流程>> 1.先调用log函数,将now作为参数2.定义并返回内函数3.赋值给now,下次调用时相当于调用返回的内函数

def now():

print '2017-10-14'


# 调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:

IN>> now() # 执行流程>> 1.相当于执行了wrapper2.在wrapper中执行扩展的功能3.调用原本的函数

OUT>> call now()

2017-10-14

如果装饰器需要参数

def log(text):

def decorator(func):

def wrapper(*args, **kw):

print '%s %s():' % (text, func.__name__)

return func(*args, **kw)

return wrapper

return decorator


@log('execute')

#执行流程

#1.调用函数log('execute')

#2.返回函数decorator,此处变成@decorator

#3.将now传给decorator函数并调用

#4.返回wrapper函数

#5.赋值给now

def now():

print '2017-10-14'

类装饰器

class MyDecorator(object):

def __init__(self, func):

print("初始化")

self.func = func # 初始化时将传进的函数保存


def __call__(self, *args, **kwargs): # 再次调用test时,相当于调用实例化后的MyDecorator的对象,调用对象时,就会调用这个方法,在这个方法实现装饰器的功能即可

print("拓展功能开始")

self.func()

print("拓展功能结束")


@MyDecorator # 调用类并将test函数传入,相当于实例化MyDecorator,实例化后赋值给test

def test():

print ("---test---")

消除装饰器对文档的副作用

使用装饰器,对文档说明会有一些副作用,旧函数的文档会被覆盖,变成内函数的文档,下面的方法可以解决这个问题

import functools

def note(func):

"外函数"

@functools.wraps(func) # 通过functools模块的wraps方法来消除

def inner():

"内函数"

print("内函数")

return func()

return inner


@note

def test():

"测试函数"

print("这是测试函数")

应用场景

  1. 引入日志
  2. 函数执行时间计算
  3. 函数执行前处理
  4. 函数执行后清理
  5. 权限校验场景
  6. 缓存

多层装饰器执行顺序: 先调用内层装饰器

细节(Details):

作用域
  1. locals()函数会返回局部的命名空间
  2. globals()函数会返回全局的命名空间

LEGB 规则(locals -> enclosing function -> globals -> builtins)

在 Python 中,变量的查找顺序遵循 LEGB 规则,首先从 locals 局部变量开始查找,再从外部嵌套函数中查找,再从全局变量中找,最后从Python 的内建变量中查找。

==和 is
  1. ==判断的是值
  2. is 判断的是地址(id)
可变类型和不可变类型
不可变类型:
1. 值一旦创建就不能被改变,任何“修改”实际都是创建了新的对象。
2. 常见类型:int、float、bool、str、tuple、frozenset、bytes、NoneType

可变类型:
1. 值可以直接修改,而不是创建新对象。
2. 常见类型:list、dict、set、bytearray、collections.deque


深拷贝和浅拷贝
对于可变类型:
浅拷贝:
只拷贝一层,地址不同
深拷贝: 完整拷贝,地址不同

对于[1,2,3],浅拷贝和深拷贝都能拷贝,且拷贝后地址都不同。
对于[1,2,3,[6,6,6]],浅拷贝拷贝了第一层,内部的[6,6,6]只拷贝了引用;深拷贝进行了完整拷贝,内部的[6,6,6]也被重新创建了一份。

对于不可变类型:

浅拷贝:只拷贝引用,地址相同

深拷贝:只拷贝引用,地址相同

特殊情况:(1,2,3,[6,6,6])

浅拷贝: 只拷贝引用,地址相同

深拷贝: 完整拷贝,地址不同

私有化

Python 中的变量

  1. xx:  普通变量
  2. _x:  一个前置下划线,私有属性或方法,在 from xx import * 的时候不会导入,类对象和子类可以访问
  3. __xx:  两个前置下划线,避免与子类中的属性命名冲突,无法在外部直接访问(因为 Python 的名字重整(name mangling),子类不会继承
  4. __xx__: 两个前后下划线,魔法对象或属性。如:__init__就是一个魔法方法,开发时不自定义这种方法
  5. xx__: 两个后置下划线,用于避免与 python 关键字冲突

细节:其实__xx只是修改了变量名,并不是真正的访问不到,通过_Class__attr就可以访问

property

示例

class Money(object):

def __init__(self):

self.__money = 0

def getMoney(self):

return self.__money

def setMoney(self, value):

if isinstance(value, int):

self.__money = value

else:

print("error:不是整形数字")

money = property(getMoney, setMoney)

# 定义一个属性,当对money进行设置时调用setMoney,取值时调用getMoney方法

装饰器版本

class Money(object):

def __init__(self):

self.__money = 0

@property

def money(self):

#使用装饰器对money进行装饰,会自动添加一个名为money的属性,当对money进行取值时,会调用这个方法

return self.__money

@money.setter

def money(self, value):

#使用装饰器对money进行装饰,当对money设置值时,会调用这个方法

if isinstance(value, int):

self.__money = value

else:

print("error:不是整形数字")

垃圾回收(gc)

小整数对象池

小整数对象池就是在内存中,将-5~256 提前存储好,在一个 python 程序中,所有位于这个范围内的整数都是使用同一个对象。​

intern 机制
靠引用计数维护说占用内存空间
如“Hello World”,定义 1000 遍,都是引用同一个地址
如果引用计数为 0,则释放

其他
单个英文字母也是常驻内存的

Python 的垃圾回收策略

引用计数为主,标记-清除,分代收集机制为辅的策略

  • 引用计数
    • 一旦对象的引用计数为0,Python垃圾回收器会立刻启动,将内存还给操作系统。
    • 缺点:
      • 1 ) 增加了对内存空间的占用
      • 2 ) 速度相对较慢
      • 3)无法处理环形数据结构,也就是含有循环引用的数据结构
  • 标记-清除
    • 标记阶段。将所有的对象看成图的节点,根据对象的引用关系构造图结构。从图的根节点遍历所有的对象,所有访问到的对象被打上标记,表明对象是“可达”的。
    • 清除阶段。遍历所有对象,如果发现某个对象没有标记为“可达”,则就回收。
  • 分代收集
    • 对象刚创建时为G0。
    • 如果在一轮GC扫描中存活下来,则移至G1,处于G1的对象被扫描次数会减少。
    • 如果再次在扫描中活下来,则进入G2,处于G2的对象被扫描次数将会更少。
    • 零代链表
    • 一代链表
    • 二代链表

当某世代中分配的对象数量与被释放的对象之差达到某个阈值的时,将触发对该代的扫描。当某世代触发扫描时,比该世代年轻的世代也会触发扫描。

多进程

程序

编写完毕的代码,编译后的代码,在没有运行的时候,就称为程序。

进程

1 ) 正在运行的程序,就称为进程,进程是操作系统分配资源的基本单位

2 ) 进程除了包含代码以外,还需要有运行的环境


fork()

1 ) 只在Unix/Linux操作系统中可以使用

2 ) 创建一个子进程,复制父进程中的所有信息

3 ) 子进程永远返回0, 父进程返回子进程的id

4 ) 子进程可以通过getppid()获取父进程的id


multiprocessing()

进程间通信需要from multiprocessing import Queue

进程池的Queue在multiprocessing.Manager当中


pool()

apply_async 非阻塞式

apply 阻塞式


同步异步

同步就是在发出一个调用时,在没有得到结果之前,该调用就不返回

异步就是在调用发出之后,这个调用就直接返回了,不管有没有结果


阻塞非阻塞

与同步异步的区别: 同步异步关注的是消息通信的机制阻塞非阻塞关注的是程序在等待调用结果时的状态

阻塞:调用结果返回之前,当前线程会被挂起

非阻塞:就是调用未得到结果之前,该调用不会阻塞当前线程


区分两个维度

分类维度

解释

同步 / 异步是谁来“继续后续操作”?
同步:你自己继续执行(像调用函数)
异步:操作系统/框架帮你回调
阻塞 / 非阻塞这个操作会不会“让你卡住”?
阻塞:会停下来等
非阻塞:不会停,先做别的去


多线程

一个程序至少有一个进程,一个进程至少有一个线程

对比

进程

1 ) 拥有独立的内存单元

2 ) 操作系统进行资源分配和调度的独立单位

3 ) 切换时耗费资源大

4 ) 需要的资源相对线程较多

5 ) 开销大,但利于资源管理和保护


线程

1 ) 线程必须依赖于进程运行,无法独立运行

2 ) CPU调度和分派的基本单位

3 ) 只拥有一点在运行中必不可少的资源(程序计数器,一组寄存器,栈)

4 ) 与同属一个进程的其他线程共享进程资源

5 ) 开销小,但不利于资源管理和保护


主线程结束不会造成子线程结束(因为进程还未结束)

线程共享全局变量,但会导致全局变量数据的混乱(线程非安全)


互斥锁

使用threading中的Lock类

好处:确保了某段代码只能由一个线程从头到尾完整地执行

坏处:阻止了多线程并发执行, 包含锁的某段代码实际上只能以单线程模式执行

​ 多个锁的情况可能造成死锁

栈:先进后出

队列:先进先出

传递变量

使用全局字典

  • 使用当前线程作为键(current_thread())

ThreadLocal

  • 通过threading.local()创建ThreadLocal对象

import threading

local_variable = threading.local()


def process_student():

std = local_variable.student

print("%s, in %s" % (std, threading.current_thread().name))


def process_thread(name):

local_variable.student = name

process_student()


t1 = threading.Thread(target=process_thread, args=("小明"), name="A")

t2 = threading.Thread(target=process_thread, args=("小蓝"), name="B")

t1.start()

t2.start()

t1.join()

t2.join()


应用场景

为每一个线程绑定一个数据库连接,HTTP请求,用户身份信息等

ThreadLocal虽然是全局变量,但每一个线程都只能读写自己线程的独立副本,互不干扰。

 生产者消费者模式

  • 生产者生产产品,并向仓库(queue)添加产品(消息)
  • 消费者消费产品,并从仓库(queue)取出产品(消息)
  • 只要仓库没满,生产者就可以继续生产,如果满了,生产者等待
  • 只要仓库没空,消费者就可以取出产品消费,如果空了,消费者等待

为什么要使用?

当生产者和消费者的处理速度差距较大,就会有一方需要等待,为了解决这个问题,引入了生产者和消费者模式。

什么是生产者消费者模式?

通过一个容器来解决生产者和消费者的强耦合问题,生产者和消费者之间不直接通讯,通过阻塞队列来进行通讯,所有生产者生产完数据后不用等待消费者处理,直接放进队列,消费者也不需要找生产者要数据,而是直接从阻塞队列里取。阻塞队列就相当于一个缓存区,平衡了生产者和消费者的处理能力。

这个阻塞队列就是用来给消费者和生产者解耦的

全局解释器锁(Global Interpreter Lock)

GIL是Cpython解释器设计遗留下的历史问题。

任何线程在运行之前必须获取GIL,每当执行完100条字节码,或者遇上IO操作,GIL就会释放,切换到其他线程执行。

这样虽然保证了多线程访问公共资源的安全性,但是GIL使得Python中的多线程不能充分利用多核计算机的优势,无论有多少个核,只有一个GIL,同一时间只有一个线程获得GIL,也就是只有一个线程能够运行。

解决方式:

  1. 使用多进程,每个进程有一个GIL,互不影响。
  2. 通过C扩展实现。(C 扩展中释放 GIL 的操作(如 numpy))
  3. 其他解释器

Python 官方接受 PEP 703:计划在 3.13 支持 “可选禁用 GIL” 

网络(NetWork)

网络协议
  • OSI参考模型 七层
  • TCP/IP协议 四层
    • 示意
      层级名称主要功能举例
      7应用层 (Application)提供用户服务接口HTTP、FTP、SMTP、DNS
      6表示层 (Presentation)数据格式转换、加解密、压缩等SSL/TLS、JPEG、MP3
      5会话层 (Session)建立、管理、终止会话RPC、NetBIOS、Socket 会话控制
      4传输层 (Transport)可靠传输、流量控制、差错恢复TCP、UDP
      3网络层 (Network)路由选择、逻辑寻址(IP 地址)IP、ICMP、IGMP
      2数据链路层 (Data Link)物理寻址(MAC 地址)、帧同步、差错检测Ethernet、PPP、交换机
      1物理层 (Physical)比特流传输、电压信号、接口规范网线、电缆、光纤、网卡、集线器
      OSI层级对应内容
      应用层你写好一封邮件
      表示层邮件内容编码成字节
      会话层建立你和收件人之间的通信会话
      传输层把信送上高速公路(TCP 拆包)
      网络层决定用哪条路去(IP 路由)
      数据链路层查收件人的门牌号(MAC)
      物理层实际快递传输过程(电信号)

端口

标识一台电脑上不同的服务

端口有65535个,两个字节

  • 知名端口
    • HTTP:80
    • FTP:21
    • SSH:22
    • 注册端口
    • 1024~49151
    • 分配给用户进程或应用进程
    • 动态端口
    • 49152~65535

使用netstat -an命令可以查看端口

ip地址

标识网络中不同的主机

ip地址的分类

  • A类IP地址
    • 1字节网络地址,3字节主机地址,网络地址以0开始
  • B类IP地址
    • 2字节网络地址,2字节主机地址,网络地址以10开始
  • C类IP地址
    • 3字节网络地址,1字节主机地址,网络地址以110开始
  • D类IP地址
    • 网络地址以1110开始
  • E类IP地址
    • 网络地址以1111开始
  • 私有IP地址
    • 只供局域网使用,不在公网中使用
    • 10.0.0.0 ~ 10.255.255.255
    • 172.16.0.0 ~ 172.31.255.255
    • 192.168.0.0 ~ 192.168.255.255

127.0.0.1是本机地址

子网掩码:常用255.255.255.0作为子网掩码


组网设备

集线器:每个数据包都是以广播的方式发送的,容易造成网络堵塞

交换机:连接内网主机

  • 转发过滤:发送到目标节点连接的端口
  • 学习功能:将地址与相应的端口映射起来存放在交换机缓存中的MAC表中

ARP协议(地址解析协议):通过发送arp广播,得到IP地址对应的MAC地址

路由器:连接不同的网络

套接字(Socket)

UDP 用户数据报协议,是一个无连接简单的面向数据报传输层协议。UDP不保证可靠性,它只是把应用程序传给IP层的数据报发出去,但不保证他们能够到达目的地由于无连接的特点,传输速度较快。

适用场景(注重速度):

1 ) 语音广播 

2 ) 视频 

3 ) QQ 

4 ) TFTP 

5 ) SNMP 

6 ) RIP 

7 ) DNS

创建UDP客户端程序的流程

  1. 创建套接字
  2. 传输数据
  3. 关闭套接字

#UDP发送数据示例

from socket import *

udp_socket = socket(AF_INET, SOCK_DGRAM) # AF_INET表示在Internet进程间通信, SOCK_DGRAM表示使用UDP协议

send_addr = ('192.168.64.56', 8888) # 端口号要使用数字

send_data = input("请输入要发送的数据")

udp_socket.sendto(send_data, send_addr)

udp_socket.close()


#UDP接收数据示例

from socket import *

udp_socket = socket(AF_INET, SOCK_DGRAM)

recv_data = udp_socket.recvfrom(1024) # 1024表示本次接收的最大字节数

print(recv_data)

udp_socket.close()

如果没有绑定端口号,每一次运行都会随机分配一个端口号,如果要使用固定的端口号,可以使用bind来进行绑定。

#绑定端口示例

from socket import *

udp_socket = socket(AF_INET, SOCK_DGRAM)

bind_addr = ('', 6666) # 要绑定的地址+端口号,ip不写,表示绑定本机的任何一个ip

udp_socket.bind(bind_addr) # 将元组作为参数传入

recv_data = udp_socker.recvfrom(1024)

print(recv_data)

udp_socket.close()

服务器与客户端

服务器:提供服务的一方,需要绑定端口

客户端:请求服务的一方,不需要绑定端口

UDP广播

from socket import *

dest = ('<broadcast>', 7788)

s = socket(AF_INET, SOCK_DGRAM)

s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)

s.sendto('Hi', dest)

TCP 传输控制协议,是一种面向连接,可靠的,基于字节流的通信协议。

TCP服务端的通信流程类似于生活中的手机

  1. 创建socket套接字(购买手机)
  2. 绑定ip和端口号(购买手机卡,确定了手机号)
  3. 设置为监听模式(将手机开机,设置为正常接听模式)
  4. 等待用户连接请求(等待电话拨入)
  5. 互相传输数据(通过手机对讲)

# TCP服务器示例

from socket import *

tcp_socket = socket(AF_INET, SOCK_STREAM) # 创建套接字

tcp_socket.bind(('', 8888)) # 绑定ip和端口号

tcp_socket.listen(5) # 设置为监听模式,参数5表示最多可以同时接收5个客户端的连接申请

new_socket, addr = tcp_socket.accept() # 通过accept取出对应的连接,取出对应的地址并创建相应的套接字

recv_data = new_socket.recv(1024) # 接收数据

print(recv_data) # 打印数据

new_socket.send("Thank you!".encode("gbk")) # 向客户端发送数据

tcp_socket.close() # 关闭套接字




TCP客户端的通信流程比较简单

from socket import *

tcp_client = socket(AF_INET, SOCK_STREAM) # 创建套接字

server_addr = ('192.168.64.56', 8888) # 服务器地址

tcp_client.connect(server_addr) # 向服务器请求连接

tcp_client.send("你好!".encode("gbk")) # 向服务器发送数据

recv_data = tcp_client.recv(1024) # 接收服务器发送过来的数据

print(recv_data) # 打印数据

tcp_client.close() # 关闭套接字

如果接收的数据长度为0,则意味着客户端关闭了连接。


三次握手
  1. 客户端向服务器发起连接请求
  2. 服务器对请求进行响应并确认(客户端--->>服务器的连接建立)
  3. 客户端接收到服务器发来的确认,并对服务器的连接请求进行确认(服务器--->>客户端的连接建立)
四次挥手

(这里的客户端和服务器不是绝对的,任何一方都可以发起断开连接的请求)

  1. 客户端发起断开连接的请求
  2. 服务器收到请求,断开连接并发回确认 (客户端--->>服务器的连接断开)
  3. 服务器将剩下的数据发送完毕,向客户端发起断开连接的请求
  4. 客户端断开连接并发回确认,进入TIME_WAIT等待状态,在2MSL之后,连接结束 (服务器--->>客户端的连接断开)

MSL:报文最大生存时间

为什么要等待2MSL?

因为有可能因为网络波动,导致对方没有收到最后一个包。对方将会在超时后重发第三次挥手的FIN包,主动关闭的一端在收到重发的FIN包后可以再发一个ACK应答包。在TIME_WAIT状态时,两端的端口不能使用,当连接处于2MSL等待阶段时,任何迟到的报文段都将丢弃。


短连接与长连接

短连接:一次连接一般只传递一次读写操作

  • 建立连接-传输数据-关闭连接...建立连接-传输数据-关闭连接
  • 优点:管理简单
  • 缺点:如果客户请求频繁,将在TCP连接的建立和释放上占用大量时间和带宽

长连接:一次连接可以传递多次读写操作

  • 建立连接-传输数据...(保持连接)...传输数据-关闭连接
  • 优点:适合频繁请求的客户,节约时间和带宽,不需要频繁建立和释放连接
  • 缺点:不容易管理,客户端的连接越来越多,服务器的压力也会越来越大
  • 解决方法:为客户端机器设置最大连接数,释放一些长时间没有操作的连接

HTTP是无状态的协议

HTTP的长连接和短连接

在HTTP/1.0当中,默认使用短连接

从HTTP/1.1起,默认使用长连接

服务器(Server)

单进程服务器

# 单线程服务器示例

1 from socket import *

2

3 tcp_socket = socket(AF_INET, SOCK_STREAM)

4 local_addr = ('', 8888)

5 tcp_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

6 tcp_socket.bind(local_addr)

7 tcp_socket.listen(5)

8

9 while True:

10 toclient_socket, client_addr = tcp_socket.accept()

11 try:

12 while True:

13 recv_data = toclient_socket.recv(1024)

14 if len(recv_data) > 0:

15 print("收到数据%s" % recv_data.decode("gbk"))

16 else:

17 print("对方已关闭连接")

18 break

19 finally:

20 toclient_socket.close()

21 tcp_socket.close()


多进程服务器

# 多进程服务器示例

1 from socket import *

2 from multiprocessing import Process

3

4 def deal_with_client(new_socket):

5 while True:

6 recv_data = new_socket.recv(1024)

7 if len(recv_data) > 0:

8 print("收到数据:%s" % recv_data)

9 else:

10 print("对方已关闭连接!")

11 new_socket.close() # 对方退出后,关闭套接字

12 break

13

14 tcp_socket = socket(AF_INET, SOCK_STREAM)

15 tcp_socket.bind(('', 8888))

16 tcp_socket.listen(5)

17

18 while True:

19 new_socket, addr = tcp_socket.accept()

20 deal_process = Process(target=deal_with_client, args=(new_socket,))

21 deal_process.start()

22 new_socket.close() # 在新生成的套接字传到进程中之后,关闭套接字。因为创建新的进程会复制一份新的变量,所以需要关闭两次

23 tcp_socket.close()


多线程服务器

from socket import *

from threading import Thread


def dealWithClient(newSocket, destAddr):

    while True:

        recvData = newSocket.recv(1024)

        if len(recvData) > 0:

            print('recv[%s]:%s' % (str(destAddr), recvData))

        else:

            print('[%s]客户端已经关闭' % str(destAddr))

            break

    newSocket.close()



def main():

    serSocket = socket(AF_INET, SOCK_STREAM)

    serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    localAddr = ('', 7788)

    serSocket.bind(localAddr)

    serSocket.listen(5)


    try:

        while True:

            print('-----主进程,等待新客户端的到来------')

            newSocket, destAddr = serSocket.accept()

            print('-----主进程,接下来创建一个新的进程负责数据处理[%s]-----' % str(destAddr))

            client = Thread(target=dealWithClient, args=(newSocket, destAddr))

            client.start()

           

            # 因为线程中共享这个套接字,如果关闭了会导致这个套接字不可用

            # 但是此时在线程中这个套接字可能还在收数据,因此不能关闭

            # newSocket.close()

    finally:

        serSocket.close()



if __name__ == '__main__':

    main()



单进程服务器-非阻塞

# 单进程非阻塞服务器示例

from socket import *

new_socket_list = [] # 创建一个列表,用来存放新连接的socket

tcp_socket = socket(AF_INET, SOCK_STREAM) # 创建服务器套接字

tcp_socket.bind(('', 9888)) # 绑定端口

tcp_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 修改设置,允许端口重用

tcp_socket.listen(5) # 开启监听

tcp_socket.setblocking(False) # 设置tcp_socket套接字为非阻塞模式

while True: # 无限循环,轮询

try: # 捕获异常

new_socket_addr = tcp_socket.accept() # 使用accept获取新连接,因为是非阻塞,所以需要写在try里面

except:

pass # 报错直接忽略

else:

new_socket_addr[0].setblocking(False) # 如果没有报错,将新的连接套接字设置为非阻塞模式

new_socket_list.append(new_socket_addr) # 将新的套接字放到列表当中

need_delete_list = [] # 创建一个待删除列表,用来存放已经关闭的套接字

for new_socket, addr in new_socket_list: # 遍历新套接字列表

try:

recv_data = new_socket.recv(1024) # 获取数据

except:

pass

else:

if len(recv_data) > 0: # 如果有数据

print(recv_data)

else: # 如果对方关闭

print("对方已关闭连接!")

new_socket.close() # 关闭套接字

need_delete_list.append((new_socket, addr)) # 将该套接字放到待删除列表当中

for need_delete in need_delete_list: # 遍历待删除列表

new_socket_list.remove(need_delete) # 在新套接字列表当中删除所有已关闭的套接字,不在轮询

tcp_socket.close() # 关闭套接字


epoll服务器
  • 优点
    • 没有最大并发连接限制,能打开的FD(文件描述符,通俗的理解就是套接字对应的数字编号)远大于1024
    • 效率较高,不是轮询的方式,不会因为连接的增加而降低效率。只有活跃的连接会调用callback,因此epoll只管理活跃的连接,效率远高于select和poll

#epoll服务器示例

import socket

import selecttcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建套接字

tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 设置端口重用

tcp_socket.bind(('', 8888)) # 绑定端口号

tcp_socket.listen(5) # 开启监听模式

epoll = select.epoll() # 创建epoll对象

epoll.register(tcp_socket.fileno(), select.EPOLLIN|select.EPOLLET) # 获取主套接字的文件描述符并注册,事件:可读 模式:ET

connections = {} # 创建一个连接字典,用来存放新连接的套接字

address = {} # 创建一个地址字典,用来存放新连接的套接字对应的地址

while True: # 循环等待客户端连接

epoll_list = epoll.poll() # 进行fd扫描,默认是阻塞等待,如果触发了已注册的事件,会返回一个元组(fd, event)

for fd, event in epoll_list: # 从元组中获取fd(文件描述符), event(事件)

if fd == tcp_socket.fileno(): # 如果是主套接字被触发,意味着有新的客户端请求连接

toclient_socket, addr = tcp_socket.accept() # 使用accept,获取新连接的套接字和地址

connections[toclient_socket.fileno()] = toclient_socket # 将套接字存进connections字典,以新套接字的文件描述符为键

address[toclient_socket.fileno()] = addr # 将地址存进address字典,以新套接字的文件描述符为键

epoll.register(toclient_socket.fileno(), select.EPOLLIN|select.EPOLLET) # 将新的套接字进行注册,事件:可读 模式:ET

elif event == select.EPOLLIN:

recv_data = connections[fd].recv(1024) # 如果第一个if不成立,意味着不是主套接字。如果是可读事件,就代表着客户端连接的套接字被触发,就可以用recv接收到数据

if len(recv_data) > 0: # 如果数据长度大于0,意味着有数据

print("收到数据:%s, 来自 %s, 地址 %s" % (recv_data, fd, address[fd])) # 打印数据

else: # 如果小于等于0,意味着对方关闭了连接

print("对方已关闭连接!") # 打印连接关闭提示

connections[fd].close() # 将对应的套接字关闭

del connections[fd] # 删除字典中的套接字

del address[fd] # 删除字典中的地址

EPOLLIN/EPOLLOUT/EPOLLET/EPOLLLT

EPOLLIN 是可读事件,从不可读转换到可读时,会触发。

EPOLLOUT 是可写事件,从不可写转换到可写时,会触发。

EPOLLET:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。

EPOLLLT:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。

协程(Coroutine)

什么是协程?

协程是比线程更小的执行单元,是轻量级的线程。协程自带CPU上下文,只要在合适的时机,我们就可以把一个协程切换到另一个协程,只要保存或恢复CPU上下文,程序就还可以运行。

通俗地说

线程当中的某个函数,可以在任何地方保存函数中的变量等信息,切换到另一个函数继续执行。注意,不是以调用函数的方式进行切换,并且切换的时机有开发者自行决定。

协程和线程

线程的切换不仅仅是保存和恢复上下文那么简单,操作系统为了程序高效运行,每个线程都会有自己的缓存Cache等数据,在切换线程的时候,操作系统还会帮你做这些数据的恢复操作。而协程的切换仅仅是上下文的切换,所以协程的切换相对线程而言,非常非常快。

协程的问题

操作系统并不会感知协程,所以协程的切换是个问题。

问题的解决

目前的协程框架大多是采用1:N的模式。

所谓1:N的模式,就是一个线程作为容器放置多个协程。而协程的切换,是协程自身决定的。

每个协程里面都有一个调度器,当这个协程发现自己执行不下去了(比如异步等待网络的数据回来,但数据还没有到),协程就会通知调度器,切换另一个协程执行。

新的问题

假设一个线程当中有一个协程是CPU密集型(框架中的协程遇上IO会自动切换)的,没有IO操作,那么它就不会主动触发调度器,其他协程就无法得到CPU,这个问题需要开发者来处理避免。

优点

协程可以解决当IO操作密集时,CPU频繁切换线程的问题。

把IO操作封装成一个协程,在触发IO操作时,会自动让出CPU给其他协程,而且协程的切换时很轻的,所以协程既保证了性能,又保证了代码的可读性。不过在CPU密集型的代码中,协程并没有什么好处。


Python 中的协程

在 Python 中,协程通常是通过 async 和 await 关键字实现的。async 用于定义协程函数,await 用于暂停执行并等待另一个协程的结果。

加了 await,协程可以“挂起”当前执行,等待一个耗时操作完成后继续执行,从而实现真正的异步并发、非阻塞。

示例:

python

CopyEdit

import asyncio # 定义一个协程函数 async def say_hello(): print("Hello") await asyncio.sleep(1) # 模拟异步操作 print("World") # 创建一个事件循环并运行协程 async def main(): await say_hello() # 运行协程 asyncio.run(main())

不加 await(同步方式):

python

CopyEdit

import asyncio import time async def download(name): print(f"Start {name}") time.sleep(2) # 阻塞操作 print(f"End {name}") async def main(): await download("page1") await download("page2") asyncio.run(main())

  • time.sleep(2) 是阻塞操作
  • 执行时间 ≈ 4 秒
  • 不能并发,只能一个一个来。
加了 await + 异步操作(协程意义体现):

python

CopyEdit

import asyncio async def download(name): print(f"Start {name}") await asyncio.sleep(2) # ⬅️ 这里是关键 print(f"End {name}") async def main(): await asyncio.gather( download("page1"), download("page2") ) asyncio.run(main())

  • await asyncio.sleep(2) 是非阻塞的协程等待。
  • 执行时间 ≈ 2 秒
  • 并发执行了两个协程。
本质说明
await 的作用:
  • await 后面必须是一个协程对象(或支持 __await__ 的对象)
  • await 表示:挂起当前协程,等后面的操作执行完再回来继续
  • 挂起期间,事件循环可以调度其他协程,实现并发

HTTP服务器

什么是HTTP协议?

HTTP协议,就是在网络上传输超文本的协议,HTTP是无连接,无状态的应用层协议。浏览器与服务器之间的通信采用的就是HTTP协议。

HTML是一种用来定义网页的文本。


无连接是指每次请求用完连接就释放。

无状态是指每次请求不记得上次的内容(可以用 Cookie/Session 保持)


WSGI(web server gateway interface)

WSGI只要求开发者实现一个函数,就可以相应HTTP请求。

WSGI 就是连接 Web 服务器(如 Nginx+uWSGI) 和 Web 应用框架(如 Flask、Django)的桥梁。

关于 ASGI

  1. ASGI 是用于支持异步(async/await)、WebSocket 的 Python Web 服务器接口规范,是对 WSGI 的升级。

你写了一个 Python 网站(比如 Flask 应用),这个网站本身不能直接处理 HTTP 请求,它需要一个“服务员”(Web 服务器)来把请求交给它处理。

浏览器
  ↓
Nginx (或 Apache)
  ↓  [反向代理]
uWSGI / Gunicorn(WSGI服务器)
  ↓  [WSGI 协议]
Flask / Django(WSGI应用)
示例代码(最小 WSGI 应用):


def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'Hello WSGI']

这个 application 就是 WSGI 规定的入口函数。

为什么 WSGI 很重要?
  1. 标准化接口:任何支持 WSGI 的服务器都能运行你的 Python Web 应用;
  2. 解耦:Web 应用(如 Django)不需要关心 TCP/HTTP 的细节,只处理业务逻辑;
  3. 兼容性好:Flask、Django、Bottle 等框架都遵循 WSGI 标准;
  4. 部署灵活:结合 uWSGI、Gunicorn、Nginx 使用,生产部署很方便。

WSGI 就是这两者之间的“约定”:

  • Web服务器 把 HTTP 请求(翻译成标准格式)交给 Python 程序
  • Python 程序 处理后再返回响应内容

#符合WSGI标准的HTTP处理函数示例

#application必须由WSGI服务器来调用

#environ: 一个包含所有HTTP请求信息的dict对象

#start_response: 一个发送HTTP响应的函数

def application(environ, start_response):

start_reponse('200 OK', [('Content-Type', 'text/html')])

return 'Hello World!'

#coding=utf-8

import socket

import sys

from multiprocessing import Process

import re

class WSGIServer(object):

addressFamily = socket.AF_INET

socketType = socket.SOCK_STREAM

requestQueueSize = 5

def __init__(self, serverAddress):

​#创建一个tcp套接字

self.listenSocket = socket.socket(self.addressFamily,self.socketType)

#允许重复使用上次的套接字绑定的port

self.listenSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

#绑定

self.listenSocket.bind(serverAddress)

#变为被动,并制定队列的长度

self.listenSocket.listen(self.requestQueueSize)

self.servrName = "localhost"

self.serverPort = serverAddress[1]

def serveForever(self):

'循环运行web服务器,等待客户端的链接并为客户端服务'

while True:

#等待新客户端到来

self.clientSocket, client_address = self.listenSocket.accept()

#方法2,多进程服务器,并发服务器于多个客户端

newClientProcess = Process(target = self.handleRequest)

newClientProcess.start()

#因为创建的新进程中,会对这个套接字+1,所以需要在主进程中减去依次,即调用一次close

self.clientSocket.close()

def setApp(self, application):

'设置此WSGI服务器调用的应用程序入口函数'

self.application = application

def handleRequest(self):

'用一个新的进程,为一个客户端进行服务'

self.recvData = self.clientSocket.recv(2014)

requestHeaderLines = self.recvData.splitlines()

for line in requestHeaderLines:

print(line)

httpRequestMethodLine = requestHeaderLines[0]

getFileName = re.match("[^/]+(/[^ ]*)", httpRequestMethodLine).group(1) # 获取请求的文件名

print("file name is ===>%s"%getFileName) #for test

if getFileName[-3:] != ".py": # 如果不是以.py结尾,则是静态文件

if getFileName == '/': # /转向index

getFileName = documentRoot + "/index.html"

else: # 不是/,将文件名与documentroot路径拼接

getFileName = documentRoot + getFileName

print("file name is ===2>%s"%getFileName) #for test

try: # 捕获异常,因为文件可能不存在

f = open(getFileName) # 尝试打开文件

except IOError: # 打开失败,表示文件不存在,返回404

responseHeaderLines = "HTTP/1.1 404 not found\r\n"

responseHeaderLines += "\r\n"

responseBody = "====sorry ,file not found===="

else: # 打开成功,表示文件存在,返回200

responseHeaderLines = "HTTP/1.1 200 OK\r\n"

responseHeaderLines += "\r\n"

responseBody = f.read()

f.close()

finally: # 不管是否成功,都要向客户端发送数据,并关闭套接字

response = responseHeaderLines + responseBody

self.clientSocket.send(response)

self.clientSocket.close()

else:

#处理接收到的请求头

self.parseRequest()

#根据接收到的请求头构造环境变量字典

env = self.getEnviron()

#调用应用的相应方法,完成动态数据的获取

bodyContent = self.application(env, self.startResponse) # 获取到响应体

#组织数据发送给客户端

self.finishResponse(bodyContent) # 完成数据构造

def parseRequest(self):

'提取出客户端发送的request'

requestLine = self.recvData.splitlines()[0]

requestLine = requestLine.rstrip('\r\n')

self.requestMethod, self.path, self.requestVersion = requestLine.split(" ")

def getEnviron(self):

env = {}

env['wsgi.version'] = (1, 0)

env['wsgi.input'] = self.recvData

env['REQUEST_METHOD'] = self.requestMethod # GET

env['PATH_INFO'] = self.path # /index.html

return env

def startResponse(self, status, response_headers, exc_info=None): # 构造响应头

serverHeaders = [

('Date', 'Tue, 31 Mar 2016 10:11:12 GMT'),

('Server', 'WSGIServer 0.2'),

]

self.headers_set = [status, response_headers + serverHeaders]

def finishResponse(self, bodyContent):

try:

status, response_headers = self.headers_set

#response的第一行

response = 'HTTP/1.1 {status}\r\n'.format(status=status)

#response的其他头信息

for header in response_headers:

response += '{0}: {1}\r\n'.format(*header)

#添加一个换行,用来和body进行分开

response += '\r\n'

#添加发送的数据

for data in bodyContent:

response += data

self.clientSocket.send(response)

finally:

self.clientSocket.close()

#设定服务器的端口

serverAddr = (HOST, PORT) = '', 8888

#设置服务器静态资源的路径

documentRoot = './html'

#设置服务器动态资源的路径

pythonRoot = './wsgiPy'

def makeServer(serverAddr, application):

server = WSGIServer(serverAddr)

server.setApp(application)

return server

def main():

if len(sys.argv) < 2:

sys.exit('请按照要求,指定模块名称:应用名称,例如 module:callable')

#获取module:callable

appPath = sys.argv[1]

#根据冒号切割为module和callable

module, application = appPath.split(':')

#添加路径套sys.path

sys.path.insert(0, pythonRoot)

#动态导入module变量中指定的模块

module = __import__(module)

#获取module变量中制定的模块的application变量指定的属性

application = getattr(module, application)

httpd = makeServer(serverAddr, application)

print('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT))

httpd.serveForever()

if __name__ == '__main__':

main()

正则表达式(Regular Expression)

用来描述一组计算机能够识别的字符串规则。

查询方法

match从头开始匹配,如果开头不匹配就直接返回None

search从任意位置开始匹配一个,只要字符串中有匹配的字符,就能匹配成功

findall找出所有匹配的,返回列表

sub将匹配到的数据替换成指定的字符串,第二个参数可以是函数

#sub函数作参数示例

import re

def add(temp):

str_num = temp.group()

num = int(str_num) + 1

return str(num)

ret = re.sub(r'\d+', add, 'python = 997')

print(ret) # 998

ret = re.sub(r'\d+', add, 'python = 99')

print(ret) # 100

split根据指定的规则对字符串进行切割

原始字符串

python中的一个反斜杠需要用'\\'表示,如果正则要匹配到这个反斜杠,就要这样写re.match('\\\\', '\\')

这样写严重降低了代码的可读性,解决方法就是使用原始字符串,在字符串前加个r,re.match(r'\\','\\')

匹配分组

| 匹配左右任意一个表达式

(ab) 将括号中的字符作为一个分组

\num 引用分组num到字符串

(?P<name>) 分组起别名

(?P=name) 引用别名为name的分组

贪婪与非贪婪

贪婪:在符合规则的情况下尽可能多地匹配

非贪婪:在符合规则的情况下尽可能少地匹配

python中的数量词默认是贪婪的

如何把贪婪变成非贪婪?

在数量词后加上'?',就能使贪婪变成非贪婪

数据库(Database)

SQL结构化查询语言(不区分大小写)
  • DQL数据查询语言,select
  • DML数据操作语言,update,delete,insert
  • TPL事物处理语言,begin,commit,rollback
  • DCL数据控制语言,grant,revoke
  • DDL数据定义语言,create,drop
  • CCL指针控制语言,declare cursor
优点
  • 可移植性强
  • 为多种语言提供了API
  • 支持多线程
  • 优化的SQL查询算法,有效地提高查询速度
  • 提供多语言支持
  • 提供TCP/IP、ODBC、JDBC等多种数据库连接途径
  • 支持多种存储引擎
  • 开源
  • 免费
数据类型

使用数据类型的原则:够用就行,尽量使用取值范围小的,节省存储空间

常用数据类型
  • 整数:int, bit
  • 小数:decimal
  • 字符串:varchar, char
  • 日期:date,time,datetime
  • 枚举类型:enum
约束
  • 主键primary key
  • 非空not null
  • 唯一unique
  • 默认default
  • 外键foreign key
  • 说明:外键约束可以保证数据的有效性,但在进行crud时,都会降低数据库的性能,所以不推荐使用,数据的有效性可以在逻辑层控制。
字段的增删查改

增:alter table 表名 add 列名 类型;

改-重命名:alter table 表名 change 原名 新名 类型;

改-不重命名:alter table 表名 modify 列名 类型及约束;

删:alter table 表名 drop 列名;

查看表的创建语句

show create table 表名;

数据操作(Crud)

增insert

删delete

查select

改update

自动增长列需要一个占位符,通常使用0,default,null来占位。
备份
  • mysqldump -uname -ppwd 数据库名 -> filename.sql
恢复
  • mysql -uname -ppwd 数据库名 <- filename.sql
三范式
  • 第一范式 (1NF):列不可分割(关系数据库的基本要求)
  • 第二范式(2NF):表中每个实例或记录必须可以被唯一地区分
  • 第三范式(3NF):任何非主属性不依赖其他非主属性,即引用主键
E-R模型
  • E-entry:实体
    • 设计实体就像定义一个类,一个实体可以转换成数据库中的一张表
  • R-relationship:关系
    • 描述两个实体之间的对应规则,关系类型包括,一对一,一对多,多对多
    • 关系也是一种数据,需要通过一个字段存储在表中
  • A对B一对一
    • 在A或B中创建一个字段,存储另一个表的主键值
  • A对B一对多
    • 在B中创建一个字段,存储另一个表的主键值
  • A对B为多对多
    • 新建表C,拥有两个字段,一个存储表A的主键值,一个存储表B的主键值
逻辑删除

对于某些重要的数据,不希望物理删除,可以使用逻辑删除

实现方法:设置一个isDelete字段,类型为bit,表示逻辑删除,默认为0

MySQL

#使用distinct可以去除重复的行

select distinct gender from students;

#完整的查询语句

-- 完整的查询语句

/*完整的查询语句*/

SELECT select_expr [,select|_expr,...] [

FROM tb_name

[WHERE 条件判断]

[GROUP BY {col_name | postion} [ASC | DESC], ...]

[HAVING WHERE 条件判断]

[ORDER BY {col_name|expr|postion} [ASC | DESC], ...]

[ LIMIT {[offset,]rowcount | row_count OFFSET offset}]

]

模糊查询

# like

select * from students where name like '黄%'; # % 表示任意多个字符

select * from students where name like '黄_'; # _ 表示任意一个字符

范围查询

select * from students where id in(1,3,8); # in表示在一个非连续的范围内

select * from students where id between 3 and 8; # between ... and ... 表示在一个连续的范围内

空判断

select * from students where height is null; # null与''是不同的

select * from students where height is not null; # is not null 非空

优先级

  • 由高到低:小括号、not、比较运算符、逻辑运算符
  • and比or先运算,如果希望or先算,需要使用小括号

排序

select * from 表名 order by 列1 asc|desc, 列2 asc|desc,... # 语法 asc从小到大排列,desc从大到小排列

聚合函数

count(*) # 计算总行数

# 示例

select count(*) from students;

-- ------------------------------------

max(列) # 求此列的最大值

# 示例

select max(id) from students where gender=0;

-- ------------------------------------

min(列) # 求此列的最小值

# 示例

select min(id) from students where isdelete=0;

-- ------------------------------------

sum(列) # 求此列之和

# 示例

select sum(id) from studetns where gender=1;

-- ------------------------------------

avg(列) # 求此列平均值

select avg(id) from students where isdelete=0 and gender=0;

分组

# 语法

select 列1,列2,聚合... from 表名 group by 列1,列2...

# 示例

select gender as 性别,count(*) from students group by gender;

分组后数据筛选

select 列1,列2,聚合... from 表名 group by 列1,列2,列3... having 列1,...聚合...

# 示例

select gender as 性别, count(*) from students group by gender having gender=1;

#获取部分行

select * from 表名 limit start,count; # 从start开始,取count条数据

# 示例

select * from students where gender=1 limit 0,3;

# 分页

select * from students where isdelete=0 limit (n-1)*m, m

where和having

where是对from指定的表进行数据筛选,属于对原始数据进行筛选

having是对group by的结果进行筛选


连接查询

当查询结果来自于多张表时,需要将多张表连接起来

  • 内连接查询:查询结果为两个表匹配到的数据
  • 右连接查询:查询结果为两个表匹配到的数据,右表特有的数据,对于左表中不存在的数据使用null填充
  • 左连接查询:查询结果为两个表匹配到的数据,左表特有的数据,对于右表中不存在的数据使用null填充

上代码吧

select * from 表1 inner或left或right join 表 on 表1.列=表2.列;

#内连接示例

select * from students inner join pythons on students.cls_id = pythons.id;

#上面的语句将学生表和班级表按照班级id进行内连接,两表id都能够对上才能匹配出来

-- -------------------------------------------------------------------

#左连接示例

select * from students left join pythons on students.cls_id = pythons.id;

#上面的语句将学生表和班级表按照id进行左连接,只要左表(students)中有的id,就能匹配出来,如果右表中没有,就填充null

-- -------------------------------------------------------------------

#右连接示例

select * from students right join pythons on students.cls_id = pythons.id;

#上面的语句将学生表和班级表按照id进行右连接,只要右表(classes)中存在的,就能匹配出来,如果左表中没有,就填充null

自关联

一张表将自身视为另一张表,自己与自己进行连接,就是自关联。

#表结构

create table areas(

aid int primary key,

atitle varchar(20),

pid int

);

#查询一共有多少个省

select count(*) from areas where pid is null;

#查询省名称为‘广东省’的所有城市

select city.atitle from areas as city inner join areas as province on city.pid=province.aid where province.atitle='广东省';

#查询市名称为'深圳市'的所有区县

select dis.atitle from areas as dis inner join areas as city on city.id=dis.pid where atitle='深圳市';

子查询

在一个select语句中,嵌入另外一个select语句,被嵌入的select就称为子查询语句。主要查询的对象则称为主查询。

  • 标量子查询:子查询返回结果是一个数据(一行一列)
  • 列级子查询:子查询返回结果是一列数据(一列多行)
  • 行级子查询:子查询返回结果是一行数据(一行多列)
  • 表级子查询:子查询返回结果是多行多列
  • 很多表级子查询都能够用连接查询实现,这种时候推荐使用连接查询,更简洁清晰。

# 行级子查询

# 查询班级中年龄最大,身高最高的学生

select * from students where(age, height) = (select max(age), max(height) from student);

子查询中特定关键字使用

in 范围

主查询 where 条件 in (列子查询)

any | some 任意一个

主查询 where 列 = any (列子查询)

all

主查询 where 列 = all(列子查询) 等于里面所有

主查询 where 列 <> all(列子查询) 不等于其中所有



通过子查询插入数据

insert into goods_cate (cate_name) select cate from goods group by cate;


# 通过子查询插入数据,不需要values关键字。

# 通过create...select 来写入数据,一步到位

create table goods_brands (

brand_id int unsigned primary key auto_increment,

brand_name varchar(40) not null) selelct brand_name from goods group by brand_name;


# 更新已存在表的外键约束

alter table goods add foreign key(cate_id) references goods_cates(cate_id);

alter table goods add foreign key(brand_id) references goods_brands(brand_id);


# 删除已存在的外键

alter table goods drop foreign 外键名;

# show create table 表名; 可以查看外键名

使用外键可能会降低表更新的效率

Mysql语句的执行顺序
  1. from 表名
  2. where ...
  3. group by ...
  4. select distinct ...
  5. having ...
  6. order by ...
  7. limit ...
关于数据库常见的安全问题
  • SQL注入:不合法输入,如单引号
  • 撞库:利用从别处得到的账户密码,尝试在另一个网站上登录
  • 攻击服务器的安全漏洞
安全建议
  • 存储密码必须加密(针对数据库设计)
  • 登录时添加验证码(针对Web开发者)
  • 在多个站点间使用不同的密码(针对普通用户)


函数:

可以在crud语句中使用

存储过程:

需要使用call调用

delimiter $$ # 设置分割符,默认为分号

create function name(args_list) returns 返回类型

begin

sql语句

end

$$

delimiter ;

相同点:
  • 两者都是为了可重复执行的操作数据库的sql语句的集合
  • 编译一次后都会被缓存起来,不需要重复编译
  • 减少网络交互,减少网络访问流量
不同点:
  • 标识符不同,function和procedure
  • 函数必须有返回值,存储过程没有返回值
  • 函数需要使用select,存储过程要使用call来调用
  • 存储过程中可以调用select,而函数中除了select...into之外的select语句都无法调用
  • 存储过程可以通过设置in,out参数,使得更加灵活,可以返回多个结果


视图

本质就是对查询的封装

#创建视图

create view 视图名 as select语句;

#查看视图

show tables; # 查看表会将所有的视图也列出来

#删除视图

drop view 视图名;

#使用视图

select * from 视图名;


事物(Transaction)
为什么需要事物?

一个转账案例,A转500元给B:

  1. 首先,系统检查A的余额是否大于等于500
  2. 在A中扣除500
  3. 在B中增加500
  4. 转账完成

如果流程正常走完,那就皆大欢喜,但是如果系统故障了呢?

流程正常走完,有一个隐藏的前提条件,就是A扣钱,B加钱,要么同时完成,要么同时失败。这就是事物的需求。


事物的四大特性(ACID)
  • 原子性(Atomicity)
    • 事物的所有操作不可分割,要么全部完成,要么全部失败
  • 一致性(Consistency)
    • 事务在执行前后,数据库的状态应始终保持一致。如果事务执行前数据库处于一致性状态,那么事务执行完毕后也应该处于一致性状态
    • 一致性就是指数据库在事务开始和结束时,始终满足所有的约束和业务规则,确保数据的有效性和合法性。
  • 隔离性(Isolation)
    • 事物的执行不受其他事物的干扰,事物执行的中间结果对其他事物必须是透明的
  • 持久性(Durability)
    • 对于任意已提交的事物,系统必须保证该事物对数据库的改动不丢失,即使数据库出现故障
爬虫学习笔记