---
title: "Encoding-Fehler in Python: Umlaute und UnicodeDecodeError"
description: "Kaputte Umlaute in Python entstehen, wenn Bytes mit dem falschen Codec dekodiert werden. So beheben Sie UnicodeDecodeError in Dateien, Konsole und Webseiten."
url: https://proxynet.io/de/blog/python-unicode-encoding-errors
date: 2026-09-24
author: "Acar Diveroli"
category: "Web Scraping, Anleitungen"
lang: de
---

# Encoding-Fehler in Python: Umlaute und UnicodeDecodeError

Ein Skript sammelt Ausschreibungen von der alten Website einer Stadtverwaltung in Türkiye. Im Browser lautet eine Überschrift `Çankırı İhale İlanı`, das Skript gibt aber `Çankýrý Ýhale Ýlaný` aus. Die Liste, die es auf einem Windows-Laptop gespeichert hat, lässt sich danach auf einem Linux-Server nicht öffnen: `'utf-8' codec can't decode byte 0xc7 in position 0: invalid continuation byte`. Beide Probleme haben dieselbe Ursache: Die Bytes wurden mit einer Codetabelle geschrieben und mit einer anderen gelesen.

Dieser Leitfaden behebt die Ursache überall, wo sie auftritt: in Quelldateien, bei `open()`, in der Windows-Konsole, bei Seiten, die Sie mit Requests und Beautiful Soup abrufen, und in CSV-Dateien für Excel. Er zeigt außerdem, wie Sie verstümmelten Text lesen und reparieren; dieselben Schritte helfen, wenn statt `ü` plötzlich `Ã¼` dasteht. Jeder Bytewert und jede Fehlermeldung unten stammt aus unseren eigenen Läufen mit Python 3.13.

> **Hinweis: Kurzantwort**
>
> In Python 3 ist ein `str` bereits Unicode. Zeichen gehen nur kaputt, wenn Bytes mit dem falschen Codec zu Text dekodiert oder Text mit dem falschen Codec zu Bytes kodiert wird. Geben Sie jedem `open()`-Aufruf `encoding="utf-8"` mit und verwenden Sie eine alte Codepage wie `cp1252` oder `cp1254` nur für Dateien, die damit geschrieben wurden. Scheitert unter Windows umgeleitete Ausgabe mit einem `'charmap'`-Fehler, setzen Sie `PYTHONUTF8=1`. Requests liest eine `text/html`-Antwort ohne Charset im Header als ISO-8859-1; übergeben Sie deshalb `r.content` an Beautiful Soup oder setzen Sie `r.encoding` selbst. CSV-Dateien für Excel schreiben Sie mit `utf-8-sig`.

## Was ist der Unterschied zwischen str und bytes in Python?

Ein `str` enthält Zeichen, jedes mit einem Unicode-Codepoint: Das türkische punktlose `ı` ist U+0131, `ü` ist U+00FC. Festplatten und Netzwerke speichern nur Bytes, also wird Text auf dem Weg nach draußen kodiert und auf dem Weg hinein dekodiert. Der Codec legt fest, welche Bytes für welches Zeichen stehen. In UTF-8 besteht `ı` aus zwei Bytes, `C4 B1`, und `ü` ebenfalls aus zwei, `C3 BC`. In windows-1254, der alten türkischen Windows-Codepage, ist `ı` ein einziges Byte, `FD`. Das westeuropäische cp1252 speichert `ü` als `FC`, kennt `ı` aber ebenso wenig wie ISO-8859-1.

Verwenden Schreiber und Leser unterschiedliche Codecs, bleiben die Bytes erhalten, aber die Buchstaben ändern sich. Das nennt man Mojibake, im Deutschen oft Zeichensalat, und es lässt sich häufig rückgängig machen.

## Wie wird aus einem Zeichen ein falsches?

Jeder Kodierungsfehler folgt denselben Schritten:

1. **Ihr Text ist Unicode.** `"Çankırı"` besteht in Python aus sieben Codepoints.
2. **Er wird mit Codec A kodiert.** Ein türkisches Windows schreibt in cp1254 `C7 61 6E 6B FD 72 FD`.
3. **Nur die Bytes reisen mit.** Eine Datei, ein HTTP-Body oder eine Pipe sagt nichts über Codec A, außer ein Header, eine BOM oder ein `<meta>`-Tag ergänzt diese Angabe.
4. **Jemand dekodiert mit Codec B.** ISO-8859-1 bildet `FD` auf `ý` ab, daraus wird `Çankýrý`. UTF-8 kann `C7 61` nicht als Zeichen lesen und löst `UnicodeDecodeError` aus.
5. **Die Ausgabe wird erneut kodiert.** `print()` und `write()` verwenden den Codec der Konsole oder der Datei, und ein Zeichen, das dieser Codec nicht kennt, löst `UnicodeEncodeError` aus.

Am Traceback erkennen Sie, welche Richtung gescheitert ist.

## Was verrät der verstümmelte Text?

Die Form des Schadens zeigt auf das Codec-Paar.

| Was Sie sehen | Richtiger Text | Was passiert ist | Was zu tun ist |
|---|---|---|---|
| `cafÃ©`, `Ã¼`, `ÃŸ`, `Ä±`, `ÅŸ` | `café`, `ü`, `ß`, `ı`, `ş` | UTF-8-Bytes (`C3 BC`, `C4 B1`) als cp1252 oder ISO-8859-1 dekodiert | Die Bytes als UTF-8 dekodieren oder den Text reparieren (siehe unten) |
| `ý`, `þ`, `ð`, `Ý` | `ı`, `ş`, `ğ`, `İ` | windows-1254-Bytes (`FD`, `FE`, `F0`, `DD`) als ISO-8859-1 dekodiert, der Standard von Requests | `r.encoding = "cp1254"` setzen oder `r.content` an Beautiful Soup übergeben |
| `ţ` statt `ş` | `ş` | Eine Schätzung hat für eine türkische Seite windows-1250 gewählt | Den Codec für diese Website festlegen |
| `�` | Jeder Nicht-ASCII-Buchstabe | Bytes mit `errors="replace"` als UTF-8 dekodiert | Im Text verloren; die Rohbytes neu dekodieren |
| `?` | Jeder Buchstabe | Text mit `errors="replace"` in einen Codec kodiert, der das Zeichen nicht kennt | Verloren; mit UTF-8 schreiben |
| Ein leeres Kästchen | Der richtige Buchstabe | Der Schriftart fehlt die Glyphe (PDF, altes Terminal) | Die Schriftart wechseln, nicht die Kodierung |

## Braucht man `# -*- coding: utf-8 -*-` in Python 3 noch?

Nein. [PEP 3120](https://peps.python.org/pep-3120/) hat UTF-8 in Python 3.0 zur Standardkodierung von Quelldateien gemacht; die Zeile ist ein Überbleibsel aus Python 2. Gehen String-Literale in Ihrer `.py`-Datei trotzdem kaputt, hat Ihr Editor sie in einer alten Codepage gespeichert („ANSI“ unter Windows); speichern Sie sie als UTF-8. Andere Ratschläge aus Python 2, etwa `u""`-Präfixe und `codecs.open()`, brauchen Sie ebenfalls nicht.

## Wie legen Sie die Kodierung beim Lesen und Schreiben von Dateien fest?

Geben Sie sie jedes Mal an: `open("cities.txt", "w", encoding="utf-8")`. Ohne diese Angabe verwendet Python 3.13 unter Windows die ANSI-Codepage. Auf unserem türkischen Windows-Rechner lieferte `locale.getpreferredencoding(False)` den Wert `cp1254`, und ein einfaches `open("cities.txt", "w")` schrieb `Çankırı` als `C7 61 6E 6B FD 72 FD`. Ein deutsches oder englisches Windows verwendet cp1252, das kein `ı` kennt, deshalb scheitert derselbe Schreibvorgang dort mit `'charmap' codec can't encode character 'ı'`.

Für Dateien, die Sie erhalten, verwenden Sie den Codec, mit dem sie geschrieben wurden: `cp1254` für ältere türkische Windows- und Excel-Exporte, `cp1252` für westeuropäische, also auch deutsche, und `cp1256` für Arabisch und Persisch. Das Argument `errors` legt fest, was mit Bytes geschieht, die nicht passen:

- `strict`, der Standard, löst einen Fehler aus; genau das wollen Sie, solange Sie den richtigen Codec suchen.
- `replace` setzt `�` an die Stelle jedes fehlerhaften Bytes, sodass Sie sehen, wo der Schaden liegt.
- `backslashreplace` lässt fehlerhafte Bytes als `\xfd` sichtbar.
- `ignore` löscht sie stillschweigend: `Iğdır`, in cp1254 geschrieben und als UTF-8 gelesen, wurde zu `Idr`.

„Nimm einfach latin-1“ scheitert auf dieselbe Weise. ISO-8859-1 ordnet allen 256 Bytewerten ein Zeichen zu, löst also nie einen Fehler aus, und türkischer Text wird unbemerkt zu `Çankýrý`.

### UnicodeDecodeError: 'utf-8' codec can't decode byte

Die Datei ist kein UTF-8, und das Byte in der Meldung ist ein Hinweis: `0xfd`, `0xfe` oder `0xf0` in türkischen Daten deuten auf cp1254, `0xe4` (ä), `0xfc` (ü) oder `0xdf` (ß) auf cp1252. So meldet `b"Gr\xfc\xdfe".decode("utf-8")` den Fehler `can't decode byte 0xfc in position 2: invalid start byte`. In pandas geben Sie den Codec an: `pd.read_csv("export.csv", sep=";", encoding="cp1254")`, für einen deutschen Export `encoding="cp1252"`.

## UnicodeEncodeError: 'charmap' codec can't encode character unter Windows

Die interaktive Windows-Konsole schreibt seit Python 3.6 UTF-8 (PEP 528), aber in eine Datei oder Pipe umgeleitete Ausgabe verwendet die ANSI-Codepage. Türkische Buchstaben passen in cp1254, ein Pfeil nicht: `python script.py > out.txt` mit `print("Istanbul → Ankara")` scheiterte mit `'charmap' codec can't encode character '→'`. Drei Lösungen haben funktioniert:

- `set PYTHONUTF8=1` (`$env:PYTHONUTF8=1` in PowerShell) schaltet den UTF-8-Modus für Dateien und Standardstreams ein.
- `python -X utf8 script.py` tut dasselbe für einen einzelnen Lauf.
- `PYTHONIOENCODING=utf-8` ändert nur die Standardstreams, nicht `open()`.

Den UTF-8-Modus beschreibt der [Windows-Leitfaden](https://docs.python.org/3/using/windows.html#utf-8-mode) der Python-Dokumentation. [PEP 686](https://peps.python.org/pep-0686/) macht den UTF-8-Modus ab Python 3.15 zum Standard; die finale Version ist für den 1. Oktober 2026 geplant. Behalten Sie `encoding="utf-8"` in Ihrem Code, bis jeder Rechner diese Version nutzt.

Die Variante `'latin-1' codec can't encode character` stammt meist aus einem HTTP-Header. `http.client`, auf dem Requests aufbaut, kodiert Header-Werte als Latin-1, deshalb scheiterte ein Header-Wert `Iğdır` auf diese Weise. Kodieren Sie solche Werte mit Prozentkodierung oder senden Sie sie im Body.

## Woher kommt die Kodierung einer Webseite?

Ein Browser bestimmt die Kodierung einer Seite in dieser Reihenfolge:

1. Eine Byte Order Mark (BOM) am Anfang des Bodys.
2. Der Parameter `charset` im Header `Content-Type`.
3. Ein `<meta charset>`-Tag, das laut [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta) in den ersten 1.024 Bytes stehen muss.
4. Eine Schätzung anhand der Bytes.

Requests liest nur den Header. Für einen `text/*`-Typ ohne Charset folgt es laut [Requests-Dokumentation](https://requests.readthedocs.io/en/latest/user/advanced/#encodings) RFC 2616 und verwendet ISO-8859-1, obwohl [RFC 7231](https://www.rfc-editor.org/rfc/rfc7231.html#appendix-B) diesen Standardwert 2014 gestrichen hat. Eine JSON-Antwort ohne Charset wird als UTF-8 gelesen, andere Typen werden geschätzt. Auf unseren Testseiten, die als einfaches `text/html` ausgeliefert wurden, war `r.encoding` jedes Mal `ISO-8859-1`. Deshalb kann eine Seite im Browser richtig und in Ihrem Skript falsch aussehen, etwa mit `Ã¼` statt `ü` auf einer UTF-8-Seite.

### Sind windows-1254 und ISO-8859-9 dasselbe?

Fast. Beide legen die türkischen Buchstaben im Bereich `0xA0` bis `0xFF` auf dieselben Bytes. Zwischen `0x80` und `0x9F` hat windows-1254 Zeichen wie `€`, `“`, `”` und `…`, ISO-8859-9 dagegen unsichtbare Steuerzeichen. Der [WHATWG Encoding Standard](https://encoding.spec.whatwg.org/) weist Browser an, `iso-8859-9` als windows-1254 und `iso-8859-1` als windows-1252 zu lesen; Python behandelt sie als getrennte Codecs. Auf unserer mit `iso-8859-9` ausgezeichneten Seite erzeugte Beautiful Soup `\x93Kampanya\x94 10\x80`, und mit `from_encoding="cp1254"` wurden dieselben Bytes zu `“Kampanya” 10€`. Für deutsche Seiten gilt dasselbe Paar: Umlaute liegen in ISO-8859-1 und windows-1252 auf denselben Bytes, der Unterschied zeigt sich bei Anführungszeichen und beim Eurozeichen. Dekodieren Sie solche Seiten als cp1254 bzw. cp1252.

### Wie sollte Ihre eigene HTML-Seite ihr Charset angeben?

Speichern Sie die Datei als UTF-8, setzen Sie `<meta charset="utf-8">` an den Anfang von `<head>` und achten Sie darauf, dass der `Content-Type`-Header des Servers kein anderes Charset nennt, denn der Header gewinnt.

## response.encoding, apparent_encoding oder Beautiful Soup?

Jede Option liest ein anderes Signal:

- `r.encoding` stammt aus dem Header. `r.encoding = "cp1254"` repariert eine bekannte Website; `"utf-8"` für jede Website zu setzen, macht die windows-1254-Seiten kaputt (`�ank�r�` auf unserer Testseite).
- `r.apparent_encoding` ist die Schätzung von charset-normalizer anhand des Bodys. Sie lautete bei einer unserer türkischen Seiten `Windows-1254` und bei zwei anderen `windows-1250`, was `ş` in `ţ` verwandelt.
- `BeautifulSoup(r.content, "html.parser")` liest das `<meta>`-Tag selbst. Übergeben Sie Bytes, nicht `r.text`; die Parsing-Seite behandelt unser [Leitfaden zu Beautiful Soup](/de/blog/beautifulsoup-tutorial).

Unsere Reihenfolge folgt dem Browser: eine BOM, ein Charset im Header, das `<meta>`-Tag, dann die Schätzung; protokollieren Sie dabei, welche Quelle Sie verwendet haben. HTTPX verhält sich anders: Version 0.28.1 nimmt UTF-8 an, wenn der Header kein Charset nennt, und unsere cp1254-Seite kam als `�ank�r�` heraus ([HTTPX, Requests und AIOHTTP im Vergleich](/de/blog/httpx-vs-requests-vs-aiohttp)).

## Ein Python-Scraper, der Seiten wie ein Browser dekodiert

Das Skript ruft jede URL einmal ab, wählt den Codec in dieser Reihenfolge, bildet die ISO-Bezeichnungen so ab wie Browser und schreibt die Überschriften in eine CSV-Datei für Excel. Es braucht `pip install requests beautifulsoup4`. Wiederholungen und IP-Rotation enthält es nicht; dazu lesen Sie [HTTP-Statuscodes beim Web Scraping](/de/blog/http-status-codes-web-scraping) und [Proxys in Python rotieren](/de/blog/how-to-rotate-proxies-in-python).

```python
"""Seiten abrufen, jede wie ein Browser dekodieren und die Überschriften als CSV für Excel speichern."""
import csv
import logging
import sys
from email.message import Message

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

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

# Browser lesen diese Bezeichnungen als Windows-Codepages (WHATWG Encoding Standard), Python nicht.
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):
    """Das Charset aus dem Content-Type-Header oder None. Das eigene r.encoding von Requests
    würde hier bei jedem text/*-Typ ohne Charset ISO-8859-1 melden."""
    msg = Message()
    msg["content-type"] = resp.headers.get("Content-Type", "")
    return msg.get_param("charset")

def pick_codec(resp):
    """Gibt (Codec, Herkunft) zurück: eine BOM, der Header, <meta charset>, dann eine Schätzung."""
    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):
    """Fasst Leerraum zusammen, auch das geschützte Leerzeichen 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 schreibt zuerst eine BOM, damit Excel die Datei als UTF-8 erkennt
    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/"])
```

Wir haben es mit Python 3.13, Requests 2.34.2 und Beautiful Soup 4.15.0 über einen lokalen HTTP-Proxy ausgeführt. Die lokalen Testseiten wurden als `text/html` ohne Charset ausgeliefert, außer `utf8hdr`; die letzte URL ist eine öffentliche Seite:

```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
```

Jede Seite mit einem Charset im Header oder einem `<meta>`-Tag kam richtig heraus, auch `“quoted” 5€` auf der mit ISO-8859-1 ausgezeichneten Seite. Die Seite ohne beides ist die Schwachstelle: Die Schätzung lautete windows-1250, und in der CSV landeten `Çankýrý` und `ţubat`.

Wenn Sie viele Websites in einem Land nach Zeitplan lesen, liefert ein [Residential-Proxy](https://proxynet.io/de/residential-proxy) in diesem Land die Seiten, die auch lokale Besucher sehen; die Codec-Logik bleibt dieselbe.

## Was bedeuten „can't decode byte 0x8b“ und „0xa0“?

### UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1

Ein gzip-Stream beginnt mit `1F 8B` ([RFC 1952](https://www.rfc-editor.org/rfc/rfc1952.html)); `0x8b` an Position 1 bedeutet also, dass komprimierte Daten als Text dekodiert werden. `gzip.compress(...).decode("utf-8")` lieferte uns genau diese Meldung. Beim Scraping passiert das, wenn Sie `r.raw` direkt lesen oder mit `urllib.request` einen kopierten `Accept-Encoding`-Header senden, denn `urllib.request` dekomprimiert nie. Requests entpackt gzip selbst; entfernen Sie also den kopierten Header und verwenden Sie `r.content`. Kopierte Werte wie `br` oder `zstd` scheitern ebenfalls, sofern urllib3 nicht das optionale Brotli- bzw. Zstandard-Paket hat.

### UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa0

In cp1252, cp1254 und ISO-8859-1 ist das geschützte Leerzeichen U+00A0 das einzelne Byte `A0`, mit dem in UTF-8 nie ein Zeichen beginnt. Lesen Sie die Datei mit ihrem echten Codec. Steht das Zeichen bereits in Ihrem Text, entfernt `"10\xa0kg".split()` es, `.split(" ")` nicht, und `unicodedata.normalize("NFKC", s)` macht daraus ein normales Leerzeichen (NFKC ändert auch Zeichen wie `²` zu `2`).

## Wie reparieren Sie Text, der bereits verstümmelt ist?

Wenn kein Byte verloren gegangen ist, kehren Sie den falschen Schritt um:

- Text wie `Ä±ÅŸÄŸ` oder `GrÃ¼ÃŸe`, kopiert von einem Bildschirm oder aus einer Tabelle: `s.encode("cp1252").decode("utf-8")` ergibt `ışğ` bzw. `Grüße`.
- Derselbe Schaden aus `r.text` von Requests enthält ein verstecktes Steuerzeichen (`\x9f`, etwa aus `ş` oder `ß`), das cp1252 nicht kodieren kann; verwenden Sie deshalb `s.encode("latin-1").decode("utf-8")`. cp1252 scheiterte daran mit `'charmap' codec can't encode character '\x9f'`.
- windows-1254-Text, der als ISO-8859-1 gelesen wurde, etwa `Çankýrý`: `s.encode("latin-1").decode("cp1254")` ergibt `Çankırı`.

Für große oder gemischte Datenmengen repariert [ftfy](https://pypi.org/project/ftfy/) 6.3.1 UTF-8-Mojibake: `ftfy.fix_text("Ä±ÅŸÄŸ")` lieferte `ışğ`. `Çankýrý` ließ es unverändert, weil das wie gültiger lateinischer Text aussieht; Verwechslungen von Codepages reparieren Sie also von Hand. Text, in dem `?` oder `�` statt Buchstaben steht, lässt sich nicht reparieren; laden Sie die Quelle erneut.

## Warum zeigt Excel in einer UTF-8-CSV verstümmelte Zeichen?

Excel öffnet eine UTF-8-CSV per Doppelklick korrekt, wenn die Datei mit einer Byte Order Mark beginnt, wie [Microsofts Supportseite](https://support.microsoft.com/en-us/office/opening-csv-utf-8-files-correctly-in-excel-8a935af5-3416-4edd-ba7e-3dfd2bc4a032) erklärt. `encoding="utf-8-sig"` fügt die drei Bytes `EF BB BF` hinzu; in pandas schreiben Sie `df.to_csv("out.csv", encoding="utf-8-sig")`. Lesen Sie solche Dateien auch mit `utf-8-sig` zurück: Mit einfachem `utf-8` lieferte uns das Modul `csv` eine erste Spalte namens `﻿şehir`, während pandas die BOM in beiden Fällen entfernte. Einen vollständigen Ablauf vom Scraping bis Excel finden Sie in [Daten von Websites extrahieren](/de/blog/extract-data-from-website).

## Warum gibt `"I".lower()` nicht `"ı"` zurück?

`str.lower()` verwendet die Standardzuordnung von Unicode, die die Sprache nicht berücksichtigt. Für Türkisch ist das gleich doppelt falsch: `"ISPARTA".lower()` gibt `isparta` statt `ısparta` zurück, und `"İ".lower()` gibt `i` plus einen kombinierenden Punkt (U+0307) zurück. `casefold()` hat dieselbe Lücke. Ordnen Sie die beiden Sonderbuchstaben zuerst zu:

```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
```

Auch im Deutschen ist die Umwandlung nicht umkehrbar: `"Straße".upper()` ergibt `STRASSE`, und `.lower()` macht daraus `strasse`. Für Vergleiche verwenden Sie `casefold()`, das `Straße` und `STRASSE` gleichermaßen zu `strasse` macht.

Auch Zahlen und Datumsangaben folgen lokalen Regeln. Ein Preis in türkischer oder deutscher Schreibweise wie `1.299,90` wird nach `s.replace(".", "").replace(",", ".")` zu einem Float, und pandas bietet dafür `decimal=","` und `thousands="."`. Vermeiden Sie `locale.setlocale()` in einem Scraper, weil es den ganzen Prozess umstellt. Die vollständige Preisbereinigung zeigt [Wettbewerberpreise überwachen](/de/blog/competitor-price-tracking).

## Anwendungsfälle

- **Preisüberwachung:** Shops, die noch alte Codepages ausliefern ([Wettbewerberpreise überwachen](/de/blog/competitor-price-tracking)).
- **Tabellen nach Excel:** eine CSV, die sich mit den richtigen Buchstaben öffnet ([Daten von Websites extrahieren](/de/blog/extract-data-from-website)).
- **PHP-Scraper:** `DOMDocument` braucht eine eigene Lösung ([Webscraping mit PHP](/de/blog/php-web-scraping)).
- **Scrapy-Exporte:** `FEED_EXPORT_ENCODING` legt den Ausgabe-Codec fest ([Scrapy mit Proxy](/de/blog/scrapy-proxy)).
- **.NET-Jobs:** `HttpClient` behandelt Charsets auf eigene Weise ([Web Scraping mit C#](/de/blog/csharp-web-scraping)).
- **Crawls über viele Websites:** den Codec jeder Seite protokollieren ([Web-Crawler](/de/web-crawler)).

## Häufige Fehler

- **Den Fehler mit `errors="ignore"` oder latin-1 unterdrücken.** Das Skript läuft, und die Buchstaben verschwinden oder ändern sich.
- **`r.encoding = "utf-8"` für jede Website fest einstellen.** Das repariert manche Seiten und macht windows-1254-Seiten kaputt.
- **Doppelt dekodieren.** Wer `.encode().decode()` auf bereits korrekt dekodierten Text anwendet, baut in guten Text einen neuen Fehler ein.
- **`iso-8859-9` oder `iso-8859-1` wörtlich nehmen.** Browser verwenden windows-1254 und windows-1252, und Anführungszeichen und Eurozeichen zeigen den Unterschied.
- **Eine `utf-8-sig`-Datei als `utf-8` lesen.** Der erste Spaltenname beginnt mit einem unsichtbaren Zeichen, und Zugriffe über den Namen scheitern.
- **Zeichenkodierung mit URL-Kodierung verwechseln.** `%40` in einem Proxy-Passwort ist Prozentkodierung, ein eigenes Thema ([Sonderzeichen in Proxy-Passwörtern](/de/blog/nodejs-proxy)).

## Entscheidungshilfe

| Bedarf | Empfehlung |
|---|---|
| Nicht-ASCII-Text in Ihrer eigenen `.py`-Datei | Als UTF-8 speichern; keine coding-Zeile |
| Eine Datei lesen oder schreiben | `encoding="utf-8"`; eine alte Codepage nur für Dateien, die damit geschrieben wurden |
| `'charmap'`-Fehler bei umgeleiteter Ausgabe unter Windows | `PYTHONUTF8=1` oder `python -X utf8`; ab Python 3.15 Standard |
| HTML ohne Charset im Header | `r.content` an Beautiful Soup übergeben, den Codec protokollieren |
| Kein Charset im Header und kein `<meta>`-Tag | Den Codec der Website festlegen; `apparent_encoding` als letzter Ausweg |
| Die Seite nennt `iso-8859-9` oder `iso-8859-1` | Als cp1254 oder cp1252 dekodieren, wie ein Browser |
| Der Text zeigt bereits `Ã¼` oder `Ä±` | `.encode("cp1252").decode("utf-8")` oder `ftfy.fix_text` |
| Das Ergebnis wird in Excel geöffnet | Die CSV mit `utf-8-sig` schreiben |

## Häufige Fragen

### Brauche ich eine Bibliothek, um Umlaute und andere Sonderzeichen in Python zu verarbeiten?

Nein. Ein `str` in Python 3 ist Unicode, und die Standardbibliothek bringt Codecs für die gängigen Codepages mit ([Standard Encodings](https://docs.python.org/3/library/codecs.html#standard-encodings)). charset-normalizer, das mit Requests installiert wird, schätzt unbekannte Codecs, und ftfy repariert Mojibake.

### Wie konvertiere ich eine cp1252- oder cp1254-Datei nach UTF-8?

Lesen Sie sie mit ihrem alten Codec und schreiben Sie sie mit dem neuen: `Path("new.txt").write_text(Path("old.txt").read_text(encoding="cp1254"), encoding="utf-8")`, mit `Path` aus `pathlib`. Für eine deutsche Datei setzen Sie `cp1252` ein. Prüfen Sie einige Namen, bevor Sie das Original löschen.

### Behebt errors="ignore" einen UnicodeDecodeError?

Es verdeckt ihn. Bytes, die nicht passen, werden ohne Warnung gelöscht, sodass Namen Buchstaben verlieren: Aus `Grüße` in einer cp1252-Datei wird beim Lesen als UTF-8 `Gre`. Suchen Sie stattdessen den richtigen Codec.

### Wie finde ich heraus, welche Kodierung eine Datei verwendet?

Sofern sie nicht mit einer BOM beginnt, speichert eine Textdatei ihren Codec nicht, deshalb schätzt jedes Werkzeug. `charset_normalizer.from_path("old.txt").best().encoding` lieferte für unsere türkische Testdatei `cp1254`. Bestätigen Sie die Schätzung, indem Sie dekodieren und einige Wörter mit lokalen Buchstaben lesen.

### Was ist der Unterschied zwischen utf-8 und utf-8-sig?

`utf-8-sig` schreibt eine BOM (`EF BB BF`) und überspringt sie beim Lesen. Verwenden Sie es für CSV-Dateien, die in Excel geöffnet werden.

### Wie behebe ich UnicodeDecodeError in pandas read_csv?

Geben Sie den Codec der Datei an: `encoding="cp1254"` für einen türkischen Windows-Export, `cp1252` für einen westeuropäischen oder deutschen und `utf-8-sig`, wenn die Datei eine BOM hat. `encoding_errors="replace"` stoppt den Fehler, kostet aber Buchstaben.

## Fazit

Encoding-Fehler in Python lassen sich auf eine Regel zurückführen: Dekodieren Sie Bytes mit dem Codec, in dem sie geschrieben wurden, und schreiben Sie Ihre eigene Ausgabe in UTF-8. Lesen Sie am verstümmelten Text das Codec-Paar ab, setzen Sie `encoding` bei jedem `open()`, nutzen Sie unter Windows den UTF-8-Modus, bis Python 3.15 das für Sie übernimmt, und übergeben Sie Webseiten als Bytes an Beautiful Soup. Für Scraping-Aufträge, die sauberen Text von vielen lokalen Websites brauchen, finden Sie mehr in unserer Lösung für [Data Scraping](/de/data-scraping).
