79

I have some escaped strings that need to be unescaped. I'd like to do this in Python.

For example, in Python 2.7 I can do this:

>>> "\\123omething special".decode('string-escape')
'Something special'
>>> 

How do I do it in Python 3? This doesn't work:

>>> b"\\123omething special".decode('string-escape')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
LookupError: unknown encoding: string-escape
>>> 

My goal is to be able to take a string like this:

s\000u\000p\000p\000o\000r\000t\000@\000p\000s\000i\000l\000o\000c\000.\000c\000o\000m\000

And turn it into:

"support@psiloc.com"

After I do the conversion, I'll probe to see if the string I have is encoded in UTF-8 or UTF-16.

SuperStormer
  • 4,997
  • 5
  • 25
  • 35
vy32
  • 28,461
  • 37
  • 122
  • 246
  • 1
    Are you absolutely certain those are escapes and not literal bytes? – Martijn Pieters Feb 11 '13 at 20:47
  • 1
    They are literal bytes! There is a backslash, then a 0, then another 0, then a third 0... I have a program that reads a binary file and outputs information like this. It outputs the binary that is actually in the file. Sometimes the content of the file is UTF-8 coded and it just passes through. But if it isn't valid UTF-8 it gets encoded this way. – vy32 Feb 11 '13 at 20:48
  • 1
    [Same question, but does not specify version](https://stackoverflow.com/questions/1885181/how-do-i-un-escape-a-backslash-escaped-string-in-python). The lowest voted answer there answers for Py3. – user202729 May 12 '18 at 05:29

6 Answers6

72

You'll have to use unicode_escape instead:

>>> b"\\123omething special".decode('unicode_escape')

If you start with a str object instead (equivalent to the python 2.7 unicode) you'll need to encode to bytes first, then decode with unicode_escape.

If you need bytes as end result, you'll have to encode again to a suitable encoding (.encode('latin1') for example, if you need to preserve literal byte values; the first 256 Unicode code points map 1-on-1).

Your example is actually UTF-16 data with escapes. Decode from unicode_escape, back to latin1 to preserve the bytes, then from utf-16-le (UTF 16 little endian without BOM):

>>> value = b's\\000u\\000p\\000p\\000o\\000r\\000t\\000@\\000p\\000s\\000i\\000l\\000o\\000c\\000.\\000c\\000o\\000m\\000'
>>> value.decode('unicode_escape').encode('latin1')  # convert to bytes
b's\x00u\x00p\x00p\x00o\x00r\x00t\x00@\x00p\x00s\x00i\x00l\x00o\x00c\x00.\x00c\x00o\x00m\x00'
>>> _.decode('utf-16-le') # decode from UTF-16-LE
'support@psiloc.com'
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • That turns my binary object into a Unicode object. I want to keep it a binary object. Any way to do that? – vy32 Feb 11 '13 at 20:42
  • @vy32: Encode it after decoding? What encoding do you expect this to fit in? ASCII, Latin 1? – Martijn Pieters Feb 11 '13 at 20:44
  • It could be anything. The program probes a variety of possible codings. It might be ASCII, UTF-8, UTF-16, Latin 1, or a dozen other possibilities. – vy32 Feb 11 '13 at 20:49
  • 1
    @vy32: Then convert to 'proper' bytes by decoding from `unicode_escape`, then back to bytes via `latin1` (which has the happy coincidence of mapping 1-on-1). You then have bytes to try decodings on. – Martijn Pieters Feb 11 '13 at 21:01
35

The old "string-escape" codec maps bytestrings to bytestrings, and there's been a lot of debate about what to do with such codecs, so it isn't currently available through the standard encode/decode interfaces.

BUT, the code is still there in the C-API (as PyBytes_En/DecodeEscape), and this is still exposed to Python via the undocumented codecs.escape_encode and codecs.escape_decode.

>>> import codecs
>>> codecs.escape_decode(b"ab\\xff")
(b'ab\xff', 6)
>>> codecs.escape_encode(b"ab\xff")
(b'ab\\xff', 3)

These functions return the transformed bytes object, plus a number indicating how many bytes were processed... you can just ignore the latter.

>>> value = b's\\000u\\000p\\000p\\000o\\000r\\000t\\000@\\000p\\000s\\000i\\000l\\000o\\000c\\000.\\000c\\000o\\000m\\000'
>>> codecs.escape_decode(value)[0]
b's\x00u\x00p\x00p\x00o\x00r\x00t\x00@\x00p\x00s\x00i\x00l\x00o\x00c\x00.\x00c\x00o\x00m\x00'
Nathaniel J. Smith
  • 11,613
  • 4
  • 41
  • 49
  • 2
    It's a horrible idea to rely on undocumented API. Never ever to this in production code. – god Aug 10 '21 at 16:37
26

If you want str-to-str decoding of escape sequences, so both input and output are Unicode:

def string_escape(s, encoding='utf-8'):
    return (s.encode('latin1')         # To bytes, required by 'unicode-escape'
             .decode('unicode-escape') # Perform the actual octal-escaping decode
             .encode('latin1')         # 1:1 mapping back to bytes
             .decode(encoding))        # Decode original encoding

Testing:

>>> string_escape('\\123omething special')
'Something special'

>>> string_escape(r's\000u\000p\000p\000o\000r\000t\000@'
                  r'\000p\000s\000i\000l\000o\000c\000.\000c\000o\000m\000',
                  'utf-16-le')
'support@psiloc.com'
MestreLion
  • 12,698
  • 8
  • 66
  • 57
  • 2
    Note that if you are OK with always ending up in utf-8, you can do this in a single roundtrip like so: `s.encode('latin1', 'backslashreplace').decode('unicode-escape')` --- see https://stackoverflow.com/a/57192592/5583443 – Glen Whitney Feb 12 '21 at 03:58
  • @GlenWhitney this doesn't seem to do quite the same thing as `decode('string-escape')` in python 2 even for UTF-8. e.g. starting with `s = '\\xe7\\xa7\\x98'`, python2 `print s.decode('string-escape')` prints `秘` as I'd hope, and this answer in python3 does the same, but the linked answer to that question prints `ç§`. – James Jun 09 '21 at 18:35
  • @James that probably means you're using a different encoding. What is `sys.stdout.encoding`? – Mark Ransom Aug 19 '21 at 22:31
  • This is good, but the first `.encode` should be `.encode(encoding)`, otherwise it doesn't work with codepoints > 255. – FHTMitchell Feb 23 '23 at 14:57
  • @FHTMitchell: if you do this in the first `.encode` you should also do it in the 2nd to have a 1:1 mapping, and it won't work in the general case. This solution implies your input string is latin1 or ascii and codepoints > 255 are escaped. – MestreLion Feb 23 '23 at 17:09
  • @MestreLion I did what I said and ran some tests and it was fine. Why do you think it won't work? What test fails? – FHTMitchell Feb 24 '23 at 22:18
  • @FHTMitchell: I'm not sure if this solution would work for all encodings if mixing backslash escapes with codepoints > 255, or if mixing `.encode(encoding)` (1st) with `.encode("latin1")` (2nd). Maybe you're right and it does, I simply didn't consider this when crafting the answer, so not sure if there are edge cases. – MestreLion Mar 01 '23 at 16:13
6

py2

"\\123omething special".decode('string-escape')

py3

"\\123omething special".encode('utf-8').decode('unicode-escape')
Sérgio
  • 6,966
  • 1
  • 48
  • 53
  • does py2 take the `r` in front of the string so you don't need to escape the `\`? – vy32 Feb 22 '21 at 22:12
  • 1
    The `r` prefix means (non-final) backslashes in the string are just literal backslashes. Perhaps that's what you tried to ask but your comment seems botched. It's available in Python 2.7 but I believe it might not be in earlier versions of Python 2. (You should be using Python 3 for any new code in this day and age anyway.) – tripleee Nov 04 '21 at 11:33
4

You can't use unicode_escape on byte strings (or rather, you can, but it doesn't always return the same thing as string_escape does on Python 2) – beware!

This function implements string_escape using a regular expression and custom replacement logic.

def unescape(text):
    regex = re.compile(b'\\\\(\\\\|[0-7]{1,3}|x.[0-9a-f]?|[\'"abfnrt]|.|$)')
    def replace(m):
        b = m.group(1)
        if len(b) == 0:
            raise ValueError("Invalid character escape: '\\'.")
        i = b[0]
        if i == 120:
            v = int(b[1:], 16)
        elif 48 <= i <= 55:
            v = int(b, 8)
        elif i == 34: return b'"'
        elif i == 39: return b"'"
        elif i == 92: return b'\\'
        elif i == 97: return b'\a'
        elif i == 98: return b'\b'
        elif i == 102: return b'\f'
        elif i == 110: return b'\n'
        elif i == 114: return b'\r'
        elif i == 116: return b'\t'
        else:
            s = b.decode('ascii')
            raise UnicodeDecodeError(
                'stringescape', text, m.start(), m.end(), "Invalid escape: %r" % s
            )
        return bytes((v, ))
    result = regex.sub(replace, text)
malthe
  • 1,237
  • 13
  • 25
0

At least in my case this was equivalent:

Py2: my_input.decode('string_escape')
Py3: bytes(my_input.decode('unicode_escape'), 'latin1')

convertutils.py:

def string_escape(my_bytes):
    return bytes(my_bytes.decode('unicode_escape'), 'latin1')
guettli
  • 25,042
  • 81
  • 346
  • 663