ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python爬虫实战:逆向Ajax接口抓取动态加载的ASMR音频数据

2026/8/26 23:59:24 拓冰建站 浏览量
Python爬虫实战:逆向Ajax接口抓取动态加载的ASMR音频数据 1. 项目概述当爬虫遇上动态加载的音频世界最近在做一个个人项目需要收集一些ASMR音频素材自然就想到了用Python爬虫。但一上手就发现事情没那么简单。很多ASMR网站为了提升用户体验和防止简单的数据抓取都采用了Ajax动态加载技术。这意味着你打开网页源代码看到的只是一个空壳子真正的音频链接和数据都是在你滚动页面或者点击按钮后由JavaScript脚本动态请求并渲染出来的。用传统的requests库直接抓取HTML只能抓到一堆div标签关键的音频地址和标题信息根本不在里面。这就像你去一家餐厅菜单上只有菜名价格和图片需要你问服务员才会告诉你一样。所以这个项目的核心就是如何“问对服务员”拿到我们真正想要的音频数据。这个项目非常适合已经掌握了Python基础语法和requests库基本用法的朋友想要挑战更真实的网页数据抓取场景。整个过程会涉及到分析网络请求、模拟浏览器行为、解析JSON数据等核心爬虫进阶技能。最终我们将实现一个能够稳定抓取目标ASMR网站在线音频标题、播放链接等信息的脚本并且会重点讲解如何应对那些“狡猾”的动态加载机制。下面我就把整个探索和实现的过程以及踩过的坑和总结的技巧毫无保留地分享出来。2. 核心思路与技术选型从静态抓取到动态模拟面对一个动态加载的网站我们的爬虫策略必须升级。不能再简单地“请求-解析”了而是要模拟一个真实用户或者说一个真实浏览器的行为。2.1 静态分析与动态分析的区别首先得搞清楚我们面对的是什么。静态网页的内容在服务器端就已经生成好一次性发送给浏览器。你用requests.get(url)拿到的HTML和你在浏览器里右键“查看网页源代码”看到的内容是完全一致的。所有数据都躺在HTML标签里用BeautifulSoup或lxml解析即可。而动态网页特别是单页应用SPA或大量使用Ajax的页面则不同。服务器第一次返回的HTML只是一个框架和加载JavaScript的指令。浏览器执行这些JS代码然后向服务器发起额外的HTTP请求通常是XHR/Fetch请求获取JSON格式的数据最后再由JS把这些数据填充到页面的对应位置。你在浏览器里右键“查看网页源代码”看到的还是那个空框架但按F12打开开发者工具在“元素”标签页里看到的却是已经渲染好的完整DOM树。我们的目标就是找到并模拟那些获取数据的额外HTTP请求。2.2 技术方案对比与选型基于以上分析我们有几种主流方案直接分析Ajax接口推荐本项目采用这是最高效、对服务器压力最小的方式。核心是使用浏览器的开发者工具F12切换到“网络”(Network)标签页勾选“保留日志”(Preserve log)和“禁用缓存”(Disable cache)然后刷新页面或触发加载动作如滚动。在请求列表中筛选XHR或Fetch请求逐个查看其“响应”(Response)内容找到包含目标数据音频列表、详情等的那个请求。然后我们在Python脚本中直接模拟这个请求。这需要分析请求的URL、方法GET/POST、请求头Headers特别是User-Agent,Referer, 可能需要的Cookie或Authorization以及请求参数Query String或Form Data。使用Selenium等浏览器自动化工具这种方式是模拟一个真实的浏览器打开网页等待JS执行完毕页面完全渲染后再直接从浏览器的DOM树中提取数据。它的优点是简单粗暴几乎能应对所有动态加载因为浏览器帮你完成了所有JS渲染工作。但缺点也很明显极其笨重和缓慢。你需要安装浏览器驱动如ChromeDriver启动一个完整的浏览器进程消耗大量内存和CPU。对于大规模、高频次的爬取任务来说这几乎是不可行的。它更适合用于需要与页面进行复杂交互如登录、点击、输入的场景或者作为分析Ajax接口的辅助工具。使用Pyppeteer或Playwright可以看作是Selenium的现代、更高效的替代品它们直接通过DevTools协议控制无头浏览器。性能比Selenium好功能也更强大但本质上仍然是启动了一个浏览器环境资源消耗依然远大于直接请求接口。我的选择逻辑对于ASMR音频爬取这类以获取数据为主要目的的任务优先选择方案一。它轻量、快速、精准。只有当目标网站的接口加密极其复杂如参数被重度混淆、需要执行特定JS计算或者数据获取流程必须依赖一连串的页面交互时才会考虑方案二或三作为备选或辅助。本项目将聚焦于方案一的完整实践。2.3 工具栈准备确定了方案我们来准备工具请求库requests用于发送HTTP请求模拟浏览器获取数据。这是我们的主力。解析库jsonPython内置模块用于解析接口返回的JSON数据。解析库BeautifulSoup4(可选)虽然主要数据来自JSON接口但初始页面的某些信息如分类ID、初始令牌可能还是藏在HTML里或者用于辅助分析。安装命令pip install beautifulsoup4。浏览器开发者工具任何现代浏览器Chrome/Firefox/Edge的F12功能是我们分析请求的“眼睛”。3. 实战演练逆向分析Ajax请求理论说再多不如动手。我们以一个假设的ASMR网站example-asmr.com为例请注意此为教学示例实际爬取请务必遵守目标网站的robots.txt协议及相关法律法规尊重版权。3.1 目标网站分析与请求捕捉打开目标页面在Chrome浏览器中打开example-asmr.com/audio。开启开发者工具按F12切换到“网络”(Network)标签页。确保勾选了“保留日志”(Preserve log)。触发数据加载向下滚动页面观察“网络”标签页中是否有新的请求出现。通常会看到一些名为getAudioList,loadMore, 或包含api,json,data等关键词的请求。定位关键请求点击新出现的请求查看其“标头”(Headers)和“响应”(Response)。标头关注Request URL请求地址、Request Method请求方法GET或POST、Request Headers请求头特别是User-Agent,Content-Type,Cookie等。响应如果响应内容是JSON格式并且包含了音频的title,url,id等信息那么这个请求就是我们的目标。假设我们找到了一个关键的GET请求Request URL: https://api.example-asmr.com/v1/audio/list?page2size20category_id1响应体Response类似这样{ code: 0, msg: success, data: { list: [ { id: 101, title: 雨夜咖啡馆的轻声细语, audio_url: https://media.example-asmr.com/audio/101.mp3, duration: 1800, author: 主播A }, // ... 更多音频数据 ], has_more: true, total: 150 } }太好了音频数据就在这里。3.2 请求参数与请求头分析现在我们需要弄清楚这个请求是如何被构造出来的。查询参数(Query Parameters)URL中的?page2size20category_id1是分页和分类参数。我们需要知道page页码和size每页数量的规律以及category_id分类ID从哪里来。分类ID可能来自页面HTML中的某个select选项值或者通过其他API获取。请求头(Request Headers)有些网站会对请求头进行校验。最常见的、必须模拟的是User-Agent用来伪装成浏览器。有时Referer来源页和Cookie会话标识也很重要。Cookie通常在你访问网站首页或登录后由服务器下发包含了你的会话状态。在开发者工具的“标头”部分找到“请求头”子项将其中的关键信息记录下来。一个基本的、需要模拟的请求头可能如下User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Referer: https://example-asmr.com/audio Accept: application/json, text/plain, */*注意Cookie字段非常敏感。如果目标网站需要登录才能访问音频列表那么Cookie就是关键。获取Cookie的一个简单方法是在浏览器中登录后从开发者工具的“应用”(Application) - “存储”(Storage) - “Cookie”中复制但这种方式获取的Cookie有有效期且涉及账户安全仅限个人学习研究使用切勿用于非法用途或大规模爬取。3.3 构建Python爬虫脚本分析完毕开始写代码。我们创建一个asmr_spider.py文件。import requests import json import time from typing import List, Dict, Optional class ASMRSpider: def __init__(self): # 基础URL和API端点从分析中得来 self.base_url https://api.example-asmr.com/v1 self.list_api /audio/list # 模拟请求头 self.headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://example-asmr.com/audio, Accept: application/json, text/plain, */*, # Cookie: 你的Cookie字符串如果需要, # 谨慎使用 } # 初始化一个会话可以保持Cookie如果需要 self.session requests.Session() self.session.headers.update(self.headers) def get_category_id(self) - Optional[int]: 获取分类ID。 方法1从首页HTML中解析如果需要。 方法2如果分类固定或已知直接返回。 这里示例返回一个固定值。 # 假设我们爬取“自然音”分类其ID为1 # 更复杂的场景可能需要先请求一个分类列表接口 return 1 def fetch_audio_list(self, page: int 1, size: int 20) - List[Dict]: 获取单页音频列表数据。 category_id self.get_category_id() if not category_id: print(无法获取分类ID) return [] params { page: page, size: size, category_id: category_id, # 可能还有其他固定或动态参数如时间戳、签名等 # _t: int(time.time() * 1000), } try: # 发送GET请求 response self.session.get( urlself.base_url self.list_api, paramsparams, timeout10 # 设置超时时间 ) response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 # 解析JSON响应 result response.json() if result.get(code) 0: # 根据接口实际的成功码判断 audio_list result.get(data, {}).get(list, []) print(f第{page}页成功获取到{len(audio_list)}条音频数据。) return audio_list else: print(f第{page}页请求失败返回信息{result.get(msg)}) return [] except requests.exceptions.RequestException as e: print(f第{page}页网络请求异常{e}) return [] except json.JSONDecodeError as e: print(f第{page}页响应JSON解析失败{e}) print(f原始响应文本{response.text[:200]}) # 打印前200字符辅助调试 return [] def crawl_all_pages(self, max_pages: int 10) - List[Dict]: 爬取多页数据直到没有更多数据或达到最大页数限制。 all_audios [] page 1 size 20 # 每页数量与接口参数一致 while page max_pages: print(f正在爬取第 {page} 页...) current_list self.fetch_audio_list(page, size) if not current_list: # 如果当前页没有数据可能已到末页 print(f第{page}页无数据爬取结束。) break all_audios.extend(current_list) # 简单延时避免请求过快被封IP time.sleep(1) # 判断是否还有下一页这里假设接口返回了has_more字段 # 实际应根据接口响应判断例如if not result.get(data, {}).get(has_more): break # 本例中我们使用简单的页数控制 page 1 print(f爬取结束共获取到 {len(all_audios)} 条音频信息。) return all_audios def save_to_json(self, data: List[Dict], filename: str asmr_audios.json): 将数据保存为JSON文件 with open(filename, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(f数据已保存至 {filename}) if __name__ __main__: spider ASMRSpider() audio_data spider.crawl_all_pages(max_pages5) # 示例爬取5页 if audio_data: spider.save_to_json(audio_data)这个脚本定义了一个ASMRSpider类结构清晰__init__初始化基础配置和会话。get_category_id演示如何获取分类参数这里简化了。fetch_audio_list核心方法负责构造参数、发送请求、解析响应。crawl_all_pages控制分页爬取的循环逻辑。save_to_json将结果保存到本地文件。4. 进阶挑战与解决方案上面的例子是一个比较理想的、接口清晰的场景。现实中你可能会遇到更复杂的情况。4.1 应对参数加密与签名有些网站为了反爬会对请求参数进行加密或添加动态签名sign。你可能会在请求参数里看到一长串毫无规律的字符串。例如...timestamp1678886400nonceabc123signmd5(secrettimestampnonce其他参数排序拼接)解决方案搜索与定位在开发者工具的“源代码”(Sources)标签页中全局搜索CtrlShiftF关键参数名如sign、encrypt、token等。调试与追踪在可能生成这些参数的JS文件行号上打上断点重新触发请求观察调用栈和变量的值。这需要一定的JavaScript调试能力。使用第三方库模拟执行如果加密逻辑不复杂可以尝试用Python的execjs或PyExecJS库来执行关键的JS加密函数。但这通常只适用于逻辑简单的加密。终极方案如果加密非常复杂逆向工程成本过高可以考虑使用Selenium或Pyppeteer让浏览器环境去执行JS生成正确的参数然后我们再从发起的网络请求中“窃取”已经构造好的完整请求URL和参数。这是一种“打不过就加入”的思路。4.2 处理登录与会话Cookie很多ASMR网站需要登录才能访问音频资源。这意味着你的爬虫需要先完成登录获取有效的Cookie或Token并在后续请求中携带。解决方案分析登录接口同样使用开发者工具在登录页面输入账号密码点击登录观察网络请求。找到一个通常是POST方法的登录请求分析其提交的数据格式通常是JSON或Form Data。模拟登录在你的Python脚本中先向这个登录接口发送请求提交账号密码。成功后服务器会在响应头Set-Cookie中返回会话信息requests.Session()会自动管理这些Cookie。使用会话后续的所有请求都使用同一个session对象发起它会自动携带已获得的Cookie。def login(self, username, password): login_url https://api.example-asmr.com/v1/user/login login_data { username: username, password: password } resp self.session.post(login_url, jsonlogin_data) if resp.status_code 200: print(登录成功) # session会自动保存cookie return True else: print(登录失败) return False重要警告自动化登录涉及账户安全务必仅在合法合规、获得授权的前提下进行。不要尝试破解或绕过验证码这不仅是技术问题更是法律和道德问题。4.3 反爬虫策略应对网站可能会设置一系列反爬措施请求头校验检查User-Agent、Referer甚至Accept-Language。我们的脚本已经模拟了基础的头信息。IP频率限制短时间内来自同一IP的请求过多会被封禁。请求参数校验如上文所述的签名。JavaScript挑战返回一段需要浏览器执行的JS代码来计算一个令牌才能继续访问。综合应对策略遵守robots.txt首先检查目标网站的robots.txt文件通常在网站根目录如example-asmr.com/robots.txt尊重网站的爬虫协议。设置合理的请求间隔在循环请求中使用time.sleep(random.uniform(1, 3))加入随机延迟模拟人类操作。使用代理IP池当单IP被限制时这是最有效的解决方案。可以从一些代理服务商购买或者自建代理池。在requests中可以通过proxies参数使用代理。proxies { http: http://your-proxy-ip:port, https: http://your-proxy-ip:port, } response session.get(url, proxiesproxies)轮换User-Agent准备一个User-Agent列表每次请求随机选择一个。保持会话使用requests.Session()它能够自动处理Cookie且在某些情况下保持长连接比每次新建连接更“友好”。5. 数据清洗与存储优化爬取到的原始数据可能需要清洗和结构化存储。5.1 数据清洗接口返回的JSON数据可能包含我们不需要的字段或者字段名不够直观。我们可以在保存前进行清洗def clean_audio_data(self, raw_list: List[Dict]) - List[Dict]: cleaned_list [] for item in raw_list: cleaned { id: item.get(id), title: item.get(title, ).strip(), # 去除首尾空格 audio_url: item.get(audio_url), duration_seconds: item.get(duration, 0), duration_formatted: self._format_duration(item.get(duration, 0)), # 格式化成 MM:SS author: item.get(author, 未知), category_id: item.get(category_id), upload_time: item.get(create_time), # 可能需转换时间戳 } # 过滤掉没有音频链接的数据 if cleaned[audio_url]: cleaned_list.append(cleaned) return cleaned_list def _format_duration(self, seconds: int) - str: mins seconds // 60 secs seconds % 60 return f{mins:02d}:{secs:02d}5.2 存储方案选择除了保存为JSON文件根据数据量和使用场景还可以选择CSV文件适合用Excel打开查看使用csv模块。SQLite数据库适合数据量较大、需要查询和去重的场景。轻量级无需安装数据库服务器。MySQL/PostgreSQL适合团队协作或需要复杂查询的生产环境。MongoDB如果数据是高度嵌套的JSON文档MongoDB这种NoSQL数据库可能更合适。这里给出一个SQLite的存储示例import sqlite3 def save_to_sqlite(self, data: List[Dict], db_pathasmr.db): conn sqlite3.connect(db_path) cursor conn.cursor() # 创建表 cursor.execute( CREATE TABLE IF NOT EXISTS audios ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, audio_url TEXT UNIQUE NOT NULL, duration INTEGER, author TEXT, category_id INTEGER, upload_time TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) # 插入数据 for audio in data: try: cursor.execute( INSERT OR IGNORE INTO audios (id, title, audio_url, duration, author, category_id, upload_time) VALUES (?, ?, ?, ?, ?, ?, ?) , (audio[id], audio[title], audio[audio_url], audio[duration_seconds], audio[author], audio[category_id], audio[upload_time])) except Exception as e: print(f插入数据失败 {audio[id]}: {e}) conn.commit() conn.close() print(f数据已保存至SQLite数据库: {db_path})6. 常见问题与排查技巧实录在实际操作中你肯定会遇到各种报错和意外情况。下面是我总结的一些常见问题及排查思路。6.1 请求返回状态码异常状态码可能原因排查与解决思路403 Forbidden1. 请求头未正确模拟缺User-Agent,Referer。2. IP被网站封禁。3. 请求带了错误的或过期的Cookie。1. 检查并补全关键请求头特别是从浏览器复制完整的User-Agent。2. 尝试更换IP或使用代理并大幅降低请求频率。3. 清除或更新Cookie重新登录获取。404 Not Found1. API接口URL拼写错误或已变更。2. 请求参数缺失或格式错误导致服务器无法找到资源。1. 仔细核对开发者工具中捕获的完整请求URL。2. 检查所有必需参数是否都已正确添加特别是分页、分类ID等。500 Internal Server Error服务器内部错误。可能是我们发送了非预期的参数触发了服务器bug。对比浏览器正常请求和你代码发送的请求确保所有参数包括隐藏参数完全一致。200 OK但返回错误信息请求成功到达服务器但业务逻辑失败如code: 1001, msg: ‘参数无效’。重点检查响应体中的JSON看具体的错误码和消息。通常是参数值不对、签名错误、或缺少权限令牌(token)。6.2 数据解析失败json.JSONDecodeError说明服务器返回的不是合法的JSON。可能的原因请求被重定向到了登录页或错误页返回的是HTML。打印response.text的前500字符查看确认是否触发了反爬如返回了验证码页面。接口需要特定的Accept头。确保请求头中包含Accept: application/json。编码问题。尝试response.content.decode(utf-8)或response.encoding utf-8后再解析。数据字段为空或结构不符接口可能更新了。定期用浏览器开发者工具重新抓包确认数据结构和字段名是否有变化。6.3 爬取速度慢或不稳定同步请求的阻塞requests是同步库time.sleep和网络IO会阻塞整个程序。解决方案考虑使用异步库如aiohttp可以并发发起大量请求极大提升效率。但异步编程复杂度较高且需注意目标网站的并发承受能力避免造成攻击。网络波动添加重试机制。可以使用tenacity库或自己实现一个简单的重试循环。import requests from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10)) def fetch_with_retry(url, params): response requests.get(url, paramsparams, timeout10) response.raise_for_status() return response6.4 法律与道德风险规避这是最重要的一点技术再强也不能越界。查看robots.txt这是网站与爬虫的协议。如果它明确禁止爬取你目标目录如Disallow: /api/请停止。控制爬取速率设置较长的请求间隔如2-5秒避免对目标网站服务器造成压力。明确数据用途爬取的数据仅用于个人学习、研究或欣赏切勿用于商业用途、公开传播或任何侵犯版权的行为。ASMR音频是创作者的作品受版权保护。尊重用户隐私如果爬取到任何用户个人信息应立即删除不得保存或使用。考虑使用官方API如果网站提供公开API优先使用API这是最合规的方式。最后爬虫技术是一把双刃剑。在动手之前多花点时间分析网站结构、理解接口规律、设计稳健的代码结构往往比盲目写代码然后四处救火要高效得多。遇到复杂加密时评估一下逆向的成本和风险有时“绕道而行”如使用无头浏览器或者寻找其他数据源可能是更明智的选择。希望这篇详细的指南能帮你顺利拿下那些动态加载的ASMR音频数据同时也建立起安全、合规的爬虫实践意识。