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

Scrapy遍历URL列表保存response.body到XML/TXT问题排查

问题根因
  • 启动即抛DropItem异常:你把raise DropItem写在了TicketsPipeline类的方法外层,属于类定义阶段就会执行的代码,Scrapy加载pipeline组件时会直接触发这个异常,根本不会进入实际爬取流程。另外DropItem的作用是标记单条无效item、跳过后续处理,只能在process_item方法内部、满足丢弃条件时抛出,正常处理完的item必须返回,不能处理完就直接抛异常。
  • 早期版本批量爬取文件被覆盖:你把生成的文件名变量定义在了start_requests循环的局部作用域,没有和单个请求绑定,parse_api回调执行时引用的是循环结束后最后一次赋值的文件名,所有请求的响应都会写入同一个文件,导致内容被覆盖。
  • 你最早的代码里parse_api方法缩进错误,被定义在了start_requests方法内部,Spider实际找不到对应的回调方法,运行时也会报错。
  • 你写的新版pipeline逻辑还有两个隐藏bug:一是filename_xml是单个字符串,不是可迭代对象,写for循环遍历会逐字符拆分文件名,生成一堆错误的单字符命名文件;二是你存入item的data是XPath返回的Selector对象或者BeautifulSoup对象,直接写入文件会触发类型错误,必须转成字节或者字符串格式才能写入。
修正方案

如果不需要做额外的字段提取,完全可以不用写Item和Pipeline,直接把文件名存在请求meta里和单条请求绑定,回调里直接写文件即可,逻辑最简单也不容易出错:

import os
import random
from datetime import datetime
import pandas as pd
import scrapy
from scrapy.crawler import CrawlerProcess

# 配置项 自行修改
SAVE_DIR = "./ticket_output"
os.makedirs(SAVE_DIR, exist_ok=True)
linkarr = df['URLOUT'].tolist() # 提前加载你的df数据源
today = datetime.today().strftime('%Y%m%d')

class MpvticketSpider(scrapy.Spider):
    name = 'mpvticket'   
    handle_httpstatus_list = [403,502,503,404]

    def start_requests(self):
        username = 'XXXX'
        password = 'XXXX'
        port = 22225
        headers = {
            'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
            'accept-language': 'en-US,en;q=0.9',
            'cache-control': 'max-age=0',
            'referer': 'https://www.mlb.com/',
            'sec-ch-ua': '".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"Windows"',
            'sec-fetch-dest': 'document',
            'sec-fetch-mode': 'navigate',
            'sec-fetch-site': 'same-origin',
            'sec-fetch-user': '?1',
            'upgrade-insecure-requests': '1',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36',
        }

        for url in linkarr:
            eventid = str(url).strip().split("pid=")[1].split("&")[0]
            # 文件名提前计算,存在meta中和当前请求绑定,不会出现串数据问题
            filename = os.path.join(SAVE_DIR, f"{eventid}_{today}.xml")
            session_id = random.random()
            super_proxy_url = f'http://{username}-country-us-session-{session_id}:{password}@zproxy.lum-superproxy.io:{port}'
            yield scrapy.Request(
                url, 
                callback=self.parse_api,
                meta={'proxy': super_proxy_url, 'save_filename': filename},
                headers=headers
            )

    def parse_api(self,response):
        filename = response.meta['save_filename']
        # 用wb模式直接写原始响应字节,不会破坏XML原有编码,避免乱码
        with open(filename, "wb") as f:
            f.write(response.body)

if __name__ == '__main__':
    process = CrawlerProcess(settings={
        "LOG_LEVEL": "INFO",
        "ROBOTSTXT_OBEY": False
    })
    process.crawl(MpvticketSpider)
    process.start()

如果一定要用Item+Pipeline的架构,修正对应代码即可:

  1. 修正pipelines.py
import os
from itemadapter import ItemAdapter

class TicketsPipeline:
    def process_item(self, item, spider):
        adapter = ItemAdapter(item)
        filename = adapter['filename_xml']
        data = adapter['data']
        os.makedirs(os.path.dirname(filename), exist_ok=True)
        with open(filename, "wb") as fd:
            fd.write(data)
        # 处理完成必须返回item,不要随意抛DropItem
        return item
  1. 修正spider中的parse_api方法
def parse_api(self,response):
    url = response.url
    eventid = str(url).strip().split("pid=")[1].split("&")[0] 
    filename_xml = os.path.join(SAVE_DIR, f"{eventid}_{today}.xml")
    item = TicketsItem()
    # 直接存原始响应字节,不要存Selector/BeautifulSoup对象
    item['data'] = response.body
    item['filename_xml'] = filename_xml
    yield item
  1. 在settings.py中开启pipeline
ITEM_PIPELINES = {
   "tickets.pipelines.TicketsPipeline": 300,
}
注意事项
  • 所有和单个请求绑定的自定义参数(文件名、代理标记等)都存在request.meta字典中跟着请求走,不要用全局变量或者循环外的公共变量,Scrapy是异步执行框架,用公共变量一定会出现数据串扰、覆盖问题。
  • 存储XML/HTML类原始响应时,优先用"wb"模式直接写入response.body字节内容,不要先转解析对象再转字符串,会破坏原有格式和编码,大概率出现乱码。

内容的提问来源于stack exchange,提问作者Suren Gunaseelan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 14:27:16