93·模块十八:综合项目实战高级

项目实战:构建一个完整的数据爬虫

projectweb-scrapingdata-collection

第93课 - 项目实战:构建一个完整的数据爬虫

学习目标

  1. 理解并设计一个完整的网络爬虫项目结构。
  2. 掌握使用 Scrapy 框架进行高效、规范的数据爬取。
  3. 学会将爬取的数据持久化到文件(CSV、JSON)和数据库(SQLite)中。
  4. 能够处理基本的反爬策略(如设置User-Agent、下载延迟)。

核心概念

一个完整的爬虫不仅仅是"请求-解析-存储"这么简单。在生产环境中,我们需要考虑代码结构、效率、稳定性和对目标网站的友好性。Scrapy 是一个为了爬取网站数据、提取结构性数据而编写的应用框架,它是 Python 中最强大、最专业的爬虫框架。

爬虫工作流程简述:

  1. 发起请求:向目标网站的URL发送HTTP请求。
  2. 获取响应:接收网站返回的HTML或JSON等格式的响应。
  3. 解析数据:从响应中提取出我们需要的结构化数据(如标题、价格、链接)。
  4. 存储数据:将解析出的数据保存到文件或数据库中。
  5. 发现新链接:在解析过程中,发现新的URL,将其加入待爬队列(这就是"爬虫"名称的由来)。

代码示例

我们将使用 Scrapy 框架,构建一个爬取"豆瓣电影Top250"的爬虫。项目结构如下:

douban_spider/
│
├── scrapy.cfg          # 项目的部署配置文件
└── douban_spider/
    ├── __init__.py
    ├── items.py        # 定义要爬取的数据结构
    ├── middlewares.py   # 中间件,可处理请求/响应
    ├── pipelines.py    # 管道,用于处理爬取到的数据(如存储)
    ├── settings.py     # 项目的设置文件
    └── spiders/
        └── top250.py   # 爬虫主逻辑

1. 安装 Scrapy:

pip install scrapy

2. 创建项目:

scrapy startproject douban_spider
cd douban_spider
scrapy genspider top250 "movie.douban.com"

3. 定义数据项 (items.py):

import scrapy

class MovieItem(scrapy.Item):
    # 定义我们要爬取的字段
    title = scrapy.Field()       # 电影名
    rank = scrapy.Field()        # 排名
    score = scrapy.Field()       # 评分
    quote = scrapy.Field()       # 一句话评价
    link = scrapy.Field()        # 电影详情页链接

4. 编写爬虫 (spiders/top250.py):

import scrapy
from douban_spider.items import MovieItem

class Top250Spider(scrapy.Spider):
    name = 'top250'  # 爬虫的唯一名称
    allowed_domains = ['movie.douban.com']  # 允许爬取的域名
    start_urls = ['https://movie.douban.com/top250']  # 起始URL

    def parse(self, response):
        # 解析每一页的电影列表
        movie_list = response.css('.grid_view li')

        for movie in movie_list:
            item = MovieItem()
            # 使用CSS选择器提取数据
            item['rank'] = movie.css('.pic em::text').get()
            item['title'] = movie.css('.hd a span:nth-child(1)::text').get()
            item['score'] = movie.css('.rating_num::text').get()
            item['quote'] = movie.css('.quote .inq::text').get()
            item['link'] = movie.css('.hd a::attr(href)').get()
            yield item  # 将item交给pipeline处理

        # 寻找下一页链接,实现翻页
        next_page = response.css('.paginator .next a::attr(href)').get()
        if next_page is not None:
            # 使用 response.follow 跟踪链接,回调parse方法
            yield response.follow(next_page, self.parse)

5. 设置数据管道 (pipelines.py):

import json
import sqlite3

class JsonPipeline:
    """将数据保存为JSON文件"""
    def open_spider(self, spider):
        self.file = open('movies.json', 'w', encoding='utf-8')

    def close_spider(self, spider):
        self.file.close()

    def process_item(self, item, spider):
        line = json.dumps(dict(item), ensure_ascii=False) + "\n"
        self.file.write(line)
        return item

class SQLitePipeline:
    """将数据保存到SQLite数据库"""
    def open_spider(self, spider):
        self.conn = sqlite3.connect('movies.db')
        self.cursor = self.conn.cursor()
        # 创建表(如果不存在)
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS top250 (
                rank TEXT,
                title TEXT,
                score TEXT,
                quote TEXT,
                link TEXT
            )
        ''')

    def close_spider(self, spider):
        self.conn.commit()
        self.conn.close()

    def process_item(self, item, spider):
        self.cursor.execute('''
            INSERT INTO top250 (rank, title, score, quote, link)
            VALUES (?, ?, ?, ?, ?)
        ''', (item['rank'], item['title'], item['score'], item['quote'], item['link']))
        self.conn.commit()
        return item

6. 配置设置 (settings.py):

# 关键配置项
BOT_NAME = 'douban_spider'
ROBOTSTXT_OBEY = True  # 遵守robots.txt协议
CONCURRENT_REQUESTS = 16  # 并发请求数
DOWNLOAD_DELAY = 2  # 每次请求间隔2秒,防止过快被封
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
# 启用数据管道,数字表示优先级,越小越先执行
ITEM_PIPELINES = {
    'douban_spider.pipelines.JsonPipeline': 300,
    'douban_spider.pipelines.SQLitePipeline': 300,
}

7. 运行爬虫:

scrapy crawl top250 -o movies.csv  # 也可以直接输出为CSV文件

实践练习

  1. (基础)修改爬虫:修改上面的 Top250Spider,使其额外爬取电影的导演信息,并将其添加到 MovieItem 中。

    • 预期输出movies.json 文件中每部电影都包含 director 字段。
    • 提示:豆瓣Top250页面上导演信息通常在 .bd p 标签内。
  2. (进阶)爬取其他网站:选择一个你感兴趣的、结构清晰的网站(如一个博客列表页),使用 Scrapy 为其创建一个爬虫,爬取文章的标题、发布时间和摘要。

    • 要求:使用 scrapy shell 先调试选择器,确保准确。
    • 预期输出:生成一个包含目标数据的 articles.json 文件。
  3. (挑战)增加反爬处理:在当前的豆瓣爬虫基础上,实现一个随机更换 User-Agent 的中间件。

    • 要求:在 middlewares.py 中定义一个 RandomUserAgentMiddleware,并在 settings.py 中启用它。
    • 提示:可以准备一个包含多个 User-Agent 的列表,每次请求随机选取一个。

常见错误

  1. 不遵守 robots.txt:虽然设置 ROBOTSTXT_OBEY = False 可以强行爬取,但这是不友好的行为,可能导致IP被封。请先查看网站的 /robots.txt 文件。
  2. 请求过于频繁:不设置 DOWNLOAD_DELAY 或设置得过小,很容易触发网站的反爬机制,导致返回403、429状态码或需要验证码。
  3. 选择器失效:网站的前端结构可能更新,导致CSS或XPath选择器失效。使用 scrapy shell 实时调试是验证选择器的最佳方式。
  4. 数据编码问题:在存储中文等非ASCII字符时,确保使用 ensure_ascii=False(JSON)或正确的文件编码(如utf-8)。

小结

本节课我们通过一个完整的实战项目,学习了如何使用 Scrapy 框架构建一个健壮的数据爬虫。关键点包括:

  1. Scrapy框架:它提供了清晰的项目结构、高效的数据提取机制(选择器)和强大的中间件/管道系统。
  2. 数据持久化:通过自定义 Pipeline,我们可以灵活地将数据存储到文件(JSON, CSV)或数据库(如 SQLite)中。
  3. 反爬策略:设置下载延迟、遵守 robots.txt、使用真实的 User-Agent 是基本且必要的礼貌行为。
  4. 完整流程:一个爬虫项目经历了"发起请求 -> 响应处理 -> 数据提取 -> 数据存储 -> 发现新链接"的循环。

你现在已经具备了构建生产级别数据爬虫的基础能力。在下一课中,我们将学习如何将爬取到的数据通过 RESTful API 服务提供给其他应用使用。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「项目实战:设计并实现一个 RESTful API 服务」 以巩固所学知识。