一、为什么要学网络编程
回想一下你日常用电脑和手机做的事情:
- 打开天气 App,看今天多少度;
- 在浏览器里下载一个安装包;
- 用 Python 调用 ChatGPT 的 API 自动生成文案;
- 写个爬虫把豆瓣电影 Top250 抓下来存到数据库。
这些操作有一个共同点:数据不在你本地,而在远端的服务器上。程序要拿到这些数据,就必须通过网络和服务器"对话"。网络编程,就是教你的程序如何:
- 发起请求:告诉服务器"我要什么";
- 接收响应:把服务器返回的数据读进来;
- 解析数据:把字节流变成我们能用的字符串、JSON、图片等。
可以说,现代应用几乎没有不联网的 。学会网络编程,你才能写出真正"有用"的程序,而不是只会在本地算算 1+1。再举几个更贴近开发的例子:你写个脚本每天定时从公司内网 API 拉取销售数据生成报表;你做个钉钉机器人,出 bug 时自动发消息到群里;你训练机器学习模型时从对象存储下载几十 G 的训练数据。这些场景背后全是网络请求。理解了网络通信的原理,你才能在请求失败时知道是 DNS 解析问题、是连接被拒、还是超时,而不是对着一个报错两眼一抹黑。
Python 在网络编程方面非常强大:标准库自带 socket、urllib、http 等模块,第三方库 requests、aiohttp 更是把 HTTP 请求简化到一两行代码。本篇我们就从底层到高层,把它们逐一讲清楚。
二、网络通信基础概念
在写代码之前,先搞懂几个最基础的概念,否则后面会一头雾水。
2.1 IP 地址
IP 地址是网络上每台设备的"门牌号"。比如 192.168.1.100 是你家里路由器分配给电脑的局域网 IP,142.250.190.46 是 Google 服务器的公网 IP。程序要和某台机器通信,首先得知道它的 IP。
小知识:
127.0.0.1是本机回环地址,也叫localhost,永远指向自己。调试时经常用到。
2.2 端口(Port)
一台机器上可能同时跑很多服务:Web 服务、SSH 服务、数据库服务......光有 IP 还不够,还要知道是哪个"门"进去,这就是端口。端口是一个 0~65535 的数字,常见端口:
80:HTTP443:HTTPS22:SSH3306:MySQL8080:常用开发端口
所以一个网络通信地址通常写成 IP:端口,例如 127.0.0.1:8080。
2.3 TCP 与 UDP
这是两个最底层的传输协议:
| 协议 | 特点 | 适用场景 |
|---|---|---|
| TCP | 面向连接、可靠、有序、慢一点 | 网页、文件传输、数据库 |
| UDP | 无连接、不可靠、可能丢包、快 | 视频直播、DNS 查询、游戏 |
通俗讲:TCP 像打电话 ,先拨通建立连接,然后一句句说,对方没听懂还会重说;UDP 像寄明信片,写好地址扔出去就不管了,可能丢、可能乱序,但快。
2.4 HTTP 协议简介
HTTP(超文本传输协议)是建立在 TCP 之上的应用层协议,也是我们日常联网最常打交道的协议。一次 HTTP 通信大致是:
- 客户端(你的程序)通过 TCP 连到服务器
IP:端口(默认 80); - 发送一个"请求",包含方法(GET/POST)、路径、头信息、正文;
- 服务器返回一个"响应",包含状态码(200/404/500)、头信息、正文;
- 连接关闭(或保持)。
常见 HTTP 方法:
GET:请求数据(最常用);POST:提交数据(表单、JSON);PUT/DELETE:更新/删除资源。
常见状态码:
200成功;301/302跳转;404找不到;403没权限;500服务器内部错误。
理解了这些,下面就能看懂代码了。再补充一个容易混淆的点:我们常说的"客户端/服务端"是相对的角色。你的爬虫对网站来说是客户端,但如果你写了一个本地 Web 服务接收别人的请求,那它就是服务端。一次通信中,主动发起连接的一方是客户端,被动等待并响应的一方是服务端。socket 编程里,服务端要 bind+listen+accept,客户端只要 connect,就是这个道理。
三、socket:最底层的网络通信
socket(套接字)是操作系统提供的网络通信接口,Python 标准库 socket 模块对它做了封装。它非常底层,你要自己处理连接、收发字节流,但也最灵活。
3.1 TCP 服务端与客户端完整示例
先写一个 TCP 服务端,它监听本机 9999 端口,收到客户端发来的消息后回一句"已收到"。
服务端 server.py:
import socket
# 1. 创建一个 IPv4(TCP) 的 socket 对象
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# AF_INET 表示 IPv4,SOCK_STREAM 表示 TCP
# 2. 绑定 IP 和端口('' 表示本机所有网卡)
server.bind(('0.0.0.0', 9999))
# 3. 开始监听,参数 5 表示允许最多 5 个连接在队列里等待
server.listen(5)
print("服务端启动,等待连接...")
while True:
# 4. 接受一个连接,返回 (连接对象, 客户端地址)
conn, addr = server.accept()
print(f"客户端 {addr} 连上来了")
while True:
# 5. 接收数据,每次最多读 1024 字节
data = conn.recv(1024)
if not data: # 收到空数据说明客户端断开了
print(f"客户端 {addr} 断开")
break
print(f"收到: {data.decode('utf-8')}")
# 6. 回复客户端
conn.sendall("已收到你的消息\n".encode('utf-8'))
conn.close() # 关闭这个连接
客户端 client.py:
import socket
# 1. 创建 socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 连接服务端
client.connect(('127.0.0.1', 9999))
# 3. 发送数据(必须是字节,所以要 encode)
client.sendall("你好,我是客户端".encode('utf-8'))
# 4. 接收回复
response = client.recv(1024)
print(f"服务端回复: {response.decode('utf-8')}")
# 5. 关闭
client.close()
运行说明:
- 先在一个终端运行
python server.py,看到"服务端启动,等待连接..."; - 再开一个终端运行
python client.py; - 客户端输出:
服务端回复: 已收到你的消息; - 服务端输出:
客户端 ('127.0.0.1', xxxxx) 连上来了和收到: 你好,我是客户端。
注意:
send/recv收发的都是字节(bytes) ,所以字符串要用.encode()编码成字节再发,收到后用.decode()解码成字符串。这是新手最容易忘的一点。
3.2 UDP 示例
UDP 不需要建立连接,直接发就行。
UDP 服务端:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # SOCK_DGRAM 表示 UDP
server.bind(('0.0.0.0', 9998))
print("UDP 服务端启动...")
while True:
data, addr = server.recvfrom(1024) # 接收数据,同时拿到对方地址
print(f"来自 {addr}: {data.decode('utf-8')}")
server.sendto("UDP回复".encode('utf-8'), addr)
UDP 客户端:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto("Hello UDP".encode('utf-8'), ('127.0.0.1', 9998))
data, _ = client.recvfrom(1024)
print(f"收到: {data.decode('utf-8')}")
UDP 没有连接、没有重传,发出去就不管了,所以代码更短,但不可靠。
3.3 socket 常用方法表
| 方法 | 说明 |
|---|---|
socket() |
创建套接字 |
bind(addr) |
绑定地址(服务端用) |
listen(n) |
开始监听 |
accept() |
接受连接,返回 (conn, addr) |
connect(addr) |
连接服务器(客户端用) |
send(data) / sendall(data) |
发送数据 |
recv(bufsize) |
接收数据 |
sendto(data, addr) |
UDP 发送 |
recvfrom(bufsize) |
UDP 接收 |
close() |
关闭 |
settimeout(t) |
设置超时时间 |
3.4 注意事项
- 收发都是字节 ,记得
encode/decode; recv(1024)一次最多读 1024 字节,大数据要循环读;- 服务端要
bind端口,客户端一般不用(系统自动分配); - 端口被占用会报
OSError: [Errno 48] Address already in use,换个端口或加socket.SO_REUSEADDR; - 长时间不收发,连接可能被防火墙断掉,记得加
settimeout。
socket 太底层,日常开发很少直接用。下面看看标准库提供的更高层工具。
四、urllib:标准库的 HTTP 工具
Python 标准库自带 urllib 模块,不用安装就能发 HTTP 请求,适合简单场景或没有第三方库的环境。
from urllib import request, parse
# GET 请求
url = "http://httpbin.org/get"
with request.urlopen(url) as resp:
print(resp.status) # 200
print(resp.read().decode('utf-8')) # 响应正文
# POST 请求(带表单数据)
data = parse.urlencode({'name': '陈序员', 'age': 18}).encode('utf-8')
with request.urlopen("http://httpbin.org/post", data=data) as resp:
print(resp.read().decode('utf-8'))
urllib 能用,但 API 比较啰嗦,要手动 urlencode、手动 decode、加 header 要构造 Request 对象......所以实际开发中,大家几乎都用第三方库 requests。urllib 这里只作为过渡了解。
五、requests:日常联网首选
requests 是 Python 世界最流行的 HTTP 库,号称"为人类设计的 HTTP 库"。它的 API 简洁优雅,几行代码就能完成复杂请求。
5.1 安装
pip install requests
5.2 GET 请求
import requests
# 最简单的 GET
resp = requests.get("https://httpbin.org/get")
print(resp.status_code) # 200
print(resp.text) # 响应文本
# 带 query 参数(自动拼到 URL 后面)
params = {'name': 'chen', 'age': 18}
resp = requests.get("https://httpbin.org/get", params=params)
print(resp.url) # https://httpbin.org/get?name=chen&age=18
# 带请求头
headers = {'User-Agent': 'MyPythonBot/1.0'}
resp = requests.get("https://httpbin.org/get", headers=headers)
# 设置超时(秒),避免一直卡住
resp = requests.get("https://httpbin.org/get", timeout=5)
运行结果:resp.text 会返回一段 JSON 字符串,里面能看到 args 字段里带着你传的参数。
5.3 POST 请求
import requests
# 1. 提交表单数据(application/x-www-form-urlencoded)
resp = requests.post("https://httpbin.org/post", data={'username': 'admin', 'pwd': '123'})
print(resp.text)
# 2. 提交 JSON(application/json),用 json= 参数
resp = requests.post("https://httpbin.org/post", json={'title': 'hello', 'body': 'world'})
print(resp.text)
# 3. 上传文件
files = {'uploadfile': open('test.txt', 'rb')}
resp = requests.post("https://httpbin.org/post", files=files)
print(resp.status_code)
区分
data=和json=:data发的是表单格式,json发的是 JSON 格式。调 API 时大多用json=。
5.4 会话 Session
如果要在多次请求之间保持 Cookie、复用 TCP 连接(更快),就用 Session:
import requests
s = requests.Session()
s.headers.update({'User-Agent': 'MyBot/1.0'}) # 给所有请求统一加 header
# 第一次请求登录,Cookie 会自动保存
s.post("https://httpbin.org/cookies/set", data={'token': 'abc123'})
# 后续请求自动带上 Cookie
resp = s.get("https://httpbin.org/cookies")
print(resp.text) # 能看到 cookies 里有 token=abc123
s.close() # 用完关闭
5.5 响应对象详解
requests.get() 返回一个 Response 对象,常用属性和方法:
| 属性/方法 | 说明 |
|---|---|
status_code |
状态码,如 200、404 |
text |
响应正文(字符串,自动猜测编码) |
content |
响应正文(字节,适合下载二进制) |
json() |
把正文当 JSON 解析成字典 |
headers |
响应头(字典) |
cookies |
响应 Cookie |
url |
最终请求的 URL(可能经过跳转) |
encoding |
字符编码 |
raise_for_status() |
状态码不是 200 就抛异常 |
示例:
import requests
resp = requests.get("https://httpbin.org/json")
print(resp.status_code) # 200
print(resp.headers['Content-Type']) # application/json
data = resp.json() # 直接拿到字典
print(data['slideshow']['title'])
5.6 下载文件(流式下载大文件)
下载小文件直接 resp.content 写到文件即可,但下载几个 G 的电影就不能一次性读进内存,要用流式下载:
import requests
url = "https://example.com/bigfile.zip"
resp = requests.get(url, stream=True) # stream=True 不立即下载正文
with open("bigfile.zip", "wb") as f:
for chunk in resp.iter_content(chunk_size=8192): # 每次读 8KB
if chunk:
f.write(chunk)
print("下载完成")
还可以加进度条(配合 tqdm):
from tqdm import tqdm
total = int(resp.headers.get('content-length', 0))
with open("bigfile.zip", "wb") as f, tqdm(total=total, unit='B', unit_scale=True) as bar:
for chunk in resp.iter_content(8192):
f.write(chunk)
bar.update(len(chunk))
5.7 代理设置
爬虫经常要挂代理:
import requests
proxies = {
'http': 'http://127.0.0.1:7890',
'https': 'http://127.0.0.1:7890',
}
resp = requests.get("https://www.google.com", proxies=proxies, timeout=10)
print(resp.status_code)
六、异常处理
网络不稳定,请求随时可能失败。requests 提供了清晰的异常体系:
import requests
from requests.exceptions import Timeout, ConnectionError, RequestException
try:
resp = requests.get("https://httpbin.org/get", timeout=3)
resp.raise_for_status() # 4xx/5xx 会抛 HTTPError
except Timeout:
print("请求超时,请检查网络")
except ConnectionError:
print("连接失败,可能是网址不对或断网")
except RequestException as e:
print(f"其他请求异常: {e}")
else:
print("成功:", resp.text[:100])
常用异常:
Timeout:超时;ConnectionError:连接失败(DNS 解析失败、拒绝连接等);HTTPError:状态码 4xx/5xx(需raise_for_status()触发);TooManyRedirects:重定向过多;RequestException:所有 requests 异常的基类。
实际开发中,建议总是加 try-except,否则一个网络抖动就让程序崩溃。
七、实战案例:简单的天气查询工具
我们用免费的 wttr.in 服务(无需注册、无需 key)写一个命令行天气查询工具:
import requests
def get_weather(city: str) -> str:
"""查询指定城市的天气,返回格式化字符串"""
url = f"https://wttr.in/{city}"
params = {'format': 'j1'} # 返回 JSON
try:
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
except requests.RequestException as e:
return f"查询失败: {e}"
current = data['current_condition'][0]
temp = current['temp_C']
desc = current['weatherDesc'][0]['value']
humidity = current['humidity']
wind = current['windspeedKmph']
return (f"【{city} 天气】\n"
f"温度: {temp}°C\n"
f"天气: {desc}\n"
f"湿度: {humidity}%\n"
f"风速: {wind} km/h")
if __name__ == '__main__':
while True:
city = input("请输入城市名(英文,如 Beijing/Shanghai,输入 q 退出): ").strip()
if city.lower() == 'q':
break
print(get_weather(city))
print("-" * 30)
运行示例:
请输入城市名(英文,如 Beijing/Shanghai,输入 q 退出): Beijing
【Beijing 天气】
温度: 18°C
天气: Partly cloudy
湿度: 45%
风速: 12 km/h
------------------------------
这个例子综合用到了 GET 请求、params、timeout、异常处理、JSON 解析,是一个完整的小工具。你可以把它扩展成定时查询、保存历史、发邮件提醒等。比如结合 schedule 库每天早上 8 点查询一次,把结果写入 SQLite 历史表,温度低于 5 度就调用邮件接口提醒你加衣服------这就从一个脚本变成了一个真正有用的小服务。再进一步,用 FastAPI 把它包成 Web 接口,前端做个页面展示,你就拥有了自己的天气应用。所有这些扩展,底层都离不开本篇学的 requests 这几行代码。
八、最佳实践
- 永远设置 timeout:不设超时的请求可能永远卡住,拖垮整个程序。
- 用 Session 复用连接:多次请求同一站点时,Session 能复用 TCP 连接,速度快很多。
- 总是处理异常:网络不可靠,try-except 是标配。
- 大文件用 stream 流式下载:避免内存爆炸。
- 加 User-Agent:很多网站会拒绝默认 UA,伪装成浏览器更稳。
- 遵守 robots.txt 和频率限制 :爬虫别太猛,加
time.sleep,做个有礼貌的爬虫。 - JSON 用
resp.json(),别手动json.loads(resp.text):前者更安全,编码处理更好。 - 敏感信息(token、密码)放 header 或环境变量,别写在 URL 里(URL 会被日志记录)。
- HTTPS 优先:HTTP 明文传输,不安全。
- 调试时打开日志 :
import logging; logging.basicConfig(level=logging.DEBUG),能看到请求详情。
九、小结
本篇我们从最底层的 socket 一路讲到日常最常用的 requests:
socket是操作系统网络接口的封装,能做 TCP/UDP 通信,但要自己处理字节、连接、收发循环;urllib是标准库的 HTTP 工具,能用但啰嗦;requests是日常首选,GET/POST/Session/文件上传/流式下载/代理一应俱全;- 网络请求一定要处理异常、设超时;
- 实战写了一个天气查询小工具,把所学串了起来。
下一篇我们将进入并发编程的世界------当你要同时下载 100 个网页时,串行一个个下太慢了,得让程序"同时干多件事",敬请期待。
十、练习题 / 思考题
- 用
socket写一个简单的"聊天室"服务端,支持多个客户端同时连接并互相转发消息(提示:结合下一篇的多线程)。 - 用
requests调用https://api.github.com/users/你的用户名,打印你的 GitHub 公开信息。 - 写一个函数
download(url, path),支持断点续传(提示:用Range请求头)。 - 用
Session模拟登录某个需要 Cookie 的网站(注意遵守对方条款)。 - 思考:为什么
requests比urllib用起来舒服这么多?它帮你封装了哪些东西? - 挑战:用
socket手写一个最简单的 HTTP 客户端,向httpbin.org发 GET 请求并解析响应(体会一下requests帮你省了多少事)。