表格序列化

lua-users home
wiki

以下這些函數能夠將表格或物件(通常,但不總是,表示為表格)序列化/非序列化,亦即將表格轉換為字串表示,反之亦然。這通常用於顯示(例如除錯)或將資料儲存在檔案中(例如永續)。

設計決策包括

由於這些不同的需求,已經有很多實作了。

實作

Metalua 提供恆等轉換的表格序列化

此範例不注重序列化後的表格可讀性。與此頁的其他範例相反,它會保留共用子結構。以下是金屬語言序列化會通過,但精緻列印器無法通過的測試範例

> x={ 1 }
> x[2] = x
> x[x] = 3
> x[3]={ 'indirect recursion', [x]=x }
> y = { x, x }
> x.y = y
> assert (y[1] == y[2])
> s = serialize (x)
> z = loadstring (s)()
> assert (z.y[1] == z.y[2])
> =s
local _={ }
_[1]={ "indirect recursion" }
_[2]={ false, false }
_[3]={ 1, false, _[1], ["y"] = _[2] }
_[3][2] = _[3]
_[1][_[3]] = _[3]
_[3][_[3]] = 3
_[2][1] = _[3]
_[2][2] = _[3]
return _[3]
>

此序列器的來源位於 MetaLua 儲存庫中:[2]

Metalua 的 table.tostring 和 table.print

這些函式專為美化列印設計,而非序列化:它們不會保留恆等性。不過,它們會終止:如果表格自我參照,會將內部出現的內容列印為「[table: 0x12345678]」,避免無限遞迴。

require "table2"
require "string2"

local u = {9}
local t = {2, "3\"4", {5, 6}, x=function() end, [u]=u}

table.print(t)
--> { [{ 9 }] = { 9 }, x = function: 0x6a2870, 2, "3\"4", { 5, 6 } }
table.print(t, 'nohash')
--> { 2, "3\"4", { 5, 6 } }
table.print(t, 'nohash', 10)
--> { 2,
--    "3\"4",
--    { 5, 6 } }

-- The `tag' field is particularly important in metalua, to represent tree-like structures.
-- As such, it has got a special syntax, introduced by a back-quote "`",
-- which is rendered by default by metalua's pretty printers.
local t = {tag='Sum', 1, {tag='Product', 2, 3}, lines={10,11}}

table.print(t)
--> `Sum{ lines = { 10, 11 }, 1, `Product{ 2, 3 } } -- metalua tag syntax
table.print(t, 'nohash')
--> `Sum{ 1, `Product{ 2, 3 } }
table.print(t, 'nohash', 10)  -- metalua tag syntax
--> `Sum{ 1,
--        `Product{ 2,
--                  3 } }

-- tags syntax can be disabled:
table.print(t, 'nohash', 'notag')
--> { tag = "Sum", 1, { tag = "Product", 2, 3 } }  -- tag syntax disabled

-- Note: table.print(t, ...) is equivalent to print(table.tostring(t, ...)).

遞迴列印表格

ISSUE:此函式應該回傳字串,而不是預設使用者如何輸出文字。

-- Print anything - including nested tables
function table_print (tt, indent, done)
  done = done or {}
  indent = indent or 0
  if type(tt) == "table" then
    for key, value in pairs (tt) do
      io.write(string.rep (" ", indent)) -- indent it
      if type (value) == "table" and not done [value] then
        done [value] = true
        io.write(string.format("[%s] => table\n", tostring (key)));
        io.write(string.rep (" ", indent+4)) -- indent it
        io.write("(\n");
        table_print (value, indent + 7, done)
        io.write(string.rep (" ", indent+4)) -- indent it
        io.write(")\n");
      else
        io.write(string.format("[%s] => %s\n",
            tostring (key), tostring(value)))
      end
    end
  else
    io.write(tt .. "\n")
  end
end

通用 tostring

function table_print (tt, indent, done)
  done = done or {}
  indent = indent or 0
  if type(tt) == "table" then
    local sb = {}
    for key, value in pairs (tt) do
      table.insert(sb, string.rep (" ", indent)) -- indent it
      if type (value) == "table" and not done [value] then
        done [value] = true
        table.insert(sb, key .. " = {\n");
        table.insert(sb, table_print (value, indent + 2, done))
        table.insert(sb, string.rep (" ", indent)) -- indent it
        table.insert(sb, "}\n");
      elseif "number" == type(key) then
        table.insert(sb, string.format("\"%s\"\n", tostring(value)))
      else
        table.insert(sb, string.format(
            "%s = \"%s\"\n", tostring (key), tostring(value)))
       end
    end
    return table.concat(sb)
  else
    return tt .. "\n"
  end
end

function to_string( tbl )
    if  "nil"       == type( tbl ) then
        return tostring(nil)
    elseif  "table" == type( tbl ) then
        return table_print(tbl)
    elseif  "string" == type( tbl ) then
        return tbl
    else
        return tostring(tbl)
    end
end

範例

print(to_string{
  "Lua",user="Mariacher",
  {{co=coroutine.create(function() end),{number=12345.6789}},
   func=function() end}, boolt=true} )

此函式會列印

"Lua"
{
  {
    {
      number = "12345.6789"
    }
    co = "thread: 0212B848"
  }
  func = "function: 01FC7C70"
}
boolt = "true"
user = "Mariacher"

(上述程式碼最初來自 TableUtils)

類 PHP 的 print_r

根據 [PHP print_r]。程式碼基於 DracoBlue 根據 Nick Gammon 編寫的程式碼修改,以符合 [PHP print_r] 的格式。

範例:print_r{ 5,3,{5,3} } -->

[1] => 5
[2] => 3
[3] => Table 
   {
     [1] => 5
     [2] => 3
   }

相容性:Lua 5.0 和 5.1

function print_r (t, indent, done)
  done = done or {}
  indent = indent or ''
  local nextIndent -- Storage for next indentation value
  for key, value in pairs (t) do
    if type (value) == "table" and not done [value] then
      nextIndent = nextIndent or
          (indent .. string.rep(' ',string.len(tostring (key))+2))
          -- Shortcut conditional allocation
      done [value] = true
      print (indent .. "[" .. tostring (key) .. "] => Table {");
      print  (nextIndent .. "{");
      print_r (value, nextIndent .. string.rep(' ',2), done)
      print  (nextIndent .. "}");
    else
      print  (indent .. "[" .. tostring (key) .. "] => " .. tostring (value).."")
    end
  end
end

function print_r (t, indent) -- alt version, abuse to http://richard.warburton.it
  local indent=indent or ''
  for key,value in pairs(t) do
    io.write(indent,'[',tostring(key),']') 
    if type(value)=="table" then io.write(':\n') print_r(value,indent..'\t')
    else io.write(' = ',tostring(value),'\n') end
  end
end

-- alt version2, handles cycles, functions, booleans, etc
--  - abuse to http://richard.warburton.it
-- output almost identical to print(table.show(t)) below.
function print_r (t, name, indent)
  local tableList = {}
  function table_r (t, name, indent, full)
    local serial=string.len(full) == 0 and name
        or type(name)~="number" and '["'..tostring(name)..'"]' or '['..name..']'
    io.write(indent,serial,' = ') 
    if type(t) == "table" then
      if tableList[t] ~= nil then io.write('{}; -- ',tableList[t],' (self reference)\n')
      else
        tableList[t]=full..serial
        if next(t) then -- Table not empty
          io.write('{\n')
          for key,value in pairs(t) do table_r(value,key,indent..'\t',full..serial) end 
          io.write(indent,'};\n')
        else io.write('{};\n') end
      end
    else io.write(type(t)~="number" and type(t)~="boolean" and '"'..tostring(t)..'"'
                  or tostring(t),';\n') end
  end
  table_r(t,name or '__unnamed__',indent or '','')
end

以下是 print_r 更完整的版本

很抱歉這麼長!

--[[
   Author: Julio Manuel Fernandez-Diaz
   Date:   January 12, 2007
   (For Lua 5.1)
   
   Modified slightly by RiciLake to avoid the unnecessary table traversal in tablecount()

   Formats tables with cycles recursively to any depth.
   The output is returned as a string.
   References to other tables are shown as values.
   Self references are indicated.

   The string returned is "Lua code", which can be procesed
   (in the case in which indent is composed by spaces or "--").
   Userdata and function keys and values are shown as strings,
   which logically are exactly not equivalent to the original code.

   This routine can serve for pretty formating tables with
   proper indentations, apart from printing them:

      print(table.show(t, "t"))   -- a typical use
   
   Heavily based on "Saving tables with cycles", PIL2, p. 113.

   Arguments:
      t is the table.
      name is the name of the table (optional)
      indent is a first indentation (optional).
--]]
function table.show(t, name, indent)
   local cart     -- a container
   local autoref  -- for self references

   --[[ counts the number of elements in a table
   local function tablecount(t)
      local n = 0
      for _, _ in pairs(t) do n = n+1 end
      return n
   end
   ]]
   -- (RiciLake) returns true if the table is empty
   local function isemptytable(t) return next(t) == nil end

   local function basicSerialize (o)
      local so = tostring(o)
      if type(o) == "function" then
         local info = debug.getinfo(o, "S")
         -- info.name is nil because o is not a calling level
         if info.what == "C" then
            return string.format("%q", so .. ", C function")
         else 
            -- the information is defined through lines
            return string.format("%q", so .. ", defined in (" ..
                info.linedefined .. "-" .. info.lastlinedefined ..
                ")" .. info.source)
         end
      elseif type(o) == "number" or type(o) == "boolean" then
         return so
      else
         return string.format("%q", so)
      end
   end

   local function addtocart (value, name, indent, saved, field)
      indent = indent or ""
      saved = saved or {}
      field = field or name

      cart = cart .. indent .. field

      if type(value) ~= "table" then
         cart = cart .. " = " .. basicSerialize(value) .. ";\n"
      else
         if saved[value] then
            cart = cart .. " = {}; -- " .. saved[value] 
                        .. " (self reference)\n"
            autoref = autoref ..  name .. " = " .. saved[value] .. ";\n"
         else
            saved[value] = name
            --if tablecount(value) == 0 then
            if isemptytable(value) then
               cart = cart .. " = {};\n"
            else
               cart = cart .. " = {\n"
               for k, v in pairs(value) do
                  k = basicSerialize(k)
                  local fname = string.format("%s[%s]", name, k)
                  field = string.format("[%s]", k)
                  -- three spaces between levels
                  addtocart(v, fname, indent .. "   ", saved, field)
               end
               cart = cart .. indent .. "};\n"
            end
         end
      end
   end

   name = name or "__unnamed__"
   if type(t) ~= "table" then
      return name .. " = " .. basicSerialize(t)
   end
   cart, autoref = "", ""
   addtocart(t, name, indent)
   return cart .. autoref
end

一個測試

-----------------------------------------------------------
--- testing table.show

t = {1, {2, 3, 4}, default = {"a", "b", d = {12, "w"}, e = 14}}
t.g = t.default

print("-----------------------------------")
print(table.show(t))                -- shows __unnamed__ table

tt = {1, h = {["p-q"] = "a", b = "e", c = {color = 3, name = "abc"}}, 2}

f = table.show
tt[f] = "OK"

print("-----------------------------------")
print(table.show(tt, "tt", "--oo-- ")) -- shows some initial 'indent'

t.m = {}
t.g.a = {}
t.g.a.c = t
t.tt = tt.new
t.show = table.show

print("-----------------------------------")
print(table.show(t, "t"))            -- most typical use

print("-----------------------------------")
print(table.show(math.tan, "tan"))   -- not a table is OK

print("-----------------------------------")
s = "a string"
print(table.show(s, "s"))            -- not a table is OK

輸出

-----------------------------------
__unnamed__ = {
   [1] = 1;
   [2] = {
      [1] = 2;
      [2] = 3;
      [3] = 4;
   };
   ["default"] = {
      [1] = "a";
      [2] = "b";
      ["e"] = 14;
      ["d"] = {
         [1] = 12;
         [2] = "w";
      };
   };
   ["g"] = {}; -- __unnamed__["default"] (self reference)
};
__unnamed__["g"] = __unnamed__["default"];

-----------------------------------
--oo-- tt = {
--oo--    [1] = 1;
--oo--    [2] = 2;
--oo--    ["function: 0x8070e20, defined in (28-99)@newprint_r.lua"] = "OK";
--oo--    ["h"] = {
--oo--       ["b"] = "e";
--oo--       ["c"] = {
--oo--          ["color"] = 3;
--oo--          ["name"] = "abc";
--oo--       };
--oo--       ["p-q"] = "a";
--oo--    };
--oo-- };

-----------------------------------
t = {
   [1] = 1;
   [2] = {
      [1] = 2;
      [2] = 3;
      [3] = 4;
   };
   ["m"] = {};
   ["show"] = "function: 0x8070e20, defined in (28-99)@newprint_r.lua";
   ["g"] = {
      [1] = "a";
      [2] = "b";
      ["e"] = 14;
      ["d"] = {
         [1] = 12;
         [2] = "w";
      };
      ["a"] = {
         ["c"] = {}; -- t (self reference)
      };
   };
   ["default"] = {}; -- t["g"] (self reference)
};
t["g"]["a"]["c"] = t;
t["default"] = t["g"];

-----------------------------------
tan = "function: 0x806f758, C function"
-----------------------------------
s = "a string"

(上述程式碼最初存在於 MakingLuaLikePhp)

警告:上述程式碼未如所顯示的正常運作

x = {1, 2, 3}
x[x]=x
print(table.show(x))

--[[output:
__unnamed__ = {
   [1] = 1;
   [2] = 2;
   [3] = 3;
   ["table: 0x695f08"] = {}; -- __unnamed__ (self reference)
};
__unnamed__["table: 0x695f08"] = __unnamed__;
--]]

將包含可辨識自我參照的物件轉儲到字串中,包括表格

local val_to_str; do
    -- Cached function references (for performance).
    local byte   = string.byte
    local find   = string.find
    local match  = string.match
    local gsub   = string.gsub
    local format = string.format
    local insert = table.insert
    local sort   = table.sort
    local concat = table.concat
    -- For escaping string values.
    local str_escape_map = {
        ['\a'] = '\\a', ['\b'] = '\\b', ['\t'] = '\\t', ['\n'] = '\\n',
        ['\v'] = '\\v', ['\f'] = '\\f', ['\r'] = '\\r', ['\\'] = '\\\\' }
    local str_escape_replace = function(c)
        return str_escape_map[c] or format('\\%03d', byte(c))
    end
    -- Keys are comparable only if the same type, otherwise just sort them by type.
    local types_order, ref_types_order = {
        ['number'] = 0, ['boolean'] = 1, ['string'] = 2, ['table'] = 3,
        ['function'] = 4 }, 5
    end
    local function compare_keys(k1, k2)
        local t1, t2 = type(k1), type(k2)
        if t1 ~= t2 then -- not the same type
            return (types_order[t1] or ref_types_order)
                 < (types_order[t2] or ref_types_order)
        elseif t1 == 'boolean' then -- comparing booleans
            return not k1 -- Sort false before true.
        elseif t1 == 'number' or t1 == 'string' then -- comparing numbers (including NaNs or infinites) or strings
            return k1 < k2 -- Keys with the same comparable type.
        else -- comparing references (including tables, functions, userdata, threads...)
            return tostring(k1) < tostring(k2) -- may be the Lua engine adds some comparable info
        end
    end
    -- String keys matching valid identifiers that are reserved by Lua.
    local reserved_keys = {
        ['and']      = 1, ['break'] = 1, ['do']    = 1, ['else']   = 1,
        ['elseif']   = 1, ['end']   = 1, ['false'] = 1, ['for']    = 1,
        ['function'] = 1, ['if']    = 1, ['in']    = 1, ['local']  = 1,
        ['nil']      = 1, ['not']   = 1, ['or']    = 1, ['repeat'] = 1,
        ['return']   = 1, ['then']  = 1, ['true']  = 1, ['until']  = 1,
        ['while']    = 1 }
    -- Main function.
    val_to_str = function(val, options)
        -- Decode and cache the options.
        local include_mt  = options and options.include_mt
        local prettyprint = options and options.prettyprint
        local asciionly   = options and options.asciionly
        -- Precompute the output formats depending on options.
        local open   = prettyprint and '{ '  or '{'
        local equals = prettyprint and ' = ' or '='
        local comma  = prettyprint and ', '  or ','
        local close  = prettyprint and ' }'  or '}'
        -- What to escape: C0 controls, the backslash, and optionally non-ASCII bytes.
        local str_escape_pattern = asciionly and '[%z\001-\031\\\127-\255]' or '[%z\001-\031\\\127]'
         -- Indexed references (mapped to ids), and counters per ref type.
        local ref_ids, ref_counts = {}, {}
        -- Helper needed to detect recursive tables and avoid infinite loops.
        local function visit(ref)
            local typ = type(ref)
            if typ == 'number' or typ == 'boolean' then
                return tostring(ref)
            elseif typ == 'string' then
                if find(ref, "'") then
                   str_escape_map['"'] = '\\"'
                   return '"' .. gsub(ref, str_escape_pattern, str_escape_replace) .. '"'
                else
                   str_escape_map['"'] = '"'
                   return "'" .. gsub(ref, str_escape_pattern, str_escape_replace) .. "'"
                end
            elseif typ == 'table' then
    	        local id = ref_ids[ref]
                if id then
                    return ':' .. typ .. '#' .. id .. ':'
                end
                id = (ref_counts[typ] or 0) + 1; ref_ids[ref], ref_counts[typ] = id, id
                -- First dump keys that are in sequence.
                local result, sequenced, keys = {}, {}, {}
                for i, val in ipairs(ref) do
                    insert(result, visit(val))
                    sequenced[i] = true
                end
                -- Then dump other keys out of sequence, in a stable order.
                for key, _ in pairs(ref) do
                    if not sequenced[key] then
                        insert(keys, key)
                    end
                end
                sequenced = nil -- Free the temp table no longer needed.
                -- Sorting keys (of any type) is needed for stable comparison of results.
                sort(keys, compare_keys)
                for _, key in ipairs(keys) do
                    insert(result,
                        (type(key) == 'string' and
                             not reserved_keys[key] and match(key, '^[%a_][%d%a_]*$') and
                             key or '[' .. visit(key) .. ']') ..
                        equals .. visit(ref[key]))
                end
                keys = nil -- Free the temp table no longer needed.
                -- Finally dump the metatable (with pseudo-key '[]'), if there's one.
                if include_mt then
                    ref = getmetatable(ref)
                    if ref then
                        insert(result, '[]' .. equals .. visit(ref))
                    end
                end
                -- Pack the result string.
                -- TODO: improve pretty-printing with newlines/indentation
                return open .. concat(result, comma) .. close
            elseif typ ~= 'nil' then -- other reference types (function, userdata, etc.)
                local id = ref_ids[ref]
                if not id then
                    id = (ref_counts[typ] or 0) + 1; ref_ids[ref], ref_counts[ref] = id, id
                end
                return ':' .. typ .. '#' .. id .. ':'
            else
                return 'nil'
            end
        end
        return visit(val)
    end
end

範例用法

    local s = val_to_str(anyvalue)
    local s = val_to_str(anyvalue, {include_mt=1, prettyprint=1}) -- you can specify optional flags

筆記


RecentChanges · 喜好設定
編輯 · 歷程
上一次編輯時間 2021 年 9 月 5 日 10:54 格林威治標準時間 (差異)