论坛首页 Java企业应用论坛

网站实时简繁转换解决方案

浏览 5129 次
精华帖 (0) :: 良好帖 (6) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2009-04-01   最后修改:2009-04-02

    最近由于工作需要做了一个将网站转成繁体呈现给用户的功能,后来参看了许多成功案例,发现思路大体都是相同的,我现在将我具体的实现拿出来讨论一下,因为它不可能是一个百分百覆盖的功能,所以希望大家能多提意见。
很多网站都提供简体版和繁体版供用户浏览,有两种实现方式,一种是做两套不同版本的挂着,这种方式没什么技术性可言,暂不讨论了;我想讨论的是另一种实现方式,实时转换,即是只需硬做一套简体版的,繁体的可以通过实习转换来实现。
我的灵感来自于一个国外人做的网站,http://www.ponyfish.com
它可以说是一个工具,几乎可以为任何网站建立RSS源,这个工具从Step1到Step2在我看来是最难解决的,也可以说是最难覆盖所有网站的,它其实对目标网页的代码进行修改和剔除,然后重新拼接,还原网页原貌。由于Ponyfish这个工具是用来为网站的文章,新闻等建RSS源的,它并不关心页面上诸如JS,FLASH等元素,它剔除了所有的JS代码,所以,比如有某个网页的顶端导航和底端是引的JS,那在Ponyfish对网页还原后,用户看不到头和尾,只能看到内容的部分。
后来我也做了一个跟Ponyfish一模一样的工具,界面也是取的它的,只是出现英文的地方被我替换成中文了,原作者用php,我用的java。当时本想将Ponyfish的实现方法分享出来的,后来做了简繁转换后发现比Ponyfish复杂得多,Ponyfish关注的是网站的文章列表或新闻列表,简繁转换需要转换后的网站跟简体版一样,用户可以提交表单、搜索、用户体验,具有所有功能,必须完全可用。简繁转换的实现已经含盖了Ponyfish的核心功能,只不过Ponyfish多一些JS体验,多一些算法(如字符串求公共子串算法)。
由于需要对目标网页的代码进行操作,要对每个不同的HTML标签进行不同操作,所以我们必须能够读取遍历目标网站的结点。我使用的是HtmlParser,大致思路是将取得目标网页的HTML代码,遍历所有结点,对每个结点进行不同操作(即修改HTML代码),将最终得到的HTML代码推送到自己的空白页面显示,这样下来,只需循环一次,拼接成的HTML可以完全还原原貌。
具体操作如下:
假如我需要转换的网站是 http://tieba.baidu.com
我会将网址做为参数传到我的方法里,类似如下

http://www.mydomain.com?methodname=big5&url=http://tieba.baidu.com

 

在实际操作中,在样的链接出现在浏览器地址栏会对以后的转换造成很多不便,这些不便在下面会我提及,得做Urlrewirte

<rule>
	<from>^/big5/(.*)</from>
	<to>/big.do?methodname=big5&amp;url=$1</to>
</rule>

 最终我们URL地址会变成

http://www.mydomain.com/big5/http://tieba.baidu.com

 

当确定目标网站后,使用htmlparser遍历它的所有结节,即所有node

URL url = new URL(strurl);
HttpURLConnection httpUrl = (HttpURLConnection) url.openConnection();
Node node;
String contentType = httpUrl.getContentType();
String charSet = getCharset(contentType);
Lexer lexer = null;
if (charSet == null)
	charset = "GBK";
else
	charset = charSet;
lexer = new Lexer(new Page(new UnicodeInputStream(httpUrl
				.getInputStream(), charset), charset));

PrototypicalNodeFactory factory = new PrototypicalNodeFactory();
lexer.setNodeFactory(factory);
while (null != (node = lexer.nextNode())) {
	if((node instanceof RemarkNode))
		{…}
	else if((node instanceof TextNode))
		{…}
	else if((node instanceof LinkTag))
		{…}
	 ……
}

 大体按这样的流程走就可以了。
由于HTML代码的写法并没有一个明确的规范标准,所以怎样兼容尽可能多的写法是一个很大的问题,还会有很多转码的问题,因为网站上有的表单,引的JS,导的iframe,可能都是不一样的编码。根据我整合三个大的门户网的经历我将一些需要注意的问题罗列出来。

 

 

1:设置读取目标网站的编码
读取网站HTML代码时,得先设置好以什么样的编码来读取它,以上的代码我在遍历结点前先取了一次编码。

String charSet = getCharset(contentType);

 如果此时的charSet为空,我会为它设置一个默认值“GBK”,默认用GBK来遍历目标网站,当遍历到某个叫”Meta”的结点时,我会查找它有没有属性中包含类似content="text/html; charset=utf-8"的东西,如果没有,继续遍历,如果有,取得该编码值,将它与当前遍历网站的编码进行比较,如果一致则继续遍历,如果不一致,说明从一开始遍历时设置的编码就是错误的,这个时候需要使用新的编码重新遍历。
关于getCharset的方法,网上已有前人写出。

public String getCharset(String content) {
		final String CHARSET_STRING = "charset";
		int index;
		String ret;
		ret = null;
		if (null != content) {
			index = content.indexOf(CHARSET_STRING);
			if (index != -1) {
				content = content.substring(index + CHARSET_STRING.length())
						.trim();
				if (content.startsWith("=")) {
					content = content.substring(1).trim();
					index = content.indexOf(";");
					if (index != -1)
						content = content.substring(0, index);
					if (content.startsWith("\"") && content.endsWith("\"")
							&& (1 < content.length()))
						content = content.substring(1, content.length() - 1);
					if (content.startsWith("'") && content.endsWith("'")
							&& (1 < content.length()))
						content = content.substring(1, content.length() - 1);
					ret = findCharset(content, ret);
				}
			}
		}
		return ret;
	}

	public String findCharset(String name, String _default) {
		String ret;
		try {
			Class<java.nio.charset.Charset> cls;
			Method method;
			Object object;
			cls = java.nio.charset.Charset.class;
			method = cls.getMethod("forName", new Class[] { String.class });
			object = method.invoke(null, new Object[] { name });
			method = cls.getMethod("name", new Class[] {});
			object = method.invoke(object, new Object[] {});
			ret = (String) object;
		} catch (NoSuchMethodException nsme) {
			ret = name;
		} catch (IllegalAccessException ia) {
			ret = name;
		} catch (InvocationTargetException ita) {
			ret = _default;
			System.out
					.println("unable to determine cannonical charset name for "
							+ name + " - using " + _default);
		}
		if (ret.equalsIgnoreCase("gb2312"))
			ret = "GBK";
		return ret;
	}

 在这里我想特别说一下,最后一句:

if (ret.equalsIgnoreCase("gb2312"))
	ret = "GBK";

 如果你实现的是简繁转换功能的话,请加上这句,因为如果网站是gb2312编码,有些繁体字显示转换出来会是乱码 ‘?’.

 

 

 2:处理相对路径。
 相对路径,不管是图片,还是链接,或是JS,VBSCRIPT,CSS等,都会出现相对路径的写法,我现在将一些最常见的相对路径罗列一下。
 假如我们的目标网站是 http://tieba.baidu.com/aaa/bbb/ccc.htm
 假如其页面源代码中的有一幅图片是
 <image src=”pic.gif”>
 那么我们遍历到该image标签时,为了拼接HTML代码后图片可见,必须将代码改成
 <image src=” http://tieba.baidu.com/aaa/bbb/pic.gif”>,即补齐它的图片路径.
 同样
 <image src=”/pic.gif”>
 转换后<image src=”http://tieba.baidu.com/pic.gif”>
 <image src=”./pic.gif”>
 转换后<image src=”http://tieba.baidu.com/aaa/bbb/pic.gif”>
<image src=”../pic.gif”>
 转换后<image src=”http://tieba.baidu.com/aaa/pic.gif”>
<image src=”../../pic.gif”>
 转换后<image src=”http://tieba.baidu.com/pic.gif”>
 也就是说只需写一个简单的算法,根据网站URL,将相对路径转成绝对路径就可以了。

 

 

3:处理普通图片、背景图片、样式表图片
普通图片即是类似 <image src=”pic.gif”>的方式,这个遍历时判断结点是否是ImageTag就可以了。
背景图片就不好判断了,可能有背景图片的标签有很多,如BODY,TD,TR,TABLE,DIV等,这些标签,一种方法是:我们必须先判断它有没有 background属性,如果有,将其转为绝对路径。这种方法不能满足所有情况,因为背景图片可能不是以background属性写出来,如下的写法也可达到一样的效果。

<body style="background-image:pic.gif"> 

 

这样的写法,我们无法准确找到pic.gif字符串并转换它。
样式表图片跟这个差不多的问题,有些CSS文件里的图片也是按相对路径的写法来写,根本准确取得描述图片的字符串。
此时需要借助正则表达式来解决,专用来处理图片。

String REGEXP_HREF = "[\\w|\\/|\\.]+(http)?(s)?(\\:\\/\\/)?[\\:|\\.|\\/|\\w|\\d|_|\\'|+|-]+\\.(gif|jpg|png|bmp)

当读取可能出现背景图片的标签时,将此标签.toHtml()代码用正则表达式处理替换。这样所有的相对路径图片才能替换成绝对路径。

 

 

4:处理JS,VBS
处理JS可以说是很难的一部分,因为大多数情况下JS会有展现的任务,即是说页面上会有一些部分是引用的JS展现的。比如某些网站的头和尾。大多如下:

<script src="/top.js"></script>

此时我们先将相对路径转为绝对路径
假如目标网址是:http://tieba.baidu.com/aaa/bbb/ccc.htm,转换后的JS是这样

<script src="http://tieba.baidu.com/top.js"></script>

 但是,单纯将JS的路径补齐是没有用的,它并不像图片那样显示完以后就没事了,JS代码也会包含简体中文,包含相对路径,这也是需要转换成繁体中文和绝对路径的,所以这个链接我们还得在它前面加上一串,变成

<script src="http://www.mydomain.com/big5/http://tieba.baidu.com/top.js"></script>

还有问题,编码问题,script标签其实有个encoding属性,默认值是当前页面的编码,如果有值就必须用encoding里值的编码解析JS。
为避免混乱,可以为解析JS专门写一个解析的方法,并且这种方法跟我们按结点编历网页的方法不同,解析JS最好按一行行进行读取,因为JS毕竟不是完全意义上由结点构成的。
解析JS的方法我们也得配置一个URLrewrite,假如编码是GBK,上面的链接最终改成

<script src="http://www.mydomain.com/big5js/GBK_http://tieba.baidu.com/top.js"></script>

 由于是一行一行读取,无法精确取得链接地址,和图片地址
所以也需要用正则表达式来替换
其实有些用JS写出来的路径是无法用正则表达式匹配到的,这时之前做的URL转换会起作用了,它会自动将地址栏的路径换算成相对路径补齐显示。

 

 

5:处理XML
有些网站的列表页打开时会默认读XML文件展示,标题,链接等都是从XML文件读出来,XML文件中也会简体中文和链接,也必须进行转换。
同样的,还是编码问题,XML的编码好判断,但会有麻烦事,UTF-8问题,有些UTF-8的XML文件BOM问题,你会发现有的XML你始终不能正确读取。在网上有前人写了个UnicodeReader的类可以解决这个问题,大家可以去找找这个类

BufferedReader br = new BufferedReader(new UnicodeReader(newURL
					.openStream(), "UTF-8"));

解析完XML以后,不能像HTML或JS一样,推送到一个空白页,可以将它放在PrintWirte里,这样页面才能调取到。

 

 

6:处理链接
正常的链接一般是

<a href="/index.htm">link</a>

 用户在繁体页上点击这个链接后,出来的页面也应该是繁体,所以需要将链接也转换一下,最终的写法是:

<a href="http://www.mydomian.com/big/http://tieba.baidu.com/index.htm">link</a>

 其实这就已经完事了,我之所以单独把处理链接拿出来说,是因为有时写法并不是这样。
例如:

<a href="javascript:goUrl('/index.htm')">link</a>
<a href="#" onclick=”goUrl('/index.htm')">link</a>

 这个时候我们按正常的流程也没办法取得准确的链接字符串,有两个办法,一是将这个LinkTag.toHtml()用正则表达式替换一次,二是将可能出现URL的属性替换一次,第二种方法虽然显得麻烦,但碰到大多数正常的A标签效率会很快。
我还碰到过一个问题,页面上有锚点,A标签根本就没有href这个属性,所以处理A标签时最好先判断href属性是否存在,其实处理大多数结点时都应这样操作,因为HTML里未知的因素和写法太多太杂了。

 

 

7:处理文本
所谓简繁转换,就是要将文本中简体中文变成繁体中文,在这里文本大多数情况指的是TextNode,我们需要一个简体转繁体的方法,每当遍历到TextNode时将字符串送进去,取得返回值就可以了。现在网络上流行很多种简转繁的方法,大多是字对字转换,整词或语义转换一般都是商业级别的。
需要说明的事,一个页面上出现的中文字并不全是TextNode类型的,
比如按钮上的字(value属性),一些提示性的文字(alt属性)等,这些都是需要进行转换的,这些可以在遍历到那个结点时进行转换。

 

 

8:设置繁体网页的编码
如果你程序采用的是UTF-8,并且是用UTF-8推送到页面上的,你需要将繁体页面的编码写成UTF-8,即<meta content=”text/html;charset=UTF-8”>
我看到很多成功案例的繁体页面的编码写的是Big5,其实写Big5也可以,不过可能往后在表单的转码上会有一些问题。

 

 

9:处理表单
这应该是简繁转换最难的一部分,转成繁体后的网站表单理论上应该可以接收简体输入和繁体输入,但它接收值后的控制层是我们不能干涉的,并且一个网站的表单可能是从很多别的网站引过来,控制层的编码也可能不同。
处理这样的表单,我们需要做的事情是将用户的输入转换成该表单可转换的编码然后再发送过去。
我们必须将form的action地址转换掉,让用户直接提交到我们的action里
在处理用户请求之前,我们还得知道原先action的编码,
然后将接收到的参数值先进行繁转简,再用URLEncoder转换成相应编码的字符串

StringBuilder requestParameters = new StringBuilder();
		Enumeration parameterNames = request.getParameterNames();
		while (parameterNames.hasMoreElements()) {
			String name = (String) parameterNames.nextElement();
			//过滤掉重复的和不需要的参数
			if(name.equals("url")||name.equals("methodname"))
				continue;
			String value = request.getParameter(name);
			String[] values = null;
			if (value == null) {
				// 判断是否是数组类型参数
				values = request.getParameterValues(name);
			}
			if (value != null) {
				//System.out.println("name:"+name+"value:"+value);
				value = ChineseToBig5.convert_tosimple(value);
				requestParameters//
						.append("%26"+URLEncoder.encode(name, encoding))//
						.append("=")//
						.append(URLEncoder.encode(value, encoding))//
				;
			} else if (values != null) {
				for (String s : values) {
					s = ChineseToBig5.convert_tosimple(s);
					requestParameters//
							.append("%26"+URLEncoder.encode(name, encoding))//
							.append("=")//
							.append(URLEncoder.encode(s, encoding))//
					;
				}
			}
		}

接着将参数和参数值拼接好,发送到原form的action中.
如果我们不关心表单提交后的返回页面是简体或是繁体,可以采用如上步骤。
当处理类似站内搜索的表单时,用户希望搜索后的返回页面也是繁体的,那得将我们的逻辑再加强一下,将参数和参数值拼接好发送到原from的action时,把拼接好的带参数的URL看成一个普通的页面在最前面加上我们简转繁的方法就可以了。

 

 

10:设置过滤
设置过滤有两种情况
第一种是页面上会有一些特殊的地方是不需要进行URL转换的,比如网站上  “简体版”和“繁体版”两个链接,如<a href=”http://tieba.baidu.com”>简体版</a>,这个URL是不需要进行转换成 http://www.mydomain.com/big5/http://tieba.baidu.com 的,因为用户点击它会直接访问而不需要转换。这个需要修改一下页面代码,用一个自定义的标签包围,如:

<MyTag><a href="http://tieba.baidu.com">简体版</a></MyTag> 

Htmlparser有自己的增加和处理自定义标签的方法,逻辑可以这样,当程序遍历时,发现MyTag标签,里面的内容不转换。
另一种过滤是指网站级的过滤。
如,我们提供的简繁转换只想提供给http://www.baidu.com 及其所有二级域名
可以建一个配置文件
在里面设置值  urllist=http://*.baidu.com
然后在action中,每当接收到url值时,与http://*.baidu.com匹配一下,不能匹配则直接跳转,可以匹配则遍历转换。
这里的匹配我详细说明一下:
Action里接收的目标url不管是什么样的,都可以取得其根域名,这个算法在网上有很多,
例如 http://tieba.baidu.com/aaa/bbb/fadsfadsf.htm 的根域名是 http://tieba.baidu.com
取得根域名后,将它与我们在配置文件里设置的值http://*.baidu.com进行匹配,这个匹配是一个叫“两字符串求公共字串”的算法,我把它的使用方法改了一下。
原来的效果是
1 - http://tieba.baidu.com/
2 - http://mp3.baidu.com/abc.htm
1和2求公共子串后得出的字符串是 http://*.baidu.com/*
我在过滤时,将得到的域名和原先设置好的url值求公共子串,如果返回值仍等于url里设置的值,那么我认为这个网站可以被转换,否则就跳转。

 

 

11:提高效率
毫无疑问,每多一个判断会就会阻碍一些性能,特别是那些大型门户网站的首页。
其实我最终写出的程序在没有设置任何缓存的情况下速度也是很快的,由于我公司最近网速不稳定,所以测试可能不够准确,但无论怎样,肯定是得加上缓存的。
我现在采用的是squid,其实一个网站浏览繁体页面的用户数量是很少的,缓存时间稍微设置长一点也是可以接受的。

 

 

差不多就是这样了,以上内容是我一下午凭记忆打出来的,不知道有没描述清楚,可能有很多遗漏和没有提及的,大家将就着看,我在最开头的时候说这种即时的转换不能涵盖所有的情况,因为HTML的写法千奇百怪,有些JS的写法更是出奇的怪异,百分之百的功能还原和网页还原是不现实的,很多网页的简转繁都有这样那样的涵盖不了的问题,希望大家能提宝贵意见,可以尽量让算法更具兼容性。

   发表时间:2009-04-02  
没有人观注抓取网页这一块么。
0 请登录后投票
   发表时间:2009-04-03  
shingo7 写道
没有人观注抓取网页这一块么。

繁简互转,几年前国内就有成熟产品了,从当初几万一套,卖到现在几百一套,LZ可以google一下。

繁简转换,最麻烦还是分析HTML,还有就是误字,繁体中有些字不是直接翻译简体就对了。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics