一、任務描述
上周,老板給我一個小任務:批量生成手機號碼並去重。給了我一個Excel表,里面是中國移動各個地區的可用手機號碼前7位(如下圖),里面有十三張表,每個表里的電話號碼前綴估計大概是八千個,需要這些7位號碼生成每個都生成后4位組成11位手機號碼,也就說每一個格子里面的手機號碼都要生成一萬個手機號。而且還有,本來服務器已經使用了一部分手機號碼了,要在生成的號碼列表里去掉已經使用過的那一批。已經使用過的這一批號碼已經導出到了一批txt文本里,約4000w,每個txt有10w個號碼,里面有重復的,老板估計實際大概是3000w左右。老板可以給我分配使用一個16G內存、8核CPU的Windows服務器來跑程序。
二、任務分析
要處理海量數據,所以程序的執行效率和占用內存不能太高,應該能在開發機的4G內存下也大概跑得動,即關掉所有編程IDE和服務器軟件,只用Notepad++和瀏覽器(用來查資料)的情況下不會卡機。這次任務可能會用到的技術有:程序Excel的處理,文件的遍歷和讀寫,大型數組的操作,多線程並發。預估任務完成周期:一周(日常工作正常進行的前提下)。
三、技術分析
PHP:很熟悉,但是執行效率和內存占用不夠好,可能會卡機,要實現多線程似乎有點復雜。(有待斟酌)
Javascript:較熟悉,執行效率和內存占用不太清楚,但是弱類型通常都比較堪憂,各種回調比較干擾思維不順手。(不考慮)
Java:略懂,學起來和寫起來都比較麻煩,開發效率比較慢。(有待斟酌)
C#:沒用過,較難學(比JAVA易,比腳本難)。(有待斟酌)
C/C++:略懂,數組處理、多線程這兩個似乎比較難搞。(不考慮)
Python:沒用過,據說很容易學,有個研究生同學用它來做物理運算等,執行效率應該不低。(試試看)
於是就打着試試看的心態,打開了菜鳥教程(Runoob)的Python教程大概看了一下,目錄中有幾個數組(List、元組、字典)、文件IO、File和多線程,看了一下例程果然好簡單。再度娘了一下python處理Excel,果斷簡單快捷!於是開啟了玩蛇之路。
四、合並(./4000w/hebing.py)
本文開頭說到,有4000w的已用號碼列表,但是里面是有重復的,而后面的處理都需要用到這些號碼。而看到這400多個txt文件加起來大小約500M,所以全部讀進去再進行處理也可以承受。所以先把這些文件讀進去合並去重,輸出成為一個txt文件。最后得到的號碼有1800w多條,輸出txt文件大小約209M。

#!/usr/bin/python # -*- coding: UTF-8 -*- #把所有的文本都讀出來 i = 1; txtStrAll=''; while i<= 402: #402 fileobj = open('list-data- ('+ str(i) +').txt'); txtStr = fileobj.read(); txtStrAll += txtStr+'\n'; fileobj.close(); i+=1; txtArr = txtStrAll.splitlines(); #分割成數組 print len( txtArr ); #39905386 txtArr = list(set(txtArr)); #去重 print len( txtArr ); #18316857 #合並成字符串,for in會要很久很久 newStr = ''; newStr = '\n'.join( txtArr ); #寫入txt newFileObj = open('list-dataAll.txt', "wb"); newFileObj.write( newStr ); newFileObj.close();
其中對數組內的元素進行合並去重的那一句是 txtArr = list( set(txtArr) ) 。很神奇對吧,這兩個是什么函數?其實這兩個都是轉換類型的函數。先把它轉換成了 set 類型,再轉換為 list 類型(列表/數組)。python的set(集合)類型是一個無序不重復的元素集,所以list轉換為set之后就自動去重了,當然同時順序也會被打亂了,不過這里的順序不重要就不用管它啦。
最后數組轉換為字符串也是直接用字符串拼接數組就轉換了,不要用for循環,非常非常耗時間的。
五、Excel處理(./preNum.py)
根據網上例程直接讀取Excel第一個表里面的內容出來,合並成數組轉成字符串存到文本里面去。在轉成字符串的時候發現報錯似乎是說數據類型不對,才知道原來python與PHP、JS不同,是強類型的=_=!於是先在Excel里把表里面的數據轉換成字符串格式(excel里准確叫文本格式),轉換后excel表里面的數據格左上角是有綠色的小三角形的。每個表單獨處理生成一個文件,一個文件里面大概有八千個手機號碼前綴。

#!/usr/bin/python # -*- coding: UTF-8 -*- #讀取Excel,把每個表的 7位 號碼都讀取出來,存入文件 #共有13個表(三位數前綴), #到python官網下載http://pypi.python.org/pypi/xlrd模塊安裝。 #安裝方法:進入下載的 xlrd的文件夾,執行命令 python setup.py install import sys reload(sys) sys.setdefaultencoding('utf-8'); #解決'ascii' codec can't encode character, Python2默認是GB2312編碼 import xlrd exel = xlrd.open_workbook('cellnum.xlsx'); tblIndex = 1; #第幾個表 table1 = exel.sheets()[tblIndex]; #第N個表 ncols = table1.ncols; #列數 #print ncols; i=0; dataAll = []; while( i< ncols): dataArr = table1.col_values( i ); # print dataArr; for num in dataArr[:]: #每列中的數字格 if not num.isdigit() : dataArr.remove(num); dataAll.append( dataArr ); i += 1; #dataAll 是一個表的數據List #print dataAll; #寫入txt newStr = ''; for eCol in dataAll: colStr = '\n'.join( eCol ); newStr += colStr+'\n'; newFileObj = open('exel-dataAll'+str(tblIndex)+'.txt', "wb"); newFileObj.write( newStr ); newFileObj.close();
六、生成號碼並去掉已用過的
首先來想想,有十三個表,一個表里面有大概八千個號碼前綴,每個前綴生成一萬個號碼,每個號碼要與前面所說的1800萬的已用手機號碼進行比對去重。你會怎么做呢???
↓↓
↓↓
我的想法是,分成十三次來做,每次一個表,每個表中八千多個號碼,使用多線程,八千多個線程,每個線程生成一萬個號碼並與那1800萬個號碼一 一比對。
其中生成和去重的核心代碼如下,每生成一個號碼的時間大概是0.5秒。所以估計了一下時間,10000*0.5s ≈ 83min,八千個線程大概要一個半小時左右。
#這里是重復的號碼 #fileobj = open('testBugNum.txt'); #測試的 fileobj = open('list-dataAll.txt'); #實際的 bugTxtStr = fileobj.read(); fileobj.close(); bugTxtArr = bugTxtStr.splitlines(); #已用過的號碼的列表 while j < 10000: #生成一萬個同前綴號碼 newNum = str( int(txtNum)*10000+j ); j += 1; # print newNum; if not newNum in bugTxtArr: #如果不在已用過的號碼列表里 numArr.append( '+86'+newNum ); print '+86'+newNum;
我在本機大概運行了一下,觀察了幾分鍾,似乎線程創建得比較慢,有些已經跑到了十幾個了,有的才剛創建線程。線程調度嘛,不按照順序嘛,除了輸出很亂以外,似乎也沒有什么其它問題。於是就上傳到服務器上去跑了,然后再過了一會就下班回家了。第二天回到公司,連上服務器看看,出乎意料啊,看到輸出的信息里面,那些線程才跑到 三百多,天吶什么時候才能跑到一萬啊。而且還有坑爹的是,偶爾就看到有些線程創建失敗⊙o⊙
七、思考思考(./doData.py)
從前面的運行信息來看,這里使用多線程似乎並沒有加快程序的運行啊,這是為什么呢?如果不能用多線程,那么生成比對的地方就要改成另外更高效的方式了,有嗎?
第一問,從網上找到答案,確實說在計算密集型程序中,多線程比單線程更糟,因為一個CPU就那么幾個核,不同的線程還是一樣要占用CPU資源,再加上線程調度的時間和空間,真是天坑。第二問,PHP中有從一個數組去除另一個數組的函數(官方說法叫做數組的差集 array_diff() ),那么python應該也會有這樣的函數,先成一萬個號碼的數組再進行差集 會不會比原來 每個號碼對比再合並效率快呢?
實踐了一下,證明確實效率高了極多極多,python中的數組差集是這個樣子的 numArr = list( set(numArr) - set(bugTxtArr) ) 測了一下,大概三秒完成一批號碼(一批約等於一萬個號碼),之前是半秒一個號碼\( ^▽^ )/。但也注意現在生成的一萬個號碼排序是亂的,因為中間轉換成的set類型是無序的,如果需要從小到大排序,那還要再加個函數排序一下,問了老板說不用按順序,那就直接這樣了。
期間調試的時候發現,使用多線程有時會報錯 Unhandled exception in thread started by sys.excepthook is missing 之類的,網上查資料說是因為主進程已經執行完畢,那么其創建的線程就會被關掉。所以我的做法就是讓主進程最后為一直執行空語句,很像當年用C語言做單片機的做法呢→_→雖然最后不用多線程了,直接單線程處理,安全穩定。

#!/usr/bin/python # -*- coding: UTF-8 -*- import thread from time import sleep #生成號碼的函數,傳入號碼前綴 def generate(txtNum): # print txtNum; numArr = []; #用來存號碼數組的 numArrStr = ''; #用來寫入文件的 global doneNum; global bugTxtArr; global totalNum; global tblIndex; j = 0; while j < 10000: #生成一萬個同前綴號碼 newNum = str( int(txtNum)*10000+j ); j += 1; # print newNum; numArr.append( newNum ); #每個對比,但是非常耗時間 # if not newNum in bugTxtArr: #如果不在已用過的號碼列表里 # numArr.append( '+86'+newNum ); # #print '+86'+newNum; # 先全部合並,再數組去除(list 差集運算) # numArr = list( set(numArr).difference(set(bugTxtArr)) ); numArr = list( set(numArr) - set(bugTxtArr) ); numArrStr = '\n'.join(numArr); #存入文件 newFileObj = open('cellNum'+str(tblIndex)+'/'+str(txtNum)+'.txt', "wb"); newFileObj.write( numArrStr ); newFileObj.close(); print '-----TASK--------'+str(txtNum)+'--OK-------------'; doneNum += 1; #完成的任務數加一 if doneNum == totalNum: print '++++++---ALL---TASK---DONE---+++++++++++++++'; #完成所有任務 #########################################函數結束##################################### #定義要處理的表 tblIndex = 9; #把一個表合並后的數據讀出來 fileobj = open('exel-dataAll'+str(tblIndex)+'.txt'); txtStr = fileobj.read(); fileobj.close(); txtArr = txtStr.splitlines(); #分割成數組 totalNum = len( txtArr ); #print txtArr; #這里是重復的號碼 #fileobj = open('testBugNum.txt'); #測試的 fileobj = open('list-dataAll.txt'); #實際的 bugTxtStr = fileobj.read(); fileobj.close(); bugTxtArr = bugTxtStr.splitlines(); #已用過的號碼的列表 #print bugTxtArr; print totalNum; doneNum = 0; num = 0; for txtNum in txtArr: if txtNum.isdigit() : generate(txtNum); #生成號碼 #多線程,有多少個前綴就多少個線程,每個線程生成一萬個號碼 #測試發現執行效率沒有提高,而內存消耗大很多,查資料了解到計算密集型任務用多線程沒幫助的 # try: # thread.start_new_thread( generate, (txtNum,) ) # except: # print "Error: "+str(txtNum)+" no Thread"; # errfileobj = open('error-log'+str(tblIndex)+'.txt', 'a'); # errfileobj.write( "Error: "+str(txtNum)+" no Thread\n" ); # errfileobj.close(); # num += 1; # if num > 10: # break; #sleep(1); #print 'sleep end'; #while 1==1 : #主程序一直執行,以防線程提早結束 # pass;
八、合並整理(./hbData.py)
經過了大概六個小時的號碼生成,最后就是把一個Excel表生成的八千多個文件整理,每十個文件合成一個,每個文件約十萬個號碼,每個號碼前面加上 “+86” 。就遍歷一下目錄,沒什么技術點就不詳說了。

#!/usr/bin/python # -*- coding: UTF-8 -*- import os def hebing( filesArr ): global tblIndex; i = 0; newStr = ''; filesArrLen = len(filesArr); #print filesArr; for file in filesArr: txtStrAll = ''; fileobj = open('cellNum'+str(tblIndex)+'/'+filesArr[i]); txtStr = fileobj.read(); fileobj.close(); txtStrAll += txtStr+'\n'; txtArr = txtStrAll.splitlines(); #分割成數組 #全部在開頭加上+86 arrLen = len(txtArr); #print arrLen; for j in range(0, arrLen): txtArr[j] = "+86"+txtArr[j]; #轉成字符串 newStr += '\n'.join(txtArr); i += 1; #每10個文件寫入txt,注意最后的不足十個的時候 if not i%10 : #print i; newFileObj = open('resList'+str(tblIndex)+'/'+str(i)+'.txt', "wb"); newFileObj.write( newStr ); newFileObj.close(); newStr = ''; elif i==filesArrLen : #print i; newFileObj = open('resList'+str(tblIndex)+'/'+str(i)+'.txt', "wb"); newFileObj.write( newStr ); newFileObj.close(); newStr = ''; #把所有的文本都讀出來 tblIndex = 1; rootDir = os.getcwd()+"\cellNum"+str(tblIndex); #print rootDir; for parent,dirs,files in os.walk(rootDir): # print files; #得到一個文件名List,按文件名排序的 # hebing( files[0:15] ); #測試 hebing( files ); """ i = 1; txtStrAll=''; while i<= 402: #402 fileobj = open('list-data- ('+ str(i) +').txt'); txtStr = fileobj.read(); txtStrAll += txtStr+'\n'; fileobj.close(); i+=1; txtArr = txtStrAll.splitlines(); #分割成數組 print len( txtArr ); #39905386 txtArr = list(set(txtArr)); #去重 print len( txtArr ); #18316857 #合並成字符串,for in會要很久很久 newStr = ''; newStr = '\n'.join( txtArr ); #寫入txt newFileObj = open('list-dataAll.txt', "wb"); newFileObj.write( newStr ); newFileObj.close(); """
后話,總共只有十三個表,這些程序稍微改一下,執行十三次就行了。值得注意的是,我這里的程序幾乎每個都有一個全局變量 tblIndex, 是以防一文件里面一個個修改目錄名和文件名,疏忽有可能導致的數據覆蓋。
總結
- 使用腳本語言有一個很重要的要點:要盡量用語言提供的函數,不要自己實現算法,尤其是循環的那種,執行速度不在一個數量級。
- 處理大批量的數據,要拆分步驟,生成中間文件。大量數據復雜操作要小批量小批量地慢慢調試,結果無誤才逐步切換成真實數據。
- 多線程在運算密集型的場景中是沒有用武之處的,就算CPU是多核也沒什么用,反而會造成順序隨機不易觀察,線程不穩定容易出錯,線程間切換內存消耗加大等弊端。
- 據說GPU可以用來挖礦、暴力破解等,對本場景這種高並發、簡單邏輯的運算應該也非常適用,以后可能要用得上GPU編程(求推薦教程)。