在Android上解析查询字符串

279

Java EE具有ServletRequest.getParameterValues()

在非EE平台上,URL.getQuery()仅返回一个字符串。

在非Java EE环境下,正确解析URL中的查询字符串的常规方法是什么?


回答中流行的方式是尝试制作自己的解析器。这是一个很有趣和激动人心的微编码项目,但我不能说这是一个好主意

下面的代码片段通常存在缺陷或错误。破解它们对读者来说是一个有趣的练习。对于攻击使用它们的网站的黑客也一样

解析查询字符串是一个已经定义明确的问题,但阅读规范并理解其细节并不容易。最好让一些平台库的开发人员为您完成艰苦的工作和修复!


1
你想从一个servlet还是JSP页面来完成这个操作?在回答之前,我需要一些澄清。 - ChadNC
我正在尝试在Android上实现这个,但所有平台上的答案都将是有用的指针(也适用于其他可能遇到这个问题的人),所以请不要犹豫! - Will
1
你还需要解析POST参数吗? - Thilo
2
即使您正在使用J2EE(或通过OSGi添加了选定的EE包,如我所做),这个问题也可能是有意义的。在我的情况下,查询字符串/ URL编码的POST正文由系统的一部分处理,该部分故意忽略诸如ServletRequest之类的东西。 - Hanno Fietz
@Will 我不知道你是否已经解决了你的问题,但是这个库 http://cxf.apache.org/docs/jax-rs-advanced-features.html 对我很有帮助!看一下,还支持FIQL。 - rafa.ferreira
显示剩余2条评论
25个回答

4
解析查询字符串比看起来复杂一些,这取决于您希望容错的程度。
首先,查询字符串是 ASCII 字节。您逐个读入这些字节并将它们转换为字符。如果字符是 ? 或 &,则表示参数名称的开始。如果字符是 =,则表示参数值的开始。如果字符是 %,则表示编码字节的开始。这里就变得棘手了。
当您读入 % 字符时,必须读取接下来的两个字节,并将它们解释为十六进制数字。也就是说,接下来的两个字节将是 0-9、a-f 或 A-F。将这两个十六进制数字粘在一起以获取字节值。但是请记住,字节不是字符。您必须知道用于编码字符的编码方式。在 UTF-8 中,字符 é 的编码方式与 ISO-8859-1 中的编码方式不同。通常情况下,无法知道给定字符集使用了什么编码。我总是使用 UTF-8,因为我的网站配置为始终使用 UTF-8 服务所有内容,但实际上您不能确定。某些用户代理将在请求中告诉您字符编码;如果您有完整的 HTTP 请求,可以尝试读取它。如果仅有一个 URL,则祝您好运。
无论如何,假设您正在使用 UTF-8 或其他多字节字符编码,现在您已经解码了一个编码的字节,您必须将其设置在一边,直到捕获下一个字节。您需要所有在一起的编码字节,因为您无法逐个字节正确地进行 URL 解码。将在一起的所有字节都设置在一边,然后一次性解码它们以重构您的字符。
如果您想要宽容并考虑用户代理破坏 URL 的情况,那么就更有趣了。例如,一些 Webmail 客户端会对事物进行双重编码。或者双倍 ?&= 字符(例如:http://yoursite.com/blah??p1==v1&&p2==v2)。如果您想要尝试优雅地处理这种情况,则需要向解析器添加更多逻辑。

这并没有解释如何解析或检索查询字符串参数值。 - ChadNC
没错,但有点繁琐。为此我们已经有了URLDecoder。 - BalusC
2
@ChadNC:第三句话告诉你如何解析:每次读取一个字节并转换为字符。第四句话警告你特殊字符的存在。等等。也许你没有仔细阅读答案? - Mr. Shiny and New 安宇
@BalusC:URLDecoder可以工作,但如果您尝试更宽松地接受什么类型的URL,则可能会出现一些故障模式。 - Mr. Shiny and New 安宇
1
同意@Mr.ShinyAndNew的观点,解析查询参数并不容易。我正在支持FIQL,这变成了一个真正的烦恼。例如:http://yoursite.com/blah??p1==v1&&p2==v2,p2==v3;p2==v4 - rafa.ferreira

3

仅供参考,这是我最终得出的代码(基于URLEncodedUtils,并返回Map)。

特点:

  • 它接受url的查询字符串部分(您可以使用request.getQueryString()
  • 空查询字符串将产生一个空的Map
  • 没有值的参数(?test)将被映射到一个空的List<String>

代码:

public static Map<String, List<String>> getParameterMapOfLists(String queryString) {
    Map<String, List<String>> mapOfLists = new HashMap<String, List<String>>();
    if (queryString == null || queryString.length() == 0) {
        return mapOfLists;
    }
    List<NameValuePair> list = URLEncodedUtils.parse(URI.create("http://localhost/?" + queryString), "UTF-8");
    for (NameValuePair pair : list) {
        List<String> values = mapOfLists.get(pair.getName());
        if (values == null) {
            values = new ArrayList<String>();
            mapOfLists.put(pair.getName(), values);
        }
        if (pair.getValue() != null) {
            values.add(pair.getValue());
        }
    }

    return mapOfLists;
}

一个兼容性辅助程序(值以字符串数组的形式存储,就像ServletRequest.getParameterMap()中一样):

public static Map<String, String[]> getParameterMap(String queryString) {
    Map<String, List<String>> mapOfLists = getParameterMapOfLists(queryString);

    Map<String, String[]> mapOfArrays = new HashMap<String, String[]>();
    for (String key : mapOfLists.keySet()) {
        mapOfArrays.put(key, mapOfLists.get(key).toArray(new String[] {}));
    }

    return mapOfArrays;
}

3
这对我有效。 我不确定为什么每个人都需要Map、List>。 我只需要一个简单的名称值Map。
为了保持简单,我使用了内置的URI.getQuery()方法。
public static Map<String, String> getUrlParameters(URI uri)
    throws UnsupportedEncodingException {
    Map<String, String> params = new HashMap<String, String>();
    for (String param : uri.getQuery().split("&")) {
        String pair[] = param.split("=");
        String key = URLDecoder.decode(pair[0], "UTF-8");
        String value = "";
        if (pair.length > 1) {
            value = URLDecoder.decode(pair[1], "UTF-8");
        }
        params.put(new String(key), new String(value));
    }
    return params;
}

1
多选表单怎么办?在合法的查询字符串(和POST表单主体)中重复键是完全正常的。还有其他缺陷和边界情况没有涵盖;其中许多已经在其他方法的评论中提到。我会避免指出它们,因为我担心你会修复它,而不是使用高质量的库,就像我在问题中所抱怨的那样 ;) - Will

3
最初的回答来自这里 在Android上,有一个位于android.net包中的Uri类。请注意,Uri是android.net的一部分,而URI是java.net的一部分。
Uri类有许多函数可用于提取查询键值对。 enter image description here 下面的函数以HashMap的形式返回键-值对。
在Java中:
Map<String, String> getQueryKeyValueMap(Uri uri){
    HashMap<String, String> keyValueMap = new HashMap();
    String key;
    String value;

    Set<String> keyNamesList = uri.getQueryParameterNames();
    Iterator iterator = keyNamesList.iterator();

    while (iterator.hasNext()){
        key = (String) iterator.next();
        value = uri.getQueryParameter(key);
        keyValueMap.put(key, value);
    }
    return keyValueMap;
}

在Kotlin中:

fun getQueryKeyValueMap(uri: Uri): HashMap<String, String> {
        val keyValueMap = HashMap<String, String>()
        var key: String
        var value: String

        val keyNamesList = uri.queryParameterNames
        val iterator = keyNamesList.iterator()

        while (iterator.hasNext()) {
            key = iterator.next() as String
            value = uri.getQueryParameter(key) as String
            keyValueMap.put(key, value)
        }
        return keyValueMap
    }

这个答案应该被接受,因为它还显示了表情符号。如果需要,也可以与以下答案一起使用:org.apache.commons.text.StringEscapeUtils.escapeJava和org.apache.commons.text.StringEscapeUtils.unescapeJava。 - Pratik Saluja

3
在Android上,您可以使用android.net.Uri类的Uri.parse静态方法来完成繁重的工作。如果您正在处理URI和意图相关的内容,无论如何都需要使用它。

2

Guava的Multimap更适合这种情况。以下是一个简短而干净的版本:

Multimap<String, String> getUrlParameters(String url) {
        try {
            Multimap<String, String> ret = ArrayListMultimap.create();
            for (NameValuePair param : URLEncodedUtils.parse(new URI(url), "UTF-8")) {
                ret.put(param.getName(), param.getValue());
            }
            return ret;
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

1
if (queryString != null)
{
    final String[] arrParameters = queryString.split("&");
    for (final String tempParameterString : arrParameters)
    {
        final String[] arrTempParameter = tempParameterString.split("=");
        if (arrTempParameter.length >= 2)
        {
            final String parameterKey = arrTempParameter[0];
            final String parameterValue = arrTempParameter[1];
            //do something with the parameters
        }
    }
}


0
public static Map <String, String> parseQueryString (final URL url)
        throws UnsupportedEncodingException
{
    final Map <String, String> qps = new TreeMap <String, String> ();
    final StringTokenizer pairs = new StringTokenizer (url.getQuery (), "&");
    while (pairs.hasMoreTokens ())
    {
        final String pair = pairs.nextToken ();
        final StringTokenizer parts = new StringTokenizer (pair, "=");
        final String name = URLDecoder.decode (parts.nextToken (), "ISO-8859-1");
        final String value = URLDecoder.decode (parts.nextToken (), "ISO-8859-1");
        qps.put (name, value);
    }
    return qps;
}

0

在这里回答,因为这是一个热门的帖子。这是 Kotlin 中使用推荐的 UrlQuerySanitizer api 的干净解决方案。请参阅官方文档。我添加了一个字符串构建器来连接和显示参数。

    var myURL: String? = null
    // if the url is sent from a different activity where you set it to a value
    if (intent.hasExtra("my_value")) {
        myURL = intent.extras.getString("my_value")
    } else {
        myURL = intent.dataString
    }

    val sanitizer = UrlQuerySanitizer(myURL)
    // We don't want to manually define every expected query *key*, so we set this to true
    sanitizer.allowUnregisteredParamaters = true
    val parameterNamesToValues: List<UrlQuerySanitizer.ParameterValuePair> = sanitizer.parameterList
    val parameterIterator: Iterator<UrlQuerySanitizer.ParameterValuePair> = parameterNamesToValues.iterator()

    // Helper simply so we can display all values on screen
    val stringBuilder = StringBuilder()

    while (parameterIterator.hasNext()) {
        val parameterValuePair: UrlQuerySanitizer.ParameterValuePair = parameterIterator.next()
        val parameterName: String = parameterValuePair.mParameter
        val parameterValue: String = parameterValuePair.mValue

        // Append string to display all key value pairs
        stringBuilder.append("Key: $parameterName\nValue: $parameterValue\n\n")
    }

    // Set a textView's text to display the string
    val paramListString = stringBuilder.toString()
    val textView: TextView = findViewById(R.id.activity_title) as TextView
    textView.text = "Paramlist is \n\n$paramListString"

    // to check if the url has specific keys
    if (sanitizer.hasParameter("type")) {
        val type = sanitizer.getValue("type")
        println("sanitizer has type param $type")
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接