package linkmeta import ( "bytes" "strings" "golang.org/x/text/encoding/htmlindex" "golang.org/x/text/transform" ) // charset.go 负责把响应字节解成 UTF-8 字符串。 // // 为什么必须做:老派中文站点(以及不少论坛/院校站)仍在用 GBK/GB2312/Big5, // 直接当 UTF-8 读会得到一串替换字符,卡片标题就成了乱码。与其显示乱码, // 不如抓对——这是纯展示层的事,不该让站主去改对方的编码。 // decode 按「HTTP 头声明 → 文档里的 → UTF-8」的优先级解码。 // 已经是 UTF-8/ASCII 时原样返回,不绕转换管线。 func decode(raw []byte, contentType string) string { raw = bytes.TrimPrefix(raw, []byte("\ufeff")) // UTF-8 BOM name := charsetFromHeader(contentType) if name == "" { // meta 标签本身是 ASCII(GBK/Big5 都是 ASCII 超集), // 所以从未解码的原始字节里嗅探是安全的,只看文档开头。 name = charsetFromMeta(raw) } if name == "" || isUTF8(name) { return string(raw) } enc, err := htmlindex.Get(name) if err != nil { return string(raw) // 没见过的字符名:按 UTF-8 尽力而为 } out, _, err := transform.Bytes(enc.NewDecoder(), raw) if err != nil && len(out) == 0 { return string(raw) } return string(out) } func charsetFromHeader(ct string) string { for _, part := range strings.Split(ct, ";") { kv := strings.SplitN(strings.TrimSpace(part), "=", 2) if len(kv) == 2 && strings.EqualFold(kv[0], "charset") { return normalizeCharset(strings.Trim(kv[1], `"'`)) } } return "" } var ( metaCharset = []byte("charset=") ) // charsetFromMeta 在文档开头找 或 // 。 func charsetFromMeta(raw []byte) string { head := raw if len(head) > 2048 { head = head[:2048] // 声明一定在前,不必扫全文 } lower := bytes.ToLower(head) i := bytes.Index(lower, metaCharset) if i < 0 { return "" } rest := head[i+len(metaCharset):] if len(rest) == 0 { return "" } // 值可能被引号包住;带引号时以配对引号收尾,不带时到引号/空格/;/ > 为止 // (不闭合的引号也当结束——http-equiv 写法里值是 content="...; charset=gbk", // 未加引号的 charset 值正好以那个收尾引号终止) if q := rest[0]; q == '"' || q == '\'' { rest = rest[1:] if end := bytes.IndexByte(rest, q); end >= 0 { rest = rest[:end] } } else if end := bytes.IndexAny(rest, `"' ;>`); end >= 0 { rest = rest[:end] } return normalizeCharset(string(rest)) } func normalizeCharset(s string) string { s = strings.ToLower(strings.TrimSpace(s)) // HTML 标准与 IANA 的常见别名统一成 htmlindex 认得的名字 switch s { case "gb2312", "gb_2312", "gb-2312": return "gbk" // GBK 是 GB2312 的超集,按声明的 GB2312 解会漏字 case "x-sjis": return "shift_jis" case "euckr", "kr": return "euc-kr" case "utf8", "utf-8", "ascii", "us-ascii", "": return s } return s } func isUTF8(name string) bool { switch name { case "", "utf-8", "utf8", "ascii", "us-ascii": return true } return false }