Python Python 3 挑战第 17 关

Python Challenge level 17 in Python 3

我最近开始玩 The Python Challenge。虽然相当复杂,但所需的编码并不是很难,这使得学习许多有用的模块非常有趣。

我的问题是关于第 17 关的。我理解在第 4 关中需要遵循线索的想法,同时收集 cookie,这就是我所做的。但是,我无法对得到的字符串进行 BZ2 解压缩。

我尝试了谷歌搜索,我发现了一个很好的博客,其中包含 Python 2 中的解决方案。具体来说,第 17 级的是 here。分析那个,我意识到我确实得到了压缩字符串(来自 cookies)并且它在 Python 2:

中正确解压
bz2.decompress(urllib.unquote_plus(compressed))

然而,Python 3 中的 bz2.decompress 需要一个字节数组而不是字符串,但是上面那行明显的 Python 3 对应物:

bz2.decompress(urllib.parse.unquote_plus(message).encode("utf8"))

失败 OSError: Invalid data stream。我尝试了所有 standard encodings 和上面的一些变体,但无济于事。

到目前为止,这是我的(非工作)解决方案:

#!/usr/bin/env python3

"""
The Python Challenge #17: http://www.pythonchallenge.com/pc/return/romance.html

This is similar to #4 and it actually uses its solution. However, the key is in
the cookies. The page's cookie says: "you+should+have+followed+busynothing..."

So, we follow the chain from #4, using the word "busynothing" and
reading the cookies.
"""

import urllib.request, urllib.parse
import re
import bz2

nothing = "12345"
last_cookie = None
message = ""
while True:
    headers = dict()
    if last_cookie:
        headers["Cookie"] = last_cookie
    r = urllib.request.Request("http://www.pythonchallenge.com/pc/def/linkedlist.php?busynothing=" + nothing, headers=headers)
    with urllib.request.urlopen(r) as u:
        last_cookie = u.getheader("Set-Cookie")
        m = re.match(r"info=(.*?);", last_cookie)
        if m:
            message += m.group(1)
        text = u.read().decode("utf8")
        print("{} >>> {}".format(nothing, text))
        m = re.search(r"\d+$", text)
        try:
            nothing = str(int(m.group(0)))
        except Exception as e:
            print(e)
            break

print("Cookies message:", message)
print("Decoded:", bz2.decompress(urllib.parse.unquote_plus(message).encode("utf8")))

所以,我的问题是:Python 3 上述问题的解决方案是什么样的,为什么我的方法没有按预期工作?

我很清楚这部分可以做得更好。我正在寻找一个快速而肮脏的解决方案,所以我在这里的兴趣只是它有效(为什么不是我上面那样做的)。

这里需要用到urllib.parse.unquote_to_bytes() function。它不支持 + 到 space 的映射,但可以通过 str.replace():

轻松解决
urllib.parse.unquote_to_bytes(message.replace('+', '%20'))

这会很好地解压缩。您可以将生成的未压缩字符串解码为 ASCII:

print("Decoded:", bz2.decompress(urllib.parse.unquote_to_bytes(message.replace('+', '%20'))).decode('ascii'))

使用不同消息的演示我准备不放弃这个谜题:

>>> import bz2
>>> import urllib.parse
>>> another_message = 'BZh91AY%26SY%80%F4C%E8%00%00%02%13%80%40%00%04%00%22%E3%8C%00+%00%22%004%D0%40%D04%0C%B7%3B%E6h%B1AIM%3D%5E.%E4%8Ap%A1%21%01%E8%87%D0'
>>> bz2.decompress(urllib.parse.unquote_to_bytes(another_message.replace('+', '%20'))).decode('ascii')
'This is not the message'

或者,告诉 urllib.unquote_plus() 使用 Latin-1 编码而不是 UTF-8。 unquote_plus() 的默认错误处理程序设置为 'replace',因此您永远不会注意到原始数据无法解码为 UTF-8,因此字节被替换为 U+FFFD 替换字符,这是什么导致解压失败。 Latin-1 将所有字节 one-on-oe 直接映射到前 256 个 Unicode 字符,因此您可以编码回原始字节:

>>> '\ufffd' in urllib.parse.unquote_plus(another_message)
True
>>> bz2.decompress(urllib.parse.unquote_plus(another_message, 'latin1').encode('latin1')).decode('ascii')
'This is not the message'