
用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名称数据内容典型应用场景APODAstronomy Picture of the Day每日天文图片及说明图片展示、天文科普站点NEONear Earth Object近地小行星轨道数据天文爱好者的轨道追踪、数据分析EONETEarth Observatory Natural Event Tracker全球自然灾害事件灾害监测、地理可视化Mars Rover Photos火星探测器拍摄的照片图像处理、物体识别数据集构建Exoplanet Archive系外行星参数天文科研、数据建模TLETwo-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的兼容性和可控性依然是最稳的选择。这套组合的好处是每个库的学习成本都低、生态成熟、彼此配合默契。尤其是requestspandas这对组合基本成了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你的keyWindows下可以用系统环境变量配置如果用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_date2024-01-01end_date2024-01-07api_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, timeout30) 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(days7)).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(days365) else: start datetime.strptime(max_date, %Y-%m-%d).date() timedelta(days1)增量更新时需要注意一个边界条件日期参数必须使用字符串格式的YYYY-MM-DD。pandas默认会用datetime.date类型直接传进去会导致URL编码异常。我用date.today().strftime(%Y-%m-%d)来保证格式统一。抓到的数据入库也很简单pandas的to_sql方法直接可以写入SQLitedf.to_sql(neo_asteroids, conn, if_existsappend, indexFalse)这里有一个高概率踩坑点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), axis1 )3.5 可视化分析示例数据清洗完毕随便做一张图都能看到有趣的现象。我拿了一段时间的近地天体数据画了一个“天体直径分布”的直方图import matplotlib.pyplot as plt plt.figure(figsize(10, 6)) plt.hist(df[diameter_est_km].dropna(), bins50, colorsteelblue, edgecolorwhite) plt.xlabel(Estimated Diameter (km)) plt.ylabel(Frequency) plt.title(Distribution of NEO Estimated Diameters) plt.grid(True, alpha0.3) plt.tight_layout() plt.savefig(neo_diameter_distribution.png, dpi150)这张图画出来之后你会发现近地天体的直径分布呈现强烈的长尾特征绝大部分天体的直径不到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, retries3): for attempt in range(retries): resp requests.get(url, timeout30) if resp.status_code 200: return resp.json() elif resp.status_code 429: wait_time 30 * (attempt 1) print(fRate 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, defaultNone): 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的官方状态页很多时候是服务端在升级维护不是你代码的问题。这种时候别反复重试稍微等一两个小时再跑往往就恢复正常了。做数据采集耐心往往比技巧更重要。