python爬虫Pragmatic系列IV
**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()直接搞定了。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225 1#-*- coding:utf-8 -*-
2#注:这里,我把赶集网首页称为主界面,首页里的公司链接及其页面称为子界面
3import os
4import re
5import sys
6import xlwt
7import xlrd
8import threading
9from bs4 import BeautifulSoup
10from time import sleep, ctime
11from urllib import urlopen, urlretrieve
12
13reload(sys)
14sys.setdefaultencoding('utf-8')
15
16class GanjiwangCrawler(threading.Thread):
17 #url表示下载的主界面,mark标识是哪个进程下载的
18 #location表明下载文件存储的文件夹,exname表明最后保存的Excel名
19 #wb是创建的Excel对象,ws是对应的sheet对象
20 def __init__(self, url, mark, location, exname, ws, wb):
21 threading.Thread.__init__(self)
22 self.url = url
23 self.mark = mark
24 self.location = location
25 self.suburls = []
26 self.exname = exname
27 self.wb = wb
28 self.ws = ws
29
30 def run(self):
31 #先下载主界面
32 self.download_pages(self.url, 'main%s.txt'%str(self.mark), self.location)
33 #分析主界面并返回主界面中包含的公司url
34 self.suburls = self.analysis_main_pages('main%s.txt'%str(self.mark), self.location)
35 #第一行依据suburls下载子界面 #第二行分析子界面并写入Excel中
36 for i,su in enumerate(self.suburls):
37 self.download_pages(su,r'file%s%s.txt'%(str(self.mark),str(i)), self.location)
38 self.analysis_sub_pages(r'file%s%s.txt'%(str(self.mark),str(i)), self.location)
39
40 def analysis_main_pages(self, fname, location):
41 suburls = []
42 filepath = location + fname
43 if os.path.exists(filepath):
44 fobj = open(filepath, 'r')
45 lines = fobj.readlines()
46 fobj.close()
47
48 soup = BeautifulSoup(''.join(lines))
49 leftBox = soup.find(attrs={'class':'leftBox'})
50 list_ = leftBox.find(attrs={'class':'list'})
51 li = list_.find_all('li')
52 href_regex = r'href="(.*?)"'
53 for l in li:
54 suburls.append('http://bj.ganji.com' + re.search(href_regex,str(l)).group(1))
55 else:
56 print('The file is missing')
57 #由于抓取的界面太多,导致赶集网会拒绝掉页面请求,这里我们修改下要抓取的公司数目(取十个)
58 return suburls if len(suburls) < 10 else suburls[0:10]
59
60 def download_pages(self, url, fname, location):
61 try:
62 urlretrieve(url, location + fname)
63 except Exception, e:
64 print 'Download page error:', url
65
66 def write_to_excel(self, record, row):
67 '该函数将给定的record字典中所有值存储到Excel相应的row行中'
68 #写入公司名称
69 companyName = record['companyName']
70 self.ws.write(row,0,companyName)
71 #写入服务特色
72 serviceFeature = record['serviceFeature']
73 self.ws.write(row,1,serviceFeature)
74 #写入服务范围
75 serviceScope = ','.join(record['serviceScope'])
76 self.ws.write(row,2,serviceScope)
77 #写入联系人
78 contacts = record['contacts']
79 self.ws.write(row,3,contacts.decode("utf-8"))
80 #写入商家地址
81 address = record['address']
82 self.ws.write(row,4,address.decode("utf-8"))
83 #写入聊天QQ
84 qqNum = record['qqNum']
85 self.ws.write(row,5,qqNum)
86 #写入联系电话
87 phoneNum = record['phoneNum']
88 phoneNum = str(phoneNum).encode("utf-8")
89 self.ws.write(row,6,phoneNum.decode("utf-8"))
90 #写入网址
91 companySite = record['companySite']
92 self.ws.write(row,7,companySite)
93 self.wb.save(self.exname)
94
95 def analysis_sub_pages(self, subfname, location):
96 filepath = location + subfname
97 f = open(filepath, 'r')
98 lines = f.readlines()
99 f.close()
100 #建立一个BeautifulSoup解析树,并提取出联系店主模块的信息(li)
101 try:
102 soup = BeautifulSoup(''.join(lines))
103 body = soup.body
104 wrapper = soup.find(id="wrapper")
105 clearfix = wrapper.find_all(attrs={'class':'d-left-box'})[0]
106 dzcontactus = clearfix.find(id="dzcontactus")
107 con = dzcontactus.find(attrs={'class':'con'})
108 ul = con.find('ul')
109 li = ul.find_all('li')
110 except Exception, e:#如果出错,即该网页不符合我们的通用模式,就忽略掉
111 return None
112 #如果该网页不符合我们的通用模式,我们就取消掉这次的分析
113 if len(li) != 10:
114 return None
115 #记录一家公司的所有信息,用字典存储,可以依靠键值对存取,也可以换成列表存储
116 record = {}
117 #公司名称
118 companyName = li[1].find('h1').contents[0]
119 record['companyName'] = companyName
120 #服务特色
121 serviceFeature = li[2].find('p').contents[0]
122 record['serviceFeature'] = serviceFeature
123 #服务提供
124 serviceProvider = []
125 serviceProviderResultSet = li[3].find_all('a')
126 for service in serviceProviderResultSet:
127 serviceProvider.append(service.contents[0])
128 record['serviceProvider'] = serviceProvider
129 #服务范围
130 serviceScope = []
131 serviceScopeResultSet = li[4].find_all('a')
132 for scope in serviceScopeResultSet:
133 serviceScope.append(scope.contents[0])
134 record['serviceScope'] = serviceScope
135 #联系人
136 contacts = li[5].find('p').contents[0]
137 contacts = str(contacts).strip().encode("utf-8")
138 record['contacts'] = contacts
139 #商家地址
140 addressResultSet = li[6].find('p')
141 re_h=re.compile('</?\w+[^>]*>')#HTML标签
142 address = re_h.sub('', str(addressResultSet))
143 record['address'] = address.encode("utf-8")
144 restli = ''
145 for l in range(8,len(li) - 1):
146 restli += str(li[l])
147 #商家QQ
148 qqNumResultSet = restli
149 qq_regex = '(\d{5,10})'
150 qqNum = re.search(qq_regex,qqNumResultSet).group()
151 record['qqNum'] = qqNum
152 #联系电话
153 phone_regex= '1[3|5|7|8|][0-9]{9}'
154 phoneNum = re.search(phone_regex,restli).group()
155 record['phoneNum'] = phoneNum
156 #公司网址
157 companySite = li[len(li) - 1].find('a').contents[0]
158 record['companySite'] = companySite
159 #将该公司记录存入Excel中
160 openExcel = xlrd.open_workbook(self.exname)
161 table = openExcel.sheet_by_name(r'CompanyInfoSheet')
162
163 self.write_to_excel(record, table.nrows)
164
165def init_excel(exname):
166 '我们初试化一个表格,并给表格一个头部,所以我们给头部不一样的字体'
167 wb = xlwt.Workbook()
168 ws = wb.add_sheet(r'CompanyInfoSheet')
169 #初始化样式
170 style = xlwt.XFStyle()
171 #为样式创建字体
172 font = xlwt.Font()
173 font.name = 'Times New Roman'
174 font.bold = True
175 #为样式设置字体
176 style.font = font
177 # 使用样式
178 #写入公司名称
179 ws.write(0,0,u'公司名称', style)
180 #写入服务特色
181 ws.write(0,1,u'服务特色', style)
182 #写入服务范围
183 ws.write(0,2,u'服务范围', style)
184 #写入联系人
185 ws.write(0,3,u'联系人', style)
186 #写入商家地址
187 ws.write(0,4,u'商家地址', style)
188 #写入聊天QQ
189 ws.write(0,5,u'QQ', style)
190 #写入联系电话
191 ws.write(0,6,u'联系电话', style)
192 #写入网址
193 ws.write(0,7,u'公司网址', style)
194 wb.save(exname)
195 return [ws, wb]
196
197def main():
198 '启动爬虫线程进行下载啦'
199 exname = r'info.xls'
200 print 'start crawler'
201 excels = init_excel(exname)
202 #初始化url
203 urls = []
204 #下载赶集网页面的个数,最多可以设为三百多,同时代表本次的线程数
205 pages = 2
206 nloops = xrange(pages)
207 for i in nloops:
208 url = 'http://bj.ganji.com/danbaobaoxian/o%s/' % str(i + 1)
209 urls.append(url)
210
211 threads = []
212 for i in nloops:
213 t = GanjiwangCrawler(urls[i], mark=i,location=r'pagestroage\\',exname=exname, ws=excels[0], wb=excels[1])
214 threads.append(t)
215
216 for i in nloops:
217 threads[i].start()
218
219 for i in nloops:
220 threads[i].join()
221
222 print 'OK, everything is done'
223if __name__ == '__main__':
224 main()
225
运行结果:
pagestroage文件夹下下载了两个main0.txt和main1.txt文件,对应两个线程。同时还下载了file0i.txt和file1j.txt文件,其中i从0到9,j也从0到9。也就是说两个线程最后从main文件中解析了url后各自下载了十个(我设定的)公司界面。info.xls中包含15条公司的记录。
我的文件目录:
插曲:
在自己开启多线程下载后发现,自己的程序经常一运行就直接退出,后来发现程序发起的url请求被赶集网给拒绝了,回复的都是机器人界面,如下图:
上图可见赶集网对抓取强度是有一定限制的,我们可以在程序中使用sleep语句来降低页面下载的速度。
后感:
考完研回校后做的第一个程序,终于认识到也有机会好好编程了。程序开发过程中总是遇到各种诡异的问题,什么编码问题,tab和空格混用问题。所幸后来都一一解决了。
未完待续。