用Python实战NASA开放API:从APOD到NEO数据可视化全流程
2026/9/9 12:59:29 网站建设 项目流程

我最早接触NASA开放API,是因为想给朋友的天文科普站做一个“今日天文图片”栏目。当时第一反应是每天手动去官网找图,后来翻到api.nasa.gov才发现,NASA早就把这些数据做成了公开API——从每日天文图片到近地小行星清单,从火星照片到太阳活动预报,全都通过HTTP接口对外开放。用Python读取和处理这类公开API数据,比想象中简单得多:requests发请求拿到JSON,pandas清洗分析,matplotlib出图,一条链路下来就能把“太空数据”变成自己的作品集。

接下来我会以实测过的APOD(每日天文图片)和NEO(近地天体)两个接口为主线,把从申请API Key、发起请求、解析JSON,到清理数据、绘制可视化图表的完整流程讲清楚,顺便把限流、529错误、单位换算、嵌套JSON这些坑都过一遍。适合刚学完Python基础、想拿真实数据练手的人,也适合想做天文科普工具、数据可视化项目的开发者。

1. NASA开放数据全家桶:除了每日天文图还有哪些接口值得接

1.1 我为什么从APOD开始

APOD全称Astronomy Picture of the Day,是NASA网站上最著名的栏目,每天更新一张天文图片和一段背景说明。它的API接口返回结构极简,核心字段就是title、url、explanation、date这几个,几乎没有深层嵌套,是最适合新手入门的NASA端点。

我第一次调用时只写了十几行代码,就成功把当天的图片信息拉回来了。那种“原来公开API这么友好”的成就感,是推动我去啃NEO这种复杂数据的直接动力。所以如果你刚开始接触NASA API,我也建议先从APOD入手,它能在5分钟内让你完整跑通“请求—响应—解析—展示”这条链路,建立起对整个流程的信心。

1.2 常用接口盘点

NASA开放数据平台不止APOD一个端点,我把用过且值得关注的接口整理成了下面的表格:

接口名称数据内容典型应用场景
APOD每日天文图片与文字说明自动更新网站头图、科普栏目、手机壁纸
NEO Feed近地小行星接近地球的实时记录小行星风险播报、天文预警工具
NEO Browse全量小行星目录浏览离线数据分析、机器学习样本
Mars Rover Photos好奇号、毅力号等火星车拍摄照片火星影像相册、科普展示
InSight火星表面的天气数据(气压、风速、温度)气象仪表盘、教学演示
DONKI日冕物质抛射、太阳耀斑等空间天气事件太阳活动监测、极光预测辅助
EPIC从深空拍摄的地球高清影像地球展示页面、动态壁纸

从数据处理难度上看,APOD最容易上手,NEO的JSON嵌套较深但信息量最丰富,Mars Photos和EPIC返回的字段也相对规整。DONKI这种接口虽然数据价值高,但字段命名比较学术化,新手处理起来会有点吃力,建议放到后面再碰。

1.3 你能拿这些数据做什么

按我的实际经验,NASA API至少能支撑这几类项目:

一类是周期性内容工具。比如写一个定时脚本,每天从APOD拉取图片和说明,生成站点的头图栏目,或者推送到公众号、群机器人。这个只需要定时任务加一次API请求,几乎没有任何维护成本。

另一类是数据分析练习。NEO数据集就是很好的pandas练手素材:有字符串数字、有单位换算、有日期排序、有布尔筛选,还能画出有意义的气泡图。比起用网上那些提前清洗好的CSV,直接处理API原始JSON更能锻炼数据工程能力。

还有一类是教学演示。火星天气、地球影像这类接口视觉冲击力强,拿来给学生或朋友演示“什么是API”“什么是JSON”非常直观,比空讲概念有效得多。

2. 申请API Key和摸清请求规矩:开工前的必修课

2.1 申请流程:没有审批,填完就发

NASA开放数据平台的Key申请入口在api.nasa.gov,页面右上角就有注册表单。需要填的四项分别是:First Name、Last Name、Email和Application URL。

这里有个容易让人犹豫的地方:Application URL到底填什么?按官方说明,这个字段用于描述你打算把API用在哪里,个人项目填个人博客地址、GitHub仓库地址或者自己的主页都可以,没有硬性验证。如果实在没有,填一个占位URL也能通过。

提交之后,刷新页面进入Account页面,就能看到系统自动分配的API Key,整个过程不需要人工审核,也没有等待期。我第一次申请的时候一度怀疑是不是漏了什么步骤,结果点开页面才发现Key早就躺在那里了。

2.2 DEMO_KEY和正式Key的差别

如果你不想注册,NASA也提供了一个公共测试Key:DEMO_KEY。所有接口都支持用它发起请求,但配额限制非常低——每IP地址每小时大约30次请求。也就是说,DEMO_KEY只适合做功能验证,跑一跑“接口能不能通”这种实验,真正要批量拉数据就必须换自己的Key。

注册后拿到的正式Key配额会高很多,官方文档给出的参考限制是每秒50次、每小时数万次。实际使用中,NASA并不是一个容易触发限流的服务,只要你不是恶意并发拉取,个人项目根本摸不到配额上限。

那为什么还要强调配额?因为很多人会在调试时反复跑同一个循环,一次脚本忘了加缓存,可能瞬间发出几百个请求。NASA虽然限流宽松,但同样会给服务器造成压力,工程上还是要有点“礼貌请求”的意识。

2.3 Key怎么带、请求长什么样

NASA API的认证方式非常简单,直接在查询参数里带上api_key字段即可,不需要配置请求头里的Authorization。

一个最基础的请求长这样:

import requests API_KEY = "你的NASA_API_Key" url = "https://api.nasa.gov/planetary/apod" params = {"api_key": API_KEY} resp = requests.get(url, params=params) print(resp.status_code) print(resp.json())

用requests的params参数传查询字符串,Python会自动把字典编码成URL末尾的query string,比手动拼接字符串更安全,不用担心Key里出现特殊字符。

2.4 读懂官方文档和响应JSON

NASA API的一大优点是每个端点都有详细的参数说明和示例响应。以APOD为例,文档页面列出了date、hd、start_date、end_date、thumbs等参数,还附了一段真实的JSON样例。

拿到文档后,我建议先不用Python,直接在浏览器地址栏访问一次带Key的URL,比如:

https://api.nasa.gov/planetary/apod?api_key=你的NASA_API_Key

浏览器会把JSON结构直接展示出来。这一步能让你在写代码前就对返回数据长什么样、字段嵌套多深有一个直观认识。我后续处理NEO时也是先用浏览器方式查看响应,再决定用哪种方式摊平数据,这比在代码里反复print调试要高效得多。

3. 实测开跑:用Requests把APOD和NEO数据拉到本地

3.1 环境准备

这次项目依赖的Python库不多,requests负责HTTP请求,pandas负责数据处理,matplotlib负责可视化,如果要做重试还可以加一个tenacity,不是必须的。

pip install requests pandas matplotlib tenacity

我用的是Python 3.10。requests库从2.25版本开始对 params 的处理已经很成熟,这里没有任何版本差异问题。

3.2 APOD:第一个请求

APOD接口最简单的用法是不传date参数,直接返回今天的图片信息。如果想拿指定日期的数据,传入YYYY-MM-DD格式的date即可:

import requests API_KEY = "你的NASA_API_Key" url = "https://api.nasa.gov/planetary/apod" params = {"api_key": API_KEY, "date": "2024-01-01"} resp = requests.get(url, params=params, timeout=30) resp.raise_for_status() data = resp.json() print(data["title"]) print(data["date"]) print(data["url"]) print(data["explanation"][:200])

这里有几件值得注意的事。

第一,务必设置timeout参数。NASA服务在太空任务高峰期偶尔会响应缓慢,不设timeout的话,脚本可能挂在请求上数分钟不返回。设置30秒是工程上的基本底线。

第二,理解explanation的文本属性。它是纯文本,包含换行和特殊字符,如果需要展示到网页,要配合前端的文本渲染方案,不能直接拼HTML。

第三,图片文件本身需要单独下载。APOD返回的url字段指向图片文件地址,是一个独立的静态资源请求:

img_resp = requests.get(data["url"], timeout=60) with open(f"apod_{data['date']}.jpg", "wb") as f: f.write(img_resp.content)

不要把这种方式和API响应搞混,API返回的是JSON元数据,图片是另一个请求。

3.3 NEO:带日期的复杂接口

NEO Feed接口用于查询指定时间段内所有近地天体的接近数据,它有非常严格的参数约束:start_date和end_date都是YYYY-MM-DD格式,且两者间隔最多不能超过7天。超过7天,接口会直接返回400错误。

这是NEO这个接口最大的一个坑。我最初以为可以传一个月的时间范围让接口自己分页,结果请求直接失败,查了官方文档才看到“max: 7 days”的限制。所以批量拉取时必须自己按7天窗口切分。

NEO Feed的响应结构嵌套很深,大致长这样:

{ "near_earth_objects": { "2024-10-01": [ { "name": "(2024 ABC)", "estimated_diameter": { "kilometers": { "estimated_diameter_min": 0.01, "estimated_diameter_max": 0.03 } }, "is_potentially_hazardous_asteroid": false, "close_approach_data": [ { "close_approach_date": "2024-10-01", "miss_distance": { "kilometers": "123456.78" }, "relative_velocity": { "kilometers_per_hour": "12345.6" } } ] } ] } }

注意几个关键点:

外层near_earth_objects是一个以日期字符串为键的字典,而不是列表。每个日期下才是一个小行星对象的列表。这种“日期键字典”结构意味着直接用json_normalize之前得先遍历日期键,把不同日期的数据合并起来。

close_approach_data是列表,因为同一个小行星可能在未来多次接近地球。多数场景下我们只关心最近一次或第一次接近,需要指定索引取出,例如close_approach_data[0]。

estimated_diameter是嵌套字典,内部按单位分为kilometers、meters、miles、feet四个细分,每个细分又包含最小值estimate_diameter_min和最大值estimated_diameter_max。

miss_distance字段看起来是字符串,实际上也是字符串形式的数值。这在JSON里非常常见,后面用pandas处理时要统一转float。

3.4 批量拉取与通用封装

因为单次Feed请求只覆盖7天,要拿一个月的数据就需要循环调用。我写了一个简单函数,按天粒度循环避免日期边界问题:

import time import requests from datetime import date, timedelta NEO_URL = "https://api.nasa.gov/neo/rest/v1/feed" def fetch_neo(start_date: date, end_date: date, api_key: str) -> list: all_objects = [] current = start_date while current <= end_date: params = { "api_key": api_key, "start_date": current.isoformat(), "end_date": current.isoformat(), } resp = requests.get(NEO_URL, params=params, timeout=30) resp.raise_for_status() today_objects = resp.json().get("near_earth_objects", {}) for date_key in today_objects: all_objects.extend(today_objects[date_key]) current += timedelta(days=1) time.sleep(1) return all_objects

这里按天请求虽然请求数量多了一些,但逻辑最直观,也不容易踩7天窗口的边界坑。每两个请求之间sleep一秒钟,既照顾了服务器负载,又完全不会触发限流。

如果你需要NEO全量目录做更深入的分析,可以使用NEO Browse接口,它支持page和size分页参数:

browse_url = "https://api.nasa.gov/neo/rest/v1/neo/browse" params = {"api_key": API_KEY, "page": 0, "size": 20}

Browse接口每次返回20条,通过增长page参数循环拉取。这个接口返回的JSON字段和Feed类似,只是不按日期分组,处理起来更简单。

4. JSON变DataFrame:摊平、换算和清洗

4.1 嵌套结构先摊平

把NEO数据塞进pandas,第一步是把不规则的嵌套JSON转成扁平的表格。有些教程喜欢用pd.json_normalize一把梭,但我实测下来,NEO这种深度嵌套结构直接normalize出来的列名会非常长,比如estimated_diameter.kilometers.estimated_diameter_min,而且close_approach_data作为列表类型在DataFrame里会变成一列对象,后续处理并不方便。

更可控的做法是手动提取字段,把需要的值整理成字典列表:

import pandas as pd rows = [] for obj in all_objects: close = obj["close_approach_data"][0] row = { "name": obj["name"], "diameter_km_min": obj["estimated_diameter"]["kilometers"]["estimated_diameter_min"], "diameter_km_max": obj["estimated_diameter"]["kilometers"]["estimated_diameter_max"], "is_potentially_hazardous": obj["is_potentially_hazardous_asteroid"], "miss_km": float(close["miss_distance"]["kilometers"]), "velocity_kmh": float(close["relative_velocity"]["kilometers_per_hour"]), "close_date": close["close_approach_date"], } rows.append(row) df = pd.DataFrame(rows)

这种方式的优点是一步到位,字段名自己定义,后续代码可读性好。缺点是当接口字段很多时,手写代码量大。我的建议是:字段少、嵌套深时手写;字段多、嵌套浅时用json_normalize快速展开。

4.2 字符串转数值与单位换算

NEO接口返回的数值字段基本都是字符串,直接拿来做计算会得到诡异结果,甚至触发类型错误。pandas的astype方法可以批量处理:

df["miss_km"] = df["miss_km"].astype(float) df["velocity_kmh"] = df["velocity_kmh"].astype(float)

这里有个单位选择的问题。NEO同时返回天文单位(astronomical)、月球距离(lunar)、英里(miles)和公里(kilometers)四种距离单位。分析时统一用公里最方便,因为公里是国际通用单位,直观且容易和其他数据源对比。

速度字段同理,返回了kilometers_per_hour和miles_per_hour,直接选公里每小时即可。如果你确实拿到了英里数据需要转换,记住1英里约等于1.60934公里就够了。

4.3 日期时间与排序

close_date字段是YYYY-MM-DD字符串,可以直接用pd.to_datetime转成datetime类型:

df["close_date"] = pd.to_datetime(df["close_date"]) df.sort_values("close_date", inplace=True)

转成datetime之后,你就可以用resample、groupby按星期或月份聚合,也可以直接筛选某个日期之后的数据。这是NEO数据最容易忽略的后处理步骤,不转到datetime类型,pandas很多时间相关功能都用不了。

4.4 筛选聚合:找出最值得关注的小行星

数据清洗完之后,真正好玩的分析才开始。我随手做了几个筛选:

最接近地球的10颗:

top10_close = df.nsmallest(10, "miss_km")

直径最大的一颗:

df["diameter_km_avg"] = (df["diameter_km_min"] + df["diameter_km_max"]) / 2 largest = df.nlargest(1, "diameter_km_avg")

潜在危险小行星(PHA)占比:

pha_count = df["is_potentially_hazardous"].sum() pha_ratio = pha_count / len(df)

实际统计下来,近地天体中标记为潜在危险的比例其实不高,大约在百分之几的水平。但只要把这些数据和可视化结合起来,一张图就能看出明显的分布规律。这部分我在第6章详细展开。

5. 所有接口都会遇到的坎:限流、529错误与本地缓存

5.1 529到底是谁的问题

用NASA API的过程中,你可能会遇到下面这样的报错:

api error: 529 overloaded. this is a server-side issue, usually temporary

这个错误特别容易让人误以为是自己的代码写错了。我第一次看到时,翻遍了请求参数、API Key配置,甚至怀疑是不是网络环境出了问题,折腾了大半个小时才意识到,529是HTTP协议里的“服务器过载”状态码,相当于服务端临时扛不住流量了。

官方提示说得很清楚:这是server-side issue,通常只是临时性的。处理方式就是退避重试,等几秒再发一次。千万不要在这个错误发生时并发重试,那只会让服务器更堵。正确做法是等待时间指数增长,比如重试第1次等2秒、第2次等4秒、第3次等8秒,给服务端留出恢复窗口。

5.2 请求频率控制

NASA接口虽然配额宽松,但批量脚本还是建议控制节奏。最朴素的做法就是循环里加time.sleep。上一节我的fetch_neo函数就sleep了1秒,这个频率对个人项目足够安全。

需要注意的是,如果你做的是定时任务,比如每小时跑一次,那频率控制几乎不用考虑。真正需要担心的是脚本里出现多层循环时,可能在短时间发出大量请求。比如你同时拉取NEO几个月的数据,又嵌套了重试逻辑,一旦重试条件写错,就可能变成循环放大。

我习惯在写批量拉取脚本时至少加上两个护栏:每两次请求间固定sleep,每个请求设置timeout,然后在请求外层再包一层计数器,达到N次后强制退出。这样即便代码逻辑有bug,也不会意外打爆别人的接口。

5.3 重试机制的正确打开方式

为了处理529和偶尔的网络抖动,我封装了一个带重试的请求函数:

import time import requests def fetch_with_retry(url, params, retries=5, timeout=30): for attempt in range(retries): try: resp = requests.get(url, params=params, timeout=timeout) if resp.status_code == 529: wait = 2 ** attempt print(f"服务器过载,{wait}秒后重试") time.sleep(wait) continue resp.raise_for_status() return resp.json() except requests.exceptions.RequestException as e: wait = 2 ** attempt print(f"请求异常: {e}, {wait}秒后重试") time.sleep(wait) raise RuntimeError("多次重试仍然失败")

核心逻辑就是指数退避。第一次失败等2秒,第二次等4秒,依此类推。这比固定时间重试要科学,既不会在服务端恢复前反复怼上去,也不会在服务端已经恢复后还傻等。

如果你项目里已经装了tenacity库,也可以用装饰器简化:

from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, max=60)) def get_json(url, params): resp = requests.get(url, params=params, timeout=30) if resp.status_code == 529: raise RuntimeError("529 overloaded") resp.raise_for_status() return resp.json()

tenacity会自动处理重试次数、等待时间和异常类型,代码更简洁,但不建议在小型脚本里为了一个重试功能引入新依赖。

5.4 本地缓存与代码可维护性

NASA的很多接口数据是按天更新的,比如APOD每日一变,NEO的接近数据在短期内不会频繁变化。这意味着同一份数据拉下来之后,本地存一份完全合理,没必要每次都重新请求。

我写了个简单的JSON缓存函数:

import json from pathlib import Path def get_json_with_cache(url, params, cache_path): cache_path = Path(cache_path) if cache_path.exists(): print(f"读取缓存: {cache_path}") return json.loads(cache_path.read_text(encoding="utf-8")) data = fetch_with_retry(url, params) cache_path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") return data

调用时按日期命名缓存文件,比如neo_20241001.json。这样脚本无论跑多少遍,只要缓存存在就不会重复消耗API配额,调试时速度也快很多。

缓存策略要注意数据时效。对NEO这种日更数据,缓存文件名带上日期即可;对DONKI这种实时空间天气数据,缓存意义不大,建议直接实时请求。过期缓存比没缓存更危险,因为它会给你展示旧数据而不自知。

6. 绘制小行星威胁分布图:从数据到可视化

6.1 筛选近地天体,先看数量再看威胁

数据清洗完成后,我用NEO一个月的数据做了一张散点图。在画图之前,先做了一次筛选:直径取上下限的均值,是否潜在危险用布尔值,距离和速度都统一成公里单位。

潜在危险小行星(PHA)的定义是“直径大于一定阈值且距离地球足够近”的天体。在数据可视化中,直接把is_potentially_hazardous字段映射到颜色上,就能很直观地看出威胁小行星在参数空间里的分布位置。

6.2 用散点图把威胁分布画出来

我用了气泡图的形式:横轴是估计直径(公里),纵轴是相对速度(公里/小时),气泡大小映射为接近距离的倒数,颜色区分是否潜在危险:

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["WenQuanYi Micro Hei", "SimHei", "Noto Sans CJK SC"] plt.rcParams["axes.unicode_minus"] = False fig, ax = plt.subplots(figsize=(12, 7)) x = df["diameter_km_avg"] y = df["velocity_kmh"] size = 3000000 / (df["miss_km"] + 1000) c = df["is_potentially_hazardous"].astype(int) sc = ax.scatter(x, y, s=size, c=c, cmap="coolwarm", alpha=0.65, edgecolors="white", linewidths=0.5) ax.set_xlabel("估计直径(公里)") ax.set_ylabel("相对速度(公里/小时)") ax.set_title("近地小行星分布:直径、速度与潜在危险标记") plt.colorbar(sc, label="是否潜在危险小行星(1=是)") plt.savefig("neo_distribution.png", dpi=150, bbox_inches="tight") plt.show()

这张图画出来之后,第一个直观结论是:绝大多数近地天体直径都在0.1公里以下,速度集中在几万公里每小时的水平。潜在危险小行星并不会因为尺寸大就自动危险,它同时取决于距离和速度,这也是可视化比表格更有价值的点。

6.3 中文乱码问题不能忽略

用matplotlib画图时,中文标签最常遇到的坑是乱码或方块字。原因通常是系统缺少对应字体,或者matplotlib没有正确识别中文字体。我在代码里用rcParams设置了常见中文字体列表,如果你的机器上装了其他字体,按实际显示名调整即可。

Linux服务器上如果画图乱码,多半是缺noto-sans-cjk或者文泉驿字体,安装之后重启Python进程一般能解决。桌面端Windows系统把SimHei放进去就够了。还有一个小细节:设置了中文字体之后,坐标轴负号会变成方块,所以要加上axes.unicode_minus=False。

6.4 导出CSV与后续扩展

数据分析收尾时,我习惯把处理好的DataFrame导出成CSV,方便其他工具或同事直接使用:

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

注意这里用的编码是utf-8-sig而不是utf-8,后者在Excel里中文会乱码。utf-8-sig会在文件开头带一个BOM标记,Excel就能正确识别编码了。这个细节在给别人交付数据文件时会省掉很多麻烦。

后续如果你想更进一步,这个项目还可以扩展到至少三个方向:一是把拉取脚本改成定时任务,配合缓存每天自动更新小行星数据;二是用Plotly或bokeh做交互式图表,悬停显示小行星名称、距离等详细信息;三是把NEO数据和其他公开天文数据集关联,比如结合DONKI位置数据做联合分析。每一条都能让这个项目从“API调用练习”进化成真正有实用价值的工具。

最后再分享两个我在这个项目里养成的习惯。第一个是所有的API调用都先写缓存再写业务逻辑,哪怕只是把响应原样存成JSON文件。调试脚本的时候速度差一个数量级,而且不会因为反复请求把自己的Key搞进限流名单。第二个是无论多小的脚本都要留日志,print本身不够,至少要能区分“从缓存读取”和“从接口请求”。这个习惯在大规模拉数据时尤其重要,不然你根本不知道脚本卡在哪个环节。NASA开放数据是一个被低估的数据源,数据质量高、接口稳定、文档清晰,对任何阶段的Python开发者来说都是非常好的练手项目。希望这篇文章能帮你少走一些我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询