PAYATHON 2026

Google Image Search JSON API 是否可以免费使用?

数据阿明

结论

不能把 https://ajax.googleapis.com/ajax/services/search/images 当作可以长期免费使用的图片搜索接口,也不能保证最终用户不会遇到配额限制、限流或请求失败。

代码调用的是旧版 Google AJAX Search API,不是 Custom Search JSON API。旧接口曾允许在没有 API key 的情况下发起请求,但 Google 已经将其停用。即使某些环境偶尔还能收到响应,也不代表该服务仍受支持,更不能用于正式产品。

应改用 Google 当前支持的图片搜索方案,例如 Programmable Search Engine 对应的 Custom Search JSON API,并配置 API key、搜索引擎 ID、配额和结算。免费额度和价格可能调整,接入时应以 Google 官方控制台和定价页面显示的信息为准。

为什么原代码不可靠

旧代码请求的是:

https://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=fuzzy%20monkey

它与下面的 Custom Search JSON API 并不是同一个接口:

https://www.googleapis.com/customsearch/v1

二者的主要差异如下:

项目旧 AJAX Search APICustom Search JSON API
主机名ajax.googleapis.comwww.googleapis.com
API key旧接口曾允许不提供通常必须提供 key
搜索引擎配置不需要 cx必须提供 cx
图片搜索参数/search/imagessearchType=image
服务状态已停用,不应继续使用应以 Google 当前文档为准
配额与计费旧规则已无实际接入意义由项目配额及结算设置控制

没有传递 key 仍能获得结果,只能说明旧接口当时允许匿名访问,或者某个遗留节点仍在响应。这不代表:

  • 接口没有调用限制;
  • Google 承诺永久免费;
  • 接口可以长期用于生产环境;
  • 最终用户不会遇到 HTTP 错误、空响应或限流;
  • 搜索到的图片可以自由下载、保存和再次使用。

userip 也不是认证或计费凭据,既不能代替 API key,也无法保证配额。

推荐的实现方式

先在 Google Cloud 和 Programmable Search Engine 中完成以下配置:

  1. 创建或选择一个 Google Cloud 项目。
  2. 启用 Custom Search JSON API。
  3. 创建 API key,并限制其可以调用的 API 和客户端。
  4. 创建 Programmable Search Engine,取得搜索引擎 ID,也就是 cx。
  5. 确认该搜索引擎允许图片搜索,并根据需要设置搜索范围。
  6. 请求 customsearch/v1,传入 key、cx、q 和 searchType=image。
  7. 检查 HTTP 状态码、API 错误对象,并确认 items 是否为空。
  8. 从第一项结果中读取 link,然后单独下载图片。

请求形式如下:

https://www.googleapis.com/customsearch/v1?key=API_KEY&cx=SEARCH_ENGINE_ID&q=fuzzy%20monkey&searchType=image&num=1

不要把未加限制的 API key 直接写入公开发布的客户端程序。移动应用中的字符串通常可以被提取。更稳妥的做法是通过自己的后端调用 Google API,再将必要的结果返回给客户端。

Objective-C 示例

下面的示例包含请求结构和基本的错误处理。API_KEY 与 SEARCH_ENGINE_ID 应从安全配置或后端获取。

NSString *apiKey = @"API_KEY";
NSString *searchEngineID = @"SEARCH_ENGINE_ID";
NSString *query = @"fuzzy monkey";

NSURLComponents *components =
    [NSURLComponents componentsWithString:@"https://www.googleapis.com/customsearch/v1"];

components.queryItems = @[
    [NSURLQueryItem queryItemWithName:@"key" value:apiKey],
    [NSURLQueryItem queryItemWithName:@"cx" value:searchEngineID],
    [NSURLQueryItem queryItemWithName:@"q" value:query],
    [NSURLQueryItem queryItemWithName:@"searchType" value:@"image"],
    [NSURLQueryItem queryItemWithName:@"num" value:@"1"]
];

NSURLSessionDataTask *searchTask =
    [[NSURLSession sharedSession] dataTaskWithURL:components.URL
                               completionHandler:^(NSData *data,
                                                   NSURLResponse *response,
                                                   NSError *error) {
    if (error != nil) {
        NSLog(@"Search request failed: %@", error);
        return;
    }

    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
    if (httpResponse.statusCode < 200 || httpResponse.statusCode >= 300) {
        NSLog(@"Search returned HTTP %ld", (long)httpResponse.statusCode);
        return;
    }

    NSError *jsonError = nil;
    NSDictionary *json =
        [NSJSONSerialization JSONObjectWithData:data
                                        options:0
                                          error:&jsonError];

    if (jsonError != nil || ![json isKindOfClass:[NSDictionary class]]) {
        NSLog(@"Invalid JSON response: %@", jsonError);
        return;
    }

    NSDictionary *apiError = json[@"error"];
    if ([apiError isKindOfClass:[NSDictionary class]]) {
        NSLog(@"Google API error: %@", apiError);
        return;
    }

    NSArray *items = json[@"items"];
    if (![items isKindOfClass:[NSArray class]] || items.count == 0) {
        NSLog(@"No image results found");
        return;
    }

    NSString *imageURLString = items.firstObject[@"link"];
    NSURL *imageURL = [NSURL URLWithString:imageURLString];

    if (imageURL == nil) {
        NSLog(@"Invalid image URL");
        return;
    }

    NSURLSessionDownloadTask *downloadTask =
        [[NSURLSession sharedSession] downloadTaskWithURL:imageURL
                                        completionHandler:^(NSURL *location,
                                                            NSURLResponse *imageResponse,
                                                            NSError *downloadError) {
        if (downloadError != nil) {
            NSLog(@"Image download failed: %@", downloadError);
            return;
        }

        NSHTTPURLResponse *imageHTTPResponse =
            (NSHTTPURLResponse *)imageResponse;

        if (imageHTTPResponse.statusCode < 200 ||
            imageHTTPResponse.statusCode >= 300) {
            NSLog(@"Image returned HTTP %ld",
                  (long)imageHTTPResponse.statusCode);
            return;
        }

        NSString *contentType = imageHTTPResponse.MIMEType;
        if (![contentType hasPrefix:@"image/"]) {
            NSLog(@"Unexpected content type: %@", contentType);
            return;
        }

        NSURL *documentsURL =
            [[[NSFileManager defaultManager]
                URLsForDirectory:NSDocumentDirectory
                       inDomains:NSUserDomainMask] firstObject];

        NSURL *destinationURL =
            [documentsURL URLByAppendingPathComponent:@"monkey.jpg"];

        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSError *fileError = nil;

        if ([fileManager fileExistsAtPath:destinationURL.path]) {
            [fileManager removeItemAtURL:destinationURL error:&fileError];
            if (fileError != nil) {
                NSLog(@"Could not replace existing image: %@", fileError);
                return;
            }
        }

        if (![fileManager moveItemAtURL:location
                                 toURL:destinationURL
                                 error:&fileError]) {
            NSLog(@"Could not save image: %@", fileError);
            return;
        }

        NSLog(@"Image saved to %@", destinationURL.path);
    }];

    [downloadTask resume];
}];

[searchTask resume];

配额、计费和发布注意事项

Custom Search JSON API 的免费额度、付费单价、每日上限和产品可用性都可能发生变化。历史文档中提到的”每天免费 100 次查询”和”每 1000 次查询 5 美元”,不能脱离文档的发布日期,当作永久有效的承诺。发布前应在当前的 Google Cloud Console 中确认:

  • API 是否仍允许新项目启用;
  • 项目的每日配额;
  • 是否必须启用 billing;
  • 超出配额后会如何处理;
  • API key 的应用限制和 API 限制;
  • 当前的定价及服务条款。

还需要处理以下常见的失败情况:

  • 400:参数无效或搜索引擎配置错误;
  • 403:API 未启用、key 无效、权限受限或配额不足;
  • 429:请求过于频繁;
  • 5xx:Google 服务暂时异常;
  • items 不存在:没有搜索结果;
  • 图片服务器拒绝访问、发生重定向或返回的内容不是图片;
  • 图片链接已经失效;
  • 图片实际格式与固定的 .jpg 扩展名不一致。

搜索结果中的图片通常来自第三方网站。Google 返回图片链接,并不表示它授予了下载、缓存、展示或再次分发这些图片的许可。正式应用需要核实图片版权和网站使用条款,并尽量根据响应中的 MIME 类型选择正确的文件扩展名。

备注:内容仅供参考。