bangumi / issues

Official issue tracking for Bangumi.tv
https://github.com/bangumi/issues/issues
24 stars 0 forks source link

含有多字节字符时签名长度判断错误 #31

Open ershiwo opened 8 years ago

ershiwo commented 8 years ago

启用了 BHC,症状是新出现的。 具体表现就是在更新过一次签名之后,这两天每天 0 点左右都会在时间线上看见自己的签名被自动更新并加上了一个“...”,已经和 @BinotaLiu 确认了不是 BHC 的问题,所以提交一下。 update: 只要进入一次设置页面,不管是否有操作,退出后就会自动更新签名了。

ghost commented 8 years ago

让我来猜 Bangumi 的源代码 (bgm38) 推测如下:

$sign = $_POST['sign'];
if(strlen($sign) > 50) {
    $sign = mb_substr($sign, 0, 50, 'utf-8') . ' ...';
}

写了一个解

$sign = $_POST['sign'];
$result = '';
$resultLength = 0;
$mbLength = mb_strlen($str, 'utf-8')
for($i = 0; $i < $mbLength; $i++) {
    $tmp = mb_substr($str, $i, 1, 'utf-8');
    $resultLength += (strlen($tmp) === mb_strlen($tmp, 'utf-8')) ? 1 : 2;
    $result .= $tmp;
    if($resultLength >= 50) {
        $result .= ' ...';
        break;
    }
}
ershiwo commented 8 years ago

但是似乎并不能解释自动更新的问题……

ghost commented 8 years ago

回信请加在信件顶部。

假設我們以第一段程序來運作,傳入的值是「巨硬信仰充值完毕,要不要坐下来盘昆特牌? ...」 運行下列程序:

$sign = $_POST['sign'];
if(strlen($sign) > 50) { //由於此處長度是「64」,所以會進入 if 內
    $sign = mb_substr($sign, 0, 50, 'utf-8') . ' ...'; //這時候以 mb_substr 取出來的前 50 字是「巨硬信仰充值完毕,要不要坐下来盘昆特牌? ...」,又 Append 了一組「 ...」了。
}

由於 strlen 所計算的是「字節數」,對於中日韓文來說,一個字符可能是二或三個字節(ANSI 或 UTF-8),所以在程序第二行得出的長度是 64 (20 個中文字 * 3 + 4 個 ASCII 字符 = 64) 關於解法我在上一封回信內有提到,這裡再對源代碼做解釋:

$sign = $_POST['sign'];
$result = '';
$resultLength = 0;
$mbLength = mb_strlen($str, 'utf-8')
for($i = 0; $i < $mbLength; $i++) { //嘗試針對輸入的每一個「字」(多字節字符算一個字)進行操作
    $tmp = mb_substr($str, $i, 1, 'utf-8');
    $resultLength += (strlen($tmp) === mb_strlen($tmp, 'utf-8')) ? 1 : 2; //若 strlen 取得的長度與 mb_strlen 取得的長度相同代表他是一個 ASCII 字符(半角字符),若否則代表是一個多字節字符(大部分情況下是全角,例外可能是半角的カタカナ,總之這裡當 2 去計算)
    $result .= $tmp; //將當前遍歷到的字符串上去
    if($resultLength >= 50) { //最後這裡判斷到目前為止拼上去字符串的長度(全角字符算 2 ,半角字符算 1),若全部大於 50 就停止
        $result .= ' ...';
        break;
    }
}
ershiwo commented 8 years ago

也就是说,当时没考虑 CJK 编码的特殊性,导致设定的允许字符数小了。如果是这样的话应该加一行截断再输出“…”的。

ghost commented 8 years ago

直接截断 ... 也是不行的,因为限制是「50字」, 如果是这句话 起来,饥寒交迫的奴隶,起来,全世界受苦的人 这一段文字虽然只有 21 个字, 但是 strlen 算出来的长度是 63 个字,超过 50 了, 所以通过 mb_substr($sign, 0, 50, 'utf-8') 截断前 50 个字并串上 ..., 在这里对 mb_substr 来说,算出来的长度是 21 哦!所以取前 50 个字就等于取全部了。 下一次再提交的时候,你的签名会是 起来,饥寒交迫的奴隶,起来,全世界受苦的人 ..., strlen 算出来长度是 67,同样超过 50 ,所以截断, 截断了前 50 个字,还是 起来,饥寒交迫的奴隶,起来,全世界受苦的人 ... ,然后又给你串了一次 ..., 所以在签名是中文时,这样子并不是正常情况,

正常情况应该是像这个样子: 输入字串为「Lorem ipsum dolor sit amet, consectetur adipiscing volutpat.」这里总共有 60 个字, 通过 strlen 算出来的长度是 60 ,超过 50 了, 所以通过 mb_substr($sign, 0, 50, 'utf-8') 截断前 50 个字并串上 ..., 这时候你的签名会变成这样:Lorem ipsum dolor sit amet, consectetur adipiscing ... 下一次你提交的时候再通过一样的过程: 通过 strlen 算出来的长度是 54 ,超过 50,所以截断再串上 ..., 截断 50 个字的时候就是 Lorem ipsum dolor sit amet, consectetur adipiscing ,所以并不会出现两次 ...