網站搬完家,後台登入正常,選單一個都沒少,你點開一篇兩年前發布的舊文章,內文卻整排都是問號,或者是一串看起來像亂碼的西歐字母,例如「好童香水」。這種畫面通常會讓人直覺去外掛市集找一個「修復亂碼」的工具,或是在資料庫裡亂試幾種字元集,結果往往是問題沒解決,反而讓原本還救得回來的資料被轉了第二次,徹底走樣。
其實「資料庫亂碼」不是單一種故障,它至少對應三種完全不同的畫面、三種不同的成因,處理方式也天差地遠。有的只是瀏覽器快取住了舊頁面,有的是資料庫欄位的字元集宣告標錯,有的則是資料在搬家過程中已經被錯誤地轉碼了不只一次,位元組本身已經跟原意不同。分不清這三種就動手改資料庫,最常見的下場,就是把一份還沒壞的資料,親手轉成真正壞掉的資料。
這篇照實際故障排除的順序走:先分辨你看到的是哪一種亂碼,排除快取與字型的假象,找出老站資料庫還停在舊版字元集的原因,追出搬家匯出匯入的哪個環節把字元集弄丟,再判斷資料是標籤貼錯還是內容真的壞了,最後才進到實際轉換與驗證的步驟。先從最基本的三種亂碼樣貌開始拆。
搬家或還原備份後,中文字出現的三種亂碼樣貌
搬家、換主機或還原一份舊備份之後,「亂碼」實際上有三種完全不同的長相,各自對應的線索也不一樣。第一種是整段文字變成一排問號「?」,這通常代表資料在寫入或轉碼的當下就已經遺失,某些原本存在的位元組被硬生生換成了問號字元,等於原始內容在那一刻就已經回不去了。第二種是變成方塊或菱形加問號的符號,也就是 Unicode 的替代字元,外觀常見是黑底反白的問號方塊,這種情況多半只是瀏覽器或系統缺少能顯示某些生僻字或表情符號的字型,底層資料其實沒壞,只是顯示不出來。第三種是一串看起來眼熟卻讀不懂的西歐字母組合,例如「好童ã€「香水」,這是最典型的「資料本身是對的,只是被拿錯字元集去解讀」,原本用 utf8mb4 編碼儲存的中文,被瀏覽器或資料庫用拉丁語系的字元集重新翻譯了一次,才會冒出這種怪異的字母串。

這三種樣貌通常出現在不同的時間點,先看清楚自己是在哪個階段撞見亂碼,才不會找錯方向。整段問號最常在匯入的當下就已經定型,重新整理頁面也不會變;方塊菱形問號幾乎都是換了瀏覽器、清了快取,或是把文章內容複製到別的系統時才冒出來,跟資料庫本身無關;西歐字母亂碼最常出現在搬家、換主機商這幾個場景,因為這正是字元集宣告最容易在匯出、匯入兩端對不上的環節。先分清楚自己是哪一種,才知道後面該往哪個方向查。這也是接下來每一節要回答的問題。
從資料庫原始內容排除字型與瀏覽器快取的干擾
分清楚自己撞見的是哪一種亂碼之後,還有一步更基本的事要先做,那就是在動手改資料庫之前,先確認問題真的出在資料庫,而不是前台顯示端。不少亂碼案例最後查出來,資料庫裡存的內容其實一字不差,只是頁面快取、CDN 快取,或是主題本身缺乏能顯示某些生僻字的字型造成的假象,這種情況去改資料庫的字元集只是白費工夫,甚至有可能把原本好端端的資料弄壞。分辨的關鍵只有一個判準,就是繞過 WordPress 的渲染層,直接看資料庫裡儲存的原始值,跟前台頁面顯示出來的內容做比對,兩者不一樣,問題才真的在資料庫這一端。
用 SQL 直接查資料庫裡儲存的原始內容
在 phpMyAdmin 的 SQL 分頁,或任何資料庫管理工具裡,直接對出問題的那一列下查詢,跳過 WordPress 的快取層與樣板渲染:
SELECT post_title, post_content FROM wp_posts WHERE ID = 123;Code language: SQL (Structured Query Language) (sql)
把 ID 換成實際出問題的那篇文章編號,撈出來的結果就是資料庫裡真正儲存的內容。如果撈出來看到的是問號或西歐字母亂碼,代表問題就在資料庫這一端,接下來的排查方向才有意義;如果撈出來是正常的中文,代表資料庫沒有壞,問題出在前台的顯示或快取層,往下走轉碼流程只會白忙一場。
這個判準同樣適用於留言、頁面、自訂欄位。凡是懷疑亂碼的內容,第一步都先用同一支 SELECT 語法直接查對應資料表,留言查 wp_comments、自訂欄位查 wp_postmeta,養成先查原始值再動手的習慣,能省掉大半排查時間。
先排除瀏覽器快取、CDN 快取與缺字型的假象
如果上一步查出來資料庫本身是乾淨的,接下來要逐一排除三個常見的假亂碼成因,才能真正確認問題不在資料庫層級。第一個是瀏覽器快取住了搬家前的舊頁面,換一個無痕視窗或強制重新整理通常就能看出差異。第二個是 CDN 或頁面快取外掛還沒清乾淨,搬家後若沒有主動清空快取,前台顯示的可能還是搬家前那份已經壞掉、或編碼不一致的舊快照。
第三個成因是缺字型,瀏覽器或作業系統裡沒有能顯示某些生僻字或表情符號的字型,畫面上就會顯示成方塊,但這其實對應前面講的第二種亂碼樣貌,底層資料是對的,只是顯示不出來。這三項逐一排除之後,如果亂碼依然存在,才能確認自己真的在處理資料庫層級的編碼問題,可以放心往下走。
老站資料庫至今仍停在舊版 utf8,不是 utf8mb4
確認過亂碼真的出在資料庫層級之後,下一步該追的是老站資料庫為什麼原本應該完整支援中文,卻還是處理不了某些字元。台灣不少經營多年的中小型網站,資料庫至今其實還停在舊版的 utf8,正式名稱是 utf8mb3,沒有真的轉成 utf8mb4。這兩種字元集的差別,就是本篇幾乎所有症狀共通的技術根源。utf8 每個字元最多只能用 3 bytes 儲存,落在 Unicode 基本多文種平面之外的字元,包括部分生僻字與大多數表情符號,完全存不進去;utf8mb4 每個字元最多可以用到 4 bytes,才能完整涵蓋 Unicode。平常打繁體中文常用字,多半還落在 3 bytes 的範圍內,看起來相安無事,這也是為什麼很多站主完全不知道自己的資料庫還停在舊版字元集,直到遇到某些生僻字、少數表情符號,或是搬家過程把字元集宣告弄亂,問題才會集中爆發出來。

WordPress 從 4.2 版起,也就是 2015 年發布的版本,會在符合條件時自動把資料表升級成 utf8mb4,但這個自動升級有明確的前提條件,不是每個網站都會被升級到。具體要同時符合三個條件:目前使用的是 utf8 字元集、MySQL 伺服器版本 5.5.3 以上,含所有 10.x 版的 MariaDB、MySQL 用戶端函式庫版本 5.5.3 以上,若用的是 mysqlnd,則要 5.0.9 以上。老站如果建站當年主機環境不符合這幾個條件,或是後來換過幾次主機商、還原過舊備份,就有可能一直停留在 utf8,自己卻毫無感覺。「看起來中文都正常」從來不代表資料庫已經是 utf8mb4,這正是最容易被誤判的一點。
另外要留意,MariaDB 自 10.6.1 版起,把 utf8 這個名稱正式定調為 utf8mb3 的別名(在此之前,方向其實是反過來,utf8mb3 才被當成 utf8 的別名)。不論哪個版本,utf8 從頭到尾都只是那個最多 3 bytes 的舊版字元集,從來沒有自動等同過 utf8mb4。換句話說,就算伺服器設定寫著 utf8,也不能假設它其實是新版的意思,要實際查詢資料表的字元集才能確認。utf8mb4 對 utf8 是完全向下相容的,這也是為什麼 WordPress 團隊敢在符合條件時自動升級,不用擔心破壞既有資料。
wp-config.php 裡 DB_CHARSET 與 DB_COLLATE 兩個常數的分工
wp-config.php 裡有兩個常數,各自管不同的事。DB_CHARSET 指定資料庫連線的字元集,預設值是 utf8,也就是 utf8mb3;DB_COLLATE 指定校對規則,官方建議留空,讓 MySQL 依照 DB_CHARSET 自動指派對應的校對規則,不需要手動填。
這裡有一個判準務必記住,如果一個既有的網站,wp-config.php 裡原本就沒有這兩行設定,不要自己手動加上去,除非完全理解字元集轉換實際在做什麼。WordPress 官方文件明確警告過,對既有網站直接加入這兩個定義,可能會造成嚴重問題。這也解釋了為什麼不少站主照著網路教學手動改一行設定,反而把整個網站弄壞:他們動的是連線層級的宣告,卻沒有同步處理底下每一張資料表、每一個欄位實際儲存的字元集,兩邊對不上,亂碼只會變得更複雜。
匯出匯入之間,字元集在這幾個環節被換掉
就算資料庫已經確定是乾淨的 utf8mb4,搬家或還原備份這個動作本身,仍然是最常把字元集搞丟、也是資料庫亂碼最常見的成因之一。具體會出錯的位置集中在三處:匯出當下沒有明確指定字元集、匯入端又用了另一個字元集去解讀匯出檔、以及來源主機與目的地主機的 MySQL 或 MariaDB 版本落差太大。台灣不少中小型主機商彼此的資料庫版本落差明顯,有些至今還在用偏舊的 MySQL 版本,這也是搬家亂碼特別常出現在台灣網站上的實務原因。
mysqldump 匯出時沒指定字元集,會用主機預設值頂替
mysqldump 這支工具的 --default-character-set 官方預設值依版本而定,並不保證等於資料庫實際存的字元集;它不會主動去偵測這個資料庫真正用的是什麼字元集,只會依連線或設定檔的值去讀取並輸出資料。台灣不少中小型主機商還在跑偏舊的 MySQL 用戶端版本,其預設字元集停在 utf8(utf8mb3),一旦這個預設值跟資料庫實際字元集(utf8mb4)不同,資料在匯出檔案這一步就已經出錯。保險做法是不論主機新舊,匯出指令一律明確加上 --default-character-set=utf8mb4,或對應資料庫實際字元集的值:
mysqldump -u使用者 -p --default-character-set=utf8mb4 資料庫名稱 > backup.sqlCode language: Bash (bash)
這一步最麻煩的地方在於,後面不管怎麼匯入都救不回來,因為壞掉的是匯出檔本身,不是匯入的過程。判斷方法很直接,打開匯出的 .sql 檔案,用文字編輯器搜尋幾個已知的中文字詞,如果檔案裡看到的已經是問號或亂碼,代表匯出這一步就已經出錯,該重新用正確的字元集參數再匯出一次,而不是在匯入端想辦法補救。
phpMyAdmin 匯出時的 MYSQL40 相容模式陷阱
用 phpMyAdmin 匯出資料庫時,匯出頁面的格式特定選項裡有一個下拉選單,名稱類似「資料庫系統或較舊的 MySQL 伺服器,以最大化輸出相容性」,選到 MYSQL40 這個相容舊版 MySQL 4.0 的選項,會讓匯出檔裡不寫入資料表原本的字元集宣告,等於把原本 utf8mb4 的資訊在匯出檔裡整段拿掉;等匯入端沒有這行宣告可以依循,就只能套用連線或資料庫的預設字元集去解讀,跟原本 utf8mb4 存的內容對不起來,一樣會冒出亂碼。
這個選項原本是為了讓匯出檔能匯入非常舊的主機才存在,但很容易在沒有意識到的情況下被選中,尤其是照著幾年前的舊教學一步步操作,教學裡若剛好選了這個相容選項,就會在使用者完全不知情的狀況下,讓原本好好的 utf8mb4 資料在匯出這一步被動了手腳。判斷方法是打開匯出設定介面,確認這個下拉選單維持在無或不勾選相容模式,才不會平白讓資料先降編一次。
主機商 MySQL 版本落差導致的 Unknown collation 錯誤
台灣搬家過程中常見的一個具體錯誤訊息是 #1273 – Unknown collation: 'utf8mb4_unicode_ci'。它的成因是匯出端主機的 MySQL 或 MariaDB 版本較新,支援 utf8mb4_unicode_ci 這個校對規則,但要匯入的目的地主機版本較舊,低於 5.5.3,根本不認得這個名稱,於是匯入直接中斷報錯。
這個判準很重要,這種情況不是資料已經壞了,而是純粹的版本不相容,處理方式跟前面兩種,也就是匯出未指定字元集、phpMyAdmin 相容模式選錯,完全不同。碰到這個錯誤,第一件事是先確認來源與目的地主機的實際 MySQL 或 MariaDB 版本號,而不是急著在匯入指令上加各種參數硬試。版本落差找出來之後,通常只需要請目的地主機商升級資料庫版本,或是先把校對規則暫時改成兩端都支援的舊版本再匯入,等日後有機會升級主機再一併轉換。
宣告不符與位元組損毀,是兩種完全不同的處境
追出匯出匯入哪個環節出錯之後,接下來要回答一個更根本的問題,而這也是全篇最關鍵的一步,也是大多數官方文件完全沒講清楚的地方。同樣看起來是亂碼,實際上可能對應兩種本質完全不同的狀況,處理方式天差地遠。情況一是資料庫裡儲存的位元組其實是對的,只是欄位或連線宣告的字元集標錯了,MySQL 用錯誤的字元集去翻譯原本正確的位元組,這種狀況救得回來。情況二是資料已經被錯誤地轉碼過不只一次,也就是雙重編碼,位元組本身已經跟原意不同,這種光靠改宣告或轉換字元集救不回來,只能靠備份還原。分不清這兩種、直接對著已經雙重編碼的欄位做字元集轉換,常常會讓資料看起來更亂,而不是變好。

判斷方法是直接查欄位儲存的原始位元組,而不是看畫面上顯示的樣子:
SELECT post_title, HEX(post_title) FROM wp_posts WHERE ID = 123;Code language: SQL (Structured Query Language) (sql)
正確編碼的 UTF-8 字元,例如帶重音的拉丁字母 é,固定會是特定長度的位元組序列;如果同一個字元的 HEX() 結果比正常情況多繞了一圈、長出一截,就代表這筆資料是雙重編碼,位元組本身已經跟原意不符,不只是宣告錯誤那麼單純。反過來,如果 HEX() 撈出來的長度符合正常的 UTF-8 編碼規則,只是被錯誤的字元集顯示成亂碼,那就是情況一,還救得回來。
這一步之所以容易被跳過,是因為多數教學文章只教怎麼把資料庫轉成 utf8mb4,卻沒教轉之前要先分辨這筆資料還救不救得回來,這正是本篇跟一般教學文章最大的差異點。MySQL 官方文件對 ALTER TABLE 轉換字元集或校對規則時可能出現的既有資料異常也有明確說明:如果轉換過程出現重複鍵錯誤,原因通常是新的校對規則把兩個原本不同的鍵值視為相同,或者資料表本身已經損毀。但這裡有一個容易搞錯的地方——官方文件同時註明 REPAIR TABLE 只支援 MyISAM、ARCHIVE、CSV 這幾種儲存引擎,不支援 InnoDB,而 MySQL 自 5.5 版起把新建資料表的預設儲存引擎從 MyISAM 換成了 InnoDB,這是資料庫伺服器版本的變動,不是 WordPress 本身的版本;只要主機的 MySQL 是 5.5 以後、且沒有另外指定引擎,新安裝的 WordPress 資料表預設就會是 InnoDB。也就是說,如果撞見這個重複鍵錯誤、確認是表已損毀而非校對規則衝突,且資料表是 InnoDB,REPAIR TABLE 起不了作用,應該改用 OPTIMIZE TABLE 或直接匯出資料重建該表,而不是照抄一般 MyISAM 年代的教學去下 REPAIR TABLE。
換句話說,看到亂碼先別急著下 ALTER TABLE。先用 HEX() 抽查幾筆確定是哪一種處境,再決定接下來要走修正宣告、安全轉換這條路,還是直接放棄搶救,去找乾淨的備份還原。這個判斷順序錯了,後面每一步都會越修越亂。
轉成 utf8mb4 該走的順序,以及容易漏掉的陷阱
在確認過資料庫層級的問題,也判斷出是宣告不符還是位元組真的壞了之後,接下來才是實際動手轉換的順序。轉換不是改一個設定值就結束,資料庫、資料表、每個欄位三層都要一起確認,而且轉換順序錯了會製造出新的亂碼,不會修好舊的。
轉換前的完整備份不能只靠自動備份外掛
轉換字元集是有風險的操作,動手前一定要有一份確定乾淨、還沒被錯誤轉碼過的完整資料庫備份。這裡的判準是「確定乾淨」,而不是「有備份就好」。如果拿一份已經被雙重編碼過的舊備份去修復,只是在錯誤的基礎上疊加新的錯誤,轉完之後看起來更亂。
實務上建議至少保留兩份備份,一份是搬家或還原之前、確認過內容正常的最舊乾淨版本,一份是轉換前當下的完整快照。前者用來對照轉壞了要退回哪裡,後者用來確保萬一轉換過程出錯,至少能回到動手前的狀態,不用整個從頭排查一次。
資料庫、資料表與欄位三層都要一起轉
WordPress 資料庫的字元集其實分好幾層,資料庫本身的預設字元集、每一張資料表的字元集,以及資料表裡每個欄位各自的字元集與校對規則。只改 wp-config.php 裡的 DB_CHARSET,只會影響新建立的連線與新寫入的資料,不會動到既有資料表跟欄位;只下 ALTER DATABASE ... CHARACTER SET utf8mb4,也不會自動改到底下每一張表跟每個欄位。三層沒有同步轉換,就會出現「新文章正常、舊文章還是亂碼」或「這張表正常、那張表還是壞的」這種局部修好的假象。
WordPress 核心其實內建了一支判斷邏輯,可以拿來理解正確的檢查順序。maybe_convert_table_to_utf8mb4() 這支函式,會先用 SHOW FULL COLUMNS FROM 查出某張資料表每一個欄位的校對規則,只要有一個欄位不屬於 utf8 或 utf8mb4 家族,就整張表都不轉,這樣才不會誤傷本來就不是文字編碼的欄位,例如儲存二進位內容的欄位。確認整張表都可以安全轉換之後,才會實際執行:
ALTER TABLE wp_posts CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Code language: SQL (Structured Query Language) (sql)
逐一對每一張存文字內容的資料表下這道指令,才算把三層都真正轉過。
用 BLOB 過渡欄位型別避免二次轉碼
如果前一節判斷出資料位元組本身是對的、只是宣告標錯了,這裡有一個容易忽略的陷阱,就是不能直接對這種欄位下 CONVERT TO CHARACTER SET。因為 MySQL 會依照欄位舊的宣告去理解這些位元組,再重新編碼一次,等於在錯誤的理解基礎上又轉了一次,反而把原本只是標籤貼錯的資料,變成真正的雙重編碼損毀。
正確做法是先把欄位型別暫時改成 BLOB,因為 BLOB 不帶字元集屬性,MySQL 在型別轉換過程中不會嘗試重新詮釋內容,原始位元組會被原封不動地保留下來;再把型別改回 VARCHAR 或 TEXT,並指定正確的 utf8mb4 字元集,這樣位元組等於是被重新貼上正確的標籤,而不是被重新翻譯一次:
ALTER TABLE wp_posts MODIFY post_content BLOB;
ALTER TABLE wp_posts MODIFY post_content LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Code language: SQL (Structured Query Language) (sql)
這個技巧只適用於宣告不符的情況,救不了真的已經雙重編碼過的資料。這也呼應前一節的判斷,先用 HEX() 分清楚是哪一種,才知道這個 BLOB 過渡技巧用不用得上。
索引長度限制,utf8mb4 下要跟著縮短的欄位
有一個容易被忽略的技術限制,MySQL 標準設定下,單一索引最多允許 767 bytes。用 utf8,也就是 3 bytes 一個字元,計算,這等於最多 255 個字元可以建索引;換成 utf8mb4,也就是 4 bytes 一個字元,之後,同樣的位元組上限只夠 191 個字元,超過就會轉換失敗或直接報錯。
WordPress 核心資料庫裡,有幾個索引欄位確實需要跟著縮短長度才能完成轉換,包括 wp_usermeta.meta_key、wp_terms.slug、wp_terms.name、wp_commentmeta.meta_key、wp_postmeta.meta_key、wp_posts.post_name,多站台模式下另外還有 wp_site.domain、wp_sitemeta.meta_key、wp_signups.domain。轉換卡在這幾個欄位時,錯誤訊息通常會提到索引長度超過限制,判斷依據就是上面這條 767 bytes 的算式,碰到這個狀況先檢查是不是踩到這幾個欄位,而不是懷疑自己哪個步驟操作錯了。
轉換完成後先驗證效果,超出範圍再找主機商
轉換做完不代表結束,資料是不是真的救回來了,還是只是畫面看起來正常、底層其實還有問題,得靠實際驗證才能確認;同時也要看清楚什麼情況已經超出自己動手能處理的範圍,該把工作交給主機商,而不是繼續自己試各種轉碼組合。
用 SHOW TABLE STATUS 確認每個資料表都已經是 utf8mb4
用 SHOW TABLE STATUS 或查詢 information_schema 底下的 TABLES 與 COLUMNS,確認資料庫裡每一張表,尤其是 wp_posts、wp_comments、wp_postmeta 這幾張存放文字內容的表,的校對規則確實已經變成 utf8mb4_unicode_ci 系列,而不是只改了 wp-config.php 或資料庫層級的預設值,卻漏掉某幾張表沒轉到。
SHOW TABLE STATUS WHERE Name = 'wp_posts';Code language: SQL (Structured Query Language) (sql)
判斷依據很直接,Collation 欄位的值只要不是以 utf8mb4 開頭,這張表就還沒轉完,得回頭補這一張。表格層級確認過之後,建議再往下查一層欄位層級,也就是 information_schema.COLUMNS,因為有些表可能表層級已經是 utf8mb4,個別欄位卻因為手動加過欄位、繼承了舊的字元集,同樣要抓出來一併轉。
存入一個生僻字或表情符號,測試資料庫是否真的修復
光看資料庫欄位的設定值還不夠,最直接的驗證方式是實際操作一次,在後台編輯一篇文章,存入一個先前存不進去的生僻字或表情符號,存檔後重新整理,確認前台顯示正常,再回到資料庫用前面教過的 SELECT 語法把這筆資料撈出來確認也正常。
這一步的意義在於,它比單看設定值更能證明轉換確實生效。有可能設定值本身改對了,但實際寫入的路徑仍然有問題,例如某個外掛還在用舊的資料庫連線字元集,或是某段自訂程式碼裡硬寫死了 utf8 而不是讀取設定值,這些狀況只看資料表的設定值是看不出來的,一定要真的存一次才知道。
位元組已經損毀,找主機商復原比自己硬轉安全
呼應前面宣告不符與位元組真的壞了的判斷,如果用 HEX() 檢查後確認資料已經是雙重編碼,位元組本身已經跟原意不同,這種狀況不是靠任何 ALTER TABLE 或字元集設定救得回來的。
這時候唯一可靠的路,是找主機商調閱轉檔前的資料庫備份,或是自己保存的那份乾淨備份,整份還原回去,而不是繼續在已經壞掉的資料上嘗試各種轉碼組合。判斷的界線很清楚,位元組層級的損毀是不可逆的,繼續試只會離原始內容越來越遠,浪費的時間換不回任何東西;找備份還原,反而是最快也最安全的解法。
資料庫亂碼看起來像同一個問題,實際上每一種畫面背後都是不同的成因,處理順序也完全不一樣,搞清楚自己看到的是哪一種,比急著找一個一鍵修復的工具重要得多。下次網站要搬家或還原備份,不妨在動手前就先確認資料庫的字元集現況,匯出匯入指令都明確指定 utf8mb4,往往比事後排查亂碼省下好幾倍的時間。
