NASA公开API实战:Python请求、pandas清洗与可视化全流程
我一直觉得拿真实数据练手永远是学习网络请求和数据分析最靠谱的路子不是造一堆假数据自己哄自己玩。NASA公开API就是一个特别值得折腾的起点——它能直接给你天文照片、近地小行星的运行参数、火星表面的气象记录这类听起来很硬核、但返回结构又相对规整的数据。前阵子帮一位朋友做演示项目需要定时抓取当天的天文照片存档我顺手把整套接口从申请到落库摸了一遍。这篇文章就把整个过程掰开揉碎讲清楚从注册拿Key、构造请求、解析JSON到用pandas把嵌套字段整理成表格最后再做点简单的可视化。适合正在学Python、想找真实项目练手的开发者也适合对太空数据感兴趣但不知道从哪下手的爱好者。不需要天文背景会基本的Python语法就行。1. 先把思路理清楚NASA公开API到底能拿什么1.1 认识这套接口的真实用途NASA公开数据门户api.nasa.gov并不是只提供一两个接口它下面挂着一整组面向公众的开放服务。最早开放、也最常被拿来当教学案例的是APODAstronomy Picture of the Day每日天文图片接口每天返回一张天文照片和对应的说明文字。还有近地天体研究中心的NEO接口返回靠近地球的小行星轨道和距离数据火星探测项目的洞察号气象接口返回火星表面的温度、风速、气压甚至还有国际空间站的实时位置接口和NASA图像库的检索接口。选择NASA这套API做练习有个很实际的好处它的认证机制足够简单只需要一个API Key不需要OAuth那套复杂的授权流程返回格式统一是JSON字段虽然嵌套但结构稳定而且数据是官方权威信息不用担心抓回来一堆垃圾。更重要的是每个接口的数据量都不大单次请求返回几百KB以内对新手来说解析压力小但又不至于简单到一眼看穿。1.2 为什么是Python requests pandas这个组合我接触过的类似项目不少很多人上来就纠结用什么技术栈。我的建议很简单网络请求用requests数据处理用pandas可视化用matplotlib。这套组合足够应付NASA公开API的绝大多数场景而且是Python生态里最成熟、教程最多的三条线。requests库的价值在于把HTTP协议的细节封装得足够友好。你不需要手动拼请求头、处理字符编码、管理TCP连接一个requests.get()就能搞定。pandas则是处理JSON数据的利器特别是NASA接口喜欢返回那种一层套一层的嵌套结构json_normalize这个函数能把嵌套字典里的字段直接拉平成表格列省去手写循环取值的麻烦。matplotlib虽然不算最炫酷的绘图库但胜在稳定、文档全、能覆盖折线图柱状图这类常用图表。1.3 从请求到可视化的整体路径整个项目拆成五步走第一步申请API Key并配置到本地环境第二步选择合适的接口构造带参数的GET请求第三步处理响应状态把JSON解析成Python字典第四步用pandas清洗和整理数据处理缺失值、统一单位第五步把结果落盘成CSV或者画成图。后面几个实战案例全都是沿着这条主线走区别只在于不同接口的字段不同、清洗逻辑略有差异。这五步里最容易出问题的是第三步和第四步之间。很多新手拿到JSON字典后直接往pandas的DataFrame里塞结果嵌套字段全变成了字典对象看起来是表格实际每个单元格里装了个小仓库。后面我会专门讲怎么处理这种嵌套结构。2. 动手前先搞定三件事2.1 申请API Key的正确姿势打开NASA API官网的申请页填一个姓名、邮箱和一段简短的用途描述就能注册。用途描述不需要写多复杂我填的是personal learning project几分钟后就会收到一封包含API Key的邮件。拿到Key之后有件事一定要养成习惯不要把它硬编码在代码里。哪怕只是自己本地跑的小脚本也很容易因为后来想分享代码而把Key带出去泄露的风险比想象中大得多。正确做法是放进环境变量用的时候再读取import os API_KEY os.getenv(NASA_API_KEY, DEMO_KEY) print(API_KEY[:5] ... if API_KEY ! DEMO_KEY else using DEMO_KEY)NASA官方提供了一个DEMO_KEY供快速体验但它的访问配额很低每小时只能请求30次、每天50次。正式申请来的Key配额会高不少不过依然有每小时限额量大的任务该控制频率还是要控制。提示环境变量在Windows下用set NASA_API_KEY你的Key在macOS/Linux下用export NASA_API_KEY你的Key。IDE里也可以配置环境变量看个人习惯。2.2 理解响应结构与限流机制NASA公开API的响应有个共同特点顶层是一个字典里面既包含你要的数据也包含一些元信息。比如APOD的响应长这样{ date: 2024-01-01, explanation: 这是今天的图片说明文字……, title: 图片标题, url: https://apod.nasa.gov/apod/image/2401/xxx.jpg, hdurl: https://apod.nasa.gov/apod/image/2401/xxx_hd.jpg, media_type: image }遇到这样一个字典第一反应应该是打印出来看看而不是直接写解析逻辑。我每次对接新接口都会先跑一个最简请求把返回的JSON原样打印到终端用肉眼确认字段名和类型。这一步看起来笨实际上能省大量排查时间。限流方面NASA的做法是HTTP 429状态码配合响应头里的Retry-After字段。代码里最稳妥的做法是每次请求前做一次最小间隔控制或者干脆在异常处理里遇到429就等一段时间重试。2.3 一个足够稳的请求基础模板基于以上两点我攒了一个通用模板后续所有实战都从这里扩展import os import time import requests API_KEY os.getenv(NASA_API_KEY, DEMO_KEY) BASE_HEADERS {User-Agent: nasa-api-demo/1.0} def safe_get(url, params, timeout30, retries3): params[api_key] API_KEY for attempt in range(retries): try: resp requests.get(url, paramsparams, headersBASE_HEADERS, timeouttimeout) if resp.status_code 429: wait int(resp.headers.get(Retry-After, 10)) time.sleep(wait) continue resp.raise_for_status() return resp.json() except requests.exceptions.Timeout: if attempt retries - 1: raise time.sleep(2 * (attempt 1)) return None这个模板做了三件很多人会忽略的事设了超时时间防止请求挂死带了User-Agent避免被服务器当成异常请求对429和超时做了重试。前两点对新手的价值尤其大因为默认情况下requests不会超时一旦网络异常程序就可能无限等下去。3. 第一个实战每日天文照片自动下载器3.1 吃透APOD接口的看家字段APOD接口是这套API里最简单的也是最容易出成果的。它的核心参数是date格式是YYYY-MM-DD不传就返回今天。如果你传start_date和end_date它会把区间内的每日数据一次性都返回注意这种批量模式下不会返回hdurl需要高清图得逐日请求。返回字段里有几个容易踩坑的点media_type可能是image也可能是video遇到视频那天url指向的是YouTube页面下载逻辑要对此做判断hdurl字段在部分图片上可能不存在代码里需要用dir()或者in先判断一下再取。3.2 完整代码按日期下载高清图附带元数据结合前面的模板写一个能指定任意日期、自动下载高清图并保存元数据的脚本import os import json from datetime import date from safe_request import safe_get # 用上面的模板 APOD_URL https://api.nasa.gov/planetary/apod def fetch_apod(dt): return safe_get(APOD_URL, {date: dt.isoformat()}, timeout20) def download_binary(url, save_path): resp requests.get(url, headersBASE_HEADERS, timeout60) resp.raise_for_status() with open(save_path, wb) as f: f.write(resp.content) if __name__ __main__: today date.today() data fetch_apod(today) if data[media_type] ! image: print(f{today} 当天不是图片跳过下载) else: image_url data.get(hdurl) or data[url] ext os.path.splitext(image_url.split(?)[0])[1] or .jpg image_path fapod_{data[date]}{ext} download_binary(image_url, image_path) meta {k: data[k] for k in (date, title, explanation, media_type)} with open(fapod_{data[date]}.json, w, encodingutf-8) as f: json.dump(meta, f, ensure_asciiFalse, indent2) print(f已保存图片和元数据{data[title]})这里有个细节值得说取ext时我做了.split(?)[0]因为有些图片URL后面会带查询参数直接.splitext()拿到的后缀是错的。这种边边角角的处理写多了自然就有肌肉记忆了。3.3 进阶玩法批量拉取一个月并做断点续传单张图片下载很快但要每天手动跑就失去自动化的意义了。更实用的做法是写一个批量脚本把最近30天的数据全部拉下来。批量时要考虑到部分日期可能请求失败所以我在循环里加了本地文件检查如果图片已经存在就跳过这样即使中途中断下次运行也能接着来。import os import time from datetime import date, timedelta start date.today() - timedelta(days30) for i in range(30): dt start timedelta(daysi) image_path fapod_{dt.isoformat()}.jpg if os.path.exists(image_path): continue data fetch_apod(dt) if data and data[media_type] image: image_url data.get(hdurl) or data[url] download_binary(image_url, image_path) print(f完成: {dt}) time.sleep(1)批量任务里的sleep(1)很重要。它不是为了应付NASA的限流而是给自己留一个观察窗口不然跑到一半想打断都来不及。另外这种批处理脚本最好搭配日志输出每完成一条就打印一行进度不要等最后才给结果。4. 第二个实战近地小行星数据整理与风险筛选4.1 拿到NEO接口最烦人的嵌套结构如果说APOD是接口里的新手村那NEONear Earth Object近地天体接口就是进阶副本。它返回的数据结构要复杂得多顶层是near_earth_objects这是一个以日期为键的字典每个键对应的值是一个列表列表里的每个元素是一颗小行星的完整信息字段特别多。一颗小行星的信息里estimated_diameter嵌套着kilometers、meters等多套单位close_approach_data又是一个列表每项包含接近时间、相对速度、错过距离等信息。如果不做拉平处理直接塞进DataFrame会得到一个全是字典或列表单元格的表根本没法分析。4.2 用json_normalize把嵌套字典拉平pandas的json_normalize函数就是为这种场景设计的。它最大的作用是把多级嵌套的字典按点号路径展开成扁平列比如estimated_diameter.meters.estimated_diameter_max会变成一个单独的列。实际操作里我习惯先不直接跑函数而是把一条样本数据打印出来确认字段路径再动手写解析逻辑。下面这段代码请求最近7天的数据把每天每颗小行星的关键信息拉平成列表最后组装成DataFrameimport requests import pandas as pd from datetime import date, timedelta NEO_URL https://api.nasa.gov/neo/rest/v1/feed def fetch_neo(start, end): params { start_date: start.isoformat(), end_date: end.isoformat(), api_key: API_KEY, } resp requests.get(NEO_URL, paramsparams, timeout30) resp.raise_for_status() return resp.json()[near_earth_objects] start date.today() - timedelta(days7) end date.today() data fetch_neo(start, end) rows [] for dt, asteroids in data.items(): for ast in asteroids: speed ast[close_approach_data][0][relative_velocity][kilometers_per_hour] miss ast[close_approach_data][0][miss_distance][kilometers] rows.append({ date: dt, name: ast[name], diameter_min_m: ast[estimated_diameter][meters][estimated_diameter_min], diameter_max_m: ast[estimated_diameter][meters][estimated_diameter_max], velocity_kmh: float(speed), miss_km: float(miss), hazardous: ast[is_potentially_hazardous_asteroid], }) df pd.DataFrame(rows) df[date] pd.to_datetime(df[date]) df[hazardous] df[hazardous].astype(bool) df[diameter_avg_m] (df[diameter_min_m] df[diameter_max_m]) / 2 df.head()注意close_approach_data是列表我这里直接取了第0个元素。这样做有个前提单颗小行星在同一接口里只会返回一条最接近的记录。如果后续接口逻辑变化返回多条这里就要改成循环展开。4.3 做出一个危险程度排行的小工具数据整理干净之后分析本身就是几个筛选排序操作hazard df[df[hazardous]].sort_values(miss_km) print(hazard[[name, diameter_avg_m, velocity_kmh, miss_km]]) df.to_csv(neo_weekly.csv, indexFalse)这样一个脚本放在服务器上每周跑一次就能自动生成一份本周最值得关注的小行星清单。虽然NASA官方已经自己做了风险页面但自己动手拉数据的好处是可以自由组合字段比如你只关心直径大于100米且速度超过每秒20公里的目标那写两行筛选条件就行。我自己跑了一期的数据7天里一般会有几十到上百颗小行星被记录其中标记为有潜在危险的通常是个位数但筛选逻辑一定要自己掌握不能只依赖官方结论。5. 第三个实战火星气象数据读取与可视化5.1 洞察号气象接口的特殊之处洞察号InSight是NASA放在火星表面的一台探测器它传回来的气象数据会通过开放接口供公众查询。这个接口和其他几个不同它没有做分页或日期参数而是直接一次性返回探测器着陆以来收集到的所有SolSol是火星日约等于24小时40分钟的气象摘要。响应体里以sol_xxx为键的字段就是一个一个火星日的数据每个Sol下面有AT气温、PRE气压、HWS风速等子记录每个子记录又包含av平均值、mn最低值、mx最高值等统计量。这种按探测器记录的原始方式来组织的结构比普通API的日期分组更贴近真实物理世界但也意味着做时间序列分析之前得先把sol_xxx字符串解析成整数索引。5.2 从字典到DataFrame再到折线图import requests import pandas as pd import matplotlib.pyplot as plt MARS_URL https://api.nasa.gov/insight_weather/ params { api_key: API_KEY, feedtype: json, ver: 1.0, } resp requests.get(MARS_URL, paramsparams, timeout30) resp.raise_for_status() data resp.json() sols [] for sol_key, sol_data in data.items(): if not sol_key.startswith(sol_): continue temp sol_data.get(AT) if temp: sols.append({ sol: int(sol_key.replace(sol_, )), temp_min_c: temp.get(mn), temp_max_c: temp.get(mx), temp_avg_c: temp.get(av), pressure_pa: sol_data.get(PRE, {}).get(av), wind_speed_mps: sol_data.get(HWS, {}).get(av), }) df_mars pd.DataFrame(sols).dropna(subset[temp_avg_c]) df_mars df_mars.sort_values(sol).tail(30) fig, ax plt.subplots(figsize(10, 5)) ax.plot(df_mars[sol], df_mars[temp_avg_c], markero, label平均温度) ax.fill_between(df_mars[sol], df_mars[temp_min_c], df_mars[temp_max_c], alpha0.2, label温度区间) ax.set_xlabel(火星日 (Sol)) ax.set_ylabel(温度 (摄氏度)) ax.set_title(洞察号着陆点近期气温变化) ax.legend() plt.tight_layout() plt.savefig(mars_temperature.png, dpi150) print(已生成火星温度变化图)这里用fill_between画了一个温度区间带比只画平均值的折线更有信息量。火星表面的昼夜温差极大动辄七八十度这个区间带一眼就能看出火星气候的极端性。5.3 可视化时容易忽略的两个细节第一个细节是中文字体问题。matplotlib默认字体不支持中文图里出现中文标题会变成方框。我习惯最小化处理图里尽量用英文标签或者显式指定字体import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] # 或 Arial Unicode MS matplotlib.rcParams[axes.unicode_minus] False第二个细节是缺失值。很多Sol只记录了一部分气象要素比如气压有了但风速缺失这时候dropna不能乱用。按需保留做温度图就按temp_avg_c去空做风速图就按wind_speed_mps去空。一套数据里不同子集保留不同的行这才符合实际场景。6. 常见问题排查与避坑清单6.1 HTTP错误状态码速查表状态码常见原因处理方式400参数格式不对date格式错误、缺必要参数检查URL上的参数拼写和日期格式YYYY-MM-DD403API Key无效或者被封禁登录官网重新生成Key404路径拼错或者请求的图片资源不存在对比官方文档的端点路径429请求频率超过配额读取Retry-After头等待后重试降低请求频率500 / 502 / 503NASA服务端临时故障退避重试连错多次就放弃本轮任务遇到4xx错误重点查自己的参数遇到5xx错误基本可以判断是对方服务问题不要反复重试猛打等几分钟再试。6.2 时间与单位里藏着的坑NASA的日期统一走ISO 8601格式也就是YYYY-MM-DD但有的接口返回的是带时间的完整字符串比如2024-01-01T00:00:00.000。如果你的分析只需要日期直接用pd.to_datetime()之后取.dt.date就行。单位是另一个重灾区。NEO接口里同一个物理量给了好几套单位estimated_diameter下既有kilometers又有metersrelative_velocity既有kilometers_per_hour又有kilometers_per_second。分析前必须搞清楚自己用的哪一套否则不同数据源一拼接单位不一致的后果比数据缺失还难排查。6.3 API Key安全与数据量认知用NASA公开API不需要像处理用户隐私一样紧张但Key管理的习惯要在项目早期养成。我见过不少人在GitHub上公开了自己的Key被脚本小子捡去盗刷配额最后不得不重置。建议养成三个习惯Key只存环境变量提交代码前用.gitignore过滤包含Key的配置一旦怀疑泄露立刻去官网撤销再申请新的。数据量方面NEO接口如果拉一年数据返回的JSON可能到几十MB解析起来会明显变慢。这时候不要想着加内存而是缩小时间范围或者只保留需要的字段。NASA公开API适合做数据和原理验证不适合当大规模数据仓库来用。7. 个人使用习惯与项目扩展建议折腾完这几个案例有几个习惯是我现在做所有API相关项目都会坚持的。第一任何新接口第一次接触一定要先打印原始JSON不跳过这一步后面所有字段路径都是从这个原始输出里核对出来的靠记忆写字段名早晚会因为大小写或下划线问题翻车。第二所有耗时循环都要加日志和进度输出跑批任务最怕的就是程序卡住了还没反应有了日志至少能知道卡在哪个日期哪个请求上。第三代码里所有API Key相关读取统一走环境变量哪怕只是本地练习也一样。这套东西做完之后后续还有不少可以扩展的方向。比如可以写一个定时任务每天早上自动拉取APOD图片并设置为桌面壁纸可以把NEO数据存进SQLite做一段时间的趋势分析还可以用Flask搭一个本地网页展示火星气象图和各指标的最新值。这些扩展本质上都是把已经拉平的数据再做一层加工核心还是前面讲的那套读取和清洗思路。最后再分享一个小技巧NASA公开API其实还有图片库和音频库的检索接口风格跟前面几个不太一样但认证和解析套路完全相同。把本文这套流程跑熟之后再遇到任何返回JSON的公开数据接口你都可以用同一套方法论快速上手。数据来源可以变字段结构可以变但从原始数据出发、先看结构再写解析、最后清洗分析的路线是始终不变的。