4.14 更新:
陆陆续续有朋友在评论区反馈说出现异常,看来确实是有朋友希望能直接用上。不过,朋友们,在业务流程还没梳理清楚的情况下,我的代码当然是不能直接使用,不然报错的可能性是很大的。为此,我在 AppleScript 的部分新增加了流程图 ,希望能把我的思路和设计表述清楚,具体代码实现反倒是不重要的。


0. 背景

换了 Macbook 一段时间,积累的文件越来越多,曾经能像闪电一样定位到文件的 Spotlight 开始力不从心。分析之后,发现了以下几个问题导致了:

  • 文件层级太多,检索的时候会蒙蔽,不知道要到哪一层级去找文件。而且层级多了也不利于 Spotlight,不同文件夹下的文件都是叫做需求文档v2.0.pdf
  • 文件版本太多,有时候同一份文档修修改改,根本找不到是哪个版本;也有时候一份文档在编辑的时候是 .graffle,交付的时候导出了 PDF 。这个工作流结束了之后,就产生了一个文件副本。

原来看似整洁但实际上难以检索的文件系统
原来看似整洁但实际上难以检索的文件系统

对个人工作需求进行分析后,得到以下几个解决思路:

文件整理

  • 文件修改时间外露:本质上是版本的管理
  • ‎文件所属文件夹外露:本质上是项目的管理
  • 打散文件夹结构,使用标签系统:本质上是让文件从单维度到多维度分类

文件搜索

  • 允许按照项目层层检索
  • 允许按照文件类型搜索
  • 允许按照关键词搜索
  • 允许按照工作流搜索(例如原型、需求文档、接口文档、设计稿...etc.)

1. 思路

从需求到落实,重新整理下思路,可以归类到三个环节的改造建立:

  1. 标签系统建立:依据文件类型、工作流程打标签,主要是为了有效搜索和检搜打基础
  2. 重命名系统建立:依据所属项目、文档修改时间重命名,关键在于可以让人一目了然地挑选出所需的文档
  3. 搜索系统改造:能比较灵活地搜索,例如针对标签、类型、关键词...

2. 标签系统建立

首先,打散文件夹层级,每个项目只有一个文件夹。粗暴地使用 Hazel 监控文件夹并直接打标签!

使用 Hazel 监控需要打标签的文件夹
使用 Hazel 监控需要打标签的文件夹

围绕 类型workflow,我这边列出的标签规则就是:

  • 类型是 numbers 或 xlsx 的,tag 表格
  • 类型是 PDF 的,tag PDF
  • 类型是 docx 或 pages 的,tag 文档
  • 类型是 keynote 或 rp 或 关键词带有「原型」的,tag 原型
  • 关键词带有「流程」的,tag 流程图
  • 关键词带有「设计稿」的,tag 设计稿
  • 关键词带有「接口」的,tag 接口文档
    Hazel 自带的规则就有打标签这个操作
    Hazel 自带的规则就有打标签这个操作

对,就是这么粗暴。

3. 重命名系统建立

摸索了手边的所有工具一圈,能满足我的工具是:没有。
唯一有可能的,是 AppleScript。而且根据经验,在 Mac 环境里,不同软件之间联动比较多,这种脚本还是使用 Workflow 封装一下,降低整个 workflow 的耦合性,比较好调试和复用。
因为我们的项目是会频繁增加的,也就是说起码「子文件夹」这个元素,是需要自动获取的,不然每个新项目,都要改下 workflow,会崩溃。

总结一下,我们分析一下业务场景:

  1. 必须要有一个不变的根目录,相当于工作的总目录,是程序要去监控的文件夹。这个文件夹以外的项目,不适用。
  2. 项目会不断增加,每个新项目都会存在一个新文件夹中。
  3. 有一些边界场景要处理:
  • 对于已有前缀的文件,很有可能是老文件,不能重复处理
  • 有些文件名,添加了前缀后,变成了一样的名称,在系统中就会引起异常,所以要抛个异常去接。

业务场景分析
业务场景分析

接下来,我们拆解下这个程序要如何设计。先把上述的业务场景转化为业务流程图。

根据业务流程图设计程序
根据业务流程图设计程序
1. 第一步,就使用 Automator 监控总文件夹,每次触发都把文件夹里面的文件列表以数组形式传入下一步。
考虑到维护性,使用 Automator 封装 Applescript
考虑到维护性,使用 Automator 封装 Applescript

  1. 主函数接收到上一步传入来的子文件列表,就开始对里面的每一个子文件夹进行遍历,调用 rename() 函数。
--重命名增加项目前缀,例如【projectName】file_original_name.extension
on run {input, parameters}
set folder_lists to input as list
repeat with i from 1 to number of items in folder_lists
set this_item to item i of folder_lists
rename(this_item)
end repeat
end run
--主要的函数本身,上方为 workflow 执行 applescript 时自带的函数
on rename(input)
tell application "System Events"
--函数里会传入input,把它设为项目文件夹「theFolder」,然后拼接【xx】,作为一个前缀
set theFolder to input as alias
set thePrefix to ("【" & name of theFolder as string) & "】"
end tell
--用于通知用的文案,可要可不要
set query to " "
--接下来要让Finder去执行任务了,把刚刚的项目文件夹里的文件,全部轮询一遍
tell application "Finder"
set all_files to every item of theFolder as list
repeat with i from 1 to number of items in all_files
set this_item to item i of all_files
--如果文件名称本身就带有这个【xx】前缀,那就跳过,大家就当冇事发生过
if thePrefix is not in name of this_item then
--如果文件夹名字内没有带前缀,那就把新名字拼接为「【xx】原名」
set new_name to thePrefix & name of this_item
try
set name of this_item to new_name as string
--做个容错性处理,如果遇到重复命名,就发个通知告诉我已存在就好了
on error the error_message number the error_number
set query_error to new_name & "已存在"
display notification query_error with title "重命名失败"
end try
--重命名成功后,把新名字记录一下,发个通知告诉我一声哪些改好了
set query to query & "
" & new_name
end if
end repeat
end tell
if query is not " " then
if query_error is "" then
display notification query with title thePrefix & "文件夹重命名已完成"
end if
end if
--最后,函数别忘了 return 和 end
return input
end rename

添加修改时间后缀的逻辑也差不多,一样是使用 workflow 封装一下。

--主函数和上述一致
on run {input}
set folder_lists to input as list
repeat with i from 1 to number of items in folder_lists
set this_item to item i of folder_lists
rename(this_item)
end repeat
end run

on rename(input)
tell application "System Events"
set theFolder to input as alias
end tell
set query to " "
set dateMark to "[t-"
tell application "Finder"
set all_files to every item of theFolder as list
end tell
repeat with i from 1 to number of items in all_files
set itemKind to kind of item i in all_files
if (itemKind as string) is not "文件夹" then
set this_item to item i of all_files
set itemName to name of this_item
set itemExtension to "." & (name extension of this_item as text)
set datetime to modification date of this_item
set theDate to mfdateToDate(datetime)
-- 时间后缀这个处理稍微复杂点,因为文件是有可能被多次修改的,因此每次修改完都要替换掉原来的后缀,而不能简单地跳过不处理
if dateMark is in itemName then
set AppleScript's text item delimiters to dateMark
set previousItemName to the first text item of itemName
set previousDate to findAndReplaceInText(itemName, previousItemName & dateMark, "")
set previousDate to findAndReplaceInText(previousDate, "]" & itemExtension, "")
if previousDate is not theDate then
set new_name to previousItemName & dateMark & theDate & "]" & itemExtension
set name of this_item to new_name
set query to query & "
" & new_name
display notification query with title theDate & "文件时间重命名已完成"
end if
end if
-- 如果文件名没有时间后缀,那很有可能是新文件,就直接加上。而且加上的时候,不能直接加在文件名后面,不然会把文件真正的代表格式的后缀名也改掉。
if dateMark is not in itemName then
set pre_itemName to findAndReplaceInText(itemName, itemExtension, "")
set new_name to pre_itemName & dateMark & theDate & "]" & itemExtension
set name of this_item to new_name
set query to query & "
" & new_name
display notification query with title theDate & "文件时间重命名已完成"
end if
end if
end repeat
end rename
-- 定义多了一个函数,用于对当前日期的格式转换
on mfdateToDate(theDate as string)
set AppleScript's text item delimiters to "年"
set theYear to the first text item of theDate
set theMonth to the second text item of theDate
set AppleScript's text item delimiters to "月"
set theDay to the second text item of theMonth
set theMonth to the first text item of theMonth
set AppleScript's text item delimiters to "日"
set theDay to the first text item of theDay
set theYear to findAndReplaceInText(theYear, "20", "")
set theDate to theMonth & "." & theDay as text
return theDate
end mfdateToDate
-> 内置的一个函数,在重命名时调用处理真正后缀名的问题
on findAndReplaceInText(theText, theSearchString, theReplacementString)
set AppleScript's text item delimiters to theSearchString
set theTextItems to every text item of theText
set AppleScript's text item delimiters to theReplacementString
set theText to theTextItems as string
set AppleScript's text item delimiters to ""
return theText
end findAndReplaceInText

配置在 workflow 上,把总工作文件夹作为监控文件夹,再把里面每一个子项目作为变量输出。
因为封装成了一个 workflow,你可以用任何喜欢的方式触发,例如 Hazel、Keyboard Maestro、Alfred、鼠标双击...
这里,我自己依然使用了 Hazel 做监控,监控到文件夹有变动,就调用这个 workflow 即可。

因为封装成了Workflow,所以调用非常灵活
因为封装成了Workflow,所以调用非常灵活

Well,配合标签系统,整个文件系统目前已经还款一新!

  • 文件层级被压扁,一目了然
  • 可以根据文件类型去检索(例如我要找到最新的表格)
  • 也可以根据工作流程(例如我要找到最新的设计稿)来寻找

文件再多,也能保持层级扁平和结构清晰
文件再多,也能保持层级扁平和结构清晰

4. 搜索系统改造

要实现比较复杂的搜索,Spotlight 肯定不能满足了;然后因为有大量中文文档, LauchBar 也指望不上了。
那么,就使用 Alfred 吧。
我观察了一下自己日常检索文件的思路顺序,无非是:

  1. 我在处理的是哪个项目?
  2. 我在找的是什么文件?
    拓展 Alfred 的自带用例,增加了使用标签、种类等方法定位文件
    拓展 Alfred 的自带用例,增加了使用标签、种类等方法定位文件

    精准搜索的使用流程
    精准搜索的使用流程

    最后,使用系统自带的 example 改造一下,捏造出了一个搜索方法:首先选择项目名,然后有四种方法供我选择筛选文件:显示文件列表、关键词、类型、标签。

5. 最后

至此,这一轮的改造完成。重新回顾一下思路:

文件整理

  • 文件修改时间和所属项目外露:使用 Applescript 进行自动命名。
  • 打散文件夹结构,使用标签系统:按照工作流、文件类型、文件关键词等维度自动打标签。

文件搜索

  • 允许按照项目层层检索:在 Finder 内使用标签做排列区分、单向检索
  • 允许按照文件类型搜索:使用 Alfred 精确搜索类型
  • 允许按照关键词搜索:使用 Alfred 搜索关键词
  • 允许按照工作流搜索(例如原型、需求文档、接口文档、设计稿...etc.):使用 Alfred 搜索标签