我的同事抱怨我的Perl看起来太像C了,这很自然,因为我大部分时间用C编程,而Perl只是一点点。这是我最近的努力。我对易于理解的Perl很感兴趣。我是一个Perl评论家,对于神秘的Perl几乎没有宽容。但考虑到可读性,下面的代码怎么能更多Perlish?
目标是进行流量分析,找出哪些IP地址在文件“ips”中给出的范围内。这是我的努力:
#!/usr/bin/perl -w
# Process the files named in the arguments, which will contain lists of IP addresses, and see if
# any of them are in the ranges spelled out in the local file "ip", which has contents of the
# form start-dotted-quad-ip-address,end-dotted-quad-ip_address,stuff_to_be_ignored
use English;
open(IPS,"ips") or die "Can't open 'ips' $OS_ERROR";
# Increment a dotted-quad ip address
# Ignore the fact that part1 could get erroneously large.
sub increment {
$ip = shift;
my ($part_1, $part_2, $part_3, $part_4) = split (/\./, $ip);
$part_4++;
if ( $part_4 > 255 ) {
$part_4 = 0;
($part_3++);
if ( $part_3 > 255 ) {
$part_3 = 0;
($part_2++);
if ( $part_2 > 255 ) {
$part_2 = 0;
($part_1++);
}
}
}
return ("$part_1.$part_2.$part_3.$part_4");
}
# Compare two dotted-quad ip addresses.
sub is_less_than {
$left = shift;
$right = shift;
my ($left_part_1, $left_part_2, $left_part_3, $left_part_4) = split (/\./, $left);
my ($right_part_1, $right_part_2, $right_part_3, $right_part_4) = split (/\./, $right);
if ($left_part_1 != $right_part_1 ) {
return ($left_part_1 < $right_part_1);
}
if ($left_part_2 != $right_part_2 ) {
return ($left_part_2 < $right_part_2);
}
if ($left_part_3 != $right_part_3 ) {
return ($left_part_3 < $right_part_3);
}
if ($left_part_4 != $right_part_4 ) {
return ($left_part_4 < $right_part_4);
}
return (false); # They're equal
}
my %addresses;
# Parse all the ip addresses and record them in a hash.
while (<IPS>) {
my ($ip, $end_ip, $junk) = split /,/;
while (is_less_than($ip, $end_ip) ) {
$addresses{$ip}=1;
$ip = increment($ip);
}
}
# print IP addresses in any of the found ranges
foreach (@ARGV) {
open(TRAFFIC, $_) or die "Can't open $_ $OS_ERROR";
while (<TRAFFIC> ) {
chomp;
if (defined $addresses{$_}) {
print "$_\n";
}
}
close (TRAFFIC);
}
答案 0 :(得分:24)
多年来看到C程序员编写的Perl代码,这里有一些通用的建议:
使用哈希。使用列表。使用哈希!使用LISTS!使用列表操作(map,grep,split,join),尤其适用于小循环。不要使用花式列表算法;流行,拼接,推,转移和不移动更便宜。不要使用树木;哈希更便宜。哈希很便宜,制作它们,使用它们并把它们扔出去!使用迭代器for循环,而不是3-arg。不要调用$ var1,$ var2,$ var3;改为使用列表。不要调用$ var_foo,$ var_bar,$ var_baz;请改用哈希。使用$foo ||= "default"
。如果必须输入,请不要使用$_
。
不要使用原型,这是一个陷阱!!
使用正则表达式,而不是substr()
或index()
。喜欢正则表达。使用/x
修饰符使其可读。
在需要无块条件时写statement if $foo
。几乎总有一种更好的方法来编写嵌套条件:尝试递归,尝试循环,尝试散列。
在需要时声明变量,而不是在子程序的顶部。用严格。使用警告,并解决所有问题。使用诊断。写测试。写POD。
使用CPAN。使用CPAN!使用CPAN!有人可能已经做得更好了。
运行perlcritic。使用--brutal
运行它只是为了踢。运行perltidy。想想你为什么要做所有事情。改变你的风格。
使用不用于语言和调试内存分配的时间来改进代码。
提问。慷慨地对您的代码进行风格评论。去参加Perl Mongers会议。转到perlmonks.org。去YAPC或Perl Workshop。您的Perl知识将实现跨越式发展。
答案 1 :(得分:20)
大多数编写代码为“Perlish”都会利用Perl中的内置函数。
例如,这个:
my ($part_1, $part_2, $part_3, $part_4) = split (/\./, $ip);
$part_4++;
if ( $part_4 > 255 ) {
$part_4 = 0;
($part_3++);
if ( $part_3 > 255 ) {
$part_3 = 0;
($part_2++);
if ( $part_2 > 255 ) {
$part_2 = 0;
($part_1++);
}
}
}
我会改写像:
my @parts = split (/\./, $ip);
foreach my $part(reverse @parts){
$part++;
last unless ($part > 255 && !($part = 0));
}
这可以解决上面发布的代码所做的事情但是更清洁。
你确定代码能做到你想要的吗?对我来说,如果之后的那个“移动到IP的前一部分”,那么看起来有点奇怪。 255。
答案 2 :(得分:15)
有时,Perlish最常做的事情是转向CPAN而不是编写任何代码。
以下是使用Net::CIDR::Lite和Net::IP::Match::Regexp:
的快速而肮脏的示例#!/path/to/perl
use strict;
use warnings;
use English;
use IO::File;
use Net::CIDR::Lite;
use Net::IP::Match::Regexp qw(create_iprange_regexp match_ip);
my $cidr = Net::CIDR::Lite->new();
my $ips_fh = IO::File->new();
$ips_fh->open("ips") or die "Can't open 'ips': $OS_ERROR";
while (my $line = <$ips_fh>) {
chomp $line;
my ($start, $end) = split /,/, $line;
my $range = join('-', $start, $end);
$cidr->add_range($range);
}
$ips_fh->close();
my $regexp = create_iprange_regexp($cidr->list());
foreach my $traffic_fn (@ARGV) {
my $traffic_fh = IO::File->new();
$traffic_fh->open($traffic_fn) or die "Can't open '$traffic_fh': $OS_ERROR";
while (my $ip_address = <$traffic_fh>) {
chomp $ip_address;
if (match_ip($ip_address, $regexp)) {
print $ip_address, "\n";
}
}
$traffic_fh->close();
}
免责声明:我刚刚说出来,它的测试很少,没有基准测试。省略完整性检查,错误处理和注释以保持行数减少。不过,我没有在这个空白处吝啬。
至于您的代码:在使用它们之前无需定义您的功能。
答案 3 :(得分:14)
另一个重写示例:
sub is_less_than {
my $left = shift; # I'm sure you just "forgot" to put the my() here...
my $right = shift;
my ($left_part_1, $left_part_2, $left_part_3, $left_part_4) = split (/\./, $left);
my ($right_part_1, $right_part_2, $right_part_3, $right_part_4) = split (/\./, $right);
if ($left_part_1 != $right_part_1 ) {
return ($left_part_1 < $right_part_1);
}
if ($left_part_2 != $right_part_2 ) {
return ($left_part_2 < $right_part_2);
}
if ($left_part_3 != $right_part_3 ) {
return ($left_part_3 < $right_part_3);
}
if ($left_part_4 != $right_part_4 ) {
return ($left_part_4 < $right_part_4);
}
return (false); # They're equal
}
对此:
sub is_less_than {
my @left = split(/\./, shift);
my @right = split(/\./, shift);
# one way to do it...
for(0 .. 3) {
if($left[$_] != $right[$_]) {
return $left[$_] < $right[$_];
}
}
# another way to do it - let's avoid so much indentation...
for(0 .. 3) {
return $left[$_] < $right[$_] if $left[$_] != $right[$_];
}
# yet another way to do it - classic Perl unreadable one-liner...
$left[$_] == $right[$_] or return $left[$_] < $right[$_] for 0 .. 3;
# just a note - that last one uses the short-circuit logic to condense
# the if() statement to one line, so the for() can be added on the end.
# Perl doesn't allow things like do_this() if(cond) for(0 .. 3); You
# can only postfix one conditional. This is a workaround. Always use
# 'and' or 'or' in these spots, because they have the lowest precedence.
return 0 == 1; # false is not a keyword, or a boolean value.
# though honestly, it wouldn't hurt to just return 0 or "" or undef()
}
另外,这里:
my ($ip, $end_ip, $junk) = split /,/;
$junk
可能需要@junk
来捕获所有垃圾,或您可以将其关闭 - 如果您指定了未知-sized数组到两个元素的“数组”,它将默默地丢弃所有额外的东西。所以
my($ip, $end_ip) = split /,/;
在这里:
foreach (@ARGV) {
open(TRAFFIC, $_) or die "Can't open $_ $OS_ERROR";
while (<TRAFFIC> ) {
chomp;
if (defined $addresses{$_}) {
print "$_\n";
}
}
close (TRAFFIC);
}
使用变量来存储文件句柄,而不是TRAFFIC
。此外,通常,您应该使用exists()
检查哈希元素是否存在,而不是defined()
- 它可能存在但设置为undef
(这不应该发生在您的程序中) ,但是当你的程序变得更复杂时,这是一个很好的习惯):
foreach (@ARGV) {
open(my $traffic, $_) or die "Can't open $_ $OS_ERROR";
while (<$traffic> ) {
chomp;
print "$_\n" if exists $addresses{$_};
}
# $traffic goes out of scope, and implicitly closes
}
当然,你也可以使用Perl的精彩<>
运算符,它打开@ARGV的每个元素进行读取,并作为迭代它们的文件句柄:
while(<>) {
chomp;
print "$_\n" if exists $addresses{$_};
}
如前所述,请尽量避免use
English
use English qw( -no_match_vars );
,除非match_vars
避免对那些邪恶的use strict;
进行重大的性能惩罚。而且还没有被注意到,但应该是......
始终 总是总是use warnings;
和-w
或者Larry Wall会从天而降并破坏你的代码。我看到你有-w
- 这已经足够了,因为即使在Unix之外,Perl也会解析shebang行,并会找到你的use warnings;
并use strict;
喜欢它。但是,您需要到my
。这将在您的代码中捕获许多严重错误,例如不使用false
声明变量或使用strict
作为语言关键字。
让您的代码在warnings
以及strict
下工作将导致更清晰的代码从未因您无法理解的原因而中断。您将花费数小时进行调试器调试,最后您可能最终会使用warnings
和{{1}}来确定错误是什么。 仅删除它们,如果(且仅当)您的代码已完成并且您正在发布它并且从不生成任何错误。
答案 4 :(得分:13)
虽然这样做肯定是Perl中的一种方法。
use strict;
use warnings;
my $new_ip;
{
my @parts = split ('\.', $ip);
foreach my $part(reverse @parts){
$part++;
if( $part > 255 ){
$part = 0;
next;
}else{
last;
}
}
$new_ip = join '.', reverse @parts;
}
这就是我实际实现它的方式。
use NetAddr::IP;
my $new_ip = ''.(NetAddr::IP->new($ip,0) + 1) or die;
答案 5 :(得分:6)
我不能说这个解决方案会让你的程序更加Perl-ish,但它可能会简化你的算法。
不是将IP地址视为dot-quad,base-256数字,而是需要使用nested-if结构来实现增量函数,而是将IP地址视为32位整数。将a.b.c.d形式的IP转换为整数(未经测试):
sub ip2int {
my $ip = shift;
if ($ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/) {
return ($1 << 24) + ($2 << 16) + ($3 << 8) + $4;
} else {
return undef;
}
}
现在很容易确定IP是否落在两个端点IP之间。只需进行简单的整数运算和比较。
$begin = "192.168.5.0";
$end = "192.168.10.255";
$target = "192.168.6.2";
if (ip2int($target) >= ip2int($begin) && ip2int($target) <= ip2int($end)) {
print "$target is between $begin and $end\n";
} else {
print "$target is not in range\n";
}
答案 6 :(得分:5)
告诉你的同事他们的perl看起来太像线路噪音了。请不要仅仅为了混淆而混淆你的代码 - 它是asinine的开发目标,就像那些因为不可读而给人们带来如此糟糕的声誉,当它是非常糟糕的程序员(显然,就像你的同事)编写邋code的代码时。结构良好,缩进和逻辑代码是一件好事。 C是件好事。
但是说真的 - 弄清楚如何编写perl的最佳位置是由Damian Conway撰写的O'Reilly“Perl Best Practices”。它告诉你他怎么认为你应该做的事情,他总是给出他的立场的充分理由,偶尔给出不同意的充分理由。我在某些方面不同意他,但他的推理是合理的。你和任何比康威先生更了解perl的人合作的几率非常渺茫,并且拥有一本印刷书籍(或者至少是一个Safari订阅版)可以为你的论点提供更坚实的支持。拿着Perl Cookbook的副本,因为查看解决常见问题的代码示例可以让您走上正确的轨道。我讨厌说“买书”,但这些是任何 perl开发者应阅读的非常好的书。
关于你的具体代码,你使用的是foreach,$_
,没有任何分支,转移等等。它看起来很像我的眼睛 - 它已经用perl开发了很长时间而。但需要注意的是 - 我讨厌英语模块。如果您必须使用它,请执行use English qw( -no_match_vars );
。 match_vars选项可以显着降低正则表达式解析速度,并且它提供的$PREMATCH
/ $POSTMATCH
变量通常不常用。
答案 7 :(得分:4)
只有一条建议:使用严格。其余部分几乎不相关。
答案 8 :(得分:3)
我确切地知道你的感受。我的第一语言是FORTRAN,就像一个优秀的FORTRAN程序员,我用以下所有语言写FORTRAN:)。
我有这本非常精彩的书Effective Perl Programming,我时不时地重读。特别是一章名为“Idiomatic Perl”。以下是我用来保持Perl看起来像Perl的一些东西:列表运算符,如map和grep,切片和哈希切片,报价运算符。
使我的Perl看起来像FORTRAN / C的另一件事是定期阅读模块源,特别是主人的那些。
答案 9 :(得分:2)
您可以使用Acme::Bleach或Acme::Morse
答案 10 :(得分:2)
虽然这样可行:
use strict;
use warnings;
use 5.010;
use NetAddr::IP;
my %addresses;
# Parse all the ip addresses and record them in a hash.
{
open( my $ips_file, '<', 'ips') or die;
local $_; # or my $_ on Perl 5.10 or later
while( my $line = <$ips_file> ){
my ($ip, $end_ip) = split ',', $line;
next unless $ip and $end_ip;
$ip = NetAddr::IP->new( $ip, 0 ) or die;
$end_ip = NetAddr::IP->new( $end_ip ) or die;
while( $ip <= $end_ip ){
$addresses{$ip->addr} = 1;
$ip++;
}
}
close $ips_file
}
# print IP addresses in any of the found ranges
use English;
for my $arg (@ARGV) {
open(my $traffic, '<',$arg) or die "Can't open $arg $OS_ERROR";
while( my $ip = <$traffic> ){
chomp $ip;
if( $addresses{$ip} ){
say $ip
}
}
close ($traffic);
}
如果可能的话我会使用netmasks,因为它变得更简单:
use Modern::Perl;
use NetAddr::IP;
my @addresses;
{
open( my $file, '<', 'ips') or die;
while( (my $ip = <$file>) =~ s(,.*){} ){
next unless $ip;
$ip = NetAddr::IP->new( $ip ) or die;
push @addresses, $ip
}
close $file
}
for my $filename (@ARGV) {
open( my $traffic, '<', $filename )
or die "Can't open $filename";
while( my $ip = <$traffic> ) {
chomp $ip;
next unless $ip;
$ip = NetAddr::IP->new($ip) or next; # skip line on error
my @match;
for my $cmp ( @addresses ){
if( $ip->within($cmp) ){
push @match, $cmp;
#last;
}
}
say "$ip => @match" if @match;
say "# no match for $ip" unless @match;
}
close ($traffic);
}
测试ips
文件:
192.168.0.1/24
192.168.0.0
0:0:0:0:0:0:C0A8:0/128
测试traffic
文件:
192.168.1.0
192.168.0.0
192.168.0.5
输出:
# no match for 192.168.1.0/32
192.168.0.0/32 => 192.168.0.1/24 192.168.0.0/32 0:0:0:0:0:0:C0A8:0/128
192.168.0.5/32 => 192.168.0.1/24
答案 11 :(得分:1)
而不是这样做:
if ($left_part_1 != $right_part_1 ) {
return ($left_part_1 < $right_part_1);
}
你可以这样做:
return $left_part_1 < $right_part_1 if($left_part_1 != $right_part_1);
此外,您可以使用Fatal模块,以避免检查错误。
答案 12 :(得分:1)
我用于“我的代码看起来如何”的唯一标准是阅读和理解代码的目的是多么容易(特别是不熟悉Perl的程序员),而不是它是否遵循特别的风格。
如果Perl语言功能使某些逻辑更容易理解,那么我使用它,如果不是我不使用它 - 即使它可以用更少的代码来完成它。
你的同事可能认为我的代码非常“非常有用”,但我敢打赌他们完全理解代码正在做什么,并且可以修改它以便毫无困难地修复/扩展它:
我的版本:
#******************************************************************************
# Load the allowable ranges into a hash
#******************************************************************************
my %ipRanges = loadIPAddressFile("../conf/ip.cfg");
#*****************************************************************************
# Get the IP to check on the command line
#*****************************************************************************
my ( $in_ip_address ) = @ARGV;
# Convert it to number for comparison
my $ipToCheckNum = 1 * sprintf("%03d%03d%03d%03d", split(/\./, $in_ip_address));
#*****************************************************************************
# Loop through the ranges and see if the number is in any of them
#*****************************************************************************
my $startIp;
my $endIp;
my $msg = "IP [$in_ip_address] is not in range.\n";
foreach $startIp (keys(%ipRanges))
{
$endIp = $ipRanges{$startIp};
if ( $startIp <= $ipToCheckNum and $endIp >= $ipToCheckNum )
{
$msg = "IP [$in_ip_address] is in range [$startIp] to [$endIp]\n";
}
}
print $msg;
#******************************************************************************
# Function: loadIPAddressFile()
# Author: Ron Savage
# Date: 04/10/2009
#
# Description:
# loads the allowable IP address ranges into a hash from the specified file.
# Hash key is the starting value of the range, value is the end of the range.
#******************************************************************************
sub loadIPAddressFile
{
my $ipFileHandle;
my $startIP;
my $endIP;
my $startIPnum;
my $endIPnum;
my %rangeList;
#***************************************************************************
# Get the arguments sent
#***************************************************************************
my ( $ipFile ) = @_;
if ( open($ipFileHandle, "< $ipFile") )
{
while (<$ipFileHandle>)
{
( $startIP, $endIP ) = split(/\,/, $_ );
# Convert them to numbers for comparison
$startIPnum = 1 * sprintf("%03d%03d%03d%03d", split(/\./, $startIP));
$endIPnum = 1 * sprintf("%03d%03d%03d%03d", split(/\./, $endIP));
$rangeList{$startIPnum} = $endIPnum;
}
close($ipFileHandle);
}
else
{
print "Couldn't open [$ipFile].\n";
}
return(%rangeList);
}
(注意:额外的“#”行是为了保留我的freakin'间距,在这里发布代码总是会受到影响)
答案 13 :(得分:0)
我错过了什么......以上任何一个阵列版本都可以使用吗? mod对for循环的局部变量执行。我认为Brad Gilbert的Net :: IP解决方案将是我的选择。克里斯·卢茨(Chris Lutz)以我的方式对其余部分进行了清理。
顺便说一下 - 关于可读性的一些评论让我感到好奇。是否有关于Erlang / Lisp语法可读性的[强烈]抱怨,因为只有一种方法可以在其中编写代码?
答案 14 :(得分:0)
这可能更像是C,但也更简单:
use Socket qw(inet_aton inet_ntoa);
my $ip = ("192.156.255.255");
my $ip_1 = inet_ntoa(pack("N", unpack("N", inet_aton($ip))+1));
print "$ip $ip_1\n";
更新:我在阅读问题中的所有代码之前发布了这个。这里的代码只是增加了ip地址。