gpslab/sitemap

sitemap.xml 构建器

v2.0.1 2021-05-14 11:28 UTC

This package is auto-updated.

Last update: 2024-08-29 04:26:46 UTC


README

Latest Stable Version Build Status Coverage Status Scrutinizer Code Quality License

Sitemap.xml 生成框架

这是一个用于流式构建 Sitemap.xml 和 Sitemap.xml 索引的框架。

有关更多详细信息,请参阅 Sitemap.xml 协议

特性

  • 流式构建(节省内存);
  • 并行多流;
  • 指定本地化 URL 版本;
  • 自动计算 URL 优先级;
  • 自动计算 URL 更新频率;
  • 通过总链接数跟踪 Sitemap 溢出;
  • 通过使用大小跟踪 Sitemap 溢出;
  • 跟踪协议合规性;
  • gzip 和 deflate 压缩;
  • 为网站部分构建 Sitemap(不仅仅是根 sitemap.xml);
  • 将 URL 分组到多个 Sitemap 中;
  • 使用 URL 构建服务;
  • 使用多个 URL 构建服务创建 Sitemap;
  • 将 Sitemap 写入文件;
  • 将 Sitemap 发送到输出缓冲区;
  • 将 Sitemap 索引写入文件;
  • 在溢出时拆分 Sitemap;
  • 在溢出时拆分 Sitemap 并将部分 Sitemap 写入 Sitemap.xml 索引;
  • 将 Sitemap 写入临时文件夹以在构建过程中在目标路径中保存有效的 sitemap.xml
  • 通过 XMLWriter 渲染 Sitemap;
  • 以纯文本形式渲染 Sitemap,不带任何依赖项;
  • 压缩或格式化 XML;
  • XML 架构验证。

分组构建

这是一个通过控制台命令构建 sitemap.xml 的示例。在此示例中,所有网站链接都被分组,并为每个组创建了一个构建服务。在此示例中,从 6675 个链接中构建了一个 sitemap,但此方法也有助于构建大型网站地图,例如 100,000 或 500,000 个链接。

Example build sitemap.xml

安装

使用 Composer 非常简单,运行

composer require gpslab/sitemap

简单用法

// URLs on your site
$urls = [
    Url::create(
        'https://example.com/', // loc
        new \DateTimeImmutable('2020-06-15 13:39:46'), // lastmod
        ChangeFrequency::always(), // changefreq
        10 // priority
    ),
    Url::create(
        'https://example.com/contacts.html',
        new \DateTimeImmutable('2020-05-26 09:28:12'),
        ChangeFrequency::monthly(),
        7
    ),
    Url::create('https://example.com/about.html'),
];

// file into which we will write a sitemap
$filename = __DIR__.'/sitemap.xml';

// configure stream
$render = new PlainTextSitemapRender();
$writer = new TempFileWriter();
$stream = new WritingStream($render, $writer, $filename);

// build sitemap.xml
$stream->open();
foreach ($urls as $url) {
    $stream->push($url);
}
$stream->close();

结果 sitemap.xml

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
    <url>
        <loc>https://example.com/</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
        <changefreq>always</changefreq>
        <priority>1.0</priority>
    </url>
    <url>
        <loc>https://example.com//contacts.html</loc>
        <lastmod>2020-05-26T09:28:12+03:00</lastmod>
        <changefreq>monthly</changefreq>
        <priority>0.7</priority>
    </url>
    <url>
        <loc>https://example.com/about.html</loc>
    </url>
</urlset>

更改频率

页面可能更改的频率。此值向搜索引擎提供一般信息,可能不会与它们爬取页面的频率完全一致。

您可以定义它

  • 作为字符串

    $change_frequency = 'daily';
  • 作为常量

    $change_frequency = ChangeFrequency::DAILY;
  • 作为对象

    $change_frequency = ChangeFrequency::daily();

优先级

此 URL 相对于您网站上其他 URL 的优先级。有效值范围为 0.01.0。此值不会影响您的页面与其他网站页面的比较 - 它仅让搜索引擎机器人知道您认为哪些页面对于搜索引擎机器人来说最为重要。

您可以定义它

  • 作为字符串

    $priority = '0.5';
  • 作为浮点数

    $priority = .5;
  • 作为整数

    $priority = 5;
  • 作为对象

    $priority = Priority::create(5 /* string|float|int */);

页面本地化版本

如果您为不同的语言或地区提供了页面的多个版本,请告诉搜索引擎机器人有关这些不同版本的信息。这样做可以帮助搜索引擎机器人通过语言或地区将用户指向您页面的最合适版本。

// URLs on your site
$urls = [
    Url::create(
        'https://example.com/english/page.html',
        new \DateTimeImmutable('2020-06-15 13:39:46'),
        ChangeFrequency::monthly(),
        7,
        [
            'de' => 'https://example.com/deutsch/page.html',
            'de-ch' => 'https://example.com/schweiz-deutsch/page.html',
            'en' => 'https://example.com/english/page.html',
            'fr' => 'https://example.fr',
            'x-default' => 'https://example.com/english/page.html',
        ]
    ),
    Url::create(
        'https://example.com/deutsch/page.html',
        new \DateTimeImmutable('2020-06-15 13:39:46'),
        ChangeFrequency::monthly(),
        7,
        [
            'de' => 'https://example.com/deutsch/page.html',
            'de-ch' => 'https://example.com/schweiz-deutsch/page.html',
            'en' => 'https://example.com/english/page.html',
            'fr' => 'https://example.fr',
            'x-default' => 'https://example.com/english/page.html',
        ]
    ),
    Url::create(
        'https://example.com/schweiz-deutsch/page.html',
        new \DateTimeImmutable('2020-06-15 13:39:46'),
        ChangeFrequency::monthly(),
        7,
        [
            'de' => 'https://example.com/deutsch/page.html',
            'de-ch' => 'https://example.com/schweiz-deutsch/page.html',
            'en' => 'https://example.com/english/page.html',
            'fr' => 'https://example.fr',
            'x-default' => 'https://example.com/english/page.html',
        ]
    ),
];

您可以在同一域内简化同一页面的本地化版本 URL 的创建。

$urls = Url::createLanguageUrls(
    [
        'de' => 'https://example.com/deutsch/page.html',
        'de-ch' => 'https://example.com/schweiz-deutsch/page.html',
        'en' => 'https://example.com/english/page.html',
        'x-default' => 'https://example.com/english/page.html',
    ],
    new \DateTimeImmutable('2020-06-15 13:39:46'),
    ChangeFrequency::monthly(),
    7,
    [
        'fr' => 'https://example.fr',
    ]
);

结果 sitemap.xml

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
    <url>
        <loc>https://example.com/deutsch/page.html</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
        <changefreq>monthly</changefreq>
        <priority>0.7</priority>
        <xhtml:link rel="alternate" hreflang="de" href="https://example.com/deutsch/page.html"/>
        <xhtml:link rel="alternate" hreflang="de-ch" href="https://example.com/schweiz-deutsch/page.html"/>
        <xhtml:link rel="alternate" hreflang="en" href="https://example.com/english/page.html"/>
        <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/english/page.html"/>
        <xhtml:link rel="alternate" hreflang="fr" href="https://example.fr"/>
    </url>
    <url>
        <loc>https://example.com/schweiz-deutsch/page.html</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
        <changefreq>monthly</changefreq>
        <priority>0.7</priority>
        <xhtml:link rel="alternate" hreflang="de" href="https://example.com/deutsch/page.html"/>
        <xhtml:link rel="alternate" hreflang="de-ch" href="https://example.com/schweiz-deutsch/page.html"/>
        <xhtml:link rel="alternate" hreflang="en" href="https://example.com/english/page.html"/>
        <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/english/page.html"/>
        <xhtml:link rel="alternate" hreflang="fr" href="https://example.fr"/>
    </url>
    <url>
        <loc>https://example.com/english/page.html</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
        <changefreq>monthly</changefreq>
        <priority>0.7</priority>
        <xhtml:link rel="alternate" hreflang="de" href="https://example.com/deutsch/page.html"/>
        <xhtml:link rel="alternate" hreflang="de-ch" href="https://example.com/schweiz-deutsch/page.html"/>
        <xhtml:link rel="alternate" hreflang="en" href="https://example.com/english/page.html"/>
        <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/english/page.html"/>
        <xhtml:link rel="alternate" hreflang="fr" href="https://example.fr"/>
    </url>
</urlset>

URL 构建器

您可以创建一个服务,该服务将返回指向您网站页面的链接。

class MySiteUrlBuilder implements UrlBuilder
{
    public function getIterator(): \Traversable
    {
        // add URLs on your site
        return new \ArrayIterator([
          Url::create(
              'https://example.com/', // loc
              new \DateTimeImmutable('2020-06-15 13:39:46'), // lastmod
              ChangeFrequency::always(), // changefreq
              10 // priority
          ),
          Url::create(
              'https://example.com/contacts.html',
              new \DateTimeImmutable('2020-05-26 09:28:12'),
              ChangeFrequency::monthly(),
              7
          ),
          Url::create(
              'https://example.com/about.html',
              new \DateTimeImmutable('2020-05-02 17:12:38'),
              ChangeFrequency::monthly(),
              7
          ),
       ]);
    }
}

这是一个简单的构建。我们添加了一个更复杂的构建器。

class ArticlesUrlBuilder implements UrlBuilder
{
    private $pdo;

    public function __construct(\PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function getIterator(): \Traversable
    {
        $section_update_at = null;
        $sth = $this->pdo->query('SELECT id, update_at FROM article');
        $sth->execute();

        while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
            $update_at = new \DateTimeImmutable($row['update_at']);
            $section_update_at = max($section_update_at, $update_at);

            // smart URL automatically fills fields that it can
            yield Url::createSmart(
                sprintf('https://example.com/article/%d', $row['id']),
                $update_at
            );
        }

        // link to section
        if ($section_update_at !== null) {
            yield Url::createSmart('https://example.com/article/', $section_update_at);
        } else {
            yield Url::create(
                'https://example.com/article/',
                new \DateTimeImmutable('-1 day'),
                ChangeFrequency::daily(),
                9
            );
        }
    }
}

我们选择了一个现有的构建器并对其进行配置。

// collect a collection of builders
$builders = new MultiUrlBuilder([
    new MySiteUrlBuilder(),
    new ArticlesUrlBuilder(/* $pdo */),
]);

// file into which we will write a sitemap
$filename = __DIR__.'/sitemap.xml';

// configure stream
$render = new PlainTextSitemapRender();
$writer = new TempFileWriter();
$stream = new WritingStream($render, $writer, $filename);

// build sitemap.xml
$stream->open();
foreach ($builders as $url) {
    $stream->push($url);
}
$stream->close();

Sitemap 索引

您可以为多个 Sitemap 文件创建 Sitemap 索引。如果您已经创建了 Sitemap 的一部分,您可以简单地创建 Sitemap 索引。

// file into which we will write a sitemap
$filename = __DIR__.'/sitemap.xml';

// configure stream
$render = new PlainTextSitemapIndexRender();
$writer = new TempFileWriter();
$stream = new WritingIndexStream($render, $writer, $filename);

// build sitemap.xml index
$stream->open();
$stream->pushSitemap(new Sitemap('https://example.com/sitemap_main.xml', new \DateTimeImmutable('-1 hour')));
$stream->pushSitemap(new Sitemap('https://example.com/sitemap_news.xml', new \DateTimeImmutable('-1 hour')));
$stream->pushSitemap(new Sitemap('https://example.com/sitemap_articles.xml', new \DateTimeImmutable('-1 hour')));
$stream->close();

结果 sitemap.xml

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <sitemap>
        <loc>https://example.com/sitemap_main.xml</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://example.com/sitemap_news.xml</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://example.com/sitemap_articles.xml</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
    </sitemap>
</sitemapindex>

拆分 URL 并创建 Sitemap 索引

您可以将URL列表简化为分区并创建Sitemap索引。

您可以将URL推送到WritingSplitIndexStream流中,它会将它们写入Sitemap分区。当达到分区大小限制时,流关闭此分区,将其添加到索引并打开下一个分区。这简化了大Sitemap的构建并消除了对后续大小限制的需求。

您将从一个大量URL中获得Sitemap索引sitemap.xml和几个分区sitemap1.xmlsitemap2.xmlsitemapN.xml

// collect a collection of builders
$builders = new MultiUrlBuilder([
    new MySiteUrlBuilder(),
    new ArticlesUrlBuilder(/* $pdo */),
]);

// file into which we will write a sitemap
$index_filename = __DIR__.'/sitemap.xml';

$index_render = new PlainTextSitemapIndexRender();
$index_writer = new TempFileWriter();

// file into which we will write a sitemap part
// filename should contain a directive like "%d"
$part_filename = __DIR__.'/sitemap%d.xml';

// web path to the sitemap.xml on your site
$part_web_path = 'https://example.com/sitemap%d.xml';

$part_render = new PlainTextSitemapRender();
// separate writer for part
// it's better not to use one writer as a part writer and a index writer
// this can cause conflicts in the writer
$part_writer = new TempFileWriter();

// configure stream
$stream = new WritingSplitIndexStream(
    $index_render,
    $part_render,
    $index_writer,
    $part_writer,
    $index_filename,
    $part_filename,
    $part_web_path
);

$stream->open();

// build sitemap.xml index file and sitemap1.xml, sitemap2.xml, sitemapN.xml with URLs
foreach ($builders as $url) {
    $stream->push($url);
}

// you can add a link to a sitemap created earlier
$stream->pushSitemap(new Sitemap('https://example.com/sitemap_news.xml', new \DateTimeImmutable('-1 hour')));

$stream->close();

因此,您将得到如下文件结构

sitemap.xml
sitemap1.xml
sitemap2.xml
sitemap3.xml
sitemap_news.xml

结果 sitemap.xml

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <sitemap>
        <loc>https://example.com/sitemap1.xml</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://example.com/sitemap2.xml</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://example.com/sitemap3.xml</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://example.com/sitemap_news.xml</loc>
        <lastmod>2020-06-15T13:39:46+03:00</lastmod>
    </sitemap>
</sitemapindex>

将URL分组

您可能不想将所有URL都分成与WritingSplitIndexStream流相同的分区。您可能想创建几个分区组。例如,要创建只包含您网站新闻URL的分区组,一个文章分区组,以及一个包含所有其他URL的组。

这有助于识别特定URL组中的问题。此外,您还可以配置您的应用程序在必要时仅重新组装单个组,而不是整个地图。

注意。分区列表存储在WritingSplitStream流中,大量的分区将消耗内存。

// file into which we will write a sitemap
$index_filename = __DIR__.'/sitemap.xml';

$index_render = new PlainTextSitemapIndexRender();
$index_writer = new TempFileWriter();

// separate writer for part
$part_writer = new TempFileWriter();
$part_render = new PlainTextSitemapRender();

$index_stream = new WritingIndexStream($index_render, $index_writer, $index_filename);

// create a stream for news

// file into which we will write a sitemap part
// filename should contain a directive like "%d"
$news_filename = __DIR__.'/sitemap_news%d.xml';
// web path to sitemap parts on your site
$news_web_path = 'https://example.com/sitemap_news%d.xml';
$news_stream = new WritingSplitStream($part_render, $part_writer, $news_filename, $news_web_path);

// similarly create a stream for articles
$articles_filename = __DIR__.'/sitemap_articles%d.xml';
$articles_web_path = 'https://example.com/sitemap_articles%d.xml';
$articles_stream = new WritingSplitStream($part_render, $part_writer, $articles_filename, $articles_web_path);

// similarly create a main stream
$main_filename = __DIR__.'/sitemap_main%d.xml';
$main_web_path = 'https://example.com/sitemap_main%d.xml';
$main_stream = new WritingSplitStream($part_render, $part_writer, $main_filename, $main_web_path);

// build sitemap.xml index
$index_stream->open();

$news_stream->open();
// build parts of a sitemap group
foreach ($news_urls as $url) {
    $news_stream->push($url);
}

// add all parts to the index
foreach ($news_stream->getSitemaps() as $sitemap) {
    $index_stream->pushSitemap($sitemap);
}

// close the stream only after adding all parts to the index
// otherwise the list of parts will be cleared
$news_stream->close();

// similarly for articles stream
$articles_stream->open();
foreach ($article_urls as $url) {
    $articles_stream->push($url);
}
foreach ($articles_stream->getSitemaps() as $sitemap) {
    $index_stream->pushSitemap($sitemap);
}
$articles_stream->close();

// similarly for main stream
$main_stream->open();
foreach ($main_urls as $url) {
    $main_stream->push($url);
}
foreach ($main_stream->getSitemaps() as $sitemap) {
    $index_stream->pushSitemap($sitemap);
}
$main_stream->close();

// finish create index
$index_stream->close();

因此,您将得到如下文件结构

sitemap.xml
sitemap_news1.xml
sitemap_news2.xml
sitemap_news3.xml
sitemap_articles1.xml
sitemap_articles2.xml
sitemap_articles3.xml
sitemap_main1.xml
sitemap_main2.xml
sitemap_main3.xml

  • MultiStream - 允许将多个流作为单个流使用;
  • WritingStream - 使用Writer来写入Sitemap;
  • WritingIndexStream - 使用Writer写入Sitemap索引;
  • WritingSplitIndexStream - 将URL列表拆分为Sitemap部分,并使用Writer将其写入Sitemap索引;
  • WritingSplitStream - 拆分URL列表并使用Writer将其写入Sitemap;
  • OutputStream - 将Sitemap发送到输出缓冲区。您可以在控制器中使用它;
  • LoggerStream - 使用PSR-3来记录添加的URL。

您可以使用流的组合。

$stream = new MultiStream(
    new LoggerStream(/* $logger */),
    new WritingSplitIndexStream(
        new PlainTextSitemapIndexRender(),
        new PlainTextSitemapRender(),
        new TempFileWriter(),
        new GzipTempFileWriter(9),
         __DIR__.'/sitemap.xml',
         __DIR__.'/sitemap%d.xml.gz',
        'https://example.com/sitemap%d.xml.gz'
    )
);

将数据流到文件并压缩结果,不包含索引。

$render = new PlainTextSitemapRender();

$stream = new MultiStream(
    new LoggerStream(/* $logger */),
    new WritingStream($render, new GzipTempFileWriter(9), __DIR__.'/sitemap.xml.gz'),
    new WritingStream($render, new TempFileWriter(), __DIR__.'/sitemap.xml')
);

将数据流到文件和输出缓冲区。

$render = new PlainTextSitemapRender();

$stream = new MultiStream(
    new LoggerStream(/* $logger */),
    new WritingStream($render, new TempFileWriter(), __DIR__.'/sitemap.xml'),
    new OutputStream($render)
);

Writer

  • FileWriter - 将Sitemap写入文件;
  • TempFileWriter - 将Sitemap写入临时文件,完成后将其移动到目标目录;
  • GzipFileWriter - 将Sitemap写入使用gzip压缩的文件;
  • GzipTempFileWriter - 将Sitemap写入使用gzip压缩的临时文件,完成后将其移动到目标目录。
  • DeflateFileWriter - 将Sitemap写入使用deflate压缩的文件;
  • DeflateTempFileWriter - 将Sitemap写入使用deflate压缩的临时文件,完成后将其移动到目标目录。

渲染

如果您安装了XMLWriter PHP扩展,您可以使用XMLWriterSitemapRenderXMLWriterSitemapIndexRender。否则,您可以使用不要求任何依赖的PlainTextSitemapRenderPlainTextSitemapIndexRender,它们更为节省资源。

Sitemap文件的存储位置

Sitemap协议对Sitemap文件中可以指定的URL施加限制,具体取决于Sitemap文件的位置

  • 列在Sitemap中的所有URL必须使用相同的协议(例如,在此例中为https)并且位于与Sitemap相同的宿主上。例如,如果Sitemap位于https://www.example.com/sitemap.xml,则不能包含来自http://www.example.com/https://subdomain.example.com的URL。
  • 一个 Sitemap 文件的位置决定了可以包含在该 Sitemap 中的 URL 集合。位于 https://example.com/catalog/sitemap.xml 的 Sitemap 文件可以包含以 https://example.com/catalog/ 开头的任何 URL,但不能包含以 https://example.com/news/ 开头的 URL。
  • 如果您使用带有端口号的路径提交 Sitemap,则必须在 Sitemap 文件中列出每个 URL 的路径中包含该端口号。例如,如果您的 Sitemap 位于 http://www.example.com:100/sitemap.xml,那么 Sitemap 中列出的每个 URL 必须以 http://www.example.com:100 开头。
  • Sitemap 索引文件只能指定与 Sitemap 索引文件位于同一站点的 Sitemap。例如,https://www.yoursite.com/sitemap_index.xml 可以包含在 https://www.yoursite.com 上的 Sitemap,但不能包含在 http://www.yoursite.comhttps://www.example.comhttps://yourhost.yoursite.com 上的 Sitemap。

被认为无效的 URL 可能会被搜索引擎机器人从进一步考虑中排除。我们不检查这些限制以提高性能,因为我们信任开发者,但您可以通过适当的装饰器启用对这些限制的检查。在 sitemap 构建过程中检测问题比在索引过程中更好。

  • ScopeTrackingStream - Stream 装饰器;
  • ScopeTrackingSplitStream - SplitStream 装饰器;
  • ScopeTrackingIndexStream - IndexStream 装饰器。

装饰器接受要装饰的流和 sitemap 范围作为参数。

// file into which we will write a sitemap
$filename = __DIR__.'/catalog/sitemap.xml';

// configure stream
$render = new PlainTextSitemapRender();
$writer = new TempFileWriter();
$wrapped_stream = new WritingStream($render, $writer, $filename);

// all URLs not started with this path will be considered invalid
$scope = 'https://example.com/catalog/';

// decorate stream
$stream = new ScopeTrackingStream($wrapped_stream, $scope);

// build sitemap.xml
$stream->open();
// this is a valid URLs
$stream->push(Url::create('https://example.com/catalog/'));
$stream->push(Url::create('https://example.com/catalog/123-my_product.html'));
$stream->push(Url::create('https://example.com/catalog/brand/'));
// using these URLs will throw exception
//$stream->push(Url::create('https://example.com/')); // parent path
//$stream->push(Url::create('https://example.com/news/')); // another path
//$stream->push(Url::create('http://example.com/catalog/')); // another scheme
//$stream->push(Url::create('https://example.com:80/catalog/')); // another port
//$stream->push(Url::create('https://example.org/catalog/')); // another domain
$stream->close();

许可证

此软件包位于 MIT 许可证 下。有关完整许可证,请参阅文件:LICENSE