有趣的解谜:Python Challenge

发布于: 2020 年 05 月 20 日
有趣的解谜:Python Challenge

Python Challenge 是一系列编程解谜集合的网站,总共 33 题,只有解决一题才能进入下一题。链接:http://www.pythonchallenge.com/

笔者最近刚接触 Python,刚好用 Python 去解决这里的谜题,本身自己也非常喜欢解谜类的游戏,娱乐学习两不误!

No.1 - Warming up

链接:http://www.pythonchallenge.com/pc/def/0.html

提示:try to change the URL address.

这一题应该是要计算 2 的 38 次方,Python 中计算幂次方有直接的操作符 ** 可以使用:

2**38 # reulst = 274877906944

接下来就是如何填上这个答案,通过观察 URL 中的 0 和提示不难发现,我们通过修改 URL 中的 0.html 即可通关。

通关指令:将 URL 中 HTML 文件名 0 改为 274877906944 即可。

No.2 - What about making trans?

链接:http://www.pythonchallenge.com/pc/def/map.html

提示:everybody thinks twice before solving this.

三个字符转换的示例,根据 ASCII 编码表得出:

K(75) -> M(77)

O(79) -> Q(81)

E(69) -> G(71)

不难发现,转换后的字符在 ASCII 编码表中指代的数值比转换前要大二,换句话说就是向后移动了两个字符。于是写了一个转换函数:

def trans(text):
result = ""
for c in text:
result += chr(ord(c) + 2)
return result
text = "g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc dmp. bmgle gr gl zw fylb gq glcddgagclr ylb rfyr'q ufw rfgq rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb. lmu ynnjw ml rfc spj."
print(trans(text))

执行结果为:i"hope"you"didnt"tr{nsl{te"it"|y"h{nd0"th{ts"wh{t"computers"{re"for0"doing"it"in"|y"h{nd"is"inefficient"{nd"th{t)s"why"this"text"is"so"long0"using"string0m{ketr{ns*+"is"recommended0"now"{pply"on"the"url0

虽然能看出不少正确的单词了,但还是有很多转换的有问题。

首先在这里空格和标点符号(如 . ' ( ))都不需要去转换,所以我们先改造一下我们的函数:

def trans(text):
result = ""
for c in text:
c_ascii = ord(c)
if not(65 <= c_ascii <= 90 or 97 <= c_ascii <= 122):
result += c
continue
result += chr(ord(c) + 2)
return result
text = "g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc dmp. bmgle gr gl zw fylb gq glcddgagclr ylb rfyr'q ufw rfgq rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb. lmu ynnjw ml rfc spj."
print(trans(text))

执行结果为:i hope you didnt tr{nsl{te it |y h{nd. th{ts wh{t computers {re for. doing it in |y h{nd is inefficient {nd th{t's why this text is so long. using string.m{ketr{ns() is recommended. now {pply on the url.

又清晰了不少,很明显解密后的字符串中出现的 { | 等字符是转换有问题的,观察一下在原始字符串中对应的字符是 y z 这两个。

显然对这两个字符的转换简单的往后移动两位是无意义的,考虑到最后解出来的应该是一段话,所以当移动到 26 个字母末尾仍需移动时则又从第一个字母开始,即 y 向后移动两位变为 a。

我们继续改造:

letters = "abcdefghijklmnopqrstuvwxyz"
def trans(text):
result = ""
for c in text:
if not c.isalpha():
result += c
continue
if c.isupper():
result += letters[(ord(c) - 65 + 2) % 26]
else:
result += letters[(ord(c) - 97 + 2) % 26]
return result
text = "g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc dmp. bmgle gr gl zw fylb gq glcddgagclr ylb rfyr'q ufw rfgq rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb. lmu ynnjw ml rfc spj."
print(trans(text))

这一次的执行结果:i hope you didnt translate it by hand. thats what computers are for. doing it in by hand is inefficient and that's why this text is so long. using string.maketrans() is recommended. now apply on the url.

解密成功,根据解密后的明文最后一句的指示,对 URL 也进行解密操作,这里笔者尝试了三次,对 pc/def/map.html 转换后得到 re/fgh/ocr.jvon,替换掉对应部分后访问发现是 404 Not Found,接着对 map.html 转换,访问后会下载得到一个名为 ocr.jvon 的文件,其内容是 Have you ever heard of jvon files !? 说明离答案很近了,但还是不对,想到解谜第一题的时候也只替换了 .html 的前缀部分,所以我们这里也只替换 mapocr 然后访问,BINGO!

通关指令:将 URL 中 HTML 文件名 map 改为 ocr 即可。

No.3 - ocr

链接:http://www.pythonchallenge.com/pc/def/ocr.html

提示:recognize the characters. maybe they are in the book, but MAYBE they are in the page source.

第一反应是从图片中找信息,但无奈图片的清晰度过低,没有什么有用信息。根据提示,可能在页面源代码中,右键选择「查看源代码」,最下面的一大段乱码有点突兀,估计就是从这里面提取线索。

乱码上面有一段文字:find rare characters in the mess below:,据此推断应该是需要统计下面这段乱码中每个字符出现的次数,找出最少的几个字符,借助 Python 来帮我们统计一下吧(str.txt 文件中存储的就是那段乱码):

from collections import Counter
fp = open('./str.txt', 'r')
frequency_cnt = Counter(fp.read())
print(frequency_cnt)

执行结果如下:

Counter({')': 6186, '@': 6157, '(': 6154, ']': 6152, '#': 6115, '_': 6112, '[': 6108, '}': 6105, '%': 6104, '!': 6079, '+': 6066,
'$': 6046, '{': 6046, '&': 6043, '*': 6034, '^': 6030, '\n': 1220, 'e': 1, 'q': 1, 'u': 1, 'a': 1, 'l': 1, 'i': 1, 't': 1, 'y': 1}
)

可以看到,出现次数为 1 的那些字符就是我们要找的,提取出来是 equality 这个英文单词,都不需要额外重组一下。

根据前几题的做法,那就是把 URL 中的 html 文件名替换成这个单词即可。

通关指令:将 URL 中 HTML 文件名 ocr 改为 equality 即可。

No.4 - re

链接:http://www.pythonchallenge.com/pc/def/equality.html

提示:One small letter, surrounded by EXACTLY three big bodyguards on each of its sides.

我们先提取已知线索:

1 - 根据图片,应该是一种规则:高高高低高高高。

2 - 根据页面的标题:re,考虑到 Python 中的正则模块名字就叫 re,这一道谜题应该是需要用到正则表达式去解。

3 - 根据提示,应该是要让我们去找到小写字母的左右两边被三个大写字母包裹的所有情况。

那么文本在哪?抱着试一试的态度去查看下页面源代码,果然在源代码的注释那里。

直接上正则,安排!

import re
# 需要匹配的文本保存在 str.txt 文件中
fp = open('./str.txt', 'r')
regex = r"[A-Z]{3}[a-z][A-Z]{3}"
matches = re.finditer(regex, fp.read(), re.MULTILINE)
for matchNum, match in enumerate(matches, start=1):
print(match.group())

执行结果得到 477 个匹配项,而且杂乱无章,看来不够准确,仍然需要找更多线索。

盯了图片一会,会不会和图片中的蜡烛形状有关,比如左边第一个大写字母应该和右边第一个大写字母相同?

加上这个规则匹配结果如下:

BEMxBOJ
VSJwVPY
UZViUEX
NWFaNWV
GGQaGTA
CYPrCXG
OJMfOOP
QIKbQHC
JVMnJRZ
AJYbABW
VGJtVJO
KTFtKUB
DHLwDZO
SVViSLM

也没什么有序逻辑= =

对了,很可能出在我们的正则表达式不够准确,我们是不是没有考虑左边大写字母之前和右边大写字母之后是什么?

再反复通读几遍提示,这个 EXACTLY 是不是可以理解为:aAAAaAAAa 这样的规则,也就是左右两边刚好是被 三个 大写字母包裹。

把我们的正则规则改一下:[a-z][A-Z]{3}[a-z][A-Z]{3}[a-z] 再去执行一下:

qIQNlQSLi
eOEKiVEYj
aZADnMCZq
bZUTkLYNg
uCNDeHSBj
kOIXdKBFh
dXJVlGZVm
gZAGiLQZx
vCJAsACFl
qKWGtIDCj

取出中间的小写字母:linkedlist 看来我们是找到来,把 URL 中的 equality 改为 linkedlist 跳转成功,此时页面显示 linkedlist.php,再把 URL 中的 html 后缀改成 php 就成功进入下一题了。

通关指令:将 URL 中 HTML 文件名及其后缀 equality.html 改为 linkedlist.php 即可。

No.5 - follow the chain

链接:http://www.pythonchallenge.com/pc/def/linkedlist.php

提示:无

没有提示,看图片也看不出什么来,先查看下页面源代码,提取到两个线索:

<!-- urllib may help. DON'T TRY ALL NOTHINGS, since it will never
end. 400 times is more than enough. -->
<a href="linkedlist.php?nothing=12345"><img src="chainsaw.jpg" border="0"/></a>

1 - 点击图片会跳转到一个带参数的链接。

2 - 这个 NOTHING 参数会一直变化,但是需要根据前一个数值才能知道下一个数值,也就是 linkedlist 的意思

先手动试一下:

访问:http://www.pythonchallenge.com/pc/def/linkedlist.php?nothing=12345,得到 and the next nothing is 44827

访问:http://www.pythonchallenge.com/pc/def/linkedlist.php?nothing=44827,得到 and the next nothing is 45439

以此类推。。。

这也是注释里说的 urllib may help 的意思,我们可以借助 Python 的 urllib 库去不断循环获取下一个数值,因为要获取到数字,所以还需要借助正则去匹配,代码如下:

from urllib import request
import re
def regexNum(data):
regex = r"\d+$"
matches = re.finditer(regex, data, re.MULTILINE)
for matchNum, match in enumerate(matches, start=1):
return match.group()
def req(prevNum):
with request.urlopen('http://www.pythonchallenge.com/pc/def/linkedlist.php?nothing=' + str(prevNum)) as f:
data = f.read()
return data.decode('utf-8')
num = 12345
for i in range(0, 400):
data = req(num)
num = regexNum(data)
print(data)

大概进行 87 次的时候,会遇到一个线索 Yes. Divide by two and keep going.,此时数值为 16044,我们对其除二得到 8022,继续代入循环。

大概又进行 57 次的时候,会遇到下一个线索 There maybe misleading numbers in the text. One example is 82683. Look only for the next nothing and the next nothing is 63579,将 63579 继续代入循环。

继续循环,直到出现最后我们要的结果:peak.html

通关指令:将 URL 中 HTML 文件名及其后缀 linkedlist.php 改为 peak.html 即可。

No.6 - peak hell

链接:http://www.pythonchallenge.com/pc/def/peak.html

提示:pronounce it

好吧,我读了几十遍 peak hell 也没想出来,页面就这幅图和两个单词的提示,继续看看页面源代码吧,应该有线索。

<html>
<head>
<title>peak hell</title>
<link rel="stylesheet" type="text/css" href="../style.css">
</head>
<body>
<center>
<img src="peakhell.jpg"/>
<br><font color="#c0c0ff">
pronounce it
<br>
<peakhell src="banner.p"/>
</body>
</html>
<!-- peak hell sounds familiar ? -->

上面的源代码中有两条线索:

1 - 有一个自定义的 HTML 标签,里面的链接打开是一大串乱码文本。

2 - 把 peak hell 读出来的谐音是什么?

按照前几题的调性,估计会用到 Python 中的库,所以让我们想一下 Python 里面有啥库的读音是这个吗?p k l 用 Google 搜索 python p k l ,第一个结果就是 Pickle in Python,看来我们要找的库就是它了。

It is used for serializing and de-serializing a Python object structure. Any object in python can be pickled so that it can be saved on disk. What pickle does is that it “serialises” the object first before writing it to file. Pickling is a way to convert a python object (list, dict, etc.) into a character stream.

我们把 自定义标签的链接 里的文本保存到 str.txt,然后用 pickle 加载出来,代码如下:

import pickle
fp = open('str.txt', 'rb')
data = pickle.load(fp)
print(data)
fp.close()

加载出来的是类似如下(只截取了一部分):

[[(' ', 95)], [(' ', 14), ('#', 5), (' ', 70), ('#', 5), (' ', 1)], [(' ', 15), ('#', 4), (' ', 71), ('#', 4), (' ', 1)], [(' ', 15), ('#', 4), (' ', 71), ('#', 4), (' ', 1)], .....

手动美化一下:

[
[(' ', 95)],
[(' ', 14), ('#', 5), (' ', 70), ('#', 5), (' ', 1)],
[(' ', 15), ('#', 4), (' ', 71), ('#', 4), (' ', 1)],
[(' ', 15), ('#', 4), (' ', 71), ('#', 4), (' ', 1)],
[(' ', 15), ('#', 4), (' ', 71), ('#', 4), (' ', 1)],
[(' ', 15), ('#', 4), (' ', 71), ('#', 4), (' ', 1)],
[(' ', 15), ('#', 4), (' ', 71), ('#', 4), (' ', 1)],
[(' ', 15), ('#', 4), (' ', 71), ('#', 4), (' ', 1)],
[(' ', 15), ('#', 4), (' ', 71), ('#', 4), (' ', 1)],
.....

盯了几分钟,是要编码转换?空格和井号干什么的?后面的数字代表什么?

还是起身倒杯柠檬水压压惊先,嗯?!这些空格和井号排列起来怎么看起来像字符!突然就想到很有可能是字符画。

具体一点就是这个二维数组的每一个元素代表一行,每个元素又是一个一维数组,每个子元素 tuple 第一项是需要输出的字符,第二项是输出字符的个数。

代码如下:

import pickle
fp = open('str.txt', 'rb')
obj = pickle.load(fp)
for arr in obj:
for item in arr:
for i in range(item[1]):
print(item[0], end='')
print()
fp.close()

执行结果如下:

##### #####
#### ####
#### ####
#### ####
#### ####
#### ####
#### ####
#### ####
### #### ### ### ##### ### ##### ### ### ####
### ## #### ####### ## ### #### ####### #### ####### ### ### ####
### ### ##### #### ### #### ##### #### ##### #### ### ### ####
### #### #### ### ### #### #### #### #### ### #### ####
### #### #### ### #### #### #### #### ### ### ####
#### #### #### ## ### #### #### #### #### #### ### ####
#### #### #### ########## #### #### #### #### ############## ####
#### #### #### ### #### #### #### #### #### #### ####
#### #### #### #### ### #### #### #### #### #### ####
### #### #### #### ### #### #### #### #### ### ####
### ## #### #### ### #### #### #### #### #### ### ## ####
### ## #### #### ########### #### #### #### #### ### ## ####
### ###### ##### ## #### ###### ########### ##### ### ######

是的,就是字符画,channel 。

通关指令:将 URL 中 HTML 文件名 peak 改为 channel 即可。

No.7 - now there are pairs

链接:http://www.pythonchallenge.com/pc/def/channel.html

提示:无

图片上第一眼看不出来什么,依旧照惯例查看页面源代码:

<html> <!-- <-- zip -->
<head>
<title>now there are pairs</title>
<link rel="stylesheet" type="text/css" href="../style.css">
</head>
<body>
<center>
<img src="channel.jpg">
<br/>
<!-- The following has nothing to do with the riddle itself. I just
thought it would be the right point to offer you to donate to the
Python Challenge project. Any amount will be greatly appreciated.
-thesamet
-->
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_xclick">
<input type="hidden" name="business" value="thesamet@gmail.com">
<input type="hidden" name="item_name" value="Python Challenge donations">
<input type="hidden" name="no_note" value="1">
<input type="hidden" name="currency_code" value="USD">
<input type="hidden" name="tax" value="0">
<input type="hidden" name="bn" value="PP-DonationsBF">
<input type="image" src="https://www.paypal.com/en_US/i/btn/x-click-but04.gif" border="0" name="submit" alt="Make payments with PayPal - it's fast, free and secure!">
<img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1">
</form>
</body>
</html>

暂且不管赞赏的部分,看第一行的注释,有一个 zip 关键词。我看到首先想到的将 URL 中的 channel 替换成 zip,试了一下,得到一条有效线索,但不是最终答案:yes. find the zip.

意思很明确,需要我们找到这个 zip 压缩包,我们再回过来看页面源代码,zip 包裹的注释 <html> <!-- <-- zip --> 多了一个箭头标示,刚好又指向了 html 标签,立马联想到应该是将 URL 中的 html 后缀改成 zip!

我们成功得到了这个 channel.zip 压缩包,直接解压出来得到几百个 num.txt 和一个 readme.txt,查看 readme.txt 文件内容:

welcome to my zipped list.
hint1: start from 90052
hint2: answer is inside the zip

我们得到了两条线索:

1 - 从 90052.txt 文件开始读取

2 - 答案在压缩包里面

import re
def regexNum(data):
regex = r"\d+$"
matches = re.finditer(regex, data, re.MULTILINE)
for matchNum, match in enumerate(matches, start=1):
return match.group()
def req(prevNum):
fp = open(str(prevNum) + '.txt', 'r')
data = fp.read()
fp.close()
return data
num = 90052
for i in range(909):
data = req(num)
num = regexNum(data)
print(data)

我们得到最后一个文件 46145.txt,内容为 Collect the comments.

评论、评论、评论。。。哪里会有我们要的评论?对了,我们是不是还没用到过 zip 相关的库。。。以及第二条提示 答案在压缩包里 我们也还没用到。

想到压缩包是可以添加评论的,试试看!

import re
import zipfile
def regexNum(data):
regex = r"\d+$"
matches = re.finditer(regex, data, re.MULTILINE)
for matchNum, match in enumerate(matches, start=1):
return match.group()
def req(prevNum):
fp = open(str(prevNum) + '.txt', 'r')
data = fp.read()
fp.close()
return data
num = 90052
comments = []
with zipfile.ZipFile(r'../channel.zip', 'r') as file:
count = len(file.infolist()) - 1
for i in range(count):
comments.append(file.getinfo(str(num) + ".txt").comment)
data = req(num)
num = regexNum(data)
for comment in comments:
print(comment.decode("utf-8"), end='')

执行结果如下:

****************************************************************
****************************************************************
** **
** OO OO XX YYYY GG GG EEEEEE NN NN **
** OO OO XXXXXX YYYYYY GG GG EEEEEE NN NN **
** OO OO XXX XXX YYY YY GG GG EE NN NN **
** OOOOOOOO XX XX YY GGG EEEEE NNNN **
** OOOOOOOO XX XX YY GGG EEEEE NN **
** OO OO XXX XXX YYY YY GG GG EE NN **
** OO OO XXXXXX YYYYYY GG GG EEEEEE NN **
** OO OO XX YYYY GG GG EEEEEE NN **
** **
****************************************************************
**************************************************************

最终我们得到了关键词 HOCKEY,修改 URL 的 channel => hockey,访问链接得到 it's in the air. look at the letters.

看来我们还差一点,不过离答案已经很接近了!我们得到的这句话什么意思:好好看看字母!

我们可以看到组成 HOCKEY 的每一个字符画的字符都是不同的,之前的题目中有一题也是字符画,CHANNEL 都是用井号画的,看来这些小字符才是我们需要的,最后得到 oxygen

通关指令:将 URL 中 HTML 文件名 channel 改为 oxygen 即可。

No.8 - smarty

链接:http://www.pythonchallenge.com/pc/def/oxygen.html

提示:无

除了这张图,找不到其他信息了。那么图片里有什么特别之处?

图片中间有一条不同灰度的点组成的线,很突兀,有猫腻。

不出意外是需要提取这些点的 RGB 数值,然后再处理一下得出线索。

Python 中处理图像的库 Pillow 能很方便的帮我们提取这些 Pixel 的 RGB 值:

from PIL import Image
with Image.open('oxygen.png') as im:
row = [im.getpixel((x, im.height/2)) for x in range(im.width)]
print(row)

执行结果如下:

[(115, 115, 115, 255), (115, 115, 115, 255), (115, 115, 115, 255), (115, 115, 115, 255), (115, 115, 115, 255), (109, 109, 109, 255), (109, 109, 109, 255), (109, 109, 109, 255), (109, 109, 109, 255), (109, 109, 109, 255), (109, 109, 109, 255), (109, 109, 109, 255), (97, 97, 97, 255), (97, 97, 97, 255),
...
...

可以发现一个规律:每七个像素点对应图片上那条灰线的一个色块。

我们用切片的步长语法糖过滤一下:

row = row[::7]

因为这条线末端并没有横穿整个图片,所以需要去掉这几个色块,也就是将 !(R = G = B) 的像素点过滤掉:

[r for r, g, b, a in row if r == g == b]

得到结果如下:

[115, 109, 97, 114, 116, 32, 103, 117, 121, 44, 32, 121, 111, 117, 32, 109, 97, 100, 101, 32, 105, 116, 46, 32, 116, 104, 101, 32, 110, 101, 120, 116, 32, 108, 101, 118, 101, 108, 32, 105, 115, 32, 91, 49, 48, 53, 44, 32, 49, 49, 48, 44, 32, 49, 49, 54, 44, 32, 49, 48, 49, 44, 32, 49, 48, 51, 44, 32, 49, 49, 52, 44, 32, 49, 48, 53, 44, 32, 49, 49, 54, 44, 32, 49, 50, 49, 93]

试试看是不是 ASCII 编码转换:

print("".join(map(chr, valid_pixels)))

得到结果如下:

smart guy, you made it. the next level is [105, 110, 116, 101, 103, 114, 105, 116, 121]

看来是的,同样的对 [105, 110, 116, 101, 103, 114, 105, 116, 121] 也转换一下就得到了我们的答案 integrity

通关指令:将 URL 中 HTML 文件名 oxygen 改为 integrity 即可。

No.8 - working hard?

链接:http://www.pythonchallenge.com/pc/def/integrity.html

提示:where is the missing link?

查看页面源代码,能够提取到三个线索:

1 - 有一系列坐标点组成的多边形区域

2 - 有一个链接 ../return/good.html

3 - 有一段注释

直观一点的就只有第二点,打开链接,弹出输入账号密码的对话框,再看注释:

<!--
un: 'BZh91AY&SYA\xaf\x82\r\x00\x00\x01\x01\x80\x02\xc0\x02\x00 \x00!\x9ah3M\x07<]\xc9\x14\xe1BA\x06\xbe\x084'
pw: 'BZh91AY&SY\x94$|\x0e\x00\x00\x00\x81\x00\x03$ \x00!\x9ah3M\x13<]\xc9\x14\xe1BBP\x91\xf08'
-->

能看出来 un 表示的是 username,pw 表示的是 password,估计是要转码,先原样复制到登录框试试,果然不行。

不知道大家对 BZh9 这个前缀有没有感觉,用 bz2 压缩文本的前缀就是这个,而且从蜜蜂 bee 的发音,标题 working hard = busy,无一不在提示 bz2 :

import bz2
# b'BZh91AY&SY_\x7f\x87U\x00\x00\x00\t\x808\x00 \x00\x08\x00 \x00!\x83A\x9a\r(qw$S\x85\t\x05\xf7\xf8uP'
print(bz2.compress(b'123sa'))
# b'BZh91AY&SY\x191e=\x00\x00\x00\x81\x00\x02D\xa0\x00!\x9ah3M\x073\x8b\xb9"\x9c(H\x0c\x98\xb2\x9e\x80'
print(bz2.compress(b'hello'))

我们解压缩一下:

import bz2
print(bz2.decompress(b'BZh91AY&SYA\xaf\x82\r\x00\x00\x01\x01\x80\x02\xc0\x02\x00 \x00!\x9ah3M\x07<]\xc9\x14\xe1BA\x06\xbe\x084'))
print(bz2.decompress(b'BZh91AY&SY\x94$|\x0e\x00\x00\x00\x81\x00\x03$ \x00!\x9ah3M\x13<]\xc9\x14\xe1BBP\x91\xf08'))

结果就是 huge 和 file 两个单词,前者是用户名,后者是密码,填入刚才的登录框即可进入下一关。

通关指令:将 URL 替换成 http://www.pythonchallenge.com/pc/return/good.html 即可。

No.9 - connect the dots

链接:http://www.pythonchallenge.com/pc/return/good.html

提示:无

查看页面源代码,有 first 和 second 两个数字列表以及一个提示 first+second=?

因为两个列表的长度明显不一样,简单相加做不了。

先研究一下这些数字有什么特征:

146,399,163,403,170,393,169,391 => (146, 399), (163, 403), (170, 393), (169, 391)

这样组合出来基本能够看出是坐标点了,结合标题说的 connect the dots,看来是要我们把坐标连起来,祭出 Pillow 神器:

from PIL import Image
# 限于篇幅,自行补全。
# fist = [...]
# second = [...]
im = Image.new('RGB', (500, 500))
for i in range(len(first)//2):
im.putpixel((first[i*2], first[i*2+1]), (255, 255, 255))
for i in range(len(second)//2):
im.putpixel((second[i*2], second[i*2+1]), (255, 255, 255))
im.show()

画出来竟然是头牛

上面的代码其实也可以用 Pillow 的 ImageDraw 的 polygon 来画,那样的话可以直接把这两个列表传进去,但是个人觉得还是上面这种画出来的更生动一些。

牛的英文单词是 cow,我们把 cow 替换到 URL 上,跳转过去提示我们 hmm. it's a male.

通关指令:将 URL 中 HTML 文件名 good 改为 bull 即可。

No.10 - connect the dots

链接:http://www.pythonchallenge.com/pc/return/bull.html

提示:len(a[30]) = ?

我们上一关画出来的牛就是它!

你可以选择点击这头牛也可以查看页面源代码找到这个链接:http://www.pythonchallenge.com/pc/return/sequence.txt,访问后得到:

a = [1, 11, 21, 1211, 111221,

看来这一关是要找规律,看上去很有规律,但找了半天没找到规律。。。上 Google 搜一下这个数列看看是不是什么著名数列,这种数列叫 Look-and-say sequence点击链接查看维基百科

规律找到了就用 Python 来写一个程序计算就好了:

# Returns n'th term in
# look-and-say sequence
def genSeq(n):
if (n == 1):
return "1"
if (n == 2):
return "11"
s = "11"
for i in range(3, n + 1):
s += '$'
l = len(s)
cnt = 1
tmp = ""
for j in range(1, l):
if (s[j] != s[j - 1]):
tmp += str(cnt)
tmp += s[j - 1]
cnt = 1
else:
cnt += 1
s = tmp
return s
print(len(genSeq(31)))
# 结果为 5808

通关指令:将 URL 中 HTML 文件名 bull 改为 5808 即可。

No.11 - odd even

链接:http://www.pythonchallenge.com/pc/return/5808.html

提示:无

这一关除了这张图片,就真的啥都找不出来其他什么信息了。根据前面几关对图像处理的惯性思维,这图片看着这么奇怪,多半也是需要图像处理。

我一直都很关注每一关的标题,这一关的标题是奇偶,结合图像处理,多少能猜出来是对图像像素点的奇偶坐标进行拆分,从而分离出两张图像,然后应该能找到一些线索,处理代码如下:

from PIL import Image
im = Image.open('cave.jpg', 'r')
oddImg = Image.new('RGB', (640, 480))
evenImg = Image.new('RGB', (640, 480))
for i in range(im.width):
for j in range(im.height):
r, g, b = im.getpixel((i, j))
if i % 2 == 0 and j % 2 == 0:
oddImg.putpixel((i, j), (r, g, b))
else:
evenImg.putpixel((i, j), (r, g, b))
oddImg.show()
evenImg.show()

分离出来的两张图像如下所示:

第一张基本和原图一样,第二张乍一看没东西啊,黑不溜秋的,站起来俯视角度一看,有一个单词 evil 出现了,一试发现答案就是它(下载下来看)。

通关指令:将 URL 中 HTML 文件名 5808 改为 evil 即可。

No.12 - dealing evil

链接:http://www.pythonchallenge.com/pc/return/evil.html

提示:无

和上一关一样没有找到什么有用的信息,所以一开始的做法是对这个图片像上一关那样处理看看,不管怎么处理都没能得到什么信息,遂放弃这个方向。

图片的文件名是 evil1.jpg,这种解谜游戏不会平白无故给一个文件名后面多加一个数字(当然也不是肯定,但要对一切都抱有怀疑态度)。

试了一下 evil2.jpg 发现还真是有这张图片:

图片告诉我们后缀不是 jpg 而是 gfx,我们把修改当前链接最后的文件后缀下载到了 evil2.gfx 文件。这文件后缀也没听过,通过程序打开来也是一段二进制文本,至少目测是看不出来什么东西了。

我们继续访问 evil3.jpg,还有。。

这图暂时也没什么用,继续访问 evil4.jpg,还真能访问到,但图是裂的。如果有一张能下载到的图片但是是裂图,很奇怪,下载下来大小有 23 bytes,搞不好这个 23 字节里有线索!

既然查看图片是裂图,那么用文本编辑器打开看看是什么内容:

Bert is evil! go back!

以为答案是 bert,替换后访问 http://www.pythonchallenge.com/pc/return/bert.html 成功跳转,得到一段话:Yes! Bert is evil! 以及一张图片:

结束了?怎么可能。。。都没跳转到下一关!我们看看手头还有什么牌还没用,就只剩那个 gfx 文件了吗。。。

要怎么处理这段二进制文本呢?还得找其他线索。

再回来看原图,图片里在发牌,这个牌上的符号很奇怪啊,看着像什么?数字 5

等等!桌上有五堆牌,意思就是把文件分成五份!

with open('evil2.gfx', 'rb') as fp:
data = fp.read()
cnt = len(data)
for i in range(5):
open('evil_res_%d.jpg' % i, 'wb').write(data[(i * cnt // 5): ((i + 1) * cnt // 5)])

不管文件后缀是啥,出来的还是乱码,看来分割方法不对。我真傻,发牌哪有一次发完一等份再发下一个的啊,当然是一人一张循环发下去!

这里我们使用 Python 切片的第三个步长参数配合第一个其实位置参数能够很方便循环出来:

with open('evil2.gfx', 'rb') as fp:
data = fp.read()
cnt = len(data)
for i in range(5):
open('evil_res_%d.jpg' % i, 'wb').write(data[i::5])

出来五张图片:

第四张图多少能看出来是 ional,把这几个单词拼起来就是 disproportional 这个单词!

通关指令:将 URL 中 HTML 文件名 evil 改为 disproportional 即可。

No.13 - call him

链接:http://www.pythonchallenge.com/pc/return/disproportional.html

提示:phone that evil

通过查看页面源代码可以知道点击数字 5 可以打开一个链接:http://www.pythonchallenge.com/pc/phonebook.php

但是这个链接用浏览器访问提示的是 XML 的错误代码:faultCode 105 | fault String XML error: Invalid document end at line 1, column 1

对 XML 用的不多,没关系,Google 一下发现关于这个错误基本是围绕 Python 的 xmlrpc 官方库,大致从官方文档上了解了一下:

XML-RPC is a Remote Procedure Call method that uses XML passed via HTTP(S) as a transport. With it, a client can call methods with parameters on a remote server (the server is named by a URI) and get back structured data. This module supports writing XML-RPC client code; it handles all the details of translating between conformable Python objects and XML on the wire.

怪不得我们通过浏览器访问是会报错的,需要换一种连接方式,代码如下:

import xmlrpc.client
conn = xmlrpc.client.ServerProxy('http://www.pythonchallenge.com/pc/phonebook.php')
print(conn.system.listMethods())
# 执行结果
# ['phone', 'system.listMethods', 'system.methodHelp', 'system.methodSignature', 'system.multicall', 'system.getCapabilities']

显然我们需要调用 phone 这个方法,但还不清楚这个方法的入参和返回都是什么,借助另外两个方法 system.methodSignature 和 system.methodHelp 就可以大致清楚了:

print(conn.system.methodSignature('phone'))
print(conn.system.methodHelp('phone'))
# 执行结果
# [['string', 'string']]
# Returns the phone of a person

那么我们现在可以大致推断:phone() 函数有一个入参,代表一个人;有一个返回值,代表电话号码。那么关键就是得到这个人叫什么,这恰好就在我们上一关里得到的那段话里:Yes! Bert is evil!

看来 Bert 就是我们要找的人!

print(conn.phone('Bert'))
# 执行结果
# 555-ITALY

把 ITALY 替换到 URL 中,访问得到:SMALL letters.

通关指令:将 URL 中 HTML 文件名 disproportional 改为 italy 即可。

No.14 - walk around

链接:http://www.pythonchallenge.com/pc/return/italy.html

提示:无

查看页面源代码,可以得到两条线索:

1 - 有一个公式:remember: 100*100 = (100+99+99+98) + (...

2 - 有一张类似条形码的图片

图片下载下来可以发现其实是 10000 1 的一张图,而不是页面上显示的 100 100,但这两者之间有联系,也就是 10000 = 100 * 100。

我们再结合这张螺旋型的面包图片以及页面标题 walk around,猜测其实是让我们把这张 10000 1 的图片中的一万个像素点转换成 100 100 的格式,如何转呢?螺旋型。

想象一下,有一个 100 * 100 的正方形方框,有一根 10000 长度的铁丝,由最初沿着方框绕逐步往内绕进去,就像:

代码如下:

from PIL import Image
im = Image.open('wire.png')
delta = [(1, 0), (0, 1), (-1, 0), (0, -1)]
out = Image.new('RGB', [100, 100])
x, y, p = -1, 0, 0
d = 200
while d / 2 > 0:
for v in delta:
steps = d // 2
for s in range(steps):
x, y = x + v[0], y + v[1]
out.putpixel((x, y), im.getpixel((p, 0)))
p += 1
d -= 1
out.show()

结果如下:

我们替换成 cat 后并没有进入下一关,而是一张图一段话:

and its name is uzi. you'll hear from him later.

通关指令:将 URL 中 HTML 文件名 italy 改为 uzi 即可。

No.15 - whom?

链接:http://www.pythonchallenge.com/pc/return/uzi.html

提示:无

未完待续。。。

发布于: 2020 年 05 月 20 日 阅读数: 4
用户头像

imxfly

关注

还未添加个人签名 2017.09.10 加入

还未添加个人简介

评论

发布
暂无评论
有趣的解谜:Python Challenge