Python模拟MapReduce分治思想 | 从单文件统计到大文件拆分聚合 学习笔记

最近啃大数据的 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 的核心就是四个字:分而治之。整套流程拆成三步:

  1. Split 拆分:把超大文件切成一堆小文件,每个小文件都能单独放进内存
  2. Map 映射:对每个小文件分别做统计,生成局部结果
  3. 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,再去学框架就会好理解很多。

相关推荐
quantdash_cc1 小时前
数据 API 的稳定性应该如何长期监控?从量化数据监控体系到 QuantDash 实践
开发语言·python·数据分析·量化交易·股票数据·quantdash
水龙吟啸1 小时前
华为研发岗AI方向9.9机考题复盘&分析
人工智能·python·算法·华为
nanawinona1 小时前
先跑通小流程,再扩展量化功能
人工智能·python
hongyucai1 小时前
一个碗引发的血案
python·几何学·拓扑学
佳児素花痴╮1 小时前
C++速通2
开发语言·c++·算法
东莞市云毅网络有限公司1 小时前
企业知识库问答的自动化评测:构建 golden set 与回归脚本
python·rag·检索·企业知识库·评测集
似水এ᭄往昔1 小时前
【Qt】--常用控件(输入类控件)
开发语言·qt
snow@li1 小时前
服务器运维:Node.js 安装笔记(Alibaba Cloud Linux 4)
笔记
江湖人称菠萝包1 小时前
【Windows】《深入浅出Windows API程序设计:编程基础篇》笔记-Chapter9-对话框
windows·笔记