谈谈我认识的优秀的人和有趣的灵魂

这两年关注的事情不太聚焦(坦率讲,应该是非常发散?),因此有意无意地接触了非常多的人。对于我这种不太擅长社交的人来说,最开始干这种事的时候是一百个不愿意的,并且喜欢用「无意义社交就是浪费时间」来宽慰自己。必须承认,这种有点混圈子嫌疑的社交的确利弊参半,但是,如果你要问对这些「弊端」是否后悔,我的回答一定是否定了。其中最重要的一个原因是,认识了一众优秀的人,和一众有趣的灵魂。

为了避免造成不必要的麻烦和误解,下面提到的人我都尽量模糊化处理。人物的顺序按照我的思路出现,比较随机,不分先后。

我眼中的“华阳乔布斯”

任何时候,评论前东家老板其实都是挺忌讳的事情。一方面,可能这个员工当初离开的时候受了小委屈;另一方面这个老板可能已经在公司制度层面要求将这些离职员工纳入招聘黑名单了。如果你在成都某游戏公司工作过,你大概率可以一次把这两条一次集齐。

但是,请别误会我。虽然一个公司的文化必然受创始人的价值观影响,但是如果你能够跳出当时的那种雇佣关系,毫无分别心的看这个人的过往和经历,往往能够对他有不一样的认识和看法。显然,前老板“华阳乔布斯”(后简称乔)是让我意识到这一点的人。

乔因为极度推崇、学习和实践苹果乔布斯而得名。举几个例子:乔如果看到某个邮件存在问题,他会在全体邮件、公司大群中直接怼人,并在末尾附上一句「建议辞退该员工」;乔如果对项目不满意,他会直接过来跟执行人直接对骂,甚至……干架;要进入 t 公司也不容易,首先要测智商,“低能儿”不要,通过所有面试以后,还有一场面向公司高管的入职演讲,这可不是做样子,是真正的大概率刷人的;离开 t 公司,会上公司招聘黑名单……

我以前对乔的这些做法挺困惑的,还在 t 公司那会儿,一次跟领导聊道,乔这样行事,是不是你们这群高管惯出来的巨婴?领导稍微迟疑了一会儿,说,他虽然年长乔好几岁,但是内心对乔的心怀尊敬的,然后他谈了公司当初是如何在乔的带领下成长和如何克服困难的。我当时没有接话,陷入了尬聊。如今想来,很多人都会当局者迷,掉入屁股决定脑袋的陷阱,如果当时我对领导的回答是狐疑的,那么,现在想来,我相信其是坦诚的。

2016年以后,手游行业逐渐成为了几个寡头的后花园,手游之都,一夜之间,倒下数百家游戏公司。而 t 公司在乔的带领下却离上市越来越近。当然,去年上市答辩临门一脚的时候遇到了挫折,导致 t 当前还未上市,乔应该还是有失落的。但无论是在 t 公司还是离开,我对乔在大方向上的判断以及具体的执行是认可的。而显然,有时候事情的落地不会让所有人都准备好,更不可能让所有员工都满意。

离职以后,虽然上了“黑名单”,但是一直关注乔的朋友圈。隐私关系,这里就不透露他朋友圈内容了。因为已经没有了分别心,看他的朋友圈反而觉得有了点意思:一个财务自由、公司健康运转(至少看起来是)的CEO,勤奋、聪明、好学,时不时还能分享一些有趣的体验和见闻,脾气可能不算好,但是并不让人生厌。对了,离职最后一天的最后一个小时,还听了他的《穷查理宝典》读书分享会,印象中,那个分享会还是要买门票才能去听的。

这两年,自己也在认知上不断的刷新和挑战自己。虽然成长背景、认知路径完全不一样,但是在很多关键问题的认知和判断上,跟乔越来越相似。但是,有一点显著不同,乔能够支配的资源远不是我辈可及,因此,很多时候不仅仅是看其人,也在看他所做的事情来验证自己的思考和判断。

什么?你说你想听乔的花边新闻?算了吧,花边只是浪费你我的时间,人都是多元和复杂的,一个人哪有什么绝对的优点和缺点,关注他的特点就行了。

冯医生的氪金手术刀

坦率讲,我与冯医生并不算熟识。唯一的交流渠道截止目前为止也仅限于推特。但是,了解一个人有很多方法:如果是一个上推的人,你看看他过往的文字就可以了。

最开始关注冯医生是因为推友转发了其一例手术的总结。处于刻奇,关注了冯医生,也翻看了他历史 feed. 没看几分钟,我发现我应该找到了中国最有意思的外科医生(之一)。精湛的手术技艺经常被推友调侃为“冯医生到处给人割肾”;会给求助的推友一些中肯的诊疗建议,但是从来不懂装懂;喜欢摄影、电影、爱折腾网络,虽然非相关专业科班出身,但是逻辑和理解力极好,往往能在推友三言两语的线索中自我解决问题;有两个女儿,与我教育的观念非常契合,经常会分享一些非常具体的子女教育的实操和感悟;身在体制内,热爱自己的职业,同时也非常清楚边界和天花板,然后去贵州开了医院……

冯医生在体制内的时候,就是你能想象到的那种韩剧中才有的偶像医生:长得帅、专业能力突出、收入丰厚。前两条不需要解释,对于最后一条,在中国这么提一个医生容易引起误解,我这里稍微解释一下。冯医生能做到这样,并不是靠患者的红包,更不是跟医药代表勾结,而是因为其在行业的影响力和口碑,使得其有工资薪酬之外的收入,最典型的诸如可以在重庆片区可以开飞刀。而这些都是可以见阳光的。

冯医生大概年长我十岁,但是看他的文字,完全没有代沟,对新事物的认知和接受速度有过之无不及。有一段时间,冯医生摆弄医院进口的达芬奇机器人,遇到一个视频录制问题,然后在推特上求助。我不是这方面的专家,自然也就没有给不靠谱的答案,但是我顺着其他人的回答看了一下,冯医生最终用不到1000块采购的设备解决了该问题。要知道,冯医生旁边的医院解决这个问题可是花了20W+! 总听老一辈说知识改变命运,其实更多时候是见识和认知。冯医生只是一个医学博士,但是专业的训练对其影响是深远的,单凡你跟他对过话,你都能深刻感受到其背后的思考以及其透出的磁力,这种磁力有时候吸引到的是你的注意力和思考,有时候是他要寻找问题的答案。

今年年初,冯医生宣布自己辞职,前往贵州创业。我是创过业的人,深刻的明白创业所面临的问题和风险。不要误解我,我说的「深刻明白」并不是简单感慨“创业维艰,且行且珍惜”,而是我相对来说能更大概率判断其创业方向是否靠谱以及创业者不失败的概率。对于冯医生,从其宣布消息开始,我一直都在默默祝福他。并不是那种廉价的客套话。而是每次看他发布关于新医院建设的问题也好、困惑也好、求助也好,都有一众人提供以各种方式提供帮助、建议和支持。在看到冯医生在院长这个方向上迅速成长后,我也看到越来越多的人在询问能否加入新的医院。最有意思的是,冯医生周末会会重庆,一次请以前医院同事吃饭,结果,几乎整个科室都出来了……有时候,你甚至都没有跟这个人见过面,但是他的所作所为就是让你非常确信他要做的事情可以落地达成;有时候,一个人可能并没有强得那么绝对,但是他的见识领先了一步,做事上靠谱、nice,那么他大概率会成为那个被大家自然托起的人。

有时我跟妞儿讲,这个优秀的人其实就在成都旁边的重庆,得空的时候,也许我们可以找他一起吃顿火锅。

英伦归来的处女座赵教授

作为算是在天朝学术圈混迹过的人,对我朝科研的现状(计算机科学方向)是非常失望的。但是,你如果要说这个圈子没有优秀和认真做事的人,我是一百个不同意的。赵教授并不是我的导师,而是我的导师邀来做过一次学术报告的学者。因此,我与赵教授其实只有一面之缘。

那次交流,赵教授先是做了一个云计算相关的学术报告,由于时间比较充裕,后续自由讨论和交流了一些问题。我被赵教授吸引除了其丰硕的研究成果,还有其对时政的敢怒敢言。而这种敢怒敢言并不是浅薄的愤青,而是实际去参政议政。前两年,赵教授拿到了广东省的五一劳动奖章和五四杰出青年,我知道这不是虚妄,而是实至名归。广东省的数字化建设和政务水平能够领先国内其他地区,以赵教授为代表的这一群人的努力是分不开的。

赵教授是我认识的时间管理做得最好的人。工作上,其在广东最好的两所高校任职,同时积极参政,还是三个孩子的父亲。但是,我发现其实赵教授从来都是举重若轻。什么抱怨、工作家庭矛盾,不存在的,赵教授还可以做更多的事情!而能做到这样,而且在每个方面都游刃有余,除了基本的聪明以外,其实是其经常自嘲的那种处女座疯子版的严格与认真。我没有看到赵教授有过一个完整的周末,也没看到过其晚上不工作。经常看到的是,以小时为单位,在学校、实验室、家、机场、会场、体育馆之间快速的切换。我们经常说,生命的长度是一定的,但是可以选择宽度。我只能说,同学,你还是太年轻了,可能你选择出来的那点宽度比别人的地下室都狭窄。

如果你有机会去广东最好的高校求学,报考的也是计算机方向的专业,并且你看到一个赵姓的教授还有名额,并且认为自己也有变得优秀的潜质的话,毫无保留的推荐你选赵教授。


每当我跟妞儿讲,遇到优秀的人的时候,我内心的喜悦和对对方的喜爱是难以言说的时候,妞儿都会默默的扔下一句“你又没他们优秀”,然后决绝而去?。要说认识这些自己欣赏的人完全没有功利心,那是不客观的。渐渐地,认识和接触的人多了,放平了心态,毫无分别心的看他们的时候,总能发现这个自己欣赏的群体的一些共性:

  • 独立思考构建的独一无二的认知。这种认知千差万别,你根本不用指望你能遇到一个跟你认知一样的人。识别出你们认知能共振的频段,珍惜交流的机会和质量即可。
  • 清晰的逻辑和极强的理解力。跟行业以及这个人的出生背景没有关系,直观感受就是他能够把他那个领域的问题很快跟你讲明白,同时他也能在很短时间内,提出挑战你自己领域的问题。
  • 严格的逻辑自恰。你也许有很多结论和判断与其是相反的,但是他一定会告诉你得出这个结论的原因和推理依据。
  • 极强的时间观念和严格的时间管理执行。其实生命的长度一样是骗人的,因为单位时间的时间运转效率差异实在是太大了。
  • 演讲力和号召力。不是政客那种带煽动和目的性太强的演讲号召。而是,因为内心有所表达,自然而然的会站到演讲台那个位置。而这种演讲往往其实没有功利心,反而可以被他人托举。
  • 喜欢阅读和写作。世界上为数不多的只有好处没有坏处的事,在这群人身上体现的尤其明显。我并不反感小资,但要说明的是,小资的阅读和写作是不在这个范畴的。
  • 好奇且富有同情心。对这个世界不敏感的人,难以有发自内心的同情心。

这个世界很大,光鲜的皮囊多,有趣的灵魂少;如果你遇到一个,请感谢时间的邂逅。

Go 中如何准确地判断和识别各种网络错误

Go 自带的网络标准库可能让很多第一次使用它的人感慨,这个库让网络编程的门槛低到了令人发指的地步。然而,封装层次与开发人员的可控性往往是矛盾的。Go 的网络库封装程度算是一个不错的折衷,绝大部分时候,我们只需要调用 Dial, Read, Write Close 几个基本操作就可以了。

但是,网络是复杂的。我们有时候需要细致的处理网络中的各种错误,根据不同的错误进行不同的处理。比如我们遇到一个网络错误时,需要区分这个错误是因为无法解析 host ip, 还是 TCP 无法建立连接,亦或是读写超时。一开始的时候,我们的写法可能是这样的:

    errString := err.Error()
    fmt.Println(errString)
    switch {
    case strings.Contains(errString, "timeout"):
        fmt.Println("Timeout")
    case strings.Contains(errString, "no such host"):
        fmt.Println("Unknown host")
    case strings.Contains(errString, "connection refused"):
        fmt.Println("Connection refused")
    default:
        fmt.Printf("Unknown error:%s", errString)
    }

这种根据错误信息进行字符串匹配进行判断的方法有非常明显的局限性:该错误信息依赖于操作系统,不同的操作系统对于同一错误返回的字符串信息可能是不同的。因此,这种判断网络错误类型的方法是不可靠的。那么有没有一种准确而可靠的判断各种网络错误的方式呢?答案是肯定的。

我们知道在 Go 中,error 是一个内建的 interface 类型:

type error interface {
        Error() string
}

要准确判断不同的错误类型,我们只需要类型断言出其错误类型即可。

在 Go 的网络标准库中,错误类型被统一封装为 net.Errorinterface 类型:

type Error interface {
        error
        Timeout() bool   // Is the error a timeout?
        Temporary() bool // Is the error temporary?
}

net.Error 类型的具体 concrete 类型又被封装为 net.OpError 类型:

type OpError struct {
        // Op is the operation which caused the error, such as
        // "dial", "read" or "write".
        Op string

        // Net is the network type on which this error occurred,
        // such as "tcp" or "udp6".
        Net string

        // For operations involving a remote network connection, like
        // Dial, Read, or Write, Source is the corresponding local
        // network address.
        Source Addr

        // Addr is the network address for which this error occurred.
        // For local operations, like Listen or SetDeadline, Addr is
        // the address of the local endpoint being manipulated.
        // For operations involving a remote network connection, like
        // Dial, Read, or Write, Addr is the remote address of that
        // connection.
        Addr Addr

        // Err is the error that occurred during the operation.
        Err error
}

其中,net.OpError.Err 可能是以下几种类型:

*os.SyscallError 错误比较特殊,与具体操作系统调用有关:

type SyscallError struct {
        Syscall string
        Err     error
}

对于我们关心的网络错误,SyscallError.Err 一般为 sys.Errno 类型,与网络错误相关的常用值有:

  • syscall.ECONNREFUSED
  • syscall.ETIMEDOUT

看到这里,你可能忍不住要吐槽 Go 这种错误嵌套处理了,事实上,官方也意识到了这种错误处理的问题,在 Go 2中,可能会出现新的错误和异常处理方式,可以参见 GopherChina 2018 keynote 点评: RETHINKING ERRORS FOR GO 2.

当前阶段,我们依然要直面这种错误处理方式。为了方便大家理解 Go 网络标准库中处理错误的方式,我们把上面的错误嵌套整理了一张关系图:

明白了网络标准库中处理错误的逻辑,判断和识别各种类型的网络错误就非常简单了:对网络错误进行类型断言。以我们团队主要关心的 DNS 解析错误、TCP 无法建立连接、读写超时为例,判断逻辑可以是这样:

func isCaredNetError(err error) bool {
    netErr, ok := err.(net.Error)
    if !ok {
        return false
    }

    if netErr.Timeout() {
        log.Println("timeout")
        return true
    }

    opErr, ok := netErr.(*net.OpError)
    if !ok {
        return false
    }

    switch t := opErr.Err.(type) {
    case *net.DNSError:
        log.Printf("net.DNSError:%+v", t)
        return true
    case *os.SyscallError:
        log.Printf("os.SyscallError:%+v", t)
        if errno, ok := t.Err.(syscall.Errno); ok {
            switch errno {
            case syscall.ECONNREFUSED:
                log.Println("connect refused")
                return true
            case syscall.ETIMEDOUT:
                log.Println("timeout")
                return true
            }
        }
    }

    return false
}

这种错误判定方式除了能解决最开始提到的可靠性和准确性问题,也具有良好的普适性。即基于 net 的其他标准库,如 net/http 也支持这种错误判断方式。

扩展阅读

那些产品设计中的小惊喜

虽然技术出身,但是日常工作中经常需要单独的设计一些产品的原型,因此,需要对产品有一些感觉。

你也许被《人人都是产品经理》“荼毒”过,不用悲伤,我也是;你也许跟我一样技术出身,习惯了从技术角度看功能和需求,对产品和业务比较麻木,不要心急,我也是。你也许无数次的跟产品经理争论这个功能的合理性和必要性,谁也说服不了谁,最后你还是做了这个功能,仅仅是“厌恶”了跟产品无意义的浪费时间,没有关系,我也是。

然而,你要知道,大多数时候我们要做的产品设计其实都被前辈验证和解决过,是存在最佳实践的,也是可以从其他产品习得的。这种习得可能是一个特定的产品实现方式,但更多的是产品设计的思考和思路。这种习得,产品经理的整套方法论并不是必要条件,只需要你在使用产品的时候有意识的培养发现问题的敏感性和好奇心。

这里,我先把这几天使用产品过程中看到和体会到的一些产品设计中有意思的「小惊喜」整理一下,以后会不定期更新,争取能形成一个系列。

登录页面:简单的功能比你想象的更难更重要

随着 OAuth 以及开放平台的发展,国内绝大部分产品都支持微信、微博、QQ等多种社交账号的登录方式。这类登录方式不仅可以降低用户登录/注册的心里决策和输入成本,还能够从技术上简化整个账号系统的设计。因此,除了阿里和腾讯自成一体的公司产品,绝大多数产品都会在第一时间支持这类登录方式。然而,把这件事情做好的产品却寥寥无几。

以国内技术社区 segmentfault 为例,很多产品的社交账号登录页面可能是这样的:

从功能层面看,支持的登录方式挺多,把使用频率高的登录方式放在前面,这都没有问题。但是,我们的用户是健忘也是“无情”的。如果因为某种原因(APP更新、网络异常、更换手机等),用户需要重新登录,当用户再次看到这个页面的时候,是比较抓狂的:因为“登录”功能的使用频率很低,用户有很大概率不记得自己当初是以什么方式登录进来的;于是用户只能一个一个的去尝试不同社交账号登录方式来判断自己以前的账号是怎么登录的(这样做唯一能想到的好处就是注册用户数会提高,提前完成 KPI ……)。即使用户粘性高如微信,你去问问身边的朋友,他们的微信是手机号登录还是QQ登录,还是微信号登录,大部分人是搞不清楚的。

几天前用喜马拉雅听《晓说》,因为前一天晚上更新过APP,登录状态被踢了出来(这是个很糟的体验),尝试重新登录:

左下角提示微信为上次登录方式,这让我有了一点惊喜的感觉!

这是最佳的实践方式吗?私以为还可以再进一步:

之所以更加推荐这种将用户上次登录的头像和昵称显示出来的方式,不是因为这是东家咕咚APP采用的方式,而是因为,1)图片的阅读或者说注意力效率是远高于文字的;2)关系亲近的人之间可能会共用设备,显示头像和昵称有助于提高账号和登录方式判定的准确率。

在很多人眼里,登录页面这些改进太过于“鸡毛蒜皮”。但是如果你们的APP在登录页面的各个环节都有埋点的话,你也对这些数据足够熟悉的话,你会发现这些微不足道的改进对用户转化率的影响比你想象的重要得多。

说起转化率,使用社交账号登录一个非常常见也是非常糟糕的设计是:社交账号登录后,要求用户绑定手机/邮箱并设定密码。我能理解绑定手机号在我朝当前实名制政策下的无奈,但是大部分业务其实是不需要强制绑定手机号的。如果你是担心太过于依赖微信,要求用户必须有独立的登录用户民和密码,你应该想想你是不是太把自己当回事了?大部分情况下,我遇到这种社交账号登录后要求用户绑定手机和邮箱的服务,都会默默问候一个“产品13B”,然后直接关闭页面。

懂场景的音乐 APP

在听觉上,我应该算是典型的木耳。但是偏偏有喜欢在开车的时候听音乐。以前一直用豆瓣 FM, 歌曲推荐做的真心赞。但是豆瓣 FM 登录模块实在太渣,会因为各种原因把我从登录状态踢出来要求重新登录,老哥,我在开车呢,这样真的好吗?几经辗转,一次出行的时候,偶然打开了虾米音乐,因为连接了车载蓝牙,判断我是要开车听音乐,询问我是否进入车载模式:

进入车载模式以后,界面就变成跟老年手机一样,按钮巨大,按键也只有简单的几个上一曲,下一曲,暂停。心里默默念了一句: MMP, 这正是我想要的!

还没听完一首歌,又发现了一个小惊喜:

虾米把显示作者信息的地方用来显示歌曲的单词了!很Low是不是?是!很解决问题是不是?当然是!以后在车里想哼两句的时候终于不用因为不知道(不是不记得哟)歌词而随机脑补了。我知道很多技术背景的同学可能会对这种实现方式嗤之以鼻,但是以我对汽车软件行业平均水平的了解和自己折腾这台车歌词显示的经历看,这就是通用性最好、成本最低的方案!

坦率讲,虾米最主要的使用场景肯定不是车载,但是,透过这两个细节,我还是能够非常清晰的感受到虾米的产品在做音乐类 APP 是对场景的细致、全面的思考和实践。我们常常说一个功能或产品好用不好用,这其实是比较抽象和模糊的,如果你想找到具体的问题和原因,回到具体的场景和交互上,你一定能发现一些有趣的问题,同时给用户带来惊喜。

微信悬浮窗:悬浮的内容,流逝的时间

微信在不久前迎来了它的年度更新,其中最为大家津津乐道的一点就是悬浮窗设计:

但凡是常用微信进行阅读的同学,都能深刻的体会阅读文章与聊天消息之间切换的痛苦。悬浮窗一出,很多人大呼痛快。

使用一段时间后,发现了一个细节,随着悬浮时间的变长,悬浮的图标会逐渐变红,最终变成全红。有人说,悬浮窗是微信拯救不断被抖音等APP吞噬的APP使用时间所祭出的武器,此言不虚,但我想也一定有产品对提供更加良好的阅读体验的细致入微的思考和设计,毕竟,大家都是成年人了,「稍后阅读」往往就变成了「永远不读」。另一方面,我也比较悲观的认为,碎片化阅读其实是有非常明显的局限和天花板的,悬浮窗能做的已经接近这个极限。如果你真的是一个重度的微信公众号订阅者,悬浮窗是解决不了你的问题的,你应该关注一下订阅号展示形式的变化。

验证码繁与简

小区楼下有两个自动存取货柜,一般没人在家的时候,我都喜欢让快递老哥帮我把快递放在货柜中。回来的时候,凭验证码提取快递。一家是已经被中国邮政收编的速递易,它的验证码是这样的:

一家是顺丰家的丰巢,它的验证码是这样的:

都是6位验证码,不同的是前者使用的书“数字+字母”组合,后者只使用了数字。前者应该更加符合工程师思维:验证码空间要足够大,这样才能减少碰撞的概率,降低安全风险。但是,我倒觉得后者其实对验证码的认识更加深刻:

  1. 验证码是需要人工输入的,输入方式越简单成本越低。打一个不恰当的对比,前者需要用户会标准键盘输入,后者只需要用户知道如何打电话就够了。“会标准键盘输入”这个要求很过分吗?对你也许都不值一提,但是你要知道小区很多大爷大妈连手机上的T9输入法都不会用,只会手写输入,你要他会基本的标准键盘输入,显然是对你的用户群不够了解。
  2. 绝大部分用户短时间记忆一串数字是经过训练的,这种训练贯穿我们的小学数学教育以及功能机时代的“背电话号码训练”。因此,对于喜欢晚上出来遛弯取快递的同学来说,他们是可以看一眼验证码,然后放心的不带手机去取货。而背诵数字+字母组合,对很多人来说几乎就是不可完成的任务。
  3. 部分数字和字母字符不易区分,引入额外的错误率。典型字符如数字“1”和字母“I”, 数字“0”和字母“O”。
  4. 货柜的安全问题其实不在验证码。这个不方便细说,可以讲的是,如果你推演一下如何非正常手段获取货柜的的货物,你最后推演出来的方法一定不是傻乎乎的到带有好几个摄像头的货柜前去试验证码。

在验证码这个问题上,并不是说丰巢做的多好,而是想说速递易实在做得太烂:产品上的很多问题发展到今天已经是被很好的解决了,如果想在一些常用的产品设计上创新,请一定思考清楚这个「改变」是仅仅为了不同,还是因为当前设计存在很具体的问题。

浏览器地址栏存在多年的 bug

相信绝大部分人都注意到我们使用的浏览器地址栏是可以显示中文的:

但是,较真的工程师会告诉你:URL 地址的合法字符是 ASCII 字符的子集,是不支持中文的。也许浏览器地址栏显示中文是一个 bug?

虽然,平日里我是不太支持研发同学说 “这不是bug, 这是feature”,但是,这一次,我得说这的确是浏览器的一个 feature: 用户毕竟不是工程师,对于他们来讲,他们不关心也不在乎地址栏上面的字符是否经过 urlencode, 地址栏提供给他们的信息越直接越好。从这个逻辑出发,我们就不难理解地址栏中的安全绿色小锁以及直接显示中文了。当然,为了确保技术上的正确性,你通过 Chrome 地址栏复制的和开发者工具上看到的 URL 地址其实是经过 urlencode 过的: