[Python教程系列-13] Web爬虫开发:数据采集与处理的艺术

40 阅读19分钟

引言

在当今信息时代,互联网上存在着海量的数据资源,这些数据对于商业分析、市场研究、学术研究等领域具有重要价值。然而,这些数据往往分散在不同的网站上,以非结构化的形式存在,手动收集和整理这些数据既费时又容易出错。Web爬虫(Web Spider)作为一种自动化数据采集工具,能够帮助我们高效地从网页中提取所需信息,并将其转换为结构化数据供进一步分析使用。

Python凭借其简洁的语法、丰富的第三方库和强大的生态系统,成为了Web爬虫开发的首选语言。从简单的网页请求到复杂的动态网页处理,从数据提取到存储,Python都能提供相应的解决方案。requests、BeautifulSoup、Scrapy等库使得开发Web爬虫变得更加简单和高效。

在前面的章节中,我们学习了Python的基础语法、数据结构、面向对象编程、文件操作、标准库使用、环境管理、正则表达式、单元测试以及自动化脚本开发等内容。本章将结合这些知识,深入学习Web爬虫开发的核心技术和实践方法,帮助你掌握从网页中提取数据的技能。

学习目标

完成本章学习后,你将能够:

  • 理解Web爬虫的基本概念和工作原理
  • 掌握HTTP协议和网页请求的基础知识
  • 学会使用requests库发送HTTP请求
  • 掌握BeautifulSoup库解析HTML文档
  • 了解XPath和CSS选择器的使用方法
  • 学会处理动态网页和JavaScript渲染内容
  • 掌握数据存储和导出的方法
  • 了解爬虫的法律和道德规范
  • 学会处理反爬虫机制

核心知识点讲解

1. Web爬虫概述

Web爬虫是一种自动化程序,它能够模拟人类浏览器的行为,向Web服务器发送请求,获取网页内容,并从中提取所需的数据。一个典型的爬虫工作流程包括:

爬虫工作流程:

  1. 发送请求:向目标网站发送HTTP请求
  2. 接收响应:获取服务器返回的HTML内容
  3. 解析内容:从HTML中提取所需数据
  4. 数据存储:将提取的数据保存到文件或数据库
  5. 循环执行:重复上述过程直到完成所有任务

爬虫类型:

  • 通用爬虫:如搜索引擎爬虫,爬取整个互联网
  • 聚焦爬虫:针对特定主题或网站的爬虫
  • 增量爬虫:只爬取新增或更新的内容
  • 深层爬虫:能够处理JavaScript渲染的动态内容

2. HTTP协议基础

HTTP(HyperText Transfer Protocol)是Web浏览器和服务器之间通信的基础协议。理解HTTP协议对于开发Web爬虫至关重要。

HTTP请求方法:

  • GET:请求指定资源
  • POST:向指定资源提交数据
  • PUT:更新指定资源
  • DELETE:删除指定资源

HTTP状态码:

  • 2xx:成功响应(200 OK, 201 Created等)
  • 3xx:重定向(301 Moved Permanently, 302 Found等)
  • 4xx:客户端错误(404 Not Found, 403 Forbidden等)
  • 5xx:服务器错误(500 Internal Server Error, 503 Service Unavailable等)

3. requests库详解

requests是Python中最受欢迎的HTTP库,它简化了HTTP请求的发送过程,提供了简洁易用的API。

主要功能:

  • 发送各种HTTP请求:GET、POST、PUT、DELETE等
  • 处理请求头和Cookie:自定义请求头,管理会话状态
  • 处理响应内容:文本、JSON、二进制数据等
  • 会话管理:保持连接和Cookie状态
  • 超时和重试:处理网络异常情况

4. HTML解析与数据提取

获取网页内容后,需要从中提取所需的数据。Python提供了多种HTML解析和数据提取的方法。

BeautifulSoup库:

  • HTML解析:将HTML文档转换为树形结构
  • 元素查找:通过标签名、属性、CSS选择器等方式查找元素
  • 数据提取:获取元素的文本内容、属性值等
  • 文档遍历:在HTML树中导航和遍历

XPath和CSS选择器:

  • XPath:XML路径语言,用于在XML文档中查找节点
  • CSS选择器:用于选择HTML元素的模式

5. 动态网页处理

现代Web应用大量使用JavaScript来动态生成内容,传统的HTML解析方法无法处理这类动态内容。

处理方法:

  • Selenium:浏览器自动化工具,能够执行JavaScript
  • Playwright:现代化的浏览器自动化库
  • Pyppeteer:Python版的Puppeteer,控制Chrome/Chromium

6. 数据存储与导出

爬取的数据需要妥善存储和管理,以便后续分析和使用。

存储方式:

  • 文件存储:CSV、JSON、Excel等格式
  • 数据库存储:SQLite、MySQL、MongoDB等
  • 云存储:AWS S3、Google Cloud Storage等

7. 反爬虫机制与应对

为了保护网站资源,许多网站采用了反爬虫机制,爬虫开发者需要了解这些机制并采取相应的应对措施。

常见反爬虫机制:

  • User-Agent检测:检查请求头中的User-Agent
  • IP限制:限制同一IP的请求频率
  • 验证码:要求用户输入验证码
  • JavaScript混淆:通过JavaScript动态生成内容
  • 蜜罐陷阱:设置虚假链接诱捕爬虫

应对策略:

  • 设置合理的请求头:模拟真实浏览器
  • 控制请求频率:添加延时,避免过于频繁的请求
  • 使用代理IP:轮换IP地址
  • 处理验证码:使用OCR或打码平台
  • 执行JavaScript:使用浏览器自动化工具

8. 法律与道德规范

Web爬虫开发需要遵守相关法律法规和道德规范,尊重网站的权益和用户隐私。

注意事项:

  • 遵守robots.txt:查看并遵守网站的爬虫协议
  • 尊重版权:不侵犯网站的知识产权
  • 保护隐私:不收集和传播个人隐私信息
  • 合理使用:避免对网站造成过大负担

代码示例与实战

实战1:基础爬虫开发

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
基础Web爬虫示例
爬取静态网页内容并提取数据
"""

import requests
from bs4 import BeautifulSoup
import csv
import json
import time
import logging
from urllib.parse import urljoin, urlparse
import argparse

class BasicSpider:
    """基础爬虫类"""
    
    def __init__(self, base_url, headers=None):
        self.base_url = base_url
        self.session = requests.Session()
        
        # 设置默认请求头
        default_headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 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.8,en-US;q=0.5,en;q=0.3',
            'Accept-Encoding': 'gzip, deflate',
            'Connection': 'keep-alive',
        }
        
        if headers:
            default_headers.update(headers)
        
        self.session.headers.update(default_headers)
        self.setup_logging()
    
    def setup_logging(self):
        """设置日志"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('spider.log'),
                logging.StreamHandler()
            ]
        )
        self.logger = logging.getLogger(__name__)
    
    def get_page(self, url, timeout=10):
        """获取网页内容"""
        try:
            response = self.session.get(url, timeout=timeout)
            response.raise_for_status()  # 检查HTTP错误
            response.encoding = response.apparent_encoding  # 自动检测编码
            return response
        except requests.RequestException as e:
            self.logger.error(f"请求失败 {url}: {e}")
            return None
    
    def parse_news_list(self, html_content, base_url):
        """解析新闻列表页面"""
        soup = BeautifulSoup(html_content, 'html.parser')
        news_items = []
        
        # 查找新闻条目(这里以常见的新闻网站结构为例)
        # 实际使用时需要根据目标网站的具体结构调整选择器
        articles = soup.find_all('div', class_='news-item')
        
        for article in articles:
            try:
                # 提取标题
                title_elem = article.find('h3') or article.find('a')
                title = title_elem.get_text(strip=True) if title_elem else "无标题"
                
                # 提取链接
                link_elem = article.find('a')
                link = link_elem.get('href') if link_elem else ""
                if link:
                    link = urljoin(base_url, link)  # 转换为绝对URL
                
                # 提取摘要
                summary_elem = article.find('p', class_='summary')
                summary = summary_elem.get_text(strip=True) if summary_elem else ""
                
                # 提取发布时间
                time_elem = article.find('span', class_='time')
                publish_time = time_elem.get_text(strip=True) if time_elem else ""
                
                news_item = {
                    'title': title,
                    'link': link,
                    'summary': summary,
                    'publish_time': publish_time
                }
                
                news_items.append(news_item)
                
            except Exception as e:
                self.logger.error(f"解析新闻条目失败: {e}")
                continue
        
        return news_items
    
    def parse_news_detail(self, html_content):
        """解析新闻详情页面"""
        soup = BeautifulSoup(html_content, 'html.parser')
        
        # 提取标题
        title_elem = soup.find('h1') or soup.find('title')
        title = title_elem.get_text(strip=True) if title_elem else ""
        
        # 提取正文内容
        content_elem = soup.find('div', class_='content') or soup.find('article')
        content = content_elem.get_text(strip=True) if content_elem else ""
        
        # 提取作者
        author_elem = soup.find('span', class_='author') or soup.find('meta', attrs={'name': 'author'})
        author = ""
        if author_elem:
            if author_elem.name == 'meta':
                author = author_elem.get('content', '')
            else:
                author = author_elem.get_text(strip=True)
        
        # 提取发布时间
        time_elem = soup.find('time') or soup.find('span', class_='publish-time')
        publish_time = time_elem.get_text(strip=True) if time_elem else ""
        
        return {
            'title': title,
            'content': content,
            'author': author,
            'publish_time': publish_time
        }
    
    def crawl_news(self, list_urls, output_file='news_data.csv'):
        """爬取新闻数据"""
        all_news = []
        
        # 爬取列表页面
        for url in list_urls:
            self.logger.info(f"正在爬取列表页: {url}")
            response = self.get_page(url)
            
            if response:
                news_items = self.parse_news_list(response.text, url)
                all_news.extend(news_items)
                
                # 添加延时,避免请求过于频繁
                time.sleep(1)
        
        # 爬取详情页面
        detailed_news = []
        for news_item in all_news:
            if news_item['link']:
                self.logger.info(f"正在爬取详情页: {news_item['link']}")
                response = self.get_page(news_item['link'])
                
                if response:
                    detail = self.parse_news_detail(response.text)
                    # 合并列表页和详情页信息
                    full_news = {**news_item, **detail}
                    detailed_news.append(full_news)
                    
                    # 添加延时
                    time.sleep(1)
        
        # 保存数据
        self.save_to_csv(detailed_news, output_file)
        self.save_to_json(detailed_news, output_file.replace('.csv', '.json'))
        
        self.logger.info(f"爬取完成,共获取 {len(detailed_news)} 条新闻")
        return detailed_news
    
    def save_to_csv(self, data, filename):
        """保存数据到CSV文件"""
        if not data:
            return
        
        fieldnames = data[0].keys()
        
        with open(filename, 'w', newline='', encoding='utf-8-sig') as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(data)
        
        self.logger.info(f"数据已保存到 CSV 文件: {filename}")
    
    def save_to_json(self, data, filename):
        """保存数据到JSON文件"""
        with open(filename, 'w', encoding='utf-8') as jsonfile:
            json.dump(data, jsonfile, ensure_ascii=False, indent=2)
        
        self.logger.info(f"数据已保存到 JSON 文件: {filename}")

# 使用示例
def main():
    """主函数"""
    parser = argparse.ArgumentParser(description='基础新闻爬虫')
    parser.add_argument('--urls', nargs='+', required=True,
                       help='新闻列表页面URL')
    parser.add_argument('--output', default='news_data.csv',
                       help='输出文件名')
    
    args = parser.parse_args()
    
    # 创建爬虫实例
    spider = BasicSpider("https://example-news-site.com")
    
    # 爬取数据
    news_data = spider.crawl_news(args.urls, args.output)
    
    # 显示前几条数据
    print(f"爬取到 {len(news_data)} 条新闻")
    for i, news in enumerate(news_data[:3]):
        print(f"\n新闻 {i+1}:")
        print(f"标题: {news.get('title', 'N/A')}")
        print(f"链接: {news.get('link', 'N/A')}")
        print(f"摘要: {news.get('summary', 'N/A')[:100]}...")

if __name__ == '__main__':
    main()

实战2:处理动态网页的爬虫

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
动态网页爬虫示例
使用Selenium处理JavaScript渲染的内容
"""

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
import time
import csv
import logging
from datetime import datetime

class DynamicSpider:
    """动态网页爬虫类"""
    
    def __init__(self, headless=True):
        self.setup_logging()
        self.driver = self.setup_driver(headless)
    
    def setup_logging(self):
        """设置日志"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('dynamic_spider.log'),
                logging.StreamHandler()
            ]
        )
        self.logger = logging.getLogger(__name__)
    
    def setup_driver(self, headless=True):
        """设置WebDriver"""
        chrome_options = Options()
        
        if headless:
            chrome_options.add_argument('--headless')  # 无头模式
        
        chrome_options.add_argument('--no-sandbox')
        chrome_options.add_argument('--disable-dev-shm-usage')
        chrome_options.add_argument('--disable-gpu')
        chrome_options.add_argument('--window-size=1920,1080')
        chrome_options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36')
        
        try:
            driver = webdriver.Chrome(options=chrome_options)
            self.logger.info("Chrome WebDriver 初始化成功")
            return driver
        except Exception as e:
            self.logger.error(f"Chrome WebDriver 初始化失败: {e}")
            raise
    
    def wait_for_element(self, locator, timeout=10):
        """等待元素出现"""
        try:
            element = WebDriverWait(self.driver, timeout).until(
                EC.presence_of_element_located(locator)
            )
            return element
        except Exception as e:
            self.logger.error(f"等待元素超时: {locator}")
            return None
    
    def scroll_to_bottom(self):
        """滚动到页面底部"""
        last_height = self.driver.execute_script("return document.body.scrollHeight")
        
        while True:
            # 滚动到页面底部
            self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            
            # 等待新内容加载
            time.sleep(2)
            
            # 计算新的滚动高度
            new_height = self.driver.execute_script("return document.body.scrollHeight")
            
            # 如果高度没有变化,说明已经到底部
            if new_height == last_height:
                break
            
            last_height = new_height
    
    def parse_product_list(self):
        """解析商品列表"""
        soup = BeautifulSoup(self.driver.page_source, 'html.parser')
        products = []
        
        # 查找商品元素(需要根据实际网站结构调整)
        product_elements = soup.find_all('div', class_='product-item')
        
        for product_elem in product_elements:
            try:
                # 提取商品信息
                name_elem = product_elem.find('h3', class_='product-name')
                name = name_elem.get_text(strip=True) if name_elem else "未知商品"
                
                price_elem = product_elem.find('span', class_='price')
                price = price_elem.get_text(strip=True) if price_elem else "价格未知"
                
                # 移除价格中的非数字字符(如¥符号)
                import re
                price_number = re.search(r'[\d.]+', price)
                price_value = float(price_number.group()) if price_number else 0.0
                
                rating_elem = product_elem.find('div', class_='rating')
                rating = rating_elem.get_text(strip=True) if rating_elem else "暂无评分"
                
                product = {
                    'name': name,
                    'price': price_value,
                    'rating': rating,
                    'crawl_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                }
                
                products.append(product)
                
            except Exception as e:
                self.logger.error(f"解析商品信息失败: {e}")
                continue
        
        return products
    
    def crawl_ecommerce_products(self, url, max_pages=5):
        """爬取电商平台商品数据"""
        self.logger.info(f"开始爬取电商网站: {url}")
        
        try:
            # 访问目标页面
            self.driver.get(url)
            
            # 等待页面加载完成
            self.wait_for_element((By.CLASS_NAME, "product-list"))
            
            all_products = []
            
            # 爬取多页数据
            for page in range(1, max_pages + 1):
                self.logger.info(f"正在爬取第 {page} 页")
                
                # 解析当前页面的商品数据
                products = self.parse_product_list()
                all_products.extend(products)
                
                self.logger.info(f"第 {page} 页获取到 {len(products)} 个商品")
                
                # 尝试点击下一页按钮
                try:
                    next_button = self.driver.find_element(By.CLASS_NAME, "next-page")
                    if next_button.is_enabled():
                        next_button.click()
                        time.sleep(2)  # 等待页面加载
                    else:
                        self.logger.info("已经是最后一页")
                        break
                except Exception as e:
                    self.logger.info("没有找到下一页按钮,可能是最后一页")
                    break
            
            return all_products
            
        except Exception as e:
            self.logger.error(f"爬取过程中发生错误: {e}")
            return []
    
    def crawl_infinite_scroll(self, url, scroll_times=3):
        """爬取无限滚动页面"""
        self.logger.info(f"开始爬取无限滚动页面: {url}")
        
        try:
            # 访问目标页面
            self.driver.get(url)
            
            # 等待初始内容加载
            self.wait_for_element((By.CLASS_NAME, "content"))
            
            # 模拟滚动加载更多内容
            for i in range(scroll_times):
                self.logger.info(f"第 {i+1} 次滚动加载")
                self.scroll_to_bottom()
                time.sleep(2)  # 等待新内容加载
            
            # 解析所有加载的内容
            products = self.parse_product_list()
            return products
            
        except Exception as e:
            self.logger.error(f"爬取无限滚动页面时发生错误: {e}")
            return []
    
    def save_to_csv(self, data, filename):
        """保存数据到CSV文件"""
        if not data:
            return
        
        fieldnames = data[0].keys()
        
        with open(filename, 'w', newline='', encoding='utf-8-sig') as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(data)
        
        self.logger.info(f"数据已保存到 CSV 文件: {filename}")
    
    def close(self):
        """关闭浏览器"""
        if self.driver:
            self.driver.quit()
            self.logger.info("浏览器已关闭")

# 使用示例
def main():
    """主函数"""
    spider = None
    
    try:
        # 创建爬虫实例(非无头模式,可以看到浏览器操作)
        spider = DynamicSpider(headless=False)
        
        # 爬取电商网站商品数据
        ecommerce_url = "https://example-ecommerce-site.com/products"
        products = spider.crawl_ecommerce_products(ecommerce_url, max_pages=3)
        
        # 保存数据
        if products:
            spider.save_to_csv(products, 'products.csv')
            print(f"成功爬取 {len(products)} 个商品")
            
            # 显示前几个商品信息
            for i, product in enumerate(products[:5]):
                print(f"\n商品 {i+1}:")
                print(f"  名称: {product['name']}")
                print(f"  价格: ¥{product['price']}")
                print(f"  评分: {product['rating']}")
        else:
            print("未获取到商品数据")
        
    except Exception as e:
        print(f"爬取过程中发生错误: {e}")
    
    finally:
        # 确保关闭浏览器
        if spider:
            spider.close()

if __name__ == '__main__':
    main()

实战3:高级爬虫框架 - Scrapy

# scrapy_spider/spiders/news_spider.py
"""
Scrapy新闻爬虫示例
"""

import scrapy
from scrapy.http import Request
from urllib.parse import urljoin
import re

class NewsSpider(scrapy.Spider):
    name = 'news'
    allowed_domains = ['example-news-site.com']
    start_urls = ['https://example-news-site.com/news']
    
    custom_settings = {
        'DOWNLOAD_DELAY': 1,  # 下载延迟1秒
        'RANDOMIZE_DOWNLOAD_DELAY': 0.5,  # 随机延迟
        'CONCURRENT_REQUESTS': 16,  # 并发请求数
        'CONCURRENT_REQUESTS_PER_DOMAIN': 8,  # 每个域名的并发请求数
        'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }
    
    def parse(self, response):
        """解析新闻列表页面"""
        # 提取新闻链接
        news_links = response.css('div.news-item a::attr(href)').getall()
        
        for link in news_links:
            absolute_url = urljoin(response.url, link)
            yield Request(absolute_url, callback=self.parse_news_detail)
        
        # 处理分页
        next_page = response.css('a.next-page::attr(href)').get()
        if next_page:
            next_url = urljoin(response.url, next_page)
            yield Request(next_url, callback=self.parse)
    
    def parse_news_detail(self, response):
        """解析新闻详情页面"""
        yield {
            'title': response.css('h1.title::text').get(default='').strip(),
            'content': ' '.join(response.css('div.content p::text').getall()),
            'author': response.css('span.author::text').get(default='').strip(),
            'publish_time': response.css('time.publish-time::text').get(default='').strip(),
            'url': response.url,
            'tags': response.css('div.tags a::text').getall()
        }

# scrapy_spider/items.py
"""
定义爬取的数据结构
"""

import scrapy

class NewsItem(scrapy.Item):
    title = scrapy.Field()
    content = scrapy.Field()
    author = scrapy.Field()
    publish_time = scrapy.Field()
    url = scrapy.Field()
    tags = scrapy.Field()

# scrapy_spider/pipelines.py
"""
数据处理管道
"""

import sqlite3
import json
from itemadapter import ItemAdapter

class NewsPipeline:
    def __init__(self):
        self.conn = None
        self.cursor = None
    
    def open_spider(self, spider):
        """爬虫开始时调用"""
        self.conn = sqlite3.connect('news.db')
        self.cursor = self.conn.cursor()
        
        # 创建表
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS news (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT,
                content TEXT,
                author TEXT,
                publish_time TEXT,
                url TEXT UNIQUE,
                tags TEXT
            )
        ''')
        self.conn.commit()
    
    def close_spider(self, spider):
        """爬虫结束时调用"""
        if self.conn:
            self.conn.close()
    
    def process_item(self, item, spider):
        """处理每个爬取的项目"""
        adapter = ItemAdapter(item)
        
        # 插入数据库
        try:
            self.cursor.execute('''
                INSERT OR IGNORE INTO news 
                (title, content, author, publish_time, url, tags)
                VALUES (?, ?, ?, ?, ?, ?)
            ''', (
                adapter.get('title', ''),
                adapter.get('content', ''),
                adapter.get('author', ''),
                adapter.get('publish_time', ''),
                adapter.get('url', ''),
                json.dumps(adapter.get('tags', []), ensure_ascii=False)
            ))
            self.conn.commit()
        except Exception as e:
            spider.logger.error(f"数据库插入失败: {e}")
        
        return item

class JsonWriterPipeline:
    def open_spider(self, spider):
        self.file = open('news.json', 'w', encoding='utf-8')
        self.file.write('[\n')
        self.first_item = True
    
    def close_spider(self, spider):
        self.file.write('\n]')
        self.file.close()
    
    def process_item(self, item, spider):
        if not self.first_item:
            self.file.write(',\n')
        else:
            self.first_item = False
        
        line = json.dumps(ItemAdapter(item).asdict(), ensure_ascii=False, indent=2)
        self.file.write(line)
        return item

# scrapy_spider/settings.py
"""
Scrapy配置文件
"""

BOT_NAME = 'scrapy_spider'

SPIDER_MODULES = ['scrapy_spider.spiders']
NEWSPIDER_MODULE = 'scrapy_spider.spiders'

# 遵守robots.txt规则
ROBOTSTXT_OBEY = True

# 配置管道
ITEM_PIPELINES = {
    'scrapy_spider.pipelines.NewsPipeline': 300,
    'scrapy_spider.pipelines.JsonWriterPipeline': 400,
}

# 下载延迟
DOWNLOAD_DELAY = 1
RANDOMIZE_DOWNLOAD_DELAY = 0.5

# 并发设置
CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 8

# 用户代理
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'

# 中间件
DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': 90,
}

# 日志级别
LOG_LEVEL = 'INFO'

# scrapy_spider/middlewares.py
"""
自定义中间件
"""

import random
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware

class RotateUserAgentMiddleware(UserAgentMiddleware):
    """轮换User-Agent中间件"""
    
    def __init__(self, user_agent=''):
        self.user_agent = user_agent
        self.user_agent_list = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0',
        ]
    
    def process_request(self, request, spider):
        ua = random.choice(self.user_agent_list)
        request.headers.setdefault('User-Agent', ua)

# scrapy_spider/run_spider.py
"""
运行爬虫的脚本
"""

from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings
from scrapy_spider.spiders.news_spider import NewsSpider

def run_spider():
    """运行新闻爬虫"""
    # 获取Scrapy设置
    settings = get_project_settings()
    
    # 创建爬虫进程
    process = CrawlerProcess(settings)
    
    # 添加爬虫
    process.crawl(NewsSpider)
    
    # 启动爬虫
    process.start()

if __name__ == '__main__':
    run_spider()

实战4:反爬虫对策和代理使用

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
反爬虫对策和代理使用示例
"""

import requests
import time
import random
from urllib.parse import urljoin
import logging
from fake_useragent import UserAgent
import json

class AntiAntiSpider:
    """反反爬虫爬虫类"""
    
    def __init__(self):
        self.session = requests.Session()
        self.ua = UserAgent()
        self.proxy_pool = self.load_proxy_pool()
        self.setup_logging()
    
    def setup_logging(self):
        """设置日志"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('anti_spider.log'),
                logging.StreamHandler()
            ]
        )
        self.logger = logging.getLogger(__name__)
    
    def load_proxy_pool(self):
        """加载代理池(示例数据)"""
        # 实际使用时可以从代理服务商API获取
        return [
            {'http': 'http://proxy1.example.com:8080'},
            {'http': 'http://proxy2.example.com:8080'},
            {'http': 'http://proxy3.example.com:8080'},
        ]
    
    def get_random_proxy(self):
        """获取随机代理"""
        if self.proxy_pool:
            return random.choice(self.proxy_pool)
        return None
    
    def get_random_user_agent(self):
        """获取随机User-Agent"""
        try:
            return self.ua.random
        except:
            # 如果fake_useragent不可用,使用预定义列表
            user_agents = [
                'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
                'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
                'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
                'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0',
            ]
            return random.choice(user_agents)
    
    def make_request(self, url, retries=3, delay_range=(1, 3)):
        """发送请求,包含反爬虫对策"""
        for attempt in range(retries):
            try:
                # 设置随机请求头
                headers = {
                    'User-Agent': self.get_random_user_agent(),
                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
                    'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
                    'Accept-Encoding': 'gzip, deflate',
                    'Connection': 'keep-alive',
                    'Upgrade-Insecure-Requests': '1',
                }
                
                # 随机选择代理
                proxy = self.get_random_proxy()
                
                # 随机延时
                delay = random.uniform(*delay_range)
                time.sleep(delay)
                
                self.logger.info(f"尝试请求 {url} (代理: {proxy})")
                
                response = self.session.get(
                    url,
                    headers=headers,
                    proxies=proxy,
                    timeout=10
                )
                
                # 检查响应状态
                if response.status_code == 200:
                    return response
                elif response.status_code == 429:  # 请求过于频繁
                    self.logger.warning("请求过于频繁,增加延时")
                    time.sleep(random.uniform(5, 10))
                    continue
                else:
                    self.logger.warning(f"HTTP {response.status_code}: {url}")
                    
            except requests.RequestException as e:
                self.logger.error(f"请求失败 (尝试 {attempt + 1}/{retries}): {e}")
                if attempt < retries - 1:
                    time.sleep(random.uniform(2, 5))
        
        return None
    
    def handle_captcha(self, captcha_image_url):
        """处理验证码(示例)"""
        # 实际使用时可以集成OCR服务或打码平台
        self.logger.info(f"检测到验证码: {captcha_image_url}")
        # 这里可以调用OCR服务或人工识别
        return input("请输入验证码: ")
    
    def crawl_with_session(self, login_url, username, password):
        """带会话的爬取(处理登录)"""
        try:
            # 获取登录页面
            login_page = self.make_request(login_url)
            if not login_page:
                return False
            
            # 解析登录表单(需要根据实际网站调整)
            from bs4 import BeautifulSoup
            soup = BeautifulSoup(login_page.text, 'html.parser')
            
            # 获取CSRF token等隐藏字段
            csrf_token = soup.find('input', {'name': 'csrf_token'})
            if csrf_token:
                csrf_token = csrf_token.get('value', '')
            
            # 准备登录数据
            login_data = {
                'username': username,
                'password': password,
                'csrf_token': csrf_token
            }
            
            # 发送登录请求
            login_response = self.session.post(
                login_url,
                data=login_data,
                headers={'User-Agent': self.get_random_user_agent()}
            )
            
            # 检查登录是否成功
            if login_response.status_code == 200:
                self.logger.info("登录成功")
                return True
            else:
                self.logger.error("登录失败")
                return False
                
        except Exception as e:
            self.logger.error(f"登录过程中发生错误: {e}")
            return False
    
    def crawl_rate_limited(self, urls, requests_per_minute=10):
        """限速爬取"""
        delay = 60.0 / requests_per_minute
        results = []
        
        for i, url in enumerate(urls):
            self.logger.info(f"爬取进度: {i+1}/{len(urls)}")
            
            response = self.make_request(url)
            if response:
                results.append({
                    'url': url,
                    'status': response.status_code,
                    'content_length': len(response.content)
                })
            else:
                results.append({
                    'url': url,
                    'status': 'failed',
                    'content_length': 0
                })
            
            # 保持请求间隔
            if i < len(urls) - 1:  # 不是最后一个请求
                time.sleep(delay + random.uniform(0, 1))
        
        return results

# 使用示例
def main():
    """主函数"""
    spider = AntiAntiSpider()
    
    # 示例URL列表
    urls = [
        'https://httpbin.org/delay/1',
        'https://httpbin.org/user-agent',
        'https://httpbin.org/ip',
    ]
    
    # 限速爬取
    results = spider.crawl_rate_limited(urls, requests_per_minute=5)
    
    # 输出结果
    for result in results:
        print(f"URL: {result['url']}")
        print(f"  状态: {result['status']}")
        print(f"  内容长度: {result['content_length']}")
        print()

if __name__ == '__main__':
    main()

小结与回顾

本章我们深入学习了Web爬虫开发的核心知识和实践方法:

  1. 爬虫基础概念:理解了Web爬虫的工作原理、类型和应用场景。

  2. HTTP协议:掌握了HTTP请求方法、状态码等基础知识。

  3. requests库:学会了使用requests库发送HTTP请求和处理响应。

  4. HTML解析:掌握了BeautifulSoup库解析HTML文档和提取数据的方法。

  5. 动态网页处理:了解了使用Selenium处理JavaScript渲染内容的技术。

  6. Scrapy框架:学习了专业的爬虫框架Scrapy的使用方法。

  7. 反爬虫对策:掌握了处理反爬虫机制的策略和技术。

  8. 数据存储:学会了将爬取的数据保存到文件和数据库中。

通过本章的学习和实战练习,你应该已经掌握了Web爬虫开发的核心技能,并能够在实际项目中运用这些技术来采集网络数据。Web爬虫是一项强大的数据获取技术,但使用时需要遵守相关法律法规和网站的使用条款。

练习与挑战

基础练习

  1. 开发以下爬虫:

    • 新闻网站爬虫:爬取新闻标题、内容、发布时间等信息
    • 电商网站爬虫:爬取商品名称、价格、评价等信息
    • 社交媒体爬虫:爬取微博、Twitter等平台的公开内容
    • 招聘网站爬虫:爬取职位信息、薪资待遇等数据
  2. 改进爬虫功能:

    • 添加异常处理和重试机制
    • 实现数据去重功能
    • 添加进度显示和日志记录
    • 实现断点续爬功能
  3. 学习使用以下工具:

    • Playwright:现代化的浏览器自动化工具
    • Scrapy-Splash:处理JavaScript渲染内容
    • ProxyPool:代理池管理工具

进阶挑战

  1. 开发分布式爬虫系统:

    • 使用Redis作为任务队列
    • 实现多进程/多线程爬取
    • 支持动态扩容和负载均衡
    • 提供监控和管理界面
  2. 实现智能爬虫:

    • 基于机器学习的内容识别
    • 自适应反爬虫策略
    • 智能代理IP管理
    • 自动验证码识别
  3. 创建爬虫平台:

    • 提供Web界面配置爬虫任务
    • 支持多种数据导出格式
    • 实现爬虫任务调度和监控
    • 集成数据分析和可视化功能

项目实战

开发一个"智能数据采集平台",集成以下功能:

  • 多种爬虫引擎支持(静态、动态、API等)
  • 可视化爬虫配置界面
  • 智能反爬虫对策
  • 分布式任务调度
  • 数据清洗和预处理
  • 多种数据存储后端
  • 实时监控和告警
  • 数据分析和报告生成

扩展阅读

  1. Scrapy官方文档: docs.scrapy.org/

    • Python最流行的爬虫框架官方文档
  2. 《用Python写网络爬虫》 by Richard Lawson:

    • 详细介绍Python爬虫开发的书籍
  3. Selenium官方文档: www.selenium.dev/documentati…

    • 浏览器自动化工具的官方文档
  4. BeautifulSoup官方文档: www.crummy.com/software/Be…

    • HTML/XML解析库的官方文档
  5. 《Web Scraping with Python》 by Ryan Mitchell:

    • 介绍Web爬虫技术的实用书籍
  6. Playwright官方文档: playwright.dev/python/

    • 现代化浏览器自动化工具
  7. 《精通Scrapy网络爬虫》:

    • 深入介绍Scrapy框架的中文书籍
  8. Robots.txt协议: www.robotstxt.org/

    • 网站爬虫协议的标准规范

通过深入学习这些扩展资源,你将进一步巩固对Web爬虫开发的理解,并掌握更多高级用法和最佳实践。Web爬虫开发是数据科学和信息检索领域的重要技能,掌握它将为你在数据分析、市场研究等领域的工作提供强大支持。