1. 问题描述
马尔可夫链算法用于生成一段随机的英文,其思想非常简单。首先读入数据,然后将读入的数据分成前缀和后缀两部分,通过前缀来随机获取后缀,籍此产生一段可读的随机英文。为了说明方便,假设我们有如下一段话:
复制代码 代码如下:
Show your flowcharts and conceal your tables and I will be mystified. Show your tables and your flowcharts will be obvious.
假设前缀的长度为2,则我们处理输入以后得到如下数据,我们首先获取一个前缀,然后在前缀的后缀列表中随机选择一个单词,然后改变前缀,重复上述过程,这样,我们产生的句子将是可读的。
下面是处理过的数据:
复制代码 代码如下:
前缀 后缀
show your flowcharts tables
your flowcharts and will
flowcharts and conceal
flowcharts will be
your tables and and
will be mystified. obvious.
be mystified. show
be obvious. (end)
处理这个文本的马尔可夫链算法将首先带引show your,然后随机取出flowcharts 或者table 两个单词,假设选择的是flowcharts, 则新的前缀就是your flowcharts,同理,选择table 时,新的前缀就是your table,有了新的前缀your flowcharts 以后,再次随即选择它的后缀,这次是在and 和 will 中随机选择,重复上述过程,就能够产生一段可读的文本。具体描述如下:
复制代码 代码如下:
设置 w1 和 w2 为文本的前两个词
输出 w1 和 w2
循环:
随机地选出 w3,它是文本中 w1 w2 的后缀中的一个
打印 w3
把 w1 和 w2 分别换成 w2 和 w3
重复循环
2.awk 程序
马尔可夫链算法并不难,我们会在后面看到,用c语言来解决这个问题会相当麻烦,而用awk则只需要5分钟就能搞定。这简直就是一个演示awk优点的问题。awk 中有关联数组,正好可以用来表示前缀和后缀的关系。程序如下:
# markov.awk: markov chain algorithm for 2-word prefixes
BEGIN { MAXGEN = 10000; NONWORD = "\n"; w1 = w2 = NONWORD }
{ for (i = 1; i <= NF; i++) { # read all words
statetab[w1,w2,++nsuffix[w1,w2]] = $i
w1 = w2
w2 = $i
}
}
END {
statetab[w1,w2,++nsuffix[w1,w2]] = NONWORD # add tail
w1 = w2 = NONWORD
for (i = 0; i < MAXGEN; i++) { # generate
r = int(rand()*nsuffix[w1,w2]) + 1 # nsuffix >= 1
p = statetab[w1,w2,r]
if (p == NONWORD)
exit
print p
w1 = w2 # advance chain
w2 = p
}
}
3. C++ 程序
该问题的主要难点就在于通过前缀随机的获取后缀,在C++ 中,我们可以借助map 来实现前缀和后缀的对应关系,以此得到较高的开发效率。/* Copyright (C) 1999 Lucent Technologies */
/* Excerpted from 'The Practice of Programming' */
/* by Brian W. Kernighan and Rob Pike */
#include <time.h>
#include <iostream>
#include <string>
#include <deque>
#include <map>
#include <vector>
using namespace std;
const int NPREF = 2;
const char NONWORD[] = "\n"; // cannot appear as real line: we remove newlines
const int MAXGEN = 10000; // maximum words generated
typedef deque<string> Prefix;
map<Prefix, vector<string> > statetab; // prefix -> suffixes
void build(Prefix&, istream&);
void generate(int nwords);
void add(Prefix&, const string&);
// markov main: markov-chain random text generation
int main(void)
{
int nwords = MAXGEN;
Prefix prefix; // current input prefix
srand(time(NULL));
for (int i = 0; i < NPREF; i++)
add(prefix, NONWORD);
build(prefix, cin);
add(prefix, NONWORD);
generate(nwords);
return 0;
}
// build: read input words, build state table
void build(Prefix& prefix, istream& in)
{
string buf;
while (in >> buf)
add(prefix, buf);
}
// add: add word to suffix deque, update prefix
void add(Prefix& prefix, const string& s)
{
if (prefix.size() == NPREF) {
statetab[prefix].push_back(s);
prefix.pop_front();
}
prefix.push_back(s);
}
// generate: produce output, one word per line
void generate(int nwords)
{
Prefix prefix;
int i;
for (i = 0; i < NPREF; i++)
add(prefix, NONWORD);
for (i = 0; i < nwords; i++) {
vector<string>& suf = statetab[prefix];
const string& w = suf[rand() % suf.size()];
if (w == NONWORD)
break;
cout << w << "\n";
prefix.pop_front(); // advance
prefix.push_back(w);
}
}
4. c 程序
如果需要程序运行得足够快,那就只能用较低级的语言来实现了。当我们用c 语言来实现时,就不得不考虑各种各样的问题了。首先,面临的第一个问题就是,如何表示前缀和后缀的关系?这里采用前缀的key,后缀为value 的方式存储前缀与后缀的关系,我们知道,hash表的查找速度最快,所以,这里采用hash表也是情理之中的事,只是看你能不能想到,用前缀作key,基于上面的思路,再仔细一点,就没有什么大问题了。
/* Copyright (C) 1999 Lucent Technologies */
/* Excerpted from 'The Practice of Programming' */
/* by Brian W. Kernighan and Rob Pike */
/*
* Markov chain random text generator.
*/
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "eprintf.h"
enum {
NPREF = 2, /* number of prefix words */
NHASH = 4093, /* size of state hash table array */
MAXGEN = 10000 /* maximum words generated */
};
typedef struct State State;
typedef struct Suffix Suffix;
struct State { /* prefix + suffix list */
char *pref[NPREF]; /* prefix words */
Suffix *suf; /* list of suffixes */
State *next; /* next in hash table */
};
struct Suffix { /* list of suffixes */
char *word; /* suffix */
Suffix *next; /* next in list of suffixes */
};
State *lookup(char *prefix[], int create);
void build(char *prefix[], FILE*);
void generate(int nwords);
void add(char *prefix[], char *word);
State *statetab[NHASH]; /* hash table of states */
char NONWORD[] = "\n"; /* cannot appear as real word */
/* markov main: markov-chain random text generation */
int main(void)
{
int i, nwords = MAXGEN;
char *prefix[NPREF]; /* current input prefix */
int c;
long seed;
setprogname("markov");
seed = time(NULL);
srand(seed);
for (i = 0; i < NPREF; i++) /* set up initial prefix */
prefix[i] = NONWORD;
build(prefix, stdin);
add(prefix, NONWORD);
generate(nwords);
return 0;
}
const int MULTIPLIER = 31; /* for hash() */
/* hash: compute hash value for array of NPREF strings */
unsigned int hash(char *s[NPREF])
{
unsigned int h;
unsigned char *p;
int i;
h = 0;
for (i = 0; i < NPREF; i++)
for (p = (unsigned char *) s[i]; *p != '\0'; p++)
h = MULTIPLIER * h + *p;
return h % NHASH;
}
/* lookup: search for prefix; create if requested. */
/* returns pointer if present or created; NULL if not. */
/* creation doesn't strdup so strings mustn't change later. */
State* lookup(char *prefix[NPREF], int create)
{
int i, h;
State *sp;
h = hash(prefix);
for (sp = statetab[h]; sp != NULL; sp = sp->next) {
for (i = 0; i < NPREF; i++)
if (strcmp(prefix[i], sp->pref[i]) != 0)
break;
if (i == NPREF) /* found it */
return sp;
}
if (create) {
sp = (State *) emalloc(sizeof(State));
for (i = 0; i < NPREF; i++)
sp->pref[i] = prefix[i];
sp->suf = NULL;
sp->next = statetab[h];
statetab[h] = sp;
}
return sp;
}
/* addsuffix: add to state. suffix must not change later */
void addsuffix(State *sp, char *suffix)
{
Suffix *suf;
suf = (Suffix *) emalloc(sizeof(Suffix));
suf->word = suffix;
suf->next = sp->suf;
sp->suf = suf;
}
/* add: add word to suffix list, update prefix */
void add(char *prefix[NPREF], char *suffix)
{
State *sp;
sp = lookup(prefix, 1); /* create if not found */
addsuffix(sp, suffix);
/* move the words down the prefix */
memmove(prefix, prefix+1, (NPREF-1)*sizeof(prefix[0]));
prefix[NPREF-1] = suffix;
}
/* build: read input, build prefix table */
void build(char *prefix[NPREF], FILE *f)
{
char buf[100], fmt[10];
/* create a format string; %s could overflow buf */
sprintf(fmt, "%%%ds", sizeof(buf)-1);
while (fscanf(f, fmt, buf) != EOF)
add(prefix, estrdup(buf));
}
/* generate: produce output, one word per line */
void generate(int nwords)
{
State *sp;
Suffix *suf;
char *prefix[NPREF], *w;
int i, nmatch;
for (i = 0; i < NPREF; i++) /* reset initial prefix */
prefix[i] = NONWORD;
for (i = 0; i < nwords; i++) {
sp = lookup(prefix, 0);
if (sp == NULL)
eprintf("internal error: lookup failed");
nmatch = 0;
for (suf = sp->suf; suf != NULL; suf = suf->next)
if (rand() % ++nmatch == 0) /* prob = 1/nmatch */
w = suf->word;
if (nmatch == 0)
eprintf("internal error: no suffix %d %s", i, prefix[0]);
if (strcmp(w, NONWORD) == 0)
break;
printf("%s\n", w);
memmove(prefix, prefix+1, (NPREF-1)*sizeof(prefix[0]));
prefix[NPREF-1] = w;
}
}
http://www.thinkingyu.com/articles/Markov/
Lua程序设计第10章最后一节给出了一个利用马尔科夫链算法实现文本生成器的完整实例,第一次遇到这个算法有种不明觉厉的感觉,通过上网学习发现,这个算法不但实用并且原理很简单,当然这是排除了对算法本身数学推理方面的深究。(既然已经站在了巨人的肩膀上,那就让我多呆一会儿。)废话不多说,直接上代码。
-- read input line and split into words for return
function returnWords ()
local line = io.read()
local pos = 1
return function ()
while line do
local _start , _end = string.find(line , "%w+" , pos)
if(_start) then
pos = _end + 1
return string.sub(line , _start , _end)
else
line = io.read()
pos = 1
end
end
return nil
end
end
--generate a string in this style "w1 w2" (note: there is a blank between w1 and
--w2)
function genpairs(w1 , w2)
return (w1.." "..w2)
end
stattable = {} --a global var which stands for the vocabulary table
--insert (key ,values) in stattable
function insert(index , value)
if not stattable[index] then
stattable[index] = {value}
else
table.insert(stattable[index] , value)
end
end
--build stattable
function init()
local placeholers1 = "#"
local placeholers2 = "#"
for value in returnWords() do
key = genpairs(placeholers1 , placeholers2)
insert(key , value)
placeholers1 = placeholers2
placeholers2 = value
end
insert(genpairs(placeholers1 , placeholers2) , "#")
end
--return num of elements of a table
function getn(table)
local MAXNUM = 100
local count = 0
for num = 1 , MAXNUM do
if table[num] ~= nil then
count = count + 1
else
break;
end
end
return count
end
--generate text
local MAXWORDS = 100
local word1 = "#"
local word2 = "#"
init()
for i = 1 , MAXWORDS do
local textunit = stattable[genpairs(word1 , word2)]
local num = getn(textunit)
local xx = math.random(num)
local textword = textunit[xx]
if "#" ~= textword then
io.write(textword , " ")
word1 = word2
word2 = textword
else
io.write("\n")
break
end
end
我会一边按照程序实际的执行流程一边讲解算法的原理,程序第102行是此脚本真正执行的开始,首先执行init函数,init函数负责生成马尔科夫链的词汇表,这个词汇表就是通常所说的前缀后缀表。马尔科夫链算法会根据指定前缀随机地选择一个后缀用于生成随机文本。这里我们的前缀长度设置为2,算法已经指出前缀的长度对于算法的执行无影响,但是文本生成的随机性和质量和后缀的数目是紧密相连的。init函数首先定义两个占位符(受Boost库的影响),当给定一段文本,第一个单词作为后缀的对应前缀是“# #”,接着程序利用Lua的泛型for返回用户输入文本的每个单词,即词汇表键值对中的值,通过genpairs函数生成词汇表的键,通过insert函数将键值对插入词汇表。insert函数很简单,函数的输入有两个,一个是键,一个是值。如果词汇表中还没有输入的键,就生成一个键值对,如果已经存在输入的键,那么将输入的值插入到该键所对应的值列表的尾端。genpairs返回形式为“w1空格w2”的键,不多介绍。init函数会以“链式”的方式进行:用前两个单词作键,读入的单词作值,完成词汇表插入后,用上次作为键的第二个单词和上次读入的单词组成新键,新读入的单词作为值进行词汇表的插入操作,如此链式般的进行下去,直到输入文本解析完毕。
有了词汇表后,生成随机文本就一马平川了,我们按照下面的步骤生成随机文本,直到遇到自定义的结束符“#”。
设置w1和w2为文本的前两个词
Loop:
用w1空格w2查词汇表,随机地选择一个后缀作为输出文本,记为w3
如果w3是“#”
退出循环
w1 = w2 ,w2 = w3
这样就完成了利用马尔科夫链算法根据输入文本,按照其统计规律生成一个可读的新文本的所有步骤。
测试如下:
本文翻译自国外一篇文章:Generating Text Using a Markov Model
马尔可夫链是一个随机过程,在这个过程中,我们假设前一个或前几个状态对预测下一个状态起决定性作用。和抛硬币不同,这些事件之间不是相互独立的。通过一个例子更容易理解。
想象一下天气只能是下雨天或者晴天。也就是说,状态空间是雨天或者晴天。我们可以将马尔可夫模型表示为一个转移矩阵,矩阵的每一行代表一个状态,每一列代表该状态转移到另外一个状态的概率。

然而,通过这个状态转移示意图更容易理解。
换句话说,假如今天是晴天,那么有90%的概率明天也是晴天,有10%的概率明天是下雨天。
文章生成器
马尔可夫模型有个很酷的应用是一种语言模型,在这个模型中,我们根据当前的一个或几个词预测下一个词是什么。如果我们只是根据上一个词预测,则它是一个一阶马尔可夫模型。如果我们用上两个词预测,则它是一个二阶马尔可夫模型。
在我的实例中,我使用Henry Thoreau的小说Walden做训练。为了好做实验,我也加入了Nietszche的Thus Spoke Zarathustra,以及一些Obama的演讲。无论你训练什么样的文本,模型都会生成相似的结果,是不是很酷?
首先我们引入NLTK,它是Python中最好的NLP库。我想说,虽然我们这里做的自然语言处理很简单,但NLTK的内置函数还是帮我节省了很多代码。然后我们利用split()函数将字符串(从文本文件中获得的)转换成一个数组。
import nltk
file = open('Text/Walden.txt', 'r')
walden = file.read()
walden = walden.split()
下边两个函数是代码的基本函数。我们最终要使用的NLTK中的“条件频率字典”必须以成对数组作为输入,所以短语“Hi my name is Alex”需要变为[(“Hi”, “my”), (“my, “name”), (“name”, “is”), (“is”, “Alex”)]。函数makePairs以一个数组(以词分割字符串得到)作为输入,输出符合上边格式的数组。
生成文章的方法,需要一个条件频率分布作为输入。想想看,“farm”的后边每一个词出现的次数是多少?这是一个“条件频率分布”的输出(对于所有的词,而不只是“farm”)。生成函数的其余部分是根据训练数据中观察到的分布输出文本。我通过创建一个出现在当前词后边的每一个词组成的数组实现这一点。数组中也有正确的计数,因此,接下来我只需要随机选择数组中的一个词即可,而这个过程也是服从分布的。
def makePairs(arr):
pairs = []
for i in range(len(arr)):
if i < len(arr) - 1:
temp = (arr[i], arr[i + 1])
pairs.append(temp)
return pairs
def generate(cfd, word='the', num=50):
for i in range(num):
# make an array with the words shown by proper count
arr = []
for j in cfd[word]:
for k in range(cfd[word][j]):
arr.append(j)
print(word, end=' ')
# choose the word randomly from the conditional distribution
word = arr[int((len(arr)) * random.random())]
最后三行代码,我们输出了一些很像Walden风格的文本。
pairs = makePairs(walden)
cfd = nltk.ConditionalFreqDist(pairs)
generate(cfd)
最终的结果:
the quality in fact that I catch."—"What's your cheek bones. Each time, and philanthropist. How often that we say that it conforms to seat of Indian and perchance the love comfort and then the Indians subject of a male or shiner may be connected under a sense will be along its whole new enlightened countries. There was as I had selected were suent. The authority of, the pond, and sudden accident I shall I required, about the universe and the inlet there.
我建议你看一下我Github上的iPython笔记,因为我继续完成了一个方法。利用这个方法,你只需要输入一个文件名,它就能输出生成的文本。Obama的例子也非常的酷。
如果你想自己尝试一下,只需要创建一个文本文件,然后把它放在合适的目录即可。
执行程序输出的上面一行是用户输入的文本,下面一行则是程序自动生成的随机文本。
转自:https://www.cnblogs.com/zhuyp1015/archive/2012/06/18/2554088.html
转自:https://www.cnblogs.com/zhuyp1015/archive/2012/06/18/2554088.html
这里介绍的马尔科夫链算法实现的功能是:读入一段英文文本,构造出由这个文本中语言使用情况而形成的统计模型,然后根据统计模型随机输出另一段文本。
马尔科夫链算法的基本思想是:将输入想象成一些相互重叠的短语构成的序列,把每个短语分割为两个部分:一部分是由多个词构成的前缀,另一部分是只包含一个词的后缀。马尔科夫链算法能够生成输出短语的序列,其方法是依据原文本的统计性质,随机地选择跟在前缀后面的特定后缀。采用三个词的短语就能够很好工作,这三个词中前两个词构成前缀来选择作为后缀的一个词设置:
w1和w2为文本的前两个词
输出w1和w2
循环:
随机地选出w3,它是文本中w1w2的后缀中的一个
打印w3
把w1和w2分别换成w2和w3
重复循环
选择二词前缀,则每个输出词w3都是根据它前面的一对词(w1,w2)得到的。前缀中词的个数对设计本身并没有影响,程序应该能对付任意的前缀长度。我们把一个前缀和它所有可能后缀的集合放在一起,称其为一个状态。
为了说明问题,假设我们要基于本章开头的引语里的几个句子生成一些随机文本。这里采用
的是两词前缀:
Show your flowchars and conceal your tables and I will be mystified. Show your tables and your flowcharts will be obvious. (end)
下面是一些输入的词对和跟随它们之后的词:
输入前缀 跟随的后缀
Show your flowcharts tables
your flowcharts and will
flowcharts and conceal
flowcharts will be
your tables and and
will be mystified. obvious.
be mystified Show
be obvious (end)
处理这个文本的马尔可夫算法将首先打印出 Show your,然后随机取出flowcharts或table。如果选中了前者,那么现在前缀就变成 your flowcharts,而下一个词应该是and或will。如果它选取 tables,下一个词就应该是 and。这样继续下去,直到产生出足够多的输出,或者在找后缀时遇到了结束标志。
#include <iostream>
#include <map>
#include <deque>
#include <vector>
#include <string>
#include <cstdio>
#include <sstream>
#include <ctime>
using namespace std;
enum {
NPREF = 2 , //前缀的个数
MAXGEN = 10000 //最多有那么多个词
};
typedef deque<string> Prefix;
map<Prefix,vector<string> > statetab; // 使用duque 作为第一项方便前缀词数扩展和改变,这里statetab将前缀和后缀关联起来
char NONWORD[] = "\n"; //伪前缀,将真正的输入作为它的后缀
void add(Prefix &prefix, const string &s)
{
if(prefix.size() == NPREF) //当前缀数目达到指定词数之后,改变前缀,也就是w1 w2 变成 w2 w3
{
statetab[prefix].push_back(s);
prefix.pop_front();
}
prefix.push_back(s);
}
void build(Prefix &prefix, istream &in)//构造统计表
{
string buf;
while(in >> buf)
add(prefix,buf);
}
void generate(int nwords)
{
Prefix prefix;
int i;
srand(time(NULL));
for(i = 0; i < NPREF; i++)
add(prefix,NONWORD);//生成伪前缀
//cout<<"NOW prefix: "<<prefix[0]<<" "<<prefix[1]<<endl;
for(i = 0; i < nwords; i++)
{
vector<string> & suf = statetab[prefix];
//cout<<"prefix:"<<prefix[0]<<" suf "<<suf[0]<<endl;
const string &w = suf[rand() % suf.size()];//在所有可能的后缀中随机的选择一个词
if(w == NONWORD)
break;
cout << w <<" ";
prefix.pop_front();//改变前缀
prefix.push_back(w);
}
cout<<endl;
}
int main()
{
int nwords = MAXGEN;
Prefix prefix;
string sentence = "Show your flowchars and conceal your tables and I will be mystified. Show your tables and your flowcharts will be obvious. (end)";
istringstream in(sentence);
//freopen("mkov.txt","r",stdin);
for(int i = 0; i < NPREF; i++)
add(prefix,NONWORD);
build(prefix,in);
add(prefix,NONWORD);
generate(nwords);
return 0;
}
马尔可夫链是一个随机过程,在这个过程中,我们假设前一个或前几个状态对预测下一个状态起决定性作用。和抛硬币不同,这些事件之间不是相互独立的。通过一个例子更容易理解。
想象一下天气只能是下雨天或者晴天。也就是说,状态空间是雨天或者晴天。我们可以将马尔可夫模型表示为一个转移矩阵,矩阵的每一行代表一个状态,每一列代表该状态转移到另外一个状态的概率。

然而,通过这个状态转移示意图更容易理解。
换句话说,假如今天是晴天,那么有90%的概率明天也是晴天,有10%的概率明天是下雨天。
文章生成器
马尔可夫模型有个很酷的应用是一种语言模型,在这个模型中,我们根据当前的一个或几个词预测下一个词是什么。如果我们只是根据上一个词预测,则它是一个一阶马尔可夫模型。如果我们用上两个词预测,则它是一个二阶马尔可夫模型。
在我的实例中,我使用Henry Thoreau的小说Walden做训练。为了好做实验,我也加入了Nietszche的Thus Spoke Zarathustra,以及一些Obama的演讲。无论你训练什么样的文本,模型都会生成相似的结果,是不是很酷?
首先我们引入NLTK,它是Python中最好的NLP库。我想说,虽然我们这里做的自然语言处理很简单,但NLTK的内置函数还是帮我节省了很多代码。然后我们利用split()函数将字符串(从文本文件中获得的)转换成一个数组。
import nltk
file = open('Text/Walden.txt', 'r')
walden = file.read()
walden = walden.split()
下边两个函数是代码的基本函数。我们最终要使用的NLTK中的“条件频率字典”必须以成对数组作为输入,所以短语“Hi my name is Alex”需要变为[(“Hi”, “my”), (“my, “name”), (“name”, “is”), (“is”, “Alex”)]。函数makePairs以一个数组(以词分割字符串得到)作为输入,输出符合上边格式的数组。
生成文章的方法,需要一个条件频率分布作为输入。想想看,“farm”的后边每一个词出现的次数是多少?这是一个“条件频率分布”的输出(对于所有的词,而不只是“farm”)。生成函数的其余部分是根据训练数据中观察到的分布输出文本。我通过创建一个出现在当前词后边的每一个词组成的数组实现这一点。数组中也有正确的计数,因此,接下来我只需要随机选择数组中的一个词即可,而这个过程也是服从分布的。
def makePairs(arr):
pairs = []
for i in range(len(arr)):
if i < len(arr) - 1:
temp = (arr[i], arr[i + 1])
pairs.append(temp)
return pairs
def generate(cfd, word='the', num=50):
for i in range(num):
# make an array with the words shown by proper count
arr = []
for j in cfd[word]:
for k in range(cfd[word][j]):
arr.append(j)
print(word, end=' ')
# choose the word randomly from the conditional distribution
word = arr[int((len(arr)) * random.random())]
最后三行代码,我们输出了一些很像Walden风格的文本。
pairs = makePairs(walden)
cfd = nltk.ConditionalFreqDist(pairs)
generate(cfd)
最终的结果:
the quality in fact that I catch."—"What's your cheek bones. Each time, and philanthropist. How often that we say that it conforms to seat of Indian and perchance the love comfort and then the Indians subject of a male or shiner may be connected under a sense will be along its whole new enlightened countries. There was as I had selected were suent. The authority of, the pond, and sudden accident I shall I required, about the universe and the inlet there.
我建议你看一下我Github上的iPython笔记,因为我继续完成了一个方法。利用这个方法,你只需要输入一个文件名,它就能输出生成的文本。Obama的例子也非常的酷。
如果你想自己尝试一下,只需要创建一个文本文件,然后把它放在合适的目录即可。
同时我也把代码和文档放到我自己的Github:xianhu/LearnPython
这里还有一个:代码仓库
😍😍😍
没有评论:
发表评论