Python爬虫实战:构建中国地震台网数据自动化采集与分析系统

Python爬虫实战:构建中国地震台网数据自动化采集与分析系统

1. 项目概述:从数据洪流中捕捉地球的脉搏

最近几年,无论是行业内的数据分析师,还是对公共安全数据感兴趣的个人开发者,都越来越关注一个领域:如何高效、稳定地获取并利用公开的灾害监测数据。中国地震台网中心(CENC)官方发布的地震信息,因其权威性、实时性和结构化程度高,成为了一个非常理想的数据源。这个项目,就是围绕“爬取中国地震台网地震数据”展开的一次实战。它远不止是写几行代码把网页上的表格扒下来那么简单,背后涉及对公开数据接口的逆向工程、应对反爬策略的攻防、海量时序数据的存储设计,以及最终让数据产生价值的分析场景。

简单来说,这个项目的核心目标是构建一个自动化管道,能够7x24小时不间断地从中国地震台网中心的官方数据发布页面,抓取全球范围内发生的地震事件信息,包括震级、震中位置、发震时刻、深度等关键参数,并将其清洗、结构化后存入数据库,为后续的地震活动性分析、区域风险评估甚至是一些科普应用提供数据基础。无论你是想研究地震活动的时空分布规律,还是为你的智慧城市项目集成灾害预警信息,亦或是单纯想做一个实时显示全球地震的“地球心跳”可视化大屏,这个项目都能给你提供一个坚实、可靠的起点。

我之所以花时间折腾这个,是因为在之前的一个区域地质风险评估项目中,需要长时间序列、高精度的地震目录数据。市面上的商业数据库要么太贵,要么数据维度不全。而官方渠道的数据虽然免费公开,但手动下载效率极低,且无法实现实时监控。于是,自己动手搭建一个稳定可靠的数据爬虫,就成了必然选择。在这个过程中,我踩了不少坑,也总结了一套相对成熟的方案,今天就把从思路到实现的完整过程,以及那些“教科书上不会写”的实操细节,分享给大家。

2. 核心思路与架构设计:稳健比炫技更重要

面对一个官方数据源,我们的第一原则必须是:合法、合规、友善。中国地震台网中心的数据是面向公众开放的公益数据,我们的爬虫行为应当尽可能模拟正常用户的访问,避免对服务器造成不必要的压力。因此,整个架构设计的核心思想是“稳健”和“可持续”,而非追求极致的抓取速度。

2.1 数据源分析与选择

中国地震台网中心对外提供数据的主要渠道有几个,我们需要根据需求进行选择:

  1. 官方数据发布页面:这是最常用也是数据最全面的源。页面通常以表格形式列出最新地震事件,包含发震时刻、震级、参考位置、深度等字段。数据更新频率高(几分钟到几十分钟),但页面结构可能随时间调整。
  2. 数据文件下载服务:中心会提供按日、按月打包的地震目录文件(如CSV或文本格式)。这种方式数据规整,适合历史数据批量补全,但实时性差。
  3. JSON/XML格式的接口:通过浏览器开发者工具抓包分析,有时可以发现网站背后调用的数据接口。这种接口返回结构化数据(JSON),是最理想的爬取目标,但需要仔细寻找和解析。

对于实时爬虫项目,我们的主攻方向是第1项和第3项。首先需要人工打开数据发布页面,使用浏览器的“检查”(Inspect)功能,切换到“网络”(Network)选项卡,刷新页面,观察加载过程中有哪些XHR或Fetch请求。寻找那些返回数据列表的请求,其响应体往往就是我们梦寐以求的JSON数据。

实操心得:不要一上来就硬解析HTML。花半小时仔细分析网络请求,如果能找到直接的数据接口,后续的开发复杂度会直线下降,稳定性和效率也会大幅提升。我曾经发现过一个返回最近24小时地震数据的JSON接口,其URL参数规律明显,这成为了整个项目的基石。

2.2 技术栈选型与考量

一个完整的爬虫系统不止是抓取(Crawl),还包括调度(Schedule)、解析(Parse)、存储(Store)和监控(Monitor)。以下是经过实践验证的技术栈:

  • 编程语言:Python 3.8+。生态丰富是王道。requests用于网络请求,BeautifulSoup4lxml用于HTML解析(如果找不到JSON接口),pandas用于数据清洗和初步分析,sqlalchemy用于数据库操作,apschedulercelery用于定时任务调度。Python几乎为爬虫的每一个环节都提供了成熟的工具。
  • 网络请求库:Requests + 自定义SessionRequests库简单易用,足以应对大多数场景。关键在于使用Session对象来保持连接和Cookies,并精心设置请求头(User-Agent, Referer等),让自己看起来更像一个真实的浏览器。
  • 解析库:优先尝试直接解析JSON。如果幸运地找到了数据接口,直接用response.json()即可。如果只能解析HTML,则使用lxml,因为它的解析速度比BeautifulSoup快得多,在需要处理大量页面时优势明显。
  • 数据存储:关系型数据库(PostgreSQL/MySQL)。地震数据是典型的时序、结构化数据。关系型数据库在复杂查询、事务支持和数据一致性方面表现更好。我选择PostgreSQL,因为它对JSON字段、地理空间数据(PostGIS扩展)的支持更原生,方便后续做空间查询分析。
  • 任务调度:APScheduler。对于这种“每隔一段时间执行一次”的定时爬取任务,APScheduler是一个轻量级且强大的选择。它可以很容易地集成到Python脚本中,实现“后台服务”式的定时抓取。
  • 部署与监控:Docker + 日志。将整个爬虫应用容器化,便于在不同环境部署。使用Python内置的logging模块记录详细日志,包括每次抓取的时间、获取的数据条数、遇到的异常等,这是后期排查问题的生命线。

架构设计图(文字描述):整个系统运行在一个定时调度器(APScheduler)的控制下。调度器每隔N分钟(如5分钟)触发一次爬取任务。任务执行器首先会向目标数据接口或页面发起HTTP请求,获取原始数据(JSON/HTML)。接着,数据解析器将原始数据转化为结构化的Python对象(字典列表)。然后,数据清洗器会处理缺失值、格式化时间字段、统一坐标格式等。清洗后的数据被送入数据存储层,与数据库中的已有记录进行比对(通过发震时刻和位置判断是否为新事件),只插入新增的地震事件。同时,整个过程的每一步都会生成日志。系统还需要一个简单的健康检查机制,比如连续多次抓取失败则发送报警通知。

3. 核心环节实现:从请求到落库的完整链路

这一部分,我们深入到代码层面,看看每一个环节具体如何实现,以及有哪些需要特别注意的“魔鬼细节”。

3.1 请求头伪装与会话管理

这是绕过基础反爬机制的第一步。一个过于简单的请求头就像在脸上写着“我是爬虫”。

import requests from datetime import datetime import time class EarthquakeSpider: def __init__(self): self.session = requests.Session() # 精心构造请求头,模仿一个常见的浏览器 self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'application/json, text/javascript, */*; q=0.01', # 表明希望接收JSON 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate', # 接受压缩,节省带宽 'Connection': 'keep-alive', 'Referer': 'http://www.ceic.ac.cn/', # 正确设置来源页,非常重要 'X-Requested-With': 'XMLHttpRequest', # 暗示这是一个Ajax请求 } self.session.headers.update(self.headers) def fetch_data(self, url, params=None): """发起网络请求,获取原始数据""" try: # 添加随机延迟,模拟人类操作,避免请求过于频繁 time.sleep(2 + random.random()) response = self.session.get(url, params=params, timeout=15) response.raise_for_status() # 如果状态码不是200,抛出HTTPError异常 # 检查返回内容类型,优先按JSON解析 content_type = response.headers.get('Content-Type', '') if 'application/json' in content_type: return response.json() else: # 如果不是JSON,再按文本处理,后续可能用HTML解析 return response.text except requests.exceptions.RequestException as e: self.logger.error(f"请求失败: {url}, 错误: {e}") return None

注意事项

  • Referer字段非常重要。很多服务器会校验请求是否来自其站内页面。将其设置为数据网站的首页或列表页地址,能大大提高成功率。
  • User-Agent可以准备一个列表,定期轮换,但对于地震台网这种公益网站,一个稳定的、常见的UA通常就足够了,频繁更换反而可疑。
  • 超时设置:一定要设置timeout参数(如15秒)。网络环境复杂,没有超时控制的爬虫很容易因为某个请求卡死而耗尽资源。
  • 异常处理:网络请求的每一步都可能出错(连接超时、DNS解析失败、服务器返回404/500等)。必须用try...except包裹,并进行日志记录,不能让整个程序因为一次请求失败就崩溃。

3.2 数据解析与清洗

假设我们找到了一个返回JSON的接口,解析工作就变得非常简单。但清洗工作同样重要,它决定了入库数据的质量。

import pandas as pd from dateutil import parser # 用于灵活解析各种格式的时间字符串 def parse_and_clean(json_data): """解析JSON数据并清洗""" if not json_data or 'data' not in json_data: return [] raw_items = json_data['data'] # 假设数据在‘data’字段下 cleaned_events = [] for item in raw_items: try: event = {} # 1. 解析时间 (字段名可能是‘O_TIME’, ‘origintime’等,需根据实际情况调整) time_str = item.get('O_TIME') if time_str: # 原始时间字符串可能为‘2023-10-27 08:15:32’,使用dateutil.parser更稳健 event['origin_time'] = parser.parse(time_str).strftime('%Y-%m-%d %H:%M:%S') else: continue # 没有时间的记录无效,跳过 # 2. 解析震级 (字段名可能是‘M’, ‘magnitude’) mag_str = item.get('M', '0.0') try: event['magnitude'] = float(mag_str) except ValueError: event['magnitude'] = 0.0 # 解析失败,设为0,但记录日志 self.logger.warning(f"震级解析失败: {mag_str}") # 3. 解析经纬度和深度 event['latitude'] = float(item.get('EPI_LAT', 0)) event['longitude'] = float(item.get('EPI_LON', 0)) event['depth'] = float(item.get('EPI_DEPTH', 0)) # 单位通常是千米 # 4. 解析位置描述 event['location'] = item.get('LOCATION_C', '').strip() # 5. 生成唯一标识(例如:时间+经纬度的哈希) # 这是一个关键步骤,用于后续去重 unique_id = hashlib.md5(f"{event['origin_time']}_{event['latitude']}_{event['longitude']}".encode()).hexdigest() event['event_id'] = unique_id cleaned_events.append(event) except Exception as e: self.logger.error(f"解析单条数据时出错: {item}, 错误: {e}") continue # 单条解析失败不影响其他数据 return cleaned_events

清洗要点

  • 时间格式化:源数据时间格式可能不统一,使用dateutil.parser可以处理大多数情况,最终统一为数据库友好的格式(如YYYY-MM-DD HH:MM:SS)。
  • 数值型字段:震级、经纬度、深度需转换为float类型,并处理可能的缺失值(如设为None或默认值)。
  • 文本字段:位置描述等信息去除首尾空格。
  • 唯一标识:为每条地震事件生成一个唯一ID至关重要。通常使用发震时刻和震中经纬度组合后取哈希值。这是后续去重和更新的依据。
  • 异常捕获:在清洗每条数据时进行try...except,防止一条脏数据导致整批数据丢失。

3.3 数据存储与去重策略

数据清洗好后,就要存入数据库。这里的关键是“去重插入”——只插入数据库中不存在的新事件。

import sqlalchemy from sqlalchemy import create_engine, Column, String, Float, DateTime, Text from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from sqlalchemy.dialects.postgresql import insert Base = declarative_base() class EarthquakeEvent(Base): __tablename__ = 'earthquake_events' id = Column(String(32), primary_key=True) # 对应我们生成的event_id origin_time = Column(DateTime, nullable=False, index=True) # 加索引便于按时间查询 magnitude = Column(Float) latitude = Column(Float) longitude = Column(Float) depth = Column(Float) location = Column(Text) created_at = Column(DateTime, default=datetime.utcnow) # 数据创建时间 class DataStorage: def __init__(self, db_url='postgresql://user:password@localhost/earthquake_db'): self.engine = create_engine(db_url) Base.metadata.create_all(self.engine) # 创建表(如果不存在) self.Session = sessionmaker(bind=self.engine) def upsert_events(self, events): """插入或更新事件,基于主键(event_id)去重""" if not events: return 0 session = self.Session() inserted_count = 0 try: # 使用SQLAlchemy Core的Insert...on_conflict_do_nothing语法(PostgreSQL) # 这种方式比先查询再插入效率高得多 stmt = insert(EarthquakeEvent.__table__).values(events) # 如果主键冲突(即event_id已存在),则什么都不做(忽略) stmt = stmt.on_conflict_do_nothing(index_elements=['id']) result = session.execute(stmt) session.commit() inserted_count = result.rowcount # 返回实际插入的行数 self.logger.info(f"成功插入 {inserted_count} 条新地震事件。") except Exception as e: session.rollback() self.logger.error(f"数据入库失败: {e}") finally: session.close() return inserted_count

存储策略详解

  • 表结构设计:除了基本的地震参数,created_at字段记录了数据被抓取到我们数据库的时间,便于追溯。origin_time字段建立索引,因为按时间范围查询是最常见的操作。
  • 去重机制:这是爬虫存储层的核心。我们使用数据库的“唯一约束冲突忽略”机制来实现高效去重。将我们生成的event_id设为主键,当执行插入时,如果id已存在,数据库会自动忽略这条插入,而不会报错。这比“先查询是否存在,再决定是否插入”的传统方式性能高出一个数量级,尤其是在高频抓取时。
  • 使用ORM与Core结合:SQLAlchemy的ORM(对象关系映射)用于定义表结构和简单的查询。但在进行批量插入/更新(upsert)时,使用SQLAlchemy Core的表达式语言通常性能更好,也更灵活。
  • 事务管理:使用try...except...finally确保数据库会话被正确关闭,发生错误时进行回滚,保证数据一致性。

4. 反爬应对与健壮性提升

即使是公益网站,也可能存在一些基本的访问频率限制或技术屏障。我们的爬虫必须足够“礼貌”和“健壮”。

4.1 频率控制与随机化

控制请求频率是网络爬虫的基本礼仪,也是对自身稳定性的保护。

import random import time class PoliteRequester: def __init__(self, base_delay=3.0, random_range=2.0): self.base_delay = base_delay # 基础延迟秒数 self.random_range = random_range # 随机延迟范围 self.last_request_time = 0 def wait(self): """根据上次请求时间,计算并等待合适的间隔""" elapsed = time.time() - self.last_request_time wait_time = self.base_delay + random.uniform(0, self.random_range) if elapsed < wait_time: time.sleep(wait_time - elapsed) self.last_request_time = time.time() # 在爬虫的fetch_data方法调用前,先执行requester.wait()

策略解析:固定的延迟(如每秒一次)仍然容易被识别为机器行为。加入随机因子(random_range)使得请求间隔时间在[base_delay, base_delay+random_range]之间波动,更接近人类操作的不确定性。

4.2 代理IP池的备用方案

对于访问量极大或限制非常严格的网站,可能需要使用代理IP。但对于中国地震台网,在遵守合理频率的前提下,通常不需要。不过,作为一个健壮的系统,我们可以预留这个接口。

class ProxyManager: def __init__(self, proxy_list=None): self.proxies = proxy_list or [] # 格式: ['http://user:pass@host:port', ...] self.current_index = 0 def get_proxy(self): if not self.proxies: return None # 不使用代理 proxy = self.proxies[self.current_index] self.current_index = (self.current_index + 1) % len(self.proxies) return {'http': proxy, 'https': proxy} # 在requests.get中传入proxies参数 # response = session.get(url, proxies=proxy_manager.get_proxy(), timeout=15)

注意事项:免费代理IP质量极不稳定,延迟高、存活时间短。如果必须使用,建议搭建一个简单的代理IP健康检查机制,定期测试可用性,并优先考虑可靠的付费代理服务。

4.3 异常重试与电路熔断

网络请求充满不确定性,一次失败不代表任务终结。我们需要一个重试机制。

from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type import requests.exceptions @retry( stop=stop_after_attempt(3), # 最多重试3次 wait=wait_exponential(multiplier=1, min=2, max=10), # 等待时间指数增长 (2s, 4s, 8s...) retry=retry_if_exception_type((requests.exceptions.ConnectionError, requests.exceptions.Timeout)), reraise=True # 重试次数用尽后,抛出原始异常 ) def fetch_data_with_retry(url, session): """带重试机制的请求函数""" return session.get(url, timeout=15)

这里使用了tenacity这个强大的重试库。它允许我们定义复杂的重试策略:例如,只对连接错误和超时进行重试;重试次数最多3次;每次重试的等待时间呈指数级增长(避免在服务器临时故障时加剧其压力)。如果重试耗尽仍失败,则向上抛出异常,由外层的任务调度器或监控系统捕获。

4.4 日志记录与监控告警

日志是爬虫的“黑匣子”,没有详细的日志,线上问题排查将如同大海捞针。

import logging import sys def setup_logger(name): logger = logging.getLogger(name) logger.setLevel(logging.INFO) # 控制台处理器 ch = logging.StreamHandler(sys.stdout) ch.setLevel(logging.INFO) # 文件处理器(按日期滚动) from logging.handlers import TimedRotatingFileHandler fh = TimedRotatingFileHandler('earthquake_spider.log', when='midnight', interval=1, backupCount=7) fh.setLevel(logging.INFO) # 日志格式 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) fh.setFormatter(formatter) logger.addHandler(ch) logger.addHandler(fh) return logger # 在爬虫类中初始化 self.logger = setup_logger('EarthquakeSpider')

日志要点

  • 分级记录INFO级别记录常规操作(如“开始抓取”、“成功插入N条数据”);WARNING记录可恢复的异常(如“某字段解析失败”);ERROR记录严重错误(如“数据库连接失败”、“连续抓取失败”)。
  • 输出到文件和控制台:方便开发时查看,也便于生产环境归档。
  • 日志轮转:使用TimedRotatingFileHandler可以避免日志文件无限增大,自动按天切割并保留最近7天的日志。
  • 监控告警:可以写一个简单的监控脚本,定期扫描日志文件中的ERROR信息,或者检查数据库在最近一段时间内是否有新数据入库。如果没有,则通过邮件、钉钉、企业微信等渠道发送报警通知。

5. 任务调度与自动化部署

一个完整的爬虫系统需要能自动、持续地运行。

5.1 基于APScheduler的定时调度

from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.interval import IntervalTrigger def main_job(): """主要的抓取任务""" spider = EarthquakeSpider() storage = DataStorage() logger = setup_logger('Scheduler') logger.info("开始执行地震数据抓取任务...") try: # 1. 获取数据 data = spider.fetch_data(target_url) if not data: logger.warning("本次未获取到数据。") return # 2. 解析清洗 events = spider.parse_and_clean(data) # 3. 存储入库 count = storage.upsert_events(events) logger.info(f"任务完成,新增{count}条记录。") except Exception as e: logger.error(f"抓取任务执行失败: {e}", exc_info=True) # exc_info=True会打印堆栈跟踪 if __name__ == '__main__': scheduler = BlockingScheduler() # 每5分钟执行一次 trigger = IntervalTrigger(minutes=5) scheduler.add_job(main_job, trigger, id='earthquake_crawl_job') logger.info("地震数据爬虫调度器已启动,按 Ctrl+C 退出。") try: scheduler.start() except (KeyboardInterrupt, SystemExit): logger.info("调度器被手动停止。")

BlockingScheduler会阻塞当前进程,适合将爬虫脚本作为一个独立的守护进程运行。在生产环境中,你可能希望使用BackgroundScheduler,以便集成到Web应用或其他更大的系统中。

5.2 使用Docker容器化部署

将整个爬虫环境打包进Docker容器,可以确保运行环境的一致性,简化部署流程。

Dockerfile示例

FROM python:3.9-slim WORKDIR /app # 安装系统依赖(如PostgreSQL客户端库) RUN apt-get update && apt-get install -y \ gcc \ libpq-dev \ && rm -rf /var/lib/apt/lists/* # 复制依赖文件并安装Python包 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # 复制应用代码 COPY . . # 设置环境变量(如数据库连接字符串) # ENV DATABASE_URL=postgresql://... # 运行命令 CMD ["python", "scheduler_main.py"]

docker-compose.yml示例(如果包含数据库):

version: '3.8' services: postgres: image: postgres:13 environment: POSTGRES_DB: earthquake_db POSTGRES_USER: user POSTGRES_PASSWORD: your_strong_password volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" crawler: build: . depends_on: - postgres environment: DATABASE_URL: postgresql://user:your_strong_password@postgres/earthquake_db restart: unless-stopped # 容器意外退出时自动重启 logging: driver: "json-file" options: max-size: "10m" max-file: "3" volumes: postgres_data:

使用docker-compose up -d即可一键启动数据库和爬虫服务。restart: unless-stopped保证了服务的持续运行。

6. 数据应用与扩展思路

数据抓取下来不是终点,而是起点。有了稳定、干净的数据流,我们可以做很多事情。

6.1 基础可视化:实时地震地图

使用FoliumPlotly等库,可以轻松地将地震数据展示在地图上。一个简单的思路是:定期(如每小时)从数据库中查询最近24小时内的地震事件,根据震级大小决定地图上标记点的半径和颜色,生成一个静态的HTML地图页面,并通过Web服务器(如Nginx)对外提供访问。

import folium from sqlalchemy import create_engine import pandas as pd def generate_map(): engine = create_engine('postgresql://...') # 查询最近24小时的数据 df = pd.read_sql(""" SELECT latitude, longitude, magnitude, location, origin_time FROM earthquake_events WHERE origin_time > NOW() - INTERVAL '24 hours' ORDER BY origin_time DESC """, engine) # 创建以中国为中心的地图 m = folium.Map(location=[35, 105], zoom_start=4) for _, row in df.iterrows(): # 震级越大,标记点越大,颜色越深(如红色) radius = row['magnitude'] * 2 color = 'red' if row['magnitude'] >= 5.0 else 'orange' if row['magnitude'] >= 3.0 else 'blue' popup_text = f""" 时间: {row['origin_time']}<br> 震级: {row['magnitude']}<br> 位置: {row['location']}<br> 深度: {row.get('depth', 'N/A')} km """ folium.CircleMarker( location=[row['latitude'], row['longitude']], radius=radius, color=color, fill=True, fillOpacity=0.6, popup=popup_text ).add_to(m) m.save('/var/www/html/earthquake_map.html') # 保存到Web服务器目录

6.2 简单分析:地震活动性统计

利用pandas和数据库的聚合查询,可以轻松实现一些统计分析:

  • 每日/每周/每月地震频次统计
  • 不同震级区间的分布统计
  • 主要地震带的活跃度分析(如环太平洋地震带、欧亚地震带)。
  • 震源深度分布直方图

这些统计结果可以定期生成报告(如CSV文件或简单的HTML页面),帮助快速把握全球地震活动态势。

6.3 扩展方向:预警与集成

  • 阈值告警:在爬虫解析数据后,立即判断震级是否超过某个阈值(例如,周边地区5.0级以上,全球7.0级以上),如果超过,则立即触发告警(发送邮件、短信或调用消息机器人API)。
  • 数据API服务:搭建一个简单的Flask或FastAPI应用,将数据库中的数据通过RESTful API对外提供,供其他系统或小程序调用。可以设计查询接口,如按时间范围、地理位置边界、震级范围查询地震事件。
  • 与GIS系统集成:将地震数据表与PostGIS扩展结合,实现更复杂的空间查询,例如“查找过去一个月内,距离某城市200公里范围内所有3级以上地震”。

7. 常见问题与排查实录

在实际运行中,你几乎一定会遇到下面这些问题。这里是我的排查笔记。

7.1 问题:突然抓不到数据了,返回空列表或错误页面。

排查步骤

  1. 手动访问:第一时间用浏览器打开目标页面,确认网站是否可正常访问,数据格式是否已变更。这是最直接有效的方法。
  2. 检查日志:查看爬虫日志中的请求URL和响应状态码。如果是403 Forbidden429 Too Many Requests,说明触发了反爬。
  3. 对比请求头:使用浏览器开发者工具,抓取一次正常访问的请求,仔细对比你的爬虫请求头与浏览器请求头的差异。重点检查User-Agent,Referer,Cookie,Accept等字段。网站可能加强了对Referer或特定Cookie的校验。
  4. 分析页面结构:如果之前是解析HTML,现在页面结构可能变了。重新分析DOM树,更新XPath或CSS选择器。
  5. 寻找新接口:再次使用开发者工具抓包,看是否有新的JSON接口出现。数据源迁移接口是常有的事。

我的案例:有一次,网站将数据从直接渲染在HTML中,改为了通过JavaScript动态加载。原来的解析方法立刻失效。解决方案是:要么使用SeleniumPlaywright这类能执行JS的浏览器自动化工具;要么更仔细地抓包,找到了一个新的、带有时效性Token的Ajax接口。我选择了后者,因为效率更高。

7.2 问题:数据解析出错,某些字段为None或格式异常。

排查步骤

  1. 打印原始数据:在解析函数中,将出错的单条item原始数据完整打印到日志或控制台。
  2. 检查字段名:确认你代码中使用的字段名(如item.get('M'))是否与JSON响应中的键名完全一致(大小写、下划线)。网站更新可能微调了字段名。
  3. 处理数据多样性:有些地震事件可能缺少某些字段(如深度depth未知)。你的清洗代码必须能优雅地处理缺失值,为其设置合理的默认值(如None0),而不是直接崩溃。
  4. 类型转换容错:在将字符串转为floatint时,务必使用try...except包裹,因为源数据可能是"-""null"或空字符串。

7.3 问题:数据库连接失败或插入速度慢。

排查步骤

  1. 检查连接字符串:确认数据库IP、端口、用户名、密码、数据库名是否正确。网络策略(如云服务器的安全组)是否允许访问。
  2. 检查数据库状态:登录数据库,查看服务是否运行,磁盘空间是否已满。
  3. 优化插入:如果一次性插入的数据量很大(比如补全历史数据),使用上面提到的insert().on_conflict_do_nothing()批量操作,而不是逐条INSERT。对于MySQL,可以使用INSERT IGNORE INTO ...ON DUPLICATE KEY UPDATE语法。
  4. 建立索引:确保在经常用于查询和去重比对的字段上建立了索引(如origin_time,event_id)。但注意,索引会降低插入速度,需权衡。

7.4 问题:爬虫运行一段时间后内存占用越来越高。

排查步骤

  1. 检查循环引用:在长时间运行的爬虫中,如果创建了大量对象且存在循环引用,Python的垃圾回收器可能无法及时释放内存。确保在大的循环中,临时变量能及时被回收。
  2. 使用生成器:如果处理的数据流很大,考虑使用生成器(yield)来逐条处理数据,而不是一次性将所有数据加载到内存的列表中。
  3. 分批次处理:对于批量补全历史数据这种任务,一定要分页或分批次请求和插入,比如一次处理1000条。
  4. 监控工具:使用memory_profiler等工具对代码进行逐行内存分析,找到内存泄漏的元凶。

这个项目从构思到稳定运行,我花了大约一周的业余时间。最大的感触是,爬虫工程的难点往往不在“爬”本身,而在于如何让这个自动化系统像钟表一样可靠、持续地运行下去,并能优雅地应对各种外部变化。它考验的是开发者的系统工程思维和问题排查能力。希望这份详细的复盘,能帮你少走些弯路,更快地搭建起属于自己的地球脉搏监听器。数据在手,接下来的分析和应用,就是展现你创造力的时刻了。