首頁 > 軟體

Ruby解析處理YAML和json格式資料

2022-04-18 19:00:03

Ruby處理YAML

Ruby的標準庫YAML基於Psych:https://ruby-doc.org/stdlib-2.6.2/libdoc/psych/rdoc/Psych.html

require 'yaml' 之後,為大多數的基本資料型別都提供了 to_ yaml() 方法,用於將各資料型別的物件轉換為yaml格式。

例如:

require 'yaml'
require 'set'

p "hello world".to_yaml
p 123.to_yaml
p %w(perl shell php).to_yaml
p ({one: 1, two: 2}).to_yaml
p Set.new([1,2,3]).to_yaml

得到:

"--- hello worldn"
"--- 123n"
"---n- perln- shelln- phpn"
"---n:one: 1n:two: 2n"
"--- !ruby/object:Setnhash:n  1: truen  2: truen  3: truen"

也可以使用YAML.dump()方法實現和to_yaml相同的功能,它還可以寫入檔案。

users = [{name: 'Bob', permissions: ['Read']},
 {name: 'Alice', permissions:['Read', 'Write']}]

File.open("/tmp/a.yml","w") { |f| YAML.dump(users, f) }

檢視檔案:

---
- :name: Bob             #=> 注意,保留了hash源資料中的符號
  :permissions:
  - Read
- :name: Alice
  :permissions:
  - Read
  - Write

用YAML.load()從YAML中讀取資料:

require 'yaml'

pp YAML.load(DATA)

__END__
mysql:
  passwd: P@ssword1!
  user: root
  port: 3306
  other1: nil
  other2: false
  other3: ""
  hosts: 
    - ip: 10.10.1.1
      hostname: node1
    - ip: 10.10.1.2
      hostname: node2

得到:

{"mysql"=>
  {"passwd"=>"P@ssword1!",      #=> 注意,key是String而非Symbol
   "user"=>"root",
   "port"=>3306,
   "other1"=>"nil",
   "other2"=>false,
   "other3"=>"",
   "hosts"=>
    [{"ip"=>"10.10.1.1", "hostname"=>"node1"},
     {"ip"=>"10.10.1.2", "hostname"=>"node2"}]}}

如果想讓hash的key是符號而非字串,可以設定選項symbolize_names: true

pp YAML.load(DATA, symbolize_names: true)

需要注意,YAML可以將物件進行序列化,所以有幾方面注意事項:

  • 在反序列化的時候需要也require涉及到的檔案,例如對Set型別序列化後,在反序列化時如不require 'set'則無法還原物件
  • 有些底層物件不能序列化,包括IO流、Ruby程式碼物件Proc、Binding等
  • 不要反序列化不被信任的資料物件(比如使用者輸入的資料),此時可使用safe_load(),它預設只允許載入以下幾種型別的資料:
    • TrueClass
    • FalseClass
    • NilClass
    • Numeric
    • String
    • Array
    • Hash
  • 如果確實想要載入額外的資料型別,可以在safe_load()中指定引數permitted_classes: []或permitted_symbols: []

Ruby處理Json資料

轉為json格式字串

使用JSON.generate()可以將物件或陣列轉換為JSON格式的資料:

require 'json'
p JSON.generate "abc"
p JSON.generate 123
p JSON.generate true
p JSON.generate nil
p JSON.generate [2,3,4]
p JSON.generate({name: "junmajinlong", age: 23})
require 'set'
p JSON.generate(Set.new([1,23,44]))

得到:

""abc""
"123"
"true"
"null"
"[2,3,4]"
"{"name":"junmajinlong","age":23}"
""#<Set: {1, 23, 44}>""

require 'json'後,很多ruby型別都具備了一個to_json的方法,可以直接將該型別的資料轉換為json資料:

p ({name: "junmajinlong", age: 23}).to_json
p (Set.new([1,23,44])).to_json

得到:

"{"name":"junmajinlong","age":23}"
""#<Set: {1, 23, 44}>""

此外,JSON.dump()也可以將物件轉換為JSON格式的字串,而且它還支援寫入檔案:

hsh = {name: "junmajinlong", age: 23}
File.open("/tmp/a.json", "w") {|f| JSON.dump(hsh, f)}

json格式字串轉為Ruby物件

要從json格式字串轉為ruby物件,有一些選項可設定,參考https://ruby-doc.org/stdlib-2.7.1/libdoc/json/rdoc/JSON.html,比如*symbolize_names*選項表示是否將json object中的key解析為符號型別的key,如果設定為false,則解析為字串的key。

要將json格式的字串解析為Ruby資料型別(Hash),使用JSON.parse()

require 'json'

hsh = '{"name": "junmajinlong", "age": 23}'

p JSON.parse(hsh)
p JSON.parse(hsh, symbolize_names: true)

注意,上面的json字串必須是合理的json資料,比如key必須使用雙引號包圍而不能使用單引號,字串必須使用雙引號包圍,等等。比如"{'name': 'junmajinlong', 'age': 23}"就不是合理的json字串。

要從json檔案中讀取json資料並轉換為Ruby資料,使用load():

data = File.open("/tmp/a.json") do |f|
  JSON.load(f)
end

pp data
#=> {"name"=>"junmajinlong", "age"=>23}

自定義物件的轉換方式

json支援的資料型別有:

  • 字串
  • 數值
  • 物件
  • 陣列
  • 布林
  • Null

從一種語言的資料轉換為Json資料時,如果資料型別也是JSON所支援的,可直接轉換,但如果包含了JSON不支援的型別,則可能報錯,也可能以一種物件字串的方式儲存,這取決於對應的實現。

可以在物件中定義as_json實體方法來決定物件如何轉換為json字串,再定義類方法from_json()來決定如何從json字串中恢復為一個物件。

例如,

require 'json'
require 'date'

class Person
  attr_accessor :name, :birthday
  def initialize name, birthday
    @name = name
    @birthday = DateTime.parse(birthday)
  end
end

File.open("/tmp/p.json", "w") do |f|
  JSON.dump(Person.new("junmajinlong", "1999-10-11"), f)
end

檢視儲存的json資料:

$ cat /tmp/p.json
"#<Person:0x00007fffc7e575d0>"

定義as_jsonfrmo_json

require 'json'
require 'date'

class Person
  attr_accessor :name, :birthday
  
  def initialize name, birthday
    @name = name
    @birthday = DateTime.parse(birthday)
  end
  
  def as_json
    {
      name: @name,
      birthday: @birthday.strftime("%F")
    }
  end

  def self.from_json json
    data = JSON.parse(json)
    new(data["name"], data["birthday"])
  end
end

之後要序列化、反序列化該物件,可:

data = Person.new("junmajinlong", "1999-10-11").as_json
p data

p1=Person.from_json(JSON.dump data)
p p1.birthday

如果是讀寫json檔案,可:

person1 = Person.new("junmajinlong", "1999-10-11")
File.open("/tmp/p.json", "w") do |f|
  JSON.dump(person1.as_json, f)
end

p1 = File.open("/tmp/p.json") do |f|
  Person.from_json(f.read)
  # Person.from_json(JSON.load(f).to_json)
end
p p1

幾種JSON解析工具的效能測試

測試了json標準庫、oj和fast_josnparser解析json的效能,測試項包括:

  • 從檔案中載入並解析json字串為ruby物件
  • 從記憶體json字串中解析json字串為ruby物件
  • 帶有symbolize_keys/symbolize_names轉換時的解析
  • json標準庫和oj將ruby物件dump為json字串
  • json標準庫和oj將ruby物件dump為json字串儲存到檔案

注:

  • fast_jsonparser沒有dump功能,只有解析json字串功能
  • oj在將物件轉換為json字串時,可能會丟失資料的精度,比如浮點數的精度

測試的json字串數量大約50M。

測試了ruby 2.7.1和ruby 3.0.1兩個版本,gem包的版本資訊如下:

fast_jsonparser (0.5.0)
json (default: 2.5.1)
oj (3.11.7)

測試程式碼:

require 'benchmark'
require 'json'
require 'oj'
require 'fast_jsonparser'

# warm
json_file='test'  # 檔案大小大約50M
str = File.read(json_file)

######## JSON

puts " load file ".center(80, '-')
Benchmark.bm(30) do |x|
  x.report("JSON.load:") { File.open(json_file){ |f| JSON.load(f) } }
  x.report("Oj.load_file:") { Oj.load_file(json_file) }
  x.report("FastJsonparser.load:") { FastJsonparser.load(json_file) }
end

puts
puts " load file with symbolize_keys ".center(80, '-')
Benchmark.bm(30) do |x|
  x.report("JSON.load:") { File.open(json_file){ |f| JSON.load(f, nil, symbolize_names: true, create_additions: false) } }
  x.report("Oj.load_file:") { Oj.load_file(json_file, symbol_keys: true) }
  x.report("FastJsonparser.load:") { FastJsonparser.load(json_file, symbolize_keys: true) }
end

puts
puts " parse str ".center(80, '-')
Benchmark.bm(30) do |x|
  x.report("JSON.parse:") { JSON.parse(str) }
  x.report("Oj.load:") { Oj.load(str) }
  x.report("FastJsonparser.parse:") { FastJsonparser.parse(str) }
end

puts
puts " parse str with symbolize_keys ".center(80, '-')
Benchmark.bm(30) do |x|
  x.report("JSON.parse:") { JSON.parse(str, symbolize_names: true) }
  x.report("Oj.load:") { Oj.load(str, symbol_keys: true) }
  x.report("FastJsonparser.parse:") { FastJsonparser.parse(str, symbolize_keys: true) }
end

obj = JSON.parse(str, symbolize_names: true)

puts 
puts " dump JSON to str ".center(80, '-')
Benchmark.bm(30) do |x|
  x.report("JSON.dump:") { JSON.dump(obj) }
  x.report("Oj.dump:") { Oj.dump(obj) }
end

puts 
puts " dump JSON to file ".center(80, '-')
Benchmark.bm(30) do |x|
  x.report("JSON.dump:") { File.open('0_json_dump', 'w') {|f| JSON.dump(obj, f) } }
  x.report("Oj.to_file:") { Oj.to_file('0_oj_dump', obj) }
end

測試結果:

Ruby 2.7.1中:

---------------------------------- load file -----------------------------------
                                     user     system      total        real
JSON.load:                       1.591831   0.058021   1.649852 (  1.738119)
Oj.load_file:                    1.350385   0.057684   1.408069 (  2.434268) <-慢
FastJsonparser.load:             0.653968   0.103258   0.757226 (  0.848913) <-快

------------------------ load file with symbolize_keys -------------------------
                                     user     system      total        real
JSON.load:                       1.212617   0.039052   1.251669 (  1.349545)
Oj.load_file:                    1.432059   0.098950   1.531009 (  2.679610) <-慢
FastJsonparser.load:             0.695538   0.008384   0.703922 (  0.797081) <-快

---------------------------------- parse str -----------------------------------
                                     user     system      total        real
JSON.parse:                      1.343596   0.000000   1.343596 (  1.350368)
Oj.load:                         1.133612   0.000000   1.133612 (  1.140939)
FastJsonparser.parse:            0.701701   0.012340   0.714041 (  0.720296) <-快

------------------------ parse str with symbolize_keys -------------------------
                                     user     system      total        real
JSON.parse:                      1.250775   0.000000   1.250775 (  1.258796)
Oj.load:                         1.131296   0.000000   1.131296 (  1.138020)
FastJsonparser.parse:            0.697433   0.015962   0.713395 (  0.719439) <-快

------------------------------- dump JSON to str -------------------------------
                                     user     system      total        real
JSON.dump:                       1.374611   0.028454   1.403065 (  1.403081)
Oj.dump:                         1.025049   0.040184   1.065233 (  1.065246) <-快

------------------------------ dump JSON to file -------------------------------
                                     user     system      total        real
JSON.dump:                       1.234362   0.040246   1.274608 (  1.369214)
Oj.to_file:                      1.168707   0.000000   1.168707 (  1.270957)

Ruby 3.0.1中:

---------------------------------- load file -----------------------------------
                                     user     system      total        real
JSON.load:                       1.362151   0.083610   1.445761 (  1.569754)
Oj.load_file:                    1.343601   0.182046   1.525647 (  2.684472) <-慢
FastJsonparser.load:             2.634435   0.052734   2.687169 (  2.776105) <-慢

------------------------ load file with symbolize_keys -------------------------
                                     user     system      total        real
JSON.load:                       1.287954   0.018572   1.306526 (  1.409770)
Oj.load_file:                    1.478750   0.043847   1.522597 (  2.668882) <-慢
FastJsonparser.load:             2.717857   0.006164   2.724021 (  2.822728) <-慢

---------------------------------- parse str -----------------------------------
                                     user     system      total        real
JSON.parse:                      1.242225   0.008661   1.250886 (  1.304554)
Oj.load:                         1.097922   0.000000   1.097922 (  1.110031)
FastJsonparser.parse:            2.602679   0.017232   2.619911 (  2.634604) <-慢

------------------------ parse str with symbolize_keys -------------------------
                                     user     system      total        real
JSON.parse:                      1.368262   0.000000   1.368262 (  1.380730)
Oj.load:                         1.332349   0.000000   1.332349 (  1.346331)
FastJsonparser.parse:            2.706804   0.007238   2.714042 (  2.726935) <-慢

------------------------------- dump JSON to str -------------------------------
                                     user     system      total        real
JSON.dump:                       1.724653   0.009250   1.733903 (  1.733912)
Oj.dump:                         1.298235   0.030041   1.328276 (  1.328279) <-快

------------------------------ dump JSON to file -------------------------------
                                     user     system      total        real
JSON.dump:                       1.765664   0.040595   1.806259 (  1.905785)
Oj.to_file:                      1.228744   0.020309   1.249053 (  1.349684) <-快
=end

效能測試結論:

  • (1).ruby 3之前,fast_jsonparser非常快,但是Ruby 3中的fast_jsonparser很慢
  • (2).OJ解析本地json字串的效能比標準庫json效能稍好,但oj從檔案中載入並解析json的速度很慢
  • (3).OJ將ruby物件解析為json字串的效率比json標準庫效能好

即:

dump:
Oj.dump > JSON.dump

ruby3 之前:
FastJsonparser.load > JSON.load > Oj.load_file
FastJsonparser.parse > Oj.load > JSON.parse

ruby3 之後:
JSON.load > Oj.load_file > FastJsonparser.load
Oj.load > JSON.parse > FastJsonparser.parse

multi_json

有一個名為multi_json的gem包,它提供多種json包的功能,預設採用OJ作為json的適配引擎。它支援下面幾種json介面卡:

  • Oj Optimized JSON by Peter Ohler
  • Yajl Yet Another JSON Library by Brian Lopez
  • JSON The default JSON gem with C-extensions (ships with Ruby 1.9+)
  • JSON Pure A Ruby variant of the JSON gem
  • NSJSONSerialization Wrapper for Apple’s NSJSONSerialization in the Cocoa Framework (MacRuby only)
  • gson.rb A Ruby wrapper for google-gson library (JRuby only)
  • JrJackson JRuby wrapper for Jackson (JRuby only)
  • OkJson A simple, vendorable JSON parser

如果oj已被require,則預設採用oj處理json,如果oj沒有被require,而是require了yajl,則採用yajl處理json,依次類推。

它提供了load()和dump()方法:

load(json_str, options = {})
  options: 
    symbolize_keys: true, false
    adapter:  oj, json_gem, yajl, json_pure, ok_json

dump(object, options = {})

更多關於Ruby處理操作YAMLRuby處理操作json方法請檢視下面的相關連結


IT145.com E-mail:sddin#qq.com