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

Scrapy爬虫无输出问题排查:CSV爬取邮箱与手机号失败

Scrapy爬虫问题排查与修复方案

核心错误分析

1. Pandas数据长度不匹配错误

报错ValueError: All arrays must be of the same length,原因是构造DataFrame时,name、category、url是长度为1的列表,但email或phone可能是长度大于1的列表(比如一个页面提取到多个邮箱),导致字段长度不一致。之前的min_length处理仅保证邮箱和手机号长度一致,但未解决与其他字段的长度匹配问题。

2. 连接丢失问题

ConnectionLost错误属于网络层问题,可能是目标服务器限制、网络波动或robots.txt请求失败导致。Scrapy默认会重试3次,若仍失败则会放弃该请求,但原代码未处理这类下载失败的场景,导致对应条目无法写入CSV。

修复后的完整代码

import scrapy
import pandas as pd
import os
import re
import logging


class BusinessSpider(scrapy.Spider):
    name = 'business_scrape'
    reject = ['example.com', 'example2.com']  # 过滤不需要爬取的域名
    output_path = 'output.csv'
    file_initialized = False  # 标记输出文件是否已初始化

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 初始化日志配置
        logging.basicConfig(
            filename='scrapy.log',
            format='%(levelname)s: %(message)s',
            level=logging.INFO
        )
        # 检查并初始化输出文件
        self.initialize_output_file()

    def initialize_output_file(self):
        """初始化输出文件,仅在爬虫启动时执行一次"""
        if os.path.exists(self.output_path):
            response = input(f"文件 {self.output_path} 已存在,是否覆盖?y/n\n")
            if response.lower() != 'y':
                self.logger.info("用户选择不覆盖文件,爬虫终止")
                raise SystemExit()
            else:
                # 覆盖文件并写入表头
                df = pd.DataFrame(columns=['name', 'category', 'email', 'phone', 'url'])
                df.to_csv(self.output_path, index=False)
                self.file_initialized = True
        else:
            # 创建新文件并写入表头
            df = pd.DataFrame(columns=['name', 'category', 'email', 'phone', 'url'])
            df.to_csv(self.output_path, index=False)
            self.file_initialized = True

    def extract_emails(self, text):
        """提取页面中的邮箱地址,多个邮箱用逗号分隔"""
        emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text)
        return ', '.join(emails) if emails else 'NA'

    def extract_phone(self, text):
        """提取页面中的手机号,多个手机号用逗号分隔"""
        phone_numbers = re.findall(r'(?:(?:\+\d{1,2}\s?)?\(?\d{3}\)?[-.\s]?)?\d{3,4}[-.\s]?\d{4}', text)
        return ', '.join(phone_numbers) if phone_numbers else 'NA'

    def start_requests(self):
        """读取CSV中的URL,过滤后生成请求"""
        csv_path = 'bozeman_businesses.csv'
        init_df = pd.read_csv(csv_path)

        for _, row in init_df.iterrows():
            name = row['name']
            url = row['url']
            category = row['category']

            # 提前过滤reject列表中的域名
            if any(reject_domain in url for reject_domain in self.reject):
                self.logger.info(f"跳过过滤域名: {url}")
                # 写入过滤条目的NA数据
                self.write_to_csv(name, category, 'NA', 'NA', url)
                continue

            # 带errback处理下载失败的情况
            yield scrapy.Request(
                url=url,
                callback=self.parse_link,
                meta={'name': name, 'category': category},
                errback=self.handle_request_failure
            )

    def parse_link(self, response):
        """解析页面,提取数据并写入CSV"""
        name = response.meta['name']
        category = response.meta['category']
        url = response.url

        try:
            html_text = response.text
            email = self.extract_emails(html_text)
            phone = self.extract_phone(html_text)
        except Exception as e:
            self.logger.error(f"解析 {url} 出错: {str(e)}")
            email = 'NA'
            phone = 'NA'

        self.write_to_csv(name, category, email, phone, url)

    def handle_request_failure(self, failure):
        """处理请求失败的情况,写入NA数据"""
        request = failure.request
        name = request.meta['name']
        category = request.meta['category']
        url = request.url

        self.logger.error(f"请求 {url} 失败: {str(failure.value)}")
        self.write_to_csv(name, category, 'NA', 'NA', url)

    def write_to_csv(self, name, category, email, phone, url):
        """统一写入CSV的方法"""
        df = pd.DataFrame({
            'name': [name],
            'category': [category],
            'email': [email],
            'phone': [phone],
            'url': [url]
        })
        df.to_csv(self.output_path, mode='a', header=False, index=False)

关键修复点说明

  • 数据长度匹配:将提取到的多个邮箱/手机号用逗号拼接成单个字符串,确保所有字段都是长度为1的列表,避免DataFrame构造错误。
  • 文件初始化优化:仅在爬虫启动时检查一次输出文件,避免每个请求都弹窗询问,同时确保表头只写入一次。
  • 请求失败处理:新增errback方法处理下载失败(如连接丢失)的请求,确保这类条目也能写入NA值到CSV。
  • 提前过滤域名:在start_requests中提前过滤reject列表中的域名,减少不必要的请求,并直接写入对应NA数据。
  • 日志配置优化:将日志初始化放到__init__方法中,避免重复配置。

有效调试方法

  • Scrapy Shell调试:使用scrapy shell <URL>命令进入交互模式,直接测试页面解析逻辑,验证正则表达式是否能正确提取数据。
  • 调整日志等级:在settings.py中设置LOG_LEVEL = 'DEBUG',查看更详细的请求和响应日志,定位具体失败原因。
  • 断点调试:在关键代码处添加import pdb; pdb.set_trace(),暂停爬虫执行,检查变量值和代码流程。
  • 统计失败请求:在handle_request_failure中记录失败的URL和原因,后续可以单独处理这些URL。
  • 测试单个URL:修改start_requests,只爬取一个有问题的URL,集中排查该URL的爬取和解析问题。

内容的提问来源于stack exchange,提问作者BegginerScraper Griff

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 08:10:35