如何做系列(4)-微博URL短网址生成算法原理(java版、php版实现实例)

短网址(Short URL),顾名思义就是在形式上比较短的网址。通常用的是asp或者php转向,在Web 2.0的今天,不得不说,这是一个潮流。目前已经有许多类似服务,借助短网址您可以用简短的网址替代原来冗长的网址,让使用者可以更容易的分享链接。
例如:http://t.cn/SzjPjA

短网址服务,可能很多朋友都已经不再陌生,现在大部分微博、手机邮件提醒等地方已经有很多应用模式了,并占据了一定的市场。估计很多朋友现在也正在使用。 
       看过新浪的短连接服务,发现后面主要有6个字符串组成,于是第一个想到的就是原来公司写的一个游戏激活码规则,也就是下面的算法2,
26个大写字母 26小写字母,10个数字,随机生成6个然后插入数据库对应一个id,短连接跳转的时候,根据字符串查询到对应id,即可实现相应的跳转!不过2的62次方,不知道有没有重复的,小概率可以,但是对应不是很大的网站应该足够了
自从twitter推出短网址(shorturl),继之国内各大微博跟风,google公开goo.gl使用API,短网址之风愈演愈烈.不得不说这是一个新兴又一大热门web2.0服务.现整理一下,包括完整短网址网站,短网址生成原理,算法举例,以及优劣比较,同时还介绍几个phper个人实现的。

短链接的好处

1、内容需要;2、用户友好;3、便于管理。

为什么要这样做的,原因我想有这样几点:

  1. 微博限制字数为140字一条,那么如果我们需要发一些连接上去,但是这个连接非常的长,以至于将近要占用我们内容的一半篇幅,这肯定是不能被允许的,所以短网址应运而生了。
  2. 短网址可以在我们项目里可以很好的对开放级URL进行管理。有一部分网址可以会涵盖暴力,广告等信息,这样我们可以通过用户的举报,完全管理这个连接将不出现在我们的应用中,应为同样的URL通过加密算法之后,得到的地址是一样的。
  3. 我们可以对一系列的网址进行流量,点击等统计,挖掘出大多数用户的关注点,这样有利于我们对项目的后续工作更好的作出决策。

算法原理
算法一
1)将长网址md5生成32位签名串,分为4段, 每段8个字节;
2)对这四段循环处理, 取8个字节, 将他看成16进制串与0x3fffffff(30位1)与操作, 即超过30位的忽略处理;
3)这30位分成6段, 每5位的数字作为字母表的索引取得特定字符, 依次进行获得6位字符串;
4)总的md5串可以获得4个6位串; 取里面的任意一个就可作为这个长url的短url地址;
这种算法,虽然会生成4个,但是仍然存在重复几率,下面的算法一和三,就是这种的实现.
算法二
a-zA-Z0-9 这64位取6位组合,可产生500多亿个组合数量.把数字和字符组合做一定的映射,就可以产生唯一的字符串,如第62个组合就是aaaaa9,第63个组合就是aaaaba,再利用洗牌算法,把原字符串打乱后保存,那么对应位置的组合字符串就会是无序的组合。
把长网址存入数据库,取返回的id,找出对应的字符串,例如返回ID为1,那么对应上面的字符串组合就是bbb,同理 ID为2时,字符串组合为bba,依次类推,直至到达64种组合后才会出现重复的可能,所以如果用上面的62个字符,任意取6个字符组合成字符串的话,你的数据存量达到500多亿后才会出现重复的可能。
具体参看这里彻底完善新浪微博接口和超短URL算法,算法四可以算作是此算法的一种实现,此算法一般不会重复,但是如果是统计的话,就有很大问题,特别是对域名相关的统计,就抓瞎了。

java语言实现:
Java代码
  1. public class ShortUrlGenerator {  
  2.   
  3.     /** 
  4.       * @param args 
  5.       */  
  6.     public static void main(String[] args) {  
  7.    
  8.        String sLongUrl = "http://474515923.qzone.qq.com" ; //长链接  
  9.        String[] aResult = shortUrl (sLongUrl);  
  10.        // 打印出结果  
  11.        for ( int i = 0; i < aResult. length ; i++) {  
  12.            System. out .println( "[" + i + "]:::" + aResult[i]);  
  13.        }  
  14.     }  
  15.    
  16.     public static String[] shortUrl(String url) {  
  17.        // 可以自定义生成 MD5 加密字符传前的混合 KEY  
  18.        String key = "mengdelong" ;  
  19.        // 要使用生成 URL 的字符  
  20.        String[] chars = new String[] { "a" , "b" , "c" , "d" , "e" , "f" , "g" , "h" ,  
  21.               "i" , "j" , "k" , "l" , "m" , "n" , "o" , "p" , "q" , "r" , "s" , "t" ,  
  22.               "u" , "v" , "w" , "x" , "y" , "z" , "0" , "1" , "2" , "3" , "4" , "5" ,  
  23.               "6" , "7" , "8" , "9" , "A" , "B" , "C" , "D" , "E" , "F" , "G" , "H" ,  
  24.               "I" , "J" , "K" , "L" , "M" , "N" , "O" , "P" , "Q" , "R" , "S" , "T" ,  
  25.               "U" , "V" , "W" , "X" , "Y" , "Z"  
  26.    
  27.        };  
  28.        // 对传入网址进行 MD5 加密  
  29.        String sMD5EncryptResult = ( new CMyEncrypt()).md5(key + url);  
  30.        String hex = sMD5EncryptResult;  
  31.    
  32.        String[] resUrl = new String[4];  
  33.        for ( int i = 0; i < 4; i++) {  
  34.    
  35.            // 把加密字符按照 8 位一组 16 进制与 0x3FFFFFFF 进行位与运算  
  36.            String sTempSubString = hex.substring(i * 8, i * 8 + 8);  
  37.    
  38.            // 这里需要使用 long 型来转换,因为 Inteper .parseInt() 只能处理 31 位 , 首位为符号位 , 如果不用 long ,则会越界  
  39.            long lHexLong = 0x3FFFFFFF & Long.parseLong (sTempSubString, 16);  
  40.            String outChars = "" ;  
  41.            for ( int j = 0; j < 6; j++) {  
  42.               // 把得到的值与 0x0000003D 进行位与运算,取得字符数组 chars 索引  
  43.               long index = 0x0000003D & lHexLong;  
  44.               // 把取得的字符相加  
  45.               outChars += chars[( int ) index];  
  46.               // 每次循环按位右移 5 位  
  47.               lHexLong = lHexLong >> 5;  
  48.            }  
  49.            // 把字符串存入对应索引的输出数组  
  50.            resUrl[i] = outChars;  
  51.        }  
  52.        return resUrl;  
  53.     }  
  54. }  

以下为php语言实现:
Php代码 
  1. <?php   
  2. function shorturl($input) {   
  3.   $base32 = array (   
  4.     'a''b''c''d''e''f''g''h',   
  5.     'i''j''k''l''m''n''o''p',   
  6.     'q''r''s''t''u''v''w''x',   
  7.     'y''z''0''1''2''3''4''5'   
  8.     );   
  9.     
  10.   $hex = md5($input);   
  11.   $hexLen = strlen($hex);   
  12.   $subHexLen = $hexLen / 8;   
  13.   $output = array();   
  14.     
  15.   for ($i = 0; $i < $subHexLen; $i++) {   
  16.     $subHex = substr ($hex, $i * 8, 8);   
  17.     $int = 0x3FFFFFFF & (1 * ('0x'.$subHex));   
  18.     $out = '';   
  19.     
  20.     for ($j = 0; $j < 6; $j++) {   
  21.       $val = 0x0000001F & $int;   
  22.       $out .= $base32[$val];   
  23.       $int = $int >> 5;   
  24.     }   
  25.     
  26.     $output[] = $out;   
  27.   }   
  28.     
  29.   return $output;   
  30. }   
  31. ?>  
 

Php代码 
  1. <?php   
  2. function random($length, $pool = '')   
  3.     {   
  4.         $random = '';   
  5.     
  6.         if (emptyempty($pool)) {   
  7.             $pool    = 'abcdefghkmnpqrstuvwxyz';   
  8.             $pool   .= '23456789';   
  9.         }   
  10.     
  11.         srand ((double)microtime()*1000000);   
  12.     
  13.         for($i = 0; $i < $length; $i++)    
  14.         {   
  15.             $random .= substr($pool,(rand()%(strlen ($pool))), 1);   
  16.         }   
  17.     
  18.         return $random;   
  19.     }   
  20. ?>  


跳转原理

当我们生成短链接之后,只需要在表中(数据库或者NoSql )存储原始链接与短链接的映射关系即可。当我们访问短链接时,只需要从映射关系中找到原始链接,即可跳转到原始链接。


短地址原理以及相关实现

短网址服务两种不同算法JAVA实现

短网址(Short URL) ,顾名思义就是看起来很短的网址。自从twitter推出短网址服务以后,各大互联网公司都推出了自己的短网址服务。个人感觉短网址最大的优点就是短,字符少,便于发布、传播、复制和存储。 

通过网上的搜索,感觉流传了2种短网址算法,一种是基于MD5码的,一种是基于自增序列的。 


1、基于MD5码 : 这种算法计算的短网址长度一般是5位或者6位,计算过程中可能出现碰撞(概率很小),可表达的url数量为62 的5次方或6次方。感觉google(http://goo.gl),微博用的是类似这种的算法(猜的),可能看起来比较美观。 

2、基于自增序列 : 这种算法实现比较简单,碰撞的可能性为0,可表达的URL可达无穷大,长度从1开始。貌似百度的短网址服 务( http://dwz.cn/)是这种算法. 

具体算法 

1、MD5码 :假设url的长度为N 

    a.计算长地址的MD5码,将32位的MD码分成4段,每段8个字符 

    b.将a得到的8个字符串看成一个16进制的数,与N * 6个1表示的二进制数进行&操作 

    得到一个N * 6长的二进制数 

    c.将b得到的数分成N段,每段6位,然后将这N个6位数分别与61进行&操作,将得到的 

    数作为INDEX去字母表取相应的字母或数字,拼接就是一个长度为N的短网址。 

		static final char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7',
				'8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
				'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
				'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
				'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
				'U', 'V', 'W', 'X', 'Y', 'Z' };
public String shorten(String longUrl, int urlLength) {
		if (urlLength < 0 || urlLength > 6) {
			throw new IllegalArgumentException(
					"the length of url must be between 0 and 6");
		}
		String md5Hex = DigestUtils.md5Hex(longUrl);
		// 6 digit binary can indicate 62 letter & number from 0-9a-zA-Z
		int binaryLength = urlLength * 6;
		long binaryLengthFixer = Long.valueOf(
				StringUtils.repeat("1", binaryLength), BINARY);
		for (int i = 0; i < 4; i++) {
			String subString = StringUtils
					.substring(md5Hex, i * 8, (i + 1) * 8);
			subString = Long.toBinaryString(Long.valueOf(subString, 16)
					& binaryLengthFixer);
			subString = StringUtils.leftPad(subString, binaryLength, "0");
			StringBuilder sbBuilder = new StringBuilder();
			for (int j = 0; j < urlLength; j++) {
				String subString2 = StringUtils.substring(subString, j * 6,
						(j + 1) * 6);
				int charIndex = Integer.valueOf(subString2, BINARY) & NUMBER_61;
				sbBuilder.append(DIGITS[charIndex]);
			}
			String shortUrl = sbBuilder.toString();
			if (lookupLong(shortUrl) != null) {
				continue;
			} else {
				return shortUrl;
			}
		}
		// if all 4 possibilities are already exists
		return null;
	}

2、自增序列: 

   a. 或者序列的自增值,将值用62进制表示。 

private AtomicLong sequence = new AtomicLong(0);

	@Override
	protected String shorten(String longUrl) {
		long myseq = sequence.incrementAndGet();
		String shortUrl = to62RadixString(myseq);
		return shortUrl;
	}

	private String to62RadixString(long seq) {
		StringBuilder sBuilder = new StringBuilder();
		while (true) {
			int remainder = (int) (seq % 62);
			sBuilder.append(DIGITS[remainder]);
			seq = seq / 62;
			if (seq == 0) {
				break;
			}
		}
		return sBuilder.toString();
	}

本章摘自IT瘾。



短址(short URL)原理及其实现

前言:

最近看了一些关于短址(short URL)方面的一些博客,有些博客说到一些好的东西,但是,也不是很全,所以,这篇博客算是对其它博客的一个总结吧。

介绍:

短址,顾名思义,就是把长的 URL 转成短的 URL, 现在提供这种服务的有很多公司,我们以google家的 URL shortener 服务: http://goo.gl/ 为例。

首先我们到 http://goo.gl/,然后把本文博客的地址http://blog.csdn.net/beiyeqingteng 输入进去,最后它会返回一个更短的URL,http://goo.gl/Jfs6q 。如下图所示:

URL 解析:

当我们在浏览器里输入 http://goo.gl/Jfs6q 时,DNS首先解析获得http://goo.gl/的IP地址。当DNS获得IP地址以后(比如:74.125.225.72),会向这个地址发送HTTP GET请求,查询 Jfs6q, 这个时候,http://goo.gl/服务器会把请求通过HTTP 301转到对应的长URL http://blog.csdn.net/beiyeqingteng 。后面的解析过程就和平常网址解析是一样的了。

短址本质:

短址本质上是实现了一个映射函数 f: X -> Y 。而这个映射函数必须同时具有两个特点:

1. 如果 x1 != x2, 则 f (x1) != f(x2);

2. 对于每一个 y, 能够找到唯一的一个 x 使得 f(x) = y;

对于任何的线性函数,比如 f(x) = 2x,都满足这样的条件。

好了,如果了解了短址的本质,我们再来看它是如何实现的。

注明:在google URL shortener 服务中,它允许一个长 url 对应多个短的url。这可能是出于安全上的考虑。在本文中,我们不考虑这种情况。

实现:

短址的长度一般设为 6 位,而每一位是由 [a - z, A - Z, 0 - 9] 总共 62 个字母组成的,所以6位的话,总共会有 62^6 ~= 568亿种组合,基本上够用了。在google URL shortener 服务中,短址长度为 5,大概有9亿多种组合.

假设我们用数据库来保存长地址和短地址的映射,那么,在表 LongtoShortURL 中,我们会有三列:

1. ID,int,  自动增长;

2. LURL,varchar,  // 长URL;

3. SURL, varchar,  // 短URL。

现在我们考虑通过如何长URL得到唯一的短URL。

在讲具体算法以前,先提一个问题:10进制数和16进制数之间的转换是否满足刚刚提到的映射函数 f: X -> Y中的两个条件?

答案: 是。

本文的思路也是利用进制之间的转换。因为我们总共有 62 个字母,我们可以自创一种进制,叫做 62 进制。其规则如下:

0  → a  
1  → b  
...  
25 → z  
...  
52 → 0  
61 → 9

所以,对于每一个长地址,我们可以根据它的ID,得到一个6位的 62 进制数,这个6位的 62 进制数就是我们的短址。具体实现如下:

public ArrayList<Integer> base62(int id) {  
      
    ArrayList<Integer> value = new ArrayList<Integer>();  
    while (id > 0) {  
        int remainder = id % 62;  
        value.add(remainder);  
        id = id / 62;  
    }  
      
    return value;  
}

举例:

对于 ID = 138,通过 base62(138), 我们得到 value = [14, 2]。根据上面的对应规则表,我们可以得到其对应的短址为:aaaabn 。(由 value 得到具体的短址,可以通过switch 语句得到,因为代码太长,在此略过。)

当我们想通过短址找到所对应的长地址,方法也很简单,就是把62进制数转成10进制数即可,这样我们就可以得到长地址的ID了。代码如下:

public static int base10(ArrayList<Integer> base62) {  
    //make sure the size of base62 is 6  
    for (int i = 1; i <= 6 - base62.size(); i++) {  
        base62.add(0, 0);  
    }  
      
    int id = 0;  
    int size = base62.size();  
    for (int i = 0; i < size; i++) {  
        int value = base62.get(i);  
        id += (int) (value * Math.pow(62, size - i - 1));  
    }  
      
    return id;  
}

比如,对于短址aaae9a,其62进制为[0, 0, 0, 4,61,0] ,则其长地址的ID 为[0, 0, 0, 4,61,0] = 0×62^5+ 0×62^4 + 0×62^3 + 4×62^2 + 61×62^1 + 0×62^0 = 1915810。有了ID,我们自然就可以得到长地址了。

参考:http://stackoverflow.com/questions/742013/how-to-code-a-url-shortener (本文算法来源)

http://blog.sina.com.cn/s/blog_65db99840100lg4n.html

本章摘自http://blog.csdn.net/beiyeqingteng。



微博URL短网址生成算法原理及(java版、php版实现实例) 

短网址(Short URL),顾名思义就是在形式上比较短的网址。通常用的是asp或者php转向,在Web 2.0的今天,不得不说,这是一个潮流。目前已经有许多类似服务,借助短网址您可以用简短的网址替代原来冗长的网址,让使用者可以更容易的分享链接。

例如:http://t.cn/SzjPjA

短网址服务,可能很多朋友都已经不再陌生,现在大部分微博、手机邮件提醒等地方已经有很多应用模式了,并占据了一定的市场。估计很多朋友现在也正在使用。 

看过新浪的短连接服务,发现后面主要有6个字符串组成,于是第一个想到的就是原来公司写的一个游戏激活码规则,也就是下面的算法2,

26个大写字母 26小写字母,10个数字,随机生成6个然后插入数据库对应一个id,短连接跳转的时候,根据字符串查询到对应id,即可实现相应的跳转!不过2的62次方,不知道有没有重复的,小概率可以,但是对应不是很大的网站应该足够了

自从twitter推出短网址(shorturl),继之国内各大微博跟风,google公开goo.gl使用API,短网址之风愈演愈烈.不得不说这是一个新兴又一大热门web2.0服务.现整理一下,包括完整短网址网站,短网址生成原理,算法举例,以及优劣比较,同时还介绍几个phper个人实现的。

短链接的好处:

1、内容需要;2、用户友好;3、便于管理。

为什么要这样做的,原因我想有这样几点:

微博限制字数为140字一条,那么如果我们需要发一些连接上去,但是这个连接非常的长,以至于将近要占用我们内容的一半篇幅,这肯定是不能被允许的,所以短网址应运而生了。

短网址可以在我们项目里可以很好的对开放级URL进行管理。有一部分网址可以会涵盖暴力,广告等信息,这样我们可以通过用户的举报,完全管理这个连接将不出现在我们的应用中,应为同样的URL通过加密算法之后,得到的地址是一样的。

我们可以对一系列的网址进行流量,点击等统计,挖掘出大多数用户的关注点,这样有利于我们对项目的后续工作更好的作出决策。

算法原理

算法一

1)将长网址md5生成32位签名串,分为4段, 每段8个字节;

2)对这四段循环处理, 取8个字节, 将他看成16进制串与0x3fffffff(30位1)与操作, 即超过30位的忽略处理;

3)这30位分成6段, 每5位的数字作为字母表的索引取得特定字符, 依次进行获得6位字符串;

4)总的md5串可以获得4个6位串; 取里面的任意一个就可作为这个长url的短url地址;

这种算法,虽然会生成4个,但是仍然存在重复几率,下面的算法一和三,就是这种的实现.

算法二

a-zA-Z0-9 这64位取6位组合,可产生500多亿个组合数量.把数字和字符组合做一定的映射,就可以产生唯一的字符串,如第62个组合就是aaaaa9,第63个组合就是aaaaba,再利用洗牌算法,把原字符串打乱后保存,那么对应位置的组合字符串就会是无序的组合。

把长网址存入数据库,取返回的id,找出对应的字符串,例如返回ID为1,那么对应上面的字符串组合就是bbb,同理 ID为2时,字符串组合为bba,依次类推,直至到达64种组合后才会出现重复的可能,所以如果用上面的62个字符,任意取6个字符组合成字符串的话,你的数据存量达到500多亿后才会出现重复的可能。

具体参看这里彻底完善新浪微博接口和超短URL算法,算法四可以算作是此算法的一种实现,此算法一般不会重复,但是如果是统计的话,就有很大问题,特别是对域名相关的统计,就抓瞎了。

java语言实现:

public class ShortUrlGenerator {  
  
    /** 
      * @param args 
      */  
    public static void main(String[] args) {  
   
       String sLongUrl = "http://474515923.qzone.qq.com" ; //长链接  
       String[] aResult = shortUrl (sLongUrl);  
       // 打印出结果  
       for ( int i = 0; i < aResult. length ; i++) {  
           System. out .println( "[" + i + "]:::" + aResult[i]);  
       }  
    }  
   
    public static String[] shortUrl(String url) {  
       // 可以自定义生成 MD5 加密字符传前的混合 KEY  
       String key = "mengdelong" ;  
       // 要使用生成 URL 的字符  
       String[] chars = new String[] { "a" , "b" , "c" , "d" , "e" , "f" , "g" , "h" ,  
              "i" , "j" , "k" , "l" , "m" , "n" , "o" , "p" , "q" , "r" , "s" , "t" ,  
              "u" , "v" , "w" , "x" , "y" , "z" , "0" , "1" , "2" , "3" , "4" , "5" ,  
              "6" , "7" , "8" , "9" , "A" , "B" , "C" , "D" , "E" , "F" , "G" , "H" ,  
              "I" , "J" , "K" , "L" , "M" , "N" , "O" , "P" , "Q" , "R" , "S" , "T" ,  
              "U" , "V" , "W" , "X" , "Y" , "Z"  
   
       };  
       // 对传入网址进行 MD5 加密  
       String sMD5EncryptResult = ( new CMyEncrypt()).md5(key + url);  
       String hex = sMD5EncryptResult;  
   
       String[] resUrl = new String[4];  
       for ( int i = 0; i < 4; i++) {  
   
           // 把加密字符按照 8 位一组 16 进制与 0x3FFFFFFF 进行位与运算  
           String sTempSubString = hex.substring(i * 8, i * 8 + 8);  
   
           // 这里需要使用 long 型来转换,因为 Inteper .parseInt() 只能处理 31 位 , 首位为符号位 , 如果不用 long ,则会越界  
           long lHexLong = 0x3FFFFFFF & Long.parseLong (sTempSubString, 16);  
           String outChars = "" ;  
           for ( int j = 0; j < 6; j++) {  
              // 把得到的值与 0x0000003D 进行位与运算,取得字符数组 chars 索引  
              long index = 0x0000003D & lHexLong;  
              // 把取得的字符相加  
              outChars += chars[( int ) index];  
              // 每次循环按位右移 5 位  
              lHexLong = lHexLong >> 5;  
           }  
           // 把字符串存入对应索引的输出数组  
           resUrl[i] = outChars;  
       }  
       return resUrl;  
    }  
}

以下为php语言实现:

<?php   
function shorturl($input) {   
  $base32 = array (   
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',   
    'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',   
    'q', 'r', 's', 't', 'u', 'v', 'w', 'x',   
    'y', 'z', '0', '1', '2', '3', '4', '5'   
    );   
    
  $hex = md5($input);   
  $hexLen = strlen($hex);   
  $subHexLen = $hexLen / 8;   
  $output = array();   
    
  for ($i = 0; $i < $subHexLen; $i++) {   
    $subHex = substr ($hex, $i * 8, 8);   
    $int = 0x3FFFFFFF & (1 * ('0x'.$subHex));   
    $out = '';   
    
    for ($j = 0; $j < 6; $j++) {   
      $val = 0x0000001F & $int;   
      $out .= $base32[$val];   
      $int = $int >> 5;   
    }   
    
    $output[] = $out;   
  }   
    
  return $output;   
}   
?>
<?php   
function random($length, $pool = '')   
    {   
        $random = '';   
    
        if (emptyempty($pool)) {   
            $pool    = 'abcdefghkmnpqrstuvwxyz';   
            $pool   .= '23456789';   
        }   
    
        srand ((double)microtime()*1000000);   
    
        for($i = 0; $i < $length; $i++)    
        {   
            $random .= substr($pool,(rand()%(strlen ($pool))), 1);   
        }   
    
        return $random;   
    }   
?>

跳转原理

当我们生成短链接之后,只需要在表中(数据库或者NoSql )存储原始链接与短链接的映射关系即可。当我们访问短链接时,只需要从映射关系中找到原始链接,即可跳转到原始链接。


原文地址:https://www.cnblogs.com/mrcharles/p/11879828.html