Pythonista说起 Pythonista,认识的人可能并不多,但是如果说 Python,相信大家就不太陌生了。同为 iOS 的明星「玩具」,Pythonista 不免要和 Workflow 相提并论。其实这样比较并不恰当,Pythonista 更像是 iOS 上的 Python IDE,而 Workflow 更多是自动化方向的,所以两者面向的问题有交集但不等同。站在一般用户追求效率的角度来看,Pyhonista 真不能说提高效率,因为我认为:前期投资时间远多于使用时间的话,这件事本身就很不效率。当然这是从自己手把手完成自己需求的角度而言的,所以 Workflow 相比之下投资的时间更少且更白菜,而使用 Pythonista,你至少需要有学习 Python 的决心。然而,Pythonista 有其不可替代的作用,能够让你更进一步地满足 iOS 上的小需求。

分析

以我在一般用户的角度评价 Pythonista 的作用,我并不需要它能实现图像处理、数学处理、数据分析等高级功能,而是它能否让我满足我在使用 iOS 时其他应用无法满足的需求,就像 Workflow 的意义对我来说并不是它能实现什么独立应用也能实现的功能,而是它能够满足用户自己使用上的需求一样。而我认为,Pythonista 正好也做到了,而且可满足的范围更广。它之所以能够帮助一般用户满足自己的小需求,我认为在于其几方面的优势:

  • Python 上手快,作为完整的一门语言,初等需求的入门相较之下真的要容易许多;
  • Pythonista 连接了 iOS 的一些常用功能,如clipboardphotos等模块,让它不单单是 Python 的一个独立工具;
  • Pythonista 的objc_util模块能实现更多 iOS 上的奇思妙想,这是我认为它能够满足我小需求的不可取代的重要因素之一;
  • Pythonista 可以自定义 UI 和 Widget,这是我认为它能够满足我小需求的不可取代的又一重要因素。

所以我为什么要使用 Pythonista 完成某项任务而不是 Workflow,就是因为后者满足不了的需求前者可以满足,或者可能可以满足(对于 Python 新手的我来说,我的认识只是其冰山一角,不能满足更多是自己能力的问题)。虽然 Pythonista 的运行效率要高于 Workflow,但正如引文所说,我认为把所有任务都搬到 Pythonista 是不可取的、不效率的,除非对于某项任务的需求真的很频繁,否则投资的时间完全没有必要。

objc_util 模块

当我认识到这一模块的作用后,我只恨自己对 ObjC 一窍不通。使用这一模块可以帮助我们极大范围地连通 iOS 环境,就像自己在独立开发应用一般,可以从 iOS 提供的 API 中实现种种奇思妙想(能力足够的话),而不再局限于 Pythonista 提供的关于 iOS 的模块。比如我看到的一些例子,如唤起 Touch ID、接入 Apple Music、调用 Microphone 等。Pythonista-1

我在自己的能力范围内也实现了两个需求(见文末附件):删除用户相册查看 URL Schemes

  1. 删除用户相册。实现这个功能我是真的高兴地快要跳起来了,可见我对这「用户相册」有多痛恨了吧。经常的,我们从一些应用保存照片时,自然而然就会生成相应的相册,而删除这些相册现在真的方便多了;Pythonista-2
  2. 查看 URL Schemes(iOS 11 已失效)。还记得钟大的Retriver吗,它能够帮助我们在手机上快速查看应用的 URL Schemes,但其缺点是需要定期重新签名,而现在查看 URL Schemes 可以真正摆脱电脑了。Pythonista-3

UI 和 Widget

Pythonista 可以自己制作 UI 和 Widget。虽然 Pythonista 也有实用的 Console,但是定制 UI 才真正让运行一个脚本看起来像是一个功能完整的应用。而 Widget 对我而言更有作用,我喜欢把一些简单的功能放在 Widget 上,如前所说的删除用户相册。为此我还自行做了一个简单的类 Launcher,Widget 的空间又节省了一个。但是 Pythonista 的 Widget 也有许多局限,它占用的内存感觉比 Workflow 要多得多,如果你也想要制作一个专属的 Widget,建议其功能简单一点、少一点,引入的模块也不要太多。

youtube-dl

youtube-dl 和 you-get 相信大家都很熟悉,它们都是 Pure Python,自然也能在 Pythonista 上运行。我一直认为 Youtube 这样的大视频不适合在 Workflow 运行,因为它并不适合缓存文件,而现在除了使用独立应用以外,还可以考虑利用此脚本(见文末附件)。Pythonista-4

使用的方法很简单,把youtube_dl的文件夹导入到 Pythonista 的Modules&Templates/site-packages,在需要的时候引用即可。

脚本导入

使用 Pythonista 的你会发现,为避开 iOS 敏感问题,Pythonista 唯独.py脚本不能从 Share Sheet 导入。那么这里也说说两种可行的导入方式:

  1. 首选使用Get Script.py。把此脚本1代码复制(见文末附件)并创建后,添加到 Share Extension Shortcuts 即可。此后打开.py文件后再唤起 Share Extension 运行此脚本即可;
  2. 其次自然是低效的复制代码并创建脚本。这在导入Get Script.py时就只能使用这种方式了。这里直接提供Get Script.py的代码,点击右上角按钮可快捷复制。

    #!/usr/bin/env python2
    # coding: utf-8
    # Olaf, Dec 2015, Pythonista 1.6 beta
    
    '''
    Appex GetFromURL
    Pythonista app extension for use on share sheet of other apps to import file from URL into Pythonista
    The file is saved at HOME/DESTINATION without user interaction (no 'save as' dialog) unless duplicate
    '''
    
    from __future__ import print_function
    try:
        import appex, console, contextlib, itertools, os, os.path, sys, time, urllib, urlparse
    except ImportError:
        assert False, 'This script needs the appex module in Pythonista version > 1.5'
    
    HOME, DESTINATION = 'Documents', '' # you can change DESTINATION to any name of your liking
    
    @contextlib.contextmanager
    def callfunctionafterwardswithsec(function):
        '''Context-manager that calls function with duration of with block (sec) after termination
        >>> def pr_sec(sec): print('Duration {:3.2} sec'.format(sec))
        >>> with callfunctionafterwardswithsec(pr_sec): pass
        Duration 0.0 sec'''
    
        start = time.clock()
        yield
        end = time.clock()
        function(end - start)
    
    def left_subpath_upto(path, sentinel):
        '''Left part (subpath) of path upto and including sentinel
        >>> print(left_subpath_upto('a/b/c', 'b'))
        a/b'''
    
        while path:
            head, tail = os.path.split(path)
            if tail == sentinel:
                break
            path = head
        return path
    
    def iter_pad(length, arg0, *args):
        '''Iterator to pad arguments (at least 1) to specified length by repetition of final argument
        >>> print(''.join(iter_pad(3, 'a', 'b')))
        abb'''
    
        args = (arg0,) + args
        return itertools.islice(itertools.chain(args, itertools.repeat(args[-1])), length)
    
    def parse_into_paths(input_url, HOME=HOME, DESTINATION=DESTINATION):
        '''Parse input URL into paths tuple for further processing
        >>> parse_into_paths('http://test.org/x.py', DESTINATION='TEST') # doctest: +ELLIPSIS
        ('x.py', 'http://test.org', 'Documents/TEST', '/private/var/.../TEST', '/priv.../TEST/x.py', True)'''
    
        url_tuple = urlparse.urlparse(input_url)
        scheme, netloc, basename = url_tuple.scheme, url_tuple.netloc, os.path.basename(url_tuple.path)
        input_short = urlparse.urlunparse(iter_pad(len(url_tuple), scheme, netloc, ''))
        output_short = os.path.join(HOME, DESTINATION)
        output_dir = os.path.join(left_subpath_upto(sys.argv[0], HOME), DESTINATION)
        output_path = os.path.join(output_dir, basename)
        is_Python = os.path.splitext(basename)[1].lower() == '.py'
        return basename, input_short, output_short, output_dir, output_path, is_Python
    
    def copy_url(input_url):
        '''Write a copy of the file at input_url to HOME/DESTINATION
        if the destination directory doesn't exist, it is created
        if the destination file already exists, the user can cancel or overwrite
        if it is a Python file, a comment line is added to log the origin'''
    
        basename, input_short, output_short, output_dir, output_path, is_Python = parse_into_paths(input_url)
    
        if not os.path.exists(output_dir):
            os.mkdir(output_dir)
            console.hud_alert('Created destination directory {}'.format(output_short))
        if os.path.exists(output_path):
            try:
                console.alert('Duplicate file',
                                            '{} already exists in {}'.format(basename, output_short),
                                            'Overwrite') # or Cancel
            except KeyboardInterrupt:
                return
    
        with contextlib.closing(urllib.urlopen(input_url)) as input:
            data = input.read()
            console.hud_alert('Got {} ({} chars) from {}'.format(basename, len(data), input_short))
        with open(output_path, 'wb') as output:
            output.write(data)
            console.hud_alert('Wrote {} to {}'.format(basename, output_short))
    
    def main():
        '''App extension logic, with unit tests if run within Pythonista'''
    
        if appex.is_running_extension():
            if appex.get_url():
                copy_url(appex.get_url())
                appex.finish()
            else:
                console.hud_alert('No input URL found', 'error')
        else:
            console.hud_alert('This script must be run from the sharing extension', 'error')
            import doctest
            doctest.testmod()
    
    if __name__ == '__main__':
        main()

总结

一款应用是否适合自己,与它是否收费或是否大众无关,而跟自己是否需要有关。Pythonista 比较适合有自己想要实现的功能的用户,特别是与 iOS 环境连通的功能。所以还请各位先自行思考自己的需求,再考虑入手,切勿盲从而不能善用,不论什么应用。

附件

Python: Delete Albums

Python: URL Schemes

Python: Youtube-dl

Python: Get Script


  1. 出自:Appex GetFromURL

如有问题,欢迎留言或邮件咨询