2017-05-08 53 views
0

我有一個Ruby腳本,它下面的文本文件:非ASCII線 如何加快Ruby腳本?還是shell腳本替代?

  • 刪除包含行「::」(兩個冒號連續)
    1. 刪除,如果有多於一個「:」存在於行中(不直接相鄰),它只保留最後冒號兩邊的字符串。
    2. 刪除前導空格
    3. 刪除不尋常的控制字符

    的問題是,我與擁有約20萬線的文件工作,和我的腳本說,它會採取〜45分鐘運行。

    有沒有一種方法可以大幅提高速度?或者,有沒有更快的方法來處理這個shell?

    require 'ruby-progressbar' 
    
    class String 
        def strip_control_characters() 
        chars.each_with_object("") do |char, str| 
         str << char unless char.ascii_only? and (char.ord < 32 or char.ord == 127) 
        end 
        end 
    
        def strip_control_and_extended_characters() 
        chars.each_with_object("") do |char, str| 
         str << char if char.ascii_only? and char.ord.between?(32,126) 
        end 
        end 
    end 
    
    class Numeric 
        def percent_of(n) 
        self.to_f/n.to_f * 100.0 
        end 
    end 
    
    def clean(file_in,file_out) 
        if !File.exists?(file_in) 
         puts "File '#{file_in}' does not exist." 
         return 
        end 
    
        File.delete(file_out) if File.exist?(file_out) 
        `touch #{file_out}` 
    
        deleted = 0 
        count = 0 
    
        line_count = `wc -l "#{file_in}"`.strip.split(' ')[0].to_i 
        puts "File has #{line_count} lines. Cleaning..." 
    
        progressbar = ProgressBar.create(total: line_count, length: 100, format: 'Progress |%B| %a %e') 
    
    
        IO.foreach(file_in) {|x| 
         if x.ascii_only? 
          line = x.strip_control_and_extended_characters.strip 
          if line == "" 
           deleted += 1 
           next 
          end 
          if line.include?("::") 
           deleted += 1 
           next 
          end 
          split = line.split(":") 
    
          c = split.count 
          if c == 1 
           deleted += 1 
           next 
          end 
          if c > 2 
           line = split.last(2).join(":") 
          end 
    
          if line != "" 
           File.open(file_out, 'a') { |f| f.puts(line) } 
          else 
           deleted += 1 
          end 
         else 
          deleted += 1 
         end 
    
         progressbar.progress += 1 
        } 
    
        puts "Deleted #{deleted} lines." 
    end 
    
  • +0

    你試過[多線程(https://ruby-doc.org/core-2.2.0/Thread.html)?基本上2個進程在同一時間。 –

    +0

    我的意思是嘗試它,但我還沒有真正想出如何在像這樣的文件中讀取時實現多個線程。我會做一個線程池並將線讀入一個pool.process部分嗎? – Max

    +1

    如果您將5次轉換(A,B,C,D,E)分解爲多個不同的程序並使用Unix管道將其輸入連接到其輸出,則可以利用並行性。或者,它們可能是同一個程序中的幾個不同的線程,它們以相同的方式通過隊列連接。如果你的程序大部分只是在等待I/O系統調用,它可能沒有什麼區別。 –

    回答

    4

    這是你的一個大問題:

    if line != "" 
        File.open(file_out, 'a') { |f| f.puts(line) } 
    end 
    

    所以,你的程序需要打開和關閉輸出文件數百萬次,因爲它做的是爲每一個行。每次打開文件時,由於它是以追加模式打開的,因此係統可能需要做大量工作才能找到文件的結尾。

    你應該真的改變你的程序,在開始時打開一次輸出文件,並在結束時關閉它。另外,運行strace以查看您的Ruby I/O操作在幕後進行的操作;它應該緩衝寫入,然後以大約4千字節的數據塊的形式將它們發送到操作系統;它不應該爲每一行發出一個write系統調用。

    爲了進一步提高性能,您應該使用Ruby分析工具來查看哪些功能花費最多的時間。

    +1

    這些帖子中有很多很好的信息,所以謝謝大家!大衛,我標記你的答案,因爲文件調用是我的代碼中*巨大*放緩。我最初是這樣寫的,認爲它可以節省內存,因爲我在非常大的文本文件中加載,但這種(更快)的方式似乎工作正常。 – Max

    +0

    無論哪種方式都非常有效,因爲打開的文件句柄不佔用太多內存。 –

    0

    似乎有隻對可能的途徑來優化這個:

    1. 併發。

      如果您的機器是基於Unix/Linux的擁有多核CPU的機器,則可以通過使用fork來分享不同進程之間的工作,從而充分利用多核心。

      由於GIL(全局指令鎖)阻止多線程一起運行,所以多線程可能無法像您期望的那樣好用。

    2. 代碼優化。

      這些包括最小化系統調用(如File.open)和最小化任何臨時對象。

      在我轉移到fork之前,我會從這種方法開始,主要是因爲使用fork時需要額外的編碼。

      第一種方法需要大量重寫腳本,而第二種方法可能更容易實現。

    例如,下面的方法最大限度地減少了一些系統調用(如文件的openclosewrite系統調用):

    require 'ruby-progressbar' 
    
    class String 
        def strip_control_characters() 
        chars.each_with_object("") do |char, str| 
         str << char unless char.ascii_only? and (char.ord < 32 or char.ord == 127) 
        end 
        end 
    
        def strip_control_and_extended_characters() 
        chars.each_with_object("") do |char, str| 
         str << char if char.ascii_only? and char.ord.between?(32,126) 
        end 
        end 
    end 
    
    class Numeric 
        def percent_of(n) 
        self.to_f/n.to_f * 100.0 
        end 
    end 
    
    def clean(file_in,file_out) 
        if !File.exists?(file_in) 
         puts "File '#{file_in}' does not exist." 
         return 
        end 
    
        File.delete(file_out) if File.exist?(file_out) 
        `touch #{file_out}` 
    
        deleted = 0 
        count = 0 
    
        line_count = `wc -l "#{file_in}"`.strip.split(' ')[0].to_i 
        puts "File has #{line_count} lines. Cleaning..." 
    
        progressbar = ProgressBar.create(total: line_count, length: 100, format: 'Progress |%B| %a %e') 
    
        file_fd = File.open(file_out, 'a') 
        buffer = "".dup 
    
        IO.foreach(file_in) {|x| 
         if x.ascii_only? 
          line = x.strip_control_and_extended_characters.strip 
          if line == "" 
           deleted += 1 
           next 
          end 
          if line.include?("::") 
           deleted += 1 
           next 
          end 
          split = line.split(":") 
    
          c = split.count 
          if c == 1 
           deleted += 1 
           next 
          end 
          if c > 2 
           line = split.last(2).join(":") 
          end 
    
          if line != "" 
           buffer += "\r\n#{line}" 
          else 
           deleted += 1 
          end 
         else 
          deleted += 1 
         end 
    
         if buffer.length >= 2048 
          file_fd.puts(buffer) 
          buffer.clear 
         end 
         progressbar.progress += 1 
        } 
    
        file_fd.puts(buffer) 
        buffer.clear 
        file_fd.close 
        puts "Deleted #{deleted} lines." 
    end 
    

    P.S.

    我會避免猴子修補 - 這是粗魯的。


    張貼此我讀@ DavidGrayson的答案,其精確定位的問題與在更短而簡潔的回答你的代碼的表現後。

    我提出了他的答案,因爲我認爲你會從這個簡單的變化中獲得巨大的性能收益。

    1

    您可以通過更改您的字符串補充的變化對提高速度:

    class String 
        def strip_control_characters() 
        gsub(/[[:cntrl:]]+/, '') 
        end 
    
        def strip_control_and_extended_characters() 
        strip_control_characters.gsub(/[^[:ascii:]]+/, '') 
        end 
    end 
    
    str = (0..255).to_a.map { |b| b.chr }.join # => "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\e\x1C\x1D\x1E\x1F !\"\#$%&'()*+,-./:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF" 
    
    str.strip_control_characters 
    # => " !\"\#$%&'()*+,-./:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF" 
    
    str.strip_control_and_extended_characters 
    # => " !\"\#$%&'()*+,-./:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" 
    

    使用內置符合POSIX字符集,而不是遍歷字符串和測試每個角色沿gsub方法。

    作爲@Myst說,猴子補丁是粗魯的。使用refinements,或者創建一些方法,並通過在字符串中

    def strip_control_characters(str) 
        str.gsub(/[[:cntrl:]]+/, '') 
    end 
    
    def strip_control_and_extended_characters(str) 
        strip_control_characters(str).gsub(/[^[:ascii:]]+/, '') 
    end 
    
    str = (0..255).to_a.map { |b| b.chr }.join # => "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\e\x1C\x1D\x1E\x1F !\"\#$%&'()*+,-./:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF" 
    
    strip_control_characters(str) 
    # => " !\"\#$%&'()*+,-./:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF" 
    
    strip_control_and_extended_characters(str) 
    # => " !\"\#$%&'()*+,-./:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" 
    

    談完...

    `touch #{file_out}` 
    

    是一個問題了。每次運行時都會創建一個子shell,然後執行touch然後將其拆卸下來,這是一個緩慢的操作。讓Ruby做到這一點:

    === Implementation from FileUtils 
    ------------------------------------------------------------------------------ 
        touch(list, noop: nil, verbose: nil, mtime: nil, nocreate: nil) 
    
    ------------------------------------------------------------------------------ 
    
    Updates modification time (mtime) and access time (atime) of file(s) in list. 
    Files are created if they don't exist. 
    
        FileUtils.touch 'timestamp' 
        FileUtils.touch Dir.glob('*.c'); system 'make' 
    

    最後,在開發過程中學習基準代碼。花點時間思考一些方法來做點什麼,然後相互測試一下,找出最快的方法。我使用Fruity,因爲它處理的問題是Benchmark類沒有,但是做一個或另一個。通過搜索我的用戶和「基準」,您可以在這裏找到很多測試用於各種事情的測試。

    require 'fruity' 
    
    class String 
        def strip_control_characters() 
        chars.each_with_object("") do |char, str| 
         str << char unless char.ascii_only? and (char.ord < 32 or char.ord == 127) 
        end 
        end 
    
        def strip_control_and_extended_characters() 
        chars.each_with_object("") do |char, str| 
         str << char if char.ascii_only? and char.ord.between?(32,126) 
        end 
        end 
    end 
    
    def strip_control_characters2(str) 
        str.gsub(/[[:cntrl:]]+/, '') 
    end 
    
    def strip_control_and_extended_characters2(str) 
        strip_control_characters2(str).gsub(/[^[:ascii:]]+/, '') 
    end 
    
    str = (0..255).to_a.map { |b| b.chr }.join 
    
    str.strip_control_characters # => " !\"\#$%&'()*+,-./:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF" 
    strip_control_characters2(str) # => " !\"\#$%&'()*+,-./:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF" 
    
    str.strip_control_and_extended_characters # => " !\"\#$%&'()*+,-./:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" 
    strip_control_and_extended_characters2(str) # => " !\"\#$%&'()*+,-./:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" 
    
    compare do 
        scc { str.strip_control_characters } 
        scc2 { strip_control_characters2(str) } 
    end 
    
    # >> Running each test 512 times. Test will take about 1 second. 
    # >> scc2 is faster than scc by 10x ± 1.0 
    

    和:

    compare do 
        scec { str.strip_control_and_extended_characters } 
        scec2 { strip_control_and_extended_characters2(str) } 
    end 
    
    # >> Running each test 256 times. Test will take about 1 second. 
    # >> scec2 is faster than scec by 5x ± 1.0