標籤:爬蟲 多線程 python爬蟲 regex
python爬蟲Pragmatic系列IV
說明:
在上一篇部落格中,我們已經做到了從趕集網上單個首頁中抓取所有的連結,並下載下來,分析後存入Excel中。
本次目標:
在本節中,我們將使用python多線程技術從趕集網上抓取連結並分析,注意,我們這次能夠抓獲的連結數目可以遠遠大於上一篇部落格中抓獲的。
分析:
用爬蟲統計資訊那自然資料越多越好,為了擷取更多的資料,我們先研究下如何開啟上千個趕集網上公司連結。
開啟首頁(http://bj.ganji.com/danbaobaoxian/o1/),在頁面底部能夠看到一排分頁,如:
簡單分析可以發現其分頁連結請求是由A+B形式組成的,A為(http://bj.ganji.com/danbaobaoxian/),而B為(oi),其中i為數字。經過驗證後發現,i的範圍為:[1,300+)。由此,我們就可以利用以上的連結去訪問各個首頁並獲得各個首頁中包含的公司頁面連結。但是問題來了,一個首頁上公司共有九十多家,假設我們抓取十個首頁面上公司的連結,每個公司從下載到分析到寫入Excel假設需要0.2s,那麼共需要180s(=0.2*10*90)。而且當網速差的時候,所需要的時間會更長。由此,我們需要多線程來處理該問題。
學習python多線程可以看這裡:w3cshoolPython多線程。
為了滿足這次爬蟲的需要,我在原來代碼的基礎上做了以下幾個改動。
使用多線程,每個線程處理每個介面上的公司連結的下載和資訊的提取寫入,這樣並發的處理能夠使程式的效率更高而且能夠抓取更多的資訊。
在之前的部落格中,我們都是單獨的使用下載類和分析類分別進行操作,需要先運行下載類,然後在運行分析類。我們發現其實這兩個操作其實都可以抽象成趕集網上抓取資訊的子功能,並且,我們也希望這兩者能夠通過一個程式運行,這樣也減少了操作的複雜性。
於是,我們構建一個趕集網爬蟲類,將下載和分析功能彙總在一起,並且,為了適應多線程,我們讓該類繼承threading.Thread類,重寫重寫__init__()和__run__()函數,使其能夠滿足我們並發下載的需要。
在設計爬蟲類時,我們發現原先代碼中很多函數並不適合直接拿過來粘貼使用,其複用性較差,於是我們需要重構幾個函數。
對於下載而言,我們之前的使用方法是先調用getPages()來開啟url,並將開啟的網頁儲存到電腦緩衝中,使用的的是urlretrieve()函數,接著使用savePages()將剛剛儲存的網頁儲存到指定的硬碟位置。我們發現,利用urlretrieve()函數可以直接將下載的網頁下載到給定的硬碟位置,所以可以使用download_pages()直接搞定了。
代碼:
#-*- coding:utf-8 -*-#註:這裡,我把趕集網首頁稱為主介面,首頁裡的公司連結及其頁面稱為子介面import osimport reimport sysimport xlwtimport xlrdimport threadingfrom bs4 import BeautifulSoupfrom time import sleep, ctimefrom urllib import urlopen, urlretrievereload(sys)sys.setdefaultencoding('utf-8')class GanjiwangCrawler(threading.Thread):#url表示下載的主介面,mark標識是哪個進程下載的#location表明下載檔案儲存體的檔案夾,exname表明最後儲存的Excel名#wb是建立的Excel對象,ws是對應的sheet對象def __init__(self, url, mark, location, exname, ws, wb):threading.Thread.__init__(self)self.url = urlself.mark = markself.location = locationself.suburls = []self.exname = exnameself.wb = wbself.ws = wsdef run(self):#先下載主介面self.download_pages(self.url, 'main%s.txt'%str(self.mark), self.location)#分析主介面並返回主介面中包含的公司urlself.suburls = self.analysis_main_pages('main%s.txt'%str(self.mark), self.location)#第一行依據suburls下載子介面 #第二行分析子介面並寫入Excel中for i,su in enumerate(self.suburls):self.download_pages(su,r'file%s%s.txt'%(str(self.mark),str(i)), self.location)self.analysis_sub_pages(r'file%s%s.txt'%(str(self.mark),str(i)), self.location)def analysis_main_pages(self, fname, location):suburls = []filepath = location + fnameif os.path.exists(filepath):fobj = open(filepath, 'r')lines = fobj.readlines()fobj.close()soup = BeautifulSoup(''.join(lines))leftBox = soup.find(attrs={'class':'leftBox'})list_ = leftBox.find(attrs={'class':'list'})li = list_.find_all('li')href_regex = r'href="(.*?)"'for l in li:suburls.append('http://bj.ganji.com' + re.search(href_regex,str(l)).group(1))else:print('The file is missing')#由於抓取的介面太多,導致趕集網會拒絕掉頁面請求,這裡我們修改下要抓取的公司數目(取十個)return suburls if len(suburls) < 10 else suburls[0:10]def download_pages(self, url, fname, location):try:urlretrieve(url, location + fname)except Exception, e:print 'Download page error:', urldef write_to_excel(self, record, row):'該函數將給定的record字典中所有值儲存到Excel相應的row行中'#寫入公司名稱companyName = record['companyName']self.ws.write(row,0,companyName)#寫入服務特色serviceFeature = record['serviceFeature']self.ws.write(row,1,serviceFeature)#寫入服務涵蓋範圍serviceScope = ','.join(record['serviceScope'])self.ws.write(row,2,serviceScope)#寫入連絡人contacts = record['contacts']self.ws.write(row,3,contacts.decode("utf-8"))#寫入商家地址address = record['address']self.ws.write(row,4,address.decode("utf-8"))#寫入聊天QQqqNum = record['qqNum']self.ws.write(row,5,qqNum)#寫入聯絡電話phoneNum = record['phoneNum']phoneNum = str(phoneNum).encode("utf-8")self.ws.write(row,6,phoneNum.decode("utf-8"))#寫入網址companySite = record['companySite']self.ws.write(row,7,companySite)self.wb.save(self.exname)def analysis_sub_pages(self, subfname, location):filepath = location + subfnamef = open(filepath, 'r')lines = f.readlines()f.close()#建立一個BeautifulSoup解析樹,並提取出聯絡店主模組的資訊(li)try:soup = BeautifulSoup(''.join(lines))body = soup.bodywrapper = soup.find(id="wrapper")clearfix = wrapper.find_all(attrs={'class':'d-left-box'})[0]dzcontactus = clearfix.find(id="dzcontactus")con = dzcontactus.find(attrs={'class':'con'})ul = con.find('ul')li = ul.find_all('li')except Exception, e:#如果出錯,即該網頁不符合我們的通用模式,就忽略掉return None#如果該網頁不符合我們的通用模式,我們就取消掉這次的分析if len(li) != 10:return None#記錄一家公司的所有資訊,用字典儲存,可以依靠索引值對存取,也可以換成列表格儲存體record = {}#公司名稱companyName = li[1].find('h1').contents[0]record['companyName'] = companyName#服務特色serviceFeature = li[2].find('p').contents[0]record['serviceFeature'] = serviceFeature#服務提供serviceProvider = []serviceProviderResultSet = li[3].find_all('a')for service in serviceProviderResultSet:serviceProvider.append(service.contents[0])record['serviceProvider'] = serviceProvider#服務涵蓋範圍serviceScope = []serviceScopeResultSet = li[4].find_all('a')for scope in serviceScopeResultSet:serviceScope.append(scope.contents[0])record['serviceScope'] = serviceScope#連絡人contacts = li[5].find('p').contents[0]contacts = str(contacts).strip().encode("utf-8")record['contacts'] = contacts#商家地址addressResultSet = li[6].find('p')re_h=re.compile('</?\w+[^>]*>')#HTML標籤address = re_h.sub('', str(addressResultSet))record['address'] = address.encode("utf-8")restli = ''for l in range(8,len(li) - 1):restli += str(li[l])#商家QQqqNumResultSet = restliqq_regex = '(\d{5,10})'qqNum = re.search(qq_regex,qqNumResultSet).group()record['qqNum'] = qqNum#聯絡電話phone_regex= '1[3|5|7|8|][0-9]{9}'phoneNum = re.search(phone_regex,restli).group()record['phoneNum'] = phoneNum#公司網址companySite = li[len(li) - 1].find('a').contents[0]record['companySite'] = companySite#將該公司記錄存入Excel中openExcel = xlrd.open_workbook(self.exname)table = openExcel.sheet_by_name(r'CompanyInfoSheet')self.write_to_excel(record, table.nrows)def init_excel(exname):'我們初試化一個表格,並給表格一個頭部,所以我們給頭部不一樣的字型'wb = xlwt.Workbook()ws = wb.add_sheet(r'CompanyInfoSheet')#初始化樣式style = xlwt.XFStyle()#為樣式建立字型font = xlwt.Font()font.name = 'Times New Roman'font.bold = True#為樣式設定字型style.font = font# 使用樣式#寫入公司名稱ws.write(0,0,u'公司名稱', style)#寫入服務特色ws.write(0,1,u'服務特色', style)#寫入服務涵蓋範圍ws.write(0,2,u'服務涵蓋範圍', style)#寫入連絡人ws.write(0,3,u'連絡人', style)#寫入商家地址ws.write(0,4,u'商家地址', style)#寫入聊天QQws.write(0,5,u'QQ', style)#寫入聯絡電話ws.write(0,6,u'聯絡電話', style)#寫入網址ws.write(0,7,u'公司網址', style)wb.save(exname)return [ws, wb]def main():'啟動爬蟲線程進行下載啦'exname = r'info.xls'print 'start crawler'excels = init_excel(exname)#初始化urlurls = []#下載趕集網頁面的個數,最多可以設為三百多,同時代表本次的線程數pages = 2nloops = xrange(pages)for i in nloops:url = 'http://bj.ganji.com/danbaobaoxian/o%s/' % str(i + 1)urls.append(url)threads = []for i in nloops:t = GanjiwangCrawler(urls[i], mark=i,location=r'pagestroage\\',exname=exname, ws=excels[0], wb=excels[1])threads.append(t)for i in nloops:threads[i].start()for i in nloops:threads[i].join()print 'OK, everything is done'if __name__ == '__main__':main()
運行結果:
pagestroage檔案夾下下載了兩個main0.txt和main1.txt檔案,對應兩個線程。同時還下載了file0i.txt和file1j.txt檔案,其中i從0到9,j也從0到9。也就是說兩個線程最後從main檔案中解析了url後各自下載了十個(我設定的)公司介面。info.xls中包含15條公司的記錄。
我的檔案目錄:
插曲:
在自己開啟多線程下載後發現,自己的程式經常一運行就直接退出,後來發現程式發起的url請求被趕集網給拒絕了,回複的都是機器人介面,如:
可見趕集網對抓取強度是有一定限制的,我們可以在程式中使用sleep語句來降低頁面下載的速度。
後感:
考完研回校後做的第一個程式,終於認識到也有機會好好編程了。程式開發過程中總是遇到各種詭異的問題,什麼編碼問題,tab和空格混用問題。所幸後來都一一解決了。
未完待續。
python爬蟲Pragmatic系列IV