立委名单/提案 Open Data / CsvToBean - day23

目标

今天写个时事题,我们来查询立委议案提案
https://ithelp.ithome.com.tw/upload/images/20211008/20138680SjT2vdaWvw.png

本日重点

  • 立法院 Open Data
  • 如何将 CSV 转成 Bean
  • DataLoader 多栏位过滤
  • ListBox
  • 自订 ListBox

为节省篇幅,本示例仅列出关键程序码,画面编排、按键 onclick、DataLoader、Grid显示...等使用方法,请参考本系列其他文章。

资料来源 : 立法院开放资料服务平台

查询现任立委

立法院提供三种资料模式,分别是 csv、txt、xls (虽然立法院开放资料服务平台上有JSON选项,但并未提供内容)
https://ithelp.ithome.com.tw/upload/images/20211008/20138680up3sbyTv4l.png

从网站资讯得知此api提供栏位如下 :

name 委员姓名 ename 委员英文姓名
sex 性别 party 党籍
partyGroup 党团 areaName 选区名称
committee 委员会 onboardDate 到职日(西元年)
degree 学历 tel 电话
experience 经历 fax 传真
addr 通讯处 picUrl 照片位址
leaveFlag 是否离职 leaveDate 离职日期(西元年)
leaveReason 离职原因

建立资料表

  • 建立存放立委资讯资料表Legislator,请开新档 V05__CreateLegislator.sql
    create TABLE Legislator(
      id bigint auto_increment PRIMARY KEY,
      term VARCHAR(3),
      name VARCHAR(50) NOT NULL,
      sex VARCHAR(10) NOT NULL,
      party VARCHAR(100),
      partyGroup VARCHAR(100),
      areaName VARCHAR(100),
      committee VARCHAR(500),
      degree VARCHAR(200),
      tel VARCHAR(200),
      experience VARCHAR(1000),
      addr VARCHAR(500),
      picUrl VARCHAR(100),
      leaveFlag VARCHAR(10),
      leaveDate VARCHAR(20),
      leaveReason VARCHAR(1000)
    );
  • 建立对应的KEntity
data class Legislator(
    override var id: Long? = null,
    var term: String? = null,
    var name: String? = null,
    var sex: String? = null,
    var party: String? = null,
    var partyGroup: String? = null,
    var areaName: String? = null,
    var committee: String? = null,
    var degree: String? = null,
    var tel: String? = null,
    var experience: String? = null,
    var addr: String? = null,
    var picUrl: String? = null,
    var leaveFlag: String? = null,
    var leaveDate: String? = null,
    var leaveReason: String? = null
):KEntity<Long> ,Serializable{
    companion object: Dao<Legislator, Long>(Legislator::class.java)
}

下载资料并转档

CSV 转成 Bean 的资料转换使用 OpenCSV 套件,请开启 build.gradle.kts,导入opencsv 套件

    implementation("com.opencsv:opencsv:5.5.2")

将下载写在download()方法,转档後储存到资料表Legislator

    fun download(){
        URL("https://data.ly.gov.tw/odw/usageFile.action?id=9&type=CSV&fname=9_CSV.csv")
            .readText().apply {
                CsvToBeanBuilder<Legislator>(StringReader(this))
                    .withType(Legislator::class.java)
                    .withSeparator(',')
                    .withIgnoreLeadingWhiteSpace(true)
                    .withIgnoreEmptyLine(true)
                    .build()
                    .parse().forEach {
                        it.save()
                    }
            }
    }

第2,3行,从立法院开放资料服务平台取得资料
第4行,使用CsvToBeanBuilder()方法进行转换,参数型态为Reader,所以将下载回来的字串进行转换
第5行,parser型态
第6行,分隔符号
第7行,忽略前置空白
第8行,忽略空白行
第11行,因LegislatorKEntity类,直接使用save()方法储存到资料库。

将下载资讯以 Grid 显示

设定 Grid 资料来源为 EntityDataLoader,首次执行时因资料表 Legislator 尚无任何资料,grid为空白。

    grid = grid<Legislator> {
        setDataLoader(Legislator.dataLoader)
        addColumnFor(Legislator::name).setHeader("委员姓名")
       (略)    
    }

下载立委资料并储存後,更新Grid

    download()
    grid.dataProvider.refreshAll()

展开立委 Detail

点选grid row 展开detail,希望detail显示相片、所属委员会、服务处地址等资讯。

所属委员会

所属委员会栏位committee内容如下:

第10届第1会期:经济委员会;第10届第2会期:经济委员会;第10届第3会期:经济委员会;第10届第3会期:纪律委员会;第10届第4会期:经济委员会;

;分隔每笔资料,先将资料整理一下再放入List,最後以ListBox显示

    val committees = mutableListOf<String>()
    data.committee?.let {
        it.split(";").forEach { committees.add(it) }
    }
    val listBox = ListBox<String>()
    listBox.setWidth("35%")
    listBox.setItems(committees)

完整内容如下

    setSelectionMode(Grid.SelectionMode.NONE)
    val renderer = ComponentRenderer<HorizontalLayout, Legislator> { data: Legislator ->
        val horizontalLayout = HorizontalLayout()
        val image = Image()
        with(image){
            isExpand = false
            width = "10%"
            setMaxWidth(200F, Unit.PIXELS)
            setMaxHeight(200F, Unit.PIXELS)
            if (!data.picUrl.isNullOrEmpty())
                src = data.picUrl
        }
        horizontalLayout.add(image)

        val committees = mutableListOf<String>()
        data.committee?.let {
            it.split(";").forEach { committees.add(it) }
        }
        val listBox = ListBox<String>()
        listBox.setWidth("35%")
        listBox.setItems(committees)
        horizontalLayout.add(listBox)

        val listBox_addr = ListBox<String>()
        val addrs = mutableListOf<String>()
        data.addr?.let {
            it.split(";").forEach { addrs.add(it) }
        }
        listBox_addr.setWidth("50%")
        listBox_addr.setItems(addrs)
        horizontalLayout.add(listBox_addr)

        horizontalLayout
    }
    setItemDetailsRenderer(renderer)

执行结果

https://ithelp.ithome.com.tw/upload/images/20211008/20138680wWKMiRaAp4.png

Refactor - 自订 Component

上述程序中,ListBox 不同的资料来源,皆须将资料切分後,使用ListBox显示,程序码看起来不仅不亲民、兀长,且未能 reuse。复习一下先前曾学过的 Custom Component,Refactor 一下,将 ListBox 改为 Component。

开新档ListComponent.kt

class ListComponent(data: String, split: String, title: String) : KComposite() {
    val list = mutableListOf<String>()
    private val root = ui {
        data.split(split).forEach {
            if (!it.isNullOrEmpty()) list.add(it.trim())
        }
        verticalLayout {
            label(title)
            listBox<String> { setItems(list) }
        }
    }
}

fun HasComponents.listComponent(
    data: String,
    split: String,
    title: String,
    block: ListComponent.() -> kotlin.Unit = {}
) = init(ListComponent(data, split, title), block)

传入三个参数,data、split、title

使用

上述兀长的资料转换、加入ListBox()等程序内容,只要改成这样就行了,是不是简单易读多了。

    horizontalLayout.add(
        ListComponent(data.committee!!, ";", "委员会" )
    )

    horizontalLayout.add(
        ListComponent(data.addr!!, ";", "服务处")
    )

查询议案提案

提供栏位有

term 届别 sessionPeriod 会期 sessionTimes 会次
meetingTimes 临时会会次 billNo 议案编号 billName 提案名称
billOrg 提案单位/委员 billProposer 提案人(委员或党团) billCosignatory 连署人
billStatus 议案状态 pdfUrl 关系文书pdf档案下载位置 docUrl 关系文书doc档案下载位置
selectTerm 届别期别筛选条件

建立资料表 ProposalOfBills

create TABLE ProposalOfBills(
    id bigint auto_increment PRIMARY KEY,
    billNo VARCHAR(20),
    billName VARCHAR(500),
    billOrg VARCHAR(50),
    billProposer VARCHAR(200),
    billCosignatory VARCHAR(1000),
    billStatus VARCHAR(10),
    pdfUrl VARCHAR(100),
    docUrl VARCHAR(100)
);

新增 KEntityProposalOfBills

data class ProposalOfBills(
    override var id: Long? = null,
    var billNo: String? = null,
    @field:NotNull
    var billName: String? = null,
    @field:NotNull
    var billOrg: String? = null,
    var billProposer: String? = null,
    var billCosignatory: String? = null,
    var billStatus: String? = null,
    var pdfUrl: String? = null,
    var docUrl: String? = null
): KEntity<Long>, Serializable{
    companion object: Dao<ProposalOfBills, Long>(ProposalOfBills::class.java)
}

下载

下载的部份和下载委员资料大同小异,仅列出和上述不同的部份

    .withThrowExceptions(false)

从前述资料可知,提供的资料栏位数远大於所需,故只储存部份栏位备用。但是这样一来,parse 过程会因为栏位对应不上而抛出 Exceptions,为免转档因此中断。所以加上这一行让parse可继续进行。

过滤委员姓名

输入委员姓名後要过滤两个栏位,提案人(billProposer)和连署人(billCosignatory)

filter = textField {
    placeholder = "委员姓名"
    addValueChangeListener { event ->
        var dp: DataLoader<ProposalOfBills> = ProposalOfBills.dataLoader
        if (!filter.value.isBlank())
            dp = dp.withFilter {
                (ProposalOfBills::billProposer contains filter.value) or
                        (ProposalOfBills::billCosignatory contains  filter.value)
            }
        grid.setDataLoader(dp)
    }
    valueChangeMode = ValueChangeMode.EAGER
    isExpand = true
}
setVerticalComponentAlignment(FlexComponent.Alignment.START, filter)

第4行,取得EntityDataLoader
第5行,若栏位不为空,加过滤条件
第6行,加条件
第7,8行,我们要找的资料是委员姓名在栏位里任一位置都可以,所以使用 contains 比对

展开提案 Detail

    val renderer = ComponentRenderer<HorizontalLayout, ProposalOfBills> { data: ProposalOfBills ->
        val horizontalLayout = HorizontalLayout()
        horizontalLayout.add(
            ListComponent(data.billProposer!!, "  ", "提案人(委员或党团)")
        )
        horizontalLayout.add(
            ListComponent(data.billCosignatory!!, "  ", "连署人")
        )
        horizontalLayout
    }

方才写好的 ListComponent 又派上用场了

执行结果

https://ithelp.ithome.com.tw/upload/images/20211008/20138680UXd2O0OUwa.png

https://ithelp.ithome.com.tw/upload/images/20211008/20138680LzD8Rgj4La.png


<<:  [Day23] Sticky Nav

>>:  30-23 之 Patterns of Enterprise Application Architecture 小总结

SQL资料库基础实作

接着进行AWS RDS的实作, 以及与地端的基本差异. 在SSMS上, 资料库按右键建立 [New ...

30天学会C语言: Day 0-第一篇不免俗的要来些基础知识

所以我说...程序是虾饺? 程序是可以直接在电脑上执行,以完成某个目的或任务的一连串指令 换句话说,...

完赛!YA!关於 Vue.js 进阶心法系列

其实原本不是要叫这个名字的。原本要叫《官网没教你的「如何把 Vue 写好」》但是太狂了,竟然敢代替官...

Day 7 - 原型 (6): 预览主页

前言 今天利用之前所建的主页, 建立一个可让使用者互动的原型。 预览 利用Figma的预览功能, 即...

【Day7】情蒐阶段的小工具 ─ 扫描篇(一)

哈罗~ 今天要来跟大家介绍扫描的小工具 在介绍工具前,我们先来review一下TCP Flags。 ...