../
笨方法写Quine ============= 2024-09-21 ## 什么是Quine Quine是一类很有意思的程序,它运行之后所输出的结果是它自身。这种程序又被称为“自 我复制程序”,具有某种类似生物一样的奇妙的自指特征,很有意思。 下面两个quine的例子是我从维基百科)上抄下来的。 示例1: Python c = 'c = %r; print(c %% c)'; print(c % c) 示例2: Java public class Quine { public static void main(String[] args) { char q = 34; // Quotation mark character String[] l = { // Array of source code "public class Quine", "{", " public static void main(String[] args)", " {", " char q = 34; // Quotation mark character", " String[] l = { // Array of source code", " ", " };", " for (int i = 0; i < 6; i++) // Print opening code", " System.out.println(l[i]);", " for (int i = 0; i < l.length; i++) // Print string array", " System.out.println(l[6] + q + l[i] + q + ',');", " for (int i = 7; i < l.length; i++) // Print this code", " System.out.println(l[i]);", " }", "}", }; for (int i = 0; i < 6; i++) // Print opening code System.out.println(l[i]); for (int i = 0; i < l.length; i++) // Print string array System.out.println(l[6] + q + l[i] + q + ','); for (int i = 7; i < l.length; i++) // Print this code System.out.println(l[i]); } } 但是这两个例子里面,代码都不是很好理解。假如我想要用第三种语言,比如C++或者 JavaScript,写一个quine,还需要重新构思。 因此,我希望找到一种通用的方式,可以以最直观最容易的方式写出quine。虽然这种 quine不一定是最短、最高效的,但是最直观、最容易举一反三。 ## Quine规律 观察上一节的两个quine,虽然看起来区别很大,但是其实总结起来大概分这么几步: 1. 开头,可能有一些import或者类定义之类的东西; 2. 一个字符串,因为其作用有点像是遗传物质,所以这里我就记作*DNA*,其内容应当包 含这个字符串前面的程序,加上这个字符串后面的程序; 3. 结尾: 1. 从字符串*DNA*中取出开头,并打印; 2. 打印这个字符串本身; 3. 从字符串*DNA*中取出结尾,并打印。 虽然看上去很简单,但是常用的编程语言中,在表达字符串的时候,换行符、引号都需要 额外的转义。例如,字符串中换行符要写作“\n”,引号前面要加上反斜杠。这些内容带来 了不必要的麻烦,这也是上面的代码难以看懂的原因。 但是,这些转义符号,其本质都是一种**编码**。所以,我的思路是,干脆直接把字符串 *DNA*编码成最简单的16进制形式,然后需要打印的时候再解码。 这样,就不需要奇怪的hack了。 而且,也不一定非得是16进制编码,用base64、base32,或者跟真正的DNA一样,用碱基字 母编码,都是可以的。 ## 妙妙小工具 为了把字符串编码成16进制,我用Python写了一个小工具,可以读入字符串,输出其16进 制形式: import sys s = sys.stdin.read() if s[-1] == '\n': s = s[:-1] print(s.encode('utf-8').hex()) 把上述代码保存为hexencode.py,然后在Shell中可以这样使用: cat input.txt | python3 hexencode.py ## 开始写代码 Python写起来比较简单,就先用Python吧。 首先定义字符串*DNA*。这里我们不知道“DNA”的内容,所以先用emoji符号代替。因为这个 字符串里面包含了两部分内容:头和尾,我们假设头是老虎,尾巴是蛇: dna = '🐱,🐍' 然后把头和尾巴取出来: head, tail = dna.split(',') 因为我们打算用16进制编码,所以这里要把头和尾都用16进制解码恢复成原来的样子: head = bytes.fromhex(head).decode('utf-8') tail = bytes.fromhex(tail).decode('utf-8') 最后,我们把头、DNA、尾巴,这三部分拼接起来,一起输出: print(head + dna + tail) 现在的程序是这样的: dna = '🐱,🐍' head, tail = dna.split(',') head = bytes.fromhex(head).decode('utf-8') tail = bytes.fromhex(tail).decode('utf-8') print(head + dna + tail) 老虎🐱头前面的部分是: dna = ' 使用妙妙小工具编码,得到: 646e61203d2027 把🐱替换成这个字符串。 接着,蛇🐍尾巴后面的部分是: ' head, tail = dna.split(',') head = bytes.fromhex(head).decode('utf-8') tail = bytes.fromhex(tail).decode('utf-8') print(head + dna + tail) 使用妙妙小工具编码,得到: 270a686561642c207461696c203d20646e612e73706c697428272c27290a68656164203d2062797465732e66726f6d6865782868656164292e6465636f646528277574662d3827290a7461696c203d2062797465732e66726f6d686578287461696c292e6465636f646528277574662d3827290a7072696e742868656164202b20646e61202b207461696c29 把🐍替换成这个字符串。 最后得到了这样的代码: dna = '646e61203d2027,270a686561642c207461696c203d20646e612e73706c697428272c27290a68656164203d2062797465732e66726f6d6865782868656164292e6465636f646528277574662d3827290a7461696c203d2062797465732e66726f6d686578287461696c292e6465636f646528277574662d3827290a7072696e742868656164202b20646e61202b207461696c29' head, tail = dna.split(',') head = bytes.fromhex(head).decode('utf-8') tail = bytes.fromhex(tail).decode('utf-8') print(head + dna + tail) 到这里,一个quine就已经完成了。运行一下试试吧。 ## 推广到其他语言 这里就用C++举例好了: 先写出差不多的模板: #include <iostream> #include <string> void split(std::string input, std::string &first, std::string &second); std::string hex_decode(std::string hex); int main() { std::string dna = "🐱,🐍"; std::string head, tail; split(dna, head, tail); head = hex_decode(head); tail = hex_decode(tail); std::cout << head << dna << tail << std::endl; } // 麻烦的地方是C++标准库里面不提供分割和16进制解码函数 // 我也懒得写了,直接AI生成一下: void split(std::string input, std::string &first, std::string &second) { size_t commaPos = input.find(','); if (commaPos != std::string::npos) { first = input.substr(0, commaPos); second = input.substr(commaPos + 1); } else { first = input; second = ""; } } std::string hex_decode(std::string input) { std::string output; output.reserve(input.size() / 2); for (size_t i = 0; i < input.size(); i += 2) { std::string byteString = input.substr(i, 2); char byte = static_cast<char>(std::strtol(byteString.c_str(), nullptr, 16)); output.push_back(byte); } return output; } 然后把🐱前面的程序文本和🐍后面的程序文本用妙妙小工具编码成16进制并替换,得到: #include <iostream> #include <string> void split(std::string input, std::string &first, std::string &second); std::string hex_decode(std::string hex); int main() { std::string dna = "23696e636c756465203c696f73747265616d3e0a23696e636c756465203c737472696e673e0a0a766f69642073706c6974287374643a3a737472696e6720696e7075742c207374643a3a737472696e67202666697273742c207374643a3a737472696e6720267365636f6e64293b0a7374643a3a737472696e67206865785f6465636f6465287374643a3a737472696e6720686578293b0a0a696e74206d61696e2829207b0a20207374643a3a737472696e6720646e61203d2022,223b0a20200a20207374643a3a737472696e6720686561642c207461696c3b0a202073706c697428646e612c20686561642c207461696c293b0a202068656164203d206865785f6465636f64652868656164293b0a20207461696c203d206865785f6465636f6465287461696c293b0a20200a20207374643a3a636f7574203c3c2068656164203c3c20646e61203c3c207461696c203c3c207374643a3a656e646c3b0a7d0a0a2f2f20e9babbe783a6e79a84e59cb0e696b9e698af432b2be6a087e58786e5ba93e9878ce99da2e4b88de68f90e4be9be58886e589b2e5928c3136e8bf9be588b6e8a7a3e7a081e587bde695b00a2f2f20e68891e4b99fe68792e5be97e58699e4ba86efbc8ce79bb4e68ea54149e7949fe68890e4b880e4b88b3a0a0a766f69642073706c6974287374643a3a737472696e6720696e7075742c207374643a3a737472696e67202666697273742c207374643a3a737472696e6720267365636f6e6429207b0a2020202073697a655f7420636f6d6d61506f73203d20696e7075742e66696e6428272c27293b0a2020202069662028636f6d6d61506f7320213d207374643a3a737472696e673a3a6e706f7329207b0a20202020202020206669727374203d20696e7075742e73756273747228302c20636f6d6d61506f73293b0a20202020202020207365636f6e64203d20696e7075742e73756273747228636f6d6d61506f73202b2031293b0a202020207d20656c7365207b0a20202020202020206669727374203d20696e7075743b0a20202020202020207365636f6e64203d2022223b0a202020207d0a7d0a0a7374643a3a737472696e67206865785f6465636f6465287374643a3a737472696e6720696e70757429207b0a202020207374643a3a737472696e67206f75747075743b0a202020206f75747075742e7265736572766528696e7075742e73697a652829202f2032293b0a20202020666f72202873697a655f742069203d20303b2069203c20696e7075742e73697a6528293b2069202b3d203229207b0a20202020202020207374643a3a737472696e672062797465537472696e67203d20696e7075742e73756273747228692c2032293b0a2020202020202020636861722062797465203d207374617469635f636173743c636861723e287374643a3a737472746f6c2862797465537472696e672e635f73747228292c206e756c6c7074722c20313629293b0a20202020202020206f75747075742e707573685f6261636b2862797465293b0a202020207d0a2020202072657475726e206f75747075743b0a7d"; std::string head, tail; split(dna, head, tail); head = hex_decode(head); tail = hex_decode(tail); std::cout << head << dna << tail << std::endl; } // 麻烦的地方是C++标准库里面不提供分割和16进制解码函数 // 我也懒得写了,直接AI生成一下: void split(std::string input, std::string &first, std::string &second) { size_t commaPos = input.find(','); if (commaPos != std::string::npos) { first = input.substr(0, commaPos); second = input.substr(commaPos + 1); } else { first = input; second = ""; } } std::string hex_decode(std::string input) { std::string output; output.reserve(input.size() / 2); for (size_t i = 0; i < input.size(); i += 2) { std::string byteString = input.substr(i, 2); char byte = static_cast<char>(std::strtol(byteString.c_str(), nullptr, 16)); output.push_back(byte); } return output; } 把这个C++源代码文件保存为quine.cpp,然后可以用以下命令验证其输出结果是否和原来 的源代码一致: g++ quine.cpp && ./a.out | diff quine.cpp - ## 多语言的Quine 还有一种quine是这样的:A语言写出来的程序输出B语言的源代码,这段B语言的源代码又 输出一开始的A语言的代码,形成了一个A到B回到A的循环。这个过程甚至可以包括更多语 言,例如A到B到C到D到E再回到A。 GitHub上有个项目就构造了一个100多种语言的循环。 我们用了这种16进制编码的“笨方法”之后,因为不需要再考虑转义字符的问题了,无论多 少种语言都可以轻松构造。 接下来我们就把上面的Python和C++结合起来,构造一个C++程序输出Python程序,Python 程序运行又输出原来的C++程序的例子。 先写出Python版本的模板,还是和上面一样: dna = '🐱,🐍' head, tail = dna.split(',') head = bytes.fromhex(head).decode('utf-8') tail = bytes.fromhex(tail).decode('utf-8') print(head + dna + tail) 然后,写一个C++程序,输出上面的模板: #include <iostream> #include <string> std::string py = "dna = '🐱,🐍'\n" "head, tail = dna.split(',')\n" "head = bytes.fromhex(head).decode('utf-8')\n" "tail = bytes.fromhex(tail).decode('utf-8')\n" "print(head + dna + tail)\n"; int main() { std::cout << py; return 0; } 然后把这段C++代码中,🐱前面的程序文本和🐍后面的程序文本用妙妙小工具编码成16进制 并替换,得到: #include <iostream> #include <string> std::string py = "dna = '23696e636c756465203c696f73747265616d3e0a23696e636c756465203c737472696e673e0a0a7374643a3a737472696e67207079203d0a2020202022646e61203d2027,275c6e220a2020202022686561642c207461696c203d20646e612e73706c697428272c27295c6e220a202020202268656164203d2062797465732e66726f6d6865782868656164292e6465636f646528277574662d3827295c6e220a20202020227461696c203d2062797465732e66726f6d686578287461696c292e6465636f646528277574662d3827295c6e220a20202020227072696e742868656164202b20646e61202b207461696c295c6e223b0a0a696e74206d61696e2829207b0a202020207374643a3a636f7574203c3c2070793b0a2020202072657475726e20303b0a7d'\n" "head, tail = dna.split(',')\n" "head = bytes.fromhex(head).decode('utf-8')\n" "tail = bytes.fromhex(tail).decode('utf-8')\n" "print(head + dna + tail)\n"; int main() { std::cout << py; return 0; } 把这段代码保存为quine.cpp,并验证结果: g++ quine.cpp ./a.out > quine.py python3 quine.py > quine2.cpp diff quine.cpp quine2.cpp 完工。 -------------------------------------------------------------------- Email: i (at) mistivia (dot) com