• day2.tcp/udp编程


    一、三次握手四次挥手

    """
    SYN 创建连接
    ACK 确认响应
    FIN 断开连接
    """

    1、三次握手

    2、四次挥手

    3、总体

    """
    三次握手
    1. 首先客户端向服务器发送一个SYN请求,与服务器建立连接.序列号为seq=x,随后客户端进入SYN-SEND状态
    2. 服务器接收请求,结束LISTEN阶段,发出相应,统一创建新连接,响应的ACK值为x+1,并且回应也要与客户端建立连接,请求的SYN序列号为seq=y,随后服务器进入SYN-RECV状态,
    (服务器同意连接的请求与服务器与客户端建立连接的请求在一次相应中完成)
    3. 客户端接收服务器的请求后,明确了从客户端到服务器的数据传输是正常的,结束SYN-SEND状态,把相应信息ACK=y+1相应给服务器,随后客户端进入established阶段
    
    服务器收到客户端的报文后,明确了从服务器到客户端是正常的,结束SYN-SEND阶段,进入established阶段,完成了三次握手
    """
    """
    四次挥手
    1. 客户端向服务器发送一个断开连接的请求
    2. 服务器接收请求,发出相应
    3. 等到服务器把所有数据发送或接收完毕后,服务器向客户端发送断开连接的请求
    4. 客户端接收请求,并发出相应,(持续2MSL)等待2MSL的最大报文生存时间后,客户端与服务器断开连接
    """

    二、Socket

    """
    Socket是应用层与传输层(TCP/IP协议族)通信的中间软件抽象层,它是一组接口它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
    
    所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
    """
    """
    socket的意义:通络通信过程中,信息拼接的工具(中文:套接字)
    # 开发中,一个端口只对一个程序生效,在测试时,允许端口重复捆绑 (开发时删掉)
    # 在bind方法之前加上这句话,可以让一个端口重复使用
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    """

    二、套接字的分类

    """
    基于文件类型的套接字家族
    套接字家族的名字:AF_UNIX
    unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
    因为一台机器上的两个程序都是基于底层的文件系统工作的
    
    基于网络类型的套接字家族
    套接字家族的名字:AF_INET
    """

    二、TCP

    1、基于TCP套接字基本写法

    import socket
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机 AF_INET 网络形式发送 SOCK_STREAM tcp
    phone.bind(('127.0.0.1', 8000)) # 绑定手机卡
    phone.listen(5) # 开机,最大监听数
    print('----------->')
    conn, addr = phone.accept() # 等电话 conn是链接客户端 addr是对方手机号
    
    msg = conn.recv(1024) # 收消息
    print('客户端发来的消息是:', msg)
    conn.send(msg.upper()) # 发消息
    
    conn.close() # 关闭链接 挂电话
    phone.close() # 关闭socket 关机
    tcp-服务器
    import socket
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.connect(('127.0.0.1', 8000)) # 拨电话
    
    phone.send('hello'.encode('utf-8')) # 发消息
    data = phone.recv(1024)
    print('收到服务器发来的消息:', data)
    tcp-客户端

    2、关于TCP的一些东西

    """
    服务端conn, addr = tcp_server.accept() 服务端阻塞
    只有客户端 .connect(('127.0.0.1', 8000))才会继续往下执行
    
    客户端执行链接后,
    print('双向链接是', conn)
    print('客户端地址是', addr)
    # 结果为<socket.socket fd=552, family=AddressFamily.AF_INET, type=
    SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8000), raddr=('127.0.0.1', 61703)>
    addr是客户端的IP地址
    """

    3、tcp那句话是建立三次链接

    """
    phone.connect(('127.0.0.1', 8000))
    conn, addr = tcp_server.accept()
    就是在建立三次握手
    """

    4、tcp为什么是可靠的

    """
    看数据传输,客户端seq序列号为x+1, 应答报文ACK=y+1,服务器
    端根据seq序列号x+1返回一个应答报文ACK=x+2告诉客户端,
    我收到你发送的消息了,
    一问一答,建立了一个可靠传输机制
    
    tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=x+2,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的
    
    而udp发送数据,对端是不会返回确认信息的,因此不可靠
    """

    5、tcp哪句是关闭三次握手

    # conn.close() 关闭三次握手 触发四次挥手

    6、tcp为什么是三次握手四次挥手

    """
    三次握手只是建立了一个没有数据的链接而已
    但是四次挥手,中间可能有数据
    因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文.但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
    """

    三、tcp循环写法

    from socket import *
    ip_port = ('127.0.0.1', 8000)
    back_log = 5
    buffer_size = 1024
    
    tcp_server = socket(AF_INET, SOCK_STREAM) # 买手机 AF_INET 网络形式发送 SOCK_STREAM tcp
    # 服务端关闭时,可是遗留timewite状态,导致服务端重新启动 没办法监听原来的ip地址, 目的重用
    # linux netstat -an | grep 8000 双向链接后状态为 LISTEN
    # 终止服务器 状态变为 FIN_WAIT2
    # 再运行服务器端就会报错
    # 重新使用IP地址
    tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    tcp_server.bind(ip_port) # 绑定手机卡 # 我自己的ip因为我是服务器别人访问我
    tcp_server.listen(back_log) # 开机,最大监听数 ,等待连接的建立 建立半链接池
    print('服务器开始运行了')
    while True:   # 多个客户端链接
        conn, addr = tcp_server.accept() # 等电话 conn是链接客户端 addr是对方手机号
        print('双向链接是', conn)
        print('客户端地址是', addr)
    
        while True:  # 每一个客户端链接的多个收发消息
            try:      # 异常处理,避免一个有两个客户端其中一个停止,服务器报错
                data = conn.recv(buffer_size) # 收消息
                print('客户端发来的消息是:', data.decode('utf-8'))
                conn.send(data.upper()) # 发消息
            except Exception:
                break
    
        conn.close() # 关闭链接 挂电话
    tcp_server.close() # 关闭socket 关机
    tcp-循环-server
    from socket import *
    ip_port = ('127.0.0.1', 8000)
    back_log = 5
    buffer_size = 1024
    
    tcp_client = socket(AF_INET, SOCK_STREAM)
    tcp_client.connect(ip_port)
    
    while True:
        msg = input('>>: ').strip()
        if not msg: continue # 如果为空,则重新执行
        tcp_client.send(msg.encode('utf-8')) # 发消息
        print('客户端已经发送消息')
        data = tcp_client.recv(buffer_size)
        print('收到服务器发来的消息:', data)
    tcp_client.close()
    tcp-循环-client1
    from socket import *
    ip_port = ('127.0.0.1', 8000)
    back_log = 5
    buffer_size = 1024
    
    tcp_client = socket(AF_INET, SOCK_STREAM)
    tcp_client.connect(ip_port)
    
    while True:
        msg = input('>>: ').strip()
        if not msg: continue # 如果为空,则重新执行
        tcp_client.send(msg.encode('utf-8')) # 发消息
        print('客户端已经发送消息')
        data = tcp_client.recv(buffer_size)
        print('收到服务器发来的消息:', data)
    tcp_client.close()
    tcp-循环-client2

    1、基于TCP实现远程执行命令

    """
    import subprocess
    from socket import *
    ip_port = ('127.0.0.1', 8080)
    back_log=5
    buffer_size = 1024
    
    tcp_server = socket(AF_INET, SOCK_STREAM)
    tcp_server.bind(ip_port)
    tcp_server.listen(back_log)
    
    while True:
        conn, addr = tcp_server.accept()
        print('新的客户端链接', addr)
        while True:
            try:
                cmd = conn.recv(buffer_size)
                if not cmd:break # linux 下这样就行 因为linux会一直取值
                print('收到客户端的命令', cmd)
    
                res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                       stderr=subprocess.PIPE,
                                       stdout=subprocess.PIPE,
                                       stdin=subprocess.PIPE)
                err = res.stderr.read()
                if err: # 如果err不为空, cmd_res = err
                    cmd_res = err
                else:
                    cmd_res = res.stdout.read()
    
                conn.send(cmd_res) # 本身就是字节 发到客户端不需要编码
            except Exception as e:
                print(e)
                break
        conn.close()
    """
    server
    """
    from socket import *
    ip_port = ('127.0.0.1', 8080)
    back_log=5
    buffer_size = 1024
    
    tcp_client = socket(AF_INET,SOCK_STREAM)
    tcp_client.connect(ip_port)
    while True:
        cmd = input('>>: ').strip()
        if not cmd:continue # 如果命令为空,重新输入
        if cmd == 'quit':break
    
        tcp_client.send(cmd.encode('utf-8'))
        cmd_res = tcp_client.recv(buffer_size)
        print('命令的执行结果是', cmd_res.decode('gbk')) # 收到服务器返回 的编码是系统编码
    tcp_client.close()
    """
    client

    四、基于UDP套接字基本写法

    import socket
    # 1.创建udp对象
    sk = socket.socket(type=socket.SOCK_DGRAM)
    # 2.绑定地址端口号
    sk.bind( ("127.0.0.1",9000) )
    # 3.udp服务器,在一开始只能够接受数据
    msg,cli_addr = sk.recvfrom(1024)
    
    print(msg.decode())
    print(cli_addr)
    
    # 服务端给客户端发送数据
    msg = "我是你老娘,赶紧给我回家吃饭"
    sk.sendto(msg.encode(),cli_addr)
    
    # 4.关闭连接
    sk.close()
    udp-server
    import socket
    # 1.创建udp对象
    sk = socket.socket(type=socket.SOCK_DGRAM)
    
    # 2.收发数据的逻辑
    
    # 发送数据
    msg = "你好,你是mm还是gg"
    # sendto( 消息,(ip,端口号) )
    sk.sendto( msg.encode() ,  ("127.0.0.1",9000)  )
    
    # 接受数据
    msg,server_addr = sk.recvfrom(1024)
    print(msg.decode())
    print(server_addr)
    
    # 3.关闭连接
    sk.close()
    udp-client

    1、UDP循环写法

    import socket
    # 1.创建udp对象
    sk = socket.socket(type=socket.SOCK_DGRAM)
    # 2.绑定地址端口号
    sk.bind( ("127.0.0.1",9000) )
    # 3.udp服务器,在一开始只能够接受数据
    while True:
        # 接受消息
        msg,cli_addr = sk.recvfrom(1024)
        print(msg.decode())
        message = input("服务端给客户端发送的消息是?:")
        # 发送数据
        sk.sendto(message.encode() , cli_addr)
    
    
    # 4.关闭连接
    sk.close()
    udp-循环-server
    import socket
    # 1.创建udp对象
    sk = socket.socket(type=socket.SOCK_DGRAM)
    
    # 2.收发数据的逻辑
    while True:
        # 发送数据
        message = input("客户端给服务端发送的消息是?:")
        sk.sendto(message.encode(), ("127.0.0.1",9000) )
        
        # 接受数据
        msg,addr = sk.recvfrom(1024)
        print(msg.decode("utf-8"))
        
    
    # 3.关闭连接
    sk.close()
    udp-server-client1
    import socket
    # 1.创建udp对象
    sk = socket.socket(type=socket.SOCK_DGRAM)
    
    # 2.收发数据的逻辑
    while True:
        # 发送数据
        message = input("客户端给服务端发送的消息是?:")
        sk.sendto(message.encode(), ("127.0.0.1",9000) )
        
        # 接受数据
        msg,addr = sk.recvfrom(1024)
        print(msg.decode("utf-8"))
        
    
    # 3.关闭连接
    sk.close()
    udp-循环-client2

    五、tcp黏包

    """
    tcp协议在发送数据时,会出现黏包现象.    
        (1)数据粘包是因为在客户端/服务器端都会有一个数据缓冲区,
        缓冲区用来临时保存数据,为了保证能够完整的接收到数据,因此缓冲区都会设置的比较大。
        (2)在收发数据频繁时,由于tcp传输消息的无边界,不清楚应该截取多少长度
        导致客户端/服务器端,都有可能把多条数据当成是一条数据进行截取,造成黏包
    """

    1、黏包出现的两种情况

    """
    #黏包现象一:
        在发送端,由于两个数据短,发送的时间隔较短,所以在发送端形成黏包
    #黏包现象二:
        在接收端,由于两个数据几乎同时被发送到对方的缓存中,所有在接收端形成了黏包
    #总结:
        发送端,包之间时间间隔短 或者 接收端,接受不及时, 就会黏包
        核心是因为tcp对数据无边界截取,不会按照发送的顺序判断
    """

    1.1、发送端产生黏包

    # 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小,会合到一起,产生粘包
    __author__ = 'kxq'
    from socket import *
    ip_port=('127.0.0.1',8080)
    
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    
    conn,addr=tcp_socket_server.accept()
    
    
    data1=conn.recv(10)
    data2=conn.recv(10)
    
    print('----->',data1.decode('utf-8'))
    print('----->',data2.decode('utf-8'))
    
    conn.close()
    服务器
    __author__ = 'kxq'
    import socket
    BUFSIZE=1024
    ip_port=('127.0.0.1',8080)
    
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    res=s.connect_ex(ip_port)
    
    
    s.send('hello'.encode('utf-8'))
    s.send('kxq'.encode('utf-8'))
    客户端
    # 结果
    #-----> hellokxq
    #-----> 

    1.2、接收方产生黏包

    # 接收方不及时接收缓冲区的包,造成多个包接收
    #(客户端发送了一段数据,服务端只收了一小部分,
    # 服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
    __author__ = 'kxq'
    from socket import *
    ip_port=('127.0.0.1',8080)
    
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    
    conn,addr=tcp_socket_server.accept()
    
    
    data1=conn.recv(2) #一次没有收完整
    data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的
    
    print('----->',data1.decode('utf-8'))
    print('----->',data2.decode('utf-8'))
    
    conn.close()
    服务器
    __author__ = 'kxq'
    import socket
    BUFSIZE=1024
    ip_port=('127.0.0.1',8080)
    
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(ip_port)
    
    
    s.send('hello kxq'.encode('utf-8'))
    View Code
    """
    udp为什么不会产生粘包
    udp没有建立连接发送100字节,接收80字节,就把20子节点包丢弃了
    udp采用链式结构来建立每一个到达的udp包,在每个udp包中就有了消息头(消息来源地址,端口等信息)
    即面向消息的通信是有消息保护边界的
    先读消息头,后面就是消息,再后面就又是消息头,所以有边界
    '''
    '''
    tcp是面向流的,不知道字节流的起始位置、终止位置,就不知道收多少数据,就产生了粘包
    产生粘包解决 udp有消息头,发消息前指定消息头可以封装一个消息长度
    """

    2、黏包对比:tcp和udp

    """
    #tcp协议:
    缺点:接收时数据之间无边界,有可能粘合几条数据成一条数据,造成黏包 
    优点:不限制数据包的大小,稳定传输不丢包
    
    #udp协议:
    优点:接收时候数据之间有边界,传输速度快,不黏包
    缺点:限制数据包的大小(受带宽路由器等因素影响),传输不稳定,可能丢包
    
    #tcp和udp对于数据包来说都可以进行拆包和解包,理论上来讲,无论多大都能分次发送
    但是tcp一旦发送失败,对方无响应(对方无回执),tcp可以选择再发,直到对应响应完毕为止
    而udp一旦发送失败,是不会询问对方是否有响应的,如果数据量过大,易丢包
    """

    3、解决黏包问题

    """
    #解决黏包场景:
        应用场景在实时通讯时,需要阅读此次发的消息是什么
    #不需要解决黏包场景:
        下载或者上传文件的时候,最后要把包都结合在一起,黏包无所谓.
    """

    4.1、low版解决黏包

    # 因为黏包无边界,那么我们给它设置一个边界
    """
    服务器端先告诉客户端我要发几个字节,客户端接收一个字节,数据为服务器
    要发送的字节大小
    然后服务器与客户端再做具体数据传输
    """
    import time
    import socket
    sk = socket.socket()
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    sk.bind( ("127.0.0.1",9000) )
    sk.listen()
    
    conn,addr = sk.accept()
    
    
    # 处理收发数据的逻辑
    # 先发送接下来要发送数据的大小
    conn.send("5".encode())
    # 发完长度之后,再发数据
    conn.send("hello".encode())
    conn.send(",world".encode())
    
    conn.close()
    sk.close()
    黏包-server
    """
    黏包出现的两种情况:
        (1) 发送端发送数据太快
        (2) 接收端接收数据太慢
    """
    import socket
    import time
    sk = socket.socket()
    sk.connect( ("127.0.0.1",9000) )
    
    time.sleep(2)
    # 处理收发数据的逻辑
    # 先接收接下来要发送数据的大小
    res = sk.recv(1)
    num = int(res.decode())
    # 接受num这么多个字节数
    res1 = sk.recv(num)
    res2 = sk.recv(1024)
    print(res1)
    print(res2)
    
    
    sk.close()
    黏包-client

    4.2、升级版

    """
    由于4.1版本将发送的数据大小写死了,
    如果服务器要发5个字节 send(5.encode()),客户端就接收1个,recv(1)再转为int
    如果服务器要发10个字节 send(10.encode()),客户端就接收2个,recv(2)再转为int
    
    升级版我们固定发送8位 send("00000100".encode()),客户端也就接收8个,
    recv(8),再转int
    """
    import time
    import socket
    sk = socket.socket()
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    sk.bind( ("127.0.0.1",9000) )
    sk.listen()
    
    conn,addr = sk.accept()
    
    
    # 处理收发数据的逻辑
    # 先发送接下来要发送数据的大小
    conn.send("00000100".encode())
    # 发完长度之后,再发数据
    msg = "hello" * 20
    conn.send(msg.encode())
    conn.send(",world".encode())
    
    conn.close()
    sk.close()
    黏包-server
    """
    黏包出现的两种情况:
        (1) 发送端发送数据太快
        (2) 接收端接收数据太慢
    """
    import socket
    import time
    sk = socket.socket()
    sk.connect( ("127.0.0.1",9000) )
    
    time.sleep(2)
    # 处理收发数据的逻辑
    # 先接受接下来要发送数据的大小
    res = sk.recv(8)
    num = int(res.decode())
    # 接受num这么多个字节数
    res1 = sk.recv(num)
    res2 = sk.recv(1024)
    print(res1)
    print(res2)
    
    
    sk.close()
    黏包-client

    4.3、终极版

    """
    4.2 也有限制,总之4.2是限制死了一次传输字节大小的位数
    """
    # 引用 struct模块
    """
    pack : 
        把任意长度数字转化成具有固定4个字节长度的字节流
    unpack :
        把4个字节值恢复成原来的数字,返回最终的是元组;
    """
    import struct
    # pack
    # i => int 要转化的当前数据是整型
    res = struct.pack("i",999999999)
    res = struct.pack("i",1)
    res = struct.pack("i",4399999)
    # pack 的范围 -2147483648 ~ 2147483647 21个亿左右
    res = struct.pack("i",2100000000)
    print(res , len(res))
    
    
    # unpack
    # i => 把对应的数据转换成int整型
    tup = struct.unpack("i",res)
    print(tup) # (2100000000,)
    print(tup[0])
    struct模块简单示例
    import time
    import socket
    import struct
    sk = socket.socket()
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    sk.bind( ("127.0.0.1",9000) )
    sk.listen()
    
    conn,addr = sk.accept()
    
    
    # 处理收发数据的逻辑
    strvar = input("请输入你要发送的数据")
    msg = strvar.encode()
    length = len(msg)
    res = struct.pack("i",length)
    
    # 第一次发送的是字节长度
    conn.send(res)
    
    # 第二次发送真实的数据
    conn.send(msg)
    
    # 第三次发送真实的数据
    conn.send("世界真美好123".encode())
    
    
    
    conn.close()
    sk.close()
    黏包-serverr
    """
    黏包出现的两种情况:
        (1) 发送端发送数据太快
        (2) 接收端接收数据太慢
    """
    import socket
    import time
    import struct
    sk = socket.socket()
    sk.connect( ("127.0.0.1",9000) )
    
    time.sleep(2)
    # 处理收发数据的逻辑
    
    # 第一次接受的是字节长度
    n = sk.recv(4)
    tup = struct.unpack("i",n)
    n = tup[0]
    
    
    # 第二次接受真实的数据
    res = sk.recv(n)
    print(res.decode())
    
    # 第三次接受真实的数据
    res = sk.recv(1024)
    print(res.decode())
    sk.close()
    黏包-client

    六、问题

    # 如果客户端发送的数据是空,他没有校验是否为空,会不会收到服务器的相应
    """
    tcp_client.send(msg.encode('utf-8')) 
    发消息是向自己的内核态中的缓存里添加数据,现在数据是空,不会发到服务端,
    服务端收消息也是在自己的缓冲区取数据
    客户端在执行
    tcp_client.recv(buffer_size)时没有数据,就会卡在这里
    
    解决:
    就是加一个判断
    """
    # 断开客户端,服务器端也断开了
    """
    做一个异常判断
    """
    # 运行两个客户端,客户端发送数据‘你好’可以相应,客户端1发送数据会卡住,为什么
    """
    客户端发送数据:在第一层while True 会拿到一个客户端的链接conn,嵌套的while True进行通讯,
    通讯之后服务端就一直在第二层while True
    客户端1发送数据:因为ip_port定义的挂起链接数是5,所以执行了客户端1的connect,这个链接请求已经
    到了服务端的半连接池里,走到了tcp_client.send 发消息打印‘客户端已经发送消息’,卡在了recv,因为
    自己的缓冲区是空的,没有完成三次链接,没回消息,因为还在和客户端在连接
    客户端断开连接后,客户端1就可以通信了,服务器就会回客户端1之前发送的消息,自己缓冲区有数据了,
    客户端1就会取到了
    """
    # 端口被占用
    """
    tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    """
    # send 和 sendall
    """
    send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,
    如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,
    用sendall就会循环调用send,数据不会丢失
    """
  • 相关阅读:
    JSP中的选择判断 C 标签 一般与 foreach循环一块使用
    python基础学习笔记(四)
    python基础学习笔记(五)
    python基础学习笔记(六)
    python基础学习笔记(三)
    python基础学习笔记(一)
    selenium使用Xpath定位之完整篇
    Selenium Python FirefoxWebDriver处理打开保存对话框
    Selenium操作示例——鼠标悬停显示二级菜单,再点击二级菜单或下拉列表
    Selenium+Python:下载文件(Firefox 和 Chrome)
  • 原文地址:https://www.cnblogs.com/kongxiangqun/p/13511181.html
Copyright © 2020-2023  润新知