zoukankan      html  css  js  c++  java
  • 用Scrapy框架开发的一个爬虫项目

      技术栈:python + scrapy + tor

      为什么要单独开这么一篇随笔,主要还是在上一篇随笔"一个小爬虫的整体解决方案"(https://www.cnblogs.com/qinyulin/p/13219838.html)中没有着重介绍Scrapy,包括后面几天也对代码做了Review,优化了一些性能,觉得还是应该把自己的劳动成果打个标,也怕后面需要的时候记不住,所以还是规规矩矩的写一篇随笔用来记录,话不多说,上干货。

      Scrapy框架,我的理解就是在Spider中请求url,在这个请求过程中,我们会用到中间件,用来劫持网络请求,对请求Request进行一些头部信息、代理的封装,然后在返回对象Response中也可以做一些处理,把获取到的网页,通过bs4解析标签元素,转换成自己需要的信息,当拿到信息的时候,可以把信息包装成对象,通过pipe管道进行数据清理,然后再进行数据存储(可以存本地文件,也可以调用API存数据库),具体的原理可以参考下面的链接https://blog.csdn.net/qq_34120459/article/details/86711728,然而在实际做的过程当中,我一次性要对于产品的评论爬成千上万条,而且还要针对失败后的断点续爬,所以我就放弃了数据管道清洗方式,把所有的业务逻辑都放在Spider里面进行,总的来说,这样做有悖于Scrapy的数据扭转原理,但是没办法,和很多人交流过,貌似目前的方式至少是可行的。

           首先来一个爬虫Spider的代码缩略图: 

           

      这里的Init函数主要是用来做一些初始化工作。详细代码如下:

     1     #初始化函数
     2     def __init__(self,saveType=1):
     3         self.keywords = []#关键词数组
     4         self.totalObj = {}#所有关键词结果的对象
     5         # self.over = True #这个参数暂时没用,观察了之后可以删除
     6         self.startTime = time.strftime("%b_%d_%Y_%H_%M_%S", time.localtime()) #开始时间,用来写入json文件名
     7         self.saveType = saveType#来源类型,用来区分是手动还是自动
     8         self.Count = 10000#定义每个Asin爬取的最大评论数量
     9         self.resCount = 0#最终发送给服务器的请求条数
    10         self.getCount = 0#用来计算keywords的索引是否完成了所有查询,每次成功之后索引+1
    11         self.successArr = []#成功发送的Asin数组
    12         self.loseArr = []#失败发送的Asin数组
    13 
    14         #########下面的代码是用来读取之前的评论数据,每次完成之后会吧数据存在这个JSON文件里面,如果中途中断下次会读取上一次的数据。
    15         review_list_file = open("data/review_detail_crawler_all.json","w+")
    16         self.review_list = review_list_file.readlines()

      

      接着进入入口函数start_requests,因为我在爬虫的时候需要定位到US,所以先模拟了一个表单请求。然后setSession和getAsinKeywords都是从服务器拿数据并进行处理,这里是自己的业务代码就不贴了。

     1     #爬虫入口函数 
     2     def start_requests(self):
     3         data = {
     4             "locationType":"LOCATION_INPUT",
     5             "zipCode": "10001",
     6             "storeContext": "hpc",
     7             "deviceType": "web",
     8             "pageType": "Detail",
     9             "actionSource": "glow",
    10         }
    11         yield scrapy.FormRequest("https://www.amazon.com/gp/delivery/ajax/address-change.html", method="POST",formdata=data, headers={'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'},dont_filter=True, callback=self.setSession,errback=self.ceshi)

       

      主要说一下parse_post_data这个函数,主要是根据获取到的网页信息,格式化并生成标签树,通过bs4插件拿到数据,注释写得比较清楚了,代码如下:

     1     #成功回调函数,先是判断是否被封,如果被封就调用Tor代理更换IP,如果没被封就跟着解析。
     2     def parse_post_data(self, response):
     3         asin = response.meta["asin"]
     4         title = BeautifulSoup(response.text, 'lxml').title
     5         title = title.string if not title is None else ""
     6         if "Robot Check" in title:
     7             print("IP被封了Spider")
     8             yield from self.renew_connection(asin)
     9         else:
    10                 #抓取该单品从开卖到现在的所有 Review
    11                 dom = BeautifulSoup(response.text, 'lxml')
    12                 try:
    13                     #获取到评论列表
    14                     ids = dom.find(id="cm_cr-review_list").select(".review")
    15                     #如果评论列表长度为0则说明爬完了。
    16                     if len(ids) == 0:
    17                         self.totalObj[asin]["over"] = True
    18                         logger.warning("{}没有下一页终止爬取数据".format(asin))
    19                     #循环获取到的评论列表,取出数据。因为是第一次做,里面获取的方法有点杂,
    20                     #因为有太多未知的错误,所以用了try方法来赋值,里面的参数没注释,可以结合API看。
    21                     for id in ids:
    22                         if self.totalObj[asin]["over"] == True or len(self.totalObj[asin]["list"]) >= self.Count:
    23                             break
    24                         obj = {}
    25                         obj["page"] = self.totalObj[asin]["pageNumber"]
    26                         obj["reviewId"] = id.attrs["id"]
    27                         
    28                         obj["title"] = id.select(
    29                             ".review-title span:first-child")[0].string
    30                         obj["username"] = id.select(".a-profile-name")[0].string
    31                         obj["content"] = id.select(".review-text-content")[0].get_text()
    32                         try:
    33                             obj["reviewDate"] = id.select(".review-date")[0].get_text()
    34                         except  Exception as e:
    35                             obj["reviewDate"] = ""
    36                         
    37                         try:
    38                             obj["voteNum"] = 0 if not len(id.select(".cr-vote-text")) else id.select(".cr-vote-text")[0].string
    39                         except Exception as e:
    40                             obj["voteNum"] = 0
    41                         try:
    42                             obj["score"] = id.select(".a-icon-alt")[0].string
    43                         except Exception as e:
    44                             obj["score"] = 0
    45                         
    46                         #如果遇到reviewId评论,说明之前已经爬取过,就把当前的over状态置为True
    47                         if obj["reviewId"] == self.totalObj[asin]["reviewId"]:
    48                             self.totalObj[asin]["over"] = True
    49                             logger.warning("{}返回reviewId终止爬取{}".format(asin,obj["reviewId"]))
    50                         else:
    51                             self.totalObj[asin]["list"].append(obj)
    52                         print(obj)
    53                         pass
    54                 except Exception as e:
    55                     pass
    56                 #上面是解析了当前页面的数据,下面的代码是用来判断是否爬完。
    57                 try:
    58                     #这里是用来判断当前Asin评论的总页码
    59                     if self.totalObj[asin]["totalStr"] == "":
    60                         try:
    61                             totalStr = dom.select("#filter-info-section .a-size-base")[0].string
    62                             self.totalObj[asin]["totalStr"] = totalStr
    63                         except Exception as e:
    64                             pass
    65                     else:
    66                         totalStr = self.totalObj[asin]["totalStr"]
    67                     #totalStr示例:Showing 1-20 of 2,442 reviews
    68                     countArr = totalStr.split(' ')
    69                     fNum = countArr[1].split('-')[1]#当前数量20
    70                     tNum = countArr[3]#总数量2442
    71                     #如果当前数量大于等于总数量,表示已经爬完。
    72                     if int(fNum.replace(",","")) >= self.Count:
    73                         self.totalObj[asin]["over"] = True
    74                         logger.warning("{}大于{}条数据终止爬取".format(asin,self.Count))
    75                     if fNum == tNum:
    76                         #最后一页,把当前asin的结束标志置为True
    77                         self.totalObj[asin]["over"] = True
    78                         logger.warning("{}最后一页{}终止爬取数据".format(asin,fNum))
    79                     print("这是{}第{}页".format(asin,self.totalObj[asin]["pageNumber"]))
    80                     #防止log信息太多,10条录入一次。
    81                     if self.totalObj[asin]["pageNumber"] % 10 == 0:
    82                         logger.warning("这是{}第{}页".format(asin,self.totalObj[asin]["pageNumber"]))
    83                 except Exception as e:
    84                     pass
    85                 
    86                 #如果当前Asin的over为false,说明没有遇到reviewId和还有下一页,则把当前Asin的页码加1继续爬。
    87                 if self.totalObj[asin]["over"] == False:
    88                     self.totalObj[asin]["pageNumber"] += 1
    89                     yield from self.getViewForAsin(asin)
    90                 else:
    91                     #如果当前Asin的over为True,说明当前Asin已经爬完,把当前Asin的数据发送到服务器,
    92                     #而且根据keywords数组进行下一个Asin的爬取,如果爬取Asin的长度大于等于keywords长度,
    93                     #说明整个爬取过程已经完成,不进行任何操作,进入close流程。
    94                     logger.warning("{}完成了一次请求,准备发送数据.".format(asin))
    95                     yield from self.sendSingelData(asin)
    96                     
    97                     self.getCount += 1
    98                     if self.getCount <= len(self.keywords) - 1:
    99                         yield from self.getViewForAsin(self.keywords[self.getCount])

      

      最后就是close函数的代码:

     1     #爬虫关闭的钩子函数
     2     def close(self, reason, spider):
     3         # 打开公共设置文件,读取search_asin_index值,并读取对应的search_asin_index文件的文本,转换成数组并组合成发送的数据
     4         if self.resCount == len(self.keywords):
     5             logger.warning("全部产品的评论发送完成,共发送{}次".format(self.resCount))
     6         else:
     7             logger.warning("此次爬虫未全部爬完数据,共发送{}次".format(self.resCount))
     8         logger.warning("成功的产品有{}".format(self.successArr))
     9         logger.warning("失败的产品有{}".format(self.loseArr))
    10         self.file = open('data/review_detail_crawler_all.json'.format(self.startTime,time.strftime("%H_%M_%S", time.localtime())), 'wb')
    11         self.file.write(json.dumps(self.totalObj).encode())
    12         self.file.close()
    13         pass


      在确定用Scrapy框架之前,我也是去体验了一把requests库和selenium,requests的话做一些简单的爬虫需求还是可以,不能规模化;selenium的话主要还是通过模拟用户行为来进行数据的爬取,主要用于自动化测试的场景,在一些需要复杂操作的爬虫还是可以的,但是如果用在大规模爬取项目是非常耗时的,所以我最终还是选取了Scrapy框架。而且在后期因为要频繁爬取,容易触发亚马逊的反爬策略,所以又研究了Tor网络,这个是用来隐藏服务器IP,通过代理去进行爬虫请求,这个从最终的代码量来看其实不大,但是在研究的过程也是备受煎熬,因为在百分百成功之前的每一步都是在反思为什么不行,从拿到需求到熟悉框架,到最后完成上线,然后再review优化代码差不多1个月时间,特别是最开始在了解了框架的原理,但是却不能解决自己的需求反复去寻求解决方案是最难熬的,不过当在一次次试错后找到最后的解决方案,还是很有成就感。

      其实Scrapy的东西还很多,我用到的只是很小的一部分,希望在后面有迭代需求的时候可以再继续研究,下面贴出项目中用到的一些名词和对应的网址,也希望看到这篇文章的小伙伴如果在研究Scrapy遇到问题,可以进行留言或者私信交流,学无止境,我们一直在路上。

    Python教程(我推荐廖雪峰的):https://www.liaoxuefeng.com/wiki/1016959663602400

    Scrapy官网:https://scrapy.org/

    requests:https://requests.readthedocs.io/en/master/

    基础爬虫+selenium教程(我就是看着这位小伙伴的连载入的门):https://www.cnblogs.com/Albert-Lee/p/6238866.html

    bs4:https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/

    tor:

      linux版本:https://medium.com/@mimizhang55555/%E5%9C%A8scrapy%E4%B8%AD%E4%BD%BF%E7%94%A8tor%E4%BB%A3%E7%90%86-20a0f07c14b2

      win版本:https://www.cnblogs.com/kylinlin/archive/2016/03/04/5242266.html

     

  • 相关阅读:
    Jquery复习DAY1(jQuery 选择器 jQuery 样式操作 jQuery 效果)
    git知识
    简单分析vue计算属性和methods方法的区别
    vue生命周期
    elementUI的级联选择器Cascader 高度不匹配问题高度溢出错误
    .Net Core3.1 常用的服务注入
    判断是否为AJAX请求
    JWT权限验证
    CodeSmith介绍
    常见ORM介绍
  • 原文地址:https://www.cnblogs.com/qinyulin/p/13223677.html
Copyright © 2011-2022 走看看