# python网络编程

***

## 网络编程

> socket和urllib的关系

提供对多种不同类型套接字的低级访问，您可以使用这些套接字通过任何端口和协议进行通信。例如，您可以将其用于电子邮件、SSH、远程桌面等，也可以用于侦听端口（对于服务器）。几乎所有Python网络库，包括`urllib`，都以某种方式使用`socket`。在`urllib`专门用于套接字的特定用途，即HTTP（和可选的TLS）和FTP协议的客户端，通常（但不总是）使用端口80、443或21。

> [Python urllib、urllib2、urllib3用法及区别](https://blog.csdn.net/jiduochou963/article/details/87564467)

urllib、urllib2是老版本，urllib3是新版本，requests是基于urllib3写的。其中urllib和urllib2是内置库

模块urllib和urllib2的功能差不多，简单来说urllib2是urllib的增强——urllib2更好一些，但是urllib中有urllib2中所没有的函数。对于简单的下载， urllib绰绰有余。 如果需要实现HTTP身份验证或Cookie亦或编写扩展来处理自己的协议，urllib2可能是更好的选择。在Python2.x中主要为urllib和urllib2，这**两个标准库是不可相互替代**的。但是**在Python3.x中将urllib2合并到了urllib**，这一点值得注意。

* urllib支持设置编码的函数urllib.urlencode，在模拟登陆的时候经常需要传递经过post编码之后的参数，如果不想使用第三方库完成模拟登录，就必须使用到标准库中的urllib。urllib提供一些比较原始基础的方法而urllib2并没有，比如urllib中的urlencode方法用来GET查询字符串的产生。
* urllib2比较有优势的地方在于urllib2.openurl中可以接受一个Request类的实例来设置Request参数，来修改/设置Header头从而达到控制HTTP Request的header部分的目的，也可以修改用户代理,设置cookie等，但urllib仅可以接受URL。如果你访问一个网站想更改User Agent(可以伪装你的浏览器)，你就需要使用urllib2。urllib2模块没有加入urllib.urlretrieve函数以及urllib.quote等一系列quote和unquote功能，这个时候就需要urllib的辅助。

## socket

> [网络7层协议，4层，5层？理清容易混淆的几个概念](https://blog.csdn.net/cc1949/article/details/79063439) [爬虫遇到 Socket，莫慌，肝就完了！](https://blog.51cto.com/u_15023272/2558327) [Python3中的SocketServer](https://www.cnblogs.com/ellisonzhang/p/10430118.html)

> socket是应用层与TCP/IP协议族通信的中间软件抽象层，它是一组接口。

在设计模式中，Socket其实就是一个门面模式，它把复杂的TCP/IP协议族隐藏在Socket接口后面，对用户来说，一组简单的接口就是全部，让Socket去组织数据，以符合指定的协议。

![img](https://pic.hycbook.com/i/hexo/bk_resources/python/22.python%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/SouthEast.webp) **Socke**t 被称为套接字，是对 \*\*TCP/IP \*\*协议的封装，它是传输层和应用层间的抽象层 相比 **HTTP** 的短连接通信方式，Socket 可实现客户端和服务器的长连接通信 **Fiddler、Charles** 只能抓取应用层的数据，如果你想抓其他层，比如：网络层、传输层、数据链路层的数据，强烈建议使用：**Wireshark**

在标准的OIS模型中并没有规定说必须有socket层，也就是说不使用socket也能完成通讯，是的，的确如此 那为什么需要socket呢？一个字**懒**，程序员都是懒的 我们发现还没有开始实现应用程序逻辑，就需要花大把时间来实现各种协议，太费事了，就有人专门把协议中一堆复杂的事情进行了封装，于是socket就诞生了 有了socket以后，无需自己编写代码实现三次握手，四次挥手，ARP请求，打包数据等等，socket已经封装好了，只需要遵循socket的规定去编程，写出的程序自然就是遵循tcp/udp标准的

> socket的发展

套接字起源于**20世纪70年代**加利福尼亚大学伯克利分校版本的 Unix，即人们所说的 BSD Unix 因此，有时人们也把套接字称为“伯克利套接字”或“BSD 套接字” 一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯，这也被称进程间通讯或 IPC **套接字有两种**(或者称为有两个种族)，分别是基于文件型的和基于网络型的

1.  **基于文件类型的套接字家族**：AF\_UNIX

    unix一切皆文件，基于文件的套接字调用的就是底层的文件系统来取数据，两个套接字进程运行在同一机器，可以通过访问同一个文件系统间接完成通信
2.  **基于网络类型的套接字家族**：AF\_INET

    还有AF\_INET6被用于ipv6，还有一些其他的地址家族，不过，他们要么是只用于某个平台，要么就是已经被废弃，或者是很少被使用，或者是根本没有实现，所有地址家族中，AF\_INET是使用最广泛的一个，python支持很多种地址家族，但是由于大部通讯都是网络通讯，所以大部分时候使用AF\_INET

> socket示例

```python
# 1.导入socket模块
import socket
# 2.创建socket对象 函数定义如下
socket.socket(socket_family,socket_type,protocal=0)
#socket_family 可以是 AF_UNIX 或 AF_INET。
#socket_type 可以是 SOCK_STREAM表示TCP协议 或 SOCK_DGRAM表示UDP协议。
#protocol 一般不填,默认值为 0。
   
# 2.1获取TCP 套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 或者 后面的参数都有默认值,可以不写,默认创建的是TCP协议socket
tcpSock = socket.socket()

# 2.2获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
```

> 服务端套接字函数

| 服务端套接字函数   | 含义                        |
| ---------- | ------------------------- |
| s.bind()   | 绑定(主机,端口号)到套接字            |
| s.listen() | 开始TCP监听                   |
| s.accept() | 被动接受TCP客户的连接,(阻塞式)等待连接的到来 |

> 客户端套接字函数

| 客户端套接字函数        | 含义                                |
| --------------- | --------------------------------- |
| s.connect()     | 主动初始化TCP服务器连接                     |
| s.connect\_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |

> 公共用途的套接字函数

| 公共用途的套接字函数      | 含义                                                                               |
| --------------- | -------------------------------------------------------------------------------- |
| s.recv()        | 接收TCP数据                                                                          |
| s.send()        | 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)                                       |
| s.sendall()     | <p>发送完整的TCP数据(本质就是循环调用send<br>sendall在待发送数据量大于己端缓存区剩余空间时，数据不丢失，循环调用send直到发完)</p> |
| s.recvfrom()    | 接收UDP数据                                                                          |
| s.sendto()      | 发送UDP数据                                                                          |
| s.getpeername() | 连接到当前套接字的远端的地址                                                                   |
| s.getsockname() | 当前套接字的地址                                                                         |
| s.getsockopt()  | 返回指定套接字的参数                                                                       |
| s.setsockopt()  | 设置指定套接字的参数                                                                       |
| s.close()       | 关闭套接字                                                                            |

> 面向锁的套接字方法

| 面向锁的套接字方法       | 含义             |
| --------------- | -------------- |
| s.setblocking() | 设置套接字的阻塞与非阻塞模式 |
| s.settimeout()  | 设置阻塞套接字操作的超时时间 |
| s.gettimeout()  | 得到阻塞套接字操作的超时时间 |

### TCP通讯流程

> TCP的通讯流程与打电话的过程非常相似

1. 买手机 == socket()
2. 装进手机卡 == bind()
3. 待机 == listen()
4. 电话来了、接受通话 == accept()
5. 听 == read()
6. 说 == write()
7. 挂电话 == close()

> TCP服务端

```python
import socket

ip_port = ('127.0.0.1', 9000)  # 电话卡
BUFSIZE = 1024  # 收发消息的尺寸
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 买手机
s.bind(ip_port)  # 手机插卡
s.listen(5)  # 手机待机
conn, addr = s.accept()  # 手机接电话
print('接到来自%s的电话' % addr[0])
msg = conn.recv(BUFSIZE)  # 听消息,听话
print(msg, type(msg))
conn.send(msg.upper())  # 发消息,说话
conn.close()  # 挂电话
s.close()  # 手机关机
```

> TCP客户端

```python
import socket

ip_port = ('127.0.0.1', 9000)
BUFSIZE = 1024
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect_ex(ip_port)  # 拨电话
s.send('linhaifeng nb'.encode('utf-8'))  # 发消息,说话(只能发送字节类型)
feedback = s.recv(BUFSIZE)  # 收消息,听话
print(feedback.decode('utf-8'))
s.close()  # 挂电话
```

注意TCP中必须先启动服务器再启动客户端,否则客户端由于无法链接服务器,直接报错!

如上就完成了一个最基本的TCP通讯，但是建立是为了传输数据，二传输数据很多时候并不是一次性就传输完成了，需要多次收发过程，所以需要给代码加上循环

> 改进版服务器端

```python
import socket
ip_port=('127.0.0.1',8081)#电话卡
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
s.bind(ip_port) #手机插卡
s.listen(5)     #手机待机
while True:                         #新增接收链接循环,可以不停的接电话
    conn,addr=s.accept()            #手机接电话
    # print(conn)
    # print(addr)
    print('接到来自%s的电话' �dr[0])
    while True:                         #新增通信循环,可以不断的通信,收发消息
        msg=conn.recv(BUFSIZE)             #听消息,听话
        print(msg,type(msg))
        conn.send(msg.upper())          #发消息,说话
    conn.close()                    #挂电话
s.close()                       #手机关机
```

> 改进版客户端

```python
import socket
ip_port=('127.0.0.1',8081)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect_ex(ip_port)           #拨电话
while True:                             #新增通信循环,客户端可以不断发收消息
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    s.send(msg.encode('utf-8'))         #发消息,说话(只能发送字节类型)
   
    feedback=s.recv(BUFSIZE)                           #收消息,听话
    print(feedback.decode('utf-8'))
s.close()                                       #挂电话
```

### 基于UDP的socket

> UDP通讯流程与对讲机非常类似(由于不需要建立连接所以省去TCP的listen()和accept()这两步)

1. 买传呼机 == socket()
2. 固定对讲频道 == bind()
3. 收信号 == recvfrom()
4. 发信号 == sendto()

> UDP服务器端

```python
import socket
ip_port=('127.0.0.1',9000) # 固定通讯频道
BUFSIZE=1024
#在TCP中socket的初始化参数可以省略, 因为默认创建的就是TCP协议的socket
#而UDP则必须手动指定相关参数
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 买对讲机
udp_server_client.bind(ip_port)
while True:
    msg,addr=udp_server_client.recvfrom(BUFSIZE) #收信息
    print(msg,addr)
    udp_server_client.sendto(msg.upper(),addr) # 发信息
```

> UDP客户端

```python
import socket
ip_port=('127.0.0.1',9000) #确定通讯频道
BUFSIZE=1024
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 买对讲机
while True:
    msg=input('>>: ').strip()
    if not msg:continue
    udp_server_client.sendto(msg.encode('utf-8'),ip_port) # 发消息
    back_msg,addr=udp_server_client.recvfrom(BUFSIZE) #收消息
```

udp是无链接的，先启动哪一端都不会报错，即使对方地址根本不存在也不会报错，强制关闭任何一方也没有任何问 另外，由于无连接的特点，服务器不需要针对摸个客户端进行循环，只要循环的接收即可，谁发来的消息都可以被处理，基于这个特点我们可以编写一个UDP程序，实现多个客户端同时与服务器通讯

> UDP聊天服务器

```python
import socket
ip_port=('127.0.0.1',8081)
udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #买手机
udp_server_sock.bind(ip_port)
while True:
    qq_msg,addr=udp_server_sock.recvfrom(1024)
    print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8')))
    back_msg=input('回复消息: ').strip()
    udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
```

> UDP聊天客户端

```python
import socket

BUFSIZE = 1024
udp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
qq_name_dic = {
    '狗哥': ('127.0.0.1', 8081),
    '天线宝宝': ('127.0.0.1', 8081),
    '巴拉巴拉小魔女': ('127.0.0.1', 8081),
    '王尼玛': ('127.0.0.1', 8081),
}
while True:
    qq_name = input('请选择聊天对象: ').strip()
    while True:
        msg = input('请输入消息,回车发送: ').strip()
        if msg == 'quit': break
        if not msg or not qq_name or qq_name not in qq_name_dic: continue
        udp_client_socket.sendto(msg.encode('utf-8'), qq_name_dic[qq_name])
        back_msg, addr = udp_client_socket.recvfrom(BUFSIZE)
        print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' % (addr[0], addr[1], back_msg.decode('utf-8')))
udp_client_socket.close()
```

### 粘包问题

粘包指的是数据与数据之间没有明确的分界线，导致不能正确读取数据！ 要理解粘包问题，需要先了解TCP协议传输数据时的具体流程，TCP协议也称之为流式协议(UDP称为数据报协议) 应用程序无法直接操作硬件，应用程序想要发送数据则必须将数据交给操作系统，而操作系统需要需要同时为所有应用程序提供数据传输服务，也就意味着，操作系统不可能立马就能将应用程序的数据发送出去，就需要为应用程序提供一个缓冲区，用于临时存放数据，具体流程如下：

1. 发送方：当应用程序调用send函数时，应用程序会将数据从应用程序拷贝到操作系统缓存，再由操作系统从缓冲区读取数据并发送出去
2. 接收方：对方计算机收到数据也是操作系统先收到，至于应用程序何时处理这些数据，操作系统并不清楚，所以同样需要将数据先存储到操作系统的缓冲区中，当应用程序调用recv时，实际上是从操作系统缓冲区中将数据拷贝到应用程序的过程 上述过程对于TCP与UDP都是相同的不同之处在于：
   * UDP: UDP在收发数据时是基于数据包的，即一个包一个包的发送，包与包之间有着明确的分界，到达对方操作系统缓冲区后也是一个一个独立的数据包，接收方从操作系统缓冲区中将数据包拷贝到应用程序 这种方式存在的问题：
     1. 发送方发送的数据长度每个操作系统会有不同的限制，数据超过限制则无法发送
     2. 接收方接收数据时如果应用程序的提供的缓存容量小于数据包的长度将造成数据丢失，而缓冲区大小不可能无限大
   * TCP: 当我们需要传输较大的数据，或需要保证数据完整性时，最简单的方式就是使用TCP协议了 与UDP不同的是，TCP增加了一套校验规则来保证数据的完整性，会将超过TCP包最大长度的数据拆分为多个TCP包 并在传输数据时为每一个TCP数据包指定一个顺序号，接收方在收到TCP数据包后按照顺序将数据包进行重组，重组后的数据全都是二进制数据，且每次收到的二进制数据之间没有明显的分界

> 基于这种工作机制TCP在三种情况下会发送粘包问题

1. 当单个数据包较小时接收方可能一次性读取了多个包的数据
2. 当整体数据较大时接收方可能一次仅读取了一个包的一部分内容
3. 另外TCP协议为了提高效率，增加了一种优化机制，会将数据较小且发送间隔较短的数据合并发送，该机制也会导致发送方将两个数据包粘在一起发送

#### 粘包的解决方案

> 解决方案：在发送数据前先发送数据长度

上述方案看起来解决了粘包问题，但是由于negle优化机制的存在，长度信息和数据还是有可能会粘包，而接受方并不知道长度信息具体几个字节，所以现在的问题是如何能够长度信息做成一个固定长度的bytes数据 我们可以将字符串拼接为一个固定长度的字符 但是这样太麻烦，struct模块为我们提供了一个功能，可以将整数类型转换为固定长度的bytes，此时就派上用场了

#### 自定义报头解决粘包

上述方案已经完美解决了粘包问题，但是扩展性不高，例如我们要实现文件上传下载，不光要传输文件数据，还需要传输文件名字，md5值等等，如何能实现呢，具体思路：

> 发送端：

1. 先将所有的额外信息打包到一个头中
2. 然后先发送头部数据
3. 最后发送真实数据

> 接收端：

1. 接收固定长度的头部长度数据
2. 根据长度数据获取头部数据
3. 根据头部数据获取真实数据

> 客户端：

```python
import socket
import struct
import json
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
    cmd = input(">>>:").strip()
    c.send(cmd.encode("utf-8"))
 
    # 头部数据
    data = c.recv(4)
    head_length = struct.unpack("i",data)[0]
    head_data = c.recv(head_length).decode("utf-8")
    head = json.loads(head_data)
    print(head)
    # 真实数据长度
    data_length = head["data_size"]
    #接收真实数据
    size = 0
    res = b""
    while size < data_length:
        temp = c.recv(1024)
        size += len(temp)
        res += temp
    print(res.decode("gbk"))
```

> 服务器：

```python
import socket
import subprocess
import struct
import json
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()
while True:
    client, addr = server.accept()
    while True:
        cmd = client.recv(1024).decode("utf-8")
        p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
  
        # 真实数据
        data = p.stdout.read() + p.stderr.read()
       
        # 头部数据
        head = {"data_size":len(data),"额外信息":"额外的值"}
        head_data = json.dumps(head).encode("utf-8")
        #头部长度
        head_len = struct.pack("i",len(head_data))
        #逐个发送
        client.send(head_len)
        client.send(head_data)
        client.send(data)
```

## urllib

> [urllib—URL handling modules](https://docs.python.org/3/library/urllib.html)
>
> [python爬虫从入门到放弃（三）之 Urllib库的基本使用](https://www.cnblogs.com/zhaof/p/6910871.html)

### 基本概述

> urllib是python内置的HTTP请求库，包括以下模块

* urllib.request: 最基本的HTTP请求模块，用来发起请求，就和人们在浏览器上输入网址来访问网页一样
* urllib.error: 异常处理模块，如果在请求时出现错误，用这个模块来抓住异常，保证程序不会因为抛出异常而挂掉
* urllib.parse url: 工具模块，提供了许多URL处理方法，比如URL的拆分、合并等等
* urllib.robotparser: 用来识别目标网站的robot.txt文件(基本用不上)

### 重要模块

#### urlopen

关于urllib.request.urlopen参数的介绍：

```python
urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False, context=None)
```

> 请求

urlopen一般常用的有三个参数，它的参数如下：urllib.requeset.urlopen(url,data,timeout)

```python
import urllib.request
# Get请求
response = urllib.request.urlopen('http://www.baidu.com')
# response.read()可以获取到网页的内容
print(response.read().decode('utf-8'))

# Post请求
import urllib.parse
import urllib.request
# 通过bytes(urllib.parse.urlencode())可以将post数据进行转换放到urllib.request.urlopen的data参数中。这样就完成了一次post请求
data = bytes(urllib.parse.urlencode({'word': 'hello'}), encoding='utf8')
print(data)
response = urllib.request.urlopen('http://httpbin.org/post', data=data)
print(response.read())

# Timeout参数
# 在某些网络情况不好或者服务器端异常的情况会出现请求慢的情况，或者请求异常
# 所以这个时候我们需要给请求设置一个超时时间，而不是让程序一直在等待结果
response = urllib.request.urlopen('http://httpbin.org/get', timeout=1)
print(response.read())
```

如果我们添加data参数的时候就是以post请求方式请求，如果没有data参数就是get请求方式

> 响应

通过response.status、response.getheaders().response.getheader("server")，获取状态码以及头部信息response.read()获得的是响应体的内容

```python
print(response.status)
200
print(response.getheaders())
[('Connection', 'close'), ('Content-Length', '49589'), ('Server', 'nginx'), ('Content-Type', 'text/html; charset=utf-8'), ('X-Frame-Options', 'DENY'), ('Via', '1.1 vegur, 1.1 varnish, 1.1 varnish'), ('Accept-Ranges', 'bytes'), ('Date', 'Wed, 18 Aug 2021 07:37:14 GMT'), ('Age', '1496'), ('X-Served-By', 'cache-bwi5120-BWI, cache-nrt18321-NRT'), ('X-Cache', 'HIT, HIT'), ('X-Cache-Hits', '2, 1285'), ('X-Timer', 'S1629272234.003034,VS0,VE0'), ('Vary', 'Cookie'), ('Strict-Transport-Security', 'max-age=63072000; includeSubDomains')]
print(response.getheader("server"))
nginx
print(response.read())
响应体的内容
```

#### request

上述的urlopen只能用于一些简单的请求，因为它无法添加一些header信息，如果后面写爬虫我们可以知道，很多情况下我们是需要添加头部信息去访问目标站的，这个时候就用到了urllib.request urllib中，request这个模块主要负责构造和发起网络请求，并在其中加入Headers、Proxy等。利用它可以模拟浏览器的一个请求发起过程

> 设置Headers

有很多网站为了防止程序爬虫爬网站造成网站瘫痪，会需要携带一些headers头部信息才能访问

最长见的有user-agent参数

```python
from urllib import request, parse

# 方式一
url = 'http://httpbin.org/post'
headers = {
    'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)',
    'Host': 'httpbin.org'
}
dict = {'name': 'hyc'}
data = bytes(parse.urlencode(dict), encoding='utf8')
req = request.Request(url=url, data=data, headers=headers, method='POST')
response = request.urlopen(req)

# 方式二
data = bytes(parse.urlencode(dict), encoding='utf8')
req = request.Request(url=url, data=data, method='POST')
# 好处是自己可以定义一个请求头字典，然后循环进行添加
req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')
response = request.urlopen(req)
```

> urlretrieve下载文件

urlretrieve()方法直接将远程数据下载到本地。

```python
urlretrieve(url, filename=None, reporthook=None, data=None)
```

1. **url**：下载链接地址
2. **filename**：指定了保存本地路径(如果参数未指定，urllib会生成一个临时文件保存数据)
3. **reporthook**：是一个回调函数，当连接上服务器、以及相应的数据块传输完毕时会触发该回调，我们可以利用这个回调函数来显示当前的下载进度
4. **data**：指post导服务器的数据，该方法返回一个包含两个元素的(filename, headers) 元组，filename 表示保存到本地的路径，header表示服务器的响应头

```python
import os
from six.moves import urllib
import sys

DATA_URL = 'http://www.python.org/ftp/python/2.7.5/Python-2.7.5.tar.bz2'
filename = DATA_URL.split('/')[-1]

def _progress(block_num, block_size, total_size):
    '''回调函数
       @block_num: 已经下载的数据块
       @block_size: 数据块的大小
       @total_size: 远程文件的大小
    '''
    sys.stdout.write('\r>> Downloading %s %.1f%%' % (filename,
                     float(block_num * block_size) / float(total_size) * 100.0))
    sys.stdout.flush()

filepath, _ = urllib.request.urlretrieve(DATA_URL, filename, _progress)
```

#### handler

> ProxyHandler

通过rulllib.request.ProxyHandler()可以设置代理,网站它会检测某一段时间某个IP 的访问次数，如果访问次数过多，它会禁止你的访问,所以这个时候需要通过设置代理来爬取数据

```python
import urllib.request

proxy_handler = urllib.request.ProxyHandler({
    'http': 'http://127.0.0.1:9743',
    'https': 'https://127.0.0.1:9743'
})
opener = urllib.request.build_opener(proxy_handler)
response = opener.open('http://httpbin.org/get')
print(response.read())
```

> HTTPCookiProcessor

cookie中保存中我们常见的登录信息，有时候爬取网站需要携带cookie信息访问,这里用到了http.cookijar，用于获取cookie以及存储cookie

```python
import http.cookiejar, urllib.request
# 方式一：直接打印
cookie = http.cookiejar.CookieJar()
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('http://www.baidu.com')
for item in cookie:
    print(item.name+"="+item.value)
    
# 方式二：MozillaCookieJar()方式
import http.cookiejar, urllib.request
filename = "cookie.txt"
cookie = http.cookiejar.MozillaCookieJar(filename)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('http://www.baidu.com')
cookie.save(ignore_discard=True, ignore_expires=True)

# 方式三：LWPCookieJar()方式
import http.cookiejar, urllib.request
filename = 'cookie.txt'
cookie = http.cookiejar.LWPCookieJar(filename)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('http://www.baidu.com')
cookie.save(ignore_discard=True, ignore_expires=True)

# 读取cookie
import http.cookiejar, urllib.request
cookie = http.cookiejar.LWPCookieJar()
cookie.load('cookie.txt', ignore_discard=True, ignore_expires=True)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('http://www.baidu.com')
print(response.read().decode('utf-8'))
```

#### except

urllib库实现了三个异常类：

1. URLError里只有一个属性：reason,即抓异常的时候只能打印错误信息
2. HTTPError里有三个属性：code,reason,headers
3. ContentTooShortError里有三个属性：reason,content

```python
from urllib import request,error
try:
    response = request.urlopen("http://pythonsite.com/1111.html")
except error.HTTPError as e:
    print(e.reason)
    print(e.code)
    print(e.headers)
except error.URLError as e:
    print(e.reason)
    # e.reason其实也可以在做深入的判断
    if isinstance(e.reason,socket.timeout):
        print("time out")

else:
    print("reqeust successfully")
```

### url parse相关

#### urlparse

```python
# urllib.parse.urlparse(urlstring, scheme='', allow_fragments=True)

from urllib.parse import urlparse

result = urlparse("http://www.baidu.com/index.html;user?id=5#comment")
print(result)
```

#### urlunpars

功能和urlparse的功能相反，它是用于拼接

```python
from urllib.parse import urlunparse

data = ['http','www.baidu.com','index.html','user','a=123','commit']
print(urlunparse(data))
```

#### urljoin

这个的功能其实是做拼接的

```python
from urllib.parse import urljoin

print(urljoin('http://www.baidu.com', 'FAQ.html'))
print(urljoin('http://www.baidu.com', 'https://pythonsite.com/FAQ.html'))
print(urljoin('http://www.baidu.com/about.html', 'https://pythonsite.com/FAQ.html'))
print(urljoin('http://www.baidu.com/about.html', 'https://pythonsite.com/FAQ.html?question=2'))
print(urljoin('http://www.baidu.com?wd=abc', 'https://pythonsite.com/index.php'))
print(urljoin('http://www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com#comment', '?category=2'))

# 从拼接的结果我们可以看出，拼接的时候后面的优先级高于前面的url
http://www.baidu.com/FAQ.html
https://pythonsite.com/FAQ.html
https://pythonsite.com/FAQ.html
https://pythonsite.com/FAQ.html?question=2
https://pythonsite.com/index.php
http://www.baidu.com?category=2#comment
www.baidu.com?category=2#comment
www.baidu.com?category=2
```

#### urlencode

这个方法可以将字典转换为url参数

```python
from urllib.parse import urlencode

params = {"name":"hyc", "age":28}
base_url = "http://www.baidu.com?"
url = base_url+urlencode(params)
print(url)
```

### cookie

登录后，得到cookie，带着这个cookie可以访问登录后的内容

```python
#!/usr/bin/env Python
# -- coding: utf-8 --

"""
@version: v1.0
@author: huangyc
@file: cookie_test.py
@Description: 
@time: 2022/11/3 11:34
"""
import urllib.parse
import urllib.parse
import urllib.request
import urllib.request
from http import cookiejar


def cookie_test():
    # 定义请求头
    headers = {"content-type": "application/x-www-form-urlencoded",
               "Users-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"}

    # 登录的网址
    login_url = "https://xx.hycbook.com/xx.php"

    # 登录的参数
    login_form_data = {"fm_usr": "xxx", "fm_pwd": "xxx"}
    # 使用urllib.request时  post方法所携带的参数不能是字典形式
    login_form_data_final = urllib.parse.urlencode(login_form_data)

    # 发送登录请求POST, 自动保存cookie
    cook_jar = cookiejar.MozillaCookieJar()
    # 定义有添加 cookie功能的处理器
    cook_handler = urllib.request.HTTPCookieProcessor(cook_jar)

    # 根据处理器生成opener
    opener = urllib.request.build_opener(cook_handler)

    # 此时headers里面还没有cookie, 还没有登录
    login_request = urllib.request.Request(login_url, headers=headers, data=login_form_data_final.encode("utf-8"))
    # 如果登录成功. cookjar自动保存cookie, opener里面有cookjar, 所以opener里有cookie
    resp = opener.open(login_request)
    res_text = resp.read().decode('utf-8')
    cook_jar.save("cookie.txt")

    # 创建管理器
    cookie_handler = urllib.request.HTTPCookieProcessor(cook_jar)
    http_handler = urllib.request.HTTPHandler()
    https_handler = urllib.request.HTTPSHandler()

    # 创建请求求管理器, 此处将登录过的cookie带进去
    opener = urllib.request.build_opener(cookie_handler, http_handler, https_handler)

    look_url = f"https://xx.hycbook.com"
    req = urllib.request.Request(look_url)

    # 发起请求
    response = opener.open(req)

    print("结束")
```

## urllib3

## requests
