网页编码基础

四种编码

1.      网页文件存储时使用的编码,比如我们使用vim作为编码时,可以通过设置fileencoding属性来设置存储的文件使用的编码,set filetype=utf-8,即将文件保存为UTF-8编码。

2.      meta标签当中设置的编码,有http-equiv=”Content-Type”content=”text/html; charset=utf-8”这样的属性,则表示设置当前页的编码为utf-8。

3.      浏览器查看时使用的编码,即浏览器当中的查看=>为编码选项当中选中的编码。

4.      浏览器端提交用户数据时使用的编码。

 

原理

1.      网页文件存储编码,是网页最为重要的编码。如果网页文件为静态的HTML文件,则Web Server将直接发送该文件至客户端的浏览器;如果网页文件为动态生成的HTML文件,则Web Server会根据动态脚本文件存储的编码来生成相应编码的数据,而这些数据将成为发送到Client Browser的HTML文件。

 

例如在一个以gbk编码存放的PHP脚本当中,使用echo ‘我爱你’,则会产生数据CE D2 B0 AE C4 E3六个字节的数据,这六个字节的数据是‘我爱你’的GBK编码,而如果在一个以utf-8编码存放的PHP脚本当中,执行echo ‘我爱你’,则会产生数据E6 88 91 E7 88 B1 E4 BD A0九个字节的数据,这九个字节的数据是‘我爱你’的UTF-8编码。

 

2.      HTML 4.01 Specification当中说明:在META标签的Content-Type值当中使用的charset用于表明当前是传送的HTML文档的编码,并且说明一个conforming的browser正确的处理这个属性。但是实际上的情况是,大部分的browser并不会把这个属性当会事,经测试Firefox 10, Chrome 17并不会follow这个属性,所以会出现一个明明是UTF-8编码的HTML文件,并且在meta charset当中设计成为了UTF-8,但是还是会出现乱码的原因。现在(2012-4-2)作出的测试(本地,使用FILE或者HTTP协议打开)发会现browser(IE 9.0+FF11+Chrome17)会按照这个属性来解析HTML文件。

 

3.      浏览器查看编码是浏览器将Web Server传输过来的数据解码来使用的编码。出来乱码的原因即在于此,如果一个HTML发过来的是GBK编码的,而Web Browser使用UTF-8去解码这个文件,那么如果该文件当中含有中文等字符,则会产生乱码。

 

4.      经过测试发现,浏览器端提交用户数据时使用的编码,只取决于当前浏览器查看网页使用的编码,与HTML网页本身的文件的编码没有任何关系。

 

总结

服务端传输过来的HTML文件的编码主要由服务端HTML文件或者脚本文件的存储编码决定,浏览器端传输的数据的编码只由浏览器端的查看编码决定。 
另外,HTTP包头当中的Content-Type属性当中的charset也可以表明服务端传输的数据的编码,但是一般的Web Server并不会发送这个charset属性。
最后,一般的现代的浏览器都具备一个编码自动检查功能,浏览器会根据当前收到的数据来检查编码。


应用

理解为什么php的move_uploaded_file有时候不支持中文文件名?

问题再现

如果存在这样一个图片上传的后台处理模块,

<?php
//这里假设客户端是以UTF-8编码发送的数据,即查看上传文件网页时,使用的是UTF-8
$old_name = $_FILES['file']['name']; 
$new_name = 'e:\web\\' . $old_name;
;move_uploaded_file($_FILES['file']['tmp_name'], $new_name);
move_uploaded_file($_FILES['file']['tmp_name'], 'e:\web\study\哈哈.jpg');
?>

如果该模块(upload.php)使用UTF-8作为文件存储编码,那么代码中的两种上传文件的方式都应该避免,最好是能够随机生成上传后的新文件名,并且新生成的文件名最好不包括中文。

因为:

第一,一般的Web Server(如httpd)并不能够很好的处理包括中文的路径名处理,也就是说其实apache httpd对于上传上来的utf-8编码的文件名并不能够作出正确的处理,其结果是$_FILES['file']['name']会出现乱码。这样,首先旧文件名就是错的。

其次,php的move_uploaded_file并不能够根据当前的文件存储编码来处理文件路径。move_uploaded_file会把转入的参数作为当前locale编码来处理(即GBK),这样的结果就是再次出现乱码。

例如 'e:\web\study\哈哈.jpg'本身作为UTF-8的编码是

65 3a 5c 77 65 62 5c 73 74 75 64 79 5c e5 93 88 e5 93 88 2e 6a 70 67

而move_uploaded_file会将其作为GBK编码来看待,其结果就是"e:\web\study\鍝堝搱.jpg",这样的结果,因为e5 93是'鍝',接下来'堝'是88 e5,最后'搱'是93 88,也就是说'哈哈'的六个UTF-8字节,被解释为三个GBK编码的汉字。其结果就是上传的文件变成了"e:\web\study\鍝堝搱.jpg",而不是原本的哈哈.jpg。更有时候,会出现UTF-8编码的字不能够被解释为GBK时,系统会默认的添加一个?号作为默认字符,其结果就是上传失败。

所以,如果你一定想使用中文文件名,那么在UTF-8编码存储的PHP文件当中,一定要先使用iconv将utf-8编码的路径转换为当前locale(gbk)编码,然后再调用move_uploaded_file。

深入探索

那么为什么move_uploaded_file会将utf-8编码的文件路径,认为是gbk编码的呢?这是不是PHP的一个bug呢?

我们来实际看一下php的源代码,

/* {{{ proto bool move_uploaded_file(string path, string new_path)
   Move a file if and only if it was created by an upload */
PHP_FUNCTION(move_uploaded_file)
{
	char *path, *new_path;
	int path_len, new_path_len;
	zend_bool successful = 0;

#ifndef PHP_WIN32
	int oldmask; int ret;
#endif

	if (!SG(rfc1867_uploaded_files)) {
		RETURN_FALSE;
	}

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &path, &path_len, &new_path, &new_path_len) == FAILURE) {
		return;
	}

	if (!zend_hash_exists(SG(rfc1867_uploaded_files), path, path_len + 1)) {
		RETURN_FALSE;
	}

	if (PG(safe_mode) && (!php_checkuid(new_path, NULL, CHECKUID_CHECK_FILE_AND_DIR))) {
		RETURN_FALSE;
	}

	if (php_check_open_basedir(new_path TSRMLS_CC)) {
		RETURN_FALSE;
	}

	if (strlen(path) != path_len) {
		RETURN_FALSE;
	}

	if (strlen(new_path) != new_path_len) {
		RETURN_FALSE;
	}

	VCWD_UNLINK(new_path);
	if (VCWD_RENAME(path, new_path) == 0) {
		successful = 1;
#ifndef PHP_WIN32
		oldmask = umask(077);
		umask(oldmask);

		ret = VCWD_CHMOD(new_path, 0666 & ~oldmask);

		if (ret == -1) {
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "%s", strerror(errno));
		}
#endif
	} else if (php_copy_file_ex(path, new_path, STREAM_DISABLE_OPEN_BASEDIR TSRMLS_CC) == SUCCESS) {
		VCWD_UNLINK(path);
		successful = 1;
	}

	if (successful) {
		zend_hash_del(SG(rfc1867_uploaded_files), path, path_len + 1);
	} else {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to move '%s' to '%s'", path, new_path);
	}

	RETURN_BOOL(successful);
}
/* }}} */

可以看到做主要工作的是VCWD_RENAME,

#define VCWD_RENAME(oldname, newname) virtual_rename(oldname, newname TSRMLS_CC)
而virtual_name,

CWD_API int virtual_rename(char *oldname, char *newname TSRMLS_DC) /* {{{ */
{
	cwd_state old_state;
	cwd_state new_state;
	int retval;
	int cch, cb;
	LPWSTR wstr;
	LPSTR mbstr;

	CWD_STATE_COPY(&old_state, &CWDG(cwd));
	if (virtual_file_ex(&old_state, oldname, NULL, CWD_EXPAND)) {
		CWD_STATE_FREE(&old_state);
		return -1;
	}
	oldname = old_state.cwd;

	CWD_STATE_COPY(&new_state, &CWDG(cwd));
	if (virtual_file_ex(&new_state, newname, NULL, CWD_EXPAND)) {
		CWD_STATE_FREE(&old_state);
		CWD_STATE_FREE(&new_state);
		return -1;
	}
	newname = new_state.cwd;

	/* rename on windows will fail if newname already exists.
	   MoveFileEx has to be used */
#ifdef TSRM_WIN32
	/* MoveFileEx returns 0 on failure, other way 'round for this function */


	retval = (MoveFileEx(oldname, mbstr, MOVEFILE_REPLACE_EXISTING|MOVEFILE_COPY_ALLOWED) == 0) ? -1 : 0;
	if (retval == -1) {
		php_error_docref(NULL TSRMLS_CC, 2, "movefileex failed");
	}
#else
	retval = rename(oldname, newname);
#endif

	CWD_STATE_FREE(&old_state);
	CWD_STATE_FREE(&new_state);

	return retval;
}
/* }}} */

可以看到最终move_uploaded_file是调用MoveFileEx来实现文件的上传,也就是将UTF-8编码的路径名当成GBK编码来处理是的MoveFileEx函数,那么为什么会出现这种结果呢?

因为PHP默认的所有Windows API的调用都是使用的ANSI版本,也就是说MoveFileEx即MoveFileExA,其参数自然就是GBK编码的字符串(最终系统通过MultiByteToWideChar来将GBK编码的字符串转换成为UTF-16 LE字符串,来调用MoveFileExW)。

所以,如果要在底层通过硬编码来解决这个问题,可以在MoveFileEx之前添加如下代码:

	/* first convert utf-8 to utf16-le */
	cch = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)newname, strlen(newname) + 1, NULL, 0);
	wstr = (LPWSTR)malloc(cch * sizeof(wchar_t));
	MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)newname, strlen(newname) + 1, wstr, cch * sizeof(wchar_t));

	/* then convert utf16-le to gbk */
	cb = WideCharToMultiByte(CP_ACP, 0, wstr, cch, NULL, 0, NULL, NULL);
	mbstr = (LPSTR)malloc(cb);
	WideCharToMultiByte(CP_ACP, 0, wstr, cch, mbstr, cb, NULL, NULL);
	free(wstr);


将UTF-8编码的字符串,转换为GBK。

从这里可以看出来,在PHP或者其他系统当中尽量不要使用中文作为文件名称(如果可以的话),因为中文编码的文件操作很容易出现兼容性问题。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值