转换简单的Python请求将POST转换为Rust reqwest

问题描述

我正在尝试在正在编写的Rust程序中使用this Python script(取自here)的一部分。如何构造具有相同内容的reqwest请求?


def login(login_url,username,password=None,token=None):
    """Log in to kattis.

    At least one of password or token needs to be provided.

    Returns a requests.Response with cookies needed to be able to submit
    """
    login_args = {'user': username,'script': 'true'}
    if password:
        login_args['password'] = password
    if token:
        login_args['token'] = token

    response = requests.post(login_url,data=login_args,headers=_HEADERS)
    return response


def submit(submit_url,cookies,problem,language,files,mainclass='',tag=''):
    """Make a submission.

    The url_opener argument is an OpenerDirector object to use (as
    returned by the login() function)

    Returns the requests.Result from the submission
    """

    data = {'submit': 'true','submit_ctr': 2,'language': language,'mainclass': mainclass,'problem': problem,'tag': tag,'script': 'true'}

    sub_files = []
    for f in files:
        with open(f) as sub_file:
            sub_files.append(('sub_file[]',(os.path.basename(f),sub_file.read(),'application/octet-stream')))

    return requests.post(submit_url,data=data,files=sub_files,cookies=cookies,headers=_HEADERS)

(请查看上面的链接获取其余代码

目前,我已经知道了(不确定cookie是否得到处理)

    let config = get_config().await?;
    let mut default_headers = header::HeaderMap::new();
    default_headers.insert(
        header::USER_AGENT,header::HeaderValue::from_static("kattis-cli-submit"),);
    let client = reqwest::ClientBuilder::new()
        .default_headers(default_headers)
        .cookie_store(true)
        .build()?;

    // Login
    let login_map = serde_json::json!({
        "user": config.username.as_str(),"script": "true","token": config.token.as_str(),});

    let login_response = client
        .post(&config.login_url)
        .header("Content-Type","application/x-www-form-urlencoded")
        .json(&login_map)
        .send()
        .await?;
    println!("{:?}",login_response);

    // Make a submission
    let submission_map = serde_json::json!({
        "submit": "true","submit_ctr": "2","language": language,"mainclass": problem,"problem": problem,});

    println!("{}",&submission_map);

    let mut form = multipart::Form::new();

    let mut sub_file = multipart::Part::text(submission).file_name(submission_filename);
    sub_file = sub_file.mime_str("application/octet-stream").unwrap();
    form = form.part("sub_file[]",sub_file);
    let submission_response = client
        .post(&config.submit_url)
        .json(&submission_map)
        .multipart(form)
        // .build();
        .send()
        .await?
        .text()
        .await?;
    let config = get_config().await?;
    let mut default_headers = header::HeaderMap::new();
    default_headers.insert(
        header::USER_AGENT,login_response);


    // Make a submission
    let submission_map = serde_json::json!({
        "submit": "true",&submission_map);


    let mut form = multipart::Form::new();

    let mut sub_file = multipart::Part::text(submission).file_name(submission_filename);
    sub_file = sub_file.mime_str("application/octet-stream").unwrap();
    form = form.part("sub_file[]",sub_file);
    let submission_response = client
        .post(&config.submit_url)
        .json(&submission_map)
        .multipart(form)
        // .build();
        .send()
        .await?
        .text()
        .await?;

    println!("Submission response:\n{:?}",submission_response);

有哪些参考资料

{"user": {"username": Some("[username]"),"token": Some("[token]")},"kattis": {"loginurl": Some("https://open.kattis.com/login"),"hostname": Some("open.kattis.com"),"submissionurl": Some("https://open.kattis.com/submit"),"submissionsurl": Some("https://open.kattis.com/submissions")}}
Response { url: "https://open.kattis.com/login",status: 200,headers: {"date": "Sun,13 Sep 2020 14:19:15 GMT","content-type": "text/html; charset=UTF-8","transfer-encoding": "chunked","connection": "keep-alive","set-cookie": "__cfduid=d0417cc7406c8d91b8659327fff8d5d9a1600006752; expires=Tue,13-Oct-20 14:19:12 GMT; path=/; domain=.kattis.com; HttpOnly; SameSite=Lax","set-cookie": "EduSiteCookie=75f873b9-5442-45be-b442-be08f349e09c; path=/; domain=.kattis.com; secure; HttpOnly","expires": "Thu,19 Nov 1981 08:52:00 GMT","cache-control": "no-store,no-cache,must-revalidate","pragma": "no-cache","cf-cache-status": "DYNAMIC","cf-request-id": "05296ea065000015fc7ca80200000001","expect-ct": "max-age=604800,report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\"","server": "cloudflare","cf-ray": "5d22807a39b015fc-ARN","alt-svc": "h3-27=\":443\"; ma=86400,h3-28=\":443\"; ma=86400,h3-29=\":443\"; ma=86400"} }
{"language":"C++","mainclass":"ants","problem":"ants","script":"true","submit":"true","submit_ctr":"2"}
Submission response:
"<!DOCTYPE html>\n\n\n<html lang=\"en\">\n<head>\n    <Meta charset=\"UTF-8\" >\n    <Meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <Meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n    <title>Log in or sign up for kattis &ndash; kattis,kattis</title>\n\n    <link href=\"//ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.min.css\" rel=\"stylesheet\">\n\n    <script src=\"//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js\"></script>\n    <script src=\"//ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js\"></script>\n\n    <!-- Fonts/Icons -->\n    <link href=\"//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css\" rel=\"stylesheet\">\n\n    <link href=\"//fonts.googleapis.com/css?family=Open+Sans:400,300,300italic,400italic,600,600italic,700,800,700italic,800italic%7cmerriweather:400,700\" rel=\"stylesheet\" type=\"text/css\">\n\n    <!-- Bootstrap CSS -->\n    <link href=\"//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css\" rel=\"stylesheet\">\n\n    <!-- Bootstrap datetimepicker css-->\n    <link href=\"//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/css/bootstrap-datetimepicker.min.css\" rel=\"stylesheet\">\n\n    <!-- DaterangePicker CSS -->\n    <link href=\"//cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css\" rel=\"stylesheet\">\n\n    <!-- Editable and Select2 -->\n    <link href=\"//cdnjs.cloudflare.com/ajax/libs/select2/3.5.4/select2.css\" rel=\"stylesheet\">\n\n    <link rel=\"shortcut icon\" href=\"/favicon\" />\n\n    <!-- Own CSS -->\n    <link rel=\"stylesheet\" href=\"/css/system.css?03bf93=\">\n    <style type=\"text/css\">\n          .header {\n         background-color: rgb(240,176,52);\n     }\n     .header .main-nav > ul > li.current:before {\n         border-bottom-color: rgb(240,52);\n     }\n\n          div.page-content.clearfix.above-everything.alert.alert-danger { color: #31708f; background: #d9edf7; border-color: #bce8f1; }\r\ndiv.page-content.clearfix.above-everything.alert.alert-danger div.main-content { padding-bottom: 0; }\r\n\n         </style>\n\n    <script type=\"text/javascript\">\n        window.page_loaded_at = new Date();\n        jQuery.noConflict();\n    </script>\n\n    <script type=\"text/javascript\">\n    jQuery.ns = function (namespace) {\n        var parts = namespace.split(\'.\');\n        var last = window;\n        for (var i = 0; i < parts.length; i++) {\n            last = last[parts[i]] || (last[parts[i]] = {});\n        }\n        return last;\n    };\n</script>\n    <script>\njQuery.extend(jQuery.ns(\'kattis.error\'),(function () {\n    var messages = {\"INTERNAL_SERVER_ERROR\":\"Internal server error.\",\"ACCESS_DENIED\":\"Access denied.\",\"NOT_AUTHENTICATED\":\"Not authenticated.\",\"METHOD_NOT_ALLOWED\":\"Method not allowed.\",\"INVALID_JSON\":\"JSON cannot be decoded or encoded data is deeper than the recursion limit.\",\"BAD_CSRF_TOKEN\":\"Token does not match session\'s csrf_token\",\"SESSION_NAME_EMPTY\":\"Session\'s name must be non empty.\",\"SESSION_START_TIME_EMPTY\":\"Session\'s start time must be non empty.\",\"SESSION_START_TIME_PASSED\":\"Session\'s start time has already passed.\",\"SESSION_DURATION_EMPTY\":\"Session\'s duration must be non empty.\",\"SESSION_DURATION_NEGATIVE\":\"Session\'s duration must be a positive number.\",\"SESSION_DURATION_EXCEEDED\":\"Maximum duration for the session was exceeded.\",\"SESSION_ALREADY_STARTED\":\"The session has already started.\",\"SESSION_ALREADY_FINISHED\":\"The session is already finished.\",\"USER_CREATED_SESSION_DURATION_EXCEEDED\":\"Contest cannot be longer than 168 hours.\",\"INVALID_PROBLEM_score\":\"Invalid problem score.\",\"INVALID_SESSION_SHORTNAME\":\"Invalid shortname for the session.\",\"INVALID_SESSION_CUTOFF\":\"Invalid cutoff for the session.\",\"INVALID_USER_NAME\":\"Invalid username or email.\",\"SESSION_NOT_FOUND\":\"No such session.\",\"COURSE_NOT_FOUND\":\"No such course.\",\"OFFERING_NOT_FOUND\":\"No such offering.\",\"TEACHER_NOT_FOUND\":\"No such teacher.\",\"TEACHER_CANNOT_REMOVE_SELF\":\"You may not remove yourself as a teacher unless you are an administrator.\",\"AUTHOR_NOT_FOUND\":\"No such author.\",\"JUDGE_NOT_FOUND\":\"No such judge.\",\"JUDGE_ALREADY_EXIST\":\"The user is already a judge.\",\"TEACHER_ALREADY_EXIST\":\"The user is already a teacher.\",\"PROBLEM_NOT_FOUND\":\"No such problem.\",\"TEAM_NOT_FOUND\":\"No such team.\",\"SESSION_PROBLEM_ALREADY_EXIST\":\"The problem has been already added to the session.\",\"SESSION_PROBLEM_DOES_NOT_EXIST\":\"The problem does not relate to the session.\",\"PROBLEM_INDEX_NEGATIVE\":\"Problem index must be non negative.\",\"AUTHOR_IS_CURRENT_TEAM_MEMBER\":\"The user you tried to add is already a member of the current team.\",\"AUTHOR_IS_ANOTHER_TEAM_MEMBER\":\"The user you tried to add is already a member of another team in the current session.\",\"AUTHOR_IS_JUDGE\":\"The user you tried to add is a judge.\",\"AUTHOR_IS_NOT_TEAM_MEMBER\":\"The user you tried to remove is not a team member.\",\"JUDGE_IS_TEAM_MEMBER\":\"The user you tried to add is a session team member or invitee.\",\"SESSION_PUBLISHING_DENIED\":\"You do not have permission to publish this session.\",\"CANNOT_PUBLISH_HISTORICAL_SESSION\":\"You cannot publish a session with a historical start time.\",\"INVALID_TEAM_NAME_TOO_LONG\":\"The team name you are trying to add is too long\",\"TEAM_NAME_IS_NOT_VISIBLE\":\"The team name you are trying to add is not visible\"};\n\n    return {\n        get_msg: function (error_code) {\n            return messages[error_code];\n        },\n\n        show_msg: function (base_message,error_code) {\n            if (error_code) {\n                alert(base_message + \": \" + this.get_msg(error_code));\n            } else {\n                alert(base_message);\n            }\n        },\n\n        show_xhr_msg: function (elem,jqXHR) {\n            var base_message = elem.data(\'fail-msg\');\n            var code = jqXHR.responseJSON && jqXHR.responseJSON.error &&\n                       jqXHR.responseJSON.error.code;\n            this.show_msg(base_message,code);\n        }\n    }\n})());\n</script>\n\n    \n\n    <script type=\"text/javascript\">\nvar rumMOKey=\"a854f3a6dd7ee5e3b7d1641570b79c34\";\n(function(){\nif(window.performance && window.performance.timing && window.performance.navigation) {\n\tvar site24x7_rum_beacon=document.createElement(\'script\');\n\tsite24x7_rum_beacon.async=true;\n\tsite24x7_rum_beacon.setAttribute(\'src\',\'//static.site24x7rum.eu/beacon/site24x7rum-min.js?appKey=\'+rumMOKey);\n\tdocument.getElementsByTagName(\'head\')[0].appendChild(site24x7_rum_beacon);\n}\n})(window)\n</script>\n\n    \n</head>\n\n<body class=\"page-master-layout \">\n\n\n<div id=\"wrapper\">\n    <header class=\"header\">\n    <div class=\"background\">\n        \n        <div class=\"wrap\">\n            <div class=\"fl\">\n                                    <a href=\"/\"><img class=\"logo logo-open\" src=\"/images/site-logo\" alt=\"\" /></a>\n                                <div class=\"title-wrapper\">\n                    <div class=\"header-title\">kattis</div>\n                    <nav class=\"main-nav\">\n                        <ul>\n                                                                                            \n                                <li class=\"\"><a href=\"/problems\">Problems</a></li>\n                                                                                            \n                                <li class=\"\"><a href=\"/contests\">Contests</a></li>\n                                                                                            \n                                <li class=\"\"><a href=\"/ranklist\">Ranklists</a></li>\n                                                                                            \n                                <li class=\"\"><a href=\"/jobs\">Jobs</a></li>\n                                                                                            \n                                <li class=\"\"><a href=\"/help\">Help</a></li>\n                            \n                                                    </ul>\n                    </nav>\n                </div>\n            </div>\n            <div class=\"user-side fr\">\n\n                <nav class=\"user-nav\">\n                    <ul class=\"user-nav-ul\">\n                                                    <li>\n                                <form action=\"/search\" class=\"site-search\" method=\"GET\">\n                                    <input type=\"text\" name=\"q\" placeholder=\"Search kattis\" />\n                                    <a href=\"#\">\n                                        <i class=\"fa fa-search\"></i>\n                                    </a>\n                                </form>\n                            </li>\n                        \n                                                                                    <li><a class=\"btn dark-bg\" href=\"/login\">Log in</a></li>\n                                                                        </ul>\n\n                </nav>\n\n            </div>\n        </div>\n    </div>\n</header>\n\n    <!--[if IE]>    <div class=\"alert alert-warning\" role=\"alert\">\n        <strong>You are using an outdated browser!</strong> Some features might not look or work like expected. kattis supports the last two versions of major browsers. Please consider upgrading to a recent version!    </div>\n    <![endif]-->\n\n    \n    \n            <div class=\"wrap\">\n            <div id=\"messages\">\n                \n                                                                            <div class=\"alert alert-dismissible alert-info\">\n                        <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\">\n                            <span aria-hidden=\"true\">&times;</span>\n                        </button>\n                        <strong>The page you are trying to access requires you to be logged in.</strong>\n                    </div>\n                            </div>\n        </div>\n    \n    \n    \n\n    <div class=\"wrap\">\n        \n\n\n\n\n\n\n\n\n\n        \n                    \n\n        <div class=\"page-content Boxed clearfix\">\n            <section class=\"Box clearfix main-content\">\n                \n                \n\t\n    <div class=\"page-headline clearfix\">\n        <div style=\"text-align:center\">\n            <h1>Log in or sign up for kattis</h1>\n        </div>\n    </div>\n\n    <br />\n\n    <div class=\"login\">\n    <div class=\"login-left\">\n    <img src=\"/images/kattis/judge.png?7f7dbf=\" alt=\"\" />\n    </div>\n\n    <div class=\"login-right\">\n\n\t\n    <div class=\"login-methods\">\n\n        \t\t                    \n                <form action=\"/oauth/Azure\" method=\"GET\" style=\"display:inline-block\">\n                    <button class=\"Azure\">\n\n                                                    <i class=\"fa fa-windows\"></i>\n                        \n                        Log in with Azure\n                    </button>\n                </form>\n\n\t\t\t\t\t\t\t\t<br/>                                \n                <form action=\"/oauth/Facebook\" method=\"GET\" style=\"display:inline-block\">\n                    <button class=\"Facebook\">\n\n                                                    <i class=\"fa fa-facebook\"></i>\n                        \n                        Log in with Facebook\n                    </button>\n                </form>\n\n\t\t\t\t\t\t\t\t<br/>                                \n                <form action=\"/oauth/Github\" method=\"GET\" style=\"display:inline-block\">\n                    <button class=\"Github\">\n\n                                                    <i class=\"fa fa-github\"></i>\n                        \n                        Log in with Github\n                    </button>\n                </form>\n\n\t\t\t\t\t\t\t\t<br/>                                \n                <form action=\"/oauth/Google\" method=\"GET\" style=\"display:inline-block\">\n                    <button class=\"Google\">\n\n                                                    <i class=\"fa fa-google\"></i>\n                        \n                        Log in with Google\n                    </button>\n                </form>\n\n\t\t\t\t\t\t\t\t<br/>                                \n                <form action=\"/oauth/LinkedIn\" method=\"GET\" style=\"display:inline-block\">\n                    <button class=\"LinkedIn\">\n\n                                                    <i class=\"fa fa-linkedin\"></i>\n                        \n                        Log in with LinkedIn\n                    </button>\n                </form>\n\n\t\t\t\t\t\t\t\t<br/>                    \n\t\t\n\t\t\n                    <form action=\"/login/email\" method=\"GET\" style=\"display:inline-block\">\n                <button class=\"email\">\n                    <i class=\"fa fa-envelope\"></i>\n                    Log in with e-mail                </button>\n\n                                    <input type=\"hidden\" name=\"todo\" value=\"redirect\" />\n                            </form>\n        \n    </div>\n\n\t<br/>\n\t<br/><a href=\"/login/more?todo=redirect\">More login methods</a>\t\n    </div></div>\n\n\n            </section>\n        </div>\n    </div>\n\n\n</div>\n\n\n<div id=\"footer\">\n    <div class=\"container\">\n        <div class=\"row\">\n            <div class=\"footer-info col-md-2 \">\n                \n                            </div>\n            <div class=\"footer-powered col-md-8\">\n                <h4>\n                                      <a href=\"/RSS/new-problems\"><i class=\"fa fa-RSS-square\" style=\"color: orange\"></i>&nbsp;RSS Feed for new problems</a> |\n                                    Powered by&nbsp;kattis                                      | <a href=\"https://www.patreon.com/kattis\">Support kattis on Patreon!</a>\n                                  </h4>\n            </div>\n        </div>\n    </div>\n</div>\n\n\n\n\n<script src=\"//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js\"></script>\n<script src=\"//cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js\"></script>\n<script src=\"//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/js/bootstrap-datetimepicker.min.js\"></script>\n<script src=\"//cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js\"></script>\n<script src=\"//cdnjs.cloudflare.com/ajax/libs/select2/3.5.4/select2.min.js\"></script>\n<script src=\"//cdnjs.cloudflare.com/ajax/libs/raphael/2.2.8/raphael.min.js\"></script>\n<script src=\"/js/system.js?203d73=\" type=\"text/javascript\"></script>\n\n\n\n\n</body>\n</html>\n"

POST请求中存在一些差异,但是我无法确切知道是什么。我还认为我可以通过第一个请求登录,但是我不确定完全保留cookie。有没有一种通用的方法可以在Rust中重写Python请求POST?具体来说,我认为我需要包含文件部分。

解决方法

您没有使用它,但是使用requests时,您可以使用 session 对象来处理cookie持久性。您已经在reqwest中找到了等效项; ClientBuilder有一个cookie store method,可启用相同的功能。使用为此配置的构建器创建两个请求,然后将一个响应上的所有cookie传递到下一个请求(遵循cookie域,路径和标志的常规规则)。

接下来,将requests.post()方法组合字段传递给filesdata到单个多部分表单请求主体中。这不会发布JSON数据,请不要在此处使用RequestBuilder.json()方法。只需使用Form.text() method将这些字段作为文本字段添加到multipart请求中即可。

您的登录功能也未发送JSON;传递给data的字典将作为表单字段处理。

这应该可行:

use std::path::Path;
use tokio::fs::File;

// UA string to pass to ClientBuilder.user_agent
let &'static user_agent = "kattis-cli-submit";

let config = get_config().await?;
let client = reqwest::ClientBuilder::new()
    .user_agent(user_agent)
    .cookie_store(true)
    .build()?;

// Login
// could also use a HashMap
let login_fields = [
    ("user",config.username.as_str()),("script","true"),("token",config.token.as_str()),];

let login_response = client
    .post(&config.login_url)
    .form(&login_fields)
    .send()
    .await?;

println!("{}",login_response);

// Make a submission

let mut form = reqwest::multipart::Form::new()
    .text("submit","true")
    .text("submit_ctr","2")
    .text("language",language)
    .text("mainclass",problem)
    .text("problem",problem)
    .text("script","true");

// add a single file,and set the part filename to the base name of the file path
let path = Path::new(submission_filename);
let sub_file_contents = std::fs::read(path)?;
let sub_file_part = reqwest::multipart::Part::bytes(sub_file_contents)
    .file_name(path.file_name().unwrap().to_string_lossy())
    .mime_str("application/octet-stream")?;

form = form.part("sub_file[]",sub_file_part);

let submission_response = client
    .post(&config.submit_url)
    .multipart(form)
    .send()
    .await?
    .text()
    .await?;

println!("Submission response:\n{}",submission_response);

我使用ClientBuilder.user_agent() method来设置User-Agent字符串,而不是手动构建标题映射。

请注意,该代码发布了一个文件,然后将文件内容首先读取到内存中。 multipart::Part::bytes()方法产生一个新的Part,然后通过附加文件名和mimetype对其进行进一步配置。

我可以衷心建议您尝试发布到https://httpbin.org/post,以查看您的代码最终发送的是什么,并将其与Python版本进行比较。

我已经创建了使用httpbin的代码的repl.it演示(进行了一些调整,可以在没有配置对象的情况下使用,并且代码设置了cookie,以便我们可以验证它是否正在传播,上传多个文件,并为附件设置唯一的零件名称,以便httpbin正确显示它们):

您可以在那里看到httpbin的响应是相同的。

Python代码将每个文件读入内存以将其发布;这样效率不高,并且限制了可以使用此代码发送的文件大小。对于此脚本来说可能很好,但是对于较大的文件,您希望在发送表单数据时将文件数据直接从磁盘流式传输到网络套接字:

use std::path::Path;
use tokio::fs::File;
use tokio_util::codec::{BytesCodec,FramedRead};

let path = Path::new(submission_filename);
// Create a Stream for the attached file,wrapped in a reqwest::Body
let file = File::open(path).await?;
let reader = FramedRead::new(file,BytesCodec::new());
let sub_file_part = reqwest::multipart::Part::stream(Body::wrap_stream(reader))
    .file_name(path.file_name().unwrap().to_string_lossy())
    .mime_str("application/octet-stream")?;

form = form.part(part_name,sub_file_part);

您可以在https://repl.it/@mjpieters/so63873082-rust-streams#so63873082/src/main.rs上看到这一点