资讯动态

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

发布时间:2026/10/14 4:40:40 来源:尧图企业网站定制
这段时间我在折腾数据可视化练手项目翻来覆去找公开数据源的时候发现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 matplotlibrequests负责发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, paramsparams) data resp.json()这里有个容易忽略的细节requests.get的params参数会自动把字典拼成URL后面的查询字符串你不需要手动拼?api_keyxxxdatexxx。写成字典的好处是参数多了以后代码依然清晰也方便做条件组装。响应的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, paramsNone, retries3, timeout30): 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, paramsparams, timeouttimeout) 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 接口返回结构APODAstronomy 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_dirapod_images): import os os.makedirs(save_dir, exist_okTrue) current start_day while current end_day: data fetch_apod(current) if data is None: current timedelta(days1) 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, timeout60) 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(days1) 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, rovercuriosity, cameraNone): 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(fsol {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], indent2, ensure_asciiFalse))把结构完整打出来再决定取哪一层。4.3 用DataFrame快速统计拿到DataFrame之后数据就开始“活”了。我通常先看一眼整体情况print(df.info()) print(df.groupby(camera).size())groupby输出直接告诉你哪个相机拍的照片最多。我实测下来导航相机类的照片数量明显多于其他相机这和火星车实际工作方式是对应的——导航相机几乎是日常持续拍摄。如果你还想做更深入一点的验证比如看哪些日期照片数量最多date_counts df.groupby(earth_date).size().sort_values(ascendingFalse) 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(days6), 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(days1) 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], cdf[hazardous], cmapcoolwarm, s(df[diameter_km] 0.01) * 80, alpha0.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, dpi150)点的大小我按照直径映射了一下这样小行星越大点在图上越明显。散点图价值在于一眼就能看出是否存在“又大又快的危险分子”如果图上出现右上角深色的大圆点那就值得多看两眼了。matplotlib默认情况下对中文支持不友好图例和标签直接用英文是最省事的做法。如果你确实需要中文可以在代码里单独设置字体那是另一个话题这里就不展开了。6. 常见故障排查与性能优化实录6.1 HTTP状态码速查与处理建议我在用这套接口过程中几乎把常见的错误状态码都碰了一遍这里整理成表格你遇到问题时可以对着查状态码含义常见原因处理办法401认证失败api_key没传或传错检查参数里是否真的带了正确的Key403禁止访问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加上时间戳否则脚本跑几百条数据之后你很难定位到底是哪一步出的问题。这个习惯帮我省下的排查时间远超写日志本身花掉的那点功夫。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑