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
相关产品推荐
相关产品推荐

