Powershell 排序 txt 文件中的 IP 地址

Powershell sort IP Addresses in txt file

我有一个包含如下 IP 的纯文本文件:

194.225.0.0 - 194.225.15.255
194.225.24.0 - 194.225.31.255
62.193.0.0 - 62.193.31.255
195.146.53.128 - 195.146.53.225
217.218.0.0 - 217.219.255.255
195.146.40.0 - 195.146.40.255
85.185.240.128 - 85.185.240.159
78.39.194.0 - 78.39.194.255
78.39.193.192 - 78.39.193.207

我想按 IP 地址对文件进行排序。我的意思是只有第一部分很重要。

我在谷歌上搜索并找到了一些程序,但我想知道这是否可以通过 Powershell 在没有其他应用程序的情况下实现。

我有这样的 Linux 方式,但无法在 Windows 中到达:

sort -n -t . -k 1,1 -k 2,2 -k 3,3 -k 4,4 file

更新1

@TheMadTechnician,这是我 运行 你的命令时的输出:

85.185.240.128 - 85.185.240.159
195.146.40.0 - 195.146.40.255
78.39.193.192 - 78.39.193.207
78.39.194.0 - 78.39.194.255
217.218.0.0 - 217.219.255.255
194.225.24.0 - 194.225.31.255
194.225.0.0 - 194.225.15.255
195.146.53.128 - 195.146.53.225
62.193.0.0 - 62.193.31.255

可能是这样的。

  Get-Content .\abc.txt |ForEach-Object {($_).split("-")[1]}|Sort-Object

一个简单的方法是拆分 . 上的每一行,取第一部分(范围内每个 IP 的第一个八位字节),然后将其转换为整数并对其进行排序。

Get-Content .\MyFile.txt | Sort-Object {$_.Split('.')[0] -as [int]}

scottwang provided a clever way to sort the IPs in a comment, using the Version Class which implements IComparable Interface.

这是另一种选择,显然效率较低,使用 hash table, the IPAddress Class 和表达式数组:

$ips = Get-Content ipfile.txt

$iptable = @{}
foreach($line in $ips) {
    if($ip = $line -replace ' -.+' -as [ipaddress]) {
        $iptable[$line] = $ip.GetAddressBytes()
    }
}

$expressions = foreach($i in 0..3) {
    { $iptable[$_] | Select-Object -Index $i }.GetNewClosure()
}

$ips | Sort-Object $expressions -Descending

使用 advanced function or an anonymous function:

可以在单个管道中执行相同的操作
Get-Content ipfile.txt | & {
    begin {
        $iptable = @{}
        $expressions = foreach($i in 0..3) {
            { $iptable[$_] | Select-Object -Index $i }.GetNewClosure()
        }
    }
    process {
        if ($ip = $_ -replace ' -.+' -as [ipaddress]) {
            $iptable[$_] = $ip.GetAddressBytes()
        }
    }
    end { $iptable.PSBase.Keys | Sort-Object $expressions -Descending }
}

使用 RegEx-replace 的简单解决方案:要使 IP 地址可排序,我们只需要 在左侧填充每个八位字节 ,这样它们都有 相同宽度。然后一个简单的字符串比较就会产生正确的结果。

对于PS 6+:

Get-Content IpList.txt | Sort-Object {
    $_ -replace '\d+', { $_.Value.PadLeft(3, '0') }
}

对于PS 5.x:

Get-Content IpList.txt | Sort-Object {
    [regex]::Replace( $_, '\d+', { $args.Value.PadLeft(3, '0') } )
}
  • -replace 运算符尝试在给定字符串中查找 正则表达式 模式的匹配项,并用给定值替换它们。
  • 对于 PS 5.x 我们需要不同的语法,因为 -replace 不支持 scriptblock。使用 .NET Regex.Replace 方法我们可以达到相同的效果。
  • 第一个$_表示文本文件的当前行。
  • \d+ 是匹配每个 IP 地址的每个八位字节的模式。有关详细说明,请参见 regex101.
  • 中的示例
  • {} 定义一个 scriptblock 输出替换值
    • 这里$_表示当前匹配(八位字节)。我们获取它的值并在左侧用零填充它,因此每个八位字节总共为 3 个字符(例如 2 变为 00292 变为 092)。最终 IP 可能类似于 194.225.024.000.

另一个解决方案使用 Tuple class。它稍长一些,但更简洁,因为它实际上比较的是数字而不是字符串。

Get-Content IpList.txt | Sort-Object {
    # Extract the first 4 numbers from the current line
    [int[]] $octets = [regex]::Matches( $_, '\d+' )[ 0..3 ].Value
    
    # Create and output a tuple that consists of the numbers
    [Tuple]::Create( $octets[0], $octets[1], $octets[2], $octets[3] )  
}
  • 使用 [regex]::Matches() we find all numbers of the current line. From the returned MatchCollection we take the first four elements. Then we use member access enumeration 创建每个 MatchCollection 元素的 Value 成员的字符串数组。

  • 通过简单地将字符串数组分配给具有 [int[]] 类型约束的变量(ints 的数组),PowerShell 会自动将字符串解析为整数。

  • 排序有效,因为 Tuple 实现了 IComparable 接口,Sort-Object 在可用时使用。元组按预期排序 lexicographically

  • 使用dynamic method invocation,我们可以像这样缩短[Tuple]::Create调用(最多适用于8个元素1):

     [Tuple]::Create.Invoke( [object[]] $octets )
    

    注意转换为 [object[]],否则 [Tuple]::Create 将仅使用一个参数调用,即 $octets 数组。


[1] 实际上大于 8 个元素的元组可以通过创建嵌套元组来创建(为剩余元素创建一个元组并将其存储在基本元组的最后一个元素中)。要一般地执行此操作,需要递归或反向循环,首先创建最嵌套的元组。

此答案最初作为我的评论发布在另一个

您可以将 IP 地址从 string 对象转换为 version 对象,“巧合”的格式与 IP 地址相同(4 组数字由 .)

Get-Content .\abc.txt | Sort-Object { [System.Version]($_).split("-")[1] }

works as long as the range start addresses differ in the first octet. To get it to sort by multiple octets, it doesn't look like Sort-Object will sort by successive values in an array returned by a single [ScriptBlock]; for that you'd need to pass a [ScriptBlock] for each octet. 展示了如何在不重复定义四个 almost-identical [ScriptBlock] 的情况下做到这一点。

相反,单个 [ScriptBlock] 可以将每个八位字节组合成一个 [UInt32] 并对其进行排序。

使用 [Math]::Pow() 生成可排序的值

Get-Content -Path 'IPv4AddressRanges.txt' |
    Sort-Object -Property {
        # Split each line on a hyphen surrounded by optional whitespace
        $rangeStartAddress = ($_ -split '\s*-\s*')[0]
        # Split the start address on a period and parse the resulting [String]s to [Byte]s
        [Byte[]] $octets = $rangeStartAddress -split '.', 0, 'SimpleMatch'

        #TODO: Handle $octets.Length -ne 4
        # Alternative: [Byte[]] $octets = [IPAddress]::Parse($rangeStartAddress).GetAddressBytes()

        [UInt32] $sortValue = 0
        # $sortValue = (256 ^ 3) * $octets[0] + (256 ^ 2) * $octets[1] + 256 * $octets[2] + $octets[3]
        for ($i = 0; $i -lt $octets.Length; $i++)
        {
            $octetScale = [Math]::Pow(256, $octets.Length - $i - 1)
            $sortValue += $octetScale * $octets[$i]
        }

        return $sortValue
    }

...输出...

62.193.0.0 - 62.193.31.255
78.39.193.192 - 78.39.193.207
78.39.194.0 - 78.39.194.255
85.185.240.128 - 85.185.240.159
194.225.0.0 - 194.225.15.255
194.225.24.0 - 194.225.31.255
195.146.40.0 - 195.146.40.255
195.146.53.128 - 195.146.53.225
217.218.0.0 - 217.219.255.255

为了更好的衡量,您可以将第一行更改为...

@('255.255.255.255', '0.0.0.0') + (Get-Content -Path 'IPv4AddressRanges.txt') |

...并看到它排序正确,排序值没有溢出。

使用 [BitConverter] 生成可排序的值

您可以使用 [BitConverter] class to convert the IP address bytes directly to a [UInt32]...

简化上述内容
Get-Content -Path 'IPv4AddressRanges.txt' |
    Sort-Object -Property {
        # Split each line on a hyphen surrounded by optional whitespace
        $rangeStartAddress = ($_ -split '\s*-\s*')[0]
        # Split the start address on a period and parse the resulting [String]s to [Byte]s
        [Byte[]] $octets = $rangeStartAddress -split '.', 0, 'SimpleMatch'

        #TODO: Handle $octets.Length -ne 4
        # Alternative: [Byte[]] $octets = [IPAddress]::Parse($rangeStartAddress).GetAddressBytes()

        # [IPAddress]::NetworkToHostOrder() doesn't have an overload for [UInt32]
        if ([BitConverter]::IsLittleEndian)
        {
            [Array]::Reverse($octets)
        }

        return [BitConverter]::ToUInt32($octets, 0)
    }

在 PowerShell class 中实施 [IComparable] 以定义其自己的排序

一个更复杂的解决方案是将我们的地址存储在实现 [IComparable] interface so Sort-Object can sort the addresses directly without needing to specify a [ScriptBlock]. [IPAddress] is, of course, the most natural .NET type in which to store an IP address, but it doesn't implement any sorting interfaces. Instead, we can use PowerShell classes 的类型中以实现我们自己的可排序类型...

# Implement System.IComparable[Object] instead of System.IComparable[IPAddressRange]
# because PowerShell does not allow self-referential base type specifications.
# Sort-Object seems to only use the non-generic interface, anyways.
class IPAddressRange : Object, System.IComparable, System.IComparable[Object]
{
    [IPAddress] $StartAddress
    [IPAddress] $EndAddress

    IPAddressRange([IPAddress] $startAddress, [IPAddress] $endAddress)
    {
        #TODO: Ensure $startAddress and $endAddress are non-$null
        #TODO: Ensure the AddressFamily property of both $startAddress and
        #      $endAddress is [System.Net.Sockets.AddressFamily]::InterNetwork
        #TODO: Handle $startAddress -gt $endAddress

        $this.StartAddress = $startAddress
        $this.EndAddress = $endAddress
    }

    [Int32] CompareTo([Object] $other)
    {
        if ($null -eq $other)
        {
            return 1
        }

        if ($other -isnot [IPAddressRange])
        {
            throw [System.ArgumentOutOfRangeException]::new(
                'other',
                "Comparison against type ""$($other.GetType().FullName)"" is not supported."
            )
        }
        
        $result = [IPAddressRange]::CompareAddresses($this.StartAddress, $other.StartAddress)
        if ($result -eq 0)
        {
            $result = [IPAddressRange]::CompareAddresses($this.EndAddress, $other.EndAddress)
        }

        return $result
    }

    hidden static [Int32] CompareAddresses([IPAddress] $x, [IPAddress] $y)
    {
        $xBytes = $x.GetAddressBytes()
        $yBytes = $y.GetAddressBytes()

        for ($i = 0; $i -lt 4; $i++)
        {
            $result = $xBytes[$i].CompareTo($yBytes[$i])
            if ($result -ne 0)
            {
                return $result
            }
        }

        return 0
    }
}

[IPAddressRange] 类型存储范围的起始地址和结束地址,因此它可以代表输入文件的整行。 CompareTo method 比较每个 StartAddress byte-by-byte,只有当它们相等时,它才会比较每个 EndAddress byte-by-byte。执行这个...

(
    '127.0.0.101 - 127.0.0.199',
    '127.0.0.200 - 127.0.0.200',
    '127.0.0.100 - 127.0.0.200',
    '127.0.0.100 - 127.0.0.101',
    '127.0.0.199 - 127.0.0.200',
    '127.0.0.100 - 127.0.0.199',
    '127.0.0.100 - 127.0.0.100',
    '127.0.0.101 - 127.0.0.200'
) + (Get-Content -Path 'IPv4AddressRanges.txt') |
    ForEach-Object -Process {
        $startAddress, $endAddress = [IPAddress[]] ($_ -split '\s*-\s*')

        return [IPAddressRange]::new($startAddress, $endAddress)
    } |
    Sort-Object

...按预期顺序对 127.0.0.* 范围进行排序...

StartAddress   EndAddress     
------------   ----------
62.193.0.0     62.193.31.255
78.39.193.192  78.39.193.207
78.39.194.0    78.39.194.255
85.185.240.128 85.185.240.159
127.0.0.100    127.0.0.100    
127.0.0.100    127.0.0.101
127.0.0.100    127.0.0.199
127.0.0.100    127.0.0.200
127.0.0.101    127.0.0.199
127.0.0.101    127.0.0.200
127.0.0.199    127.0.0.200
127.0.0.200    127.0.0.200
194.225.0.0    194.225.15.255
194.225.24.0   194.225.31.255
195.146.40.0   195.146.40.255
195.146.53.128 195.146.53.225
217.218.0.0    217.219.255.255

请注意,我们只为 Sort-Object 添加了对 [IPAddressRange] 个实例进行排序的功能,而不是其各个属性。这些仍然是 [IPAddress] 类型,它不提供自己的排序,所以如果我们尝试 ... | Sort-Object -Property 'EndAddress' 之类的东西,它不会产生预期的结果。

我的 was quite inefficient hence I decided to provide another alternative using a Class that implements the IComparable Interface and holds an instance of IpAddress:

class IpComparer : IComparable {
    [ipaddress] $IpAddress

    IpComparer ([string] $IpAddress) {
        $this.IPAddress = $IpAddress
    }

    [int] GetHashCode() {
        return $this.IpAddress.GetHashCode()
    }

    [string] ToString() {
        return $this.IpAddress.ToString()
    }

    hidden static [bool] Equals ([object] $LHS, [object] $RHS) {
        return $LHS.IpAddress -eq $RHS.IpAddress
    }
    [bool] Equals ([object] $Ip) {
        return [IpComparer]::Equals([IpComparer] $this, [IpComparer] $Ip)
    }

    hidden static [int] CompareTo ([IpComparer] $LHS, [IpComparer] $RHS) {
        $x = $LHS.IpAddress.GetAddressBytes()
        $y = $RHS.IpAddress.GetAddressBytes()

        for($i = 0; $i -lt 4; $i++) {
            if($ne = $x[$i].CompareTo($y[$i])) {
                return $ne
            }
        }
        return 0
    }
    [int] CompareTo ([object] $Ip) {
        if($null -eq $Ip) {
            return 1
        }
        return [IpComparer]::CompareTo([IpComparer] $this, [IpComparer] $Ip)
    }
}

现在实例可以可比

[IpComparer] '194.225.0.0' -lt '194.225.15.255'  # => True
[IpComparer] '194.225.15.255' -lt '194.225.0.0'  # => False
[IpComparer] '194.225.0.0' -gt '194.225.15.255'  # => False
[IpComparer] '194.225.15.255' -gt '194.225.0.0'  # => True

测试平等

[IpComparer] '194.225.15.25' -ge '194.225.15.25' # => True
'194.225.15.25' -le [IpComparer] '194.225.15.25' # => True

$hs = [Collections.Generic.HashSet[IpComparer]]::new()
$hs.Add('194.225.0.0') # => True
$hs.Add('194.225.0.0') # => False

([IpComparer[]]('194.225.0.0', '194.225.0.0') | Select-Object -Unique).Count # => 1
([IpComparer] '194.225.15.255').Equals('194.225.15.255') # => True

因此,可排序:

Get-Content ipfile.txt | Sort-Object { $_ -replace ' -.+' -as [IpComparer] } -Descending