python 分布式爬虫、爬虫合规与综合实战项目

文章目录

1 分布式爬虫原理

为什么需要分布式爬虫

单机爬虫的瓶颈很明显:一台服务器的带宽、IP数量、计算能力都有限。当需要爬取海量数据时,分布式爬虫通过多台机器协同工作来突破这些限制。

适用场景:

  • 爬取任务量大(百万级URL以上)
  • 需要多IP轮换(被封IP后快速切换)
  • 对实时性要求高(新闻监控、价格监控)
  • 单机速度不够(24小时无法完成的任务)

分布式爬虫架构

复制代码
Master节点(任务分发)
    │
    ├── Redis任务队列(存储待爬URL)
    │       │
    ├───────┼─────────────────┐
    │       │                 │
Worker1   Worker2          Worker3
(爬取)    (爬取)            (爬取)
    │       │                 │
    └───────┴─────────────────┘
                    │
            共享Redis去重队列
                    │
                数据库存储

2 Scrapy-Redis分布式框架

Scrapy-Redis是最流行的分布式爬虫方案,基于Redis实现任务队列和去重。

安装

bash 复制代码
pip install scrapy-redis

需要运行Redis服务:

bash 复制代码
# Docker方式(推荐)
docker run -d -p 6379:6379 redis

# 或直接安装Redis
# Windows: 下载Redis for Windows
# Mac: brew install redis && redis-server
# Linux: apt-get install redis-server

将普通Scrapy爬虫改造为分布式

原来的普通爬虫:

python 复制代码
import scrapy

class ProductSpider(scrapy.Spider):
    name = 'product'
    start_urls = ['https://example.com/products']
    
    def parse(self, response):
        pass

改造为分布式:

python 复制代码
from scrapy_redis.spiders import RedisSpider

class ProductSpider(RedisSpider):
    name = 'product'
    redis_key = 'product:start_urls'  # Redis中存储起始URL的key
    
    def parse(self, response):
        # 提取商品数据
        for item in response.css('div.product'):
            yield {
                'title': item.css('h3::text').get(),
                'price': item.css('span.price::text').get(),
                'url': response.url
            }
        
        # 提取翻页链接(自动去重,不会重复爬取)
        next_page = response.css('a.next::attr(href)').get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)

配置文件

settings.py 中添加分布式配置:

python 复制代码
# 使用Scrapy-Redis的Scheduler
SCHEDULER = 'scrapy_redis.scheduler.Scheduler'

# 使用Scrapy-Redis的去重
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'

# Redis连接
REDIS_URL = 'redis://localhost:6379'

# 不清空Redis队列(支持断点续爬)
SCHEDULER_PERSIST = True

# 并发请求数
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 16

# 下载延时
DOWNLOAD_DELAY = 0.5

运行分布式爬虫

启动所有Worker节点(每台机器执行):

bash 复制代码
# 机器1
scrapy crawl product

# 机器2(相同命令,连接同一个Redis)
scrapy crawl product

# 机器3
scrapy crawl product

在Master节点推送起始URL:

python 复制代码
import redis

r = redis.Redis(host='master-server-ip', port=6379)

# 推送起始URL
urls = [
    'https://example.com/products?category=1',
    'https://example.com/products?category=2',
    'https://example.com/products?category=3'
]

for url in urls:
    r.lpush('product:start_urls', url)

print(f'已推送 {len(urls)} 个URL到任务队列')

断点续爬

由于 SCHEDULER_PERSIST = True,爬虫意外中断后重启会从Redis中继续未完成的任务。

python 复制代码
# 查看Redis中剩余任务数
import redis
r = redis.Redis(host='localhost', port=6379)
print(f'待爬URL数: {r.llen("product:start_urls")}')
print(f'已去重URL数: {r.scard("product:dupefilter")}')

3 并发爬虫进阶

asyncio + aiohttp 异步爬虫

对于不需要Scrapy的轻量场景,asyncio+aiohttp是高效的选择。

bash 复制代码
pip install aiohttp
python 复制代码
import asyncio
import aiohttp
from bs4 import BeautifulSoup
import json
from datetime import datetime

async def fetch(session, url):
    """异步获取页面"""
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
    }
    try:
        async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as response:
            if response.status == 200:
                return await response.text()
            return None
    except Exception as e:
        print(f'请求失败 {url}: {e}')
        return None

async def parse_page(html, url):
    """解析页面(同步操作)"""
    if not html:
        return []
    
    soup = BeautifulSoup(html, 'lxml')
    items = []
    for div in soup.select('div.product'):
        items.append({
            'title': div.select_one('h3').text.strip() if div.select_one('h3') else '',
            'price': div.select_one('span.price').text.strip() if div.select_one('span.price') else '',
            'url': url
        })
    return items

async def crawl_all(urls, concurrency=10):
    """并发爬取多个URL"""
    semaphore = asyncio.Semaphore(concurrency)  # 限制并发数
    all_items = []
    
    async def fetch_with_limit(session, url):
        async with semaphore:
            html = await fetch(session, url)
            items = await asyncio.get_event_loop().run_in_executor(
                None, parse_page, html, url  # 在线程池中执行同步解析
            )
            return items
    
    connector = aiohttp.TCPConnector(limit=concurrency)
    async with aiohttp.ClientSession(connector=connector) as session:
        tasks = [fetch_with_limit(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
    
    for items in results:
        all_items.extend(items)
    
    return all_items

# 使用
urls = [f'https://example.com/products?page={i}' for i in range(1, 51)]
items = asyncio.run(crawl_all(urls, concurrency=10))
print(f'共爬取 {len(items)} 条数据')

4 爬虫合规规范(必读)

在学习爬虫技术的同时,必须了解法律边界和合规要求。这不是可选项。

robots.txt规范

每个网站都可以通过 robots.txt 文件声明爬虫规则。

复制代码
# 示例 robots.txt
User-agent: *          # 适用于所有爬虫
Disallow: /admin/      # 禁止爬取/admin/目录
Disallow: /user/       # 禁止爬取用户数据
Crawl-delay: 1         # 建议爬取间隔1秒

User-agent: Googlebot  # 专门针对Google爬虫
Allow: /               # 允许爬取所有内容

用Python检查robots.txt:

python 复制代码
from urllib.robotparser import RobotFileParser

def can_fetch(url, user_agent='*'):
    """检查是否允许爬取该URL"""
    from urllib.parse import urlparse
    parsed = urlparse(url)
    robots_url = f'{parsed.scheme}://{parsed.netloc}/robots.txt'
    
    rp = RobotFileParser()
    rp.set_url(robots_url)
    try:
        rp.read()
        return rp.can_fetch(user_agent, url)
    except:
        return True  # 无法读取则默认允许

# 使用
url = 'https://www.example.com/products'
if can_fetch(url):
    print('允许爬取')
else:
    print('robots.txt禁止爬取该URL')

Scrapy中默认遵守robots.txt(需确认 settings.pyROBOTSTXT_OBEY = True)。

合法爬虫边界

可以爬取:

  • 公开展示的新闻、博客、商品信息
  • 统计、研究目的的公开数据
  • 网站明确授权的数据

禁止爬取:

  • 用户隐私数据(手机号、身份证、地址)
  • 需要付费才能查看的内容
  • 账号密码等安全信息
  • 网站明确禁止的内容(robots.txt标注的)

行为准则:

  • 控制请求频率,不攻击服务器
  • 不爬取后用于商业牟利侵权
  • 发现数据泄露风险及时停止

近年相关法规参考

  • 《数据安全法》:对数据处理活动进行规范
  • 《个人信息保护法》:保护个人信息不被滥用
  • 《网络安全法》:保护网络基础设施安全

爬虫本身不违法,但爬取隐私数据、攻击服务器、侵权商用等行为可能触犯法律。

5 综合实战项目

项目一:豆瓣电影数据分析系统

功能:爬取豆瓣电影Top250数据,进行统计分析和可视化。

python 复制代码
# 数据采集部分(完整版)
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
import random

def crawl_douban_movies():
    movies = []
    base_url = 'https://movie.douban.com/top250'
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }
    
    for start in range(0, 250, 25):
        url = f'{base_url}?start={start}'
        response = requests.get(url, headers=headers, timeout=10)
        soup = BeautifulSoup(response.text, 'lxml')
        
        for item in soup.select('div.item'):
            movies.append({
                'rank': item.select_one('em').text,
                'title': item.select_one('span.title').text,
                'rating': item.select_one('span.rating_num').text,
                'votes': item.select_one('span[property="v:votes"]').text,
                'year': item.select_one('span.inq').text.strip() if item.select_one('span.inq') else ''
            })
        
        time.sleep(random.uniform(1, 2))
    
    return pd.DataFrame(movies)

# 数据分析部分
def analyze_movies(df):
    df['rating'] = pd.to_numeric(df['rating'])
    df['votes'] = pd.to_numeric(df['votes'])
    
    print(f'平均评分: {df["rating"].mean():.2f}')
    print(f'最高评分: {df["rating"].max()} - {df.loc[df["rating"].idxmax(), "title"]}')
    print(f'最多投票: {df["votes"].max()} - {df.loc[df["votes"].idxmax(), "title"]}')
    
    # 评分区间分布
    bins = [8.0, 8.5, 9.0, 9.5, 10.0]
    labels = ['8.0-8.5', '8.5-9.0', '9.0-9.5', '9.5+']
    df['rating_range'] = pd.cut(df['rating'], bins=bins, labels=labels)
    print('\n评分分布:')
    print(df['rating_range'].value_counts())
    
    return df

# 运行
# df = crawl_douban_movies()
# df.to_csv('movies.csv', encoding='utf-8-sig', index=False)
# analyze_movies(df)

项目二:电商价格监控系统

python 复制代码
import requests
import json
from datetime import datetime
import sqlite3

class PriceMonitor:
    def __init__(self, db_path='prices.db'):
        self.conn = sqlite3.connect(db_path)
        self._init_db()
    
    def _init_db(self):
        """初始化数据库"""
        self.conn.execute('''
        CREATE TABLE IF NOT EXISTS price_history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            product_id TEXT,
            product_name TEXT,
            price REAL,
            original_price REAL,
            discount REAL,
            record_time TEXT
        )
        ''')
        self.conn.commit()
    
    def fetch_price(self, product_id):
        """获取商品价格(需根据具体网站调整)"""
        headers = {'User-Agent': 'Mozilla/5.0 ...'}
        url = f'https://api.example.com/product/{product_id}'
        response = requests.get(url, headers=headers, timeout=10)
        data = response.json()
        
        return {
            'product_id': product_id,
            'name': data.get('name', ''),
            'price': data.get('current_price', 0),
            'original_price': data.get('original_price', 0),
        }
    
    def record_price(self, product_data):
        """记录价格到数据库"""
        price = product_data['price']
        original = product_data['original_price']
        discount = round((1 - price / original) * 100, 1) if original > 0 else 0
        
        self.conn.execute('''
        INSERT INTO price_history (product_id, product_name, price, original_price, discount, record_time)
        VALUES (?, ?, ?, ?, ?, ?)
        ''', (
            product_data['product_id'],
            product_data['name'],
            price, original, discount,
            datetime.now().isoformat()
        ))
        self.conn.commit()
    
    def check_price_drop(self, product_id, threshold=10):
        """检查价格是否大幅下降"""
        cursor = self.conn.execute('''
        SELECT price FROM price_history 
        WHERE product_id = ? 
        ORDER BY record_time DESC 
        LIMIT 10
        ''', (product_id,))
        
        prices = [row[0] for row in cursor.fetchall()]
        if len(prices) < 2:
            return False
        
        latest = prices[0]
        avg_recent = sum(prices[1:]) / len(prices[1:])
        drop_percent = (avg_recent - latest) / avg_recent * 100
        
        if drop_percent >= threshold:
            print(f'价格预警!商品 {product_id} 价格下降 {drop_percent:.1f}%')
            return True
        return False

项目三:定时数据采集脚本

python 复制代码
import schedule
import time

def run_crawler():
    """定时爬取任务"""
    print(f'[{datetime.now()}] 开始爬取...')
    # 执行爬虫逻辑
    # ...
    print(f'[{datetime.now()}] 爬取完成')

# 每天早上9点运行
schedule.every().day.at("09:00").do(run_crawler)

# 每小时运行一次
schedule.every().hour.do(run_crawler)

# 每30分钟运行一次
schedule.every(30).minutes.do(run_crawler)

while True:
    schedule.run_pending()
    time.sleep(60)  # 每分钟检查一次

Linux服务器部署定时任务(crontab):

bash 复制代码
# 编辑crontab
crontab -e

# 每天早上8点运行爬虫
0 8 * * * /usr/bin/python3 /home/user/spider/main.py >> /home/user/spider/logs/cron.log 2>&1

# 查看任务列表
crontab -l

6 学习路线总结

学完这套教程,你应该掌握了:

基础层

  • Python语法、数据结构、文件操作
  • HTTP协议、网络请求、浏览器抓包
  • HTML/CSS/XPath数据定位

技术层

  • requests/urllib 发起请求
  • BeautifulSoup/lxml/json 解析数据
  • Selenium/Playwright 处理动态页面
  • 接口逆向、参数分析

进阶层

  • 反爬对抗:UA池、代理池、验证码识别
  • Scrapy工程化框架
  • Scrapy-Redis分布式爬虫
  • asyncio并发爬虫

实战层

  • 完整项目开发流程
  • 数据库存储(MySQL/MongoDB)
  • 定时任务与服务器部署
  • 数据分析与可视化

持续提升方向:

  1. 深入JS逆向(扣代码、AST反混淆)
  2. 安卓APP逆向(爬取手机App数据)
  3. 自动化测试(爬虫质量保证)
  4. 数据工程(ETL、数据仓库)

爬虫是工具,数据是目的。学好爬虫技术的同时,要持续学习数据分析、可视化、机器学习,才能真正发挥爬取数据的价值。

相关推荐
审小匠OpenCPAi1 小时前
银行流水核查怎么自动化?单边匹配、双向勾稽与图聚类异常检测的工程对比
java·前端·人工智能·python·审计
元Y亨H2 小时前
Pandas 解析 Excel 导致的内存溢出(MemoryError)
python·excel
金銀銅鐵3 小时前
[Python] 用 turtle 来绘制瑞典国旗
python
Albert Edison3 小时前
【GUI 自动化测试】Pywinauto 常见操作
自动化测试·python·pywinauto
海兰3 小时前
【高速缓存】RedisVL为文本生成嵌入向量实践指南
人工智能·python·机器学习
2601_955760073 小时前
如何用 Claude Opus 5 API 批量扩展长尾关键词和文章选题
前端·python·搜索引擎
艾斯特_4 小时前
从模型调用到可用聊天应用:会话状态与消息协议
python·ai·aigc
Marst Code4 小时前
Python 3.9 已停止维护!从 3.9 到 3.14 全版本深度对比,生产环境该选哪个?
开发语言·python
llwszx4 小时前
【Java/Go后端手撸原生Agent(第九篇):Plan-and-Execute规划模式——从“走一步看一步“到“先谋后动“】
java·python·golang·agent开发·plan模式·规划执行模式