首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 解析 VCF 聯繫人檔案並實現資料庫導入導出完整教學

     編輯:Java 相關知識

前言

近期專案需求要求開發一個小功能:將手機的 VCF 通訊錄檔案匯出後導入資料庫,並能從資料庫讀取記錄生成 VCF 檔案,匯入手機後可直接作為通訊錄使用。VCF(vCard)格式規格可參考相關文件,本文將分享完整的 Java 實作細節。

開源庫參考

專案參考了 mime-dir-j 開源專案的原始碼,經過分析後自行實作核心邏輯。

一、Servlet 導出 VCF 檔案

在 Servlet 中透過輸出流動態生成 VCF 內容,關鍵在於正確設定 HTTP 標頭與編碼處理:

response.setHeader("Content-Disposition", "attachment; filename=" 
    + new String(bean.getTrueName().getBytes("GBK"), "ISO8859-1") + ".vcf");

try {
    java.io.PrintWriter bufout = response.getWriter();
    bufout.write("BEGIN:VCARD\r\n");
    bufout.write("VERSION:2.1\r\n");
    // 姓名需經 Quoted-Printable 編碼
    bufout.write("N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:" 
        + AddressServlet.qpEncodeing(bean.getTrueName()) + ";\r\n");
    
    if (bean.getMobile() != null && !bean.getMobile().isEmpty()) {
        bufout.write("TEL;CELL:" + bean.getMobile() + "\r\n");
    }
    if (bean.getWorkMobile() != null && !bean.getWorkMobile().isEmpty()) {
        bufout.write("TEL;WORK:" + bean.getWorkMobile() + "\r\n");
    }
    if (bean.getTelephone() != null && !bean.getTelephone().isEmpty()) {
        bufout.write("TEL;HOME:" + bean.getTelephone() + "\r\n");
    }
    if (bean.getEmail() != null && !bean.getEmail().isEmpty()) {
        bufout.write("EMAIL:" + bean.getEmail() + "\r\n");
    }
    bufout.write("END:VCARD\r\n");
    response.getOutputStream().flush();
    response.getOutputStream().close();
} catch (IOException e) {
    System.out.println("VCF 匯出發生 I/O 錯誤!");
}

關鍵點: qpEncodeing() 方法用於將中文姓名轉為 Quoted-Printable 編碼,確保手機正確識別。實作可參考 此篇文章。

二、Servlet 上傳 VCF 並導入資料庫

使用 Apache Commons FileUpload 處理檔案上傳,限制 2MB 大小,驗證副檔名為 .vcf:

FileItem fi = null;
try {
    FileUpload fu = new FileUpload();
    fu.setSizeMax(2097152); // 2MB
    DiskFileItemFactory factory = new DiskFileItemFactory();
    fu.setFileItemFactory(factory);
    List fileItems = fu.parseRequest(requestContext);
    Iterator i = fileItems.iterator();
    
    while (i.hasNext()) {
        fi = (FileItem) i.next();
        String fileName = fi.getName();
        if (fi.getSize() > 2097152) return "檔案過大";
        if (fileName == null) return "檔案錯誤";
        
        String fileExt = StringUtils.getFileExt(fileName).toLowerCase();
        if ("vcf".equals(fileExt)) {
            Properties p = this.createProperties();
            try {
                ArrayList li = this.importVCFFileContact(fi.getInputStream(), p);
                if (li == null || li.isEmpty()) {
                    return "導入 0 筆,檔案格式可能不正確";
                }
                this.addAddresses(li);
                return "1";
            } catch (IOException e) {
                return "檔案格式發生錯誤";
            }
        } else {
            return "非 VCF 檔案";
        }
    }
} catch (Exception e) {
    if (e.getMessage().indexOf("maximum") > 0) {
        return "導入檔案錯誤,檔案大於 2MB";
    }
    log.debug(e.getMessage());
    return "導入檔案發生錯誤: " + e.getMessage();
} finally {
    try { if (fi != null) fi.delete(); } catch (Exception ignored) {}
}
return "發生錯誤";

三、VCF 內容解析為 Java Bean

核心解析邏輯使用正則表達式提取每張 vCard 的欄位,支援多筆聯繫人:

public ArrayList importVCFFileContact(InputStream in, Properties pp) throws SystemException {
    ArrayList abs = new ArrayList();
    try {
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        StringBuffer bu = new StringBuffer();
        String line;
        while ((line = DecorGroup.nextLine(reader)) != null) {
            bu.append(line).append("\r\n");
        }
        // 以 BEGIN:VCARD ... END:VCARD 分組
        Pattern p = Pattern.compile("BEGIN:VCARD(\\r\\n)([\\s\\S\\r\\n\\.]*?)END:VCARD");
        Matcher m = p.matcher(bu.toString());
        while (m.find()) {
            AddressBean be = new AddressBean();
            be.setCateId(gb.getId());
            be.setUserId(gb.getUserId());
            String vcard = m.group(0);
            
            // 解析姓名(支援 QUOTED-PRINTABLE 與 CHARSET)
            Pattern pn = Pattern.compile("N;([\\s\\S\\r\\n\\.]*?)([\\r\\n])");
            Matcher mn = pn.matcher(vcard);
            while (mn.find()) {
                String name = "";
                if (mn.group(1).indexOf("ENCODING=QUOTED-PRINTABLE") > -1) {
                    name = mn.group(1).substring(
                        mn.group(1).indexOf("ENCODING=QUOTED-PRINTABLE:") + "ENCODING=QUOTED-PRINTABLE:".length());
                    name = name.substring(name.indexOf(":") + 1);
                    if (name.indexOf(";") > -1) name = name.substring(0, name.indexOf(";"));
                    be.setTrueName(AddressServlet.qpDecoding(name));
                } else {
                    Pattern pnn = Pattern.compile("CHARSET=([A-Za-z0-9-]*?):");
                    Matcher mnn = pnn.matcher(mn.group(1));
                    while (mnn.find()) {
                        name = mn.group(1).substring(
                            mn.group(1).indexOf(mnn.group(0)) + mnn.group(0).length());
                        be.setTrueName(name);
                    }
                }
            }
            if (be.getTrueName().length() > 20) return null; // 防禦性檢查
            
            // 解析手機、公司電話、住宅電話、Email
            be.setMobile(extractField(vcard, "TEL;CELL:"));
            be.setWorkMobile(extractField(vcard, "TEL;WORK:"));
            be.setTelephone(extractField(vcard, "TEL;HOME:"));
            be.setEmail(extractEmail(vcard));
            
            abs.add(be);
        }
        reader.close();
    } catch (Exception e) {
        log.debug(e.getMessage());
        throw new SystemException("系統忙碌,請稍後再試!");
    }
    return abs;
}

// 輔助方法:提取單一欄位
private String extractField(String vcard, String prefix) {
    Pattern p = Pattern.compile(prefix + "\\d*");
    Matcher m = p.matcher(vcard);
    String val = "";
    while (m.find()) {
        val = m.group(0).substring(prefix.length());
    }
    return val;
}

// 輔助方法:提取 Email
private String extractEmail(String vcard) {
    Pattern p = Pattern.compile("\\w+(\\.\\w+)*@\\w+(\\.\\w+)+");
    Matcher m = p.matcher(vcard);
    String val = "";
    while (m.find()) val = m.group(0);
    return val;
}

四、行續行處理工具方法

VCF 規範允許長行以空白或 Tab 續行,需正確還原:

public static String nextLine(BufferedReader reader) throws IOException {
    String line, nextLine;
    do {
        line = reader.readLine();
        if (line == null) return null;
    } while (line.length() == 0);
    
    // Evolution 風格:以 = 結尾續行
    while (line.endsWith("=")) {
        line = line.substring(0, line.length() - 1) + reader.readLine();
    }
    // RFC 2425 風格:下一行以空白或 Tab 開頭
    reader.mark(1000);
    nextLine = reader.readLine();
    if (nextLine != null && nextLine.length() > 0 
        && (nextLine.charAt(0) == 0x20 || nextLine.charAt(0) == 0x09)) {
        line += nextLine.substring(1);
    } else {
        reader.reset();
    }
    return line.trim();
}

五、實測相容性說明

程式已在 Nokia、Samsung、Motorola、波導等品牌手機實測通過。因各廠牌 VCF 欄位可能略有差異,本實作僅提取共通欄位:姓名、行動電話、住宅電話、公司電話、Email。如需擴充欄位,可依樣畫葫蘆增加正則解析。

結語

完整程式碼已整理於上,若有疑歡迎留言討論。掌握 VCF 解析與生成技巧,即可輕鬆實現手機通訊錄與系統資料庫的雙向同步。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved