Перейти к содержанию

Создаем простой запрос в Rust

Posted on:20 апреля 2023 г. at 12:21

Example Dynamic OG Image link В этой статье я расскажу о том, как мы можем создавать запросы в Rust. Прежде чем мы начнем, пожалуйста, добавьте следующие зависимости в ваш файл cargo.toml.

[dependencies]
futures = "0.3.28"
tokio = {version = "1.27.0",  features = ["full"]}
reqwest = {version = "0.11.16", features = ["json"]}
serde = {version = "1.0.159", features = ["derive"]}
serde_json = "1.0.95"

Целью этого запроса является извлечение данных с веб-сайта asx с указанием названия компании. Более конкретно, мы будем использовать функцию прогнозирующего поиска ASX. Ниже будет указан URL-адрес нашего запроса.

https://asx.api.markitdigital.com/asx-research/1.0/search/predictive?searchText={search}

Вот пример того, как выполняется запрос API, в котором “woolworths” является параметром поиска.

Если мы выйдем в Network, мы сможем увидеть фактический запрос API, в то же время мы сможем узнать, какиме заголовки есть у нашего запроса.

Вот как выглядит запрос curl!

curl 'https://asx.api.markitdigital.com/asx-research/1.0/search/predictive?searchText=woolworths' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
  -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' \
  -H 'Cache-Control: max-age=0' \
  -H 'Connection: keep-alive' \
  -H 'Sec-Fetch-Dest: document' \
  -H 'Sec-Fetch-Mode: navigate' \
  -H 'Sec-Fetch-Site: none' \
  -H 'Sec-Fetch-User: ?1' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36' \
  -H 'sec-ch-ua: "Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --compressed

Теперь, когда у нас есть все детали, необходимые для оформления запроса, давайте посмотрим на запрос crate reqwest. Reqwest - интересный пакет, особенно в том, что касается реализации функции синхронным и асинхронным способом. Основным отличительным фактором с точки зрения семантики является включение функции blocking. В то же время, почему это называется блокировкой? Ну, поскольку синхронный код, по сути, блокирует!

Предположим, мы должны сделать запрос, но получение ответа занимает у нас 2 секунды, синхронный код будет означать, что мы застреваем на этой строке кода на 2 секунды, пока не получим ответ, тогда как в асинхронном коде мы могли бы делать что-то еще в ожидании ответа, например, рендеринг веб-сайт, если пользователь нажимает на что-то. Вы же не хотите, чтобы веб-сайт отображался после того, как ваш запрос получит ответ через 2 секунды, не так ли?

Хотя вы могли бы привести доводы в пользу синхронного запроса. Как сказано в reqwest crate, “Для приложений, желающих выполнить всего несколько HTTP-запросов, reqwest::blocking API может быть более удобным”. Однако, с точки зрения удобства, это, скорее всего, говорит о том, насколько проще настроить синхронный запрос, чем асинхронный, и поскольку выполняется всего несколько запросов, это не соответствует варианту использования для выполнения асинхронных запросов. Чтобы добавить к этому, существует нечто, называемое синхронной связью, некоторым системам, возможно, придется придерживаться этого принципа проектирования, где он должен обрабатывать 1 запрос и 1 ответ в точном порядке, однако при асинхронной связи может быть множество запросов, и каждый из ответов может возвращаться в случайном порядке.

Однако в сегодняшнем уроке мы будем использовать асинхронный фреймворк!

struct ASXHeaders {
    headers : HeaderMap,
}

impl ASXHeaders {
    fn new() -> Self {
        let mut h = ASXHeaders {headers : HeaderMap::new()};
        h.headers.insert("Accept", "application/json, text/plain, */*".parse().unwrap());
        h.headers.insert("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8".parse().unwrap());
        h.headers.insert("Authorization", "Bearer 83ff96335c2d45a094df02a206a39ff4".parse().unwrap());
        h.headers.insert("Connection", "keep-alive".parse().unwrap());
        h.headers.insert("Origin", "https://www2.asx.com.au".parse().unwrap());
        h.headers.insert("Referer", "https://www2.asx.com.au/".parse().unwrap());
        h.headers.insert("Sec-Fetch-Dest", "empty".parse().unwrap());
        h.headers.insert("Sec-Fetch-Mode", "cors".parse().unwrap());
        h.headers.insert("Sec-Fetch-Site", "cross-site".parse().unwrap());
        h.headers.insert("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36".parse().unwrap());
        h.headers.insert("sec-ch-ua", "\"Google Chrome\";v=\"111\", \"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"111\"".parse().unwrap());
        h.headers.insert("sec-ch-ua-mobile", "?0".parse().unwrap());
        h.headers.insert("sec-ch-ua-platform", "\"macOS\"".parse().unwrap());
        h
    }
}

// Давайте создадим асинхронную функцию!
async fn asxsearch(client : &Client, search : &str) -> Result<Response, reqwest::Error> {

    tokio::time::sleep(tokio::time::Duration::from_millis(5000)).await;
    let search = search.replace(" ", "+");
    let url = format!(
        "https://asx.api.markitdigital.com/asx-research/1.0/search/predictive?searchText={search}",
         search = search
    );
    // Создание заголовков с помощью HeaderMap
    let header_info = ASXHeaders::new();
    // Отправьте запрос с ожиданием, которое инициирует действие.
    let res = client.get(url)
        .headers(header_info.headers)
        .send().await?;
    Ok(res)
}

В приведенном выше коде, как вы можете видеть, мы включили заголовки из запроса curl, используя HeaderMap из библиотеки reqwest. Для каждого нового запроса мы создаем новый экземпляр этого объекта (на самом деле нам это не обязательно, скорее всего, есть какие-то другие обходные пути) и отправляем наш запрос с нашим отформатированным url. Обратите внимание, как вы можете видеть, я добавил функцию ожидания, так что весь этот запрос должен занять не менее 5000 миллисекунд.

Теперь мы создаем main функцию.

// Еще один способ создания Исполнителя! Наша основная функция ТАКЖЕ должна быть асинхронной!
#[tokio::main]
async fn main() {
    // Создадим клиент (неблокирующий)
    let client = reqwest::Client::new();
    let woolworth_req = asxsearch(&client, "Woolworths");
    let coles_req = asxsearch(&client, "Coles");
    use std::time::Instant;
    let now = Instant::now();
    let (woolworth_resp, coles_resp) = tokio::join!(woolworth_req, coles_req);
    let vec_resps = vec![woolworth_resp.unwrap(), coles_resp.unwrap()];
    for response in vec_resps {
        match response.status() {
            reqwest::StatusCode::OK => {
                println!("все в порядке");
                match response
                      .json::<serde_json::Value>()
                      .await
                {
                    Ok(parsed) =>
                    {
                        let other : SearchData = serde_json::from_value(parsed).unwrap();
                        println!("Успех! {:?}", other)
                    },
                    Err(_) => println!("Ответ не соответствовал той форме, которую мы ожидали."),
                };
            }
            reqwest::StatusCode::UNAUTHORIZED => {
                println!("Нужно получить новый токен");
            }
            other => {
                panic!("О, о! Произошло что-то неожиданное: {:?}", other);
            }
        };
    }
    let elapsed = now.elapsed();
    println!("Прошло времени: {:.2?}", elapsed);
}

Первое, что может броситься вам в глаза, это то, что основная функция является асинхронной. Причина, по которой у нас это, кроется в макросе прямо над которым находится. #[tokio::main]. Взятый из документации reqwest, этот макрос, по сути, “помечает асинхронную функцию, которая должна выполняться выбранной средой выполнения. Этот макрос помогает настроить среду выполнения, не требуя от пользователя непосредственного использования среды выполнения или постороения.” В этом случае асинхронная функция ЯВЛЯЕТСЯ основной функцией, которая отличается от настройки в предыдущем примере.

let woolworth_req = asxsearch(&client, "Woolworths");
let coles_req = asxsearch(&client, "Coles");
//use std::time::Instant;
//let now = Instant::now();
let (woolworth_resp, coles_resp) = tokio::join!(woolworth_req, coles_req);

Первые две строки здесь создают 2 фьючерса. tokio::join! затем берет эти фьючерсы и выполняет их асинхронным образом. Таким образом, общее время, затраченное на эти 2 запроса, несмотря на то, что для возврата ответа обоим требуется не менее 5000 миллисекунд, составит … около 6250 миллисекунд. Вот вывод результатов:

it was ok
Success! SearchData { data: JSONResponse { items: [Data { display_name: "WOOLWORTHS GROUP LIMITED", industry_group: "Consumer Staples Distribution & Retail", issue_type: "CS", market_cap: 47614672645, price_change_one_week_percent: 1.1017166282346904, price_last: 39.46, security_type: 1, symbol: "WOW", xid: "287002", xid_entity: 204119616 }], spark_chart_base_url: "https://api.markitondemand.com/apiman-gateway/MOD/chartworks-image/1.0/Chart/sparkLine" } }
it was ok
Success! SearchData { data: JSONResponse { items: [Data { display_name: "COLES GROUP LIMITED.", industry_group: "Consumer Staples Distribution & Retail", issue_type: "CS", market_cap: 24291806342, price_change_one_week_percent: -1.0342950462711011, price_last: 18.18, security_type: 1, symbol: "COL", xid: "502428539", xid_entity: 204225017 }], spark_chart_base_url: "https://api.markitondemand.com/apiman-gateway/MOD/chartworks-image/1.0/Chart/sparkLine" } }
Time Elapsed: 6.25s

И, таким образом, на этом мой урок заканчивается!

Просто на случай, если всем интересно, как я все десериализовал:

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Data {
    display_name : String,
    industry_group : String,
    issue_type : String,
    market_cap : u64,
    price_change_one_week_percent : f64,
    price_last : f32,
    security_type : i32,
    symbol : String,
    xid : String,
    xid_entity : u32
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct JSONResponse {
    items : Vec<Data>,
    spark_chart_base_url : String
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct SearchData {
    data : JSONResponse
}