meta charset utf-8:AdSense 爬蟲弄壞韓文圖片網址造成的 404 與 php-fpm 占滿問題

測試環境RHEL 系列AlmaLinux 9.8Nginx 與 Apache 架構由 nginx 直接回應快取外掛的靜態 HTML2026-11-04 確認
部分網址、路徑與數字為範例值

如果讀取端在 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 文件時,像韓文字母這樣由多個位元組組成的一個字元,會變成好幾個不同的字元。

🧭 meta charset utf-8 인코딩 결정 순서
설명 다이어그램
🔍 순서
📄 응답
🔖BOM(바이트 순서 표시)있으면 그 인코딩으로 정함없음없음없음
📨Content-Type 머리의 charset있으면 그 값으로 정함charset 없음charset 없음
🏷️meta charset 줄앞 1,024바이트 안에 있으면 그 값으로 정함1,024바이트 뒤로 밀려 찾지 못함
➜
🔎 읽는 쪽
🔤문서 인코딩찾은 값으로 읽음찾은 값으로 읽음찾은 값으로 읽음짐작하거나 기본값 windows-1252로 읽음
인코딩을 정할 때는 BOM(바이트 순서 표시)을 먼저 보며, BOM이 있으면 그 인코딩으로 읽습니다.
BOM이 없으면 다음은 응답 머리 Content-Type 뒤에 붙는 charset입니다.
그다음이 앞 1,024바이트 미리 읽기로 찾은 meta charset입니다. head 영역 안에 있더라도 1,024바이트 뒤로 밀린 meta charset은 찾지 못하는 읽는 쪽이 생깁니다.
셋 다 없으면 바이트 모양으로 짐작하거나 정해 둔 기본값으로 읽게 됩니다. 머리에 charset이 없고 meta charset이 1,024바이트 뒤에 있으면, 대부분의 locale에서 기본값인 windows-1252로 읽힐 수 있습니다.

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 -1
15199:<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 -2
203.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 秒。

🌐 Mediapartners-Google名稱損壞的圖片網址請求
nginx 圖片 locationtry_files 檢查磁碟上的檔案
有檔案nginx 直接回應 200
沒有檔案轉給 try_files 最後一項的後端
Apache → WordPress用 PHP 產生 404 頁面(中位數 1.4 秒)
php-fpm worker一個請求一個 worker
集區上限(pm.max_children)占滿後,後面的請求要等待(499、504)

名稱損壞的圖片請求從 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-8
curl -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/htmltext/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 & 推薦內容

Google 的爬蟲文件說明,針對 Mediapartners-Google 的檢索設定會影響 AdSense 與 Ad Manager 這類廣告產品。這個爬蟲不遵守 robots.txt 的全體規則 [User-agent: *],要封鎖就必須另外寫出它的名稱。如果問題是損壞網址的 404,讓網頁伺服器對不存在的檔案直接回應 404,就不會動到廣告檢索。

HTTP 快取標準規定儲存回應時要連同收到的回應標頭一起儲存,因此修正前 CDN 儲存的副本會照舊以沒有 charset 的 [Content-Type: text/html] 送出。等儲存期限到了、重新從原始伺服器取得,或只從 CDN 快取清除那篇文章的網址後,就會換成帶有 charset 的新標頭。

URL 標準規定網址路徑中的 ASCII 以外字元,要轉成 UTF-8 百分比編碼後再請求,因此以 UTF-8 正確讀取文件的一方會用原本的檔名請求。只用英文字母與數字的檔名,即使文件被誤用 windows-1252 讀取也會維持相同字元,所以就算頁面同時缺少回應標頭的 charset 與 meta charset,網址也不會損壞。

ℹ️ 聯盟行銷說明
本站內容包含聯盟行銷連結。訪客透過這些連結購買商品或服務時,本站會從賣家獲得佣金。在這個過程中,購買者支付的金額(遇到活動折扣時金額會降低↓)並不會因此提高。刊登的價格、折扣與庫存資訊以撰寫當時為準,可能與實際情況不同,購買前請向賣家做最後確認。商品的挑選與評價依本站自訂的標準撰寫,是否支付佣金不會影響介紹順序或評價內容。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Prove your humanity: 10   +   6   =