在这里插入图片描述

文章目录


学习目标

学完这一课,你将能够:

  1. 理解全站爬虫的核心设计思想——知道如何从一个入口页面出发,自动发现、遍历并采集整个网站或某个板块的所有数据
  2. 掌握URL去重与爬取队列管理——用集合实现高性能去重,用队列控制爬取顺序和广度优先策略
  3. 实现自动分页和循环翻页——自动识别“下一页”链接,无需手动构造分页URL
  4. 控制爬取深度与范围——限制爬取层级,避免爬出目标域名,防止陷入无限链接黑洞
  5. 集成异常重试与日志系统——让爬虫在遇到网络波动时自动重试,并用日志记录每一步运行状态
  6. 开发完整的全站爬虫项目——将前面18课的知识(请求、解析、存储、多线程等)整合成一个可复用的通用爬虫框架
  7. 建立良好的工程化思维——代码分层、配置分离、面向接口设计,后续可轻松扩展

这一课是整个专栏的“集大成者”,你会看到之前学过的所有技术点如何有机组合,形成一个真正的生产级爬虫项目。

一、通俗原理:全站爬虫就像扫描整栋大楼

1.1 从“单页采集”到“全站遍历”

之前的课程中,我们每次都是针对一个已知的URL列表(比如分页URL、商品详情页URL)进行采集。这是“定点采集”:你知道目标页面的地址规律,直接构造请求。

但全站爬虫面对的是未知的链接结构。你只有一个入口网址(比如博客首页、商品分类页),你需要让程序自己“逛遍”整个网站,找到所有文章页或商品详情页,并把数据抓回来。

类比:你进入一栋陌生大楼(网站),手里只有一楼大厅的门(入口URL)。你的任务是拿到每个房间里的文件(数据)。你需要:

  • 从大厅开始,看到走廊和房间门牌(超链接)
  • 走进每个房间,拿到文件(解析数据)
  • 同时记住还有哪些走廊没走(待爬队列)
  • 避免重复进入同一个房间(URL去重)
  • 设定最多走几层楼(深度控制),防止大楼无限延伸

1.2 广度优先 vs 深度优先

爬虫遍历网站主要有两种策略:

  • 广度优先(BFS):先爬完当前页面的所有链接,再处理下一层。优点是能尽快覆盖更广泛的内容,且天然防止栈溢出。适合网站结构较深但内容分布均匀的情况。
  • 深度优先(DFS):沿着一条链接深入,直到尽头再回溯。优点是实现简单,但容易陷入很深的分支而忽略其他重要页面。

全站爬虫几乎都采用广度优先,因为它对爬取深度控制更好,也更容易实现增量抓取。

1.3 核心组件图解

一个通用的全站爬虫主要由以下模块组成:

[种子URL] → [URL队列] → [调度器] → [下载器] → [解析器] → [数据存储]
                ↑                           ↓
                └─────── [URL去重池] ←── [提取新URL]
                
同时伴随:[日志系统] [重试机制] [深度控制] [限速]

调度器:从队列中取出URL,交给下载器。下载器负责发送HTTP请求;解析器从HTML中提取数据,同时提取新的URL;新URL经过去重后加入队列。整个过程循环,直到队列为空或达到停止条件(如最大页面数、最大深度)。

二、全站爬虫开发的核心技术难点

2.1 URL去重池

问题:网页之间互相链接,很容易形成循环引用(A→B→C→A)。如果不加去重,爬虫会无限重复爬取同一页面。

解决方案:用一个集合seen_urls存储已经处理过(或已加入队列)的URL的规范化字符串。每次提取到新URL时,先检查是否在集合中,不在才加入队列。

注意:URL规范化处理非常重要。同一个页面可能有多个不同形式的URL:

  • http://example.com/page
  • http://example.com/page?utm_source=google
  • https://example.com/page
  • http://example.com/page#section

通常要去掉锚点(#后面的部分)、统一协议(强制转为http或https),去除无关的查询参数(如跟踪参数)。但对于分页参数(?page=2),必须保留。因此规范化规则需要根据目标网站定制。

2.2 深度控制

问题:网站可能有无穷的链接(例如日历控件、无限滚动),或者你不希望爬虫跑到外层页面(比如点击了广告链接跳到其他域名)。

解决方案:在每个URL入队时记录其深度(种子URL深度为0)。从该页面提取的新URL深度为当前深度+1。在添加新URL之前,检查是否超过最大深度限制(例如max_depth=3)。

同时,限制只爬取特定域名(allowed_domains),防止爬出站外。

2.3 自动分页与循环翻页

问题:对于列表页,你需要自动找到“下一页”链接,并不断爬取,直到没有下一页为止。

解决方案:在解析列表页时,除了提取数据项,还要提取“下一页”的链接。如果存在,就将该链接去重后加入队列。由于这是同层级的分页,其深度应保持与当前页相同(而不是深度+1),否则会把分页当作子页面,导致深度迅速超标。

2.4 异常重试与容错

网络请求不可能永远成功。需要为每个URL设置重试次数(例如最多3次),并在失败时等待后重试。同时要避免重复重试,避免无限卡死。

2.5 爬取策略与优先级

对于大型网站,你可能需要区分“列表页”和“详情页”的不同处理逻辑。通常做法是:

  • 列表页只提取分页链接和数据项链接
  • 详情页提取实际业务数据(标题、正文、价格等)
  • 可以使用正则或XPath根据URL特征区分页面类型

2.6 限速与礼貌爬取

全站爬虫可能产生大量请求,必须设置请求间隔(如每次请求后sleep(random.uniform(0.5, 2.0))),并尊重robots.txt中的Crawl-delay。另外,可以设置每秒最大请求数(Rate Limiter)。

也可以使用多线程提高速度,但需要用队列和锁控制。

三、设计方案与项目结构

3.1 项目结构

我们将构建一个可配置、可复用的全站爬虫框架,文件结构如下:

spider_framework/
│
├── config.py           # 配置文件(目标网站、请求头、深度限制、输出设置等)
├── spider.py           # 核心爬虫类(调度、下载、解析、去重)
├── url_utils.py        # URL处理工具(规范化、域名提取、相对路径转绝对路径)
├── data_storage.py     # 数据存储模块(CSV/JSON/数据库,可插拔)
├── logger.py           # 日志配置
├── main.py             # 入口程序
└── requirements.txt    # 依赖库

3.2 数据流设计

  1. 初始化:配置日志、创建存储对象、初始化URL队列和去重集合。
  2. 添加种子URL(入口)。种子URL深度为0。
  3. 主循环:当队列不为空且已爬取页面数未达上限时:
    • 从队列取出一个URL(先进先出,广度优先)
    • 标记为“已处理”,记录深度
    • 下载页面内容(带重试)
    • 解析页面:提取业务数据(调用解析回调函数)、提取新URL
    • 对每个新URL:规范化、去重、未超深度、域名符合 → 加入队列
    • 存储提取到的业务数据
    • 增加爬取计数,控制请求频率
  4. 循环结束,输出统计信息。

3.3 扩展性设计

  • 解析器插件化:通过传入不同的解析函数(parse_page),适配不同网站的数据提取逻辑。
  • 存储可插拔:通过继承BaseStorage,实现CSV、SQLite、MySQL等不同存储方式。
  • 请求处理器:可以替换requests为aiohttp(异步)或添加代理支持。

四、手把手实现全站爬虫(完整代码)

下面我们逐步实现一个稳健的全站爬虫框架。为了便于理解,我们以爬取一个开源测试网站 http://quotes.toscrape.com/ 为例,它包含分页和作者链接,非常适合演示全站爬取。

4.1 第一步:配置模块(config.py)

# config.py
"""
全站爬虫配置文件
"""
# 目标网站配置
START_URL = "http://quotes.toscrape.com/"   # 入口URL
ALLOWED_DOMAINS = ["quotes.toscrape.com"]   # 允许爬取的域名列表
MAX_DEPTH = 3                               # 最大爬取深度(种子为0)
MAX_PAGES = 100                             # 最多爬取页面数(防止无限爬)
REQUEST_DELAY = (1, 3)                      # 请求延迟范围(秒),均匀随机
REQUEST_TIMEOUT = 10                        # 请求超时秒数
MAX_RETRIES = 3                             # 请求失败最大重试次数
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"

# 解析规则(示例:提取名言网站的名言数据)
PARSE_RULES = {
    "list_page": {      # 列表页特征(包含分页链接和名言条目)
        "item_url_xpath": "//div[@class='quote']/span/a/@href",  # 详情页链接
        "next_page_xpath": "//li[@class='next']/a/@href",       # 下一页链接
        "item_selector": ".quote"                               # 用于测试页面类型
    },
    "detail_page": {    # 详情页提取字段(名言网站没有独立详情页,示例中不加)
        # 这里可根据需求定义
    }
}

# 存储配置
OUTPUT_TYPE = "csv"     # csv 或 json
OUTPUT_FILE = "quotes_data.csv"
# 数据库配置(如果使用MySQL需填写)
DB_CONFIG = {
    "host": "localhost",
    "user": "root",
    "password": "",
    "database": "spider_db"
}

4.2 日志模块(logger.py)

# logger.py
import logging

def setup_logger(name="spider", log_file="spider.log", level=logging.INFO):
    """配置日志记录器"""
    logger = logging.getLogger(name)
    logger.setLevel(level)
    
    # 文件处理器
    fh = logging.FileHandler(log_file, encoding='utf-8')
    fh.setLevel(level)
    
    # 控制台处理器
    ch = logging.StreamHandler()
    ch.setLevel(level)
    
    # 格式化
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    fh.setFormatter(formatter)
    ch.setFormatter(formatter)
    
    logger.addHandler(fh)
    logger.addHandler(ch)
    return logger

4.3 URL工具模块(url_utils.py)

# url_utils.py
from urllib.parse import urljoin, urlparse, urlunparse
import re

def normalize_url(url, base_url=None):
    """
    规范化URL:
    - 将相对路径转换为绝对路径(基于base_url)
    - 去除fragment(#后面的部分)
    - 统一协议为http(可选,实际可保留https)
    - 去除常见的跟踪查询参数(如utm_*)
    """
    if base_url:
        url = urljoin(base_url, url)
    
    parsed = urlparse(url)
    # 去除fragment
    parsed = parsed._replace(fragment='')
    # 可选:统一scheme为http(如果两个都有,为了去重,可统一为https)
    # 但应谨慎,因为http和https可能不同页面
    # 这里不强制,保持原样
    
    # 去除跟踪参数(保留分页参数等需要根据情况,这里简单示例)
    query_params = []
    if parsed.query:
        # 只保留常用参数,过滤掉utm_source等
        for param in parsed.query.split('&'):
            if not param.startswith('utm_'):
                query_params.append(param)
        new_query = '&'.join(query_params)
        parsed = parsed._replace(query=new_query)
    
    return urlunparse(parsed)

def get_domain(url):
    """提取域名"""
    parsed = urlparse(url)
    return parsed.netloc

def is_allowed_url(url, allowed_domains):
    """检查URL域名是否在白名单中"""
    domain = get_domain(url)
    for allowed in allowed_domains:
        if domain == allowed or domain.endswith('.' + allowed):
            return True
    return False

def extract_links(html, base_url, xpath_expr=None):
    """
    从HTML中提取链接(使用XPath或正则)
    这里简化,实际使用时可根据xpath提取
    """
    # 本示例中我们会在解析模块中直接使用BeautifulSoup,所以这里只是占位
    pass

4.4 数据存储模块(data_storage.py)

# data_storage.py
import csv
import json
import os

class BaseStorage:
    """存储基类"""
    def save(self, item):
        raise NotImplementedError
    
    def close(self):
        pass

class CSVStorage(BaseStorage):
    def __init__(self, filename, fieldnames):
        self.filename = filename
        self.fieldnames = fieldnames
        self.file_exists = os.path.isfile(filename)
        self.f = open(filename, 'a', encoding='utf-8-sig', newline='')
        self.writer = csv.DictWriter(self.f, fieldnames=fieldnames)
        if not self.file_exists:
            self.writer.writeheader()
    
    def save(self, item):
        self.writer.writerow(item)
    
    def close(self):
        self.f.close()

class JSONStorage(BaseStorage):
    def __init__(self, filename):
        self.filename = filename
        self.data = []
    
    def save(self, item):
        self.data.append(item)
    
    def close(self):
        with open(self.filename, 'w', encoding='utf-8') as f:
            json.dump(self.data, f, ensure_ascii=False, indent=2)

class MultiStorage:
    """支持同时写入多个存储后端"""
    def __init__(self, storages):
        self.storages = storages
    
    def save(self, item):
        for s in self.storages:
            s.save(item)
    
    def close(self):
        for s in self.storages:
            s.close()

4.5 核心爬虫类(spider.py)

这是最重要的部分,实现调度、下载、解析、去重、重试等全部逻辑。

# spider.py
import requests
import time
import random
from urllib.parse import urljoin
from bs4 import BeautifulSoup
from collections import deque
from threading import Lock
import traceback

from url_utils import normalize_url, is_allowed_url
from logger import setup_logger

class WebSpider:
    def __init__(self, config, parse_callback=None, storage=None):
        """
        初始化爬虫
        :param config: 配置字典(包括 START_URL, MAX_DEPTH, MAX_PAGES, REQUEST_DELAY 等)
        :param parse_callback: 自定义解析函数,接收 (url, html, depth),返回 (items, new_urls)
        :param storage: 存储实例
        """
        self.config = config
        self.start_url = config['START_URL']
        self.allowed_domains = config.get('ALLOWED_DOMAINS', [])
        self.max_depth = config.get('MAX_DEPTH', 3)
        self.max_pages = config.get('MAX_PAGES', 100)
        self.request_delay = config.get('REQUEST_DELAY', (1, 3))
        self.timeout = config.get('REQUEST_TIMEOUT', 10)
        self.max_retries = config.get('MAX_RETRIES', 3)
        self.user_agent = config.get('USER_AGENT', 'Mozilla/5.0')
        
        self.parse_callback = parse_callback or self.default_parse
        self.storage = storage
        
        self.logger = setup_logger(name="Spider")
        
        # URL队列(广度优先,使用deque)
        self.queue = deque()
        # 去重集合(存储规范化后的URL)
        self.seen_urls = set()
        # 统计
        self.crawled_count = 0
        self.lock = Lock()  # 保护计数和日志等,若单线程可不加,但预留
        
        # 添加种子URL
        self._add_url(self.start_url, depth=0)
    
    def _add_url(self, url, depth):
        """添加URL到队列,进行去重和深度检查"""
        norm_url = normalize_url(url)
        if norm_url in self.seen_urls:
            return
        if not is_allowed_url(norm_url, self.allowed_domains):
            self.logger.debug(f"Ignored out-of-domain URL: {norm_url}")
            return
        if depth > self.max_depth:
            self.logger.debug(f"URL depth {depth} exceeds max_depth {self.max_depth}: {norm_url}")
            return
        self.seen_urls.add(norm_url)
        self.queue.append((norm_url, depth))
        self.logger.info(f"Added URL (depth={depth}): {norm_url}")
    
    def _request_with_retry(self, url):
        """带重试的请求"""
        for attempt in range(self.max_retries):
            try:
                headers = {'User-Agent': self.user_agent}
                # 随机延迟(礼貌爬取)
                delay = random.uniform(*self.request_delay)
                time.sleep(delay)
                resp = requests.get(url, headers=headers, timeout=self.timeout)
                if resp.status_code == 200:
                    return resp.text
                else:
                    self.logger.warning(f"Request failed with status {resp.status_code}, url={url}, attempt={attempt+1}")
            except Exception as e:
                self.logger.warning(f"Request exception: {e}, url={url}, attempt={attempt+1}")
            # 指数退避等待
            wait = 2 ** attempt
            time.sleep(wait)
        self.logger.error(f"Failed to fetch {url} after {self.max_retries} attempts")
        return None
    
    def default_parse(self, url, html, depth):
        """
        默认解析函数,提取所有链接(用于通用爬虫,不提取具体数据)
        返回 (items, new_urls)
        """
        soup = BeautifulSoup(html, 'lxml')
        new_urls = []
        # 提取所有 <a> 标签的 href
        for a in soup.find_all('a', href=True):
            href = a['href']
            abs_url = urljoin(url, href)
            new_urls.append(abs_url)
        return [], new_urls
    
    def crawl(self):
        """开始爬取"""
        self.logger.info(f"Starting spider, seed: {self.start_url}, max_depth={self.max_depth}, max_pages={self.max_pages}")
        
        while self.queue and self.crawled_count < self.max_pages:
            url, depth = self.queue.popleft()
            self.logger.info(f"Crawling [{self.crawled_count+1}/{self.max_pages}]: {url} (depth={depth})")
            
            # 下载页面
            html = self._request_with_retry(url)
            if not html:
                continue
            
            # 解析页面
            try:
                items, new_urls = self.parse_callback(url, html, depth)
            except Exception as e:
                self.logger.error(f"Parse error for {url}: {e}\n{traceback.format_exc()}")
                continue
            
            # 存储数据项
            if items and self.storage:
                for item in items:
                    if isinstance(item, dict):
                        self.storage.save(item)
                    else:
                        self.logger.warning(f"Ignored non-dict item: {item}")
            
            # 添加新URL到队列
            for new_url in new_urls:
                norm = normalize_url(new_url, base_url=url)
                # 新URL的深度 = 当前深度 + 1
                self._add_url(norm, depth + 1)
            
            self.crawled_count += 1
        
        self.logger.info(f"Crawl finished. Total crawled: {self.crawled_count}, queue remaining: {len(self.queue)}")
        if self.storage:
            self.storage.close()

4.6 自定义解析示例(针对quotes.toscrape.com)

在main.py中定义具体的解析函数,提取名言数据并发现新链接。

# main.py
import sys
sys.path.append('.')  # 确保导入模块

from spider import WebSpider
from data_storage import CSVStorage
from config import *
from bs4 import BeautifulSoup
from urllib.parse import urljoin

def parse_quotes(url, html, depth):
    """
    针对 quotes.toscrape.com 的解析函数
    返回 (items, new_urls)
    items: 名言数据字典列表
    new_urls: 待爬取的新URL列表(包括分页和作者页)
    """
    soup = BeautifulSoup(html, 'lxml')
    items = []
    new_urls = []
    
    # 1. 提取当前页面的名言数据
    quote_divs = soup.find_all('div', class_='quote')
    for quote in quote_divs:
        text = quote.find('span', class_='text').text if quote.find('span', class_='text') else ''
        author = quote.find('small', class_='author').text if quote.find('small', class_='author') else ''
        tags = [tag.text for tag in quote.find_all('a', class_='tag')]
        items.append({
            'text': text,
            'author': author,
            'tags': ', '.join(tags),
            'source_url': url
        })
    
    # 2. 提取“下一页”链接(分页)
    next_li = soup.find('li', class_='next')
    if next_li:
        next_a = next_li.find('a')
        if next_a and next_a.get('href'):
            next_url = urljoin(url, next_a['href'])
            new_urls.append(next_url)
    
    # 3. 提取作者详情页链接(可选,深度控制)
    author_links = soup.find_all('a', href=True)
    for a in author_links:
        href = a['href']
        if '/author/' in href:
            full_url = urljoin(url, href)
            new_urls.append(full_url)
    
    return items, new_urls

def main():
    # 配置
    config = {
        'START_URL': START_URL,
        'ALLOWED_DOMAINS': ALLOWED_DOMAINS,
        'MAX_DEPTH': MAX_DEPTH,
        'MAX_PAGES': MAX_PAGES,
        'REQUEST_DELAY': REQUEST_DELAY,
        'REQUEST_TIMEOUT': REQUEST_TIMEOUT,
        'MAX_RETRIES': MAX_RETRIES,
        'USER_AGENT': USER_AGENT
    }
    
    # 存储
    storage = CSVStorage(OUTPUT_FILE, fieldnames=['text', 'author', 'tags', 'source_url'])
    
    # 创建爬虫
    spider = WebSpider(config, parse_callback=parse_quotes, storage=storage)
    
    # 开始爬取
    spider.crawl()

if __name__ == '__main__':
    main()

4.7 运行与测试

运行 python main.py,你会看到爬虫从首页出发:

  1. 爬取首页(深度0),提取名言数据和下一页链接(/page/2/)以及作者详情页链接。
  2. 将下一页加入队列,深度保持0(因为我们希望分页属于同层,但在我们的实现中,_add_url使用的是depth+1,导致下一页深度+1。我们需要特殊处理:对于分页链接,通常不希望增加深度。可以改进:在解析函数中,对分页链接设置new_depth = depth,对普通链接设置depth+1)。

改进建议:修改解析函数返回new_urls时,可以同时返回每个URL的建议深度,或者让调用者根据URL特征判断。为了简单,我们可以修改_add_url方法,允许传入depth参数,而解析函数可以自定义。这里为了演示完整性,暂时理解深度控制需要精细处理。

实际应用中,你可以为不同类型的链接分配不同深度增量。

4.8 增强功能:多线程版本

利用第18课的多线程知识,可以快速将上述爬虫改为多线程,提升采集速度。主要改动:

  • queue替换为线程安全的queue.Queue
  • 使用threading.Thread启动多个工作线程
  • 共享seen_urls集合需要加锁

由于篇幅,这里不展开完整代码,但提供了思路。

五、场景举例:全站爬虫的应用

5.1 博客网站全量文章备份

假设你想备份一个技术博客的所有文章。入口是首页,文章页面URL一般包含/post//article/。你只需要:

  • 配置允许域名
  • 解析函数中识别文章页(URL模式),提取标题、正文、发布时间
  • 限制最大深度为2(首页→分类页→文章页)
  • 存储为Markdown文件或数据库

5.2 电商网站商品信息采集

电商网站通常有复杂的分类和分页。全站爬虫可以:

  • 从商品分类页开始
  • 提取每个商品详情页链接
  • 同时提取“更多分类”链接(需控制深度)
  • 对商品详情页提取名称、价格、销量、评论等
  • 设置MAX_DEPTH为3(首页→分类→商品列表→商品详情)
  • 注意不要爬取购物车、用户中心等无用页面

5.3 新闻网站热点追踪

新闻网站首页有很多新闻链接,点击进入正文。可以定时运行全站爬虫,只爬深度1的页面,采集标题和内容,用于舆情监控。

5.4 站内搜索功能模拟

有些网站没有提供API,你可以通过遍历所有页面,建立自己的站内索引,实现离线搜索。

六、新手常见误区

误区1:不对URL进行规范化,导致大量重复

两个URL http://example.com?page=2http://example.com?page=2&utm_source=email 如果不处理,会被视为不同URL,浪费资源。一定要做规范化。

误区2:忽略robots.txt和爬取许可

全站爬虫更应该遵守robots.txt。可以使用robotparser模块检查。本课为了简化未加入,但生产环境必须加入。

误区3:深度控制不当导致爬取范围失控

如果不限制深度,爬虫可能顺着友情链接爬到外站,或者陷入日历链接的无限循环。必须设置深度限制和域名白名单。

误区4:忘记处理相对路径

提取到的链接很多是相对路径(/about),需要用urljoin转换为绝对路径再入队。

误区5:没有做请求频率限制

全站爬虫会产生大量请求,必须设置随机延迟,否则容易被封IP。

误区6:内存爆炸

seen_urls集合随着爬取页面增多而增大,对于数百万URL,内存可能不足。此时需要改用基于磁盘的去重(如布隆过滤器、Redis Set)。本课示例适用于中小规模网站。

误区7:不考虑网站编码

有些网站编码不是UTF-8,可能导致乱码。应在requests中自动检测编码或显式设置。

误区8:过度追求全站,忽略业务聚焦

有时候我们需要只爬取特定类型的页面(如商品详情),而不是所有页面。解析函数中应该根据URL模式或页面特征过滤,只处理目标页面。

误区9:没有异常处理导致崩溃

网络请求、解析都可能出现异常,应该全面捕获,确保单个页面失败不影响整体。

误区10:单线程太慢,多线程没有同步

如果采用多线程,共享数据(seen_urls, queue)必须加锁,否则会出现重复爬取或数据错乱。

七、总结

本课核心知识清单

知识点 掌握程度
全站爬虫设计思路 能说出广度优先架构和各模块职责
URL去重与规范化 能实现set去重和对URL进行基本规范化
深度控制 能限制爬取层级,避免无限爬取
自动分页处理 能识别“下一页”链接并加入队列
异常重试机制 能编写带退避的重试逻辑
数据落地与日志 能集成存储模块和日志系统
项目结构分层 能按模块组织代码,便于维护和扩展
生产环境注意点 了解robots.txt、限速、内存管理等

与前面课程的关系

课程 应用到本课
第1-3课 认知、HTTP、抓包
第4课 requests请求与异常处理
第5-7课 正则、BS4、XPath解析数据
第8课 综合静态爬虫项目结构
第9课 Cookie/Session模拟登录(全站爬虫可集成)
第10课 数据落地CSV/Excel
第11课 数据库存储
第12课 动态页面识别(全站爬虫需处理)
第13-14课 Selenium/Playwright(可集成渲染)
第15-16课 反爬对抗(加入UA轮换、代理)
第17课 验证码处理(可用于登录)
第18课 多线程加速

下一步进阶方向

  • 分布式爬虫:使用Redis作为共享队列和去重集合,多机协同
  • 增量爬取:记录上次爬取时间,只爬取新页面或更新页面
  • 聚焦爬虫:基于内容相似度或链接重要性,有选择地爬取
  • 与Scrapy框架对比:学习Scrapy,理解其组件化设计和中间件机制

八、课后作业

作业1:运行并理解全站爬虫代码(必做)

将上面提供的完整代码复制到本地,配置正确的依赖(requests, beautifulsoup4),运行爬取 http://quotes.toscrape.com/,观察输出日志和生成的CSV文件。修改MAX_DEPTH为1,重新运行,对比爬到多少数据。

作业2:实现URL规范化增强(必做)

完善normalize_url函数,支持:

  • 移除默认端口(80/443)
  • 将协议统一为https(可选开关)
  • 对查询参数进行排序,使?a=1&b=2?b=2&a=1视为相同

作业3:添加robots.txt遵守(选做)

_request_with_retry之前,使用urllib.robotparser.RobotFileParser检查目标URL是否允许爬取。需要先下载robots.txt并缓存。

作业4:为爬虫添加多线程支持(选做)

基于第18课的知识,将WebSpider改造为多线程版本。需要注意:

  • 使用queue.Queue代替deque
  • 使用threading.Lock保护seen_urlscrawled_count
  • 创建3-5个工作线程,每个线程不断从队列中取URL处理
  • 处理完成后优雅退出

作业5:定制解析器爬取真实博客(必做)

选择一个你喜欢的技术博客(如 https://www.ruanyifeng.com/blog/),编写解析函数提取每篇文章的标题、发布时间、正文前200字。注意处理分页和列表页的链接。运行全站爬虫,将数据保存到CSV。

作业6:避免爬取重复内容(选做)

有些网站不同URL可能指向相同内容(如分页第1页和?page=1)。在去重集合中,除了URL去重,还可以对页面的内容进行哈希(如提取标题和正文前200字的MD5)来避免重复内容。

作业7:思考题(必做)

如果目标网站有无限滚动加载(通过AJAX),传统全站爬虫如何应对?请结合第12课和第14课的知识,设计一种方案。

结束语:恭喜你学完了第19课。从第一课的“什么是爬虫”到现在,你已经掌握了构建一个全站通用爬虫的全部技能。这个爬虫框架不仅可以用于练习,稍加改造就能应对许多真实采集需求。全站爬虫是“授人以渔”的终极形式——不再依赖已知的URL列表,而是让程序自动发现和遍历。

最后一课(第20课)我们将讨论爬虫工程师的职业发展、法律风险规避、以及如何持续学习进阶技术。感谢你一路坚持到这里。


🔗《20节课精通网页爬虫》系列课程导航

GO


🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐