1.1 使用Hadoop URL讀數據
想要使java識別出hdfs開頭的URL標示需要一點額外的工作要做:通過URL的setURLStreamHandlerFactory()方法爲 java設置一個FSUrlStreamHandlerFactory。這個方法在每個JVM中只能調用一次,所以它通常會被放在一個static block中執行(如下所示),但是如果你的某部分程序(例如一個你無法修改源代碼的第三方組件)已經調用了這個方法,那你就不能通過URL來這樣讀取數據了。相關代碼如下:
static {
URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory());
}
public static void main(String[] args) {
InputStream in = null;
try {
in = new URL("hdfs://172.20.59.227:8888/user/myuser/output10").openStream();
IOUtils.copyBytes(in, System.out, 4096, false);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeStream(in);
}
}
上例中我們使用了Hadoop中IOUtils類的兩個靜態方法:
1)IOUtils.copyBytes(),其中in表示拷貝源,System.out表示拷貝目的地(也就是要拷貝到標準輸出中去),4096表示用來拷貝的buffer大小,false表明拷貝完成後我們並不關閉拷貝源和拷貝目的地(因爲System.out並不需要關閉,in可以在finally語句中被關閉)。
2)IOUtils.closeStream(in),用來關閉in流。
執行結果如下:
2014-11-30 10:43:44,667 WARN conf.Configuration (Configuration.java:<clinit>(191)) - DEPRECATED: hadoop-site.xml found in the classpath. Usage of hadoop-site.xml is deprecated. Instead use core-site.xml, mapred-site.xml and hdfs-site.xml to override properties of core-default.xml, mapred-default.xml and hdfs-default.xml respectively hello, today is xx. i am happy to play with hadoop yes, if you do,everything is possible this example is just a case. |
如果沒有設置FSUrlStreamHandlerFactory,即缺少上例中的static block,程序則會報錯畸形的URL異常,如下所示:
java.net.MalformedURLException: unknown protocol: hdfs at java.net.URL.<init>(URL.java:590) at java.net.URL.<init>(URL.java:480) at java.net.URL.<init>(URL.java:429) at org.seandeng.hadoop.fs.URLCat.main(URLCat.java:27) |
1.2 使用FileSystem讀取數據
Hadoop文件系統中的文件是用Hadoop的Path對象來表示的(而不是java中的java.io.File對象)。可以把一個Path對象看做Hadoop文件系統中的某一個URL,如上例中的“hdfs://172.20.59.227:8888/user/myuser/output10”。
下面列出了幾個Filesystem的用於抽取Filesystem實例的幾個靜態方法:
public static FileSystem get(Configuration conf) public static FileSystem get(URI uri, Configuration conf) public static FileSystem get(final URI uri, final Configuration conf, final String user) |
一個Configuration對象封裝了客戶端或服務器端的配置信息,這些配置信息是通過從conf/core-site.xml之類的配置文件中讀取出來的鍵值對來設置的。下面我們一一說明上面的三個方法:
1)第一個方法返回一個默認的文件系統(在conf/core-site.xml中通過fs.default.name來指定的,如果在conf/core-site.xml中沒有設置則返回本地文件系統)。
2)第二個方法通過uri來指定要返回的文件系統(例如,如果uri是上個測試例子中的hdfs://172.20.59.227:8888/user/myuser/output10,即以hdfs標識開頭,那麼就返回一個hdfs文件系統,如果uri中沒有相應的標識則返回本地文件系統)。
3)第三個方法返回文件系統的機理同(2)是相同的,但它同時又限定了該文件系統的用戶,這在安全方面是很重要的。
有時候你可能想要使用一個本地文件系統,你可以使用另一個很方便的方法:
public static LocalFileSystem getLocal(Configuration conf) throws IOException
得到一個文件系統的實例後,我們可以調用該實例的open()方法來打開某個給定文件的輸入流(第一個方法使用一個默認的4KB的輸入緩衝):
示例代碼如下:
public static void main(String[] args) { String uri = "hdfs://172.20.59.227:8888/user/myuser/files/aaa.txt"; Configuration configuration = new Configuration(); try { FileSystem fs = FileSystem.get(URI.create(uri), configuration); InputStream in = null; try { in = fs.open(new Path(uri)); IOUtils.copyBytes(in, System.out, 4096, false); } catch (Exception e) { e.printStackTrace(); } finally { IOUtils.closeStream(in); } } catch (IOException e) { e.printStackTrace(); } } |
運行結果如下所示:
2014-11-30 11:01:49,139 WARN conf.Configuration (Configuration.java:<clinit>(191)) - DEPRECATED: hadoop-site.xml found in the classpath. Usage of hadoop-site.xml is deprecated. Instead use core-site.xml, mapred-site.xml and hdfs-site.xml to override properties of core-default.xml, mapred-default.xml and hdfs-default.xml respectively hello, today is xx. i am happy to play with hadoop yes, if you do,everything is possible this example is just a case. |
1.3 FSDataInputStream
與URL的openStream()方法返回InputStream不同,FileSystem的open()方法返回的是一個 FSDataInputStream對象(繼承關係:java.io.InputStream -->java.io.FilterInputStream-->java.io.DataInputStream--> org.apache.hadoop.fs.FSDataInputStream)。由於FSDataInputStream實現了Closeable,DataInput,PositionedReadable,Seekable等接口,你可以從流中的任意一個位置讀取數據。
Seekable接口的seek()和getPos()方法允許我們跳轉到流中的某個位置並得到其位置。
如果調用seek()時指定了一個超過文件長度的位移值,會拋出IOException異常。
與java.io.Inputstream的skip()方法指明一個相對位移值不同,seek()方法使用的是絕對位移值。如下所示的代碼通過seek()方法兩次讀取了輸入文件:
public static void main(String[] args) throws Exception { String uri = "hdfs://172.20.59.227:8888/user/myuser/files/aaa.txt";
Configuration configuration = new Configuration(); FileSystem fs = FileSystem.get(URI.create(uri), configuration); FSDataInputStream in = null; try { in = fs.open(new Path(uri)); IOUtils.copyBytes(in, System.out, 4096, false); in.seek(0); // 回到文件的起點 IOUtils.copyBytes(in, System.out, 4096, false); } finally { IOUtils.closeStream(in); } } |
執行結果如下:
2014-11-30 11:06:58,209 WARN conf.Configuration (Configuration.java:<clinit>(191)) - DEPRECATED: hadoop-site.xml found in the classpath. Usage of hadoop-site.xml is deprecated. Instead use core-site.xml, mapred-site.xml and hdfs-site.xml to override properties of core-default.xml, mapred-default.xml and hdfs-default.xml respectively hello, today is xx. i am happy to play with hadoop yes, if you do,everything is possible this example is just a case. hello, today is xx. i am happy to play with hadoop yes, if you do,everything is possible this example is just a case. |
需要注意的是調用seek()方法的代價比較高,應儘量避免使用。你的程序應該基於流式訪問來構建,而不是執行一大堆seek。
FSDataInputStream也實現了PositionedReadable接口,這允許你從流中的某個給定位置讀取給定長度的內容。
2 寫數據
FileSystem類有很多方法用來創建一個文件,最簡單的就是以欲創建文件的Path對象爲參數的create(Path f)方法,該方法返回一個用來寫入數據的輸出流:
public FSDataOutputStream create(Path f) throws IOException |
該方法還有幾個重載的方法,通過這些重載的方法你可以指定是否覆蓋該文件名已存在的文件,這個文件的備份數,用來寫數據的buffer size,該文件的block大小和文件權限等。
create()方法會創建指定的文件名中包含的任何不存在的父目錄,這樣雖然很方便,但不推薦使用(因爲如果某個父目錄中存在其他數據,會被覆蓋掉從而導致文件丟失)。如果你想要當父目錄不存在時該創建操作失敗,你可以在調用create()方法之前調用exists()方法檢查指明的父目錄是否存在,如果存在則報錯以讓create()失敗。exists()方法如下所示:
/** Check if exists. * @param f source file */ public boolean exists(Path f) throws IOException |
create()方法還有一個重載方法可以讓你傳遞一個回調的接口(Progressable),這樣你的程序就會知道你的數據被寫入了多少,即寫入的進度(progress):
public interface Progressable { /** * Report progress to the Hadoop framework. */ public void progress(); } |
除了創建一個新文件以寫入數據以外,我們還可以使用append()方法向一個已存在文件添加數據:
public FSDataOutputStream append(Path f) throws IOException |
有了這個函數,應用程序就可以向那些不能限制大小的文件寫數據了。append操作在Hadoop的fileSystem中是可選的,例如HDFS實現了它,但S3就沒有。
下面這個例子展示瞭如何從本地文件系統拷貝一個文件到HDFS,我們在每64KB大小的數據寫入之後調用一次progress()函數,這個函數每被調用一次打印一個句點:
public static void main(String[] args) throws Exception { String localSrc = "Z:\\cygwin\\home\\myuser\\hadoop-1.0.0\\bin\\DUCEAP-0.5.0-B2610.log"; String dst ="hdfs://172.20.59.227:8888/user/myuser/files/DUCEAP-0.5.0-B2610.log"; InputStream in = new BufferedInputStream(new FileInputStream(localSrc)); Configuration conf = new Configuration(); FileSystem fs = FileSystem.get(URI.create(dst), conf); OutputStream out = fs.create(new Path(dst), new Progressable() { public void progress() { System.out.print("."); } }); IOUtils.copyBytes(in, out, 4096, true); } |
執行結果如下:
2014-11-30 11:10:52,227 WARN conf.Configuration (Configuration.java:<clinit>(191)) - DEPRECATED: hadoop-site.xml found in the classpath. Usage of hadoop-site.xml is deprecated. Instead use core-site.xml, mapred-site.xml and hdfs-site.xml to override properties of core-default.xml, mapred-default.xml and hdfs-default.xml respectively ........................................ |
2.1 FSDataOutputStream
FileSystem中的create()方法返回一個FSDataOutputStream,像FSDataInputStream一樣,它也有一個用於查詢位移的方法(但並沒有類似於FSDataInputStream中seek()的方法,因爲Hadoop不允許向流中的任意位置寫數據,我們只能在一個文件的末尾處添加數據):
public class FSDataOutputStream extends DataOutputStream implements Syncable { public long getPos() throws IOException; } |
2.2 mkdirs
mkdirs()方法是在給定目錄下創建一個子目錄,代碼如下所示:
public static void main(String[]args) throws IOException { String uri = "hdfs://172.20.59.227:8888//user/myuser/"; Configuration conf = new Configuration(); FileSystem fs = FileSystem.get(URI.create(uri), conf); Path path = new Path("/user/myuser/newDir"); fs.mkdirs(path); FileStatus stat = fs.getFileStatus(path); System.out.println(System.currentTimeMillis()); System.out.println(stat.getModificationTime()); } |
執行結果如下:
2014-11-30 12:01:07,451 WARN conf.Configuration (Configuration.java:<clinit>(191)) - DEPRECATED: hadoop-site.xml found in the classpath. Usage of hadoop-site.xml is deprecated. Instead use core-site.xml, mapred-site.xml and hdfs-site.xml to override properties of core-default.xml, mapred-default.xml and hdfs-default.xml respectively 1417320069145 1417320069081 |
3 刪除數據
使用FIleSystem的delete()方法可以永久的刪除一個文件或目錄:
public boolean delete(Path f, boolean recursive) throws IOException |
如果傳入的Path f是一個文件或者空目錄,recursive的值會被忽略掉。當recursive值爲true時,給定的非空目錄連同其內容會被一併刪除掉。
4 查詢文件系統信息
4.1 文件元數據:FileStatus
任何文件系統的典型功能就是能夠遍歷它的目錄結構從而獲取有關目錄和文件的信息。Hadoop中的FileStatus類爲文件和目錄包裝了其元數據(包括文件長度,block大小,冗餘度,修改時間,文件所有者和權限等信息),其getFileStatus()方法提供了獲取某個給定文件或目錄的 FileStatus對象的途徑,如下所示:
package org.seandeng.hadoop.fs; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hdfs.MiniDFSCluster; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; import static org.hamcrest.Matchers.*; public class ShowFileStatusTest { private MiniDFSCluster cluster; // use an in-process HDFS cluster for testing (這個類在最新的Hadoop1.0.4中已經被廢棄了) private FileSystem fs; @Before public void setUp() throws IOException { Configuration conf = new Configuration(); if (System.getProperty("test.build.data") == null) { System.setProperty("test.build.data", "/tmp"); } cluster = new MiniDFSCluster(conf, 1, true, null); fs = cluster.getFileSystem(); OutputStream out = fs.create(new Path("/dir/file")); out.write("content".getBytes("UTF-8")); out.close(); } @After public void tearDown() throws IOException { if (fs != null) { fs.close(); } if (cluster != null) { cluster.shutdown(); } }
@Test(expected = FileNotFoundException.class) public void throwsFileNotFoundForNonExistentFile() throws IOException { fs.getFileStatus(new Path("no-such-file")); }
@Test public void fileStatusForFile() throws IOException { Path file = new Path("/dir/file"); FileStatus stat = fs.getFileStatus(file); assertThat(stat.getPath().toUri().getPath(), is("/dir/file")); assertThat(stat.isDir(), is(false)); assertThat(stat.getLen(), is(7L)); assertThat(stat.getModificationTime(), is(lessThanOrEqualTo(System.currentTimeMillis()))); assertThat(stat.getReplication(), is((short) 1)); assertThat(stat.getBlockSize(), is(64 * 1024 * 1024L)); assertThat(stat.getOwner(), is("myuser")); assertThat(stat.getGroup(), is("supergroup")); assertThat(stat.getPermission().toString(), is("rw-r--r--")); }
@Test public void fileStatusForDirectory() throws IOException { Path dir = new Path("/dir"); FileStatus stat = fs.getFileStatus(dir); assertThat(stat.getPath().toUri().getPath(), is("/dir")); assertThat(stat.isDir(), is(true)); assertThat(stat.getLen(), is(0L)); assertThat(stat.getModificationTime(), is(lessThanOrEqualTo(System.currentTimeMillis()))); assertThat(stat.getReplication(), is((short) 0)); assertThat(stat.getBlockSize(), is(0L)); assertThat(stat.getOwner(), is("myuser")); assertThat(stat.getGroup(), is("supergroup")); assertThat(stat.getPermission().toString(), is("rwxr-xr-x")); } } |
4.2 Listing files
除了從某個單一文件或目錄獲取文件信息以外,你可能還需要列出某個目錄中的所有文件,這就要使用FileSystem的listStatus()方法了:
public FileStatus[] listStatus(Path f) public FileStatus[] listStatus(Path[] files) |
當傳入參數是一個文件時,它獲取此文件的FileStatus對象,當傳入文件是目錄時,它返回零個或多個FileStatus對象,分別代表該目錄下所有文件的對應信息。
重載後的函數允許你指定一個PathFilter來進一步限定要匹配的文件或目錄。
下面我們使用listStatus()方法獲得參數中指定的目錄的元數據信息,存放在一個FIleStatus數組中,再使用stat2Paths()方法把FileStatus數組轉化爲Path數組,最後打印出文件名來:
public static void main(String[] args) throws Exception { String uri = "hdfs://172.20.59.227:8888/user/myuser/"; Configuration conf = new Configuration(); FileSystem fs = FileSystem.get(URI.create(uri), conf); Path[] paths = new Path[1]; paths[0] = new Path(uri); FileStatus[] status = fs.listStatus(paths); Path[] listedPaths = FileUtil.stat2Paths(status); for (Path p : listedPaths) { System.out.println(p); } } |
執行結果如下:
2014-11-30 11:38:44,549 WARN conf.Configuration (Configuration.java:<clinit>(191)) - DEPRECATED: hadoop-site.xml found in the classpath. Usage of hadoop-site.xml is deprecated. Instead use core-site.xml, mapred-site.xml and hdfs-site.xml to override properties of core-default.xml, mapred-default.xml and hdfs-default.xml respectively hdfs://172.20.59.227:8888/user/myuser/files hdfs://172.20.59.227:8888/user/myuser/input hdfs://172.20.59.227:8888/user/myuser/input2 hdfs://172.20.59.227:8888/user/myuser/output10 |
4.3 文件模式
在某個單一操作中處理一些文件是很常見的。例如一個日誌處理的MapReduce作業可能要分析一個月的日誌量。如果一個文件一個文件或者一個目錄一個目錄的聲明那就太麻煩了,我們可以使用通配符來匹配多個文件。Hadoop提供了兩種方法來處理文件組:
public FileStatus[] globStatus(Path pathPattern) |
globStatus()方法返回匹配文件模式的多個文件的FileStatus數組(以Path排序)。一個可選的PathFilter可以用來進一步限制匹配模式。Hadoop中的匹配符與Unix中bash相同,如下所示:
假設某個日誌文件的組織結構如下:
則對應於該組織結構有如下表示:
4.4 PathFilter
使用文件模式有時候並不能有效的描述你想要的一系列文件,例如如果你想排除某個特定文件就很難。所以FileSystem的listStatus()和globStatus()方法就提供了一個可選參數:PathFilter——它允許你一些更細化的控制匹配:
public interface PathFilter { |
PathFilter的作用就像java.io.FileFilter,只不過前者針對Path對象,而後者針對File對象。下面我們用PathFIlter來排除一個符合給定正則表達式的文件:
public class RegexExcludePathFilter implements PathFilter { private final String regex; public RegexExcludePathFilter(String regex) { this.regex = regex; } public boolean accept(Path path) { return !path.toString().matches(regex); } } |
RegexExcludePathFilter只讓不匹配給定正則表達式的文件通過,我們通過文件模式(file pattern)得到所需的文件集後,再用RegexExcludePathFilter來過濾掉我們不需要的文件:
fs.globStatus(new Path("/2007/*/*"), new RegexExcludeFilter("^.*/2007/12/31$")) |
這樣我們就得到:/2007/12/30
注意:Filter只能根據文件名來過濾文件,是不能通過文件的屬性(如修改時間,文件所有者等)來過濾文件的。