使用file_get_contents函数-修复相对URL问题

3

我正在尝试使用PHP下载网站并向用户显示该网站。以下是我使用的脚本:

<?php
$url = 'http://stackoverflow.com/pagecalledjohn.php';
//Download page
$site = file_get_contents($url);
//Fix relative URLs
$site = str_replace('src="','src="' . $url,$site);
$site = str_replace('url(','url(' . $url,$site);
//Display to user
echo $site;
?>

到目前为止,这个脚本运行得很好,除了str_replace函数有一些严重的问题。问题出在相对URL上。如果我们在名为john.php的虚构页面上使用一张猫的图片(类似这样:Cat)。它是一个png格式的图片,我发现它可以使用6个不同的URL放置在页面上:
1. src="//www.stackoverflow.com/cat.png"
2. src="http://www.stackoverflow.com/cat.png"
3. src="https://www.stackoverflow.com/cat.png"
4. src="somedirectory/cat.png" 

在这种情况下不适用于4,但仍然被添加了!

5. src="/cat.png"
6. src="cat.png"

有没有一种使用php的方法,可以搜索src =“并将其替换为正在下载的页面的url(文件名已删除),但如果它是选项1,2或3,则不要将url粘贴在那里,并更改4,5和6的过程稍微?

3个回答

9
不需要改变源代码中的路径引用,为什么不在头部注入 <base> 标签来明确指示所有相对 URL 应该基于哪个基础 URL 进行计算呢?
您可以使用您选择的 DOM 操作工具来实现此功能。以下示例演示如何使用 DOMDocument 和相关类完成此操作。
参考链接:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
$target_domain = 'http://stackoverflow.com/';
$url = $target_domain . 'pagecalledjohn.php';
//Download page
$site = file_get_contents($url);
$dom = DOMDocument::loadHTML($site);

if($dom instanceof DOMDocument === false) {
    // something went wrong in loading HTML to DOM Document
    // provide error messaging and exit
}

// find <head> tag
$head_tag_list = $dom->getElementsByTagName('head');
// there should only be one <head> tag
if($head_tag_list->length !== 1) {
    throw new Exception('Wow! The HTML is malformed without single head tag.');
}
$head_tag = $head_tag_list->item(0);

// find first child of head tag to later use in insertion
$head_has_children = $head_tag->hasChildNodes();
if($head_has_children) {
    $head_tag_first_child = $head_tag->firstChild;
}

// create new <base> tag
$base_element = $dom->createElement('base');
$base_element->setAttribute('href', $target_domain);

// insert new base tag as first child to head tag
if($head_has_children) {
    $base_node = $head_tag->insertBefore($base_element, $head_tag_first_child);
} else {
    $base_node = $head_tag->appendChild($base_element);
}

echo $dom->saveHTML();

如果你真的想修改源代码中的所有路径引用,至少我强烈建议使用DOM操作工具(如DOMDOcument、DOMXPath等),而不是正则表达式。我认为你会发现这是一个更加稳定的解决方案。


谢谢 - 你的解决方案是否有办法修复链接? - JBithell
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Mike Brant
1
为了更快地完成,您可以简单地执行类似于 str_replace('</head>', '<base href="http://www.stackoverflow.com"></head>',$site ) 的操作。 - zen

2

我不确定我是否完全理解了你的问题,如果你想处理所有被src=""包含的文本序列,下面的模式可以实现:

~(\ssrc=")([^"]+)(")~

这段代码包含三个捕获组,其中第二个包含您感兴趣的数据。第一个和最后一个有用于更改整个匹配项。

现在,您可以使用回调函数替换所有实例并更改位置。我已经创建了一个简单的字符串,其中包含您拥有的所有6种情况:

$site = <<<BUFFER
1. src="//www.stackoverflow.com/cat.png"
2. src="http://www.stackoverflow.com/cat.png"
3. src="https://www.stackoverflow.com/cat.png"
4. src="somedirectory/cat.png"
5. src="/cat.png"
6. src="cat.png"
BUFFER;

暂且不考虑周围的HTML标签,反正你也没有要求解析HTML而是要求使用正则表达式。在下面的例子中,中间匹配的内容(即URL)将被包含以便清楚地显示它已经匹配:

现在,为了替换每个链接,让我们从轻松的角度开始,只需在字符串中突出显示它们。

$pattern = '~(\ssrc=")([^"]+)(")~';

echo preg_replace_callback($pattern, function ($matches) {
    return $matches[1] . ">>>" . $matches[2] . "<<<" . $matches[3];
}, $site);

给定示例的输出如下:
1. src=">>>//www.stackoverflow.com/cat.png<<<"
2. src=">>>http://www.stackoverflow.com/cat.png<<<"
3. src=">>>https://www.stackoverflow.com/cat.png<<<"
4. src=">>>somedirectory/cat.png<<<"
5. src=">>>/cat.png<<<"
6. src=">>>cat.png<<<"

由于替换字符串的方式将被更改,因此可以进行提取,这样更容易进行更改:

$callback = function($method) {
    return function ($matches) use ($method) {
        return $matches[1] . $method($matches[2]) . $matches[3];
    };
};

这个函数根据您传递的替换方法创建替换回调函数。

这样的替换函数可能是:

$highlight = function($string) {
    return ">>>$string<<<";
};

它被称为以下内容:

$pattern = '~(\ssrc=")([^"]+)(")~';
echo preg_replace_callback($pattern, $callback($highlight), $site);

输出结果不变,这只是为了说明提取的工作原理:
1. src=">>>//www.stackoverflow.com/cat.png<<<"
2. src=">>>http://www.stackoverflow.com/cat.png<<<"
3. src=">>>https://www.stackoverflow.com/cat.png<<<"
4. src=">>>somedirectory/cat.png<<<"
5. src=">>>/cat.png<<<"
6. src=">>>cat.png<<<"

这样做的好处在于替换函数只需要处理URL匹配为单个字符串,而不是针对不同组的正则表达式匹配数组。
现在回答你问题的后半部分:如何用特定的URL处理方式来替换它,比如删除文件名。可以通过解析URL并从路径组件中删除文件名(基名)来完成此操作。由于提取作用,您可以将其放入一个简单的函数中:
$removeFilename = function ($url) {
    $url  = new Net_URL2($url);
    $base = basename($path = $url->getPath());
    $url->setPath(substr($path, 0, -strlen($base)));
    return $url;
};

这段代码使用了Pear的Net_URL2 URL组件(也可通过Packagist和Github获取,您的操作系统包可能也有它)。它可以轻松解析和修改URL,因此在工作中很有用。

现在,使用新的URL文件名替换函数完成替换:

$pattern = '~(\ssrc=")([^"]+)(")~';
echo preg_replace_callback($pattern, $callback($removeFilename), $site);

并且结果如下:
1. src="//www.stackoverflow.com/"
2. src="http://www.stackoverflow.com/"
3. src="https://www.stackoverflow.com/"
4. src="somedirectory/"
5. src="/"
6. src=""

请注意,这只是一个示例。它展示了如何使用正则表达式实现此操作。但您也可以使用HTML解析器来执行此操作。让我们将其制作成一个实际的HTML片段:
1. <img src="//www.stackoverflow.com/cat.png"/>
2. <img src="http://www.stackoverflow.com/cat.png"/>
3. <img src="https://www.stackoverflow.com/cat.png"/>
4. <img src="somedirectory/cat.png"/>
5. <img src="/cat.png"/>
6. <img src="cat.png"/>

接下来,使用创建的替换过滤函数处理所有<img>标签的"src"属性:

$doc   = new DOMDocument();
$saved = libxml_use_internal_errors(true);
$doc->loadHTML($site, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
libxml_use_internal_errors($saved);

$srcs = (new DOMXPath($doc))->query('//img/@hsrc') ?: [];
foreach ($srcs as $src) {
    $src->nodeValue = $removeFilename($src->nodeValue);
}

echo $doc->saveHTML();

结果再次是:
1. <img src="//www.stackoverflow.com/cat.png">
2. <img src="http://www.stackoverflow.com/cat.png">
3. <img src="https://www.stackoverflow.com/cat.png">
4. <img src="somedirectory/cat.png">
5. <img src="/cat.png">
6. <img src="cat.png">

使用了不同的解析方式,但替换仍然相同。只是提供两种部分相同的不同方式。


1
我建议分几步来做。
为了不让解决方案变得复杂,我们假设任何src值都是一个图像(它也可以是其他东西,例如脚本)。 此外,让我们假设没有空格,在等号和引号之间(如果有的话,这很容易修复)。最后,让我们假设文件名不包含任何转义引号(如果有,正则表达式会更复杂)。 因此,您可以使用以下正则表达式找到所有图像引用: src="([^"]*)"。(此外,这并不涵盖src被单引号括起来的情况。但是可以轻松创建类似于该情况的正则表达式。)
但是,处理逻辑可以使用preg_replace_callback函数完成,而不是使用str_replace。您可以提供一个回调函数给此函数,其中每个URL都可以根据其内容进行处理。
因此,您可以像这样做(未经测试!):
$site = preg_replace_callback(
    'src="([^"]*)"',
    function ($src) {
           $url = $src[1];
           $ret = "";
           if (preg_match("^//", $url)) {
               // case 1.
               $ret = "src='" . $url . '"';
           }
           else if (preg_match("^https?://", $url)) {
               // case 2. and 3.
               $ret = "src='" . $url . '"';
           }
           else {
               // case 4., 5., 6.
               $ret = "src='http://your.site.com.com/" . $url . '"';
           }
           return $ret;
    },
    $site
);

preg_replace_callback() 调用中调用 preg_ 函数表明第一个模式不足或次优。我不会考虑这种解决方案。 - mickmackusa

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