Ruby の rexml で XML をパースするの２

# はじめに
移植やってます。
( from python 3.7 to ruby 2.7 )
# 概要
https://qiita.com/superrino130/items/91d3357ce808008eb96d

前回の記事で簡単そうだったのですが、実際は難しい操作でした。
各種のxmlに対応させるclassのため、`xpath`が不定でタグをスキャンして、その階層に合わせてパースする必要がありました。
# どうすRuby
```xml:sample
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema targetNamespace="http://regis-web.systemsbiology.net/pepXML" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://regis-web.systemsbiology.net/pepXML" xmlns:pepx="http://regis-web.systemsbiology.net/pepXML" elementFormDefault="qualified">
	<xs:annotation>
	</xs:annotation>
	<xs:element name="msms_pipeline_analysis">
		<xs:complexType>
			<xs:sequence>
				<xs:element name="analysis_summary" minOccurs="0" maxOccurs="unbounded">
				</xs:element>
				<xs:element name="dataset_derivation" minOccurs="0">
					<xs:annotation>
					</xs:annotation>
					<xs:complexType>
						<xs:sequence>
						</xs:sequence>
						<xs:attribute name="generation_no" type="xs:nonNegativeInteger" use="required">
						</xs:attribute>
					</xs:complexType>
				</xs:element>
			</xs:sequence>
		</xs:complexType>
	</xs:element>
</xs:schema>
```
この様な`xsd`から、処理の流れとして
1) `xs:attribute`タグを探す
2) `type`属性の値が`['int', 'long', 'nonNegativeInteger', 'positiveInt', 'integer', 'unsignedInt']`のいずれか
3) 親タグが`element`なら`name`属性の値を取る
4) 親タグが`complexType`なら更に上の親タグの`element`の`name`属性を取る
5) 配列に格納する（結果として、下記のデータセットが格納）
6) `xs:attribute`タグがなくなるまで繰り返す
```rb
["dataset_derivation", "generation_no"]
```
```rb:sax2
require 'rexml/parsers/sax2parser'
require 'rexml/sax2listener'
require 'rexml/document'
require 'set'

module REXML
  module Parsers
		class SAX2Parser
			def get_tag_stack
				@tag_stack.dup
			end
		end
	end
end

xml = (<<XML)
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema targetNamespace="http://regis-web.systemsbiology.net/pepXML" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://regis-web.systemsbiology.net/pepXML" xmlns:pepx="http://regis-web.systemsbiology.net/pepXML" elementFormDefault="qualified">
	<xs:annotation>
	</xs:annotation>
	<xs:element name="msms_pipeline_analysis">
		<xs:complexType>
			<xs:sequence>
				<xs:element name="analysis_summary" minOccurs="0" maxOccurs="unbounded">
				</xs:element>
				<xs:element name="dataset_derivation" minOccurs="0">
					<xs:annotation>
					</xs:annotation>
					<xs:complexType>
						<xs:sequence>
						</xs:sequence>
						<xs:attribute name="generation_no" type="xs:nonNegativeInteger" use="required">
						</xs:attribute>
					</xs:complexType>
				</xs:element>
			</xs:sequence>
		</xs:complexType>
	</xs:element>
</xs:schema>
XML

parser = REXML::Parsers::SAX2Parser.new(xml)
doc = REXML::Document.new(xml)

elements = []
parser.listen(:start_element, ["xs:attribute"]){|uri, localname, qname, attrs|
  elements << [parser.get_tag_stack, attrs]
}
parser.parse

dataset = Set.new()
elements.each do |element|
	if ['int', 'long', 'nonNegativeInteger', 'positiveInt', 'integer', 'unsignedInt'].any?{ element[1]["type"].include?(_1) }
		REXML::XPath.match(doc, element[0].join('/')).each do |xp|
			case xp.parent.local_name
			when 'element'
				dataset << [xp.parent.attribute('name').value, xp.attribute('name').value]
			when 'complexType'
				if xp.parent.parent.local_name == 'element'
					dataset << [xp.parent.parent.attribute('name').value, xp.attribute('name').value]
				end
			end
		end
	end
end
p dataset

# <Set: {["dataset_derivation", "generation_no"]}>
```
無理矢理感が凄まじい。
```rb:sax2parser
module REXML
  module Parsers
		class SAX2Parser
			def get_tag_stack
				@tag_stack.dup
			end
		end
	end
end
```
`SAX2Parser.rb`の`@tag_stack`にツリー情報がありますので、オープンクラスで取得します。
```rb:parser
parser.listen(:start_element, ["xs:attribute"]){|uri, localname, qname, attrs|
  elements << [parser.get_tag_stack, attrs]
}
```
一見分かりにくいのですが、`parser.listen()`にブロックを渡しています。

`SAX`の情報が少ないので大変です。
# 追記（xpath版）
本番環境で上記`sax2版`ですと、`elements`が空になりました。
そこで、xmlの階層が深いことはないだろうと考え、xpath版に切り替えます。

```rb:xpath
require 'rexml/document'
require 'set'

xml = (<<XML)
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema targetNamespace="http://regis-web.systemsbiology.net/pepXML" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://regis-web.systemsbiology.net/pepXML" xmlns:pepx="http://regis-web.systemsbiology.net/pepXML" elementFormDefault="qualified">
	<xs:annotation>
	</xs:annotation>
	<xs:element name="msms_pipeline_analysis">
		<xs:complexType>
			<xs:sequence>
				<xs:element name="analysis_summary" minOccurs="0" maxOccurs="unbounded">
				</xs:element>
				<xs:element name="dataset_derivation" minOccurs="0">
					<xs:annotation>
					</xs:annotation>
					<xs:complexType>
						<xs:sequence>
						</xs:sequence>
						<xs:attribute name="generation_no" type="xs:nonNegativeInteger" use="required">
						</xs:attribute>
					</xs:complexType>
				</xs:element>
			</xs:sequence>
		</xs:complexType>
	</xs:element>
</xs:schema>
XML

doc = REXML::Document.new(xml)
trees = Set.new()
attrees = Set.new()
ret = []

doc.children.each do |ch|
	if ch.respond_to?(:expanded_name)
		trees << ch.expanded_name
	end
end

while trees.empty?.!
	adtrees = Set.new()
	trees.each do |tree|
		doc.elements.each(tree) do |chs|
			chs.children.each do |ch|
				if ch.respond_to?(:expanded_name)
					adtrees << tree + '/' + ch.expanded_name
					if ch.name == 'attribute'
						attrees << tree + '/' + ch.expanded_name
						if ['int', 'long', 'nonNegativeInteger', 'positiveInt', 'integer', 'unsignedInt'].include?(ch.attributes['type'].sub(ch.prefix + ':', ''))
							case ch.parent.local_name
							when 'element'
								ret << [ch.parent.attribute('name').value, ch.attribute('name').value]
							when 'complexType'
								if ch.parent.parent.local_name == 'element'
									ret << [ch.parent.parent.attribute('name').value, ch.attribute('name').value]
								end
							end
						end
					end
				end	
			end
		end
	end
	trees = adtrees
end
p ret

# output
[["dataset_derivation", "generation_no"]]
```
これも無理矢理感が凄いです。
```rb:element
		doc.elements.each(tree) do |chs|
```
ここを`each`にしないと、１番めの要素のみを拾います。

https://uxmilk.jp/25936
# メモ
+ Ruby の rexml を学習した
+ 百里を行く者は九十里を半ばとす