前言
近日,笔者在接到一项需求,就是用程序将两个文本的内容以行为单位进行比对,找出其中差异的部分进行展示,以便能够一眼看出修改人对文件做出了哪些修改。
刚接到这项需求时,感到颇有难度,但是经过深入思考,终于想出来实现文本内容对比的算法,并且写成程序得以实现。现将算法和代码公布,欢迎各位软件研发人员、热爱算法的同仁阅读和交流。
笔者:,如果任何问题,请加笔者。
人的思维是怎么比对文本的?
刚开始,我对这个问题也一筹莫展,因为一行行的去进行两个文本对比的话,如果新文本中有增加或者删除的行,那么行与行的对应关系就全乱了。后来我开始想,我们人的思维在进行文本内容对比的时候是怎样的一个过程呢?既然我们人类大脑的思维具有足够的对比文本内容差异的智能,那么人脑的思维过程就完全可以借鉴一下。经过思考,我总结出人脑的思维在进行文本内容差异比对的时候的过程。
为了描述形象,采用以下新旧两个文本示例来描述。
1、首先我们的目光会将将新旧文本分别从第一行开始,逐行去看两行的内容是否一样。
2、当我们第一次看到两行的内容不一样时,我们的大脑就会想,已经开始进入内容有区别的区域了(下文称差异区域)。对于示例图的两个文本,新旧文本的差异区域的起点均是第5行,即从第5行开始,两个文本出现差异。
3、随后我们会在两个文本里面继续往下看,直到再次看到两个内容相同的行为止,这时我们的大脑就会认为差异区域已经结束。
但是,我们千万不要把问题想得这么简单。因为如果修改内容是删除或者增加了若干行,那么新旧文本之间的行与行的对应关系就会乱掉。为此我们的大脑做了一个我们可能不容易发现的动作。那就是从差异区域的起点开始,如果发现下文找不到差异区域的终点,那么试探性的将新文本或旧文本进行整体性的上移若干行,然后再逐行对比移动后的文本。例如如果是新文本中新增了N行,则需要将新文本的所有内容整体性上移N行数;如果是旧文本中删除了N行,也需要将旧文本整体性的上移N行数,这样才能对比出差异区域的终点位置。
对于示例图的两个文本,需要将新文本上移两行才能找到差异区域的终点。旧文本差异区域的终点是第5行,新文本差异区域的终点是第7行。
具体过程如图所示:
4、当我们的大脑分别在新旧文本里面捕捉到差异区域的开始点与结束点以后,我们的大脑的就会让我们不要再继续往下看了,要先分析一下差异区域里面是被做了怎样的修改。
5、我们的大脑会依据新旧文本在差异区域上各自的起点和终点,来判断文本的变化类型。
为了方便描述,我们不妨将旧文本差异区域的起点行号为oldStart,旧文本差异区域的终点行号为oldEnd;将新文本差异区域的起点行号为newStart,新文本差异区域的终点行号为newEnd。
这里先直接给出一个结论,具体推导过程见下文:如果oldEnd!=oldStart && newEnd!=newStart && (oldEnd-oldStart)<(newEnd-newStart),说明变化类型是既有新增也有修改了若干行。示例图中的oldStart、oldEnd、newStart、newEnd分别为5、6、5、8,说明其属于新增且修改了若干行。
差异区域的变化类型有5种,分别是纯粹新增若干行、纯粹删除若干行、纯粹修改若干行、新增并修改若干行、删除并修改若干行。
当然还有删除并新增若干行这种情况,但是删除之后再新增,变化的结果等同于修改,所以此种类型可以直接合并到以上5种变化类型之内。
7、当我们的大脑在已经得出第一处差异区域的变化类型和内容后,就会命令我们的眼睛从本次差异区域的终点开始(这点很重要,必须是从差异区域的终点开始),接着对比剩余的文本。
算法与代码
第一步:读取新旧文本内容
读取文本内容的每一行,直接将每行存入一个ArrayList集合,ArrayList素的下标可以作为行号来使用。
//此方法用于读取文件,并且去除空行 static public List<String> readFile(String path) {
BufferedReader reader = null; File file = new File(path); if(!file.exists()) {
System.out.println("文件不存在"); } String tempStr; try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8")); List<String> lines=new ArrayList<>(); while ((tempStr = reader.readLine()) != null) {
//读取文本时,每一行采用行号+行文本内容键值对的形式进行存储,行号作为该行的唯一标识 if(!tempStr.trim().equals("")){
lines.add(tempStr); } } return lines; } catch (IOException e) {
e.printStackTrace(); return null; }finally {
if(reader!=null) {
try {
reader.close(); } catch (IOException e) {
e.printStackTrace(); } } } }
第二步:寻找差异区域的起点和终点
差异区域的起点很好找,直接逐行比对即可。
但是差异区域的终点就要麻烦很多,因为需要考虑三种情况:
1、新文本增加了若干行,导致行与行之间的对应关系错乱;
2、旧文本删除了若干行,导致行与行之间的对应关系错乱;
3、先删除了N行,再在下文新增了相同数量N行。导致逐行比对时将删除与新增中间的区域也误认为是差异区域。
对于第1种情况,可以将新文本上移。如下面的文本,新文本新增了“xxx”,直接上移一行,再逐行比对就可以得到差异区域的终点为旧文本的第2行与新文本的第3行。
对于第2种情况,可以将旧文本上移。如下面的文本,旧文本删除了“bbb”,将旧文本上移1行,就可以得到差异区域的终点为旧文本的第3行与新文本的第1行。
对于第3种情况,最难处理。例如下面的变化类型是旧文本中删除第2行,新文本中增加第5行。但是如果逐行比对,也能找到差异区域的终点为旧文本的第6行与新文本的第6行。于是很容易将2、3、4、5行都作为差异区域,变化类型全部为修改。
因此,在逐行比对得到终点以后,仍然需要试探性的将新文本或旧文本上移,观察移动文本之后能否得到离起点更近的终点,如果有,则取最近的。
例如将旧文本上移1行就可以得到真实的差异区域终点为旧文本的第3行和新文本的第2行。
相关代码为:
/ * 逐行对比文本,找到第一个不同或者相同的行 * 其中的oldLines和newLines为成员属性,表示旧文本的每一行和新文本的每一行 * type:是找第一个内容相同的行还是不同的行,true为找相同,false为找不同 * oldLineStart:旧文本从第几行开始 * newLineStart:新文本从第几行开始 * 返回true表示寻找成功,返回false表示寻找失败 */ static public boolean compareLines(boolean type,int oldLineStart,int newLineStart){
if(oldLineStart >= oldLines.size() || newLineStart >= newLines.size()){
return false; } //行号计数器 int lineCount = 0; //开始逐行比对两个文本 int oldLineNumber,newLineNubmer; while ((oldLineNumber=oldLineStart+lineCount)<oldLines.size() && (newLineNubmer=newLineStart+lineCount)<newLines.size()){
//分别取出新旧文本中的一行 String lineOld = oldLines.get(oldLineNumber); String lineNew = newLines.get(newLineNubmer); //下面代码中的oldEnd、oldStart、newEnd、newStart为实例属性, //分别表示旧文本差异区域的起终点和新文本差异区域的起终点 //找到完全相同的两行,其可以作为差异区域的终点 if(type && lineOld.equals(lineNew)){
//如果是第一次找到终点,先记录在oldEnd、newEnd两个属性中 if(isFirstGetEnd){
oldEnd = oldLineNumber; newEnd = newLineNubmer; isFirstGetEnd = false; //如果不是第一次找到,比较哪个终点与起点最近,取最近的终点 }else if(newLineNubmer<newEnd){
oldEnd = oldLineNumber; newEnd = newLineNubmer; } return true; } //找到差异的两行,其可以作为差异区域的起点 if(!type && !lineOld.equals(lineNew)){
oldStart = oldLineNumber; newStart = newLineNubmer; return true; } lineCount++; } //到文本的最后还没找到,返回false return false; } //在新旧文本寻找差异区域的起点,oldLines和newLines分别为存储新旧文本行内容的Map集合 static public boolean getDifferenceAreaStart() {
return compareLines(false,oldEnd,newEnd); } //寻找差异区域的终点,也就是新旧文本重新复合的点。 static public boolean getDifferenceAreaEnd() {
//重置为true isFirstGetEnd = true; //标记是否找到终点 boolean haveEnd = false; //moveLines为文本下移的行数 int moveLines = 0; int oldLineNumber=oldStart,newLineNubmer=newStart; while ((oldLineNumber<oldLines.size() || newLineNubmer < newLines.size()) ){
//newStart为0时不移动文本,newStart大于0时尝试以移动文本的方式来找终点 if(compareLines(true,oldLineNumber,newStart) || compareLines(true,oldStart,newLineNubmer)){
haveEnd = true; } moveLines ++; oldLineNumber = oldStart + moveLines; newLineNubmer = newStart + moveLines; } return haveEnd; }
第三步:判断变化类型
现在我们已经得到差异区域的起点和终点了,不知道读者还是否记得上文所说的,这时我们的大脑不会再让我们往下阅读,而是转而判断差异区域的变更类型。
关于如何判断差异区域的变化类型,需要使用新旧文本的差异区域的起点行号与终点行号来进行分析,这里我们不妨先定义一下,以方便书写:
然后,我来为你演示每种变化类型:
(2)、如果oldEnd<oldStart&&newEnd<newStart&&(oldEnd-oldStart)==(newEnd-newStart),则说明变化类型是纯粹的修改了若干行:
如下图所示:我们将文本中第二行的内容由“bbb”修改为“ddd”,则旧文本中差异区域的起点和终点分别为2和3,新文本中差异区域的起点和终点则同样分别为第2行和第3行。
(3)如果(oldEnd-oldStart)>(newEnd-newStart)&&newEnd==newStart,说明文本的变化类型是纯粹的被删除了若干行。
(4)如果oldEnd!=oldStart&&newEnd!=newStart&&(oldEnd-oldStart)<(newEnd-newStart),说明变化类型是既有新增也有修改了若干行。
如下图所示,我们将文本中第二行的内容由“bbb”修改为“bbx”,再新增“ddd”。则旧文本中差异区域的起点和终点分别为2和3,新文本中差异区域的起点和终点则分别为第2行和第4行。
(5)如果oldEnd!=oldStart&&newEnd!=newStart&&(oldEnd-oldStart)>(newEnd-newStart),说明变化类型是既有删除也有修改了若干行。
如下图所示,我们将文本中第二行的内容由“bbb”修改为“bbx”,再删除“ccc”。则旧文本中差异区域的起点和终点分别为2和4,新文本中差异区域的起点和终点则分别为第2行和第3行。
代码:
//分析文本的变化类型,存入结果集合中 public static void analChangeType() {
//下面开始分析差异区域的变化类型,然后按照类型进行处理 //oldEnd、oldStart、newEnd、newStart为实例属性, //分别表示旧文本差异区域的起终点和新文本差异区域的起终点 int oldNumDiff = oldEnd-oldStart; int newNumDiff = newEnd-newStart; //纯修改 if(oldNumDiff == newNumDiff){
int number=oldEnd-oldStart; for(int i = 0 ;i<number ;i++){
updateLines.put(oldStart+i,newStart+i); } }else if(oldNumDiff > newNumDiff){
if(newEnd==newStart){
//纯删除 for(int i=oldStart;i<oldEnd;i++) {
//取出被删除的行,存入集合 delLines.add(i); } }else {
//删除加修改 //计算修改的行数 int updateNum=newNumDiff; //获取修改的行,getUpdateLines为获取修改行对于关系的方法, // 返回的Map中存储的是修改前后的行号 Map<Integer, Integer> changeLineMap=getUpdateLines(updateNum); updateLines.putAll(changeLineMap); //获取删除的行 for(int lineNum = oldStart ;lineNum <oldEnd ; lineNum ++){
if(!changeLineMap.containsKey(lineNum)){
delLines.add(lineNum); } } } }else {
if(oldEnd==oldStart){
//纯新增 for(int i=newStart;i<newEnd;i++) {
addLines.add(i); } }else {
//新增加修改 //此时修改的行数是: int number=oldNumDiff; //获取修改的旧文本行号与新文本行号组成键值对的集合 Map<Integer, Integer> changeLineMap = getUpdateLines(number); updateLines.putAll(changeLineMap); //获取新增的行 for(int lineNum = newStart ;lineNum <newEnd ; lineNum ++){
if(!changeLineMap.values().contains(lineNum)){
addLines.add(lineNum); } } } } }
第四步:寻找修改前后的两行对应关系
以下为程序代码
//准备方法,计算两个字符串相同字符的数量 static public int numJewelsInStones(String J, String S) {
J=J.trim(); S=S.trim(); char[] Ja = J.toCharArray(); char[] Sa = S.toCharArray(); int r = 0; for (int i = 0;i < Ja.length ; i ++){
for(int j = 0; j < Sa.length; j++){
if(Ja[i] == Sa[j]) r ++; } } return r; } //找出差异区域内哪些是修改的行 //参数n表示我们需要找的修改前后的行有几对 public static Map<Integer, Integer> getUpdateLines(int n) {
Map<Integer, Integer> resultMap=new HashMap(); //准备数组,用来储存组队两行的重复字符个数 int[] repeatCounts = new int[(oldEnd-oldStart)*(newEnd-newStart)]; //用来储存组队两行的重复字符个数和行之间的对应关系 Map<String,Integer> contAndLines = new HashMap<String,Integer>(); int num = 0; for(int i = oldStart; i < oldEnd ; i++){
for(int j = newStart ; j <newEnd ;j++){
int count=numJewelsInStones(oldLines.get(i),newLines.get(j)); repeatCounts[num] = count; contAndLines.put(String.valueOf(i)+":"+String.valueOf(j),count); num++; } } //对数组进行升序 Arrays.sort(repeatCounts); //标记已经找到的修行前后对应行的数量 int lineCount = 0; out: for(int i = repeatCounts.length-1 ; i>=(repeatCounts.length - n);i--){
for(String lineInfo : contAndLines.keySet()){
if(contAndLines.get(lineInfo).intValue() == repeatCounts[i]){
String[] lineNumA=lineInfo.split(":"); resultMap.put(Integer.valueOf(lineNumA[0]),Integer.valueOf(lineNumA[1])); if(++lineCount >= n){
break out; } } } } return resultMap; }
第五步:继续递归分析剩余文本
此时一次差异区域的分析已经完成,继续往下逐行比对文本内容。
//递归比对文本 public static void compare(){
//如果能找到差异区域的起点 if(getDifferenceAreaStart()){
//也能找到差异区域的终点 if(getDifferenceAreaEnd()){
analChangeType(); compare(); }else {
//如果找不到差异区域的终点,说明从起点开始下文全是差异区域 oldEnd = oldLines.size(); newEnd = newLines.size(); analChangeType(); } } }
完整代码
package practice; import java.io.*; import java.util.*; public class ContrastFile {
//旧文本中差异区域的起点 static int oldStart = 0; //旧文本中差异区域的终点 static int oldEnd = 0; //新文本中差异区域的起点 static int newStart = 0; //新文本中差异区域的终点 static int newEnd = 0; //在寻找一块差异区域的终点时,是否是第一次找到终点 static boolean isFirstGetEnd = true; //存储旧文本的每一行 static List<String> oldLines; //存储新文本的每一行 static List<String> newLines; //下面的集合用来存储对比结果 //存储增加的行在新文本中的行号 static List<Integer> delLines = new LinkedList<Integer>(); //存储删除的行在旧文本中的行号 static List<Integer> addLines = new LinkedList<Integer>(); //存储修改的行分别在新旧文本中的行号 static Map<Integer,Integer> updateLines = new HashMap<Integer,Integer>(); public static void main(String[] args) {
//作为旧文本 String path1="F://comparetest/1.txt"; //作为新文本 String path2="F://comparetest/2.txt"; //获取比对结果 List<String> differMsgList = getContrastResult(path1,path2); //打印出对比结果 for (String differ : differMsgList){
System.out.println(differ); } } //比对文本,并收集整理对比结果 public static List<String> getContrastResult(String path1,String path2){
//读取文件 oldLines = readFile(path1); newLines = readFile(path2); //调用对比方法 compare(); List<String> differMsgList = new ArrayList<String>(); for(int lineNum : delLines){
differMsgList.add("旧文本中删除了第"+(lineNum+1)+"行,内容是:"+oldLines.get(lineNum)); } for(int lineNum : addLines){
differMsgList.add("新文本中增加了第"+(lineNum+1)+"行,内容是:"+newLines.get(lineNum)); } for(int oldNum : updateLines.keySet()){
differMsgList.add("旧文本中的第"+(oldNum+1)+"行,内容是:"+oldLines.get(oldNum) +",修改为新文本中的第"+(updateLines.get(oldNum)+1)+"行,内容是:"+newLines.get(updateLines.get(oldNum))); } return differMsgList; } //此方法用于读取文件,并且去除空行 static public List<String> readFile(String path) {
BufferedReader reader = null; File file = new File(path); if(!file.exists()) {
System.out.println("文件不存在"); } String tempStr; try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8")); List<String> lines=new ArrayList<>(); while ((tempStr = reader.readLine()) != null) {
//读取文本时,每一行采用行号+行文本内容键值对的形式进行存储,行号作为该行的唯一标识 if(!tempStr.trim().equals("")){
lines.add(tempStr); } } return lines; } catch (IOException e) {
e.printStackTrace(); return null; }finally {
if(reader!=null) {
try {
reader.close(); } catch (IOException e) {
e.printStackTrace(); } } } } //准备方法,计算两个字符串相同字符的数量 static public int numJewelsInStones(String J, String S) {
J=J.trim(); S=S.trim(); char[] Ja = J.toCharArray(); char[] Sa = S.toCharArray(); int r = 0; for (int i = 0;i < Ja.length ; i ++){
for(int j = 0; j < Sa.length; j++){
if(Ja[i] == Sa[j]) r ++; } } return r; } / * 逐行对比文本,找到第一个不同或者相同的行 * 其中的oldLines和newLines为成员属性,表示旧文本的每一行和新文本的每一行 * type:是找第一个内容相同的行还是不同的行,true为找相同,false为找不同 * oldLineStart:旧文本从第几行开始 * newLineStart:新文本从第几行开始 * 返回true表示寻找成功,返回false表示寻找失败 */ static public boolean compareLines(boolean type,int oldLineStart,int newLineStart){
if(oldLineStart >= oldLines.size() || newLineStart >= newLines.size()){
return false; } //行号计数器 int lineCount = 0; //开始逐行比对两个文本 int oldLineNumber,newLineNubmer; while ((oldLineNumber=oldLineStart+lineCount)<oldLines.size() && (newLineNubmer=newLineStart+lineCount)<newLines.size()){
//分别取出新旧文本中的一行 String lineOld = oldLines.get(oldLineNumber); String lineNew = newLines.get(newLineNubmer); //下面代码中的oldEnd、oldStart、newEnd、newStart为实例属性, //分别表示旧文本差异区域的起终点和新文本差异区域的起终点 //找到完全相同的两行,其可以作为差异区域的终点 if(type && lineOld.equals(lineNew)){
//如果是第一次找到终点,先记录在oldEnd、newEnd两个属性中 if(isFirstGetEnd){
oldEnd = oldLineNumber; newEnd = newLineNubmer; isFirstGetEnd = false; //如果不是第一次找到,比较哪个终点与起点最近,取最近的终点 }else if(newLineNubmer<newEnd){
oldEnd = oldLineNumber; newEnd = newLineNubmer; } return true; } //找到差异的两行,其可以作为差异区域的起点 if(!type && !lineOld.equals(lineNew)){
oldStart = oldLineNumber; newStart = newLineNubmer; return true; } lineCount++; } //到文本的最后还没找到,返回false return false; } //在新旧文本寻找差异区域的起点,oldLines和newLines分别为存储新旧文本行内容的Map集合 static public boolean getDifferenceAreaStart() {
return compareLines(false,oldEnd,newEnd); } //寻找差异区域的终点,也就是新旧文本重新复合的点。 static public boolean getDifferenceAreaEnd() {
//重置为true isFirstGetEnd = true; //标记是否找到终点 boolean haveEnd = false; //moveLines为文本下移的行数 int moveLines = 0; int oldLineNumber=oldStart,newLineNubmer=newStart; while ((oldLineNumber<oldLines.size() || newLineNubmer < newLines.size()) ){
//newStart为0时不移动文本,newStart大于0时尝试以移动文本的方式来找终点 if(compareLines(true,oldLineNumber,newStart) || compareLines(true,oldStart,newLineNubmer)){
haveEnd = true; } moveLines ++; oldLineNumber = oldStart + moveLines; newLineNubmer = newStart + moveLines; } return haveEnd; } //找出差异区域内哪些是修改的行 //参数n表示我们需要找的修改前后的行有几对 public static Map<Integer, Integer> getUpdateLines(int n) {
Map<Integer, Integer> resultMap=new HashMap(); //准备数组,用来储存组队两行的重复字符个数 int[] repeatCounts = new int[(oldEnd-oldStart)*(newEnd-newStart)]; //用来储存组队两行的重复字符个数和行之间的对应关系 Map<String,Integer> contAndLines = new HashMap<String,Integer>(); int num = 0; for(int i = oldStart; i < oldEnd ; i++){
for(int j = newStart ; j <newEnd ;j++){
int count=numJewelsInStones(oldLines.get(i),newLines.get(j)); repeatCounts[num] = count; contAndLines.put(String.valueOf(i)+":"+String.valueOf(j),count); num++; } } //对数组进行升序 Arrays.sort(repeatCounts); //标记已经找到的修行前后对应行的数量 int lineCount = 0; out: for(int i = repeatCounts.length-1 ; i>=(repeatCounts.length - n);i--){
for(String lineInfo : contAndLines.keySet()){
if(contAndLines.get(lineInfo).intValue() == repeatCounts[i]){
String[] lineNumA=lineInfo.split(":"); resultMap.put(Integer.valueOf(lineNumA[0]),Integer.valueOf(lineNumA[1])); if(++lineCount >= n){
break out; } } } } return resultMap; } //分析文本的变化类型,存入结果集合中 public static void analChangeType() {
//下面开始分析差异区域的变化类型,然后按照类型进行处理 //oldEnd、oldStart、newEnd、newStart为实例属性, //分别表示旧文本差异区域的起终点和新文本差异区域的起终点 int oldNumDiff = oldEnd-oldStart; int newNumDiff = newEnd-newStart; //纯修改 if(oldNumDiff == newNumDiff){
int number=oldEnd-oldStart; for(int i = 0 ;i<number ;i++){
updateLines.put(oldStart+i,newStart+i); } }else if(oldNumDiff > newNumDiff){
if(newEnd==newStart){
//纯删除 for(int i=oldStart;i<oldEnd;i++) {
//取出被删除的行,存入集合 delLines.add(i); } }else {
//删除加修改 //计算修改的行数 int updateNum=newNumDiff; //获取修改的行,getUpdateLines为获取修改行对于关系的方法, // 返回的Map中存储的是修改前后的行号 Map<Integer, Integer> changeLineMap=getUpdateLines(updateNum); updateLines.putAll(changeLineMap); //获取删除的行 for(int lineNum = oldStart ;lineNum <oldEnd ; lineNum ++){
if(!changeLineMap.containsKey(lineNum)){
delLines.add(lineNum); } } } }else {
if(oldEnd==oldStart){
//纯新增 for(int i=newStart;i<newEnd;i++) {
addLines.add(i); } }else {
//新增加修改 //此时修改的行数是: int number=oldNumDiff; //获取修改的旧文本行号与新文本行号组成键值对的集合 Map<Integer, Integer> changeLineMap = getUpdateLines(number); updateLines.putAll(changeLineMap); //获取新增的行 for(int lineNum = newStart ;lineNum <newEnd ; lineNum ++){
if(!changeLineMap.values().contains(lineNum)){
addLines.add(lineNum); } } } } } //递归对比文本 public static void compare(){
//如果能找到差异区域的起点 if(getDifferenceAreaStart()){
//也能找到差异区域的终点 if(getDifferenceAreaEnd()){
analChangeType(); compare(); }else {
//如果找不到差异区域的终点,说明从起点开始下文全是差异区域 oldEnd = oldLines.size(); newEnd = newLines.size(); analChangeType(); } } } }
程序计算结果测试
这是用于对比的初始文本(即代码中的“F://comparetest/1.txt”):
汉皇重色思倾国,
御宇多年求不得。
杨家有女初长成,
养在深闺人未识。
天生丽质难自弃,
一朝选在君王侧。
回眸一笑百媚生,
六宫粉黛无颜色。
春寒赐浴华清池,
温泉水滑洗凝脂。
侍儿扶起娇无力,
始是新承恩泽时。
云鬓花颜金步摇,
芙蓉帐暖度春宵。
春宵苦短日高起,
从此君王不早朝。
我们将其修改为(即代码中的“F://comparetest/2.txt”):
汉皇重色思倾国,
美人多年求不得。
杨家有女初成年,
养在深闺人未识。
天生丽质难自弃,
回头一笑百媚生,
六宫粉黛无颜色。
春寒赐浴华清池,
温泉水滑洗凝脂。
侍儿扶起娇无力,
始是新承恩泽时。
后宫佳丽三千人,
三千宠爱在一身。
云鬓花颜摇啊摇,
芙蓉帐暖度春宵。
春宵苦短日高起,
从此君王不上朝。
控制台打印的对比结果是(结果准确无误):
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ji-chu/96884.html