Tutorial · 示例页面
用 Python 写第一个 Web 爬虫
一份零构建、可离线打开的教程页面示例。正文中文,技术术语首次出现附英文;所有示例都真实可运行,目标站点为教学专用站 quotes.toscrape.com。
你将学到什么
- 用
requests发送请求(request)并读取响应(response) - 用 BeautifulSoup 解析(parse)HTML 并提取字段
- 清洗数据并写入 CSV 文件
- 处理分页(pagination)与基础反爬兜底
- 把以上能力组织成一个完整、可运行的爬虫项目
环境准备与项目结构
先把「枪」备好:安装 Python、装好 requests 与 BeautifulSoup,并建立清晰的目录骨架。
检查 Python 与 pip
爬虫本质上是「用程序发请求、再处理返回的内容」,而 Python 是完成这件事最顺手的选择。这里我们假设你已经写过一些 Python(见过变量、函数和循环),但还从没写过爬虫。
第一步是确认电脑上装了 Python。打开终端(terminal),输入下面这条命令,注意看第 1 行输出的版本号(重点):
python3 --version
pip3 --version
如果看到类似 Python 3.11.6 的输出,说明环境就绪。本文全部示例要求 Python 3.8+。若命令提示找不到 python3,请先到 python.org 安装,再回来继续。
强烈建议为教程项目创建虚拟环境(virtual environment,简称 venv),把依赖装进隔离空间,避免污染系统 Python。创建方式见下一小节。
安装依赖库
我们只用两个第三方库(library):requests 负责发 HTTP 请求,beautifulsoup4(常简写为 bs4)负责解析返回的 HTML。先用 venv 建环境,再用 pip 安装:
mkdir crawler-tutorial && cd crawler-tutorial
python3 -m venv .venv
source .venv/bin/activate
激活环境后,命令行前缀会出现 (.venv)。接下来安装依赖:
pip install requests beautifulsoup4
安装完成后可以验证版本,顺便把依赖清单(dependency list)固定下来,方便日后复现环境:
python3 -c "import requests, bs4; print(requests.__version__, bs4.__version__)"
pip freeze > requirements.txt
搭建项目结构
别急着写代码。先想清楚这个爬虫要产出什么,把文件规划出来。一个极简但完整的爬虫项目通常长这样:
crawler-tutorial/
├── .venv/ # 虚拟环境(不提交到 git)
├── requirements.txt # 依赖清单
├── crawler.py # 主脚本(先留空)
└── data/ # 输出目录(先建好)
└── quotes.csv # 之后爬下来的数据
目录就是项目的「地图」。等我们写到第 6 章的完整项目时,你会发现好结构能让你一眼找到:代码在哪、依赖在哪、输出在哪。
爬虫有「边界」:请只在目标站点允许的前提下抓取。本教程使用的 quotes.toscrape.com 是官方教学站,专门开放给学习者练习。抓取任何其他站点前,请先读它的 robots.txt(我们会在第 5 章讲)。
Q1. 要安装解析 HTML 用的库,正确的命令是哪条?
Q2. 虚拟环境(venv)的主要作用是?
动手任务
创建虚拟环境并安装依赖,然后确认版本号能打印出来。
- 能运行
python3 -c "import requests, bs4"且不报错 - 工作目录里有
requirements.txt和空的data/目录
发送 HTTP 请求
爬虫的第一步是「问网站要东西」。这一章我们用 requests 发一个真实请求,并学会读懂返回。
第一个 GET 请求
爬虫的所有动作都建立在 HTTP 协议上:你的程序扮演一个客户端(client),向服务器(server)发送请求(request),服务器把页面内容作为响应(response)还回来。其中最常见的请求是 GET——「把某个资源给我」。
在项目目录下新建 crawler.py,输入下面这段代码(注意第 3 行的 URL 是教学站首页):
import requests
url = "https://quotes.toscrape.com/"
resp = requests.get(url)
print(resp.status_code)
运行 python3 crawler.py,你应该看到 200。状态码(status code)200 表示「成功」——服务器找到了这个页面并正常返回。代码只有四行,但「发请求」这件事已经完成了。
如果网络不通或站点不可达,requests 会抛出 requests.exceptions.ConnectionError。先检查网络,再检查 URL 是否拼写正确。这是新手最常见的第一个报错。
读懂响应:状态码与响应体
一个 HTTP 响应由三块组成:状态行(状态码 + 原因短语)、响应头(headers,描述元信息)和响应体(body,真正的页面内容)。我们把三样都打印出来看看:
print("状态码:", resp.status_code, resp.reason)
print("编码:", resp.encoding)
print("响应头 Content-Type:", resp.headers.get("Content-Type"))
print("响应体前 200 个字符:\n", resp.text[:200])
你应该会看到类似下面的输出(内容以实际为准):
状态码: 200 OK
编码: ISO-8859-1
响应头 Content-Type: text/html; charset=utf-8
响应体前 200 个字符:
有两个点值得记住(重点):
resp.text返回的是 解码后的字符串,可以直接搜索、切片、交给解析器。resp.status_code的常用取值:200成功、404页面不存在、403禁止访问、429请求太频繁。爬虫要养成「先看状态码,再碰内容」的习惯。
| 维度 | requests | urllib.request |
|---|---|---|
| 学习成本 | 低,API 直观 | 中等,样板代码多 |
| 设置请求头 | headers={...} 传字典即可 | 需构造 Request 对象 |
| 超时(timeout) | 内置参数,直接支持 | 需手动处理 |
| 会话与重试 | 内置 Session / 第三方配合 | 几乎要全手写 |
| 中文教程生态 | 主流,资料最多 | 相对少 |
设置请求头 User-Agent
网站服务器会从请求头里读到「你是谁」。默认情况下 requests 会带上 python-requests/x.y 这样的标识(User-Agent)。很多站点会据此把爬虫挡在门外(403)。
解决办法是把 User-Agent 伪装成浏览器。大部分真实浏览器 UA 长得像下面第 3 行那样(重点):
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0 Safari/537.36",
}
resp = requests.get(url, headers=headers, timeout=10)
print("服务器看到的 UA:", resp.request.headers["User-Agent"])
注意第 8 行的 timeout=10:给每次请求设超时(timeout)是爬虫的底线素养——否则网络卡住时程序会一直挂在那里。
伪装 UA 是为了「像普通用户一样访问」,不代表可以绕过站点的访问控制或授权限制。合理用法是:低频、节流、只取公开数据。
Q1. 状态码 200 表示什么?
Q2. 服务器主要靠哪个请求头识别「这是不是浏览器」?
动手任务
请求 quotes.toscrape.com,打印出状态码、响应头里的 Server 字段,以及响应体总长度(字符数)。
- 能打印
200状态码 - 能打印
Server头(如nginx/1.25.3) - 能打印响应体字符数(约 8000+)
解析 HTML
请求拿回来的是整张网页字符串。这一章学怎么用 BeautifulSoup 从中「挑出」我们想要的内容。
认识 BeautifulSoup
quotes.toscrape.com 首页每一行名言(quote)在 HTML 里都长这样(我们关心的是 class 为 text 和 author 的元素):
<div class="quote">
<span class="text">“The world as we have created it is a process of our thinking...”</span>
<span>by <small class="author">Albert Einstein</small></span>
</div>
BeautifulSoup 会把这段 HTML 文本变成一棵可遍历的「树」,我们就能用名字或属性去树上找节点。基本用法三步:先创建解析对象(parser),再调用查找方法(find / find_all / select)。
from bs4 import BeautifulSoup
soup = BeautifulSoup(resp.text, "html.parser")
first_quote = soup.find("div", class_="quote")
print(first_quote.text[:120])
第 3 行:用 html.parser(Python 内置)把字符串解析成 soup 对象。第 5 行 find 返回第一个匹配的元素;注意参数是 class_(带下划线),因为 class 在 Python 里是关键字(keyword)。
find_all 与 CSS 选择器
首页一共有 10 条名言。把「找第一个」换成「找全部」,再配合循环,就拿到了全部数据。两种写法殊途同归:
# 写法一:find_all 按标签 + class
quotes = soup.find_all("div", class_="quote")
print("用 find_all 找到", len(quotes), "条")
# 写法二:select 用 CSS 选择器(效果相同)
quotes2 = soup.select("div.quote")
print("用 select 找到", len(quotes2), "条")
两种都返回一个列表。写法的取舍看个人习惯:find_all 参数直观;select 能表达更复杂的路径,比如「所有 div 里的 span」写成 soup.select("div span")。
写选择器之前,先在浏览器里「右键 → 检查」目标元素,确认它的标签名和 class。写对选择器,解析就成功了一半。
提取第一条 quote 的字段
现在把「一句话」拆成三个字段:名言文本、作者、标签。注意第 6 行,作者藏在 <small class="author"> 里(重点):
quote = quotes[0]
text = quote.find("span", class_="text").text
author = quote.find("small", class_="author").text
tags = [tag.text for tag in quote.find_all("a", class_="tag")]
print("名言:", text)
print("作者:", author)
print("标签:", tags)
输出大致是:
名言: “The world as we have created it is a process of our thinking...” # 截断示意
作者: Albert Einstein
标签: ['change', 'deep-thoughts', 'thinking', 'world']
| 方法 / 属性 | 作用 | 示例 |
|---|---|---|
find(tag, ...) | 返回第一个匹配节点 | soup.find("div", class_="quote") |
find_all(tag, ...) | 返回全部匹配节点(列表) | soup.find_all("span") |
select(css) | 用 CSS 选择器查找 | soup.select("div.quote .text") |
.text | 取节点内全部纯文本 | tag.text |
["attr"] | 取属性值 | a["href"] |
Q1. 哪个方法使用 CSS 选择器来查找元素?
Q2. 为什么 bs4 里写 class_ 而不是 class?
动手任务
抓取首页并打印出全部 10 条名言的作者。
- 能打印出 10 位作者(含重复作者也算)
- 代码里同时用到了
find_all与select各至少一次
数据清洗与存储
解析出来的是「差不多能用」的数据。这一章把它洗干净、结构化,再落盘成 CSV。
清洗文本
直接从网页拿到的文本往往带着杂质:名言首尾有多余空格或引号、作者旁边可能有换行、标签里混着空白。清洗(cleaning)的目标是让数据「可分析、可入库」。
这一站比较干净,我们主要做三件事:去首尾空白、去掉多余的引号字符、过滤空标签(重点在第 2、5 行):
def clean_text(s):
"""去掉首尾空白并剥掉中文/英文引号"""
s = s.strip()
s = s.strip("\u201c\u201d\u2018\u2019\"'")
return s.strip()
raw = "“The world as we have created it...”"
print(clean_text(raw))
\u201c 是中文左双引号 “,\u201d 是右双引号 ”。Python 源码里写成转义序列是为了可读性和跨平台稳定(重点:转义序列永远比直接粘贴特殊字符可靠)。
写出 CSV
把清洗后的数据一行一行写进 CSV(逗号分隔值,comma-separated values)。CSV 是表格类数据最通用的交换格式,Excel、Pandas、数据库都能直接吃。
推荐用 csv.DictWriter:字段名写在表头,行数据用字典(dictionary)组织,一眼就能看出「这一列是什么」(重点在第 3 行):
import csv
rows = [] # 每行一个 dict:{"text": ..., "author": ..., "tags": ...}
for q in quotes:
rows.append({
"text": clean_text(q.find("span", class_="text").text),
"author": q.find("small", class_="author").text.strip(),
"tags": ", ".join(t.text for t in q.find_all("a", class_="tag")),
})
with open("data/quotes.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["text", "author", "tags"])
writer.writeheader()
writer.writerows(rows)
print("已写入", len(rows), "行")
跑完检查一下 data/quotes.csv,前两行应该长这样:
text,author,tags
The world as we have created it is a process of our thinking...,Albert Einstein,"change, deep-thoughts, thinking, world"
It is our choices, Harry, that show what we truly are...,J.K. Rowling,"abilities, choices"
写 CSV 时 encoding="utf-8" 和 newline="" 缺一不可:前者保证中文不乱码,后者避免 Windows 下出现多余空行。这是中文爬虫数据落盘最常见的两个坑。
编码与追加
把第 4 章和第 5 章连起来想:第 5 章要抓 10 页,难道每页都重写一次文件?正确姿势是「只写一次表头,其余页用追加(append)模式」。下面这段在第 5 章会直接用:
def append_rows(filepath, rows):
write_header = not Path(filepath).exists()
with open(filepath, "a", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["text", "author", "tags"])
if write_header:
writer.writeheader()
writer.writerows(rows)
第 4 行判断文件是否已存在:文件不存在时写表头,存在时直接追加。这样「跑一次补一页」也不会弄坏表头。
| 目的 | 做法 | 示例 |
|---|---|---|
| 去首尾空白 | str.strip() | " abc ".strip() → "abc" |
| 去指定字符 | str.strip(chars) | "'abc'".strip("'") → "abc" |
| 合并空白 | re.sub(r"\s+", " ", s) | 多空格/换行 → 单个空格 |
| 字符串拼接 | ", ".join(list) | 标签列表 → 逗号分隔字符串 |
Q1. 写 CSV 时为什么需要 newline=""?
动手任务
把首页 10 条名言清洗后写入 data/quotes.csv。
- CSV 共 11 行(1 行表头 + 10 行数据)
- 名言文本没有首尾引号,作者列无空白
- 用文本编辑器或 Excel 打开无乱码
分页与反爬
首页只有 10 条,可教学站一共有 100 条。这一章解决「翻页抓全部」,以及怎么当一个不讨人厌的爬虫。
理解分页 URL
quotes.toscrape.com 把内容按页(page)切分:第 2 页的 URL 是 https://quotes.toscrape.com/page/2/,第 3 页是 .../page/3/。规则简单到可以写进一个 for 循环。
我们先手动验证一下第 2 页能正常请求:
import requests
for page in range(1, 4):
url = f"https://quotes.toscrape.com/page/{page}/"
resp = requests.get(url, headers=headers, timeout=10)
print(f"第 {page} 页 -> {resp.status_code}, 长度 {len(resp.text)}")
第 4 行的 f-string(f-string)用 {page} 把页码拼进 URL。看到三个 200,就说明分页规律找对了。
循环抓取全部页面
教学站总共 10 页。把第 4 章的解析逻辑接进来,一个「抓全部」的循环就成型了。注意第 8 行:先看状态码再干活,避免把 404 页面也当成数据存下来(重点):
from bs4 import BeautifulSoup
all_rows = []
for page in range(1, 11):
resp = requests.get(f"https://quotes.toscrape.com/page/{page}/",
headers=headers, timeout=10)
if resp.status_code != 200:
print(f"第 {page} 页失败: {resp.status_code}")
continue
soup = BeautifulSoup(resp.text, "html.parser")
for q in soup.select("div.quote"):
all_rows.append({
"text": clean_text(q.select_one(".text").text),
"author": q.select_one(".author").text.strip(),
"tags": ", ".join(t.text for t in q.select("a.tag")),
})
print("共抓到", len(all_rows), "条")
这一段把第 3、4 章的知识全部串起来了:选择器定位、清洗、结构化。跑完应该输出 共抓到 100 条。
现实世界的站点不会让你 for i in range(1, 100000) 白嫖。分页循环必须配合「何时停」的判断:比如下一页链接为空就退出,而不是写死页数。见下方「优雅停止」示例。
更稳的写法是跟着「下一页」按钮走,而不是硬编码页数:
url = "https://quotes.toscrape.com/"
while url: # 下一页为空时退出
resp = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(resp.text, "html.parser")
next_link = soup.select_one("li.next a")
url = "https://quotes.toscrape.com" + next_link["href"] if next_link else None
print("抓取:", url)
第 4 行找「下一页」按钮:没有它就把 URL 置空,第 7 行 while url 自动结束循环。这样即使站点哪天加了页,也不用改代码。
延时、重试与节流
高频请求会占用服务器资源,触发反爬(anti-crawling)机制:返回 403/429,甚至封 IP。三个基础兜底:
- 延时(delay):每页之间
time.sleep()休息一下。 - 重试(retry):请求失败或超时,退避后重来几次。
- 节流(throttle):控制每秒请求数,整体压低频率。
import time
def get_with_retry(url, max_retries=3):
for attempt in range(max_retries):
try:
resp = requests.get(url, headers=headers, timeout=10)
if resp.status_code == 200:
return resp
except requests.RequestException:
pass
time.sleep(2 ** attempt) # 指数退避:2s, 4s, 8s
return None
resp = get_with_retry("https://quotes.toscrape.com/page/2/")
print("结果:", resp.status_code if resp else "多次重试仍失败")
第 7 行的指数退避(exponential backoff)是重试的黄金法则:第一次失败等 2 秒,第二次 4 秒,第三次 8 秒——给服务器喘息,也提高自己成功的概率。每次请求之间再补一个 time.sleep(1),就是最朴素的节流。
最后,别忘了爬虫的道德课。教学站的 robots.txt 你可以先读为敬:
curl -s https://quotes.toscrape.com/robots.txt
# User-agent: *
# Allow: /page/*
# Disallow: /
看懂了吗?它允许抓 /page/*(我们的目标恰好在这里),禁止抓首页之外的部分。照着规则来,就是「规矩的爬虫」。
robots.txt 是「君子协定」,不是法律。真正判断「能不能抓」还得看网站的条款(ToS)与内容版权。教学站允许练习,不代表所有站点都这样。
Q1. 为什么爬虫要在请求之间加 time.sleep()?
Q2. 指数退避(exponential backoff)的含义是?
动手任务
抓取全部 10 页名言并追加写入 CSV,加上延时与重试。
- 最终
data/quotes.csv有 101 行(1 行表头 + 100 行数据) - 代码里包含
time.sleep()与重试函数 - 抓取过程中没有任何 403/429 状态码
完整项目实战与总结
把前五章碎片拼成一个像样的项目:带函数、带配置、带输出目录,跑一遍收获 100 条数据。
整理成项目
散装的脚本能跑,但不好维护。一个「拿得出手」的爬虫项目至少有四样:入口(main())、可配置项(URL、延时、输出路径)、职责拆分的函数、以及清晰的输出。把配置集中放到文件顶部,是第 1 章说的「地图」落地的地方:
BASE_URL = "https://quotes.toscrape.com"
HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..."}
DELAY = 1.0 # 每页之间延时(秒)
MAX_RETRIES = 3 # 最大重试次数
OUT_CSV = "data/quotes.csv"
完整脚本 crawler.py
下面就是完整项目的主脚本,约 60 行。它把前五章的函数全部收编,结构一目了然:
import csv
import time
from pathlib import Path
import requests
from bs4 import BeautifulSoup
BASE_URL = "https://quotes.toscrape.com"
HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0 Safari/537.36"}
DELAY = 1.0
MAX_RETRIES = 3
OUT_CSV = "data/quotes.csv"
FIELD_NAMES = ["text", "author", "tags"]
def clean_text(s):
"""去掉首尾空白并剥掉中文/英文引号"""
s = s.strip().strip("\u201c\u201d\u2018\u2019\"'")
return s.strip()
def get_with_retry(url, max_retries=MAX_RETRIES):
"""带指数退避的请求,失败返回 None"""
for attempt in range(max_retries):
try:
resp = requests.get(url, headers=HEADERS, timeout=10)
if resp.status_code == 200:
return resp
except requests.RequestException:
pass
time.sleep(2 ** attempt)
return None
def parse_quotes(html):
"""从一页 HTML 中提取名言列表"""
soup = BeautifulSoup(html, "html.parser")
rows = []
for q in soup.select("div.quote"):
rows.append({
"text": clean_text(q.select_one(".text").text),
"author": q.select_one(".author").text.strip(),
"tags": ", ".join(t.text for t in q.select("a.tag")),
})
return rows
def append_rows(rows):
"""追加写入 CSV,文件不存在时先写表头"""
write_header = not Path(OUT_CSV).exists()
Path(OUT_CSV).parent.mkdir(parents=True, exist_ok=True)
with open(OUT_CSV, "a", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=FIELD_NAMES)
if write_header:
writer.writeheader()
writer.writerows(rows)
def crawl():
"""从第一页开始,跟着『下一页』一直抓到没有为止"""
url = BASE_URL + "/page/1/"
total = 0
while url:
resp = get_with_retry(url)
if resp is None:
print("跳过:", url)
break
rows = parse_quotes(resp.text)
append_rows(rows)
total += len(rows)
print(f"已抓 {len(rows)} 条({url}),累计 {total}")
soup = BeautifulSoup(resp.text, "html.parser")
next_link = soup.select_one("li.next a")
url = BASE_URL + next_link["href"] if next_link else None
time.sleep(DELAY)
print(f"完成,共 {total} 条,写入 {OUT_CSV}")
if __name__ == "__main__":
crawl()
第 10 行起是「配置区」,crawl() 函数把抓取主循环写清楚:请求 → 解析 → 落盘 → 翻页 → 延时,一步不落。运行它:
python3 crawler.py
输出会像这样逐页滚动:
已抓 10 条(https://quotes.toscrape.com/page/1/),累计 10
已抓 10 条(https://quotes.toscrape.com/page/2/),累计 20
...
完成,共 100 条,写入 data/quotes.csv
「读的即是下载的」——上面的脚本与示例项目压缩包里的 crawler.py 完全一致。可以直接下载完整项目到本地跑:
解压后先 pip install -r requirements.txt,再 python3 crawler.py。需要 Python 3.8+,断网或站点维护时会优雅跳过而不是崩溃。
运行与总结
至此,你的第一个 Web 爬虫已经跑通了完整生命周期:请求 → 解析 → 清洗 → 存储 → 分页 → 反爬兜底。回看整个旅程,真正有用的不是这 100 条名言,而是三件可迁移的东西:
- 方法论:任何抓取任务都是「先看结构 → 再写选择器 → 后加兜底」。
- 工程习惯:配置集中、函数拆分、先看状态码、尊重 robots.txt。
- 排错思路:403 查 UA、429 查频率、404 查 URL、乱码查编码——每一章踩过的坑都有对应的信号。
| 术语 | 英文 | 一句话解释 |
|---|---|---|
| 请求 | request | 客户端向服务器要资源的动作 |
| 响应 | response | 服务器返回的状态与内容 |
| 解析 | parse | 把 HTML 文本变成可查询的结构 |
| 分页 | pagination | 把内容切成多页返回的机制 |
| 反爬 | anti-crawling | 站点识别并限制爬虫的手段 |
| 指数退避 | exponential backoff | 重试间隔随次数指数级增长 |
| 虚拟环境 | virtual environment | 隔离项目依赖的 Python 环境 |
Q1. 完整项目里,分页循环最稳妥的结束方式是什么?
动手任务
运行完整脚本,然后验证结果。
- 运行
python3 crawler.py输出「完成,共 100 条」 data/quotes.csv共 101 行,无重复数据- 删除 CSV 后重跑一次,仍然正常生成(验证表头逻辑)
常见问题 FAQ
目标站点反爬很严,怎么办?
先读 robots.txt 确认自己有没有越界;然后从「礼貌」入手:降低频率、加延时、设随机延时、换更真实的 UA、用 Session 保持连接。再不行才考虑代理池、分布式等重手段——但对绝大多数学习项目,前几条就够了。
抓下来的数据量太大,内存爆了怎么办?
不要让数据全部堆积在内存里。养成「抓一页、存一页」的习惯(本教程的 append_rows 就是这个思路)。海量数据时换用流式写入、SQLite 或数据库,而不是一次性 all_rows.append(...)。
页面结构一改,解析就失败,怎么降低脆弱性?
选择器尽量「从稳不从全」:优先用语义明确的 class,少用依赖层级的位置选择器;关键字段加 try/except 兜底;把选择器集中到配置区方便一处修改。爬虫维护的常态就是「跟着改版走」。
会不会因为爬虫被封 IP?
低频率、尊重 robots.txt 的爬虫极少触发封禁。封 IP 通常源于高频请求、异常 UA、或对敏感资源的密集访问。请始终把延时开足、把范围控制在公开内容。
抓下来的数据可以商用吗?
看站点条款(ToS)与内容版权。教学站可随意练习,但很多站点的数据有使用限制。商用前请务必阅读目标站点的授权条款,必要时咨询法律意见——这是爬虫从业者的底线。
为什么用 requests + BeautifulSoup,而不是只用标准库?
标准库 urllib 能做到,但样板代码多、头处理繁琐、没有超时与重试的友好封装。requests 的 API 更直观,bs4 的解析器容错性也远好于手写正则。选型理由详见第 2 章对比表。