Python爬虫实战:通用爬虫引擎,只负责读取 YAML 配置文件,根据配置里的规则自动抓取、解析并存储数据!

㊙️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!

㊗️爬虫难度指数:⭐⭐⭐

🚫声明:本数据&代码仅供学习交流,严禁用于商业用途、倒卖数据或违反目标站点的服务条款等,一切后果皆由使用者本人承担。公开榜单数据一般允许访问,但请务必遵守"君子协议",技术无罪,责任在人。

全文目录:

      • [🌟 开篇语](#🌟 开篇语)
      • [1️⃣ 摘要(Abstract)](#1️⃣ 摘要(Abstract))
      • [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
      • [3️⃣ 合规与注意事项(必写)](#3️⃣ 合规与注意事项(必写))
      • [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
      • [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
      • [6️⃣ 核心定义:YAML 契约(The Contract)](#6️⃣ 核心定义:YAML 契约(The Contract))
      • [7️⃣ 核心实现:通用引擎(The Engine)](#7️⃣ 核心实现:通用引擎(The Engine))
      • [8️⃣ 运行方式与结果展示(必写)](#8️⃣ 运行方式与结果展示(必写))
      • [9️⃣ 常见问题与排错(💡 经验之谈)](#9️⃣ 常见问题与排错(💡 经验之谈))
      • [🔟 进阶优化(可选但加分)](#🔟 进阶优化(可选但加分))
      • [1️⃣1️⃣ 总结与延伸阅读](#1️⃣1️⃣ 总结与延伸阅读)
      • [🌟 文末](#🌟 文末)
        • [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
        • [✅ 互动征集](#✅ 互动征集)

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

  我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

  📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏👉《Python爬虫实战》👈

  
💕订阅后更新会优先推送,按目录学习更高效💯~

1️⃣ 摘要(Abstract)

🎯 目标:构建一个"通用爬虫引擎",它本身不包含任何特定网站的业务逻辑。它只负责读取 YAML 配置文件,根据配置里的规则自动抓取、解析并存储数据。

🛠 工具Python 3 + PyYAML (配置解析) + Parsel (类似 Scrapy 的强大解析库) + Requests

💡 读完收获

  1. 彻底理解**"数据与逻辑分离"**的架构思想。
  2. 学会设计一套优雅的爬虫 DSL(领域特定语言)
  3. 得到一个只需改改 YAML 就能爬遍天下列表页的"万能框架"。

2️⃣ 背景与需求(Why)

🤔 为什么要配置化?

想象一下,老板让你今天爬"新浪新闻",明天爬"网易新闻",后天爬"虎嗅"。

  • 传统做法sina_spider.py, 163_spider.py, huxiu_spider.py... 复制粘贴到手软,维护起来简直是地狱。🔥
  • 配置化做法spider_engine.py (永远不动) + sina.yaml, 163.yaml, huxiu.yaml
    如果网页改版了,你只需要改 YAML 里的一行 CSS 选择器,甚至不需要重启服务(如果你做了热加载的话)。

🎯 核心需求

我们需要定义一套规则,告诉引擎:

  1. 种子 URL 是什么?
  2. 列表在哪里(Loop 区域)?
  3. 每个字段怎么提取(Title, Link, Date)?
  4. 下一页怎么找(Pagination)?

3️⃣ 合规与注意事项(必写)

框架的威力与责任

当你拥有了快速生成爬虫的能力,意味着你产生流量的速度也会倍增。

  1. 全局限速 :在框架底层必须强制加入 Global Delay(全局延迟),防止用户在 YAML 里配置了 0 秒延迟导致对目标站点发起 DDoS 攻击。
  2. User-Agent 池:框架应默认提供随机 UA,避免因特征过于单一被封杀。
  3. 合法合规:该框架仅用于公开数据的采集,严禁用于绕过验证码或抓取鉴权接口。

4️⃣ 技术选型与整体流程(What/How)

🛠 为什么选 Parsel 而不是 BS4?

这是一个资深专家的选择。

  • BeautifulSoup :适合手写代码,但它的 API(如 find('a')['href'])很难用纯字符串在 YAML 里描述。
  • Parsel :Scrapy 剥离出来的解析库。它支持 CSS 选择器伪类扩展 ,比如 a::attr(href)p::text。这意味着我们可以直接把提取逻辑写成一个字符串,非常适合配置化!✨

🔄 整体流程
读取 YAML ➡️ Engine 初始化 ➡️ Fetch (通用请求) ➡️ Parse (通用解析) ➡️ Next Page (通用翻页) ➡️ Save

5️⃣ 环境准备与依赖安装(可复现)

我们需要 PyYAML 来处理配置文件,parsel 来处理提取逻辑。

bash 复制代码
pip install requests pyyaml parsel pandas

📂 推荐项目结构

text 复制代码
config_spider/
├── configs/            # 存放各种网站的 YAML 配置文件
│   ├── quotes.yaml
│   └── books.yaml
├── data/               # 结果输出
├── engine.py           # 核心引擎代码(这次只写这一个文件!)
└── main.py             # 启动入口

6️⃣ 核心定义:YAML 契约(The Contract)

在写代码前,我们必须先设计好"规则"。这就是我们的 DSL。

让我们以爬取 toscrape 为例,设计一个 quotes.yaml

yaml 复制代码
# configs/quotes.yaml
spider_name: "quotes_spider"
base_url: "http://quotes.toscrape.com"
start_urls:
  - "http://quotes.toscrape.com/page/1/"

# 列表项的选择器(表示每一个名言卡片)
item_selector: "div.quote"

# 字段定义
fields:
  - name: "text"
    selector: "span.text::text"  # ::text 是 parsel 的特技,直接取文本
  - name: "author"
    selector: "small.author::text"
  - name: "tags"
    selector: "div.tags a.tag::text"
    is_list: true  # 标记这个字段是多值(列表)

# 翻页规则
pagination:
  selector: "li.next a::attr(href)"  # ::attr(href) 直接取属性
  max_pages: 3

7️⃣ 核心实现:通用引擎(The Engine)

这是本篇的精华。我们要写一个类,它能"读懂"上面的 YAML。

python 复制代码
import yaml
import requests
import time
from parsel import Selector
import pandas as pd
import os

class UniversalSpider:
    def __init__(self, config_path):
        """
        初始化:加载 YAML 配置
        """
        with open(config_path, 'r', encoding='utf-8') as f:
            self.config = yaml.safe_load(f)
            
        self.results = []
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }

    def fetch(self, url):
        """
        通用请求方法
        """
        try:
            print(f"📥 Fetching: {url}")
            resp = requests.get(url, headers=self.headers, timeout=10)
            if resp.status_code == 200:
                return resp.text
            print(f"⚠️ Status {resp.status_code}")
            return None
        except Exception as e:
            print(f"❌ Error: {e}")
            return None

    def extract_field(self, node, field_conf):
        """
        核心魔法:根据配置提取单个字段
        node: 当前的 HTML 节点 (parsel.Selector)
        field_conf: 字段配置字典
        """
        selector_str = field_conf.get('selector')
        is_list = field_conf.get('is_list', False)
        
        # parsel 的 css 方法返回的是 SelectorList
        if is_list:
            # 如果是列表,用 getall()
            return node.css(selector_str).getall()
        else:
            # 如果是单值,用 get(),strip() 去除首尾空格
            val = node.css(selector_str).get()
            return val.strip() if val else None

    def parse(self, html):
        """
        通用解析流程
        """
        sel = Selector(text=html)
        
        # 1. 定位到列表区域 (Loop Area)
        item_selector = self.config.get('item_selector')
        items = sel.css(item_selector)
        print(f"🔍 Found {len(items)} items.")
        
        current_page_data = []
        
        # 2. 遍历每个列表项
        for item in items:
            record = {}
            # 3. 动态提取所有配置的字段
            for field in self.config.get('fields', []):
                field_name = field['name']
                record[field_name] = self.extract_field(item, field)
            
            current_page_data.append(record)
            
        self.results.extend(current_page_data)
        
        # 4. 处理翻页 (Next Page)
        next_url = None
        pag_conf = self.config.get('pagination')
        if pag_conf:
            next_link = sel.css(pag_conf['selector']).get()
            if next_link:
                # 处理相对路径,拼接 base_url
                if not next_link.startswith('http'):
                    # 简单拼接,实际建议用 urllib.parse.urljoin
                    base = self.config.get('base_url', '').rstrip('/')
                    next_link = next_link.lstrip('/')
                    next_url = f"{base}/{next_link}"
                else:
                    next_url = next_link
                    
        return next_url

    def run(self):
        """
        主循环
        """
        urls = self.config.get('start_urls', [])
        max_pages = self.config.get('pagination', {}).get('max_pages', 5)
        page_count = 0
        
        # 简单的队列,实际可用 collections.deque
        queue = urls
        
        while queue and page_count < max_pages:
            url = queue.pop(0)
            page_count += 1
            
            html = self.fetch(url)
            if not html:
                continue
                
            next_url = self.parse(html)
            
            # 如果找到了下一页,加入队列
            if next_url:
                print(f"👉 Next page found: {next_url}")
                queue.append(next_url)
                
            # 必须的礼貌延迟
            time.sleep(1)
            
        print(f"🏁 Done! Total collected: {len(self.results)}")
        self.save()

    def save(self):
        """
        通用存储
        """
        if not self.results:
            print("📭 No data to save.")
            return
            
        df = pd.DataFrame(self.results)
        filename = f"data/{self.config['spider_name']}.csv"
        os.makedirs('data', exist_ok=True)
        df.to_csv(filename, index=False, encoding='utf-8-sig')
        print(f"💾 Saved to {filename}")

# 🔍 代码详细解析:
# 1. extract_field: 这是最关键的抽象。我们不再写 `item.find(...)`,而是根据 YAML 里的 `selector` 字符串动态调用 `css()`。
# 2. ::text 和 ::attr(): 这是 Parsel 的特性。在 YAML 里写 `a::attr(href)` 比在代码里写 `link['href']` 更直观,真正实现了"配置即逻辑"。
# 3. Pagination: 逻辑非常通用。只要提取到下一页链接,就扔回 queue 里继续跑。

8️⃣ 运行方式与结果展示(必写)

现在,我们不需要改任何 Python 代码,只需要创建 YAML 文件即可。

场景一:爬取名言网 (Quotes)

创建 configs/quotes.yaml (内容见第 6 节)。

运行:

python 复制代码
# main.py
from engine import UniversalSpider

if __name__ == "__main__":
    spider = UniversalSpider("configs/quotes.yaml")
    spider.run()

运行结果 (data/quotes_spider.csv)

text author tags
"The world as we have created..." Albert Einstein 'change', 'deep-thoughts', 'thinking', 'world'
"It is our choices, Harry..." J.K. Rowling 'abilities', 'choices'

场景二:🔥 复用能力测试 ------ 爬取书籍网 (Books)

不用动 engine.py!直接新建 configs/books.yaml

yaml 复制代码
spider_name: "books_spider"
base_url: "http://books.toscrape.com/catalogue"
start_urls:
  - "http://books.toscrape.com/catalogue/page-1.html"

item_selector: "article.product_pod"

fields:
  - name: "title"
    selector: "h3 a::attr(title)"
  - name: "price"
    selector: "p.price_color::text"
  - name: "availability"
    selector: "p.instock.availability::text"

pagination:
  selector: "li.next a::attr(href)"
  max_pages: 2

改一下 main.py 入口参数为 configs/books.yaml,再次运行。

结果 (data/books_spider.csv)

title price availability
A Light in the Attic £51.77 In stock

看到威力了吗?只需 5 分钟写个 YAML,一个新的爬虫就诞生了! 🎉

9️⃣ 常见问题与排错(💡 经验之谈)

  1. 选择器写错导致数据为空

    • 现象:程序跑完了,CSV 是空的。
    • 排错 :Parsel/Scrapy shell 是调试神器。在终端输入 scrapy shell "URL",然后在交互式命令行里测试你的 CSS 选择器是否正确,测准了再填进 YAML。
  2. 相对路径陷阱

    • 现象 :下一页 URL 变成了 http://quotes.toscrape.com/page/1/page/2/,路径重复了。
    • 解决 :我在代码里写了简单的 urljoin 逻辑,但真实网页情况复杂(有的以 / 开头,有的不是)。建议在引擎里引入 urllib.parse.urljoin 做标准处理。
  3. YAML 格式错误

    • YAML 对缩进极其敏感。如果报错,请检查是不是用 Tab 代替了空格,或者层级没对齐。

🔟 进阶优化(可选但加分)

如果你想把这个框架做成企业级产品

  1. 支持多种解析器 :在 YAML 里加一个 type: xpathtype: regex 字段,让引擎支持 XPath 或 正则提取。

  2. 后处理管道 (Post-Processors)

    在 YAML 里定义清洗规则,例如:

    yaml 复制代码
    fields:
      - name: "price"
        selector: "p.price::text"
        processors: 
          - "strip"
          - "remove_currency_symbol"  # 对应引擎里预定义的函数
  3. 数据库配置 :把 save() 方法改造一下,支持在 YAML 里配置 MySQL 连接串,直接入库。

1️⃣1️⃣ 总结与延伸阅读

🎉 复盘

今天我们完成了一次从"脚本小子"到"架构师"的思维跃迁。

我们不再为每个网站单独写代码,而是构建了一个解释器。这个解释器读取名为 YAML 的"乐谱",演奏出抓取数据的乐章。🎶

👣 下一步

现在的框架是单线程的。你可以尝试:

  1. 结合 asyncioaiohttp,把 fetch 方法改成异步的,让它并发处理多个 YAML 配置。
  2. 做一个 Web UI,在网页上填填表单就能生成 YAML 文件,让非技术人员也能配爬虫!

这就是低代码(Low-Code)爬虫平台的核心原理!恭喜你,你已经摸到了高阶爬虫开发的门槛!

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。技术无罪,责任在人!!!

相关推荐
青 春 记 忆7 小时前
Dify Docker Compose 通用无损升级指南:从备份、双版本预演到切换与回滚
运维·人工智能·python·docker·容器
GlueNa2SiO37 小时前
03-Flask模板引擎Jinja2详解
笔记·python·flask
临沂GEO9 小时前
GEO搜索优化科普|正规地理位置流量运营入门指南
大数据·人工智能·python·流量运营
大模型码小白9 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring
深蓝电商API10 小时前
常见反爬策略全解析
爬虫·反爬策略
荷蒲11 小时前
【小白量化Qbuddy】利用AI学习中文Python
人工智能·python·学习
数据狐(Datafox)12 小时前
京东商品列表API技术解析与落地应用(含标准 JSON 示例)
java·大数据·前端·人工智能·python·数据分析·json
量化吞吐机12 小时前
2026年下半年手工交易规则走向量化表达,产品该先接住哪一步
人工智能·python
Metaphor69213 小时前
使用 Python 读取和删除 Excel 文件属性
python·excel
迷迭香yy14 小时前
Python构建转融券多空识别系统从接口到因子的全流程 IG50免费开源股票数据API接口
开发语言·python·开源