---
title: "رفع خطای JSONDecodeError: Expecting Value در Python"
description: "خطای JSONDecodeError: Expecting Value یعنی تجزیه‌گر در نخستین نویسه هیچ JSON پیدا نکرد: بدنه خالی، HTML یا متن ساده است. روش یافتن علت را توضیح می‌دهیم."
url: https://proxynet.io/fa/blog/jsondecodeerror-expecting-value
date: 2026-09-24
author: "Acar Diveroli"
category: "آموزش‌ها, وب اسکرپینگ"
lang: fa
---

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

فهرست محصولات یک فروشگاه را از `/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: line 1 column 1 (char 0)` یعنی تجزیه‌گر JSON در Python در نخستین نویسه متن هیچ مقدار JSON پیدا نکرده است. اتصال برقرار شده؛ مشکل در بدنه است. بدنه یا خالی است (`204`، درخواست `HEAD`، یک `200` خالی) یا JSON نیست: صفحه مسدودسازی، هدایت به صفحه ورود، صفحه خطای سرور، صفحه `407` پروکسی، محدودیت نرخ به شکل متن ساده یا JSONP. پیش از `r.json()`، مقدارهای `r.status_code`، `r.headers.get("Content-Type")` و `r.text[:200]` را بررسی کنید. اگر وضعیت `2xx` نیست یا نوع محتوا `application/json` نیست، تجزیه نکنید؛ نخست بفهمید چرا سرور چیز دیگری فرستاده است.

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

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

این خطای اتصال نیست. درخواستی که هرگز به سرور نرسیده زودتر، درون `requests.get()`، با خطاهایی مانند `ConnectionError` یا `ProxyError` شکست می‌خورد ([خطای Max Retries Exceeded With URL](/fa/blog/max-retries-exceeded-with-url)). وقتی `JSONDecodeError` را می‌بینید، پاسخی رسیده است؛ فقط محتوای آن را نمی‌شد به‌عنوان JSON خواند.

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

عددها نشان می‌دهند تجزیه در کجا شکست خورد. کلاس `json.JSONDecodeError` آن‌ها را به شکل ویژگی (attribute) با خود دارد: `msg` (علت)، `doc` (کل متن)، `pos` (جایگاه نویسه‌ای که تجزیه در آن شکست خورد)، `lineno` و `colno` ([مستندات json در Python](https://docs.python.org/3/library/json.html#json.JSONDecodeError)). مقدار `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](https://github.com/psf/requests/blob/main/HISTORY.md)، این کلاس از استثناهایی که پیش‌تر پرتاب می‌شدند ارث می‌برد و یک `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](/fa/blog/httpx-vs-requests-vs-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/html` | `Expecting 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 Requests` | `429`، `text/plain` | `Expecting value: line 1 column 1 (char 0)` | محدودیت نرخ به شکل متن ساده | سرعت را کم کنید؛ `Retry-After` را بخوانید |
| `cb({"items": ...});` | `200`، `application/javascript` | `Expecting value: line 1 column 1 (char 0)` | JSONP | از نقطه اتصال بدون callback استفاده کنید |
| `{"id":1}` سپس سطر جدید و `{"id":2}` | `200`، `application/x-ndjson` | `Extra data: line 2 column 1 (char 9)` | NDJSON | سطربه‌سطر تجزیه کنید |
| `{'id': 1, ...}` | `200`، اغلب `text/plain` | `Expecting property name enclosed in double quotes: line 1 column 2 (char 1)` | یک dict که با `str()` نوشته شده | در کد تولیدکننده از `json.dumps()` استفاده کنید |
| یک BOM نامرئی، سپس `{` | `200`، `application/json` | `Unexpected 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⁩](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.5) می‌گوید پاسخ `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 در وب اسکرپینگ](/fa/blog/http-status-codes-web-scraping)، صفحه‌های Cloudflare را در [اسکرپر Cloudflare](/fa/blog/cloudflare-scraper) و امتیازدهی بات را در [تشخیص بات چگونه کار می‌کند؟](/fa/blog/how-bot-detection-works) توضیح داده‌ایم. راه‌های مشروع عبارت‌اند از API رسمی، سرعت کمتری که `robots.txt` را رعایت کند یا اجازه صاحب سایت.

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

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

وقتی URL به خود صفحه اشاره کند و نه به API، باز هم HTML می‌رسد. JSON از درخواست جداگانه‌ای می‌آید که صفحه می‌فرستد و آن را در پنل Network پیدا می‌کنید ([نخست درخواست API را پیدا کنید](/fa/blog/static-vs-dynamic-pages)).

## از طریق پروکسی: پاسخ 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](/fa/blog/max-retries-exceeded-with-url)).

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

## بدنه‌هایی که شبیه 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](https://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()` فقط برای داده‌ای امن است که خودتان تولید کرده‌اید. اگر این داده را خودتان با Python Requests به یک API می‌فرستید، به‌جای تبدیل dict به متن با `str()`، آن را به `json=` بدهید؛ تفاوت `json=` و `data=` را در مقاله [ارسال JSON با POST در Python Requests: معادل‌های cURL](/fa/blog/python-requests-post-json) توضیح داده‌ایم.

نشانه ترتیب بایت (BOM) در آغاز بدنه پیام `Unexpected UTF-8 BOM` را می‌دهد؛ جنبه رمزگذاری را در [خطاهای رمزگذاری یونیکد در Python](/fa/blog/python-unicode-encoding-errors) توضیح داده‌ایم.

## نمونه کامل: `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 در وب اسکرپینگ](/fa/blog/http-status-codes-web-scraping)، چرخش در [چرخاندن پروکسی در Python](/fa/blog/how-to-rotate-proxies-in-python) و درخواست‌های موازی در [همروندی و موازی‌سازی](/fa/blog/concurrency-vs-parallelism) آمده است.

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

تابع `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 کپی کرده‌اید، وقتی نشست یا توکن پشت آن منقضی شود، دیگر کار نمی‌کند ([صفحه‌های ایستا و پویا](/fa/blog/static-vs-dynamic-pages)).
- **رصد قیمت:** کار روزانه‌ای که JSON محصولات را می‌خواند، روزی که بیش از اندازه سریع اجرا شود صفحه مسدودسازی می‌گیرد ([رصد قیمت رقبا](/fa/blog/competitor-price-tracking)).
- **صفحه‌بندی API:** صفحه پس از آخرین صفحه ممکن است به‌جای فهرست خالی، `204` یا بدنه خالی برگرداند ([صفحه‌بندی در وب اسکرپینگ](/fa/blog/pagination-web-scraping)).
- **ابزارهای اتوماسیون:** گره HTTP Request در ⁦n8n⁩ انتظار JSON دارد و صفحه خطای HTML دریافت می‌کند ([تنظیم پروکسی در ⁦n8n⁩](/fa/blog/n8n-proxy)).
- **خط لوله‌های داده:** یک پاسخ HTML میان هزاران پاسخ JSON باید دسته را متوقف کند، نه اینکه به پایگاه داده راه پیدا کند ([استخراج داده](/fa/data-scraping)).
- **خزنده‌ها:** خزنده‌ای که نقطه‌های اتصال JSON را در میزبان‌های زیادی می‌خواند، برای هر میزبان ناموفق به یک پیام روشن نیاز دارد ([خزنده وب](/fa/web-crawler)).

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

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

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

| آنچه می‌بینید | چه باید کرد |
|---|---|
| وضعیت `204` یا بدنه خالی | `r.json()` را فرانخوانید؛ آن را «بدون داده» بدانید |
| `403`، `429` یا `503` با `text/html` | تجزیه را متوقف کنید و وضعیت را درست کنید ([کدهای وضعیت HTTP در وب اسکرپینگ](/fa/blog/http-status-codes-web-scraping)) |
| `200`، اما `r.history` یک `302` به صفحه ورود نشان می‌دهد | نشست را تازه کنید ([نشست و کوکی در Python](/fa/blog/python-login-session-cookies)) |
| `407` با بدنه HTML یا خالی | اطلاعات ورود پروکسی و لیست سفید IP را بررسی کنید |
| `ProxyError: Tunnel connection failed: 407` | همان علت، برای مقصدهای `https://` ([خطای Max Retries Exceeded With URL](/fa/blog/max-retries-exceeded-with-url)) |
| `Extra data` | با `r.iter_lines()` سطربه‌سطر تجزیه کنید |
| بدنه با `callback(` آغاز می‌شود | از نقطه اتصال بدون callback استفاده کنید یا پوشش تابع را جدا کنید |
| `Unexpected UTF-8 BOM` | با `utf-8-sig` رمزگشایی کنید ([خطاهای رمزگذاری یونیکد در Python](/fa/blog/python-unicode-encoding-errors)) |

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

### چرا `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](/fa/blog/curl-in-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 است. انواع پروکسی را که می‌توانید جلوی چنین کاری بگذارید در صفحه [خدمات پروکسی ما](/fa/proxy) فهرست کرده‌ایم.
