检查一个字符串中的单词是否在另一个字符串中的最快方法是什么?

时间:2010-03-31 02:51:37

标签: ruby-on-rails ruby regex performance

我有一串话;我们称他们为bad

bad = "foo bar baz"

我可以将此字符串保留为以空格分隔的字符串或列表:

bad = bad.split(" ");

如果我有另一个字符串,就像这样:

str = "This is my first foo string"

检查bad字符串中的任何单词是否在我的比较字符串中,,如果找到该字词的最快方法是什么,最快的方法是什么?

#Find if a word is there
bad.split(" ").each do |word|
  found = str.include?(word)
end

#Remove the word
bad.split(" ").each do |word|
  str.gsub!(/#{word}/, "")
end

8 个答案:

答案 0 :(得分:9)

如果坏词列表变得很大,那么散列要快得多:

    require 'benchmark'

    bad = ('aaa'..'zzz').to_a    # 17576 words
    str= "What's the fasted way to check if any word from the bad string is within my "
    str += "comparison string, and what's the fastest way to remove said word if it's "
    str += "found" 
    str *= 10

    badex = /\b(#{bad.join('|')})\b/i

    bad_hash = {}
    bad.each{|w| bad_hash[w] = true}

    n = 10
    Benchmark.bm(10) do |x|

      x.report('regex:') {n.times do 
        str.gsub(badex,'').squeeze(' ')
      end}

      x.report('hash:') {n.times do
        str.gsub(/\b\w+\b/){|word| bad_hash[word] ? '': word}.squeeze(' ')
      end}

    end
                user     system      total        real
regex:     10.485000   0.000000  10.485000 ( 13.312500)
hash:       0.000000   0.000000   0.000000 (  0.000000)

答案 1 :(得分:3)

bad =“foo bar baz”

=> “foo bar baz”

str =“这是我的第一个foo字符串”

=> “这是我的第一个foo字符串”

(str.split('') - bad.split(''))。join('')

=> “这是我的第一个字符串”

答案 2 :(得分:1)

如果案例不匹配,所有解决方案都会遇到问题。通过添加ignore-case标志,最容易解决正则表达式解决方案:

badex = /\b(#{bad.split.join('|')})\b/i

此外,使用"String".include?(" String ")将导致字符串中的第一个和最后一个单词或目标单词具有标点符号或连字符的字符串出现边界问题。对这些情况进行测试将导致需要许多其他代码。因此,我认为正则表达式解决方案是最好的解决方案。它不是最快的,但开箱即可更灵活,如果其他算法被调整以处理大小写折叠和复合词,那么正则表达式解决方案可能会继续前进。

#!/usr/bin/ruby

require 'benchmark'

bad = 'foo bar baz comparison'
badex = /\b(#{bad.split.join('|')})\b/i
str = "What's the fasted way to check if any word from the bad string is within my comparison string, and what's the fastest way to remove said word if it's found?" * 10

n = 10_000
Benchmark.bm(20) do |x|
  x.report('regex:') do 
    n.times { str.gsub(badex,'').gsub('  ',' ') }
  end

  x.report('regex with squeeze:') do 
    n.times{ str.gsub(badex,'').squeeze(' ') }
  end

  x.report('array subtraction') do
    n.times { (str.split(' ') - bad.split(' ')).join(' ') }
  end
end

我使str变量更长一些,以使例程更加有效。

                          user     system      total        real
regex:                0.740000   0.010000   0.750000 (  0.752846)
regex with squeeze:   0.570000   0.000000   0.570000 (  0.581304)
array subtraction     1.430000   0.010000   1.440000 (  1.449578)

Doh!,我已经习惯了其他语言如何处理他们的基准测试。现在我让它工作,看起来更好!

关于OP正在尝试做什么的一点评论:黑名单删除很容易被愚弄,并且难以保持。 L33t-sp34k让悄悄话语变得微不足道。根据应用程序的不同,人们会认为这是一种游戏,可以找到通过过滤推动攻击性词语的方法。我在被要求处理这个问题时找到的最佳解决方案是创建一个生成器,该生成器可以创建单词的所有变体并将它们转储到数据库中,其中某些进程可以尽快检查,而不是实时检查。如果您正在搜索一长串冒犯性词语,那么要检查的一百万个小字符串可能需要一段时间;我相信我们可以提出一些人会发现令人反感的事情清单,但那是一个不同日子的练习。

我没有在Ruby中看到类似于Perl Regexp::Assemble的任何类似内容,但这是解决此类问题的好方法。你可以传递一个单词数组,加上大小写折叠和单词边界的选项,它会吐出一个匹配所有单词的正则表达式模式,它们的共性被认为会产生与所有单词匹配的最小模式。列表。之后的问题是找到原始字符串中的哪个单词与模式找到的匹配项匹配,因此可以删除它们。单词案例和复合词中命中的差异使得替换更有趣。

根据具体情况,我们甚至不会谈论良性或冒犯性的言论。


我为数组减法基准测试添加了一些更全面的测试,以适应在真正的代码片段中工作的方式。 if子句在答案中指定,现在反映它:

#!/usr/bin/env ruby

require 'benchmark'

bad = 'foo bar baz comparison'
badex = /\b(#{bad.split.join('|')})\b/i
str = "What's the fasted way to check if any word from the bad string is within my comparison string, and what's the fastest way to remove said word if it's found?" * 10

str_split = str.split
bad_split = bad.split

n = 10_000
Benchmark.bm(20) do |x|
  x.report('regex') do 
    n.times { str.gsub(badex,'').gsub('  ',' ') }
  end

  x.report('regex with squeeze') do 
    n.times{ str.gsub(badex,'').squeeze(' ') }
  end

  x.report('bad.any?') do
    n.times { 
      if (bad_split.any? { |bw| str.include?(bw) })
        (str_split - bad_split).join(' ')
      end
    }
  end

  x.report('array subtraction') do
    n.times { (str_split - bad_split).join(' ') }
  end

end

进行两次测试:

ruby test.rb 
                          user     system      total        real
regex                 1.000000   0.010000   1.010000 (  1.001093)
regex with squeeze    0.870000   0.000000   0.870000 (  0.873224)
bad.any?              1.760000   0.000000   1.760000 (  1.762195)
array subtraction     1.350000   0.000000   1.350000 (  1.346043)

ruby test.rb 
                          user     system      total        real
regex                 1.000000   0.010000   1.010000 (  1.004365)
regex with squeeze    0.870000   0.000000   0.870000 (  0.868525)
bad.any?              1.770000   0.000000   1.770000 (  1.775567)
array subtraction     1.360000   0.000000   1.360000 (  1.359100)

答案 3 :(得分:0)

我通常会指出在没有测量的情况下不进行优化,但这是一个摇摆不定的事情:

为了使它快速,你应该遍历每个字符串一次。你想避免一个循环计数错误* str count内部比较。所以,你可以用它构建一个大的正则表达式和gsub。

(添加foo变体以测试单词边界的工作原理)

str = "This is my first foo fooo ofoo string"

=> "This is my first foo fooo ofoo string"

badex = /\b(#{bad.split.join('|')})\b/

=> /\b(foo|bar|baz)\b/

str.gsub(badex,'').gsub('  ',' ')

=> "This is my first fooo ofoo string"

当然,由此产生的巨大的正则表达式可能与我在其他答案中隐含的嵌套迭代一样慢。唯一知道的方法就是衡量。

答案 4 :(得分:0)

bad = %w(foo bar baz)
str = "This is my first foo string"

# find the first word in the list
found = bad.find {|word| str.include?(word)}

# remove it
str[found] = ''  ;# str => "This is my first  string"

答案 5 :(得分:0)

我会对此进行基准测试:

bad = "foo bar baz".split(' ')
str = "This is my first foo string".split(' ')

# 1. What's the fasted way to check if any word from the bad string is within my comparison string
p bad.any? { |bw| str.include?(bw) }

# 2. What's the fastest way to remove said word if it's found?
p (str - bad).join(' ')

任何?一看到匹配就会快速检查。如果您可以根据概率订购坏词,则可以节省一些周期。

答案 6 :(得分:0)

这是一个会检查单词和短语的人。

 def checkContent(str)
     bad = ["foo", "bar", "this place sucks", "or whatever"]

     # may be best to map and singularize everything as well. 
     # maybe add some regex to catch those pesky, "How i make $69 dollars each second online..."
     # maybe apply some comparison stuff to check for weird characters in those pesky, "How i m4ke $69 $ollars an hour"


     bad_hash = {}
     bad_phrase_hash = {}

     bad.map(&:downcase).each do |word|
         words = word.split().map(&:downcase)
         if words.length > 1
             words.each do |inner|
                if bad_hash.key?(inner)
                    if bad_hash[inner].is_a?(Hash) && !bad_hash[inner].key?(words.length)
                         bad_hash[inner][words.length] = true
                    elsif bad_hash[inner] === 1
                        bad_hash[inner] = {1=>true,words.length => true}
                    end
                else
                    bad_hash[inner] = {words.length => true}
                end
             end
             bad_phrase_hash[word] = true
         else
             bad_hash[word] = 1
         end
     end

     string = str.split().map(&:downcase)
     string.each_with_index do |word,index|
        if bad_hash.key?(word)
            if bad_hash[word].is_a?(Hash)
                if bad_hash[word].key?(1)
                    return false
                else
                    bad_hash[word].keys.sort.each do |length|
                        value = string[index...(index + length)].join(" ")
                        if bad_phrase_hash.key?(value)
                            return false
                        end
                    end
                end
            else
                return false
            end
        end
     end
     return true
  end

答案 7 :(得分:-2)

包括哪?方法就是你所需要的。 ruby String specificacion说:

str.include?(string) - >对或错 如果str包含给定的字符串或字符,则返回true。

“你好” .INCLUDE? “lo” - >真

“你好” .INCLUDE? “ol” - >假

“你好” .INCLUDE? ?h - >真

请注意,它有O(n),你的目的是O(n ^ 2)