有效地创建和扩展从大文本文件

时间:2015-10-12 03:21:07

标签: clojure

我正在尝试在AWS S3存储桶中复制大约1200万个文档,以便为它们提供新名称。这些名称以前有一个前缀,现在都只是文档名称。因此,a/b/123重命名后将为123。最后一段是uuid,因此不会有任何命名冲突。

此过程已部分完成,因此有些已被复制,有些仍需要。我有一个包含所有文档名称的文本文件。我想要一种有效的方法来确定哪些文件尚未移动。

我有一些天真的代码,显示了我想要完成的任务。

(def doc-names ["o/123" "o/234" "t/543" "t/678" "123" "234" "678"])

(defn still-need-copied [doc-names]
  (let [last-segment  (fn [doc-name] 
                        (last (clojure.string/split doc-name #"/")))
        by-position   (group-by #(.contains % "/") doc-names)
        top           (set (get by-position false))
        nested        (set (map #(last-segment %) (get by-position true)))
        needs-copied  (clojure.set/difference nested top)]
    (filter #(contains? needs-copied (last-segment %)) doc-names)))

1 个答案:

答案 0 :(得分:0)

我会提出这个解决方案:

(defn still-need-copied [doc-names]
  (->> doc-names
       (group-by #(last (clojure.string/split % #"/")))
       (keep #(when (== 1 (count (val %))) (first (val %))))))

首先按最后一个元素拆分字符串对所有项目进行分组,得到以下输入:

{"123" ["o/123" "123"], 
 "234" ["o/234" "234"], 
 "543" ["t/543"], 
 "678" ["t/678" "678"]}

然后你只需要选择长度为1的地图的所有值,并采用它们的第一个元素。

我想说它比你的变体更具可读性,而且似乎也更有效率。 这就是原因:

据我所知,你的代码可能有复杂的

N (仅使用2个键分组到地图)+

Nlog(N)(创建和填充顶部集)+

Nlog(N)(嵌套集的创建和填充)+

Nlog(N)(设置差异)+

Nlog(N)(过滤+搜索需要复制的集合中的每个元素)=

4Nlog(N) + N

而我的变体可能具有

的复杂性

Nlog(N)(将值分组到具有大量键的地图中)+

N(保持所需的值)=

N + Nlog(N)

虽然渐渐地它们都是O(Nlog(N)),但实际上我的可能会更快完成。

ps:不是复杂性理论的专家。刚做了一些非常粗略的估计

这是一个小测试:

(defn generate-data [len]
  (doall (mapcat
           #(let [n (rand-int 2)]
              (if (zero? n)
                [(str "aaa/" %) (str %)]
                [(str %)]))
           (range len))))

(defn still-need-copied [doc-names]
  (let [last-segment  (fn [doc-name] 
                        (last (clojure.string/split doc-name #"/")))
        by-position   (group-by #(.contains % "/") doc-names)
        top           (set (get by-position false))
        nested        (set (map #(last-segment %) (get by-position true)))
        needs-copied  (clojure.set/difference nested top)]
    (filter #(contains? needs-copied (last-segment %)) doc-names)))

(defn still-need-copied-2 [doc-names]
  (->> doc-names
       (group-by #(last (clojure.string/split % #"/")))
       (keep #(when (== 1 (count (val %))) (first (val %))))))

(def data-100k (generate-data 100000))
(def data-1m (generate-data 1000000))

user> (let [_ (time (dorun (still-need-copied data-100k)))
            _ (time (dorun (still-need-copied-2 data-100k)))
            _ (time (dorun (still-need-copied data-1m)))
            _ (time (dorun (still-need-copied-2 data-1m)))])
"Elapsed time: 714.929641 msecs"
"Elapsed time: 243.918466 msecs"
"Elapsed time: 7094.333425 msecs"
"Elapsed time: 2329.75247 msecs"

所以它快3倍,就像我预测的那样

<强>更新 找到了一个解决方案,这不是那么优雅,但似乎有效。 你说你正在使用iota,所以我生成了一个包含~15百万行的巨大文件(前面提到的生成数据fn)

然后我决定在斜线后的最后一部分排序(以便“123”和“aaa / 123”站在一起。

(defn last-part [s] (last (clojure.string/split s #"/")))

(def sorted (sort-by last-part (iota/seq "my/file/path")))

它完成得非常快。所以我必须要做的最后一件事是,如果附近有一个项目具有相同的最后部分,则对每个项目进行简单的循环检查:

(def res (loop [res [] [item1 & [item2 & rest :as tail] :as coll] sorted]
           (cond (empty? coll) res
                 (empty? tail) (conj res item1)
                 (= (last-part item1) (last-part item2)) (recur res rest)
                 :else (recur (conj res item1) tail))))

它也完成没有任何明显的困难,所以我得到了所需的结果,没有任何map / reduce框架。 我认为,如果你不将变量的coll保留在var中,你可能会通过避免巨大的头部保留来节省内存:

(def res (loop [res []
                [item1 & [item2 & rest :as tail] :as coll] (sort-by last-part (iota/seq "my/file/path"))]
           (cond (empty? coll) res
           (empty? tail) (conj res item1)
           (= (last-part item1) (last-part item2)) (recur res rest)
           :else (recur (conj res item1) tail))))