开发文章

用Java来进行文件切割和简单的内容过滤

一 由来

去年由于项目的需求,要将一个任意一个文件制作成一个xml文件,并且需要保持文件内容本身不产生变化,还要能够将这个xml重新还原为原文件。如果小型的文件还好处理,大型的xml,比如几个G的文件,基本上就OOM了,很难直接从节点中提取数据。所以我采用了流的方式。于是有了这个文件的裁剪工具。

二 使用场景

本工具可能的使用场景:
1.对任一文件的切割/裁剪。通过字节流的方式,开始节点和终止节点,裁剪出两个节点之间的部分。
2.往任一文件的头/尾拼接指定字符串。可以很容易将一个文件嵌入在某一个节点中。
3.简单的文本抽取。可以根据自己定义的规则,提取出来想要的文本内容,并且允许对提取出来的文本进行再处理(当然,只是进行简单地抽取文字,并不是什么智能的复杂过程的抽取T_T )。
4.文本过滤。根据自己制定的规则,过滤掉指定的文字。

整个工具仅是对Java文件操作api的简单加工,并且也没有使用nio。在需要高效率的文件处理情景下,本工具的使用有待考量。文章目的是为了给出自己的一种解决方案,若有更好的方案,欢迎大家给出适当的建议。

三 如何使用

别的先不说,来看看如何使用吧!

1.读取文件指定片段
读取第0~1048个字节之间的内容。

复制内容到剪贴板
  1. public void readasbytes(){  
  2.         FileExtractor cuter = new FileExtractor();  
  3.         byte[] bytes = cuter.from("D:\\11.txt").start(0).end(1048).readAsBytes();  
  4.     }  

2.文件切割
将第0~1048个字节之间的部分切割为一个新文件。

复制内容到剪贴板
  1. public File splitAsFile(){  
  2.         FileExtractor cuter = new FileExtractor();  
  3.         return cuter.from("D:\\11.txt").to("D:\\22.txt").start(0).end(1048).extractAsFile();  
  4.     }  

3.将文件拼接到一个xml节点中
将整个文件的内容作为Body节点,写入到一个xml文件中。返回新生成的xml文件对象。

复制内容到剪贴板
  1. public File appendText(){  
  2.   
  3.       FileExtractor cuter = new FileExtractor();  
  4.       return cuter.from("D:\\11.txt").to("D:\\44.xml").appendAsFile("<Document><Body>""</Body></Document>");  
  5.   
  6.   }  

4.读取并处理文件中的指定内容
假如有需求:读取11.txt的前三行文字。其中,第一行和第二行不能出现”帅”字,并且在第三行文字后加上字符串“我好帅!”。

复制内容到剪贴板
  1. public String  extractText(){  
  2.       FileExtractor cuter = new FileExtractor();  
  3.       return cuter.from("D:\\11.txt").extractAsString(new EasyProcesser() {  
  4.           @Override  
  5.           public String finalStep(String line, int lineNumber, Status status) {  
  6.   
  7.               if(lineNumber==3){  
  8.                   status.shouldContinue = false;//表示不再继续读取文件内容  
  9.                   return line+"我好帅!";  
  10.               }  
  11.               return line.replaceAll("帅","");  
  12.           }  
  13.       });  
  14.   
  15.   }  

4.简单的文本过滤
将一个文件中所有的“bug”去掉,且返回一个处理后的新文件。

复制内容到剪贴板
  1. public File killBugs(){  
  2.      FileExtractor cuter = new FileExtractor();  
  3.      return cuter.from("D:\\bugs.txt").to("D:\\nobug.txt").extractAsFile(new EasyProcesser() {  
  4.          @Override  
  5.          public String finalStep(String line, int lineNumber, Status status) {  
  6.              return line.replaceAll("bug""");  
  7.          }  
  8.      });   
  9.  }  

四 基本流程

通过接口回调的方式,将文件的读取过程和处理过程分离开来;定义了IteratorFile类来负责遍历一个文件,读取文件的内容;分字节、行两种的方式来进行文件内容的遍历。下面的介绍,也会分为读取和处理两个部分单独介绍。

五 文件的读取

定义回调接口

定义一个接口Process,对外暴露了两个文件内容处理方法,一个支持按字节进行读取,一个方法支持按行读取。

复制内容到剪贴板
  1. public interface Process{  
  2.   
  3.     /** 
  4.      * @param b 本次读取的数据 
  5.      * @param length 本次读取的有效长度 
  6.      * @param currentIndex 当前读取到的位置 
  7.      * @param available 读取文件的总长度 
  8.      * @return true 表示继续读取文件,false表示终止读取文件 
  9.      * @time 2017年1月22日 下午4:56:41 
  10.      */  
  11.     public boolean doWhat(byte[] b,int length,int currentIndex,int available);  
  12.   
  13.     /** 
  14.      *  
  15.      * @param line 本次读取到的行 
  16.      * @param currentIndex 行号 
  17.      * @return true 表示继续读取文件,false表示终止读取文件 
  18.      * @time 2017年1月22日 下午4:59:03 
  19.      */  
  20.     public boolean doWhat(String line,int currentIndex);  

让ItratorFile中本身实现这个接口,但是默认都是返回true,不做任何的处理。如下所示:

复制内容到剪贴板
  1. public  class IteratorFile implements Process  
  2. {  
  3. ......  
  4. /** 
  5.      * 按照字节来读取遍历文件内容,根据自定义需要重写该方法 
  6.      */  
  7.     @Override  
  8.     public boolean doWhat(byte[] b, int length,int currentIndex,int available) {  
  9.         return true;  
  10.     }  
  11.   
  12.     /** 
  13.      * 按照行来读取遍历文件内容,根据自定义需要重写该方法 
  14.      */  
  15.     @Override  
  16.     public boolean doWhat(String line,int currentIndex) {  
  17.         return true;  
  18.     }  
  19. ......  
  20. }  

按字节遍历文件内容

实现按照字节的方式来进行文件的遍历(读取)。在这里使用了skip()方法来控制从第几个节点开始读取内容;然后在使用文件流读取的时候,将每次读取到得数据传递给回调接口的方法;需要注意的是,每次读取到得数据是存在一个字节数组bytes里面的,每次读取的长度也是需要传递给回调接口的。我们很容易看出,一旦dowhat()返回false,文件的读取立即就退出了。

复制内容到剪贴板
  1. public void iterator2Bytes(){  
  2.         init();  
  3.         int length = -1;  
  4.         FileInputStream fis = null;  
  5.         try {  
  6.             file = new File(in);  
  7.             fis = new FileInputStream(file);  
  8.             available = fis.available();  
  9.             fis.skip(getStart());  
  10.             readedIndex = getStart();  
  11.             if (!beforeItrator()) return;  
  12.             while ((length=fis.read(bytes))!=-1) {  
  13.                 readedIndex+=length;  
  14.                 if(!doWhat(bytes, length,readedIndex,available)){  
  15.                     break;  
  16.                 }  
  17.             }  
  18.             if(!afterItrator()) return;  
  19.         } catch (FileNotFoundException e) {  
  20.             e.printStackTrace();  
  21.         } catch (IOException e) {  
  22.             e.printStackTrace();  
  23.         }finally{  
  24.             try {  
  25.                 fis.close();  
  26.             } catch (IOException e) {  
  27.                 e.printStackTrace();  
  28.             }  
  29.         }  
  30.     }  

按行来遍历文件内容

常规的文件读取方式,在while循环中,调用了回调接口的方法,并且传递相关的数据。

复制内容到剪贴板
  1. public void iterator2Line(){  
  2.        init();  
  3.        BufferedReader reader = null;  
  4.        FileReader read = null;  
  5.        String line = null;  
  6.        try {  
  7.            file = new File(in);  
  8.            read = new FileReader(file);  
  9.            reader = new BufferedReader(read);  
  10.            if (!beforeItrator()) return;  
  11.            while ( null != (line=reader.readLine())) {  
  12.                readedIndex++;  
  13.                if(!doWhat(line,readedIndex)){  
  14.                    break;  
  15.                }  
  16.            }  
  17.            if(!afterItrator()) return ;  
  18.        } catch (FileNotFoundException e) {  
  19.            e.printStackTrace();  
  20.        } catch (IOException e) {  
  21.            e.printStackTrace();  
  22.        }finally{  
  23.            try {  
  24.                read.close();  
  25.                reader.close();  
  26.            } catch (IOException e) {  
  27.                e.printStackTrace();  
  28.            }  
  29.        }  
  30.    }  

然后,还需要提供方法来设置要读取的源文件路径。

复制内容到剪贴板
  1. public IteratorFile from(String in){  
  2.     this.in = in;  
  3.     return this;  
  4. }  

六 文件内容处理

FileExtractor介绍

定义了FileExtractor类,来封装对文件内容的处理操作;该类会引用到遍历文件所需要的类IteratorFile。

FileExtractor的基本方法

复制内容到剪贴板
  1.    /** 
  2.      * 往文件头或者文件结尾插入字符串 
  3.      * @tips 不能对同一个文件输出路径反复执行该方法,否则会出现文本异常,因为用到了RandomAccessFile,如有需要,调用前需手动删除原有的同名文件 
  4.      * @param startStr 文件开头要插入的字符串 
  5.      * @param endStr 文件结尾要插入的字符串 
  6.      * @return 生成的新文件 
  7.      * @time 2017年1月22日 下午5:05:35 
  8.      */  
  9.     public File appendAsFile(final String startStr,String endStr){}  
  10.   
  11.   
  12. /** 
  13.      * 从指定位置截取文件 
  14.      * @tips 适合所有的文件类型 
  15.      * @return 
  16.      * @time 2017年1月22日 下午5:06:36 
  17.      */  
  18.     public File splitAsFile(){}  
  19.   
  20.   
  21. /** 
  22.      * 文本文件的特殊处理(情景:文本抽取,文本替换等) 
  23.      * @tips 只适合文本文件,对于二进制文件,因为换行符的原因导致文件出现可能无法执行等问题。 
  24.      * @time 2017年1月22日 下午5:09:14 
  25.      */  
  26.     public File extractAsFile(FlowLineProcesser method) {  
  27.   
  28.   
  29. /** 
  30.      * 文本文件的特殊处理(情景:文本抽取,文本替换等) 
  31.      * @tips 只适合文本文件,对于二进制文件,因为换行符的原因导致文件出现可能无法执行等问题。 
  32.      * @time 2017年1月22日 下午5:09:14 
  33.      */  
  34.     public String extractAsString(FlowLineProcesser method) {}  
  35.   
  36.     /** 
  37.      * 读取指定位置的文件内容为字节数组 
  38.      * @return 
  39.      * @time 2017年1月23日 上午11:06:18 
  40.      */  
  41.     public byte[] readAsBytes(){}  

其中,返回值为File的方法在处理完成后,都出返回一个经过内容后的新文件。

其他方法

同样,设置源文件位置的方法,以及截取位置的相关方法

复制内容到剪贴板
  1. /** 
  2.    * 设置源文件 
  3.    */  
  4.   public FileExtractor from(String in){  
  5.       this.in = in;  
  6.       return this;  
  7.   }  
  8.   
  9.   /** 
  10.    * 设置生成临时文件的位置(返回值为File的方法均需要设置) 
  11.    */  
  12.   public FileExtractor to(String out) {  
  13.       this.out = out;  
  14.       return this;  
  15.   }  
  16.   
  17.   /** 
  18.    * 文本开始截取的位置(包含此位置),字节相关的方法均需要设置 
  19.    */  
  20.   public FileExtractor start(int start){  
  21.       this.startPos = start;  
  22.       return this;  
  23.   }  
  24.   
  25.   /** 
  26.    * 文本截取的终止位置(包含此位置),字节相关方法均需要设置 
  27.    */  
  28.   public FileExtractor end(int end) {  
  29.       this.endPos = end;  
  30.       return this;  
  31.   }  

按字节读取文件时的文件内容处理

有几个重点:

1.因为要根据字节的位置来进行文件截取,所以需要根据字节来遍历文件,所以要重写doWhat()字节遍历的的方法。并在外部构造一个OutPutStream来进行新文件的写出工作。

2.每次遍历读取出的文件内容,都存放在一个字节数组b里面,但并不是b中的数据都是有用的,所以需要传递b有效长度length。

3.readedIndex记录了到本次为止(包括本次)为止,已经读取了多少位数据。

4.按照自己来遍历文件时,如何判断读取到了的终止位置?
当(已读的数据总长度)readedIndex>endPos(终止节点)时,说明本次读取的时候超过了应该终止的位置,此时b数组中有一部分数据就是多读的了,这部分数据是不应该被保存的。我们可以通过计算得到读超了多少位,即length-(readedIndex-endPos-1),那么只要保存这部分数据就可以了。

读取指定片段的文件内容:

复制内容到剪贴板
  1. //本方法在需要读取的数据多时,不建议使用,因为byte[]是不可变的,多次读取的时候,需要进行多次的byete[] copy过程,效率“感人”。  
  2. public byte[] readAsBytes(){  
  3.   
  4.     try {  
  5.         checkIn();  
  6.     } catch (Exception e) {  
  7.         e.printStackTrace();  
  8.         return null;  
  9.     }  
  10.   
  11.     //临时保存字节的容器  
  12.     final BytesBuffer buffer = new BytesBuffer();  
  13.   
  14.     IteratorFile c = new IteratorFile(){  
  15.         @Override  
  16.         public boolean doWhat(byte[] b, int length, int currentIndex,  
  17.                 int available) {  
  18.             if(readedIndex>endPos){  
  19.                 //说明已经读取到了endingPos位置并且读超了  
  20.                 buffer.addBytes(b, 0, length-(readedIndex-endPos-1)-1);  
  21.                 return false;  
  22.             }else{  
  23.                 buffer.addBytes(b, 0, length-1);  
  24.             }  
  25.             return true;  
  26.         }  
  27.     };  
  28.     //按照字节进行遍历  
  29.     c.from(in).start(startPos).iterator2Bytes();  
  30.   
  31.     return buffer.toBytes();  
  32.   
  33. }  

当文件很大时,生成一个新的文件的比较靠谱的方法,所以,类似直接返回byte[],在文件读取之前,设置一个outputSteam,在内容循环读取的过程中,将读取的内容写入到一个新文件中去。

复制内容到剪贴板
  1. public File splitAsFile(){  
  2.       ......  
  3.       final OutputStream os = FileUtils.openOut(file);  
  4.       try {  
  5.           IteratorFile itFile = new IteratorFile(){  
  6.               @Override  
  7.               public boolean doWhat(byte[] b, int length,int readedIndex,int available) {  
  8.                   try {  
  9.                       if(readedIndex>endPos){  
  10.                           //说明已经读取到了endingPos位置,并且读超了readedIndex-getEnd()-1位  
  11.                           os.write(b, 0, length-(readedIndex-endPos-1));  
  12.                           return false;//终止读取  
  13.                       }else{  
  14.                           os.write(b, 0, length);  
  15.                       }  
  16.                       return true;  
  17.                   } catch (IOException e) {  
  18.                       e.printStackTrace();  
  19.                       return false;  
  20.                   }  
  21.               }  
  22.           }.from(in).start(startPos);  
  23.   
  24.           itFile.iterator2Bytes();  
  25.   
  26.       } catch (Exception e) {  
  27.           e.printStackTrace();  
  28.           this.tempFile = null;  
  29.       }finally{  
  30.           try {  
  31.               os.flush();  
  32.               os.close();  
  33.           } catch (IOException e) {  
  34.               e.printStackTrace();  
  35.           }  
  36.       }  
  37.       return getTempFile();  
  38.   }  

按行来读取时的文件内容处理

首先,再次声明,按行来遍历文件的时候,只适合文本文件。除非你对每一行的换行符用\r还是\n没有要求。像exe文件,如果用行来遍历的话,你写出为一个新的文件的时候,任意一个的换行符的不对都可能导致一个exe文件变为”unexe”文件!

过程中,我用到了:

一个辅助类Status,来辅助控制遍历的流程。
一个接口FlowLineProcesser,类似于一个处理文本的流水线。

Status和FlowLineProcesser是相互辅助的,Status也能辅助FlowLineProcesse是流水线的具体过程,Status是控制处理过程中怎么处理d的。

我也想了许多次,到底要不要把这个过程搞的这么复杂。但是还是先留着吧…

先看辅助类Status:

 

复制内容到剪贴板
  1. public class Status{  
  2.     /** 
  3.      * 是否找到了开头,默认false,若true则后续的遍历不会执行相应的firstStep()方法 
  4.      */  
  5.     public boolean overFirstStep = false;  
  6.   
  7.     /** 
  8.      * 是否找到了结尾,默认false,若true则后续的遍历不会执行相应的finalStep()方法 
  9.      */  
  10.     public boolean overFinalStep = false;  
  11.   
  12.     /** 
  13.      * 是否继续读取源文件,默认true表示继续读取,false则表示,执行本次操作后,遍历终止 
  14.      */  
  15.     public boolean shouldContinue = true;  
  16. }  

然后是FlowLineProcesser接口:

FlowLineProcesser是一个接口,类似于一个流水线。定义了两步操作,分别对应两个方法fistStep()和finalStep()。其中两个方法的返回值都是String,firstStep接受到得line是真正从文件中读取到的行,它将line经过自己的处理后,返回处理后的line给finalStep。所以,finalStep中得line其实是firstStep处理后的结果。但是最终真正返回给主处理流程的line,正是finalStep处理后的返回值。

复制内容到剪贴板
  1. public interface FlowLineProcesser{  
  2.     /** 
  3.      *  
  4.      * @param line 读取到的行 
  5.      * @param lineNumber 行号,从1开始 
  6.      * @param status 控制器 
  7.      * @return 
  8.      * @time 2017年1月22日 下午5:02:02 
  9.      */  
  10.     String firstStep(String line,int lineNumber,Status status);  
  11.   
  12.     /** 
  13.      * @tips  
  14.      * @param line 读取到的行(是firstStep()处理后的结果) 
  15.      * @param lineNumber 行号,从1开始 
  16.      * @param status 控制器 
  17.      * @return 
  18.      * @time 2017年1月22日 下午5:02:09 
  19.      */  
  20.     String finalStep(String line,int lineNumber,Status status);  
  21. }  

现在,可以来看一下如何去实现文本的抽取了:
所有读取的行,都临时存到一个stringbuilder中去。firstStep先进行一次处理,得到返回值后传递给finalStep,再次处理后,将得到的结果保存下来。如果最后的结果是null,则不会保存。

复制内容到剪贴板
  1. public String extractAsString(FlowLineProcesser method) {  
  2.   
  3.        try {  
  4.            checkIn();  
  5.        } catch (Exception e) {  
  6.            e.printStackTrace();  
  7.            return null;  
  8.        }  
  9.   
  10.        final StringBuilder builder = new StringBuilder();  
  11.   
  12.        this.mMethod = method;  
  13.   
  14.        new IteratorFile(){  
  15.            Status status = new Status();  
  16.            @Override  
  17.            public boolean doWhat(String line, int currentIndex) {  
  18.                String lineAfterProcess = "";  
  19.   
  20.                if(!status.overFirstStep){  
  21.                    lineAfterProcess = mMethod.firstStep(line, currentIndex,status);  
  22.                }  
  23.   
  24.                if(!status.shouldContinue){  
  25.                    return false;  
  26.                }  
  27.   
  28.                if(!status.overFinalStep){  
  29.                    lineAfterProcess = mMethod.finalStep(lineAfterProcess,currentIndex,status);  
  30.                }  
  31.   
  32.                if(lineAfterProcess!=null){  
  33.                    builder.append(lineAfterProcess);  
  34.                    builder.append(getLineStr());//换行符被写死在这里了  
  35.                }  
  36.   
  37.                if(!status.shouldContinue){  
  38.                    return false;  
  39.                }  
  40.                return true;  
  41.        }  
  42.   
  43.        }.from(in).iterator2Line();  
  44.   
  45.        return builder.toString();  
  46.   
  47.    }  

当要抽取的文本太大的时候,可以采用生成新文件的方式。与返回string的流程基本一致。

复制内容到剪贴板
  1. public File extractAsFile(FlowLineProcesser method) {  
  2.   
  3.      try {  
  4.          checkIn();  
  5.          checkOut();  
  6.      } catch (Exception e) {  
  7.          e.printStackTrace();  
  8.          return null;  
  9.      }  
  10.   
  11.      this.mMethod = method;  
  12.      File file = initOutFile();  
  13.      if(file==null){  
  14.          return null;  
  15.      }  
  16.   
  17.      FileWriter fileWriter = null;  
  18.      try {  
  19.          fileWriter = new FileWriter(file);  
  20.      } catch (Exception e) {  
  21.          e.printStackTrace();  
  22.          return null;  
  23.      }  
  24.   
  25.      final BufferedWriter writer = new BufferedWriter(fileWriter);  
  26.   
  27.      IteratorFile itfile = new IteratorFile(){  
  28.          Status status = new Status();  
  29.          @Override  
  30.          public boolean doWhat(String line, int currentIndex) {  
  31.              String lineAfterProcess = "";  
  32.   
  33.              if(!status.overFirstStep){  
  34.                  lineAfterProcess = mMethod.firstStep(line, currentIndex,status);  
  35.              }  
  36.   
  37.              if(!status.shouldContinue){  
  38.                  return false;  
  39.              }  
  40.   
  41.              if(!status.overFinalStep){  
  42.                  lineAfterProcess = mMethod.finalStep(lineAfterProcess,currentIndex,status);  
  43.              }  
  44.   
  45.              if(lineAfterProcess!=null){  
  46.                  try {  
  47.                      writer.write(lineAfterProcess);  
  48.                      writer.newLine();//TODO 换行符在此给写死了  
  49.                  } catch (IOException e) {  
  50.                      e.printStackTrace();  
  51.                      return false;  
  52.                  }  
  53.              }  
  54.   
  55.              if(!status.shouldContinue){  
  56.                  return false;  
  57.              }  
  58.              return true;  
  59.   
  60.          }  
  61.      };  
  62.   
  63.      itfile.from(in).iterator2Line();  
  64.   
  65.      if(writer!=null){  
  66.          try {  
  67.              writer.close();  
  68.          } catch (IOException e) {  
  69.              e.printStackTrace();  
  70.          }  
  71.      }  
  72.      try {  
  73.          fileWriter.close();  
  74.      } catch (IOException e) {  
  75.          e.printStackTrace();  
  76.      }  
  77.      return getTempFile();  
  78.   
  79.  }  

好啦,介绍到此就要结束啦,我们下次再聊~

代码包供您下载哦!—> 代码包http://download.csdn.net/detail/sonnyching/9744899

 

感谢 sonnyching 支持 磐实编程网 原文地址:
blog.csdn.net/sonnyching/article/details/54744023

文章信息

发布时间:2017-02-01

作者:sonnyching

发布者:aquwcw

浏览次数: