分享一下前段学习Scala做的一个爬虫程序。
【关于爬虫】
接触爬虫的时间并不长,发现python在这个领域有很大的份额。虽然也用过python,但是始终觉得动态语言做这种“严谨“工作还是不如Java,当然更没法和Scala比。
总结一下爬虫的主要困难:
痛点1:网断,大量爬取时,各种超时错是司空见惯,需要有良好的重试机制防止被打断。
痛点2:验证码,一般大网站都有反爬机制,当一定时间访问过多,就会跳转到验证码页面(携程就有)甚至禁止访问。另外,做模拟登陆的时候这个更是是绕不开的坎,真正的爬虫噩梦。详见: 知乎上一篇《为什么有些验证码看起来很容易但是没人做自动识别的?》 黄凯迪的文章。
痛点3:速度瓶颈,一般爬取数据都是百万级甚至更多,为了获得好的速度,多线程是必不可少的,单机不能满足需求就要分布式。但是这个又会增加上面两个问题的解决难度。
【关于反爬虫】
为什么聊这个?当然是知己知彼百战不殆。
网上看到一篇,还正好是携程出的,名字挺牛气。《关于反爬虫,看这一篇就够了》
【项目简述】
本篇程序用Scala+Jsoup 实现一个携程游记的爬虫,单机角度解决上面的问题。
先简要分析下携程游记,http://you.ctrip.com/travels/,作为国内数一数二的旅游类平台,携程主要通过收购小网站的方式壮大其游记规模,已经到了巨无霸级别,这次主要爬取游记目录规模 100万篇左右。由于数量过多,按照携程自己做的标签分类进行过滤,“精华”,“美图”,“典藏”,“实用”四类作为抓取对象。
【那些包?】
全部是标准库
import java.io.File import java.io.PrintWriter import java.text.SimpleDateFormat import java.util.Date import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.ConcurrentHashMap import org.jsoup.Jsoup import org.jsoup.nodes.Document import scala.collection.JavaConversions._ import scala.collection.parallel.ForkJoinTaskSupport import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.forkjoin.ForkJoinPool import scala.util.Failure import scala.util.Success import scala.util.Try
废话不多说,上程序,第一部分是纯网页分析的东东,用携程自身的地区分类索引做遍历,最大分页号发现页面上给的是错的,就花了几分钟调查了一下手写(关于爬取的页面分析,网上有很多,也是爬虫基本功,这里就不详述。):
// 携程游记一览Url,可变部分(1:地区 2:最大分页号(每页9篇游记)) val Url = "http://you.ctrip.com/travels/%s/s3-p%d.html" val ctripMap = Map( "国内" -> ("china110000", 42398), "亚洲" -> ("asia120001", 50071), "欧洲" -> ("europe120002", 2987), "大洋洲" -> ("oceania120003", 866), "非洲" -> ("africa120006", 463), "南美洲" -> ("southamerica120005", 115), "北美洲" -> ("northamerica120004", 1273), "南极洲" -> ("antarctica120481", 11) ) // 解析单页的游记,过滤出“精华”,“美图”,“典藏”,“实用”四种类型游记 def parseDoc(doc: Document) = { var allCnt, objCnt = 0 for (e <- doc.select("a.journal-item.cf")) { var tn = "" if (!e.select("span.pic-tagico-1").isEmpty()) tn += "精" if (!e.select("span.pic-tagico-2").isEmpty()) tn += "美" if (!e.select("span.pic-tagico-3").isEmpty()) tn += "实" if (!e.select("span.pic-tagico-4").isEmpty()) tn += "典" if (tn != "") { // 只保留符合条件的数据 map.put(e.attr("href"),e.attr("href") + "\t" //Url + e.select("dt.ellipsis").html + "\t" //标题 + tn + "\t" //类型名(精|美|实|典) + e.select("dd.item-user").html.replaceAll("\n", "") + "\t" //作者+发表时间 + e.select("dd.item-short").html + "\t" //摘要 + e.select("span.tips_a").html + "\t" //天数+旅游时间+花费+同伴关系 + e.select("span.tips_b").html + "\t" //tips_b + e.select("i.numview").html + "\t" //点击数 + e.select("i.want").html + "\t" //点赞数 + e.select("i.numreply").html); //回复数 objCnt += 1 } allCnt += 1 } (allCnt,objCnt) }
下面利用Try语法及递归调用,解决各种异常重试问题,注意携程有时侦测到单机IP大量访问会强制跳转到验证码页面,好在只维持一段时间,这里简化处理,休眠后再试。怎么看都比Java的trycatch 漂亮多了不是?
def sleep(i: Long) = Thread.sleep(i) val aiAll, aiCnt, aiFail: AtomicInteger = new AtomicInteger(0) val map = new java.util.concurrent.ConcurrentHashMap[String,String]() // 利用递归实现自动重试(重试100次,每次休眠30秒) def promiseGetUrl(times: Int=100, delay: Long=30000)(z: String, i: Int): Unit = { Try(Jsoup.connect(Url.format(z,i)).get()) match { case Failure(e) => if (times != 0) { println(e.getMessage); aiFail.addAndGet(1); sleep(delay); promiseGetUrl(times - 1, delay)(z,i) }else throw e case Success(d) => val (all, obj) = parseDoc(d); if (all ==0) {sleep(delay); promiseGetUrl(times - 1, delay)(z,i) }//携程跳转验证码走这里! aiAll.addAndGet(all); aiCnt.addAndGet(obj); } }
相比其他语言Scala中递归算是很常见的了,Scala还可以通过@tailrec注解确保对尾递归实施优化,当然本例并不适合。其实对于限定次数来说,大多数时候没有必要担心内存压力。
第三步,解决加速问题,使用Scala并发集合的线程池,用起来感觉像在上外挂,非常简洁。
// 并发集合多线程执行 def concurrentCrawler(zone: String, maxPage: Int, threadNum: Int) = { val loopPar = (1 to maxPage).par loopPar.tasksupport = new ForkJoinTaskSupport(new ForkJoinPool(threadNum)) // 设置并发线程数 loopPar.foreach(promiseGetUrl()(zone, _)) // 利用并发集合多线程同步抓取 output(zone) }
最后,输出结果,值得注意的是,线程池不宜设置过大,过大会导致网站反爬跳转高发反而拖慢速度,需要在不同时段尝试,我的机器上测试出白天网络比较慢30就可以了,晚上可调高一些。
// 获取当前日期 (简单机能用Java就Ok) def getNowDate():String={ new SimpleDateFormat("yyyyMMdd").format( new Date() ) } // 爬取内容写入文件 def output(zone: String) = { val writer = new PrintWriter(new File(getNowDate()+"_"+zone++".txt")) for ((_,value) <- map) writer.println(value) writer.close() } val Thread_Num = 30 //指定并发执行线程数 val t1 = System.currentTimeMillis //全体抓取 ctripMap.foreach{ m => concurrentCrawler(m._2._1, m._2._2, map, Thread_Num) map = new ConcurrentHashMap[String,String](); } //个别抓取 //val tup = ctripMap("欧洲"); concurrentCrawler(tup._1, tup._2, Thread_Num) val t2 = System.currentTimeMillis println(s"抓取数:$aiCnt 重试数:$aiFail 耗时(秒):"+(t2-t1)/1000)
到此,一个无需监控的爬虫完工,实测它可以抵御任何网络异常超时以及携程的屏蔽,不间断(休眠时间除外)执行到完成所有任务。
【执行结果】
下面是晚上8点左右,开50线程的执行结果
***********************************************************
已分析游记数:883656 好游记:26018
抓取数:26018 重试数:28 耗时(秒):4541
***********************************************************
1小时15分搞定,平均抓取速度 21.6个url/秒,考虑到单机无人值守无漏抓保证,这个速度还是比较满意了。
同时赞一下携程网给力的服务器。
【引申话题】
本篇只是爬虫一个简单小例子,还属于“屌丝”级别。在真实环境实现一个强大的爬虫要做到大规模实时要求,分布式是必不可少的,另外规避验证码这类反爬手段不能再用“傻等”的方式了,代理ip池成为必备,当然这个也是需要付出一些成本。还有webspec,selenium,PhantomJs等等神兵利器,更有高大上的图像识别技术搞定验证码,这里不一一介绍了,有兴趣的同学可以接着了解。个人感觉作为Scala的杀手级应用Akka,在实现一个分布式爬虫方面也是大有可为。
【广告时间】
如果你对Java八股一样的语法倦了,对python那样动态语言又没有安全感,就来试试Scala吧。
相关推荐
该项目为基于Scala语言的Webmagicx爬虫框架设计源码,包含136个文件,涵盖67个Java文件、47个Scala文件、8个HTML文件、3个属性文件、3个XML文件...Webmagicx是一款可配置化的爬虫框架,适用于基于Webmagic的基础架构。
Scala3,也被称为Scala 3或Dotty,是Scala编程语言的一个重大更新,旨在提高其简洁性、可读性和类型安全性。Scala3的发布标志着该语言的进一步成熟,它引入了一系列改进,旨在解决早期版本中的一些痛点,同时保持对...
Scala SDK,全称为Scala Software Development Kit,是用于开发Scala...例如Apache Spark,一个流行的分布式计算框架,就是用Scala编写的。因此,熟悉Scala SDK对于希望在JVM平台上进行高效编程的开发者来说至关重要。
使用Scala进行Web开发使用Scala进行Web开发使用Scala进行Web开发使用Scala进行Web开发使用Scala进行Web开发使用Scala进行Web开发使用Scala进行Web开发使用Scala进行Web开发使用Scala进行Web开发使用Scala进行Web开发...
- **诞生历史**:Scala起源于瑞士联邦理工学院洛桑(EPFL),由Martin Odersky在2001年开始设计,其灵感来源于Funnel——一种结合了函数式编程思想与Petri网的编程语言。Odersky之前还参与开发了Generic Java以及Sun...
- 在Scala中,我们需要创建一个`SqlSessionFactoryBuilder`,然后使用它来构建`SqlSessionFactory`。这通常在应用的初始化阶段完成。 - 配置文件(如`mybatis-config.xml`)通常包含数据源、事务管理器和Mappers的...
Scala是一种强大的多范式编程语言,它融合了面向对象和函数式编程的特性。这个"scala学习源代码"的压缩包文件很可能包含了用于教学或自我学习Scala编程的基础示例。让我们深入了解一下Scala语言的关键概念和特性。 ...
### Scala学习之路(一)—— 开发环境搭建与首个程序 #### 一、Scala简介 Scala是一种多范式编程语言,旨在实现可扩展性,并融合了面向对象编程和函数式编程的最佳特性。作为一种与Java非常相似的语言,Scala能够...
在本文中,我们将深入探讨如何使用Scala API操作HBase数据库。HBase是一个分布式、面向列的NoSQL数据库,它构建于Hadoop之上,提供实时访问大量数据的能力。Scala是一种强大的函数式编程语言,与Java虚拟机(JVM)...
Spark是一个用Scala编写的分布式计算框架,它利用Scala的简洁语法和强大的功能来构建大规模数据处理应用。Scala与Spark的紧密集成使得开发者可以编写出高效的并行和分布式代码。在Windows上配置好Scala环境后,你...
Scala是一种强大的多范式编程语言,它融合了面向对象和函数式编程的概念。这个"scala2.12.1Windows镜像包"是为Windows操作系统设计的Scala编程环境的安装包,版本号为2.12.1。Scala 2.12.x系列是其重要的一个稳定...
Scala是一种强大的多范式编程语言,它融合了面向对象和函数式编程的概念。这个"scala-2.12.10.zip"文件是Scala编程语言的特定版本——2.12.10,专为Windows操作系统设计的安装包。Scala 2.12.x系列是该语言的一个...
Scala是一种强大的多范式编程语言,它融合了面向对象和函数式编程的特性。这个压缩包文件"快学scala习题及答案详解"显然是为学习Scala的人设计的,提供了逐步学习和自我测试的资源。通过章节习题和答案,学习者可以...
标题中的“图解,Eclipse+ADT+ScalaIDE用Scala写Android程序”指的是使用Eclipse集成开发环境(IDE),Android Developer Tools (ADT)插件以及ScalaIDE扩展来编写Android应用程序的过程。这个过程涉及了Java语言替代...
Scala是一种强大的多范式编程语言,它融合了面向对象和函数式编程的特性,被广泛应用于大数据处理、分布式计算和Web开发等领域。Spark是基于Scala构建的大数据处理框架,其高性能和易用性使得Scala在大数据领域备受...
Scala 是一种多范式的编程语言,它融合了面向对象和函数式编程的特性。下面将详细解释题目中涉及的Scala知识点: 1. **var、val 和 def 的区别**: - `var` 定义可变变量,可以多次赋值。 - `val` 定义不可变变量...
Scala是一种强大的多范式编程语言,它融合了面向对象和函数式编程的特性,使得它在处理并发和大数据分析方面表现出色。"Scala实战高清讲解"这本书是学习Scala的宝贵资源,尤其对于那些希望深入理解并提升Scala技能的...
Spark是使用Scala编写的一个开源大数据处理框架,它提供了高级API,使得开发者可以快速地构建大规模数据处理应用。Spark使用Scala的Actor模型来实现并发,这使得Spark可以在多核系统或分布式集群上高效运行。 Scala...
1 scala用起来比java更灵活 2 强大的collection,可以更加方便的处理collection类的数据 3 不同于java的并行处理方法,有点像c的逻辑思路 4 开发成本比java小,但是语言学习成本比java高很多 正在阅读这本书的...
`mongo-scala-driver` 提供了一个 `MongoClients` 对象,我们可以用它来创建到 MongoDB 服务器的连接: ```scala import org.mongodb.scala._ val mongoClient = MongoClient("mongodb://localhost:27017") ``` ...