Python爬虫实战:从零搭建字体库爬虫 - requests+lxml 实战采集字体网字体信息数据(附 CSV 导出)!

㊙️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!

㊗️爬虫难度指数:⭐

🚫声明:本数据&代码仅供学习交流,严禁用于商业用途、倒卖数据或违反目标站点的服务条款等,一切后果皆由使用者本人承担。公开榜单数据一般允许访问,但请务必遵守"君子协议",技术无罪,责任在人。

全文目录:

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: 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

核心步骤

  1. 采集:requests 发送 HTTP 请求获取 HTML
  2. 解析:lxml 用 XPath 提取目标字段
  3. 清洗:处理缺失值、统一格式、去重
  4. 存储: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

关键要点说明

  1. Session 管理 :使用 requests.Session() 复用 TCP 连接,提升性能

  2. Headers 配置

    • User-Agent:伪装成 Chrome 浏览器
    • Referer:表明来源(有些站点会检查)
    • Accept-Language:优先返回中文内容
  3. 失败处理

    • 超时重试:最多重试 3 次,采用指数退避(2、4、8 秒)
    • 403/429 处理:记录日志并跳过,避免触发 IP 封禁
  4. 编码处理 :使用 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

解析策略说明

  1. XPath vs CSS Selector

    • 本文选择 XPath(功能更强大,支持复杂逻辑)
    • 如果熟悉 CSS,可用 cssselect 库:tree.cssselect('.font-item')
  2. 列表页解析

    • 提取每个字体的基本信息和详情页链接
    • 使用 detail_url 进入详情页获取更多字段
  3. 详情页解析

    • 补充下载链接、文件大小等信息
    • 合并到列表页的数据中
  4. 容错机制

    • 字段缺失:用空字符串填充(不中断流程)
    • 解析异常:记录日志并跳过当前项
    • 必需字段验证font_idfont_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

现象 :请求被拒绝,日志显示 403429 状态码

原因

  • 请求频率过快,触发反爬机制
  • 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 异步加载

排查步骤

  1. 打开浏览器开发者工具(F12)→ Network 标签
  2. 刷新页面,查看 XHR/Fetch 请求
  3. 找到返回 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 自动生成字体评测文章,打造内容平台

推荐学习资源

写在最后

写爬虫的过程,就像是在和网站"对话"。你要尊重它的规则(robots.txt),理解它的结构(HTML),也要学会应对它的脾气(反爬)。记住:技术是中性的,关键在于如何使用

希望这篇文章不仅教会你如何写代码,更重要的是培养你独立解决问题的能力。当你遇到新站点时,能够快速分析、调试、优化------这才是爬虫工程师的核心竞争力。

如果你在实践中遇到问题,欢迎在评论区交流。祝你采集顺利,数据满满!

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


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

相关推荐
2301_790300962 小时前
Python深度学习入门:TensorFlow 2.0/Keras实战
jvm·数据库·python
程序员敲代码吗4 小时前
用Python生成艺术:分形与算法绘图
jvm·数据库·python
Yyyyy123jsjs4 小时前
如何通过免费的外汇API轻松获取实时汇率数据
开发语言·python
喵手4 小时前
Python爬虫实战:GovDataMiner —— 开放数据门户数据集元数据采集器(附 CSV 导出)!
爬虫·python·爬虫实战·python爬虫工程化实战·零基础python爬虫教学·open data·开放数据门户数据集列表
历程里程碑4 小时前
滑动窗口---- 无重复字符的最长子串
java·数据结构·c++·python·算法·leetcode·django
人工智能AI技术5 小时前
【Agent从入门到实践】43 接口封装:将Agent封装为API服务,供其他系统调用
人工智能·python
Darkershadow5 小时前
蓝牙学习之Time Set
python·学习·蓝牙·ble·mesh
m0_736919106 小时前
超越Python:下一步该学什么编程语言?
jvm·数据库·python