构建可应对HTML标记不一致的健壮网络爬虫
爬虫健壮性优化与数据入库适配方案
问题背景
你在爬取某网站的球员与裁判数据并存储到数据库时遇到了以下棘手情况:
- 初始测试页中,所有目标内容可通过
response.css("div.prelims p.indent::text")获取,并用正则区分球员和裁判,流程顺畅; - 但在其他页面发现结构不一致:新增了带
num类的<div>和<span>元素,且原本有效的正则在这类页面的第一个.indent段落上失效; - 你使用
DjangoItem,需要确保处理后的数据能顺利存入对应数据库表。
提升爬虫健壮性的通用原则与落地方法
1. 采用松耦合的选择器策略,避免依赖单一层级结构
不要局限于固定的父容器选择器,而是直接定位目标内容的核心特征,同时兼容多种父容器场景:
- 放弃仅依赖
div.prelims的限制,改用更宽泛的选择器匹配所有.indent段落,再过滤有效内容:# 匹配所有包含球员/裁判信息的.indent段落,不管父容器是prelims还是num all_indent_texts = response.css("p.indent::text").getall() - 如果需要区分不同父容器的内容,可以结合父元素特征做二次筛选:
# 同时获取段落和其父容器的class,方便后续判断 for p in response.css("p.indent"): parent_class = p.xpath("../@class").get() text = p.css("::text").get().strip() # 根据parent_class或text内容做分类处理
2. 优化正则匹配逻辑,增强容错性
正则失效往往是因为文本格式在不同页面有细微差异,你可以:
- 避免写过于严格的正则表达式,用模糊匹配+关键特征定位:
比如如果球员文本包含"Player"或球衣号码,裁判文本包含"Referee",可以调整正则为:import re player_pattern = re.compile(r"(Player|No\.\s+\d+)", re.IGNORECASE) referee_pattern = re.compile(r"(Referee|Umpire)", re.IGNORECASE) for text in all_indent_texts: if player_pattern.search(text): # 处理球员数据 pass elif referee_pattern.search(text): # 处理裁判数据 pass - 先对文本做标准化处理(去空格、去特殊字符)再匹配:
cleaned_text = re.sub(r"\s+", " ", text.strip())
3. 引入数据校验与清洗层,适配DjangoItem入库要求
在数据存入数据库前,增加专门的清洗步骤,确保数据符合模型字段要求:
- 针对
DjangoItem,可以在Item类中定义clean_<field>方法做字段级校验:from scrapy_djangoitem import DjangoItem from myapp.models import Player, Referee class PlayerItem(DjangoItem): django_model = Player def clean_name(self): name = self.get('name') if not name or len(name.strip()) == 0: raise DropItem("Invalid player name") return name.strip() - 统一处理空值、异常格式,比如把缺失的字段设为默认值,或者直接丢弃无效条目:
from scrapy.exceptions import DropItem class DataCleaningPipeline: def process_item(self, item, spider): # 校验核心字段是否存在 if not item.get('content'): raise DropItem("Missing content field") # 清洗文本 item['content'] = item['content'].strip() return item
4. 实现结构化的异常处理与日志记录
遇到页面结构变化时,能快速定位问题,避免爬虫崩溃:
- 在解析函数中捕获异常,并记录详细日志:
import logging logger = logging.getLogger(__name__) def parse(self, response): try: all_indent_texts = response.css("p.indent::text").getall() # 处理逻辑 except Exception as e: logger.error(f"Error parsing page {response.url}: {str(e)}") # 可以选择继续爬取其他页面,或者标记该页面后续重试 - 对无法匹配的页面,单独记录URL和页面结构快照,方便后续分析调整选择器。
5. 采用分层解析架构,分离数据提取与业务逻辑
把页面解析、数据清洗、入库操作拆分成独立模块,降低耦合度:
- 单独写一个
parser模块负责提取原始数据:# parsers.py def extract_indent_content(response): return [p.css("::text").get().strip() for p in response.css("p.indent") if p.css("::text").get()] - 写一个
classifier模块负责区分球员/裁判数据:# classifiers.py def classify_content(text): if "Referee" in text: return "referee" elif "Player" in text: return "player" else: return None - 在爬虫的
parse方法中调用这些模块,专注于流程控制。
内容的提问来源于stack exchange,提问作者Malik A. Rumi
相关产品推荐
相关产品推荐

