meta charset utf-8:AdSense 爬蟲弄壞韓文圖片網址造成的 404 與 php-fpm 占滿問題
如果讀取端在 HTML 開頭找不到 meta charset utf-8 宣告,回應標頭也沒有 charset,就只能猜測文件的字元編碼,或用預設值讀取。在 Nginx 與 Apache 架構的 WordPress 伺服器上,AdSense 爬蟲集中對損壞的韓文(非 ASCII)圖片網址發出請求,處理 404 時把 PHP worker 全部占滿。依日誌追查後,我在快取頁面的回應標頭加上 charset,並讓 nginx 對不存在的靜態檔案直接回應 404,使同樣的請求不再占滿 worker。
1️⃣ meta charset utf-8:必須放在前 1,024 位元組內的原因
伺服器可以在兩個地方告知文件的字元編碼:回應標頭 [Content-Type] 後面附加的 charset,以及寫在 HTML 裡的 meta charset 那一行。
HTML 標準規定字元編碼宣告只能出現在前 1,024 位元組內,也建議讀取端只預先讀取這前 1,024 位元組來尋找。因此即使位於 head 之內,被推到 1,024 位元組之後的 meta charset 也可能讓部分讀取端找不到。
決定編碼時,讀取端會先看 BOM(位元組順序標記),接著是回應標頭的 charset,再來是預先讀取前 1,024 位元組時找到的 meta charset。三者都沒有時,就依位元組的樣子猜測,或用事先設定的預設值讀取,而在 HTML 標準建議的預設值表中,大多數 locale 是 windows-1252。
用 windows-1252 讀取 UTF-8 文件時,像韓文字母這樣由多個位元組組成的一個字元,會變成好幾個不同的字元。
2️⃣ AdSense 爬蟲 Mediapartners-Google 弄壞的韓文(非 ASCII)圖片網址
以前 1,024 位元組為基準檢視修正前的快取頁面,回應標頭的 Content-Type 沒有 charset,meta charset 那一行位於第 15,199 位元組。因此 AdSense 爬蟲 Mediapartners-Google 收到的快取頁面,兩個地方都找不到編碼。
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/html在這個架構中,只要在網址後面加上查詢字串,就會略過快取頁面,改由 PHP 直接產生回應,而那個回應確實帶有 charset。
curl -sI 'https://example.com/sample-post/?nocache=1' | grep -i '^content-type'content-type: text/html; charset=UTF-8快取檔案中 meta charset 從哪個位置開始,可以用 [grep -b] 確認到位元組單位。
grep -bo '<meta charset' /var/www/html/wp-content/cache/wp-rocket/example.com/sample-post/index-https.html | head -115199:<meta charset同一個快取檔案的 img 標籤裡,UTF-8 多語言檔名(這裡是韓文)未經百分比編碼就原樣放著。用 windows-1252 讀取這段 HTML 時,[대시보드-화면-예시.png] 這樣的名稱會變成 [대시보드-화면-예시.png] 這樣的字元,再把這些字元以 UTF-8 百分比編碼後請求,伺服器就會去找一個不存在的檔案。
從存取日誌中挑出這個爬蟲收到的圖片 404,名稱損壞的網址會接連出現。
grep 'Mediapartners-Google' /var/log/nginx/access.log | grep ' 404 ' | grep 'GET /wp-content/uploads/' | cut -d' ' -f1,4,7,9 | head -2203.0.113.3 [03/Nov/2026:20:40:51 /wp-content/uploads/2025/07/---1024x682.png 404
203.0.113.1 [03/Nov/2026:20:40:51 /wp-content/uploads/2025/07/---300x159.jpg 404日誌留下的網址,和把原始檔名的 UTF-8 位元組當成 windows-1252 字元讀取、再以 UTF-8 編碼後得到的值,連位元組都相同。當天日誌中以同樣方式損壞的圖片網址請求,來自這個爬蟲的有 202 筆,來自一支腳本(python-requests)的有 6 筆,來自訪客的是 0 筆。
把損壞的名稱 [대시보드-화면-예시.png] 放進 UTF-8 轉換器還原後,會被判定為 UTF-8 位元組被當成 windows-1252 讀取而損壞的情況,並得到原本的名稱 [대시보드-화면-예시.png]。
3️⃣ 不存在圖片的 404 占滿 php-fpm 的過程
損壞網址的圖片在伺服器上並不存在,因此 nginx 依圖片 location 規則中 try_files 的最後一項,把請求轉給 Apache,再由 WordPress 用 PHP 產生 404 頁面。查看同一天白天記錄的 587 筆 404,沒有快取的 404 頁面每個花費的中位數為 1.4 秒。
名稱損壞的圖片請求從 20:40 起 3 分鐘內湧入 200 多筆,其中 133 筆以 404 結束。php-fpm worker 每個請求分配一個,在每筆 404 花費 4 到 6 秒的期間請求不斷進來,於是在 20:40:55 達到集區上限。
集區達到上限的那一刻,會在 php-fpm 錯誤紀錄中留下 php-fpm max_children 警告。
grep 'max_children' /var/log/php-fpm/error.log[03-Nov-2026 20:40:55] WARNING: [pool www] server reached pm.max_children setting (20), consider raising it集區占滿後進來的請求要等待空出的 worker。等最久的 404 在 58 秒後結束,爬蟲等待 60 秒後中斷的請求記錄為 499,有一筆沒收到後端回應的請求記錄為 504。
同一時間執行的快取預先載入、排程工作(Action Scheduler),以及每分鐘執行的 wp-cron,經確認後都排除在原因之外。預先載入整天都以相同的低速執行,排程工作在那段時間只處理預先載入的工作,而 wp-cron 以命令列 PHP 執行,不使用 php-fpm worker。
4️⃣ nginx charset utf-8 與不存在的靜態檔案直接 404
我在輸出快取頁面的 nginx location 加上一行 [charset utf-8;],讓回應標頭帶有 charset;上傳資料夾中的圖片與靜態檔案若不在磁碟上,nginx 會直接回傳 404,不再轉給 WordPress。
# http 區塊:只對 404 回應設定 Cache-Control 值
map $status $up404_cc { 404 'no-store'; default ''; }
# 直接輸出快取 HTML 的 location
location / {
try_files $cache_file @backend;
charset utf-8;
}
# 上傳資料夾:不存在的圖片不轉給後端,直接 404
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
try_files $uri =404;
add_header Cache-Control $up404_cc always;
}map 放在 http 區塊中,而值為空字串時 nginx 不會送出該標頭,所以 200 回應不會帶上 no-store。正規表示式 location 由上而下套用先符合的那一個,因此我把它放在既有的圖片 location 之前,並為 css、js、字型這類靜態檔案再加了一個同樣方式的 location。
修正後再次請求同一個網址,快取頁面的回應標頭已帶有 charset。
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/html; charset=utf-8curl -sI https://example.com/wp-content/uploads/2025/07/no-such-image.png | grep -i -E '^(HTTP|content-type|cache-control)'# 修正前:WordPress 產生的 404
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# 修正後:nginx 直接回傳的 404
HTTP/2 404
content-type: text/html
cache-control: no-store不存在圖片的 404,從原本 1.01 秒產生 319,058 位元組的 WordPress 頁面,變成 0.015 秒回傳 555 位元組的 nginx 預設頁面。只對 404 加上 [Cache-Control: no-store],讓 CDN 不儲存不存在圖片的 404。PHP 已帶 charset 送出的回應,nginx 不會再加一次 charset,所以回應標頭中只出現一次。
| 確認的請求 | 修正前 | 修正後 |
|---|---|---|
| 不存在的圖片 | 319,058B,1.01 秒(WordPress 404) | 555B,0.015 秒(nginx 404) |
| 名稱損壞的圖片 | 1.05 秒(WordPress 404) | 0.014 秒(nginx 404) |
| 快取 HTML 的回應標頭 | text/html | text/html; charset=utf-8 |
| PHP 直接產生的回應標頭 | text/html; charset=UTF-8 | 相同(charset 一次) |
| 存在的圖片 | 200 | 維持 200 |
若網站使用的圖片外掛會在請求進來後才用 PHP 產生縮圖或 WebP,就不適合這種 404 處理。因為 nginx 會先回傳 404,圖片永遠不會被產生。如果您的架構是在上傳時就先把 WebP 做成檔案,可以直接套用。
5️⃣ meta charset 被推到後面的原因:WP Rocket Delay JS
即使回應標頭已加上 charset,快取 HTML 中的 meta charset 仍停在第 15,199 位元組。在 PHP 直接產生的頁面中,同一行位於第 135 位元組。
開啟 WP Rocket 的 Delay JS 後,產生快取檔案時會在 head 後面緊接著插入 lazyload 這類延遲執行的腳本,接著再經過一個把 meta charset 移回 head 最前面的步驟。這個移動步驟是對整份文件套用正規表示式,因此在約 1.2MB 的大型文章中會超過 PHP 的 backtrack 上限而失敗。
正規表示式失敗時,WP Rocket 會把已插入腳本、但 meta charset 沒能移動的 HTML 原樣存入快取,結果 meta charset 被推到前 1,024 位元組之外。用相同的 PHP 設定另外重現時,backtrack 上限為預設值 1,000,000 與 5,000,000 時移動失敗,從 10,000,000 起才成功。
這次是以在回應標頭加上 charset 的方式解決,沒有修正 meta charset 的位置。調高 backtrack 上限雖然能恢復 WP Rocket 原本的行為,但同一個 PHP 中執行的所有正規表示式上限都會增加到 10 倍,因此不建議。
🔢 FAQ & 推薦內容
ℹ️ 聯盟行銷說明
本站內容包含聯盟行銷連結。訪客透過這些連結購買商品或服務時,本站會從賣家獲得佣金。在這個過程中,購買者支付的金額(遇到活動折扣時金額會降低↓)並不會因此提高。刊登的價格、折扣與庫存資訊以撰寫當時為準,可能與實際情況不同,購買前請向賣家做最後確認。商品的挑選與評價依本站自訂的標準撰寫,是否支付佣金不會影響介紹順序或評價內容。