前面四十多篇拿到的都是文字——网页内容、表格数据、JSON。但网上更多好东西是文件:PDF 通知、Excel 模板、图片素材、安装包。今天新概念只有一个:二进制文件写入——
resp.content拿到的不再是”字”,而是”原样的字节”,得用open(path, "wb")原封不动写进硬盘。学完这篇,”右键→另存为→等→再右键→再另存为”的体力活就彻底交给程序了:它半夜能把 30 份 PDF 整整齐齐搬进你的文件夹。
一、真实场景:月底的”资料搬运工”
单位的”资料下载”栏每月更新一批文件:本月的工作简报 PDF(12 期,编号 01 到 12)、报销模板 Excel、还有几张通知截图。领导让你”把这些都下下来,归到 D:月度资料10月 里”。
这事没有任何技术含量:点开链接 → 右键 → 另存为 → 选文件夹 → 改文件名 → 重复 15 遍。难就难在烦:每次都要选路径、等进度条、文件名还容易存错(简报(1).pdf、简报(2).pdf 这种鬼东西就是这么来的)。而且这种活每月一次,下个月还得再来一遍。
更别提另一种情况:批量。比如教研组要下载 200 份学生交的电子作业(每份一个 PDF 链接,都在一个页面上排着),手工点 200 次另存为?点到手抽筋。
其实浏览器”另存为”干的事,就三步:把文件内容从网上拿回来 → 起个文件名 → 写进硬盘。前两步你早会了(requests.get、字符串拼接),今天只补第三步:
文件的”字节”要写进”二进制文件”:
open(path, "wb").write(resp.content)——”wb” 的 b 就是 binary(二进制),意思是”别翻译,原样存”。
就这一句。剩下的全是老手艺:requests.get(第37篇)、正则抠链接(第43篇)、BeautifulSoup 找 a 标签(第41篇)、os.makedirs 建文件夹(第30篇)、try/except 防崩(第10篇)、Session(第45篇,下载”要登录才能拿”的内部资料时直接派上用场)。
二、新概念只有一个:文字 vs 字节,”w” vs “wb”
resp.text 和 resp.content 的区别
先看一个实验。下载一个小文件,打印两种”拿法”的类型:
import requests
resp = requests.get("https://www.python.org/static/img/python-logo.png") # 官网 logo 图片
print(type(resp.text)) # —— 字符串(文字)
print(type(resp.content)) # —— 字节串(原始字节)
resp.text是文字:requests 把服务器发来的字节按编码”翻译”成字符串。网页、JSON、CSV 这些本来就是文字的东西,用它没错(前面 45 篇都用的它);resp.content是原样的字节:服务器发来什么就是什么,一个字节没动。
图片、PDF、Excel、压缩包、音视频——它们不是文字。 一张 PNG 图片的字节里,”翻译成文字”这个动作本身就是错的:强行翻译,遇到翻译不了的字节就乱码甚至直接崩(UnicodeDecodeError)。就像把一张照片塞进翻译软件:照片不是文字,翻译软件只能瞎编或者报错。
所以下载文件的铁律一句话:文件下载用 resp.content,别用 resp.text。
“wb”:原样写入
第9篇学过写文字文件:open("a.txt", "w")。今天加一个字母:
with open("python-logo.png", "wb") as f: # wb = write binary,二进制写入
f.write(resp.content) # 把字节原样写进硬盘
w= 写文字(str),写之前会把字符串按编码翻译回字节;wb= 写字节(bytes),什么都不翻译,来什么存什么。
配对关系别搞反,一共就两种组合:
| 读/写 | 文字(str) | 字节(bytes) |
|---|
|——|————|————–|
| 拿内容 | resp.text |
resp.content |
|---|---|---|
| 开文件 | open(path, "w") |
open(path, "wb") |
下载文件 = resp.content + "wb",两个必须配对用。用 resp.text + "w" 存 PDF?写出来的文件大小不对、打不开、或者直接崩——这是今天头号大坑,疫苗打在坑1。
类比记忆:
resp.text是把外文书翻译成中文给你看——适合本来就字字是文字的网页;resp.content是把原版书整本塞给你——图片 PDF 这种”不是字”的东西,翻译是糟蹋,原样保存才对。"wb"就是”原样入柜”的章。
文件名和文件夹
顺带复习两个小手艺(都是老知识):
import os
os.makedirs("月度资料/10月", exist_ok=True) # 建文件夹(第30篇),已存在不报错
path = os.path.join("月度资料", "10月", "简报01.pdf") # 拼路径,别手写斜杠
os.path.join 拼路径比手写 "月度资料/10月/简报01.pdf" 稳——Windows 用 、Mac/Linux 用 /,join 替你操心。文件名从网页上抠下来时注意去掉非法字符(Windows 文件名不能带 /:*?"|),疫苗打在坑3。
三、动手:先下一个文件,再搬一整批
第 1 步:下一个文件(20 行)
先用最简单的场景把”下载→存盘”跑通:
# download_one.py
import os
import requests
url = "https://www.python.org/static/img/python-logo.png"
resp = requests.get(url, timeout=15)
if resp.status_code != 200:
print(f"下载失败,状态码: {resp.status_code}")
else:
os.makedirs("downloads", exist_ok=True)
path = os.path.join("downloads", "python-logo.png")
with open(path, "wb") as f:
f.write(resp.content)
print(f"下载完成: {path},大小 {len(resp.content)} 字节")
跑一下:python3.14 download_one.py——downloads/ 文件夹里躺着一张 python-logo.png,双击能打开,就说明”字节搬运”打通了。len(resp.content) 就是文件的字节数,和右键”属性”里看到的大小应该一致。
第 2 步:批量下载一整批(抠链接 + 循环下载)
真实任务都是”一整批”。写一个假的下载页练习(省得去打扰真实网站),再写批量下载脚本:
# fake_download_site.py —— 假的"资料下载"栏,用来安全练习
from flask import Flask, send_file
import os
app = Flask(__name__)
os.makedirs("fake_files", exist_ok=True)
# 先造几个假 PDF(内容随便写点字节,扩展名是 pdf 就行,练习够用)
for i in range(1, 4):
with open(f"fake_files/简报{i:02d}.pdf", "wb") as f:
f.write(b"%PDF-1.4 fake report " + str(i).encode() + b"n" * 100)
@app.route("/")
def index():
links = "".join(f'简报{i:02d} ' for i in range(1, 4))
return f"资料下载
{links}
"
@app.route("/files/")
def files(name):
return send_file(f"fake_files/{name}")
if __name__ == "__main__":
app.run(port=5002)
# batch_download.py —— 批量下载:抠链接 → 循环下载 → 存盘
import os
import re
import requests
from bs4 import BeautifulSoup
BASE = "http://127.0.0.1:5002"
SAVE_DIR = "月度资料"
resp = requests.get(BASE, timeout=10)
soup = BeautifulSoup(resp.text, "html.parser")
# 第一步:把所有下载链接抠出来(第41篇的手艺)
links = [a["href"] for a in soup.find_all("a") if a.get("href", "").endswith(".pdf")]
print(f"发现 {len(links)} 个文件")
# 第二步:循环下载(第6篇的手艺)
os.makedirs(SAVE_DIR, exist_ok=True)
ok, fail = 0, 0
for href in links:
url = BASE + href if href.startswith("/") else href
filename = os.path.basename(href) # 从链接里取文件名:简报01.pdf
path = os.path.join(SAVE_DIR, filename)
try:
r = requests.get(url, timeout=30)
if r.status_code != 200:
print(f"跳过 {filename}: 状态码 {r.status_code}")
fail += 1
continue
with open(path, "wb") as f:
f.write(r.content)
ok += 1
print(f"已下载 {filename}({len(r.content)} 字节)")
except requests.RequestException as e:
print(f"失败 {filename}: {e}")
fail += 1
print(f"完成: 成功 {ok} 个, 失败 {fail} 个, 存在 {SAVE_DIR}/")
先 python3.14 fake_download_site.py 起服务(另开一个终端),再 python3.14 batch_download.py——月度资料/ 里整整齐齐躺着 简报01.pdf、简报02.pdf、简报03.pdf,文件名是从链接里抠的,没有一个 简报(1).pdf。真实的”资料下载”栏把 BASE 换成真网址即可。
注意循环里的三件小事:每个文件都 try 一下——一批 200 个文件坏一个不该全军覆没(第10篇的手艺);下载完打印进度——安静的批量任务最让人心里没底;最后汇总成功/失败个数——工具自己交代干得怎么样,这是脚本和工具的分界线。
跟AI说需求的参考说法:
“这是我们要下载的页面(贴 HTML):里面有一排 a 标签,链接是 PDF。帮我写个 Python 脚本:把所有 PDF 链接抠出来,逐个下载到’月度资料’文件夹,用链接里的文件名保存,每个文件下载失败要报错但不影响后面的,最后打印成功失败汇总。”
四、改进:从”能下载”到”下载工具”
三步升级,都跟AI协作完成:
- 大文件不卡死、不吃内存:
resp.content是”一口气把整个文件装进内存”,下个 500MB 的视频,内存先爆了。改成分块边下边写:resp.iter_content(chunk_size=8192)每次只拿 8KB,拿到一块写一块——内存里永远只有一小块。跟AI说:”帮我把下载改成流式写入(iter_content),并打印下载进度(每下完 10% 打一行)。”(完整代码留在练习3) - 下过的不重下:批量任务跑一半断了,重跑一遍从头来?加一行判断:
os.path.exists(path)且文件大小正常就跳过。跟AI说:”下载前如果目标文件已存在且大于 0 字节,就跳过并打印’已存在’。”——中断重跑就变成”接着干”,200 个文件重跑只补缺的那几个。 - 下载完”验货”:存盘前查一眼
resp.headers.get("Content-Type"),下载 PDF 却收到text/html?那多半是 404 页或登录页被存成了假 PDF(坑2的疫苗)。跟AI说:”下载后检查 Content-Type 是否符合预期(PDF 应该是 application/pdf),不符合就删掉文件并报错。”汇报也补一句:”下载 12 个文件,跳过 3 个已存在,失败 0 个,共 45.2MB。”
五、三个必踩的坑(先打疫苗)
坑1:拿 resp.text 存文件,文件”存出来了”但全打不开。这是下载任务的头号死法:代码跑得欢,文件也生成了,双击 PDF——”文件已损坏”;双击图片——根本打不开。原因就是第二节说的:PDF/图片的字节被当文字”翻译”了一道,翻译过程丢字节、加乱码,写出来的文件早不是原来那个。更隐蔽的是有些字节恰好能翻译,文件”能打开一半”,错误直到打印时才发现。疫苗:下载文件的固定搭配是 resp.content + "wb",写进你的肌肉记忆。判断标准就一条:文件下完双击能正常打开、大小和网页上标的差不多,才算下成功。
坑2:把”错误页”存成了文件——404 页面躺在简报01.pdf 里。链接失效、网站改版、需要登录……服务器照样会”好心”地给你返回点东西(404 错误页、登录页、”请先登录”的 HTML),status_code 甚至可能不是 200。程序不管三七二十一直接 write(resp.content)——简报01.pdf 生成了,两页 HTML 藏在 .pdf 扩展名里,双击打不开,你又开始怀疑人生。疫苗三查:一查 resp.status_code == 200;二查 Content-Type 是不是预期类型(PDF 是 application/pdf,Excel 是 spreadsheet,图片是 image/);三查文件大小(错误页一般只有几 KB)。三查不过就大声报错、删掉坏文件,别让假文件混进资料夹。
坑3:文件名里有非法字符/重名覆盖,资料夹变成垃圾场。文件名是从网页上抠的,网页作者想写什么写什么:报销模板(新)最终版!!.pdf、2026/10/简报.pdf(注意这个斜杠!)、简报?.pdf——Windows 文件名不能带 / : * ? " |,直接拿来当文件名轻则报错,重则 简报?.pdf 被解释成路径的一部分,文件存到你根本找不到的地方。另外同名文件会被静默覆盖:上个月的”简报01.pdf”被这个月的盖掉,你连丢了什么都不知道。疫苗:文件名先清洗(把非法字符替换成 _,os.path.basename 去掉路径部分),重名先处理(跳过、或者加日期前缀 2026-10-简报01.pdf),归档类文件名自带日期永远是好习惯。跟AI说:”帮我写个安全文件名函数,去掉非法字符、限制长度、重名时自动加序号。”
六、跟AI协作的正确姿势(下载特供版)
- 把 F12 抓的”下载那一下”贴给AI:F12 → Network → 点下载链接 → 看那条请求的网址和响应头(
Content-Type、Content-Length)。跟AI说:”这个网址返回一个 PDF(Content-Type: application/pdf),帮我写下载脚本。”——连预期文件大小 AI 都能替你写进验证里。 - 让AI把”下载+验证+存盘”包成一个函数:”帮我写一个
download_file(url, save_dir)函数:检查状态码和 Content-Type、清洗文件名、分块写入、已存在就跳过、失败抛中文异常。”一次写好,之后所有批量下载脚本都直接调用。 - 批量任务让AI加上”断点续跑”:”我的批量下载任务经常下到一半断网,帮我改成:已下载的文件跳过,最后汇总哪些成功哪些失败,失败的存成 retry_list.txt 下次重试。”这是批量任务从玩具变工具的关键一步。
- 拿不准”能不能下”就问AI:”这个网站的文件我想批量下载,合适吗?有什么要注意的?”——AI 会提醒你看 robots.txt、控制速度、别把人家服务器压垮。下一篇(第47篇)专门讲这些”爬虫礼仪”,先打个招呼:程序能干不等于可以随便干。
七、今日小练习
练习1:跑通”下一个”和”搬一批”。先跑 download_one.py,确认 downloads/python-logo.png 双击能打开、大小和网页上标的一致;再起 fake_download_site.py、跑 batch_download.py,确认 月度资料/ 里有 3 个 PDF,文件名是 简报01.pdf 到 简报03.pdf,没有 简报(1).pdf 这种鬼名字。
提示:图片打不开先看文件大小——如果只有 1KB 左右且用记事本能打开一堆乱码,多半是错用了 resp.text;批量脚本如果一个链接都没抠到,先在浏览器打开 http://127.0.0.1:5002 确认页面正常、链接结尾确实是 .pdf。
练习2:故意触发”错误页”,看疫苗灵不灵。把 batch_download.py 里的 BASE + href 改成 BASE + "/files/不存在.pdf" 手动下一回,观察现在的程序会把什么存进文件夹;然后按坑2的”三查”给代码加上状态码检查,再跑一次——应该是”跳过 不存在.pdf: 状态码 404″,文件夹里不留下假 PDF。
提示:判断程序好坏的标准不是”不报错”,而是”错得让你看得见”。对比一下加检查前后 月度资料/ 里的文件——用记事本打开那个假 PDF 看看里面藏的是什么,你就明白为什么必须验货了。
练习3(动脑+选做):给工具装上”大文件模式”和”断点续跑”。按第四节跟AI协作实现两点:①用 resp.iter_content(chunk_size=8192) 分块下载,每完成 10% 打印一行进度;②目标文件已存在且大于 0 字节就跳过。验收:把 chunk_size 调成 1024 下一个几 MB 的文件看进度打印;中断一次(Ctrl+C)再重跑,确认已下的文件被跳过。选做挑战:下载失败的 URL 收集进 retry_list.txt,写个 retry.py 读取重试——批量任务的”断点续跑”就齐活了。
提示:分块写入用 with open(path, "wb") as f: for chunk in resp.iter_content(...): if chunk: f.write(chunk)——if chunk 是过滤心跳包的空块,别省。进度百分比用 已写字节 / 总字节 × 100,总字节在 resp.headers.get("Content-Length") 里(它可能缺失,缺失就不打百分比、只打已下多少 KB)。
下篇预告
下一篇(第47篇):爬虫礼仪 — 程序能干,不等于可以随便干。下载和爬取都是”从别人网站拿东西”,拿得太凶、拿了不该拿的,轻则被封 IP,重则吃官司。下一篇学两样护身符:robots.txt(网站贴在门口的”欢迎/不欢迎”告示)和限速(做个体面的访客)。今天先把搬运手艺练熟——下一篇教你怎么搬得礼貌。
发表回复