分类
Technology|技术

通义听悟简析情景剧流程与其他LLM比较

题记:

本文旨在测试“通义听悟”对音频和视频的转写识别和信息提炼能力。以及与其他大语言模型(Large Language Model,LLM)对纯文本进行信息提炼能力的比较。本文完整真实地转载测试结果。所有结果均为第一次测试得出。本文无意推广任一LLM。

本文通过转写情景剧流程进行测试。使用该情景剧的原因是该剧有充足的对白内容,且不含可能造成干扰的音效。同时该剧中有单人独白、双人对话,多人讨论的形式,能够充分测试LLM。本文仅使用该剧作为测试素材,不评价剧中提及的观点,也无意推广该剧涉及的课程。

文末附本次测试所使用的相关资源链接。


准备内容

收到阿里云的广告短信,邀请体验“通义听悟”。恰好最近在学习管理网课,就尝试一下它能不能提高我的学习效率。

首先要准备网课的音频或视频材料。为了实验,我做了三手准备。第一,使用Chrome的Sample插件直接内录网页播放的音频。第二,手机录屏了网课视频。第三,看视频写文字稿。

新建内容主要有实时记录、上传音视频和导入云盘三大途径。本文用的是上传音视频的方式。常见的音视频格式都支持。单个视频最大支持4G、单个音频最大支持500M。

可以按事先的了解选择区分发言人。但是实测,即使选择“2人对话”但实际出现多于2人还是能够区分开的。

图1 上传文件

内容在上传完毕后,根据设置自动开始转写为记录。实测接近6分钟的内容转写时间为1分钟左右。转写完成后,以卡片的形式出现在“我的记录”页面的对应文件夹中。

卡片题目默认为上传文件的文件名,可以修改。然后是一些AI提炼的关键词。主内容部分,如果是上传的是视频,会出现视频的第一帧的缩略图。如果是音频,则会出现转写文本。最后是音视频的长度和上传时间。

图2 记录卡片

记录页面还可以星标收藏某些记录。切换视图为卡片或列表模式。按记录是否已读、语言类型、文件来源、上传日期等要素筛选。以及批量标记已读、移动到其他文件夹或删除。


剧本

以下是我结合字幕听写的剧本全文:

《刚当上manager 既兴奋又让你头疼》

时长:5分51秒
下面我会分镜头画面描述这个视频。

镜头画面一
办公室的卡座里,一部电话响了。一个成年男性接起电话,接下来是电话交流的对话:
小刘(成年男性):“喂,吴总。”
吴总(成年女性):小刘啊,你能过来一下吗?有点事找你。
小刘:好的好的,我现在就过去。
小刘离开办公室卡座,镜头画面一结束。

镜头画面二
小刘敲了一下吴总办公室的门。
吴总:“进来。把门带上。”
小刘关了门,然后坐在吴总的对面,吴总首先发话。
吴总:“小刘。今天叫你过来,就是想跟你商量一下最近你们组的人事变动。你看,你来公司也很多年了。从你还是应届毕业生的时候就加入我们公司。那逐步就成长为一个资深的销售。我对你个人的工作表现一直都是挺满意的。”
小刘:“嗯,谢谢吴总的肯定。这也离不开您对我们的指导。”
吴总:“那最近呢,就是你们组的王经理,他提出了离职。其实我们也觉得挺遗憾的。”
小刘:“啊?老大要离职?”
吴总:“对。这是他个人的决定。我们也表示尊重。与此同时,这个manager的职位就空出来了。所以呢,我就在跟HR去评估和商量一下。那我们看了一些人员以往的这个能力和以往的业绩表现。吴总:我会觉得说你相对来说是比较适合manager这个职位的。是个不错的人选。所以我就想今天来问问看你个人的想法。”
小刘:“非常感谢您对我的信任!其实我从实习到现在,进入这家公司已经有近七个年头了。在这段时间当中,我也跟着老大做了不少的项目。那我现在其实也已经开始自己带徒弟了。在这个过程当中其实收获的确挺多的。有机会担任这个职务的话,我相信也可以带着团队在未来做出更好的成绩。”
吴总:“嗯,很好!很高兴你能这么想。那这么看你,这件事我们就可以基本上先这么商定下来。后续呢,我会请HR去走一些流程。那这个流程走完了之后,会大概几天时间吧。会有邮件就是向外宣布通知大家。嗯,这个时候我会希望你能够做一些准备。为什么呢?因为我觉得做leader跟做组员是不太一样的。那我还是对你有信心。我是希望你能够带着你们组呢继续好好地干。”
小刘:“好的!一定!那今后还请吴总多多指导。”
吴总:“行!没问题。你先去忙吧。我这边跟HR再沟通一下。”

镜头画面三
小刘的内心独白:“啊,我终于要成为manager啦!大展宏图的时候到了!我做manager,我们组的业绩肯定要超出之前我们老大定的业绩。然后给每个人多发点奖金。让大家都开心地把活干了。哼!显示我管理能力的时候到了。跟着我这样的老大有肉吃!”

镜头画面四
小刘上任的第一天。
组员甲:“老刘(由于组员甲年龄比小刘小,所以称他为老刘)荣升我们组经理啦,我们大家表示一下。”
各组员鼓掌。
小刘:“谢谢。感谢大家的信任。那以后还需要大家多多支持。那今晚我请客,有家有室的赶紧回家报备一下吧。”
组员乙:“放心。肯定支持。在你的带领下,我们组肯定越来越好的。”
组员甲:“老刘领着我们赚钱啊。”
小刘:“没问题。”

镜头画面五
小刘的内心独白:“哈哈哈。终于要走上人生巅峰了!”

镜头画面六
会议室里,小刘和组员小肖、小华一起开会。
小刘:“A公司下个季度的采购计划就要出来了。那需要咱们组出一份方案。上个季度的方案和输出我带着小华一起做的。但是我最近手头上的事情比较多。这次小肖你带着小华一起做怎么样。客户那边小华也已经比较熟了。”
小肖:“我这边没什么问题啊。小华怎么样?”
小华:“好的,没问题。”
小刘:“行,那我们下周二再过一下。”
小肖:“OK。”
小华:“好的。”

镜头画面七
周二上午,小刘和组员小肖、小华一起开会。
小刘:“哎,这个方案没有沿用我们之前的思路嘛。你们有没有看过上个季度的方案?”
小华:“嗯。我跟小肖讲过了。但是我们这次想尝试另一个想法。”
小肖:“对。我们关注到这家客户正在做转型。所以用了不同的框架来展示我们的产品。那我觉得这个的话会更加适合我们的客户。”
小刘:“这样的做法会有些风险。对方的采购负责人一直以来都比较认同我们之前的思路。不确定他们是否会满意这个方案。要不然这样吧,我们先保留这个方案你们再出一个备选怎么样?”
小肖:“好呀。那我和小华再看一下。”
小华:“嗯。”

镜头画面八
周三下班前,小刘工位卡座。
小肖:“老大,我和小华又做了一版方案。回头你看一下?”
小刘:“好。你先放这,我马上看。”
小肖:“行。”

镜头画面九
小刘的内心独白:“这做的都什么玩意儿?又得打回去重新让他们做。哎,算了算了。明天就要交了,这次我先来吧。这都5点半了,今儿又要熬了。和小丽的面谈推到明天吧。今天晚上的课程也没空听了。”

情景剧结束。

镜头画面十
蒋芸,成年女性,肯耐珂萨专家讲师。十多年企业管理实战经验。曾任世界500强企业高级经理人。
蒋芸:“你好,我是蒋芸。是《明星经理人成长营:角色认知》这门课程的老师情景剧里,小刘被任命为部门经理。他踌躇满志,想要从此带领团队一飞冲天。但是理想很丰满,现实却很骨感。小刘很快就碰到了问题。下属做的方案不合适。只能自己加班加点做。管理之路,好像没那么容易。那么小刘的问题出在哪里?又该如何解决呢?你是不是也和他有相同的困惑呢?接下来就让我们一起进入到这门课程的学习。一起探寻答案吧。”

关键词

首先看看音频和视频提炼的关键词的异同。我列出该词实际在对白中出现的次数并对它们的关键程度作出人工评价:

音频视频词频关键程度
老大老大5一般
业绩业绩3重要
季度3不重要
客户客户3重要
离职离职2一般
managermanager4重要
商量商量2不重要
职位职位2重要
信任2不重要
表1 音视频提炼关键词对比

老大、离职:王经理作为旧老大离职,是小刘接替成为新老大原因。

业绩:小刘、小肖、小华都面临他们上级的业绩压力。

季度:属于小刘部门员工的业务计划的具体细节,可替换为其他细节不影响剧情表达。

客户:小刘部门的业务主要面对的就是客户,不可替换。

Manager、职位:该情景剧主要发生在小刘升任部门manager一职前后一小段时间。

商量:吴总的客套话。

信任:小刘的客套话。

根据上面的分析可以得出,音频和视频提炼的关键词基本一致。但是一些不是很重要的词也被纳入关键词里了。

全文概要

以下是音频转写的全文概要:

全文概要(音频转写)

吴总和小刘进行了商量,吴总告诉小刘最近公司的人事变动,王经理提出了离职,空出了manager职位,吴总认为小刘相对适合这个职位并希望他能够担任。他们商定后,吴总会请HR走一些流程,小刘表示一定会做好准备。另外,他们还讨论了下一季度的采购计划,小刘和小华做了一个方案,但吴总认为不合适,建议保留备选方案并重新制定。最后,吴总还提到了蒋云老师的课程《明星经理人成长营》,提到了老刘的问题和解决方法。

这段概要有如下问题:

  1. 第三句的“他们”指代不清,如果和第二句的“他们”一样指的是吴总和小刘的话,那么就是错误的。
  2. 应该是小肖和小华做方案,小刘认为不合适。
  3. 吴总并没有提到蒋芸的课程。

以下是视频转写的全文概要:

全文概要(视频转写)

吴总和小刘讨论了小刘所在团队的人事变动,吴总提出了让小刘担任新的manager职位的想法。小刘对此表示感谢并表示有信心带领团队取得更好的成绩。吴总还告诉小刘需要准备一些东西,以便能够更好地胜任这个职位。另外,还有一位老师介绍了一门关于经理人角色认知的课程。

这段概要的问题是小华和小肖提出的方案让小刘犯难这一关键剧情没有概括到。

章节速览

以下是音频转写划分的章节速览:

小刘对公司人事变动的看法。

吴总与小刘商量公司销售部门的人事变动,其中王经理提出离职,空出了经理职位。吴总认为小刘适合担任这个职位,并询问他的想法。小刘感谢吴总的信任,表示自己在这家公司近七年来一直跟着王经理做项目,并开始带自己的徒弟,他相信自己可以带领团队做出更好的成绩。

对hr的要求及其人员的安排。

一位员工被告知将成为团队的经理,他的主管向他表示祝贺并商定了后续的流程。主管希望他能够准备好,因为作为经理和组员是不太一样的,他对他有信心。主管还承诺要超过之前定下的业绩,并给每个人多发奖金。他表示感谢并希望大家继续支持他。最后,他邀请大家今晚一起吃饭,表示感谢并展望团队的未来。

关于采购方案的讨论。

讨论了A公司下季度的采购计划方案和输出,并决定由小华和小乔一起合作完成。他们关注到一家客户正在做转型,因此决定尝试另一种展示产品的方案。虽然存在一定风险,但他们认为这个方案更适合该客户。在讨论中还提到了之前的方案和思路,以及保留这个方案并再出一个备选方案的建议。最后,蒋云老师介绍了一门名为明星经理人成长营的课程,其中包含了老刘作为部门经理所面临的问题和解决方法。

以下是视频转写划分的章节速览:

对员工离职的原理。

吴总与小刘商量公司销售部门的人事变动,王经理提出离职,公司考虑评估小刘作为接替者。吴总表示感谢对小刘的信任,并肯定其在公司的工作表现,认为他相对适合担任经理职位。小刘也表示愿意承担这个机会,带领团队做出更好的成绩。

对hr的要求及其人员安排。

一位员工被告知将成为团队的经理,他的主管向他表示祝贺并商定了一些后续流程。在此期间,主管提到了该员工在担任组员时与担任领导者时的不同,并表示对他的信心。主管还向员工保证将来会有更多的奖金和发展机会。最后,主管请求其他员工支持和保留该员工,并宣布今晚他将请客庆祝。

关于采购方案的讨论。

讨论了A公司下季度的采购计划,并决定由小华和小乔一起合作。他们计划针对一家正在转型的客户制定不同的方案,但也准备了一个备选方案。讨论中还提到了之前方案的成功和新方案的风险,以及蒋老师的一门关于管理的课程。

从剧本镜头可知内容大概可以分为以下几部分:

  1. 镜头一至镜头三:吴总打算升小刘为销售部门经理,小刘洋洋自得。
  2. 镜头四至镜头五:小刘新官上任,踌躇满志。
  3. 镜头六至镜头九:小肖和小华能力不足。小刘紧急救火,心生埋怨。
  4. 镜头十:蒋芸总结情景剧,引出课程。

对比之下可以看出,无论是音频还是视频,其“断章取义”能力都非常不足。


发言总结

全内容有发言的角色按首次发言先后顺序分别为:小刘(老刘)、吴总、组员甲、组员乙、小肖、小华、蒋芸。共有7人。

音频方面自动划分出10个发言人,比实际上多3个,即音纹识别方面出现误差。部分发言内容太少,不能总结。已总结出的有:

发言人1的总结

首先感谢吴总的信任和肯定,并承诺尽快处理相关事务。他自豪地提到自己在这家公司已经有近七年的经历,跟着老大参与了不少项目。如果有机会担任这个职务,他相信可以带领团队取得更好的成绩。最后,他向吴总表示今后会继续努力。

从内容可知,发言人1为小刘。

发言人2的总结

告诉小刘,老大要离职,自己终于要成为manager了,并且决心要超过之前老大定的业绩,给每个人多发奖金,让大家都开心地把活干完。他还表示跟着他这样的老大有优势,并且自信能够管理好团队。然而,他也抱怨了一些工作上的问题,并决定先自己处理。最后,他提到今天晚上要熬夜,并取消了和小丽的面谈。

发言人2混合了多个角色的发言,归纳错误。

发言人3的总结

召集小刘商量公司人事变动,小刘被提升为资深销售经理,并有望接替离职的王经理担任manager职位。他表示对小刘的个人工作表现一直满意,希望他能够带领团队继续前进。HR流程走完后,会宣布通知大家。

从内容可知,发言人3为吴总。

发言人10的总结

他为蒋云,是明星经理人成长营角色认知课程的老师。他提出了老刘作为部门经理的困境,并邀请听众一起学习这门课程,探寻解决方案。

发言人10明示是蒋芸,但是蒋芸是女性角色,“他”字错用。

视频方面自动划分出11个发言人,比实际上多4个,即音纹识别方面出现也误差。同时也可以得出AI没有深入分析视频的内容。部分发言内容太少,不能总结。已总结出的有:

发言人2的总结

感谢吴总的肯定和信任,并介绍了自己在公司的工作经历和成就。他表示自己已经带了不少徒弟,并相信可以在未来带领团队做出更好的成绩。最后,他表示今后会继续请吴总多多指导。

从内容可知,发言人2为小刘。

发言人3的总结

讨论了公司销售团队的人事变动,提到了王经理的离职和空出的manager职位,并表示认为员工小刘相对适合担任该职位。他向小刘征求个人意见,并商定了后续流程,希望他能够准备好成为团队的领导者。最后,他嘱咐小刘先去忙,自己会与HR继续沟通。

从内容可知,发言人3为吴总。但是吴总是女性角色,“他”字错用。

发言人四的总结

表示自己即将成为manager,希望能够带领团队实现更好的业绩,并给每个人多发奖金。他自信满满地表示跟着他这样的老大有优势,并且称赞自己的管理能力。此外,他还提到了一些工作中遇到的问题,但并未透露具体是什么问题。最后,他表示今晚要熬夜加班,并推迟和小丽的面谈到明天。

从内容可知,发言人3为小刘。但是没有和发言人2的内容融合。

发言人11的总结

他介绍了自己是明星经理人成长营角色认知课程的老师,通过一个情景剧描述了老刘作为部门经理的问题和困惑,并提出了一个问题:你是否也遇到了相似的问题?邀请听众一起参与课程学习,寻找解决方案。

从内容可知,发言人11明示是蒋芸,但是蒋芸是女性角色,“他”字错用。

综上分析可知,发言人的序号与角色出场说话的顺序一致。


问题回顾

问题回顾属于实验性的功能,AI识别出内容中的问句,并标记时间。点击后可以转跳到音频或视频。

图3 问句识别功能

音视频在这方面表现一致。问句识别涉及到语法、语气和音调等语言学要素的识别。并非内容中的所有问句都被识别到。

在情景剧中该功能不是很重要,不过多介绍。


对白识别

上述的内容实际上都是建立在对每一句对白的识别上的。

图4 对白呈现

音视频的对比呈现方式一致,都是以这种对话框的方式呈现。点击对白可以转跳到相应的播放进度。

可以修改发言人的名称。修改时,可以指定单一对话框的发言人名称。也可以指定所有对话框的该发言人名称。

可以修改对白内容。可以直接编辑对白内容。可以回车新建对话框。也可以后退合并对话框。

一些口癖和语气助词,如“呃、嗯、呢,啊”等,不会被转写到对白文本中。

目前语音识别为文字的识别率比较高。除了个别字因口音、轻重音等问题导致识别错误,大部分都成功识别。但如前述,发言人划分方面表现较差。在该情景剧中,发言人划分面临的挑战包括:同一个人的电话交谈音色与面对面交谈音色不同。同一个人的内心独白音色与多人对白音色不同。多人发言重叠等。

遗憾的是,目前即使修正了全部对白文本,前述的由AI生成的关键词、全文概要、章节速览、发言总结、问题回顾等内容也不会更正。


导出对白

可以将转写的对白文本导出为doc、pdf、srt三种格式。

图5 导出的doc文本

可以选择是否需要附上时间戳。


BARD的答案

我使用以下Prompt向BARD提问:

Prompt(BARD)

请根据以下的情景剧剧本,回答一些问题:
“我结合字幕听写的剧本全文”
问题一:请提炼出该情景剧的关键词。数量不少于5个。
问题二:请写出该情景剧的全文概要。
问题三:请给该情景剧划分章节。用一句话最大程度概括每一章节的内容。并用一段话总结该章节的内容。
问题四:请总结每个角色的发言。

以下是BARD的回答:

关键词

新经理、新工作、压力、挑战、成长

全文概要

小刘是一家公司的资深销售人员。他被任命为部门经理。他很兴奋,也感到压力。他希望能带领团队取得成功,但他不知道该怎么做。他遇到了很多挑战,但他也从中成长。最后,他成功地带领团队取得了成功。

章节速览

  • 第一章:小刘被任命为部门经理。他很兴奋,也感到压力。他不知道该怎么做。
  • 第二章:小刘面临很多挑战。他的下属不听他的指挥,客户不满意他的方案。
  • 第三章:小刘克服挑战。他学习如何管理团队,如何制定方案。他也学会了如何与客户沟通。
  • 第四章:小刘取得成功。他的团队取得了销售业绩的突破。客户也对他的方案非常满意。

发言总结

  • 小刘:他是一个有理想有抱负的人。他希望能带领团队取得成功,但他也知道这并不容易。他遇到了很多挑战,但他也从中成长。最后,他成功地带领团队取得了成功。
  • 吴总:她是一个有经验的领导者。她信任小刘,也给予了他支持。她帮助小刘克服了挑战,取得了成功。
  • 小肖和小华:他们是小刘的下属。他们年轻有活力,但也缺乏经验。小刘帮助他们学习和成长。最后,他们也成为了团队中不可或缺的成员。

从BARD的回答可以看到,它在胡编剧本没有的内容。小刘在该情景剧中只是在面对困难,并没有完全克服困难并带领团队取得成绩。小肖和小华也还没有成长。


GPT4的答案

我使用以下Prompt向GPT4提问:

Prompt(GPT4)

请根据以下的情景剧剧本,回答一些问题:
“我结合字幕听写的剧本全文”
问题一:请提炼出该情景剧的关键词。数量不少于5个。
问题二:请写出该情景剧的全文概要。
问题三:请给该情景剧划分章节。用一句话最大程度概括每一章节的内容。并用一段话总结该章节的内容。
问题四:请总结每个角色的发言。

以下是GPT4的回答:

关键词

提拔、经理、责任、团队管理、角色转变

全文概要

该情景剧讲述了小刘从一个资历丰富的销售人员被提拔为经理的过程。老板吴总对小刘的工作表现和能力给予了充分的肯定,并希望他在新的角色中可以带领团队取得好成绩。小刘对此表示热切的期待,但在接手工作后发现,管理团队并非想象中那么简单,他不得不加班处理一些麻烦事,感受到了责任和压力。

章节速览

  • 第一章:小刘被任命为经理。在和吴总的对话中,小刘被告知将担任经理的位置,他对此充满期待,并表示会全力以赴。
  • 第二章:小刘上任的第一天。小刘宣布他的打算,并从团队那里收到了信任和支持,他振奋的计划着以后的工作。
  • 第三章:小刘开始了管理工作。小刘开始分派任务,对组员的工作提出了建议,并开始面临到实际的问题,他发现管理工作并不像他想象的那样简单。
  • 第四章:小刘的反思和困惑。在经历了一段时间的管理工作后,小刘发现团队的问题需要他进行解决,他开始感受到压力。

发言总结

  • 小刘:对被提拔为经理表示高兴和期待,表示会全力以赴,但在实际操作中遇到了问题,表现出困惑和压力。
  • 吴总:肯定了小刘的工作表现和能力,并对他新的经理角色寄予了期望。
  • 小肖和小华:对小刘的指示表示服从和合作,但也在工作中表达出自己的观点和想法。

文心一言的答案

由于文心一言每次输入限制2000字符,我使用以下两部分Prompt向文心一言提问:

Prompt(文心一言 1/2)

请根据以下的情景剧剧本,回答一些问题:
“我结合字幕听写的剧本开始至镜头七”

Prompt(文心一言 2/2)

“我结合字幕听写的剧本镜头八至结尾”
问题一:请提炼出该情景剧的关键词。数量不少于5个。
问题二:请写出该情景剧的全文概要。
问题三:请给该情景剧划分章节。用一句话最大程度概括每一章节的内容。并用一段话总结该章节的内容。
问题四:请总结每个角色的发言。

以下是文心一言的回答:

关键词

新任部门经理、团队管理、方案不合适、加班加点、碰到了问题、困惑、学习、探寻答案

全文概要

该情景剧描述了一个刚被任命为部门经理的员工小刘,面临着如何带领团队、解决团队中出现的问题的挑战。他试图在第一次开会时提出一个方案,但被其他团队成员的方案替代。他感到不安,并在后续的会议中试图解决问题,但最终方案还是被打回重新做。小刘感到困惑和疲惫,同时也意识到管理并不像他想象的那么简单。

章节速览

  • 第一章:小刘被任命为部门经理,吴总找他商谈。
  • 第二章:小刘在会议中提议的方案被团队成员替代。
  • 第三章:小刘试图在后续的会议中解决问题,但方案还是被否决。
  • 第四章:小刘感到困惑和疲惫,意识到管理并不简单。

发言总结

  • 小刘的发言包括:对吴总提出自己想法的肯定和感谢;表达对王经理离职的惊讶;表示对成为manager的信心和对团队未来的期望;对组员表示信任并承诺会提供支持;确认新的方案并安排后续的会议。
  • 吴总的发言包括:对小刘的工作表现表示肯定和满意;告知小刘王经理离职的消息并表达遗憾;提出让小刘担任manager的建议并征询他的意见;对小刘表示信任并鼓励他担任好这个角色;安排HR进行相关流程并向外公布消息;邀请小刘一起庆祝并表示后续会提供指导。
  • 组员们的发言包括:对小刘成为部门经理表示祝贺和信任;提出新的方案并表示愿意支持小刘;对小刘的决策表示理解和配合;同时也表达了对自身工作成果的肯定和自信。

四个大语言模型对比

关键词:通义有比较多非重点的内容作为关键词。BARD和GPT4的关键词概括比较准确。文心一言不能提炼出关键词。

全文概要:通义有张冠李戴和遗漏重点的情况。BARD编造剧本中没有的内容。GPT4前面大部分都概括得比较好,但和BARD、文心一言都遗漏了“蒋芸”部分的内容。文心一言对人物关系理解不到位。

章节速览:通义的章节划分基本错误。BARD编造剧本中没有的章节。GPT4章节划分比较正确,但是和文心一言都遗漏了“蒋芸”部分的内容。文心一言部分章节总结与全文概要一样理解有误。

发言总结:通义使用音频划分发言人,与BARD和GPT4直接用剧本的难度不一致,不具可比性。BARD纯属放飞自我,胡编乱造。GPT4基本能够根据发言文本合理总结出每个人的剧情走向,但仍然遗漏了“蒋芸”部分的内容。文心一言的发言总结大部分正确,但也存在张冠李戴等明显错误。


相关资源

通义听悟:https://tingwu.aliyun.com

BARD:https://bard.google.com

GPT4:https://chat.openai.com

文心一言:https://yiyan.baidu.com

网课视频:https://www.mlearning365.com/courseDetail.html#/videoPlay/5292

分类
Technology|技术

一种 PDF 电子文档规整方法

前期准备

某些人群在工作和学习中,可能会接触到较多的 PDF 格式的电子文档。PDF 是由 ADOBE 公司发明,基于 PostScript 编程语言制作的格式。多年以前,它一直是 ADOBE 公司的专有格式,直到被 ISO 标准化后,现已成为一种开放标准。

市面上能够查看、编辑、转换 PDF 格式的应用程序有很多。它们之中有功能收费的,也有完全免费的。有轻量绿色的,也有套件完备的。下面介绍的方法实现,基于其中的一款名为 BookxNote 软件。

官方网站http://www.bookxnote.com/
本文所用版本直链地址http://www.bookxnote.com/setup/bookxnote-windows-x86-1.0.0.193-20200610.zip
SHA1BF8E4A07DFCB388FFC6E3170C987BCC3A297FA68

下载、解压并安装后,可以直接打开该应用程序。在左侧菜单栏,点击“关于”即见作者对该程序的宣传语。

全新电子书阅读笔记软件,助力您的高效学习之路。

BookxNote

BookxNote (以下简称“BN”)是一款轻量级的、免费的、在 Windows 操作系统下使用的 PDF 管理与查看软件。下面,就着 BN 来介绍一种 PDF 电子文档的规整方法。BN 同时也支持 EPUB 格式,有兴趣的读者可以自行研究,本文仅涉及 PDF。

配置存储路径

打开 BN 后,在左侧菜单栏中点击“选项”会弹出一个新的窗口。该窗口中包含了 BN 的所有可配置项。首先要注意的是“基本设置”里的第一和第二项。

在文件系统的恰当的位置中新建一个名为“DigitalLibrary”的文件夹,再于其中新建一个名为“Content”的文件夹。形如 E:\DigitalLibrary\Content

在 BN 的“用户数据保存目录”栏,点击修改目录,定位至 DigitalLibrary 文件夹。此时 BN 提醒需要重启软件后才能使设置生效,可暂时不用理会。再在 “我的图书馆工作目录”栏,点击修改目录,定位至 Content 文件夹。然后重启软件,界面应如下图所示。

BN 基本设置

导入电子文档

在 BN 的左侧工具栏中选择“打开”,点击顶部的“导入书籍按键”,然后在弹出的文件框中选择想要导入的 PDF。此时界面中将会出现一条书籍记录。

BN 第一次导入

双击该书籍记录即可阅读对应的 PDF 文件。在导入完成后,该 PDF 将按原名称复制一份到“我的图书馆工作目录”,即 Content 文件夹。以后在 BN 内的“我的图书馆”页面中打开的该书籍,都是指向 Content 文件夹中的 PDF,而与被复制的源文件无关。

规则一:不要直接对 DigitalLibrary 目录及其子目录进行写入操作。

何为写入操作包括往目录中新建文件或文件夹、粘贴文件或文件夹、删除文件或文件夹、修改文件或文件夹名及修改除 PDF 外其他文件的文件内容。

此时,再次导入相同来源的 PDF 文件还是可以导入成功的,书籍记录会出现相同名称的条目。若再次导入,则会提示“(源文件目录)”的书籍已存在,无需添加。若将源文件复制到其他目录,再在其他目录导入,还是可以导入成功的。

因此同 PDF 文件名的书籍可存在多本,第一本会指向 Content 文件夹的 PDF,其余会指向复制来源的 PDF。同一来源的 PDF 文件对应的书籍只会存在一本,但在书籍记录中无法便捷地区分,因为它们的名称都一样。

规则二:同一文件名的 PDF 文件只导入一次。

规范文档名称

在理论上,PDF 文件的文件名可以任意修改为符合 Windows 系统命名要求的字符串排列。因此,不同来源的 PDF 很有可能会发生重名的情况。为了保证“规则二”不被违反,需要对将被导入的 PDF 文件名进行规范约束。

规则三:采用“来源.编号.版本号(可选).pdf”的格式约束文档名称。

就如 JAVA 的包名和 Android 的 application ID,为了避免重复,除了概括内容的标题外还需要加入专有的限定符。

在本方法中,“来源”名一般指 PDF 文件的第一发布方。可以是自然人名、组织团体名或商业公司名等。书写形式可以是一级域名或常用缩写,甚至是美股代码。

“编号”可以是 PDF 文件的一级标题。但有时一级标题会过于冗长,或包含不能作为 Windows 文件名的字符。所以在有文档来源对 PDF 的编号的情况下,尽量使用该编号。

对于电子书籍来说,同样具备实体书籍的再版特征,每次再版都可能会修改内容。尤其对电子类的文档,会随着时间的推移进行修订和勘误。但是,版本的变化有时会反映在“编号”项上,有时却不会。为了统一命名规则,在文件名中引入“版本号”项。若文档本身使用数字代表的版本号,则可以直接沿用。若使用α、β作为版本号,则使用小写字母a、b代替。若没有版本号,则使用文档内标注发布的日期作为版本号。若文档甚至没有标注发布日期,或类似于论文这种不经常修订的内容载体,可以忽略版本号或使用文档导入的日期代替。

规则四:版本号的格式为V[数字版本号]|[小写字母版本号]|[四位字符年三字符月两字符日]

如下图所示为一种有效的文档命名方式。

文件命名范例

在将规范命名后的文档导入“我的图书馆后”,书名处显示的为规范名称。规范名称虽然有利于计算机整理,却不利于人类阅读。右键点击书名,选择修改书名,键入文档的一级标题或觉得有利于自己分辨书籍的名称。

修改书名

修改书名的操作不会影响在 Content 目录中的实际文件名,只会影响在“我的图书馆”中显示的名称。

归类电子文档

导入 PDF 文件后,它们会全部存在于“所有书籍”的一级分类下。当分类下的书籍逐渐变多后,就有必要对齐进行分类整理。就类似于手机上安装的应用变多后,需要将一些功能类似的应用图标从一级的 Launcher 界面归类到二级集合中。

每个读者的“馆藏”内容都不一样,这里并不能提出一个适合所有读者的分类方法。因此,仅给出一些笔者遇到的电子文档的分类规则。

规则五:对于在国内出版的电子书,根据其 CIP 编号归类。

例如,有一本《线性代数》的书籍,其 CIP 描述页面上标注的分类号为 O151.2。即应该归类在【O 数理科学和化学】-【O1 数学】-【O15 代数、数论、组合理论】-【O151 代数方程论、线性代数】-【O151.2 线性代数】。

规则六:对于非在国内出版或其他无 CIP 编号的电子书,参照中国版本图书馆分类法对其归类。

读者可以在一开始就建立如此多级的目录,并将每一本书籍归入具体目录内。亦可先建立二级目录,在二级目录放了7本书的时候才新建出三级目录,如此类推。笔者更倾向于第二种方法,避免“过早优化”,但在此不作为规则要求。

规则七:对于标准文件,根据 ICS 编号归类。具有多个 ICS 编号时,优先考虑分入已存在较多标准文件的目录。

考虑到若某个目录已经存在较多的标准文件时,理论上说明使用者更加关注该领域。所以把有多个 ICS 编号的文件,编入该领域更便于信息利用。

规则八:对于某具体型号电子元件有关的文档。二级目录按制造商区分。三级目录按产品线区分。四级目录按二级产品线区分。在制造商级别之下,可参各个照制造商自身的分类方法。

例如前面图片提到的《Getting started with STM32F10xxx hardware development》文档,内容是介绍 STM32F10xxx 系列的硬件电路设计方法并与该系列密切相关。该方法并不完全适用于 STM32F4 或 STM32F7 系列。所以,它应该被归类到【ST】-【微型控制器】-【STM32F1】目录中。

规则九:对于一般性的电子元件有关的文档。二级目录按制造商区分。三级目录按产品线区分。四级目录按技术种类区分。在制造商级别之下,可参各个照制造商自身的分类方法。

例如《USB hardware and PCB guidelines using STM32 MCUs》一文是介绍使用 STM32 微型控制器的 USB 接口时,PCB 应该如何设计。它与其他厂商的 IC 无关,也与同厂商的微型处理器无关。所以,它应该被归类到【ST】-【微型控制器】-【USB】目录中。

总而言之,一个好的归类方法应该能够在最大程度上对同一种电子文档进行归类。图书有图书的方法,标准有标准的方法。若现时没有业界统一的方法,就应该根据行业经验编制出合适的方法,而后在使用中去修改调整。

具体的实现方式为,在“我的图书馆”界面,点击书架分类右侧的“+”按钮,或者书架处右键选择“新建子书架分类”。在选中二级“子书架”的时候,再做同样的操作,即可在其下新建三级子书架。

书架分类

数据备份与还原

BN 是一个 PDF 文件查看器,并不具备编辑 PDF 文件内容的功能。PDF 文件在导入后,会生成一个对应的 UUID。然后在便携式数据库中新增一条 PDF 文件存储路径和 UUID 对应的记录。之后,在 BN 的使用过程中, UUID 便指代该 PDF 文件。

对 PDF 文件的“修改书名”操作和“新增子书架分类”操作,实际上是通过额外的格式化文件存储的。而这些文件一旦丢失,则这些修改都会消失。它们被保存在“用户数据保存目录中”,即 E:\DigitalLibrary\Workspace\BookxNote 路径下。因此,想要备份整个“我的图书馆”数据,只需要将 DigitalLibrary 文件夹压缩打包即可。也可利用版本控制工具进行增量备份。

当 PDF 文件较多时,Content 文件夹将会占用很大的空间,可能不足以支持日常备份。使用 BN 选项中自带的“备份”功能和“还原”功能,可以仅备份和还原用户数据部分。而 PDF 文件本体可以采用其他策略备份。

规则十:定期备份。
分类
Technology|技术

基于3-2-1备份策略的存储实践

前言

免费、便捷的存储大行其道的今天,数据似乎变得越来越没有存在感。昨天好友发来了一张趣图,今天打开对话框就能转发给别人。上午没写完的方案保存在桌面,下午回来接着写。开会要用到的演示文档,复制到U盘里再粘贴到投影仪专用电脑上……诸如此类的理所当然,归功于产品经理、电子工程师、计算机科学家和软件程序员的不懈努力。

但是,这些数据安全吗?所谓安全,一是想删掉的数据没有任何手段可以找得回来,二是想保存的数据不会因任何意外而丢失,三是未经允许的任何实体都不能获取到数据。然而世界上的意外和恶意如此之多,没有人和方法能够做到滴水不漏。社交媒体上是否时而能看到误删聊天记录无法恢复的哀怨?有没有遇到过软件崩溃导致没及时保存的设计稿灰飞烟灭的情况?经历过U盘所插过的电脑全部中勒索病毒的绝望吗?

除此之外,从数据的载体上看,每种介质都有其天然的缺陷。光盘与机械硬盘的盘片非常害怕划痕。基于电子势态或者逻辑门的各类内存除了自身的读写次数有限外,还对周围的电磁环境和电路稳定性十分敏感。而网络存储,没有哪个空间供应商能够保证万无一失的。

某网盘的免责声明

鉴于网络服务的特殊性,用户同意xx网盘服务有权随时变更、中断或终止部分或全部 的网络服务。如变更、中断或终止的网络服务属于免费网络服务,xx网盘服务无需通知用户,也无需对任何用户或任何第三方承担任何责任。

某笔记软件的免责声明

在法律允许范围内,我们对以下情形导致的服务中断或受阻不承担责任:
网络服务特性而特有的情况,包括但不限于基础网络运营商或网络设备技术提供商的故障、计算机或互联网相关技术缺陷、互联网覆盖范围限制、受到计算机病毒、木马或其他恶意程序、黑客攻击的破坏。

某聊天软件的免责声明

xx不对你在本软件及服务中相关数据的删除或储存失败负责;
xx有权根据实际情况自行决定单个用户在本软件及服务中数据的最长储存期限,并在服务器上为其分配数据最大存储空间等。你可根据自己的需要自行备份本软件及服务中的相关数据;

没有人(代指“实体”)敢为数据安全兜底,没有人有能力为数据安全兜底。而这篇文章讨论的是,基于3-2-1备份策略在数据存储方面促进数据安全。

3-2-1备份策略的基本思想

3-2-1备份策略指的是,3份副本,2种介质,1个异地。举两个简单的例子说明之:

  1. 在家里的电脑上新建一个文档,在文件夹里复制多两份。这满足了“3份副本”的原则,即使不小心把剪切按成了删除,还有2份副本可供恢复。但是不满足后两个原则。
  2. 在上面的基础上,把其中一副本刻录到光盘里。这就额外满足了“2种介质”的原则,即使硬盘中毒挂掉了,还能从光盘里把文档恢复到别的电脑上。
  3. 在上面的基础上,把光盘放到公司。这就额外满足了“1个异地”的原则,即使家中失火,光盘里的数据还能够安然无恙。

可见,要实践这个策略其实并不复杂,仅需本地留两份副本,再把一份副本复制出来放到别处去即可。然而真正实践起来,还有其他需要注意的地方。3、2、1仅是最低限度的要求,提升这些数字能够降低数据丢失的几率。一旦其中一项、两项或三项标准不能满足了,应该尽快恢复到能够满足最低限度的状态。对一台设备上产生的一批数据做备份的确不复杂,但是对多台设备不断产生的数据组织备份计划才是日常生活中的情景,才是工程学上要解决的实际问题。

实践过程

利益声明:本博客仅作分享技术用途,由作者全资运作。作者不靠任何由本博客产生的价值营利。过去、现在、将来没有也不会接受任何形式的推广、资助或捐赠。下文由于涉及到实践细节,将会不可避免地出现具体厂家。

硬件准备

  • 一台NAS。品牌:威联通/QNAP,型号:TS-551-2G,参考价格:¥1489。
  • 两块硬盘。品牌:西部数据/Western Digital,型号:My Book 8TB,参考价格:¥868.31✖2。
  • 一台路由器。品牌:华硕/ASUS,型号:RT-AC86U,参考价格:¥899。
  • 一台UPS。品牌:施耐德/APC,型号:BK650-CH,参考价格:¥494。

NAS的作用把存储空间独立在日常使用的电脑之外。购买该NAS是因为它自带了根据3-2-1备份策略设计的软件,其他品牌的NAS和自组主机也可以。但要充分考虑系统稳定性和功率。

硬盘选择了两块同型号的8T氦气盘。硬盘容量根据已有数据和计划存储的数据量决定。要考虑硬盘的噪音和散热。

路由器使用了内外网口都是千兆的无线路由器。在有足够接口带宽的支持下,数据传输才不会成为瓶颈。

突然掉电会损坏机械硬盘,加一台UPS作为后备电源能够避免意外掉电带来的危害。UPS应该选择带通讯接口的,目的是通知NAS及早进入保护模式或安全关机。

实践步骤和要点

首先对所有硬件进行基本检查,防止买到翻新件、二手件,有防伪码的就去验证一下。对硬盘进行基本的检测,包括但不限于读写测试、坏道测试、S.M.A.R.T评判。然后进行一段时间的普通使用再重复检测一次。如果硬盘有暗病,有很大几率在初期使用的时候就会显现出来。在模拟断电的情况下测试UPS是否正确启动。在硬件完全准备就绪之后才进入下一步,硬件是一切方法的根基。

下一步是把硬盘妥当地安装在NAS的插槽中,然后锁入NAS盘位。为了让两块8T硬盘只保存纯粹的“数据”,额外放了一块小硬盘保存NAS系统的应用程序。首次开机,跟随向导进行一系列初始化配置。在此过程中,可以先不处理数据盘,只在软件盘划出分区安装应用即可。

等待成功进入NAS的操作系统后,就可以开始思考如何设计存储策略了。

从分散到集中

每分每秒,都在产生数据(特指已数字化的)。有因工作而产生的,有因社交而产生的,也有纯粹属于内省的数据。这些数据分散在手机、电脑等数码设备中。存储策略的第一步,就是把分散的数据集中起来。

数据从分散流向集中

数据一开始被数字化的地方可以是多种多样的,最后都被集中到NAS中处理。传输的途径可以是内网内无线到有线,可以是内网内有线到有线,可以是内网穿透到外网再穿透到内网,甚至是从NAS主动抓取互联网的数据。数据传输手段和网络拓扑形式不在本文讨论范围之内。总之,在此实践中,数据都被集中到NAS中存储了。

需要使用RAID吗?

关于RAID的基本概念不在本文的介绍范围之内。有两块相同容量的硬盘时,无非只有两种选择:RAID0,数据被撕开两半分别存储于两个硬盘中,总可用容量为两个硬盘容量之和。因为可以同时从两块硬盘上访问数据,理论上读写速度是所有RAID中最快的。但是当其中一块硬盘坏时,丢失全部数据。另一种选择是RAID1,数据被镜像存储于两个硬盘中,总可用容量为单个硬盘。由于两个硬盘内的数据一模一样,理论上防损性是所有RAID中最好的。当任何一块硬盘坏时,不影响另一块硬盘中的数据。

看起来RAID1很符合3-2-1策略,使用这种模式自然地会有两个副本。但是基于两个原因,此实践不使用RAID。一,RAID的设计目的是提高可用性。要理解高可用性,就要同步理解容错和灾难恢复。借用很经典的三个例子来描述这三个概念。容错,就像有多发动机的飞机。在任何一个发动机罢工,但还没有爆炸的前提下,飞机还是可以靠剩余的发动机完成降落。灾难恢复,就像战斗飞机的弹射座椅。在飞机中弹后,还能借此逃出生天。高可用性,就像越野车的备胎。当轮胎被扎破了,花少量时间换上备胎,接着进行任务。备胎是给车子快速换上,让车子在最短时间内能够跟之前没两样地继续跑起来。而RAID在硬盘损坏后,凭借健康硬盘中存在的部分或全部,已损坏了的硬盘中的数据,尽快让存储系统恢复到硬盘损坏前的状态。这对企业来说是非常重要的,停止服务的时间每多一秒中都会带来经济上的损失。而对家庭用户来说,没有那么高的可用性需求。今天看不成十年前的照片,明天看也一样,只有数据还能被恢复即可。二,使用RAID1是会无条件地全盘复制到另一个硬盘上,对存储空间的占用极大。有些数据在网上随处可见,存在NAS中只是为了方便访问,即使丢失了也随时可以重新下载。这种数据就没必要保存副本了。

存储池和卷

当确定了每块物理硬盘都独立使用后,就确定了存储池的形式。按照威联通NAS的存储空间组织形式,最底层的存储单位就是一块块物理硬盘。然后在上层使用软件的方式把符合条件的物理硬盘以一定的方式,组织成RAID(或独立使用)。之后,将一个或多个RAID或独立硬盘融合成存储池。若两块8TB的物理硬盘以RAID0的方式组成一个16TB的空间,再将这个空间组成存储池后,用户只需把它当成一个16TB的“硬盘”使用,至于底层怎么将数据分布存储在每块物理硬盘中的,用户无需关心。

然而卷才是被文件系统访问的基本单位。按照威联通NAS的存储空间组织形式,有三种卷。静态卷,直接构建在RAID或独立物理硬盘上,不能在存储池上构成。因而静态卷有最佳的读写性能,但是不支持快照和LUN等功能。厚卷和精简卷在存储池之上构建。一个存储池可以有一个或多个这两种卷,且不能跨存储池。厚卷的概念和Windows的逻辑分区类似,在创建之时就会占用固定大小的空间。这个空间不能超过存储池的大小。就像某些下载软件的空间占用策略一样,先用一大堆的“0”把要下载文件的空间占用了,然后随着下载的进程用真实的数据逐个覆盖掉占位用的无意义数据。精简卷是先声明要占用一定大小的空间,这个空间可以超过存储池的大小。因为存储池也可以在日后扩展空间的。但是精简卷只会实际占用被卷内有效数据占用的空间,因此多个精简卷的声明空间总和可以超过当前存储池的空间。例如存储池的空间有8TB。一,可以创建两个4TB的厚卷。二,可以创建7TB的厚卷加上10TB的精简卷。三,可以创建好几个10TB的精简卷。但无论哪种方式,在存储池不扩展的情况下,总共就只能存下8TB的有效数据。同时,由于机械硬盘的存储在物理上是连续的,应该尽量避免数据碎片化。

  • 几个物理硬盘组成RAID
  • 一个或几个RAID可以组成存储池
  • 一个或几个物理硬盘(非RAID形式)可以组成存储池
  • 一个或几个RAID混合一个或几个物理硬盘(非RAID形式)可以组成存储池
  • 厚卷和精简卷需要创建在存储池中
  • 静态卷需要直接创建在一个RAID或一个物理硬盘中
  • 厚卷和精简卷可以共存在同一个存储池中
  • 厚卷和精简卷不能跨存储池创建
  • 厚卷创建时就占用固定的空间
  • 精简卷创建时只声明需要占用的空间
  • 静态卷创建时占用全部的RAID或物理硬盘空间
  • 厚卷和精简卷都可以在创建后调整占有空间的大小
  • 厚卷占用空间的大小不能超过存储池
  • 精简卷占用空间(声明式)的大小可以超过存储池
  • 存储池可以在创建后再调整大小

NAS本地存储空间规划

基于上述的信息,结合手上已有两块硬盘(实际上插入NAS中有三块,其中一块用于保存操作系统软件,不参与数据存储实践)进行分析。首先,两块硬盘独立存在,不组RAID。然后,一块硬盘单独构建存储池,2TB的空间构建厚卷,用于存储需要永久保存的数据。1TB的空间构建精简卷,用于保存没那么重要,丢了也没所谓的数据。最后,另一块硬盘单独构建静态卷,用于定期从厚卷中备份数据。

实践的存储结构

为什么第一块硬盘要分厚卷和精简卷?快照等存储池功能是分卷实施的,分两个卷易于数据管理。需要永久保存的数据在本实践中变化是较为缓慢的,一旦有数据被划分为永久保存数据就需要进行保存,因此需要已经确定的空间来存储,分为厚卷最为合适。不需要永久保存的数据在短期内的变化是较快的,使用精简卷较为合适。

为什么第二块硬盘只作为静态卷使用?静态卷更接近硬件底层,因此有更高的性能。而且第二块硬盘的唯一作用就是备份第一块硬盘中厚卷的数据,不需要其他存储池特有的功能支持。越简单故障恢复的几率越大。

厚卷与精简卷的空间分配策略为何如此?厚卷作为永久保存数据的地方,首先要满足能够保存所有现有的需要被永久保存数据的大小。为了减少调整卷大小的频率,再留一定的余量。余量越大,后期需要调整分区大小的频率就越低。当然这也与数据总量的增速有关。精简卷可以不必过多关注,先随意分配一点空间就够了。但注意不要一次就把整个存储池占满,因为部分数据管理有关的操作需要存储池有未被分配的空间。当实际数据占用率已经接近物理存储空间的80%就需要考虑购入新的硬盘扩展存储池了。

完成NAS本地存储规划后,也就满足了“3-2-1”策略中的两个数据副本目标和一个存储介质目标。还剩一个数据副本,一个另外的存储介质和一个异地保存。

实践中的空间分配(由于系统功能占用了部分空间,数据可用空间少于硬盘大小)

云存储

最直接实现“3-2-1”策略的方式就是使用云存储服务。将一个副本放在云上,就能满足三个数据副本的目标。云上的存储可能是机械硬盘,也可能是固态硬盘,这不需要考虑太多,可以直接把云硬盘当做第二种存储介质。而且云本来就和NAS异地。

威联通在软件层面上支持远程NAS、Rsync服务器、FTP服务器、CIFS/SMB服务器等协议型服务器以及Amazon S3、阿里云、Azure、Google等商品云。在商品云中使用的是对象存储服务。

本实践中使用的是阿里云的OSS服务。购买了OSS的存储资源包和流量资源包后,新建一个Bucket用于存储数据。

然后新建一个子账户,并授予其OSS的管理权限。并获得其Access Key和Secret Key。

自动化备份

只有当备份的过程越接近无感,才能越容易地把备份策略执行下去。

在威联通自带的Hybird Backup Sync中配置两条自动备份策略。

  1. 源位置:本机NAS的厚卷(具体文件夹)到目标位置:本机NAS的静态卷(具体文件夹)。
  2. 源位置:本机NAS厚卷(具体文件夹)到目标位置:阿里云的OSS Bucket(具体文件夹)。

为防止数据混乱,以上自动备份策略皆是单向策略。要恢复数据需要手动进行,且不建议从同一路径回流数据。即在厚卷中误删了某个文件,不要直接从静态卷复制到厚卷。先从静态卷取到电脑,再从电脑上传到NAS的厚卷。数据以一个方向流动不易造成混乱,尤其在开启了版本管理的情况下。

备份计划

结语

本文解释了何为3-2-1备份策略,并介绍了其中一种实践方式。三份副本中,两份在本地的两个不同硬盘中,一份在云中。两种存储介质分别为机械硬盘和云端。一个异地为云端。

不想因为数据丢失而措手不及,最简单有效的方式就是多留备份。不同的备份方式对于减少数据永久丢失的贡献是不同的。不同的备份方式对使用者的知识水平和时间精力的要求也是不同的。

及早地行动起来,不要等失去之后才来后悔。

于本网站所提及之商标、服务标志、商号名称、标志、品牌、产品名称及其他商品表征,倘若非属 本网站 所有,均属个别所有人之商标或服务标志。本网站亦保留未详述的其他权利。

分类
Algorithm|算法 CSharp|C#

使用集合和高级查询计算指数或个股相关度

前言

近年来,各种互联网金融机构和基金公司一直都在教育散户投资者进行基金定时定额投资或定时不定额投资。关于基金定投的收益和风险不在本文的讨论之列,读者可自行在网上搜索。下面仅谈谈我个人关于基金定投(以下简称“定投”)的看法。

无论互金们怎么把定投吹上天都不要相信,最重要的是自己掌握复盘的方法。对历史行情复盘后发现,按照定投的方法,无论在何时买入,到某个时间点总能实现盈利。所谓Any time is the best time——只要市场行情的整体趋势是向上的即可。因此定投是比较适合没时间盯行情,又打算拿出一笔钱来进行长期投资的职场人士。

基金类型又分为很多种,依据定投的天然属性,越是波动剧烈的基金越是适合进行定投。所以,偏股类基金或者以某个指数作为投资标的的基金是最好的选择。同时,为了分散风险,即鸡蛋不放在同一个篮子里,可以同时选择多支指数/偏股基金进行定投。

要做,就要做到最好。为了最大程度的分摊风险,所选的基金行情走向应该呈现不相关甚至是负相关的关系。因为,若是选择了两只相关的基金,同时大涨固然好。若是同时大跌,又选择了定期不定额的“慧定投”模式,可能在几天之内就会把“子弹”(计划用于投资的资金)打光。所以,当选择了两只不相关的基金,在一只赚钱到达止盈线之后可以进行收割,也有更充足的“弹药”应对另一只走低的基金。

指定的两只基金的相关性还算是容易计算。抓取K线后复权的收盘价之后,用EXCEL里面的CORREL函数即可计算出来。几只基金两两之间的收盘价,辛苦一点也是可以扒拉出来的。但是几十只基金之间的呢?以及如何从中选择出相关性比较低的一种组合呢?

目标

我个人从去年就开始了第一笔定投,当时主要是结合股市常识选择了5个指数,哪些基金追踪的这些指数就不说了。上证50,代表大型公司。中证500,代表中型公司。创业板指,代表创业型公司。恒生国企指数,代表港股。纳斯达克指数,代表美股。它们之间很明显的没有互相包含关系,所以即使不用算也知道它们的平均相关性很低。但是现在正值股灾,我想在此基础上再多定投几只指数(基金),以此来调整个人的资产配置比例。适逢我人生哲学的其中一个信条就是:

“重复的事情交给机器去做,解放出劳动力来进行更有意义的创造。”

因而我编写了一个程序,专门用来计算多项数据之间的平均相关性,并从中挑选出相关性最低的组合。是的,它不仅仅可以用来计算指数或股票走势的相关性。任何多项数据都可以,只要每项数据的数据元素个数一致即可。因为本来计算相关性的公式就要求如此。

步骤

第一步,准备好数据源。

我从沪深(中证)、上证、深证、港股、美股中挑选出了21个指数作为备选项。挑选的条件是能够从蚂蚁财富中买到相应的跟踪基金,并且可选的基金个数不少于两个。主题指数不参与计算,有兴趣的读者可以自行实现。然后从行情软件中下载好2017年10月16日到2018年10月16日的每日收盘价。

 

第二步,进行数据清洗。

按日期(按列)码齐每个指数是最基本的。然后就会发现A股(大陆)指数、港股指数、美股指数都会出现不连续的空隙。这是由于各个证交所放假导致交易日不同而导致的。对于这些数据,我采用了手工进行线性插值的方法处理。这部分数据对整体的影响不大。为了避免后面涉及到对XLS文件的繁琐编程,把该sheet数据单独保存为一个CSV文件。这样即可在编程的时候使用普通的文件流读取方法把数据载入内存中。

第三步,建立验证数据。

由于是第一次进行该类编程,还需要有一个参考标准来对比验证程序的计算结果。因此我又在EXCEL中利用CORREL公式计算了出所有21个指数两两之间的相关度。

第四步,编程。

4.1读取CSV文件,并格式化地保存进原始集合。由于文件是存储在硬盘上的,而硬盘的读写速度远比内存低。因此计算前先将数据读入到原始集合,然后按一定的格式(类或结构体)来存储每条数据是以后计算的基础。

4.2单独抽出指数据标志(指数名称)保存进名称集合。后面对指数进行组合时,依据的是指数的唯一标志,即指数的名称。而带着几百条收盘价一起进行组合是没有必要且浪费资源的。输出所有读取到的指数(名称)给用户进行检查。

4.3对数据标志进行两两组合。这里建立了一个递归函数,实现对输入N个元素中抽K个进行组合保存进配对集合。每种组合只出现一次,组合中无重复的元素,组合内元素无顺序区别。这样才能符合实际需求。该函数主要使用了高级的Linq查询技巧,但可能并不是时间和空间上的最优解,是一个可接受的解。

4.4求配对集合中每个组合的相关度。建立一个与CORREL函数计算方法一样的代数计算函数。遍历组合,用每个组合中元素1和2的名称在原始集合中找到对应的收盘价数组,并一同放入计算函数中计算。这里就得到了与第三步中两两指数相关度一样的结果,并可以对结果进行检验,以验证组合函数和计算函数的正确性。

4.5对数据标志进行K个元素的组合。按照用户的需求,从N个元素中每次抽取进行K个元素的组合,并存放进一个新的结果集合中。

4.6遍历结果集合,组合出在每个组合(A组合)中元素的两两组合,然后从配对集合中查询出一样的组合并取出相关度放入A组合中。当查询完成时,A组合中就同时包含了K个元素和K个元素两两之间的所有相关度。这里的逻辑嵌套层次较多,文字描述不清楚可直接看代码。

4.7对结果集合中的A组合进行排序,排序依据为每个A组合的平均相关度。按用户需求数量输出A组合的内容。

4.8对用户输入的数据标志(指数名称)字符串进行分割。如输入了M个指数名称则利用循环进行M次查询。每次查询寻找出所有包含当前数据标志的A组合形成一个新的筛选集合,抛弃其他组合。然后下一个数据标志从刚刚得到的筛选集合中查询。经过M次比较后即可得出满足用户输入最终筛选集合。然后输出给用户检验。

用法

对我而然,首先将准备好的CSV文件和编译好的IndexCorrelation.exe放在同一目录下。然后打开EXE,输入CSV的文件名。

如我想在原来已有的5个指数基础上再增加1个指数,则按6个指数一组进行组合。

这时程序会给出一共可能产生的组合个数。之后我想阅览相关性最小的10个组合。

程序会按平均相关性从小到大罗列。最后,我根据我现有的投资条件进行查询。

根据我现在的投资条件,选择添加标普500进入我的指数定投序列会是平均相关性最低的选择,但不一定是最好的选择。这是显然的,因为标普500和A股指数、港股指数都是负相关的,只和纳斯达克是正相关的,会严重拉低整个平均相关性。

后语

老读者可能会发现,在准备原始集合的部分,完全可以参照《基于C#的股票数据获取并进行基础分析的程序》一文中的程序,从网上直接下载指数数据。是的,如果只是用于分析证券市场数据的相关性的确可以这样做。我目前使用文件导入的做法是为了方便后面可以对其他类型的数据进行分析。有心的读者可以自行拼接这两个程序。

鉴于本文中的程序大量地使用了集合、泛型、Linq查询等技术。即使不用于进行相关性分析的读者也可以参考其中的编程技巧。再次声明,程序中的所有解法不一定是时间度量和空间度量上的最优解,只是我可以接受的解。

集合(数组)在使用过程中,一定要注意越界访问的问题。我作为编程人员和使用者,会自然而然地只输入合法的条件进入程序。感兴趣的读者可以自行检查所有可能会发生越界访问的地方。

IndexCorrelation

分类
MachineLearning|机器学习

使用机器学习预测世界杯赛果

目标

本文的目标是在学习了TensorFlow的官方Graph Execution示例后,通过举一反三运用其他例子来进一步理解官方示例。因此,本文中的所有结果仅作学术研究用途。

前提

IDE准备

Visual Studio 2017

打开Visual Studio Installer修改当前使用的VS,确保工作负载Web和云分类下的Python开发被选中。然后切换到单个组件,确保开发活动中的Python语言支持编译器、生成工具和运行时中的Python 3 64-bit被勾选。

点击右下角的修改,等待组件被下载和安装。

项目准备

新建一个Python应用程序项目。在解决方案资源管理器中找到Python环境,并右键添加虚拟环境。虚拟环境的基础解析器为Python 3中的某个版本。

右键点击新建的虚拟环境,选择安装Python包

分别安装pip、pandas和tensorflow。这些包在安装的同时,会把所有需要的包进行链式安装。以防万一,以下列出需要用到的所有包及版本。

absl-py==0.2.2
astor==0.6.2
bleach==1.5.0
certifi==2018.4.16
chardet==3.0.4
cycler==0.10.0
gast==0.2.0
get==1.0.3
grpcio==1.13.0
html5lib==0.9999999
idna==2.7
kiwisolver==1.0.1
Markdown==2.6.11
matplotlib==2.2.2
numpy==1.14.5
pandas==0.23.1
pip==10.0.1
post==1.0.2
protobuf==3.6.0
public==1.0.3
pyparsing==2.2.0
python-dateutil==2.7.3
pytz==2018.5
query-string==1.0.2
request==1.0.2
requests==2.19.1
setuptools==39.2.0
six==1.11.0
tensorboard==1.8.0
tensorflow==1.8.0
termcolor==1.1.0
urllib3==1.23
Werkzeug==0.14.1
wheel==0.31.1

数据准备

既然要进行机器学习,就需要准备好训练数据集测试数据集。训练数据集用于构建出一个能根据特征值推断出标签值模型,而测试数据集用于评估这个模型的准确度,它们都来自一个数据超集。为了有效地对模型进行评判,测试数据集与训练数据集不能有交集。

我构建的数据超集每一行代表一场比赛,因为我希望以一场比赛为单位预测它的赛果,并有如下的列,并描述为什么我要纳入这些列:

  • 序号:描述这是第几场比赛。随着赛程时间的进行,球员逐渐适应气候环境的情况以及产生的疲倦都可能会影响赛果。
  • 主队:以数字的方式表示这场比赛的主队。我按出场顺序为每支球队定义了一个编号,实际上编号的大小无所谓,只要在整个数据集中用同一个编号表示同一支球队即可。编号对应的球队在后面会给出。
  • 客队:以数字的方式表示这场比赛的客队。
  • 主队胜的平均初始赔率:赔率代表着某家公司对该场比赛该队表现的看法,它本身即是各种可能影响比赛因素的一个综合提炼。而取多家公司的平均赔率更具客观性。
  • 平局的平均初始赔率
  • 主队负的平局初始赔率
  • 主队胜的平局即时赔率:通过与同一行中的初始赔率作比较,获得赔率变化。这个变化可能是因为某些临赛前影响比赛的因素变化所导致的,也可能是公司一开始在掩盖自己的真实意图。即时赔率在数据超集中为截止投注前的最后一次公布的赔率,而在预测条件中为当前得知的最后一次公布的赔率。
  • 平局的平均即时赔率
  • 主队负的平均即时赔率
  • 主队近10场比赛的胜场数:近期比赛会在一定程度上反映出该队伍的状态。
  • 主队近10场比赛的平场数
  • 主队近10场比赛的负场数
  • 客队近10场比赛的胜场数
  • 客队近10场比赛的平场数
  • 客队近10场比赛的负场数
  • 主队积分:这场比赛开始前,按小组赛规则的积分。因为数据集中也纳入了淘汰赛的数据,所以也按胜方积3分,平局各积1分,负方不积分的方式统计。积分在一定程度上反映了之前的比赛结果对该队伍造成的影响。
  • 客队积分
  • 赛果:以数字方式表述这场比赛的结果。主胜为2,平局为1,主负为0.

前17列为特征列,每列中的每个值即为特征值。最后一列为标签列,每列中的每个值即为标签值。机器学习的目的就是找出已知的特征值与特征值组合和已知标签值之间的关联,然后根据已知的特征值去预测未知的标签。

这些数据的来源通常可以在投注网站找得到。

这时前提准备已经完成,可以往项目中添加.py文件及代码。

代码展示

先上代码,然后对代码进行讲解。

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import argparse
import tensorflow as tf

import game_data


parser = argparse.ArgumentParser()
parser.add_argument('--batch_size', default=4, type=int, help='捆绑大小')
parser.add_argument('--train_steps', default=2000, type=int,
                    help='训练步数')

def main(argv):
    args = parser.parse_args(argv[1:])

    # 拉取数据
    (train_x, train_y), (test_x, test_y) = game_data.load_data()

    # 描述输入特征列
    my_feature_columns = []
    for key in train_x.keys():
        my_feature_columns.append(tf.feature_column.numeric_column(key=key))

    # 构建DNN分类器
    classifier = tf.estimator.DNNClassifier(
        feature_columns=my_feature_columns,
        # 3个隐藏层
        hidden_units=[20, 15, 5],
        # 设置3个类别
        n_classes=3)

    # 训练模型
    classifier.train(
        input_fn=lambda:game_data.train_input_fn(train_x, train_y,
                                                 args.batch_size),
        steps=args.train_steps)

    # 评估模型
    eval_result = classifier.evaluate(
        input_fn=lambda:game_data.eval_input_fn(test_x, test_y,
                                                args.batch_size))

    print('\n测试集准确度: {accuracy:0.3f}\n'.format(**eval_result))

    # 建立需要使用模型做出预测的数据
    predict_x = {
        'Game': [57],
        'Home': [4],
        'Away': [9],
        'WinInitialOdds': [4.3],
        'DrawInitialOdds': [3.17],
        'LossInitialOdds': [2.00],
        'WinCurrentOdds': [4.44],
        'DrawCurrentOdds': [3.11],
        'LossCurrentOdds': [2.03],
        'HomeWinCount': [8],
        'HomeDrawCount': [1],
        'HomeLossCount': [1],
        'AwayWinCount': [6],
        'AwayDrawCount': [3],
        'AwayLossCount': [1],
        'HomePoint': [12],
        'AwayPoint': [10]
    }

    predictions = classifier.predict(
        input_fn=lambda:game_data.eval_input_fn(predict_x,
                                                labels=None,
                                                batch_size=args.batch_size))

    template = ('\预测结果是"{}" ({:.1f}%)"')

    for pred_dict in predictions:
        class_id = pred_dict['class_ids'][0]
        probability = pred_dict['probabilities'][class_id]

        print(template.format(game_data.RESULT[class_id],
                              100 * probability))


if __name__ == '__main__':
    tf.logging.set_verbosity(tf.logging.INFO)
    tf.app.run(main)
import pandas as pd
import tensorflow as tf

TRAIN_URL = "http://www.halve.top/wp-content/uploads/2018/07/game_training.csv"
TEST_URL = "http://www.halve.top/wp-content/uploads/2018/07/game_test.csv"

CSV_COLUMN_NAMES = ['Game','Home','Away',
                    'WinInitialOdds','DrawInitialOdds','LossInitialOdds',
                    'WinCurrentOdds','DrawCurrentOdds','LossCurrentOdds',
                    'HomeWinCount','HomeDrawCount','HomeLossCount',
                    'AwayWinCount','AwayDrawCount','AwayLossCount',
                    'HomePoint','AwayPoint','Result']

RESULT = ['Loss','Draw','Win']

def maybe_download():
    train_path = tf.keras.utils.get_file(TRAIN_URL.split('/')[-1], TRAIN_URL)
    test_path = tf.keras.utils.get_file(TEST_URL.split('/')[-1], TEST_URL)

    return train_path, test_path

def load_data(y_name='Result'):
    """以(train_x, train_y), (test_x, test_y)的形式返回比赛数据集"""
    train_path, test_path = maybe_download()

    train = pd.read_csv(train_path, names=CSV_COLUMN_NAMES, header=0)
    train_x, train_y = train, train.pop(y_name)

    test = pd.read_csv(test_path, names=CSV_COLUMN_NAMES, header=0)
    test_x, test_y = test, test.pop(y_name)

    return (train_x, train_y), (test_x, test_y)

def train_input_fn(features, labels, batch_size):
    """训练输入函数"""
    # 将输入转换为数据集
    dataset = tf.data.Dataset.from_tensor_slices((dict(features), labels))

    # 打乱、重复以及捆绑样本
    dataset = dataset.shuffle(1000).repeat().batch(batch_size)

    # 返回数据集
    return dataset

def eval_input_fn(features, labels, batch_size):
    """验证和预测输入函数"""
    features=dict(features)
    if labels is None:
        # 没有标签,只有特征
        inputs = features
    else:
        inputs = (features, labels)

    # 将输入转换为数据集
    dataset = tf.data.Dataset.from_tensor_slices(inputs)

    # 捆绑样本
    assert batch_size is not None, "batch_size must not be None"
    dataset = dataset.batch(batch_size)

    # 返回数据集
    return dataset

CSV_TYPES = [[0.0], [0.0], [0.0], 
             [0.0], [0.0], [0,0],
             [0.0], [0.0], [0.0],
             [0.0], [0.0], [0.0],
             [0.0], [0.0], [0.0],
             [0.0], [0.0], [0]]

def _parse_line(line):
    # 解码行
    fields = tf.decode_csv(line, record_defaults=CSV_TYPES)

    # 将结果打包为字典
    features = dict(zip(CSV_COLUMN_NAMES, fields))

    # 将标签从特征中分离
    label = features.pop('Species')

    return features, label


def csv_input_fn(csv_path, batch_size):
    # 创建一个包含多行文本的数据集
    dataset = tf.data.TextLineDataset(csv_path).skip(1)

    # 转换每一行
    dataset = dataset.map(_parse_line)

    # 打乱、重复和捆绑样本
    dataset = dataset.shuffle(1000).repeat().batch(batch_size)

    # 返回数据集
    return dataset

首先说WorldCupPredict.py的代码。引用了一些固定的模块,然后确定了步数(step)和捆绑(batch)两个参数。步长并不是越大越好,模型的准确度会从第一步开始逐渐波动上升,到达某个最大值值后可能会下降一点然后再上升一点。当训练数据多的时候,捆绑值可以设大一点。具体意义参考TensorFlow官网的术语表。

然后会调用game_data模块获得训练数据和测试数据。再对特征值进行标记。之后构建了一个分类器,因为实质上这是个按特征把比赛归类为主胜、平局和主负三种不同赛果的问题。

分类器中设置了三个隐藏层,每一个特征值都会流进第一层的每个神经元里,第一层神经元的计算结果又会流进第二层,如此类推。直至输出到三个不同的赛果类别中。最佳的隐藏层数的设置和每层神经元数的设置需要通过重复实验和一定的经验决定。往往多的层和多的神经元需要更多的训练数据去达到有效的训练。

训练完成后会使用测试数据对模型进行评估,并给出准确度。之后可以手动构造一个或多个比赛数据给模型预测。这里使用的是第57场乌拉圭对战法国的数据,这里并没有给出赛果作为标签值。

模型会根据特征值预测出归属于不同标签的可能性,这里文本输出了最大可能性的标签及其可能性。

接下来说一下game_data.py的代码。首先定义了两个数据源,采用的是本网站整理好的训练集和测试集。数据源的形式是csv文件,是一种以换行表示数据行,逗号表示一行中的数据列的表格型数据。

首先需要从网络把数据源下载回到本地,然后将csv数据源整理成可以被计算的张量(tensor)。解析csv文件需要用到一个数据模板向程序说明每列是一个什么数据类型的数据。这里把前面17列的特征值作为浮点数,最后一列作为整数。因为张量转换过程中所有数据的类型要相同。这个文件的代码主要是做了数据整理的工作。

运行结果

过程中会给出在训练模型时的信息,包括了步数、进行到当前步数的损失和每秒进行了多少步。

模型训练完成后,会放入测试集,用于评估模型的准确度。最后会给出一个无标签的特征值的标签预测结果。如上图所示,即评估准确度达到50%,而有39.5%的概率本场(乌拉圭VS法国)的比赛中,主队(乌拉圭)会输。

读者可以尝试进行以下几种工作:

  1. 调整步长和捆绑对训练和评估的影响。
  2. 调整隐藏层数和神经元个数对训练和评估的影响。
  3. 加入新的特征列和删除已有特征列对训练和评估的影响。
  4. 把该项目作为模板处理其他分类问题。
  5. 壕砸2块钱法国赢。

数据集文件

点此下载

分类
CSharp|C#

基于C#的股票数据获取并进行基础分析的程序

本程序基于C#控制台应用开发,二进制文件(文末附下载地址)需求支持库.NET Framework 4.6.1。主要实现了从HTTPS的JSON数据源获取结构化的股票数据,然后对数据进行分析处理,给出分析结论的功能。

先放上代码,后面再对代码进行解释。

using System;
using System.Collections.Generic;

namespace FundsInvestment
{
    class Program
    {

        static void Main(string[] args)
        {
            Console.WriteLine("!!!免责声明!!!");
            Console.WriteLine("本程序仅用于演示如何从JSON数据源获取数据,并进行基本的分析");
            Console.WriteLine("演示数据源:雪球 www.xueqiu.com");
            Console.WriteLine("演示分析算法:蚂蚁财富 慧定投");
            Console.WriteLine("自动放弃本程序版权,不收取费用或打赏,也不会对你的投资盈亏负责。");
            Console.WriteLine("PROGRAMMING BY:暂置元帅");
            Console.WriteLine("--------------------");
            List<Index> list = new List<Index>(5)
            {
                new Index("SH000016"),
                new Index("SH000905"),
                new Index("SZ399006"),
                new Index("NASDAQ"),
                new Index("HKHSCEI")
            };
            foreach (var item in list)
            {
                if (item.FetchData() == true)
                {
                    Console.WriteLine(item.Symbol+"拉取数据成功");
                    Console.WriteLine("最近一日收盘价" + item.Close);
                    Console.WriteLine(item.GetAntFortuneAlgrithmAdvice());
                }
                else
                {
                    Console.WriteLine("拉取数据失败");
                }
                Console.WriteLine("--------------------");
            }
            while (true)
            {
                try
                {
                    Console.Write("输入股票或指数代码(SH/SZ+六位数字):");
                    string symbol = Console.ReadLine();
                    Console.Write("输入最后一个查询日(年/月/日):");
                    DateTime endTime = DateTime.Parse(Console.ReadLine());
                    Console.Write("输入往前查询天数(整数):");
                    int dayCount = Int32.Parse(Console.ReadLine());
                    Console.Write("基础投资额:");
                    double baseLine = double.Parse(Console.ReadLine());
                    Console.WriteLine("--------数据拉取中--------");

                    Index myIndex = new Index(symbol, endTime, dayCount);
                    myIndex.BaseLine = baseLine;
                    if (myIndex.FetchData() == true)
                    {
                        Console.WriteLine(myIndex.Symbol + "拉取数据成功");
                        Console.WriteLine("最近一日收盘价" + myIndex.Close);
                        Console.WriteLine(myIndex.GetAntFortuneAlgrithmAdvice());
                    }
                    else
                    {
                        Console.WriteLine("拉取数据失败");
                    }
                    Console.WriteLine("--------------------");
                }
                catch (Exception)
                {
                    Console.WriteLine("输入格式有误");
                }

            }
        }
    }
}
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;

namespace FundsInvestment
{
    /// <summary>
    /// 指数类
    /// </summary>
    class Index
    {
        /// <summary>
        /// 指数符号
        /// </summary>
        public string Symbol { get; set; }

        /// <summary>
        /// 数据储存对象
        /// </summary>
        private Rootobject DataArray { get; set; }

        /// <summary>
        /// 数据结束日期时间戳
        /// </summary>
        private string EndTimeStamp { get; set; }

        /// <summary>
        /// 数据起始日期时间戳
        /// </summary>
        private string BeginTimeStamp { get; set; }

        /// <summary>
        /// 交易日数量
        /// </summary>
        private int ExchangeDayCount { get; set; }

        /// <summary>
        /// 均值
        /// </summary>
        public double MA { get { return GetMA(); } }

        /// <summary>
        /// 投资基线(默认值20元)
        /// </summary>
        private double baseLine = 20;

        /// <summary>
        /// 投资基线(默认值20元)
        /// </summary>
        public double BaseLine { get => baseLine; set => baseLine = value; }

        /// <summary>
        /// 最近一日收盘价
        /// </summary>
        public double Close
        {
            get
            {
                if(DataArray != null && DataArray.chartlist.Length != 0)
                {
                    return DataArray.chartlist.Last().close;
                }
                return 0;
            }
        }

        /// <summary>
        /// 构造指数查询地址(默认从180个交易日前到今天)
        /// </summary>
        /// <param name="Symbol">指数符号</param>
        public Index(string Symbol)
        {
            this.Symbol = Symbol;
            DateTime endDay = DateTime.Today;
            //超量请求数据,填充非交易日
            DateTime beginDay = new DateTime(endDay.Ticks - new TimeSpan(180 * 2, 0, 0, 0).Ticks);
            BeginTimeStamp = GetTimeStamp(beginDay);
            EndTimeStamp = GetTimeStamp(endDay);
            ExchangeDayCount = 180;
        }

        /// <summary>
        /// 构造指数查询地址
        /// </summary>
        /// <param name="Symbol">指数符号</param>
        /// <param name="EndDay">结束日期</param>
        /// <param name="DayCount">交易日个数</param>
        public Index(string Symbol,DateTime EndDay,int DayCount)
        {
            this.Symbol = Symbol;
            DateTime endDay = EndDay;
            //超量请求数据,填充非交易日
            DateTime beginDay = new DateTime(endDay.Ticks - new TimeSpan(DayCount * 2, 0, 0, 0).Ticks);
            BeginTimeStamp = GetTimeStamp(beginDay);
            EndTimeStamp = GetTimeStamp(endDay);
            ExchangeDayCount = DayCount;
        }

        /// <summary>
        /// 拉取数据
        /// </summary>
        /// <returns>是否拉取成功</returns>
        public bool FetchData()
        {
            string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36";
            CookieContainer container = new CookieContainer();
            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://www.xueqiu.com");
                request.UserAgent = UserAgent;
                request.Method = "GET";
                request.CookieContainer = container;
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();

                var jsonRequest = WebRequest.CreateHttp(BuildUri());
                jsonRequest.UserAgent = UserAgent;
                jsonRequest.CookieContainer = container;
                jsonRequest.Method = "GET";
                HttpWebResponse jsonResponse = (HttpWebResponse)jsonRequest.GetResponse();
                var stream = jsonResponse.GetResponseStream();
                var streamReader = new StreamReader(stream);
                var content = streamReader.ReadToEnd();

                DataArray = JsonConvert.DeserializeObject<Rootobject>(content);

                if(DataArray.success == "true")
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception e)
            {
                return false;
            }
        }

        /// <summary>
        /// 获得蚂蚁财富慧定投算法推荐定投额
        /// </summary>
        public string GetAntFortuneAlgrithmAdvice()
        {
            int length = DataArray.chartlist.Length;
            if (length == 0)
            {
                return "无可供计算的数据";
            }
            string advice = string.Empty;
            double ma = GetMA();
            double indexValue = DataArray.chartlist.Last().close;
            if(indexValue > ma)
            {
                double rate = (indexValue - ma) / ma;
                advice += "当前指数值高于均线" + Math.Round((rate * 100),2) + "%,建议本轮投资";
                if (rate < 0.15)
                {
                    advice += (BaseLine * 0.9) + "元"; 
                }
                else if(rate >= 0.15 && rate < 0.5)
                {
                    advice += (BaseLine * 0.8) + "元";
                }
                else if (rate >= 0.5 && rate < 1)
                {
                    advice += (BaseLine * 0.7) + "元";
                }
                else if (rate >= 1)
                {
                    advice += (BaseLine * 0.6) + "元";
                }
            }
            else if(indexValue == ma)
            {
                advice += ("当前指数值等于均线,建议本轮投资" + BaseLine + "元");
            }
            else if(indexValue < ma)
            {
                double rate = (ma - indexValue) / ma;
                advice += "当前指数值低于均线" + Math.Round((rate * 100),2) + "%,";
                List<double> tenDay = new List<double>(10);
                int start = 0;
                //防止数组总长小于10
                if(length >= 10)
                {
                    start = length - 10;
                }
                else
                {
                    start = 0;
                }
                for (; start < length; start++)
                {
                    tenDay.Add(DataArray.chartlist[start].close);
                }
                if ((tenDay.Max() / tenDay.Min()) - 1 > 0.05)
                {
                    advice += "过去10日振幅大于5%,建议本轮投资";
                    if(rate < 0.05)
                    {
                        advice += (BaseLine * 0.6) + "元";
                    }
                    else if(rate >= 0.05 && rate < 0.1)
                    {
                        advice += (BaseLine * 0.7) + "元";
                    }
                    else if (rate >= 0.1 && rate < 0.2)
                    {
                        advice += (BaseLine * 0.8) + "元";
                    }
                    else if (rate >= 0.2 && rate < 0.3)
                    {
                        advice += (BaseLine * 0.9) + "元";
                    }
                    else if (rate >= 0.3 && rate < 0.4)
                    {
                        advice += BaseLine + "元";
                    }
                    else if (rate >= 0.4)
                    {
                        advice += (BaseLine * 1.1) + "元";
                    }
                }
                else if ((tenDay.Max() / tenDay.Min()) - 1 <= 0.05)
                {
                    advice += "过去10日振幅小于或等于5%,建议本轮投资";
                    if (rate < 0.05)
                    {
                        advice += (BaseLine * 1.6) + "元";
                    }
                    else if (rate >= 0.05 && rate < 0.1)
                    {
                        advice += (BaseLine * 1.7) + "元";
                    }
                    else if (rate >= 0.1 && rate < 0.2)
                    {
                        advice += (BaseLine * 1.8) + "元";
                    }
                    else if (rate >= 0.2 && rate < 0.3)
                    {
                        advice += (BaseLine * 1.9) + "元";
                    }
                    else if (rate >= 0.3 && rate < 0.4)
                    {
                        advice += (BaseLine * 2) + "元";
                    }
                    else if (rate >= 0.4)
                    {
                        advice += (BaseLine * 2.1) + "元";
                    }
                }
            }
            return advice;
        }

        /// <summary>
        /// 获取均值
        /// </summary>
        /// <returns>均值</returns>
        private double GetMA()
        {
            double summary = 0;
            int length = DataArray.chartlist.Length;
            for (int i = length - ExchangeDayCount; i < length; i++)
            {
                summary += DataArray.chartlist[i].close;
            }
            return summary / ExchangeDayCount;
        }

        /// <summary>
        /// 构造指数JSON数据源请求地址
        /// </summary>
        /// <returns>请求地址</returns>
        private string BuildUri()
        {
            string requestUri = string.Empty;
            string headStr = "https://xueqiu.com/stock/forchartk/stocklist.json?";
            requestUri += headStr;
            string symbolStr = "symbol=" + Symbol;
            requestUri += symbolStr;
            requestUri += "&";
            string periodStr = "period=1day";
            requestUri += periodStr;
            requestUri += "&";
            string typeStr = "type=before";
            requestUri += typeStr;
            requestUri += "&";
            string beginStr = "begin=" + BeginTimeStamp;
            requestUri += beginStr;
            requestUri += "&";
            string endStr = "end=" + EndTimeStamp;
            requestUri += endStr;
            return requestUri;
        }

        /// <summary>
        /// 获得时间戳
        /// </summary>
        /// <param name="time">时间</param>
        /// <returns>时间戳</returns>
        private string GetTimeStamp(DateTime time)
        {
            TimeSpan timeSpan = time - new DateTime(1970, 1, 1, 0, 0, 0);
            return Convert.ToInt64(timeSpan.Ticks / 10000).ToString();
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FundsInvestment
{
    public class Rootobject
    {
        /// <summary>
        /// 股票对象
        /// </summary>
        public Stock stock { get; set; }
        /// <summary>
        /// 查询是否成功
        /// </summary>
        public string success { get; set; }
        public Chartlist[] chartlist { get; set; }
        public class Stock
        {
            /// <summary>
            /// 股票符号
            /// </summary>
            public string symbol { get; set; }
        }
        public class Chartlist
        {
            public long volume { get; set; }
            /// <summary>
            /// 开盘价
            /// </summary>
            public double open { get; set; }
            /// <summary>
            /// 最高价
            /// </summary>
            public double high { get; set; }
            /// <summary>
            /// 收盘价
            /// </summary>
            public double close { get; set; }
            /// <summary>
            /// 最低价
            /// </summary>
            public double low { get; set; }
            /// <summary>
            /// 涨跌额
            /// </summary>
            public double chg { get; set; }
            /// <summary>
            /// 涨跌幅
            /// </summary>
            public double percent { get; set; }
            /// <summary>
            /// 换手率
            /// </summary>
            public double turnrate { get; set; }
            /// <summary>
            /// 5日均值
            /// </summary>
            public double ma5 { get; set; }
            /// <summary>
            /// 10日均值
            /// </summary>
            public double ma10 { get; set; }
            /// <summary>
            /// 20日均值
            /// </summary>
            public double ma20 { get; set; }
            /// <summary>
            /// 30日均值
            /// </summary>
            public double ma30 { get; set; }
            /// <summary>
            /// DIFF值
            /// </summary>
            public double dif { get; set; }
            /// <summary>
            /// DEA值
            /// </summary>
            public double dea { get; set; }
            /// <summary>
            /// MACD值
            /// </summary>
            public double macd { get; set; }
            public long lot_volume { get; set; }
            public long timestamp { get; set; }
            public string time { get; set; }
        }
    }
}

Program.cs类是项目自动生成的类。作为程序的入口,并对程序的执行流程进行控制。流程控制主要分为几个部分:1.声明免责信息。2.构造Index类的对象集合,处理例子数据。3.通过无限循环读取用户输入的值构造Index对象,处理特定数据。

其中使用了Index类的两种构造方法。第一种只需输入股票符号,默认按从180个交易日前到今天进行数据获取。第二种需要输入股票符号、结束日期和交易日个数。

Rootobject.cs类由JSON字符串通过Visual Studio 2017的“将JSON粘贴为类”功能自动生成,作为反序列化后的模板和DataModel使用。需要注意的是,某些字段的基本变量类型要定义为长整形才由足够的空间存放数据。

Index.cs类为本程序的业务逻辑类。由构造函数负责获取拼接出股票数据源地址所需的所有参数。

FetchData方法中使用了HttpWebRequest去获取雪球数据源地址的JSON数据,并把JSON字符串反序列化为Rootobject对象,最后判断是否正确获得数据。这里两次进行GET Request是因为数据源需要先访问主页获取到Cookies权限才能正常访问到数据。

GetAntFortuneAlgrithmAdvice方法对指定交易日内的数据进行了遍历统计和分析。分析方法参照了蚂蚁财富的慧定投算法。

编写这个程序的目的是为了按照慧定投的模式对我购买5只互相关度最低指数型基金进行投资,关于股市指数的相关度自动计算程序可能以后会开发。

慧定投官方给出的跟踪标的并没有我需要的指数,而每个投资周期都去手动计算投资额又过于繁琐。所以本着“重复性的劳动交给程序处理,……”这一人生哲学信条,开发了本程序以解放劳动,同时得以回馈社区。

最后再次声明:本程序不代表任何投资建议,入市自负盈亏。

二进制文件

分类
CSharp|C#

dot Net体系命名指引——第二节:选词规则

.net 体系命名指引

第二节 选词规则

原则一:选择易读性强的命名方式。

解释:当名称由多个单词组成时,以可读性最强的方式排列它们。

例子:当需要命名一个表格形式的视图容器成员变量时,使用GridView可以比ViewGrid更清晰的定义这个变量。因为“View”既可以作为名词,也可以作为动词。当作为后者时,容易与函数命名的“动词+名词”模式搞混。

原则二:易读性优于缩略性。

解释:诚然使用较短的名称能够缩短每行代码的长度,但它不应该以牺牲易读性为前提。

例子:理科公式中很多量使用了单个西文字母表示,当需要将这些公式编程为函数时,这些量应该用其本来代表的实际意义来表示,至少也要用其读音来表示希腊字母。例如“ω”在某些公式中代表角度,这个量应该命名为Angle,至少也要命名为Omiga,而不是一个“w”。

原则三:尽量只用“英文字母和数字”的字符。

解释:即使某些语言允许使用除了英文字母和数字之外的字符来命名,也要少用。一定要用也要有统一的规则。

例子:为界面上的控件——“确定”按钮命名时,使用BtnConfirm已经能够清晰地分割“按钮”和“确定”两个语义。但是,有时Btn_Confirm这样的命名方式更快地传达出这是一个UI控件的成员,通常由模板生成的,不能自由删改。

原则四:彻底抛弃匈牙利命名法。

解释:使用帕斯卡命名法和驼峰命名法已经足够对付dot Net体系的程序元素。首先目前宇宙最强大的IDE——Visual Studio 2017已经能够非常娴熟地对付类型不匹配等早期错误。然后匈牙利命名法和帕斯卡命名法是完全冲突的。

例子:虽然不应该严格遵守匈牙利命名法,但是其中某些要素是可以吸收改造的。可以使用Is、Can等含有是否、能否语义的单词来代替b组合一个布尔型变量,如使用IsOpen而不是bOpen

原则五:不使用被各种编程语言广泛作为关键字的单词。

解释:很明显这会和编程语言本身产生语义级别的冲突。需要注意的是,C#的关键字就有一百多个。

例子:readonly是关键字之一,所以表示一个只读的变量时,使用IsReadonly

关于缩略语

不要自定缩略语。尤其是自定的缩略语会和已有的单词产生歧义。

尽量少使用缩略语,即使要使用缩略语,也要用被广泛接受的缩略语。

关于语言特有的命名

关注语义去命名而不是语言特性关键字。

例如GetLengthGetSizeGetCount是不同类里面的方法,返回的是一个整型的变量。这时使用前三者而不是GetInt更好。

使用一些常见的名字,如valueitem,而不是将成员和类型的名字起得一样。除非这个成员没有明显的语义,或者作为函数参数来说它的类型不重要。

分类
CSharp|C#

dot Net体系命名指引——第一节:命名法与分词

.net 体系命名指引

第一节 命名法与分词

帕斯卡命名法

所有单词的首字母大写

PascalCasing

只由一个单词构成

Seed

单词只由一个字母构成

CSharp

由两个单词的首字母缩写的词,都作大写

IPAddress

由大于两个单词的首字母缩写的词,只大写第一个字母

NbaStars

不是由首字母缩写的词,也只大写第一个字母

IdCard

驼峰命名法

除第一个单词首字母小写外,其余单词首字母大写

camelCasing

只由一个单词构成

value

单词只由一个字母构成

ePay

由两个单词的首字母缩写的词,都作小写

uiFramework

由大于两个单词的首字母缩写的词,都作小写

cssBuilder

不是由首字母缩写的词,都作小写

lvHolding

合成词的处理

计算机科学专业英语中,有很多常见的合成词。即能从一个单词中明显拆分出两个或以上的独立单词。

该合成词已被权威英语词典收录时,按一个单词处理

Password/password

否则按多个单词处理

UserName/userName

至于按哪本权威词典算,由组织内部决定,牛津、柯林斯等都可以。

美式英语偏好

由于编程语言由美国公司或组织发明、维护、贡献较多,且美式英语单词往往比英式英语更短,所以偏好使用美式英语词汇。

color colour

大小写敏感性

不同的语言对大小写敏感的支持不同。因此,不能以大小写的不同来区分不同的内容。

分类
Technology|技术

全栈工程师硬度指数

1 Extremely Soft EXS 极软级
二三线工作,管理,采购,财务,扫地,打水等

2 Ultra Soft US超软级
与编码无关的工作,算法设计,美工等

3 Very Soft VS 甚软级
网页前端设计等

4 Soft S 软级
桌面程序设计,移动设备应用设计,数据库设计,通讯协议设计,信息管理与处理等

5 Server Network SN 服务器与网络级
与后台服务器及网络有关的工作,服务器软硬件环境架设,组网等

6 Embedded E 嵌入级
与硬件编码有关的工作,嵌入式编程,单片机编程,PLC,FPGA等

7 Hard H 硬级
与实施设备安装与维护有关的工作,仪器的分拆,组装,维修等

8 Very Hard VH 甚硬级
与实施设备电路有关的工作,装置电路板设计等

9 Ultra Hard UH 超硬级
与实施设备机械有关的工作,装置结构设计与制造等

10 Extremely Hard EXH 极硬级
与实施环境有关的工作,土木,水路,强电布置与设计等

分类
CSharp|C# Web|网页

使用CEF作为桌面应用UI开发框架的分析

在本人编写的几个上位机程序中,程序界面部分多数使用WinForm作为开发框架。WinForm框架的基本容器单位为窗体,即“Form”。每个窗体中包含了若干个不同种类的控件,即“Control”。控件分为数据类、容器类等。窗体和控件执行了不同的事件,如“单击”、“右击”等,会触发相应的回调函数,以此实现交互功能。创建每个窗体后,按照框架预定义的规则会生成Form.designer.cs及Form.cs两个不同的文件。这两个文件实质上属于同一个类(Class),由partial关键词划分。前者主要包含了该窗体及其中的控件的声明和定义代码,由框架自动生产。后者为使用者实际编码处理具体逻辑区域。

使用WinForm的好处是在界面外观要求不高的情况下,可以十分快速地在所见即所得式的界面编辑器通过拖拽控件的方式实现界面布局。然后在控件属性页中静态地定义每个控件的各项属性,在控件事件页中指定控件能够产生的每个动作对应的回调函数。以上方便之处,得益于WinForm框架预先已对这些控件作出了标准化处理。

当需要实现较为高级的界面功能时,WinForm的弱点就会凸显出来。如当程序运行于不同分辨率屏幕的时候,界面往往不能与在生产环境中保持一致。当某个控件中的文本内容过长时,会超出容器边界或被右方控件遮盖。正确的处理方法应该是将超出部分文本换行,并下移下方控件位置。

还有许多如界面主题、动画效果等高级界面功能在WinForm中较难实现。虽然从编程角度上看,这些功能的确可以通过对WinForm进行底层绘图级别的扩展和补充一一实现,但是解决这些属于框架先天设计的问题往往需要耗费大量的时间和精力。

引入CEF(Chromium Embedded Framework)作为应用程序UI框架。CEF实质上是一个谷歌浏览器的内核。与WinForm框架中自带的WebView控件不同的是,WebView的内核是IE,对HTML特性的支持十分糟糕。而CEF的WebKit内核能够良好地支持HTML 5特性。

得益于互联网尤其是移动互联网的快速发展和社区的力量,以Bootstrap、Foundation和Semantic UI为代表的一系列开源前端框架已经在HTML+CSS+JS的体系中将UI功能开发得十分成熟。其常用控件种类上能够基本满足桌面程序的要求。从美观角度上看,这些标记式的界面框架完全优于一众传统的代码式的界面框架。从运行性能上看,以Web为主要应用场景的UI实现方式天生需要适应不同的软硬件环境,所以在性能优化上已经有了长足的积累,在处理复杂特效时有比同样是标记式界面框架的WPF有更多的优势。

CEF代替Form作为前端界面的容器,界面由WEB技术实现,而事务逻辑仍然由C#实现这种开发方式与移动端的Hybird APP有着很多相似之处,后者已经以WEB+JAVA或WEB+Objectiv-C的实现方式得到了业界的大量运用。