程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Python >> Python處理海量手機號碼

Python處理海量手機號碼

編輯:Python

一、任務描述

上周,老板給我一個小任務:批量生成手機號碼並去重。給了我一個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();
hebing.py

其中對數組內的元素進行合並去重的那一句是 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();
preNum.py

六、生成號碼並去掉已用過的

首先來想想,有十三個表,一個表裡面有大概八千個號碼前綴,每個前綴生成一萬個號碼,每個號碼要與前面所說的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) ); 測了一下,大概三秒完成一批號碼(一批約等於一萬個號碼),之前是半秒一個號碼\( ^▽^ )/。

期間調試的時候發現,使用多線程有時會報錯 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 = 0;

#把一個表合並後的數據讀出來
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:
#    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;
doData.py

八、合並整理(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 = "F:\cellphone\cellNum"+str(tblIndex);
#print rootDir;
for parent,dirs,files in os.walk(rootDir):
#    print files;    #得到一個文件名List,按文件名排序的
#    hebing( files[0:15] );    #測試
    hebing( files );
hbData.py

後話,總共只有十三個表,這些程序稍微改一下,執行十三次就行了。值得注意的是,我這裡的程序幾乎每個都有一個全局變量 tblIndex, 是以防一文件裡面一個個修改目錄名和文件名,疏忽有可能導致的數據覆蓋。

總結

  1. 使用腳本語言有一個很重要的要點:要盡量用語言提供的函數,不要自己實現算法,尤其是循環的那種,執行速度真的差很遠很遠。
  2. 多線程在運算密集型的場景中是沒有用武之處的,就算CPU是多核也沒什麼用,反而會造成順序隨機不易觀察,線程不穩定容易出錯,線程間切換內存消耗加大等弊端。
  3. 據說GPU可以用來挖礦、暴力破解等,對本場景這種高並發、簡單邏輯的運算應該也非常適用,以後可能要用得上GPU編程(求推薦教程)。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved