㊙️本期内容已收录至专栏《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。
💡 读完收获:
- 彻底理解**"数据与逻辑分离"**的架构思想。
- 学会设计一套优雅的爬虫 DSL(领域特定语言)。
- 得到一个只需改改 YAML 就能爬遍天下列表页的"万能框架"。
2️⃣ 背景与需求(Why)
🤔 为什么要配置化?
想象一下,老板让你今天爬"新浪新闻",明天爬"网易新闻",后天爬"虎嗅"。
- 传统做法 :
sina_spider.py,163_spider.py,huxiu_spider.py... 复制粘贴到手软,维护起来简直是地狱。🔥 - 配置化做法 :
spider_engine.py(永远不动) +sina.yaml,163.yaml,huxiu.yaml。
如果网页改版了,你只需要改 YAML 里的一行 CSS 选择器,甚至不需要重启服务(如果你做了热加载的话)。
🎯 核心需求:
我们需要定义一套规则,告诉引擎:
- 种子 URL 是什么?
- 列表在哪里(Loop 区域)?
- 每个字段怎么提取(Title, Link, Date)?
- 下一页怎么找(Pagination)?
3️⃣ 合规与注意事项(必写)
框架的威力与责任:
当你拥有了快速生成爬虫的能力,意味着你产生流量的速度也会倍增。
- 全局限速 :在框架底层必须强制加入
Global Delay(全局延迟),防止用户在 YAML 里配置了 0 秒延迟导致对目标站点发起 DDoS 攻击。 - User-Agent 池:框架应默认提供随机 UA,避免因特征过于单一被封杀。
- 合法合规:该框架仅用于公开数据的采集,严禁用于绕过验证码或抓取鉴权接口。
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️⃣ 常见问题与排错(💡 经验之谈)
-
选择器写错导致数据为空:
- 现象:程序跑完了,CSV 是空的。
- 排错 :Parsel/Scrapy shell 是调试神器。在终端输入
scrapy shell "URL",然后在交互式命令行里测试你的 CSS 选择器是否正确,测准了再填进 YAML。
-
相对路径陷阱:
- 现象 :下一页 URL 变成了
http://quotes.toscrape.com/page/1/page/2/,路径重复了。 - 解决 :我在代码里写了简单的
urljoin逻辑,但真实网页情况复杂(有的以 / 开头,有的不是)。建议在引擎里引入urllib.parse.urljoin做标准处理。
- 现象 :下一页 URL 变成了
-
YAML 格式错误:
- YAML 对缩进极其敏感。如果报错,请检查是不是用 Tab 代替了空格,或者层级没对齐。
🔟 进阶优化(可选但加分)
如果你想把这个框架做成企业级产品:
-
支持多种解析器 :在 YAML 里加一个
type: xpath或type: regex字段,让引擎支持 XPath 或 正则提取。 -
后处理管道 (Post-Processors) :
在 YAML 里定义清洗规则,例如:
yamlfields: - name: "price" selector: "p.price::text" processors: - "strip" - "remove_currency_symbol" # 对应引擎里预定义的函数 -
数据库配置 :把
save()方法改造一下,支持在 YAML 里配置 MySQL 连接串,直接入库。
1️⃣1️⃣ 总结与延伸阅读
🎉 复盘:
今天我们完成了一次从"脚本小子"到"架构师"的思维跃迁。
我们不再为每个网站单独写代码,而是构建了一个解释器。这个解释器读取名为 YAML 的"乐谱",演奏出抓取数据的乐章。🎶
👣 下一步:
现在的框架是单线程的。你可以尝试:
- 结合
asyncio和aiohttp,把fetch方法改成异步的,让它并发处理多个 YAML 配置。 - 做一个 Web UI,在网页上填填表单就能生成 YAML 文件,让非技术人员也能配爬虫!
这就是低代码(Low-Code)爬虫平台的核心原理!恭喜你,你已经摸到了高阶爬虫开发的门槛!
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)

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