維基百科:模板限制
維基百科所用的MediaWiki軟件使用一組參數來限制頁面的複雜度和頁面包含其他頁面的數量。這些限制作用於解析一個頁面時進行包含頁面或者替換引用頁面是怎樣工作,而不包含解析該頁面時的原始源碼的情況。本頁面是解析該限制是如何工作的,和如何在這些限制下正常使用模板等功能。
背景
這是什麼?
MediaWiki使用語法分析器來將wikitext轉化為HTML顯示。它是通過「預處理器」將wikitext整理為一種類似XML的數據結構,然後再將其「展開」,將雙尖括號(包括包含頁(也就是模板)、變量、魔術字、解析器函數等)和三尖括號(例如:模板變量)括着的內容替換為相應的值。
當頁面進行解析時,會生成若干個計數器用於跟蹤頁面生成的複雜度,當頁面開始解析時,計數器初始為0,當頁面的解析行為達到計數器的限值時,解析處理會被限制。
為什麼要限制?
非常長的或複雜的頁面解析起來非常費時。對於用戶的請求體驗來說相當不好,而且很容易被黑客利用來進行DDOS攻擊——也就是請求MediaWiki去處理解析極為大量的不合理數據。這些限制可以用來防禦這種攻擊,同時控制頁面的渲染在合理的時間內。當然,有時過於複雜的頁面可能會顯示為請求超時,這取決於伺服器的運行負載。
關於限制的作用
當頁面達到限制時,最常見的處理辦法是,參見下面的方法,將模板的大小縮小。如果實在辦不到,儘量將模板的內容直接展開到原始的原始碼中,而非通過模板嵌入來讓渲染時展開。(例如:直接使用<references />代替{{reflist}})。不過另一方面,模板有助於伺服器避免重複處理一些相同的解析數據。
什麼時候出現問題?
頁面包含上限通常出現大量地調用同一個模板,例如在一個非常長的表格里每行調用一次。雖然每次調用可能只會往解析頁面中添加很小的內容字節數,但其每次調用依然會被統計到計數器,導致頁面過早地達到包含上限。一般情況,頁面只使用很少量的模板並不會這麼快達到限制,除非每個模板都包含大量的內容字節數。
如何取得限制報告
當頁面完成渲染後,會在頁面內容渲染輸出層(也就是<div class="mw-parser-output">)的結尾輸出一段以HTML註釋標註的名為「NewPP limit report」的限制報告,將會包含各計數器最終計數和一些模板用時信息。由於計數器的統計方式,Preprocessor visited node count、Preprocessor generated node count、Post‐expand include size這三個計數器通常會少於限值的,如果這三個值逼近限制的話,可能會出現部分模板內容沒有展開(而連結的方式顯現)。沒有展開的模板位置會標註出來並包含相應的錯誤信息。
下面是Wikipedia:沙盒在2020年9月23日 (三) 10:36 (UTC)的限制報告例子。
<!--
NewPP limit report
Parsed by mw2316
Cached time: 20200923103611
Cache expiry: 2592000
Dynamic content: false
Complications: []
CPU time usage: 0.136 seconds
Real time usage: 0.185 seconds
Preprocessor visited node count: 358/1000000
Post‐expand include size: 16223/2097152 bytes
Template argument size: 4557/2097152 bytes
Highest expansion depth: 11/40
Expensive parser function count: 6/500
Unstrip recursion depth: 0/20
Unstrip post‐expand size: 1978/5000000 bytes
Lua time usage: 0.041/10.000 seconds
Lua memory usage: 1.11 MB/50 MB
Number of Wikibase entities loaded: 0/400
-->
<!--
Transclusion expansion time report (%,ms,calls,template)
100.00% 138.492 1 -total
96.32% 133.399 1 Template:請注意:請在這行文字底下進行您的測試,請不要刪除或變更這行文字以及這行文字以上的部份。
47.18% 65.341 1 Template:Shortcut
25.37% 35.135 1 Template:Columns
8.06% 11.159 1 Template:If_mobile
6.61% 9.148 2 Template:Fullurl
6.43% 8.909 2 Template:Fullurl2
3.73% 5.161 1 Template:Div_col
3.08% 4.260 1 Template:请注意:请在这行文字底下进行您的测试,请不要删除或变更这行文字以及这行文字以上的部分。
1.55% 2.149 1 Template:NoEdit
-->
另外還有使用JavaScript腳本注入相應的報告數據,可以通過調用JavaScript APImw.config.get("wgPageParseReport")
來獲得。
或者可以通過調用Mediawiki APIaction=parse&prop=limitreportdata|limitreporthtml
來獲得本次API查詢即時的頁面或wikitext的解析器限制報告的信息(包括用於JavaScript的可讀數據和人類可讀的HTML數據。),注意的是,只限於本次API查詢(可以理解為重新調用parse來渲染本次的頁面解析請求),其數值可能與現有的頁面輸出有少許差異。
關於展開
模板中未被執行的分支數據是不會被展開解析的,所以不會計入計數器中。例如wikitext{{#if:yes|{{bar}}|{{foo}}}}
,{{bar}}
會被展開,而{{foo}}
則不會被展開。但相反地,模板參數的內容展開可能導致計數增加,即使參數的內容最終不會被輸出到最終結果,例如wikitext{{#if:{{foo}}|yes|no}}
在解析時,{{foo}}
展開後的內容字節數會被計算入展開後計數中,因為必須將{{foo}}
展開後才能判斷需要選擇哪個顯示分支。
部分計數器參數解析
預處理器節點計數
預處理器節點(Preprocessor node)表示的是頁面的複雜度(但不是頁面的內容大小)。在頁面被渲染時,會生成類似樹形的數據結構用對應其生成的HTML樹結構。在展開時樹節點的每次訪問會被計入到預處理器訪問節點計數(Preprocessor visited node count)中,當達到限制時,會生成「Node-count limit exceeded」的顯示錯誤。
純文本計數為1,一對<nowiki>的內容計數為3,一個標籤頭計數為2,如此類推。一個連結內容不會計數,而{{#switch}}每添加一個判斷條件計數增加2。相同內容的模板如果不傳入參數的話會只被計算1次,但傳入參數(即使是常數)的話則分別計算。相比之下,如果模板只傳入一個固定的模板參數,而且模板被多次同樣的調用,該模板的展開結果將會被多次重複利用。
超過該限制的頁面將會在Category:頁面的節點數超出限制中顯示。
模板展開後長度
模板展開後長度(Post‐expand include size)是指將模板、變量、解析器函數展開後的wikitext的字節總和。當解析器將一個模板的源碼展開出來時(也就是模板被調用頁面嵌入或者替換引用),其展開的代碼長度會被累加到計數器中。如果該計數器值超過限制,解析流程會被限制,展開內容不會替換上去,並且生成一段包含錯誤信息的HTML註釋插入其中。如未超過限制,計數器將被替換為新的計數值,並且繼續解析。同一個模板被多次展開將會多次計入計數值,例如第一層wikitext是10位元組長,其中4位元組普通文字,6位元組調用模板A的代碼,模板A的wikitext有10位元組長,模板A展開後,6位元組被替換為10位元組,計數器累計為14位元組。
調用不帶參數的模板會緩存其展開後內容的wikitext。所以若模板A包含沒參數的模板B,模板B在模板A中多次調用都只會被計算一次展開後的長度值,但是如果模板B需要傳入參數的話,則每次調用帶參數的模板B,就算是傳入了相同的參數,還是會加上一次展開後的長度值。
超過該限制的頁面將會在Category:引用模板後大小超過限制的頁面中顯示。
使用註釋、<noinclude>、<onlyinclude>
只有通過預處理器擴展階段的數據才會被計算入展開後計數器中。使用HTML註釋的代碼部分是不會被計算入展開後計數器中,而且其結果也不會被輸出到最終HTML代碼中。在<noinclude>內或<onlyinclude>外的wikitext也不會被展開而計算到展開後計數中。這也意味着通過包含模板來對頁面進行分類時,只有被包含時產生分類效果才會貢獻展開後計數值。
嵌套展開
注意,所有被展開的模板和解析器函數的wikitext展開值是累加值,即使是嵌套的情況。(phab:T15260)所以會產生額外的計數重複。例如模板A包含模板B,模板B包含模板C,模板C的展開後字節數會被模板A的計數器計算了2次。類似有,在模板中包含一個解析器函數,或解析器函數使用了模板的輸出值作為其輸入參數等。所以有時候需要使用直接產生模板的調用名來代替直接產生模板結果,來避免這種計數重複。
例如:
{{#if:{{{test|}}}|{{template1}}|{{template2}} }}
應該替換為
{{ {{#if:{{{test|}}}|template1|template2}} }}
沒被渲染出來的展開
沒被渲染出來的展開也可能會被計入統計數中,常見的是如解析器函數中if的條件判斷是通過輸入一個模板展開後的內容,例如這樣{{#if:{{SB}}|...}},這樣判斷內容即使沒輸出渲染,一樣被算入展開的計數中。對於Lua模組也有類似道理,例如通過mw.getCurrentFrame():preprocess
的外部解析器方法解析內容但又沒有將其輸出,一樣被算入展開的計數中。
#invoke 語法
有些模板實際上其Lua模組實現的包裝(warpper),例如{{Navbox}},如果不調用其模板而是直接調用其內部調用模組的語句(例如{{Navbox}}以{{#invoke:Navbox|navbox|...}}代替),也能降低展開量計數,原理實際就是嵌套展開。但由於這樣會降低代碼可讀性,非不得已,不建議這樣做。
拆分條目
理想情況下,條目長度應該由內容相關決定而非技術問題,但如果由於一個長條目(例如長列表)無法解決展開量問題,可能需要拆分條目來使每個小條目都不超過展開量限制。
模板參數字節計數
模板參數字節計數(Template argument size)是用於跟蹤被替換的計算模板參數總長度。
例如,假設{{2x}}用於將參數1的內容連續複製2次,同理,{{3x}}為複製3次,則{{3x|{{2x|abcde}}}}計算器記為40位元組,參數「abcde」計算了2次,參數「abcdeabcde」計算了3次。
模板傳入參數沒被模板內變量匹配調用的,不計入計算器中。
如果使用了switch解析器函數,沒被匹配的參數不會計入計算器,如果存在匹配的話,匹配的鍵參數長度會被計算為2次,匹配的值參數長度計算1次,按照其賦值的展開後值長度計算。
包含該頁面超出模板參數大小限制的頁面將會在Category:含有略過模板參數的頁面中顯示。
最大擴展深度
最大擴展深度(Highest expansion depth)用於跟蹤模板展開後所達到的最大層級計數,該計數器限值默認為40。
超過該限制的頁面將會在Category:模板遞歸深度超出限制的頁面中顯示。
高開銷解析器函數調用次數
高開銷解析器函數調用次數(Expensive parser function calls)用於跟蹤部分高開銷的解析器函數的使用次數,該計數器限值默認為500。
以下為屬於高開銷的解析器函數、魔術字或相應調用:
- #ifexist——判斷是否存在特定頁面來選擇分支。當達到限制時,將會認為特定頁面不存在。
- PAGESINCATEGORY 或 PAGESINCAT
- PAGESIZE
- CASCADINGSOURCES
- REVISIONUSER
- REVISIONTIMESTAMP
- 部分Lua對象方法
在Lua腳本中,可以通過調用mw.incrementExpensiveFunctionCount來手工增加高開銷解析函數的調用次數數量。
超過該限制的頁面將會在Category:有過多高開銷解析器函數調用的頁面中顯示。
關於{{#time}}解析器函數
{{#time}}的格式化字符串被限制在6000個字符,超出則顯示錯誤(對應消息為MediaWiki:Pfunc_time_too_long)。一個格式化字符串或時間表達式展開後的wikitext,將會被緩存起來,能被重複使用,而只計算一份展開字節量。
本方法調用沒有在計數器顯示統計數據。
Special:展開模板
當頁面達到限制時,一個比較粗糙的排查方法是通過Special:展開模板來排查。不同於替換,它會遞歸地展開全部層級,不需要使用safesubst:
或類似的代碼來將其替換展開。除了預處理器節點計數器,其他計數器將會設定為0,來降低模板展開的限制。
歷史
2006年8月14日,User:Tim Starling 在英語維基百科上開始嘗試限制模板的引用。而新的解析處理器則在2008年1月投入使用,並且將其中一個參數「展開前計數」(pre-expand include limit)改用預處理器節點計數器來代替。其中2006年的功能引入確立了展開前計數的限制為2MB,該限制也延續到相似參數展開後計數中。
部分模板限制問題的常見解決
- 可使用{{NavboxV2}}嘗試解決{{Navbox}}導致的模板超載問題,尤其是以子Navbox作為每list項分組內容時。
- 直接連結到模板頁面,而非嵌入一個展開字節量大的模板、作為另一個模板的傳參,清理範例如Special:Diff/70473549。
- 減少使用{{Navboxes}}收納完整模板(使用Navboxes收納模板內部連結不在此限),或者使用{{Navboxes top}}和{{Navboxes bottom}}配套使用代替。
- 模板中使用
hlist
css類名加上wikicode無序列表語法(*
),代替{{•}}
、{{·}}
、{{,}}
、{{.w}}
等模板,因為後面這些模板展開字節較大。清理範例如Special:Diff/70435404,待清理的模板可見Category:沒有使用水平列表的導航框、[1]、[2]、[3]、[4]。 - 將參考列表模板
{{reflist}}
展開為純HTML內容加<references />
。頁面解析逼近展開限制值時,無法繼續渲染{{reflist}}
。 - 避免嵌入太多公共轉換組到{{noteTA}}。部分轉換組展開量很大,除非裏面的項目能大量適配行文中用詞,否則沒必要為一兩個用詞引入龐然巨物。可參考高級字詞轉換語法的H語法(清理範例如Special:Diff/74029949),單次使用手工字詞轉換(清理範例如Special:Diff/71591061),或者在{{noteTA}}添加少量涉及的用詞轉換。
- 將「已有本地條目的跨語言連結」改為一般內部連結,清理方式跟待清理列表見Category:有藍鏈卻未移除內部連結助手模板的頁面,特別應優先清理模板因為影響頁面較多。
其他參考資料
- 當時的互助客棧技術版的討論 (已存檔到en:Wikipedia talk:Template limits)
- Tim's posting on wikipedia-l
- 設置中對解析器計數器限制有相應的設置
- mw:Manual:$wgMaxArticleSize:控制頁面最大大小,和頁面預展開,模板參數字節長度等有關。
- mw:Manual:$wgExpensiveParserFunctionLimit:高開銷調用的限制值。
- mw:Manual:Configuration settings#Parser:對解析器的設置,大部分計數器限制歸類於此。
- mw:NewPP parser report