在PHP中获取绝对路径的相对路径

49

我注意到在输入标题时出现了一些类似的问题,但它们似乎不是关于PHP的。那么有没有使用PHP函数解决此问题的方法呢?

需要具体说明。

$a="/home/apache/a/a.php";
$b="/home/root/b/b.php";
$relpath = getRelativePath($a,$b); //needed function,should return '../../root/b/b.php'
任何好的建议吗?谢谢。

11
当你已经有真实路径时,你需要相对路径的用例是什么? - Tim Lytle
你能否发布一些类似的问题?编写 PHP 的端口比重新发明一切要容易。 - Crozin
2
@Tim Lytle,我认为在进行一些链接/包含的操作时可能会有一些意义。同时也是出于兴趣。 - Young
对于那些对PowerShell端口感兴趣的人,我已经写了一个:http://stackoverflow.com/questions/13239620/getting-relative-path-from-absolute-path-in-powershellt - ComFreek
1
已经提出了几个答案。我对它们进行了基准测试,因此在决定使用哪个答案之前,请先阅读我的帖子;-) - lucaferrario
用于创建符号链接。 - Bell
11个回答

72

试试这个:

function getRelativePath($from, $to)
{
    // some compatibility fixes for Windows paths
    $from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
    $to   = is_dir($to)   ? rtrim($to, '\/') . '/'   : $to;
    $from = str_replace('\\', '/', $from);
    $to   = str_replace('\\', '/', $to);

    $from     = explode('/', $from);
    $to       = explode('/', $to);
    $relPath  = $to;

    foreach($from as $depth => $dir) {
        // find first non-matching dir
        if($dir === $to[$depth]) {
            // ignore this directory
            array_shift($relPath);
        } else {
            // get number of remaining dirs to $from
            $remaining = count($from) - $depth;
            if($remaining > 1) {
                // add traversals up to first matching dir
                $padLength = (count($relPath) + $remaining - 1) * -1;
                $relPath = array_pad($relPath, $padLength, '..');
                break;
            } else {
                $relPath[0] = './' . $relPath[0];
            }
        }
    }
    return implode('/', $relPath);
}
这将会给出。
$a="/home/a.php";
$b="/home/root/b/b.php";
echo getRelativePath($a,$b), PHP_EOL;  // ./root/b/b.php

$a="/home/apache/a/a.php";
$b="/home/root/b/b.php";
echo getRelativePath($a,$b), PHP_EOL; // ../../root/b/b.php

并且

$a="/home/root/a/a.php";
$b="/home/apache/htdocs/b/en/b.php";
echo getRelativePath($a,$b), PHP_EOL; // ../../apache/htdocs/b/en/b.php

$a="/home/apache/htdocs/b/en/b.php";
$b="/home/root/a/a.php";
echo getRelativePath($a,$b), PHP_EOL; // ../../../../root/a/a.php

1
看起来我们已经想出了几乎完全相同的算法,我的是用英语编写的,你的是用PHP编写的(我太懒了不想写代码,而你不是)。 - webbiedave
1
我测试了这个和所有其他的函数,你可以在我的帖子中向下滚动找到我的基准测试结果。这个实现是最好的,所以我建议使用它!;-) - lucaferrario
请勿使用此函数,在这种情况下它会返回错误的结果 getRelativePath("/home/some/../ws.a", "/home/modules/../ws.b"); => ../../modules/../ws.bhttp://sandbox.onlinephpfunctions.com/code/2634034cb4b3688b319a97b5a45ea2442ac2d419 - Edwin Rodríguez
1
@EdwinRodríguez 您正在传递相对路径。OP 仅要求输入绝对路径。请先通过 realpath 运行它们以使其成为绝对路径。 - Gordon
@Gordon 我传递绝对路径,因为它们包含根目录。如果文件在文件系统中不存在,运行 realpath 也无法解决问题。 - Edwin Rodríguez
@EdwinRodríguez 好的,那就不要使用这个函数。它在你的特定场景中无法工作。 - Gordon

20

由于我们已经有了几个答案,我决定测试它们并进行基准测试。 我使用以下路径进行测试:

$from = "/var/www/sites/web/mainroot/webapp/folder/sub/subf/subfo/subfol/subfold/lastfolder/"; 注意:如果它是文件夹,则必须为函数放置尾随斜杠才能正常工作! 所以__DIR__将不起作用。 相反,请使用__FILE____DIR__ . '/'

$to = "/var/www/sites/web/mainroot/webapp/folder/aaa/bbb/ccc/ddd";

结果:(小数分隔符为逗号,千分位分隔符为点)

  • Gordon的函数:结果正确,100,000次执行的时间1.222
  • Youg的函数:结果正确,100,000次执行的时间1.540
  • Ceagle的函数:结果错误(它适用于某些路径但在其他一些路径上失败,例如在测试中使用和上面写的那些路径)
  • Loranger的函数:结果错误(它适用于某些路径但在其他一些路径上失败,例如在测试中使用和上面写的那些路径)

因此,我建议您使用Gordon的实现!(标记为答案的那一个)

Youg的函数也不错,并且对于简单目录结构(例如“a / b /c.php”)执行效果更佳,而Gordon的函数则对具有许多子目录(例如在此基准测试中使用的那些目录)的复杂结构执行效果更佳。


注意:我在下面写入了使用$from$to作为输入返回的结果,以便您可以验证其中2个正确,而另外2个错误:

  • Gordon:../../../../../../aaa/bbb/ccc/ddd --> 正确
  • Youg:../../../../../../aaa/bbb/ccc/ddd --> 正确
  • Ceagle:../../../../../../bbb/ccc/ddd --> 错误
  • Loranger:../../../../../aaa/bbb/ccc/ddd --> 错误

不错的基准测试。你按照要求对文件夹进行了测试,而不是文件,这就是我的函数未能返回正确路径的原因。无论如何,你是对的,为了可靠性,该函数应始终返回正确的路径,无论它使用文件还是文件夹,所以我只是简单地修复了这个问题。我很想知道我的结果。你能再次进行基准测试吗? - loranger
1
对不起Loranger,编写一个可靠的基准测试脚本花费了我一些时间...不幸的是,我现在没有它了,也没有时间再写另一个来重复测试。无论如何,如果你已经解决了这个问题,做得好! :-) - lucaferrario

9
相对路径?这似乎更像旅行路径。您似乎想知道从路径A到路径B的路径。如果是这种情况,您可以在'/'上以$a和$b为参数使用explode函数,然后反向循环$aParts,将它们与$bParts的相同索引进行比较,直到找到“公共分母”目录(记录沿途的循环次数)。然后创建一个空字符串,并将'../'添加到其中$numLoops-1次,然后将$b减去公共分母目录添加到该字符串中。

6
const DS = DIRECTORY_SEPARATOR; // for convenience

function getRelativePath($from, $to) {
    $dir = explode(DS, is_file($from) ? dirname($from) : rtrim($from, DS));
    $file = explode(DS, $to);

    while ($dir && $file && ($dir[0] == $file[0])) {
        array_shift($dir);
        array_shift($file);
    }
    return str_repeat('..'.DS, count($dir)) . implode(DS, $file);
}

我的目标是刻意简化,虽然在性能方面可能没有什么不同。 我将把基准测试留给好奇的读者作为练习。 然而,这是相当强大的,并且应该是跨平台的。
请注意使用array_intersect函数的解决方案,因为如果并行目录具有相同的名称,这些解决方案将会失效。 例如,getRelativePath('start / A / end /','start / B / end /'将返回“../ end”,因为array_intersect会找到所有相同的名称,在本例中有2个,但实际上只应该有1个。

这几乎与我刚想出的解决方案完全相同,只是用了str_repeatdirname使代码更加简洁。太棒了!谢谢。 - mpen

3
这段代码取自Symfony URL生成器的源码:https://github.com/symfony/Routing/blob/master/Generator/UrlGenerator.php
    /**
     * Returns the target path as relative reference from the base path.
     *
     * Only the URIs path component (no schema, host etc.) is relevant and must be given, starting with a slash.
     * Both paths must be absolute and not contain relative parts.
     * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
     * Furthermore, they can be used to reduce the link size in documents.
     *
     * Example target paths, given a base path of "/a/b/c/d":
     * - "/a/b/c/d"     -> ""
     * - "/a/b/c/"      -> "./"
     * - "/a/b/"        -> "../"
     * - "/a/b/c/other" -> "other"
     * - "/a/x/y"       -> "../../x/y"
     *
     * @param string $basePath   The base path
     * @param string $targetPath The target path
     *
     * @return string The relative target path
     */
    function getRelativePath($basePath, $targetPath)
    {
        if ($basePath === $targetPath) {
            return '';
        }

        $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath);
        $targetDirs = explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? substr($targetPath, 1) : $targetPath);
        array_pop($sourceDirs);
        $targetFile = array_pop($targetDirs);

        foreach ($sourceDirs as $i => $dir) {
            if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
                unset($sourceDirs[$i], $targetDirs[$i]);
            } else {
                break;
            }
        }

        $targetDirs[] = $targetFile;
        $path = str_repeat('../', count($sourceDirs)).implode('/', $targetDirs);

        // A reference to the same base directory or an empty subdirectory must be prefixed with "./".
        // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
        // as the first segment of a relative-path reference, as it would be mistaken for a scheme name
        // (see http://tools.ietf.org/html/rfc3986#section-4.2).
        return '' === $path || '/' === $path[0]
            || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
            ? "./$path" : $path;
    }

2
基于戈登函数,我的解决方案如下:
function getRelativePath($from, $to)
{
   $from = explode('/', $from);
   $to = explode('/', $to);
   foreach($from as $depth => $dir)
   {

        if(isset($to[$depth]))
        {
            if($dir === $to[$depth])
            {
               unset($to[$depth]);
               unset($from[$depth]);
            }
            else
            {
               break;
            }
        }
    }
    //$rawresult = implode('/', $to);
    for($i=0;$i<count($from)-1;$i++)
    {
        array_unshift($to,'..');
    }
    $result = implode('/', $to);
    return $result;
}

2

常见情况的简单一句话:

str_replace(getcwd() . DIRECTORY_SEPARATOR, '', $filepath)

或者:

substr($filepath, strlen(getcwd())+1)

要检查路径是否为绝对路径,请尝试:

$filepath[0] == DIRECTORY_SEPARATOR

1

以下是我的解决方法。出于某种未知原因,对于这个问题最受欢迎的答案并没有按预期工作。

public function getRelativePath($absolutePathFrom, $absolutePathDestination)
{
    $absolutePathFrom = is_dir($absolutePathFrom) ? rtrim($absolutePathFrom, "\/")."/" : $absolutePathFrom;
    $absolutePathDestination = is_dir($absolutePathDestination) ? rtrim($absolutePathDestination, "\/")."/" : $absolutePathDestination;
    $absolutePathFrom = explode("/", str_replace("\\", "/", $absolutePathFrom));
    $absolutePathDestination = explode("/", str_replace("\\", "/", $absolutePathDestination));
    $relativePath = "";
    $path = array();
    $_key = 0;
    foreach($absolutePathFrom as $key => $value)
    {
        if (strtolower($value) != strtolower($absolutePathDestination[$key]))
        {
            $_key = $key + 1;
            for ($i = $key; $i < count($absolutePathDestination); $i++)
            {
                $path[] = $absolutePathDestination[$i];
            }
            break;
        }
    }
    for ($i = 0; $i <= (count($absolutePathFrom) - $_key - 1); $i++)
    {
        $relativePath .= "../";
    }

    return $relativePath.implode("/", $path);
}

如果$a = "C:\xampp\htdocs\projects\SMS\App\www\App\index.php"并且
    $b = "C:\xampp\htdocs\projects\SMS\App/www/App/bin/bootstrap/css/bootstrap.min.css"
那么$c将是$b相对于$a的路径,即
$c = getRelativePath($a, $b) = "bin/bootstrap/css/bootstrap.min.css"

1

有些原因,戈登的方法对我不起作用... 这是我的解决方案

function getRelativePath($from, $to) {
    $patha = explode('/', $from);
    $pathb = explode('/', $to);
    $start_point = count(array_intersect($patha,$pathb));
    while($start_point--) {
        array_shift($patha);
        array_shift($pathb);
    }
    $output = "";
    if(($back_count = count($patha))) {
        while($back_count--) {
            $output .= "../";
        }
    } else {
        $output .= './';
    }
    return $output . implode('/', $pathb);
}

也许您在$from中输入了文件夹路径但没有加上斜杠。Gordon和Young的这些函数需要文件夹路径后面加上斜杠。不幸的是,您的函数可以处理一些路径,但对于其他路径则会失败。请阅读我在另一篇帖子中所做的测试。该函数似乎不可靠,不应使用。 - lucaferrario

1

我使用相同的数组操作得出了相同的结果:

function getRelativePath($path, $from = __FILE__ )
{
    $path = explode(DIRECTORY_SEPARATOR, $path);
    $from = explode(DIRECTORY_SEPARATOR, dirname($from.'.'));
    $common = array_intersect_assoc($path, $from);

    $base = array('.');
    if ( $pre_fill = count( array_diff_assoc($from, $common) ) ) {
        $base = array_fill(0, $pre_fill, '..');
    }
    $path = array_merge( $base, array_diff_assoc($path, $common) );
    return implode(DIRECTORY_SEPARATOR, $path);
}

第二个参数是相对路径的文件。它是可选的,因此您可以获取相对路径,而不管当前所在的网页。 为了与@Young或@Gordon的示例一起使用,因为您想要知道从$a到$b的相对路径,您将不得不使用

getRelativePath($b, $a);

1
很不幸,你的函数可以处理一些路径,但对于其他一些路径则失败了。请阅读我在另一篇帖子中所做的测试。该函数似乎不可靠,不应该使用。相反,我建议使用Gordon的函数,它总是返回正确的结果。 - lucaferrario
@lucaferrario,请查看我上面的评论 - loranger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接