C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

Re[14]: エスケープされた文字列の分割


(過去ログ 64 を表示中)

[トピック内 27 記事 (1 - 20 表示)]  << 0 | 1 >>

■36290 / inTopicNo.1)  エスケープされた文字列の分割
  
□投稿者/ みきぬ (443回)-(2009/05/26(Tue) 22:11:58)

分類:[VB.NET/VB2005 以降] 

環境:VisualStudio 2005
※分類は VB.NET にしていますが、C# でも平気です。

外部の処理からStringオブジェクトを受け取り、";#" で分割してString配列にするという処理です。
単純な処理ではあるものの、うまい方法が見つからずにもやもやしています。

[入力の書式]
<文字列1>;#<文字列2>;#<文字列3>;# (...) ;#<文字列n>

[求めたい結果]
<文字列1'> 〜 <文字列n'> を要素に持つ、長さ n の String配列
ただし、<文字列n> は、<文字列n'> の中にある ";" を ";;" に置換した文字列。

<文字列n'> に ";#" が含まれている場合、<文字列n> では置換されて ";;#" になるわけですが、
これを分割の対象にしてはいけません。
この仕様があるため、String.Split() で単純に分割するということができなくて困っています。
Regex.Split() でいけるかな? とも考えてみたのですが、うまい正規表現が見つかりません。

で、以下の方法で一応できそうなのですが、<文字列> に含まれることがあり得ないような
ダミーの文字列を考えなくてはならないところに難を感じています。

        Const DUMMY_TEXT As String = "__==__"

        Dim fromString As String = "ほげ;;#ほげ;#ふがふ;;;;#が;#ぴよ;;ぴよ"

        Dim escapeString As String = fromString.Replace(";;", DUMMY_TEXT)
        Dim escapeArray As String() = escapeString.Split(New String() {";#"}, StringSplitOptions.None)

        Dim toList As New List(Of String)
        For Each text As String In escapeArray
            toList.Add(text.Replace(DUMMY_TEXT, ";"))
        Next

        Return toList.ToArray()

[結果]
ほげ;#ほげ
ふがふ;;#が
ぴよ;ぴよ

何かよい知恵がありましたらお貸しください m(_ _)m

引用返信 編集キー/
■36295 / inTopicNo.2)  Re[1]: エスケープされた文字列の分割
□投稿者/ 鶏唐揚 (335回)-(2009/05/26(Tue) 23:06:09)
2009/05/26(Tue) 23:06:46 編集(投稿者)

No36290 (みきぬ さん) に返信
その文字列の出所によりますが、ユーザからの手入力であれば
入力できないような文字コードをダミーにしてやればいいかと思います

例:
「Chr(0) & Chr(1)」 など


もっと様々なパターンに対応するとなると若干遅くなりますが、
Chr(0)から+1しつつIndexOfで存在を確認し、最初に未存在が確認できたものを使う…とか。
引用返信 編集キー/
■36321 / inTopicNo.3)  Re[1]: エスケープされた文字列の分割
□投稿者/ いしだ (202回)-(2009/05/27(Wed) 13:57:43)
2009/05/27(Wed) 14:12:20 編集(投稿者)

> Regex.Split() でいけるかな? とも考えてみたのですが、うまい正規表現が見つかりません。

"(?<=[^;](;;)*);#"
こんな感じでいけないでしょうか?

[編集]
これじゃ無理でした。
もうちょい考えます。
引用返信 編集キー/
■36327 / inTopicNo.4)  Re[1]: エスケープされた文字列の分割
□投稿者/ 魔界の仮面弁士 (1089回)-(2009/05/27(Wed) 14:45:46)
No36290 (みきぬ さん) に返信
> 環境:VisualStudio 2005
> ※分類は VB.NET にしていますが、C# でも平気です。
> Dim fromString As String = "ほげ;;#ほげ;#ふがふ;;;;#が;#ぴよ;;ぴよ"
> 
> [結果]
> ほげ;#ほげ
> ふがふ;;#が
> ぴよ;ぴよ

どうかな…。


using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

class Sample {
  static void Main() {
     Array.ForEach(Split("ほげ;;#ほげ;#ふがふ;;;;#が;#ぴよ;;ぴよ"), Console.WriteLine);
  }

  static string[] Split(string fromString) {
    List<string> list = new List<string>(Regex.Split(fromString, "(?<!;);#"));
    return list.ConvertAll<string>(delegate(string s) { return s.Replace(";;", ";"); }).ToArray();
  }
}

引用返信 編集キー/
■36328 / inTopicNo.5)  Re[1]: エスケープされた文字列の分割
□投稿者/ よねKEN (334回)-(2009/05/27(Wed) 14:49:01)
2009/05/27(Wed) 14:58:00 編集(投稿者)

うんうん唸って考えてみた結果がこちら↓
いまいちですorz

For Each text As String in Regex.Replace(fromString, "([^;]);#", "$1" & ChrW(0)).Split(ChrW(0))
toList.Add(text.Replace(";;", ";"))
Next

Regex.Splitでは恐らく無理じゃないですかね。(実現可能なら私もぜひ知りたいです!)
分割するときの区切りの判断には";#"(ただし、";;#"の一部ではないこと)と指定しつつ、
実際の分割作業は";#"で行わなければならず、そういう指定方法がSplitにはなさそうなので。

私の方法の例のように、Regex.Replaceなら$1とか使って、
置換後の文字列に置換前の文字列の一部を反映できますが、
区切り文字をリプレースしてしまうために
鶏唐揚さん案を採用してChrW(0)に一度置き換えてから、
さらにString.Splitで分けてます。

結局、みきぬさんの案と考え方が同じなので改善案とは呼べませんね・・・

#<追記>
#グループ化構成体
http://msdn.microsoft.com/ja-jp/library/bs2twtah(VS.80).aspx
#ゼロ幅の負の後読みアサーション というのを使えばできるんですねorz
#</追記>

引用返信 編集キー/
■36329 / inTopicNo.6)  Re[2]: エスケープされた文字列の分割
□投稿者/ いしだ (203回)-(2009/05/27(Wed) 14:58:54)
2009/05/27(Wed) 15:01:04 編集(投稿者)

結局わかりませんでした。

ほげ;;;#ほげ

のような場合、期待するのは、

ほげ;
ほげ

だと思いますが、先に私の提示した正規表現でRegex.Splitを使用すると、

ほげ;
;
ほげ

になってしまいます。

引用返信 編集キー/
■36357 / inTopicNo.7)  Re[3]: エスケープされた文字列の分割
□投稿者/ 魔界の仮面弁士 (1095回)-(2009/05/27(Wed) 21:24:53)
No36329 (いしだ さん) に返信
> ほげ;;;#ほげ
> のような場合、期待するのは、
おぉぅ。私のコード(No36327)も、そのパターンに対応できていませんでした。

ところで、セミコロンの対応が不正な場合はどうするべきでしょうかね。
"あいう;" は例外を投げるべきか、それとも "あいう;;" として扱うべきなのか。


>>> <文字列> に含まれることがあり得ないような
>>> ダミーの文字列を考えなくてはならないところに難を感じています。
原始的に一文字ずつ処理していく方法しか思いつかず…。

public static string[] Split(string fromString)
{
    return new List<string>(Iterator(new StringBuilder(fromString))).ToArray();
}

private static IEnumerable<string> Iterator(StringBuilder fromString)
{
    int max = fromString.Length;
    // fromString.Append(';'); // セミコロン補正を行う場合はコメント解除
    StringBuilder sb = new StringBuilder();
    for (int p = 0; p < max; ++p)
    {
        if (fromString[p] == ';')
        {
            if (fromString[p + 1] == '#')
            {
                yield return sb.ToString();
                ++p;
                sb.Length = 0;
            }
            else
            {
                sb.Append(fromString[p]);
                if (fromString[p + 1] == ';') ++p;
            }
        }
        else
        {
            sb.Append(fromString[p]);
        }
    }
    yield return sb.ToString();
}

引用返信 編集キー/
■36361 / inTopicNo.8)  Re[2]: エスケープされた文字列の分割
□投稿者/ みきぬ (444回)-(2009/05/27(Wed) 22:14:49)
みなさん、回答ありがとうございます。

とりあえずの結論としては、自分が思ってた以上に一筋縄ではいかないようなので、
現状の処理に
・鶏唐揚さんの「入力できない文字列を使う」
・魔界の仮面弁士さんの「List<T>.ConvertAll()」
を取り入れたものでやりたいと思います。

ただ、あまり複雑でなければ正規表現でできるのが自分としては理想なので、
Regex.Split() でできないか、もうちょっと考えてみようと思います。
これだけに時間を割くわけにもいかないので、空いた時間でぼちぼち考えることになりますが。

# 『なんでこんなけったいな仕様なんだ!』とお叱りを受ける前に背景を説明しておきますと、
# SharePoint のカスタムリストの列の種類に「他のリストの参照」というものがありまして、
# その複数選択可能なものを選ぶと、列の値はこんな感じの文字列で保存されています。
# この値を取得して、グリッド形式でデータを表示するのが最終目標だったりします。

以下、簡単ですが個別にコメントを…。

No36295 (鶏唐揚 さん) に返信
> その文字列の出所によりますが、ユーザからの手入力であれば
> 入力できないような文字コードをダミーにしてやればいいかと思います
>
お察しの通り、そもそもの出所は手入力(1行 TextBox からの入力)です。
自分でもタブ文字を考えたのですが、コピペで貼り付けられるような気がしたので没にしていました。

> もっと様々なパターンに対応するとなると若干遅くなりますが、
> Chr(0)から+1しつつIndexOfで存在を確認し、最初に未存在が確認できたものを使う…とか。
>
「元の文字列にないパターンを探して使う」というアイデアはありませんでした。
今回はたぶんそこまでしなくても、Chr(0)とChr(1) の組み合わせくらいで大丈夫だと思います。


No36321 (いしだ さん) に返信
No36329 (いしだ さん) に返信
> ほげ;;;#ほげ
>
このパターンは考えていませんでした。
確かにこれは「ほげ;」と「ほげ」に分割されてほしいです。難しいですね…。


No36327 (魔界の仮面弁士 さん) に返信
> List<string> list = new List<string>(Regex.Split(fromString, "(?<!;);#"));
>
よねKENさんのフォローもあって、やっと正規表現の意味がわかりました。
質問に書いたパターンは大丈夫だったのですが、残念ながらいしださんのパターンではうまくいきませんでした。
あとほんの少しでうまくできそうな気がするので、時間を見つけて正規表現をこねくり回してみたいと思います。

> return list.ConvertAll<string>(delegate(string s) { return s.Replace(";;", ";"); }).ToArray();
>
ConvertAll の存在は知りませんでした。
List<T> で使えそうなメソッドを探してはいたのですが、見事に見落としていたようです…。


No36328 (よねKEN さん) に返信
> Regex.Splitでは恐らく無理じゃないですかね。(実現可能なら私もぜひ知りたいです!)
>
私も無理かなと思っていたのですが、希望が持てたのでもうちょっと探してみようと思います。

> 結局、みきぬさんの案と考え方が同じなので改善案とは呼べませんね・・・
>
いえいえ。別の方が考えても同じ考え方に辿り着くのは、なんとなく安心します(笑)
引用返信 編集キー/
■36364 / inTopicNo.9)  Re[4]: エスケープされた文字列の分割
□投稿者/ 魔界の仮面弁士 (1097回)-(2009/05/27(Wed) 22:44:25)
2009/05/27(Wed) 22:51:09 編集(投稿者)
No36357 (魔界の仮面弁士 さん) に返信
> 原始的に一文字ずつ処理していく方法しか思いつかず…。

今度はどうかな…。単純に ";#" で千切った後、必要に応じて再連結という処理方法です。


Public Class Form1
    Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
        TextBox1.Text = "ほげ;;#ほげ;#ふがふ;;;;#が;#ぴよ;;ぴよ"
    End Sub

    Public Shared Function GetFields(ByVal fromString As String) As String()
        If String.IsNullOrEmpty(fromString) Then
            Return New String() {}
        End If

        'とりあえず、";#" で分割しておく
        Dim fields() As String = Split(fromString, ";#")

        ';;# のパターンを補正
        Dim list As New List(Of String)()
        Dim max As Integer = UBound(fields)
        For index As Integer = 0 To max
            Dim s As String = fields(index)
            If (s.Length - s.TrimEnd(";"c).Length) Mod 2 = 1 Then
                If index = max Then
                    'Throw New ArgumentException("末尾に不正な[;]が存在します。", "fromString")
                    list.Add(s & ";")
                Else
                    '末尾の;が奇数個なら再連結
                    fields(index + 1) = s & ";#" & fields(index + 1)
                End If
            Else
                list.Add(s)
            End If
        Next

        '最後に、";;" を ";" に置換
        For n As Integer = 0 To list.Count - 1
            list(n) = list(n).Replace(";;", ";")
        Next
        Return list.ToArray()
    End Function

    Private Sub TextBox1_TextChanged(ByVal sender As Object, ByVal e As EventArgs) Handles TextBox1.TextChanged
        ListBox1.DataSource = GetFields(TextBox1.Text)
        ListBox1.SelectedIndex = ListBox1.Items.Count - 1
    End Sub
End Class

引用返信 編集キー/
■36365 / inTopicNo.10)  Re[3]: エスケープされた文字列の分割
□投稿者/ みきぬ (445回)-(2009/05/27(Wed) 22:49:00)
※1つ前のコメントは、 No36357 を読む前に書きました。

No36357 (魔界の仮面弁士 さん) に返信
> ところで、セミコロンの対応が不正な場合はどうするべきでしょうかね。
> "あいう;" は例外を投げるべきか、それとも "あいう;;" として扱うべきなのか。
元の文字列を作っているのは SharePoint なので、そこは気にしていませんでした。
個人的には例外になっても、例外にならずに誤処理されたとしても構わないと思っています(ぉぃ

コードを読ませていただきました。

文字列を1文字ずつ読んで、sb に追加していくのが基本。
1. 読んだ文字が ";" だったら、次の1文字をこっそり見る。
1-1. 次の1文字も含めて ";#" だったら、そこで区切りと判断して、sb を IEnumerable<string> の要素として返す。
   sb はクリアして、";#" の次から読むのを再開する。
1-2. 次の1文字も含めて ";;" だった場合は、1文字スキップする(※その結果、 ";;" は ";" になる)
2. 読んだ文字が ";" でなければ、sb に追加する。
3. 末尾に達したら、sb を IEnumerable<string> の要素として返す。

美しい…感動しました。

-----
って読んでる間に No36364 の投稿が!
すいません、読むのは明日にさせてください m(_ _)m
引用返信 編集キー/
■36370 / inTopicNo.11)  Re[5]: エスケープされた文字列の分割
□投稿者/ 魔界の仮面弁士 (1099回)-(2009/05/28(Thu) 03:18:13)
No36364 (魔界の仮面弁士 さん) に返信
> ■No36357 (魔界の仮面弁士 さん) に返信
>>原始的に一文字ずつ処理していく方法しか思いつかず…。
> 今度はどうかな…。単純に ";#" で千切った後、必要に応じて再連結という処理方法です。

さらに別案。


public static string[] GetFields(string fromString)
{
    return new List<string>(Iterator(fromString)).ToArray();
}

private static IEnumerable<string> Iterator(string fromString)
{
    if (string.IsNullOrEmpty(fromString)) yield break;
    string[] fields = Regex.Split(fromString, ";[;#]");
    fromString += ";#";
    StringBuilder sb = new StringBuilder();
    int p = 0;
    foreach (string piece in fields)
    {
        sb.Append(piece);
        p += piece.Length + 1;
        if (fromString[p++] == ';')
        {
            sb.Append(';');
        }
        else
        {
            yield return sb.ToString();
            sb.Length = 0;
        }
    }
}

引用返信 編集キー/
■36389 / inTopicNo.12)  Re[4]: エスケープされた文字列の分割
□投稿者/ よねKEN (335回)-(2009/05/28(Thu) 11:26:41)
2009/05/28(Thu) 11:28:12 編集(投稿者)
私も考えてみました。;#、;;#〜;;;;;#のようなものを順番にチェックしておき、
;;;;;#の桁数が偶数なら、区切りとしての";#"ありと判断する方法を取っています。
(末尾に";#"を番兵として加えることはしていないので、後半の処理がうまくまとまって
 おらず美しくありません。)
--

public static string[] Split(string fromString)
{
    List<string> list = new List<string>();
    StringBuilder sb = new StringBuilder(fromString.Length);
    Regex re = new Regex(";+#");    /// ;#や;;#や;;;;;;#のようなものを全部調べるため
    int startIndex = 0;
    foreach (Match m in re.Matches(fromString))
    {
        sb.Append(fromString.Substring(startIndex, m.Index - startIndex));
        startIndex = m.Index + m.Length;
        if ((m.Length % 2) == 0)
        {
            // ↑マッチの文字数が偶数ということは、
            // ;#、;;;#、;;;;;#のように区切りの";#"が含まれるということ
            sb.Append(m.Value.Substring(0, m.Length - 2));  // ";#"の2文字分は分割結果に含めない
            sb.Replace(";;", ";");
            list.Add(sb.ToString());
            sb.Length = 0;

            if (startIndex >= fromString.Length)
            {
                list.Add("");
            }
        }
        else
        {
            sb.Append(m.Value);
        }
    }
    if (startIndex < fromString.Length)
    {
        sb.Append(fromString.Substring(startIndex));
    }
    if (sb.Length > 0)
    {
        sb.Replace(";;", ";");
        list.Add(sb.ToString());
        sb.Length = 0;
    }
    return list.ToArray();
}

引用返信 編集キー/
■36393 / inTopicNo.13)  Re[5]: エスケープされた文字列の分割
□投稿者/ みきぬ (446回)-(2009/05/28(Thu) 12:09:43)
要は (奇数個の ";") + "#" で分割できればいいんだよなー、と思ってこねくり回してみました。

    Private Function Split2(ByVal fromString As String) As String()
        Dim list As List(Of String) = New List(Of String)(Regex.Split(fromString, "(?<=[^;](?:;;)*);#"))
        Return list.ConvertAll(Of String)(AddressOf decode).ToArray()
    End Function

    Private Function decode(ByVal s As String) As String
        Return s.Replace(";;", ";")
    End Function

よくよく見たら、 No36321 のいしださんの書き込みにある正規表現の改良版ですね。
これでOKかどうかは後で検証します。

引用返信 編集キー/
■36397 / inTopicNo.14)  Re[6]: エスケープされた文字列の分割
□投稿者/ いしだ (204回)-(2009/05/28(Thu) 13:36:10)
2009/05/28(Thu) 13:39:54 編集(投稿者)

> "(?<=[^;](?:;;)*);#"

おお、素晴らしい!
試してみたけど、いけてますね。
さすがです。

MSDNによると、

(?:subexpression)
キャプチャしないグループです。部分式と一致した部分文字列をキャプチャしません。

とありました。なるほど。
引用返信 編集キー/
■36409 / inTopicNo.15)  Re[6]: エスケープされた文字列の分割
□投稿者/ 魔界の仮面弁士 (1100回)-(2009/05/28(Thu) 16:08:59)
No36393 (みきぬ さん) に返信
> Dim list As List(Of String) = New List(Of String)(Regex.Split(fromString, "(?<=[^;](?:;;)*);#"))

これだけだと、
 「;#テスト」 →「」「テスト」
のパターンを処理できないので、ちょっと手を加えてみました。


Public Shared Function GetFields(ByVal fromString As String) As String()
    If String.IsNullOrEmpty(fromString) Then Return New String() {}
    Dim list As New List(Of String)(Regex.Split("*" & fromString, "(?<=[^;](?:;;)*);#"))
    list(0) = list(0).Substring(1)
    Return list.ConvertAll(Of String)(AddressOf decode).ToArray()
End Function

Private Shared Function decode(ByVal s As String) As String
    Return s.Replace(";;", ";")
End Function

引用返信 編集キー/
■36413 / inTopicNo.16)  Re[7]: エスケープされた文字列の分割
□投稿者/ みきぬ (447回)-(2009/05/28(Thu) 16:50:24)
No36409 (魔界の仮面弁士 さん) に返信
> これだけだと、
>  「;#テスト」 →「」「テスト」
> のパターンを処理できないので、

ががーん。
というわけで改良した結果、"(?<=(?:^|[^;])(?:;;)*);#" になりました。

# しかし保守できるのかな、これ

引用返信 編集キー/
■36414 / inTopicNo.17)  Re[8]: エスケープされた文字列の分割
□投稿者/ よねKEN (336回)-(2009/05/28(Thu) 16:59:45)
> というわけで改良した結果、"(?<=(?:^|[^;])(?:;;)*);#" になりました。
>
> # しかし保守できるのかな、これ

私なら・・・厳しいなorz
言葉で説明してみる。
「先頭の";#"または、";#"、";;;#"、";;;#"のように;が奇数個並んだパターンに
 マッチした場合に、そのマッチしたパターンの末尾の";#"の部分で分割します。」

#この説明だと、たぶん既にその仕様をわかっている人にしか、ぱっとわからないよねorz

引用返信 編集キー/
■36415 / inTopicNo.18)  Re[8]: エスケープされた文字列の分割
□投稿者/ 魔界の仮面弁士 (1102回)-(2009/05/28(Thu) 17:27:17)
No36413 (みきぬ さん) に返信
> というわけで改良した結果、"(?<=(?:^|[^;])(?:;;)*);#" になりました。
おぉ、素晴らしい!!

# 2008 であったなら、実質一行で処理できてしまうわけだ。

Function Foo(ByVal fromString As String) As String()
 Return Array.ConvertAll(Of String, String)(Regex.Split(fromString, "(?<=(?:^|[^;])(?:;;)*);#"), Function(s) s.Replace(";;", ";"))
End Function


そういえば、「」が渡された時には、
 Return New String() {}
 Return New String() { "" }
のどちらを返すべきでしょうかね。(後者かな…)


> # しかし保守できるのかな、これ
説明コメントを付けたら、コード本体より長くなったりして…。(汗)
引用返信 編集キー/
■36425 / inTopicNo.19)  Re[9]: エスケープされた文字列の分割
□投稿者/ 明智重蔵 (1回)-(2009/05/28(Thu) 20:22:52)
明智重蔵 さんの Web サイト
;の個数は、0以上の整数ですので、
奇数であることは、
偶数でないことと同値である
という数学が使えます。

そして、否定戻り読みで偶数でないことをチェックするのです。

Imports System.Text.RegularExpressions

Module Module1
    Sub Main()
        'Dim fromString As String = "ほげ;;#ほげ;#ふがふ;;;;#が;#ぴよ;;ぴよ"
        Dim fromString As String = ";#テスト"
        Dim strArr As String() = Regex.Split(fromString, "(?<!(;;)*;);#")
        'Dim strArr As String() = Regex.Split(fromString, "(?<=[^;](;;)*);#")

        For i As Integer = 0 To strArr.GetUpperBound(0)
            Console.WriteLine(strArr(i).Replace(";;", ";"))
        Next i
        Console.ReadLine()

    End Sub

End Module

引用返信 編集キー/
■36428 / inTopicNo.20)  Re[10]: エスケープされた文字列の分割
 
□投稿者/ いしだ (205回)-(2009/05/28(Thu) 21:29:52)
Dim fromString As String = "aa;;;#テスト"

のような場合、期待する値は「aa;」と「テスト」ですが、

> Dim strArr As String() = Regex.Split(fromString, "(?<!(;;)*;);#")

の場合に取得出来る値は「aa;;#テスト」になってしまいますね。

引用返信 編集キー/

次の20件>
トピック内ページ移動 / << 0 | 1 >>

管理者用

- Child Tree -