如何加速Ruby脚本?还是shell脚本替代?

时间:2017-05-08 22:02:14

标签: ruby shell

我有一个Ruby脚本,它对文本文件执行以下操作:

  1. 删除非ASCII行
  2. 删除包含" ::"的行(连续两个冒号)
  3. 如果有多个":"出现在行中(它们不是直接相邻),它只保留最后一个冒号两边的字符串。
  4. 删除前导空格
  5. 删除异常控制字符
  6. 问题是,我正在处理拥有大约2000万行的文件,我的脚本说它需要大约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
    

3 个答案:

答案 0 :(得分:4)

这是你的一个大问题:

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

因此,您的程序需要打开和关闭输出文件数百万次,因为它正在为每一行执行此操作。每次打开它时,由于它是以追加模式打开的,因此系统可能需要做很多工作才能找到文件的末尾。

你应该真正改变程序,在开始时打开输出文件一次,最后只关闭它。此外,运行strace以查看您的Ruby I / O操作在幕后执行的操作;它应该缓冲写入,然后一次以大约4千字节的块发送到操作系统;它不应该为每一行发出write系统调用。

为了进一步提高性能,您应该使用Ruby分析工具来查看哪些功能花费的时间最多。

答案 1 :(得分: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 !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`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 
# => " !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`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 
# => " !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"

使用内置的gsub方法和POSIX字符集,而不是迭代字符串并测试每个字符。

正如@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 !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`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)
# => " !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`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)
# => " !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`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   # => " !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`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) # => " !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`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   # => " !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
strip_control_and_extended_characters2(str) # => " !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`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

答案 2 :(得分:0)

似乎只有可能的方法来优化这个:

  1. 并发

    如果您的计算机是基于Unix / Linux的计算机,具有多核CPU,则可以使用fork来利用多核,从而在不同进程之间划分工作。

    多线程可能无法像您期望的那样使用Ruby,因为有一个GIL(全局指令锁)可以阻止多个线程一起运行。

  2. 代码优化。

    这些包括最小化系统调用(例如File.open)和最小化任何临时对象。

    在我转到fork之前,我会从这种方法开始,主要是因为使用fork时需要额外的编码。

    第一种方法需要大量重写脚本,而第二种方法可能更容易实现。

  3. 例如,以下方法可以最大限度地减少某些系统调用(例如文件的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的回答,该回答通过简短而简洁的答案确定了代码性能的问题。

    我对他的回答进行了投票,因为我认为通过这一简单的改变你将获得巨大的业绩增长。