C# 调用谷歌搜索 API:HttpClient 与 System.Text.Json 实战

C#/.NET 项目接搜索数据的场景不少:内部工具、WinForm 小程序、ASP.NET 后台服务。这篇用最少的依赖把"调谷歌搜索 API"做成一个可复用的客户端------只用 HttpClient 和 System.Text.Json,不引第三方包,老项目也能直接抄。

客户端代码

csharp 复制代码
// SerpClient.cs
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

public sealed class SerpClient
{
    private static readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(30) };

    private readonly string _apiKey;
    public SerpClient(string apiKey) => _apiKey = apiKey;

    public async Task<JsonElement> SearchAsync(
        string q, string hl = "en", string gl = "us", int page = 1,
        CancellationToken ct = default)
    {
        using var req = new HttpRequestMessage(HttpMethod.Post,
            "https://api.serpbase.dev/google/search");
        req.Headers.TryAddWithoutValidation("X-API-Key", _apiKey);
        req.Content = new StringContent(
            JsonSerializer.Serialize(new { q, hl, gl, page }),
            Encoding.UTF8, "application/json");

        using var resp = await _http.SendAsync(req, ct);
        resp.EnsureSuccessStatusCode();

        using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync(ct));
        var root = doc.RootElement;

        // 信封:status=0 成功;失败时 error 带原因、credits_charged 为 0(不扣费)
        if (root.TryGetProperty("status", out var st) && st.GetInt32() != 0)
            throw new SerpApiException(st.GetInt32(),
                root.TryGetProperty("error", out var e) ? e.GetString() : "unknown");

        return root.Clone();   // Clone 后 doc 释放仍可用
    }
}

public sealed class SerpApiException(int code, string message)
    : Exception($"SERP API error {code}: {message}");

调用侧------遍历自然结果只需要几行:

csharp 复制代码
var client = new SerpClient(Environment.GetEnvironmentVariable("SERPBASE_API_KEY")!);
var data = await client.SearchAsync("mechanical keyboard review");

foreach (var r in data.GetProperty("organic").EnumerateArray())
    Console.WriteLine($"{r.GetProperty("rank").GetInt32(),3}  {r.GetProperty("title").GetString()}");
//     取其他字段前先 TryGetProperty:snippet、display_url 都是可选字段

信封结构、参数表(q/hl/gl/page/device)与 organic 字段定义以 SerpBase 官方文档 为准;rank 是本页内的 1 起算位次,link 是每条必有字段。

错误分诊表

信号 含义 动作
SerpApiException 1001 key 缺失/无效 不重试,查配置
SerpApiException 1020 余额不足 告警,人工充值
SerpApiException 1029 触发限流 指数退避重试,最多 3 次
TaskCanceledException 30 秒超时 重试一次,再失败记日志
HttpRequestException 网络/连接层 退避重试

计费口径:search 端点每次成功请求 1 credit;100 次免费试用够把这个类在开发环境调稳,标准包 $0.50/1k 起。

三个 .NET 特有的坑

  1. HttpClient 要复用 :每个请求 new HttpClient() 会耗尽socket,上面代码用静态单例;要按请求换 key,改用 HttpRequestMessage 加头(本例正是这么写的),而不是改 DefaultRequestHeaders。
  2. JsonDocument 的生命周期 :RootElementJsonDocument 释放后失效,返回前 Clone();遍历时用 TryGetProperty 处理可选字段,别直接 GetProperty 抛 KeyNotFoundException。
  3. SSL/代理环境 :公司内网常配自签证书或代理,HttpClientHandlerServerCertificateCustomValidationCallbackProxy 属性在这里配,别散在业务代码里。

FAQ

为什么不用 Refit/RestSharp? 一个 POST 接口,手写 30 行换来零依赖;Refit 适合接口面大的场景,这里收益为负。

翻页怎么做? SearchAsync(q, page: 2),page 从 1 起;翻到 organic 为空或与上页重复就停。

能同时抓 PAA、相关搜索吗? 接口返回里这些是可选模块,同样用 TryGetProperty 判存后遍历;字段结构以实际返回和文档为准,先原样存 JSON 再建模。

把这个类放进项目,配一个 DI 注册(services.AddSingleton(new SerpClient(key))),后台服务里就能随时拿结构化搜索结果了。

相关推荐
qq_4101942911 小时前
.NET 8.0 下基于 PaddleOCRSharp 的离线文字识别落地指南
算法·.net
piikee13 小时前
论坛帖子百度收录越来越慢?提高收录速度实测案例
搜索引擎·百度收录·必应快收·好用蜘蛛池·蜘蛛1号·收录慢
goujunwe13 小时前
GEO 评估方法:如何自测你的内容,会不会被 AI 引用
搜索引擎·网络安全·游戏引擎·全文检索·中文分词
唐青枫2 天前
别急着拆服务:C#.NET 微服务架构从边界设计到实战
c#·.net
hi_ro_a2 天前
基于正倒排索引的boost搜索引擎
linux·c++·搜索引擎·项目
LaughingZhu2 天前
Product Hunt 每日热榜 | 2026-09-17
人工智能·深度学习·神经网络·搜索引擎·百度
Y3815326622 天前
SEO 周报自动化:从采集数据到一页报告
数据库·搜索引擎
Elastic 中国社区官方博客3 天前
列式存储并不等同于列式数据库。Columnar 模式为 Elasticsearch 带来了什么
大数据·运维·数据库·elasticsearch·搜索引擎
唐青枫3 天前
别把 Razor 当成几段 HTML:C#.NET Razor Pages、MVC 视图与实战详解
c#·.net