[C++]狗屁不通文章生成器

[C++]狗屁不通文章生成器
1 概況
由用戶輸入啟動詞,根據語料庫中統計的詞語前后綴關系,自動生成一片新的文章。
比如:春天來了,大地媽媽穿上了碧綠的衣裳。嫩綠的小草從地下探出頭來,陶醉在美麗的春天里。
前后綴關系:[前綴,后綴]。上面這段話的前后綴關系有:[春天,來/里],[天來,了],[天里,。],[來了,,]等。
說明:
- 啟動詞:用戶輸入的一個詞,由這個詞開始生成文章所有內容。
- 前/后綴:一個詞前后連續的n個字符。比如前綴為“春天”,由例句中得到后綴可為“來”或“里”,即表示一種語言的前后關系。
2 基本要求
- 準備語料庫:準備相關文章,存為文件。利用程序讀取文章內容,獲取文章語句中詞語的前后關系,即語料庫。語料庫的豐富程度由文章的數量決定,語料庫又決定程序運行的時間和生成的文章質量。
- 構建前后綴關系:根據語料庫,依據設定的前后綴長度,構建字詞的前后綴關系。
- 生成文章:用戶輸入啟動詞,根據啟動詞為前綴生成后綴得到文段,再根據文段生成新的前綴,以新前綴生成新后綴以此類推,得到一片文章。
- 應盡量避免循環:有時語料庫中可能出現類似“為所欲為”的接龍結構,造成死循環,同時生成的內容也沒有了意義。
- 輸入輸出形式:
//輸入:
2 //前綴詞長度
2 //后綴詞長度
春天 //啟動詞
//輸出:
[一篇文章]
3 程序分析
3.1 文件流讀寫
- 讀文件:從多篇文章中讀取文件內容為字符串,以前/后綴的長度遍歷獲取前/后綴,并建立前后綴關系。
- 寫文件:為觀察程序執行情況,將前/后綴字符對和生成的文章寫入文件。
3.2 建立前后綴關系
當需要在兩種數據間建立一種關系時,可以使用結構化數據進行存儲,比如建立前后綴關系可以采用字典類結構進行存儲,C++也有相應的頭文件。
除此之外,還可以將其抽象為一種類,可以定制類的行為和結構。這里選擇自建一個類進行存儲。
類設計:
- 一個類記錄一個前綴和它的所有后綴,
- 記錄后綴的出現次數
- 記錄后綴的個數
class wordpair{
private:
char **suffix //后綴字符串數組,一個后綴存為一個字符串
int *freq //一個后綴出現的次數
public:
char *prefix //前綴字符串,聲明為公共成員,方便外部查找
int length //記錄后綴的個數
}
3.3 字符串切片
在C++的頭文件中,可以使用string類型代替char類型的字符串,而且對于字符串的操作也更方便。比如
string str1 = "abcd";
string str2 = "efghijk";
string str3 = str1 + str2; // = “abcdefghijk”; 字符串拼接
string str4 = str3.substr(3,2); // = "de"; 從下標為3的字符開始,截取長度為2的子字符串
以前一直覺得C語言和C++處理字符串很麻煩,現在倒覺得還是很方便的。
3.4 變長數組
C語言和C++中,基礎的數組長度相對固定,但也不是不能改變,只是相對麻煩一些。在這個程序中,一個wordpair只存儲一個前綴和它的后綴們,所以需要創建一個wordpair類型的列表來存儲全部的前后綴。
我用的方法是來回地復制
int main(){
int len;
int list[len]; //假設有一個長度為len的整型數組
int p[len+1]; //開辟一個len+1個整型長度的空間
// 把 list 復制到 p,然后
list = new int[len+1]; //將list長度+1
//把 p 復制過來
}
4 代碼實現
4.1 函數:數組加長
由于數組加長在程序中多次調用,且需要增長的數組各不相同,所以在這里我定義了一個函數模板,以盡可能少的代碼完成相同的任務。
template<class T>
T *append2list(T *list, T t, int len){
T copy[len + 1]; // 用于備份的空間
for(int i=0; i<len; i++){
copy[i] = list[i];}
copy[len] = t; //在末尾增加元素
len++;
list = new T[len]; //變長
for(int i=0; i<len; i++){ //拷貝回來
list[i] = copy[i];}
return list;
}
4.2 類wordpair定義
首先是類成員,應該有:
class Wordpair
{
private:
string *suffix; //后綴列表,一個前綴可能對應多個后綴
int *freq; // 整型數組,依此記錄后綴的頻率
public:
string prefix; // 前綴
int length; // 記錄長度 , 一個前綴對應size個后綴
Wordpair(string prefix, string suffix){ //構造函數
this->prefix = prefix;
this->suffix = new string[1];
this->suffix[0] = suffix;
this->freq = new int[1];
this->freq[0] = 1;
this->length = 1;
}
Wordpair(){ //構造函數
this->prefix = "",
this->suffix = new string[1];
this->suffix[0] = "";
this->freq = new int[1];
this->freq[0] = 0;
this->length = 0;
}
/* 判斷這個后綴是否已經有記錄,有返回下標,沒有則返回-1 */
int hasRecorded(string word){}
/* 添加一個后綴 */
bool push(string word){}
/* 找出出現次數最多的后綴的下標,采用更可信的后綴 */
string maxFrequency(){}
/* 轉化為字符串,方便輸出 */
string to_String()const{}
/* 重載賦值運算符,方便與其他類型的列表共用函數 */
Wordpair& operator=(Wordpair &pair){}
};
除此之外,還可以重載輸出運算符<<,便于調試時在函數中輸出wordpair值:
ostream& operator<<(ostream& out, const Wordpair& w){
out<<w.to_String();
return out;
}
4.3 函數:讀取文件
程序運行時,需要讀取文件為字符串,當文件較多時把這個功能抽象出來,調用很方便。
// 讀文件
string getfile(char *path){
string alticle = ""; //初始化字符串
ifstream fin(path, ios::in); //打開文件
if(!fin.is_open()){
cout<<"文件讀取錯誤!"<<endl;
return NULL;
}
string buffer;
while(getline(fin,buffer)){ //讀取
alticle.append(buffer); //新的行添加到alticle尾部
}
fin.close();
return alticle;
}
4.4 函數:寫入文件
主要是寫入生成的字符對,方便調試
// 寫文件,記錄詞組對
void exportData(Wordpair *pairlist, int len, int prelen, int suflen){
char path[32];
sprintf(path,"./word-pairs(%dx%d).txt",prelen,suflen);
ofstream fout(path, ios::out);
for(int i=0; i<len; i++){
fout<<pairlist[i]; //在這里就體現了重載<<運算符的好處
}
fout.close();
cout<<"詞組對已經寫入文件< "<<path<<" >"<<endl;
}
4.5 核心函數:字符串分割
讀取到文件后,將字符串從下標0開始,讀取前綴+后綴的長度,然后從1開始讀取前綴+后綴的長度。循環的次數應該是字符串總長度 - (前綴長度+后綴長度 -1),以保證下標不會溢出。
Wordpair *alticle2Wordpair(Wordpair *pairlist, int &length,string alticle, int prefix_len, int suffix_len){
for(int i=0; i<alticle.length()/2-prefix_len-suffix_len+1; i++){
bool hasrecord = false;
string prefix=alticle.substr(i*2,prefix_len*2); // i為什么要×2?因為在devcpp中發現一個中文字符相當于兩個英文字符,不乘2會亂碼。
string suffix=alticle.substr((i+prefix_len)*2,suffix_len*2);
for(int j=0;j<length;j++){
if(pairlist[j].prefix == prefix){ // 如果已經有了這個前綴,則添加后綴
pairlist[j].push(suffix);
hasrecord = true;
break;
}
}
if(!hasrecord){ // 沒有這個前綴則詞組對列表長度增加
Wordpair pair(prefix, suffix);
pairlist = append2list(pairlist, pair, length);
length++;
}
}
return pairlist;
}
在此基礎上,對每次讀文件都進行一次,就能獲取全部文件的字符對。
4.6 核心函數:文章拼接
得到語料庫之后,需要根據語料庫拼接出文章。我這里采用的方法有點問題,當完全防止出現循環文本的時候,文章過短,當放開一點對循環文本的時候,循環文本總是出現,算法上想不通。希望有大佬提供一點思路。
// 判斷前綴是否在列表內,有則返回下標,沒有則返回-1
int hasrecord(Wordpair *pairlist, int len, string preword){
for(int i=0; i<len; i++){
if(preword == pairlist[i].prefix){
return i;
}
}
return -1;
}
// 拼接文章
void createAlticle(Wordpair *pairlist, int len, string startword, int prefix_len, int suffix_len){
string preword = startword;
int i=0;
int index = hasrecord(pairlist, len, preword);
string alticle = preword;
int alticle_len = prefix_len; //長度(中文字符標準)
while(index != -1){
string newword = pairlist[index].maxFrequency();
// 避免循環
if(alticle.find(newword)==string::npos //表示這個前綴沒有在文章中出現過
// || alticle_len - alticle.rfind(newword) > 600 //表示相同的詞之間最少間隔多少。加上這個條件后有循環,注釋后文章顯著變短
){
alticle.append(pairlist[index].maxFrequency());
alticle_len += suffix_len;
preword = alticle.substr((alticle_len-prefix_len)*2, alticle_len*2);}
else{
preword = pairlist[index+1].maxFrequency();
}
index = hasrecord(pairlist, len, preword);
}
cout<<alticle<<endl;
ofstream fout(CREATE_ALTICLE, ios::out);
fout<<alticle;
fout.close();
cout<<"文章寫入文件 < "<<CREATE_ALTICLE<<" >"<<endl;
}
5 總結
在沒有機器學習的支持下,這種拼接的文章十分粗糙,無法準確的給出最優的選擇。另一方面,代碼寫的過于拖沓,算法有待優化。
浙公網安備 33010602011771號