学完Python的条件判断、循环、字符串方法这些基础知识后,最大的困惑往往是:这些东西凑在一起到底能干什么?这篇文章用一个真实项目回答这个问题。我们从豆瓣电影Top250的公开数据出发,用Python完成一次从脏数据到分析报告的全流程实战。过程中,程序流程控制和数值与字符串的核心知识点会一个接一个地自然登场——不是因为"该讲这个语法了",而是"要解决这个问题,正好需要它"。我们的数据集:一份"脏兮兮"的豆瓣电影Top250豆瓣电影Top250是一个广为人知的电影榜单。我们从豆瓣网站采集了完整的250部电影信息,保存为CSV文件。每条记录包含七个字段:排名、电影名、外文名、年份、评分、评价人数和一句话短评。但这份数据是从网页上直接采集的"原始版本",打开一看就知道问题不少:排名,电影名,外文名,年份,评分,评价人数,一句话短评1, 肖申克的救赎,The Shawshank Redemption,1994,9.7,3304229人评价,希望让人自由。2,霸王别姬 ,Farewell My Concubine,1993,9.6分,2438107,风华绝代。3,泰坦尼克号,Titanic,(1997), 9.5 ,2514227人评价,失去的才是永恒的。4,阿甘正传 , Forrest Gump ,1994年,9.5,244.3万,一部美国近现代史。5,千与千寻,Spirited Away,2001,9.4分,2552709人评价,最好的宫崎骏,最好的久石让。
随便看几行就能发现一堆毛病:电影名前后夹着空格(" 肖申克的救赎"、"霸王别姬 "),年份有"1994"、"(1997)"、"1994年"三种不同写法,评分有的带"分"字有的不带,评价人数有的纯数字、有的带"人评价"后缀、有的甚至用"244.3万"表示,还因为采集失误存在几条重复录入的数据。这就是真实世界的数据——不会像教科书例题那样整整齐齐地等着你。而"把脏数据变成干净数据",恰恰是数据分析的第一步,也是最耗时的一步。Python的内置 csv 模块可以方便地读取CSV文件:import csvrows = []with open("douban_top250_dirty.csv", "r", encoding="utf-8-sig") as f: reader = csv.reader(f) header = next(reader) # 跳过表头 for row in reader: if len(row) >= 7: # 过滤掉不完整的空行 rows.append(row)
这里有一个重要的细节:encoding="utf-8-sig"。CSV文件是用UTF-8编码保存的,如果省略这个参数,在某些系统上(特别是Windows默认的GBK编码环境)读取中文时会出现乱码。utf-8-sig 比普通的 utf-8 多了一步:它会自动跳过文件开头的BOM(字节序标记),避免BOM字符混入第一个字段名中。这涉及到第4章的字符串编码知识——编码和解码必须使用相同的格式,否则数据就会"乱码"甚至报错。csv.reader 每次返回一行数据,自动按照逗号分隔字段。读出来的每一行是一个列表,比如第一条记录:['1', ' 肖申克的救赎', 'The Shawshank Redemption', '1994', '9.7', '3304229人评价', '希望让人自由。']
注意 ' 肖申克的救赎' 开头有一个空格——这就是我们要清洗的"脏东西"。拿到一行数据后,第一件事就是把每个字段两端的空白字符去掉。strip()是最常用的字符串清洗方法:title = row[1].strip() # '霸王别姬 ' → '霸王别姬'eng_title = row[2].strip() # ' Forrest Gump ' → 'Forrest Gump'rating_raw = row[4].strip() # ' 9.5 ' → '9.5'
strip()会删除字符串两端的所有空白字符(空格、制表符\t、换行符\n等)。它的两个变体也很实用:lstrip()只删左边,rstrip()只删右边。但strip()处理不了字符串中间的空格。比如有的电影名中间夹了空格,这时候就要用replace():title = row[1].strip().replace(" ", "")# ' 大 话 西 游 ' → strip → '大 话 西 游' → replace → '大话西游'
replace(old, new)会把字符串中所有匹配的旧子串替换为新子串。它还有一个可选的第三个参数来限制替换次数:"hello world hello".replace("hello", "hi", 1) # 'hi world hello'
字符串清洗三板斧:strip() 去两端,replace() 替换中间,split() 拆分字段。这三个方法几乎在每个数据处理项目中都会用到。字符串两端洗干净了,但很多字段的内容格式不统一,需要做进一步处理。这正是条件判断大显身手的场景。看看数据里的年份字段,至少有三种写法:"1993"(纯数字)、"(1997)"(带中文括号)、"1994年"(带"年"字)。year_raw = "(1997)"# 用 find() 定位第一个数字的位置,再用切片提取四位年份year = Nonefor start_char in "0123456789": pos = year_raw.find(start_char) if pos != -1 and pos + 4 <= len(year_raw): year_str = year_raw[pos:pos+4] if year_str.isdigit(): year = int(year_str) # 1997 break # 找到有效年份才退出循环
find(sub) 返回子串在字符串中首次出现的位置(索引从0开始)。如果没找到,返回 -1。与之类似的index(sub) 方法功能相同,但没找到时会抛出ValueError异常。两者的区别在于:find()适合"不确定是否存在"的场景(返回-1做判断),index()适合"确定一定存在"的场景(找不到直接报错提醒你)。注意上面代码中break的位置——它放在if year_str.isdigit()内部,意味着只有找到合法的四位年份才退出循环。如果某个数字字符恰好不够截取四位(比如 "2014年" 中 '0' 出现在位置1,截取4位是"014年",不是合法年份),循环会继续尝试下一个数字字符,直到找到'2' 在位置0,截取"2014"才退出。这个细节看似微小,却是很多初学者容易忽略的逻辑陷阱。切片year_raw[pos:pos+4]是Python中截取子串的核心语法。格式为 str[start:end:step],取从start到end(不含)之间的字符。省略start表示从头开始,省略end表示到末尾。比如:s = "Python数据分析"print(s[0:6]) # 'Python'print(s[6:]) # '数据分析'print(s[::2]) # 'Pto数分'(步长为2,隔一个取一个)
评分字段也有两种写法:"9.5" 和 "9.5分"。处理方式很简单:rating_str = "9.5分"rating_str = rating_str.replace("分", "") # '9.5'rating = float(rating_str) # 9.
但更稳妥的做法是加一层条件判断,用字符串的isdigit()和isalnum()等测试方法来做预检查。Python提供了一组以is开头的字符串测试方法:isdigit() 检查是否全为数字,isalpha()检查是否全为字母,isalnum()检查是否全为字母或数字,isspace()检查是否全为空白字符。这些方法返回布尔值(True/False),是条件判断的好搭档。评价人数字段的格式更加混乱:"2856431人评价"、"3201876"、"285万"。需要分情况处理:reviews_raw = "2856431人评价"# 先用 replace() 去掉已知后缀reviews_clean = reviews_raw.replace("人评价", "").strip()# 条件判断:是否包含"万"字if "万" in reviews_clean: # '285万' → 去掉'万' → 转浮点 → 乘以10000 → 转整数 review_count = int(float(reviews_clean.replace("万", "")) * 10000)else: review_count = int(reviews_clean)print(review_count) # 2856431
这里用到了in运算符来做子串包含测试:"万" in reviews_clean检查字符串中是否包含"万"字。这种写法比find()更简洁直观,适合只需要判断"有没有"而不关心"在哪里"的场景。前面提取评分的代码看起来很顺利,但如果某条数据的评分字段是"abc"或者干脆是空字符串呢?float("abc")会直接抛出ValueError,程序当场崩溃。在真实的数据处理中,脏数据是常态。程序不能因为一条坏记录就全盘终止——这就是异常处理的价值。Python用try-except结构来捕获和处理异常:try: rating_clean = rating_raw.replace("分", "") rating = float(rating_clean) assert 0 <= rating <= 10, f"评分 {rating} 超出 0-10 范围"except ValueError: print(f"评分格式错误:'{rating_raw}',已跳过") continueexcept AssertionError as e: print(f"数据异常:{e},已跳过") continue
把"可能出错的代码"放在try块里,用except捕获特定类型的异常。这里有几个要点。多个except子句可以分别处理不同类型的异常。ValueError捕获类型转换失败(比如float("abc")),AssertionError捕获断言失败。Python会从上到下匹配异常类型,一旦匹配就执行对应的处理代码。assert断言是一种"防御性编程"工具。assert 条件, 提示信息 在条件为False 时抛出AssertionError。它不是用来处理用户输入的(那是if的事),而是用来声明"代码逻辑上不应该出现的情况"。评分超出0-10说明数据源头有严重问题,断言失败就是在"报警"。try-except还可以搭配else和finally:try: rating = float(rating_clean)except ValueError: print("转换失败")else: print(f"转换成功:{rating}") # 没出错时才执行finally: total_processed += 1 # 无论是否出错都执行
else只在try块没有异常时执行,finally无论是否发生异常都会执行(通常用于资源清理,比如关闭文件、释放连接)。单条记录的处理逻辑搞定了,现在用循环来批量处理所有数据。for循环是最自然的选择——遍历列表中的每一条记录:clean_movies = []invalid_count = 0seen_titles = set() # 用于去重for i, row in enumerate(rows): title = row[1].strip() # ... 前面讲的清洗逻辑 ... # 去重:用集合检查标题是否已处理过 title_key = title.lower() # 统一转小写再比较 if title_key in seen_titles: print(f"[去重] 《{title}》重复,已跳过") invalid_count += 1 continue seen_titles.add(title_key) clean_movies.append({ "title": title, "year": year, "rating": rating, "reviews": review_count, "quote": quote, })
enumerate(rows)是Python内置函数,它在遍历时同时返回索引和元素,免去了手动维护计数变量的麻烦。continue的作用是跳过当前这一轮循环,直接进入下一条记录。与之对应的break是彻底终止整个循环。在处理数据时,continue用得更多——遇到一条坏数据,跳过它继续处理后面的,而不是整个停下来。注意到代码中用了一个set(集合)来去重。集合的查找速度是O(1)的,比在列表中用in查找(O(n))快得多。对于250条数据这个差异还不太明显,但如果处理25万条数据,集合的优势就非常突出了——这也是第3章提到的循环优化思想:选择合适的数据结构可以大幅提升循环效率。title.lower()把标题统一转为小写再比较,避免了因大小写不同而误判为不同记录。字符串的大小写转换方法包括:lower()(全转小写)、upper()(全转大写)、capitalize()(首字母大写)、title()(每个单词首字母大写)、swapcase()(大小写互换)。250条有效数据清洗完毕(5条重复记录被去掉了),现在来做统计分析。all_ratings = [m["rating"] for m in clean_movies]avg_rating = sum(all_ratings) / len(all_ratings)max_movie = max(clean_movies, key=lambda m: m["rating"])min_movie = min(clean_movies, key=lambda m: m["rating"])print(f"平均评分:{avg_rating:.2f}")print(f"最高评分:《{max_movie['title']}》{max_movie['rating']}分")
sum() 求和、len()求长度、max() / min()求最大最小值,这些都是Python内置函数,执行效率很高(底层用C实现)。key=lambda m: m["rating"]告诉 max()按照评分来比较,而不是默认比较整个字典。for m in clean_movies: if m["rating"] >= 9.5: m["grade"] = "神作级" elif m["rating"] >= 9.0: m["grade"] = "经典级" elif m["rating"] >= 8.5: m["grade"] = "优秀级" else: m["grade"] = "良好级"
多分支条件的排列顺序至关重要。 Python从上到下依次检查,一旦某个条件为True就执行对应代码块并跳出。如果写成:# 错误示范!if rating >= 8.5: grade = "优秀级" # 9.7分也会走到这里!elif rating >= 9.0: grade = "经典级"
一部9.7分的电影会被错误归类为"优秀级"。记住:最严格的条件放在最前面。from collections import Counterdecades = Counter()for m in clean_movies: decade = (m["year"] // 10) * 10 # 1994 → 1990, 2001 → 2000 decades[f"{decade}s"] += 1
1994 // 10 的结果是 199(整除,丢弃余数),再乘以10得到 1990。这种"先整除再乘回"的技巧在做数据分组时非常实用。Counter 是Python标准库 collections 模块提供的一个工具类,它会自动统计每个键出现的次数,比手动用字典计数更简洁。top5 = sorted(clean_movies, key=lambda m: m["reviews"], reverse=True)[:5]
sorted()返回一个新列表,不修改原数据。reverse=True表示降序排列。最后用切片 [:5] 取前5个。在计算平均分时,我们做了 `sum(allratings) / len(allratings)`,得到的是一个浮点数。浮点数在Python(以及几乎所有编程语言)中有一个经典问题:>>> 0.1 + 0.20.30000000000000004 # 不是 0.3!
这不是Python的bug,而是因为十进制小数在二进制存储时无法精确表示。在大多数情况下这个微小误差不会影响结果,但如果你需要精确比较两个浮点数,千万不要直接用==:import matha = 0.1 + 0.2print(a == 0.3) # False!print(math.isclose(a, 0.3)) # True,这才是正确做法
math.isclose()会检查两个数的差值是否在可接受的容差范围内,避免了精度问题导致的误判。数据分析完了,最后一步是用f-string生成一份漂亮的报告。f-string(Python 3.6+引入)是目前最推荐的字符串格式化方式。在字符串前加 f 前缀,花括号 {} 内可以直接嵌入变量和表达式:print(f"有效电影数:{len(clean_movies)} 部")print(f"平均评分:{avg_rating:.2f}")
:.2f 是格式说明符,表示"保留2位小数"。f-string还支持对齐控制:print(f"{'豆瓣Top250数据分析报告':^48}")
^48 表示居中对齐,总宽度48个字符。类似地,> 是右对齐,< 是左对齐。这些对齐方式在生成表格式输出时非常有用。print(f"{3201876:,}") # 输出:3,201,876
for decade in sorted(decades.keys()): bar = "█" * decades[decade] print(f" {decade}: {bar} ({decades[decade]}部)")
"█" * 5 是字符串的重复操作,生成 "█████"。这比写一个循环来拼接字符要简洁得多。 1970s: ██ (2部) 1980s: ██ (2部) 1990s: ████████████ (12部) 2000s: ████████████ (12部) 2010s: ██████ (6部)
把上面的片段整合在一起,就是一个完整的数据清洗与分析工具。你可以直接运行:"""豆瓣电影Top250数据清洗与分析工具综合运用:字符串操作、条件判断、循环、异常处理、数值计算、格式化输出"""import csvfrom collections import Counter# ========== 1. 读取原始数据 ==========rows = []with open("douban_top250_dirty.csv", "r", encoding="utf-8-sig") as f: reader = csv.reader(f) header = next(reader) for row in reader: if len(row) >= 7: rows.append(row)print(f"原始数据共 {len(rows)} 条记录\n")# ========== 2. 数据清洗 ==========clean_movies = []invalid_count = 0seen_titles = set()total_processed = 0for i, row in enumerate(rows): total_processed += 1 # 字符串清洗 title = row[1].strip() eng_title = row[2].strip() year_raw = row[3].strip() rating_raw = row[4].strip() reviews_raw = row[5].strip() quote = row[6].strip() # 异常处理:提取评分 try: rating_str = rating_raw.replace("分", "") rating = float(rating_str) assert 0 <= rating <= 10, f"评分 {rating} 超出 0-10 范围" except (ValueError, AssertionError) as e: print(f"[跳过] 第{total_processed}条《{title}》评分异常: {e}") invalid_count += 1 continue # 条件判断 + 字符串操作:提取年份 year = None for start_char in "0123456789": pos = year_raw.find(start_char) if pos != -1 and pos + 4 <= len(year_raw): year_str = year_raw[pos:pos+4] if year_str.isdigit(): year = int(year_str) break if year is None: print(f"[跳过] 第{total_processed}条《{title}》无法解析年份: {year_raw}") invalid_count += 1 continue # 字符串方法:提取评价人数 reviews_clean = reviews_raw.replace("人评价", "").replace(",", "").strip() try: if "万" in reviews_clean: review_count = int(float(reviews_clean.replace("万", "")) * 10000) else: review_count = int(reviews_clean) except ValueError: print(f"[跳过] 第{total_processed}条《{title}》评价人数异常: {reviews_raw}") invalid_count += 1 continue # 去重 title_key = title.lower() if title_key in seen_titles: print(f"[去重] 《{title}》重复,已跳过") invalid_count += 1 continue seen_titles.add(title_key) # 评分分级(多分支条件判断) if rating >= 9.5: grade = "神作级" elif rating >= 9.0: grade = "经典级" elif rating >= 8.5: grade = "优秀级" else: grade = "良好级" clean_movies.append({ "rank": total_processed, "title": title, "eng_title": eng_title, "year": year, "rating": rating, "reviews": review_count, "quote": quote, "grade": grade, })print(f"\n清洗完成:有效数据 {len(clean_movies)} 条,无效/重复 {invalid_count} 条\n")# ========== 3. 统计分析 ==========all_ratings = [m["rating"] for m in clean_movies]avg_rating = sum(all_ratings) / len(all_ratings)max_movie = max(clean_movies, key=lambda m: m["rating"])min_movie = min(clean_movies, key=lambda m: m["rating"])# 年代分布decades = Counter()for m in clean_movies: decade = (m["year"] // 10) * 10 decades[f"{decade}s"] += 1# 评分等级分布grade_dist = Counter(m["grade"] for m in clean_movies)# 评价人数Top5top5_reviews = sorted(clean_movies, key=lambda m: m["reviews"], reverse=True)[:5]# ========== 4. 格式化输出报告 ==========print("=" * 56)print(f"{'豆瓣Top250数据分析报告':^48}")print("=" * 56)print(f"有效电影数:{len(clean_movies)} 部")print(f"平均评分:{avg_rating:.2f}")print(f"最高评分:《{max_movie['title']}》{max_movie['rating']}分")print(f"最低评分:《{min_movie['title']}》{min_movie['rating']}分")print(f"\n{'【年代分布】'}")for decade in sorted(decades.keys()): bar = "█" * decades[decade] print(f" {decade}: {bar} ({decades[decade]}部)")print(f"\n{'【评分等级】'}")for grade in ["神作级", "经典级", "优秀级", "良好级"]: count = grade_dist.get(grade, 0) if count > 0: print(f" {grade}({count}部)")print(f"\n{'【评价人数 Top 5】'}")for j, m in enumerate(top5_reviews, 1): print(f" {j}. 《{m['title']}》 {m['reviews']:,}人评价")print(f"\n{'【经典短评】'}")quoted = [m for m in clean_movies if m["quote"]]for m in quoted[:8]: short = m["quote"][:20] + "..." if len(m["quote"]) > 20 else m["quote"] print(f" 《{m['title']}》—— \"{short}\"")print("\n" + "=" * 56)print("报告生成完毕。")
清洗完成:有效数据 250 条,无效/重复 5 条============================================================================================================ 1980s: ████████████████ (16部) 1990s:████████████████████████████████████████████████████████ (56部) 2000s:███████████████████████████████████████████████████████████████████████████ (75部) 2010s:████████████████████████████████████████████████████████████████████████████████ (80部)
2. 《这个杀手不太冷》 2,564,038人评价 《千与千寻》—— "最好的宫崎骏,最好的久石让。" 《星际穿越》—— "爱是一种力量,让我们超越时空感知它的存在..." 《这个杀手不太冷》—— "怪蜀黍和小萝莉不得不说的故事。"======================================================我们用一份真实的豆瓣电影数据,走完了"脏数据→清洗→分析→报告"的全流程。回顾一下每个知识点在项目中扮演的角色。字符串操作是数据清洗的基础工具。strip() 去两端空白,replace() 替换中间杂质,find() 定位子串位置,切片 [start:end] 截取指定范围的字符,isdigit() 等测试方法做预检查,lower() 统一大小写辅助去重,join() 和 * 运算符做字符串拼接和重复。这些方法之所以好用,根源在于字符串是不可变类型——每次操作都返回新字符串,原数据不会被意外修改。条件判断贯穿了整个处理流程。if-elif-else 做评分分级和年份格式判断,in 运算符做子串包含检测,三元运算符简化简单分支。多分支的条件排列顺序直接决定逻辑正确性,这是最容易踩的坑。循环结构负责批量处理。for 循环遍历每条记录,continue 跳过无效数据,enumerate() 同时获取索引和元素。选择合适的数据结构(比如用集合做去重查找)是循环优化的关键。异常处理是程序健壮性的保障。try-except 捕获非法数据,assert 做防御性检查,else 和 finally 处理收尾逻辑。健壮的程序不是"不会出错",而是"出错后能优雅地恢复"。数值类型与格式化完成最后的分析和呈现。浮点数精度陷阱要用 math.isclose() 规避,整除 // 做数据分组,f-string 的格式说明符(:,.2f、^48、>)让报告对齐美观。这些知识点不是孤立的——在实际项目中,一条数据的处理往往同时涉及字符串清洗、条件判断和异常处理。编程能力本质上不是"记住多少语法",而是"面对一个问题时,知道该组合使用哪些工具来解决它"。下次遇到"学了语法不知道怎么用"的时候,去找一份真实的小数据,动手把它洗干净。所有的语法知识会在实战中自然而然地串起来。