文章目录
音乐数据中心平台离线数仓综合项目
第一个业务:歌曲热度与歌手热度排行
业务分析
- 业务需求: 统计歌曲热度、歌手热度
-
用户可以登录机器进行点播歌曲,根据用户过去每日在机器上的点播歌曲统计出最近
1日、7日、30日
歌曲热度及歌手热度 -
需要统计的指标:
- 1日-7日-30日歌曲热度
- 1日-7日-30日歌手热度
需求
数据准备
- 要完成昨日的歌曲热度与歌手热度分析,需要以下两类数据:
歌曲歌手的基本信息
- 这些信息放在业务系统的关系型数据库 MySql song 表中。通过 sqoop 每天定时全量覆盖抽取到数据仓库 Hive 中的 ODS 层中。
-
关于 song 表的结构信息参看 “
01-歌曲热度与歌手热度-数据仓库模型
” 文件及 mysql 数据 song 表数据。
用户在机器上
-
这部分数据是用户在各个机器上当天的点歌播放行为数据,这些数据是运维每天零点打包以gz压缩文件的方式上传到HDFS平台,这里我们假定将数据
currentday_clientlog.tar.gz
每天凌晨定时上传到HDFS路径
hdfs://mycluster/logdata
中,这里在企业中应当上传到某个以天命名的结构目录下,通过Spark数据清洗将数据存放到Hive数仓ODS层中。关于用户在机器上的点歌行为数据参照
事件上报协议.docx
文档。 - 日志数据如下图所示:
- 事件上报协议:
数据仓库分层设计
- 这里根据需求将表分成如下三层:
- 基于以上逻辑表建立物理模型如下:
CREATE DATABASE IF NOT EXISTS `music`;
USE `music`;
-- 1. TO_CLIENT_SONG_PLAY_OPERATE_REQ_D 客户端歌曲播放表
CREATE EXTERNAL TABLE IF NOT EXISTS `TO_CLIENT_SONG_PLAY_OPERATE_REQ_D` (
`SONGID` string,
`MID` string,
`OPTRATE_TYPE` string,
`UID` string,
`CONSUME_TYPE` string,
`DUR_TIME` string,
`SESSION_ID` string,
`SONGNAME` string,
`PKG_ID` string,
`ORDER_ID` string
) partitioned by (data_dt string)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
LOCATION 'hdfs://node01/user/hive/warehouse/music.db/TO_CLIENT_SONG_PLAY_OPERATE_REQ_D';
-- 2. TO_SONG_INFO_D 歌库歌曲表
CREATE EXTERNAL TABLE IF NOT EXISTS `TO_SONG_INFO_D` (
`NBR` string,
`NAME` string,
`OTHER_NAME` string,
`SOURCE` int,
`ALBUM` string,
`PRDCT` string,
`LANG` string,
`VIDEO_FORMAT` string,
`DUR` int,
`SINGER_INFO` string,
`POST_TIME` string,
`PINYIN_FST` string,
`PINYIN` string,
`SING_TYPE` int,
`ORI_SINGER` string,
`LYRICIST` string,
`COMPOSER` string,
`BPM_VAL` int,
`STAR_LEVEL` int,
`VIDEO_QLTY` int,
`VIDEO_MK` int,
`VIDEO_FTUR` int,
`LYRIC_FTUR` int,
`IMG_QLTY` int,
`SUBTITLES_TYPE` int,
`AUDIO_FMT` int,
`ORI_SOUND_QLTY` int,
`ORI_TRK` int,
`ORI_TRK_VOL` int,
`ACC_VER` int,
`ACC_QLTY` int,
`ACC_TRK_VOL` int,
`ACC_TRK` int,
`WIDTH` int,
`HEIGHT` int,
`VIDEO_RSVL` int,
`SONG_VER` int,
`AUTH_CO` string,
`STATE` int,
`PRDCT_TYPE` string
) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
LOCATION 'hdfs://node01/user/hive/warehouse/music.db/TO_SONG_INFO_D';
-- 3. TW_SONG_BASEINFO_D 歌曲基本信息日全量表
CREATE EXTERNAL TABLE IF NOT EXISTS `TW_SONG_BASEINFO_D`(
`NBR` string,
`NAME` string,
`SOURCE` int,
`ALBUM` string,
`PRDCT` string,
`LANG` string,
`VIDEO_FORMAT` string,
`DUR` int,
`SINGER1` string,
`SINGER2` string,
`SINGER1ID` string,
`SINGER2ID` string,
`MAC_TIME` int,
`POST_TIME` string,
`PINYIN_FST` string,
`PINYIN` string,
`SING_TYPE` int,
`ORI_SINGER` string,
`LYRICIST` string,
`COMPOSER` string,
`BPM_VAL` int,
`STAR_LEVEL` int,
`VIDEO_QLTY` int,
`VIDEO_MK` int,
`VIDEO_FTUR` int,
`LYRIC_FTUR` int,
`IMG_QLTY` int,
`SUBTITLES_TYPE` int,
`AUDIO_FMT` int,
`ORI_SOUND_QLTY` int,
`ORI_TRK` int,
`ORI_TRK_VOL` int,
`ACC_VER` int,
`ACC_QLTY` int,
`ACC_TRK_VOL` int,
`ACC_TRK` int,
`WIDTH` int,
`HEIGHT` int,
`VIDEO_RSVL` int,
`SONG_VER` int,
`AUTH_CO` string,
`STATE` int,
`PRDCT_TYPE` array<string>
) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
STORED AS PARQUET
LOCATION 'hdfs://node01/user/hive/warehouse/music.db/TW_SONG_BASEINFO_D';
-- 4. TW_SONG_FTUR_D 歌曲特征日统计表
CREATE EXTERNAL TABLE IF NOT EXISTS `TW_SONG_FTUR_D`(
`NBR` string,
`NAME` string,
`SOURCE` int,
`ALBUM` string,
`PRDCT` string,
`LANG` string,
`VIDEO_FORMAT` string,
`DUR` int,
`SINGER1` string,
`SINGER2` string,
`SINGER1ID` string,
`SINGER2ID` string,
`MAC_TIME` int,
`SING_CNT` int,
`SUPP_CNT` int,
`USR_CNT` int,
`ORDR_CNT` int,
`RCT_7_SING_CNT` int,
`RCT_7_SUPP_CNT` int,
`RCT_7_TOP_SING_CNT` int,
`RCT_7_TOP_SUPP_CNT` int,
`RCT_7_USR_CNT` int,
`RCT_7_ORDR_CNT` int,
`RCT_30_SING_CNT` int,
`RCT_30_SUPP_CNT` int,
`RCT_30_TOP_SING_CNT` int,
`RCT_30_TOP_SUPP_CNT` int,
`RCT_30_USR_CNT` int,
`RCT_30_ORDR_CNT` int
) PARTITIONED BY (data_dt string)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
LOCATION 'hdfs://node01/user/hive/warehouse/music.db/TW_SONG_FTUR_D';
-- 5. TW_SINGER_RSI_D 歌手影响力日统计表
CREATE EXTERNAL TABLE IF NOT EXISTS `TW_SINGER_RSI_D`(
`PERIOD` int,
`SINGER_ID` string,
`SINGER_NAME` string,
`RSI` string,
`RSI_RANK` int
) PARTITIONED BY (data_dt string)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
LOCATION 'hdfs://node01/user/hive/warehouse/music.db/TW_SINGER_RSI_D';
-- 6. TW_SONG_RSI_D 歌曲影响力日统计表
CREATE EXTERNAL TABLE IF NOT EXISTS `TW_SONG_RSI_D`(
`PERIOD` int,
`NBR` string,
`NAME` string,
`RSI` string,
`RSI_RANK` int
) PARTITIONED BY (data_dt string)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
LOCATION 'hdfs://node01/user/hive/warehouse/music.db/TW_SONG_RSI_D';
- DM层两张表会在后期处理过程中,以 SparkSQL 方式每天覆盖更新到 MySQL 表中,这里不单独建立对应的物理模型。
- 以上各个物理表之间的流转关系如下:
数据处理流程
1. 准备客户端日志,上传至HDFS中
-
将客户端日志
currentday_clientlog.tar.gz
上传至HDFS目录
hdfs://node01/logdata
下,如果 HDFS 没有此目录需要先创建这个目录。这里模拟运维人员每天凌晨将数据上传至HDFS中。如果程序在本地运行,上传至当前项目下的 data 目录下,可以在项目
application.conf
进行配置。
2. 清洗客户端日志数据,保存到数仓ODS层
-
这里使用的是Spark读取上传到HDFS目录下的压缩数据,压缩数据中有很多张表,本业务中我们只需要获取客户端歌曲播放日志信息即可,这些数据在日志数据中的标识为
MINIK_CLIENT_SONG_PLAY_OPERATE_REQ
,这里读取这些数据进行ETL,保存到 HDFS 目录
hdfs://node01/logdata/all_client_tables
下,后期使用 SparkSQL 加载到对应的 Hive 仓库 ODS 层的
TO_CLIENT_SONG_PLAY_OPERATE_REQ_D
客户端歌曲播放表中。 -
对应的处理数据的代码文件:
ProduceClientLog.scala
,本地运行该程序并查看结果:
- 查看 hive 中数据:
-
至此,我们就得到了
TO_CLIENT_SONG_PLAY_OPERATE_REQ_D 客户端歌曲播放表
,客户端日志数据已经导入到 HDFS 中,并将我们对应关注事件的数据存入到 Hive 指定的表中了。
3. 抽取 MySQL 中 song 数据到 Hive ODS
-
将MySQL中的
songdb
库中的
song
表通过 sqoop 抽取到对应的 ODS 层表
TO_SONG_INFO_D
中,这里需要安装 sqoop 工具,
每天定时全量
覆盖更新到Hive中。 - 在执行导入命令前,先创建 hive 表
-
在 node03 上执行 sqoop 导入数据脚本,将 MySQL 中的 song 表数据导入到 Hive 数仓
TO_SONG_INFO_D
中,脚本内容如下:
sqoop import --connect jdbc:mysql://node01:3306/songdb?dontTrackOpenResources=true\&defaultFetchSize=10000\&useCursorFetch=true\&useUnicode=yes\&characterEncoding=utf8 --username root --password 123456 --table song --target-dir /user/hive/warehouse/data/song/TO_SONG_INFO_D/ --delete-target-dir --num-mappers 1 --fields-terminated-by '\t'
- 查询 hive 表中的数据:
-
至此,我们就得到了
TO_SONG_INFO_D 歌库歌曲表
,并将 MySQL 数据库中歌曲的基本信息导入到了 Hive 表中。
Sqoop 后可以指定的参数,解释如下:
- –connect :连接jdbc url
- dontTrackOpenResources=true:你关闭所有的statement和resultset,但是如果你没有关闭connection的话,connection就会引用到statement和resultset上,从而导致GC不能释放这些资源(statement和resultset),当设置参数dontTrackOpenResources=true时,在statement关闭后,resultset也会被关闭,达到节省内存目的。
- defaultFetchSize:设置每次拉取数据量。
- useCursorFetch=true:设置连接属性useCursorFetch=true (5.0版驱动开始支持),表示采用服务器端游标,每次从服务器取fetch_size条数据。
- useUnicode=yes&characterEncoding=utf8:设置编码格式UTF8
- –username:数据库用户名
- –password:数据库密码
- –table:import出的数据库表
- –target-dir:指定数据导入到HDFS中的路径
- –delete-target-dir:如果数据输出目录已存在则先删除
- –num-mappers:当数据导入HDFS中时生成的MR任务使用几个map任务
- –hive-import:将数据从关系数据库中导入到 hive 表中,自动将数据导入到hive中,
生成对应–hive-table 指定的表,表中的字段是直接从MySql中映射过来的字段
,因此在这里并没有采用这种方式导入- –hive-database:数据导入Hive中使用的Hive 库
- –hive-overwrite:覆盖掉在 Hive 表中已经存在的数据,与append只能使用一个
- –fields-terminated-by:指定字段列分隔符,默认分隔符是’\001’,建议指定分隔符
- –hive-table:导入的Hive表
4. 清洗“歌库歌曲表”生成“歌曲基本信息日全量表”
-
由ODS层的歌库歌曲表
TO_SONG_INFO_D
ETL得到歌曲基本信息日全量表
TW_SONG_BASEINFO_D
,主要是对原来数据字段切分、脏数据过滤、时间格式整理、字段提取等操作。
- 创建 hive 表 TW_SONG_BASEINFO_D:
-
对应的 ETL 处理数据的代码文件:
GenerateTwSongBaseinfoD.scala
,本地运行该程序并查看结果:
-
至此,我们得到了
TO_SONG_BASEINFO_D 歌曲基本信息日全量表
,并将 TO_SONG_INFO_D 中的数据清洗存入到该表中。
5. EDS 层生成“歌曲特征日统计表”
-
基于
客户端歌曲播放表 TO_CLIENT_SONG_PLAY_OPERATE_REQ_D
和
歌曲基本信息日全量表 TW_SONG_BASEINFO_D
,生成歌曲特征日统计表:
TW_SONG_FTUR_D
,主要是按照两张表的歌曲 ID 进行关联,统计出歌曲在当天、7天、30天内的点唱信息和点赞信息。 - 创建 hive 表 TW_SONG_FTUR_D
-
对应的数据处理文件:
GenerateTwSongFturD.scala
,本地运行该程序并查看结果:
-
至此,我们得到了
TW_SONG_FTUR_D 歌曲特征日统计表
,并将统计结果存入到该表中。
6. 统计歌手和歌曲热度
-
这里统计歌手和歌曲热度都是根据歌曲特征日统计表:
TW_SONG_FTUR_D
计算得到,主要借助了
微信指数
来统计对应的歌曲和歌手的热度,统计了当天、近7天、近30天每个歌手和歌曲的热度信息存放在对应的EDS层的歌手影响力指数日统计表
TW_SINGER_RSI_D
和歌曲影响力指数日统计表
TW_SONG_RSI_D
。 -
微信传播指数WCI可以显示出微信公众号的热度排名,公式由“
清博指数
”提供,其计算公式如下:
-
说明:
- R 为评估时间段内所有文章(n)的阅读总数,这里可以看成某个歌曲在一段时间内的总点唱数。
- Z 为评估时间段内所有文章(n)的点赞总数,这里可以看成某个歌曲在一段时间内的总点赞数。
- d 为评估时间段内所含天数,一般为周取7天,月取30天,年度取365天,其他自定义时间以真实的天数计算。
- n 为评估时间段内某公众号所发文章数,这里和歌曲无对应。
- Rmax 和 Zmax 为评估时间段内公众号所发文章的最高阅读数和点赞数,这里可以看成某个歌曲在一段时间内的总点唱数和总点赞数。
- 以上指数计算方式对应到歌曲指数计算方式如下:
-
在对歌曲特征日统计表 TW_SONG_FTUR_D 进行统计得到
歌手影响力指数日统计表 TW_SINGER_RSI_D
和
歌曲影响力指数日统计表 TW_SONG_RSI_D
时,分别还将对应的结果使用 SparkSQL 保存到了 MySQL 中,对应的 MySQL 库为 song_result 库。所以首先创建 MySQL数据库
create database song_result default character set utf8;
-
对应的表分别为:
tm_singer_rsi
、
tm_song_rsi
。方便后期从 MySQL 总查看数据结果。 - 首先创建 hive 表 TW_SINGER_RSI_D 和 TW_SONG_RSI_D
-
对应的数据处理文件:
GenerateTmSingerRsiD.scala
、
GenerateTmSongRsiD.scala
,本地运行该程序并查看结果:
-
至此,我们得到了
TW_SINGER_RSI_D 歌手影响力日统计表 和 TW_SONG_RSI_D 歌曲影响力日统计表
,并将统计结果存入到该表中,排名前 30 名的数据还存入到对应的 MySQL 表中。
使用 Azkaban 配置任务流
- 这里使用Azkaban来配置任务流进行任务调度。集群中提交任务,需要修改项目中的 application.conf 文件配置项:local.run=“false”,并打包上传至集群服务器 node01、node02 和 node03 上。
- 保证在 Hive 中对应的表都已经创建好。
1. 脚本准备
-
① 清洗客户端日志脚本
1_produce_clientlog.sh
#!/bin/bash
currentDate=`date -d today +"%Y%m%d"`
if [ x"$1" = x ]; then
echo "====使用自动生成的今天日期===="
else
echo "====使用 Azkaban 传入的日期===="
currentDate=$1
fi
echo "日期为: $currentDate"
ssh hadoop@node01 > /tmp/logs/music_project/music-rsi.log 2>&1 <<aabbcc
hostname
source /etc/profile
spark-submit --master yarn-client --class com.yw.musichw.ods.ProduceClientLog \
/bigdata/data/music_project/musicwh-1.0.0-SNAPSHOT-jar-with-dependencies.jar $currentDate
exit
aabbcc
echo "all done!"
-
② mysql 数据抽取数据到 Hive ODS 脚本
2_extract_mysqldata_to_ods.sh
#!/bin/bash
ssh hadoop@node03 > /tmp/logs/music_project/music-rsi.log 2>&1 <<aabbcc
hostname
source /etc/profile
sqoop import --connect jdbc:mysql://node01:3306/songdb?dontTrackOpenResources=true\&defaultFetchSize=10000\&useCursorFetch=true\&useUnicode=yes\&characterEncoding=utf8 \
--username root --password 123456 --table song --target-dir /user/hive/warehouse/music.db/TO_SONG_INFO_D/ \
--delete-target-dir --num-mappers 1 --fields-terminated-by '\t'
exit
aabbcc
echo "all done!"
-
③ 清洗歌库歌曲表脚本
3_generate_tw_song_baseinfo.sh
#!/bin/bash
currentDate=`date -d today +"%Y%m%d"`
if [ x"$1" = x ]; then
echo "====使用自动生成的今天日期===="
else
echo "====使用 Azkaban 传入的日期===="
currentDate=$1
fi
echo "日期为: $currentDate"
ssh hadoop@node01 > /tmp/logs/music_project/music-rsi.log 2>&1 <<aabbcc
hostname
source /etc/profile
spark-submit --master yarn-client --class com.yw.musichw.eds.content.GenerateTwSongBaseinfoD \
/bigdata/data/music_project/musicwh-1.0.0-SNAPSHOT-jar-with-dependencies.jar $currentDate
exit
aabbcc
echo "all done!"
-
④ 生成歌曲特征日统计表脚本
4_generate_tw_song_ftur.sh
#!/bin/bash
currentDate=`date -d today +"%Y%m%d"`
if [ x"$1" = x ]; then
echo "====使用自动生成的今天日期===="
else
echo "====使用 Azkaban 传入的日期===="
currentDate=$1
fi
echo "日期为: $currentDate"
ssh hadoop@node01 > /tmp/logs/music_project/music-rsi.log 2>&1 <<aabbcc
hostname
source /etc/profile
spark-submit --master yarn-client --class com.yw.musichw.eds.content.GenerateTwSongFturD \
/bigdata/data/music_project/musicwh-1.0.0-SNAPSHOT-jar-with-dependencies.jar $currentDate
exit
aabbcc
echo "all done!"
-
⑤ 生成歌手热度脚本
5_generate_tm_singer_rsi.sh
#!/bin/bash
currentDate=`date -d today +"%Y%m%d"`
if [ x"$1" = x ]; then
echo "====使用自动生成的今天日期===="
else
echo "====使用 Azkaban 传入的日期===="
currentDate=$1
fi
echo "日期为: $currentDate"
ssh hadoop@node01 > /tmp/logs/music_project/music-rsi.log 2>&1 <<aabbcc
hostname
source /etc/profile
spark-submit --master yarn-client --class com.yw.musichw.dm.content.GenerateTmSingerRsiD \
/bigdata/data/music_project/musicwh-1.0.0-SNAPSHOT-jar-with-dependencies.jar $currentDate
exit
aabbcc
echo "all done!"
-
⑥ 生成歌手热度脚本
6_generate_tm_song_rsi.sh
#!/bin/bash
currentDate=`date -d today +"%Y%m%d"`
if [ x"$1" = x ]; then
echo "====使用自动生成的今天日期===="
else
echo "====使用 Azkaban 传入的日期===="
currentDate=$1
fi
echo "日期为: $currentDate"
ssh hadoop@node01 > /tmp/logs/music_project/music-rsi.log 2>&1 <<aabbcc
hostname
source /etc/profile
spark-submit --master yarn-client --class com.yw.musichw.dm.content.GenerateTmSongRsiD \
/bigdata/data/music_project/musicwh-1.0.0-SNAPSHOT-jar-with-dependencies.jar $currentDate
exit
aabbcc
echo "all done!"
2. 编写 Azkaban 各个Job组成任务流
-
新建
flow20.project
,内容如下:
azkaban-flow-version: 2.0
-
新建
music-rsi.flow
,内容如下:
nodes:
- name: Job1_ProduceClientLog
type: command
config:
command: sh 1_produce_clientlog.sh ${mydate}
- name: Job2_ExtractMySQLDataToODS
type: command
config:
command: sh 2_extract_mysqldata_to_ods.sh
- name: Job3_GenerateTwSongBaseinfo
type: command
config:
command: sh 3_generate_tw_song_baseinfo.sh
dependsOn:
- Job2_ExtractMySQLDataToODS
- name: Job4_GenerateTwSongFtur
type: command
config:
command: sh 4_generate_tw_song_ftur.sh ${mydate}
dependsOn:
- Job1_ProduceClientLog
- Job3_GenerateTwSongBaseinfo
- name: Job5_GenerateTmSingerAndSongRsi
type: command
config:
command: sh 5_generate_tm_singer_rsi.sh ${mydate}
command.1: sh 6_generate_tm_song_rsi.sh ${mydate}
dependsOn:
- Job4_GenerateTwSongFtur
-
将以上六个脚本文件、
music-rsi.flow
与
flow20.project
压缩生成 zip 文件
music-rsi.zip
3. 清空数据
- 由于前面我们在本地已经将整个数据处理流程跑过一次,现在 hive 和 mysql 中已经存在数据。在提交作业执行前,先清除掉 hive 和 mysql 中的数据。
-
编写脚本
vim drop_song_tables.sql
,内容如下:
drop table `music`.`to_client_song_play_operate_req_d`;
drop table `music`.`to_song_info_d`;
drop table `music`.`tw_singer_rsi_d`;
drop table `music`.`tw_song_baseinfo_d`;
drop table `music`.`tw_song_ftur_d`;
drop table `music`.`tw_song_rsi_d`;
-
执行命令:
hive -f drop_song_tables.sql
,删除表。由于这些都是外部表,真正的数据还在 HDFS,所以还需要删除相关的数据。
-
然后重新创建 hive 表,编写脚本
vim create_song_tables.sql
,内容在前面”数据仓库分层设计“这一小节。 -
执行命令
hive -f create_song_tables.sql
,创建表。 -
删除 mysql 中生成结果的两张表
tm_singer_rsi
和
tm_song_rsi
。 -
把之前的日志数据也清空掉,并执行命令
hdfs dfs -put currentday_clientlog.tar.gz /logdata/
上传客户端日志数据
4. 提交Azkaban作业
- 在 Azkaban 的 web server ui界面创建项目
-
然后上传项目 zip 文件
music-rsi.zip
- 查看任务
- 配置任务参数
- 执行成功,最终结果保存到了 mysql 表中。
5. 遇到的问题
Access denied for user ‘root‘@‘centos128‘ (using password: YES)
- 解决办法:把远程访问的%改成主机名即可
mysql> grant all privileges on *.* to 'root'@'centos128'' identified by '123456' with grant option;
mysql> flush privileges;
InnoDB is limited to row-logging when transaction isolation level is READ COMMITTED or READ UNCOMMITTED.
-
解决办法:修改配置文件
vim /etc/my.cnf
,增加参数
binlog_format=row
,然后重启 mysql,
systemctl restart mysqld
使用Superset数据可视化
1. superset环境搭建
2. 在superset中设计表可视化
- 登录 superset:http://192.168.254.130:8088/
- 添加数据源:依次点击 Data → Databases → 添加,配置 jdbc 连接串,并测试连通性OK后保存。
- 添加数据表:依次点击 Data → Datasets → 添加,添加“songresult”库下的表“tm_singer_rsi”和“tm_song_rsi”。
- 修改表中对应字段显示名称:编辑表记录,找到列标签,编辑各个列的全称
- 编辑图表:
- 面板可视化展示:
版权声明:本文为yangwei234原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。