Java-эквивалент encodeURIComponent JavaScript, который производит идентичный вывод?

Я экспериментировал с различными фрагментами кода Java, пытаясь придумать что-то, что будет кодировать строку, содержащую кавычки, пробелы и «экзотические» символы Unicode, и создавать выходные данные, идентичные encodeURIComponent.

Моя тестовая строка для пыток: "A" B ± "

Если я введу следующий оператор JavaScript в Firebug:

encodeURIComponent('"A" B ± "');

Затем я получаю:

"%22A%22%20B%20%C2%B1%20%22"

Вот моя небольшая тестовая программа на Java:

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class EncodingTest
{
  public static void main(String[] args) throws UnsupportedEncodingException
  {
    String s = "\"A\" B ± \"";
    System.out.println("URLEncoder.encode returns "
      + URLEncoder.encode(s, "UTF-8"));

    System.out.println("getBytes returns "
      + new String(s.getBytes("UTF-8"), "ISO-8859-1"));
  }
}

Эта программа выводит:

URLEncoder.encode returns %22A%22+B+%C2%B1+%22
getBytes returns "A" B ± "

Близко, но не сигара! Каков наилучший способ кодирования строки UTF-8 с использованием Java, чтобы она давала тот же результат, что и JavaScript encodeURIComponent?

EDIT: я использую Java 1.4, скоро перейду на Java 5.


person John Topley    schedule 03.03.2009    source источник


Ответы (13)


Глядя на различия в реализации, я вижу, что:

MDC на encodeURIComponent():

  • литеральные символы (представление регулярного выражения): [-a-zA-Z0-9._*~'()!]

документация по Java 1.5.0 по URLEncoder :

  • литеральные символы (представление регулярного выражения): [-a-zA-Z0-9._*]
  • символ пробела " " преобразуется в знак плюса "+".

Таким образом, чтобы получить желаемый результат, используйте URLEncoder.encode(s, "UTF-8"), а затем выполните некоторую постобработку:

  • заменить все вхождения "+" на "%20"
  • заменить все вхождения "%xx", представляющие любой из [~'()!], на их буквальные аналоги
person Tomalak    schedule 03.03.2009
comment
Я бы хотел, чтобы вы написали Заменить все вхождения %xx, представляющие любой из [~'()!], обратно их буквальными аналогами на каком-нибудь простом языке. :( моя крохотная голова не в состоянии это понять....... - person Shailendra Singh Rajawat; 01.05.2014
comment
@ Шайлендра [~'()!] означает "~" или "'" или "(" или ")" или "!". :) Я также рекомендую изучить основы регулярных выражений. (Я также не стал распространяться об этом, поскольку по крайней мере два других ответа показывают соответствующий код Java.) - person Tomalak; 01.05.2014
comment
Замена всех вхождений "+" на "%20" потенциально разрушительна, поскольку "+" является допустимым символом в путях URI (но не в строке запроса). Например, a+b c следует кодировать как "a+b%20c"; это решение преобразует его в "a%20b%20c". Вместо этого используйте new URI(null, null, value, null).getRawPath(). - person Chris Nitchie; 28.04.2016
comment
@ChrisNitchie Вопрос был не в этом. Вопрос заключался в следующем: Java-эквивалент JavaScript encodeURIComponent, который производит идентичный вывод?, а не Общая функция Java encode-URI-component?. - person Tomalak; 28.04.2016

Это класс, который я придумал в конце:

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;

/**
 * Utility class for JavaScript compatible UTF-8 encoding and decoding.
 * 
 * @see http://stackoverflow.com/questions/607176/java-equivalent-to-javascripts-encodeuricomponent-that-produces-identical-output
 * @author John Topley 
 */
public class EncodingUtil
{
  /**
   * Decodes the passed UTF-8 String using an algorithm that's compatible with
   * JavaScript's <code>decodeURIComponent</code> function. Returns
   * <code>null</code> if the String is <code>null</code>.
   *
   * @param s The UTF-8 encoded String to be decoded
   * @return the decoded String
   */
  public static String decodeURIComponent(String s)
  {
    if (s == null)
    {
      return null;
    }

    String result = null;

    try
    {
      result = URLDecoder.decode(s, "UTF-8");
    }

    // This exception should never occur.
    catch (UnsupportedEncodingException e)
    {
      result = s;  
    }

    return result;
  }

  /**
   * Encodes the passed String as UTF-8 using an algorithm that's compatible
   * with JavaScript's <code>encodeURIComponent</code> function. Returns
   * <code>null</code> if the String is <code>null</code>.
   * 
   * @param s The String to be encoded
   * @return the encoded String
   */
  public static String encodeURIComponent(String s)
  {
    String result = null;

    try
    {
      result = URLEncoder.encode(s, "UTF-8")
                         .replaceAll("\\+", "%20")
                         .replaceAll("\\%21", "!")
                         .replaceAll("\\%27", "'")
                         .replaceAll("\\%28", "(")
                         .replaceAll("\\%29", ")")
                         .replaceAll("\\%7E", "~");
    }

    // This exception should never occur.
    catch (UnsupportedEncodingException e)
    {
      result = s;
    }

    return result;
  }  

  /**
   * Private constructor to prevent this class from being instantiated.
   */
  private EncodingUtil()
  {
    super();
  }
}
person John Topley    schedule 04.03.2009
comment
Добавление подсказки. В Android 4.4 я обнаружил, что нам также нужно заменить %0A, что означает клавишу возврата во вводе Android, иначе это приведет к сбою js. - person Aloong; 20.03.2014
comment
Вы освещаете все здесь: developer.mozilla.org /en-US/docs/Web/JavaScript/Reference/ - person kamaci; 16.06.2017
comment
@Aloong Что вы подразумеваете под заменой "%0A"? Какой персонаж будет заменой? Это просто пустая строка""? - person HendraWD; 07.06.2018
comment
Нет необходимости использовать replaceAll, когда простой replace имеет тот же эффект. Нет необходимости экранировать % в регулярных выражениях, поэтому вместо \\% просто напишите %. Если это исключение никогда не должно возникать, скорее выдайте Error или, по крайней мере, IllegalStateException, но не делайте молча что-то глючное. - person Roland Illig; 18.04.2021

Используя механизм javascript, поставляемый с Java 6:


import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class Wow
{
    public static void main(String[] args) throws Exception
    {
        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName("JavaScript");
        engine.eval("print(encodeURIComponent('\"A\" B ± \"'))");
    }
}

Выход: %22A%22%20B%20%c2%b1%20%22

Случай другой, но он ближе к тому, что вы хотите.

person Ravi Wallau    schedule 03.03.2009
comment
Ах, извините ... Я должен был упомянуть в вопросе, что я на Java 1.4 скоро перейду на Java 5! - person John Topley; 03.03.2009
comment
Ииипс. Тогда у меня нет для вас решения :-). - person Ravi Wallau; 03.03.2009
comment
Если javascript - единственное решение, вы можете попробовать Rhino, но это слишком много для этой маленькой проблемы. - person Ravi Wallau; 03.03.2009
comment
Даже если он использовал Java 6, я думаю, что это решение НАМНОГО лучше. Я не думаю, что он ищет способ напрямую вызвать метод javascript, просто способ его эмулировать. - person Outlaw Programmer; 03.03.2009
comment
Может быть. Я думаю, что самым простым решением было бы написать свою собственную функцию escape, если вы не можете найти ничего, что помогло бы вам. Просто скопируйте какой-нибудь метод из класса StringEscapeUtils (Jakarta Commons Lang) и повторно реализуйте его в соответствии с вашими потребностями. - person Ravi Wallau; 03.03.2009
comment
Это действительно работает, и если вы не беспокоитесь о производительности... Я думаю, что это хорошо. - person 2rs2ts; 29.08.2014
comment
К сожалению, я попытался передать фактический объект String компоненту encodeURI, и оказалось, что движок интерпретирует его как объект, а не строковое представление. Даже при попытке применить к нему toString(). Любые известные решения для этого? System.out.println(engine.eval(encodeURIComponent(+stringObject.toString()+)); возвращает Object%20objc - person Pasha Skender; 31.07.2016
comment
Код будет полезнее, если добавить ввод/вывод: (String) engine.eval("encodeURIComponent(x)", new javax.script.SimpleBindings(new HashMap<>(){{ put("x", x); }})). - person tellnobody; 07.05.2020
comment
Это, наверное, самый умный ответ здесь. Вы всегда будете кодировать и декодировать ТОЧНО как во внешнем интерфейсе, так как используете тот же движок. С другой стороны, производительность может немного пострадать, поскольку вам нужно инициализировать js-движок, но это можно сделать при запуске. - person jsaddwater; 18.06.2020

Я использую java.net.URI#getRawPath(), например.

String s = "a+b c.html";
String fixed = new URI(null, null, s, null).getRawPath();

Значение fixed будет a+b%20c.html, что вам и нужно.

Постобработка вывода URLEncoder.encode() удалит все плюсы, которые должны быть в URI. Например

URLEncoder.encode("a+b c.html").replaceAll("\\+", "%20");

даст вам a%20b%20c.html, что будет интерпретировано как a b c.html.

person Chris Nitchie    schedule 09.02.2016
comment
Подумав, что это должен быть лучший ответ, я попробовал его на практике с несколькими именами файлов, и он потерпел неудачу по крайней мере в двух, один с кириллическими символами. Так что нет, это явно не было проверено достаточно хорошо. - person AsGoodAsItGets; 27.06.2016
comment
не работает для таких строк, как: http://a+b c.html , это вызовет ошибку - person balazs; 22.06.2017

Я придумал свою версию encodeURIComponent, потому что в опубликованном решении есть одна проблема: если в строке, которая должна быть закодирована, присутствует +, она будет преобразована в пробел.

Итак, вот мой класс:

import java.io.UnsupportedEncodingException;
import java.util.BitSet;

public final class EscapeUtils
{
    /** used for the encodeURIComponent function */
    private static final BitSet dontNeedEncoding;

    static
    {
        dontNeedEncoding = new BitSet(256);

        // a-z
        for (int i = 97; i <= 122; ++i)
        {
            dontNeedEncoding.set(i);
        }
        // A-Z
        for (int i = 65; i <= 90; ++i)
        {
            dontNeedEncoding.set(i);
        }
        // 0-9
        for (int i = 48; i <= 57; ++i)
        {
            dontNeedEncoding.set(i);
        }

        // '()*
        for (int i = 39; i <= 42; ++i)
        {
            dontNeedEncoding.set(i);
        }
        dontNeedEncoding.set(33); // !
        dontNeedEncoding.set(45); // -
        dontNeedEncoding.set(46); // .
        dontNeedEncoding.set(95); // _
        dontNeedEncoding.set(126); // ~
    }

    /**
     * A Utility class should not be instantiated.
     */
    private EscapeUtils()
    {

    }

    /**
     * Escapes all characters except the following: alphabetic, decimal digits, - _ . ! ~ * ' ( )
     * 
     * @param input
     *            A component of a URI
     * @return the escaped URI component
     */
    public static String encodeURIComponent(String input)
    {
        if (input == null)
        {
            return input;
        }

        StringBuilder filtered = new StringBuilder(input.length());
        char c;
        for (int i = 0; i < input.length(); ++i)
        {
            c = input.charAt(i);
            if (dontNeedEncoding.get(c))
            {
                filtered.append(c);
            }
            else
            {
                final byte[] b = charToBytesUTF(c);

                for (int j = 0; j < b.length; ++j)
                {
                    filtered.append('%');
                    filtered.append("0123456789ABCDEF".charAt(b[j] >> 4 & 0xF));
                    filtered.append("0123456789ABCDEF".charAt(b[j] & 0xF));
                }
            }
        }
        return filtered.toString();
    }

    private static byte[] charToBytesUTF(char c)
    {
        try
        {
            return new String(new char[] { c }).getBytes("UTF-8");
        }
        catch (UnsupportedEncodingException e)
        {
            return new byte[] { (byte) c };
        }
    }
}
person Joe Mill    schedule 05.04.2012
comment
Спасибо за хорошее решение! Остальные выглядят совершенно... неэффективными, ИМО. Возможно, без BitSet на сегодняшнем оборудовании было бы даже лучше. Или два жестко закодированных длинных значения для 0...127. - person Jonas N; 17.04.2012
comment
URLEncoder.encode("+", "UTF-8"); дает "%2B", что является правильной кодировкой URL, поэтому ваше решение, приношу свои извинения, совершенно не нужно. Почему URLEncoder.encode не превращает пробелы в %20, я не понимаю. - person 2rs2ts; 23.09.2014

Я придумал другую реализацию, задокументированную по адресу http://blog.sangupta.com/2010/05/encodeuricomponent-and.html. Реализация также может обрабатывать байты Unicode.

person sangupta    schedule 23.10.2010

Это то, что я использую:

private static final String HEX = "0123456789ABCDEF";

public static String encodeURIComponent(String str) {
    if (str == null) return null;

    byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
    StringBuilder builder = new StringBuilder(bytes.length);

    for (byte c : bytes) {
        if (c >= 'a' ? c <= 'z' || c == '~' :
            c >= 'A' ? c <= 'Z' || c == '_' :
            c >= '0' ? c <= '9' :  c == '-' || c == '.')
            builder.append((char)c);
        else
            builder.append('%')
                   .append(HEX.charAt(c >> 4 & 0xf))
                   .append(HEX.charAt(c & 0xf));
    }

    return builder.toString();
}

Он выходит за рамки Javascript за счет процентного кодирования каждого символа, который не является незарезервированным символом в соответствии с RFC 3986. .


Это обратное преобразование:

public static String decodeURIComponent(String str) {
    if (str == null) return null;

    int length = str.length();
    byte[] bytes = new byte[length / 3];
    StringBuilder builder = new StringBuilder(length);

    for (int i = 0; i < length; ) {
        char c = str.charAt(i);
        if (c != '%') {
            builder.append(c);
            i += 1;
        } else {
            int j = 0;
            do {
                char h = str.charAt(i + 1);
                char l = str.charAt(i + 2);
                i += 3;

                h -= '0';
                if (h >= 10) {
                    h |= ' ';
                    h -= 'a' - '0';
                    if (h >= 6) throw new IllegalArgumentException();
                    h += 10;
                }

                l -= '0';
                if (l >= 10) {
                    l |= ' ';
                    l -= 'a' - '0';
                    if (l >= 6) throw new IllegalArgumentException();
                    l += 10;
                }

                bytes[j++] = (byte)(h << 4 | l);
                if (i >= length) break;
                c = str.charAt(i);
            } while (c == '%');
            builder.append(new String(bytes, 0, j, UTF_8));
        }
    }

    return builder.toString();
}
person Nuno Cruces    schedule 18.09.2018

Я успешно использовал класс java.net.URI следующим образом:

public static String uriEncode(String string) {
    String result = string;
    if (null != string) {
        try {
            String scheme = null;
            String ssp = string;
            int es = string.indexOf(':');
            if (es > 0) {
                scheme = string.substring(0, es);
                ssp = string.substring(es + 1);
            }
            result = (new URI(scheme, ssp, null)).toString();
        } catch (URISyntaxException usex) {
            // ignore and use string that has syntax error
        }
    }
    return result;
}
person Mike Bryant    schedule 22.05.2015
comment
Нет, этот подход не совсем успешен, но относительно неплох. Однако у вас все еще есть проблемы. Например, кардинальный символ # java будет кодироваться как %23, javascript не будет его кодировать. См.: developer.mozilla.org/en-US/ docs/Web/JavaScript/Reference/ Javascript не использует пробелы. AZ AZ 0-9 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) # И для некоторых из них java будет espace. - person 99Sono; 10.09.2020
comment
Хорошо бы сделать UNIT-тест со следующим выражением: ''' Строковые символыJavascriptDoesNotEspace = A-Za-z0-9;,/?:@&=+$-_.!~*'()#; ''' Кардинал является единственным исключением. Поэтому исправление приведенного выше алгоритма, чтобы сделать его совместимым с javascript, тривиально. - person 99Sono; 10.09.2020

Это простой пример решения Рави Валлау:

public String buildSafeURL(String partialURL, String documentName)
        throws ScriptException {
    ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
    ScriptEngine scriptEngine = scriptEngineManager
            .getEngineByName("JavaScript");

    String urlSafeDocumentName = String.valueOf(scriptEngine
            .eval("encodeURIComponent('" + documentName + "')"));
    String safeURL = partialURL + urlSafeDocumentName;

    return safeURL;
}

public static void main(String[] args) {
    EncodeURIComponentDemo demo = new EncodeURIComponentDemo();
    String partialURL = "https://www.website.com/document/";
    String documentName = "Tom & Jerry Manuscript.pdf";

    try {
        System.out.println(demo.buildSafeURL(partialURL, documentName));
    } catch (ScriptException se) {
        se.printStackTrace();
    }
}

Вывод: https://www.website.com/document/Tom%20%26%20Jerry%20Manuscript.pdf

Он также отвечает на висячий вопрос в комментариях Лорен Шкипогня о том, как передать переменную String в encodeURIComponent(). Метод scriptEngine.eval() возвращает Object, поэтому его можно преобразовать в строку с помощью String.valueOf() среди других методов.

person silver    schedule 25.09.2016

для меня это сработало:

import org.apache.http.client.utils.URIBuilder;

String encodedString = new URIBuilder()
  .setParameter("i", stringToEncode)
  .build()
  .getRawQuery() // output: i=encodedString
  .substring(2);

или с другим UriBuilder

import javax.ws.rs.core.UriBuilder;

String encodedString = UriBuilder.fromPath("")
  .queryParam("i", stringToEncode)
  .toString()   // output: ?i=encodedString
  .substring(3);

На мой взгляд, использование стандартной библиотеки лучше, чем ручная постобработка. Также ответ @Chris выглядел хорошо, но он не работает для URL-адресов, таких как «http://a+b c .html"

person balazs    schedule 22.06.2017
comment
Использование стандартной библиотеки — это хорошо... ...если только вы не являетесь промежуточным продуктом и не зависите от другой версии стандартной библиотеки, и тогда любой, кто использует ваш код, должен возиться с зависимостями, а затем надеяться, что ничего не сломается... - person Ajax; 24.07.2017
comment
Было бы здорово, если бы это решение работало, но оно ведет себя не так, как запрос encodeURIComponent. encodeURIComponent возвращает для ?& результат %3F%26%20, но ваше предложение возвращает %3F%26+. Я знаю, что это упоминается несколько раз в других вопросах и ответах, но следует упомянуть здесь, прежде чем люди будут слепо доверять этому. - person Philipp; 23.03.2020

Я использовал String encodedUrl = new URI(null, url, null).toASCIIString(); для кодирования URL-адресов. Чтобы добавить параметры после существующих в url, я использую UriComponentsBuilder

person AlexN    schedule 22.07.2019
comment
Я создал демонстрацию, используя этот подход, который я считаю лучшим, мой вариант использования заключался в том, чтобы закодировать json, который можно было получить на стороне js, прочитав его из атрибута данных: repl.it/@raythurnevoid/URIEncodeJSON#Main.java - person raythurnevoid; 24.11.2020

Я нашел класс PercentEscaper из библиотеки google-http-java-client, который можно легко использовать для реализации encodeURIComponent.

http://javadoc.google-http-java-client.googlecode.com/hg/1.9.0-beta/com/google/api/client/util/escape/PercentEscaper.html http://code.google.com/p/google-http-java-client/

person honzajde    schedule 25.05.2012

В библиотеке Guava есть PercentEscaper:

Escaper percentEscaper = new PercentEscaper("-_.*", false);

"-_.*" - безопасные символы

false говорит, что PercentEscaper экранирует пробел с помощью «% 20», а не «+»

person Aliaksei Nikuliak    schedule 23.03.2016