1. 项目概述:博客园文章采集与Markdown转换
最近在整理技术资料时,发现博客园(Cnblogs)上有大量优质的技术文章,但平台自带的收藏功能无法满足我的需求——我需要将这些文章以结构化的方式保存到本地,并且能够方便地进行二次编辑和整理。于是决定用Python开发一个爬虫工具,专门用于采集博客园的文章标题、发布日期、标签以及HTML正文内容,并自动转换为Markdown格式。
这个爬虫项目虽然难度不高(⭐️),但涵盖了爬虫开发的完整流程:从页面请求、数据解析到格式转换和本地存储。特别适合想要学习Python爬虫的开发者作为入门实战项目,也适合需要批量保存技术文章的技术人员使用。
2. 技术选型与整体架构
2.1 为什么选择这些技术?
请求库选择:使用requests而不是urllib,因为:
requests的API设计更加人性化- 自动处理连接池和会话保持
- 内置JSON解析等实用功能
- 社区支持更好,文档更完善
解析工具选择:使用BeautifulSoup而不是正则表达式,因为:
- 博客园的HTML结构相对规范但不完全一致
- CSS选择器比正则表达式更易维护
- 可以处理不太规范的HTML标记
存储格式选择:转换为Markdown而不是直接存储HTML,因为:
- Markdown更轻量,便于后续编辑
- 兼容性更好,几乎所有笔记软件都支持
- 可以去除不必要的样式和广告内容
2.2 系统架构设计
整个爬虫分为三个主要模块:
- Fetcher:负责发送HTTP请求获取页面内容
- Parser:负责从HTML中提取所需数据
- Storage:负责将数据转换为Markdown并保存
mermaid复制graph TD
A[开始] --> B[Fetcher获取页面]
B --> C[Parser解析数据]
C --> D[Storage保存为MD]
D --> E[结束]
3. 环境准备与依赖安装
3.1 Python环境配置
建议使用Python 3.8+版本,我测试的环境是Python 3.9.7。可以使用conda或venv创建虚拟环境:
bash复制python -m venv cnblogs_spider
source cnblogs_spider/bin/activate # Linux/Mac
cnblogs_spider\Scripts\activate # Windows
3.2 安装必要依赖库
bash复制pip install requests beautifulsoup4 html2markdown
关键库说明:
requests:2.26.0版本(注意不要使用太新的版本,避免API变动)beautifulsoup4:4.10.0版本html2markdown:3.3.3版本
提示:建议固定这些版本,避免后续API变动导致代码不兼容
4. 核心实现:请求层(Fetcher)
4.1 基础请求函数
python复制import requests
from urllib.parse import urljoin
BASE_URL = "https://www.cnblogs.com/"
def fetch_page(url, timeout=10, retry=3):
"""
获取网页内容
:param url: 相对或绝对URL
:param timeout: 超时时间(秒)
:param retry: 重试次数
:return: 页面HTML内容
"""
full_url = urljoin(BASE_URL, url)
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"
}
for attempt in range(retry):
try:
response = requests.get(full_url, headers=headers, timeout=timeout)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
print(f"请求失败({attempt+1}/{retry}): {e}")
if attempt == retry - 1:
raise
time.sleep(2 ** attempt) # 指数退避
return None
4.2 请求优化技巧
- 设置合理的超时时间:建议10-15秒,太短容易因网络波动失败,太长会卡住程序
- 使用指数退避重试:第一次失败后等待2秒,第二次4秒,第三次8秒
- 模拟真实浏览器UA:避免使用明显是爬虫的User-Agent
- 处理相对URL:使用
urljoin确保URL拼接正确
注意:博客园对爬虫比较友好,但也不宜请求过于频繁,建议在请求间添加随机延迟
5. 核心实现:解析层(Parser)
5.1 HTML解析基础
博客园文章页的主要数据分布在以下几个部分:
- 文章标题:
<h1 class="postTitle"> - 发布日期:
<span id="post-date"> - 文章标签:
<div class="postDesc">下的a标签 - 正文内容:
<div id="cnblogs_post_body">
5.2 完整解析代码
python复制from bs4 import BeautifulSoup
import re
def parse_article(html):
"""
解析文章页面
:param html: 页面HTML内容
:return: 包含文章信息的字典
"""
soup = BeautifulSoup(html, 'html.parser')
# 提取文章标题
title_tag = soup.find('h1', class_='postTitle')
title = title_tag.get_text(strip=True) if title_tag else "无标题"
# 提取发布日期
date_span = soup.find('span', id='post-date')
date = date_span.get_text(strip=True) if date_span else "未知日期"
# 提取文章标签
post_desc = soup.find('div', class_='postDesc')
tags = []
if post_desc:
tag_links = post_desc.find_all('a', href=re.compile(r'/tag/'))
tags = [tag.get_text(strip=True) for tag in tag_links]
# 提取正文内容
content_div = soup.find('div', id='cnblogs_post_body')
content = str(content_div) if content_div else ""
return {
'title': title,
'date': date,
'tags': tags,
'content': content
}
5.3 解析中的常见问题处理
- 元素不存在的情况:每个字段都要检查是否为None
- 多余空白字符:使用
get_text(strip=True)去除 - 相对路径转换:正文中的图片链接可能需要转换为绝对路径
- 特殊字符处理:Markdown中的特殊字符需要转义
6. 核心实现:存储层(Storage)
6.1 HTML转Markdown
使用html2markdown库进行转换:
python复制import html2markdown
def html_to_markdown(html):
"""
将HTML转换为Markdown
:param html: HTML内容
:return: Markdown格式文本
"""
# 转换前可以先做一些预处理
markdown = html2markdown.convert(html)
# 后处理:修复转换中的一些常见问题
markdown = re.sub(r'\n{3,}', '\n\n', markdown) # 去除多余空行
markdown = markdown.replace('\\[', '[').replace('\\]', ']') # 去除不必要的转义
return markdown
6.2 生成完整的Markdown文档
python复制import os
from datetime import datetime
def save_as_markdown(article, output_dir='output'):
"""
将文章保存为Markdown文件
:param article: 文章数据字典
:param output_dir: 输出目录
"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 生成文件名:日期-标题
try:
date_obj = datetime.strptime(article['date'], '%Y-%m-%d %H:%M')
date_str = date_obj.strftime('%Y%m%d')
except:
date_str = 'nodate'
# 清理标题中的非法文件名字符
clean_title = re.sub(r'[\\/*?:"<>|]', '', article['title'])
filename = f"{date_str}-{clean_title[:50]}.md" # 限制文件名长度
# 构建Markdown内容
content = f"""# {article['title']}
**日期**: {article['date']}
**标签**: {', '.join(article['tags'])}
{html_to_markdown(article['content'])}
"""
# 保存文件
filepath = os.path.join(output_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
return filepath
6.3 存储优化建议
- 文件名规范化:去除特殊字符,限制长度
- 按日期分类:可以创建子目录按年月组织
- 添加元信息:在Markdown开头添加YAML front matter
- 处理图片下载:可选功能,将图片也保存到本地
7. 完整流程与示例代码
7.1 主程序代码
python复制import time
import random
def crawl_article(url):
"""
爬取单篇文章并保存
:param url: 文章相对URL
"""
try:
print(f"开始处理: {url}")
html = fetch_page(url)
article = parse_article(html)
saved_path = save_as_markdown(article)
print(f"保存成功: {saved_path}")
return True
except Exception as e:
print(f"处理失败: {url} - {str(e)}")
return False
def crawl_multiple(start_urls, delay=(1, 3)):
"""
批量爬取多篇文章
:param start_urls: 文章URL列表
:param delay: 随机延迟范围(秒)
"""
for i, url in enumerate(start_urls, 1):
success = crawl_article(url)
if i < len(start_urls):
sleep_time = random.uniform(*delay)
print(f"等待 {sleep_time:.1f} 秒后继续...")
time.sleep(sleep_time)
if __name__ == "__main__":
# 示例:爬取几篇热门文章
example_urls = [
"/p/12345678.html", # 替换为实际文章URL
"/p/87654321.html",
"/csharp/p/11223344.html"
]
crawl_multiple(example_urls)
7.2 运行结果示例
运行后会生成类似这样的Markdown文件:
markdown复制# Python爬虫最佳实践
**日期**: 2023-05-15 14:30
**标签**: Python, 爬虫, 数据分析
## 1. 爬虫设计原则
现代爬虫开发需要考虑以下几个关键因素...

## 2. 反爬应对策略
...
8. 常见问题与解决方案
8.1 请求被拒绝或返回403
可能原因:
- User-Agent被识别为爬虫
- 请求频率过高
解决方案:
- 轮换多个User-Agent
- 增加请求间隔时间
- 添加Referer头
python复制headers = {
"User-Agent": "Mozilla/5.0...",
"Referer": "https://www.cnblogs.com/"
}
8.2 解析不到预期内容
可能原因:
- 页面结构发生变化
- 元素class/id被修改
解决方案:
- 更新CSS选择器
- 添加更多容错逻辑
- 使用更通用的选择方式
python复制# 更健壮的选择方式
title = soup.find('h1', {'class': re.compile(r'postTitle')})
8.3 Markdown转换格式问题
常见问题:
- 代码块转换不正确
- 表格格式混乱
- 图片链接丢失
解决方案:
- 预处理HTML内容
- 使用更专业的转换库如
markdownify - 手动修复特定问题
9. 进阶优化方向
9.1 增加代理支持
python复制def fetch_page(url, proxies=None):
if proxies:
response = requests.get(url, headers=headers, proxies=proxies)
else:
response = requests.get(url, headers=headers)
9.2 实现增量爬取
- 记录已爬取的URL
- 检查文章最后更新时间
- 只爬取新文章或更新过的文章
9.3 添加自动分类功能
- 根据标签自动分类
- 使用机器学习模型分析内容
- 生成分类目录结构
10. 项目总结
这个博客园文章爬虫虽然功能简单,但涵盖了爬虫开发的核心流程。在实际使用中,我发现了几个值得注意的点:
- 请求控制:即使目标站点没有严格的反爬,也应控制请求频率,1-3秒的随机间隔是不错的选择
- 错误处理:网络请求和HTML解析都要有完善的错误处理,避免因个别失败影响整体任务
- 格式转换:HTML到Markdown的转换不可能完美,重要的文章需要人工检查
这个爬虫还可以进一步扩展,比如添加自动分类、全文搜索等功能,或者集成到笔记软件中作为插件使用。