ProxynetProxynet

ارسال JSON با POST در Python Requests: معادل‌های cURL

تاریخ انتشار:

17 دقیقه مطالعه

Acar Diveroli
نویسنده: Acar Diveroli
جعبه‌های گزینه cURL روی نوار وارد دستگاه می‌شوند؛ از راست ⁦headers=⁩، ⁦json=⁩ و ⁦files=⁩ بیرون می‌آیند و ⁦json=⁩ آبی است

مستندات API یک شرکت حمل مرسوله، ساختن مرسوله تازه را با یک فرمان cURL نشان می‌دهد: curl -X POST https://api.example.com/v1/shipments -H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" -d '{"recipient": "Ayse Demir", "weight_kg": 3}'. در ترمینال پاسخ 201 Created برمی‌گردد. فرمان را به شکل requests.post(url, data=json.dumps(body), headers={"Authorization": ...}) به Python منتقل می‌کنید و سرور پاسخ 415 Unsupported Media Type می‌دهد: وقتی در data= یک رشته باشد، Requests هیچ Content-Type نمی‌فرستد، و سطری که این هدر را تنظیم می‌کرد در فرمان cURL جا مانده است.

این راهنما json=، data= و files=، پارامترهای پرس‌وجو، هدرها و توکن‌های Bearer، خواندن پاسخ و آرگومان Requests متناظر با هر گزینه رایج cURL را پوشش می‌دهد. در پایان یک فهرست بررسی برای درخواست‌هایی آمده که در cURL کار می‌کنند اما در Python نه، و یک کلاینت کوچک API که با احتیاط دوباره تلاش می‌کند. همه نمونه‌ها روی ⁦Python 3.13.9⁩، ⁦Requests 2.34.2⁩ و ⁦curl 8.21.0⁩ (⁦Windows 11⁩)، در برابر httpbin.org و یک سرور محلی که بایت‌های دریافتی را عیناً برمی‌گرداند، اجرا شدند.

چگونه با Python Requests درخواست POST بفرستیم؟

بسته را با pip install requests نصب کنید. PyPI نسخه 2.34.2 را، که در 14 مه 2026 منتشر شد، نسخه فعلی نشان می‌دهد؛ این نسخه به ⁦Python 3.10⁩ یا جدیدتر نیاز دارد. انتخاب میان کتابخانه‌ها را در مقایسه HTTPX، Requests و AIOHTTP بررسی کرده‌ایم.

یک POST با بدنه JSON فقط یک فراخوانی است. httpbin.org/post هر چه را دریافت کرده برمی‌گرداند:

python
import requests

payload = {"recipient": "Ayse Demir", "weight_kg": 3}
r = requests.post("https://httpbin.org/post", json=payload, timeout=20)

print(r.status_code)                         # 200
print(r.json()["headers"]["Content-Type"])   # application/json
print(r.json()["data"])                      # {"recipient": "Ayse Demir", "weight_kg": 3}
print(r.json()["json"])                      # the same body, parsed back into a dict

فیلد data بدنه را به همان شکلی نشان می‌دهد که فرستاده شده، با یک فاصله پس از هر دونقطه و ویرگول. requests.put()، requests.patch() و requests.delete() همین آرگومان‌ها را می‌پذیرند. همیشه timeout بدهید: Requests مقدار پیش‌فرض ندارد، پس سروری که پاسخ نمی‌دهد اسکریپت شما را منتظر نگه می‌دارد (خطاهای timeout در خطای Max Retries Exceeded With URL آمده‌اند).

تفاوت json=، data= و files= چیست؟

هر آرگومان بدنه را به شیوه‌ای دیگر می‌سازد و Content-Type دیگری تنظیم می‌کند. یک دیکشنری را به چهار شکل فرستادیم:

python
import json
import requests

url = "https://httpbin.org/post"
body = {"recipient": "Ayse Demir", "weight_kg": 3}

for label, kwargs in [
    ("json=body", {"json": body}),
    ("data=body", {"data": body}),
    ("data=json.dumps(body)", {"data": json.dumps(body)}),
    ("json= and data=", {"json": body, "data": {"note": "x"}}),
]:
    echo = requests.post(url, timeout=20, **kwargs).json()
    print(f"{label:22} {echo['headers'].get('Content-Type')!s:34} form={echo['form']} json={echo['json']}")
text
json=body              application/json                   form={} json={'recipient': 'Ayse Demir', 'weight_kg': 3}
data=body              application/x-www-form-urlencoded  form={'recipient': 'Ayse Demir', 'weight_kg': '3'} json=None
data=json.dumps(body)  None                               form={} json={'recipient': 'Ayse Demir', 'weight_kg': 3}
json= and data=        application/x-www-form-urlencoded  form={'note': 'x'} json=None

آنچه این چهار سطر نشان می‌دهند:

  • json= دیکشنری را به JSON تبدیل می‌کند و Content-Type: application/json را تنظیم می‌کند. برای APIهای JSON از همین استفاده کنید.
  • data= با دیکشنری یک فرم می‌فرستد و هر مقدار به متن تبدیل می‌شود: weight_kg به شکل '3' رسید.
  • data= با رشته هیچ Content-Type نمی‌فرستد. httpbin با این حال آن را تجزیه کرد؛ یک API سخت‌گیر پاسخ 415 یا 400 می‌دهد. هدر را خودتان تنظیم کنید.
  • json= همراه با data= یا files= بی‌هیچ خطایی JSON را از دست می‌دهد. راهنمای شروع سریع Requests می‌گوید اگر data یا files داده شود، پارامتر json نادیده گرفته می‌شود.

متن JSON را فقط وقتی از راه data= بفرستید که بایت‌های دقیق مهم باشند. وقتی API بدنه را با HMAC امضا می‌کند، بایت‌هایی را که دریافت کرده بررسی می‌کند، و json= فاصله می‌افزاید و نویسه‌های غیر ASCII را به شکل دنباله \u می‌نویسد ("İzmir" به شکل "\u0130zmir" فرستاده می‌شود). بایت‌ها را خودتان بسازید:

python
import json
import requests

body = {"recipient": "Ayse Demir", "city": "İzmir", "weight_kg": 3}
raw = json.dumps(body, separators=(",", ":"), ensure_ascii=False).encode("utf-8")

r = requests.post("https://httpbin.org/post", data=raw,
                  headers={"Content-Type": "application/json"}, timeout=20)
print(r.json()["data"])  # {"recipient":"Ayse Demir","city":"İzmir","weight_kg":3}

هش ⁦SHA-256⁩ این بایت‌ها با هش بدنه‌ای که curl --data-binary @body.json از همان فایل ⁦UTF-8⁩ فرستاد یکی بود. ورود با فرم و توکن‌های CSRF در نشست و کوکی در Python آمده و files= در ادامه همین نوشته.

یک فرمان cURL را گام‌به‌گام چگونه به Requests تبدیل کنیم؟

همین درخواست، وقتی در پنل وب شرکت حمل مرسوله از پنل Network مرورگر کپی شود، چند سطر اضافه دارد. یافتن چنین درخواستی را در صفحه‌های ایستا و پویا در وب اسکرپینگ توضیح داده‌ایم.

bash
curl -X POST "https://api.example.com/v1/shipments?notify=false" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept-Encoding: gzip, deflate, br" \
  -H "Cookie: session=abc123" \
  --data-raw '{"recipient": "Ayse Demir", "weight_kg": 3}'
  1. نحو شل را کنار بگذارید. شکست‌های سطر با \ (یا ^ در کپی‌ای که برای cmd ویندوز ساخته شده) و نقل‌قول‌ها را بردارید.
  2. رشته پرس‌وجو را به params= ببرید. ?notify=false به params={"notify": "false"} تبدیل می‌شود.
  3. هدرها را پالایش کنید. آنچه را API لازم دارد نگه دارید: Authorization، Accept و کلیدهای API. Accept-Encoding را که Requests خودش مدیریت می‌کند حذف کنید، و Content-Type را هم وقتی از json= استفاده می‌کنید. Host یا Content-Length را هرگز کپی نکنید؛ Requests آن‌ها را خودش محاسبه می‌کند. سطرهای مرورگر مانند sec-fetch-* به‌ندرت لازم‌اند، و جای سطر Cookie در cookies= یا یک Session است.
  4. آرگومان بدنه را انتخاب کنید. JSON به json= می‌رود (یا اگر بدنه امضا می‌شود، data= با بایت‌های دقیق)، -d "a=1&b=2" به data={"a": "1", "b": "2"} و -F به files=.
  5. متد را انتخاب کنید. -d، --data-raw، --json و -F یعنی POST، مگر آنکه -X چیز دیگری بگوید؛ -G درخواست را به یک پرس‌وجوی GET تبدیل می‌کند.
  6. یک timeout= بیفزایید و r.raise_for_status() را فراخوانی کنید.
  7. مقایسه کنید. فرمان cURL و فراخوانی Python را به https://httpbin.org/anything بفرستید و متد، نشانی، هدرها و بدنه‌ای را که برمی‌گردد با هم مقایسه کنید.

نتیجه، با توکنی که از یک متغیر محیطی خوانده می‌شود:

python
import os
import requests

r = requests.post(
    "https://api.example.com/v1/shipments",
    params={"notify": "false"},
    headers={"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
    json={"recipient": "Ayse Demir", "weight_kg": 3},
    timeout=20,
)
r.raise_for_status()
print(r.status_code, r.headers.get("Location"))

curlconverter گام‌های 1 تا 5 را انجام می‌دهد و خروجی پیش‌فرضش Python Requests است. آن را با npm نصب کنید و در فرمان، curl را با curlconverter جایگزین کنید. برای فرمان بالا، نسخه 4.12.0 مقدار notify را به params، کوکی را به cookies= و بدنه را به json= برد، و Content-Type و Accept-Encoding را به‌صورت کامنت درآورد. هیچ timeout نیفزود، و README آن هشدار می‌دهد که کد تولیدشده هدایت‌ها را دنبال می‌کند، مگر آنکه فرمان سیاست هدایت را تعیین کرده باشد. فرمانی که از مرورگر کپی شده کوکی نشست فعال و توکن شما را در خود دارد، پس آن را روی دستگاه خودتان تبدیل کنید، نه در یک وب‌سایت.

کدام آرگومان Requests با هر گزینه cURL متناظر است؟

گزینه cURLRequestsتفاوت
-d '{"a":1}' با Content-Type: application/jsonjson={"a": 1}Requests فاصله می‌افزاید؛ برای بایت‌های دقیق از data= استفاده کنید
--json '{"a":1}'json={"a": 1}، headers={"Accept": "application/json"}--json (از ⁦curl 7.82.0⁩ به بعد) Accept را هم تنظیم می‌کند
-d "a=1&b=2"data={"a": "1", "b": "2"}بایت‌های یکسان
--data-binary @body.jsondata=open("body.json", "rb")-d @file شکست‌های سطر را حذف می‌کند؛ این دو آن‌ها را نگه می‌دارند
-F "file=@report.csv"files={"file": open("report.csv", "rb")}curl نوع بخش را application/octet-stream می‌گذارد؛ Requests فقط با تاپل سه‌تایی نوع می‌افزاید
-G --data-urlencode "q=kargo takip"params={"q": "kargo takip"}پرس‌وجوی یکسان: ?q=kargo+takip
-X PUTrequests.put(url, ...)برای PATCH و DELETE هم همین‌طور
-H "Name: value"headers={"Name": "value"}مقدارها باید رشته باشند؛ int خطای InvalidHeader می‌دهد
-A "ShipmentSync/1.0"headers={"User-Agent": "ShipmentSync/1.0"}در غیر این صورت هر ابزار نام خودش را می‌فرستد
-b "session=abc123"cookies={"session": "abc123"}همان هدر Cookie
-u user:passauth=("user", "pass")همان هدر Basic
-Lپیش‌فرضcURL به -L نیاز دارد؛ در Requests allow_redirects=False آن را خاموش می‌کند
--max-redirs 5session.max_redirects = 5پیش‌فرض Requests: 30
--connect-timeout 3 -m 20timeout=(3.05, 20)-m کل انتقال را محدود می‌کند؛ timeout خواندن فاصله میان بایت‌هاست
-k / --cacert ca.pemverify=False / verify="ca.pem"verify=False فقط در آزمون‌های محلی
-x http://user:pass@pr.proxynet.io:8000proxies={"http": url, "https": url}استفاده از cURL با پروکسی را ببینید
--compressedهیچRequests فشرده‌سازی را خودش مدیریت می‌کند
-Irequests.head(url)برای HEAD هدایت دنبال نمی‌شود
-i / -vr.headers / r.request.headersآنچه دریافت و فرستاده شد

هر گزینه در راهنمای curl توضیح داده شده است. پروکسی تازه برای هر درخواست کار جداگانه‌ای است (چرخاندن پروکسی در Python).

پارامترهای پرس‌وجو و هدرها را چگونه بفرستیم؟

مقدارهای پرس‌وجو را به شکل دیکشنری به params= بدهید:

python
import requests

params = {"q": "kargo takip", "status": ["pending", "shipped"], "sort": None}
r = requests.get("https://httpbin.org/get", params=params, timeout=20)
print(r.url)  # https://httpbin.org/get?q=kargo+takip&status=pending&status=shipped

فهرست کلید را تکرار می‌کند، None کنار گذاشته می‌شود، و فاصله‌ها و نویسه‌های غیر ASCII برای شما کدگذاری می‌شوند. رشته پرس‌وجویی که از پیش در نشانی هست می‌ماند و params= پس از آن افزوده می‌شود. پیمایش صفحه‌به‌صفحه نتایج را در صفحه‌بندی در وب اسکرپینگ توضیح داده‌ایم.

هدرها در یک دیکشنری از رشته‌ها قرار می‌گیرند و توکن از محیط خوانده می‌شود:

python
import os
import requests

headers = {
    "Authorization": f"Bearer {os.environ['API_TOKEN']}",
    "Accept": "application/json",
}
r = requests.get("https://httpbin.org/headers", headers=headers, timeout=20)
print(r.json()["headers"]["Authorization"])  # Bearer <your token>

r = requests.get("https://httpbin.org/basic-auth/user/pass", auth=("user", "pass"), timeout=20)
print(r.status_code)  # 200

هدرهایی که همه فراخوانی‌ها لازم دارند یک بار روی یک requests.Session و از راه session.headers تنظیم می‌شوند. سه قاعده از مستندات Requests که آزمون‌های ما هر سه را تأیید کردند:

  • .netrc بر headers= مقدم است. یک ورودی .netrc برای همان میزبان، هدر Bearer ما را با یک هدر Basic جایگزین کرد؛ auth= بر هر دو مقدم است.
  • Authorization روی میزبان اصلی می‌ماند. پس از هدایت از 127.0.0.1 به localhost، این هدر حذف شده بود.
  • مقدارها رشته‌اند. headers={"X-Page": 2} خطای InvalidHeader داد.

اگر User-Agent خودتان را ندهید، Requests مقدار python-requests/2.34.2 را می‌فرستد. اینکه در آن چه بنویسید در User-Agent چیست؟ آمده، و اینکه چرا هدرهای یک کلاینت باید با هم سازگار بمانند در وب اسکرپینگ بدون مسدود شدن.

پاسخ را چگونه بخوانیم و خطاهای HTTP را بگیریم؟

یک Response این‌ها را در اختیار شما می‌گذارد: r.status_code، r.headers (دیکشنری‌ای که به بزرگی و کوچکی حروف حساس نیست)، r.content (بایت‌های خام)، r.text (بایت‌هایی که با r.encoding رمزگشایی شده‌اند) و r.json(). نویسه‌های به‌هم‌ریخته در r.text یعنی کدگذاری اشتباه حدس زده شده است (خطاهای کدگذاری در Python).

مستندات Requests هشدار می‌دهد که موفق بودن r.json() به معنای موفق بودن درخواست نیست: سرور می‌تواند همراه 500 یک بدنه خطای JSON بفرستد. نخست وضعیت را بررسی کنید:

python
import requests

r = requests.get("https://httpbin.org/status/404", timeout=20)
try:
    r.raise_for_status()
except requests.HTTPError as exc:
    print(exc)  # 404 Client Error: NOT FOUND for url: https://httpbin.org/status/404

raise_for_status() برای هر 4xx یا 5xx خطای HTTPError برمی‌انگیزد. وقتی بدنه خالی یا HTML باشد، r.json() با JSONDecodeError شکست می‌خورد (علت‌ها در رفع خطای JSONDecodeError: Expecting Value آمده‌اند). اینکه کدام کدها را دوباره تلاش کنید در کدهای وضعیت HTTP در وب اسکرپینگ آمده است.

با Requests چگونه فایل آپلود و دانلود کنیم؟

files= یک بدنه multipart/form-data می‌سازد. تاپل سه‌تایی نام فایل و نوع بخش را تعیین می‌کند، و فیلدهای data= به شکل بخش‌های اضافه فرستاده می‌شوند. چون json= در کنار files= نادیده گرفته می‌شود، JSON را به شکل بخشی جداگانه بفرستید:

python
import json
import requests

with open("report.csv", "rb") as f:
    files = {
        "file": ("report.csv", f, "text/csv"),
        "meta": (None, json.dumps({"source": "warehouse"}), "application/json"),
    }
    r = requests.post("https://httpbin.org/post", files=files, data={"note": "daily"}, timeout=20)

print(r.json()["files"])  # {'file': 'sku,price\n1001,19.90\n'}
print(r.json()["form"])   # {'meta': '{"source": "warehouse"}', 'note': 'daily'}

فایل را در حالت دودویی ("rb") باز کنید: مستندات توضیح می‌دهد که Requests ممکن است Content-Length را برابر تعداد بایت‌های فایل بگذارد و حالت متنی می‌تواند این مقدار را نادرست کند. برای آپلودهای بسیار بزرگ، همان صفحه بسته requests-toolbelt را معرفی می‌کند که بدنه را به‌صورت جریانی می‌فرستد.

برای دانلود، stream=True بدنه بزرگ را بیرون از حافظه نگه می‌دارد:

python
import requests

url = "https://example.com/export.csv"  # replace with your file's URL
with requests.get(url, stream=True, timeout=(3.05, 60)) as r:
    r.raise_for_status()
    with open("export.csv", "wb") as f:
        for chunk in r.iter_content(chunk_size=64 * 1024):
            f.write(chunk)

دانلود فایل‌های فراوان از یک صفحه در دانلود همه تصویرهای یک وب‌سایت آمده است.

چرا درخواستی که در cURL کار می‌کند در Requests پاسخ دیگری می‌گیرد؟

معمولاً دو درخواست با هم فرق دارند. به این ترتیب بررسی کنید:

  1. هدایت‌ها. cURL بدون -L در یک 3xx می‌ایستد؛ Requests برای همه متدها جز HEAD هدایت را دنبال می‌کند. وقتی هر یک از دو ابزار هدایت را دنبال کند، درخواست POST که پاسخ 301، 302 یا 303 بگیرد به GET بدون بدنه تبدیل می‌شود، و 307 یا 308 همان POST را نگه می‌دارند؛ این رفتار با ⁦RFC 9110⁩ هماهنگ است. یک استثنا: با -X POST و -L، cURL پس از یک 302 یک POST بدون بدنه فرستاد؛ --follow (از ⁦curl 8.16.0⁩ به بعد) به GET تغییر می‌دهد. به r.history نگاه کنید، یا allow_redirects=False بدهید و Location را بخوانید.
  2. هدرهای پیش‌فرض. ⁦curl 8.21.0⁩ هدرهای User-Agent: curl/8.21.0 و Accept: */* را فرستاد و Accept-Encoding نفرستاد. Requests این‌ها را فرستاد: python-requests/2.34.2، Accept: */*، Connection: keep-alive و Accept-Encoding: gzip, deflate (به‌علاوه br اگر brotli نصب باشد، و zstd روی ⁦Python 3.14⁩). با r.request.headers مقایسه کنید.
  3. نسخه HTTP. Requests فقط با HTTP/1.1 کار می‌کند: r.raw.version برای یک سایت HTTPS مقدار 11 برگرداند. curl برای HTTPS به‌طور پیش‌فرض ⁦HTTP/2⁩ را مذاکره می‌کند، اگر نسخه ساخته‌شده‌اش از آن پشتیبانی کند (curl -V عبارت HTTP2 را فهرست می‌کند)، و -w "%{http_version}" نسخه به‌کاررفته را چاپ می‌کند. نسخه ویندوزی ما این پشتیبانی را نداشت و از 1.1 استفاده کرد.
  4. محیط. وقتی Session.trust_env روی مقدار پیش‌فرض True است، Requests متغیرهای HTTP_PROXY، HTTPS_PROXY و NO_PROXY را می‌خواند، اگر متغیری تنظیم نشده باشد تنظیمات پروکسی سیستم را در ویندوز و macOS، و همچنین .netrc را. cURL متغیرها را می‌خواند (http_proxy فقط با حروف کوچک) اما تنظیمات سیستم را نه، و .netrc را فقط با --netrc. requests.utils.get_environ_proxies(url) نشان می‌دهد Requests کدام پروکسی را برداشته است؛ این متغیرها در استفاده از پروکسی در wget توضیح داده شده‌اند.
  5. گواهی‌ها. Requests از بسته گواهی certifi استفاده می‌کند؛ curl در ویندوز با Schannel از مخزن گواهی ویندوز. پشت پروکسی سازمانی‌ای که TLS را بازرسی می‌کند، cURL ممکن است کار کند در حالی که Requests خطای SSLError می‌دهد (راه‌حل در خطای Max Retries Exceeded With URL).
  6. بایت‌های بدنه. json= بدنه را دوباره به JSON تبدیل می‌کند و -d @file شکست‌های سطر را حذف می‌کند. اگر API بدنه را امضا می‌کند، از هر دو بدنه هش بگیرید و مقایسه کنید.
  7. آنچه روی سیم می‌رود. یک پروکسی MITM محلی هر دو درخواست را کنار هم نشان می‌دهد.

اگر همه‌چیز یکسان است و پاسخ باز هم فرق دارد، سایت درباره خود کلاینت قضاوت می‌کند، برای نمونه درباره دست‌دادن TLS آن، که هدرها تغییرش نمی‌دهند. اسکرپر Cloudflare توضیح می‌دهد چنین پاسخی را چگونه بخوانید، و اثر انگشت TLS و ⁦JA3⁩ نشان می‌دهد دست‌دادن چه چیزی را آشکار می‌کند. راه پیش رو API رسمی سایت یا اجازه صاحب آن است؛ به ابزارهایی که اسکریپت را به‌جای مرورگر جا می‌زنند نمی‌پردازیم.

نمونه کامل: یک کلاینت کوچک API با تلاش دوباره ایمن

اسکریپت توکن Bearer و هدرهای مشترک را روی یک Session نگه می‌دارد، params= را با یک GET و json= را با یک POST می‌فرستد و وضعیت هر پاسخ را بررسی می‌کند. فقط GET را دوباره تلاش می‌کند: MDN متد POST را idempotent نمی‌داند، یعنی تکرار آن می‌تواند مرسوله دومی بسازد. کلاس Retry در ⁦urllib3⁩ به‌طور پیش‌فرض POST را کنار می‌گذارد؛ اسکریپت این را صریح می‌نویسد.

python
"""A small API client: GET with params, POST with json=, retries for GET only."""
import os

import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

API = os.environ.get("API_BASE", "https://api.example.com/v1")
TIMEOUT = (3.05, 20)  # connect timeout, read timeout (seconds)


def make_session():
    session = requests.Session()
    session.headers.update({
        "Authorization": f"Bearer {os.environ['API_TOKEN']}",  # never hard-code the token
        "Accept": "application/json",
        "User-Agent": "ShipmentSync/1.0 (+https://example.com/contact)",
    })
    retry = Retry(
        total=3,
        backoff_factor=0.5,
        status_forcelist=[502, 503, 504],
        allowed_methods=["GET"],  # a repeated POST could create the same shipment twice
        raise_on_status=False,    # hand back the last response, raise_for_status() reports it
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    proxy = os.environ.get("PROXY_URL")  # optional, e.g. http://user:pass@pr.proxynet.io:8000
    if proxy:
        session.proxies = {"http": proxy, "https": proxy}
        session.trust_env = False  # otherwise HTTPS_PROXY or the system proxy wins over session.proxies
    return session


def list_shipments(session, status="pending", page=1):
    r = session.get(f"{API}/shipments", params={"status": status, "page": page}, timeout=TIMEOUT)
    r.raise_for_status()
    return r.json()


def create_shipment(session, recipient, weight_kg):
    body = {"recipient": recipient, "weight_kg": weight_kg}
    r = session.post(f"{API}/shipments", json=body, timeout=TIMEOUT)
    r.raise_for_status()
    return r.status_code, r.headers.get("Location"), r.json()


if __name__ == "__main__":
    with make_session() as s:
        try:
            print(list_shipments(s))
            print(create_shipment(s, "Ayse Demir", 3))
        except requests.HTTPError as exc:
            print("API error:", exc)

در برابر یک سرور محلی که API را شبیه‌سازی می‌کرد، GET فهرست را برگرداند و POST پاسخ 201 را همراه هدر Location. وقتی سرور 503 داد، GET چهار بار فرستاده شد (با انتظار 0، 1 و 2 ثانیه میان تلاش‌ها) و سپس raise_for_status() خطا را گزارش کرد؛ یک POST به همان نشانی فقط یک بار فرستاده شد. از راه یک پروکسی آزمایشی محلی که در PROXY_URL تنظیم شده بود، هر دو فراخوانی کار کردند و رمز نادرست پاسخ 407 Proxy Authentication Required داد.

⁦urllib3⁩ به‌طور پیش‌فرض به Retry-After در پاسخ 503 هم احترام می‌گذارد؛ مدیریت 429 در کدهای وضعیت HTTP در وب اسکرپینگ آمده، و فراخوانی‌های موازی فراوان در همروندی و موازی‌سازی در وب اسکرپینگ.

کاربردها

اشتباهات رایج

  • data=json.dumps(body) بدون Content-Type. سرور نمی‌تواند تشخیص دهد که بدنه JSON است؛ از json=body استفاده کنید.
  • json= و data= در یک فراخوانی. JSON بی‌صدا کنار گذاشته می‌شود.
  • نبودن timeout. یک سرور بی‌پاسخ اسکریپت را متوقف می‌کند.
  • چسباندن دستی رشته پرس‌وجو. فاصله‌ها و نویسه‌های غیر ASCII نشانی را خراب می‌کنند.
  • r.json() پیش از بررسی وضعیت. یک 500 با بدنه خطای JSON بی‌مشکل تجزیه می‌شود.
  • باز کردن فایل آپلود در حالت متنی. از "rb" استفاده کنید.
  • کپی کردن Host و Content-Length. Requests هدر Host کپی‌شده را بدون تغییر فرستاد، پس آزمونی که روی سرور دیگری اجرا شد هنوز نام سرور قبلی را داشت. یک Content-Length کپی‌شده روی GET بدون بدنه، سرور ما را تا پایان timeout خواندن منتظر نگه داشت.
  • چسباندن فرمان‌های مرورگر در مبدل‌های آنلاین. این فرمان‌ها کوکی نشست و توکن شما را در خود دارند.
  • verify=False در محیط عملیاتی. بررسی گواهی را خاموش می‌کند.
  • تلاش دوباره کورکورانه برای POST. تلاش دوباره پس از timeout می‌تواند رکورد دومی بسازد.

راهنمای انتخاب

نیازپیشنهاد
بدنه JSON برای یک APIrequests.post(url, json=data, timeout=20)، بدون Content-Type دستی
بدنه امضاشده، بایت به بایتdata= با بایت‌هایی که خودتان ساخته‌اید، به‌علاوه Content-Type
فرم ساده (نه فرم ورود)data= با دیکشنری؛ ورود و CSRF در راهنمای نشست‌ها
فایل همراه فیلدهای اضافهfiles= به‌علاوه data=؛ JSON به شکل بخش جداگانه application/json
تبدیل سریع cURLجدول بالا، یا curlconverter روی دستگاه خودتان
در cURL کار می‌کند، در Python نهr.history، r.request.headers، trust_env و نسخه HTTP را بررسی کنید
⁦HTTP/2⁩ یا فراخوانی ناهمگامHTTPX؛ AIOHTTP فقط برای ناهمگام

پرسش‌های متداول

آیا هنگام ارسال JSON با Requests باید هدر Content-Type را تنظیم کنم؟

با json= نه: Requests خودش Content-Type: application/json را تنظیم می‌کند. وقتی متن JSON را از راه data= می‌فرستید به آن نیاز دارید، چون رشته‌ای که در data= باشد بدون این هدر فرستاده می‌شود و یک API سخت‌گیر پاسخ 415 Unsupported Media Type یا 400 می‌دهد.

تفاوت json= و data=json.dumps() در Requests چیست؟

هر دو متن JSON می‌فرستند، اما فقط json= هدر Content-Type را می‌افزاید. بایت‌ها هم ممکن است فرق کنند: json= پس از دونقطه‌ها و ویرگول‌ها فاصله می‌گذارد و نویسه‌های غیر ASCII را به شکل دنباله \u می‌نویسد. به‌طور پیش‌فرض از json= استفاده کنید، و وقتی API بدنه را امضا می‌کند از data= با بایت‌های خودتان.

چگونه با Python Requests توکن Bearer بفرستیم؟

از headers={"Authorization": f"Bearer {token}"} استفاده کنید، یا آن را یک بار در session.headers تنظیم کنید. توکن را از یک متغیر محیطی بخوانید. اگر فایل .netrc برای همان میزبان اطلاعات ورود داشته باشد، Requests به‌جای توکن از همان اطلاعات استفاده می‌کند؛ Session.trust_env = False این رفتار را خاموش می‌کند.

آیا می‌توان یک فرمان cURL را خودکار به Python تبدیل کرد؟

بله. curlconverter فرمان cURL را به کد Requests تبدیل می‌کند و روی دستگاه خودتان اجرا می‌شود. خروجی را بازبینی کنید: timeout نمی‌افزاید، و Requests هدایت‌هایی را دنبال می‌کند که فرمان cURL دنبال نمی‌کرد. فرمان‌هایی را که کوکی یا توکن دارند به مبدل‌های آنلاین ندهید.

آیا Requests هدایت‌ها را دنبال می‌کند و چرا POST من به GET تبدیل می‌شود؟

Requests برای همه متدها جز HEAD هدایت‌ها را دنبال می‌کند. پس از 301، 302 یا 303، درخواست POST مانند مرورگرها به GET تبدیل می‌شود و بدنه‌اش را از دست می‌دهد؛ پس از 307 یا 308 همان POST می‌ماند. allow_redirects=False در نخستین پاسخ متوقف می‌شود.

آیا Python Requests از ⁦HTTP/2⁩ پشتیبانی می‌کند؟

نه. Requests فقط با HTTP/1.1 کار می‌کند؛ در آزمون ما r.raw.version برای یک سایت HTTPS مقدار 11 برگرداند. HTTPX وقتی httpx[http2] را نصب کنید و کلاینت را با http2=True بسازید از ⁦HTTP/2⁩ پشتیبانی می‌کند؛ این قابلیت به‌طور پیش‌فرض خاموش است.

خلاصه

برای یک API مبتنی بر JSON، requests.post(url, json=data, timeout=20) کل کار است: Requests بدنه را به JSON تبدیل می‌کند و هدر را تنظیم می‌کند. data= فرم‌ها یا بایت‌های دقیق را می‌فرستد، files= بدنه‌های چندبخشی را و params= رشته پرس‌وجو را. یک فرمان cURL گزینه به گزینه به همین آرگومان‌ها نگاشته می‌شود؛ اگر پاسخ‌ها باز هم فرق داشتند، به این ترتیب هدایت‌ها، هدرهای پیش‌فرض، نسخه HTTP و محیط را بررسی کنید. وقتی API باید از کشوری مشخص یا از نشانی ثابتی فراخوانی شود، گزینه‌ها را در صفحه خدمات پروکسی ما مقایسه کنید.

پرسش از ChatGPTپرسش از Claude