返回介绍

14.3 创建爬虫模块

发布于 2024-01-26 22:39:51 字数 15065 浏览 0 评论 0 收藏 0

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文