如何用两个绝对path(或URL)在Java中构build相对path?

给定两个绝对path,例如

/var/data/stuff/xyz.dat /var/data 

如何创build一个使用第二个path作为其基础的相对path? 在上面的例子中,结果应该是: ./stuff/xyz.dat

这有点迂回,但为什么不使用URI? 它有一个相对的方法,为你做所有必要的检查。

 String path = "/var/data/stuff/xyz.dat"; String base = "/var/data"; String relative = new File(base).toURI().relativize(new File(path).toURI()).getPath(); // relative == "stuff/xyz.dat" 

从Java 7开始,您可以使用relativize方法:

 import java.nio.file.Path; import java.nio.file.Paths; public class Test { public static void main(String[] args) { Path pathAbsolute = Paths.get("/var/data/stuff/xyz.dat"); Path pathBase = Paths.get("/var/data"); Path pathRelative = pathBase.relativize(pathAbsolute); System.out.println(pathRelative); } } 

输出:

 stuff/xyz.dat 

在撰写本文时(2010年6月),这是通过我的testing用例的唯一解决scheme。 我不能保证这个解决scheme是没有错误的,但它确实通过了包含的testing用例。 我写的方法和testing依赖于Apache commons IO的FilenameUtils类。

该解决scheme使用Java 1.4进行testing。 如果您使用的是Java 1.5(或更高版本),则应考虑用StringBufferreplaceStringBuffer (如果您仍在使用Java 1.4,则应考虑更改雇主)。

 import java.io.File; import java.util.regex.Pattern; import org.apache.commons.io.FilenameUtils; public class ResourceUtils { /** * Get the relative path from one file to another, specifying the directory separator. * If one of the provided resources does not exist, it is assumed to be a file unless it ends with '/' or * '\'. * * @param targetPath targetPath is calculated to this file * @param basePath basePath is calculated from this file * @param pathSeparator directory separator. The platform default is not assumed so that we can test Unix behaviour when running on Windows (for example) * @return */ public static String getRelativePath(String targetPath, String basePath, String pathSeparator) { // Normalize the paths String normalizedTargetPath = FilenameUtils.normalizeNoEndSeparator(targetPath); String normalizedBasePath = FilenameUtils.normalizeNoEndSeparator(basePath); // Undo the changes to the separators made by normalization if (pathSeparator.equals("/")) { normalizedTargetPath = FilenameUtils.separatorsToUnix(normalizedTargetPath); normalizedBasePath = FilenameUtils.separatorsToUnix(normalizedBasePath); } else if (pathSeparator.equals("\\")) { normalizedTargetPath = FilenameUtils.separatorsToWindows(normalizedTargetPath); normalizedBasePath = FilenameUtils.separatorsToWindows(normalizedBasePath); } else { throw new IllegalArgumentException("Unrecognised dir separator '" + pathSeparator + "'"); } String[] base = normalizedBasePath.split(Pattern.quote(pathSeparator)); String[] target = normalizedTargetPath.split(Pattern.quote(pathSeparator)); // First get all the common elements. Store them as a string, // and also count how many of them there are. StringBuffer common = new StringBuffer(); int commonIndex = 0; while (commonIndex < target.length && commonIndex < base.length && target[commonIndex].equals(base[commonIndex])) { common.append(target[commonIndex] + pathSeparator); commonIndex++; } if (commonIndex == 0) { // No single common path element. This most // likely indicates differing drive letters, like C: and D:. // These paths cannot be relativized. throw new PathResolutionException("No common path element found for '" + normalizedTargetPath + "' and '" + normalizedBasePath + "'"); } // The number of directories we have to backtrack depends on whether the base is a file or a dir // For example, the relative path from // // /foo/bar/baz/gg/ff to /foo/bar/baz // // ".." if ff is a file // "../.." if ff is a directory // // The following is a heuristic to figure out if the base refers to a file or dir. It's not perfect, because // the resource referred to by this path may not actually exist, but it's the best I can do boolean baseIsFile = true; File baseResource = new File(normalizedBasePath); if (baseResource.exists()) { baseIsFile = baseResource.isFile(); } else if (basePath.endsWith(pathSeparator)) { baseIsFile = false; } StringBuffer relative = new StringBuffer(); if (base.length != commonIndex) { int numDirsUp = baseIsFile ? base.length - commonIndex - 1 : base.length - commonIndex; for (int i = 0; i < numDirsUp; i++) { relative.append(".." + pathSeparator); } } relative.append(normalizedTargetPath.substring(common.length())); return relative.toString(); } static class PathResolutionException extends RuntimeException { PathResolutionException(String msg) { super(msg); } } } 

这个通过的testing用例是

 public void testGetRelativePathsUnix() { assertEquals("stuff/xyz.dat", ResourceUtils.getRelativePath("/var/data/stuff/xyz.dat", "/var/data/", "/")); assertEquals("../../b/c", ResourceUtils.getRelativePath("/a/b/c", "/a/x/y/", "/")); assertEquals("../../b/c", ResourceUtils.getRelativePath("/m/n/o/a/b/c", "/m/n/o/a/x/y/", "/")); } public void testGetRelativePathFileToFile() { String target = "C:\\Windows\\Boot\\Fonts\\chs_boot.ttf"; String base = "C:\\Windows\\Speech\\Common\\sapisvr.exe"; String relPath = ResourceUtils.getRelativePath(target, base, "\\"); assertEquals("..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath); } public void testGetRelativePathDirectoryToFile() { String target = "C:\\Windows\\Boot\\Fonts\\chs_boot.ttf"; String base = "C:\\Windows\\Speech\\Common\\"; String relPath = ResourceUtils.getRelativePath(target, base, "\\"); assertEquals("..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath); } public void testGetRelativePathFileToDirectory() { String target = "C:\\Windows\\Boot\\Fonts"; String base = "C:\\Windows\\Speech\\Common\\foo.txt"; String relPath = ResourceUtils.getRelativePath(target, base, "\\"); assertEquals("..\\..\\Boot\\Fonts", relPath); } public void testGetRelativePathDirectoryToDirectory() { String target = "C:\\Windows\\Boot\\"; String base = "C:\\Windows\\Speech\\Common\\"; String expected = "..\\..\\Boot"; String relPath = ResourceUtils.getRelativePath(target, base, "\\"); assertEquals(expected, relPath); } public void testGetRelativePathDifferentDriveLetters() { String target = "D:\\sources\\recovery\\RecEnv.exe"; String base = "C:\\Java\\workspace\\AcceptanceTests\\Standard test data\\geo\\"; try { ResourceUtils.getRelativePath(target, base, "\\"); fail(); } catch (PathResolutionException ex) { // expected exception } } 

当使用java.net.URI.relativize时,您应该知道Java错误: JDK-6226081(URI应该能够使具有部分根的path相对化)

目前, URIrelativize()方法只会在另一个URI的前缀时使URI相对化。

这基本上意味着java.net.URI.relativize不会为你创build“..”。

@Peter Mueller的答案中提到的错误由Apache HttpComponents中的URIUtils解决

 public static URI resolve(URI baseURI, String reference) 

根据基本URIparsingURI引用。 解决java.net.URI()中的错误

如果你知道第二个string是第一个string的一部分:

 String s1 = "/var/data/stuff/xyz.dat"; String s2 = "/var/data"; String s3 = s1.substring(s2.length()); 

或者如果你真的想在这个开始的时期,如你的例子:

 String s3 = ".".concat(s1.substring(s2.length())); 

recursion产生一个更小的解决scheme。 如果结果是不可能的(例如不同的Windows磁盘)或不切实际的(root是唯一的公共目录),则会引发exception。

 /** * Computes the path for a file relative to a given base, or fails if the only shared * directory is the root and the absolute form is better. * * @param base File that is the base for the result * @param name File to be "relativized" * @return the relative name * @throws IOException if files have no common sub-directories, ie at best share the * root prefix "/" or "C:\" */ public static String getRelativePath(File base, File name) throws IOException { File parent = base.getParentFile(); if (parent == null) { throw new IOException("No common directory"); } String bpath = base.getCanonicalPath(); String fpath = name.getCanonicalPath(); if (fpath.startsWith(bpath)) { return fpath.substring(bpath.length() + 1); } else { return (".." + File.separator + getRelativePath(parent, name)); } } 

Java 8中,你可以简单地做(和URI ,它是无缺陷的):

 Path#relativize(Path) 

我的版本松散地基于马特和史蒂夫的版本:

 /** * Returns the path of one File relative to another. * * @param target the target directory * @param base the base directory * @return target's path relative to the base directory * @throws IOException if an error occurs while resolving the files' canonical names */ public static File getRelativeFile(File target, File base) throws IOException { String[] baseComponents = base.getCanonicalPath().split(Pattern.quote(File.separator)); String[] targetComponents = target.getCanonicalPath().split(Pattern.quote(File.separator)); // skip common components int index = 0; for (; index < targetComponents.length && index < baseComponents.length; ++index) { if (!targetComponents[index].equals(baseComponents[index])) break; } StringBuilder result = new StringBuilder(); if (index != baseComponents.length) { // backtrack to base directory for (int i = index; i < baseComponents.length; ++i) result.append(".." + File.separator); } for (; index < targetComponents.length; ++index) result.append(targetComponents[index] + File.separator); if (!target.getPath().endsWith("/") && !target.getPath().endsWith("\\")) { // remove final path separator result.delete(result.length() - File.separator.length(), result.length()); } return new File(result.toString()); } 

马特B的解决scheme获取目录的数量回溯错误 – 它应该是基本path的长度减去共同的path元素的数量减一(对于最后一个path元素,这是一个文件名或尾随通过split )。 它恰好与/a/b/c//a/x/y/ ,但用/m/n/o/a/b/c//m/n/o/a/x/y/replace参数/m/n/o/a/x/y/ ,你会看到这个问题。

另外,它需要在第一个for循环中有一个else break ,或者它会错误地处理恰好匹配目录名的path,如/a/b/c/d//x/y/c/zc在两个数组中都在同一个插槽中,但不是实际的匹配。

所有这些解决scheme缺乏处理不能相对于彼此的path的能力,因为它们具有不兼容的根,例如C:\foo\barD:\baz\quux 。 可能只是Windows上的一个问题,但值得注意。

我花了比我想要的时间更长,但没关系。 我真的需要这个工作,所以谢谢大家的意见,我相信这个版本也会有更正!

 public static String getRelativePath(String targetPath, String basePath, String pathSeparator) { // We need the -1 argument to split to make sure we get a trailing // "" token if the base ends in the path separator and is therefore // a directory. We require directory paths to end in the path // separator -- otherwise they are indistinguishable from files. String[] base = basePath.split(Pattern.quote(pathSeparator), -1); String[] target = targetPath.split(Pattern.quote(pathSeparator), 0); // First get all the common elements. Store them as a string, // and also count how many of them there are. String common = ""; int commonIndex = 0; for (int i = 0; i < target.length && i < base.length; i++) { if (target[i].equals(base[i])) { common += target[i] + pathSeparator; commonIndex++; } else break; } if (commonIndex == 0) { // Whoops -- not even a single common path element. This most // likely indicates differing drive letters, like C: and D:. // These paths cannot be relativized. Return the target path. return targetPath; // This should never happen when all absolute paths // begin with / as in *nix. } String relative = ""; if (base.length == commonIndex) { // Comment this out if you prefer that a relative path not start with ./ //relative = "." + pathSeparator; } else { int numDirsUp = base.length - commonIndex - 1; // The number of directories we have to backtrack is the length of // the base path MINUS the number of common path elements, minus // one because the last element in the path isn't a directory. for (int i = 1; i <= (numDirsUp); i++) { relative += ".." + pathSeparator; } } relative += targetPath.substring(common.length()); return relative; } 

这里是testing涵盖几个案例:

 public void testGetRelativePathsUnixy() { assertEquals("stuff/xyz.dat", FileUtils.getRelativePath( "/var/data/stuff/xyz.dat", "/var/data/", "/")); assertEquals("../../b/c", FileUtils.getRelativePath( "/a/b/c", "/a/x/y/", "/")); assertEquals("../../b/c", FileUtils.getRelativePath( "/m/n/o/a/b/c", "/m/n/o/a/x/y/", "/")); } public void testGetRelativePathFileToFile() { String target = "C:\\Windows\\Boot\\Fonts\\chs_boot.ttf"; String base = "C:\\Windows\\Speech\\Common\\sapisvr.exe"; String relPath = FileUtils.getRelativePath(target, base, "\\"); assertEquals("..\\..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath); } public void testGetRelativePathDirectoryToFile() { String target = "C:\\Windows\\Boot\\Fonts\\chs_boot.ttf"; String base = "C:\\Windows\\Speech\\Common"; String relPath = FileUtils.getRelativePath(target, base, "\\"); assertEquals("..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath); } public void testGetRelativePathDifferentDriveLetters() { String target = "D:\\sources\\recovery\\RecEnv.exe"; String base = "C:\\Java\\workspace\\AcceptanceTests\\Standard test data\\geo\\"; // Should just return the target path because of the incompatible roots. String relPath = FileUtils.getRelativePath(target, base, "\\"); assertEquals(target, relPath); } 

这是一个免费的解决scheme:

 Path sourceFile = Paths.get("some/common/path/example/a/b/c/f1.txt"); Path targetFile = Paths.get("some/common/path/example/d/e/f2.txt"); Path relativePath = sourceFile.relativize(targetFile); System.out.println(relativePath); 

输出

 ..\..\..\..\d\e\f2.txt 

[编辑]实际上它输出更.. ..因为源文件不是目录。 对我的情况正确的解决scheme是:

 Path sourceFile = Paths.get(new File("some/common/path/example/a/b/c/f1.txt").parent()); Path targetFile = Paths.get("some/common/path/example/d/e/f2.txt"); Path relativePath = sourceFile.relativize(targetFile); System.out.println(relativePath); 

其实我的其他答案没有工作,如果目标path不是基本path的孩子。

这应该工作。

 public class RelativePathFinder { public static String getRelativePath(String targetPath, String basePath, String pathSeparator) { // find common path String[] target = targetPath.split(pathSeparator); String[] base = basePath.split(pathSeparator); String common = ""; int commonIndex = 0; for (int i = 0; i < target.length && i < base.length; i++) { if (target[i].equals(base[i])) { common += target[i] + pathSeparator; commonIndex++; } } String relative = ""; // is the target a child directory of the base directory? // ie, target = /a/b/c/d, base = /a/b/ if (commonIndex == base.length) { relative = "." + pathSeparator + targetPath.substring(common.length()); } else { // determine how many directories we have to backtrack for (int i = 1; i <= commonIndex; i++) { relative += ".." + pathSeparator; } relative += targetPath.substring(common.length()); } return relative; } public static String getRelativePath(String targetPath, String basePath) { return getRelativePath(targetPath, basePath, File.pathSeparator); } } 

 public class RelativePathFinderTest extends TestCase { public void testGetRelativePath() { assertEquals("./stuff/xyz.dat", RelativePathFinder.getRelativePath( "/var/data/stuff/xyz.dat", "/var/data/", "/")); assertEquals("../../b/c", RelativePathFinder.getRelativePath("/a/b/c", "/a/x/y/", "/")); } } 

凉!! 我需要一些这样的代码,但比较Linux机器上的目录path。 我发现这是不工作的父目录是目标的情况下。

这是一个目录友好版本的方法:

  public static String getRelativePath(String targetPath, String basePath, String pathSeparator) { boolean isDir = false; { File f = new File(targetPath); isDir = f.isDirectory(); } // We need the -1 argument to split to make sure we get a trailing // "" token if the base ends in the path separator and is therefore // a directory. We require directory paths to end in the path // separator -- otherwise they are indistinguishable from files. String[] base = basePath.split(Pattern.quote(pathSeparator), -1); String[] target = targetPath.split(Pattern.quote(pathSeparator), 0); // First get all the common elements. Store them as a string, // and also count how many of them there are. String common = ""; int commonIndex = 0; for (int i = 0; i < target.length && i < base.length; i++) { if (target[i].equals(base[i])) { common += target[i] + pathSeparator; commonIndex++; } else break; } if (commonIndex == 0) { // Whoops -- not even a single common path element. This most // likely indicates differing drive letters, like C: and D:. // These paths cannot be relativized. Return the target path. return targetPath; // This should never happen when all absolute paths // begin with / as in *nix. } String relative = ""; if (base.length == commonIndex) { // Comment this out if you prefer that a relative path not start with ./ relative = "." + pathSeparator; } else { int numDirsUp = base.length - commonIndex - (isDir?0:1); /* only subtract 1 if it is a file. */ // The number of directories we have to backtrack is the length of // the base path MINUS the number of common path elements, minus // one because the last element in the path isn't a directory. for (int i = 1; i <= (numDirsUp); i++) { relative += ".." + pathSeparator; } } //if we are comparing directories then we if (targetPath.length() > common.length()) { //it's OK, it isn't a directory relative += targetPath.substring(common.length()); } return relative; } 

我假设你有fromPath (一个文件夹的绝对path)和toPath (文件夹/文件的绝对path),并且你正在寻找一个代表toPath中的文件/文件夹作为相对path的path从fromPath (你当前的工作目录是fromPath ),那么这样的事情应该工作:

 public static String getRelativePath(String fromPath, String toPath) { // This weirdness is because a separator of '/' messes with String.split() String regexCharacter = File.separator; if (File.separatorChar == '\\') { regexCharacter = "\\\\"; } String[] fromSplit = fromPath.split(regexCharacter); String[] toSplit = toPath.split(regexCharacter); // Find the common path int common = 0; while (fromSplit[common].equals(toSplit[common])) { common++; } StringBuffer result = new StringBuffer("."); // Work your way up the FROM path to common ground for (int i = common; i < fromSplit.length; i++) { result.append(File.separatorChar).append(".."); } // Work your way down the TO path for (int i = common; i < toSplit.length; i++) { result.append(File.separatorChar).append(toSplit[i]); } return result.toString(); } 

这里已经有很多答案,但是我发现他们并没有处理所有的情况,比如基础和目标是一样的。 该函数采用基本目录和目标path并返回相对path。 如果不存在相对path,则返回目标path。 File.separator是不必要的。

 public static String getRelativePath (String baseDir, String targetPath) { String[] base = baseDir.replace('\\', '/').split("\\/"); targetPath = targetPath.replace('\\', '/'); String[] target = targetPath.split("\\/"); // Count common elements and their length. int commonCount = 0, commonLength = 0, maxCount = Math.min(target.length, base.length); while (commonCount < maxCount) { String targetElement = target[commonCount]; if (!targetElement.equals(base[commonCount])) break; commonCount++; commonLength += targetElement.length() + 1; // Directory name length plus slash. } if (commonCount == 0) return targetPath; // No common path element. int targetLength = targetPath.length(); int dirsUp = base.length - commonCount; StringBuffer relative = new StringBuffer(dirsUp * 3 + targetLength - commonLength + 1); for (int i = 0; i < dirsUp; i++) relative.append("../"); if (commonLength < targetLength) relative.append(targetPath.substring(commonLength)); return relative.toString(); } 

这里有一个方法可以parsing一个基本path的相对path,不pipe它们在相同或不同的根目录下:

 public static String GetRelativePath(String path, String base){ final String SEP = "/"; // if base is not a directory -> return empty if (!base.endsWith(SEP)){ return ""; } // check if path is a file -> remove last "/" at the end of the method boolean isfile = !path.endsWith(SEP); // get URIs and split them by using the separator String a = ""; String b = ""; try { a = new File(base).getCanonicalFile().toURI().getPath(); b = new File(path).getCanonicalFile().toURI().getPath(); } catch (IOException e) { e.printStackTrace(); } String[] basePaths = a.split(SEP); String[] otherPaths = b.split(SEP); // check common part int n = 0; for(; n < basePaths.length && n < otherPaths.length; n ++) { if( basePaths[n].equals(otherPaths[n]) == false ) break; } // compose the new path StringBuffer tmp = new StringBuffer(""); for(int m = n; m < basePaths.length; m ++) tmp.append(".."+SEP); for(int m = n; m < otherPaths.length; m ++) { tmp.append(otherPaths[m]); tmp.append(SEP); } // get path string String result = tmp.toString(); // remove last "/" if path is a file if (isfile && result.endsWith(SEP)){ result = result.substring(0,result.length()-1); } return result; } 
 private String relative(String left, String right){ String[] lefts = left.split("/"); String[] rights = right.split("/"); int min = Math.min(lefts.length, rights.length); int commonIdx = -1; for(int i = 0; i < min; i++){ if(commonIdx < 0 && !lefts[i].equals(rights[i])){ commonIdx = i - 1; break; } } if(commonIdx < 0){ return null; } StringBuilder sb = new StringBuilder(Math.max(left.length(), right.length())); sb.append(left).append("/"); for(int i = commonIdx + 1; i < lefts.length;i++){ sb.append("../"); } for(int i = commonIdx + 1; i < rights.length;i++){ sb.append(rights[i]).append("/"); } return sb.deleteCharAt(sb.length() -1).toString(); } 

通过Dónal的testing,唯一的变化 – 如果没有共同的根,它返回目标path(它可能已经是相对的)

 import static java.util.Arrays.asList; import static java.util.Collections.nCopies; import static org.apache.commons.io.FilenameUtils.normalizeNoEndSeparator; import static org.apache.commons.io.FilenameUtils.separatorsToUnix; import static org.apache.commons.lang3.StringUtils.getCommonPrefix; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotEmpty; import static org.apache.commons.lang3.StringUtils.join; import java.io.File; import java.util.ArrayList; import java.util.List; public class ResourceUtils { public static String getRelativePath(String targetPath, String basePath, String pathSeparator) { File baseFile = new File(basePath); if (baseFile.isFile() || !baseFile.exists() && !basePath.endsWith("/") && !basePath.endsWith("\\")) basePath = baseFile.getParent(); String target = separatorsToUnix(normalizeNoEndSeparator(targetPath)); String base = separatorsToUnix(normalizeNoEndSeparator(basePath)); String commonPrefix = getCommonPrefix(target, base); if (isBlank(commonPrefix)) return targetPath.replaceAll("/", pathSeparator); target = target.replaceFirst(commonPrefix, ""); base = base.replaceFirst(commonPrefix, ""); List<String> result = new ArrayList<>(); if (isNotEmpty(base)) result.addAll(nCopies(base.split("/").length, "..")); result.addAll(asList(target.replaceFirst("^/", "").split("/"))); return join(result, pathSeparator); } } 

如果你正在编写一个Maven插件,你可以使用Plexus的PathTool

 import org.codehaus.plexus.util.PathTool; String relativeFilePath = PathTool.getRelativeFilePath(file1, file2); 

org.apache.ant有一个带有getRelativePath方法的FileUtils类。 还没有尝试过,但可能值得检查一下。

http://javadoc.haefelinger.it/org.apache.ant/1.7.1/org/apache/tools/ant/util/FileUtils.html#getRelativePath(java.io.File,java.io.File

伪代码:

  1. 按path分隔符(“/”)拆分string
  2. 通过迭代拆分string的结果find最大的共同path(所以你最终会在你的两个例子中得到“/ var / data”或“/ a”)
  3. return "." + whicheverPathIsLonger.substring(commonPath.length);