以前提到“取多行文本的某列文字再连成一行”这样一个功能,用PowerShell实现虽然不算什么难事,但其中的坑还请多加注意。另外其实这也有奇技淫巧的用武之地,值不值得就看情况了。
缘起
参见PowerShell代码备忘录里的“取多行文本的某列文字再连成一行”。抱歉,又说了一次w
环境
哎,OS是Windows 10 20H2,$PSCulture
和$PSUICulture
均为ja-JP
,提到PowerShell 5的时候用的是版本5.1
,而pwsh 7的话则是7.1
。
参考
- PowerShellTraps:PowerShell的坑。不只是单纯讲坑,也说了一些对应方法,甚至到了
奇技淫巧技巧的程度。 - About Arrays:这里暂且就用着pwsh自带的array就行,所以文档就参考这个了。
- Measure-Command:用来测执行时间。
- System.Globalization.Stringinfo:本文主角。
正文
前提
<#
过去存的一个巨大的输入法码表。多达28127行,内容大概是中古汉语拼音什么的,不是很懂。
#>
$path = 'D:\ime\zyenpheng.dict.yaml'
顺带一提,虽然现在说有点早,没在用char[]
或者CharEnumerator
的原因是,这边真的想要得到“字”——或者说grapheme——而不是能用16位char就能搞定的。
现在用[Globalization.StringInfo]::GetTextElementEnumerator()
就OK。如果需求比较特别,.EnumerateRunes()
也可以用。虽然其实这里好像真的用不上w
执行时间测试
把代码段打成scriptblock
……就是用{}
包起来,用Measure-Command
调一下就行。
比如说,
Measure-Command {
(1..1KB).ForEach{ $_ }
}
会返回一个TimeSpan
,挺容易读的所以不用多说也行。
利用array的方法
(Get-Content $path).ForEach{
[System.Globalization.StringInfo]::GetTextElementEnumerator(
($_, ' ')[!$_] # (1)
).Where(
{ $true },
'First'
)
} -join ''
这个,PowerShell 5也可以用。如果在用pwsh 7及以上的话,(1)
那边可以用[string]::IsNullOrEmpty($_) ? ' ' : $_
这样的三目运算符表达式来替换……看起来更严谨了是吧,其实在这里没必要,看情况用吧w
再提一嘴,(1)
这个可以说是pwsh 7以下(不含),三目运算符(ternary operator)表达式的一个代用办法(PowerShellTraps/Basic/Missing-ternary-operator),莫名很爽,建议用哦~
这边,执行时间的话,PowerShell 5和pwsh 7两边都试了,大概在500~600ms这样。似乎pwsh 7更快一些。
这个说实在的有点厉害。
多说点儿
pwsh的array自带的这两个方法:ForEach()
和Where()
,不仅用途广泛,而且效率着实可以。如果只要传一个scriptblock
参数,完全可以不用写小括号,直接接上大括号就好——不过左大括号必须紧贴方法名,或许看着没那么好看吧。
不过,稍等,Get-Content $path
返回的是一个array,这没有问题;但[System.Globalization.StringInfo]::GetTextElementEnumerator()
却返回一个TextElementEnumerator
——它实现了IEnumerator
哦,是一个enumerator。既然它可以使用Where()
,恐怕是作成了一个类似array的东西。那,它是不是提前求出来了结果呢?还没调查到这一步,先用着就是。
另外,这两个方法都返回一个Collection
,尽管一般来说没什么困扰,但类型确实不同,注意一下为妙。
List<T>
这样的对象也可以用这两个方法,但我只好说,并不好用。为了减轻心智负担,对它们直接用Linq处理会好些。
差点忘了,有Clear()
方法可以用。不过这是后话。
利用管道
参见PowerShellTraps/Cmdlets/ForEach-Object以及PowerShellTraps/Cmdlets/Where-Object。其中提到了ForEach-Object
和Where-Object
的性能问题,并提供了解决方案:.{ process { ... } }
之类。但是吧,这也是要利用管道的,所以不要太指望性能多好就是。
{
param (
$Source,
$Target
)
$idx = 0
$Source |
. {
process {
$idx = 0
[System.Globalization.StringInfo]::GetTextElementEnumerator(
($_, ' ')[!$_]
)
}
} |
. {
process {
if ($idx -eq $Target) {
$_
}
$idx++
}
}
}.Invoke((Get-Content $path), 0) -join ''
说实在的,我跑通的时候已经傻了。
执行时间,这边大概在800~900ms,依旧是pwsh 7稍微快一些。
使用ForEach-Object
和Where-Object
的办法我就不测了哦w
多说点儿
用了临时的变量用来计数,算不上是很好的办法,只是能用了而已。
. { process {} }
,这个结构,.
是dot source,process {}
造了一个能接受管道输入的代码块。
考虑到[System.Globalization.StringInfo]::GetTextElementEnumerator()
返回的是一个enumerator,直接拿它操作也不错,或许之后会尝试。
呃,对了,话说为什么非要自己得出TextElementEnumerator
不可呢?——因为当时没看到其他的方法。
直接取值
(Get-Content $path).ForEach{
[System.Globalization.StringInfo]::GetNextTextElement(($_, ' ')[!$_])
} -join ''
……为什么没早点儿发现这个呢w
执行时间,PowerShell 5和pwsh 7都在400ms上下,pwsh 7看似稳定一些。相当不错哦。
好耶,不用劳烦了ww
后记
pwsh是真的weird嚯w
总之注意躲坑哟。