# 編碼
任何HTML或XML文檔都有自己的編碼方式,比如ASCII 或 UTF-8,但是使用Beautiful Soup解析后,文檔都被轉換成了Unicode:
```
markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
soup = BeautifulSoup(markup)
soup.h1
# <h1>Sacré bleu!</h1>
soup.h1.string
# u'Sacr\xe9 bleu!'
```
這不是魔術(但很神奇),Beautiful Soup用了 [編碼自動檢測](#unicode-dammit) 子庫來識別當前文檔編碼并轉換成Unicode編碼. `BeautifulSoup` 對象的 `.original_encoding` 屬性記錄了自動識別編碼的結果:
```
soup.original_encoding
'utf-8'
```
[編碼自動檢測](#unicode-dammit) 功能大部分時候都能猜對編碼格式,但有時候也會出錯.有時候即使猜測正確,也是在逐個字節的遍歷整個文檔后才猜對的,這樣很慢.如果預先知道文檔編碼,可以設置編碼參數來減少自動檢查編碼出錯的概率并且提高文檔解析速度.在創建 `BeautifulSoup` 對象的時候設置 `from_encoding` 參數.
下面一段文檔用了ISO-8859-8編碼方式,這段文檔太短,結果Beautiful Soup以為文檔是用ISO-8859-7編碼:
```
markup = b"<h1>\xed\xe5\xec\xf9</h1>"
soup = BeautifulSoup(markup)
soup.h1
<h1>νεμω</h1>
soup.original_encoding
'ISO-8859-7'
```
通過傳入 `from_encoding` 參數來指定編碼方式:
```
soup = BeautifulSoup(markup, from_encoding="iso-8859-8")
soup.h1
<h1>????</h1>
soup.original_encoding
'iso8859-8'
```
少數情況下(通常是UTF-8編碼的文檔中包含了其它編碼格式的文件),想獲得正確的Unicode編碼就不得不將文檔中少數特殊編碼字符替換成特殊Unicode編碼,“REPLACEMENT CHARACTER” (U+FFFD, ?) \[9\] . 如果Beautifu Soup猜測文檔編碼時作了特殊字符的替換,那么Beautiful Soup會把 `UnicodeDammit` 或 `BeautifulSoup` 對象的 `.contains_replacement_characters` 屬性標記為 `True` .這樣就可以知道當前文檔進行Unicode編碼后丟失了一部分特殊內容字符.如果文檔中包含?而 `.contains_replacement_characters` 屬性是 `False` ,則表示?就是文檔中原來的字符,不是轉碼失敗.
## 輸出編碼
通過Beautiful Soup輸出文檔時,不管輸入文檔是什么編碼方式,輸出編碼均為UTF-8編碼,下面例子輸入文檔是Latin-1編碼:
```
markup = b'''
<html>
<head>
<meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type" />
</head>
<body>
<p>Sacr\xe9 bleu!</p>
</body>
</html>
'''
soup = BeautifulSoup(markup)
print(soup.prettify())
# <html>
# <head>
# <meta content="text/html; charset=utf-8" http-equiv="Content-type" />
# </head>
# <body>
# <p>
# Sacré bleu!
# </p>
# </body>
# </html>
```
注意,輸出文檔中的`<meta>`標簽的編碼設置已經修改成了與輸出編碼一致的UTF-8.
如果不想用UTF-8編碼輸出,可以將編碼方式傳入 `prettify()` 方法:
```
print(soup.prettify("latin-1"))
# <html>
# <head>
# <meta content="text/html; charset=latin-1" http-equiv="Content-type" />
# ...
```
還可以調用 `BeautifulSoup` 對象或任意節點的 `encode()` 方法,就像Python的字符串調用 `encode()` 方法一樣:
```
soup.p.encode("latin-1")
# '<p>Sacr\xe9 bleu!</p>'
soup.p.encode("utf-8")
# '<p>Sacr\xc3\xa9 bleu!</p>'
```
如果文檔中包含當前編碼不支持的字符,那么這些字符將唄轉換成一系列XML特殊字符引用,下面例子中包含了Unicode編碼字符SNOWMAN:
```
markup = u"<b>\N{SNOWMAN}</b>"
snowman_soup = BeautifulSoup(markup)
tag = snowman_soup.b
```
SNOWMAN字符在UTF-8編碼中可以正常顯示(看上去像是?),但有些編碼不支持SNOWMAN字符,比如ISO-Latin-1或ASCII,那么在這些編碼中SNOWMAN字符會被轉換成“☃”:
```
print(tag.encode("utf-8"))
# <b>?</b>
print tag.encode("latin-1")
# <b>☃</b>
print tag.encode("ascii")
# <b>☃</b>
```
## Unicode, dammit! (靠!)
[編碼自動檢測](#unicode-dammit) 功能可以在Beautiful Soup以外使用,檢測某段未知編碼時,可以使用這個方法:
```
from bs4 import UnicodeDammit
dammit = UnicodeDammit("Sacr\xc3\xa9 bleu!")
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'utf-8'
```
如果Python中安裝了 `chardet` 或 `cchardet` 那么編碼檢測功能的準確率將大大提高.輸入的字符越多,檢測結果越精確,如果事先猜測到一些可能編碼,那么可以將猜測的編碼作為參數,這樣將優先檢測這些編碼:
```
dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'latin-1'
```
[編碼自動檢測](#unicode-dammit) 功能中有2項功能是Beautiful Soup庫中用不到的
### 智能引號
使用Unicode時,Beautiful Soup還會智能的把引號 \[10\] 轉換成HTML或XML中的特殊字符:
```
markup = b"<p>I just \x93love\x94 Microsoft Word\x92s smart quotes</p>"
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="html").unicode_markup
# u'<p>I just “love” Microsoft Word’s smart quotes</p>'
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
# u'<p>I just “love” Microsoft Word’s smart quotes</p>'
```
也可以把引號轉換為ASCII碼:
```
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
# u'<p>I just "love" Microsoft Word\'s smart quotes</p>'
```
很有用的功能,但是Beautiful Soup沒有使用這種方式.默認情況下,Beautiful Soup把引號轉換成Unicode:
```
UnicodeDammit(markup, ["windows-1252"]).unicode_markup
# u'<p>I just \u201clove\u201d Microsoft Word\u2019s smart quotes</p>'
```
### 矛盾的編碼
有時文檔的大部分都是用UTF-8,但同時還包含了Windows-1252編碼的字符,就像微軟的智能引號 \[10\] 一樣.一些包含多個信息的來源網站容易出現這種情況. `UnicodeDammit.detwingle()` 方法可以把這類文檔轉換成純UTF-8編碼格式,看個簡單的例子:
```
snowmen = (u"\N{SNOWMAN}" * 3)
quote = (u"\N{LEFT DOUBLE QUOTATION MARK}I like snowmen!\N{RIGHT DOUBLE QUOTATION MARK}")
doc = snowmen.encode("utf8") + quote.encode("windows_1252")
```
這段文檔很雜亂,snowmen是UTF-8編碼,引號是Windows-1252編碼,直接輸出時不能同時顯示snowmen和引號,因為它們編碼不同:
```
print(doc)
# ????I like snowmen!?
print(doc.decode("windows-1252"))
# a??a??a??“I like snowmen!”
```
如果對這段文檔用UTF-8解碼就會得到 `UnicodeDecodeError` 異常,如果用Windows-1252解碼就回得到一堆亂碼.幸好, `UnicodeDammit.detwingle()` 方法會吧這段字符串轉換成UTF-8編碼,允許我們同時顯示出文檔中的snowmen和引號:
```
new_doc = UnicodeDammit.detwingle(doc)
print(new_doc.decode("utf8"))
# ???“I like snowmen!”
```
`UnicodeDammit.detwingle()` 方法只能解碼包含在UTF-8編碼中的Windows-1252編碼內容,但這解決了最常見的一類問題.
在創建 `BeautifulSoup` 或 `UnicodeDammit` 對象前一定要先對文檔調用 `UnicodeDammit.detwingle()` 確保文檔的編碼方式正確.如果嘗試去解析一段包含Windows-1252編碼的UTF-8文檔,就會得到一堆亂碼,比如: a??a??a??“I like snowmen!”.
`UnicodeDammit.detwingle()` 方法在Beautiful Soup 4.1.0版本中新增