一、为什么要学网络编程
回想一下你日常用电脑和手机做的事情:
- 打开天气 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 requests5.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帮你省了多少事)。