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

Scrapy多URL爬取CSV数据被覆盖问题求助

问题:Scrapy多URL爬取时CSV数据被覆盖

我写了个简单的Scrapy脚本,用来从jobs2careers爬取招聘信息,用Item组件导出到CSV。但碰到个头疼的问题:当配置多个起始URL时,导出的CSV数据会被覆盖。我试过用openpyxl这类库,怀疑是不是多Spider运行导致的?下面是我的脚本代码:

import scrapy
from scrapy.selector import Selector
from ..items import QuotetutorialItem
class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    n = 1
    start_urls = ['https://www.jobs2careers.com/results3.php?q=Fashion&l=Miami%2C+FL&s=00']
    def parse(self, response):
        items = QuotetutorialItem()
        s = Selector(response)
        quote = s.xpath("//div[@id='jobs_container']/div")
        for q in quote:
            url = response.url
            industry = q.xpath("//input[@id='search_q']/@value").get()
            state = q.xpath("//*[@id='results_controller']/div[1]/div[1]/div/div/div/div/a[3]/text()").get()
            company_name = q.xpath(".//div[@class='companyname']/span[@class='company']/text()").get()
            job_title = q.xpath(".//div[@class='title title1 hidden-xs']/text()").get()
            items['url'] = url
            items['industry'] = industry
            items['state'] = state
            items['company_name'] = company_name
            items['job_title'] = job_title
            yield items
        num = int(response.xpath("//h1[@class='result2']//text()").get().split("\n")[0].replace(',', ''))
        if num > 1000:
            num = 1000
        total = int(num) // 10 + 1
        np = response.url
        np = np[np.rfind('=') + 1:]
        next_page = response.url.replace(np, str((self.n * 10)))
        if self.n < total:
            self.n += 1
            yield response.follow(next_page,callback = self.parse)

解决方案

先给你拆解下问题根源,再一步步解决:

1. CSV覆盖的核心原因:默认导出是覆盖模式

Scrapy自带的CSV导出器,默认是覆盖写入文件的——不管你是单个Spider加多个起始URL,还是多次运行Spider,都会把旧文件清空再写新数据。这是最主要的问题,咱们先解决这个:

方法一:改成追加模式(推荐)

在项目的settings.py里添加/修改FEEDS配置,把overwrite设为False:

FEEDS = {
    'jobs_data.csv': {
        'format': 'csv',
        'encoding': 'utf-8',
        'overwrite': False,  # 关键:设为False就会追加,不会覆盖旧数据
        'store_empty': False,
    }
}

这样不管你加多少个起始URL,或者多次运行爬虫,新数据都会追加到CSV末尾,不会丢旧数据。

方法二:生成动态文件名(避免同文件追加)

如果你不想把所有数据堆在一个文件里,想每次运行生成新文件,可以用时间戳命名:

from datetime import datetime

FEEDS = {
    f'jobs_data_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv': {
        'format': 'csv',
        'encoding': 'utf-8',
        'overwrite': True,
    }
}

每次运行都会生成一个带时间戳的新CSV,彻底避免覆盖问题。

2. 修复你脚本里的隐藏bug

除了CSV覆盖,你的脚本还有几个会导致数据不准确的小问题,顺便一起改了:

  • Item实例化位置错了:你把items = QuotetutorialItem()放在循环外面,循环里复用同一个Item对象,这会导致最后yield的Item数据被后面的循环项覆盖!必须把Item实例化放到循环里面。
  • 全局XPath写法不规范:industry和state是整个页面的全局数据,不需要在循环里每次取,而且你的XPath没加.,虽然当前页面没问题,但写法不严谨,容易出bug。
  • 页码截取太脆弱:用np.rfind('=')截取页码参数,万一URL里有多个=就会出错,用urlparse处理URL参数更可靠。

修改后的完整脚本:

import scrapy
from scrapy.selector import Selector
from ..items import QuotetutorialItem
from urllib.parse import urlparse, parse_qs, urlencode

class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    # 可以添加多个起始URL
    start_urls = [
        'https://www.jobs2careers.com/results3.php?q=Fashion&l=Miami%2C+FL&s=00',
        'https://www.jobs2careers.com/results3.php?q=Tech&l=New+York%2C+NY&s=00'
    ]

    def parse(self, response):
        s = Selector(response)
        # 全局数据只取一次,放循环外面
        url = response.url
        industry = s.xpath("//input[@id='search_q']/@value").get()
        state = s.xpath("//*[@id='results_controller']/div[1]/div[1]/div/div/div/div/a[3]/text()").get()
        
        quote = s.xpath("//div[@id='jobs_container']/div")
        for q in quote:
            # 每个循环项都新建一个Item实例,避免数据覆盖
            items = QuotetutorialItem()  
            company_name = q.xpath(".//div[@class='companyname']/span[@class='company']/text()").get()
            job_title = q.xpath(".//div[@class='title title1 hidden-xs']/text()").get()
            
            # 处理空值,避免输出乱码或空字符串
            items['url'] = url
            items['industry'] = industry.strip() if industry else None
            items['state'] = state.strip() if state else None
            items['company_name'] = company_name.strip() if company_name else None
            items['job_title'] = job_title.strip() if job_title else None
            
            yield items

        # 用urlparse处理页码,更稳定
        parsed_url = urlparse(response.url)
        query_params = parse_qs(parsed_url.query)
        current_s = int(query_params.get('s', ['00'])[0])
        
        num_text = s.xpath("//h1[@class='result2']//text()").get()
        if num_text:
            num = int(num_text.split("\n")[0].replace(',', ''))
            num = min(num, 1000)  # 限制最大爬取量
            next_s = current_s + 10
            if next_s < num:
                query_params['s'] = str(next_s)
                next_page_url = f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}?{urlencode(query_params, doseq=True)}"
                yield response.follow(next_page_url, callback=self.parse)

3. 关于“多Spider运行”的疑问

如果你是同时运行多个不同的Spider,默认每个Spider会用同一个FEEDS配置,这时候如果文件名相同,确实会互相覆盖。解决方法是给每个Spider单独配置FEEDS:

class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    custom_settings = {
        'FEEDS': {
            'quotes_jobs.csv': {
                'format': 'csv',
                'overwrite': False
            }
        }
    }
    # ... 其他代码

每个Spider用不同的文件名,就不会互相干扰了。


内容的提问来源于stack exchange,提问作者Aleksandar Ciric

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 09:22:48