简介百度迁徙数据爬取项目是一款适合Python爬虫入门至中级学习者以及交通规划、公共卫生等研究人员的实用脚本工具能够自动从百度地图实时页面或历史数据中抓取城市间人口迁徙信息并整理成便于后续分析的结构化表格。压缩包共1个py文件体积仅2KB代码短小精悍在有限篇幅内完整覆盖了HTTP请求、网页解析、字段提取与表格导出等核心爬虫环节。已有1564人浏览学习因贴近真实业务场景而受到爬虫爱好者关注脚本以春运数据为典型样例演示了如何定位迁徙信息对应的网页结构借助正则表达式抓取日期、城市以及迁入迁出量并通过数据清洗与格式转换后保存为通用表格文件。针对动态加载与反爬限制代码中给出了浏览器自动化工具与代理池管理两种进阶扩展思路方便读者根据实际需求加固爬虫。整个脚本具有即拿即用的特点既可直接运行获取特定日期数据也能作为课程设计或科研项目的二次开发基础由此掌握爬虫全流程与数据清洗方法。1. 百度迁徙数据爬取从抓包到落库摸清接口就能拿全春运人口流向做交通分析的人多半遇到过这种需求手里没有运营商数据却想还原某个城市春节前后的人口迁入迁出曲线。百度迁徙平台恰好提供了一个公开入口——以地图热力图形式展示全国热门迁徙路线、城市迁入迁出趋势和实时迁徙指数。早期版本直接暴露了 JSONP 接口用网络爬虫就能批量取数十几行 Python 就能把一座城市整个春运周期的数据拉下来。这篇笔记会把接口怎么找、参数怎么设、数据怎么清洗、哪些地方容易翻车一次讲透适合有 Python 基础、想拿真实动态数据做项目练习的开发者。2. 摸清数据源头迁徙平台的页面结构与 JSONP 接口2.1 从页面行为看数据请求趋势图是怎么加载出来的打开百度迁徙平台默认是一个中国地图页面顶部有「迁入趋势」「迁出趋势」「热门迁徙路线」几个 Tab。随便点一个城市右侧会出现一条折线图下方是排行榜。如果直接右键查看源代码你会发现页面主体是空壳所有数据都是异步加载的。我一般习惯先打开浏览器开发者工具切到 Network 面板勾选 Fetch/XHR然后重新点击选项卡就能看到一批带jsonp后缀的请求。请求名比较直白比如historycurve、top、cityrank。选中任意一条右侧能看到完整的请求 URL、Query String Parameters以及 Response 里的内容。这一步的关键是确认数据不是前端本地计算出来的而是服务端直接返回——Response 里是一个以callback(...)包着的 JSON 结构里面就是我们要的数值序列。这里有个容易被忽略的细节这些接口都是 JSONP 形式而不是标准 JSON。JSONP 是早期前端用来绕过跨域限制的约定服务端把数据塞进一个函数调用里返回由前端 script 标签执行。爬虫直接请求它时收到的是jQuery...({...})这样的文本必须做一次「去掉函数外层」的处理才能拿到纯 JSON。2.2 接口地址与参数解析dt、id、type 和 callback 的含义把请求 URL 拆开看核心参数就几个。以historycurve接口为例https://huiyan.baidu.com/migration/historycurve.jsonp ?dtcity id360 typemove_in callbackjQuery123dt表示数据类型传city是城市维度传province是省份维度传nation是全国维度。id是城市编码这里用的是行政区划代码比如 360 是某座沿海城市的区划前几位实际返回的是id360对应的城市。注意它不是完整的六位数县级码而是市级四位码的某种映射得从另一个接口拿。type决定方向move_in是迁入move_out是迁出。callback是 JSONP 回调参数随便填一个不重复的字符串就行服务端会原样包在返回体外面。另外两个接口也类似top接口返回热门迁徙路线比如从哪到哪、排名、迁徙指数参数里会有city之类字段cityrank接口返回全国迁入/迁出排行榜参数多一个date用于指定日期。我在模拟请求时通常会保留callbackquery这种固定值方便解析时用正则直接提取。如果你用 requests 直接请求可以完全不传 callback服务端大概率会返回纯 JSON。但有的版本不稳定所以稳妥做法是带上 callback然后在解析层统一处理。2.3 用 requests 构造最小请求一条命令拿回 JSONP 并解析import requests import re import json url https://huiyan.baidu.com/migration/historycurve.jsonp params { dt: city, id: 360, type: move_in, callback: query } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://huiyan.baidu.com/migration/ } resp requests.get(url, paramsparams, headersheaders, timeout10) text resp.text # 去掉 JSONP 外层函数 json_str re.sub(r^[^(]*\((.*)\);?\s*$, r\1, text, flagsre.S) data json.loads(json_str) print(data.keys()) print(data.get(data))这段代码做了三件事带参数请求 JSONP 接口用正则把query({...})里的 JSON 主体剥出来再用json.loads转成字典。re.sub里的正则从字符串开头匹配到第一个左括号然后一直抓到末尾的右括号中间允许换行。需要注意返回体末尾可能带分号所以;?是必要的容错。解析后data[data]里通常是一个数组每个元素是{date: 2024-02-01, value: 123.45}这样的结构value就是当天的迁入指数。如果你在测试时发现data是None先不用怀疑正则多半是请求头里的 Referer 没带服务端返回了空壳。我在本地测试时发现这个接口对 Referer 非常敏感带对之后响应体才是正常数据。请求频率也不要一开始就拉满先跑通单个请求再考虑批量。3. 完整抓取流程把迁入迁出趋势、热门路线、城市排行三类数据落地3.1 定义抓取函数与统一解析器实际写爬虫时不建议每个接口单独处理做一个统一入口更省事。我习惯先把请求和 JSONP 解析封装成一个函数接口地址作为参数传入这样后续加新接口只需要补 URL 和字段映射。def fetch_jsonp(url, params): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://huiyan.baidu.com/migration/ } resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() text resp.text match re.search(r^[^(]*\((.*)\);?\s*$, text, flagsre.S) if not match: raise ValueError(funexpected response: {text[:200]}) return json.loads(match.group(1))fetch_jsonp返回的是已经转成 Python 字典的完整响应。如果响应不是 JSONP 格式比如被反爬系统拦截返回 HTMLraise_for_status不会报错状态码可能是 200但正则匹配不到内容这时候抛出异常可以帮我们快速发现问题。接着定义三个数据函数def get_history(city_id, move_type): url https://huiyan.baidu.com/migration/historycurve.jsonp params {dt: city, id: city_id, type: move_type, callback: query} data fetch_jsonp(url, params) return data[data] def get_top(city_id, date_str): url https://huiyan.baidu.com/migration/top.jsonp params {dt: city, id: city_id, date: date_str, callback: query} data fetch_jsonp(url, params) return data[data] def get_rank(date_str, move_type): url https://huiyan.baidu.com/migration/cityrank.jsonp params {dt: city, type: move_type, date: date_str, callback: query} data fetch_jsonp(url, params) return data[data]这里date_str的格式不同接口可能不一样有的要YYYY-MM-DD有的要YYYYMMDD。常见做法是先在官方页面抓一次包看请求参数里 date 的实际值再决定怎么格式化。我在模拟项目 X 里遇到过top接口传2024-02-10有效、cityrank必须传20240210的情况所以这一步不要靠猜。3.2 按城市和日期批量抓取控制请求频率拿到单城市数据后批量就是对参数做循环。但循环里有两个坑一是城市编码表要提前准备好二是抓取间隔不能是死循环里的裸time.sleep(0.5)因为本地网络波动会导致实际间隔不均反而更容易被限流。import time import csv city_ids [360, 320, 440, 510] # 示例城市编码实际操作时从映射接口获取 date_range [2024-01-26, 2024-02-24] # 春运时间段 with open(migration_trend.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([city_id, date, move_in, move_out]) for cid in city_ids: try: in_data get_history(cid, move_in) out_data get_history(cid, move_out) except Exception as e: print(f[{cid}] failed: {e}) time.sleep(2) continue in_map {item[date]: item[value] for item in in_data} out_map {item[date]: item[value] for item in out_data} for d in sorted(in_map.keys()): if date_range[0] d date_range[1]: writer.writerow([cid, d, in_map.get(d, ), out_map.get(d, )]) time.sleep(1.2) # 两次城市之间留出足够间隔这里的time.sleep(1.2)是深拷贝官方页面操作节奏出来的经验值。实测下来每城市请求两个方向各一次后休息 1 秒左右比较稳太快会在几分钟后触发验证码。utf-8-sig是为了让 CSV 在 Excel 里打开不乱码写中国数据时这个细节值得养成习惯。还有一个容易出错的地方get_history返回的迁入和迁出数据长度可能不一致。比如某城市迁出曲线从腊月十六开始迁入曲线从腊月二十才开始直接按in_map.keys()循环会导致后面缺失两天没有写。上面的代码用get(d, )兜了底但更可靠的做法是先取两个日期集合的并集再循环。3.3 数据清洗与落盘CSV 字段设计与去重接口返回的原始值并不全是干净的数值。historycurve的 value 是浮点数但有时会出现null比如当天该方向没有统计到数据。top接口里的迁徙指数则是字符串还是数字取决于版本我看到过index: 10.36的情况。清洗时最好在写入前统一转 float转不了就填空。def clean_value(v): if v is None: return try: return float(v) except (TypeError, ValueError): return str(v).strip()除了值字段命名也要先想清楚。我常用的一套字段是city_id, date, move_in, move_out, in_rank, out_rank。如果你还抓了热门路线就加from_city, to_city, index, rank。避免在原文件上直接改每个接口单独落地一个文件最后再用 pandas 按 city_id 和 date 做 merge。这样做的好处是单个接口失败时不需要重跑全部。对于热门路线数据top接口返回的数组里通常包含city_name和一组target子对象每个子对象有name目的地城市、value指数、rank。写文件时要把嵌套结构拍平。下面这段是常见的拍平写法def flatten_top(raw): rows [] for item in raw: src item.get(city_name) for target in item.get(target, []): rows.append({ src_city: src, dst_city: target.get(name), index: clean_value(target.get(value)), rank: target.get(rank) }) return rows值得提醒的是city_name在有的版本里是name有的版本里是中文名城市我建议以实际抓包返回为准不要拿网上老代码直接套。这也是这类爬虫项目最大的一个变数平台改过字段名老博客里的写法很可能已经失效。4. 踩坑与排查反爬、字段缺失、时间校准一个都别漏4.1 请求头缺 Referer返回 200 但 data 是 null现象用 requests 直接请求接口状态码 200响应体是一段query(null)正则解析后data为None。原因服务端检查了Referer头没有正确的来源域名时直接返回空数据而不是报错。这是比较隐蔽的一种反爬因为状态码正常很多新手会反复调参数实际上问题在请求头。解决在 headers 里显式设置Referer: https://huiyan.baidu.com/migration/。如果你是从其他页面发起的请求Referer 必须改成平台页面地址否则同样会被过滤。我测试过Referer 缺失和 Referer 错误都会导致空数据只有精确匹配才稳定。4.2 callback 参数写错解析出的 JSON 混入前缀正则匹配失败现象re.sub报错或者解析出的 json 里多出一个带花括号的字符串。原因不同接口对 callback 的容忍度不一样。有的接口要求 callback 必须是合法的 JS 标识符如果你传了带数字开头的字符串服务端会返回一个标准的 JSON 而不是 JSONP有的接口会把 callback 原样塞进返回体导致正则按函数形式匹配失败。解决统一使用callbackquery这种简单合法标识符。同时正则要兼容「没有外层函数」的情况先判断文本是否以query(开头不是就直接json.loads。我在模拟项目 X 的排错过程中就是这么改的遇到过两次所谓「接口挂了」其实都是 callback 参数导致的格式回退。4.3 当天日期数据不完整曲线末尾缺失误判为爬取失败现象抓今天的数据发现最近 3 天的 value 是 null或者根本没有返回这些日期的条目。原因迁徙平台的数据不是实时全量更新的当天和前一两天的数据往往处于「预估/未完稿」状态接口会延迟发布。这是业务规则不是反爬。解决抓取时把日期范围往前推两天比如要分析 2 月 10 日到 2 月 20 日实际请求的数据范围写到 2 月 22 日。这样最终拼接后曲线才完整。更重要的是不要用「今天」作为最后一天否则你拿到的新鲜数据里尾巴是缺的画图时会掉链子。4.4 高频批量请求触发验证码爬几分钟后响应变成 HTML现象批量脚本跑了几十个城市后突然所有返回体的开头不再是query(而是html.../html状态码依然是 200。在浏览器里打开平台页面出现滑块验证或点选验证。原因虽然没有明确限流阈值但按几百毫秒一次的高频请求持续几分钟服务端会把这个 IP 标记为异常。JSONP 接口用的是 GET 请求没有签名难度的接口通常靠频率和请求头双重控制。解决把请求间隔提升到 1.5 秒以上并为每次请求随机打乱 User-Agent。我已经踩过这个坑后来强制设置time.sleep(random.uniform(1.2, 2.0))并且在脚本里记录最近一次请求时间防止多个线程同时醒来后瞬间打爆。如果已经被验证码拦住停止脚本等 10 到 15 分钟再继续硬顶是没有用的。4.5 城市 ID 映射缺失id 参数不是地图网页里的数字而是城市代码现象从地图页面 URL 里复制了一段idxxx比如id1请求后返回的数据对应的是某个莫名其妙的地方和自己想抓的城市对不上。原因迁徙平台的id不是行政区划代码而是平台内部的实体编号。页面 URL 里的参数会随路由变化有的版本里显示的是city360有的显示id360这个360恰好是城市代码但它不是标准国标码需要从平台的「城市列表」接口里动态获取。解决用另一个接口拉取城市对照表url https://huiyan.baidu.com/migration/citylist.jsonp params {callback: query} data fetch_jsonp(url, params) city_id_map {} for item in data[data]: # 结构示例: {id: 360, name: 某市, short_name: 某} city_id_map[item[name]] item[id]然后你就可以用城市中文名做索引把「某市」映射成360再传给get_history。这一步在批量抓取前做一次把映射表存成 JSON后面所有城市都从这份文件里取 id。我见过有同事直接硬编码城市 id 表结果平台更新过一次 id 后全表作废所以动态拉取才是长久之计。5. 验证数据有效性拿官方页面交叉核对再算一份净迁入排名数据爬下来不等于能用先做两步验证。第一步是交叉核对在平台页面手动选择迁入趋势记录某城市某天的指数再和本地 CSV 里同一行的 value 对比。我通常取三个不同日期的点包括腊月二十八、正月初六、正月十五这三个节点数值变化明显只要有一个对不上就说明字段映射或日期解析有问题。注意页面上显示的数值可能是保留一位小数而你抓到的可能是原始浮点对比时先四舍五入到小数点后一位。第二步是用数据算一份净迁入排名。把迁入值减去迁出值正数代表净流入负数代表净流出。春运期间最有价值的信息不是绝对值而是净迁入的排序变化。比如某劳务输出型城市迁出高峰在腊月二十多号而净迁入为负到了正月初六开始返程净迁入转正。这个信号可以用来判断返程高峰的启动时机。import pandas as pd df pd.read_csv(migration_trend.csv) df[net] df[move_in] - df[move_out] pivot df.pivot_table(valuesnet, indexcity_id, columnsdate, aggfuncsum) peak_net pivot.max(axis1).sort_values(ascendingFalse) print(peak_net.head(10))pivot_table会把同城市同一天的多个值加总防止因为重复抓取导致数据叠加。max(axis1)取每个城市在观测期内的净迁入最高值排名靠前的城市就是节日期间人口净流入最大的地方。如果你关心特定日期直接pivot[[2024-02-10]]单独看。做完这两步你的数据集才算真正可用。从那以后我每个迁移数据项目都会强制走一遍「抓包 → 单请求验证 → 批量 → 交叉核对」的完整链路宁可多花十分钟确认三个日期点也不等画图时才发现整张表的值全部偏移。希望这份笔记能帮你在做类似数据采集时少踩几个坑。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?