news 2026/10/14 4:40:40

用Python实战NASA开放API:从数据抓取到数据可视化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python实战NASA开放API:从数据抓取到数据可视化

这段时间我在折腾数据可视化练手项目,翻来覆去找公开数据源的时候,发现NASA开放API是个特别顺手的家伙。它不像很多商业接口那样要搞复杂的OAuth授权,也不像爬普通网站那样要跟各种HTML结构搏斗,它给的是干净的JSON,文档结构清晰,题材还自带吸引力——每日天文图片、火星车照片、近地小行星监测,随便挑一个都能做出像样的数据作品。

如果你正在学Python的数据抓取和清洗,或者想找一个真实、规范、免费的API项目来做练习,NASA这套接口是性价比很高的选择。这篇文章我会从实际使用的角度,讲清楚怎么申请访问权限、如何封装请求、怎么解析三种典型接口的数据,并把我在真实操作中遇到的坑一并写出来。内容不复杂,你会用requests和pandas就能跟着做,看完基本可以直接上手。

1. 项目概述:为什么拿NASA的API练手很值

1.1 这个项目解决什么问题

单纯学Python语法很容易陷入“懂了但不知道干嘛”的状态,而纯练爬虫又总是被反爬、加密参数搞得怀疑人生。NASA开放API刚好卡在一个很好的中间位置:它是真正的RESTful接口,有规范的鉴权方式、有明确的数据结构、有现实意义的数据内容,但又不设置高门槛。

你可以借这个项目一次性练到几件事:

  • 用requests调用HTTP接口,理解URL、查询参数、响应状态码这些基础概念;
  • 用JSON解析嵌套数据结构,学会从一堆层级不明的字典里稳妥地取字段;
  • 用pandas把接口数据转成表格,做筛选、分组、统计;
  • 用matplotlib做基础可视化,让数据“说话”。

整个流程下来,等于把数据工作中最常接触的“采集—清洗—分析—呈现”四个环节都走了一遍。

1.2 接口选型:我为什么选这三个

NASA开放API的目录里有不少接口,我实际挑选了三个特点各异的作为主线,分别对应三种典型场景:

接口数据特点核心练习点
APOD每日天文图片单条JSON记录,包含图片链接参数请求、图片二进制下载
火星探测器照片批量照片数据,含多个筛选维度循环分页、嵌套字段解析、DataFrame统计
近地小行星数据按日期分组的对象集合日期参数、多层嵌套解析、简单可视化

这三个接口基本覆盖了日常数据开发的大部分形态:有返回单条记录的,有返回多条记录的,还有按时间维度分组的。选它们做练习,比死磕某一个接口要值得多,因为你学的是“通用的处理思路”,而不是某个接口独有的调用方式。

2. 环境准备与请求层设计

2.1 开发环境的依赖

这个项目的依赖很少,一个Python 3.9以上的环境就够了。建议你单独建一个虚拟环境,避免和系统环境互相污染。

pip install requests pandas matplotlib

requests负责发HTTP请求,pandas负责数据整理,matplotlib负责画图。如果你后面打算做更多数据分析,再补一个numpy也不迟,但目前这三个就够用了。

2.2 理解REST API的调用套路

NASA的接口和绝大多数公开API一样,遵循同一套规则:你给我一个HTTP请求,我给你一个JSON响应。

一个基本调用长这样:

import requests url = "https://api.nasa.gov/planetary/apod" params = { "api_key": "你的API Key", "date": "2024-05-20" } resp = requests.get(url, params=params) data = resp.json()

这里有个容易忽略的细节:requests.get的params参数会自动把字典拼成URL后面的查询字符串,你不需要手动拼?api_key=xxx&date=xxx。写成字典的好处是参数多了以后代码依然清晰,也方便做条件组装。

响应的JSON就是Python字典和列表的组合,直接用索引就能取值。但真实数据往往嵌套很深,所以写代码时一定要有“逐层拆解”的意识,不要一上来就写data["a"]["b"]["c"]这种长链,很容易在某一层遇到KeyError。

2.3 封装一个带重试的通用请求函数

我在实际写这个项目时,没有每个接口都单独写一遍requests调用,而是封装了一个通用函数。好处是统一处理了API Key注入、超时、重试、限流这些脏活,后面每个接口的代码都清爽很多。

import time import requests NASA_API_BASE = "https://api.nasa.gov" API_KEY = "你的API Key" def nasa_get(endpoint, params=None, retries=3, timeout=30): url = NASA_API_BASE + endpoint if params is None: params = {} params["api_key"] = API_KEY for attempt in range(retries): try: resp = requests.get(url, params=params, timeout=timeout) if resp.status_code == 429: wait = int(resp.headers.get("Retry-After", 60)) print(f"请求过于频繁,等待 {wait} 秒后重试") time.sleep(wait) continue resp.raise_for_status() return resp.json() except requests.exceptions.Timeout: if attempt < retries - 1: wait = 2 ** attempt time.sleep(wait) continue raise except requests.exceptions.RequestException as e: print(f"请求失败: {e}") if attempt < retries - 1: time.sleep(2 ** attempt) continue raise return None

这个函数做了三件事:

  • 自动把api_key塞进参数里,你不需要每个接口都记得带;
  • 对429限流做等待重试,读取响应头里的Retry-After作为等待秒数;
  • 对网络超时做指数退避重试,第一次等1秒,第二次等2秒,第三次等4秒。

指数退避是请求外部服务时很实用的策略。服务器出现临时故障时,你连续快速重试反而可能加重问题,而等待时间逐步拉长,给了服务恢复的时间,也减小了你自己的请求压力。

2.4 注意:API Key的获取和限流

API Key需要在官网申请,流程很简单:打开API页面,填一个邮箱和名字,系统会给你发一封带Key的邮件,基本是即时到账。

拿到Key后,我建议设置一个环境变量来保存,而不是直接写在代码里。你可以写个小配置模块:

import os API_KEY = os.environ.get("NASA_API_KEY", "你的备用Key")

这样做的好处是代码提交到Git仓库时不会把Key泄露出去,纯属个人习惯,但长期来看是值得养成的。

另外要注意,用了demo级别Key虽然也能跑通接口,但限流比较严格。我实际用下来,连续请求几十次就可能触发429。所以批量拉数据时,循环里加点time.sleep(0.5)这类小间隔,能少很多麻烦。

3. 实战一:APOD每日天文图片抓取与归档

3.1 接口返回结构

APOD(Astronomy Picture of the Day)是NASA最经典的每日天文图片接口。请求一个日期,它会返回当天的图片信息、标题和一段说明文字。

响应结构大致是这样的:

{ "date": "2024-05-20", "title": "某张天文图片标题", "explanation": "一段很长的说明文字", "url": "https://apod.nasa.gov/apod/image/xxxx.jpg", "hdurl": "https://apod.nasa.gov/apod/image/xxxx_hd.jpg", "media_type": "image" }

注意里面的media_type字段,它可能是image也可能是video,因为NASA偶尔会放视频。写抓取逻辑时必须先判断这个字段,不然直接把video链接当图片下载,会得到一堆没用的文件。

3.2 批量抓取与本地归档

我的目标是抓取一个日期范围内所有图片,保存到本地文件夹。日期范围用datetime的date对象来迭代,比手动数天数要可靠。

from datetime import date, timedelta def fetch_apod(day): data = nasa_get("/planetary/apod", {"date": day.isoformat()}) return data def download_apod_images(start_day, end_day, save_dir="apod_images"): import os os.makedirs(save_dir, exist_ok=True) current = start_day while current <= end_day: data = fetch_apod(current) if data is None: current += timedelta(days=1) continue print(f"抓取 {current}: {data.get('title')}") if data.get("media_type") == "image": url = data.get("hdurl") or data.get("url") filename = f"{current.isoformat()}.jpg" try: img_resp = requests.get(url, timeout=60) img_resp.raise_for_status() with open(os.path.join(save_dir, filename), "wb") as f: f.write(img_resp.content) print(f" 已保存 {filename}") except Exception as e: print(f" 图片下载失败: {e}") current += timedelta(days=1) time.sleep(0.5)

这个循环里我特意在每次请求之间加了0.5秒的延迟。实际体验下来,如果完全不加停顿,连续请求几十次后很容易被限流,加了延迟之后,整个批量过程基本没再触发过429。

3.3 这个场景里的两个容易踩的坑

第一个坑是图片链接的域名偶尔会出问题。APOD的历史图片链接有时会指向一个已经不存在的路径,200状态码也未必能保证图片真的能显示。所以我在这里做了两层防护:先用try/except接住下载异常,同时判断media_type,从逻辑上排除非图片文件。

第二个坑是“当天”的界定。APOD的更新并不严格等于自然日零点,它通常在北京时间当天下午到夜间才更新当天的条目。如果你在清晨去抓“今天”的图片,很可能拿到的是昨天的数据。处理办法很简单:抓当前日期时,如果返回的date字段不是你要的那天,按旧数据跳过即可,不要把它硬存成当天的文件。

4. 实战二:火星探测器照片数据的筛选与分析

4.1 火星照片接口的筛选维度

火星探测器照片接口是我觉得NASA接口里最有意思的一个,它允许你按火星车、火星日、地球日期、相机类型等维度查照片。以好奇号火星车为例:

def fetch_mars_photos(sol, rover="curiosity", camera=None): endpoint = f"/mars-photos/api/v1/rovers/{rover}/photos" params = {"sol": sol} if camera: params["camera"] = camera data = nasa_get(endpoint, params) return data.get("photos", [])

请求参数里的sol是火星日,指火星车在火星上度过的天数,从着陆那天算起。更常用的可能是earth_date,也就是对应的地球日期。但这两个参数不能同时传,接口文档里明确说它们是互斥的,同时传通常会报参数错误。

camera参数可以筛选特定相机,好奇号上有多个相机:导航相机、桅杆相机、化学相机等,每种拍摄的图片风格完全不同。如果你对数据进行了筛选,字段值不对会导致结果为空,这时候先检查一下相机名称缩写是否拼对。

4.2 多Sol循环拉取与数据整理

单次请求返回的照片数量是有限制的,我记得常规返回的条目通常不超过25条左右。想拿更长时间段的数据,就得按时间维度循环去拉。

我写了一个循环:遍历连续的火星日,把每张照片的关键信息抽出来,统一放到列表里,最后再用pandas转成DataFrame。

import pandas as pd def collect_mars_photos(start_sol, end_sol): all_rows = [] for sol in range(start_sol, end_sol + 1): photos = fetch_mars_photos(sol) for p in photos: all_rows.append({ "id": p["id"], "sol": p["sol"], "earth_date": p["earth_date"], "camera": p["camera"]["full_name"], "img_url": p["img_src"] }) print(f"sol {sol}: 获取到 {len(photos)} 张照片") time.sleep(0.3) return pd.DataFrame(all_rows) df = collect_mars_photos(1000, 1009)

这里有个细节值得多说一句:p["camera"]本身还是一个字典,里面有name、full_name等字段。我刚上手时图省事,直接用p["camera"]["name"],后来发现输出的是缩写,像NAVCAM_LEFT这种,做图表标签时很不友好。改成full_name之后就正常多了。

遇到嵌套字段时不要靠猜,可以先print(json.dumps(photos[0], indent=2, ensure_ascii=False))把结构完整打出来,再决定取哪一层。

4.3 用DataFrame快速统计

拿到DataFrame之后,数据就开始“活”了。我通常先看一眼整体情况:

print(df.info()) print(df.groupby("camera").size())

groupby输出直接告诉你哪个相机拍的照片最多。我实测下来,导航相机类的照片数量明显多于其他相机,这和火星车实际工作方式是对应的——导航相机几乎是日常持续拍摄。

如果你还想做更深入一点的验证,比如看哪些日期照片数量最多:

date_counts = df.groupby("earth_date").size().sort_values(ascending=False) print(date_counts.head(10))

这种多几步的聚合操作能帮你养成一个习惯:拿到任何数据集,先做基本的分布探查,而不是急着画图。分布对了,后面的分析才有意义。

5. 实战三:近地小行星数据解析与可视化

5.1 近地天体接口的数据结构

第三个接口是近地天体数据,返回结构比前两个复杂一些,但拆解起来更锻炼数据处理能力。

def fetch_neo(start_date, end_date): params = { "start_date": start_date.isoformat(), "end_date": end_date.isoformat() } data = nasa_get("/neo/rest/v1/feed", params) return data

返回结果的顶层是near_earth_objects,它是一个字典,键是日期,值是该日期所有小行星对象的列表。每个对象里有名字、预估直径、是否具有潜在威胁、接近速度等字段。

这里要注意close_approach_data是数组,不是单个字典。虽然大多数情况下它只有一个元素,但你写代码时最好按数组来处理,万一以后接口返回多条接近记录,你的代码也不会崩。

5.2 日期窗口与参数细节

这个接口有个硬性限制:一次请求最多查7天的数据,而且结束日期不能晚于当前日期。我一开始没有细看文档,直接传了一个月的日期范围,接口返回400错误,查了文档才发现问题。

解决办法很简单,把时间窗口切成7天一段,循环请求再合并结果:

from datetime import timedelta def fetch_neo_range(start_day, end_day): all_rows = [] current = start_day while current < end_day: seg_end = min(current + timedelta(days=6), end_day) data = fetch_neo(current, seg_end) for day_str, objects in data.get("near_earth_objects", {}).items(): for obj in objects: approach = obj["close_approach_data"][0] diameter = obj["estimated_diameter"]["kilometers"] all_rows.append({ "date": day_str, "name": obj["name"], "diameter_km": (diameter["estimated_diameter_min"] + diameter["estimated_diameter_max"]) / 2, "hazardous": obj["is_potentially_hazardous_asteroid"], "velocity_km_s": float(approach["relative_velocity"]["kilometers_per_second"]), "miss_distance_km": float(approach["miss_distance"]["kilometers"]) }) current = seg_end + timedelta(days=1) time.sleep(0.5) return pd.DataFrame(all_rows)

这段代码里我做了两个关键动作:直径取最小和最大估值的平均值,作为该天体的代表性直径;速度字段从字符串转float,因为接口返回的数值大多是字符串类型。不做转换的话,你后续做数学运算时会直接TypeError。

5.3 半分钟画一张散点图

数据整理完之后,可视化就是临门一脚。我习惯先做一个散点图,横轴是估计直径,纵轴是相对地球的接近速度,用颜色区分是否被标记为潜在危险天体。

import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(10, 6)) scatter = ax.scatter( df["diameter_km"], df["velocity_km_s"], c=df["hazardous"], cmap="coolwarm", s=(df["diameter_km"] + 0.01) * 80, alpha=0.7 ) ax.set_xlabel("Estimated Diameter (km)") ax.set_ylabel("Relative Velocity (km/s)") ax.set_title("Near-Earth Objects") plt.tight_layout() plt.savefig("neo_scatter.png", dpi=150)

点的大小我按照直径映射了一下,这样小行星越大,点在图上越明显。散点图价值在于一眼就能看出是否存在“又大又快的危险分子”,如果图上出现右上角深色的大圆点,那就值得多看两眼了。

matplotlib默认情况下对中文支持不友好,图例和标签直接用英文是最省事的做法。如果你确实需要中文,可以在代码里单独设置字体,那是另一个话题,这里就不展开了。

6. 常见故障排查与性能优化实录

6.1 HTTP状态码速查与处理建议

我在用这套接口过程中,几乎把常见的错误状态码都碰了一遍,这里整理成表格,你遇到问题时可以对着查:

状态码含义常见原因处理办法
401认证失败api_key没传或传错检查参数里是否真的带了正确的Key
403禁止访问Key无效或IP被封了一段时间换Key尝试,确认请求频率
404地址不存在endpoint拼写错误核对官方文档里的路径,别漏掉前缀
400参数错误日期格式不对、参数互斥、日期超过范围按文档逐个核验参数
429请求过于频繁触发限流等待Retry-After,或者降低请求频率
500/503服务端异常NASA接口临时故障用指数退避重试

一个比较隐蔽的排查技巧是:当状态码是401或403时,先确认你的API Key是否能正常访问首页。NASA在下发Key的时候偶尔会出现邮件丢了的情况,重新申请一次也未尝不可。另一个更隐蔽的情况是,某些网络环境下请求被中间层注入了一个假页面,状态码是200但JSON解析失败,这时候用print(resp.text[:200])看下响应头部,立刻就能知道拿到的是不是真正的JSON。

6.2 限流与缓存策略

NASA接口虽然比较慷慨,但不是无限制的。尤其是在批量抓取火星照片那种需要发几百个请求的场景里,限流是必然会发生的事。

我后来给自己定了三条规矩:

  • 批量请求前先估算总请求数,超过百次就拆成多批次跑;
  • 每两次请求之间至少间隔0.3到0.5秒;
  • 已经抓过的数据写一个简单的本地缓存,避免重复请求。

本地缓存做起来很简单,比如照片数据可以保存成JSON文件,文件名里带上查询参数。下次请求之前先看看缓存文件是否存在,存在就直接读文件,不发HTTP请求。这个做法不仅节省你的请求额度,还能大幅缩短重复运行脚本的时间。对于纯学习场景,缓存的价值甚至超过接口本身的稳定性。

6.3 接口相关的几个数据坑

除了HTTP层面的问题,数据本身的坑也不少见。我遇到的比较有代表性的有三个。

第一个是日期时区问题。接口返回的日期字段大多是UTC格式,如果你的脚本按本地时间做了日期分割,在跨时区边界时会出现数据错位。解决办法是做一个统一的parse_day函数,把字符串转成date对象时明确时区,不要依赖操作系统的默认设置。

第二个是字段缺失。近地天体接口里每个对象的字段几乎一样,但极少数历史对象可能缺少某些字段,例如相对速度为空。我的习惯是取字段用obj.get("字段名", None),不要用obj["字段名"]直接索引。前者遇到缺失只返回None,顶多让单行数据为空;后者直接抛KeyError,整个循环都会崩。

第三个是火星照片的图片链接并非永久有效。我试过跑历史照片归档时,有几个img_src返回了404。所以批量下载图片时,代码里一定要有异常捕获和失败记录,我习惯把它们写进一个failed.txt,跑完脚本再决定要不要手动重试。

6.4 后续还能怎么扩展

这个项目练完,扩展方向其实很多。我自己接着做的是把一个定时抓取脚本挂到服务器上,每天自动拉取APOD图片和当天的近地天体数据,然后推送一个总结到自己的通知渠道。整个过程中用到的技能点,从定时任务到文件管理,又额外补了一遍。

如果你对数据分析更感兴趣,可以往更重的方向走:把历史火星照片数据积累起来,分析不同火星季节下照片数量的变化;或者把近地天体数据收集半年,做一张动态的轨道图。这些想法都不需要换工具,pandas和matplotlib基本都能撑住。

7. 几点自己的后续计划

这个项目做到后面,我觉得收获最大的其实不是某个接口本身的用法,而是“如何稳妥地跟一个不完美但可用的外部服务打交道”。API会挂、限流会触发、数据会缺失、字段会变,这些才是真实工作中天天面对的问题。

如果你也跟着做了一遍,建议你在跑通这几个例子之后,回头尝试一个自己的小任务:比如把你所在城市每天的气象数据存下来,或者定期抓某个公开数据源的TOP榜。数据源是什么不重要,重要的是把“请求—清洗—存储—展示”这条链路跑熟了,以后遇到任何新的公开API,你都能用一个小时左右的时间完成从读到用的全过程。

最后再分享一个实用小习惯:所有批量抓取脚本的输出,一定不要用print糊墙式的堆在屏幕上。把关键信息写到日志文件里,或者至少用print加上时间戳,否则脚本跑几百条数据之后,你很难定位到底是哪一步出的问题。这个习惯帮我省下的排查时间,远超写日志本身花掉的那点功夫。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 4:40:23

投机解码调优实战:本地大模型推理从14 tok/s飙升到159 tok/s

本地跑大模型&#xff0c;我最烦的就是输出像挤牙膏。前段时间把 Qwen3.8-27B 拉到本地&#xff0c;默认配置下速度只有 14 tok/s&#xff0c;生成一段几百字的回答能等半分钟&#xff0c;根本没法当工具用。后来花了一个晚上做投机解码调优&#xff0c;把同一块显卡上的单流推…

作者头像 李华
网站建设 2026/10/14 4:40:16

MySQL数据可视化核心技能:SQL数据加工与查询优化实战指南

直接上手&#xff0c;不绕弯子。MySQL做数据可视化&#xff0c;真正被低估的技能点&#xff0c;是SQL层面的数据加工能力。很多人以为可视化就是拖拽图表、调色配色&#xff0c;但实际一接业务数据就露怯&#xff1a;要么数据粒度对不上&#xff0c;要么聚合结果差一行&#xf…

作者头像 李华
网站建设 2026/10/14 4:40:16

Java实现冒泡排序:从朴素版到优化版,彻底搞懂原理与复杂度

我帮不少准备算法面试和数据结构的同学做模拟测试&#xff0c;发现一个很有意思的现象&#xff1a;聊到排序算法&#xff0c;几乎所有人都会第一时间跳过冒泡排序。理由非常统一——太简单了&#xff0c;两层循环、一个临时变量、一个交换条件&#xff0c;看一遍就会。可真让他…

作者头像 李华
网站建设 2026/10/14 4:40:03

CSAPP链表实验详解:随机数生成与头插尾插实现

不绕弯子&#xff0c;直接说&#xff1a;这个实验是《深入理解计算机系统》课程里比较经典的一个链表专项练习。题目要求很简单&#xff1a;随机生成100个整数&#xff0c;范围1~999&#xff0c;如果第一个随机数是偶数&#xff0c;就按生成顺序依次插入到链表中&#xff1b;如…

作者头像 李华
网站建设 2026/10/14 4:39:13

机器学习中的数据泄露:为什么离线AUC很高,线上效果却崩盘?

上个月帮一个做风控模型的朋友排查线上效果&#xff0c;开发组的验证集AUC跑到了0.98&#xff0c;团队上下都挺兴奋&#xff0c;结果上线试运行一周&#xff0c;实际转化预测的区分度只有0.6出头。一群人围在一起查了整整两天&#xff0c;最后定位到问题出在一个看似无辜的特征…

作者头像 李华