---
title: "مشکل حروف فارسی در پایتون: رفع خطای UnicodeDecodeError"
description: "حروف فارسی در پایتون وقتی خراب می‌شوند که بایت‌ها با کدک اشتباه رمزگشایی شوند. رفع خطا در فایل‌ها، ⁦windows-1256⁩، کنسول ویندوز و صفحه‌های وب را توضیح می‌دهیم."
url: https://proxynet.io/fa/blog/python-unicode-encoding-errors
date: 2026-09-24
author: "Acar Diveroli"
category: "وب اسکرپینگ, آموزش‌ها"
lang: fa
---

# مشکل حروف فارسی در پایتون: رفع خطای UnicodeDecodeError

اسکریپتی آگهی‌های مناقصه را از وب‌سایت قدیمی یک شهرداری در ترکیه جمع می‌کند. عنوانی که در مرورگر `Çankırı İhale İlanı` خوانده می‌شود، در خروجی اسکریپت به شکل `Çankýrý Ýhale Ýlaný` چاپ می‌شود. فهرستی هم که اسکریپت روی یک لپ‌تاپ ویندوزی ذخیره کرده، روی سرور لینوکس باز نمی‌شود و این خطا را می‌دهد: `'utf-8' codec can't decode byte 0xc7 in position 0: invalid continuation byte`. هر دو مشکل یک علت دارند: بایت‌ها با یک جدول کد نوشته شده‌اند و با جدولی دیگر خوانده شده‌اند.

این راهنما همین علت را هر جا که ظاهر شود برطرف می‌کند: فایل‌های منبع، `open()`، کنسول ویندوز، صفحه‌هایی که با Requests و Beautiful Soup دریافت می‌شوند و فایل‌های CSV برای Excel. همچنین نشان می‌دهد متن خراب را چگونه بخوانید و ترمیم کنید. همه مقدارهای بایت و پیام‌های خطای این نوشته از اجراهای خودمان با ⁦Python 3.13⁩ آمده‌اند.

> **نکته: پاسخ کوتاه**
>
> در ⁦Python 3⁩ یک `str` از پیش یونیکد است. نویسه‌ها فقط وقتی خراب می‌شوند که بایت‌ها با کدک اشتباه به متن رمزگشایی شوند یا متن با کدک اشتباه به بایت رمزگذاری شود. به هر فراخوانی `open()` آرگومان `encoding="utf-8"` را بدهید و صفحه کدی قدیمی مانند `cp1254` را فقط برای فایل‌هایی به کار ببرید که با همان نوشته شده‌اند. در ویندوز، اگر خروجی هدایت‌شده با خطای `'charmap'` شکست می‌خورد، `PYTHONUTF8=1` را تنظیم کنید. Requests پاسخ `text/html` را که در هدرش charset ندارد ⁦ISO-8859-1⁩ می‌خواند، پس `r.content` را به Beautiful Soup بدهید یا `r.encoding` را خودتان تعیین کنید. فایل‌های CSV را برای Excel با `utf-8-sig` بنویسید.

## تفاوت str و bytes در پایتون چیست؟

یک `str` نویسه‌ها را نگه می‌دارد و هر نویسه یک نقطه کد یونیکد دارد: حرف ترکی `ı` (i بی‌نقطه) ⁦U+0131⁩ است و `é` ⁦U+00E9⁩. دیسک‌ها و شبکه‌ها فقط بایت ذخیره می‌کنند، پس متن هنگام بیرون رفتن رمزگذاری و هنگام وارد شدن رمزگشایی می‌شود. کدک تعیین می‌کند کدام بایت‌ها نماینده کدام نویسه‌اند. در ⁦UTF-8⁩، حرف `ı` دو بایت است: `C4 B1`. در ⁦windows-1254⁩، صفحه کد قدیمی ترکی در ویندوز، یک بایت است: `FD`. صفحه کد اروپای غربی ⁦cp1252⁩ و ⁦ISO-8859-1⁩ اصلاً `ı` ندارند. حروف فارسی هم همین وضع را دارند: `ی` (⁦U+06CC⁩) در ⁦UTF-8⁩ دو بایت `DB 8C` است، اما ⁦windows-1256⁩، صفحه کد قدیمی عربی و فارسی در ویندوز، این حرف را ندارد و فقط یای عربی (⁦U+064A⁩) را در بایت `ED` نگه می‌دارد.

وقتی نویسنده و خواننده کدک‌های متفاوتی به کار می‌برند، بایت‌ها سالم می‌مانند اما حرف‌ها عوض می‌شوند. به این پدیده mojibake می‌گویند و اغلب می‌توان آن را برگرداند.

## یک نویسه چگونه به نویسه‌ای اشتباه تبدیل می‌شود؟

هر خطای رمزگذاری همین گام‌ها را طی می‌کند:

1. **متن شما یونیکد است.** `"Çankırı"` در پایتون هفت نقطه کد است.
2. **با کدک A رمزگذاری می‌شود.** ویندوز ترکی در ⁦cp1254⁩ بایت‌های `C7 61 6E 6B FD 72 FD` را می‌نویسد.
3. **فقط بایت‌ها جابه‌جا می‌شوند.** یک فایل، بدنه یک پاسخ HTTP یا یک pipe چیزی درباره کدک A نمی‌گوید، مگر اینکه یک هدر، یک BOM یا تگ `<meta>` آن را اضافه کند.
4. **کسی با کدک B رمزگشایی می‌کند.** ⁦ISO-8859-1⁩ بایت `FD` را به `ý` نگاشت می‌کند و نتیجه `Çankýrý` می‌شود. ⁦UTF-8⁩ نمی‌تواند `C7 61` را یک نویسه بخواند، پس `UnicodeDecodeError` می‌دهد.
5. **خروجی دوباره رمزگذاری می‌شود.** `print()` و `write()` کدک کنسول یا فایل را به کار می‌برند و نویسه‌ای که در آن کدک نباشد `UnicodeEncodeError` می‌دهد.

traceback به شما می‌گوید کدام جهت شکست خورده است.

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

شکل خرابی نشان می‌دهد کدام دو کدک با هم اشتباه گرفته شده‌اند.

| آنچه می‌بینید | متن واقعی | چه اتفاقی افتاده | چه باید کرد |
|---|---|---|---|
| `cafÃ©`، `Ã¼`، `Ä±`، `ÅŸ` | `café`، `ü`، `ı`، `ş` | بایت‌های ⁦UTF-8⁩ (`C3 A9`، `C4 B1`) با ⁦cp1252⁩ یا ⁦ISO-8859-1⁩ رمزگشایی شده‌اند | بایت‌ها را با ⁦UTF-8⁩ رمزگشایی کنید یا متن را ترمیم کنید (پایین‌تر) |
| `ý`، `þ`، `ð`، `Ý` | `ı`، `ş`، `ğ`، `İ` | بایت‌های ⁦windows-1254⁩ (`FD`، `FE`، `F0`، `DD`) با ⁦ISO-8859-1⁩، یعنی پیش‌فرض Requests، رمزگشایی شده‌اند | `r.encoding = "cp1254"` را تنظیم کنید یا `r.content` را به Beautiful Soup بدهید |
| `ţ` به‌جای `ş` | `ş` | حدس خودکار برای یک صفحه ترکی ⁦windows-1250⁩ را انتخاب کرده است | کدک آن سایت را ثابت کنید |
| `�` | هر حرف غیر ASCII | بایت‌ها با ⁦UTF-8⁩ و `errors="replace"` رمزگشایی شده‌اند | در متن از دست رفته است؛ بایت‌های خام را دوباره رمزگشایی کنید |
| `?` | هر حرفی | متن با `errors="replace"` به کدکی رمزگذاری شده که آن حرف را ندارد | از دست رفته است؛ با ⁦UTF-8⁩ بنویسید |
| یک مربع خالی | حرف درست | فونت گلیف آن حرف را ندارد (PDF، ترمینال قدیمی) | فونت را عوض کنید، نه رمزگذاری را |

متن فارسی در حالت ردیف نخست شکلی آشنا پیدا می‌کند: اگر بایت‌های ⁦UTF-8⁩ واژه «سلام» با ⁦cp1252⁩ خوانده شوند، `Ø³Ù„Ø§Ù…` نمایش داده می‌شود.

## آیا در ⁦Python 3⁩ هنوز به `# -*- coding: utf-8 -*-` نیاز دارید؟

نه. [⁦PEP 3120⁩](https://peps.python.org/pep-3120/) در ⁦Python 3.0⁩ ⁦UTF-8⁩ را رمزگذاری پیش‌فرض فایل‌های منبع کرد، پس این خط یادگار ⁦Python 2⁩ است. اگر رشته‌های متنی فایل `.py` شما هنوز خراب می‌شوند، ویرایشگرتان آن را در یک صفحه کد قدیمی ذخیره کرده است (در ویندوز «ANSI»)؛ آن را با ⁦UTF-8⁩ ذخیره کنید. توصیه‌های دیگر دوران ⁦Python 2⁩، مانند پیشوند `u""` و `codecs.open()`، هم دیگر لازم نیستند.

## هنگام خواندن و نوشتن فایل رمزگذاری را چگونه تعیین کنیم؟

هر بار آن را بدهید: `open("cities.txt", "w", encoding="utf-8")`. بدون آن، ⁦Python 3.13⁩ در ویندوز صفحه کد ANSI را به کار می‌برد. روی دستگاه ویندوز ترکی ما `locale.getpreferredencoding(False)` مقدار `cp1254` را برگرداند و یک `open("cities.txt", "w")` ساده واژه `Çankırı` را به شکل `C7 61 6E 6B FD 72 FD` نوشت. ویندوز انگلیسی ⁦cp1252⁩ را به کار می‌برد که `ı` ندارد، پس همان نوشتن با خطای `'charmap' codec can't encode character '\u0131'` شکست می‌خورد.

برای فایل‌هایی که دریافت می‌کنید، کدکی را به کار ببرید که با آن نوشته شده‌اند: `cp1254` برای خروجی‌های قدیمی ویندوز و Excel ترکی، `cp1252` برای خروجی‌های اروپای غربی و `cp1256` برای عربی و فارسی. آرگومان `errors` تعیین می‌کند با بایت‌هایی که در کدک جا نمی‌شوند چه شود:

- `strict`، یعنی پیش‌فرض، خطا می‌دهد و تا وقتی دنبال کدک درست هستید همین را می‌خواهید.
- `replace` به‌جای هر بایت خراب `�` می‌گذارد تا ببینید خرابی کجاست.
- `backslashreplace` بایت‌های خراب را به شکل `\xfd` قابل دیدن نگه می‌دارد.
- `ignore` آن‌ها را بی‌صدا حذف می‌کند: `Iğdır` که با ⁦cp1254⁩ نوشته و با ⁦UTF-8⁩ خوانده شد، به `Idr` تبدیل شد.

توصیه «فقط ⁦latin-1⁩ را امتحان کنید» هم به همین شکل شکست می‌خورد. ⁦ISO-8859-1⁩ هر 256 مقدار بایت را به نویسه نگاشت می‌کند، پس هرگز خطا نمی‌دهد و متن ترکی بی‌صدا به `Çankýrý` تبدیل می‌شود.

### خطای ⁦UnicodeDecodeError: 'utf-8' codec can't decode byte⁩

فایل ⁦UTF-8⁩ نیست و بایتی که در پیام آمده سرنخ است: `0xfd`، `0xfe` یا `0xf0` در داده ترکی به ⁦cp1254⁩ اشاره می‌کند، در حالی که `0xe9` (é) یا `0xfc` (ü) نشان ⁦cp1252⁩ است. در فایل فارسی ⁦cp1256⁩ واژه «پروکسی» با بایت `0x81` (پ) آغاز می‌شود و ⁦UTF-8⁩ روی همان بایت با `invalid start byte` متوقف شد. در pandas کدک را بدهید: `pd.read_csv("export.csv", sep=";", encoding="cp1254")`.

## خطای ⁦UnicodeEncodeError: 'charmap' codec can't encode character⁩ در ویندوز

کنسول تعاملی ویندوز از ⁦Python 3.6⁩ به بعد (⁦PEP 528⁩) با ⁦UTF-8⁩ می‌نویسد، اما خروجی‌ای که به فایل یا pipe هدایت شود صفحه کد ANSI را به کار می‌برد. حرف‌های ترکی در ⁦cp1254⁩ جا می‌شوند، اما پیکان جا نمی‌شود: `python script.py > out.txt` با `print("Istanbul → Ankara")` با خطای `'charmap' codec can't encode character '\u2192'` شکست خورد. سه راه‌حل کار کرد:

- `set PYTHONUTF8=1` (در PowerShell: `$env:PYTHONUTF8=1`) حالت ⁦UTF-8⁩ را برای فایل‌ها و جریان‌های استاندارد روشن می‌کند.
- `python -X utf8 script.py` همین کار را برای یک اجرا انجام می‌دهد.
- `PYTHONIOENCODING=utf-8` فقط جریان‌های استاندارد را تغییر می‌دهد، نه `open()` را.

[راهنمای پایتون در ویندوز](https://docs.python.org/3/using/windows.html#utf-8-mode) حالت ⁦UTF-8⁩ را مستند کرده است. [⁦PEP 686⁩](https://peps.python.org/pep-0686/) حالت ⁦UTF-8⁩ را از ⁦Python 3.15⁩ پیش‌فرض می‌کند که انتشار نهایی آن برای 1 اکتبر 2026 برنامه‌ریزی شده است. تا وقتی همه دستگاه‌ها این نسخه را اجرا نکنند، `encoding="utf-8"` را در کدتان نگه دارید.

گونه `'latin-1' codec can't encode character` معمولاً از یک هدر HTTP می‌آید. `http.client`، که Requests در لایه زیرین به کار می‌برد، مقدار هدرها را با ⁦Latin-1⁩ رمزگذاری می‌کند، پس هدری با مقدار `Iğdır` به همین شکل شکست خورد. چنین مقدارهایی را با کدگذاری درصدی بفرستید یا در بدنه درخواست قرار دهید.

## رمزگذاری یک صفحه وب از کجا می‌آید؟

مرورگر رمزگذاری صفحه را به این ترتیب تعیین می‌کند:

1. نشانه ترتیب بایت (BOM) در ابتدای بدنه.
2. پارامتر `charset` در هدر `Content-Type`.
3. تگ `<meta charset>` که به گفته [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta) باید در 1024 بایت نخست باشد.
4. حدسی بر پایه خود بایت‌ها.

Requests فقط هدر را می‌خواند. برای نوع `text/*` بدون charset، [مستندات Requests](https://requests.readthedocs.io/en/latest/user/advanced/#encodings) می‌گوید از ⁦RFC 2616⁩ پیروی می‌کند و ⁦ISO-8859-1⁩ را به کار می‌برد، هرچند [⁦RFC 7231⁩](https://www.rfc-editor.org/rfc/rfc7231.html#appendix-B) این پیش‌فرض را در 2014 حذف کرد. پاسخ JSON بدون charset به‌صورت ⁦UTF-8⁩ خوانده می‌شود و نوع‌های دیگر حدس زده می‌شوند. در صفحه‌های آزمایشی ما که با `text/html` ساده ارائه می‌شدند، `r.encoding` هر بار `ISO-8859-1` بود. به همین دلیل یک صفحه می‌تواند در مرورگر درست و در اسکریپت شما نادرست دیده شود.

### آیا ⁦windows-1254⁩ و ⁦ISO-8859-9⁩ یکی هستند؟

تقریباً. هر دو حرف‌های ترکی را روی بایت‌های یکسانی از `0xA0` تا `0xFF` قرار می‌دهند. در بازه `0x80` تا `0x9F`، ⁦windows-1254⁩ نویسه‌های `€`، `“`، `”` و `…` را دارد، در حالی که ⁦ISO-8859-9⁩ آنجا کدهای کنترلی نامرئی دارد. [استاندارد Encoding از WHATWG](https://encoding.spec.whatwg.org/) به مرورگرها می‌گوید `iso-8859-9` را ⁦windows-1254⁩ و `iso-8859-1` را ⁦windows-1252⁩ بخوانند؛ پایتون آن‌ها را کدک‌های جداگانه می‌داند. در صفحه ما که برچسب `iso-8859-9` داشت، Beautiful Soup خروجی `\x93Kampanya\x94 10\x80` داد و با `from_encoding="cp1254"` همان بایت‌ها به `“Kampanya” 10€` تبدیل شدند. چنین صفحه‌هایی را با ⁦cp1254⁩ یا ⁦cp1252⁩ رمزگشایی کنید.

### صفحه HTML خودتان charset را چگونه اعلام کند؟

فایل را با ⁦UTF-8⁩ ذخیره کنید، `<meta charset="utf-8">` را در بالای `<head>` بگذارید و مطمئن شوید هدر `Content-Type` سرور charset دیگری را نام نمی‌برد، چون هدر بر تگ برتری دارد.

## response.encoding، apparent_encoding یا Beautiful Soup؟

هر گزینه نشانه متفاوتی را می‌خواند:

- `r.encoding` از هدر می‌آید. تنظیم `r.encoding = "cp1254"` یک سایت شناخته‌شده را درست می‌کند؛ تنظیم `"utf-8"` برای همه سایت‌ها صفحه‌های ⁦windows-1254⁩ را خراب می‌کند (در صفحه آزمایشی ما `�ank�r�`).
- `r.apparent_encoding` حدسی است که charset-normalizer از روی بدنه می‌زند. برای یکی از صفحه‌های ترکی ما `Windows-1254` گفت و برای دو صفحه دیگر `windows-1250` که `ş` را به `ţ` تبدیل می‌کند.
- `BeautifulSoup(r.content, "html.parser")` خودش تگ `<meta>` را می‌خواند. بایت بدهید، نه `r.text`؛ بخش تجزیه در [آموزش Beautiful Soup](/fa/blog/beautifulsoup-tutorial) آمده است.

ترتیب ما از مرورگر پیروی می‌کند: BOM، charset در هدر، تگ `<meta>` و سپس حدس؛ و ثبت کنید کدام را به کار برده‌اید. HTTPX رفتار دیگری دارد: نسخه 0.28.1 وقتی هدر charset ندارد ⁦UTF-8⁩ را فرض می‌کند و صفحه ⁦cp1254⁩ ما به شکل `�ank�r�` درآمد ([مقایسه HTTPX، Requests و AIOHTTP](/fa/blog/httpx-vs-requests-vs-aiohttp)).

## اسکرپر پایتونی که صفحه‌ها را مانند مرورگر رمزگشایی می‌کند

اسکریپت هر URL را یک بار دریافت می‌کند، کدک را به همان ترتیب انتخاب می‌کند، برچسب‌های ISO را مانند مرورگرها نگاشت می‌کند و عنوان‌ها را در یک فایل CSV برای Excel می‌نویسد. به `pip install requests beautifulsoup4` نیاز دارد. تلاش دوباره نمی‌کند و IP را نمی‌چرخاند؛ برای این دو، [کدهای وضعیت HTTP در وب اسکرپینگ](/fa/blog/http-status-codes-web-scraping) و [چرخاندن پروکسی در پایتون](/fa/blog/how-to-rotate-proxies-in-python) را ببینید.

```python
"""Fetch pages, decode each one the way a browser would, and save the headings to a CSV for Excel."""
import csv
import logging
import sys
from email.message import Message

import requests
from bs4 import BeautifulSoup
from bs4.dammit import EncodingDetector

PROXY = None  # for example "http://user:pass@pr.proxynet.io:8000"
USER_AGENT = "heading-reader/1.0 (+https://example.com/bot)"

# Browsers read these labels as Windows code pages (WHATWG Encoding Standard); Python does not.
BROWSER_ALIASES = {
    "iso-8859-1": "cp1252", "iso8859-1": "cp1252", "latin1": "cp1252", "latin-1": "cp1252",
    "us-ascii": "cp1252", "ascii": "cp1252",
    "iso-8859-9": "cp1254", "iso8859-9": "cp1254", "latin5": "cp1254",
}

log = logging.getLogger("headings")

def header_charset(resp):
    """The charset from the Content-Type header, or None. Requests' own r.encoding
    would say ISO-8859-1 here for any text/* type without a charset."""
    msg = Message()
    msg["content-type"] = resp.headers.get("Content-Type", "")
    return msg.get_param("charset")

def pick_codec(resp):
    """Return (codec, where it came from): a BOM, the header, <meta charset>, then a guess."""
    bom = EncodingDetector.strip_byte_order_mark(resp.content)[1]
    if bom:
        return bom, "bom"
    declared = header_charset(resp)
    source = "header"
    if not declared:
        declared = EncodingDetector.find_declared_encoding(resp.content, is_html=True)
        source = "meta"
    if not declared:
        return resp.apparent_encoding or "utf-8", "guess"
    return BROWSER_ALIASES.get(declared.lower(), declared), source

def clean(text):
    """Collapse runs of whitespace, including the non-breaking space U+00A0."""
    return " ".join(text.split())

def read_headings(session, url):
    resp = session.get(url, timeout=20)
    resp.raise_for_status()
    codec, source = pick_codec(resp)
    soup = BeautifulSoup(resp.content, "html.parser", from_encoding=codec)
    level = logging.WARNING if source == "guess" else logging.INFO
    log.log(level, "%s: %s from %s", url, codec, source)
    if soup.contains_replacement_characters:
        log.warning("%s: some bytes did not fit %s and became U+FFFD", url, codec)
    return [(url, codec, source, clean(h.get_text())) for h in soup.select("h1, h2")]

def main(urls, out="headings.csv"):
    session = requests.Session()
    session.headers["User-Agent"] = USER_AGENT
    if PROXY:
        session.proxies = {"http": PROXY, "https": PROXY}
    rows = []
    for url in urls:
        try:
            rows += read_headings(session, url)
        except requests.RequestException as exc:
            log.error("%s: %s", url, exc)
    # utf-8-sig writes a BOM first, so Excel recognises the file as UTF-8
    with open(out, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        writer.writerow(["url", "codec", "source", "heading"])
        writer.writerows(rows)
    log.info("%d headings written to %s", len(rows), out)

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(message)s")
    main(sys.argv[1:] or ["https://example.com/"])
```

آن را با ⁦Python 3.13⁩، ⁦Requests 2.34.2⁩ و ⁦Beautiful Soup 4.15.0⁩ از راه یک پروکسی HTTP محلی اجرا کردیم. صفحه‌های آزمایشی محلی، به‌جز `utf8hdr`، با `text/html` و بدون charset ارائه می‌شدند و آخرین URL یک صفحه عمومی است:

```text
INFO    http://127.0.0.1:8057/cp1254: windows-1254 from meta
INFO    http://127.0.0.1:8057/iso9: cp1254 from meta
INFO    http://127.0.0.1:8057/utf8meta: utf-8 from meta
INFO    http://127.0.0.1:8057/utf8hdr: utf-8 from header
INFO    http://127.0.0.1:8057/latin1: cp1252 from meta
WARNING http://127.0.0.1:8057/nometa: windows-1250 from guess
WARNING https://example.com/: ascii from guess
INFO    12 headings written to headings.csv
```

همه صفحه‌هایی که charset در هدر یا تگ `<meta>` داشتند درست درآمدند، از جمله `“quoted” 5€` در صفحه‌ای که برچسب ⁦ISO-8859-1⁩ داشت. نقطه ضعف صفحه‌ای است که هیچ‌کدام را نداشت: حدس ⁦windows-1250⁩ بود و در CSV مقدارهای `Çankýrý` و `ţubat` نشست.

اگر سایت‌های زیادی را در یک کشور طبق زمان‌بندی می‌خوانید، یک [پروکسی مسکونی](https://proxynet.io/fa/residential-proxy) در همان کشور صفحه‌هایی را برمی‌گرداند که بازدیدکنندگان محلی می‌بینند؛ منطق انتخاب کدک همان می‌ماند.

## `can't decode byte 0x8b` و `0xa0` یعنی چه؟

### خطای ⁦UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1⁩

جریان gzip با `1F 8B` آغاز می‌شود ([⁦RFC 1952⁩](https://www.rfc-editor.org/rfc/rfc1952.html))، پس `0x8b` در موقعیت 1 یعنی داده فشرده به‌عنوان متن رمزگشایی می‌شود؛ `gzip.compress(...).decode("utf-8")` دقیقاً همین پیام را به ما داد. در اسکرپینگ این اتفاق وقتی می‌افتد که `r.raw` را مستقیم بخوانید، یا هدر `Accept-Encoding` کپی‌شده را با `urllib.request` بفرستید که هرگز پاسخ را از حالت فشرده بیرون نمی‌آورد. Requests خودش gzip را باز می‌کند، پس هدر کپی‌شده را حذف کنید و `r.content` را به کار ببرید. مقدارهای کپی‌شده `br` یا `zstd` هم شکست می‌خورند، مگر اینکه بسته اختیاری Brotli یا Zstandard برای ⁦urllib3⁩ نصب شده باشد.

### خطای ⁦UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa0⁩

در ⁦cp1252⁩، ⁦cp1254⁩ و ⁦ISO-8859-1⁩، فاصله نشکن ⁦U+00A0⁩ تک‌بایت `A0` است و ⁦UTF-8⁩ هرگز هیچ نویسه‌ای را با این بایت آغاز نمی‌کند. فایل را با کدک واقعی‌اش بخوانید. اگر این نویسه از قبل در متن شماست، `"10\xa0kg".split()` آن را حذف می‌کند، `.split(" ")` نه، و `unicodedata.normalize("NFKC", s)` آن را به فاصله ساده تبدیل می‌کند (NFKC نویسه‌هایی مانند `²` را هم به `2` تبدیل می‌کند).

## متنی را که از قبل خراب شده چگونه ترمیم کنیم؟

اگر هیچ بایتی از دست نرفته باشد، گام اشتباه را برعکس کنید:

- متنی مانند `Ä±ÅŸÄŸ` که از صفحه نمایش یا صفحه‌گسترده کپی شده: `s.encode("cp1252").decode("utf-8")` نتیجه `ışğ` را می‌دهد. همین فرمان `Ø³Ù„Ø§Ù…` را هم به «سلام» برمی‌گرداند.
- همین خرابی در `r.text` از Requests یک نویسه کنترلی (`\x9f`) را پنهان می‌کند که ⁦cp1252⁩ نمی‌تواند رمزگذاری کند، پس `s.encode("latin-1").decode("utf-8")` را به کار ببرید. ⁦cp1252⁩ روی آن با خطای `'charmap' codec can't encode character '\x9f'` شکست خورد.
- متن ⁦windows-1254⁩ که با ⁦ISO-8859-1⁩ خوانده شده، مانند `Çankýrý`: `s.encode("latin-1").decode("cp1254")` نتیجه `Çankırı` را می‌دهد.

برای داده‌های بزرگ یا ترکیبی، [ftfy](https://pypi.org/project/ftfy/) نسخه 6.3.1 خرابی‌های ⁦UTF-8⁩ را ترمیم می‌کند: `ftfy.fix_text("Ä±ÅŸÄŸ")` مقدار `ışğ` را برگرداند. اما `Çankýrý` را دست‌نخورده گذاشت، چون شبیه متن لاتین معتبر است؛ پس اشتباه‌های صفحه کد را دستی درست کنید. متنی که به‌جای حرف‌ها `?` یا `�` دارد قابل ترمیم نیست؛ منبع را دوباره دریافت کنید.

## چرا Excel در فایل CSV با ⁦UTF-8⁩ نویسه‌های خراب نشان می‌دهد؟

Excel فایل CSV با ⁦UTF-8⁩ را با دوبار کلیک فقط وقتی درست باز می‌کند که فایل با نشانه ترتیب بایت آغاز شود، همان‌طور که [صفحه پشتیبانی Microsoft](https://support.microsoft.com/en-us/office/opening-csv-utf-8-files-correctly-in-excel-8a935af5-3416-4edd-ba7e-3dfd2bc4a032) اشاره می‌کند. `encoding="utf-8-sig"` سه بایت `EF BB BF` را اضافه می‌کند؛ در pandas هم `df.to_csv("out.csv", encoding="utf-8-sig")`. چنین فایل‌هایی را با `utf-8-sig` هم بخوانید: با `utf-8` ساده، ماژول `csv` ستون نخستی به نام `\ufeffşehir` به ما داد، در حالی که pandas در هر دو حالت BOM را حذف کرد. جریان کامل از اسکرپ تا Excel در [استخراج داده از وب‌سایت](/fa/blog/extract-data-from-website) آمده است.

## چرا `"I".lower()` مقدار `"ı"` را برنمی‌گرداند؟

`str.lower()` نگاشت پیش‌فرض یونیکد را به کار می‌برد که زبان را در نظر نمی‌گیرد. برای ترکی این کار دو بار نادرست است: `"ISPARTA".lower()` به‌جای `ısparta` مقدار `isparta` را برمی‌گرداند و `"İ".lower()` حرف `i` را همراه با یک نقطه ترکیبی (⁦U+0307⁩) برمی‌گرداند. `casefold()` هم همین کمبود را دارد. نخست دو حرف ویژه را نگاشت کنید:

```python
TR_LOWER = str.maketrans({"I": "ı", "İ": "i"})
TR_UPPER = str.maketrans({"i": "İ", "ı": "I"})

print("ISPARTA İZMİR".translate(TR_LOWER).lower())  # ısparta izmir
print("istanbul ışık".translate(TR_UPPER).upper())  # İSTANBUL IŞIK
```

عددها و تاریخ‌ها هم از قواعد محلی پیروی می‌کنند. قیمتی ترکی مانند `1.299,90` پس از `s.replace(".", "").replace(",", ".")` به عدد اعشاری تبدیل می‌شود و pandas پارامترهای `decimal=","` و `thousands="."` را دارد. در اسکرپر از `locale.setlocale()` پرهیز کنید، چون کل پروسه را تغییر می‌دهد. پاک‌سازی کامل قیمت‌ها در [رصد قیمت رقبا](/fa/blog/competitor-price-tracking) آمده است.

## کاربردها

- **رصد قیمت:** فروشگاه‌هایی که هنوز صفحه‌هایشان را با صفحه کدهای قدیمی ارائه می‌دهند ([رصد قیمت رقبا](/fa/blog/competitor-price-tracking)).
- **جدول‌ها به Excel:** فایل CSV که با حرف‌های درست باز می‌شود ([استخراج داده از وب‌سایت](/fa/blog/extract-data-from-website)).
- **اسکرپرهای PHP:** `DOMDocument` راه‌حل خودش را دارد ([وب اسکرپینگ با PHP](/fa/blog/php-web-scraping)).
- **خروجی‌های Scrapy:** `FEED_EXPORT_ENCODING` کدک خروجی را تعیین می‌کند ([Scrapy با پروکسی](/fa/blog/scrapy-proxy)).
- **کارهای .NET:** `HttpClient` با charset به شیوه خودش رفتار می‌کند ([استخراج داده با C#](/fa/blog/csharp-web-scraping)).
- **خزش در سایت‌های فراوان:** کدک هر صفحه را ثبت کنید ([خزنده وب](/fa/web-crawler)).

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

- **خاموش کردن خطا با `errors="ignore"` یا ⁦latin-1⁩.** اسکریپت اجرا می‌شود و حرف‌ها ناپدید یا عوض می‌شوند.
- **ثابت نوشتن `r.encoding = "utf-8"` برای همه سایت‌ها.** برخی صفحه‌ها را درست و صفحه‌های ⁦windows-1254⁩ را خراب می‌کند.
- **دو بار رمزگشایی.** فراخوانی `.encode().decode()` روی متنی که از قبل درست رمزگشایی شده، خطای تازه‌ای به متن سالم اضافه می‌کند.
- **پذیرفتن `iso-8859-9` یا `iso-8859-1` همان‌طور که اعلام شده‌اند.** مرورگرها ⁦windows-1254⁩ و ⁦windows-1252⁩ را به کار می‌برند و علامت‌های نقل‌قول و نشانه یورو تفاوت را آشکار می‌کنند.
- **خواندن فایل `utf-8-sig` به‌صورت `utf-8`.** نام ستون نخست با یک نویسه نامرئی آغاز می‌شود و جست‌وجو بر اساس آن نام شکست می‌خورد.
- **اشتباه گرفتن رمزگذاری نویسه با کدگذاری URL.** `%40` در رمز پروکسی کدگذاری درصدی است و موضوعی جداگانه است ([نویسه‌های ویژه در رمز پروکسی](/fa/blog/nodejs-proxy)).

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

| نیاز | پیشنهاد |
|---|---|
| متن غیر ASCII در فایل `.py` خودتان | آن را با ⁦UTF-8⁩ ذخیره کنید؛ خط coding لازم نیست |
| خواندن یا نوشتن فایل | `encoding="utf-8"`؛ صفحه کد قدیمی فقط برای فایل‌هایی که با آن نوشته شده‌اند |
| خطای `'charmap'` در خروجی هدایت‌شده در ویندوز | `PYTHONUTF8=1` یا `python -X utf8`؛ از ⁦Python 3.15⁩ پیش‌فرض |
| HTML بدون charset در هدر | `r.content` را به Beautiful Soup بدهید و کدک را ثبت کنید |
| نه charset در هدر و نه تگ `<meta>` | کدک سایت را ثابت کنید؛ `apparent_encoding` فقط به‌عنوان آخرین راه |
| صفحه می‌گوید `iso-8859-9` یا `iso-8859-1` | مانند مرورگر با ⁦cp1254⁩ یا ⁦cp1252⁩ رمزگشایی کنید |
| متن از قبل به شکل `Ã©` یا `Ä±` درآمده | `.encode("cp1252").decode("utf-8")` یا `ftfy.fix_text` |
| نتیجه در Excel باز می‌شود | CSV را با `utf-8-sig` بنویسید |

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

### آیا برای کار با حروف فارسی و دیگر نویسه‌های غیرانگلیسی در پایتون به کتابخانه نیاز دارم؟

نه. یک `str` در ⁦Python 3⁩ یونیکد است و کتابخانه استاندارد برای صفحه کدهای رایج کدک دارد ([رمزگذاری‌های استاندارد](https://docs.python.org/3/library/codecs.html#standard-encodings)). charset-normalizer، که همراه Requests نصب می‌شود، کدک‌های ناشناخته را حدس می‌زند و ftfy متن‌های mojibake را ترمیم می‌کند.

### فایل ⁦cp1252⁩ یا ⁦cp1254⁩ را چگونه به ⁦UTF-8⁩ تبدیل کنم؟

آن را با کدک قدیمی‌اش بخوانید و با کدک تازه بنویسید: `Path("new.txt").write_text(Path("old.txt").read_text(encoding="cp1254"), encoding="utf-8")` که در آن `Path` از `pathlib` می‌آید. پیش از پاک کردن فایل اصلی، چند نام را بررسی کنید.

### آیا `errors="ignore"` خطای UnicodeDecodeError را برطرف می‌کند؟

فقط آن را پنهان می‌کند. بایت‌هایی که جا نمی‌شوند بدون هشدار حذف می‌شوند، پس نام‌ها حرف از دست می‌دهند. به‌جای آن کدک درست را پیدا کنید.

### چگونه بفهمم یک فایل با چه رمزگذاری‌ای نوشته شده است؟

یک فایل متنی، مگر اینکه با BOM آغاز شود، کدک خود را جایی ثبت نمی‌کند، پس هر ابزاری فقط حدس می‌زند. `charset_normalizer.from_path("old.txt").best().encoding` برای فایل آزمایشی ترکی ما `cp1254` را برگرداند. حدس را با رمزگشایی و خواندن چند واژه که حرف‌های محلی دارند تأیید کنید.

### تفاوت `utf-8` و `utf-8-sig` چیست؟

`utf-8-sig` هنگام نوشتن یک BOM (`EF BB BF`) می‌نویسد و هنگام خواندن از آن می‌گذرد. آن را برای فایل‌های CSV به کار ببرید که دیگران در Excel باز می‌کنند.

### خطای UnicodeDecodeError را در `read_csv` از pandas چگونه برطرف کنم؟

کدک فایل را بدهید: `encoding="cp1254"` برای خروجی ویندوز ترکی، `cp1252` برای خروجی اروپای غربی و `utf-8-sig` اگر فایل BOM دارد. `encoding_errors="replace"` خطا را متوقف می‌کند، اما حرف‌ها از دست می‌روند.

## خلاصه

خطاهای رمزگذاری در پایتون به یک قاعده برمی‌گردند: بایت‌ها را با کدکی رمزگشایی کنید که با آن نوشته شده‌اند و خروجی خودتان را با ⁦UTF-8⁩ بنویسید. متن خراب را بخوانید تا دو کدکی را که با هم اشتباه گرفته شده‌اند پیدا کنید، روی هر `open()` مقدار `encoding` را تعیین کنید، تا وقتی ⁦Python 3.15⁩ این کار را برایتان انجام دهد در ویندوز حالت ⁦UTF-8⁩ را به کار ببرید و صفحه‌های وب را به‌صورت بایت به Beautiful Soup بدهید. برای کارهای اسکرپینگی که از سایت‌های محلی فراوان متن تمیز لازم دارند، راه‌حل [استخراج داده](/fa/data-scraping) ما را ببینید.
