维基百科所用的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