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

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

新規登録して質問してみよう
ただいま回答率
85.49%
Ruby

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

Q&A

解決済

3回答

1317閲覧

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

ykomuro0719

総合スコア7

Ruby

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

0グッド

1クリップ

投稿2018/01/04 09:21

###前提・実現したいこと
ネストの深いhashのパスを再帰的に取得したい

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

ruby

1 2 sample_hash = 3{"nest1-1"=>"value1-1", 4 "nest1-2"=> 5 {"nest2-1"=>{"nest3-1a"=>"26513", 6 "nest3-1b"=>"3" 7 }, 8 "nest2-2"=>{"nest3-2a"=>"317829", 9 "nest3-2b"=>"50" 10 } 11 } 12 } 13#ほしい結果 14result_array 15[ 16["nest1-1"], 17["nest1-2"]["nest2-1"]["nest3-1a"], 18["nest1-2"]["nest2-1"]["nest3-1b"], 19["nest1-2"]["nest2-2"]["nest3-2a"], 20["nest1-2"]["nest2-2"]["nest3-2b"] 21] 22 23#上記結果を用いて、ネストしたHashのKey一覧をだし、valueを一意に特定できるようにしたい 24sample_hash[result_array[4]] =>"50" 25=> sample_hash["nest1-2"]["nest2-2"]["nest3-2b"] 26=>"50" 27

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

ruby

1 2 def get_nested_path(target_hash, *current_path) 3 target = target_hash 4 path = current_path || [] 5 target.each do |k,v| 6 if target[k].instance_of?(Hash) 7 path << target[k] 8 get_nested_path(target[k], path) 9 else 10 path << target[k] 11 return path 12 end 13 end 14 end 15 16get_nested_path(sample_hash,nil) 17=> [nil, "value1-1"] 18

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

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

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

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

こんにちは。

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

ruby

1# coding: utf-8 2 3sample_hash = 4 {"nest1-1" => "value1-1", 5 "nest1-2" => 6 { 7 "nest2-1" => { 8 "nest3-1a" => "26513", 9 "nest3-1b" => "3" 10 }, 11 "nest2-2" => { 12 "nest3-2a" => "317829", 13 "nest3-2b" => "50" 14 } 15 } 16 } 17 18def recursive_traverse(h, paths, pref='') 19 h.each do |k, v| 20 if v.is_a? String 21 paths["#{pref}#{k}"] = v 22 elsif v.is_a? Hash 23 recursive_traverse(v, paths, "#{pref}#{k}/") 24 end 25 end 26end 27 28paths = {} 29 30recursive_traverse(sample_hash, paths) 31 32paths.each do |k, v| 33 puts "#{k}: #{v}" 34end 35 36 37

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

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

1<?xml version="1.0" encoding="UTF-8"?> 2<root> 3 <nest1-1>value1-1</nest1-1> 4 <nest1-2> 5 <nest2-1> 6 <nest3-1a>26513</nest3-1a> 7 <nest3-1b>3</nest3-1b> 8 </nest2-1> 9 <nest2-2> 10 <nest3-2a>317829</nest3-2a> 11 <nest3-2b>50</nest3-2b> 12 </nest2-2> 13 </nest1-2> 14</root>

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

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

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

ruby

1# coding: utf-8 2 3require 'nokogiri' 4 5sample_hash = 6 {'nest1-1' => 'value1-1', 7 'nest1-2' => 8 { 9 'nest2-1' => { 10 'nest3-1a' => '26513', 11 'nest3-1b' => '3' 12 }, 13 'nest2-2' => { 14 'nest3-2a' => '317829', 15 'nest3-2b' => '50' 16 } 17 } 18 } 19 20class Hash 21 def to_xml 22 map do |k, v| 23 text = Hash === v ? v.to_xml : v 24 '<%s>%s</%s>' % [k, text, k] 25 end.join 26 end 27end 28 29xml ='<?xml version="1.0" encoding="UTF-8"?><root>%s</root>' % [sample_hash.to_xml] 30 31Nokogiri::XML(xml).xpath('//text()').each do |item| 32 xpath = Nokogiri::CSS.xpath_for item.css_path 33 puts '%s=%s' % [xpath[0], item] 34end

上記では、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のテキストノードへのパスを取得する」問題と置き換えれば、
自分で再帰メソッドを書く手間をかけなくても欲しい情報が得られるのでは?
ということでした。

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

投稿2018/01/04 10:43

編集2018/01/04 18:51
jun68ykt

総合スコア9058

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

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"]

投稿2018/01/04 14:13

asm

総合スコア15147

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

ベストアンサー

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

rb

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

ではありませんか?

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

rb

1def paths_of_hash(hash) 2 result = [] 3 hash.each do |k, v| 4 if v.is_a?(Hash) 5 result.concat paths_of_hash(v).map{ |path| path.unshift(k) } 6 else 7 result << [k] 8 end 9 end 10 result 11end 12 13paths = paths_of_hash(sample_hash)

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

rb

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

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

投稿2018/01/04 10:46

scivola

総合スコア2108

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ykomuro0719

2018/01/07 04:38

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.49%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問