Python爬虫实战:从抓包到可视化,稳定采集百度迁徙数据的完整方案
简介针对百度迁徙数据抓取编写的爬虫脚本适合网络爬虫入门者及需要研究人口流动、交通规划、公共健康等领域的数据分析人员。资源包仅含一个Python脚本文件压缩后约2KB代码精简便于直接阅读、修改与二次复用脚本通过自动化方式请求百度地图迁徙相关接口解析迁入迁出数据并整理为结构化表格省去手动采集与整理的繁琐流程。已有1553人学习下载。借助该脚本可以直观理解HTTP请求、HTML解析、数据清洗与文件存储等爬虫核心环节脚本涉及requests、Selenium、pandas等常用库的综合运用并预留命令行参数输入用户只需指定日期即可自动抓取对应数据。整体而言这是一份轻量实用的爬虫示例适合教学演示、课程作业或作为自定义数据采集任务的起点尤其对于希望掌握动态网页抓取与数据处理流程的读者具有参考价值。 很多人学 Python 爬虫第一反应都是去抓招聘网站结果被登录墙、验证码、字体反爬轮番教育。我后来换了一个数据源百度迁徙。它不需要登录、不需要处理字体反爬返回的是干净的 JSON数据覆盖全国城市特别适合拿来练手和做研究。这篇文章把我从抓包定位到落地爬虫、再到可视化和长期稳定运行的一整套方案完整写出来看完你能直接抄作业也能避开我踩过的那些坑。1. 为什么选百度迁徙当爬虫练手项目1.1 招聘网站数据难抓迁徙数据反而香Python 爬虫圈里天天有人问“怎么抓 boss 直聘”“怎么抓智联招聘”但说实话这类站点对爬虫的防御是逐年加码的登录态校验、滑块验证码、字体反爬、接口参数加密样样齐全。新手照着老教程抓第一关就可能卡在请求头签名上很容易劝退。百度迁徙是另一类典型。它的核心数据是城市之间的人口迁入迁出以天为粒度更新。这类数据本身是给公众看的大盘趋势官方也没有做特别强的反爬接口直接返回结构化 JSON。你不需要逆向 JS不需要处理验证码只需要把日期、城市编号、迁移类型这几个参数拼对就能稳定拿到数据。把这种项目作为爬虫入门或者进阶练习性价比非常高。1.2 百度迁徙到底暴露了哪些数据我实际抓下来百度迁徙主要提供三类数据迁徙规模指数反映某个城市当天迁入、迁出总规模的相对指数适合画时间序列曲线。迁入/迁出城市 Top 榜单给定一个城市和日期返回迁入来源地或迁出去向地的排行通常每个方向能拿到几十到上百个城市。城市之间的 OD 明细更细粒度的人群流动记录适合做城市关联分析和弧线图可视化。对大多数个人研究和练手场景第二类数据也就是迁入迁出 Top 榜单是最实用也最好爬的。它能直接回答“春节前一天北京的人主要去了哪里”“上海的外来人口来源地有哪些”这类问题。1.3 数据能用来做什么拿到这些数据之后可以做的事非常多分析城市群联系强度比如长三角、珠三角内部的城市流动规律做节假日出行预测观察春运、国庆期间的迁徙峰值结合天气、旅游热度做相关性分析单纯作为爬虫项目练习 requests、Pandas、定时任务和增量抓取。无论你是想学爬虫还是想做城市研究这个数据源都是接近白嫖的宝藏。2. 浏览器开发者工具里五分钟定位接口2.1 从页面交互到 XHR 过滤打开百度迁徙页面按 F12 进入开发者工具切到 Network 面板然后在上方筛选框里点 XHR这样只看异步请求不看图片、CSS 这些无关资源。接着在页面左侧点一个城市比如点“北京”右侧会显示迁入、迁出、规模和排名。你切到“迁入”或“迁出”标签时Network 面板就会刷出对应的接口请求。点开其中一个在 Response 里能看到 JSON里面就是城市排名列表。这一步非常关键它告诉你真实接口地址长什么样而不是去猜。2.2 读懂接口参数date、city 与 type 的含义把我抓到的请求 URL 复制下来核心参数一般长这样dtcity id340 typemove_in date20240101这几个参数的意思很直观参数含义取值示例dt数据类型city 表示按城市粒度id目标城市编号百度内部城市编码type迁移方向move_in 迁入move_out 迁出date日期20240101注意这里的 id 不是行政区划代码而是百度自己的城市编码。我第一次跑就踩了这个坑拿行政区划代码去试结果返回空数据。正确做法是在页面多切换几个城市观察 Network 里 id 的变化把每个城市对应的 id 记录下来后面做成字典用。2.3 用小脚本验证请求绕过不友好的调试体验定位到接口之后我习惯先用 requests 模拟请求验证能不能直接拿到数据。如果你用的是 Anaconda 环境requests 和 pandas 基本都自带了少什么就装什么conda install requests pandas然后写一个最简脚本import requests url https://huiyan.baidu.com/migration/cityrank.json params { dt: city, id: 340, # 示例编号以抓包为准 type: move_in, date: 20240101, } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, paramsparams, headersheaders, timeout10) print(resp.status_code) print(resp.json())如果返回的 JSON 里包含城市名和 value 字段说明接口可以直接用。如果返回 404 或者空数据先别改代码回到 DevTools 重新复制一遍最新的接口地址。百度迁徙的接口路径改过好几次网上很多老文章里的 URL 已经失效了以你抓包拿到的为准。3. 一个能落地的 Python 爬虫雏形3.1 环境准备只用 requests 和 pandas写正式爬虫之前我强烈建议先想清楚一个问题你是不是真的需要数据库很多人一上来就上 MySQL、MongoDB结果光调库就折腾半天。对百度迁徙这种每天几十个请求、数据量几 MB 的体量CSV 文件完全够用后期真有分析需求再导入数据库也不迟。我的第一版脚本只用 requests 发起请求pandas 整理数据。3.2 爬迁入迁出 Top 榜单的完整代码下面这段代码是我实际跑过的版本的精简版注释都写好了你可以直接改参数用。import os import time import random import requests import pandas as pd UA Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 API_URL https://huiyan.baidu.com/migration/cityrank.json # 以抓包为准 CITY_CODE { 北京: 340, # 示例映射换成你自己抓到的 id # 上海: ???, } def fetch_rank(city_code: str, date_str: str, move_type: str): params { dt: city, id: city_code, type: move_type, # move_in / move_out date: date_str, } resp requests.get(API_URL, paramsparams, headers{User-Agent: UA}, timeout10) resp.raise_for_status() data resp.json() rows [] city_list data.get(data, {}).get(list, []) for item in city_list: rows.append({ date: date_str, base_city: city_code, direction: move_type, target_city: item.get(city_name), value: item.get(value), }) return rows def main(): dates pd.date_range(20240101, 20240107, freqD) csv_path baidu_migration.csv file_exists os.path.exists(csv_path) for day in dates: date_str day.strftime(%Y%m%d) for city_name, code in CITY_CODE.items(): for move_type in [move_in, move_out]: rows fetch_rank(code, date_str, move_type) df pd.DataFrame(rows) df.to_csv(csv_path, modea, indexFalse, headernot file_exists, encodingutf-8-sig) file_exists True # 礼貌延时别一口气打几十个请求 time.sleep(random.uniform(1, 2)) if __name__ __main__: main()这里有个细节容易忽略CSV 文件如果已经存在再用to_csv追加时不应该重复写表头。我用了一个file_exists变量来控制第一次写带表头之后只追加行。文件编码用了utf-8-sig因为 Excel 直接打开 utf-8 的 CSV 会乱码这个编码兼容性最好。3.3 日期循环与 CSV 落盘别急着上数据库上面代码的日期的循环方式用的是pd.date_range好处是可以用freq参数灵活控制粒度不只有按天改成按小时也可以。抓下来之后每行数据包含日期、目标城市、方向和流动数值后续做统计直接pd.read_csv就能分析。我建议第一版不要设计太复杂先跑通一个星期的数据确认每一列都是自己想要的再考虑增量、断点、可视化这些进阶功能。4. 把脚本改造成能长期跑的样子4.1 百度迁徙的真实反爬强度与我的应对先说结论百度迁徙的反爬强度远低于招聘网站但这不意味着可以疯狂请求。我测试过连续快速请求几十次之后接口偶尔会返回异常或者空结果。它没有弹滑块验证码但如果你把频率拉满很容易触发服务端的限流。我的应对方式非常简单带一个正常的浏览器 User-Agent每次请求之间随机睡眠 1 到 3 秒不做并发单线程顺序抓遇到异常先退避重试而不是立刻继续打。这样跑几个月基本没遇到过被限制的情况。说到底爬虫能不能稳定运行很多时候不是看你用了多牛的技巧而是看你愿不愿意慢下来。4.2 断点续爬从已下载数据里恢复日期数据抓取最怕跑到一半脚本崩了。如果不做断点续爬重跑就得全部重来既浪费时间又增加服务器压力。我的方案是维护一个done.txt文件每成功抓完一条记录就把它的唯一标识写进去。下次启动时先读这个文件如果当前任务已经完成就跳过。done_path done.txt done_keys set() if os.path.exists(done_path): with open(done_path, r, encodingutf-8) as f: done_keys set(line.strip() for line in f if line.strip()) # 在 main 循环里生成 key 之后先检查 key f{date_str}_{city_code}_{move_type} if key in done_keys: print(fskip {key}) continue # 抓取并写 CSV成功后追加 key with open(done_path, a, encodingutf-8) as f: f.write(key \n)有了这个机制脚本崩溃了、断网了、电脑休眠了都不怕重新跑一次会自动跳过已经完成的部分从断点继续。4.3 重试与随机延时减少被限制的概率网络请求会遇到各种意外连接超时、服务端 5xx、返回 JSON 为空。这时候简单重试一下就好了但重试要有策略不能无限打。def fetch_with_retry(params, max_retries3): for attempt in range(max_retries): try: resp requests.get(API_URL, paramsparams, headers{User-Agent: UA}, timeout10) if resp.status_code ! 200: print(fstatus{resp.status_code}, retry{attempt1}) else: data resp.json() if data.get(data) is not None: return data except requests.RequestException as exc: print(frequest error: {exc}) time.sleep(2 * (attempt 1) random.uniform(0, 1)) return None重试时间我用的是线性退避第一次失败等 2 秒第二次等 4 秒第三次等 6 秒再加一点随机抖动。这个比固定间隔更合理固定间隔容易让请求形成节奏被识别成机器行为随机退避则更接近人工操作习惯。5. 从数据到可视化迁徙方向一眼看清5.1 数据结构化与城市名归一化爬下来的原始 CSV 里城市名字段看起来可能不一致有的叫“北京”有的叫“北京市”还有“自治州”“特别行政区”等特殊名称。做可视化之前一定要做城市名归一化。我的做法是先把末尾的“市”“省”“自治区”等后缀清理掉再做一次映射表修正city_clean target_city.replace(市, ).replace(省, )正规的项目可以直接用行政区划表和百度城市编码做 JOIN但个人项目维护一张手工映射表就够了。反正要可视化的城市数量通常就几十个不需要过度工程化。城市名对齐之后再关联一份城市经纬度表后续画地图弧线就简单了。经纬度表可以从公开的行政边界数据里提取网上很多现成的 JSON 可以直接用。5.2 pyecharts 弧线图与其他画图思路画迁徙流向图我最早用的是 pyecharts 的 Geo 图表效果还不错。把爬到的数据按“迁出地 - 迁入地”整理成[[出发地, 到达地], 数值]的格式然后传入 Geo 图的 lines 系列。from pyecharts import options as opts from pyecharts.charts import Geo # data_pairs [[北京, 上海, 15.2], [北京, 广州, 8.7], ...] geo Geo().add_schema(maptypechina) geo.add(迁出流向, data_pairs, type_lines) geo.set_series_opts(label_optsopts.LabelOpts(is_showFalse)) geo.render(migration.html)如果嫌 pyecharts 版本兼容麻烦也可以退一步用 matplotlib 画柱状图把 Top 城市按数值排序画横向条形图一样能看出主要流向。可视化不是目的理解数据才是。5.3 时间序列上的迁徙规模指数除了城市之间的流向迁徙规模指数也是非常有价值的时间序列。如果你没找到单独的规模指数接口也可以用我们爬到的 Top 榜单数据做一个近似指标把当天迁入 Top 榜单的数值求和得到一个相对总量。虽然和官方指数口径不同但趋势曲线基本一致做相关性分析够用了。画时间序列用 pandas 加 matplotlib 就够了import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(baidu_migration.csv) daily df.groupby([date, direction])[value].sum().unstack() daily.plot(kindline, titledaily migration trend) plt.show()我自己的习惯是先把每天的迁入、迁出总量曲线画出来先看整体趋势再去看具体城市对。整体趋势能帮你发现数据是否有缺失比如某天某个方向突然掉到 0那大概率是采集没跑全而不是真实波动。6. 三个月运行下来我踩过的坑6.1 接口偶尔抽风与返回值缺失运行时间长了就会发现百度迁徙接口偶尔会返回一个没有数据列表但状态正常的 JSON。一开始我以为是 IP 被限制后来同一个请求隔几分钟再试就成功了。这大概率是服务端临时问题或者历史上当天数据还没产出。应对方式很简单给“返回空列表”单独做一个标记不要和失败重试混为一谈。失败是指 HTTP 错误或请求异常这类需要重试空列表是正常响应只是没有数据这类记录下来之后统一补跑。6.2 日切节点与数据修正百度迁徙的数据不是当天发布后就不变了。我遇到过早上抓到的数据和几天后回头看完全不一样的情况尤其春运、国庆这种高峰期数值会修正。后来我养成了补跑最近七天的习惯每周日把最近七天的数据重新抓一遍用新数据覆盖旧数据。断点续爬的 key 如果只记录日期和城市覆盖重抓会有点麻烦。我的做法是多加一个“是否已补跑”的状态字段或者干脆补跑逻辑单独写一个脚本输出到migration_fixed.csv分析时优先用修正后的版本。6.3 合规与心态别把免费接口薅到挂最后的提醒可能不性感但很重要。百度迁徙的数据虽然是公开接口但它依然属于别人的服务。个人学习研究低频抓取是一回事高频并发、批量倒卖、商用变现是另一回事。我的原则是单天请求量控制在百次以内程序跑的时间尽量避开对方高峰数据只用于自己的分析和博客案例。说实话这种免费且高质量的数据源越来越少。我现在的策略就三个字慢一点。抓不完的数据永远在被抓封了才真的什么都没了。如果你也想长期维护这份数据保持克制比什么技巧都管用。本文还有配套的精品资源点击获取