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 特有的坑
- HttpClient 要复用 :每个请求
new HttpClient()会耗尽socket,上面代码用静态单例;要按请求换 key,改用HttpRequestMessage加头(本例正是这么写的),而不是改 DefaultRequestHeaders。 - JsonDocument 的生命周期 :
RootElement在JsonDocument释放后失效,返回前Clone();遍历时用TryGetProperty处理可选字段,别直接GetProperty抛 KeyNotFoundException。 - SSL/代理环境 :公司内网常配自签证书或代理,
HttpClientHandler的ServerCertificateCustomValidationCallback和Proxy属性在这里配,别散在业务代码里。
FAQ
为什么不用 Refit/RestSharp? 一个 POST 接口,手写 30 行换来零依赖;Refit 适合接口面大的场景,这里收益为负。
翻页怎么做? SearchAsync(q, page: 2),page 从 1 起;翻到 organic 为空或与上页重复就停。
能同时抓 PAA、相关搜索吗? 接口返回里这些是可选模块,同样用 TryGetProperty 判存后遍历;字段结构以实际返回和文档为准,先原样存 JSON 再建模。
把这个类放进项目,配一个 DI 注册(services.AddSingleton(new SerpClient(key))),后台服务里就能随时拿结构化搜索结果了。