將多個路徑字串轉換成XML文件樹

小龍貓發表於2016-05-09

假設有下面的字串:

1
2
3
4
5
6
7
/home/usr/abc/def/文字.txt
/home/usr/desktop/音樂.mp3
/etc/init.d/mysql/mysql
/etc/profile
/tmp/垃圾.tmp
/usr/bin/open-jdk7/java
...

給定一個根節點名字root和葉子節點名字leaf,如何將它們轉換成一顆像下面這樣的XML文件樹呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<root>
  <home>
    <usr>
      <abc>
        <leaf>文字.txt</leaf>
      </abc>
      <desktop>
        <leaf>音樂.mp3</leaf>
      </desktop>
    </usr>
  </home>
  <etc>
    <init.d>
      <mysql>
        <leaf>mysql</leaf>
      </mysql>
    </init.d>
    <leaf>profile</leaf>
  </etc>
  <tmp>
    <leaf>垃圾.tmp</leaf>
  </tmp>
  <usr>
    <bin>
      <open-jdk7>
        <leaf>java</leaf>
      </open-jdk7>
    </bin>
  </usr>
</root>

對於這個問題,一個解決的思路是:先建立一顆以root作為根標籤的XML文件樹,再迴圈迭代每個字串,將其按照`/`切開(Split),然後依次對每個資料夾名字建立一個XML節點(Node),並搜尋整棵樹,如果該節點存在,則直接pass掉,否則將節該點追加到某個父節點下。但是此種方法太麻煩,因為每次增加一個節點,你就需要去遍歷一次。有很多情況下,前幾層節點是存在的,這樣判斷就不那麼高效了。舉個例子,現在有一條深度為10的資料夾路徑(比如/a/b/c/d/e/f/g/h/i/j/**.sh),為了插入這條路徑,首先需要判斷/a是否存在,存在就pass掉,不存在就建立/a;之後判斷/a/b是否存在;之後是/a/b/c是否存在。。。

很顯然,這樣做,越到後面效率越低。而且,直接操作Xml文件會佔用很多資源。

那麼,有沒有一種簡單又高效的方式呢?答案是肯定的,這就是寫這篇部落格的原因了。這只是一種方案,也許還有更好的,有興趣的同學可以自行研究,哈哈……

首先,我們可以將這些資料夾路徑進行預處理────轉換成中間格式進行儲存。這裡,我們可以先定義一個Map結構,Key用於儲存當前節點名字和節點的XPath,中間可用特殊字元隔開;Value用於儲存當前節點的父節點的名字和父節點的XPath(根節點root的父親為null),中間也用相同的特殊字元隔開,像下面這樣:

1
2
<home#/root, root#null>
<user#/root/home, home#/root>

第一行表示:當前的home節點XPath為/root,其父節點root的xPath為null,同理,第二行表示:當前節點user的XPath為/root/home,而其父節點home的XPath為/root。把Key設計成這樣有一個好處是,當一條路徑中的多層裡有相同名字的資料夾時,也可以輕易分辨。比如有一條路徑為:/home/home/home,那麼在建立節點時,會建立以下幾個鍵值對:

1
2
3
<home#/root, root#null>
<home#/root/home, home#/root>
<home#/root/home/home, home#/root/home>

就是說,在同一個XPath(例如/root/home)下,只會存在一個名為home的資料夾。如果以後的資料夾路徑中還包含/home/home/home這樣的地址,那麼這些資料夾的子資料夾將會被放在已經存在的/root/home/home/home路徑下。再通俗一點:每個Key-Value都是唯一的,它唯一表示了一條路徑的存在。

有人可能會問:為什麼Value也要設計成一樣的結構呢?

答案很簡單,便於在以後的處理中直接使用這個值,後面會提到。

通過這樣的轉換,我們可以看出,第二行的value實際上就是第一行的key,這樣我們就可以表示一條一條的子孫——祖宗鏈,都是由子節點指向父節點。

轉換之後的Map像下面這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<home#/root, root#null>
<user#/root/home, home#/root>
<abc#/root/home/user, user#/root/home>
<def#/root/home/user/abc, abc#/root/home/user>
<文字.txt#/root/home/user/abc/def, def#/root/home/user/abc/def>
 
<!-- 第二個URL中的home資料夾和user資料夾對應的key就是第一個key,
已經存在,則不會在增加這個key了 -->
<desktop#/root/home/user, user#/root/home>
<音樂.mp3#/root/home/user/desktop, desktop#/root/home/user>
 
<etc#/root, root#null>
<init.d#/root/etc, etc#/root>
<mysql#/root/etc/init.d, init.d#/root/etc>
<mysql#/root/etc/init.d/mysql, mysql#/rot/etc/init.d>
...

細心的同學可能已經看見了,第5行以後,處理第二個URL時,home和user兩個資料夾已經存在,則直接pass掉,接著處理desktop資料夾了。還有desktop資料夾對應的value是user#/root/home,這和第3行的value相同,因此資料夾desktop和abc屬於同一級,都在/root/home/user下。

轉換後,我們可以做以下幾件事:

  1. 直接遍歷這個Map,先找出根目錄(即value為root#null的)root下的所有節點(這裡是home, etc, tmp, usr,當然也可能直接就是一個檔案了)。判定的標準為:所有的value都為root#null的。

  2. 迴圈遍歷root的子節點集合,判定每個子節點是否為葉子節點(即檔案),若是,則加上<leaf>節點名字<leaf>,可以考慮使用StringBuilder.append();,若不是,則對每個子節點做以下幾件事:

    1. 加上節點開始標記<節點名字>

    2. 做第1步,只是當前目錄不是根目錄而已(遞迴了)

    3. 加上節點結束標誌<節點名字>


這樣遞迴下去,就會形成多顆以資料夾路徑開始的資料夾作為根目錄的XML樹,最後將所有得到的小樹都新增到新建立的<root></root>根標籤中,到此,文件生成完成。

程式碼就不寫了,思路已經相當清晰了,哈哈

使用這種方式,雖然迭代次數可能比較多,但是使用Map來儲存樹的結構以及使用字串來生成XML的資源消耗都不大,而且效率都相當高。至於有多高,我用以上資料做了個測試,執行時間在1~3ms,感興趣的親們可以試試。


後記

    開始的一個版本是:將Map中的Key和Value都表示為當前【節點名字#深度】,但是當我按照這種思路寫完後,立刻發現生成的XML不是我想要的,因為我的測試資料中存在多個【節點名字】和【深度】都相同的節點,但是其根節點不同。。。。究其原因,是因為我們設計時可能存在同樣的Key,於是後面的同深度且同名的資料夾名字則被pass掉了,導致該深度下所有同名的資料夾都被新增到了第一顆根節點下。

    後來想了半天,才把這個【節點名字#深度】替換為【節點名字#XPath】,這個必須是唯一的了。


相關文章