Powershell-将datetime字符串解析为datetime对象

Powershell-将datetime字符串解析为datetime对象,powershell,date,datetime,parsing,Powershell,Date,Datetime,Parsing,我试图解析一个文件,并将字符串日期转换为en-US区域性中的对象datetime,但是 我得到错误参数OutOfRangeException 我有两个错误: a需要一个月,但看起来我不能使用'a'格式。我可以放弃使用字符串格式“Wed”、“Sun”和“Sat”的日期,以简化脚本 b现在需要更换到 有人能帮我吗?正如@Matt在评论中提到的,您的问题的第一部分是数据格式-当您使用子字符串78、25时,您依赖的是正确的确切列宽,如果您的数据看起来不正确 PS> $line = "L4

我试图解析一个文件,并将字符串日期转换为en-US区域性中的对象datetime,但是 我得到错误参数OutOfRangeException

我有两个错误:

a需要一个月,但看起来我不能使用'a'格式。我可以放弃使用字符串格式“Wed”、“Sun”和“Sat”的日期,以简化脚本 b现在需要更换到
有人能帮我吗?

正如@Matt在评论中提到的,您的问题的第一部分是数据格式-当您使用子字符串78、25时,您依赖的是正确的确切列宽,如果您的数据看起来不正确

PS> $line = "L40065L8    IEPort1     DRP_TAPE_DRPLTO    _DRP_GLB_SECOND_COPY_TAPE_WEEK    Wed Mar 31 10:13:07 2021 "
PS> $line.Substring(78)
ed Mar 31 10:13:07 2021
给ed 2021年3月31日10:13:07,而不是你可能期望的2021年3月31日10:13:07

如果可以,最好将数据格式更改为csv或json,以便更轻松地提取字段,但如果不能这样做,则可以尝试动态计算列宽-例如:

$columns = [regex]::Matches($lines[1], "-+").Index;
# 0
# 12
# 24
# 43
# 77
这基本上可以找到每个---标题下划线的起始位置,然后可以执行以下操作:

$objects = $lines | % {
    return [PSCustomObject] @{
        BARCODE  = $_.Substring($columns[0], $columns[1] - $columns[0]).Trim()
        LOCATION = $_.Substring($columns[1], $columns[2] - $columns[1]).Trim()
        LIBRARY = $_.Substring($columns[2], $columns[3] - $columns[2]).Trim()
        STORAGEPOLICY = $_.Substring($columns[3], $columns[4] - $columns[3]).Trim()
        RETAINUNTIL = [datetime]::ParseExact(
            $_.Substring($columns[4]).Trim(),
            "a dd hh:mm:ss yyyy",
            [Globalization.CultureInfo]::CreateSpecificCulture("en-US")
        )
    }
}
除了现在,我们得到了这个错误:

Exception calling "ParseExact" with "3" argument(s): "String 'Wed Mar 31 10:13:07 2021' was not recognized as a valid DateTime."
我们可以通过以下方式解决:

[datetime]::ParseExact(
   "Wed Mar 31 10:13:07 2021",
   "ddd MMM dd HH:mm:ss yyyy",
   [Globalization.CultureInfo]::CreateSpecificCulture("en-US")
)
# 31 March 2021 10:13:07
但你也有这样的日期格式:

2022年3月6日星期日22:34:39

当日部分为一位数时,两个空格

因此,我们需要使用ParseExact重载来允许两种格式:

[datetime]::ParseExact(
   "Sun Mar  6 22:34:39 2022",
   [string[]] @( "ddd MMM dd HH:mm:ss yyyy", "ddd MMM  d HH:mm:ss yyyy"),
   [Globalization.CultureInfo]::CreateSpecificCulture("en-US"),
   "None"  
)
然后我们现在需要考虑文本字符串,因此最终代码为:

$lines = [System.IO.File]::ReadAllLines("c:\temp\qmedia.txt")

$columns = [regex]::Matches($lines[1], "-+").Index;

$lines = $lines | Select-Object -Skip 2
$objects = $lines | % {
    return [PSCustomObject] @{
        BARCODE  = $_.Substring($columns[0], $columns[1] - $columns[0]).Trim()
        LOCATION = $_.Substring($columns[1], $columns[2] - $columns[1]).Trim()
        LIBRARY = $_.Substring($columns[2], $columns[3] - $columns[2]).Trim()
        STORAGEPOLICY = $_.Substring($columns[3], $columns[4] - $columns[3]).Trim()
        RETAINUNTIL = if( $_.Substring($columns[4]).Trim() -eq "now" ) {
            " " } else {
            [datetime]::ParseExact(
                $_.Substring($columns[4]).Trim(),
                [string[]] @( "ddd MMM dd HH:mm:ss yyyy", "ddd MMM  d HH:mm:ss yyyy"),
                [Globalization.CultureInfo]::CreateSpecificCulture("en-US"),
                "None"
            )
        }
    }
}

$objects | ft

#BARCODE  LOCATION LIBRARY         STORAGEPOLICY                    RETAINUNTIL
#-------  -------- -------         -------------                    -----------
#L40065L8 IEPort1  DRP_TAPE_DRPLTO _DRP_GLB_SECOND_COPY_TAPE_WEEK   31/03/2021 10:13:07
#L40063L8 slot 1   DRP_TAPE_DRPLTO _DRP_GLB_SECOND_COPY_TAPE_MONTH  06/03/2022 22:34:39
#L40072L8 slot 5   DRP_TAPE_DRPLTO _DRP_GLB_SECOND_COPY_TAPE_ANNUAL
#L40071L8 slot 6   DRP_TAPE_DRPLTO
#L40070L8 slot 7   DRP_TAPE_DRPLTO
#L40064L8 slot 8   DRP_TAPE_DRPLTO _DRP_GLB_SECOND_COPY_TAPE_MONTH  19/03/2022 11:10:37
更新

受此启发,为您的文件格式提供一个通用解析器可能会很有用-这将返回一组pscustomobjects,其属性与文件头匹配:

function ConvertFrom-MyFormat
{

    param
    (
        [Parameter(Mandatory=$true)]
        [string[]] $Lines
    )

    # find the positions of the underscores so we can access each one's index and length
    $matches = [regex]::Matches($Lines[1], "-+");

    # extract the header names from the first line using the 
    # positions of the underscores in the second line as a cutting guide
    $headers = $matches | foreach-object {
        $Lines[0].Substring($_.Index, $_.Length);
    }

    # process the data lines and return a custom objects for each one.
    # (the property names will match the headers)
    $Lines | select-object -Skip 2 | foreach-object {
        $line = $_;
        $values = [ordered] @{};
        0..($matches.Count-2) | foreach-object {
            $values.Add($headers[$_], $line.Substring($matches[$_].Index, $matches[$_+1].Index - $matches[$_].Index));
        }
        $values.Add($headers[-1], $line.Substring($matches[-1].Index));
        new-object PSCustomObject -Property $values;
    }

}
然后,您的主代码就变成了一个清理和重构此函数结果的例子:

$lines = [System.IO.File]::ReadAllLines("c:\temp\qmedia.txt")

$objects = ConvertFrom-MyFormat -Lines $lines | foreach-object {
    return new-object PSCustomObject -Property ([ordered] @{
        BARCODE = $_.BARCODE.Trim()
        LOCATION = $_.LOCATION.Trim()
        LIBRARY = $_.LIBRARY.Trim()
        STORAGEPOLICY = $_.STORAGEPOLICY.Trim()
        RETAINUNTIL = if( $_."RETAIN UNTILL DATE".Trim() -eq "now" ) {
            " " } else {
            [datetime]::ParseExact(
                $_."RETAIN UNTILL DATE".Trim(),
                [string[]] @( "ddd MMM dd HH:mm:ss yyyy", "ddd MMM  d HH:mm:ss yyyy"),
                [Globalization.CultureInfo]::CreateSpecificCulture("en-US"),
                "None"
            )
        }
    })
}

$objects | ft;

正如@Matt在评论中提到的,问题的第一部分是数据格式-当您使用Substring78、25时,您依赖的是正确的确切列宽,而在您的数据看来是不正确的

PS> $line = "L40065L8    IEPort1     DRP_TAPE_DRPLTO    _DRP_GLB_SECOND_COPY_TAPE_WEEK    Wed Mar 31 10:13:07 2021 "
PS> $line.Substring(78)
ed Mar 31 10:13:07 2021
给ed 2021年3月31日10:13:07,而不是你可能期望的2021年3月31日10:13:07

如果可以,最好将数据格式更改为csv或json,以便更轻松地提取字段,但如果不能这样做,则可以尝试动态计算列宽-例如:

$columns = [regex]::Matches($lines[1], "-+").Index;
# 0
# 12
# 24
# 43
# 77
这基本上可以找到每个---标题下划线的起始位置,然后可以执行以下操作:

$objects = $lines | % {
    return [PSCustomObject] @{
        BARCODE  = $_.Substring($columns[0], $columns[1] - $columns[0]).Trim()
        LOCATION = $_.Substring($columns[1], $columns[2] - $columns[1]).Trim()
        LIBRARY = $_.Substring($columns[2], $columns[3] - $columns[2]).Trim()
        STORAGEPOLICY = $_.Substring($columns[3], $columns[4] - $columns[3]).Trim()
        RETAINUNTIL = [datetime]::ParseExact(
            $_.Substring($columns[4]).Trim(),
            "a dd hh:mm:ss yyyy",
            [Globalization.CultureInfo]::CreateSpecificCulture("en-US")
        )
    }
}
除了现在,我们得到了这个错误:

Exception calling "ParseExact" with "3" argument(s): "String 'Wed Mar 31 10:13:07 2021' was not recognized as a valid DateTime."
我们可以通过以下方式解决:

[datetime]::ParseExact(
   "Wed Mar 31 10:13:07 2021",
   "ddd MMM dd HH:mm:ss yyyy",
   [Globalization.CultureInfo]::CreateSpecificCulture("en-US")
)
# 31 March 2021 10:13:07
但你也有这样的日期格式:

2022年3月6日星期日22:34:39

当日部分为一位数时,两个空格

因此,我们需要使用ParseExact重载来允许两种格式:

[datetime]::ParseExact(
   "Sun Mar  6 22:34:39 2022",
   [string[]] @( "ddd MMM dd HH:mm:ss yyyy", "ddd MMM  d HH:mm:ss yyyy"),
   [Globalization.CultureInfo]::CreateSpecificCulture("en-US"),
   "None"  
)
然后我们现在需要考虑文本字符串,因此最终代码为:

$lines = [System.IO.File]::ReadAllLines("c:\temp\qmedia.txt")

$columns = [regex]::Matches($lines[1], "-+").Index;

$lines = $lines | Select-Object -Skip 2
$objects = $lines | % {
    return [PSCustomObject] @{
        BARCODE  = $_.Substring($columns[0], $columns[1] - $columns[0]).Trim()
        LOCATION = $_.Substring($columns[1], $columns[2] - $columns[1]).Trim()
        LIBRARY = $_.Substring($columns[2], $columns[3] - $columns[2]).Trim()
        STORAGEPOLICY = $_.Substring($columns[3], $columns[4] - $columns[3]).Trim()
        RETAINUNTIL = if( $_.Substring($columns[4]).Trim() -eq "now" ) {
            " " } else {
            [datetime]::ParseExact(
                $_.Substring($columns[4]).Trim(),
                [string[]] @( "ddd MMM dd HH:mm:ss yyyy", "ddd MMM  d HH:mm:ss yyyy"),
                [Globalization.CultureInfo]::CreateSpecificCulture("en-US"),
                "None"
            )
        }
    }
}

$objects | ft

#BARCODE  LOCATION LIBRARY         STORAGEPOLICY                    RETAINUNTIL
#-------  -------- -------         -------------                    -----------
#L40065L8 IEPort1  DRP_TAPE_DRPLTO _DRP_GLB_SECOND_COPY_TAPE_WEEK   31/03/2021 10:13:07
#L40063L8 slot 1   DRP_TAPE_DRPLTO _DRP_GLB_SECOND_COPY_TAPE_MONTH  06/03/2022 22:34:39
#L40072L8 slot 5   DRP_TAPE_DRPLTO _DRP_GLB_SECOND_COPY_TAPE_ANNUAL
#L40071L8 slot 6   DRP_TAPE_DRPLTO
#L40070L8 slot 7   DRP_TAPE_DRPLTO
#L40064L8 slot 8   DRP_TAPE_DRPLTO _DRP_GLB_SECOND_COPY_TAPE_MONTH  19/03/2022 11:10:37
更新

受此启发,为您的文件格式提供一个通用解析器可能会很有用-这将返回一组pscustomobjects,其属性与文件头匹配:

function ConvertFrom-MyFormat
{

    param
    (
        [Parameter(Mandatory=$true)]
        [string[]] $Lines
    )

    # find the positions of the underscores so we can access each one's index and length
    $matches = [regex]::Matches($Lines[1], "-+");

    # extract the header names from the first line using the 
    # positions of the underscores in the second line as a cutting guide
    $headers = $matches | foreach-object {
        $Lines[0].Substring($_.Index, $_.Length);
    }

    # process the data lines and return a custom objects for each one.
    # (the property names will match the headers)
    $Lines | select-object -Skip 2 | foreach-object {
        $line = $_;
        $values = [ordered] @{};
        0..($matches.Count-2) | foreach-object {
            $values.Add($headers[$_], $line.Substring($matches[$_].Index, $matches[$_+1].Index - $matches[$_].Index));
        }
        $values.Add($headers[-1], $line.Substring($matches[-1].Index));
        new-object PSCustomObject -Property $values;
    }

}
然后,您的主代码就变成了一个清理和重构此函数结果的例子:

$lines = [System.IO.File]::ReadAllLines("c:\temp\qmedia.txt")

$objects = ConvertFrom-MyFormat -Lines $lines | foreach-object {
    return new-object PSCustomObject -Property ([ordered] @{
        BARCODE = $_.BARCODE.Trim()
        LOCATION = $_.LOCATION.Trim()
        LIBRARY = $_.LIBRARY.Trim()
        STORAGEPOLICY = $_.STORAGEPOLICY.Trim()
        RETAINUNTIL = if( $_."RETAIN UNTILL DATE".Trim() -eq "now" ) {
            " " } else {
            [datetime]::ParseExact(
                $_."RETAIN UNTILL DATE".Trim(),
                [string[]] @( "ddd MMM dd HH:mm:ss yyyy", "ddd MMM  d HH:mm:ss yyyy"),
                [Globalization.CultureInfo]::CreateSpecificCulture("en-US"),
                "None"
            )
        }
    })
}

$objects | ft;
提供良好的解释和有效的解决方案

让我用一种方法来补充它:

一般解析固定列宽的输入文件 假设可以从第2行的分隔符行可靠地推断列宽,这样相邻列分隔符之间的每个子字符串(如-)表示一列。 注意:该代码需要PowerShell Core v6.2.1+,但也可以在Windows PowerShell中使用

$sepChar = '-' # The char. used on the separator line to indicate column spans.
Get-Content c:\temp\qmedia.txt | ForEach-Object {
  $line = $_
  switch ($_.ReadCount) {
    1 { 
      # Header line: save for later analysis
      $headerLine = $line
      break
    } 
    2 { 
      # Separator line: it is the only reliable indicator of column width.
      # Construct a regex that captures the column values.
      # With the sample input's separator line, the resulting regex is:
      #     (.{12})(.{12})(.{19})(.{34})(.{25})
      # Note: Syntax requires PowerShell (Core) v6.2.1+
      $reCaptureColumns = 
        $line -replace ('{0}+[^{0}]+' -f [regex]::Escape($sepChar)), 
                       { "(.{$($_.Value.Length)})" }
      # Break the header line into column names.
      if ($headerLine -notmatch $reCaptureColumns) { Throw "Unexpected header line format: $headerLine" }
      # Save the array of column names.
      $columnNames = $Matches[1..($Matches.Count - 1)].TrimEnd()  
      break
    }
    default {
      # Data line:
      if ($line -notmatch $reCaptureColumns) { Throw "Unexpected line format: $line" }
      # Construct an ordered hashtable from the column values.
      $oht = [ordered] @{ }
      foreach ($ndx in 1..$columnNames.Count) {
        $oht[$columnNames[$ndx-1]] = $Matches[$ndx].TrimEnd()
      }
      [pscustomobject] $oht # Convert to [pscustomobject] and output.
    }
  }
}
上面输出了[pscustomobject]实例流,允许进行健壮、方便的进一步处理,如mclayton的回答中所示的所需日期解析。当然,您可以将此处理直接集成到上面的代码中,但是我想单独展示固定宽度解析解决方案。

提供了很好的解释和有效的解决方案

让我用一种方法来补充它:

一般解析固定列宽的输入文件 假设可以从第2行的分隔符行可靠地推断列宽,这样相邻列分隔符之间的每个子字符串(如-)表示一列。 注意:该代码需要PowerShell Core v6.2.1+,但也可以在Windows PowerShell中使用

$sepChar = '-' # The char. used on the separator line to indicate column spans.
Get-Content c:\temp\qmedia.txt | ForEach-Object {
  $line = $_
  switch ($_.ReadCount) {
    1 { 
      # Header line: save for later analysis
      $headerLine = $line
      break
    } 
    2 { 
      # Separator line: it is the only reliable indicator of column width.
      # Construct a regex that captures the column values.
      # With the sample input's separator line, the resulting regex is:
      #     (.{12})(.{12})(.{19})(.{34})(.{25})
      # Note: Syntax requires PowerShell (Core) v6.2.1+
      $reCaptureColumns = 
        $line -replace ('{0}+[^{0}]+' -f [regex]::Escape($sepChar)), 
                       { "(.{$($_.Value.Length)})" }
      # Break the header line into column names.
      if ($headerLine -notmatch $reCaptureColumns) { Throw "Unexpected header line format: $headerLine" }
      # Save the array of column names.
      $columnNames = $Matches[1..($Matches.Count - 1)].TrimEnd()  
      break
    }
    default {
      # Data line:
      if ($line -notmatch $reCaptureColumns) { Throw "Unexpected line format: $line" }
      # Construct an ordered hashtable from the column values.
      $oht = [ordered] @{ }
      foreach ($ndx in 1..$columnNames.Count) {
        $oht[$columnNames[$ndx-1]] = $Matches[$ndx].TrimEnd()
      }
      [pscustomobject] $oht # Convert to [pscustomobject] and output.
    }
  }
}

上面输出了[pscustomobject]实例流,允许进行健壮、方便的进一步处理,如mclayton的回答中所示的所需日期解析。当然,您可以将此处理直接集成到上面的代码中,但是我想单独展示固定宽度解析解决方案。

您的源文件在这里是如何显示的?看起来您正在解析一个固定宽度的文件,除非您能保证一致性,否则它会变得很危险。这看起来很容易由powershell生成

陆上通信线。你能控制那个源文件吗?我知道这不是你要问的,但它会帮助你更容易地处理问题。@Matt-举个例子:L40065L8 IEPort1 DRP_TAPE_DRPLTO_DRP_GLB_SECOND_COPY_TAPE_WEEK Wed Mar 31 10:13:07 2021。substring 78=>ed Mar 31 10:13:07 2021,而不是Wed Mar 31 10:13:07 2021:@Matt这是一个文本文件的命令输出,我可以100%保证一致性,但几乎可以。使用rn o类似的工具进行拆分可能更好,因为我对powershell知之甚少,代码也很少,所以我选择了子字符串选项:你的源文件是如何在这里显示的?看起来您正在解析一个固定宽度的文件,除非您能保证一致性,否则它会变得很危险。看起来它也可以很容易地由powershell生成。你能控制那个源文件吗?我知道这不是你要问的,但它会帮助你更容易地处理问题。@Matt-举个例子:L40065L8 IEPort1 DRP_TAPE_DRPLTO_DRP_GLB_SECOND_COPY_TAPE_WEEK Wed Mar 31 10:13:07 2021。substring 78=>ed Mar 31 10:13:07 2021,而不是Wed Mar 31 10:13:07 2021:@Matt这是一个文本文件的命令输出,我可以100%保证一致性,但几乎可以。使用rn o类似的工具进行拆分可能更好,因为我对powershell知之甚少,代码也很少,所以我选择了子字符串选项:非常感谢@mclayton!更好吗?RETAILUNTIL=if$\.Substring$columns[4],$columns[4]-$columns[4]。Trim-eq now{}else{[datetime]::ParseExact$\.Substring$columns[4],$columns[5]-$columns[4]。Trim,[string[]@ddd MMM dd HH:mm:ss yyyy,ddd MMM d HH:mm:ss yyyyy,[Globalization.CultureInfo]::CreateSpecificCultureen US,None}好的,就$columns[4],因为$[columns[5]的存在就是-$\子字符串$x,$y表示从$x开始取$y个字符,而$\子字符串$x表示从字符$x取到字符串末尾,这很好。一些简化的想法:[Globalization.CultureInfo]::CreateSpecificCultureen-US->[CultureInfo]“en-US”,但您也可以传递[CultureInfo]::InvariantCulture,因为它也基于英文的日和月名称。您不必构造两个格式字符串,只需用一个空格替换多个空格的运行即可预处理输入字符串:[datetime]::ParseExact$子字符串$columns[4].修剪-替换'+','',ddd MMM d HH:mm:ss yyyy',[cultureinfo]::InvariantCultureThanks再次,爱这部分$columns=[regex]::匹配$lines[1],-+.Index;匹配columnsThanks@mclayton!更好吗?RetailTill=if$子字符串$columns[4],$columns[4]$columns[4].修剪-eq现在{}else{[datetime]::ParseExact$\子字符串$columns[4],$columns[5]-$columns[4]。Trim[string[]@ddd MMM dd HH:mm:ss yyyy,ddd MMM d HH:ss yyyy,[Globalization.CultureInfo]::CreateSpecificCultureen-US,None}好的,只有$columns[4],因为$[columns[5]dosent的existsYeah正是-$\子字符串$x,$y表示从$x开始接受$y个字符,而$\子字符串$x表示从字符$x到字符串末尾做得很好。简化的一些想法:[全球化.CultureInfo]::CreateSpecificCultureen US->[CultureInfo]“en US”,但您也可以传递[CultureInfo]::InvariantCulture,因为它也基于英文日和月名称。不必构造两个格式字符串,只需将多个空格的运行分别替换为一个空格即可预处理输入字符串:[datetime]::ParseExact$。Substring$columns[4]。Trim-replace'+','','ddd MMM d HH:mm:ss yyyy',[cultureinfo]::InvariantCultureThanks再次,喜欢这部分$columns=[regex]::匹配$line[1],-+.Index;为了匹配由此产生的列,我在我的答案末尾无耻地添加了一个通用的ConvertFrom MyFormat函数:-@mclayton:Nice,不过为了更通用,我会将分隔符char.configurable设置为可配置,并且我不会假设每个列名的长度与其对应的分隔符运行长度完全相同。O因此,最好不要像我的解决方案那样对每一行执行正则表达式匹配,以提高性能-尽管为了性能,我会避免在函数$lines[2..$lines.Count-1].ForEach{…}中使用管道。处理日期解析的后处理代码不需要重新创建对象,它只需更新中的属性即可place@mklement0-我的目标只是比你低一点,基于OP的文件名从QMediaFormat进行一个通用的转换,而你的文件名看起来像是从FixedWidth进行更灵活的转换:-,但我会玩一下你的管道行建议。重新创建新对象时,OP的date属性的名称与文件头中的名称不同,因此我创建了新对象以匹配其原始输出,但重点是更新对象以提高性能。
M.受此启发,我无耻地在我的答案末尾添加了一个通用的convertfromyFormat函数:-@mclayton:Nice,不过为了更具通用性,我会使用分隔符char。可配置,并且我不会假设每个列名的长度与其相应的分隔符运行长度完全相同。否则,最好不要像我的解决方案那样对每一行执行正则表达式匹配,以提高性能-尽管为了性能,我会避免在函数$lines[2..$lines.Count-1].ForEach{…}中使用管道。处理日期解析的后处理代码不需要重新创建对象,它只需更新中的属性即可place@mklement0-我的目标比你低一点,基于OP文件名的QMediaFormat的通用ConvertFrom,而你的文件名看起来像是更灵活的ConvertFrom FixedWidth:-,但我会考虑一下你的建议。在重新创建新对象时,OP的date属性的名称与文件头中的名称不同,因此我构建了新对象以匹配其原始输出,但重点是更新对象以提高性能。M