查看全课大纲(第 22 / 52 节)
  1. 1.量化分析基本概念:从咖啡馆到华尔街
  2. 2.量化交易员学习路线:从零基础到策略开发
  3. 3.量化策略生命周期:数据分析的五大流程
  4. 4.量化数据类型:数值型与类别型数据
  5. 5.量化沙盒搭建(1):Python与Anaconda安装
  6. 6.量化沙盒搭建(2):Jupyter Notebook与库配置
  7. 7.本地多源数据:量化历史回测的基石
  8. 8.TXT数据流操作:处理金融交易日志
  9. 9.JSON数据流操作:解析股票行情API响应
  10. 10.CSV行情文件操作:读取与存储历史K线
  11. 11.Excel报表操作:清洗财报与财务指标
  12. 12.量化数据库:SQL常用查询语法
  13. 13.量化数据库:Python连接与写入
  14. 14.量化多表连接:关联股票信息与行情
  15. 15.实战:小象咖啡店营业数据与个股财务指标的多源数据分析
  16. 16.量化数据采集:爬虫工作原理与合规
  17. 17.量化爬虫核心:URL管理与去重策略
  18. 18.量化网页下载:防反爬与请求头伪装
  19. 19.网页解析(1):使用XPath定位财经新闻
  20. 20.网页解析(2):使用CSS选择器与ItemLoader
  21. 21.Scrapy框架:构建工业级量化爬虫
  22. 22.Scrapy实战(1):新建项目与数据模型定义
  23. 23.Scrapy实战(2):编写财经爬虫逻辑
  24. 24.Scrapy实战(3):启用数据管道清洗行情
  25. 25.Scrapy实战(4):防反爬中间件配置
  26. 26.实战:构建每日热门个股行情自动采集系统
  27. 27.NumPy与SciPy:加速量化矩阵计算
  28. 28.NumPy数组:构建股票价格矩阵
  29. 29.数组切片:提取特定股票与交易日数据
  30. 30.NumPy常用方法:计算均值与波动率
  31. 31.向量化计算:摆脱缓慢的For循环
  32. 32.通用函数ufunc:计算对数收益率与累计收益
  33. 33.实战:股票组合资产收益率与协方差矩阵计算
  34. 34.Pandas Series:管理单只股票收盘价
  35. 35.Pandas DataFrame:构建多只股票行情面板
  36. 36.Pandas Index:时间序列索引与对齐
  37. 37.DataFrame索引操作:loc与iloc定位行情
  38. 38.索引总结:量化多字段筛选与定位
  39. 39.运算与对齐:合并不同交易日的数据
  40. 40.Pandas map:对股票代码与状态做映射
  41. 41.Pandas apply:滚动计算技术指标
  42. 42.排序操作:按市盈率与收益率对股票排名
  43. 43.数据清洗:填充与剔除停牌股票缺失值
  44. 44.数据清洗:剔除重复报价与异常交易数据
  45. 45.常用统计:量化最大回撤与分位数风险评估
  46. 46.极值定位:寻找历史最高价与最低价的发生日
  47. 47.风险与累计收益:计算年化波动率与累计回报
  48. 48.实战:小象咖啡店营业数据与热门个股时间序列清洗分析
  49. 49.Series索引操作:按位置与标签访问行情数据
  50. 50.文件读写操作:CSV行情数据导入导出
  51. 51.数据清洗:替换异常报价数据
  52. 52.常用统计:sum/mean/median/count聚合指标

Scrapy实战(1):新建项目与数据模型定义

约 10 分钟

小象实战讲义 · 量化交易Python入门:从咖啡馆到华尔街

本章我们将探寻如何使用 Scrapy 框架进行网络数据采集。作为量化分析师,你将编写爬虫从财经网站爬取最新的股票行情和社交媒体上的咖啡店舆情,为策略引擎输入第一手另类数据。

💡 核心导读

本节将带领读者掌握Scrapy框架的基础使用流程,从环境配置到第一个爬虫项目的创建与运行。核心技能点包括:Scrapy项目结构解析、Spider组件编写规范、数据提取的两种核心方法(CSS选择器与XPath)。通过一个完整的新闻标题抓取案例,读者将学会如何构建基础爬虫并理解Scrapy的异步处理机制。


环境准备与项目创建

安装Scrapy

使用pip安装最新稳定版Scrapy(需Python 3.8+环境):

pip install scrapy
scrapy version

创建项目骨架

Scrapy采用工程化方式管理爬虫项目,执行以下命令生成标准目录结构:

scrapy startproject news_crawler
cd news_crawler

生成的项目结构说明:

news_crawler/
├── scrapy.cfg            # 部署配置文件
└── news_crawler/         # Python模块目录
    ├── __init__.py
    ├── items.py          # 数据容器定义
    ├── middlewares.py    # 中间件配置
    ├── pipelines.py      # 数据处理管道
    ├── settings.py       # 项目设置
    └── spiders/          # 爬虫代码目录
        └── __init__.py

编写第一个Spider

基础Spider模板

在spiders/目录下创建news_spider.py,实现抓取新闻标题的功能:

import pandas as pd

# 模拟新闻数据解析,使用pandas DataFrame演示结构化输出
news_data = [
    {'title': '量化交易入门指南发布', 'source': 'https://news.example.com/latest'},
    {'title': '华尔街日报:市场波动加剧', 'source': 'https://news.example.com/latest'},
    {'title': '科技股财报季来临', 'source': 'https://news.example.com/latest'}
]

df_news = pd.DataFrame(news_data)
print("解析到的新闻条目:")
print(df_news.to_string(index=False))
print(f"\n共找到 {len(df_news)} 条新闻标题。")
解析到的新闻条目:
       title                          source
  量化交易入门指南发布 https://news.example.com/latest
华尔街日报:市场波动加剧 https://news.example.com/latest
    科技股财报季来临 https://news.example.com/latest

共找到 3 条新闻标题。

数据提取方法对比

Scrapy支持两种主要的数据提取方式:

方法示例适用场景
CSS选择器response.css('div::attr(class)')现代网页、简洁语法
XPathresponse.xpath('//div/@class')复杂DOM结构、精准定位
import pandas as pd

# 模拟详细新闻数据提取,使用pandas DataFrame演示字段提取
detailed_news = [
    {'title': 'AAPL股价创新高', 'time': '2023-10-26T09:30:00Z'},
    {'title': '美联储利率决议在即', 'time': '2023-10-27T14:00:00Z'}
]

df_detailed = pd.DataFrame(detailed_news)
print("解析到的详细新闻数据:")
print(df_detailed.to_string(index=False))
print("\n字段说明:")
print("  - title: 新闻标题")
print("  - time: 发布时间 (ISO 8601格式)")
解析到的详细新闻数据:
    title                 time
AAPL股价创新高 2023-10-26T09:30:00Z
美联储利率决议在即 2023-10-27T14:00:00Z

字段说明:
  - title: 新闻标题
  - time: 发布时间 (ISO 8601格式)

运行与调试

启动爬虫

在项目根目录执行以下命令运行爬虫并保存结果到JSON文件:

scrapy crawl news -o results.json -L INFO

调试技巧

  1. 使用Scrapy Shell快速测试选择器:
scrapy shell 'https://news.example.com/latest'
>>> response.css('h2::text').get()
  1. 修改settings.py提高调试效率:
# 爬虫配置示例,使用字典和常量演示配置概念
DOWNLOAD_DELAY = 2
USER_AGENT = 'Mozilla/5.0 (compatible; Googlebot/2.1)'

# 配置字典
crawler_config = {
    'DOWNLOAD_DELAY': DOWNLOAD_DELAY,
    'USER_AGENT': USER_AGENT,
    'name': 'configured_spider',
    'start_urls': ['https://example.com']
}

print("爬虫配置:")
for key, value in crawler_config.items():
    if key == 'DOWNLOAD_DELAY':
        print(f"{key}: {value}秒")
    else:
        print(f"{key}: {value}")
爬虫配置:
DOWNLOAD_DELAY: 2秒
USER_AGENT: Mozilla/5.0 (compatible; Googlebot/2.1)
name: configured_spider
start_urls: ['https://example.com']

📝 动手练一练

第 1 题 · 概念应用

你正在为量化策略构建一个舆情数据采集模块,需要从多个财经新闻网站抓取文章标题、发布时间和来源。为了确保数据在后续的清洗、分析和存入数据库的流程中保持结构一致,你决定使用 Scrapy 框架的 Item 类来定义数据模型。

以下关于使用 scrapy.Item 和 scrapy.Field() 来声明数据结构的说法,哪一项是正确的?

  • A. 它能够根据网页内容自动推断并设置字段的数据类型,例如将字符串“2024-01-15”自动转换为 datetime 对象。
  • B. 它主要的作用是提升爬虫的网络请求并发数,从而加快数据采集速度。
  • C. 它提供了一个类似字典的标准化容器,显式声明允许的字段名,可以有效避免在后续处理中因字段名拼写错误导致的问题。
  • D. 在项目创建时,它会根据定义的字段自动在本地生成对应的数据库表。
👉 点击查看参考答案

答案:C

解析:

  • 选项 A 错误:scrapy.Field() 本身不具备自动类型转换的功能。它只是一个占位符,用于声明字段的存在,数据提取和类型转换需要在 Spider 或 Pipeline 中手动处理。
  • 选项 B 错误:Item 类用于定义数据结构,与网络请求的并发控制无关。并发设置通常在 settings.py 中通过 CONCURRENT_REQUESTS 等参数配置。
  • 选项 C 正确:这是 scrapy.Item 的核心价值。它通过类属性显式定义字段,创建的对象支持字典式访问(如 item['title']),同时对未声明字段进行约束,使数据在 Spider、Pipeline 等组件间传递时更加规范和安全。
  • 选项 D 错误:Item 定义是纯 Python 代码,不会自动生成数据库表。将 Item 数据持久化到数据库需要在 pipelines.py 中编写相应的逻辑。

第 2 题 · 代码填空

scrapy.Item 的核心思想可以用纯 Python 模拟:子类用类属性声明有哪些字段,实例像字典一样存取数据,并拒绝写入未声明的字段。下面是一个极简版的 Field 与 Item 基类。请补全 FinancialNewsItem 的声明,让它成为一个只允许包含 headline、publish_date、source_url 三个字段的数据容器。

class Field:
    """模拟 scrapy.Field:标记一个已声明字段,可挂载元数据"""

    def __init__(self):
        self.metadata = {}


class Item:
    """模拟 scrapy.Item:类字典容器,只允许写入已声明字段"""

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.fields = {
            name: attr for name, attr in cls.__dict__.items()
            if isinstance(attr, Field)
        }

    def __init__(self):
        self._values = {}

    def __setitem__(self, key, value):
        if key not in self.fields:
            raise KeyError(f"未声明的字段: {key}")
        self._values[key] = value

    def __getitem__(self, key):
        return self._values[key]

    def keys(self):
        return self._values.keys()

    def __iter__(self):
        return iter(self._values)


class FinancialNewsItem(______):
    headline = Field()
    publish_date = Field()
    source_url = Field()


news_item = FinancialNewsItem()
news_item['headline'] = '美联储宣布维持利率不变'
news_item['publish_date'] = '2023-11-02'
news_item['source_url'] = 'https://example.com/fed-news'

print("声明的字段:", list(FinancialNewsItem.fields.keys()))
print("填充的数据:", dict(news_item))

try:
    news_item['invalid_field'] = 'test'
except KeyError as e:
    print("拦截未声明字段:", e)
👉 点击查看参考答案

空位应填:Item

补全后的完整可运行代码:

class Field:
    """模拟 scrapy.Field:标记一个已声明字段,可挂载元数据"""

    def __init__(self):
        self.metadata = {}


class Item:
    """模拟 scrapy.Item:类字典容器,只允许写入已声明字段"""

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.fields = {
            name: attr for name, attr in cls.__dict__.items()
            if isinstance(attr, Field)
        }

    def __init__(self):
        self._values = {}

    def __setitem__(self, key, value):
        if key not in self.fields:
            raise KeyError(f"未声明的字段: {key}")
        self._values[key] = value

    def __getitem__(self, key):
        return self._values[key]

    def keys(self):
        return self._values.keys()

    def __iter__(self):
        return iter(self._values)


class FinancialNewsItem(Item):
    headline = Field()
    publish_date = Field()
    source_url = Field()


news_item = FinancialNewsItem()
news_item['headline'] = '美联储宣布维持利率不变'
news_item['publish_date'] = '2023-11-02'
news_item['source_url'] = 'https://example.com/fed-news'

print("声明的字段:", list(FinancialNewsItem.fields.keys()))
print("填充的数据:", dict(news_item))

try:
    news_item['invalid_field'] = 'test'
except KeyError as e:
    print("拦截未声明字段:", e)

运行结果:

声明的字段: ['headline', 'publish_date', 'source_url']
填充的数据: {'headline': '美联储宣布维持利率不变', 'publish_date': '2023-11-02', 'source_url': 'https://example.com/fed-news'}
拦截未声明字段: '未声明的字段: invalid_field'

解释:

  1. 真实 Scrapy 中对应的写法是 class FinancialNewsItem(scrapy.Item),并用 scrapy.Field() 声明字段;本题的 Item 基类模拟了同样的机制,空位填 Item。
  2. __init_subclass__ 在类定义时扫描类属性,把三个 Field() 实例收集进 fields,形成“字段白名单”。
  3. 给已声明字段赋值时走 __setitem__ 正常存入内部字典;对未声明的 invalid_field 赋值时触发 KeyError,这正是 Scrapy 数据模型能防止字段名拼写错误(如把 headline 误写成 headine)的原因。
  4. 配合 keys()、__getitem__ 与 __iter__,实例可以像普通字典一样读取、转换(dict(news_item)),与 Pipeline 中字典式的数据流转保持一致。

本章小结

📋 行动清单

  • 创建一个新的Scrapy项目,尝试修改start_urls指向真实新闻网站(注意遵守robots.txt规则)
  • 在Scrapy Shell中练习使用response.css()和response.xpath()提取不同网页元素
  • 扩展NewsSpider使其能同时抓取新闻标题和发布时间,并将结果导出为CSV格式

—— 小象教研组