kotlin爬虫——利用lambda减少代码重用
kotlin中很多的语句结构都是表达式,比如try{ … }catch(){ … }、if … else …在书写上比Java简单许多。Java的句法较为啰嗦,在制作爬虫程序时远不如Python和nodejs简单。
在爬虫程序中,nodejs和Python有健全的库,如node的puppeteer模块,由谷歌开发,可以模拟chrome环境,可以执行事件,可以安装监听器,可以爬取动态网页。相比之下,Java就显得十分费力,在这方面的使用较为繁琐。kotlin吸收的许多语言的语法糖,而kotlin的lambda表达式有点类似Js的回调函数,使得写法上比Java更为简单,灵活,大大降低代码量。
kotlin很容易对异常进行处理,使得它尽可能不报错。因为这样的报错不是十分必要的,反而会终断程序.
同时,kotlin允许编写扩展函数,我们可以基于此写一写函数,使得原来的库更加好用。同时这写扩展函数和包内的函数使用起来没有任何区别。
这里,我爬取豆瓣图书Top250,只在列表界面进行爬取,不进入每一本书的页面。
读取和写入表格使用POI包,XSSFWorkBook对象可以读写xlsx格式文件。
表格文件层级关系是workbook>sheet>row>cell
首先,写表格的流程十分固定,创建WorkBook,传入输入流,向WorkBook写入数据,将WorkBook写入进文件的输出流。
于是将向workbook写入数据的过程用一个函数表示,我们只需补充这个函数数就行了.为了提高性能,将其定义为内联函数。
import org.apache.poi.ss.usermodel.CellType
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.xssf.usermodel.XSSFSheet
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
inline fun writeXLSX(fileName: String, operateFile: (XSSFWorkbook) -> Unit) {
//如果文件不存在,就创建一个新文件
if (!File(fileName).exists()) {
File(fileName).createNewFile()
}
val fIS = FileInputStream(fileName)
//这里使用try{}catch{},如果输入流为空,就创建一个空的WorkBook
val workbook = try {
XSSFWorkbook(fIS)
} catch (e: Exception) {
XSSFWorkbook()
}
//这里调用lambda函数,可以对其自定义
operateFile(workbook)
val out = FileOutputStream(fileName)
workbook.write(out)
fIS.close()
out.close()
workbook.close()
}
这样当我们向表格写数据时,只需表格 的文件路径,和补充写数据的过程。
writeXLSX("D:\\IdeaProjects\\spider\\src\\main\\java\\org\\wang\\spider\\out.xlsx") {
// 这里写上对表格的数据,使用it便可以得到函数的自变量,一个XSSFWorkBook对象
}
接下来,写一个扩展函数,可以向sheet中的一行默认从0开始写入一个数组,当然是可以通过关键字start修改的,isCreated表示是重新创建还是获取行
import org.apache.poi.ss.usermodel.CellType
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.xssf.usermodel.XSSFSheet
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
fun Sheet.writeArray(data: Array<String>, row: Int, start: Int = 0, isCreate: Boolean = true) {
val currentRow = if (isCreate) {
createRow(row)
} else {
getRow(row)
}
for ((index, d) in data.withIndex()) {
currentRow.createCell(index + start, CellType.STRING).setCellValue(d)
}
}
然后,写一个函数用于创建sheet,即使sheet名字重复也不会报错
import org.apache.poi.ss.usermodel.CellType
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.xssf.usermodel.XSSFSheet
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
fun XSSFWorkbook.creatSheetIfNotExit(name: String): XSSFSheet = try {
getSheet(name)
} catch (e: IllegalArgumentException) {
createSheet(name)
} catch (e: NullPointerException) {
createSheet(name)
}
然后我们可以写一个函数用于测量程序运行的时间
import org.apache.poi.ss.usermodel.CellType
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.xssf.usermodel.XSSFSheet
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
fun measureTime(operation: () -> Unit): Long {
val t1 = System.currentTimeMillis()
operation()
val t2 = System.currentTimeMillis()
return t2 - t1
}
我们可以看到,kotlin的lambda就像js的回调函数一样,使用起来十分方便
接下来,对豆瓣的网页解析。解析网页使用的是Jsoup包,这个包和js
的用法几乎完全相同,可以使用css3选择器,对元素进行精确的选择。同时对其中一些极为特殊的书不做处理了,比如圣经,论语
这里同样使用lambda,处理返回的每一项的结果:
关键部分:
<td valign="top">
<div class="pl2">
<a href="https://book.douban.com/subject/1007305/" onclick=""moreurl(this,{i:'0'})"" title="红楼梦">
红楼梦
</a>
</div>
<p class="pl">[清] 曹雪芹 著 / 人民文学出版社 / 1996-12 / 59.70元</p>
<div class="star clearfix">
<span class="allstar50"></span>
<span class="rating_nums">9.6</span>
<span class="pl">(
314870人评价
)</span>
</div>
<p class="quote" style="margin: 10px 0; color: #666">
<span class="inq">都云作者痴,谁解其中味?</span>
</p>
<span class="rr">
<a name="1007305" class="j ll colbutt a_add2cart add2cart" href="javascript:;"><span><em>加入购书单</em></span></a>
<span class="hidden">已在<a href="https://book.douban.com/cart">购书单</a></span>
</span><br class="clearfix">
<span class="gact">
<a href="/wish/197805944/update?add=1007305" name="sbtn-1007305-wish" class="j a_collect_btn" rel="nofollow">想读</a>
</span>
<span class="gact">
<a href="/do/197805944/update?add=1007305" name="sbtn-1007305-do" class="j a_collect_btn" rel="nofollow">在读</a>
</span>
<span class="gact">
<a href="/collection/197805944/update?add=1007305" name="sbtn-1007305-collect" class="j a_collect_btn" rel="nofollow">读过</a>
</span>
</td>
import org.jsoup.nodes.Document
inline fun parserDomOfDouban(dom: Document, callBack: (Array<String>, Int) -> Unit) {
/* result[0] : 中文名,
result[1] : 原名,
result[2] : 作者,
result[3] : 译者,
result[4] : 出版社,
result[5] : 出版时间,
result[6] : 定价,
result[7] : 评分,
result[8] : 评价人数,
result[9] : 一句话简介,
*/
val items = dom.select("tr.item td[valign=top]:nth-child(2)")
items.mapIndexed {
index, it ->
val result = Array(10) {
"" }
val bookName = it.select("div.pl2")
val cName = bookName.select("a").text()
val fName = it.select("div.pl2 > span").text()
result[0] = cName
result[1] = fName
val infos = it.select("p.pl")[0].text()
// println(infos)
val infosSplit = infos.split("/")
when (infosSplit.size) {
5 -> {
result[2] = infosSplit[0]
result[3] = infosSplit[1]
result[4] = infosSplit[2]
result[5] = infosSplit[3]
result[6] = infosSplit[4]
}
4 -> {
result[2] = infosSplit[0]
result[4] = infosSplit[1]
result[5] = infosSplit[2]
result[6] = infosSplit[3]
}
6 -> {
result[2] = infosSplit[0]
result[3] = infosSplit[1]
result[4] = infosSplit[2]
result[5] = infosSplit[3]
result[6] = infosSplit[4] + " ; " + infosSplit[5]
}
}
result[7] = it.select("span.rating_nums").text()
result[8] = it.select("div.star.clearfix span:nth-child(3)").text().substringBefore('人').substring(2)
result[9] = it.select("span.inq").text()
callBack(result, index)
}
}
mian函数运行程序
fun main() {
val t = measureTime {
for (i in 0..9) {
val baseUrl = "https://book.douban.com/top250?start=${
i * 25}"
val dom = Jsoup.connect(baseUrl).get()
writeXLSX("D:\\IdeaProjects\\spider\\src\\main\\java\\org\\wang\\spider\\out.xlsx") {
wb ->
val sheet = wb.creatSheetIfNotExit("豆瓣图书Top250")
val title = listOf(
"中文名", "原名", "作者", "译者", "出版社", "出版时间", "定价",
"评分", "评价人数", "一句话简介"
).toTypedArray()
sheet.writeArray(title, 0)
parserDomOfDouban(dom) {
data, index ->
sheet.writeArray(data, index+i*25+1)
}
}
}
}
println("花费时间为 $t ms")
}
然后就大功告成了