如何用Scrapy高效抓取电商搜索分页下的40000个产品页面
我正在尝试用Scrapy抓取某电商网站的产品数据:网站的搜索页面(比如www.example.com/search)返回分页的产品链接列表,总共有约40000个产品页面,每个页面结构相似,抓取难度不高。我的需求是先全量抓取入库,之后每日调度爬虫抓取新增产品。
目前我的代码思路大致如下(无法直接运行,仅作逻辑展示):
class ExampleSpider(scrapy.Spider): next_page = 1 last_page = 100 start_urls = ['example.com/search?page={}'.format(next_page)] def parse(self, response): yield scrapy.Request(response.url, callback=self.follow_product_links) yield scrapy.Request(response.url, callback=self.follow_pagination_links) def follow_product_links(self,response): for href in selector_that_gets_all_the_product_links: yield response.follow(href, callback=self.parse_product) def parse_product(self,response): # 抓取产品页面数据并生成Item # 具体逻辑不影响当前问题 pass def follow_pagination_links(self,response): self.next_page += 1 if self.next_page < self.last_page: url = 'example.com/search?page={}'.format(self.next_page) scrapy.Request(url, callback=self.parse)
我知道Scrapy是异步框架能提升效率,但想了解有没有更优的技巧来处理这种大规模抓取和后续增量更新的场景,另外该网站没有公开API可用。
一、先把全量抓取的效率拉满
修正分页逻辑,别做重复功
你现在的parse方法对同一个页面发了两次请求,这完全是浪费资源!直接在parse里同时处理产品链接和分页就好,省掉一次重复请求:def parse(self, response): # 先处理当前页的所有产品链接 for href in response.css('selector_that_gets_all_the_product_links'): yield response.follow(href, callback=self.parse_product) # 再处理分页跳转 self.next_page += 1 # 别硬编码last_page,建议从页面里提取总页数(比如分页栏的“共X页”) if self.next_page <= self.last_page: next_url = f'example.com/search?page={self.next_page}' yield scrapy.Request(next_url, callback=self.parse)硬编码
last_page=100很容易出错,比如网站产品更新后页数变了,最好从页面的分页控件里动态提取总页数,这样更稳妥。调优Scrapy并发参数
异步框架的优势就是并发,你得把settings.py里的参数调到位:CONCURRENT_REQUESTS = 32 # 默认16,可根据网站承受力调整,先从20开始试 CONCURRENT_REQUESTS_PER_DOMAIN = 16 # 单域名并发别太高,避免被封 DOWNLOAD_DELAY = 0.25 # 请求间隔,0.25秒比较适中,反爬严的话可以调大 AUTOTHROTTLE_ENABLED = True # 开启自动节流,让Scrapy根据网站响应速度自动调并发自动节流特别有用,既能保证抓取速度,又不会因为请求太猛被网站拉黑。
批量入库,别插一条存一条
抓一个产品就往数据库插一次,效率极低!在Item Pipeline里攒一批(比如100个)再批量插入,能把入库速度提升好几倍:class BatchInsertPipeline: def __init__(self): self.item_buffer = [] self.batch_size = 100 def process_item(self, item, spider): self.item_buffer.append(item) if len(self.item_buffer) >= self.batch_size: self._insert_to_db() self.item_buffer = [] return item def close_spider(self, spider): # 爬虫结束时把剩下的item也插入 if self.item_buffer: self._insert_to_db() def _insert_to_db(self): # 这里写你的批量插入数据库逻辑,比如用SQL的INSERT ... VALUES (...) pass开启请求缓存,省重复请求
开启HTTPCACHE_ENABLED = True,Scrapy会缓存已经爬过的页面,调试或者中途重启爬虫时,不用再重新请求已经抓过的页面,能省不少时间。全量抓完后可以关掉,或者调整缓存过期时间。
二、搞定每日增量更新
记录已抓取的产品唯一标识
全量抓取时,把每个产品的唯一ID(比如URL里的product_id,或者页面里的产品编号)存在数据库或者Redis里。增量抓取时,先从搜索页抓新的产品链接,提取ID和已有的对比,只抓没见过的ID对应的页面,避免重复抓取。找增量抓取的最优入口
别每次增量都从头翻所有分页!很多电商网站有“最新上架”的筛选选项,直接抓这个筛选下的前几页,就能拿到最新的产品,减少无效请求。如果没有这个选项,就记录上次抓取的最后一页页码,下次从那页开始往后抓,或者每天抓最近的10页,再去重。定时调度爬虫
用cron(Linux/macOS)或者Windows任务计划程序,每天固定时间启动增量爬虫。如果想更专业点,可以用Scrapyd部署爬虫,然后通过API或者Airflow来调度任务,还能监控爬虫运行状态。
三、避坑小贴士
- 反爬应对:4万条数据不算少,建议整个代理IP池(比如用
scrapy-proxies插件),再加上随机User-Agent,避免固定IP和UA被封。 - 错误处理:在
parse_product里加异常捕获,别因为单个产品页面报错导致整个爬虫挂掉;开启RETRY_ENABLED = True,让Scrapy自动重试失败的请求(比如500错误、超时)。 - 日志监控:打开Scrapy的日志,设置合适的日志级别,方便你监控抓取进度、排查错误。
内容的提问来源于stack exchange,提问作者Taylor Handy

