ProxynetProxynet

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

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

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

Acar Diveroli
نویسنده: Acar Diveroli
سه لایه ایزومتریک: بایت‌های ⁦C4 B1⁩ در پایین، کدک آبی ⁦UTF-8⁩ در میانه و ⁦U+0131⁩ در بالا؛ در چپ، صفحه خط‌چین ⁦ISO-8859-1⁩
فهرست مطالب

اسکریپتی آگهی‌های مناقصه را از وب‌سایت قدیمی یک شهرداری در ترکیه جمع می‌کند. عنوانی که در مرورگر Ç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⁩ آمده‌اند.

تفاوت 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⁩ در ⁦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() را.

راهنمای پایتون در ویندوز حالت ⁦UTF-8⁩ را مستند کرده است. ⁦PEP 686⁩ حالت ⁦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 باید در 1024 بایت نخست باشد.
  4. حدسی بر پایه خود بایت‌ها.

Requests فقط هدر را می‌خواند. برای نوع text/* بدون charset، مستندات Requests می‌گوید از ⁦RFC 2616⁩ پیروی می‌کند و ⁦ISO-8859-1⁩ را به کار می‌برد، هرچند ⁦RFC 7231⁩ این پیش‌فرض را در 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 به مرورگرها می‌گوید 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 آمده است.

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

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

اسکریپت هر URL را یک بار دریافت می‌کند، کدک را به همان ترتیب انتخاب می‌کند، برچسب‌های ISO را مانند مرورگرها نگاشت می‌کند و عنوان‌ها را در یک فایل CSV برای Excel می‌نویسد. به pip install requests beautifulsoup4 نیاز دارد. تلاش دوباره نمی‌کند و IP را نمی‌چرخاند؛ برای این دو، کدهای وضعیت HTTP در وب اسکرپینگ و چرخاندن پروکسی در پایتون را ببینید.

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 نشست.

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

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

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

جریان gzip با 1F 8B آغاز می‌شود (⁦RFC 1952⁩)، پس 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 نسخه 6.3.1 خرابی‌های ⁦UTF-8⁩ را ترمیم می‌کند: ftfy.fix_text("ışğ") مقدار ışğ را برگرداند. اما Çankýrý را دست‌نخورده گذاشت، چون شبیه متن لاتین معتبر است؛ پس اشتباه‌های صفحه کد را دستی درست کنید. متنی که به‌جای حرف‌ها ? یا دارد قابل ترمیم نیست؛ منبع را دوباره دریافت کنید.

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

Excel فایل CSV با ⁦UTF-8⁩ را با دوبار کلیک فقط وقتی درست باز می‌کند که فایل با نشانه ترتیب بایت آغاز شود، همان‌طور که صفحه پشتیبانی Microsoft اشاره می‌کند. 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 در استخراج داده از وب‌سایت آمده است.

چرا "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() پرهیز کنید، چون کل پروسه را تغییر می‌دهد. پاک‌سازی کامل قیمت‌ها در رصد قیمت رقبا آمده است.

کاربردها

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

  • خاموش کردن خطا با 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 در رمز پروکسی کدگذاری درصدی است و موضوعی جداگانه است (نویسه‌های ویژه در رمز پروکسی).

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

نیازپیشنهاد
متن غیر 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⁩ یونیکد است و کتابخانه استاندارد برای صفحه کدهای رایج کدک دارد (رمزگذاری‌های استاندارد). 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 بدهید. برای کارهای اسکرپینگی که از سایت‌های محلی فراوان متن تمیز لازم دارند، راه‌حل استخراج داده ما را ببینید.

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