
📌 写在前面
在日常工作中,我们经常需要备份自己的博客文章,或者对某个优质博主的文章进行系统性的整理和分析。手动一个一个复制显然效率太低,这时候就需要一个专业的爬虫工具来帮助我们。
本文将带你从零开始,使用 Python 构建一个完整的 CSDN 博客爬虫工具。我们将涵盖:
- 项目架构设计
- 爬虫核心实现
- 多格式导出器
- 最佳实践
技术栈:Python 3.8+, Requests, BeautifulSoup4, lxml
项目源码 :csdn-blog-scraper(欢迎 Star ⭐)
个人主页 :艺杯羹
文章目录
-
- [📌 写在前面](#📌 写在前面)
- 一、项目架构设计
-
- [1.1 为什么需要模块化设计?](#1.1 为什么需要模块化设计?)
- [1.2 模块职责分工](#1.2 模块职责分工)
- 二、配置管理模块
-
- [2.1 使用 `@dataclass` 简化配置类](#2.1 使用
@dataclass简化配置类) - [2.2 配置类的优点](#2.2 配置类的优点)
- [2.3 序列化支持](#2.3 序列化支持)
- [2.1 使用 `@dataclass` 简化配置类](#2.1 使用
- 三、日志模块封装
-
- [3.1 为什么需要专业的日志模块?](#3.1 为什么需要专业的日志模块?)
- [3.2 日志效果展示](#3.2 日志效果展示)
- 四、爬虫核心实现
-
- [4.1 爬虫类初始化](#4.1 爬虫类初始化)
- [4.2 请求头与延迟控制](#4.2 请求头与延迟控制)
- [4.3 安全请求与重试机制](#4.3 安全请求与重试机制)
- [4.4 页面解析------提取文章信息](#4.4 页面解析——提取文章信息)
- [4.5 分页爬取与控制](#4.5 分页爬取与控制)
- 五、多格式导出器设计
-
- [5.1 为什么使用工厂模式?](#5.1 为什么使用工厂模式?)
- [5.2 基类与子类实现](#5.2 基类与子类实现)
- [5.3 工厂类实现](#5.3 工厂类实现)
- [5.4 各格式输出效果对比](#5.4 各格式输出效果对比)
- 六、整合运行与测试
-
- [6.1 便捷的保存方法](#6.1 便捷的保存方法)
- [6.2 完整使用示例](#6.2 完整使用示例)
- [6.3 运行效果截图](#6.3 运行效果截图)
- 七、总结与最佳实践
-
- [7.1 核心知识点回顾](#7.1 核心知识点回顾)
- [7.2 最佳实践建议](#7.2 最佳实践建议)
- [7.3 常见问题解答](#7.3 常见问题解答)
- [7.4 扩展方向](#7.4 扩展方向)
- [📦 项目源码](#📦 项目源码)
一、项目架构设计
1.1 为什么需要模块化设计?
一个好的项目,从清晰的架构开始。我们采用模块化设计,将不同功能拆分为独立的模块,每个模块各司其职:
csdn-blog-scraper/
├── src/ # 源代码目录
│ ├── __init__.py # 包初始化
│ ├── config.py # 配置管理
│ ├── scraper.py # 核心爬虫
│ ├── exporters.py # 多格式导出
│ └── utils.py # 工具函数
├── main.py # 命令行入口
└── requirements.txt # 依赖列表
1.2 模块职责分工
| 模块 | 文件 | 职责 |
|---|---|---|
| 配置管理 | config.py |
管理爬虫配置参数 |
| 核心爬虫 | scraper.py |
HTTP请求、HTML解析、文章提取 |
| 多格式导出 | exporters.py |
JSON/CSV/TXT格式导出 |
| 工具函数 | utils.py |
日志、文件名处理等 |
| 入口文件 | main.py |
命令行参数解析 |
设计原则:
- 单一职责:每个模块只做一件事
- 开闭原则:对扩展开放,对修改关闭
- 依赖倒置:高层模块不依赖低层模块
二、配置管理模块
2.1 使用 @dataclass 简化配置类
Python 3.7 引入的 @dataclass 装饰器,可以自动生成 __init__、__repr__ 等方法,非常适合用于配置管理:
python
from dataclasses import dataclass
from typing import Optional
import os
@dataclass
class Config:
"""爬虫配置类"""
blog_url: str = "https://blog.csdn.net/qq_46987323" # 目标博客URL
min_delay: float = 1.5 # 最小请求延迟(秒)
max_delay: float = 3.0 # 最大请求延迟(秒)
request_timeout: int = 30 # 请求超时时间
max_retries: int = 3 # 最大重试次数
max_pages: Optional[int] = None # 最大爬取页数(None=全部)
output_dir: str = "outputs" # 输出目录
user_agent: Optional[str] = None # 自定义UA(None=随机)
verify_ssl: bool = True # 是否验证SSL
def __post_init__(self):
"""初始化后自动验证配置并创建输出目录"""
if self.min_delay < 0:
raise ValueError("min_delay 不能为负数")
if self.max_delay < self.min_delay:
raise ValueError("max_delay 必须 >= min_delay")
if self.max_pages is not None and self.max_pages <= 0:
raise ValueError("max_pages 必须为正整数")
# 自动创建输出目录
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir)
2.2 配置类的优点
为什么选择 @dataclass?
- 代码简洁 :自动生成
__init__、__repr__、__eq__等方法 - 类型安全:配合类型提示,IDE智能提示更友好
- 不可变性 :配合
frozen=True可创建不可变对象 - 可扩展性 :
__post_init__方法可做初始化验证
运行截图效果:
Config 使用示例:
┌─────────────────────────────────────────────┐
│ blog_url: https://blog.csdn.net/qq_46987323 │
│ min_delay: 1.5秒 │
│ max_delay: 3.0秒 │
│ max_pages: 全部 │
│ output_dir: outputs/ │
└─────────────────────────────────────────────┘
2.3 序列化支持
python
def to_dict(self) -> dict:
"""转为字典(方便保存为配置文件)"""
return {
"blog_url": self.blog_url,
"min_delay": self.min_delay,
"max_delay": self.max_delay,
# ... 其他字段
}
@classmethod
def from_dict(cls, data: dict) -> "Config":
"""从字典创建配置(方便读取配置文件)"""
return cls(**data)
三、日志模块封装
3.1 为什么需要专业的日志模块?
好的日志系统是调试程序的"眼睛"。我们使用 RotatingFileHandler 实现日志轮转,避免日志文件无限增长:
python
import logging
from logging.handlers import RotatingFileHandler
from typing import Optional
def setup_logger(
name: str = "csdn_scraper",
log_file: Optional[str] = None,
log_level: int = logging.INFO
) -> logging.Logger:
"""配置日志系统------同时输出到控制台和文件"""
logger = logging.getLogger(name)
logger.setLevel(log_level)
logger.handlers.clear() # 防止重复添加
# 统一的日志格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 控制台输出
console_handler = logging.StreamHandler()
console_handler.setLevel(log_level)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# 文件输出(支持日志轮转)
if log_file:
file_handler = RotatingFileHandler(
log_file,
maxBytes=5 * 1024 * 1024, # 5MB 轮转
backupCount=5, # 保留 5 个备份
encoding='utf-8'
)
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
3.2 日志效果展示
运行爬虫时,控制台会实时显示运行状态:
2026-05-06 12:34:56 - csdn_scraper - INFO - 🚀 开始爬取文章...
2026-05-06 12:34:57 - csdn_scraper - INFO - 📄 正在爬取第1页: ...
2026-05-06 12:35:00 - csdn_scraper - INFO - ✅ 第1页: +20 篇文章
2026-05-06 12:35:02 - csdn_scraper - INFO - 📊 总计: 20 篇文章
日志轮转机制:
- 单个日志文件最大 5MB
- 保留最近 5 个备份
- 超出后自动删除最旧的日志
四、爬虫核心实现
这是项目的核心部分,也是代码量最多的模块。我们将它拆解为几个关键步骤。
4.1 爬虫类初始化
python
import time
import random
import requests
from bs4 import BeautifulSoup
from typing import List, Dict, Any, Optional
from urllib.parse import urljoin
class CSDNBlogScraper:
"""CSDN博客爬虫主类"""
def __init__(self, config: Config = None, logger: logging.Logger = None):
self.config = config or Config()
self.logger = logger or setup_logger()
self.session = requests.Session() # 复用Session提升性能
# 预定义User-Agent池------规避反爬虫
self.user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) '
'Gecko/20100101 Firefox/125.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15',
]
4.2 请求头与延迟控制
反爬虫是爬虫开发中最常见的挑战。我们通过以下策略来规避:
策略一:动态User-Agent
随机切换浏览器标识,模拟真实用户访问:
python
def _get_headers(self, referer: Optional[str] = None) -> dict:
"""构造HTTP请求头------模拟真实浏览器"""
user_agent = self.config.user_agent or random.choice(self.user_agents)
headers = {
'User-Agent': user_agent,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,'
'image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
}
if referer:
headers['Referer'] = referer # 模拟页面跳转来源
return headers
策略二:随机延迟
每次请求之间随机等待,避免被识别为机器行为:
python
def _random_delay(self):
"""随机延迟------避免请求频率过高被封锁"""
delay = random.uniform(self.config.min_delay, self.config.max_delay)
self.logger.debug(f"⏱️ 等待 {delay:.1f} 秒...")
time.sleep(delay)
4.3 安全请求与重试机制
网络请求不可靠,所以重试机制是必不可少的:
python
def _safe_request(self, url: str, retry: int = 0,
referer: Optional[str] = None) -> Optional[requests.Response]:
"""带重试的安全HTTP请求"""
if retry >= self.config.max_retries:
self.logger.error(f"❌ 请求失败(已重试{retry}次): {url}")
return None
try:
response = self.session.get(
url,
headers=self._get_headers(referer),
timeout=self.config.request_timeout,
verify=self.config.verify_ssl
)
response.raise_for_status() # 检查HTTP状态码
# 返回内容过短时触发重试(可能被反爬)
if len(response.text) < 200:
self.logger.warning(f"⚠️ 响应过短,正在重试...")
self._random_delay()
return self._safe_request(url, retry + 1, referer)
return response
except requests.RequestException as e:
self.logger.warning(f"⚠️ 请求失败: {e}, 正在重试...")
self._random_delay()
return self._safe_request(url, retry + 1, referer)
重试流程图:
开始请求 → 请求成功? → 是 → 内容正常? → 是 → 返回结果
↓ ↓
否 否
↓ ↓
等待随机延迟 ← 重试次数+1 等待随机延迟 ← 重试次数+1
↓ ↓
不超过3次 → 继续请求 不超过3次 → 继续请求
↓ ↓
超过3次 → 返回None 超过3次 → 返回None
4.4 页面解析------提取文章信息
这是最核心的环节。CSDN的页面结构可能会变化,所以我们使用多重选择器来提升鲁棒性:
python
def _parse_article_item(self, item: BeautifulSoup) -> Optional[Dict[str, Any]]:
"""解析单篇文章信息"""
article_info = {}
# [核心] 提取标题和链接------支持多种选择器
title_tag = (item.find('h4') or
item.find('a', class_='title') or
item.find('a'))
if title_tag:
if title_tag.name == 'a':
link_tag = title_tag
else:
link_tag = title_tag.find('a')
if link_tag:
article_info['title'] = link_tag.get_text(strip=True)
article_info['url'] = link_tag.get('href', '')
if 'title' not in article_info or not article_info['title']:
return None # 跳过无效条目
# [核心] 提取发布日期
date_tag = item.find('span', class_='date')
if date_tag:
article_info['date'] = date_tag.get_text(strip=True)
# [核心] 提取阅读量
read_tag = (item.find('span', class_='read-num') or
item.find('span', class_='read-count'))
if read_tag:
article_info['views'] = read_tag.get_text(strip=True)
return article_info
4.5 分页爬取与控制
CSDN的博客文章是按分页展示的,我们需要自动遍历所有页面:
python
def scrape_page(self, page_num: int) -> List[Dict[str, Any]]:
"""爬取单页文章列表"""
page_url = f"{self.config.blog_url}/article/list/{page_num}"
response = self._safe_request(page_url, referer=self.config.blog_url)
if not response:
return []
soup = BeautifulSoup(response.text, 'lxml')
# 支持多种CSS选择器------适配不同页面结构
selectors = [
'div.article-item-box', # 新版列表
'ul.colu_author_c > li', # 个人主页
'article.blog-list-box', # 文章卡片
'div.article-list > div', # 旧版列表
]
article_items = []
for selector in selectors:
items = soup.select(selector)
if items:
article_items = items
break
if not article_items:
return []
# 逐一解析
articles = []
for item in article_items:
info = self._parse_article_item(item)
if info:
articles.append(info)
return articles
主循环------自动遍历所有分页:
python
def scrape_all_articles(self) -> List[Dict[str, Any]]:
"""爬取所有文章(自动遍历所有分页)"""
self.logger.info("🚀 开始爬取文章...")
all_articles = []
page_num = 1
while True:
# 检查页数限制
if self.config.max_pages and page_num > self.config.max_pages:
self.logger.info(f"✅ 达到最大页数限制: {self.config.max_pages}")
break
# 爬取当前页
articles = self.scrape_page(page_num)
if not articles:
self.logger.info(f"✅ 没有更多文章(第{page_num}页为空)")
break
all_articles.extend(articles)
# 随机延迟------礼貌爬取
self._random_delay()
page_num += 1
# 安全限制------最多100页
if page_num > 100:
break
self.logger.info(f"🎉 爬取完成!共 {len(all_articles)} 篇文章")
return all_articles
五、多格式导出器设计
5.1 为什么使用工厂模式?
工厂模式是一种创建型设计模式,它定义一个创建对象的接口,让子类决定实例化哪个类。在我们的项目中,它的优势非常明显:
不使用工厂模式:
python
# 每次都要写大量的if-else
if format == "json":
exporter = JSONExporter()
elif format == "csv":
exporter = CSVExporter()
elif format == "txt":
exporter = TXTExporter()
使用工厂模式:
python
# 一行代码搞定
exporter = ExporterFactory.get_exporter("json")
5.2 基类与子类实现
python
class BaseExporter:
"""导出器基类------定义统一的接口"""
def export(self, articles: List[dict], filepath: str) -> None:
raise NotImplementedError("子类必须实现 export 方法")
class JSONExporter(BaseExporter):
"""JSON格式导出"""
def export(self, articles: List[dict], filepath: str) -> None:
output_data = {
"metadata": {
"exported_at": datetime.now().isoformat(),
"article_count": len(articles),
},
"articles": articles,
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(output_data, f, ensure_ascii=False, indent=2)
class CSVExporter(BaseExporter):
"""CSV格式导出(支持Excel打开)"""
def export(self, articles: List[dict], filepath: str) -> None:
fieldnames = ["index", "title", "url", "date", "views"]
with open(filepath, 'w', encoding='utf-8-sig', newline='') as f:
# 使用 utf-8-sig 编码,Excel可以直接打开
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for idx, article in enumerate(articles, 1):
writer.writerow({
"index": idx,
"title": article.get("title", ""),
"url": article.get("url", ""),
"date": article.get("date", ""),
"views": article.get("views", ""),
})
class TXTExporter(BaseExporter):
"""文本格式导出------清晰易读"""
def export(self, articles: List[dict], filepath: str) -> None:
with open(filepath, 'w', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write("CSDN 博客文章列表\n")
f.write(f"导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"文章总数: {len(articles)}\n")
f.write("=" * 80 + "\n\n")
for idx, article in enumerate(articles, 1):
f.write(f"[{idx}] {article.get('title', '')}\n")
f.write(f" URL: {article.get('url', '')}\n")
f.write(f" 日期: {article.get('date', '')}\n")
f.write(f" 阅读量: {article.get('views', '')}\n\n")
5.3 工厂类实现
python
class ExporterFactory:
"""导出器工厂------统一创建导出器实例"""
_exporters = {
"json": JSONExporter,
"csv": CSVExporter,
"txt": TXTExporter,
}
@classmethod
def get_exporter(cls, format: str) -> BaseExporter:
"""获取指定格式的导出器"""
format = format.lower()
if format not in cls._exporters:
raise ValueError(f"不支持的格式: {format}")
return cls._exporters[format]()
@classmethod
def list_formats(cls) -> List[str]:
"""列出所有支持的格式"""
return list(cls._exporters.keys())
5.4 各格式输出效果对比
JSON格式(适合程序处理):
json
{
"metadata": {
"exported_at": "2026-05-06T12:34:56",
"article_count": 77
},
"articles": [
{
"title": "文章标题",
"url": "https://blog.csdn.net/...",
"date": "2026-05-01",
"views": "阅读量: 1000"
}
]
}
CSV格式(适合Excel分析):
csv
index,title,url,date,views
1,文章标题,https://blog.csdn.net/...,2026-05-01,阅读量: 1000
2,第二篇文章,https://blog.csdn.net/...,2026-04-28,阅读量: 500
TXT格式(适合直接阅读):
================================================================================
CSDN 博客文章列表
导出时间: 2026-05-06 12:34:56
文章总数: 77
================================================================================
[1] 文章标题
URL: https://blog.csdn.net/...
日期: 2026-05-01
阅读量: 阅读量: 1000
六、整合运行与测试
6.1 便捷的保存方法
为了让调用更加便捷,我们在爬虫类中添加了简洁的保存方法:
python
def save(self, articles, format="txt", filename=None):
"""统一保存接口------支持多种格式"""
if filename is None:
filename = f"csdn_articles_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{format}"
filepath = f"{self.config.output_dir}/{filename}"
exporter = ExporterFactory.get_exporter(format)
exporter.export(articles, filepath)
return filepath
def save_to_csv(self, articles, filename=None):
return self.save(articles, "csv", filename)
def save_to_json(self, articles, filename=None):
return self.save(articles, "json", filename)
def save_to_txt(self, articles, filename=None):
return self.save(articles, "txt", filename)
6.2 完整使用示例
python
from src import CSDNBlogScraper, Config
# 第一步:创建配置
config = Config(
blog_url="https://blog.csdn.net/qq_46987323",
min_delay=1.5, # 请求间隔1.5-3秒
max_delay=3.0,
)
# 第二步:创建爬虫
scraper = CSDNBlogScraper(config)
# 第三步:爬取所有文章
articles = scraper.scrape_all_articles()
print(f"共爬取 {len(articles)} 篇文章")
# 第四步:保存为多种格式
scraper.save_to_csv(articles)
scraper.save_to_json(articles)
scraper.save_to_txt(articles)
6.3 运行效果截图
🚀 开始爬取文章...
📄 正在爬取第1页: https://blog.csdn.net/qq_46987323/article/list/1
✅ 第1页: +20 篇文章
📊 总计: 20 篇文章
⏱️ 等待 2.3 秒...
📄 正在爬取第2页: https://blog.csdn.net/qq_46987323/article/list/2
✅ 第2页: +20 篇文章
📊 总计: 40 篇文章
⏱️ 等待 1.8 秒...
...(继续爬取)...
🎉 爬取完成!共 77 篇文章
✅ 成功导出77篇文章到 outputs/csdn_articles_20260506_123456.csv
✅ 成功导出77篇文章到 outputs/csdn_articles_20260506_123456.json
✅ 成功导出77篇文章到 outputs/csdn_articles_20260506_123456.txt
七、总结与最佳实践
7.1 核心知识点回顾
| 知识点 | 应用场景 | 关键代码 |
|---|---|---|
@dataclass |
配置管理 | @dataclass class Config |
| Session复用 | HTTP请求优化 | requests.Session() |
| 随机延迟 | 反爬虫规避 | random.uniform(min, max) |
| 重试机制 | 网络容错 | _safe_request() 递归重试 |
| 多重选择器 | 页面结构适配 | 多个CSS选择器依次尝试 |
| 工厂模式 | 格式扩展 | ExporterFactory.get_exporter() |
| 日志轮转 | 日志管理 | RotatingFileHandler |
7.2 最佳实践建议
1. 请求频率控制
python
# ✅ 好的做法:随机延迟,模拟人类行为
delay = random.uniform(1.5, 3.0)
time.sleep(delay)
# ❌ 不好的做法:固定间隔,容易被识别
time.sleep(2)
2. User-Agent 轮换
python
# ✅ 好的做法:使用User-Agent池
user_agent = random.choice(user_agent_pool)
# ❌ 不好的做法:固定UA
user_agent = "Chrome/124..."
3. 错误处理
python
# ✅ 好的做法:自动重试
response = self._safe_request(url, retry=0, max_retries=3)
# ❌ 不好的做法:一次失败就放弃
response = requests.get(url) # 没有try-except
7.3 常见问题解答
Q1:为什么爬取不到文章?
- 检查URL格式是否正确(必须是
https://blog.csdn.net/用户名) - 检查网络连接是否正常
- 查看日志文件中的具体错误信息
Q2:如何避免被封IP?
- 设置合理的请求延迟(建议1.5-3秒)
- 使用User-Agent轮换
- 不要爬取频率过高
Q3:添加新的导出格式要怎么做?
- 继承
BaseExporter类 - 实现
export方法 - 在
ExporterFactory._exporters中注册
7.4 扩展方向
- GUI可视化界面:使用Tkinter开发图形界面
- 文章内容爬取:不仅爬取列表,还能爬取全文
- 多线程优化:使用并发提升爬取效率
- 代理IP池:配合代理提升稳定性
- 数据库存储:支持MongoDB、MySQL等
📦 项目源码
本文所有代码源自开源项目:csdn-blog-scraper
开发者 :艺杯羹
如果本文对你有帮助,欢迎 Star ⭐ 支持!
享受编码! 🚀