DOBON.NET DOBON.NETプログラミング掲示板過去ログ

Sectionが被るINIファイルの読み書き(VB)

環境/言語:[Windows 8, VB 2010]
分類:[.NET]

VB 2010を使っています。
次のようなINIファイル
[ObjectA]
Name = c
x = 300
y = 500
[ObjectA]
Name = d
x = 240
y = 490
を読み込み、
cという名前のObjectAというオブジェクトを作成、プロパティx = 300 プロパティy = 500
dという名前のObjectAというオブジェクトを作成、プロパティx = 240 プロパティy = 490
のようなプログラムを組みたいと思っていますが、
この目的を果たせるようにINIファイルを読み込むにはどうすればいいでしょうか。
INIの仕様は変更できません。
Section名が被るINIはどう読み書きすればいいでしょうか。
> この目的を果たせるようにINIファイルを読み込むにはどうすればいいでしょうか。
> INIの仕様は変更できません。
> Section名が被るINIはどう読み書きすればいいでしょうか。

  GetPrivateProfile... API の仕様には逸脱している為、APIは
  使えません。

  よって、普通に自分でファイルオープンして、1行づつ読み込み
  解析して必要な情報を取得するしかないでしょう。

以上。
■No31893に返信(オショウさんの記事)
>   よって、普通に自分でファイルオープンして、1行づつ読み込み
>   解析して必要な情報を取得するしかないでしょう。

そのファイルから読み込んだ結果を

<package>
  <objectA Name="c">
    <x>300</x>
    <y>500</x>
  </object>
  <objectA Name="d">
    <x>240</x>
    <y>490</x>
  </object>
</package>

という形式の XDocument オブジェクトとして読み込むようにしてみました。


Imports System.IO
Imports System.Text.RegularExpressions

Public Function LoadTree(ByVal filePath As String) As XDocument
    Dim opt = RegexOptions.Singleline Or RegexOptions.CultureInvariant
    Dim reSection As New Regex("^\s*\[(?<Section>[^\]]+)\].*$", opt)
    Dim reEntry As New Regex("^\s*(?<Key>[^=]+)=(?<Value>.*?)$", opt)

    Dim Doc As New XDocument(<Package/>)
    Dim root = Doc.Root
    Dim item As XElement = Nothing
    Dim m As Match
    For Each line In File.ReadLines(filePath)
        line = Trim(line)
        If line Like "[;#]*" Then
            Continue For    'コメント行は読み飛ばす
        End If
        m = reSection.Match(line)
        If m.Success Then
            item = <<%= m.Groups("Section").Value %> Name="" />
            root.Add(item)
            Continue For
        End If
        m = reEntry.Match(line)
        If m.Success Then
            If item Is Nothing Then
                item = <Default Name=""/>
                root.Add(item)
            End If
            Dim key As String = Trim(m.Groups("Key").Value)
            Dim value As String = Trim(m.Groups("Value").Value)
            If key.ToLowerInvariant() = "name" Then
                item.@Name = value
            Else
                item.Add(<<%= key %>><%= value %></>)
            End If
        End If
    Next
    Return Doc
End Function
2013/11/11(Mon) 15:50:16 編集(投稿者)
2013/11/11(Mon) 10:14:01 編集(投稿者)

それはiniファイルでなければダメなのでしょうか?
iniファイルから下記の様なxmlの中間ファイルを作成し

<?xml version="1.0" standalone="yes"?>
<root>
  <ObjectA>
    <Name>c</Name>
    <x>300</x>
    <y>500</y>
  </ObjectA>
  <ObjectA>
    <Name>d</Name>
    <x>240</x>
    <y>490</y>
  </ObjectA>
</root>

下記のような感じで読み込むのではダメですかね?

Imports System.IO

Public Class Form1

    Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
        Dim dataSet As New DataSet
        Dim row As DataRow
        Dim dtObjectA As DataTable
        Dim ConfigFileName As String = "c:\init.xml"

        Try
            dataSet.ReadXml(ConfigFileName)

            dtObjectA = dataSet.Tables("ObjectA")

            If IsNothing(dtObjectA) Then
                'ObjectAタグが無い場合の処理
            Else
                For Each row In dtObjectA.Rows
                    Debug.Print(row.Item("Name"))
                    Debug.Print(row.Item("x"))
                    Debug.Print(row.Item("y"))
                Next
            End If

        Catch ex As FileNotFoundException
            'ファイルが無い場合の処理
            dtObjectA = New DataTable
            With dtObjectA
                .TableName = "ObjectA"
                With .Columns
                    .Add("Name")
                    .Add("x")
                    .Add("y")
                End With

                Dim Ar(,) As String = {{"c", "300", "500"}, {"d", "240", "490"}}
                For i = 0 To Ar.Rank - 1
                    With .Rows
                        .Add(Ar(i, 0).ToString, Ar(i, 1).ToString, Ar(i, 2).ToString)
                    End With
                Next
            End With

            With dataSet
                .DataSetName = "root"
                .Tables.Add(dtObjectA)
                .WriteXml(ConfigFileName)
            End With

        Catch ex As Exception
        End Try

    End Sub
End Class

ありゃ、回答若干かぶった。
■No31892に返信(Ltd.Exp.Nisonさんの記事)
> INIの仕様は変更できません。

Windows 形式の INI ファイルには、一般的に
GetPrivateProfileStringW API あるいは
GetPrivateProfileStringA API が使われますが、
今回は形式が異なるため、これらの API は一切使えません。


一応、空指定で(セクションとキーを Nothing にして)上記 API を呼び出せば
同名セクションであっても、『セクション名の一覧を列挙する』ぐらいはできました。

ただ、肝心の『値を取得する』方については、同一セクションを識別するような
指定方法ができないため、やはり GetPrivateProfileString API は使えません。
(WritePrivateProfileString API についても同様です)


とりあえず No31895 のコードで変換したコードについては、
その内容をこんな感じで読み取れます。


Dim ini = LoadTree("C:\temp\test.ini")

' Name = c な [ObjectA]セクションの x と y を読み取る
Dim itemName As String = "c"
Dim obj = ini.Root.Elements.Where(Function(tag) tag.@Name = itemName).First()
Dim x As String = obj.<x>.Value
Dim y As String = obj.<y>.Value

'別案:XPath で取り出す場合
'Imports System.Xml.XPath
Dim obj2 = Extensions.XPathSelectElement(ini, "//*[@Name='" & itemName & "']")
x = obj2.<x>.Value
y = obj2.<x>.Value




> どう読み書きすればいいでしょうか。

書き込み例:


'最初に見つかった ObjectA の x を、111 に変更
ini.Root.<ObjectA>.<x>.Value = "111"

'2番目に見つかった ObjectA の y を、222 に変更
ini.Root.<ObjectA>(1).<y>.Value = "222"

'Name=c なエントリーの y を 333 に変更
ini.Descendants.Where(Function(tag) tag.@Name = "c").<y>.Value = "333"

'Name、x、y のプロパティを持つ ObjectA のエントリーを追加
Dim o = New With {.Name = "xxx", .x = "100", .y = "200"}  'これの内容を追加

ini.Root.Add(<ObjectA Name=<%= o.Name %>>
                 <x><%= o.x %></x>
                 <y><%= o.x %></y>
             </ObjectA>)


'XML 形式のままファイルに保存
'ini.Save("C:\temp\test.xml")

'元の ini 形式に復元して保存
Using stm As New StreamWriter("C:\temp\test.ini", False, Encoding.Default)
    For Each tag In ini.Root.Elements
        stm.WriteLine("[" & tag.Name.LocalName & "]")
        stm.WriteLine("Name=" & tag.@Name)
        For Each prop In tag.Elements
            stm.WriteLine(prop.Name.LocalName & "=" & prop.Value)
        Next
        stm.WriteLine()
    Next
    stm.Close()
End Using



もしも XDocument 形式のままだと扱いにくいなら、セクション情報と値の一覧を
Dictionary(Of String, Dictionary(Of String, String)) や
Dictionary(Of String, ExpandoObject) などで管理してみたり、
あるいはコレクション化せず、特定のセクション/エントリのみを
読み書きする Function を用意するといった方法もあるかと思います。


まぁ、いずれにせよ No31893 でも言われているように
>>>  よって、普通に自分でファイルオープンして、1行づつ読み込み
>>>  解析して必要な情報を取得するしかないでしょう。
になってしまうかと思います。
気になる点・・・

そのINIファイルを作成する際には、どうやって作ったの?
WritePrivateProfileString 等を使って行えば、そういう仕様のINIファイル
は作成できない。

逆に普通にファイルストリーム等のテキストファイルを出力するプログラム
で作成したなら、GetPrivateProfileString 等を使わず、そのままファイル
ストリームとして読み込んで解析すればよい。

INIファイルの仕様としては逸脱していますが、ニワトリが先か卵が先か・・・
みたいな話に思えます。

以上。
■No31892に返信(Ltd.Exp.Nisonさんの記事)
INIファイルのような形式をしているだけで
処理内容はファイルを順に読んでいけばよい
内容なのでINIファイルと思わないで普通に
テキストファイルの読込を行えばよいと思います。
投稿ありがとうございます。
赤の他人が作ったアプリケーションで使うファイルなので、
形式変更はできません。さらには、手打ちで読み書きするものです。
「赤の他人が作ったアプリケーション」では読み込み、処理はしますが
編集はできません。
編集ツールみたいなものを作ろうとしています。
やはり一行ずつ読み込むのが得策、ですか。
頑張ってやってみます。
ありがとうございました。
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板