多线程双向 JSON 解析器

用Ruby实现了一个非常有趣且高级的概念:多线程双向 JSON 解析器(Bi-Directional Onion Parser)

它的核心目标是通过并行处理来提高大文件 JSON 的解析速度。它将文件一分为二,前半部分用"正向解析器"读取,后半部分用"逆向解析器"倒着读取,最后将两部分的结果在中间的"切断点"进行无缝合并。

下面是各个组件的详细原理解析:

一. 核心架构思想

通常 JSON 解析是线性的(从头读到尾)。但对于巨大的文件(例如几百 MB 或 GB),单线程是瓶颈。这个解析器的策略是:

  1. ForwardParser(前锋):从文件头(Byte 0)读到文件中间(Byte N/2)。
  2. ReverseParser(后卫):从文件尾(Byte Max)读到文件中间(Byte N/2)。
  3. OnionBiDirectionalParser(指挥官):负责调度线程,并在中间点将两个"残缺"的解析结果缝合在一起。

二. ForwardParser(正向解析器)

这是一个基于状态机的流式解析器。

  • 工作方式:它像标准的 JSON 解析器一样逐字符读取。
  • 特殊之处
    • 它不需要解析完整个 JSON。当它读到指定的文件中间位置时,它会停止。
    • 它会保留解析状态
      • stack:当前嵌套的层级(比如正在解析哪个数组或对象)。
      • current_container:当前正在填充的数据结构。
      • pending_key:如果正在解析 Hash,记录下当前等待值的 Key。
    • raw_incomplete :这是最关键的属性。当流结束时,最后剩下的无法构成完整 Token 的字符串(例如 "hello wor)会被保存在这里,等待与后半部分拼接。

三. ReverseParser(逆向解析器)

这是整个代码中最具创新性的部分。解析 JSON 通常需要知道上下文(比如"这是数组的第一个元素吗?"),但逆向解析是从 }] 开始倒着推导。

  • 工作方式 :从后往前读字符。
    • 遇到 }:调用 parse_object,开始倒着收集键值对。
    • 遇到 ]:调用 parse_array,倒着收集元素。
    • 遇到 ":调用 parse_string,向前寻找开头的引号。
  • 难点与解决方案(ANON)
    • 当逆向解析器读到中间切分点时,数据会被截断。例如 value"},它解析出了字符串 value,但不知道这个值的 Key 是什么(因为 Key 在前半部分)。
    • 为了解决这个问题,它引入了 ANON ("ANON") 常量。
    • 当它解析到一个孤立的值,或者无法确定其父级关系时,它会将这个值包裹在一个带有 ANON 键的 Hash 中。这相当于告诉合并器:"这里有个东西,但我不知道它的名字,它是断裂点。"

四. OnionBiDirectionalParser(洋葱合并器)

这是逻辑最复杂的地方,负责将两头对接。

三种合并

  1. Hash with pending key :断在对象的值上(如 {"val": "xxx...
  2. Array:断在数组元素上
  3. Hash without pending key:断在对象的键上

合并流程 (merge 方法):

  1. 提取前缀 :查看正向解析器剩下的 raw_incomplete

    • 比如前半部分读到了 {"message": "Hello Wor
    • raw_incomplete 就是 "Hello Wor
    • 提取出字符串前缀 Hello Wor
  2. 寻找最内层 :查看逆向解析器的结果 rev.result

    • 逆向结果可能是 {"__ANON__": {"__ANON__": "ld"}}(因为是倒着构建的,最深层的 ANON 通常对应切分点)。
    • 它会剥离外层的 ANON 包装,找到最核心的那个值(这里是 ld)。
  3. 缝合伤口

    • 将前缀 Hello Wor 和后缀 ld 拼接成 Hello World
    • 填充状态 :检查正向解析器的状态。
      • 如果正向解析器停在 Hash 的 Value 处(有 pending_key),则将拼接好的值赋给这个 Key。
      • 如果停在 Array 中,则将拼接好的值 push 进数组。
  4. 合并剩余结构

    • 逆向解析器除了切断点的值,还可能解析出了属于同一个对象的其他键值对。
    • 代码会遍历逆向结果的剩余部分(top_keys),将它们合并到正向解析器的 root 对象中。

五. 举例演示

假设文件内容是:

javascript 复制代码
{"id": 123, "text": "Hello World", "status": true}

文件在 Hello Wor 和 ld 之间被切分。

线程 1 (Forward):

  • 解析了 {"id": 123,
  • 读到了 Key "text"
  • 读到了前缀 "Hello Wor
  • 状态
    • current_container: {"id": 123}
    • pending_key: "text"
    • raw_incomplete: "Hello Wor

线程 2 (Reverse):

  • 倒着读了 } (对象结束)。
  • 倒着读了 true (Value),"status" (Key)。
  • 倒着读了 ld"
  • 结果
    • 它不知道 ld 属于哪个 Key(因为 Key 在前半部分)。
    • 生成结构(简化版):{"status": true, "__ANON__": "ld"}

合并 (Merge):

  1. Forward 说:"我剩个 Hello Wor,而且我正等着给 text 赋值。"
  2. Reverse 说:"我最里面的断头数据是 ld,但我还有个完整的 status: true。"
  3. 合并器操作:
    • 拼接:Hello Wor + ld = Hello World
    • 赋值:container["text"] = "Hello World"
    • 追加:把 Reverse 里的 status: true 合并进 container
  4. 最终结果{"id": 123, "text": "Hello World", "status": true}

六. 总结

这段代码是一个非常硬核的工程实现。

  • 优点:充分利用多核 CPU,对于包含长字符串或大数组的巨型 JSON 文件,理论解析速度可以翻倍。
  • 技术栈 :涉及手写词法分析(Lexing)、状态机(State Machine)、递归下降(尽管是倒着的)、多线程同步和复杂的数据结构合并算法。

七. 完整代码

ruby 复制代码
# frozen_string_literal: true

require 'thread'
require 'json'

# ==========================================
# 正向解析器 (流式)
# ==========================================
class ForwardParser
  attr_reader :root, :raw_incomplete, :pending_key
  attr_reader :stack, :current_container

  WHITESPACE = " \t\n\r"
  NUM_CHARS = '0123456789.eE+-'

  def initialize
    @root = nil
    @stack = []
    @current_container = nil
    @pending_key = nil
    @state = :value
    @buffer = String.new(capacity: 4096)
  end

  def parse_chunk(chunk)
    @buffer << chunk
  end

  def finalize
    @raw_incomplete = parse_internal(@buffer)
    @root
  end

  private

  def parse_internal(data)
    i = 0
    len = data.length

    while i < len
      c = data[i]

      if WHITESPACE.include?(c)
        i += 1
        next
      end

      case c
      when '{'
        push_container({}, :key)
      when '['
        push_container([], :value)
      when '}', ']'
        pop_container
      when ':'
        @state = :value
      when ','
        @state = @current_container.is_a?(Hash) ? :key : :value
      when '"'
        e = find_str_end(data, i + 1, len)
        if e
          handle_string(unescape(data, i + 1, e))
          i = e
        else
          return data[i..-1]
        end
      when '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
        e = i + 1
        while e < len
          break unless NUM_CHARS.include?(data[e])
          e += 1
        end

        if e == len
          return data[i..-1]
        end

        attach_value(parse_num(data[i...e]))
        i = e - 1
      when 't'
        if i + 4 <= len && data[i, 4] == 'true'
          attach_value(true)
          i += 3
        else
          return data[i..-1]
        end
      when 'f'
        if i + 5 <= len && data[i, 5] == 'false'
          attach_value(false)
          i += 4
        else
          return data[i..-1]
        end
      when 'n'
        if i + 4 <= len && data[i, 4] == 'null'
          attach_value(nil)
          i += 3
        else
          return data[i..-1]
        end
      end
      i += 1
    end

    ""
  end

  def push_container(cont, state)
    attach_value(cont)
    @stack.push({ container: @current_container, key: @pending_key })
    @current_container = cont
    @pending_key = nil
    @state = state
  end

  def pop_container
    frame = @stack.pop
    if frame
      @current_container = frame[:container]
      @pending_key = frame[:key]
    end
    @state = :comma
  end

  def handle_string(str)
    if @state == :key
      @pending_key = str
      @state = :colon
    else
      attach_value(str)
      @state = :comma
    end
  end

  def attach_value(val)
    if @root.nil?
      @root = val
      @current_container = val if val.is_a?(Hash) || val.is_a?(Array)
    elsif @current_container.is_a?(Hash) && @pending_key
      @current_container[@pending_key] = val
      @pending_key = nil
    elsif @current_container.is_a?(Array)
      @current_container << val
    end
  end

  def find_str_end(data, start, len)
    i = start
    while i < len
      c = data[i]
      return i if c == '"'
      i += (c == '\\' ? 2 : 1)
    end
    nil
  end

  def parse_num(s)
    return 0 if s.nil? || s.empty?
    s.include?('.') || s.include?('e') || s.include?('E') ? s.to_f : s.to_i
  end

  def unescape(data, from, to)
    s = data[from...to]
    return s unless s.include?('\\')
    s.gsub('\\\\', "\x00").gsub('\\"', '"')
     .gsub('\\n', "\n").gsub('\\r', "\r").gsub('\\t', "\t").gsub("\x00", '\\')
  end
end

# ==========================================
# 逆向解析器 (流式,内存高效)
# ==========================================
class ReverseParser
  ANON = "__ANON__"
  WHITESPACE = " \t\n\r"
  NUM_CHARS = '0123456789.eE+-'

  attr_reader :result, :raw_incomplete

  def initialize(debug: false)
    @debug = debug
    @result = nil
    @stack = []
    @buffer = ""
    @pending_val = nil
  end

  def parse_chunk(chunk)
    @buffer = chunk + @buffer
    process_buffer
  end

  def finalize
    @raw_incomplete = @buffer

    current = @raw_incomplete

    if @pending_val
      current = { ANON => { key: ANON, value: @pending_val } }
    end

    if @result
      if @result.is_a?(Array)
        @result.unshift(current)
        current = { ANON => @result }
      elsif @result.is_a?(Hash)
        @result[ANON] = current
        current = @result
      end
    end

    while (frame = @stack.pop)
      parent = frame[:container]
      if parent.is_a?(Array)
        parent.unshift(current)
        current = { ANON => parent }
      elsif parent.is_a?(Hash)
        parent[ANON] = current
        current = parent
      end
    end

    @result = current.is_a?(String) ? { ANON => current } : current
  end

  private

  def process_buffer
    loop do
      len = @buffer.length
      return if len == 0

      pos = len - 1

      while pos >= 0 && WHITESPACE.include?(@buffer[pos])
        pos -= 1
      end

      break if pos < 0

      if pos < len - 1
        @buffer = @buffer[0..pos]
        len = pos + 1
      end

      c = @buffer[pos]

      case c
      when '}', ']'
        container = (c == '}') ? {} : []
        push_stack(container)
        @buffer = @buffer[0...pos]

      when '{', '['
        expected_type = (c == '{') ? Hash : Array

        if @result.is_a?(expected_type)
          closed_container = @result
          pop_stack
          attach_value(closed_container)
          @buffer = @buffer[0...pos]
        else
          break
        end

      when ':', ','
        @buffer = @buffer[0...pos]

      when '"'
        val, start_idx = parse_string(pos)
        if val == :incomplete
          break
        else
          attach_value(val)
          @buffer = @buffer[0...start_idx]
        end

      else
        val, start_idx = parse_primitive(pos)
        if val == :incomplete
          break
        else
          attach_value(val)
          @buffer = @buffer[0...start_idx]
        end
      end
    end
  end

  def push_stack(container)
    @stack.push({ container: @result, pending_val: @pending_val })
    @result = container
    @pending_val = nil
  end

  def pop_stack
    frame = @stack.pop
    if frame
      @result = frame[:container]
      @pending_val = frame[:pending_val]
    else
      @result = nil
    end
  end

  def attach_value(val)
    if @result.is_a?(Array)
      @result.unshift(val)
    elsif @result.is_a?(Hash)
      if @pending_val.nil?
        @pending_val = val
      else
        key = val
        @result[key] = @pending_val
        @pending_val = nil
      end
    else
      @result = val
    end
  end

  def parse_string(end_pos)
    i = end_pos - 1
    while i >= 0
      if @buffer[i] == '"'
        esc = 0
        j = i - 1
        while j >= 0 && @buffer[j] == '\\'
          esc += 1
          j -= 1
        end

        if esc.even?
          str = unescape(@buffer[(i+1)...end_pos])
          return [str, i]
        end
      end
      i -= 1
    end
    [:incomplete, end_pos]
  end

  def parse_primitive(end_pos)
    i = end_pos
    while i >= 0
      c = @buffer[i]
      is_valid = NUM_CHARS.include?(c) || "truefalsenull".include?(c)
      break unless is_valid
      i -= 1
    end

    if i < 0
      return [:incomplete, end_pos]
    end

    start_idx = i + 1
    s = @buffer[start_idx..end_pos]
    val = nil

    case s
    when 'true' then val = true
    when 'false' then val = false
    when 'null' then val = nil
    else
      if s =~ /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/
        val = s.include?('.') || s.include?('e') || s.include?('E') ? s.to_f : s.to_i
      else
        return [:incomplete, end_pos]
      end
    end

    [val, start_idx]
  end

  def unescape(s)
    return s unless s.include?('\\')
    s.gsub('\\\\', "\x00").gsub('\\"', '"')
     .gsub('\\n', "\n").gsub('\\r', "\r").gsub('\\t', "\t").gsub("\x00", '\\')
  end
end

# ==========================================
# 洋葱式双向解析器
# ==========================================
class OnionBiDirectionalParser
  ANON = ReverseParser::ANON

  def initialize(filepath, chunk_size: 8192, debug: false)
    @filepath = filepath
    @file_size = File.size(filepath)
    @chunk_size = chunk_size
    @debug = debug
  end

  def parse
    return parse_single_stream if @file_size <= @chunk_size

    mid = @file_size / 2

    fwd = ForwardParser.new
    rev = ReverseParser.new(debug: @debug)

    t1 = Thread.new { stream_forward(fwd, 0, mid) }
    t2 = Thread.new { stream_reverse(rev, mid, @file_size) }

    t1.join
    t2.join

    log "=== 合并开始 ==="
    log "正向incomplete: #{fwd.raw_incomplete.inspect}"
    log "逆向incomplete: #{rev.raw_incomplete.inspect}"

    merge(fwd, rev)
  end

  private

  def stream_forward(parser, from, to)
    File.open(@filepath, 'rb') do |f|
      f.seek(from)
      remaining = to - from
      while remaining > 0
        chunk = f.read([remaining, @chunk_size].min)
        break unless chunk && !chunk.empty?
        parser.parse_chunk(chunk)
        remaining -= chunk.length
      end
    end
    parser.finalize
  end

  def stream_reverse(parser, from, to)
    File.open(@filepath, 'rb') do |f|
      pos = to
      while pos > from
        read_size = [pos - from, @chunk_size].min
        start_pos = pos - read_size
        f.seek(start_pos)

        chunk = f.read(read_size)
        parser.parse_chunk(chunk)

        pos -= read_size
      end
    end
    parser.finalize
  end

  def parse_single_stream
    fwd = ForwardParser.new
    File.open(@filepath, 'rb') do |f|
      while (chunk = f.read(@chunk_size))
        fwd.parse_chunk(chunk)
      end
    end
    fwd.finalize
  end

  def merge(fwd_parser, rev_parser)
    fwd_root = fwd_parser.root
    rev_root = rev_parser.result
    fwd_current = fwd_parser.current_container
    fwd_pending = fwd_parser.pending_key
    fwd_stack = fwd_parser.stack
    fwd_incomplete = fwd_parser.raw_incomplete || ""

    return fwd_root if rev_root.nil?
    return clean_anon!(rev_root) if fwd_root.nil?

    string_prefix = extract_string_prefix(fwd_incomplete)
    log "字符串前缀: #{string_prefix.inspect}"

    innermost, top_keys = find_innermost(rev_root)
    log "最内层: #{innermost.inspect[0..200]}"
    log "正向栈深度: #{fwd_stack.length}"

    # 获取逆向解析的完整数组(如果存在)
    reverse_array = find_complete_array(rev_root)
    log "逆向数组: #{reverse_array ? "长度#{reverse_array.length}" : "无"}"

    merged_value = nil
    remaining_siblings = {}

    # 计算合并后的值
    if innermost.is_a?(Hash) && innermost.key?(ANON)
      core = innermost[ANON]
      remaining_siblings = innermost.reject { |k, _| k == ANON }

      if core.is_a?(Hash) && core.key?(:key)
        suffix = strip_trailing_quote(core[:key], string_prefix)
        full_key = string_prefix + suffix
        merged_value = { full_key => core[:value] }
      else
        if string_prefix.empty?
          raw_val = fwd_incomplete + core.to_s
          merged_value = try_parse_primitive(raw_val)
        else
          suffix = strip_trailing_quote(core, string_prefix)
          merged_value = string_prefix + suffix
        end
      end
    elsif innermost.is_a?(String)
      if string_prefix.empty?
        raw_val = fwd_incomplete + innermost
        merged_value = try_parse_primitive(raw_val)
      else
        suffix = strip_trailing_quote(innermost, string_prefix)
        merged_value = string_prefix + suffix
      end
    else
      merged_value = innermost
    end

    log "合并值: #{merged_value.inspect[0..100]}"

    # 填充到正向结构
    if fwd_pending && fwd_current.is_a?(Hash)
      log "场景: Hash with pending key"
      fwd_current[fwd_pending] = merged_value
      remaining_siblings.each { |k, v| fwd_current[k] = clean_anon!(v) }

      # 关键修复:检查这个 Hash 是否在数组中
      # 如果是,需要将逆向数组的剩余元素添加到父数组
      parent_array = find_parent_array(fwd_stack, fwd_current)
      if parent_array && reverse_array && reverse_array.length > 1
        log "Hash 在数组中,添加数组后续元素"
        reverse_array[1..-1].each do |item|
          parent_array << clean_anon!(item)
        end
        log "父数组最终长度: #{parent_array.length}"
      end

    elsif fwd_current.is_a?(Array)
      log "场景: Array 填充"
      fwd_current << merged_value
      log "添加合并值后数组长度: #{fwd_current.length}"

      # 处理逆向数组的其余元素
      if reverse_array && reverse_array.is_a?(Array) && reverse_array.length > 1
        log "添加逆向数组元素: #{reverse_array.length - 1} 个"
        reverse_array[1..-1].each do |item|
          fwd_current << clean_anon!(item)
        end
        log "数组最终长度: #{fwd_current.length}"
      end

    elsif fwd_current.is_a?(Hash)
      log "场景: Hash without pending key"
      if merged_value.is_a?(Hash)
        merged_value.each { |k, v| fwd_current[k] = clean_anon!(v) }
      end
      remaining_siblings.each { |k, v| fwd_current[k] = clean_anon!(v) }
    end

    # 合并顶层其他键
    if fwd_root.is_a?(Hash)
      top_keys.each do |k, v|
        fwd_root[k] = clean_anon!(v) unless fwd_root.key?(k)
      end
    end

    fwd_root
  end

  # 查找包含当前容器的父数组
  # 这是解决大文件测试问题的关键方法
  def find_parent_array(stack, current_container)
    return nil if stack.empty?

    # 从栈顶向下查找包含 current_container 的数组
    stack.reverse_each do |frame|
      container = frame[:container]
      return container if container.is_a?(Array) && container.include?(current_container)
    end

    nil
  end

  # 查找逆向解析中的完整数组(包含所有元素)
  def find_complete_array(obj)
    return nil unless obj.is_a?(Hash)

    curr = obj
    while curr.is_a?(Hash) && curr.key?(ANON)
      anon_val = curr[ANON]
      return anon_val if anon_val.is_a?(Array)
      curr = anon_val
    end
    nil
  end

  def try_parse_primitive(str)
    return str unless str.is_a?(String)
    return str if str.strip.empty?

    case str.strip
    when 'true' then true
    when 'false' then false
    when 'null' then nil
    else
      if str =~ /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/
        str.include?('.') || str.include?('e') || str.include?('E') ? str.to_f : str.to_i
      else
        str
      end
    end
  end

  def strip_trailing_quote(str, prefix)
    return str if prefix.nil? || prefix.empty?
    return str unless str.is_a?(String)
    str.end_with?('"') ? str[0...-1] : str
  end

  def extract_string_prefix(incomplete)
    return "" if incomplete.nil? || incomplete.empty?
    if incomplete =~ /:\s*"([^"]*)$/
      $1
    elsif incomplete =~ /"([^"]*)$/
      $1
    else
      ""
    end
  end

  def find_innermost(obj)
    top_keys = {}
    current = obj

    while current.is_a?(Hash) && current.key?(ANON)
      current.each do |k, v|
        top_keys[k] = v unless k == ANON
      end
      current = current[ANON]
    end

    if current.is_a?(Array) && current.any?
      first = current.first
      if first.is_a?(Hash) && first.key?(ANON)
        inner, extra = find_innermost(first)
        return [inner, top_keys.merge(extra)]
      else
        return [first, top_keys]
      end
    end

    if current.is_a?(Hash) && !current.key?(ANON)
      current.each do |k, v|
        top_keys[k] = v unless k == ANON
      end
    end

    [current, top_keys]
  end

  def clean_anon!(obj)
    case obj
    when Hash
      if obj.key?(ANON)
        anon_val = obj.delete(ANON)
        return clean_anon!(anon_val) if obj.empty?
      end
      obj.transform_values! { |v| clean_anon!(v) }
    when Array
      obj.map! { |v| clean_anon!(v) }
    end
    obj
  end

  def log(msg)
    puts "[Onion] #{msg}" if @debug
  end
end

# ==========================================
# 测试用例
# ==========================================
if __FILE__ == $0
  def test(name, json, chunk_size: 64, debug: false)
    file = "test_#{name}.json"
    File.write(file, json)
    expected = JSON.parse(json)

    begin
      result = OnionBiDirectionalParser.new(file, chunk_size: chunk_size, debug: debug).parse
    rescue => e
      puts "#{name}: 💥 Error: #{e.message}"
      puts e.backtrace[0..5].join("\n")
      File.delete(file) rescue nil
      return false
    end

    ok = result == expected

    print "#{name}: #{ok ? '✅' : '❌'}"
    puts

    File.delete(file) rescue nil
    ok
  end

  puts "=" * 50
  puts "洋葱式双向 JSON 解析器测试"
  puts "=" * 50

  puts "\n基础测试"
  r = []
  r << test("t1", '{"a": 1, "b": 2, "c": 3}')
  r << test("t2", '{"x": {"y": 1}, "z": 2}')
  r << test("t3", '[1, 2, 3, 4, 5]')
  r << test("t4", '{"arr": [1, 2, 3], "obj": {"k": "v"}}')

  puts "\n边界测试"
  r << test("str1", '{"key": "hello world"}', chunk_size: 10)
  r << test("num1", '{"a": 12345}', chunk_size: 5)
  r << test("num2", '{"a": 12345}', chunk_size: 7)

  puts "\n大文件测试"
  large = {"data" => (1..100).map { |i| {"id" => i, "value" => "item_#{i}" * 5} }}
  r << test("large_100", JSON.generate(large), chunk_size: 256)

  puts "\n大文件测试 (不同 chunk_size)"
  large = {"data" => (1..1000).map { |i| {"id" => i, "value" => "item_#{i}" * 10} }}
  large_json = JSON.generate(large)

  [64, 256, 1024, 4096].each do |cs|
    r << test("large_#{cs}", large_json, chunk_size: cs)
  end

  puts "\n总结: #{r.count(true)}/#{r.length}"
end
相关推荐
开心香辣派小星1 小时前
23种设计模式-19策略模式(Strategy Pattern)
java·设计模式·策略模式
xcLeigh1 小时前
超全 Kingbase KES V9R3C15 JSON 函数指南:从基础操作到高级应用
json·函数·国产数据库·kingbase·金仓数据库
苏小瀚1 小时前
[JavaSE] 网络原理(TCP_IP)
服务器·网络·tcp/ip
qq_2153978971 小时前
java 依赖包引入本地maven库
java·maven
青衫码上行1 小时前
【JavaWeb学习 | 第18篇】Servlet与MVC
java·学习·servlet·mvc
IDOlaoluo1 小时前
apache-maven-3.9.9-bin.zip 使用步骤(超简单版)
java·maven·apache
曹牧1 小时前
Java:list<map<string,sting>>与C#互操作
java·c#·list
卷到起飞的数分1 小时前
23.Maven高级——私服
java·maven
model20051 小时前
Alibaba linux 3安装mapserver
linux·运维·服务器