有没有办法精确匹配某些 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'