最近啃大数据的 MapReduce,光看概念总觉得虚,索性用 Python 纯手写了一遍完整流程。从最基础的单文件统计,到大文件拆分、Map 局部计算、Reduce 汇总,不用搭任何大数据框架,就能把分治的核心逻辑摸得明明白白。
这篇是我整理的实操笔记,代码都拆成了小段,每一步干啥都讲清楚,入门练手很合适。
一、先热个身:小文件直接统计
先从最简单的场景入手,比如统计班级人数、统计各城市过车数量。逻辑都是统一的:读数据 → 提取关键字段 → 字典计数。
例子1:统计每个班级的学生人数
对应代码:Demo01_student.py
第一步先把文件内容读进来,顺便去掉每行末尾的换行符:
python
with open("student.txt", mode="r", encoding="utf-8") as f:
# 逐行读取并去除首尾空白
students = [line.strip() for line in f.readlines()]
数据格式是 姓名,年龄,班级,我们只需要最后一列的班级信息。这里用 map 加 lambda 提取,写起来比较简洁:
python
# 按逗号分割,取每行最后一列的班级
clazzs = list(map(lambda line: line.split(",")[-1], students))
接下来就是核心的统计逻辑,用字典存结果。key 是班级,value 是人数;遍历的时候判断,不在字典里就初始化为 1,已经存在就加 1:
python
clazz_num = {}
for clazz in clazzs:
if clazz not in clazz_num:
clazz_num[clazz] = 1
else:
clazz_num[clazz] += 1
print(clazz_num)
例子2:统计每个城市的车流量
对应代码:Demo02_gateway_records.py 和上面的逻辑完全一致,只是把"班级"换成了"城市"。车辆数据里第三列是城市,所以下标取 2 就行:
python
with open("gateway_records.txt", mode="r", encoding="utf-8") as f:
cars = [line.strip() for line in f.readlines()]
# 提取第三列:城市
citys = [line.split(",")[2] for line in cars]
# 字典计数统计
city_num = {}
for city in citys:
if city not in city_num:
city_num[city] = 1
else:
city_num[city] += 1
print(city_num)
到这里都是基础操作,几十兆的小文件随便跑。但如果是几个 G 甚至更大的文件?一次性读进内存直接就崩了,这就轮到 MapReduce 的分治思想出场。
二、分治思路:大文件拆成小块逐个处理
MapReduce 的核心就是四个字:分而治之。整套流程拆成三步:
- Split 拆分:把超大文件切成一堆小文件,每个小文件都能单独放进内存
- Map 映射:对每个小文件分别做统计,生成局部结果
- Reduce 归约:把所有局部结果汇总,得到最终的全局结果
打个比方,数一仓库乒乓球,一个人数太慢,就分成好多小盒,每个人数一盒(这就是 Map),最后把所有人的数加起来(这就是 Reduce)。
三、Split 阶段:大文件切分
对应代码:Demo03_split.py 我们按行数切分,比如每 10000 行存成一个小文件。
跑之前记得先建好 split 文件夹,不然写入会报错。
先初始化变量:分片编号、行数计数器,并且打开第一个输出文件:
python
page = 0 # 分片文件的序号
count = 0 # 当前分片已经写了多少行
# 打开第一个分片文件,准备写入
split_f = open(f"split/part-{page}", mode="w", encoding="utf-8")
然后循环读取大文件,边读边往分片文件里写:
python
with open("gateway_records.txt", mode="r", encoding="utf-8") as f:
line = f.readline()
while line: # 读到文件末尾就退出循环
count += 1
split_f.write(line)
line = f.readline()
关键逻辑:当行数达到 10000 时,关闭当前文件,序号 +1,打开新的分片文件,计数器归零:
python
if count == 10000:
print(f"已生成:part-{page}")
count = 0
page += 1
split_f.close()
split_f = open(f"split/part-{page}", mode="w", encoding="utf-8")
跑完之后,一个大文件就被切成了一堆 part-x 命名的小文件,每个都是 10000 行。
四、Map 阶段:每个分片单独统计
对应代码:Demo04_map.py 现在 split 文件夹里有一堆小文件了,我们遍历每个文件,各自统计城市数量,把结果存到 map 文件夹里。
同样,提前建好 map 文件夹。
先拿到 split 目录下所有的文件名:
python
import os
base_dir = "split\\"
files = os.listdir(base_dir)
循环处理每一个文件,里面的统计逻辑和最开始的小文件统计一模一样:
python
for file in files:
file_path = base_dir + file
with open(file_path, mode="r", encoding="utf-8") as f:
lines = [line.strip() for line in f.readlines()]
# 提取城市列
citys = [line.split(",")[2] for line in lines]
# 当前分片的局部统计
city_num = {}
for city in citys:
if city not in city_num:
city_num[city] = 1
else:
city_num[city] += 1
统计完之后,把这个分片的结果写入新文件,格式是 城市,数量:
python
# 保存当前分片的统计结果
with open(f"map\\{file}", mode="w", encoding="utf-8") as w_f:
for city, num in city_num.items():
w_f.write(f"{city},{num}\n")
w_f.flush()
print(f"{file_path}:处理完成!")
这一步做完,每个小文件都有了自己的统计结果。这就是 Map 阶段:只负责自己这块数据的计算,互不干扰。
五、Reduce 阶段:汇总所有结果
对应代码:Demo05_reduce.py 最后一步,把 Map 阶段所有文件的结果合起来,相同城市的数量累加。
记得建好 reduce 文件夹。
还是先遍历 map 目录下的所有结果文件,准备一个总字典存最终结果:
python
import os
base_dir = "map\\"
files = os.listdir(base_dir)
city_num = {} # 最终结果字典
逐行读取每个文件,把城市和数量拆出来,累加到总字典里:
python
for file in files:
file_path = base_dir + file
with open(file_path, mode="r", encoding="utf-8") as f:
lines = [line.strip() for line in f.readlines()]
for line in lines:
city = line.split(",")[0]
num = int(line.split(",")[1])
if city not in city_num:
city_num[city] = num
else:
city_num[city] += num
最后把最终结果保存下来就大功告成了:
python
with open("reduce/part-0", mode="w", encoding="utf-8") as w_f:
for city, num in city_num.items():
w_f.write(f"{city},{num}\n")
w_f.flush()
六、附:模拟数据生成脚本
对应代码:gen_data.py 如果想自己测试大文件效果,可以用这个脚本生成卡口过车数据。字段包括车牌号、卡口编号、城市、车辆品牌、道路编号、车速、行驶方向。
比如生成随机车牌号的函数:
python
def generate_plate_number():
provinces = ["京"]
letters = string.ascii_uppercase
numbers = ''.join(random.choices(string.digits, k=5))
return random.choice(provinces) + random.choice(letters) + numbers
生成城市、品牌就更简单了,给个列表随机选:
python
def generate_city():
cities = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "南京", "重庆", "天津"]
return random.choice(cities)
最后主函数里控制生成条数,直接写入文件:
python
if __name__ == "__main__":
file_path = "gateway_records2.txt"
num_records = 10000000 # 生成1千万条,自己测试可以调小
generate_and_save_records(file_path, num_records)
print(f"已生成 {num_records} 条数据并保存到 {file_path}")
写在最后
整套跑下来就能很直观地感受到 MapReduce 的思想:拆分大任务 → 并行处理小任务 → 合并所有结果。
真实的 Hadoop 里还涉及排序、分区、多节点调度这些,但核心逻辑和我们手写的这个流程是一致的。用 Python 跑一遍小 demo,再去学框架就会好理解很多。