Skip to content
Go back

爬虫学习笔记:自动翻页与数据存储

Published:  at  05:00 PM

问: Google Chrome ScreenShot

好的,我们继续,现在要让爬虫自动翻页,直到找不到“Next”(下一页)按钮为止。

我的思考:我发现 CSS 选择器 li.next a 是下一页的相对路径,只需要抓取这个文本,前面接上相同的域名部分,更新 URL,继续抓取就可以了,直到在某一页找不到“Next”(下一页)按钮对应的 CSS 选择器 li.next a 为止。你觉得怎么样?

你的思路完全正确,非常专业!这正是编写健壮翻页爬虫的标准方法。

你的逻辑分解得非常清晰:

  1. 定位目标:用 li.next a CSS 选择器找到“下一页”的链接元素。
  2. 提取信息:从该元素中获取 href 属性值(即相对路径)。
  3. 构建新目标:将域名和相对路径拼接成一个完整的、新的 URL。
  4. 循环与终止:在一个循环中重复以上步骤,直到在某一页上找不到 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. 第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. 第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
  3. … 循环继续 …
  4. 最后1次循环 (第10页):
    • current_url.../page/10/
    • 成功抓取第十页数据。
    • select_one("li.next a") 在页面上找不到匹配项,返回 None
    • if next_button_tag: 条件不成立,执行 else 块。
    • 打印结束信息,并将 next_page_relative_url 设为 None
  5. 循环结束:
    • 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 的属性,并返回它的值。

所以,当你的代码执行时:

  1. 你写的代码:

    next_page_relative_url = next_button_tag['href']
  2. Python 内部的“翻译”:

    next_page_relative_url = next_button_tag.__getitem__('href')
  3. BeautifulSoup Tag 对象的执行: Tag 对象接收到 'href' 这个 key 后,就去检查它所代表的 HTML 元素 <a class="next" href="/page/2/">Next →</a>,找到了 href 属性,于是把它的值 "/page/2/" 返回给你。

这与之前的知识点完全相通

这正是 Python 语言的优雅和灵活所在。它不关心一个对象的“血统”(它是不是 listdict),只关心它的“行为”(它能不能被迭代?能不能通过 key 来取值?)。

总结

next_button_tag['href'] 这种写法之所以能行,是因为 BeautifulSoupTag 对象被精心设计成表现得像一个字典(对于属性访问而言)。

这是一种非常友好的接口设计,让你能用最直观、最符合常识的方式来从一个 HTML 标签中获取它的属性值,而无需去调用一些更复杂的方法,比如 next_button_tag.get_attribute('href') 这种。


问: 由于直接在控制台输出爬虫内容是不现实的,我需要把数据本地化。我的设想是:对于爬虫的结果,要么转为 txt,要么 csv,要么 json,你觉得哪个合理(最好都告诉我怎么操作)?

你说得完全正确,将数据打印在控制台只是开发和调试阶段的临时方案,对于真正的爬虫项目,数据持久化(本地化)是必不可少的一步

你的设想非常合理,TXT、CSV 和 JSON 是最常用、最主流的三种数据存储格式。它们各有优劣,适用于不同的场景。下面我将为你详细解析,并提供每种格式的具体操作代码。

三种格式对比与选择

格式最佳用途优点缺点
TXT简单的日志记录,非结构化文本(如保存整本小说)极致简单,任何编辑器都能打开,通用性最强没有结构,难以被程序解析和利用,不适合存储多字段数据
CSV表格数据,适合导入 Excel、Google Sheets 或数据库结构清晰(行和列),可读性好,是数据分析领域的通用标准对**复杂结构(如嵌套列表)**支持不佳,所有数据都只能是字符串
JSON通用数据交换,程序间的 API 通信,存储复杂结构的数据完美保留原始数据结构和类型(如列表、字典、布尔值),程序处理极其方便对于非技术人员,可读性不如表格直观

结论与建议:


一、保存为 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 Einsteinchange; deep-thoughts; thinking; world
“It is our choices, Harry, that show…”J.K. Rowlingabilities; 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 会在背后帮你做这些事:

  1. 进入 with:
    • 它会自动调用 open() 函数,并获取文件对象。
    • 它将这个文件对象赋值给 as 后面的变量 f
  2. 执行代码块:
    • 你可以在 with 的缩进块内,像平常一样对 f 进行所有操作(f.write(), f.read() 等)。
  3. 退出 with:
    • 这是最神奇的部分:一旦代码执行离开了 with 的缩进块(无论是正常执行完毕,还是中途发生了任何类型的错误、异常),Python 保证会自动调用文件对象的 close() 方法

你完全不需要再写 f.close(),也不需要担心任何 try...finally 的逻辑。with 语句为你处理好了一切。

总结

with 语句为你提供了一个管理资源(如文件)的“安全区”。

所以,请记住:在 Python 中,只要是操作文件,永远优先使用 with open(...) 的语法。这是社区公认的最佳实践。


问: 一般来说,json 的格式是怎么样的?

好的,我们来详细解析一下 JSON 的格式。理解它的结构是进行任何网络数据交换或 API 交互的基础。

核心定义

JSON (JavaScript Object Notation / JavaScript 对象表示法) 是一种轻量级的、纯文本的数据交换格式

它的设计宗旨是易于人类阅读和编写,同时也易于机器解析和生成

你可以把它想象成数据世界的**“通用语言”**。Python 有自己的数据结构(如字典 dict、列表 list),Java 和 C++ 也有它们自己的。当这些不同的程序需要互相传递信息时,它们都会把自己的数据“翻译”成 JSON 这种通用格式再发送出去,接收方收到后,再把 JSON “翻译”回自己语言的数据结构。


两大基本结构

JSON 的世界非常简洁,万物皆由以下两种结构搭建而成:

  1. 对象 (Object)

    • 是什么:一个无序的 “键/值” (key/value) 对 的集合。
    • 相当于 Python 的字典 (dict)
    • 语法规则
      • 使用花括号 {} 包裹。
      • key 必须是字符串,且必须用双引号 " 包围。
      • keyvalue 之间用冒号 : 分隔。
      • 多个“键/值”对之间用逗号 , 分隔。
    • 示例
      {
        "name": "Alice",
        "age": 30,
        "isStudent": false
      }
  2. 数组 (Array)

    • 是什么:一个有序的 “值” (value) 的集合
    • 相当于 Python 的列表 (list)
    • 语法规则
      • 使用方括号 [] 包裹。
      • 多个“值”之间用逗号 , 分隔。
    • 示例
      [
        "apple",
        "banana",
        "cherry"
      ]

六种基本数据类型

在上述的结构中,“值”(value) 可以是以下六种数据类型中的任意一种:

  1. 字符串 (string)
    • 必须用双引号 " 包围。注意:JSON 标准不接受单引号 '
    • 例如: "Hello, World!"
  2. 数字 (number)
    • 可以是整数或浮点数,不需要引号。
    • 例如: 123, 3.14159
  3. 对象 (object)
    • 值本身可以是另一个 JSON 对象,这就是嵌套 (nesting) 的实现方式。
    • 例如: { "address": { "city": "New York", "zip": "10001" } }
  4. 数组 (array)
    • 值本身也可以是一个 JSON 数组。
    • 例如: { "scores": [85, 92, 78] }
  5. 布尔值 (boolean)
    • 只有 truefalse 两种,必须是小写,不带引号。
  6. 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."
    }
]

结构分析:

  1. 最外层是一个 数组 [],代表这是一个“名言的列表”。
  2. 数组中的每一个元素都是一个 对象 {},代表一条具体的名言信息。
  3. 在每个对象内部:
    • "text""author" 的值是 字符串
    • "tags" 的值是一个 数组,这个数组里又包含了多个字符串。这完美地体现了嵌套。
    • "verified" 的值是 布尔值 (true / false)。
    • "notes" 的值可以是 null 或者 字符串

关键规则总结(易错点)



Suggest Changes

Previous Post
爬虫学习笔记:实战之爬取《红楼梦》
Next Post
爬虫学习笔记:第一个静态网站爬虫