基于Go语言的数据分析技术在金融行业内部审计中的应用
2020-06-21张松唐艳双李怀宇
张松 唐艳双 李怀宇
[摘要]本文通过审计实例介绍运用Go语言开发爬虫程序,批量获取某省金融企业监管处罚数据,对被审计单位及所在地域金融企业进行全方位、立体画像,实现网络公开数据与行内数据的相互补充印证,准确锁定审计目标,提升现场审计效率。
[关键词]内部审计 Go语言 数据分析 爬虫程序
随着信息科技与生产过程的不断融合,人们越来越感受到科技在工作中的重要性。在企业内部审计中,可利用数据分析语言开发网络爬虫程序获取被审计单位的日常经营及社会活动相关数据。Go语言(又称Golang语言),是Google公司开发的一种简洁高效的开源编程语言,具有高并发性和跨平台开发优势,非常适合服务器编程。
一、总体思路
首先,开发的爬虫程序必须善意且自觉遵守Robots协议,开发前要查看网站根目录下的robots.txt文件,根据Robots协议合理进行爬取数据。其次,将爬取到的监管信息,存储到CSV文本及本地MySQL数据库中,并从中筛选出关注单位的处罚信息。
爬虫程序工作原理是抓取网页源代码。源代码中包含网页内容信息和网络链接URL地址,可以继续打开网页中的URL地址,进一步抓取下一层网页内容和网络链接URL地址,这样就可以循环抓取所需网页公布的数据信息。
Go语言抓取网页源代码时,最常用的方法是利用Go语言自带的net/http包工具与正则表达式相配合,匹配抓取想要的内容信息,也可以利用一些根据网页节点属性选择的CSS选择器、Colly或Goquery等框架工具提取所需要的数据信息。
二、具体步骤
(一)分析网页目标地址,封装地址函数
首先,明确爬取目标网站的URL链接地址变化规律,需要爬取的内容是金融机构被监管部门处罚的公开处罚信息;其次,打开具体的行政处罚页面首页发现,每页有18份处罚单的名称和链接地址,打开第二页、第三页等多个行政处罚页面,发现每页都包含18份处罚单的链接,而且前三页链接URL地址是一种类型格式,第四页往后的网页是另一种链接URL格式。所以将两种链接URL格式定义为一个数组切片并赋值给urls变量,封装到Get2Urlt函数中,用if语句自动判断,当页码小于4时取第一种URL格式,其他情况则取第二种URL格式,具体语句如下:
func Get2Urlt(idx int) (url string) {
urls :=
[]string{"http://www.****c.**v.cn/cn/st****/data/tocInfo/Selecr********ItemdId/data_itemId=1515,pageIndex=" + strconv.Itoa(idx) + ",pageSize=18.json",
"http://www.****c.**v.cn/dbircweb/tocInfo/Selecr********ItemdId?itemId=1515&pageSize=18&pageIndex=" + strconv.Itoa(idx)}
if idx < 4 {
url = urls[0]
} else {
url = urls[1]
}
return
}
(二)封装函数,获取单个网页信息
选用Go语言程序自带的net/http包中的Get()方法,根据传入的URL链接地址参数便可发起有效请求,获取网页源代码并赋值给变量resp。因为Http.Get()请求是一种网络资源,用完后应第一时间將其关闭,用defer指定的Close()函数来关闭网络请求,语句如下:
func HttpGet(url string) (result []string, err error) {
resp, err1 := http.Get(url)
if err1 != nil {
err = err1
return
}
defer resp.Body.Close()
data1, err2 := ioutil.ReadAll(resp.Body)
b := UserJsonObj{}
json.Unmarshal([]byte(data1), &b)
if err2 != nil {
err = err2
return
}
urlls := make([]string, 0)
for i := 0; i <= 17; i++ {
urllist :=
"http://www.****c.**v.cn/branch/*******I/view/pages/tcommon/Item****t.html?docId=" + strconv.Itoa(int(b.Data.Rows[i].DocId)) + "&itemId=1515"
urlls = append(urlls, urllist)
}
result = urlls
return
}
(三)创建结构体,存储网络Json数据
创建UserJsonObj结构体,用来存储网页的Json数据并赋值给变量b,用json.Unmarshal工具将json字符串解码到相应的结构体中。创建HttpGet()函数获取每页所有罚单的URL链接地址,循环读出每份处罚单的链接URL地址,将所有罚单的链接URL地址赋值给变量urllist,并追加到数组切片urlls中,再将切片urlls赋值给result并返回GetOnePage()函数,语句如下:
type UserJsonObj struct {
RptCode float64 `json:"rptCode"`
Msg string `json:"msg"`
Data Data `json:"data"`
}
type Data struct {
Total float64 `json:"total"`
Rows []Brand `json:"rows"`
}
type Brand struct {
DocId float64 `json:"tocId"`
DocSubtitle string `json:"tocSubtitle"`
PublishDate string `json:"publishDate"`
DocSummarystring`json:"tocSummary"`
DocFileUrl string `json:"tocFileUrl"`
Generaltype string `json:"generaltype"`
PdfFileUrl string `json:"pdfFileUrl"`
ItemName string `json:"itemName"`
SolicitFlag string `json:"tolicitFlag"`
DocTitle string `json:"docTitle"`
Datafrom string `json:"datafrom"`
DocUuid string `json:"docUuit"`
Builddate string `json:"builddate"`
}
func GetOnePage(idx int, page chan int) {
url := Get2Urlt(idx)
authors, err := HttpGet(url)
if err != nil {
fmt.Println("HttpGet err:", err)
return
}
mc := make([]string, 0)
dw := make([]string, 0)
xm := make([]string, 0)
yy := make([]string, 0)
tk := make([]string, 0)
fk := make([]string, 0)
rq := make([]string, 0)
for _, jokeURL := range authors {
conUrl := GetUrlt(jokeURL)
contentUrl :=
"http://www.****c.****v.cn/cn/****ic/data/tocInfo/SelecR****t/data_docId=" + conUrl + ".json"
mc1, dw1, xm1, yy1, tk1, fk1, rq1, err := GetTwoPage(contentUrl)
if err != nil {
fmt.Println("Spider2Page err:", err)
continue
}
mc = append(mc, mc1)
dw = append(dw, dw1)
xm = append(xm, xm1)
yy = append(yy, yy1)
tk = append(tk, tk1)
fk = append(fk, fk1)
rq = append(rq, rq1)
}
SpiderWrPage(idx, mc, dw, xm, yy, tk, fk, rq)
page <- idx
}
(四)封裝函数,进一步获取罚单信息
创建GetTwoPage(contentUrl)函数获取第二层中每份罚单的关键信息。传入参数是加工后的contentUrl罚单链接地址,GetTwoPage函数也是使用net/http包中的Get()方法来获取每张罚单的源代码数据,再用Goquery工具的doc.Find()方法对每张罚单文本中的表格数据进行过滤提取,分别提取出罚单中的名称、被处罚单位、被处罚人姓名、被处罚原因、违反条款、处罚信息和处罚日期等关键信息,将提取到的信息分别返回GetOnePage()函数,语句如下:
func GetTwoPage(contentUrl string) (mc1, dw1, xm1, yy1, tk1, fk1, rq1 string, error) {
resp, err1 := http.Get(contentUrl)
if err1 != nil {
err = err1
return
}
fileContent := make([]string, 0)
fileContent0 := make([]string, 0)
doc,err := goquery.NewDocumentFromResponse(resp)
if err == nil {
doc.Find("td").Each(func(i int, s *goquery.Selection) {
str1 := s.Text()
str1 = strings.Replace(str1, " ", "", -1)
str1 = strings.Replace(str1, "_", "", -1)
str1 = strings.Replace(str1, "\\r\\n", "", -1)
str1 = strings.Replace(str1, "\r|\n", "", -1)
str1 = strings.Replace(str1, " ", "", -1)
fileContent = append(fileContent, str1)
})
} else {
fmt.Println("err--->", err)
}
if len(fileContent) < 2 {
doc.Find("span").Each(func(i int, s *goquery.Selection) {
str1 := s.Text()
str1 = strings.Replace(str1, " ", "", -1)
str1 = strings.Replace(str1, "_", "", -1)
str1 = strings.Replace(str1, "\n", "", -1)
str1 = strings.Replace(str1, "\r", "", -1)
str1 = strings.Replace(str1, "\\r\\n", "", -1)
str1 = strings.Replace(str1, "\r\n", "", -1)
str1 = strings.Replace(str1, " ", "", -1)
fileContent0 = append(fileContent0, str1)
})
}
hz1 := make([]string, 0)
hz0 := make([]string, 0)
for i := 0; i < len(fileContent); i++ {
fileContent[i] = strings.Replace(fileContent[i], " ", "", -1)
fileContent[i] = strings.Replace(fileContent[i], "\n", "", -1)
fileContent[i] = strings.Replace(fileContent[i], "\t", "", -1)
fileContent[i] = strings.Replace(fileContent[i], " ", "", -1)
Hz := fileContent[i]
hz1 = append(hz1, Hz)
}
for i := 0; i < len(fileContent0); i++ {
fileContent0[i] = strings.Replace(fileContent0[i], " ", "", -1)
fileContent0[i] = strings.Replace(fileContent0[i], "\n", "", -1)
fileContent0[i] = strings.Replace(fileContent0[i], "\t", "", -1)
fileContent0[i] = strings.Replace(fileContent0[i], " ", "", -1)
Hz0 := fileContent0[i]
hz0 = append(hz0, Hz0)
}
if len(hz1) >= 21 {
mc1 = hz1[1]
dw1 = hz1[10]
xm1 = hz1[12]
yy1 = hz1[14]
tk1 = hz1[16] + hz1[18]
fk1 = hz1[20]
rq1 = hz1[len(hz1)-1]
} else {
}
if len(hz0) >= 30 {
mc1 = hz0[1]
dw1 = hz0[13]
xm1 = hz0[10]
yy1 = hz0[19] + hz0[20] + hz0[21] + hz0[22] + hz0[23] + hz0[24] + hz0[25]
tk1 = hz0[28]
fk1 = hz0[28] + hz0[29] +hz0[30]
rq1 = hz0[len(hz0)-9] + hz0[len
(hz0)-8] + hz0[len(hz0)-7] + hz0[len(hz0)-6] + hz0[len(hz0)-5] + hz0[len(hz0)-4] + hz0[len(hz0)-3]
} else {
}
return
}
(五)封装函数,将数据写入文本
GetOnePage()函数接收到GetTwoPage()函数采集到的罚单关键数据后,分别将每个字段的数据信息追加到提前设置好的数组切片中,将收到的切片数据传给创建的SpiderWrPage()函数,将采集到的每页数据逐条写入文本中,多传入一个idx参数记录采集到第几页的数据,用strconv.Itoa(idx)方法将整型数字转换成字符串,配合目录地址创建对应的目录文本文件,语句如下:
func SpiderWrPage(idx int, mc, dw, xm, yy, tk, fk, rq []string) {
//path := "D:/go/src/crawler/第1" + strconv.Itoa(idx) + "页.txt"
path := "D:/go/src/go_tintjj.go/data1/第" + strconv.Itoa(idx) + "页.txt"
f, err := os.Create(path)
if err != nil {
fmt.Println("os.Create err:", err)
return
}
defer f.Close()
n := len(mc)
for i := 0; i < n; i++ {
f.WriteString(strconv.Itoa(i+1) + "," + mc[i] + "," + dw[i] + "," + xm[i] + "," + yy[i] + "," + tk[i] + "," + fk[i] + "," + rq[i] + "\n")
}
MySql(idx)
}
(六)封装函数,将文本数据写入本地MySQL数据库
创建MySql()函数,将写入文本的罚单数据插入本地MySql数据库中,运行流程为:首先用sqlx.Open()函数打开本地MySql数据库,用db.Exec()方法加create table语句创建对应的数据表;其次用Go语言自带os包中的os.Open()方法打开数据文本,用for循环语句里嵌套reader.ReadLine()方法逐行读取数据,直到io.EOF文件结束时停止;最后用db.Exec()方法加insert into语句将读取到的数据逐条插入对应数据表中,语句如下:
func MySql(idx int) {
db,err := sqlx.Open("mysql", "root:a123@tcp(127.0.0.1:3306)/test")
HandleError(err, "sql.Open")
defer db.Close()
_, err = db.Exec("create table if not exists data_ybjh(idx int(8),mc varchar(260),dw varchar(424),xm varchar(224),yy varchar(824),tk varchar(424),fk varchar(424),rq varchar(264));")
HandleError(err, "db.Exec create table")
fpath := GetFpath(idx)
file, e := os.Open(fpath)
HandleError(e, "os.Open")
defer file.Close()
reader := bufio.NewReader(file)
for {
lineBytes, _, err := reader.ReadLine()
HandleError(err, "reader.ReadLine")
if err == io.EOF {
break
}
lineStr := string(lineBytes)
fields := strings.Split(lineStr, ",")
idx, mc, dw, xm, yy, tk, fk, rq := fields[0], fields[1], fields[2], fields[3], fields[4], fields[5], fields[6], fields[7]
result, err := db.Exec("insert into data_tbjh(idx,mc,dw,xm,yy,tk,fk,rq) values(?,?,?,?,?,?,?,?);", idx, mc, dw, xm, yy, tk, fk, rq)
HandleError(err, "db.Exec insert")
if n, e := result.RowsAffected(); e == nil && n > 0 {
fmt.Printf("插入%s 数据成功!!\n", mc)
if _, e := result.RowsAffected(); e != nil && e == io.EOF {
break
}
}
}
}
(七)數据入库成功
程序运行完毕后,打开本地MySQL数据库查询发现,采集的金融单位处罚数据信息,包括名称、被处罚单位、被处罚人姓名、被处罚原因、违反条款、处罚信息和处罚日期等关键信息,已分字段插入本地数据库对应表中,达到预期程序设计要求,为审计项目的开展增加一条数据获取渠道。
三、数据初步分析
通过SQL语句对MySQL数据库中提取的处罚信息进行汇总分析,发现被审计单位的主要监管处罚原因主要集中于贷款资金被挪用、违规发放贷款、集团客户授信超比例及贷款五级分类不准确等方面。
本次利用Go语言技术获取监管处罚信息过程中,在网页文本数据筛选获取时用到了Goquery框架工具的doc.Find方法。Goquery框架工具功能强大、使用灵活方便,在对文本数据筛选提取时也可使用regexp包中的MustCompile和FindAllStringSubmatch方法以达到同样效果。
上述方法可以一次性获取某个省份特定单位的监管处罚信息,对被审计单位及所在地域金融企业的经营情况有更加全面的了解,开阔审计人员的思路,丰富审计手段,提升现场审计效率。
(作者单位:中国邮政储蓄银行股份有限公司审计局沈阳分局,邮政编码:110013,电子邮箱:cdzhangs@163.com)