Scala语言编写的爬虫应用-爬取一部小说

这几天使用手机看玄幻小说《斗罗大陆3-龙王传说》,页面总是弹出色色的广告,不但浪费了流量延迟了加载时间,而且严重影响心情,决定写一个爬虫爬取该网站的小说部分的内容,把它保存成txt格式,直接使用手机阅读器阅读,告别烦人的广告,爽得飞起!我所爬取得小说的下载地址在本文末尾给出,可以免费下载。

  • 使用语言:Scala
  • 代码使用:下面所有的代码都是在一个.scala文件中的,复制粘贴点击运行就可以了。从第一个给定的url开始爬取符合条件的小说内容。
  • 优化:使用线程池,多个线程异步的方式获取远端的网页内容。虽然这样子代码复制度相对于同步的方式复杂一些,但是下载速度很快。
  • 总体思路:
    1、从第一个网页开始,提取所有符合条件的url,再从这些新的url对应的页面中提取新的url,直到提取完成为止。上面这个过程我们保存了所有符合条件的页面内容和对应的url。(所以我把它们保存在一个treemap中,key是url值,value是页面内容,treemap自带排序功能)。
    2、从页面内容中提取出我想要的小说内容。
    3、保存小说内容到文件系统中。
  • 其他说明:本代码先把所爬取的内容先全部保存到内存中,再存储成txt文件。(因为需要排序,所以采用这样的方式)

import java.net.{HttpURLConnection, SocketTimeoutException, URL}

import scala.collection.JavaConversions._
import java.io._
import java.util.concurrent._
import java.util.HashSet
import java.util.regex.Pattern

import scala.collection.mutable

/**
  *
  * @param startPage
  * @param outputPath 所爬取小说的存储路径,默认为当前目录下的crawl.txt文件.
  * @param filter url的过滤条件, 默认为true
  */
class Crawler2(startPage: String, outputPath: String = "./crawl.txt", filter: (String => Boolean) = (url: String) => true) {

  /**
    * 获取链接的正则表达式
    */
  private val linkRegex = """ (src|href)="([^"]+)"|(src|href)='([^']+)' """.trim.r

  /**
    * 文件类型正则
    */
  private val htmlTypeRegex = "\btext/html\b"

  /**
    * 存储符合条件的链接
    */
  private val crawledPool = new HashSet[String]

  /**
    * 连接超时时间
    */
  private val CONN_TIME_OUT = 10*1000

  /**
    * 读取超时时间超时时间
    */
  private val READ_TIME_OUT = 15*1000

  def crawl():Unit ={
    //爬取原始html页面
    val linksAndContent = doCrawlPages(startPage)
    //解析和提取有价值的内容
    linksAndContent.foreach(entry => {linksAndContent += (entry._1 -> extractTitleAndContent(entry._2))})
    //保存数据到文件系统中
    storeContent(linksAndContent, outputPath)
  }


  /**
    * 这个函数负责主要的爬取任务。它调用线程池中的一条线程来爬取一个页面,返回所爬取的页面内容和对应的url。
    * 该方法没有采用递归的方式来爬取网页内容,取而代之的是自定义一个栈结构,从而避免了大量的link造成的栈溢出和速度慢的问题。
    * 主线程需要等待其他爬取工作线程结束之后再进行下一步动作,又因为这里的爬取工作线程的数量是不确定的,这里的解决方法是
    * 让主线程循环等待,直到所有的爬取工作线程结束(正常完成任务或者超时).
    *
    * @param pageUrl
    * @return  存储链接和对应的页面数据(HTML). key:url, value:原始的html页面数据。
    */
  private def doCrawlPages(pageUrl: String):mutable.TreeMap[String,String] ={
    //创建线程池
    val threadPool: ThreadPoolExecutor = new ThreadPoolExecutor(10, 200, 3, TimeUnit.SECONDS,
      new LinkedBlockingDeque[Runnable](),
      new ThreadPoolExecutor.CallerRunsPolicy())
    //设置线程池相关属性
    threadPool.allowCoreThreadTimeOut(true)
    threadPool.setKeepAliveTime(6, TimeUnit.SECONDS)
    //存储该函数的返回值
    val result = new collection.mutable.TreeMap[String, String]()
    //用于存储每个页面符合条件的url,该栈共享于多个线程
    val LinksStack = mutable.Stack[String]()
    LinksStack.push(pageUrl)
    try{
      do{//线程池中还有任务在进行
        while(!LinksStack.isEmpty){//link栈不空
        val link = LinksStack.pop()

          val future = new FutureTask[(Int, String, Map[String, String])](() => getPageFromRemote(link))
          threadPool.execute(future)

          val pageContent = future.get(this.READ_TIME_OUT, TimeUnit.SECONDS)._2

          val tempLinks = parseCrawlLinks(link, pageContent)
          tempLinks.filter(!crawledPool.contains(_)).foreach(LinksStack.push(_))
          result += (link -> pageContent)
        }
        Thread.sleep(200)
      }while(threadPool.getActiveCount != 0)
    }finally {
      threadPool.shutdown()
    }
    result
  }


  /**
    * 连接将要爬取得网站,并下载该url的内容
    *
    * @param url
    * @return ResponseCode, page内容, headers
    */
  def getPageFromRemote(url: String):(Int, String, Map[String, String]) = {
    val uri = new URL(url);

    var conn:HttpURLConnection = null
    var status:Int = 0
    var data:String = ""
    var headers:Map[String,String] = null
    try{
      conn = uri.openConnection().asInstanceOf[HttpURLConnection];
      conn.setConnectTimeout(CONN_TIME_OUT)
      conn.setReadTimeout(this.READ_TIME_OUT)
      val stream = conn.getInputStream()
      val bufferedReader = new BufferedReader(new InputStreamReader(stream, "utf-8"))

      val strBuf = new StringBuilder()
      var line = bufferedReader.readLine()
      while (line != null) {
        strBuf.append(line)
        line = bufferedReader.readLine()
      }
      data = strBuf.toString()
      status = conn.getResponseCode()
      //根据status code判断页面是否被重定向了,从而进一步处理。这里略掉此步骤。

      headers = conn.getHeaderFields().toMap.map {
        head => (head._1, head._2.mkString(","))
      }
    }catch{
      case e:SocketTimeoutException => println(e.getStackTrace)
      case e2:Exception => println(e2.getStackTrace)
    }finally {
      if(conn != null) conn.disconnect
      crawledPool.add(url)
    }
    return (status, data, headers)
  }

  /**
    * 从HTML文件中提取符合条件的URL
    *
    * @param parentUrl
    * @param html
    * @return
    */
  private def parseCrawlLinks(parentUrl: String, html: String) = {
    val baseHost = getHostBase(parentUrl)
    val links = fetchLinks(html).map {
      link =>
        link match {
          case link if link.startsWith("/") => baseHost + link
          case link if link.startsWith("http:") || link.startsWith("https:") => link
          case _ =>
            val index = parentUrl.lastIndexOf("/")
            parentUrl.substring(0, index) + "/" + link
        }
    }.filter {
      link => !crawledPool.contains(link) && this.filter(link)
    }
    println("find " + links.size + " links at page " + parentUrl)
    links
  }

  /**
    * 通过正则表达式从页面中提取出所有的url,包含不符合条件的
    *
    * @param html
    * @return
    */
  private def fetchLinks(html: String):Set[String] = {
    val list = for (m <- linkRegex.findAllIn(html).matchData if (m.group(1) != null || m.group(3) != null)) yield {
      if (m.group(1) != null) m.group(2) else m.group(4)
    }

    list.filter {
      link => !link.startsWith("#") && !link.startsWith("javascript:") && link != "" && !link.startsWith("mailto:")
    }.toSet
  }

  /**
    * 根据第一个url得到该网站的基本url
    *
    * @param url
    * @return
    */
  private def getHostBase(url: String) = {
    val uri = new URL(url)
    val portPart = if (uri.getPort() == -1 || uri.getPort() == 80) "" else ":" + uri.getPort()
    uri.getProtocol() + "://" + uri.getHost() + portPart
  }

  /**
    * 判断所爬取的网页是不是文本类型
    *
    * @param headers
    * @return
    */
  private def isTextPage(headers: Map[String, String]) = {
    val contentType = if (headers contains "Content-Type") headers("Content-Type") else null
    contentType match {
      case null => false
      case contentType if contentType isEmpty => false
      case contentType if Pattern.compile(htmlTypeRegex).matcher(contentType).find => true
      case _ => false
    }

  }

  /**
    * 从原始的html文件中提取出自己想要的内容。所以需要修改这个函数来适应不同的网站页面。
    *
    * @param html
    * @return
    */
  private def extractTitleAndContent(html:String):String ={
    val h1StartIndex = html.indexOf("<h1>")
    val h1EndIndex = html.indexOf("</h1>", h1StartIndex)
    val contentStartIndex = html.indexOf("<div>", h1EndIndex)
    val contentEndIndex = html.indexOf("</div>", contentStartIndex)
     if(h1StartIndex < 0 || h1EndIndex < 0 || contentStartIndex < 0 || contentEndIndex < 0)
      return ""

    val title = html.substring(h1StartIndex+4, h1EndIndex)

    val content = html
      .substring(contentStartIndex+5, contentEndIndex)
      .replaceAll("<br />|&nbsp;+|\t+", "")

    s"${title}\n${content}\n\n"
  }

  /**
    * 保存所爬取得数据到文件系统中。
    *
    */
  private def storeContent(linksAndContent:mutable.TreeMap[String, String], outputPath:String):Unit = {
    val writer = new BufferedWriter(new FileWriter(new File(outputPath)))
    val values = linksAndContent.valuesIterator
    while(values.hasNext){
      writer.write(values.next())
    }
    writer.close()
  }
}

object CrawlTest{
  def main(args:Array[String]): Unit ={
    new Crawler2("http://www.7caimi.com/xiaoshuo/2/",
      "crawl.txt",
      filter = (url:String) => url.contains("http://www.7caimi.com/xiaoshuo/2/")).crawl()
  }
}

小说下载地址:http://pan.baidu.com/s/1mhWyZ8k

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值