sed 匹配字符范围

sed 匹配字符范围

有没有办法精确匹配某些 Unicode 范围。
让我们使用西里尔字母范围例如:U+400 到 U+52f

可以使用以下命令打印整个字符范围(从 bash 或 zsh):

$ echo -e $(printf '\\U%x' $(seq 0x400 0x52f)) ЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљњћќѝўџѠѡѢѣѤѥѦѧѨѩѪѫѬѭѮѯѰѱѲѳѴѵѶѷѸѹѺѻѼѽѾѿҀҁ҂҃҄҇ҊҋҌҍҎҏҐґҒғҔҕҖҗҘҙҚқҜҝҞҟҠҡҢңҤҥҦҧҨҩҪҫҬҭҮүҰұҲҳҴҵҶҷҸҹҺһҼҽҾҿӀӁӂӃӄӅӆӇӈӉӊӋӌӍӎӏӐӑӒӓӔӕӖӗӘәӚӛӜӝӞӟӠӡӢӣӤӥӦӧӨөӪӫӬӭӮӯӰӱӲӳӴӵӶӷӸӹӺӻӼӽӾӿԀԁԂԃԄԅԆԇԈԉԊԋԌԍԎԏԐԑԒԓԔԕԖԗԘԙԚԛԜԝԞԟԠԡԢԣԤԥԦԧԨԩԪԫԬԭԮԯ

$ a=$(zsh -c 'echo -e $(printf '\''\\U%x'\'' $(seq 0x400 0x52f))')

要过滤它的某些范围,让我们使用 0x452 到 0x490,这是预期的输出:

$ b=$(bash -c 'echo -e $(printf '\''\\U%x'\'' $(seq 0x452 0x490))')
$ echo "$b"
ђѓєѕіїјљњћќѝўџѠѡѢѣѤѥѦѧѨѩѪѫѬѭѮѯѰѱѲѳѴѵѶѷѸѹѺѻѼѽѾѿҀҁ҂҃҄҇ҊҋҌҍҎҏҐ
$ echo "$b" | xxd
00000000: d192 d193 d194 d195 d196 d197 d198 d199  ................
00000010: d19a d19b d19c d19d d19e d19f d1a0 d1a1  ................
00000020: d1a2 d1a3 d1a4 d1a5 d1a6 d1a7 d1a8 d1a9  ................
00000030: d1aa d1ab d1ac d1ad d1ae d1af d1b0 d1b1  ................
00000040: d1b2 d1b3 d1b4 d1b5 d1b6 d1b7 d1b8 d1b9  ................
00000050: d1ba d1bb d1bc d1bd d1be d1bf d280 d281  ................
00000060: d282 d283 d284 d285 d286 d287 d288 d289  ................
00000070: d28a d28b d28c d28d d28e d28f d290 0a    ...............

但用sed过滤似乎不可能。这不起作用:

$ echo "$a" | sed 's/[^\x452-\x490]//g'

也不是这样(结果与其他字符匹配(可能是整理问题)):

$ echo "$a" | sed $'s/[^\u452-\u490]//g' АБВГжзийклмнопрстуфхцчшщъыьэюяёђєѕіїјљњћќѝўџҋҍҏҐҗҙқҝҟҡңҥҧҩҫҭүұҳҵҷҹһҽҿӂӄӆӈӊӌӎӐӒӔӝӟӡӣӥӧөӫӭӯӱӳӵӹԅԇԉԋԍԏ

甚至不是这个(相同的整理问题):

$ echo "$a" | sed 's/[^ђ-Ґ]//g'

这与 awk 一起工作:

$ echo "$a" | awk '{gsub(/[^ђ-Ґ]/,"")}1'

但使用十六进制范围的唯一方法是使用 shell 将十六进制转换为 unicode 字符

$ echo "$a" | awk $'{gsub(/[^\u452-\u490]/,"")}1'

或(两种解决方案):

$ c=$(bash -c 'printf "\u452-\u490"') 
$ echo "$a" | awk '{gsub(/[^'"$c"']/,"")}1'
$ echo $a | awk -v ra="[^$c]" '{gsub(ra,"")}1'

问题:

  • 有没有办法用 sed 来做到这一点?
  • awk 可以在没有更高 shell 的情况下以十六进制数字执行此操作吗?

  • 如果可能的话,sed 与 一起使用的整理序列所匹配的范围到底是什么sed 's/[^ђ-Ґ]//g'

PS:我知道可以用 perl 完成,谢谢。

答案1

根据 POSIX,括号表达式中的范围仅指定为基于 C/POSIX 语言环境中的代码点。在其他语言环境中,它是未指定的,并且通常在某种程度上基于您发现的整理顺序。您会发现,在某些语言环境中,根据工具的不同,[g-j]例如i还包括ı, ǵ,有时甚至I甚至ch像在某些捷克语言环境中一样。

zsh是罕见的范围之一,其[x-y]范围基于代码点,无论区域设置如何。对于单字节字符集,这将基于字节值,对于 Unicode 代码点或系统用来表示的任何内容的多字节字符集宽字符与 and co 内部 mbstowc()。 API(通常是 Unicode)。

所以在zsh,

  • [[ $char = [$'\u452'-$'\u490'] ]]
  • [[ $char = [^ђ-Ґ] ]]
  • y=${x//[^ђ-Ґ]/}

如果语言环境的字符集是多字节并且具有这两个字符,则在您的情况下可以匹配该 Unicode 范围内的字符。有些单字节字符集包含其中一些字符(例如 ISO8859-5,其中大部分字符位于 U+0401 .. U+045F 中),但在使用这些字符的语言环境中,范围[ђ-Ґ]将基于字节值(相应字符集中的代码点,而不是 Unicode 代码点)。

在 C 语言环境中,范围基于代码点,但 C 语言环境中的字符集仅保证包含便携式字符集这只是编写 POSIX 或 C 代码所需的几个字符(西里尔字母中没有这些字符)。也保证是单字节所以不可能包含 Unicode 中指定的所有字符。实际上,它最常见的是 ASCII。

实际上,如果不设置为 C(或至少具有单字节字符集的语言环境),LC_COLLATE则无法设置为 C。LC_CTYPE然而,许多系统都有一个C.UTF-8您可以在此处使用的区域设置。

UTF-8 是可以表示所有 Unicode 字符以及任何字符集中的所有字符的字符集之一。所以你可以这样做:

< file iconv -t utf-8 |
  LC_ALL=C.UTF-8 sh -c 'sed "$(printf "s/[^\321\222-\322\220]//g")"' |
  iconv -f utf-8

第一个iconv从用户的区域设置字符集转换为 UTF-8,\321\222并且\322\220分别是 U+0452 和 U+0490 的 UTF-8 编码,第二个iconv转换回区域设置的字符集。

如果当前语言环境已使用 UTF-8 作为字符集(并且file使用该字符集编写),则可以简化为:

<file LC_ALL=C.UTF-8 sed 's/[^ђ-Ґ]//g'

或者:

<file LC_ALL=C.UTF-8 sed "$(printf "s/[^\321\222-\322\220]//g")"

在 GNUsed提供的$POSIXLY_CORRECT环境中,您可以根据其编码的字节值来指定字符。

<file LC_ALL=C.UTF-8 sed 's/[^\321\222-\322\220]//g'

尽管在旧版本中您可能需要:

<file LC_ALL=C.UTF-8 sed 's/[^\o321\o222-\o322\o220]//g'

或者十六进制变体:

<file LC_ALL=C.UTF-8 sed 's/[^\xd1\x92-\xd2\x90]//g'

对于使用多字节字符集(包括基于 Unicode 的宽字符表示的系统上的那些字符)的语言环境,另一种选择是使用 GNUawk和:

awk 'BEGIN{for (i = 0x452; i<=0x490; i++) range = range sprintf("%c", i)}
     {gsub("[^" range "]", ""); print}'

(最初,我认为 POSIX 要求 awk 实现的行为类似于 GNU awk,但事实并非如此,因为 POSIXsprintf("%c", i)对于i编码区域设置中的字符(不是代码点)。这意味着它不能可移植地用于多字节字符)。

无论如何,请注意 U+0400 .. U+052F 范围并不是西里尔文中唯一的 Unicode 字符脚本,更不用说使用西里尔字母作为脚本的语言了。字符列表也随 Unicode 版本的不同而变化。

在类似 Debian 的系统上,您可以通过以下方式获取它们的列表:

unicode --max 0 cyrillic

(在 Ubuntu 16.04 上给出 435 个不同的,在 Debian sid 上给出 444 个(可能使用不同版本的 Unicode)。

在 中perl,请参阅\p{Block: Cyrillic}, \p{Block: Cyrillic_Ext_A,B,C}, \p{Block: Cyrillic_Supplement}... 以匹配 Unicode 块并\p{Cyrillic}匹配西里尔文字的字符(当前在您的版本 perl正在使用的 Unicode 版本中分配(请参阅perl -MUnicode::UCD -le 'print Unicode::UCD::UnicodeVersion'示例))。

所以:

perl -Mopen=locale 's/\P{Cyrillic}//g'

答案2

在基本 sed 中,括号表达式中的范围遵循 Posix。在 Posix 中,括号表达式中的范围遵循排序规则。仅在 C 语言环境中将排序规则定义为基于字符数值。但仅适用于单字节值。其余的语言环境在 Posix 中未定义。

为了使范围在 sed 括号表达式中工作,我们需要使用按数字 Unicode 代码点排序的排序规则,即 C.UTF-8。但这产生了用 utf8 编码范围字符的次要要求:

  • 获取 unicode 代码点范围的字符八进制表示形式(如果使用的区域设置是 utf-8):

    $ printf '\u452\u490' | od -An -to1
    

    如果不是 utf-8 语言环境,请将值转换为 utf-8:

    $ printf '\u452\u490' | iconv -t utf-8 | od -An -to1
    321 222 322 220
    
  • 添加破折号和 \o 以使其在旧/现在的 sed 中工作:

    $ printf '\o%s\o%s-\o%s\o%s' $(printf '\u452\u490'|iconv -tutf-8|od -An -to1)
    \o321\o222-\o322\o220
    
  • 使用该范围可以在 sed 中使用:

    $ echo "$a" | LC_ALL=C.UTF-8 sed 's/[^\o321\o222-\o322\o220]//g'
    
  • 但请确保区域设置为 C.UTF-8 并且给定的字符串以 utf8 编码并转换回所使用的区域设置:

    $ echo "$a" | iconv -t utf-8 |
                  LC_ALL=C.UTF-8 sed 's/[^\o321\o222-\o322\o220]//g' |
                                    iconv -f utf-8
    

    笔记上面我们使用了一个shell来转换\u452\u490

GNU awk 能够生成给定十六进制 Unicode 代码点的字符串(前提是有效的语言环境允许此类字符):

<<<"$a" awk 'BEGIN{for(i=0x452;i<=0x490;i++){r=r sprintf("%c", i)}}
 {gsub("[^" range "]", "")}1'

如果当前语言环境在 Unicode 代码点编号处不包含这些 Unicode 代码点,那么您需要转换为已知包含此类代码点的语言环境,并使用匹配的语言环境环境变量,例如:

<<<"$a" iconv -t utf8 |  
LC_ALL=en_US.UTF-8 awk '
        BEGIN{for(i=0x452;i<=0x490;i++){r=r sprintf("%c", i)}}
        {gsub("[^" r "]", "")}1
        ' | iconv -f utf8

底线需要更高版本的 shell(GNU bash 或 zsh)或 awk(仅 GNU)。

或者使用更高级的语言,如 perl:

$ echo "$a" | perl -Mopen=locale -ane 's/[^\x{452}-\x{490}]//g; print'

相关内容