ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Python调用NASA公开API构建真实科学数据清洗与分析项目

用Python调用NASA公开API构建真实科学数据清洗与分析项目

经常有读者问我:想练 Python 数据分析,但电商、金融类的数据集要么付费,要么清洗起来特别费劲,有没有数据免费、接口稳定、还自带科学感的练手项目?我的答案通常是 NASA 公开 API。没错,就是那个做航天探索的研究机构,它把每日天文影像、近地小行星轨道、火星车拍摄的照片、地球高清影像等数据都做成了公开接口。你只要会用 Python 的requests发起 HTTP 请求,再用pandas做解析清洗,就能拿到一份平时根本接触不到的真实科学数据。这篇文章就围绕“用 Python 读取和处理 NASA 公开 API 数据”这条主线,从环境准备讲到清洗落盘,再讲到定时采集和踩坑排查,全程给出可直接复跑的代码。

1. 为什么拿 NASA 公开 API 当练手数据源

1.1 接口数量与内容盘点

NASA 的开放数据平台统一入口是 api.nasa.gov,所有接口共用同一个 API Key 体系,交互方式就是标准的 REST + JSON。这意味着你不需要理解复杂协议,只要会拼 URL、会解析字典,就能开始干活。目前比较常用、也最适合数据练手的接口有这几个:

接口名称数据内容典型场景
APOD 每日天文图每天一张天文影像及专业文字说明入门第一个请求、图片元数据解析
Asteroid NeoWs近地小行星的轨道、尺寸、速度、接近时间嵌套 JSON 清洗、时间序列分析
Mars Rover Photos火星车按 sol(火星日)或地球日期拍摄的照片多参数筛选、批量下载
EPIC 地球影像从卫星视角拍摄的地球多波段影像时间轴数据、图像处理
TLE 卫星轨道数据低轨卫星的两行轨道根数航天计算、时间格式处理

这些接口看起来都带着“科学光环”,但数据结构并不复杂,大多是字典套列表、列表套字典的模式,恰好是练习数据清洗的绝佳材料。尤其是 NeoWs 的返回结构,嵌套层数多、字段单位杂(公里、英里、秒差距都有),比手工编造的练习数据有意思多了。

1.2 这套数据源适合谁、能练到什么

如果你正在学 Python 数据分析,做项目最怕什么?怕数据是“假”的。NASA 的数据是科学探测产生的真实记录,清洗完之后你会发现字段之间确实存在规律。比如小行星的直径和它的运动速度、轨道周期之间有明显相关性,这种真实感是任何模拟数据集都给不了的。

这套数据源还特别适合三类人:一是刚学完requests和pandas基础语法、需要项目串联知识的新手;二是做数据可视化想找免费且版权干净的数据源的爱好者,NASA 数据公开允许非商业用途使用;三是对天文、航天感兴趣、想用代码“够到”真实航天数据的业余玩家。我自己就是从 APOD 接口开始,一步步把整套接口全跑了一遍,过程中踩的坑几乎都集中在限流、参数格式和字段结构上,后面会详细拆开讲。

2. 申请 Key 和搭建环境:提前一小时,后面省一天

2.1 注册与配额理解

访问 api.nasa.gov 后点申请键,填一个邮箱就能拿到专属 API Key。官方也提供了公共的演示密钥 DEMO_KEY,但配额非常低,大概在每小时几十次请求的级别,做严肃练习我建议直接申请正式 Key,免费、秒批,配额会高很多。

拿到 Key 后,先理解配额的含义:每个 Key 在单位时间内能发出的请求数有限,一旦超过就会返回 429 状态码,提醒你“慢一点”。很多人第一次跑批量采集任务时,连续请求几百次,结果后半段几乎全是 429,这就是没做限速管理。我的习惯是:单次请求之间至少间隔 0.5 到 1 秒,并且把抓到的数据及时缓存到本地,具体策略后面专门讲。

2.2 本地环境三件套

我的建议是用虚拟环境隔离依赖,避免不同项目的包互相冲突。新建项目目录后执行:

python3 -m venv nasa-env source nasa-env/bin/activate pip install requests pandas matplotlib python-dotenv

这里必须装的四个库各有分工:requests负责发 HTTP 请求;pandas负责把 JSON 转成规整的表格;matplotlib用来画基础图表;python-dotenv用来读取.env配置文件,把 API Key 从代码里剥离出去。如果你只想先跑通最小的例子,装requests和pandas就够了,后面两个是数据处理和可视化的加分项。

2.3 Key 别硬编码:用环境变量管理

新手最容易犯的错,就是把 API Key 直接写死在脚本里,然后不小心把脚本传到了公开仓库。这个 Key 虽然不涉及付费,但它关联你的邮箱和请求配额,被陌生人刷爆了你自己的额度就很冤了。

我常用的做法是在项目根目录建一个.env文件,内容只有一行:

NASA_API_KEY=你的密钥

然后在代码里这样读取:

import os from dotenv import load_dotenv load_dotenv() API_KEY = os.getenv("NASA_API_KEY")

这样一来,代码本身是干净的,密钥只存在本地文件里。配合.gitignore把.env排除在版本管理之外,既安全又省心。注意.env文件不要加引号,不要有多余空格,读取不到时先检查文件路径是否在 Python 的工作目录下。

3. 请求数据只是开始:从 JSON 到 DataFrame 的完整链路

3.1 先拿 APOD 跑通第一行请求

所有接口里最友好的是 APOD,返回结构简单,适合验证整条链路是否通畅。用日期参数指定某一天,代码长这样:

import requests from dotenv import load_dotenv import os load_dotenv() API_KEY = os.getenv("NASA_API_KEY") url = "https://api.nasa.gov/planetary/apod" params = { "api_key": API_KEY, "date": "2024-06-01", } resp = requests.get(url, params=params, timeout=10) data = resp.json() print(data["title"]) print(data["url"]) print(data["explanation"][:80])

这里有两个容易忽略的细节:第一,请求必须带timeout参数,否则网络异常时脚本会一直挂起,看起来像死循环;第二,params里放 API Key 而不是拼在 URL 里,requests会自动做 URL 编码,省去手工拼接的麻烦。跑通之后,把代码里的date换掉就能拿到任意日期的天文影像信息。

3.2 封装一个带重试的请求函数

跑通单次请求只是第一步。实际要批量拉数据时,网络抖动、服务端限流都会让请求失败,如果每次失败都得手动重跑,效率太低了。我习惯把请求封装成一个独立函数,内置指数退避重试:

import time import requests def nasa_get(url, params=None, max_retries=3): for attempt in range(max_retries): try: resp = requests.get(url, params=params, timeout=15) if resp.status_code == 429: wait = 2 ** attempt print(f"触发限流,等待 {wait} 秒后重试") time.sleep(wait) continue resp.raise_for_status() return resp.json() except requests.RequestException as exc: if attempt == max_retries - 1: raise wait = 2 ** attempt + 0.5 time.sleep(wait) data = nasa_get("https://api.nasa.gov/planetary/apod", {"api_key": API_KEY, "date": "2024-01-01"})

指数退避的意思是:第一次失败等 2 秒,第二次等 4 秒,第三次等 8 秒,给服务端留出恢复时间。这是控制客户端对公网 API 请求的基本礼仪,也是规范化脚本和“一把梭代码”的核心区别。注意,捕获异常时我单独处理了429,因为限流属于可恢复状态,和真的断网、DNS 解析失败性质不同。

3.3 摸清 NeoWs 的 JSON 结构再动手

APOD 结构太简单,练不到嵌套解析。真正值得上手的是小行星接口 NeoWs,返回结构长这样:

{ "near_earth_objects": { "2024-06-01": [ { "id": "123456", "name": "(2024 ABCD)", "estimated_diameter": { "kilometers": { "estimated_diameter_min": 0.12, "estimated_diameter_max": 0.28 } }, "close_approach_data": [ { "close_approach_date": "2024-06-01", "relative_velocity": { "kilometers_per_second": "5.67" }, "miss_distance": { "kilometers": "4300000.0" } } ], "is_potentially_hazardous_asteroid": false } ] } }

看到这个结构,第一反应不应该是直接写解析代码,而是先搞清楚三个关键点:第一,外层near_earth_objects是字典,键是日期字符串,值是对应当天的小行星列表,同一天的数据要按列表遍历;第二,estimated_diameter下面分公里、英里、英尺多个单位体系,每个单位体系里又有最小和最大两个值;第三,close_approach_data是列表,里面大概率只有一个元素,但严谨的脚本必须按列表处理,不能直接取下标 0。

我自己的经验是:拿到陌生接口的返回后,先打印一屏原始 JSON 看结构,别急着写字典取值。可以用print(json.dumps(data, indent=2))格式化输出,或者先把返回体保存成.json文件,用编辑器查看。这个习惯能帮你省掉大量调试时间。

4. 小行星数据清洗实战:展平、筛选、落盘与绘图

4.1 把嵌套字段展平成行记录

理解了结构之后,清洗工作就顺理成章了。把 7 天的小行星数据拉下来,然后展平成一行一条记录的表格:

url = "https://api.nasa.gov/neo/rest/v1/feed" params = { "api_key": API_KEY, "start_date": "2024-06-01", "end_date": "2024-06-07", } data = nasa_get(url, params) rows = [] for date_str, asteroids in data["near_earth_objects"].items(): for ast in asteroids: approach = ast["close_approach_data"][0] rows.append({ "date": date_str, "name": ast["name"], "diameter_km": ast["estimated_diameter"]["kilometers"]["estimated_diameter_max"], "approach_date": approach["close_approach_date"], "miss_distance_km": float(approach["miss_distance"]["kilometers"]), "velocity_kms": float(approach["relative_velocity"]["kilometers_per_second"]), "is_hazardous": ast["is_potentially_hazardous_asteroid"], }) import pandas as pd df = pd.DataFrame(rows) print(df.head())

这段代码的关键在于单位转换:接口返回的速度和距离是字符串类型,进入 DataFrame 之前必须float()转成数值,否则后续做排序、求均值时会出现类型错误。直径字段我取的是最大值,你也可以同时保留最小值和最大值,做区间分析。

4.2 单位换算与潜在危险小行星筛选

NASA 提供的单位体系非常多,不换算直接看数字很容易产生误解。比如miss_distance同时给了公里、英里、天文单位、月球距离,我通常只保留公里并用float转换。速度字段relative_velocity则同时有“公里/秒”和“公里/小时”,如果要做动能分析,必须先统一成同一套单位体系。

清洗完成后,一个很直观的分析是查看潜在危险小行星的规律:

hazardous = df[df["is_hazardous"]] print(f"潜在危险小行星数量:{len(hazardous)}") print(hazardous.sort_values("miss_distance_km", ascending=True) .head(10)[["name", "miss_distance_km", "diameter_km", "velocity_kms"]])

按“最接近地球”排序后,你会发现危险小行星里确实有不少直径较大、速度较快的个体。这里的核心训练点是:先明确分析目标,再决定筛选用哪些字段和条件,而不是把全部字段一股脑丢进 DataFrame。

4.3 统计汇总、CSV 导出和基础可视化

处理好单表之后,可以做聚合统计,比如看看这七天里每天新增发现的小行星数量:

daily_count = df.groupby("date").size() print(daily_count)

导出 CSV 也很简单:

df.to_csv("asteroids_2024_06.csv", index=False, encoding="utf-8-sig")

utf-8-sig是给 Excel 用户准备的小细节,直接双击 CSV 时不会出现中文乱码。如果只是给代码读,默认的utf-8就够了。

画图方面,用matplotlib画一个直径与速度的散点图,能直观看出两者关系:

import matplotlib.pyplot as plt plt.figure(figsize=(8, 5)) plt.scatter(df["diameter_km"], df["velocity_kms"], c=df["is_hazardous"], cmap="viridis", alpha=0.7) plt.colorbar(label="is_hazardous") plt.xlabel("直径(公里)") plt.ylabel("速度(公里/秒)") plt.title("近地小行星直径与速度分布") plt.savefig("asteroid_scatter.png", dpi=150) plt.show()

到了这一步,从接口拉数据、JSON 解析、字段清洗、表格落盘、可视化输出就形成了一个完整的数据处理闭环。我建议大家亲手把这条链路跑通,因为后续不管换成哪个接口,核心代码骨架都是这个套路。

5. 我踩过的高频报错:限流、参数和字段变动的完整排查链路

5.1 HTTP 429 限流:别硬冲,退避重试加缓存

遇到 429 太常见了。现象是脚本跑着跑着开始连续输出 429 状态码,最后直接抛异常。原因就是单位时间内请求次数超过了配额。我第一次批量拉取近 30 天小行星数据时,压根没考虑配额,脚本跑一半就全红了。

正确的排查链路是这样:先看状态码,确认是 429 而不是 500。429 表示服务端明确告诉你“请求过多”,不是你的代码逻辑错误。然后退避重试,等待时间用 2 的指数次幂递增。最后是从源头解决问题——把已经请求过的数据存到本地缓存,避免重复请求同一份数据。三步走完,限流基本就不会再成为瓶颈了。

5.2 400 参数错误:先打印实际请求 URL

参数错误是最容易自己解决的,但很多人不知道怎么看。比如小行星接口的start_date和end_date中间跨的天数最多是 7 天,超过就返回 400。再比如日期格式必须是YYYY-MM-DD,写成2024/06/01也会报错。

我排查这类问题的固定套路是:打印resp.request.url,看实际发出的 URL 长什么样。往往答案一眼就能看出来——要么日期格式不对,要么某个参数名拼错了,要么 API Key 没生效。requests的params会自动把参数编码进 URL,直接打印请求对象就能看到完整的拼接结果,这比盯着代码猜高效得多。

5.3 KeyError 字段缺失:先看原始 JSON 再写解析逻辑

另一个高频坑是KeyError或IndexError。现象可能是close_approach_data列表是空的,直接取[0]就崩了;也可能是某个小行星的直径字段单位体系里没有你想要的值。

这类问题的根子都在于:你假设字段必然存在,但真实数据往往有缺失或结构变化。我后来养成了习惯,先随机抽几个对象打印原始 JSON,确认所有分支都覆盖到了,再写解析代码。对于空列表,先用if approach_data:判断再取值;对于缺失字段,用ast.get("xxx", None)给默认值。排查的思路就是:不要相信“所有记录都一样”,每条记录都要按独立个体对待。

用一个统一的flatten_asteroid函数把单条对象解析逻辑隔离出来,遇到脏数据时单独调试,比在循环里层层排查舒服很多。

6. 从一把梭到长期跑:缓存、定时任务与更多接口玩法

6.1 本地 JSON 缓存:节省配额也方便离线调试

采集类的项目,最忌讳每次分析都重新拉一遍接口。我通常把原始响应按请求参数缓存到本地,文件名里带上日期或请求摘要:

import hashlib import json from pathlib import Path cache_dir = Path("./cache") cache_dir.mkdir(exist_ok=True) def cached_get(url, params): key = hashlib.md5((url + str(sorted(params.items()))).encode()).hexdigest() cache_file = cache_dir / f"{key}.json" if cache_file.exists(): return json.loads(cache_file.read_text(encoding="utf-8")) data = nasa_get(url, params) cache_file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") return data

缓存的好处不只是省配额,更重要的是离线可复现:数据源没有变动时,分析结果不会因为接口偶尔抽风而改变。注意缓存键要包含 URL 和所有参数,否则换成不同日期后可能读到旧数据。

6.2 定时采集的轻量方案

如果你想把小行星数据长期积累下来,做成一个趋势库,可以加一层定时调度。最轻量的方式是schedule库:

import schedule def daily_job(): data = cached_get("https://api.nasa.gov/neo/rest/v1/feed", {"api_key": API_KEY, "start_date": "2024-06-01", "end_date": "2024-06-07"}) # 解析并追加到 CSV print("数据更新完成") schedule.every().day.at("09:00").do(daily_job) while True: schedule.run_pending() time.sleep(60)

定时任务的重点是任务本身必须幂等,即重复执行不会产生重复数据。我的做法是先查 CSV 里已有的日期范围,只拉缺失的那几天,避免每天往同一日期追加数据造成重复。每天只请求一次接口,配额消耗很可控。

6.3 进阶路线:火星照片、地球影像与卫星 TLE

当你把小行星这条线跑顺之后,可以横向扩展到其他接口。火星车照片接口就是一个很好的下一个项目,核心参数是rover、sol或earth_date,以及相机代号:

url = "https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos" params = { "api_key": API_KEY, "sol": 1000, "camera": "fhaz", } photos = nasa_get(url, params) for p in photos["photos"][:10]: print(p["earth_date"], p["img_src"])

拿到img_src之后,你可以用requests下载图片,再做批量缩略图或图像分类练习。EPIC 接口则返回地球影像的元数据,时间维度跨度大,适合做时间序列分析。TLE 接口的数据更专业,适合对航天轨道计算有兴趣的读者。

这些接口的共同点是返回结构清晰、字段说明完善。官方文档里每个接口都附有示例返回体,建议动手之前先对照文档理解一遍,再决定解析脚本的结构。我现在做新接口的接入,流程基本固定:读文档 → 看返回体 → 写最小请求 → 逐层展平 → 落盘验证,整个流程下来很少再翻车。


分享几个我切身实践后的习惯:第一,公网的开放接口是公共资源,哪怕官方没有严格要求,我也会在请求之间加至少 0.5 秒的间隔,既是对平台的尊重,也是让自己养成规范编码的习惯;第二,所有解析代码尽量写成独立函数,配合缓存机制,遇到数据异常时只需要重跑一个函数,不用从零开始;第三,把每天的采集结果归档成带日期的文件,一个月后再回头看,你会发现积累的数据带给你很多“临时拉取”看不到的长期趋势。这套 API 接入流程不仅适用 NASA,换成其他提供 JSON 接口的公共服务同样成立,趁手的工具和方法是会迁移复用的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进