วิธีประมวลผลไฟล์ใน PowerShell ทีละบรรทัดเป็นสตรีม


90

ฉันกำลังทำงานกับไฟล์ข้อความหลายกิกะไบต์และต้องการประมวลผลสตรีมโดยใช้ PowerShell มันเป็นเรื่องง่ายๆเพียงแค่แยกวิเคราะห์แต่ละบรรทัดและดึงข้อมูลบางส่วนออกมาจากนั้นจัดเก็บไว้ในฐานข้อมูล

น่าเสียดายที่get-content | %{ whatever($_) }ดูเหมือนว่าจะเก็บเส้นทั้งหมดไว้ที่ขั้นตอนนี้ของท่อในหน่วยความจำ นอกจากนี้ยังช้าอย่างน่าประหลาดใจใช้เวลานานมากในการอ่านทั้งหมด

ดังนั้นคำถามของฉันมีสองส่วน:

  1. ฉันจะทำให้มันประมวลผลสตรีมทีละบรรทัดและไม่เก็บข้อมูลทั้งหมดไว้ในหน่วยความจำได้อย่างไร ฉันต้องการหลีกเลี่ยงการใช้ RAM หลายกิ๊กเพื่อจุดประสงค์นี้
  2. ฉันจะทำให้มันทำงานเร็วขึ้นได้อย่างไร PowerShell วนซ้ำget-contentดูเหมือนจะช้ากว่าสคริปต์ C # 100 เท่า

ฉันหวังว่าจะมีอะไรโง่ ๆ ที่ฉันทำที่นี่เช่นไม่มี-LineBufferSizeพารามิเตอร์หรืออะไรบางอย่าง ...


9
เพื่อเร่งความเร็วget-contentให้ตั้งค่า -ReadCount เป็น 512 โปรดทราบว่า ณ จุดนี้ $ _ ใน Foreach จะเป็นอาร์เรย์ของสตริง
Keith Hill

1
อย่างไรก็ตามฉันจะไปตามคำแนะนำของ Roman ในการใช้โปรแกรมอ่าน. NET ซึ่งเร็วกว่ามาก
Keith Hill

ด้วยความอยากรู้อยากเห็นจะเกิดอะไรขึ้นถ้าฉันไม่สนใจความเร็ว แต่เป็นแค่ความทรงจำ? NET เป็นไปได้มากว่าฉันจะใช้คำแนะนำของผู้อ่าน.
scobi

7
เพื่อลดการบัฟเฟอร์ให้น้อยที่สุดให้หลีกเลี่ยงการกำหนดผลลัพธ์ของGet-Contentตัวแปรเนื่องจากจะโหลดไฟล์ทั้งหมดลงในหน่วยความจำ ตามค่าเริ่มต้นใน pipleline จะGet-Contentประมวลผลไฟล์ทีละบรรทัด ตราบเท่าที่คุณไม่ได้สะสมผลลัพธ์หรือใช้ cmdlet ที่สะสมภายใน (เช่น Sort-Object และ Group-Object) การตีหน่วยความจำก็ไม่ควรเลวร้ายเกินไป Foreach-Object (%) เป็นวิธีที่ปลอดภัยในการประมวลผลแต่ละบรรทัดทีละบรรทัด
Keith Hill

2
@dwarfsoft ที่ไม่สมเหตุสมผลเลย บล็อก -End จะทำงานเพียงครั้งเดียวหลังจากการประมวลผลทั้งหมดเสร็จสิ้น คุณจะเห็นได้ว่าหากคุณพยายามใช้get-content | % -End { }มันก็บ่นเพราะคุณไม่ได้ให้บล็อกกระบวนการ ดังนั้นจึงไม่สามารถใช้ -End โดยค่าเริ่มต้นได้ต้องใช้ -Process ตามค่าเริ่มต้น และลอง1..5 | % -process { } -end { 'q' }ดูว่า end block เกิดขึ้นเพียงครั้งเดียวปกติgc | % { $_ }จะไม่ทำงานหาก scriptblock ผิดนัดเป็น -End ...
TessellatingHeckler

คำตอบ:


93

หากคุณกำลังจะทำงานกับไฟล์ข้อความหลายกิกะไบต์อย่าใช้ PowerShell แม้ว่าคุณจะหาวิธีอ่านได้เร็วขึ้นการประมวลผลบรรทัดจำนวนมากก็จะช้าใน PowerShell อยู่ดีและคุณไม่สามารถหลีกเลี่ยงสิ่งนี้ได้ แม้แต่ลูปธรรมดา ๆ ก็มีราคาแพงเช่นกันสำหรับการทำซ้ำ 10 ล้านครั้ง (ค่อนข้างจริงในกรณีของคุณ) เรามี:

# "empty" loop: takes 10 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) {} }

# "simple" job, just output: takes 20 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) { $i } }

# "more real job": 107 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) { $i.ToString() -match '1' } }

UPDATE:หากคุณยังไม่กลัวลองใช้. NET reader:

$reader = [System.IO.File]::OpenText("my.log")
try {
    for() {
        $line = $reader.ReadLine()
        if ($line -eq $null) { break }
        # process the line
        $line
    }
}
finally {
    $reader.Close()
}

อัปเดต 2

มีความคิดเห็นเกี่ยวกับโค้ดที่ดีกว่า / สั้นกว่านี้ ไม่มีอะไรผิดปกติกับรหัสเดิมforและไม่ใช่รหัสหลอก แต่ตัวแปรที่สั้นที่สุด (สั้นที่สุด?) ของลูปการอ่านคือ

$reader = [System.IO.File]::OpenText("my.log")
while($null -ne ($line = $reader.ReadLine())) {
    $line
}

3
FYI การคอมไพล์สคริปต์ใน PowerShell V3 ช่วยปรับปรุงสถานการณ์เล็กน้อย ลูป "งานจริง" เปลี่ยนจาก 117 วินาทีใน V2 เป็น 62 วินาทีบน V3 ที่พิมพ์ที่คอนโซล เมื่อฉันใส่ลูปลงในสคริปต์และวัดการทำงานของสคริปต์บน V3 มันจะลดลงเหลือ 34 วินาที
Keith Hill

ฉันทำการทดสอบทั้งสามครั้งในสคริปต์และได้ผลลัพธ์เหล่านี้: V3 Beta: 20/27/83 วินาที; V2: 14/21/101 ดูเหมือนว่าในการทดสอบ V3 ของฉันเร็วกว่าในการทดสอบ 3 แต่มันค่อนข้างช้ากว่าในสองครั้งแรก มันเป็นเบต้าหวังว่าประสิทธิภาพจะดีขึ้นใน RTM
Roman Kuzmin

ทำไมผู้คนถึงยืนกรานที่จะหยุดพักในวงแบบนั้น ทำไมไม่ใช้ลูปที่ไม่จำเป็นต้องใช้และอ่านได้ดีขึ้นเช่นแทนที่ for loop ด้วยdo { $line = $reader.ReadLine(); $line } while ($line -neq $null)
BeowulfNode42

1
โอ๊ะที่ควรจะเป็น - ไม่เท่ากัน do .. ในขณะที่ลูปนั้นมีปัญหาว่า null ที่ท้ายไฟล์จะถูกประมวลผล (ในกรณีนี้คือเอาต์พุต) เพื่อหลีกfor ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line }
เลี่ยง

4
@ BeowulfNode42 เราสามารถทำได้สั้นกว่านี้: while($null -ne ($line = $read.ReadLine())) {$line}. แต่หัวข้อไม่ได้เกี่ยวกับเรื่องดังกล่าวจริงๆ
Roman Kuzmin

52

System.IO.File.ReadLines()เหมาะสำหรับสถานการณ์นี้ จะส่งคืนทุกบรรทัดของไฟล์ แต่ช่วยให้คุณสามารถเริ่มต้นการทำซ้ำในบรรทัดได้ทันทีซึ่งหมายความว่าไม่จำเป็นต้องเก็บเนื้อหาทั้งหมดไว้ในหน่วยความจำ

ต้องการ. NET 4.0 หรือสูงกว่า

foreach ($line in [System.IO.File]::ReadLines($filename)) {
    # do something with $line
}

http://msdn.microsoft.com/en-us/library/dd383503.aspx


6
จำเป็นต้องใช้หมายเหตุ: .NET Framework - รองรับใน: 4.5, 4 ดังนั้นสิ่งนี้อาจไม่ทำงานใน V2 หรือ V1 ในบางเครื่อง
Roman Kuzmin

สิ่งนี้ทำให้ฉัน System.IO.File ไม่มีข้อผิดพลาด แต่รหัสด้านบนของ Roman ใช้งานได้สำหรับฉัน
Kolob Canyon

นี่เป็นเพียงสิ่งที่ฉันต้องการและง่ายต่อการวางลงในสคริปต์ powershell ที่มีอยู่โดยตรง
user1751825

5

หากคุณต้องการใช้ PowerShell แบบตรงโปรดดูรหัสด้านล่าง

$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
    Write-Host $line
}

16
นั่นคือสิ่งที่ OP ต้องการกำจัดเนื่องจากGet-Contentไฟล์ขนาดใหญ่ทำงานช้ามาก
Roman Kuzmin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.