14.3 创建爬虫模块
14.1节通过genspider命令已经创建了一个基于CrawlSpider类的爬虫模板,类名称为ZhihuComSpider,当然现在什么功能都没有。在爬虫模块中,我们需要完成登录、解析当前用户信息、动态加载和解析关系用户ID的功能。
14.3.1 登录知乎
首先完成登录操作,要在进行爬取之前完成,因此需要重写start_requests方法。首先通过Firebug抓取登录post包,数据包如下:
POST /login/phone_num HTTP/1.1 Host: www.zhihu.com User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Fire fox/50.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Referer: https:// www.zhihu.com/ Content-Length: 226 Cookie:q_c1=e0dd3bc1e3fb43e192d66da7772d2255|1480422237000|1480422237000; _xsrf= 1451c04ef9f408b69a94196b71c64b07; l_cap_id="ZTA5NzQ3MGE0MzY1NGIxY2IzYWU2OGFm YzQwNmI0OWY=|1480422237|35a69271df72e4b588c34ecb427af38e9ad1ffa3"; cap_id="Yz Q2N2YwYzcyNGQ1NDYyYjgwMmE1MjU0OGExZjJmNjU=|1480422237|7bc08fee3c211de4195635e58ed3b1e0ca5e098d";n_c=1;_zap=9c0e1ceb-6476-413e-837c-67b6c1043993; __utma=51854390.1255459250.1480422230.1480422230.1480422230.1; __utmb=51854390.2.10.1480422230;__utmc=51854390; __utmz=51854390.1480422230.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utmv=51854390.000--|3=entry_date=20161129=1;__utmt=1; d_c0="AGAC_lUW7AqPTjlRMkFmtScK_2Dnm6X_imE=|1480422238"; r_cap_id="OGE3ZjllOTcwYWE3NDI2NzkxNjBlODc0M2I2MDNlOTI=|1480422239|e47878a0b995e909ceb 878c9ef385a5eb833913f";l_n_c=1; login="ZTY1OTEyNDI0YTNjNDc3N2JmMzYzZjU1MmFiM2I5N2 I=|1480422257|649e6261252f60f825c82e73ad032f1fbab0d331" Connection: keep-alive _xsrf=1451c04ef9f408b69a94196b71c64b07 captcha_type=cn password=xxxxxxxxxxxx phone_num=xxxxxxxxxxx
登录成功后,返回json数据,内容为登录成功,格式如下:
{"r":0,"msg": "\u767b\u5f55\u6210\u529f"}
通过第10章的讲解,我们知道_xsrf属于隐藏表单,需要从页面进行提取,提取出来之后,构造Request请求,进行发送。总共需要3个方法,start_requests用于进入登录页面,start_login用于构造登录请求,after_login用于判断登录状态,如果登录成功则开始爬取起始url。在ZhihuComSpider中代码如下:
def start_requests(self): # 首先进入登录界面 return [Request('https:// www.zhihu.com/# signin', callback=self.start_login, meta={'cookiejar':1}) ] def start_login(self,response): # 开始登录 self.xsrf = Selector(response).xpath( '// input[@name="_xsrf"]/@value' ).extract_first() return [FormRequest( 'https:// www.zhihu.com/login/phone_num', method='POST', meta={'cookiejar': response.meta['cookiejar']}, formdata={ '_xsrf': self.xsrf, 'phone_num': 'xxxxxxx', 'password': 'xxxxxx', 'captcha_type': 'cn'}, callback=self.after_login )] def after_login(self,response): if json.loads(response.body)['msg'].encode('utf8') == "登录成功": self.logger.info(str(response.meta['cookiejar'])) return [Request( self.start_urls[0], meta={'cookiejar':response.meta['cookiejar']}, callback=self.parse_user_info, errback=self.parse_err, )] else: self.logger.error('登录失败') return
代码中的phone_num和password字段都用“xxxxxx”代替,大家可以填写自己的账号和密码。
因为要使用到Cookie,需要在Settings中将COOKIES_ENABLED设置为True。同时还需要伪装一下默认请求的User-Agent字段,因为默认的User-Agent字段包含Scrapy关键字,容易被发现,所以在Setting中将USER_AGENT设置为:Mozilla/5.0(Windows NT 6.1;WOW64;rv:50.0)Gecko/20100101Firefox/50.0。
14.3.2 解析功能
登录成功后,我们开始解析数据。首先解析用户信息数据,如图14-1所示。和之前的开发手段一样,通过Firebug分析页面,可以确定用户信息的XPath表达式如下:
·用户头像链接user_image_url://img[@class='Avatar Avatar--l']/@src
·用户昵称name://*[@class='title-section']/span/text()
·居住地location://*[@class='location item']/@title
·技术领域business://*[@class='business item']/@title
·性别gender://*[@class='item gender']/i/@class
·公司employment://*[@class='employment item']/@title
·职位position://*[@class='position item']/@title
·关注者和被关注者followees_num,followers_num第一种情况://div[@class='zm-profile-side-following zg-clear']/a[@class='item']/strong/text()
·关注者和被关注者followees_num,followers_num第二种情况://div[@class='Profile-followStatusValue']/text()
·关注者和被关注者页面跳转链接relations_url第一种情况://*[@class='zm-profile-side-following zg-clear']/a/@href
·关注者和被关注者页面跳转链接relations_url第二种情况://a[@class='Profile-followStatus']/@href
解析用户信息的功能在parse_user_info方法中实现,部分代码如下:
def parse_user_info(self,response): ''' 解析用户信息 :param response: :return: ''' user_id = os.path.split(response.url)[-1] user_image_url = response.xpath("// img[@class='Avatar Avatar--l']/@src").extract_first() name = response.xpath("// *[@class='title-section']/span/text()").extract_first() location = response.xpath("// *[@class='location item']/@title").extract_first() business = response.xpath("// *[@class='business item']/@title").extract_first() gender = response.xpath("// *[@class='item gender']/i/@class").extract_first() if gender and u"female" in gender: gender = u"female" else: gender = u"male" employment = response.xpath("// *[@class='employment item']/@title").extract_first() position = response.xpath("// *[@class='position item']/@title").extract_first() education = response.xpath("// *[@class='education item']/@title").extract_first() try: followees_num,followers_num = tuple(response.xpath("// div[@class='zm- profile-side-following zg-clear']/a[@class='item']/strong/text()").extract()) relations_url = response.xpath("// *[@class='zm-profile-side-following zg-clear']/a/@href").extract() except Exception,e: followees_num,followers_num =tuple(response.xpath ("// div[@class='Profile-followStatusValue']/text()").extract()) relations_url =response.xpath("// a[@class='Profile-followStatus']/@href"). extract() user_info_item = UserInfoItem(user_id=user_id,user_image_url=user_image_url, name=name,location=location,business=business, gender=gender,employment=employment,position=position, education=education,followees_num=int(followees_num), followers_num=int(followers_num)) yield user_info_item
其中有一点需要说明,user_id可以从响应链接中获取,链接类似于以下这种情况:
https://www.zhihu.com/people/qi-ye-59-20
对于性别的判断,可以判断提取出来的gender字段是否包含female。最后将所有的用户信息提取出来,构造成UserInfoItem返回即可。
接下来我们需要进入关注者界面和被关注者界面,上面的代码已经将relations_url提取出来,需要根据relations_url构造Request,开始进入分析用户关系的阶段。代码如下:
def parse_user_info(self,response): ...... 省略以上代码..... yield user_info_item for url in relations_url: if u"followees" in url: relation_type = u"followees" else: relation_type = u"followers" yield Request(response.urljoin(url=url), meta={ 'user_id':user_id, 'relation_type':relation_type, 'cookiejar': response.meta['cookiejar'], 'dont_merge_cookies': True }, errback=self.parse_err, callback=self.parse_relation )
代码中通过url是否包含followees来判断关系类型,然后将关系类型和用户ID添加到Request.meta字段中进行绑定。由于关注者界面和被关注者界面的页面结构一样,所以统一通过parse_relation方法进行解析,但是因为知乎通过动态加载的方式获取用户关注者,因此需要两个方法来负责用户关系的提取,一个负责进入关注者界面已经存在的20个以内的数据,另一个负责发送请求,动态获取数据。parse_relation属于前者。要提取关注者的ID,我们只需要从图14-2中提取关注者的链接即可,链接中包含关注者ID,XPath表达式如下:
// *[@class='zh-general-list clearfix']/div/a/@href
提取当前静态页面所有关注者id,代码如下:
def parse_relation(self,response): ''' 解析和我有关系的用户,只能处理前20条 :param response: :return: ''' user_id = response.meta['user_id'] relation_type = response.meta['relation_type'] relations_url = response.xpath("// *[@class='zh-general-list clearfix']/div/ a/@href").extract() relations_id = [os.path.split(url)[-1] for url in relations_url] yield RelationItem(user_id=user_id, relation_type=relation_type, relations_id=relations_id)
将user_id、relation_type、relations_id打包成RelationItem,存储并返回即可。
前20条数据提取完成后,下面我们需要构造请求,动态加载剩余的数据内容。首先看一下请求的方式和内容,请求头如下:
POST /node/ProfileFollowersListV2 HTTP/1.1 Host: www.zhihu.com User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Fire fox/50.0 Accept: */* Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate, br X-Xsrftoken: 1451c04ef9f408b69a94196b71c64b07 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Referer: https://www.zhihu.com/people/tombkeeper/followers Content-Length: 132 Cookie: q_c1=e0dd3bc1e3fb43e192d66da7772d2255|1480422237000|1480422237000; _xsrf=1451c04ef9f408b69a94196b71c64b07; l_cap_id="ZTA5NzQ3MGE0MzY1NGIxY2IzYWU2OGFmYzQwNmI0OWY=|1480422237|35a69271df72e4b588c34ecb427af38e9ad1ffa3"; cap_id="YzQ2N2YwYzcyNGQ1NDYyYjgwMmE1MjU0OGExZjJmNjU=|1480422237|7bc08fee3c211de4195635e58ed3b1e0ca5e098d"; _zap=9c0e1ceb-6476-413e-837c-67b6c1043993; __utma=51854390.1255459250.1480422230.1480422230.1480422230.1; __utmb=51854390. 16.10.1480422230; __utmc=51854390; __utmz=51854390.1480422230.1.1.utmcsr=(direct)| utmccn=(direct)|utmcmd=(none); __utmv=51854390.100-1|2=registration_date=20160504=1^3=entry_date=20160504=1; d_c0="AGAC_lUW7AqPTjlRMkFmtScK_2Dnm6X_imE=|1480422238"; r_cap_id="OGE3ZjllOTcwYWE3NDI2NzkxNjBlODc0M2I2MDNlOTI=|1480422239|e47878a0b995e909ceb878c9ef385a5eb833913f"; l_n_c=1; login="ZTY1OTEyNDI0YTNjNDc3N2JmMzYzZjU1MmFiM2I5N2I=|1480422257|649e6261252f60f8 25c82e73ad032f1fbab0d331"; a_t="2.0ADBAt9gp3wkXAAAAogVlWAAwQLfYKd8JAGAC_lUW7AoXAAAA YQJVTX8AZVgASZjhTCU3MGhlW3IVuMCb9Hsv-G4zdCSTu9oWQreLGA2bXCQPpxQM-A=="; z_c0=Mi4wQUR CQXQ5Z3Azd2tBWUFMLVZSYnNDaGNBQUFCaEFsVk5md0JsV0FCSm1PRk1KVGN3YUdWYmNoVzR3SnYwZXlfNGJn|1480423586|8fa5513790936d0788752bbedce6dc1c0b1058dd; __utmt=1 Connection: keep-alive
POST请求的数据内容如下:
method:next params:{"offset":7,"order_by":"created","hash_id":"40be3c5c5aa1d4b4be674d2f6bebebca"}
对我们来说比较关键的是请求头中的X-Xsrftoken参数,和POST请求的数据内容从何而来。
首先X-Xsrftoken数据内容是我们的登录时的xsrf参数。POST请求的数据内容可以从网页中提取,如图14-6所示。
图14-6 参数提取
所在的标记为<divclass=“zh-general-list clearfix”data-init=“{”params“:{”offset“:0,”order_by“:“created”,“hash_id”:“40be3c5c5aa1d4b4be674d2f6bebebca”},“nodename”:“ProfileFolloweesListV2”}“>,XPath表达式为//*[@class='zh-general-list clearfix']/@data-init。
经过以上分析可知,通过动态改变参数中offset的值模拟请求,就可以不断获取加载内容,parse_relation方法动态模拟的代码如下:
# 提出POST所需的参数和和我有关系的人数 users_num = response.xpath("// *[@class='zm-profile-section-name']/text()").extract_ first() users_num = int(re.search(r'\d+', users_num).group())if users_num else len(rela tions_url) # 提取要POST出去的参数 # data-init="{"params": {"offset": 0, "order_by": "created", "hash_id": # "fbbe3c439118fddec554b03734f9da99"}, "nodename": "ProfileFollowersListV2"}" data_init = response.xpath("// *[@class='zh-general-list clearfix']/@data-init"). extract_first() try: nodename =json.loads(data_init)['nodename'] params = json.loads(data_init)['params'] post_url = 'https:// www.zhihu.com/node/%s'% nodename # 下面获取剩余的数据POST if users_num > 20: params['offset'] = 20 payload = { 'method':'next', 'params':params } post_header={ 'Host': 'www.zhihu.com', 'Connection': 'keep-alive', 'Accept': '*/*', 'X-Requested-With': 'XMLHttpRequest', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/ 20100101Firefox/50.0', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'X-Xsrftoken':self.xsrf } yield Request(url=post_url,method='POST', headers=post_header, body=urlencode(payload), cookies=self.cookies, meta={'user_id':user_id, 'relation_type':relation_type, 'offset':20, 'payload':payload, 'users_num':users_num, 'cookiejar': response.meta['cookiejar'] }, callback=self.parse_next_relation, errback=self.parse_err, priority=100 ) except Exception,e: self.logger.warning('no second post--'+str(data_init)+'--'+str(e)) for url in relations_url: yield Request(response.urljoin(url=url), meta={'cookiejar': response.meta['cookiejar']}, callback=self.parse_user_info, errback=self.parse_err)
代码中判断关注者是否大于20,如果大于则模拟POST请求,获取动态数据,动态数据的解析放到了parse_next_relation方法中。
下面看一下POST请求发送后,获取的动态响应格式,然后才知道如何进行解析。响应格式如图14-7所示。
图14-7 返回数据
POST请求的响应为JSON格式,数据内容其实就是HTML代码,我们可以在HTML代码中通过XPath表达式提取用户链接,XPath表达式如下:
// a[@class="zm-item-link-avatar"]/@href
以上就是动态加载部分的全部分析,parse_next_relation方法代码如下:
def parse_next_relation(self,response): ''' 解析和我有关的人的剩余部分 :param response: :return: ''' user_id = response.request.meta['user_id'] relation_type = response.request.meta['relation_type'] payload = response.request.meta['payload'] relations_id=[] offset = response.request.meta['offset'] users_num = response.request.meta['users_num'] body = json.loads(response.body) user_divs = body.get('msg', []) for user_div in user_divs: selector = Selector(text=user_div) user_url = selector.xpath('// a[@class="zm-item-link-avatar"]/@href'). extract_first() relations_id.append(os.path.split(user_url)[-1]) # 发送请求 yield Request(response.urljoin(url=user_url), meta={'cookiejar': response.meta['cookiejar'] }, callback=self.parse_user_info, errback=self.parse_err) # 发送捕获到的关系数据 yield RelationItem(user_id=user_id, relation_type=relation_type, relations_id=relations_id) # 判断是否还有更多的数据 if offset + 20 < users_num: payload['params']['offset'] = offset+20 more_post = response.request.copy() more_post = more_post.replace( body=urlencode(payload), meta={'user_id':user_id, 'relation_type':relation_type, 'offset':offset+20, 'users_num':users_num, 'cookiejar': response.meta['cookiejar']}) yield more_post
上述代码通过解析动态响应中的HTML代码,提取其中关注者的链接,然后将关注者链接构造Request,交给parse_user_info解析用户信息,而且从链接中提取出用户user_id,构造成RelationItem交给Pipeline处理,最后判断已经获取的数据是否大于总的数据,如果没有,说明还有数据,改变offset偏移量,继续发送动态请求进行循环处理。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论