質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.53%

  • Ruby

    7646questions

    Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

ネストの深いhashのパスを再帰的に取得したい

解決済

回答 3

投稿

  • 評価
  • クリップ 1
  • VIEW 423

ykomuro0719

score 1

前提・実現したいこと

ネストの深いhashのパスを再帰的に取得したい

以下のようなネストのあるHashのパスをネストがなくなるまで再帰的に取得したいと考えています

 sample_hash = 
{"nest1-1"=>"value1-1",
 "nest1-2"=>
    {"nest2-1"=>{"nest3-1a"=>"26513", 
                "nest3-1b"=>"3"
                },
     "nest2-2"=>{"nest3-2a"=>"317829", 
                 "nest3-2b"=>"50"
                 }
    }
 }
#ほしい結果
result_array
[
["nest1-1"],
["nest1-2"]["nest2-1"]["nest3-1a"],
["nest1-2"]["nest2-1"]["nest3-1b"],
["nest1-2"]["nest2-2"]["nest3-2a"],
["nest1-2"]["nest2-2"]["nest3-2b"]
]

#上記結果を用いて、ネストしたHashのKey一覧をだし、valueを一意に特定できるようにしたい
sample_hash[result_array[4]] =>"50"
=> sample_hash["nest1-2"]["nest2-2"]["nest3-2b"] 
=>"50"

試したこと

以下のようなメソッドを作り実行しました。

  def get_nested_path(target_hash, *current_path)
    target = target_hash
    path = current_path || []
    target.each do |k,v|
      if target[k].instance_of?(Hash)
        path << target[k]
        get_nested_path(target[k], path)
      else
        path << target[k]
        return path
      end
    end
  end

get_nested_path(sample_hash,nil)
=> [nil, "value1-1"]


上記メソッド自体、1回目のkeyを拾った時点でreturnしてしまうのでうまくいかないことは
理解していますが、上記に記載した結果を得られるようなメソッドの書き方が思いつきません

また、実際に扱うHashはネストの深さが確定していないため、N階層ネストしたHashで実行可能な
メソッドを作れればと思っています。

どなたかわかる方、ご教授ください

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

+1

こんにちは。

以下のような感じで、いかがでしょうか?

# coding: utf-8

sample_hash =
  {"nest1-1" => "value1-1",
   "nest1-2" =>
    {
      "nest2-1" => {
        "nest3-1a" => "26513",
        "nest3-1b" => "3"
      },
      "nest2-2" => {
        "nest3-2a" => "317829",
        "nest3-2b" => "50"
      }
    }
  }

def recursive_traverse(h, paths, pref='')
  h.each do |k, v|
    if v.is_a? String
      paths["#{pref}#{k}"] = v
    elsif v.is_a? Hash
      recursive_traverse(v, paths, "#{pref}#{k}/")
    end
  end
end

paths = {}

recursive_traverse(sample_hash, paths)

paths.each do |k, v|
  puts "#{k}: #{v}"
end

上記を実行すると、以下が出力されます。

nest1-1: value1-1
nest1-2/nest2-1/nest3-1a: 26513
nest1-2/nest2-1/nest3-1b: 3
nest1-2/nest2-2/nest3-2a: 317829
nest1-2/nest2-2/nest3-2b: 50

参考になりましたら幸いです。


追記

以下のような別解を考えました。
(ただし、質問で問われている本題から離れてしまうと思うので、あくまで参考に留めて頂ければと思います)

所与のハッシュsample_hashは、構造的に次のXMLと等価です。

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <nest1-1>value1-1</nest1-1>
    <nest1-2>
        <nest2-1>
            <nest3-1a>26513</nest3-1a>
            <nest3-1b>3</nest3-1b>
        </nest2-1>
        <nest2-2>
            <nest3-2a>317829</nest3-2a>
            <nest3-2b>50</nest3-2b>
        </nest2-2>
    </nest1-2>
</root>


ただしXMLにするためには、最上位のノードは1つなので、上記では
便宜的に <root> 要素をトップに追加しています。

XMLを扱う問題に置き換えれば、value1-1 や 26513は XMLのテキストノードで、
これらはXPath で //text() という指定でまとめて取得できます。
あとは、テキストノードのそれぞれについて、XPathを逆に求めれば、
テキストノードとそれに至るXPathの組を得ることができます。

この考えで作成したスクリプトが以下です。

# coding: utf-8

require 'nokogiri'

sample_hash =
    {'nest1-1' => 'value1-1',
     'nest1-2' =>
         {
             'nest2-1' => {
                 'nest3-1a' => '26513',
                 'nest3-1b' => '3'
             },
             'nest2-2' => {
                 'nest3-2a' => '317829',
                 'nest3-2b' => '50'
             }
         }
    }

class Hash
  def to_xml
    map do |k, v|
      text = Hash === v ? v.to_xml : v
      '<%s>%s</%s>' % [k, text, k]
    end.join
  end
end

xml ='<?xml version="1.0" encoding="UTF-8"?><root>%s</root>' % [sample_hash.to_xml]

Nokogiri::XML(xml).xpath('//text()').each do |item|
    xpath = Nokogiri::CSS.xpath_for item.css_path
    puts '%s=%s' % [xpath[0], item]
end


上記では、XMLをパース(解析)するために、nokogiri を使っており、
このスクリプトを実行すると以下が表示されます。

//root/nest1-1/child::text()=value1-1
//root/nest1-2/nest2-1/nest3-1a/child::text()=26513
//root/nest1-2/nest2-1/nest3-1b/child::text()=3
//root/nest1-2/nest2-2/nest3-2a/child::text()=317829
//root/nest1-2/nest2-2/nest3-2b/child::text()=50

このように与題をXMLのテキストノードの走査と考えて、//text() でテキストノードをまとめて
取ってこれるパーサーを使えば、自分で再帰的にテキストノードを探すメソッドを作らなくても
よくなります。

上記のコードで、Hash#to_xml を再帰メソッドとして実装しているので
「結局手間としては同じでは?」とお思いになるかもしれませんが、
ハッシュをXMLにするメソッドも自分で書かなくても、
以下のようなgemがあります。

この別解で、何が言いたいかと言えば、与件を
「XMLのテキストノードへのパスを取得する」問題と置き換えれば、
自分で再帰メソッドを書く手間をかけなくても欲しい情報が得られるのでは?
ということでした。

ご参考になれば幸いです。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

checkベストアンサー

0

まず,「ほしい結果」ですが,本当は

[["nest1-1"],
 ["nest1-2", "nest2-1", "nest3-1a"],
 ["nest1-2", "nest2-1", "nest3-1b"],
 ["nest1-2", "nest2-2", "nest3-2a"],
 ["nest1-2", "nest2-2", "nest3-2b"]]

ではありませんか?

そういうものを得るメソッドは,こんなふうに書けます。

def paths_of_hash(hash)
  result = []
  hash.each do |k, v|
    if v.is_a?(Hash)
      result.concat paths_of_hash(v).map{ |path| path.unshift(k) }
    else
      result << [k]
    end
  end
  result
end

paths = paths_of_hash(sample_hash)

そして,特定のパスを使って入れ子ハッシュの値を得るには,Hash#dig が使えます。

p sample_hash.dig(*paths[4])
# => "50"

以上のコードでわからない点があったらお尋ねください。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/01/07 13:38

    解答ありがとうございます!
    今回まさにやりたいことが上記でできました!

    キャンセル

0

valueを無名再帰を用いて列挙してみると

sample_hash = 
 {"nest1-1"=>"value1-1",
  "nest1-2"=>
    {"nest2-1"=>{"nest3-1a"=>"26513", 
                "nest3-1b"=>"3"
                },
     "nest2-2"=>{"nest3-2a"=>"317829", 
                 "nest3-2b"=>"50"
                 }
    }
 }

func = proc{|f, (k, v), memo|
  if v.is_a? Hash
    v.each_with_object(memo, &f.curry[f])
  else
    memo << v
  end
}

sample_hash.each_with_object([], &func.curry[func])
# => ["value1-1", "26513", "3", "317829", "50"]

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.53%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • Ruby

    7646questions

    Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。