310
社区成员




这个作业属于哪个课程 | 软件工程实践-2023学年-W班社区-CSDN社区云 |
---|---|
这个作业要求在哪里 | 软件工程实践第二次作业——个人实战-CSDN社区 |
这个作业的目标 | 在文章开头给出新建的Gitcode项目地址 详细阅读作业要求 完成代码编写并进行测试 撰写博客 描述解题思路 设计实现过程 关键代码展示 PSP表格 |
其他参考文献 | https://www.cnblogs.com/xinz/archive/2011/11/20/2255830.html |
## 仓库地址
ImBloodGirl / 222100306 · GitCode
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 5 | 5 |
Development | 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 20 | 660(这个时间都在学C++爬虫,还没学会......)+400(改用python在调爬虫bug...) |
• Design Spec | • 生成设计文档 | 45 | 120 |
• Design Review | • 设计复审 | 45 | 60 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 28 | 60 |
• Design | • 具体设计 | 45 | 70 |
• Coding | • 具体编码 | 60 | 500(感觉大部分时间是在写爬虫......) |
• Code Review | • 代码复审 | 30 | 300 |
• Test | • 测试(自我测试,修改代码,提交修改) | 60 | 300 |
Reporting | 报告 | ||
• Test Repor | • 测试报告 | 30 | 45 |
• Size Measurement | • 计算工作量 | 10 | 20 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 45 | 60 |
合计 | 378 | 2620 |
首先,要对数据进行爬取。爬取后进行数据清洗。数据清洗后需要提取并整理出样例给出的数据格式。根据题目要求,我们需要获取选手的名字、性别、国籍、比赛项目、排名、成绩。
比赛结果 |世界游泳官方 (worldaquatics.com)
比赛运动员 |世界游泳官方 (worldaquatics.com)
文件读取函数
数据爬取
数据清洗
数据格式整理
文件输出函数
错误反馈处理
本来的数据库表结构是这样的:
Column Name | Data Type | Description |
---|---|---|
id | Integer | Primary key, autoincrement |
EventName | String(255) | Name of the event |
DisciplineName | String(255) | Name of the discipline |
EventResultDate | Date | Date of the event result |
OverallRank | Integer | Overall rank of the athlete |
Country | String(255) | Country of the athlete |
Athlete | String(255) | Name of the athlete |
Age | Integer | Age of the athlete |
Points | Float | Points earned by the athlete |
PtsBehind | Float | Points behind the leader |
Column Name | Data Type | Description |
---|---|---|
id | Integer | Primary key, autoincrement |
Country | String(255) | Country of the athlete |
Athlete | String(255) | Name of the athlete |
Gender | String(10) | Gender of the athlete (e.g., 'Male', 'Female') |
DOB | Date | Date of birth of the athlete |
Discipline | String(255) | Discipline of the athlete (e.g., 'Swimming', 'Athletics') |
Column Name | Data Type | Description |
---|---|---|
id | Integer | Primary key, autoincrement |
event_result_id | Integer | Foreign key, references the event result |
DiveOrder | Integer | Order of the dive |
DivePoints | Float | Points awarded for the dive |
【改进】但失败
所有不同项目是存放在同一个表下的,但是认为这样会使查询速度减慢,因此,如果改为以比赛项目名为名字存储数据,建立多个表。
修改失败,搞不了动态创建表。
因为C++的IDE问题难以修复,不能使用第三方库,只能把数据库文件存为csv,读取文件。
读取文件
识别文件内容
如果是 players
,就调用输出选手所有数据的函数
如果是
result ...
,则进一步处理:
result
必须是独立的单词。
如果格式为 result ... detail
,就调用输出详细比赛结果的函数。
如果格式为 result ...
(不含 detail
),就调用输出相应比赛结果的函数。
如果 result
后面的条件不符合要求,就输出 N/A
。
如果无法识别是 players
还是 result
,则输出 Error
。
整理格式
详细思路见实现过程
Python语言,Pycharm软件
(1)根据结构建立表
# 创建数据库连接引擎 engine = create_engine('mysql+pymysql://root:123456@localhost:3306/swim_cmpt') # 创建一个基类 Base = declarative_base() # 定义比赛结果表的ORM类 class EventResult(Base): __tablename__ = 'event_results' id = Column(Integer, primary_key=True, autoincrement=True) EventName = Column(String(255)) DisciplineName = Column(String(255)) EventResultDate = Column(Date) OverallRank = Column(Integer) Country = Column(String(255)) Athlete = Column(String(255)) Age = Column(Integer) Points = Column(Float) PtsBehind = Column(Float) # 创建表 Base.metadata.create_all(engine) # 创建一个Session类 Session = sessionmaker(bind=engine)
(2)根据含有各比赛id的url爬出此特定id
def get_competition_events(): url = "https://api.worldaquatics.com/fina/competitions/3337/events" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0" } # 发送 GET 请求 response = requests.get(url, headers=headers) # 检查请求是否成功 if response.status_code == 200: # 解析 JSON 响应 data = response.json() # print(data) # 提取比赛信息 competitions = data.get('Sports', [])[0].get("DisciplineList",[]) print(competitions) for competition in competitions: competition_id = competition.get('Id') competition_name = competition.get('DisciplineName') # 构建比赛对应的URL competition_url = f"https://api.worldaquatics.com/fina/events/{competition_id}" # 获取比赛结果数据并处理 get_event_results(competition_url, competition_name) else: print("Failed to fetch data from the API") print("获取比赛信息失败!")
类似以上,根据获取的特定id访问比赛具体信息url,爬取信息存入数据库
(3)将爬取到的信息存为csv
1. 解析数据文件
读取并解析athlete_info.csv
文件,获取选手的全名、性别和国籍信息。
2. 实现主功能
根据input.txt
中的指令内容,决定执行哪个功能。对于功能1,当内容为players
时,调用输出所有选手信息的函数 outputPlayersData
。
3. 输出格式
输出内容格式化为指定格式,并写入到output.txt
文件中。每个选手的信息后面跟随一行 -----
作为分隔符。
1. 解析数据文件
读取并解析event_results.csv
文件,获取各个赛事的比赛结果。
2. 实现主功能
根据
input.txt
中的指令内容,决定执行哪个功能。
对于内容为
result ...
的指令:
result
必须是独立的单词。
如果格式为 result ... detail
,调用输出详细比赛结果的函数 outputDetailedResult
。
如果格式为 result ...
(不含 detail
),调用输出相应比赛结果的函数 outputEventResult
。
3. 输出格式
输出内容格式化为指定格式,并写入到output.txt
文件中。每个结果信息后面跟随一行 -----
作为分隔符。
1. 详细比赛结果
读取并解析更详细的比赛结果文件,获取初赛、半决赛和决赛的详细得分。
2. 实现主功能
当指令内容为 result ... detail
时,调用输出详细比赛结果的函数 outputDetailedResult
。
3. 输出格式
输出内容格式化为指定格式,并写入到output.txt
文件中。每个详细结果信息后面跟随一行 -----
作为分隔符。
1. 处理无法识别的指令
如果无法识别是 players
还是 result
,则输出 Error
,并写入到output.txt
文件中。
2. 处理无效的赛事条件
如果 result
后面的条件不符合要求(由 isValidEvent
函数判断),则输出 N/A
,并写入到output.txt
文件中。
接口类:DivingChampionShshiy
运行命令:
g++ src\DWASearch.cpp src\DivingChampionship.cpp -o DWASearch
DWASearch src\input.txt src\output.txt src\data\athlete_info.csv src\data\event_results2.csv src\data\event_details.csv
接口功能:
处理命令:根据命令内容执行相应的操作。
验证事件:验证赛事类型的有效性。
验证命令:验证命令格式的有效性。
读取数据文件:读取不同格式的 CSV 数据文件。
格式化输出:格式化比赛结果并输出到文件。
#ifndef DIVING_CHAMPIONSHIP_H #define DIVING_CHAMPIONSHIP_H #include <string> #include <vector> #include <map> // 结构体 struct Athlete { std::string fullName; std::string lastName; std::string gender; std::string country; }; struct AthleteRank { std::string preliminaryRank; std::string semifinalRank; std::string finalRank; }; struct DetailedAthlete { std::string fullName; std::string rank; std::string preliminaryScore; std::string semifinalScore; std::string finalScore; }; struct EventResult { int id; std::string eventName; std::string disciplineName; std::string eventResultDate; std::string heatName; int overallRank; std::string country; std::string athlete; std::string age; double points; std::string ptsBehind; }; class DivingChampionship { public: // 处理命令,根据命令内容调用相应的处理函数 void processCommand(const std::string& command, const std::string& athleteInfoFile, const std::string& eventResultsFile, const std::string& diveScoresFile, const std::string& outputFile); // 验证赛事类型的有效性 bool isValidEvent(const std::string& event); // 验证命令格式的有效性 bool isValidResultCommand(const std::string& command); private: // 读取CSV文件并存储数据 void readCSV(const std::string& filename, std::vector<std::vector<std::string>>& data); // 格式化得分 std::string formatScore(const std::vector<double>& scores); // 提取选手的姓氏 std::string extractLastName(const std::string& fullName); // 读取选手信息 std::vector<Athlete> readAthleteInfo(const std::string& filename); // 读取赛事结果 std::vector<EventResult> readEventResults(const std::string& filename); // 读取跳水得分 std::map<int, std::vector<double>> readDiveScores(const std::string& filename); // 写入赛事结果到文件 void writeEventResult(const std::string& filename, const std::vector<EventResult>& results, const std::map<int, std::vector<double>>& diveScores); // 写入选手信息到文件 void writeAthleteInfo(const std::string& filename, const std::vector<Athlete>& athletes); // 写入详细赛事结果到文件 void writeDetailedEventResult(const std::string& filename, const std::vector<DetailedAthlete>& athletes); // 转换字符串为小写 std::string toLower(const std::string& str); // 输出错误信息到文件 void outputError(const std::string& outputFile); // 输出N/A到文件 void outputNA(const std::string& outputFile); }; #endif // DIVING_CHAMPIONSHIP_H
#include "DivingChampionship.h" #include <iostream> #include <fstream> #include <sstream> #include <algorithm> #include <unordered_set> /** * @brief 读取CSV文件并存储数据 * * @param filename CSV文件名 * @param data 用于存储读取数据的二维字符串向量 */ void DivingChampionship::readCSV(const std::string& filename, std::vector<std::vector<std::string>>& data) { // 打开文件并读取内容 // 将每行数据分割成多个单词并存储到data中 } /** * @brief 格式化得分 * * @param scores 跳水得分向量 * @return 格式化后的得分字符串 */ std::string DivingChampionship::formatScore(const std::vector<double>& scores) { // 遍历得分向量并计算总分 // 将得分和总分格式化为字符串 return formattedScore; } /** * @brief 提取选手的姓氏 * * @param fullName 选手全名 * @return 选手的姓氏 */ std::string DivingChampionship::extractLastName(const std::string& fullName) { // 分割全名字符串并提取最后一个单词作为姓氏 return lastName; } /** * @brief 读取选手信息 * * @param filename CSV文件名 * @return 选手信息向量 */ std::vector<Athlete> DivingChampionship::readAthleteInfo(const std::string& filename) { // 打开文件并读取选手信息 // 将读取的每行数据解析成Athlete结构体并存储到向量中 return athletes; } /** * @brief 读取赛事结果 * * @param filename CSV文件名 * @return 赛事结果向量 */ std::vector<EventResult> DivingChampionship::readEventResults(const std::string& filename) { // 打开文件并读取赛事结果 // 将读取的每行数据解析成EventResult结构体并存储到向量中 return results; } /** * @brief 读取跳水得分 * * @param filename CSV文件名 * @return 以赛事ID为键,得分向量为值的映射表 */ std::map<int, std::vector<double>> DivingChampionship::readDiveScores(const std::string& filename) { // 打开文件并读取跳水得分 // 将读取的每行数据解析成键值对并存储到映射表中 return diveScores; } /** * @brief 写入赛事结果到文件 * * @param filename 输出文件名 * @param results 赛事结果向量 * @param diveScores 跳水得分映射表 */ void DivingChampionship::writeEventResult(const std::string& filename, const std::vector<EventResult>& results, const std::map<int, std::vector<double>>& diveScores) { // 打开输出文件并将赛事结果和得分写入文件 } /** * @brief 写入选手信息到文件 * * @param filename 输出文件名 * @param athletes 选手信息向量 */ void DivingChampionship::writeAthleteInfo(const std::string& filename, const std::vector<Athlete>& athletes) { // 打开输出文件并将选手信息写入文件 } /** * @brief 写入详细赛事结果到文件 * * @param filename 输出文件名 * @param athletes 详细赛事结果向量 */ void DivingChampionship::writeDetailedEventResult(const std::string& filename, const std::vector<DetailedAthlete>& athletes) { // 打开输出文件并将详细赛事结果写入文件 } /** * @brief 转换字符串为小写 * * @param str 输入字符串 * @return 转换后的字符串 */ std::string DivingChampionship::toLower(const std::string& str) { // 将输入字符串中的每个字符转换为小写 return lowerStr; } /** * @brief 验证赛事类型的有效性 * * @param event 赛事类型字符串 * @return 赛事类型是否合法 */ bool DivingChampionship::isValidEvent(const std::string& event) { // 检查赛事类型是否在预定义的有效赛事类型集合中 return valid; } /** * @brief 验证命令格式的有效性 * * @param command 命令字符串 * @return 命令格式是否合法 */ bool DivingChampionship::isValidResultCommand(const std::string& command) { // 检查命令格式是否符合预期,包括是否包含有效的赛事类型和详细标志 return valid; } /** * @brief 处理命令,根据命令内容调用相应的处理函数 * * @param command 命令字符串 * @param athleteInfoFile 选手信息文件名 * @param eventResultsFile 赛事结果文件名 * @param diveScoresFile 跳水得分文件名 * @param outputFile 输出文件名 */ void DivingChampionship::processCommand(const std::string& command, const std::string& athleteInfoFile, const std::string& eventResultsFile, const std::string& diveScoresFile, const std::string& outputFile) { if (command == "players") { // 读取选手信息并写入输出文件 } else if (!isValidResultCommand(command)) { // 输出N/A到文件 } else if (command.rfind("result ", 0) == 0 && command.find("detail") == std::string::npos) { // 读取赛事结果并写入输出文件 } else if (command.rfind("result ", 0) == 0 && command.find("detail") != std::string::npos) { // 读取详细赛事结果并写入输出文件 } else { // 输出错误信息到文件 } }
这里处理命令的逻辑来进行命令的识别/报错和分类。不同情况下的代码流。
bool isValidResultCommand(const std::string& command) { if (command.rfind("result ", 0) != 0 && command.find("result") == 0) { return false; // If it starts with "result" but not followed by space, it is an error } std::string eventType; if (command.find(" detail") != std::string::npos) { eventType = command.substr(7, command.find(" detail") - 7); return isValidEvent(eventType); } else { eventType = command.substr(7); return isValidEvent(eventType); } }
void processCommand(const std::string& command, std::ofstream& outFile) { if (command == "players") { outputPlayersData(outFile); } else if (command.find("result") == 0) { std::string event = command.substr(7); // 去掉 "result " 部分 std::string detailSuffix = "detail"; // 处理详细结果 if (event.size() > detailSuffix.size() && event.compare(event.size() - detailSuffix.size(), detailSuffix.size(), detailSuffix) == 0) { std::string eventType = event.substr(0, event.size() - detailSuffix.size() - 1); if (isValidEvent(eventType)) { outputDetailedResult(outFile, eventType); } else { outputNA(outFile); } } // 处理普通结果 else if (isValidEvent(event)) { outputEventResult(outFile, event); } else { outputNA(outFile); } } else { outputError(outFile); } } bool isValidEvent(const std::string& event) { static const std::vector<std::string> validEvents = { "women 1m springboard", "women 3m springboard", "women 10m platform", "women 3m synchronised", "women 10m synchronised", "men 1m springboard", "men 3m springboard", "men 10m platform", "men 3m synchronised", "men 10m synchronised" }; return std::find(validEvents.begin(), validEvents.end(), event) != validEvents.end(); } int main(int argc, char* argv[]) { if (argc != 3) { std::cerr << "Usage: " << argv[0] << " <input_file> <output_file>\n"; return 1; } std::ifstream inFile(argv[1]); std::ofstream outFile(argv[2]); if (!inFile) { std::cerr << "Error opening input file\n"; return 1; } if (!outFile) { std::cerr << "Error opening output file\n"; return 1; } std::string command; while (std::getline(inFile, command)) { processCommand(command, outFile); } return 0; }
下面是合法条件的确定:
bool isValidEvent(const std::string& event) { std::unordered_set<std::string> validEvents = { "women 1m springboard", "women 3m springboard", "women 10m platform", "women 3m synchronised", "women 10m synchronised", "men 1m springboard", "men 3m springboard", "men 10m platform", "men 3m synchronised", "men 10m synchronised" }; return validEvents.find(event) != validEvents.end(); }
先让程序正常读取文件。
1. 处理无法识别的指令
当输入文件中的指令既不是 players
也不是以 result
开头时,程序会输出 Error
。这是为了确保程序能在遇到无效输入时,提供明确的反馈,而不会崩溃或产生不可预期的行为。
具体实现方法如下:
在 processCommand
函数中,首先检查指令是否为 players
,如果是则调用 outputPlayersData
函数。
如果指令不是 players
,则检查它是否以 result
开头。
如果既不是 players
也不是 result
,则调用 outputError
函数,向输出文件写入 Error
。
2. 处理无效的赛事条件
在处理以 result
开头的指令时,需要进一步检查指令的详细内容是否有效。具体的检查步骤如下:
如果指令格式为 result ... detail
,即包含 detail
关键字,程序会提取出赛事条件并检查其有效性。如果条件无效,则输出 N/A
。
如果指令格式为 result ...
(不含 detail
),程序同样会检查赛事条件的有效性。如果条件无效,也会输出 N/A
。
为了实现上述检查,定义了一个辅助函数 isValidEvent
来验证赛事条件的有效性。这个函数包含了所有有效的赛事条件,并检查给定的条件是否在其中。
3. 处理文件读取和写入错误
为了确保程序在读取和写入文件时能够处理可能出现的错误,程序会在打开文件时检查文件是否成功打开。如果文件无法打开,程序将输出相应的错误信息,并终止执行。
具体实现方法如下:
在 main
函数中,尝试打开输入文件和输出文件。
如果输入文件无法打开,输出错误信息 Error opening input file
。
如果输出文件无法打开,输出错误信息 Error opening output file
。
原因:在csv中rank1的pst behind值为空,导致读取失败
尝试学习用C++爬取网页数据,实在不理解,尽管看了很多网课,大部分都是爬取图片,讲解者隐晦带着黄色的腔调令人不适。
所以我用Python爬取了网页数据,并用C++处理数据。
理想:我用python写过爬虫,无非是
请求网页,抓取html内容
用正则清洗数据
将数据安放到数据库
而已,用到的库大概有requests
或 urllib
库。
现实:但在 C++ 中抓取网页内容相对 Python 来说复杂一些。
因为 C++ 并没有内置像 Python 中requests
或 urllib
这样的方便的库 。
在 C++ 中,我需要使用一大堆第三方库来发送 HTTP 请求和处理响应。常用的库包括 cURL、libcurl、WinINet 等。
哎理想很丰满,现实很骨感,我以为爬虫是个很简单的作业,但是由于C++爬虫和Python有很大不同,还是得重新学习,学习爬虫的原理也是个曲折的过程......
其他:另外,我发现仅仅只熟悉一个编译器,是远远不够的,因为有时候编译器因为各种原因无法使用。
比如,我的Cion许可证过期了,需要邮件到校园邮箱验证,而我给邮箱管理员发的邮件一周都没回,网站也登不进去......
这时我想到可以用vscode,但发现自己并不熟悉库的安装,还要另外下载vspkg才能安装库......
学习原来是耗时最长的......
烦......
有些东西我们总是看起来“会”、“有思路”,做起来比预期时间要长得多,所以应该预留时间,未雨绸缪,才有望按时交付。
把所有困难的事情看作自我成长,就不会那么困难了。
比如一开始不知道数据到底要爬什么,虽然会用python爬虫,但是着手取数据的时候还是纠结半天。
现在知道看到作业给出两个链接,只要爬两个链接下相关的数据即可。不需要想太多复杂的。