news 2026/11/5 2:41:25

Python气象爬虫工程化:动态解析、重试限流与数据归一化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python气象爬虫工程化:动态解析、重试限流与数据归一化

简介:面向Python爬虫初学者与需要获取实时天气数据的开发者,这是一份以中国天气网为目标的实战项目资料,重点演示如何使用requests发送HTTP请求、借助BeautifulSoup解析HTML,并提取日期、天气状况、温度与风力等字段,可用于数据分析、展示或爬虫技能练习。压缩包共1498个文件,约8.03MB,包含840个py源码、515个pyc编译文件及25个exe等,另有txt说明、xml配置和测试相关文件,目录中保留虚拟环境与依赖记录,便于直接运行和对照学习。已有622人学习。资料提供完整代码示例与逐段解析,读者可据此理解请求构造、页面结构定位、字段清洗和异常处理思路,也能借鉴项目组织方式,把示例改造成面向特定城市的天气采集脚本,适合作为入门到进阶的实战参考。

1. 抓天气数据的难点不在发请求,而在判断它藏在页面哪一层

很多人对 Python 爬虫 的第一印象是requests.get加一行正则,跑通一次就以为完事。换成气象类页面,同样的写法第二天往往返回空列表——不是网络抖动,而是温度数字压根不在首次返回的 HTML 里。这件事值得认真拆解,因为它会迫使你区分三层:服务端渲染的静态标签、脚本异步注入的片段、以及压缩过的数据响应。先搞清哪一层出数据,后面的解析、限流、容错才有意义,否则所有优化都只是在错误的地基上刷漆。

2. 页面结构与数据定位:为什么正则总是先崩

2.1 先确认数据在哪一层,再谈选择器

气象页面看起来朴素,实际常常三种出数据的方式并存:首屏 HTML 里直接渲染好的七天预报<ul>,页面加载后由脚本再发一次请求拿到的实时观测,以及藏在<script>里的一段初始状态对象。新手最容易犯的错是从浏览器里复制一段看得见的 DOM,扔进正则,本地跑通,然后换个时段就全军覆没——因为复制的那份 DOM 是浏览器执行完脚本之后的结果,而requests拿到的是脚本执行之前的结果。这两者在结构上可能差了一整棵树。

判断顺序应该是自上而下:先看原始响应里有没有目标数值,再看页面引用了多少外部脚本,最后才去猜数据是不是由某个异步请求返回。这个顺序不能颠倒,否则你会花两小时调试一个根本不存在的选择器。常见的实现思路是写一个极简的探测脚本,只做「命中判断」,不做解析。

import os import re import requests BASE = os.environ["WEATHER_BASE"] # 站点根地址从环境变量注入,避免硬编码 UA = "Mozilla/5.0 (compatible; WeatherCollector/1.0)" def probe(city_code: str) -> None: url = f"{BASE}/weather/{city_code}" # timeout 必给,否则一个慢连接就能把整个脚本挂住 resp = requests.get(url, timeout=8, headers={"User-Agent": UA}) resp.encoding = resp.apparent_encoding # 站点未声明 charset 时的兜底 html = resp.text # 第一问:目标数值是否已经出现在首屏 HTML 里 print("温度命中:", re.findall(r"(-?\d+)\s*℃", html)[:5]) # 第二问:页面引用了多少外部脚本,数据可能在脚本里被注入 print("外部脚本数:", len(re.findall(r'<script[^>]+src="([^"]+)"', html))) # 第三问:页面里是否内联了一段大 JSON(初始状态) print("疑似初始状态:", bool(re.search(r"window\.__[A-Z_]+__\s*=", html)))

这段代码的价值不在于输出,而在于它把「猜」变成了「测」。timeout=8控制单次请求的耐心上限,爬虫里最忌讳的就是无限等待;resp.apparent_encoding是从字节序列反推编码,比盲信响应头里的 charset 更可靠,但计算成本偏高,只适合在探测阶段用;第三条正则匹配的是常见的初始状态挂载写法,不同站点变量名不一样,命中不了很正常,命中一次就能省掉后面所有的逆向工作。三个判断跑完,你基本就知道该写选择器、写 JSON 解析,还是去抓异步请求了。

提示:探测脚本不要写进正式流程,它只负责回答「数据在哪」,回答完就该删掉,留着只会让主逻辑变脏。

2.2 动态注入的判定:脚本变量、初始状态与异步请求

同样是「数据不在 HTML 里」,背后的机制却分成两类,处理方式完全不同。第一类是首屏就把数据塞进内联脚本,形如一个巨大的对象字面量,页面渲染时由前端代码读出来填进 DOM。这类页面的特征是:用curl拿到的 HTML 体积很大,肉眼能搜到城市名和温度,但正则匹配标签结构永远对不上。处理它的正确姿势是把这个对象截出来,用json.loads或ast.literal_eval还原成字典,绕开 DOM 直接取字段。

第二类是真正异步:首屏只有一个空壳,脚本执行后另发一次请求拿数据。这类页面的原始 HTML 里搜不到任何数值,脚本数量明显偏多。判断它需要看脚本里的请求路径,通常能从字符串常量里找到端倪;退一步,也可以按经验猜常见的路径规律,然后用探测脚本逐个验证。这里没有玄学,只有穷举和排除,区别在于熟手知道优先试哪几个。

把这两类混为一谈是血泪经验里最常见的一条。明明数据就在内联脚本里,却非要去模拟异步请求,结果多写了一层会话管理;或者反过来,数据在异步请求里,却对着空壳 HTML 死磕选择器,调到最后怀疑人生。判断依据很简单:搜得到数值 → 静态或内联;搜不到数值但脚本多 → 异步。一条命令的事,别靠感觉。

2.3 编码与压缩:看起来像空数据的两种情况

有时候数据确实在 HTML 里,你却没匹配到,问题出在字节层。第一种是编码错配:站点用 GBK 系列编码返回,响应头却缺失或写错,requests按默认的 ISO-8859-1 解码,中文全变成乱码,你写的「晴」「多云」自然一个都匹配不上,正则返回空列表,看起来和「没数据」一模一样。这类问题的排查信号是:len(html)正常,<div>之类的 ASCII 标签能匹配,只有中文匹配不到。

第二种是压缩。多数站点会返回 gzip 压缩的响应体,requests会自动解压,这点不用操心;但少数站点用 brotli,需要额外依赖,缺了就会拿到一堆二进制垃圾。判断方法是打印resp.headers.get("Content-Encoding")和resp.content[:16],前者告诉你用了什么压缩,后者告诉你字节开头是不是可读文本。这两个值一看,问题基本就定位了。

还有一个隐蔽的坑:不可见字符。有些页面在数值中间插入了零宽字符或全角空格,肉眼完全看不出来,正则\d+直接失效。稳妥的写法是在解析前统一做一次清洗,把\u200b、\xa0这类字符替换成普通空格,再做匹配。这一步成本极低,能省掉大量「明明有数据却解析不出来」的困惑。

3. 请求层:Session、退避重试与令牌桶各自解决什么

3.1 连接复用:Session 省下的不只是几十毫秒

用requests.get直接发请求,每次都会新建连接、走完 TCP 握手,HTTPS 还要再加一轮 TLS 协商。抓几十个城市时这点开销无所谓,但气象数据天生是「多城市 × 多时段」的二维结构,请求量上去之后,握手成本会占到总耗时的三成以上。Session的作用就是把连接留在连接池里复用,同时把 Cookie、默认请求头这些上下文一并维护住。

连接池不是越大越好。池子大小要和并发数匹配,池子太小会在高并发时排队等连接,表现为「明明没被限流却很慢」;池子太大则对服务端不友好,也容易触发对方的风控。常见的经验区间是每个进程 8 到 16 个连接,配合适度的并发,足够覆盖日常采集量。

import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(pool_size: int = 8) -> requests.Session: session = requests.Session() retry = Retry( total=3, # 最多重试 3 次 backoff_factor=0.8, # 退避节奏:0.8s / 1.6s / 3.2s status_forcelist=(429, 500, 502, 503, 504), allowed_methods=("GET",), # 只对幂等方法重试 raise_on_status=False, # 交给我们自己判断状态码 ) adapter = HTTPAdapter( pool_connections=pool_size, pool_maxsize=pool_size, max_retries=retry, ) session.mount("http://", adapter) session.mount("https://", adapter) session.headers.update({ "User-Agent": "Mozilla/5.0 (compatible; WeatherCollector/1.0)", "Accept-Encoding": "gzip, deflate", # 明确声明可接受的压缩方式 }) return session

关键在参数的含义而不是写法。total限制的是重试总次数,防止一次失败演变成无限循环;backoff_factor控制退避的指数增长,0.8 意味着第一次等待不到一秒,第三次接近三秒,给对端留出恢复窗口;status_forcelist只把 429 和 5xx 列为可重试,因为 4xx 里的 400、403、404 重试一万次也是同样的结果,纯属浪费;allowed_methods限定 GET 是幂等性的直接体现,如果采集流程里混进了写操作,这个限制能救命。raise_on_status=False让异常处理集中在一处,而不是散落在库内部。

注意:连接池复用意味着长连接会被服务端保持一段时间,如果对端主动关闭而你还在复用,会收到连接重置。这类错误表现为间歇性的ConnectionError,交给上层重试一次即可,不必深究。

3.2 重试不是 while 循环:退避、抖动与幂等

见过不少脚本的重试逻辑长这样:while True: try: ... except: continue。这行代码在网络抖动时会把对端打爆,在对方限流时会让情况更糟,最要命的是它没有终止条件,一个失效的地址能让整个任务卡死到天亮。真正的重试应该是一个带状态机的过程:限定次数、按指数退避、加随机抖动、区分错误类型。

抖动这件事值得单独讲。如果十个线程同时失败、同时按 0.8 秒退避,它们会在同一毫秒再次冲击服务端,形成所谓的惊群。给退避时间乘一个 0.5 到 1.5 的随机因子,请求就会被摊平到一个小窗口里,对端看到的压力曲线是平滑的。这个改动只有一行,但效果比调任何参数都明显。

哪些错误该重试,也有一套判断标准。连接超时、读超时、429、5xx,属于「可能自愈」,重试有意义;域名解析失败、404、403,属于「确定性错误」,重试只会浪费时间;JSON 解析失败比较特殊,要看是否偶发,如果同一地址每次都解析失败,那说明结构变了,不是重试能解决的问题。把这几类分开处理,重试逻辑才算完整。

3.3 频率控制:令牌桶为什么比 sleep 更贴近真实需求

最简单的限速是每次请求后time.sleep(0.5)。它能用,但有两个硬伤:一是把「限速」和「耗时」绑死了,一次请求如果本身花了 2 秒,加上 sleep 就是 2.5 秒,实际速率远低于设定值;二是无法应对突发,比如你只想在开头快速拉一批历史数据,之后再转入低频增量,固定 sleep 表达不了这种意图。

令牌桶把「速率」和「突发」拆成了两个参数。桶以恒定速率补充令牌,请求取走令牌才能发出,桶容量决定了允许多大的突发。速率设成每秒 2 个、容量设成 4,意味着长期平均不超过 2 QPS,但允许瞬间连发 4 个请求。这个模型和真实采集需求高度吻合:需要补数据时用掉积累的令牌,平稳期则被速率约束住。

import threading import time class TokenBucket: def __init__(self, rate: float = 2.0, burst: int = 4): self.rate = rate # 每秒补充的令牌数,即长期平均 QPS self.burst = burst # 桶容量,即允许的瞬时并发上限 self.tokens = float(burst) self.ts = time.monotonic() self.lock = threading.Lock() def acquire(self, n: int = 1) -> None: while True: with self.lock: now = time.monotonic() # 按流逝时间补充令牌,上限不超过桶容量 self.tokens = min(self.burst, self.tokens + (now - self.ts) * self.rate) self.ts = now if self.tokens >= n: self.tokens -= n return wait = (n - self.tokens) / self.rate time.sleep(wait) # 睡在锁外面,避免阻塞其他线程

用time.monotonic()而不是time.time(),是因为后者会受到系统时钟调整影响,采集任务跑上几个小时,一次时钟回拨就能让令牌计算出错。sleep放在锁外面是关键细节:如果在持锁状态下睡眠,其他线程连计算余量的机会都没有,整个桶就退化成了串行锁。这两处不写对,令牌桶不但不省事,还会成为新的性能瓶颈。

4. 解析层:从标签树到一张能算的表

4.1 解析器容错差异:为什么同一段 HTML 给出不同结果

气象页面的 HTML 质量普遍不高,未闭合的<li>、错位的<span>、属性值里带引号,都是常事。不同解析器对同一段破损 HTML 的处理结果可能完全不同:lxml修补能力强、速度快,遇到缺失的结束标签会自动补全;标准库的html.parser更宽松,但补全策略不一样;html5lib最贴近浏览器的行为,代价是慢一个数量级。

选择依据是你的用途。如果只是取几个稳定存在的字段,lxml的容错已经足够,速度优势在成千上万次解析里非常明显;如果发现某个字段偶发缺失,而你确信页面上有,那就要怀疑是修补策略导致的层级错位,换成html5lib对比一次,往往能立刻看出差别。这个对比手法比盯着源码看半天有效得多。

还有一个容易忽略的点:选择器命中的数量。写soup.select("ul.t li")之前,先打印len(soup.select("ul.t")),如果它是 2 而不是 1,说明页面上有两个同类元素,你取[0]拿到的可能是一个隐藏模板或者广告位。这类错误的症状是「字段有值但明显不对」,比直接报错更难查。

4.2 字段抽取与归一化:把「3 级」「微风」变成可计算的数值

原始页面里的数据几乎都是给人看的,不是给程序算的。温度可能是「12℃」也可能是「12/18℃」,风力是「3 级」「微风」「3-4 级」混着来,日期可能是「今天」「12日」这种相对表述。如果直接把字符串塞进数据库,后面想画趋势图、做同比,全都要在查询时再做一次清洗,那才是真正的技术债。

正确的做法是在解析阶段就把结构化和归一化一起完成:文本转数值、区间转上下界、相对日期转绝对日期。归一化函数要能容忍脏数据,遇到无法识别的输入返回None而不是抛异常——采集任务里一条脏数据不该拖垮整批。

import re from dataclasses import dataclass, field from typing import Optional @dataclass(frozen=True) class DailyRecord: city_code: str date: str # 统一成 YYYY-MM-DD high: Optional[int] # 摄氏度,无法识别时为 None low: Optional[int] wind_level: Optional[int] # 风力等级,微风记 0 source: str = field(default="daily") def to_int(text: Optional[str]) -> Optional[int]: """从任意文本里抠出第一个整数,失败返回 None""" if not text: return None m = re.search(r"-?\d+", text) return int(m.group()) if m else None def parse_span(text: Optional[str]) -> tuple: """解析 '12/18℃' 这类区间,返回 (low, high)""" if not text: return None, None nums = [int(x) for x in re.findall(r"-?\d+", text)] if len(nums) >= 2: return min(nums), max(nums) return (nums[0], None) if nums else (None, None) def parse_wind(text: Optional[str]) -> Optional[int]: if not text: return None if "微风" in text: return 0 m = re.search(r"(\d+)\s*级", text) return int(m.group(1)) if m else None

DailyRecord用frozen=True不是为了性能,而是为了约束:一条记录一旦生成就不该被后续步骤改写,想在流水线后半段补字段,就新建一个对象,让数据流向始终单向。parse_span用min/max而不是按位置取,是因为有些站点的高级预报会把高温写在前面;这种防御性写法看起来很笨,但它是唯一能同时兼容两种排版的方式。parse_wind里「微风」单独判断,是因为它不带数字,纯正则永远匹配不上,这类文案映射只能靠人工枚举,别指望自动推断。

4.3 数据结构:一条记录该带哪些字段

字段设计决定了这份数据以后能干什么。最常被漏掉的是时间语义:date是预报日期还是观测日期,collected_at是采集时刻,两者混为一谈,后续做「预报准确率回算」时就彻底没法对齐。气象数据天生带时间维度,采集时刻必须单独存,它记录的是「你在什么时候看到的这条预报」,而不是「这条预报描述的是哪一天」。

第二个要考虑的是唯一键。同一城市同一日期,一天之内你可能采了十次,每次的预报值都可能微调。如果把(city_code, date)当唯一键直接覆盖,历史预报就被抹掉了;正确做法是(city_code, date, collected_at)或至少精确到小时。这个决策在生产环境里几乎不可逆,前期图省事少存一个字段,后期想补数据只能从头再跑一遍。

第三个是来源标记。同一个城市的数据可能来自页面渲染、初始状态、异步响应三种路径,保留一个source字段,出问题时你能立刻判断是哪条路径的解析出了问题,而不是对着一个数字发呆。这类元数据字段成本近乎为零,价值却随系统复杂度线性增长。

5. 避坑与排查:气象数据采集最常见的五类翻车

5.1 现象:昨天跑通,今天全是空列表

原因:绝大多数不是代码问题,而是页面结构或数据路径变了。常见的情况有三种:站点把某个字段从页面渲染改成了异步加载,于是原来的选择器命中一个空壳;URL 里多了一个必须携带的查询参数,缺失时返回的是默认城市而不是你请求的城市;响应头里的编码声明变了,中文变成乱码导致文案匹配失败。

解决:建立一个最小成本的回归检查,每次运行前先对目标页面做一次快照并比对摘要。不要比对全文,全文一直在变,比对的是「关键字段是否仍可解析」——比如能否从响应里提取出至少一个温度值、能否命中至少一条日期。这条检查失败就立刻停止任务并保留原始响应,而不是硬着头皮跑完全程。

import hashlib import pathlib def dump_snapshot(text: str, tag: str) -> pathlib.Path: # 用内容摘要做文件名,重复抓取同一份内容不会覆盖历史 digest = hashlib.md5(text.encode("utf-8")).hexdigest()[:8] path = pathlib.Path("snapshots") / f"{tag}-{digest}.html" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") return path

这个函数只做一件事:把出问题的现场留下来。采集类任务最怕的是「当时没留证据」,等第二天想复现,页面早就变了。摘要做文件名既避免了覆盖,又能一眼看出内容有没有变化。

5.2 现象:字段有值,但数值明显不对

原因:选择器命中了错误的元素。气象页面里「今天」和「明天」往往共用同一套 class,第一个li可能是一个隐藏的模板节点;有些页面为了自适应,会把同一份数据渲染两遍,一个给桌面端一个给移动端,用 CSS 控制显隐。你取到的可能正是那个看不见的副本。

解决:别直接取[0],先把候选集打印出来看长度和文本。如果长度明显大于预期天数,说明有冗余节点,此时应该用更精确的定位——父元素的 id、或者带业务含义的属性。另一个通用手法是过滤掉文本为空的节点,隐藏模板通常是空的或者只有占位符。

5.3 现象:跑到一半开始报连接错误或 429

原因:两个层面。一是没有设置连接池上限和超时,短时间发起大量请求,服务端把你当成了异常流量;二是重试逻辑把 429 也当成普通失败,立刻重试,越重试压力越大,形成正反馈。这两种情况在监控上的表现高度相似,都是「前几十个成功,后面全挂」。

解决:先加令牌桶把速率压下来,再把 429 从「立即重试」改成「长退避」。429 的含义是「你太快了」,正确的应对是等待而不是加速。如果确认已经严重影响任务,就降低并发、拉长间隔,让整体节奏回到对方能接受的范围。这条经验没有捷径,踩过一次就会记住。

5.4 现象:数据看着对,但时间序列缺了几个小时

原因:去重逻辑写错了。用(city_code, date)做唯一键覆盖写入,每采集一次就把之前的记录替换掉,最终只剩下最后一次的结果。表面上看数据完整,实际上历史的预报变化全丢了,想做「预报偏差分析」时才发现根本没法回溯。

解决:唯一键必须包含采集时刻,写入用追加而不是覆盖。如果存储层不方便改,至少要把原始响应按时间戳落盘一份,哪怕后面不解析,也比没有强。这类问题的代价是隐性的,往往在项目做了两个月之后才暴露出来。

5.5 现象:内存持续上涨,跑几小时就被系统杀掉

原因:最常见的是把每次的响应体都追加进一个大列表,想着最后统一处理。单次响应几百 KB,跑上几万次就是几个 GB。其次是没关闭响应对象,虽然requests在读取完text后会释放连接,但如果在异常路径上提前返回,连接可能悬挂在池子里。

解决:改成流式处理,解析完立刻丢弃原始文本,只保留结构化结果;需要留存原文时走落盘而不是内存。用生成器把「抓取 → 解析 → 写入」串成管道,任何时刻内存里只有当前这一条记录。这个改动能把内存占用从线性增长压成常数,代价只是代码结构要调整一次。

6. 设计取舍:脚本和采集管线之间那条线画在哪

一次性的采集脚本和能长期跑的管线,差别不在代码量,而在职责边界是否清晰。我倾向把整条链路切成三层,每层只回答一个问题:

层输入输出失效时的典型表现
请求层URL、会话配置原始响应文本超时、429、编码错乱
解析层原始文本结构化记录字段为 None、条数不符
存储层结构化记录幂等写入结果重复、覆盖、缺时段

这条边界最实际的价值是排障。字段为None时你不必去怀疑网络,直接拿快照复现解析;出现 429 时也不用去看解析逻辑,问题一定在请求层。三层混在一个函数里的脚本,出问题时只能从头到尾重读一遍,这就是踩坑和经验的差别。

另一个取舍是「要不要为容错写那么多代码」。我的答案是分层看待:请求层的容错必须写足,因为网络是外部世界,你控制不了;解析层的容错要克制,遇到结构性错误宁可让任务失败并保留现场,也不要写一堆兜底逻辑把真正的变更掩盖掉;存储层必须幂等,因为重跑是常态而非例外。

判断一段采集代码是否合格,我会问一个很朴素的问题:它失败的时候,能不能在五分钟内告诉你为什么失败。做不到这一点,功能再全也只是黑匣子。从那以后我每次写采集脚本,都强制先跑一遍「原始响应快照 + 关键字段命中」这两步,再动解析逻辑——这个习惯让我少熬了很多个夜晚。希望帮到你。

本文还有配套的精品资源,点击获取

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

模拟退火算法在AGV路径规划中的工业实践

1. 项目背景与核心挑战在工业自动化与物流仓储领域&#xff0c;机器人路径规划一直是影响效率的关键因素。想象一下&#xff0c;在一个大型电商仓库中&#xff0c;每天有数百台AGV小车需要在数万平米的场地内穿梭取货。如果每台小车多走10米&#xff0c;整个系统每天就会浪费上…

作者头像 李华
网站建设 2026/11/5 2:41:12

10A太阳能充电控制器自研:PWM三段式充电与采样校准

简介&#xff1a;面向电子初学者、嵌入式开发者与太阳能DIY爱好者的10A太阳能充电控制器完整资料包&#xff0c;围绕PWM/MPPT充电控制、电池管理与安全保护展开&#xff0c;可用于理解中小型光伏系统的软硬件协同设计。压缩包共390个文件、约1.99MB&#xff0c;包含c、h源文件与…

作者头像 李华
网站建设 2026/11/5 2:41:02

电商系统数据库设计与高并发优化实战

1. 电商数据库设计核心挑战做电商系统最头疼的就是数据库设计&#xff0c;我经历过3个日订单量10万的电商平台重构&#xff0c;发现90%的性能问题都源于早期数据库设计缺陷。一个典型的电商系统需要同时处理商品信息、用户行为、交易流水、库存变动等高并发写入&#xff0c;还要…

作者头像 李华
网站建设 2026/11/5 2:40:52

MySQL查询优化实战:提升数据库性能的关键技巧

1. MySQL查询效率优化实战&#xff1a;从原理到技巧作为一名长期奋战在一线的数据库工程师&#xff0c;我深知SQL查询效率对系统性能的关键影响。在实际工作中&#xff0c;我们经常会遇到查询缓慢、插入卡顿等问题&#xff0c;这些问题往往源于对MySQL底层机制的理解不足。今天…

作者头像 李华
网站建设 2026/11/5 2:39:57

Blender渲染参数与出图流程:引擎、采样、光程、降噪与提速

1. 先把“渲染”这件事拆成三层来看很多人第一次点开 Blender 的渲染面板&#xff0c;会被密密麻麻的折叠菜单劝退&#xff0c;其实真正影响一张简单静物图成色的&#xff0c;就那么十来个参数。渲染在 Blender 里不是“一个按钮”&#xff0c;而是三层不同性质的东西叠在一起&…

作者头像 李华
网站建设 2026/11/5 2:39:43

EKF与神经网络融合的无人机轨迹预测优化实践

1. 项目背景与核心价值在工业控制和自动驾驶领域&#xff0c;运动物体的轨迹估计一直是个经典难题。传统滤波算法如卡尔曼滤波&#xff08;KF&#xff09;在理想线性系统中表现优异&#xff0c;但面对现实世界的非线性、噪声干扰等问题时往往力不从心。我最近在做一个无人机追踪…

作者头像 李华