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

