先贴上链接吧:https://github.com/frostnotfall/DoubanMovieTool

恩,这是一个关于电影的bot。基于豆瓣的数据实现。

过年一堆琐碎闲事,愣是拖到了今天才再次更一下博客。

最初的版本功能只有一个,根据电影的影评生成词云图片;半年后在此基础上添加了一些其他的功能,使之成为了一个便捷的电影工具。

基于Python实现,提供目前有“正在上映”、“即将上映”、“新片榜”‘“快捷搜索”等功能,还有一个基于旧接口实现的搜索(不太准,以后可能遗弃),通过CustomKeyboardButton、InlineKeyboardButton,InlineQuery,Instant View 方式呈现输出。

逻辑代码基本通过爬虫实现获取数据,少数数据通过接口获取。比较头疼的是这个框架并没有详尽的的文档,只有最简单的功能示例。碰到问题磕磕碰碰,最终还是完成了。

比较遗憾的是工具本身基于聊天平台,该聊天工具的数据呈现方式只有单一的text,image,vedio等,并不支持两种以上的复合呈现(尽管如此,还是比微信要好用多了)。所以尽可能通过CustomKeyboardButton、InlineKeyboardButton,InlineQuery呈现输出,避免交互方式单调,尽管如此,还是有些掣肘。

比如,api 提供图片+文字的数据发送方式,并且文字支持markdown或html解析,但是只支持最基本的粗体、斜体、url插入等方式,甚至不支持字号的设置。

目前已添加了 Instant View,电影详情以及影人详情经由 Instant View 输出,规避了之前的问题,数据呈现方式比原先好了不少。

开始的几个版本中,电影、演员的搜索方式通过返回文本+字符串截取的方式来实现,想来想去还是不太方便了,于是开始加入“快捷搜索”功能,(效果图见下方)通过InlineQuery返回结果,所见所得(之前的搜索功能依然保留,后期可能会弃之不用了)。

其他效果图就不贴了。

目前还想加入一项 top250 的功能,但是以当前使用的交互方式并不合适,所以可能会添加 Instant View 来改善一下数据的呈现方式。除此之外,目前想不到其它可添加的功能了。

2.17更新:关于 top250 的功能,目前考虑的方式是使用内联键盘更新按钮的方式。目前待定吧。

2.25更新:已添加。 采用列表分页, 通过内联键盘更新。

关于性能优化:

目前按照 python-telegram-bot开发者的建议,使用了 run_async、 wekhook 和ujson 库,但问题并不在这里,而是网络延迟,目前服务器到豆瓣的 ping 值 有250+ms,单纯请求一次http就要花费1.5s的时间,与 telegram 服务器之间的延迟也有130ms左右,真正留给程序的时间并不多,虽然对获取评论这种需要多次url请求的地方做了异步,(开始是使用多线程,但考虑到单核CPU多线程的创建、切换、销毁的开销……so),但对单个的请求使用异步并不能带来速度的提升。

所以如何缩短这部分的时间一直在考虑中:

  • 加缓存或许是个不错的方案,但使用的人少,命中率也低。
  • 加CDN也行,但也只降低了程序到telegram服务器的延迟。与豆瓣服务器的延迟没有改观。毕竟域名不是自己的。
  • 走国内代理,但免费代理很少有国际线路优化的。

迅雷是越来越不能用了,卸了好多次又装,这次是终于下决心不再用了,年费超级会员就当白扔了,祝早亡。

作为替代的下载工具,qbittorrent 着实不错,代码开源,一般的做种功能就不说了。有 Web UI,可根据 Client IP 设定白名单免密码访问,觉得原版 Web UI 丑的话,甚至可以替换成自己的 Web UI,支持动态域名,提供 WebAPI,支持导入 TLS 证书,支持RSS订阅,下载完成后可以电子邮件通知,也可执行外部程序(这里我使用windows 局域网通知功能),另外Wiki 也很详细。作为 NAS 上的下载工具非常不错。

qbittorrent 支持导入外部 Tracker 功能,玩BT下载的都知道 Tracker 的重要性,但是 Tracker 地址的维护却一直是个问题,Github 上一直在维护的的 trackerslist 数目终归是太少。

想要提高下载速度,只能自己维护了。

于是自己写了个 Tracker 维护脚本,通过遍历目录下的 Torrent 文件,提取出 Tracker 地址,做连通性测试,记录有效的地址到 txt 文件中,然后去重。

代码不多,100来行,做了多线程优化,速度也还行。放在了 Github 上。

可惜的是 qbittorrent 本身的 API 不支持永久导入 Tracker 功能,只能手动,也不支持插件的方式来实现,目前只支持搜索功能的插件,当然因为开源的原因,可以理解。

当然也不是没办法,配置文件是明文,可以通过替换特定行的方式来实现,以后再说了。

日前看到一个zabbix自动发现tomcat的脚本,发现使用了subprocess模块,遂研究了一番

subprocess是一个用来创建并运行子进程的模块

subprocess.call()
父进程等待子进程完成
返回退出信息(returncode,相当于Linux exit code)

subprocess.check_call()
父进程等待子进程完成
返回0
检查退出信息,如果returncode不为0,则举出错误subprocess.CalledProcessError,该对象包含有returncode属性,可用try…except…来检查

subprocess.check_output()
父进程等待子进程完成
返回子进程向标准输出的输出结果
检查退出信息,如果returncode不为0,则举出错误subprocess.CalledProcessError,该对象包含有returncode属性和output属性,output属性为标准输出的输出结果,可用try…except…来检查。

这三个函数的使用方法相类似,下面来以subprocess.call()举例说明:


>>> import subprocess
>>> retcode = subprocess.call(["ls", "-l"])
#和shell中命令ls -a显示结果一样
>>> print retcode
0

将程序名(ls)和所带的参数(-l)一起放在一个表中传递给subprocess.call()

shell默认为False,在Linux下,shell=False时, Popen调用os.execvp()执行args指定的程序;shell=True时,如果args是字符串,Popen直接调用系统的Shell来执行args指定的程序,如果args是一个序列,则args的第一项是定义程序命令字符串,其它项是调用系统Shell时的附加参数。

上面例子也可以写成如下:

>>> retcode = subprocess.call("ls -l",shell=True)

在Windows下,不论shell的值如何,Popen调用CreateProcess()执行args指定的外部程序。如果args是一个序列,则先用list2cmdline()转化为字符串,但需要注意的是,并不是MS Windows下所有的程序都可以用list2cmdline来转化为命令行字符串。

subprocess.Popen()

class Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)

实际上,上面的几个函数都是基于Popen()的封装(wrapper)。这些封装的目的在于让我们容易使用子进程。当我们想要更个性化我们的需求的时候,就要转向Popen类,该类生成的对象用来代表子进程。
与上面的封装不同,Popen对象创建后,主程序不会自动等待子进程完成。我们必须调用对象的wait()方法,父进程才会等待 (也就是阻塞block),举例:

>>> import subprocess
>>> child = subprocess.Popen(['ping','-c','4','blog.linuxeye.com'])
>>> print 'parent process'

从运行结果中看到,父进程在开启子进程之后并没有等待child的完成,而是直接运行print。
对比等待的情况:

>>> import subprocess
>>> child = subprocess.Popen('ping -c4 blog.linuxeye.com',shell=True)
>>> child.wait()
>>> print 'parent process'

从运行结果中看到,父进程在开启子进程之后并等待child的完成后,再运行print。
此外,你还可以在父进程中对子进程进行其它操作,比如我们上面例子中的child对象:

child.poll() # 检查子进程状态
child.kill() # 终止子进程
child.send_signal() # 向子进程发送信号
child.terminate() # 终止子进程

子进程的PID存储在child.pid

二、子进程的文本流控制

子进程的标准输入、标准输出和标准错误如下属性分别表示:

child.stdin
child.stdout
child.stderr

可以在Popen()建立子进程的时候改变标准输入、标准输出和标准错误,并可以利用subprocess.PIPE将多个子进程的输入和输出连接在一起,构成管道(pipe),如下2个例子:

>>> import subprocess
>>> child1 = subprocess.Popen(["ls","-l"], stdout=subprocess.PIPE)
>>> print child1.stdout.read(),
#或者child1.communicate()
>>> import subprocess
>>> child1 = subprocess.Popen(["cat","/etc/passwd"], stdout=subprocess.PIPE)
>>> child2 = subprocess.Popen(["grep","0:0"],stdin=child1.stdout, stdout=subprocess.PIPE)
>>> out = child2.communicate()

subprocess.PIPE实际上为文本流提供一个缓存区。child1的stdout将文本输出到缓存区,随后child2的stdin从该PIPE中将文本读取走。child2的输出文本也被存放在PIPE中,直到communicate()方法从PIPE中读取出PIPE中的文本。
注意:communicate()是Popen对象的一个方法,该方法会阻塞父进程,直到子进程完成

其它Popen方法:

Popen.poll()

用于检查子进程是否已经结束。设置并返回returncode属性。

Popen.wait()

等待子进程结束。设置并返回returncode属性。

Popen.send_signal(signal)

向子进程发送信号。

Popen.terminate()

停止(stop)子进程。在windows平台下,该方法将调用Windows API TerminateProcess()来结束子进程。

Popen.kill()

杀死子进程。

Popen.stdin

如果在创建Popen对象是,参数stdin被设置为PIPE,Popen.stdin将返回一个文件对象用于策子进程发送指令。否则返回None。

Popen.stdout

如果在创建Popen对象是,参数stdout被设置为PIPE,Popen.stdout将返回一个文件对象用于策子进程发送指令。否则返回None。

Popen.stderr

如果在创建Popen对象是,参数stdout被设置为PIPE,Popen.stdout将返回一个文件对象用于策子进程发送指令。否则返回None。

Popen.pid

获取子进程的进程ID。

Popen.returncode

获取进程的返回值。如果进程还没有结束,返回None。