目录
一、项目背景与概述
二、环境准备与工具配置
2.1 开发环境要求
2.2 辅助工具配置
三、详细抓取流程解析
3.1 页面加载机制分析
3.2 关键请求识别技巧
3.3 参数规律深度分析
四、爬虫代码实现
五、实现关键
六、法律与道德规范
一、项目概述
在当今互联网时代,数据采集与分析已成为重要的技术能力。本教程将以今日头条街拍美图为例,详细介绍如何通过Python实现Ajax数据抓取的完整流程。
今日头条作为国内领先的内容平台,其图片内容资源丰富,特别是街拍类图片深受用户喜爱。通过分析其Ajax接口,我们可以学习到现代Web应用的数据加载方式,掌握处理动态内容的爬虫开发技巧。
二、环境准备与工具配置
2.1 开发环境要求
1. Python环境:建议使用Python 3.6及以上版本
- 可通过官网下载安装:https://www.python.org/downloads/
- 安装后验证:`python --version`
2. 必需库安装:
pip install requests urllib3 hashlib multiprocessing
3. 推荐开发工具:
- IDE:PyCharm、VS Code
- 浏览器:Chrome(用于开发者工具分析)
- 调试工具:Postman(用于接口测试)
2.2 辅助工具配置
1. Chrome开发者工具:
- 快捷键F12或右键"检查"打开
- 重点使用Network面板和Elements面板
- 建议开启"Preserve log"选项保留请求记录
2. 代理设置(可选):
略
三、详细抓取流程解析
3.1 页面加载机制分析
现代Web应用普遍采用前后端分离架构,理解其数据加载原理至关重要:
1. 传统网页:服务端渲染,HTML包含所有内容
2. 现代SPA:初始HTML为空壳,通过Ajax动态加载数据
下面以今日头条为例分析:
首次加载基础框架
在今日头条首页( https://www.toutiao.com/),直接输入街拍搜索,然后如下点击图片选项,就会出现大把的美女图片。
这时按ctrl+s即可保存所有已加载的网页内容,保存下来的网页可以直接搜到jpeg的图片链接。但是我们现在要查看接口请求,我们按F12打开开发者工具,重新加载页面,查看所有的网络请求。
首先,查看发起第一个网络请求,请求的URL是 ( https://so.toutiao.com/search?dvpf=pc&source=search_subtab_switch&keyword=%E8%A1%97%E6%8B%8D&pd=atlas&action_type=search_subtab_switch&page_num=0&search_id=&from=gallery&cur_tab_title=gallery )。然后打开Preview选项卡,查看响应体内容。要是页面上的内容是依据第一个请求的结果渲染出来的 。
由于查看他的请求头内容Headers没有明确的 X-Requested-With
请求头,看不出是ajax请求。
通过XHR请求获取实际内容
然而我点击Fetch/XHR进行过滤ajax请求,并快速向下滚动页面就会发现下面某接口:
https://so.toutiao.com/search?dvpf=pc&source=search_subtab_switch&keyword=%E8%A1%97%E6%8B%8D&pd=atlas&action_type=search_subtab_switch&page_num=1&search_id=20250416152239B787556EC505238FF4F8&from=gallery&cur_tab_title=gallery&rawJSON=1
其中请求头内容的sec-fetch-mode: cors
和其他请求头(如 accept: */*
)以及请求的行为模式(如通过查询参数发送数据),这些都是AJAX 请求特征。
然后点击Preview查看响应数据的数据结构
看得出为了防止连续被爬取图片,参数page_num和search_id是有限制关系的,即使page_num有规律,但是search_id却另外计算获取。
3.2 关键请求识别技巧
在开发者工具的Network面板中:
1. 过滤XHR请求:快速定位Ajax调用
2. Preview功能:直观查看JSON数据结构
3. Headers分析:
- Request URL:接口地址
- Request Method:GET/POST
- Query Parameters:请求参数
4. 时序分析:通过Waterfall了解加载顺序
3.3 参数规律深度分析
从提供的 URL,我们可以解析出一系列的查询参数(query parameters),每个参数都有其特定的作用:
1. `dvpf=pc`: 这个参数可能指示请求是从个人计算机(PC)发出的,可能用于区分不同设备类型的请求,如移动端或平板电脑。
2. `source=search_subtab_switch`: 这个参数可能表示请求的来源,这里是“search_subtab_switch”,可能是指用户在搜索结果的子标签页之间进行切换时触发的请求。
3. `keyword=%E8%A1%97%E6%8B%8D`: 这是经过URL编码的搜索关键词,解码后是“街拍”。这是用户在搜索框中输入的关键词。
4. `pd=atlas`: 这个参数的具体含义不是很清晰,它可能指的是请求的特定部分或页面(Page Division)。
5. `action_type=search_subtab_switch`: 类似于 `source` 参数,这个参数可能指明了用户执行的动作类型,这里是在搜索子标签页之间切换。
6. `page_num=1`: 这个参数指示请求的是搜索结果的第几页,这里是第2页,从0开始算。
7. `search_id=20250416152239B787556EC505238FF4F8`: 这是一个唯一的搜索会话标识符,用于追踪用户的搜索会话。
8. `from=gallery`: 这个参数可能指示用户是从图库部分发起搜索的。
9. `cur_tab_title=gallery`: 这个参数可能表示当前活动的标签页标题是图库。
10. `rawJSON=1`: 这个参数指示服务器返回的数据格式应该是原始的 JSON 格式,而不是经过任何服务器端处理的数据。
每个参数都是键值对的形式,通过 `&` 符号连接。服务器会解析这些参数,以便根据用户的请求提供定制化的内容。例如,服务器可以根据 `keyword` 参数提供搜索关键词的相关结果,`page_num` 参数用来分页显示结果,而 `search_id` 用来保持搜索会话的连续性。
四、爬虫代码实现
模拟请求的时候需要设置必要的请求头参数,包括cookie等,不然请求拿不到数据。因为search_id暂时还没找到解决办法,只能获取某一页数据。由于search_id会变,所以下面拿了最新的模拟请求:
import requests
import re
import os
from hashlib import md5
import json
from urllib.parse import urlencode
import time
import gzip
import iodef get_page(page_num):base_url = 'https://so.toutiao.com/search?'params = {'dvpf': 'pc','source': 'search_subtab_switch','keyword': '街拍','pd': 'atlas','action_type': 'search_subtab_switch','page_num': page_num,'search_id': '202504161626333A15E72F0B54BF7C39AB','from': 'gallery','cur_tab_title': 'gallery','rawJSON': '1'}headers = {'Accept': '*/*','Accept-Language': 'zh-CN,zh;q=0.9','Connection': 'keep-alive','Cookie': 'tt_webid=7493748220343043611; _tea_utm_cache_4916=undefined; _S_DPR=1; _S_IPAD=0; s_v_web_id=verify_m9jdj7kh_AAv9h1Yy_tQ7d_4YCM_Bi1J_NMRjG0VhwXwy; notRedShot=1; _ga=GA1.1.1977312639.1744775745; n_mh=cLfs9nL5b4PcW0-N8UbVRswdJYzGe1l4yne0DeQP69A; passport_auth_status=7434e2c0910f588f9110b55667f76765%2C39b0d143549b4c6761ea2d3398afbe9a; passport_auth_status_ss=7434e2c0910f588f9110b55667f76765%2C39b0d143549b4c6761ea2d3398afbe9a; sid_guard=4337463570ade0a3133b7705c077800d%7C1744776132%7C5184002%7CSun%2C+15-Jun-2025+04%3A02%3A14+GMT; uid_tt=47365c817295ff0854b31381946d71b2; uid_tt_ss=47365c817295ff0854b31381946d71b2; sid_tt=4337463570ade0a3133b7705c077800d; sessionid=4337463570ade0a3133b7705c077800d; sessionid_ss=4337463570ade0a3133b7705c077800d; _S_WIN_WH=1920_396; __ac_nonce=067ff6999008e39e36643; __ac_signature=_02B4Z6wo00f01lt23uAAAIDD6btFq9NvMj5bVtpAAPEx9d; __ac_referer=https://so.toutiao.com/search?dvpf=pc&source=input&keyword=%E8%A1%97%E6%8B%8D','Host': 'so.toutiao.com','Referer': 'https://so.toutiao.com/search?dvpf=pc&source=search_subtab_switch&keyword=%E8%A1%97%E6%8B%8D&pd=atlas&action_type=search_subtab_switch&page_num=0&search_id=&from=gallery&cur_tab_title=gallery','Sec-Fetch-Dest': 'empty','Sec-Fetch-Mode': 'cors','Sec-Fetch-Site': 'same-origin','User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36','sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"','sec-ch-ua-mobile': '?0','sec-ch-ua-platform': '"Windows"'}url = base_url + urlencode(params)try:# 直接请求数据response = requests.get(url, headers=headers, stream=True)if response.status_code == 200:try:# 尝试直接解析响应内容content = response.texttry:json_data = json.loads(content)return json_dataexcept json.JSONDecodeError:print(f"Failed to decode JSON. Response content: {content[:200]}...")# 如果解析失败,尝试解码原始内容raw_content = response.content.decode('utf-8', errors='ignore')print(f"Raw content: {raw_content[:200]}...")return Noneexcept Exception as e:print(f"Error processing response: {str(e)}")print(f"Response headers: {response.headers}")return Noneelse:print(f"Request failed with status code: {response.status_code}")return Noneexcept requests.ConnectionError as e:print('Error occurred while fetching the page:', e)return Nonedef parse_images(json_data):if not json_data or 'rawData' not in json_data:return# 解析返回的JSON数据data = json_data['rawData'].get('data', [])for item in data:if item.get('text') and (item.get('img_url') or item.get('original_image_url')):title = item.get('text', '')# 优先使用img_urlimage_url = item.get('img_url', '')if not image_url:image_url = item.get('original_image_url', '')yield {'title': title,'image': image_url,'width': item.get('width', 0),'height': item.get('height', 0)}def save_image(item):if not item.get('title') or not item.get('image'):return# 清理文件名中的非法字符title = re.sub(r'[\\/:*?"<>|]', '_', item.get('title'))img_path = 'img' + os.path.sep + titleif not os.path.exists(img_path):os.makedirs(img_path)max_retries = 3retry_count = 0original_url = item.get('image')# 构建可能的URL列表image_urls = []if 'pstatp.com' in original_url or 'ttcdn-tos' in original_url:# 移除协议头以便替换域名url_without_protocol = original_url.split('://')[-1]# 替换域名domains = ['p3-search.byteimg.com','p1-search.byteimg.com','p6-search.byteimg.com','p3-tt.byteimg.com','p1-tt.byteimg.com','p6-tt.byteimg.com']# 替换图片格式和尺寸formats = ['~640x640.jpeg','~480x480.jpeg','~noop.image','~tplv-tt-cs0.jpeg']base_url = url_without_protocol.split('~')[0]for domain in domains:for fmt in formats:url = f'https://{domain}/{base_url.split("/", 1)[1]}{fmt}'if url not in image_urls:image_urls.append(url)# 始终添加原始URL作为最后的选项if original_url not in image_urls:image_urls.append(original_url)# 请求头img_headers = {'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8','Accept-Language': 'zh-CN,zh;q=0.9','Connection': 'keep-alive','Referer': 'https://so.toutiao.com/','Origin': 'https://so.toutiao.com','Sec-Fetch-Dest': 'image','Sec-Fetch-Mode': 'no-cors','Sec-Fetch-Site': 'cross-site','User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36','Cookie': 'tt_webid=7493748220343043611; _tea_utm_cache_4916=undefined; _S_DPR=1; _S_IPAD=0; s_v_web_id=verify_m9jdj7kh_AAv9h1Yy_tQ7d_4YCM_Bi1J_NMRjG0VhwXwy'}success = Falsefor image_url in image_urls:if retry_count >= max_retries:breaktry:# 使用stream=True来分块下载response = requests.get(image_url, headers=img_headers, stream=True, timeout=10)if response.status_code == 200:try:# 使用响应内容的前8192字节来计算MD5first_chunk = next(response.iter_content(chunk_size=8192))file_name = md5(first_chunk).hexdigest()file_path = img_path + os.path.sep + f'{file_name}.jpg'if not os.path.exists(file_path):# 分块写入文件,先写入第一块with open(file_path, 'wb') as f:f.write(first_chunk)# 继续写入剩余的块for chunk in response.iter_content(chunk_size=8192):if chunk:f.write(chunk)print('Downloaded image path is %s' % file_path)print(f'Image size: {item.get("width")}x{item.get("height")}')else:print('Already Downloaded', file_path)success = Truebreakexcept Exception as e:print(f'Error processing image data: {str(e)}')continueelse:print(f"Failed to download image, status code: {response.status_code}")print(f"Image URL: {image_url}")except Exception as e:print('Error downloading image:', str(e))print(f"Image URL: {image_url}")retry_count += 1if not success and retry_count < max_retries:print(f"Retrying... ({retry_count}/{max_retries})")time.sleep(2)if not success:print(f"Failed to download image after trying all URL variations")def main(page):json_data = get_page(page)if json_data:for item in parse_images(json_data):save_image(item)else:print(f"Failed to get data for page {page}")if __name__ == '__main__':# 爬取第1页print('Starting page 1...')main(1)print('Page 1 completed')
爬取结果:
五、实现关键
1. 请求头模拟
headers = {'Accept': '*/*','Accept-Language': 'zh-CN,zh;q=0.9','Connection': 'keep-alive','Referer': 'https://so.toutiao.com/','Origin': 'https://so.toutiao.com','Sec-Fetch-Dest': 'empty','Sec-Fetch-Mode': 'cors','Sec-Fetch-Site': 'same-origin','User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...','sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24"...','sec-ch-ua-mobile': '?0','sec-ch-ua-platform': '"Windows"'
}
- 模拟真实浏览器的请求头
- 添加 Referer 和 Origin 防盗链
- 设置正确的 Sec-Fetch-* 系列头
- 使用最新的 Chrome User-Agent
2. Cookie 处理
'Cookie': 'tt_webid=7493748220343043611; _tea_utm_cache_4916=undefined; s_v_web_id=verify_m9jdj7kh_AAv9h1Yy_tQ7d_4YCM_Bi1J_NMRjG0VhwXwy; ...'
- 使用有效的会话 Cookie
- 包含必要的认证信息
- 保持登录状态
3. 多域名策略
domains = ['p3-search.byteimg.com','p1-search.byteimg.com','p6-search.byteimg.com','p3-tt.byteimg.com','p1-tt.byteimg.com','p6-tt.byteimg.com'
]
- 尝试多个 CDN 域名
- 在域名被封禁时自动切换
- 使用不同的图片服务器
4. URL 格式变换
formats = ['~640x640.jpeg','~480x480.jpeg','~noop.image','~tplv-tt-cs0.jpeg'
]
- 尝试不同的图片格式和尺寸
- 使用多种图片后缀
- 适应不同的图片处理参数
5. 请求延时和重试
time.sleep(2) # 增加等待时间
max_retries = 3 # 最大重试次数
- 添加请求间隔,避免频率过高
- 实现重试机制
- 错误时优雅退出
6. 分块下载
response = requests.get(url, headers=headers, stream=True)
for chunk in response.iter_content(chunk_size=8192):if chunk:f.write(chunk)
- 使用流式下载
- 分块处理大文件
- 避免内存占用过大
7. 错误处理
try:# 下载尝试
except Exception as e:print('Error downloading image:', str(e))print(f"Image URL: {image_url}")
- 详细的错误信息记录
- 异常捕获和处理
- 失败时的优雅降级
8. URL 优先级策略
# 优先使用img_url
image_url = item.get('img_url', '')
if not image_url:image_url = item.get('original_image_url', '')
- 优先使用缩略图 URL
- 备选原始图片 URL
- 多重 URL 备份
9. 请求超时设置
response = requests.get(image_url, headers=img_headers, stream=True, timeout=10)
- 设置合理的超时时间
- 避免请求挂起
- 提高程序稳定性
10. 文件处理优化
file_name = md5(first_chunk).hexdigest()
if not os.path.exists(file_path):# 写入文件
- 使用 MD5 避免重复下载
- 检查文件是否存在
- 文件名清理和规范化
由于反爬虫的限制,上面的关键步骤提高了爬虫的成功率和稳定性,同时也避免了对目标服务器造成过大压力。
后续优化:后续可以尝试分页爬取
六、法律与道德规范
1. robots.txt检查:
- 访问 `http://www.toutiao.com/robots.txt`
- 遵守爬虫协议规定
2. 合理使用原则:
- 控制请求频率
- 不进行商业性使用
- 尊重版权信息
3. 数据使用建议:
- 仅用于学习研究
- 不存储敏感信息
- 及时删除原始数据