Requests+BeautifulSoup:Python爬虫入门与实战全解析
2026/9/9 20:18:05 网站建设 项目流程

我第一次写爬虫的时候,面对一个再普通不过的新闻列表页,完全不知道从哪里下手。浏览器能正常打开,数据肉眼可见,但右键查看源代码之后,密密麻麻的HTML标签直接把我劝退了。手动复制了半天的数据,胳膊酸了不说,第二次换了个页面又得重来一遍。后来我才逐渐摸清,做Web爬虫入门,最稳的一条路就是用Requests把网页数据拿回来,再用BeautifulSoup从HTML里把信息挑出来。这一对组合,至今都是我在做静态页面采集、页面内容提取时最常用的基础方案。

这篇文章适合刚开始接触Python爬虫、想快速搭建一套“请求—解析—保存”完整流程的读者。不管你是写脚本处理重复性工作,还是做数据分析需要批量获取公开数据,这套组合都能帮你把思路理清楚。我会把环境准备、两个库的核心用法、一个完整可复现的小项目、还有真正运行中会遇到的429限流问题和数据清洗事项,都按我自己踩过坑的顺序讲一遍。

1. 为什么入门爬虫,我首推Requests加BeautifulSoup这对组合

1.1 浏览器里能看到的,不一定都能直接Copy下来

很多人开始爬虫的契机,就是觉得“这页面上的数据我手动复制太麻烦”。但真到自己动手写脚本时,第一个疑惑就是:我到底该用什么工具去拿数据?

其实可以这么理解:浏览器帮我们做了太多事——发请求、下载HTML、解析CSS、执行JavaScript、渲染页面。爬虫要做的第一步,只是把里面最核心的“发请求、拿HTML”这一步抽出来,自己控制。而Requests这个库,就是把“发请求”这件事做到了极致简单。它面向Python的urllib,把URL拼接、请求头设置、Cookie管理、重定向处理这些琐碎细节全部包了一层,让开发者用几行代码就能完成一次HTTP请求。

拿到HTML之后,第二件事就是从这堆标签里找到目标数据。BeautifulSoup干的就是这个活:它把HTML解析成一棵标签树,你可以按标签名、class、id、属性甚至CSS选择器去定位元素。整个过程很像在一张有目录的大报纸里找某一栏新闻——你不需要逐字读,按标题、栏目、关键词去查就行。

1.2 主流方案对比:Requests解析、Scrapy框架和Selenium模拟浏览器

很多教程一上来就推荐Scrapy,或者干脆让人直接学Selenium。但我觉得入门阶段,工具越简单越好,关键是把原理吃透。我整理了一个对比,方便你自己判断:

方案适合场景学习成本主要问题
Requests + BeautifulSoup静态页面、普通列表页、接口返回的HTML拿不到动态渲染的内容
Scrapy大规模采集、需要分布式、有清晰爬取规则中高框架概念多,调试链路长
Selenium / Playwright需要登录、页面内容由JavaScript动态生成重量级,响应慢,维护成本高

所以我的建议很明确:先学Requests加BeautifulSoup。等到你真遇到“页面源码里什么都没有,数据全靠JS动态加载”的时候,再考虑上Selenium或Playwright也不迟。而且即使后面你改用Scrapy,它的下载器本质上也还是请求和响应这套逻辑,BeautifulSoup的解析器也仍然可以直接作为Scrapy的Item处理工具使用,现在学的这些基本功不会白费。

1.3 这对组合的学习曲线,适合什么阶段的开发者

Python基础语法刚学完、知道字典和列表、懂一点函数和模块的用法,就完全可以上手。条件有两点,第一点是HTML基础不需要很精通,但最好知道标签长什么样,比如<div><a><h2>这些常见结构;第二点是理解HTTP的GET和POST大概是什么意思,不理解也能跑通,但理解了更容易排查问题。

就算这两点都还模糊,也不用太担心。下面正文里我会把关键概念用生活化的方式解释。你要做的,只是准备一个能运行Python3的电脑,然后跟着一步步操作。

2. 动手前的环境准备:版本、虚拟环境、装库避坑

2.1 Python版本选择和安装

首先明确一点,现在用Python 3.8以上版本都可以,我个人建议直接用3.10或更高版本。原因很简单,语法更简洁,第三方库的兼容性也更好。安装的时候,Windows用户要特别注意一点:安装向导第一步会有一个“Add Python to PATH”的复选框,一定要勾上,否则后面在命令行里敲python会提示找不到命令。

安装完成后,打开终端或命令行窗口,输入:

python --version

如果弹出版本号,说明安装成功。有些Windows环境会安装成Microsoft Store版本,这时候终端会直接打开应用商店,而不是输出版本号。遇到这种情况,建议去Python官网下载安装包重新装,别在Store版上浪费时间。

2.2 使用venv虚拟环境,避免把系统搞乱

我见过太多初学者直接往系统Python里pip install一堆库,装到后面不同项目之间的依赖版本互相冲突,然后又不知道是哪个包导致的,只能重装环境。这个坑我自己也踩过。

所以从第一天开始,就养成用虚拟环境的习惯。方法很简单,先创建一个项目文件夹,然后执行:

mkdir spider_demo cd spider_demo python -m venv venv

创建好之后,激活它:

  • Windows:venv\Scripts\activate
  • Linux / macOS:source venv/bin/activate

激活成功后,命令行前面会出现(venv)字样,这就表示你现在的Python环境是独立的项目环境了。后面装什么包都不会污染系统Python,项目之间也不会互相干扰。

2.3 pip安装Requests、BeautifulSoup和lxml

激活虚拟环境后,直接执行:

pip install requests beautifulsoup4 lxml

这里一次装了三个包。requests是HTTP请求库;beautifulsoup4是解析HTML用的,注意命令行里包名是beautifulsoup4,导入时用的却是bs4lxml是BeautifulSoup的解析器引擎,解析速度快,容错能力也不错。装好之后可以验证一下:

python -c "import requests; print(requests.__version__)" python -c "from bs4 import BeautifulSoup; print(BeautifulSoup)"

正常输出版本号或对象信息,说明安装成功。

如果你在安装时下载速度特别慢,可以临时指定国内镜像源:

pip install requests beautifulsoup4 lxml -i https://pypi.tuna.tsinghua.edu.cn/simple

这是我的习惯做法,能省下不少时间。

2.4 运行环境里最容易忽略的细节

这里提三个我实际中经常遇到的问题提醒一下。

第一个,确保虚拟环境激活后再装包和运行脚本,不要开新终端就直接跑,新终端默认不会自动激活虚拟环境。第二个,Windows用户如果遇到pip不是内部或外部命令,多半是之前没勾“Add to PATH”,重装Python即可。第三个,别直接在系统自带的Python里装包,macOS和很多Linux发行版会把系统Python保护起来,强行安装轻则权限报错,重则影响系统工具,用venv能绕开这些麻烦。

3. Requests库的核心用法:从简单GET到维护会话

3.1 第一次发起请求:GET和响应对象

写爬虫最基础的动作,就是把一个网页的HTML拿下来。在Requests里,这只要一行代码:

import requests url = "https://example.com" resp = requests.get(url) print(resp.status_code) print(resp.text[:300])

resp.status_code是HTTP状态码,200表示正常;resp.text就是服务端返回的HTML文本。这里有个非常容易踩的坑:如果直接用resp.text打印中文,经常会出现乱码。原因是Requests会猜测页面的编码方式,有时猜错了。解决办法是主动指定编码:

resp.encoding = "utf-8"

或者用resp.apparent_encoding让Requests基于页面内容自行检测。更稳妥一点的做法:

resp.encoding = resp.apparent_encoding

这一行能解决很大一部分中文乱码问题,但也别盲目信任它,后面我会结合BeautifulSoup再讲更系统的处理思路。

3.2 带参数请求和请求头伪装

很多列表页面都支持翻页,URL会是这种形式:

https://example.com/articles?page=2&size=10

手动拼URL当然可以,但更规范的方式是用params参数:

params = { "page": 2, "size": 10, } resp = requests.get("https://example.com/articles", params=params)

Requests会自动把字典拼接成查询串,这样代码更清晰,参数再多也好维护。

请求头是另一个关键点。很多网站会检查User-Agent,如果你的请求看起来像是脚本,它可能直接返回错误页面。默认情况下Requests的User-Agentpython-requests/x.x.x,这等于把身份写在了脸上。所以在构造请求时,最好伪装成一个浏览器的身份:

headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9", } resp = requests.get(url, params=params, headers=headers)

这里的User-Agent字符串直接从浏览器开发者工具里复制就行,不需要背。

3.3 Session:保持登录态和Cookie的关键

如果你需要连续访问同一个网站,比如先登录、再获取个人页面数据,那强烈建议使用Session,而不是连续多次调用requests.get()

Session在底层维护了一个连接池和Cookie存储,相当于一个“浏览器会话实例”。第一次请求时服务器下发的Cookie,后续请求会自动带上。举个例子:

session = requests.Session() session.headers.update(headers) login_data = { "username": "your_account", "password": "your_password", } session.post("https://example.com/login", data=login_data) resp = session.get("https://example.com/profile") print(resp.text)

使用Session的好处是两个:一是保持了登录状态,二是复用了底层TCP连接,爬取速度更快、对服务器也更友好。就算不需要登录,只是连续多次访问同一站点的不同页面,也可以统一用session.get,比反复创建新连接好得多。

3.4 超时和重试机制:不要长时间干等

初学者最常犯的错,就是请求不带timeout参数。如果一个接口无响应,脚本会一直挂在那里,看起来像死循环。正确做法是给每次请求设置超时:

resp = requests.get(url, headers=headers, timeout=10)

timeout=10表示10秒内没有收到响应就抛出异常。更细致一点,可以分别设置连接超时和读取超时:

timeout=(3, 10)

也就是连接阶段最多3秒,数据读取阶段最多10秒。这样比单一超时值更合理。

关于重试机制,我单独写一段,因为这里的坑真的很多。

3.5 网络异常、HTTP错误码和重试策略

网络请求不是每次都能成功。常见的异常包括requests.ConnectionErrorrequests.Timeout,还包括服务端返回5xx错误、429限流等。我的原则是:能重试的错误就重试,不能重试的错误就直接报错停止。

最简单的重试方式是用循环:

import time def fetch_with_retry(url, headers, retries=3): for attempt in range(retries): try: resp = requests.get(url, headers=headers, timeout=(3, 10)) if resp.status_code == 200: return resp elif resp.status_code in (429, 500, 502, 503): time.sleep(2 * (attempt + 1)) continue else: resp.raise_for_status() except requests.RequestException as e: print(f"请求异常:{e}") time.sleep(2 * (attempt + 1)) return None

这里的time.sleep(2 * (attempt + 1))是退避等待,第一次重试等2秒,第二次等4秒,第三次等6秒。这样既给了服务器喘息时间,也提高了自己请求成功的概率。后面第6章我会结合429限流,再讲一种更标准化的重试封装方式。

4. BeautifulSoup解析逻辑:从HTML标签里精准找到目标数据

4.1 解析器选型:html.parser还是lxml

当你把HTML文本传给BeautifulSoup时,它需要一个解析器来理解这堆字符串。内置的html.parser不需要额外安装,但解析速度一般。lxml解析速度快、容错能力强,是绝大多数场景下的首选,前提是你在环境准备阶段已经装好了lxml

解析器速度容错性依赖建议
html.parser中等一般内置临时测试可用
lxml需要安装lxml日常首选
html5lib基于浏览器标准,容错极强需要安装html5lib处理不规范HTML时用

我自己写爬虫,基本都固定用lxml。有些HTML标签严重不闭合,用html.parser解析出来的树是乱的,换成lxml往往就好了。

4.2 BeautifulSoup对象:把HTML变成一棵标签树

用BeautifulSoup加载HTML后,会得到一个soup对象,这个对象把HTML组织成了一棵可以用属性和方法遍历的树。最简单的方法是直接用标签名访问:

from bs4 import BeautifulSoup html_doc = """ <ul> <li class="item">第一项</li> <li class="item">第二项</li> <li>第三项</li> </ul> """ soup = BeautifulSoup(html_doc, "lxml") print(soup.li)

print(soup.li)只会输出第一个<li>标签,因为直接访问标签名相当于“找到第一个匹配项”。要拿到所有<li>,就要用find_all

4.3 find()和find_all():最基础也最好用的定位方式

find()返回第一个匹配的标签,find_all()返回所有匹配的列表。它们都支持按标签名、属性、class、id等条件过滤。

items = soup.find_all("li") for item in items: print(item.get_text())

如果要按class查找,注意class是Python关键字,所以要用class_

items = soup.find_all("li", class_="item")

还可以用字典传多个属性:

node = soup.find("a", attrs={"class": "title", "data-id": "123"})

find_all还支持过滤函数,比如只保留含某个关键字的标签,不过入门阶段会用前两种就够应付绝大多数页面了。

4.4 CSS选择器:用select()优雅提取

如果说find_all是按你手动指定的条件查找,那select()则是让你直接用CSS选择器的语法去选元素。这对有过前端经验的人来说非常友好,即使没经验,学起来也很快。

# 选择class为item的所有li items = soup.select("li.item") # 选择id为main下的所有a标签 links = soup.select("#main a") # 选择class为title的h2标签 titles = soup.select("h2.title")

拿到节点后,常用操作有三个。

取文本:

text = node.get_text(strip=True)

取属性:

href = node["href"]

取子孙节点:

children = node.select("span.date")

这里我特别推荐get_text(strip=True)这个参数,它会把文本前后的空格和换行自动去掉,省去后面再清洗的功夫。我在写解析代码时几乎每个节点都会带上strip=True

4.5 标签导航:从一个节点找到它的父级和兄弟

有时候目标数据不在同一个标签里,比如标题在<h2>中,但对应的链接在邻近的<a>里。这时可以使用标签树的导航属性。

title_node = soup.select_one("h2.title") # 父节点 parent = title_node.parent # 下一个兄弟节点 next_node = title_node.find_next_sibling() # 上一个兄弟节点 prev_node = title_node.find_previous_sibling()

select_oneselect的区别,类似findfind_all,前者只拿一个节点。实战里最常见的情况是,卡片结构里既有标题又有摘要,先用CSS选择器定位到整张卡片,再在卡片内部二次查找。这种方式代码可读性很高。

cards = soup.select("div.card") for card in cards: title = card.select_one("h2 a").get_text(strip=True) summary = card.select_one("p.summary").get_text(strip=True) link = card.select_one("h2 a")["href"]

4.6 中文乱码的完整处理思路

解析阶段最让人头疼的就是乱码,但绝大多数情况下问题出在编码检测,而不是解析器。我给一个标准处理流程,按顺序排查:

第一步,查看网页源代码里的<meta charset>声明,知道页面真正的编码是什么。比如<meta charset="gbk">,那就在拿到响应后手动指定resp.encoding = "gbk"

第二步,如果页面没声明编码,用resp.apparent_encoding自动检测。

第三步,如果resp.text已经乱码了,直接操作resp.content

from bs4 import BeautifulSoup soup = BeautifulSoup(resp.content, "lxml", from_encoding="gbk")

resp.content是原始的字节数据,不受Requests自动解码影响。把原始字节和正确的编码一起交给BeautifulSoup,它就能正确解析出文本。这一招尤其在处理老旧的GBK编码网站时特别管用。

5. 一个小项目串起全流程:抓取文章列表并保存

5.1 项目目标、目标站点选择和合规检查

这一章我用一个完整的示例来把前面所有知识点串联起来。假设的场景是:抓取一个技术博客类网站的文章列表页,提取每篇文章的标题、链接、发布日期和摘要。我在代码里用示例地址占位,你自己实际使用时替换成合规目标。

动手之前,第一批要检查的是:页面是否只包含公开信息?网站是否允许爬取?robots.txt里有没有明确禁止采集?

先看一眼robots.txt,这一步很简单:

import requests robots_url = "https://example.com/robots.txt" resp = requests.get(robots_url, timeout=10) print(resp.text)

如果里面写着Disallow: /article/,那就别碰这个目录。虽然有robots.txt不代表绝对法律边界,但至少是网站管理员的意愿体现,入门阶段的爬虫选手应该学会尊重它。

5.2 目标页面结构和解析方案

我假设目标页面结构像这样:

<div class="post"> <h2><a href="/post/1">标题一</a></h2> <p class="date">2024-11-20</p> <p class="summary">这是摘要内容</p> </div> <div class="post"> <h2><a href="/post/2">标题二</a></h2> <p class="date">2024-11-19</p> <p class="summary">这是另一篇摘要</p> </div>

解析方案就很直接了:外层用div.post定位到每一篇文章卡片,内层用h2 a拿标题和链接,用p.date拿日期,用p.summary拿摘要。找到规律之后,解析代码几乎就是结构复刻。

5.3 完整代码:带请求、解析、保存和礼貌策略

下面是一段能直接运行的示例脚本:

import csv import random import time import requests from bs4 import BeautifulSoup BASE_URL = "https://example.com/articles" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", } def fetch_page(url, headers=HEADERS, timeout=(3, 10)): resp = requests.get(url, headers=headers, timeout=timeout) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text def parse_article_list(html): soup = BeautifulSoup(html, "lxml") articles = [] for card in soup.select("div.post"): link_node = card.select_one("h2 a") if not link_node: continue title = link_node.get_text(strip=True) link = link_node["href"] if link.startswith("/"): link = "https://example.com" + link date_node = card.select_one("p.date") date_text = date_node.get_text(strip=True) if date_node else "" summary_node = card.select_one("p.summary") summary = summary_node.get_text(strip=True) if summary_node else "" articles.append({ "title": title, "link": link, "date": date_text, "summary": summary, }) return articles def save_to_csv(articles, filename="articles.csv"): with open(filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title", "link", "date", "summary"]) writer.writeheader() writer.writerows(articles) def main(): html = fetch_page(BASE_URL) articles = parse_article_list(html) save_to_csv(articles) for article in articles: print(article) print(f"共抓取 {len(articles)} 篇文章") if __name__ == "__main__": main()

这里有几个细节值得说清楚。

第一,resp.encoding = resp.apparent_encoding这行很关键,尤其在抓取中文页面时,能避免标题和摘要显示成乱码。

第二,链接补全做了一个判断:如果href是相对路径,就拼上站点的域名,这样保存下来的链接才是可以直接访问的完整URL。很多新手爬到这步会忽略,结果保存的链接全是/post/1这种无法直接打开的地址。

第三,CSV文件保存时用了encoding="utf-8-sig",而不是普通的utf-8。这个区别很重要:如果你用Excel直接打开一个普通utf-8编码的CSV,中文会乱码;而utf-8-sig会在文件开头写一个BOM标记,Excel能正确识别,这是Windows环境下保存中文CSV的标准姿势。

5.4 运行结果和常见报错排查

运行这个脚本,如果顺利,终端会打印出每条文章信息,并生成一个articles.csv文件。如果报错,大概率是这几类:

出现requests.exceptions.ConnectionError,说明目标网站连不上,可能是URL写错、网络不通,或者网站主动拒绝了这个请求。先确认浏览器能正常打开同一个地址,再用curl或浏览器开发者工具对比请求头。

出现raise_for_status()触发的HTTPError,说明页面返回了4xx或5xx状态码。403通常是被反爬机制拦截了,需要检查User-Agent和请求头;404是URL路径不对;500是对方服务器问题,可以稍后重试。

出现AttributeError: 'NoneType' object has no attribute 'get_text',说明select_one没找到节点,返回了None。大多数情况是页面结构和你写的不一样,或者整个页面被反爬策略替换成了验证页面。遇到这种报错,先打印resp.text看看真正返回的HTML是什么。

6. 真实爬虫最常撞的墙:429限流与请求被识别

6.1 429状态码到底在说什么

在你开始写爬虫之后,很快就会遇到一个状态码:429 Too Many Requests。这个状态码的意思是:服务器已经明确告诉你,请求频率太快了,短期内不要再来了。

我最早遇到429的时候,还以为是程序写错了,反复检查URL和请求头,后来又加了一堆代理池,结果不仅没解决,反而让问题更严重。实际上429不是错误,它更像服务器在对你说:你是个好用户,但你现在太热情了,稍微冷静一下。处理好429的正确姿势,不是去绕开它,而是合理放慢节奏。

6.2 请求频率设计:每秒请求数和随机延迟

一个很实际的建议:如果只是做个人学习和小规模数据采集,请求间隔设置成1到2秒,大多数正常网站都不会为难你。如果目标页面数量特别大,那就要考虑更长的间隔,比如2到5秒,并且加上随机抖动,避免出现固定频率的规律请求。

一个简单的随机延迟写法:

import random import time time.sleep(random.uniform(1, 2))

这样每次请求之间的间隔在1到2秒之间随机波动,既不会对服务器造成压力,也更接近真人浏览的行为。这个看似不起眼的小技巧,比什么高级反封策略都管用。

6.3 用Retry机制优雅应对限流

requests本身不内置重试机制,但底层依赖的urllib3提供了Retry类,可以把重试逻辑做得更标准。看下面这段代码:

from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() retry = Retry( total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET"], ) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter) resp = session.get("https://example.com/articles", timeout=(3, 10))

这里的backoff_factor值决定了退避等待时间。urllib3的实际等待时间是0.5 * backoff_factor * (2 ^ attempt),也就是第一次重试前等0.5秒,第二次等1秒,第三次等2秒。这个指数退避策略比固定时间等待更科学:给服务器更多恢复时间,同时避免自己的重试风暴加重服务器负担。

另外注意我把allowed_methods限定为["GET"],因为对POST请求自动重试是有风险的。POST可能造成数据重复提交,而GET是幂等操作,重试几次都安全。这个细节,很多人容易忽略。

6.4 那些号称“教你绕过封禁”的资料,为什么我不建议学

入门阶段你会看到很多文章分享“绕过IP限制”“破解验证码”等灰色操作。我建议你直接跳过。一方面,很多网站的反爬策略越来越复杂,暴力绕过手段的生命周期很短,学了很快就失效;另一方面,爬虫技术本身是中性工具,但一旦被用于突破网站访问控制,问题的性质就变了。正确做法不是想怎么绕,而是想怎么优雅地、合规地获取数据。控制频率、合理设置User-Agent、遵守robots.txt、处理429限流时退避重试,这套思路才是可持续的。

7. 数据清洗与落地保存:爬下来只是开始

7.1 清洗空白字符和残缺字段

从网页里解析出来的文本,经常带着乱七八糟的空白字符、换行和缩进。get_text(strip=True)已经解决了一部分,但有时候仍会出现连续的空白。可以用正则表达式做一次统一清洗:

import re def clean_text(text): return re.sub(r"\s+", " ", text).strip()

re.sub(r"\s+", " ", text)会把所有连续的空白字符(包括换行和制表符)替换成单个空格,再strip()去掉首尾空格。摘要字段经过这一步处理后,会变得干净很多。

残缺字段的处理思路是:有条件就补全,补不了就给默认值。上面示例代码里,date_node不存在时返回空字符串,而不是直接报错,就是出于这个考虑。

7.2 时间字段规范化

网页上的日期格式五花八门,可能是2024-11-20,也可能是2024年11月20日,还可能是3天前这种相对时间。如果需要统一格式,建议都转成标准日期字符串:

from datetime import datetime raw_date = "2024年11月20日" normalized = raw_date.replace("年", "-").replace("月", "-").replace("日", "") print(normalized) # 输出: 2024-11-20

如果字符串格式比较标准,可以直接用datetime.strptime解析:

dt = datetime.strptime("2024-11-20", "%Y-%m-%d")

统一之后,数据后续做排序、筛选时间区间,会很方便。

7.3 CSV和JSON保存时的编码细节

CSV已经在前面的示例里提到了utf-8-sig。JSON的保存也有一个类似的坑,就是中文会被转成\u开头的ASCII转义序列,看起来非常难受。解决办法是在json.dump时设置ensure_ascii=False

import json with open("articles.json", "w", encoding="utf-8") as f: json.dump(articles, f, ensure_ascii=False, indent=2)

indent=2是让JSON有缩进,直接打开看也方便调试。这里提醒一点:如果JSON文件也要用Excel打开,那就别用JSON格式,直接用之前的CSV方式,Excel对JSON支持并不友好。

7.4 数据去重的简单策略

爬取过程中经常遇到重复数据,比如某篇文章同时出现在列表页的两个分类下。最简单的去重策略,是在内存里维护一个已经见过的URL集合:

seen = set() for link in link_list: if link in seen: continue seen.add(link)

如果数据量特别大,超过了内存能承受的范围,可以把已访问URL持久化到文件里,每次启动脚本时加载。入门阶段用集合就够了,但养成“重要字段必须去重”的意识很要紧,因为后续做大项目时,重复数据会直接影响分析结果。

8. 爬虫的边界:分清哪些数据能爬,哪些不能碰

8.1 robots.txt、公开数据和用户协议

如果你平时只在本地做小规模、学习性质的爬虫,遵守一个核心原则就够了:只爬公开可见的数据,不碰需要登录才能看到的数据,不碰有明确协议禁止采集的数据。登录后的数据通常涉及用户账户和隐私,这已经超出了“公开数据采集”的范畴。

robots.txt值得看一眼,但我也要说清楚:robots.txt规范了爬虫对站点的访问偏好,不是法律强制文件。但一个负责任的爬虫开发者,应该把它当作“网站管理员意愿”来看待。额外还要看网站的用户协议和条款,有些网站会在条款里写明“禁止使用自动化工具采集数据”,这种即使技术上能爬,也不应该作为数据源。

8.2 个人学习与商业采集的界限

个人学习和研究用途,小规模爬取公开数据,通常问题不大。但一旦数据用于商业变现、大规模公开传播,或者采集的是个人隐私信息、商业交易数据,风险会急剧上升。我见过太多人栽在这上面:不只是技术难度,而是数据来源的风险。

我的原则很简单,宁可多花时间找一个合规的数据渠道,也不在灰色边缘试探。

8.3 请求频率和资源占用,是爬虫的道德底线

爬虫对服务器造成的压力,很多时候比访问限制更值得关注。一次正常的网页请求,服务器要处理数据库查询、渲染模板、返回内容,如果脚本以每秒几十次的速度发起请求,对于小站点来说很容易直接把服务拖垮。

所以无论你爬什么,都记住这一点:速度比你想象的慢一点,再慢一点。1到2秒的间隔,加上随机抖动,既不影响你的数据获取,也能体面地不打扰别人。

8.4 我个人的实操体会

回到一开始说的那个新闻列表页。现在我写爬虫之前会先做三件事:查一下robots.txt,估算一下自己需要的数据量,再设计一个比真人浏览更从容的请求节奏。做完这些,真正写代码的时间和调试反爬的时间会大幅减少,数据质量也更稳定。

Requests和BeautifulSoup这套组合,陪我处理过很多重复的列表页提取、信息整理工作。它不花哨,但胜在稳定和可控。对于刚入门爬虫的你来说,我的建议是先把这一套用得滚瓜烂熟,再考虑Scrapy这类重量级框架。把请求、解析、保存、处理限流和频率控制这套基本功打牢,遇到什么项目心里都有底。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询