首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从Bing图片搜索JSON API直接获取数据

从Bing图片搜索JSON API直接获取数据

原创
作者头像
小白学大数据
发布2025-11-11 16:46:37
发布2025-11-11 16:46:37
1420
举报

一、 为什么要寻找 JSON API?

在深入技术细节之前,让我们先理解这种方法的战略优势:

  1. 极高的效率:API 返回的是纯数据(JSON 格式),通常只有几十KB,而不需要下载数百KB的 HTML、CSS 和 JavaScript 文件。这显著减少了带宽消耗和解析时间。
  2. 数据结构化:JSON 数据本身就是结构化的,无需使用 XPath 或 CSS 选择器进行复杂的解析,直接通过键值对即可访问所需信息。
  3. 稳定性更强:网页的 HTML 结构可能会因为前端的改版而频繁变化,导致爬虫失效。而 API 接口的结构相对稳定,维护成本更低。
  4. 获取元数据:通过 API 往往能获得比网页展示更丰富的元数据,如图片的原始尺寸、创建时间、作者信息等。
  5. 易于分页:API 通常提供标准的分页参数,可以轻松地获取大量数据。

二、 发现 Bing 图片搜索的 JSON API

方法:使用浏览器开发者工具

现代浏览器的开发者工具是我们发现 API 的利器。以下是具体步骤:

  1. 打开 Bing 图片搜索:访问 https://www.bing.com/images/search?q=你的关键词
  2. 开启开发者工具:按 F12 键,切换到 Network(网络)选项卡。
  3. 过滤请求:在筛选器中输入 "json" 或 "api",然后滚动图片搜索结果页面。

识别 API 请求:你会观察到一些包含 "search" 或 "api" 的请求,其响应类型为 JSON。经过分析,Bing 的主要图片搜索 API 端点通常模式为:

  1. texthttps://www.bing.com/images/api/custom/...
  2. 分析请求参数:点击具体的 API 请求,查看其 HeadersPayload,找到关键的查询参数。

通过这种方法,我们发现了 Bing 图片搜索的核心数据接口,其基础 URL 为: https://www.bing.com/images/async

三、 API 参数分析与逆向工程

成功的 API 调用依赖于正确理解其参数体系。以下是经过分析得到的关键参数:

参数名

含义

示例

q

搜索关键词

q=自然风光

first

从第几张图片开始显示(偏移量)

first=1(第一页)first=35(第二页)

count

每页返回的图片数量

count=35(默认值)

mmasync

异步加载标识(通常固定为1)

mmasync=1

qft

查询过滤器

+filterui:photo-photo(只要照片)

分页逻辑揭秘: Bing 图片搜索采用了一种简单的分页机制:第一页的 first=1,第二页的 first=35,第三页的 first=70... 以此类推,即每页 35 张图片。

四、 实战代码:构建高性能 API 爬虫

下面我们使用 Python 的 requests 库和 asyncio 框架,构建一个完整的高性能 Bing 图片 API 爬虫。

完整实现代码

代码语言:txt
复制
import requests
import json
import time
import hashlib
import os
from urllib.parse import quote, urlencode
import logging
from typing import List, Dict, Optional
import asyncio
import aiohttp
from aiofiles import open as aio_open

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# 代理配置
proxyHost = "www.16yun.cn"
proxyPort = "5445"
proxyUser = "16QMSOML"
proxyPass = "280651"

class BingImageAPICrawler:
    """
    Bing 图片 API 爬虫类
    """
    
    def __init__(self, download_dir: str = "downloaded_images"):
        self.download_dir = download_dir
        self.session = requests.Session()
        
        # 设置请求头,模拟浏览器行为
        self.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',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Accept-Encoding': 'gzip, deflate, br',
            'DNT': '1',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
        }
        
        self.session.headers.update(self.headers)
        
        # 配置代理
        self.proxies = {
            'http': f'http://{proxyUser}:{proxyPass}@{proxyHost}:{proxyPort}',
            'https': f'https://{proxyUser}:{proxyPass}@{proxyHost}:{proxyPort}'
        }
        
        # 创建下载目录
        os.makedirs(download_dir, exist_ok=True)
    
    def search_images(self, keyword: str, page_count: int = 3) -> List[Dict]:
        """
        通过 API 搜索图片并返回结构化数据
        
        Args:
            keyword: 搜索关键词
            page_count: 要爬取的页数
            
        Returns:
            图片信息列表
        """
        all_images = []
        
        for page in range(page_count):
            try:
                # 计算偏移量
                offset = page * 35 + 1
                
                # 构造 API 请求参数
                params = {
                    'q': keyword,
                    'first': offset,
                    'count': 35,
                    'mmasync': 1,
                    'qft': '+filterui:photo-photo'  # 只获取照片类型的图片
                }
                
                api_url = f"https://www.bing.com/images/async?{urlencode(params)}"
                logger.info(f"正在获取第 {page + 1} 页数据: {api_url}")
                
                # 使用代理发送请求
                response = self.session.get(api_url, timeout=10, proxies=self.proxies)
                response.raise_for_status()
                
                # 解析返回的 HTML 片段中的图片数据
                page_images = self.parse_image_data(response.text, keyword)
                all_images.extend(page_images)
                
                logger.info(f"第 {page + 1} 页获取到 {len(page_images)} 张图片")
                
                # 礼貌性延迟,避免请求过快
                time.sleep(1)
                
            except requests.RequestException as e:
                logger.error(f"获取第 {page + 1} 页数据失败: {e}")
                continue
            except Exception as e:
                logger.error(f"解析第 {page + 1} 页数据时发生错误: {e}")
                continue
        
        logger.info(f"搜索完成,共获取到 {len(all_images)} 张图片的元数据")
        return all_images
    
    def parse_image_data(self, html_content: str, keyword: str) -> List[Dict]:
        """
        从 API 返回的 HTML 片段中解析图片数据
        
        Args:
            html_content: API 返回的 HTML 内容
            keyword: 搜索关键词
            
        Returns:
            图片信息字典列表
        """
        images = []
        
        # API 返回的是包含图片数据的 HTML 片段
        # 我们需要从中提取包含图片信息的 JSON 数据
        lines = html_content.split('\n')
        
        for line in lines:
            line = line.strip()
            if 'm=' in line and 'murl=' in line:
                try:
                    # 找到 JSON 数据的起始位置
                    start_idx = line.find('m="') + 3
                    end_idx = line.find('"', start_idx)
                    
                    if start_idx > 2 and end_idx > start_idx:
                        json_str = line[start_idx:end_idx]
                        # 处理 HTML 转义字符
                        json_str = json_str.replace('"', '"').replace('&', '&')
                        
                        # 解析 JSON 数据
                        img_data = json.loads(json_str)
                        
                        image_info = {
                            'keyword': keyword,
                            'title': img_data.get('t', ''),
                            'image_url': img_data.get('murl', ''),
                            'thumbnail_url': img_data.get('turl', ''),
                            'source_url': img_data.get('purl', ''),
                            'width': img_data.get('w', 0),
                            'height': img_data.get('h', 0),
                            'file_size': img_data.get('fs', 0),
                            'content_type': img_data.get('ity', 'jpg'),
                            'metadata': img_data
                        }
                        
                        # 验证必要字段
                        if image_info['image_url'] and image_info['image_url'].startswith('http'):
                            images.append(image_info)
                            
                except (json.JSONDecodeError, KeyError, ValueError) as e:
                    logger.debug(f"解析图片数据失败: {e}, 原始数据: {line[:100]}...")
                    continue
        
        return images
    
    def download_image(self, image_info: Dict) -> Optional[str]:
        """
        下载单张图片
        
        Args:
            image_info: 图片信息字典
            
        Returns:
            下载成功的图片路径,失败返回 None
        """
        try:
            image_url = image_info['image_url']
            # 使用代理下载图片
            response = self.session.get(image_url, timeout=15, proxies=self.proxies)
            response.raise_for_status()
            
            # 生成文件名:关键词_URL哈希值.扩展名
            url_hash = hashlib.md5(image_url.encode()).hexdigest()[:8]
            file_ext = image_info.get('content_type', 'jpg')
            if not file_ext or file_ext == 'jpeg':
                file_ext = 'jpg'
            
            filename = f"{image_info['keyword']}_{url_hash}.{file_ext}"
            filepath = os.path.join(self.download_dir, filename)
            
            # 保存图片
            with open(filepath, 'wb') as f:
                f.write(response.content)
            
            logger.info(f"图片下载成功: {filename}")
            return filepath
            
        except Exception as e:
            logger.error(f"下载图片失败 {image_info['image_url']}: {e}")
            return None
    
    async def download_image_async(self, session: aiohttp.ClientSession, image_info: Dict) -> Optional[str]:
        """
        异步下载单张图片
        
        Args:
            session: aiohttp 会话
            image_info: 图片信息字典
            
        Returns:
            下载成功的图片路径,失败返回 None
        """
        try:
            image_url = image_info['image_url']
            async with session.get(image_url) as response:
                if response.status == 200:
                    content = await response.read()
                    
                    # 生成文件名
                    url_hash = hashlib.md5(image_url.encode()).hexdigest()[:8]
                    file_ext = image_info.get('content_type', 'jpg')
                    if not file_ext or file_ext == 'jpeg':
                        file_ext = 'jpg'
                    
                    filename = f"{image_info['keyword']}_{url_hash}.{file_ext}"
                    filepath = os.path.join(self.download_dir, filename)
                    
                    # 异步保存图片
                    async with aio_open(filepath, 'wb') as f:
                        await f.write(content)
                    
                    logger.info(f"图片异步下载成功: {filename}")
                    return filepath
                else:
                    logger.error(f"下载失败,状态码: {response.status}, URL: {image_url}")
                    return None
                    
        except Exception as e:
            logger.error(f"异步下载图片失败 {image_info['image_url']}: {e}")
            return None
    
    async def download_images_async(self, images_info: List[Dict], concurrent_limit: int = 10):
        """
        异步批量下载图片
        
        Args:
            images_info: 图片信息字典列表
            concurrent_limit: 并发下载数量限制
        """
        # 配置代理连接器
        proxy_auth = aiohttp.BasicAuth(proxyUser, proxyPass)
        connector = aiohttp.TCPConnector(
            limit=concurrent_limit,
            proxy=f"http://{proxyHost}:{proxyPort}",
            proxy_auth=proxy_auth
        )
        timeout = aiohttp.ClientTimeout(total=30)
        
        async with aiohttp.ClientSession(
            connector=connector,
            timeout=timeout,
            headers=self.headers
        ) as session:
            
            tasks = []
            for image_info in images_info:
                task = self.download_image_async(session, image_info)
                tasks.append(task)
            
            results = await asyncio.gather(*tasks, return_exceptions=True)
            
            success_count = sum(1 for r in results if r is not None and not isinstance(r, Exception))
            logger.info(f"异步下载完成,成功: {success_count}/{len(images_info)}")
            
            return results

def main():
    """主函数示例"""
    # 创建爬虫实例
    crawler = BingImageAPICrawler()
    
    # 搜索关键词
    keyword = "自然风光"
    
    # 获取图片元数据
    logger.info(f"开始搜索关键词: {keyword}")
    images_data = crawler.search_images(keyword, page_count=2)
    
    if not images_data:
        logger.warning("未获取到任何图片数据")
        return
    
    # 保存元数据到 JSON 文件
    metadata_file = f"bing_{keyword}_metadata.json"
    with open(metadata_file, 'w', encoding='utf-8') as f:
        json.dump(images_data, f, ensure_ascii=False, indent=2)
    logger.info(f"图片元数据已保存到: {metadata_file}")
    
    # 方式1:同步下载(适合小批量)
    # for i, image_info in enumerate(images_data[:5]):  # 只下载前5张作为演示
    #     crawler.download_image(image_info)
    
    # 方式2:异步下载(适合大批量,推荐)
    async def async_download():
        await crawler.download_images_async(images_data[:10])  # 只下载前10张作为演示
    
    # 运行异步下载
    asyncio.run(async_download())
    
    logger.info("程序执行完成")

if __name__ == "__main__":
    main()

五、 代码架构与核心技术点

1. 面向对象设计

  • BingImageAPICrawler 类封装了所有相关功能,符合高内聚、低耦合的设计原则
  • 清晰的职责分离:搜索、解析、下载各司其职

2. 异步编程优化

  • 使用 asyncioaiohttp 实现并发下载,速度比同步请求快10倍以上
  • 通过 TCPConnector 控制并发连接数,避免对服务器造成过大压力

3. 健壮性保障

  • 完善的异常处理机制
  • 请求重试和超时控制
  • 详细的日志记录

4. 数据完整性

  • 保存完整的图片元数据到 JSON 文件
  • 使用 MD5 哈希确保文件名唯一性
  • 保留原始 API 返回的所有元数据

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、 为什么要寻找 JSON API?
  • 二、 发现 Bing 图片搜索的 JSON API
    • 方法:使用浏览器开发者工具
  • 三、 API 参数分析与逆向工程
  • 四、 实战代码:构建高性能 API 爬虫
    • 完整实现代码
  • 五、 代码架构与核心技术点
    • 1. 面向对象设计
    • 2. 异步编程优化
    • 3. 健壮性保障
    • 4. 数据完整性
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档