【问题标题】:Reading large text file very slow读取大文本文件非常慢
【发布时间】:2019-04-30 09:50:15
【问题描述】:

所以我被赋予了编写一个 vb 程序的任务,我在其中读取一个大的 .txt 文件(从 500mb 到 2GB 不等),这个文件通常以 13 位数字开头,然后在每行之后加载其他信息。 (例如“1578597500548 info info info info etc.”)我必须让用户输入一个 13 位数字,然后我的程序在每行开头搜索该数字的大文件,如果找到,则将整行写入新的 . txt文件!

我当前的程序运行良好,但我注意到我添加到列表/流阅读器部分占用了大约 90% 的处理时间。平均每次运行大约 27 秒。任何想法如何加快速度? 这是我写的。

Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    Dim wtr As IO.StreamWriter
    Dim listy As New List(Of String)
    Dim i = 0

    stpw.Reset()
    stpw.Start()

    'reading in file of large data 700mb and larger
    Using Reader As New StreamReader("G:\USER\FOLDER\tester.txt")
        While Reader.EndOfStream = False
            listy.Add(Reader.ReadLine)
        End While
    End Using

    'have a textbox which finds user query number
    Dim result = From n In listy
                 Where n.StartsWith(TextBox1.Text)
                 Select n

    'writes results found into new file
    wtr = New StreamWriter("G:\USER\searched-number.txt")
    For Each word As String In result
        wtr.WriteLine(word)
    Next
    wtr.Close()

    stpw.Stop()
    Debug.WriteLine(stpw.Elapsed.TotalMilliseconds)

    Application.Exit()
End Sub

更新我已经接受了一些建议,不要先将其放入列表中,而只是在内存中搜索,时间大约快 5 秒,仍然需要 23 秒完成并且它写出我正在搜索的数字上方的行,所以如果你能告诉我我哪里出错了。谢谢大家!

wtr = New StreamWriter("G:\Karl\searchednumber.txt")
        Using Reader As New StreamReader("G:\Karl\AC\tester.txt")
            While Reader.EndOfStream = False
                lineIn = Reader.ReadLine
                If Reader.ReadLine.StartsWith(TextBox1.Text) Then
                    wtr.WriteLine(lineIn)

                Else

                    Continue While
                End If
            End While
            wtr.Close()
        End Using

【问题讨论】:

  • 当你只需要一行时,为什么要将整个文件加载到listy?从用户那里获取输入,打开文件,然后开始一次读取一行并尝试匹配输入。如果不匹配,请将其扔掉并继续下一行。如果匹配,你已经得到了这条线,你就停止读取文件。
  • 看到一个 13 位数字可能出现不止 1 次,因此必须搜索整个文件。
  • 是的,当然,但是您现在正在将整个文件加载到内存中,然后您正在为查询创建这些字符串的枚举。您需要阅读每一行,并在匹配时将其存储。然后移动到下一个。不要存储不匹配的字符串。 LINQ 在这里不是你的朋友。
  • 如果你想使用 LINQ,然后创建一个 List(Of String) 用作可枚举源,使用 System.IO.File.ReadLines Method 将创建字符串迭代器。即Dim result = From n In System.IO.File.ReadLines("G:\USER\FOLDER\tester.txt").
  • 更新到上面的新代码

标签: vb.net visual-studio visual-studio-2017 streamreader


【解决方案1】:

在程序加载时索引文件。

创建一个Dictionary(Of ULong, Long),并在程序加载时读取该文件。对于每一行,在字典中添加一个条目,将每行前面的 13 位值显示为 ULong 键,并将文件流中的位置显示为 Long 值。

然后,当用户输入一个键时,您可以立即检查字典,找到您需要的磁盘上的确切位置并直接查找。

在程序启动时建立文件索引可能需要一些时间,但您只需一次。现在,您要么需要在每次用户想要进行搜索时搜索整个内容,要么在内存中保留数百兆字节的文本文件数据。一旦你有了索引,在字典中查找一个值然后直接查找它应该几乎是立即发生的。


我刚看到这条评论:

13 位数字可能出现不止 1 次,因此必须搜索整个文件。

基于此,索引应该是Dictionary(Of ULong, List(Of Long)),其中向条目添加值首先会创建一个列表实例(如果尚不存在),然后将新值添加到列表中。

以下是在没有测试数据或 Visual Studio 的帮助下直接输入回复窗口的基本尝试,因此可能仍包含几个错误:

Public Class MyFileIndexer
    Private initialCapacity As Integer = 1
    Private Property FilePath As String
    Private Index As Dictionary(Of ULong, List(Of Long))

    Public Sub New(filePath As String)
        Me.FilePath = filePath
        RebuildIndex()
    End Sub

    Public Sub RebuildIndex()
        Index = New Dictionary(Of ULong, List(Of Long))()

        Using sr As New StreamReader(FilePath)
            Dim Line As String = sr.ReadLine()
            Dim position As Long = 0
            While Line IsNot Nothing

                'Process this line
                If Line.Length > 13 Then
                   Dim key As ULong = ULong.Parse(Line.SubString(0, 13))
                   Dim item As List(Of Long)
                   If Not Index.TryGetValue(key, item) Then
                       item = New List(Of Long)(initialCapacity)
                       Index.Add(key, item)
                   End If

                   item.Add(position)
                End If

                'Prep for next line
                position = sr.BaseStream.Position
                Line = sr.ReadLine()
            End While
        End Using   
    End Sub

    'Expect key to be a 13-character numeric string
    Public Function Search(key As String) As List(Of String)
        'Will throw an exception if parsing fails. Be prepared for that.
        Dim realKey As ULong = ULong.Parse(key)
        Return Search(realKey)
    End Function

    Public Function Search(key As ULong) As List(Of String)
        Dim lines As List(Of Long)
        If Not Index.TryGetValue(key, lines) Then Return Nothing

        Dim result As New List(Of String)()
        Using sr As New StreamReader(FilePath)
            For Each position As Long In lines
                sr.BaseStream.Seek(position, SeekOrigin.Begin)
                result.Add(sr.ReadLine())
            Next position
        End Using
        Return Result
    End Function
End Class

'Somewhere public, when your application starts up:
Public Index As New MyFileIndexer("G:\USER\FOLDER\tester.txt")

Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    Dim lines As List(Of String) = Nothing
    Try
        lines = Index.Search(TextBox1.Text)
    Catch
        'Do something here
    End Try

    If lines IsNot Nothing Then
        Using sw As New StreamWriter($"G:\USER\{TextBox1.Text}.txt")
            For Each line As String in lines
                 sw.WriteLine(line)
            Next 
        End Using
    End If
End Sub

为了好玩,这里有一个通用版本的类,它允许您提供自己的键选择器函数来索引 任何 文件,该文件存储每行的键,我可以看到它通常对,比如说,更大的 csv 数据集。

Public Class MyFileIndexer(Of TKey)
    Private initialCapacity As Integer = 1
    Private Property FilePath As String
    Private Index As Dictionary(Of TKey, List(Of Long))
    Private GetKey As Func(Of String, TKey) 

    Public Sub New(filePath As String, Func(Of String, TKey) keySelector)
        Me.FilePath = filePath
        Me.GetKey = keySelector
        RebuildIndex()
    End Sub

    Public Sub RebuildIndex()
        Index = New Dictionary(Of TKey, List(Of Long))()

        Using sr As New StreamReader(FilePath)
            Dim Line As String = sr.ReadLine()
            Dim position As Long = 0
            While Line IsNot Nothing

               Dim key As TKey = GetKey(Line)
               Dim item As List(Of Long)
               If Not Index.TryGetValue(key, item) Then
                   item = New List(Of Long)(initialCapacity)
                   Index.Add(key, item)
               End If   
               item.Add(position)

                'Prep for next line
                position = sr.BaseStream.Position
                Line = sr.ReadLine()
            End While
        End Using   
    End Sub

    Public Function Search(key As TKey) As List(Of String)
        Dim lines As List(Of Long)
        If Not Index.TryGetValue(key, lines) Then Return Nothing

        Dim result As New List(Of String)()
        Using sr As New StreamReader(FilePath)
            For Each position As Long In lines
                sr.BaseStream.Seek(position, SeekOrigin.Begin)
                result.Add(sr.ReadLine())
            Next position
        End Using
        Return Result
    End Function
End Class

【讨论】:

  • 这听起来和我一直在寻找的完全一样!!我还没有真正使用过字典中的列表,任何机会都可以解释一下我会做更多的事情或示例吗?我记得听到有人说他们会使用索引
  • @K.Madden 许愿
  • 现在更新以更好地将功能封装到一个类中
  • 你太棒了!非常感谢你做的这些。明天早上我将尝试实施它!再次感谢你! :)
  • 嗨@JoelCoehoorn 最后测试你的代码和速度,哇!它是如此之快,最长的部分只是在构建索引时才加载应用程序,但仍然只有大约 8 秒,并且搜索是即时的。我看到的唯一问题是我正在搜索的数字会打印出它后面的数字吗?所以就像说我要搜索的数字在第 345 行,它会打印 346 上的任何内容?一直在尝试改变起始位置,但没有运气!对此感到抱歉
猜你喜欢
  • 2021-10-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-23
  • 2012-06-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多