You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Scrapy爬取CoinMarketCap:数据存储、字段提取与分页问题求解

Scrapy爬取CoinMarketCap问题解决方案

1. 多页数据统一存储与自定义爬取范围实现

scrapy crawl -O cmc.csv 命令本身就会把全量爬取流程中所有yield的字典数据统一写入同一个CSV文件,不需要额外做文件合并操作,只需要保证所有yield的数据字段对齐即可。
原代码存在两个影响存储的问题:

  • 列表页和详情页yield的字段不统一,列表页返回name/price/vol_24H,详情页仅返回name,会导致CSV出现大量空列、错位行
  • 未实现分页逻辑,也没有预留自定义爬取数量的配置入口
    对应修正方案:
  • 在Spider中添加FEED_EXPORT_FIELDS配置,固定CSV导出列为name/price/watchlists三列,顺序完全匹配需求
  • 增加crawl_limit自定义参数,运行时可通过-a crawl_limit=数量指定爬取的排名区间上限,不传参数默认爬取全部币种
  • 列表页仅提取币种名称、价格、详情页链接,把前两个字段通过meta参数传递给详情页请求,等详情页抓到关注数字段后,再统一yield完整的三列数据,避免字段错位
  • 适配站点分页规则,CoinMarketCap列表页分页地址格式为https://coinmarketcap.com/?page=N(N从1开始,每页默认返回100条排名数据),当前页爬取完成后判断未达到数量上限时,自动构造下一页请求持续爬取
  • 修正原代码中start_urls的HTTP协议为HTTPS,避免不必要的重定向损耗

2. Watchlists字段精准提取

原选择器定位到了包含三个标签的父容器,直接缩小选择范围定位到关注数所在的子节点即可,不需要拿到整块HTML后做字符串切割。
正确提取代码:

watchlists = response.css('.bILTHz .namePill:contains("watchlists")::text').re_first(r'([\d,]+)')

逻辑说明:先选中class为bILTHz的容器下,文本包含watchlists的namePill子节点,提取其文本内容后通过正则直接抓取中间的数字串,自动剔除前后的On 和 watchlists冗余文字,拿到的结果为带千分位逗号的纯数字字符串,需要转整数的话额外加.replace(',', '')处理即可。

3. 首页仅爬取到16条数据的动态加载问题

该问题是站点前端采用虚拟滚动+懒加载机制导致:首屏服务端仅渲染前16条币种数据,剩余条目需要页面滚动到对应位置时才会触发接口加载,Scrapy本身没有浏览器JS渲染能力,自然拿不到滚动后加载的内容。
不需要引入浏览器渲染方案解决该问题,直接采用前述分页遍历的方式即可:站点分页地址返回的是服务端完整渲染的100条/页数据,按页码顺序遍历就能拿到全量排名数据,爬取效率远高于模拟浏览器滚动,也不容易触发反爬规则。


修正后可直接运行的完整Spider代码

import scrapy


class CmcSpider(scrapy.Spider):
    name = 'cmc'
    # 固定CSV导出列顺序
    custom_settings = {
        'FEED_EXPORT_FIELDS': ['name', 'price', 'watchlists']
    }

    def __init__(self, crawl_limit=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 解析自定义爬取数量参数
        self.crawl_limit = int(crawl_limit) if crawl_limit else None
        self.crawled_count = 0
        self.start_urls = ['https://coinmarketcap.com/']

    def parse(self, response):
        # 遍历当前页所有币种行
        for row in response.css('tbody tr'):
            # 达到设定爬取上限直接终止
            if self.crawl_limit and self.crawled_count >= self.crawl_limit:
                return

            name = row.css('td:nth-child(3) p::text').get()
            price = row.css('td:nth-child(4) span::text').get()
            detail_path = row.css('td:nth-child(3) a::attr(href)').get()
            # 跳过无效空行
            if not all([name, price, detail_path]):
                continue

            self.crawled_count += 1
            # 传递列表页已拿到的字段到详情页
            yield response.follow(
                detail_path,
                callback=self.parse_currency,
                meta={'name': name, 'price': price}
            )

        # 未达爬取上限时继续爬取下一页
        if not self.crawl_limit or self.crawled_count < self.crawl_limit:
            next_page_path = response.css('li.next a::attr(href)').get()
            if next_page_path:
                yield response.follow(next_page_path, callback=self.parse)

    def parse_currency(self, response):
        # 接收列表页传递的字段
        name = response.meta['name']
        price = response.meta['price']
        # 提取关注数
        watchlists = None
        watchlists_raw = response.css('.bILTHz .namePill:contains("watchlists")::text').re_first(r'([\d,]+)')
        if watchlists_raw:
            watchlists = watchlists_raw.replace(',', '')

        yield {
            'name': name,
            'price': price,
            'watchlists': watchlists
        }

运行命令参考

  • 爬取全部排名币种:scrapy crawl cmc -O cmc.csv
  • 爬取前100名币种:scrapy crawl cmc -O cmc.csv -a crawl_limit=100

内容的提问来源于stack exchange,提问作者Rafael Meireles

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 13:30:53