一、概述
海外APP,经常会翻译国外语言,项目中间突然加入一门新语言,一般海外市场部会提供一个翻译好的Excel表格,里边包含了所有的字符串和对应的翻译,如下所示:
如果项目不断迭代,对应的字符串也可能会有N多个表。手动翻译翻译,需要在项目的strings.xml文件和Excel表格中来回切换,逐条搜索替换,很不方便,使用java语言,来操作Excel表格,代码处理,可以减少了繁琐劳动,降低出错率。
需要考虑的问题很多,大致如下:
1.使用java,怎么操作Excel文件,不仅要读取,匹配好的数据也要放入一个表格里,此时就涉及到了写
2.操作时,对Excel文件的格式有个要求,不同格式的Excel文件处理时,有何不同
3.第一次处理,共用到三个Excel文件:
1). 翻译好的文件,即源文件,ori.xls。里边都是单纯的翻译,不包含<string name=""></string>这种样式。
2). 复制strings.xml内容到Excel文件中,需要比对的文件,des.xls。它是含有<string name="nameStr">contentStr</string>的字符串。
3). 以翻译乌克兰语为例,得到ori.xls里C、Q两列的内容后,再获取des.xls中每个单元格的内容,解析出nameStr和contentStr。根据contentStr,和ori.xls里的结果匹配比较,找到contentStr在ori.xls中的行列坐标,这样就可以推知乌克兰语的行列坐标。重新组装为<string name="nameStr">translate content</string>,放入结果文件result.xls中。
4. 此时要考虑des.xls中每条string的内容格式和结果文件result.xls的内容格式。
1).怎么截取name和content部分,最终格式为: <string name="nameStr">translate content</string>
2).该条若注释掉了,直接跳过
3).获取到content后,要判断是否被双引号""包围,若没被双引号包围,出现了 ' 要转义为 \'
4).content中若包含非英语形式的特殊符号,如?!-等,要替换处理
5). Excel文件中的&在xml中表示为 &,也要处理
等等。
二、读Excel文件
// 创建file对象
File file = new File(filePath);
// 创建输入流,读取Excel
InputStream is = new FileInputStream(file);
// jxl提供的Workbook类
Workbook wb = Workbook.getWorkbook(is);
//获取第一个sheet
Sheet sheet = wb.getSheet(0);
Log.d(TAG, "readOriExcel: sheet name = "+ sheet.getName());
// 得到所有的行数
int rows = sheet.getRows();
for (int j = 0; j < rows; j++) {
// 得到这行每个单元格的数据
Cell[] cells = sheet.getRow(j);
}
三、写Excel文件
/**
* 将数据写入到excel中
*/
public static void writeExcel() {
//1 创建一个workbook对应一个excel文件
HSSFWorkbook workbook = new HSSFWorkbook();
//2 在workbook中创建一个sheet对应excel中的sheet
HSSFSheet sheet = workbook.createSheet(name);
for (int i = 0; i < desSheetAllData.size(); i++) {
HSSFRow row = sheet.createRow(i);
List<String> rowData = desSheetAllData.get(i);
for (int j = 0; j < rowData.size(); j++) {
//3 创建单元格并设值
row.createCell(j).setCellValue(rowData.get(j));
}
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(PATH+"/result.xls");
workbook.write(fos);
} catch (IOException e) {
Log.d(TAG, "writeExcel: catch - " + e.getMessage());
} finally {
try {
//数据量较大,使用flush刷新该流的缓冲区
//一般写字符时要用,因为字符是先进入的缓冲区
fos.flush();
//关闭流对象
fos.close();
} catch (IOException e) {
Log.d(TAG, "writeExcel: finally -" + e.getMessage());
}
}
Log.d(TAG, "writeExcel: success!!!");
}
注:此处使用HSSFWorkbook创建的是xls文件,若创建xlsx文件,要用XSSFWorkbook。 HSSFWorkbook和XSSFWorkbook 类都实现了Workbook接口。
四、解析字符串 <string name="nameStr">contentStr</string>
1.利用正则表达式"<[^>]+>"解析出contentStr。
String str = "<string name=\"nameStr\">contentStr</string>";
// 定义xml标签的正则表达式
String regExp = "<[^>]+>";
// 过滤xml标签
String contentStr = Pattern.compile(regExp).matcher(str).replaceAll("");
2. 利用"\">"来解析出<string name="nameStr来。
String[] split = str.split("\">");
String nameStr = split[0];
根据contentStr找到对应的翻译以后,再把<string name="nameStr补充完整:<string name="nameStr">translate content</string>
五、处理要替换的内容contentStr
1. 汉语中的?!- 等,使用replaceAll()方法。注意,调用replaceAll()方法的使用方式:
content = content.replaceAll("\'", "'");
必须接收一次replaceAll返回的字符串,这样,才是处理后,想要的结果。
2. 若想得到某个字符,如-的ASIIC码用(int)('-')就可以得到。
3. 去掉首尾的引号,使用substring()方法。
content = content.substring(1, content.length() - 1);//去掉首尾的引号
str.length()-1为字符串最后一位的下标,它作为substring()的参数2,为开区间,表示不包含最后一位。参数1位闭区间,包含第一位。故:
String test = "0123456";
Log.d(TAG, "lym12345: " + test.substring(0, test.length()-1));// --> 012345
Log.d(TAG, "lym12345: " + test.substring(1, test.length()-2));// --> 1234
4. 若contentStr中含有\",比如:open \"location\",在英语和乌克兰语匹配过程中,是以open "location"来匹配的,此时要把\"替换为",即把\去掉
//处理句中的\"
if (content.contains("\\\"")){
content = content.replaceAll("\\\\", "");
}
java中,\用\\表示,"用\"表示,\"则用\\\"表示。正则表达式中"\\\\"表示\。
\' 替换 ',\" 替换 ":
//没有双引号的时候,遇到'再用\'替换,遇到"再用\"替换
if (content.contains("'")) {
content = content.replace("'", "\\'");
} else if (content.contains("\"")){
content = content.replace("\"", "\\\"");
}
5. 去除字符串前后的空格,java的string.trim()只能去英文半角空格
public class StringUtil {
/**普通的英文半角空格Unicode编码*/
private static final int SPACE_32 = 32;
/**中文全角空格Unicode编码(一个中文宽度)*/
private static final int SPACE_12288 = 12288;
/**普通的英文半角空格但不换行Unicode编码(== ==   == no-break space)*/
private static final int SPACE_160 = 160;
/**半个中文宽度(==   == en空格)*/
private static final int SPACE_8194 = 8194;
/**一个中文宽度(==   == em空格)*/
private static final int SPACE_8195 = 8195;
/**四分之一中文宽度(四分之一em空格)*/
private static final int SPACE_8197 = 8197;
/**窄空格*/
private static final int SPACE_8201 = 8201;
/**
* 去除字符串前后的空格, 包括半角空格和全角空格(中文)等各种空格, java的string.trim()只能去英文半角空格
* @param str
*/
public static String trim(String str) {
if (TextUtils.isEmpty(str)) {
return str;
}
char[] val = str.toCharArray();
int st = 0;
int len=val.length;
while ((st < len) && isSpace(val[st])) {
st++;
}
while ((st < len) && isSpace(val[len - 1])) {
len--;
}
return ((st > 0) || (len < val.length)) ? str.substring(st, len) : str;
}
private static boolean isSpace(char aChar) {
return aChar == SPACE_32 || aChar == SPACE_12288 || aChar == SPACE_160 || aChar == SPACE_8194
|| aChar == SPACE_8195 || aChar == SPACE_8197 || aChar == SPACE_8201;
}
}
六、其他
1. 每次写入时有上限,不要多余150行;
2. 为便于查看,Excel中不要空行;
3. 读写SD卡里的文件,打开操作后的Excel,提示: 无法打开文件,可以尝试重启手机、AndroidStudio
七、参考
HSSFworkbook,XSSFworkbook,SXSSFworkbook区别总结
八、代码附上
public class Excel {
private static final String TAG = "lym1234";
private static String PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/xml/";
private static String DES_FILE = "des.xls";
private static String ORI_FILE = "ori.xls";
private static int indexR = 0;
private static int indexC = 0;
private static final int DOT = 1;//.
private static final int QUESTION_MARK = 2;//?
private static final int POINT = 3;//!
private static List<List<String>> oriSheetAllData = new ArrayList<>(); // ori所有的数据
private static List<List<String>> desSheetAllData = new ArrayList<>(); // des所有的数据
public static void operate() {
try {
oriSheetAllData.clear();
desSheetAllData.clear();
readOriExcel();
readDesExcel();
writeExcel();
} catch (Exception e) {
Log.d(TAG, "e: " + e.getMessage());
}
}
private static void readOriExcel() throws Exception {
// 读取Excel文件
File file = new File(PATH+ORI_FILE);
// 创建输入流,读取Excel
InputStream is = new FileInputStream(file.getAbsolutePath());
// jxl提供的Workbook类
Workbook wb = Workbook.getWorkbook(is);
int sheetNum = wb.getNumberOfSheets();
Log.d(TAG, "readOriExcel: sheetNum = " + sheetNum);
for (int i = 0; i < sheetNum; i++){
Sheet sheet = wb.getSheet(i);//获取每个sheet
Log.d(TAG, "readOriExcel: sheet name = "+ sheet.getName());
// 得到所有的行数
int rows = sheet.getRows();
Log.d(TAG, "readOriExcel: rows = " + rows);
// 越过第一行 它是列名称
for (int j = 1; j < rows; j++) {
List<String> oneData = new ArrayList<>();
// 得到每一行的单元格的数据
Cell[] cells = sheet.getRow(j);
Log.d(TAG, "readOriExcel: cols = " + cells.length);
for (int k = 0; k < cells.length; k++) {
// 读取每行的第3/17/18列元素
// 若Excel表格中,第5列的数据全部为空,则第一行的第5个单元格要填上数据,不能为空。
// 否则,从第二行开始,每行下标为4的数据应该为空,就会变成第6个单元格的内容,
// 且读取的总长度会减1
if (2 == k || 16 == k || 17 == k) {
oneData.add(StringUtil.trim(cells[k].getContents()));
}
}
// 存储每一条数据
oriSheetAllData.add(oneData);
}
}
}
private static void readDesExcel() throws Exception{
boolean beginWithDoubleQuotes = false;//被双引号包围
boolean haveDoubleQuotes = false;//句中有双引号
int endWithSpecialChar = 0;
// 读取Excel文件
File file = new File(PATH+DES_FILE);
// 创建输入流,读取Excel
InputStream is = new FileInputStream(file.getAbsolutePath());
// jxl提供的Workbook类
Workbook wb = Workbook.getWorkbook(is);
Sheet sheet = wb.getSheet(0);//获取每个sheet
Log.d(TAG, "readOriExcel: sheet name = "+ sheet.getName());
// 得到所有的行数
int rows = sheet.getRows();
Log.d(TAG, "readOriExcel: rows = " + rows);
Log.d(TAG, "readDesExcel: " + (int)('–') + " : " + (int)('-'));
for (int j = 0; j < rows; j++) {
List<String> oneData = new ArrayList<>();
// 得到每一行的单元格的数据
Cell[] cells = sheet.getRow(j);
if (0 == cells.length){//空的单元格
oneData.add(" ");
oneData.add(" ");
continue;
}
//Log.d(TAG, "readOriExcel: cols = " + cells.length);
for (int k = 0; k < 1/*cells.length*/; k++) {
String str = StringUtil.trim(cells[k].getContents().trim());
if (str.startsWith("<!--") || TextUtils.isEmpty(str)){
Log.d(TAG, "readDesExcel: 注释 or empty - " + str);
oneData.add(str);//注释掉的和空的,原样输出,占位用
oneData.add(str);
continue;
}
String[] split = str.split("\">");
//Log.d(TAG, "readDesExcel: " + split[0] + split[1]);
String name = split[0];
String regExpContent = "<[^>]+>";
String content = Pattern.compile(regExpContent).matcher(str).replaceAll("");
Log.d(TAG, "readDesExcel: content1 = " + content);
if (content.equalsIgnoreCase("\"\"")){
Log.d(TAG, "readDesExcel: content is \"\"");
oneData.add(str);
oneData.add(str);
continue;
}
//是否是双引号开头
if (content.startsWith("\"")){
beginWithDoubleQuotes = true;
content = content.substring(1, content.length() - 1);//去掉首尾的引号
} else {
//去掉转义字符\
beginWithDoubleQuotes = false;
if (content.contains("\'")){
content = content.replaceAll("\'", "'");
}
}
//是否以.?!等结尾
if (content.contains("?")){//替换中文下的?!
content = content.replaceAll("?", "?");
}
if (content.contains("!")){
content = content.replaceAll("!", "!");
}
if (content.endsWith(".") || content.endsWith("?") || content.endsWith("!")){
endWithSpecialChar = content.endsWith(".") ?
DOT : (content.endsWith("?") ? QUESTION_MARK : POINT);
content = content.substring(0, content.length()-1);
} else {
endWithSpecialChar = 0;
}
//替换汉字中的-
if (content.contains("–")){
content = content.replaceAll("–", "-");
}
//替换&
if (content.contains("&")){
content = content.replaceAll("&", "&");
}
//去掉,后的空格
if (content.contains(",")){//替换中文下的,
content = content.replaceAll(",", ",");
}
if (content.contains(", ")){
content = content.replaceAll(", ", ",");
}
//处理句中的\"
if (content.contains("\\\"")){
haveDoubleQuotes = true;
content = content.replaceAll("\\\\", "");
} else {
haveDoubleQuotes = false;
}
Log.d(TAG, "readDesExcel: content2 = " + content);
handlerStr(content);
if (0 == indexR || 0 != indexC){//未找到对应的翻译
Log.d(TAG, "readDesExcel: indexR == 0 or indexC = " + indexC);
oneData.add(str);
oneData.add(str);
continue;
}
String ukStr = getContentStr(name, beginWithDoubleQuotes,
haveDoubleQuotes, endWithSpecialChar, 1);
Log.d(TAG, "readDesExcel: ukStr = " + ukStr);
String roStr = getContentStr(name, beginWithDoubleQuotes,
haveDoubleQuotes, endWithSpecialChar, 2);
Log.d(TAG, "readDesExcel: roStr = " + roStr);
oneData.add(ukStr);
oneData.add(roStr);
}
// 存储每一条数据
desSheetAllData.add(oneData);
}
// 打印出每一条数据
//Log.d(TAG, desSheetAllData.toString().trim());
}
//两个文件的原字符串比较,获取字符串的行下标
private static void handlerStr(String content) {
indexR = 0;
indexC = 0;
boolean find = false;
for (int i = 0; i < oriSheetAllData.size(); i++){
if (find){
break;
}
for (int j = 0; j < oriSheetAllData.get(i).size(); j++) {
String oriStr = oriSheetAllData.get(i).get(j).trim();
if (oriStr.startsWith("\"")){//被双引号包围
oriStr = oriStr.substring(1, oriStr.length()-1);
}
//是否以.?!等结尾
if (oriStr.contains("?")){//替换中文下的?!
oriStr = oriStr.replaceAll("?", "?");
}
if (oriStr.contains("!")){
oriStr = oriStr.replaceAll("!", "!");
}
if (oriStr.endsWith(".") || oriStr.endsWith("?") || oriStr.endsWith("!")) {//以.结尾
oriStr = oriStr.substring(0, oriStr.length() - 1);
}
//去掉,后的空格
if (oriStr.contains(",")){//替换中文下的,
oriStr = oriStr.replaceAll(",", ",");
}
if (oriStr.contains(", ")){
oriStr = oriStr.replaceAll(", ", ",");
}
//处理句中的\"
if (oriStr.contains("\\\"")) {
//oriStr = oriStr.replaceAll("\\\"", "\"");
content = content.replaceAll("\\\\", "");
}
if (oriStr.equalsIgnoreCase(content)) {
indexR = i;
indexC = j;
find = true;
break;
} else {
find = false;
}
}
}
//Log.d(TAG, "handlerStr: " + oriSheetAllData.get(indexR));
//Log.d(TAG, "handlerStr: " + oriSheetAllData.get(indexR).get(indexC));
}
//获取翻译的字符串 乌克兰
private static String getContentStr(String name, boolean beginWithDoubleQuotes,
boolean haveDoubleQuotes, int endWithSpecialChar, int i){
Log.d(TAG, "getContentStr: name = " + name + ", double = " + beginWithDoubleQuotes);
String content = oriSheetAllData.get(indexR).get(indexC+i);
//替换中文下的?!
if (content.contains("?")){
content = content.replaceAll("?", "?");
}
if (content.contains("!")){
content = content.replaceAll("!", "!");
}
//替换&,xml文件才可以识别
if (content.contains(" & ")) {
content = content.replaceAll(" & ", " & ");
}
//被双引号包围
if (!beginWithDoubleQuotes) {
//没有双引号的时候,遇到'再用\'替换,遇到"再用\"替换
if (content.contains("'")) {
content = content.replace("'", "\\'");
} else if (content.contains("\"")){
content = content.replace("\"", "\\\"");
}
//以特殊符号结尾
if (DOT == endWithSpecialChar && !content.endsWith(".")){
content = content.concat(".");
}
if (QUESTION_MARK == endWithSpecialChar && !content.endsWith("?")){
content = content.concat("?");
}
if (POINT == endWithSpecialChar && !content.endsWith("!")){
content = content.concat("!");
}
} else {
//句中有双引号,还被双引号包围,此时,句中的双引号要带\
//若都带\",则不需要处理
// if (content.){
// content.contains("")
// }
//有双引号,则补上
if (!content.startsWith("\"")) {
content = "\"" + content + "\"";
}
}
//Log.d(TAG, "getContentStr: content = " + content);
String contentStr = name+"\">"+content+"</string>";
//Log.d(TAG, "getContentStr: contentStr = " + contentStr);
return contentStr;
}
private static boolean test = false;
/**
* 将数据写入到excel中
*/
public static void writeExcel() {
Date d = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("HH_mm_ss");
String timeStr = sdf.format(d);
//System.out.println("格式化后的日期:" + dateNowStr);
//第一步,创建一个workbook对应一个excel文件
HSSFWorkbook workbook = new HSSFWorkbook();
//第二部,在workbook中创建一个sheet对应excel中的sheet
HSSFSheet sheet = workbook.createSheet(timeStr);
if (test){
//第三部,在sheet表中添加表头第0行,老版本的poi对sheet的行列有限制
HSSFRow row = sheet.createRow(0);
//第四步,创建单元格,设置表头
HSSFCell cell = row.createCell(0);
cell.setCellValue("date1");
cell = row.createCell(1);
cell.setCellValue("month1");
cell = row.createCell(2);
cell.setCellValue("weekday1");
cell = row.createCell(3);
cell.setCellValue("毫升/单位1");
} else {
Log.d(TAG, "writeExcel: rowCnt = " + desSheetAllData.size());
for (int i = 0; i < desSheetAllData.size(); i++) {
Log.d(TAG, "writeExcel: row " + i);
HSSFRow row = sheet.createRow(i);
List<String> rowData = desSheetAllData.get(i);
for (int j = 0; j < rowData.size(); j++) {
//创建单元格设值
Log.d(TAG, "writeExcel: col " + j + " : " + rowData.get(j));
row.createCell(j).setCellValue(rowData.get(j));
}
}
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(PATH+"/result.xls");
workbook.write(fos);
} catch (IOException e) {
Log.d(TAG, "writeExcel: catch - " + e.getMessage());
} finally {
try {
//数据量较大,使用flush刷新该流的缓冲区
//一般写字符时要用,因为字符是先进入的缓冲区
fos.flush();
//关闭流对象
fos.close();
} catch (IOException e) {
Log.d(TAG, "writeExcel: finally -" + e.getMessage());
}
}
Log.d(TAG, "writeExcel: success!!!");
}
}