ASP.NET乱码深度剖析

写在前面

在Web开发中,乱码应该算一个常客了。今天还好好的一个页面,第二天过来打开一看,中文字符全变“外星文”了。有时为了解决这样的问题,需要花上很长的时间去调试,直至抓狂,笔者也曾经历过这样的时期。有时虽然是“侥幸”解决了,但对其中的原理却一知半解。

为了弄清楚这个问题,今天查了大半天的资料、测试。现把这些点滴记录下来,以激励自己重视基础,同时和大家分享一下,望大家不吝批评指正。

预备知识

先介绍一些字符编码方面的基本知识,如果你对这些已经比较了解了,请直接跳过此节。

1.       字符集与字符编码概述

简单来说,字符集就是与特定区域相关的一系列有效字符的有序集合,比如字母、数字、标点符号等。注意关键字“有序”,表明集合中的每一个字符都是具有唯一数字编号(码值)的。不同国家使用的语言文字、符号不一样,相应的字符集必定也不一样。比如中国使用汉字,美国使用英语,韩国使用韩文,等等。

字符集是为了信息交互而设计的,最终还是要转化成计算机的表示法。我们知道,计算机只认识0和1,它对字符集符号不感冒。所以,我们必须想办法把字符转化为0和1的序列。我们知道,计算机最小的存储单位是位(bit),程序中一般使用的最小单位是字节(byte)。为了把字符存储到计算机中,我们就要考虑用几个byte几个bit,考虑每一个bit上是0还是1,考虑存储和读取效率,并且必须兼顾整个字符集,这就是字符编码

一句话,字符集只关心字符的定义,而字符编码负责字符的存储和读取细节。用三层模式来打比喻的话,字符集是模型层,而字符编码是业务层。注意:一般常说的GB2312、GBK等其实同时包含了这两方面的定义

2.       常用中文字符编码简介

GB2312

GB2312的全称是《信息交换用汉字编码字符集-基本集》,由国家标准总局于1980发布,1981年5月1日施行,中国大陆、新加坡使用此编码。基本集收录了6763个汉字,只能显示简体汉字。

GBK

1995颁布,全称是《汉字编码扩展规范》。在GB2312的其他上,增加了繁体汉字,支持ISO/IEC 10646-1 和GB-13000-1的全部中、日、韩(CJK)字符,共20902个。向下兼容GB2312。

GB18030

全称是《信息交换用汉字编码字符集基本集的扩充》,目前两个版本,分别于2000年和20005年颁布。该字符集收录了70000多个汉字,包括了藏、蒙古、维吾文等少数民族字符,是我国计算机系统必须遵循的基础性标准之一。向下兼容GBK和GB2312。

BIG5

台湾和港台地区使用的汉字编码,俗称“大五码”,共收录了13060个汉字。

UTF-8

这是目前使用最多的一种Unicode编码,是Visual Studio内置的编码,相信大家一定都不陌生。根据字符码值的不同,可能用1、2、3个字节表示。

注意,编码之间一般都不是兼容的。其它编码在此不作介绍,若想进一步了解字符编码,请看我收藏的一篇文章:http://blog.csdn.net/tomysea/article/details/6712344

3.       字符串、字符数组和字节数组

C#中的字符串(string)和字符(char)其实都是对象,他们有相应的类String和Char,string和char只是这两个类的一别名而已,内部都是采用Unicode码值表示。请注意我说的是码值,不是编码。

我们已经知道,Unicode的字符大多是多字节表示的,那么一个char就得用几个byte来表示。这里我要说的重点是,使用不同的编码表示字符串,其对应的byte可能是不一样的。请看下面的代码,注意输出字节数部分。UTF-8编码的字节数是22,而GB2312编码的字节数是16。

string title = "2012真的来了吗?";      //字符串

char[] chars = title.ToCharArray();     //字符数组

byte[] bytes = System.Text.Encoding.UTF8.GetBytes(title);

Response.Write(chars.Length + " ");     //10  (字符数)

Response.Write(bytes.Length + " ");     //22  (UTF-8编码的字节数)

bytes = System.Text.Encoding.GetEncoding("GB2312").GetBytes(title);

Response.Write(bytes.Length + " ");     //16  (GB2312编码的字节数)

从http请求响应模型说起

http是一个请求/响应的模型,这个我们大家都知道。http请求可以分为请求头和请求实体两部分,相应地http响应也可以分为响应头和响应实体。请求头或响应头是浏览器与Web服务器通信用的(假定用浏览器访问Web服务器),而实体则是实际发送的数据,比如Form表单的数据、Ajax提交的数据、传回来的html代码等。不管是浏览器还是Web服务器,在发送实体前都会把它转换为字节流。明白这一点很重要,因为涉及字节流就一定会与字符编码有关。

从上面的请求响应模型中我们可以得出一个结论:请求和响应编码必须严格保持一致!为什么呢?这很好理解,浏览器和Web服务器是要通信的,如果编码不一样的话,势必会造成许多“误解”。假设浏览器是中国人(不懂E文),而Web服务器是美国人,他们两个的“编码”(语言)不一致,悲催的结局不言而喻。

ASP.NET中请求响应编码的设置

你可以在machine.config或web.config文件指定全局配置,也可以在页面级特别指定。如果你未手动指定且machine.config中也为空,则默认会读取计算机上“区域选项”中的设置。

1.       全局配置

在machine.config或web.config文件(根目录或者子目录都有效)中的system.web节点中配置globalization节点。如果在根目录下的web.config配置,则会响应整个网站,若只是在子目录下配置,则只会响应该目录及其子目录。 详细配置如下:

<system.web>

<globalization fileEncoding="utf-8" requestEncoding="utf-8" responseEncoding="utf-8"/>

<!--按顺序是:文件编码 请求编码 响应编码-->

<!—-fileEncoding会在后面说到-->

<!--后面还有其它配置-->

2.       页面级的配置

在aspx页面的Page指令中设置响应编码

<%@ Page Language="C#" AutoEventWireup="true" ResponseEncoding="utf-8"

CodeBehind="byte.aspx.cs" Inherits="DevKit.Web.test.charset._byte" %>

在aspx页面中手动指定meta标签

<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />

在后台cs文件中配置

Request.ContentEncoding = System.Text.Encoding.UTF8;    //请求编码

Response.ContentEncoding = System.Text.Encoding.UTF8;   //响应编码   接下来,我们从几个示例中去体验乱码,从而总结出解决乱码的一般方法。

测试环境

操作系统:Windows XP Professional SP3 雨林木风版

开发环境:Visual Studio 2008 专业版 + SP1(.NET 3.5)

Web容器:VS集成的Development Server

浏览器:IE8 、FireFox 5

实例分析与研究

实例1  aspx页面提示意外的字符“XXX”,引号里面是乱码

背景

网站配置了在根目录配置了文件、请求、响应编码都为utf-8,页面成功编译,没有任务错误。详细错误见下图:

html代码

<div>

aspx页面中的中文

<br />后台的中文变量:<em><%=汽车%></em>

</div>

后台代码

view plain

public partial class _byte : System.Web.UI.Page

{

protected string 汽车= "我是凯迪拉克"; //别怀疑,中文变量是可以的:)

protected void Page_Load(object sender, EventArgs e)

{

//...

}

}

分析与解决

既然web.config已经配置了一样的请求响应编码,而且页面级别也没设置,可以排除这方面的问题了。注意到文件编码是UTF-8,会不是会文件编码引起的呢?(提示:这里的文件编码指的是保存文件时指定的编码,点击“另存为…”,在弹出的窗口中选择“编码保存”可以看到)。果然,此aspx页面的保存编码为GB2312,与web.config文件不一样,把它修改为UTF-8。

小提示:UTF-8有两种编码:UTF-8(带签名)和UTF-8(无签名)。带签名的UTF-8会在文件的开头写入“EF BB BF”(16进制),以标示自己采用的编码格式,这个标志称为BOM(Byte Order Mark),即字节序。打个比方,UTF-8(带签名)戴了校徽的学生,就算不认识他的人,一看校徽就明白了;而UTF-8(无签名)则是没戴校徽的。这里的校徽就是我们说的BOM,一个能够表明自己身份的标志。

小结

这是因为文件的保存编码与当前网站指定的文件编码不一致引起的,所以最佳实践是:手动在web.config中指定文件编码,并确保所有页面的保存编码与web.config一致。

其实最容易出这种问题的是js和css文件,如果你用其它工具(比如DreamWeaver)来编写这些文件却采用不同的编码保存,一旦文件包含中文就可能出这样的错误,导致js脚本错误,css无效!

 

实例2    跨页post提交时接收的Form数据变成了乱码

背景

有两个页面,注册页面(register.html)和处理注册的页面(handle.aspx),注册页面的表单信息以post方式提交到handle.aspx。根目录的配置的文件编码、请求编码和响应编码都是UTF-8。

register.html页面的关键html

<head>

<title></title>

<meta http-equiv="Content-Type" content="text/html;charset=gb2312" />

</head>

<body>

<form id="form1" name="form1" action="handle.aspx" method="post">

<input type="text" id="txtName" name="txtName" />

<input type="submit" id="btnSubmit" value="Post" />

</form>

handle.aspx页面关键后台代码

view plain

protected void Page_Load(object sender, EventArgs e)

{

string name = Request.Form["txtName"];

Response.Write(name);

}

错误信息如下

分析与解决

这是在提交表单信息过程产生的乱码,这里就涉及http请求和http响应的编码问题。我们上面说过,在请求时浏览器会把表单信息按指定编码转化成字节流发向Web服务器,在服务器ASP.NET会把这些字节流按指定的编码解码,以取得表单信息。那么我们就要检查这两个页面的编码了。仔细检查之后,发现register.html有这么一行“<meta http-equiv="Content-Type" content="text/html;charset=gb2312" />”,这里手动指定了页面编码为GB2312。问题很有可能就出在这里了,把本行删除之后,handle.aspx页面成功接收到表单信息。

没错,这就是由于两个页面的编码不一样引起。让我们再深入一点,仔细看看问题是怎么一步一步产生的吧。register.html的编码为GB2312,当我们点击了“Post”按钮时,浏览器会把“我是中文”这几个字按GB2312的方式编码成字节流,然后提到到handle.aspx页面。handle.aspx没有手动指定编码,那么他将会采用web.config里面的配置,为UTF-8。它收到请求后,用UTF-8编码解码字符流。由于请求用的是GB2312,而接收用的却是UTF-8,这样就导致乱码的产生。通过下面这幅图可以看到这个过程。

小结

所有的页面(不管是aspx,还是html,或其它)都必须使用相同的编码。如果涉及跨页提交,不管是get还是post,更应该严格保持相关页面编码的一致性。特别是跨站点提交时,更应该注意!

实例3  cookie存取发生乱码

背景

这是一个旧项目,现在决定增加一个自动登录的功能。详细过程是这样的:

在登录页面,用户登录成功后把用户名写到cookie中。这样,当用户再次访问时,就可以根据cookie判定用户是否已登录,从而实现自动登录。

登录成功后cookie是这样保存的

view plain

string userName = "cookie大侠";     //待保存的用户名

userName = HttpUtility.UrlEncode(userName);     //编码特殊字符,如中文

HttpCookie cookie = new HttpCookie("userName", userName);

Response.Cookies.Add(cookie);

判断用户是否已登录时,代码是这样的

view plain

string userName = Request.Cookies["userName"].Value;

userName = Server.UrlDecode(userName);

Response.Write(userName);    //总是获取不到cookie,所以决定打印出来看看

结果在测试读取cookie的时候,页面输出了乱码,如下图:

分析与解决

全球化信息是这样配置的

<globalization fileEncoding="utf-8" requestEncoding="gb2312" responseEncoding="gb2312"/>

所有页面的保存编码都为UTF-8,请求响应编码是GB2312。再次声明,这是一个旧项目,任何改动都必须向后兼容。

首先从全球化配置里看到三种编码不一致,初步怀疑是这里引发的问题。尝试把请求、响应编码都修改为UTF-8,再次运行页面,乱码消失了。窃喜,小样,原来问题就在这里。但是,这样一来,在其它很多页面中却莫名其妙出现了乱码。这…,心里好不容易生起的一股小火,却被这样无情的浇灭了。冷静地回忆了下,自己只改了请求响应编码,其它地方没动过啊。于是改回来原来的GB2312,其它页面运行也正常了。如果把编码改为UTF-8的话,就不能兼容以前的页面,且会导致一连串的问题,全部修改将是一个非常艰巨的任务。

仔细检查了几遍所有页面的编码,都没有手动设置过,那应该都是读取配置文件的GB2312。新功能急于上线,交期一秒一秒狠狠地砸着绷紧的神经。怎么办呢?难道是GB2312不支持cookie存取吗?搜索了大量资料后,也没有发现什么端倪,感觉这也不太可能,毕竟中国这么多GB2312的网站…。

现在可以确定的是编码没有任何问题!那问题会出现在哪里呢?是自己写的代码有问题吗?仔细检查了之后,就发现了一点:

userName = HttpUtility.UrlEncode(userName);     //编码特殊字符,如中文
userName = Server.UrlDecode(userName);

红色部分不一样,从智能提示中可以看到这样的说明。

原来,两个调用的是不同类的方法。一个是HttpUtility的方法,另一个是HttpServerUtility的方法,不小心还真看不出来。于是把Server.UrlEncode()换成了HttpUtility.UrlEncode(),重新运行测试页面,页面正常显示。既然都已经到这里了,我们不防看看这两个方法的实现细节有哪些差异吧。打开Reflector,找到System.Web.HttpUtility中的UrlEecode方法。嘿嘿,终于被我发现了这样一个片段(我把反射后的代码加上了注释):

//这是HttpUtility的UrlEncode方法

public static string UrlEncode(string str)

{

if (str == null)

{

return null;

}

return UrlEncode(str, Encoding.UTF8);   //默认采用UTF-8编码

}

接着看看HttpServerUtility.UrlDecode()方法,Page.Server其实是HttpServerUtility的一个实例,但它并不是在Page类中实例化的,而是在HttpContext中。

view plain

//HttpServerUtility中的UrlDecode方法

public string UrlDecode(string s)

{

//注意这里的差异,会优先使用context中的编码,

//也就是我们配置了的GB2312

Encoding e = (this._context != null) ?

this._context.Request.ContentEncoding : Encoding.UTF8;

return HttpUtility.UrlDecode(s, e);

}

这下总算明白为什么了,存cookie时调用的是HttpUtility.Encode()方法,将以UTF-8编码。而读取时调用的是HttpServerUtility的Decode()方法,它会根据当前上下文采用GB2312方法,自然无法正确解析UTF-8编码的字符串了。

小结

在调用方法时,要成对调用。比如编码时调用的是HttpUtility.UrlEncode(),那么在解码时你就必须调用HttpUtility.UrlDecode(),保持这种一致性,有利于减少错误的发生。

必须充分考虑代码的向后兼容性。

如果你有兴趣,去看看微软是怎么实现这些方法的吧,这样对你的帮助会很大。

实例4   jQuery Ajax请求传中文参数导致乱码

背景

老项目(实例3提到的)的需求又来了,大致要求是这样的:在前台页面中,要根据当前商品的名称去异步获取它的详细说明(当然了,一般是按id等主键获取的,这里我只是做一个假设),当用户点击时就显示。于是决定用jQuery 的Ajax去做,简单方便且功能强大。由于jQuery的易用性,代码一下子就写好了,后台采用ashx处理ajax请求,先看看是怎么实现的吧。

前台页面的代码

view plain

<head runat="server">

<title>产品列表页</title>

<script src="../../js/jquery-1.4.2.js" type="text/javascript"></script>

<script type="text/javascript">

$(function() {

$(‘#product‘).click(function() {

var productName = this.innerHTML;   //产品名称

$.get(‘getInfo.ashx‘, { name: productName }, function(description) {

alert(description); //显示详细说明

});

});

});

</script>

</head>

<body>

<form id="form1" runat="server">

<div>

<a id="product" href="javascript:void(0);">奋斗牌牙膏</a>

</div>

</form>

</body>

ashx的关键处理代码

view plain

string name = context.Request.QueryString["name"];

context.Response.Write(name + ": ");   //调试用,看参数传递是否正确

if (name != null && name.Trim() == "奋斗牌牙膏")

{

context.Response.Write("奋斗牌,你懂的!\n每天一点点,强身健体,天气再冷,牙也不颤!");

}

但不幸的是,运行页面时又出乱码了,无法正确获取产品名称。

分析与解决

项目的所有配置还是和例3一样,请求响应编码都是GB2312。首先也是尝试把web.config文件的请求编码改为UTF-8,运行页面,可以正常显示。但这样改肯定是不行的,必须考虑其它页面的兼容性。有了例3的经验,现在已经知道问题出现在哪个部分了。必定是请求的编码和解析请求的编码不一致产生的!现在的重点是找出产生这个不一致的原因。

仔细检查了产品页面的编码,没任何任何与编码相关的设置,所以这个页面肯定也是用web.config中的请求编码GB2312。在ashx中也没有设置,它肯定也是用GB2312来解析请求。理论上应该不会出现乱码的啊。

为了能看清楚细节,打开抓包工具Fiddler,监测Ajax请求,看到的请求头是这样的。

可以看到,$.get()方法自动把请求参数附加到url里,并且实施了url编码。所以我们就得$.get()这个方法入手,看看请求参数是如何被附加到url中的。打开jquery.1.4.2.js,一步一步查找,发现了这样一个方法:

function add( key, value ) {

// If value is a function, invoke it and return its value

value = jQuery.isFunction(value) ? value() : value;

s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value);

原来是对参数调用了encodeURIComponent()方法来进行url编码的,但这个方法的实现细节是看不到的,不像.NET里可以反编译。在网上搜索了很多资料,但很少有资料提到它的工作细节。

好吧,我们就自己实践一下吧。还是用“奋斗牌牙膏”这几个字来测试,先看看用不同的字符编码来对它实行url编码后产生的字符串是什么吧。下面是我的测试结果:

把这些结果和Fiddler的抓包结果对比一下(看url中的参数),发现当采用UTF-8进行UrlEncode时,两者的结果是一致的。

可以确定encodeURIComponent()是采用UTF-8编码来进行url编码的。不仅仅是get方法,jQuery实现的所有ajax方法都是一样的,采用UTF-8字符编码对参数进行url编码。到这里,问题产生的原因已经很明朗了,请求的字节流采用了UTF-8编码,而服务器端的ASP.NET却采用GB2312来解析,肯定解析不到了。

因此,我们可以在ASP.NET中手动指明解析请求的编码。修改后的ashx代码如下:

context.Request.ContentEncoding = System.Text.Encoding.UTF8; //指明Request使用的编码是UTF-8

string name = context.Request.QueryString["name"];

context.Response.Write(name + ": ");   //调试用,看参数传递是否正确

if (name != null && name.Trim() == "奋斗牌牙膏")

{

context.Response.Write("奋斗牌,你懂的!\n每天一点点,强身健体,天气再冷,牙也不颤!");

}

再次点击页面,成功返回所需的内容。

总结

使用jQuery的ajax方法时,一定要记得它是采用UTF-8编码数据的。

把http的请求响应过程弄清楚。

通过上面4个例子,你应该对乱码产生的原因有所了解了吧。记住一点,最根本的原因是字符编码不一致产生的。然后顺着这条线索,顺藤摸瓜,一步一步把确切的原因找出来。方法很重要,你不应该为了得到了答案而高兴,而应该真正弄懂问题产生的原因,这样你才能真正成长。这时体会一下“授人以鱼不如授人以渔”这句话的魅力吧。

后记

其实问题真的不可怕,适当地来些问题,来些压力,会让你更好地成长。不要只是想得到答案,更重要的是积累获得答案的方法。

写这篇文章的过程中,遇到了不少问题,感谢那么多的前辈们分享了他们的经验,让我得以站在他们的肩膀上。

 

参考资料:

字符集与字符编码简介:http://www.2cto.com/kf/201110/109312.html

HTTP 请求报头详解:http://www.2cto.com/kf/201110/109311.html

encodeURIComponent()导致乱码解决:http://www.2cto.com/kf/201110/109310.html

unicode编码 http://baike.baidu.com/view/40801.htm

摘自 tomysea的专栏

时间: 2024-10-20 03:57:42

ASP.NET乱码深度剖析的相关文章

[ 1011] &lt;&lt;C语言深度剖析&gt;&gt; 测试 TEST

/******************************** **  wzsts<C语言深度剖析>2016 ** **                    ** **     fun1~fun6代表6章节内容 ** **fun10~fun19代表fun1所调用函数 ** **                    ** **     世界因规则而美好     ** ** #if(1)可运行,#if(0)不运行. ** ** Data  Author   PC       ** **16

横瓜先生深度剖析QQ空间前端后端技术AJAX与FORM等等

[皇帝]北京-横瓜-7年(601069289)  22:06:34 [元帅]横瓜-PHP教父(601069289)  21:35:29 这里是QQ空间的图片上传方法 是FLASH 我草 [元帅]横瓜-PHP教父(601069289)  21:36:31 竟然不是JS QQ空间用FLASH 图片上传方法 [元帅]横瓜-PHP教父(601069289)  21:37:35 至此,整个QQ空间的技术,已经全部被我破解 @IT柏拉图 [元帅]IT柏拉图(2500875)  21:38:11 你真是天才,

【源码】StringBuilder和StringBuffer源码深度剖析

//------------------------------------------------------------------------ 写篇博客不容易,请尊重作者劳动成果.转载请注明出处:http://blog.csdn.net/chdjj //------------------------------------------------------------------------ 我觉得要通过源码研究一个类,应该先从整体上了解这个类,比如说这个类的继承体系,有哪些超类,继承

深度剖析WordPress主题结构(转)

利用强大的技术,可以把基于wordpress的网站做成各种各样的形式,这除了要求wordpress主题开发人员精通html,PHP,JS,CSS等技术,还需要开发者掌握WordPress主题的框架. Gevin今天结合The anatomy of a WordPress theme这篇文章,和大家一起剖析WordPress主题的结构.原文作者用图文形式,分别从网站外观.页面组成和后台文件三个方面,形象的向大家展示了WordPress的架构,下面Gevin和大家一起分析WordPress是如何架构

DDOS学习笔记(《破坏之王-DDOS攻击与防范深度剖析》)

最近花了点时间把<破坏之王-DDOS攻击与防范深度剖析>看了一遍,坦白来说,这本书比较浅显,可以说是入门书,当然对于我这种对DDOS一知半解的人来说,也是一本不错的书,起码我学到了一些东西. DDOS是分布式拒绝服务(Distributed Denial of Service, DDOS)的简写,从名字可以看出,其攻击是分布式的,即多台(可能上万台,甚至更多)电脑同时对目标进行攻击,攻击的目的是让目标无法提供服务.从根本上来说,让目标无法提供服务,办法有很多,比如侵入目标服务器,删除关键服务或

深度剖析消息反射机制

深度剖析消息反射机制作者:hustli    摘要:在前面我们分析了控件通知消息WM_NOTIFY,和WM_NOTIFY紧密联系的还有一个MFC新特性:消息反射.本文中,我想就这个问题作一个全面的论述,如果有错误,还望各路大虾批评指正.    什么是消息反射?    在windows里面,子控件经常向父控件发送消息,例如很多子控件要绘制自己的背景,就可能向父窗口发送消息WM_CTLCOLOR.对于从子控件发来的消息,父控件有可能在处理之前,把消息返还给子控件处理,这样消息看起来就想是从父窗口反射

解决Asp输出乱码问题

在一个Asp页面中页面正常的中文字符都没有问题,但如果用Asp程序输出的话就显示为乱码 终于在百度经验上找到解决方案: 1.将文件编码更改为Utf-8 2.在页头添加"<%@LANGUAGE="VBSCRIPT" CODEPAGE="65001"%>" 3.在页面title里添加Content-Type的meta指定页面编码 “<metahttp-equiv="Content-Type"content=&qu

《AngularJS深度剖析与最佳实践》简介

由于年末将至,前阵子一直忙于工作的事务,不得已暂停了微信订阅号的更新,我将会在后续的时间里尽快的继续为大家推送更多的博文.毕竟一个人的力量微薄,精力有限,希望大家能理解,仍然能一如既往的关注和支持shuang_lang_shuo[破狼]微信号,同时也欢迎大家的高质量文章的投稿. 在2015年一年时间中,我.雪狼大叔.彭洪伟一起共同编写了<AngularJS深度剖析与最佳实践>这本前端Angular.js框架的进阶书籍.在写作期间也得到很多人的支持,特别是在Angularjs中文社区群中的各位群

深度剖析「营销QQ辅助软件」的6大自动营销功能

大家都知道,营销QQ所传播的营销思维就是"大数据管理客户,营销QQ本身具有10万好友的容量,每天可加1000个好友3000个免费消息".把它可以运用到其他个个行业和各大营销类公司,发现这个营销工具真是百搭,放在哪里都好使.基于这个营销工具,我公司总结开发出了 "针对营销QQ全自动化的营销QQ辅助软件":专注的把软件做到极致,全然自动化,用口碑营销达到快速传播的目的. 针对营销QQ全自动化的6个营销功能 首先概括一下营销QQ辅助软件在工作应该包括哪些功能.每个公司根据