การตั้งค่าการเข้ารหัสที่ถูกต้องเมื่อ piping stdout ใน Python


343

เมื่อไพพ์เอาท์พุทของโปรแกรม Python ล่าม Python สับสนเกี่ยวกับการเข้ารหัสและตั้งค่าเป็น None นี่หมายถึงโปรแกรมเช่นนี้:

# -*- coding: utf-8 -*-
print u"åäö"

จะทำงานได้ดีเมื่อทำงานตามปกติ แต่ล้มเหลวด้วย:

UnicodeEncodeError: ตัวแปลงสัญญาณ 'ascii' ไม่สามารถเข้ารหัสอักขระ u '\ xa0' ในตำแหน่ง 0: เลขลำดับไม่อยู่ในช่วง (128)

เมื่อใช้ในลำดับท่อ

เป็นวิธีที่ดีที่สุดในการทำให้งานนี้เมื่อท่อได้อย่างไร ฉันสามารถบอกให้ใช้การเข้ารหัสเชลล์ / ระบบไฟล์ / อะไรก็ได้ที่กำลังใช้อยู่

คำแนะนำที่ฉันได้เห็นในตอนนี้คือการแก้ไข site.py ของคุณโดยตรงหรือการเข้ารหัสฮาร์ดโค้ดเริ่มต้นโดยใช้แฮ็คนี้:

# -*- coding: utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
print u"åäö"

มีวิธีที่ดีกว่าในการทำให้ท่อทำงานหรือไม่



2
หากคุณมีปัญหานี้ในหน้าต่างคุณสามารถเรียกใช้chcp 65001ก่อนที่จะดำเนินการสคริปต์ของคุณ สิ่งนี้อาจมีปัญหา แต่มักจะช่วยได้และไม่จำเป็นต้องพิมพ์มาก (น้อยกว่าset PYTHONIOENCODING=utf_8)
Tomasz Gandor

คำสั่ง chcp ไม่เหมือนกับการตั้งค่า PYTHONIOENCODING ฉันคิดว่า chcp เป็นเพียงการกำหนดค่าสำหรับเทอร์มินัลและไม่มีอะไรเกี่ยวข้องกับการเขียนไฟล์ (ซึ่งเป็นสิ่งที่คุณกำลังทำเมื่อ piping stdout) พยายามsetx PYTHONENCODING utf-8ทำให้มันถาวรถ้าคุณต้องการบันทึกการพิมพ์
ejm


ฉันพบปัญหาที่เกี่ยวข้องบ้างและพบวิธีแก้ปัญหาที่นี่ -> stackoverflow.com/questions/48782529//
bkrishna2006

คำตอบ:


162

รหัสของคุณใช้งานได้เมื่อทำงานในสคริปต์เนื่องจาก Python เข้ารหัสเอาต์พุตไปยังสิ่งที่เข้ารหัสแอปพลิเคชันเทอร์มินัลที่คุณใช้ หากคุณกำลังวางท่อคุณจะต้องเข้ารหัสด้วยตัวเอง

กฎง่ายๆคือใช้ Unicode ภายในเสมอ ถอดรหัสสิ่งที่คุณได้รับและเข้ารหัสสิ่งที่คุณส่ง

# -*- coding: utf-8 -*-
print u"åäö".encode('utf-8')

อีกตัวอย่างหนึ่งในการสอนคือโปรแกรม Python สำหรับแปลงระหว่าง ISO-8859-1 และ UTF-8 ทำให้ทุกอย่างเป็นตัวพิมพ์ใหญ่ระหว่างนั้น

import sys
for line in sys.stdin:
    # Decode what you receive:
    line = line.decode('iso8859-1')

    # Work with Unicode internally:
    line = line.upper()

    # Encode what you send:
    line = line.encode('utf-8')
    sys.stdout.write(line)

การตั้งค่าการเข้ารหัสเริ่มต้นของระบบเป็นแนวคิดที่ไม่ดีเนื่องจากโมดูลและไลบรารีบางตัวที่คุณใช้สามารถพึ่งพาข้อเท็จจริงที่ว่ามันคือ ASCII อย่าทำมัน


11
ปัญหาคือผู้ใช้ไม่ต้องการระบุการเข้ารหัสอย่างชัดเจน เขาต้องการเพียงแค่ใช้ Unicode สำหรับ IO และการเข้ารหัสที่เขาใช้ควรเป็นการเข้ารหัสที่ระบุในการตั้งค่าโลแคลไม่ใช่ในการตั้งค่าแอปพลิเคชันเทอร์มินัล AFAIK, Python 3 ใช้การเข้ารหัสภาษาในกรณีนี้ การเปลี่ยนแปลงsys.stdoutดูเหมือนเป็นวิธีที่น่าพอใจมากขึ้น
Andrey Vlasovskikh

4
การเข้ารหัส / ถอดรหัสสตริงทุกเส้นอย่างถูกผูกมัดเพื่อก่อให้เกิดข้อบกพร่องเมื่อการโทรเข้ารหัสหรือถอดรหัสหายไปหรือเพิ่มหนึ่งครั้งเพื่อมาก สามารถตั้งค่าการเข้ารหัสเอาต์พุตเมื่อเอาต์พุตเป็นเทอร์มินัลดังนั้นจึงสามารถตั้งค่าได้เมื่อเอาต์พุตไม่ใช่เทอร์มินัล มีแม้กระทั่งสภาวะแวดล้อม LC_CTYPE มาตรฐานเพื่อระบุ มันเป็น แต่ในไพ ธ อนที่ไม่เคารพสิ่งนี้
Rasmus Kaj

65
คำตอบนี้ผิด คุณไม่ควรแปลงแต่ละอินพุทและเอาท์พุทของโปรแกรมของคุณด้วยตนเอง; ที่เปราะบางและไม่สามารถรักษาได้อย่างสมบูรณ์
Glenn Maynard

29
@Glenn Maynard: แล้ว IYO คำตอบที่ถูกคืออะไร? เป็นประโยชน์มากกว่าที่จะบอกเรามากกว่าเพียงแค่พูดว่า'คำตอบนี้ผิด'
smci

14
@smci: คำตอบคืออย่าแก้ไขสคริปต์ของคุณตั้งค่าPYTHONIOENCODINGหากคุณเปลี่ยนเส้นทาง stdout ของสคริปต์ใน Python 2
jfs

168

ก่อนอื่นเกี่ยวกับวิธีแก้ปัญหานี้:

# -*- coding: utf-8 -*-
print u"åäö".encode('utf-8')

ไม่สามารถพิมพ์ด้วยการเข้ารหัสที่กำหนดทุกครั้งอย่างชัดเจน ที่จะซ้ำและผิดพลาดได้ง่าย

ทางออกที่ดีกว่าคือการเปลี่ยนsys.stdoutเมื่อเริ่มต้นโปรแกรมของคุณเพื่อเข้ารหัสด้วยการเข้ารหัสที่เลือก นี่คือวิธีแก้ไขปัญหาเดียวที่ฉันพบในPython: sys.stdout.encoding ถูกเลือกอย่างไร โดยเฉพาะอย่างยิ่งความคิดเห็นโดย "toka":

import sys
import codecs
sys.stdout = codecs.getwriter('utf8')(sys.stdout)

7
น่าเสียดายที่การเปลี่ยน sys.stdout เพื่อยอมรับเฉพาะ unicode จะทำลายไลบรารีจำนวนมากที่คาดว่าจะยอมรับการเข้ารหัสแบบทดสอบ
nosklo

6
nosklo: ถ้าอย่างนั้นมันจะทำงานได้อย่างน่าเชื่อถือและเป็นอัตโนมัติอย่างไร
Rasmus Kaj

3
@Rasmus Kaj: เพียงแค่กำหนดฟังก์ชั่นการพิมพ์ Unicode ของคุณเองและใช้เวลาที่คุณต้องการที่จะพิมพ์ Unicode ทุก: def myprint(unicodeobj): print unicodeobj.encode('utf-8')- ให้คุณโดยอัตโนมัติตรวจจับการเข้ารหัสขั้วโดยการตรวจสอบsys.stdout.encodingแต่คุณควรพิจารณากรณีที่เป็นNone(เช่นเมื่อเปลี่ยนเส้นทางออกไปยังแฟ้ม) ดังนั้นคุณต้องมีฟังก์ชั่นแยกต่างหาก
nosklo

3
@nosklo: นี่ไม่ได้ทำให้ sys.stdout ยอมรับ Unicode เท่านั้น คุณสามารถส่งทั้ง str และ unicode ไปยัง StreamWriter
Glenn Maynard

9
ฉันคิดว่าคำตอบนี้มีไว้สำหรับ python2 โปรดใช้ความระมัดระวังเกี่ยวกับเรื่องนี้ในรหัสซึ่งมีวัตถุประสงค์เพื่อสนับสนุนทั้ง python2 และ python3 สำหรับฉันมันทำลายสิ่งต่างๆเมื่อวิ่งภายใต้ python3
Wim

130

คุณอาจต้องการลองเปลี่ยนตัวแปรสภาพแวดล้อม "PYTHONIOENCODING" เป็น "utf_8" ฉันได้เขียนหน้าในการทดสอบของฉันกับปัญหานี้

Tl; dr ของโพสต์บล็อก:

import sys, locale, os
print(sys.stdout.encoding)
print(sys.stdout.isatty())
print(locale.getpreferredencoding())
print(sys.getfilesystemencoding())
print(os.environ["PYTHONIOENCODING"])
print(chr(246), chr(9786), chr(9787))

ให้คุณ

utf_8
False
ANSI_X3.4-1968
ascii
utf_8
ö ☺ ☻

2
เปลี่ยน sys.stdout.encoding อาจจะไม่ทำงาน แต่เปลี่ยน sys.stdout sys.stdout = codecs.getwriter(encoding)(sys.stdout)ทำงาน: สิ่งนี้สามารถทำได้จากภายในโปรแกรมไพ ธ อนดังนั้นผู้ใช้จึงไม่ถูกบังคับให้ตั้งค่าตัวแปร env
blueFast

7
@ jeckyll2hide: PYTHONIOENCODINGใช้งานได้ วิธีตีความไบต์เป็นข้อความที่ถูกกำหนดโดยสภาพแวดล้อมของผู้ใช้ สคริปต์ของคุณไม่ควรสมมติและกำหนดสภาพแวดล้อมของผู้ใช้ในการเข้ารหัสอักขระที่จะใช้ ถ้า Python ไม่รับการตั้งค่าโดยอัตโนมัติPYTHONIOENCODINGคุณสามารถตั้งค่าสคริปต์ของคุณได้ คุณไม่จำเป็นต้องใช้มันนอกจากว่าเอาต์พุตจะถูกเปลี่ยนเส้นทางไปยังไฟล์ / ไพพ์
jfs

8
+1 สุจริตฉันคิดว่ามันเป็นข้อผิดพลาดงูหลาม เมื่อฉันเปลี่ยนเส้นทางฉันต้องการไบต์เดียวกันเหล่านั้นที่จะอยู่ใน terminal แต่ในไฟล์ อาจไม่ใช่สำหรับทุกคน แต่เป็นค่าเริ่มต้นที่ดี ล้มเหลวอย่างหนักโดยไม่มีคำอธิบายเกี่ยวกับการทำงานที่ไม่สำคัญซึ่งโดยปกติแล้ว "เพิ่งได้ผล" เป็นค่าเริ่มต้นที่ไม่ดี
SnakE

@SnakE: วิธีเดียวที่ฉันสามารถหาเหตุผลว่าทำไมการดำเนินการของ Python โดยเจตนาจะบังคับให้มีการเลือกชุดเกราะและการเข้ารหัสถาวรบน stdout ในเวลาเริ่มต้นอาจเป็นการป้องกันไม่ให้สิ่งที่เข้ารหัสไม่ดีออกมาในภายหลัง หรือการเปลี่ยนแปลงมันเป็นเพียงคุณสมบัติที่ยังไม่ได้ใช้งานซึ่งในกรณีนี้การอนุญาตให้ผู้ใช้เปลี่ยนในภายหลังจะเป็นการร้องขอคุณสมบัติ Python ที่สมเหตุสมผล
daveagp

2
@daveagp ประเด็นของฉันคือพฤติกรรมของโปรแกรมของฉันไม่ควรขึ้นอยู่กับว่ามันถูกเปลี่ยนเส้นทางหรือไม่ --- ถ้าฉันไม่ต้องการมันจริงๆ หลามทำงานกับประสบการณ์ของฉันกับเครื่องมือคอนโซลอื่น ๆ นี่เป็นการละเมิดหลักการที่ทำให้ประหลาดใจน้อยที่สุด ฉันคิดว่านี่เป็นข้อบกพร่องในการออกแบบเว้นแต่จะมีเหตุผลที่แข็งแกร่งมาก
SnakE

62
export PYTHONIOENCODING=utf-8

ทำงาน แต่ไม่สามารถตั้งค่าบน python เอง ...

สิ่งที่เราสามารถทำได้คือตรวจสอบว่าไม่ได้ตั้งค่าไว้และบอกให้ผู้ใช้ตั้งค่าก่อนโทรสคริปต์ด้วย:

if __name__ == '__main__':
    if (sys.stdout.encoding is None):
        print >> sys.stderr, "please set python env PYTHONIOENCODING=UTF-8, example: export PYTHONIOENCODING=UTF-8, when write to stdout."
        exit(1)

อัปเดตเพื่อตอบกลับความคิดเห็น: ปัญหามีอยู่เมื่อ piping ไปยัง stdout ฉันทดสอบใน Fedora 25 Python 2.7.13

python --version
Python 2.7.13

cat b.py

#!/usr/bin/env python
#-*- coding: utf-8 -*-
import sys

print sys.stdout.encoding

กำลังทำงาน. / b.py

UTF-8

กำลังทำงาน. /b.py | น้อยกว่า

None

2
การตรวจสอบนั้นไม่ทำงานใน Python 2.7.13 sys.stdout.encodingถูกตั้งค่าโดยอัตโนมัติตามLC_CTYPEค่าโลแคล
แอมเฟตามาจิน

1
mail.python.org/pipermail/python-list/2011-June/605938.htmlตัวอย่างยังคงใช้ได้เช่นเมื่อคุณใช้. /a.py> out.txt sys.stdout.encoding ไม่มี
Sérgio

ฉันมีปัญหาคล้ายกันกับสคริปต์ซิงค์จาก Backblaze B2 และส่งออก PYTHONIOENCODING = utf-8 แก้ปัญหาของฉัน Python 2.7 ในการยืดเดเบียน
0x3333

5

ฉันมีปัญหาที่คล้ายกันเมื่อสัปดาห์ที่แล้ว มันง่ายที่จะแก้ไขใน IDE ของฉัน (PyCharm)

นี่คือการแก้ไขของฉัน:

เริ่มต้นจากแถบเมนู PyCharm: ไฟล์ -> การตั้งค่า ... -> ตัวแก้ไข -> การเข้ารหัสไฟล์จากนั้นตั้งค่า: "การเข้ารหัส IDE", "การเข้ารหัสโครงการ" และ "การเข้ารหัสเริ่มต้นสำหรับไฟล์คุณสมบัติ" ALL to UTF-8 และตอนนี้เธอทำงานแล้ว ชอบเสน่ห์

หวังว่านี่จะช่วยได้!


4

คำตอบของ Craig McQueen รุ่นที่ถูกสุขลักษณะ

import sys, codecs
class EncodedOut:
    def __init__(self, enc):
        self.enc = enc
        self.stdout = sys.stdout
    def __enter__(self):
        if sys.stdout.encoding is None:
            w = codecs.getwriter(self.enc)
            sys.stdout = w(sys.stdout)
    def __exit__(self, exc_ty, exc_val, tb):
        sys.stdout = self.stdout

การใช้งาน:

with EncodedOut('utf-8'):
    print u'ÅÄÖåäö'

2

ฉันสามารถ "ทำให้เป็นอัตโนมัติ" ด้วยการโทรไปที่:

def __fix_io_encoding(last_resort_default='UTF-8'):
  import sys
  if [x for x in (sys.stdin,sys.stdout,sys.stderr) if x.encoding is None] :
      import os
      defEnc = None
      if defEnc is None :
        try:
          import locale
          defEnc = locale.getpreferredencoding()
        except: pass
      if defEnc is None :
        try: defEnc = sys.getfilesystemencoding()
        except: pass
      if defEnc is None :
        try: defEnc = sys.stdin.encoding
        except: pass
      if defEnc is None :
        defEnc = last_resort_default
      os.environ['PYTHONIOENCODING'] = os.environ.get("PYTHONIOENCODING",defEnc)
      os.execvpe(sys.argv[0],sys.argv,os.environ)
__fix_io_encoding() ; del __fix_io_encoding

ใช่เป็นไปได้ที่จะวนซ้ำไม่สิ้นสุดที่นี่หาก "setenv" นี้ล้มเหลว


1
น่าสนใจ แต่ดูเหมือนจะไม่มีความสุขกับเรื่องนี้
n611x007

2

ฉันแค่คิดว่าฉันพูดถึงบางสิ่งบางอย่างที่นี่ซึ่งฉันต้องใช้เวลานานในการทดสอบก่อนที่ฉันจะรู้ว่าสิ่งที่เกิดขึ้นในที่สุด นี่อาจจะชัดเจนสำหรับทุกคนที่นี่ที่พวกเขาไม่ได้ใส่ใจพูดถึง แต่มันจะช่วยฉันได้ถ้าพวกเขามีหลักการนั้น ... !

NB: ฉันใช้Jythonโดยเฉพาะ v 2.7 ดังนั้นอาจเป็นไปได้ว่านี่อาจใช้ไม่ได้กับCPython ...

NB2: สองบรรทัดแรกของไฟล์. py ของฉันที่นี่คือ:

# -*- coding: utf-8 -*-
from __future__ import print_function

กลไกการสร้างสตริง "%" (AKA "ตัวดำเนินการแก้ไข") ทำให้เกิดปัญหาเพิ่มเติมเช่นกัน ... หากการเข้ารหัสเริ่มต้นของ "สภาพแวดล้อม" คือ ASCII และคุณพยายามทำบางสิ่งเช่นนี้

print( "bonjour, %s" % "fréd" )  # Call this "print A"

คุณจะไม่มีปัญหาในการทำงานใน Eclipse ... ในหน้าต่าง Windows CLI (ดอส DOS) คุณจะพบว่าการเข้ารหัสคือรหัสหน้า 850 (ระบบปฏิบัติการ Windows 7 ของฉัน) หรือบางสิ่งที่คล้ายกันซึ่งสามารถจัดการอักขระที่เน้นเสียงในยุโรปได้อย่างน้อย จะได้ผล

print( u"bonjour, %s" % "fréd" ) # Call this "print B"

ยังจะทำงาน

ถ้า OTOH คุณส่งไฟล์จาก CLI การเข้ารหัส stdout จะไม่มีซึ่งจะเริ่มต้นเป็น ASCII (บนระบบปฏิบัติการของฉัน) ซึ่งจะไม่สามารถจัดการกับการพิมพ์ข้างต้นอย่างใดอย่างหนึ่ง ... (การเข้ารหัสหวั่น ข้อผิดพลาด)

ดังนั้นคุณอาจคิดถึง stdout ของคุณโดยใช้

sys.stdout = codecs.getwriter('utf8')(sys.stdout)

และลองเรียกใช้ในการบีบอัดไฟล์ CLI ไปที่ไฟล์ ... แปลกมากการพิมพ์ A ด้านบนจะใช้งานได้ ... แต่การพิมพ์ B ด้านบนจะทำให้เกิดข้อผิดพลาดในการเข้ารหัส! อย่างไรก็ตามสิ่งต่อไปนี้จะใช้งานได้:

print( u"bonjour, " + "fréd" ) # Call this "print C"

ข้อสรุปที่ฉันได้มา (ชั่วคราว) คือถ้าสตริงที่ระบุว่าเป็นสตริงUnicodeโดยใช้คำนำหน้า "u" ถูกส่งไปยังกลไก% -handling มันดูเหมือนจะเกี่ยวข้องกับการใช้การเข้ารหัสสภาพแวดล้อมเริ่มต้นโดยไม่คำนึงถึง ไม่ว่าคุณจะตั้งค่า stdout เพื่อเปลี่ยนเส้นทาง!

วิธีที่ผู้คนจัดการกับเรื่องนี้เป็นเรื่องของการเลือก ฉันยินดีต้อนรับผู้เชี่ยวชาญ Unicode ที่จะบอกว่าทำไมสิ่งนี้จึงเกิดขึ้นไม่ว่าฉันจะทำสิ่งใดผิดวิธีที่เป็นที่นิยมในการแก้ปัญหานี้ไม่ว่าจะใช้กับCPythonไม่ว่ามันจะเกิดขึ้นใน Python 3 ฯลฯ ฯลฯ


ไม่แปลกเลยนั่นเป็นเพราะ"fréd"ลำดับไบต์และไม่ใช่สตริง Unicode ดังนั้นcodecs.getwriterwrapper จะปล่อยมันไว้ตามลำพัง คุณต้องชั้นนำหรือu from __future__ import unicode_literals
Matthias Urlichs

@MatthiasUrlich ตกลง ... ขอบคุณ ... แต่ฉันเพิ่งพบว่าการเข้ารหัสหนึ่งในด้านที่ทำให้โกรธที่สุดของไอที คุณเข้าใจจากที่ไหน ตัวอย่างเช่นฉันเพิ่งโพสต์คำถามอื่นเกี่ยวกับการเข้ารหัสที่นี่: stackoverflow.com/questions/44483067/… : นี่เป็นเรื่องเกี่ยวกับ Java, Eclipse, Cygwin & Gradle หากความเชี่ยวชาญของคุณก้าวไปไกลโปรดช่วย ... เหนือสิ่งอื่นใดฉันอยากรู้ว่าจะเรียนรู้เพิ่มเติมได้ที่ไหน!
ไมค์สัตว์ฟันแทะ

1

ฉันพบปัญหานี้ในแอปพลิเคชันรุ่นเก่าและมันยากที่จะระบุว่าสิ่งที่พิมพ์ออกมา ฉันช่วยตัวเองด้วยแฮ็คนี้:

# encoding_utf8.py
import codecs
import builtins


def print_utf8(text, **kwargs):
    print(str(text).encode('utf-8'), **kwargs)


def print_utf8(fn):
    def print_fn(*args, **kwargs):
        return fn(str(*args).encode('utf-8'), **kwargs)
    return print_fn


builtins.print = print_utf8(print)

ที่ด้านบนของสคริปต์ test.py:

import encoding_utf8
string = 'Axwell Λ Ingrosso'
print(string)

โปรดทราบว่าสิ่งนี้จะเปลี่ยนการเรียกทั้งหมดเพื่อพิมพ์เพื่อใช้การเข้ารหัสดังนั้นคอนโซลของคุณจะพิมพ์สิ่งนี้:

$ python test.py
b'Axwell \xce\x9b Ingrosso'

1

บน Windows ฉันพบปัญหานี้บ่อยมากเมื่อใช้งานรหัส Python จากโปรแกรมแก้ไข (เช่น Sublime Text) แต่ไม่ใช่ถ้าเรียกใช้จากบรรทัดคำสั่ง

ในกรณีนี้ให้ตรวจสอบพารามิเตอร์ของบรรณาธิการ ในกรณีของ SublimeText สิ่งนี้Python.sublime-buildจะแก้ไขได้:

{
  "cmd": ["python", "-u", "$file"],
  "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
  "selector": "source.python",
  "encoding": "utf8",
  "env": {"PYTHONIOENCODING": "utf-8", "LANG": "en_US.UTF-8"}
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.