基於Python實現MapReduce

changwan發表於2024-05-14

一、什麼是MapReduce

首先,將這個單詞分解為Map、Reduce。

  • Map階段:在這個階段,輸入資料集被分割成小塊,並由多個Map任務處理。每個Map任務將輸入資料對映為一系列(key, value)對,並生成中間結果。
  • Reduce階段:在這個階段,中間結果被重新分組和排序,以便相同key的中間結果被傳遞到同一個Reduce任務。每個Reduce任務將具有相同key的中間結果合併、計算,並生成最終的輸出。

舉個例子,在一個很長的字串中統計某個字元出現的次數。

from collections import defaultdict
def mapper(word):
    return word, 1

def reducer(key_value_pair):
    key, values = key_value_pair
    return key, sum(values)
def map_reduce_function(input_list, mapper, reducer):
    '''
    - input_list: 字元列表
    - mapper: 對映函式,將輸入列表中的每個元素對映到一個鍵值對
    - reducer: 聚合函式,將對映結果中的每個鍵值對聚合到一個鍵值對
    - return: 聚合結果
    '''
    map_results = map(mapper, input_list)
    shuffler = defaultdict(list)
    for key, value in map_results:
        shuffler[key].append(value)
    return map(reducer, shuffler.items())

if __name__ == "__main__":
    words = "python best language".split(" ")
    result = list(map_reduce_function(words, mapper, reducer))
    print(result)

輸出結果為

[('python', 1), ('best', 1), ('language', 1)]

但是這裡並沒有體現出MapReduce的特點。只是展示了MapReduce的執行原理。

二、基於多執行緒實現MapReduce

from collections import defaultdict
import threading

class MapReduceThread(threading.Thread):
    def __init__(self, input_list, mapper, shuffler):
        super(MapReduceThread, self).__init__()
        self.input_list = input_list
        self.mapper = mapper
        self.shuffler = shuffler

    def run(self):
        map_results = map(self.mapper, self.input_list)
        for key, value in map_results:
            self.shuffler[key].append(value)

def reducer(key_value_pair):
    key, values = key_value_pair
    return key, sum(values)
def mapper(word):
    return word, 1
def map_reduce_function(input_list, num_threads):
    shuffler = defaultdict(list)
    threads = []
    chunk_size = len(input_list) // num_threads
    
    for i in range(0, len(input_list), chunk_size):
        chunk = input_list[i:i+chunk_size]
        thread = MapReduceThread(chunk, mapper, shuffler)
        thread.start()
        threads.append(thread)
    
    for thread in threads:
        thread.join()

    return map(reducer, shuffler.items())

if __name__ == "__main__":
    words = "python is the best language for programming and python is easy to learn".split(" ")
    result = list(map_reduce_function(words, num_threads=4))
    for i in result:
        print(i)

這裡的本質一模一樣,將字串分割為四份,並且分發這四個字串到不同的執行緒執行,最後將執行結果歸約。只不過由於Python的GIL機制,導致Python中的執行緒是併發執行的,而不能實現並行,所以在python中使用執行緒來實現MapReduce是不合理的。(GIL機制:搶佔式執行緒,不能在同一時間執行多個執行緒)。

三、基於多程序實現MapReduce

由於Python中GIL機制的存在,無法實現真正的並行。這裡有兩種解決方案,一種是使用其他語言,例如C語言,這裡我們不考慮;另一種就是利用多核,CPU的多工處理能力。

from collections import defaultdict
import multiprocessing

def mapper(chunk):
    word_count = defaultdict(int)
    for word in chunk.split():
        word_count[word] += 1
    return word_count

def reducer(word_counts):
    result = defaultdict(int)
    for word_count in word_counts:
        for word, count in word_count.items():
            result[word] += count
    return result

def chunks(lst, n):
    for i in range(0, len(lst), n):
        yield lst[i:i + n]

def map_reduce_function(text, num_processes):
    chunk_size = (len(text) + num_processes - 1) // num_processes
    chunks_list = list(chunks(text, chunk_size))

    with multiprocessing.Pool(processes=num_processes) as pool:
        word_counts = pool.map(mapper, chunks_list)

    result = reducer(word_counts)
    return result

if __name__ == "__main__":
    text = "python is the best language for programming and python is easy to learn"
    num_processes = 4
    result = map_reduce_function(text, num_processes)
    for i in result:
        print(i, result[i])

這裡使用多程序來實現MapReduce,這裡就是真正意義上的並行,依然是將資料切分,採用並行處理這些資料,這樣才可以體現出MapReduce的高效特點。但是在這個例子中可能看不出來很大的差異,因為資料量太小。在實際應用中,如果資料集太小,是不適用的,可能無法帶來任何收益,甚至產生更大的開銷導致效能的下降。

四、在100GB的檔案中檢索資料

這裡依然使用MapReduce的思想,但是有兩個問題

  1. 檔案太大,讀取速度慢

解決方法:

使用分塊讀取,但是在分割槽時不宜過小。因為在建立分割槽時會被序列化到程序,在程序中又需要將其解開,這樣反覆的序列化和反序列化會佔用大量時間。不宜過大,因為這樣建立的程序會變少,可能無法充分利用CPU的多核能力。

  1. 檔案太大,記憶體消耗特別大

解決方法:

使用生成器和迭代器,但需獲取。例如分塊為8塊,生成器會一次讀取一塊的內容並且返回對應的迭代器,以此類推,這樣就避免了讀取記憶體過大的問題。

from datetime import datetime
import multiprocessing
def chunked_file_reader(file_path:str, chunk_size:int):
    """
    生成器函式:分塊讀取檔案內容
    - file_path: 檔案路徑
    - chunk_size: 塊大小,預設為1MB
    """
    with open(file_path, 'r', encoding='utf-8') as file:
        while True:
            chunk = file.read(chunk_size)
            if not chunk:
                break
            yield chunk

def search_in_chunk(chunk:str, keyword:str):
    """在檔案塊中搜尋關鍵字
    - chunk: 檔案塊
    - keyword: 要搜尋的關鍵字
    """
    lines = chunk.split('\n')
    for line in lines:
        if keyword in line:
            print(f"找到了:", line)

def search_in_file(file_path:str, keyword:str, chunk_size=1024*1024):
    """在檔案中搜尋關鍵字
    file_path: 檔案路徑
    keyword: 要搜尋的關鍵字
    chunk_size: 檔案塊大小,為1MB
    """
    with multiprocessing.Pool() as pool:
        for chunk in chunked_file_reader(file_path, chunk_size):
            pool.apply_async(search_in_chunk, args=(chunk, keyword))
        
if __name__ == "__main__":
    start = datetime.now()
    file_path = "file.txt"
    keyword = "張三"
    search_in_file(file_path, keyword)
    end = datetime.now()
    print(f"搜尋完成,耗時 {end - start}")

最後程式執行時間為兩分鐘左右。

相關文章