维基词典:Scribunto
維基詞典支援伺服器端腳本來為頁面產生內容,使用的是Scribunto擴充功能。它被用作模板的補充,特別是解析器函數如{{#if:}}、{{#switch:}}等等。腳本被分為位於Module:命名空間中的模組,並且使用Lua程式語言編寫。
入門
[编辑]以下是一些有用的連結,幫助您開始學習Lua和Scribunto。
- Learning Lua – 如果您還不熟悉這個語言,或者對程式設計不太了解,這是一個很好的起點。這不涵蓋在wiki中使用Lua的特定部分,只是「通用」的Lua。
- Scribunto/Lua教學 – 一個簡短的教學,解釋如何在wiki中使用Scribunto/Lua。
- Scribunto/Lua參考手冊 – 適用於Scribunto擴充功能的Lua參考手冊。這也列出了普通Lua中不存在的Wiki特定功能。
- Lua 5.1官方參考手冊 – 語言的快速參考,適用於有經驗的程式設計師。同樣,這是通用的Lua,不涵蓋在維基詞典上使用它的具體細節,但它有Scribunto特定手冊所缺少的資訊。
- lua-users wiki – 一個由使用者編寫的wiki,包含許多關於Lua各個方面的文章。
- Programming in Lua by Roberto Ierusalimschy,Lua的創造者之一 – 深入討論Lua 5.0的基礎知識。
關於在維基詞典上特別使用Scribunto的資訊:
- /轉換模板 - 一些將模板「翻譯」為Lua的常見做法的技巧和竅門。
- Wiktionary:編碼慣例
Scribunto如何與wiki互動
[编辑]Scribunto模組本身實際上是一個大函數:它從上到下執行,並且期望回傳一個值。通常,模組的回傳值是一個函數及其名稱的列表,然後可以從另一個模組或從「wiki空間」呼叫這些函數。但是,理論上,模組可以回傳函數列表以外的其他內容。它可以回傳字串表、包含其他表的表,甚至是單一值。但是,只有回傳函數及其名稱列表的模組才能從wiki文本中呼叫;其他任何內容都只能從Scribunto本身內部匯入和使用。
Scribunto模組中的函數從wiki文本中呼叫時就像是解析器函數一樣,使用這種記號:{{#invoke:moduleName|functionName}} 然後函數回傳wiki文本作為其輸出。這個wiki文本可以包含HTML風格的wiki文本(如<b>和<table>等等)和wiki特定的標記(如'''…'''用於粗體和[[…]]),但不能呼叫模板、解析器函數、魔術字或解析器擴充標籤(例如,{{!}}這樣的東西會被解釋為字面字串{{!}},而不是被展開為|)。如果模組需要呼叫模板或解析器函數,它必須為此目的使用mw命名空間中的特殊函數。但希望這些函數不會太經常被需要。
由於函數為了有用,需要關於其被呼叫的上下文的資訊,Scribunto擴充功能會向它傳遞一個參數,習慣上命名為frame。這個參數可以用來獲取各種關鍵的資訊;特別是,如果參數paramName=paramValue被傳遞給呼叫Scribunto函數的模板,那麼在函數內部,frame:getParent().args.paramName將是字串'paramValue'。(任何編號/未命名參數都可以按編號存取;例如,模板的{{{1}}}變成模組的frame:getParent().args[1]。)即使在沒有frame參數的情況下,Scribunto程式碼可以使用getCurrentFrame()來獲取其值;因此,函數實際上不需要將frame傳遞給彼此。模組可以呼叫Module:parameters中的process來清理參數,並對其有效性進行一些檢查。
模板也可能(儘管通常不是必要的)在呼叫過程中傳遞進一步的參數,在這種情況下,這些參數將通過frame.args可用。例如,如果模組使用{{#invoke:moduleName|functionName|paramName=paramValue}}呼叫,那麼frame.args.paramName將是字串'paramValue'。(編號/未命名參數類似地工作。)
調試和錯誤報告
[编辑]當Lua在腳本中遇到錯誤時,它會中止腳本並在頁面上顯示大紅色、可點選的「Module error」文字。點選此文字以查看導致錯誤的原因。
模組錯誤也會將頁面新增到Category:Pages with module errors。在編寫模組或轉換模板時,最好檢查這個分類,看看使用它的任何頁面是否觸發了錯誤。您也可以自己觸發錯誤,使用以下方法:
error("You forgot to supply a required parameter!")
這可以用來檢查模組及其配套模板是否被正確使用,否則向使用者顯示錯誤。強烈建議您盡可能使用這個功能,以使您的模組更加穩健並更容易發現錯誤。
當您正在處理腳本時,偶爾產生調試訊息以便您可以看到腳本中特定點發生的情況可能會很有用。您可以使用mw.log函數來做到這一點:
mw.log("Testing the script. The value of the variable 'a' is : " .. a)
如果您在調試控制台中執行模組,例如透過鍵入p.main()(如果您要執行的函數名為main且不需要參數),此函數會將其參數輸出到Scribunto調試控制台。它會自動在訊息末尾新增換行符。
函數os.clock可以用於給定函數的簡單基準測試。它可以這樣使用:
function p.foo(frame)
local start = os.clock()
-- do whatever the function needs to do here
mw.log("Function took " .. os.clock() - start .. " seconds.")
-- return
end
當為執行腳本分配的時間在頁面上的所有腳本都能執行完成之前就到期時,也會發生錯誤。如果您正在對模組進行複雜且可能耗時的編輯,您可以使用「Preview page with this template」來預覽一個非常大的、模組密集的頁面,如[[a]],以檢查您的腳本是否過度拖慢了它。
中文維基詞典也有自己的專門調試模組,恰當地命名為Module:debug。函數track可以用來追蹤滿足特定條件的條目,而不干擾函數或模板的操作。它的目的與Category:模板追蹤相似。
「框架」和「親代框架」
[编辑]實際上有兩種方式可以將值傳遞給Scribunto模組。第一種是上面顯示的方式,其中值作為參數直接傳遞給模組呼叫。所以例如,如果有一個Lua函數LanguageData.getLangName,它為zh產生(比如說)漢語,模板{{langname}}會呼叫並傳遞參數,其他頁面會透過編寫(例如){{langname|zh}}來存取這個函數。使用這種方法,Lua函數需要存取傳遞給#invoke的參數;為此,它可能這樣編寫:
{{#invoke:LanguageData|getLangName|{{{1}}}}}
function LanguageData.getLangName(frame)
local args = frame.args
local langCode = args[1]
local langName = ... -- some code to determine langName
return langName
end
但是,還有另一種方式,推薦使用,因為它更快來源。每個模組也可以存取其所謂的「親代框架」,它包含傳遞給模組的參數集合,而不是傳遞給呼叫它的模板的參數。所以,與其呼叫模組並明確傳遞值,不如不帶參數呼叫模組。模組本身可以使用親代框架存取傳遞給模板的參數。上面的例子然後會這樣編寫:
{{#invoke:LanguageData|getLangName}}
function LanguageData.getLangName(frame)
local args = frame:getParent().args
local langCode = args[1]
local langName = ... -- some code to determine langName
return langName
end
如您所見,唯一真正的區別是使用frame:getParent().args來獲取「親代框架」(即模板呼叫)的參數,而不是模組呼叫本身的參數。
可以編寫支援兩種方法的函數(透過使用invokeArgs[1] or templateArgs[1])。這對於可以從模板以及另一個模組或多或少直觀地呼叫的簡單函數可能偶爾有用。但對於更複雜的函數,最好在一個函數中編寫「主要」程式碼,並編寫另一個可以從模板呼叫的函數,然後收集參數並呼叫主函數。
請注意,從模板傳遞的空參數「計算在內」;即模板呼叫{{MyTemplate||MySecondArgument}}會導致相關條件
if args[1] then
-- <do something>
end
被滿足,因為空字串被解釋為true。程式碼
if args[1] and args[1] ~= '' then
-- <do something>
end
另一方面,只會回應非空的第一個參數。
效率
[编辑]Lua的效率可以透過模板預覽功能檢查:按下「預覽」後,右鍵點選頁面預覽並請求頁面原始碼。在頁面原始碼中,搜尋「NewPP」以查看Lua模組的執行花費了多少時間(例如:Lua time usage: 0.004s)。搜尋「served」以查看呈現整個頁面花費了多長時間(例如:Served by mw1035 in 0.498 secs)。後面這個時間可以用來比較使用或不使用Lua模組時呈現同一頁面需要多長時間。
可以使用各種技術來提高效率。以下來自一個章節,出自Lua Programming Gems:避免在迴圈內建立函數和表格。使用區域變數。記憶化昂貴的函數。透過使用table.insert將字串插入表格並使用table.concat建立最終字串,避免大量單獨的字串連接操作。
每個單獨的連接操作(無論它涉及兩個字串,"a" .. "b",還是幾個,"a" .. "b" .. "c" .. "d")都會產生一個新字串(部落格文章),它被儲存在Lua記憶體中。許多連接操作(例如,在迴圈中)可能會使用大量記憶體,因為會建立許多中間字串。
要記憶化有一個參數和一個回傳值的函數,您可能能夠使用Module:fun中的memoize函數。但要注意它不能作為gsub的第三個參數使用,因為它回傳一個表格。
對於Scribunto特別來說,在執行大量字串操作時使用基本string函數而不是mw.ustring函數會提高效率。Ustring函數主要在PHP中實作(見Phabricator上的UstringLibrary.php),它們必須將字串(位元組序列)解析為程式碼點,模式匹配函數必須將Lua模式轉換為PHP正則表達式,然後才能做它們的工作。基本字串函數操作位元組,所以它們消除了這個中間步驟。在下面閱讀更多關於這個的內容。
mw.text.gsplit函數使用mw.ustring函數(原始碼)。在許多情況下,可以使用函數string.gmatch代替,並且會快得多。例如,要迭代wiki文本的行,使用for line in string.gmatch(wikitext, "[^\n]*") do --[[ something or other ]] end而不是for line in mw.text.gsplit(wikitext, "\n") do --[[ something or other ]] end。函數mw.text.split可以透過使用同一個迴圈將項目插入表格來複製。
建立陣列(具有連續整數鍵的表格:{ "a", "b", "c" },例如)的方法在速度上有所不同。以下三種方法按從最快到最慢排列。第一種方法在Module:table中使用,該模組使用頻率足夠高,需要盡可能高效。
local str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
----
local t = {}
local i = 0
for character in string.gmatch(str, ".") do
i = i + 1
t[i] = character
end
----
local t = {}
for character in string.gmatch(str, ".") do
t[#t + 1] = character
end
----
local t = {}
for character in string.gmatch(str, ".") do
table.insert(t, character)
end
第一種方法最快,因為在每次迭代中只需要做一個加法操作和建立一個索引。在第二種方法中,必須為每次迭代重新計算表格的長度。函數table.insert的操作方式與第二種方法相似,但它必須首先確定是否提供了第三個參數。
Lua表格包含陣列和雜湊部分。陣列部分每個欄位佔用的記憶體比雜湊部分少,所以當記憶體是問題時,使用陣列而不是雜湊是個好主意。根據World of Warcraft wiki,陣列欄位使用16位元組,而雜湊欄位使用40位元組。
陣列和雜湊部分的大小都是2的冪次方。對於陣列部分,大小是大於或等於陣列部分中最大索引的最小2的冪次方;對於雜湊部分,大小是大於或等於雜湊部分中元素數量的最小2的冪次方。陣列部分只能包含由正整數索引的欄位(t[1], t[2], t[3]),而雜湊部分包含具有任何類型索引的欄位。
當表格字面量中的元素沒有明確索引時(t = { "a", "b", "c" }建立一個陣列部分大小為四的表格),或者在某些條件下,當使用索引運算子新增元素時(t = {}; t[1] = "a"; t[2] = "b"; t[3] = "c"),會新增陣列欄位。當表格字面量包含明確的數字索引時,會新增雜湊欄位:t = { [1] = "a", [2] = "b", [3] = "c" }建立一個雜湊部分包含四個欄位的表格。(如果向表格新增更多欄位,這些欄位可能會移到表格的陣列部分。)
在原版Lua和Scribunto中,沒有辦法檢查表格中有多少欄位在陣列部分和雜湊部分。長度運算子不檢查正整數索引欄位是在陣列部分還是雜湊部分。
Unicode
[编辑]Lua本身不理解Unicode;雖然有超過一百萬個可能的Unicode字元,但Lua中的「字串」只是0–255範圍內位元組的序列。(不幸的是,Lua文件將這些位元組稱為「字元」,但不要被欺騙。)
為了解決這個缺乏,Scribunto擴充功能為我們做了(至少)四件事:
- 每當任何文字被傳遞到Lua模組中(例如,作為模板參數),原始字元字串會使用UTF-8轉換為位元組字串。UTF-8是變長編碼:ASCII字元轉換為僅一個位元組,而其他Unicode字元轉換為二、三或四個位元組。
- Lua模組回傳的文字被解釋為UTF-8,並轉換回Unicode字元字串。這意味著,例如,如果模組接收一段文字並未修改地回傳它,那麼一切都會正常。
- 技術說明:
- Scribunto模組的原始碼使用UTF-8編碼,所以我們可以在Lua字串字面量中使用Unicode字元。
- Scribunto擴充功能包含一個
mw.ustring(「Unicode字串」)模組,它總是可用的。這個模組提供Lua內建字串函數的UTF-8感知類似物。本質上,這個模組中的函數允許您操作UTF-8編碼的位元組字串,就好像它仍然是原始的Unicode字元字串。
即便如此,在使用mw.ustring函式庫時,仍有一些需要注意的注意事項。雖然函式庫能夠將幾個位元組的序列解釋為單一Unicode字元,但在單一邏輯字元中可能仍有多個Unicode字元。例如,雖然я́對我們來說顯示為單一邏輯字元,但它實際上編碼為兩個不同的Unicode字元:西里爾字母я (U+044F)後跟組合銳音符(U+0301)。因此,程式碼mw.ustring.len("я́")實際上會回傳2,而不是1。更微妙的是,以下也會回傳有效結果:mw.ustring.find("я", "[я́]")。這是因為字元類別在模式"[я́]"中實際包含兩個字元(西里爾字母和重音符號);函數會個別搜尋每個字元,並找到第一個(西里爾字母)。
MediaWiki在Unicode字元被輸入到文字框或在頁面上顯示時,會將其轉換為標準組合標準化形式(NFC)(參見MediaWiki關於Unicode標準化考慮的頁面)。除其他事項外,這意味著裸字母字元加上組合字元會變成組合形式(如果可能),一些個別字元會變更為具有相似外觀的字元。例如,兩字元序列a (U+0061) + ◌́ (組合銳音符, U+0301)變成á (U+00E1),中日韓相容表意文字變更為來自中日韓統一表意文字區塊之一的對應字元(豈 (U+F900) → 豈 (U+8C48))。要顯示否則會被轉換的字元,使用字元值參照,如豈。
在測試回傳分解形式(NFD)的模組函數輸出時,使用如Module:UnitTests這樣的模組要小心標準化形式。即使「實際」和「預期」欄位在頁面上顯示相同,它們在模組中可能不同,在這種情況下測試會失敗。(例如,「實際」欄位可能有字母-組合字元序列,而「預期」欄位有對應的字母加變音符號字元。)使用mw.ustring.toNFC或mw.ustring.toNFD將它們轉換為相同的標準化形式(NFC或NFD),以確保比較正確進行。
產生Unicode字元
[编辑]在Lua模組中鍵入Unicode字元有幾種方式:將字元本身新增到Lua字串中,將代表UTF-8編碼中位元組的十進位跳脫序列新增到Lua字串中,或將程式碼點(以十六進位或十進位)放入mw.ustring.char中。例如,字母á(帶銳音符的拉丁小寫字母a,程式碼點U+00E1)可以輸入為:
"á""\195\161"mw.ustring.char(0xE1)mw.ustring.char(225)
Scribunto擴充功能目前使用Lua版本5.1(具有5.2的一些功能),所以在Lua版本5.2和5.3左右新增的十六進位跳脫序列和Unicode跳脫序列不受支援。在Lua 5.3中,跳脫序列"\xc3\xa1, \xC3\xA1, \u{e1}, \u{E1}"都產生字元á,而在Scribunto中它們產生xC3xA1 xc3xa1 u{e1} u{E1}。
應該避免位元組序列(方法2),因為它們難以閱讀和編寫,且容易出錯。它們與程式碼點不同:例如,組合銳音符(在點圓圈上顯示:◌́)的位元組序列是"\204\129",或十六進位0xCC, 0x81,而程式碼點是U+0301(十進位769)。除非查看個別位元,否則沒有對應關係。位元組序列可以轉換為程式碼點,反之亦然,但沒有程式輸助很難做到。
雖然程式碼點可以使用十進位(方法4)輸入到mw.ustring.char中,但十六進位(方法3)更容易識別,因為這是程式碼點通常表示的方式。例如,U+00E1代表字母á,對應於Lua程式碼mw.ustring.char(0xE1)。
組合字元最好不要單獨輸入。例如,直接在引號內新增組合銳音符("́"或'́'是不可能閱讀的,因為它直接顯示在其中一個引號上方。
字串在被作為參數提供給呼叫的函數之前,會經過Unicode組合標準化,在作為輸出回傳時也是如此。因此,字串可能在輸入和輸出過程中被修改。例如,兩個程式碼點U+0061和U+0301(拉丁小寫a後跟組合銳音符)會自動轉換為單一程式碼點U+00E1(帶銳音符的拉丁小寫a,單一字元)。要逐字元分析字串,您需要在Lua內使用mw.ustring.gcodepoint函數來做,您不能依賴頁面輸出包含您回傳的確切字元。
字串函數
[编辑]Scribunto包含基本的Lua字串函數和mw.ustring函數。一些Ustring函數是基本字串函數的副本,其他是修改後的等效函數,適用於包含基本ASCII字元集之外Unicode字元的字串,還有一些新函數。
修改過的函數包括mw.ustring.char、mw.ustring.codepoint、mw.ustring.find、mw.ustring.gmatch、mw.ustring.gsub、mw.ustring.lower、mw.ustring.sub、mw.ustring.upper。
基本Lua字串函數檢視位元組,而Ustring函數檢視以UTF-8編碼的程式碼點。
對於基本Lua函數,長度意味著位元組數。基本ASCII之外的任何內容的長度都會大於顯示字元的數量。
string.len("a") --> 1
string.match("a", ".") --> "a"
string.len("á") --> 2
string.match("á", "..") --> "á" (U+00E1, LATIN SMALL LETTER A WITH ACUTE); a two-byte character
string.len("ἀ") --> 3
string.match("ἀ", "...") --> "ἀ" (U+1F00, GREEK SMALL LETTER ALPHA WITH PSILI); a three-byte character
string.len("𐌀") --> 4
string.match("𐌀", "....") --> "𐌀" (U+10300, OLD ITALIC LETTER A); a four-byte character
模式
[编辑]注意:以下章節僅適用於維基詞典(以及其他MediaWiki專案)使用的UTF-8編碼。其他編碼遵循不同的規則。
在下面的討論中,ASCII指程式碼點範圍U+0000到U+0080的Unicode字元。它們每個編碼為一個位元組:位元組"\0"到"\127"(二進位0xxxxxxx)。非ASCII指程式碼點範圍U+0080到U+10FFFF的Unicode字元。它們使用二、三或四個位元組編碼。第一個位元組(前導位元組)在範圍"\194"到"\244"(110xxxxx, 1110xxxx, and 11110xxx),後面的一到三個位元組(續接位元組)在範圍"\128"到"\191"(10xxxxxx)。因此ASCII等同於單位元組,非ASCII等同於多位元組。
還要注意的是,ASCII和非ASCII使用不同的位元組,所以很容易確定任意位元組屬於哪一個。
基本字串模式
[编辑]如果模式滿足某些條件,它在基本字串和Ustring函數中的行為會相同:它必須只包含ASCII或簡單的非ASCII字元序列。因此模式"abc"、"[abc]"或"[^abc]"、"αβγ"無論在基本字串函數還是Ustring函數中使用都會正確工作。
但包含非ASCII字元的量詞或集合會失敗。它們作用於個別位元組,而不是字元。包含非ASCII字元的集合會匹配字元編碼中任何一個位元組。量詞會作用於緊接在它之前的最後一個位元組。
例如,在基本Lua字串函數中,量化項目"á+"不匹配一個或多個字元"á"的序列("á", "áá", "ááá", ...")。字元á是兩位元組序列,等同於位元組跳脫序列"\195\161",所以模式"á+"實際上是"\195\161+",它匹配位元組"\195"加上一個或多個位元組"\161":"\195\161", "\195\161\161", "\195\161\161\161", ..."。(其中只有第一個選項是有效的UTF-8。其餘的如果在維基詞典頁面上發布會顯示為á�, á��,它們不太可能在模組中出現。)
類似地,集合"[áé]"不匹配「字元á或字元é」。相反,它匹配用於在UTF-8中編碼程式碼點á或é的位元組之一"[\195\161\195\169]",如果應用到"é"(= "\195\169"),它只會匹配第一個位元組"\195"。
參見en:Module:User:Erutuon/patterns中的函數,用於確定模式在基本字串函數中是否與在Ustring函數中的行為相同。
Ustring模式
[编辑]ustring函數修復了這些問題。它們處理程式碼點而不是位元組。所以任何編碼Unicode字元的多位元組序列都被視為一個單位。
如果模式包含作用於非ASCII字元的量詞、意圖查找Unicode字元的字元類別,或包含非ASCII字元的集合,必須使用Ustring函數。使用基本字串函數可能回傳不正確的結果。這些例子:
string.gsub("áéíóúý", "[áéíóú]", "") --> "\189" (invalid UTF-8)
mw.ustring.gsub("áéíóúý", "[áéíóú]", "") --> "ý"
這裡模式等同於"[\161\169\173\179\186\195]",如果移除重複項並對位元組排序。"áéíóúý"等同於"\195\161\195\169\195\173\195\179\195\186\195\189";唯一「新」位元組是"\189","ý"編碼中的第二個位元組。
string.match("ábc", ".b") --> "\161b" (invalid UTF-8) = "\195\161bc", "[\0-\255]b"
mw.ustring.match("ábc", ".b") --> "áb"
string.match("ábc", "%a+") --> "bc" = "[a-zA-Z]+"
mw.ustring.match("ábc", "%a+") --> "ábc"
string.match("ááábc", "á+") --> "á"; = "\195\161+"
mw.ustring.match("ááábc", "á+") --> "ááá"
要在基本字串函數中匹配單一UTF-8字元,可以使用模式"[%z\1-\127\194-\244][\128-\191]*"。例如,下面兩個表達式給出相同結果。string.gsub會更快,因為它在將字串"áéíóúý"與模式比較之前沒有處理要做,而mw.ustring.gsub必須在匹配之前將字串和模式都解析為程式碼點。
local repl = { ["á"] = "a", ["é"] = "e", ["í"] = "i", ["ó"] = "o", ["ú"] = "u", ["ý"] = "y", }
string.gsub("áéíóúý", "[%z\1-\127\194-\244][\128-\191]*", repl) --> "aeiouy
mw.ustring.gsub("áéíóúý", ".", repl) --> "aeiouy"
組織Lua模組
[编辑]在/doc子頁面上記錄Lua模組。文件會出現在模組頁面的頂部。
分類不能直接輸入到模組中。將分類放在文件頁面上,用頂部和底部的<includeonly>標籤與文件分開:
(documentation) <includeonly> [[Category:Ukrainian modules]] [[Category:Transliteration modules]] </includeonly>