如何从两个绝对路径(或 URL)构造 Java 中的相对路径?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/204784/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 11:20:57  来源:igfitidea点击:

How to construct a relative path in Java from two absolute paths (or URLs)?

javaurlfilepath

提问by VoidPointer

Given two absolute paths, e.g.

给定两个绝对路径,例如

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

How can one create a relative path that uses the second path as its base? In the example above, the result should be: ./stuff/xyz.dat

如何创建以第二条路径为基础的相对路径?在上面的例子中,结果应该是:./stuff/xyz.dat

采纳答案by Adam Crume

It's a little roundabout, but why not use URI? It has a relativize method which does all the necessary checks for you.

这有点绕,但为什么不使用 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"

Please note that for file path there's java.nio.file.Path#relativizesince Java 1.7, as pointed out by @Jirka Meluzinin the other answer.

请注意java.nio.file.Path#relativize,正如@Jirka Meluzin另一个答案中指出的那样,文件路径是从 Java 1.7 开始的。

回答by Keeg

If you know the second string is part of the first:

如果您知道第二个字符串是第一个字符串的一部分:

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

or if you really want the period at the beginning as in your example:

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

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

回答by matt b

Psuedo-code:

伪代码:

  1. Split the strings by the path seperator ("/")
  2. Find the greatest common path by iterating thru the result of the split string (so you'd end up with "/var/data" or "/a" in your two examples)
  3. return "." + whicheverPathIsLonger.substring(commonPath.length);
  1. 通过路径分隔符(“/”)分割字符串
  2. 通过遍历拆分字符串的结果来找到最大的公共路径(因此在您的两个示例中,您最终会得到“/var/data”或“/a”)
  3. return "." + whicheverPathIsLonger.substring(commonPath.length);

回答by Steve Armstrong

I'm assuming you have fromPath(an absolute path for a folder), and toPath(an absolute path for a folder/file), and your're looking for a path that with represent the file/folder in toPathas a relative path from fromPath(your current working directory is fromPath) then something like this should work:

我假设您有fromPath(文件夹的绝对路径)和toPath(文件夹/文件的绝对路径),并且您正在寻找将toPath 中的文件/文件夹表示为相对路径的路径从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();
}

回答by matt b

Actually my other answer didn't work if the target path wasn't a child of the base path.

实际上,如果目标路径不是基本路径的子级,则我的其他答案不起作用。

This should work.

这应该有效。

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?
        // i.e., 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/", "/"));
    }

}

回答by Christian K.

When using java.net.URI.relativize you should be aware of Java bug: JDK-6226081 (URI should be able to relativize paths with partial roots)

使用 java.net.URI.relativize 时,您应该注意 Java 错误: JDK-6226081(URI 应该能够将具有部分根的路径相对化)

At the moment, the relativize()method of URIwill only relativize URIs when one is a prefix of the other.

目前,只有当 URI 是另一个的前缀时, 的relativize()方法URI才会相对化 URI。

Which essentially means java.net.URI.relativizewill not create ".."'s for you.

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

回答by Gili

My version is loosely based on Mattand Steve's versions:

我的版本大致基于MattSteve的版本:

/**
 * 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());
 }

回答by Matuszek

Matt B's solution gets the number of directories to backtrack wrong -- it should be the length of the base path minus the number of common path elements, minus one (for the last path element, which is either a filename or a trailing ""generated by split). It happens to work with /a/b/c/and /a/x/y/, but replace the arguments with /m/n/o/a/b/c/and /m/n/o/a/x/y/and you will see the problem.

Matt B 的解决方案得到的目录数错误地回溯——它应该是基本路径的长度减去公共路径元素的数量,再减去一个(对于最后一个路径元素,它要么是文件名,要么是""由 生成的尾随split) . 它发生在与工作/a/b/c//a/x/y/,但替换的参数/m/n/o/a/b/c//m/n/o/a/x/y/,你会看到这个问题。

Also, it needs an else breakinside the first for loop, or it will mishandle paths that happen to have matching directory names, such as /a/b/c/d/and /x/y/c/z-- the cis in the same slot in both arrays, but is not an actual match.

此外,它需要else break在第一个 for 循环内部,否则它会错误处理碰巧具有匹配目录名称的路径,例如/a/b/c/d/and /x/y/c/z--c在两个数组中的同一插槽中,但不是实际匹配。

All these solutions lack the ability to handle paths that cannot be relativized to one another because they have incompatible roots, such as C:\foo\barand D:\baz\quux. Probably only an issue on Windows, but worth noting.

所有这些解决方案都缺乏处理无法相互相对化的路径的能力,因为它们具有不兼容的根,例如C:\foo\barD:\baz\quux。可能只是 Windows 上的一个问题,但值得注意。

I spent far longer on this than I intended, but that's okay. I actually needed this for work, so thank you to everyone who has chimed in, and I'm sure there will be corrections to this version too!

我在这方面花费的时间比我预期的要长得多,但这没关系。我实际上需要这个来工作,所以感谢所有参与进来的人,我相信这个版本也会有更正!

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;
}

And here are tests to cover several cases:

以下是涵盖多种情况的测试:

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);
}

回答by skaffman

The bug referred to in another answeris addressed by URIUtilsin Apache HttpComponents

在提到的错误另一个答案是解决URIUtilsApache的HttpComponents

public static URI resolve(URI baseURI,
                          String reference)

Resolves a URI reference against a base URI. Work-around for bug in java.net.URI ()

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

回答by Rachel

Cool!! I need a bit of code like this but for comparing directory paths on Linux machines. I found that this wasn't working in situations where a parent directory was the target.

凉爽的!!我需要一些这样的代码,但用于比较 Linux 机器上的目录路径。我发现这在父目录是目标的情况下不起作用。

Here is a directory friendly version of the method:

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

 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;
}