Соответствие регулярных выражений в Котлине

Как мне сопоставить secret_code_data в строке:

xeno://soundcloud/?code=secret_code_data#

Я пробовал

val regex = Regex("""xeno://soundcloud/?code=(.*?)#""")
field = regex.find(url)?.value ?: ""

без везения. Я подозреваю? прежде чем код может быть проблемой, я должен каким-то образом избежать этого. Вы можете помочь?

Ответы

Ответ 1

Вот три варианта, первый из которых содержит хорошее Regex, которое делает то, что вы хотите, и два других для синтаксического анализа URL, используя альтернативу Regex, которые правильно обрабатывают кодирование/декодирование URL-адреса.

Анализ с использованием Regex

ПРИМЕЧАНИЕ.. Метод Regex небезопасен в большинстве случаев использования, поскольку он неправильно анализирует URL-адрес в компонентах, а затем декодирует каждый компонент отдельно. Обычно вы не можете декодировать весь URL-адрес в одну строку, а затем безопасно разбираться, потому что некоторые закодированные символы могут запутать Regex позже. Это похоже на разбор XHTML с использованием regex (как описанного здесь). См. Альтернативы Regex ниже.

Вот очищенное регулярное выражение как случай unit test, который безопасно обрабатывает больше URL-адресов. В конце этого сообщения есть unit test, который вы можете использовать для каждого метода.

private val SECRET_CODE_REGEX = """xeno://soundcloud[/]?.*[\?&]code=([^#&]+).*""".toRegex()
fun findSecretCode(withinUrl: String): String? =
        SECRET_CODE_REGEX.matchEntire(withinUrl)?.groups?.get(1)?.value

Это регулярное выражение обрабатывает следующие случаи:

  • с и без конца / в пути
  • с фрагментом и без него
  • как первый, средний или последний в списке параметров
  • как только параметр

Обратите внимание, что идиоматический способ создания регулярного выражения в Kotlin someString.toRegex(). Это и другие методы расширения можно найти в Справочник по API Kotlin.

Анализ с использованием UriBuilder или аналогичного класса

Вот пример, используя UriBuilder из библиотека Klutter для Котлина. Эта версия обрабатывает кодирование/декодирование, включая более современные кодировки jQi-кода JavaScript, которые не обрабатываются классом Java URI (который имеет много проблем). Это безопасно, легко, и вам не нужно беспокоиться о каких-либо особых случаях.

Реализация:

fun findSecretCode(withinUrl: String): String? {
    fun isValidUri(uri: UriBuilder): Boolean = uri.scheme == "xeno"
                    && uri.host == "soundcloud"
                    && (uri.encodedPath == "/" || uri.encodedPath.isNullOrBlank())
    val parsed = buildUri(withinUrl)
    return if (isValidUri(parsed)) parsed.decodedQueryDeduped?.get("code") else null
}

Артефакт Klutter uy.klutter:klutter-core-jdk6:$klutter_version мал и включает в себя некоторые другие расширения, включая модернизированную кодировку/декодирование URL. (Для $klutter_version используйте самую последнюю версию).

Разбор с классом URI JDK

Эта версия немного длиннее и показывает, что вам нужно самостоятельно разобрать необработанную строку запроса, декодировать после разбора, а затем найти параметр запроса:

fun findSecretCode(withinUrl: String): String? {
    fun isValidUri(uri: URI): Boolean = uri.scheme == "xeno"
            && uri.host == "soundcloud"
            && (uri.rawPath == "/" || uri.rawPath.isNullOrBlank())

    val parsed = URI(withinUrl)
    return if (isValidUri(parsed)) {
        parsed.getRawQuery().split('&').map {
            val parts = it.split('=')
            val name = parts.firstOrNull() ?: ""
            val value = parts.drop(1).firstOrNull() ?: ""
            URLDecoder.decode(name, Charsets.UTF_8.name()) to URLDecoder.decode(value, Charsets.UTF_8.name())
        }.firstOrNull { it.first == "code" }?.second
    } else null
}

Это может быть написано как расширение для самого класса URI:

fun URI.findSecretCode(): String? { ... }

В теле удалите переменную parsed и используйте this, так как у вас уже есть URI, ну, вы ARE URI. Затем вызовите с помощью:

val secretCode = URI(myTestUrl).findSecretCode()

Единичные тесты

Для любой из вышеперечисленных функций запустите этот тест, чтобы убедиться, что он работает:

class TestSo34594605 {
    @Test fun testUriBuilderFindsCode() {
        // positive test cases

        val testUrls = listOf("xeno://soundcloud/?code=secret_code_data#",
                "xeno://soundcloud?code=secret_code_data#",
                "xeno://soundcloud/?code=secret_code_data",
                "xeno://soundcloud?code=secret_code_data",
                "xeno://soundcloud?code=secret_code_data&other=fish",
                "xeno://soundcloud?cat=hairless&code=secret_code_data&other=fish",
                "xeno://soundcloud/?cat=hairless&code=secret_code_data&other=fish",
                "xeno://soundcloud/?cat=hairless&code=secret_code_data",
                "xeno://soundcloud/?cat=hairless&code=secret_code_data&other=fish#fragment"
        )

        testUrls.forEach { test ->
            assertEquals("secret_code_data", findSecretCode(test), "source URL: $test")
        }

        // negative test cases, don't get things on accident

        val badUrls = listOf("xeno://soundcloud/code/secret_code_data#",
                "xeno://soundcloud?hiddencode=secret_code_data#",
                "http://www.soundcloud.com/?code=secret_code_data")

        badUrls.forEach { test ->
            assertNotEquals("secret_code_data", findSecretCode(test), "source URL: $test")
        }
    }

Ответ 2

Добавьте побег перед первым вопросительным знаком, поскольку он имеет особое значение

? 

становится

\?

Вы также захватываете секретный код в первой группе. Не уверен, что код kotlin, который следует, извлекает первую группу, хотя.