วิธีที่ดีที่สุดในการแยกสตริงออกเป็นเส้น


143

คุณจะแยกสตริงหลายบรรทัดเป็นเส้นได้อย่างไร

ฉันรู้แบบนี้

var result = input.Split("\n\r".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

ดูน่าเกลียดเล็กน้อยและสูญเสียบรรทัดว่าง มีวิธีแก้ปัญหาที่ดีกว่านี้ไหม?



1
ฉันชอบวิธีนี้ฉันไม่รู้วิธีที่จะทำให้ง่ายขึ้น แน่นอนว่าพารามิเตอร์ที่สองจะลบสิ่งที่ว่างเปล่าออกไป
NappingRabbit

คำตอบ:


172
  • หากดูเหมือนน่าเกลียดเพียงแค่ลบToCharArrayสายที่ไม่จำเป็น

  • หากคุณต้องการแยกโดย\nหรือ\rคุณมีสองตัวเลือก:

    • ใช้อาร์เรย์ตัวอักษร - แต่จะให้บรรทัดว่างสำหรับการสิ้นสุดบรรทัดสไตล์ Windows \r\n:

      var result = text.Split(new [] { '\r', '\n' });
    • ใช้นิพจน์ปกติตามที่ระบุโดย Bart:

      var result = Regex.Split(text, "\r\n|\r|\n");
  • หากคุณต้องการเก็บรักษาบรรทัดว่างไว้ทำไมคุณถึงบอกให้ C # เลิกใช้อย่างชัดเจน ( StringSplitOptionsพารามิเตอร์) - ใช้StringSplitOptions.Noneแทน


2
การลบ ToCharArray จะทำให้โค้ดเฉพาะแพลตฟอร์ม (NewLine สามารถเป็น '\ n')
Konstantin Spirin

1
@Will: บนปิดโอกาสที่คุณหมายถึงฉันแทนของคอนสแตนติ: ผมเชื่อว่า ( อย่างยิ่ง ) ที่แยกรหัสควรมุ่งมั่นในการทำงานบนแพลตฟอร์มทั้งหมด (คือมันยังควรอ่านไฟล์ข้อความที่ถูกเข้ารหัสบนที่แตกต่างกันแพลตฟอร์มมากกว่าแพลตฟอร์มการดำเนินการ ) ดังนั้นสำหรับการแยกวิเคราะห์Environment.NewLineไม่ต้องไปไกลเท่าที่ฉันกังวล ในความเป็นจริงของการแก้ปัญหาที่เป็นไปได้ทั้งหมดฉันชอบที่ใช้นิพจน์ทั่วไปเนื่องจากเฉพาะที่จัดการแพลตฟอร์มแหล่งที่มาทั้งหมดอย่างถูกต้อง
Konrad Rudolph

2
@Hamish ดีแค่ดูเอกสารของ enum หรือดูในคำถามเดิม StringSplitOptions.RemoveEmptyEntriesมัน
Konrad Rudolph

8
วิธีการเกี่ยวกับข้อความที่มี '\ r \ n \ r \ n' string.Split จะส่งคืนบรรทัดว่าง 4 บรรทัดอย่างไรก็ตามด้วย '\ r \ n' ควรให้ 2 มันแย่ลงถ้า '\ r \ n' และ '\ r' ผสมกันในไฟล์เดียว
ชื่อผู้ใช้

1
@SurikovPavel ใช้การแสดงออกปกติ นั่นเป็นตัวแปรที่ต้องการอย่างแน่นอนเนื่องจากทำงานได้อย่างถูกต้องกับการรวมกันของการสิ้นสุดบรรทัดใด ๆ
Konrad Rudolph

134
using (StringReader sr = new StringReader(text)) {
    string line;
    while ((line = sr.ReadLine()) != null) {
        // do something
    }
}

12
นี่เป็นวิธีที่สะอาดที่สุดในความเห็นส่วนตัวของฉัน
primo

5
มีความคิดในแง่ของประสิทธิภาพ (เทียบกับstring.SplitหรือRegex.Split)?
Uwe Keim

52

อัปเดต: ดูที่นี่สำหรับโซลูชันทางเลือก / async


วิธีนี้ใช้งานได้ดีและเร็วกว่า Regex:

input.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.None)

เป็นสิ่งสำคัญที่จะมีลำดับ"\r\n"แรกในอาร์เรย์เพื่อที่จะได้รับการแบ่งบรรทัดเดียว ข้างต้นให้ผลลัพธ์เช่นเดียวกับโซลูชัน Regex เหล่านี้:

Regex.Split(input, "\r\n|\r|\n")

Regex.Split(input, "\r?\n|\r")

ยกเว้นว่า Regex จะช้ากว่าประมาณ 10 เท่า นี่คือการทดสอบของฉัน:

Action<Action> measure = (Action func) => {
    var start = DateTime.Now;
    for (int i = 0; i < 100000; i++) {
        func();
    }
    var duration = DateTime.Now - start;
    Console.WriteLine(duration);
};

var input = "";
for (int i = 0; i < 100; i++)
{
    input += "1 \r2\r\n3\n4\n\r5 \r\n\r\n 6\r7\r 8\r\n";
}

measure(() =>
    input.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.None)
);

measure(() =>
    Regex.Split(input, "\r\n|\r|\n")
);

measure(() =>
    Regex.Split(input, "\r?\n|\r")
);

เอาท์พุท:

00: 00: 03,8527616

00: 00: 31.8017726

00: 00: 32.5557128

และนี่คือวิธีการขยาย:

public static class StringExtensionMethods
{
    public static IEnumerable<string> GetLines(this string str, bool removeEmptyLines = false)
    {
        return str.Split(new[] { "\r\n", "\r", "\n" },
            removeEmptyLines ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None);
    }
}

การใช้งาน:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines

โปรดเพิ่มรายละเอียดเพิ่มเติมเพื่อให้คำตอบของคุณมีประโยชน์มากขึ้นสำหรับผู้อ่าน
Mohit Jain

เสร็จสิ้น เพิ่มการทดสอบเพื่อเปรียบเทียบประสิทธิภาพกับโซลูชัน Regex
orad

รูปแบบที่ค่อนข้างเร็วกว่าเนื่องจากมีการย้อนรอยน้อยกว่าด้วยฟังก์ชันการทำงานเดียวกันหากใช้งานอยู่[\r\n]{1,2}
--megaMan

@OmegaMan มีพฤติกรรมที่แตกต่างกันบ้าง มันจะจับคู่\n\rหรือแบ่ง\n\nเป็นบรรทัดเดียวซึ่งไม่ถูกต้อง
orad

3
@OmegaMan Hello\n\nworld\n\nกรณีขอบเป็นอย่างไร เห็นได้ชัดว่ามีหนึ่งบรรทัดพร้อมข้อความตามด้วยบรรทัดว่างแล้วตามด้วยบรรทัดอื่นที่มีข้อความตามด้วยบรรทัดว่าง
Brandin

36

คุณสามารถใช้ Regex.Split:

string[] tokens = Regex.Split(input, @"\r?\n|\r");

แก้ไข: เพิ่ม|\rไปยังบัญชีสำหรับ (ตัวเก่า) Mac terminator


สิ่งนี้จะไม่สามารถใช้งานได้กับไฟล์ข้อความสไตล์ OS X เนื่องจากสิ่งเหล่านี้ใช้\rเป็นเพียงการสิ้นสุดบรรทัด
Konrad Rudolph

2
@ Konrad Rudolph: AFAIK มีการใช้ '\ r' กับระบบ MacOS ที่เก่ามากและแทบจะไม่เคยพบเจออีกเลย แต่ถ้า OP ต้องการบัญชี (หรือถ้าฉันเข้าใจผิด) จากนั้น regex สามารถขยายไปยังบัญชีได้อย่างง่ายดาย: \ r? \ n | \ r
Bart Kiers

@Bart: ฉันไม่คิดว่าคุณเข้าใจผิด แต่ฉันได้พบจุดสิ้นสุดที่เป็นไปได้ทั้งหมดในอาชีพของฉันในฐานะโปรแกรมเมอร์
Konrad Rudolph

@ Konrad คุณอาจพูดถูก ปลอดภัยกว่าดีกว่าขออภัยฉันเดา
46432 Bart Kiers

1
@ ΩmegaMan: นั่นจะสูญเสียบรรทัดว่างเช่น \ n \ n
Mike Rosoft

9

ถ้าคุณต้องการเก็บบรรทัดว่างไว้ให้ลบ StringSplitOptions

var result = input.Split(System.Environment.NewLine.ToCharArray());

2
NewLine สามารถเป็น '\ n' และข้อความอินพุตสามารถมี "\ n \ r"
Konstantin Spirin

4

ฉันมีนี้คำตอบอื่น ๆแต่อันนี้ขึ้นอยู่กับแจ็คคำตอบ , อย่างมีนัยสำคัญได้เร็วขึ้นอาจจะเป็นที่ต้องการเนื่องจากมันทำงานแบบไม่พร้อมแม้จะเล็กน้อยช้า

public static class StringExtensionMethods
{
    public static IEnumerable<string> GetLines(this string str, bool removeEmptyLines = false)
    {
        using (var sr = new StringReader(str))
        {
            string line;
            while ((line = sr.ReadLine()) != null)
            {
                if (removeEmptyLines && String.IsNullOrWhiteSpace(line))
                {
                    continue;
                }
                yield return line;
            }
        }
    }
}

การใช้งาน:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines

ทดสอบ:

Action<Action> measure = (Action func) =>
{
    var start = DateTime.Now;
    for (int i = 0; i < 100000; i++)
    {
        func();
    }
    var duration = DateTime.Now - start;
    Console.WriteLine(duration);
};

var input = "";
for (int i = 0; i < 100; i++)
{
    input += "1 \r2\r\n3\n4\n\r5 \r\n\r\n 6\r7\r 8\r\n";
}

measure(() =>
    input.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
);

measure(() =>
    input.GetLines()
);

measure(() =>
    input.GetLines().ToList()
);

เอาท์พุท:

00: 00: 03,9603894

00: 00: 00,0029996

00: 00: 04,8221971


ฉันสงสัยว่าเป็นเพราะคุณไม่ได้ตรวจสอบผลลัพธ์ของตัวแจงนับจริงหรือไม่และดังนั้นจึงไม่ได้รับการดำเนินการ น่าเสียดายที่ฉันขี้เกียจเกินกว่าจะตรวจสอบ
James Holwell

ใช่มันเป็นเรื่องจริง !! เมื่อคุณเพิ่ม. ToList () ให้กับการโทรทั้งสองวิธีการแก้ปัญหาของ StringReader จะช้าลงจริง ๆ ! บนเครื่องของฉันมันคือ 6.74s กับ 5.10s
JCH2k

นั่นทำให้รู้สึก ฉันยังคงชอบวิธีนี้เพราะมันทำให้ฉันได้รับสายแบบอะซิงโครนัส
orad

บางทีคุณควรลบหัวข้อ "ทางออกที่ดีกว่า" ในคำตอบอื่น ๆ ของคุณและแก้ไขข้อผิดพลาดนี้ ...
JCH2k


2

บิดเล็กน้อย แต่มีตัววนซ้ำทำหน้าที่:

public static IEnumerable<string> Lines(this string Text)
{
    int cIndex = 0;
    int nIndex;
    while ((nIndex = Text.IndexOf(Environment.NewLine, cIndex + 1)) != -1)
    {
        int sIndex = (cIndex == 0 ? 0 : cIndex + 1);
        yield return Text.Substring(sIndex, nIndex - sIndex);
        cIndex = nIndex;
    }
    yield return Text.Substring(cIndex + 1);
}

จากนั้นคุณสามารถโทร:

var result = input.Lines().ToArray();

1
    private string[] GetLines(string text)
    {

        List<string> lines = new List<string>();
        using (MemoryStream ms = new MemoryStream())
        {
            StreamWriter sw = new StreamWriter(ms);
            sw.Write(text);
            sw.Flush();

            ms.Position = 0;

            string line;

            using (StreamReader sr = new StreamReader(ms))
            {
                while ((line = sr.ReadLine()) != null)
                {
                    lines.Add(line);
                }
            }
            sw.Close();
        }



        return lines.ToArray();
    }

1

มันยากที่จะจัดการกับจุดสิ้นสุดของเส้นผสมอย่างเหมาะสม ที่เรารู้ว่าตัวละครเลิกจ้างบรรทัดสามารถ "สายฟีด" (ASCII 10 \n, \x0A, \u000A), "กลับบิน" (ASCII 13 \r, \x0D, \u000D) หรือการรวมกันของพวกเขาบาง กลับไปที่ DOS Windows ใช้ลำดับสองตัวอักษร CR-LF \u000D\u000Aดังนั้นชุดค่าผสมนี้ควรปล่อยบรรทัดเดียวเท่านั้น Unix ใช้เพียงเครื่องเดียว\u000Aและ Mac ที่อายุมากใช้\u000Dตัวอักษรตัวเดียว วิธีมาตรฐานในการปฏิบัติต่อการผสมผสานของตัวละครเหล่านี้ภายในไฟล์ข้อความเดียวมีดังนี้:

  • อักขระ CR หรือ LF ทุกตัวควรข้ามไปที่บรรทัดถัดไปยกเว้น ...
  • ... ถ้า CR ตามมาทันทีด้วย LF ( \u000D\u000A) จากนั้นทั้งสองเข้าด้วยกันข้ามเพียงหนึ่งบรรทัด
  • String.Empty เป็นอินพุตเดียวที่ส่งคืนไม่มีบรรทัด (อักขระใด ๆ ที่สร้างขึ้นอย่างน้อยหนึ่งบรรทัด)
  • บรรทัดสุดท้ายต้องถูกส่งคืนแม้ว่าจะไม่มี CR หรือ LF

กฎก่อนหน้านี้อธิบายพฤติกรรมของStringReader.ReadLineและฟังก์ชั่นที่เกี่ยวข้องและฟังก์ชั่นที่แสดงด้านล่างให้ผลลัพธ์ที่เหมือนกัน มันเป็นฟังก์ชั่นการแบ่งสายC # ที่มีประสิทธิภาพซึ่งปฏิบัติตามแนวทางเหล่านี้อย่างถูกต้องเพื่อจัดการกับลำดับหรือการรวมกันของ CR / LF ได้อย่างถูกต้อง บรรทัดที่แจกแจงไม่มีอักขระ CR / LF ใด ๆ String.Emptyบรรทัดว่างจะถูกเก็บไว้และกลับมาเป็น

/// <summary>
/// Enumerates the text lines from the string.
///   ⁃ Mixed CR-LF scenarios are handled correctly
///   ⁃ String.Empty is returned for each empty line
///   ⁃ No returned string ever contains CR or LF
/// </summary>
public static IEnumerable<String> Lines(this String s)
{
    int j = 0, c, i;
    char ch;
    if ((c = s.Length) > 0)
        do
        {
            for (i = j; (ch = s[j]) != '\r' && ch != '\n' && ++j < c;)
                ;

            yield return s.Substring(i, j - i);
        }
        while (++j < c && (ch != '\r' || s[j] != '\n' || ++j < c));
}

หมายเหตุ: หากคุณไม่สนใจค่าใช้จ่ายในการสร้างStringReaderอินสแตนซ์ในการโทรแต่ละครั้งคุณสามารถใช้รหัสC # 7ต่อไปนี้แทน ดังที่ระบุไว้ในขณะที่ตัวอย่างข้างต้นอาจมีประสิทธิภาพมากกว่าเล็กน้อยทั้งสองฟังก์ชันเหล่านี้ให้ผลลัพธ์ที่เหมือนกัน

public static IEnumerable<String> Lines(this String s)
{
    using (var tr = new StringReader(s))
        while (tr.ReadLine() is String L)
            yield return L;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.