URI — унифицированный идентификатор ресурса. То же: Uniform (прежде Universal) Resource Identifier.
URI бывает двух-трёх видов:
Пример URL: http://karmanov.su/URI.html
. Пример URN: urn:isbn:1937785536
. В одном случае мы можем получить требуемый ресурс моментально (страница данного сайта), в другом случае моментально этого не получится сделать. ISBN — это международная нумерация книг. urn:isbn:1937785536
тоже обозначает конкретный ресурс (книга Джо Армстронга), однако в этом обозначении не сказано, как получить ресурс. Мы можем воспользоваться поисковиком, чтобы найти интернет-магазин, торгующий данной книгой. Скорее всего, будет найдено несколько источников, но может быть и так, что книги уже нет в продаже.
URL указывает на электронный ресурс, URN может указывать на материально существующую вещь. На одну и ту же вещь могут указывать несколько идентификаторов. Например, мы можем пронумеровать определённым образом книги в домашней библиотеке — и тогда идентификатор будет показывать на конкретную книгу, стоящую на полке. У этой же книги, скорее всего, есть и ISBN — как и у других книг, вышедших этим же тиражом.
Нет и не может быть в принципе какого-то единого алгоритма превращения URI в наличный ресурс. URI — расширяемый способ идентификации ресурсов. Есть несколько схем идентификации внутри URI, многие будут созданы. Нет ничего страшного в том, чтобы придумать свои схемы, свои протоколы.
Если мы строим свою Систему, используя URI, мы легко можем реализовать универсальное обращение к ресурсам. Часто в качестве аргументов функций мы передаём имя файла, например /home/user/report-20241014.csv
. Это удобно до тех пор, пока мы получаем файлы локально. Пройдёт время, и возможно, нам понадобится больше гибкости. Допустим, на одной ноде мы и дальше сможем получать файлы локально, но на другой ноде — нет.
Тогда удобным станет обрабатывать не имена файлов, а URL. В одном случае будет обрабатываться URL file:///home/user/report-20241014.csv
. В другом: http://localhost/report-20241014.csv
. Функция получает этот URL и смотрит, что это за зверь такой, по какому протоколу надо получить этот ресурс (file или http). В зависимости от протокола она получает нужный ресурс и дальше с ним что-то делает.
Пройдёт время и, может быть, возникнет такая ситуация, что мы точно знаем, что где-то на компьютере есть файл report-20241014.csv
, но где точно он находится, мы не знаем. Разные бывают ситуации, и не всё зависит от нас. Мы тогда можем придумать какой-нибудь свой специальный URN для таких случаев, и тогда функция (помимо других вариантов) будет принимать ещё и URI вроде urn:lostfile:report-20241014.csv
. Проанализировав полученный URI, функция поймёт, что ей придётся попотеть и найти нужный файл на компьютере самостоятельно. Для этого она, например, запустит команду find.
Почему мы начали свой URI с urn:
? Сложилась такая практика, что когда URI не указывает на конкретное место, когда он не связан с каким-то конкретным протоколом или методом доступа, тогда используется префикс urn:
.
Для работы с URI есть специальный модуль uri_string. Чтобы разложить URI на части, можно воспользоваться функцией parse/1 из этого модуля, которая в качестве аргумента принимает строку, а возвращает карту, содержащую части URI.
1> uri_string:parse("http://karmanov.su/URI.html").
#{scheme => "http",path => "/URI.html",host => "karmanov.su"}
2> uri_string:parse("ftp://ftp.is.co.za/rfc/rfc1808.txt").
#{scheme => "ftp",path => "/rfc/rfc1808.txt",
host => "ftp.is.co.za"}
3> uri_string:parse("mailto:alex@karmanov.su").
#{scheme => "mailto",path => "alex@karmanov.su"}
4> uri_string:parse("telnet://192.0.2.16:80/").
#{port => 80,scheme => "telnet",path => "/",
host => "192.0.2.16"}
5> uri_string:parse("tel:+1-816-555-1212").
#{scheme => "tel",path => "+1-816-555-1212"}
Как видим, во всех случаях были выделены части scheme и path. Остальное — факультативно. Всего может быть выделено в URI семь частей:
Попробуем получить полный набор этих “запчастей”:
10> uri_string:parse("foo://alex@localhost:8080/some/util?name=
pushkin#balda").
#{port => 8080,scheme => "foo",path => "/some/util",
host => "localhost",fragment => "balda",
query => "name=pushkin",userinfo => "alex"}
О чём говорит этот URI и карта, которую мы получили? На локальном компьютере (“localhost”) запущен некий сервер, который готов общаться с нами по некоторому протоколу (“foo”). Сервер прослушивает определённый порт (8080) и ожидает лишь определённых пользователей (“alex”). В ведении сервера есть некий каталог, а в нём некоторая утилита (“/some/util”). Эта утилита принимает аргументы, и если ей передать аргумент “name=pushkin”, то она, видимо, отдаст страницу, содержащую ряд пушкинских стихотворений. Там, очевидно, есть “Сказка о попе и о работнике его Балде”, раз указан фрагмент “balda”. Мы желаем, чтобы наша программа-обозреватель открыла нам сборник стихотворений именно на этом месте.
Тут, конечно, есть некоторые условности. Для аутентификации на сервере совсем не обязательно в URI задавать имя пользователя, можно использовать другие механизмы. Та часть, которая идёт после имени хоста и порта (/some/util?name=pushkin
) не является безусловным доказательством, что на сервере и правда есть каталог some, а в нём программа util. Это внутреннее дело сервера — как интерпретировать данную строку. Вместо /some/util?name=pushkin
, в принципе, может быть и /someutilnamepushkin
. Если сервер сможет это разобрать, значит, он молодец.
Каждый URI это цепочка символов. В URI могут использоваться лишь ASCII-символы, и то далеко не все. Функция parse, встречая такие символы, выдаёт ошибку.
1> uri_string:parse("foo://бегемот/").
{error,invalid_uri,":"}
2> uri_string:parse("foo://begemot/a`b").
{error,invalid_uri,":"}
Внутри системы целесообразно хранить и передавать URI не в сыром виде (строка), а в уже разложенном виде — в виде терма карты. Это удобно, особенно если мы собираемся анализировать URI.
Когда URI попадает в Систему, он превращается в карту, а когда выходит обратно, то из карты превращается назад в строку. Это может произойти, например, в случае, когда мы желаем использовать какую-то внешнюю команду, например curl, для получения ресурса.
В uri_string есть функция, обратная parse, это recompose.
1> Uri1 = "foo://alex@localhost:8080/some/util?name=pushkin#balda".
"foo://alex@localhost:8080/some/util?name=pushkin#balda"
2> Uri_map = uri_string:parse(Uri1).
#{port => 8080,scheme => "foo",path => "/some/util",
host => "localhost",fragment => "balda",
query => "name=pushkin",userinfo => "alex"}
3> Uri2 = uri_string:recompose(Uri_map).
"foo://alex@localhost:8080/some/util?name=pushkin#balda"
4> Uri1 = Uri2.
"foo://alex@localhost:8080/some/util?name=pushkin#balda"
Разложив, а потом снова сложив, мы получили, как видно, ту же самую строку URI.
Также в модуле uri_string есть удобная функция normalize, которая позволяет нормализовать URI. Эту функцию, кстати, можно использовать вместо recompose/1.
В названии хоста не имеет значения, в каком регистре написаны буквы. В пути, однако, это имеет значение. Нормализуясь, имя хоста состоит только из букв в нижнем регистре.
1> uri_string:normalize(#{scheme => "http", host => "Localhost",
path => "/Index.html"}).
"http://localhost/Index.html"
Неправильные символы получают правильное отображение.
2> uri_string:normalize(#{scheme => "http", host => "локалхост",
path => "/index.html"}).
"http://%D0%BB%D0%BE%D0%BA%D0%B0%D0%BB%D1%85%D0%BE%D1%81%D1%82/
index.html"
Запутанные пути распутываются.
3> uri_string:normalize(#{scheme => "http", host => "localhost",
path => "/a/b/c/./../../d"}).
"http://localhost/a/d"
Порт, который является дефолтным, будет опущен.
4> uri_string:normalize(#{scheme => "http", host => "localhost",
path => "/index.html", port => 80}).
"http://localhost/index.html"
Через URI может передаваться аргумент, который будет воспринят сервером и обработан соответствующим образом. Для правильной генерации этой части URI (query) мы можем воспользоваться функцией uri_string:compose_query. В качестве аргумента она принимает список кортежей, каждый из которых состоит из двух элементов: ключ и значение.
1> L = [{"Автор","Державин"},{"Произведение","Снигирь"}].
[{[1040,1074,1090,1086,1088],
[1044,1077,1088,1078,1072,1074,1080,1085]},
{[1055,1088,1086,1080,1079,1074,1077,1076,1077,1085,1080,
1077],
[1057,1085,1080,1075,1080,1088,1100]}]
2> uri_string:compose_query(L).
"%D0%90%D0%B2%D1%82%D0%BE%D1%80=%D0%94%D0%B5%D1%80%D0%B6%D0%B0
%D0%B2%D0%B8%D0%BD&%D0%9F%D1%80%D0%BE%D0%B8%D0%B7%D0%B2%D0
%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5=%D0%A1%D0%BD%D0%B8%D0%B3
%D0%B8%D1%80%D1%8C"
Для функции compose_query есть обратная — dissect_query, которая принимает query (часть URI — запрос) и возвращает список кортежей “ключ-значение”.
$ erl +pc unicode
1> uri_string:dissect_query("%D0%90%D0%B2%D1%82%D0%BE%D1%80=%D0
%94%D0%B5%D1%80%D0%B6%D0%B0%D0%B2%D0%B8%D0%BD&%D0%9F%D1%80%D0%BE
%D0%B8%D0%B7%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5=%D0%A1%D0
%BD%D0%B8%D0%B3%D0%B8%D1%80%D1%8C").
[{"Автор","Державин"},{"Произведение","Снигирь"}]
Если стоит задача кодировать в приемлемые символы какую-то часть URI (путь, запросы), для этого может пригодиться функция quote.
1> uri_string:quote("Поэты/Лермонтов").
"%D0%9F%D0%BE%D1%8D%D1%82%D1%8B%2F%D0%9B%D0%B5%D1%80%D0%BC%D0
%BE%D0%BD%D1%82%D0%BE%D0%B2"
2> uri_string:quote("Поэты/Лермонтов", "/").
"%D0%9F%D0%BE%D1%8D%D1%82%D1%8B/%D0%9B%D0%B5%D1%80%D0%BC%D0%BE
%D0%BD%D1%82%D0%BE%D0%B2"
В первом случае кодируются все неправильные символы, включая слэш. Если эта строка — запрос (аргумент для программы на сервере), то это нормально. Но возможно, что “Поэты/Лермонтов” относится к пути, поэтому слэш надо сохранить. В этом случае можно использовать quote/2, где второй аргумент — список символов, которые не надо кодировать.
Чтобы декодировать строку — часть URI, есть обратная функция — unquote/1.
3> uri_string:unquote("%D0%9F%D0%BE%D1%8D%D1%82%D1%8B%2F%D0%9B
%D0%B5%D1%80%D0%BC%D0%BE%D0%BD%D1%82%D0%BE%D0%B2").
"Поэты/Лермонтов"
4> uri_string:unquote("%D0%9F%D0%BE%D1%8D%D1%82%D1%8B/%D0%9B%D0
%B5%D1%80%D0%BC%D0%BE%D0%BD%D1%82%D0%BE%D0%B2").
"Поэты/Лермонтов"
В разных частях URI используется разный набор символов. Например, в схеме можно использовать лишь цифры, буквы (в обоих регистрах), плюс, минус и точку. Другие символы приведут к ошибке:
1> uri_string:parse("fo_od://localhost").
{error,invalid_uri,":"}
2> uri_string:parse("fo%od://localhost").
{error,invalid_uri,":"}
Но как узнать или вспомнить, в какой части URI какие символы используются? Для этого есть удобная функция uri_string:allowed_characters/0. Она носит справочный характер, но её, конечно, можно использовать и в программах для автоматизации каких-либо процессов проверки.
1> uri_string:allowed_characters().
[{scheme,"+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn
opqrstuvwxyz"},
{userinfo,"!$%&'()*+,-.0123456789:;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_
abcdefghijklmnopqrstuvwxyz~"},
{host,"!$&'()*+,-.0123456789:;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcd
efghijklmnopqrstuvwxyz~"},
{ipv4,".0123456789"},
{ipv6,".0123456789:ABCDEFabcdef"},
{regname,"!$%&'()*+,-.0123456789;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_a
bcdefghijklmnopqrstuvwxyz~"},
{path,"!$%&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_a
bcdefghijklmnopqrstuvwxyz~"},
{query,"!$%&'()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ
_abcdefghijklmnopqrstuvwxyz~"},
{fragment,"!$%&'()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVW
XYZ_abcdefghijklmnopqrstuvwxyz~"},
{reserved,"!#$&'()*+,/:;=?@[]"},
{unreserved,"-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijk
lmnopqrstuvwxyz~"}]
Как уже говорилось выше, при нормализации URI запутанный путь в нём распутывается. Если у нас не стоит задача нормализовать URI, но хочется уже распутать запутанные пути, для этого есть функция resolve.
1> uri_string:resolve("/abs/ol/ute", "http://localhost/a/b/c/d?
query").
"http://localhost/abs/ol/ute"
2> uri_string:resolve("http://nonlocal/abs/ol/ute",
"http://localhost/a/b/c/d?query").
"http://nonlocal/abs/ol/ute"
3> uri_string:resolve("../../b/.././",
"http://localhost/a/b/c/d?query").
"http://localhost/a/"
Второй аргумент — базовый URI. Первый — путь, который мы желаем распутать. Он может быть как абсолютным, так и относительным.
© Алексей Карманов, 2024.