㊙️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!
㊗️爬虫难度指数:⭐
🚫声明:本数据&代码仅供学习交流,严禁用于商业用途、倒卖数据或违反目标站点的服务条款等,一切后果皆由使用者本人承担。公开榜单数据一般允许访问,但请务必遵守"君子协议",技术无罪,责任在人。

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [1️⃣ 标题 && 摘要(Abstract)](#1️⃣ 标题 && 摘要(Abstract))
- [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
- [3️⃣ 合规与注意事项(必写)](#3️⃣ 合规与注意事项(必写))
- [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
-
- [静态 vs 动态 vs API](#静态 vs 动态 vs API)
- 整体流程
- [为什么选 requests + lxml?](#为什么选 requests + lxml?)
- [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
- [6️⃣ 核心实现:请求层(Fetcher)](#6️⃣ 核心实现:请求层(Fetcher))
- [7️⃣ 核心实现:解析层(Parser)](#7️⃣ 核心实现:解析层(Parser))
- [8️⃣ 数据存储与导出(Storage)](#8️⃣ 数据存储与导出(Storage))
- [9️⃣ 运行方式与结果展示(必写)](#9️⃣ 运行方式与结果展示(必写))
- [🔟 常见问题与排错(强烈建议写)](#🔟 常见问题与排错(强烈建议写))
-
- [问题1:403 Forbidden / 429 Too Many Requests](#问题1:403 Forbidden / 429 Too Many Requests)
- [问题2:HTML 抓到空壳(动态渲染)](#问题2:HTML 抓到空壳(动态渲染))
- [问题3:XPath/CSS 选择器找不到元素](#问题3:XPath/CSS 选择器找不到元素)
- 问题4:编码乱码
- 问题5:数据库锁死(SQLite)
- [1️⃣1️⃣ 进阶优化(可选但加分)](#1️⃣1️⃣ 进阶优化(可选但加分))
- [1️⃣2️⃣ 总结与延伸阅读](#1️⃣2️⃣ 总结与延伸阅读)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏👉《Python爬虫实战》👈
💕订阅后更新会优先推送,按目录学习更高效💯~
1️⃣ 标题 && 摘要(Abstract)
一句话概括:使用 Python requests + lxml 爬取求字体网的字体信息(字体名称、分类、预览图、下载链接),最终输出为结构化的 SQLite 数据库 + CSV 文件。
读完本文你将获得:
- 掌握静态网页采集的完整工作流:请求 → 解析 → 清洗 → 存储
- 学会处理分页逻辑、字段缺失、重复数据等实战问题
- 获得一套可直接运行的字体信息采集系统(支持增量更新和断点续跑)
2️⃣ 背景与需求(Why)
为什么要爬字体站?
作为设计师或内容创作者,经常需要在海量字体库中寻找合适的字体。手动浏览效率太低,而免费字体站(如求字体网)虽然资源丰富,但缺少批量筛选、对比的功能。通过爬虫采集数据后,我们可以:
- 信息聚合:将分散在不同页面的字体统一管理
- 数据分析:统计各类字体的数量分布、热门架字体,第一时间获取资源
目标字段清单
| 字段名 | 说明 | 示例值 |
|---|---|---|
| font_name | 字体名称 | "思源黑体 CN Bold" |
| category | 字体分类 | "黑体" / "手写体" |
| preview_url | 预览图链接 | "https://example.com/preview/123.png" |
| download_url | 下载链接 | "https://example.com/download/123.zip" |
| file_size | 文件大小 | "2.3 MB" |
| upload_time | 上传时间 | "2024-12-15" |
| font_id | 唯一标识 | "qiuziti_12345" |
3️⃣ 合规与注意事项(必写)
robots.txt 基本说明
在开始之前,必须先检查目标站点的 robots.txt 文件(通常在 https://example.com/robots.txt)。虽然很多字体站允许爬虫访问公开页面,但我们仍需遵守以下原则:
- 尊重 Disallow 规则:如果明确禁止某些路径,不要强行访问
- 合理使用数据:仅用于个人学习、数据分析,不用于商业转售
- 不抓取付费内容:有些字体需要付费或登录才能下载,不要绕过限制
频率控制
- 请求间隔:每次请求间隔 1-3 秒,避免对服务器造成压力
- 并发限制:初期单线程运行,进阶后控制在 3-5 个并发
- User-Agent:使用真实浏览器的 UA,避免被识别为机器人
数据使用边界
- ✅ 允许:采集公开展示的字体名称、分类等元数据
- ❌ 禁止:批量下载字体文件用于分发、破解付费限制、抓取用户隐私数据
4️⃣ 技术选型与整体流程(What/How)
静态 vs 动态 vs API
经过实际测试,求字体网的列表页和详情页均为服务端渲染的静态 HTML ,无需 JavaScript 执行即可获取完整内容。因此选择 requests + lxml 方案,性能高且代码简洁。
如果遇到动态加载(Ajax 分页),可以:
- 方案A:抓包找到 API 接口直接请求 JSON
- 方案B:使用 Selenium/Playwright 模拟浏览器
整体流程
[列表页] → 解析字体ID和基本信息 → [详情页] → 补充完整字段
↓ ↓
分页逻辑(翻页直到无数据) 提取下载链接、文件大小等
↓ ↓
数据清洗(去重、格式化) → 存储到 SQLite + 导出 CSV
核心步骤:
- 采集:requests 发送 HTTP 请求获取 HTML
- 解析:lxml 用 XPath 提取目标字段
- 清洗:处理缺失值、统一格式、去重
- 存储:SQLite 持久化 + CSV 备份
为什么选 requests + lxml?
- requests:轻量级 HTTP 库,支持 session 管理、自动重试
- lxml:基于 C 语言的 XML/HTML 解析器,速度是 BeautifulSoup 的 5-10 倍
- 适用场景:静态页面、数据量中等(几千到几万条)
如果需要更强的工程能力(分布式、中间件、数据管道),可以升级到 Scrapy。
5️⃣ 环境准备与依赖安装(可复现)
Python 版本
建议使用 Python 3.8+(本文基于 Python 3.10 测试)
依赖安装
bash
pip install requests lxml pandas --break-system-packages
依赖说明:
requests:HTTP 请求lxml:HTML 解析pandas:数据清洗和导出
推荐项目结构
json
font_crawler/
│
├── main.py # 主入口
├── fetcher.py # 请求层
├── parser.py # 解析层
├── storage.py # 存储层
├── config.py # 配置文件
├── requirements.txt # 依赖清单
│
├── data/
│ ├── fonts.db # SQLite 数据库
│ └── fonts.csv # CSV 导出文件
│
└── logs/
└── crawler.log # 运行日志
6️⃣ 核心实现:请求层(Fetcher)
代码实现(fetcher.py)
python
import requests
import time
from typing import Optional
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class FontFetcher:
"""字体站请求器"""
def __init__(self):
self.session = requests.Session()
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'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',
'Referer': 'https://www.qiuziti.com/',
'Connection': 'keep-alive'
}
self.timeout = 10
self.retry_times = 3
self.delay = 2 # 请求间隔(秒)
def fetch(self, url: str) -> Optional[str]:
"""
获取页面 HTML
Args:
url: 目标 URL
Returns:
HTML 字符串,失败返回 None
"""
for attempt in range(self.retry_times):
try:
response = self.session.get(
url,
headers=self.headers,
timeout=self.timeout
)
response.raise_for_status() # 检查 HTTP 状态码
# 尝试自动检测编码
response.encoding = response.apparent_encoding
# 请求间隔(避免频率过快)
time.sleep(self.delay)
logger.info(f"✅ 成功获取: {url}")
return response.text
except requests.exceptions.Timeout:
logger.warning(f"⏱️ 超时 (尝试 {attempt + 1}/{self.retry_times}): {url}")
time.sleep(2 ** attempt) # 指数退避
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
logger.error(f"🚫 403 Forbidden: {url} - 可能被反爬拦截")
elif e.response.status_code == 429:
logger.error(f"⚠️ 429 Too Many Requests: {url} - 请求过快")
time.sleep(10) # 冷却时间
else:
logger.error(f"❌ HTTP 错误 {e.response.status_code}: {url}")
return None
except Exception as e:
logger.error(f"❌ 未知错误: {url} - {str(e)}")
return None
logger.error(f"❌ 重试 {self.retry_times} 次后失败: {url}")
return None
关键要点说明
-
Session 管理 :使用
requests.Session()复用 TCP 连接,提升性能 -
Headers 配置:
User-Agent:伪装成 Chrome 浏览器Referer:表明来源(有些站点会检查)Accept-Language:优先返回中文内容
-
失败处理:
- 超时重试:最多重试 3 次,采用指数退避(2、4、8 秒)
- 403/429 处理:记录日志并跳过,避免触发 IP 封禁
-
编码处理 :使用
apparent_encoding自动检测(避免乱码)
7️⃣ 核心实现:解析层(Parser)
代码实现(parser.py)
python
from lxml import etree
from typing import List, Dict, Optional
import re
import logging
logger = logging.getLogger(__name__)
class FontParser:
"""字体信息解析器"""
@staticmethod
def parse_list_page(html: str) -> List[Dict]:
"""
解析列表页,提取字体基本信息
Args:
html: 列表页 HTML
Returns:
字体信息列表
"""
tree = etree.HTML(html)
fonts = []
# XPath 示例(需根据实际页面结构调整)
font_items = tree.xpath('//div[@class="font-item"]')
for item in font_items:
try:
font_data = {
'font_id': FontParser._extract_id(item),
'font_name': FontParser._extract_text(item, './/h3[@class="font-name"]/text()'),
'category': FontParser._extract_text(item, './/span[@class="category"]/text()'),
'preview_url': FontParser._extract_attr(item, './/img[@class="preview"]', 'src'),
'detail_url': FontParser._extract_attr(item, './/a[@class="detail-link"]', 'href')
}
# 数据验证(必需字段不能为空)
if font_data['font_id'] and font_data['font_name']:
fonts.append(font_data)
else:
logger.warning(f"⚠️ 跳过无效数据: {font_data}")
except Exception as e:
logger.error(f"❌ 解析列表项失败: {str(e)}")
continue
logger.info(f"📄 列表页解析完成,提取 {len(fonts)} 个字体")
return fonts
@staticmethod
def parse_detail_page(html: str) -> Dict:
"""
解析详情页,补充完整字段
Args:
html: 详情页 HTML
Returns:
详细信息字典
"""
tree = etree.HTML(html)
detail = {
'download_url': FontParser._extract_attr(tree, '//a[@class="download-btn"]', 'href'),
'file_size': FontParser._extract_text(tree, '//span[@class="file-size"]/text()'),
'upload_time': FontParser._extract_text(tree, '//span[@class="upload-time"]/text()'),
'description': FontParser._extract_text(tree, '//div[@class="description"]/text()')
}
return detail
@staticmethod
def _extract_id(element) -> Optional[str]:
"""从元素中提取唯一 ID"""
try:
# 方法1:从 data-id 属性提取
font_id = element.xpath('./@data-id')
if font_id:
return font_id[0]
# 方法2:从 URL 中提取
url = element.xpath('.//a/@href')
if url:
match = re.search(r'/font/(\d+)', url[0])
if match:
return f"qiuziti_{match.group(1)}"
except:
pass
return None
@staticmethod
def _extract_text(element, xpath: str) -> str:
"""提取文本内容"""
try:
result = element.xpath(xpath)
return result[0].strip() if result else ""
except:
return ""
@staticmethod
def _extract_attr(element, xpath: str, attr: str) -> str:
"""提取属性值"""
try:
result = element.xpath(xpath)
if result:
value = result[0].get(attr, "")
# 处理相对路径
if value and not value.startswith('http'):
value = f"https://www.qiuziti.com{value}"
return value
except:
pass
return ""
@staticmethod
def has_next_page(html: str) -> bool:
"""判断是否有下一页"""
tree = etree.HTML(html)
next_btn = tree.xpath('//a[@class="next-page" and not(@disabled)]')
return len(next_btn) > 0
解析策略说明
-
XPath vs CSS Selector:
- 本文选择 XPath(功能更强大,支持复杂逻辑)
- 如果熟悉 CSS,可用
cssselect库:tree.cssselect('.font-item')
-
列表页解析:
- 提取每个字体的基本信息和详情页链接
- 使用
detail_url进入详情页获取更多字段
-
详情页解析:
- 补充下载链接、文件大小等信息
- 合并到列表页的数据中
-
容错机制:
- 字段缺失:用空字符串填充(不中断流程)
- 解析异常:记录日志并跳过当前项
- 必需字段验证 :
font_id和font_name为空时丢弃数据
8️⃣ 数据存储与导出(Storage)
代码实现(storage.py)
python
import sqlite3
import pandas as pd
import logging
from typing import List, Dict
from pathlib import Path
logger = logging.getLogger(__name__)
class FontStorage:
"""字体数据存储管理器"""
def __init__(self, db_path: str = "data/fonts.db"):
self.db_path = db_path
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
self.conn = sqlite3.connect(db_path)
self._create_table()
def _create_table(self):
"""创建数据表"""
create_sql = """
CREATE TABLE IF NOT EXISTS fonts (
font_id TEXT PRIMARY KEY,
font_name TEXT NOT NULL,
category TEXT,
preview_url TEXT,
download_url TEXT,
file_size TEXT,
upload_time TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
self.conn.execute(create_sql)
self.conn.commit()
logger.info("✅ 数据表初始化完成")
def save_fonts(self, fonts: List[Dict]) -> int:
"""
批量保存字体信息(去重)
Args:
fonts: 字体数据列表
Returns:
新增数据条数
"""
insert_sql = """
INSERT OR IGNORE INTO fonts
(font_id, font_name, category, preview_url, download_url, file_size, upload_time, description)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
"""
data_tuples = [
(
font.get('font_id'),
font.get('font_name'),
font.get('category', ''),
font.get('preview_url', ''),
font.get('download_url', ''),
font.get('file_size', ''),
font.get('upload_time', ''),
font.get('description', '')
)
for font in fonts
]
cursor = self.conn.executemany(insert_sql, data_tuples)
self.conn.commit()
inserted = cursor.rowcount
logger.info(f"💾 成功插入 {inserted} 条新数据(重复数据已跳过)")
return inserted
def export_to_csv(self, csv_path: str = "data/fonts.csv"):
"""导出为 CSV 文件"""
df = pd.read_sql_query("SELECT * FROM fonts", self.conn)
df.to_csv(csv_path, index=False, encoding='utf-8-sig')
logger.info(f"📊 数据已导出到 {csv_path}(共 {len(df)} 条)")
def get_existing_ids(self) -> set:
"""获取已存在的字体 ID(用于增量更新)"""
cursor = self.conn.execute("SELECT font_id FROM fonts")
return {row[0] for row in cursor.fetchall()}
def get_stats(self) -> Dict:
"""统计数据"""
stats = {}
# 总数
cursor = self.conn.execute("SELECT COUNT(*) FROM fonts")
stats['total'] = cursor.fetchone()[0]
# 分类统计
cursor = self.conn.execute("""
SELECT category, COUNT(*)
FROM fonts
GROUP BY category
ORDER BY COUNT(*) DESC
""")
stats['by_category'] = dict(cursor.fetchall())
return stats
def close(self):
"""关闭数据库连接"""
self.conn.close()
字段映射表
| 字段名 | 类型 | 示例值 | 说明 |
|---|---|---|---|
| font_id | TEXT (主键) | "qiuziti_12345" | 唯一标识(防重复) |
| font_name | TEXT | "思源黑体 CN Bold" | 字体名称(必填) |
| category | TEXT | "黑体" | 字体分类 |
| preview_url | TEXT | "https://..." | 预览图链接 |
| download_url | TEXT | "https://..." | 下载地址 |
| file_size | TEXT | "2.3 MB" | 文件大小 |
| upload_time | TEXT | "2024-12-15" | 上传时间 |
| description | TEXT | "适合标题使用" | 字体描述 |
| created_at | TIMESTAMP | "2025-01-27 10:30:00" | 入库时间 |
去重策略
- 主键去重 :使用
font_id作为主键,INSERT OR IGNORE自动跳过重复数据 - 增量更新 :通过
get_existing_ids()获取已采集的 ID,避免重复请求详情页 - 内容 Hash (可选):如果没有唯一 ID,可对
font_name + category计算 MD5 作为唯一标识
9️⃣ 运行方式与结果展示(必写)
主程序(main.py)
python
import logging
from fetcher import FontFetcher
from parser import FontParser
from storage import FontStorage
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler('logs/crawler.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def main():
"""主流程"""
# 初始化组件
fetcher = FontFetcher()
parser = FontParser()
storage = FontStorage()
# 配置
base_url = "https://www.qiuziti.com/fonts"
max_pages = 10 # 最多爬取页数
logger.info("🚀 开始采集字体数据...")
total_fetched = 0
for page in range(1, max_pages + 1):
# 构造列表页 URL
list_url = f"{base_url}?page={page}"
# 获取 HTML
html = fetcher.fetch(list_url)
if not html:
logger.warning(f"⚠️ 第 {page} 页获取失败,跳过")
continue
# 解析列表页
fonts = parser.parse_list_page(html)
if not fonts:
logger.info(f"📭 第 {page} 页无数据,停止采集")
break
# 获取已存在的 ID(避免重复请求)
existing_ids = storage.get_existing_ids()
# 补充详情页信息
for font in fonts:
if font['font_id'] in existing_ids:
logger.info(f"⏭️ 跳过已存在: {font['font_name']}")
continue
if font.get('detail_url'):
detail_html = fetcher.fetch(font['detail_url'])
if detail_html:
detail = parser.parse_detail_page(detail_html)
font.update(detail) # 合并详情数据
# 保存到数据库
inserted = storage.save_fonts(fonts)
total_fetched += inserted
logger.info(f"✅ 第 {page} 页处理完成")
# 检查是否有下一页
if not parser.has_next_page(html):
logger.info("📄 已到最后一页")
break
# 导出 CSV
storage.export_to_csv()
# 统计信息
stats = storage.get_stats()
logger.info(f"""
========== 采集完成 ==========
📊 总计采集: {total_fetched} 条新数据
📚 数据库总量: {stats['total']} 条
📂 分类统计: {stats['by_category']}
==============================
""")
storage.close()
if __name__ == "__main__":
main()
启动方式
bash
# 1. 进入项目目录
cd font_crawler
# 2. 创建必要的文件夹
mkdir -p data logs
# 3. 运行爬虫
python main.py
输出示例
终端日志:
json
2025-01-27 14:30:15 [INFO] 🚀 开始采集字体数据...
2025-01-27 14:30:17 [INFO] ✅ 成功获取: https://www.qiuziti.com/fonts?page=1
2025-01-27 14:30:17 [INFO] 📄 列表页解析完成,提取 20 个字体
2025-01-27 14:30:20 [INFO] 💾 成功插入 18 条新数据(重复数据已跳过)
...
2025-01-27 14:35:42 [INFO] 📊 数据已导出到 data/fonts.csv(共 156 条)
CSV 文件示例(data/fonts.csv):
| font_id | font_name | category | preview_url | download_url | file_size | upload_time |
|---|---|---|---|---|---|---|
| qiuziti_10001 | 思源黑体 CN Bold | 黑体 | https://... | https://... | 2.3 MB | 2024-12-10 |
| qiuziti_10002 | 方正楷体简体 | 楷体 | https://... | https://... | 1.8 MB | 2024-12-12 |
| qiuziti_10003 | 站酷快乐体 | 手写体 | https://... | https://... | 3.1 MB | 2024-12-15 |
🔟 常见问题与排错(强烈建议写)
问题1:403 Forbidden / 429 Too Many Requests
现象 :请求被拒绝,日志显示 403 或 429 状态码
原因:
- 请求频率过快,触发反爬机制
- User-Agent 被识别为爬虫
- IP 被临时封禁
解决方案:
python
# 1. 增加请求间隔
self.delay = 3 # 改为 3-5 秒
# 2. 随机化 User-Agent
import random
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...',
# 更多 UA
]
self.headers['User-Agent'] = random.choice(user_agents)
# 3. 使用代理池(如确实被封禁)
proxies = {'http': 'http://proxy-server:port'}
response = self.session.get(url, proxies=proxies)
问题2:HTML 抓到空壳(动态渲染)
现象 :HTML 中找不到目标数据,全是空的 <div id="root"></div>
原因:页面使用 React/Vue 等前端框架,数据通过 Ajax 异步加载
排查步骤:
- 打开浏览器开发者工具(F12)→ Network 标签
- 刷新页面,查看 XHR/Fetch 请求
- 找到返回 JSON 数据的 API 接口
解决方案:
python
# 方案A:直接请求 API(最佳)
api_url = "https://www.qiuziti.com/api/fonts?page=1"
response = requests.get(api_url, headers=headers)
data = response.json()
# 方案B:使用 Selenium/Playwright(次选)
from selenium import webdriver
driver = webdriver.Chrome()
driver.get(url)
html = driver.page_source
问题3:XPath/CSS 选择器找不到元素
现象 :parser.parse_list_page() 返回空列表
原因:
- 选择器写错(拼写、大小写)
- 网站改版,HTML 结构变化
- 使用了动态生成的 class 名(如
class="css-1a2b3c")
调试技巧:
python
# 1. 打印完整 HTML,检查结构
with open('debug.html', 'w', encoding='utf-8') as f:
f.write(html)
# 2. 在浏览器中测试 XPath
# Chrome Console: $x('//div[@class="font-item"]')
# 3. 使用更宽松的选择器
# 从 '//div[@class="font-item"]' 改为 '//div[contains(@class, "font")]'
问题4:编码乱码
现象 :中文显示为 ������ 或 \xe4\xb8\xad
解决方案:
python
# 方法1:让 requests 自动检测
response.encoding = response.apparent_encoding
# 方法2:手动指定(如已知是 GBK)
response.encoding = 'gbk'
# 方法3:CSV 导出时使用 BOM(Excel 兼容)
df.to_csv('fonts.csv', encoding='utf-8-sig')
问题5:数据库锁死(SQLite)
现象 :sqlite3.OperationalError: database is locked
原因:多线程同时写入 SQLite(不支持高并发写)
解决方案:
python
# 方案A:使用队列 + 单线程写入
import queue
db_queue = queue.Queue()
def db_writer():
while True:
fonts = db_queue.get()
storage.save_fonts(fonts)
# 方案B:切换到 MySQL/PostgreSQL(支持并发)
1️⃣1️⃣ 进阶优化(可选但加分)
并发加速
单线程太慢?试试多线程:
python
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch_detail(font):
"""获取单个字体的详情"""
if font.get('detail_url'):
html = fetcher.fetch(font['detail_url'])
if html:
detail = parser.parse_detail_page(html)
font.update(detail)
return font
# 在 main() 中使用线程池
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(fetch_detail, font) for font in fonts]
for future in as_completed(futures):
result = future.result()
# 处理结果
注意事项:
- 控制并发数(建议 3-5 个)
- 线程安全:SQLite 写入仍需单线程
断点续跑
需求:爬虫中途断了,不想从头再来
实现思路:
python
# 1. 记录已完成的页数
with open('checkpoint.txt', 'r') as f:
start_page = int(f.read().strip())
# 2. 从断点处继续
for page in range(start_page, max_pages + 1):
# ... 爬取逻辑
# 3. 每爬完一页就更新检查点
with open('checkpoint.txt', 'w') as f:
f.write(str(page))
日志与监控
需求:实时查看成功率、失败原因
增强日志:
python
import logging
from logging.handlers import RotatingFileHandler
# 按大小自动切割日志(每个 10MB)
handler = RotatingFileHandler(
'logs/crawler.log',
maxBytes=10*1024*1024,
backupCount=5
)
# 统计指标
metrics = {
'total_requests': 0,
'success': 0,
'failed': 0,
'retry': 0
}
# 每 100 次请求输出一次统计
if metrics['total_requests'] % 100 == 0:
success_rate = metrics['success'] / metrics['total_requests'] * 100
logger.info(f"📊 成功率: {success_rate:.2f}%")
定时任务
需求:每天自动采集新字体
方案A:Linux Cron
bash
# 编辑 crontab
crontab -e
# 每天凌晨 2 点执行
0 2 * * * cd /path/to/font_crawler && /usr/bin/python3 main.py
方案B:Python APScheduler
python
from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler()
scheduler.add_job(main, 'cron', hour=2) # 每天 2 点
scheduler.start()
1️⃣2️⃣ 总结与延伸阅读
我们完成了什么?
通过这篇文章,你已经掌握了:
✅ 完整的爬虫工作流 :从请求到存储的全链路实现
✅ 工程化实践 :分层架构、错误处理、日志记录
✅ 数据管理能力 :去重、增量更新、多格式导出
✅ 问题排查技巧:403/429、动态渲染、编码乱码等常见坑的解决方案
这套代码不是玩具,是真正能跑起来的生产级爬虫。你可以直接用它采集几千条字体数据,也可以改几行代码适配其他站点。
下一步可以做什么?
如果你想进一步提升,可以尝试:
🚀 升级到 Scrapy :学习专业爬虫框架,支持分布式、中间件、Item Pipeline
🎭 处理复杂反爬 :研究 Playwright(绕过 Cloudflare)、验证码识别(OCR/打码平台)
☁️ 云端部署 :把爬虫部署到 AWS/阿里云,用 Docker 容器化
📊 数据可视化 :用 Matplotlib/ECharts 分析字体分类趋势、下载量分布
🤖 AI 辅助:用 GPT-4 自动生成字体评测文章,打造内容平台
推荐学习资源:
- Scrapy 官方文档:https://docs.scrapy.org
- 《Python 网络爬虫权威指南》(Web Scraping with Python)
- 爬虫反反爬实战案例集:https://github.com/luyishisi/Anti-Anti-Spider
写在最后
写爬虫的过程,就像是在和网站"对话"。你要尊重它的规则(robots.txt),理解它的结构(HTML),也要学会应对它的脾气(反爬)。记住:技术是中性的,关键在于如何使用。
希望这篇文章不仅教会你如何写代码,更重要的是培养你独立解决问题的能力。当你遇到新站点时,能够快速分析、调试、优化------这才是爬虫工程师的核心竞争力。
如果你在实践中遇到问题,欢迎在评论区交流。祝你采集顺利,数据满满!
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。技术无罪,责任在人!!!