软件工程第二次作业-个人实战

222100306洪朗晨 2024-06-14 13:38:00

作业基本信息

这个作业属于哪个课程软件工程实践-2023学年-W班社区-CSDN社区云
这个作业要求在哪里软件工程实践第二次作业——个人实战-CSDN社区
这个作业的目标在文章开头给出新建的Gitcode项目地址 详细阅读作业要求 完成代码编写并进行测试 撰写博客 描述解题思路 设计实现过程 关键代码展示 PSP表格
其他参考文献https://www.cnblogs.com/xinz/archive/2011/11/20/2255830.html

## 仓库地址

ImBloodGirl / 222100306 · GitCode

PSP表格

    
PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划  
• Estimate• 估计这个任务需要多少时间55
Development开发  
• Analysis• 需求分析 (包括学习新技术)20660(这个时间都在学C++爬虫,还没学会......)+400(改用python在调爬虫bug...)
• Design Spec• 生成设计文档45120
• Design Review• 设计复审4560
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)2860
• Design• 具体设计4570
• Coding• 具体编码60500(感觉大部分时间是在写爬虫......)
• Code Review• 代码复审30300
• Test• 测试(自我测试,修改代码,提交修改)60300
Reporting报告  
• Test Repor• 测试报告3045
• Size Measurement• 计算工作量1020
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划4560
 合计3782620

 

解题思路

首先,要对数据进行爬取。爬取后进行数据清洗。数据清洗后需要提取并整理出样例给出的数据格式。根据题目要求,我们需要获取选手的名字、性别、国籍、比赛项目、排名、成绩。

比赛结果 |世界游泳官方 (worldaquatics.com)

比赛运动员 |世界游泳官方 (worldaquatics.com)

(一)需要函数

  • 文件读取函数

  • 数据爬取

    • 数据清洗

    • 数据格式整理

  • 文件输出函数

  • 错误反馈处理

(二)数据库结构

(1)比赛表event_results

本来的数据库表结构是这样的:

Column NameData TypeDescription
idIntegerPrimary key, autoincrement
EventNameString(255)Name of the event
DisciplineNameString(255)Name of the discipline
EventResultDateDateDate of the event result
OverallRankIntegerOverall rank of the athlete
CountryString(255)Country of the athlete
AthleteString(255)Name of the athlete
AgeIntegerAge of the athlete
PointsFloatPoints earned by the athlete
PtsBehindFloatPoints behind the leader

(2)选手表athlete_info

Column NameData TypeDescription
idIntegerPrimary key, autoincrement
CountryString(255)Country of the athlete
AthleteString(255)Name of the athlete
GenderString(10)Gender of the athlete (e.g., 'Male', 'Female')
DOBDateDate of birth of the athlete
DisciplineString(255)Discipline of the athlete (e.g., 'Swimming', 'Athletics')

(3) 具体得分表envent_details

Column NameData TypeDescription
idIntegerPrimary key, autoincrement
event_result_idIntegerForeign key, references the event result
DiveOrderIntegerOrder of the dive
DivePointsFloatPoints awarded for the dive

【改进】但失败

所有不同项目是存放在同一个表下的,但是认为这样会使查询速度减慢,因此,如果改为以比赛项目名为名字存储数据,建立多个表

修改失败,搞不了动态创建表。

因为C++的IDE问题难以修复,不能使用第三方库,只能把数据库文件存为csv,读取文件。

(三)整体思路

  • 读取文件

  • 识别文件内容

    • 如果是 players,就调用输出选手所有数据的函数

    • 如果是

      result ...

      ,则进一步处理:

      • result 必须是独立的单词。

      • 如果格式为 result ... detail,就调用输出详细比赛结果的函数。

      • 如果格式为 result ...(不含 detail),就调用输出相应比赛结果的函数。

      • 如果 result 后面的条件不符合要求,就输出 N/A

    • 如果无法识别是 players 还是 result,则输出 Error

  • 整理格式

详细思路见实现过程

实现过程

(一)爬取数据

1. 编译平台

Python语言,Pycharm软件

2. 爬取结果展示

image-20240305175016436

image-20240305175113327

3. 爬取过程

(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

接口功能:

  1. 处理命令:根据命令内容执行相应的操作。

  2. 验证事件:验证赛事类型的有效性。

  3. 验证命令:验证命令格式的有效性。

  4. 读取数据文件:读取不同格式的 CSV 数据文件。

  5. 格式化输出:格式化比赛结果并输出到文件。

(二)接口函数

#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. 先让程序正常读取文件。

image-20240307233609227

异常处理说明

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

心得体会

遇到的问题

(一)输出等级时总是读不到Rank1的内容

原因:在csv中rank1的pst behind值为空,导致读取失败

反思和经验

(一)爬取数据之失败的尝试

尝试学习用C++爬取网页数据,实在不理解,尽管看了很多网课,大部分都是爬取图片,讲解者隐晦带着黄色的腔调令人不适。

所以我用Python爬取了网页数据,并用C++处理数据。

(二)Planning时间与预期相差甚远,以及爬虫的学习经验

  • 理想:我用python写过爬虫,无非是

    • 请求网页,抓取html内容

    • 用正则清洗数据

    • 将数据安放到数据库

    而已,用到的库大概有requestsurllib 库。

  • 现实:但在 C++ 中抓取网页内容相对 Python 来说复杂一些。

    • 因为 C++ 并没有内置像 Python 中requestsurllib 这样的方便的库 。

    • 在 C++ 中,我需要使用一大堆第三方库来发送 HTTP 请求和处理响应。常用的库包括 cURL、libcurl、WinINet 等。

哎理想很丰满,现实很骨感,我以为爬虫是个很简单的作业,但是由于C++爬虫和Python有很大不同,还是得重新学习,学习爬虫的原理也是个曲折的过程......

  • 其他:另外,我发现仅仅只熟悉一个编译器,是远远不够的,因为有时候编译器因为各种原因无法使用。

    • 比如,我的Cion许可证过期了,需要邮件到校园邮箱验证,而我给邮箱管理员发的邮件一周都没回,网站也登不进去......

    • 这时我想到可以用vscode,但发现自己并不熟悉库的安装,还要另外下载vspkg才能安装库......

(三)学习是耗时最长的

学习原来是耗时最长的......

烦......

有些东西我们总是看起来“会”、“有思路”,做起来比预期时间要长得多,所以应该预留时间,未雨绸缪,才有望按时交付。

(四)

把所有困难的事情看作自我成长,就不会那么困难了。

比如一开始不知道数据到底要爬什么,虽然会用python爬虫,但是着手取数据的时候还是纠结半天。

现在知道看到作业给出两个链接,只要爬两个链接下相关的数据即可。不需要想太多复杂的。

...全文
198 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

310

社区成员

发帖
与我相关
我的任务
社区描述
福州大学的软件工程实践-2023学年-W班
软件工程需求分析结对编程 高校 福建省·福州市
社区管理员
  • FZU_SE_teacherW
  • Pity·Monster
  • 助教张富源
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧