ProxynetProxynet

رفع خطای JSONDecodeError: Expecting Value در Python

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

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

Acar Diveroli
نویسنده: Acar Diveroli
دروازه باریک ⁦r.json()⁩: در چپ کارت‌های قرمز با بدنه خالی، ⁦<!DOCTYPE html>⁩ و صفحه 407؛ کارت ⁦{"ok": true}⁩ عبور می‌کند

فهرست محصولات یک فروشگاه را از /api/products?page=1 جمع می‌کنید، نشانی‌ای که در پنل Network مرورگر پیدا کرده‌اید. اسکریپت 300 صفحه نخست را پشت سر می‌گذارد و سپس در data = r.json() با خطای requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0) می‌ایستد. همان نشانی، همان کد. یک سطر اضافه می‌کنید، print(r.status_code, r.headers.get("Content-Type"), r.text[:200])، و تصویر عوض می‌شود: 403، text/html و صفحه‌ای که با <!DOCTYPE html> آغاز می‌شود. سرور به‌جای JSON یک صفحه وب فرستاده و تجزیه‌گر در نخستین نویسه، یعنی <، دست کشیده است.

در این راهنما معنای پیام و جایگاهی که نشان می‌دهد، استثنایی که هر کتابخانه Python پرتاب می‌کند و یک بررسی سه‌سطری را که علت را پیدا می‌کند توضیح می‌دهیم. سپس پاسخ‌های خالی، صفحه‌های HTML، پاسخ 407 پروکسی و بدنه‌هایی را که فقط شبیه JSON هستند مرور می‌کنیم و در پایان تابع کمکی آزموده‌شده parse_json() را می‌آوریم.

خطای JSONDecodeError: Expecting value یعنی چه؟

یعنی تجزیه‌گر انتظار آغاز یک مقدار JSON را داشت و به چیز دیگری رسید. بر پایه تعریف ⁦RFC 8259⁩، یک متن JSON تنها یک مقدار است: یک شیء، یک آرایه، رشته‌ای درون نقل‌قول دوتایی، یک عدد، true، false یا null. بنابراین متن معتبر، پس از فضای خالی اختیاری، فقط می‌تواند با {، [، "، یک رقم، -، t، f یا n آغاز شود. وقتی تجزیه‌گر به < یک صفحه HTML، به T در Too Many Requests یا به پایان یک رشته خالی می‌رسد، با پیام «Expecting value» متوقف می‌شود.

این خطای اتصال نیست. درخواستی که هرگز به سرور نرسیده زودتر، درون requests.get()، با خطاهایی مانند ConnectionError یا ProxyError شکست می‌خورد (خطای Max Retries Exceeded With URL). وقتی JSONDecodeError را می‌بینید، پاسخی رسیده است؛ فقط محتوای آن را نمی‌شد به‌عنوان JSON خواند.

بخش line 1 column 1 (char 0) چه چیزی را نشان می‌دهد؟

عددها نشان می‌دهند تجزیه در کجا شکست خورد. کلاس json.JSONDecodeError آن‌ها را به شکل ویژگی (attribute) با خود دارد: msg (علت)، doc (کل متن)، pos (جایگاه نویسه‌ای که تجزیه در آن شکست خورد)، lineno و colno (مستندات json در Python). مقدار char 0 نخستین نویسه بدنه است، پس JSON اصلاً آغاز نشده است.

جایگاه‌های دیگر اطلاعات بیشتری می‌دهند:

  • line 1 column 4 (char 3): بدنه سه فاصله بود و دیگر هیچ. فضای خالی نادیده گرفته می‌شود و سپس متن تمام می‌شود.
  • line 2 column 1 (char 1): بدنه با یک نویسه سطر جدید آغاز می‌شود و پس از آن چیزی می‌آید که JSON نیست، اغلب یک صفحه HTML.
  • جایگاهی در عمق متن: JSON آغاز شده، اما جایی بعدتر شکسته است، برای نمونه در دانلودی که نیمه‌کاره قطع شده.

درون بلوک except، عبارت e.doc[:200] آغاز همان متن را نشان می‌دهد.

کتابخانه‌های requests، json، httpx و aiohttp کدام استثنا را پرتاب می‌کنند؟

هر کتابخانه را با ⁦Python 3.13⁩، ⁦Requests 2.34.2⁩، ⁦HTTPX 0.28.1⁩ و ⁦AIOHTTP 3.14.3⁩ بررسی کردیم:

  • json: json.loads() خطای json.JSONDecodeError را پرتاب می‌کند که زیرکلاس ValueError است.
  • Requests: از نسخه 2.27.0 (ژانویه 2022)، r.json() خطای requests.exceptions.JSONDecodeError را پرتاب می‌کند. بر پایه تاریخچه تغییرات Requests، این کلاس از استثناهایی که پیش‌تر پرتاب می‌شدند ارث می‌برد و یک RequestException هم هست.
  • Requests وقتی simplejson نصب است: کلاس والد به simplejson.errors.JSONDecodeError تبدیل می‌شود. در آزمون ما، در این حالت except json.JSONDecodeError آن را نگرفت؛ except ValueError همچنان آن را گرفت.
  • HTTPX: Response.json() همان json.decoder.JSONDecodeError استاندارد را پرتاب می‌کند.
  • AIOHTTP: await resp.json() نخست Content-Type را بررسی می‌کند و بدون تجزیه، ContentTypeError (Attempt to decode JSON with unexpected mimetype: text/html) را پرتاب می‌کند. با content_type=None خطای json.JSONDecodeError را پرتاب می‌کند.

در Requests، requests.exceptions.JSONDecodeError را بگیرید: در هر دو حالت کار می‌کند. تفاوت‌های دیگر این کلاینت‌ها را در مقایسه HTTPX، Requests و AIOHTTP آورده‌ایم.

چگونه علت را با سه سطر پیدا کنیم؟

پیش از تجزیه، آنچه را رسیده چاپ کنید:

python
print(r.status_code, r.history, r.url)
print(r.headers.get("Content-Type"), len(r.content))
print(r.text[:200])

سپس خروجی را به این ترتیب بخوانید:

  1. وضعیت و تاریخچه. آیا وضعیت 2xx است؟ آیا در مسیر یک 301 یا 302 رخ داده است؟ پس از هدایت، r.status_code مقدار نهایی 200 را نشان می‌دهد و فقط r.history مقدار [<Response [302]>] را نشان می‌دهد.
  2. URL نهایی. r.url نشانی پس از هدایت‌هاست. اگر به /login یا /consent ختم شود، به API نرسیده‌اید.
  3. Content-Type. آنچه می‌خواهید application/json یا نوعی است که به +json ختم می‌شود. هر چیز دیگری به یکی از علت‌های زیر اشاره دارد.
  4. طول و نخستین نویسه‌ها. 0 یعنی خالی، < یعنی HTML، {' یعنی یک dict در Python که به شکل متن چاپ شده و cb( یعنی JSONP.
  5. نتیجه را با جدول زیر تطبیق دهید.

فقط 200 نویسه نخست را در لاگ ثبت کنید: بدنه کامل ممکن است توکن یا داده شخصی داشته باشد.

نخستین نویسه‌های بدنه چه چیزی را نشان می‌دهند؟

همه پیام‌های زیر از اجرای ما با ⁦Python 3.13.9⁩ و ⁦Requests 2.34.2⁩ روی یک سرور آزمایشی محلی به دست آمده‌اند؛ نسخه‌های دیگر Python ممکن است آن‌ها را با عبارت دیگری بنویسند.

آغاز بدنهوضعیت و Content-Type معمولپیام r.json()علت محتملچه باید کرد
(هیچ چیز)204، 304، HEAD یا یک 200 خالیExpecting value: line 1 column 1 (char 0)نقطه اتصال محتوایی برنمی‌گرداندپیش از تجزیه، وضعیت و len(r.content) را بررسی کنید
<!DOCTYPE html>403، 429، 503 یا 200 پس از 302؛ text/htmlExpecting value: line 1 column 1 (char 0)صفحه مسدودسازی، هدایت به صفحه ورود، صفحه خطانخست وضعیت را درست کنید
<html>...407... یا هیچ چیز407 و Proxy-Authenticate، مقصد http://Expecting value: line 1 column 1 (char 0)اطلاعات ورود یا IP نادرست برای پروکسیuser:pass و لیست سفید IP را بررسی کنید
Too Many Requests429، text/plainExpecting value: line 1 column 1 (char 0)محدودیت نرخ به شکل متن سادهسرعت را کم کنید؛ Retry-After را بخوانید
cb({"items": ...});200، application/javascriptExpecting value: line 1 column 1 (char 0)JSONPاز نقطه اتصال بدون callback استفاده کنید
{"id":1} سپس سطر جدید و {"id":2}200، application/x-ndjsonExtra data: line 2 column 1 (char 9)NDJSONسطربه‌سطر تجزیه کنید
{'id': 1, ...}200، اغلب text/plainExpecting property name enclosed in double quotes: line 1 column 2 (char 1)یک dict که با str() نوشته شدهدر کد تولیدکننده از json.dumps() استفاده کنید
یک BOM نامرئی، سپس {200، application/jsonUnexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)نشانه ترتیب بایت (BOM)با utf-8-sig رمزگشایی کنید

پنج ردیف نخست پیام یکسانی دارند، پس پیام به‌تنهایی هرگز علت را نام نمی‌برد؛ وضعیت و Content-Type آن را نام می‌برند.

پاسخ‌های خالی: 204، HEAD و 304

برخی پاسخ‌ها طبق تعریف بدنه ندارند. ⁦RFC 9110⁩ می‌گوید پاسخ 204 No Content نمی‌تواند محتوا داشته باشد، 304 Not Modified هم محتوایی ندارد و پاسخ درخواست HEAD فقط هدر دارد. بسیاری از APIها به DELETE و PUT با 204 پاسخ می‌دهند: عملیات موفق بوده و چیزی برای تجزیه نیست، با این حال r.json() خطای Expecting value را پرتاب می‌کند.

این پاسخ‌ها را «بدون داده» بدانید، نه خطا، و پیش از تجزیه r.status_code را بررسی کنید. یک 200 خالی فرق دارد: معمولاً نشانه خطای سرور یا نقطه اتصال نادرست است و ارزش یک سطر لاگ را دارد.

دریافت HTML به‌جای JSON: صفحه‌های مسدودسازی، ورود و خطا

سه نوع صفحه HTML جایی می‌رسند که انتظار JSON می‌رفت و کد وضعیت آن‌ها را از هم جدا می‌کند.

صفحه مسدودسازی یا بررسی. یک 403، 429 یا 503 با text/html اغلب پاسخ سامانه محافظت در برابر بات است. تگ <title> آن، مانند «Just a moment...» یا «Access denied»، برای شناختنش کافی است. آن را تجزیه نکنید و در حلقه دوباره تلاش نکنید. معنای هر کد را در کدهای وضعیت HTTP در وب اسکرپینگ، صفحه‌های Cloudflare را در اسکرپر Cloudflare و امتیازدهی بات را در تشخیص بات چگونه کار می‌کند؟ توضیح داده‌ایم. راه‌های مشروع عبارت‌اند از API رسمی، سرعت کمتری که robots.txt را رعایت کند یا اجازه صاحب سایت.

صفحه ورود پس از هدایت. وضعیت 200 است، پس این حالت به‌آسانی از چشم می‌افتد. r.history مقدار [<Response [302]>] را نشان می‌دهد و r.url به /login ختم می‌شود: نشست شما منقضی شده است. برای حساب خودتان، راه‌حل مدیریت نشست است (نشست و کوکی در Python).

صفحه خطای سرور. یک 500، 502 یا 504 همراه HTML از خود سرور یا دروازه‌ای که جلوی آن قرار دارد می‌آید: مشکل در API است، نه در تجزیه‌گر شما.

وقتی URL به خود صفحه اشاره کند و نه به API، باز هم HTML می‌رسد. JSON از درخواست جداگانه‌ای می‌آید که صفحه می‌فرستد و آن را در پنل Network پیدا می‌کنید (نخست درخواست API را پیدا کنید).

از طریق پروکسی: پاسخ 407

اگر یک درخواست ساده http:// را با رمز عبور نادرست از پروکسی بفرستید، خود پروکسی با 407 Proxy Authentication Required پاسخ می‌دهد. Requests این پاسخ را همچون پاسخی عادی، با هر بدنه‌ای که پروکسی بفرستد، به کد شما تحویل می‌دهد: یک صفحه HTML، متنی کوتاه یا هیچ چیز. با دو پروکسی آزمایشی محلی، یکی با بدنه HTML و دیگری با بدنه خالی، r.json() هر دو بار Expecting value داد.

برای مقصد https://، پاسخ 407 هنگام برپایی تونل می‌رسد، پس requests.get() خطای ProxyError: Tunnel connection failed: 407 را پرتاب می‌کند و کار هرگز به r.json() نمی‌رسد (خطای Max Retries Exceeded With URL).

پاسخ 407 همراه هدر Proxy-Authenticate به پروکسی اشاره دارد، نه به مقصد. نام کاربری و رمز عبور را بررسی کنید، نویسه‌های ویژه را در URL پروکسی کدگذاری کنید (@ به %40 تبدیل می‌شود) یا مطمئن شوید IP شما در لیست سفید است (احراز هویت پروکسی: نام کاربری و رمز یا لیست سفید IP).

بدنه‌هایی که شبیه JSON هستند اما نیستند: JSONP، NDJSON و نقل‌قول تکی

برخی بدنه‌ها JSON دارند، اما نه به شکل یک مقدار تمیز و یکپارچه.

JSONP

JSONP داده JSON را درون فراخوانی یک تابع می‌پیچد، مانند cb({"items": [1]});، و معمولاً با نوع application/javascript فرستاده می‌شود. تجزیه‌گر c را می‌بیند و در char 0 پیام Expecting value را گزارش می‌کند. از نقطه اتصال بدون پارامتر callback استفاده کنید یا متن میان نخستین ( و آخرین ) را بردارید.

NDJSON و JSON Lines

نقطه‌های اتصال خروجی‌گیری (export) و استریم اغلب در هر سطر یک مقدار JSON می‌فرستند، قالبی که در jsonlines.org شرح داده شده است. سطر نخست تجزیه می‌شود، سپس تجزیه‌گر به متن بیشتری می‌رسد و با Extra data: line 2 column 1 متوقف می‌شود. این بدنه‌ها را با r.iter_lines() سطربه‌سطر بخوانید.

نقل‌قول تکی و مقدارهای Python

بدنه‌ای مانند {'id': 1} به‌جای json.dumps() با str() در Python نوشته شده و تجزیه‌گر در char 1 با Expecting property name enclosed in double quotes متوقف می‌شود. بدنه None یا True، که املای Python است، با Expecting value شکست می‌خورد، چون JSON آن‌ها را null و true می‌نویسد. کدی را که داده را می‌نویسد اصلاح کنید: text.replace("'", '"') هر مقداری را که آپاستروف دارد خراب می‌کند و ast.literal_eval() فقط برای داده‌ای امن است که خودتان تولید کرده‌اید.

نشانه ترتیب بایت (BOM) در آغاز بدنه پیام Unexpected UTF-8 BOM را می‌دهد؛ جنبه رمزگذاری را در خطاهای رمزگذاری یونیکد در Python توضیح داده‌ایم.

نمونه کامل: parse_json() که پیش از خواندن JSON بدنه را بررسی می‌کند

این تابع کمکی همان بررسی سه‌سطری را به کد تبدیل می‌کند. داده تجزیه‌شده را برمی‌گرداند، برای پاسخ‌هایی که طبق تعریف بدنه ندارند None برمی‌گرداند، یا یک خطای NotJSON پرتاب می‌کند که وضعیت، هدایت‌ها، Content-Type، URL نهایی و آغاز بدنه را نام می‌برد. Requests را با pip install requests نصب کنید.

python
"""Read a JSON response, or say in one line why the body is not JSON."""
import json

import requests

PROXY = "http://user:pass@pr.proxynet.io:8000"
PROXIES = {"http": PROXY, "https": PROXY}


class NotJSON(ValueError):
    """The server answered, but not with the JSON we asked for."""


def describe(r):
    """Status, redirects, Content-Type, final URL and the first 200 characters."""
    hops = "".join(f"{h.status_code} -> " for h in r.history)
    ctype = r.headers.get("Content-Type", "none")
    start = r.text[:200].replace("\n", " ")
    return f"HTTP {hops}{r.status_code}, {ctype}, {r.url}, body {start!r}"


def media_type(r):
    return r.headers.get("Content-Type", "").split(";")[0].strip().lower()


def is_json_type(mtype):
    return mtype == "application/json" or mtype.endswith("+json")


def parse_json(r):
    """Return the parsed body, None for "no content", or raise NotJSON with the reason."""
    if r.status_code in (204, 304) or r.request.method == "HEAD":
        return None  # these answers carry no body by definition
    mtype = media_type(r)
    if not r.ok and not is_json_type(mtype):
        raise NotJSON(f"error response, not JSON: {describe(r)}")
    if not r.content:
        raise NotJSON(f"empty body: {describe(r)}")
    if mtype == "application/x-ndjson":
        return [json.loads(line) for line in r.iter_lines() if line.strip()]
    if r.text.lstrip().startswith("<"):
        raise NotJSON(f"HTML instead of JSON: {describe(r)}")
    try:
        return r.json()
    except requests.exceptions.JSONDecodeError as e:
        raise NotJSON(f"{e.msg} at char {e.pos}: {describe(r)}") from e


if __name__ == "__main__":
    url = "https://example.com/api/products?page=1"
    r = requests.get(url, proxies=PROXIES, timeout=(5, 30))
    try:
        data = parse_json(r)
    except NotJSON as e:
        print("stop:", e)
    else:
        if not r.ok:
            print("API error:", r.status_code, data)
        elif data is None:
            print("no content")
        else:
            print("ok:", type(data).__name__, len(data))

وضعیت خطا همراه بدنه غیر JSON پیش از همه متوقف می‌شود، پس صفحه 403 یا پاسخ 407 پروکسی هرگز تجزیه نمی‌شود. وضعیت خطا همراه بدنه JSON، مانند 400 با {"error": ...}، برگردانده می‌شود، چون بسیاری از APIها خطاها را این‌گونه توضیح می‌دهند؛ فراخواننده r.ok را بررسی می‌کند. متغیر PROXIES اختیاری است و timeout=(5, 30) به اتصال 5 ثانیه و به پاسخ 30 ثانیه مهلت می‌دهد.

این تابع عمداً تلاش دوباره نمی‌کند، IP را نمی‌چرخاند و به‌صورت موازی اجرا نمی‌شود. اینکه کدام وضعیت‌ها ارزش تلاش دوباره دارند در کدهای وضعیت HTTP در وب اسکرپینگ، چرخش در چرخاندن پروکسی در Python و درخواست‌های موازی در همروندی و موازی‌سازی آمده است.

خروجی چه شکلی دارد

تابع parse_json() را روی یک سرور آزمایشی محلی اجرا کردیم که به هر مسیر با یکی از بدنه‌های جدول پاسخ می‌دهد؛ سطر آخر از یک پروکسی آزمایشی گذشت که با HTML پاسخ 407 داد.

text
/api/products -> {'items': [1, 2, 3]}
/api/items/7 -> None
/api/empty -> NotJSON: empty body: HTTP 200, application/json, http://127.0.0.1:8111/api/empty, body ''
/api/blocked -> NotJSON: error response, not JSON: HTTP 403, text/html; charset=utf-8, http://127.0.0.1:8111/api/blocked, body '<!DOCTYPE html><html><head><title>Just a moment...</title></head></html>'
/api/private -> NotJSON: HTML instead of JSON: HTTP 302 -> 200, text/html; charset=utf-8, http://127.0.0.1:8111/login, body '<!DOCTYPE html> <html><head><title>Sign in</title></head></html>'
/api/slow -> NotJSON: error response, not JSON: HTTP 429, text/plain, http://127.0.0.1:8111/api/slow, body 'Too Many Requests'
/api/jsonp -> NotJSON: Expecting value at char 0: HTTP 200, application/javascript, http://127.0.0.1:8111/api/jsonp, body 'cb({"items": [1]});'
/api/export -> [{'id': 1}, {'id': 2}]
/api/dict -> NotJSON: Expecting property name enclosed in double quotes at char 1: HTTP 200, text/plain, http://127.0.0.1:8111/api/dict, body "{'id': 1, 'name': 'Lamp'}"
/api/bad-request -> {'error': 'page must be a number'} (status 400)
proxy, wrong password -> NotJSON: error response, not JSON: HTTP 407, text/html, http://example.com/api/products, body '<html><head><title>407 Proxy Authentication Required</title></head><body><h1>407</h1></body></html>'

مسیر /api/items/7 با 204 پاسخ داد و /api/export داده NDJSON فرستاد، پس هیچ‌کدام خطا نیست. سطر /api/private هدایتی را نشان می‌دهد که بررسی وضعیت به‌تنهایی آن را نمی‌بیند: 302 -> 200 که به /login می‌رسد.

کاربردها: کدام اسکریپت‌هایی که JSON انتظار دارند به این خطا می‌خورند؟

  • فراخوانی API خود سایت: درخواستی که از پنل Network کپی کرده‌اید، وقتی نشست یا توکن پشت آن منقضی شود، دیگر کار نمی‌کند (صفحه‌های ایستا و پویا).
  • رصد قیمت: کار روزانه‌ای که JSON محصولات را می‌خواند، روزی که بیش از اندازه سریع اجرا شود صفحه مسدودسازی می‌گیرد (رصد قیمت رقبا).
  • صفحه‌بندی API: صفحه پس از آخرین صفحه ممکن است به‌جای فهرست خالی، 204 یا بدنه خالی برگرداند (صفحه‌بندی در وب اسکرپینگ).
  • ابزارهای اتوماسیون: گره HTTP Request در ⁦n8n⁩ انتظار JSON دارد و صفحه خطای HTML دریافت می‌کند (تنظیم پروکسی در ⁦n8n⁩).
  • خط لوله‌های داده: یک پاسخ HTML میان هزاران پاسخ JSON باید دسته را متوقف کند، نه اینکه به پایگاه داده راه پیدا کند (استخراج داده).
  • خزنده‌ها: خزنده‌ای که نقطه‌های اتصال JSON را در میزبان‌های زیادی می‌خواند، برای هر میزبان ناموفق به یک پیام روشن نیاز دارد (خزنده وب).

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

  • بلعیدن خطا. except JSONDecodeError: pass چیزی ذخیره نمی‌کند و علت را پنهان می‌کند.
  • اعتماد به وضعیت 200. صفحه ورود پس از 302 هم با 200 می‌رسد. r.history و r.url را بررسی کنید.
  • تبدیل نقل‌قول تکی به دوتایی با replace(). هر مقداری را که آپاستروف دارد خراب می‌کند.
  • به کار بردن eval() روی بدنه پاسخ. هر کدی را که سرور فرستاده باشد اجرا می‌کند.
  • تلاش دوباره روی صفحه مسدودسازی با همان سرعت. همان درخواست‌ها با همان نرخ همان 429 یا 403 را می‌گیرند؛ نخست نرخ را پایین بیاورید (⁦429 Too Many Requests⁩).
  • ثبت کل بدنه در لاگ. 200 نویسه نخست علت را نام می‌برند؛ بقیه ممکن است توکن و داده شخصی داشته باشد.
  • یک except RequestException برای هر دو get() و json(). از نسخه 2.27.0 هر دو را می‌گیرد، پس خطای شبکه و خطای تجزیه یکسان به نظر می‌رسند.
  • جست‌وجوی باگ در ماژول json. تجزیه‌گر درست کار می‌کند؛ بدنه JSON نیست.

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

آنچه می‌بینیدچه باید کرد
وضعیت 204 یا بدنه خالیr.json() را فرانخوانید؛ آن را «بدون داده» بدانید
403، 429 یا 503 با text/htmlتجزیه را متوقف کنید و وضعیت را درست کنید (کدهای وضعیت HTTP در وب اسکرپینگ)
200، اما r.history یک 302 به صفحه ورود نشان می‌دهدنشست را تازه کنید (نشست و کوکی در Python)
407 با بدنه HTML یا خالیاطلاعات ورود پروکسی و لیست سفید IP را بررسی کنید
ProxyError: Tunnel connection failed: 407همان علت، برای مقصدهای https:// (خطای Max Retries Exceeded With URL)
Extra dataبا r.iter_lines() سطربه‌سطر تجزیه کنید
بدنه با callback( آغاز می‌شوداز نقطه اتصال بدون callback استفاده کنید یا پوشش تابع را جدا کنید
Unexpected UTF-8 BOMبا utf-8-sig رمزگشایی کنید (خطاهای رمزگذاری یونیکد در Python)

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

چرا r.json() با وجود کد وضعیت 200 شکست می‌خورد؟

کد 200 چیزی درباره قالب بدنه نمی‌گوید. صفحه ورود پس از هدایت، پاسخ JSONP یا بدنه خالی همگی ممکن است با 200 برسند. Content-Type، r.history و آغاز r.text را بررسی کنید.

تفاوت requests.exceptions.JSONDecodeError و json.JSONDecodeError چیست؟

از ⁦Requests 2.27.0⁩ به بعد، r.json() خطای requests.exceptions.JSONDecodeError را پرتاب می‌کند. این کلاس زیرکلاس JSONDecodeError کتابخانه JSON و RequestException خود Requests است، پس هر دو آن را می‌گیرند. استثنا simplejson است: وقتی نصب باشد، except json.JSONDecodeError خطا را نمی‌گیرد، پس کلاس Requests را بگیرید.

چرا خطای JSONDecodeError: Extra data می‌گیرم؟

تجزیه‌گر یک مقدار کامل JSON را خواند و سپس به متن بیشتری رسید: معمولاً NDJSON، یا دو شیء که پشت سر هم نوشته شده‌اند. سطربه‌سطر تجزیه کنید یا برای خواندن یک مقدار در هر بار از json.JSONDecoder().raw_decode() استفاده کنید.

پیام «Expecting property name enclosed in double quotes» یعنی چه؟

یکی از کلیدهای درون شیء در نقل‌قول دوتایی نیست. علت معمول، یک dict در Python است که با str() نوشته شده و از نقل‌قول تکی استفاده می‌کند. در ⁦Python 3.12⁩ و نسخه‌های قدیمی‌تر، ویرگول پایانی پیش از } هم همین پیام را می‌دهد؛ ⁦Python 3.13⁩ آن را با Illegal trailing comma before end of object گزارش می‌کند.

این خطا را در yfinance یا spotdl می‌گیرم. چه کنم؟

کتابخانه از یک سرویس راه دور JSON خواسته و چیز دیگری دریافت کرده است. کتابخانه را به‌روز کنید، آن را کمتر فراخوانی کنید و در بخش issueهای پروژه به دنبال همین پیام بگردید.

همین خطا در JavaScript چه شکلی دارد؟

در ⁦Node.js 24⁩، JSON.parse() روی یک صفحه HTML خطای SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON و روی رشته خالی خطای SyntaxError: Unexpected end of JSON input را پرتاب می‌کند. آنجا هم پیش از response.json() وضعیت و Content-Type را بررسی کنید (معادل cURL در JavaScript). اعلان «The response is not a valid JSON response» در ویرایشگر WordPress مشکل دیگری است.

خلاصه

خطای JSONDecodeError: Expecting value نشانه است، نه علت. اتصال برقرار شد و سرور پاسخ داد، اما بدنه خالی بود یا JSON نبود. سه بررسی علت را پیدا می‌کند: کد وضعیت همراه r.history، Content-Type و 200 نویسه نخست بدنه. یک 204 خالی عادی است، صفحه HTML یعنی صفحه مسدودسازی، ورود یا خطا، 407 به پروکسی اشاره دارد و Extra data یا نقل‌قول تکی یعنی بدنه فقط شبیه JSON است. انواع پروکسی را که می‌توانید جلوی چنین کاری بگذارید در صفحه خدمات پروکسی ما فهرست کرده‌ایم.

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