从不同格式的 YouTube URL 字符串中解析视频 ID 和开始时间

问题描述 投票:0回答:4

我需要从用户可以输入的任何类型的 YouTube 网址中提取

video Id
start time
。我有一个可行的解决方案,但它不正确。

问题:

  • 有人可以帮我修复 preg_match 模式来处理测试中注释的 url 吗?
  • 还有其他类型的 YouTube 网址吗?

2024/01/16更新:它也必须与播放列表一起工作

我已经检查了this stackoverflow 页面来构建我自己的 youtube url 解析器。

此 preg_match 可以提取

video Id
start time
但无法处理许多不同的 YouTube 网址格式:

preg_match("/[a-zA-Z\/\/:\.]*youtu(?:be.com\/watch\?v=|.be\/)([a-zA-Z0-9\-_]+)(?:[&?\/]t=)?(\d*)(?:[a-zA-Z0-9\/\*\-\_\?\&\;\%\=\.]*)/i", $url, $matches);

此 preg_match 处理许多不同的 YouTube 网址(也许是所有类型?),但不提取开始时间:

preg_match("/^(?:http(?:s)?:\/\/)?(?:www\.)?(?:m\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user|shorts)\/))([^\?&\"'>]+)/", $url, $matches);

我已经更改了它并且它对我有用,但我知道我的更改是不正确的,因为我没有正确解析网址的末尾:

preg_match("/^(?:http(?:s)?:\/\/)?(?:www\.)?(?:m\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user|shorts)\/))([^\?&\"'>]+)(?:[&?\/]t=)?(\d*)/", $url, $matches);

代码

<?php
declare(strict_types=1);

namespace AppBundle\Value;

class YoutubeVideoData
{
    private function __construct(public ?string $videoId = null, public ?int $time = null)
    {
    }

    public static function fromUrl(string $url): self
    {
        // `#action=share` is not supported
        preg_match("/^(?:http(?:s)?:\/\/)?(?:www\.)?(?:m\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user|shorts)\/))([^\?&\"'>]+)(?:[&?\/]t=)?(\d*)/", $url, $matches);

        $videoId = null;
        if (isset($matches[1])) {
            $videoId = $matches[1];
        }

        $time = null;
        if (isset($matches[2]) && $matches[2] !== "") {
            $time = (int) $matches[2];
        }

        return new self($videoId, $time);
    }

}

测试:

<?php

namespace Justimmo\Tests\Value;

use AppBundle\Value\YoutubeVideoData;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

/**
 * @covers \AppBundle\Value\YoutubeVideoData::class
 */
class YoutubeVideoDataTest extends TestCase
{
    #[DataProvider('urlProvider')]
    public function testUrls(string $url, ?string $expectedVideoId, ?int $expectedTime)
    {
        $videoData = YoutubeVideoData::fromUrl($url);

        $this->assertSame($expectedVideoId, $videoData->videoId);
        $this->assertSame($expectedTime, $videoData->time);
    }

    public static function urlProvider(): iterable
    {
        // playlist
        // playlist
        yield 'youtube_link_pl1' => ['https://www.youtube.com/watch?v=YjdIF7PuUug&list=PLiIQbaWYR99iZFpLIJ5SImA2y8DaDTv9G&index=21', 'YjdIF7PuUug', null];
        yield 'youtube_link_pl2' => ['https://www.youtube.com/watch?list=PLiIQbaWYR99iZFpLIJ5SImA2y8DaDTv9G&v=YjdIF7PuUug&index=21', 'YjdIF7PuUug', null];
        yield 'youtube_link_pl3' => ['https://youtu.be/YjdIF7PuUug?list=PLiIQbaWYR99iZFpLIJ5SImA2y8DaDTv9G', 'YjdIF7PuUug', null];

        // without https://www
        yield 'youtube_link_1' => ['youtube.com/v/dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_2' => ['youtube.com/v/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_3' => ['youtube.com/vi/dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_4' => ['youtube.com/?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_5' => ['youtube.com/?vi=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_6' => ['youtube.com/watch?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_7' => ['youtube.com/watch?vi=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_8' => ['youtu.be/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_9' => ['youtube.com/embed/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_10' => ['youtube.com/shorts/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_11' => ['m.youtube.com/watch?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        // without https://
        yield 'youtube_link_12' => ['www.youtube.com/v/dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_13' => ['www.youtube.com/v/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_14' => ['www.youtube.com/vi/dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_15' => ['www.youtube.com/?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_16' => ['www.youtube.com/?vi=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_17' => ['www.youtube.com/watch?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_18' => ['www.youtube.com/watch?vi=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_19' => ['www.youtu.be/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_20' => ['www.youtube.com/embed/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_21' => ['www.youtube.com/shorts/dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        // http
        yield 'youtube_link_22' => ['http://youtube.com/v/dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_23' => ['http://youtube.com/v/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_24' => ['http://youtube.com/vi/dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_25' => ['http://www.youtube.com/?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_26' => ['http://www.youtube.com/?vi=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_27' => ['http://www.youtube.com/watch?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_28' => ['http://www.youtube.com/watch?vi=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_29' => ['http://www.youtu.be/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_30' => ['http://youtube.com/embed/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_31' => ['http://www.youtube.com/shorts/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_32' => ['http://m.youtube.com/watch?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        // https
        yield 'youtube_link_33' => ['https://youtube.com/v/dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_34' => ['https://youtube.com/v/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_35' => ['https://youtube.com/vi/dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_36' => ['https://www.youtube.com/?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_37' => ['https://www.youtube.com/?vi=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_38' => ['https://www.youtube.com/watch?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_39' => ['https://www.youtube.com/watch?vi=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        yield 'youtube_link_40' => ['https://www.youtu.be/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_41' => ['https://youtube.com/embed/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_42' => ['https://www.youtube.com/shorts/dE5jPNvLvOk', 'dE5jPNvLvOk', null];
        yield 'youtube_link_43' => ['https://m.youtube.com/watch?v=dE5jPNvLvOk', 'dE5jPNvLvOk', null];

        // with start time
        yield 'youtube_link_44' => ['https://youtube.com/v/dE5jPNvLvOk?t=30', 'dE5jPNvLvOk', 30];

        yield 'youtube_link_45' => ['https://youtube.com/v/dE5jPNvLvOk?t=30', 'dE5jPNvLvOk', 30];
        yield 'youtube_link_46' => ['https://youtube.com/vi/dE5jPNvLvOk?t=30', 'dE5jPNvLvOk', 30];

        yield 'youtube_link_47' => ['https://www.youtube.com/?v=dE5jPNvLvOk&t=30', 'dE5jPNvLvOk', 30];
        yield 'youtube_link_48' => ['https://www.youtube.com/?vi=dE5jPNvLvOk&t=30', 'dE5jPNvLvOk', 30];

        yield 'youtube_link_49' => ['https://www.youtube.com/watch?v=dE5jPNvLvOk&t=30', 'dE5jPNvLvOk', 30];
        yield 'youtube_link_50' => ['https://www.youtube.com/watch?vi=dE5jPNvLvOk&t=30', 'dE5jPNvLvOk', 30];

        yield 'youtube_link_51' => ['https://www.youtu.be/dE5jPNvLvOk?t=30', 'dE5jPNvLvOk', 30];
        yield 'youtube_link_52' => ['https://youtube.com/embed/dE5jPNvLvOk?t=30', 'dE5jPNvLvOk', 30];
        yield 'youtube_link_53' => ['https://www.youtube.com/shorts/dE5jPNvLvOk?t=30', 'dE5jPNvLvOk', 30];
        yield 'youtube_link_54' => ['https://m.youtube.com/watch?v=dE5jPNvLvOk&t=30', 'dE5jPNvLvOk', 30];

        // with feature
        yield 'youtube_link_55' => ['https://www.youtube.com/watch?dev=inprogress&v=7HCZvhRAk-M&feature=related', '7HCZvhRAk-M', null];

        yield 'youtube_link_56' => ['https://youtube.com/v/dE5jPNvLvOk?feature=youtube_gdata_player', 'dE5jPNvLvOk', null];

        yield 'youtube_link_57' => ['https://youtube.com/v/dE5jPNvLvOk?feature=youtube_gdata_player', 'dE5jPNvLvOk', null];
        yield 'youtube_link_58' => ['https://youtube.com/vi/dE5jPNvLvOk?feature=youtube_gdata_player', 'dE5jPNvLvOk', null];

        yield 'youtube_link_59' => ['https://www.youtube.com/?v=dE5jPNvLvOk&feature=youtube_gdata_player', 'dE5jPNvLvOk', null];
        yield 'youtube_link_60' => ['https://www.youtube.com/?vi=dE5jPNvLvOk&feature=youtube_gdata_player', 'dE5jPNvLvOk', null];

        yield 'youtube_link_61' => ['https://www.youtube.com/watch?v=dE5jPNvLvOk&feature=youtube_gdata_player', 'dE5jPNvLvOk', null];
        yield 'youtube_link_62' => ['https://www.youtube.com/watch?vi=dE5jPNvLvOk&feature=youtube_gdata_player', 'dE5jPNvLvOk', null];

        yield 'youtube_link_63' => ['https://www.youtu.be/dE5jPNvLvOk?feature=youtube_gdata_player', 'dE5jPNvLvOk', null];
        yield 'youtube_link_64' => ['https://youtube.com/embed/dE5jPNvLvOk?feature=youtube_gdata_player', 'dE5jPNvLvOk', null];
        yield 'youtube_link_65' => ['https://www.youtube.com/shorts/dE5jPNvLvOk?feature=youtube_gdata_player', 'dE5jPNvLvOk', null];
        yield 'youtube_link_66' => ['https://m.youtube.com/watch?v=dE5jPNvLvOk&feature=youtube_gdata_player', 'dE5jPNvLvOk', null];

        // these tests do not work - with #action=share
//        yield 'youtube_link_67' => ['https://youtube.com/v/dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
//
//        yield 'youtube_link_68' => ['https://youtube.com/v/dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
//        yield 'youtube_link_69' => ['https://youtube.com/vi/dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
//
//        yield 'youtube_link_70' => ['https://www.youtube.com/?v=dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
//        yield 'youtube_link_71' => ['https://www.youtube.com/?vi=dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
//
//        yield 'youtube_link_72' => ['https://www.youtube.com/watch?v=dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
//        yield 'youtube_link_73' => ['https://www.youtube.com/watch?vi=dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
//
//        yield 'youtube_link_74' => ['https://www.youtu.be/dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
//        yield 'youtube_link_75' => ['https://youtube.com/embed/dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
//        yield 'youtube_link_76' => ['https://www.youtube.com/shorts/dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
//        yield 'youtube_link_77' => ['https://m.youtube.com/watch?v=dE5jPNvLvOk#action=share ', 'dE5jPNvLvOk', null];
    }

}
php regex youtube text-extraction urlparse
4个回答
5
投票

您可以使用 PHP 的

parse_url
parse_str
方法,而不是使用正则表达式。

$urlParts = parse_url($url);
$queryParams = [];
parse_str($urlParts['query'], $queryParams);

$videoId =
    $queryParams['vi'] ?: (
        $queryParams['v'] ?: (
            basename($urlParts['path'])
        )
    );

$time =
    isset($queryParams['t']) ? (int) $queryParams['t'] :
    null;

我还没有针对您的所有测试用例进行过尝试,但这似乎是一种更可靠的方法。

如果您想对结果 $videoId 值更有信心,您甚至可以将此与 KIKO Software 的答案 (

https://stackoverflow.com/a/79358821/20418616
) 结合起来。


1
投票

我不太喜欢又长又复杂的正则表达式。它们很难理解,而且太多可能会出错。为什么不采用稍微不同的方法呢?

假设我们采用这样的 youTube URL:

https://www.youtube.com/watch?v=Og40mpl8VNc

视频 ID 为

Og40mpl8VNc
。这是一个 base64 编码 的数字。这是 URL 中唯一经过 Base64 编码的内容。所有其他部分,如
https
www
youtube
com
等)都不是有效的 Base64 编码字符串。也许我们可以用这个?

检查某个内容是否为有效的 Base64 字符串的一个简单方法是对其进行解码和重新编码。那不应该改变字符串。只有

Og40mpl8VNc
可以在不改变的情况下解码和重新编码。

我们可以使用您的

urlProvider()
方法将此检查应用于您所有 Youtube 视频 URL 的所有部分:

$youtubeId = [];
foreach (urlProvider() as $data) {
    foreach (array_reverse(preg_split('/[\/?=#&]/', $data[0])) as $part) {
       if (trim(base64_encode(base64_decode($part, true)) , '=') == $part) {
          $youtubeId[$data[0]] = $part; 
          break;
       }
    }
}

现场演示:https://3v4l.org/KKuRl

我将 URL 拆分为这五个字符:

/?=#&
,以获取所有部分。然后反转这些,因为视频 id 通常位于末尾,然后遍历所有部分寻找有效的 base64 字符串。

请注意,我还删除了所有 base64

=
填充。

现在无可否认,我还没有彻底测试过这一点。这只是一个想法。显然,误报的风险非常小,但我希望这种风险可以忽略不计。

现在我没有花时间,因为我认为你可以很容易地得到它,因为它始终是一个参数。我也没有检查整个 URL 是否有效,但我认为这并不是您真正想知道的。


1
投票

下面的正则表达式清除了 80 个链接(更新为清除 3 个播放列表链接)。*

(http:\/\/|https:\/\/)?(((www.)?youtu.be\/)|(www.|m.)?youtube.com\/((embed\/|shorts\/)|(\?|(watch\?(dev=inprogress&|list=[0-9a-zA-Z_-]+&)?)?)?vi?(\/|=)))(([0-9a-zA-Z_-])+)([$=#&?\/'"\s])(t=([0-9]+(:[0-9](2))*)*)?
<sup> (NOTE: Above updated to include playlist 19JAN2025.)

它将捕获

<video-id>
<start-time>
。开始时间可以是整数,也可以是整数后跟冒号后跟 2 位整数,后跟冒号后跟 2 位整数...,例如
t=2055577:33:44:00


在 PHP** 中,您可以通过在捕获括号的左括号“

?<video-id>
”之后立即添加
?<start-time>
(
来识别/捕获组模式,在本例中为 和 模式,如下所示( *):
(?<video-id>([0-9a-zA-Z_-])+)
(?<start-time>t=([0-9]+(:[0-9](2))*)*)?

这是 PHP 的更新正则表达式,带有

<video-id>
<start-time>
组标识符:

(http:\/\/|https:\/\/)?(((www.)?youtu.be\/)|(www.|m.)?youtube.com\/((embed\/|shorts\/)|(\?|(watch\?(dev=inprogress&|list=[0-9a-zA-Z_-]+&)?)?)?vi?(\/|=)))(?<video-id>([0-9a-zA-Z_-])+)([$=#&?\/'"\s])(?<start-time>t=([0-9]+(:[0-9](2))*)*)?

(注:以上内容已更新,包含 2025 年 1 月 19 日的播放列表。)

* 在 https://regex101.com/ 测试正则表达式模式,适用于 PHP>=7.3

** PHP 的正则表达式捕获组位于 * https://www.phptutorial.net/php-tutorial/regex-capturing-groups/,


0
投票

我的观点是,如果您使用正则表达式或其他字符串手术工具来解析 PHP 已经具有本机解析器的内容,您的代码库将带有“updoc”的味道。

此外,您的任务似乎更多的是文本提取而不是文本验证,所以我假设我们始终使用合法的 youtube 格式的字符串。

解析 url,然后解析查询字符串(如果存在)。 然后,当您尝试从生成的数组中提取所需的值时,进行空合并。 与维护繁琐的正则表达式相比,这将为您省去很多麻烦。 演示

foreach (urlProvider() as $data) {
    unset($params);  // prevent previous iteration data bleeding into current iteration
    $components = parse_url($data[0]);
    if (isset($components['query'])) {
        parse_str($components['query'], $params);
    }
    var_export(
        [
           'id' => $params['vi'] ?? $params['v'] ?? basename($components['path'] ?? '') ?: null,
            't' => $params['t'] ?? null,
        ]
    );
}

此方法似乎适用于您提供的所有测试用例。

现在我正在查看其他答案,我的答案似乎是 Rob 答案的更可靠版本(他们的答案没有充分利用空合并)。

© www.soinside.com 2019 - 2024. All rights reserved.