21 12
发新话题
打印

[存档]让 Discuz! 论坛支持全部 Unicode 字符的方法

[存档]让 Discuz! 论坛支持全部 Unicode 字符的方法

经过三天的反复测试,终于找到了 Discuz! 遇到 Ext-B 区汉字发生截断的问题所在了:是 MySQL 的 Bug。

贴于这仅仅为了存档,如果以后发生这样的问题的话可以有一个解决的思路,但是这个仅仅是可行解,并不是最佳解,所以如果可能的话最好就保持现状,不要升级 MySQL 的版本。

废话少说,下面就是原文:

××××××××××××××××××××××××××××××××××××××


首先,需要说明的是,Discuz! + MySQL 的方式之所以无法支持扩展区的汉字,并不是 Discuz! 的原因,而是 MySQL 的问题,当 MySQL 遇到扩展区的汉字(5 个字节的 utf8 编码)时,MySQL 的校验机制会认为是错误字节而将之截断。

换句话说,如果将来某一天 MySQL 修正了这个问题,那么什么都不要改,直接安装 utf8 的 Discuz! 论坛就行了。也就是说,现在改了以后,将来还要再想法改回来,不能直接升级,切记!

解决这个 Bug 的关键就是让 MySQL 的错误纠正机制不起作用,那么怎么办才能不起作用呢?让系统认为是单字节编码,从 \x01~\xFF 全部用上的单字节编码,这样就不会遇到所谓的“错误”字节了。而 latin1 恰好是这样的编码,所以原理就是这样的:在传输和显示时都是以 utf8 编码的格式来进行,但是在存储时则以 latin1 的编码格式来存储,这样就不会有字符丢失的现象了。

不过这个方案有个缺点:直接查看数据库就会发现汉字全部都是拉丁乱码了,假如要直接修改数据库的数值的话就有些困难了。但是在 Discuz! 论坛网页上则都是正确的 utf8 字符了。

那么改如何以 latin1 的编码格式来存储呢?这个稍后在说,转成 latin1 编码后,虽然之后存储的文字都是可以正常存取的,但是原先在数据库中的数据就会变成“?????”,一旦变成“?????”,那是绝对没办法恢复的,切记!

所以首先要做的是:将原来的数据转换成拉丁乱码,存储进数据库。

1、运行以下命令导出数据库脚本(注意:不是备份数据库)
引用:
mysqldump -uroot -p discuz >C:\utf8.sql
mysqldump:是安装 MySQL 之后自带的一个导出工具,在命令行下运行。
-uroot:这个是导出使用的帐号,一般 root 是具有管理员权限的,紧跟着参数 -u 就行了,如果要使用其他帐号,就写成“-uusername”,“-u”和“username”之间不要加空格。
-p:提示输入密码。
discuz:要导出的数据库名称,这个按实际情况修改。
>C:\utf8.sql:导出的路径,请按实际情况修改。

运行后,会提示输入密码,然后就会自动导出到指定文件了。

2、用 EmEditor 打开 C:\utf8.sql 文件,先检查一遍是不是正确的汉字,右下角是不是显示“UTF-8 不带签名”。这一点很重要,请务必确认。

3、双击 EmEditor 右下角的“UTF-8 不带签名”,然后编码选择“西欧”(一般是最后一个,请注意不要选错,不然导入后会无法正确显示汉字的。)

4、全选-复制,然后新建一个文件,粘贴。

5、保存。在保存时,建议换个文件另存,不要把原来的文件替换;然后“编码”下拉菜单选择“UTF-8”,取消“添加一个 Unicode 签名(BOM)”,然后保存。

6、打开保存的文件进行确认(这里假设保存为 latin1.sql 文件),注意看右下角是不是显示“UTF-8 不带签名”。如果不是,请重复步骤 2、3、4、5。

7、上述步骤确认无误后,就可以执行导入的命令了
引用:
mysql -uroot -p discuz <C:\latin1.sql
导入命令和导出命令的参数类似,请自行修改数据库名“discuz”,和导入的脚本路径“C:\latin1.sql”。

导入成功后,现在查看数据库,应该发现里面的汉字都是乱码了(如果不是乱码请重复步骤 2、3、4、5)。接下来就是修改程序代码,使其可以正常显示 utf8 字符,而不是显示拉丁乱码。

8、打开 Discuz! 论坛路径下的 \include\db_mysql.class.php 文件,在第 35 行有如下代码:
引用:
@mysql_query("SET character_set_connection=$dbcharset, character_set_results=$dbcharset, character_set_client=binary", $this->link);
将其改为:
引用:
@mysql_query("SET character_set_connection=$dbcharset, character_set_results=$dbcharset, character_set_client=binary, character_set_database=latin1, character_set_system=$dbcharset", $this->link);
保存(不用改编码)。

然后打开论坛看看,应该什么都没变吧。但是其实可以说现在整个论坛已经脱胎换骨了,随便输入一个扩展区的汉字看看呢,成功了吧。

×××××××××××××××××××××××××××××××××××××××××××××××××××××

啰啰嗦嗦讲了这么多,下面再来说说如何恢复成原来的状态吧。

1、导出数据库脚本:参见上面第一步。(假设导出的文件名为 latin1.sql)
2、用 EmEditor 打开 latin1.sql。
3、新建一个空白文件,另存一下(在 EmEditor 中新建的空白文件似乎无法保存),编码选择“西欧”(假设新建的文件是 utf8.sql)。
4、确认 utf8.sql 文件的编码是“西欧”。
5、将 latin1.sql 中的文字全部复制到 utf8.sql 中去,然后保存。
6、双击 utf8.sql 文件窗口右下角的“西欧”,将编码改成“UTF-8”,确认中文是否显示正常,如果不正常,则重复步骤 2、3、4、5。
7、将 utf8.sql 导入数据库。
8、打开 Discuz! 论坛路径下的 \include\db_mysql.class.php 文件,在第 35 行有如下代码:
引用:
@mysql_query("SET character_set_connection=$dbcharset, character_set_results=$dbcharset, character_set_client=binary, character_set_database=latin1, character_set_system=$dbcharset", $this->link);
将其恢复为:
引用:
@mysql_query("SET character_set_connection=$dbcharset, character_set_results=$dbcharset, character_set_client=binary", $this->link);
至此就大功告成了。
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

扩展区汉字在论坛中不能正常搜索,据说UCS-4可以解决扩展区汉字的问题?

TOP

UCS-4 是指四个字节的 Unicode 编码(包括英文),每个字符都是四个字节。

UTF-8 则是可变的字节编码,英文是一个字节,基本汉字是三个字节,扩展汉字是五个字节。

这两者只是编码规范而已,事实上流行的是 UTF-8 编码格式,因为省空间(对英文而言),而且不会出现 \x00 字节,对旧的程序兼容性更强,不会被莫名截断。

两者理论上都是支持全部 Unicode 编码点(Code Point,32 位二进制代码)的,不能检索的原因可能还是 MySQL 的 Bug。
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

在MsSQL,也是不能直接查询扩展区汉字的

TOP

其实能够正常存取就能正常检索了,事实上刚刚我搜了一下“𠀀”就成功找到啦。如果你指的无法搜索是用 Google 搜索的话,那就更没问题啦。
附件: 您所在的用户组无法下载或查看附件
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

我查询“𠃇”字,即U+200C7 : CJK 统一汉字-200C7是查询不到的


但奇怪的是,如果我直接打开这个主题(从汉典网站http://www.zdic.net/zd/zi3/ZdicF0ZdicA0Zdic83Zdic87.htm
再次查询就查到了


另外,在MSSQL的查询窗口中,也是一样查不到扩展区的汉字
附件: 您所在的用户组无法下载或查看附件

TOP

我发现搜索中选择“汉字资料区”版块就能搜索到了,真奇怪啊。

至于 MySQL 查不到的原因就在于旧版的 MySQL 是以 latin1 编码方式存储的,所以不能直接搜索,要先在 EmEditor 中转换保存为 UTF-8 文件,然后再用“西欧”编码打开,以拉丁乱码的方式来查询……
复制内容到剪贴板
代码:
SELECT * FROM zi WHERE zi LIKE '𠃇';
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

呃……上面代码里的空格应该是 \xA0,发到网页上就自动变成 \x20 了。或者用下面的方式查询看看?
复制内容到剪贴板
代码:
SELECT * FROM zi WHERE zi LIKE 'ð%';
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

顶这样的态度

tantiancai的专业严谨的态度让我敬佩,加油哦。我看好你的。
心中有爱,做事认真!

TOP

经过更详细的测试,发现只要把 \include\db_mysql.class.php 文件中的以下代码删除即可:
引用:
@mysql_query("SET character_set_connection=$dbcharset, character_set_results=$dbcharset, character_set_client=binary", $this->link);
换句话说,就是不设置和数据库的连接编码,这样 MySQL 似乎默认就会以 Latin1 编码的方式存储了。
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

至于 GBK(Big5)版的 Discuz! 论坛显示扩展字符的方法可以参考:

http://www.pkucn.com/viewthread.php?tid=176085&page=1#pid1218157593

PS:感觉这个更安全,不用改数据库的。
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

要让论坛支持扩展字符除了对数据库做手脚之外,还有一个方法,那就是用 HTML 转义字符(HTML 实体,HTML Entity)来实现。 HTML 转义字符的含义和其他程序语言的转义字符的含义一致,都是通过若干普通字符的组合,来表示特殊字符。

比如:人,在 HTML 转义字符中可以写成:&#x4EBA;,其代码是十六进制 Unicode 编码。

要实现转义字符的正常显示,首先要修改 Discuz! 论坛 /include/global.func.php 文件的第 187 行:
引用:
$string = preg_replace('/&amp;((#(\d{3,5}|x[a-fA-F0-9]{4})|[a-zA-Z][a-z0-9]{2,5});)/', '&\\1',
将代码中的 5 改成 6,4 改成 5,这样就能支持全部 Unicode 字符的显示了。

[hr]分割线[/hr]

这样改了以后,理论上论坛就支持扩展字符了,可是问题是:一般人不太可能发帖的时候还去查某个扩展字符的 Unicode 编码,并写成转义字符的形式吧。这样就需要对 Discuz! 论坛程序本身再次进行修改,在用户发帖时通过 Javascript 脚本代码,自动将扩展字符转换成 HTML 转义字符。

打开 /include/javascript/post_editor.js 文件,在其中添加自动转换的脚本代码:
复制内容到剪贴板
代码:
function toentities()
{
        if(wysiwyg)
        {
                editdoc.body.innerHTML = TextToEntities(getEditorContents(), true);
        }
        else
        {
                editdoc.value = TextToEntities(getEditorContents(), false);
        }
}

function TextToEntities(str, andEntity)
{
        var strFull = new Array();
        var intLowChar;
        var strEntity;
        var strChar;
        var intChar;

    for (var i = 0; i < str.length; i++)
    {
                strChar = str.charAt(i);
                intChar = str.charCodeAt(i);

                //CodePoint=((HighSurr-0xD800)/64+1)*0x10000+(HighSurr-0xD800) mod 64*1024+LowSurr-0xDC00
                if(intChar >= 0xD800 && intChar <= 0xDBFF)
                {
                        intLowChar = str.charCodeAt(i + 1);
                        intChar = ((intChar - 0xD800) / 64 + 1) * 0x10000 + (intChar - 0xD800) % 64 * 1024 + intLowChar - 0xDC00;
                        i++;
                        if(andEntity)
                        {
                                strEntity = "&amp;#" + intChar + ";";
                        }
                        else
                        {
                                strEntity = "&#" + intChar + ";";
                        }
                        strFull.push(strEntity);
                }
                else
                {
                        strFull.push(strChar);
                }
        }
        return strFull.join("");
}
打开 templates/default/post_js.htm 文件(具体位置请按照实际使用的模板文件来修改),找到这样一行:
引用:
$('postform').onsubmit = function() {validate(this);if($('postsubmit').name != 'editsubmit') return false};
这个就是点击“发表”按钮后执行的脚本命令,可以在“validate(this);”这句话之前添加“toentities();”语句,执行转换函数,修改后的代码是这样的:
引用:
$('postform').onsubmit = function() {toentities();validate(this);if($('postsubmit').name != 'editsubmit') return false};
到此修改就结束了,重新打开浏览器试试看吧。

不得不说明一下:这样一来虽然发帖是正常了,可是其他要输入文字的地方还是不可以,比如搜索、短消息等等,如果要一个个改的话,我觉得不如改数据库来得干脆呢。

另外,如果启用“Html 代码”的话,那么在“所见即所得模式”下会把转义字符直接显示出来,解决办法有两个:

1、禁用“Html 代码”;

2、切换到“Discuz! 代码模式”然后再发布。
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

通过查找“短消息”“搜索”等代码,我发现用的脚本文件并不通用,所以要达到通用的目的,只能把那段脚本加在 common.js 里面,这样才能保证通用性,至于之前的帖子嘛……先无视吧。

在 common.js 里面加入以下代码(之前如果已经在 post_editor.js 文件添加了代码的话请删除,当时没有考虑通用性的问题):
复制内容到剪贴板
代码:
function toentities()
{
        if($("srchtxt"))        // search by text
                $("srchtxt").value = TextToEntities($("srchtxt").value, false);
        if($("srchname"))        // search by username
                $("srchname").value = TextToEntities($("srchname").value, false);
        if($("pm_textarea"))        // pm
                $("pm_textarea").value = TextToEntities($("pm_textarea").value, false);
        if($("subject"))        // thread subject
                $("subject").value = TextToEntities($("subject").value, false);
        if($("message"))        // thread content
                $("message").value = TextToEntities($("message").value, false);
        if($("tags"))        // tags
                $("tags").value = TextToEntities($("tags").value, false);
        if(document.getElementsByName("polloptions")[0])        //polloptions
                document.getElementsByName("polloptions")[0].value = TextToEntities(document.getElementsByName("polloptions")[0].value, false);
        if($("activityclass"))        // activityclass
                $("activityclass").value = TextToEntities($("activityclass").value, false);
        if($("activityplace"))        // activityplace
                $("activityplace").value = TextToEntities($("activityplace").value, false);
        if($("activitycity"))        // activitycity
                $("activitycity").value = TextToEntities($("activitycity").value, false);
        if($("counterdesc"))        // counter sellers
                $("counterdesc").value = TextToEntities($("counterdesc").value, false);
        if($("aboutcounter"))        // about counter
                $("aboutcounter").value = TextToEntities($("aboutcounter").value, false);
        if($("item_name"))        // sold item's name
                $("item_name").value = TextToEntities($("item_name").value, false);
        if($("item_locus"))        // sold item's location
                $("item_locus").value = TextToEntities($("item_locus").value, false);
        if($("affirmpoint"))        // affirm point
                $("affirmpoint").value = TextToEntities($("affirmpoint").value, false);
        if($("negapoint"))        // nega point
                $("negapoint").value = TextToEntities($("negapoint").value, false);
        if($("umpire"))        // umpire
                $("umpire").value = TextToEntities($("umpire").value, false);

        if(typeof(editdoc) != "undefined")        // thread
        {
                if(wysiwyg)
                {
                        editdoc.body.innerHTML = TextToEntities(getEditorContents(), true);
                }
                else
                {
                        editdoc.value = TextToEntities(getEditorContents(), false);
                }
        }
}

function TextToEntities(str, andEntity)
{
        var strFull = new Array();
        var intLowChar;
        var strEntity;
        var strChar;
        var intChar;

        for (var i = 0; i < str.length; i++)
        {
                strChar = str.charAt(i);
                intChar = str.charCodeAt(i);

                //CodePoint=((HighSurr-0xD800)/64+1)*0x10000+(HighSurr-0xD800) mod 64*1024+LowSurr-0xDC00
                if(intChar >= 0xD800 && intChar <= 0xDBFF)
                {
                        intLowChar = str.charCodeAt(i + 1);
                        intChar = ((intChar - 0xD800) / 64 + 1) * 0x10000 + (intChar - 0xD800) % 64 * 1024 + intLowChar - 0xDC00;
                        i++;
                        if(andEntity)
                        {
                                strEntity = "&amp;#" + intChar + ";";
                        }
                        else
                        {
                                strEntity = "&#" + intChar + ";";
                        }
                        strFull.push(strEntity);
                }
                else
                {
                        strFull.push(strChar);
                }
        }
        return strFull.join("");
}
然后是 post.js 文件,在 function ctlent(event) 的 if 语句后面加上 toentities(); 函数,修改后的代码如下:
复制内容到剪贴板
代码:
function ctlent(event) {
        if(postSubmited == false && (event.ctrlKey && event.keyCode == 13) || (event.altKey && event.keyCode == 83) && $('postsubmit')) {
                toentities();
                if(in_array($('postsubmit').name, ['topicsubmit', 'replysubmit', 'editsubmit', 'pmsubmit']) && !validate($('postform'))) {
                        …………
接着改以下文件(第一行为原来的代码,第二行为修改后的代码):
// global.func.php
复制内容到剪贴板
代码:
$string = preg_replace('/&amp;((#(\d{3,5}|x[a-fA-F0-9]{4})|[a-zA-Z][a-z0-9]{2,5});)/', '&\\1',
$string = preg_replace('/&amp;((#(\d{3,6}|x[a-fA-F0-9]{5})|[a-zA-Z][a-z0-9]{2,5});)/', '&\\1',
// post_js.htm
复制内容到剪贴板
代码:
$('postform').onsubmit = function() {validate(this);if($('postsubmit').name != 'editsubmit') return false};
$('postform').onsubmit = function() {toentities();validate(this);if($('postsubmit').name != 'editsubmit') return false};
// pm_send.htm
复制内容到剪贴板
代码:
<form method="post" id="postform" action="pm.php?action=send&pmsubmit=yes" onSubmit="return validate(this)">
<form method="post" id="postform" action="pm.php?action=send&pmsubmit=yes" onSubmit="toentities();return validate(this)">
// search.htm
复制内容到剪贴板
代码:
<form method="post" action="search.php" {if $qihoo['status']}onSubmit="if(this.srchtype[0].value=='qihoo' && this.srchtype[0].checked) this.target='_blank'; else this.target=''; return true;"{/if}>
<form method="post" action="search.php" onSubmit="toentities();{if $qihoo['status']}if(this.srchtype[0].value=='qihoo' && this.srchtype[0].checked) this.target='_blank'; else this.target=''; return true;{/if}">
// pm_search.htm
复制内容到剪贴板
代码:
<form method="post" onSubmit="if(this.srchtype[0].value=='qihoo' && this.srchtype[0].checked) this.target='_blank'; else this.target=''; return true;">
<form method="post" onSubmit="toentities();if(this.srchtype[0].value=='qihoo' && this.srchtype[0].checked) this.target='_blank'; else this.target=''; return true;">
// forumdisplay.htm
复制内容到剪贴板
代码:
<form method="post" id="postform" action="post.php?action=newthread&amp;fid=$fid&amp;extra=$extra&amp;topicsubmit=yes" onSubmit="return validate(this)">
<form method="post" id="postform" action="post.php?action=newthread&amp;fid=$fid&amp;extra=$extra&amp;topicsubmit=yes" onSubmit="toentities();return validate(this)">
// viewthread.htm
复制内容到剪贴板
代码:
<form method="post" id="postform" action="post.php?action=reply&amp;fid=$fid&amp;tid=$tid&amp;extra=$extra&amp;replysubmit=yes" onSubmit="return validate(this)">
<form method="post" id="postform" action="post.php?action=reply&amp;fid=$fid&amp;tid=$tid&amp;extra=$extra&amp;replysubmit=yes" onSubmit="toentities();return validate(this)">
// viewthread_fastreply.htm
复制内容到剪贴板
代码:
<form method="post" id="postform" action="post.php?action=reply&amp;fid=$fid&amp;tid=$tid&amp;extra=$extra&amp;replysubmit=yes" onSubmit="return validate(this)">
<form method="post" id="postform" action="post.php?action=reply&amp;fid=$fid&amp;tid=$tid&amp;extra=$extra&amp;replysubmit=yes" onSubmit="toentities();return validate(this)">
// post_editpost_activity.htm
复制内容到剪贴板
代码:
<form method="post" id="postform" action="post.php?action=edit&extra=$extra&editsubmit=yes&mod=$mod" $enctype onSubmit="return validate(this)">
<form method="post" id="postform" action="post.php?action=edit&extra=$extra&editsubmit=yes&mod=$mod" $enctype onSubmit="toentities();return validate(this)">
这些代码大多都是加入了提交时的转换函数,这样在提交时就自动会转化了。
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

这样改了以后就“短消息”(短消息搜索后显示有问题,不过搜索没问题)“搜索”“发帖”都支持扩展字符了。

不过没办法注册成扩展字符的用户名,这个是系统限制,不是改脚本就可以解决的了。

另外,上传附件的“描述”也不支持扩展字符。
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

更正

由于原帖不能再编辑了,所以只能写在了这里。

在 global.func.php 文件中,应该将以下代码:
引用:
$string = preg_replace('/&amp;((#(\d{3,5}|x[a-fA-F0-9]{4})|[a-zA-Z][a-z0-9]{2,5});)/', '&\\1',
改成如下形式:
引用:
$string = preg_replace('/&amp;((#(\d{3,7}|x[a-fA-F0-9]{4,6})|[a-zA-Z][a-z0-9]{2,5});)/', '&\\1',
为避免论坛自动转换造成的误解,在此上传附件,按照附件的说明进行更改后,就能以 HTML 转义字符的方式正常显示扩展字符了。


[ 本帖最后由 tantiancai 于 2008-1-14 14:58 编辑 ]
附件: 您所在的用户组无法下载或查看附件
看了柯南想学日语,听了“約束の地”想学拉丁语。目前最大的愿望是用标准的梵(fàn)语念“般若(bōrě)波罗蜜多心经”。哈哈,我果然是天才。对,就是天才!

TOP

 21 12
发新话题