第19课:网页爬虫|全站通用爬虫【从单页到全站的架构实战】

文章目录
学习目标
学完这一课,你将能够:
- 理解全站爬虫的核心设计思想——知道如何从一个入口页面出发,自动发现、遍历并采集整个网站或某个板块的所有数据
- 掌握URL去重与爬取队列管理——用集合实现高性能去重,用队列控制爬取顺序和广度优先策略
- 实现自动分页和循环翻页——自动识别“下一页”链接,无需手动构造分页URL
- 控制爬取深度与范围——限制爬取层级,避免爬出目标域名,防止陷入无限链接黑洞
- 集成异常重试与日志系统——让爬虫在遇到网络波动时自动重试,并用日志记录每一步运行状态
- 开发完整的全站爬虫项目——将前面18课的知识(请求、解析、存储、多线程等)整合成一个可复用的通用爬虫框架
- 建立良好的工程化思维——代码分层、配置分离、面向接口设计,后续可轻松扩展
这一课是整个专栏的“集大成者”,你会看到之前学过的所有技术点如何有机组合,形成一个真正的生产级爬虫项目。
一、通俗原理:全站爬虫就像扫描整栋大楼
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/pagehttp://example.com/page?utm_source=googlehttps://example.com/pagehttp://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 数据流设计
- 初始化:配置日志、创建存储对象、初始化URL队列和去重集合。
- 添加种子URL(入口)。种子URL深度为0。
- 主循环:当队列不为空且已爬取页面数未达上限时:
- 从队列取出一个URL(先进先出,广度优先)
- 标记为“已处理”,记录深度
- 下载页面内容(带重试)
- 解析页面:提取业务数据(调用解析回调函数)、提取新URL
- 对每个新URL:规范化、去重、未超深度、域名符合 → 加入队列
- 存储提取到的业务数据
- 增加爬取计数,控制请求频率
- 循环结束,输出统计信息。
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,你会看到爬虫从首页出发:
- 爬取首页(深度0),提取名言数据和下一页链接(/page/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=2 和 http://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_urls和crawled_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节课精通网页爬虫》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)