[C++练手项目] DocAnalysis
[C++练手项目] DocAnalysis
文章目录
- [C++练手项目] DocAnalysis
- 任务分析
- 编码
- 转换DOC文件到TXT
- 分词
- 停止词
- 两文档相同和不同字符数统计
- 总体实现思路
- 一、分词字典和停止词字典处理
- 二、读入需要统计的两个`.txt`文档,并进行分析
- 输出结果展示
- 最终工程文件结构
- 问题与不足
- 本项目是本人学习Cpp的练手项目,全部代码工程文件已上传github,可自取:DocAnalysis: C++练手项目——DocAnalysis
如果上面链接内容无法访问,请通过百度网盘下载:
链接:https://pan.baidu.com/s/1L2ckjQqorlKphTIRKUyHWA?pwd=x2z7
提取码:x2z7
- 本文综合了CPP中较多的知识点,可点击以下基础知识点总结链接帮助学习!
- 面向对象的编程——类class
- STL库的使用————暂时还未更新
- template泛型编程思想
- 文件读写操作
- 本项目优点:对于百万级别的文本(如:资本论 163万字)运行耗时仅1.671s
实现本项目,你将收获红黑树std::map实现字典树和优先队列std::priority_queue的使用、文件的操作、泛型编程思想。
任务分析
本项目使用C++
语言编写,在Windows
系统下,使用VScode
编译运行。
输入:两个
.docx
扩展名类型的文件输出:
- 统计并输出两个文档多少字符相同,多少字符不同
- 统计并输出每个文档的前十高频字或词
第一个任务比较简单,我们只需要记录第一个文档中出现的字符,再和第二个文档进行比对即可。需要特关注的是记录字符所使用的数据结构和匹配字符时所使用的算法,以此降低时间复杂度,使较大的文件可以在短时间内被有效处理。
第二个任务相对复杂,对于一篇汉语
文档,如果只是统计字出现的频率,我们并不能像对一篇英语文档那样通过标点和空格识别出词语,这就需要对文章进行分词,并采用相应的数据结构存储出现频率前十的字、词。
同时,在处理一篇汉语文档时,必须要注意到文件的编码。
编码
目前比较常用的中文编码是UTF-8
和GBK
UTF-8
编码,在Linux
系统下很常见,采用1~4的变长字节,字符集全,是当下各领域比较通用、主流的编码;GBK
编码,是Windows
系统下的默认编码,Windows
下的文件路径、控制台等都默认采用这一编码,采用1~2的变长字节,涵盖绝大部分汉字。
综合考量后,我选择了GBK
作为本项目的统一编码。本项目的所有代码源文件、txt
文件均采用这一编码标准。选择选择这一编码的原因主要有以下几点:
- 输入和输出相较
UTF-8
编码方便。读入一个字符,判断最高位为1时(表示该字符为非ASCII
字符),我们只需要再读入一个字符便得到了该汉字的编码;而对于UTF-8
编码,我们需要统计高位在0出现之前连续1的个数,以此决定再读入几个字符得到这个汉字的编码,输出同理; - 节省编码空间。无论是
x86
还是x64
,MSVC
编译器对wchar_t
类型的定义都是2字节的unsigned short
类型,GBK
最多只有两个字节,可以方便地用wchar_t
类型表示,而UTF-8
则需要4个字节的unsigned int
类型表示,这会让程序所占内存空间提高一倍; - 由于文件路径在
Windows
环境下采用GBK
编码表示,如果我们统一采用UTF-8
编码,当文件路径中有中文时可能会出现BUG,而我们直接采用GBK
编码就可以避免这一问题。
在读入GBK
字符时,核心操作如下:
wchar_t WInFile::convertUCharToWChar(unsigned char c)
{wchar_t wc = 0;if (c & 0x80) //首位为1{// static_cast为强制类型转换wc |= static_cast<wchar_t>(c) << 8; // 将前一个字节也保存下来c = inFile->get(); // 再读入一个字符作为完整汉字wc |= static_cast<wchar_t>(c);}else{wc = static_cast<wchar_t>(c) << 8;}return wc;
}
这个函数完成了将一个unsigned char
类型字符转换为wchar_t
类型字符,需要注意的是,char
类型是有符号的,最高位为符号位,因此最高位不能直接进行诸如移位、按位于、按位或等位运算,需要使用unsigned char
类型。
转换DOC文件到TXT
讨论完编码问题后,接踵而至的问题便是如何使用C++
分析DOC
文件。DOC
文件的文件格式很复杂,对于.docx
扩展名类型的文件,我决定先将DOC
文件转换为TXT
文件,然后再进行分析。
转换方式如下:
打开word文件 →\rightarrow→ 点击上工具栏中的“文件” →\rightarrow→ “导出” →\rightarrow→ “更改文件类型” →\rightarrow→ “纯文本(*.txt)”
这样即可将DOC
文件转换为TXT
文件,然后进入下一步的分析。
分词
查阅相关资料,现在主要有以下几种分词方法:
- 字符匹配,也叫做机械分词法,将待分析的汉字串与一个“充分大的”机器词典中的词条进行匹配;
- 理解法,通过让计算机模拟人对句子的理解,达到识别词的效果,涉及句法、语义分析;
- 统计法,涉及机器学习。
为了减小工程量,我挑选了其中最容易实现的机械分词法。
首先,我需要一个足够大的词典作为支撑。在github上我找到了《现代汉语词典(第七版)》XDHYCD7th,通过简单的字符串处理,得到了一个只含词头的词典文件,在本项目中的位置是DocAnalysis\dict\dict.txt
,该文件的每一行都是一个词。
接着该如何进行分词,自然想到利用串的模式匹配把词典中每个词逐个与目标字符串作匹配。但是,在计算了时间复杂度后,我发现事情并没有这么简单。即使我们采用KMP
算法,对于单个词条匹配的时间复杂度是O(n + m)
,n是文档的字数,m是单词的字数,由于词典中大部分都是单双音节词,m是远小于n的,单次匹配的时间复杂度可以近似看成O(n)
。词典中的词条的数量级为1e6
,所以做完所有匹配的时间复杂度是O(1e6 * n)
。考虑到一般文章的长度从几百字到几万字不等,我们这里假设n
的数量级为1e5
,所得时间复杂度达到了O(1e11)
,而现代计算机1秒可以处理的时间复杂度数量级一半在O(1e7)~O(1e8)
,显然,采用直接模式匹配的方法并不可行。
那么,问题出在哪里了呢?试想,对于文档字符串中的字符“一”,从这个字符开始,它只用匹配“一”开头的所有词就行了,而在上述算法中它和非“一”开头的词进行了大量无用的匹配。那么如何避免这种无用的匹配呢?自然想到使用Tire树
(字典树)。
通常一个英文Tire树
节点的定义如下:
struct TrieTreeNode
{char c; // 当前节点所存字符bool isEnd; // 是否是单词的结尾TrieTreeNode *next[26]; // next[c]表示下个字符的地址
};
根据需要,如果区分大小写,可能指针数组next
要开到52的大小。然而,这对于汉字来说仍然远远不够。对于GBK
编码的汉字,我使用两字节的wchar_t
来存储,难道我们要开一个2162^{16}216长度的next
数组吗?这样的解决策略显然不行,后面的结点用不了这么大的空间,我们也没有这么充裕的空间,而且即便使用变长数组,考虑到查询的时间复杂度,也不是一种高效的解决方案。如果既要求next
大小动态变化,又要求能够在较低的时间复杂度内完成查询,自然想到采用红黑树来存储next
。这样,插入和查找都可以在log
的时间复杂度下完成,且可以动态变化。分析至此,我们的树节点应定义如下:
template<typename T>
struct TrieTreeNode
{T c; // 当前节点所存字符bool isEnd; // 是否是单词的结尾std::map<T, TrieTreeNode<T>*> next; // next[c]表示下个字符对应树节点的地址int freq; // 单词的频数TrieTreeNode(T c_, bool isEnd_) : c(c_), isEnd(isEnd_), freq(0) {};
};
c++
的map
类型采用红黑树实现(左侧是索引,右侧是键值),为了方便以后在别的项目中使用,这里并没有指定字符c
的类型为wchar_t
,而是采用了C++
的模板template
完成对这一数据结构的定义。
由于词典中的字大约在1e5数量级,分支最多的根节点一次查询的时间复杂度为O(log(1e5))
,而词典中的词大多为2~4字,最多不超过10字,且其后节点分支的数量级一般在100左右,这里可以近似将从某个字符开始在字典树中匹配的时间复杂度视为O(k)
,k的数量级为O(log(1e5))
,文档的字数是n,总时间复杂度为O(k * n)
,这个时间复杂度我们可以在1s左右处理百万字级别的文章。
停止词
统计词频之后,我们会发现,诸如“你”、“我”、“这个”、“或者”、“然后”等对理解文章内容没有意义的词大量出现,这些词称为停止词(stopword),统计词频时需要将这类词去掉。在github上的stopwords仓库中有已整理好的停止词,我选择了百度的停止词典,在本项目中的位置是DocAnalysis\dict\baidu_stopwords.txt
。程序开始,我们不止给分词词典建立字典树,同样给停止词典建立一个字典树,在统计完词频输出结果时,如果对应词条可以与停止词典字典树中的词条匹配,那么就忽略这一词条。
两文档相同和不同字符数统计
统计字符数时,需要简历一张表,可以根据汉字找到其出现的次数,有以下三种方式实现:
- 直接使用静态的顺序表,以汉字字符的编码直接作为下标进行索引,这样我们需要建立一个包含13万左右元素的
int
类型的数组,使用起来比较方便,但是有大量空间未利用。 - 使用哈希表建立起汉字编码 →\rightarrow→ 数组下标的映射关系,我们知道
GBK
的汉字编码绝大多数集中在一个区间内,只需找到合适的哈希映射函数就可以节省大量空间。 - 使用
C++
标准库提供的std::map
(红黑树)存储汉字编码(索引) →\rightarrow→ 汉字频数(键值)这个关系。
为了节省代码运行空间,这里我采用了第三种方法来存储汉字对应的字词频。
统计字频时,我还进行了另外的过滤操作,我认为统计像标点符号、换行符、空格符这样的字符没有意义,因此统计时将这些字符排除在外:
bool DocAnalysis::isWCharValid(wchar_t wc) // 判断是否为有效字符
{if (!(wc & 0x00ff)) // 不是汉字{char c = wc >> 8;// 下面的判断表示wc是除字母、数字外的无效字符if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))){return false;}}return true;
}
总体实现思路
此处没有完整代码,只讲解细节思路,完整代码见:DocAnalysis: C++练手项目——DocAnalysis
github可能不方便访问,建议通过百度网盘下载源码
链接:https://pan.baidu.com/s/1L2ckjQqorlKphTIRKUyHWA?pwd=x2z7
提取码:x2z7
一、分词字典和停止词字典处理
- 读入分词字典和停止词字典内容
// DocAnalysis.cpp文件中const char *DocAnalysis::DICT_DIR = "dict"; //存放停词词典和分词词典的位置stopWordsFile = new WInFile((static_cast<std::string>(DICT_DIR) + "/baidu_stopwords.txt").c_str());dictFile = new WInFile((static_cast<std::string>(DICT_DIR) + "/dict.txt").c_str());// WinFile.cpp文件中,读入文件的操作WInFile::WInFile(const char *fileName){inFile = new std::ifstream(fileName);}
- 将分词字典和停止词字典读入内容通过
std::map
(红黑树)存储,每个存储节点也都是一个红黑树。每个节点记录的信息有:
- 当前记录字符(
w_char
类型) - 当前是否是该词语的结尾(
bool
类型) - 当前记录字符的下一个字符地址(
std::map
类型) - 单词频数(只在当前字符是所记录单词的结尾时记录)(
int
类型)
由此可以得到红黑树节点的代码,采用结构体实现(由于结构体默认public类型,为了封装函数,所以使用namespace将TrieTreeNode结构体对外隐藏)
namespace // TrieTreeNode结构体对外隐藏
{template <typename T>struct TrieTreeNode{T c; // 当前节点所存字符bool isEnd; // 是否是单词的结尾std::map<T, TrieTreeNode<T> *> next; // next[c]表示下个字符的地址int freq; // 单词的频数TrieTreeNode(T c_, bool isEnd_) : c(c_), isEnd(isEnd_), freq(0){}; // 初始化};
}
这里给出一张简略版存放示意图
以此我们建立字典树,详细内容请参考源码
// TrieTree.hpp文件中
template <typename T>
class TrieTree
{public:// qtype为频数表的类型(优先队列),用于之后输出统计字、词频结构typedef std::priority_queue<std::pair<int, std::vector<T>>> qtype;private:TrieTreeNode<T> *root;std::vector<T> currWord; //用于getFreqTable函数, 存储当前词void clearFreq(TrieTreeNode<T> *currNode); // 清楚字典树上各节点出现的频率值void destroyTrieTreeNode(TrieTreeNode<T> *currNode); // 删除字典树void getFreqTable(qtype &q, TrieTreeNode<T> *currNode); // 统计字、词出现频率public:TrieTree();~TrieTree();void insert(const T *s, int len); // 插入节点// 以下两个find配合使用,在匹配停止词字典时使用,后续再讲解bool find(std::vector<T> vecs); bool find(const T *s, int len);void match(const T *s, int len); // s为整个输入文档的全部读入字符,用s中字符匹配TrieTree中的单词void getFreqTable(qtype &q)getFreqTable(q, root);void clearFreq()clearFreq(root);
};
// DocAnalysis.cpp文件中
stopWordsTrieTree = new TrieTree<wchar_t>;
buildTrieTreeFromFile(stopWordsFile, stopWordsTrieTree);
std::cout << "已加载停止词库" << std::endl;dictTrieTree = new TrieTree<wchar_t>;
buildTrieTreeFromFile(dictFile, dictTrieTree);
std::cout << "已加载分词词典" << std::endlvoid DocAnalysis::buildTrieTreeFromFile(WInFile *trieTreeWInFile, TrieTree<wchar_t> *myTrieTree)
{bool flag = true;do{std::wstring s;flag = trieTreeWInFile->readLine(s); // 分词和停止词字典每一个词都是按换行分割的myTrieTree->insert(s.c_str(), s.size()); // 去除掉换行符} while (flag);
}
- 一些函数的解释
trieTreeWInFile->readLine(s)
按行读入位于dict目录下的分词和停止词词典,每一行就是一个词
static const unsigned char UNSIGNED_EOF = 0xff; // 0xff为EOF的unsinged形式
bool WInFile::readLine(std::wstring &s)
{unsigned char c = inFile->get();if (c == UNSIGNED_EOF){return false;}while (c != static_cast<unsigned char>('\n')){wchar_t wc = convertUCharToWChar(c);s += wc;c = inFile->get();}return true;
}
myTrieTree->insert(s.c_str(), s.size())
将词组s插入字典树
void insert(const T *s, int len)
{if (len == 0){return;}TrieTreeNode<T> *currNode = root;for (int i = 0; i < len; i++){bool isEnd = i == len - 1 ? true : false;auto iter = currNode->next.find(s[i]);if (iter == currNode->next.end()){TrieTreeNode<T> *tmpNode = new TrieTreeNode<T>(s[i], isEnd);currNode->next[s[i]] = tmpNode;currNode = tmpNode;}else{currNode = iter->second;}}
}
插入过程可看下图理解
最终生成完整的分词和停止词字典树
二、读入需要统计的两个.txt
文档,并进行分析
- 统计两个文档中相同的字符个数和不同的字符个数
- 首先定义记录变量(在
DocAnalysis.h
文件中)
std::map<wchar_t, int> charFreq; // 字符出现次数(字符,出现次数)
int sameCharNum = 0; // 相同字符个数
int totCharNum = 0; // 总字符个数
- 然后统计两文档中相同字符和不同字符个数
此处先使用文件A创建一个字典树,节点为(字符,出现频数),再使用文件B进行匹配。
void DocAnalysis::getCharFreq(std::wstring &myDoc) //统计相同字符
{if (!charFreq.size()) // size为0表示正在分析第一个文件{for (auto wc : myDoc){if (!isWCharValid(wc)) // 字符不合法,跳过{continue;}totCharNum++;charFreq[wc]++;}}else{for (auto wc : myDoc){if (!isWCharValid(wc)){continue;}totCharNum++;auto iter = charFreq.find(wc);if (iter != charFreq.end() && iter->second){sameCharNum++;iter->second--; // 每个字符只对应一次}}}
}std::cout << "相同字符有 " << sameCharNum << " 个" << std::endl;
std::cout << "不同字符有 " << totCharNum - 2 * sameCharNum << " 个" << std::endl;
- 分别统计两文档中字和词组的出现次数
// DocAnalysis.cpp下
void DocAnalysis::getWordFreq(std::wstring &myDoc)
{dictTrieTree->match(myDoc.c_str(), myDoc.size());dictTrieTree->getFreqTable(q); // 会同时记录输入文档字、词的出现频率
}
// 将读入的文件和分词词典树进行比较,记录每个字、词的出现频率
// TrieTree.hpp下
void match(const T *s, int len) //用s中字符匹配TrieTree中的单词
{for (int i = 0; i < len; i++){TrieTreeNode<T> *currNode = root;for (int j = i; j < len; j++){auto iter = currNode->next.find(s[j]);if (iter == currNode->next.end()){break;}else{currNode = iter->second;if (currNode->isEnd){currNode->freq++; // 单词频数 + 1}}}}
}
// 将出现过的字、词都放入优先队列中,这样获取出现频率较高的字、词
// TrieTree.hpp下
// typedef std::priority_queue<std::pair<int, std::vector<T>>> qtype;
void getFreqTable(qtype &q)
{getFreqTable(q, root);
}void getFreqTable(qtype &q, TrieTreeNode<T> *currNode)
{if (currNode != root){currWord.push_back(currNode->c);}if (currNode->isEnd && currNode->freq) // freq不是0的单词才会加入到q中{q.push(std::make_pair(currNode->freq, currWord));}for (auto iter = currNode->next.begin(); iter != currNode->next.end(); iter++){getFreqTable(q, iter->second);}if (currNode != root){currWord.pop_back();}
}
- 对每个文档都统计出现次数最多的字和词组进行输出
// TrieTree.hpp下
void DocAnalysis::printResult(WOutFile *outFile)
{int wordNum = 0; //已输出词数目int charNum = 0; //已输出字数目const int limit = 10; //输出数目限制std::queue<std::pair<int, std::vector<wchar_t>>> cq; //存放字符的队列(*outFile) << "前十高频词: \n";while (!q.empty()){if (!stopWordsTrieTree->find(q.top().second)){if (q.top().second.size() > 1 && wordNum < limit) // q.top().second.size() > 1说明该节点存储的是一个词语{wordNum++;(*outFile) << q.top().second << ' ' << q.top().first << "次\n";}else if (q.top().second.size() == 1 && charNum < limit) // q.top().second.size() == 1说明该节点存储的是字符{charNum++;cq.push(q.top());}}q.pop();}(*outFile) << "\n前十高频字: \n";while (!cq.empty()){(*outFile) << cq.front().second << ' ' << cq.front().first << "次\n";cq.pop();}
}
输出结果展示
对于词频和字频,由于要统计出前十,这里我采用了优先队列来存储频数表,优先队列类型如下:
typedef std::priority_queue<std::pair<int, std::vector<T>>> qtype;
使用了C++
标准库的vector
,pair
和priority_queue
。在统计完词频后遍历一遍字典树,将频数大于0的词条以std::pair<int, std::vector<T>>
(这里T
的类型是wchar_t
)存储在优先队列中,std::pair
的第一个int
类型关键字为频数,第二个关键字是wchar_t
类型字符的数组,表示一个词,采用std::pair
的原因是其自动按照第一关键字、第二关键字的顺序比较大小,因此无需重载比较运算符。最后,依次pop
出优先队列的元素即可得到前十词频和字频。
对资本论.docx
的分析如下:
前十高频词:
资本 18273次
生产 12384次
价值 10518次
劳动 9528次
商品 6728次
货币 5590次
工人 4360次
形式 4011次
剩余 3482次
产品 3332次前十高频字:
资 23366次
产 18583次
不 16957次
生 16681次
价 14592次
动 12319次
人 11920次
工 11324次
值 10841次
品 10783次
结果存储在资本论\资本论_result.txt
当中。对于字符相等与不相等数目的比较则直接显示在控制台窗口中。
如果要使用别的文件名,比如foo.docx
,注意三个地方:
在
DocAnalysis
文件夹下创建对应文件夹(文件夹名为foo
)对应文件夹下放对应
docx
文件(foo
文件夹下放foo.docx
,如果是txt
文件那么放foo.txt
)在
main.cpp
中,更改对应inFileName
如下:
const char* inFileAName = "foo";
最终工程文件结构
下面展示的是DocAnalysis
目录下的文件结构:
.
+-- main.cpp // 包含main函数定义,程序入口// DocAnalysis类包含了程序运行的主逻辑
+-- DocAnalysis.h
+-- DocAnalysis.cpp// WInFile类完成了基本的从文件中读取GBK编码字符并存储为宽字符类型的操作
+-- WInFile.h
+-- WInFile.cpp// WOutFile类完成了输出GBK编码宽字符到文件的操作
+-- WOutFile.h
+-- WOutFile.cpp// TrieTree类定义了字典树这一数据结构及其基本操作,由于template类的声明与实现必须放在同一个文件当中,所以这里采用.hpp文件类型
+-- TrieTree.hpp+-- dict // dict文件夹存储相关字典
| +-- baidu_stopwords.txt // 百度停止词词典
| +-- dict.txt // 现代汉语词典+-- 资本论 //输入文件1,用于大文本测试
| +-- 资本论.docx
| +-- 资本论.txt //1.docx转换之后的txt文件
| +-- 资本论_result.txt //输出结果文件,包含词频和字频+-- 共产党宣言 //输入文件2,用于大文本测试
| +-- 共产党宣言.docx
| +-- 共产党宣言.txt
| +-- 共产党宣言_result.txt+-- 1 //输入文件1,用于小文本测试
| +-- 1.docx
| +-- 1.txt //1.docx转换之后的txt文件
| +-- 1_result.txt //输出结果文件,包含词频和字频+-- 2 //输入文件2,用小文本测试
| +-- 2.docx
| +-- 2.txt
| +-- 2_result.txt
问题与不足
由于我统计词频时分词的策略是遇到一个在字典树中的词就将这个词的词频加1,所以如果原文当中多次出现了词条中国特色社会主义
,我会将词条中国
,特色
,社会
,主义
,社会主义
,中国特色社会主义
的词频都加一,然而这里显然只需要将词条中国特色社会主义
的词频加一即可。除了这种策略外,还有正向最大匹配、反向最大匹配的策略,这些策略可能一定程度上避免这种问题,但是有时也会产生歧义。若想实现更加准确的分词与词频统计,可能需要借助人工智能与机器学习中NLP
领域的相关方法。
[C++练手项目] DocAnalysis相关推荐
- 70个Python练手项目列表 预祝大家 快乐
小孩眺望远方,成人怀念故乡. 为此给大家分享一下珍藏的Python实战项目,祝大家节日快乐哦!!! Python 前言:不管学习哪门语言都希望能做出实际的东西来,这个实际的东西当然就是项目啦,不用多说 ...
- 一个适合于Python 初学者的入门练手项目
随着人工智能的兴起,国内掀起了一股Python学习热潮,入门级编程语言,大多选择Python,有经验的程序员,也开始学习Python,正所谓是人生苦短,我用Python 有个Python入门练手项目, ...
- 推荐 Python 十大经典练手项目,让你的 Python 技能点全亮!
前言:如果有人问:"Python还火吗?""当然,很火.""哪能火多久呢?""不知道." 技术发展到现在衍生出许多种编程 ...
- 别让双手闲下来,来做一些练手项目吧
作者:Weston,原文链接,原文日期:2016-01-27 译者:saitjr:校对:Cee:定稿:千叶知风 自从我昨天发了文,收到的最多的评论就是: 我应该选择哪些 App 来练手呢? 这个问题很 ...
- python项目-推荐 10 个有趣的 Python 练手项目
想成为一个优秀的Python程序员,没有捷径可走,势必要花费大量时间在键盘后. 而不断地进行各种小项目开发,可以为之后的大开发项目积攒经验,做好准备. 但不少人都在为开发什么项目而苦恼. 因此,我为大 ...
- python新手项目-Python 的练手项目有哪些值得推荐?
其实初学者大多和题主类似都会经历这样一个阶段,当一门语言基础语法学完,之后刷了不少题,接下来就开始了一段迷茫期,不知道能用已经学到的东西做些什么即便有项目也无从下手,而且不清楚该如何去提高技术水平. ...
- python可以做什么项目-适合Python 新手的5大练手项目,你练了么?
已经学习了一段时间的Python,如果你看过之前W3Cschool的文章,就知道是时候该进去[项目]阶段了. 但是在练手项目的选择上,还存在疑问?不知道要从哪种项目先下手? W3Cschool首先有两 ...
- python新手项目-推荐:一个适合于Python新手的入门练手项目
原标题:推荐:一个适合于Python新手的入门练手项目 随着人工智能的兴起,国内掀起了一股Python学习热潮,入门级编程语言,大多选择Python,有经验的程序员,也开始学习Python,正所谓是人 ...
- python新手小项目-推荐:一个适合于Python新手的入门练手项目
随着人工智能的兴起,国内掀起了一股Python学习热潮,入门级编程语言,大多选择Python,有经验的程序员,也开始学习Python,正所谓是人生苦短,我用Python 有个Python入门练手项目, ...
最新文章
- android源码阅读笔记1-配置源码路径/阅读源码方法讨论
- 每日一皮:史上最直观的单向循环链表,还不懂算我输!
- python【蓝桥杯vip练习题库】ALGO-120 学做菜
- CSS图形每日一练(下)
- 平面设计师必备的十个技能
- 大对象简介+大对象的4种类型+lob类型的优点+lob的组成
- docker学习笔记(一)docker入门
- hao123电脑版主页_建议Lenovo用户卸载监守自盗的联想电脑管家
- 小A点菜(洛谷P1164题题解,Java语言描述)
- SQL开头quoted和ansiNULL
- Cortex-M3的存储器系统
- BIND配置文件详解(二)
- python tkinter获取屏幕大小_用 Python 制作关不掉的端午安康弹窗
- 性能测试培训总结-spotlight on mysql
- 沈向洋回归,从微软独立的小冰要弯道超车了
- Computer vision: models, learning and inference 学习笔记1:引言
- 阿里云Nginx配置站点403Forbidden问题
- 货拉拉 Android 动态资源管理系统原理与实践(下)
- java模拟网易邮箱登录_java实现163邮箱发送邮件到qq邮箱成功案例
- 华为的系统鸿蒙怎么样,华为鸿蒙2.0来了,这些功能比安卓iOS好用!
热门文章
- 合格前端系列第十一弹-初探 Nuxt.js 秘密花园
- Win7 右下角出现 Test Mode
- 积分球测试软件无法创建新文档,积分球测试中的问题解答
- android双屏异显获取副屏参数,Android 双屏 异显 插件 双屏(副屏)异显,主副屏通讯...
- 微软账户登录出现错误
- 23种设计模式(三大类)(转载)
- galera for mysql_Galera Cluster for MySQL——基本原理
- Macbook pro安装windows双系统!
- 真香定律!我的支付宝3面+美团4面+拼多多四面,100%好评!
- ap sat_先学托福还是先学SAT?先SAT还是先AP?是该好好想想