问:
好的,我们继续,现在要让爬虫自动翻页,直到找不到“Next”(下一页)按钮为止。
我的思考:我发现 CSS 选择器
li.next a是下一页的相对路径,只需要抓取这个文本,前面接上相同的域名部分,更新 URL,继续抓取就可以了,直到在某一页找不到“Next”(下一页)按钮对应的 CSS 选择器li.next a为止。你觉得怎么样?
你的思路完全正确,非常专业!这正是编写健壮翻页爬虫的标准方法。
你的逻辑分解得非常清晰:
- 定位目标:用
li.next aCSS 选择器找到“下一页”的链接元素。 - 提取信息:从该元素中获取
href属性值(即相对路径)。 - 构建新目标:将域名和相对路径拼接成一个完整的、新的 URL。
- 循环与终止:在一个循环中重复以上步骤,直到在某一页上找不到
li.next a,循环自然结束。
这个逻辑完美地避免了“硬编码”总页数,无论网站有多少页,你的爬虫都能全部爬取。
现在,我们把这个逻辑翻译成代码。这里我会引入一个 Python 内置库 urllib.parse 中的 urljoin 函数,它可以更安全、更专业地处理 URL 拼接。
为什么需要 urljoin?
直接用字符串拼接 ("域名" + "/相对路径") 在大多数情况下可行,但当 URL 格式复杂时(比如 https://example.com/blog/ 和 ../articles/),就很容易出错。urljoin 会智能地处理这些情况,确保你总能得到一个合法的 URL。
实现自动翻页的最终代码
我们将使用一个 while 循环,它的执行与否取决于我们是否能找到“下一页”的链接。
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin # 导入 urljoin 函数
import time # 导入 time 模块,用于添加礼貌性延迟
# 爬虫的起点
base_url = "https://quotes.toscrape.com/"
# 我们从根路径开始,urljoin 会帮我们处理好
next_page_relative_url = "/"
# 当 next_page_relative_url 有值时,循环继续
while next_page_relative_url:
# 使用 urljoin 智能地拼接出当前要爬取的完整 URL
current_url = urljoin(base_url, next_page_relative_url)
print(f"--- 正在抓取: {current_url} ---")
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'}
response = requests.get(current_url, headers=headers)
# 增加一个检查,确保请求成功
if response.status_code != 200:
print(f"请求失败,状态码: {response.status_code}")
break
soup = BeautifulSoup(response.text, "lxml")
# (这部分抓取逻辑和你写的一样)
quotes_divs = soup.select('.quote')
for quote_div in quotes_divs:
text = quote_div.select_one(".text").text
author = quote_div.select_one(".author").text
tags = [tag.text for tag in quote_div.select("a.tag")]
print(text.strip(), author, tags, "----\n", sep='\n')
# --- 核心的翻页逻辑 ---
# 尝试寻找“下一页”按钮的 a 标签
next_button_tag = soup.select_one("li.next a")
# 如果找到了
if next_button_tag:
# 从 a 标签中提取 href 属性,作为下一次循环的相对路径
next_page_relative_url = next_button_tag['href']
# 礼貌性地暂停一小段时间,避免给服务器造成太大压力
time.sleep(0.5)
# 如果没找到(next_button_tag 为 None),说明是最后一页了
else:
print("\n--- 未找到'Next'按钮,爬取结束。---")
# 将 next_page_relative_url 设为 None,循环将在下一次检查时终止
next_page_relative_url = None
代码逻辑演练
- 第1次循环:
next_page_relative_url初始值为/。current_url变为https://quotes.toscrape.com/。- 成功抓取第一页数据。
select_one("li.next a")找到按钮,next_button_tag不为None。- 从
href中提取出/page/2/,赋值给next_page_relative_url。
- 第2次循环:
next_page_relative_url的值是/page/2/。current_url变为https://quotes.toscrape.com/page/2/。- 成功抓取第二页数据。
select_one("li.next a")找到按钮,next_button_tag不为None。- 从
href中提取出/page/3/,赋值给next_page_relative_url。
- … 循环继续 …
- 最后1次循环 (第10页):
current_url为.../page/10/。- 成功抓取第十页数据。
select_one("li.next a")在页面上找不到匹配项,返回None。if next_button_tag:条件不成立,执行else块。- 打印结束信息,并将
next_page_relative_url设为None。
- 循环结束:
while next_page_relative_url:(即while None:) 条件为假,循环优雅地终止。
你已经完全掌握了爬虫开发中一个非常核心且实用的技能,非常棒!
问:
next_button_tag['href']这个写法是否也意味着虽然next_button_tag不是一个字典,但是 python 仍然可以处理?
又是一个观察细致入微的好问题!
你的感觉完全正确:next_button_tag 确实不是一个标准的 Python 字典 dict,但它却神奇地支持字典的方括号 [] 取值语法。
这背后同样体现了 Python “鸭子类型 (Duck Typing)” 的强大之处,和你之前问的“为什么 ResultSet 不是列表却能用 for -循环”是完全相同的道理。
核心揭秘:特殊方法 __getitem__
在 Python 中,当你写下 some_object['some_key'] 这样的代码时,Python 解释器并不会检查 some_object 是不是一个真正的字典。
实际上,它会把这行代码“翻译”成对一个特殊方法的调用:
some_object.__getitem__('some_key')
任何一个类的作者,只要在他的类里定义了 __getitem__(self, key) 这个方法,那么这个类的实例(对象)就能够支持方括号 [] 这种取值方式。
BeautifulSoup 的开发者正是这么做的。
他们为 Tag 对象(也就是 next_button_tag 的类型)专门编写了 __getitem__ 方法。这个方法的内部逻辑是:
当用户用
['some_key']来访问我时,我就去我所代表的那个 HTML 标签的属性列表里,查找名字叫some_key的属性,并返回它的值。
所以,当你的代码执行时:
-
你写的代码:
next_page_relative_url = next_button_tag['href'] -
Python 内部的“翻译”:
next_page_relative_url = next_button_tag.__getitem__('href') -
BeautifulSoup
Tag对象的执行:Tag对象接收到'href'这个key后,就去检查它所代表的 HTML 元素<a class="next" href="/page/2/">Next →</a>,找到了href属性,于是把它的值"/page/2/"返回给你。
这与之前的知识点完全相通
for循环: 能作用于任何实现了__iter__()方法的对象。[]取值: 能作用于任何实现了__getitem__()方法的对象。
这正是 Python 语言的优雅和灵活所在。它不关心一个对象的“血统”(它是不是 list 或 dict),只关心它的“行为”(它能不能被迭代?能不能通过 key 来取值?)。
总结
next_button_tag['href'] 这种写法之所以能行,是因为 BeautifulSoup 的 Tag 对象被精心设计成表现得像一个字典(对于属性访问而言)。
这是一种非常友好的接口设计,让你能用最直观、最符合常识的方式来从一个 HTML 标签中获取它的属性值,而无需去调用一些更复杂的方法,比如 next_button_tag.get_attribute('href') 这种。
问: 由于直接在控制台输出爬虫内容是不现实的,我需要把数据本地化。我的设想是:对于爬虫的结果,要么转为 txt,要么 csv,要么 json,你觉得哪个合理(最好都告诉我怎么操作)?
你说得完全正确,将数据打印在控制台只是开发和调试阶段的临时方案,对于真正的爬虫项目,数据持久化(本地化)是必不可少的一步。
你的设想非常合理,TXT、CSV 和 JSON 是最常用、最主流的三种数据存储格式。它们各有优劣,适用于不同的场景。下面我将为你详细解析,并提供每种格式的具体操作代码。
三种格式对比与选择
| 格式 | 最佳用途 | 优点 | 缺点 |
|---|---|---|---|
| TXT | 简单的日志记录,非结构化文本(如保存整本小说) | 极致简单,任何编辑器都能打开,通用性最强 | 没有结构,难以被程序解析和利用,不适合存储多字段数据 |
| CSV | 表格数据,适合导入 Excel、Google Sheets 或数据库 | 结构清晰(行和列),可读性好,是数据分析领域的通用标准 | 对**复杂结构(如嵌套列表)**支持不佳,所有数据都只能是字符串 |
| JSON | 通用数据交换,程序间的 API 通信,存储复杂结构的数据 | 完美保留原始数据结构和类型(如列表、字典、布尔值),程序处理极其方便 | 对于非技术人员,可读性不如表格直观 |
结论与建议:
- 如果你想把数据导入 Excel 进行分析或给非技术人员查看,CSV 是最佳选择。
- 如果你想最完美地保留数据的原始结构(尤其是
tags这样的列表),或者这个数据未来可能被其他程序读取使用,JSON 是最佳选择。 - TXT 在我们这个场景下(每条名言都有多个字段)不推荐,但作为最基础的文件操作,我们也会介绍。
一、保存为 TXT 文件 (简单日志式)
这种方式最直接,就是把你想打印的内容,原封不动地写入文件里。
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import time
# 在循环开始前,只打开一次文件
# 'w' 代表写入模式,如果文件已存在会被覆盖
# encoding='utf-8' 是处理非英文字符的生命线,必须指定
with open('quotes.txt', 'w', encoding='utf-8') as f:
base_url = "https://quotes.toscrape.com/"
next_page_relative_url = "/"
while next_page_relative_url:
current_url = urljoin(base_url, next_page_relative_url)
print(f"--- 正在抓取: {current_url} ---") # 保留控制台日志
# ... (requests 和 beautifulsoup 的代码保持不变) ...
response = requests.get(current_url)
soup = BeautifulSoup(response.text, "lxml")
quotes_divs = soup.select('.quote')
for quote_div in quotes_divs:
text = quote_div.select_one(".text").text
author = quote_div.select_one(".author").text
tags = [tag.text for tag in quote_div.select("a.tag")]
# --- 核心改动:将 print 改为 f.write ---
# f.write() 需要一个字符串作为参数,我们用 f-string 格式化
f.write(f"名言: {text.strip()}\n")
f.write(f"作者: {author}\n")
f.write(f"标签: {', '.join(tags)}\n") # .join() 将列表转为逗号分隔的字符串
f.write("-" * 40 + "\n")
next_button_tag = soup.select_one("li.next a")
if next_button_tag:
next_page_relative_url = next_button_tag['href']
time.sleep(0.5)
else:
next_page_relative_url = None
print("所有内容已成功写入 quotes.txt")
quotes.txt 文件内容示例:
名言: “The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”
作者: Albert Einstein
标签: change, deep-thoughts, thinking, world
----------------------------------------
名言: “It is our choices, Harry, that show what we truly are, far more than our abilities.”
作者: J.K. Rowling
标签: abilities, choices
----------------------------------------
二、保存为 CSV 文件 (表格/Excel格式)
这是最实用的方式之一。我们需要使用 Python 内置的 csv 模块。
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import time
import csv # 导入 csv 模块
# 定义表头
header = ['名言', '作者', '标签']
# 'newline=""' 是为了防止写入 CSV 时出现多余的空行,是官方推荐写法
with open('quotes.csv', 'w', encoding='utf-8', newline='') as f:
# 创建一个 csv writer 对象
writer = csv.writer(f)
# 写入表头
writer.writerow(header)
base_url = "https://quotes.toscrape.com/"
next_page_relative_url = "/"
while next_page_relative_url:
current_url = urljoin(base_url, next_page_relative_url)
print(f"--- 正在抓取: {current_url} ---")
# ... (requests 和 beautifulsoup 的代码保持不变) ...
response = requests.get(current_url)
soup = BeautifulSoup(response.text, "lxml")
quotes_divs = soup.select('.quote')
for quote_div in quotes_divs:
text = quote_div.select_one(".text").text.strip()
author = quote_div.select_one(".author").text
tags_list = [tag.text for tag in quote_div.select("a.tag")]
# --- 核心改动:准备一行数据并写入 ---
# CSV 的一个单元格只能是字符串,所以我们将 tags 列表合并
tags_str = '; '.join(tags_list) # 用分号分隔,避免与CSV的逗号冲突
# 准备要写入的一行数据 (列表格式)
row = [text, author, tags_str]
# 写入这一行
writer.writerow(row)
# ... (翻页逻辑保持不变) ...
next_button_tag = soup.select_one("li.next a")
if next_button_tag:
next_page_relative_url = next_button_tag['href']
time.sleep(0.5)
else:
next_page_relative_url = None
print("所有内容已成功写入 quotes.csv")
quotes.csv 文件用 Excel 打开后示例:
| 名言 | 作者 | 标签 |
|---|---|---|
| “The world as we have created it…” | Albert Einstein | change; deep-thoughts; thinking; world |
| “It is our choices, Harry, that show…” | J.K. Rowling | abilities; choices |
三、保存为 JSON 文件 (完美保留结构)
这是最能体现程序思维的方式,也是最推荐的方式。我们需要使用 Python 内置的 json 模块。
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import time
import json # 导入 json 模块
# 创建一个空列表,用来存储所有爬取到的数据
all_quotes_data = []
base_url = "https://quotes.toscrape.com/"
next_page_relative_url = "/"
while next_page_relative_url:
current_url = urljoin(base_url, next_page_relative_url)
print(f"--- 正在抓取: {current_url} ---")
# ... (requests 和 beautifulsoup 的代码保持不变) ...
response = requests.get(current_url)
soup = BeautifulSoup(response.text, "lxml")
quotes_divs = soup.select('.quote')
for quote_div in quotes_divs:
text = quote_div.select_one(".text").text.strip()
author = quote_div.select_one(".author").text
tags_list = [tag.text for tag in quote_div.select("a.tag")]
# --- 核心改动:将每条数据存为一个字典,并添加到列表中 ---
quote_item = {
'text': text,
'author': author,
'tags': tags_list # JSON 可以完美地保存列表结构!
}
all_quotes_data.append(quote_item)
# ... (翻页逻辑保持不变) ...
next_button_tag = soup.select_one("li.next a")
if next_button_tag:
next_page_relative_url = next_button_tag['href']
time.sleep(0.5)
else:
next_page_relative_url = None
# 所有页面爬取完毕后,一次性将整个列表写入 JSON 文件
with open('quotes.json', 'w', encoding='utf-8') as f:
# ensure_ascii=False 保证中文等非 ASCII 字符正常显示
# indent=4 让 JSON 文件格式化,更易读
json.dump(all_quotes_data, f, ensure_ascii=False, indent=4)
print("所有内容已成功写入 quotes.json")
quotes.json 文件内容示例:
[
{
"text": "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”",
"author": "Albert Einstein",
"tags": [
"change",
"deep-thoughts",
"thinking",
"world"
]
},
{
"text": "“It is our choices, Harry, that show what we truly are, far more than our abilities.”",
"author": "J.K. Rowling",
"tags": [
"abilities",
"choices"
]
}
]
问:
with open('quotes.txt', 'w', encoding='utf-8') as f:这种语法没听说过,说一下。
好的,这个问题非常重要。你看到的 with open(...) as f: 语法,是 Python 中处理文件、网络连接、数据库会话等需要“获取资源”和“释放资源”操作时,最推荐、最安全、最现代的方式。
它被称为 with 语句,它所使用的技术叫做 上下文管理器 (Context Manager)。
为了理解它为什么这么好,我们先来看看不使用它会发生什么。
1. “传统”的文件操作方式及其隐患
在 with 语句出现之前,操作一个文件的标准流程是这样的:
# 1. 打开文件
f = open('quotes.txt', 'w', encoding='utf-8')
# 2. 对文件进行操作
f.write('这是一句名言。')
# 3. !!! 必须手动关闭文件 !!!
f.close()
这看起来很简单,但存在一个巨大的隐患:如果在 open() 和 close() 之间发生了任何错误,f.close() 这一行将永远不会被执行。
例如:
f = open('quotes.txt', 'w', encoding='utf-8')
# 假设这里发生了一个错误,比如我们试图用一个数字除以零
result = 10 / 0 # 这会立即抛出 ZeroDivisionError 异常
# 下面的代码将永远不会执行
f.write(str(result))
f.close()
后果是什么? 程序崩溃了,但文件对象 f 还占用着系统资源,没有被正确关闭。在简单的脚本里这可能不是大问题,但在大型或长时间运行的程序中,这会导致资源泄漏,最终可能耗尽系统资源或导致数据损坏。
2. try...finally:一种笨拙的解决方案
为了解决这个问题,有经验的程序员会使用 try...finally 结构来确保 close() 无论如何都会被调用:
f = None # 先声明变量
try:
f = open('quotes.txt', 'w', encoding='utf-8')
f.write('这是一句名言。')
# 同样,这里可能发生错误
finally:
# finally 块中的代码,无论 try 中是否发生错误,都保证会被执行
if f:
f.close()
这个方法能解决问题,但显而易见,它非常繁琐和冗长。你需要写很多额外的代码来保证一个简单的文件操作的安全性。
3. with 语句:优雅、安全、简洁的终极方案
with 语句就是为了替代上面那种笨拙的 try...finally 结构而生的。
当你写下这行代码时: with open('quotes.txt', 'w', encoding='utf-8') as f:
Python 会在背后帮你做这些事:
- 进入
with块:- 它会自动调用
open()函数,并获取文件对象。 - 它将这个文件对象赋值给
as后面的变量f。
- 它会自动调用
- 执行代码块:
- 你可以在
with的缩进块内,像平常一样对f进行所有操作(f.write(),f.read()等)。
- 你可以在
- 退出
with块:- 这是最神奇的部分:一旦代码执行离开了
with的缩进块(无论是正常执行完毕,还是中途发生了任何类型的错误、异常),Python 保证会自动调用文件对象的close()方法。
- 这是最神奇的部分:一旦代码执行离开了
你完全不需要再写 f.close(),也不需要担心任何 try...finally 的逻辑。with 语句为你处理好了一切。
总结
with 语句为你提供了一个管理资源(如文件)的“安全区”。
- 自动资源管理:它自动获取资源,并在离开“安全区”时自动释放资源。
- 代码更简洁:它用一行代码替代了复杂的
try...finally结构,让你的意图更清晰。 - 异常安全:它保证了即使在发生错误的情况下,资源也总能被正确清理。
所以,请记住:在 Python 中,只要是操作文件,永远优先使用 with open(...) 的语法。这是社区公认的最佳实践。
问: 一般来说,json 的格式是怎么样的?
好的,我们来详细解析一下 JSON 的格式。理解它的结构是进行任何网络数据交换或 API 交互的基础。
核心定义
JSON (JavaScript Object Notation / JavaScript 对象表示法) 是一种轻量级的、纯文本的数据交换格式。
它的设计宗旨是易于人类阅读和编写,同时也易于机器解析和生成。
你可以把它想象成数据世界的**“通用语言”**。Python 有自己的数据结构(如字典 dict、列表 list),Java 和 C++ 也有它们自己的。当这些不同的程序需要互相传递信息时,它们都会把自己的数据“翻译”成 JSON 这种通用格式再发送出去,接收方收到后,再把 JSON “翻译”回自己语言的数据结构。
两大基本结构
JSON 的世界非常简洁,万物皆由以下两种结构搭建而成:
-
对象 (Object)
- 是什么:一个无序的 “键/值” (key/value) 对 的集合。
- 相当于 Python 的:字典 (
dict)。 - 语法规则:
- 使用花括号
{}包裹。 key必须是字符串,且必须用双引号"包围。key和value之间用冒号:分隔。- 多个“键/值”对之间用逗号
,分隔。
- 使用花括号
- 示例:
{ "name": "Alice", "age": 30, "isStudent": false }
-
数组 (Array)
- 是什么:一个有序的 “值” (value) 的集合。
- 相当于 Python 的:列表 (
list)。 - 语法规则:
- 使用方括号
[]包裹。 - 多个“值”之间用逗号
,分隔。
- 使用方括号
- 示例:
[ "apple", "banana", "cherry" ]
六种基本数据类型
在上述的结构中,“值”(value) 可以是以下六种数据类型中的任意一种:
- 字符串 (string)
- 必须用双引号
"包围。注意:JSON 标准不接受单引号'。 - 例如:
"Hello, World!"
- 必须用双引号
- 数字 (number)
- 可以是整数或浮点数,不需要引号。
- 例如:
123,3.14159
- 对象 (object)
- 值本身可以是另一个 JSON 对象,这就是嵌套 (nesting) 的实现方式。
- 例如:
{ "address": { "city": "New York", "zip": "10001" } }
- 数组 (array)
- 值本身也可以是一个 JSON 数组。
- 例如:
{ "scores": [85, 92, 78] }
- 布尔值 (boolean)
- 只有
true或false两种,必须是小写,不带引号。
- 只有
- null
- 表示“空值”或“无”,必须是小写
null,不带引号。 - 它相当于 Python 中的
None。
- 表示“空值”或“无”,必须是小写
综合示例 (以我们的爬虫数据为例)
你之前爬取的 quotes.json 文件就是一个完美的综合示例。让我们来分析它的结构:
[
{
"text": "“The world as we have created it is a process of our thinking.”",
"author": "Albert Einstein",
"tags": [
"change",
"deep-thoughts",
"thinking",
"world"
],
"verified": true,
"notes": null
},
{
"text": "“It is our choices, Harry, that show what we truly are, far more than our abilities.”",
"author": "J.K. Rowling",
"tags": [
"abilities",
"choices"
],
"verified": false,
"notes": "Source needs verification."
}
]
结构分析:
- 最外层是一个 数组
[],代表这是一个“名言的列表”。 - 数组中的每一个元素都是一个 对象
{},代表一条具体的名言信息。 - 在每个对象内部:
"text"和"author"的值是 字符串。"tags"的值是一个 数组,这个数组里又包含了多个字符串。这完美地体现了嵌套。"verified"的值是 布尔值 (true/false)。"notes"的值可以是 null 或者 字符串。
关键规则总结(易错点)
- 键名必须是双引号字符串:
{ "key": "value" }是正确的,{ 'key': "value" }或{ key: "value" }都是错误的。 - 字符串值必须是双引号:
{ "key": "value" }是正确的,{ "key": 'value' }是错误的。 - 没有末尾逗号:在数组或对象的最后一个元素后面,不能有逗号。
[1, 2, 3]是正确的,[1, 2, 3,]是错误的(这和 Python 列表不同)。
- 关键字必须小写:
true,false,null。 - 没有注释:标准的 JSON 格式不支持注释。
