检查字符串仅包含 ASCII 字符

问题描述 投票:0回答:4

Go 有什么方法或者有建议如何检查字符串是否只包含 ASCII 字符?正确的做法是什么?

根据我的研究,解决方案之一是检查是否存在大于 127 的字符。

func isASCII(s string) bool {
    for _, c := range s {
        if c > unicode.MaxASCII {
            return false
        }
    }

    return true
}
go ascii
4个回答
29
投票

在 Go 中,我们关心性能,因此,我们会对您的代码进行基准测试:

func isASCII(s string) bool {
    for _, c := range s {
        if c > unicode.MaxASCII {
            return false
        }
    }
    return true
}

BenchmarkRange-4    20000000    82.0 ns/op

更快(更好、更惯用)的版本,避免不必要的符文转换:

func isASCII(s string) bool {
    for i := 0; i < len(s); i++ {
        if s[i] > unicode.MaxASCII {
            return false
        }
    }
    return true
}

BenchmarkIndex-4    30000000    55.4 ns/op

ascii_test.go

package main

import (
    "testing"
    "unicode"
)

func isASCIIRange(s string) bool {
    for _, c := range s {
        if c > unicode.MaxASCII {
            return false
        }
    }
    return true
}

func BenchmarkRange(b *testing.B) {
    str := ascii()
    b.ResetTimer()
    for N := 0; N < b.N; N++ {
        is := isASCIIRange(str)
        if !is {
            b.Fatal("notASCII")
        }
    }
}

func isASCIIIndex(s string) bool {
    for i := 0; i < len(s); i++ {
        if s[i] > unicode.MaxASCII {
            return false
        }
    }
    return true
}

func BenchmarkIndex(b *testing.B) {
    str := ascii()
    b.ResetTimer()
    for N := 0; N < b.N; N++ {
        is := isASCIIIndex(str)
        if !is {
            b.Log("notASCII")
        }
    }
}

func ascii() string {
    byt := make([]byte, unicode.MaxASCII+1)
    for i := range byt {
        byt[i] = byte(i)
    }
    return string(byt)
}

输出:

$ go test ascii_test.go -bench=.
BenchmarkRange-4    20000000    82.0 ns/op
BenchmarkIndex-4    30000000    55.4 ns/op
$

8
投票

看起来你的方式是最好的。

ASCII 简单地定义为:

ASCII 将 128 个指定字符编码为七位整数

因此,字符的值为 0-27(或 0-127、0x0-0x7F)。

Go 没有提供方法来检查字符串中的每个符文(或切片中的字节)是否具有特定范围内的数值,因此您的代码似乎是执行此操作的最佳方法。


2
投票

另一种选择:

package main
import "golang.org/x/exp/utf8string"

func main() {
   {
      b := utf8string.NewString("south north").IsASCII()
      println(b) // true
   }
   {
      b := utf8string.NewString("🧡💛💚💙💜").IsASCII()
      println(b) // false
   }
}

https://pkg.go.dev/golang.org/x/exp/utf8string#String.IsASCII


0
投票

接受的答案比最初提出的解决方案更快,但我认为最初提出的解决方案更惯用。最初的解决方案是惯用的,因为几乎总是,当您在 go 中查看字符串的内容时,您对代码点而不是单个 utf8 编码的字节值更感兴趣。这里的范围是 100% 惯用的。

在这种特定情况下,索引字符串和比较字节恰好更快,并且仍然给我们一个正确的结果。但这仍然不是一个优化的解决方案。还差得远呢

那么我们来建一个自行车棚吧。

因此,对于速度更快的非惯用(尽管基于 stdlib 代码)实现,我提出以下内容:

func isASCIIIndexBy8s32(s string) bool {
    // idea adapted from here:
    // https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/unicode/utf8/utf8.go;l=528
    for len(s) > 0 {
        if len(s) >= 8 {
            first32 := uint32(s[0]) | uint32(s[1])<<8 | uint32(s[2])<<16 | uint32(s[3])<<24
            second32 := uint32(s[4]) | uint32(s[5])<<8 | uint32(s[6])<<16 | uint32(s[7])<<24
            if (first32|second32)&0x80808080 != 0 {
                return false
            }
            s = s[8:]
            continue
        }
        if s[0] > unicode.MaxASCII {
            return false
        }
        s = s[1:]
    }
    return true
}

这比索引示例快两倍多。针对 64 位 cpu 的进一步优化使我们比该 cpu 又提高了 20%。

完整代码在这里。

package main

import (
    "testing"
    "unicode"
)

func isASCIIRange(s string) bool {
    for _, c := range s {
        if c > unicode.MaxASCII {
            return false
        }
    }
    return true
}

func isASCIIIndex(s string) bool {
    for i := 0; i < len(s); i++ {
        if s[i] > unicode.MaxASCII {
            return false
        }
    }
    return true
}

func isASCIIIndexBy8s32(s string) bool {
    // idea adapted from here:
    // https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/unicode/utf8/utf8.go;l=528
    for len(s) > 0 {
        if len(s) >= 8 {
            first32 := uint32(s[0]) | uint32(s[1])<<8 | uint32(s[2])<<16 | uint32(s[3])<<24
            second32 := uint32(s[4]) | uint32(s[5])<<8 | uint32(s[6])<<16 | uint32(s[7])<<24
            if (first32|second32)&0x80808080 != 0 {
                return false
            }
            s = s[8:]
            continue
        }
        if s[0] > unicode.MaxASCII {
            return false
        }
        s = s[1:]
    }
    return true
}

func isASCIIIndexBy8s64(s string) bool {
    // optimizing the 32 bit example above for 64 bit cpus
    for len(s) > 0 {
        if len(s) >= 8 {
            chunk := uint64(s[0]) | uint64(s[1])<<8 | uint64(s[2])<<16 | uint64(s[3])<<24 |
                uint64(s[4])<<32 | uint64(s[5])<<40 | uint64(s[6])<<48 | uint64(s[7])<<56
            if chunk&0x8080808080808080 != 0 {
                return false
            }
            s = s[8:]
            continue
        }
        if s[0] > unicode.MaxASCII {
            return false
        }
        s = s[1:]
    }
    return true
}

var tests = []struct {
    n string
    f func(string) bool
}{
    {
        n: "range",
        f: isASCIIRange,
    }, {
        n: "index",
        f: isASCIIIndex,
    }, {
        n: "index 8 32",
        f: isASCIIIndexBy8s32,
    }, {
        n: "index 8 64",
        f: isASCIIIndexBy8s64,
    },
}

func TestAsciis(t *testing.T) {
    atxt := "this is ascii text"
    utxt := "this is some unicode 💩💩and a lot more ascii text afterwards"
    for _, tst := range tests {
        t.Run(tst.n, func(t *testing.T) {
            if !tst.f(atxt) {
                t.Errorf("%s failed for %s", tst.n, atxt)
            }
            if tst.f(utxt) {
                t.Errorf("%s failed for %s", tst.n, utxt)
            }
        })
    }
}

func BenchmarkAsciis(b *testing.B) {
    str := ascii()
    for _, bnch := range tests {
        b.Run(bnch.n, func(b *testing.B) {
            b.ResetTimer()
            for n := 0; n < b.N; n++ {
                if !bnch.f(str) {
                    b.Errorf("not ascii")
                }
            }
        })
    }
}

func ascii() string {
    byt := make([]byte, unicode.MaxASCII+1)
    for i := range byt {
        byt[i] = byte(i)
    }
    return string(byt)
}

运行此命令会给出:

=== RUN   TestAsciis
=== RUN   TestAsciis/range
=== RUN   TestAsciis/index
=== RUN   TestAsciis/index_8_32
=== RUN   TestAsciis/index_8_64
--- PASS: TestAsciis (0.00s)
    --- PASS: TestAsciis/range (0.00s)
    --- PASS: TestAsciis/index (0.00s)
    --- PASS: TestAsciis/index_8_32 (0.00s)
    --- PASS: TestAsciis/index_8_64 (0.00s)
goos: darwin
goarch: amd64
pkg: playground
cpu: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
BenchmarkAsciis
BenchmarkAsciis/range
BenchmarkAsciis/range-8             13469913            86.72 ns/op
BenchmarkAsciis/index
BenchmarkAsciis/index-8             24108583            49.56 ns/op
BenchmarkAsciis/index_8_32
BenchmarkAsciis/index_8_32-8        62092825            20.89 ns/op
BenchmarkAsciis/index_8_64
BenchmarkAsciis/index_8_64-8        77044797            15.91 ns/op
PASS
© www.soinside.com 2019 - 2024. All rights reserved.