本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結
對於處理檔案,我們介紹了流的方式,57節介紹了位元組流,58節介紹了字元流,同時,也介紹了比較底層的操作檔案的方式,60節介紹了隨機讀寫檔案,61節介紹了記憶體對映檔案,我們也介紹了物件的序列化/反序列化機制,62節介紹了Java標準的序列化,63節介紹瞭如何用Jackson處理其他序列化格式如XML/JSON和MessagePack。
在日常程式設計中,我們還經常會需要處理一些具體型別的檔案,如CSV, Excel, HTML,直接使用前面幾節介紹的方式來處理一般是很不方便的,往往有一些第三方的類庫,基於之前介紹的技術,提供了更為方便易用的介面。
本節,我們就來簡要介紹如何利用Java SDK和一些第三方類庫,來處理如下五種型別的檔案:
- 屬性檔案:屬性檔案是常見的配置檔案,用於在不改變程式碼的情況下改變程式的行為。
- CSV:CSV是Comma-Separated Values的縮寫,表示逗號分割值,是一種非常常見的檔案型別,大部分日誌檔案都是CSV,CSV也經常用於交換表格型別的資料,待會我們會看到,CSV看上去很簡單但處理的複雜性經常被低估。
- Excel:Excel大家都知道,在程式設計中,經常需要將表格型別的資料匯出為Excel格式,以方便使用者檢視,也經常需要接受Excel型別的檔案作為輸入以批量匯入資料。
- HTML:所有網頁都是HTML格式,我們經常需要分析HTML網頁,以從中提取感興趣的資訊。
- 壓縮檔案:壓縮檔案有多種格式,也有很多壓縮工具,大部分情況下,我們可以藉助工具而不需要自己寫程式處理壓縮檔案,但某些情況,需要自己程式設計壓縮檔案或解壓縮檔案。
屬性檔案
屬性檔案一般很簡單,一行表示一個屬性,屬性就是鍵值對,鍵和值用等號(=)或冒號(:)分隔,一般用於配置程式的一些引數。比如,在需要連線資料庫的程式中,經常使用配置檔案配置資料庫資訊,比如,有這麼個檔案config.properties,內容大概如下所示:
db.host = 192.168.10.100
db.port : 3306
db.username = zhangsan
db.password = mima1234
複製程式碼
處理這種檔案使用字元流也是比較容易的,但Java中有一個專門的類java.util.Properties,它的使用也很簡單,有如下主要方法:
public synchronized void load(InputStream inStream)
public String getProperty(String key)
public String getProperty(String key, String defaultValue)
複製程式碼
load用於從流中載入屬性,getProperty用於獲取屬性值,可以提供一個預設值,如果沒有找到配置的值,則返回預設值。對於上面的配置檔案,可以使用類似下面的程式碼進行讀取:
Properties prop = new Properties();
prop.load(new FileInputStream("config.properties"));
String host = prop.getProperty("db.host");
int port = Integer.valueOf(prop.getProperty("db.port", "3306"));
複製程式碼
使用類Properties處理屬性檔案的好處是:
- 可以自動處理空格,我們看到分隔符=前後的空格會被自動忽略
- 可以自動忽略空行
- 可以新增註釋,以字元#或!開頭的行會被視為註釋,進行忽略
不過,使用Properties也有限制,它不能直接處理中文,在配置檔案中,所有非ASCII字元需要使用Unicode編碼,比如,不能在配置檔案中直接這麼寫:
name=老馬
複製程式碼
"老馬"需要替換為Unicode編碼,如下所示:
name=\u8001\u9A6C
複製程式碼
在Java IDE如Eclipse中,如果使用屬性檔案編輯器,它會自動替換中文為Unicode編碼,如果使用其他編輯器,可以先寫成中文,然後使用JDK提供的命令native2ascii轉換為Unicode編碼,用法如下例所示:
native2ascii -encoding UTF-8 native.properties ascii.properties
複製程式碼
native.properties是輸入,其中包含中文,ascii.properties是輸出,中文替換為了Unicode編碼,-encoding指定輸入檔案的編碼,這裡指定為了UTF-8。
CSV檔案
CSV是Comma-Separated Values的縮寫,表示逗號分割值,一般而言,一行表示一條記錄,一條記錄包含多個欄位,欄位之間用逗號分隔。不過,一般而言,分隔符不一定是逗號,可能是其他字元如tab符'\t'、冒號':',分號';'等。程式中的各種日誌檔案通常是CSV檔案,在匯入匯出表格型別的資料時,CSV也是經常用的一種格式。
CSV格式看上去很簡單,比如,我們在58節儲存學生列表時,使用的就是CSV格式,如下所示:
張三,18,80.9
李四,17,67.5
複製程式碼
使用之前介紹的字元流,看上去就可以很容易處理CSV檔案,按行讀取,對每一行,使用String.split進行分割即可。但其實CSV有一些複雜的地方,最重要的是:
- 欄位內容中包含分割符怎麼辦?
- 欄位內容中包含換行符怎麼辦?
對於這些問題,CSV有一個參考標準,RFC-4180,tools.ietf.org/html/rfc418…,但實踐中不同程式往往有其他處理方式,所幸的是,處理方式大體類似,大概有兩種處理方式:
- 使用引用符號比如",在欄位內容兩邊加上",如果內容中包含"本身,則使用兩個"
- 使用轉義字元,常用的是\,如果內容中包含\,則使用兩個\
比如,如果欄位內容有兩行,內容為:
hello, world \ abc
"老馬"
複製程式碼
使用第一種方式,內容會變為:
"hello, world \ abc
""老馬"""
複製程式碼
使用第二種方式,內容會變為:
hello\, world \\ abc\n"老馬"
複製程式碼
CSV還有其他一些細節,不同程式的處理方式也不一樣,比如:
- 怎麼表示null值?
- 空行和欄位之間的空格怎麼處理?
- 怎麼表示註釋?
由於以上這些複雜問題,使用簡單的字元流就難以處理了。有一個第三方類庫,Apache Commons CSV,對處理CSV提供了良好的支援,它的官網地址是:commons.apache.org/proper/comm…
本節使用其1.4版本,簡要介紹其用法。如果使用Maven管理專案,可引入以下檔案中的依賴:github.com/swiftma/pro…。如果非Maven,可從下面地址下載依賴庫:github.com/swiftma/pro…
Apache Commons CSV中有一個重要的類CSVFormat,它表示CSV格式,它有很多方法以定義具體的CSV格式,如:
//定義分隔符
public CSVFormat withDelimiter(final char delimiter)
//定義引號符
public CSVFormat withQuote(final char quoteChar)
//定義轉義符
public CSVFormat withEscape(final char escape)
//定義值為null的物件對應的字串值
public CSVFormat withNullString(final String nullString)
//定義記錄之間的分隔符
public CSVFormat withRecordSeparator(final char recordSeparator)
//定義是否忽略欄位之間的空白
public CSVFormat withIgnoreSurroundingSpaces(final boolean ignoreSurroundingSpaces)
複製程式碼
比如,如果CSV格式定義為:使用分號;作為分隔符,"作為引號符,使用N/A表示null物件,忽略欄位之間的空白,CSVFormat可以這樣建立:
CSVFormat format = CSVFormat.newFormat(';')
.withQuote('"').withNullString("N/A")
.withIgnoreSurroundingSpaces(true);
複製程式碼
除了自定義CSVFormat,CSVFormat類中也定義了一些預定義的格式,如:CSVFormat.DEFAULT, CSVFormat.RFC4180。
CSVFormat有一個方法,可以分析字元流:
public CSVParser parse(final Reader in) throws IOException
複製程式碼
返回值型別為CSVParser,它有如下方法獲取記錄資訊:
public Iterator<CSVRecord> iterator()
public List<CSVRecord> getRecords() throws IOException
public long getRecordNumber()
複製程式碼
CSVRecord表示一條記錄,它有如下方法獲取每個欄位的資訊:
//根據欄位列索引獲取值,索引從0開始
public String get(final int i)
//根據列名獲取值
public String get(final String name)
//欄位個數
public int size()
//欄位的迭代器
public Iterator<String> iterator()
複製程式碼
分析CSV檔案的基本程式碼如下所示:
CSVFormat format = CSVFormat.newFormat(';')
.withQuote('"').withNullString("N/A")
.withIgnoreSurroundingSpaces(true);
Reader reader = new FileReader("student.csv");
try{
for(CSVRecord record : format.parse(reader)){
int fieldNum = record.size();
for(int i=0; i<fieldNum; i++){
System.out.print(record.get(i)+" ");
}
System.out.println();
}
}finally{
reader.close();
}
複製程式碼
除了分析CSV檔案,Apache Commons CSV也可以寫CSV檔案,有一個CSVPrinter,它有很多列印方法,比如:
//輸出一條記錄,引數可變,每個引數是一個欄位值
public void printRecord(final Object... values) throws IOException
//輸出一條記錄
public void printRecord(final Iterable<?> values) throws IOException
複製程式碼
看個程式碼示例:
CSVPrinter out = new CSVPrinter(new FileWriter("student.csv"),
CSVFormat.DEFAULT);
out.printRecord("老馬", 18, "看電影,看書,聽音樂");
out.printRecord("小馬", 16, "樂高;賽車;");
out.close();
複製程式碼
輸出檔案student.csv中的內容為:
"老馬",18,"看電影,看書,聽音樂"
"小馬",16,樂高;賽車;
複製程式碼
Excel
Excel主要有兩種格式,字尾名分別為.xls和.xlsx,.xlsx是Office 2007以後的預設副檔名。Java中處理Excel檔案及其他微軟文件廣泛使用POI類庫,其官網是http://poi.apache.org/。
本節使用其3.15版本,簡要介紹其用法。如果使用Maven管理專案,可引入以下檔案中的依賴:github.com/swiftma/pro…。如果非Maven,可從下面地址下載依賴庫:github.com/swiftma/pro…
使用POI處理Excel檔案,有如下主要類:
- Workbook: 表示一個Excel檔案物件,它是一個介面,有兩個主要類HSSFWorkbook和XSSFWorkbook,前者對應.xls格式,後者對應.xlsx格式。
- Sheet: 表示一個工作表
- Row: 表示一行
- Cell: 表示一個單元格
比如,儲存學生列表到student.xls,程式碼可以為:
public static void saveAsExcel(List<Student> list) throws IOException {
Workbook wb = new HSSFWorkbook();
Sheet sheet = wb.createSheet();
for (int i = 0; i < list.size(); i++) {
Student student = list.get(i);
Row row = sheet.createRow(i);
row.createCell(0).setCellValue(student.getName());
row.createCell(1).setCellValue(student.getAge());
row.createCell(2).setCellValue(student.getScore());
}
OutputStream out = new FileOutputStream("student.xls");
wb.write(out);
out.close();
wb.close();
}
複製程式碼
如果要儲存為.xlsx格式,只需要替換第一行為:
Workbook wb = new XSSFWorkbook();
複製程式碼
使用POI也可以方便的解析Excel檔案,使用WorkbookFactory的create方法即可,如下所示:
public static List<Student> readAsExcel() throws Exception {
Workbook wb = WorkbookFactory.create(new File("student.xls"));
List<Student> list = new ArrayList<Student>();
for(Sheet sheet : wb){
for(Row row : sheet){
String name = row.getCell(0).getStringCellValue();
int age = (int)row.getCell(1).getNumericCellValue();
double score = row.getCell(2).getNumericCellValue();
list.add(new Student(name, age, score));
}
}
wb.close();
return list;
}
複製程式碼
以上我們只是介紹了基本用法,如果需要更多資訊,如配置單元格的格式、顏色、字型,可參看http://poi.apache.org/spreadsheet/quick-guide.html。
HTML
HTML是網頁的格式,如果不熟悉,可以參看http://www.w3school.com.cn/html/html_intro.asp。在日常工作中,可能需要分析HTML頁面,抽取其中感興趣的資訊。有很多HTML分析器,我們簡要介紹一種,jsoup,其官網地址為https://jsoup.org/。
本節使用其1.10.2版本。如果使用Maven管理專案,可引入以下檔案中的依賴:github.com/swiftma/pro…。如果非Maven,可從下面地址下載依賴庫:github.com/swiftma/pro…。
我們通過一個簡單例子來看jsoup的使用,我們要分析的網頁地址是:http://www.cnblogs.com/swiftma/p/5631311.html
瀏覽器中看起來的樣子是這樣的(部分截圖):
將網頁儲存下來,其HTML程式碼看上去是這樣的(部分截圖): 假定我們要抽取網頁主題內容中每篇文章的標題和連結,怎麼實現呢?jsoup支援使用CSS選擇器語法查詢元素,如果不瞭解CSS選擇器,可參看http://www.w3school.com.cn/cssref/css_selectors.asp。定位文章列表的CSS選擇器可以是
#cnblogs_post_body p a
複製程式碼
我們來看程式碼(假定檔案為articles.html):
Document doc = Jsoup.parse(new File("articles.html"), "UTF-8");
Elements elements = doc.select("#cnblogs_post_body p a");
for(Element e : elements){
String title = e.text();
String href = e.attr("href");
System.out.println(title+", "+href);
}
複製程式碼
輸出為(部分):
計算機程式的思維邏輯 (1) - 資料和變數, http://www.cnblogs.com/swiftma/p/5396551.html
計算機程式的思維邏輯 (2) - 賦值, http://www.cnblogs.com/swiftma/p/5399315.html
複製程式碼
jsoup也可以直接連線URL進行分析,比如,上面程式碼的第一行可以替換為:
String url = "http://www.cnblogs.com/swiftma/p/5631311.html";
Document doc = Jsoup.connect(url).get();
複製程式碼
關於jsoup的更多用法,請參看其官網。
壓縮檔案
壓縮檔案有多種格式,Java SDK支援兩種:gzip和zip,gzip只能壓縮一個檔案,而zip檔案中可以包含多個檔案。下面我們介紹Java SDK中的基本用法,如果需要更多格式,可以考慮Apache Commons Compress:http://commons.apache.org/proper/commons-compress/
先來看gzip,有兩個主要的類:
java.util.zip.GZIPOutputStream
java.util.zip.GZIPInputStream
複製程式碼
它們分別是OutputStream和InputStream的子類,都是裝飾類,GZIPOutputStream加到已有的流上,就可以實現壓縮,而GZIPInputStream加到已有的流上,就可以實現解壓縮。比如,壓縮一個檔案的程式碼可以為:
public static void gzip(String fileName) throws IOException {
InputStream in = null;
String gzipFileName = fileName + ".gz";
OutputStream out = null;
try {
in = new BufferedInputStream(new FileInputStream(fileName));
out = new GZIPOutputStream(new BufferedOutputStream(
new FileOutputStream(gzipFileName)));
copy(in, out);
} finally {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
}
}
複製程式碼
呼叫的copy方法是我們在57節介紹的。解壓縮檔案的程式碼可以為:
public static void gunzip(String gzipFileName, String unzipFileName)
throws IOException {
InputStream in = null;
OutputStream out = null;
try {
in = new GZIPInputStream(new BufferedInputStream(
new FileInputStream(gzipFileName)));
out = new BufferedOutputStream(new FileOutputStream(
unzipFileName));
copy(in, out);
} finally {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
}
}
複製程式碼
zip檔案支援一個壓縮檔案中包含多個檔案,Java SDK主要的類是:
java.util.zip.ZipOutputStream
java.util.zip.ZipInputStream
複製程式碼
它們也分別是OutputStream和InputStream的子類,也都是裝飾類,但不能像GZIPOutputStream/GZIPInputStream那樣簡單使用。
ZipOutputStream可以寫入多個檔案,它有一個重要方法:
public void putNextEntry(ZipEntry e) throws IOException
複製程式碼
在寫入每一個檔案前,必須要先呼叫該方法,表示準備寫入一個壓縮條目ZipEntry,每個壓縮條目有個名稱,這個名稱是壓縮檔案的相對路徑,如果名稱以字元'/'結尾,表示目錄,它的構造方法是:
public ZipEntry(String name)
複製程式碼
我們看一段程式碼,壓縮一個檔案或一個目錄:
public static void zip(File inFile, File zipFile) throws IOException {
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(
new FileOutputStream(zipFile)));
try {
if (!inFile.exists()) {
throw new FileNotFoundException(inFile.getAbsolutePath());
}
inFile = inFile.getCanonicalFile();
String rootPath = inFile.getParent();
if (!rootPath.endsWith(File.separator)) {
rootPath += File.separator;
}
addFileToZipOut(inFile, out, rootPath);
} finally {
out.close();
}
}
複製程式碼
引數inFile表示輸入,可以是普通檔案或目錄,zipFile表示輸出,rootPath表示父目錄,用於計算每個檔案的相對路徑,主要呼叫了addFileToZipOut將檔案加入到ZipOutputStream中,程式碼為:
private static void addFileToZipOut(File file, ZipOutputStream out,
String rootPath) throws IOException {
String relativePath = file.getCanonicalPath().substring(
rootPath.length());
if (file.isFile()) {
out.putNextEntry(new ZipEntry(relativePath));
InputStream in = new BufferedInputStream(new FileInputStream(file));
try {
copy(in, out);
} finally {
in.close();
}
} else {
out.putNextEntry(new ZipEntry(relativePath + File.separator));
for (File f : file.listFiles()) {
addFileToZipOut(f, out, rootPath);
}
}
}
複製程式碼
它同樣呼叫了copy方法將檔案內容寫入ZipOutputStream,對於目錄,進行遞迴呼叫。
ZipInputStream用於解壓zip檔案,它有一個對應的方法,獲取壓縮條目:
public ZipEntry getNextEntry() throws IOException
複製程式碼
如果返回值為null,表示沒有條目了。使用ZipInputStream解壓檔案,可以使用類似如下程式碼:
public static void unzip(File zipFile, String destDir) throws IOException {
ZipInputStream zin = new ZipInputStream(new BufferedInputStream(
new FileInputStream(zipFile)));
if (!destDir.endsWith(File.separator)) {
destDir += File.separator;
}
try {
ZipEntry entry = zin.getNextEntry();
while (entry != null) {
extractZipEntry(entry, zin, destDir);
entry = zin.getNextEntry();
}
} finally {
zin.close();
}
}
複製程式碼
呼叫extractZipEntry處理每個壓縮條目,程式碼為:
private static void extractZipEntry(ZipEntry entry, ZipInputStream zin,
String destDir) throws IOException {
if (!entry.isDirectory()) {
File parent = new File(destDir + entry.getName()).getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
OutputStream entryOut = new BufferedOutputStream(
new FileOutputStream(destDir + entry.getName()));
try {
copy(zin, entryOut);
} finally {
entryOut.close();
}
} else {
new File(destDir + entry.getName()).mkdirs();
}
}
複製程式碼
小結
本節簡要介紹了五種常見檔案型別的處理:屬性檔案、CSV、EXCEL、HTML和壓縮檔案,介紹了基本用法和更多資訊的參考連結。
至此,關於檔案的所有部分,我們就介紹完了。
從下一節開始,讓我們一起探索併發和執行緒的世界!
(與其他章節一樣,本節所有程式碼位於 github.com/swiftma/pro…)
未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。