By 白熊花田(http://blog.csdn.net/whiterbear)
在上一篇博客中,我们已经做到了从赶集网上单个首页中抓取所有的链接,并下载下来,分析后存入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 os import re import sys import xlwt import xlrd import threading from bs4 import BeautifulSoup from time import sleep, ctime from urllib import urlopen, urlretrieve reload(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 = url self.mark = mark self.location = location self.suburls = [] self.exname = exname self.wb = wb self.ws = ws def run(self): #先下载主界面 self.download_pages(self.url, 'main%s.txt'%str(self.mark), self.location) #分析主界面并返回主界面中包含的公司url self.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 + fname if 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:', url def 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")) #写入聊天QQ qqNum = 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 + subfname f = open(filepath, 'r') lines = f.readlines() f.close() #建立一个BeautifulSoup解析树,并提取出联系店主模块的信息(li) try: soup = BeautifulSoup(''.join(lines)) body = soup.body wrapper = 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]) #商家QQ qqNumResultSet = restli qq_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) #写入聊天QQ ws.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) #初始化url urls = [] #下载赶集网页面的个数,最多可以设为三百多,同时代表本次的线程数 pages = 2 nloops = 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和空格混用问题。所幸后来都一一解决了。
未完待续。