• R语言
  • R 中 `nchar` 函数的一个疑问

楼主对一个列表使用 nchar 函数计算字符数,最开始感到迷惑的是执行nchar(list)后得到的62和21,不晓得是怎么算出来的。翻了下文档发现这个函数还能计算字节数,然而执行nchar(list, type = 'bytes')得到的96和21,还是不晓得怎么算出来的,请路过的坛友们有兴趣帮忙瞅瞅吧。

list <- list(c("财政部 卫生健康委", "财政部 卫生健康委", NA, Inf, -Inf, NaN, "", " ", NULL),
             c(NA, Inf, -Inf, NaN, NULL))

nchar(list)
## [1] 62 21

nchar(list, type = 'bytes')
## [1] 96 21

lapply(list, function(x)
  nchar(x))
## [[1]]
## [1]  9  9 NA  3  4  3  0  1
## 
## [[2]]
## [1] NA  3  4  3

lapply(list, function(x)
  nchar(x, type = 'bytes'))
## [[1]]
## [1] 27 25 NA  3  4  3  0  1
## 
## [[2]]
## [1] NA  3  4  3

    yuanfan 函数 nchar() 接受的输入类型应该是字符,而不是列表。理论上你必须传一个字符向量给它。如果非要塞给它一个列表,那它只能先强行转字符,再吞下去。你看一眼 as.character(list) 的结果就知道怎么回事了。这就好比,嘴本是用来进食的,不要把灯泡放进去;非要放进去的话,嘴只能假装灯泡是食物了。

    又及:我知道这只是一段随意的示例代码,但把一个列表对象命名为 list 实在让人有点难以忍受(就像给一只狗取名为“狗”一样)。

      应该是NA,NULL处理成字符串了:

      > nchar(NA)
      [1] NA
      > nchar(NULL)
      integer(0)
      > nchar(list(1,2,NA,NULL))
      [1] 1 1 2 4

        yihui

        人啊,你好,我看一眼as.character(list)的结果也没知道是怎么回事,喏,不信你看:

        nchar(as.character(list))
        ## [1] 62 21
        nchar(as.character(list), type = 'bytes')
        ## [1] 96 21

        其实我知道62是怎么来的了,62 = 27 +25+ NA + 3 + 4+ 3+ 0+ 1。不过还是不懂96和21是怎么出来的……这位大人,要不你再想一想。

        lapply(list, function(x)
          nchar(x, type = 'bytes'))
        ## [[1]]
        ## [1] 27 25 NA  3  4  3  0  1
        ## 
        ## [[2]]
        ## [1] NA  3  4  3

          vickkk
          我本来探索 nchar 时用的是下面这个例子,没想到 NA 放到 list 里面再 nchar 会默认也处理成字符串。

          vector <- c(str1, NA, Inf, -Inf, NaN, "", " ", NULL, '\u2764', '∠( ᐛ 」∠)_',"❤ ")
          
          length(vector)
          ## [1] 10
          nchar(vector)
          ##  [1]  9 NA  3  4  3  0  1  1  9  2
          nchar(vector, keepNA = FALSE)
          ##  [1] 9 2 3 4 3 0 1 1 9 2

            看了下,问题分几块

            1) 列表对象传给 nchar 之前被 as.character 转了一道

            yihui 你看一眼 as.character(list) 的结果就知道怎么回事了

            我觉得下面这个例子可以比较明显看出来是怎么个转法

            as.character(list(1:2, c(1, 2)))
            #> [1] "1:2"     "c(1, 2)"

            所以传给 nchar 的就需要是个字符向量,传给它一个列表就会导致各种看不懂的结果

            2) nchar 里面的参数 type = 'bytes',加与不加分别代表着计算字节和字符数,字节和字符的区别涉及字符编码与存储,属于比较难啃且没事的时候没必要啃的内容,绝大多数情况下你想要的都是字符数。而这里的问题简单来说,就是对于中文字符,在 UTF-8 下,1 字符 = 3 字节

            nchar('a')
            #> [1] 1
            nchar('a', type = 'bytes')
            #> [1] 1
            
            nchar('严')
            #> [1] 1
            nchar('严', type = 'bytes')
            #> [1] 3

            当然一定有人会想了解下字符字节这种概念,放个链接好了
            https://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

            yihui 把一个列表对象命名为 list 实在让人有点难以忍受

            进一步抛个例子看大伙能不能忍受

            a <- 1
            b <- 2
            c <- 3
            d <- c + c(a, b)

              yuanfan 我看一眼as.character(list)的结果也没知道是怎么回事

              你看的不是我让你看的。我让你看的是 as.character(list),不是让你看 nchar(as.character(list))

              list <- list(c("财政部 卫生健康委", "财政部 卫生健康委", NA, Inf, -Inf, NaN, "", " ", NULL),
                           c(NA, Inf, -Inf, NaN, NULL))
              as.character(list)
              # [1] "c(\"财政部 卫生健康委\", \"财政部 卫生健康委\", NA, \"Inf\", \"-Inf\", \"NaN\", \"\", \" \")"
              # [2] "c(NA, Inf, -Inf, NaN)"   

              你滴,明白?

              yuanfan 没想到 NA 放到 list 里面再 nchar 会默认也处理成字符串

              非也,不是 NA 放到列表里会处理成字符串,而是列表不是 nchar() 的天然食物,它只能被强行转换为字符再喂给 nchar(),而列表转字符这个操作多数情况下都是没什么意义的(会转成 R 源代码的字符)。

              meeeeeeeeo 进一步抛个例子看大伙能不能忍受

              一个冷笑话:https://d.cosx.org/d/13050
              一个更冷的笑话:https://d.cosx.org/d/419563/6

                yihui
                我滴,我咪西咪西滴不明白,还是哧溜哧溜滴好奇。这样吧,我来把我的脑回路展开讲讲,也许这样大人们能帮我再去去水分。

                我最近在写一篇关于 R base 字符串处理函数的博客,写这类函数并不只是想要写众所周知的功能,还想弄清楚这些函数在面对一些极端情况时会出现什么问题,之所以这么想是因为日常工作中的数据就是会有各种各样的奇葩之处。

                另外,我知道 R 中存在“强制转换”,不晓得这是不是我才会遇到的问题,就是前期数据探索的时候不能把数据中所有奇葩之处都梳理出来,但由于存在一些我也不了解的“强制转换”,使得最后还是跑出结果。所以从我的视角看,nchar 这个函数也能强制转换,就好比c()里也能输入数字,数字会强制转换成带引号的字符串,我看到nchar(as.character(list))``
                nchar(as.character(list), type = 'bytes')
                的结果和没加上as.character的结果是一样的,所以对于96和21依然好奇那是怎么来的。

                当然,依然好奇之中还包含着一个动机,我总觉得世界上很多工具被发明出来的时候,发明者可能只是为了解决问题 A,但许多脑洞大开的使用者会发现能用那工具去实现 BCDEF,当把列表输入 nchar 发现没报错的时候,我默默猜想也许 R中的强制转换包容我这么干,允许我探索 BCDEF,于是执着地继续感到好奇。

                  话说我又发现了一点,"c(NA, Inf, -Inf, NaN)"的长度就是21,用 nchar 不管是计算字符数还是计算字节数都好像是21。

                    yuanfan
                    其实在我已经写好的部分博客里,对 nchar 这个函数是这么写的。

                    R 中通常用nchar()函数来计算字符串的长度,用length()函数来计算向量的长度(PS.在 sql 语言中用length()函数来计算字符串长度)。
                    
                    下面有两个极为相似的字符串,长度一致但并不相等。
                    
                    str1 <- "财政部 卫生健康委"
                    str2 <- "财政部 卫生健康委"
                    nchar(str1)
                    ## [1] 9
                    nchar(str2)
                    ## [1] 9
                    length(str1)
                    ## [1] 1
                    str1 == str2
                    ## [1] FALSE
                    如果将这两个字符串分别转换成原始的16进制字节序列,比较转换后存在的差异,可以看到前者独有“e2 80 82”(代表半个空格,EN SPACE),后者独有“20”(代表一个普通空格,SPACE)。
                    
                    raw1 <- charToRaw(str1)
                    raw2 <- charToRaw(str2)
                    print(raw1)
                    ##  [1] e8 b4 a2 e6 94 bf e9 83 a8 e2 80 82 e5 8d ab e7 94 9f e5 81 a5 e5 ba b7 e5
                    ## [26] a7 94
                    print(raw2)
                    ##  [1] e8 b4 a2 e6 94 bf e9 83 a8 20 e5 8d ab e7 94 9f e5 81 a5 e5 ba b7 e5 a7 94
                    
                    # 比较差异,raw1 有,raw2无
                    setdiff(as.character(raw1), as.character(raw2))
                    ## [1] "e2" "80" "82"
                    
                    # 比较差异,raw1 无,raw2有
                    setdiff(as.character(raw2), as.character(raw1))
                    ## [1] "20"
                    如果再把这两个16进制字节序列转换为字符串,用普通的眼睛看也还是很难看出区别。
                    
                    rawToChar(as.raw(c(0xe2, 0x80, 0x82)))
                    ## [1] " "
                    rawToChar(as.raw(c(0x20)))
                    ## [1] " "

                    另外,我写的很多 R 相关的博客里,经常都是data <- data.frame(...)``data1 <- data.frame(...)这样直接用 data 或者data 后面加数字的名字来命名建立的数据框。

                    yuanfan 我滴,我咪西咪西滴不明白

                    nchar("c(\"财政部 卫生健康委\", \"财政部 卫生健康委\", NA, \"Inf\", \"-Inf\", \"NaN\", \"\", \" \")")
                    # [1] 62
                    nchar("c(NA, Inf, -Inf, NaN)")
                    # [1] 21

                    你滴,现在明白?

                    yuanfan 话说我又发现了一点,"c(NA, Inf, -Inf, NaN)"的长度就是21,用 nchar 不管是计算字符数还是计算字节数都好像是21。

                    因为这个字符串里的所有字符都是单字节字符,所以字符和字节数一样。只有字符串含有多字节字符时结果才会不一样。

                      yihui

                      我滴,我要是再不明白那可真是天理难容、容光焕发、发愤图强、强身健体、体恤民情、情……情深深雨濛濛……

                      字母就都是单字节字符吧,剩下的我会自己梳理清楚的,俺衷心地感谢益辉人类的解答啊。

                        yuanfan 你可以近似认为键盘上(英文输入模式下)能敲出来的字符都是单字节。

                        x = intToUtf8(1:200, TRUE)
                        data.frame(x, chars = nchar(x), bytes = nchar(x, type = 'bytes'))

                          yihui
                          因为你给出的这部分内容我只看一眼没看明白咋回事,所以我又花时间做了下面的梳理,请你瞅瞅哈(ps注意,念第四声)。

                          字符、字节、编码标准

                          字符(Character)是计算机中文本的最小单位,有许多类型如数字、字母、符号等,一串字符组合起来就是字符串(String),在 R 中通常由单引号或双引号包裹起来。

                          字节(Byte)是计算机中数据处理的最小单位,通常由8位二进制数字(8bits)组成,一位二进制数字(0或1)是计算机中数据存储的最小单位。

                          而字符与字节的关系在不同的编码标准下,有所不同。

                          1. ASCII 编码,是最早诞生的字符编码标准,ASCII 字符集共有128个字符,包括大写字母、小写字母、(阿拉伯)数字等,均用一个字节代表一个字符。

                          2. ANSI 编码,是一种关于编码方式和存储方式的方案标准,通常用1到2个字节代表1个字符,ANSI 字符集中前128个与 ASCII 码保持一致,后面的由不同的国家或地区基于各自的需求来制定,因而也衍生出许多不同的字符编码方案。这些编码方案在 Windows 系统中被称为代码页(Code Page):比如支持简体中文的代码页编号是936,对应的 ANSI 标准是GB2312;支持英语及其他西欧语言的代码页编号是1252,对应的 ANSI 标准是 ISO 8859-1(Latin-1);支持繁体中文的代码页编号是950,对应的ANSI标准是 Big5。

                          3. GB2312编码,是中国发布的汉字编码标准,后来扩展成GBK,又扩展成GB18030。

                          4. Unicode 编码,是几乎能够覆盖世界上所有字符的统一编码标准,在Unicode 符号表中,不同语言的不同符号都有唯一对照的编码。最常用的存储方式有UTF-8(每个字符由1到4个字节组成)、UTF-16(每个字符由2到4个字节组成)、UTF-32(每个字符固定由4个字节组成)。

                          在 R 中,执行Encoding()函数可以将字符向量识别为四种不同的编码标准和存储格式:UTF-8(Unicode 编码)、Latin-1(ISO 8859-1)、Bytes(非 ASCII 字符的原始字节)、unknown(本地或未指定的编码)。