跳至內容
設定

WordPress 資料庫搜尋取代網址:不破壞序列化資料的做法

在資料庫裡更換 WordPress 網站網址,又不讓小工具、選單和佈景主題設定變成空白。用能保住序列化資料的 WP-CLI 指令。

發布於

千萬不要在 WordPress 資料庫上直接跑 UPDATE ... SET column = REPLACE(column, 'oldurl', 'newurl')。它確實會把文字正確換掉,同時也會把你的設定毀掉,因為 WordPress 把陣列和物件存成 PHP 序列化字串,而這種格式會記錄裡面每一段字串的位元組長度。純 SQL 的取代只改寫文字,完全不會動到記錄下來的長度,這筆資料從此就讀不出來了。

請改用能辨識序列化格式的工具。在指令列上就是 wp search-replace。先備份資料庫,先試跑一次,確認沒問題再真的執行。

實際壞掉的是什麼

一段 PHP 序列化字串長這樣:

s:18:"http://example.com"

那個 18 是後面這段文字的位元組長度。自己數一次 —— http://example.com 剛好就是 18 個字元。

現在想像一筆存起來的佈景主題設定:

a:1:{s:4:"logo";s:18:"http://example.com";}

用純 SQL 把 http:// 取代成 https://,你以為會得到:

a:1:{s:4:"logo";s:19:"https://example.com";}

但你不會。你實際得到的是這個:

a:1:{s:4:"logo";s:18:"https://example.com";}

字串現在是 19 個位元組,卻仍然聲稱自己是 18。PHP 讀取宣告的長度,往前走 18 個位元組,在它預期的位置找不到收尾的引號和分號,於是直接放棄。整個陣列反序列化失敗。

為什麼它會靜悄悄地失敗

這正是這個問題代價昂貴的原因。WordPress 不會拋出錯誤然後停下來。它讀取某個選項時會把值丟進反序列化的輔助函式,PHP 對這種格式錯誤的資料回傳 false,於是 WordPress 交給你的程式一個「就像從來沒設定過」的值。外掛或佈景主題接著退回它自己的預設值。

所以症狀不是錯誤頁面。症狀是:

  • 側邊欄的小工具不見了
  • 自訂器裡的設定退回佈景主題預設值
  • 頁面編輯器的版面資料變成空的,頁面渲染出來一片空白
  • 外掛的設定頁看起來像是剛安裝完
  • 選單失去它被指派的顯示位置

後台不會有任何地方告訴你出了問題。那筆資料還在資料庫裡,裡面看起來也還是正確的文字。錯的只有前面那個長度數字,差個一兩個位元組而已。反序列化失敗時 PHP 確實會發出警告,但在關閉錯誤顯示的正式站上,根本沒有人看得到。把 WP_DEBUG 打開,同時啟用 WP_DEBUG_LOG 並把 WP_DEBUG_DISPLAY 設為 false,這些警告就會寫進記錄檔,你才讀得到。

損害的分布也不平均。單純的純量值 —— siteurlhome、一般的文章內容、單獨佔一個欄位的網址 —— 經過粗暴取代之後完全沒事。只有序列化的值會壞。這就是為什麼一次糟糕的取代,乍看之下往往像是成功了。

動手之前先檢查 wp-config.php

如果 wp-config.php 定義了 WP_HOMEWP_SITEURL,這兩個常數會壓過資料庫裡的內容。改 wp_options 會看起來完全沒有效果。在你斷定問題出在資料庫之前,先找找有沒有這樣的幾行:

define( 'WP_HOME', 'http://example.com' );
define( 'WP_SITEURL', 'http://example.com' );

把它們更新成新網址,或是整行刪掉、交給資料庫決定。

先備份,這句話是認真的

序列化損壞不見得能輕易復原。一旦長度數字和內容對不上,你沒辦法從壞掉的值反推出原始的值 —— 不知道原始字串是什麼,就無法知道正確的長度應該是多少。備份是唯一真正的還原手段。

wp db export backup-before-replace.sql

如果沒有 WP-CLI,就用 mysqldump:

mysqldump -u USER -p DBNAME > backup-before-replace.sql

可以的話把備份放到伺服器以外的地方。躺在網站根目錄裡的備份檔,既是還原點,也是資安漏洞。

安全的指令

WP-CLI 的 search-replace 會把每個值反序列化,走過解碼後的結構,在裡面取代,再用正確的長度重新序列化。先試跑:

wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid

把報告讀完。它會告訴你每張表、每個欄位會發生多少次取代。如果資料表清單看起來不對,或某張你根本不認得的表出現好幾千筆命中,先停下來看清楚,再考慮真的執行。

確認無誤後執行:

wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only

關於這幾個參數,說點實話:

  • --all-tables 會涵蓋 WordPress 自己沒有註冊的資料表,而外掛的資料常常就住在那裡。少了它,有些外掛的資料表會被整張跳過。
  • --skip-columns=guid 很重要。guid 是給 RSS 閱讀器用的永久識別碼,不是實際運作的網址。改寫它可能會讓訂閱者把你的整個文章庫當成新文章收一遍。
  • 搜尋字串不要帶結尾斜線,也不要包含你不想動到的協定變體。取代單純的 example.com 會連電子郵件地址、以及文章內文裡的任何提及一起改掉。

如果你沒辦法使用指令列,支援序列化的搬家外掛能在後台做同樣的解碼工作。你要找的關鍵描述,是它明確寫出「處理序列化資料」;如果一個工具沒這樣寫,就當它不支援。

牽涉到哪些資料表和欄位

資料表欄位序列化風險
wp_optionsoption_value高 —— 小工具、佈景主題設定、暫存資料、大部分外掛設定
wp_postmetameta_value高 —— 頁面編輯器版面、自訂欄位陣列
wp_usermetameta_value高 —— 使用者權限、畫面與控制台偏好設定
wp_termmetameta_value中 —— 視外掛而定
wp_postspost_content, guid內容部分風險低,但短代碼屬性和區塊標記裡會夾帶網址
wp_commentscomment_content, comment_author_url
wp_linkslink_url, link_image低,而且通常沒在用

多站台環境還要加上 wp_blogswp_sitewp_sitemeta,以及各子站的 wp_2_optionswp_3_options 等等。多站台的網址變更除了搜尋取代之外還有額外的規則 —— 那要當成另一件事來處理。

search-replace 解決不了的情況

老實說說它的極限:

存在資料庫裡的 JSON。 有些頁面編輯器把版面資料存成 JSON,斜線是跳脫過的,所以你的網址在裡面長成 https:\/\/example.com,而不是 https://example.com。搜尋一般寫法會完全漏掉它。你可能得針對跳脫後的字串再跑第二輪。請先在複本上測試。

Base64 編碼過的值。 如果某個外掛在儲存前先把設定編碼過,任何以文字為基礎的取代都看不到那串網址。這類設定通常只能透過外掛自己的設定畫面重新輸入一次。

寫死在檔案裡的網址。 任何寫進 functions.php、子佈景主題範本,或是寫死的資源路徑,都不在資料庫裡,也不會被動到。佈景主題目錄要另外用 grep 掃過。

快取和產生出來的輸出。 物件快取、頁面快取和 CDN 上的副本,在你正確取代之後仍然會繼續送出舊網址。先清掉它們,再回頭檢查,不要急著認定取代失敗。

如果你已經跑了那道錯誤的指令

還原備份。這就是答案,直說比端出一套多半沒用的修補流程要好。

如果連備份都沒有,能救回來的路很窄:原本就沒有序列化的值沒事,壞掉的序列化資料只能靠手動重建,或是重新儲存對應的設定頁面來重設。重新儲存一次小工具區塊、重新指定一次選單位置,或重新輸入一次外掛設定,都會寫入一筆全新且序列化正確的值,覆蓋掉壞掉的那一筆。很瑣碎,但確實有效,而且總比把損壞的資料留在原地、讓下一個接手這個網站的人一頭霧水要好。

在動手之前想先產生正確的敘述句,可以用WordPress 搜尋取代 SQL 工具產出 —— 它會標示哪些欄位可以安全地用純 SQL 處理、哪些必須交給支援序列化的工具,讓你在決定指令之前先看清楚這條分界線。

FAQ

常見問題

為什麼直接用 SQL 的 REPLACE 會把 WordPress 網站弄壞?

因為 WordPress 把陣列和物件存成 PHP 序列化文字,而這種格式會記錄裡面每一段字串的位元組長度。直接下 REPLACE 只會改掉文字,長度數字仍然停留在舊值。PHP 因此拒絕反序列化這筆資料,WordPress 退回預設值,設定看起來就是一片空白,而不是內容錯誤。

更換 WordPress 網站網址最安全的做法是什麼?

先做一份完整的資料庫備份,接著執行 WP-CLI 的 wp search-replace,而且第一次一定要加上試跑參數。WP-CLI 會把每個值反序列化、在解碼後的結構裡取代,再以正確的長度重新序列化。如果沒有指令列環境,支援序列化的搬家外掛也能在後台完成同樣的工作。

舊網址會存在哪些資料表裡?

主要是 wp_options、wp_posts、wp_postmeta、wp_usermeta、wp_termmeta 和 wp_comments。序列化的風險集中在 wp_options 以及任何 meta_value 欄位,因為小工具、佈景主題設定、暫存資料和頁面編輯器的版面資料都是以陣列型態儲存。多站台環境還要加上 wp_blogs、wp_site 和 wp_sitemeta,網域也記在這幾張表裡。

如果 wp-config.php 有定義網址,改資料庫還有用嗎?

沒有用。只要 wp-config.php 定義了 WP_HOME 或 WP_SITEURL,這兩個常數就會蓋過 wp_options 裡的 siteurl 和 home。改了資料庫也不會有任何可見的變化,除非常數被更新或刪除。請先檢查 wp-config.php,很多搬家失敗的案例就卡在這一行。

guid 欄位也要一起取代嗎?

不要。guid 是給 RSS 閱讀器辨識用的永久識別碼,不是實際運作的網址,改寫它可能會讓訂閱者把每一篇舊文章都當成新文章重新收到一次。執行 WP-CLI 時請用 skip-columns 排除 guid。唯一常見的例外,是從來沒有對外提供訂閱來源、也沒有訂閱者的網站。

取代出錯之後,怎麼判斷某一筆資料壞掉了?

壞掉的序列化資料在資料庫裡看起來完全正常,但在 WordPress 裡讀出來會是空的。小工具消失、佈景主題自訂設定被重設、外掛選項退回預設值,而且完全沒有錯誤訊息。開啟 WP_DEBUG 和 WP_DEBUG_LOG 就能把反序列化的警告記錄下來。把宣告的字串長度和實際的位元組數對比一下,也能直接確認。