URL 使用正则表达式在 Kotlin 中解析

URL Parsing in Kotlin using Regex

如何匹配字符串中的 secret_code_data:

xeno://soundcloud/?code=secret_code_data#

我试过了

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

运气不好。我猜测 ?在代码可能成为问题之前,我是否应该以某种方式逃避它。你能帮忙吗?

在第一个问号前添加一个转义符,因为它有特殊含义

? 

变成

\?

你也正在捕获第一组的密码。不过不确定后面的 kotlin 代码是否提取了第一组。

这里有三个选项,第一个提供一个很好的 Regex 来做你想做的事,另外两个用于解析 URL's 使用 Regex 的替代方法来处理 URL 组件 encoding/decoding正确。

使用 Regex 进行解析

注意: Regex 方法在大多数情况下是不安全的,因为它没有正确解析 URL 成组件,然后分别解码每个组件。通常你不能将整个 URL 解码成一个字符串然后安全地解析,因为一些编码的字符可能会在以后混淆正则表达式。这类似于使用正则表达式(如 described here)解析 XHTML。请参阅下面的 Regex 替代方法。

这是一个经过清理的正则表达式,作为单元测试用例,可以安全地处理更多 URLs。最后 post 是一个单元测试,您可以对每个方法使用它。

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(). It and other extension methods can be found in the Kotlin API Reference

使用 UriBuilder 或类似工具解析 class

这是一个使用 UriBuilder from the Klutter library for Kotlin. This version handles encoding/decoding 的示例,其中包括更现代的 Java 脚本 unicode 编码,Java 标准 URI class(有很多问题)。这是安全、简单的,您无需担心任何特殊情况。

实施:

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 encoding/decoding。 (对于 $klutter_version 使用 most current release)。

使用 JDK URI Class

进行解析

这个版本有点长,说明你需要自己解析原始查询字符串,解析后解码,然后找到查询参数:

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 class 本身的扩展:

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

在正文中删除 parsed 变量并使用 this 因为您已经有了 URI,那么您就是 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")
        }
    }