本教程旨在解决动态加载图片网站(如Dermnet)的爬取难题。当传统爬虫工具(BeautifulSoup、Selenium)因JavaScript动态渲染而失效时,通过浏览器开发者工具深入分析网络请求,直接定位并利用网站后台调用的Google Custom Search API接口,获取结构化的JSON数据,从而高效、精准地提取目标图片信息,并探讨分页处理策略。
传统爬取方法的局限性
在进行网页数据抓取时,开发者通常会首先考虑使用beautifulsoup或selenium等工具。然而,面对现代网站日益复杂的动态加载机制,这些工具可能会遇到瓶颈。
- BeautifulSoup的限制: BeautifulSoup是一个强大的HTML/XML解析库,但它只能处理静态HTML内容。对于通过JavaScript在页面加载后动态生成或插入的内容,BeautifulSoup无法执行这些JavaScript代码,因此也无法“看到”这些动态加载的数据。例如,Dermnet网站的图片并非直接嵌入在初始HTML中,而是通过JavaScript调用外部API后才渲染到页面上。
- Selenium的局限性: Selenium通过模拟真实浏览器行为,可以执行JavaScript并等待页面内容加载。这对于许多动态网站是有效的。然而,在某些情况下,如Dermnet网站,图片数据是通过Google Custom Search API(CSE API)直接获取的JSON数据,然后由前端JavaScript解析并渲染。虽然Selenium最终能看到渲染后的图片,但直接从API获取JSON数据通常更高效、更稳定,并且能提供更结构化的原始数据,避免了从复杂的DOM结构中提取信息的麻烦。此外,运行Selenium需要启动浏览器实例,资源消耗相对较高。
当发现BeautifulSoup无法找到目标元素,或者Selenium虽然能加载页面但效率低下时,就需要考虑更深层次的解决方案。
解决方案:深入分析网络请求
解决动态加载内容爬取问题的关键在于理解其背后的数据加载机制。大多数动态内容都是通过前端JavaScript向后端API发送请求并获取数据。通过浏览器开发者工具,我们可以“窥探”这些幕后的API调用。
1. 开发者工具的应用
打开目标网站(例如Dermnet的搜索结果页),按下F12键或右键点击页面选择“检查”打开开发者工具。导航到“网络 (Network)”标签页。
2. 定位API请求
在“网络”标签页中,重新加载页面或执行触发图片加载的操作(如滚动、点击搜索)。你会看到一系列的网络请求。我们需要仔细观察这些请求,寻找与图片数据相关的API调用。
立即学习“前端免费学习笔记(深入)”;
- 筛选请求: 可以使用过滤器(例如,按“XHR”或“JS”类型过滤)来缩小范围。
- 识别关键URL: 在Dermnet的案例中,通过观察发现存在一个指向https://cse.google.com/cse/element/v1?的请求。这个URL明显与Google Custom Search Engine相关。
- 检查请求详情: 点击这个请求,查看其“标头 (Headers)”和“响应 (Response)”标签页。
- 请求URL和参数: 在“标头”中,可以找到完整的请求URL及其携带的查询参数。这些参数至关重要,它们定义了搜索的关键词、返回结果的数量、搜索类型(图片)、自定义搜索ID等。 例如:q=basal%20cell%20carcinoma%20dermoscopy (查询词), searchtype=image (搜索类型), num=16 (结果数量), cx=015036873904746004277:nz7deehiccq (自定义搜索ID)。
- 响应数据结构: 在“响应”标签页中,你会看到API返回的数据。对于Dermnet,它返回的是一个JSONP(JSON with Padding)格式的字符串,通常以一个回调函数名包裹着JSON对象,例如google.search.cse.api10440({…})。这个JSON对象内部包含一个results字段,其中包含了所有图片的详细信息(如图片URL、标题、缩略图URL等)。
3. 处理分页
在“网络”标签页中,尝试点击网站上的“下一页”按钮或滚动加载更多内容。观察API请求的URL参数如何变化。通常,会有一个参数(如start、page或offset)用于控制分页。例如,如果第一页从start=1开始,每页显示16张图片,那么第二页可能从start=17开始。
实现步骤与示例代码
一旦我们定位了API请求并理解了其参数和响应结构,就可以使用HTTP客户端库(如Python的requests)直接调用API来获取数据。
1. 构建请求URL与参数
根据开发者工具中观察到的信息,构建API请求的URL和参数字典。
import requests import json import re # For parsing JSONP def fetch_dermnet_images_from_api(query_term, start_index=1, num_results=16): """ 从Dermnet网站的Google CSE API抓取图片信息。 :param query_term: 搜索关键词。 :param start_index: 结果的起始索引(用于分页)。 :param num_results: 每页返回结果的数量。 :return: 包含图片信息的列表。 """ base_url = "https://cse.google.com/cse/element/v1" # 根据网络请求观察到的参数构建 params = { "rsz": "large", "num": num_results, "hl": "en", "source": "gcsc", "gss": ".com", "cselibv": "8e77c7877b8339e2", # 注意: 此参数可能随时间变化,需定期检查 "searchtype": "image", "cx": "015036873904746004277:nz7deehiccq", "q": query_term, "safe": "off", "start": start_index, # 分页参数 "exp": "csqr,cc,bf", "callback": "google.search.cse.api10440" # 保持此参数以获取JSONP格式 } # 模拟浏览器User-Agent,避免被识别为爬虫 headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) appleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" } images_data = [] try: response = requests.get(base_url, params=params, headers=headers, timeout=10) response.raise_for_status() # 如果请求失败(HTTP状态码非200),则抛出异常 # 解析JSONP响应 # 响应格式通常为 google.search.cse.apiXXXXX({...JSON_CONTENT...}) jsonp_content = response.text match = re.search(r'^w+((.*))$', jsonp_content, re.DOTALL) if match: json_str = match.group(1) data = json.loads(json_str) if "results" in data: for item in data["results"]: if "image" in item: images_data.append({ "title": item.get("title", "No Title"), "url": item["image"].get("src", ""), "thumbnail_url": item["image"].get("thumbnailSrc", ""), "width": item["image"].get("width", 0), "height": item["image"].get("height", 0) }) else: print(f"Error: Could not parse JSONP response for query '{query_term}'.") print(f"Raw response start: {jsonp_content[:200]}...") except requests.exceptions.RequestException as e: print(f"Network or HTTP error for query '{query_term}': {e}") except json.JSONDecodeError as e: print(f"JSON decoding error for query '{query_term}': {e}") except Exception as e: print(f"An unexpected error occurred for query '{query_term}': {e}") return images_data # 示例用法: if __name__ == "__main__": search_query = "basal cell carcinoma dermoscopy" print(f"--- 抓取第一页 '{search_query}' 的图片 ---") first_page_images = fetch_dermnet_images_from_api(search_query, start_index=1, num_results=16) if first_page_images: print(f"第一页共找到 {len(first_page_images)} 张图片。") for i, img in enumerate(first_page_images[:3]): # 只打印前3张示例 print(f" {i+1}. 标题: {img['title']}, URL: {img['url']}") else: print("未找到第一页图片或发生错误。") print(f"n--- 抓取第二页 '{search_query}' 的图片 ---") # 假设每页16张,第二页从索引17开始 second_page_images = fetch_dermnet_images_from_api(search_query, start_index=17, num_results=16) if second_page_images: print(f"第二页共找到 {len(second_page_images)} 张图片。") for i, img in enumerate(second_page_images[:3]): # 只打印前3张示例 print(f" {i+1}. 标题: {img['title']}, URL: {img['url']}") else: print("未找到第二页图片或发生错误。")
2. 代码解释
- base_url 和 params: base_url是Google CSE API的端点。params字典包含了所有从开发者工具中观察到的查询参数。q是搜索关键词,start用于控制分页(结果的起始索引),num是每页返回的结果数量。
- headers: 模拟User-Agent是一个良好的实践,可以减少被网站识别为爬虫的风险。
- requests.get(): 发送HTTP GET请求到API端点。
- response.raise_for_status(): 检查HTTP响应状态码,如果不是2xx,则抛出HTTPError。
- JSONP解析: 由于API返回的是JSONP格式(以函数调用包裹JSON),我们使用正则表达式re.search(r’^w+((.*))$’, jsonp_content, re.DOTALL)来提取括号内的纯JSON字符串,然后使用json.loads()进行解析。
- 数据提取: 解析后的data字典中,results字段是一个列表,包含了每张图片的详细信息。我们遍历这个列表,提取出title、url和thumbnail_url等关键信息。
- 分页处理: 通过循环调用fetch_dermnet_images_from_api函数,并递增start_index参数(例如,start_index = current_page * num_results + 1),即可实现多页数据的抓取。
注意事项
在进行API抓取时,需要考虑以下几点以确保爬虫的稳定性和合规性:
- API参数的动态性: 某些API参数(如cselibv、cse_tok)可能是动态生成的或有时效性。如果爬虫突然失效,首先应检查这些参数是否已改变,可能需要重新通过开发者工具获取最新的值。
- User-Agent: 始终建议在请求中设置User-Agent头,模拟主流浏览器,以降低被服务器识别为爬虫并拒绝访问的风险。
- 频率限制(Rate Limiting): API通常会有调用频率限制。频繁或过快的请求可能导致IP被临时或永久封禁。应加入适当的延迟(time.sleep())并实现重试机制。
- 错误处理: 编写健壮的代码,处理各种可能的错误,如网络连接失败、HTTP错误状态码、JSON解析失败等。
- robots.txt: 在抓取任何网站之前,请务必检查其robots.txt文件(例如https://dermnetnz.org/robots.txt),了解网站的爬取规则,尊重网站的意愿。
- 法律与道德: 确保您的爬取行为符合当地法律法规,不侵犯版权、隐私权或其他合法权益。对于抓取到的图片,应明确其使用限制,避免未经授权的商业用途。
总结
当传统的网页爬取方法在面对JavaScript动态加载内容的网站时遇到困难,通过深入分析浏览器开发者工具中的网络请求,直接定位并利用网站后端调用的API接口,是一种更高效、更稳定的解决方案。这种方法不仅能绕过前端渲染的复杂性,直接获取结构化的数据,还能更好地处理分页等场景。掌握这一技能,是成为一名高级爬虫工程师的重要一步。
以上就是动态网站图片抓取进阶:利用Google CSE API绕过javascript python java html js 前端 json go 正则表达式 windows 浏览器 Python JavaScript json 正则表达式 html beautifulsoup xml 回调函数 字符串 循环 数据结构 接口 JS 对象 dom padding http https