feat: 初始化Google Search API Wrapper项目
添加项目基础文件,包括Cargo.toml、.gitignore、.env、README.md和src/main.rs。实现基于Actix-web的RESTful API,支持搜索和补全建议功能,使用Google Custom Search API和Google Suggest API获取数据。
This commit is contained in:
commit
6eeb5f88bb
2
.env
Normal file
2
.env
Normal file
@ -0,0 +1,2 @@
|
||||
GOOGLE_API_KEY=AIzaSyCB3N08QDsBa3p9DLZy5r5fq1Y727JJPz4
|
||||
SEARCH_ENGINE_ID=d3655c32541054823
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
2296
Cargo.lock
generated
Normal file
2296
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "search"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
dotenv = "0.15"
|
24
README.md
Normal file
24
README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Google Search API Wrapper
|
||||
|
||||
这是一个使用Rust和Actix-web框架封装的Google Search API服务。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 提供RESTful API接口
|
||||
- 支持JSON格式的请求和响应
|
||||
- 异步处理提高性能
|
||||
- 环境变量配置
|
||||
|
||||
## 技术栈
|
||||
|
||||
- [Actix-web](https://actix.rs/) - Rust的Web框架
|
||||
- [Reqwest](https://docs.rs/reqwest/) - HTTP客户端
|
||||
- [Tokio](https://tokio.rs/) - 异步运行时
|
||||
- [Serde](https://serde.rs/) - 序列化/反序列化
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. 克隆仓库
|
||||
```bash
|
||||
git clone https://github.com/your-repo/search.git
|
||||
cd search
|
186
src/main.rs
Normal file
186
src/main.rs
Normal file
@ -0,0 +1,186 @@
|
||||
use actix_web::{get, web, App, HttpServer, Responder, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use dotenv::dotenv;
|
||||
use reqwest::header;
|
||||
|
||||
// 补全建议的JSON结构
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CompletionResponse {
|
||||
success: bool,
|
||||
message: String,
|
||||
suggestions: Vec<String>,
|
||||
}
|
||||
|
||||
// 搜索结果的JSON结构
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct SearchResult {
|
||||
title: String,
|
||||
link: String,
|
||||
snippet: String,
|
||||
}
|
||||
|
||||
// API响应结构
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ApiResponse {
|
||||
success: bool,
|
||||
message: String,
|
||||
data: Option<Vec<SearchResult>>,
|
||||
current_page: u32,
|
||||
total_pages: u32,
|
||||
}
|
||||
|
||||
// 查询参数结构
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SearchParams {
|
||||
q: String,
|
||||
page: Option<u32>,
|
||||
}
|
||||
|
||||
// 从Google CSE获取搜索结果
|
||||
async fn fetch_google_search_results(
|
||||
query: &str,
|
||||
start: u32,
|
||||
) -> Result<(Vec<SearchResult>, u32), Box<dyn std::error::Error>> {
|
||||
dotenv().ok();
|
||||
let api_key = env::var("GOOGLE_API_KEY").expect("GOOGLE_API_KEY must be set");
|
||||
let search_engine_id = env::var("SEARCH_ENGINE_ID").expect("SEARCH_ENGINE_ID must be set");
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let num_results = 10; // 每页10条结果
|
||||
|
||||
let response = client
|
||||
.get("https://www.googleapis.com/customsearch/v1")
|
||||
.query(&[
|
||||
("key", &api_key),
|
||||
("cx", &search_engine_id),
|
||||
("q", &query.to_string()),
|
||||
("num", &num_results.to_string()),
|
||||
("start", &start.to_string()),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// 打印原始响应
|
||||
let raw_response = response.text().await?;
|
||||
|
||||
let json: serde_json::Value = serde_json::from_str(&raw_response)?;
|
||||
|
||||
// 获取总结果数并计算总页数
|
||||
let total_results = json["searchInformation"]["totalResults"]
|
||||
.as_str()
|
||||
.and_then(|s| s.parse::<u64>().ok()) // 使用u64处理大数字
|
||||
.unwrap_or(0);
|
||||
|
||||
let total_pages = (total_results as f64 / num_results as f64).ceil() as u32;
|
||||
|
||||
// 解析结果项
|
||||
let items = json["items"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.map(|item| SearchResult {
|
||||
title: item["title"].as_str().unwrap_or("").to_string(),
|
||||
link: item["link"].as_str().unwrap_or("").to_string(),
|
||||
snippet: item["snippet"].as_str().unwrap_or("").to_string(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
Ok((items, total_pages))
|
||||
}
|
||||
|
||||
// 处理搜索请求
|
||||
// 获取Google搜索建议
|
||||
async fn fetch_google_suggestions(query: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("https://www.google.com/complete/search?q={}&client=chrome", query);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header(header::USER_AGENT, "Mozilla/5.0")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let suggestions: Vec<String> = response
|
||||
.json::<Vec<serde_json::Value>>()
|
||||
.await?
|
||||
.get(1)
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
Ok(suggestions)
|
||||
}
|
||||
|
||||
// 处理补全建议请求
|
||||
#[get("/completion")]
|
||||
async fn completion(params: web::Query<SearchParams>) -> impl Responder {
|
||||
match fetch_google_suggestions(¶ms.q).await {
|
||||
Ok(suggestions) => {
|
||||
let response = CompletionResponse {
|
||||
success: true,
|
||||
message: "Suggestions fetched successfully".to_string(),
|
||||
suggestions,
|
||||
};
|
||||
HttpResponse::Ok().json(response)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = CompletionResponse {
|
||||
success: false,
|
||||
message: format!("Failed to fetch suggestions: {}", e),
|
||||
suggestions: Vec::new(),
|
||||
};
|
||||
HttpResponse::InternalServerError().json(error_response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/search")]
|
||||
async fn search(params: web::Query<SearchParams>) -> impl Responder {
|
||||
let current_page = params.page.unwrap_or(1);
|
||||
let start = (current_page - 1) * 10 + 1; // Google CSE的start参数从1开始
|
||||
|
||||
match fetch_google_search_results(¶ms.q, start).await {
|
||||
Ok((results, total_pages)) => {
|
||||
|
||||
let response = ApiResponse {
|
||||
success: true,
|
||||
message: "Search successful".to_string(),
|
||||
data: Some(results),
|
||||
current_page,
|
||||
total_pages,
|
||||
};
|
||||
HttpResponse::Ok().json(response)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = ApiResponse {
|
||||
success: false,
|
||||
message: format!("Search failed: {}", e),
|
||||
data: None,
|
||||
current_page: 0,
|
||||
total_pages: 0,
|
||||
};
|
||||
HttpResponse::InternalServerError().json(error_response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
println!("Server started at http://0.0.0.0:8080");
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.service(search)
|
||||
.service(completion)
|
||||
})
|
||||
.bind("0.0.0.0:8080")?
|
||||
.run()
|
||||
.await
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user