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 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。技术无罪,责任在人!!!

相关推荐
YJlio9 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
l1t10 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
0思必得010 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
山塘小鱼儿11 小时前
本地Ollama+Agent+LangGraph+LangSmith运行
python·langchain·ollama·langgraph·langsimth
码说AI11 小时前
python快速绘制走势图对比曲线
开发语言·python
wait_luky11 小时前
python作业3
开发语言·python
Libraeking13 小时前
爬虫的“法”与“术”:在牢狱边缘疯狂试探?(附高阶环境配置指南)
爬虫
Python大数据分析@13 小时前
tkinter可以做出多复杂的界面?
python·microsoft
大黄说说13 小时前
新手选语言不再纠结:Java、Python、Go、JavaScript 四大热门语言全景对比与学习路线建议
java·python·golang