属于:00-首页
第 1 章 网页是什么,爬虫是什么
这一章读完,你电脑上会跑着一个真能抓东西的程序,六行代码。但在这六行代码之前,得先花十分钟说清楚一件事——很多人写了半年爬虫都没说清楚的事:
网页到底是个什么东西。
你每天打开的网页,其实是个文本文件
现在打开浏览器,随便进一个页面,按 Ctrl + U。
一屏幕尖括号对吧。全是 <div>、<a href="...">、<span> 这种东西,密密麻麻,毫无美感。
这就是网页的真身。一个纯文本文件,格式名叫 HTML。
你平时看到的排版、图片、颜色、动画,是浏览器读完这个文本之后画出来的。也就是说浏览器其实干了两件事:
第一,把 HTML 文本从服务器上下载下来; 第二,照着文本里的指令,把它渲染成你看到的样子。
爬虫只干第一件事。
这就是整个爬虫技术全部的地基:爬虫不是什么神秘的黑客工具,它是一个只下载、不画画的浏览器。你写爬虫,就是写一个程序替你做第 1 步,然后把下载到的文本交给你自己处理。
(所以请记住这个推论,后面能救你很多次:当你发现”爬虫抓到的内容和浏览器看到的不一样”时,多半不是爬虫坏了,而是浏览器画出来的样子骗了你——下载下来的文本里,可能压根没有那块内容。)
卡片:网页就是一个文本文件
先认清你要抓的东西:URL
爬虫请求一个页面,靠的是 URL。把它拆开看:
https://www.example.com:443/search?q=python&page=2#result
└─┬─┘ └─────┬─────┘└┬┘└──┬──┘└──────┬──────┘└──┬──┘
协议 主机 端口 路径 查询参数 片段
协议、主机、路径没什么好说的。查询参数是 ? 后面、用 & 分隔的键值对,翻页、搜索关键词大多藏在这里,第 7 章靠它吃饭。
这里只强调一个东西:最后那个 #result,叫片段(fragment)。
它永远不会被发送到服务器。
它是浏览器本地的指令,意思是”页面加载完之后,滚动到 id 为 result 的位置”。服务器根本不知道它的存在。
由此得到一个极其重要的判断依据:如果你发现一个网站 URL 里 # 后面的内容变了、页面内容也跟着变,那这些内容一定不是服务器直接给你的 HTML,而是浏览器拿到 HTML 之后自己再去要来的。这种页面,你用本章的方法抓不到正文。
现在记不住没关系,第 13 章我们会回来收拾它。但请先记住这句话,它是判断”这页面能不能直接抓”的第一把尺子。
卡片:URL片段永远不上服务器
装环境,两分钟的事
Windows 按 Win + R,输入 cmd 回车,敲:
python --version
能打印出 Python 3.x.x 就行。如果提示”不是内部或外部命令”,去 python.org 下载安装,安装界面第一个页面务必勾上 Add Python to PATH。
这个勾没勾,是新手第一天最常见的卡点,没有之一。
然后装本章唯一要用的库:
pip install requests
为什么是 requests 而不是 Python 自带的 urllib?因为 urllib 的写法停留在二十年前,而 requests 是这个行业事实上的标准,你以后去看任何别人的爬虫代码,十有八九是它。本书所有代码也用它。不争论,跟着用。
第一个爬虫
新建一个 spider1.py,六行:
import requests
r = requests.get("http://quotes.toscrape.com/")
print(r.status_code)
print(r.text)跑它:
python spider1.py输出大概长这样(中间省略):
200
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Quotes to Scrape</title>
...
第一行的 200 是状态码,意思是”服务器把东西给你了”。后面那一大坨尖括号,就是这个网站的 HTML 文本——和你在那个页面按 Ctrl + U 看到的是同一个东西。
恭喜,这是一个能用的爬虫。
本书后面所有内容,都是在让这六行代码变得更狠:抓得更快、抓得更稳、抓那些不想被你抓的站点。
顺便说一句为什么选 quotes.toscrape.com:这是一个专门给人练爬虫用的站点,没有反爬,结构干净,法律上零风险。练枪法先用靶场,别一上来就上街。
抓下来的中文是乱码?编码这关躲不掉
先跑一行代码,见一个老熟人:
print("\ufffd\ufffd".encode("utf-8").decode("gbk"))输出:
锟斤拷
看到”锟斤拷”三个字,老爬虫会心一笑。这三个字不是玄学,来历精确到字节:\ufffd 是 Unicode 里的替换符,某个程序翻译字节失败时拿它顶包;顶包后的字符串按 UTF-8 存盘,字节变成 EF BF BD;之后另一个程序拿 GBK 码表去翻这串字节,EF BF 翻成”锟”,BD EF 翻成”斤”,BF BD 翻成”拷”。锟斤拷,诞生。
所以乱码现场通常站着两个事故:第一次翻译失败(顶了替换符),第二次翻译用错码表(翻出锟斤拷)。你在自己爬虫里碰到的乱码,多半是第一种事故的直接表现:requests 猜错了编码。
事情的本质是这样。服务器给你的是字节,一串 0 和 1。要把字节变成你能读的中文,必须按某本”码表”去翻译,这本码表就叫编码,常见的有 UTF-8 和 GBK 两本。码表用错,翻出来就是乱码;翻不了的地方顶替换符,再被错翻一次,就是锟斤拷。
在 requests 里,这两个东西分别对应:
r.content:原始字节,服务器给什么就是什么;r.text:requests 替你翻译好的字符串。
乱码永远出在”翻译”这一步。requests 默认先看响应头里有没有声明编码,很多中文站点不声明或者声明是错的,它就只好瞎猜,猜错就乱码。
修法两种,任选:
r.encoding = r.apparent_encoding # 让库根据内容自己再猜一次,多数情况能救回来
print(r.text)或者你明确知道对方是 GBK(老站常见):
r.encoding = "gbk"
print(r.text)这一节没有更深的东西,就一个认知:字节和字符串是两回事,乱码是翻译事故,不是抓取事故。 抓到的字节永远是对的,错的只是翻法。
卡片:乱码是翻译事故
把抓到的页面存下来
光打印没意义,存盘:
import requests
r = requests.get("http://quotes.toscrape.com/")
with open("page.html", "w", encoding="utf-8") as f:
f.write(r.text)跑完去文件夹里双击 page.html,浏览器会把它打开。
你会看到内容都在,但页面光秃秃的:排版没了,灰底白框全消失,只剩裸文字。
翻开你存下来的 page.html 开头几行,答案就在里面:
<link rel="stylesheet" href="/static/bootstrap.min.css">
<link rel="stylesheet" href="/static/main.css">样式是另外两个文件,HTML 里只留了两行地址。浏览器打开本地 page.html 时,拿 /static/bootstrap.min.css 这种相对地址去你本地文件夹里找,找不到,于是放弃打扮,素颜见你。换个带图片的站存一遍,你看到的就是图片裂成小方块——同一个道理。
别慌,这不是 bug,这恰恰揭示了一个你早就该知道的事实:一个网页从来不是”一个文件”,而是”一个 HTML 加一堆资源文件”。HTML 里写着 <img src="...">、<link href="...css">,浏览器看到这些行,会再发一批请求去把图片、样式、脚本一个个取回来,拼成完整页面。quotes 这个站刚好一张图都没有(我数过,img 标签 0 个),所以你在它身上只看到丢样式,看不到裂图。
你刚才只存了那个 HTML 文本,资源一个没拿,于是就成了你看到的半残样子。
(在浏览器里按 F12 切到 Network 标签再刷新页面,你能亲眼看到这一堆请求排着队发出去。下一章我们就站在这个标签页上,把你和服务器之间说的每一句话都拆开看。)
第一次被拒:状态码 200,拿到的却是登录页
靶场打完了,去碰一下真站点。把 URL 换成:
r = requests.get("https://www.xiaohongshu.com")
print(r.status_code, r.url)输出(2026-11-15 实测):
200 https://www.xiaohongshu.com/login?redirectPath=http%3A%2F%2Fwww.xiaohongshu.com%2Fexplore%3FexSource%3Dnull
注意看,状态码是 200。成功了?
再看第二行:你被带到登录页去了。下载回来的三万多字节 HTML,是登录框的壳,一条笔记都没有。
这比 403 阴险。403 是当面摔门,你知道自己被拒了;这种是开门把你引进一间空屋子,你的程序还美滋滋地认为抓成功了。记住这个感觉,第 3 章和第 12 章我们还会反复回到它身上:
爬虫最大的敌人不是报错,是不报错的错误。
有的站确实会直接摔门(返回 403、412 之类),那是另一种拒法,下一章开始逐个认。
先试最古老的一招。每次 HTTP 请求都带着一组自我介绍,叫请求头(headers),其中一行叫 User-Agent,标明”我是谁”。requests 默认自报家门是 python-requests/2.x.x,等于胸牌上写着”我是爬虫”。换成 Chrome 的自我介绍:
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
}
r = requests.get("https://www.xiaohongshu.com", headers=headers)
print(r.status_code, r.url)实测:还是 200,还是登录页,一个字都没变。
因为小红书这一步查的不是胸牌,是身份——你有没有登录后的 Cookie。胸牌只能证明”我假装是浏览器”,Cookie 才能证明”我是某个登录过的用户”。后者是第 10 章的内容。
现在你只需要带走两件事:200 不等于抓到了内容;换 UA 只是对抗的最外层一层皮,有的站连这层皮都不看。
检查你自己
自测:三道题,全过进下一章
题 1:把脚本的 URL 改成
http://quotes.toscrape.com/page/2/,跑一遍。 过关标准:状态码 200,且打印内容和第 1 页不同。你能说清楚”第 2 页”这个信息是写在 URL 的哪一段里的。题 2:把 URL 改成
http://quotes.toscrape.com/this-page-does-not-exist,跑一遍。 过关标准:程序没报错,打印出 404。你能说清楚为什么程序没崩——以及,为什么”程序没崩”这件事本身是个陷阱(上面小红书那一节你已经见过它的第一种形态:状态码漂亮,内容为空)。题 3:存下来的
page.html用浏览器打开,排版为什么没了?再换一个带图片的站存一次,图片为什么会裂? 过关标准:不用翻书,直接说出”HTML 只是文本,样式和图片是独立资源,我只下载了 HTML 本身”。
三题全过,下一章见。下一章我们把这六行代码按在地上拆开,看你和服务器之间到底互相说了些什么。