
简介这是一份面向Python初、中级学习者的百度指数数据分析完整示例代码包。代码包围绕Pandas、NumPy、Matplotlib与requests库演示如何自动获取、清洗、分析并可视化百度指数数据适合想入门爬虫与数据分析、关注舆情热点的开发者和研究者。压缩包共6个文件包括可运行的Python脚本、CSV数据文件、Docx说明文档和TXT城市代码整体仅24KB轻量便携。脚本不仅提供接口请求框架与数据预处理流程还附有日期转换、均值统计和趋势绘图等关键实现配套文档解释了关键词映射与城市代码的用法。资源已获得697人浏览学习能够帮助读者快速搭建一条从数据采集到图表展示的完整分析链路并便于替换关键词复用到个人项目。1. 百度指数数据分析python完整示例代码把搜索信号变成可复用分析流程百度指数数据分析python完整示例代码听起来像是一份现成脚本实操起来其实是一条从取数到建模的完整链路。百度指数是最容易拿到的搜索行为数据之一看完趋势图几乎零成本但一旦你想把指数数据当作变量做竞品对比、销量预测、选题判断时指数页面的图形和“复制数据”功能根本不够用。这份示例代码要解决的核心问题就是把百度指数变成DataFrame、变成能落库能画图能建模的规范数据流。适合三类读者运营和投手拿来做关键词波段分析策略岗拿来做竞品先行指标以及刚学python数据分析的人拿真实数据练手。跟着下面的步骤走你会拿到一份可复现的工程骨架而不是只能看不能改的玩具脚本。2. 用python爬虫拿百度指数登录、抓包与JSON落库2.1 为什么不能直接请求百度指数的数据藏在XHR里百度指数的趋势曲线并不是页面静态HTML的一部分。打开index.baidu.com搜索一个词按F12切到Network面板并刷新过滤XHR就能看到一条返回JSON的接口请求日期和指数值都在里面页面上的折线图只是这个JSON的渲染结果。这种设计对前端交互友好但对你来说取数这件事就变成了“模拟登录加模拟请求”两个动作。麻烦点在登录态。指数数据在未登录状态下能看一部分但接口对请求的校验很敏感裸requests几乎没有一次能直接成功。网上大量“百度指数爬虫”代码频繁更新就是因为接口的参数或校验逻辑经常变化。我踩过的最浅的坑是拿requests直接访问首页返回200但响应里根本没有结构化数据最后还是要回到浏览器看XHR。这也解释了为什么一个做数据采集的朋友说他干脆用“人工登录加脚本拉数”这种笨办法反而最稳。提示百度指数接口的请求头有较严格的来源校验Referer必须是index.baidu.comCookie必须有效。本文不涉及任何绕过登录的手段扫码登录是官方页面本来就支持的方式。2.2 用Selenium扫码登录并保存Cookie先上工具Selenium配合Chrome用--user-data-dir指定一个独立的浏览器配置目录。这样做的好处是登录状态会持久化下次启动不需要重新扫码。下面是只做一件事的示例代码打开带配置目录的Chrome让用户扫码登录百度指数然后按回车保存Cookie到本地JSON。# login_baidu_index.py import json import time from selenium import webdriver from selenium.webdriver.chrome.options import Options def get_cookie_and_save(): opts Options() opts.add_argument(--user-data-dir./baidu_index_profile) opts.add_experimental_option(excludeSwitches, [enable-automation]) driver webdriver.Chrome(optionsopts) driver.get(https://index.baidu.com) # 手动登录看到页面右上角出现头像即登录成功 input(登录完成后按回车 ...) time.sleep(2) cookies driver.get_cookies() with open(cookies.json, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse, indent2) print(f已保存 {len(cookies)} 个Cookie) driver.quit() if __name__ __main__: get_cookie_and_save()这段代码的逻辑很直接。--user-data-dir给了浏览器一个独立的用户数据目录登录状态、localStorage都落在这个目录里。excludeSwitches去掉“Chrome正在受到自动软件控制”的提示条降低被前端检测的概率。input()的作用是让脚本停下来等真人操作不用写死等待时间实际使用中比time.sleep(30)舒服很多。保存后的cookies.json就是后续所有数据请求的通行证。参数说明--user-data-dir./baidu_index_profile浏览器数据目录目录越大越不容易丢失登录态但不能放在临时目录否则系统一清理就要重新登录。excludeSwitches只去掉自动化提示不等同于反爬规避别指望它让脚本完全隐身。2.3 从XHR响应里解析JSON两种兼容结构登录工作完成后下一步是在浏览器开发者工具里找到那条返回指数数据的XHR请求。复制它的完整请求信息尤其是Cookie、Referer、User-Agent这三样。不同时期的接口路径会有调整我在这份示例代码里不写死URL因为写死了在你的环境里大概率已失效。更稳的做法是把Network里复制的响应体保存为raw.json然后用下面的解析函数把它变成DataFrame。# parse_trend.py import json import pandas as pd def parse_trend_json(raw_json: str) - pd.DataFrame: data json.loads(raw_json) # 不同版本接口返回的层级不一样常见两种data.list 或 data.index raw_data ( data.get(data, {}).get(index, {}) if index in data.get(data, {}) else data.get(data, {}).get(list) ) if not raw_data: print(解析失败请先观察data字段的实际结构) return pd.DataFrame() # raw_data通常是 {word: [...]} 这种映射 rows [] word_key list(raw_data.keys())[0] for item in raw_data[word_key]: rows.append({ date: item.get(date), # 不同接口value的类型不一样统一交给pandas处理 index: item.get(value) or item.get(index) }) df pd.DataFrame(rows) # 时间字段标准化后续所有分析都依赖这一列 df[date] pd.to_datetime(df[date]) df[index] pd.to_numeric(df[index], errorscoerce) return df.sort_values(date).reset_index(dropTrue) if __name__ __main__: with open(raw.json, r, encodingutf-8) as f: result parse_trend_json(f.read()) print(result.head())解析逻辑的关键在前面的分支兼容index和list是接口改版前后出现过的两种容器名。保留这个兼容判断是为了让你把任意一份raw.json丢进来都能先跑通这也是整个分析流程里最重要的第一步先看到DataFrame再谈后续。参数说明pd.to_datetime把日期字符串变成datetime64类型后面的重采样、滞后计算都依赖这个类型不能省。errorscoerce遇到空值或-这类占位符时转成NaN而不是抛异常因为指数接口偶尔会在个别日期返回异常占位符。常见返回字段整理成下面这张表方便你在解析失败时对照检查字段名含义类型备注date日期str格式通常是yyyy-MM-ddvalue搜索指数值int/float主要分析对象word关键词str批量拉取时用来分组type数据维度str可能是整体趋势或移动趋势2.4 批量抓取多关键词频率与游标设计拿到单个词的流程后批量分析需求的差距只在一个循环。不过批量有两个坑请求频率和翻页游标。请求频率方面我会在每次请求之间加3到5秒的停顿这个值没有标准答案主要看你的账号权重和请求总量宁可慢也不要让IP在短时间内被限流。翻页游标方面接口返回的数据经常不是一次给全要用“返回数据里的最后一天”去推进下一次请求的起始日期而不是自己按日历加一天这样能避免重复段和漏段。# fetch_data.py import time import requests import json # 换成你抓包看到的真实地址接口路径变动时只改这里 INDEX_API https://index.baidu.com/api/SearchApi/xxx def load_cookies() - dict: with open(cookies.json, r, encodingutf-8) as f: cookie_list json.load(f) return {c[name]: c[value] for c in cookie_list} def fetch_index_data(keyword: str, start: str, end: str) - str: cookies load_cookies() headers { Referer: https://index.baidu.com/, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), X-Requested-With: XMLHttpRequest, } resp requests.get( INDEX_API, params{word: keyword, startDate: start, endDate: end}, headersheaders, cookiescookies, timeout15, ) return resp.text def batch_fetch_words(word_list, start, end): all_data [] for word in word_list: raw fetch_index_data(word, startstart, endend) df parse_trend_json(raw) if df.empty: print(f{word} 拉取结果为空跳过) continue df[word] word all_data.append(df) time.sleep(4) # 请求间隔经验值4秒 return pd.concat(all_data, ignore_indexTrue)这里有一个细节fetch_index_data返回的是文本真正的解析统一交给parse_trend_json完成。这样设计的好处是“取数”和“解析”两个环节可以独立调试接口变了只改一个函数数据结构变了只改另一个函数。time.sleep(4)是经验值连续拉50个词时这个脚本会跑几分钟这是正常的别觉得它卡住了。3. 数据清洗与特征加工把指数变成可建模的时间序列数据拿到手后别急着画图。百度指数的日度数据有两个特征存在周末效应且个别日期会缺失。如果不过滤这些干扰趋势预测和异常检测都会被带偏。常见做法是先构造工作日和节假日的哑变量而不是简单粗暴地移动平均。3.1 先把日期对齐reindex补洞而不是fillna现象是DataFrame排序后日期出现断层。原因通常是接口返回的数据源本身有缺口或者解析时日期格式不一致。第一步要做的是把日期对齐到完整日历。最忌讳的是直接fillna(0)这在搜索指数里是有意义的0代表没人搜。而null代表数据缺失两者混在一起会让周度聚合产生假低谷。def align_daily_index(df, start, end): # 生成完整日期序列与数据做对齐 full_index pd.date_range(startstart, endend, freqD) df df.set_index(date).reindex(full_index) df.index.name date return df.reset_index()align_daily_index适合单个词的场景。如果有多个词必须先在word维度分组再对每个组单独reindex。原因是多词合并后的DataFrame长度是“词数乘天数”直接reindex会把所有词强制对齐到同一个日期序列上导致有的词出现本来不存在的日期。3.2 工作日、节日与业务信号的构造搜索行为有明显的周内节律周一到周四的“数据分析”指数通常高于周五到周日。这种周期性不完全算噪声有一部分是真实需求节律。建模时与其让模型自己摸索不如直接把它变成字段喂进去。def add_calendar_features(df, holidaysNone): df df.copy() df[weekday] df[date].dt.weekday df[is_weekend] df[weekday].isin([5, 6]).astype(int) df[month] df[date].dt.month df[day_of_year] df[date].dt.dayofyear if holidays: # holidays建议是日期列表比如[2024-02-10]这类春节假期 df[is_holiday] df[date].isin(holidays).astype(int) else: df[is_holiday] 0 return df这里is_holiday的取值建议用公开的节假日安排而不是只把周六周日当休息日。春节前一周“火车票”的搜索会暴涨周六字段完全看不出来只有节假日字段能解释。节假日表我一般手工维护成一年一份的配置文件每年年初更新一次比加载第三方节假日库可控得多。3.3 相对指数归一化跨词可比性的关键做竞品对比时不同关键词的指数量级差异很大大词日指数可能几万长尾词可能只有几百直接画在同一个坐标系里小词会变成一条紧贴横轴的直线。常见做法是各自除以观测期均值变成“相对指数”以100为基准看涨跌幅度。这里有一个坑窗口不同归一化的结果不可比。比较两个词的年度趋势就要都以过去12个月做基准比较淡旺季差异可以用短窗口但报告里必须注明基准口径。def normalize_to_base(df): # base是每个词自己的全期均值 df[base] df.groupby(word)[index].transform(mean) df[index_normal] df[index] / df[base] * 100 return df.drop(columns[base])transform(mean)会在不改变行数的前提下把每个词的全期均值广播回每一行比先聚合再merge省事。归一化之后的指数已经变成“相对强度”再做跨词相关性分析结果才不会被量级差异误导。3.4 重采样到周度/月度边界与聚合方式一起决定日度数据噪声大做趋势判断通常要聚合到周度或月度。聚合方式有讲究搜索指数是流量指标按周聚合成sum更符合业务含义按mean会把小周的波动磨平。月度同理用sum。另外要格外注意重采样的周期边界W默认按周一到周日如果你的业务周期是周五到周四就要用W-FRI。def resample_weekly(df, value_colindex): df df.set_index(date) weekly df[value_col].resample(W).sum() return weekly.reset_index()这段代码是给单个词的。多词数据要先groupby(word)再重采样否则不同词的日期边界会因为重采样窗口而错位。聚合方式对结果的影响可以看下面这张表聚合方式业务含义适用场景注意点sum周期内总搜索量销量预测、投放评估受长假影响大mean周期内日均水平趋势对比、季节性分析会削弱峰值max周期内峰值热点监控对异常敏感4. 趋势分析与可视化把搜索信号从噪声里拆出来4.1 用seasonal_decompose拆掉周节律搜索指数受一周七天的影响非常大直接看原始序列容易把周期当趋势。用statsmodels的seasonal_decompose做加性分解把序列拆成趋势、季节、残差三部分。周期参数period设为7而不是自然月因为搜索行为的节律是周级的。如果数据不够两个完整周期分解结果不稳定建议至少取90天。from statsmodels.tsa.seasonal import seasonal_decompose def decompose_trend(df, period7): ts df.set_index(date)[index].astype(float) result seasonal_decompose(ts, modeladditive, periodperiod) df df.copy() df[trend] result.trend df[seasonal] result.seasonal df[resid] result.resid return df分解之后的trend是真正的缓变趋势resid里藏着需要留意的异常。注意trend和resid的前后两端会有NaN因为分解需要前后各留半个窗口。这不是数据丢失是算法的边界行为画图时用dropna()处理即可做建模时不要删除原始序列。modeladditive是加性模型适合搜索指数这种波动幅度不随趋势剧烈变化的序列。如果你的词条指数在旺季能冲到淡季的十倍以上比如“防晒霜”这种季节性极强的词可以改成multiplicative乘性模型观察一下结果对比。4.2 用残差Z分数做异常检测window与阈值的配合指数突然翻倍是需求真的爆了还是数据接口异常直接看原值很难判断因为周末的固有低谷本身就是一种“假异常”。用分解后的残差来做判断更可靠先算残差的滚动标准差再用Z分数标记偏离程度。def flag_anomalies(df, z_thresh2.5, window30): df df.copy() resid_std df[resid].rolling(window).std() resid_mean df[resid].rolling(window).mean() df[resid_z] (df[resid] - resid_mean) / resid_std df[anomaly] df[resid_z].abs() z_thresh return df关键参数有两个。window30是滚动窗口窗口太短会把一次正常促销引起的短期波动当成异常太长又会让真正的突发信号被平均掉。z_thresh取决于你对误报的容忍度运营场景下我会先取3宁可漏报也不让团队每天看一堆假警报自动化监控场景里降到2.5多抓一些候选再人工审核。参数组合的经验值参考下表场景windowz_thresh说明运营周报303.0低误报只看强信号日常监控142.5高召回人工复核大促复盘72.0短窗口捕捉瞬时波动4.3 跨词滞后相关找领先指标而不是事后相关选品和内容选题时常用一个操作把两个词的指数错开N天算相关系数找出谁在领着谁走。例如“咖啡机”和“咖啡豆”的搜索理论上咖啡机的搜索会领先咖啡豆几天。用滞后相关可以验证这种方向性。def cross_lag_corr(df_a, df_b, max_lag30): res {} for lag in range(-max_lag, max_lag 1): # 正lag表示A领先B s pd.concat([df_a[index], df_b[index].shift(lag)], axis1).dropna() if len(s) 30: res[lag] s.corr().iloc[0, 1] return pd.Series(res).sort_index()注意shift(lag)之后会丢掉前后lag行越大的lag保留下来的数据越少相关系数的置信度越低。所以max_lag不要超过数据长度的四分之一否则最后几个lag点是拿很少的样本在算数字漂亮但不能信。滞后相关的输出是一根从负到正的曲线峰值在哪个lag那个词就是领先指标。这里的“领先”是统计意义上的领先写报告时要用“领先搜索信号”这类表达不能直接写成“A导致B”。4.4 可视化一张图画全趋势、季节与异常用matplotlib把趋势、季节成分和异常点叠在一张图上是给业务方讲清楚指数变化最快的方式。下面这段代码直接对分解后的DataFrame画图异常点用散点标红业务方一眼就能看到什么时间发生了非正常波动。import matplotlib.pyplot as plt def plot_trend(df, keyword): fig, ax plt.subplots(3, 1, figsize(12, 8), sharexTrue) ax[0].plot(df[date], df[index], colorgray, alpha0.6, labelraw) ax[0].plot(df[date], df[trend], colorblack, labeltrend) ax[0].legend() ax[1].plot(df[date], df[seasonal], colorblue, labelseasonal) ax[1].legend() anomalies df[df[anomaly]] ax[2].plot(df[date], df[resid], colororange, labelresid) ax[2].scatter(anomalies[date], anomalies[resid], colorred, s30, labelanomaly) ax[2].legend() ax[2].set_title(keyword) plt.tight_layout() return fig中文字体在matplotlib里经常显示成方块我一般会在脚本开头加一行plt.rcParams[font.sans-serif] [SimHei]。如果报告是英文环境不做这步也没问题。画图本身不是分析它的作用是帮你快速发现规律所以图上的标注越少越好三个子图分别对应原始值、季节成分和残差足够覆盖绝大多数观测需求。5. 百度指数数据分析排查五个常见坑从现象到解决5.1 日期对不上接口翻页游标写错现象明明请求的是整月数据解析出来的DataFrame只有二十几天末尾缺了一截。原因接口对较长的历史区间会分页返回有些实现把分页逻辑藏在了响应里直接拿第一页数据就停了。另一种情况是当天数据还没完全更新最后一天的记录本身就是缺失的。解决先看返回JSON里的边界日期字段如果实际边界和请求参数对不上就用“本次返回的最后一天”作为下一次请求的起始日期循环翻页直到取满。判断取满的条件是“本次返回的最后一天等于请求的end”而不是“拿到多少算多少”。游标推进建议直接用返回值的最大日期避免自行对日期做加减而产生重复段。5.2 Cookie过关但请求仍失败Referer或其他请求头缺了现象Cookie复制进requests请求返回200但响应内容是个空壳或登录跳转页。原因百度指数接口校验的不只是Cookie还有Referer和X-Requested-With甚至Sec-Fetch-Site这一类浏览器自动携带的头部。requests默认不带上这些服务端校验失败后给了一个伪装成正常访问的降级响应。解决不要手写headers费时且容易漏。直接把开发者工具Network里的请求复制为cURL再用curl_cffi的from_curl转成requests会话。这一步能把你从逐项对照请求头的体力活里解放出来也是我个人最常用的做法。5.3 重采样后数据缩水重复索引与groupby顺序现象日度数据看起来正常resample(W)之后只剩几十条感觉丢了大半数据。原因resample要求索引是标准且唯一的datetime类型。如果DataFrame里有重复日期或者索引不是datetime64重采样会静默产生NaN不再报错提示。解决先pd.to_datetime统一类型再set_index(date).sort_index()。重复日期要先groupby(date).agg(sum)否则同一天的两条记录会被当成两条数据周度求和后数值虚高。多关键词场景还要先groupby(word)再重采样避免不同词的边界错位。5.4 指数值隔天翻倍量纲问题现象两个词的指数数值差距悬殊一个上千一个几百直接合并计算相关性结果被数值大的词主导。原因百度指数在不同关键词之间的指数标定并不统一有的词本身搜索基数大指数绝对值就大。直接把原始值当普通数值做相关分析本质是在比较量纲而不是比较变化趋势。解决统一走相对指数也就是第3章的index_normal。每个词都除以自己的基期均值之后比较的是“相对自己涨了多少”量纲影响被消除。需要特别注意两个词的基期窗口必须相同否则相对指数也不可比。5.5 节假日打乱周期分解换STL鲁棒模式现象seasonal_decompose(period7)之后春节前后几周的残差大幅跳动甚至把整体trend都带偏。原因长假会打破周节律搜索流量在春节、国庆的分布和平时完全不同。用固定7天周期做分解长假会表现为跨多日的大残差把滚动标准差撑大后续异常检测的灵敏度整体降低。解决有两种思路。简单做法是把长假前后各一周的日期加入is_holiday字段建模时作为哑变量。进阶做法是把分解算法换成STL并开启robustTrue让长假产生的强脉冲不污染全局参数。from statsmodels.tsa.seasonal import STL def robust_decompose(df): ts df.set_index(date)[index].astype(float) stl STL(ts, period7, robustTrue).fit() df[trend] stl.trend df[seasonal] stl.seasonal df[resid] stl.resid return dfSTL比seasonal_decompose慢一些但遇到长假这类强脉冲时robustTrue会自动降低异常点的权重趋势线更稳。日度数据量上千时STL也能在几秒内跑完性能完全不需要担心。6. 把完整示例代码改成每天能跑的工具配置化与报告化第2章到第5章解决的是从零到一的问题。这一章把示例代码从一次性脚本升级成常驻任务核心做两件事让关键词列表和日期范围变成配置文件让输出收敛成一张报告页。我常用的做法是只输出两个东西一个trend_daily.csv给后续建模用一个report.md给人看。配置和代码分离运营改关键词时不需要碰Python文件。keywords: - 数据分析 - Python - 机器学习 start: 2023-01-01 end: 2025-12-31 z_thresh: 3.0 output_dir: ./output对应的主流程只有六行每行对应前面一个章节可读性极强。真正封装时我建议把fetch_index_data这类会临时出错的函数统一包一层try/except失败时写一条日志到errors.log然后跳过这个词继续跑。一次批量55个词中间挂一个就全停是非常糟糕的体验。import yaml with open(config.yaml, encodingutf-8) as f: config yaml.safe_load(f) df batch_fetch_words(config[keywords], config[start], config[end]) df align_daily_index(df, config[start], config[end]) df add_calendar_features(df) df normalize_to_base(df) df decompose_trend(df, period7) df flag_anomalies(df, z_threshconfig[z_thresh]) df.to_csv(f{config[output_dir]}/trend_daily.csv, indexFalse)报告部分把每个词的最新异常点、周趋势同比、相关性Top1拼成一个markdown表格。思路是用pandas.DataFrame.to_markdown()把DataFrame转成md表格再和标题拼在一起写入report.md。如果环境里没有tabulate库先退一步用to_csv()输出别让报告生成阻塞主流程。我最后提醒自己一句拿到缺失数据不要直接fillna(0)搜索指数里的0是有业务含义的缺失就是缺失强行填0会让后续所有相关分析和预测全部失真。判断数据质量要先看NaN占比超过5%就回源补数这是整条链路里最值得养成的习惯。希望帮到你。本文还有配套的精品资源点击获取