Jacky Liu's Blog

继续 FSE 插件

---- 这是到目前为止写得最好,也是最有用的一个插件,各种爱不释手,现在用 Vim 时已经默认开在左边了。曾经看见有些小盆友在网上说什么“Vim 是个挺好的编辑器但是没法代替现代的 IDE”,各种鄙夷。我要是个程序员,一定按自己的喜好把 Vim 打造成最顺手的 IDE,用厂家的 IDE 只不过是在圈好的地里吃草而已。

---- 几个小改进:

1. 文件改名功能,也可以用来移动节点,用的是 Python 的 os.rename() 函数。

2. 新增一个自定义按键,在指定节点下打开文件浏览器 Nautilus

3. 批量删除功能,在 v 模式或 v line 模式下选中多个同级节点以后可以批量删除。

4. 智能定位按键在 v 模式和 v line 模式下也能用了,这样可以方便快速选中比较多的同级节点然后批量删除。

5. 浏览界面新增一个外观,节点前面添加了树形的分支符号,如图。因为字体的关系,符号之间有空隙,所以效果没有想像的那么好。总体上我觉得 Ubuntu 的 Courier 10 字体看着还是很顺眼的,只是没有十全十美。原来用 <Tab> 加不同颜色作缩进,是因为这样比较简单。这种方式现在还能用,靠选项可以切换。
 



---- 以后可能继续添加的功能:

1. 节点名过滤功能,通过自定义命令输入 py pattern,对节点名进行过滤。比如:
    “只显示 .py 文件”
    “不显示名称含有 'tmp' 的目录”
等等。

2. 标记功能,可以对经常访问的文件做标记,对应的高亮显示,快速跳转按键操作,等等。

3. 文件查找功能。与上面类似,通过自定义命令输入 py pattern 查找指定目录下的节点,结果在浏览界面里标识出来。

4. 改颜色 -- 收藏夹里的颜色难看到死,这我知道。FSE 是分两部分写的,中间隔比较久,写收藏夹那部分时,还不知道有这么多好看的 预定义颜色 可以用。有空再改改吧。

5. 在节点排序模式里添加 "bytype" 模式,按照类型来排序文件。这个比较麻烦,因为只靠扩展名来判断类型是不够的,比如图片文件和视频文件都有许多种扩展名,这个有时间慢慢弄。

6. 继续添加对文件类型的支持,能够调用合适的程序打开更多的文件类型

7. 添加对 soft link 和 mount 类型节点的支持。


---- 另外在改写插件的时候被一个非著名问题难住了:


在 Vim 里怎样得到当前光标所在列的列号?


写下这个问题时我就穿越了,人生真的就是循环往复。。。

这里说的列号不是 col('.'),那个是光标前的内容在内存里占多少字节,而我只是要知道光标前面有多少个字符而已。因为换用树形的符号作缩进以后,在相同位置上用 col('.') 或者 getpos('.') 得到的值与以前用 <Tab> 作缩进时不一样了,而如果不能正确判断列号的话,智能定位功能会有问题。我就纳闷,用 Vim 都 N 久了,怎么现在才遇到这个问题,而且一时还想不出办法来。

最后终于想出一个:

    collen= len(vim.eval(' col(".")==1 ? "" : getline(".")[:col(".")-2] ')) + 1

上面这是 Python 代码,因为 FSE 插件是用 Python 接口写的。如果是纯 VimScript 的话,就得这样:

    let collen= strlen( substitute( (col('.')==1 ? "" : getline('.')[:col('.')-2]) , '.', 'x', 'g') ) + 1

介个。。。拜托!谁有好点的办法没有?
 

------------------------------------------------------------------------- <补记> ---------------------------------------------------------------------------

---- 用 strchars() 函数好一点:

    let collen= strchars(col('.')==1 ? "" : getline('.')[:col('.')-2]) + 1

另外,用 strwidth()strdisplaywidth() 可以得到一个 string 在 Vim 里的显示宽度。

---- 对这几个函数怎么一点印象都没有 。。。

 

------------------------------------------------------------------------- <又补记> ---------------------------------------------------------------------------

---- virtcol() 函数,得到当前光标的显示位置(在第几个显示单元上)。功能不全一样,但也可用来判断光标位置与缩进等级之间的关系,感谢 依云 的回复。

 

Vim 的补全模式加速器,轻松玩转全部 15 种自动补全模式

---- 这是两年前写的一个小工具,是为了学习掌握 Vim 的补全模式,也是为了用起来方便。在那之前,我对 Vim 的补全功能还只是稍微了解,从没用过,因为补全功能默认的按键都是 emacs 式的,太蛋疼了,觉得这样还不如直接手敲。但是后来开始越来越多地写代码(我不是程序员,之前没写过多少),就觉得必须要熟悉 Vim 的补全功能,于是写了这个东东。它主要做两件事:

1: 把 Vim 的 15 种补全模式分成 5 组,映射到 <M-a> 到 <M-g> 5 个按键上,方便使用。好处有两个,一是不用去记 15 个按键组合(我是记不住),二是可以摆脱默认的那种 emacs 式的按键。

2: 在状态栏里做了一个补全模式状态显示器(可选,如下图),可以实时地显示补全模式的状态。不想要的话可以关闭,它就不会显示在状态栏里。

所以,就像是在众多 RPG 游戏里的“腰带”或“快速物品栏”一样,平时把常用的物品放在里面,需要时按一个键就能使用。这个工具自身并没有实质的补全功能,它等于是给 Vim 的补全模式添加了一个“快速物品栏”,里面各种补全模式的分组和相对位置都可以自己设置,可以把最常用的补全模式放在顺手的位置,需要时按一个键就能启动。Vim 提供的 15 种补全模式已经很厉害了,这个工具如同给它们再装上一个加速器,使它们用起来更顺手,当只按很少几个键就能自动补全很复杂的文本之时,编辑速度自然会进一步提高。

 

1. 关于 Vim 补全模式

    ---- Vim 一共提供了 15 种自动补全的模式(:help ins-completion)。其中有两种的补全列表内容与另外两种相同,只是排序不同,这 15 种模式:

 

1.1 文字编辑用的 3 种:

    ---- 文字编辑显然是不用 Vim 的,所以这三种模式比较悲催,我从没用过:

    K 模式    (Vim 默认: CTRL-X CTRL-K) -- 字典补全,查找字典文件中的匹配单词,组成补全列表
    H 模式    (Vim 默认: CTRL-X CTRL-T) -- 分类补全,查找分类文件(thesaurus 文件)中的匹配单词,组成补全列表
    S 模式    (Vim 默认: CTRL-X s)        -- 拼写建议

 

1.2 Vim 牛人用的两种:

    ---- 自定义的,我也没用过:

    O 模式    (Vim 默认: CTRL-X CTRL-O) -- 全能补全,由一个自定义函数生成补全列表
    U 模式    (Vim 默认: CTRL-X CTRL-U) -- 自定义补全,也是由自定义函数生成补全列表

 

1.3 所有人都喜欢的四种:

    ---- 我用来应付 99% 的情况:

    n 模式    (Vim 默认: CTRL-N)        -- 关键字补全,查找 'complete' 选项指定文件中的匹配单词,组成补全列表
    N 模式    (Vim 默认: CTRL-X CTRL-N) -- 关键字补全,查找当前 buffer 里的匹配单词,组成补全列表

    另外两种: p 模式与 P 模式,分别与 n 模式和 N 模式相同,只是补全列表中候选词的排序相反。

 

1.4 程序员用的三种:

    ---- 我不是程序员,也没用过(悲催了,当初干嘛要写这个 :(...):

    T 模式    (Vim 默认: CTRL-X CTRL-]) -- tag 补全,查找 tag 中的匹配单词,组成补全列表
    I 模式    (Vim 默认: CTRL-X CTRL-I) -- 头文件补全,查找当前 buffer 和 include file 中的匹配单词,组成补全列表
    D 模式    (Vim 默认: CTRL-X CTRL-D) -- 定义补全,查找当前 buffer 与 include file 中匹配的名称定义,组成补全列表

 

1.5 特殊语境下专用的三种:

    ---- 还好,偶尔用一用 ...

    V 模式    (Vim 默认: CTRL-X CTRL-V) -- Vim 补全,查找 Vim 的命令名, 函数名等等,组成补全列表
    F 模式    (Vim 默认: CTRL-X CTRL-F) -- 文件名补全,查找匹配的路径或文件名,组成补全列表
    L 模式    (Vim 默认: CTRL-X CTRL-L) -- 整行补全,查找 'complete' 选项指定文件中匹配的整行内容,组成补全列表

 

2. 使用:

 

2.1 显示器里状态显示的含义

    ---- 按 '\b' 可以切换补全状态显示器的开启和关闭。按下 '\b' 后显示器就会出现在状态栏里,再按 '\b' 显示器从状态栏里消失。下图显示的是 normal 模式下,补全模式未启用时的状态。状态显示器中黑色的字符是按键提示符,其它的是补全模式提示符。可以看到 15 种补全模式被分成 5 组,映射到 5 个按键上。带下划线的是对应分组里最近使用的模式,白色带下划线的是所有分组里最近使用的模式。进入 insert 模式后,按键提示符会“点亮”,开启某种补全模式后,相应的补全模式提示符的显示也会跟着调整。

 



2.2 使用常用的补全模式

    ---- 进入 insert 模式后,显示器里的按键提示符会“点亮”,提示这些按键可用,如下图。此时按相应的按键,就可以开启对应分组里的第一种补全模式。比如在 insert 模式下按 <M-d> 可以开启 p 模式,按 <M-g> 可以开启 V 模式。也就是说,一共有 5 种模式是只需要按一次键就能开启的。

 



2.3 使用其它补全模式

    ---- 要使用其它补全模式,先进入对应的分组然后按 <M-w> 或 <M-r> 往两边切换。比如,要使用 F 模式,就先按 <M-g> 进入 V 模式,再按 <M-r> 往右切换,此时就会先退出 V 模式再开启 F 模式,显示器里的状态显示也会跟着更新。要使用 L 模式时就再按一次 <M-r>,或者在 V 模式下直接按 <M-w> 也行。要使用 D 模式,就在 insert 模式下 <M-f><M-w> 或者 <M-f><M-r><M-r>。

 

2.4 选择候选单词,接受当前单词或退出

    ---- 当成功开启了某个补全模式,补全列表就会出现(即使只有一个匹配也会),此时按 i 和 k 可在列表中上下移动,选择候选单词,按空格接受当前选中的单词并留在 insert 模式下继续输入,按 <Esc> 拒绝候选单词,退出补全模式并留在 insert 模式下,再按一次 <Esc> 才会退出 insert 模式,回到 normal 模式。如果启用补全模式失败(没找到候选单词,或者相关选项没设置),就不会出现补全列表,但是显示器里显示的最近使用的模式仍会相应地更新。

    ---- 另外,如果补全列表比较长,也可以按 <C-f> 和 <C-b> 往前/往后翻动,以便快速定位到目标单词上面,这些操作与 normal 模式下的含义接近,用起来最顺手。

    ---- 选择 i 和 k 作为按键映射稍微限制了补全模式的功能,因为本来开启了补全模式以后按这些键是可以继续输入文本的,Vim 会根据新输入的内容即时查找匹配并不断调整补全列表的内容。但是对我来说,实时匹配功能不重要。如果对补全列表的内容不满意,我会先 <Esc> 退出补全模式,修改输入以后再重新开启。相比之下,按键用着方便对我来说最重要。

 

2.5 <M-e> 键

    ---- 这个键有两个功能。当进入 insert 模式还未启用补全的时候,按 <M-e> 会再次启用上次使用的补全模式(显示器中以白色显示)。当已经开启某个补全模式的时候(补全列表已显示),按 <M-e> 会先接受当前的候选单词,然后再次启用同样的补全模式,如下图:
 



    图中显示了在 F 模式下,连续使用 <M-e> 键来依次补全各级目录名的情况。遇到多个选项就按 i 或 k 选择,然后再按 <M-e> 补全,一直到达要找的那个节点。

 

2.6 <Tab> 键

    ---- 按照 Vim 的设置,进入某些补全模式后再次按同样的按键可以启用一些后续的特殊补全行为模式,比如按 'C-x C-l' 进入 L 模式以后再按 'C-x C-l',还有按 'C-x C-p' 进入 p 模式后,再按 'C-x C-p' 或者 'C-x C-n',等等。在这个工具里,这些进入补全模式以后的后续按键都被映射到 <Tab> 上。也就是说,进入 L 模式后按 <Tab>,就相当于按 'C-x C-l'; 进入 p 模式后按 <Tab>,就相当于按 'C-x C-p',等等。

    ---- 当启用某种补全模式以后,如果显示器里的 <Tab> 按键提示符也点亮,就说明这种补全模式下 <Tab> 键可用。至于这种模式下 <Tab> 是什么含义,详细解释在文档里,一般就是 "连续补全" 或 "差异补全" 两种后续的补全方法。

    ---- "连续补全" 是指接受某个文本单元以后,继续把那个文本单元前期出现时相邻的下一个文本单元(一个单词或一行)作为新的补全列表,这样连续按 <Tab> 就可以连续补全前面出现过的内容,直到把一整段都复制到当前的位置来。而 "差异补全"(文档里所说的 "other context")是指输入了 'se' 以后,想要的补全列表不是 ['self', 'sex', 'select'],而是 ['se:xxx', 'se.yyy', 'se#zzz'] 这种。

    ---- 下图两个图显示了在 p 模式下的连续补全和差异补全:
 

 


    连续补全: p 模式下连续按 <Tab>,可以把前面的一整段内容抄过来。

 

           


    差异补全: p 模式下不选中任何候选词然后按 <Tab> 进入差异补全子模式,新出现的候选词与一般情况下不一样,其中后面几个都不是一般的词,前面两个 "正常" 的词是因为它们出现在其它 buffer 里却没出现在当前 buffer 里,这些都是故意要这样的,所以才叫 "差异补全"。
 

 

    ---- 所以。。。看明白了吧?基本上都是噱头。



2.7 关于补全模式的分组

    ---- 跟“快速物品栏”一样,15 种模式怎么分组,每组怎么排序,哪些模式放在每组的第一位,这些都可以在代码里设置。

 

3. 局限

    ---- 开启一种补全模式会牵涉到一系列后台操作,主要是扫描文件并生成补全列表,过程可能有些延迟(如果候选词列表较长),也有可能失败(比如相关的 Vim 选项没有设置)。如果出现这些情况,Vim 会“发呆”一段时间,期间有可能会“吃掉”一些输入字符。所以,如果在 Vim“发呆”的时候就迫不及待地连续按这些被映射过的按键,那结果可能不是想要的。这不是什么严重问题,最多导致一些不想要的文本被输入,按 'u' 可以取消,但是最好避免这种麻烦,等 Vim 有了反应,比如补全列表出现或者显示出了 error message 以后再做下一步操作。使用 Vim 默认的按键是不会有这个问题的,因为按那种 emacs 式的组合按键不可能按得这么快(emacs 的好处体现出来了)。

 

4. 代码

    ---- 代码现在分散在不同文件里,需要整理。等有空把它们整理好我就贴上来。




 

在 Vim 里控制外部程序,Vim 与 Emacs 与 Conque

---- 上一篇里说的想拿 Vim 当自编程序的 UI,基本已经做到了:



---- 底部的那个 buffer 属于 CSA 模块,这个新写的 Vim 插件就是负责充当外部程序在 Vim 里的界面,buffer 里显示的是外部程序的输出。关于用 Vim 来当 UI 的话题在 第一篇 里讲过。

---- 果然即使用 Python 接口建立了多个线程,也是没有办法让这些线程自动刷新 Vim buffer 里的显示的,况且还有上一篇里说的那个大 Bug。最后采用比较妥协的办法,负责监听的辅助线程还是建的,但是不产生任何输出,只修改后台数据。而主线程每次向外部程序发送输入之后,就 sleep 0.3 秒然后根据后台数据刷新显示,一般情况下都能即时显示外部程序的输出。另外再定义一个手动刷新的按键。

---- 要点记一下:

1. 上一篇里说的那个 bug,是 GUI 的问题,用终端版的 Vim 没事。GUI 版如果在后台建多个线程的话,只允许其中一个线程有可见的输出(也就是造成 Gvim 窗口内显示内容改变),其它线程只能修改后台数据。

2. 如上,即使多线程也不可能自动刷新 Vim buffer 的内容,但可以显示消息(不考虑 bug 的情况下)。自动刷新 Vim buffer 目前已知的只有一个办法:用 VimScript 的 feedkeys() 函数,但是有严重局限,相当于屏蔽了用户的其它输入。这个绝对不用。

3. 辅助线程监听外部程序的输出用的是 Python 的 select.select() 函数,可以监听一个 file descriptor 列表并返回其中有内容可读的 descriptor,但有一点要注意:在产生输出的外部程序已经终止以后,会出现 file descriptor 一直有效但读不到内容的情况,如果不注意这一点就有可能让监听过程陷入死循环,知道的话只要稍微处理一下就可以了。

---- 下一步打算升级程序的 Plotting 模块。现在这个模块还是测试性质的,上次试着画了 2000 多只股票自 2010 年以来的走势,全部单线程运行,花了两个多小时。现在打算改用 Python 的 multiprocessing 模块实现。据文档里说,这个模块与 threading 模块的接口十分类似,这就好了,以前那些关于 threading 的代码可以直接拿过来抄。以后批量执行绘图任务的时候,应该能省不少时间。

 

----------------------------------------------------- <以下补记> -------------------------------------------------------



---- 重大发现,感谢 依云 在回复里的提醒。以上要点第 2 条其实是不成立的。Vim 本身虽然是单线程的,但是通过 Python 完全能够实现多线程的特性。上面所说的辅助线程在监听到外部程序输出以后,就可以操作 vim.buffer 对象,这时 Vim buffer 实际上已经更新了,所缺的只是刷新显示的步骤而已。只要在刷新过程的最后添上一条 Vim 命令: "redraw!" 就可以了,出来的效果实在是 nice!

---- 这样一来,我完全想不到有什么是 Vim + Python 不能做的了。想起来在去年的时候,我想拿 Vim 当自编程序的界面,因为在 Vim 里找不到与外部进程交流的办法而被逼到去看 Emacs 。。。 如今看来,Vim + Python 真是个无敌组合,我忍不住要恶心一把。。。Emacs 你去 Shǐ 吧,哈哈,你的 Elisp 再强,能强得过 Python 吗?速度比不过 Vim,功能又比不过 Python。再想想 Elisp 里头那无数个括号,是要蛋疼到怎样啊?你那 N 个窗口,图片显示功能或许很酷,可是搁不住它慢啊!这事情一慢起来还有什么意思?好在 Emacs 早就顾及这一点,于是定义了更加蛋疼的操作方式,用 Emacs 的人,想快也快不起来,啊哈哈哈 ...

---- 以上讲笑,请浮云看待。如有用 Emacs 的看到,请先冷静 10 秒开骂 。。。

---- 既然这样,Conque 这个 Vim 插件就好拿出来再说一下了。确实像 依云 说的那样,它的实现可以有重大改进。Conque 是在 Vim 里模拟 Shell 终端的一个很流行的插件,它不同版本的实现曾经有过很大改变,但都使用了 Python 接口,插件负责底层交流的部分是 Python 写的。我在去年 7 月左右看过当时最新版的代码,学习了很多。首先,知道使用 Python 接口来编写 Vim 插件就是来自 Conque 的启发,这个还要感谢当时在 Vim-cn 群里跟我提起这个插件的网友 “Strange”。另外,Conque 使用了 Python 语言里负责底层系统操作的标准模块,比如 tty、select 这些,来与外部 Shell 进程交流,这些模块直接对应于底层的 Unix 系统调用,效率高而且可靠,我在自己写的程序里也用 select.select() 来监听外部进程,这也是从 Conque 里学来的。

---- 从当时的 Conque 的代码来看,它是单线程的,从键盘接收用户输入发送给 Shell 进程,而接收靠的是 Vim 函数 feedkeys(),以这个函数作为触发,不停地监听和提取 Shell 输出,经过格式、颜色处理以后发送到 Vim buffer 里。就像上面说的,用 feedkeys() 有严重局限,因为它完全模拟了用户的按键操作。这样一来,真正的用户就只能进行一个键的操作了。如果按键输入多于一个键,后面的键就会被 feedkeys() 冲掉。而 Conque 的解决办法,就是把键盘上几乎每一个键都做了映射,使得 Conque 能通过这些映射来区分是真正的用户输入,还是 feedkeys() 的输入,从而做不同处理。我觉得,这是个很无奈的做法。如果它对速度的影响还不那么糟糕的话,实际上它还屏蔽了 Vim 强大的编辑功能。如果只是把 Shell 原样不动地搬进 Vim 的窗口里是没多大意思的,能够在 Vim 里操作 Shell 的最大好处在于,在编辑那些火星文的 Shell 语句的时候,要能够毫无保留地使用 Vim 的强大编辑功能,这样才真正有意义。

---- 今天这个突破应该能够让以上成为现实,或许一半成为现实。。。因为还有 GUI 的那个大 Bug 在。是不是 Conque 的作者没有用多线程是因为,他当时就知道这个 bug 啊?为了顾及到插件的通用性,就只好放弃这个几近完美的解决方案了。因为这是 X 系统的 bug,我就叫你 X-bug 吧,万事具备,只欠踩死这只 bug!哈哈!

关于通过 vim 的 python 接口使用多线程特性的问题

    ---- Vim Bug 第三弹,以下代码:

python3 << EOF

import threading
import time

print('xxx')

def print_to_vim():
	print('yyy')

threading.Thread(name='test', target=print_to_vim).start()

print('xxx')

time.sleep(3)

EOF

    ---- source 以后 gvim 窗口神奇蒸发,得到如下结果:

    ---- 结论: 通过 python 接口开了多个线程的话,只容许其中一个线程使用 print() 函数。不过这看起来像是 Gnome 或者 X 的问题,不是 Vim 的问题。
   
    ---- 我就是想拿 Vim 当我自编程序的用户界面而已,这至于吗。

    ---- 设计目标:

    1. 从 Vim 窗口里通过自定义命令和按键控制外部程序,不需要进 Shell。
    2. 程序输出通过 Vim 窗口来显示。利用 Python 的多线程特性,主线程处理日常功能,辅助线程专门监听程序的输出,一有输出就刷新 buffer 显示。

    ---- 以上。再配合用新写的 FSE 模块来浏览和管理程序文件,别的什么都不需要了。

    ---- 要是一切如常的话,搞定这些是完全没有问题的,就是怕这些层出不穷的 bug。我容易吗我。

 

FileSystemExplorer 写好了

        ---- 感谢 依云 解决了 Py3 接口内存泄漏的问题。已经下了 firefox 的源码来测试,一共 11 级目录 45000 多个节点,用扩展输出形式,一次打开花费 13 秒左右,反复关闭打开最多用 350 MB 内存,不会一直增加。

        ---- 左边是收藏夹功能,右边是文件结构浏览功能。可以建多个实例浏览不同路径,收藏夹只有一个。


        ---- 收藏夹界面里的操作:

  'O':   添加同级标题
  'o':   添加下级标题
  '\a':  将当前主窗口内的路径加入收藏
  '\s':  显示当前条目的信息
  '\d':  删除当前光标下的标题(recursive)或节点
  '\r':  修改当前标题


        ---- 浏览界面里的操作:

设置属性:
	:Set hidden	不显示隐藏文件(默认)
	:Set nohidden	显示隐藏文件
	:Set byname	按名称排序(默认)
	:Set bysize	按大小排序
	:Set bymtime	按修改时间排序
	:Set byatime	按访问时间排序
	:Set desc	逆序
	:Set asc	正序(默认)
	:Set basic	显示基本信息(只有文件或目录名)
	:Set extend	显示扩展信息(默认,包括大小、修改时间、访问时间)
	:Set lock	锁定当前根节点,只能在其内部浏览
	:Set unlock	解除根节点锁定(默认)
浏览:
	:CD {dir}	切换到指定节点
	"Enter":	打开/关闭目录,打开文件。
	"Shift-Enter":	递归式打开/关闭目录,打开文件。
	"\r":		刷新显示。原来打开的节点仍打开,原来关闭的仍关闭
	"\f":		显示当前节点的信息。
	"\s":		把根节点切换到指定节点。
	"\c":		关闭所有目录节点,除了当前节点和它的上级节点以外
	"Alt-i, Alt-k" 定位到上一个/下一个节点。
		1. 当前光标位于缩进部分的第 i 个 Tab 上时,定位到上一个/下一个 i 级节点。
		2. 当光标位于其它位置时,定位到上一个/下一个同级节点。
文件操作:
	"o":		在指定目录下新建一个节点。
	"O":		建立一个指定节点的同级节点。
	"\d":		删除指定的节点。
	"\t":		在指定目录或指定文件所在的目录下开启一个终端。 


        ---- 源文件在 “Vim 插件” 页面里。但是自用插件写的时候完全没考虑通用性,所以换一台电脑八成是不能用的,只给感兴趣的人作参考。

Gvim7.3 + Python3 接口的内存使用完全失控

    ---- 为什么会有这种鸟事,为什么为什么。一度以为 Vim 已经很熟不需要再搞了,谁知道风平浪静的日子是没有的。快抓狂了。Vim 是这么成熟的编辑器,这种事难道该发生么。

    ---- 原本以为是 Python 代码写的有问题,对 Python 不熟,内存泄漏什么的。逐渐排除了以后开始锁定到 Python 接口上面,具体讲就是 vim.buffer 这个东西。这个是测试代码:
 

lcd %:h
tabedit tmpbuffer
setlocal buftype=nofile

python3 << EOF
for i in range(3):
	flines= ['x'*200] * 50000
	vim.command("%s+\\_.*++g")
	for fl in flines:
		vim.current.buffer.append(fl)
	del flines[:]
EOF


    ---- 而这个是结果:

        在 gvim7.2 + Python2 接口下面:
            第 1 次 source:    46.8 MB    内存,关闭 tmpbuffer 后不变
            第 2 次 source:    59.2 MB    内存,关闭 tmpbuffer 后 58.1
            第 3 次 source:    61.6 MB    内存,关闭 tmpbuffer 后 60.4
            第 4 次 source:    63.9 MB    内存,关闭 tmpbuffer 后 63.6
            第 5 次 source:    66.3 MB    内存,关闭 tmpbuffer 后 65.1
       
        在 gvim7.3 + Python3 接口下面:
            第 1 次 source:    142.6 MB 内存,关闭 tmpbuffer 后不变
            第 2 次 source:    238.5 MB 内存,关闭 tmpbuffer 后不变
            第 3 次 source:    334.5 MB 内存,关闭 tmpbuffer 后不变

    ---- buftype 选项其实对结果无影响。第一个看上去还好。第二个,好像 gvim7.3 的 Python3 接口是没有任何内存回收的,就这么任由它增长。另外初次运行的内存消耗也是 gvim7.2 的三倍。这没问题么? 问题大了。后来索性一直运行,这个是最后的结果:

    ---- 我表示这不是我的问题。考虑把这个贴到 vim-use 群里面。

    ---- 这个很打击我写插件的热情。不过还是要写,不写就亏大了,尼玛感觉就跟炒股被套一样。昨天新加的:

        1. 丰富了颜色

        2. 添加了功能: 除当前节点和它的上级节点之外,关闭其它所有打开的节点。

        3. 添加了功能: 锁定根节点,只能在根节点内部浏览。

用来演示 gvim 预定义颜色项的脚本

    ---- 原来贴在 vim-cn 群里的一个脚本,这里也贴一下。

 

 

    ---- gvim 有好几百种预定义的颜色名称,比如 'DarkBlue', 'LightRed' 之类。这个脚本用来展示这些颜色在你的 gvim 里看起来如何。

    ---- 纯 VimScript 写的。用 gvim 打开脚本,然后
       
        :source %
   
    就可以了。

    ---- 会开启一个叫 'ShowVimColors.tmp' 的临时 buffer 用作显示,是 “纯” buffer 没有磁盘文件。内容分两部分,前一部分展示颜色本身,后一部分展示与黑白两色的搭配效果。自己定义语法项要分配颜色的时候,可以用作参考。

    ---- 如果想搞 DIY 自己配色,记得 依云 在她的 blog 里写过一个 gvim 调色板,在 这里。也可以自己先写出来,比如 '#aabbcc',加进这个脚本里再运行一遍就能看见效果。

 

"========================================================================================================================
"=======	ShowVimColors.vim
"========================================================================================================================
"=======	作者:Jacky Liu
"=======	bluegene8210@gmail.com
"========================================================================================================================

" 网上找到的 Vim 里的预定义颜色
let	s:VimColors=[
\	'Snow',			'GhostWhite',		'WhiteSmoke',		'Gainsboro',		'FloralWhite',		'OldLace',		'Linen',
\	'AntiqueWhite',		'PapayaWhip',		'BlanchedAlmond',	'Bisque',		'PeachPuff',		'NavajoWhite',		'Azure',
\	'Moccasin',		'Cornsilk',		'Ivory',		'LemonChiffon',		'Seashell',		'Honeydew',		'MintCream',
\	'AliceBlue',		'Lavender',		'LavenderBlush',	'MistyRose',		'White',		'Black',		'DarkSlateGray',
\	'DimGray',		'SlateGray',		'LightSlateGray',	'Gray',			'LightGray',		'MidnightBlue',		'Navy',
\	'NavyBlue',		'CornflowerBlue',	'DarkSlateBlue',	'SlateBlue',		'MediumSlateBlue',
\	'LightSlateBlue',	'MediumBlue',		'RoyalBlue',		'Blue',			'DodgerBlue',		'DeepSkyBlue',
\	'SkyBlue',		'LightSkyBlue',		'SteelBlue',		'LightSteelBlue',	'LightBlue',		'PowderBlue',
\	'PaleTurquoise',	'DarkTurquoise',	'MediumTurquoise',	'Turquoise',		'Cyan',			'LightCyan',
\	'CadetBlue',		'MediumAquamarine',	'Aquamarine',		'DarkGreen',		'DarkOliveGreen',
\	'DarkSeaGreen',		'SeaGreen',		'MediumSeaGreen',	'LightSeaGreen',	'PaleGreen',
\	'SpringGreen',		'LawnGreen',		'Green',		'Chartreuse',		'MediumSpringGreen',	'GreenYellow',
\	'LimeGreen',		'YellowGreen',		'ForestGreen',		'OliveDrab',		'DarkKhaki',		'Khaki',
\	'PaleGoldenrod',	'LightGoldenrodYellow',	'LightYellow',		'Yellow',		'Gold',
\	'LightGoldenrod',	'Goldenrod',		'DarkGoldenrod',	'RosyBrown',		'IndianRed',		'SaddleBrown',
\	'Sienna',		'Peru',			'Burlywood',		'Beige',		'Wheat',		'SandyBrown',		'Tan',			'Chocolate',		'Firebrick',
\	'Brown',		'DarkSalmon',		'Salmon',		'LightSalmon',		'Orange',		'DarkOrange',		'Coral',		'LightCoral',
\	'Tomato',		'OrangeRed',		'Red',			'HotPink',		'DeepPink',		'Pink',			'LightPink',		'PaleVioletRed',
\	'Maroon',		'MediumVioletRed',	'VioletRed',		'Magenta',		'Violet',		'Plum',			'Orchid',
\	'MediumOrchid',		'DarkOrchid',		'DarkViolet',		'BlueViolet',		'Purple',		'MediumPurple',
\	'Thistle',		'Snow1',		'Snow2',		'Snow3',		'Snow4',		'Seashell1',		'Seashell2',		'Seashell3',
\	'Seashell4',		'AntiqueWhite1',	'AntiqueWhite2',	'AntiqueWhite3',	'AntiqueWhite4',
\	'Bisque1',		'Bisque2',		'Bisque3',		'Bisque4',		'PeachPuff1',		'PeachPuff2',		'PeachPuff3',
\	'PeachPuff4',		'NavajoWhite1',		'NavajoWhite2',		'NavajoWhite3',		'NavajoWhite4',
\	'LemonChiffon1',	'LemonChiffon2',	'LemonChiffon3',	'LemonChiffon4',	'Cornsilk1',
\	'Cornsilk2',		'Cornsilk3',		'Cornsilk4',		'Ivory1',		'Ivory2',		'Ivory3',		'Ivory4',		'Honeydew1',
\	'Honeydew2',		'Honeydew3',		'Honeydew4',		'LavenderBlush1',	'LavenderBlush2',
\	'LavenderBlush3',	'LavenderBlush4',	'MistyRose1',		'MistyRose2',		'MistyRose3',
\	'MistyRose4',		'Azure1',		'Azure2',		'Azure3',		'Azure4',		'SlateBlue1',		'SlateBlue2',
\	'SlateBlue3',		'SlateBlue4',		'RoyalBlue1',		'RoyalBlue2',		'RoyalBlue3',		'RoyalBlue4',
\	'Blue1',		'Blue2',		'Blue3',		'Blue4',		'DodgerBlue1',		'DodgerBlue2',		'DodgerBlue3',
\	'DodgerBlue4',		'SteelBlue1',		'SteelBlue2',		'SteelBlue3',		'SteelBlue4',		'DeepSkyBlue1',
\	'DeepSkyBlue2',		'DeepSkyBlue3',		'DeepSkyBlue4',		'SkyBlue1',		'SkyBlue2',		'SkyBlue3',
\	'SkyBlue4',		'LightSkyBlue1',	'LightSkyBlue2',	'LightSkyBlue3',	'LightSkyBlue4',
\	'SlateGray1',		'SlateGray2',		'SlateGray3',		'SlateGray4',		'LightSteelBlue1',
\	'LightSteelBlue2',	'LightSteelBlue3',	'LightSteelBlue4',	'LightBlue1',		'LightBlue2',
\	'LightBlue3',		'LightBlue4',		'LightCyan1',		'LightCyan2',		'LightCyan3',		'LightCyan4',
\	'PaleTurquoise1',	'PaleTurquoise2',	'PaleTurquoise3',	'PaleTurquoise4',	'CadetBlue1',
\	'CadetBlue2',		'CadetBlue3',		'CadetBlue4',		'Turquoise1',		'Turquoise2',		'Turquoise3',
\	'Turquoise4',		'Cyan1',		'Cyan2',		'Cyan3',		'Cyan4',		'DarkSlateGray1',	'DarkSlateGray2',
\	'DarkSlateGray3',	'DarkSlateGray4',	'Aquamarine1',		'Aquamarine2',		'Aquamarine3',
\	'Aquamarine4',		'DarkSeaGreen1',	'DarkSeaGreen2',	'DarkSeaGreen3',	'DarkSeaGreen4',
\	'SeaGreen1',		'SeaGreen2',		'SeaGreen3',		'SeaGreen4',		'PaleGreen1',		'PaleGreen2',
\	'PaleGreen3',		'PaleGreen4',		'SpringGreen1',		'SpringGreen2',		'SpringGreen3',
\	'SpringGreen4',		'Green1',		'Green2',		'Green3',		'Green4',		'Chartreuse1',		'Chartreuse2',
\	'Chartreuse3',		'Chartreuse4',		'OliveDrab1',		'OliveDrab2',		'OliveDrab3',		'OliveDrab4',
\	'DarkOliveGreen1',	'DarkOliveGreen2',	'DarkOliveGreen3',	'DarkOliveGreen4',	'Khaki1',
\	'Khaki2',		'Khaki3',		'Khaki4',		'LightGoldenrod1',	'LightGoldenrod2',	'LightGoldenrod3',
\	'LightGoldenrod4',	'LightYellow1',		'LightYellow2',		'LightYellow3',		'LightYellow4',
\	'Yellow1',		'Yellow2',		'Yellow3',		'Yellow4',		'Gold1',		'Gold2',		'Gold3',		'Gold4',		'Goldenrod1',
\	'Goldenrod2',		'Goldenrod3',		'Goldenrod4',		'DarkGoldenrod1',	'DarkGoldenrod2',
\	'DarkGoldenrod3',	'DarkGoldenrod4',	'RosyBrown1',		'RosyBrown2',		'RosyBrown3',
\	'RosyBrown4',		'IndianRed1',		'IndianRed2',		'IndianRed3',		'IndianRed4',		'Sienna1',		'Sienna2',
\	'Sienna3',		'Sienna4',		'Burlywood1',		'Burlywood2',		'Burlywood3',		'Burlywood4',		'Wheat1',
\	'Wheat2',		'Wheat3',		'Wheat4',		'Tan1',			'Tan2',			'Tan3',			'Tan4',			'Chocolate1',		'Chocolate2',
\	'Chocolate3',		'Chocolate4',		'Firebrick1',		'Firebrick2',		'Firebrick3',		'Firebrick4',
\	'Brown1',		'Brown2',		'Brown3',		'Brown4',		'Salmon1',		'Salmon2',		'Salmon3',		'Salmon4',
\	'LightSalmon1',		'LightSalmon2',		'LightSalmon3',		'LightSalmon4',		'Orange1',		'Orange2',
\	'Orange3',		'Orange4',		'DarkOrange1',		'DarkOrange2',		'DarkOrange3',		'DarkOrange4',		'Coral1',
\	'Coral2',		'Coral3',		'Coral4',		'Tomato1',		'Tomato2',		'Tomato3',		'Tomato4',		'OrangeRed1',
\	'OrangeRed2',		'OrangeRed3',		'OrangeRed4',		'Red1',			'Red2',			'Red3',			'Red4',			'DeepPink1',
\	'DeepPink2',		'DeepPink3',		'DeepPink4',		'HotPink1',		'HotPink2',		'HotPink3',		'HotPink4',
\	'Pink1',		'Pink2',		'Pink3',		'Pink4',		'LightPink1',		'LightPink2',		'LightPink3',		'LightPink4',
\	'PaleVioletRed1',	'PaleVioletRed2',	'PaleVioletRed3',	'PaleVioletRed4',	'Maroon1',
\	'Maroon2',		'Maroon3',		'Maroon4',		'VioletRed1',		'VioletRed2',		'VioletRed3',		'VioletRed4',
\	'Magenta1',		'Magenta2',		'Magenta3',		'Magenta4',		'Orchid1',		'Orchid2',		'Orchid3',		'Orchid4',
\	'Plum1',		'Plum2',		'Plum3',		'Plum4',		'MediumOrchid1',	'MediumOrchid2',	'MediumOrchid3',
\	'MediumOrchid4',	'DarkOrchid1',		'DarkOrchid2',		'DarkOrchid3',		'DarkOrchid4',		'Purple1',
\	'Purple2',		'Purple3',		'Purple4',		'MediumPurple1',	'MediumPurple2',	'MediumPurple3',
\	'MediumPurple4',	'Thistle1',		'Thistle2',		'Thistle3',		'Thistle4',
\	'DarkCyan',		'DarkMagenta',		'DarkRed',		'LightGreen',
\	'#132b4a',		'#233b5a',		'#005c70',		'#031b4a',		'#d0d0e0',
\	'NONE']



let s:statements= []	" 用来定义 syntax group 以及调用 matchadd() 的 VimScript 语句。
let s:filelines= []	" 加到临时 buffer 内的内容

call add(s:filelines, '输出的第一部分,展示颜色本身')
call add(s:filelines, '')

for vcolor in s:VimColors
	call add(s:filelines, vcolor . repeat(' ', 160-len(vcolor)))
	
	if vcolor =~ '^#' | let vcname= vcolor[1:] | else | let vcname= vcolor | endif
	
	"	syntax group container
	"======================================================================================================================
	let sgcontname= vcname . '_cont'
	call add(s:statements, 'syntax match ' . sgcontname . ' /^' . vcolor . '\> *$/')

	"	指定主体,默认背景
	"======================================================================================================================
	let fghigrpname= '_' . vcname . '_' . 'NONE'			" hi group name (as frontground)
	call add(s:statements, 'hi ' . fghigrpname . ' guifg=' . vcolor . ' guibg=' . 'NONE')	" 定义 hi group
	let fgsyngrpname= 'sgrp_' . fghigrpname				" syntax group name (as frontground)
	call add(s:statements, 'syntax match ' . fgsyngrpname . ' /^' . vcolor . '\>/ containedin=' . sgcontname . ' contained')	" 定义 syntax group
	call add(s:statements, 'hi link '. fgsyngrpname . ' ' . fghigrpname)	" syntax group 与 hi group 联系起来

	"	默认主体,指定背景
	"======================================================================================================================
	let bghigrpname= '_' . 'NONE' . '_' . vcname			" hi group name (as background)
	call add(s:statements, 'hi ' . bghigrpname . ' guifg=' . 'NONE' . ' guibg=' . vcolor)	" 定义 hi group
	let bgsyngrpname= 'sgrp_' . bghigrpname				" syntax group name (as background)
	call add(s:statements, 'syntax match ' . bgsyngrpname . ' / \+/' . ' containedin=' . sgcontname . ' contained')	" 同上
	call add(s:statements, 'hi link '. bgsyngrpname . ' ' . bghigrpname)	" 同上
	
endfor

call add(s:filelines, '')
call add(s:filelines, '输出的第二部分,展示作为主体色或作为背景色与黑白两色的搭配效果')
call add(s:filelines, '')

for vcolor in s:VimColors

	if vcolor =~ '^#' | let vcname= vcolor[1:] | else | let vcname= vcolor | endif

	"	指定主体,黑色背景
	"======================================================================================================================
	let gname_1= '_' . vcname . '_' . 'Black'						" syntax group name
	call add(s:statements, 'hi	' . gname_1 . '	guifg=' . vcolor . '	guibg=' . 'Black')	" 定义语句
	
	let skwname= 'skw_' . gname_1
	call add(s:statements, 'syntax keyword ' . skwname . ' ' . gname_1)
	call add(s:statements, 'hi link ' . skwname . ' ' . gname_1)

	"	黑色主体,指定背景
	"======================================================================================================================
	let gname_2= '_' . 'Black' . '_' . vcname						" syntax group name
	call add(s:statements, 'hi	' . gname_2 . '	guifg=' . 'Black' . '	guibg=' . vcolor)	" 定义语句
	
	let skwname= 'skw_' . gname_2
	call add(s:statements, 'syntax keyword ' . skwname . ' ' . gname_2)
	call add(s:statements, 'hi link ' . skwname . ' ' . gname_2)

	"	指定主体,白色背景
	"======================================================================================================================
	let gname_3= '_' . vcname . '_' . 'White'						" syntax group name
	call add(s:statements, 'hi	' . gname_3 . '	guifg=' . vcolor . '	guibg=' . 'White')	" 定义语句
	
	let skwname= 'skw_' . gname_3
	call add(s:statements, 'syntax keyword ' . skwname . ' ' . gname_3)
	call add(s:statements, 'hi link ' . skwname . ' ' . gname_3)

	"	白色主体,指定背景
	"======================================================================================================================
	let gname_4= '_' . 'White' . '_' . vcname						" syntax group name
	call add(s:statements, 'hi	' . gname_4 . '	guifg=' . 'White' . '	guibg=' . vcolor)	" 定义语句
	
	let skwname= 'skw_' . gname_4
	call add(s:statements, 'syntax keyword ' . skwname . ' ' . gname_4)
	call add(s:statements, 'hi link ' . skwname . ' ' . gname_4)

	"	buffer 新加入一行
	"======================================================================================================================
	let newline=	gname_1 . repeat(' ', 30-len(gname_1)) .
	\		gname_2 . repeat(' ', 30-len(gname_2)) .
	\		gname_3 . repeat(' ', 30-len(gname_3)) . gname_4
	call add(s:filelines, newline)

endfor



tabedit ShowVimColors.tmp	" 开启临时 buffer
setlocal buftype=nofile		" 设置 buffer 属性
call append(0, s:filelines)	" 加入内容

for stat in s:statements	" “上色”
	exe stat
endfor

"	autocmd BufWinLeave ShowVimColors.tmp	call clearmatches()	" 走之前清掉。不然 Vim 会一直很慢



 

Vim 的 Python 接口的内存回收机制有问题 !!!!!

    ---- 续写 FileSystemExplorer 这个插件,现在写好的:
        1. 能刷新
        2. 能设置属性:
            a. 是否显示隐藏文件
            b. 显示基本输出(只有名称)还是扩展输出(包括大小,修改时间与访问时间)
            c. 设置根据名称 / 大小 / 修改时间 / 访问时间排序,设置正序或逆序。
        3. 能递归式打开节点(但是结果恐怖,后述。)
   
    ---- 几个要点记一下:

        1. 数据结构内部不要形成引用回路(reference cycle)。让文件节点同时保持对上级和下级节点的引用可以方便操作,但是对上级的引用要用 weakref 实现。如果形成引用回路就会产生没法析构的对象,以及内存泄漏。

        2. 怎样定义一个类似 list 的对象: 直接继承 list 类型不是个好主意,应该继承 Abstract Base Classes(ABC)里面的 MutableSequence,然后覆盖掉以下 “虚函数”: __init__(), __len__(), __getitem__(), __del__(), __setitem__(), insert()。

        3. 类 list 对象的读取操作与 list 形式一样,可以用 slicing,也可以用 comprehension。但是赋值不一样,不能直接
           

                self= 另一个sequence


        ,需要注意。

        4. 可以使用 del[:] 清除一个 类 list 对象,但是操作之前要先清除成员之间的引用关系。否则即使没有引用回路存在,Python 也不知道先清除哪个成员,结果又是一堆没法析构的对象。(对这一点还不是十分确定,有可能是太过谨慎了,回头写个程序验证一下。)

        5. 在函数的默认参数里不要使用可变值类型(mutable type)。比如:
           
            def my_function(arg=[]):
                pass

        这样是不对的,第二次调用时那个值就会变掉。应该这样:

            def my_function(arg=None):
                if arg is None: arg= []

    ---- 测试: 使用基本输出形式,显示隐藏文件,用 recursive 方式打开我的根目录(但是产生输出内容用的是线性处理方式),将近 49000 个节点(垃圾文件触目惊心),时间大概 8 秒。记得以前用 NerdTree 递归式打开 firefox 源文件的目录,也是几万个节点,花了两分钟以上。用扩展输出形式,多用 10 秒。我的电脑是 07 年的双核笔记本。所以 Python 接口的速度还是不错的,跟 VimScript 相比。

    ---- 最后一个大要点必须单独写:

                    Vim 的 Python 接口的内存回收机制有问题 !!!!!

    如上。虽然已经通过定义 __del__() 等方式确认所建立的 Python 数据对象都能被正确析构,但是内存占用还是一路彪升。用 recursive 方式打开一次根目录会增加几十 MB 内存,但是这些对象析构的时候内存却不减少。试着来回打开关闭了十几次,内存就到了 400 MB 以上,通过资源管理器来看,gvim 成了最耗内存的程序。

    后来把 Python 代码搬出来,改成一般的 Python 测试程序,通过 Shell 运行,没出现这种情况。递归式建立 49000 个节点会耗用 160MB 内存,但是后面无论怎样销毁再建立,内存都不再增加,可见内存回收在起作用。所以不是我代码写的有问题,有可能是 Vim 与 Python 的 garbage collector 通气不畅所致。

    [补记]:

    ---- 又想了个办法,在原来的 Vim 与 Python 混合代码里定义了一个测试命令,模拟其它所有内部操作但只是不往 Vim Buffer 做任何输出。通过此命令反复进行大量数据结构的建立与销毁操作,结果与上面的测试程序一样,内存占用是固定的,不会一直增加。所以问题出在 Python 接口上面。

    ---- 先不想这么多,内存问题绝对是我能力以外的事情。只要平时不会二到用 recursive 方式打开几万个节点的目录再关闭,再打开再关闭,再打开再关闭 ... 这个插件还是能用的。

    ---- 关于 Reference Cycle 的测试代码如下,如果两个对象互相硬指向对方的话,析构函数 __del__() 不会被调用,于是内存泄漏。

 

# -*- coding: utf-8 -*-

import weakref

class Child:
	
	def __init__(self, parent):
		
		# 这里切换使用软指向还是硬指向
		#	self._parent= parent	# XXX: 硬指向上级对象
		self._parent= weakref.ref(parent)	# XXX: 软指向上级对象
		
		print('Child.__init__() -- called !')

	def __del__(self):
		print('Child.__del__() -- called !')


class Parent:
	
	def __init__(self):
		self._child= None
		print('Parent.__init__() -- called !')

	def add_child(self, child):
		self._child= child			# XXX: 硬指向下级对象

	def __del__(self):
		print('Parent.__del__() -- called !')
		


parent= Parent()
child= Child(parent=parent)
parent.add_child(child=child)

del parent
del child

 

FileSystemExplorer 终于成形了

---- 外部的 Vim 插件我基本都是装上试试然后就卸掉,用到的基本都自己写。在文件操作方面,因为自带的 Netrw 实在不够用,一些 VimScript 写的外部插件(比如 NerdTree)又有慢的问题。自己写的 FileSystemExplorer 这个插件自从起个头以后名不副实了很久,现在终于成形了。

---- 主体还是 Python 写的(通过 Vim 的 Python 接口),纯 VimScript 写的太慢。但是目前还是很菜。以下是设想中,准备逐步添加的功能:

1. 刷新(也可以用于删除、新建操作之后)

2. 改变排序方式(也可以用于递归式打开目录之后)

3. 删除目录或文件

4. 批量删除

5. 转移目录或文件

6. 改名

7. 递归式打开目录

8. 新建目录或文件

9. 快速定位(到同级上一个/下一个节点,到上级节点)

10. 智能化开启文件(通过后台调用其它程序)

---- 基本上 Vim 和 Python 折腾到现在,不确定性已经越来越少。接下来的事可能跟工厂做工差不多。继续开工 。。。

用 Python / Matplotlib 画出来的股票 K线图 (二)

 

---- 最新的在这里: 用 Python / Matplotlib 画出来的股票 K线图 (四)

 

---- 下一篇在这里: 用 Python / Matplotlib 画出来的股票 K线图 (三)

 

---- 上一版的改进,双股同列 + 无数细小改进,如下图。dpi= 300。明的一条是个股走势,暗的是同期的指数走势。这大概是近期最强的一只。

 

---- 要想培养对走势的感觉,采用固定比例尺的图形是必须的。一般股票软件里的图形都为显示方便而做了变形处理,用处不大。

 

---- 图形感觉差不多了,告一段落。接下来的目标是 股本结构、历史分配、行业板块、股东研究 这些信息,还包括个股资讯。实时的数据仍然暂时不碰。

 

 

---- 源码贴出来。因为 Matplotlib 还不支持 Python3, 所以单写了一个 Python2 脚本。注意绘图数据是用 pickle file 传递的。

[补记:我决定放弃线性坐标了。这个脚本只支持对数坐标。]

 

# -*- coding: utf-8 -*-

import os
import sys
import pickle
import math
import datetime
import matplotlib

matplotlib.use("WXAgg", warn=True)	# 这个要紧跟在 import matplotlib 之后,而且必须安装了 wxpython 2.8 才行。

import matplotlib.pyplot as pyplot
import matplotlib.font_manager as font_manager 

import numpy
from matplotlib.ticker import FixedLocator, MultipleLocator, FuncFormatter, NullFormatter



__font_properties__=font_manager.FontProperties(fname='/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc')
__color_lightsalmon__= '#ffa07a'
__color_pink__= '#ffc0cb'
__color_navy__= '#000080'






def Plot(pfile, figpath):
	'''
	pfile 指明存放绘图数据的 pickle file,figpath 指定图片需存放的路径
	'''

	fileobj= open(name=pfile, mode='rb')
	pdata= pickle.load(fileobj)
	fileobj.close()
	os.remove(pfile)

	#	计算图片的尺寸(单位英寸)
	#	注意:Python2 里面, "1 / 10" 结果是 0, 必须写成 "1.0 / 10" 才会得到 0.1
	#==================================================================================================================================================
	length= len(pdata[u'日期'])		# 所有数据的长度,就是天数

	open_price_pri= pdata[u'开盘'][0]	# int 类型
	open_price_sec= pdata[u'开盘二'][0]	# 同上

	highest_price_pri= max( [phigh for phigh in pdata[u'最高'] if phigh != None] )		# 第一个行情的最高价
	highest_price_sec= max( [phigh for phigh in pdata[u'最高二'] if phigh != None] )	# 第二个行情的最高价
	highest_price= max(highest_price_pri, highest_price_sec*open_price_pri/open_price_sec)	# 以第一个行情为基准修正出的总最高价
	
	lowest_price_pri= min( [plow for plow in pdata[u'最低'] if plow != None] )	# 最低价
	lowest_price_sec= min( [plow for plow in pdata[u'最低二'] if plow != None] )	# 最低价
	lowest_price= min(lowest_price_pri, lowest_price_sec*open_price_pri/open_price_sec)	# 以第一个行情为基准修正出的总最低价



	yhighlim_price= int(highest_price * 1.1)	# K线子图 Y 轴最大坐标
	ylowlim_price=  int(lowest_price / 1.1)		# K线子图 Y 轴最小坐标



	xfactor= 10.0/230.0	# 一条 K 线的宽度在 X 轴上所占距离(英寸)
	yfactor= 0.3	# Y 轴上每一个距离单位的长度(英寸),这个单位距离是线性坐标和对数坐标通用的

	expbase= 1.1	# 底数,取得小一点,比较接近 1。股价 3 元到 4 元之间有大约 3 个单位距离
	
	# XXX: 价格在 Y 轴上的 “份数”。注意,虽然最高与最低价是以第一个行情为基准修正出来的,但其中包含的倍数因子对结果无影响,即:
	#	log(base, num1) - log(base, num2) == 
	#	log(base, num1/num2) ==
	#	log(base, k*num1/k*num2) ==
	#	log(base, k*num1) - log(base, k*num2)
	# ,这是对数运算的性质。
	ymulti_price= math.log(yhighlim_price, expbase)	- math.log(ylowlim_price, expbase)	
	
	ymulti_vol= 3.0		# 成交量部分在 Y 轴所占的 “份数”
	ymulti_top= 1.2		# 顶部空白区域在 Y 轴所占的 “份数”
	ymulti_bot= 1.2		# 底部空白区域在 Y 轴所占的 “份数”

	xmulti_left= 12.0	# 左侧空白区域所占的 “份数”
	xmulti_right= 12.0	# 右侧空白区域所占的 “份数”

	xmulti_all= length + xmulti_left + xmulti_right
	xlen_fig= xmulti_all * xfactor		# 整个 Figure 的宽度
	ymulti_all= ymulti_price + ymulti_vol + ymulti_top + ymulti_bot
	ylen_fig= ymulti_all * yfactor		# 整个 Figure 的高度
	
	rect_1= (xmulti_left/xmulti_all, (ymulti_bot+ymulti_vol)/ymulti_all, length/xmulti_all, ymulti_price/ymulti_all)	# K线图部分
	rect_2= (xmulti_left/xmulti_all, ymulti_bot/ymulti_all, length/xmulti_all, ymulti_vol/ymulti_all)	# 成交量部分



	#	建立 Figure 对象
	#==================================================================================================================================================
	figfacecolor= __color_pink__
	figedgecolor= __color_navy__
	figdpi= 300
	figlinewidth= 1.0

	figobj= pyplot.figure(figsize=(xlen_fig, ylen_fig), dpi=figdpi, facecolor=figfacecolor, edgecolor=figedgecolor, linewidth=figlinewidth)	# Figure 对象

	# 整个 figure 的标题
	title_pri= (pdata[u'代码'] + ' ' if u'代码' in pdata else '') + pdata[u'简称']
	title_sec= (pdata[u'代码二'] + '   ' if u'代码二' in pdata else '') + pdata[u'简称二']
	
	figobj.suptitle(title_pri + ' / ' + title_sec, fontsize=12, fontproperties=__font_properties__)



	#==================================================================================================================================================
	#==================================================================================================================================================
	#=======	
	#=======	XXX: 第一只:成交量部分
	#=======	
	#==================================================================================================================================================
	#==================================================================================================================================================

	#	第一只:添加 Axes 对象
	#==================================================================================================================================================
	axes_2= figobj.add_axes(rect_2, axis_bgcolor='black')
	axes_2.set_axisbelow(True)	# 网格线放在底层

	#	第一只:改变坐标线的颜色
	#==================================================================================================================================================
	for child in axes_2.get_children():
		if isinstance(child, matplotlib.spines.Spine):
			child.set_color('lightblue')

	#	第一只:得到 X 轴 和 Y 轴 的两个 Axis 对象
	#==================================================================================================================================================
	xaxis_2= axes_2.get_xaxis()
	yaxis_2= axes_2.get_yaxis()

	#	第一只:设置两个坐标轴上的 grid
	#==================================================================================================================================================
	xaxis_2.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2)
	xaxis_2.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1)

	yaxis_2.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2)
	yaxis_2.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1)



	#==================================================================================================================================================
	#=======	第一只:成交量绘图
	#==================================================================================================================================================
	xindex= numpy.arange(length)	# X 轴上的 index,一个辅助数据

	zipoc= zip(pdata[u'开盘'], pdata[u'收盘'])
	up=   numpy.array( [ True if po < pc and po != None else False for po, pc in zipoc] )		# 标示出该天股价日内上涨的一个序列
	down= numpy.array( [ True if po > pc and po != None else False for po, pc in zipoc] )		# 标示出该天股价日内下跌的一个序列
	side= numpy.array( [ True if po == pc and po != None else False for po, pc in zipoc] )		# 标示出该天股价日内走平的一个序列



	if u'成交额' in pdata:
		volume= pdata[u'成交额']
	else:
		volume= pdata[u'成交量']

	rarray_vol= numpy.array(volume)
	volzeros= numpy.zeros(length)	# 辅助数据

	# XXX: 如果 up/down/side 各项全部为 False,那么 vlines() 会报错。
	if True in up:
		axes_2.vlines(xindex[up], volzeros[up], rarray_vol[up], edgecolor='red', linewidth=3.0, label='_nolegend_')
	if True in down:
		axes_2.vlines(xindex[down], volzeros[down], rarray_vol[down], edgecolor='green', linewidth=3.0, label='_nolegend_')
	if True in side:
		axes_2.vlines(xindex[side], volzeros[side], rarray_vol[side], edgecolor='0.7', linewidth=3.0, label='_nolegend_')
	


	#	第一只:设定 X 轴坐标的范围 
	#==================================================================================================================================================
	axes_2.set_xlim(-1, length)



	#	第一只:设定 X 轴上的坐标
	#==================================================================================================================================================
	datelist= [ datetime.date(int(ys), int(ms), int(ds)) for ys, ms, ds in [ dstr.split('-') for dstr in pdata[u'日期'] ] ]

	# 确定 X 轴的 MajorLocator
	mdindex= []	# 每个月第一个交易日在所有日期列表中的 index
	years= set([d.year for d in datelist])	# 所有的交易年份

	for y in sorted(years):		
		months= set([d.month for d in datelist if d.year == y])		# 当年所有的交易月份
		for m in sorted(months):
			monthday= min([dt for dt in datelist if dt.year==y and dt.month==m])	# 当月的第一个交易日
			mdindex.append(datelist.index(monthday))

	xMajorLocator= FixedLocator(numpy.array(mdindex))

	# 第一只:确定 X 轴的 MinorLocator
	wdindex= {}	# value: 每周第一个交易日在所有日期列表中的 index; key: 当周的序号 week number(当周是第几周)
	
	for d in datelist:
		isoyear, weekno= d.isocalendar()[0:2]
		dmark= isoyear*100 + weekno
		if dmark not in wdindex:
			wdindex[dmark]= datelist.index(d)

	xMinorLocator= FixedLocator(numpy.array( sorted(wdindex.values()) ))

	# 第一只:确定 X 轴的 MajorFormatter 和 MinorFormatter
	def x_major_formatter_2(idx, pos=None):
		return datelist[idx].strftime('%Y-%m-%d')

	def x_minor_formatter_2(idx, pos=None):
		return datelist[idx].strftime('%m-%d')

	xMajorFormatter= FuncFormatter(x_major_formatter_2)
	xMinorFormatter= FuncFormatter(x_minor_formatter_2)

	# 第一只:设定 X 轴的 Locator 和 Formatter
	xaxis_2.set_major_locator(xMajorLocator)
	xaxis_2.set_major_formatter(xMajorFormatter)

	xaxis_2.set_minor_locator(xMinorLocator)
	xaxis_2.set_minor_formatter(xMinorFormatter)

	# 第一只:设定 X 轴主要坐标点与辅助坐标点的样式
	for malabel in axes_2.get_xticklabels(minor=False):
		malabel.set_fontsize(4)
		malabel.set_horizontalalignment('right')
		malabel.set_rotation('45')

	for milabel in axes_2.get_xticklabels(minor=True):
		milabel.set_fontsize(4)
		milabel.set_color('blue')
		milabel.set_horizontalalignment('right')
		milabel.set_rotation('45')



	#	第一只:设定成交量 Y 轴坐标的范围 
	#==================================================================================================================================================
	maxvol= max(volume)	# 注意是 int 类型
	axes_2.set_ylim(0, maxvol)



	#	第一只:设定成交量 Y 轴上的坐标
	#==================================================================================================================================================
	vollen= len(str(maxvol))
	
	volstep_pri= int(round(maxvol/10.0+5000, -4))
	
	yMajorLocator_2= MultipleLocator(volstep_pri)



	# 第一只:确定 Y 轴的 MajorFormatter
	dimsuffix= u'元' if u'成交额' in pdata else u'股'
	def y_major_formatter_2(num, pos=None):
		if num >= 10**8:	# 大于 1 亿
			return (str(round(num/10.0**8, 2)) + u'亿' + dimsuffix) if num != 0 else '0'
		else:
			return (str(num/10.0**4) + u'万' + dimsuffix) if num != 0 else '0'

	#	def y_major_formatter_2(num, pos=None):
	#		return int(num)
	yMajorFormatter_2= FuncFormatter(y_major_formatter_2)

	# 确定 Y 轴的 MinorFormatter
	#	def y_minor_formatter_2(num, pos=None):
	#		return int(num)
	#	yMinorFormatter_2= FuncFormatter(y_minor_formatter_2)
	yMinorFormatter_2= NullFormatter()

	# 第一只:设定 X 轴的 Locator 和 Formatter
	yaxis_2.set_major_locator(yMajorLocator_2)
	yaxis_2.set_major_formatter(yMajorFormatter_2)

	#	yaxis_2.set_minor_locator(yMinorLocator_2)
	yaxis_2.set_minor_formatter(yMinorFormatter_2)

	# 第一只:设定 Y 轴主要坐标点与辅助坐标点的样式
	for malab in axes_2.get_yticklabels(minor=False):
		malab.set_font_properties(__font_properties__)
		malab.set_fontsize(4.5)	# 这个必须放在前一句后面,否则作用会被覆盖
	


	#	第一只:成交量数值在图中间的显示
	#==================================================================================================================================================
	for iy in range(volstep_pri, maxvol, volstep_pri):
		for ix in mdindex[1:-1:3]:
			newlab= axes_2.text(ix+8, iy, y_major_formatter_2(iy))
			newlab.set_font_properties(__font_properties__)
			newlab.set_color('0.3')
			newlab.set_fontsize(3)
			newlab.set_zorder(0)		# XXX: 放在底层
			#	newlab.set_verticalalignment('center')



	#==================================================================================================================================================
	#==================================================================================================================================================
	#=======	
	#=======	XXX: 第二条成交量图线
	#=======	
	#==================================================================================================================================================
	#==================================================================================================================================================

	#	添加 Axes 对象
	#==================================================================================================================================================
	axes_2_sec= axes_2.twinx()
	#	axes_2_sec.set_axisbelow(True)	# 网格线放在底层
	
	axes_2_sec.set_axisbelow(True)	# 网格线放在底层

	#	改变坐标线的颜色
	#==================================================================================================================================================
	#	for child in axes_2_sec.get_children():
	#		if isinstance(child, matplotlib.spines.Spine):
	#			child.set_color('lightblue')

	#	得到 X 轴 和 Y 轴 的两个 Axis 对象
	#==================================================================================================================================================
	xaxis_2_sec= axes_2_sec.get_xaxis()
	yaxis_2_sec= axes_2_sec.get_yaxis()

	#	设置两个坐标轴上的 grid
	#==================================================================================================================================================
	#	xaxis_2_sec.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2)
	#	xaxis_2_sec.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1)

	#	yaxis_2_sec.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2)
	#	yaxis_2_sec.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1)



	#==================================================================================================================================================
	#=======	绘图
	#==================================================================================================================================================

	if u'成交额二' in pdata:
		volume_sec= pdata[u'成交额二']
	else:
		volume_sec= pdata[u'成交量二']

	zipoc_sec= zip(pdata[u'开盘二'], pdata[u'收盘二'])
	up_sec=   numpy.array( [ True if po < pc and po != None else False for po, pc in zipoc_sec] )		# 标示出该天股价日内上涨的一个序列
	down_sec= numpy.array( [ True if po > pc and po != None else False for po, pc in zipoc_sec] )		# 标示出该天股价日内下跌的一个序列
	side_sec= numpy.array( [ True if po == pc and po != None else False for po, pc in zipoc_sec] )		# 标示出该天股价日内走平的一个序列
	
	rarray_vol_sec= numpy.array(volume_sec)
	volzeros_sec= numpy.zeros(length)	# 辅助数据

	# XXX: 如果 up_sec/down_sec/side_sec 各项全部为 False,那么 vlines() 会报错。
	if True in up_sec:
		axes_2_sec.vlines(xindex[up_sec], volzeros_sec[up_sec], rarray_vol_sec[up_sec], edgecolor='pink', linewidth=1.0, label='_nolegend_', alpha=0.3)
	if True in down_sec:
		axes_2_sec.vlines(xindex[down_sec], volzeros_sec[down_sec], rarray_vol_sec[down_sec], edgecolor='lightgreen', linewidth=1.0, label='_nolegend_', alpha=0.3)
	if True in side_sec:
		axes_2_sec.vlines(xindex[side_sec], volzeros_sec[side_sec], rarray_vol_sec[side_sec], edgecolor='0.7', linewidth=1.0, label='_nolegend_', alpha=0.3)
	


	#	设定 X 轴坐标的范围 
	#==================================================================================================================================================
	#	XXX: 不用了,与 axes_2 共用。


	#	设定 Y 轴坐标的范围 
	#==================================================================================================================================================
	maxvol_sec= max(volume_sec)	# 注意是 int 类型
	axes_2_sec.set_ylim(0, maxvol_sec)



	#	设定 Y 轴上的坐标
	#==================================================================================================================================================
	
	volstep_sec= volstep_pri*maxvol_sec/float(maxvol)
	yMajorLocator_2_sec= MultipleLocator(volstep_sec)

	# 确定 Y 轴的 MajorFormatter
	dimsuffix_sec= u'元' if u'成交额二' in pdata else u'股'
	def y_major_formatter_2_sec(num, pos=None):
		if num >= 10**8:	# 大于 1 亿
			print(('num= ' + str(num) + ', result= ' + str(round(num/10.0**8, 3)) + u'亿' + dimsuffix_sec).encode('utf8'))
			
			return (str(round(num/10.0**8, 3)) + u'亿' + dimsuffix_sec) if num != 0 else '0'
		else:
			return (str(round(num/10.0**4, 2)) + u'万' + dimsuffix_sec) if num != 0 else '0'

	#	def y_major_formatter_2_sec(num, pos=None):
	#		return int(num)
	yMajorFormatter_2_sec= FuncFormatter(y_major_formatter_2_sec)

	# 确定 Y 轴的 MinorFormatter
	#	def y_minor_formatter_2(num, pos=None):
	#		return int(num)
	#	yMinorFormatter_2_sec= FuncFormatter(y_minor_formatter_2)
	yMinorFormatter_2_sec= NullFormatter()

	# 设定 X 轴的 Locator 和 Formatter
	yaxis_2_sec.set_major_locator(yMajorLocator_2_sec)
	yaxis_2_sec.set_major_formatter(yMajorFormatter_2_sec)

	#	yaxis_2_sec.set_minor_locator(yMinorLocator_2_sec)
	yaxis_2_sec.set_minor_formatter(yMinorFormatter_2_sec)

	# 设定 Y 轴主要坐标点与辅助坐标点的样式
	for malab in axes_2_sec.get_yticklabels(minor=False):
		malab.set_font_properties(__font_properties__)
		malab.set_fontsize(4.5)	# 这个必须放在前一句后面,否则作用会被覆盖
	




	#==================================================================================================================================================
	#==================================================================================================================================================
	#=======	
	#=======	XXX: K 线图部分
	#=======	
	#==================================================================================================================================================
	#==================================================================================================================================================

	#	添加 Axes 对象
	#==================================================================================================================================================
	axes_1= figobj.add_axes(rect_1, axis_bgcolor='black', sharex=axes_2)
	axes_1.set_axisbelow(True)	# 网格线放在底层
	
	axes_1.set_yscale('log', basey=expbase)		# 使用对数坐标
	
	#	改变坐标线的颜色
	#==================================================================================================================================================
	for child in axes_1.get_children():
		if isinstance(child, matplotlib.spines.Spine):
			child.set_color('lightblue')

	#	得到 X 轴 和 Y 轴 的两个 Axis 对象
	#==================================================================================================================================================
	xaxis_1= axes_1.get_xaxis()
	yaxis_1= axes_1.get_yaxis()

	#	设置两个坐标轴上的 grid
	#==================================================================================================================================================
	xaxis_1.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2)
	xaxis_1.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1)

	yaxis_1.grid(True, 'major', color='0.3', linestyle='solid', linewidth=0.2)
	yaxis_1.grid(True, 'minor', color='0.3', linestyle='dotted', linewidth=0.1)



	#==================================================================================================================================================
	#=======	绘图
	#==================================================================================================================================================

	#	绘制 K 线部分
	#==================================================================================================================================================
	
	#	对开收盘价进行视觉修正
	for idx, poc in enumerate( zip(pdata[u'开盘'], pdata[u'收盘']) ):
		if poc[0] == poc[1] and None not in poc:
			variant= round((poc[1]+1000)/2000, 0)
			pdata[u'开盘'][idx]= poc[0] - variant		# 稍微偏离一点,使得在图线上不致于完全看不到
			pdata[u'收盘'][idx]= poc[1] + variant

	rarray_open= numpy.array(pdata[u'开盘'])
	rarray_close= numpy.array(pdata[u'收盘'])
	rarray_high= numpy.array(pdata[u'最高'])
	rarray_low= numpy.array(pdata[u'最低'])

	# XXX: 如果 up, down, side 里有一个全部为 False 组成,那么 vlines() 会报错。
	# XXX: 可以使用 alpha 参数调节透明度
	if True in up:
		axes_1.vlines(xindex[up], rarray_low[up], rarray_high[up], edgecolor='red', linewidth=0.6, label='_nolegend_')		
		axes_1.vlines(xindex[up], rarray_open[up], rarray_close[up], edgecolor='red', linewidth=3.0, label='_nolegend_')

	if True in down:
		axes_1.vlines(xindex[down], rarray_low[down], rarray_high[down], edgecolor='green', linewidth=0.6, label='_nolegend_')
		axes_1.vlines(xindex[down], rarray_open[down], rarray_close[down], edgecolor='green', linewidth=3.0, label='_nolegend_')

	if True in side:
		axes_1.vlines(xindex[side], rarray_low[side], rarray_high[side], edgecolor='0.7', linewidth=0.6, label='_nolegend_')
		axes_1.vlines(xindex[side], rarray_open[side], rarray_close[side], edgecolor='0.7', linewidth=3.0, label='_nolegend_')

	#	绘制均线部分
	#==================================================================================================================================================
	if u'5日均' in pdata:
		rarray_5dayave= numpy.array(pdata[u'5日均'])
		axes_1.plot(xindex, rarray_5dayave, 'o-', color='white', linewidth=0.1, label='ave_5', \
			markersize=0.7, markeredgecolor='white', markeredgewidth=0.1)	# 5日均线
	
	if u'10日均' in pdata:
		rarray_10dayave= numpy.array(pdata[u'10日均'])
		axes_1.plot(xindex, rarray_10dayave, 'o-', color='yellow', linewidth=0.1, label='ave_10', \
			markersize=0.7, markeredgecolor='yellow', markeredgewidth=0.1)	# 10日均线
	
	if u'30日均' in pdata:
		rarray_30dayave= numpy.array(pdata[u'30日均'])
		axes_1.plot(xindex, rarray_30dayave, 'o-', color='cyan', linewidth=0.1, label='ave_30', \
			markersize=0.7, markeredgecolor='cyan', markeredgewidth=0.1)	# 30日均线



	#	绘制 复权提示
	#==================================================================================================================================================
	if u'复权' in pdata:
		adjdict= dict(pdata[u'复权'])
		
		for idx, dstr in enumerate(pdata[u'日期']):
			if dstr in adjdict:
				axes_1.plot([idx, idx], [ylowlim_price, yhighlim_price], '-', color='purple', linewidth=0.3)
	




	#	设定 X 轴坐标的范围 
	#==================================================================================================================================================
	axes_1.set_xlim(-1, length)



	#	先设置 label 位置,再将 X 轴上的坐标设为不可见。因为与 成交量子图 共用 X 轴
	#==================================================================================================================================================

	# 设定 X 轴的 Locator 和 Formatter
	xaxis_1.set_major_locator(xMajorLocator)
	xaxis_1.set_major_formatter(xMajorFormatter)

	xaxis_1.set_minor_locator(xMinorLocator)
	xaxis_1.set_minor_formatter(xMinorFormatter)

	# 将 X 轴上的坐标设为不可见。
	for malab in axes_1.get_xticklabels(minor=False):
		malab.set_visible(False)

	for milab in axes_1.get_xticklabels(minor=True):
		milab.set_visible(False)

	# 用这一段效果也一样
	#	pyplot.setp(axes_1.get_xticklabels(minor=False), visible=False)
	#	pyplot.setp(axes_1.get_xticklabels(minor=True), visible=False)



	#	设定 Y 轴坐标的范围 
	#==================================================================================================================================================
	axes_1.set_ylim(ylowlim_price, yhighlim_price)



	#	设定 Y 轴上的坐标
	#==================================================================================================================================================
	
	# XXX: 不用 LogLocator 了,因为不能控制坐标点的位置。

	#	主要坐标点
	#----------------------------------------------------------------------------
	yticks_major_pri= []
	for i in range(1, 999):
		newloc= ylowlim_price * (expbase**i)
		if newloc <= yhighlim_price:
			yticks_major_pri.append(newloc)
		else:
			break

	yMajorLocator_1= FixedLocator(numpy.array(yticks_major_pri))

	# 确定 Y 轴的 MajorFormatter
	def y_major_formatter_1(num, pos=None):
		return str(round(num/1000.0, 2))
	
	yMajorFormatter_1= FuncFormatter(y_major_formatter_1)

	# 设定 X 轴的 Locator 和 Formatter
	yaxis_1.set_major_locator(yMajorLocator_1)
	yaxis_1.set_major_formatter(yMajorFormatter_1)

	# 设定 Y 轴主要坐标点与辅助坐标点的样式
	for mal in axes_1.get_yticklabels(minor=False):
		mal.set_fontsize(6)
	
	

	#	辅助坐标点
	#----------------------------------------------------------------------------
	yticks_minor_pri= []
	mtstart= ylowlim_price * (1.0+(expbase-1.0)/2)
	for i in range(999):
		newloc= mtstart * (expbase**i)
		if newloc <= yhighlim_price:
			yticks_minor_pri.append(newloc)
		else:
			break

	yMinorLocator_1= FixedLocator(numpy.array(yticks_minor_pri))		# XXX minor ticks 已经在上面一并设置,这里不需要了。

	# 确定 Y 轴的 MinorFormatter
	def y_minor_formatter_1(num, pos=None):
		return str(round(num/1000.0, 2))
		
	yMinorFormatter_1= FuncFormatter(y_minor_formatter_1)
	
	# 设定 X 轴的 Locator 和 Formatter
	yaxis_1.set_minor_locator(yMinorLocator_1)
	yaxis_1.set_minor_formatter(yMinorFormatter_1)
	# 设定 Y 轴主要坐标点与辅助坐标点的样式
	for mal in axes_1.get_yticklabels(minor=True):
		mal.set_fontsize(5)
		mal.set_color('blue')



	#	第一只:价格数值在图中间的显示
	#==================================================================================================================================================
	for iy in yticks_major_pri:
		for ix in mdindex[1:-1:3]:
			newlab= axes_1.text(ix+8, iy*1.001, y_major_formatter_1(iy))
			newlab.set_font_properties(__font_properties__)
			newlab.set_color('0.3')
			newlab.set_fontsize(3)
			newlab.set_zorder(0)		# XXX: 放在底层
			#	newlab.set_verticalalignment('center')



	#	第一只:日期在图中间的显示
	#==================================================================================================================================================
	for iy in yticks_minor_pri[1:-1:5]:
		for ix in mdindex:
			newlab= axes_1.text(ix-1, iy, pdata[u'日期'][ix])
			newlab.set_font_properties(__font_properties__)
			newlab.set_color('0.3')
			newlab.set_fontsize(4)
			newlab.set_rotation('vertical')
			#	newlab.set_horizontalalignment('left')
			#	newlab.set_verticalalignment('bottom')
			newlab.set_zorder(0)		# XXX: 放在底层
			#	newlab.set_verticalalignment('center')



	#==================================================================================================================================================
	#==================================================================================================================================================
	#=======	
	#=======	XXX: 第二条 K 线图
	#=======	
	#==================================================================================================================================================
	#==================================================================================================================================================

	#	添加 Axes 对象
	#==================================================================================================================================================
	axes_1_sec= axes_1.twinx()
	#	axes_1_sec.set_axisbelow(True)	# 网格线放在底层
	
	axes_1_sec.set_yscale('log', basey=expbase)	# 使用对数坐标


	#	得到 X 轴 和 Y 轴 的两个 Axis 对象
	#==================================================================================================================================================
	xaxis_1_sec= axes_1_sec.get_xaxis()
	yaxis_1_sec= axes_1_sec.get_yaxis()



	#==================================================================================================================================================
	#=======	绘图
	#==================================================================================================================================================

	#	绘制 K 线部分
	#==================================================================================================================================================
	
	#	对开收盘价进行视觉修正
	for idx, poc in enumerate( zipoc_sec ):
		if poc[0] == poc[1] and None not in poc:
			pdata[u'开盘二'][idx]= poc[0] - 5		# 稍微偏离一点,使得在图线上不致于完全看不到
			pdata[u'收盘二'][idx]= poc[1] + 5		
	
	rarray_open= numpy.array(pdata[u'开盘二'])
	rarray_close= numpy.array(pdata[u'收盘二'])
	rarray_high= numpy.array(pdata[u'最高二'])
	rarray_low= numpy.array(pdata[u'最低二'])

	# XXX: 如果 up_sec, down_sec, side_sec 里有一个全部为 False 组成,那么 vlines() 会报错。
	# XXX: 可以使用 alpha 参数调节透明度
	if True in up_sec:
		axes_1_sec.vlines(xindex[up_sec], rarray_low[up_sec], rarray_high[up_sec], edgecolor='red', linewidth=0.6, label='_nolegend_', alpha=0.3)
		axes_1_sec.vlines(xindex[up_sec], rarray_open[up_sec], rarray_close[up_sec], edgecolor='red', linewidth=3.0, label='_nolegend_', alpha=0.3)

	if True in down_sec:
		axes_1_sec.vlines(xindex[down_sec], rarray_low[down_sec], rarray_high[down_sec], edgecolor='green', linewidth=0.6, label='_nolegend_', alpha=0.3)
		axes_1_sec.vlines(xindex[down_sec], rarray_open[down_sec], rarray_close[down_sec], edgecolor='green', linewidth=3.0, label='_nolegend_', alpha=0.3)

	if True in side_sec:
		axes_1_sec.vlines(xindex[side_sec], rarray_low[side_sec], rarray_high[side_sec], edgecolor='0.7', linewidth=0.6, label='_nolegend_', alpha=0.3)
		axes_1_sec.vlines(xindex[side_sec], rarray_open[side_sec], rarray_close[side_sec], edgecolor='0.7', linewidth=3.0, label='_nolegend_', alpha=0.3)



	#	设定 X 轴坐标的范围 
	#==================================================================================================================================================
	axes_1_sec.set_xlim(-1, length)



	#	先设置 label 位置,再将 X 轴上的坐标设为不可见。因为与 成交量子图 共用 X 轴
	#==================================================================================================================================================

	# 设定 X 轴的 Locator 和 Formatter
	xaxis_1_sec.set_major_locator(xMajorLocator)
	xaxis_1_sec.set_major_formatter(xMajorFormatter)

	xaxis_1_sec.set_minor_locator(xMinorLocator)
	xaxis_1_sec.set_minor_formatter(xMinorFormatter)

	# 将 X 轴上的坐标设为不可见。
	for malab in axes_1_sec.get_xticklabels(minor=False):
		malab.set_visible(False)

	for milab in axes_1_sec.get_xticklabels(minor=True):
		milab.set_visible(False)



	#	设定 Y 轴坐标的范围 
	#==================================================================================================================================================
	axes_1_sec.set_ylim(ylowlim_price*open_price_sec/open_price_pri, yhighlim_price*open_price_sec/open_price_pri)



	#	设定 Y 轴上的坐标
	#==================================================================================================================================================
	
	#	主要坐标点
	#----------------------------------------------------------------------------
	yticks_major_sec= []
	ylowlim_price_sec=  ylowlim_price*open_price_sec/open_price_pri
	yhighlim_price_sec= yhighlim_price*open_price_sec/open_price_pri
	
	for i in range(1, 999):
		newloc= ylowlim_price_sec * (expbase**i)
		if newloc <= yhighlim_price_sec:
			yticks_major_sec.append(newloc)
		else:
			break

	yMajorLocator_1_sec= FixedLocator(numpy.array(yticks_major_sec))

	# 确定 Y 轴的 MajorFormatter
	def y_major_formatter_1_sec(num, pos=None):
		return str(round(num/1000.0, 2))
	
	yMajorFormatter_1_sec= FuncFormatter(y_major_formatter_1_sec)

	# 设定 X 轴的 Locator 和 Formatter
	yaxis_1_sec.set_major_locator(yMajorLocator_1_sec)
	yaxis_1_sec.set_major_formatter(yMajorFormatter_1_sec)

	# 设定 Y 轴主要坐标点与辅助坐标点的样式
	for mal in axes_1_sec.get_yticklabels(minor=False):
		mal.set_fontsize(6)
	
	

	#	辅助坐标点
	#----------------------------------------------------------------------------
	yticks_minor_sec= []
	mtstart_sec= ylowlim_price_sec * (1.0+(expbase-1.0)/2)
	for i in range(999):
		newloc= mtstart_sec * (expbase**i)
		if newloc <= yhighlim_price_sec:
			yticks_minor_sec.append(newloc)
		else:
			break

	yMinorLocator_1_sec= FixedLocator(numpy.array(yticks_minor_sec))		# XXX minor ticks 已经在上面一并设置,这里不需要了。

	# 确定 Y 轴的 MinorFormatter
	def y_minor_formatter_1_sec(num, pos=None):
		return str(round(num/1000.0, 2))
		
	yMinorFormatter_1_sec= FuncFormatter(y_minor_formatter_1_sec)
	
	# 设定 X 轴的 Locator 和 Formatter
	yaxis_1_sec.set_minor_locator(yMinorLocator_1_sec)
	yaxis_1_sec.set_minor_formatter(yMinorFormatter_1_sec)
	# 设定 Y 轴主要坐标点与辅助坐标点的样式
	for mal in axes_1_sec.get_yticklabels(minor=True):
		mal.set_fontsize(5)
		mal.set_color('blue')



	#	显示图片
	#==================================================================================================================================================
	#	pyplot.show()

	#	保存图片
	#==================================================================================================================================================
	figobj.savefig(figpath, dpi=figdpi, facecolor=figfacecolor, edgecolor=figedgecolor, linewidth=figlinewidth)



if __name__ == '__main__':
	Plot(pfile=sys.argv[1], figpath=sys.argv[2])







 




Host by is-Programmer.com | Power by Chito 1.3.3 beta | © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee