知识点思维导图
28 个知识节点
参考资料
Python(28) - 爬虫入门
读完后,你应能完成以下任务:
- 绘制“Python(28) - 爬虫入门 / 先给锚点:爬虫 = 没有浏览器的 fetch + querySelector”的关键对象与数据流,解释“关键边界(哪里不一样):requests 拿到的是服务器原始返回的那段 HTML 字符串,它不是浏览器——不会执行 JavaScript。”,并用源码位置、日志或 Trace 标注证据。
- 为“Python(28) - 爬虫入门 / requests:发请求的四件事”设计正常与异常输入,验证“边界提醒:和 fetch 一样,requests 对 4xx/5xx 不会自动报错,它只是如实返回那个状态码。”,输出首个偏差位置与回归测试结果。
- 实现“Python(28) - 爬虫入门 / 伪装成浏览器:User-Agent 与请求头”的最小代码或配置,检验“原因是:默认 requests 会带一个 User-Agent: python-requests/2.x 的请求头,等于在门口举牌「我是爬虫」。”,输出命令、结果与 Diff,并说明不适用边界。
你在前端见多了「调后端 JSON 接口」的活,但很多数据根本没有接口,只有一个网页。爬虫要解决的就是:把浏览器替你做的事——发请求拿 HTML、再从 HTML 里抠出想要的数据——用代码自动跑一遍。本篇用两件兵器把这条链路打通:
requests负责「发请求」(≈ 你熟的fetch/axios),BeautifulSoup负责「解析 HTML 抠数据」(≈ 浏览器里的document.querySelector)。学完你能写出一个能跑、能翻页、不被一脚封掉的最小爬虫。
阶段六是「自动化」主线,本篇是入口。这里不碰 Scrapy 那种重型框架,先把「请求 + 解析」这条最小可用链路吃透,下一篇(29)再讲怎么把脚本调度起来。
一、先给锚点:爬虫 = 没有浏览器的 fetch + querySelector
前端里你要从一个页面拿数据,心智模型是这样的:
// JS:在浏览器/Node 里抓页面 + 解析
const res = await fetch("https://example.com") // 发请求
const html = await res.text() // 拿到 HTML 字符串
// 浏览器里你会直接用 DOM 选择器抠元素:
const title = document.querySelector("h1").textContent
Python 爬虫就是把这两步换成两个库,心智几乎一一对应:
先用对照表把直觉立起来:
| 浏览器 / JS | Python 爬虫 | 说明 |
|---|---|---|
fetch(url) |
requests.get(url) |
发 GET 请求 |
res.text() |
resp.text |
拿响应体文本(HTML) |
res.json() |
resp.json() |
把 JSON 响应解析成对象 |
res.status |
resp.status_code |
HTTP 状态码 |
res.headers |
resp.headers |
响应头 |
document.querySelector(s) |
soup.select_one(s) |
CSS 选第一个 |
document.querySelectorAll(s) |
soup.select(s) |
CSS 选全部(返回列表) |
el.textContent |
el.get_text() |
取纯文本 |
el.getAttribute("href") |
el["href"] |
取属性 |
关键边界(哪里不一样):
requests拿到的是服务器原始返回的那段 HTML 字符串,它不是浏览器——不会执行 JavaScript。前端 SPA(React/Vue 渲染出来的页面)你 F12 看到的 DOM,和requests拿到的 HTML 往往对不上:后者可能只有一个空<div id="root">。这是新手第一大坑,第六节专门讲怎么判断和应对。
先装库:
pip install requests beautifulsoup4
# beautifulsoup4 是包名,但导入时写 from bs4 import ...,这俩名字对不上是历史原因,记住即可
二、requests:发请求的四件事
requests 的 API 设计得非常直白,前端调接口会的那几样这里全都有。
拿到 resp 后怎么用:
边界提醒:和
fetch一样,requests对 4xx/5xx 不会自动报错,它只是如实返回那个状态码。想让失败「炸出来」,得自己判断status_code或调raise_for_status()。这点和axios(默认对非 2xx reject)不同,反而更接近原生fetch。
三、伪装成浏览器:User-Agent 与请求头
你直接 requests.get 很多网站会返回 403 或一个「请用浏览器访问」的页面。原因是:默认 requests 会带一个 User-Agent: python-requests/2.x 的请求头,等于在门口举牌「我是爬虫」。
解决办法是把请求头伪装成真实浏览器,这和前端在 Node 里用 fetch 手动设 headers 是同一件事:
// JS 对照:Node 里手动设 headers,形状完全一样
const resp = await fetch("https://example.com", {
headers: {
"User-Agent": "Mozilla/5.0 ... Chrome/120.0.0.0 Safari/537.36",
"Accept-Language": "zh-CN,zh;q=0.9",
},
})
多次请求同一站点时,更专业的做法是用
requests.Session():它会自动复用连接、保留 cookie,相当于浏览器的「同一个会话」。需要登录态、要连着翻很多页时尤其有用。
四、BeautifulSoup:用 CSS 选择器从 HTML 抠数据
requests 拿到的 resp.text 是一坨 HTML 字符串。直接正则去抠又脆又难写,正确姿势是把它喂给 BeautifulSoup,得到一棵「能查询的 DOM 树」。
首选 select / select_one——直接写 CSS 选择器,和你前端肌肉记忆完全一致:
取文本和取属性:
边界(哪里不一样):
select返回的是普通 Python 列表,能直接for、len()、列表推导。这比浏览器里querySelectorAll返回的NodeList(伪数组,要Array.from才能用数组方法)更顺手。这是 Python 这边占便宜的地方。
CSS 选择器对照表(语法和前端一模一样,直接迁移):
| 选择器 | 含义 |
|---|---|
"div" |
所有 div 标签 |
".title" |
class=title |
"#main" |
id=main |
"ul > li" |
ul 的直接子级 li |
"a[href]" |
带 href 属性的 a |
"div.card a" |
card 卡片内的所有 a(后代) |
五、把它跑成一个完整最小爬虫
把前面的零件拼起来——一个「抓列表页 → 抠出每条标题和链接 → 翻页」的最小可用爬虫,这是你 90% 场景的骨架:
这套结构和你写前端「分页拉接口 + map 渲染」几乎同构,只是数据源从 JSON 接口换成了 HTML 页面。
六、新手必踩的坑
坑 1:页面是 JS 渲染的,requests 抓到的是空壳。
现代前端站(React/Vue SPA)的数据多是前端再发 ajax 填进去的。你 requests.get 拿到的 HTML 里根本没有那些数据,select 自然选不到,返回空列表。
- 怎么判断:把
resp.text打印出来,或者浏览器里「查看网页源代码」(不是 F12 的 Elements,那是渲染后的)。如果源码里搜不到目标文字,就是 JS 渲染。 - 怎么破:优先去 F12 的 Network 面板找它实际请求的那个 JSON 接口,直接
requests.get那个接口——又快又稳,连解析 HTML 都省了。实在没有接口、非渲染不可,才上playwright/selenium这种真·浏览器(重,本篇不展开)。
坑 2:中文乱码。 requests 有时猜错编码,中文变成「ä½ å¥½」。修法见上面代码:resp.encoding = resp.apparent_encoding,让它用 chardet 分析响应字节、统计推断真实编码(注意:它看的是原始字节,不是 <meta charset> 声明)。
坑 3:select_one 没匹配到却直接取属性 / 文本,报 AttributeError。
select_one 找不到时返回的是 None,你再 .get_text() 就会炸(类似 JS 的 Cannot read properties of null)。务必先判空:
坑 4:请求太快被封。 循环里不 sleep、并发狂打,轻则 IP 被限流,重则被拉黑。加 time.sleep、控制频率是基本礼貌,也是自保。
坑 5:无视 robots.txt 和站点条款。 爬之前看一眼目标站的 /robots.txt,别爬明令禁止的路径,别爬需要登录的私密数据,别把人家服务器打挂。这是合规底线,不是技术问题。
七、和 JS 生态的整体对照
| 能力 | JS/Node 生态 | Python 爬虫生态 |
|---|---|---|
| 发请求 | fetch / axios |
requests |
| 解析 HTML | cheerio(服务端 jQuery) |
BeautifulSoup |
| CSS 选择器 | querySelector(All) |
select_one / select |
| 真·浏览器(跑 JS) | puppeteer / playwright |
playwright / selenium |
| 重型爬虫框架 | crawlee |
Scrapy |
如果你之前用过 cheerio,那 BeautifulSoup 几乎是无缝迁移——都是「拿字符串、给选择器、抠数据」。
八、总结
- 先给锚点:爬虫 = 没有浏览器的 fetch + querySelector:关键边界(哪里不一样):requests 拿到的是服务器原始返回的那段 HTML 字符串,它不是浏览器——不会执行 JavaScript。
- requests:发请求的四件事:边界提醒:和 fetch 一样,requests 对 4xx/5xx 不会自动报错,它只是如实返回那个状态码。
- 伪装成浏览器:User-Agent 与请求头:原因是:默认 requests 会带一个 User-Agent: python-requests/2.x 的请求头,等于在门口举牌「我是爬虫」。
- BeautifulSoup:用 CSS 选择器从 HTML 抠数据:requests 拿到的 resp.text 是一坨 HTML 字符串。
- 把它跑成一个完整最小爬虫:这套结构和你写前端「分页拉接口 + map 渲染」几乎同构,只是数据源从 JSON 接口换成了 HTML 页面。
- 新手必踩的坑:坑 1:页面是 JS 渲染的,requests 抓到的是空壳。
学完自测
选择所有正确答案;提交后逐项核对判断依据。