用Python读取和处理NASA公开API数据
最近因为一个研究小任务,需要拉一批NASA的公开数据来做分析。本来想着直接从官网下载数据集,结果发现NASA的数据接口做得比想象中规整得多,干脆直接用Python写了一套读取和处理流程。这套东西跑通之后,我觉得非常值得整理出来分享,因为它其实不光是NASA适用——NASA的公开API覆盖了地球观测、天文图片、近地天体、火星探测器照片、太空天气等十几个方向,而且全部免费、无需付费、注册即用。对于想练手Python爬虫、数据清洗、数据分析的同学来说,NASA的API可以说是一套绝佳的练手数据源。
这篇文章我会按照实际操作的顺序来写:先讲清楚NASA API是什么、能拿到什么数据,然后带你完成注册、申请API Key、用Python请求数据、解析JSON、清洗整理、最后落库和可视化。整个流程都是我用真实代码跑通过再整理出来的,你照着操作,基本不会卡壳。
1. 内容整体设计与思路拆解
1.1 NASA API生态概览:不只是天文图片
很多人一听到NASA API,第一反应是“每天一张天文图片”(APOD),实际上NASA开放的数据接口远不止这些。目前NASA Open APIs官方提供的接口按主题划分,比较常用的有这么几类:
| API名称 | 数据内容 | 典型应用场景 |
|---|---|---|
| APOD(Astronomy Picture of the Day) | 每日天文图片及说明 | 图片展示、天文科普站点 |
| NEO(Near Earth Object) | 近地小行星轨道数据 | 天文爱好者的轨道追踪、数据分析 |
| EONET(Earth Observatory Natural Event Tracker) | 全球自然灾害事件 | 灾害监测、地理可视化 |
| Mars Rover Photos | 火星探测器拍摄的照片 | 图像处理、物体识别数据集构建 |
| Exoplanet Archive | 系外行星参数 | 天文科研、数据建模 |
| TLE(Two-Line Element Set) | 卫星轨道两行根数 | 卫星追踪、航天工程入门 |
| Insight Mars Weather | 火星天气数据 | 气象数据分析、可视化教学 |
我当时做那个任务用到的就是NEO近地天体数据和火星天气数据这两类。选择这两类的逻辑很简单:NEO数据字段结构清晰、字段多,非常适合用来做数据清洗和特征工程练习;火星天气数据则带有时间序列特征,可以做趋势分析和可视化。如果你只是想快速上手,建议优先选NEO或者APOD,因为这两个接口的返回格式最简单,踩坑最少。
1.2 为什么选择API方式而非直接下载数据集
NASA官网上其实也提供大量数据集的直接下载入口,有的甚至可以直接下载CSV或Excel文件。那为什么我还是建议通过API来拿数据?原因有三个:
第一,API拿到的数据是实时的。以大雾山为例,直接下载的历史数据集中,时间滞后可能高达几个月到一年,而API返回的数据往往是最新状态,对于需要时效性的分析场景非常重要。
第二,API支持参数化查询。你可以在请求时指定日期范围、分类条件、阈值等参数,让数据源先做一层过滤,避免把整个大文件拉下来后再本地处理。这一点在数据量大的时候特别关键。
第三,API的返回格式是统一的结构化JSON。相比从PDF报告或者嵌套网页里抓数据,JSON解析在Python里几乎是零成本操作,直接json.loads()就能用,无需额外做一大堆页面解析清洗工作。
当然API方式也有缺点,最典型的限制就是请求速率配额。NASA的免费API Key限制是每小时最多1000次请求,每天最多1000次——注意不是每小时重置,而是按小时和天双重限制。普通个人使用完全够用,但如果你要做大规模批量抓取,就需要注意控制请求频率,合理规划抓取任务。
1.3 整体技术方案选型
整套数据读取和处理流程,我最后确定的技术栈是这样的:
- Python 3.10+作为主语言,主要考虑到类型注解和
match语法糖在数据处理时很好用; requests库负责HTTP请求,它是Python生态里最成熟的HTTP客户端,比urllib好用太多;pandas负责数据处理,它几乎成了Python数据分析的代名词,处理表格型数据无可替代;json和time是Python标准库,分别处理JSON解析和请求限速;- 存储层用了SQLite,因为它是单文件数据库,零配置、开箱即用,适合做本地数据集的持久化;
- 可视化用了
matplotlib,虽然现在很多人用plotly或者seaborn,但matplotlib的兼容性和可控性依然是最稳的选择。
这套组合的好处是:每个库的学习成本都低、生态成熟、彼此配合默契。尤其是requests+pandas这对组合,基本成了Python数据采集和分析的黄金搭档。任何初学者照着这个方案走,踩的坑都会少很多。
2. 核心细节解析与实操要点
2.1 API Key的申请流程与细节
NASA API的注册流程是我用过的所有API服务里最简单的之一,但也正因为它简单,很多人会忽略一些细节。
注册地址是https://api.nasa.gov/,页面往下拉就能看到表单。填写信息时注意几个要点:邮箱必须是真实有效的,因为API Key会直接发到邮箱;组织名称可以随便填,个人用户填Personal Use即可;使用场景描述建议写清楚用途,比如Data analysis for personal research,这个字段主要是NASA做数据使用统计的,不影响申请结果。
提交之后,正常情况几秒到几分钟内邮箱就会收到一封标题包含Your NASA API Key的邮件,里面就是你的API Key。这里有个非常容易被忽略的点:NASA的API Key本质上是DEMO_KEY的升级版,区别在于请求配额不同。如果你不注册,直接用DEMO_KEY也能请求API,但配额极低——每小时30次,每天30次——这连做个小实验都不够。
收到API Key之后,建议立刻存到环境变量里,而不是硬编码在代码文件中。一方面,代码公开分享时不会泄露密钥;另一方面,使用环境变量管理密钥也更符合工程规范。在Linux/macOS下,直接export NASA_API_KEY='你的key';Windows下可以用系统环境变量配置;如果用IDE跑代码,建议在IDE的环境变量配置里加,一劳永逸。
2.2 NASA API返回的数据结构与字段解读
以NEO近地天体接口为例,它的API地址是这样的:
https://api.nasa.gov/neo/rest/v1/neo/browse注意,这个browse端点是直接返回全部近地天体数据,每次返回20条左右,按页加载。另一个常用的端点是按日期查询:
https://api.nasa.gov/neo/rest/v1/feed?start_date=2024-01-01&end_date=2024-01-07&api_key=你的key这个feed端点会返回指定日期范围内所有近地天体的数据,返回的JSON结构大致如下:
{ "near_earth_objects": { "2024-01-01": [ { "id": "123456", "name": "123456 (2002 AB)", "absolute_magnitude_h": 22.5, "estimated_diameter": { "kilometers": { "estimated_diameter_min": 0.1, "estimated_diameter_max": 0.2 } }, "is_potentially_hazardous_asteroid": false, "close_approach_data": [ { "close_approach_date": "2024-01-01", "relative_velocity": { "kilometers_per_second": "15.5" }, "miss_distance": { "kilometers": "5000000" } } ] } ] } }这个结构非常经典:外部是一个字典,键是日期,值是该日期所有天体的列表;每个天体的信息又是一个多层嵌套字典。用pandas处理这种嵌套JSON时,最常见的做法是用json_normalize把层级结构整理成扁平的DataFrame,然后再做字段筛选。
字段方面,最值得关注的几项:is_potentially_hazardous_asteroid表示是否有潜在威胁,estimated_diameter.kilometers表示估算直径范围,close_approach_data数组里的miss_distance表示与地球的最近距离,relative_velocity表示相对速度。这些字段天然适合做数据分析,比如分析“潜在威胁天体的轨道速度分布”,或者“近地天体与地球距离的分布规律”。
2.3 请求频率控制与错误处理的关键经验
NASA API最让人头疼的就是限流限制。DEMO_KEY配额是每小时30次,拥有正式Key后每小时1000次,但如果你不加控制地疯狂请求,API照样会返回429 Too Many Requests。
我在实际操作中踩过一个大坑:当时为了抓取一整年的NEO数据,写了一个循环每天请求一次feed端点,运行到一半就开始收到429错误。排查下来才发现问题出在哪——feed端点虽然一次能返回7天的数据,但我代码里设置了end_date动态增长,在某些日期边界条件下重复请求了同一天的数据,白白浪费了配额。
正确的做法是在循环中强制加入延时,每次请求之间至少间隔1秒,并且在代码里维护一个已请求过的日期集合,避免重复请求。Python里最简单的实现方式就是time.sleep(1),如果要做得更精细,可以用ratelimit库的sleep_and_retry装饰器,按分钟控制请求数。
处理API响应时,我建议做统一的三层容错:
- 检查HTTP状态码,
200代表正常,429代表限流,500代表服务端错误; - 对
429做退避重试,第一次等待30秒,第二次等60秒,仍失败就放弃并记录日志; - 对JSON解析做异常处理,防止NASA偶尔返回HTML错误页导致
json.loads崩溃。
3. 实操过程与核心环节实现
3.1 环境准备与依赖安装
开始写代码之前先把环境准备好。如果你还没装Python,推荐直接从官方网站下载安装包,目前稳定版本是3.12或3.10,安装时务必勾选Add Python to PATH选项,否则命令行里找不到python命令会很崩溃。
依赖安装非常简单,打开终端执行:
pip install requests pandas matplotlib如果你用Anaconda或者Miniconda管理环境,也可以用:
conda install requests pandas matplotlib装完之后建议快速验证一下版本,避免出现版本不兼容的问题:
import requests import pandas as pd import matplotlib print(requests.__version__) print(pd.__version__) print(matplotlib.__version__)我在实操中遇到过一个小坑:pandas升级到2.0之后,json_normalize的默认参数行为有一些变化,部分旧代码会报KeyError。所以如果你之前用的是1.x版本,升级后建议顺手跑一下官方迁移指南里的检查项。
3.2 三步完成API请求与数据提取
这是整套流程中最核心的环节。我用最少的代码来实现一个“请求-解析-存入DataFrame”的完整链路。
第一步,构造请求URL。NASA API的URL结构非常统一,基本都是https://api.nasa.gov/xxx?参数1=值&参数2=值&api_key=你的Key。这里要用到urllib.parse.urlencode来编码参数,避免特殊字符导致请求失败。
第二步,发送请求并解析响应。用requests.get时,建议加上timeout参数,我一般设置30秒,防止网络异常导致程序卡死。
第三步,将JSON转换为pandas的DataFrame。这一步推荐用json_normalize。
我把这三步封装成了一个函数,完整代码如下:
import os import requests import pandas as pd from urllib.parse import urlencode from datetime import date, timedelta API_KEY = os.environ.get("NASA_API_KEY") def fetch_neo_data(start_date, end_date): base_url = "https://api.nasa.gov/neo/rest/v1/feed" params = { "start_date": start_date, "end_date": end_date, "api_key": API_KEY, } url = f"{base_url}?{urlencode(params)}" resp = requests.get(url, timeout=30) resp.raise_for_status() data = resp.json() records = [] for day, asteroids in data["near_earth_objects"].items(): for ast in asteroids: record = { "date": day, "id": ast["id"], "name": ast["name"], "diameter_min_km": ast["estimated_diameter"]["kilometers"]["estimated_diameter_min"], "diameter_max_km": ast["estimated_diameter"]["kilometers"]["estimated_diameter_max"], "is_hazardous": ast["is_potentially_hazardous_asteroid"], "velocity_kps": ast["close_approach_data"][0]["relative_velocity"]["kilometers_per_second"], "miss_distance_km": ast["close_approach_data"][0]["miss_distance"]["kilometers"], } records.append(record) return pd.DataFrame(records) # 测试:请求最近7天数据 today = date.today() df = fetch_neo_data(today.strftime("%Y-%m-%d"), (today - timedelta(days=7)).strftime("%Y-%m-%d")) print(df.head())这里有几处细节需要特别说明:
close_approach_data是列表类型,正常情况下每个天体只有一条数据,但用[0]取第一条属于“偷懒”写法。严谨的做法是先判断列表长度再取,否则遇到空列表会直接IndexError,我在中途就遇到过一颗太阳同步卫星的数据,close_approach_data列表长度为0,导致程序崩溃。加个判断就好:
if ast["close_approach_data"]: velocity = ast["close_approach_data"][0]["relative_velocity"]["kilometers_per_second"] else: velocity = Noneresp.raise_for_status()这行也很重要。如果HTTP请求返回4xx或5xx状态码,它会直接抛出异常,省去了手动判断状态码的麻烦。唯一的例外是429限流,它会抛HTTPError而不是静默返回,这正好提醒我们去处理限流问题。
3.3 批量抓取与增量更新机制
单个时间范围内的数据很容易抓,但如果要构建一个长期维护的数据集,就需要考虑批量抓取和增量更新。
我设计的增量更新机制核心思路是:先检查本地SQLite数据库里已有的最大日期,再从最大日期的次日开始抓取。
import sqlite3 conn = sqlite3.connect("nasa_neo.db") cursor = conn.cursor() # 查询已有数据的最大日期 cursor.execute("SELECT MAX(date) FROM neo_asteroids") max_date = cursor.fetchone()[0] # 如果没有数据,从一年前开始抓 if max_date is None: start = date.today() - timedelta(days=365) else: start = datetime.strptime(max_date, "%Y-%m-%d").date() + timedelta(days=1)增量更新时需要注意一个边界条件:日期参数必须使用字符串格式的YYYY-MM-DD。pandas默认会用datetime.date类型,直接传进去会导致URL编码异常。我用date.today().strftime("%Y-%m-%d")来保证格式统一。
抓到的数据入库也很简单,pandas的to_sql方法直接可以写入SQLite:
df.to_sql("neo_asteroids", conn, if_exists="append", index=False)这里有一个高概率踩坑点:to_sql默认不会处理主键冲突。如果两条记录的id重复,第二次写入会在数据库中产生重复记录。解决方式是在建表时定义主键,然后写入时用INSERT OR REPLACE语义。手动建表的SQL大概长这样:
CREATE TABLE IF NOT EXISTS neo_asteroids ( id TEXT PRIMARY KEY, date TEXT, name TEXT, diameter_min_km REAL, diameter_max_km REAL, is_hazardous INTEGER, velocity_kps REAL, miss_distance_km REAL );创建好表结构后,写入时用INSERT OR REPLACE INTO ...来替代to_sql的简单追加,这样才能保证增量更新不产生脏数据。
3.4 数据清洗与特征工程实操
拿到的原始JSON虽然已经是结构化数据,但距离“可分析”还有一段距离。我在清洗阶段主要做这几件事:
处理缺失值。NEO数据中,部分天体的estimated_diameter范围字段可能缺失,导致diameter_min_km和diameter_max_km为NaN。对于这种数值型缺失,我选择用该字段的中位数填充,因为小行星直径分布大概率是长尾的,中位数比均值更稳健。
处理字段类型。is_hazardous在JSON里是布尔值,存入SQLite后自动变成0/1整数,但如果要读回DataFrame,需要显式转换类型。velocity_kps是字符串类型,需要转成float才能计算。
构造新特征。最有价值的特征之一是天体直径的估算值,通常取min和max的几何平均值(sqrt(min * max)),因为小行星的实际尺寸往往更接近几何均值而非算术均值。我还构造了一个“威胁等级评分”的简易规则:潜在威胁且距离小于一定阈值的标记为高危。
清洗之后的代码如下:
df["velocity_kps"] = df["velocity_kps"].astype(float) df["diameter_est_km"] = (df["diameter_min_km"] * df["diameter_max_km"]).apply(lambda x: x ** 0.5) df["threat_level"] = df.apply( lambda row: "high" if row["is_hazardous"] and float(row["miss_distance_km"]) < 5000000 else ("medium" if row["is_hazardous"] else "low"), axis=1 )3.5 可视化分析示例
数据清洗完毕,随便做一张图都能看到有趣的现象。我拿了一段时间的近地天体数据,画了一个“天体直径分布”的直方图:
import matplotlib.pyplot as plt plt.figure(figsize=(10, 6)) plt.hist(df["diameter_est_km"].dropna(), bins=50, color="steelblue", edgecolor="white") plt.xlabel("Estimated Diameter (km)") plt.ylabel("Frequency") plt.title("Distribution of NEO Estimated Diameters") plt.grid(True, alpha=0.3) plt.tight_layout() plt.savefig("neo_diameter_distribution.png", dpi=150)这张图画出来之后你会发现,近地天体的直径分布呈现强烈的长尾特征:绝大部分天体的直径不到1公里,极少数直径超过5公里。这个规律对理解近地天体的风险评估很有意义——真正需要担心的其实不是数量多的小天体,而是数量稀少的大天体。
如果你是第一次跑这个数据分析流程,建议先用APOD接口练手,因为它的JSON结构最简单,拿到的图片URL直接可以下载。但如果你想要有点挑战性,NEO数据是更好的选择,因为字段丰富、结构嵌套,能完整走一遍“请求-解析-清洗-分析”全流程。
4. 常见问题与排查技巧实录
4.1 请求限流(429)的应对策略
这是NASA API使用中遇到频率最高的问题。现象是代码跑到中途,突然报出如下异常:
HTTPError: 429 Client Error: Too Many Requests for url排查思路分三步走:
- 先确认自己当前用的是不是
DEMO_KEY,如果是,立刻换正式Key; - 检查代码里是否有循环重复请求同一URL的情况,比如日期参数没有随循环更新;
- 在请求之间加入延时,最低1秒,建议2秒更稳。
我给自己的代码里加了一个简单的“带退避的重试机制”:
import time def fetch_with_retry(url, retries=3): for attempt in range(retries): resp = requests.get(url, timeout=30) if resp.status_code == 200: return resp.json() elif resp.status_code == 429: wait_time = 30 * (attempt + 1) print(f"Rate limited, waiting {wait_time}s...") time.sleep(wait_time) else: resp.raise_for_status() raise Exception("Max retries reached")这段代码的逻辑很直白:遇到限流就多等一会儿再重试,直到成功或达到重试上限。实测下来,配合1秒间隔的请求节流,基本不会触发重试机制。
4.2 Key不生效或权限异常的排查
有时候申请的API Key明明填对了,但请求依然报403 Forbidden。这种情况通常有三个原因:
Key填写错误。检查是不是多了空格、少了前缀或者是复制粘贴过程中截断了。NASA的Key是一长串字母和数字,没有连字符,核对时可用print(len(API_KEY))看长度是否与邮件一致。
URL中参数位置错误。所有查询参数必须拼接在URL的?之后,用&分隔,API Key参数名是api_key,不是key、不是apikey。拼错一个字符就前功尽弃。
环境变量未生效。如果你把Key放在环境变量里,改了环境变量之后没有重启终端或IDE,代码读到的还是旧值。一个排查技巧是在代码开头直接打印os.getenv("NASA_API_KEY"),如果输出None,说明环境变量没有加载进来。
4.3 JSON解析错误与字段缺失的防御方案
NEO接口返回的数据有个特点:close_approach_data字段并非总是存在,部分老旧天体记录中这条数据可能为空。另外,estimated_diameter字段在某些极端情况下也可能缺失。我在代码里统一做了防御处理:
def safe_get(dict_obj, keys, default=None): for key in keys: if not isinstance(dict_obj, dict) or key not in dict_obj: return default dict_obj = dict_obj[key] return dict_obj这个函数思路很简单:逐层检查字典键是否存在,不存在就返回默认值。用这个函数替代硬编码索引,整个代码的健壮性会提升一个台阶。
4.4 通过日期范围优化API请求次数
NEO的feed端点一次最多返回7天的数据。如果你要抓取一年的数据,理论上需要约53次请求。但如果你调用browse端点,一次能拿20条记录,可以用来做全量数据的抽样。
我实际测试发现一个规律:feed端点返回的数据,即使指定7天范围,NAS内部也会做一定的“合并”处理,部分天可能数据为空。这会导致返回JSON中某些日期键缺失,用data["near_earth_objects"].items()遍历时不会报错,但如果直接通过data["near_earth_objects"]["2024-01-03"]去取就会出现KeyError。
解决方式是遍历时用dict.get(day, [])替代直接索引,这样即使某天没有数据也能正常处理。
4.5 其他API接口的常见差异
NASA不同API之间的返回结构差异巨大,我顺手整理了个简表,方便你切换接口时快速适应:
| 接口 | 返回结构特点 | 易踩坑点 |
|---|---|---|
| APOD | 单条记录,字段少而简单 | 无日期参数时的默认返回,容易忘传date参数 |
| NEO browse | 分页列表,字段嵌套深 | 嵌套字段取不到时直接报错 |
| Mars Rover Photos | 按火星探测日和相机筛选 | earth_date与sol参数互斥,两者不可同时传入 |
| EONET | 事件列表含分类和几何信息 | 分类字段是对象而非字符串 |
| Insight Mars Weather | 火星日气象集合 | 数据往往覆盖过去数天,适合趋势分析 |
我个人觉得,如果你是初学者,先从APOD入手;如果你是想练数据清洗能力,NEO是不二之选;如果你对图像数据感兴趣,Mars Rover Photos接口配合requests下载图片,半个月内就能搭出一个完整的图像分类数据集。
5. 拓展建议与进阶方向
跑到这里,整条“用Python读取和处理NASA公开API数据”的流程其实已经完整跑通了。但这套能力不该止步于此,我根据自己做数据分析的经验,给你几个可选的进阶方向。
第一个方向是构建本地数据集仓库。你可以把NEO、火星天气、天文图片等数据全部定时存入SQLite,形成个人小型数据仓库。之后所有的分析、建模、可视化都可以基于本地数据运行,不再依赖频繁网络请求。定时任务用cron或者schedule库都能轻松实现。
第二个方向是接入可视化大屏。拿到NEO数据之后,很多人的第一反应是想看“哪些天体靠近地球”。你可以把数据输出成JSON或者直接用folium库在地图上标点。folium是一个基于Leaflet的Python地图库,交互效果好,代码量小,非常适合做灾害监测类的可视化。
第三个方向是构建分类模型。NEO数据中的is_hazardous字段天然是一个二分类标签,直径、速度、距离等字段天然是特征。你完全可以把这套数据当作练手集,训练一个简单的逻辑回归或随机森林分类器,预测一个天体是否有潜在威胁。虽然这个模型本身可能没有科研价值,但对训练机器学习流程、熟悉特征工程,价值非常大。
第四个方向是自然语言处理任务。APOD接口返回的数据中包含天文图片的文字说明,这些文字说明很规范、风格统一,把若干天的数据汇总起来就是一个不错的小型文本语料库。跑个词频统计、主题模型或者简单的摘要生成,都是很好玩的实践。
我个人在实际使用中最大的体会是:NASA的API体系虽然不复杂,但它把“公开数据接口”这件本该很标准的事情做到了极致——免费、稳定、文档清晰、接口设计统一。拿它来练习Python数据处理,比用什么模拟数据、爬虫数据都靠谱得多。
最后再分享一个小技巧:如果某天API请求突然大量失败,先去看一眼NASA API的官方状态页,很多时候是服务端在升级维护,不是你代码的问题。这种时候别反复重试,稍微等一两个小时再跑,往往就恢复正常了。做数据采集,耐心往往比技巧更重要。