我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

抖音关键词搜索脚本:批量采集与数据落盘实战

抖音关键词搜索脚本:批量采集与数据落盘实战 简介这份资源是一套面向互联网营销与数据分析人员的抖音关键词搜索自动化脚本适合具备一定Python基础、希望批量采集抖音视频与用户信息的开发者使用。压缩包共5个文件约9.8MB包含Python脚本、批处理文件、APK安装包及依赖清单等分别用于搜索逻辑实现、MITM代理拦截调试、安卓端辅助运行与运行环境配置。脚本核心涉及关键词匹配算法、网络爬虫、HTML与JSON数据解析、API接口调用及中间人代理等技术点可帮助读者理解从请求构造到数据提取的完整链路。已有922人学习下载读者可借此掌握模拟登录、请求头与cookies处理、反爬规避及依赖管理等实操思路并对照代码结构快速搭建自己的采集环境。使用时需注意遵守平台条款与隐私政策控制抓取规模并关注抖音接口变动对脚本运行的影响。1. 抖音关键词搜索脚本从手动刷到批量采集的工程化路径做内容运营或者竞品分析的人大概都经历过这样的场景想看看某个关键词下最近冒出了哪些新账号、哪些视频在起量于是打开抖音搜索框输入关键词一屏一屏往下滑滑到第三十屏手酸了眼睛也花了最后只记下三五个觉得有用的。这种手动方式不仅效率低而且根本没法做趋势对比——你今天看的数据和上周看的口径完全对不上。抖音关键词搜索脚本要解决的就是这个问题把「搜索→翻页→提取字段→落盘」这条链路自动化让采集变成可重复、可对比、可积累的工程动作。它适合做内容选题、竞品监控、达人筛选的运营和开发也适合想练手爬虫但不想一上来就啃硬骨头的工程师。需要提前说清楚的是抖音的页面结构和接口签名一直在变脚本不是一劳永逸的你得有跟着调整的预期。2. 先搞清楚搜什么关键词搜索脚本的字段设计与请求构造2.1 搜索结果页到底能拿到哪些字段很多人一上来就写代码结果跑通了才发现拿到的字段根本不是自己想要的。我一般会先花十分钟把搜索结果页的卡片结构拆一遍。抖音关键词搜索的结果列表里每条视频卡片通常包含这些可提取的信息视频标题或文案、作者昵称、作者主页链接、视频链接、点赞数、评论数、发布时间、视频封面图地址。有些版本还会显示「相关推荐」标签或者话题标签。你要根据业务目标决定采哪些字段——如果做选题分析标题和点赞数是核心如果做达人筛选作者昵称、主页链接和近期视频表现更重要。字段确定之后要理解请求是怎么发的。抖音搜索页在浏览器里看起来是一个长列表但底层是分页接口在支撑。常见做法是打开开发者工具切到 Network 面板筛选 XHR 或 Fetch 请求然后在页面上触发一次「加载更多」观察新出现的请求。你会看到一个带有 search 关键字的接口返回的是 JSON 结构里面包含 data 数组每个元素对应一条视频卡片。关键参数通常包括 keyword搜索词、offset 或 cursor翻页游标、count每页数量。不同时期参数名可能不同但逻辑是一致的用游标或偏移量控制翻页用关键词控制搜索范围。提示不要一上来就追求「全量采集」。先跑通「一个关键词、一页数据、五个字段」的最小闭环再逐步加翻页和字段。2.2 用 Python 构造一次最小可用的搜索请求下面这段代码演示的是最基础的请求构造思路。注意我在这里用的是通用的 requests 库来发请求实际运行时你需要把 URL 和参数替换成你当前抓到的真实值。代码里保留了关键的注释方便你对照自己的抓包结果修改。import requests import json import time # 搜索接口地址实际使用时替换为你在开发者工具里看到的真实 URL SEARCH_URL https://www.douyin.com/aweme/v1/web/search/item/ # 请求头User-Agent 和 Referer 是必须的Cookie 根据实际情况补充 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.douyin.com/, Accept: application/json, text/plain, */*, } def search_once(keyword, offset0, count10): 发起一次搜索请求 keyword: 搜索关键词 offset: 翻页偏移量第一页为 0 count: 每页返回数量通常为 10 或 20 params { keyword: keyword, offset: offset, count: count, search_channel: aweme_general, # 搜索频道通用视频 sort_type: 0, # 0 综合排序1 最新2 最热 publish_time: 0, # 0 不限1 一天内7 一周内等 } try: resp requests.get(SEARCH_URL, headersHEADERS, paramsparams, timeout10) resp.raise_for_status() data resp.json() return data except requests.RequestException as e: print(f请求失败: {e}) return None if __name__ __main__: result search_once(露营装备, offset0, count10) if result: # 打印返回的顶层键方便确认数据结构 print(json.dumps(result, ensure_asciiFalse, indent2)[:2000])这段代码的逻辑很直白拼参数、发请求、拿 JSON。参数里sort_type和publish_time是两个很实用的筛选维度做趋势分析时把publish_time设成 7就能只看最近一周的内容。offset每次增加count的数量就能实现翻页。代码跑通后你会看到返回的 JSON 里有一个data字段里面是列表每个元素就是一条视频信息。下一步就是从这些元素里把标题、作者、点赞数抠出来。2.3 翻页逻辑与频率控制的参数怎么设翻页不是简单地把 offset 一直加下去。抖音的搜索接口对翻页深度是有限制的通常翻到几十页之后就会返回空数据或者要求验证。我一般会把单次任务的翻页上限设在 10 到 20 页之间也就是 100 到 200 条视频这个量级对于大多数选题分析已经够用了。如果你确实需要更多数据更稳妥的做法是换关键词、换时间切片而不是死磕一个关键词往深了翻。频率控制是另一个必须认真对待的参数。我见过有人把time.sleep设成 0.5 秒跑了几百次请求之后直接被封 IP。比较安全的做法是每次请求之间随机间隔 2 到 5 秒每跑完 5 到 10 页就休息 30 秒以上。下面是一个带频率控制的翻页示例import random def search_pages(keyword, max_pages10, count10): 翻页采集带随机间隔 max_pages: 最大翻页数 count: 每页数量 all_items [] for page in range(max_pages): offset page * count print(f正在采集第 {page 1} 页offset{offset}) data search_once(keyword, offsetoffset, countcount) if not data or not data.get(data): print(返回为空停止翻页) break items data[data] all_items.extend(items) # 随机间隔 2 到 5 秒 sleep_time random.uniform(2, 5) time.sleep(sleep_time) # 每 5 页多休息一会儿 if (page 1) % 5 0: print(已采集 5 页休息 30 秒) time.sleep(30) return all_items这里的max_pages和count是两个需要根据实际情况调的参数。count设大了单页返回多但被风控的概率也高设小了请求次数多总耗时更长。我一般用count10配合 2 到 5 秒的间隔在稳定性和效率之间取一个平衡。max_pages则根据你的采集目标来定做日常监控 5 页足够做深度分析可以到 20 页但再往上就建议换策略了。3. 从 JSON 到表格字段提取、清洗与落盘3.1 把嵌套 JSON 拍平成一行行记录搜索接口返回的 JSON 是嵌套结构一条视频的信息可能分散在aweme_info、author、statistics这几个层级里。直接存 JSON 当然可以但后续做分析时很不方便。我一般会写一个提取函数把每条视频拍平成一行记录字段包括视频 ID、标题、作者昵称、作者 ID、点赞数、评论数、分享数、发布时间戳、视频链接。下面是一个提取函数的示例def extract_item(item): 从单条搜索结果中提取关键字段 item: 搜索结果列表中的一个元素 aweme item.get(aweme_info, {}) author aweme.get(author, {}) stats aweme.get(statistics, {}) # 视频 ID 用于拼接链接 aweme_id aweme.get(aweme_id, ) record { aweme_id: aweme_id, title: aweme.get(desc, ).replace(\n, ).strip(), author_name: author.get(nickname, ), author_id: author.get(uid, ), digg_count: stats.get(digg_count, 0), comment_count: stats.get(comment_count, 0), share_count: stats.get(share_count, 0), create_time: aweme.get(create_time, 0), video_url: fhttps://www.douyin.com/video/{aweme_id} if aweme_id else , } return record这个函数的关键在于防御性取值每一层都用.get()并给默认值避免某个字段缺失导致整个脚本崩溃。title里把换行符替换成空格是为了后续写入 CSV 时不出现错行。create_time是 Unix 时间戳后面可以转成可读日期。video_url用视频 ID 拼接方便直接点开核对。3.2 用 pandas 落盘成 CSV 和 Excel提取出来的记录列表用 pandas 转成 DataFrame 再落盘是最省事的。CSV 适合后续用脚本处理Excel 适合直接拿给运营看。下面这段代码同时输出两种格式import pandas as pd from datetime import datetime def save_records(records, keyword): 将记录列表保存为 CSV 和 Excel records: extract_item 返回的记录列表 keyword: 搜索关键词用于文件名 if not records: print(没有记录可保存) return df pd.DataFrame(records) # 时间戳转可读日期 df[create_time_readable] df[create_time].apply( lambda x: datetime.fromtimestamp(x).strftime(%Y-%m-%d %H:%M:%S) if x else ) # 按点赞数降序排列 df df.sort_values(digg_count, ascendingFalse) # 生成带时间戳的文件名 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) csv_file fdouyin_search_{keyword}_{timestamp}.csv excel_file fdouyin_search_{keyword}_{timestamp}.xlsx df.to_csv(csv_file, indexFalse, encodingutf-8-sig) df.to_excel(excel_file, indexFalse) print(f已保存 {len(df)} 条记录到 {csv_file} 和 {excel_file}) return df这里有几个细节值得注意。encodingutf-8-sig是为了让 Excel 打开 CSV 时不乱码这个坑我踩过好几次。按digg_count降序排列是为了让你打开文件第一眼就能看到表现最好的内容。文件名里带关键词和时间戳方便多次采集后做对比。如果你要长期监控同一个关键词可以每天跑一次文件名里的日期就是天然的时间切片。3.3 去重与增量采集的简单策略同一个关键词反复采集一定会遇到重复视频。最简单的去重方式是用aweme_id做唯一键每次保存前先读一下历史文件把已经存在的 ID 过滤掉。下面是一个增量采集的示例import os def load_existing_ids(csv_file): 读取已有 CSV 中的 aweme_id 集合 if not os.path.exists(csv_file): return set() df pd.read_csv(csv_file) return set(df[aweme_id].astype(str).tolist()) def incremental_save(new_records, keyword): 增量保存跳过已存在的视频 # 找到最新的历史文件 existing_files [f for f in os.listdir(.) if f.startswith(fdouyin_search_{keyword}_) and f.endswith(.csv)] existing_ids set() if existing_files: latest sorted(existing_files)[-1] existing_ids load_existing_ids(latest) print(f历史文件 {latest} 中有 {len(existing_ids)} 条记录) # 过滤新记录 fresh [r for r in new_records if str(r[aweme_id]) not in existing_ids] print(f新记录 {len(fresh)} 条重复 {len(new_records) - len(fresh)} 条) if fresh: save_records(fresh, keyword) else: print(没有新记录需要保存)这个策略的核心是每次采集前先加载历史 ID 集合只保存新的。对于日常监控场景你甚至可以把这个脚本挂到定时任务里每天跑一次只关注新增内容。load_existing_ids里用astype(str)是为了避免 ID 被 pandas 读成数字后精度丢失这个坑在 ID 较长时特别容易翻车。4. 避坑与排查关键词搜索脚本最常见的五个翻车点4.1 请求返回空数据或 403现象脚本跑了几页之后突然返回空列表或者直接 403换关键词也不行。原因最常见的是触发了频率限制其次是请求头里的 Cookie 过期或者签名参数缺失。抖音的接口对没有有效签名的请求会直接拒绝而签名算法是动态变化的。解决先降低频率把间隔调到 5 秒以上每 3 页休息 60 秒。如果还是不行检查请求头里的User-Agent和Referer是否完整Cookie 是否过期。对于签名问题常见做法是使用浏览器自动化工具如 Playwright来维持会话让浏览器自己处理签名脚本只负责提取数据。这样虽然慢一些但稳定性高很多。4.2 翻页到一定深度后数据重复现象翻到第 8 页之后返回的视频和前面几页有大量重复甚至完全一样。原因抖音的搜索接口在翻页深度较大时会返回推荐内容而不是严格的下一页结果。这是接口设计上的限制不是脚本 bug。解决把翻页上限控制在 10 页以内用aweme_id去重后实际有效数据可能只有 60 到 80 条。如果需要更多数据换关键词或者换时间切片而不是继续往深了翻。我一般会在脚本里加一个重复率检测如果连续两页的重复率超过 50%就自动停止翻页。4.3 字段提取时 KeyError 频发现象脚本跑着跑着突然报KeyError: aweme_info整个任务中断。原因搜索结果里不全是视频卡片还可能混有广告、话题、用户推荐等不同类型的元素它们的结构不一样。解决在提取函数里做类型判断只处理包含aweme_info的元素。下面是一个简单的过滤逻辑def filter_video_items(items): 只保留视频类型的搜索结果 videos [] for item in items: if aweme_info in item and item[aweme_info].get(aweme_id): videos.append(item) return videos在翻页循环里先调filter_video_items再调extract_item就能避免大部分 KeyError。4.4 中文乱码与 Excel 打开异常现象CSV 文件用 Excel 打开后中文全是乱码或者标题里的换行导致行错位。原因Excel 默认用 GBK 编码打开 CSV而脚本写的是 UTF-8。标题里的换行符没有清理干净。解决写 CSV 时用encodingutf-8-sig这个编码会在文件头加 BOMExcel 能正确识别。标题字段在提取时就做replace(\n, ).strip()处理。如果还是有问题直接输出 Excel 格式.xlsxpandas 的to_excel对中文支持很好。4.5 脚本跑久了内存暴涨现象采集几百页之后脚本占用内存越来越大最后被系统杀掉。原因所有记录都堆在内存里的all_items列表里没有及时落盘。解决改成边采集边写入的模式。每采集一页就提取记录并追加到 CSV 文件里而不是全部攒着最后写。下面是一个流式写入的示例def stream_save(records, csv_file): 追加写入 CSV适合长时间采集 df pd.DataFrame(records) # 第一次写入带表头后续追加不带表头 header not os.path.exists(csv_file) df.to_csv(csv_file, modea, headerheader, indexFalse, encodingutf-8-sig)这样内存里始终只有当前页的数据跑再久也不会爆。5. 进阶技巧用 Playwright 绕过签名难题并做可视化验证如果你已经跑通了前面的 requests 版本但发现签名问题越来越难搞那下一步可以考虑用 Playwright 来做浏览器自动化。它的思路不是去逆向签名算法而是让真实的浏览器去发请求脚本只负责控制翻页和提取数据。这样做的好处是稳定性高缺点是速度慢一些资源占用大一些。我一般会这样组织 Playwright 脚本先启动浏览器打开抖音搜索页输入关键词然后模拟滚动加载。每加载一批就用page.evaluate从 DOM 里提取视频卡片的信息。下面是一个最小示例from playwright.sync_api import sync_playwright import time def search_with_playwright(keyword, max_scroll5): 用 Playwright 打开搜索页并滚动加载 keyword: 搜索关键词 max_scroll: 最大滚动次数 with sync_playwright() as p: browser p.chromium.launch(headlessFalse) # 调试时用有头模式 page browser.new_page() # 打开搜索页 page.goto(fhttps://www.douyin.com/search/{keyword}, timeout30000) page.wait_for_timeout(5000) # 等待首屏加载 for i in range(max_scroll): # 滚动到底部 page.evaluate(window.scrollTo(0, document.body.scrollHeight)) page.wait_for_timeout(3000) print(f已滚动 {i 1} 次) # 提取视频卡片信息 items page.evaluate( () { const cards document.querySelectorAll([data-e2escroll-list] li); return Array.from(cards).map(card { const titleEl card.querySelector([data-e2evideo-desc]); const authorEl card.querySelector([data-e2evideo-author]); const likeEl card.querySelector([data-e2evideo-like]); return { title: titleEl ? titleEl.innerText : , author: authorEl ? authorEl.innerText : , like: likeEl ? likeEl.innerText : }; }); } ) browser.close() return items这段代码的关键在于page.evaluate里的 DOM 选择器。style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
返回列表