regex 总览待审核
基础 · \p{P} 标点七子类

\p{P} 标点七子类:脚注符号 * † ‡ § ¶ ‖ 到底归哪一类

码点的档案页的 gc 表里,\p{P}(Punctuation 标点)只占一行——但它 其实切成 7 个子类 Pc Pd Ps Pe Pi Pf Po。传统脚注符号 * § 全落在成员最多的 catch-all 子类 Po 里;而看着 «成对»的开括号 / 闭括号(Ps / Pe)、前引号 / 后引号(Pi / Pf) 数量却配不齐。这页把这 7 个子类全用原生 /\p{…}/u 实测着看清楚:每个码点恰好属于一个 gc 子类,首字母 P 就是它的大类。

gc 子类是「身份」,不是「住址」。 一个码点的 General_Category 在 UCD 里是单值的—— * 的 gc 永远是 Po,无论它出现在脚注、乘法还是脚标里。这和 Block(住址) 不同:住址只说它住哪段码位。本页所有「是不是某子类」都用 /^\p{…}$/u 当场测,不是查表。

先回答那个问题:六个脚注符号全是 Po

排版里第一个脚注用 *、第二个 、第三个 ……(传统排版有固定次序 * † ‡ § ‖ ¶——这是 排版约定的次序,Unicode 的码位顺序根本不照它走:§ 早早躺在 U+00A7/00B6、 在 U+2016、 才到 U+2020/2021,被彻底打散)。它们在 gc 里没有专属子类, 一律归入「其他标点 Po」——下表每行的 gc 都是当场 /^\p{Po}$/u 测出来的:

对照:(U+2016)是 Po,而长得像它的管道符 |(U+007C)却是 Sm(数学符号,根本不在 \p{P} 里);§ Unicode 6.1(2012)之前曾是 So(其他符号),之后才并入 Po——老数据可能仍标成 Symbol。 不止 gc——任何可查属性都不编码「能当脚注」。 把最沾边的二元属性挨个试过去也全落空:这六个符号 \p{Terminal_Punctuation} / \p{Sentence_Terminal} / \p{Dash} / \p{Quotation_Mark} 一律 No。唯一「全 Yes」的是 \p{Pattern_Syntax}——但它是 干扰项(red herring):它的含义是「保留给编程语法、不可作标识符」,把 ! ? % & 和上百个符号一并圈入,同样挑不出脚注。下面这两行是当场 /\p{…}/u 实测的:

category 查不出「脚注用途」—— 但可按字符 Name 筛选候选

既然六个脚注符号在 gc 里全是 Po、和 ! ? % & 同属 641 个成员的 catch-all 子类,那 「哪些字符能当脚注 / 引用记号」这件事 category 根本不编码 —— 它是排版约定(editorial convention, Chicago / Oxford 等 style manual 沿袭的活字传统),不是字符属性。Unicode 唯一沾边的可机读线索是 字符的 Name(及 NamesList.txt 注解),比如 U+203B 干脆就叫 REFERENCE MARK。 所以只能反过来按名字筛选候选,而非用 category 过滤。下面按 Name 关键词筛(数据取自官方 UnicodeData.txt; \p{} 查不到 Name,和 Block 一样,故走内嵌名单):

两点结论:其一 用 gc 选不出脚注符号 —— Po 里九成以上跟脚注无关,「属于 Po」推不出「能当脚注」; 其二 即便改用 Name 关键词,命中也横跨 Po / Sm / So / Mn / Lo / Cf —— 「名字里有 ASTERISK」既不等于「是标点」( ASTERISK OPERATOR 是 Sm、❃ 是 So 装饰、 ◌͙ COMBINING ASTERISK BELOW 是 Mn),更不等于「能当脚注」。Name 是线索不是标签; 「footnote-ness」这种语义 Unicode 不编码 —— 真要判定,得查 style manual,不是查码点属性。 连 NamesList 注解也只是「线索」,给不了判定。 反例两头都有:U+070D SYRIAC HARKLEAN ASTERISCUS 的注解明写 “marks the beginning of a phrase, word, or morpheme that has a marginal note”——语义上就是旁注记号、名字也含 ASTERIS、gc 还是 Po,却没被收进上面的候选(它是 Syriac 专用);反过来 ASTERISM(U+2042) 在 NamesList 里一条注解都没有,却作为正统排版记号被收了。可见「是不是脚注符号」最终由 style manual 的排版约定裁定—— UnicodeData / NamesList 只能帮你缩小候选,最后一步还得人工按约定取舍。

\p{P} 的七个子类(Unicode 17.0 实测基数)

每个码点恰好属于其中一个。Po 是 catch-all 子类,独占四分之三:

子类名称码点数典型成员
PoOther 其他标点641. , ! ? ; : … · * † ‡ § ¶ ‖ 、 。
PsOpen 开标点79( [ { 「 『 (
PeClose 闭标点77) ] } 」 』 )
PdDash 横线标点27- – — ~
PiInitial 前引号12« ‹ “ ‘ ‛ ‟
PfFinal 后引号10» › ” ’
PcConnector 连接标点10_ ‿ ⁀ _
合计 856 个(Unicode 17.0)。随版本会微涨;计数 = 全码空间逐码点 /\p{…}/u 枚举所得。

探针:任意字符落在哪个子类

输入一个字符,实时测它是不是 \p{P}、是 7 子类里的哪一个;末行还报它的「配对角色」(开 / 闭 / 引号始 / 引号终)。 试试 _(下划线竟是标点 Pc)、|(不是标点,是 Sm)、(顿号 Po)。

逐项原生 /^\p{…}$/u 实测

七子类着色器:把标点从文本里挑出来上色

粘一段中英混排文本,逐码点把属于 \p{P} 的字符按 7 子类上色,其余字符灰显——看 gc 怎么在真实标点上切分:

开闭、引号为什么配不齐

直觉上「有开就有闭」,可实测数量对不上:Ps 79 ≠ Pe 77Pi 12 ≠ Pf 10, 两组各差 2。根源是同一件事——引号天生塞不进「开 / 闭」二分法。gc 标的是字符自身的形状 / 角色, 不是「配对关系」,于是配对就破了。落单的两组都是引号(下面每个 gc 当场实测):

PsPe 多 2:德式低位引号 (U+201A)(U+201E)被归成开标点 Ps (德语 „…“ 在基线起头),但它们的「闭合形」是抬高的 ,而那俩被归进了 Pi 而非 Pe —— 于是这两个低引号在 Ps 有身位、Pe 找不到对应。
PiPf 多 2:反 9 字形引号 (U+201B)(U+201F)是「翻转当开引号用」的变体, 没有专属闭合形(要闭合直接用普通的 ),于是 Pi 这边凭空多两个。
那「成对」到底记在哪? 真正需要左右镜像的是括号,Unicode 用另一套正交属性专门管: Bidi_Paired_Bracket / Bidi_Paired_Bracket_Type(open / close / none)和 Bidi_Mirrored。 引号因为跨语言方向不固定,根本没被纳入 bracket 配对。所以「成对」靠这套属性表达,而不是指望 Ps 数量等于 Pe。可惜 JS 正则的 \p{…} 不支持 Bidi 系列属性(和 Block一样查不到),本页只能用 gc 把现象呈现出来。 和别的页串起来:本页是 码点的档案 \p{…} 里 gc「七大类 30 小类」表中 \p{P} 那一行的展开,和 \p{Math} 全量速查 是「按身份深挖一类」的两个样本 (一个是 gc 子类、一个是二元属性)。

相关链接