Bash Script To Find Dollar Words Not As Fast As Was Hoping
我已经创建了一个bash脚本来查找美元单词。对于那些不知道的人来说,一个一美元的单词就是当a的值为1时,字母的值加起来是100,b的值为2,c的值为3,一直到z的值是26。
我对编程还不熟悉,所以我创建了一个非常粗糙的脚本来完成这类工作,但它的工作速度不如我预期的快。我的代码中有些东西正在减慢速度,但我不知道是什么。这是我的密码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | #!/bin/bash #370101 total words in Words.txt line=$(cat line.txt) function wordcheck { letter=({a..z}) i=0 while ["$i" -le 25 ] do occurences["$i"]=$(echo $word | grep ${letter["$i"]} -o | wc -l) ((i++)) done ((line++)) } until ["$line" -ge"370102" ] do word=$(sed -n"$line"p Words.txt) wordcheck echo"$line"> line.txt x=0 while ["$x" -le '25' ] do y=$((x+1)) charsum["$x"]=$((${occurences[x]} * $y)) ((x++)) done wordsum=0 for n in ${charsum[@]} do (( wordsum += n )) done tput el printf"Word #" printf"$(($line - 1))" if ["$wordsum" = '100' ] then echo $word >> DollarWords.txt printf" " printf"$word " printf '$$$DOLLAR WORD$$$ ' else printf" Not A Dollar Word $word " tput cuu1 fi done |
我只能推测它与while循环有关,或者与它如何不断地将EDOCX1的值(0)写入文件有关。
我之前已经创建了一个脚本,它添加数字来生成斐波那契序列,几乎是瞬间完成的。
所以我的问题是,有哪些方法可以帮助我的代码更高效地运行?如果这属于代码审查,请道歉。
任何帮助都非常感谢。
谢谢
编辑:
虽然我接受了戈丹·戴维斯的回答,但如果你想这样做,其他的回答也一样好。在试一试之前,我建议大家先看看别人的答案。而且,正如许多用户指出的那样,bash并不是一种很好的语言。再次感谢大家的建议。
鉴于:
1 2 | $ wc -l words.txt 370101 words.txt |
(即链接在此处的370101字文件)
仅在bash中,从一个循环开始,该循环一行一行地读取文件:
1 2 3 4 5 6 | c=0 while IFS= read -r word; do (( c+=1 )) done <words.txt echo"$c" # prints 370,101 |
要计算bash(同一文件)中的行数,在我的计算机上需要7.8秒。相比之下,
一旦你有了逐字的文件,你就可以一个字符一个字符地读取每个字符,并在字母表的字符串中找到该字符的索引:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | lcl=' abcdefghijklmnopqrstuvwxyz' ucl=' ABCDEFGHIJKLMNOPQRSTUVWXYZ' while IFS= read -r word; do ws=0 for (( i=0; i<${#word}; i++ )); do ch=${word:i:1} if [["$ch" == [a-z] ]]; then x="${lcl%%$ch*}" (( ws +="${#x}" )) elif [["$ch" == [A-Z] ]]; then x="${ucl%%$ch*}" (( ws +="${#x}" )) fi done if (( ws==100 )); then echo"$word" fi done <words.txt |
印刷品:
1 2 3 4 5 6 7 8 9 10 11 12 | abactinally abatements abbreviatable abettors abomasusi abreption ... zincifies zinkify zithern zoogleas zorgite |
在370101字的文件中大约需要1:55。
作为比较,考虑在python中使用相同的函数:
1 2 3 4 5 6 7 8 9 10 | import string lets={k:v for v,k in enumerate(string.lowercase, 1)} lets.update({k:v for v,k in enumerate(string.uppercase, 1)}) with open('/tmp/words.txt') as f: for word in f: word=word.strip() if sum(lets.get(c,0) for c in word)==100: print word |
在580毫秒内更容易理解和执行。
bash非常适合将不同的工具粘合在一起。在大型处理任务中不是很好。大任务使用
由于您正在寻找加快处理速度的方法,下面是用户
我提取了man/tr/sort并将结果转储到一个文件(words.txt)中,以模拟文件已经存在的原始问题(即,我想从计时中提取man/tr/sort):
1 2 | man bash csh dash ksh busybox find file sed tr gcc perl python make | tr '[:upper:][ \t]' '[:lower:] ' | sort -u > Words.txt |
这个调整的要点是用一个循环替换eval/sed子进程调用,该循环逐步遍历一个有效单词的字符。[见帖子-如何在bash中对字符串中的每个字符执行for循环?-有关更多详细信息,请参阅用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #!/bin/bash # make an Associative Array of the 26 letters and values. declare -A lval=\($(seq 26 | for i in [{a..z}] ; do read x ; echo $i=$x ; done)\) while read word do # skip words that contain a non-letter [[ !"${word}" =~ ^[a-z]+$ ]] && continue sum=0 # process ${word} one character at a time while read -n 1 char do # here string dumps a newline on the end of ${word}, so we'll # run a quick test to break out of the loop for a non-letter [["${char}" != [a-z] ]] && break sum=$(( sum + lval[${char}] )) # from the referenced SO link - see above - the solutions of interest # use process substitution and printf to pass the desired string into # the while loop; I've replaced this with the 'here' string and added # the test to break the loop when we see the the newline character. #done < <(printf $s"${word}") done <<<"${word}" (( sum == 100 )) && \ echo"${word}" done < Words.txt |
在运行在旧i5上的Linux虚拟机中运行3个不同测试的时间(前10个字符串):
- AGC的解决方案:37秒
- 上述溶液w/工艺替代:11秒
- 上面的解决方案w/here字符串:2.7秒
编辑:关于各种命令正在做什么的一些注释…
$(seq 26 | for/do/read/echo/done) 生成字符串列表"[a]=1[b]=2…[Z]=26declare -A lval=\( $(seq...done) \) :声明lval为关联数组,并加载前26个条目([a]=1[b]=2…[Z]=26)=~ 用于测试特定的模式;^ 表示模式的开始,$ 表示字符串的结束,[a-z]表示匹配a 和z 之间的任何字符,+ 表示匹配1个或多个字符。如果$word是a)仅由字母
a-z 组成,并且b)至少有一个字母,则"${word}" =~ ^[a-z]+$ 的计算结果为真。! 否定了模式测试;在这种情况下,我正在寻找任何具有非字母字符的单词[注意:有许多方法可以测试特定模式;这恰好是我选择用于此脚本的方法][[ !"${word}" ... ]] && continue :如果单词包含非字母,测试生成true 和(&& ),然后我们continue (即,我们对这个单词不感兴趣,所以跳到下一个单词;换句话说,跳到循环的下一个迭代)while read -n 1 char :一次解析输入(在本例中,${word} 作为'here'字符串传入)1个字符,将得到的字符串放入名为'char'的变量中。[["${char}" != [a-z] ]] && break :另一种/不同的模式匹配方法;这里我们测试单个字符$char变量,看它是否是字母,如果是(例如,evals为true),那么我们将break 退出当前循环;如果$char是字母(a-z),那么处理将继续执行循环中的下一个命令(本例中为sum=... )。(( sum == 100 )) && \ echo"${word}" :另一种运行测试的方法;在这种情况下,我们要测试字母的和是否为100;如果它的值为真,那么我们也要测试echo"${word}" [注:反斜杠(\ 表示继续下一行的命令]done <<<"${word}" :<<< 称为"这里"字符串;在这种情况下,它允许我将当前字符串(${word} 作为参数传递给while read -n 1 char 循环。
注意:请跳到3以获得更快的方法。
一个循环,一个(长)流方法:
1 2 3 4 5 6 7 8 9 10 11 | # make an Associative Array of the 26 letters and values. declare -A lval=\($(seq 26 | for i in [{a..z}] ; do read x; echo $i=$x ; done)\) # spew out 240,000 words from some man pages. man bash csh dash ksh busybox find file sed tr gcc perl python make | tr '[:upper:][ \t]' '[:lower:] ' | sort -u | while read x ; do ["$x" ="${x//[^a-z]/}" ] && (( 100 == $(sed 's/./lval[&]+/g' <<< $x) 0 )) && echo"$x" done | head |
输出以打印前10个字(在Intel核心上大约13秒)I3-230M):
1 2 3 4 5 6 7 8 9 10 | accumulate activates addressing allmulti analysis applying augments backslashes bashopts boundary |
它是如何工作的。
除上述方法外,其余方法与上述方法十分相似。代替了带
1 | (( 100 == $( hexdump -ve '/1"(%3i - 96) +" ' <<< $x ;) 86 )) |
这里,
1 | (102 - 96) + (111 - 96) + (111 - 96) + ( 10 - 96) + |
代码:
1 2 3 4 5 | while read x ; do ["$x" ="${x//[^a-z]/}" ] && (( 100 == $( hexdump -ve '/1"(%3i - 96) +" ' <<< $x ;) 86 )) && echo"$x" done < words.txt |
它比关联数组方法快20%。
软件工具预循环方法,使用
1 2 3 | man bash csh dash ksh busybox find file sed tr gcc perl python make | tr '[:upper:][ \t]' '[:lower:] ' | sort -u | egrep '^[a-z]+$' > words.txt |
然后将所有单词粘贴到它们各自的方程式旁边,(请参见上一个答案),将这些输入循环,然后打印美元话:
1 2 3 4 5 6 7 | paste words.txt <(hexdump -ve '/1"%3i" ' < words.txt | sed 's/ *[^12]10[^0-9] */ /g;s/^ //;s/ $//' | sed 's/ \+\|$/ + -96 + /g;s/ + $//' ) | while read a b ; do (( 100 == $b )) && echo $a ; done |
在循环之前进行处理是一个很大的改进。它需要大约一秒钟就可以打印出整个美元单词列表。
工作原理:需要的是decdump(即decimal dump)将每一个单词在一个单独的行上。因为
正如@thatotherguy在评论中指出的,这里有两个大问题。首先,从文件中读取行的方式是每行读取整个文件。也就是说,要读取运行
1 2 3 | while read word; do ... done <Words.txt |
注意,如果循环中有任何内容试图从标准输入中读取,它将从words.txt中窃取一些输入。在这种情况下,您可以通过fd_3发送文件,而不是使用
第二个问题是这个位:
1 | occurences["$i"]=$(echo $word | grep ${letter["$i"]} -o | wc -l) |
…它创建了3个子进程(
1 2 | matches="${word//[^${letter[i]}]/}" occurences[i]="${#matches}" |
它的工作原理是将所有不是$letter[i]的字符替换为",然后查看结果字符串的长度。解析完全发生在shell进程中,因此应该更快。
让我们用
注意:我不是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | awk ' # initialize an array of character-to-number values BEGIN { # split our alphabet into an array: c[1]=a c[2]=b ... c[26]=z; # NOTE: assumes input is all lower case, otherwise we could either # add array values for upper case letters or modify processing to # convert all characters to lower case ... split("abcdefghijklmnopqrstuvwxyz", c,"") # build associative array to match letters w/ numeric values: # ord[a]=1 ord[b]=2 ... ord[z]=26 for (i=1; i <= 26; i++) { ord[c[i]]=i } } # now process our file of words { # loop through words; just in case more than 1 word per line (ie, NF > 1) word=1 while ( word <= NF ) { sum=0 # split our word into an array of characters split($word, c,"") # loop through our array of characters for (i=1; i <= length($word); i++) { # if not a letter then break out of loop if ( c[i] !~ /[a-z]/ ) { sum=999 break } # add letter to our running sum sum=sum + ord[c[i]] # if we go over 100 then break if ( sum >= 101 ) break } # end of character loop if ( sum == 100 ) print $word word++ } # end of word loop }' Words.txt |
我用整个words.txt文件运行了一些测试:
我以前的bash解决方案:我们不要谈论我的机器有多慢!
dawg 的bash解决方案:3分钟32秒(比dawg 的机器慢2倍左右)在
awk 解决方案之上:3.5秒(在我的电脑以外的任何设备上都会更快)