diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000000..13566b81b01 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 00000000000..b14da94ef6b --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dex0423.github.io.iml b/.idea/dex0423.github.io.iml new file mode 100644 index 00000000000..1f1ea62e76c --- /dev/null +++ b/.idea/dex0423.github.io.iml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000000..06aa343a355 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,45 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000000..dd4c951ef44 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000000..a2e120dcc86 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000000..694003b732b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 00000000000..68993fb7f5a --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000000..171a714c821 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 00000000000..7fa516ed761 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,16 @@ +{ + "ExpandedNodes": [ + "", + "\\css", + "\\files", + "\\fonts", + "\\img", + "\\img-post", + "\\js", + "\\less", + "\\pwa", + "\\_posts" + ], + "SelectedNode": "\\img\\post-bg-alibaba.jpg", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/dex0423.github.io/project-colors.json b/.vs/dex0423.github.io/project-colors.json new file mode 100644 index 00000000000..0eec3dfe1f4 --- /dev/null +++ b/.vs/dex0423.github.io/project-colors.json @@ -0,0 +1,11 @@ +{ + "Version": 1, + "ProjectMap": { + "a2fe74e1-b743-11d0-ae1a-00a0c90fffc3": { + "ProjectGuid": "a2fe74e1-b743-11d0-ae1a-00a0c90fffc3", + "DisplayName": "杂项文件", + "ColorIndex": -1 + } + }, + "NextColorIndex": 0 +} \ No newline at end of file diff --git a/.vs/dex0423.github.io/v17/.suo b/.vs/dex0423.github.io/v17/.suo new file mode 100644 index 00000000000..0d54fc08258 Binary files /dev/null and b/.vs/dex0423.github.io/v17/.suo differ diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 00000000000..20cd20aab98 Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/1-resources.html b/1-resources.html deleted file mode 100644 index 2427d14fe68..00000000000 --- a/1-resources.html +++ /dev/null @@ -1,180 +0,0 @@ ---- -layout: page -title: "Resources" -description: "学习三叶语超级简单" -header-img: "img/post-bg-rwd.jpg" ---- - - - - - -
- - -

每天只要15分钟,一个月就能掌握
- 学一点儿是一点儿,不用也不会忘

- -

这里是三叶语学习的相关资料(持续更新中,请常来看看)。

- - -

- -

手册类

- - - -

教程类

- - - -

词典类

- - - -

网站类

- - - -
- - -特别说明: -
  • 因为三叶语的设计和词汇还在完善中,您现在学习的内容将来可能还会有少量变化,敬请理解。
  • -
  • 若不同文件存在不一致的地方,请以“详解系列”和最新版的“三叶语词汇表”为准,或咨询作者。
  • -
    - - -
    - - - - - - - - - -{% if site.gitalk.enable %} - - - - -
    - -{% endif %} - - - -{% if site.disqus.enable %} - -
    -
    -
    -
    - - - - - -{% endif %} - diff --git a/README.md b/README.md index 700d203c011..8b137891791 100644 --- a/README.md +++ b/README.md @@ -1,16 +1 @@ -本项目仅仅是为了前来Fork的朋友保留的,原则上不再更新。本项目示范网站地址为:https://klovien.github.io 。 -三叶草国际语已更名为“格罗比言·全球语”,新的项目地址为: - -* 中文版:https://gitee.com/globien/globien -* 英文版:https://github.com/globien/globien.github.io - -### 致谢 - -1. 这个模板是从这里 [BY](https://github.com/qiubaiying/qiubaiying.github.io) fork 的, 感谢作者BY。 -2. BY的模板应该是从这个模板 [Hux](https://github.com/Huxpro/huxpro.github.io) fork 的, 也一起感谢一下。 -3. 感谢 Jekyll、Github Pages 和 Bootstrap! - -### License - -遵循 MIT 许可证。有关详细,请参阅 [LICENSE](https://github.com/klovien/klovien.github.io/blob/master/LICENSE)。 diff --git a/_config.yml b/_config.yml index d8149ce6197..334158293b2 100644 --- a/_config.yml +++ b/_config.yml @@ -1,18 +1,18 @@ # Site settings -title: 三叶草国际语 -SEOTitle: 三叶草国际语 - 让沟通更容易 +title: dex0423 的博客 +SEOTitle: dex0423的博客 header-img: img/post-bg-desk.jpg -email: klovien@gmail.com -description: "中西融合, 即学即会, 从此不再辛苦背单词" -keyword: "人工语言, 国际语, 世界语, 三叶草国际语, 三叶语" -url: "https://klovien.github.io" # your host, for absolute URL +email: pandong423@icloud.com +description: "记录工作学习的点点滴滴。" +keyword: "爬虫, 安卓逆向, js逆向, python, 数据抓取" +url: "https://dex0423.github.io" # your host, for absolute URL baseurl: "" # for example, '/blog' if your blog hosted on 'host/blog' -github_repo: "https://github.com/klovien/klovien.github.io.git" # you code repository +github_repo: "https://github.com/dex0423/dex0423.github.io.git" # you code repository # Sidebar settings sidebar: true # whether or not using Sidebar. -sidebar-about-description: "Goals determine what you're going to be!" -sidebar-avatar: /img/about-HJ.jpg # use absolute URL, seeing it's used in both `/` and `/about/` +sidebar-about-description: "一个野生程序员。" +sidebar-avatar: /img/微信图片_20220126102606.jpg # use absolute URL, seeing it's used in both `/` and `/about/` @@ -62,11 +62,11 @@ kramdown: # Gitalk gitalk: enable: true #是否开启Gitalk评论 - clientID: d18b343dd9c7ca49518a #生成的clientID - clientSecret: 539e88b8a7feffaddbb553b9c421dfefcbc6eced #生成的clientSecret - repo: klovien.github.io #仓库名称 - owner: klovien #github用户名 - admin: klovien + clientID: a0022dad87aac5fea6bc #生成的clientID + clientSecret: eb4dc34f917d1982dd2730868c0f4fe756097913 #生成的clientSecret + repo: dex0423.github.io #仓库名称 + owner: dex0423 #github用户名 + admin: dex0423 distractionFreeMode: true #是否启用类似FB的阴影遮罩 @@ -86,7 +86,7 @@ gitalk: # Featured Tags featured-tags: true # 是否使用首页标签 -featured-condition-size: 1 # 相同标签数量大于这个数,才会出现在首页 +featured-condition-size: 0 # 相同标签数量大于这个数,才会出现在首页 @@ -99,13 +99,14 @@ service-worker: true # 友情链接,请尽量不要删除,谢谢! friends: [ { - title: "星球国际语", - href: "https://globien.gitee.io" - },{ - title: "星球语互动社区", - href: "https://tieba.baidu.com/f?kw=%E6%A0%BC%E7%BD%97%E6%AF%94%E8%A8%80&fr=home" - },{ - title: "知乎:人造国际语", - href: "https://zhuanlan.zhihu.com/Artificial-International-Language" - } + title: "简书博客", + href: "https://www.jianshu.com/u/87b1e5423400" + }, +# { +# title: "星球语互动社区", +# href: "https://tieba.baidu.com/f?kw=%E6%A0%BC%E7%BD%97%E6%AF%94%E8%A8%80&fr=home" +# },{ +# title: "知乎:人造国际语", +# href: "https://zhuanlan.zhihu.com/Artificial-International-Language" +# } ] diff --git "a/_drafts/2022-01-01-\346\216\250\350\215\220\347\263\273\347\273\237\357\274\232ALS \344\272\244\346\233\277\346\234\200\345\260\217\344\272\214\344\271\230\346\263\225.md" "b/_drafts/2022-01-01-\346\216\250\350\215\220\347\263\273\347\273\237\357\274\232ALS \344\272\244\346\233\277\346\234\200\345\260\217\344\272\214\344\271\230\346\263\225.md" new file mode 100644 index 00000000000..4e16bc8735b --- /dev/null +++ "b/_drafts/2022-01-01-\346\216\250\350\215\220\347\263\273\347\273\237\357\274\232ALS \344\272\244\346\233\277\346\234\200\345\260\217\344\272\214\344\271\230\346\263\225.md" @@ -0,0 +1,33 @@ +--- +layout: post +title: 推荐系统:ALS 交替最小二乘法 +subtitle: 用户评分矩阵 +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 推荐系统 +--- + + + +#### 矩阵因式分解 + + +- 推荐系统弊端:审美疲劳 + + ![]({{site.baseurl}}/img-post/用户画像-11.png) + + +#### 显示反馈 VS 隐式反馈 + + ![]({{site.baseurl}}/img-post/推荐系统-1.png) + +#### 协同过滤( collaborative filtering) + + + + + + diff --git "a/_drafts/2022-01-01-\346\216\250\350\215\220\347\263\273\347\273\237\357\274\232\347\232\256\345\260\224\351\200\212\347\233\270\345\205\263\347\263\273\346\225\260.md" "b/_drafts/2022-01-01-\346\216\250\350\215\220\347\263\273\347\273\237\357\274\232\347\232\256\345\260\224\351\200\212\347\233\270\345\205\263\347\263\273\346\225\260.md" new file mode 100644 index 00000000000..c8cd22e320e --- /dev/null +++ "b/_drafts/2022-01-01-\346\216\250\350\215\220\347\263\273\347\273\237\357\274\232\347\232\256\345\260\224\351\200\212\347\233\270\345\205\263\347\263\273\346\225\260.md" @@ -0,0 +1,33 @@ +--- +layout: post +title: 推荐系统:ALS 交替最小二乘法 +subtitle: 用户评分矩阵 +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 推荐系统 +--- + + + +https://blog.csdn.net/chenxy_bwave/article/details/121576303?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-5-121576303-blog-102665790.pc_relevant_multi_platform_whitelistv2_exp3w&spm=1001.2101.3001.4242.4&utm_relevant_index=8 + + + + +https://www.jianshu.com/p/fa683b8b139d + + + + +https://blog.csdn.net/qq_40459859/article/details/106508228 + + +https://blog.csdn.net/NoBuggie/article/details/102665790 + + + + + diff --git "a/_drafts/2022-01-01-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\345\256\236\346\227\266\346\225\260\344\273\223\346\220\255\345\273\272\346\226\271\346\263\225.md" "b/_drafts/2022-01-01-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\345\256\236\346\227\266\346\225\260\344\273\223\346\220\255\345\273\272\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..f4cfd3fca51 --- /dev/null +++ "b/_drafts/2022-01-01-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\345\256\236\346\227\266\346\225\260\344\273\223\346\220\255\345\273\272\346\226\271\346\263\225.md" @@ -0,0 +1,49 @@ +--- +layout: post +title: 数仓建模:实时数仓搭建方法 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓建模 +--- + + +# 1. 实时数仓 + +目前不少公司都在尝试以Flink、Kudu为基础的实时数仓架构,里面的数仓分层模型和离线的数仓架构基本相同。 + +下图为实时数仓架构,离线和实时的差不多,画图好难 ,所以在网上拷贝了这个图,如侵删,具体实时的架构图见: + + ![]({{site.baseurl}}/img-post/实时数仓-1.png) + + + +1、ODS原始层是存放原始数据,主要是埋点数据(日志数据)和业务操作数据(binlong),数据源主要是Mysql、HDFS、Kafka等 + +2、DW中间层主要存放ETL和主题汇总之后的中间层数据,这块又分为: + +DWD:事实表(data warehouse detail) 数据仓库明细表,以业务过程作为建模驱动,基于每个具体的业务过程特点,构建最细粒度的明细层事实表。 +DWS:事实表 (data warehouse summary) 数据仓库轻度汇总层,按照各个业务域进行轻度汇总成分析某一个主题域的服务数据,一般是宽表。 +DIM:维度表,公共维度层,基于维度建模理念思想,建立整个业务过程的一致性维度,主要使用 MySQL、Hbase、Redis 三种存储引擎,对于维表数据比较少的情况可以使用 MySQL,对于单条数据大小比较小,查询 QPS 比较高的情况,可以使用 Redis 存储,降低机器内存资源占用,对于数据量比较大,对维表数据变化不是特别敏感的场景,可以使用HBase 存储。 +3、DM数据集市层,以数据域+业务域的理念建设公共汇总层,对于DM层比较复杂,需要综合考虑对于数据落地的要求以及具体的查询引擎来选择不同的存储方式,分为轻度汇总层和高度汇总层。 + +轻度汇总层以宽表的形式存在,主要是针对业务域进行快速方便的查询; +高度汇总层由明细数据层或轻度汇总层通过聚合计算后写入到存储引擎中,产出一部分实时数据指标需求,灵活性比较差,主要做大屏展现。 +4、理论上上面还一APP层,应用层,主要是通过这几层之后,生成轻度或者高度汇总的数据,然后根据业务域进行接口封装提供给上层使用。 + +但是实时数仓面临以下几个实施关键点: + +端到端数据延迟、数据流量的监控; +故障的快速恢复能力; +数据的回溯处理,系统支持消费指定时间段内的数据; +实时数据从实时数仓中查询,T+1数据借助离线通道修正; +业务数据质量的实时监控; + + + +思考: + +实时数仓架构和数据中台一样,虽然都是属于当前比较热门的概念,但是对于实时数仓的狂热追求大可不必。首先,在技术上几乎没有难点,基于强大的开源中间件(例如:Flink、kudu等)实现实时数据仓库的需求已经变得没有那么困难。其次,实时数仓的建设一定是伴随着业务的发展而发展,武断的认为实时数仓架构最符合当前公司的需求是不对的。实际情况中随着业务的发展数仓的架构变得没有那么非此即彼。最后,如何顺畅的将传统的离线数仓+实时链路处理流程升级到实时数仓架构是个很大的问题,毕竟中间涉及到很多的数据模式、技术中间件、计算引擎都不太一样。 \ No newline at end of file diff --git "a/_drafts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\237\272\344\272\216 TF-IDF \347\256\227\346\263\225\347\232\204\346\240\207\347\255\276\346\235\203\351\207\215\346\250\241\345\236\213.md" "b/_drafts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\237\272\344\272\216 TF-IDF \347\256\227\346\263\225\347\232\204\346\240\207\347\255\276\346\235\203\351\207\215\346\250\241\345\236\213.md" new file mode 100644 index 00000000000..a64d9cd3ce0 --- /dev/null +++ "b/_drafts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\237\272\344\272\216 TF-IDF \347\256\227\346\263\225\347\232\204\346\240\207\347\255\276\346\235\203\351\207\215\346\250\241\345\236\213.md" @@ -0,0 +1,14 @@ +--- +layout: post +title: 用户画像:基于 TF-IDF 算法的标签权重模型 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + + \ No newline at end of file diff --git "a/_drafts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\345\205\264\350\266\243\345\201\217\345\245\275\346\240\207\347\255\276\350\256\241\347\256\227\351\200\273\350\276\221.md" "b/_drafts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\345\205\264\350\266\243\345\201\217\345\245\275\346\240\207\347\255\276\350\256\241\347\256\227\351\200\273\350\276\221.md" new file mode 100644 index 00000000000..eb3ebe7d127 --- /dev/null +++ "b/_drafts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\345\205\264\350\266\243\345\201\217\345\245\275\346\240\207\347\255\276\350\256\241\347\256\227\351\200\273\350\276\221.md" @@ -0,0 +1,31 @@ +--- +layout: post +title: 用户画像:用户兴趣偏好标签计算逻辑 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +# 互动行为 + + +# 互动深度 + + +# 权重设置 + + + + + + +# 1. 用户标签的分类 + + +- 示例: + ![]({{site.baseurl}}/img-post/标签体系-5.png) diff --git "a/_drafts/2022-01-01-\347\273\237\350\256\241\357\274\232\347\233\270\345\205\263\345\210\206\346\236\220.md" "b/_drafts/2022-01-01-\347\273\237\350\256\241\357\274\232\347\233\270\345\205\263\345\210\206\346\236\220.md" new file mode 100644 index 00000000000..37492683adb --- /dev/null +++ "b/_drafts/2022-01-01-\347\273\237\350\256\241\357\274\232\347\233\270\345\205\263\345\210\206\346\236\220.md" @@ -0,0 +1,47 @@ +--- +layout: post +title: 统计:相关分析 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 推荐系统 +--- + + +#### 相关关系 + +- 线性关系 + - 由一条直线,表示两个变量之间的消长关系; +- 曲线关系 + - TODO + + +#### 相关关系 + +- 正相关 + - x 增则 y 增; +- 负相关 + - x 增则 y 减; + +#### 相关系数 + +- 计算 +- 检验 + +#### 皮尔逊相关系数 +- 描述【定量变量】之间关系; + +#### 斯皮尔曼相关系数 +- 定序【变量之间】关系; + + + + ![]({{site.baseurl}}/img-post/用户画像-11.png) + + + +# 推荐模型效果测试 + diff --git "a/_drafts/2022-01-02-\345\206\205\345\256\271\350\277\220\350\220\245\357\274\232\345\206\205\345\256\271\350\277\220\350\220\245\347\232\204\345\205\263\351\224\256\346\214\207\346\240\207.md" "b/_drafts/2022-01-02-\345\206\205\345\256\271\350\277\220\350\220\245\357\274\232\345\206\205\345\256\271\350\277\220\350\220\245\347\232\204\345\205\263\351\224\256\346\214\207\346\240\207.md" new file mode 100644 index 00000000000..4ba5d55fb62 --- /dev/null +++ "b/_drafts/2022-01-02-\345\206\205\345\256\271\350\277\220\350\220\245\357\274\232\345\206\205\345\256\271\350\277\220\350\220\245\347\232\204\345\205\263\351\224\256\346\214\207\346\240\207.md" @@ -0,0 +1,183 @@ +--- +layout: post +title: 内容运营:内容运营的关键指标 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 内容运营 +--- + + +# 1. 内容运营关键流程 + +#### 1.1. 内容发布 + +- PGC +- UGC + +#### 1.2. 内容曝光 + +- 资源位 +- PUSH +- FEED + +#### 1.3. 内容点击 + +- 软连接 +- 详情页 + + +#### 1.4. 用户阅读 + +- 中途跳出 +- 阅读完毕 + +#### 1.5. 用户评论 + +- 评论内容 +- 正负向 + +#### 1.6. 用户转发 + +- 转发渠道 + - ins + - 朋友圈 + - Facebook + - …… + +# 2. 发布内容环节指标 + +#### 2.1. 内容生产指标 + +- 内容更新数量 + - 深度原创 + - 伪原创 + - 转载 + - 超链接导入 + +- 参与生产用户情况 + - 参与生产用户数 + - 用户生产内容的时间分布 + - 各时间段发帖数 + - 各时间段发帖用户量 + +#### 2.2. 内容发布指标 + +- 内容发布趋势 + - 数量增长 + - 数量下降 + - 无更新 + +- 内容发布频率 + - 含义 + - 内容发布频率 = 更新数量 / 时间周期 + - 作用 + - 反应图文更新频率 + +- 发布时间 + - 时间点 + - 发布数量 + +# 3. 内容曝光环节指标 + +- 站内曝光量 +- 站外渠道分发量 +- push + - 达到率 + - 点开率 +- banner + - 曝光数在 APP 日活中的占比 +- feed + - 不同位置内容曝光占比 + +# 4. 内容点击环节指标 + +- 周期内点击人数 +- 统计时间周期 + +- 内容平均点击人数 + - 含义 + - 平均点击人数 = 内容点击数 / 内容数量 + - 作用 + - 体现内容水平 + +- 内容平均阅读量趋势 + - 指标含义: + - 体现内容质量是否有提升 + - 体现影响力是否扩大 + - 作用 + - 是否找到有效的内容传播增长途径 + - 例如: + - 曝光渠道增加 + - 选题正确 + +# 5. 内容阅读环节指标 + +- 用户在线时长 + - 含义 + - 一个会话周期内,用户阅读内容的时间总和 + - 作用 + - 可以评估内容的质量 + - 停留时间短,说明质量和内容深度不够 + - 是广告变现、知识付费平台的重要指标 +- 人均阅读时长 + - 含义 + - 人均阅读时长 = 用户在线时长 / 用户数量 + - 作用 + - 评估内容的质量 +- 人均阅读次数 + - 含义 + - 人均阅读次数 = 内容阅读次数 / 内容点击人数 + - 作用 + - 反应内容的用户粘性 +- 阅读完成率 / 完播率 + - 含义 + - 阅读完成率 = 完成阅读人数 / 文章阅读总人数 + - 作用 + - 体系内容写作质量的高低 + - 反应内容对用户的友好程度 + - 应用 + - 头条、小红书、抖音,都会对内容阅读完成率、视频完播率进行监测 + +# 6. 评论环节指标 + +- 评论人数 +- 评论率 + - 含义 + - 评论率 = 评论人数 / 内容点击人数 +- 评论量 + - 总评论次数 +- 人均平均数量 + - 含义 + - 人均平均数量 = 总评论数量 / 总评论人数 +- 内容平均评价量 + - 含义 + - 内容平均评价量 = 总评论数量 / 内容数量 +- 内容平均评价量趋势 + - 含义 + - 增长 / 下降 + - 作用 + - 辅助归因分析 +- 最高内容评论量 + - 最高评分数值对应的文章 + - 分析内容特征 + - 总结内容选题和编辑技巧 + +# 7. 用户分享指标 + +- 内容分享人数 +- 内容分享率 +- 内容分享量 +- 内容人均分享量 +- 内容平均分享量 +- 内容平均分享量趋势 +- 内容最高分享量 + + + + + + diff --git "a/_drafts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\345\270\270\350\247\201\347\232\204\345\275\222\345\233\240\345\210\206\346\236\220\346\226\271\346\263\225.md" "b/_drafts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\345\270\270\350\247\201\347\232\204\345\275\222\345\233\240\345\210\206\346\236\220\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..28a1f37caf9 --- /dev/null +++ "b/_drafts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\345\270\270\350\247\201\347\232\204\345\275\222\345\233\240\345\210\206\346\236\220\346\226\271\346\263\225.md" @@ -0,0 +1,26 @@ +--- +layout: post +title: 数据产品:数据产品的基本概念 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 数据产品 +--- + + +常用的归因包括:首次点击、末次点击、多点归因等,根据分配方式又可以分为线性归因、时间衰减、马尔可夫、shap值分解等。我们在站外拉新召回、站内页面导购、用户触达三个电商常用场景我们沉淀了三个通用归因能力: + + + +渠道归因:核心解决如何衡量站外各组合营销渠道广告带来的转化效果,从而更好得指导广告投放。主要是通过将每一个激活和订单,通过合理规则归属到一个渠道上,在此基础搭建分析体系。严选渠道归因采用业界最常用的单点末次归因。一个订单用户被多个渠道干预的情况下,订单将归属于:在订单提交之前的、优先级最高的、事件时间最晚的访问记录的渠道。 + + + +页面导购归因:按照app用户浏览链路划分必经页面入口页与承接页,主要分为末次归因、三步多点归因两种方式对用户行为进行追踪,通过导购链路归属订单下单来源,从而量化站内坑位流量转化价值。 + + + +触达归因:主要应用于主站体系内的push/短信/弹窗等自动手动营销效果归因,结合场景和效期选取了时间衰减模型多点归因,核心思路为两点:借助时间衰减曲线初步确定权重基数(时间距因子),个性化场景权重系数调整。 \ No newline at end of file diff --git "a/_drafts/2022-01-24-DMP\357\274\232DMP \347\263\273\347\273\237\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_drafts/2022-01-24-DMP\357\274\232DMP \347\263\273\347\273\237\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..ac1c38f66c1 --- /dev/null +++ "b/_drafts/2022-01-24-DMP\357\274\232DMP \347\263\273\347\273\237\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,89 @@ +--- +layout: post +title: 用户画像:用户画像系统的概念详解 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +DMP即Data Management Platform,数据管理平台。近几年,随着大数据与程序化广告的普及和推广,DMP市场高速增长,2020年市场规模预计将突破2000亿(Source: Mob研究院)。 + + +![]({{site.baseurl}}/img-post/DMP-1.png) + + +DMP的核心思想,是记录每一个个体用户在不同营销触点上的“交互痕迹”,基于这些痕迹,区分不同用户的特征,并对不同特征的用户群体提供针对性营销策略,或输出这些人群作为细分受众给其他营销机构。 + +Part 1 DMP是什么?有哪些功能? + +DMP是一个全面的数据收集、加工、整合平台,吸收各种数据源数据,以用户为基本单位,清洗、整理形成结构化的数据表,并进行用户标签计算,以期能够精准描述各种用户。 + +DMP不带有任何营销性质和功能,它可以和DSP(Demand-Side Platform,需求方平台), CMS(Content Management System,内容管理系统), CRM(Customer Relationship Management,客户关系管理系统)等产品结合,有一些数据上的流通,DMP的核心就是将数据处理好。强大的DMP是互联网时代的数据大脑,是一切营销的基础。 + +DMP的功能 + +精准营销、广告投放、个性化推荐、其他数据分析应用 + +DMP核心特征 + +有很多数据源,包括企业内数据(第一方),合作方数据(第二方),外部第三方数据; +以用户(或设备)为基本单位,整合各方数据,将数据进行清洗,结构化的存储; +对结构化的数据进行分析、运行算法,将用户进行多维定性,打上标签; +使用方可以通过DMP提取想要的用户群体,下游的营销产品也可以直接通过接口进行数据的访问和提取。 +Part 2 DMP的基础架构及数据加工流程 + +一个DMP需要的功能模块有:①数据获取入口、②数据清洗&整合模块、③标签计算&挖掘模块、④打标签及标签展示模块、⑤用户分群模块。其中,①②③是DMP的基础,决定了一个DMP的质量;④⑤比较偏产品前端,DMP是否简单易用,是否友好易懂,由这部分体现出来。 + + + +Part 3 DMP分类 + +1.核心互联网大厂自有DMP + +如阿里妈妈的达摩盘,腾讯的广点通,百度的DMP数据服务;其他诸如字节跳动、京东等大公司也会有自己的DMP。核心互联网大厂拥有绝对的用户流量优势,因此数据积累量非常大,但各大厂之间的数据却相对封闭,不流通共享。 + +此外,各大厂的DMP也会根据自己的业务进行一些深度定制,如阿里的达摩盘是面向电商场景的DMP,通用性可能不高。 + +2.广告技术公司自有DMP + +这类DMP主要是广告技术公司基于DSP等业务扩展需求而搭建,直接为业务服务的。美国市场非常细分,有很多专门的DMP公司;中国市场高度整合,DMP需求往往与 DSP/SSP/Trading Desk等联系在一起,广告技术公司自有 DMP能够更直接地满足这类整合需求。 + +3.广告主自有DMP + +这类 DMP的数据相对封闭,数据安全性较好,但相应的流通性较差,数据挖掘和使用不足,通常需要接入其他数据平台,实现数据的融合和流通后,才能更好地发挥价值。 + +4.独立第三方DMP + +随着大数据技术的发展,精准营销已经成为企业用户增长的必选方案。因此,出现了一些专门为企业客户提供服务的第三方数据供应商,即第三方DMP。 + +第三方DMP相对独立,也比较专业,企业可以快速获取期望的用户数据。但目前国内市场对于数据交易、数据交换还没有相应的执行标准,数据安全也无法得到很好的保障。 + +Part 4 DMP在营销中的主要应用方向——用户画像 + +关于用户画像,分为两类定义:User Persona和User Profile。 + +l User Persona:是产品设计、运营人员从用户群体中抽象出来的典型用户。例如,在用户调研阶段,产品经理经过调查问卷、客户访谈了解用户的共性与差异,汇总成不同的虚拟用户。 + +l User Profile:根据每个人在产品中的用户行为数据,输出描述用户的标签的集合。例如猜测这个用户的性别,生活工作所在地,兴趣爱好,喜欢哪个明星,要买什么东西等。 + +* 以下所提到的用户画像,均为User Profile。 + +用户画像与DMP的关系 + +DMP作为数据管理平台,在数据输入其中后,会输出一系列标签,或其他想要的结果。用户画像则是输入用户数据到DMP,然后DMP输出了用户标签。用户画像是DMP在营销中的主要应用方向。用户画像的常见应用有个性化推荐(电商、资讯类产品)、风控、预测等。 + +其实,DMP不只可以输出用户标签,也可以输出其他的标签,如输入文章,输出文章标签等。 + +用户画像怎么做 + + + +基础数据收集:收集用户在产品/服务使用过程中的静态数据和动态数据; +行为建模:基于用户的基础数据,通过技术手段进行行为建模; +构建画像:通过行为建模,输出一系列的用户标签,每个用户的标签都可以形成一个集合,这个标签的集合可以表示出该用户的特点。 +完善的标签体系可以帮助广告主做多维度、全方位的用户画像刻画与洞察,从而实现广告的精准定向。广告主可以利用标签体系自由选择、组合投放人群特征和行为,精准锁定目标受众进行广告投放。同时,广告主也可以利用标签体系对自身的种子人群数据进行深度挖掘,根据种子人群特征拓展更多相似人群,扩大广告投放的潜在用户群。 diff --git "a/_drafts/2022-01-24-SQL\357\274\232HASH JOIN & NEST LOOP JOIN & SORT MERGE JOIN.md" "b/_drafts/2022-01-24-SQL\357\274\232HASH JOIN & NEST LOOP JOIN & SORT MERGE JOIN.md" new file mode 100644 index 00000000000..f0049f90951 --- /dev/null +++ "b/_drafts/2022-01-24-SQL\357\274\232HASH JOIN & NEST LOOP JOIN & SORT MERGE JOIN.md" @@ -0,0 +1,20 @@ +--- +layout: post +title: SQL:SQL 常用窗口函数 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + + +2022-01-24-SQL:SQL 常用窗口函数.md + + + + + + diff --git "a/_drafts/2022-01-24-SQL\357\274\232SQL \345\270\270\347\224\250\347\252\227\345\217\243\345\207\275\346\225\260.md" "b/_drafts/2022-01-24-SQL\357\274\232SQL \345\270\270\347\224\250\347\252\227\345\217\243\345\207\275\346\225\260.md" new file mode 100644 index 00000000000..c07fc76e907 --- /dev/null +++ "b/_drafts/2022-01-24-SQL\357\274\232SQL \345\270\270\347\224\250\347\252\227\345\217\243\345\207\275\346\225\260.md" @@ -0,0 +1,264 @@ +--- +layout: post +title: SQL:SQL 常用窗口函数 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + + +# SELECT 语句执行顺序 + +- SELECT +- DISTINCT + - 数据除重 +- FROM + - <表名> # 选取表,将多个表数据通过笛卡尔积变成一个表。 +- ON + - <筛选条件> # 对笛卡尔积的虚表进行筛选 +- JOIN + - # 指定join,用于添加数据到on之后的虚表中,例如left join会将左表的剩余数据添加到虚表中 +- WHERE + - # 对上述虚表进行筛选 +- GROUP BY + - <分组条件> # 分组 + # 用于having子句进行判断,在书写上这类聚合函数是写在having判断里面的 +- HAVING + - <分组筛选> # 对分组后的结果进行聚合筛选 +- SELECT + - <返回数据列表> # 返回的单列必须在group by子句中,聚合函数除外 +- ORDER BY + - <排序条件> # 排序 +- LIMIT + - <行数限制> + +# 多表查询 + +#### 表示例 + +![employee]({{site.baseurl}}/img-post/employee.png) + +![department]({{site.baseurl}}/img-post/department.png) + +![salary_grade]({{site.baseurl}}/img-post/salary_grade.png) + +![salary]({{site.baseurl}}/img-post/salary.png) + +![manager]({{site.baseurl}}/img-post/sql-manager.png) + + +#### 等值连接 & 非等值连接 + +```aidl +SELECT + a.employee_id, b.salary_grade +FROM + salary AS a, salary_grade AS b +WHERE + a.salary_amount BETWEEN b.lowest_salary AND b.highest_salary; +``` + +![sql-1]({{site.baseurl}}/img-post/sql-1.png) + + +#### JOIN 使用 + +![sql-5]({{site.baseurl}}/img-post/sql-5.png) + +#### LEFT JOIN + +```aidl +SELECT + a.employee_id,a.employee_name,b.manager_id,c.manager_name +FROM + employee AS a +LEFT JOIN + department AS b +ON + a.department_id=b.department_id +LEFT JOIN + manager AS c +ON + c.manager_id=b.manager_id; +``` + +![sql-3]({{site.baseurl}}/img-post/sql-3.png) + + +#### UNION VS UNION ALL + +![sql-4]({{site.baseurl}}/img-post/sql-4.png) + +- UNION ALL 操作符返回两个查询的结果集的并集时,对于结果集的重复部分 **不去重**。 +- UNION ALL 不需要执行去重操作,执行时所需要的资源比 UNION 少; +- 如果明知道合并后的数据结果不存在重复数据,或者不需要去重,则可以使用 UNION ALL,以提高查询的效率。 +- 使用 UNION ALL 需要注意,是否存在重复数据! + + +# 流程控制函数 + +#### IF + +``` +IF(value,value1,value2) +``` +- 如果 value 值为 True,则返回 value1,否则返回 value2; + +#### IF NULL +``` +IF NULL(value1,value2) +``` +- 如果 value1 为 NULL 则返回 value2,否则返回 value1; + +#### CASE WHEN + +```aidl +CASE +WHEN 条件1 THEN 结果1 +WHEN 条件2 THEN 结果2 +WHEN 条件3 THEN 结果3 +WHEN 条件4 THEN 结果4 +... +ELSE 结果n +END +``` +- 相当于 python 的 `if ... elif ... else ...` + +```aidl +CASE expr +WHEN 常量值1 THEN 值1 +WHEN 常量值2 THEN 值2 +WHEN 常量值3 THEN 值3 +... +ELSE 值n +END +``` +- 相当于 JAVA 的 `swith ... case ...` + +# 聚合函数 + +#### AVG / SUM + + +#### MAX / MIN + + +#### COUNT + +- COUNT 执行时不会计算 NULL; +- InnoDB 引擎下,COUNT(*) = COUNT(1) > COUNT(字段); +- MyISAM 引擎下,COUNT(*) = COUNT(1) = COUNT(字段); + +#### GROUP BY + +- 使用单个列分组: +```aidl +SELECT + dep_id, AVG(salary_amnt) AS dep_avg_sal_amnt +FROM + ( + SELECT + a.employee_id AS emp_id,a.salary_amount AS salary_amnt,b.department_id AS dep_id + FROM + test.salary AS a + LEFT JOIN + test.employee AS b + ON + a.employee_id=b.employee_id + ) c +GROUP BY + dep_id + ; +``` + +![sql-7]({{site.baseurl}}/img-post/sql-7.png) + +- 使用多个列分组: + +```aidl +SELECT + dep_id, job_id, AVG(salary_amnt) AS dep_avg_sal_amnt +FROM + ( + SELECT + a.employee_id AS emp_id,a.salary_amount AS salary_amnt,b.department_id AS dep_id,b.job_id AS job_id + FROM + test.salary AS a + LEFT JOIN + test.employee AS b + ON + a.employee_id=b.employee_id + ) c +GROUP BY + dep_id,job_id + ; +``` + +![sql-6]({{site.baseurl}}/img-post/sql-6.png) + +#### HAVING + +- 如果过滤条件中使用了聚合函数,则必须使用 HAVING 来替代; +- 如果过滤条件中没有使用聚合函数,则使用 WHERE 和 HAVING 都可以,但一般使用 WHERE,因为 **WHERE 的执行效率更高**; +- HAVING 必行声明在 GROUP BY 后面; +- 如果没有使用 GROUP BY,则没有必要使用 HAVING; + +```aidl +SELECT + job_id, AVG(salary_amnt) AS dep_avg_sal_amnt +FROM + ( + SELECT + a.salary_amount AS salary_amnt,b.department_id AS dep_id,b.job_id AS job_id + FROM + test.salary AS a + LEFT JOIN + test.employee AS b + ON + a.employee_id=b.employee_id + ) c +GROUP BY + job_id +HAVING + AVG(salary_amnt)>7000 + ; +``` + +![sql-8]({{site.baseurl}}/img-post/sql-8.png) + + +# 常用经验 + +#### 一旦给表起了别名,就必须使用这个别名、不能再使用原名。 + +```aidl +SELECT + a.employee_name +FROM + employee AS a +``` + +#### 从 SQL 优化的角度考虑,多表查询时、每个字段前都应该指明字段所在的表; + +```aidl +SELECT + a.employee_name, + b.department_name +FROM + employee AS a,department AS b +WHERE + a.department_id = b.department_id +``` + +![sql-2]({{site.baseurl}}/img-post/sql-2.png) + + + + + + + diff --git "a/_drafts/2022-01-24-SQL\357\274\232\350\264\242\345\212\241\347\273\237\350\256\241\344\270\255\347\232\204\345\260\276\345\267\256\351\227\256\351\242\230\345\217\212\350\247\243\345\206\263\346\200\235\350\267\257.md" "b/_drafts/2022-01-24-SQL\357\274\232\350\264\242\345\212\241\347\273\237\350\256\241\344\270\255\347\232\204\345\260\276\345\267\256\351\227\256\351\242\230\345\217\212\350\247\243\345\206\263\346\200\235\350\267\257.md" new file mode 100644 index 00000000000..f7809e6c14f --- /dev/null +++ "b/_drafts/2022-01-24-SQL\357\274\232\350\264\242\345\212\241\347\273\237\350\256\241\344\270\255\347\232\204\345\260\276\345\267\256\351\227\256\351\242\230\345\217\212\350\247\243\345\206\263\346\200\235\350\267\257.md" @@ -0,0 +1,45 @@ +--- +layout: post +title: SQL:财务统计中的尾差问题及解决思路 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + +# 尾差问题 + +- 在财务、金融、汇率、税务等统计时,经常会遇到数据计算以后数据不一致问题; +- 在技术层面这个问题主要是由于小数四舍五入导致的,而金额和单价、数量的小数位数都是有限的,这就会导致尾差问题。 + +# 解决思路 + +#### 减少乘法除法、计算次数 + + + +#### 多用加法、少用乘法 + +- 例如: + - 订单上可能有这么几个字段:数量、单价、含税单价、税率、金额、税额、价税合计。 + - 关系如下: + - 数量×单价=金额 + - 数量×含税单价=价税合计; + - 数量×单价×税率=税额; + - 金额+税额=价税合计。 + +- 如果这里分别用乘法算出金额、税额、价税合计,那么最后很可能就出现金额+税额不等于价税合计。 + +对于这种情况,只能用乘法算出其中的两个,然后第三个,就其中的这两个金额做加法或者减法得到。例如,先用乘法算出金额和税额,然后用金额+税额得出价税合计;或者先用乘法算出价税合计和税额,然后用减法,算出金额。这样就不会产生误差。 + +2、注意用“倒挤”算法(分摊误差)即计算尾差 +数量乘以单价不等于金额的现象肯定存在,这是允许的,实际手工业务中也是允许的,因为四舍五入的关系,谁也避免不了这个;但是软件中的做法是:允许误差,但是误差们要能自圆其说。时刻牢记要让“总计数要等于明细数的合计”。例如,总计数是A,总计数被分成了若干份,分别是B、C、D,最后一定要保证:B+C+D=A。 +例如,一笔钱是456.87元,要按比例分成三份,第一份是15.8%,第二份是47.3%,第三份是36.9%,没有经验的程序员肯定是用总金额分别乘以三个比例,那肯定就有问题了,第一份:456.87×0.158=72.19。第二份:456.87×0.473=216.1,第三份:456.87×0.369=168.59,那么把这三个数再合计起来等于多少?72.19+216.1+169.59=456.88。比原来的总额多出了一分钱!这就不平了。所以,碰到这种情况,一定要选择分摊误差的地方,一般来说都是用最后一笔分担误差,所以叫做倒挤算法。所以这个例子应该这么算,前两笔都用乘法没问题,到了最后就一定不能总额乘以比例了,而是要用总额减去前面两笔的合计数,得出第三笔:456.87-(72.19+216.1)=156.58。这样就对了。 + +3、每一次乘除法以后立即按照精度四舍五入 +一般在软件系统中会预先让用户定义数量、单价、金额的小数位数,这是常识性的做法,但是程序员必须在程序中注意四舍五入的时机:在得出一个结果(数量、单价、金额三者任何一个)之后,必须立即四舍五入以后,再投入其他运算,如果做了一次乘法、除法以后却没有立即根据定义的精度四舍五入,而是直接投入其他运算,最后再四舍五入,那肯定不对。例如:单价:8.49,数量:45.5789,用数量乘以单价得出金额:386.964861,如果你这时候你不把这个金额先四舍五入了再投入其他运算,那么肯定产生误差。 + +4、写程序注意选择相同类型的数值类型进行乘除运算。如果不相同类型的数值类型乘除,计算机自己都有可能产生误差,我就碰到过计算机算出:3.1×2=6.3这种情况 diff --git "a/_drafts/2022-01-24-\345\256\236\346\227\266\346\225\260\344\273\223\357\274\232Flink + ClickHouse \345\256\236\346\227\266\346\225\260\344\273\223.md" "b/_drafts/2022-01-24-\345\256\236\346\227\266\346\225\260\344\273\223\357\274\232Flink + ClickHouse \345\256\236\346\227\266\346\225\260\344\273\223.md" new file mode 100644 index 00000000000..215a7aa7d56 --- /dev/null +++ "b/_drafts/2022-01-24-\345\256\236\346\227\266\346\225\260\344\273\223\357\274\232Flink + ClickHouse \345\256\236\346\227\266\346\225\260\344\273\223.md" @@ -0,0 +1,48 @@ +--- +layout: post +title: 实时数仓:Flink + ClickHouse 实时数仓 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 实时数仓 +--- + + + + +# 尾差问题 + +- 在财务、金融、汇率、税务等统计时,经常会遇到数据计算以后数据不一致问题; +- 在技术层面这个问题主要是由于小数四舍五入导致的,而金额和单价、数量的小数位数都是有限的,这就会导致尾差问题。 + +# 解决思路 + +#### 减少乘法除法、计算次数 + + + +#### 多用加法、少用乘法 + +- 例如: + - 订单上可能有这么几个字段:数量、单价、含税单价、税率、金额、税额、价税合计。 + - 关系如下: + - 数量×单价=金额 + - 数量×含税单价=价税合计; + - 数量×单价×税率=税额; + - 金额+税额=价税合计。 + +- 如果这里分别用乘法算出金额、税额、价税合计,那么最后很可能就出现金额+税额不等于价税合计。 + +对于这种情况,只能用乘法算出其中的两个,然后第三个,就其中的这两个金额做加法或者减法得到。例如,先用乘法算出金额和税额,然后用金额+税额得出价税合计;或者先用乘法算出价税合计和税额,然后用减法,算出金额。这样就不会产生误差。 + +2、注意用“倒挤”算法(分摊误差)即计算尾差 +数量乘以单价不等于金额的现象肯定存在,这是允许的,实际手工业务中也是允许的,因为四舍五入的关系,谁也避免不了这个;但是软件中的做法是:允许误差,但是误差们要能自圆其说。时刻牢记要让“总计数要等于明细数的合计”。例如,总计数是A,总计数被分成了若干份,分别是B、C、D,最后一定要保证:B+C+D=A。 +例如,一笔钱是456.87元,要按比例分成三份,第一份是15.8%,第二份是47.3%,第三份是36.9%,没有经验的程序员肯定是用总金额分别乘以三个比例,那肯定就有问题了,第一份:456.87×0.158=72.19。第二份:456.87×0.473=216.1,第三份:456.87×0.369=168.59,那么把这三个数再合计起来等于多少?72.19+216.1+169.59=456.88。比原来的总额多出了一分钱!这就不平了。所以,碰到这种情况,一定要选择分摊误差的地方,一般来说都是用最后一笔分担误差,所以叫做倒挤算法。所以这个例子应该这么算,前两笔都用乘法没问题,到了最后就一定不能总额乘以比例了,而是要用总额减去前面两笔的合计数,得出第三笔:456.87-(72.19+216.1)=156.58。这样就对了。 + +3、每一次乘除法以后立即按照精度四舍五入 +一般在软件系统中会预先让用户定义数量、单价、金额的小数位数,这是常识性的做法,但是程序员必须在程序中注意四舍五入的时机:在得出一个结果(数量、单价、金额三者任何一个)之后,必须立即四舍五入以后,再投入其他运算,如果做了一次乘法、除法以后却没有立即根据定义的精度四舍五入,而是直接投入其他运算,最后再四舍五入,那肯定不对。例如:单价:8.49,数量:45.5789,用数量乘以单价得出金额:386.964861,如果你这时候你不把这个金额先四舍五入了再投入其他运算,那么肯定产生误差。 + +4、写程序注意选择相同类型的数值类型进行乘除运算。如果不相同类型的数值类型乘除,计算机自己都有可能产生误差,我就碰到过计算机算出:3.1×2=6.3这种情况 diff --git "a/_drafts/2022-01-24-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\346\225\260\346\215\256\346\214\207\346\240\207\344\275\223\347\263\273\346\220\255\345\273\272\346\226\271\346\263\225\350\256\272.md" "b/_drafts/2022-01-24-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\346\225\260\346\215\256\346\214\207\346\240\207\344\275\223\347\263\273\346\220\255\345\273\272\346\226\271\346\263\225\350\256\272.md" new file mode 100644 index 00000000000..ad575b9df3c --- /dev/null +++ "b/_drafts/2022-01-24-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\346\225\260\346\215\256\346\214\207\346\240\207\344\275\223\347\263\273\346\220\255\345\273\272\346\226\271\346\263\225\350\256\272.md" @@ -0,0 +1,199 @@ +--- +layout: post +title: 指标体系:数据指标体系搭建方法论 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 指标体系 +--- + + +#### 指标 + +- 指标,只面向特定的业务,衡量目标事务的数据。 +- 指标,通常包括以下内容: + - 指标名称 + - 指标定义 + - 计算方法 + - 度量单位 + - 指标数值 + - 计算口径 + - 维度 + +#### 指标分类 + +- 基础指标 +- 复合指标 + +#### 指标体系 + +指标体系 = 指标 + 维度 + +#### 指标体系标准化 + +- 统一命名 +- 统一定义 +- 统一维度 +- 统一粒度 +- 统一口径 +- 统一来源 +- 统一出口 + +#### 指标使用的目的 + +- 监控 +- 归因 / 诊断 +- 预测 +- 指示 + +#### 数据分类 + +- 用户数据 + - 用户的基本属性; +- 行为数据 + - 用户的行为记录; +- 业务数据 + - 用户下单支付情况; + + +#### 事件上报一定是由用户主动操作,才能作为事件上报。 + +- 谨防系统行为,误报为用户行为事件。 + +>使用第三方系统尤其要注意这个问题。 + +#### 统计粒度越细,越能真实的反映问题。 + +#### 统计的维度越多,发现的问题越多。 + +#### 注意用户操作的行为路径,是否绕过关键事件的埋点位置。 + +- 比如:用户收到 push 以后直接进入商品详情页,而没有进入首页,导致首页的埋点数据无法上报。 + +#### 指标分级 + + ![]({{site.baseurl}}/img-post/指标体系方法论-1.png) + +- 某营销活动指标体系示例 + + ![]({{site.baseurl}}/img-post/指标体系方法论-2.png) + +- 指标体系方法论 + + ![]({{site.baseurl}}/img-post/指标体系方法论-3.png) + + + +#### 指标体系常见问题 + +- 指标命名模糊、混乱 +- 指标为左描述和解释 +- 指标未做分级和归类 +- 指标未做日常维护 + +#### 指标问题解决方案 + +- 同一指标命名规则 +- 指标字典 +- 指标分级拆解 & 分类管理 +- 及时更新处理新指标和废弃指标 + +#### OneID + +- 用户唯一标识 + - 登录账户 ID + - 设备 ID +- 登录依赖 + - 登录价值 + - 非登录用户对于业务是否有价值; + - 强依赖登录 + - 基于登录账户 + - 弱依赖登录 + - 基于设备 + + +# 活动示例:提升 DAU + +- 活动策划 + - 个性化素材 + - 活动吸引力 +- 渠道触达 + - 投放渠道 + - 覆盖人群 + - push 频次 + - push 时间 + - 触达方式 +- 活动分发 + - 提升曝光 + - 提升点击 +- 活跃互动 + - 游戏 + - 趣味性 + - 难易度 + - 反馈 + - 奖励提示 +- 任务奖励 + - 红包 + - 代币 + - 道具 + - 卡券 +- 产生消费 + - 引导消费 + - 引导支付 +- 分享裂变 + - 引导分享 + - 引导关注 + +- 过程控制 + - 节省时间 + - 丰富内容 + - 促进交易 + - 促进支付 + - 控制成本 + - 反作弊 + + +# 指标设计规范 + +#### 指标规范设计目的 + +- 指标定义标准化: + - 名称: + - 原子指标的英文名称、中文名称、概述; + - 口径: + - 主题域内,指标口径必须一致; + - 统一对外输出的数据口径,避免同一指标不同口径的情况发生。 + - 含义: + - 主题域内,无歧义; + - 数据出口: + - 通过数据分层,提供统一的数据出口; + - 方便平台化开发。 + +#### 指标分类 + +- 原子指标: + - 原子指标三要素: + - 业务过程; + - 度量值; + - 聚合逻辑; +- 派生指标: + - 在原子指标的基础之上,进行一系列逻辑运算得出。 + +#### 指标梳理 + +- 原子指标的归属产线、业务板块、数据域、业务过程; +- 原子指标的统计数据,来源于该业务过程下的原始数据源; +- 指标计算函数; +- 根据指标函数,自动生成原子指标的定义表达式; +- 根据指标定义、表达式以及数据源表,生成原子指标SQL; +- 注意: + - 在数据治理中,我们将需求梳理到的所有指标进行进一步梳理,明确其口径; + - 指标口径的不一致,使得数据使用的成本极高,经常出现口径打架、反复核对数据的问题; + - 如果存在两个指标名称相同,但口径不一致,先判断是否是进行合并,如需要同时存在,那么在命名上必须能够区分开。 + + + + diff --git "a/_drafts/2022-01-24-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232ClickHose \347\224\250\346\210\267\347\224\273\345\203\217.md" "b/_drafts/2022-01-24-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232ClickHose \347\224\250\346\210\267\347\224\273\345\203\217.md" new file mode 100644 index 00000000000..9b82d40bca0 --- /dev/null +++ "b/_drafts/2022-01-24-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232ClickHose \347\224\250\346\210\267\347\224\273\345\203\217.md" @@ -0,0 +1,13 @@ +--- +layout: post +title: 用户画像:ClickHose 用户画像 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + diff --git "a/_drafts/2022-01-24-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\344\272\272\347\276\244\346\240\207\347\255\276 & AB Test.md" "b/_drafts/2022-01-24-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\344\272\272\347\276\244\346\240\207\347\255\276 & AB Test.md" new file mode 100644 index 00000000000..f3eebf2869f --- /dev/null +++ "b/_drafts/2022-01-24-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\344\272\272\347\276\244\346\240\207\347\255\276 & AB Test.md" @@ -0,0 +1,32 @@ +--- +layout: post +title: 用户画像:ClickHose 用户画像 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +#### 营销例子 + +- A 银行的电子支付团队计划与滴滴打车合作, 在春节后短信推送优惠券的方式进行营销, 选择了多类人群进行投放, 其中有 "有打车需求且有车" 和 "有打车需求且无车人群", 本以为 "有打车需求且无车" 的广告触达效果更好, 结果却完全相反 + +- 这可能反映了无论是开车还是打车, 习惯了车以后反而离不开车了 + +- 从这个例子可以看出来, 为了更好的解决业务问题, 首先必须明确业务需求, 而用户画像是帮助企业明确目标客群的重要手段之一 + +- 明确了目标客群后, 就可以将有类似画像特征人群的潜在用户变成自己的用户了, 而想要做到这一点, 就必须要先有用户画像 + + + +# 互动行为 + + +# 互动深度 + + +# 权重设置 diff --git "a/_drafts/2022-01-24-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\351\241\271\347\233\256\347\256\241\347\220\206.md" "b/_drafts/2022-01-24-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\351\241\271\347\233\256\347\256\241\347\220\206.md" new file mode 100644 index 00000000000..4712b03fb34 --- /dev/null +++ "b/_drafts/2022-01-24-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\351\241\271\347\233\256\347\256\241\347\220\206.md" @@ -0,0 +1,62 @@ +--- +layout: post +title: 用户画像:用户画像项目管理 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +#### 项目规模 + +- 标签数量: + - 150 个 +- 开发人员:20人 + - 前端:3 人 + - 后端:5 人 + - 数据:12 人 + - 主要工作是 spark 开发 +- 研发周期 + - 需求设计:2 个月 + - 开发:10 个月 + + +模型计算 + +Spark 日处理数据 260G + +每天日志总量 5000万 + +每条日志大约 5KB + +\(50000000 \times 1204 \times 1024 \approx 250G\) + +页面响应 + +页面响应时间 < 5s + +系统并发数量 = 50 + +对于普通页面刷新, 从请求到展示, 不能超过五秒 + +Web 平台允许最大的并发操作为 50, 避免过多占用资源, 这个要求和业务系统无关, 只是用户画像的 Web 管理平台 + +数据库响应 + +SQL 查询响应时间 < 5s + +连接池余量 > 80% + +连接 MySQL 查询时间不能超过五秒 + +已经配置好的连接池, 必须保持20%以上的余量 + +外部接口 + +外部接口响应时间 < 5s + +外部可以通过 Restful 接口调用服务, 响应时间不得超过五秒 \ No newline at end of file diff --git "a/_drafts/2022-01-24-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\347\224\250\346\210\267\345\210\206\345\261\202\346\250\241\345\236\213\345\217\212\345\272\224\347\224\250.md" "b/_drafts/2022-01-24-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\347\224\250\346\210\267\345\210\206\345\261\202\346\250\241\345\236\213\345\217\212\345\272\224\347\224\250.md" new file mode 100644 index 00000000000..039565f7cc6 --- /dev/null +++ "b/_drafts/2022-01-24-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\347\224\250\346\210\267\345\210\206\345\261\202\346\250\241\345\236\213\345\217\212\345\272\224\347\224\250.md" @@ -0,0 +1,63 @@ +--- +layout: +title: 用户运营:用户分层模型及应用 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 用户运营 +--- + + +# 1. 分析思路 + +- 在做用户行为分析时,必须要又有个清醒的认识,就是分析的维度越多、信息的粒度越细,越能真实地反映问题。 +- 如果仅仅是按照常规路径,很宏观的做一个笼统的分析,那并不能真实的了解用户的所思所想,也无法有效支撑业务需求。 +- 分析场景的颗粒度 + - 根据实际场景灵活定义用户的起始行为、目标用户群体,细化的颗粒度对进一步的分析可提供更具价值的数据; +- 数据的对比维度 + - 针对核心指标增加对比维度,多维度的数据比较可帮助用户综合对比,更容易分析问题所在,提升分析效率。 + +# 2. 按新老用户分组 + + - 新老用户同期群分析方法,是用户分析最普遍的方法。 + - 比如:用每周的新用户,观察相同时间间隔后的表现。 + - 例如: + - 下图中,2019/1/1的新用户在第一周的留存率是49%,但2019/2/5的新用户在第一周的留存率是却只有40%,这就说明新用户的留存率在下降,需要重点关注。并且可以对比后续每周的表现,看是否好转。 + + ![]({{site.baseurl}}/img-post/用户行为-8.png) + + ![]({{site.baseurl}}/img-post/用户行为-7.png) + + - 为什么要区分新老用户呢?因为新老用户对于产品的反应是有很大差别的,一定要区分来看。比如你第一次去京东,由于不熟悉这家电商,很有可能逛逛就走了;但如果你是一个京东的老用户,登录京东后就很可能产生购物行为。通过区分新老,能够清晰的看到这两种用户的表现,便于发现到底是哪种用户发生了问题。 + + - 如果是新用户的留存下降,很可能是新用户没有快速的感受到产品的核心价值。比如物流,用户的主要诉求就是快,那么对于新用户是否能让他感受到这个价值。如果是老用户的留存率下降,也许是产品的体验在变差,或者受其他竞品的影响。 + +# 3. 按产品功能分组 + + - 一个产品一般具有很多功能,通过分析了解各个功能的价值,找到各个功能的提升空间,进而通过功能优化来整体提升用户留存。 + - 以下图为例,矩阵的横轴是功能的留存率,表示当前功能的用户黏性;纵轴是活跃用户的数量。做出这样一个矩阵后,我们就可以看到不同的功能在矩阵中的位置分布。 + ![]({{site.baseurl}}/img-post/用户行为-9.png) + - 比如橘色代表的功能就是产品的核心功能,使用率和留存率都很高,我们要保证核心功能的体验越来越好,并持续监控使用情况,防止意外发生; + - 比如绿色代表的功能,这个功能虽然使用的人数不多,但留存率非常高,说明这个功能的体验很好,我们要尽量引导用户使用这个功能; + - 而对于红色代表的功能,虽然使用的用户很多,但留存率不高。也许是这个功能有用,但体验不好;也许是这个功能本身就是鸡肋;所以我们要继续深入分析,来决定是优化功能还是直接下线。 + +# 4. 按渠道分组 + +- 企业经常采取多种渠道来获客。有线上的方式,比如百度搜索或者抖音短视频等;有新媒体的方式,比如公众号,知乎等;有线下的方式,比如线下沙龙和公众活动。 +- 各种渠道的获客都需要成本的,我们需要知道是哪种渠道的新用户留存高,留存率高说明这是高价值渠道,我们可以在这里做更多的投入。 + +- 比如下图,可以明显观察到,渠道一用户的留存率明显高于渠道二和渠道三,说明渠道一的用户和产品的契合度更高,为高质量渠道,应该在这里加大投入。 + + ![]({{site.baseurl}}/img-post/用户行为-10.png) + +# 5. 按兴趣划分 + +# 6. 按地域划分 + +# 7. 按划分 + +# 8. + diff --git "a/_drafts/2022-01-24-\350\256\241\347\256\227\345\271\277\345\221\212\357\274\232SparkSQL + Kudu \350\277\233\350\241\214\345\271\277\345\221\212\346\225\260\346\215\256\345\210\206\346\236\220.md" "b/_drafts/2022-01-24-\350\256\241\347\256\227\345\271\277\345\221\212\357\274\232SparkSQL + Kudu \350\277\233\350\241\214\345\271\277\345\221\212\346\225\260\346\215\256\345\210\206\346\236\220.md" new file mode 100644 index 00000000000..3ef078fd1c0 --- /dev/null +++ "b/_drafts/2022-01-24-\350\256\241\347\256\227\345\271\277\345\221\212\357\274\232SparkSQL + Kudu \350\277\233\350\241\214\345\271\277\345\221\212\346\225\260\346\215\256\345\210\206\346\236\220.md" @@ -0,0 +1,45 @@ +--- +layout: post +title: 计算广告:SparkSQL + Kudu 进行广告数据分析 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 实时数仓 +--- + +# 尾差问题 + +- 在财务、金融、汇率、税务等统计时,经常会遇到数据计算以后数据不一致问题; +- 在技术层面这个问题主要是由于小数四舍五入导致的,而金额和单价、数量的小数位数都是有限的,这就会导致尾差问题。 + +# 解决思路 + +#### 减少乘法除法、计算次数 + + + +#### 多用加法、少用乘法 + +- 例如: + - 订单上可能有这么几个字段:数量、单价、含税单价、税率、金额、税额、价税合计。 + - 关系如下: + - 数量×单价=金额 + - 数量×含税单价=价税合计; + - 数量×单价×税率=税额; + - 金额+税额=价税合计。 + +- 如果这里分别用乘法算出金额、税额、价税合计,那么最后很可能就出现金额+税额不等于价税合计。 + +对于这种情况,只能用乘法算出其中的两个,然后第三个,就其中的这两个金额做加法或者减法得到。例如,先用乘法算出金额和税额,然后用金额+税额得出价税合计;或者先用乘法算出价税合计和税额,然后用减法,算出金额。这样就不会产生误差。 + +2、注意用“倒挤”算法(分摊误差)即计算尾差 +数量乘以单价不等于金额的现象肯定存在,这是允许的,实际手工业务中也是允许的,因为四舍五入的关系,谁也避免不了这个;但是软件中的做法是:允许误差,但是误差们要能自圆其说。时刻牢记要让“总计数要等于明细数的合计”。例如,总计数是A,总计数被分成了若干份,分别是B、C、D,最后一定要保证:B+C+D=A。 +例如,一笔钱是456.87元,要按比例分成三份,第一份是15.8%,第二份是47.3%,第三份是36.9%,没有经验的程序员肯定是用总金额分别乘以三个比例,那肯定就有问题了,第一份:456.87×0.158=72.19。第二份:456.87×0.473=216.1,第三份:456.87×0.369=168.59,那么把这三个数再合计起来等于多少?72.19+216.1+169.59=456.88。比原来的总额多出了一分钱!这就不平了。所以,碰到这种情况,一定要选择分摊误差的地方,一般来说都是用最后一笔分担误差,所以叫做倒挤算法。所以这个例子应该这么算,前两笔都用乘法没问题,到了最后就一定不能总额乘以比例了,而是要用总额减去前面两笔的合计数,得出第三笔:456.87-(72.19+216.1)=156.58。这样就对了。 + +3、每一次乘除法以后立即按照精度四舍五入 +一般在软件系统中会预先让用户定义数量、单价、金额的小数位数,这是常识性的做法,但是程序员必须在程序中注意四舍五入的时机:在得出一个结果(数量、单价、金额三者任何一个)之后,必须立即四舍五入以后,再投入其他运算,如果做了一次乘法、除法以后却没有立即根据定义的精度四舍五入,而是直接投入其他运算,最后再四舍五入,那肯定不对。例如:单价:8.49,数量:45.5789,用数量乘以单价得出金额:386.964861,如果你这时候你不把这个金额先四舍五入了再投入其他运算,那么肯定产生误差。 + +4、写程序注意选择相同类型的数值类型进行乘除运算。如果不相同类型的数值类型乘除,计算机自己都有可能产生误差,我就碰到过计算机算出:3.1×2=6.3这种情况 diff --git "a/_drafts/2022-01-26-Hive\357\274\232HQL \347\256\200\344\273\213\345\217\212\345\205\266\344\270\216 SQL \347\232\204\345\214\272\345\210\253.md" "b/_drafts/2022-01-26-Hive\357\274\232HQL \347\256\200\344\273\213\345\217\212\345\205\266\344\270\216 SQL \347\232\204\345\214\272\345\210\253.md" new file mode 100644 index 00000000000..b61b82462b9 --- /dev/null +++ "b/_drafts/2022-01-26-Hive\357\274\232HQL \347\256\200\344\273\213\345\217\212\345\205\266\344\270\216 SQL \347\232\204\345\214\272\345\210\253.md" @@ -0,0 +1,55 @@ +--- +layout: post +title: Hive:HQL 简介及其与 SQL 的区别 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hive +--- + +# 什么是 HQL + +HQL 是 Hibernate Query Language(Hibernate 查询语言)的缩写,提供更加丰富灵活、更为强大的查询能力;HQL 更接近 SQL 语句查询语法。Hibernate 查询语言(HQL)是一种面向对象的查询语言,类似于 SQL,但不是去对表和列进行操作,而是面向对象和它们的属性。 HQL 查询被 Hibernate 翻译为传统的 SQL 查询从而对数据库进行操作。 + +# 执行 HQL 查询的步骤 + + 1、获得HibernateSession对象 + 2、编写HQL语句 + 3、调用Session的createQuery方法创建查询对象 + 4、如果HQL语句包含参数,则调用Query的setXxx方法为参数赋值 + 5、调用Query对象的list等方法返回查询结果。 + +二、在Hibernate中推荐使用hql语句,不建议直接使用sql。 +原因: + 1、sql不是以面对对象的方式操作数据库,这是Hibernate不提倡的。 + 2、如果编写了某种数据库特有的函数或语法,那么在更换数据库时肯定要修改源码重新编译。 + +三、Hibernate把hql编译成sql语句传送到数据库进行查询。 + 在执行效率方面,不考虑其它的影响, 一般sql 效率要高于 hql ,如果考虑 缓存,关联映射,语句的质量就要看具体情况,不过sql 的功能是比hql大。 + + +#### 面向库表 VS 面向对象 + +- sql 面向数据库表查询; +- hql 面向对象查询。 + +#### 对象属性做条件 VS 表字段做条件 + +- hql + - from 后面跟的 类名+类对象 where 后 用 对象的属性做条件; +- sql + - from 后面跟的是表名 where 后 用表中字段做条件查询。 + +#### 大小写敏感 + +- 因为 HQL 是面向对象的,而对象类的名称和属性都是大小写敏感的,所以 HQL 是大小写敏感的; +- HQL语句:`from Cat as cat where cat.id > 1;` 与 `from Cat as cat where cat.ID > 1;` 是不一样的,这点与 SQL 不同。 + + + + + + diff --git "a/_drafts/2022-01-27-BI\357\274\232Tableau PowerBI \345\257\271\346\257\224\345\210\206\346\236\220.md" "b/_drafts/2022-01-27-BI\357\274\232Tableau PowerBI \345\257\271\346\257\224\345\210\206\346\236\220.md" new file mode 100644 index 00000000000..a6297496ca1 --- /dev/null +++ "b/_drafts/2022-01-27-BI\357\274\232Tableau PowerBI \345\257\271\346\257\224\345\210\206\346\236\220.md" @@ -0,0 +1,84 @@ +--- +layout: post +title: BI:Tableau PowerBI 对比分析 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - BI +--- + + +# Tableau +### Tableau 使用 + +Tableau 的强项在于数据可视化,但在数据清洗、预处理方面并不擅长,需要对数据进行数据预处理。 + +>比如:自动拆分功能中,对于名字拆分,英文名字有空格的话只会保留空格前内容,多个名字只保留第一个名字,这是需要单独对数据进行预处理。虽然可以进行自定义拆分,但是整体操作会比较麻烦。 + + +# 常用图表 + +### 标靶图 + +### 甘特图 + +分析供应商的交货延迟情况,一般的表格只有【计划交货日期】和【实际交货日期】,需要创建【计算字段】:【交货时间】,添加计算公式实际日期减去计划日期,得出交货天数。 + +通过颜色设置,分辨提前交货和延迟交货。 + +注意,除了按照供应商维度,分析供应商的交货延迟情况,还可以按照商品维度,分析不同商品的延迟交货情况。 + + +### 瀑布图 + +产品盈亏情况, + + + +# tableau 进阶 + +# tableau 多表连接 + +### 数据集合并 + +#### 内联接 + +使用内联接来合并表时,生成的表将包含与两个表均匹配的值。我们可以理解为两张表的交集。 + +#### 左联接 + +使用左联接来合并表时,生成的表将包含左侧表中的所有值以及右侧表中的对应匹配项。 + +当左侧表中的值在右侧表中没有对应匹配项时,您将在数据网格中看到 null 值。 + +#### 右联接 + +使用右联接来合并表时,生成的表将包含右侧表中的所有值以及左侧表中的对应匹配项。 + +当右侧表中的值在左侧表中没有对应匹配项时,您将在数据网格中看到 null 值。 + +#### 外联接 + +使用完全外部联接来合并表时,生成的表将包含两个表中的所有值。 + +当任一表中的值在另一个表中没有匹配项时,您将在数据网格中看到 null 值。 + + + + + +![]({{site.baseurl}}/img-post/fwq-1-1.jpg) + + + + + + +# Tableau 地图数据 + +![]({{site.baseurl}}/img-post/tableau-map-1.jpg) + +![]({{site.baseurl}}/img-post/tableau-map-2.jpg) \ No newline at end of file diff --git "a/_drafts/2022-01-27-BI\357\274\232\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265.md" "b/_drafts/2022-01-27-BI\357\274\232\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265.md" new file mode 100644 index 00000000000..7f0b5a84cb0 --- /dev/null +++ "b/_drafts/2022-01-27-BI\357\274\232\350\256\241\347\256\227\345\271\277\345\221\212\345\237\272\346\234\254\346\246\202\345\277\265.md" @@ -0,0 +1,37 @@ +--- +layout: post +title: 计算广告基本概念 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - BI +--- + +# 1. 互联网商业化 + +#### 1.1. 获取 root 权限 + +将流量、数据、影响力三类资产,通过商业产品转变成实际收入的过程,即互联网商业化。 + +# 2. 在线广告形式 + +#### 2.1。 横幅(banner ad) + +#### 2.2. 文字链(text ad) + +#### 2.3. 富媒体(rich media ad) + +#### 2.4. 视频广告(video ad) + +#### 2.5. 交互式广告(playable ad) + +#### 2.6. 社交广告(social media) + + + + + ![]({{site.baseurl}}/img-post/hadoop-4.png) + diff --git "a/_drafts/2022-01-27-Hadoop\357\274\232Hadoop \351\253\230\345\217\257\347\224\250\357\274\210HA\357\274\211\346\234\272\345\210\266\347\232\204\345\267\245\344\275\234\345\216\237\347\220\206.md" "b/_drafts/2022-01-27-Hadoop\357\274\232Hadoop \351\253\230\345\217\257\347\224\250\357\274\210HA\357\274\211\346\234\272\345\210\266\347\232\204\345\267\245\344\275\234\345\216\237\347\220\206.md" new file mode 100644 index 00000000000..2b7678235c4 --- /dev/null +++ "b/_drafts/2022-01-27-Hadoop\357\274\232Hadoop \351\253\230\345\217\257\347\224\250\357\274\210HA\357\274\211\346\234\272\345\210\266\347\232\204\345\267\245\344\275\234\345\216\237\347\220\206.md" @@ -0,0 +1,100 @@ +--- +layout: post +title: Hadoop:Hadoop 高可用(HA)机制的工作原理 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hadoop +--- + + +https://blog.csdn.net/weixin_44825085/article/details/119645899 + + + + + + +# 1. HDFS 概念 + +#### 1.1. HDFS 工作原理 + +- HDFS 概念 + - HDFS 是Hadoop Distribute File System 的简称,HDFS 是 Hadoop 生态的核心。 + - 它允许文件通过网络在多台主机上分享的文件系统,可以让多台机器上的多个用户分享文件和存储空间。 + >HDFS只是分布式文件管理系统中的一种。 + + + + + +HDFS和Yarn都是典型MS结构,即一个Master和多个Slave,这样就会产生单点故障,万一主节点挂掉了就不能对外提供服务,此时就需要配置我们的高可用,让集群可以7*24小时不间断的提供服务 + + +#### NameNode 单点故障(SPOF) + +- NameNode 机器发生意外(如宕机),集群将无法使用,直到管理员重启; +- NameNode 机器需要升级(如软件、硬件升级),此时集群将无法使用; +- HDFS 配置 Active / Standby 两个 NameNode 实现集群主从热备,解决上述问题; +- 如果出现单点故障,可以迅速将 NameNode 切换到另外一台机器; + +#### 如何保证三台 NameNode 数据一致 + +- FSImage + - 让一台 NameNode 生成数据,其他机器的 NameNode 同步 +- Edtis & JournalNode + - 引进新的模块 JournalNode 保证 Edtis 文件的数据一致性 + +#### 如何实现同一时刻只有一台 Active,多台 Standby + +- 手动分配 +- 自动分配 + +#### 如何定期合并 FSImage 和 Edtis + +- Standby 的 NameNode 来负责 + +#### NameNode 故障时,如何自动切换到 Standby 的 NameNode + +- 手动故障转移 +- 自动故障转移 + + +#### JounalNode 规划 + +![]({{site.baseurl}}/img-post/hadoop-ha-1.png) + + +#### ZooKeeper + +![]({{site.baseurl}}/img-post/hadoop-ha-2.png) + + + +(1)hadoop-HA集群运作机制介绍 + +所谓HA,即高可用(7*24小时不中断服务),实现高可用最关键的是消除单点故障。 +hadoop-ha严格来说应该分成各个组件的HA机制——HDFS的HA、YARN的HA + +(2)HDFS的HA机制详解 + +通过双 namenode 消除单点故障,双 namenode 协调工作的要点: + +A、元数据管理方式需要改变: +- 内存中各自保存一份元数据 +- Edits日志只能有一份,只有Active状态的namenode节点可以做写操作 +- 两个namenode都可以读取edits +- 共享的edits放在一个共享存储中管理(qjournal和NFS两个主流实现) + +B、需要一个状态管理功能模块: +- 实现了一个zkfailover,常驻在每一个namenode所在的节点 +- 每一个zkfailover负责监控自己所在namenode节点,利用zk进行状态标识 +- 当需要进行状态切换时,由zkfailover来负责切换 +- 切换时需要防止brain split现象的发生 + + + + diff --git "a/_drafts/2022-01-27-Hadoop\357\274\232IO \346\223\215\344\275\234\345\216\237\347\220\206\345\217\212\351\230\273\345\241\236\351\227\256\351\242\230\345\272\224\345\257\271\347\255\226\347\225\245.md" "b/_drafts/2022-01-27-Hadoop\357\274\232IO \346\223\215\344\275\234\345\216\237\347\220\206\345\217\212\351\230\273\345\241\236\351\227\256\351\242\230\345\272\224\345\257\271\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..00b373267f3 --- /dev/null +++ "b/_drafts/2022-01-27-Hadoop\357\274\232IO \346\223\215\344\275\234\345\216\237\347\220\206\345\217\212\351\230\273\345\241\236\351\227\256\351\242\230\345\272\224\345\257\271\347\255\226\347\225\245.md" @@ -0,0 +1,136 @@ +--- +layout: post +title: Hadoop:IO 操作原理及阻塞问题应对策略 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hadoop +--- + +https://www.cnblogs.com/likai198981/archive/2013/01/07/2849854.html + +https://www.xujun.org/note-32192.html + +https://www.bbsmax.com/A/D854rKVxzE/ + +https://blog.csdn.net/qq_35688140/article/details/89151196 + +https://baijiahao.baidu.com/s?id=1693282638128969790&wfr=spider&for=pc + + + + + +Hadoop宕机 + +(1)如果MR造成系统宕机。此时要控制Yarn同时运行的任务数,和每个任务申请的最大内存。调整参数:yarn.scheduler.maximum-allocation-mb(单个任务可申请的最多物理内存量,默认是8192MB) + +(2)如果写入文件过量造成NameNode宕机。那么调高Kafka的存储大小,控制从Kafka到HDFS的写入速度。高峰期的时候用Kafka进行缓存,高峰期过去数据同步会自动跟上。 + +#### 报错:no YARN_RESOURCEMANAGER_USER defined + +- 报错内容: + `ERROR: but there is no YARN_RESOURCEMANAGER_USER defined. Aborting operation.` +- 解决办法: + + ``` + cd /usr/local/hadoop-3.1.3/sbin/ + ``` + + ``` + vim start-dfs.sh + + # 在文件顶部添加以下参数 + HDFS_NAMENODE_USER=root + HDFS_DATANODE_USER=root + HDFS_SECONDARYNAMENODE_USER=root + YARN_RESOURCEMANAGER_USER=root + YARN_NODEMANAGER_USER=root + ``` + + ``` + vim stop-dfs.sh + + # 在文件顶部添加以下参数 + HDFS_NAMENODE_USER=root + HDFS_DATANODE_USER=root + HDFS_SECONDARYNAMENODE_USER=root + YARN_RESOURCEMANAGER_USER=root + YARN_NODEMANAGER_USER=root + ``` + + ```aidl + vim start-yarn.sh + + # 在文件顶部添加以下参数 + YARN_RESOURCEMANAGER_USER=root + HADOOP_SECURE_DN_USER=yarn + YARN_NODEMANAGER_USER=root + ``` + + ```aidl + vim stop-yarn.sh + + # 在文件顶部添加以下参数 + YARN_RESOURCEMANAGER_USER=root + HADOOP_SECURE_DN_USER=yarn + YARN_NODEMANAGER_USER=root + ``` + +#### Error: JAVA_HOME is not set and could not be found. + +- 报错内容: + ``` + Error: JAVA_HOME is not set and could not be found. + ``` +- 解决办法 1: 添加系统环境变设置 + - 检查环境变量设置 + ``` + which java + + java -version + ``` + - 如果返回异常,则需要手动添加 + ``` + vim /etc/profile + + # JAVA_HOME + export JAVA_HOME=/usr/local/bin/jdk1.8 + export PATH=$PATH:$JAVA_HOME/bin + ``` + - 启动生效 + ``` + source /etc/profile + ``` + +- 解决方法 2:在 `hadoop-env.sh` 中的环境变量设置 + ``` + cd /usr/local/hadoop-3.1.3/etc/hadoop/ + + vim hadoop-env.sh + + export JAVA_HOME=/usr/local/bin/jdk1.8 + ``` + +- 注意: + - 对于 hadoop 集群而言,前面所说的环境变量配置,必须要在 master 上进行,并在集群内部分发、而且启动生效; + - 如果按照上面的操作进行以后仍然报错,请 **仔细检查是否是在 master 上进行这些配置操作,检查集群机器是否全部配置生效**。 + +#### bash: jps: command not found + +- 报错内容: + ```aidl + bash: jps: command not found + ``` + +- 解决方法: + - 删除原有的 jps 软连接,并新建一个 + ```aidl + rm /usr/local/bin/jps + ln -s /usr/local/bin/jdk1.8/bin/jps /usr/local/bin/jps + ``` + +#### diff --git "a/_drafts/2022-01-27-Hadoop\357\274\232YARN \346\200\247\350\203\275\345\217\202\346\225\260\350\260\203\344\274\230.md" "b/_drafts/2022-01-27-Hadoop\357\274\232YARN \346\200\247\350\203\275\345\217\202\346\225\260\350\260\203\344\274\230.md" new file mode 100644 index 00000000000..8322d0a499e --- /dev/null +++ "b/_drafts/2022-01-27-Hadoop\357\274\232YARN \346\200\247\350\203\275\345\217\202\346\225\260\350\260\203\344\274\230.md" @@ -0,0 +1,149 @@ +--- +layout: post +title: Hadoop:YARN 性能参数调优 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hadoop +--- + + +# YARN 的概念 + +- YARN 是一个通用的资源管理系统和调度平台,可以为上层提供统一的资源管理和调度; + +- 资源管理系统 + - 集群的硬件资源,和程序运行相关,比如内存、CPU; +- 调度平台 + - 解决多个程序同时申请计算资源,如何分配、调度规则等问题; + +>YARN 不管理 磁盘,磁盘是由 HDFS 管理。 + +- 可以把 YARN 理解为一个分布式操作系统平台,而 MapReduce 等计算程序,则相当于运行操作系统之上的应用程序。 + + +- YARN 具有良好的通用性和包容性: + +- 正是因为有了 YARN,更多的计算框架可以接入到 HDFS 中,不仅是 MapReduce,Flink、Spark 都可以顺利接入 HDFS。 +- 正是因为 YARN 的包容性,是的其他的计算框架可以专注于计算性能的提升。 + + +![]({{site.baseurl}}/img-post/yarn-1.png) + +# YARN 的作用 + +YARN 使得集群具备以下优点: + +- 可扩展性: + - 可以平滑地扩展至数万节点和并发的应用 +- 可维护性: + - 保证集群软件的升级与用户应用程序完全解耦 +- 多租户: + - 支持同一集群中多个租户并存,同时支持多个租户间细颗粒度地共享单个节点 +- 位置感知: + - 将计算移至数据所在位置 +- 高集群使用率: + - 实现底层物理资源的高使用率 +- 安全和可审计的操作: + - 以安全、可审计的方式使用集群资源 +- 可靠性和可用性: + - 具有高度可靠的用户交互、并支持高可用性 +- 对编程模型多样化的支持: + - 不仅支持mapreduce,还支持其他模型 +- 灵活的资源模型: + - 支持各个节点的动态资源配置以及灵活的资源模型 +- 向后兼容: + - 保持现有的mapreduce应用程序的向后兼容性。 + + +# YARN 三大组件 + +#### ResourceManager(RM) + +- YARN 集群中的主角色,决定系统中所有应用程序之间资源分配的最终权限; + - 调度器根据容量、队列等限制条件(如每个队列分配一定的资源,最多执行一定数量的作业等),将系统中的资源分配给各个正在运行的应用程序。 + - 需要注意的是,该调度器是一个“纯调度器”,它不再从事任何与具体应用程序相关的工作,比如: + - RM 不负责监控或者跟踪应用的执行状态等, + - RM 也不负责重新启动因应用执行失败或者硬件故障而产生的失败任务, + - 这些均交由应用程序相关的 ApplicationMaster 完成。 +- RM 调度器仅根据各个应用程序的资源需求进行资源分配,而资源分配单位用一个抽象概念“资源容器”(Resource Container,简称 Container)表示,Container 是一个动态资源分配单位,它将内存、CPU、磁盘、网络等资源封装在一起,从而限定每个任务使用的资源量。 +- 此外,RM 该调度器是一个可插拔的组件,用户可根据自己的需要设计新的调度器,YARN 提供了多种直接可用的调度器,比如 Fair Scheduler和Capacity Scheduler 等。 +- 接收用户的作业提交,并通过 NodeManger 分配、管理各个机器上的计算资源; + +#### NodeManager(NM) + +- YARN 中的从角色,一台机器上一个,负责管理机器上的计算资源; +- 根据 RM 命令,启动 Container 容器、监视容器的资源使用情况,并向 RM 主角色汇报资源使用情况; + + +#### ApplicationMaster(AM) + +- 用户提交的每个应用程序,均包含一个 AM; +- 应用程序内的“老大”,负责程序内部各个阶段的资源申请,监督程序的执行情况; +- AM 与 RM 调度器协商以获取资源(资源用 Container 表示); +- 将得到的任务进一步分配给内部的任务(资源的二次分配); +- 与 NM 通信以启动 / 停止任务; +- 监控所有任务运行状态,并在任务运行失败时重新为任务申请资源以重启任务。 +- 当前 YARN 自带了两个AM实现,一个是用于演示AM编写方法的实例程序 distributedshell,它可以申请一定数目的 Container 以并行运行一个 Shell 命令或者Shell脚本;另一个是运行 MapReduce 应用程序的AM—MRAppMaster。 + +#### Container + +- Container 是 YARN 中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等,当AM向 RM 申请资源时,RM 为 AM 返回的资源便是用 Container 表示。 +- YARN 会为每个任务分配一个 Container,且该任务只能使用该 Container 中描述的资源。 + +# YARN 核心交互流程 + +#### 核心流程 + +- MR 作业提交 client -> RM +- 资源申请 MRAppMaster -> RM +- MR 作业状态汇报 Container( Map | Reduce Task ) -> Container(MRAppMaster) +- 节点的状态汇报 NM -> RM + +#### 具体步骤 + +- 当用户向yarn中提交一个应用程序后,yarn将分两个阶段运行该应用程序。 + - 第一个阶段是启动 ApplicationMaster。 + - 第二个阶段是由 Applicationmaster 刨建应用程序,为它申请资源,并监控它的整个运行过程,直到运行完成。 + +- 第1步:用户向yarn中提交应用程序,其中包括ApplicationMaster程序、启动 ApplicationMaster的命令、用户程序等。 +- 第2步:Resourcemanager为该应用程序分配第一个Container,并与对应的 NodeManager通信,要求它在这个Container中启动应用程序的ApplicationMaster。 +- 第3步:Applicationmaster首先向ResourceManager注册,这样用户可以直接通过 Resourcemanager查看应用程序的运行状态,然后它将为各个任务申请资源,并监控它的运行状态,直到运行结束。即重复步骤4~7。 +- 第4步:ApplicationMaster通过RPC协议向Resourcemanager申请和领取资源。 +- 第5步:一旦ApplicationMaster申请到资源后,便与对应的Nodemanager通信,要求它启动任务。 +- 第6步:Nodemanager为任务设置好运行环境后(包括环境变量、JAR包、二进制程序等),将任务启动命令写到一个脚本中,并通过运行该脚本启动任务。 +- 第7步:各个任务通过RPC协议向ApplicationMaster汇报自己的状态和进度,让 ApplicationMaster随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务。在应用程序运行过程中,用户可随时通过RPC向Applicationmaster查询应用程序的当前运行状态。 +- 第8步:应用程序运行完成后,ApplicationMaster向Resourcemanager注销并关闭自己。 + +# YARN 的 RM 重启机制 +- ResourceManager负责资源管理和应用的调度,是yarn的核心组件,存在单点故障的问题。 +- ResourceManager restart重启机制是使RM在重启动时能够使yarn集群继续正常工作,并且使RM出现的失败不被用户知道。 +- 重启机制需要借助zookeeper集群来存储信息实现。 +- 重启机制并不是自动帮我们重启的意思,所以不能解决单点故障问题。 + +- 不开启RM重启机制。RM故障重启后,之前集群上跑的任务信息都会消失,正在执行的作业也会失败。 +- +- RM重启机制有两种,一种是保留工作的重启机制,另一种是不保留工作的重启机制。 + - 不保留工作的RM重启机制。只保存了application提交的信息和最终执行状态,并不保存运行过程中的相关数据,所以RM重启后,会先杀死正在执行的任务,再重新提交,从零开始执行任务。 + - 保留工作的重启机制。保存了application运行中的状态数据,所以在RM重启之后,不需要杀死之前的任务,而是接着原来执行到的进度继续执行。 + +# YARN 的高可用性架构 +- ResourceManager 负责资源管理和应用的调度,是yarn的核心组件,集群的主角色,存在单点故障问题。为了解决RM的单点故障问题,yarn设计了一套Active/Standby模式的ResourceManager HA架构。 +- HA架构能够保证运行的ResourceManager挂掉后备用的ResourceManager能迅速接替工作,保证计算任务不会中断,转换过程对用户无感。 +- 该HA架构也使用zookeeper集群实现。 + +- 实现HA集群的关键是: + - 主备之间状态数据同步、主备之间顺利切换(故障转移机制)。 +- 针对数据同步问题,可以通过zookeeper来存储共享集群的状态数据,因为zookeeper本质也是一个小文件存储系统。 +- 针对主备顺利切换,也可以基于zookeeper自动实现。 + + +# YARN 的通信协议 +- 分布式环境下,需要涉及跨机器跨网络通信,yarn底层使用RPC协议实现通信。 + +- RPC是远程过程调用( Remote procedure call)的缩写形式。 +- 基于RPC进行远程调用就像本地调用一样。 +- 在RPC协议中,通信双方有一端是Client,另一端为Server,且Client总是主动连接Server的。因此,yarn实际上采用的是拉式(pull-based)通信模型。 \ No newline at end of file diff --git "a/_drafts/2022-01-27-Hadoop\357\274\232\345\260\217\346\226\207\344\273\266\351\227\256\351\242\230\345\217\212\345\272\224\345\257\271\347\255\226\347\225\245.md" "b/_drafts/2022-01-27-Hadoop\357\274\232\345\260\217\346\226\207\344\273\266\351\227\256\351\242\230\345\217\212\345\272\224\345\257\271\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..cd17e0c608f --- /dev/null +++ "b/_drafts/2022-01-27-Hadoop\357\274\232\345\260\217\346\226\207\344\273\266\351\227\256\351\242\230\345\217\212\345\272\224\345\257\271\347\255\226\347\225\245.md" @@ -0,0 +1,13 @@ +--- +layout: post +title: Hadoop:小文件问题及应对策略 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓 +--- + + diff --git "a/_drafts/2022-01-27-Hadoop\357\274\232\345\270\270\350\247\201\347\232\204 HDFS \345\217\202\346\225\260\350\260\203\344\274\230\347\255\226\347\225\245.md" "b/_drafts/2022-01-27-Hadoop\357\274\232\345\270\270\350\247\201\347\232\204 HDFS \345\217\202\346\225\260\350\260\203\344\274\230\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..885844ebcaa --- /dev/null +++ "b/_drafts/2022-01-27-Hadoop\357\274\232\345\270\270\350\247\201\347\232\204 HDFS \345\217\202\346\225\260\350\260\203\344\274\230\347\255\226\347\225\245.md" @@ -0,0 +1,163 @@ +--- +layout: post +title: Hadoop:常见的 HDFS 参数调优策略 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓 +--- + +# 1. hadoop运行环境 + +![]({{site.baseurl}}/img-post/hadoop调优-1.png) + +# 2. 硬件选择 + +#### 2.1. 主节点可靠性要好于从节点 + +#### 2.2. 多路多核,高频率cpu、大内存, +- namenode 100万文件的元数据要消耗800M内存,内存决定了集群保存文件数的总量, resourcemanager同时运行的作业会消耗一定的内存。 +- datanode 的内存需要根据cpu的虚拟核数(vcore) 进行配比,CPU的vcore数计算公式为=cpu个数 * 单cpu核数* HT(超线程) +- 内存容量大小 = vcore数 * 2GB(至少2GB) + +#### 2.3. 根据数据量确定集群规模 +- 一天增加10GB, 365天,原数据1TB,replacation=3,1.3 mapreduce 计算完保存的数据,规划容量 (1TB + 10GB*365)*3 * 1.3 =17.8TB,如果一台datanode的存储空间为2TB,18/2= 9,总节点为 = 9+2 =11 ,还要考虑作业并不是均匀分布的; +- 有可能会倾斜到某一个时间段,需要预留资源 + +#### 2.4. 不要让网络 I/O 成为瓶颈 +- hadoop 作业通常是 I/O密集型而非计算密集型, 瓶颈通常集中出现在I/O上; +- 计算能力可以通过增加新节点进行线性扩展,要注意网络设别处理能力。 + +# 3. 操作系统 + +#### 3.1. 避免使用 swap 分区 + +- 将 hadoop 守护进程的数据交换到硬盘的行为,可能会导致操作超时。 + +#### 3.2. 调整内存分配策略 + +- 操纵系统内核根据 vm.oversommit_memory 的值来决定分配策略; +- 并且通过vm.overcommit_ratio的值来设定超过物理内存的比例。 + +#### 3.3. 修改 net.core.somaxconn 参数 + +- 该参数表示socker监听backlog的上限,默认为128,socker的服务器会一次性处理backlog中的所有请求,hadoop的ipc.server.listen.queue.size参数和linux的net.core.somaxconn +- 参数控制了监听队列的长度,需要调大。 + +#### 3.4. 增大同时打开文件描述符的上限 + +- 对内核来说,所有打开的文件都通过文件描述符引用,文件描述符是一个非负整数; +- hadoop的作业经常会读写大量文件,需要增大同时打开文件描述符的上限。 + +#### 3.5. 选择合适的文件系统,并禁用文件的访问时间 + +- 文件访问时间可以让用户知道那些文件近期被查看或修改; +- 但对hdfs来说,获取某个文件的某个块被修改过没有意义,可以禁用。 + +#### 3.6. 关闭THP (transparent Huge Pages) + +- THP 是一个使管理 Huge Pages自动化的抽象层, 它会引起cpu占用率增大, 需要关闭。 + + ```aidl + echo never > /sys/kernel/mm/redhat_transparent_hugepage/defrag + echo never > /sys/kernel/mm/redhat_transparent_hugepage/enabled + echo never > /sys/kernel/mm/transparent_hugepage/enabled + echo never > /sys/kernel/mm/transparent_hugepage/defrag + ``` + + + +# 4. HDFS 调优 + +#### 4.1. dfs.block.size + - 设置合理的块大小; + +#### 4.2. mapred.local.dir + - 将中间结果目录设置为分布在多个硬盘,以提升写入速度; + +#### 4.3. dfs.datanode.handler.count + - 该值用于设置datanode处理RPC的线程数,,默认为3; + - 大集群可以适当加大(dfs.datanode.handler.count),可以适当加大为 10。 + +#### 4.4. dfs.namenode.handler.count + - NameNode有一个工作线程池,用来处理不同DataNode的并发心跳以及客户端并发的元数据操作,参数dfs.namenode.handler.count的默认值10; + - 对于大集群或者有大量客户端的集群来说,通常需要增大该值的设置,设置该值的一般原则是将其设置为集群大小的自然对数乘以20,即20logN,N为集群大小; + - 计算公式:dfs.namenode.handler.count=20 * log2(Cluster Size); + - 比如集群规模为 8台 时,此参数设置为 60。 + +#### dfs.datanode.data.dir + - HDFS数据存储目录; + - 将数据存储分布在各个磁盘上可充分利用节点的I/O读写性能。 + + +#### 日志存储路径 & 镜像文件存储路径 +- 编辑日志存储路径dfs.namenode.edits.dir设置与镜像文件存储路径dfs.namenode.name.dir尽量分开,达到最低写入延迟,提高集群的读写方面性能 + +#5. YARN 调优 + +- yarn的资源表示模型为 ceontainer(容器),container 将资源抽象为两个维度,内存和虚拟cpu(vcore) + - 兼容各种计算框架; + - 动态分配资源,减少资源浪费; + +#### 5.1. 容器内存 + +- yarn.nodemanager.resource.memory-mb + - 表示该节点上YARN可使用的物理内存总量,默认是8192(MB); + - 如果你的节点内存资源不够 8GB,则需要调减小这个值; +- 注意: + - YARN 不会智能的探测节点的物理内存总量,必须手动修改; + - 如果不修改会导致内存使用率过低,洪峰过来时集群会挂掉。 +- 调优方法: + - 根据电脑内存来调节; + - 比如: + - 电脑内存是 128G 的,那这里的值就调成 100G。 + +#### 4.7. 最小容器内存 + +- yarn.scheduler.minimum-allocation-mb + - 表示单个任务可申请的最多物理内存容量,默认是 8192(MB); + - MR 过程并不吃内存,所以这里一般不需要修改,除非代码写的太烂; + - 如果确有需要,调大到 16G 就可以。 + +#### 4.8. 容器内存增量 +- yarn.scheduler.increment-allocation-mb + +#### 4.9. 最大容器内存 +- yarn.scheduler.maximum-allocation-mb +- 单个任务可申请的最多物理内存量,默认是8192(MB)。 + +#### 4.10. 容器虚拟cpu内核 +- yarn.nodemanager.resource.cpu-vcores + +#### 4.11. 最小容器虚拟cpu内核数量 +- yarn.scheduler.minimum-allocation-vcores + +#### 4.12. 容器虚拟cpu内核增量 +- yarn.scheduler.increment-allocation-vcores + +#### 4.13. 最大容器虚拟cpu内核数量 +- yarn.scheduler.maximum-allocation-vcores + +#### 4.14. MapReduce调优 + +- 三大原则: + - 增大作业并行程度 + - 给每个任务足够的资源 + - 在满足前2个条件下,尽可能的给shuffle预留资源 + +#### 示例: + +情景描述:总共7台机器,每天几亿条数据,数据源->Flume->Kafka->HDFS->Hive + +面临问题:数据统计主要用HiveSQL,没有数据倾斜,小文件已经做了合并处理,开启的JVM重用,而且IO没有阻塞,内存用了不到50%。但是还是跑的非常慢,而且数据量洪峰过来时,整个集群都会宕掉。基于这种情况有没有优化方案。 + +(2)解决办法: + +内存利用率不够。这个一般是Yarn的2个配置造成的,单个任务可以申请的最大内存大小,和Hadoop单个节点可用内存大小。调节这两个参数能提高系统内存的利用率。 + +(a)yarn.nodemanager.resource.memory-mb +(b)yarn.scheduler.maximum-allocation-mb + diff --git "a/_drafts/2022-01-27-Hadoop\357\274\232\346\225\260\346\215\256\345\200\276\346\226\234\351\227\256\351\242\230\345\217\212\345\272\224\345\257\271.md" "b/_drafts/2022-01-27-Hadoop\357\274\232\346\225\260\346\215\256\345\200\276\346\226\234\351\227\256\351\242\230\345\217\212\345\272\224\345\257\271.md" new file mode 100644 index 00000000000..b232773f4bf --- /dev/null +++ "b/_drafts/2022-01-27-Hadoop\357\274\232\346\225\260\346\215\256\345\200\276\346\226\234\351\227\256\351\242\230\345\217\212\345\272\224\345\257\271.md" @@ -0,0 +1,42 @@ +--- +layout: post +title: Hadoop:数据倾斜问题及应对 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓 +--- + + + +H# 数据均衡 +#### 不同服务器间的均衡; +- start-balancer.sh -threshold,默认为 10,指的是集群中各个节点的磁盘空间利用相差不超过 10%; +- 注意指的是单个节点的磁盘使用,占集群总磁盘空间的比值; +- 停止数据均衡: + ```aidl + stop-balancer.sh + ``` +#### 磁盘间的数据均衡 +- 生成执行计划 + + ```aidl + # 生成一个shufang102.plan.json文件,类似于kafka的分区重分配生成的计划 + hdfs diskbalancer -plan hadoop101 + ``` + +- 执行均衡计划 + ``` + hdfs diskbalancer -execute hadoop101.plan.json + ``` +- 查看当前均衡任务的执行情况 + ```aidl + hdfs diskbalancer -query hadoop101 + ``` +- 取消均衡任务 + ```aidl + hdfs diskbalancer -cancel hadoop101.plan.json + ``` diff --git "a/_drafts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232DMP \345\212\237\350\203\275\344\273\213\347\273\215\345\217\212\347\263\273\347\273\237\346\220\255\345\273\272.md" "b/_drafts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232DMP \345\212\237\350\203\275\344\273\213\347\273\215\345\217\212\347\263\273\347\273\237\346\220\255\345\273\272.md" new file mode 100644 index 00000000000..69df59ef3e1 --- /dev/null +++ "b/_drafts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232DMP \345\212\237\350\203\275\344\273\213\347\273\215\345\217\212\347\263\273\347\273\237\346\220\255\345\273\272.md" @@ -0,0 +1,52 @@ +--- +layout: post +title: 数据中台:数据湖的概念详解 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据中台 +--- + + +# 1. DMP 介绍 + +#### 1.1. 定义 + +- DMP(Data Management Platform)数据管理平台,是把分散的多方数据进行整合纳入统一的技术平台,并对这些数据进行标准化和细分,让用户可以把这些细分结果推向现有的互动营销环境里的平台。 + + + +# 2. 数据来源 + +#### 2.1. 业务系统 + +- CRM +- BPM +- OA +- WMS +- TMS +- OMS + +### 2.2. 埋点数据 + +- 前端埋点 +- 后端埋点 + +#### 2.3. 第三方数据 + +- 第三方企业提供的数据 + - 如:阿里妈妈、巨量引擎; + +#### 2.4. 手工数据 + +- 人工手动处理的表格; + +#### 2.5. 爬虫数据 + +- 通过爬虫技术获取的数据 + + +# 多源、异构数据融合 \ No newline at end of file diff --git "a/_drafts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232Apache Atlas \346\236\266\346\236\204\347\256\200\344\273\213\345\217\212\344\275\277\347\224\250\346\226\271\346\263\225.md" "b/_drafts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232Apache Atlas \346\236\266\346\236\204\347\256\200\344\273\213\345\217\212\344\275\277\347\224\250\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..dc7b3bc28d9 --- /dev/null +++ "b/_drafts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232Apache Atlas \346\236\266\346\236\204\347\256\200\344\273\213\345\217\212\344\275\277\347\224\250\346\226\271\346\263\225.md" @@ -0,0 +1,26 @@ +--- +layout: post +title: 数据治理:Apache Atlas 架构简介及使用方法 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据治理 +--- + +# Apache Atlas 架构 + +![]({{site.baseurl}}/img-post/元数据-3.png) + +# 3. + + +https://blog.csdn.net/qq_44665283/article/details/120026806 + + +https://blog.csdn.net/qq_22473611/article/details/118729193 + + +https://blog.csdn.net/weixin_43597208/article/details/121409058 \ No newline at end of file diff --git "a/_drafts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\345\272\224\347\224\250\345\275\261\345\223\215\345\272\246\345\210\206\346\236\220.md" "b/_drafts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\345\272\224\347\224\250\345\275\261\345\223\215\345\272\246\345\210\206\346\236\220.md" new file mode 100644 index 00000000000..a62c76617a8 --- /dev/null +++ "b/_drafts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\345\272\224\347\224\250\345\275\261\345\223\215\345\272\246\345\210\206\346\236\220.md" @@ -0,0 +1,39 @@ +--- +layout: post +title: 数据治理:应用影响度分析 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据治理 +--- + +# 应用影响分析 + +#### + +- 将系统数据库目录,和开发语言中的应用代码,自动收集到知识库中并解析,进而提供代码和数据库之间的结构及相关信息; +- 该功能,可以与之数据库目录信息(即表结构)的变更,可能带来的对源代码程序的影响和风险; +- 应用影响分析,节约了对当前系统的人工分析成本,自动、全面的提高运维效率,在系统改造、业务需求变化、需要变更表结构时,是非常有用的。 + + +#### 唯一性 + + +#### 有效性 + + +#### 一致性 + + +#### 正确性 + + +#### 及时性 + + + + + diff --git "a/_drafts/2022-01-27-\346\225\260\346\215\256\346\271\226\357\274\232Apache Hudi \346\212\200\346\234\257\345\216\237\347\220\206.md" "b/_drafts/2022-01-27-\346\225\260\346\215\256\346\271\226\357\274\232Apache Hudi \346\212\200\346\234\257\345\216\237\347\220\206.md" new file mode 100644 index 00000000000..d262fdbac9f --- /dev/null +++ "b/_drafts/2022-01-27-\346\225\260\346\215\256\346\271\226\357\274\232Apache Hudi \346\212\200\346\234\257\345\216\237\347\220\206.md" @@ -0,0 +1,16 @@ +--- +layout: unpost +title: 数据湖:Apache Hudi 技术原理 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: 数据湖 +tags: + - 数仓 +--- + + +# 1. + + diff --git "a/_drafts/2022-01-27-\346\225\260\346\215\256\350\265\204\344\272\247\357\274\232\344\274\201\344\270\232\346\225\260\346\215\256\350\265\204\344\272\247\346\242\263\347\220\206\346\200\235\350\267\257.md" "b/_drafts/2022-01-27-\346\225\260\346\215\256\350\265\204\344\272\247\357\274\232\344\274\201\344\270\232\346\225\260\346\215\256\350\265\204\344\272\247\346\242\263\347\220\206\346\200\235\350\267\257.md" new file mode 100644 index 00000000000..11564f69855 --- /dev/null +++ "b/_drafts/2022-01-27-\346\225\260\346\215\256\350\265\204\344\272\247\357\274\232\344\274\201\344\270\232\346\225\260\346\215\256\350\265\204\344\272\247\346\242\263\347\220\206\346\200\235\350\267\257.md" @@ -0,0 +1,17 @@ +--- +layout: post +title: 数据资产:企业数据资产梳理思路 +subtitle: +date: 2022-01-31 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据治理 +--- + + + +基于企业业务架构,从满足企业经营管理、数据分析、数据共享、数据集成等需求入手,对各个系统的数据资源进行盘点。 + +通过梳理数据现状,厘清业务开展过程中业务流、单据流以及数据流,明确数据资产分布,数据的质量情况、数据集成情况、数据管理情况等问题;明确各基础数据和指标数据的业务含义、数据口径、适用场景、数据来源、数据关系等信息。 \ No newline at end of file diff --git "a/_drafts/2022-01-28-DolphinScheduler\357\274\232DolphinScheduler \344\275\277\347\224\250\346\226\271\346\263\225\344\273\213\347\273\215.md" "b/_drafts/2022-01-28-DolphinScheduler\357\274\232DolphinScheduler \344\275\277\347\224\250\346\226\271\346\263\225\344\273\213\347\273\215.md" new file mode 100644 index 00000000000..940d79f9f67 --- /dev/null +++ "b/_drafts/2022-01-28-DolphinScheduler\357\274\232DolphinScheduler \344\275\277\347\224\250\346\226\271\346\263\225\344\273\213\347\273\215.md" @@ -0,0 +1,85 @@ +--- +layout: post +title: DolphinScheduler:DolphinScheduler 使用方法介绍 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Zookeeper +--- + +# 1. 准备工作 + +#### 1.1. 集群规划 + + ![]({{site.baseurl}}/img-post/flume-1.png) + + + +#### 1.2. 下载 + +
  • 点击此处下载:https://dlcdn.apache.org/flume/1.9.0/apache-flume-1.9.0-bin.tar.gz
  • + +#### 1.3. 安装 + +- 解压缩文件,到常用软件安装位置。 + + ```aidl + tar tar -zxf apache-flume-1.9.0-bin.tar.gz + + # 移动文件到指定位置 + mv apache-flume-1.9.0-bin/ flume + ``` + +# 2. 修改配置 + +#### 2.1. 删除低版本 guava 包 + +- 在 flume 文件夹 `/lib` 目录下执行 + + ```aidl + cd /lib + mv guava-11.0.2.jar guava-11.0.2.jar_bak + ``` + +#### 2.2. 修改日志配置 + +- 在 flume 文件夹 `/conf` 目录下修改 `log4j.properties` + + ```aidl + # 指定日志文件的绝对路径 + flume.log.dir=/usr/local/flume/logs + ``` + +#### 2.3. 调整堆内存 + +- 在 `flume/conf/` 目录下编辑 `flume-env.sh` + + ``` + cd flume/conf/ + mv flume-env.sh.template flume-env.sh + + vim flume-env.sh + ``` + +- 修改内存: + - 最小 1G、最大 4G; + - 根据实际需要,尽可能多配一点防止挂掉; + + ``` + export JAVA_OPTS="-Xms1000m -Xmx4000m -Dcom.sun.management.jmxremote" + ``` + + - Xms,表示 JVM Heap(堆内存)最小尺寸,初始分配; + - Xmx,表示 JVM Heap(堆内存)最大允许的尺寸,按需进行分配。 + +#### 2.4. 分发文件 + +- xsync 分发文件 + + ```aidl + xsync flume/ + ``` + diff --git "a/_drafts/2022-01-28-Hive\357\274\232Hive Metastore \346\234\215\345\212\241\351\205\215\347\275\256.md" "b/_drafts/2022-01-28-Hive\357\274\232Hive Metastore \346\234\215\345\212\241\351\205\215\347\275\256.md" new file mode 100644 index 00000000000..6426690fcc4 --- /dev/null +++ "b/_drafts/2022-01-28-Hive\357\274\232Hive Metastore \346\234\215\345\212\241\351\205\215\347\275\256.md" @@ -0,0 +1,85 @@ +--- +layout: post +title: Hive:Hive Metastore 服务配置 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Zookeeper +--- + +# 1. 准备工作 + +#### 1.1. 集群规划 + + ![]({{site.baseurl}}/img-post/flume-1.png) + + + +#### 1.2. 下载 + +
  • 点击此处下载:https://dlcdn.apache.org/flume/1.9.0/apache-flume-1.9.0-bin.tar.gz
  • + +#### 1.3. 安装 + +- 解压缩文件,到常用软件安装位置。 + + ```aidl + tar tar -zxf apache-flume-1.9.0-bin.tar.gz + + # 移动文件到指定位置 + mv apache-flume-1.9.0-bin/ flume + ``` + +# 2. 修改配置 + +#### 2.1. 删除低版本 guava 包 + +- 在 flume 文件夹 `/lib` 目录下执行 + + ```aidl + cd /lib + mv guava-11.0.2.jar guava-11.0.2.jar_bak + ``` + +#### 2.2. 修改日志配置 + +- 在 flume 文件夹 `/conf` 目录下修改 `log4j.properties` + + ```aidl + # 指定日志文件的绝对路径 + flume.log.dir=/usr/local/flume/logs + ``` + +#### 2.3. 调整堆内存 + +- 在 `flume/conf/` 目录下编辑 `flume-env.sh` + + ``` + cd flume/conf/ + mv flume-env.sh.template flume-env.sh + + vim flume-env.sh + ``` + +- 修改内存: + - 最小 1G、最大 4G; + - 根据实际需要,尽可能多配一点防止挂掉; + + ``` + export JAVA_OPTS="-Xms1000m -Xmx4000m -Dcom.sun.management.jmxremote" + ``` + + - Xms,表示 JVM Heap(堆内存)最小尺寸,初始分配; + - Xmx,表示 JVM Heap(堆内存)最大允许的尺寸,按需进行分配。 + +#### 2.4. 分发文件 + +- xsync 分发文件 + + ```aidl + xsync flume/ + ``` + diff --git "a/_drafts/2022-01-28-Hive\357\274\232Hive \345\256\211\350\243\205\346\255\245\351\252\244 copy.md" "b/_drafts/2022-01-28-Hive\357\274\232Hive \345\256\211\350\243\205\346\255\245\351\252\244 copy.md" new file mode 100644 index 00000000000..35831dd2eaf --- /dev/null +++ "b/_drafts/2022-01-28-Hive\357\274\232Hive \345\256\211\350\243\205\346\255\245\351\252\244 copy.md" @@ -0,0 +1,85 @@ +--- +layout: post +title: Hive:Hive 安装步骤 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Zookeeper +--- + +# 1. 准备工作 + +#### 1.1. 集群规划 + + ![]({{site.baseurl}}/img-post/flume-1.png) + + + +#### 1.2. 下载 + +
  • 点击此处下载:https://dlcdn.apache.org/flume/1.9.0/apache-flume-1.9.0-bin.tar.gz
  • + +#### 1.3. 安装 + +- 解压缩文件,到常用软件安装位置。 + + ```aidl + tar tar -zxf apache-flume-1.9.0-bin.tar.gz + + # 移动文件到指定位置 + mv apache-flume-1.9.0-bin/ flume + ``` + +# 2. 修改配置 + +#### 2.1. 删除低版本 guava 包 + +- 在 flume 文件夹 `/lib` 目录下执行 + + ```aidl + cd /lib + mv guava-11.0.2.jar guava-11.0.2.jar_bak + ``` + +#### 2.2. 修改日志配置 + +- 在 flume 文件夹 `/conf` 目录下修改 `log4j.properties` + + ```aidl + # 指定日志文件的绝对路径 + flume.log.dir=/usr/local/flume/logs + ``` + +#### 2.3. 调整堆内存 + +- 在 `flume/conf/` 目录下编辑 `flume-env.sh` + + ``` + cd flume/conf/ + mv flume-env.sh.template flume-env.sh + + vim flume-env.sh + ``` + +- 修改内存: + - 最小 1G、最大 4G; + - 根据实际需要,尽可能多配一点防止挂掉; + + ``` + export JAVA_OPTS="-Xms1000m -Xmx4000m -Dcom.sun.management.jmxremote" + ``` + + - Xms,表示 JVM Heap(堆内存)最小尺寸,初始分配; + - Xmx,表示 JVM Heap(堆内存)最大允许的尺寸,按需进行分配。 + +#### 2.4. 分发文件 + +- xsync 分发文件 + + ```aidl + xsync flume/ + ``` + diff --git "a/_drafts/2022-01-28-Hive\357\274\232Hive \345\256\211\350\243\205\346\255\245\351\252\244.md" "b/_drafts/2022-01-28-Hive\357\274\232Hive \345\256\211\350\243\205\346\255\245\351\252\244.md" new file mode 100644 index 00000000000..35831dd2eaf --- /dev/null +++ "b/_drafts/2022-01-28-Hive\357\274\232Hive \345\256\211\350\243\205\346\255\245\351\252\244.md" @@ -0,0 +1,85 @@ +--- +layout: post +title: Hive:Hive 安装步骤 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Zookeeper +--- + +# 1. 准备工作 + +#### 1.1. 集群规划 + + ![]({{site.baseurl}}/img-post/flume-1.png) + + + +#### 1.2. 下载 + +
  • 点击此处下载:https://dlcdn.apache.org/flume/1.9.0/apache-flume-1.9.0-bin.tar.gz
  • + +#### 1.3. 安装 + +- 解压缩文件,到常用软件安装位置。 + + ```aidl + tar tar -zxf apache-flume-1.9.0-bin.tar.gz + + # 移动文件到指定位置 + mv apache-flume-1.9.0-bin/ flume + ``` + +# 2. 修改配置 + +#### 2.1. 删除低版本 guava 包 + +- 在 flume 文件夹 `/lib` 目录下执行 + + ```aidl + cd /lib + mv guava-11.0.2.jar guava-11.0.2.jar_bak + ``` + +#### 2.2. 修改日志配置 + +- 在 flume 文件夹 `/conf` 目录下修改 `log4j.properties` + + ```aidl + # 指定日志文件的绝对路径 + flume.log.dir=/usr/local/flume/logs + ``` + +#### 2.3. 调整堆内存 + +- 在 `flume/conf/` 目录下编辑 `flume-env.sh` + + ``` + cd flume/conf/ + mv flume-env.sh.template flume-env.sh + + vim flume-env.sh + ``` + +- 修改内存: + - 最小 1G、最大 4G; + - 根据实际需要,尽可能多配一点防止挂掉; + + ``` + export JAVA_OPTS="-Xms1000m -Xmx4000m -Dcom.sun.management.jmxremote" + ``` + + - Xms,表示 JVM Heap(堆内存)最小尺寸,初始分配; + - Xmx,表示 JVM Heap(堆内存)最大允许的尺寸,按需进行分配。 + +#### 2.4. 分发文件 + +- xsync 分发文件 + + ```aidl + xsync flume/ + ``` + diff --git "a/_drafts/2022-01-28-Kafka\357\274\232Kafka \344\270\216 flume \347\232\204\346\257\224\350\276\203\344\273\245\345\217\212\351\233\206\346\210\220\346\226\271\346\263\225.md" "b/_drafts/2022-01-28-Kafka\357\274\232Kafka \344\270\216 flume \347\232\204\346\257\224\350\276\203\344\273\245\345\217\212\351\233\206\346\210\220\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..d9a65dca53a --- /dev/null +++ "b/_drafts/2022-01-28-Kafka\357\274\232Kafka \344\270\216 flume \347\232\204\346\257\224\350\276\203\344\273\245\345\217\212\351\233\206\346\210\220\346\226\271\346\263\225.md" @@ -0,0 +1,66 @@ +--- +layout: post +title: Kafka:Kafka 与 flume 的比较以及集成方法 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Kafka +--- + + +# Kafka与Flume比较 +在企业中必须要清楚流式数据采集框架flume和kafka的定位是什么: +flume:cloudera公司研发: + 适合多个生产者; +适合下游数据消费者不多的情况; +适合数据安全性要求不高的操作; +适合与Hadoop生态圈对接的操作。 +kafka:linkedin公司研发: +适合数据下游消费众多的情况; +适合数据安全性要求较高的操作,支持replication。 +因此我们常用的一种模型是: +线上数据 --> flume --> kafka --> flume(根据情景增删该流程) --> HDFS + +# Flume与kafka集成 +1)配置flume(flume-kafka.conf) +``` +# define +a1.sources = r1 +a1.sinks = k1 +a1.channels = c1 + +# source +a1.sources.r1.type = exec +a1.sources.r1.command = tail -F -c +0 /opt/module/datas/flume.log +a1.sources.r1.shell = /bin/bash -c + +# sink +a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink +a1.sinks.k1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092 +a1.sinks.k1.kafka.topic = first +a1.sinks.k1.kafka.flumeBatchSize = 20 +a1.sinks.k1.kafka.producer.acks = 1 +a1.sinks.k1.kafka.producer.linger.ms = 1 + +# channel +a1.channels.c1.type = memory +a1.channels.c1.capacity = 1000 +a1.channels.c1.transactionCapacity = 100 + +# bind +a1.sources.r1.channels = c1 +a1.sinks.k1.channel = c1 +``` + +2) 启动kafkaIDEA消费者 + +3) 进入flume根目录下,启动flume + +$ bin/flume-ng agent -c conf/ -n a1 -f jobs/flume-kafka.conf + +4) 向 /opt/module/datas/flume.log里追加数据,查看kafka消费者消费情况 + +$ echo hello > /opt/module/datas/flume.log \ No newline at end of file diff --git "a/_drafts/2022-01-28-Neo4j\357\274\232Neo4j \345\233\276\346\225\260\346\215\256\345\272\223\345\216\237\347\220\206.md" "b/_drafts/2022-01-28-Neo4j\357\274\232Neo4j \345\233\276\346\225\260\346\215\256\345\272\223\345\216\237\347\220\206.md" new file mode 100644 index 00000000000..566394e7932 --- /dev/null +++ "b/_drafts/2022-01-28-Neo4j\357\274\232Neo4j \345\233\276\346\225\260\346\215\256\345\272\223\345\216\237\347\220\206.md" @@ -0,0 +1,33 @@ +--- +layout: post +title: Neo4j:Neo4j 图数据库原理 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Neo4j +--- + + +https://www.jianshu.com/p/4ce05d42833d + +https://www.jianshu.com/p/182c8dc2d266 + + +https://www.jianshu.com/p/91d870fbc905 + + +https://cloud.tencent.com/developer/article/1142179 + + +https://www.jianshu.com/p/9938be9c66c2 + +https://blog.csdn.net/superman_xxx/article/details/88723316 + +https://blog.csdn.net/u013416034/article/details/105365397/ + +https://zhuanlan.zhihu.com/p/403062270 + + diff --git "a/_drafts/2022-01-28-PostgreSQL\357\274\232PostgreSQL \347\256\200\344\273\213\345\217\212\345\267\245\344\275\234\345\216\237\347\220\206.md" "b/_drafts/2022-01-28-PostgreSQL\357\274\232PostgreSQL \347\256\200\344\273\213\345\217\212\345\267\245\344\275\234\345\216\237\347\220\206.md" new file mode 100644 index 00000000000..83d5bfbf9b1 --- /dev/null +++ "b/_drafts/2022-01-28-PostgreSQL\357\274\232PostgreSQL \347\256\200\344\273\213\345\217\212\345\267\245\344\275\234\345\216\237\347\220\206.md" @@ -0,0 +1,142 @@ +--- +layout: post +title: PostgreSQL:PostgreSQL 简介及工作原理 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - PostgreSQL +--- + + + +https://www.bilibili.com/read/cv8138815/ + + +#### 事务的一些重要性质 + + 事务是所有数据库系统的基础概念。 + + 事务最重要的一点是它将多个步骤捆绑成了一个单一的、要么全完成要么全不完成的操作。步骤之间的中间状态对于其他并发事务是不可见的,并且如果有某些错误发生导致事务不能完成,则其中任何一个步骤都不会对数据库造成影响。即: + + 1、一个事务被称为是原子的:从其他事务的角度来看,它要么整个发生要么完全不发生。 + + 2、一个事务型数据库保证一个事务在被报告为完成之前它所做的所有更新都被记录在持久存储(即磁盘)。 + + 事务型数据库的另一个重要性质与原子更新的概念紧密相关:当多个事务并发运行时,每一个都不能看到其他事务未完成的修改。所以事务的全做或全不做并不只体现在它们对数据库的持久影响,也体现在它们发生时的可见性。一个事务所做的更新在它完成之前对于其他事务是不可见的,而之后所有的更新将同时变得可见。 + +#### PostSQL中的事务 + + PostgreSQL实际上将每一个SQL语句都作为一个事务来执行。如果我们没有发出BEGIN命令,则每个独立的语句都会被加上一个隐式的BEGIN以及(如果成功)COMMIT来包围它。 一组被BEGIN和COMMIT包围的语句也被称为一个事务块。值得注意的是,某些客户端库会自动发出BEGIN和COMMIT命令,因此我们可能会在不被告知的情况下得到事务块的效果。具体请查看所使用的接口文档。 + + 也可以利用保存点来以更细的粒度来控制一个事务中的语句。保存点允许我们有选择性地放弃事务的一部分而提交剩下的部分。在使用SAVEPOINT定义一个保存点后,我们可以在必 要时利用ROLLBACK TO回滚到该保存点。该事务中位于保存点和回滚点之间的数据库修改都会被放弃,但是早于该保存点的修改则会被保存。 + + 在回滚到保存点之后,它的定义依然存在,因此我们可以多次回滚到它。反过来,如果确定不再需要回滚到特定的保存点,它可以被释放以便系统释放一些资源。记住不管是释放保存点还是回滚到保存点都会释放定义在该保存点之后的所有其他保存点。 + + 所有这些都发生在一个事务块内,因此这些对于其他数据库会话都不可见。当提交整个事务块时,被提交的动作将作为一个单元变得对其他会话可见,而被回滚的动作则永远不会变得可见。 + + 另外,在一个事务块中使用保存点存在很多种控制可能性。以及,使用ROLLBACK TO是唯一重新控制一个由于错误被系统置为中断状态的事务块的途径,而不是完全回滚它并重新启动。 + + +# 强大的索引类型 + PostgreSQL 支持索引类型 + + B-tree(默认):适合顺序检索、范围查询 + Hash:适合简单的等式比较 + GIN:(Generalized Inverted Index)即倒排索引,存储被索引字段的VALUE或VALUE的元素,以及行号的list或tree,适合数组元素查询、全文搜索等。 + rum:GIN的加强,官方开源插件。 + GiST:GiST是一个通用的索引接口,可以使用GiST实现b-tree, r-tree等索引结构。不同的类型,支持的索引检索也各不一样。例如:1)几何类型支持位置距离排序;2)标量类型支持距离排序等等 + SP-GiST:SP-GiST类似GiST,是一个通用的索引接口,但是SP-GIST使用了空间分区的方法,使得SP-GiST可以更好的支持非平衡数据结构,例如quad-trees, k-d tree, radis tree。例如:几何类型支持正交、相交、在上下左右等 + BRIN:BRIN 索引是块级索引,有别于B-TREE等索引,BRIN记录并不是以行号为单位记录索引明细,而是记录每个数据块或者每段连续的数据块的统计信息。因此BRIN索引空间占用特别的小,对数据写入、更新、删除的影响也很小。BRIN属于LOSSLY索引,当被索引列的值与物理存储相关性很强时,BRIN索引的效果非常的好。例如时序数据,在时间或序列字段创建BRIN索引,进行等值、范围查询时效果很棒。 + bloom:基于bloom filter构造的一个索引接口,属于lossy索引,可以收敛结果集(排除绝对不满足条件的结果,剩余的结果里再挑选满足条件的结果),因此需要二次check,bloom支持任意列组合的等值查询。 + zombodb:PostgreSQL与ElasticSearch结合的一个索引接口,可以直接用sql读写ES。 + 其它:PostgreSQL 9种索引的原理和应用场景-阿里云开发者社区 + 可扩展性强:如向量索引(图搜索、人脸识别、个性推荐) 高维向量检索技术在PG中的设计与实践 ——杨文(缁尘)-阿里云开发者社区 + 【PostgreSQL的索引文件和数据文件是完全分开的。都为非聚集索引】 + + + +大数据时代,人们使用数据库系统处理的数据量越来越大,请求越来越复杂,对数据库系统的大数据处理能力和混合负载能力提出更高的要求。PostgreSQL 作为世界上最先进的开源数据库,在大数据处理方面做了很多工作,如并行和分区。 + + +# PostgreSQL 的并行查询特性 + +- 并行查询基础组件,包括: + - 后台工作进程(Background Work Process) + - 动态共享内存(Dynamic Shared Memory) + - 后台工作进程间的通信机制和消息传递机制 + +并行执行算子的实现,包括并行顺序扫描、并行索引扫描等并行扫描算子,三种连接方式的并行执行以及并行 Append +并行查询优化,介绍并行查询引入的两种计划节点,基于规则计算后台工作进程数量以及代价估算 + +max_parallel_workers_per_gather 参数控制执行节点的最大并行进程数,通过以上并行计划可知,开启并行后,会启动两个 worker 进程(即 Workers Launched: 2)并行执行,且执行时间(Execution Time)仅为不并行的40%。 + + + + +原理 + +- PostgreSQL的并行化包含三个重要组件: + - 进程本身(leader进程) + - gather + - workers + +- 并行查询计划中,我们将处理用户请求的 backend 进程称之为主进程(leader),将执行时动态生成的进程称之为工作进程(worker); +- 每个 worker 执行 Gather 节点以下计划的一个副本,leader 节点主要负责处理 Gather 及其以上节点的操作,根据 worker 数不同,leader 也可能会执行 Gather 以下计划的副本; +- 没有开启并行化的时候,进程自身处理所有的数据;一旦计划器决定某个查询或查询中部分可以使用并行的时候,就会在查询的并行化部分添加一个gather节点,将gather节点作为子查询树的根节点; + + +查询执行是从 leader 进程开始。一旦开启了并行或查询中部分支持并行,就会分配一个gather节点和多个worker线程。relation的blocks在各个workers线程之间划分。workers的数量受postgresql的配置参数控制。workers之间使用共享内存相互协调和通信,一旦worker完成了自己的工作,结果就被传给了leader进程。 + +workers和leader进程之间使用消息队列(依赖共享内存)进行通信。每个进程有两个队列:一个是error队列;一个是tuples队列。 + + +# + +并行查询依赖的两个重要基础组件:后台工作进程和动态共享内存。前者用于动态创建 worker,以并行执行子查询计划;后者用于 leader 和 worker 间通信和数据交互。 + +# 后台工作进程(Background Worker Process) + +PostgreSQL 是多进程架构,主要包括以下几类进程: + +守护进程,通常称之为 postmaster 进程,接收用户的连接并 fork 一个子进程处理用户的请求 +backend 进程,即 postmaster 创建的用于处理用户请求的进程,每个连接对应一个 backend 进程 +辅助进程,用于执行 checkpoint,后台刷脏等操作的进程 +后台工作进程,用于执行特定任务而动态启动的进程,如上文提到的 worker 进程 + +上图中 server process 即 postmaster 进程,在内核中,postmaster 与 backend 进程都是 postgres 进程,只是角色不同。对于一个并行查询,其创建 worker 进程的大致流程如下: +client 创建连接,postmaster 为其 fork 一个 backend 进程处理请求 +backend 接收用户请求,并生成并行查询计划 +执行器向 backgroudworker 注册 worker 进程(并没有启动) +执行器通知(kill)postmaster 启动 worker 进程 +worker 进程与 leader 进程协调执行,将结果返回 client + + +# 动态共享内存(Dynamic Shared Memory) + +PostgreSQL 是多进程架构,进程间通信往往通过共享内存和信号量来实现。对于并行查询而言,执行时创建的 worker 进程与 leader 进程同样通过共享内存实现数据交互。但这部分内存无法像普通的共享内存那样在系统启动时预先分配,毕竟直到真正执行时才知道有多少 worker 进程,以及需要分配多少内存。PostgreSQL 实现了动态共享内存,即在执行时动态创建,用于 leader 与 worker 间通信,执行完成后释放。基于动态共享内存的队列用于进程间传递元组和错误消息。 + +# 并行支持算子 + +并行执行的算子的实现原理,包括: + +并行扫描,如并行顺序扫描,并行索引扫描等 +并行连接,如并行哈希连接,并行 NestLoop 连接等 +并行 Append + +# 并行顺序扫描(Parallel sequential scan) + +在PostgreSQL 9.6中,增加了对并行顺序扫描的支持。顺序扫描是在表上进行的扫描,在该表中一个接一个的块顺序地被评估。就其本质而言,顺序扫描允许并行化。这样,整个表将在多个workers线程之间顺序扫描。 + +并行顺序扫描快并不是因为可以并行地读,而是将数据分散到了多个cpu。 +顺序扫描产生了大量的行,但是没有使用聚合函数。因此,查询使用的是单个cpu核心。 + +增加一个sum函数后,很明显使用了两个工作线程,从而使得查询加速: + +# 并行聚合(Parallel Aggregation) + +在数据库中,计算聚合是非常昂贵的操作。如果以单个进程进行执行,则这将花费相当长的时间。在PostgreSQL 9.6中,通过简单地将它们分成多个块(分而治之策略)来增加了并行计算的能力。多个worker线程执行聚合的部分,然后leader再根据它们的结果计算最终结果。 + +从技术上讲,将Partial Aggregate节点添加到计划树中,并且每个Partial Aggregate节点包含一个worker线程的输出。然后将这些输出发送到Finalize Aggregate节点,该节点合并来自多个(所有)Partial Aggregate节点的聚合。如此有效的并行部分计划在根部包括一个Finalize Aggregate节点,以及一个将Partial Aggregate节点作为子节点的Gather节点。 diff --git "a/_drafts/2022-01-30-MPP\357\274\232MPP \345\270\270\347\224\250\346\225\260\346\215\256\345\272\223\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" "b/_drafts/2022-01-30-MPP\357\274\232MPP \345\270\270\347\224\250\346\225\260\346\215\256\345\272\223\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" new file mode 100644 index 00000000000..48aca327957 --- /dev/null +++ "b/_drafts/2022-01-30-MPP\357\274\232MPP \345\270\270\347\224\250\346\225\260\346\215\256\345\272\223\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" @@ -0,0 +1,51 @@ +--- +layout: post +title: MPP:MPP 常用数据库的对比分析 +subtitle: +date: 2022-01-30 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - MPP +--- + + +# 1. 影响 Hive 效率的因素 + +#### 1.1. 数据倾斜 + +#### 1.2. 数据冗余 + +#### 1.3. JOB / IO 过多 + +#### 1.4. MapReduce 分配不合理 + + +# 2. 优化思路 + +#### 2.1. 对 Hive SQL 语句的优化 + + +#### 2.2. Hive 配置项优化 + + +#### 2.3. MapReduce 配置优化 + + +# 3. 优化方法 + +#### 3.1. 列裁剪 & 分区裁剪 + +- 列裁剪,就是在查询时只读取需要的列; +- 分区裁剪,就是只读取需要的分区。 +>全列扫描和全表扫描,他们的效率都很低。 + + + + + + ![]({{site.baseurl}}/img-post/es-5.png) + +- 这表示安装成、服务已被启动; + diff --git "a/_drafts/2022-01-30-MPP\357\274\232MPP \346\225\260\346\215\256\345\272\223\344\270\216 Hadoop \345\214\272\345\210\253.md" "b/_drafts/2022-01-30-MPP\357\274\232MPP \346\225\260\346\215\256\345\272\223\344\270\216 Hadoop \345\214\272\345\210\253.md" new file mode 100644 index 00000000000..b9e6c35d97b --- /dev/null +++ "b/_drafts/2022-01-30-MPP\357\274\232MPP \346\225\260\346\215\256\345\272\223\344\270\216 Hadoop \345\214\272\345\210\253.md" @@ -0,0 +1,51 @@ +--- +layout: post +title: MPP:MPP 数据库与 Hadoop 区别 +subtitle: +date: 2022-01-30 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - MPP +--- + + +# 1. + +#### 1.1. 数据倾斜 + +#### 1.2. 数据冗余 + +#### 1.3. JOB / IO 过多 + +#### 1.4. MapReduce 分配不合理 + + +# 2. 优化思路 + +#### 2.1. 对 Hive SQL 语句的优化 + + +#### 2.2. Hive 配置项优化 + + +#### 2.3. MapReduce 配置优化 + + +# 3. 优化方法 + +#### 3.1. 列裁剪 & 分区裁剪 + +- 列裁剪,就是在查询时只读取需要的列; +- 分区裁剪,就是只读取需要的分区。 +>全列扫描和全表扫描,他们的效率都很低。 + + + + + + ![]({{site.baseurl}}/img-post/es-5.png) + +- 这表示安装成、服务已被启动; + diff --git "a/_drafts/2022-01-30-MPP\357\274\232MPP \346\225\260\346\215\256\345\272\223\346\246\202\345\277\265\344\273\213\347\273\215.md" "b/_drafts/2022-01-30-MPP\357\274\232MPP \346\225\260\346\215\256\345\272\223\346\246\202\345\277\265\344\273\213\347\273\215.md" new file mode 100644 index 00000000000..a2e3b4f21fb --- /dev/null +++ "b/_drafts/2022-01-30-MPP\357\274\232MPP \346\225\260\346\215\256\345\272\223\346\246\202\345\277\265\344\273\213\347\273\215.md" @@ -0,0 +1,51 @@ +--- +layout: post +title: MPP:MPP 数据库概念介绍 +subtitle: +date: 2022-01-30 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - MPP +--- + + +# 1. + +#### 1.1. 数据倾斜 + +#### 1.2. 数据冗余 + +#### 1.3. JOB / IO 过多 + +#### 1.4. MapReduce 分配不合理 + + +# 2. 优化思路 + +#### 2.1. 对 Hive SQL 语句的优化 + + +#### 2.2. Hive 配置项优化 + + +#### 2.3. MapReduce 配置优化 + + +# 3. 优化方法 + +#### 3.1. 列裁剪 & 分区裁剪 + +- 列裁剪,就是在查询时只读取需要的列; +- 分区裁剪,就是只读取需要的分区。 +>全列扫描和全表扫描,他们的效率都很低。 + + + + + + ![]({{site.baseurl}}/img-post/es-5.png) + +- 这表示安装成、服务已被启动; + diff --git "a/_drafts/2022-01-31-CDH\357\274\232\345\237\272\344\272\216\351\230\277\351\207\214\344\272\221\345\210\233\345\273\272 CM yum \346\272\220.md" "b/_drafts/2022-01-31-CDH\357\274\232\345\237\272\344\272\216\351\230\277\351\207\214\344\272\221\345\210\233\345\273\272 CM yum \346\272\220.md" new file mode 100644 index 00000000000..e3d08b82806 --- /dev/null +++ "b/_drafts/2022-01-31-CDH\357\274\232\345\237\272\344\272\216\351\230\277\351\207\214\344\272\221\345\210\233\345\273\272 CM yum \346\272\220.md" @@ -0,0 +1,158 @@ +--- +layout: post +title: ETL方法论:数据读取 +subtitle: +date: 2022-01-31 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - ETL +--- + + +``` +vi /etc/yum.repos.d/cloudera-manager.repo +``` + + +```aidl +[cloudera-manager] + +name = Cloudera Manager, Version 6.2.1 + +baseurl =http://hadoop102:8900/cloudera-repos/cm6/6.2.1/redhat7/yum/ + + +enabled=1 + +gpgcheck = 1 +``` + +注意MySQL的版本选择5.5或5.6,因其和Hadoop生态圈的Oozie、Hue、Hive/Impala、Sqoop等的兼容性比较好。 + + +安装数据库 +``` +yum -y install mariadb mariadb-server +``` + +``` +systemctl start mariadb +systemctl enable mariadb +systemctl status mariadb +``` + +- 配置mariadb,设置密码为 password + +``` +/usr/bin/mysql_secure_installation +``` + +```aidl +mysql -uroot -p +``` + +```aidl +create database metastore default character set utf8; +CREATE USER 'hive'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON metastore.* TO 'hive'@'%'; +FLUSH PRIVILEGES; + +create database scm default character set utf8; +CREATE USER 'scm'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON scm.* TO 'scm'@'%'; +FLUSH PRIVILEGES; + +create database rm default character set utf8; +CREATE USER 'rm'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON rm.* TO 'rm'@'%'; +FLUSH PRIVILEGES; + +create database am default character set utf8; +CREATE USER 'am'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON am.* TO 'am'@'%'; +FLUSH PRIVILEGES; + +create database hue default character set utf8; +CREATE USER 'hue'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON hue.* TO 'hue'@'%'; +FLUSH PRIVILEGES; + +create database oozie default character set utf8; +CREATE USER 'oozie'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON oozie.* TO 'oozie'@'%'; +FLUSH PRIVILEGES; + +create database sentry default character set utf8; +CREATE USER 'sentry'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON sentry. * TO 'sentry'@'%'; +FLUSH PRIVILEGES; + +create database nav_ms default character set utf8; +CREATE USER 'nav_ms'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON nav_ms. * TO 'nav_ms'@'%'; +FLUSH PRIVILEGES; + +create database nav_as default character set utf8; +CREATE USER 'nav_as'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON nav_as. * TO 'nav_as'@'%'; +FLUSH PRIVILEGES; +``` + +mysql-connector-java-5.1.49-bin.jar + +安装jdbc驱动:把名字:mysql-connector-java-5.1.49.jar 改为:mysql-connector-java.jar + +创建/usr/share/java/文件夹,然后将MySQL的JDBC驱动包mysql-connector-java.jar上传至该目录 + +- 下载依赖包 + +```aidl +yum -y install chkconfig bind-utils psmisc libxslt zlib sqlite cyrus-sasl-plain cyrus-sasl-gssapi fuse portmap fuse-libs redhat-lsb +``` + + +```aidl +/opt/cloudera/cm/schema/scm_prepare_database.sh mysql scm root password +``` + +- 启动 + +master: +``` +systemctl enable cloudera-scm-agent + +systemctl enable cloudera-scm-server + +systemctl start cloudera-scm-server + +systemctl start cloudera-scm-agent +``` + +- 机器: +``` +systemctl enable cloudera-scm-agent + +systemctl start cloudera-scm-agent +``` + + +- 查看7180端口是否被监听。当出现如下所示7180端口被监听,则可以打开浏览器Web页面。 +```aidl +netstat -lnpt | grep 7180 +``` + + +![]({{site.baseurl}}/img-post/cdh-1.png) + + +`hadoop10[2-4]` + +![]({{site.baseurl}}/img-post/cdh-2.png) + +`http://xxx.xxx.xxx.xxx/cloudera-repos/cm6/6.2.1` + + + + diff --git "a/_drafts/2022-10-01-MySQL \345\270\270\350\247\201\347\232\204\351\233\206\347\276\244\351\203\250\347\275\262\346\250\241\345\274\217.md" "b/_drafts/2022-10-01-MySQL \345\270\270\350\247\201\347\232\204\351\233\206\347\276\244\351\203\250\347\275\262\346\250\241\345\274\217.md" new file mode 100644 index 00000000000..f67bf4119fb --- /dev/null +++ "b/_drafts/2022-10-01-MySQL \345\270\270\350\247\201\347\232\204\351\233\206\347\276\244\351\203\250\347\275\262\346\250\241\345\274\217.md" @@ -0,0 +1,21 @@ +--- +layout: post +title: MySQL 常见的集群部署模式 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 数据库 +--- + + + +https://blog.csdn.net/daqu1314/article/details/121381362 + + +https://www.cnblogs.com/lvxueyang/p/14967617.html + + +http://www.cppcns.com/shujuku/mysql/253326.html \ No newline at end of file diff --git "a/_drafts/2022-10-01-\346\225\217\346\215\267\345\274\200\345\217\221\357\274\232\345\205\263\344\272\216\346\225\217\346\215\267\345\274\200\345\217\221\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.md" "b/_drafts/2022-10-01-\346\225\217\346\215\267\345\274\200\345\217\221\357\274\232\345\205\263\344\272\216\346\225\217\346\215\267\345\274\200\345\217\221\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.md" new file mode 100644 index 00000000000..588d155a906 --- /dev/null +++ "b/_drafts/2022-10-01-\346\225\217\346\215\267\345\274\200\345\217\221\357\274\232\345\205\263\344\272\216\346\225\217\346\215\267\345\274\200\345\217\221\347\232\204\344\270\200\344\272\233\346\200\235\350\200\203.md" @@ -0,0 +1,60 @@ +--- +layout: post +title: 敏捷开发:关于敏捷开发的一些思考 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 敏捷开发 +--- + + +# 需求梳理 + +#### 将需求尽可能的拆分到小颗粒度 + +#### 从业务需求到功能需求的多层级拆分 + + +# 架构设计 + +#### 进行合理的架构解耦 + +从单体架构到分布式架构最后到微服务架构 + +#### 加强编码阶段的质量保证,进行充分的单元测试和代码检查,尽早的发现问题。 + +#### 完善的测试管理以及充分的自动化UI测试、接口测试,确保产品输出的质量 + +#### 实施过程 + +实施过程中包括设计、编码、测试等获得,通过每日站会同步进度,并处理遇到的问题和障碍。 + + +# 优秀开发者的特点 + +- 设计简洁: + - 努力用最简洁的方式实现复杂的需求。 + - 复杂的代码理解和维护都很困难,且容易出现性能问题。 + +- 程序稳定: + - 考虑功能实现的同时还要考虑性能和稳定性。 + +- 代码易读: + - 努力提高代码的可读性。 + +- 预见并规避问题: + - 花更多的精力在源端数据探查,能够提前考虑导数据源端的各种问题从而开发出相对健壮的代码。 + +- 充分了解需求: + - 充分了解业务,对需求有前瞻性预判,甚至能够辅助需求方做出改进,使得需求更加合理同时开发更简单。 + - 极端情况下要有 A/B 预案,比如: + - 需求方提出了极不合理的需求又不听劝告,我们只能按需求方需求实现的同时,准备了更好的方案,以便需求方将来更改需求时能够从容应对。 + +- 少出 bug: + - 般不会出现重大质量问题,并且极少的出现 Bug。 + +- 方便下游: + - 程序开发的同时,也能兼顾考虑后续的流程和数据质量监控问题。 diff --git "a/_drafts/2022-10-01-\346\225\260\346\215\256\345\210\206\346\236\220\357\274\232\346\225\260\346\215\256\346\240\207\345\207\206\345\214\226.md" "b/_drafts/2022-10-01-\346\225\260\346\215\256\345\210\206\346\236\220\357\274\232\346\225\260\346\215\256\346\240\207\345\207\206\345\214\226.md" new file mode 100644 index 00000000000..04c578ff133 --- /dev/null +++ "b/_drafts/2022-10-01-\346\225\260\346\215\256\345\210\206\346\236\220\357\274\232\346\225\260\346\215\256\346\240\207\345\207\206\345\214\226.md" @@ -0,0 +1,21 @@ +--- +layout: post +title: 数据分析:数据标准化 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 数据分析 +--- + + +# 需求梳理 + +#### 将需求尽可能的拆分到小颗粒度 + +#### 从业务需求到功能需求的多层级拆分 + + + diff --git "a/_drafts/2022-10-01-\346\234\215\345\212\241\345\231\250\357\274\232\345\272\224\347\224\250\346\234\215\345\212\241\345\231\250 \345\222\214 WEB\346\234\215\345\212\241\345\231\250 \347\232\204\345\214\272\345\210\253.md" "b/_drafts/2022-10-01-\346\234\215\345\212\241\345\231\250\357\274\232\345\272\224\347\224\250\346\234\215\345\212\241\345\231\250 \345\222\214 WEB\346\234\215\345\212\241\345\231\250 \347\232\204\345\214\272\345\210\253.md" new file mode 100644 index 00000000000..333a0083920 --- /dev/null +++ "b/_drafts/2022-10-01-\346\234\215\345\212\241\345\231\250\357\274\232\345\272\224\347\224\250\346\234\215\345\212\241\345\231\250 \345\222\214 WEB\346\234\215\345\212\241\345\231\250 \347\232\204\345\214\272\345\210\253.md" @@ -0,0 +1,120 @@ +--- +layout: post +title: 服务器:应用服务器 和 WEB服务器 的区别 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 硬件网络 +--- + + + +# 应用服务器 + +#### 什么是应用服务器 + +- 应用服务器是应用的服务器,提供应用服务; +- 如J2EE中间件:基于jboss,weblogic等的应用,也可以是自己的网络应用服务器,接口服务器是提供给第三方调用的服务,主要是为了我们自己的应用得安全性,所以我们只把能供给第三方调用的东西封装在接口服务器。 +- 应用服务器为客户端提供对业务逻辑的访问。这种服务器根据客户端的请求,将数据转换为动态内容。比如上面打开个人微博的例子,需要应用服务器执行程序,从数据库中找到用户的最新微博信息再把信息转换成HTML网页显示在客户面前。 + 通常满足一个用户需求还需要数据库来支持。 +- 应用服务器的搭建很多时候依赖于应用程序的开发语言,各种编程语言生态下对应不同的软件,比如使用java语言开发的项目 通常选择tomcat 或JBoss等作为程序运行的应用服务器,而使用python语言开发web应用一般会选择 django等Python框架下的软件,来作为它的应用服务器。 + +- 应用服务器种类: + - Tomcat应用服务器 + + - Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,它运行时占用的系统资源小,扩展性好,支持负载平衡与邮件服务等开发应用系统常用的功能; + Tomcat 部分是Apache 服务器的扩展,但它是独立运行的,所以当你 运行tomcat 时,它实际上作为一个与Apache 独立的进程单独运行的。 + 因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。 + + - Weblogic应用服务器 + + - Web Logic 是美国bea公司出品的一个application server确切的说是一个基于Javaee架构的中间件,BEA Web Logic是用于开发、集成、部署和管理大型分布式Web应用、网络应用和数据库应用的Java应用服务器。 + - Web Logic 将Java的动态功能和Java Enterprise标准的安全性引入大型网络应用的开发、集成、部署和管理之中,是用来构建网站的必要软件,拥有解析发布网页等功能,它是用纯java开发的。 + + - Web logic应用服务器有以下优点: + - 对业内多种标准的全面支持,包括EJB、JSB、JMS、JDBC、XML和WML,使Web应用系统的实施更为简单,并且保护了投资,同时也使基于标准的解决方案的开发更加简便。 + - 无限的可扩展性BEA Web Logic Server以其高扩展的架构体系闻名于业内,包括客户机连接的共享、资源pooling以及动态网页和EJB组件群集。 + - 凭借对EJB和JSP的支持,以及BEA Web Logic Server 的Servlet组件架 构体系,可加速投放市场速度。这些开放性标准与Web Gain Studio配合时,可简化开发,并可发挥已有的技能,迅速部署应用系统。 + - BEA Web Logic Server的特点是与领先数据库、操作系统和Web服务器紧密集成。 + - 关键任务可靠性其容错、系统管理和安全性能已经在全球数以千记的关键任务环境中得以验证。 + - BEA Web Logic Server简化了可移植及可扩展的应用系统的开发,并为其它应用系统和系统提供了丰富的互操作性。 + - 凭借其出色的群集技术,BEA Web Logic Server拥有最高水平的可扩展 性和可用性。 + - BEA Web Logic Server既实现了网页群集,也实现了EJB组件 群集,而且不需要任何专门的硬件或操作系统支持。 + - 网页群集可以实现透明的复制、负载平衡以及表示内容容错,如Web购物车;组件群集则处理复杂的复制、负载平衡和EJB组件容错,以及状态对象(如EJB实体)的恢复。 + - 无论是网页群集,还是组件群集,对于电子商务解决方案所要求的可扩展性和可用性都是至关重要的。共享的客户机/服务器和数据库连接以及数据缓存和EJB都增强了性能表现。这是其它Web应用系统所不具备的。 + + +#### 应用服务器的作用是什么 + +- 应用服务器位于网络和数据库之间,应用程序服务器是为应用程序提供业务逻辑的。它是基于组件的,位于以服务器为中心的架构的中间件。 + + ![]({{site.baseurl}}/img-post/服务器-1.png) + + +- 应用服务器通过各种协议向客户端应用程序打开业务逻辑。它还可以包括计算机,web服务器或其他应用服务器上的图形用户界面。业务逻辑通过组件API。它还管理自己的资源以及执行安全性,事务处理,资源和连接池以及消息传递。 + +- 对于高端要求,应用服务器往往具有高可用性监控,集群,负载平衡,集成冗余和高性能分布式应用服务,并支持复杂的数据库访问。 + +- 应用程序服务器与Web服务器不同,因为前者通过多种协议处理向应用程序提供业务逻辑,而Web服务器响应并处理HTTP请求;它托管一个网站并存储静态内容,如图像,CSS,JavaScript和HTML页面。虽然Web服务器可能不支持事务或数据库连接,但它可能具有容错和可扩展性功能,如负载平衡,缓存和集群。 + +#### 应用服务器的优势 + +- 当需要与现有数据库和服务器(如Web服务器)集成时,使用应用程序服务器,可以通过启用集中式方法、来提供应用程序更新和升级来提供数据和代码的完整性。 +- 可伸缩性是使用应用服务器的另一个原因和好处。应用程序服务器可以与数据库连接。这意味着企业可以扩展Web服务器群,而不需要增加数据库连接的数量。 +- 使用应用服务器的另一个好处是安全,从网页到数据库的直接链接如果暴露,可导致SQL注入攻击基础架构。通过单独的数据访问层执行数据验证和/或显示业务逻辑,可以确保以Web表单输入的文本不被用作SQL调用。通过集中身份验证过程以及数据访问管理,还可以提高安全性。 +- 应用服务器还可以通过对网络流量进行限制,来提高对性能要求高的应用程序的性能。 + +#### 应用程序服务器与Web服务器的不同 + +- 应用程序服务器与Web服务器不同,因为前者通过多种协议处理向应用程序提供业务逻辑,而Web服务器响应并处理HTTP请求;它托管一个网站并存储静态内容,如图像,CSS,JavaScript和HTML页面。 +- 虽然Web服务器可能不支持事务或数据库连接,但它可能具有容错和可扩展性功能,如负载平衡,缓存和集群。 +- 当 Web 浏览器成为主要的客户端时,应用服务器和 Web 服务器之间的界限变得模糊。 +- 大多数 Web 服务器都有支持动态内容生成的脚本语言(ASP、JSP、PHP等)插件。例如,如果我们将 .NET 插件添加到 IIS 环境中,或者把PHP插件添加到Apache中,我们就可以在 Web 服务器端为客户端提供动态内容了。 +- 由于技术的重叠,最流行的服务器通常是这两种类型的混合。从而可以确保最佳系统速度和用户体验。 + +#### 应用程序服务器与数据库服务器的不同点 + +- 应用程序服务器与数据库服务器不同,因为该服务器执行诸如数据分析,存储,数据处理,归档以及其他数据管理相关任务之类的任务。 +- 数据库服务器使用诸如ODBC,JDBC等协议。他们还将托管数据库,如Oracle,SQL Server,MySQL等。 +- 相比而言,数据库服务器的处理器性能要求比较高,因为其要进行频繁的操作,内存要求大,加快数据存取速度。 +- 应用服务器相对而言要求低一些,如果是FTP服务器的话网卡的速率要求要高,起码是千兆的,网页服务器对于网卡的速率也同样有较高的要求,但对于处理器性能要求就不那么高了。 + +# WEB服务器 + +- WEB服务器负责响应来自用户端比如浏览器的请求,并向客户端返回静态资源的网页,比如图片,视频,网盘上分享的各种文件下载等。Web 服务器只处理静态的文件而不处理动态内容,仅接受和完成HTTP超文本传输协议的请求。 +- 目前流行的用来搭建Web服务可选软件有Apache,Nginx及微软的IIS等。 + +#### WEB 服务器种类 +- Apache服务器 + - Apache是世界使用排名第一的Web服务器。它可以运行在几乎所有广泛使用的计算机平台上。源于NCSAhttpd服务器,经过多次修改,成为世界上最流行的Web服务器软件之一。Apache取自“a patchy server”的读音,意思是充满补丁的服务器,Apache有多种产品,可以支持SSL技术,支持多个虚拟主机。Apache是以进程为基础的结构,进程要比线程消耗更多的系统开支,不太适合于多处理器环境,因此,在一个Apache Web站点扩容时,通常是增加服务器或扩充群集节点而不是增加处理器。 + - Apache web服务器软件拥有以下特性: + - 支持最新的HTTP/1.1通信协议 + - 拥有简单而强有力的基于文件的配置过程 + - 支持通用网关接口 + - 支持基于IP和基于域名的虚拟主机 + - 支持多种方式的HTTP认证 + - 集成Perl处理模块 + - 集成代理服务器模块 + - 支持实时监视服务器状态和定制服务器日志 + - 支持服务器端包含指令(SSI) + - 支持安全Socket层(SSL) + - 提供用户会话过程的跟踪 + - 支持FastCGI +- IIS web服务器 + - IIS是Internet Information Services的缩写,是一个World Wide Web server。 + - Gopher server和FTP server全部包容在里面。 IIS意味着你能发布网页,并且有ASP(Active Server Pages)、JAVA、VBscript产生页面,有着一些扩展功能。 + - IIS支持一些有趣的东西,像有编辑环境的界面(FRONTPAGE)、有 全文检索功能的(INDEX SERVER)、有多媒体功能的(NET SHOW) 其次,IIS是随Windows NT Server 4.0一起提供的文件和应用程序服务器,是在Windows NT Server上建立Internet服务器的基本组件。它与Windows NT Server完全集成,允许使用Windows NT Server内置的安全性以及NTFS文件系统建立强大灵活的Internet/Intranet站点。IIS(Internet Information Server,互联网信息服务)是一种Web(网页)服务组件,其中包括Web服务器、FTP服务器、NNTP服务器和SMTP服务器,分别用于网页浏览、文件传输、新闻服务和邮件发送等方面,它使得在网络(包括互联网和局域网)上发布信息成了一件很容易的事。 + - IIS应用服务器有以下特性: + - IIS 6.0 与Windows Server 2003为网络应用服务器的管理提供了许多新的特性,包括实用性、可靠性、安全性与可扩展性。 + - IIS 6.0也增强了开发和国际化支持,Windows Server 2003和IIS 6.0为您提供了一整套最可靠、高效、连接的一体化网络应用解决方案。微软自带的产品,操作简单,下一步下一步就可以完成架设了。 + +#### WEB服务器与应用服务器的区别 + +- 严格意义上Web服务器只负责处理HTTP协议,只能发送静态页面的内容。 +- 而JSP,ASP,PHP等动态内容需要通过CGI、FastCGI、ISAPI等接口交给其他程序去处理。 +- 这个其他程序就是应用服务器。比如Web服务器包括Nginx,Apache,IIS等。而应用服务器包括WebLogic,JBoss等。 +- 应用服务器一般也支持HTTP协议,因此界限没这么清晰。 +- 但是应用服务器的HTTP协议部分仅仅是支持,一般不会做特别优化,所以很少有见Tomcat直接暴露给外面,而是和Nginx、Apache等配合,只让Tomcat处理JSP和Servlet部分。 \ No newline at end of file diff --git "a/_drafts/2022-10-01-\346\234\215\345\212\241\345\231\250\357\274\232\346\226\207\344\273\266\346\234\215\345\212\241\345\231\250\343\200\201\346\225\260\346\215\256\345\272\223\346\234\215\345\212\241\345\231\250\347\232\204\345\214\272\345\210\253.md" "b/_drafts/2022-10-01-\346\234\215\345\212\241\345\231\250\357\274\232\346\226\207\344\273\266\346\234\215\345\212\241\345\231\250\343\200\201\346\225\260\346\215\256\345\272\223\346\234\215\345\212\241\345\231\250\347\232\204\345\214\272\345\210\253.md" new file mode 100644 index 00000000000..e3fa66b44c4 --- /dev/null +++ "b/_drafts/2022-10-01-\346\234\215\345\212\241\345\231\250\357\274\232\346\226\207\344\273\266\346\234\215\345\212\241\345\231\250\343\200\201\346\225\260\346\215\256\345\272\223\346\234\215\345\212\241\345\231\250\347\232\204\345\214\272\345\210\253.md" @@ -0,0 +1,94 @@ +--- +layout: post +title: 服务器:文件服务器、数据库服务器的区别 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 硬件网络 +--- + + +# 文件服务器 + +### 文件服务器的作用 + +文件系统把数据组织成相互独立的数据文件,实现了记录内的结构性,但整体无结构;而数据库系统实现整体数据的结构化,这是数据库的主要特征之一,也是数据库系统与文件系统的本质区别。在文件系统中,数据冗余度大,浪费存储空间,容易造成数据的不一致;数据库系统中,数据是面向整个系统,数据可以被多个用户、多个应用共享使用,减少了数据冗余。 + +文件系统中的文件是为某一特定应用服务的,当要修改数据的逻辑结构时,必须修改应用程序,修改文件结构的定义,数据和程序之间缺乏独立性。数据库系统中,通过DBMS的两级映象实现了数据的物理独立性和逻辑独立性,把数据的定义从程序中分离出去,减少了应用程序的维护和修改。文件系统和数据库系统均可以长期保存数据,由数据管理软件管理数据,数据库系统是在文件系统基础上发展而来。 + +数据库系统主要管理数据库的存储、事务以及对数据库的操作。文件系统是操作系统管理文件和存储空间的子系统,主要是分配文件所占的簇、盘块或者建立FAT、管理空间空间等。一般来说数据库系统会调用文件系统来管理自己的数据文件,但也有些数据库系统能够自己管理数据文件,甚至在裸设备上。文件系统是操作系统必须的,而数据库系统只是数据库管理和应用所必需的。 + +### 文件服务器 VS 数据库服务器 + +#### 文件系统和数据库系统之间的区别 + +- 文件系统用文件将数据长期保存在外存上,数据库系统用数据库统一存储数据; +- 文件系统中的程序和数据有一定的联系,数据库系统中的程序和数据分离; +- 文件系统用操作系统中的存取方法对数据进行管理,数据库系统用DBMS统一管理和控制数据; +- 文件系统实现以文件为单位的数据共享,数据库系统实现以记录和字段为单位的数据共享。 + +#### 文件系统和数据库系统之间的联系 + +- 均为数据组织的管理技术; +- 均由数据管理软件管理数据,程序与数据之间用存取方法进行转换; +- 数据库系统是在文件系统的基础上发展而来的。 + + +# 数据库服务器 + +### 数据库服务器的作用 + +数据库其实本质上也是文件,使用数据库的主要目的是便于检索,图片和视频除了标题之类的文字信息外,本身内容中没有需要检索的数据,所以用数据库存储完全没有必要。 所以一般数据库中只会存储图片、视频的基本信息和路径,再通过路径找到文件。 + +在文件服务器数据库中,数据存放在文件之中,数据的各个用户直接从文件中取得他们所需的东西.当有修改发生时,应用程序打开文件并写入新数据.当需要显示现有数据时,应用程序打开文件并读取数据.如果一个数据库有20个不同的用户,那么所有20个用户均读取和写入这个相同的文件. + +在数据库服务器中,情况同文件服务器的情形相反。虽然数据仍然存放在文件中,但文件访问由一个统一的主程序控制。当一个应用程序需要利用现有数据时,这个应用程序向服务器发送一个请求。服务器查找相应的数据,并将这个数据发回到应用程序.当一个应用程序需要向数据库中写入新数据时,它将该数据发送到服务器,然后由服务器执行实际的写入操作。只有一个统一的程序对数据文件执行读取和写入操作. + +一般说来,面向单用户桌面的数据库(比如Acess\Foxpro)都是文件服务器,而面向部门、公司或企业用户的数据库(Oracle\sybase\sql)都是数据库服务器。 + +### 数据库服务器在大型环境中的优势 + +- 由于只有一个统一的程序读取和写入数据,所以破坏关键数据的意外修改或瘫痪的可能性更小。 + +- 这个统一的服务器程序可以充当一个监视所有客户的看门人,从而使安全政策的建立和执行变得更容易。 + +- 由于线路上只有请求流和结果流,所以客户/服务器数据库会比文件服务器数据库更有效的利用网络带宽。 + +- 由于只有所有的读取和写入都由一台统一的计算机来完成,所以升级这台计算机来提高数据库性能变得更容易。 + +- 客户/服务器数据库往往提供保护数据的特性,比如日志事务和磁盘或网络错误的恢复。严格地说,文件服务器数据库也会提供这些特性 + +### 如何选择数据库服务器 + +#### 高性能 + +确保所选服务器既能满足操作系统和业务处理的需求,又能满足一定时期内业务量的增长。通常,所需的服务器TpmC值可以根据经验公式来计算(Tpmc是用于测量计算机系统的事务处理能力的程序),然后可以比较由各种服务器制造商和TPC组织公布的TpmC值来选择相应的模型。同时,从服务器的市场价格/报价中去除计算出的TpmC值,得到每单位TpmC值的价格,然后选择性价比高的服务器。 + +结论:服务器处理器的性能非常关键。CPU的主频率应该很高,并且应该有一个大的缓存。 + +#### 可靠性 + +可靠性原则是所有设备和系统选择的首要考虑因素,特别是在具有大处理要求和长期运行的大型系统中。考虑到服务器系统的可靠性,不仅是服务器单个节点的可靠性或稳定性,还包括服务器与相关辅助系统(如网络系统、安全系统、远程打印系统等)之间连接的整体可靠性。必要时,关键服务器也应考虑集群技术,如双机热备份或集群并行访问技术,甚至可能的完全容错机器。 + +结论:服务器应该有冗余技术。同时,硬盘、网卡、内存、电源等设备应主要稳定耐用,性能次之。 + +#### 可扩展性 + +确保所选服务器具有出色的可扩展性原则。因为服务器是所有系统处理的核心,所以需要有大的数据吞吐率,包括:输入输出速率和网络通信速率。此外,服务器需要能够处理一定时间内业务发展带来的数据量,并且需要能够根据相应时间的业务发展需要进行自我升级,如:升级中央处理器型号、内存扩展、硬盘扩展、更换网卡、增加终端数量、磁盘阵列连接或集群系统与其他服务器并发访问集中数据等。这一切都要求所选服务器总体上具有良好的可扩展性。在大型计费系统的设计中,通用数据库和计费应用服务器将采用集群的方法来提高可靠性。根据数据量和投资考虑,连接的磁盘存储系统可以采用DAS、NAS或SAN等实施技术。 + +结论:服务器的输入输出应该是高的,否则当CPU和内存都是高性能时,就会出现瓶颈。此外,服务器具有更好的可扩展性,以满足未来企业发展的需要。 + +#### 安全 + +大多数服务器处理相关系统的核心数据,关键事务和重要数据存储在这些系统上并运行。这些交易和数据是所有者的重要资产,其安全性非常敏感。服务器的安全性与系统的整体安全性密切相关,如网络系统的安全性、数据加密、密码系统等。服务器本身,包括软件和硬件,应该从安全的角度来设计和考虑。在外部安全设施的帮助下,服务器应该确保自身的高安全性。结论:首先,服务器的材料应具有高硬度、高防护性等条件。第二,服务器的冷却系统及其对环境的适应性应该很强,以满足服务器在硬件上的安全性要求。 + +#### 可管理性 + +服务器不仅是整个系统的核心,也是节点的一部分。正如网络系统需要管理和维护一样,它也需要服务器的有效管理。这要求服务器的软件和硬件支持标准管理系统,尤其是其上的操作系统,并且还包括一些重要的系统组件。 + +结论:试着选择支持更多系统的服务器,因为服务器的系统越兼容,你就有越多的选择。 + +概要:首先,数据库服务器的性能要求很高,所以对CPU、内存、硬盘等都有很高的要求。第二,存储,存储应该有良好的稳定性,以满足长期运行的服务器在任何时候的读写操作没有错误。 \ No newline at end of file diff --git a/_layouts/keynote.html b/_layouts/keynote.html index 59f138127a2..2b086eec631 100644 --- a/_layouts/keynote.html +++ b/_layouts/keynote.html @@ -46,7 +46,7 @@

    {{ page.title }}

    {% comment %} if page.subtitle {% endcomment %}

    {{ page.subtitle }}

    {% comment %} endif {% endcomment %} - Posted by {% if page.author %}{{ page.author }}{% else %}{{ site.title }}{% endif %} on {{ page.date | date: "%B %-d, %Y" }} + diff --git a/_layouts/page.html b/_layouts/page.html index be771c09282..926d5f81c37 100644 --- a/_layouts/page.html +++ b/_layouts/page.html @@ -36,11 +36,11 @@

    {% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}

    -
    FEATURED TAGS
    +
    FEATURED TAGS
    {% for tag in site.tags %} {% if tag[1].size > {{site.featured-condition-size}} %} - + {{ tag[0] }} {% endif %} @@ -85,11 +85,11 @@
    MORE SITES
    {% if site.featured-tags %}
    -
    FEATURED TAGS
    +
    FEATURED TAGS
    {% for tag in site.tags %} {% if tag[1].size > {{site.featured-condition-size}} %} - + {{ tag[0] }} {% endif %} @@ -100,10 +100,10 @@
    FEATURED TAGS
    -
    ABOUT ME
    +
    ABOUT ME
    {% if site.sidebar-avatar %} - + {% endif %} diff --git a/_layouts/post.html b/_layouts/post.html index 100ca825ff4..b79d8f2f8f2 100644 --- a/_layouts/post.html +++ b/_layouts/post.html @@ -30,7 +30,7 @@
    {% for tag in page.tags %} - {{ tag }} + {{ tag }} {% endfor %}

    {{ page.title }}

    @@ -40,7 +40,7 @@

    {{ page.title }}

    {% comment %} if page.subtitle {% endcomment %}

    {{ page.subtitle }}

    {% comment %} endif {% endcomment %} - Posted by {% if page.author %}{{ page.author }}{% else %}{{ site.title }}{% endif %} on {{ page.date | date: "%B %-d, %Y" }} +
    @@ -143,11 +143,11 @@
    {% if site.featured-tags %}
    -
    FEATURED TAGS
    +
    FEATURED TAGS
    {% for tag in site.tags %} {% if tag[1].size > {{site.featured-condition-size}} %} - + {{ tag[0] }} {% endif %} diff --git "a/_posts/2018-12-01-\344\270\200\351\227\250\346\226\260\347\232\204\345\233\275\351\231\205\350\257\255\345\215\263\345\260\206\350\257\236\347\224\237.md" "b/_posts/2018-12-01-\344\270\200\351\227\250\346\226\260\347\232\204\345\233\275\351\231\205\350\257\255\345\215\263\345\260\206\350\257\236\347\224\237.md" deleted file mode 100644 index 597dd0e3698..00000000000 --- "a/_posts/2018-12-01-\344\270\200\351\227\250\346\226\260\347\232\204\345\233\275\351\231\205\350\257\255\345\215\263\345\260\206\350\257\236\347\224\237.md" +++ /dev/null @@ -1,16 +0,0 @@ ---- -layout: post -title: 一门新的国际语即将诞生 -subtitle: 三叶草国际语发布在即 -date: 2018-12-01 -author: Henri Jambo -header-img: img/the-first.png -catalog: false -tags: - - 新闻故事 ---- - - -融合三大语言,瞬间掌握语音语法,秒速学会猜词会意。 -挥一挥手,让背单词的痛苦离我们而去。 -敬请期待! \ No newline at end of file diff --git "a/_posts/2018-12-10-\350\203\214\345\215\225\350\257\215\346\230\257\344\270\252\350\200\201\345\244\247\351\232\276\351\227\256\351\242\230.md" "b/_posts/2018-12-10-\350\203\214\345\215\225\350\257\215\346\230\257\344\270\252\350\200\201\345\244\247\351\232\276\351\227\256\351\242\230.md" deleted file mode 100644 index d72dee575df..00000000000 --- "a/_posts/2018-12-10-\350\203\214\345\215\225\350\257\215\346\230\257\344\270\252\350\200\201\345\244\247\351\232\276\351\227\256\351\242\230.md" +++ /dev/null @@ -1,26 +0,0 @@ ---- -layout: post -title: 背单词是个老大难问题 -subtitle: 还在苦苦背单词吗? -date: 2018-12-10 -author: Henri Jambo -header-img: img/post-bg-recitewords.jpg -catalog: false -tags: - - 随笔杂谈 ---- - - -人造国际语发展到今天,读音和语法方面的简化,都已经做得足够好了。 - -但在词汇方面,由于存在基本词的记忆量和复合词的计算量之间的平衡问题,仍然没有找到最佳方案。 - -因此,无论是元老级的世界语、IDO语,还是新近的逻辑语、Interlingua等,尤其对东方人来说,记单词仍然是一个老大难的问题。 - -就像那些背单词软件所说的那样:举头望明月,低头背单词;少壮不努力,老大背单词;洛阳亲友如相问,就说我在背单词...... - -背单词是学习外语的必经之路,人造国际语虽然少了很多单词,但作为”低频接触“的语言而言,两三千单词仍然是忘了背,背了忘。 - -这很可能是阻碍国际语发展的一个重要因素。 - -三叶语的发明,将致力于解决这一问题。 \ No newline at end of file diff --git "a/_posts/2018-12-15-\344\272\272\351\200\240\345\233\275\351\231\205\350\257\255\350\203\214\345\215\225\350\257\215\344\273\215\346\230\257\346\214\245\344\270\215\345\216\273\347\232\204\347\227\233.md" "b/_posts/2018-12-15-\344\272\272\351\200\240\345\233\275\351\231\205\350\257\255\350\203\214\345\215\225\350\257\215\344\273\215\346\230\257\346\214\245\344\270\215\345\216\273\347\232\204\347\227\233.md" deleted file mode 100644 index da55ba8b5f6..00000000000 --- "a/_posts/2018-12-15-\344\272\272\351\200\240\345\233\275\351\231\205\350\257\255\350\203\214\345\215\225\350\257\215\344\273\215\346\230\257\346\214\245\344\270\215\345\216\273\347\232\204\347\227\233.md" +++ /dev/null @@ -1,122 +0,0 @@ ---- -layout: post -title: 人造国际语,背单词仍是挥不去的痛 -subtitle: 国际语不成功的关键原因是什么? -date: 2018-12-15 -author: Henri Jambo -header-img: img/post-bg-recitewords.jpg -catalog: false -tags: - - 随笔杂谈 ---- - -#### 一 - -国际通用语一百多年的发展历史,许多人出于对迄今为止仍然是最成功的人工语言——世界语(Esperanto)的种种不满意,自立门户,自行造语。或改造,或自创,方案繁多。 - -造语离不开其铁三角:语音、语法、词汇。(唔,还有造字符……) - -通过不计其数方案的探索,应该说对语音、语法的看法已经大体趋同。 - -语音方面,多数原则达成一致。例如,一字一音的原则如果不要求刻板执行的话,大家都是认可的。元音嘛基本上就是aoeiu五元组,辅音嘛大部分跟英语一样,发音嘛就基本按简单清晰、分隔度大、对比度大的原则来,比如倾向于送气不送气对立而不是清浊对立。 - -语法方面(包括词法),方向上也基本达成一致。比如简化性、数、位的变化,减少语序的自由性,等等。如果把中文的“了”、“过”等也看成是后缀的话,大家在“分析还是曲折”的问题上争议其实也并不那么大。 - -唯有词汇,即取词(或造词)的方法,百花齐放,百家争鸣。很难达成一致。 -分歧大说明问题大。 - -#### 二 - -在电影《Downsizing》中,女主人公——一个住在贫民窟的来自越南的偷渡客——操着“流利”的但语法混乱、常常没有谓语的英语,把男主人公指使得团团转,还赢得了他的爱情。你可以注意到她的词汇量是不错的,所以日常沟通甚至感情交流都没有问题。 - -词汇是核心。 - -“You doctor?”,谁都明白这句话的意思。 - -然而,人工语言最有待解决的问题,恰恰是词汇的问题。 - -#### 三 - -词汇的难点是什么? - -记忆力与遗忘曲线! - -如果是完全与母语无关的外语,每天花20分钟到30分钟学习和复习,每天新增10~15个单词的话,大概500~1000个单词可以很快记住[1]。再多,在边际效应的作用下记忆力就迅速下降了,并且还会混淆。 - -记住了也还会慢慢遗忘。若是在国外呆久了,连母语都会慢慢地变得不熟练。 - -作为人造语言,我们的目标是不超过150小时的学习时间基本掌握该语言。如果有500~1000个单词,如果每天花20分钟到30分钟来学习,一年正好!如果每天花6小时到8小时来学习,一个月正好! - -这就能在某种程度上解释,为什么很多人造语言方案都有意无意地“宣称”把基本词汇量控制在500到1000个左右,虽然不一定是真的[2]。但即使稍微再多一些,问题似乎也不大。就算要背两三千个单词,应该也还行吧? - -#### 四 - -去年年底不太忙,于是自学了一门著名的人工语言。真的很简单,两三个小时就算基本掌握了读音、语法、词法。然后花点儿时间把代词、介词、连词、数词好好学了一下,最基本的动词学几个,就可以借助词典读一些简单的文字,甚至可以写些简单的句子跟学友交流了。 -哈哈,人工语言,简单!就是好! - -然后就是要背单词。学什么语言都要过这个坎儿啊。 - -三个月,每天花将近一个小时,在Memrise上背了600多个单词和短语(我比较笨,没办法)。嗯,总算基本上都记住了!跟学友交流更容易了。真好! - -接下来半年,有事忙别的去了。其间只是时不时地稍微看一下。 - -半年后检验,能记住的不到100个!武功全废! - -这就是人工语言最尴尬的地方——没有实际环境(除非你专门做这个事),学了很快就忘,然后即使偶尔有机会使用,对不起,用不起来了。 - -但是,语音和语法我没忘。 - -忘的是单词。 - -#### 五 - -可能这就是我这种没有外语天资的人背单词的困境。 - -我的英语也是这样。学了几十年,也一直在胡乱地用,到了七八千的词汇量就再也上不去。而每次下决心背单词,不管用什么工具——买本单词书,还是某种特殊的方法,或者百词斩之类的——总是背千把新单词就背不下去了。 - -然后过一段时间再去检验,又忘记了十之八九。能记住的,都是那些接下来碰巧用到过几次的,为数不多。所以我的词汇量就永远是这样的心电图:____/\\\____/\\\____/\\\____,证明我还活着。每当看到那些“词汇量过万的狠招”之类的文章,我的内心是崩溃的。 - -只能说,不用(无用),词汇就无法增长。关键是使用频度,笨蛋! - -但人工语言的窘境恰恰就是无用。无用,人少;人少,无用。死循环。因此,人工语言必须要有更为严格的“容易记、不易忘”的标准。我觉得这个标准差不多应该是这样的: - -* 100个以内的常用词,随便造。这个体量的词汇再笨的人死记硬背也是没问题的。 -* 100个以上的基本词,不仅要看几遍就能记住,而关键是一旦通过几天到十来天的验证仍然是记住的情况下,就能做到半年到一年内即使不接触也不会忘掉多少,即使不算熟悉,想一想也要能大体想起来。 -* 基本词汇不超过3000(包括词根词缀),复合词要能够通过它们比较直接的得到结果,不能绕太多。比如像“football=foot+ball=足+球=足球”那样直接了当最好,“standard=stand+(h)ard=站得稳=军旗,标准”稍微绕一下也可以,但像“edifice=edi+fice=燃烧+制作=火炉=居住地=大厦”就绕得太远了,实在不好记。 - -如此这般,才能保证即使很低频度使用,但下一次你有机会用到的时候,还勉强能用,还能增长,内心才不会有那种忘得一干二净的绝望。要知道那种绝望的次数多了,下一步必然就是放弃。来来回回,进进出出,社群难以扩大。 - -#### 六 - -怎样的词汇设计,才能做到“容易记、不易忘”呢? - -少,是一个方向,是多数人工语言的做法,首先追求“容易记”。然而就算一两千的词汇,在很少使用、纯凭记忆的情况下,仍然是很大的挑战。而想再少一些也是基本不可能,因为太少的话,复合词就要害死人了,或者句子就太长了[3]。比如“枫树”、“蟋蟀”、“菊花”这种级别的词,你是要放进基本词汇呢,还是复合词汇呢?这种频度的词如果都放进基本词汇,一两千肯定搞不定,五百八百想都别想了。如果放进复合词汇,要怎么放?“秋天红的树”?“晚上叫的虫”?“由很多花瓣排成一圈的小小的花”?(大大的是向日葵^_^) - -逻辑,是另一个方向,或趣向,也许能做到“不易忘”。就像圆周率3.14159不好背,但“山巅一寺一壶酒,尔乐苦煞我……” 则可以让你轻易背到二十多位,因为它含有你熟知且有逻辑的东西。然而,高度逻辑化的构词法,支持者似乎并不多[4]。所有的复合词汇其实都是一种逻辑词汇,这个大家都是愿意接受的;但如果基本词汇也“讲逻辑”,大家反而害怕了。这可能有点儿像法语里面数数数到70就要开始做算数题了一样,比较讨厌。 - -还有什么方法? - -#### 七 - -做到“易记不忘”还有一个根本的难题是:东西方人的语言背景差异极大,因此对“易记不忘”的标准完全不一样。 - -假若你要造“美丽”一词,你用bela也好,bele也好,bell也好,对西方人来说,都觉得好记。但中国人觉得不好记啊,没有逻辑啊,几个几十个还好,一旦有几百几千这样的词,就要很花功夫了,就变成一门功课了——而且是一门基本没有实用价值的功课喔。好玩的东西要轻松,变成负担就不好玩了。 - -反过来,你若要将就中国人,就算中国人都同意使用拉丁字母好咧,叫meili也好,meli也好,mely也好,这下西方人变成了丈二和尚。 -绝望吧?是不是无解了啊?[5] - -姑且把这叫做“造词困境”吧。无关乎文化中立,只关乎是否能做到让东西方大多数人都可以在低频使用的情况下“易记不忘”。 - -只有解决了“造词困境”,做到所有人都“易记不忘”,才能让“无用的人造国际语”的使用者慢慢多起来,然后慢慢变得有用起来,最终形成正循环。 - -这就是三叶语要做的事情! - -
    - -**注:** - -1. 感兴趣的读者可以使用艾宾浩斯记忆曲线进行推算,看看每天记10~15个单词的话,300天能记住多少个单词。 -2. 例如,世界语开始时只有900词根,但现在实际上也有2000到3000个常用词根。简语有1000到2000基本词汇。格罗沙语有1000左右的词根。看起来通常情况下底线至少是1000。 -3. 据说世界上所有的语言可以归结为62个单词。若如此,那背单词就绝对不是问题了。但那就意味着“我要吃饭”这句话可能需要四五句话,几十个单词才能勉强说清楚。道本语做到只有118个单词,非常了不起,不过基本也就只能表达“我要吃饭”、“他住城里”这一类最简单的日常生活用语,而颜色都得自己“调”,数字都得自己“算”,基本上又走到“逻辑派”了,而且还是要极具想象力的“逻辑”。地球村语只有400来个根词,但大量常用词汇也需要靠充满想象力的逻辑思维来派生,与道本语有同工异曲之妙。 -4. 可以到知乎“国际语”专栏中了解一下Sogo实验语的情况。Sogo相当于只有二三十个“基础单词”——即字母,每个字母都有含义,由这些字母的含义经由逻辑构成“复合词汇”。 -5. 逻辑语和地球村语似乎有意融入汉语元素,不过这并不是它们的核心追求。由于缺乏较为严格的引入规则,反向推导很困难,所以事实上对单词记忆的帮助也有很限,只能是偶尔遇到了便有一种亲切感:啊,这个词跟中文有点儿像。另外,大同语也考虑了一些中文的音素,但显然也未把重点放在“易记不忘”的词汇方面。 \ No newline at end of file diff --git "a/_posts/2018-12-20-\344\270\211\345\217\266\350\257\255\351\207\214\350\212\261\347\224\237\344\270\272\344\275\225\345\217\253pounuto.md" "b/_posts/2018-12-20-\344\270\211\345\217\266\350\257\255\351\207\214\350\212\261\347\224\237\344\270\272\344\275\225\345\217\253pounuto.md" deleted file mode 100644 index 2e5f8038362..00000000000 --- "a/_posts/2018-12-20-\344\270\211\345\217\266\350\257\255\351\207\214\350\212\261\347\224\237\344\270\272\344\275\225\345\217\253pounuto.md" +++ /dev/null @@ -1,19 +0,0 @@ ---- -layout: post -title: 花生为何造词为pounuto呢? -subtitle: 论组合词的构造 -date: 2018-12-20 -author: Henri Jambo -header-img: img/post-bg-alibaba.jpg -catalog: false -tags: - - 设计点滴 ---- - -花生在英文里叫peanut,是一个复合词,意思是“像豌豆一样的坚果”。 - -花生这个词,在中文里也是一个复合词,并且这个复合的意思还比较难理解。 - -所以在三叶语里我们选择英文的复合方式,以便于理解记忆。 - -具体来讲,在三叶语里,豌豆按照组词规则形成的词叫pou,词韵是“豆dou”的韵母;坚果叫nuto,词韵来自“果guo”。所以,花生还是理解为“像豌豆一样的坚果”,那么就叫pounuto咯。 diff --git "a/_posts/2018-12-30-\344\270\211\345\217\266\350\257\255\347\256\200\344\273\213.md" "b/_posts/2018-12-30-\344\270\211\345\217\266\350\257\255\347\256\200\344\273\213.md" deleted file mode 100644 index 1a1fc57b214..00000000000 --- "a/_posts/2018-12-30-\344\270\211\345\217\266\350\257\255\347\256\200\344\273\213.md" +++ /dev/null @@ -1,103 +0,0 @@ ---- -layout: post -title: 三叶语简介 -subtitle: 三分钟了解三叶语的核心内容 -date: 2018-12-10 -author: Henri Jambo -header-img: img/post-bg-miui6.jpg -catalog: true -tags: - - 学习资料 ---- - -三叶草国际语(简称三叶语)是一种人造语言。它完美融合了中西方语言,与著名的世界语(Esperanto)相比,更加注重东方人的语言习惯,因而有希望纳入更多东方人,成为真正的“世界语”。 - -首先,三叶语独创**“词基-词韵-词尾”**的单词结构。其中词基来自英语/法语/西班牙语为主的西方语言,词韵来自汉语拼音,词尾代表词性词类等功能。因此,无论讲英语/法语/西班牙语的人,还是讲中文的人,都能从单词中找到相应的提示部分,便于猜词和记词。而这几种语言的人群覆盖范围很广,所以对世界上大多数人来说,三叶语的词汇都很容易掌握。![三叶语单词结构图]({{site.baseurl}}/img-post/2018-12-30-overview.png)例如,“树”叫tru,“火”叫firo,“雾”叫fogu,每个单词都含有词韵——也就是汉语拼音的韵母部分,对中国人来说就很容易记忆了;即使没有完全记住,在有上下文的情况下也很容易猜对词意,所以学起来完全不难。 - -除了词汇设计的特色之外,极易读的语音(与拼音很相似)和极易用的语法(核心只需记六个字),让三叶语基本能做到即学即会。 - - -#### 音素表 - -音素是语言中最小的语音单位。三叶语的音素尽量取中英法三种语言的交集,因此比它们各自的音素都要少很多,且留下的读音都比较容易。例如,去掉了英语中[æ][ɜ][ʌ]等对中国人来说特别容易混淆的读音,但保留了中文和法语中都有的[y](迂)这个清晰的读音。 - -这样,三叶语选取了一共37个音素:18个元音(包括双元音、鼻元音)和19个辅音。 - -元音主要来自汉语拼音,共18个(方括号中是国际音标): - -|:-------:|:-------:|:-------:|:-------:|:------:| -| a [a] | e [e] | o [o] | -| i [i] | u [u] | y [y] | -| an [an] | en [en] | on [on] | -| in [in] | un [un] | yn [yn] | am [aŋ] | -| ai [ai] | ei [ei] | au [au] | ou [əu] | eu [ə] | - -辅音主要来自英语,但去掉晦涩难读的音,共19个(方括号中是国际音标): - -|:-------:|:-------:|:-------:|:-------:|:------:| -| b [b] | p [p] | m [m] | f [f] | -| d [d] | t [t] | l [l] | n [n] | -| g [g] | k [k] | h [h] | -| j [dʒ] | ch [tʃ] | sh [ʃ] | -| z [z] | c [ts] | s [s] | r [ɹ] | v [v] | - -是不是有很亲切熟悉的感觉?是的,三叶语的音素表跟汉语拼音表非常相似。需要特别注意的只是eu=[ə],am=[aŋ],以及z和r的发音更接近英语。 - -另外,三叶语遵循广义的“一字一音”原则,基本做到“看到能读,听到能写”。 - -#### 时态语态 - -体现在谓语动词上的时态、语态、体貌是语法的核心内容,也是最难的内容。很多西方语言比如法语,谓语动词的变化非常之多,学习困难。虽然英语有所简化,但对中国人来说还是太复杂。 - -但在三叶语中,谓语动词只有一种变化形式——加-ed成为过去式,相当于中文的“了”,用于表示过去发生的事,但不强调是否完成。而当前的事使用动词原形,但不强调是否正在进行。 - -归纳起来,动词时态语态的总和公式为: - -**(将vil) (不ba) (已deja) (在jen) (被ez/ed) 动词(-ed)。** - -简化为六字口诀:将-不-已-在-被-动,谐音为“江湖已在波动”,即刻记牢。 - -上述公式应从右往左按顺序考虑: - -1. 最简单的形式是动词原形,表示一般状况或当前事件。 -2. 动词加 -ed 成为过去式,表示过去的状况或事件。 -3. 加系词 ez(是/被),表达被动语态。此时动词必须用过去式,但不表示过去事件;用系动词ez的过去式(ed)表示过去的事件。 -4. 加副词 jen (正在),强调事件正在发生。 -5. 加副词 deja (已经),强调事件已经发生。此时动词必须用过去式。 -6. 加副词 ba (不/没),表示否定,即事件未曾发生或不会发生。 -7. 加副词 vil (将/会),表示将来的状况或事件,即预计会发生。 - -以上2~6每项内容都是可选,需要时按照上述固定顺序增加即可。固定的六字顺序让学习和记忆无比简单。 - - -#### 基本词汇规则 - -名词、动词、形容词、副词都有特殊的词尾,因此可以通过词尾判断词性。同时每个词都含有词韵(拼音韵母),可以帮助你猜测和记忆单词的意思。 - -**名词以词韵结尾,全部为元音**。词韵来自对应中文关键字的拼音韵母;扣除词韵的部分则为词基,来自对应的英语/法语(西班牙语)单词。词韵帮助讲中文的人猜词记词,词基帮助懂英语/法语的人猜词记词。例如,melua (瓜; 甜瓜) 一词,词韵是-ua,来自“瓜”的拼音韵母;词基是mel-,来自英语和法语同形的单词melon。如果你不熟悉这个词或者有些忘了,相信下次遇到时词韵-ua可以给你很好的提示。 - -**动词的标准词尾为-t,倒数第一个元音组为词韵**。扣除词韵和词尾后,剩下的部分为词基。同样,词韵来自拼音韵母,词基来自英语/法语。动词过去式则在原形后面加-ed。例如 marchout (走,走路),词韵为-ou,来自“走”的韵母;词基为march-,来自英语单词march和法语单词marcher。而其过去式为marchouted。 - -**形容词的标准词尾为-e,此外的倒数第一个元音组为词韵**。扣除词尾和词韵后,剩下的部分为词基。例如:gronde (宏伟的),词韵为-on,来自“宏”的韵母;词基是不连续的,为gr..d-,来自英语和法语单词grand。 - -动词和形容词的词基都有可能是不连续的。 - -另外,形容词添加词尾-li得到相应的副词,即副词的词尾为-eli。 - - -#### 特点总结 - -归纳一下,三叶语“易学易记不易忘”的特点来源于其在语音、语法和词汇方面的巧妙设计: - -* **语音方面**,舍弃了那些繁琐和易混淆的发音,保留最公认的字母和发音,并做到一字一音。 -* **语法方面**,核心关键部分只需要记住“将不已在被动”六字口诀即可掌握。 -* **词汇方面**,融合英法中三语词源,习语者总能找到自己的助记符,背单词不再难。 - - -欲了解更多细节,请前往[三叶语概述]({{site.baseurl}}/2019/01/11/三叶语概述)或更多[学习资料]({{site.baseurl}}/1-resources)。 - ------- - -注:p/t/k的发音实为送气音[ph]/[th]/[kh],h为上标。为简化起见,一律不标注h。而b/d/g发音实为不送气音[p]/[t]/[k],类似英语和汉语的b/d/g发音,但发法语和德语那样的浊音[b]/[d]/[g]也可以。 - -(修订于2019-09-22) diff --git "a/_posts/2019-01-11-\344\270\211\345\217\266\350\257\255\346\246\202\350\277\260.md" "b/_posts/2019-01-11-\344\270\211\345\217\266\350\257\255\346\246\202\350\277\260.md" deleted file mode 100644 index 016f463b6e1..00000000000 --- "a/_posts/2019-01-11-\344\270\211\345\217\266\350\257\255\346\246\202\350\277\260.md" +++ /dev/null @@ -1,345 +0,0 @@ ---- -layout: post -title: 三叶语概述 -subtitle: 三叶语的主要规则都在这里了 -date: 2019-01-11 -author: Henri Jambo -header-img: img/post-bg-coffee.jpg -catalog: true -tags: - - 学习资料 ---- - -### ☘ 引言:让背单词不再难 - -人造国际语发展到今天,读音和语法方面的简化,都已经做得非常好了。但在词汇方面,由于存在基本词的记忆量和复合词的计算量之间的平衡问题,仍然没有找到最佳方案。 - -因此,无论是元老级的世界语、IDO语,还是新近的逻辑语、Interlingua等,对习语者尤其是东方习语者来说,记单词仍然是一个老大难的问题。这,很可能是阻碍国际语发展的一个重要因素。 - -考虑一下中文、英语、法语这三种世界上使用最为广泛的语言。中文覆盖的人口数量最多,而英语和法语覆盖的地区最广。如果把第二官方语言考虑在内,三种语言合计可以覆盖全世界超过60%的国家和地区;如果把主要外语也考虑在内,可以覆盖35亿人口,占全世界将近一半。 - -因此,如果有一种人工语言不仅简化读音和语法,而且在词汇方面能够很好地融入这三种语言的元素,使得每种语言的占比都在六成以上,那么全世界大部分人在学习这种人工语言时,都能做到易记不易忘。这将更有效地扩大国际语人群,更好地推动国际语发展。 - -三叶草国际语(Klovien Interlingua,简称三叶语,Klovien)正是为此而生的。其基本词构成元素的词源覆盖设计目标为: - -中文:99% -英语:80% -法语:70% -在三叶语中,几乎每一个基本词都会包含一个中文词韵,所以说中文覆盖率达到99%并不夸张;同时,基本词的来源全部为英语或法语,且大部分来自英法双语形似的单词,所以英法分别达到七八成的占比也能做到。三大语言各自都有如此高的覆盖比例,让全世界一半人口都可以不费力地直接掌握三叶语。 - -最重要的是,三叶语构词在引入英语、法语和中文时的规则是极其清晰的,因而学习者可以轻易分辨出来单词中哪些成分属于英语/法语,哪些属于中文,从而可以结合自己的母语或已掌握的外语知识反推单词含义,极大地帮助学习。 - -那么三叶语是怎样融合东西方三种语言的呢? - -首先,在英语/法语中,辅音比元音重要,所以缩写一般为辅音,例如copy缩写为cp。另一方面,由于汉语的韵母非常丰富,所以相对而言元音的地位更为重要。这就给我们提供了一个特别的构词方法:采用汉语的韵母替代英语/法语中的部分元音。这样,就能做到基本不增加音节数量(即基本不降低表达效率)的情况下,在单词中同时包含中西方语言的元素。 - -举个例子,三叶语设计了一个单词叫lunue。其规则告诉我们,这是一个名词,前半部分lun-来自英语,后半部分-ue来自中文。假如有语境暗示我们它是一种天体,那么无论你母语是英文还是中文,是不是立即可以猜到这个词是“月亮”呢?再如,tru就是“树”,shie就是“鞋子”,是不是很好猜也很好玩呢? - -虽然好玩也是好学的一部分,但“猜字游戏”绝不仅仅是为了好玩,而是能更好地帮助学习: - -* 单词容易记,不易忘。 -* 即使句子中有不少生词,也很容易读懂。 - -这两条大大降低了学习和持续学习的门槛,在低频使用的环境下,对于帮助人们坚持国际语的学习、帮助国际语积累使用人群非常重要! - -三叶语的特点归结为七个字:易学易记不易忘。她像现实中的三叶草一样,简单而又易辨认。希望她不仅像三叶草一样携带好运祝福,更像三叶草一样有顽强的生命力,在世界各个角落生长蔓延开来。 - -### ☘ 语音:去繁留简,一字一音 - -三叶语的字母表采用英法中三语中共同的26字母表,去掉了法语和汉语拼音中的变音字母。元音字母的名称和其读音一致,而辅音字母的名称统一为字母读音之前或之后加[e](除了字母h和q略微特殊)。这样处理后,比较类似于法语的字母表,但更加规范。如下表所示: - -但实际上三叶语并不使用q、w、x这三个字母,保留它们仅用于特殊的外来语。这几个字母容易引起混淆,去掉它们可以让生活更简单。 - -三叶语的音素(最小读音单位)在设计上结合了英法中三种语言,拼写的表达上更接近法语和中文,如字母 i 表示读 [i] 而不是[ai]或[ɪ];但发声方法上更接近英语和中文,如p/t/k的发音为送气塞音,b/d/g的发音对比更清晰。这样的做法乃优势互补,让拼写与读音更容易。 - -三叶语用到的音素列举如下: - -* 辅音19个:b, p, v, f; d, t, l, n; g, k, h, m; z, c, s, r; j, ch, sh。基本采用英语的读音,但注意三点:一是c念“呲”[ts]以区别于s[s],若需发硬音,要改写为k;二是g永远发硬音“歌”[g],若需发软音,要改为j[dʒ];三是r[r]的发音按中文而不是英文念,以普通话“尔”的尾音为标准读音,同时也接受齿根颤音。 -* 单元音6个:a, e, o, i, u, y。元音偏法语和中文的发音。注意e读[e],接近汉语“耶”的韵母而不是“喝”的韵母。三叶语中保留了法语和中文中的“迂”[y]音,但没有英文中的[ju]音,[ju]转换为u,念“乌”[u]。 -* 双元音5个:ai, ei, au, ou, eu。读音上偏向法语和中文,但拼写上有调整,如au对应拼音ao,eu对应拼音e [ə](其实不是双元音,是双字母表示的单元音)。分别念“唉”、“诶”、“凹”、“欧”、“呃”。 -* 鼻元音7个:an, en, on, in, un, yn, am。其中am为后鼻音,相当于拼音ang,而en/on/in/un不区分前后鼻音。分别接近念“安”、“嗯”、“ong”、“音”、“温”、“盎”。 - -一共37个音素(19个辅音,18个元音),拼写与读音一一对应,亦即符合“一字一音”的原则,完全做到“所听所写,所见所读”。每个音素发音清晰响亮,且各音素之间的间隔度大、对比度大,因此对不同口音的容忍度大。比如,把e读成[ɛ],也没问题。全部音素的详细规则参见附录一。 - -### ☘ 词汇:西为基,中为韵,易记不易忘 - -三叶语的单词分为四大类:基本词、辅助词、复合词和聚类词。 - -**基本词**包括基本的名词、动词和形容词,由英语/法语与汉语按简单严谨的规则融合而成。来自英语/法语的成分叫词基,来自中文的成分叫词韵,表示词性的成分叫词尾。基本词都有确定的词性词尾,且词源清晰,词韵和词基分别给予中西方语言背景的人相应的语义提示,并且总数不超过2000个,学习和记忆很容易。 - -**辅助词**是用来补充构建语句的单词,包括代词、介词、连词、疑问词、数词等,以及一些语法副词。辅助词借鉴中英法三语的词汇,直接规定构成,虽然其构词规律不是很强,但词形简单,且总数不超过100个(不包括复合型和短语介词),记忆不会困难。 - -**复合词**用于扩充词汇。原则上,基本词和辅助词已经可以表达一门语言所需要表达的所有内容,但是在表达复杂意思时,需要的单词数可能会太多。适当引入复合词可以减少句子的复杂程度。复合词可以不断扩充,但三叶语的复合词构词规则简单而严谨,无需专门记忆。 - -**聚类词**是三叶语的特色设计。在三叶语中,定义了花鸟、兽、鱼、虫、草、树、化学元素、拟声词等若干类聚类词,每个类别对应一个特定的词尾。这些单词多数并不常用,所以一般不用记,能从词尾判别类型即可。这种方式大大降低了词汇记忆的负担! - -无论何种类型的词,构词时如果来源成分存在与三叶语的语音规则不一致的地方,会进行相应的调整。如拼音中的uo要改为o,英语中的oo要改成u,等等,以避免拼读不一致的现象。具体的规整化规则见附录二,大致了解这些规则有助于反推词义,但这些规则不是百分百被遵守的,少量词汇根据词形的需要会略加调整。 - -#### 1、基本词 - -**名词** - -名词的标准词尾是-o,但大多数情况下并不使用此标准词尾,只是在词性转换时或某些特殊情况下需要添加此标准词尾。由于设计巧妙,三叶语多样化的名词结尾,并不妨碍其与动词和形容词的显著区分,反而还可以避免固定词尾所带来的呆板性(比如诗韵很有限)。 - -基本名词以词韵结尾,全部为元音,来自对应中文关键字的拼音韵母;扣除词韵的部分则为词基,来自对应的英语/法语单词。词韵帮助说中文的人猜词记词,词基帮助懂英语/法语的人猜词记词。 - -例如,melua (瓜; 甜瓜) 一词,词韵是-ua,来自“瓜”的拼音韵母;词基是mel-,来自英语和法语同形的单词melon。如果你不熟悉这个词或者有些忘了,相信下次遇到时词尾-ua可以给你很好的提示。 - -关于冠词:三叶语不要求在名词前使用冠词来进行限制。需要时,可以用 un表示“一个”,相当于英语的a/an或法语的un,泛指;用 le表示“这个/那个”,相当于英语的定冠词the或法语的le/la,特指。 - -数与格:名词复数形式为加-s,与英语类似,例如enfons (孩童们,孩子们);名词所有格为加-'d,与中文类似,例如Mari'd (玛丽的)。复数形式与所有格可以重叠使用,如enfons'd bukus (孩子们的书)。 - -**动词** - -动词的标准词尾为-t。扣除词尾-t后,最后的元音组为词韵。扣除处于后半部分的词韵和词尾后,剩下的部分为词基。同样,词韵来自拼音韵母,词基来自英语/法语。 - -动词有两个形式: - -* 动词原形和动词现在式同形,总以-t 结尾; -* 动词过去式则在原形后面加-ed,所以总以-ted结尾。 - -例如 marchout (走,走路),词韵为-ou,来自“走”的韵母;词基为march,来自英语单词march和法语单词marcher。而其过去式为marchouted。 - -三个最常见的动词比较特别,他们是(原形/过去式):ez/ed (是),haz/had (有),doz/dod (做)。 - -**形容词** - -形容词的标准词尾为-e。扣除词尾-e后,倒数第一组元音组为词韵。扣除词尾和词韵后,剩下的部分为词基。注意,形容词的词尾和词韵之间可能还有一个辅音,它也属于词基。 - -例如:facile (易,容易),词韵为-i,来自“易”的韵母;词基是不连续的,为fac..l-,来自英语和法语同形的单词facile。当然,就这个例子而言,你可能注意到三叶语单词与英语/法语同形——这是巧合,但这种巧合在三叶语中并不是极个别现象。 - -比较级:mor (更多) / les (更少) + 形容词,例如 hupe (愉快,开心) → mor hupe (更开心)。 - -最高级:most (最多) / lest (最少) + 形容词,例如 longe (长) → most longe (最长) - -形容词添加词尾-li得到意思完全一样的副词,即此类副词(占副词绝大部分)的词尾为-eli。 - -#### 2、辅助词 - -这里列举主要的一些辅助词。 - -* 代词。人称代词5个:mo-我,vi-你,lu-它/他/她,ilu-他,elu-她;指示代词6个:zit/zis/zir -这个/这些/这儿,nat/nas/nar-那个/那些/那儿;反身代词:seu-自己。其它代词都为复合词,由这些代词衍生或者合成,如mos-我们(加名词复数词尾),mose-我们的(加形容词词尾),moseseu-我们自己(加词根seu)。 -* 介词。总计只有11个基本的介词:ov-...的,at-在 (具体),in-在 (范围),on-在上面,from-从,tu-向/至,bai-凭,wiz-与...(一起),az-与...(一样),azov-关于,por-为了。其余为复合词。 -* 疑问词。一般疑问词:ka-...吗。特殊疑问词:kua-什么,porkua-为什么,kuen-什么时间,kuer-什么地方,ki-谁,kie-谁的,kele-哪个,kome-如何,conbien-多少。 -* 连词。主要的连词:e-和,or-或,coz-因为,donk-所以,zen-然后。 -* 数词。一至十:zero,un,du,tri,fua,cink,sis,sep,ok,nof,dek。cent-百,千-mil,兆-milion 。三叶语的数词由上述简单合成,例如123为:un cent dudek-tri,其中十位和个位之间用连字符连起来。另外,中间有未被表达出来的零时,要用e(和)表示,这符合中文的习惯,也更清晰,如2048为:du kilo e kuadek-ok。序数词为数词词尾加-me。 -* 语法副词。后文语法部分会提到一些,这里不一一列举。 - -#### 3、复合词 - -复合词包括转性词、派生词和组合词。原则上,复合词不需要专门记忆,熟悉其构词规则即可。 - -**转性词** - -转性词通过修改具象词的词性词尾来实现。如前文所述,大部分副词是由形容词直接转换而来,属于转性词。除此之外,名词、动词、形容词也可以相互转换: - -* 名词加-t转换为动词,表示“形成......”,如 fogu(雾)→ fogut(起雾); - 名词加-je转换为形容词,表示“具有......性质的”,如 nati (自然)→ natije(自然的)。 -* 形容词加-o转换为名词,表示对应的事情,如 sinke(病的)→ sinkeo(疾病); - 形容词加-t转换为动词,表示“使......”,如 seide(悲伤)→ seidet(使......悲伤) -* 动词加-o转换为名词,表示相应动作或行为,例如 chamt(唱)→ chamto(唱歌);
    - 动词加-oi转换为动名词,与英语中的动名词功能相同,可以做主语或宾语;
    - 动词加-e转换为动形词,相当于英语中的现在分词,表示“正在......的”;
    - 动词加-ed是过去式同时也是形容词,表示“被......的”,如 poluted(被污染的)。 - -每一个动词都可以做动形词(-e)和动名词(-oi)的转换,且完全符合规则,所以动形词和动名词一般情况下不列入词典。注意,动词加-o转换而来的词是一种动作名词而不是动名词,不可以带宾语,但可以被冠词或形容词修饰。 - -三叶语词性转换规则严谨,转换后的意思明确。而且,虽然转性词也使用名词、动词、形容词的标准词尾,但实际上由于设计巧妙,转性词与基本词在词尾形式上是有显著区别的,从而使单词词性和来源更易于辨认。 - -**派生词** - -派生词通过基本词添加词缀来实现,主要的词缀参见附录三。 - -派生词中的人物职业和工具名词,均以-(eu)r或-(i)so 结尾。名词加-r表示该方面的人,动词加-eur表示做此事的人、职业或工具,如果以-(i)sto结尾则表示专家、大师。如: - -* edukut(教育)→ edukuteur(教师,教育工作者) -* krackiet(破裂)→ krakieteur(碎裂器,胡桃钳子,英语cracker) -* arti(艺术)→ artisto(艺术家) - -**组合词** - -合成词通过两个或两个以上的基本词或衍生词直接组合而成。例如:futu (足) + bou (球) → futubou (足球)。 - -三叶语词典只收录一些非常高频的组合词,但人们可以自行创造组合词。自创的组合词,必须在成分词之间加连字符。例如需要自创一个特殊的名词叫“闪电球”,就可以写作flashan-bou。广泛认可的自创组合词,在收录进词典后,将去掉连字符。 - -其它复合词也可以自创。各种合理的、意义清楚的自创复合词,在实际使用中是词汇来源的很大一部分。通过长期实践,一部分自创复合词,会以一种严谨的方式慢慢吸收进三叶语的词典。 - -#### 4、聚类词 - -聚类词中的大多数无需花时间记忆,每一类只需要掌握其中少数几个常见的单词,及其特殊的词尾就可以了。其余词汇大多数情况下通过词尾判断其类别即可,即使不认识也不影响阅读理解。聚类词的设定可以节省几百个不重要但也常见的单词的记忆负担,例如“大雁”、“刺猬”、“鳗鱼”、“蟋蟀”、“槐树”、“菊花”这样的一些词。 - -主要的聚类词及其规定的词尾为: - -动植物类:鸟- b,兽-p,鱼-v,虫-f,草-g,树-k。如poluliamg-杨树。注意“鸡、鸭、鹅、马、牛、羊、猪、狗、猫、鼠”十种家禽家畜并不放在聚类词中,“人”也不放在聚类词中,它们都在基本名词里。 -拟声词类:拟声词名词词尾为-h,动词词尾为-ht。注意h要发音。 -洲名国名:洲-a,国家-ia,相应的语言-ien。如Zhongia (中国),Zhongien (中文,中国人)。 -科学名词:物理粒子-on。化学元素-um。化学元素取自拉丁文,这样更符合科学习惯。 -有些聚类词没有词韵,但由于聚类词的词尾已经能很好地帮助对单词的识别,并且大部分聚类词通常并不需记住,所以没有词韵并不会带来学习困难。 - -### ☘ 语法:核心内容只需六字口诀 - -三叶语的语法主要参考中文和英语,其语序和时态语态规则十分自然、精确且逻辑性很强,不仅让学语者极其容易掌握、不易忘记,而且有利于将来的人机对话和智能语言分析。 - -#### 一般句式 - -三叶语一般句式及其语序与英语完全一致,与中文的区别仅需注意长定语(即定语短语和定语从句)、时间状语、地点状语的位置有所不同。 - -遵循简单的主-谓-宾语序,但疑问句以疑问词开头; -短定语位于名词前,长定语位于名词后; -状语位于动词前,但时间状语、地点状语一般位于句子最后,也可以在最前; -补语一般位于语句最后。 -时态语态 - -谓语的时态、体貌和语态是语法的核心内容,也是其重要标志。三叶语用独特的方式融合了中文和英语,简单清晰,容易运用。 - -首先,动词只有一种变化形式——加-ed成为过去式,相当于中文的“了”,用于表示过去发生的事,但不强调是否完成。而当前的事使用动词原形,但不强调是否正在进行。例如: - -Mari bakauted bredian. 玛丽烤了面包。(过去的事,但不强调是否完成) -Mari bakaut bredian. 玛丽(在)烤面包。 (当前的事,但不强调是否正在进行) - -进一步,表示时态、体貌和语态的其它成分按如下次序添加在动词前: - -* 加系词 ez (被),用于表达被动语态。此时动词必须用过去式,但不表示过去事件;用系动词ez的过去式(ed)表示过去的事件。 -* 加副词 jen (正在),强调事件正在发生。 -* 加副词 deja (已经),强调事件已经发生。此时动词必须用过去式。 -* 加副词 ba (不/没),表示否定,即事件未曾发生或不会发生。 -* 加副词 vil (将/会),表示将来,即事件预计会发生。 - -归纳起来,动词时态语态的总和形式为: - -**(将vil) (不ba) (已deja) (在jen) (被ez/ed) 动词(-ed)** - -括号表示可选。此公式包含了英语中绝大部分时态和被动语态,以及相应的否定式的构成,同时基本符合中文的习惯。只有少数情况不及英语精确,但可以通过时间状语或上下文明确。 - -此公式简单清晰,一目了然,对中文习语者还可简称为六字口诀:将-不-已-在-被-动,谐音为“江湖已在波动”,即刻记牢。 - -#### 语气情态 - -一共四个情态副词: - -* vud:愿意,想要 -* kud:可以,能够,被允许 -* vuad:应该,必须 -* shud:就该,本该,蛮好(虚拟语气,希望发生但未曾发生或不太可能发生的事件) - -情态副词在句子中占据六字口诀里“将vil”的位置。 例如: - -Mari vil gut lunue. 玛丽将去月球。 (预计会发生) -Mari vud gut lunue. 玛丽想去月球。(希望会发生) -Mari shud gut lunue. 玛丽就该去月球。(希望会发生,但不太可能发生) -Mari shud guted lunue. 玛丽本该去月球的。(希望过去发生,但实际未曾发生) - -#### 特殊句式与复杂句式 - -三叶语在特殊句式和复杂句式的构成方面与英语基本一样。但三叶语没有不定式,动词可以连续使用,后面的动词使用原形即可;而需要用不定式做主语或宾语时,可以用动名词短语或者名词性从句代替。 - -特殊句式的构成: - -* 疑问句:疑问词前置,其余语序不变。例如:Ka tru ez vurte? (树是绿色的吗?) -* 祈使句:省略主语,谓语开头,其余不变。例如:Ne plerut! (别哭!) -* 无主句:用代词lu充当主语,例如 Lu vil plut in posdi. (明天会下雨。) -* 存在句:与英语类似,Nar ez ...。例如 Nar ez un buku em le table. (桌上有本书。) - -#### 复杂句式的构成(初学者可跳过): - -* 动名词短语:动名词或动名词短语可以做主语或宾语,相当于英语中的不定式或动名词做主语或宾语;但内容较为复杂时,应考虑使用名词性从句代替。注意,动名词短语做主语时,不可后置,也就是没有英语中的“It is ... to do ...”这样的结构。 -* 名词性从句:名词性从句可以做主语或宾语,以nat (那个)、ki (谁) 等引导。主语从句如果很长,可以用lu代替,并把主语从句后置。例如 Lu ez bone pensamto nat elu studuet chamto. (她去学唱歌是个好想法。) -* 动形词短语:动形词或动形词短语可以做定语,前者前置,后者后置。内容较为复杂时,应考虑用形容词性从句代替。 -* 形容词性从句:形容词性从句可以做定语,以kele (哪个) 、kie (谁的) 等引导,并后置。 -* 副词性从句:kuen、kuer等引导副词性从句,做状语,一般放于句子前面。 -* 条件句与因果句:条件句if... (zen),因果句coz...或者...donk。 - -### ☘ 结束语:你已是一个三叶语者! - -归纳一下,三叶语“易学易记不易忘”的特点来源于其在语音、语法和词汇方面的巧妙设计: - -* 语音方面,舍弃了那些繁琐和易混淆的发音,保留最公认的字母和发音,并做到一字一音。 -* 语法方面,核心关键部分只需要记住“将不已在被动”六字口诀即可掌握。 -* 词汇方面,融合英法中三语词源,习语者总能找到自己的助记符,背单词不再难。 - -三叶语如此之简单,所以在阅读前文的内容后,如果你能记住人称代词 mo / vi / lu、最常见的几个介词 di / at / in / em / bai / por、以及最常用的三个动词 ez / haz / doz,那么你已经可以开始使用三叶语了! - -何以?首先,你已能猜句子;其次,你已能日常问候;第三,你已能阅读很简单的材料。 - -附录四中给出了这三个方面的例子,你现在就可以尝试,且全程不需查词典。当然,如果你愿意查词典以期更好更快地掌握词汇,你可以到......下载三叶语词典(顺便提一下,三叶语的词典也很有特色,它是按自然分类而不是字母顺序排列的)。 - -更重要的是,如果你在简单使用中积累了一些常用词汇和语句,那么,即使你“有事离开三叶语半年”,回来你基本上依然记得! - -看完本文,你已是一个三叶语者!这意味着,三叶语一诞生,即刻就有35亿用户——即中英法三大语言已经覆盖的人群——只要他们愿意花二十分钟阅读本文,了解一下即可! - -而且,这里面又有将近一半的人,已经或很快就算得上“基本熟练使用者”(可以简单对话,借助词典可以阅读一般材料及写作简单的信件或短文)。他们包括英语母语者、法语母语者且略为学过英语的、中文母语者且熟练掌握汉语拼音和英语的。 - -也许,你就是其中一员! - -### 附录一 三叶语音素总表 - - - -### 附录二 三叶语词源规整化规则 - -#### 总体原则: - -* 所有重复的连续辅音,合并成一个辅音,如ball→bal。 -* 单元音一般不修改,即使其发音与三叶语读音规则不一致。 -* 拼音中的双元音、三元音,即使不属于三叶语的双元音,一般也不修改,应分别读出。 -* 英/法的双元音、三元音等元音字组,按其在单词中的发音修改为三叶语中最接近的元音。 -* 与三叶语读音基本一致的辅音不修改,不一致的按其在单词中的读音修改为三叶语的辅音。 - -常见的修改内容见下表: - - -### 附录三 三叶语词缀表 - - -### 附录四 即学即用的例子 - -#### 1、猜句子 - -让我们看一下这两句话: - -Mo denpeli aimait vi. Vi ez splandide soli in moe hartin. - -在实际语言中,这两个句子并不是属于非常简单的句子,而是有点儿诗意的。现在,你要来猜一下它们的意思。 - -假定,你除了本文已经提到的单词外,三叶语的其他单词一概不知。并且假定你也没有英语基础——对大多数人来说,这个假设显然有点儿看低了,但没关系,我们只是做测试。 - -根据前文的知识,这两句话中有些内容你是认识的,例如mo-我,vi-你,ez-是,in-在...中,moe-我的。所以抛开不认识的部分,你看到的句子会是这样: - -Mo __en__ (ad.) ___ai (v.) vi. Vi ez __an__ (a.) ___i (n.) in moe ___in (n.). - -下划线是你看不懂的部分,但根据词尾判断出词性后,你找到了它们的词韵(斜体表示)。 - -* 第一句,我“怎样”你?以ai为韵。不难猜到可能是“爱”。 -“怎样地”爱?以en为韵。不难猜出是“深深地”。所以第一句是“我深深地爱着你”。通的! -* 第二句,你是“什么”?以i为韵。这个稍微有点儿难,可以留一下。 -* 在我的“什么”中?以in为韵。很容易想到是“心”吧。 -* 回过来,你是我心中的“什么”?以i为韵。应该是“太阳”,但韵好像不对,不过多想一下可以理解韵母来自ri(日)。 -* 现在再看是“什么样的”太阳呢?以an为韵。很容易想到“灿烂”。 - -连起来就是:我深深地爱着你,你是我心中灿烂的阳光。 - -上面看起来挺长的猜词过程,其实在脑子里只是一瞬间。不妨再回过去看一下原句,看看是不是确实可以很快猜出。如果有上下文,或者你已经有点儿三叶语的基础,抑或你学过点儿英语,那猜起来就更容易了。 - -倘若是英语母语或法语母语者,猜起来就更是易如反掌了。这里不再模拟其过程。 - -#### 2、日常对话 - -看看你是不是已经能看懂并说出下面这段日常对话。你可能需要猜4个词:dankie、namin、mitut (mituto)、bai。 - -A: Hai ! Kome ez vi ? - -B: Bone. Et vi ? - -A: Bone. Dankie ! Qua ez vie namin ? - -B: Mo ez Marko. Et vi ? - -A: Alice. Bone mituto vi ! - -B: Bone mituto vi ! Bai ! - ------ - -注:p/t/k的发音实为送气音[ph]/[th]/[kh],h为上标。为简化起见,一律不标注h。而b/d/g发音实为不送气音[p]/[t]/[k],类似英语和汉语的b/d/g发音,但发法语和德语那样的浊音[b]/[d]/[g]也可以。 - -(修订于2019-09-10) diff --git "a/_posts/2019-09-03-\345\246\202\344\275\225\345\244\204\347\220\206\346\213\274\351\237\263\344\270\255\347\232\204ui\345\222\214iu\347\274\251\345\206\231.md" "b/_posts/2019-09-03-\345\246\202\344\275\225\345\244\204\347\220\206\346\213\274\351\237\263\344\270\255\347\232\204ui\345\222\214iu\347\274\251\345\206\231.md" deleted file mode 100644 index 11ab11ea847..00000000000 --- "a/_posts/2019-09-03-\345\246\202\344\275\225\345\244\204\347\220\206\346\213\274\351\237\263\344\270\255\347\232\204ui\345\222\214iu\347\274\251\345\206\231.md" +++ /dev/null @@ -1,30 +0,0 @@ ---- -layout: post -title: 三叶语是如何处理拼音中的ui和iu的? -subtitle: 实事求是,灵活处理,让大家都更舒服 -date: 2019-09-03 -author: Henri Jambo -header-img: img/post-bg-YesOrNo.jpg -catalog: false -tags: - - 设计点滴 ---- - -在汉语拼音方案中,ui是uei的简写,iu是iou的简写。 - -但在三叶语中,我们取词韵的时候,对上述两个缩写的处理方式有所不同:ui缩写保留,而iu缩写不保留。 - -为什么这样做呢? - -首先,按照三叶语的读音规则,ui与uei的读音非常接近,发音结束时的口型和位置很相似,而iu和iou的区别就比较大了。所以从这个意义上来说,保留ui缩写是可以的,这样可以缩短单词长度,但保留iu缩写就不太好了。 - -其次,在英语中,也有大量的单词含有ui这样的组合结构,如suit,fruit等,因此保留ui缩写让西人对三叶语更有熟悉感,而iu就没有这样的功效。 - -综上,三叶语对于这两种缩写相关的元音组合处理如下: - -1. 保留ui缩写,并把wei也改写为ui。 -2. 不保留iu缩写,所有iu还原成iou,并把you也改写成iou。 - -其实汉语拼音中还有一种简写,即把uen简写成了un,对此我们也保留。所以相应的,wen也需要改写成un。 - -注意,改完后的读音方法还是要与三叶语的读音规则一直,即ui念快速的“乌衣”而不是“威”,与汉语拼音有所不同;而iou念快速的“衣欧”则与汉语拼音的iu基本一致。 diff --git "a/_posts/2019-09-22-\344\270\211\345\217\266\350\257\255\350\257\246\350\247\243\350\257\255\351\237\263.md" "b/_posts/2019-09-22-\344\270\211\345\217\266\350\257\255\350\257\246\350\247\243\350\257\255\351\237\263.md" deleted file mode 100644 index 389bba72e4c..00000000000 --- "a/_posts/2019-09-22-\344\270\211\345\217\266\350\257\255\350\257\246\350\247\243\350\257\255\351\237\263.md" +++ /dev/null @@ -1,276 +0,0 @@ ---- -layout: post -title: 三叶语详解(一 语音) -subtitle: 英汉互补,不学就会;一字一音,看到能读,听到能写 -date: 2019-09-22 -author: Henri Jambo -header-img: img/post-bg-miui6.jpg -catalog: true -tags: - - 学习资料 ---- - -### 〇、总则 -三叶语选取英语和汉语中比较容易发音且不容易混淆的那些发音作为音素,让来自不同母语背景的人都可以几乎不用学习就可以掌握三叶语的发音。 - -三叶语语音设计主要遵循以下原则: - -* 字母以简单为原则:1)不使用变音符号;2)双字母尽量少。 -* 辅音以英语为主要参考,但去掉汉语中没有类似发音的音素。 -* 元音以汉语拼音为主要参考,但减少双元音和鼻元音的数量。 - -另外,三叶语遵循广义的“一字一音”原则,基本做到“看到能读,听到能写”。 - -### 一、字母表 -三叶语使用最广泛的26罗马字母表,但其中q、w、x三个字母仅用于外来语,实际使用23个字母。 - -| 字母 | 字母名读法 | 字母读法 | Notis | -|:---:|:-----:|:------:|:---------------| -| Aa | a | \[a\] | -| Bb | be | \[b\] | -| Cc | ce | \[ts\] | -| Dc | de | \[d\] | -| Ee | e | \[e\] | -| Ff | ef | \[f\] | -| Gg | ge | \[g\] | -| Hh | ech | \[h\] | -| Ii | i | \[i\] | -| Jj | je | \[dʒ\] | -| Kk | ke | \[k\] | 实为送气音(加上角标h) | -| Ll | el | \[l\] | -| Mm | em | \[m\] | -| Nn | en | \[n\] | -| Oo | o | \[o\] | -| Pp | pe | \[p\] | 实为送气音(加上角标h) | -| Qq | kue | \[k\] | 仅用于外来词 | -| Rr | er | \[ɹ\] | -| Ss | es | \[s\] | -| Tt | te | \[t\] | 实为送气音(加上角标h) | -| Uu | u | \[u\] | -| Vv | ve | \[v\] | -| Ww | we | \[w\] | 仅用于外来词 | -| Xx | eks | \[ks\] | 仅用于外来词 | -| Yy | y | \[y\] | -| Zz | ze | \[z\] | - -* y相当于拼音字母ü(迂),不会念ü者也可按英语念[ju];用于外来词时,可以发[j] - -### 二、音素表 - -#### 1、辅音19个 - -| 辅音 | 拼写 | IPA | 类似读音(推荐读音:en) | -|:---:|:---:|:------:|:----------------------| -| 1 | b | \[b\] | en:Bus, fr:Bone, cn:Ba | -| 2 | p | \[p\] | en:Put, fr:Plus, cn:Pa | -| 3 | v | \[v\] | en:Van, fr:Vous, cn:// | -| 4 | f | \[f\] | en:For, fr:Fils, cn:Fa | -| 5 | d | \[d\] | en:Dad, fr:Dent, cn:Da | -| 6 | t | \[t\] | en:Ted, fr:Trop, cn:Ta | -| 7 | l | \[l\] | en:Let, fr:Loin, cn:La | -| 8 | n | \[n\] | en:Not, fr:Neuf, cn:Na | -| 9 | g | \[g\] | en:Get, fr:Gros, cn:Ga | -| 10 | k | \[k\] | en:Ken, fr:Quoi, cn:Ka | -| 11 | h | \[h\] | en:How, fr:Rose, cn:Ha | -| 12 | m | \[m\] | en:Mum, fr:Mere, cn:Ma | -| 13 | z | \[z\] | en:Zen, fr:Zero, cn:Za | -| 14 | c | \[ts\] | en:iTS, fr: // , cn:Ca | -| 15 | s | \[s\] | en:Sir, fr:Sept, cn:Sa | -| 16 | r | \[ɹ\] | en:Rat, fr: // , cn:RU | -| 17 | j | \[dʒ\] | en:Jim, fr: // , cn:Ji | -| 18 | ch | \[tʃ\] | en:CHin, fr:maTCH,cn:Qi | -| 19 | sh | \[ʃ\] | en:SHip, fr:CHer ,cn:Xi | - -* 除了本表所列复辅音,其它相连辅音都按照各个辅音的读音快速连读 -* 为简便起见,省略了送气音[p]/[t]/[k]的上角标h -* b/d/g发音实为不送气音[p]/[t]/[k],类似英语和汉语发音,但发法语那样的浊音[b]/[d]/[g]也可 - -#### 2、元音18个 - -| 元音 | 拼写 | IPA | 类似读音(推荐读音:cn) | -|:---:|:---:|:-----:|:---------------------------| -| 1 | a | \[a\] | en:bAr, fr:grAs, cn:bA | -| 2 | e | \[e\] | en:gEt, fr:chEz, cn:yE | -| 3 | o | \[o\] | en:fOr, fr:grOs, cn:bO | -| 4 | i | \[i\] | en:bEE, fr:grIs, cn:bI | -| 5 | u | \[u\] | en:flU, fr:vOUs, cn:bU | -| 6 | y | \[y\] | en:fEW, fr:menU, cn:jU | -| 双元音 | -| 7 | ai | \[ai\] | en:EYE, fr: // , cn:AI | -| 8 | ei | \[ei\] | en:hEI, fr: // , cn:EI | -| 9 | au | \[aʊ\] | en:hOW, fr: // , cn:AO | -| 10 | ou | \[əʊ\] | en:lOW, fr: // , cn:OU | -| 11 | eu | \[ə\] | en:thE, fr:mEnu, cn:kE | -| 鼻元音 | -| 12 | an | \[an\] | en:bAN, fr:cINq, cn:AN | -| 13 | en | \[en\] | en:tEN, fr: // , cn:EN | -| 14 | on | \[on\] | en:tONe, fr:rONd, cn:kONG | -| 15 | in | \[in\] | en:sINk, fr: // , cn:yIN | -| 16 | un | \[un\] | en:mOON, fr: // , cn:kUN | -| 17 | yn | \[yn\] | en:tUNe, fr:dUNe, cn:jUN | -| 18 | am | \[aŋ\] | en:swAMp,fr:jAMbe,cn:kANG | - -* 除了ang(am),其他后鼻音都按前鼻音发音 -* 如果鼻元音后面紧跟字母g,g需单独发音读作[g] -* 其它形式的元音组合,需将每个元音分别读出,快速连读 -* 鼻元音后面紧跟形容词词尾-e时,m/n视为重复,下一音节视为以m/n开始 - - -### 三、国际音标音位表 - -按国际音标规则的音位表表示如下。 - -##### 辅音 - - - - - - - - - - - - -
    -Labial -Alveolar -Post-alveolar -Velar -Glottal -
    Nasal - -m - -n - - - - -
    Stop -ph -b(p) -th -d(t) -tʃ -dʒ -kh -ɡ(k) - -
    Fricative -f -v -s ʦ -z -ʃ - - -h -
    Approximant - - -l - -ɹ - - - -
    - -##### 元音 - - - - - - - - - - - - - - - - - -
    -Front -Nearfront -Central -Nearback -Back -
    High - i - y - - - u -
    Near_High - - - - - -
    High_Mid - - - - - -
    Mid - - e̞ - ə (eu) - - o̞ -
    Low_Mid - - - - - -
    Near_Low - - - - - -
    Low - - - ä - - -
    - -为简单起见,我们在别的地方会省略所有的上标、变音符号等。 - -### 四、音节 -与英语不同,三叶语划分音节的时候不会把连续的辅音或连续的元音分开。我们把一个或多个连续的辅音叫一个辅音组,一个或多个连续的元音叫一个元音组。 - -音节划分原则: - -* 按“(辅)元-辅元-辅元…辅元(辅)”的结构划分音节; -* 每个音节由“一个辅音组+一个元音组”的方式构成; -* 第一个音节可以没有辅音组,最后一个音节则可能多一个辅音组。 - -例如fabrikaut(造,制造),音节划分为fa-bri-kaut。 - -### 五、重音 -由于一字一音的准则,使得三叶语的重读、轻读不影响发音,所以三叶语里重音位置并不重要。 - -我们的一般指导原则是: - -* 语辅词的重音在最后一个音节,如i'lu(他),e'lu(她),kom'bien(多少); -* 原形动词的重音在倒数第二个音节,如'manjit(吃),en'kontyt(遇见); -* 名词和形容词的重音在倒数第三个音节,如'fiziku(物理),'facile(容易); - -另外,在句子未结束的情况下,常把重音移到最后一个音节上,即类似英语中的升调。多用升调听上去比较优雅。 - - - - - diff --git "a/_posts/2019-10-18-\345\255\246\344\271\240\344\270\211\345\217\266\350\257\255\350\246\201\350\212\261\345\244\232\345\260\221\346\227\266\351\227\264.md" "b/_posts/2019-10-18-\345\255\246\344\271\240\344\270\211\345\217\266\350\257\255\350\246\201\350\212\261\345\244\232\345\260\221\346\227\266\351\227\264.md" deleted file mode 100644 index aa8bf6b40d2..00000000000 --- "a/_posts/2019-10-18-\345\255\246\344\271\240\344\270\211\345\217\266\350\257\255\350\246\201\350\212\261\345\244\232\345\260\221\346\227\266\351\227\264.md" +++ /dev/null @@ -1,59 +0,0 @@ ---- -layout: post -title: 学习三叶语要花多少时间? -subtitle: 设计目标:半天入门,一月掌握,两年精通 -date: 2019-10-18 -author: Henri Jambo -header-img: img/post-bg-alibaba.jpg -catalog: true -tags: - - 设计点滴 ---- - -作为人造国际通用语,三叶语的设计目标是:简单易学易用,大多数人可以在很短时间内学会。 - -那么三叶语究竟有多简单,学习时间要多少呢? - -三叶语的特点是词汇融合中西方语言,所以简单到单词可以“猜”,加上其语法为极其简单的固定词序规则,以及读音为中西共通且规则标准,所以学习是非常简单的。 - -以下我们从快速入门、进阶学习、大师水平三个级别来看学习三叶语需要的时间精力。 - -#### 1、快速入门:半天时间,达到A2/B1水平,即基本可用 -(1)半天学习目标详解 - - 1、朗读与听写:即使不熟悉词义,也能基本做到“看到能读,听到能写” - 2、阅读:A1/A2级阅读材料可以基本看懂,B1/B2文章借助词典可以看懂 - 3、写作:借助词典可以写出A2/B1级的短文 - 4、听力:听懂A1级内容,A2级内容在语速很慢和反复的情况下可以基本听懂 - 5、口语:可以说出自我介绍,打招呼等A1级内容 - -(2)半天学习时间分配 - -| 语言背景 | 中文 \+ 英文 | 中文(会拼音) | 英文(B2以上) | -|:-------------:|:--------:|:------:|:--------:| -| 语音 | 5 | 10 | 15 | -| 掌握基本单词100个 | 20 | 30 | 30 | -| 基本语法 | 20 | 30 | 30 | -| 熟悉基本单词500个 | 30 | 30 | 30 | -| 浏览全部基本词汇1500个 | 30 | 30 | 30 | -| 对话/短文 | 30 | 45 | 30 | -| 总计学习时间(分钟) | 145 | 175 | 165 | - - * 上述语言背景栏里的英文可以换为法文,相应学习时间略有增加。 - * 中、英、法皆无基础的学习者,视其母语与它们的相似性,学习时间增加50%到100%。 - -#### 2、进阶学习:一个月时间,达到B2水平,即基本掌握 - - 1、阅读:可阅读一般报纸杂志文章,有时需要查词典 - 2、写作:借助词典,可以写作B2/C1级文章 - 3、听力:可以听懂B1/B2级文章 - 4、口语:可以进行日常对话和简单的思想交流 - -学习内容主要为背熟全部1500个基本单词,以及听、说、读、写练习 - -#### 3、大师水水准:一年到两年时间,完全掌握,流利对话,并具备教学能力 - -上述是假定连续学习所需要的时间。实际情况是可以每天少学一些时间,总时间拉长的。比如每天学习半小时到一小时,入门阶段拉长至3~5天,进阶阶段拉长到半年左右。由于三叶语“易记不易忘”的特点,所以中间有中断也没有关系。 - -以上为是三叶语的设计目标,能否做到尚需实践检验。 - diff --git a/_posts/2019-11-12-renamed.md b/_posts/2019-11-12-renamed.md deleted file mode 100644 index d00d590a5d4..00000000000 --- a/_posts/2019-11-12-renamed.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -layout: post -title: 三叶语更名为「星球语」 -subtitle: 本网站停止更新,请移步新网站 -date: 2019-11-12 -author: Henri Jambo -header-img: img/the-first.png -catalog: false -tags: - - 新闻故事 ---- - -三叶草国际语已更名为「星球语」(Globien),全称「星球国际语」。请前往新网址: - -* 中文版:[globien.gitee.io](https://globien.gitee.io) -* 英文版:[globien.github.io](https://globien.github.io) - -新名称可以更好地体现其人造国际语的定位。与世界语(Esperanto)相比,其特点是包含东方语言元素,更具全球性。例如: - -* 元音(包括双元音/鼻元音)的拼写和发音与汉语拼音更像; -* 单词中含有来自拼音的“词韵”部分,可以让中文背景者学习单词更容易; -* 动词变位减少到只有一种,而时态体貌简化为“将不已在被动”六个字的固定次序。 - -了解更多详情请前往新网站的相关介绍:[星球语简介](https://globien.gitee.io/2018/12/10/briefing)。 diff --git "a/_posts/2019-91-16-\344\270\211\345\217\266\350\257\255\351\246\226\344\270\252\347\211\210\346\234\254\345\217\221\345\270\203.md" "b/_posts/2019-91-16-\344\270\211\345\217\266\350\257\255\351\246\226\344\270\252\347\211\210\346\234\254\345\217\221\345\270\203.md" deleted file mode 100644 index 2ff0fa97b23..00000000000 --- "a/_posts/2019-91-16-\344\270\211\345\217\266\350\257\255\351\246\226\344\270\252\347\211\210\346\234\254\345\217\221\345\270\203.md" +++ /dev/null @@ -1,42 +0,0 @@ ---- -layout: post -title: 三叶语首个版本发布 -subtitle: v0.6版 -date: 2019-01-16 -author: Henri Jambo -header-img: img/post-bg-cook.jpg -catalog: true -tags: - - 新闻故事 ---- - -![]({{site.baseurl}}/img/logo.png) - -#### 发布信息 - -2019年1月16日。V0.6版。 - -此版本为三叶语第一次公开。 - -发布媒体: - -* 百度贴吧:[三叶草国际语](https://tieba.baidu.com/p/6009002082) -* 知乎专栏:[三叶草国际语概述(V0.6)](https://zhuanlan.zhihu.com/p/46039432) - -#### 三叶语特点 - -融合中西语言的人造国际语。 - -* 单词中含有中文韵母成分(词韵),便于中国人学习和记忆。 - -* 语音倾向于英文和中文的结合,易于发声。 - -* 语法简洁,核心部分即谓词变化为固定次序,“将不已在被动”六个字即可概况。 - -#### 完成进度 - -* 语音基本完成,完成度99%。 - -* 语法主体完成,完成度80%。 - -* 词汇中语辅词基本完成,其它基本词部分完成,完成度<50%。 diff --git "a/_posts/2022-01-01-\346\216\250\350\215\220\347\263\273\347\273\237\357\274\232\346\216\250\350\215\220\347\263\273\347\273\237\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/_posts/2022-01-01-\346\216\250\350\215\220\347\263\273\347\273\237\357\274\232\346\216\250\350\215\220\347\263\273\347\273\237\345\237\272\347\241\200\347\237\245\350\257\206.md" new file mode 100644 index 00000000000..aab9066cb90 --- /dev/null +++ "b/_posts/2022-01-01-\346\216\250\350\215\220\347\263\273\347\273\237\357\274\232\346\216\250\350\215\220\347\263\273\347\273\237\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -0,0 +1,143 @@ +--- +layout: post +title: 推荐系统:推荐系统基础知识 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 推荐系统 +--- + + + +# 推荐系统概念 + +#### 推荐系统要解决的核心是长尾对象。 + +从某种意义上说,推荐系统和搜索引擎对于用户来说是两个互补的工具。 + +搜索引擎满足了用户有明确目的时的主动查找需求,而推荐系统能够在用户没有明确目的的时候帮助他们发现感兴趣的新内容。 + +如果要通过发掘长尾提高销售额,就必须充分研究用户的兴趣,而这正是个性化推荐系统主要解决的问题。推荐系统通过发掘用户的行为,找到用户的个性化需求,从而将长尾商品准确地推荐给需要它的用户,帮助用户发现那些他们感兴趣但很难发现的商品。 + +#### 推荐系统不仅准确预测用户的行为,而且能够扩展用户的视野、帮助用户发现可能会感兴趣的东西。 + +- 推荐系统不仅仅能够淮准确预测用户的行为,而且能够扩展用户的视野,帮助用户发现那些他们可能会感兴趣,但却不那么容易发现的东西。 + +- 同时,推荐系统还要能够帮助商家,将那些被埋没在长尾中的好商品、介绍给可能会对它们感兴趣的用户。这也正是《长尾理论》的作者在书中不遗余力介绍推荐系统的原因。 + + +# 个性化推荐系统 + +- 个性化推荐系统在这些网站中的主要作用是通过分析大量用户行为日志,给不同用户提供不同的个性化页面展示,来提高网站的点击率和转化率。 + +- 个性化推荐的成功应用,需要两个条件: + - 第一,存在信息过载。 + - 如果用户可以很容易地从所有物品中找到喜欢的物品,就不需要个性化推荐了。 + - 第二,用户大部分时候没有特别明确的需求 + - 用户如果有明确的需求,可以直接通过搜索引擎找到感兴趣的物品。 + +- 示例:个性化阅读 + - 个性化阅读符合前面提出的需要个性化推荐的两个因素: + - 首先,互联网上的文章非常多,用户面临信息过载的问题; + - 其次,用户很多时候并没有必须看某篇具体文章的需求,他们只是想通过阅读特定领域的文章了解这些领域的动态。 + +- 个性化广告投放和狭义个性化推荐的区别是: + - 个性化推荐着重于帮助用户、找到可能令他们感兴趣的物品;而广告推荐着重于帮助广告、找到可能对它们感兴趣的用户; + - 即一个是以用户为核心,而另一个以广告为核心。 + +- 目前的个性化广告投放技术主要分为 3 种: + - 上下文广告 + - 通过分析用户正在浏览的网页内容,投放和网页内容相关的广告,代表系统是谷歌的 Adsense。 + - 搜索广告 + - 通过分析用户在当前会话中的搜索记录,判断用户的搜索目的,投放和用户目的相关的广告。 + - 个性化展示广告 + - 我们经常在很多网站看到大量展示广告(例如 banner),它们是根据用户的兴趣,对不同用户投放不同的展示广告。 + + +# 推荐系统模型 + +- 模型作用: + - 给用户推荐产品,给产品推荐用户。 + +#### 协同过滤( collaborative filtering) + +- 比如: + - 著名的IMDB电影排行榜,看看别人都在看什么电影,别人都喜欢什么电影,然后找一部广受好评的电影观看。 +- 这种方式可以进-步扩展: + - 如果能找到和自己历史兴趣相似的一群用户,看看他们最近在看什么电影,那么结果可能比宽泛的热门排行榜更能符合自己的兴趣,这种方式称为基于协同过滤(collaborative filtering)的推荐。 + + +# 推荐算法分类 + +#### 基于关联规则的分类 + +- 购物篮分析; +- 计算量比较大(排列组合); + +#### 基于内容的推荐 + +- 比较内容相似度; +- 网站没有用户的时候,可以使用此算法做冷启动; + +#### 基于人口统计的推荐(用户画像标签) + +- 按照用户的标签进行推荐; + +#### 基于协同过滤的推荐 + +- 基于用户的推荐 +- 基于物品的推荐 +- 推荐系统弊端:审美疲劳 + + ![]({{site.baseurl}}/img-post/用户画像-11.png) + +# 推荐模型效果测试 + +#### 显示反馈 VS 隐式反馈 + + ![]({{site.baseurl}}/img-post/推荐系统-1.png) + +#### 测试推荐效果的实验方法 + +- 离线实验(offline experiment) + - 离线实验的方法一般由如下几个步骤构成: + - 通过日志系统获得用户行为数据,并按照一定格式生成一个标准的数据集; + - 将数据集按照一定的规则分成训练集和测试集; + - 在训练集上训练用户兴趣模型,在测试集上进行预测; + - 通过事先定义的离线指标评测算法在测试集上的预测结果。 + +- 用户调查(user study) + +- 在线实验 (online experiment)。 + - A/B 测试 是一种很常用的在线评测算法的实验方法。 + - 它通过一定的规则将用户随机分成几组,并对不同组的用户采用不同的算法,然后通过统计不同组用户的各种不同的评测指标比较不同算法,比如可以统计不同组用户的点击率,通过点击率比较不同算法的性能。 + +#### A/B Test + +- 对AB测试感兴趣的读者,可以参考网站 http://www.abtests.com/,该网站给出了很多通过实际AB测试提高网站用户满意度的例子,从中我们可以学习到如何进行合理的AB测试。 + +- 一个大型网站的架构分前端和后端,从前端展示给用户的界面到最后端的算法,中间往往经过了很多层,这些层往往由不同的团队控制而且都有可能做AB测试。如果为不同的层分别设计AB测试系统,那么不同的AB测 试之间往往会互相干扰。 + +- 比如,当我们进行一个后台推荐算法的AB测试,同时网页团队在做推荐页面的界面AB测试,最终的结果就是你不知道测试结果是自己算法的改变,还是推荐界面的改变造成的。 + +- 因此,切分流量是AB测试中的关键。 + - 不同的层以及控制这些层的团队、需要从一个统一的地方获得自己AB测试的流量; + - 首先,用户进入网站后,流量分配系统决定用户是否需要被进行AB测试,如果需要的话,流量分配系统会给用户打上在测试中属于什么分组的标签。 + - 然后,用户浏览网页,而用户在浏览网页时的行为都会被通过日志系统发回后台的日志数据库。此时,如果用户有测试分组的标签,那么该标签也会被发回后台数据库。 + - 在后台,实验人员的工作首先配置流量分配系统,决定满足什么条件的用户参加什么样的测试。其次,实验人员需要统计日志数据库中的数据,通过评测系统生成不同分组用户的实验报告, + +#### TopN 推荐 + +- 亚马逊前科学家 Greg Linden 2009 年在 Communications of the ACM 网站发表了一篇文章,指出: + - 电影推荐的目的,是找到用户最有可能感兴趣的电影,而不是预测用户看了电影后会给电影什么样的评分。 + - 因此,TopN 推荐更符合实际的应用需求。 + - 也许有一部电影用户看了之后会给很高的分数,但用户看的可能性非常小。 + - 所以,预测用户是否会看一部电影,应该比预测用户看了电影后会给它什么评分更加重要。 + + + + + diff --git "a/_posts/2022-01-01-\346\225\260\346\215\256\347\273\223\346\236\204\357\274\232\344\275\215\345\233\276\347\232\204\347\211\271\346\200\247\344\270\216\347\274\272\351\231\267.md" "b/_posts/2022-01-01-\346\225\260\346\215\256\347\273\223\346\236\204\357\274\232\344\275\215\345\233\276\347\232\204\347\211\271\346\200\247\344\270\216\347\274\272\351\231\267.md" new file mode 100644 index 00000000000..87053f166d9 --- /dev/null +++ "b/_posts/2022-01-01-\346\225\260\346\215\256\347\273\223\346\236\204\357\274\232\344\275\215\345\233\276\347\232\204\347\211\271\346\200\247\344\270\216\347\274\272\351\231\267.md" @@ -0,0 +1,69 @@ +--- +layout: post +title: 数据结构:位图的特性与缺陷 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据结构 +--- + + + +# 1. 什么是位图 + +- 前面介绍了哈希算法,同时也指出了哈希算法存在的空间复杂度太高问题,那么有没有一种数据结构满足 **既能处理大量数据,同时又不占用太多空间** 的要求呢? +- 这里我们介绍另一种叫 **位图** 的数据结构,这里的位图并不真的是图,而是按照集合中最大元素 max 创建一个长度为 max+1 的新数组,在数据存入时扫描原始数组,每当遇到一个数值就把新数组该位置写为 1,比如遇到 5 就给新数组的第 6 个元素置 1,这样下次再遇到 5 时发现新数组的第 6 个元素已经是 1 了,这说明这次的数据肯定和以前的数据存在着重复。这种给新数组 初始化时置零其后置一的做法,类似于位图的处理方法故称 位图算法; +- 这段定义文字可能看起来很拗口,我们解读一下他的含义,位图算法的作用是可以用一个 **数据位** 来存放一种状态(比如 1 和 0),如果我们想知道某个数值是否已经存在,我们只需要找到对应的数据位查看该数据为的状态即可; + +# 2. 位图算法的实现 + +- 假设我们有一个数组 {1,3,7},我们需要设计一种方法来存储它,有什么方法可以佣金可能小的内存空间把它存储下来呢? +- 我们可以使用使用一个 8 位的地址空间,在从 0 到 7 这 8 个位上,我们按顺序将数值对应的位的值置为 1,得到的结果就是 01010001,如下图所示: + +![]({{site.baseurl}}/img-post/哈希-2.png) + +- 在这样一个 8 位的地址空间上,我们存储了 3 个 0 到 8 之间的数字; +- 那么我们如果想存储的数字介于 0 到 15 之间呢,比如 {1, 2, 5, 7, 11},其实解决问题的方法和上面是一样的,只不过用于存储的空间需要更长,这里我们申请一个 16 位的地址空间,然后根据位置对应的值依次重置数值; + +![]({{site.baseurl}}/img-post/哈希-3.png) + +- 位图最典型的应用,是在一段已知长度的地址空间中,用二进制方式(即 1 和 0)存储数据的状态,而一个数只有存在和不存在两个状态,这里的 1 代表存在,0 代表不存在,当我们查询的时候只需要知道目标位置上是 1 还是 0; +- 当有有多个值需要进行复杂的查询的时候,我们只需要对二进制数组进行 **位运算**,就可以很快找到我们想要的结果,这也是算法被称为 **位图** 的原因; +>关于位图算法,有一篇很经典的文章,是由 **程序员小灰** 所写的《[漫画:什么是Bitmap算法?](https://zhuanlan.zhihu.com/p/54783053?utm_source=wechat_timeline)》,建议读者朋友去读一读,看完以后对于位图算法的实现和应用将有更加清晰的认识。 + +# 3. 512M 内存查找 43 亿数据 + +- 关于位图算法,网上的文章中经常会讲到这个例子,这个例子同时 BAT 的经典算法题之一,我们也用这个例子做个讲解; +- 假设我们现在有 43 条数据,需要判断一个数据是否已经出现过,如果已经出现就丢弃,否则就作为新数据添加到 43 亿条数据中,我们该如何实现这个需求呢? +- **构建数组** + - 首先,我们需要注意 43 亿这个数字,接触过 32 位系统知识的读者可能已经注意到,2 的 32 次方 等于4294967296,约等于这里的 43 亿; + - 结合前面的位图算法相关知识,我们在这里申请一个 32 位的数组,初始状态下每个位都置 0,表示该位上为空(没有数值),当我们拿到一个数字,我们就到对应的位上把值置为 1,表示此位上对应的数值已经被添加; + - 按照上面的顺序类推,我们就构建了一个容量为 43 亿的数组,在这个数组中最大可以存储 43 亿个数值的状态; +- **查找数据** + - 当我们拿到一个新的数值的时候,我们只需要到对应的位上查看该位状态值是否为 1,就知道是否已经存在该数值; +- **内存使用情况** + - 分析下内存空间,我们申请了 32 位的内存,每个位上只有 1 和 0 两种状态,总计 2 的 32 次方个位的空间,就是 2 的 29 次方个字节,就相当于 500MB; +- 这样,我们就解决了 512M 内存查找 43 亿数据的问题,而解决问题的思路其实就是 位图 算法的思想。 + +# 4. 位图算法的应用 + +- 位图算法适用于数据量很大但数据状态又不是很多的情况,通常是用来判断某个数据存不存在的; + +- 具体应用: + - 快速查找某个数据是否在一个集合中; + - 排序; + - 求两个集合的交集、并集等; + - 操作系统中磁盘块标记; +- 在这里我们就不对这些具体应用展开来讲了,感兴趣的朋友可以自行百度谷歌了解相关知识。 + +# 5. 位图算法处理不了哈希冲突 + +- 前面讲的位图算法的内容,都是围绕 **数值** 展开的,但实际业务场景下要处理的对象很少严格意义的数值,最常见的处理对象其实是 **字符串**,那么我们该怎样将字符串映射 +- 有些人应该想到了,就是我们前面讲的哈希,使用一个哈希函数将字符串对象映到一个数组的位置上,比如我们拿到一个 url 以后,代入哈希函数得到一个数值,然后通过位图算法在数 +- 位图更节省空间,但因为处理不了哈希冲突,所以仍不能满足业务需求; + + + diff --git "a/_posts/2022-01-01-\346\225\260\346\215\256\347\273\223\346\236\204\357\274\232\345\223\210\345\270\214\350\241\250\347\232\204\347\211\271\346\200\247\344\270\216\347\274\272\351\231\267.md" "b/_posts/2022-01-01-\346\225\260\346\215\256\347\273\223\346\236\204\357\274\232\345\223\210\345\270\214\350\241\250\347\232\204\347\211\271\346\200\247\344\270\216\347\274\272\351\231\267.md" new file mode 100644 index 00000000000..b94aa0cb137 --- /dev/null +++ "b/_posts/2022-01-01-\346\225\260\346\215\256\347\273\223\346\236\204\357\274\232\345\223\210\345\270\214\350\241\250\347\232\204\347\211\271\346\200\247\344\270\216\347\274\272\351\231\267.md" @@ -0,0 +1,126 @@ +--- +layout: post +title: 数据结构:哈希表的特性与缺陷 +subtitle: 哈希冲突 & 内存溢出 +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据结构 +--- + + + +# 1. 哈希产生的背景 + +- 问题: + - 在开发过程中,我们经常需要判断一个元素是不是在一个集合里; + - 比如:我们现在有个爬虫,已经抓了 **十亿** 个 url,我们在接下来的抓取过程中,需要先判断 url 是否已经被爬过了,如果 url 已经下载过了那我们就不再爬取,那如何实现该逻辑呢? +- 思路: + - 一般的思路是将所有元素保存起来,然后将对象和所有元素的集合进行比对,链表、树等等数据结构都是这种思路; + - 但是这种思路有个问题,随着集合中元素的增加,我们需要的存储空间越来越大,检索速度也会越来越慢,简言之:资源浪费、性能低下; +>试想一下: +- 假设我们在 redis 中使用指纹完成去重过程,一个去重指纹我们按照 40Bytes 来计算, 8GB(8GB=8589934592Bytes)的内存大约可以存储 214,748,000(2亿)个指纹; +- 按照这样计算的话,如果我们的 url 数量达到十亿级的话,需要去重的指纹占用空间将达到 40GB,这对于服务器内存来说将是一场灾难; +- 针对上面的问题,我们不禁要问什么样的数据结构,可以解决 **元素增加、存储空间增大、检索速度下降** 这个问题呢? +- 这里,我们引入一种叫作哈希表(Hash table)的数据结构,它可以通过一个 Hash 函数将一个元素映射成一个位阵列(Bit Array)中的一个点,检索时我们只要看看这个点是不是 1 就知道可以集合中有没有它了; +- 具体来说: + - 之前的查找是这样一种思路:集合中拿出来一个元素,看看是否与我们要找的相等,如果不等就缩小范围继续查找,经典二叉树查、冒泡查找找其实都是为了尽可能缩小查找对象; + - 哈希表是完全另外一种思路:当我知道 key 值以后,我就可以直接计算出这个元素在集合中的位置,然后到这个位置里找到这个元素,不需要像上面那样把所有元素当做潜在查找对象; +> - 哈希的核心作用: + - 哈希的核心作用,是使元素的存储位置与它的关键码之间能够建立一一映射的关系,在查找时通过该函数可以很快找到该元素; + - 举个例子:如果全部数据是由一个个房屋组成的居民区的话,哈希就像是一个 **门牌号地址生成器**,给每一栋房子生成一个独一无二的 **门牌号地址**,当想要找某栋房屋的时候,只需要通过它的 门牌号地址 就可以知道这栋房子在什么地方,然后就能快速的找到这栋房子。 + +# 2. 哈希函数 + +- 定义: + - 给定表 M 存在函数 f(key),对任意给定的关键字值 key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表 M 为哈希(Hash)表,函数 f(key) 为哈希(Hash)函数; +- 理解: + - 哈希表(Hash table),也叫散列表,是根据关键码值(Key value)而直接进行访问的数据结构,也就是说它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。 + - 这个映射函数叫做 **哈希函数(散列函数)**,存放记录的数组叫做 **哈希表(散列表)**; + + ![]({{site.baseurl}}/img-post/哈希-1.jpg) + +- 构造哈希函数的原则是: + - 函数本身便于计算; + - 计算出来的地址分布均匀,即对任意输入 k、f(k) 对应不同地址的概率相等,目的是尽可能减少冲突; +- 哈希函数构造方法: + - 数字分析法,如数值取模; + - 平方取中法; + - 分段叠加法; + - 除留余数法; + - 伪随机数法; +- 最典型的哈希函数是 **取模法**,即 H(key) = key % p,p <= m,(m 为所有元素的个数,另外 p 可以为小于等于 m 的最大素数); +>如欲详细了解,请参阅《[https://www.cnblogs.com/zzdbullet/p/10512670.html](https://www.cnblogs.com/zzdbullet/p/10512670.html)》文章中 **哈希函数的构造方法** 相关章节内容,此处不赘述。 + +# 3. 哈希的散列性 + +- 哈希表之所以又叫 散列表,是因为哈希函数具有散列性; +- 哈希函数的散列性,指的是不同输入会均匀分布在输出域上; +- 哈希函数的作用,就是努力的把比较大的数据指向相对较小的空间中; + +>**举个栗子**: +- 输入 0-99 的 100 个数字,用哈希函数除 3 取模那输出域就是 0、1、2; +- 当我们将这 100 个数字通过哈希函数进行映射后,0、1、2 的数量都会接近 33 个,也就是分布比较均匀; +- 哈希散列性的经典应用: + - 场景:如果现在有一个超级大的文件,需要统计其中重复的字符串 ,我们要怎么办? + - 已知:可以供给1000台计算机; + - 思路:可以使用哈希的散列性对这个超大文件分流; + - **具体操作**:对大文件进行多行读取,每一行都计算哈希值,并取模 1000,则这些字符串均匀分布在 0-999 范围内; + - 给电脑编号 0-999,对应分布在一个范围内的一些字符串用同一台电脑进行处理,达到大数据分流的效果; + - 每台电脑只统计自己处理的重复字符串,最后将结果进行汇总。 + +| 电脑编号 | 文件行号 | +|:----:|:----:| +| 000 | 第 0 行、第 1000 行、第 2000 行、第 3000 行、第 4000 行、... | +| 001 | 第 1 行、第 1001 行、第 2001 行、第 3001 行、第 4001 行、... | +| 002 | 第 2 行、第 1002 行、第 2002 行、第 3002 行、第 4002 行、... | +| 003 | 第 3 行、第 1003 行、第 2003 行、第 3003 行、第 4003 行、... | +| 004 | 第 4 行、第 1004 行、第 2004 行、第 3004 行、第 4004 行、... | +| ... | ... | +| 995 | 第 995 行、第 1995 行、第 2995 行、第 3995 行、第 4995 行、... | +| 996 | 第 996 行、第 1996 行、第 2996 行、第 3996 行、第 4996 行、... | +| 997 | 第 997 行、第 1997 行、第 2997 行、第 3997 行、第 4997 行、... | +| 998 | 第 998 行、第 1998 行、第 2998 行、第 3998 行、第 4998 行、... | +| 999 | 第 999 行、第 1999 行、第 2999 行、第 3999 行、第 4999 行、... | + +# 4. 哈希函数特点 + +>- 输入的值范围可以无穷大,比如数据库记录; +- 输出的值范围却是有限的,比如上面的 0 到 999; + +>- 输入相同输出肯定相同; +- 输入不相同输出也可能相同(哈希冲突); + + +# 5. 哈希冲突 + +- 哈希冲突,简言之就是 **输入不同,但有可能得到相同的输出**; +- 哈希冲突是 Hash 最大的问题; + +#### 5.1. 哈希冲突产生的原因 + +- 为什么会有哈希冲突呢?其实就是因为哈希函数对输入值进行映射的时候,哈希表的位桶的数目远小于输入的关键字的个数,所以对于输入域的关键字来说,很可能会产生这样一种输入不同、输出却相同的情况; +- 用个形象一点的比喻来说,就是饭店来了 100 个客人,但店里却只有 10 个位置,那么每个作为上要安排 10 个人,这 10 个人在一个位子上怎么坐得下呢?这就是哈希冲突的原因。 + +#### 5.2. 哈希冲突的解决方法 + +- 开放地址法: + - 用开放地址处理冲突就是当冲突发生时,形成一个地址序列,沿着这个序列逐个深测,直到找到一个“空”的开放地址,将发生冲突的关键字值存放到该地址中去; + +- 链地址法: + - 把所有关键字为同义词的记录存储在一个线性链表中,这个链表成为同义词链表,即把具有相同哈希地址的关键字值存放在同义链表中; + +- 再哈希法: + - 再哈希法其实很简单,就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就再次哈希,直至不发生冲突位置,但这样有一个缺点,每次冲突都要重新哈希,计算时间可能会增加,而且增幅不可控; + +- 公共溢出区法 + - 公共溢出区法,将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表; + +# 6 哈希的空间复杂度太高(内存溢出) + +- 前面我们简单介绍哈希冲突以及冲突的解决方法,其实都是在为这个结论做铺垫; +- 哈希表的空间效率不够高,这是由哈希函数和哈希冲突处理的方法决定的,一般来说 **哈希冲突越低,哈希的效率越高**; +- 如何降低哈希冲突呢?在实际工作中,往往需要采用大于实际存储数据数量的哈希表,本质上来说其实就是 **以空间换时间**; +>关于哈希空间复杂度,网上的解释并不是太多,也不够清晰准确,感兴趣的朋友可以参考《[数据结构之------什么是哈希表?(哈希表是一个以空间换取时间的数据结构!!! 加快查找速度!!!)](https://www.cnblogs.com/feichengwulai/articles/3682302.html)》一文,该文对用开放地址法解决哈希冲突做一个相对来说比较详细的解说。 diff --git "a/_posts/2022-01-01-\346\225\260\346\215\256\347\273\223\346\236\204\357\274\232\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250\347\232\204\345\267\245\344\275\234\345\216\237\347\220\206.md" "b/_posts/2022-01-01-\346\225\260\346\215\256\347\273\223\346\236\204\357\274\232\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250\347\232\204\345\267\245\344\275\234\345\216\237\347\220\206.md" new file mode 100644 index 00000000000..047d72839eb --- /dev/null +++ "b/_posts/2022-01-01-\346\225\260\346\215\256\347\273\223\346\236\204\357\274\232\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250\347\232\204\345\267\245\344\275\234\345\216\237\347\220\206.md" @@ -0,0 +1,93 @@ +--- +layout: post +title: 数据结构:布隆过滤器的工作原理 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据结构 +--- + + + +# 1. 布隆过滤产生的背景 + +- 布隆过滤在位图的基础上产生 + - 对于数字的话,位图是可以处理的,因为他们是不存在冲突的,每一个数字对于一个唯一的哈希地址; + - 但是对于字符串,位图是没有办法处理的,因为字符串存在哈希冲突。 + - 因此,我们只能通过我们将一个元素经过多个散列函数映射到多个位上来降低误判的概率,布隆过滤器正是基于这个特点应用而生的。 +- 布隆过滤器的基本思想是: + - 通过一个 Hash 函数将一个元素映射成一个位图矩阵中的一个点; + - 我们只需查看这个位置是 1 还是 0 就知道集合中是否存在它,到这里读者朋友应该已经明白了布隆过滤和位图算法之间的关系; +- 但是由于哈希冲突的原因,即不同的元素经过散列函数后可能产生相同的哈希地址,我们很可能导致误判; +- 为了降低误判的概率,我们将一个元素经过多个散列函数映射到多个位上, + - 如果这几个位都为1,我们认为它是存在的; + - 如果有一位为0,我们认为它不存在。 + +# 2. 布隆过滤的工作原理 + +- 布隆过滤器(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出,由一个很长的二进制向量(位向量)和一系列随机映射函数组成,可以用于检索一个元素是否在一个集合中; +- 其工作过程,是将一个元素用多个哈希函数映射到一个位图中,分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零, 代表该元素一定不在哈希表中,否则可能在哈希表中; + +> **哈希表** 的缺点是**浪费空间**; +> +> **位图** 的缺点是 **不能处理哈希冲突**; +> +> 将 **哈希与位图结合**,即是此处要讲的布隆过滤器; +> +> 布隆过滤器的基本思想,是在位图中添加多个哈希函数,减小需要开辟的存储空间。 + +- 具体步骤: + - 我们使用 K 个哈希函数,对同一个输入元素进行求哈希值,那会得到 K 个不同的哈希值,我们分别记作 Y1,Y2,Y3,…,YK; + - 我们把这 K 个数字作为位图中的下标,将对应的 BitMap[Y1],BitMap[Y2],BitMap[Y3],…,BitMap[YK]都设置成 true,也就是说,我们用 K 个二进制位,来表示一个数字的存在; + - 当我们要查询某个数字是否存在的时候,我们用同样的 K 个哈希函数,对这个数字求哈希值,分别得到 Z1,Z2,Z3,…,ZK; + - 我们看这 K 个哈希值,对应位图中的数值是否都为 true,如果都是 true,则说明这个数字存在,如果有其中任意一个不为 true,那就说明这个数字不存在。 + + ![]({{site.baseurl}}/img-post/布隆过滤-1.png) + + +- 误判问题: + - 在上面的步骤中有个问题,如果某个数字经过布隆过滤器判断不存在,那说明这个数字真的不存在,不会发生误判; + - 如果某个数字经过布隆过滤器判断存在,这个时候才会有可能误判,有可能并不存在; + - 不过,只要我们调整哈希函数的个数、位图大小跟要存储数字的个数之间的比例,那就可以将这种误判的概率降到非常低。 + + +# 3. 布隆过滤器优点 + +- 优点是空间效率和查询时间都远远超过一般的算法; + - 布隆过滤器存储空间和插入/查询时间都是常数,增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关; + - 布隆过滤器的特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函 数,将一个数据映射到位图结构中,此种方式不仅可以提升查询效率,也可以节省大量的内存空间; +- 另外, Hash 函数相互之间没有关系,方便由硬件并行实现; +- 布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势; +- 数据量很大时,布隆过滤器可以表示**全集**,其他数据结构不能; +- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算; + + +# 4. 布隆过滤器的误判问题 + +- 所谓误判,就是要查找的元素并不存在,但是以前插入的数据刚好将 key 经过几个映射函数的 bit 位置为 1,这就造成一种假象,要查找的数据存在; +- 当输入的数据量比较小的时候,这种情况发生的概率还是比较低的,但是当输入的数据量很大的时候,这种错误率就明显上升了; +- 误判率的大小与 seeds 的数量、申请的内存大小、去重对象的数量有关,下面的表格展示了几个元素之间的关系: + - m 表示内存大小(多少个位),n 表示去重对象的数量,k 表示 seed 的个数; + - 假如代码中申请了 512M 内存,即1<<32(m=2^32=4294967296,约43亿),seed 设置了7个。看 k=7 那一列,当漏失率为 8.56e-05 时,m/n 值为 23。所以 n = 43/23 = 1.86(亿),表示漏失概率为 8.56e-05 时,512M 内存可满足 1.86 亿条字符串的去重; + + ![]({{site.baseurl}}/img-post/布隆过滤-2.png) + +- 解决布隆过滤器误判方法 + - TODO + +# 5. 布隆过滤器应用场景 + +- 在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省; +- 对于爬虫工程师来说,布隆过滤器的典型应用是 **高并发爬虫去重**,正如文章开始时举的例子,如果爬虫的请求数量达到 **亿计、十亿计** 这个量级,那么布隆去重将是去重策略的首选; +- 除了爬虫之外,布隆过滤器广泛应用于数据库设计,例如: + - Google 著名的分布式数据库 Bigtable 使用了布隆过滤器来查找不存在的行或列,以减少磁盘查找的IO次数; +- 在很多 Key-Value 系统中也使用了布隆过滤器来加快查询过程,如 Hbase,Accumulo,Leveldb,一般而言 Value 保存在磁盘中,访问磁盘需要花费大量时间,然而使用布隆过滤器可以快速判断某个 Key 对应的 Value 是否存在,因此可以避免很多不必要的磁盘IO操作,只是引入布隆过滤器会带来一定的内存消耗; +>注意:Bloom Filter 不适合那些 “零错误” 的应用场合; +- 缺点 + - 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中,接下来会细讲; + - 不能获取元素本身; + - 一般情况下不能从布隆过滤器中删除元素; + - 如果采用计数方式删除,可能会存在计数回绕问题; diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232ETL \346\225\260\346\215\256\346\265\201\345\220\221.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232ETL \346\225\260\346\215\256\346\265\201\345\220\221.md" new file mode 100644 index 00000000000..505f5ec4b71 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232ETL \346\225\260\346\215\256\346\265\201\345\220\221.md" @@ -0,0 +1,586 @@ +--- +layout: post +title: 用户画像:ETL 数据流向 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +#### 数据流向 + +![]({{site.baseurl}}/img-post/用户画像-5.png) + +- 从项目全貌来说,首先是收集日志和属性数据,从线上业务库抽取业务数据、或从日志服务器抽取日志数据,把抽取的数据放到数据仓库里。 +- 然后基于收仓的数据打标签、建模和行为主题的宽表建立。 +- 数据仓库层面一般分多层,比如ods做基础标签,dws层做直接推送到服务层的数据。 +- 业务数据库接入的数据、爬虫外部抓取的数据都放在数仓的ods层。 +- 基于ods层数据围绕应用场景进行数据建模后,把数据模型结果写入到dws层,然后推送到各服务层需要调用的数据库,以备线上OLAP分析、接口服务的调用。 + +#### 数据导入 + +![]({{site.baseurl}}/img-post/用户画像etl-1.png) + +#### 数据源表 + +- 订单商品表:tbl_goods + + ```aidl + CREATE TABLE `tbl_goods` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `siteId` int(10) unsigned NOT NULL, + `isTest` tinyint(1) unsigned NOT NULL COMMENT '是否是测试网单', + `hasRead` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已读,测试字段', + `supportOneDayLimit` tinyint(1) unsigned NOT NULL COMMENT '是否支持24小时限时达', + `orderId` int(10) unsigned NOT NULL, + `cOrderSn` varchar(50) NOT NULL COMMENT 'child order sn 子订单编码 C0919293', + `isBook` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否是预订网单', + `cPaymentStatus` smallint(3) unsigned NOT NULL COMMENT '子订单付款状态', + `cPayTime` int(10) unsigned NOT NULL COMMENT '子订单付款时间', + `productType` varchar(50) NOT NULL COMMENT '商品类型', + `productId` int(10) unsigned NOT NULL COMMENT '抽象商品id(可能是商品规格,也可能是套装,由商品类型决定)', + `productName` varchar(100) NOT NULL COMMENT '商品名称:可能是商品名称加颜色规格,也可能是套装名称', + `sku` varchar(60) NOT NULL COMMENT '货号', + `price` decimal(10,2) unsigned NOT NULL COMMENT '商品单价', + `number` smallint(5) unsigned NOT NULL COMMENT '数量', + `lockedNumber` int(10) unsigned NOT NULL COMMENT '曾经锁定的库存数量', + `unlockedNumber` int(10) unsigned NOT NULL COMMENT '曾经解锁的库存数量', + `productAmount` decimal(10,2) NOT NULL COMMENT '此字段专为同步外部订单而加,商品总金额=price*number+shippingFee-优惠金额,但优惠金额没在本系统存储', + `balanceAmount` decimal(10,2) unsigned NOT NULL COMMENT '余额扣减', + `couponAmount` decimal(10,2) unsigned NOT NULL COMMENT '优惠券抵扣金额', + `esAmount` decimal(10,2) unsigned NOT NULL COMMENT '节能补贴金额', + `giftCardNumberId` int(10) unsigned NOT NULL COMMENT '礼品卡号ID,关联GiftCardNumbers表的主键', + `usedGiftCardAmount` decimal(10,2) unsigned NOT NULL COMMENT '礼品卡抵用的金额', + `couponLogId` int(10) unsigned NOT NULL COMMENT '使用的优惠券记录ID', + `activityPrice` decimal(10,2) unsigned NOT NULL COMMENT '活动价,当有活动价时price的值来源于activityPrice', + `activityId` int(10) unsigned NOT NULL COMMENT '活动ID', + `cateId` int(11) NOT NULL COMMENT '分类ID', + `brandId` int(11) NOT NULL COMMENT '品牌ID', + `netPointId` int(10) NOT NULL COMMENT '网点id', + `shippingFee` decimal(10,2) NOT NULL COMMENT '配送费用', + `settlementStatus` tinyint(1) NOT NULL COMMENT '结算状态0 未结算 1已结算 ', + `receiptOrRejectTime` int(10) unsigned NOT NULL COMMENT '确认收货时间或拒绝收货时间', + `isWmsSku` tinyint(1) NOT NULL COMMENT '是否淘宝小家电', + `sCode` varchar(10) NOT NULL COMMENT '库位编码', + `tsCode` varchar(10) NOT NULL DEFAULT '' COMMENT '转运库房编码', + `tsShippingTime` int(11) NOT NULL DEFAULT '0' COMMENT '转运时效(小时)', + `status` smallint(3) NOT NULL COMMENT '状态', + `productSn` varchar(60) NOT NULL COMMENT '商品条形码', + `invoiceNumber` varchar(60) NOT NULL COMMENT '运单号', + `expressName` varchar(255) NOT NULL COMMENT '快递公司', + `invoiceExpressNumber` varchar(60) NOT NULL COMMENT '发票快递单号', + `postMan` varchar(20) NOT NULL COMMENT '送货人', + `postManPhone` varchar(15) NOT NULL COMMENT '送货人电话', + `isNotice` smallint(5) NOT NULL COMMENT '是否开启预警', + `noticeType` smallint(5) NOT NULL, + `noticeRemark` varchar(255) NOT NULL, + `noticeTime` varchar(8) NOT NULL COMMENT '预警时间', + `shippingTime` int(10) NOT NULL COMMENT '发货时间', + `lessOrderSn` varchar(50) NOT NULL COMMENT 'less 订单号', + `waitGetLesShippingInfo` tinyint(1) unsigned NOT NULL COMMENT '是否等待获取LES物流配送节点信息', + `getLesShippingCount` int(10) unsigned NOT NULL COMMENT '已获取LES配送节点信息的次数', + `outping` varchar(20) NOT NULL COMMENT '出库凭证', + `lessShipTime` int(10) NOT NULL COMMENT 'less出库时间', + `closeTime` int(10) unsigned NOT NULL COMMENT '网单完成关闭或取消关闭时间', + `isReceipt` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '是否需要发票', + `isMakeReceipt` int(1) NOT NULL DEFAULT '1' COMMENT '开票状态', + `receiptNum` text NOT NULL COMMENT '发票号', + `receiptAddTime` varchar(30) NOT NULL COMMENT '开票时间', + `makeReceiptType` tinyint(3) NOT NULL COMMENT '开票类型 0 初始值 1 库房开票 2 共享开票', + `shippingMode` varchar(60) NOT NULL COMMENT '物流模式,值为B2B2C或B2C', + `lastTimeForShippingMode` int(10) unsigned NOT NULL COMMENT '最后修改物流模式的时间', + `lastEditorForShippingMode` varchar(200) NOT NULL COMMENT '最后修改物流模式的管理员', + `systemRemark` text NOT NULL COMMENT '系统备注,不给用户显示', + `tongshuaiWorkId` int(11) NOT NULL DEFAULT '-1' COMMENT '统帅定制作品ID', + `orderPromotionId` int(10) unsigned NOT NULL COMMENT '下单立减活动ID', + `orderPromotionAmount` decimal(10,2) unsigned NOT NULL COMMENT '下单立减金额', + `externalSaleSettingId` int(10) unsigned NOT NULL COMMENT '淘宝套装设置ID', + `recommendationId` int(10) unsigned NOT NULL COMMENT '推荐购买ID', + `hasSendAlertNum` tinyint(1) unsigned NOT NULL COMMENT '是否已发送了购买数据报警邮件(短信)', + `isNoLimitStockProduct` tinyint(1) unsigned NOT NULL COMMENT '是否是无限制库存量的商品', + `hpRegisterDate` int(11) DEFAULT '0' COMMENT 'HP注册时间', + `hpFailDate` int(11) DEFAULT '0' COMMENT 'HP派工失败时间', + `hpFinishDate` int(11) DEFAULT '0' COMMENT 'HP派工成功时间', + `hpReservationDate` int(11) NOT NULL DEFAULT '0' COMMENT 'HP回传预约送货时间', + `shippingOpporunity` tinyint(4) NOT NULL DEFAULT '0' COMMENT '活动订单发货时机,0定金发货 1尾款发货', + `isTimeoutFree` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否超时免单 0未设置 1是 2否', + `itemShareAmount` decimal(10,2) DEFAULT '0.00' COMMENT '订单优惠价格分摊', + `lessShipTInTime` int(10) DEFAULT '0' COMMENT 'less转运入库时间', + `lessShipTOutTime` int(10) DEFAULT '0' COMMENT 'less转运出库时间', + `cbsSecCode` varchar(10) DEFAULT '' COMMENT 'cbs库位', + `points` int(11) DEFAULT '0' COMMENT '网单使用积分', + `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', + `splitFlag` tinyint(3) unsigned NOT NULL COMMENT '拆单标志,0:未拆单;1:拆单后旧单;2:拆单后新单', + `splitRelateCOrderSn` varchar(50) NOT NULL COMMENT '拆单关联单号', + `channelId` tinyint(4) DEFAULT '0' COMMENT '区分EP和商城', + `activityId2` int(11) NOT NULL DEFAULT '0' COMMENT '运营活动id', + `pdOrderStatus` int(4) NOT NULL DEFAULT '0' COMMENT '日日单状态', + `omsOrderSn` varchar(20) NOT NULL DEFAULT '' COMMENT '集团OMS单号', + `couponCode` varchar(20) NOT NULL DEFAULT '' COMMENT '优惠码编码', + `couponCodeValue` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠码优惠金额', + `storeId` int(10) unsigned NOT NULL DEFAULT '0', + `storeType` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '店铺类型', + `stockType` varchar(10) NOT NULL DEFAULT 'WA', + `o2oType` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT 'o2o网单类型1非O2O网单2线下用户分销商城3商城分销旗舰店4创客', + `brokerageType` varchar(100) DEFAULT NULL, + `ogColor` varchar(30) DEFAULT NULL COMMENT '算法预留字段', + PRIMARY KEY (`id`), + KEY `orderId` (`orderId`), + KEY `sCode` (`sCode`), + KEY `cOrderSn` (`cOrderSn`), + KEY `netPointId` (`netPointId`), + KEY `isNotice` (`isNotice`), + KEY `noticeTime` (`noticeTime`), + KEY `closeTime` (`closeTime`), + KEY `cPayTime` (`cPayTime`), + KEY `productId` (`productId`), + KEY `activityId` (`activityId`), + KEY `idx_OrderProducts_cPaymentStatus_status` (`cPaymentStatus`,`status`), + KEY `idx_OrderProducts_waitGetLesShippingInfo` (`waitGetLesShippingInfo`), + KEY `idx_OrderProducts_status` (`status`), + KEY `idx_OrderProducts_sku` (`sku`), + KEY `ix_OrderProducts_lessShipTime` (`lessShipTime`), + KEY `modified` (`modified`), + KEY `ix_OrderProducts_receiptAddTime` (`receiptAddTime`), + KEY `idx_pdOrderStatus` (`pdOrderStatus`), + KEY `idx_lessShipTInTime` (`lessShipTInTime`) + ) ENGINE=InnoDB AUTO_INCREMENT=3853572 DEFAULT CHARSET=utf8; + ``` + +- 用户表:tbl_users + + ```aidl + CREATE TABLE `tbl_users` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `siteId` int(10) unsigned NOT NULL, + `avatarImageFileId` varchar(255) DEFAULT NULL, + `email` varchar(120) DEFAULT NULL, + `username` varchar(60) NOT NULL COMMENT '用户名', + `password` varchar(32) DEFAULT NULL COMMENT '密码', + `salt` varchar(10) DEFAULT NULL COMMENT '扰码', + `registerTime` int(10) unsigned NOT NULL COMMENT '注册时间', + `lastLoginTime` int(10) unsigned DEFAULT NULL COMMENT '最后登录时间', + `lastLoginIp` varchar(15) DEFAULT NULL COMMENT '最后登录ip', + `memberRankId` int(10) unsigned DEFAULT NULL COMMENT '特殊会员等级id,0表示非特殊会员等级', + `bigCustomerId` int(10) unsigned DEFAULT NULL COMMENT '所属的大客户ID', + `lastAddressId` int(10) unsigned DEFAULT NULL COMMENT '上次使用的收货地址', + `lastPaymentCode` varchar(20) DEFAULT NULL COMMENT '上次使用的支付方式', + `gender` tinyint(3) unsigned DEFAULT NULL COMMENT '性别:0保密1男2女', + `birthday` date DEFAULT NULL COMMENT '生日', + `qq` varchar(20) DEFAULT NULL, + `job` varchar(60) DEFAULT NULL COMMENT '职业;1学生、2公务员、3军人、4警察、5教师、6白领', + `mobile` varchar(15) DEFAULT NULL COMMENT '手机', + `politicalFace` int(1) unsigned DEFAULT NULL COMMENT '政治面貌:1群众、2党员、3无党派人士', + `nationality` varchar(20) DEFAULT NULL COMMENT '国籍:1中国大陆、2中国香港、3中国澳门、4中国台湾、5其他', + `validateCode` varchar(32) DEFAULT NULL COMMENT '找回密码时的验证code', + `pwdErrCount` tinyint(3) DEFAULT NULL COMMENT '密码输入错误次数', + `source` varchar(20) DEFAULT NULL COMMENT '会员来源', + `marriage` varchar(60) DEFAULT NULL COMMENT '婚姻状况:1未婚、2已婚、3离异', + `money` decimal(15,2) DEFAULT NULL COMMENT '账户余额', + `moneyPwd` varchar(32) DEFAULT NULL COMMENT '余额支付密码', + `isEmailVerify` tinyint(1) DEFAULT NULL COMMENT '是否验证email', + `isSmsVerify` tinyint(1) DEFAULT NULL COMMENT '是否验证短信', + `smsVerifyCode` varchar(30) DEFAULT NULL COMMENT '邮件验证码', + `emailVerifyCode` varchar(30) DEFAULT NULL COMMENT '短信验证码', + `verifySendCoupon` tinyint(1) DEFAULT NULL COMMENT '是否验证发送优惠券', + `canReceiveEmail` tinyint(1) DEFAULT NULL COMMENT '是否接收邮件', + `modified` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', + `channelId` tinyint(4) DEFAULT '0' COMMENT '??EP???', + `grade_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '等级ID', + `nick_name` varchar(60) NOT NULL DEFAULT '' COMMENT '昵称', + `is_blackList` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否黑名单 : 0:非黑名单 1:黑名单', + PRIMARY KEY (`id`), + KEY `siteId` (`siteId`,`email`), + KEY `memberRankId` (`memberRankId`) + ) ENGINE=InnoDB AUTO_INCREMENT=951 DEFAULT CHARSET=utf8; + ``` + +- 行为日志表:tbl_logs + + ```aidl + CREATE TABLE `tbl_logs` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `log_id` varchar(50) DEFAULT NULL, + `remote_ip` varchar(50) DEFAULT NULL, + `site_global_ticket` varchar(250) DEFAULT NULL, + `site_global_session` varchar(250) DEFAULT NULL, + `global_user_id` varchar(50) DEFAULT NULL, + `cookie_text` mediumtext, + `user_agent` varchar(250) DEFAULT NULL, + `ref_url` varchar(250) DEFAULT NULL, + `loc_url` varchar(250) DEFAULT NULL, + `log_time` varchar(50) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `log_time` (`log_time`) + ) ENGINE=MyISAM AUTO_INCREMENT=1160286 DEFAULT CHARSET=utf8; + ``` + +- 订单数据表:tbl_orders + + ```aidl + CREATE TABLE `tbl_orders` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `siteId` int(10) unsigned NOT NULL, + `isTest` tinyint(1) unsigned NOT NULL COMMENT '是否是测试订单', + `hasSync` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已同步(临时添加)', + `isBackend` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '是否为后台添加的订单', + `isBook` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '是否为后台添加的订单', + `isCod` tinyint(1) unsigned NOT NULL COMMENT '是否是货到付款订单', + `notAutoConfirm` tinyint(1) unsigned NOT NULL COMMENT '是否是非自动确认订单', + `isPackage` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否为套装订单', + `packageId` int(10) unsigned NOT NULL COMMENT '套装ID', + `orderSn` varchar(50) NOT NULL COMMENT '订单号', + `relationOrderSn` varchar(50) NOT NULL COMMENT '关联订单编号', + `memberId` int(10) unsigned NOT NULL COMMENT '会员id', + `predictId` int(10) unsigned NOT NULL COMMENT '会员购买预测ID', + `memberEmail` varchar(120) NOT NULL COMMENT '会员邮件', + `addTime` int(10) unsigned NOT NULL, + `syncTime` int(10) unsigned NOT NULL COMMENT '同步到此表中的时间', + `orderStatus` smallint(3) NOT NULL COMMENT '订单状态', + `payTime` int(10) unsigned NOT NULL COMMENT '在线付款时间', + `paymentStatus` smallint(3) unsigned NOT NULL COMMENT '付款状态:0 买家未付款 1 买家已付款 ', + `receiptConsignee` varchar(20) NOT NULL COMMENT '发票收件人', + `receiptAddress` varchar(255) NOT NULL COMMENT '发票地址', + `receiptZipcode` varchar(20) NOT NULL COMMENT '发票邮编', + `receiptMobile` varchar(30) NOT NULL COMMENT '发票联系电话', + `productAmount` decimal(10,2) unsigned NOT NULL COMMENT '商品金额,等于订单中所有的商品的单价乘以数量之和', + `orderAmount` decimal(10,2) unsigned NOT NULL COMMENT '订单总金额,等于商品总金额+运费', + `paidBalance` decimal(10,2) unsigned NOT NULL COMMENT '余额账户支付总金额', + `giftCardAmount` decimal(10,2) unsigned NOT NULL COMMENT '礼品卡抵用金额', + `paidAmount` decimal(10,2) unsigned NOT NULL COMMENT '已支付金额', + `shippingAmount` decimal(10,2) NOT NULL COMMENT '淘宝运费', + `totalEsAmount` decimal(10,2) unsigned NOT NULL COMMENT '网单中总的节能补贴金额之和', + `usedCustomerBalanceAmount` decimal(10,2) unsigned NOT NULL COMMENT '使用的客户的余额支付金额', + `customerId` int(10) unsigned NOT NULL COMMENT '用余额支付的客户ID', + `bestShippingTime` varchar(100) NOT NULL COMMENT '最佳配送时间描述', + `paymentCode` varchar(20) NOT NULL COMMENT '支付方式code', + `payBankCode` varchar(20) NOT NULL COMMENT '网银代码', + `paymentName` varchar(60) NOT NULL COMMENT '支付方式名称', + `consignee` varchar(60) NOT NULL COMMENT '收货人', + `originRegionName` varchar(255) NOT NULL COMMENT '原淘宝收货地址信息', + `originAddress` varchar(255) NOT NULL COMMENT '原淘宝收货人详细收货信息', + `province` int(10) unsigned NOT NULL COMMENT '收货地址中国省份', + `city` int(10) unsigned NOT NULL COMMENT '收货地址中的城市', + `region` int(10) unsigned NOT NULL COMMENT '收货地址中城市中的区', + `street` int(10) unsigned NOT NULL COMMENT '街道ID', + `markBuilding` int(10) NOT NULL COMMENT '标志建筑物', + `poiId` varchar(64) NOT NULL DEFAULT '' COMMENT '标建ID', + `poiName` varchar(100) DEFAULT '' COMMENT '标建名称', + `regionName` varchar(200) NOT NULL COMMENT '地区名称(如:北京 北京 昌平区 兴寿镇)', + `address` varchar(255) NOT NULL COMMENT '收货地址中用户输入的地址,一般是区以下的详细地址', + `zipcode` varchar(20) NOT NULL COMMENT '收货地址中的邮编', + `mobile` varchar(15) NOT NULL COMMENT '收货人手机号', + `phone` varchar(20) NOT NULL COMMENT '收货人固定电话号', + `receiptInfo` text NOT NULL COMMENT '发票信息,序列化数组array(''title'' =>.., ''receiptType'' =>..,''needReceipt'' => ..,''companyName'' =>..,''taxSpotNum'' =>..,''regAddress''=>..,''regPhone''=>..,''bank''=>..,''bankAccount''=>..)', + `delayShipTime` int(10) unsigned NOT NULL COMMENT '延迟发货日期', + `remark` text NOT NULL COMMENT '订单备注', + `bankCode` varchar(255) DEFAULT NULL COMMENT '银行代码,用于银行直链支付', + `agent` varchar(255) DEFAULT NULL COMMENT '处理人', + `confirmTime` int(11) DEFAULT NULL COMMENT '确认时间', + `firstConfirmTime` int(10) unsigned NOT NULL COMMENT '首次确认时间', + `firstConfirmPerson` varchar(200) NOT NULL COMMENT '第一次确认人', + `finishTime` int(11) DEFAULT NULL COMMENT '订单完成时间', + `tradeSn` varchar(255) DEFAULT NULL COMMENT '在线支付交易流水号', + `signCode` varchar(20) NOT NULL COMMENT '收货确认码', + `source` varchar(30) NOT NULL COMMENT '订单来源', + `sourceOrderSn` varchar(60) NOT NULL COMMENT '外部订单号', + `onedayLimit` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否支持24小时限时达', + `logisticsManner` int(1) NOT NULL COMMENT '物流评价', + `afterSaleManner` int(1) NOT NULL COMMENT '售后评价', + `personManner` int(1) NOT NULL COMMENT '人员态度', + `visitRemark` varchar(400) NOT NULL COMMENT '回访备注', + `visitTime` int(11) NOT NULL COMMENT '回访时间', + `visitPerson` varchar(20) NOT NULL COMMENT '回访人', + `sellPeople` varchar(20) NOT NULL COMMENT '销售代表', + `sellPeopleManner` int(1) NOT NULL COMMENT '销售代表服务态度', + `orderType` tinyint(2) NOT NULL COMMENT '订单类型 默认0 团购预付款 团购正式单 2', + `hasReadTaobaoOrderComment` tinyint(1) unsigned NOT NULL COMMENT '是否已读取过淘宝订单评论', + `memberInvoiceId` int(10) unsigned NOT NULL COMMENT '订单发票ID,MemberInvoices表的主键', + `taobaoGroupId` int(10) unsigned NOT NULL COMMENT '淘宝万人团活动ID', + `tradeType` varchar(100) NOT NULL COMMENT '交易类型,值参考淘宝', + `stepTradeStatus` varchar(100) NOT NULL COMMENT '分阶段付款的订单状态,值参考淘宝', + `stepPaidFee` decimal(10,2) NOT NULL COMMENT '分阶段付款的已付金额', + `depositAmount` decimal(10,2) unsigned NOT NULL COMMENT '定金应付金额', + `balanceAmount` decimal(10,2) unsigned NOT NULL COMMENT '尾款应付金额', + `autoCancelDays` int(10) unsigned NOT NULL COMMENT '未付款过期的天数', + `isNoLimitStockOrder` tinyint(1) unsigned NOT NULL COMMENT '是否是无库存限制订单', + `ccbOrderReceivedLogId` int(10) unsigned NOT NULL COMMENT '建行订单接收日志ID', + `ip` varchar(50) NOT NULL COMMENT '订单来源IP,针对商城前台订单', + `isGiftCardOrder` tinyint(1) unsigned NOT NULL COMMENT '是否为礼品卡订单', + `giftCardDownloadPassword` varchar(200) NOT NULL COMMENT '礼品卡下载密码', + `giftCardFindMobile` varchar(20) NOT NULL COMMENT '礼品卡密码找回手机号', + `autoConfirmNum` int(10) unsigned NOT NULL COMMENT '已自动确认的次数', + `codConfirmPerson` varchar(100) NOT NULL COMMENT '货到付款确认人', + `codConfirmTime` int(11) NOT NULL COMMENT '货到付款确认时间', + `codConfirmRemark` varchar(255) NOT NULL COMMENT '货到付款确认备注', + `codConfirmState` tinyint(1) unsigned NOT NULL COMMENT '货到侍确认状态0无需未确认,1待确认,2确认通过可以发货,3确认无效,订单可以取消', + `paymentNoticeUrl` text NOT NULL COMMENT '付款结果通知URL', + `addressLon` decimal(9,6) NOT NULL COMMENT '地址经度', + `addressLat` decimal(9,6) NOT NULL COMMENT '地址纬度', + `smConfirmStatus` tinyint(4) NOT NULL COMMENT '标建确认状态。1 = 初始状态;2 = 已发HP,等待确认;3 = 待人工处理;4 = 待自动处理;5 = 已确认', + `smConfirmTime` int(10) NOT NULL COMMENT '请求发送HP时间,格式为时间戳', + `smManualTime` int(10) DEFAULT '0' COMMENT '转人工确认时间', + `smManualRemark` varchar(200) DEFAULT '' COMMENT '转人工确认备注', + `isTogether` tinyint(3) unsigned NOT NULL COMMENT '货票通行', + `isNotConfirm` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否是无需确认的订单', + `tailPayTime` int(11) NOT NULL DEFAULT '0' COMMENT '尾款付款时间', + `points` int(11) DEFAULT '0' COMMENT '网单使用积分', + `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', + `channelId` tinyint(4) DEFAULT '0' COMMENT '区分EP和商城', + `isProduceDaily` int(2) NOT NULL DEFAULT '0' COMMENT '是否日日单(1:是,0:否)', + `couponCode` varchar(20) NOT NULL DEFAULT '' COMMENT '优惠码编码', + `couponCodeValue` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠码优惠金额', + `ckCode` varchar(200) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY `ux_Orders_ordersn` (`orderSn`), + KEY `memberId` (`memberId`), + KEY `agent` (`agent`), + KEY `addTime` (`addTime`), + KEY `payTime` (`payTime`), + KEY `orderStatus` (`orderStatus`), + KEY `sourceOrderSn` (`sourceOrderSn`), + KEY `smConfirmStatus` (`smConfirmStatus`), + KEY `idx_orders_source_orderStatus_hasReadTaobaoOrderComment` (`source`,`orderStatus`,`hasReadTaobaoOrderComment`), + KEY `idx_order_mobile` (`mobile`), + KEY `idx_orders_codConfirmState` (`codConfirmState`), + KEY `modified` (`modified`), + KEY `tailPayTime` (`tailPayTime`), + KEY `ix_Orders_syncTime` (`syncTime`), + KEY `ix_Orders_relationOrderSn` (`relationOrderSn`), + KEY `ix_Orders_consignee` (`consignee`), + KEY `idx_firstConfirmTime` (`firstConfirmTime`), + KEY `ix_Orders_paymentStatus` (`paymentStatus`) + ) ENGINE=InnoDB AUTO_INCREMENT=120128 DEFAULT CHARSET=utf8; + ``` + + +#### 同步源数据至 Hive 数据仓库 + +- 创建表 + + - 行为日志表:tbl_logs + + ```/export/servers/sqoop/bin/sqoop create-hive-table \ + --connect jdbc:mysql://bd001:3306/tags_dat \ + --table tbl_logs \ + --username root \ + --password 123456 \ + --hive-table tags_dat2.tbl_logs \ + --fields-terminated-by '\t' \ + --lines-terminated-by '\n' + ``` + + - 商品表:tbl_goods + ```/export/servers/sqoop/bin/sqoop create-hive-table \ + --connect jdbc:mysql://bd001:3306/tags_dat \ + --table tbl_goods \ + --username root \ + --password 123456 \ + --hive-table tags_dat2.tbl_goods \ + --fields-terminated-by '\t' \ + --lines-terminated-by '\n' + ``` + + - 订单数据表:tbl_orders + ``` + /export/servers/sqoop/bin/sqoop create-hive-table \ + --connect jdbc:mysql://bd001:3306/tags_dat \ + --table tbl_orders \ + --username root \ + --password 123456 \ + --hive-table tags_dat2.tbl_orders \ + --fields-terminated-by '\t' \ + --lines-terminated-by '\n' + ``` + + - 用户信息表:tbl_users + ```/export/servers/sqoop/bin/sqoop create-hive-table \ + --connect jdbc:mysql://bd001:3306/tags_dat \ + --table tbl_users \ + --username root \ + --password 123456 \ + --hive-table tags_dat2.tbl_users \ + --fields-terminated-by '\t' \ + --lines-terminated-by '\n' + ``` + +- 导入数据至 Hive 表 + - 使用 Sqoop 将 MySQL 数据库表中的数据导入到 Hive 表中(本质就是存储在HDFS上)。 + ![]({{site.baseurl}}/img-post/用户画像etl-2.png) + - 行为日志表:tbl_logs + ``` + /export/servers/sqoop/bin/sqoop import \ + --connect jdbc:mysql://bd001:3306/tags_dat \ + --username root \ + --password 123456 \ + --table tbl_logs \ + --direct \ + --hive-overwrite \ + --delete-target-dir \ + --fields-terminated-by '\t' \ + --lines-terminated-by '\n' \ + --hive-table tags_dat2.tbl_logs \ + --hive-import \ + --num-mappers 20 + ``` + + - 商品表:tbl_goods + ``` + /export/servers/sqoop/bin/sqoop import \ + --connect jdbc:mysql://bd001:3306/tags_dat \ + --username root \ + --password 123456 \ + --table tbl_goods \ + --direct \ + --hive-overwrite \ + --delete-target-dir \ + --fields-terminated-by '\t' \ + --lines-terminated-by '\n' \ + --hive-table tags_dat2.tbl_goods \ + --hive-import \ + --num-mappers 5 + ``` + + - 订单数据表:tbl_orders + ``` + /export/servers/sqoop/bin/sqoop import \ + --connect jdbc:mysql://bd001:3306/tags_dat \ + --username root \ + --password 123456 \ + --table tbl_orders \ + --direct \ + --hive-overwrite \ + --delete-target-dir \ + --fields-terminated-by '\t' \ + --lines-terminated-by '\n' \ + --hive-table tags_dat2.tbl_orders \ + --hive-import \ + --num-mappers 10 + ``` + + - 用户信息表:tbl_users + ``` + /export/servers/sqoop/bin/sqoop import \ + --connect jdbc:mysql://bd001:3306/tags_dat \ + --username root \ + --password 123456 \ + --table tbl_users \ + --direct \ + --hive-overwrite \ + --delete-target-dir \ + --fields-terminated-by '\t' \ + --lines-terminated-by '\n' \ + --hive-table tags_dat2.tbl_users \ + --hive-import \ + --num-mappers 1 + ``` + +#### 数据导入 HBase + +![]({{site.baseurl}}/img-post/用户画像etl-3.png) + +- Sqoop 直接导入 + - 可以使用 SQOOP 将 MySQL 表的数据导入到 HBase 表中,指定表的名称、列簇及 RowKey; + - 范例如下所示: + ``` + /export/servers/sqoop/bin/sqoop import \ + -D sqoop.hbase.add.row.key=true \ + --connect jdbc:mysql://bd001:3306/tags_dat \ + --username root \ + --password 123456 \ + --table tbl_users \ + --hbase-create-table \ + --hbase-table tbl_users2 \ + --column-family detail \ + --hbase-row-key id \ + --num-mappers 2 + ``` + - 参数含义解释: + ``` + -D sqoop.hbase.add.row.key=true 是否将rowkey相关字段写入列族中,默认为false,默认情况下你将在列族中看不到任何row key中的字段。注意,该参数必须放在import之后。 + + --hbase-create-table 如果hbase中该表不存在则创建 + + --hbase-table 对应的hbase表名 + + --hbase-row-key hbase表中的rowkey,注意格式 + + --column-family hbase表的列族 + ``` + +- HBase ImportTSV + - ImportTSV功能描述: + - 将tsv(也可以是csv,每行数据中各个字段使用分隔符分割)格式文本数据,加载到HBase表中。 + + - 直接导入Put方式 + ``` + HADOOP_HOME=/export/servers/hadoop + HBASE_HOME=/export/servers/hbase + HADOOP_CLASSPATH=`${HBASE_HOME}/bin/hbase mapredcp`:${HBASE_HOME}/conf ${HADOOP_HOME}/bin/yarn jar ${HBASE_HOME}/lib/hbase-server-1.2.0-cdh5.14.0.jar \ + importtsv \ + -Dimporttsv.columns=HBASE_ROW_KEY,detail:log_id,detail:remote_ip,detail:site_global_ticket,detail:site_global_session,detail:global_user_id,detail:cookie_text,detail:user_agent,detail:ref_url,detail:loc_url,detail:log_time \ + tbl_logs2 \ + /user/hive/warehouse/tags_dat2.db/tbl_logs + ``` + + - 上述命令本质上运行一个 MapReduce 应用程序,将文本文件中每行数据转换封装到 Put 对象,然后插入到 HBase 表中。 + + - 转换为 HFile 文件,再加载至表 + - 生成 HFILES 文件 + ``` + HADOOP_HOME=/export/servers/hadoop + HBASE_HOME=/export/servers/hbase + HADOOP_CLASSPATH=`${HBASE_HOME}/bin/hbase mapredcp`:${HBASE_HOME}/conf ${HADOOP_HOME}/bin/yarn jar ${HBASE_HOME}/lib/hbase-server-1.2.0-cdh5.14.0.jar \ + importtsv \ + -Dimporttsv.bulk.output=hdfs://bd001:8020/datas/output_hfile/tbl_tag_logs \ + -Dimporttsv.columns=HBASE_ROW_KEY,detail:log_id,detail:remote_ip,detail:site_global_ticket,detail:site_global_session,detail:global_user_id,detail:cookie_text,detail:user_agent,detail:ref_url,detail:loc_url,detail:log_time \ + tbl_logs2 \ + /user/hive/warehouse/tags_dat2.db/tbl_logs + ``` + - 将 HFILE 文件加载到表中 + ``` + HADOOP_CLASSPATH=`${HBASE_HOME}/bin/hbase mapredcp`:${HBASE_HOME}/conf ${HADOOP_HOME}/bin/yarn jar \ + ${HBASE_HOME}/lib/hbase-server-1.2.0-cdh5.14.0.jar \ + completebulkload \ + hdfs://bd001:8020/datas/output_hfile/tbl_tag_logs \ + tbl_logs2 + ``` + + - 这种方法的缺点是: + - ROWKEY不能是组合主键,只能是某一个字段 + - 当表中列很多时,书写-Dimporttsv.columns值时很麻烦,容易出错 + +- HBase Bulkload + - 在大量数据需要写入HBase时,通常有 put 方式和 bulkLoad 两种方式。 + + - put方式为单条插入 + - 在 put 数据时会先将数据的更新操作信息和数据信息写入 WAL,在写入到 WAL 后,数据就会被放到 MemStore 中,当 MemStore 满后数据就会被 flush 到磁盘(即形成 HFile 文件); + - 这种写操作过程会涉及到 flush、split、compaction 等操作,容易造成节点不稳定,数据导入慢,耗费资源等问题; + - 在海量数据的导入过程极大的消耗了系统性能,避免这些问题最好的方法就是使用 BulkLoad 的方式来加载数据到HBase中。 + + ``` + val put = new Put(rowKeyByts) + put.addColumn(cf, column, value) + put.addColumn(cf, column, value) + put.addColumn(cf, column, value) + put.addColumn(cf, column, value) + + table.put(put) + ``` + + - BulkLoader + - BulkLoader 利用 HBase 数据按照 HFile 格式存储在 HDFS 的原理,使用 MapReduce 直接批量生成 HFile 格式文件后,RegionServers 再将 HFile 文件移动到相应的 Region 目录下。 + - Extract + - 异构数据源数据导入到 HDFS 之上。 + - Transform + - 通过用户代码,可以是 MR 或者 Spark 任务将数据转化为 HFile。 + - Load + - HFile 通过 loadIncrementalHFiles 调用将 HFile 放置到 Region 对应的 HDFS 目录上,该过程可能涉及到文件切分。 + - 不会触发 WAL 预写日志,当表还没有数据时进行数据导入不会产生 Flush 和 Split。 + - 减少接口调用的消耗,是一种快速写入的优化方式。 + + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232Hive \346\225\260\344\273\223\345\220\204\345\261\202\350\241\250\350\256\276\350\256\241\347\244\272\344\276\213.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232Hive \346\225\260\344\273\223\345\220\204\345\261\202\350\241\250\350\256\276\350\256\241\347\244\272\344\276\213.md" new file mode 100644 index 00000000000..eaa15296f77 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232Hive \346\225\260\344\273\223\345\220\204\345\261\202\350\241\250\350\256\276\350\256\241\347\244\272\344\276\213.md" @@ -0,0 +1,82 @@ +--- +layout: post +title: 用户画像:Hive 数仓各层表设计示例 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +# ODS 层业务数据表 + +#### 商品订单表 + +- 商品订单表(dw.order_info_fact) + + ![]({{site.baseurl}}/img-post/用户画像-24.png) + +#### 商品评论表 + +- 商品评论表(dw.book_comment) + + ![]({{site.baseurl}}/img-post/用户画像-27.png) + +#### 用户收藏表 + +- 用户收藏表(dw.book_collection_df) + + ![]({{site.baseurl}}/img-post/用户画像-29.png) + +#### 购物车信息表 + +- 购物车信息表(dw.shopping_cart_df) + + ![]({{site.baseurl}}/img-post/用户画像-30.png) + +# ODS 层用户行为数据表 + +#### 埋点日志 + +- 埋点日志表(ods.page_event_log) + + ![]({{site.baseurl}}/img-post/用户画像-25.png) + +#### 访问日志表 + +- 访问日志表(ods.page_view_log) + + ![]({{site.baseurl}}/img-post/用户画像-26.png) + +#### 搜索日志表 + +- 搜索日志表(dw.app_search_log) + + ![]({{site.baseurl}}/img-post/用户画像-28.png) + + +# DIM 层维度表 + +#### 用户信息表 + +- 用户信息表(dim.user_basic_info) + + ![]({{site.baseurl}}/img-post/用户画像-23.png) + +# ADS 层宽表 + +#### 用户基本属性信息 + +- 用户属性宽表 + + ![]({{site.baseurl}}/img-post/用户画像-31.png) + +#### 用户日活跃宽表 + +- 用户日活跃宽表 + + ![]({{site.baseurl}}/img-post/用户画像-32.png) + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232ID Mapping \347\232\204\350\256\276\350\256\241\344\270\216\345\256\236\347\216\260.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232ID Mapping \347\232\204\350\256\276\350\256\241\344\270\216\345\256\236\347\216\260.md" new file mode 100644 index 00000000000..7cbd7f4840d --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232ID Mapping \347\232\204\350\256\276\350\256\241\344\270\216\345\256\236\347\216\260.md" @@ -0,0 +1,224 @@ +--- +layout: post +title: 用户画像:ID Mapping 的设计与实现 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +# 1. ID Mapping 的概念 + +#### 1.1. 定义 + +- ID Mapping,就如同它的名字一样,咱们要做的就是将一系列的ID 关联起来, + - 一些列的ID 可能是用户在不同平台上的标识,也可能是用户在不同设施上的标识,也可能是用户在不同状态下的标识; + - ID Mapping 就是将这一系列的ID 关联起来,尽可能地将用户的数据买通,从而提供更加全面精确的剖析。 + +#### 1.2. ID Mapping 的价值 + +- ID Mapping 要做的核心工作,就是 **多端数据 的识别和 多源数据 的打通**; +- ID mapping 的建立,有助于帮助营销人员获得渠道有效性的反馈,进而可以帮助营销人员进行更加精准的投放,在获得用户画像反馈之后,也能进一步提升用户体验。 + +#### 1.3. One ID + +- ID Mapping 的最终产出是 **One ID**: + - 也就是说我们为这些 Mapping 上的 ID ⽣成⼀个新的 ID,这个ID 就是One ID,也就是说当我们的One ID 成之后我们就可以使这个 One ID 来打通所有的业务系统。 + +# 2. 常见的 ID 标识 + +#### 2.1. 设备 ID + +- Android + - 1.10.5 之前版本,默认应用 UUID(例如:550e8400-e29b-41d4-a716-446655440000),App 卸载重装 UUID 会变,为了保障设施 ID 不变,能够通过配置应用 AndroidId(例如:774d56d682e549c3); + - 1.10.5 及之后的版本 SDK 默认应用 AndroidId 作为设施 ID,如果 AndroidId 获取不到则获取随机的 UUID。 +- iOS + - 1.10.18 及之后版本,如果 App 引入了 AdSupport 库,SDK 默认应用 IDFA 作为匿名 ID。 + - 1.10.18 之前版本,能够优先应用 IDFV(例如:1E2DFA10-236A-47UD-6641-AB1FC4E6483F),如果 IDFV 获取失败,则应用随机的 UUID(例如:550e8400-e29b-41d4-a716-446655440000),个别状况下都可能获取到 IDFV。 + - 如果应用 IDFV 或 UUID ,当用户卸载重装 App 时设施 ID 会变。也能够通过配置应用 IDFA(例如:1E2DFA89-496A-47FD-9941-DF1FC4E6484A),如果开启 IDFA ,能够优先获取 IDFA,如果获取失败再尝试获取 IDFV。 + - 应用 IDFA 能防止用户在重装 App 后设施 ID 发生变化的状况,须要留神的是IDFA 也是能够被重置的 + +#### 2.2. 登录 ID + +- 登录 ID 通常是业务数据库里的主键或其它惟一标识,登录 ID 相对来说更准确更长久。 +- 然而,用户在应用时不肯注册或者登录,而这个时候是没有登录 ID 的。 + +#### 2.3. 平台 ID + +- JavaScript + - cookie_id + - 默认状况下应用 cookie_id(例如:15ffdb0a3f898-02045d1cb7be78-31126a5d-250125-15ffdb0a3fa40a),cookie_id 存贮在浏览器的 cookie 中,仍然有被重置的危险 + - 这里还有一个问题,就是 cookie 只能隶属于同一个域名,也就是说你拜访邮箱的 cookie ,与百度广的 cookie 并不是同一个,所以在网站与网站也要做 ID Mapping ,这就是为什么你在百度上搜寻了“养生”,到购物网站上就会给你举荐“枸杞”。 + +- 微信小程序 + - UUID, + - 默认状况下应用 UUID(例如:1558509239724-9278730-00c1875d5f63f8-41373096),然而删除小程序,UUID 会变。 + - openid, + - 为了保障设施 ID 不变,倡议获取并应用 openid(例如:oWDMZ0WHqfsjIz7A9B2XNQOWmN3E)。 如果抉择应用 openid 的话,请留神【操作暂存】,因为获取 openid 是一个异步的操作,然而冷启动事件等会先产生,这时候这个冷启动事件的 distinct_id 就不对了。所以咱们会把先产生的操作,暂存起来,等获取到 openid 等后调用 sa.init() 后才会发送数据。 openid 的获取和操作暂存的办法。 + +#### 2.4. 其他类型 ID + +- 账号信息 + - 微信账号 + - QQ 号 + - 邮箱 + - 手机号 + +- deviceid + - app 日志采集埋点开发人员自己定义一种逻辑id,可能取自android、imei、openudid 等,是逻辑上的 id + +- MAC(Media Access Control) + - MAC位址,为网卡的标识,唯一标识网络设备。 + +- IMEI(International Mobile Equipment Identity) + - 通常说的手机序列号、手机“串号”,在移动电话网络中识别每一部独立的手机等行动通讯装置; + - 序列号共有15位数字,前6位(TAC)是型号核准号码,代表手机类型。接着2位(FAC)是最后装配号,代表产地。后6位(SNR)是串号,代表生产顺序号。最后1位(SP)一般为0,是检验码,备用。 + +- IMSI(International Mobile SubscriberIdentification Number) + - 储存在SIM卡中,区别移动用户的有效信息;其总长度不超过15位,同样使用0~9的数字。其中: + - MCC 是移动用户所属国家代号,占3位数字,中国的MCC规定为460; + - MNC 是移动网号码,最多由两位数字组成,用于识别移动用户所归属的移动通信网; + - MSIN 是移动用户识别码,用以识别某一移动通信网中的移动用户。 + +- Android ID是系统随机生成的设备ID 为一串64位的编码(十六进制的字符串),通过它可以知道设备的寿命(在设备恢复出厂设置或刷机后,该值可能会改变)。 + +- UDID (Unique Device Identifier) + - 苹果IOS设备的唯一识别码,它由40个字符的字母和数字组成,为了保护用户隐私苹果已经禁止读取这个标识了。 + +- UUID(Universally Unique IDentifier),是基于iOS设备上面某个单个的应用程序,只要用户没有完全删除应用程序,则这个 UUID 在用户使用该应用程序的时候一直保持不变。如果用户删除了这个应用程序,然后再重新安装,那么这个 UUID 已经发生了改变。缺点是用户删除了你开发的程序后,基本上无法获取关联之前的数据。 + +- OpenUDID + - 不是苹果官方的,是一个替代 UDID 的第三发解决方案, 缺点是如果你完全删除全部带有OpenUDID SDK 包的App(比如恢复系统等),那么OpenUDID 会重新生成,而且和之前的值会不同。 + +- IDFA (广告标示符) + - 苹果禁用UDID后想出了折中办法,就是提供另外一套和硬件无关的标识符,用于给商家监测广告效果,这就是IDFA。 + - 用户可以在手机设置里改变这串字符,会导致商家没有办法长期跟踪用户行为。 + + +# 3. ID Mapping 的难点 + +#### 3.1. 各个平台和各个设备ID无法直接关联 + +- 要想关联需要找到关联对象,⽤SQL 举例就是如果要想把 A 和 C 关联起来,必须找到可以同时和它们可以关联起来的B,⽽我们的用户 ID 非常多,所以要想关联起来需要梳理清楚关联关系,且还得写大量的关联代码。 +- +#### 3.2. ID 过期问题 + +- 有些数据可能属于同一个,但在某个阶段上、这些数据之间没有任何联系,那么这类的数据可能会被打上两个不同的标识,也就是说需要在某时刻同时获得这些信息,但是这是非常困难的。 + +#### 3.3. 异常数据 + +- 借用朋友设备; + +- 记录设备数据格式错误,有脏数据; + +- 风控场景:刷号等行为; + +# 4. ID Mapping 的实现 + +#### 4.1. 物理层 Mapping + +- 这是最单纯基本的层面,也就是如何精准地记录和标识一个用户,例如利用硬件设备码生成一个统一的设备码,利用一些强账号来标识用户等等。 +- 这个层面上主要的技术难度,在于ID的稳定性、唯一性和持久性。 + +- 账号对应: + - 按账号优先级的方案实现简单,将系统中所有账号体系的 ID 设定优先级,按照优先级顺序作为用户的唯一标识。 + - 美团账号 ID mapping 示例: + ![]({{site.baseurl}}/img-post/id-mapping.png) + + - 缺点:用户多对多等场景下,识别正确率比较低。 + + +#### 4.2. 借助图计算 + +- 图计算是将数据转为“点集合”,将数据之间的关系转化为“线集合”,通过最短路径规划、频繁子集等算法,形成 id映射字典。 + +**具体步骤:** +- 步骤一:根据日志获取 ID 号,获取每一行日志中的全部的 ID 号; + - 数据格式: + ``` + Array( + Tuple(1, Set("0006")), + Tuple(2, Set("0007")), + Tuple(3, Set("0006", "0008")), + ... + Tuple(1000, Set("1000")), + ) + ``` +- 步骤二:修改数据格式 + - 修改后的数据格式: + ```aidl + 0006 1 + 0007 2 + 0006 3 + 0008 3 + ... + 1000 1000 + ``` +- 步骤三:分组 + - 分组后的数据格式: + ``` + 0006, {1, 3} + 0007, {2} + 0008, {3} + ... + 1000, {1000} + ``` +- 步骤四:生成用户关系表 + ```aidl + one_id_0001 0006, 0008 + one_id_0002 0007 + ... + one_id_1000 1000 + ``` +- 步骤五:获取之前的用户标签,合并结果表 + ```aidl + 0006(user_id_0006) (上海市, 1)(今日头条, 1) + 0007(user_id_0007) (北京市, 2)(梨视频, 2)(西瓜视频, 2) + 0008(user_id_0008) (上海市, 3)(奔驰用户, 3) + ... + 1000(user_id_1000) (深圳市, 1000)(抖音, 1000) + ``` +- 步骤六:合并结果表 + ```aidl + one_id_0001 (user_id_0006, user_id_0008) (((上海市, 1)(今日头条, 1)),((上海市, 3)(奔驰用户, 3))) + one_id_0002 (user_id_0007, ) (北京市, 2)(梨视频, 2)(西瓜视频, 2) + ... + one_id_1000 (user_id_1000, ) (深圳市, 1000)(抖音, 1000) + ``` + +# 5. ID Mapping 设计经验 + +#### 5.1. 基于用户兴趣做相似用户的合并 + +- 如果说物理层面 mapping,主要还在判断标识一个用户的 ID 是否正确,那么层面三致力于把行为相似的用户给合并起来。 + +- 例如:某一个用户的设备多次连接同一个Wi-Fi网关,但是每次链接都会随机更换ID,那么相当于这个用户的数据“分裂”在多个不同ID下。那么如何把这些ID合并成同一个用户呢? + +- 除了上述做法之外,还可以开发了相似用户合并技术。基于用户的上网时间偏好、网址访问偏好、点击行为偏好、浏览行为偏好、APP偏好和社交账号偏好等,为每个用户提取了上千个特征之后,进行相似用户的聚类。 + +- 聚类中选择类中心附近的用户,再加上一些辅助准则判定,就可以把用户合并起来。 + +#### 5.2. 基于用户行为做迭代滚动 Mapping + +- 由于原始数据存在噪音,同一个用户的多份数据、多种ID之间是“多对多”的关系,那么哪些ID是可信的却无法判断。 +- 为此,可以设计置信度传播的机器学习图模型,来帮助确定哪些身份ID是可信的。 + - 具体来说,这个算法的过程是给每一个ID,以及两个ID,如IMEI和邮箱之间的pair关系都有一个预设的置信度。 + - 所有的ID根据两两关联构成了一张图,那么每个ID的置信度根据这张网的结构传播给相关联的ID,同时也从其他ID那边接收置信度,而pair关系的置信度不变。 + - 当算法迭代收敛时,高置信度的ID就是可信的。同一个子图内的ID就标识了同一个用户。用类似的算法,我们也可以评价每个数据源的质量等。 + +#### 5.3. 解决 ID 过期问题 + +- 对于僵尸用户,或者长期不用的用户,保存数据没有意义,浪费资源而且数据长期不更新后可能数据不准确。 + +- 可以对每个ID加入活跃度参数,一方面代表用户的活跃程度,一方面可以对ID的存储做控制。 + +- 用户行为数据:代表了用户的活跃度,数据入表活跃度设置为0 + +- ID Mapping历史数据:按周更新,代表上周用户的数据,迭代计算时,活跃度+1 + +- 全量用户信息数据:代表全量用户,数据引入时,设置活跃度参数为一个合理值 + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\344\273\216 Hive \346\225\260\344\273\223 \345\220\214\346\255\245\346\240\207\347\255\276\346\225\260\346\215\256\345\210\260 HBase.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\344\273\216 Hive \346\225\260\344\273\223 \345\220\214\346\255\245\346\240\207\347\255\276\346\225\260\346\215\256\345\210\260 HBase.md" new file mode 100644 index 00000000000..fcd02ea1876 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\344\273\216 Hive \346\225\260\344\273\223 \345\220\214\346\255\245\346\240\207\347\255\276\346\225\260\346\215\256\345\210\260 HBase.md" @@ -0,0 +1,104 @@ +--- +layout: post +title: 用户画像:从 Hive 数仓 同步标签数据到 HBase +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + + +#### 用户画像系统数据流向 + +- 用户画像数仓架构 + + ![]({{site.baseurl}}/img-post/用户画像-22.png) + +- 用户画像系统数仓 ETL 加工流程,是将业务数据、日志数据、埋点数据等经过 ETL 过程,加工到数据仓库对应的 ODS 层、DW 层、DM 层中; +- 中间的虚线框即为用户画像建模的主要环节,是对基于数据仓库 ODS 层、DW 层、DM 层中与用户相关数据的二次建模加工; +- 在 ETL 过程中将用户标签计算结果写入 Hive,由于不同数据库有不同的应用场景,后续需要进一步将数据同步到MySQL、HBase、Elasticsearch 等数据库中; + + +#### 从 Hive 同步标签数据到 HBase + +- 用户画像系统中,Hive 作为数仓、在 ADS 层存储用户标签计算结果; +- 用户标签数据在 Hive 中加工完成后: + - 一部分标签通过 Sqoop 同步到 MySQL 数据库,提供用于 BI 报表展示的数据、多维透视分析数据、圈人服务数据; + - 另一部分标签同步到HBase数据库用于产品的线上个性化推荐。 + + +#### HBase 标签同步应用场景 + +- 例如:圈人服务 + + - 人群圈选,即通过组合标签查询对应的用户人群; + - 首先,通过组合标签的条件,在 Elasticsearch 中查询对应的索引数据; + - 然后,通过索引数据去 HBase 中批量获取 row key 对应的数据。 + + - 业务方根据规则圈定人群后,进一步通过分析、明确该人群是其要运营的人群后,将该人群推送到相应的业务系统中; + + ![]({{site.baseurl}}/img-post/用户画像-34.png) + + - 工程调度流程 + + ![]({{site.baseurl}}/img-post/用户画像-35.png) + + +#### HBase 标签同步方案 + +- 注意 + - 创建 hbase 表,需要创建好预分区 + +- 映射表 + - 方案 + - hive 和 hbase 的表建立映射关系; + - 读取的是同一份 HDFS 文件,只是在上层建立 hbase 到 hive 表的映射。 + - 优点 + - 一份数据存储,两种查询模式,数据存储最低; + - 缺点 + - 底层还是格式化的 HDFS 文件,查询需要进行映射转换,效率较低; + +- bulkload 导入 + - 方案 + - 将 hive 的数据生成 hfile; + - 通过 bulkload 导入到 hbase + - 这样底层数据的格式会转变成 Hfile,存储在 hbase中; + - 将 hbase 完全作为一个数据库去查询; + - 优点 + - 查询效率高; + - 缺点 + - 同一份数据,两份存储格式,空间换取时间; + +#### HBase 同步需校验数据 + +- 因为灌入到 HBase 中的数据一般直接应用到线上,反馈到用户那里; +- 所以,在hive数据同步 HBase 数据的时候,需要做一些校验机制来保障结果的准确性,防止在同步数据的过程中出现问题; + - 比如:hive 中数据 5000 万条,同步到 HBase 后才 1000 万条; + + +#### HBase 标签同步数据校验方案 + +- temp 临时表 + - hive 到 HBase 同步数据后,先 HBase 中建立一个 temp 临时表; + - 然后校验 HBase 的这个临时表和对应 hive 表的数量差异; + - 如果在可接受范围内,则将 hbase 的该临时表进行重命名为正式表; + +- 状态表 + - hive 到 hbase 同步数据后,直接将数据写入正式表,同时在 hbase 中建立一张状态表,用于标志状态位; + - 当校验 hbase 的这张正式表和 hive 的数量差异在可接受范围内时,写入对应的状态表中; + - 接口请求时,只读取状态位这张表中,最近日期的那张表 + - 如果 hbase 的数据同步异常,不会写入状态表中,也不会影响线上数据的读取; + + + + + + + + + + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\344\273\216\346\225\260\346\215\256\344\270\255\345\217\260\345\210\260\347\224\250\346\210\267\346\240\207\347\255\276.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\344\273\216\346\225\260\346\215\256\344\270\255\345\217\260\345\210\260\347\224\250\346\210\267\346\240\207\347\255\276.md" new file mode 100644 index 00000000000..42a97af30f2 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\344\273\216\346\225\260\346\215\256\344\270\255\345\217\260\345\210\260\347\224\250\346\210\267\346\240\207\347\255\276.md" @@ -0,0 +1,67 @@ +--- +layout: post +title: 用户画像:从数据中台到用户标签 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + +# 用户标签在数据中台的位置 + +- 数据中台是数据+技术+产品+组织的有机组合,是快、准、全、统、通的智能大数据体系。与数据仓库等传统数据工具相比,数据中台是一种新的理念,以“技术+业务”为双驱动,是企业开展新型运营的一个中枢系统。 +- 数据资产和数据服务中最核心的是标签:数据资产本身以标签为组织载体,而数据服务本质上是一种将标签传递给业务端使用的价值管道。标签是数据中台价值链路中“核心的核心”。 +- 用户标签可以按照标签的控制深度划分为基本属性标签、行为属性标签、商业属性标签等;也可以按照数据中台的数据分层结构,分为事实标签、模型标签、预测标签等。 + + ![]({{site.baseurl}}/img-post/用户画像-2-1.png) + +# 数据中台生成数据标签的流程 + +- 第一,需要从业务视角对企业数据进行梳理,并将各业务域、各渠道、各类型的数据进行采集和汇聚。 +- 第二,通过对数据进行分类处理,从中提炼出可复用的行为元素(业务线、实体对象、实体属性、动作等),通过沉淀行为元素,可以更好地规范来源数据。 + - 这里业务线是在不同业务运营线,例如:某造纸公司有生活纸运营线,文化纸运营线、工业纸运营线、特种纸运营线等。 + - 实体对象是指操作和被操作的各商业主体,例如:用户、产品等。实体属性是指实体对象的属性特征信息,例如:用户的年龄、性别、喜好等。动作就是主体发出的操作,例如:询价、购买、浏览等。 +- 第三,根据对象的行为元素给对象打上相应的“标签”,以支持信息查询、信息推送等应用。与传统博客、CMS(文章管理系统)的手动给内容打标签不同,数据中台是根据对象的行为规则自动给对象打标签,并且可以设置行为数据的时间衰减算法,为不同标签分配不同的权重,形成全面的“用户画像”,做到“比用户自己还了解他自己”。 +- 第四,各相关应用直接调用数据中台的标签体系、画像服务,支持企业的精准营销、个性推荐、渠道优化、产品创新等应用场景。 + +- 其中,执行第二、第三个步骤的前提就是要做好标签类目体系的规划。也就是说,标签体系也是具有一定的分类结构的。 + +# 数据中台为标签体系提供哪些能力 + + +#### 打通多源数据 + +- 数据中台的核心价值,在于对多源异构数据的统一管理,连接了企业的数据孤岛。 + +#### ID-Mapping + +- 所谓ID-Mapping,就是对不同业务中(ID不同)同一个对象进行打通。以便让产品和运营能够站在”上帝”的视角看用户,了解每个用户在使用产品生命周期全过程。 + - 如:用户从哪里来?什么时间,什么地点喜欢打开APP?喜欢做什么?喜欢谈论什么?最近需要什么……ID-Mapping,首先要通过ID-ID之间两两关系得到,通过两两关系表,再将多种关系关联起来(SuperID),这里的ID通常有:身份证、手机号、邮箱账号、IMEI、通行证账号、交易账号等。 +- 在建立关系表时,有的时候两两关系并不是确定不变的,而是带有置信度的。比如,因为业务上一个手机号可以登录多个通行证账号。再比如,一个通行证账号可以登录不同交易账号的场景。以上情况我们无法确保ID-ID关系是一对一的关系。这样,在使用不同跨ID画像的时候,我们就要明确使用场景。 +- 有的使用场景,对ID的匹配精准的要求非常严格,比如,需要对用户总资产做统计并且显示在用户资金账户上。而有的场景则不需要完全匹配,比如说,内容推荐的场景。此外,ID-Mapping还可以用在反欺诈场景中。假设我们发现一个身份ID与其他很多账号有着“盘根错节,剪不断理还乱”的关系,很可能这就是一个问题ID。 + +#### 标签管理标签 + +- 站在"人","物",“关系” 层面,通过一定的逻辑将数据处理成机器或者业务能够理解的数据。按照计算方式不同,标签又可以分为统计类标签,规则类标签,模型类标签和预测类标签。 +- 按照一定的业务需求、人工规则或者AI赋能,将晦涩难用的数据转化成业务可理解、可用的数据资产。 +- 标签管理主要包括: + - 标签元数据管理 + - 任务调度管理 + - 标签类目管理等方面内容。 +- 任务调度管理是对每个标签生成的任务进行管理。一个任务可以生成多个标签,但一个标签只能由一个任务产生。建立标签任务的时候,要考虑数据源,来自相同数据源的标签可以放在同一个任务中进行。在执行任务的过程中,需要考虑源数据执行完成的时间和速度。 + - 而对于标签的标签(二次标签,即基于标签生成的标签),我们不仅需要考虑源数据执行完成的时间和速度,还要考虑该二次标签所依赖的标签生成的时间。 +- 标签类目管理,是标签体系建设重要环节。就如同一本书的索引,一个良好的类目管理可以方便业务迅速准确的定位自己想要的标签。标签类目管理设计方式不唯一,不同的公司因业务不同,所关心的用户群体和用户特征是不一样的。 + +#### 标签服务 + +- 无论是用户画像标签还是物品画像标签,都是通过标签服务创造价值的。如果标签只是被生产出来,而不能为企业服务,那么和数据库中的数据一样都将会是一潭死水,终无一用。 +- 标签服务主要体现在: + - 标签分析 + - 人群圈定 + - 精准营销 + - 反欺诈 + - 客户关系管理系统 + - 个性化推荐等 diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\237\272\344\272\216 Spark \345\274\200\345\217\221\346\240\207\347\255\276\346\250\241\345\236\213.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\237\272\344\272\216 Spark \345\274\200\345\217\221\346\240\207\347\255\276\346\250\241\345\236\213.md" new file mode 100644 index 00000000000..b0b597afac9 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\237\272\344\272\216 Spark \345\274\200\345\217\221\346\240\207\347\255\276\346\250\241\345\236\213.md" @@ -0,0 +1,621 @@ +--- +layout: post +title: 用户画像:基于 Spark 开发标签模型 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +# 标签模型开发流程 + +- 标签开发,是用户画像工程化的重点模块,包含统计类、规则类、挖掘类、流式计算类标签的开发,以及人群计算功能的开发,打通画像数据和各业务系统之间的通路,提供接口服务等开发内容。 + +![]({{site.baseurl}}/img-post/标签模型开发-2.png) + +- 标签建模 + - 所有的离线标签系统,都需要两个核心模型: + - 离线标签 + - 用户维表 + +- 读取数据: + - 从 MySQL 数据库中读取标签规则数据; + - 从 HBase 表中获取原始数据; + - 在此之前,如果原始数据存储于 Hive,可以用 bulkload 将数据导入 HBase; + +- 计算标签: + - 基于 SparkSQL 计算得到标签结果; + - >每个标签,对应一个应用程序。需要一个标签,就开发一套 Spark 程序。如果不再使用标签,则直接停止该程序; + +- 数据落表: + - 标签结果数据,存储到 HBase; + - 基于 ES 做二级索引以方便查找; + +# 标签模型 + +#### 标签建模 + +- 离线标签 + - 画像系统的标签,分为离线标签和实时标签。 + - 离线标签又分为基于统计类型的标签和基于算法性标签,大部分或者 90% 的标签都是统计类型标签; + - 机器学习的算法标签很少: + - 一方面是因为开发周期很长, + - 另一方面效果也有限。 + - 但是基于某些场景,还是得需要使用机器学习,只是机器学习标签的比重会很小。 + - 有时我们做实时数据支持,会通过实时数据流给用户做刻画或者做特征标记,可做线上服务接口调用查询。 +- 用户维表 + - 用户维表是一张大宽表,大宽表基于筛选用户的属性或分析师分析用户的时候用。除了标签跑批、用户维表,还有人群计算、行为数据等的数据开发。 + +#### 模型示例 + +- 标签表上的每条记录,与模型表中的记录,是一一对应的。 +- 标签表 + ``` + CREATE TABLE `tbl_basic_tag` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '标签ID', + `name` varchar(50) DEFAULT NULL COMMENT '标签名称', + `industry` varchar(30) DEFAULT NULL COMMENT '行业、子行业、业务类型、标签、属性', + `rule` varchar(300) DEFAULT NULL COMMENT '标签规则', + `business` varchar(100) DEFAULT NULL COMMENT '业务描述', + `level` int(11) DEFAULT NULL COMMENT '标签等级', + `pid` bigint(20) DEFAULT NULL COMMENT '父标签ID', + `ctime` datetime DEFAULT NULL COMMENT '创建时间', + `utime` datetime DEFAULT NULL COMMENT '修改时间', + `state` int(11) DEFAULT NULL COMMENT '状态:1申请中、2开发中、3开发完成、4已上线、5已下线、6已禁用', + `remark` varchar(100) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`) + ) ENGINE=InnoDB AUTO_INCREMENT=233 DEFAULT CHARSET=utf8 COMMENT='基础标签表'; + ``` + +- 模型表 + - 详细记录标签模型 application 的位置; + ``` + CREATE TABLE `tbl_model` ( + `id` bigint(20) DEFAULT NULL, + `tag_id` bigint(20) DEFAULT NULL COMMENT '标签ID', + `type` int(11) DEFAULT NULL COMMENT '算法类型:统计-Statistics、规则匹配-Match、挖掘-具体算法-DecisionTree', + `model_name` varchar(200) DEFAULT NULL COMMENT '模型名称', + `model_main` varchar(200) DEFAULT NULL COMMENT '模型运行主类名称', + `model_path` varchar(200) DEFAULT NULL COMMENT '模型JAR包HDFS路径', + `sche_time` varchar(200) DEFAULT NULL COMMENT '模型调度时间', + `ctime` datetime DEFAULT NULL COMMENT '创建模型时间戳', + `utime` datetime DEFAULT NULL COMMENT '更新模型时间戳', + `state` int(11) DEFAULT NULL COMMENT '模型状态,1:运行;0:停止', + `remark` varchar(100) DEFAULT NULL, + `operator` varchar(100) DEFAULT NULL, + `operation` varchar(100) DEFAULT NULL, + `args` varchar(100) DEFAULT NULL COMMENT '模型运行应用配置参数,如资源配置参数' + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + ``` + +# Spark 计算引擎 + +#### Spark 计算标签数据的优势 + +- 计算速度比较快 + - Spark 是内存计算,整个流程的处理时间相对Hadoop减少5~10倍。 + +- Spark 支持多种数据源 + +- Spark 提供了非常丰富的数据处理操作: + - Map & reduce + - filter + - join + - cogroup, +- scala 语言简洁,使得代码量大为减少。 + +#### Spark 计算标签的不足 + +- 人群投影问题: + - 两个数据集 (RDD) A、B,其中: + - A 是全量数据,包括id和标签; + - B 数据集只包含 id; + - 需要知道 B 数据集中有多少 id 在 A 中存在,以及需要知道存在的 id 的标签统计结果,根据 id 对 A、B 俩数据集进行 join 来过滤出在 A、B 中同时存在的 id,然后再统计这些 id 各标签的数量; +- Spark 原生的 join 会对两个 RDD 都进行一次 shuffle,每个 worker 将数据根据 hash 值重新分发到各 worker 上,由于 A 数据集是全量数据、量非常大,而且常驻内存、而且一天才更新一次,这样系统开销很大; +- 解决思路: + - 不需要每次计算时都要对它进行 shuffle,而是可以先对 A 数据集进行 Shuffle 并且在每个 partition 上按照 id 排好序; + - 这样 A 和 B 进行 join 时、A 不用 shuffle,只需要将 B 按照同样的 hash 算法 Shuffle 然后排序,再按序遍历 A 和 B 相同的 partition,就能过滤出 AB 中同时存在的 id。 + - 类似于做好工作后,求两个有序数组的交集,只需 O(M+N) 的时间就够。 + + +# 模型开发示例 + +#### 需求描述 + +- 示例:开发一个统计型标签,计算用户的客单价。 + - 客单价就是一个客户所有的订单金额/订单数量; + - 简单说就是需要统计每个用户每笔订单所花的钱。 + +#### 创建 SparkSession + + - 因为我们汇总计算需要使用到 SparkSQL,所以我们需要先创建 SparkSQL 的运行环境。 + - 为了方便我们后期运行时查看控制台,我们可以设置一下日志级别。 + ``` + val spark: SparkSession = SparkSession.builder().appName("AgeTag").master("local[*]").getOrCreate() + + // 设置日志级别 + spark.sparkContext.setLogLevel("WARN") + ``` + +#### JDBC 连接 MySQL + +- 这里采用 Spark 通过 jdbc 的方式连接 MySQL。 + ``` + // 设置Spark连接MySQL所需要的字段 + var url: String ="jdbc:mysql://bd001:3306/tags_new2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&user=root&password=123456" + var table: String ="tbl_basic_tag" //mysql数据表的表名 + var properties:Properties = new Properties + + // 连接MySQL + val mysqlConn: DataFrame = spark.read.jdbc(url,table,properties) + ``` + +#### 读取 MySQL 数据库四级标签的数据 + +- 因为后续可能需要对读取的数据,做一些形式上的转换,所以这里先引入了隐式转换和 SparkSQL 的内置函数,然后根据 MySQL 的连接对象,读取了四级标签的数据,并对其做了一定的处理。 + ``` + // 引入隐式转换 + import spark.implicits._ + + //引入sparkSQL的内置函数 + import org.apache.spark.sql.functions._ + + //读取MySQL数据库的四级标签 + val fourTagsDS: Dataset[Row] = mysqlConn.select("rule").where("id=137") + + // 对四级标签数据做处理 + val KVMap: Map[String, String] = fourTagsDS.map(row => { + + // 获取到rule值 + val RuleValue: String = row.getAs("rule").toString + + // 使用"##"对数据进行切分 + val KVMaps: Array[(String, String)] = RuleValue.split("##").map(kv => { + + val arr: Array[String] = kv.split("=") + (arr(0), arr(1)) + }) + KVMaps + }).collectAsList().get(0).toMap + + // 将Map 转换成HBaseMeta的样例类 + val hbaseMeta: HBaseMeta = toHBaseMeta(KVMap) + ``` + +- 因为涉及到了样例类的调用,这里提前写好了样例类。 + +``` + //将mysql中的四级标签的rule 封装成HBaseMeta + //方便后续使用的时候方便调用 + def toHBaseMeta(KVMap: Map[String, String]): HBaseMeta = { + //开始封装 + HBaseMeta(KVMap.getOrElse("inType",""), + KVMap.getOrElse(HBaseMeta.ZKHOSTS,""), + KVMap.getOrElse(HBaseMeta.ZKPORT,""), + KVMap.getOrElse(HBaseMeta.HBASETABLE,""), + KVMap.getOrElse(HBaseMeta.FAMILY,""), + KVMap.getOrElse(HBaseMeta.SELECTFIELDS,""), + KVMap.getOrElse(HBaseMeta.ROWKEY,"") + ) + } +``` + +#### 读取 MySQL 数据库五级标签的数据 + +- 上一步,已经读取完了四级标签,这一步需要读取 MySQL 中五级标签的数据,也就是标签值的数据。 同样,再读取完之后,需要对数据进行处理。 +- 因为标签值是一个范围的数据,例如 1-999,需要将这个范围的开始和结束的数字获取到,然后将其添加为 DataFrame 的 Schema,方便后期对其与 Hbase 数据进行关联查询的时候获取到区间起始数据。 + + ``` + val fiveTagsDS: Dataset[Row] = mysqlConn.select("id","rule").where("pid=137") + + val fiveTagDF: DataFrame = fiveTagsDS.map(row => { + // row 是一条数据 + // 获取出id 和 rule + val id: Int = row.getAs("id").toString.toInt + val rule: String = row.getAs("rule").toString + + //133 1-999 + //134 1000-2999 + var start: String = "" + var end: String = "" + + val arr: Array[String] = rule.split("-") + + if (arr != null && arr.length == 2) { + start = arr(0) + end = arr(1) + } + // 封装 + (id, start, end) + }).toDF("id", "start", "end") + + fiveTagDF.show() + + //+---+-----+----+ + //| id|start| end| + //+---+-----+----+ + //|138| 1| 999| + //|139| 1000|2999| + //|140| 3000|4999| + //|141| 5000|9999| + //+---+-----+----+ + ``` + +#### 读取 Hbase 中的标签值数据 + +- 到了这一步,开始逐渐显得与匹配型标签的操作不一样了,在读取完了Hbase中的数据之后,需要展开分析。 + - 因为一个用户可能会有多条数据,也就会有多个支付金额,这里需要将数据按照用户id进行分组,然后获取到金额总数和订单总数,求余就是客单价。 + + ``` + val hbaseDatas: DataFrame = spark.read.format("com.czxy.tools.HBaseDataSource") + // hbaseMeta.zkHosts 就是 192.168.10.20 和 下面是两种不同的写法 + .option("zkHosts",hbaseMeta.zkHosts) + .option(HBaseMeta.ZKPORT, hbaseMeta.zkPort) + .option(HBaseMeta.HBASETABLE, hbaseMeta.hbaseTable) + .option(HBaseMeta.FAMILY, hbaseMeta.family) + .option(HBaseMeta.SELECTFIELDS, hbaseMeta.selectFields) + .load() + + hbaseDatas.show(5) + //+--------+-----------+ + //|memberId|orderAmount| + //+--------+-----------+ + //|13823431| 2479.45| + //| 4035167| 2449.00| + //| 4035291| 1099.42| + //| 4035041| 1999.00| + //|13823285| 2488.00| + //+--------+-----------+ + + // 因为一个用户可能会有多条数据 ,也就会有多个支付金额 + // 我们需要将数据按照用户id进行分组,然后获取到金额总数和订单总数,求余就是客单价 + val userFirst: DataFrame = hbaseDatas.groupBy("memberId").agg(sum("orderAmount").cast("Int").as("sumAmount"),count("orderAmount").as("countAmount")) + + userFirst.show(5) + + //+---------+---------+-----------+ + //| memberId|sumAmount|countAmount| + //+---------+---------+-----------+ + //| 4033473| 251930| 142| + //| 13822725| 179298| 116| + //| 13823681| 169746| 108| + //|138230919| 240061| 125| + //| 13823083| 233524| 132| + //+---------+---------+-----------+ + + // val frame: DataFrame = userFirst.select($"sumAmount" / $"countAmount") + val userAvgAmount: DataFrame = userFirst.select('memberId,('sumAmount / 'countAmount).cast("Int").as("AvgAmount")) + + userAvgAmount.show(5) + //+---------+-------------------------+ + //| memberId|(sumAmount / countAmount)| + //+---------+-------------------------+ + //| 4033473| 1774.1549295774648| + //| 13822725| 1545.6724137931035| + //| 13823681| 1571.7222222222222| + //|138230919| 1920.488| + //| 13823083| 1769.121212121212| + //+---------+-------------------------+ + ``` + +#### 数据关联 JOIN + +- 在第四步和第五步中,分别对 MySQL 中的五级标签数据和 Hbase 中的标签值数据进行了处理。 +- 在第六步中,应对其进行关联。 + - 因为客单价的标签值是一个范围的数据,所以这里使用到了 Between; + - 想要获取到区间范围的起始值只需要用五级标签返回的 DataFrame 对象 fiveTagDF.col 的形式即可获取到。 + ``` + // 将 Hbase的数据与 五级标签的数据进行 关联 + val dataJoin: DataFrame = userAvgAmount.join(fiveTagDF, userAvgAmount.col("AvgAmount") + .between(fiveTagDF.col("start"), fiveTagDF.col("end"))) + + dataJoin.show() + + // 选出我们最终需要的字段,返回需要和Hbase中旧数据合并的新数据 + val AvgTransactionNewTags: DataFrame = dataJoin.select('memberId.as("userId"),'id.as("tagsId")) + + AvgTransactionNewTags.show(5) + ``` + +#### 解决数据覆盖的问题 + +- 在获取到了新数据之后,需要将 Hbase 结果表中的“旧数据”读取出来,然后与之进行合并,所以需要定义一个udf,用于解决标签值重复或者数据合并的问题。 + ``` + /* 定义一个udf,用于处理旧数据和新数据中的数据合并的问题 */ + val getAllTages: UserDefinedFunction = udf((genderOldDatas: String, jobNewTags: String) => { + + if (genderOldDatas == "") { + jobNewTags + } else if (jobNewTags == "") { + genderOldDatas + } else if (genderOldDatas == "" && jobNewTags == "") { + "" + } else { + val alltages: String = genderOldDatas + "," + jobNewTags //可能会出现 83,94,94 + // 对重复数据去重 + alltages.split(",").distinct // 83 94 + // 使用逗号分隔,返回字符串类型 + .mkString(",") // 83,84 + } + }) + + // 读取hbase中的历史数据 + val genderOldDatas: DataFrame = spark.read.format("com.czxy.tools.HBaseDataSource") + // hbaseMeta.zkHosts 就是 192.168.10.20 和 下面是两种不同的写法 + .option("zkHosts","192.168.10.20") + .option(HBaseMeta.ZKPORT, "2181") + .option(HBaseMeta.HBASETABLE, "test") + .option(HBaseMeta.FAMILY, "detail") + .option(HBaseMeta.SELECTFIELDS, "userId,tagsId") + .load() + + // 新表和旧表进行join + val joinTags: DataFrame = genderOldDatas.join(AvgTransactionNewTags, genderOldDatas("userId") === AvgTransactionNewTags("userId")) + + joinTags.show() + + val allTags: DataFrame = joinTags.select( + // 处理第一个字段 + when((genderOldDatas.col("userId").isNotNull), (genderOldDatas.col("userId"))) + .when((AvgTransactionNewTags.col("userId").isNotNull), (AvgTransactionNewTags.col("userId"))) + .as("userId"), + getAllTages(genderOldDatas.col("tagsId"), AvgTransactionNewTags.col("tagsId")).as("tagsId") + ) + + // 新数据与旧数据汇总之后的数据 + allTags.show(10) + ``` +#### 数据写入 + +- 在合并完了数据之后,最后将其写入到Hbase中即可。 + ``` + // 将最终结果进行覆盖 + allTags.write.format("com.czxy.tools.HBaseDataSource") + .option("zkHosts", hbaseMeta.zkHosts) + .option(HBaseMeta.ZKPORT, hbaseMeta.zkPort) + .option(HBaseMeta.HBASETABLE,"test") + .option(HBaseMeta.FAMILY, "detail") + .option(HBaseMeta.SELECTFIELDS, "userId,tagsId") + .option("repartition",1) + .save() + ``` + +#### 完整代码 + +```aidl +import java.util.Properties + +import com.czxy.bean.HBaseMeta +import org.apache.spark.sql.expressions.UserDefinedFunction +import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession} + +/* + * @Author: Alice菌 + * @Date: 2020/6/12 21:10 + * @Description: + * + * 基于用户的客单价统计标签分析 + */ +object AvgTransactionTag { + + def main(args: Array[String]): Unit = { + + val spark: SparkSession = SparkSession.builder().appName("AgeTag").master("local[*]").getOrCreate() + + // 设置日志级别 + spark.sparkContext.setLogLevel("WARN") + // 设置Spark连接MySQL所需要的字段 + var url: String ="jdbc:mysql://bd001:3306/tags_new2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&user=root&password=123456" + var table: String ="tbl_basic_tag" //mysql数据表的表名 + var properties:Properties = new Properties + + // 连接MySQL + val mysqlConn: DataFrame = spark.read.jdbc(url,table,properties) + + // 引入隐式转换 + import spark.implicits._ + + //引入sparkSQL的内置函数 + import org.apache.spark.sql.functions._ + + // 读取MySQL数据库的四级标签 + val fourTagsDS: Dataset[Row] = mysqlConn.select("rule").where("id=137") + + val KVMap: Map[String, String] = fourTagsDS.map(row => { + + // 获取到rule值 + val RuleValue: String = row.getAs("rule").toString + + // 使用"##"对数据进行切分 + val KVMaps: Array[(String, String)] = RuleValue.split("##").map(kv => { + + val arr: Array[String] = kv.split("=") + (arr(0), arr(1)) + }) + KVMaps + }).collectAsList().get(0).toMap + + println(KVMap) + + // 将Map 转换成HBaseMeta的样例类 + val hbaseMeta: HBaseMeta = toHBaseMeta(KVMap) + + //4. 读取mysql数据库的五级标签 + val fiveTagsDS: Dataset[Row] = mysqlConn.select("id","rule").where("pid=137") + + val fiveTagDF: DataFrame = fiveTagsDS.map(row => { + // row 是一条数据 + // 获取出id 和 rule + val id: Int = row.getAs("id").toString.toInt + val rule: String = row.getAs("rule").toString + + //133 1-999 + //134 1000-2999 + var start: String = "" + var end: String = "" + + val arr: Array[String] = rule.split("-") + + if (arr != null && arr.length == 2) { + start = arr(0) + end = arr(1) + } + // 封装 + (id, start, end) + }).toDF("id", "start", "end") + + fiveTagDF.show() + + //+---+-----+----+ + //| id|start| end| + //+---+-----+----+ + //|138| 1| 999| + //|139| 1000|2999| + //|140| 3000|4999| + //|141| 5000|9999| + //+---+-----+----+ + + + // 5. 读取hbase中的数据,这里将hbase作为数据源进行读取 + val hbaseDatas: DataFrame = spark.read.format("com.czxy.tools.HBaseDataSource") + // hbaseMeta.zkHosts 就是 192.168.10.20 和 下面是两种不同的写法 + .option("zkHosts",hbaseMeta.zkHosts) + .option(HBaseMeta.ZKPORT, hbaseMeta.zkPort) + .option(HBaseMeta.HBASETABLE, hbaseMeta.hbaseTable) + .option(HBaseMeta.FAMILY, hbaseMeta.family) + .option(HBaseMeta.SELECTFIELDS, hbaseMeta.selectFields) + .load() + + hbaseDatas.show(5) + //+--------+-----------+ + //|memberId|orderAmount| + //+--------+-----------+ + //|13823431| 2479.45| + //| 4035167| 2449.00| + //| 4035291| 1099.42| + //| 4035041| 1999.00| + //|13823285| 2488.00| + //+--------+-----------+ + + // 因为一个用户可能会有多条数据 ,也就会有多个支付金额 + // 我们需要将数据按照用户id进行分组,然后获取到金额总数和订单总数,求余就是客单价 + val userFirst: DataFrame = hbaseDatas.groupBy("memberId").agg(sum("orderAmount").cast("Int").as("sumAmount"),count("orderAmount").as("countAmount")) + + userFirst.show(5) + + //+---------+---------+-----------+ + //| memberId|sumAmount|countAmount| + //+---------+---------+-----------+ + //| 4033473| 251930| 142| + //| 13822725| 179298| 116| + //| 13823681| 169746| 108| + //|138230919| 240061| 125| + //| 13823083| 233524| 132| + //+---------+---------+-----------+ + + // val frame: DataFrame = userFirst.select($"sumAmount" / $"countAmount") + val userAvgAmount: DataFrame = userFirst.select('memberId,('sumAmount / 'countAmount).cast("Int").as("AvgAmount")) + + userAvgAmount.show(5) + //+---------+-------------------------+ + //| memberId|(sumAmount / countAmount)| + //+---------+-------------------------+ + //| 4033473| 1774.1549295774648| + //| 13822725| 1545.6724137931035| + //| 13823681| 1571.7222222222222| + //|138230919| 1920.488| + //| 13823083| 1769.121212121212| + //+---------+-------------------------+ + + // 将 Hbase的数据与 五级标签的数据进行 关联 + val dataJoin: DataFrame = userAvgAmount.join(fiveTagDF, userAvgAmount.col("AvgAmount") + .between(fiveTagDF.col("start"), fiveTagDF.col("end"))) + + dataJoin.show() + + println("---------------------------------------------") + // 选出我们最终需要的字段,返回需要和Hbase中旧数据合并的新数据 + val AvgTransactionNewTags: DataFrame = dataJoin.select('memberId.as("userId"),'id.as("tagsId")) + + AvgTransactionNewTags.show(5) + + // 7、解决数据覆盖的问题 + // 读取test,追加标签后覆盖写入 + // 标签去重 + /* 定义一个udf,用于处理旧数据和新数据中的数据合并的问题 */ + val getAllTages: UserDefinedFunction = udf((genderOldDatas: String, jobNewTags: String) => { + + if (genderOldDatas == "") { + jobNewTags + } else if (jobNewTags == "") { + genderOldDatas + } else if (genderOldDatas == "" && jobNewTags == "") { + "" + } else { + val alltages: String = genderOldDatas + "," + jobNewTags //可能会出现 83,94,94 + // 对重复数据去重 + alltages.split(",").distinct // 83 94 + // 使用逗号分隔,返回字符串类型 + .mkString(",") // 83,84 + } + }) + + // 读取hbase中的历史数据 + val genderOldDatas: DataFrame = spark.read.format("com.czxy.tools.HBaseDataSource") + // hbaseMeta.zkHosts 就是 192.168.10.20 和 下面是两种不同的写法 + .option("zkHosts","192.168.10.20") + .option(HBaseMeta.ZKPORT, "2181") + .option(HBaseMeta.HBASETABLE, "test") + .option(HBaseMeta.FAMILY, "detail") + .option(HBaseMeta.SELECTFIELDS, "userId,tagsId") + .load() + + // 新表和旧表进行join + val joinTags: DataFrame = genderOldDatas.join(AvgTransactionNewTags, genderOldDatas("userId") === AvgTransactionNewTags("userId")) + + joinTags.show() + + val allTags: DataFrame = joinTags.select( + // 处理第一个字段 + when((genderOldDatas.col("userId").isNotNull), (genderOldDatas.col("userId"))) + .when((AvgTransactionNewTags.col("userId").isNotNull), (AvgTransactionNewTags.col("userId"))) + .as("userId"), + getAllTages(genderOldDatas.col("tagsId"), AvgTransactionNewTags.col("tagsId")).as("tagsId") + ) + + // 新数据与旧数据汇总之后的数据 + allTags.show(10) + + // 将最终结果进行覆盖 + allTags.write.format("com.czxy.tools.HBaseDataSource") + .option("zkHosts", hbaseMeta.zkHosts) + .option(HBaseMeta.ZKPORT, hbaseMeta.zkPort) + .option(HBaseMeta.HBASETABLE,"test") + .option(HBaseMeta.FAMILY, "detail") + .option(HBaseMeta.SELECTFIELDS, "userId,tagsId") + .option("repartition",1) + .save() + + } + + + //将mysql中的四级标签的rule 封装成HBaseMeta + //方便后续使用的时候方便调用 + def toHBaseMeta(KVMap: Map[String, String]): HBaseMeta = { + //开始封装 + HBaseMeta(KVMap.getOrElse("inType",""), + KVMap.getOrElse(HBaseMeta.ZKHOSTS,""), + KVMap.getOrElse(HBaseMeta.ZKPORT,""), + KVMap.getOrElse(HBaseMeta.HBASETABLE,""), + KVMap.getOrElse(HBaseMeta.FAMILY,""), + KVMap.getOrElse(HBaseMeta.SELECTFIELDS,""), + KVMap.getOrElse(HBaseMeta.ROWKEY,"") + ) + } + +} +``` \ No newline at end of file diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\237\272\344\272\216\346\213\211\351\223\276\350\241\250\345\256\236\347\216\260 ID Mapping.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\237\272\344\272\216\346\213\211\351\223\276\350\241\250\345\256\236\347\216\260 ID Mapping.md" new file mode 100644 index 00000000000..76b8d7e2b33 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\237\272\344\272\216\346\213\211\351\223\276\350\241\250\345\256\236\347\216\260 ID Mapping.md" @@ -0,0 +1,127 @@ +--- +layout: post +title: 用户画像:基于拉链表实现 ID Mapping +subtitle: 基于 Hive ETL 实现 +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +#### ID-Map + +- 用户在未登录 App 的状态下,在 App 站内访问、搜索相关内容时,记录的是设备 id(即cookieid)相关的行为数据。 +- 用户在登录 App 后,访问、收藏、下单等相关的行为记录的是账号 id(即userid)相关行为数据。 + +![]({{site.baseurl}}/img-post/用户标签存储-3.png) + +- 虽然是同一个用户,但其在登录和未登录设备时、记录的行为数据之间是未打通的。 +- 通过 ID-MApping 打通 userid 和 cookieid 的对应关系,可以在用户登录、未登录设备时都能捕获其行为轨迹。 + +#### 拉链表 + +- 缓慢变化维是在维表设计中常见的一种方式,维度并不是不变的,随时间也会发生缓慢变化。 +- 如用户的手机号、邮箱等信息可能会随用户的状态变化而改变,再如商品的价格也会随时间变化而调整上架的价格。因此在设计用户、商品等维表时会考虑用缓慢变化维来开发。 +- 同样,在设计 ID-Mapping 表时,由于一个用户可以在多个设备上登录,一个设备也能被多个用户登录,所以考虑用缓慢变化维表来记录这种不同时间点的状态变化。 + +![]({{site.baseurl}}/img-post/用户标签存储-2.png) + +- 通过拉链表记录了 userid 每一次关联到不同 cookieid 的情况。 +- 其中 start_date 表示该记录的开始日期,end_date 表示该记录的结束日期,当 end_date 为 99991231 时,表示该条记录当前仍然有效。 + +#### Hive & ETL + +- 首先,需要从埋点表和访问日志表里面,获取到 cookieid 和 userid 同时出现的访问记录。 + - ods.page_event_log 是埋点日志表; + - ods.page_view_log 是访问日志表; + - 将获取到的 userid 和 cookieid 信息插入 cookieid-userid 关系表(ods.cookie_user_signin)中; + + ``` + INSERT OVERWRITE TABLE ods.cookie_user_signin PARTITION (data_date = '${data_date}') + SELECT t.* + FROM ( + SELECT userid, + cookieid, + from_unixtime(eventtime,'yyyyMMdd') as signdate + FROM ods.page_event_log -- 埋点表 + WHERE data_date = '${data_date}' + UNION ALL + SELECT userid, + cookieid, + from_unixtime(viewtime,'yyyyMMdd') as signdate + FROM ods.page_view_log -- 访问日志表 + WHERE data_date = '${data_date}' + ) t + ``` + +- 其次,创建 ID-Map 拉链表,将每天新增到 ods.cookie_user_signin 表中的数据与拉链表历史数据做比较,如果有变化或新增数据则进行更新。 + + ``` + CREATE TABLE `dw.cookie_user_zippertable`( + `userid` string COMMENT '账号ID', + `cookieid` string COMMENT '设备ID', + `start_date` string COMMENT 'start_date', + `end_date` string COMMENT 'end_date') + COMMENT 'id-map拉链表' + ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' + ``` + +- 创建完成后,每天ETL调度将数据更新到ID-Mapping拉链表中, + + ``` + INSERT OVERWRITE TABLE dw.cookie_user_zippertable + SELECT t.* + FROM ( + SELECT t1.user_num, + t1.mobile, + t1.reg_date, + t1.start_date, + CASE WHEN t1.end_date = '99991231' AND t2.userid IS NOT NULL THEN '${data_date}' + ELSE t1.end_date + END AS end_date + FROM dw.cookie_user_zippertable t1 + LEFT JOIN ( SELECT * + FROM ods.cookie_user_signin + WHERE data_date='${data_date}' + )t2 + ON t1.userid = t2.userid + UNION + SELECT userid, + cookieid, + '${data_date}' AS start_date, + '99991231' AS end_date + FROM ods.cookie_user_signin + WHERE data_date = '${data_date + }' + ) t + ``` + +- 查看某日(如20190801)的快照数据: + ``` + select * + from dw.cookie_user_zippertable + where start_date<='20190801' and end_date>='20190801' + ``` + +- 查看某个时间点 userid 对应的 cookieid + + ``` + select cookieid + from dw.cookie_user_zippertable + where userid='32101029' and start_date<='20190801' and end_date>='20190801' + ``` + ![]({{site.baseurl}}/img-post/用户标签存储-4.png) + + - 上图可看出用户‘32101029’在历史中曾登录过3个设备,通过限定时间段可找到特定时间下用户的登录设备。 + +#### 数据膨胀问题 + +- 在开发中需要注意关于 userid 与 cookieid 的多对多关联,如果不加条件限制就做关联,很可能引起数据膨胀问题; +- 在实际应用中,会遇到许多需要将 userid 和 cookieid 做关联的情况; +- 例如,需要在 userid 维度开发出该用户近 30 日的购买次数、购买金额、登录时长、登录天数等标签; +- 前两个标签可以很容易地从相应的业务数据表中、根据算法加工出来,而登录时长、登录天数的数据存储在相关日志数据中; +- 日志数据表记录的 userid 与 cookieid 为多对多关系。因此在结合业务需求开发标签时,要确定好标签口径定义。 + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\246\202\344\275\225\351\252\214\350\257\201\347\224\250\346\210\267\347\224\273\345\203\217\347\232\204\345\207\206\347\241\256\346\200\247.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\246\202\344\275\225\351\252\214\350\257\201\347\224\250\346\210\267\347\224\273\345\203\217\347\232\204\345\207\206\347\241\256\346\200\247.md" new file mode 100644 index 00000000000..7a3bc9ce7f1 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\246\202\344\275\225\351\252\214\350\257\201\347\224\250\346\210\267\347\224\273\345\203\217\347\232\204\345\207\206\347\241\256\346\200\247.md" @@ -0,0 +1,75 @@ +--- +layout: post +title: 用户画像:如何验证用户画像的准确性 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +#### 验证对象 + +- 用户基本信息 + - 主要是用户基本信息,如年龄、性别、学历、职业、收入、资产、婚否、是否有房、是否有车等; +- 用户行为画像 + - 基于互联网业务,此处的用户行为主要指用户的消费行为; + - 如品类偏好、下单预测、分期意愿等; +- 用户分群画像 + - 此类画像基于统计方法(聚类)将同类型用户划为一类,根据不同业务需求,群体特征也不尽相同。 + +#### 验证思路 + +- 用户基本信息 & 用户行为画像 + - 常用的模型指标验证主要用户前两类画像的验证,通用的包括: + - AUC + - K-S + - F1 + - ROC + - Confusion Matrix + - Precision + - Recall + - 具体参考文章:https://zhuanlan.zhihu.com/p/46714763。 +- 用户分群画像 + - 对于第三类基于聚类的用户画像没有较常用的验证指标; + - 通常情况下,聚类效果图不能很好的说明问题,还需结合业务及每个群体用户覆盖度进行相应调整,最后的验证通常也是基于事后的业务反馈效果。 + +#### 事中验证 + +- 抽样验证 + - 千万级甚至上亿级用户,不可能一一验证; + - 这种情况下可以采取分层抽样验证或随机抽样验证。 + - 缺陷: + - 由于抽样验证的数据量相对较小,因此说服度不高。 + +- 交叉验证 + - 画像类的指标间会存在一些相关性,此时可进行交叉验证。 + - 例如: + - 收入与资产存在一定的相关性,通常收入越高资产也会越高; + - 此时就可用这两个画像评分进行交叉验证。 + - 如果公司购买的第三方机构数据也有相应的画像指标,也可用于参考进行交叉验证 + +#### 事后验证 + +- 真实数据验证 + - 画像信息会随着业务的发展及产品的运维从无到有慢慢积累; + - 可以用真实数据用于验证画像类指标,这种方法是最准确的; + - 验证方法主要看业务反馈排序、与画像模型排序模型是否呈现单调性。 + - 示例: + ![]({{site.baseurl}}/img-post/用户画像-21.png) + - 如上图: + - Level1-Level10 预测概率等级呈降序,即 level1 概率最高,level10 概率最低; + - 基于用户画像指标(下单预测概率模型)将用户划分为十个等级。 + - 实际业务中,会从每个level随机抽一部分用户用于营销,而后根据实际情况(即下单率)来检测画像模型排序能力; + - 上图中,下单率与画像模型排序呈现严格单调性,因此就业务角度而言该画像是有效的,能用于业务并对业务起到一定提升作用。 + - 如果不单调,则需要考虑是否需求进一步优化画像模型。 + +- A/B Test + - 最常用的验证方法,一般基于用户画像制定的策略在上线时都会进行严格的对比试验,以测试画像的准确性。 + - 示例: + - 下图为一组对比试验,假设纵轴为用户响应率,横轴为营销活动开始时间; + - 可以看出实验组的效果优于对照组,因此该画像可认为相对准确。 + ![]({{site.baseurl}}/img-post/用户画像-20.png) \ No newline at end of file diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\270\270\350\247\201\347\232\204\347\224\250\346\210\267\346\240\207\347\255\276\344\275\223\347\263\273.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\270\270\350\247\201\347\232\204\347\224\250\346\210\267\346\240\207\347\255\276\344\275\223\347\263\273.md" new file mode 100644 index 00000000000..a47f2fba052 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\345\270\270\350\247\201\347\232\204\347\224\250\346\210\267\346\240\207\347\255\276\344\275\223\347\263\273.md" @@ -0,0 +1,774 @@ +--- +layout: post +title: 用户画像:常见的用户标签体系 +subtitle: 标签梳理 & 标签分类 +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +# 1. 用户标签的分类 + +#### 1.1. 按照维度划分 + +- 自然属性 + ![]({{site.baseurl}}/img-post/用户画像-11.png) + +- 商业属性 + ![]({{site.baseurl}}/img-post/用户画像-12.png) + +- 行为属性 + ![]({{site.baseurl}}/img-post/用户画像-13.png) + +- 价值属性 + ![]({{site.baseurl}}/img-post/用户画像-14.png) + +#### 1.2. 按照层级划分 + +![]({{site.baseurl}}/img-post/用户画像-2.png) + +- 主分类标签 + - 一级标签 + - 二级标签 + - 三级标签 +- 业务标签 + - 四级标签 +- 属性标签 + - 五级标签 + +#### 1.3. 按照生成方式划分 + + ![]({{site.baseurl}}/img-post/用户画像-2-1.png) + + - 事实标签 + - 属性标签: + - 可以直接提取 + - 包括一些基本信息,比如:年龄、性别、城市、职业等 + - 事实标签: + - 可以从用户数据、行为数据、消费数据中统计得出 + - 这些标签来自基于行为数据的统计信息,比如:1日登录次数、7日登录次数、7日下单次数等 + - 事实标签,构成了用户画像的基础 + + - 规则类标签 + - 又叫:建模标签 + - 在统计指标基础上通过规则生成,是对统计类指标的有效补充 + - 需要基于用户行为自定义规则 + - 比如: + - 活跃用户,定义规则为每天登录一次以上的用户 + - 高净值用户,消费总额在某个阈值以上 + - 在开发过程中,运营人员对业务更熟悉,数据维护人员对数据结构、分布、特征更熟悉,所以多数时候由运营人员和数据人员共同搭建维护; + + - 预测类标签 + - 又叫:机器学习标签 + - 是非确定性标签(前两项都属于确定性标签) + - 需要基于已有的信息“预测”用户特征,需要挖掘才能获得 + - 实际开发过程中,这类项目开发周期长、成本高,属于探索性项目 + - 有些公司使用外包人员、手动打标签,效果可能会更好、而且投入也很低 + +#### 1.4. 按数据类型划分 + +- 数值型 + - 这些标签都是按照一定的规则统计生成的,均是数值型标签,其实本质上和指标没有太大区别。 + - 如: + - 用户最近7天购买金额 + - 用户近1天浏览次数 + - 统计方式: + - 计数 + - 求和 + - 最值 + - 均值 + - 中值 + - 组成公式: + - 时间范围 + 行为方式 + 统计方式 + +- 单值枚举型 + - 最大特征是一个用户在这个标签中,只能有一个选项值; + - 如:性别 + - 男 + - 女 + - 未知 + - 和数值型标签的区别在于: + - 单选型标签的选项值是可穷举的,是离散的。 + - 生成方式 + - 可以是用户自己填写生成的; + - 也可以是通过数值型标签进行加工、或者算法模型生成的; + - 例如: + - 用户的生命周期,通过模型计算分析推导出来; + - 用户肯定是处于【成长期】、【成熟期】、【衰退期】、【沉睡期】其中的一个,不可能属于两个或者多个。 + +- 多值型枚举型 + - 和单选型标签的区别就在于,一个用户可以有多个值。 + - 复选型标签也是离散值,选项是可穷举的。 + - 例如: + - 用户的收货城市,用户可以有多个城市; + +- 文本型标签 + - 这类标签最大的特征,是不连续、且不可穷举。 + - 例如: + - 用户常用热搜词,每个用户都可以有自己的常用热搜词,但热搜词的数量是巨大的,不能像单选型标签或者复选型标签那样,几个、甚至最多几十个选项,就能覆盖所有。 + + +# 2. 用户标签落表建模 + +#### 2.1. 用户标签命名方式 + +- 标签主题: + - 用于刻画属于那种类型的标签, + - 如: + - 用户属性、用户行为、用户消费、风险控制等多种类型,可用A、B、C、D等字母表示各标签主题; +- 标签类型: + - 标签类型可划为分类型和统计型这两种类型,其中分类型用于刻画用户属于哪种类型,如是男是女、是否是会员、 + 是否已流失等标签,统计型标签用于刻画统计用户的某些行为次数,如历史购买金额、优惠券使用次数、近30日登陆次数等 + 标签,这类标签都需要对应一个用户相应行为的权重次数; +- 开发方式: + - 开发方式可分为直接统计型开发和算法预测型开发两大开发方式。其中: + - 直接统计型开发,可直接从数据仓库中各主题表建模加工而成; + - 算法预测型开发,需要对数据做机器学习的算法处理得到相应的标签; +- 是否互斥标签: + - 对应同一级类目下(如一级标签、二级标签),各标签之间的关系是否为互斥,可将标签划分为互斥关系和 + 非互斥关系。 + - 例如对于男、女标签就是互斥关系,同一个用户不是被打上男性标签就是女性标签,对于高活跃、中活跃、低 + 活跃标签也是互斥关系; +- 用户唯一标识: + - 用于刻画该标签是打在用户唯一标识(userid)上,还是打在用户使用的设备(cookieid)上。可用U、C等字 + 母分别标识userid和cookieid维度。 + + ![]({{site.baseurl}}/img-post/标签体系-7.png) + +- 标签命名示例: + ![]({{site.baseurl}}/img-post/标签体系-8.png) + +#### 2.2. 用户标签需求描述 + +- 标签id +- 标签名称 +- 标签汉语 +- 标签主题 +- 一级标签id +- 一级标签 +- 二级标签id +- 二级标签 +- 标签类型 +- 开发方式 +- 是否互斥 +- 更新频率 +- 标签算法规则描述 + +# 3. 用户价值标签 + +#### 3.1. RFM + +- 最近一次消费 (Recency) + - 理论上,上一次消费时间越近的顾客应该是比较好的顾客,对提供即时的商品或是服务也最有可能会有反应。 + - 示例: + ![]({{site.baseurl}}/img-post/标签体系-1.png) + +- 消费频率 (Frequency) + - 最常购买的消费者,忠诚度也就最高。 + - 示例: + ![]({{site.baseurl}}/img-post/标签体系-3.png) + +- 消费金额 (Monetary) + - 消费金额越高,客户价值越高。 + - 示例: + ![]({{site.baseurl}}/img-post/标签体系-2.png) + +- 口径规则 + - >注意:理论上M值和F值是一样的,都带有时间范围。 + - 示例: + - ![]({{site.baseurl}}/img-post/标签体系-4.png) + +- 维度交叉分析 + - 八类人群 + - 重要价值客户(111) + - RFM都很高,提供VIP服务。 + - 重要保持客户(011) + - 最容易转化成第一类客户的群体,一定要想办法提高他们的消费频率。 + - 重要发展客户(101) + - 主动保持联系,提高复购。 + - 重要挽留客户(001) + - 客户消费频率低和最近消费时间间隔比较远,但是消费金额高,这种用户即将流失,要主动联系用户,调查问题出在哪里,想办法挽回。 + - 一般价值客户(111) + - 一般保持客户(011) + - 一般发展客户(101) + - 一般挽留客户(001) + - ![]({{site.baseurl}}/img-post/标签体系-6.png) + - 示例: + ![]({{site.baseurl}}/img-post/标签体系-5.png) + +#### 3.2 流失 + +- 流失定义 + - 回访率: + - 30日内,首次访问用户,回访的比率。 + - 流失率: + - 30日内,首次访问后,无回访的既是流失用户。 + +#### 3.3. 活跃 + +- 活跃度: + - 高,0.2 + - 中,0.16 + - 低,0.64 + - >遵循:二八原则。 + +# 4. 用户属性主题标签 + +- 性别 + - 释义:身份证标识的性别 + - 标签: + - 男 + - 女 + - 其他未知 +- 省份 + - 安徽 + - 北京 + - ... +- 年龄/分层 + - 释义:对平台用户年龄进行分群分析 + - 标签: + - 新生婴儿0~28天 + - 婴儿(28天~1年) + - 幼儿(1~4年) + - 儿童(5~13) + - 少年(14~18) + - 青年(19~44) + - 中年(45~59) + - 老年(60以上) +- 电话号码所在区域/分层 + - 释义:一二三四线城市 or 城乡标识 + - 标签: + - 一线 + - 二线 + - 三线 + - 四线 + - 乡镇 + - 城郊 + - 农村 +- 是否临时账户 + - 释义:为第三方账号登录,没有进行过账号验证绑定的账号 + - 标签: + - 是 + - 否 +- 注册时间 + - 释义:用户的注册日期,格式yyyy-mm-dd hh:mm:ss + - 标签: + - 2022/9/25 11:10 +- 新老用户标识 + - 释义:基于用户注册时间及订单业务情况建模分析 + - 标签: + - 新用户 + - 老用户 +- 教育程度 + - 释义:用户的学历信息 + - 标签: + - 博士及以上 + - 研究生 + - 本科 + - 大专 + - 高中 + - 初中及以下 +- 身高 + - 标签: + - 175cm +- 体重 + - 释义: + - 标签: + - 65kg +- 职业类型 + - 白领 + - 学生 + - 个体 + - 公务员 + - 工人 + - 农民 +- 收入水平 + - 释义:根据各因子模型预测用户收入等级 + - 高 + - 中 + - 低 +- 星座 + - 释义:根据用户生日进行分群 + - 白羊/金牛/双子/狮子等12星座 +- 婚姻状况 + - 释义:根据各因子模型分析结婚与否 + - 标签: + - 是 + - 否 +- 生育状态 + - 释义:根据各因子模型预测用户生育情况 + - 标签: + - 未生育 + - 备孕 + - 怀孕 + - 已生育 +- 是否有老人 + - 释义:识别是否有60以上 + - 标签: + - 是 + - 否 +- 是否有小孩 + - 释义:识别是否有小于10岁以下 + - 标签: + - 是 + - 否 +- 是否二胎 + - 释义:是否有2个小于10岁以下 + - 标签: + - 是 + - 否 + + +# 5. 用户行为主题标签 + +- 订单评价 + - 好评 + - 中评 + - 差评 +- 现金券使用次数 +- 现金券使用额度 +- 虚拟现金券使用次数 +- 虚拟现金券使用额度 +- 邀请注册量 +- 邀请新客量 +- 首单营销方式 + - 首单正常购买 + - 首单免费礼物 + - 首单新人价商品 + - 首单优惠券 + - 首单新人专享优惠商品组 + - 首单红包 +- 近30天行为 + - 近30天购买次数(含退拒) + - 近30天购买金额(含退拒) + - 近30天购物车次数 + - 近30天购物车放弃数 + - 近30天购物车提交商品数 + - 近30天客单价 + - 近30天活跃天数 +- 近7天行为 + - 近7天购买次数(含退拒) + - 近7天购买金额(含退拒) + - 近7天客单价 + - 近7天购物车次数 + - 近7天购物车放弃数 + - 近7天购物车提交商品数 + - 近7天活跃天数 +- 单笔订单最小金额 + - 单笔订单最小金额 + - 单笔订单最大金额 +- 首单距今时间 +- 尾单距今时间 +- 营销方式敏感度 + - 满减 + - 满返 + - 满赠 + - 拼团 + - 多件多折 + - 免费礼物 + - 现金券 +- 访问深度 + - TODO +- 购买阶段近5日 + - 加购未下单 + - 下单未支付 + - 浏览未购买 + - 未付款成功 +- 高频活跃时间段 + - 上午 + - 中午 + - 下午 + - 晚上 + - 凌晨 +- 购买品类 + - 购买品类单一 + - 多品类购买 +- 邮件渠道活跃 + - 打开活跃度 + - 转化活跃度 + - 转化偏好 +- 邮件渠道活跃时间段 + - 上午 + - 中午 + - 下午 + - 晚上 + - 凌晨 +- 短信渠道活跃 + - 打开活跃度 + - 转化活跃度 + - 转化偏好 + - 时间偏好 + - 上午 + - 中午 + - 下午 + - 晚上 + - 凌晨 +- push活跃 + - 周活跃度 + - 月活跃度 + - 时间偏好 + - 上午 + - 中午 + - 下午 + - 晚上 + - 凌晨 + +- 渠道黑名单 + - EDM黑名单 + - EDM灰名单 + - SMD黑名单 +- 最近下单距今天数 +- 最近加购距今天数 +- 站内广告偏好 + - 悬浮 + - 弹窗 + - 首页轮播 + - 站内信 + - ... +- 商品 + +- 近7天行为 + - 最近7天浏览次数最多的未购买的类目(三级类目) + - ... +- 近3天行为 + - 最近3天浏览次数最多的未购买的类目(三级类目) + - 最近3天搜索未购买的类目(三级类目 + - ... +- 近1天行为 + - 最近1天订单情况(24小时后) + - ... +- 最近行为 + - 最近一次搜索未购买的类目(三级类目) + - 最近一次收藏商品类目(三级类目) + - 最近一次访问app日期 + - 上一次支付成功距今天数 + - ... +- 历史行为 + - 历史收藏商品中数量最多的类目(三级类目) + - ... +- 收藏属性 + - 收藏商品发生降价 + - 收藏商品库存不足20件 + - 收藏商品参与商品组优惠活动 + - ... +- 购物车属性 + - 购物车商品金额 + - 购物车商品发生降价 + - 购物车商品参与商品组优惠活动 + - 购物车商品数目最多的类目(三级类目) + +- 活跃地 + - 最近7日常登陆地 + + +# 6. 用户偏好 + +#### 6.1. 价格敏感度 +- 低 +- 中 +- 高 + +#### 6.2. 优惠偏好 + +- 优惠券 +- 折扣 +- 赠品 +- 满减 +- 包邮 + +#### 6.3. 品类偏好 + +- 层级 + - 一级品类 + - 二级品类 + - 三级品类 + - 四季品类 +- 类目 + - 白酒 + - 西餐 + - 游戏手柄 + - 单反相机 + - 等等 + +#### 6.4. 主题偏好 + +- 户外 + - 露营 + - 徒步 + - 野钓 +- 车友 + - 越野 + - 环游 + - 自驾游 +- 美妆 + - 发型 + - 抗衰 + - 去皱纹 + - 去法令纹 + - 祛斑 +- 穿搭 + - 西装 + - 机能风 + - 仙女 + - 纯欲 + - 职场风 +- 发烧友 + - 航模 + - 无线电 + - 手办 +- 美食 + - 西餐 + - 粤菜 + - 轻食 + - 减脂餐 + - 方便菜 +- 宠物 + - 营养餐 + - 猫奴 +- 等等 + +#### 6.7. 渠道偏好 +- 站内广告 +- 站外 +- 小红书种草 +- 天猫 +- 聚划算 +- 促销活动 +- ... +#### 6.6. 广告内容偏好 +- 折扣 +- 品类 +- 新品 +- 热销 + +#### 6.5. 购买行为偏好 +- 闪购 +- Category页面 +- 新品 +- 包邮 +- best deals +- 促销 +- 搜索 + +#### 6.6. 购买单价偏好 + +- 偏好低价 +- 中等价位 +- 高价位 + +#### 6.7. 店铺偏好 + +- 店铺星级 +- 店铺评分 +- 探新店 +- 店铺风格 +- 经营年限 +- 店铺区域 + +# 7. 用户价值标签 + +#### 7.1. 用户资产 + +- 手机品牌 + - 高档 + - 中档 + - 低档 +- 汽车资产 + - 高档 + - 中档 + - 低档 +- 房产 + - 有房 + - 位置 + - 一线城市 + - 二线城市 + - 三线及以下城市 + - 区位 + - 中心区 + - 郊区 + - 无房 + +#### 7.2. 用户等级 + +- 会员 + - 购卡激活会员 + - 积分试用会员 + - 赠送会员 + - 补偿会员 + - 历史会员 + - 非会员 + - 白银会员 + - 黄金会员 + - 白金会员 + - 钻石会员 +- 用户身份 + - 超级用户 + - 金牌用户 + - 银牌用户 + - 铜牌用户 + - 正式会员 + - 试用会员 + - 赠送会员 +- 用户价值 + - 购买客单价 + - 下单次数 + - 首单用户 + +#### 7.3. 生命周期 + +- 安装距今天数 + - 7 天内 + - 7 ~ 30 天 + - 30 ~ 180 天 + - 180 天以上 +- 注册状态 + - 已注册 + - 未注册 +- 历史购买状态 + - 购买过 + - 未购买过 +- 预流失用户 + - 高风险 + - 中风险 + - 低风险 +- 老用户召回 + - 是 + - 否 +- 投诉后流失用户 + - 是 + - 否 +- 第一次购买后流失用户 + - 是 + - 否 +- 最近一次活跃距离今天天数 + - 7 天内 + - 7 ~ 30 天 + - 30 ~ 180 天 + - 180 天以上 +- 最近一次购买距离今天天数 + - 7 天内 + - 7 ~ 30 天 + - 30 ~ 180 天 + - 180 天以上 + +#### 7.4. 用户购买力 + +- 重复咨询 + - 高频 + - 中频 + - 低频 +- 购买风格 + - 搜索购买型 + - 浏览购买型 + - 促销购买型 +- 平均客单价 + - 高价格段 + - 中价格段 + - 低价格段 +- 购买价格段偏好 + - 高价格段 + - 中价格段 + - 低价格段 +- 账户优惠券 + - 账户内是否有可用优惠券 + - 账户内可用优惠券到期日期(有多个优惠券) + - 账户内积分值 +- 支付偏好 + - 银行卡 + - 信用卡 + - 储蓄卡 + - 支付宝 + - 花呗 + - 余额 + - 微信 + - 白条 + - ... + +#### 7.5. 用户诚信度 + +- 用户诚信度 + - 高诚信 + - 无异常 + - 低诚信 +- 用户忠诚度 + - 新用户 + - 熟客 + - 高忠诚 + +- 退货频率 + - 高频 + - 中频 + - 低频 +- 投诉频率 + - 高频 + - 中频 + - 低频 + +#### 7.6. 用户活跃度 +- 活跃状态 + - 高活跃用户 + - 中活跃用户 + - 低活跃用户 + - 流失用户 +- 消费状态 + - 购买次数 + - 平均购买金额 + - 最近一次购买间隔 +- 活跃度 + - 高 + - 中 + - 低 +- 购买活跃度 + - 高 + - 中 + - 低 +- 流失度 + - 高 + - 中 + - 低 + +#### 7.7. RFM + +- 重要价值用户 +- 重要发展用户 +- 重要保持用户 +- 重要挽留用户 +- 一般价值用户 +- 一般发展用户 +- 一般保持用户 +- 一般挽留用户 + +#### 8. 用户风控主题标签 + +- 异常挂起子单数 +- 终结类型 + - 连签1单 + - 连签2单 + - 连签3单 + - 连拒1单 + - 连拒2单 + - 连拒3单 +- 签收率 +- 联系不上率 +- 历史付款失败订单数 +- 问题类型 + - 退货率 + - 赔付率 + - 退换货 / 货品异常 + - 多账户使用同样的收获地址 +- 问题来源 + - 邮件表单渠道 + - 客服客诉电话 + - 商品评价 + - 商品退换货 +- 是否问题用户 + - 问题用户 + - 非问题用户 + - 未知用户 +- 问题反馈时间 + - 最近一次问题反馈时间 + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\346\240\207\347\255\276\346\250\241\345\236\213\345\274\200\345\217\221\346\265\201\347\250\213.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\346\240\207\347\255\276\346\250\241\345\236\213\345\274\200\345\217\221\346\265\201\347\250\213.md" new file mode 100644 index 00000000000..26a1dd9f405 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\346\240\207\347\255\276\346\250\241\345\236\213\345\274\200\345\217\221\346\265\201\347\250\213.md" @@ -0,0 +1,90 @@ +--- +layout: post +title: 用户画像:标签模型开发流程 +subtitle: 开发流程阶段 & 各阶段关键产出 +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +# 开发流程阶段 + +#### 目标解读 + +- 在建立用户画像前,首先需要明确用户画像服务于企业的对象,再根据业务方需求,明确未来产品建设目标和用户画像分析之后的预期效果。 +- 一般而言,用户画像的服务对象包括运营人员和数据分析人员,不同业务方对用户画像的需求有不同的侧重点; +- 就运营人员来说,他们需要分析用户的特征、定位用户行为偏好,做商品或内容的个性化推送以提高点击转化率,所以画像的侧重点就落在了用户个人行为偏好上; +- 就数据分析人员来说,他们需要分析用户行为特征,做好用户的流失预警工作,还可根据用户的消费偏好做更有针对性的精准营销。 + +#### 任务分解与需求调研 + +- 经过第一阶段的需求调研和目标解读,明确了用户画像的服务对象与应用场景; +- 接下来需要针对服务对象的需求侧重点,结合产品现有业务体系和“数据字典”规约实体和标签之间的关联关系,明确分析维度。 +- 需要从用户属性画像、用户行为画像、用户偏好画像、用户群体偏好画像等角度去进行业务建模。 + +#### 需求场景讨论与明确 + +- 在本阶段,数据运营人员需要根据与需求方的沟通结果,输出产品用户画像需求文档; +- 在该文档中明确画像应用场景、最终开发出的标签内容与应用方式,并就该文档与需求方反复沟通并确认无误。 + +#### 应用场景与数据口径确认 + +- 经过第三个阶段,明确了需求场景与最终实现的标签维度、标签类型后,数据运营人员需要结合业务与数据仓库中已有的相关表,明确与各业务场景相关的数据口径。 +- 在该阶段中,数据运营方需要输出产品用户画像开发文档,该文档需要明确应用场景、标签开发的模型、涉及的数据库与表以及应用实施流程。 +- 该文档不需要再与运营方讨论,只需面向数据运营团队内部就开发实施流程达成一致意见即可。 + +#### 特征选取与模型数据落表 + +- 本阶段中数据分析挖掘人员,需要根据前面明确的需求场景进行业务建模,写好 HQL 逻辑; +- 将相应的模型逻辑写入临时表中,并抽取数据校验是否符合业务场景需求。 + +#### 线下模型数据验收与测试 + +- 数据仓库团队的人员将相关数据落表后,设置定时调度任务,定期增量更新数据。 +- 数据运营人员需要验收数仓加工的 HQL 逻辑是否符合需求,根据业务需求抽取表中数据查看其是否在合理范围内; +- 如果发现问题要及时反馈给数据仓库人员调整代码逻辑和行为权重的数值。 + +#### 线上模型发布与效果追踪 + +- 经过第六阶段,数据通过验收之后,会部署上线; +- 上线后通过持续追踪标签应用效果及业务方反馈,调整优化模型及相关权重配置。 + + +# 各阶段关键产出 + + ![]({{site.baseurl}}/img-post/用户画像-33.png) + +#### 标签开发 + +- 根据业务需求和应用场景梳理标签指标体系,调研业务上定义的数据口径,确认数据来源,开发相应的标签。 +- 标签开发在整个画像项目周期中占有较大比重。 + +#### ETL调度开发 + +- 梳理需要调度的各任务之间的依赖关系; +- 开发调度脚本及调度监控告警脚本; +- 上线调度系统。 + +#### 打通服务层接口 + +- 为了让画像数据走出数据仓库,应用到用户身上,需要打通数据仓库和各业务系统的接口。 + +#### 画像产品化 + +- 需要产品经理与业务人员、技术开发人员一起,对接业务需求点和产品功能实现形式,画产品原型、确定工作排期。 +- Java Web 端开发完成后,需要数据开发人员向对应的库表中灌入数据。 + +#### 开发调优 + +- 在画像的数据和产品端搭建好架构、能提供稳定服务的基础上,为了让调度任务执行起来更加高效、提供服务更加稳健,需要对标签计算脚本、调度脚本、数据同步脚本等相关计算任务进行重构优化。 + +#### 面向业务方推广应用 + +- 用户画像最终的价值产出点,是业务方应用画像数据进行用户分析,多渠道触达运营用户,分析ROI,提升用户活跃度或营收。 +- 面向业务人员推广画像系统的使用方式、提供针对具体业务场景的解决方案显得尤为重要。 +- 在该阶段,相关人员需要撰写画像的使用文档,提供业务支持。 + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\346\240\207\347\255\276\346\250\241\345\236\213\347\232\204\350\256\276\350\256\241\350\247\204\350\214\203.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\346\240\207\347\255\276\346\250\241\345\236\213\347\232\204\350\256\276\350\256\241\350\247\204\350\214\203.md" new file mode 100644 index 00000000000..fdcb7277478 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\346\240\207\347\255\276\346\250\241\345\236\213\347\232\204\350\256\276\350\256\241\350\247\204\350\214\203.md" @@ -0,0 +1,240 @@ +--- +layout: post +title: 用户画像:标签模型的设计规范 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +# 1. 标签规范 + +#### 1.1. 规范对象 + +- 标签编码 +- 标签命名 +- 标签描述(定义) +- 标签计算逻辑 +- 标签取值规则 +- 更新周期 +- 安全等级 +- 物理存储信息 +- 标签负责人 +- 开发时间(版本) + +#### 1.2. 规范内容 + +- 格式规范 +- 用词规范 +- 内容规范 +- 取值规范 + +#### 1.3. 标签分类 + +- 主分类标签 + - 一级标签 + - 二级标签 + - 三级标签 +- 业务标签 + - 四级标签 +- 属性标签 + - 五级标签 + +# 2. 标签编码 + +![]({{site.baseurl}}/img-post/标签体系-7.png) + +- 标签主题: + - 用于刻画属于那种类型的标签,如用户属性、用户行为、用户消费、风险控制等多种类型,可用A、B、C、D等 + 字母表示各标签主题; + +- 标签类型: + - 标签类型可划为分类型和统计型这两种类型,其中分类型用于刻画用户属于哪种类型,如是男是女、是否是会员、 + 是否已流失等标签,统计型标签用于刻画统计用户的某些行为次数,如历史购买金额、优惠券使用次数、近30日登陆次数等 + 标签,这类标签都需要对应一个用户相应行为的权重次数; + +- 开发方式: + - 开发方式可分为直接统计型开发和算法预测型开发两大开发方式。其中: + - 直接统计型开发,可直接从数据仓库中各主题表建模加工而成; + - 算法预测型开发,需要对数据做机器学习的算法处理得到相应的标签; + +- 是否互斥标签: + - 对应同一级类目下(如一级标签、二级标签),各标签之间的关系是否为互斥,可将标签划分为互斥关系和 + 非互斥关系。例如对于男、女标签就是互斥关系,同一个用户不是被打上男性标签就是女性标签,对于高活跃、中活跃、低 + 活跃标签也是互斥关系; + +- 用户唯一标识: + - 用于刻画该标签是打在用户唯一标识(userid)上,还是打在用户使用的设备(cookieid)上。可用U、C等字 + 母分别标识 userid 和 cookieid 维度。 + +# 3. 标签命名 + +#### 3.1. 原子类标签命名 + +- 【业务主题】+【原子指标】+【场景】+【时空修饰词】 + +- 业务主题: + - 商品类目: + - 例如:电子产品类目、服装类目; + - 流量渠道: + - 例如:小红书、抖音直播、天猫旗舰店等; + - 业务内容: + - 例如:客户投诉、供应链、仓储等; + - 用户对象: + - 例如:VIP 用户、新客户、流失客户等; + +- 原子指标: + - 基础属性类标签; + +- 场景: + - 如:电商交易、线下交易、门店零售交易、批发交易等; + +- 时空修饰词: + - 时间维度: + - 最近一天、最近一周、最近一月等; + - 空间维度: + - 主城区、一线城市、华东区域、大中华区域等; + +#### 3.2. 统计类标签命名 + +- 【业务主题】+【原子指标】+【场景】+【时空修饰词】+【统计计算方法】+【可选修饰词】 + +- 统计计算方法: + - 例如:总和、平均值、最大值等; + +- 可选修饰词: + - 自定义修饰内容; + +#### 3.3. 预测类标签 + +- 【业务主题】+【原子指标】+【场景】+【时空修饰词】+【预测内容】+【预测方法】+【可选修饰词】 + +- 预测内容: + - 例如:性格类型、消费能力、动漫喜好度、冲动消费偏好类型等; + +- 预测方法: + - 例如:算法模型(逻辑回归)等; + + +# 4. 标签描述(定义) + +#### 4.1. 包含内容 + +- 标签含义; +- 业务场景; +- 适用范围; +- 取值规则; + +#### 4.2. 撰写原则 + +- 原则: + - 详细描述标签的含义; + - 宜繁不宜简; + +- 防止出现: + - 歧义、模糊、多义; + +# 5. 标签计算逻辑 + +#### 5.1. 统计类标签计算逻辑 + +- 函数; +- 计算公式; +- 统计方法; + +#### 5.2. 规则类标签计算逻辑 + +- 统计规则; +- 计算公式; +- 正则表达式; +- 条件判断逻辑; +- 阈值分段设定规则; + +#### 5.3. 预测类标签计算逻辑 + +- 重要特征项; +- 正负样本定义; +- 学习样本; +- 模型选型; +- 模型结构; +- 模型输出结果形式; +- 预测结果性能指标。 + +# 6. 标签取值 + +#### 6.1. 标签取值类型 + +- 整数 +- 浮点数 +- 文本 +- 日期、 +- 枚举、 +- K-V 等; + +#### 6.2. 标签取值字典 + +- 各种可能取值的枚举 +- 例如: + - 性别取值:男、女; + - 消费能力取值:低、中、高; + +# 7. 更新周期 + +- 每 1 小时; +- 每 6 小时; +- 每 12 小时; +- 每 1 天; +- 每 7 天; +- 每 15 天; +- 每 1 月; +- 每 3 月; +- 每 6 月; + +# 8. 使用权限 + +#### 8.1. 部门 & 人员权限 + +- 明确可以查看标签的部门、人员角色; +- 无权限的用户,不得获取、查看相关数据; + +#### 8.2. 安全等级 + +- 明确安全等级,根据安全等级划分用户权限; + +# 9. 物理存储信息 + +#### 9.1. 标签映射表 + +- 需要创建标签与物理表字段的标签映射表; + +- 标签映射内容: + - 表名 + - 字段名 + +#### 9.2. 标签映射生成方式 + +- 自动生成: + - 最好是创建标签时、系统自动生成; +- 手动回填: + - 也可以在开发后工程师回填,但容易出现问题; + +# 10. 标签负责人 + +#### 10.1. 业务负责人 + +- 需求部门接口人,数据的需求方; +- 原始数据的提供人; + +#### 10.2. 开发负责人 + +- 标签开发负责人; + +# 11. 开发时间(版本) + +- 标签上线时间/版本,有助于系统更新迭代升级; + + \ No newline at end of file diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\346\240\207\347\255\276\347\232\204\345\237\272\346\234\254\346\246\202\345\277\265.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\346\240\207\347\255\276\347\232\204\345\237\272\346\234\254\346\246\202\345\277\265.md" new file mode 100644 index 00000000000..f5d77b21b1e --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\346\240\207\347\255\276\347\232\204\345\237\272\346\234\254\346\246\202\345\277\265.md" @@ -0,0 +1,128 @@ +--- +layout: post +title: 用户画像:用户标签的基本概念 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + +# 1. 用户标签概念 + +#### 1.1. 用户标签 + +- 标签,原意是标明物品的品名、重量、体积、用途等信息的简要标牌,例如:商品标签、图书标签、车检标签、文件标签、服装吊牌等。 +- 从这个概念衍生到网络标签,是人工或系统自动或用户自发的,通过相关性很强的关键字对事物或内容进行描述,帮助人们分类内容,以便于检索和分享。 +- 例如:我们也可以给“人”这个对象打上男人或女人,老人或青年的标签。可见,标签也有维度或分类,而属性也是一种标签。 +- 标签往往映射为某一对象的属性,包括固有属性和动态属性,一般都需要结构化到字段粒度,保障可被后续数据服务便捷使用。它面向数据应用的业务端,核心解答的是数据怎么用、资产价值在哪里的问题。 +- 标签示例: + ![]({{site.baseurl}}/img-post/标签体系-8.png) + +#### 1.2. 用户分类 + +- 分类,就是指按照种类、等级、性质或特征的归类。 + - 分类,把相同属性或特征的“对象”归集在一起,形成不同的类别,方便人们通过类别来对“对象”进行的查询、识别、管理和使用。 + - “对象”可以是人、产品、物料或其他实体,例如:人可以分为男人、女人,也可以分为老年人、中年人、青少年。 + +- 用户属性 + - 属性,是事物所具有的性质或特征,重点强调的是事物本身,例如:人的性别、身高、胖瘦、年龄、性格等都是人的属性。 + - 可以将某个事物的属性抽象出来作为事物的分类,如我们上边举的例子,男人、女人是按照性别属性对人的分类;老年人、中年人、青少年是按照年龄属性对人的分类。 + - 事物可以按照属性来分类,分类也是事物的属性之一。 + +#### 1.3. 用户分类 VS 用户标签 + +- 分类和标签有时候没有明确的分界线,分类可以作为一个标签,标签也可以抽象出分类。 +- 分类由于只能隶属于一个,所以往往带有武断和不恰当的色彩,它是一个“is a”的问题,属于本质论的范畴,而对事物的“本质”的认定,严格来说,这事只有上帝才能做,换句话说,谁做都不合适。 +- 分类是一种严谨的数据组织方式,一般按照一个或多个维度自上而下、从整体到明细的穷举,遵循“相互独立,完全穷举” 的原则。 +- 标签则不同,它是一个“has a”的问题,说某个东西有某种属性,要求就没那么严格了。 +- 标签是一种灵活的数据组织方式,放弃大而全的框架,基于业务场景自下而上地倒推标签需求。 +- 不同点归纳: + - 分类一般是面向团队或组织的,注重标准化;而标签可以面向组织,也可以是面向个人,注重的是个性化。 + - 分类具有排他性,分类之间是独立的、不能交叉;而标签允许交叉,标签之间可以相互关联、相互依赖。 + - 分类体系需要事先规划,在标准化的框架下进行使用;而标签可以静态的,也可以是动态的,允许随时添加。 + - 分类注重结构化,具有层级控制,是一个树状结构;而标签的结构是松散、灵活、开放的,整体看是一个网状结构。 + ![]({{site.baseurl}}/img-post/用户分类标签-1.jpg) + + +# 2. 用户标签分类 + +#### 2.1. 按照维度划分 + + - 自然属性 + - 社会属性 + - 行为习惯 + - 购买能力 + - 消费习惯 + - 偏好特征 / 兴趣爱好 + +#### 2.2. 按照层级划分 + + ![]({{site.baseurl}}/img-post/用户画像-2.png) + +#### 2.3. 按照生成方式划分 + +- 统计类标签 + - 又叫:事实标签 + - 可以直接提取 + - 包括一些基本信息,比如:年龄、性别、城市、职业等 + - 也可以从用户数据、行为数据、消费数据中统计得出 + - 这些标签来自基于行为数据的统计信息,比如:1日登录次数、7日登录次数、7日下单次数等 + - 这类标签,构成了用户画像的基础 + +- 规则类标签 + - 又叫:建模标签 + - 在统计指标基础上通过规则生成,是对统计类指标的有效补充 + - 需要基于用户行为自定义规则 + - 比如: + - 活跃用户,定义规则为每天登录一次以上的用户 + - 高净值用户,消费总额在某个阈值以上 + - 在开发过程中,运营人员对业务更熟悉,数据维护人员对数据结构、分布、特征更熟悉,所以多数时候由运营人员和数据人员共同搭建维护; + +- 预测类标签 + - 又叫:机器学习标签 + - 是非确定性标签(前两项都属于确定性标签) + - 需要基于已有的信息“预测”用户特征,需要挖掘才能获得 + - 实际开发过程中,这类项目开发周期长、成本高,属于探索性项目 + - 有些公司使用外包人员、手动打标签,效果可能会更好、而且投入也很低 + + +# 3. 用户标签应用 + +- 标签(网络标签)是随着互联网发展产生的,最早用在博客、文章的内容分类中,方便用户管理和聚合内容。 +- 随着大数据的发展,标签体系的作用也越来越大,被互联网企业广泛使用,通过特征集合并关联打标签的对象,对分析对象生成画像,挖掘对象的价值。 +- 例如:各大互联网APP(淘宝、今日头条、抖音等)都有一个基于标签体系的推荐引擎模块,通过用户静态属性和行为属性给用户打标签,形成360度用户画像,然后根据用户的偏好将信息或产品推送给用户。 + +- 归纳起来,用户标签包括以下价值: + - 用户洞察: + - 深入了解产品用户,指导业务规划发展 + - 数据分析: + - 丰富用户分析维度,提高分析应用效率 + - 精细化运营: + - 基于用户分层进行差异化运营策略和动作,提升运营效果 + - 产品化应用: + - 应用于产品个性化功能/CRM/数据产品中,增强产品能力 + +#### 3.1. 精准营销与推荐系统 + +- 用户画像描述了用户日常的购物偏好和习惯,例如某个用 户对价格敏感,偏好数码产品。则推荐系统可以根据用户的偏好,给用户推荐一些折扣 优惠较大的数码类产品。 +- 用户可能原本没有打算购买,但是看到折扣优惠较大,购买的 可能性提高,对电商来说就更容易促成一笔交易。 + +#### 3.2. 推送系统 + +- 推送系统是往移动端 APP 下发通知的消息系统。假设某个新闻类 APP, 用户画像系统记录了某个用户喜欢在工作日的早上 8 点到 8 点 45 分登录应用看新闻, 而周末没有这样的习惯,很可能该用户是在上班通勤的时间段使用该 APP。 +- 那推送系统 就可以在工作日 8 点左右给用户推送重要的新闻消息,而在休息日不推送。这样可以增 加工作日用户使用该 APP 的可能性,而周末又能避免打扰用户,提高用户体验。 +- 也提可以结合位置与时间,利用画像预测兴趣,动态选取内容实现个性化推荐。 + +#### 3.3. 风险控制与反作弊 + +- 通过画像系统收集到的用户行为数据,例如使用时长、点击率、页面请求次数等,根据这些指标可以通过算法甄别出流量作假的挂机账号,可以评估渠道带来的真实流量从而调整广告投放。 + +#### 3.4. 用户人群分析与 BI 系统 + +- 通过对全局用户画像数据的某些维度进行聚合,则可以分析产品的用户人群特点。例如通过检验手机品牌价值与消费能力是否呈现正相关,进而对用户群进行分层。 +- 针对不同的用户群体,制定不同的运营策略,更好地服务不同群体。 + + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\346\240\207\347\255\276\350\220\275\350\241\250\345\255\230\345\202\250.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\346\240\207\347\255\276\350\220\275\350\241\250\345\255\230\345\202\250.md" new file mode 100644 index 00000000000..100e4f8b51b --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\346\240\207\347\255\276\350\220\275\350\241\250\345\255\230\345\202\250.md" @@ -0,0 +1,309 @@ +--- +layout: post +title: 用户画像:用户标签落表存储 +subtitle: Hive & MySQL & Hbase & Elasticsearch & Spark Streaming +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +[//]: # (# 标签存储) + +[//]: # () +[//]: # (- 横表不适合存储标签数据;) + +# Hive + +#### Hive 存储标签表 + +- 适用场景: + - 存储数据相关标签表、人群计算表的表结构设计; + +- Hive 数仓: + - 建立用户画像,首先需要建立数据仓库。 + - Hive 是基于 Hadoop 的数据仓库工具,依赖于 HDFS 存储数据,提供的 SQL 语言可以查询存储在 HDFS 中的数据。 + - 开发时一般使用 Hive 作为数据仓库,存储标签和用户特征库等相关数据。 + +- Hive 宽表 ETL 花费时间较长 + - 如果将用户标签开发成一张大的宽表,在这张宽表下放几十种类型标签,那么每天该画像宽表的 ETL 作业将会花费很长时间,而且不便于向这张宽表中新增标签类型。 + - 要解决这种 ETL 花费时间较长的问题,可以从以下几个方面着手: + - 将数据分区存储,分别执行作业; + - 标签脚本性能调优; + - 基于一些标签共同的数据来源开发中间表。 + +- 分区存储: + - 人口属性表:dw.userprofile_attritube_all; + - 行为属性表:dw.userprofile_action_all; + - 用户消费表:dw.userprofile_consume_all; + - 风险控制表:dw.userprofile_riskmanage_all; + - 社交属性表:dw.userprofile_social_all + ![]({{site.baseurl}}/img-post/用户标签存储-5.png) + + - 人口属性宽表示例: + ![]({{site.baseurl}}/img-post/用户标签存储-6.png) + + - partition 分区提高查询效率: + -实现: + - 为了提高数据的插入和查询效率,在 Hive 中可以使用分区表的方式,将数据存储在不同的目录中。 + - 在 Hive 使用 select 查询时一般会扫描整个表中所有数据,将会花费很多时间扫描不是当前要查询的数据,为了扫描表中关心的一部分数据,在建表时引入了 partition 的概念。 + - 在查询时,可以通过 Hive 的分区机制来控制一次遍历的数据量。 + - 缺点: + - 用户的每个标签都插入到相应的分区下面,但是对一个用户来说,打在他身上的全部标签存储在不同的分区下面。 + + +#### Hive 标签汇聚 + +- 为了方便分析和查询,需要将用户身上的标签做聚合处理; +- 标签汇聚后,一个每个用户身上的全量标签汇聚到一个字段中; + +![]({{site.baseurl}}/img-post/用户标签存储-7.png) + +- 标签汇聚表结构设计: + - 标签字段 userlabels 格式为 `map`; + + ``` + CREATE TABLE `dw.userprofile_userlabel_map_all` + ( + `userid` string COMMENT 'userid', + `userlabels` map COMMENT 'tagsmap', + ) + COMMENT 'userid 用户标签汇聚' + PARTITIONED BY ( `data_date` string COMMENT '数据日期') + ``` + +- 汇聚过程(UDF 函数) + - 将用户身上的标签汇聚成 json 字符串,将按分区存储的标签进行汇聚; + ``` + insert overwrite table dw.userprofile_userlabel_map_all partition(data_date= "data_date") + select userid, + cast_to_json(concat_ws(',',collect_set(concat(labelid,':',labelweight)))) as userlabels + from “用户各维度的标签表” + where data_date= " data_date " + group by userid + ``` + +- 汇聚后用户标签的存储格式: + ![]({{site.baseurl}}/img-post/用户标签存储-8.png) + +- 将用户身上的标签进行聚合便于查询和计算。 + - 例如,在画像产品中,输入用户id后通过直接查询该表,解析标签id和对应的标签权重后,即可在前端展示该用户的相关信息; + +#### Hive 拉链表 实现 ID-Mapping + +- 适用场景: + - ID-Mapping; + - 用户在未登录 App 的状态下,在 App 站内访问、搜索相关内容时,记录的是设备 id(即cookieid)相关的行为数据。 + - 用户在登录 App 后,访问、收藏、下单等相关的行为记录的是账号 id(即userid)相关行为数据。 + - 虽然是同一个用户,但其在登录和未登录设备时、记录的行为数据之间是未打通的。 + - 通过 ID-MApping 打通 userid 和 cookieid 的对应关系,可以在用户登录、未登录设备时都能捕获其行为轨迹。 + +- 实现方式:hive 拉链表 + - 通过 Hive 的 ETL 工作完成 ID-Mapping 的数据清洗工作; + - 缓慢变化维是在维表设计中常见的一种方式,维度并不是不变的,随时间也会发生缓慢变化。 + - 如用户的手机号、邮箱等信息可能会随用户的状态变化而改变,再如商品的价格也会随时间变化而调整上架的价格。因此在设计用户、商品等维表时会考虑用缓慢变化维来开发。 + - 同样,在设计 ID-Mapping 表时,由于一个用户可以在多个设备上登录,一个设备也能被多个用户登录,所以考虑用缓慢变化维表来记录这种不同时间点的状态变化。 + + +# MySQL + +- MySQL 优点: + - 对于量级较小的数据,MySQL 具有更快的读写速度,查找延迟低; + - 范围查询优势明显, 可以实现复杂的查询; + +- MySQL 缺点: + - 随着数据的增多, 插入性能递减; + - 完整存储所有数据, 不适合稀疏表。 + +#### MySQL 存储标签元数据 + +- 适用场景: + - 存储标签元数据、监控数据; + + ![]({{site.baseurl}}/img-post/用户标签存储-1.png) + +- 工作原理: + - MySQL 作为关系型数据库,在用户画像中可用于元数据管理、监控预警数据、结果集存储等应用中。 + - 相比于 Hive 适合于大数据量的批处理作业,对于量级较小的数据,MySQL 具有更快的读写速度。Web 端产品读写 MySQL 数据库会有更快的速度,方便标签的定义、管理。 + +#### MySQL 存储 ETL 监控信息 + +- 适用场景: + - 存储服务层同步监控数据,存储每天对 ETL 结果的监控信息。 + +- 工作原理: + - 将标签相关数据从Hive数仓向服务层同步的过程中,有出现差错的可能,因此需要记录相关数据在Hive中的数量及同步到 对应服务层后的数量,如果数量不一致则触发告警。 + - 在对画像的数据监控中,调度流每跑完相应的模块,就将该模块的监控数据插入MySQL中,当校验任务判断达到触发告警阈值时,发送告警邮件,同时中断后续的调度任务。待开发人员解决问题后,可重启后续调度。 + - 从整个画像调度流的关键节点来看,需要监控的环节主要包括对每天标签的产出量、服务层数据同步情况的监控等主要场景。 + - 用户标签系统的服务层,一般采用 HBase、Elasticsearch 等作为数据库存储标签数据供线上调用,将标签相关数据从 Hive 数仓向服务层同步的过程中,有出现差错的可能; + - 因此需要使用 MySQL 记录相关数据在 Hive 中的数量,及同步到对应服务层后的数量,如果数量不一致则触发告警。 + +#### MySQL 存储标签数据结果集 + +- 适用场景: + - 存储标签数据结果集; + - 电商、保险、金融等公司的客服部门的日常工作内容之一,是对目标用户群(如已流失用户、高价值用户等)进行主动外呼,以此召回用户来平台进行购买或复购,需要将 Hive 中存储的与用户身份相关的数据同步到客服系统中。 + +- 实现方式: + - 使用 Sqoop 将 Hive 中的标签数据迁移到 MySQL + + ``` + # -*- coding: utf-8 -*- + import os + import MySQLdb + import sys + def export_data(hive_tab, data_date): + sqoop_command = "sqoop export --connect jdbc:mysql://10.xxx.xxx.xxx:3306/mysql_database --username username --password password --table mysql_table --export-dir hdfs://nameservice1/user/hive/warehouse + /dw.db/" + hive_tab + "/data_date=" + data_date + " --input-fields-terminated-by '\001'" + os.system(sqoop_command) + print(sqoop_command) + + if __name__ == '__main__': + export_data("dw.userprofile_userservice_all", '20181201') + ``` + + ``` + sqoop export + --connect 指定JDBC连接字符串,包括IP 端口 数据库名称 \ + --username JDBC连接的用户名\ + --passowrd JDBC连接的密码\ + --table 表名\ + --export-dir 导出的Hive表, 对应的是HDFS地址 \ + --input fileds-terminated-by ‘,’ 分隔符号 + ``` + +# Hbase 存储标签 + +- 适用场景: + - 存储线上接口实时调用的数据; + +- Hbase 优点: + - HBase 是 KV 型数据库, 是不需要提前预设 Schema 的,添加新的标签时候比较方便 + - 列式存储 + - 标签经常变化不固定 + - 标签随业务变化比较大 + - 标签经常增加 + - 画像的数据量不小, HBase 可以存储海量数据; + +- 工作原理: + - HBase 是一个高性能、列存储、可伸缩、实时读写的分布式存储系统,同样运行在 HDFS 之上。 + - 与 Hive 不同的是,HBase 能够在数据库上实时运行,而不是跑 MapReduce 任务,适合进行大数据的实时查询。 + + - row key: + - 用来表示唯一一行记录的主键,HBase 的数据是按照 row key 的字典顺序进行全局排列的。 + - Rowkey 设计时需要遵循三大原则: + - 唯一性原则: + - rowkey 需要保证唯一性,不存在重复的情况。 + - 在用户画像系统中一般使用 **用户 id 作为 rowkey**。 + - 长度原则: + - rowkey 的长度一般为 10-100 bytes。 + - 散列原则: + - rowkey 的散列分布有利于数据均衡分布在每个 RegionServer,可实现负载均衡。 + - columns family: + - 指列簇,HBase 中的每个列都归属于某个列簇。 + - 列簇是表的 schema 的一部分,必须在使用表之前定义。 + - 划分 columns family 的原则如下: + - 是否具有相似的数据格式; + - 是否具有相似的访问类型。 + - 访问 HBase 中的行只有 3种 方式: + - 通过单个 row key 访问; + - 通过 row key 的正则访问; + - 全表扫描。 + - 由于 HBase 通过 rowkey 对数据进行检索,而 rowkey 由于长度限制的因素,不能将很多查询条件拼接在 rowkey 中,因此 HBase 无法像关系数据库那样、根据多种条件对数据进行筛选。 + - 一般地,HBase 需建立二级索引、来满足根据复杂条件查询数据的需求。 + +- 一级缓存: BlockCache + - MySQL 的 B+树 并不是把数据直接存放在树中, 而是把数据组成 页(Page) 然后再存入 B+树, MySQL 中最小的数据存储单元是 Page + - HBase 也一样, 其最小的存储单元叫做 Block, Block 会被缓存在 BlockCache 中, 读数据时, 优先从 BlockCache 中读取 + - BlockCache 是 RegionServer 级别的 + - BlockCache 叫做读缓存, 因为 BlockCache 缓存的数据是读取返回结果给客户端时存入的 + +- 二级缓存: + - 当查找数据时, 会先查内存, 后查磁盘, 然后汇总返回 + - 因为写是写在 Memstore 中, 所以从 Memstore 就能立刻读取最新状态 + - Memstore 没有的时候, 扫描 HFile, 通过布隆过滤器优化读性能 + +- HBase ETL 校验机制 + - 同步问题 + - 灌入到 hbase 中的数据一般直接应用到线上,反馈到用户那里; + - 所以在 hive 数据同步 hbase 数据的时候,需要做一些校验机制来保障结果的准确性; + - 防止在同步数据的过程中出现问题(比如hive中数据5000万条,同步到 hbase后才1000万条) + - 解决方案一:temp 临时表 + - hive 到 hbase 同步数据后,先在 hbase 中建立一个 temp 临时表,然后校验 hbase 的这个临时表和对应 hive 表的数量差异; + - 如果在可接受范围内,则将hbase的该临时表进行重命名为正式表; + - 解决方案二:标志状态位 + - hive 到 hbase 同步数据后,直接将数据写入正式表; + - 同时,在 hbase 中建立一张状态表,用于标志状态位; + - 当校验 hbase 的这张正式表和 hive 的数量差异在可接受范围内时,写入对应的状态表中。 + - 接口请求时,只读取状态位这张表中,最近日期的那张表; + - 所以如果 hbase 的数据同步异常,不会写入状态表中,也不会影响线上数据的读取; + +# Elasticsearch + +#### Elasticsearch 实现标签查询、人群计算、用户群多维透视分析 + +- 适用场景: + - 存储标签用于用户标签查询、用户人群计算、用户群多维透视分析; + +- 工作原理: + - Elasticsearch 是一个开源的分布式全文检索引擎,由于使用倒排索引机制,可以近乎实时地存储、检索数据,而且可扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。 + - 对于用户标签查询、用户人群计算、用户群多维透视分析这类对响应时间要求较高的场景,也可以考虑选用 Elasticsearch 进行存储。 + - Elasticsearch 是面向文档型数据库,一条数据在这里就是一个文档,用 json 作为文档格式。 + - 在关系型数据库中查询数据时可通过选中数据库、表、行、列来定位所查找的内容,在 Elasticsearch 中通过索引(index)、类型(type)、文档(document)、字段来定位查找内容。 + - 一个 Elasticsearch 集群可以包括多个索引(数据库),也就是说其中包含了很多类型(表),这些类型中包含了很多的文档(行),然后每个文档中又包含了很多的字段(列)。 + +#### Elasticsearch 存储 HBase 的索引,支持多条件复杂查询。 + +- 适用场景: + - 采用 Elasticsearch 存储 HBase 的索引信息,以支持复杂的多条件查询功能。 + +- 工作原理: + - 基于 HBase 的存储方案并没有解决数据的高效检索问题。 + - 在实际应用中,经常有根据特定的几个字段进行组合后检索的应用场景,而 HBase 采用 rowkey 作为一级索引,不支持多条件查询; + - 如果要对库里的非 rowkey 进行数据检索和查询,往往需要通过 MapReduce 等分布式框架进行计算,时间延迟上会比较高,难以同时满足用户对于复杂条件查询和高效率响应这两方面的需求。 + - 为了既能支持对数据的高效查询,同时也能支持通过条件筛选进行复杂查询,需要在 HBase 上构建二级索引,以满足对应的需要。 + +- 采用 Elasticsearch 存储 HBase 的索引信息,就可以支持复杂高效的查询功能。主要查询过程包括: + - 在 Elasticsearch 中存放用于检索条件的数据,并将 rowkey 也存储进去; + - 使用 Elasticsearch 的 API 根据组合标签的条件查询出 rowkey 的集合; + - 使用上一步得到的 rowkey 去 HBase 数据库查询对应的结果。 + +- HBase 数据存储数据的索引放在 Elasticsearch 中,实现了数据和索引的分离。 + - 在 Elasticsearch 中 documentid 是文档的唯一标识,在 HBase 中 rowkey 是记录的唯一标识。 + - 在工程实践中,两者可同时选用用户在平台上的唯一标识(如 userid 或 deviceid)作为 rowkey 或documentid,进而解决 HBase 和 Elasticsearch 索引关联的问题。 + +# Spark Streaming 流式标签存储 + +- 适用场景 + - 流式标签存储; + +- 工作原理 + - Spark Streaming 是 Spark Core API 的扩展,支持实时数据流的处理,并且有可扩展、高吞吐量、容错的特点。 + - 数据可以从 Kafka、Flume 等多个来源获取,可以使用 map、reduce、window 等多个高级函数对业务逻辑进行处理。最后,处理后的数据被推送到文件系统、数据库等。 + + - Spark Streaming 接收实时数据流,并将数据分成多个 batch 批次,然后由 Spark 引擎进行处理,批量生成结果流。 + - Spark Streaming 提供了一个高层抽象,称为 Discretized Stream 或 Dstream,它表示连续的数据流。 + - Dstream 可以通过 Kafka、Flume 等来源的数据流创建,也可以通过在其他 Dstream 上应用高级操作来创建。 + +- Kafka + - Kafka 的核心功能是作为分布式消息中间件。 + - Kafka 集群由多个 Broker server 组成,其中: + - 消息的发送者称为 Producer; + - 消息的消费者称为 Consumer; + - Broker 是消息处理的节点,多个 Broker 组成 Kafka 集群; + - Topic 是数据主题,用来区分不同的业务系统,消费者通过订阅不同的 Topic 来消费不同主题的数据; + - 每个 Topic 又被分为多个 Partition,Partition 是 topic 的分组,每个 Partition 都是一个有序队列; + - offset 用于定位消费者在每个 Partition 中消费的位置。 + - Kafka 对外使用 Topic 概念,生产者向 Topic 里写入消息,消费者从 Topic 中读取消息。 + - 一个 Topic 由多个 Partition 组成。 + - 生产者向 Brokers 指定的 Topic 中写消息,消费者从 Brokers 里面拉取指定的 Topic 消息,然后进行业务处理。 + +- Spark Streaming 可以通过 Receiver 和 Direct 两种模式来集成 Kafka。 + - 在 Receiver 模式下,Spark Streaming 作为 Consumer 拉取 Kafka 中的数据,将获取的数据存储在 Executor 内存中。 + - 但可能会因为数据量大而造成内存溢出,所以启用预写日志机制(Write AheadLog)将溢出部分写入到 HDFS 上。 + - 在接收数据中,当一个 Receiver 不能及时接收所有的数据时,再开启其他 Receiver 接收。它们必须属于同一个 Consumer Group,这样可以提高 Streaming 程序的吞吐量(如图4-21所示)。 + - 整体来说,Receiver模式效率较低,容易丢失数据,在生产环境中使用较少。 + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\237\345\221\275\345\221\250\346\234\237\346\240\207\347\255\276\350\256\241\347\256\227\346\226\271\346\263\225.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\237\345\221\275\345\221\250\346\234\237\346\240\207\347\255\276\350\256\241\347\256\227\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..1020708f93b --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\237\345\221\275\345\221\250\346\234\237\346\240\207\347\255\276\350\256\241\347\256\227\346\226\271\346\263\225.md" @@ -0,0 +1,133 @@ +--- +layout: post +title: 用户画像:用户生命周期标签计算方法 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +# 用户生命周期 + +![]({{site.baseurl}}/img-post/用户画像-18.png) + +- 引入期 + - 用户刚刚开始使用产品或者服务,初步建立起品牌的认知 + +- 成长期 + - 用户对产品服务开始逐渐信任,使用频次、深度不断加强 + +- 成熟期 + - 用户对平台的服务非常熟悉,可以无障碍地完成各种内容,使用的频次深度趋于稳定 + +- 休眠期 + - 用户逐步丧失对平台的兴趣,使用频率、热度越来越低 + +- 流失期 + - 用户完全不再使用该产品 + +# 生命周期标签计算方法 + +#### 通过逻辑规则 + +- 用户生命周期五个阶段,分别对应四个边界: + - 形成边界 + - 活跃边界 + - 沉睡边界 + - 流失边界 + +- 在计算用户生命周期时,核心的四个数据分别是: + - 用户首单时间 + - 有效订单量及发生时间 + - 最近一单时间 + - 购物频率 + +- 计算逻辑: + + ![]({{site.baseurl}}/img-post/用户画像-19.png) + + - 首先,找出全部历史有订单的用户,判断历史第一单是否在 6 个月以内: + - 如果第一单是在 6 个月以内: + - 接下来,判断用户是否只下了1单, + - 如果是的话,根据这单产生的时间,把用户划分为三个不同的形成阶段; + - 如果用户下了两单及以上,根据用户最后一个订单的产生时间,把用户划分为三个不同的适应阶段; + - 如果用户第一单在 6 个月以: + - 那么判断用户的最后一单的产生时间: + - 如果在 1 年以前,那么用户处于流失阶段; + - 如果在 6 个月到 1 年之间,那么用户处于准流失阶段; + - 如果在 3 个月到 6 个月之间,那么用户处于沉睡阶段。 + - 其次,根据用户近 3 个月与近 6 个月的购物频率对比: + - 如果频率增加,那么用户处于成长阶段; + - 频率不变,用户处于稳定阶段; + - 频率减少,用户处于衰退阶段。 + - 最后,根据最后一单的产生时间判断: + - 如果在 1 个月内,那么用户的对应阶段分别为成长1、稳定1、衰退1; + - 如果在1-3个月内,那么用户对应的阶段分别为成长2、稳定2、衰退2。 + + +#### 通过算法生成 + +- 朴素贝叶斯 +- SVN + +- 这里先给自己挖个坑,后续再补充。 + +# 不同阶段运营策略 + +- 运营策略的核心目的有两个: + +- 促活:提高用户参与度 + - 用户多使用 APP,才会更了解APP,我们也才会更了解用户。 + - 通过这种闭环,我们可以更了解用户需求,不断改进产品; + - 而用户,可以不断体会到产品的价值,加深相互之间的交流,提高产品与用户的契合度。 + +- 停留:延长用户使用时间 + - 在如今人口红利基本到顶的时代,C 端产品的每一个用户,都花费了一定的成本。 + - 企业花钱买到了用户,肯定是希望买到的用户,能够产生更多更高的收益。 + - 那么,提高用户使用时长,在这个价格敏感(客单价提升空间小)的时代,必然可以提升用户收益。 + +#### 新客户期 + +- 在这个阶段,更多的需要引导用户注册、转化,并了解相关的产品内容。 +- 我们可以尽量多的让用户体验核心功能,提供一些预设的优质内容,设定相应的新手教程,新手奖励,新手红包等。 +- 目的就是转化访客,提升用户对 APP 的了解程度。 + +#### 成长期 + +- 在这个阶段,需要不断培养用户的使用习惯,让用户对产品产生依赖,或者场景化的印象。 +- 比如: + - 运动型 APP 可以在晚上固定的时间提醒用户运动; + - 打车 APP 在下班到来前提醒你道路拥堵情况; + - 购物型 APP 不断的发送浏览过的物品的促销推送。 +- 这些策略,都是要用户不断的使用 APP,提升用户留存和活跃度,推动用户到达我们设定的成熟期指标。 + +#### 成熟期 + +- 在这个阶段,需要提升用户对核心功能的复用率、分享率,最大限度的挖掘用户的价值,并引导用户多次输出价值。 +- 当然,对于进入成熟期的判断标志,我们还需要结合相应的业务场景,寻找相应的 magic number 或者 aha moment。 +- 这个阶段: + - 根据用户的历史喜好,不断的用一些奖励措施,增加用户的好感度; + - 并尽可能的引导用户分享,通过老带新的结合策略,增加新用户的到来,并提升通过分享来的新用户的成长速度。 + +#### 衰退期 + +- 进入衰退期,用户的活跃度会有较明显的下降。 +- 在这个时期运营的目标,是通过拉活的手段、唤醒用户,并使其返回成熟期。 +- 运营策略可以使用各种优惠鼓励,或者给予增值服务,不断的刺激用户活跃,从而提升用户的留存率和活跃频次。 +- 在这个时期,还需要对用户进行预警,如果用户来到了衰退期,我们需要及时的知道,才能迅速的采取策略。 + +#### 流失期 + +- 流失用户,就一个运营目标:用户召回。 +- 我们可以选择不同的召回手段,如: + - 短信 + - push + - 电话 + - 邮件 + - 等。 +- 最重要的,需要对流失原因做归因分析,寻找到用户的流失原因。 + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\345\212\237\350\203\275\350\256\276\350\256\241.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\345\212\237\350\203\275\350\256\276\350\256\241.md" new file mode 100644 index 00000000000..bf0e456dcdd --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\345\212\237\350\203\275\350\256\276\350\256\241.md" @@ -0,0 +1,126 @@ +--- +layout: post +title: 用户画像:用户画像功能设计 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + +# 系统架构 + +#### 系统架构图 + +![]({{site.baseurl}}/img-post/用户画像-6.png) + +#### ETL 数据流向图 + +![]({{site.baseurl}}/img-post/用户画像-5.png) + + +# 画像看板 + +#### 单用户画像 +- 基于用户 ID 查询用户标签 +- 用户信息 +- 词云 +- 营销提醒 +- 用户转化率分析 +- 用户漏斗分析 +- 用户路径分析 +- 用户兴趣偏好 + +#### 群体用户画像 + +- 群体用户信息 +- 群体用户词云 +- 群体用户营销提醒 +- 群体用户转化率分析 +- 群体用户漏斗分析 +- 群体用户路径分析 +- 群体用户兴趣偏好 + +# 人群功能 + +#### 人群圈选 + +- 创建、编辑、删除人群 +- 基于不同规则筛选目标人群; + + ![]({{site.baseurl}}/img-post/用户画像-9.png) + +- 对人群进行修改、删除操作; + +#### 多标签圈选 + +- 组合查询多个标签,获取人群; + - 例如:组合“近30日购买次数”大于3次和“高活跃”“女性”用户这三个标签进行定义目标人群,查看该类人群覆盖的用户量,以及该部分人群的各维度特征。 + ![]({{site.baseurl}}/img-post/用户画像-8.png) + - 在自定义编辑用户分群时,对于有统计值类型的标签,可以自定义筛选该标签的取值范围,如上图中“近30日购买次数”标签,业务人员可筛选该标签的数值。 + +#### 人群洞察 + +- 作用: + - 在选定人群后,对人群进行各个维度的透视分析; +- 例如: + - 选定年龄为“30-35岁”、居住在“一线城市”且有“汽车”资产的“男性”; + - 对该人群进行透视分析后发现,这部分用户的“价格敏感度”较低、对于“自驾游”的兴趣比较高; + +#### 相似性拓展 + +- 作用: + - 基于种子人群进行相似人群的匹配。 + +- 种子人群圈选 + - 用户自己通过标签系统圈选人群; + +- 扩展倍数设置 + - 即要将种子人群扩大的倍数; + - 通常作为配置项让用户进行选择。 + +- 扩展人群洞察 + - 将种子人群按照扩展倍数扩充后,获得扩展的相似人群; + - 该人群的特征规律应该和种子人群的特征规律比较一致或者相近。 + +- 扩展人群管理 + - 扩展人群上架; + - 扩展人群下架; + - 扩展人群修改; + +# 标签管理 + +#### 标签管理 + +- 标签上架 +- 标签下架 +- 标签修改 +- 标签规则 + +#### 标签视图 + +- 层级化地展示了目前已经上线使用的全部用户标签。 + - 用户可以层级化地通过点击标签,查看每个标签的详细介绍。 + +#### 标签集市 + +- 从标签维度分析用户群体; + +# 系统管理 + +#### 系统服务 + +- API +- JDBC(数据库接口) + +#### 系统设置 + +- 权限管理 +- 账号管理 + + + + + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\347\232\204\345\272\224\347\224\250\345\234\272\346\231\257.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\347\232\204\345\272\224\347\224\250\345\234\272\346\231\257.md" new file mode 100644 index 00000000000..1515f3e6075 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\347\232\204\345\272\224\347\224\250\345\234\272\346\231\257.md" @@ -0,0 +1,83 @@ +--- +layout: post +title: 用户画像:用户画像的应用场景 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + + + +#### 用户行为分析 + +- 行为分析模型,是基于分析场景对数据的抽象。 +- 例如:常见的留存分析、事件分析、分布分析、漏斗分析等等,本质上是基于用户行为日志+用户属性数据,来对用户从多个分析维度进行分析。 +- 直白说就是将分析师常写的分析SQL固化成产品和数据模型来实现,省去人工介入的时间。 + +- 示例:用户路径分析 + - 可以分析不同类型用户的访问路径、停留时长、浏览内容等,基于此可以不断优化页面布局,优化用户访问路径引导策略,提升用户停留时长、或者方便用户快速找到目标信息。 + +#### 流量渠道分析 + +- 渠道留存分析 + - 例如:对渠道用户质量的评价一般通过留存率来进行,主要指标包括次日留存率、7日留存率、月留存率等。这几个指标的计算口径如下: + - 次日留存率=第一天新增的用户中第二天还登录的用户数/第一天新增用户总数; + - 七日留存率=第一天新增的用户中在往后的7天还有登录的用户数/第一天新增总用户数; + - 月留存率=第一天新增的用户中在往后的30天还有登录的用户数/第一天新增总用户数。 + +- 渠道活动码(裂变营销) + - 例如:用企业微信沉淀顾客,可以在每个渠道都附上带活码的渠道专属二维码,比如:在自己的公众号、朋友圈广告、企业官网以及其他物料上都附上二维码,将前来咨询新来客户都引进到企业微信,这些客户会被随机分配给某个员工,这样既能保证员工工作的公平,又能避免单个号当日加人达到上限的情况。 + - 用户在扫码过程中会被自动打上标签,标识渠道来源。 + +#### 漏斗分析 + +- 漏斗图是一种外形类似漏斗的可视化图表,使用该方法可以直观地追踪产品的整体流程、追踪业务的转化路径、追踪不同生命周期阶段下的用户群体表现。 +- 通过一系列转化率的分析,可以迅速定位问题,方便运营人员及时调整运营策略 + +#### 人群洞察 + +- 用户人群特征分析可以通过组合标签来自定义人群,然后对自定义人群从各个维度进行透视分析或建立对照组人群做人群对比分析。 +- 根据分析经验,在做人群分析时一定要去做对比,单纯看单个人群的分布没有太多信息含量,不对比看不出差异。 +- 借助画像产品形态,可以分析圈定的用户群在各个维度上的特征情况。 + +#### 精准推送 + +- APP Push +- 短信推送 +- 邮件推送 + +#### 精准营销 + +- 当画像系统做成产品形态后,业务人员可以根据业务规则组合标签圈定相应人群,将该批人群推送到对应的业务系统中进行运营。 +- 营销卖货 + - 短信/邮件/电话/微信等各种渠道的营销来卖货。 +- 营销促活 + - 同样是各种渠道的营销来把用户拉/引到平台上,有人活跃的地方就有流量就有生意。 + +#### 风控预警 + +- 基于不同行业不同应用场景来设计预警管控的模型,实现对潜在风险的预警与预防。 +- 比如: + - 识别潜在的风险账号,把账号直接拉黑名单; + - 直接识别出风险用户,直接做人工的干预管控,基本是基于实时数据对前方风险去做预防。 + +#### 营销效果评估 + +- 精准营销是数据价值的一个重要出口,但如何评估效果好坏,不同业务线的人员有不同的关注重点。总体来看,可分为流量提升导向和GMV提升导向两种情况。 + +- 有的业务线人员背的KPI指标是流量,因此关注的重点是流量提升,如负责Push业务线的人员。这种情况下,对效果的分析会对比使用圈定人群进行精准推送方式带来的点击率,与没有使用用户画像进行无差别普通推送带来的点击率相比是否有所提升、提升了多少个百分点。 + +- 有的业务线人员背的KPI指标是GMV,因此关注的重点是ROI的转化,如短信营销、外呼营销的业务线人员。这种情况下,对效果的分析会关注营销活动中营销了多少用户、实际触达了多少用户、有多少用户实际付费以及带来的GMV,对比实际营销成本(短信、外呼电话的成本)分析营销的ROI。 + +#### 个性化推荐系统 + +- 在用户画像的开发过程中不仅会开发用户标签维度的数据,同时也会开发用户行为特征库、商品特征库、商家特征库等相关数据。为算法开发人员做用户相关商品、内容的个性化推荐提供底层数据支持。 +- 另外,基于画像标签系统可以为用户的个性化服务提供支持。例如,针对高质量用户提供VIP专人客服,可以让该部分头部用户享受到高质量服务,有效提升用户体验。 +- 对于业务人员从经营分析的多个维度分析了解用户特征,可进一步通过消息推送、短信、邮件等多渠道触达、运营用户,有效帮助流量增长和GMV转化,提升用户体验。 +- 同时画像标签数据、用户行为特征库的构建为个性化推荐相关人员进行数据挖掘提供了底层支持。 + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\347\263\273\347\273\237\347\256\200\344\273\213.md" new file mode 100644 index 00000000000..4907032f7b4 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -0,0 +1,210 @@ +--- +layout: post +title: 用户画像:用户画像系统简介 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + +# 1. 什么是用户画像 + +- 用户画像,也可称标签模型,是对用户信息的标签化过程; + - 收集用户的多维度数据,对用户或者产品特征属性进行刻画,并对这些特征进行分析、统计,挖掘数据潜在价值,从而抽象出用户的信息全貌。 + +- 标签化过程: + - **信息 > 文本 > 数据 > 标签** + +# 2. 用户标签包括那些内容 + +#### 2.1. 按照维度划分 + - 自然属性 + - 社会属性 + - 行为习惯 + - 购买能力 + - 消费习惯 + - 偏好特征 / 兴趣爱好 + +#### 2.2. 按照层级划分 + + ![]({{site.baseurl}}/img-post/用户画像-2.png) + +#### 2.3. 按照生成方式划分 + + - 统计类指标 + - 又叫:事实标签 + - 可以直接提取 + - 包括一些基本信息,比如:年龄、性别、城市、职业等 + - 也可以从用户数据、行为数据、消费数据中统计得出 + - 这些标签来自基于行为数据的统计信息,比如:1日登录次数、7日登录次数、7日下单次数等 + - 这类标签,构成了用户画像的基础 + + - 规则类标签 + - 又叫:建模标签 + - 在统计指标基础上通过规则生成,是对统计类指标的有效补充 + - 需要基于用户行为自定义规则 + - 比如: + - 活跃用户,定义规则为每天登录一次以上的用户 + - 高净值用户,消费总额在某个阈值以上 + - 在开发过程中,运营人员对业务更熟悉,数据维护人员对数据结构、分布、特征更熟悉,所以多数时候由运营人员和数据人员共同搭建维护; + + - 预测类标签 + - 又叫:机器学习标签 + - 是非确定性标签(前两项都属于确定性标签) + - 需要基于已有的信息“预测”用户特征,需要挖掘才能获得 + - 实际开发过程中,这类项目开发周期长、成本高,属于探索性项目 + - 有些公司使用外包人员、手动打标签,效果可能会更好、而且投入也很低 + +# 3. 用户画像的价值是什么 + +#### 3.1. 业务决策 + - 定位用户人群 + - 聚焦目标用户 + - 统计指标展示 + - 排名统计 + - 地域分析 + - 特征分析 + - 行业趋势 + - 竞品分析 + - 等等 + +#### 3.2. 精准营销 / 圈人服务 + - 定向推送 + - 邮件、短信、APP + - 定点推送 + - 站外广告 + - 站内广告 + +#### 3.3. 个性化服务 + - 个性化推荐 + ![]({{site.baseurl}}/img-post/用户画像-1.png) + - 个性化搜索 + +#### 3.4. 用户研究 + - 挖掘用户特征 + - 多维度用户分析 + +# 4. 用户画像系统的数据流向 + +![]({{site.baseurl}}/img-post/用户画像-3.png) + +# 5. 用户画像项目团队及协作 + +- 运营经理 / 业务产品经理 +- 数据产品经理 +- 数仓工程师 + - 数仓模型搭建 +- 算法工程师 + - 算法模型建设 +- 后端工程师 + - 系统功能模块建设 +- 测试人员 + - 数据测试 + - 功能测试 + +# 6. 用户画像项目技术栈 + +#### 6.1. 数据开发 + - Spark + - Flink + +#### 6.2. 数据存储 & 查询 + - Hbase + - Hive + - MySQL + +#### 6.3. 流式计算 + - Kafka + - Spark Streaming + +#### 6.4. 作业调度 + - Crontab + - Airflow + +#### 6.5. 开发语言 + - Scala + - 标签的复杂计算 + - Spark Streaming + - Hive SQL + - Python + - 监控脚本 + - 调度流 + - Shell + + +# 7. 用户画像项目开发流程 + +#### 7.1. 需求调研 + - 业务需求分析 + - 数据盘点 + - 竞品调研 + - **输出:《产品用户画像需求文档》**,明确应用场景、标签内容、应用方式等; + +#### 7.2. 产品设计 + - 业务架构 + - 产品架构 + - 标签体系设计 + - 画像系统功能 + - 产品版本计划 + - **输出:《产品用户画像产品规划文档》**,明确应用方式、标签模型、功能模块、UI/Demo等; + +#### 7.3. 技术规划 + - 目标解读 + - 任务分解 + - 技术方案选型 + - 标签数据落表 + - 逻辑模型 + - 数据字典 + - 血缘关系 + - 标签权重 + - 数据采集体系规划 + - One ID(主要在多平台场景)设计 + - 项目计划 & 开发流程 + - **输出:《产品用户画像技术规划文档》**,明确技术选型、方案设计、开发计划、数据库表等; + +#### 7.4. 技术开发 + +- 用户建模 + - 确定要提取的用户特征维度 +- 数据采集 + - 读取需要的数据,统计存放到数据仓库 +- 数据清洗 + - 通常直接在 hive 中进行,包括统计类标签的生成 +- 模型训练 + - 对于无法直接得到的标签,建立模型进行训练 +- 属性预测 + - 利用训练得到的模型,和用户的已知特征,预测未知特征,同时不断优化模型 +- 标签落表 + - 特征选取 & 标签数据落表 +- 数据合并 + - 把多数据源提取的特征进行合并 +- 数据分发 / 接口封装 + - 对于合并后的结果数据,分发到各个平台,为业务提供支撑 + +#### 7.5. 灰度测试 + +- 线下模型数据测试 +- 线上灰度测试 + +#### 7.3. 运营反馈 + +- 线上模型发布 +- A/B 测试 +- 效果追踪 +- 模型修正 + +#### 7.6. 项目评审四个关键点 + +- 立项评审 +- 需求评审 +- 提测演示 +- 产品发布验收 + + + + + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\347\263\273\347\273\237\350\256\276\350\256\241.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\347\263\273\347\273\237\350\256\276\350\256\241.md" new file mode 100644 index 00000000000..efa10f359dc --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\250\346\210\267\347\224\273\345\203\217\347\263\273\347\273\237\350\256\276\350\256\241.md" @@ -0,0 +1,182 @@ +--- +layout: post +title: 用户画像:用户画像系统设计 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + +# 1. 系统架构 + +#### 1.1. 系统架构图 + +![]({{site.baseurl}}/img-post/用户画像-6.png) + +#### 1.2. ETL 数据流向图 + +![]({{site.baseurl}}/img-post/用户画像-5.png) + + +# 2. 用户画像系统分层 + +#### 2.1. 应用层 + + - 广告投放系统 + - 营销系统 + - CRM 系统 + - 推荐系统 + - 用户分析平台 + - 产品开发分析 + + +#### 2.2. 服务层 + + - 业务服务 + - 画像看板 + - 单用户画像 + - 群体用户画像 + - 相似性拓展 + - 标签集市 + - 人群洞察 + - 标签管理 + - 标签上架 + - 标签下架 + - 标签规则 + - 权限管理 + - 账号管理 + - 系统服务 + - API + - 数据库接口 + +#### 2.3. 数据挖掘层 + + - 用户唯一标识(One ID) + - 跨平台映射关联(ID-Mapping) + - 用户档案 + - 用户数据集市 + - 按主题,在数仓搭建用户相关表汇聚层 + - 包含内容: + - 用户基础数据 + - 用户行为数据 + - 用户交易数据 + - 用户客诉数据 + - 标签建模 + - 事实类标签 + - 规则类标签 + - 预测类标签 + - 标签宽表 + - 宽表建模 & 存储 + - 包括内容: + - 日全量宽表: + - 用户基础信息宽表 + - 用户行为信息宽表 + - 用户消费偏好宽表 + - 用户价值维度宽表(消费能力、客诉敏感度、价格敏感度) + - 用户风控维度宽表(反作弊) + - 日增量宽表: + - 用户行为 + - 用户偏好 + - 群体偏好 + +#### 2.4. ETL 层 + + - 数据清洗转换 + - ETL 过程 + + ![]({{site.baseurl}}/img-post/用户画像-4.png) + +#### 2.5. 数据采集层 + + - 业务数据 + - 埋点数据 + - 日志数据 + - 第三方数据 + - 爬虫数据 + + +# 3. 用户标签建模 + +#### 3.1. 基本信息 + - 自然属性 + - 社会属性 + - 平台属性 + +#### 3.2. 行为特征 + - 订单统计 + - 商品偏好 + +#### 3.3. 兴趣特征 + - 兴趣爱好 + - 浏览内容 + - 收藏内容 + - 阅读咨询 + - 购买物品偏好等 + +#### 3.4. 消费特征 + - 优惠使用信息 + +#### 3.5. 社交特征 + - 用户社交相关数据 + +#### 3.6. 服务特征 + - 售后&反馈 + + +# 4. 用户标签落表 + +#### 4.1. Hive + +- 在 hive 中做主题建模 +- 存储用户画像标签 + +#### 4.2. Hbase / Redis +- 个性化推荐 +- 线上实时数据 + +#### 4.3. MySQL +- 用户画像标签-元数据管理 +- BI 报表展示 + +#### 4.4. Elasticsearch +- 圈人服务 & 用户列表 + - ES 中快速获取标签对应的用户列表,进而定向推送营销信息 +- ES 方便复杂查询 + +# 5. 系统模块 + +#### 5.1. 标签模块 + +- 基础标签 + - 标签名称 + - 业务含义 + - 标签规则 +- 组合标签 + +#### 5.2. 标签引擎 + +- 数据源管理 +- 规则标签模型 +- 统计标签模型 +- 挖掘标签模型 +- 引擎状态监控 + +#### 5.3. 画像模块 + +- 个体画像 +- 群体画像 + +#### 5.4. 标签查询 + +- 覆盖用户查询 + - 基于组合标签,查询用户人群 +- 标签模型查询 + +#### 5.5. 系统设置 + +- 用户管理 +- 权限管理 + diff --git "a/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\273\345\203\217\347\263\273\347\273\237\345\274\200\345\217\221\347\232\204\345\205\250\346\265\201\347\250\213.md" "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\273\345\203\217\347\263\273\347\273\237\345\274\200\345\217\221\347\232\204\345\205\250\346\265\201\347\250\213.md" new file mode 100644 index 00000000000..be84abc72e8 --- /dev/null +++ "b/_posts/2022-01-01-\347\224\250\346\210\267\347\224\273\345\203\217\357\274\232\347\224\273\345\203\217\347\263\273\347\273\237\345\274\200\345\217\221\347\232\204\345\205\250\346\265\201\347\250\213.md" @@ -0,0 +1,293 @@ +--- +layout: post +title: 用户画像:画像系统开发的全流程 +subtitle: +date: 2022-01-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 用户画像 +--- + + + +# 1. 用户画像系统开发流程图 + +![]({{site.baseurl}}/img-post/用户画像-7.png) + +# 2. 系统框架设计 + +- 用户画像系统包括三层结构,分别是: + - 基础数据收集 + - 行为建模 + - 构建画像 +- 其中: + - 基础数据收集的对象包含了业务数据和用户行为数据; + - 行为建模,指的是在基础数据上对用户数据进行多维度的挖掘分析; + - 构建画像,则是要给用户打上标签,并进行落表存储或提服务; + +![]({{site.baseurl}}/img-post/用户画像-17.png) + +- 在系统设计阶段 + - 需要明确用户画像系统包含哪些模块,数据仓库架构是什么样子,开发流程是什么样,表结构如何设计,ETL如何设计等。 + - 只有明确了上面的方向,后续才能做好项目的排期和人员投入预算,这对于评估每个开发阶段重要指标和关键产出非常重要。 + +# 3. 数据指标体系 + +- 根据业务线梳理,包括用户属性、用户行为、用户消费、风险控制等维度的指标体系。 + +- 关于本节,详细内容参见:《 用户画像:常见的用户标签体系 》。 + +# 4. 标签数据存储 + +- 标签相关数据可存储在 Hive、MySQL、HBase、Elasticsearch 等数据库中,不同存储方式适用于不同的应用场景。 + +#### 4.1. Hive + +- 在 hive 中做主题建模 +- 存储用户画像标签 + +#### 4.2. Hbase / Redis + +- 个性化推荐 +- 线上实时数据 + +#### 4.3. MySQL + +- 用户画像标签-元数据管理 +- BI 报表展示 + +#### 4.4. Elasticsearch + +- 圈人服务 & 用户列表 + - ES 中快速获取标签对应的用户列表,进而定向推送营销信息 +- ES 方便复杂查询 + +# 5. 标签数据开发 + +- 标签开发 + - 标签开发,是用户画像工程化的重点模块,包含统计类、规则类、挖掘类、流式计算类标签的开发,以及人群计算功能的开发,打通画像数据和各业务系统之间的通路,提供接口服务等开发内容。 +- 离线标签 + - 画像系统的标签,分为离线标签和实时标签。 + - 离线标签又分为基于统计类型的标签和基于算法性标签,大部分或者 90% 的标签都是统计类型标签; + - 机器学习的算法标签很少: + - 一方面是因为开发周期很长, + - 另一方面效果也有限。 + - 但是基于某些场景,还是得需要使用机器学习,只是机器学习标签的比重会很小。 + - 有时我们做实时数据支持,会通过实时数据流给用户做刻画或者做特征标记,可做线上服务接口调用查询。 +- 用户维表 + - 用户维表是一张大宽表,大宽表基于筛选用户的属性或分析师分析用户的时候用。除了标签跑批、用户维表,还有人群计算、行为数据等的数据开发。 + +#### 5.1. 标签开发 + +- 每个标签,一个应用程序,需要一个标签,就开发一 Spark 套程序,如果不再使用标签,则直接停止该程序; + +- 标签表示例: + ``` + CREATE TABLE `tbl_basic_tag` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '标签ID', + `name` varchar(50) DEFAULT NULL COMMENT '标签名称', + `industry` varchar(30) DEFAULT NULL COMMENT '行业、子行业、业务类型、标签、属性', + `rule` varchar(300) DEFAULT NULL COMMENT '标签规则', + `business` varchar(100) DEFAULT NULL COMMENT '业务描述', + `level` int(11) DEFAULT NULL COMMENT '标签等级', + `pid` bigint(20) DEFAULT NULL COMMENT '父标签ID', + `ctime` datetime DEFAULT NULL COMMENT '创建时间', + `utime` datetime DEFAULT NULL COMMENT '修改时间', + `state` int(11) DEFAULT NULL COMMENT '状态:1申请中、2开发中、3开发完成、4已上线、5已下线、6已禁用', + `remark` varchar(100) DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`) + ) ENGINE=InnoDB AUTO_INCREMENT=233 DEFAULT CHARSET=utf8 COMMENT='基础标签表'; + ``` + +- 模型表示例: + - 详细记录标签模型 application 的位置; + ``` + CREATE TABLE `tbl_model` ( + `id` bigint(20) DEFAULT NULL, + `tag_id` bigint(20) DEFAULT NULL COMMENT '标签ID', + `type` int(11) DEFAULT NULL COMMENT '算法类型:统计-Statistics、规则匹配-Match、挖掘-具体算法-DecisionTree', + `model_name` varchar(200) DEFAULT NULL COMMENT '模型名称', + `model_main` varchar(200) DEFAULT NULL COMMENT '模型运行主类名称', + `model_path` varchar(200) DEFAULT NULL COMMENT '模型JAR包HDFS路径', + `sche_time` varchar(200) DEFAULT NULL COMMENT '模型调度时间', + `ctime` datetime DEFAULT NULL COMMENT '创建模型时间戳', + `utime` datetime DEFAULT NULL COMMENT '更新模型时间戳', + `state` int(11) DEFAULT NULL COMMENT '模型状态,1:运行;0:停止', + `remark` varchar(100) DEFAULT NULL, + `operator` varchar(100) DEFAULT NULL, + `operation` varchar(100) DEFAULT NULL, + `args` varchar(100) DEFAULT NULL COMMENT '模型运行应用配置参数,如资源配置参数' + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + ``` + +- 标签表上的每条记录,与模型表中的记录,是一一对应的。 + +- 标签开发流程图: + + ![]({{site.baseurl}}/img-post/用户画像-10.png) + +- 组合标签开发 + + ![]({{site.baseurl}}/img-post/用户画像-15.png) + +#### 5.2. 相似性拓展 + +- 通过匹配标签的方式获取 + - 识别种子人群的特征标签,基于识别的典型特征,进行人群的扩展。 + - 例如: + - 种子人群有【高消费】、【偏好奢侈品】、【小镇中产】等标签; + - 那么可以将包含这些特征的人群都先圈出来(交集或者并集),这样就完成了相似人群的扩展。 + - 通过标签的方式,比较容易理解。 + +- 通过相似度的方式直接计算 + - 将人群的特征进行向量化,然后计算向量之间的距离。 + - 例如: + - 某人群总共有 100 个标签、400 个特征(所谓特征可以理解成标签取值),那么每个用户都可以用 400 维的向量进行标识。 + - 基于每个用户的向量,计算种子人群的向量均值,然后用其余待匹配人群的特征向量,与种子人群的向量均值求距离。 + - 最后,按照距离进行排序,获得距离最近的top取值即可。 + - 用距离的方法,比较容易计算。 + +- 通过机器学习的方式训练 + - 算法的具体实现,其实就是一个典型的分类问题,即判断一个人属不属于种子人群,而种子人群作为机器学习训练集。 + +# 6. 系统性能调优 + +- 标签加工、人群计算等脚本上线调度后,为了缩短调度时间、保障数据的稳定性等,需要对开发的脚本进行迭代重构、调优。 + +#### 6.1. 数据倾斜调优 + +- 解决数据倾斜的几个思路: + - 业务上:避免热点key的设计或者打散热点key,例如可以把北京和上海分成地区,然后单独汇总。 + - 技术上:在热点出现时,需要调整方案避免直接进行聚合,可以借助框架本身的能力,例如进行mapside-join。 + - 参数上:无论是Hadoop、Spark还是Flink都提供了大量的参数可以调整。 + +- 调优方法: + - 当少量key重复次数特别多,如果这种key不是业务需要的key,可以直接过滤掉。 + - 事前对连接 key 进行预处理 + - map join + - 特殊值分开处理 + - 随机数分配法 + - 解决小表过大无法 map join 问题 + - 处理连接 key 为 NULL 空值 + - 处理连接 key 为不同数据类型 + - 提高 Reduce 任务的并行度 + +#### 6.2. 合并小文件 + +- Spark 执行 `insert overwrite table` 语句时,由于多线程并行向 HDFS 写入且 RDD 默认分区为 200 个,因此默认情况下会产生200个小文件。 +- Spark 中可以使用 reparation 或 coalesce 对 RDD 的分区重新进行划分,reparation 是 coalesce 接口中 shuffle 为true的实现。 +- 在 Spark 内部会对每一个分区分配一个 task 执行,如果 task 过多,那么每个 task 处理的数据量很小,这就会造成线程频繁在 task 之间切换,导致集群工作效率低下。 +- 为解决这个问题,常采用 RDD 重分区函数来减少分区数量,将小分区合并为大分区,从而提高集群工作效率。 + +#### 6.3. 缓存中间数据 + +- Spark 的一个重要的能力就是将数据持久化缓存,这样在多个操作期间都可以访问这些持久化的数据。 +- 当持久化一个 RDD 时,每个节点的其他分区都可以使用 RDD 在内存中进行计算,在该数据上的其他 action 操作将直接使用内存中的数据,这样会使其操作计算速度加快。对 RDD 的复杂操作如果没有持久化,那么一切的操作都会从源头开始,一步步往后计算,不会复用原始数据。 +- 在画像标签每天 ETL 的时候,对于一些中间计算结果可以不落磁盘,只需把数据缓存在内存中。而使用 Hive 进行 ETL 时需要将一些中间计算结果落在临时表中,使用完临时表后再将其删除。 +- RDD 可以使用 persist 或 cache 方法进行持久化,使用 StorageLevel 对象给 persist 方法设置存储级别时,常用的存储级别如下所示: + - MEMORY_ONLY:只存储在内存中 + - MEMORY_ONLY_2:只存储在内存中,每个分区在集群中两个节点上建立副本; + - DISK_ONLY:只存储在磁盘中; + - MEMORY_AND_DISK:先存储在内存中,内存不够的话存储在磁盘中 +- 其中 cache 方法等同于调用 persist()的 MEMORY_ONLY方法 +- 在画像标签开发中,一般从 Hive 中读取数据,然后将需要做中间处理的 DataFrame 注册成缓存表。 +- 也可以将读取的用户数据缓存在内存中并注册为一张视图,后续直接从视图中读取对应用户数据。在该 Spark 任务执行完成后,释放内存,不需要清除该缓存数据。 + +#### 6.4. 开发中间表 + +- 在用户画像迭代开发的过程中,初期开发完标签后,通过对标签加工作业的血缘图整理,可以找到使用相同数据源的标签,对这部分标签,可以通过加工中间表缩减每日画像调度作业时间。 +- 做中间层设计前需要明确几个重要的点: + - 这个中间层对应的业务场景、业务目标是什么? + - 业务方有了这份中间层数据以后可以进行哪些维度的分析,ETL时有了这份中间层数据可以减少对哪些数据的重复开发计算? + - 这个业务场景分析中包含哪些分析维度和指标? + - 同时面向很多业务场景的中间层不一定是好的中间层。 + + +- ETL 调度时间过长是一个较难解决的“瓶颈”,每天的调度在跑完计算标签、标签校验预警、计算人群、人群校验预警、同步到服务层等环节后往往需要几个小时,最后提供到服务层数据时也比较晚了。 + + +# 7. 作业流程调度 + +- 标签加工、人群计算、同步数据到业务系统、数据监控预警等脚本开发完成后,需要调度工具把整套流程调度起来。 +- ETL 调度模型,包括标签的加工计算、数据标签的校验、人群计算、跑一些分析宽表。 +- 跑完数据模型后, + - 推荐到线上服务层,比如:Redis、Clickhouse、ES; + - 服务调用,包括站内的服务调用以及TOC的客户端服务调用。 + +![]({{site.baseurl}}/img-post/用户画像-5.png) + +# 8. 用户画像产品化 + +- 为了能让用户数据更好地服务于业务方,需要以产品化的形态应用在业务上。产品化的模块主要包括标签视图、用户标签查询、用户分群、透视分析等。 + +#### 8.1. 单用户画像 + +- 基于用户 ID 查询用户标签 +- 用户信息 +- 词云 +- 营销提醒 +- 用户转化率分析 +- 用户漏斗分析 +- 用户路径分析 +- 用户兴趣偏好 + +#### 8.2. 群体用户画像 + +- 群体用户信息 +- 群体用户词云 +- 群体用户营销提醒 +- 群体用户转化率分析 +- 群体用户漏斗分析 +- 群体用户路径分析 +- 群体用户兴趣偏好 + +#### 8.3. 人群圈选 + +- 创建、编辑、删除人群 +- 基于不同规则筛选目标人群; + + ![]({{site.baseurl}}/img-post/用户画像-9.png) + +#### 8.4. 多标签圈选 + +- 组合查询多个标签,获取人群; + - 例如:组合“近30日购买次数”大于3次和“高活跃”“女性”用户这三个标签进行定义目标人群,查看该类人群覆盖的用户量,以及该部分人群的各维度特征。 + ![]({{site.baseurl}}/img-post/用户画像-8.png) + - 在自定义编辑用户分群时,对于有统计值类型的标签,可以自定义筛选该标签的取值范围,如上图中“近30日购买次数”标签,业务人员可筛选该标签的数值。 + +#### 8.5. 人群洞察 + +- 作用: + - 在选定人群后,对人群进行各个维度的透视分析; +- 例如: + - 选定年龄为“30-35岁”、居住在“一线城市”且有“汽车”资产的“男性”; + - 对该人群进行透视分析后发现,这部分用户的“价格敏感度”较低、对于“自驾游”的兴趣比较高; + +#### 8.6. 相似性拓展 + +- 作用: + - 基于种子人群进行相似人群的匹配。 + +- 种子人群圈选 + - 用户自己通过标签系统圈选人群; + +- 扩展倍数设置 + - 即要将种子人群扩大的倍数; + - 通常作为配置项让用户进行选择。 + +- 扩展人群洞察 + - 将种子人群按照扩展倍数扩充后,获得扩展的相似人群; + - 该人群的特征规律应该和种子人群的特征规律比较一致或者相近。 + +- 扩展人群管理 + - 扩展人群上架; + - 扩展人群下架; + - 扩展人群修改; + +# 9. 用户画像应用 + +- 画像的应用场景包括用户特征分析、短信、邮件、站内信、Push消息的精准推送、客服针对用户的不同话术、针对高价值用户的极速退货退款等VIP服务应用。 + +- 关于本节,详细内容参见:《 用户画像:常见的用户标签体系 》 \ No newline at end of file diff --git "a/_posts/2022-01-02-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\346\214\207\346\240\207\344\275\223\347\263\273\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-02-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\346\214\207\346\240\207\344\275\223\347\263\273\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..65753882ed6 --- /dev/null +++ "b/_posts/2022-01-02-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\346\214\207\346\240\207\344\275\223\347\263\273\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,181 @@ +--- +layout: post +title: 指标体系:指标体系概念详解 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 指标体系 +--- + + +# 1. 指标分类 + +#### 1.1. 原子指标 + +- 原子指标和度量的含义相同,基于某一业务事件行为下的度量,是业务定义中不可再拆分的指标; +- 原子指标具有明确的业务含义,如支付金额。 + +#### 1.2. 派生指标 + +- 派生指标 = 一个原子指标 + 多个修饰词(可选) + 时间周期; +- 派生指标,可以理解为对原子指标统计范围的圈定。 +- 如: + - 原子指标:支付金额; + - 派生指标:最近一天海外买家的支付金额; +- 派生指标分为两类: + - 事务型指标: + - 对业务过程进行的衡量,如:新发商品数量,重发商品数量; + - 存量型指标: + - 对实体对象的某些状态的统计,如:商品总数,注册会员总数; + +#### 1.3. 衍生指标 + +- 是在派生指标基础上,计算得出的复合指标; +- 主要有: + - 比率型: + - 如:客退率、点击率; + - 比值型: + - 如:CMP、CPA; + - 统计型: + - 如:日活跃用户数; + +# 2. 指标分级 + +![]({{site.baseurl}}/img-post/指标体系-1.jpg) + +- 一级指标往往是业务流程最终的结果,例如积分抵扣金额,是业务流程(会员-›购买旅游产品-›使用积分抵扣-›支付金额) 最后的一个结果。 +- 光看一个最后结果是无法监督、改进业务流程的,这就需要更细致一些的指标,也就是拆解为二、三、四级指标。例如,在业务流程中不同会员等级可以抵扣的金额不一样。不同旅游产品线可以抵扣的金额比例也不一样。所以,需要把二级指标按照业务流程拆解为更细的三级指标。 + +- 示例: + - GMV=新用户业绩+老用户业绩 + - GMV=新用户业绩+(老用户复购+老用户转介绍)业绩 + - 老用户业绩=老用户数量*复购率*客单价+老用户转介绍带来的新增流量*进店率*付费率*客单价 + - 新用户业绩=新用户流量*留电率*进店率*付费率*客单价 + - 新用户业绩=(免费流量*免费访问量留电率+付费流量*付费流量留电率)*进店率*付费率*客单价 + - 新用户业绩=(微信公众号流量*微信公众号留电率+官网流量*官网留电率+微博流量*微博留电率+美团点评流量*美团点评留电率+异业合作客户)*进店率*付费率*客单价 + +# 3. 指标口径 + +- 指标含义: + - 这个指标在业务上表示什么。 +- 指标定义: + - 这个指标是怎么定义的。 +- 数据来源: + - 从什么地方收集的原始数据; + - 数据统计的时间范围是什么。 + +# 4. 指标查询 + +#### 4.1. 固化查询 + +- 对于一些固化下来的取数、看树的需求,通过数据产品的形式提供给用户,童儿提高分析和运营的效率; +- 固化查询,SQL 有固定的模式。 +- 固化查询,是预先开发好的; + +#### 4.2. 即席查询 + +- 用户根据自己的需求,灵活的选择查询条件,系统根据用户的选择生成返回响应结果; +- 例如:用户自定义的统计报表; +- 即席查询,明确需求范围,但不是固定模式的 SQL 查询; +- 即席查询,是用户自定义查询条件的。 + + +# 5. 北极星指标 + +- 北极星指标也叫唯一关键指标(OMTM,One me-tric that matters),如果此指标变好说明公司此时的产品正在健康向上发展。 +- 北极星指标是产品成功的关键指标。产品通过解决特定用户在特定场景下的特定需求而创造营收,北极星指标正是描述了解决需求与企业营收之间的重要联系。 + +- 示例: + - Airbnb:北极星指标是交易总额,分拆为:用户总数 & 订房次数; + - 知乎:北极星指标是问答总数,分拆为:用户总数 & 问答贡献率; + +- 北极星指标都有以下三点重要作用: + - 指引未来:它能够清楚表明产品在未来阶段需要优化的内容,以及可被传达的功能点。 + - 团队协同:它能够让公司内其他产品组的同事知道该产品组的实时进展,以便在需要时得到跨部门的资源协助。 + - 结果导向:最重要的是,它使产品和运营人员对结果负责。 + +![]({{site.baseurl}}/img-post/指标体系-1.jpg) + +- 制定北极星指标: + + - 体现产品的核心价值,即可以知道用户是否体验到了产品要提供、传达的核心价值 + - 反映用户的活跃程度,指标越高是否说明用户活跃程度更高 + - 反映产品的实时发展,指标提高是否说明产品在往好的方向发展(警惕虚荣指标) + - 未来营收的先导指标,即根据这一指标可以一定程度预测未来的营收情况 + - 易于理解、明确衡量,即定义清晰,并且容易被所有人理解、交流、执行 + +# 6. OSM 模型 + +- Object + - 业务目标,产品的功能目的或需要满足的需求; +- Strategy + - 业务策略,为了实现目标所采取的方法; +- Measure + - 业务度量,衡量策略对目标的实现程度。 + +- 简单来说,就是以目标为导向,基于采取的策略形成具体的衡量指标。 + +![]({{site.baseurl}}/img-post/指标体系-2.jpg) + +# 7. UJM 模型 + +- UJM,即User-journey-map,用户旅程地图模型。 +- 简单来说,就是通过拆分用户使用产品的阶段性行为,从中挖掘用户的需求,从而在每个阶段确定能够提升的指标。 +- 还是以用户在外卖平台选外卖为例: + - 如图可知,用户在外卖下单流程中,一般包括:搜索-浏览有兴趣的商品-付费-复购;在每个环节中,我们可以基于用户在产品的接触点,挖掘用户在每个环节产生的痛点,从而识别出产品提升的方向。 + +![]({{site.baseurl}}/img-post/指标体系-3.jpg) + +# 8. OSM模型 结合 UJM模型 + +- OSM能够促使我们去思考产品的重要目标,UJM能够然我们去站在用户角度思考产品UJM所挖掘的痛点和机会点,也可以反哺OSM中的目标和策略。 + +- 所以将两个模型结合,就可以帮助我们以更加全面的角度去思考,进一步帮助我们更加系统的构建指标体系。 + +![]({{site.baseurl}}/img-post/指标体系-4.jpg) + +# 9. 不同类型业务关注的指标 + +#### 9.1. 工具类业务 + +- 帮助用户节省时间,产品自身提供价值。如墨迹天气、TEA。核心指标应该聚焦到判断工具的使用率。 + +- 比如:为用户提供工具类型业务策略,一般是为了让用户节省时间,快速的定位到所需要的信息或者完成某一种任务; +- 这种策略核心的价值就在于提升效率,一般的衡量指标是: + - 使用量 + - 目标达成率 + - 频次 +- 以电商的例子来讲的话,就是第一步中的流量推荐位,以及搜索功能是不是能够让用户快速的定位到它所感兴趣的直播内容,那这种情况下做优化流量位的内容和搜索匹配优化,衡量标准就是它的效率、曝光、点击、转化效率。 + +#### 9.2. 内容类业务 + +- 比如:为了用户提供消遣的内容,让用户可以消磨时间,那么这种策略的核心价值就在于为了用户提供丰富的高质量的内容,不管是短视频,直播,或者活动玩法。 +- 这种策略的核心的价值就在于为用户提供内容的量与质,一般衡量指标是: + - 消费人数 + - 消费广度 + - 消费市场 + - 用户与内容的互动 +- 以上指标用来衡量用户对于内容的喜爱。 + - 比如:B站的弹幕就是一种用户对于内容认可的更高层次的情感表达。 + +#### 9.2. 电商类业务 + +- 帮助用户节省时间,产品通过链接其他资源提供价值。 + - 如:淘宝、京东金融,核心指标应该聚焦到转化率。 + +- 这种策略的核心价值就在于为用户提供好的购物体验,能够提升付费页面转化效率,提升购买的总规模,客单价以及复购率。 + +#### 9.3. 社交类业务 + +- 杀掉用户时间,产品通过链接其他资源提供价值。 + - 如:Soul、探探,核心指标应该聚焦到用户的活跃程度。 + +- 比如:为了用户提供与其他人的情感连接,促进用户和用户之间的关系沉淀,进而让用户对于平台或者对于业务更有依赖性,促进用户的活跃和互动,一般的衡量指标是: + - 内容的发布量 + - 用户和用户之间的互动量 + - 沉淀下来的关系对数 + +- 微信,是用户和用户之间的一个情感的连接,那衡量这种连接的紧密性主要是人与人之间的互动量,点评赞数量,沉淀的关系的数量。 diff --git "a/_posts/2022-01-02-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\346\214\207\346\240\207\345\255\227\345\205\270\350\256\276\350\256\241\350\247\204\350\214\203.md" "b/_posts/2022-01-02-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\346\214\207\346\240\207\345\255\227\345\205\270\350\256\276\350\256\241\350\247\204\350\214\203.md" new file mode 100644 index 00000000000..4fd63f47976 --- /dev/null +++ "b/_posts/2022-01-02-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\346\214\207\346\240\207\345\255\227\345\205\270\350\256\276\350\256\241\350\247\204\350\214\203.md" @@ -0,0 +1,199 @@ +--- +layout: post +title: 指标体系:指标字典设计规范 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 指标体系 +--- + + +# 1.1. 指标字典概念 + +#### 1.1. 指标字典解决的问题 + +- 业务视角 + - 业务分析场景指标、维度不明确; + - 频繁的需求变更和反复迭代,数据报表臃肿,数据参差不齐; + - 用户分析具体业务问题找数据、核对确认数据成本较高。 + +- 技术视角 + - 指标定义,指标命名混乱,指标不唯一,指标维护口径不一致; + - 指标生产,重复建设,数据汇算成本较高; + - 指标消费,数据出口不统一,重复输出,输出口径不一致; + +- 产品视角 + - 缺乏系统产品化支持从生产到消费数据流没有系统产品层面打通 + + +#### 1.2. 制定指标字典的目标 + +- 规范维度和量度命名,命名规则明确、通用、易懂。 +- 对确认的维度和度量,统一计算口径,避免歧义。 +- 涵盖尽可能多的核心维度和度量,以此推动数据建设。 +- 基于指标字典接入指标提取工具,实现自助分析与查询。 + +#### 1.3. 指标字典的价值 + +- 通过指标字典,可以对指标进行共享和统一修改和维护; +- 如果有指标管理系统,再配合上血缘关系,就更方便追踪数据流转了。 + +#### 1.4. 指标字典基本概念 + +- 维度: + - 从哪些角度去衡量事物。 +- 汇总方式: + - 用哪种方法衡量,比如:平均、汇总等。 +- 量度: + - 指度量单位,只有加了度量单位才有意义。 + +#### 1.5. 指标命名四要素 + +- 量化词: + - 对一事物的测量,比如金额、次数。 +- 业务过程: + - 用来描述过程性指标的。 +- 统计对象: + - 要统计的对象名称,比如订单、用户。 +- 限定词: + - 用来对指标进行限定约束,比如当天、累计。 + +- 建议指标命名: + - 限定词+【业务过程】+统计对象+量化词 + - 示例:当月累计+支付成功+订单+总数 + +#### 1.6. 指标字典设计要求 + +- 命名要规范 + - 规范维度和量度命名,命名规则尽量做到明确、易懂 + +- 定义清晰 + - 对维度和量度统一计算口径,避免歧义,明确指标取数逻辑 + +- 覆盖范围广 + - 涵盖尽可能多的核心维度和量度,确保指标字典里的覆盖的维度可区分,指标可统计。 + +- 可拓展性强 + +# 2. 指标字典需要明确的内容 + +#### 2.1. 数据域 + +指面向业务分析,将业务过程或者维度进行抽象的集合。其中,业务过程可以概括为一个个不拆分的行为事件,在业务过程之下,可以定义指标;维度,是度量的环境,如乘客呼单事件,呼单类型是维度。为了保障整个体系的生命力,数据域是需要抽象提炼,并且长期维护更新的,变动需执行变更流程。 + +#### 2.2. 业务过程 + +指公司的业务活动事件,如,呼单、支付都是业务过程。其中,业务过程不可拆分。 + +#### 2.3. 时间周期 + +用来明确统计的时间范围或者时间点,如最近30天、自然周、截止当日等。 + +#### 2.4. 修饰类型 + +是对修饰词的一种抽象划分。修饰类型从属于某个业务域,如日志域的访问终端类型涵盖APP端、PC端等修饰词。 + +#### 2.5. 修饰词 + +指的是统计维度以外指标的业务场景限定抽象,修饰词属于一种修饰类型,如在日志域的访问终端类型下,有修饰词APP、PC端等。 + +#### 2.6. 度量/原子指标 + +原子指标和度量含义相同,基于某一业务事件行为下的度量,是业务定义中不可再拆分的指标,具有明确业务含义的名称,如支付金额。 + +#### 2.7. 维度 + +维度是度量的环境,用来反映业务的一类属性,这类属性的集合构成一个维度,也可以称为实体对象。维度属于一个数据域,如地理维度(其中包括国家、地区、省市等)、时间维度(其中包括年、季、月、周、日等级别内容)。 + +#### 2.8. 维度属性 + +维度属性隶属于一个维度,如地理维度里面的国家名称、国家ID、省份名称等都属于维度属性。 + +#### 2.9. 指标分类 + +- 主要分为原子指标、派生指标、衍生指标 + +- 原子指标 + - 原子指标和度量的含义相同,基于某一业务事件行为下的度量,是业务定义中不可再拆分的指标; + - 原子指标具有明确的业务含义,如支付金额。 + +- 派生指标 + - 派生指标 = 一个原子指标 + 多个修饰词(可选) + 时间周期; + - 派生指标,可以理解为对原子指标统计范围的圈定。 + - 如: + - 原子指标:支付金额; + - 派生指标:最近一天海外买家的支付金额; + - 派生指标分为两类: + - 事务型指标: + - 对业务过程进行的衡量,如:新发商品数量,重发商品数量; + - 存量型指标: + - 对实体对象的某些状态的统计,如:商品总数,注册会员总数; + +- 衍生指标 + - 是在派生指标基础上,计算得出的复合指标; + - 主要有: + - 比率型: + - 如:客退率、点击率; + - 比值型: + - 如:CMP、CPA; + - 统计型: + - 如:日活跃用户数; + +# 3. 指标内容 + +#### 3.1. 指标编码 + +- 为了方便查找和管理,需要对指标定义一套编码。 + +#### 3.2. 指标口径 + +- 指标最重要的就是,明确指标的统计口径,就是这个指标是怎么算出来的,口径统一了,才不会产生歧义。 + +- 口径梳理 + - 一开始指标的梳理是很麻烦的,因为要统一一个口径,需要和不同的部门去沟通协调; + - 还有可能会有各种各样的指标出现,需要去判断是否真的需要这个指标,是否可以用其他指标来替代;指标与指标之间的关系也需要理清楚。 + +#### 3.3. 计算公式 + +- 对业务口径的翻译,需要业务方告知你从哪里的数据去计算 + +#### 3.4. 指标版本 + +- 第一版指标梳理好之后,需要进行推广和维护,不断地迭代,持续推动,让公司所有部门都统一站在一个视角关注问题。 + +# 4. 指标定义 + +- 基础指标 = 主题 + 业务过程 + 量化对象 + 度量 + 汇总方式 + - 如:交易额 = 交易 + 支付完成 + 订单 + 金额 + 求和 + +- 衍生指标 = 基础指标 + 维度修饰词 + 统计周期 + - 如:美团小程序交易额 = 交易额 + 流量维度修饰词 + 周期 + +- 计算指标 = 明确计算公式 + + +# 5. 命名规则 + +#### 5.1. 词根 + +- 简写 +- 省略 +- 顺序 + +- 规避问题: + - 同义不同名 + - 同名不同义 + +#### 5.2. 词库 + +- 中英文对照词库 +- 高频词汇固定命名 +- + + + + + diff --git "a/_posts/2022-01-02-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\347\224\265\345\225\206\350\277\220\350\220\245\345\237\272\347\241\200\346\214\207\346\240\207\344\275\223\347\263\273\346\220\255\345\273\272.md" "b/_posts/2022-01-02-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\347\224\265\345\225\206\350\277\220\350\220\245\345\237\272\347\241\200\346\214\207\346\240\207\344\275\223\347\263\273\346\220\255\345\273\272.md" new file mode 100644 index 00000000000..cb4f40f4b16 --- /dev/null +++ "b/_posts/2022-01-02-\346\214\207\346\240\207\344\275\223\347\263\273\357\274\232\347\224\265\345\225\206\350\277\220\350\220\245\345\237\272\347\241\200\346\214\207\346\240\207\344\275\223\347\263\273\346\220\255\345\273\272.md" @@ -0,0 +1,445 @@ +--- +layout: post +title: 指标体系:电商运营数据基础指标体系搭建 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 指标体系 +--- + + +# 1. 运营分析概述 + +#### 1.1. 核心主题 + +- 用户 +- 流量 +- 会员 +- 商品 +- 活动 +- 销售 +- 渠道 +- 媒介 +- 区域 +- 供应链 + +#### 1.2. 分析目标 + +- 引流 +- 提单量 +- 提销售额 +- 提客单价 +- 提毛利率 +- 提转化率 +- 拉新客户 +- 减库存 +- 提库存周转 +- 降次品率 +- 将客诉率 +- 降广告费 +- 提广告转化率 +- 提广告ROI +- 提活动效果 +- 提活动 ROI + +#### 1.3. 分析漏斗 + +![]({{site.baseurl}}/img-post/电商数据指标-1.png) + +#### 1.4. 商业模式 + +- 平台电商 +- 电商卖家 +- 零售商 +- 品牌/工厂卖家 + +#### 1.5. 数据来源 + +- 电商平台数据 +- 独立站后台数据 +- 线下门店数据 +- 第三方数据 +- 爬虫数据 +- ERP 系统数据 + +# 2. 总体运营指标 + +#### 2.1. 成交额度 + +- GMV + - 注意退货 +- 销售额 +- 税收收入 +- 退款额 + +#### 2.2. 成交量 + +- 成功支付订单量 +- 退款订单量 + +#### 2.3. 利润 + +- 毛利 +- 净利 + +#### 2.4. 用户数 + +- 注册用户数 +- 登录用户数 +- 活跃用户数 +- 复购用户数 + +#### 2.5. 用户行为 + +- 浏览时长 +- 加购次数 +- 页面停留时长 + +#### 2.6. 访问量 + +- PV +- UV +- 访问页面数 +- 访问页面深度 + +# 3. 网站流量指标 + +- 日活 +- 月活 +- 新增 +- 沉默 +- 回流 +- 流失 +- 留存 +- 三周活跃 +- 三天活跃 +- 七天活跃 +- PV +- UV +- AUV +- IP +- 访问/在线时长 +- 跳出率 + +# 4. 销售转化指标 + +#### 4.1. 漏斗分析 + +- 漏斗分析核心指标 + - 注册 + - 登录 + - 收藏 + - 加购 + - 下单 + - 复购 + +- 登录 + - 着陆页停留时长 +- 浏览 + - 详情页停留时长 + - 搜索页至详情页转化率 + - 详情页至收藏页转化率 + - 详情页至加购页转化率 + - 详情页至下单页转化率 +- 加购 + - 加购量 + - 加购率 +- 收藏 + - 收藏量 + - 收藏率 +- 下单 + - 下单页停留时长 + - 下单页至支付页转化率 +- 支付 + - 支付页停留时长 + - 销售金额 + - 活动销售金额 + - 日常销售金额 + - 广告销售金额 + - 销售量 + - 活动销售量 + - 日常销售量 + - 广告销售量 + +- 订单数 + - 7天订单数 + - 30天订单数 +- 下单人数 + +- 流量转化标签 + - 高流量高转化 + - 高流量低转化 + - 低流量高转化 + - 低流量低转化 + +- 单日数据: + - 单日下单笔数 + - 单日下单金额 + - 单日下单用户数 + - 单日支付笔数 + - 单日支付金额 + - 单日支付人数 + - 单日支付商品数 + - 单日支付平均时长 +- 7 日数据: + - 7日登陆天数 + - 7日支付次数 + - 7日支付金额 + - 7日下单次数 + - 7日下单金额 + - 7日被加入购物车次数 + - 7日被加入购物车件数 +- 30 日数据: + - 30 日登陆天数 + - 30日支付次数 + - 30日支付金额 + - 30日下单次数 + - 30日下单金额 + - 30日被加入购物车次数 + - 30日被加入购物车件数 +- 累计数据: + - 累积登录天 + - 累计下单次数 + - 累计下单金额 + - 累计支付次数 + - 累计支付金额 + - 累计退款次数 + - 累计退款件数 + - 累计退款金额 + - 累计被加入购物车件数 + +#### 4.2. 销售预估 + +- 历史销售情况 +- 近期增长情况 +- 同行竞争情况 + +# 5. 商品品类指标 + +#### 5.1. 商品指标 + +- spu +- sku +- 曝光量 +- 上架量 + +#### 5.2. 销售指标 + +- 商品销量 +- 商品收藏量 +- 商品加购量 +- 商品退货量 + +#### 5.3. 排名指标 + +- 商品销量排名 +- 商品收藏排名 +- 商品加购排名 +- 商品退款排名 + +#### 5.4. 复购指标 + +- 时间区间 + - 第一次购买时间 + - 最后一次购买时间 +- 购买次数 +- 复购周期 +- 单次复购率 +- 多次复购率 +- 购买人数 +- 多次购买人数 + +#### 5.5. 差评客诉 + +- 退货率 +- 换货率 +- 次品率 + +#### 5.6. 新客采购 + +- 新购买客户数 +- 新客户占比 +- 新客订单数 +- 新客订单占比 +- 新客采购金额 +- 新客采购金额占比 +- 新客利润 +- 新客利润率 +- 新客广告 ROI + +#### 5.7. 商品生命周期 + +![]({{site.baseurl}}/img-post/电商数据指标-4.png) + +- 新品开发 + - 友商新品 + - 供应商新品 + - 用户需求关键词 +- 库存清理 + - 库存周转时间 + - 库存周转率 + +# 6. 客户价值指标 + + +#### 6.1. 消费维度用户分层 + +![]({{site.baseurl}}/img-post/电商数据指标-2.png) + + +#### 6.2. 价格维度用户分层 + +![]({{site.baseurl}}/img-post/电商数据指标-3.png) + +- 会员本身的价格敏感度 +- 不同品类的消费者的价格敏感度 +- 获客成本 +- 用户收益 +- 流失率 + +# 7. 活动运营指标 + +#### 7.1. 渠道 + +- 渠道获客数量 +- 渠道投放成本 +- 渠道获客成本 +- 活动销售额 +- 活动销售量 +- 活动 ROI + +#### 7.2. 对照组(A/B测试) + +- 参照组 + +# 8. 风险控制指标 + +#### 8.1. 差评客诉 + +- 评价率 +- 商品好评率 + - 有图评价率 +- 商品差评率 + - 差评撤销率 +- 差评关键词 + - 词云 + +#### 8.2. 配送 + +- 配送延迟 +- 运输破损 +- 发错货 +- 多发货 +- 少发货 + +# 9. 市场竞争指标 + +#### 第三方指标 + +- 市场占有率 +- 市占增长率 +- 用户份额 +- 交易排名 +- 流量排名 + + +# 10. 媒介渠道指标 + + +#### 10.1. 投放效果 + +- 用户来源 +- 媒介内容 +- 广告形式 +- 广告素材 +- 广告浏览 +- 广告评论 +- 广告分享 +- CPA +- CPC +- CPM +- 不同渠道用户留存 +- 不同渠道用户转化 +- 不同渠道用户活跃 +- 不同渠道用户下单 +- 不同渠道客单价 +- 不同渠道客诉率 +- 不同渠道流失率 + + +#### 10.2. 广告反作弊 + +- 防止广告商故意消耗广告费; + - 第三方反作弊服务商; + +- 作弊方式: + - 一个用户,多个渠道消耗广告,最终下载/支付; + - 机器人模拟用户; + - 同一用户重复多次操作; + +# 11. 采购供应链指标 + +- 到货率 +- 到货准确率 +- 次品率 +- 伸偿率 +- 新品率 +- 滞销率 + +# 12. 库存指标 + +#### 12.1. 库存盘点 + +- 库存可销天数 +- 库存量 + - 7天库存 + - 30天库存 + - 60天库存 + - 90天库存 + - 120天库存 + - 可退货库存 + - 不可退货库存 + - 期末库存 +- 最近 n 天平均销量 +- 安全库存天数 +- 订货天数 + + +#### 12.2. 缺货 + +- 送货天数 +- 缺货量 +- 缺货率 + +#### 12.3. 周转情况 + +- 库存结构 +- 库龄 +- 周转天数 +- 周转率 + +- 不同行业,库存周转情况差别很大 + - 快消、服装,卖的比较快 + - 家电、汽车,卖的比较慢 + +#### 12.4. 库存指标示例 + +![]({{site.baseurl}}/img-post/电商数据指标-5.jpg) + +- A 类商品 + - SKU 只占据 10% + - 占据主要销售额,达到 70% +- B 类商品 + - SKU 占据 10% ~ 30% + - 销售额只贡献了 20% +- C 类商品 + - SKU 占据 65% + - 销售额只有 10% +- 零销售 + - 没有销售 +- 季节商品 + - 销售周期 + diff --git "a/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\344\270\215\345\220\214\347\261\273\345\236\213\350\241\250\345\217\212\345\220\214\346\255\245\347\255\226\347\225\245.md" "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\344\270\215\345\220\214\347\261\273\345\236\213\350\241\250\345\217\212\345\220\214\346\255\245\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..cf9b8690f19 --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\344\270\215\345\220\214\347\261\273\345\236\213\350\241\250\345\217\212\345\220\214\346\255\245\347\255\226\347\225\245.md" @@ -0,0 +1,106 @@ +--- +layout: post +title: 数仓建模:不同类型表及同步策略 +subtitle: 日全量表 & 增量表 & 快照表 & 切片表 & 流水表 & 拉链表 +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓建模 +--- + +# 日全量表 + +#### 日全量表概念 + +- 全量表存放的,是每天的所有的最新状态的数据; +- 全量表无论有无变化都要上报; + - 每次上报的数据都是所有的数据(变化的 + 没有变化的); +- 全量表只有一个分区或者没有分区; +- 每次往全量表里面写数据都会覆盖之前的数据,因此全量表不能记录数据的历史变化,只能截止到当前最新、全量的数据。 +- 全量表一般用作维度表,比如:用户,商品,商家,销售员等 + +#### 全量表表同步策略 + +- 有变化数据 + - 由于数据量比较小,通常可以做每日全量,就是每天存一份完整数据,即每日全量。 +- 无变化数据 + - 针对没变化的客观世界的维度(比如性别,地区,民族,政治成分,鞋子尺码),可以只存一份固定值。 + +# 增量表 + +#### 增量表概念 + +- 如果一张表只含有某个更新周期内的数据,那就叫增量表。 +- 每天新增的数据和改变的数据,都会存储在当日的分区中; +- 增量表记录每次增加或变化的数据,只报变化量、无变化的不用报; +- 一般增量表只保存 7 天的数量; +- 增量表设计过程,假设以18号与19号数据为例; + +#### 增量表同步策略 + +- 增量更新指的是,从上次的更新截止点开始,抽取这次新增加的数据,放在目标表里,这就是增量更新。 +- 增量更新的时候,我们需要注意增量字段,还得小心更新失败、漏更新等,需要进行数据更新的校核。优点是数据处理量小,速度快;缺点是需要注意的事情比较多。 + +# 快照表 + +#### 快照表概念 + +- 每天的数据,都是截止到当天的全量数据; +- 因为全量表无法反映历史的变化,而快照表记录截止数据日期的全量数据(每个分区都是记录截止当前分区日期的全量数据); +- 但是在数据量大的情况下,每个分区存储的都是全量数据,数据冗余和浪费存储空间; + + +# 全量表 vs 增量表 vs 快照表 + +![]({{site.baseurl}}/img-post/数仓建模-4.png) + + +# 拉链表 + +#### 拉链表概念 + +- 拉链表不是 SCD,它只是用来处理缓慢变化维的一种手段而已。 +- 将分析的角度存放在维度表中,但维度表里的数据可能发生一些变化,尽管可能跨越很久,例如用户信息表; +- 能够解决快照表数据冗余问题,还能维护数据历史状态和最新状态,记录截止数据日期的全量数据,一个事物从开始,一直到当前状态的所有变化信息; +- 拉链表含义 + - 记录一个事物从开始,一直到当前状态的所有变化的信息; + - 拉链表每次上报的都是历史记录的最终状态,是记录在当前时刻的历史总量; + - 当前记录存的是当前时间之前的所有历史记录的最后变化量(总量); + - 存量是在某一时刻的总量; + - 存量一般设计成拉链表(月报-常用、日报); + - 流量和存量的区别:流量是增量;存量是总量; + - 封链时间可以是 9999 等等比较大的年份; + - 拉链表到期数据要报0; + - 拉链表和增量表的共同点:表结构基本一样; + - 在有些情况下,为了保持历史的一些状态、需要用拉链表来做,这样做目的在可以保留所有状态的情况下可以节省空间。 + +- 拉链表适用于以下几种情况吧 + - 数据量大; + - 表中某些字段有变化,但变化的频率也不是很高; + - 业务需求需要统计这种变化状态; + - 每天全量一份数据不太现实,不仅浪费了存储空间,有时可能业务统计也会麻烦; + +#### 拉链表概念同步策略 + +- 拉链表,可以采用增量更新的方式进行同步; + +# 流水表 + +#### 流水表概念 + +- 流水表,对于表中的每一个修改都会记录,可以用于反映实际记录的变更; +- 比如:交易流水,操作日志,出库入库记录等。 + +#### 流水表同步策略 + +- 同步更新数据 + - 因为数据不会变化,而且数据量巨大,所以每天只同步更新数据即可; +- 每日一个分区 + - 可以做成每日增量表,即每日创建一个分区存储。 + +- 区别于拉链表: + - 拉链表通常是对账户信息的历史变动进行处理保留的结果,流水表是每天的交易形成的历史; + - 流水表用于统计业务相关情况,拉链表用于统计账户及客户的情况 + diff --git "a/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\213\211\351\223\276\350\241\250\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\213\211\351\223\276\350\241\250\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..e64789a9025 --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\213\211\351\223\276\350\241\250\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,150 @@ +--- +layout: post +title: 数仓建模:拉链表的概念详解 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓建模 +--- + + +# 1. 什么是拉链表 + +#### 1.1. 拉链表定义 + +- 所谓拉链,顾名思义,就是记录历史,记录一个事物从开始,一直到当前状态的所有变化的信息。 +- 拉链表,是针对数据仓库设计中表存储数据的方式而定义的。拉链表的核心思想,像个拉链,支持开链,支持闭链,支持退链。我们通常将最新的数据称为开链数据,历史数据称为闭链数据。 +- 拉链表在一个时间维度中,同一个用户只保存一条用户状态。 +- 拉链表通常会增加三个技术字段:开始日期 `start_time`、结束日期 `endtime`、状态标识 `mark`。 +- 通过主键(PK)与历史数据进行对比,判断当前数据与历史数据是否发生变化,如果发生变化或者新增则进行相应的开链、闭链操作。 +- 拉链表支持历史数据查询,且空间占用较小,但是数据加工处理较为繁琐,属于时间换空间的设计方式。 + +#### 1.2. 拉链表示例 + +- 下面是一张拉链表,存储的是用户的最基本信息以及每条记录的生命周期,通过这张表可以获取到最新的当天数据以及之前的历史数据。 + +- 拉链表示例: + + ![]({{site.baseurl}}/img-post/拉链表-1.png) + + - `t_start_date`,表示该条记录的生命周期开始时间; + - `t_end_date`,表示该条记录的生命周期结束时间; + - `t_end_date='9999-12-31'`,表示该条记录目前处于有效状态; + - 如果查询当前所有有效的记录,则使用 `select * from user where t_end_date = '9999-12-31'` 即可以获取; + - 如果查询 `2017-01-01` 当天的历史快照,则 `select * from user where t_start_date <= '2017-01-01' and end_date >= '2017-01-01'`。 + +# 2. 缓慢变化维 + +#### 2.1. 缓慢变化维(SCD,Slowly Changing Dimensions) + +- 缓慢变化维的提出是因为在现实世界中,维度的属性并不是静态的,它会随着时间的流逝发生缓慢的变化。 +- 这种随时间发生变化的维度我们一般称之为缓慢变化维,并且把处理维度表的历史变化信息的问题称为处理缓慢变化维的问题,有时也简称为处理SCD的问题。 + +#### 2.2. 缓慢变化维处理的方式 + +- 重写覆盖 + - 与业务系统保持一致,直接更新为最新的状态数据即可;适用于:数据必须正确,比如客户的身份证;不需要考虑历史变化维度,没有意义没有价值的维度;优点是直接更新即可,缺点无法恢复,不能查看历史变化; +- 增加新行 + - 更新历史数据时间戳,新增新行记录新值;适用于仅需保持历史数据的业务场景,相应的事实表的的关联需要更新为最新的 id; +- 增加新列 + - 如果某个维度发生多次变化,会产生列爆炸 +- 拉链表 + - 拉链表的具体使用 + +# 3. 为什么需要拉链表 + +#### 3.1. 海量数据更新问题 + +- 在数据仓库的数据模型设计过程中,经常会遇到下面这种表的设计: + - 数据量比较大; + - 表中的部分字段会被 update,如: + - 用户联系方式; + - 客户收件地址; + - 产品的描述信息; + - 订单的状态; + - 等等。 + - 需要查看某一个时间点或者时间段的历史快照信息,比如: + - 查看某一个订单在历史某一个时间点的状态; + - 查看某一个用户在过去某一段时间内更新过几次; + - 等等。 + - 变化的比例和频率不是很大,比如: + - 总共有 `1000万` 的会员信息记录,每天新增和发生变化的记录有 `10万` 左右; + +- 对于上面的需求,如果对表每天都保留一份全量数据,那么每次全量中会保存很多不变的信息,这对存储是极大的浪费; +- 针对上面的需求,该如何设计表,有三种方案可以选择: + - 方案一:每天只留一份最新的数据。 + - 方案二:保留一份全量的切片数据。 + - 方案三:使用拉链表。 + +#### 3.2. 方案一:每天只留一份最新的数据 + +- 这种方案实现起来很简单,每天 `drop` 掉前一天的数据,重新抽取一份最新的数据。 +- 优点: + - 很明显,节省空间,一些普通的使用也很方便,不用在选择表的时候加一个时间分区什么的。 +- 缺点: + - 也很明显,没有历史数据,想翻翻旧账只能通过其他方式,比如从流水表里面抽。 + +#### 3.3. 方案二:每天保留一份全量的切片数据 + +- 每天一份全量的切片是一种比较稳妥的方案,而且历史数据也在。 +- 缺点: + - 存储空间占用的量太大了,如果对这个表每天都保留一份全量,那么每次全量的数据中会有很多不变的数据,对存储是极大的浪费。 + - 当然我们也可以做一些取舍,比如只保留最近一个月的数据。但是,数据的生命周期就会被严重制约,历史数据大量丢失不符合大数据的建设理念。 + + +#### 3.4. 方案三:每天同步一次更新数据 + +- 每天只更新有变化的数据; +- 缺点: + - 无法反应数据的变化情况; + - 无法得到某一个历史时间点(时间切片,如某一天)的切片数据; + +#### 3.5. 方案四:使用拉链表 + +- 拉链表可以看做是对前两种方案的一种折中处理。 +- 首先,拉链表在空间上做了一个取舍,虽然不像方案一那样占用量那么小,但是它每日的增量可能只有方案二的千分之一甚至是万分之一,所以空间占用问题也在可接受范围内。 +- 其次,它也能满足方案二所能满足的需求,既能获取最新的数据,也能添加筛选条件、以获取历史的数据。 + + +# 4. 拉链表的实现 + +#### 4.1. 基于 MySQL 实现拉链表 + +- 数据源 + - ODS 层用户全量表,需要用它来做初始化。 + - 每日的用户更新表,即每日增量表。 + +- 时间粒度 + - 确定拉链表的时间粒度,比如说每天拉链表只取一个状态,那即使一天有多次变化,也只取最后一次状态; + - 这种具体到天粒度的表,其实已经能解决大部分问题了。 + +- 获取更新变化 + - 每日的用户更新表,有三种方式拿到每日的用户增量: + - 方法一:监听 MYSQL 数据的变化,比如说用 Canal,最后合并每日的变化,获取到最后一个状态; + - 假设我们每天都会获得一份切片数据,我们可以通过两天切片数据的不同来作为每日更新表的依据; + - 这种情况下我们对所有的字段先进性 concat、再去MD5,就能实现更新了。 + - 方法二:流水表,有每日变更的流水表 + - 方法三:同步 ODS 用户全量表,对 `modify_time` 为当日时间的数据进行增量同步。 + +- 查询性能 + - 随着数据量的增长,链表也会遇到查询性能的问题,比如: + - 存放了 5年 的用户信息数据,表记录数量势必会比较大,查询的时候性能就比较低; + +- 解决思路 + - 在一些查询引擎中,对 start_date 和 end_date 做索引; + - 设计两张拉链表: + - 一张表存放全量的拉链表数据; + - 另一张表对外暴露,只提供近3个月数据的拉链表。 + +#### 4.2. 基于 Hive 实现拉链表 + +> **注意**: +> +> 在现在的大数据场景下,大部分公司都会选择以 HDFS 和 HIVE 为主的数据仓库架构。目前的 HDFS 版本来讲,其文件系统中的文件是不可做改变的,也就是说 HIVE 的表只能进行删除和添加操作,而不能 `update` 操作,基于这个因素我们需要采用 **中间表** 的方式实现。 + +- 中间表设计 + - TODO + - 参考:https://mp.weixin.qq.com/s/VGZbHLjd1-7Qx6WG4_N4ew \ No newline at end of file diff --git "a/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\344\273\223\344\270\255\347\232\204\346\225\260\346\215\256\345\210\206\345\261\202.md" "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\344\273\223\344\270\255\347\232\204\346\225\260\346\215\256\345\210\206\345\261\202.md" new file mode 100644 index 00000000000..47839acfacf --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\344\273\223\344\270\255\347\232\204\346\225\260\346\215\256\345\210\206\345\261\202.md" @@ -0,0 +1,211 @@ +--- +layout: post +title: 数仓建模:数仓中的数据分层 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓建模 +--- + + +# 1. 数据流向 + +![]({{site.baseurl}}/img-post/dw-layer-1.png) + +# 2. 数仓分层的价值 + +数据分层的好处: + +- 清晰数据结构: + - 让每个数据层都有自己的作用和职责,在使用和维护的时候能够更方便和理解; +- 复杂问题简化: + - 将一个复杂的任务拆解成多个步骤来分步骤完成,每个层只解决特定的问题; +- 统一数据口径: + - 通过数据分层,提供统一的数据出口,统一输出口径; +- 复用中间数据结果,减少重复开发: + - 规范数据分层,开发通用的中间层,可以极大地减少重复计算的工作,更加灵活应对企业开发需求; +- 降低存储压力 +- 降低企业使用成本 +- 提升数据处理速度 + - 不需要所有需求都从数据量最大的ODS层进行数据处理 + +# 3. ODS 源数据层 + +#### 3.1. ODS 定义 + +ODS(Operation Data Store),也称为贴源层。数据源中的数据,经过抽取、洗净、传输,也就是ETL过程之后进入本层。 + +#### 3.2. ODS 功能 + +ODS 层的数据,是后续数据仓库加工数据的来源: +- ODS是后面数据仓库层的准备区 +- 为DWD层提供原始数据 +- 减少对业务系统的影响 + +>为了考虑后续可能需要追溯数据问题,因此对于这一层就不建议做过多的数据清洗工作,原封不动地接入原始数据即可。 + +#### 3.3. ODS 数据来源 + +- 业务库: + - sqoop定时抽取数据;实时方面考虑使用canal监听mysql的binlog日志,实时接入即可 + +- 埋点日志: + - 日志一般是以文件的形式保存,可以选择使用flume来定时同步;可以使用spark streaming或者Flink、Kafka来实时接入 + +- 消息队列: + - 来自ActiveMQ、Kafka的数据等 + +#### 3.4. ODS 层数据存在的问题 + +- 字段缺失 +- 数据字段不统一 +- 格式错误 +- 关键信息丢失等等 +- 数据来源混杂 +- 数据类型不一 + - 例如json,xml,text,csv的, + - 压缩了的,没有压缩的 + +#### 3.5. ODS 层的数据存储 + +- 如果数据来自于日志文件,一般和原始日志文件格式一样; +- 如果数据来自于数据库,则看需要决定存储方式; + +>如果使用 hive 做 ODS 数据存储管理,遇到 JSON 时可以使用 JsonSerde 进行数据解析。 + +#### 3.6. ODS 层数据分区 + +- 一般都是按照天进行分区,如使用hive、则partitioned by 一般都是按照天进行存放。 + +#### 3.7. ODS 层存储容量设计 + +日志数据估算,如日活100万用户,每个用户访问1次,每次操作5min,每个用户平均3秒一条日志数据,一条数据1kb.最后体积是100w*1*5*60/3*1kb=100w*100kb=97656.25MB=95.36GB; + +>注意,数据估算最好结合公司实际情况,如果已经运行一段,可以让运维同事帮忙做估算 + +# 4. DWD 数据细节层 + +#### 4.1. DWD 定义 + +DWD(data warehouse details),该层是业务层和数据仓库的隔离层,保持和ODS层一样的数据颗粒度。 + +#### 4.2. DWD 功能 + +主要是对ODS数据层做一些数据的清洗和规范化的操作,比如去除空数据、脏数据、离群值等。 + +>为了提高数据明细层的易用性,该层通常会才采用一些维度退化方法,将维度退化至事实表中,减少事实表和维表的关联。 + +#### 4.3. ETL 数据清洗 + +ETL 数据清洗的数据,一般放到 DWD 层。具体的可以参考另一篇文章:《ETL方法论:数据清洗》。 + +#### 4.4. 维度表 + +>DWD 层,一般只存储维度表、事实表、实体表。 + +- 维度表,顾名思义就是一些维度信息,这种表数据,一般就直接存储维度信息,很多时候维度表都不会很大。 +- 维度表,一定是被共享的、通用性的,在数仓系统内一项维度只会有一张维度表; +- 维度表,轻易不会做变更修改,可以增加、尤其是不要轻易做修改和删除操作; +- 维度表,会与事实表的维度列做外键关联,使事实表可以生成更多的维度信息; + +- 维度表设计 + - 一般会把维度信息单独存放,其他表要使用时,记录对应维度的id即可。这样,就算维度表中数据发生了变化,其他表数据因为只是记录了id,不会有影响。 + - 维度信息放在一张表中存放,而不是每个表中存储一份,将来需要调整,只需要做一次工作即可,降低了数据冗余。 + + +#### 4.5. 事实表 + +- 事实表,就是表述一些事实信息,如订单、收藏,添加购物车等信息,这种数据量较大; +- 事实表由维度和度量组成,事实表中存储的是维度信息(维度列)、以及可度量的值(属性列); + - 比如:区域销售表中,地区是维度,而销售额是度量; +- 事实表中一般会使用一个代号、或者整数(如:ID)来代表维度成员,而不是描述性的名称; + - 比如:区域销售统计中,地区一般记录地区ID,而不是地区名称; +- 因为数据可能会变化,这种一般存储维度主键,具体维度值在后续处理分析时再临时关联; +- 事实表中的维度,一定会对应一张维度表; + +#### 4.6. 实体表 + +- 实体表,类似 javabean、用来描述信息的,如优惠券表、促销表,内部就是一些描述信息; +- 处理的频率,一般看数据量以及变化程度,大部分时候都是全量导入,导入周期则看具体而定。 + +#### 4.7. 拉链表 + + + +# 5. DWM 数据中间层 + +#### 5.1. DWM 定义 + +DWM(Data Warehouse Middle),该层是在DWD层的数据基础上,对数据做一些轻微的聚合操作。 + +#### 5.2. DWM 功能 + +- DWM 通过聚合,生成一些列的中间结果表: + - 提升公共指标的复用性,减少重复加工的工作。 + - 主要是对通用的核心维度进行聚合操作,算出相应的统计指标。 + +# 6. DWS 数据服务层 + +#### 6.1. DWS 定义 + +DWS(Data Warehouse Service),是基于DWM上的基础数据,整合汇总成分析某一个主题域的数据服务层,一般是宽表。 + +- 宽表,是维度表和统计后的事实表的整合; +- 宽表中,会包括详细的维度信息和度量值,需要 DWD 的表进行维度退化操作; + +#### 6.2. DWS 功能 + +DWS 用于提供后续的业务查询,OLAP分析,数据分发等。 + +>一般来说,该层的数据表会相对较少;一张表会涵盖比较多的业务内容,由于其字段较多,因此一般也会称该层的表为宽表。 + +# 7. ADS 数据应用层 + +#### 7.1. ADS 定义 + +ADS(Application Data Service),数据应用层,通常是存放计算得出的数据结果值,统计报表直接从这一层获取数据,然后在 BI 进行展示。 + +#### 7.2. ADS 功能 + +- DWS 主要是提供给数据产品和数据分析使用的数据: + - 一般会存放在ES、Redis、PostgreSql等系统中供线上系统使用; + - 也可能存放在hive或者Druid中,供数据分析和数据挖掘使用,比如常用的数据报表就是存在这里的。 + +# 8. 层次调用规范 + +#### 8.1. 不同需求阶段的规范 + +- 稳定业务, + - 按照标准的数据流向进行开发,即 ODS –> DWD –> DWS –> APP; + +- 非稳定业务或探索性需求, + - 遵循 ODS -> DWD -> APP 或者 ODS -> DWD -> DWM -> APP 两个模型数据流。 + +#### 8.2 分层引用原则 + +在保障了数据链路的合理性之后,也必须保证模型分层引用原则: + +- 正常流向: + - ODS -> DWD -> DWM -> DWS -> APP; + - 当出现 ODS -> DWD -> DWS -> APP 这种关系时,说明主题域未覆盖全,应将 DWD 数据落到 DWM 中; + - 对于使用频度非常低的表允许 DWD -> DWS。 + +- 尽量避免: + - 避免出现 DWS 宽表中使用 DWD 又使用(该 DWD 所归属主题域)DWM 的表; + - 尽量避免同一主题域内,DWM 生成 DWM 的表,否则会影响 ETL 的效率。 + +- 禁止出现: + - DWM、DWS 和 APP 中禁止直接使用 ODS 的表, ODS 的表只能被 DWD 引用; + - 禁止出现反向依赖,例如 DWM 的表依赖 DWS 的表。 + +# 9. 数仓分层注意事项 + +- 分层是解决数据流向和快速支撑业务的目的; +- 必须按照主题域和业务域进行贯穿; +- 层级之间不可逆向依赖; +- 如果依赖ODS层数据可以完成数据支撑,那么业务方直接使用落地层这也有利于快速、低成本地进行一些数据方面的探索和尝试。 +- 确定分层规范后,后续最好都遵循这个架构,约定成俗即可; +- 血缘关系、数据依赖、数据字典、数据命名规范等配套先行; \ No newline at end of file diff --git "a/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\344\273\223\345\273\272\346\250\241\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\344\273\223\345\273\272\346\250\241\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..fa1862bfa4d --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\344\273\223\345\273\272\346\250\241\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,295 @@ +--- +layout: post +title: 数仓建模:数仓建模的概念详解 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓建模 +--- + + +# 1. 数据模型 + +#### 1.1. 数据库类型 + +- 关系型数据库 + +![]({{site.baseurl}}/img-post/数仓建模-1.png) + +- 非关系型数据库 + +![]({{site.baseurl}}/img-post/数仓建模-2.png) + +#### 1.2. 数据模型的概念 + +> 信息 = 元数据 + 数据 + +数据模型,是将数据元素以标准化的模式组织起来,用来模拟现实世界的信息框架。 + +#### 1.3. 数据建模流程图 + +![]({{site.baseurl}}/img-post/数仓建模-3.png) + +#### 1.4. 主题域划分方法 + +- 按照业务域划分 + - 业务 + - 指业务模块、业务线。 + - 业务过程 + - 指企业的业务活动事件,如下单、支付、退款都是业务过程,通俗的讲业务过程就是企业活动中的事件。 +- 按照数据域划分 + - 数据域,是指面向业务分析,将业务过程或者维度进行抽象的集合。 + - 其中,业务过程可以概括为一个个不可拆分的行为事件,在业务过程下,可以定义指标,维度是指度量的环境,如买家下单事件,买家是维度。 + - 为保障整个体系的生命力,数据域是需要抽象提炼,并且长期维护和更新的,但不轻易变动; + - 在划分数据域时,既能涵盖当前所有的业务需求,又能在新业务进入时便于被包含进已有的数据域中、扩展新的数据域。 + +#### 1.5. 数仓模型设计原则 + +- 高内聚、低耦合 + - 即主题内部高内聚、 不同主题间低耦合; + - 明细层按照业务过程划分主题,汇总层按照“实体+ 活动”划分不同分析主题,应用层根据应用需求划分不同应用主题。 + +- 核心模型和扩展模型要分离 + - 核心模型包括的字段支持常用的核心业务,扩展模型包括的字段支持个性化或少量应用的需要; + - 不能让扩展模型的字段过度侵入核心模型,以免破坏核心模型的架构简洁性与可维护性。 + +- 公共处理逻辑下沉及单一 + - 越是底层公用的处理逻辑,越应该在数据调度依赖的底层进行封装与实现; + - 不要让公用的处理逻辑暴露给应用实现,不要让公共逻辑多处同时存在。 + +- 成本与性能平衡 + - 适当的数据冗余可换取查询和刷新性能; + - 同时不宜过度冗余与数据复制。 + +- 模型可扩展性 + - 不能新增业务、就建新表,应在模型设计之初就考虑未来如何扩展; + +- 数据可回滚 + - 处理逻辑不变,在不同时间多次运行数据结果确定不变。 + +# 2. 低质量的数据模型 + +#### 2.1. 数据模型的常见问题 + +- 没有准确捕获需求 + - 主要是需求调研不够、沟通不充分、需求理解不准确、测试不详尽导致的; + - 会导致系统上线后,许多功能无法满足,进而出现表结构的大量的调整; + - 比如:子公司 HR 系统中员工编号不统一,导致数仓中员工编号重复。 +- 数据模型不完整 + - 主要指的是元数据不完整; + - 常见的问题是,业务系统表设计的时候没有数据字典,数仓建设时无法理解表结构; +- 各层模型与其角色不匹配 + - TODO +- 数据结构不合理 + - 主要指表结构设计不合理 + - 比如: + - 主键设计失误导致主键数据为空; + - 同一字段在不同表含义、属性、长度不一致; +- 抽象化不够,模型不灵活 + - TODO +- 没有遵循命名规范 + - 主要是表、字段命名太随意 + - 汉语拼音、汉字做字段 +- 缺少数据模型的定义和描述 + - 常见的问题,如: + - 表、字段缺少注释; + - 表、字段命名不符合见名知意原则; +- 模型可读性差 + - 主要集中在缺少标准化的管理工具和管理文档,比如: + - 直接使用 DDL、Word、Excel 管理模型; + - 没有 ER 图; + - 缺少数据字典; +- 元数据与数据不匹配 + - 常见的问题,比如: + - 在客户表中,为图方便存储了供应商、代理商信息,导致维护表时无所适从; +- 数据模型与企业标准不一致 + - 业务系统在建设的时候,实施单位只考虑项目需要,没有统一的企业级规范。 + +#### 2.2. 低质量模型的危害 + +- 大量修改和重做; +- 重复建设; + - 每做一个新需求,都需要重新建库、建表; +- 知识丢失; + - 主要指的是缺乏文档; + >三个月内 DBA 知道,三个月后鬼才知道。 +- 下游开发困难; + - DATA Market 不理解表含义; +- 高成本; + - 沟通成本、时间成本、培训成本; +- 数据质量低; +- 阻碍新业务展开。 + +# 3. 高质量的数据模型 + +#### 3.1. 理想的数据模型 + +数据模型的要求: +- 直观模拟真实世界; +- 容易被人理解; +- 便于被计算机实现; +- 框架稳定、灵活; + - 能满足当下和未来的需求; +- 全局视角; +- 高性能; +- 低冗余; +- 易访问; +- 易接入; +- 易开发; +- 易维护。 + +#### 3.2. 高质量数据模型的价值 + +- 高效率; +- 灵活应对变动; +- 方便系统集成; +- 简化接口; +- 低冗余; +- 低风险; +- 灵活容纳新数据; +- 低成本。 + +>数据模型牵一发而动全身,一旦搭建好以后,就不要轻易作出改动,否则不仅会影响下游的数据库、表,还有会影响与之关联的代码程序。 + +# 4. 概念模型 + +概念模型环节,需要确定系统的核心,划清系统范围和边界。 + +#### 4.1. 概念模型包括的内容 + + - 实体关系图(关联关系); + - 核心业务流程; + - 组织架构(角色权限); + - 业务内容: + - 行业术语; + - 特殊流程; + - 专有术语; + - 用户群体; + +#### 4.2. 概念模型要注意的问题 + + - 注重全局、而非关注细节; + - 开始考虑整体架构、物理模型的实现; + - 如: + - 是否要上云计算、 + - 分布式还是集中式系统、 + - 容量及并发情况、 + - 选择开源产品还是商业产品、 + - 是否有特殊需求如 nosql、 + - 用 3NF 建模还是维度建模 + - 概念模型通常是自上而下模式,需要与基层反复沟通确认需求; + - 粗略估算项目时间 + - 给出项目计划草案 + - 估算项目费用预算 + - 包括硬件设备、软件、人员等; + - 数据工程师应该与业务部门之间建立良好的关系; + - 应划定系统边界、避免方向性错误; + - 最核心的,是确定那些东西不做; + - 需要业务部门支撑; + +#### 4.3. 概念模型交付品的特点 + - 与客户一致的商业语言; + - 一页纸描述清楚; + +# 5. 逻辑模型 + +逻辑模型环节,着重梳理业务规则,了解底层的数据细节。这个环节占据数据建模时间的 80% 以上。 + +#### 5.1. 逻辑模型包括的内容 + + - 主题: + - 有多少个主题; + - 每个主题包含的实体,比如: + - 客户; + - 商品; + - 店铺; + - 订单; + - 金额; + - 等。 + - 实体的属性 + - 比如: + - 客户属性包括:客户编号、客户姓名、电话、住址、性别、年龄…… + - 实体间关系 + - 比如: + - 客户编码,来自客户实体(客户信息维度表); + - 商品编号,来自商品实体(商品信息维度表); + - 关系约束 + - 即不同实体间的关系约束; + - 比如: + - 订单与客户之间为关系约束; + - 客户编码唯一; + - 一个订单,只能对应一个客户; + - 一个客户,可以对应多个订单; + +#### 5.2. 逻辑模型要注意的问题 + + - 实体数量超过 100 时,需要定义术语表; + - 先试用 3NF 规范化建模,再逆规范化、进一步优化; + - 使用 CASE 工具做逻辑建模工具; + - 如:PowerDesigner + >使用 CASE 工具,可以直接将逻辑模型、转换为物理模型。 + - 需要定期做同级评审,不能闭门造车; + - 在使用外部数据前,需要先检验数据质量是否合格; + - 关键属性,要用真实数据做验证,不要上来就用; + - 参考成熟的建模模式 + - 如:Pattern + - 需要对数据做一定的抽象化处理; + - 需要做高质量的模型定义; + - 重要的关联关系,需要强制建立主外键; + - 逻辑模型,需要与关系模型保持一致; + - 注意模型的版本管理; + - 需要数据库专家 DBA 借入; + - 注意细节; + - 不要忽略属性的长度定义和约束定义; + - 不要忽略属性的默认值; + - 使用控制数据范围的域; + - 逆规范化在这一层完成。 + +#### 5.3. 逻辑模型交付品的特点 + + - 所有的实体,都需要添加属性; + - 实体间的关系,需要描述清楚; + - 使用术语表; + - 严格遵循命名规则; + - 使用 case 工具创建项目文件; + +# 6. 物理模型 + +#### 6.1. 物理模型包括的内容 + + - 字段类型、长度 + - 是否可为空 + - 默认值 + - 所属的域 + - 字段值定义 + - 比如字段为枚举类型,每个枚举值对应的具体含义是什么; + - 约束 + - 唯一主键 + - 唯一值 + - 强关系约束 + - 术语表 + - 缩写规范 + +#### 6.2. 物理模型要注意的问题 + - 不直接生成,使用 CASE 工具由逻辑模型自动生成; + - 使用术语表,自动转换生成字段名称; + - 对表空间、索引、试图、物化视图、主键、外键等,都有命名规则; + - 逆规范化不在物理层完成,而是在逻辑层完成; + - DBA 深度介入,需要 DBA 的评审; + - 物理层模型,需要和数据库的 DDL 保持一致; + - 注意版本管理,开发、测试、生产一定要分开管理; + - 需要注意性能; + - 估算数据规模; + - 考虑数据归档; + - 应充分考虑未来使用数据库的优缺点,选择合适的数据库; + +#### 6.3. 物理模型交付品的特点 + - 自动生成基础库表结构; + - 必须选择合适的数据库; + - 需要生成并发布数据字典; + - 可以直接生成 DDL; + - DDL 中需要注意注释文本; + diff --git "a/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\344\273\223\345\273\272\350\256\276\350\247\204\350\214\203\346\214\207\345\215\227.md" "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\344\273\223\345\273\272\350\256\276\350\247\204\350\214\203\346\214\207\345\215\227.md" new file mode 100644 index 00000000000..8de0424d9f6 --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\344\273\223\345\273\272\350\256\276\350\247\204\350\214\203\346\214\207\345\215\227.md" @@ -0,0 +1,712 @@ +--- +layout: post +title: 数仓建模:数仓建设规范指南 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓建模 +--- + +>本文原文刊自《五分钟学大数据》,此处修正了原文的部分错误,增加了部分内容和细节。 + +# 1. 数仓模型设计原则 + +#### 1.2. 数仓模型设计目标 + +- 高内聚、低耦合 + - 即主题内部高内聚、 不同主题间低耦合; + - 明细层按照业务过程划分主题,汇总层按照“实体+ 活动”划分不同分析主题,应用层根据应用需求划分不同应用主题。 + +- 核心模型和扩展模型要分离 + - 核心模型包括的字段支持常用的核心业务,扩展模型包括的字段支持个性化或少量应用的需要; + - 不能让扩展模型的字段过度侵入核心模型,以免破坏核心模型的架构简洁性与可维护性。 + +- 公共处理逻辑下沉及单一 + - 越是底层公用的处理逻辑,越应该在数据调度依赖的底层进行封装与实现; + - 不要让公用的处理逻辑暴露给应用实现,不要让公共逻辑多处同时存在。 + +- 成本与性能平衡 + - 适当的数据冗余可换取查询和刷新性能; + - 同时不宜过度冗余与数据复制。 + +- 数据可回滚 + - 处理逻辑不变,在不同时间多次运行数据结果确定不变。 + +#### 1.2. 数仓模型设计经验 + +- 分层是解决数据流向和快速支撑业务的目的; +- 必须按照主题域和业务域进行贯穿; +- 层级之间不可逆向依赖; +- 如果依赖ODS层数据可以完成数据支撑,那么业务方直接使用落地层这也有利于快速、低成本地进行一些数据方面的探索和尝试。 +- 确定分层规范后,后续最好都遵循这个架构,约定成俗即可; +- 血缘关系、数据依赖、数据字典、数据命名规范等配套先行; + +# 2.层次调用规范 + +#### 2.1. 不同需求阶段的调用关系 + +- 稳定业务, + - 按照标准的数据流向进行开发,即 ODS –> DWD –> DWS –> APP; + +- 非稳定业务或探索性需求, + - 遵循 ODS -> DWD -> APP 或者 ODS -> DWD -> DWM ->APP 两个模型数据流。 + +#### 2.2 分层引用原则 + +在保障了数据链路的合理性之后,也必须保证模型分层引用原则: + +- 正常流向: + - ODS -> DWD -> DWM -> DWS -> APP; + - 当出现 ODS -> DWD -> DWS -> APP 这种关系时,说明主题域未覆盖全,应将 DWD 数据落到 DWM 中; + - 对于使用频度非常低的表允许 DWD -> DWS。 + +- 尽量避免: + - 避免出现 DWS 宽表中使用 DWD 又使用(该 DWD 所归属主题域)DWM 的表; + - 尽量避免同一主题域内,DWM 生成 DWM 的表,否则会影响 ETL 的效率。 + +- 禁止出现: + - DWM、DWS 和 APP 中禁止直接使用 ODS 的表, ODS 的表只能被 DWD 引用; + - 禁止出现反向依赖,例如 DWM 的表依赖 DWS 的表。 + +#### 2.3. 数据流向 + +![]({{site.baseurl}}/img-post/dw-layer-1.png) + +# 3. 表结构及数据内容规范 + +#### 3.1. 数据类型规范 + +需统一规定不同的数据的数据类型,严格按照规定的数据类型执行。 + +例如: +- 金额: + - double 或 使用 decimal(28,6) 控制精度等,明确单位是分还是元; +- 字符串: + - string; +- id类: + - bigint; +- 时间: + - string; +- 状态: + - string; + +常见的数据类型: + +![]({{site.baseurl}}/img-post/dw-layer-3.png) + +#### 3.2. 数据冗余规范 + +宽表的冗余字段要确保: + +- 冗余字段要使用高频,下游3个或以上使用; +- 冗余字段引入不应造成本身数据产生过多的延后; +- 冗余字段和已有字段的重复率不应过大, + - 原则上不应超过 60%,如需要可以选择 join 或原表拓展。 + +#### 3.3. NULL值处理规范 +- NULL 不代表 0,也不代表 0 长度的字符串; +- NULL 含义: + - 真未知; + - 尚未知; + - 不适用; + +> NULL 的长度不是 0 ! + +- 同一个数据库,对于 Null 值的处理应有统一的原则; +- 数据质量要求不高的,默认值可以为 Null,然后对特殊字段特殊处理; +- 数据质量要求较高的,默认值均为 Not Null,然后特殊字段特殊处理; + +#### 3.4. 范式建模 Null 值使用原则 + +- 主数据表: + - CODE 为主键,不可以为空; + - 未知的值,可以按以下方式赋值: + - 未知; + - 不适用; + - 非法; + - 未分类; + - NAME 不可以为空; + - 默认值可设为空字符串 ''; +- Xref 表: + - 多字段构成联合主键,不可以为空; + - 主要关心的的是多对多关系; +- 事务表: + - 引用主数据表的字段、设为主键,不可以为空; + - 为防止聚合计算时出现错误,应充分考虑 Null 值的处理方式,比如增加特殊字段; + +#### 3.5. Null 存在的隐患 + +- 数学计算 + - 任何值和 Null 计算,结果都是 Null; + - 1 + Null = Null; + - 1 / Null = Null; +- WEHRE 子句 + - 使用 <>、全选时,Null 值所在的这条记录,会被忽视掉; +- Join + - Null 和 Null 做 Join 操作的时候,是不会有结果的; +- 聚合函数 + - Null 在计算的时候,没有参与聚合计算; + - 若 Null 重设为 0,则 AVG、MIN、MAX、COUNT 都会受到影响; +- 子查询 + - 使用 NOT IN 的时候,会出现错误; + - 使用 NOT EXISTS,才能保证正常结果; + +![]({{site.baseurl}}/img-post/数仓建模-关系型数据库建模-2.png) + +#### 3.6. 键设计注意问题 + +- 表更新的复杂性 + - 多系统合并、需要多个自然键组合做主键时,尽量不要使用复合键; + - 可以使用 Checksum key,即对多系统的自然键进行 MD5 计算,用 MD5 结果值作为单键主键; + +- SQL 语句复杂性 + - 当 SQL 需要多表关联的时候,SQL 复杂性会迅速增加。 + +- 空间占用 + - 自然键: + - 维度表,需要空间比较少; + - 事实表,通常需要更多空间; + - 整体来说,需要更多空间; + - 代理键: + - 维度表,需要更多空间; + - 事实表,需要更少空间; + - 整体来说,需要更少空间; + +- SQL 语句资源消耗 + - 代理键为整型,查询性能较好; + - 自然键如是 VARCHAR,性能比较差; + - 单键的性能较好; + - 复合键的性能差; + - 当数据量达到百万级的时候,性能差异会凸显; + +- 数据加载速度 + - 自然键: + - 维度表,直接从源系统过数据,额外开销比较少; + - 代理键: + - 维度表,代理键插入必须计算最新插入的代理键,需要时间,但和自然键差别不大; + +# 4. 主题域划分规范 + +#### 4.1. 按照业务域划分 +- 业务 + - 指业务模块、业务线。 +- 业务过程 + - 指企业的业务活动事件,如下单、支付、退款都是业务过程,通俗的讲业务过程就是企业活动中的事件。 + +#### 4.2. 按照数据域划分 + +- 数据域,是指面向业务分析,将业务过程或者维度进行抽象的集合。 + - 其中,业务过程可以概括为一个个不可拆分的行为事件,在业务过程下,可以定义指标,维度是指度量的环境,如买家下单事件,买家是维度。 +- 为保障整个体系的生命力,数据域是需要抽象提炼,并且长期维护和更新的,但不轻易变动; +- 在划分数据域时,既能涵盖当前所有的业务需求,又能在新业务进入时便于被包含进已有的数据域中、扩展新的数据域。 + +#### 4.3. 按照部门划分 +- 运营域,如工资支出分析、活动宣传效果分析等主题; +- 技术域。 + +#### 4.4. 根据需求方划分 + +- 如财务部,就可以设定对应的财务主题域,而财务主题域里面可能就会有员工工资分析,投资回报比分析等主题。 + +# 5. 指标设计规范 + +#### 5.1. 指标规范设计目的 + +- 指标定义标准化: + - 名称: + - 原子指标的英文名称、中文名称、概述; + - 口径: + - 主题域内,指标口径必须一致; + - 统一对外输出的数据口径,避免同一指标不同口径的情况发生。 + - 含义: + - 主题域内,无歧义; + - 数据出口: + - 通过数据分层,提供统一的数据出口; + - 方便平台化开发。 + +#### 5.2. 指标分类 + +- 原子指标: + - 原子指标三要素: + - 业务过程; + - 度量值; + - 聚合逻辑; +- 派生指标: + - 在原子指标的基础之上,进行一系列逻辑运算得出。 + + +#### 5.3. 指标梳理 + +- 原子指标的归属产线、业务板块、数据域、业务过程; +- 原子指标的统计数据,来源于该业务过程下的原始数据源; +- 指标计算函数; +- 根据指标函数,自动生成原子指标的定义表达式; +- 根据指标定义、表达式以及数据源表,生成原子指标SQL; +- 注意: + - 在数据治理中,我们将需求梳理到的所有指标进行进一步梳理,明确其口径; + - 指标口径的不一致,使得数据使用的成本极高,经常出现口径打架、反复核对数据的问题; + - 如果存在两个指标名称相同,但口径不一致,先判断是否是进行合并,如需要同时存在,那么在命名上必须能够区分开。 + +# 6. 数据表处理规范 + +#### 6.1. 增量表处理规范 + +- 增量数据是上次导出之后的新增数据; +- 记录每次增加的量,而不是总量; +- 增量表,只报变化量,无变化不用报; +- 每天一个分区。 + +#### 6.2. 全量表处理规范 + +- 每天的所有的最新状态的数据。 +- 全量表,有无变化,都要报; +- 每次上报的数据,都是所有的数据(变化的 + 没有变化的); +- 只有一个分区。 + +#### 6.3. 快照表处理规范 + +- 按日分区,记录截止数据日期的全量数据。 +- 快照表,有无变化,都要报; +- 每次上报的数据都是所有的数据(变化的 + 没有变化的); +- 一天一个分区。 + +#### 6.4. 拉链表处理规范 + +- 记录截止数据日期的全量数据; +- 记录一个事物从开始,一直到当前状态的所有变化的信息; +- 拉链表每次上报的,都是历史记录的最终状态,是记录在当前时刻的历史总量; +- 当前记录存的,是当前时间之前、所有历史记录的最后变化量(总量); +- 只有一个分区。 + +# 7. 表的生命周期管理 + +#### 7.1. 表的等级划分 + +- 表的等级划分,主要将历史数据划分P0、Pl、P2、P3 四个等级; + - P0 + - 非常重要的主题域数据和非常重要的应用数据,具有不可恢复性,如交易、日志、集团 KPI 数据、 IPO 关联表; + - P1 + - 重要的业务数据和重要的应用数据,具有不可恢复性,如重要的业务产品数据; + - P2 + - 重要的业务数据和重要的应用数据,具有可恢复性,如交易线 ETL 产生的中间过程数据; + - P3 + - 不重要的业务数据和不重要的应用数据,具有可恢复性,如某些 SNS 产品报表。 + +#### 7.2. 表类型划分 + +- 事件型流水表(增量表) + - 事件型流水表(增量表)指数据无重复或者无主键数据,如日志; +- 事件型镜像表(增量表) + - 事件型镜像表(增量表)指业务过程性数据,有主键,但是对于同样主键的属性会发生缓慢变化,如交易、订单状态与时间会根据业务发生变更; +- 维表 + - 维表包括维度与维度属性数据,如用户表、商品表; +- Merge 全量表 + - Merge 全量表,包括业务过程性数据或者维表数据; + - 由于数据本身有新增的或者发生状态变更,对于同样主键的数据可能会保留多份,因此可以对这些数据根据主键进行 Merge 操作,主键对应的属性只会保留最新状态,历史状态保留在前一天分区中。 + - 例如,用户表、交易表等都可以进行 Merge 操作。 +-ETL 临时表 + - ETL 临时表是指 ETL 处理过程中产生的临时表数据,一般不建议保留,最多7天。 +- TT 临时数据 + - TT 拉取的数据和 DbSync 产生的临时数据最终会流转到 ODS 层,ODS 层数据作为原始数据保留下来,从而使得 TT & DbSync 上游数据成为临时数据; + - 这类数据不建议保留很长时间,生命周期默认设置为 93天,可以根据实际情况适当减少保留天数。 +- 普通全量表 + - 很多小业务数据或者产品数据,BI一般是直接全量拉取; + - 这种方式效率快,对存储压力也不是很大,而且表保留很长时间,可以根据历史数据等级确定保留策略。 + +#### 7.3. 表生命周期管理规范 + +通过对历史数据的等级划分、与对表类型的划分,生成相应的生命周期管理矩阵。 + +![]({{site.baseurl}}/img-post/数仓建设规范指南-1.png) + + +# 8. ODS 层设计规范 + +#### 8.1. 0DS 层表同步规范 + +- 一个系统源表只允许同步一次; +- 全量初始化同步和增量同步处理逻辑要清晰; +- 以统计日期和时间进行分区存储; +- 目标表字段在源表不存在时要自动填充处理。 + +#### 8.2. 表生命周期管理规范 + +- ods 流水全量表: + - 不可再生的永久保存; + - 日志可按留存要求; + - 按需设置保留特殊日期数据; + - 按需设置保留特殊月份数据; + +- ods 镜像型全量表: + - 推荐按天存储; + - 对历史变化进行保留; + - 最新数据存储在最大分区; + - 历史数据按需保留; + +- ods 增量数据: + - 推荐按天存储; + - 有对应全量表的,建议只保留14天数据; + - 无对应全量表的,永久保留; + +- ods etl 过程中的临时表: + - 推荐按需保留; + - 最多保留7天; + - 建议用完即删,下次使用再生成; + +- BDSync非去重数据: + - 通过中间层保留,默认用完即删,不建议保留。 + +#### 8.3. ODS 层数据质量规范 + +- 全量表必须配置唯一性字段标识; +- 对分区空数据进行监控; +- 对枚举类型字段,进行枚举值变化和分布监控; +- ods表数据量级和记录数做环比监控; +- ods全表都必须要有注释; + +# 9. DIM 层设计规范 + +#### 9.1. DIM 层维度表设计流程 + +- 选择维度 + - 要确保维度一致性有且只允许有一个维度定义。 + +- 确定主维表 + - 主维表一般是ODS 表,直接与业务系统同步。 + +- 确定相关维表 + - 确定哪些表和主维表存在关联关系,并选择其中的某些表用于生成维度属性。 + +- 确定维度属性 + - 一方面,尽可能生成丰富的维度属性; + - 另一方面,尽量沉淀出通用的维度属性,确定那些是核心属性。 + + +#### 9.2. DIM 层维度表设计规范 + +- DIM 层一致性 + - 在不同的物理表中的字段名称、数据类型、数据内容必须保持一致(历史原因不一致,要做好版本控制); + - 两个维度的公共维度属性结构和内容相同 + +- DIM 层共享维表 + - 重要的公共维度维表只能有一个,基于这些公共维度进行的交叉探查、不会存在任何问题; + +- 变化维的处理方式 + - 直接覆盖 + - 使用的较少,主要用于及其缓慢变化的维度表; + - 新增一列 + - 用以存储新、旧两次属性; + - 适用场景:适用于变化的维度属性较少; + - 新增一行 + - 每次变化都会新增一行; + - 适用场景:维表数据量较大,且用户关注历史变化。(一般采用拉链表的方式实现) + - 天级快照 + - 当维度数量较少时,可以每天保留一份全量快照数据; + - 缺点是重复存储严重。 + +- DIM 层维度表组合 + - 水平整合 + - 是指不同的来源表包含不同的数据集,会使得维度本身的记录数变多,是对原有维度的记录数上的补充; + - 这里需要考虑: + - 多个来源表是否存在重复,如果有就需要去重; + - 多个数据源表的自然键是否存在冲突,是否需要重新组合形成新的超自然键; + - 垂直整合 + - 简单来说就是增加字段; + - 不同的来源表包含相同的数据集,维度本身记录数不会变,只是会对维度属性进行补充; + +- DIM 层维度表拆分 + - 针对重要性,业务相关性、源、使用频率等可分为核心表、扩展表; + +>整合&拆分原则:出于扩展性、产出时间、易用性等方面的考虑,主要目标是保证主维表信息的全面性和稳定性,不因为个别维度的加入而导致维表产出不稳定。 + +- DIM 层特殊维度处理 + - 递归层次 + - 如服务记录的叶节点和根节点,可以选择打平或者降低粒度; + - 行为维度 + - 这类维表一般是快速变化维。如果维表太大且采用了拉链表时,可以考虑把快速变化的维度属性拆出来; + - 多值维度 + - 一条记录的一个维度属性有多个值 + - 比如一笔电商订单有多个商品,每笔订单能关联到商品维表多条记录; + - 一般采用降低粒度、多字段、桥接表的方式; + - 多值属性 + - 维表中的某个属性字段同时有多个值,称之为“多值属性”; + - 这种一般要么降低粒度,每个值一条,要么不拆,将字段整合到一个字段或者多个字段当中; + - 杂项维度 + - 由操作型系统中的指示符、或者标志宇段组合而成的维度,一般使用频率较小; + - 这种一般来说是会保留在事实表当中较好; + - 也可以构建一个集合杂项维度的特殊属性; + - 关联属性组合 + - 将维度与关联性强的字段进行组合,一起查询,一起展示,两个维度必须具有天然的关系,如:商品的基本属性和所属品牌; + - 适当的聚合分类 + - 经过计算的度量结果值,但下游当维度处理的,可以做聚合分类; + - 如:点击量 0-1000,100-1000; + +- DIM 层冗余 + - 数据记录较大的维度,可以适当冗余一些子集。 + +# 10. DWD 层设计规范 + +#### 10.1. 事务型事实表设计准则 + +- 基于数据应用需求的分析、设计事务型事实表; + - 结合下游、较大的针对某个业务过程和分析指标需求,可考虑基于某个事件过程、构建事务型实时表; + - 一般选用事件的发生日期或时间作为分区字段,便于扫描和裁剪; + +- 维度退化原则 + - 维度退化: + - 当一个维度没有数据仓库需要的任何数据时就可以退化此维度。需要把退化维度的相关数据迁移到事实表中,然后删除退化的维度。 + - 维度退化的优点: + - 在 DWD 层,事实表维度退化,可以减少后续使用 join 成本; + - 在 DWS 层,维度退化会提高 SQL 的执行速度,也会让数据关系更直观清晰; + - 简单的模式比复杂的更容易理解,也有更好的查询性能。 + +- 适当冗余原则 + - 有利于降低后续 IO 开销; + +#### 10.2. 快照事实表设计准则 + +- 周期快照事实表 + - 周期快照事实表中,每行汇总了发生在某一标准周期,如某一天、某周、某月的多个度量事件; + - 粒度是周期性的,不是个体的事务; + - 通常包含许多事实,任何与事实表粒度一致的度量事件、都是被允许的。 + +- 累积快照事实表 + - 多个业务过程联合分析而构建的事实表,如采购单的流转环节; + - 用于分析事件时间和时间之间的间隔周期; + - 少量的且当前事务型不支持的,如关闭、发货等相关的统计。 + +- 快照表生命周期管理 + - 按天分区: + - 3个月内最大访问跨度<=4天时,建议保留最近7天分区; + - 3个月内最大访问跨度<=12天时,建议保留最近15天分区; + - 3个月内最大访问跨度<=30天时,建议保留最近33天分区; + - 3个月内最大访问跨度<=90天时,建议保留最近120天分区; + - 3个月内最大访问跨度<=180天时,建议保留最近240天分区; + - 3个月内最大访问跨度<=300天时,建议保留最近400天分区; + +# 11. DWS 层设计规范 + +- 数据仓库的性能,是数据仓库建设是否成功的重要标准之一; +- 聚集的主要作用,是通过汇总明细粒度数据来获得改进查询性能的效果; +- 通过访问聚集数据,可以减少数据库在响应查询时必须执行的工作量,能够快速响应用户的查询,同时有利于减少不同用访问明细数据带来的结果不一致问题。 + +#### 11.1. DWS 聚集的基本原则 + +- 一致性 + - 聚集表必须提供与查询明细粒度数据一致的查询结果; + +- 避免单一表设计 + - 不要在同一个表中存储不同层次的聚集数据; + +- 聚集粒度可不同 + - 聚集并不需要保持与原始明细粒度数据一样的粒度,聚集只关心所需要查询的维度。 + +#### 11.2. DWS 层聚集的基本步骤 + +- 第一步:确定聚集维度 + - 在原始明细模型中会存在多个描述事实的维度,如日期、商品类别、卖家等,这时候需要确定根据什么维度聚集,如果只关心商品的交易额情况,那么就可以根据商品维度聚集数据。 + +- 第二步:确定一致性上钻 + - 这时候要关心是按月汇总还是按天汇总,是按照商品汇总还是按照类目汇总,如果按照类目汇总,还需要关心是按照大类汇总还是小类汇总。 + - 当然,这么做的只是了解用户需要什么,然后按照他们想要的进行聚集。 + +- 第三步:确定聚集事实 + - 在原始明细模型中可能会有多个事实的度量,比如在交易中有交易额、交易数量等,这时候要明确是按照交易额汇总还是按照成交数量汇总。 + +#### 11.3. DWS 层设计原则 + +除了聚集基本的原则外,公共汇总层还必须遵循以下原则: + +- 数据公用性 + - 需要思考汇总的聚集是否有多个用户使用,基于某个维度的聚集是不是经常用于数据分析中; + - 如果答案是肯定的,那么就有必要把明细数据经过汇总沉淀到聚集表中。 + +- 不跨数据域 + - 数据域是在较高层次上对数据进行分类聚集的抽象。如以业务区分统计周期; + - 在表的命名上要能说明数据的统计周期,如 _Id表示最近1天,_td 表示截至当天,_nd 表示最近N天。 + +#### 11.4. 关于宽表的误区 + +- 宽表的概念 + - 宽表,通是把很多的维度、事实上卷或者下钻之后关联到某一个事实表中,形成一张既包含了大量维度又包含了相关事实的表。 +- 宽表的优点 + - 宽表的使用,有其一定的便利性,使用方不需要再去考虑跟维度表的关联,也不需要了解维度表和事实表是什么东西。 +- 宽表的缺陷 + - 随着业务的增长,我们始终无法预见性地设计和定义宽表究竟该冗余多少维度,也无法清晰地定义出宽表冗余维度的底线在哪里。 + - 一个可能存在的情况是,为了满足使用上的需求,要不断地将维表中已经存在的列增加到宽表中。这直接导致了宽表的表结构频繁发生变动。 + +- 宽表问题应对方法 + - 根据主题域和业务域,将某个业务的所有节点梳理清楚; + - 将关键节点的数据作为事实表依据,然后横向扩充其他事实表上卷数据(包含一些统计指标),同时纵向的添加该节点上一些主键对应的维度; + - 宽表的涉及不依赖具体的业务需求而是根据整体业务线相匹配; + - 尽量用维度建模代替宽表; + +#### 11.5. 用维度建模代替宽表的原因 + +- 就算字段和数据会冗余,维度建模的方式也会表全量数据的宽表模式较好; +- 维度建模是以某一个既定的事实为依据,既然是事实表,那么这块的业务如果不变动的情况下,事实表的粒度基本不会改变; +- 事实表和维度表解耦,维度表的变更事实表基本不会影响,结果表也只需要回刷一下数据流程即可; +- 新增维度完全可以按照星型模型或者雪花模型动态添加新维度; +- 维度模型可以作为宽表的基础,一旦确定全部的数据流程,可以通过维度模型再生成对应宽表进行快速的业务支撑; + +# 12. 数仓命名规范 + +#### 12.1. 词根设计规范 + +>词根的作用,就是用来统一命名,比如表名、字段名、主题域名、索引名、键名等,使相同字段表达同一个广为人知的含义。 +> +>词根属于数仓建设中的规范,属于元数据管理的范畴,现在把这个划到数据治理的一部分。完整的数仓建设是包含数据治理的,只是现在谈到数仓偏向于数据建模,而谈到数据治理,更多的是关于数据规范、数据管理。 + +- 表命名 + - 很大程度上是对元数据描述的一种体现,表命名规范越完善,能从表名获取到的信息就越多; + - 比如: + - 一部分业务是关于货架的,英文名是:rack, rack 就是一个词根,那就在所有的表、字段等用到的地方都叫 rack,不要叫成别的什么。 + - 指标体系中有很多“率”的指标,都可以拆解成 XXX+率,率可以叫 rate,那我 们所有的指标都叫做 XXX+rate。 + + +- 示例: + - 以流程图的方式来展示,更加直观和易懂,本图侧重 dwm 层表的命名 规范,其余命名是类似的道理: + +![]({{site.baseurl}}/img-post/数仓建设规范指南-2.png) + +>第一个判断条件是该表的用途,是中间表、原始日志还是业务展示用的表 如果该表被判断为中间表,就会走入下一个判断条件:表是否有 group 操作 通过是否有 group 操作来判断该表该划分在 dwd 层还是 dwm 和 dws 层 如果不是 dwd 层,则需要判断该表是否是多个行为的汇总表(即宽表) 最后再分别填上事业群、部门、业务线、自定义名称和更新频率等信息即可。 + +- 分层:表的使用范围; + +- 事业群和部门:生产该表或者该数据的团队; + +- 业务线:表明该数据是哪个产品或者业务线相关; + +- 主题域:分析问题的角度,对象实体; + +- 自定义:一般会尽可能多描述该表的信息,比如活跃表、留存表等; + +- 更新周期:比如说天级还是月级更新; + +#### 12.2. 数仓层次命名规范 + + - 公用维度:dim + - DM层:dm + - ODS层:ods + - DWD层:dwd + - DWS层:dws + - DIM层:dim + - ADS层:ads + +#### 12.3. 周期/数据范围命名规范 + + - 日快照:d + - 增量:i + - 全量:f + - 周:w + - 拉链表:l + - 非分区全量表:a + +#### 12.4. 常规表命名规范 + + - 常规表是我们需要固化的表,是正式使用的表,是目前一段时间内需要去维护去完善的表; + - 范例:`<分层前缀[dwd|dws|ads]>_<部门>_<业务域>_<主题域>_<表内容>_<更新周期|数据范围>`; + - 业务域、主题域我们都可以用词根的方式枚举清楚,不断完善; + - 更新周期主要的是时间粒度、日、月、年、周等; + +#### 12.5. 中间表命名规范 + + - 中间表一般出现在 Job 中,是 Job 中临时存储的中间数据的表; + - 中间表的作用域只限于当前 Job 执行过程中,Job 一旦执行完成,该中间表的使命就完成了,是可以删除的; + - 按照自己公司的场景自由选择,有写公司会保留几天的中间表数据,用来排查问题。 + - 范例:`__<[0~9|dim]>_20200101` + - table_name 是我们任务中目标表的名字,通常来说一个任务只有一个目标表,这里加上表名是为了防止自由发挥的时候表名冲突; + - 末尾后缀,可以自由发挥起一些有意义的名字,或者简单粗暴使用数字代替,各有优劣谨慎选择; + - 遇到需要补全维度的表,这里使用 dim 结尾; + - 如果要保留历史的中间表,可以加上日期或者时间戳。 + +#### 12.6. 临时表命名规范 + + - 临时表是临时测试的表,是临时使用一次的表,就是暂时保存下数据看看,后续一般不再使用的表,是可以随时删除的表; + - 范例:`_` + - 只要加上 tmp 开头即可,其他名字随意; + - 注意 tmp 开头的表不要用来实际使用,只是测试验证而已。 + +#### 12.6. 维度表命名规范 + + - 维度表是基于底层数据,抽象出来的描述类的表; + - 维度表可以自动从底层表抽象出来,也可以手工来维护; + - 范例:_ + - 维度表,统一以 dim 开头,后面加上,对该指标的描述。 + +#### 12.7. 手工表命名规范 + +- 手工表是手工维护的表,手工初始化一次之后,一般不会自动改变,后面变更,也是手工来维护。 +- 一般来说,手工的数据粒度是偏细的,所以暂时统一放在 dwd 层,后面如果有目标值或者其他类型手工数据,再根据实际情况分层。 +- 范例:_<业务域>__ + - 手工表,增加特殊的主题域,manual,表示手工维护表。 + +#### 12.8. 指标命名规范 + +- 公共规则 + - 所有单词小写 + - 单词之间下划线分割(反例:appName 或 AppName) + - 可读性优于长度 (词根,避免出现同一个指标,命名一致性) + - 禁止使用 sql 关键字,如字段名与关键字冲突时 +col + - 数量字段后缀 _cnt 等标识... + - 金额字段后缀 _price 标识 + - 天分区使用字段 dt,格式统一(yyyymmdd 或 yyyy-mm-dd) + - 小时分区使用字段 hh,范围(00-23) + - 分钟分区使用字段 mi,范围(00-59) + - 布尔类型标识:is_{业务},不允许出现空值 + +# 13. 指标管理规范 + +#### 13.1. 指标管理体系的必要性 + +数仓模型中,最重要的模块可能就是数据治理,我们在建立数仓分层的时候,虽然解决 ETL 任务及工作流的组织、数据的流向、读写权限的控制、不同需求的满足等各类问题,但是在给业务方提供不同数据需求的情况下不可避免的会发生一下几个问题: + +- 指标定义不够清晰明确,两个页面上的指标定义其实是不同的,但是展示给商家看到的可能是同一个中文名称。又或者同样一个含义的指标在不同的界面上展示的名称却不相同,让人产生歧义。 + +- 同一个指标因为由不同的数据开发同学来制作,可能会被重复开发,不但造成资源浪费,还会造成维护困难。 + +- 对于需要新开发的指标,不仅缺少开发工具简化开发流程,甚至该使用哪些表,不该使用哪些表很大程度上都要凭借数据开发同学与数仓同学的经验。如果稍微马虎一点或者缺乏经验,比如使用了某些业务域下特有的表或者不是由数仓提供的统一中间层的表就可能会使用错误的数据,造成后期返工等情况。 + +- 而且在数据需求越来越多,数据中台提供的指标也日益丰富。但是指标定义混乱,描述不清会严重影响数据的可信度和数据开发的成本,所以就需要搭建一个指标系统,来维护已有的数据指标,并为未来可能新增的指标建立相应的规范。 + +#### 13.1. 指标管理体系建设规范 + +- 原子指标 + - 原子指标和度量含义相同,基于某一业务事件行为下的度量,是业务定义中不可再拆分的指标,具有明确业务含义的名词; + - 如支付金额。原子指标描述的其实是一种指标的类型,比如订单支付金额,支付订单数,下单订单数,PV,UV 等等。 + - 在数仓分层的时候,进行维度建模,那么就必须指定好相应的主题域和事实表处理的最小逻辑(也就是事实),那么在这个基础上可以先定义原子指标。 + + - 但业务方更关心的指标,是有实际业务含义,可以直接取数据的指标。比如: + - 店铺近1天订单支付金额就是一个派生指标,会被直接在产品上展示给商家看。 + - 这个指标却不能直接从数仓的统一中间层里取数(因为没有现成的事实字段,数仓提供的一般都是大宽表)。 + - 需要有一个桥梁连接数仓中间层和业务方的指标需求,于是便有了派生指标。 + +- 派生指标 + - 派生指标 = 业务域 + 维度 + 原子指标 + 修饰词。 + - 当维度,原子指标,修饰词都确定的时候就可以唯一确定一个派生指标,同时给出具体数值。 + - 例如:店铺近1天订单支付金额中店铺是维度,近1天是一个时间类型的修饰词,支付金额是一个原子指标。 + - 业务方制作每一个派生指标都是通过选择维度,原子指标,修饰词三种元数据来定义的,相对于使用名称来区别不同指标,更可以保证指标的唯一性。 + - 如果2个派生指标是不同的,那他们的组成部分一定会有区别,或是不同维度,或是不同原子指标,修饰词。 + - 在数仓搭建的时候,业务域、维度、原子指标都是已经明确的,而修饰词是维度的某一些特殊的值,对应 SQL 中的 where 过滤条件。 + - 如果在数据产品的层面在某个业务域对指标数据定义、生产、使用等过程的流程规范化与平台化,那么就能够从源头上解决上面出现的数据指标不统一、重复开发、指标体系不好维护的问题。 + +#### 13.3. 指标库 + +- 在指标管理的过程中,指标库给予每个指标一个精确且唯一的定义。通过指标库可以快速且规范的查询,开发和使用指标。 + +- 指标库主要提供如下服务: + + - 通过设置指标的组成要素来唯一精确定义每个指标(派生指标)。 + - 通过指标在业务域内唯一的性质,解决指标重复定义,重复开发,部分数据对不上的问题。 + - 通过将数仓中间层录入指标库为新制作指标提供指导性的 SQL 或库表推荐。 + - 打通其他各数据平台: + - 打通数据开发平台和统一数据服务平台,为指标的定义,调度,在线使用提供一条龙服务,简化开发流程。 + - 打通数据资产管理平台,沉淀指标的资产价值。 + - 打通 BI 平台,提供拖拽维度,指标生成报表的功能。 + + ![]({{site.baseurl}}/img-post/dw-layer-2.png) + + diff --git "a/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\346\215\256\345\273\272\346\250\241\345\221\275\345\220\215\350\247\204\350\214\203.md" "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\346\215\256\345\273\272\346\250\241\345\221\275\345\220\215\350\247\204\350\214\203.md" new file mode 100644 index 00000000000..35ab9f7f870 --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\346\225\260\346\215\256\345\273\272\346\250\241\345\221\275\345\220\215\350\247\204\350\214\203.md" @@ -0,0 +1,164 @@ +--- +layout: post +title: 数仓建模:数据建模命名规范 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓建模 +--- + + +# 1. 命名规范作用 + +- 防止出现多个系统、多套命名规范; + - 不同的数据库,有自己不同的规范; + - 比如 Oracle 就有自己的规范,其他系统的规范在这个系统就不适用; +- 防止出现歧义; +- 减少不理解命名现象; +- 降低沟通成本; + +# 2. 总体要求 + +- 清晰: + - 见名知意; + - 无歧义; +- 简单: + - 易读; + - 易懂; + - 不要滥用缩写; +- 通用: + - 可以共享; +- 稳定: + - 遵循经验共识; + - 不轻易变更; +- 一致性: + - 不同环境、保持同一含义; +- 用英文: + - 不要用汉字和拼音; +- 多用前后缀; + - 如:un_xxx、pre_xxx; + +# 3. 禁忌规范 + +#### 3.1. 命名禁忌 + +- 不用副词: + - 如:never、all; +- 不用冠词: + - 如:an、the; +- 不用代词: + - 如:it、them; +- 不用负向词: + - 如:not、no; +- 不用介词: + - 如:in、on、of; +- 不用连词: + - 如:and、or; +- 不用比较级: + - 如:better、more、best; +- 不用模糊词: + - 如:like、any; +- 不要用同义词: + - 同一含义、只用一个词; +- 不要一词多用: + - 每个词指标是一个意思; +- 不要有错别字; +- 不要用太专业的生僻词; + - 尤其是使用比较广泛的表和字段,尽量通俗易懂; +- 不要标新立异: + - 不要用大家看不懂的命名; +- 不用所有格: + - 如:customers_name; +- 不用时态、动名词; +- 缩写不要小写; + - 尽量用大写,如:CRM; +- 不要超过 75 个字符; + +#### 3.2. 缩写规范 + +- 尽可能使用现有缩写; +- 尽可能行业耳熟能详,不要标新立异; +- 词组,一般首字母缩写; +- 大写字母; +- to: + - 使用数字 2; +- for: + - 使用数字 4; +- 避免缩写冲突: + - 同一缩写,不能有多个含义; +- 保留辅音字母,省略元音字母; + +# 4. 命名对象 + +#### 4.1. 表空间 + + - 数据内容_用途_表空间_序号; + - 数据内容: + - 如:customer -> CSTM; + - 用途: + - 如: + - 表空间: + - 表内数据量; + - 如:10000条 -> 10K; + - 序号: + - 如:第一张表 -> 001; + +#### 4.2. 索引 Index + + - `_`; + +#### 4.3. 键 Key + + - 主键: + - `_`; + - 外键: + - `_`; + +#### 4.4. 约束 + + - `_`; + +#### 4.5. ODS 层表命名 + + - Schema Name + - 按照数据来源命名; + - Table Name + - 与源表保持一致 + +#### 4.6. DW 层表命名 + + - Schema Name + - 按照主题命名; + - Table Name + - `<表分类>__<后缀>`; + - 后缀: + - Detail; + - Log; + - REF: + - Reference,参照表,分类表,维度表; + +#### 4.7. DM 层表命名 + + - Schema Name + - 按照主题命名; + - Table Name + - `<分类表>__<后缀>`; + - 后缀: + - DIM + - Dimension,维度表; + - BRG + - Bridge,桥接表; + - FACT + - 常规事实表 + - PERIOD_SNAP + - Period_Snapshot,周期快照; + - ACCUM_SNAP + - Accumulate_Snapshot,累积快照表; + - ATOM + - 原子级别 + - SUMMARY + - 汇总级别 + diff --git "a/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\347\273\264\345\272\246\345\273\272\346\250\241.md" "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\347\273\264\345\272\246\345\273\272\346\250\241.md" new file mode 100644 index 00000000000..9df76e13a94 --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\347\273\264\345\272\246\345\273\272\346\250\241.md" @@ -0,0 +1,222 @@ +--- +layout: post +title: 数仓建模:维度建模 +subtitle: 维度建模 & 星型模型 & 雪花模型 & 星座模型 +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓建模 +--- + + +# 1. 常见的数据建模方法 + +- 数据仓库本质是从数据库衍生出来的,所以数据仓库的建模也是不断衍生发展的。 +- 从最早的借鉴数据库的范式建模,到逐渐提出维度建模,Data Vault模型,Anchor模型等等,越往后建模的要求越高,越需满足3NF,4NF等。 +- 对于数据仓库来说,目前主流还是维度建模,会夹杂着范式建模。 + + ![]({{site.baseurl}}/img-post/数仓建模方法-1.png) + +# 2. 维度表 + +#### 2.1. 维度表概念 + - 维度表,顾名思义就是一些维度信息,这种表数据,一般就直接存储维度信息,很多时候维度表都不会很大。 + - 维度表,一定是被共享的、通用性的, + >在数仓系统内,一项维度只会有一张维度表; + - 维度表,轻易不会做变更修改, + >维度可以增加,但是不要轻易做修改和删除操作; + - 维度表,会与事实表的维度列做外键关联,使事实表可以生成更多的维度信息; + - 维度表的属性,是所有查询约束和报表标示的来源; + - 维度提供数据的入口点,提供所有 DW/BI 分析的最终标识和分组。 + +#### 2.2. 维度表设计 + - 一般会把维度信息单独存放,其他表要使用时,记录对应维度的id即可。这样,就算维度表中数据发生了变化,其他表数据因为只是记录了id,不会有影响。 + - 维度信息放在一张表中存放,而不是每个表中存储一份,将来需要调整,只需要做一次工作即可,降低了数据冗余。 + - 维度表用于描述环境,多使用单一主键; + +#### 2.3. 维度分类 + - 退化维度(DegenerateDimension) + - 退化维度,指的是直接把一些简单的维度放在事实表中; + - 退化维度是维度建模领域中的一个非常重要的概念,它对理解维度建模有着非常重要的作用,退化维度一般在分析中可以用来做分组使用; + + - 缓慢变化维(Slowly Changing Dimensions) + - 维度的属性并不是始终不变的,它会随着时间的流逝发生缓慢的变化,这种随时间发生变化的维度我们一般称之为缓慢变化维(SCD); + - 比如员工表中的部门维度,员工的所在部门有可能两年后调整一次。 + + >如果按照范式建模规范,维度建模会存在大量冗余,但这些冗余在维度建模中是必要的的。 + +# 3. 事实表 + +#### 3.1. 事实表概念 + - 事实表,就是表述一些事实信息,如订单、收藏,添加购物车等信息,这种数据量较大; + - 事实表由维度和度量组成,事实表中存储的是维度信息(维度列)、以及可度量的值(属性列); + - 比如:区域销售表中,地区是维度,而销售额是度量; + - 事实表用于存储的是属性的度量; + +#### 3.2. 事实表设计 + - 事实表中的维度,一定会对应一张维度表; + - 事实表一般会有两个或者多个外健,与维度表的主键进行关联; + - 事实表的主键一般是组合健,表达多对多的关系。 + - 事实表中一般会使用一个代号、或者整数(如:ID)来代表维度成员,而不是描述性的名称; + - 比如:区域销售统计中,地区一般记录地区ID,而不是地区名称; + - 因为数据可能会变化,事实表一般存储维度主键,具体维度值在后续处理分析时再临时关联; + +#### 3.4. 事务事实表 + +- 事务事实表概念: + - 事务事实表,用于承载事务数据,通常粒度比较低; + - 事务事实表,是面向事务的,其粒度是每一行对应一个事务; + - 事务事实表,是最细粒度的事实表,例如产品交易事务事实、ATM交易事务事实; +- 事务事实表缺陷: + - 大表 join 大表,性能会比较低; + - 例如: + - 对下单事实表、和支付事实表,统计平均下单后支付时间,两个表都是大表,join 查询会很慢; + +#### 3.5. 周期快照事实表 + +- 周期快照事实表概念: + - 周期快照事实表,按照一定的时间周期间隔(每天,每月)来捕捉业务活动的执行情况,一旦装入事实表就不会再去更新,它是事务事实表的补充。 + - 用来记录有规律的、固定时间间隔的业务累计数据,通常粒度比较高, +- 使用场景: + - 存量记录: + - 账户月余额; + - 商品库存; + - 状态记录: + - 空气温度; + - 汽车行驶速度; + - 状态是连续的,必须进行周期性采样,构建周期快照事实表; +- 周期快照事实表,与事务事实表,是互补关系,二者可以同时存在; +- 示例: + - 统计: **每日-仓库-商品-数量**; + +#### 3.6. 累积快照事实表 + +- 累积快照事实表概念: + - 累积快照事实表,用来记录具有时间跨度的业务处理过程的整个过程的信息; + - 累积快照事实表,每个生命周期一行,通常这类事实表比较少见。 +- 累积快照事实表示例: + ![]({{site.baseurl}}/img-post/维度建模-1.png) + +# 4. 星型模型 + +- 星型模型主要是维表和事实表,以事实表为中心,所有维度直接关联在事实表上,呈星型分布。 + + ![]({{site.baseurl}}/img-post/数仓建模方法-6.png) + + 示例: + + ![]({{site.baseurl}}/img-post/数仓建模方法-3.png) + +# 5. 雪花模型 + +- 雪花模型,维度表的涉及更加规范,一般符合3NF; +- 雪花模型,有效降低数据冗余,维度表之间不会相互关联; +- 相比星型模型,雪花模型的特点是贴近业务,数据冗余较少,但由于表连接的增加,导致了效率相对星型模型来的要低一些。 + + ![]({{site.baseurl}}/img-post/数仓建模方法-7.png) + + +# 6. 星座模型 + +- 星座模型也是星型模型的扩展; +- 区别是星座模型中存在多张事实表,不同事实表之间共享维表信息,常用于数据关系更复杂的场景; +- 其经常被称为星系模型。 + + ![]({{site.baseurl}}/img-post/数仓建模方法-8.png) + +>星型模型 VS 雪花模型 +> +>- 星型模型,一般采用降维的操作,反规范化、不符合3NF,利用冗余来避免模型过于复杂,查询时只需要做少量的表链接,以提高易用性和分析效率,星型模型效率相对较高; +>- 雪花模型,维度表的涉及更加规范,一般符合三范式,有效降低数据冗余,维度表之间不会相互关联。雪花模型,可以看做是星型模型额变种。 +> +> +>- 星型模型和雪花模型的区别在于:维度表是直接连接到事实表还是其他维度表。 + + ![]({{site.baseurl}}/img-post/数仓建模方法-9.png) + +# 7. 维度建模的常规步骤 + +#### 7.1. 选择业务过程 + - 业务过程,通常表示的是业务执行的活动,与之相关的维度描述和每个业务过程事件关联的描述性环境; + - 通常由某个操作型系统支持,例如:订单系统; + - 业务过程建立或获取关键性能度量; + - 一系列过程产生一系列事实表; + +#### 7.2. 声明粒度 + - 粒度传递的是与事实表度量有关的细节级别; + - 精确定义某个事实表的每一行表示什么; + - 对事实表的粒度要达成共识; + - 粒度是非常重要的,粒度用于确定事实表的行表示什么; + +#### 7.3. 确认维度 + - 健壮的维度集合来粉饰事实表; + - 维度表示承担每个度量环境中所有可能的单值描述符; + +#### 7.4. 确认事实 + - 不同粒度的事实必须放在不同的事实表中; + - 事实表的设计完全依赖物理活动,不受最终报表的影响; + - 事实表通过外健关联与之相关的维度; + - 查询操作主要是基于事实表开展计算和聚合。 + +# 8. 维度表的设计 + +#### 8.1. 主键设计 + - 维度表的唯一主键,应该是代理键,而不是来自系统的标示符,也就是所谓的自然健; + - 因为自然键通常具有一定的业务含义,但日久天长,这些信息是有可能发生变化的; + - 而代理健可以提高关联效率并将关系数据库设计和业务的解耦; + - 维度表和事实表关联的每个连接应该基于无含义的整数代理健; + +#### 8.2. 事实表粒度 + - 建议从关注原子级别的粒度数据开始设计,因为原子粒度能够承受无法预估的用户查询; + - 注意可能的下钻需求,原子数据可以以各种可能的方式进行上卷,而一旦选择了高粒度,则无法满足用户下钻细节的需求; + +#### 8.3. 维度退化 + - 固定深度层次在维度表中应该扁平化; + - 规范化的雪花模型不利于多属性浏览; + - 而且大量的表和连接操作会影响性能; + - 非完全独立的维度应该合并为一个维度,将同一层次的元素、标示为事实表中不同的维度是错误的,会增加查询和存储负担,最后变成蜈蚣表; + - 例如:日维度、周维度、月维度等可以合并为一个周期维度。 + - 维度退化的目的: + - 方便浏览; + - 减少 join; + +#### 8.4. 维度表示例 + - 售前流程的雪花模型: + - 事实表:客户创建信息表; + - 维度表:销售信息表、店铺信息表、跟进表/约见表/风控通过表/订单表的维度上卷。 + ![]({{site.baseurl}}/img-post/数仓建模方法-5.png) + + +# 9. DataVault 模型 + +#### 9.1. DataVault 模型三种基本结构 +- 中心表-Hub + - 唯一业务键的列表,唯一标识企业实际业务,企业的业务主体集合 +- 链接表-Link + - 表示中心表之间的关系,通过链接表串联整个企业的业务关联关系 +- 卫星表-Satellite + - 历史的描述性数据,数据仓库中数据的真正载体 + +![]({{site.baseurl}}/img-post/数仓建模方法-4.png) + +- Hub 像人体的骨架,Link 是连接骨架的韧带组织,而 satelite 就是骨架上的血肉; +- DataVault 是对 E-R模型 更近一步的规范化,由于对数据的拆解和更偏向于基础数据组织; +- DataVault 在处理分析类场景时相对复杂,适合数仓低层构建,目前实际应用场景较少。 + +#### 9.2. DataVault 建模流程 +- 梳理所有主要实体; +- 将有入边的实体定义为中心表; +- 将没有入边切仅有一个出边的表定义为中心表; +- 源苦衷没有入边且有两条或以上出边的表定义为连接表; +- 将外键关系定义为链接表; + +# 10. Anchor 模型 + +Anchor 是对 DataVault 模型做了更近一步的规范会处理,初衷是为了设计高度可扩展的模型,核心思想是所有的扩张只添加而不修改,于是设计出的模型基本变成了 k-v 结构的模型,模型范式达到了6NF; +>由于过度规范化,使用中牵涉到太多的 join 操作,因而开发效率和执行性能都是严重挑战。 + + + + diff --git "a/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\350\214\203\345\274\217\345\273\272\346\250\241.md" "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\350\214\203\345\274\217\345\273\272\346\250\241.md" new file mode 100644 index 00000000000..2e3bc7ad626 --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\344\273\223\345\273\272\346\250\241\357\274\232\350\214\203\345\274\217\345\273\272\346\250\241.md" @@ -0,0 +1,416 @@ +--- +layout: post +title: 数仓建模:范式建模 +subtitle: 三范式 & E-R 模型 +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓建模 +--- + +# 1. 范式建模的概念 + +#### 1.1. 三范式 & 四范式 & 五范式: + +- 第一范式: + - 保证数据的原子性,没有重复列,列不可再分,也没有重复行; + - 数据规整成二维表; + - 确保每一列表达同一含义; + - 比如成绩中,分数和评价,要拆分成两列; + - 去掉多值属性; + - 比如:地址信息,拆分成省、市、区、详细地址; + - 拆成多列; + - 比如:一个人有多个电话号码,应拆分成电话_1、电话_2; + - 去掉重复组,挪到新表里面; + - 不好拆分的列,可以单独建表; + - 比如:每个人都有多个电话,就可以单独建一个电话号码表,新表中每一行、写入一组人员ID+电话号码; + - 确定主键; + - 确保每一行都是唯一的; + +- 第二范式: + - 确保表中的每列都和主键相关; + - 也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中; + - 如果不遵守第二范式: + - 会比存在数据冗余; + - 同时存在数据不一致的风险; + - 另外,关系也会被隐藏在表中,不能很直观的展示出来; + - 比如:下方学生成绩表中,学生姓名、学生课程,各自都只与主键学号有关,这是就应该拆分成两个表; + ![]({{site.baseurl}}/img-post/数仓建模-数仓建模注意的问题-1.png) + +- 第三范式 + + - 非主键属性只能依赖于主键,不能依赖于其他非主键属性; + - 如果非主键属性、依赖于其他非主键属性,则需要移出去创建新表; + - 比如:下方学生班级成绩表中,班级名称依赖于班级编号,而不是主键学号,这是就应该分拆成两个表; + ![]({{site.baseurl}}/img-post/数仓建模-数仓建模注意的问题-2.png) + + +- BCNF + + - BCNF 中,任何属性(包括主属性和非主属性),都不能被非主属性所决定; + - BCNF 必须规避下面的问题: + - 存在联合主键; + - 存在多个可选键; + - 可选键存在重叠; + - 比如:下表中,班级+课程、班级+教师,都可以作为联合主键,就可以拆分成两个表; + ![]({{site.baseurl}}/img-post/数仓建模-数仓建模注意的问题-3.png) + + +- 第四范式 + + - 多值依赖; + - 多值依赖:三个属性A、B、C在一起做联合主键,AB、AC都存在依赖关系,并且B和C之间没有依赖关系; + - 第四范式就是要消除多值依赖; + - 比如:下表中,学号、课程、语言做联合主键,学号+课程、学号+语言,是相互依赖的,但是课程和语言没有依赖关系,这时候就要拆分成两个表; + ![]({{site.baseurl}}/img-post/数仓建模-数仓建模注意的问题-4.png) + - 第四范式下: + - 数据冗余会减少; + - 数据关系会更清晰; + +- 第五范式 + + - 连接依赖: + - 连接依赖:三个属性A、B、C在一起做联合主键,AB、AC、BC都存在依赖关系; + - 第五范式就是要消除连接依赖; + + ![]({{site.baseurl}}/img-post/数仓建模-数仓建模注意的问题-5.png) + +- 范式模型常见表类型: + - 主数据表; + - 参照数据表; + - Xref 表; + - 事务表; + +- 三范式的优点: + - 减少冗余,同一含义的数据只存一份; + +#### 1.2. E-R 模型 + +- E-R 模型的组成是由实体,属性和联系。 + +- 实体 Entity + - 实体,是具体事物的抽象化描述,是数据库中存储的所有信息; + - 实体,对应数据库的表; + - 实体中的实例,指的就是一行行的数据; + - 每个实体(除非是弱实体)都必须有一个唯一标识属性的最小化集合。这个集合叫做实体的主键。 + + >实体,通常指的是业务场景中【名词】,也指一个要处理的抽象的【对象】。 + +- 关系 Relationship + + ![]({{site.baseurl}}/img-post/数仓建模方法-2.png) + + - 一对一 + - 指实体之间的关系是一一对应的,比如个人与身份证信息之间的关系就是一对一的关系。 + + - 一对多 + - 指一边的实体通过关系,可以对应多个另外一边的实体。相反,另外一边的实体通过这个关系,则只能对应唯一的一边的实体。 + - 比如说,我们新建一个班级表,而每个班级都有多个学生,每个学生则对应一个班级,班级对学生就是一对多的关系。 + + - 多对一 + - 与一对多情况相反。 + + - 多对多 + - 指关系两边的实体都可以通过关系对应多个对方的实体。比如在进货模块中,供货商与超市之 间的关系就是多对多的关系,一个供货商可以给多个超市供货,一个超市也可以从多个供货商那里采购商品。 + +- ER图示例 + ![]({{site.baseurl}}/img-post/E-R.png) + +# 2. 实体 Entity + +#### 2.1. 实体的定义 + +- 实体是一个数据的使用者,其代表软件系统中客观存在的生活中的实物,如人、动物,物体、列表、部门、项目等,而同一类实体就构成了一个实体集。 +- 实体中的所有特性称为属性,如用户有姓名、性别、住址、电话等。 +- "实体标识符"是在一个实体中,能够唯一表示实体的属性和属性集的标示符,但针对于一个实体只能使用一个"实体标识符"来标明。实体标识符也就是实体的主键。 +- 实体不会是单独存在的,实体和其他的实体之间是有着千丝万缕的联系的。 + +#### 2.2. 实体的分类 + +![]({{site.baseurl}}/img-post/数仓建模-关系型数据库建模-1.png) + +# 3. 属性 Attribute + +#### 3.1. 属性的定义 + +- 属性,对应的是表的列; +- 在关系型数据库中,属性通常都是单值的; + - 如果存在多值,则需要另外建属性表; + +#### 3.2. 属性的分类 + +- ID; + - 标识实体的唯一性,一般是唯一主键; +- 描述; +- 引用; + - 引用的其他实体的信息,比如订单表中会引入商品信息、客户信息,就需要加入商品ID、客户ID; +- 分类; + - 实体所属的分类,比如客户分为国内客户、国外客户; + - 注意:不同分类方法,会对应不同的属性值,需要注意对应关系和唯一性; +- 限制; + - 比如:最低价格; +- 数量; + - 比如:订单的采购数量、交易的金额等; +- 时间相关; + - 日期型; + - 时间戳性; + - 时间型; +- 人物相关; + - 比如:企业法人; +- 组织相关; + - 比如:销售门店; +- 地点相关; + - 比如:城市、国家、IP地址; +- 途径相关; + - 比如:销售途径平台电商、自建独立站、直播间、代理商; +- 站点相关; + - 比如:APP、web 站; +- 状态; + - 比如:员工在职、离职; +- 审计; + - 数仓中最常用的; + - 创建、修改间戳; + - 比如:add_time、update_time; + - 操作人; + - 订单录入人、系统自动生成等; + - 数据来源; + - 数据的来源; + - 比如:客户姓名、来自于客户信息系统,就要标注客户信息系统ID; +- 派生; + - 预计算数据; + - 数据内容的 MD5 校验码; + - ETL 数据对账中经常使用; + +#### 3.3. 属性的特性 + +- 强制 / 可选 + - 属性值是否可以为空; +- 原子 / 组合 + - 比如:姓、名都是原子的,二者在一起,组合构成姓名; +- 直接 / 派生 + - 比如:国家 + 省 + 城市 + 街道 + 门牌号 = 详细地址; +- 单值 / 多值 + - 关系型数据,不支持单实体、多属性,否则会违反 3NF,需要单独建子表; +- 可选键 + - 某一属性、或多个属性,可以作为判断唯一性的标识,即唯一键; +- 数据类型 + - INT、CHAR、DATE 等等; +- 默认值 + - 如果没有值,则赋与其默认值; + - 比如:审计中的更新时间,为系统自动生成时间 current timestamp; +- 派生属性计算逻辑 + - 比如:手机号前缀(如+86) + 手机号 = 完整手机号; + +# 4. 值域 Domain + +#### 4.1. 值域的定义 + +- 值域,指的是属性所有取值范围的集合; +- 值域,可以理解为自定义的数据类型,同时可以加上约束条件; +- 值域范围,需要根据实际的业务场景决定取值范围; + +#### 4.2. 值域的类型 + +- 格式: + - varchar; + - currency; + - email; + - 正则表达式匹配 @ 符: + - 身份证号; + - 位数限制、校验码; + - 等等; +- 列表: + - 枚举; + - 比如:销售渠道包含淘宝店、京东店铺、抖音店铺,除此以外的值都是非法; + - 月份; + - 从 1月 到 12 月; + - 性别; + - Male、Female、Unknown; + - 名称: + - LongName:VARCHAR(255),容纳公司名称、其他名称; + - ShortName:VARCHAR(50),容纳人名、部门名称; + - 等等; +- 范围: + - 年龄:0 ~ 100; + - 角度:-180 ~ +180; + + +#### 4.3. 值域的作用 + +- 数据质量: + - 值域范围规范,可以大幅提高数据质量; +- 易懂易用: + - 数据模型会更加容易理解和使用; +- 数据标准化: + - 提高模型质量和建模效率; + +#### 4.4. 值域的应用 + +- PostgreSQL、Oracle; + +>注意:MySQL,不支持值域!!! + +# 5. 关系 + +#### 5.1. 一对一 + +指实体之间的关系是一一对应的,比如个人与身份证信息之间的关系就是一对一的关系。 + +#### 5.2. 一对多 + +指一边的实体通过关系,可以对应多个另外一边的实体。相反,另外一边的实体通过这个关 系,则只能对应唯一的一边的实体。比如说,我们新建一个班级表,而每个班级都有多个学生,每个学生则对应一个班级,班级对学生就是一对多的关系。 + +#### 5.3. 多对一 + +与一对多情况相反。 + +#### 5.4. 多对多 + +指关系两边的实体都可以通过关系对应多个对方的实体。比如在进货模块中,供货商与超市之 间的关系就是多对多的关系,一个供货商可以给多个超市供货,一个超市也可以从多个供货商那里采购 商品。 + + +#### 5.5. E-R 关系图 + +![]({{site.baseurl}}/img-post/数仓建模方法-2.png) + +# 6. 键 Key + +#### 6.1. 候选键 + +- 一个或多个属性的结合,可以确定唯一实体; +- 比如: + - 员工编号、员工邮箱,都是唯一的,可以作为候选键; + +#### 6.2. 主键 + +- 从候选键中,选中作为唯一标识的属性、或者属性组; + +- 主键的特点: + - 唯一性:不可重复; + - 强制性:不可以为空; + - 永久性:不可以改变; + - 最小集合:不可以掺杂多余的属性; + >如果两个字段可以作为联合主键确定唯一值,就没有必要再加上第三个字段; + +- 设计主键要注意的问题: + - 尽量不要使用 SmartKey; + - SmartKey,指的是存在含义的编码 Key; + - 比如:身份证号码,对应位置的数值,是有特殊含义的,比如行政区域,这就存在超级辖区人口数量暴增,导致数值溢出的风险; + - 不要将 ID 编码植入程序, + - 否则一旦编码变化、程序会出现很大问题。 + - 比如:部门编码,指定位置指定对应的部门,但是如果组织架构调整,就会出现问题; + - 不能随着环境变化而收到影响 + - 设计之初,就要考虑未来可能遇到的场景; + - 键值应易管理、易维护; + - 不要有人工输入; + +#### 6.3. 单键 & 复合键 + +- 单键: + - 主键只有一个属性,称为单键; +- 复合键: + - 主键是多个属性的组合,称为复合键; + +#### 6.4. 自然键 & 代理键 + +- 自然键: + - 已经真实存在的键,通常具有真实的商业含义; + - 可以使单键,也可以是复合键; + - 灵活性: + - 自然键做主键,实体主键的变化、会带来外键相关表的连锁反应; + - 一旦 update,只能删除重做; +- 代理键: + - 完全没有商业含义,通常由系统自动生成; + - 灵活性: + - 代理键本身无意义,可以修改原自然键内容; + - 代理键 update 会更加容易; + - 使用场景: + - 完全没有自然键做主键; + - 分布式结构数据库,多联合主键的事实表,为了分区方便,创建单独的代理键,以避免数据分区的偏移; +- 外键: + - 引用其他表的主键; + +#### 6.3. 可选键 + +- 候选键中,没有选中的其他键; + +#### 6.4. 键的命名规则 + +- 自然键 +- 代理键 + - xxx_SK +- Checksum key + - xxx_CK + +# 7. 约束 + +#### 7.1. 唯一标识 + +- unique key; + +#### 7.2. 非空约束 + +- not null; + +#### 7.3. 默认值 + +- default; + +#### 7.4. 检查 + +- 属性取值符合规则; + +#### 7.5. 实体完整性(Entity integrity) +- 主属性不能为空值(即任一候选码的属性不能为空); +- 在实际上,只要主键属性不为空就好; + +#### 7.6. 参照完整性(Referential integrity) + +- 这是对外键的约束: +- 外键只能有两种取值情况: + - 这个外键在对应的表中是存在的; + - 空值; + +# 8. 建模经验 + +#### 8.1. ID 设计 + +- 8位 / 10位 / 15位 流水号; + - 如果从 1、2、3 开始,ID 长度都会不一致, + +- 设计规则 + - 前 2位(3位) 对应业务系统,后面为流水号; + - 注意流水号要考虑到数据增长情况; + - 项目整体规范化、标准化; + +#### 8.2. 表名称统一命名 + +- 统一的命名规则 + - ods_业务系统简称_业务系统原始表名称 + - dwd_业务系统简称_业务系统原始表名称 + +#### 8.3. 有限可穷举字段,一般用代码表示 + - 主要是为了统一标准格式; + - 比如:国家、民族,长度不一致, + - 可以通过关联对应的字典表获取, + - 国家有统一发布的规范标准。 + +#### 8.4. 第一范式必须严格遵守 +- 遵守第一范式,是范式建模的基础 + +#### 8.5. 数据与数据的关系,用表关联的方式体现出来 +- 数据与数据的关系,用表关联的方式体现出来,而不是用数据内容来体现 + +#### 8.6. 尽可能的减少冗余 +- 同一含义的数据,尽可能只存一分 + +#### 8.7. 一般不用第四范式和第五范式 +- 第四范式和第五范式,可以参考但实际不常用; + +# 9. 建模工具 + +- Powerdesigner +- E-Rwin; +- Datablau; \ No newline at end of file diff --git "a/_posts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\344\272\247\345\223\201\347\273\217\347\220\206\346\211\213\345\206\214.md" "b/_posts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\344\272\247\345\223\201\347\273\217\347\220\206\346\211\213\345\206\214.md" new file mode 100644 index 00000000000..a608c126515 --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\344\272\247\345\223\201\347\273\217\347\220\206\346\211\213\345\206\214.md" @@ -0,0 +1,246 @@ +--- +layout: post +title: 数据产品:产品经理手册 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 数据产品 +--- + + +# 数据探查(入职必做) + +#### 数据来源 + + - 来源系统 + - 如:ERP、金蝶、TMS + - 来源(维护)单位 + - 如:销售部门、第三方服务商 + +#### 数据获取方式 + + - API + - JDBC + - 手工上传 + +#### 表内容 + + - 按分类 + - 事实表 + - 事务事实表 + - 周期快照事实表 + - 累积快照事实表 + - 维度表 + - 统计中间表 + - 如:业务部门手工计算后汇总的销售数据; + - 按问题 + - 脏数据 + - 缺失值 + - 重复数据 + - 异常数据 + +#### 抽取方式 + + - 全量同步 + - 增量同步 + +#### 数据统计 + + - 总量 + - 总大小 + - 总库数 + - 总表数 + - 增量 + - 周增量 + - 历史每月增量 + - 大表 + - 总数 + - 条数 + - 大小 + - 周增量 + - 条数 + - 大小 + - 重要日期数据量 + - 如: + - 618 + - 双十一 + - 双十二 + - 女生节 + - 情人节 + - 春节 + - 单日条数 + - 当周条数 + - 环比增长 + - 分区逻辑 + +# 需求梳理 + +#### 功能模块 + + - 重要性 + - 核心功能 + - 次要功能 + - 实现难度 + - 简单功能 + - 复杂功能 + - 紧急度 + - 紧急需求 + - 不紧急需求 + +#### 指标拆解 + + - 一级指标 + - 二级指标 + - 三级指标 + - 四级指标 + +#### 需求方 + + - 组织 + - 部门 + - 上下级部门 + - 部门间业务协作关系 + - 人员 + - 接口人 + - 部门 leader + - 大领导 + - 人员角色关系 + +#### 重叠需求 + - 即: + - 其他部门是否有相同需求内容; + - 货是否与其他部门需求有重叠内容; + - 重叠部门 + - 重叠内容 + - 重叠数据 + - 重叠指标 + - 重叠逻辑 + - 整合思路 + +#### 源数据 + + - 获取方式 + - API + - JDBC + - 手工导入 + - 字段类型 + - 字段长度 + - 上传频率 + - 上传时间 + - 易出错字段 + - 唯一标识 + - 唯一键 + - 联合唯一键 + - 数据更新频率 + - 日 \ 周 \ 月 \ 年 + - 数据维护部门 + - 数据维护人员 + +#### 处理逻辑 + + - 统计口径 + - 所需字段 + - 注意: + - 每个指标对应一套逻辑 + +#### 更新频率 + + - 每 小时 \ 日 \ 周 更新一次 + - 注意: + - 更新频率需要的计算资源是否扛得住; + - 注意节日大促前,提前测试计算资源可靠性; + +#### 版本 + + - 所属版本号,例如:v1.1 + - 注意: + - 保留历史版本信息,以方便后续溯源; + +# ETL / 数仓 + +#### 源表 + +- 审计字段 + - create_time + - update_time + +#### ODS 层表 + +- 审计字段 + - create_time + - update_time + +#### 数据同步 + + - 全量同步 + - 增量同步 + - 增量同步逻辑 + +#### 数据对账 + + - 对账逻辑 + - 例如: + - 日增量同步,检查今天零点前的数据量是否一致; + - 全量数据,检查全表数据量是否一致; + +#### 中间表 + - 表字段 + - 可复用字段 + - 复用任务 + +#### 目标表 + + - DWS 表 + - ADS 宽表 + +#### 数仓分层 + +- ODS +- DW + - DWD + - DWM + - DWS +- ADS + +#### 任务依赖 + + - 上下游依赖关系 + - 是否存在参数传递 + +#### 任务调度 + + - 任务时间 + - 任务频率 + +# 数据分析 + +#### 分析思路 + +- 根据具体业务逻辑、统计口径设计,此处不赘述; +- 基于 DWS 层宽表数据展开; +- 数据分析任务,在 ETL 中配置好依赖关系和调度逻辑; + +#### 统计工具 + +- SQL +- Python +- R +- 等 + +# 数据产品 + +#### 可视化产品 + +- BI +- 报表 + +#### 数据服务 + +- 例如: + - 为用户画像提供标签数据服务; +- API +- JDBC + diff --git "a/_posts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\346\225\260\346\215\256\344\272\247\345\223\201\346\266\211\345\217\212\347\232\204\346\212\200\346\234\257\345\267\245\345\205\267.md" "b/_posts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\346\225\260\346\215\256\344\272\247\345\223\201\346\266\211\345\217\212\347\232\204\346\212\200\346\234\257\345\267\245\345\205\267.md" new file mode 100644 index 00000000000..71cf012ef9f --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\346\225\260\346\215\256\344\272\247\345\223\201\346\266\211\345\217\212\347\232\204\346\212\200\346\234\257\345\267\245\345\205\267.md" @@ -0,0 +1,148 @@ +--- +layout: post +title: 数据产品:数据产品涉及的技术工具 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 数据产品 +--- + +# 1. 数据采集 + + +#### 1.1. 硬采集 + +- 包括:传感器、摄像头、语音设备、可穿戴智能设备。还有我们最常用的手机,都是数据采集的很好设备。这部分涉及到的产品,大都是硬件,产品形态是以一种解决方案的形式提供。 + +#### 1.2. 软采集 + +- 利用一些互联网手段采集数据,最常见的数据采集产品就是数据埋点了,通过埋点的方式,对用户日志采集。 +- 日志埋点方式又分为可视化埋点、前端埋点、后端埋点。 + +#### 2. 数据清洗 + +- 数据清洗,统一叫ETL(Extract-Transform-Load)或ELT,它是搭建数据仓库常用到的技术,一般都是给数据开发工程师使用的产品,但是也会有一些厂商把产品做的简单可配,降低使用门槛。 +- 数据清洗主要任务是过滤掉不符合要求的数据,处理不完整的数据、错误的数据、重复的数据三大类,保证数据质量的完整性、一致性、唯一性。 +- ETL,负责将分布、异构的数据源中的数据,如关系数据、平面数据、文件等取到临时中间层,进行清洗、转换、集成,最后加载到数据仓库或数据集市,成为联机分析处理、数据挖掘的基础。 + +- ETL 的目的,是将其中分散、零乱、标准不统一的数据,整合到一起,为企业的决策提供分析依据。ETL 是 BI 项目的重要环节,一般要花掉整个 BI 项目的三分之一的时间精力,ETL 设计的好坏直接关系到 BI 项目的成败。 + +- 其中,ETL三个部分中: + - Extractor,是从不同的数据源抽取到 ODS(operational data store),这部分尽可能的选用高效的抽取方法,以提升 ETL 的性能; + - Transform,数据清洗和转换,最耗费时间,一般这部分工作占了整个 ETL 的三分之二。 + - Load,清洗完的数据一般直接写入数仓 DW(data warehouse)中。 + +# 3. 存储 & 计算 + +![sql-17]({{site.baseurl}}/img-post/数据库-1.png) + +#### 3.1. 关系型数据库 + +- MySQL +- PostgreSQL +- MySQL 与 PostgreSQL 的异同: + - 编程语言: + - MySQL是C/C++混合开发,PostgreSQL则是完全的C语言开发 + - 事务支持: + - PostgreSQL支持事务的强一致性,事务保证性好,完全支持ACID特性。 + - MySQL只有innodb引擎支持事务,事务一致性保证上可根据实际需求调整,为了最大限度的保护数据,MySQL可配置双一模式,对ACID的支持上比PG稍弱弱。 + - SQL标准支持: + - PostgreSQL几乎支持所有的SQL标准,支持类型相当丰富。 + - MySQL只支持部分SQL标准,相比于PG支持类型稍弱。 + - PostgreSQL 和 MySQL 都依赖于 SQL; + - MySQL 主要功能包括所有 SQL 标准命令,以及事务和 ACID 合规性(代表原子性、一致性、隔离性和持久性); + +#### 3.2. 非关系型数据库 + +- 键值(key-value)数据库: + - 面向高性能并发读写,典型代表如Redis。 + +- 列存储(Columnar Storage)数据库: + - 面向PB级的分析应用,如:HBase,Hypertable。京东、阿里、腾讯、唯品会、圆通、顺丰等都将HBase大规模应用于准实时的数据分析挖掘计算,以及提供历史归档数据的存储和查询服务。 + +- 文档数据库: + - 特点是可以在海量的数据中快速地查询数据,如网页和移动应用数据。 + - 典型代表:MongoDB,CouchDB,Mark Logic + +- 图形数据库: + - 如应用在推荐系统、关系图谱,典型代表:new4j,InfiniteGraph,OrientDB + +#### 3.3. 新式关系型数据库(NewSQL) + +- NewSQL提供与NoSQL系统相同的扩展性能,且保持传统数据库支持的ACID特性。 +- 典型代表:SAP HANA,VoltDB,nuoDB,MariaDB,Pivotal。 + +#### 3.4. MPP(Massively Parallel Processing)数据库 + +- 指使用多个SQL数据库节点搭建的数据仓库系统,MPP解决了单个SQL数据库不能存放海量数据的问题。 +- 代表产品有Teradata,Vertica,Redshift,Greenplum + +#### 3.5. 常用数据计算存储选型 + +- 不同数据系统有各自的优势和适合的场景,但并没有一个数据系统可以适合各种各样的存储计算场景。 + +这里大致列举一些比较通用的选型: + +关系型数据库(Oracle/MySQL等):适合小数据量的复杂关系计算 + +分布式列存储系统 + +✔ Kudu:Scan优化,适合OLAP分析计算场景 + +✔ HBase:随机读写,适合提供数据服务场景 + +✔ Cassandra:高性能写,适合海量数据高频写入场景 + +✔ ClickHouse:高性能计算,适合只有insert写入场景(后期将支持更新删除操作) + +分布式文件系统 +✔ HDFS/Parquet/Hive:append only,适合海量数据批量计算场景 + +分布式文档系统 +✔ MongoDB:平衡能力,适合大数据量中等复杂计算 + +分布式索引系统 +✔ ElasticSearch:索引能力,适合做模糊查询和OLAP分析场景 + +分布式预计算系统 +✔ Druid/Kylin:预计算能力,适合高性能OLAP分析场景 + + +# 4. 可视化 + +- BI + - tableau + - power BI + - fine BI + - 永洪BI +- 可视化大屏 + - eacharts + - +- 报表 + +# 5. 数据挖掘 + +#### 5.1. 数据挖掘的应用 + +- 个性化推荐 +- 搜索排序 +- 风控模型 +- 用户画像 +- 计算广告 + +# 6. 数据服务 + +- SpringCloud +- SOA +- Restful API +- JDBC + +# 7. 数据管理 + +- 数据治理 +- 血缘关系 +- 数据标准化 +- 数据权限管理 \ No newline at end of file diff --git "a/_posts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\346\225\260\346\215\256\344\272\247\345\223\201\347\232\204\345\237\272\346\234\254\346\246\202\345\277\265.md" "b/_posts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\346\225\260\346\215\256\344\272\247\345\223\201\347\232\204\345\237\272\346\234\254\346\246\202\345\277\265.md" new file mode 100644 index 00000000000..cdfc24bd951 --- /dev/null +++ "b/_posts/2022-01-02-\346\225\260\346\215\256\344\272\247\345\223\201\357\274\232\346\225\260\346\215\256\344\272\247\345\223\201\347\232\204\345\237\272\346\234\254\346\246\202\345\277\265.md" @@ -0,0 +1,56 @@ +--- +layout: post +title: 数据产品:数据产品的基本概念 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 数据产品 +--- + +# 1. 数据产品定义 + +- 广义上,数据产品指的是:可以降低用户使用数据门槛,提高数据使用效率,发挥数据价值,辅助用户决策/行动的一类产品。 +- 其形态包含平台类型产品、系统功能模块、算法模型系统等。 + +# 2. 数据产品分类 + + +#### 2.1. 内容型数据产品 + +- 指的是以提供内容为主要目标的数据产品。内容型数据产品与业务结合更紧密,主要是研究业务逻辑,分析业务问题,进行诊断,提出解决方案,例如:建设业务的指标体系,各种具体的分析报告等。 + +#### 2.2. 平台型数据产品 + +- 指的是建设的数据产品具备平台属性。 +- 例如:大数据平台、机器学习平台、用户画像平台等。 + +#### 2.3. 工具类数据产品 + +- 旨在帮助业务进行数据统计分析,数据可视化展示上提供工具性的支持,帮助业务快速高质量应用数据,实现业务数据化。这类一般是在公司发展到一定程度后,为了减轻业务人员、分析师或业务数据产品负担,自行开发一整套关于数据分析展示的工具。 + +#### 2.4. 数仓方向的数据产品 + +- 常做的是数据治理相关的工作,有少部分可能要上手做数仓建模或数据开发。与平台方向的数据产品经理之间的区别就是,数仓方向的更关注平台上的数据内容。 + +#### 2.5. 算法类数据产品 + +- 比如个性化推荐、搜索排序、风控模型、用户画像等形式的产品。 + +# 3. 数据产品建设的两个阶段 + +#### 3.1. 辅助决策型数据产品(如 可视化大屏、BI) + +- 通过对数据的挖掘分析和展现,帮助人们进行业务分析、辅助决策的一类数据产品。我们根据辅助决策型数据产品的复杂程度将它分为两种不同的产品形态。 + + - 第一种,静态展示型数据产品。这是一类简单的数据产品,它展现给用户的是一种静态图表,没有深入的动态交互功能,如图表PPT、信息图、数据可视化大屏等。这类数据产品的受众面很广,受众基本上无须具备太多的数据分析知识,通过简单的数据呈现就可以理解数据产品想要传达的信息。 + + - 第二种,动态交互型数据产品。这是一类专业综合型的数据产品,如销售报表、财务报表数据决策系统、设备在线状态监测系统、城市交通监控调度系统等。这类数据产品的受众是普通业务人员和企业管理者,受众需要具备一定的业务知识才能很好地理解这些数据产品想要传达的信息。 + +#### 3.2. 智能决策型数据产品(如推荐系统、风控系统等) + +- 根据数据分析结果自动执行决策和行动的一类数据产品。这类数据产品往往不是将数据分析的结果直接呈现给用户,而是根据数据分析的结果进行决策并行动,其反馈用户的是行动后的结果,而不是数据分析结果。 +- 辅助决策型数据产品通过可视化界面提供数据分析结果,而智能决策型数据产品的数据分析结果一般是看不到的,用户只能看到最终行动后的结果,普通用户并不能直观感受到这类数据产品的存在。 + diff --git "a/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\345\237\272\344\272\216\346\240\207\347\255\276\347\232\204\347\224\250\346\210\267\345\210\206\347\276\244\347\244\272\344\276\213.md" "b/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\345\237\272\344\272\216\346\240\207\347\255\276\347\232\204\347\224\250\346\210\267\345\210\206\347\276\244\347\244\272\344\276\213.md" new file mode 100644 index 00000000000..0fe675305a2 --- /dev/null +++ "b/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\345\237\272\344\272\216\346\240\207\347\255\276\347\232\204\347\224\250\346\210\267\345\210\206\347\276\244\347\244\272\344\276\213.md" @@ -0,0 +1,192 @@ +--- +layout: post +title: 用户运营:基于标签的用户分群示例 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 用户运营 +--- + +# 1. 分析思路 + +- 在做用户行为分析时,必须要又有个清醒的认识,就是分析的维度越多、信息的粒度越细,越能真实地反映问题。 +- 如果仅仅是按照常规路径,很宏观的做一个笼统的分析,那并不能真实的了解用户的所思所想,也无法有效支撑业务需求。 +- 分析场景的颗粒度 + - 根据实际场景灵活定义用户的起始行为、目标用户群体,细化的颗粒度对进一步的分析可提供更具价值的数据; +- 数据的对比维度 + - 针对核心指标增加对比维度,多维度的数据比较可帮助用户综合对比,更容易分析问题所在,提升分析效率。 + +# 2. 按新老用户分组 + + - 新老用户同期群分析方法,是用户分析最普遍的方法。 + - 比如:用每周的新用户,观察相同时间间隔后的表现。 + - 例如: + - 下图中,2019/1/1的新用户在第一周的留存率是49%,但2019/2/5的新用户在第一周的留存率是却只有40%,这就说明新用户的留存率在下降,需要重点关注。并且可以对比后续每周的表现,看是否好转。 + + ![]({{site.baseurl}}/img-post/用户行为-8.png) + + ![]({{site.baseurl}}/img-post/用户行为-7.png) + + - 为什么要区分新老用户呢?因为新老用户对于产品的反应是有很大差别的,一定要区分来看。比如你第一次去京东,由于不熟悉这家电商,很有可能逛逛就走了;但如果你是一个京东的老用户,登录京东后就很可能产生购物行为。通过区分新老,能够清晰的看到这两种用户的表现,便于发现到底是哪种用户发生了问题。 + + - 如果是新用户的留存下降,很可能是新用户没有快速的感受到产品的核心价值。比如物流,用户的主要诉求就是快,那么对于新用户是否能让他感受到这个价值。如果是老用户的留存率下降,也许是产品的体验在变差,或者受其他竞品的影响。 + +# 3. 按产品功能分组 + + - 一个产品一般具有很多功能,通过分析了解各个功能的价值,找到各个功能的提升空间,进而通过功能优化来整体提升用户留存。 + - 以下图为例,矩阵的横轴是功能的留存率,表示当前功能的用户黏性;纵轴是活跃用户的数量。做出这样一个矩阵后,我们就可以看到不同的功能在矩阵中的位置分布。 + ![]({{site.baseurl}}/img-post/用户行为-9.png) + - 比如橘色代表的功能就是产品的核心功能,使用率和留存率都很高,我们要保证核心功能的体验越来越好,并持续监控使用情况,防止意外发生; + - 比如绿色代表的功能,这个功能虽然使用的人数不多,但留存率非常高,说明这个功能的体验很好,我们要尽量引导用户使用这个功能; + - 而对于红色代表的功能,虽然使用的用户很多,但留存率不高。也许是这个功能有用,但体验不好;也许是这个功能本身就是鸡肋;所以我们要继续深入分析,来决定是优化功能还是直接下线 + +# 4. 按渠道分组 + +- 企业经常采取多种渠道来获客。有线上的方式,比如百度搜索或者抖音短视频等;有新媒体的方式,比如公众号,知乎等;有线下的方式,比如线下沙龙和公众活动。 +- 各种渠道的获客都需要成本的,我们需要知道是哪种渠道的新用户留存高,留存率高说明这是高价值渠道,我们可以在这里做更多的投入。 + +- 比如下图,可以明显观察到,渠道一用户的留存率明显高于渠道二和渠道三,说明渠道一的用户和产品的契合度更高,为高质量渠道,应该在这里加大投入。 + + ![]({{site.baseurl}}/img-post/用户行为-10.png) + + +# 5. 按兴趣标签分组 + +- 电商用户兴趣标签示例: + - 户外露营 + - 数码极客 + - 动漫手办 + - 航模 + - 汽车发烧友 + - 猫奴 + - ... +- LBS 社交软件兴趣标签示例: + - 摄影 & 美图 + - 健身达人 + - 约炮 + - 线下游戏 + - 熬夜冠军 + - 旅游达人 + - 户外活动 + - JK / 汉服 + +# 6. 按地域分组 + +- 分组方法 + - 按一二三线城市分组 + - 按东中西南北区域分组 + - 按城市分组 + - 按省份分组 + +# 7. 按购买风格分组 + +- 购买风格 + - 搜索购买型 + - 浏览购买型 + - 促销购买型 +- 购买业务偏好 + - 闪购 + - Category页面 + - 新品 + - 包邮 + - best deals + - 促销 + - 搜索 + - ... + +# 8. 按营销方式敏感度分组 + +- 首单营销方式 + - 首单正常购买 + - 首单免费礼物 + - 首单新人价商品 + - 首单优惠券 + - 首单新人专享优惠商品组 + - 首单红包 +- 营销方式敏感度 + - 满减 + - 满返 + - 满赠 + - 拼团 + - 多件多折 + - 免费礼物 + - 现金券 + +# 9. 按广告偏好分组 + +- 站内广告形式偏好 + - 悬浮 + - 弹窗 + - 首页轮播 + - 站内信 + - ... +- 广告内容偏好 + - 折扣 + - 品类 + - 新品 + - 热销 + +# 10. 按流失风险等级分组 + +- 等级分类: + - 高风险 + - 中风险 + - 低风险 +- 计算指标: + - 访问频次 + - 最近一次登录时间 + - 7 / 15 / 30 / 60 / 180 天连续登陆测试 + - 注册时间 + - 在线时长 + - 客诉差评 + - 退货 + - 社交频次 + - 分享行为 + - + +# 11. 按退货频率分组 + +- 问题类型 + - 退货率 + - 赔付率 + - 退换货 / 货品异常 +- 用户诚信度 + - 高诚信 + - 无异常 + - 低诚信 + +# 12. 按投诉频率分组 + +- 问题类型 + - 退货率 + - 赔付率 + - 退换货 / 货品异常 + - 多账户使用同样的收获地址 + +# 13. 按会员身份分组 + +- 用户身份 + - 超级用户 + - 金牌用户 + - 银牌用户 + - 铜牌用户 + - 正式会员 + - 试用会员 + - 赠送会员 + +# 14. 按活跃状态分组 + +- 最近一次活跃距离今天天数 + - 7 天内 + - 7 ~ 30 天 + - 30 ~ 180 天 + - 180 天以上 +- 活跃状态 + - 高活跃用户 + - 中活跃用户 + - 低活跃用户 + - 流失用户 + diff --git "a/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\345\270\270\350\247\201\347\232\204\347\224\250\346\210\267\350\241\214\344\270\272\345\210\206\346\236\220\346\250\241\345\236\213.md" "b/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\345\270\270\350\247\201\347\232\204\347\224\250\346\210\267\350\241\214\344\270\272\345\210\206\346\236\220\346\250\241\345\236\213.md" new file mode 100644 index 00000000000..04523b67ab1 --- /dev/null +++ "b/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\345\270\270\350\247\201\347\232\204\347\224\250\346\210\267\350\241\214\344\270\272\345\210\206\346\236\220\346\250\241\345\236\213.md" @@ -0,0 +1,216 @@ +--- +layout: post +title: 用户运营:常见的用户行为分析模型 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 用户运营 +--- + + +# 1. 行为事件分析 + +- 行为事件分析: + - 行为事件分析法主要用于研究某行为事件的发生对产品的影响及影响程度,一般来说,事件通过埋点来获取。 + - 对于一具体的行为,首先要对其进行定义,将人物(Who)、时间(When)、地点(Where)、交互(How)、交互内容(What)进行聚合,构成一个完整的用户行为事件。 + - Who:事件的参与主体,如用户id,设备id等; + - When:事件发生的时间; + - Where:事件发生的地点,如通过ip地址解析,GPS获取; + - How:用户从事行为的方式,如使用的设备、app版本、渠道等; + - What:用户在事件中所做行为的具体内容,如对于购买行为事件,可能包含购买商品名称、类型、数量、金额、付款方式等。 +- 作用: + - 定义完成后,需要进行多维度的下钻分析,进行细分,确认导致该行为的原因,针对存在的现象,找出产生这一现象的行为。 + - 如登录页面下,点击登录和跳过登录的新用户有什么行为差别。通过对用户行为事件的定义,然后进行多维度(如位置、事件、app版本等)拆分,找到原因。 + +# 2. 路径分析 + +![]({{site.baseurl}}/img-post/用户行为-1.png) + +- 用户行为路径分析作用: + - 明确用户现存路径有哪些,发现路径问题; + - 优化用户行为沿着最优访问路径前进; + - 结合业务场景需求进行前端布局调整。 +- 应用场景: + - 确定产品用户从访问到转化/流失都经过了哪些流程; + - 分析转化用户与流失用户是否有行为区别; + - 分析用户行为路径是否符合预期。 +- 涉及的数据指标: + - 全链路页面级PV、UV,以及路径流转关系 + + +# 3. 漏斗分析 + +- 漏斗分析作用: + - 描述从一个事件环节的最开始、到最终转化成购买的整个流程中,相邻环节的转化率表现力。 + - 就是指用数据指标来量化每一个步骤的表现。 + >流量漏斗模型在产品中的经典运用是 AARRR 模型,不过现在已经流行更新的 RARRA 模型。 + +- 应用场景: + - 衡量每一个转化步骤的转化率; + - 通过异常数据指标找出有问题的环节并解决,最终提升整体购买转化率。 + +- AARRR 模型: + + ![]({{site.baseurl}}/img-post/用户行为-2.png) + +- 漏斗分析核心指标 + - 注册 + - 登录 + - 收藏 + - 加购 + - 下单 + - 复购 + +- 登录 + - 着陆页停留时长 +- 浏览 + - 详情页停留时长 + - 搜索页至详情页转化率 + - 详情页至收藏页转化率 + - 详情页至加购页转化率 + - 详情页至下单页转化率 +- 加购 + - 加购量 + - 加购率 +- 收藏 + - 收藏量 + - 收藏率 +- 下单 + - 下单页停留时长 + - 下单页至支付页转化率 +- 支付 + - 支付页停留时长 + - 销售金额 + - 活动销售金额 + - 日常销售金额 + - 广告销售金额 + - 销售量 + - 活动销售量 + - 日常销售量 + - 广告销售量 + +- 订单数 + - 7天订单数 + - 30天订单数 +- 下单人数 + +- 流量转化标签 + - 高流量高转化 + - 高流量低转化 + - 低流量高转化 + - 低流量低转化 + +- 单日数据: + - 单日下单笔数 + - 单日下单金额 + - 单日下单用户数 + - 单日支付笔数 + - 单日支付金额 + - 单日支付人数 + - 单日支付商品数 + - 单日支付平均时长 +- 7 日数据: + - 7日登陆天数 + - 7日支付次数 + - 7日支付金额 + - 7日下单次数 + - 7日下单金额 + - 7日被加入购物车次数 + - 7日被加入购物车件数 +- 30 日数据: + - 30 日登陆天数 + - 30日支付次数 + - 30日支付金额 + - 30日下单次数 + - 30日下单金额 + - 30日被加入购物车次数 + - 30日被加入购物车件数 +- 累计数据: + - 累积登录天 + - 累计下单次数 + - 累计下单金额 + - 累计支付次数 + - 累计支付金额 + - 累计退款次数 + - 累计退款件数 + - 累计退款金额 + +# 4. 归因分析 + +- 归因模型与漏斗模型相关联,它其实是一种既定的规则,我们需要根据产品的实际需求,将达成目标(形成转化)之前的功劳根据设定的权重分配给每一个转化节点。 +- 产品形成一次转化,用户可能要经历很多个转化节点(这里的转化并不一定指用户走完设计的全部流程。一次注册也可以看作一次转化,一次访问也可以看作一次转化,要根据业务实际需求制定)。 +- 归因模型在使用过程中通常分为几类: + - 最终互动模型 + - 首次互动模型 + - 线性归因模型 + - 时间衰减归因模型。 +- 归因模型的作用: + - 归因模型的意义在于寻找到真正对于现阶段产品发展有利的方面,并将优势扩大化。同时,它是具有时效性的,产品的不同阶段归因模型所得到的结果很可能是不一样的。 + + ![]({{site.baseurl}}/img-post/用户行为-3.png) + +- 归因分析应用示例 + - 场景:电商业务下站内归因 + - 在电商案例中,我们对某时间段内成交订单进行归因分析; + - 此处我们选用的归因计算方式是“末次归因”。 + - 归因窗口期设为 1 天,即观察用户在发生订单行为之前的 24 时之内点击了哪些坑位。 + - 核心是找到离“提交订单”最近的一个坑位点击行为。 + - 分析过程: + + ![]({{site.baseurl}}/img-post/用户行为-4.png) + + - 如上图,APP 内多个坑位中,首页精选推荐,商详页相关推荐,首页 Banner 以及首页运营位对于成单的 贡献分别占据了 37.5%、20.83%、20.83%、12.5%。 + - 搜索和购物车下方的相关推荐仅带来不足 10% 的成单贡献。 + - 分析结果: + - 最终的贡献度反映了不同坑位对最终成单转化的贡献及互相之间的差异。 + - 对比不同坑位的有效转化点击率,可得知不同坑位对用户的吸引程度。 + +# 5. 点击热力图 + +- 点击分析: + - 点击热力图是用不同颜色的高亮来代表点击的频率。 + - 点击分析被应用于显示页面或页面组区域中不同元素的点击密度示。包括:元素被点击的次数、占比、发生点击的用户列表、按钮的当前与历史内容等因素。 + - 点击分析具有分析过程高效、灵活、易用,效果直观的特点。 +- 作用: + - 点击分析采用可视化的设计思想与架构,直观地呈现访客热衷的区域,在精细化运营上起到了重要的作用。 + - 它可以精准地评估用户与产品交互背后的深层次的关系; + - 实现网页内跳转点击分析,对网页深层次的点击进行抽丝剥茧般的分析; + - 还可以与其他分析模型配合,以更加全面的视角来探索数据的价值,切实感知用户体验,完成运营的科学决策。 +- 网页热力图示例: + + ![]({{site.baseurl}}/img-post/数据埋点-1.png) + +# 6. 用户分群 + +- 用户分群: + - 用户分群数据分析方法,是进行用户画像的关键数据分析模型,这是企业进行数据分析、精细化运营的第一步。 + - 用户分群即用户信息标签化,通过用户的历史行为路径、行为特征、偏好等属性,将具有相同属性的用户划分为一个群体,并进行后续分析。 + - 一般来说,用户分群可以分为普通分群和预测分群,普通分群是依据用户的属性特征和行为特征将用户群体进行分类;预测分群是根据用户以往的行为属性特征,运用机器学习算法来预测他们将来会发生某些事件的概率。 +- 作用: + - 与漏斗模型关注阶段差异相比,用户分群更关注群体差异。 + - 通过漏斗分析模型,可以看到用户在不同阶段所表现出的行为是不同的。 + - 然而,由于群体特征不同,在相同阶段不同群体的行为会有很大差别,因此运营人员或者产品人员希望可以根据历史数据将用户进行划分,将具有一定规律特性的用户群体进行聚类,进而再次观察该群体的具体行为,形成更加清晰的用户画像。 + - 通过用户分群,企业可以打破数据孤岛并真实地了解客户,定位营销目标群体,帮助企业实现精准、高效地营销。 + + ![]({{site.baseurl}}/img-post/用户行为-5.png) + +# 7. 留存分析 + +- 留存分析: + - 留存分析是一种用来分析用户参与情况/活跃程度的分析模型,考察进行初始行为的用户中,有多少人会进行后续行为。这是用来衡量产品对用户价值高低的重要方法。 + - 留存用户和留存率体现了应用的质量和保留用户的能力,还可以从宏观上把握用户生命周期长度以及定位产品可改善至之处。 + - 目前市面上存在很多专业的数据机构在提供数据统计服务,国外比较流行的是Flurry,Google Analytics,国内比较有名的是友盟、TalkingData以及无需埋点即可实现数据统计分析的GrowingIO。 + - 用户留存分析一般需要先按照不同的维度进行用户分组(时间/渠道/行为等),然后对分组进行数据的比较分析,找到数据变化的原因。 +- 用户留存示例: + ![]({{site.baseurl}}/img-post/用户行为-6.png) + +# 8. 流失预警 + +- 在用户生命周期中,用户进入成熟期之后,并不代表就运营人员可以高枕无忧,这一阶段的用户随时会滑向休眠阶段; +- 一旦用户进入休眠阶段、则用户随时流失,必须要尽快识别出休眠用户,并针对性调整精细化运营策略、唤醒用户,才能阻止用户流失; +- 新用户获取的成本高昂,用户流失后的召回成本也不低,而且效果很不理想; +- 相比而言,维系老用户的边际成本几乎可以忽略不计,及时发现用户流失倾向、对于产品运营者而言,所付出的成本和收益相比微乎其微; +- 用户流失预警模型,对于延长用户生命周期至关重要。 diff --git "a/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\346\265\201\345\244\261\347\224\250\346\210\267\345\217\254\345\233\236\347\255\226\347\225\245.md" "b/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\346\265\201\345\244\261\347\224\250\346\210\267\345\217\254\345\233\236\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..9ff84f855b4 --- /dev/null +++ "b/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\346\265\201\345\244\261\347\224\250\346\210\267\345\217\254\345\233\236\347\255\226\347\225\245.md" @@ -0,0 +1,85 @@ +--- +layout: post +title: 用户运营:流失用户召回策略 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 用户运营 +--- + +# 1. 用户流失的定义 + +- 不同产品、不同场景,流失的行为定义均是不同的,关键在于做预警的目的是为了什么。 +- 对于电商型产品而言,购买可作为关键行为,用户一段时间没有下单,认为此用户已经流失; +- 对于内容/功能型产品而言,登录可作为关键行为,用户一段时间没有登录,认为此用户已经流失。 + + +# 2. 建立用户流失预警 + +- 关于本节,可参考文章用户运营:用户流失预警。 + +# 3. 进行流失用户画像 + +- 关于本节,可参考文章用户画像:常见的用户画像标签体系,以及。 +- 需要注意的是,我们需要针对流失用户建立针对性的流失用户画像。 + +# 4. 用户流失的归因分析 + +- 关于本节,可参考文章用户行为:用户分群的常见维度,结合前面的用户画像得出的用户标签; +>结合不同维度的人群属性,分析流失用户的共性特征,在这个过程中极有可能发现产品存在的深层次问题。 + +# 5. 流失用户召回策略 +- 指定召回策略前需要注意; + - 当用户对产品产生不满而离开,此时通过短信、Push等手段对用户进行召回,用户是非常抵触的。 + - 用户在离开的时候,很可能已经将APP卸载,部分召回手段是无法触达到用户的。 + +- 召回方式 + - 短信 + - 可批量发放;但易被当成垃圾短信,并引发投诉;适用于普通用户。 + - 邮件 + - 低成本,可大量发送,但点击率低;使用于普通用户。 + - push + - 效果较好,但取决于用户安装中是否选择允许推送; + - 微信通知 + - 即微信服务号的用户通知模板,效果取决于用户是否关注相关微信号; + - 电话回访 + - 成本高,无法批量操作,主要适用于VIP用户。 + - 礼物召回 + - 包括赠送纪念品、周边、伴手礼等,适用于种子用户。 + - 福利召回 + - 含优惠券,现金红包、体验金等福利。 + - 活动召回 + - 取决于活动类型和宣传渠道。 + + - 召回方式都有各自的优缺点和适用场景,运营需要结合产品业务类型、现有资源情况、以及流失用户特征等因素来分析,选择与用户属性最匹配的召回方式。 + +- 召回文案 + + - 场景打造+情感共鸣+福利刺激,给予用户实实在在的共鸣和利益; + - 如包含用户进入链接(重新登录或下载APP),需要打磨优化跳转链接(中间页); + - 注意,在批量发放消息前记得测试连接是否能顺利打开。 + +- 召回路径 + - 在正式行动前先模拟用户召回路径是否通畅,确保用户体验是否良好。 + ![sql-17]({{site.baseurl}}/img-post/用户召回-1.png) + + +# 6. 效果评估 & A/B TEST + +- 一般认识上,发送短信后 24小时 内,重新登录的流失用户即为成功召回用户; +- 超过 24 小时仍未回归的用户,基本上就不会再回归,即可视作该用户召回失败; + +- 户召回的效果分析思路: + - 各渠道召回数据 + - 包含各渠道信息发放数、信息点击率、用户召回数、用户召回比例。 + - 各渠道召回成本 + - 各渠道召回用户的单个成本投入和总成本投入。 + - 总回归登录用户数 + - 总回归用户数与各渠道召回用户总数是否吻合,有多少用户不通过链接直接登录APP的。 + - 用户回归登录后的行为数据 + - 监测用户回归后有的留存、活跃情况,防止再次流失。 + +- 召回效果分析,需要结合 A/B实验 等手段来不断优化 ROI。 \ No newline at end of file diff --git "a/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\347\224\250\346\210\267\346\265\201\345\244\261\351\242\204\350\255\246.md" "b/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\347\224\250\346\210\267\346\265\201\345\244\261\351\242\204\350\255\246.md" new file mode 100644 index 00000000000..d02039c6ae2 --- /dev/null +++ "b/_posts/2022-01-02-\347\224\250\346\210\267\350\277\220\350\220\245\357\274\232\347\224\250\346\210\267\346\265\201\345\244\261\351\242\204\350\255\246.md" @@ -0,0 +1,225 @@ +--- +layout: post +title: 用户运营:用户流失预警 +subtitle: +date: 2022-01-02 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 用户运营 +--- + + +# 1. 用户流失预警 + +#### 1.1. 用户生命周期 + +- 在用户生命周期中,用户进入成熟期之后,并不代表就运营人员可以高枕无忧,这一阶段的用户随时会滑向休眠阶段; +- 一旦用户进入休眠阶段、则用户随时流失,必须要尽快识别出休眠用户,并针对性调整精细化运营策略、唤醒用户,才能阻止用户流失; +- 新用户获取的成本高昂,用户流失后的召回成本也不低,而且效果很不理想; +- 相比而言,维系老用户的边际成本几乎可以忽略不计,及时发现用户流失倾向、对于产品运营者而言,所付出的成本和收益相比微乎其微; +- 用户流失预警模型,对于延长用户生命周期至关重要。 + +![sql-17]({{site.baseurl}}/img-post/用户流失预警-1.png) + +#### 1.2. 用户流失预警数据 + +- 总的来说,流失预警模型需要以下三类数据; + - 用户基础数据 + - 用户行为数据 + - 用户消费数据 + +# 2. 用户基础数据 + +- 性别 +- 省份 +- 年龄/分层 +- 电话号码所在区域/分层 +- 是否临时账户 +- 注册时间 +- 新老用户标识 +- 教育程度 +- 职业类型 +- 收入水平 +- 星座 +- 婚姻状况 +- 生育状态 +- 是否有老人 +- 是否有小孩 +- 是否二胎 +- 手机品牌 +- 汽车资产 +- 会员身份 + +# 3. 用户行为数据 +- 注册状态 +- 活跃度 + - 启动次数 + - 启动间隔时间 + - 在线时长 +- 安装距今天数 +- 重复咨询 +- 退货频率 +- 投诉频率 +- 订单评价 +- 邀请注册量 +- 邀请新客量 +- 首单营销方式 +- 近30天行为 + - 近30天购买次数(含退拒) + - 近30天购买金额(含退拒) + - 近30天购物车次数 + - 近30天购物车放弃数 + - 近30天购物车提交商品数 + - 近30天客单价 + - 近30天活跃天数 +- 近7天行为 + - 近7天购买次数(含退拒) + - 近7天购买金额(含退拒) + - 近7天客单价 + - 近7天购物车次数 + - 近7天购物车放弃数 + - 近7天购物车提交商品数 + - 近7天活跃天数 +- 单笔订单最小金额 + - 单笔订单最小金额 + - 单笔订单最大金额 +- 首单距今时间 +- 尾单距今时间 +- 购买阶段近5日 + - 加购未下单 + - 下单未支付 + - 浏览未购买 + - 未付款成功 +- 最近加购距今天数 +- 近7天行为 + - 最近7天浏览次数最多的未购买的类目(三级类目) + - ... +- 近3天行为 + - 最近3天浏览次数最多的未购买的类目(三级类目) + - 最近3天搜索未购买的类目(三级类目 + - ... +- 近1天行为 + - 最近1天订单情况(24小时后) + - ... +- 最近行为 + - 最近一次搜索未购买的类目(三级类目) + - 最近一次收藏商品类目(三级类目) + - 最近一次访问app日期 + - 上一次支付成功距今天数 + - ... +- 历史行为 + - 历史收藏商品中数量最多的类目(三级类目) + - ... +- 用户偏好 + - 购买单价偏好 + - ... +- 活跃地 + - 最近7日常登陆地 + +## 4. 用户消费数据 +- 购买活跃度 +- 平均客单价 +- 购买价格段偏好 +- 一次购买后流失用户 +- 最近一次活跃距离今天天数 +- 最近一次购买距离今天天数 +- 购买风格 + - 搜索购买型 + - 浏览购买型 + - 促销购买型 +- 历史购买状态 +- 账户优惠券 +- 消费状态 + - 购买次数 + - 平均购买金额 + - 最近一次购买间隔 + + + +# 5. RFM 模型(电商类应用) + +- 最近一次消费 (Recency) + - 理论上,上一次消费时间越近的顾客应该是比较好的顾客,对提供即时的商品或是服务也最有可能会有反应。 + - 示例: + ![]({{site.baseurl}}/img-post/标签体系-1.png) + +- 消费频率 (Frequency) + - 最常购买的消费者,忠诚度也就最高。 + - 示例: + ![]({{site.baseurl}}/img-post/标签体系-3.png) + +- 消费金额 (Monetary) + - 消费金额越高,客户价值越高。 + - 示例: + ![]({{site.baseurl}}/img-post/标签体系-2.png) + +- 口径规则 + - >注意:理论上M值和F值是一样的,都带有时间范围。 + - 示例: + - ![]({{site.baseurl}}/img-post/标签体系-4.png) + +- 维度交叉分析 + - 八类人群 + - 重要价值客户(111) + - RFM都很高,提供VIP服务。 + - 重要保持客户(011) + - 最容易转化成第一类客户的群体,一定要想办法提高他们的消费频率。 + - 重要发展客户(101) + - 主动保持联系,提高复购。 + - 重要挽留客户(001) + - 客户消费频率低和最近消费时间间隔比较远,但是消费金额高,这种用户即将流失,要主动联系用户,调查问题出在哪里,想办法挽回。 + - 一般价值客户(111) + - 一般保持客户(011) + - 一般发展客户(101) + - 一般挽留客户(001) + - ![]({{site.baseurl}}/img-post/标签体系-6.png) + - 示例: + ![]({{site.baseurl}}/img-post/标签体系-5.png) + +# 6. 流失预警模型 + +- 流失预警模型,提取历史数据后,通过观察特定窗口期的各种相关数据,评估用户在表现窗口内流失可能性,从而预测当前用户在未来的表现。 + +![sql-17]({{site.baseurl}}/img-post/用户流失预警-2.png) + +- 特征工程指标: + - 用户的基本属性: + - 性别,年龄,收入水平,区域等; + - 不同类型的用户可能流失也有所区别 + - 用户的产品行为: + - 所处产品的生命周期,活跃的频次,关键功能的使用频次等; + - 基础指标一般是流失原因的表象,和流失具有相关性,但不具备因果性,不是导致流失的关键特征 + - 其他加工指标: + - 基础指标可能不能很好的挖掘到影响留存的关键特征,需要基于业务理解加工出新的指标,和基础指标一起作为模型训练的特征。 + +- 常见的加工指标方法有: + + - 深度指标:反应用户使用深度的指标,用户不仅要用,而且要用的比较深入,比如关键功能的使用次数。 + + - 频次指标:用户不仅要用的深,还要用的频繁,这个频繁的定义依据不同的产品类型而有不同的定义,有的产品可能需要每天都要用,甚至一天要用几次,有的可能要求一周要用几次,不一而足。但是可以根据产品的特点加工出一个频次指标,比如日/周均使用次数或者日/周均使用天数,这样用户的使用频次得以表征。 + + - 趋势指标:用户使用产品的趋势变化,用户使用的趋势直接关系着用户的流失,如果一个用户使用的越来越少了,那大概率用户是要流失了,所以一些常见的趋势指标如近三个月每周平均活跃天数的变化率,可以理解为一个斜率,如果每周的平均活跃天数在一直减少,斜率应该是负值,否则斜率应该是正值,以此表征用户使用情况的变化趋势。 + +- 常用的预警算法包含决策树、随机森林、逻辑回归等。 + + +- 流失预警模型通常从准确率、命中率、覆盖率三个维度去评估。 + + - 准确率=(正确预测为流失的用户数+正确预测为不流失的用户数)/总用户数 + + - 命中率=正确预测为流失的用户数 / 预测为流失的用户数 + + - 覆盖率=正确预测为流失的用户数 / 实际为流失的用户数 + + +# 8. 风险用户促活 + +- 发送优惠券及优惠金额调整; +- 增加 app 内的用户引导; +- 场景化提醒文案; +- 优化关联推荐; +- 个性化push文案; +- 个性化短信; +- 等。 diff --git "a/_posts/2022-01-24-SQL\357\274\232SQL \344\274\230\345\214\226\347\273\217\351\252\214\346\200\273\347\273\223.md" "b/_posts/2022-01-24-SQL\357\274\232SQL \344\274\230\345\214\226\347\273\217\351\252\214\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..5287e94fcde --- /dev/null +++ "b/_posts/2022-01-24-SQL\357\274\232SQL \344\274\230\345\214\226\347\273\217\351\252\214\346\200\273\347\273\223.md" @@ -0,0 +1,125 @@ +--- +layout: post +title: SQL:SQL 优化经验总结 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + + +# SQL 优化经验 + + +#### 在表中建立索引,优先考虑 where、group by 使用到的字段。 + + +#### 尽量避免使用 select *,返回无用的字段会降低查询效率。 + +- 优化方式: + - 使用具体的字段代替 *,只返回使用到的字段。 + +#### 尽量避免使用 in 和 not in,会导致数据库引擎放弃索引进行全表扫描。 + +- 如下: + - `SELECT * FROM t WHERE id IN (2,3)` + - `SELECT * FROM t1 WHERE username IN (SELECT username FROM t2)` + +- 优化方式: + - 如果是连续数值,可以用 `between代替`。 + - 如下: + - `SELECT * FROM t WHERE id BETWEEN 2 AND 3` + - 如果是子查询,可以用 `exists代替`。 + - 如下: + - `SELECT * FROM t1 WHERE EXISTS (SELECT * FROM t2 WHERE t1.username = t2.username)` + +#### 尽量避免使用 or,会导致数据库引擎放弃索引进行全表扫描。 + +- 如下: + -`SELECT * FROM table t WHERE t.a = 1 OR t.b = 3` +- 优化方式: + - 可以用 union 代替 or。 + - 如下: + - `SELECT * FROM table WHERE a = 1 + UNION + SELECT * FROM table WHERE b = 3` + - 原理:使用 union 扫描的是索引,or 扫描的是全表。 + +#### 尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描。 + +- 如下: + - `SELECT * FROM t WHERE username LIKE '%li%'` +- 优化方式: + - 尽量在字段后面使用模糊查询。 + - 如下: + - `SELECT * FROM t WHERE username LIKE 'li%'` + +#### 尽量避免进行 null 值的判断,会导致数据库引擎放弃索引进行全表扫描。 + +- 如下: + - `SELECT * FROM t WHERE score IS NULL` +- 优化方式: + - 可以给字段添加默认值0,对0值进行判断。 + - 如下: + - `SELECT * FROM t WHERE score = 0` + +#### 尽量避免在 where 条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描。 + +- 如下: + - `SELECT * FROM t2 WHERE score/10 = 9 + - `SELECT * FROM t2 WHERE SUBSTR(username,1,2) = 'li'` +- 优化方式: + - 可以将表达式、函数操作移动到等号右侧。 + - 如下: + - `SELECT * FROM t2 WHERE score = 10*9` + - `SELECT * FROM t2 WHERE username LIKE 'li%'` + +#### 当数据量大时,避免使用 where 1=1 的条件。 + +- 通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描。 +- 如下: + - `SELECT * FROM t WHERE 1=1` +- 优化方式: + - 用代码拼装 sql 时进行判断,没 where 加 where,有 where 加 and。 + +#### 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。 + +#### 尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。 + +#### 一般情况下不鼓励在 where 子句使用 like 操作。 + +#### % 在前的模糊查询 将导致全表扫描。 +-例如: + `select id from t where name like '%abc%'` + +#### inner join 资源消耗小于 left join。 + +- 如果连接方式是 inner join,在没有其他过滤条件的情况下 MySQL 会自动选择小表作为驱动表,但是 left join 在驱动表的选择上遵循的是左边驱动右边的原则,即 left join 左边的表名为驱动表。 + +#### count(*) 和 count(1)和count(列名)区别 执行效果上: + +- count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为 NULL; +- count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为 NULL +- count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。 + +#### 指定查询的索引 + +- 当 sql 查询的字段有多个索引的时候,mysql 优化器会自动选择一个索引进行查询,我们也可以通过 sql 字段进行自定义, + +#### use index(索引): 推荐使用指定的索引(最终用不用该索引,还需要 mysql 自己判断) + +- 示例: + - `select * from use index(索引A)` + +#### ignore index(索引) : 忽略掉这个索引 + +- 示例: + - `select * from ignore index(索引A)` + +#### force index(索引): 强制使用该索引 + +- 示例: + - `select * from force index(索引A)` diff --git "a/_posts/2022-01-24-SQL\357\274\232SQL \346\211\247\350\241\214\351\241\272\345\272\217\345\217\212\345\270\270\347\224\250\345\207\275\346\225\260.md" "b/_posts/2022-01-24-SQL\357\274\232SQL \346\211\247\350\241\214\351\241\272\345\272\217\345\217\212\345\270\270\347\224\250\345\207\275\346\225\260.md" new file mode 100644 index 00000000000..f6b1b4bb496 --- /dev/null +++ "b/_posts/2022-01-24-SQL\357\274\232SQL \346\211\247\350\241\214\351\241\272\345\272\217\345\217\212\345\270\270\347\224\250\345\207\275\346\225\260.md" @@ -0,0 +1,396 @@ +--- +layout: post +title: SQL:SQL 执行顺序及常用函数 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + + +# SELECT 语句执行顺序 + +- SELECT +- DISTINCT + - 数据除重 +- FROM + - <表名> # 选取表,将多个表数据通过笛卡尔积变成一个表。 +- ON + - <筛选条件> # 对笛卡尔积的虚表进行筛选 +- JOIN + - # 指定join,用于添加数据到on之后的虚表中,例如left join会将左表的剩余数据添加到虚表中 +- WHERE + - # 对上述虚表进行筛选 +- GROUP BY + - <分组条件> # 分组 + # 用于having子句进行判断,在书写上这类聚合函数是写在having判断里面的 +- HAVING + - <分组筛选> # 对分组后的结果进行聚合筛选 +- SELECT + - <返回数据列表> # 返回的单列必须在group by子句中,聚合函数除外 +- ORDER BY + - <排序条件> # 排序 +- LIMIT + - <行数限制> + +# 多表查询 + +#### 表示例 + +![employee]({{site.baseurl}}/img-post/employee.png) + +![department]({{site.baseurl}}/img-post/department.png) + +![salary_grade]({{site.baseurl}}/img-post/salary_grade.png) + +![salary]({{site.baseurl}}/img-post/salary.png) + +![manager]({{site.baseurl}}/img-post/sql-manager.png) + + +#### 等值连接 & 非等值连接 + +```aidl +SELECT + a.employee_id, b.salary_grade +FROM + salary AS a, salary_grade AS b +WHERE + a.salary_amount BETWEEN b.lowest_salary AND b.highest_salary; +``` + +![sql-1]({{site.baseurl}}/img-post/sql-1.png) + + +#### JOIN 使用 + +![sql-5]({{site.baseurl}}/img-post/sql-5.png) + +#### LEFT JOIN + +```aidl +SELECT + a.employee_id,a.employee_name,b.manager_id,c.manager_name +FROM + employee AS a +LEFT JOIN + department AS b +ON + a.department_id=b.department_id +LEFT JOIN + manager AS c +ON + c.manager_id=b.manager_id; +``` + +![sql-3]({{site.baseurl}}/img-post/sql-3.png) + + +#### UNION VS UNION ALL + +![sql-4]({{site.baseurl}}/img-post/sql-4.png) + +- UNION ALL 操作符返回两个查询的结果集的并集时,对于结果集的重复部分 **不去重**。 +- UNION ALL 不需要执行去重操作,执行时所需要的资源比 UNION 少; +- 如果明知道合并后的数据结果不存在重复数据,或者不需要去重,则可以使用 UNION ALL,以提高查询的效率。 +- 使用 UNION ALL 需要注意,是否存在重复数据! + + +# 流程控制函数 + +#### IF + +``` +IF(value,value1,value2) +``` +- 如果 value 值为 True,则返回 value1,否则返回 value2; + +#### IF NULL +``` +IF NULL(value1,value2) +``` +- 如果 value1 为 NULL 则返回 value2,否则返回 value1; + +#### CASE WHEN + +```aidl +CASE +WHEN 条件1 THEN 结果1 +WHEN 条件2 THEN 结果2 +WHEN 条件3 THEN 结果3 +WHEN 条件4 THEN 结果4 +... +ELSE 结果n +END +``` +- 相当于 python 的 `if ... elif ... else ...` + +```aidl +CASE expr +WHEN 常量值1 THEN 值1 +WHEN 常量值2 THEN 值2 +WHEN 常量值3 THEN 值3 +... +ELSE 值n +END +``` +- 相当于 JAVA 的 `swith ... case ...` + +# 聚合函数 + +#### AVG / SUM + + +#### MAX / MIN + + +#### COUNT + +- COUNT 执行时不会计算 NULL; +- InnoDB 引擎下,COUNT(*) = COUNT(1) > COUNT(字段); +- MyISAM 引擎下,COUNT(*) = COUNT(1) = COUNT(字段); + +#### GROUP BY + +- 使用单个列分组: +```aidl +SELECT + dep_id, AVG(salary_amnt) AS dep_avg_sal_amnt +FROM + ( + SELECT + a.employee_id AS emp_id,a.salary_amount AS salary_amnt,b.department_id AS dep_id + FROM + test.salary AS a + LEFT JOIN + test.employee AS b + ON + a.employee_id=b.employee_id + ) c +GROUP BY + dep_id + ; +``` + +![sql-7]({{site.baseurl}}/img-post/sql-7.png) + +- 使用多个列分组: + +```aidl +SELECT + dep_id, job_id, AVG(salary_amnt) AS dep_avg_sal_amnt +FROM + ( + SELECT + a.employee_id AS emp_id,a.salary_amount AS salary_amnt,b.department_id AS dep_id,b.job_id AS job_id + FROM + test.salary AS a + LEFT JOIN + test.employee AS b + ON + a.employee_id=b.employee_id + ) c +GROUP BY + dep_id,job_id + ; +``` + +![sql-6]({{site.baseurl}}/img-post/sql-6.png) + +#### HAVING + +- 如果过滤条件中使用了聚合函数,则必须使用 HAVING 来替代; +- 如果过滤条件中没有使用聚合函数,则使用 WHERE 和 HAVING 都可以,但一般使用 WHERE,因为 **WHERE 的执行效率更高**; +- HAVING 必行声明在 GROUP BY 后面; +- 如果没有使用 GROUP BY,则没有必要使用 HAVING; + +```aidl +SELECT + job_id, AVG(salary_amnt) AS dep_avg_sal_amnt +FROM + ( + SELECT + a.salary_amount AS salary_amnt,b.department_id AS dep_id,b.job_id AS job_id + FROM + test.salary AS a + LEFT JOIN + test.employee AS b + ON + a.employee_id=b.employee_id + ) c +GROUP BY + job_id +HAVING + AVG(salary_amnt)>7000 + ; +``` + +![sql-8]({{site.baseurl}}/img-post/sql-8.png) + + +# JOIN + +#### 创建表 + +- 需求: + - 需要根据 employee、attendance、calendar 三张表,统计员工的出勤情况; +- 创建日历表 calendar + + ```aidl + CREATE TABLE calendar( + id INTEGER NOT NULL PRIMARY KEY, -- 日历编号 + calendar_date DATE NOT NULL UNIQUE, -- 日历日期 + calendar_year INTEGER NOT NULL, -- 日历年 + calendar_month INTEGER NOT NULL, -- 日历月 + calendar_day INTEGER NOT NULL, -- 日历日 + is_work_day VARCHAR(1) DEFAULT 'Y' NOT NULL -- 是否工作日 + ); + ``` +- 创建考勤记录表 attendance + ``` + CREATE TABLE attendance( + id INTEGER NOT NULL PRIMARY KEY, -- 考勤记录编号 + check_date DATE NOT NULL, -- 考勤日期 + emp_id INTEGER NOT NULL, -- 员工编号 + clock_in TIMESTAMP, -- 上班打卡时间 + clock_out TIMESTAMP, -- 下班打卡时间 + CONSTRAINT uk_attendance UNIQUE (check_date, emp_id) + ); + ``` +- 创建员工表 employee + ``` + CREATE TABLE `employee` ( + `employee_id` int NOT NULL AUTO_INCREMENT, + `employee_name` varchar(45) DEFAULT NULL, + `department_id` int DEFAULT NULL, + `job_id` int DEFAULT NULL, + PRIMARY KEY (`employee_id`), + UNIQUE KEY `employee_id_UNIQUE` (`employee_id`) + ) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + ``` + + +#### 笛卡尔积(交叉连接)CROSS JOIN + +- 交叉连接不带 WHERE 子句,它返回被连接的两个表所有数据行的笛卡尔积; +- CROSS JOIN 返回结果集合中的数据行数,等于第一个表中的数据行数乘以第二个表中的数据行数; + +- 应用实例: + - 如本文所示,在统计之前,需要将每个员工的 calendar 数据先行 JOIN,然后才能对比出勤情况,这里就会用到 CROSS JOIN; + ```aidl + SELECT + c.*, e.* + FROM + calendar c + CROSS JOIN + employee e + LIMIT 1000000; + ``` + ![sql-9]({{site.baseurl}}/img-post/sql-9.png) + + - 这个时候,CROSS JOIN 其实也可以写成如下样式: + ```aidl + SELECT + * + FROM + calendar,employee + LIMIT 1000000; + ``` + ![sql-10]({{site.baseurl}}/img-post/sql-10.png) + +#### 内连接 INNER JOIN ... ON + +- 主要指带 ON 的内连接,内连接按照 ON 条件合并两个表,返回满足条件的记录; +- 应用实例: + - 在本文示例中,如果我们想查看全部员工的考勤记录,可以使用 INNER JOIN ON,得到全部员工的考勤记录; + + ```aidl + SELECT + a.*, e.* + FROM + attendance a + INNER JOIN + employee e + ON + a.emp_id=e.employee_id + LIMIT 1000000; + ``` +- INNER JOIN ... ON vs 等值连接 WHERE + - 使用 where 和 = 将表连接起来的查询,其查询结果中列出被连接表中的所有列; + - 上面的例子,用如下 SQL 也可以获取同样的结果; + + ```aidl + SELECT + a.*, e.* + FROM + attendance a,employee e + WHERE + a.emp_id=e.employee_id + LIMIT 1000000; + ``` +- 注意: + - 从逻辑上来说,等值连接与内连接没什么不同; + - 但实际上等值连接和内连接的执行计划并不相同,**当参与连接的两个表比较大时,使用内连接(inner join)的效率更高**。 + +#### LEFT JOIN ON + +- 左外连接会保留左表的全部记录,相当于在左表的基础上加上右表中满足 ON 条件的数据; +- 剩余的空位以 NULL 填充; + +- 示例: + + ```aidl + SELECT + a.*, e.* + FROM + employee e + LEFT JOIN + attendance a + ON + a.emp_id=e.employee_id + LIMIT 1000000; + ``` + ![sql-11]({{site.baseurl}}/img-post/sql-11.png) + +#### OUTER JOIN + +- 左外连接会保留右表的全部记录,相当于在左表的基础上加上右表中满足 ON 条件的数据; +- 剩余的空位以 NULL 填充; + +- 其情况与 LEFT JOIN 刚好相反,具体不再赘述。 + + +# 常用经验 + +#### 一旦给表起了别名,就必须使用这个别名、不能再使用原名。 + +```aidl +SELECT + a.employee_name +FROM + employee AS a +``` + +#### 从 SQL 优化的角度考虑,多表查询时、每个字段前都应该指明字段所在的表; + +```aidl +SELECT + a.employee_name, + b.department_name +FROM + employee AS a,department AS b +WHERE + a.department_id = b.department_id +``` + +![sql-2]({{site.baseurl}}/img-post/sql-2.png) + + + + + + + diff --git "a/_posts/2022-01-24-SQL\357\274\232SQL \347\252\227\345\217\243\345\207\275\346\225\260 & \350\201\232\345\220\210\345\207\275\346\225\260.md" "b/_posts/2022-01-24-SQL\357\274\232SQL \347\252\227\345\217\243\345\207\275\346\225\260 & \350\201\232\345\220\210\345\207\275\346\225\260.md" new file mode 100644 index 00000000000..bd3f3a5cfed --- /dev/null +++ "b/_posts/2022-01-24-SQL\357\274\232SQL \347\252\227\345\217\243\345\207\275\346\225\260 & \350\201\232\345\220\210\345\207\275\346\225\260.md" @@ -0,0 +1,482 @@ +--- +layout: post +title: SQL:SQL 窗口函数 & 聚合函数 +subtitle: RANK & DENSE RANK & PRECEDING & UNBOUNDED & LAG & LEAD & GROUP_CONCAT +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + + +# 1. 窗口函数 + +- 窗口函数,也叫 OLAP 函数(Online Anallytical Processing,联机分析处理),可以对数据库数据进行实时分析处理。 + +- 基本语法 + ``` + <窗口函数> over (partition by <用于分组的列名> + order by <用于排序的列名>) + ``` + +- 窗口函数分类 + - 专用窗口函数,包括 rank, dense_rank, row_number 等专用窗口函数; + - 聚合函数,如 sum、avg、count、max、min 等; + +- 因为窗口函数是对 where 或者 group by 子句处理后的结果进行操作,所以窗口函数原则上只能写在 select 子句中。 + +# 2. 窗口函数 VS GROUP BY + +- 窗口函数具备了我们之前学过的 group by 子句分组的功能和 order by 子句排序的功能,但是二者具有完全不同的应用场景。 + + +- 窗口函数有以下功能: + - 同时具有分组和排序的功能 + - 不减少原表的行数 + + +# 3. RANK + +- 语法 + + ``` + select *, + rank() over (partition by 分组字段 + order by 排序字段 desc) as ranking + from table + ``` + + - partition by 用来对表分组; + - order by 子句的功能是对分组后的结果进行排序; + +- 示例 + + ``` + select *, + rank() over (partition by t.department_id + order by t.salary_amount desc) as ranking + from ( + SELECT s.employee_id,e.employee_name,e.department_id as department_id,s.salary_amount + FROM test.employee e join test.salary s on s.employee_id=e.employee_id + ) t; + ``` + + ![sql-2]({{site.baseurl}}/img-post/sql-21.png) + + +# 4. DENSE RANK + +- dense_rank,排序时会将相同值的排名排为同一个值; + +- 语法 + + ``` + select *, + dense_rank() over (partition by 分组字段 + order by 排序字段 desc) as ranking + from table + ``` + + - partition by 用来对表分组; + - order by 子句的功能是对分组后的结果进行排序; + +- 示例 + + ``` + select *, + dense_rank() over (partition by t.department_id + order by t.salary_amount desc) as ranking + from ( + SELECT s.employee_id,e.employee_name,e.department_id as department_id,s.salary_amount + FROM test.employee e join test.salary s on s.employee_id=e.employee_id + ) t; + ``` + + ![sql-2]({{site.baseurl}}/img-post/sql-21.png) + +# 窗口聚合函数 + +- 示例 + ``` + select *, + sum(salary_amount) over (partition by t.department_id) as dep_sum_salary, + avg(salary_amount) over (partition by t.department_id) as dep_avg_salary, + count(salary_amount) over (partition by t.department_id) as dep_count, + max(salary_amount) over (partition by t.department_id) as dep_max_salary, + from ( + SELECT s.employee_id,e.employee_name,e.department_id as department_id,s.salary_amount + FROM test.employee e join test.salary s on s.employee_id=e.employee_id + ) t; + ``` + + ![sql-2]({{site.baseurl}}/img-post/sql-23.png) + + +#### TOP N 问题 + +- 示例 + + ``` + select * from ( -- 注意:这里需要使用嵌套才能使用排名字段 + select *, + dense_rank() over (PARTITION by t.department_id order by t.salary_amount desc) as 排名 + from ( + SELECT s.employee_id,e.employee_name,e.department_id as department_id,s.salary_amount + FROM test.employee e join test.salary s on s.employee_id=e.employee_id + ) t + ) t1 + where t1.排名<=10 + ; + ``` + + ![sql-2]({{site.baseurl}}/img-post/sql-24.png) + + +#### 组内比较问题 + +- 示例 + + ``` + select * from ( + select *, + avg(salary_amount) over (partition by t.department_id) as dep_avg_salary + from ( + SELECT s.employee_id,e.employee_name,e.department_id as department_id,s.salary_amount + FROM test.employee e join test.salary s on s.employee_id=e.employee_id + ) t + ) t1 + where t1.salary_amount>=t1.dep_avg_salary + ; + ``` + + ![sql-2]({{site.baseurl}}/img-post/sql-25.png) + + +# PRECEDING + +- 往前 n 行数据 + +#### 最近 N (小时/天/周/月)平均统计问题 + +- 示例: + - 查找不同产品每个月销量、以及截至当前月最近3个月的平均销售额; + + ``` + SELECT m.product,m.ym,m.amount, + AVG(m.amount) OVER( + PARTITION BY m.product + ORDER BY m.ym + ROWS BETWEEN 2 PRECEDING AND CURRENT ROW + ) + FROM sales_monthly m + ORDER BY m.product,m.ym + ``` + + - AVG 函数 OVER 子句中的 PARTITION BY 选项表示按照产品进行分区。 + - ORDER BY选项表示按照月份进行排序; + - ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 表示窗口从当前行的前2行开始,直到当前行结束。 + +# FOLLOWING + +- 往后 n 行数据 +- 使用方法,与 PRECEDING 类似,只不过对象是统计到当前行开始往后。 + + +# UNBOUNDED + +- UNBOUNDED PRECEDING 表示从前面的起点; + +#### 累计求和问题(ROWS BETWEEN) + +- 示例: + - 查找不同产品截至当前月份的累计销售额 + + ``` + SELECT m.product,m.ym,m.amount, + SUM(m.amount) OVER( + PARTITION BY m.product + ORDER BY m.ym + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) + FROM sales_monthly m + ORDER BY m.product,m.ym + ``` + + - SUM函数OVER子句中的PARTITION BY选项表示按照产品进行分区。 + - ORDER BY选项表示按照月份进行排序。 + - ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 表示窗口从当前分区第1行开始,直到当前行结束。 + +- UNBOUNDED FOLLOWING 表示到后面的终点。 + +#### 累计求和问题(RANGE BETWEEN) + +- 示例: + - 查找短期之内(5天)累计转账超过100万元的账户。 + + ``` + SELECT log_ts,from_user,total_amount FROM ( + SELECT to_char(t.log_ts,'yyyy-mm-dd hh24:mi:ss') log_ts,t.from_user,t.amount, + SUM(t.amount) OVER( + PARTITION BY t.from_user + ORDER BY t.log_ts + RANGE INTERVAL '5' DAY PRECEDING + ) AS total_amount + FROM transfer_log t + WHERE t.type = '转账' + ) + WHERE total_amount >= 1000000; + ``` + + +# LAG + +- 往前第 n 行数据 +- 语法: + ``` + Lag ( scalar_expression [ ,offset ] , [ default ] ) OVER ( [ partition_by_clause ] order_by_clause ) + ``` + +#### 去年同期问题 + +TODO + + +# LEAD + +- 往后第 n 行数据 +- 基本语法: + ``` + LAG( 想要的返回值(可以是各种函数) + [,偏移量(从1开始)] + [,如果偏移后没有选定的行,则返回的默认值]) + OVER ( [ partition_by 分区 ] order_by 顺序 ) + ``` +- 高级语法: + ``` + lag(列名,1,0) over (partition by 分组列 order by 排序列 rows between 开始位置 preceding and 结束位置 following) + ``` + +#### 用户访问时长问题 + +- 有一个日志登陆列表,获取用户在某个页面停留时长 + ``` + +------------------+----------------------+---------------+--+ + | userid | time | url | + +------------------+----------------------+---------------+--+ + | Peter | 2015-10-12 01:10:00 | url1 | + | Peter | 2015-10-12 01:15:10 | url2 | + | Peter | 2015-10-12 01:16:40 | url3 | + | Peter | 2015-10-12 02:13:00 | url4 | + | Peter | 2015-10-12 03:14:30 | url5 | + | Marry | 2015-11-12 01:10:00 | url1 | + | Marry | 2015-11-12 01:15:10 | url2 | + | Marry | 2015-11-12 01:16:40 | url3 | + | Marry | 2015-11-12 02:13:00 | url4 | + | Marry | 2015-11-12 03:14:30 | url5 | + +------------------+----------------------+---------------+--+ + ``` +- 代码 + ``` + select + userid + ,time + ,UNIX_TIMESTAMP( + lead(time,1) over(partition by userid order by time),'yyyy-MM-dd HH:mm:ss' + ) + - + UNIX_TIMESTAMP( + time,'yyyy-MM-dd HH:mm:ss' + ) as period + ,url + from + user_log + ``` +- 结果 + ``` + +---------+----------------------+----------------------+---------+-------+--+ + | userid | stime | etime | period | url | + +---------+----------------------+----------------------+---------+-------+--+ + | Marry | 2015-11-12 01:10:00 | 2015-11-12 01:15:10 | 310 | url1 | + | Marry | 2015-11-12 01:15:10 | 2015-11-12 01:16:40 | 90 | url2 | + | Marry | 2015-11-12 01:16:40 | 2015-11-12 02:13:00 | 3380 | url3 | + | Marry | 2015-11-12 02:13:00 | 2015-11-12 03:14:30 | 3690 | url4 | + | Marry | 2015-11-12 03:14:30 | NULL | NULL | url5 | + | Peter | 2015-10-12 01:10:00 | 2015-10-12 01:15:10 | 310 | url1 | + | Peter | 2015-10-12 01:15:10 | 2015-10-12 01:16:40 | 90 | url2 | + | Peter | 2015-10-12 01:16:40 | 2015-10-12 02:13:00 | 3380 | url3 | + | Peter | 2015-10-12 02:13:00 | 2015-10-12 03:14:30 | 3690 | url4 | + | Peter | 2015-10-12 03:14:30 | NULL | NULL | url5 | + +---------+----------------------+----------------------+---------+-------+--+ + ``` + +#### 连续登录问题 + +- 寻找至少连续出现3次的数字 + ``` + +--------------+----+ + | id |num | + +--------------+----+ + | 1 | 1 | + | 2 | 1 | + | 3 | 1 | + | 4 | 1 | + | 5 | 2 | + | 6 | 2 | + | 7 | 3 | + | 8 | 3 | + | 9 | 3 | + | 10 | 4 | + +--------------+----+ + ``` +- 思路:增加两列,使用lag函数-把下面的数据往上错位一个,错位2个,判断num和错位的两列是否相等 +- 代码 + ``` + select + id + ,distinct num + from + ( + select + id + ,num + ,lag(num,1) over(partition by id) as lag1 + ,lag(num,2) over(partition by id) as lag2 + from + log_table + ) a + where num=lag1 and lag1=lag2 + ``` + +#### 用户先后进行某项操作问题 + +- 统计每天符合以下条件的用户数: + - A 操作(opr_id)之后是 B 操作,AB 操作必须相邻 + + ``` + select date,count(*) + from( + select user_id + from( + select user_id, + convert(log_time,date) date, + opr_id f, + lag(opr_id,1) over( + partition by user_id,convert(log_time,date) order by log_time + ) l + from tracking_log + ) a + where f='A' and l='B' + ) b + group by date; + ``` + +#### 最近 N 天(小时/周/月)重复行为问题 + +- 获取在 48 小时之内重复的记录 + + ``` + SELECT * + + FROM ( + SELECT b.* , + LAG(b.OperatorTime, 1, b.OperatorTime) OVER ( PARTITION BY b.No ORDER BY b.OperatorTime ) AS BeforTime , + LEAD(b.OperatorTime, 1, b.OperatorTime) OVER ( PARTITION BY b.No ORDER BY b.OperatorTime ) AS NextTime + FROM Test b + ) a + WHERE + DATEDIFF(HH, a.BeforTime, a.OperatorTime) < 24 + AND DATEDIFF(HH, a.OperatorTime, a.NextTime) < 24 + AND a.No IN ( + SELECT c.No + FROM dbo.Test c + GROUP BY c.No + HAVING COUNT(c.No) > 1 + ) + ``` + +# NTILE(n) + +- 把有序分区中的行分发到指定数据的组中,各个组有编号,编号从1开始,对于每一行,NTILE 返回此行所属的组的编号。 +- 注意:n 必须为 int 类型。 + + +# GROUP_CONCAT + +- 示例 1: + + - 有下面的数据表,要求按分数 score 进行分组,并将分组后的学生姓名打印下来; + ``` + + |id |subject |student|teacher|score| + --------------------------------------- + |1 |数学 |小红 |王老师 |80 | + |2 |数学 |小李 |王老师 |80 | + |3 |数学 |小王 |王老师 |70 | + |4 |数学 |小张 |王老师 |90 | + |5 |数学 |小赵 |王老师 |70 | + |6 |数学 |小孙 |王老师 |80 | + |7 |数学 |小钱 |王老师 |90 | + |8 |数学 |小高 |王老师 |70 | + |9 |数学 |小秦 |王老师 |80 | + |10 |数学 |小马 |王老师 |90 | + |11 |数学 |小朱 |王老师 |90 | + |12 |语文 |小高 |李老师 |70 | + |15 |语文 |小秦 |李老师 |70 | + |18 |语文 |小马 |李老师 |80 | + |21 |语文 |小朱 |李老师 |90 | + |24 |语文 |小钱 |李老师 |90 | + ``` + + - 代码: + + ``` + select score,group_concat(student) from exam group by score; + ``` + - 执行结果为 + + ``` + |score |group_concat(student) | + ------------------------------------- + |70 |小王,小赵,小高,小高,小秦 | + |80 |小红,小李,小孙,小秦,小马 | + |90 |小张,小钱,小马,小朱,小朱,小钱 | + ``` + + - 如果我们需要去重,则需要给函数中加一个distinct参数: + + ``` + select score,group_concat(distinct student) from exam group by score; + ``` + + - 执行结果为: + + ``` + |score |group_concat(student) | + --------------------------------- + |70 |小王,小赵,小高,小秦 | + |80 |小红,小李,小孙,小秦,小马 | + |90 |小张,小钱,小马,小朱 | + ``` + +- 示例 2: + - 统计用户行为序列为 A-B-D 的用户数 + - 其中:A-B 之间可以有任何其他浏览记录(如 C、E 等); + - B-D 之间除了 C 记录可以有任何其他浏览记录(如 A、E 等)。 + ``` + select count(*) + from( + select user_id,group_concat(opr_id) ubp + from tracking_log + group by user_id + ) a + where ubp like '%A%B%D%' and ubp not like '%A%B%C%D%' + ``` + + +# 参考链接 + +- https://blog.csdn.net/weixin_34280060/article/details/123130155 + + diff --git "a/_posts/2022-01-24-SQL\357\274\232WITH RECURSIVE \351\200\222\345\275\222\346\237\245\350\257\242\344\275\277\347\224\250\346\226\271\346\263\225.md" "b/_posts/2022-01-24-SQL\357\274\232WITH RECURSIVE \351\200\222\345\275\222\346\237\245\350\257\242\344\275\277\347\224\250\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..fce25a0f16a --- /dev/null +++ "b/_posts/2022-01-24-SQL\357\274\232WITH RECURSIVE \351\200\222\345\275\222\346\237\245\350\257\242\344\275\277\347\224\250\346\226\271\346\263\225.md" @@ -0,0 +1,80 @@ +--- +layout: post +title: SQL:WITH RECURSIVE 递归查询使用方法 +subtitle: 生成模拟销售数据 +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + + +# WITH RECURSIVE 用法 + +- WITH xxxx AS () 是对一个查询子句做别名,同时数据库会对该子句生成临时表; +- WITH RECURSIVE 则是一个递归的查询子句,他会把查询出来的结果再次代入到查询子句中继续查询; +- 如下面的语句: + ``` + WITH RECURSIVE number(n, fact) AS ( + -- 递归开始的第一条记录,只要这个这条语句一执行,就会将第一条记录了的结果存储到 number 这种表中,此时可以理解为 number 只有一条记录 + SELECT + 0,1 + FROM + DUAL + UNION ALL + -- UNION ALL 语句执行的结果,会和之前的执行结果一起,写入到返回结果中,这里的 n 就是 number 执行时传入的参数; + SELECT + n+1, (n+1)*fact + FROM + number + WHERE + n < 7 -- 只要 n < 7 条件成立,该递归就会一直执行下去; + ) + SELECT * FROM number; + + ``` +- 执行结果: + + ![sql-15]({{site.baseurl}}/img-post/sql-15.png) + +# 模拟销售数据需求: + - 创建 products、sales 表,并写入模拟销售数据; +- 创建 products + + ```aidl + create table products( + product_id integer not null primary key, + product_name varchar(100) not null unique, + product_subcategory varchar(100) not null, + product_category varchar(100) not null + ); + ``` +- 创建 sales + ```language + create table sales( + product_id integer not null, + sale_time timestamp not null, + quantity integer not null + ); + ``` + +# 写入模拟销售数据 + +```aidl +insert into sales +with recursive s(product_id, sale_time, quantity) as ( + select product_id, '2022-04-01 00:00:00', floor(10*rand(0)) from products + union all + select product_id, sale_time + interval 1 minute, floor(10*rand(0)) + from s + where sale_time < '2022-04-01 10:00:00' +) +select * from s; + +``` + +- 得到结果 + + ![sql-14]({{site.baseurl}}/img-post/sql-14.png) diff --git "a/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 CASE \345\222\214 GROUP BY \345\256\236\347\216\260\346\225\260\346\215\256\351\200\217\350\247\206.md" "b/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 CASE \345\222\214 GROUP BY \345\256\236\347\216\260\346\225\260\346\215\256\351\200\217\350\247\206.md" new file mode 100644 index 00000000000..c3e11913d65 --- /dev/null +++ "b/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 CASE \345\222\214 GROUP BY \345\256\236\347\216\260\346\225\260\346\215\256\351\200\217\350\247\206.md" @@ -0,0 +1,1755 @@ +--- +layout: post +title: SQL:使用 CASE 和 GROUP BY 实现数据透视 +subtitle: 统计不同产品、不同渠道销售情况 +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + +# 创建表 +- 需求: + - 需要根据 employee、attendance、calendar 三张表,统计员工的出勤情况; +- - 创建销售数据表sales_data +``` +-- saledate表示销售日期,product表示产品名称,channel表示销售渠道,amount表示销售金额 + +CREATE TABLE sales_data(saledate DATE, product VARCHAR(20), channel VARCHAR(20), amount NUMERIC(10, 2)); +``` + +# 写入测试数据 + +```aidl +INSERT INTO sales_data VALUES ('2019-01-01','桔子','淘宝',1864.00); +INSERT INTO sales_data VALUES ('2019-01-01','桔子','京东',1329.00); +INSERT INTO sales_data VALUES ('2019-01-01','桔子','店面',1736.00); +INSERT INTO sales_data VALUES ('2019-01-01','香蕉','淘宝',1573.00); +INSERT INTO sales_data VALUES ('2019-01-01','香蕉','京东',1364.00); +INSERT INTO sales_data VALUES ('2019-01-01','香蕉','店面',1178.00); +INSERT INTO sales_data VALUES ('2019-01-01','苹果','淘宝',511.00); +INSERT INTO sales_data VALUES ('2019-01-01','苹果','京东',568.00); +INSERT INTO sales_data VALUES ('2019-01-01','苹果','店面',847.00); +INSERT INTO sales_data VALUES ('2019-01-02','桔子','淘宝',1923.00); +INSERT INTO sales_data VALUES ('2019-01-02','桔子','京东',775.00); +INSERT INTO sales_data VALUES ('2019-01-02','桔子','店面',599.00); +INSERT INTO sales_data VALUES ('2019-01-02','香蕉','淘宝',1612.00); +INSERT INTO sales_data VALUES ('2019-01-02','香蕉','京东',1057.00); +INSERT INTO sales_data VALUES ('2019-01-02','香蕉','店面',1580.00); +INSERT INTO sales_data VALUES ('2019-01-02','苹果','淘宝',1345.00); +INSERT INTO sales_data VALUES ('2019-01-02','苹果','京东',564.00); +INSERT INTO sales_data VALUES ('2019-01-02','苹果','店面',1953.00); +INSERT INTO sales_data VALUES ('2019-01-03','桔子','淘宝',729.00); +INSERT INTO sales_data VALUES ('2019-01-03','桔子','京东',1758.00); +INSERT INTO sales_data VALUES ('2019-01-03','桔子','店面',918.00); +INSERT INTO sales_data VALUES ('2019-01-03','香蕉','淘宝',1879.00); +INSERT INTO sales_data VALUES ('2019-01-03','香蕉','京东',1142.00); +INSERT INTO sales_data VALUES ('2019-01-03','香蕉','店面',731.00); +INSERT INTO sales_data VALUES ('2019-01-03','苹果','淘宝',1329.00); +INSERT INTO sales_data VALUES ('2019-01-03','苹果','京东',1315.00); +INSERT INTO sales_data VALUES ('2019-01-03','苹果','店面',1956.00); +INSERT INTO sales_data VALUES ('2019-01-04','桔子','淘宝',547.00); +INSERT INTO sales_data VALUES ('2019-01-04','桔子','京东',1462.00); +INSERT INTO sales_data VALUES ('2019-01-04','桔子','店面',1418.00); +INSERT INTO sales_data VALUES ('2019-01-04','香蕉','淘宝',1205.00); +INSERT INTO sales_data VALUES ('2019-01-04','香蕉','京东',1326.00); +INSERT INTO sales_data VALUES ('2019-01-04','香蕉','店面',746.00); +INSERT INTO sales_data VALUES ('2019-01-04','苹果','淘宝',940.00); +INSERT INTO sales_data VALUES ('2019-01-04','苹果','京东',898.00); +INSERT INTO sales_data VALUES ('2019-01-04','苹果','店面',1610.00); +INSERT INTO sales_data VALUES ('2019-01-05','桔子','淘宝',1624.00); +INSERT INTO sales_data VALUES ('2019-01-05','桔子','京东',915.00); +INSERT INTO sales_data VALUES ('2019-01-05','桔子','店面',1683.00); +INSERT INTO sales_data VALUES ('2019-01-05','香蕉','淘宝',1970.00); +INSERT INTO sales_data VALUES ('2019-01-05','香蕉','京东',833.00); +INSERT INTO sales_data VALUES ('2019-01-05','香蕉','店面',1954.00); +INSERT INTO sales_data VALUES ('2019-01-05','苹果','淘宝',565.00); +INSERT INTO sales_data VALUES ('2019-01-05','苹果','京东',1940.00); +INSERT INTO sales_data VALUES ('2019-01-05','苹果','店面',1006.00); +INSERT INTO sales_data VALUES ('2019-01-06','桔子','淘宝',1645.00); +INSERT INTO sales_data VALUES ('2019-01-06','桔子','京东',1285.00); +INSERT INTO sales_data VALUES ('2019-01-06','桔子','店面',1069.00); +INSERT INTO sales_data VALUES ('2019-01-06','香蕉','淘宝',1593.00); +INSERT INTO sales_data VALUES ('2019-01-06','香蕉','京东',1504.00); +INSERT INTO sales_data VALUES ('2019-01-06','香蕉','店面',817.00); +INSERT INTO sales_data VALUES ('2019-01-06','苹果','淘宝',2000.00); +INSERT INTO sales_data VALUES ('2019-01-06','苹果','京东',1373.00); +INSERT INTO sales_data VALUES ('2019-01-06','苹果','店面',1450.00); +INSERT INTO sales_data VALUES ('2019-01-07','桔子','淘宝',727.00); +INSERT INTO sales_data VALUES ('2019-01-07','桔子','京东',698.00); +INSERT INTO sales_data VALUES ('2019-01-07','桔子','店面',759.00); +INSERT INTO sales_data VALUES ('2019-01-07','香蕉','淘宝',673.00); +INSERT INTO sales_data VALUES ('2019-01-07','香蕉','京东',729.00); +INSERT INTO sales_data VALUES ('2019-01-07','香蕉','店面',1706.00); +INSERT INTO sales_data VALUES ('2019-01-07','苹果','淘宝',1575.00); +INSERT INTO sales_data VALUES ('2019-01-07','苹果','京东',1419.00); +INSERT INTO sales_data VALUES ('2019-01-07','苹果','店面',1017.00); +INSERT INTO sales_data VALUES ('2019-01-08','桔子','淘宝',1811.00); +INSERT INTO sales_data VALUES ('2019-01-08','桔子','京东',1849.00); +INSERT INTO sales_data VALUES ('2019-01-08','桔子','店面',1405.00); +INSERT INTO sales_data VALUES ('2019-01-08','香蕉','淘宝',1406.00); +INSERT INTO sales_data VALUES ('2019-01-08','香蕉','京东',1453.00); +INSERT INTO sales_data VALUES ('2019-01-08','香蕉','店面',1800.00); +INSERT INTO sales_data VALUES ('2019-01-08','苹果','淘宝',1070.00); +INSERT INTO sales_data VALUES ('2019-01-08','苹果','京东',1403.00); +INSERT INTO sales_data VALUES ('2019-01-08','苹果','店面',613.00); +INSERT INTO sales_data VALUES ('2019-01-09','桔子','淘宝',1009.00); +INSERT INTO sales_data VALUES ('2019-01-09','桔子','京东',1453.00); +INSERT INTO sales_data VALUES ('2019-01-09','桔子','店面',2038.00); +INSERT INTO sales_data VALUES ('2019-01-09','香蕉','淘宝',1495.00); +INSERT INTO sales_data VALUES ('2019-01-09','香蕉','京东',1073.00); +INSERT INTO sales_data VALUES ('2019-01-09','香蕉','店面',1298.00); +INSERT INTO sales_data VALUES ('2019-01-09','苹果','淘宝',2039.00); +INSERT INTO sales_data VALUES ('2019-01-09','苹果','京东',641.00); +INSERT INTO sales_data VALUES ('2019-01-09','苹果','店面',777.00); +INSERT INTO sales_data VALUES ('2019-01-10','桔子','淘宝',836.00); +INSERT INTO sales_data VALUES ('2019-01-10','桔子','京东',621.00); +INSERT INTO sales_data VALUES ('2019-01-10','桔子','店面',1630.00); +INSERT INTO sales_data VALUES ('2019-01-10','香蕉','淘宝',1761.00); +INSERT INTO sales_data VALUES ('2019-01-10','香蕉','京东',818.00); +INSERT INTO sales_data VALUES ('2019-01-10','香蕉','店面',1797.00); +INSERT INTO sales_data VALUES ('2019-01-10','苹果','淘宝',1990.00); +INSERT INTO sales_data VALUES ('2019-01-10','苹果','京东',960.00); +INSERT INTO sales_data VALUES ('2019-01-10','苹果','店面',1997.00); +INSERT INTO sales_data VALUES ('2019-01-11','桔子','淘宝',1671.00); +INSERT INTO sales_data VALUES ('2019-01-11','桔子','京东',2010.00); +INSERT INTO sales_data VALUES ('2019-01-11','桔子','店面',1391.00); +INSERT INTO sales_data VALUES ('2019-01-11','香蕉','淘宝',658.00); +INSERT INTO sales_data VALUES ('2019-01-11','香蕉','京东',1786.00); +INSERT INTO sales_data VALUES ('2019-01-11','香蕉','店面',1205.00); +INSERT INTO sales_data VALUES ('2019-01-11','苹果','淘宝',1528.00); +INSERT INTO sales_data VALUES ('2019-01-11','苹果','京东',1158.00); +INSERT INTO sales_data VALUES ('2019-01-11','苹果','店面',623.00); +INSERT INTO sales_data VALUES ('2019-01-12','桔子','淘宝',1299.00); +INSERT INTO sales_data VALUES ('2019-01-12','桔子','京东',1698.00); +INSERT INTO sales_data VALUES ('2019-01-12','桔子','店面',1497.00); +INSERT INTO sales_data VALUES ('2019-01-12','香蕉','淘宝',1377.00); +INSERT INTO sales_data VALUES ('2019-01-12','香蕉','京东',667.00); +INSERT INTO sales_data VALUES ('2019-01-12','香蕉','店面',910.00); +INSERT INTO sales_data VALUES ('2019-01-12','苹果','淘宝',1374.00); +INSERT INTO sales_data VALUES ('2019-01-12','苹果','京东',1621.00); +INSERT INTO sales_data VALUES ('2019-01-12','苹果','店面',1443.00); +INSERT INTO sales_data VALUES ('2019-01-13','桔子','淘宝',637.00); +INSERT INTO sales_data VALUES ('2019-01-13','桔子','京东',1625.00); +INSERT INTO sales_data VALUES ('2019-01-13','桔子','店面',1548.00); +INSERT INTO sales_data VALUES ('2019-01-13','香蕉','淘宝',873.00); +INSERT INTO sales_data VALUES ('2019-01-13','香蕉','京东',1916.00); +INSERT INTO sales_data VALUES ('2019-01-13','香蕉','店面',1624.00); +INSERT INTO sales_data VALUES ('2019-01-13','苹果','淘宝',1958.00); +INSERT INTO sales_data VALUES ('2019-01-13','苹果','京东',1632.00); +INSERT INTO sales_data VALUES ('2019-01-13','苹果','店面',1897.00); +INSERT INTO sales_data VALUES ('2019-01-14','桔子','淘宝',1715.00); +INSERT INTO sales_data VALUES ('2019-01-14','桔子','京东',1582.00); +INSERT INTO sales_data VALUES ('2019-01-14','桔子','店面',817.00); +INSERT INTO sales_data VALUES ('2019-01-14','香蕉','淘宝',1667.00); +INSERT INTO sales_data VALUES ('2019-01-14','香蕉','京东',1203.00); +INSERT INTO sales_data VALUES ('2019-01-14','香蕉','店面',777.00); +INSERT INTO sales_data VALUES ('2019-01-14','苹果','淘宝',1008.00); +INSERT INTO sales_data VALUES ('2019-01-14','苹果','京东',1311.00); +INSERT INTO sales_data VALUES ('2019-01-14','苹果','店面',2013.00); +INSERT INTO sales_data VALUES ('2019-01-15','桔子','淘宝',1668.00); +INSERT INTO sales_data VALUES ('2019-01-15','桔子','京东',794.00); +INSERT INTO sales_data VALUES ('2019-01-15','桔子','店面',1126.00); +INSERT INTO sales_data VALUES ('2019-01-15','香蕉','淘宝',1741.00); +INSERT INTO sales_data VALUES ('2019-01-15','香蕉','京东',1538.00); +INSERT INTO sales_data VALUES ('2019-01-15','香蕉','店面',768.00); +INSERT INTO sales_data VALUES ('2019-01-15','苹果','淘宝',1183.00); +INSERT INTO sales_data VALUES ('2019-01-15','苹果','京东',859.00); +INSERT INTO sales_data VALUES ('2019-01-15','苹果','店面',880.00); +INSERT INTO sales_data VALUES ('2019-01-16','桔子','淘宝',1543.00); +INSERT INTO sales_data VALUES ('2019-01-16','桔子','京东',1684.00); +INSERT INTO sales_data VALUES ('2019-01-16','桔子','店面',1951.00); +INSERT INTO sales_data VALUES ('2019-01-16','香蕉','淘宝',930.00); +INSERT INTO sales_data VALUES ('2019-01-16','香蕉','京东',1760.00); +INSERT INTO sales_data VALUES ('2019-01-16','香蕉','店面',1517.00); +INSERT INTO sales_data VALUES ('2019-01-16','苹果','淘宝',1918.00); +INSERT INTO sales_data VALUES ('2019-01-16','苹果','京东',2073.00); +INSERT INTO sales_data VALUES ('2019-01-16','苹果','店面',1373.00); +INSERT INTO sales_data VALUES ('2019-01-17','桔子','淘宝',1487.00); +INSERT INTO sales_data VALUES ('2019-01-17','桔子','京东',1976.00); +INSERT INTO sales_data VALUES ('2019-01-17','桔子','店面',950.00); +INSERT INTO sales_data VALUES ('2019-01-17','香蕉','淘宝',1324.00); +INSERT INTO sales_data VALUES ('2019-01-17','香蕉','京东',1627.00); +INSERT INTO sales_data VALUES ('2019-01-17','香蕉','店面',1967.00); +INSERT INTO sales_data VALUES ('2019-01-17','苹果','淘宝',1576.00); +INSERT INTO sales_data VALUES ('2019-01-17','苹果','京东',1229.00); +INSERT INTO sales_data VALUES ('2019-01-17','苹果','店面',1105.00); +INSERT INTO sales_data VALUES ('2019-01-18','桔子','淘宝',1792.00); +INSERT INTO sales_data VALUES ('2019-01-18','桔子','京东',1676.00); +INSERT INTO sales_data VALUES ('2019-01-18','桔子','店面',1856.00); +INSERT INTO sales_data VALUES ('2019-01-18','香蕉','淘宝',1740.00); +INSERT INTO sales_data VALUES ('2019-01-18','香蕉','京东',1274.00); +INSERT INTO sales_data VALUES ('2019-01-18','香蕉','店面',2080.00); +INSERT INTO sales_data VALUES ('2019-01-18','苹果','淘宝',796.00); +INSERT INTO sales_data VALUES ('2019-01-18','苹果','京东',946.00); +INSERT INTO sales_data VALUES ('2019-01-18','苹果','店面',1548.00); +INSERT INTO sales_data VALUES ('2019-01-19','桔子','淘宝',1000.00); +INSERT INTO sales_data VALUES ('2019-01-19','桔子','京东',1563.00); +INSERT INTO sales_data VALUES ('2019-01-19','桔子','店面',1843.00); +INSERT INTO sales_data VALUES ('2019-01-19','香蕉','淘宝',1310.00); +INSERT INTO sales_data VALUES ('2019-01-19','香蕉','京东',1031.00); +INSERT INTO sales_data VALUES ('2019-01-19','香蕉','店面',1451.00); +INSERT INTO sales_data VALUES ('2019-01-19','苹果','淘宝',1186.00); +INSERT INTO sales_data VALUES ('2019-01-19','苹果','京东',1386.00); +INSERT INTO sales_data VALUES ('2019-01-19','苹果','店面',1137.00); +INSERT INTO sales_data VALUES ('2019-01-20','桔子','淘宝',633.00); +INSERT INTO sales_data VALUES ('2019-01-20','桔子','京东',1235.00); +INSERT INTO sales_data VALUES ('2019-01-20','桔子','店面',1140.00); +INSERT INTO sales_data VALUES ('2019-01-20','香蕉','淘宝',1431.00); +INSERT INTO sales_data VALUES ('2019-01-20','香蕉','京东',642.00); +INSERT INTO sales_data VALUES ('2019-01-20','香蕉','店面',1036.00); +INSERT INTO sales_data VALUES ('2019-01-20','苹果','淘宝',1801.00); +INSERT INTO sales_data VALUES ('2019-01-20','苹果','京东',1386.00); +INSERT INTO sales_data VALUES ('2019-01-20','苹果','店面',2083.00); +INSERT INTO sales_data VALUES ('2019-01-21','桔子','淘宝',1694.00); +INSERT INTO sales_data VALUES ('2019-01-21','桔子','京东',887.00); +INSERT INTO sales_data VALUES ('2019-01-21','桔子','店面',1236.00); +INSERT INTO sales_data VALUES ('2019-01-21','香蕉','淘宝',719.00); +INSERT INTO sales_data VALUES ('2019-01-21','香蕉','京东',2094.00); +INSERT INTO sales_data VALUES ('2019-01-21','香蕉','店面',828.00); +INSERT INTO sales_data VALUES ('2019-01-21','苹果','淘宝',1990.00); +INSERT INTO sales_data VALUES ('2019-01-21','苹果','京东',1749.00); +INSERT INTO sales_data VALUES ('2019-01-21','苹果','店面',1517.00); +INSERT INTO sales_data VALUES ('2019-01-22','桔子','淘宝',1990.00); +INSERT INTO sales_data VALUES ('2019-01-22','桔子','京东',1965.00); +INSERT INTO sales_data VALUES ('2019-01-22','桔子','店面',1883.00); +INSERT INTO sales_data VALUES ('2019-01-22','香蕉','淘宝',1454.00); +INSERT INTO sales_data VALUES ('2019-01-22','香蕉','京东',875.00); +INSERT INTO sales_data VALUES ('2019-01-22','香蕉','店面',1356.00); +INSERT INTO sales_data VALUES ('2019-01-22','苹果','淘宝',1207.00); +INSERT INTO sales_data VALUES ('2019-01-22','苹果','京东',1595.00); +INSERT INTO sales_data VALUES ('2019-01-22','苹果','店面',1797.00); +INSERT INTO sales_data VALUES ('2019-01-23','桔子','淘宝',2073.00); +INSERT INTO sales_data VALUES ('2019-01-23','桔子','京东',696.00); +INSERT INTO sales_data VALUES ('2019-01-23','桔子','店面',1099.00); +INSERT INTO sales_data VALUES ('2019-01-23','香蕉','淘宝',1120.00); +INSERT INTO sales_data VALUES ('2019-01-23','香蕉','京东',733.00); +INSERT INTO sales_data VALUES ('2019-01-23','香蕉','店面',1739.00); +INSERT INTO sales_data VALUES ('2019-01-23','苹果','淘宝',1665.00); +INSERT INTO sales_data VALUES ('2019-01-23','苹果','京东',1569.00); +INSERT INTO sales_data VALUES ('2019-01-23','苹果','店面',1786.00); +INSERT INTO sales_data VALUES ('2019-01-24','桔子','淘宝',2111.00); +INSERT INTO sales_data VALUES ('2019-01-24','桔子','京东',1280.00); +INSERT INTO sales_data VALUES ('2019-01-24','桔子','店面',1082.00); +INSERT INTO sales_data VALUES ('2019-01-24','香蕉','淘宝',2099.00); +INSERT INTO sales_data VALUES ('2019-01-24','香蕉','京东',874.00); +INSERT INTO sales_data VALUES ('2019-01-24','香蕉','店面',1369.00); +INSERT INTO sales_data VALUES ('2019-01-24','苹果','淘宝',1235.00); +INSERT INTO sales_data VALUES ('2019-01-24','苹果','京东',993.00); +INSERT INTO sales_data VALUES ('2019-01-24','苹果','店面',1363.00); +INSERT INTO sales_data VALUES ('2019-01-25','桔子','淘宝',1468.00); +INSERT INTO sales_data VALUES ('2019-01-25','桔子','京东',888.00); +INSERT INTO sales_data VALUES ('2019-01-25','桔子','店面',1017.00); +INSERT INTO sales_data VALUES ('2019-01-25','香蕉','淘宝',885.00); +INSERT INTO sales_data VALUES ('2019-01-25','香蕉','京东',773.00); +INSERT INTO sales_data VALUES ('2019-01-25','香蕉','店面',878.00); +INSERT INTO sales_data VALUES ('2019-01-25','苹果','淘宝',662.00); +INSERT INTO sales_data VALUES ('2019-01-25','苹果','京东',1622.00); +INSERT INTO sales_data VALUES ('2019-01-25','苹果','店面',1148.00); +INSERT INTO sales_data VALUES ('2019-01-26','桔子','淘宝',1418.00); +INSERT INTO sales_data VALUES ('2019-01-26','桔子','京东',729.00); +INSERT INTO sales_data VALUES ('2019-01-26','桔子','店面',643.00); +INSERT INTO sales_data VALUES ('2019-01-26','香蕉','淘宝',1111.00); +INSERT INTO sales_data VALUES ('2019-01-26','香蕉','京东',692.00); +INSERT INTO sales_data VALUES ('2019-01-26','香蕉','店面',728.00); +INSERT INTO sales_data VALUES ('2019-01-26','苹果','淘宝',1600.00); +INSERT INTO sales_data VALUES ('2019-01-26','苹果','京东',1202.00); +INSERT INTO sales_data VALUES ('2019-01-26','苹果','店面',851.00); +INSERT INTO sales_data VALUES ('2019-01-27','桔子','淘宝',1233.00); +INSERT INTO sales_data VALUES ('2019-01-27','桔子','京东',761.00); +INSERT INTO sales_data VALUES ('2019-01-27','桔子','店面',1816.00); +INSERT INTO sales_data VALUES ('2019-01-27','香蕉','淘宝',909.00); +INSERT INTO sales_data VALUES ('2019-01-27','香蕉','京东',757.00); +INSERT INTO sales_data VALUES ('2019-01-27','香蕉','店面',981.00); +INSERT INTO sales_data VALUES ('2019-01-27','苹果','淘宝',1376.00); +INSERT INTO sales_data VALUES ('2019-01-27','苹果','京东',741.00); +INSERT INTO sales_data VALUES ('2019-01-27','苹果','店面',1240.00); +INSERT INTO sales_data VALUES ('2019-01-28','桔子','淘宝',635.00); +INSERT INTO sales_data VALUES ('2019-01-28','桔子','京东',1366.00); +INSERT INTO sales_data VALUES ('2019-01-28','桔子','店面',1623.00); +INSERT INTO sales_data VALUES ('2019-01-28','香蕉','淘宝',1383.00); +INSERT INTO sales_data VALUES ('2019-01-28','香蕉','京东',713.00); +INSERT INTO sales_data VALUES ('2019-01-28','香蕉','店面',1891.00); +INSERT INTO sales_data VALUES ('2019-01-28','苹果','淘宝',1781.00); +INSERT INTO sales_data VALUES ('2019-01-28','苹果','京东',978.00); +INSERT INTO sales_data VALUES ('2019-01-28','苹果','店面',2044.00); +INSERT INTO sales_data VALUES ('2019-01-29','桔子','淘宝',2044.00); +INSERT INTO sales_data VALUES ('2019-01-29','桔子','京东',1026.00); +INSERT INTO sales_data VALUES ('2019-01-29','桔子','店面',1551.00); +INSERT INTO sales_data VALUES ('2019-01-29','香蕉','淘宝',1071.00); +INSERT INTO sales_data VALUES ('2019-01-29','香蕉','京东',1819.00); +INSERT INTO sales_data VALUES ('2019-01-29','香蕉','店面',1655.00); +INSERT INTO sales_data VALUES ('2019-01-29','苹果','淘宝',1089.00); +INSERT INTO sales_data VALUES ('2019-01-29','苹果','京东',805.00); +INSERT INTO sales_data VALUES ('2019-01-29','苹果','店面',1722.00); +INSERT INTO sales_data VALUES ('2019-01-30','桔子','淘宝',1197.00); +INSERT INTO sales_data VALUES ('2019-01-30','桔子','京东',1785.00); +INSERT INTO sales_data VALUES ('2019-01-30','桔子','店面',804.00); +INSERT INTO sales_data VALUES ('2019-01-30','香蕉','淘宝',1424.00); +INSERT INTO sales_data VALUES ('2019-01-30','香蕉','京东',888.00); +INSERT INTO sales_data VALUES ('2019-01-30','香蕉','店面',935.00); +INSERT INTO sales_data VALUES ('2019-01-30','苹果','淘宝',1109.00); +INSERT INTO sales_data VALUES ('2019-01-30','苹果','京东',1167.00); +INSERT INTO sales_data VALUES ('2019-01-30','苹果','店面',1062.00); +INSERT INTO sales_data VALUES ('2019-01-31','桔子','淘宝',1465.00); +INSERT INTO sales_data VALUES ('2019-01-31','桔子','京东',1918.00); +INSERT INTO sales_data VALUES ('2019-01-31','桔子','店面',1178.00); +INSERT INTO sales_data VALUES ('2019-01-31','香蕉','淘宝',2075.00); +INSERT INTO sales_data VALUES ('2019-01-31','香蕉','京东',1918.00); +INSERT INTO sales_data VALUES ('2019-01-31','香蕉','店面',1908.00); +INSERT INTO sales_data VALUES ('2019-01-31','苹果','淘宝',1563.00); +INSERT INTO sales_data VALUES ('2019-01-31','苹果','京东',1166.00); +INSERT INTO sales_data VALUES ('2019-01-31','苹果','店面',1987.00); +INSERT INTO sales_data VALUES ('2019-02-01','桔子','淘宝',1324.00); +INSERT INTO sales_data VALUES ('2019-02-01','桔子','京东',817.00); +INSERT INTO sales_data VALUES ('2019-02-01','桔子','店面',835.00); +INSERT INTO sales_data VALUES ('2019-02-01','香蕉','淘宝',1233.00); +INSERT INTO sales_data VALUES ('2019-02-01','香蕉','京东',721.00); +INSERT INTO sales_data VALUES ('2019-02-01','香蕉','店面',1221.00); +INSERT INTO sales_data VALUES ('2019-02-01','苹果','淘宝',2145.00); +INSERT INTO sales_data VALUES ('2019-02-01','苹果','京东',1152.00); +INSERT INTO sales_data VALUES ('2019-02-01','苹果','店面',900.00); +INSERT INTO sales_data VALUES ('2019-02-02','桔子','淘宝',1665.00); +INSERT INTO sales_data VALUES ('2019-02-02','桔子','京东',1606.00); +INSERT INTO sales_data VALUES ('2019-02-02','桔子','店面',1070.00); +INSERT INTO sales_data VALUES ('2019-02-02','香蕉','淘宝',1247.00); +INSERT INTO sales_data VALUES ('2019-02-02','香蕉','京东',2158.00); +INSERT INTO sales_data VALUES ('2019-02-02','香蕉','店面',709.00); +INSERT INTO sales_data VALUES ('2019-02-02','苹果','淘宝',1406.00); +INSERT INTO sales_data VALUES ('2019-02-02','苹果','京东',1437.00); +INSERT INTO sales_data VALUES ('2019-02-02','苹果','店面',952.00); +INSERT INTO sales_data VALUES ('2019-02-03','桔子','淘宝',1701.00); +INSERT INTO sales_data VALUES ('2019-02-03','桔子','京东',1906.00); +INSERT INTO sales_data VALUES ('2019-02-03','桔子','店面',1479.00); +INSERT INTO sales_data VALUES ('2019-02-03','香蕉','淘宝',2118.00); +INSERT INTO sales_data VALUES ('2019-02-03','香蕉','京东',1221.00); +INSERT INTO sales_data VALUES ('2019-02-03','香蕉','店面',1247.00); +INSERT INTO sales_data VALUES ('2019-02-03','苹果','淘宝',1146.00); +INSERT INTO sales_data VALUES ('2019-02-03','苹果','京东',1146.00); +INSERT INTO sales_data VALUES ('2019-02-03','苹果','店面',1015.00); +INSERT INTO sales_data VALUES ('2019-02-04','桔子','淘宝',909.00); +INSERT INTO sales_data VALUES ('2019-02-04','桔子','京东',2065.00); +INSERT INTO sales_data VALUES ('2019-02-04','桔子','店面',1536.00); +INSERT INTO sales_data VALUES ('2019-02-04','香蕉','淘宝',746.00); +INSERT INTO sales_data VALUES ('2019-02-04','香蕉','京东',1234.00); +INSERT INTO sales_data VALUES ('2019-02-04','香蕉','店面',1698.00); +INSERT INTO sales_data VALUES ('2019-02-04','苹果','淘宝',926.00); +INSERT INTO sales_data VALUES ('2019-02-04','苹果','京东',1812.00); +INSERT INTO sales_data VALUES ('2019-02-04','苹果','店面',1764.00); +INSERT INTO sales_data VALUES ('2019-02-05','桔子','淘宝',1497.00); +INSERT INTO sales_data VALUES ('2019-02-05','桔子','京东',1806.00); +INSERT INTO sales_data VALUES ('2019-02-05','桔子','店面',766.00); +INSERT INTO sales_data VALUES ('2019-02-05','香蕉','淘宝',1741.00); +INSERT INTO sales_data VALUES ('2019-02-05','香蕉','京东',1311.00); +INSERT INTO sales_data VALUES ('2019-02-05','香蕉','店面',1712.00); +INSERT INTO sales_data VALUES ('2019-02-05','苹果','淘宝',2151.00); +INSERT INTO sales_data VALUES ('2019-02-05','苹果','京东',1898.00); +INSERT INTO sales_data VALUES ('2019-02-05','苹果','店面',1710.00); +INSERT INTO sales_data VALUES ('2019-02-06','桔子','淘宝',705.00); +INSERT INTO sales_data VALUES ('2019-02-06','桔子','京东',1149.00); +INSERT INTO sales_data VALUES ('2019-02-06','桔子','店面',992.00); +INSERT INTO sales_data VALUES ('2019-02-06','香蕉','淘宝',997.00); +INSERT INTO sales_data VALUES ('2019-02-06','香蕉','京东',685.00); +INSERT INTO sales_data VALUES ('2019-02-06','香蕉','店面',732.00); +INSERT INTO sales_data VALUES ('2019-02-06','苹果','淘宝',1811.00); +INSERT INTO sales_data VALUES ('2019-02-06','苹果','京东',2138.00); +INSERT INTO sales_data VALUES ('2019-02-06','苹果','店面',1288.00); +INSERT INTO sales_data VALUES ('2019-02-07','桔子','淘宝',898.00); +INSERT INTO sales_data VALUES ('2019-02-07','桔子','京东',1124.00); +INSERT INTO sales_data VALUES ('2019-02-07','桔子','店面',1775.00); +INSERT INTO sales_data VALUES ('2019-02-07','香蕉','淘宝',1248.00); +INSERT INTO sales_data VALUES ('2019-02-07','香蕉','京东',1363.00); +INSERT INTO sales_data VALUES ('2019-02-07','香蕉','店面',1669.00); +INSERT INTO sales_data VALUES ('2019-02-07','苹果','淘宝',2114.00); +INSERT INTO sales_data VALUES ('2019-02-07','苹果','京东',1439.00); +INSERT INTO sales_data VALUES ('2019-02-07','苹果','店面',733.00); +INSERT INTO sales_data VALUES ('2019-02-08','桔子','淘宝',1648.00); +INSERT INTO sales_data VALUES ('2019-02-08','桔子','京东',1700.00); +INSERT INTO sales_data VALUES ('2019-02-08','桔子','店面',1880.00); +INSERT INTO sales_data VALUES ('2019-02-08','香蕉','淘宝',1241.00); +INSERT INTO sales_data VALUES ('2019-02-08','香蕉','京东',1022.00); +INSERT INTO sales_data VALUES ('2019-02-08','香蕉','店面',1511.00); +INSERT INTO sales_data VALUES ('2019-02-08','苹果','淘宝',1332.00); +INSERT INTO sales_data VALUES ('2019-02-08','苹果','京东',2088.00); +INSERT INTO sales_data VALUES ('2019-02-08','苹果','店面',2147.00); +INSERT INTO sales_data VALUES ('2019-02-09','桔子','淘宝',874.00); +INSERT INTO sales_data VALUES ('2019-02-09','桔子','京东',2069.00); +INSERT INTO sales_data VALUES ('2019-02-09','桔子','店面',1876.00); +INSERT INTO sales_data VALUES ('2019-02-09','香蕉','淘宝',1909.00); +INSERT INTO sales_data VALUES ('2019-02-09','香蕉','京东',2094.00); +INSERT INTO sales_data VALUES ('2019-02-09','香蕉','店面',845.00); +INSERT INTO sales_data VALUES ('2019-02-09','苹果','淘宝',721.00); +INSERT INTO sales_data VALUES ('2019-02-09','苹果','京东',912.00); +INSERT INTO sales_data VALUES ('2019-02-09','苹果','店面',850.00); +INSERT INTO sales_data VALUES ('2019-02-10','桔子','淘宝',778.00); +INSERT INTO sales_data VALUES ('2019-02-10','桔子','京东',2048.00); +INSERT INTO sales_data VALUES ('2019-02-10','桔子','店面',813.00); +INSERT INTO sales_data VALUES ('2019-02-10','香蕉','淘宝',1386.00); +INSERT INTO sales_data VALUES ('2019-02-10','香蕉','京东',761.00); +INSERT INTO sales_data VALUES ('2019-02-10','香蕉','店面',1252.00); +INSERT INTO sales_data VALUES ('2019-02-10','苹果','淘宝',976.00); +INSERT INTO sales_data VALUES ('2019-02-10','苹果','京东',1324.00); +INSERT INTO sales_data VALUES ('2019-02-10','苹果','店面',1930.00); +INSERT INTO sales_data VALUES ('2019-02-11','桔子','淘宝',1965.00); +INSERT INTO sales_data VALUES ('2019-02-11','桔子','京东',1258.00); +INSERT INTO sales_data VALUES ('2019-02-11','桔子','店面',1189.00); +INSERT INTO sales_data VALUES ('2019-02-11','香蕉','淘宝',2013.00); +INSERT INTO sales_data VALUES ('2019-02-11','香蕉','京东',716.00); +INSERT INTO sales_data VALUES ('2019-02-11','香蕉','店面',2199.00); +INSERT INTO sales_data VALUES ('2019-02-11','苹果','淘宝',1703.00); +INSERT INTO sales_data VALUES ('2019-02-11','苹果','京东',1267.00); +INSERT INTO sales_data VALUES ('2019-02-11','苹果','店面',1031.00); +INSERT INTO sales_data VALUES ('2019-02-12','桔子','淘宝',1029.00); +INSERT INTO sales_data VALUES ('2019-02-12','桔子','京东',1914.00); +INSERT INTO sales_data VALUES ('2019-02-12','桔子','店面',934.00); +INSERT INTO sales_data VALUES ('2019-02-12','香蕉','淘宝',986.00); +INSERT INTO sales_data VALUES ('2019-02-12','香蕉','京东',2093.00); +INSERT INTO sales_data VALUES ('2019-02-12','香蕉','店面',808.00); +INSERT INTO sales_data VALUES ('2019-02-12','苹果','淘宝',2167.00); +INSERT INTO sales_data VALUES ('2019-02-12','苹果','京东',1807.00); +INSERT INTO sales_data VALUES ('2019-02-12','苹果','店面',2207.00); +INSERT INTO sales_data VALUES ('2019-02-13','桔子','淘宝',822.00); +INSERT INTO sales_data VALUES ('2019-02-13','桔子','京东',1838.00); +INSERT INTO sales_data VALUES ('2019-02-13','桔子','店面',929.00); +INSERT INTO sales_data VALUES ('2019-02-13','香蕉','淘宝',977.00); +INSERT INTO sales_data VALUES ('2019-02-13','香蕉','京东',1916.00); +INSERT INTO sales_data VALUES ('2019-02-13','香蕉','店面',777.00); +INSERT INTO sales_data VALUES ('2019-02-13','苹果','淘宝',1091.00); +INSERT INTO sales_data VALUES ('2019-02-13','苹果','京东',1102.00); +INSERT INTO sales_data VALUES ('2019-02-13','苹果','店面',837.00); +INSERT INTO sales_data VALUES ('2019-02-14','桔子','淘宝',1648.00); +INSERT INTO sales_data VALUES ('2019-02-14','桔子','京东',1383.00); +INSERT INTO sales_data VALUES ('2019-02-14','桔子','店面',1466.00); +INSERT INTO sales_data VALUES ('2019-02-14','香蕉','淘宝',1378.00); +INSERT INTO sales_data VALUES ('2019-02-14','香蕉','京东',1144.00); +INSERT INTO sales_data VALUES ('2019-02-14','香蕉','店面',2019.00); +INSERT INTO sales_data VALUES ('2019-02-14','苹果','淘宝',1862.00); +INSERT INTO sales_data VALUES ('2019-02-14','苹果','京东',952.00); +INSERT INTO sales_data VALUES ('2019-02-14','苹果','店面',2029.00); +INSERT INTO sales_data VALUES ('2019-02-15','桔子','淘宝',1861.00); +INSERT INTO sales_data VALUES ('2019-02-15','桔子','京东',1955.00); +INSERT INTO sales_data VALUES ('2019-02-15','桔子','店面',1096.00); +INSERT INTO sales_data VALUES ('2019-02-15','香蕉','淘宝',2187.00); +INSERT INTO sales_data VALUES ('2019-02-15','香蕉','京东',774.00); +INSERT INTO sales_data VALUES ('2019-02-15','香蕉','店面',800.00); +INSERT INTO sales_data VALUES ('2019-02-15','苹果','淘宝',911.00); +INSERT INTO sales_data VALUES ('2019-02-15','苹果','京东',1050.00); +INSERT INTO sales_data VALUES ('2019-02-15','苹果','店面',2184.00); +INSERT INTO sales_data VALUES ('2019-02-16','桔子','淘宝',1013.00); +INSERT INTO sales_data VALUES ('2019-02-16','桔子','京东',1012.00); +INSERT INTO sales_data VALUES ('2019-02-16','桔子','店面',1786.00); +INSERT INTO sales_data VALUES ('2019-02-16','香蕉','淘宝',1010.00); +INSERT INTO sales_data VALUES ('2019-02-16','香蕉','京东',1119.00); +INSERT INTO sales_data VALUES ('2019-02-16','香蕉','店面',1408.00); +INSERT INTO sales_data VALUES ('2019-02-16','苹果','淘宝',1224.00); +INSERT INTO sales_data VALUES ('2019-02-16','苹果','京东',1382.00); +INSERT INTO sales_data VALUES ('2019-02-16','苹果','店面',1109.00); +INSERT INTO sales_data VALUES ('2019-02-17','桔子','淘宝',1290.00); +INSERT INTO sales_data VALUES ('2019-02-17','桔子','京东',1762.00); +INSERT INTO sales_data VALUES ('2019-02-17','桔子','店面',1501.00); +INSERT INTO sales_data VALUES ('2019-02-17','香蕉','淘宝',1413.00); +INSERT INTO sales_data VALUES ('2019-02-17','香蕉','京东',1190.00); +INSERT INTO sales_data VALUES ('2019-02-17','香蕉','店面',2165.00); +INSERT INTO sales_data VALUES ('2019-02-17','苹果','淘宝',2159.00); +INSERT INTO sales_data VALUES ('2019-02-17','苹果','京东',1848.00); +INSERT INTO sales_data VALUES ('2019-02-17','苹果','店面',1088.00); +INSERT INTO sales_data VALUES ('2019-02-18','桔子','淘宝',1963.00); +INSERT INTO sales_data VALUES ('2019-02-18','桔子','京东',1496.00); +INSERT INTO sales_data VALUES ('2019-02-18','桔子','店面',1325.00); +INSERT INTO sales_data VALUES ('2019-02-18','香蕉','淘宝',1772.00); +INSERT INTO sales_data VALUES ('2019-02-18','香蕉','京东',1132.00); +INSERT INTO sales_data VALUES ('2019-02-18','香蕉','店面',1055.00); +INSERT INTO sales_data VALUES ('2019-02-18','苹果','淘宝',2143.00); +INSERT INTO sales_data VALUES ('2019-02-18','苹果','京东',1094.00); +INSERT INTO sales_data VALUES ('2019-02-18','苹果','店面',1104.00); +INSERT INTO sales_data VALUES ('2019-02-19','桔子','淘宝',2224.00); +INSERT INTO sales_data VALUES ('2019-02-19','桔子','京东',1285.00); +INSERT INTO sales_data VALUES ('2019-02-19','桔子','店面',1434.00); +INSERT INTO sales_data VALUES ('2019-02-19','香蕉','淘宝',2182.00); +INSERT INTO sales_data VALUES ('2019-02-19','香蕉','京东',1568.00); +INSERT INTO sales_data VALUES ('2019-02-19','香蕉','店面',1716.00); +INSERT INTO sales_data VALUES ('2019-02-19','苹果','淘宝',1738.00); +INSERT INTO sales_data VALUES ('2019-02-19','苹果','京东',1848.00); +INSERT INTO sales_data VALUES ('2019-02-19','苹果','店面',2106.00); +INSERT INTO sales_data VALUES ('2019-02-20','桔子','淘宝',921.00); +INSERT INTO sales_data VALUES ('2019-02-20','桔子','京东',847.00); +INSERT INTO sales_data VALUES ('2019-02-20','桔子','店面',1262.00); +INSERT INTO sales_data VALUES ('2019-02-20','香蕉','淘宝',1300.00); +INSERT INTO sales_data VALUES ('2019-02-20','香蕉','京东',1402.00); +INSERT INTO sales_data VALUES ('2019-02-20','香蕉','店面',789.00); +INSERT INTO sales_data VALUES ('2019-02-20','苹果','淘宝',2067.00); +INSERT INTO sales_data VALUES ('2019-02-20','苹果','京东',2080.00); +INSERT INTO sales_data VALUES ('2019-02-20','苹果','店面',1244.00); +INSERT INTO sales_data VALUES ('2019-02-21','桔子','淘宝',2002.00); +INSERT INTO sales_data VALUES ('2019-02-21','桔子','京东',2009.00); +INSERT INTO sales_data VALUES ('2019-02-21','桔子','店面',863.00); +INSERT INTO sales_data VALUES ('2019-02-21','香蕉','淘宝',855.00); +INSERT INTO sales_data VALUES ('2019-02-21','香蕉','京东',1731.00); +INSERT INTO sales_data VALUES ('2019-02-21','香蕉','店面',1618.00); +INSERT INTO sales_data VALUES ('2019-02-21','苹果','淘宝',1440.00); +INSERT INTO sales_data VALUES ('2019-02-21','苹果','京东',1263.00); +INSERT INTO sales_data VALUES ('2019-02-21','苹果','店面',2010.00); +INSERT INTO sales_data VALUES ('2019-02-22','桔子','淘宝',1761.00); +INSERT INTO sales_data VALUES ('2019-02-22','桔子','京东',1171.00); +INSERT INTO sales_data VALUES ('2019-02-22','桔子','店面',869.00); +INSERT INTO sales_data VALUES ('2019-02-22','香蕉','淘宝',2125.00); +INSERT INTO sales_data VALUES ('2019-02-22','香蕉','京东',1150.00); +INSERT INTO sales_data VALUES ('2019-02-22','香蕉','店面',1409.00); +INSERT INTO sales_data VALUES ('2019-02-22','苹果','淘宝',1314.00); +INSERT INTO sales_data VALUES ('2019-02-22','苹果','京东',1087.00); +INSERT INTO sales_data VALUES ('2019-02-22','苹果','店面',2232.00); +INSERT INTO sales_data VALUES ('2019-02-23','桔子','淘宝',790.00); +INSERT INTO sales_data VALUES ('2019-02-23','桔子','京东',2085.00); +INSERT INTO sales_data VALUES ('2019-02-23','桔子','店面',1840.00); +INSERT INTO sales_data VALUES ('2019-02-23','香蕉','淘宝',2151.00); +INSERT INTO sales_data VALUES ('2019-02-23','香蕉','京东',2257.00); +INSERT INTO sales_data VALUES ('2019-02-23','香蕉','店面',1937.00); +INSERT INTO sales_data VALUES ('2019-02-23','苹果','淘宝',1163.00); +INSERT INTO sales_data VALUES ('2019-02-23','苹果','京东',1307.00); +INSERT INTO sales_data VALUES ('2019-02-23','苹果','店面',1089.00); +INSERT INTO sales_data VALUES ('2019-02-24','桔子','淘宝',1208.00); +INSERT INTO sales_data VALUES ('2019-02-24','桔子','京东',1129.00); +INSERT INTO sales_data VALUES ('2019-02-24','桔子','店面',924.00); +INSERT INTO sales_data VALUES ('2019-02-24','香蕉','淘宝',1702.00); +INSERT INTO sales_data VALUES ('2019-02-24','香蕉','京东',875.00); +INSERT INTO sales_data VALUES ('2019-02-24','香蕉','店面',2178.00); +INSERT INTO sales_data VALUES ('2019-02-24','苹果','淘宝',1810.00); +INSERT INTO sales_data VALUES ('2019-02-24','苹果','京东',975.00); +INSERT INTO sales_data VALUES ('2019-02-24','苹果','店面',1655.00); +INSERT INTO sales_data VALUES ('2019-02-25','桔子','淘宝',1178.00); +INSERT INTO sales_data VALUES ('2019-02-25','桔子','京东',1666.00); +INSERT INTO sales_data VALUES ('2019-02-25','桔子','店面',2168.00); +INSERT INTO sales_data VALUES ('2019-02-25','香蕉','淘宝',933.00); +INSERT INTO sales_data VALUES ('2019-02-25','香蕉','京东',1166.00); +INSERT INTO sales_data VALUES ('2019-02-25','香蕉','店面',1079.00); +INSERT INTO sales_data VALUES ('2019-02-25','苹果','淘宝',1042.00); +INSERT INTO sales_data VALUES ('2019-02-25','苹果','京东',1031.00); +INSERT INTO sales_data VALUES ('2019-02-25','苹果','店面',1469.00); +INSERT INTO sales_data VALUES ('2019-02-26','桔子','淘宝',1695.00); +INSERT INTO sales_data VALUES ('2019-02-26','桔子','京东',1590.00); +INSERT INTO sales_data VALUES ('2019-02-26','桔子','店面',1802.00); +INSERT INTO sales_data VALUES ('2019-02-26','香蕉','淘宝',1667.00); +INSERT INTO sales_data VALUES ('2019-02-26','香蕉','京东',1615.00); +INSERT INTO sales_data VALUES ('2019-02-26','香蕉','店面',1622.00); +INSERT INTO sales_data VALUES ('2019-02-26','苹果','淘宝',1242.00); +INSERT INTO sales_data VALUES ('2019-02-26','苹果','京东',1501.00); +INSERT INTO sales_data VALUES ('2019-02-26','苹果','店面',1614.00); +INSERT INTO sales_data VALUES ('2019-02-27','桔子','淘宝',919.00); +INSERT INTO sales_data VALUES ('2019-02-27','桔子','京东',1904.00); +INSERT INTO sales_data VALUES ('2019-02-27','桔子','店面',2161.00); +INSERT INTO sales_data VALUES ('2019-02-27','香蕉','淘宝',1243.00); +INSERT INTO sales_data VALUES ('2019-02-27','香蕉','京东',842.00); +INSERT INTO sales_data VALUES ('2019-02-27','香蕉','店面',1019.00); +INSERT INTO sales_data VALUES ('2019-02-27','苹果','淘宝',1397.00); +INSERT INTO sales_data VALUES ('2019-02-27','苹果','京东',1774.00); +INSERT INTO sales_data VALUES ('2019-02-27','苹果','店面',1125.00); +INSERT INTO sales_data VALUES ('2019-02-28','桔子','淘宝',1310.00); +INSERT INTO sales_data VALUES ('2019-02-28','桔子','京东',1319.00); +INSERT INTO sales_data VALUES ('2019-02-28','桔子','店面',1335.00); +INSERT INTO sales_data VALUES ('2019-02-28','香蕉','淘宝',2195.00); +INSERT INTO sales_data VALUES ('2019-02-28','香蕉','京东',1721.00); +INSERT INTO sales_data VALUES ('2019-02-28','香蕉','店面',2226.00); +INSERT INTO sales_data VALUES ('2019-02-28','苹果','淘宝',2088.00); +INSERT INTO sales_data VALUES ('2019-02-28','苹果','京东',1879.00); +INSERT INTO sales_data VALUES ('2019-02-28','苹果','店面',1117.00); +INSERT INTO sales_data VALUES ('2019-03-01','桔子','淘宝',897.00); +INSERT INTO sales_data VALUES ('2019-03-01','桔子','京东',2151.00); +INSERT INTO sales_data VALUES ('2019-03-01','桔子','店面',1378.00); +INSERT INTO sales_data VALUES ('2019-03-01','香蕉','淘宝',1591.00); +INSERT INTO sales_data VALUES ('2019-03-01','香蕉','京东',1566.00); +INSERT INTO sales_data VALUES ('2019-03-01','香蕉','店面',2187.00); +INSERT INTO sales_data VALUES ('2019-03-01','苹果','淘宝',1113.00); +INSERT INTO sales_data VALUES ('2019-03-01','苹果','京东',953.00); +INSERT INTO sales_data VALUES ('2019-03-01','苹果','店面',1522.00); +INSERT INTO sales_data VALUES ('2019-03-02','桔子','淘宝',1960.00); +INSERT INTO sales_data VALUES ('2019-03-02','桔子','京东',1420.00); +INSERT INTO sales_data VALUES ('2019-03-02','桔子','店面',2248.00); +INSERT INTO sales_data VALUES ('2019-03-02','香蕉','淘宝',1294.00); +INSERT INTO sales_data VALUES ('2019-03-02','香蕉','京东',1554.00); +INSERT INTO sales_data VALUES ('2019-03-02','香蕉','店面',1868.00); +INSERT INTO sales_data VALUES ('2019-03-02','苹果','淘宝',1169.00); +INSERT INTO sales_data VALUES ('2019-03-02','苹果','京东',2012.00); +INSERT INTO sales_data VALUES ('2019-03-02','苹果','店面',1924.00); +INSERT INTO sales_data VALUES ('2019-03-03','桔子','淘宝',1409.00); +INSERT INTO sales_data VALUES ('2019-03-03','桔子','京东',1129.00); +INSERT INTO sales_data VALUES ('2019-03-03','桔子','店面',1418.00); +INSERT INTO sales_data VALUES ('2019-03-03','香蕉','淘宝',1748.00); +INSERT INTO sales_data VALUES ('2019-03-03','香蕉','京东',1649.00); +INSERT INTO sales_data VALUES ('2019-03-03','香蕉','店面',1947.00); +INSERT INTO sales_data VALUES ('2019-03-03','苹果','淘宝',2294.00); +INSERT INTO sales_data VALUES ('2019-03-03','苹果','京东',1554.00); +INSERT INTO sales_data VALUES ('2019-03-03','苹果','店面',1378.00); +INSERT INTO sales_data VALUES ('2019-03-04','桔子','淘宝',2234.00); +INSERT INTO sales_data VALUES ('2019-03-04','桔子','京东',1357.00); +INSERT INTO sales_data VALUES ('2019-03-04','桔子','店面',972.00); +INSERT INTO sales_data VALUES ('2019-03-04','香蕉','淘宝',1061.00); +INSERT INTO sales_data VALUES ('2019-03-04','香蕉','京东',1459.00); +INSERT INTO sales_data VALUES ('2019-03-04','香蕉','店面',828.00); +INSERT INTO sales_data VALUES ('2019-03-04','苹果','淘宝',1644.00); +INSERT INTO sales_data VALUES ('2019-03-04','苹果','京东',2255.00); +INSERT INTO sales_data VALUES ('2019-03-04','苹果','店面',1599.00); +INSERT INTO sales_data VALUES ('2019-03-05','桔子','淘宝',1542.00); +INSERT INTO sales_data VALUES ('2019-03-05','桔子','京东',1078.00); +INSERT INTO sales_data VALUES ('2019-03-05','桔子','店面',1762.00); +INSERT INTO sales_data VALUES ('2019-03-05','香蕉','淘宝',2269.00); +INSERT INTO sales_data VALUES ('2019-03-05','香蕉','京东',2238.00); +INSERT INTO sales_data VALUES ('2019-03-05','香蕉','店面',882.00); +INSERT INTO sales_data VALUES ('2019-03-05','苹果','淘宝',2217.00); +INSERT INTO sales_data VALUES ('2019-03-05','苹果','京东',1232.00); +INSERT INTO sales_data VALUES ('2019-03-05','苹果','店面',1636.00); +INSERT INTO sales_data VALUES ('2019-03-06','桔子','淘宝',1790.00); +INSERT INTO sales_data VALUES ('2019-03-06','桔子','京东',1606.00); +INSERT INTO sales_data VALUES ('2019-03-06','桔子','店面',1352.00); +INSERT INTO sales_data VALUES ('2019-03-06','香蕉','淘宝',1414.00); +INSERT INTO sales_data VALUES ('2019-03-06','香蕉','京东',2210.00); +INSERT INTO sales_data VALUES ('2019-03-06','香蕉','店面',1676.00); +INSERT INTO sales_data VALUES ('2019-03-06','苹果','淘宝',2028.00); +INSERT INTO sales_data VALUES ('2019-03-06','苹果','京东',1653.00); +INSERT INTO sales_data VALUES ('2019-03-06','苹果','店面',1020.00); +INSERT INTO sales_data VALUES ('2019-03-07','桔子','淘宝',1675.00); +INSERT INTO sales_data VALUES ('2019-03-07','桔子','京东',1647.00); +INSERT INTO sales_data VALUES ('2019-03-07','桔子','店面',1775.00); +INSERT INTO sales_data VALUES ('2019-03-07','香蕉','淘宝',2248.00); +INSERT INTO sales_data VALUES ('2019-03-07','香蕉','京东',1571.00); +INSERT INTO sales_data VALUES ('2019-03-07','香蕉','店面',2321.00); +INSERT INTO sales_data VALUES ('2019-03-07','苹果','淘宝',910.00); +INSERT INTO sales_data VALUES ('2019-03-07','苹果','京东',1822.00); +INSERT INTO sales_data VALUES ('2019-03-07','苹果','店面',1470.00); +INSERT INTO sales_data VALUES ('2019-03-08','桔子','淘宝',933.00); +INSERT INTO sales_data VALUES ('2019-03-08','桔子','京东',1161.00); +INSERT INTO sales_data VALUES ('2019-03-08','桔子','店面',1420.00); +INSERT INTO sales_data VALUES ('2019-03-08','香蕉','淘宝',1722.00); +INSERT INTO sales_data VALUES ('2019-03-08','香蕉','京东',1888.00); +INSERT INTO sales_data VALUES ('2019-03-08','香蕉','店面',1683.00); +INSERT INTO sales_data VALUES ('2019-03-08','苹果','淘宝',1169.00); +INSERT INTO sales_data VALUES ('2019-03-08','苹果','京东',1842.00); +INSERT INTO sales_data VALUES ('2019-03-08','苹果','店面',1606.00); +INSERT INTO sales_data VALUES ('2019-03-09','桔子','淘宝',1241.00); +INSERT INTO sales_data VALUES ('2019-03-09','桔子','京东',1749.00); +INSERT INTO sales_data VALUES ('2019-03-09','桔子','店面',2028.00); +INSERT INTO sales_data VALUES ('2019-03-09','香蕉','淘宝',2061.00); +INSERT INTO sales_data VALUES ('2019-03-09','香蕉','京东',1219.00); +INSERT INTO sales_data VALUES ('2019-03-09','香蕉','店面',1314.00); +INSERT INTO sales_data VALUES ('2019-03-09','苹果','淘宝',1094.00); +INSERT INTO sales_data VALUES ('2019-03-09','苹果','京东',1813.00); +INSERT INTO sales_data VALUES ('2019-03-09','苹果','店面',1203.00); +INSERT INTO sales_data VALUES ('2019-03-10','桔子','淘宝',1955.00); +INSERT INTO sales_data VALUES ('2019-03-10','桔子','京东',1526.00); +INSERT INTO sales_data VALUES ('2019-03-10','桔子','店面',2041.00); +INSERT INTO sales_data VALUES ('2019-03-10','香蕉','淘宝',2155.00); +INSERT INTO sales_data VALUES ('2019-03-10','香蕉','京东',875.00); +INSERT INTO sales_data VALUES ('2019-03-10','香蕉','店面',1363.00); +INSERT INTO sales_data VALUES ('2019-03-10','苹果','淘宝',1605.00); +INSERT INTO sales_data VALUES ('2019-03-10','苹果','京东',2298.00); +INSERT INTO sales_data VALUES ('2019-03-10','苹果','店面',2109.00); +INSERT INTO sales_data VALUES ('2019-03-11','桔子','淘宝',1606.00); +INSERT INTO sales_data VALUES ('2019-03-11','桔子','京东',888.00); +INSERT INTO sales_data VALUES ('2019-03-11','桔子','店面',1611.00); +INSERT INTO sales_data VALUES ('2019-03-11','香蕉','淘宝',2251.00); +INSERT INTO sales_data VALUES ('2019-03-11','香蕉','京东',991.00); +INSERT INTO sales_data VALUES ('2019-03-11','香蕉','店面',1942.00); +INSERT INTO sales_data VALUES ('2019-03-11','苹果','淘宝',1341.00); +INSERT INTO sales_data VALUES ('2019-03-11','苹果','京东',1883.00); +INSERT INTO sales_data VALUES ('2019-03-11','苹果','店面',1500.00); +INSERT INTO sales_data VALUES ('2019-03-12','桔子','淘宝',2199.00); +INSERT INTO sales_data VALUES ('2019-03-12','桔子','京东',2226.00); +INSERT INTO sales_data VALUES ('2019-03-12','桔子','店面',1017.00); +INSERT INTO sales_data VALUES ('2019-03-12','香蕉','淘宝',1476.00); +INSERT INTO sales_data VALUES ('2019-03-12','香蕉','京东',1132.00); +INSERT INTO sales_data VALUES ('2019-03-12','香蕉','店面',1931.00); +INSERT INTO sales_data VALUES ('2019-03-12','苹果','淘宝',1168.00); +INSERT INTO sales_data VALUES ('2019-03-12','苹果','京东',858.00); +INSERT INTO sales_data VALUES ('2019-03-12','苹果','店面',2314.00); +INSERT INTO sales_data VALUES ('2019-03-13','桔子','淘宝',1652.00); +INSERT INTO sales_data VALUES ('2019-03-13','桔子','京东',1122.00); +INSERT INTO sales_data VALUES ('2019-03-13','桔子','店面',1797.00); +INSERT INTO sales_data VALUES ('2019-03-13','香蕉','淘宝',2020.00); +INSERT INTO sales_data VALUES ('2019-03-13','香蕉','京东',2237.00); +INSERT INTO sales_data VALUES ('2019-03-13','香蕉','店面',983.00); +INSERT INTO sales_data VALUES ('2019-03-13','苹果','淘宝',1721.00); +INSERT INTO sales_data VALUES ('2019-03-13','苹果','京东',2053.00); +INSERT INTO sales_data VALUES ('2019-03-13','苹果','店面',1018.00); +INSERT INTO sales_data VALUES ('2019-03-14','桔子','淘宝',2249.00); +INSERT INTO sales_data VALUES ('2019-03-14','桔子','京东',1323.00); +INSERT INTO sales_data VALUES ('2019-03-14','桔子','店面',982.00); +INSERT INTO sales_data VALUES ('2019-03-14','香蕉','淘宝',2018.00); +INSERT INTO sales_data VALUES ('2019-03-14','香蕉','京东',2084.00); +INSERT INTO sales_data VALUES ('2019-03-14','香蕉','店面',1025.00); +INSERT INTO sales_data VALUES ('2019-03-14','苹果','淘宝',1284.00); +INSERT INTO sales_data VALUES ('2019-03-14','苹果','京东',1990.00); +INSERT INTO sales_data VALUES ('2019-03-14','苹果','店面',1171.00); +INSERT INTO sales_data VALUES ('2019-03-15','桔子','淘宝',886.00); +INSERT INTO sales_data VALUES ('2019-03-15','桔子','京东',992.00); +INSERT INTO sales_data VALUES ('2019-03-15','桔子','店面',2214.00); +INSERT INTO sales_data VALUES ('2019-03-15','香蕉','淘宝',1541.00); +INSERT INTO sales_data VALUES ('2019-03-15','香蕉','京东',2341.00); +INSERT INTO sales_data VALUES ('2019-03-15','香蕉','店面',2090.00); +INSERT INTO sales_data VALUES ('2019-03-15','苹果','淘宝',1708.00); +INSERT INTO sales_data VALUES ('2019-03-15','苹果','京东',1467.00); +INSERT INTO sales_data VALUES ('2019-03-15','苹果','店面',872.00); +INSERT INTO sales_data VALUES ('2019-03-16','桔子','淘宝',1293.00); +INSERT INTO sales_data VALUES ('2019-03-16','桔子','京东',1790.00); +INSERT INTO sales_data VALUES ('2019-03-16','桔子','店面',885.00); +INSERT INTO sales_data VALUES ('2019-03-16','香蕉','淘宝',1258.00); +INSERT INTO sales_data VALUES ('2019-03-16','香蕉','京东',1087.00); +INSERT INTO sales_data VALUES ('2019-03-16','香蕉','店面',1153.00); +INSERT INTO sales_data VALUES ('2019-03-16','苹果','淘宝',2200.00); +INSERT INTO sales_data VALUES ('2019-03-16','苹果','京东',2252.00); +INSERT INTO sales_data VALUES ('2019-03-16','苹果','店面',1035.00); +INSERT INTO sales_data VALUES ('2019-03-17','桔子','淘宝',2333.00); +INSERT INTO sales_data VALUES ('2019-03-17','桔子','京东',1624.00); +INSERT INTO sales_data VALUES ('2019-03-17','桔子','店面',2238.00); +INSERT INTO sales_data VALUES ('2019-03-17','香蕉','淘宝',996.00); +INSERT INTO sales_data VALUES ('2019-03-17','香蕉','京东',1513.00); +INSERT INTO sales_data VALUES ('2019-03-17','香蕉','店面',1200.00); +INSERT INTO sales_data VALUES ('2019-03-17','苹果','淘宝',1118.00); +INSERT INTO sales_data VALUES ('2019-03-17','苹果','京东',1171.00); +INSERT INTO sales_data VALUES ('2019-03-17','苹果','店面',924.00); +INSERT INTO sales_data VALUES ('2019-03-18','桔子','淘宝',1288.00); +INSERT INTO sales_data VALUES ('2019-03-18','桔子','京东',1600.00); +INSERT INTO sales_data VALUES ('2019-03-18','桔子','店面',2059.00); +INSERT INTO sales_data VALUES ('2019-03-18','香蕉','淘宝',1599.00); +INSERT INTO sales_data VALUES ('2019-03-18','香蕉','京东',1622.00); +INSERT INTO sales_data VALUES ('2019-03-18','香蕉','店面',2186.00); +INSERT INTO sales_data VALUES ('2019-03-18','苹果','淘宝',1448.00); +INSERT INTO sales_data VALUES ('2019-03-18','苹果','京东',2298.00); +INSERT INTO sales_data VALUES ('2019-03-18','苹果','店面',2162.00); +INSERT INTO sales_data VALUES ('2019-03-19','桔子','淘宝',1178.00); +INSERT INTO sales_data VALUES ('2019-03-19','桔子','京东',1646.00); +INSERT INTO sales_data VALUES ('2019-03-19','桔子','店面',1268.00); +INSERT INTO sales_data VALUES ('2019-03-19','香蕉','淘宝',1185.00); +INSERT INTO sales_data VALUES ('2019-03-19','香蕉','京东',2069.00); +INSERT INTO sales_data VALUES ('2019-03-19','香蕉','店面',2188.00); +INSERT INTO sales_data VALUES ('2019-03-19','苹果','淘宝',1200.00); +INSERT INTO sales_data VALUES ('2019-03-19','苹果','京东',957.00); +INSERT INTO sales_data VALUES ('2019-03-19','苹果','店面',905.00); +INSERT INTO sales_data VALUES ('2019-03-20','桔子','淘宝',1488.00); +INSERT INTO sales_data VALUES ('2019-03-20','桔子','京东',2292.00); +INSERT INTO sales_data VALUES ('2019-03-20','桔子','店面',2292.00); +INSERT INTO sales_data VALUES ('2019-03-20','香蕉','淘宝',1653.00); +INSERT INTO sales_data VALUES ('2019-03-20','香蕉','京东',2250.00); +INSERT INTO sales_data VALUES ('2019-03-20','香蕉','店面',1540.00); +INSERT INTO sales_data VALUES ('2019-03-20','苹果','淘宝',1516.00); +INSERT INTO sales_data VALUES ('2019-03-20','苹果','京东',2371.00); +INSERT INTO sales_data VALUES ('2019-03-20','苹果','店面',2178.00); +INSERT INTO sales_data VALUES ('2019-03-21','桔子','淘宝',1846.00); +INSERT INTO sales_data VALUES ('2019-03-21','桔子','京东',1119.00); +INSERT INTO sales_data VALUES ('2019-03-21','桔子','店面',980.00); +INSERT INTO sales_data VALUES ('2019-03-21','香蕉','淘宝',1895.00); +INSERT INTO sales_data VALUES ('2019-03-21','香蕉','京东',1527.00); +INSERT INTO sales_data VALUES ('2019-03-21','香蕉','店面',1700.00); +INSERT INTO sales_data VALUES ('2019-03-21','苹果','淘宝',1574.00); +INSERT INTO sales_data VALUES ('2019-03-21','苹果','京东',2246.00); +INSERT INTO sales_data VALUES ('2019-03-21','苹果','店面',942.00); +INSERT INTO sales_data VALUES ('2019-03-22','桔子','淘宝',1384.00); +INSERT INTO sales_data VALUES ('2019-03-22','桔子','京东',1319.00); +INSERT INTO sales_data VALUES ('2019-03-22','桔子','店面',2365.00); +INSERT INTO sales_data VALUES ('2019-03-22','香蕉','淘宝',1166.00); +INSERT INTO sales_data VALUES ('2019-03-22','香蕉','京东',1612.00); +INSERT INTO sales_data VALUES ('2019-03-22','香蕉','店面',1626.00); +INSERT INTO sales_data VALUES ('2019-03-22','苹果','淘宝',1549.00); +INSERT INTO sales_data VALUES ('2019-03-22','苹果','京东',1912.00); +INSERT INTO sales_data VALUES ('2019-03-22','苹果','店面',1311.00); +INSERT INTO sales_data VALUES ('2019-03-23','桔子','淘宝',1357.00); +INSERT INTO sales_data VALUES ('2019-03-23','桔子','京东',2232.00); +INSERT INTO sales_data VALUES ('2019-03-23','桔子','店面',1388.00); +INSERT INTO sales_data VALUES ('2019-03-23','香蕉','淘宝',1377.00); +INSERT INTO sales_data VALUES ('2019-03-23','香蕉','京东',1330.00); +INSERT INTO sales_data VALUES ('2019-03-23','香蕉','店面',1290.00); +INSERT INTO sales_data VALUES ('2019-03-23','苹果','淘宝',1279.00); +INSERT INTO sales_data VALUES ('2019-03-23','苹果','京东',2093.00); +INSERT INTO sales_data VALUES ('2019-03-23','苹果','店面',1150.00); +INSERT INTO sales_data VALUES ('2019-03-24','桔子','淘宝',1935.00); +INSERT INTO sales_data VALUES ('2019-03-24','桔子','京东',1224.00); +INSERT INTO sales_data VALUES ('2019-03-24','桔子','店面',1136.00); +INSERT INTO sales_data VALUES ('2019-03-24','香蕉','淘宝',1723.00); +INSERT INTO sales_data VALUES ('2019-03-24','香蕉','京东',2174.00); +INSERT INTO sales_data VALUES ('2019-03-24','香蕉','店面',1360.00); +INSERT INTO sales_data VALUES ('2019-03-24','苹果','淘宝',1808.00); +INSERT INTO sales_data VALUES ('2019-03-24','苹果','京东',1674.00); +INSERT INTO sales_data VALUES ('2019-03-24','苹果','店面',1992.00); +INSERT INTO sales_data VALUES ('2019-03-25','桔子','淘宝',1118.00); +INSERT INTO sales_data VALUES ('2019-03-25','桔子','京东',2358.00); +INSERT INTO sales_data VALUES ('2019-03-25','桔子','店面',1848.00); +INSERT INTO sales_data VALUES ('2019-03-25','香蕉','淘宝',1165.00); +INSERT INTO sales_data VALUES ('2019-03-25','香蕉','京东',1342.00); +INSERT INTO sales_data VALUES ('2019-03-25','香蕉','店面',2266.00); +INSERT INTO sales_data VALUES ('2019-03-25','苹果','淘宝',1130.00); +INSERT INTO sales_data VALUES ('2019-03-25','苹果','京东',1608.00); +INSERT INTO sales_data VALUES ('2019-03-25','苹果','店面',1478.00); +INSERT INTO sales_data VALUES ('2019-03-26','桔子','淘宝',1862.00); +INSERT INTO sales_data VALUES ('2019-03-26','桔子','京东',2262.00); +INSERT INTO sales_data VALUES ('2019-03-26','桔子','店面',995.00); +INSERT INTO sales_data VALUES ('2019-03-26','香蕉','淘宝',2273.00); +INSERT INTO sales_data VALUES ('2019-03-26','香蕉','京东',1214.00); +INSERT INTO sales_data VALUES ('2019-03-26','香蕉','店面',2322.00); +INSERT INTO sales_data VALUES ('2019-03-26','苹果','淘宝',1256.00); +INSERT INTO sales_data VALUES ('2019-03-26','苹果','京东',1687.00); +INSERT INTO sales_data VALUES ('2019-03-26','苹果','店面',1247.00); +INSERT INTO sales_data VALUES ('2019-03-27','桔子','淘宝',1646.00); +INSERT INTO sales_data VALUES ('2019-03-27','桔子','京东',2066.00); +INSERT INTO sales_data VALUES ('2019-03-27','桔子','店面',940.00); +INSERT INTO sales_data VALUES ('2019-03-27','香蕉','淘宝',1891.00); +INSERT INTO sales_data VALUES ('2019-03-27','香蕉','京东',1590.00); +INSERT INTO sales_data VALUES ('2019-03-27','香蕉','店面',1254.00); +INSERT INTO sales_data VALUES ('2019-03-27','苹果','淘宝',2117.00); +INSERT INTO sales_data VALUES ('2019-03-27','苹果','京东',2403.00); +INSERT INTO sales_data VALUES ('2019-03-27','苹果','店面',1018.00); +INSERT INTO sales_data VALUES ('2019-03-28','桔子','淘宝',1072.00); +INSERT INTO sales_data VALUES ('2019-03-28','桔子','京东',1806.00); +INSERT INTO sales_data VALUES ('2019-03-28','桔子','店面',1787.00); +INSERT INTO sales_data VALUES ('2019-03-28','香蕉','淘宝',2154.00); +INSERT INTO sales_data VALUES ('2019-03-28','香蕉','京东',2009.00); +INSERT INTO sales_data VALUES ('2019-03-28','香蕉','店面',1730.00); +INSERT INTO sales_data VALUES ('2019-03-28','苹果','淘宝',1586.00); +INSERT INTO sales_data VALUES ('2019-03-28','苹果','京东',2259.00); +INSERT INTO sales_data VALUES ('2019-03-28','苹果','店面',2157.00); +INSERT INTO sales_data VALUES ('2019-03-29','桔子','淘宝',1443.00); +INSERT INTO sales_data VALUES ('2019-03-29','桔子','京东',979.00); +INSERT INTO sales_data VALUES ('2019-03-29','桔子','店面',1355.00); +INSERT INTO sales_data VALUES ('2019-03-29','香蕉','淘宝',2006.00); +INSERT INTO sales_data VALUES ('2019-03-29','香蕉','京东',1921.00); +INSERT INTO sales_data VALUES ('2019-03-29','香蕉','店面',1197.00); +INSERT INTO sales_data VALUES ('2019-03-29','苹果','淘宝',2081.00); +INSERT INTO sales_data VALUES ('2019-03-29','苹果','京东',1773.00); +INSERT INTO sales_data VALUES ('2019-03-29','苹果','店面',1492.00); +INSERT INTO sales_data VALUES ('2019-03-30','桔子','淘宝',1987.00); +INSERT INTO sales_data VALUES ('2019-03-30','桔子','京东',2114.00); +INSERT INTO sales_data VALUES ('2019-03-30','桔子','店面',2263.00); +INSERT INTO sales_data VALUES ('2019-03-30','香蕉','淘宝',2314.00); +INSERT INTO sales_data VALUES ('2019-03-30','香蕉','京东',1335.00); +INSERT INTO sales_data VALUES ('2019-03-30','香蕉','店面',1904.00); +INSERT INTO sales_data VALUES ('2019-03-30','苹果','淘宝',2329.00); +INSERT INTO sales_data VALUES ('2019-03-30','苹果','京东',2300.00); +INSERT INTO sales_data VALUES ('2019-03-30','苹果','店面',1069.00); +INSERT INTO sales_data VALUES ('2019-03-31','桔子','淘宝',1163.00); +INSERT INTO sales_data VALUES ('2019-03-31','桔子','京东',1997.00); +INSERT INTO sales_data VALUES ('2019-03-31','桔子','店面',1052.00); +INSERT INTO sales_data VALUES ('2019-03-31','香蕉','淘宝',1256.00); +INSERT INTO sales_data VALUES ('2019-03-31','香蕉','京东',2139.00); +INSERT INTO sales_data VALUES ('2019-03-31','香蕉','店面',1928.00); +INSERT INTO sales_data VALUES ('2019-03-31','苹果','淘宝',2113.00); +INSERT INTO sales_data VALUES ('2019-03-31','苹果','京东',1863.00); +INSERT INTO sales_data VALUES ('2019-03-31','苹果','店面',1507.00); +INSERT INTO sales_data VALUES ('2019-04-01','桔子','淘宝',1418.00); +INSERT INTO sales_data VALUES ('2019-04-01','桔子','京东',1024.00); +INSERT INTO sales_data VALUES ('2019-04-01','桔子','店面',1341.00); +INSERT INTO sales_data VALUES ('2019-04-01','香蕉','淘宝',1145.00); +INSERT INTO sales_data VALUES ('2019-04-01','香蕉','京东',1532.00); +INSERT INTO sales_data VALUES ('2019-04-01','香蕉','店面',1385.00); +INSERT INTO sales_data VALUES ('2019-04-01','苹果','淘宝',1565.00); +INSERT INTO sales_data VALUES ('2019-04-01','苹果','京东',1103.00); +INSERT INTO sales_data VALUES ('2019-04-01','苹果','店面',2371.00); +INSERT INTO sales_data VALUES ('2019-04-02','桔子','淘宝',1832.00); +INSERT INTO sales_data VALUES ('2019-04-02','桔子','京东',2253.00); +INSERT INTO sales_data VALUES ('2019-04-02','桔子','店面',1714.00); +INSERT INTO sales_data VALUES ('2019-04-02','香蕉','淘宝',2389.00); +INSERT INTO sales_data VALUES ('2019-04-02','香蕉','京东',1801.00); +INSERT INTO sales_data VALUES ('2019-04-02','香蕉','店面',1388.00); +INSERT INTO sales_data VALUES ('2019-04-02','苹果','淘宝',2212.00); +INSERT INTO sales_data VALUES ('2019-04-02','苹果','京东',1675.00); +INSERT INTO sales_data VALUES ('2019-04-02','苹果','店面',1783.00); +INSERT INTO sales_data VALUES ('2019-04-03','桔子','淘宝',1681.00); +INSERT INTO sales_data VALUES ('2019-04-03','桔子','京东',1570.00); +INSERT INTO sales_data VALUES ('2019-04-03','桔子','店面',1648.00); +INSERT INTO sales_data VALUES ('2019-04-03','香蕉','淘宝',1810.00); +INSERT INTO sales_data VALUES ('2019-04-03','香蕉','京东',1787.00); +INSERT INTO sales_data VALUES ('2019-04-03','香蕉','店面',1200.00); +INSERT INTO sales_data VALUES ('2019-04-03','苹果','淘宝',1917.00); +INSERT INTO sales_data VALUES ('2019-04-03','苹果','京东',2098.00); +INSERT INTO sales_data VALUES ('2019-04-03','苹果','店面',2394.00); +INSERT INTO sales_data VALUES ('2019-04-04','桔子','淘宝',1406.00); +INSERT INTO sales_data VALUES ('2019-04-04','桔子','京东',1771.00); +INSERT INTO sales_data VALUES ('2019-04-04','桔子','店面',1816.00); +INSERT INTO sales_data VALUES ('2019-04-04','香蕉','淘宝',1968.00); +INSERT INTO sales_data VALUES ('2019-04-04','香蕉','京东',2239.00); +INSERT INTO sales_data VALUES ('2019-04-04','香蕉','店面',1890.00); +INSERT INTO sales_data VALUES ('2019-04-04','苹果','淘宝',2359.00); +INSERT INTO sales_data VALUES ('2019-04-04','苹果','京东',2434.00); +INSERT INTO sales_data VALUES ('2019-04-04','苹果','店面',972.00); +INSERT INTO sales_data VALUES ('2019-04-05','桔子','淘宝',1299.00); +INSERT INTO sales_data VALUES ('2019-04-05','桔子','京东',1554.00); +INSERT INTO sales_data VALUES ('2019-04-05','桔子','店面',1130.00); +INSERT INTO sales_data VALUES ('2019-04-05','香蕉','淘宝',1220.00); +INSERT INTO sales_data VALUES ('2019-04-05','香蕉','京东',2432.00); +INSERT INTO sales_data VALUES ('2019-04-05','香蕉','店面',2428.00); +INSERT INTO sales_data VALUES ('2019-04-05','苹果','淘宝',1979.00); +INSERT INTO sales_data VALUES ('2019-04-05','苹果','京东',2366.00); +INSERT INTO sales_data VALUES ('2019-04-05','苹果','店面',1774.00); +INSERT INTO sales_data VALUES ('2019-04-06','桔子','淘宝',2417.00); +INSERT INTO sales_data VALUES ('2019-04-06','桔子','京东',2128.00); +INSERT INTO sales_data VALUES ('2019-04-06','桔子','店面',1000.00); +INSERT INTO sales_data VALUES ('2019-04-06','香蕉','淘宝',1744.00); +INSERT INTO sales_data VALUES ('2019-04-06','香蕉','京东',1349.00); +INSERT INTO sales_data VALUES ('2019-04-06','香蕉','店面',1609.00); +INSERT INTO sales_data VALUES ('2019-04-06','苹果','淘宝',2432.00); +INSERT INTO sales_data VALUES ('2019-04-06','苹果','京东',2199.00); +INSERT INTO sales_data VALUES ('2019-04-06','苹果','店面',2437.00); +INSERT INTO sales_data VALUES ('2019-04-07','桔子','淘宝',1177.00); +INSERT INTO sales_data VALUES ('2019-04-07','桔子','京东',1662.00); +INSERT INTO sales_data VALUES ('2019-04-07','桔子','店面',2080.00); +INSERT INTO sales_data VALUES ('2019-04-07','香蕉','淘宝',1111.00); +INSERT INTO sales_data VALUES ('2019-04-07','香蕉','京东',2102.00); +INSERT INTO sales_data VALUES ('2019-04-07','香蕉','店面',1386.00); +INSERT INTO sales_data VALUES ('2019-04-07','苹果','淘宝',1962.00); +INSERT INTO sales_data VALUES ('2019-04-07','苹果','京东',1605.00); +INSERT INTO sales_data VALUES ('2019-04-07','苹果','店面',1160.00); +INSERT INTO sales_data VALUES ('2019-04-08','桔子','淘宝',1392.00); +INSERT INTO sales_data VALUES ('2019-04-08','桔子','京东',1504.00); +INSERT INTO sales_data VALUES ('2019-04-08','桔子','店面',1134.00); +INSERT INTO sales_data VALUES ('2019-04-08','香蕉','淘宝',1399.00); +INSERT INTO sales_data VALUES ('2019-04-08','香蕉','京东',1832.00); +INSERT INTO sales_data VALUES ('2019-04-08','香蕉','店面',1718.00); +INSERT INTO sales_data VALUES ('2019-04-08','苹果','淘宝',1559.00); +INSERT INTO sales_data VALUES ('2019-04-08','苹果','京东',2082.00); +INSERT INTO sales_data VALUES ('2019-04-08','苹果','店面',1680.00); +INSERT INTO sales_data VALUES ('2019-04-09','桔子','淘宝',1522.00); +INSERT INTO sales_data VALUES ('2019-04-09','桔子','京东',1596.00); +INSERT INTO sales_data VALUES ('2019-04-09','桔子','店面',1581.00); +INSERT INTO sales_data VALUES ('2019-04-09','香蕉','淘宝',2326.00); +INSERT INTO sales_data VALUES ('2019-04-09','香蕉','京东',1537.00); +INSERT INTO sales_data VALUES ('2019-04-09','香蕉','店面',1234.00); +INSERT INTO sales_data VALUES ('2019-04-09','苹果','淘宝',2351.00); +INSERT INTO sales_data VALUES ('2019-04-09','苹果','京东',2307.00); +INSERT INTO sales_data VALUES ('2019-04-09','苹果','店面',1608.00); +INSERT INTO sales_data VALUES ('2019-04-10','桔子','淘宝',1490.00); +INSERT INTO sales_data VALUES ('2019-04-10','桔子','京东',2269.00); +INSERT INTO sales_data VALUES ('2019-04-10','桔子','店面',1337.00); +INSERT INTO sales_data VALUES ('2019-04-10','香蕉','淘宝',1451.00); +INSERT INTO sales_data VALUES ('2019-04-10','香蕉','京东',2466.00); +INSERT INTO sales_data VALUES ('2019-04-10','香蕉','店面',2019.00); +INSERT INTO sales_data VALUES ('2019-04-10','苹果','淘宝',1051.00); +INSERT INTO sales_data VALUES ('2019-04-10','苹果','京东',1096.00); +INSERT INTO sales_data VALUES ('2019-04-10','苹果','店面',1642.00); +INSERT INTO sales_data VALUES ('2019-04-11','桔子','淘宝',1462.00); +INSERT INTO sales_data VALUES ('2019-04-11','桔子','京东',2083.00); +INSERT INTO sales_data VALUES ('2019-04-11','桔子','店面',2272.00); +INSERT INTO sales_data VALUES ('2019-04-11','香蕉','淘宝',1642.00); +INSERT INTO sales_data VALUES ('2019-04-11','香蕉','京东',2490.00); +INSERT INTO sales_data VALUES ('2019-04-11','香蕉','店面',1290.00); +INSERT INTO sales_data VALUES ('2019-04-11','苹果','淘宝',1792.00); +INSERT INTO sales_data VALUES ('2019-04-11','苹果','京东',1404.00); +INSERT INTO sales_data VALUES ('2019-04-11','苹果','店面',2138.00); +INSERT INTO sales_data VALUES ('2019-04-12','桔子','淘宝',1030.00); +INSERT INTO sales_data VALUES ('2019-04-12','桔子','京东',1983.00); +INSERT INTO sales_data VALUES ('2019-04-12','桔子','店面',1740.00); +INSERT INTO sales_data VALUES ('2019-04-12','香蕉','淘宝',1725.00); +INSERT INTO sales_data VALUES ('2019-04-12','香蕉','京东',1015.00); +INSERT INTO sales_data VALUES ('2019-04-12','香蕉','店面',2346.00); +INSERT INTO sales_data VALUES ('2019-04-12','苹果','淘宝',2316.00); +INSERT INTO sales_data VALUES ('2019-04-12','苹果','京东',2351.00); +INSERT INTO sales_data VALUES ('2019-04-12','苹果','店面',1393.00); +INSERT INTO sales_data VALUES ('2019-04-13','桔子','淘宝',1065.00); +INSERT INTO sales_data VALUES ('2019-04-13','桔子','京东',2216.00); +INSERT INTO sales_data VALUES ('2019-04-13','桔子','店面',1215.00); +INSERT INTO sales_data VALUES ('2019-04-13','香蕉','淘宝',1683.00); +INSERT INTO sales_data VALUES ('2019-04-13','香蕉','京东',1211.00); +INSERT INTO sales_data VALUES ('2019-04-13','香蕉','店面',2489.00); +INSERT INTO sales_data VALUES ('2019-04-13','苹果','淘宝',2025.00); +INSERT INTO sales_data VALUES ('2019-04-13','苹果','京东',1667.00); +INSERT INTO sales_data VALUES ('2019-04-13','苹果','店面',2460.00); +INSERT INTO sales_data VALUES ('2019-04-14','桔子','淘宝',1555.00); +INSERT INTO sales_data VALUES ('2019-04-14','桔子','京东',1728.00); +INSERT INTO sales_data VALUES ('2019-04-14','桔子','店面',1066.00); +INSERT INTO sales_data VALUES ('2019-04-14','香蕉','淘宝',2201.00); +INSERT INTO sales_data VALUES ('2019-04-14','香蕉','京东',2191.00); +INSERT INTO sales_data VALUES ('2019-04-14','香蕉','店面',2149.00); +INSERT INTO sales_data VALUES ('2019-04-14','苹果','淘宝',1973.00); +INSERT INTO sales_data VALUES ('2019-04-14','苹果','京东',1333.00); +INSERT INTO sales_data VALUES ('2019-04-14','苹果','店面',2140.00); +INSERT INTO sales_data VALUES ('2019-04-15','桔子','淘宝',2268.00); +INSERT INTO sales_data VALUES ('2019-04-15','桔子','京东',2130.00); +INSERT INTO sales_data VALUES ('2019-04-15','桔子','店面',1048.00); +INSERT INTO sales_data VALUES ('2019-04-15','香蕉','淘宝',1906.00); +INSERT INTO sales_data VALUES ('2019-04-15','香蕉','京东',2155.00); +INSERT INTO sales_data VALUES ('2019-04-15','香蕉','店面',2026.00); +INSERT INTO sales_data VALUES ('2019-04-15','苹果','淘宝',1141.00); +INSERT INTO sales_data VALUES ('2019-04-15','苹果','京东',1375.00); +INSERT INTO sales_data VALUES ('2019-04-15','苹果','店面',2036.00); +INSERT INTO sales_data VALUES ('2019-04-16','桔子','淘宝',2486.00); +INSERT INTO sales_data VALUES ('2019-04-16','桔子','京东',1191.00); +INSERT INTO sales_data VALUES ('2019-04-16','桔子','店面',1886.00); +INSERT INTO sales_data VALUES ('2019-04-16','香蕉','淘宝',1374.00); +INSERT INTO sales_data VALUES ('2019-04-16','香蕉','京东',1246.00); +INSERT INTO sales_data VALUES ('2019-04-16','香蕉','店面',1593.00); +INSERT INTO sales_data VALUES ('2019-04-16','苹果','淘宝',1579.00); +INSERT INTO sales_data VALUES ('2019-04-16','苹果','京东',1919.00); +INSERT INTO sales_data VALUES ('2019-04-16','苹果','店面',1794.00); +INSERT INTO sales_data VALUES ('2019-04-17','桔子','淘宝',1563.00); +INSERT INTO sales_data VALUES ('2019-04-17','桔子','京东',1439.00); +INSERT INTO sales_data VALUES ('2019-04-17','桔子','店面',2456.00); +INSERT INTO sales_data VALUES ('2019-04-17','香蕉','淘宝',1513.00); +INSERT INTO sales_data VALUES ('2019-04-17','香蕉','京东',1978.00); +INSERT INTO sales_data VALUES ('2019-04-17','香蕉','店面',1669.00); +INSERT INTO sales_data VALUES ('2019-04-17','苹果','淘宝',1564.00); +INSERT INTO sales_data VALUES ('2019-04-17','苹果','京东',1665.00); +INSERT INTO sales_data VALUES ('2019-04-17','苹果','店面',1345.00); +INSERT INTO sales_data VALUES ('2019-04-18','桔子','淘宝',1204.00); +INSERT INTO sales_data VALUES ('2019-04-18','桔子','京东',1127.00); +INSERT INTO sales_data VALUES ('2019-04-18','桔子','店面',1668.00); +INSERT INTO sales_data VALUES ('2019-04-18','香蕉','淘宝',2328.00); +INSERT INTO sales_data VALUES ('2019-04-18','香蕉','京东',2376.00); +INSERT INTO sales_data VALUES ('2019-04-18','香蕉','店面',1278.00); +INSERT INTO sales_data VALUES ('2019-04-18','苹果','淘宝',2357.00); +INSERT INTO sales_data VALUES ('2019-04-18','苹果','京东',1762.00); +INSERT INTO sales_data VALUES ('2019-04-18','苹果','店面',2413.00); +INSERT INTO sales_data VALUES ('2019-04-19','桔子','淘宝',1868.00); +INSERT INTO sales_data VALUES ('2019-04-19','桔子','京东',1888.00); +INSERT INTO sales_data VALUES ('2019-04-19','桔子','店面',1272.00); +INSERT INTO sales_data VALUES ('2019-04-19','香蕉','淘宝',1384.00); +INSERT INTO sales_data VALUES ('2019-04-19','香蕉','京东',1849.00); +INSERT INTO sales_data VALUES ('2019-04-19','香蕉','店面',1438.00); +INSERT INTO sales_data VALUES ('2019-04-19','苹果','淘宝',2245.00); +INSERT INTO sales_data VALUES ('2019-04-19','苹果','京东',2198.00); +INSERT INTO sales_data VALUES ('2019-04-19','苹果','店面',1659.00); +INSERT INTO sales_data VALUES ('2019-04-20','桔子','淘宝',1317.00); +INSERT INTO sales_data VALUES ('2019-04-20','桔子','京东',1258.00); +INSERT INTO sales_data VALUES ('2019-04-20','桔子','店面',1057.00); +INSERT INTO sales_data VALUES ('2019-04-20','香蕉','淘宝',2086.00); +INSERT INTO sales_data VALUES ('2019-04-20','香蕉','京东',1791.00); +INSERT INTO sales_data VALUES ('2019-04-20','香蕉','店面',1466.00); +INSERT INTO sales_data VALUES ('2019-04-20','苹果','淘宝',2012.00); +INSERT INTO sales_data VALUES ('2019-04-20','苹果','京东',2274.00); +INSERT INTO sales_data VALUES ('2019-04-20','苹果','店面',2415.00); +INSERT INTO sales_data VALUES ('2019-04-21','桔子','淘宝',1156.00); +INSERT INTO sales_data VALUES ('2019-04-21','桔子','京东',1313.00); +INSERT INTO sales_data VALUES ('2019-04-21','桔子','店面',1554.00); +INSERT INTO sales_data VALUES ('2019-04-21','香蕉','淘宝',1471.00); +INSERT INTO sales_data VALUES ('2019-04-21','香蕉','京东',1482.00); +INSERT INTO sales_data VALUES ('2019-04-21','香蕉','店面',1647.00); +INSERT INTO sales_data VALUES ('2019-04-21','苹果','淘宝',2104.00); +INSERT INTO sales_data VALUES ('2019-04-21','苹果','京东',1276.00); +INSERT INTO sales_data VALUES ('2019-04-21','苹果','店面',1487.00); +INSERT INTO sales_data VALUES ('2019-04-22','桔子','淘宝',2352.00); +INSERT INTO sales_data VALUES ('2019-04-22','桔子','京东',1102.00); +INSERT INTO sales_data VALUES ('2019-04-22','桔子','店面',2219.00); +INSERT INTO sales_data VALUES ('2019-04-22','香蕉','淘宝',2230.00); +INSERT INTO sales_data VALUES ('2019-04-22','香蕉','京东',1930.00); +INSERT INTO sales_data VALUES ('2019-04-22','香蕉','店面',1567.00); +INSERT INTO sales_data VALUES ('2019-04-22','苹果','淘宝',2462.00); +INSERT INTO sales_data VALUES ('2019-04-22','苹果','京东',2274.00); +INSERT INTO sales_data VALUES ('2019-04-22','苹果','店面',2376.00); +INSERT INTO sales_data VALUES ('2019-04-23','桔子','淘宝',1365.00); +INSERT INTO sales_data VALUES ('2019-04-23','桔子','京东',1983.00); +INSERT INTO sales_data VALUES ('2019-04-23','桔子','店面',2039.00); +INSERT INTO sales_data VALUES ('2019-04-23','香蕉','淘宝',1984.00); +INSERT INTO sales_data VALUES ('2019-04-23','香蕉','京东',2256.00); +INSERT INTO sales_data VALUES ('2019-04-23','香蕉','店面',2252.00); +INSERT INTO sales_data VALUES ('2019-04-23','苹果','淘宝',1997.00); +INSERT INTO sales_data VALUES ('2019-04-23','苹果','京东',1797.00); +INSERT INTO sales_data VALUES ('2019-04-23','苹果','店面',1498.00); +INSERT INTO sales_data VALUES ('2019-04-24','桔子','淘宝',2423.00); +INSERT INTO sales_data VALUES ('2019-04-24','桔子','京东',1269.00); +INSERT INTO sales_data VALUES ('2019-04-24','桔子','店面',1232.00); +INSERT INTO sales_data VALUES ('2019-04-24','香蕉','淘宝',2293.00); +INSERT INTO sales_data VALUES ('2019-04-24','香蕉','京东',1375.00); +INSERT INTO sales_data VALUES ('2019-04-24','香蕉','店面',1495.00); +INSERT INTO sales_data VALUES ('2019-04-24','苹果','淘宝',1297.00); +INSERT INTO sales_data VALUES ('2019-04-24','苹果','京东',1796.00); +INSERT INTO sales_data VALUES ('2019-04-24','苹果','店面',1927.00); +INSERT INTO sales_data VALUES ('2019-04-25','桔子','淘宝',1899.00); +INSERT INTO sales_data VALUES ('2019-04-25','桔子','京东',1356.00); +INSERT INTO sales_data VALUES ('2019-04-25','桔子','店面',2158.00); +INSERT INTO sales_data VALUES ('2019-04-25','香蕉','淘宝',2336.00); +INSERT INTO sales_data VALUES ('2019-04-25','香蕉','京东',1153.00); +INSERT INTO sales_data VALUES ('2019-04-25','香蕉','店面',2205.00); +INSERT INTO sales_data VALUES ('2019-04-25','苹果','淘宝',2000.00); +INSERT INTO sales_data VALUES ('2019-04-25','苹果','京东',2327.00); +INSERT INTO sales_data VALUES ('2019-04-25','苹果','店面',1580.00); +INSERT INTO sales_data VALUES ('2019-04-26','桔子','淘宝',2517.00); +INSERT INTO sales_data VALUES ('2019-04-26','桔子','京东',2239.00); +INSERT INTO sales_data VALUES ('2019-04-26','桔子','店面',1304.00); +INSERT INTO sales_data VALUES ('2019-04-26','香蕉','淘宝',2338.00); +INSERT INTO sales_data VALUES ('2019-04-26','香蕉','京东',2545.00); +INSERT INTO sales_data VALUES ('2019-04-26','香蕉','店面',2228.00); +INSERT INTO sales_data VALUES ('2019-04-26','苹果','淘宝',1817.00); +INSERT INTO sales_data VALUES ('2019-04-26','苹果','京东',1969.00); +INSERT INTO sales_data VALUES ('2019-04-26','苹果','店面',1923.00); +INSERT INTO sales_data VALUES ('2019-04-27','桔子','淘宝',1514.00); +INSERT INTO sales_data VALUES ('2019-04-27','桔子','京东',1411.00); +INSERT INTO sales_data VALUES ('2019-04-27','桔子','店面',1165.00); +INSERT INTO sales_data VALUES ('2019-04-27','香蕉','淘宝',1952.00); +INSERT INTO sales_data VALUES ('2019-04-27','香蕉','京东',1268.00); +INSERT INTO sales_data VALUES ('2019-04-27','香蕉','店面',1369.00); +INSERT INTO sales_data VALUES ('2019-04-27','苹果','淘宝',2119.00); +INSERT INTO sales_data VALUES ('2019-04-27','苹果','京东',2496.00); +INSERT INTO sales_data VALUES ('2019-04-27','苹果','店面',1679.00); +INSERT INTO sales_data VALUES ('2019-04-28','桔子','淘宝',2554.00); +INSERT INTO sales_data VALUES ('2019-04-28','桔子','京东',1233.00); +INSERT INTO sales_data VALUES ('2019-04-28','桔子','店面',2416.00); +INSERT INTO sales_data VALUES ('2019-04-28','香蕉','淘宝',1917.00); +INSERT INTO sales_data VALUES ('2019-04-28','香蕉','京东',2061.00); +INSERT INTO sales_data VALUES ('2019-04-28','香蕉','店面',1201.00); +INSERT INTO sales_data VALUES ('2019-04-28','苹果','淘宝',1505.00); +INSERT INTO sales_data VALUES ('2019-04-28','苹果','京东',1827.00); +INSERT INTO sales_data VALUES ('2019-04-28','苹果','店面',1284.00); +INSERT INTO sales_data VALUES ('2019-04-29','桔子','淘宝',1145.00); +INSERT INTO sales_data VALUES ('2019-04-29','桔子','京东',1262.00); +INSERT INTO sales_data VALUES ('2019-04-29','桔子','店面',2546.00); +INSERT INTO sales_data VALUES ('2019-04-29','香蕉','淘宝',1655.00); +INSERT INTO sales_data VALUES ('2019-04-29','香蕉','京东',1204.00); +INSERT INTO sales_data VALUES ('2019-04-29','香蕉','店面',2210.00); +INSERT INTO sales_data VALUES ('2019-04-29','苹果','淘宝',1884.00); +INSERT INTO sales_data VALUES ('2019-04-29','苹果','京东',2467.00); +INSERT INTO sales_data VALUES ('2019-04-29','苹果','店面',2180.00); +INSERT INTO sales_data VALUES ('2019-04-30','桔子','淘宝',1542.00); +INSERT INTO sales_data VALUES ('2019-04-30','桔子','京东',1714.00); +INSERT INTO sales_data VALUES ('2019-04-30','桔子','店面',1579.00); +INSERT INTO sales_data VALUES ('2019-04-30','香蕉','淘宝',2390.00); +INSERT INTO sales_data VALUES ('2019-04-30','香蕉','京东',2148.00); +INSERT INTO sales_data VALUES ('2019-04-30','香蕉','店面',1910.00); +INSERT INTO sales_data VALUES ('2019-04-30','苹果','淘宝',2476.00); +INSERT INTO sales_data VALUES ('2019-04-30','苹果','京东',1520.00); +INSERT INTO sales_data VALUES ('2019-04-30','苹果','店面',2098.00); +INSERT INTO sales_data VALUES ('2019-05-01','桔子','淘宝',1270.00); +INSERT INTO sales_data VALUES ('2019-05-01','桔子','京东',2564.00); +INSERT INTO sales_data VALUES ('2019-05-01','桔子','店面',2019.00); +INSERT INTO sales_data VALUES ('2019-05-01','香蕉','淘宝',1870.00); +INSERT INTO sales_data VALUES ('2019-05-01','香蕉','京东',2533.00); +INSERT INTO sales_data VALUES ('2019-05-01','香蕉','店面',2167.00); +INSERT INTO sales_data VALUES ('2019-05-01','苹果','淘宝',1700.00); +INSERT INTO sales_data VALUES ('2019-05-01','苹果','京东',1865.00); +INSERT INTO sales_data VALUES ('2019-05-01','苹果','店面',1643.00); +INSERT INTO sales_data VALUES ('2019-05-02','桔子','淘宝',1822.00); +INSERT INTO sales_data VALUES ('2019-05-02','桔子','京东',2290.00); +INSERT INTO sales_data VALUES ('2019-05-02','桔子','店面',2391.00); +INSERT INTO sales_data VALUES ('2019-05-02','香蕉','淘宝',2021.00); +INSERT INTO sales_data VALUES ('2019-05-02','香蕉','京东',2345.00); +INSERT INTO sales_data VALUES ('2019-05-02','香蕉','店面',2563.00); +INSERT INTO sales_data VALUES ('2019-05-02','苹果','淘宝',1977.00); +INSERT INTO sales_data VALUES ('2019-05-02','苹果','京东',1410.00); +INSERT INTO sales_data VALUES ('2019-05-02','苹果','店面',1177.00); +INSERT INTO sales_data VALUES ('2019-05-03','桔子','淘宝',1602.00); +INSERT INTO sales_data VALUES ('2019-05-03','桔子','京东',2209.00); +INSERT INTO sales_data VALUES ('2019-05-03','桔子','店面',2559.00); +INSERT INTO sales_data VALUES ('2019-05-03','香蕉','淘宝',1192.00); +INSERT INTO sales_data VALUES ('2019-05-03','香蕉','京东',1156.00); +INSERT INTO sales_data VALUES ('2019-05-03','香蕉','店面',1678.00); +INSERT INTO sales_data VALUES ('2019-05-03','苹果','淘宝',1677.00); +INSERT INTO sales_data VALUES ('2019-05-03','苹果','京东',2452.00); +INSERT INTO sales_data VALUES ('2019-05-03','苹果','店面',1231.00); +INSERT INTO sales_data VALUES ('2019-05-04','桔子','淘宝',2496.00); +INSERT INTO sales_data VALUES ('2019-05-04','桔子','京东',2337.00); +INSERT INTO sales_data VALUES ('2019-05-04','桔子','店面',1661.00); +INSERT INTO sales_data VALUES ('2019-05-04','香蕉','淘宝',1999.00); +INSERT INTO sales_data VALUES ('2019-05-04','香蕉','京东',2508.00); +INSERT INTO sales_data VALUES ('2019-05-04','香蕉','店面',1626.00); +INSERT INTO sales_data VALUES ('2019-05-04','苹果','淘宝',1418.00); +INSERT INTO sales_data VALUES ('2019-05-04','苹果','京东',1777.00); +INSERT INTO sales_data VALUES ('2019-05-04','苹果','店面',1559.00); +INSERT INTO sales_data VALUES ('2019-05-05','桔子','淘宝',2490.00); +INSERT INTO sales_data VALUES ('2019-05-05','桔子','京东',2382.00); +INSERT INTO sales_data VALUES ('2019-05-05','桔子','店面',2329.00); +INSERT INTO sales_data VALUES ('2019-05-05','香蕉','淘宝',1533.00); +INSERT INTO sales_data VALUES ('2019-05-05','香蕉','京东',1599.00); +INSERT INTO sales_data VALUES ('2019-05-05','香蕉','店面',2014.00); +INSERT INTO sales_data VALUES ('2019-05-05','苹果','淘宝',1319.00); +INSERT INTO sales_data VALUES ('2019-05-05','苹果','京东',2515.00); +INSERT INTO sales_data VALUES ('2019-05-05','苹果','店面',1754.00); +INSERT INTO sales_data VALUES ('2019-05-06','桔子','淘宝',1282.00); +INSERT INTO sales_data VALUES ('2019-05-06','桔子','京东',1892.00); +INSERT INTO sales_data VALUES ('2019-05-06','桔子','店面',2064.00); +INSERT INTO sales_data VALUES ('2019-05-06','香蕉','淘宝',1354.00); +INSERT INTO sales_data VALUES ('2019-05-06','香蕉','京东',2384.00); +INSERT INTO sales_data VALUES ('2019-05-06','香蕉','店面',1663.00); +INSERT INTO sales_data VALUES ('2019-05-06','苹果','淘宝',1303.00); +INSERT INTO sales_data VALUES ('2019-05-06','苹果','京东',2467.00); +INSERT INTO sales_data VALUES ('2019-05-06','苹果','店面',1709.00); +INSERT INTO sales_data VALUES ('2019-05-07','桔子','淘宝',1876.00); +INSERT INTO sales_data VALUES ('2019-05-07','桔子','京东',1538.00); +INSERT INTO sales_data VALUES ('2019-05-07','桔子','店面',1556.00); +INSERT INTO sales_data VALUES ('2019-05-07','香蕉','淘宝',1997.00); +INSERT INTO sales_data VALUES ('2019-05-07','香蕉','京东',1419.00); +INSERT INTO sales_data VALUES ('2019-05-07','香蕉','店面',1278.00); +INSERT INTO sales_data VALUES ('2019-05-07','苹果','淘宝',2544.00); +INSERT INTO sales_data VALUES ('2019-05-07','苹果','京东',2303.00); +INSERT INTO sales_data VALUES ('2019-05-07','苹果','店面',1171.00); +INSERT INTO sales_data VALUES ('2019-05-08','桔子','淘宝',1559.00); +INSERT INTO sales_data VALUES ('2019-05-08','桔子','京东',2611.00); +INSERT INTO sales_data VALUES ('2019-05-08','桔子','店面',1838.00); +INSERT INTO sales_data VALUES ('2019-05-08','香蕉','淘宝',2003.00); +INSERT INTO sales_data VALUES ('2019-05-08','香蕉','京东',2481.00); +INSERT INTO sales_data VALUES ('2019-05-08','香蕉','店面',1601.00); +INSERT INTO sales_data VALUES ('2019-05-08','苹果','淘宝',1713.00); +INSERT INTO sales_data VALUES ('2019-05-08','苹果','京东',1395.00); +INSERT INTO sales_data VALUES ('2019-05-08','苹果','店面',2080.00); +INSERT INTO sales_data VALUES ('2019-05-09','桔子','淘宝',2612.00); +INSERT INTO sales_data VALUES ('2019-05-09','桔子','京东',1599.00); +INSERT INTO sales_data VALUES ('2019-05-09','桔子','店面',1980.00); +INSERT INTO sales_data VALUES ('2019-05-09','香蕉','淘宝',1746.00); +INSERT INTO sales_data VALUES ('2019-05-09','香蕉','京东',1756.00); +INSERT INTO sales_data VALUES ('2019-05-09','香蕉','店面',1246.00); +INSERT INTO sales_data VALUES ('2019-05-09','苹果','淘宝',1184.00); +INSERT INTO sales_data VALUES ('2019-05-09','苹果','京东',1984.00); +INSERT INTO sales_data VALUES ('2019-05-09','苹果','店面',2505.00); +INSERT INTO sales_data VALUES ('2019-05-10','桔子','淘宝',1728.00); +INSERT INTO sales_data VALUES ('2019-05-10','桔子','京东',2168.00); +INSERT INTO sales_data VALUES ('2019-05-10','桔子','店面',2352.00); +INSERT INTO sales_data VALUES ('2019-05-10','香蕉','淘宝',2312.00); +INSERT INTO sales_data VALUES ('2019-05-10','香蕉','京东',1414.00); +INSERT INTO sales_data VALUES ('2019-05-10','香蕉','店面',1260.00); +INSERT INTO sales_data VALUES ('2019-05-10','苹果','淘宝',1238.00); +INSERT INTO sales_data VALUES ('2019-05-10','苹果','京东',2281.00); +INSERT INTO sales_data VALUES ('2019-05-10','苹果','店面',1549.00); +INSERT INTO sales_data VALUES ('2019-05-11','桔子','淘宝',1391.00); +INSERT INTO sales_data VALUES ('2019-05-11','桔子','京东',2200.00); +INSERT INTO sales_data VALUES ('2019-05-11','桔子','店面',1227.00); +INSERT INTO sales_data VALUES ('2019-05-11','香蕉','淘宝',1432.00); +INSERT INTO sales_data VALUES ('2019-05-11','香蕉','京东',2624.00); +INSERT INTO sales_data VALUES ('2019-05-11','香蕉','店面',1204.00); +INSERT INTO sales_data VALUES ('2019-05-11','苹果','淘宝',2136.00); +INSERT INTO sales_data VALUES ('2019-05-11','苹果','京东',1992.00); +INSERT INTO sales_data VALUES ('2019-05-11','苹果','店面',2550.00); +INSERT INTO sales_data VALUES ('2019-05-12','桔子','淘宝',2606.00); +INSERT INTO sales_data VALUES ('2019-05-12','桔子','京东',2575.00); +INSERT INTO sales_data VALUES ('2019-05-12','桔子','店面',1315.00); +INSERT INTO sales_data VALUES ('2019-05-12','香蕉','淘宝',2051.00); +INSERT INTO sales_data VALUES ('2019-05-12','香蕉','京东',2547.00); +INSERT INTO sales_data VALUES ('2019-05-12','香蕉','店面',1774.00); +INSERT INTO sales_data VALUES ('2019-05-12','苹果','淘宝',1391.00); +INSERT INTO sales_data VALUES ('2019-05-12','苹果','京东',1652.00); +INSERT INTO sales_data VALUES ('2019-05-12','苹果','店面',2389.00); +INSERT INTO sales_data VALUES ('2019-05-13','桔子','淘宝',1502.00); +INSERT INTO sales_data VALUES ('2019-05-13','桔子','京东',1701.00); +INSERT INTO sales_data VALUES ('2019-05-13','桔子','店面',1739.00); +INSERT INTO sales_data VALUES ('2019-05-13','香蕉','淘宝',1367.00); +INSERT INTO sales_data VALUES ('2019-05-13','香蕉','京东',2284.00); +INSERT INTO sales_data VALUES ('2019-05-13','香蕉','店面',1261.00); +INSERT INTO sales_data VALUES ('2019-05-13','苹果','淘宝',2574.00); +INSERT INTO sales_data VALUES ('2019-05-13','苹果','京东',1951.00); +INSERT INTO sales_data VALUES ('2019-05-13','苹果','店面',1530.00); +INSERT INTO sales_data VALUES ('2019-05-14','桔子','淘宝',1194.00); +INSERT INTO sales_data VALUES ('2019-05-14','桔子','京东',2049.00); +INSERT INTO sales_data VALUES ('2019-05-14','桔子','店面',1171.00); +INSERT INTO sales_data VALUES ('2019-05-14','香蕉','淘宝',1598.00); +INSERT INTO sales_data VALUES ('2019-05-14','香蕉','京东',2290.00); +INSERT INTO sales_data VALUES ('2019-05-14','香蕉','店面',2220.00); +INSERT INTO sales_data VALUES ('2019-05-14','苹果','淘宝',1676.00); +INSERT INTO sales_data VALUES ('2019-05-14','苹果','京东',2573.00); +INSERT INTO sales_data VALUES ('2019-05-14','苹果','店面',2194.00); +INSERT INTO sales_data VALUES ('2019-05-15','桔子','淘宝',1734.00); +INSERT INTO sales_data VALUES ('2019-05-15','桔子','京东',2063.00); +INSERT INTO sales_data VALUES ('2019-05-15','桔子','店面',1541.00); +INSERT INTO sales_data VALUES ('2019-05-15','香蕉','淘宝',1634.00); +INSERT INTO sales_data VALUES ('2019-05-15','香蕉','京东',2014.00); +INSERT INTO sales_data VALUES ('2019-05-15','香蕉','店面',1461.00); +INSERT INTO sales_data VALUES ('2019-05-15','苹果','淘宝',1794.00); +INSERT INTO sales_data VALUES ('2019-05-15','苹果','京东',1411.00); +INSERT INTO sales_data VALUES ('2019-05-15','苹果','店面',1353.00); +INSERT INTO sales_data VALUES ('2019-05-16','桔子','淘宝',2418.00); +INSERT INTO sales_data VALUES ('2019-05-16','桔子','京东',1651.00); +INSERT INTO sales_data VALUES ('2019-05-16','桔子','店面',1855.00); +INSERT INTO sales_data VALUES ('2019-05-16','香蕉','淘宝',2152.00); +INSERT INTO sales_data VALUES ('2019-05-16','香蕉','京东',1994.00); +INSERT INTO sales_data VALUES ('2019-05-16','香蕉','店面',2396.00); +INSERT INTO sales_data VALUES ('2019-05-16','苹果','淘宝',1231.00); +INSERT INTO sales_data VALUES ('2019-05-16','苹果','京东',2201.00); +INSERT INTO sales_data VALUES ('2019-05-16','苹果','店面',2020.00); +INSERT INTO sales_data VALUES ('2019-05-17','桔子','淘宝',1337.00); +INSERT INTO sales_data VALUES ('2019-05-17','桔子','京东',2120.00); +INSERT INTO sales_data VALUES ('2019-05-17','桔子','店面',1316.00); +INSERT INTO sales_data VALUES ('2019-05-17','香蕉','淘宝',1706.00); +INSERT INTO sales_data VALUES ('2019-05-17','香蕉','京东',2150.00); +INSERT INTO sales_data VALUES ('2019-05-17','香蕉','店面',2200.00); +INSERT INTO sales_data VALUES ('2019-05-17','苹果','淘宝',1712.00); +INSERT INTO sales_data VALUES ('2019-05-17','苹果','京东',2583.00); +INSERT INTO sales_data VALUES ('2019-05-17','苹果','店面',1825.00); +INSERT INTO sales_data VALUES ('2019-05-18','桔子','淘宝',1272.00); +INSERT INTO sales_data VALUES ('2019-05-18','桔子','京东',1599.00); +INSERT INTO sales_data VALUES ('2019-05-18','桔子','店面',1738.00); +INSERT INTO sales_data VALUES ('2019-05-18','香蕉','淘宝',2302.00); +INSERT INTO sales_data VALUES ('2019-05-18','香蕉','京东',2163.00); +INSERT INTO sales_data VALUES ('2019-05-18','香蕉','店面',2631.00); +INSERT INTO sales_data VALUES ('2019-05-18','苹果','淘宝',2673.00); +INSERT INTO sales_data VALUES ('2019-05-18','苹果','京东',2627.00); +INSERT INTO sales_data VALUES ('2019-05-18','苹果','店面',1975.00); +INSERT INTO sales_data VALUES ('2019-05-19','桔子','淘宝',1469.00); +INSERT INTO sales_data VALUES ('2019-05-19','桔子','京东',1757.00); +INSERT INTO sales_data VALUES ('2019-05-19','桔子','店面',2221.00); +INSERT INTO sales_data VALUES ('2019-05-19','香蕉','淘宝',1652.00); +INSERT INTO sales_data VALUES ('2019-05-19','香蕉','京东',1500.00); +INSERT INTO sales_data VALUES ('2019-05-19','香蕉','店面',1197.00); +INSERT INTO sales_data VALUES ('2019-05-19','苹果','淘宝',2331.00); +INSERT INTO sales_data VALUES ('2019-05-19','苹果','京东',2477.00); +INSERT INTO sales_data VALUES ('2019-05-19','苹果','店面',2016.00); +INSERT INTO sales_data VALUES ('2019-05-20','桔子','淘宝',2057.00); +INSERT INTO sales_data VALUES ('2019-05-20','桔子','京东',2537.00); +INSERT INTO sales_data VALUES ('2019-05-20','桔子','店面',1547.00); +INSERT INTO sales_data VALUES ('2019-05-20','香蕉','淘宝',1403.00); +INSERT INTO sales_data VALUES ('2019-05-20','香蕉','京东',2694.00); +INSERT INTO sales_data VALUES ('2019-05-20','香蕉','店面',2487.00); +INSERT INTO sales_data VALUES ('2019-05-20','苹果','淘宝',1539.00); +INSERT INTO sales_data VALUES ('2019-05-20','苹果','京东',1720.00); +INSERT INTO sales_data VALUES ('2019-05-20','苹果','店面',1957.00); +INSERT INTO sales_data VALUES ('2019-05-21','桔子','淘宝',2564.00); +INSERT INTO sales_data VALUES ('2019-05-21','桔子','京东',2257.00); +INSERT INTO sales_data VALUES ('2019-05-21','桔子','店面',1865.00); +INSERT INTO sales_data VALUES ('2019-05-21','香蕉','淘宝',1709.00); +INSERT INTO sales_data VALUES ('2019-05-21','香蕉','京东',2345.00); +INSERT INTO sales_data VALUES ('2019-05-21','香蕉','店面',2278.00); +INSERT INTO sales_data VALUES ('2019-05-21','苹果','淘宝',2261.00); +INSERT INTO sales_data VALUES ('2019-05-21','苹果','京东',1961.00); +INSERT INTO sales_data VALUES ('2019-05-21','苹果','店面',1756.00); +INSERT INTO sales_data VALUES ('2019-05-22','桔子','淘宝',2212.00); +INSERT INTO sales_data VALUES ('2019-05-22','桔子','京东',1954.00); +INSERT INTO sales_data VALUES ('2019-05-22','桔子','店面',1703.00); +INSERT INTO sales_data VALUES ('2019-05-22','香蕉','淘宝',1503.00); +INSERT INTO sales_data VALUES ('2019-05-22','香蕉','京东',2233.00); +INSERT INTO sales_data VALUES ('2019-05-22','香蕉','店面',2270.00); +INSERT INTO sales_data VALUES ('2019-05-22','苹果','淘宝',2533.00); +INSERT INTO sales_data VALUES ('2019-05-22','苹果','京东',2695.00); +INSERT INTO sales_data VALUES ('2019-05-22','苹果','店面',2580.00); +INSERT INTO sales_data VALUES ('2019-05-23','桔子','淘宝',2546.00); +INSERT INTO sales_data VALUES ('2019-05-23','桔子','京东',2341.00); +INSERT INTO sales_data VALUES ('2019-05-23','桔子','店面',2371.00); +INSERT INTO sales_data VALUES ('2019-05-23','香蕉','淘宝',1872.00); +INSERT INTO sales_data VALUES ('2019-05-23','香蕉','京东',1703.00); +INSERT INTO sales_data VALUES ('2019-05-23','香蕉','店面',2213.00); +INSERT INTO sales_data VALUES ('2019-05-23','苹果','淘宝',2223.00); +INSERT INTO sales_data VALUES ('2019-05-23','苹果','京东',1911.00); +INSERT INTO sales_data VALUES ('2019-05-23','苹果','店面',2212.00); +INSERT INTO sales_data VALUES ('2019-05-24','桔子','淘宝',2021.00); +INSERT INTO sales_data VALUES ('2019-05-24','桔子','京东',2259.00); +INSERT INTO sales_data VALUES ('2019-05-24','桔子','店面',1242.00); +INSERT INTO sales_data VALUES ('2019-05-24','香蕉','淘宝',1282.00); +INSERT INTO sales_data VALUES ('2019-05-24','香蕉','京东',2123.00); +INSERT INTO sales_data VALUES ('2019-05-24','香蕉','店面',2299.00); +INSERT INTO sales_data VALUES ('2019-05-24','苹果','淘宝',1947.00); +INSERT INTO sales_data VALUES ('2019-05-24','苹果','京东',2632.00); +INSERT INTO sales_data VALUES ('2019-05-24','苹果','店面',1944.00); +INSERT INTO sales_data VALUES ('2019-05-25','桔子','淘宝',1530.00); +INSERT INTO sales_data VALUES ('2019-05-25','桔子','京东',2198.00); +INSERT INTO sales_data VALUES ('2019-05-25','桔子','店面',2710.00); +INSERT INTO sales_data VALUES ('2019-05-25','香蕉','淘宝',2087.00); +INSERT INTO sales_data VALUES ('2019-05-25','香蕉','京东',1706.00); +INSERT INTO sales_data VALUES ('2019-05-25','香蕉','店面',1959.00); +INSERT INTO sales_data VALUES ('2019-05-25','苹果','淘宝',2585.00); +INSERT INTO sales_data VALUES ('2019-05-25','苹果','京东',2003.00); +INSERT INTO sales_data VALUES ('2019-05-25','苹果','店面',1487.00); +INSERT INTO sales_data VALUES ('2019-05-26','桔子','淘宝',2155.00); +INSERT INTO sales_data VALUES ('2019-05-26','桔子','京东',1837.00); +INSERT INTO sales_data VALUES ('2019-05-26','桔子','店面',1482.00); +INSERT INTO sales_data VALUES ('2019-05-26','香蕉','淘宝',2030.00); +INSERT INTO sales_data VALUES ('2019-05-26','香蕉','京东',1672.00); +INSERT INTO sales_data VALUES ('2019-05-26','香蕉','店面',2612.00); +INSERT INTO sales_data VALUES ('2019-05-26','苹果','淘宝',1691.00); +INSERT INTO sales_data VALUES ('2019-05-26','苹果','京东',2334.00); +INSERT INTO sales_data VALUES ('2019-05-26','苹果','店面',1606.00); +INSERT INTO sales_data VALUES ('2019-05-27','桔子','淘宝',2699.00); +INSERT INTO sales_data VALUES ('2019-05-27','桔子','京东',1852.00); +INSERT INTO sales_data VALUES ('2019-05-27','桔子','店面',2311.00); +INSERT INTO sales_data VALUES ('2019-05-27','香蕉','淘宝',2201.00); +INSERT INTO sales_data VALUES ('2019-05-27','香蕉','京东',2658.00); +INSERT INTO sales_data VALUES ('2019-05-27','香蕉','店面',1856.00); +INSERT INTO sales_data VALUES ('2019-05-27','苹果','淘宝',2228.00); +INSERT INTO sales_data VALUES ('2019-05-27','苹果','京东',2725.00); +INSERT INTO sales_data VALUES ('2019-05-27','苹果','店面',1264.00); +INSERT INTO sales_data VALUES ('2019-05-28','桔子','淘宝',1818.00); +INSERT INTO sales_data VALUES ('2019-05-28','桔子','京东',1963.00); +INSERT INTO sales_data VALUES ('2019-05-28','桔子','店面',2686.00); +INSERT INTO sales_data VALUES ('2019-05-28','香蕉','淘宝',2547.00); +INSERT INTO sales_data VALUES ('2019-05-28','香蕉','京东',2273.00); +INSERT INTO sales_data VALUES ('2019-05-28','香蕉','店面',2164.00); +INSERT INTO sales_data VALUES ('2019-05-28','苹果','淘宝',2537.00); +INSERT INTO sales_data VALUES ('2019-05-28','苹果','京东',1640.00); +INSERT INTO sales_data VALUES ('2019-05-28','苹果','店面',2650.00); +INSERT INTO sales_data VALUES ('2019-05-29','桔子','淘宝',1781.00); +INSERT INTO sales_data VALUES ('2019-05-29','桔子','京东',1510.00); +INSERT INTO sales_data VALUES ('2019-05-29','桔子','店面',1938.00); +INSERT INTO sales_data VALUES ('2019-05-29','香蕉','淘宝',2048.00); +INSERT INTO sales_data VALUES ('2019-05-29','香蕉','京东',2440.00); +INSERT INTO sales_data VALUES ('2019-05-29','香蕉','店面',2549.00); +INSERT INTO sales_data VALUES ('2019-05-29','苹果','淘宝',2304.00); +INSERT INTO sales_data VALUES ('2019-05-29','苹果','京东',1745.00); +INSERT INTO sales_data VALUES ('2019-05-29','苹果','店面',1497.00); +INSERT INTO sales_data VALUES ('2019-05-30','桔子','淘宝',2197.00); +INSERT INTO sales_data VALUES ('2019-05-30','桔子','京东',2216.00); +INSERT INTO sales_data VALUES ('2019-05-30','桔子','店面',2610.00); +INSERT INTO sales_data VALUES ('2019-05-30','香蕉','淘宝',2577.00); +INSERT INTO sales_data VALUES ('2019-05-30','香蕉','京东',2185.00); +INSERT INTO sales_data VALUES ('2019-05-30','香蕉','店面',1733.00); +INSERT INTO sales_data VALUES ('2019-05-30','苹果','淘宝',2159.00); +INSERT INTO sales_data VALUES ('2019-05-30','苹果','京东',1657.00); +INSERT INTO sales_data VALUES ('2019-05-30','苹果','店面',1661.00); +INSERT INTO sales_data VALUES ('2019-05-31','桔子','淘宝',1290.00); +INSERT INTO sales_data VALUES ('2019-05-31','桔子','京东',2660.00); +INSERT INTO sales_data VALUES ('2019-05-31','桔子','店面',1661.00); +INSERT INTO sales_data VALUES ('2019-05-31','香蕉','淘宝',1324.00); +INSERT INTO sales_data VALUES ('2019-05-31','香蕉','京东',1743.00); +INSERT INTO sales_data VALUES ('2019-05-31','香蕉','店面',2389.00); +INSERT INTO sales_data VALUES ('2019-05-31','苹果','淘宝',1274.00); +INSERT INTO sales_data VALUES ('2019-05-31','苹果','京东',1554.00); +INSERT INTO sales_data VALUES ('2019-05-31','苹果','店面',1927.00); +INSERT INTO sales_data VALUES ('2019-06-01','桔子','淘宝',2209.00); +INSERT INTO sales_data VALUES ('2019-06-01','桔子','京东',1361.00); +INSERT INTO sales_data VALUES ('2019-06-01','桔子','店面',2336.00); +INSERT INTO sales_data VALUES ('2019-06-01','香蕉','淘宝',2123.00); +INSERT INTO sales_data VALUES ('2019-06-01','香蕉','京东',1902.00); +INSERT INTO sales_data VALUES ('2019-06-01','香蕉','店面',2606.00); +INSERT INTO sales_data VALUES ('2019-06-01','苹果','淘宝',1321.00); +INSERT INTO sales_data VALUES ('2019-06-01','苹果','京东',2710.00); +INSERT INTO sales_data VALUES ('2019-06-01','苹果','店面',2306.00); +INSERT INTO sales_data VALUES ('2019-06-02','桔子','淘宝',2635.00); +INSERT INTO sales_data VALUES ('2019-06-02','桔子','京东',2279.00); +INSERT INTO sales_data VALUES ('2019-06-02','桔子','店面',1316.00); +INSERT INTO sales_data VALUES ('2019-06-02','香蕉','淘宝',1392.00); +INSERT INTO sales_data VALUES ('2019-06-02','香蕉','京东',1731.00); +INSERT INTO sales_data VALUES ('2019-06-02','香蕉','店面',2287.00); +INSERT INTO sales_data VALUES ('2019-06-02','苹果','淘宝',2757.00); +INSERT INTO sales_data VALUES ('2019-06-02','苹果','京东',1563.00); +INSERT INTO sales_data VALUES ('2019-06-02','苹果','店面',1728.00); +INSERT INTO sales_data VALUES ('2019-06-03','桔子','淘宝',1750.00); +INSERT INTO sales_data VALUES ('2019-06-03','桔子','京东',2482.00); +INSERT INTO sales_data VALUES ('2019-06-03','桔子','店面',2144.00); +INSERT INTO sales_data VALUES ('2019-06-03','香蕉','淘宝',2166.00); +INSERT INTO sales_data VALUES ('2019-06-03','香蕉','京东',2522.00); +INSERT INTO sales_data VALUES ('2019-06-03','香蕉','店面',2054.00); +INSERT INTO sales_data VALUES ('2019-06-03','苹果','淘宝',2577.00); +INSERT INTO sales_data VALUES ('2019-06-03','苹果','京东',2596.00); +INSERT INTO sales_data VALUES ('2019-06-03','苹果','店面',2547.00); +INSERT INTO sales_data VALUES ('2019-06-04','桔子','淘宝',2220.00); +INSERT INTO sales_data VALUES ('2019-06-04','桔子','京东',2625.00); +INSERT INTO sales_data VALUES ('2019-06-04','桔子','店面',1357.00); +INSERT INTO sales_data VALUES ('2019-06-04','香蕉','淘宝',1397.00); +INSERT INTO sales_data VALUES ('2019-06-04','香蕉','京东',2079.00); +INSERT INTO sales_data VALUES ('2019-06-04','香蕉','店面',1463.00); +INSERT INTO sales_data VALUES ('2019-06-04','苹果','淘宝',2478.00); +INSERT INTO sales_data VALUES ('2019-06-04','苹果','京东',1447.00); +INSERT INTO sales_data VALUES ('2019-06-04','苹果','店面',2109.00); +INSERT INTO sales_data VALUES ('2019-06-05','桔子','淘宝',2334.00); +INSERT INTO sales_data VALUES ('2019-06-05','桔子','京东',1518.00); +INSERT INTO sales_data VALUES ('2019-06-05','桔子','店面',2069.00); +INSERT INTO sales_data VALUES ('2019-06-05','香蕉','淘宝',1885.00); +INSERT INTO sales_data VALUES ('2019-06-05','香蕉','京东',1393.00); +INSERT INTO sales_data VALUES ('2019-06-05','香蕉','店面',1588.00); +INSERT INTO sales_data VALUES ('2019-06-05','苹果','淘宝',1942.00); +INSERT INTO sales_data VALUES ('2019-06-05','苹果','京东',1525.00); +INSERT INTO sales_data VALUES ('2019-06-05','苹果','店面',2059.00); +INSERT INTO sales_data VALUES ('2019-06-06','桔子','淘宝',1474.00); +INSERT INTO sales_data VALUES ('2019-06-06','桔子','京东',1527.00); +INSERT INTO sales_data VALUES ('2019-06-06','桔子','店面',2367.00); +INSERT INTO sales_data VALUES ('2019-06-06','香蕉','淘宝',1941.00); +INSERT INTO sales_data VALUES ('2019-06-06','香蕉','京东',2012.00); +INSERT INTO sales_data VALUES ('2019-06-06','香蕉','店面',2085.00); +INSERT INTO sales_data VALUES ('2019-06-06','苹果','淘宝',1321.00); +INSERT INTO sales_data VALUES ('2019-06-06','苹果','京东',1413.00); +INSERT INTO sales_data VALUES ('2019-06-06','苹果','店面',1841.00); +INSERT INTO sales_data VALUES ('2019-06-07','桔子','淘宝',2115.00); +INSERT INTO sales_data VALUES ('2019-06-07','桔子','京东',2730.00); +INSERT INTO sales_data VALUES ('2019-06-07','桔子','店面',1677.00); +INSERT INTO sales_data VALUES ('2019-06-07','香蕉','淘宝',1897.00); +INSERT INTO sales_data VALUES ('2019-06-07','香蕉','京东',2180.00); +INSERT INTO sales_data VALUES ('2019-06-07','香蕉','店面',1532.00); +INSERT INTO sales_data VALUES ('2019-06-07','苹果','淘宝',1984.00); +INSERT INTO sales_data VALUES ('2019-06-07','苹果','京东',2307.00); +INSERT INTO sales_data VALUES ('2019-06-07','苹果','店面',2341.00); +INSERT INTO sales_data VALUES ('2019-06-08','桔子','淘宝',2181.00); +INSERT INTO sales_data VALUES ('2019-06-08','桔子','京东',2020.00); +INSERT INTO sales_data VALUES ('2019-06-08','桔子','店面',2523.00); +INSERT INTO sales_data VALUES ('2019-06-08','香蕉','淘宝',1521.00); +INSERT INTO sales_data VALUES ('2019-06-08','香蕉','京东',1579.00); +INSERT INTO sales_data VALUES ('2019-06-08','香蕉','店面',2766.00); +INSERT INTO sales_data VALUES ('2019-06-08','苹果','淘宝',2315.00); +INSERT INTO sales_data VALUES ('2019-06-08','苹果','京东',2190.00); +INSERT INTO sales_data VALUES ('2019-06-08','苹果','店面',1384.00); +INSERT INTO sales_data VALUES ('2019-06-09','桔子','淘宝',2633.00); +INSERT INTO sales_data VALUES ('2019-06-09','桔子','京东',1361.00); +INSERT INTO sales_data VALUES ('2019-06-09','桔子','店面',1639.00); +INSERT INTO sales_data VALUES ('2019-06-09','香蕉','淘宝',1916.00); +INSERT INTO sales_data VALUES ('2019-06-09','香蕉','京东',1555.00); +INSERT INTO sales_data VALUES ('2019-06-09','香蕉','店面',1886.00); +INSERT INTO sales_data VALUES ('2019-06-09','苹果','淘宝',1504.00); +INSERT INTO sales_data VALUES ('2019-06-09','苹果','京东',2217.00); +INSERT INTO sales_data VALUES ('2019-06-09','苹果','店面',2619.00); +INSERT INTO sales_data VALUES ('2019-06-10','桔子','淘宝',2313.00); +INSERT INTO sales_data VALUES ('2019-06-10','桔子','京东',2262.00); +INSERT INTO sales_data VALUES ('2019-06-10','桔子','店面',2757.00); +INSERT INTO sales_data VALUES ('2019-06-10','香蕉','淘宝',1375.00); +INSERT INTO sales_data VALUES ('2019-06-10','香蕉','京东',1592.00); +INSERT INTO sales_data VALUES ('2019-06-10','香蕉','店面',2701.00); +INSERT INTO sales_data VALUES ('2019-06-10','苹果','淘宝',1767.00); +INSERT INTO sales_data VALUES ('2019-06-10','苹果','京东',2204.00); +INSERT INTO sales_data VALUES ('2019-06-10','苹果','店面',2096.00); +INSERT INTO sales_data VALUES ('2019-06-11','桔子','淘宝',2019.00); +INSERT INTO sales_data VALUES ('2019-06-11','桔子','京东',1408.00); +INSERT INTO sales_data VALUES ('2019-06-11','桔子','店面',1623.00); +INSERT INTO sales_data VALUES ('2019-06-11','香蕉','淘宝',1575.00); +INSERT INTO sales_data VALUES ('2019-06-11','香蕉','京东',2299.00); +INSERT INTO sales_data VALUES ('2019-06-11','香蕉','店面',2353.00); +INSERT INTO sales_data VALUES ('2019-06-11','苹果','淘宝',1308.00); +INSERT INTO sales_data VALUES ('2019-06-11','苹果','京东',2530.00); +INSERT INTO sales_data VALUES ('2019-06-11','苹果','店面',2643.00); +INSERT INTO sales_data VALUES ('2019-06-12','桔子','淘宝',2788.00); +INSERT INTO sales_data VALUES ('2019-06-12','桔子','京东',2060.00); +INSERT INTO sales_data VALUES ('2019-06-12','桔子','店面',2048.00); +INSERT INTO sales_data VALUES ('2019-06-12','香蕉','淘宝',1382.00); +INSERT INTO sales_data VALUES ('2019-06-12','香蕉','京东',1897.00); +INSERT INTO sales_data VALUES ('2019-06-12','香蕉','店面',2114.00); +INSERT INTO sales_data VALUES ('2019-06-12','苹果','淘宝',1726.00); +INSERT INTO sales_data VALUES ('2019-06-12','苹果','京东',2519.00); +INSERT INTO sales_data VALUES ('2019-06-12','苹果','店面',2374.00); +INSERT INTO sales_data VALUES ('2019-06-13','桔子','淘宝',2322.00); +INSERT INTO sales_data VALUES ('2019-06-13','桔子','京东',2732.00); +INSERT INTO sales_data VALUES ('2019-06-13','桔子','店面',1801.00); +INSERT INTO sales_data VALUES ('2019-06-13','香蕉','淘宝',2145.00); +INSERT INTO sales_data VALUES ('2019-06-13','香蕉','京东',2246.00); +INSERT INTO sales_data VALUES ('2019-06-13','香蕉','店面',2763.00); +INSERT INTO sales_data VALUES ('2019-06-13','苹果','淘宝',2102.00); +INSERT INTO sales_data VALUES ('2019-06-13','苹果','京东',2320.00); +INSERT INTO sales_data VALUES ('2019-06-13','苹果','店面',1555.00); +INSERT INTO sales_data VALUES ('2019-06-14','桔子','淘宝',2008.00); +INSERT INTO sales_data VALUES ('2019-06-14','桔子','京东',2792.00); +INSERT INTO sales_data VALUES ('2019-06-14','桔子','店面',2465.00); +INSERT INTO sales_data VALUES ('2019-06-14','香蕉','淘宝',2805.00); +INSERT INTO sales_data VALUES ('2019-06-14','香蕉','京东',2006.00); +INSERT INTO sales_data VALUES ('2019-06-14','香蕉','店面',2568.00); +INSERT INTO sales_data VALUES ('2019-06-14','苹果','淘宝',1623.00); +INSERT INTO sales_data VALUES ('2019-06-14','苹果','京东',2276.00); +INSERT INTO sales_data VALUES ('2019-06-14','苹果','店面',2062.00); +INSERT INTO sales_data VALUES ('2019-06-15','桔子','淘宝',2677.00); +INSERT INTO sales_data VALUES ('2019-06-15','桔子','京东',2284.00); +INSERT INTO sales_data VALUES ('2019-06-15','桔子','店面',1793.00); +INSERT INTO sales_data VALUES ('2019-06-15','香蕉','淘宝',2514.00); +INSERT INTO sales_data VALUES ('2019-06-15','香蕉','京东',2262.00); +INSERT INTO sales_data VALUES ('2019-06-15','香蕉','店面',2542.00); +INSERT INTO sales_data VALUES ('2019-06-15','苹果','淘宝',1752.00); +INSERT INTO sales_data VALUES ('2019-06-15','苹果','京东',2334.00); +INSERT INTO sales_data VALUES ('2019-06-15','苹果','店面',1629.00); +INSERT INTO sales_data VALUES ('2019-06-16','桔子','淘宝',2561.00); +INSERT INTO sales_data VALUES ('2019-06-16','桔子','京东',2754.00); +INSERT INTO sales_data VALUES ('2019-06-16','桔子','店面',1343.00); +INSERT INTO sales_data VALUES ('2019-06-16','香蕉','淘宝',2125.00); +INSERT INTO sales_data VALUES ('2019-06-16','香蕉','京东',2261.00); +INSERT INTO sales_data VALUES ('2019-06-16','香蕉','店面',2760.00); +INSERT INTO sales_data VALUES ('2019-06-16','苹果','淘宝',2610.00); +INSERT INTO sales_data VALUES ('2019-06-16','苹果','京东',1592.00); +INSERT INTO sales_data VALUES ('2019-06-16','苹果','店面',2191.00); +INSERT INTO sales_data VALUES ('2019-06-17','桔子','淘宝',2563.00); +INSERT INTO sales_data VALUES ('2019-06-17','桔子','京东',2383.00); +INSERT INTO sales_data VALUES ('2019-06-17','桔子','店面',1701.00); +INSERT INTO sales_data VALUES ('2019-06-17','香蕉','淘宝',2804.00); +INSERT INTO sales_data VALUES ('2019-06-17','香蕉','京东',1572.00); +INSERT INTO sales_data VALUES ('2019-06-17','香蕉','店面',1674.00); +INSERT INTO sales_data VALUES ('2019-06-17','苹果','淘宝',2448.00); +INSERT INTO sales_data VALUES ('2019-06-17','苹果','京东',1557.00); +INSERT INTO sales_data VALUES ('2019-06-17','苹果','店面',2360.00); +INSERT INTO sales_data VALUES ('2019-06-18','桔子','淘宝',2201.00); +INSERT INTO sales_data VALUES ('2019-06-18','桔子','京东',1865.00); +INSERT INTO sales_data VALUES ('2019-06-18','桔子','店面',1821.00); +INSERT INTO sales_data VALUES ('2019-06-18','香蕉','淘宝',1444.00); +INSERT INTO sales_data VALUES ('2019-06-18','香蕉','京东',1716.00); +INSERT INTO sales_data VALUES ('2019-06-18','香蕉','店面',2780.00); +INSERT INTO sales_data VALUES ('2019-06-18','苹果','淘宝',1911.00); +INSERT INTO sales_data VALUES ('2019-06-18','苹果','京东',1405.00); +INSERT INTO sales_data VALUES ('2019-06-18','苹果','店面',2216.00); +INSERT INTO sales_data VALUES ('2019-06-19','桔子','淘宝',1634.00); +INSERT INTO sales_data VALUES ('2019-06-19','桔子','京东',1837.00); +INSERT INTO sales_data VALUES ('2019-06-19','桔子','店面',1730.00); +INSERT INTO sales_data VALUES ('2019-06-19','香蕉','淘宝',1938.00); +INSERT INTO sales_data VALUES ('2019-06-19','香蕉','京东',1568.00); +INSERT INTO sales_data VALUES ('2019-06-19','香蕉','店面',1655.00); +INSERT INTO sales_data VALUES ('2019-06-19','苹果','淘宝',1951.00); +INSERT INTO sales_data VALUES ('2019-06-19','苹果','京东',2363.00); +INSERT INTO sales_data VALUES ('2019-06-19','苹果','店面',2586.00); +INSERT INTO sales_data VALUES ('2019-06-20','桔子','淘宝',1886.00); +INSERT INTO sales_data VALUES ('2019-06-20','桔子','京东',2148.00); +INSERT INTO sales_data VALUES ('2019-06-20','桔子','店面',1352.00); +INSERT INTO sales_data VALUES ('2019-06-20','香蕉','淘宝',2747.00); +INSERT INTO sales_data VALUES ('2019-06-20','香蕉','京东',1876.00); +INSERT INTO sales_data VALUES ('2019-06-20','香蕉','店面',2401.00); +INSERT INTO sales_data VALUES ('2019-06-20','苹果','淘宝',1614.00); +INSERT INTO sales_data VALUES ('2019-06-20','苹果','京东',1845.00); +INSERT INTO sales_data VALUES ('2019-06-20','苹果','店面',2638.00); +INSERT INTO sales_data VALUES ('2019-06-21','桔子','淘宝',1958.00); +INSERT INTO sales_data VALUES ('2019-06-21','桔子','京东',1464.00); +INSERT INTO sales_data VALUES ('2019-06-21','桔子','店面',1364.00); +INSERT INTO sales_data VALUES ('2019-06-21','香蕉','淘宝',1483.00); +INSERT INTO sales_data VALUES ('2019-06-21','香蕉','京东',2325.00); +INSERT INTO sales_data VALUES ('2019-06-21','香蕉','店面',1889.00); +INSERT INTO sales_data VALUES ('2019-06-21','苹果','淘宝',1964.00); +INSERT INTO sales_data VALUES ('2019-06-21','苹果','京东',2429.00); +INSERT INTO sales_data VALUES ('2019-06-21','苹果','店面',2265.00); +INSERT INTO sales_data VALUES ('2019-06-22','桔子','淘宝',1908.00); +INSERT INTO sales_data VALUES ('2019-06-22','桔子','京东',1505.00); +INSERT INTO sales_data VALUES ('2019-06-22','桔子','店面',2336.00); +INSERT INTO sales_data VALUES ('2019-06-22','香蕉','淘宝',2785.00); +INSERT INTO sales_data VALUES ('2019-06-22','香蕉','京东',1794.00); +INSERT INTO sales_data VALUES ('2019-06-22','香蕉','店面',2828.00); +INSERT INTO sales_data VALUES ('2019-06-22','苹果','淘宝',1670.00); +INSERT INTO sales_data VALUES ('2019-06-22','苹果','京东',2387.00); +INSERT INTO sales_data VALUES ('2019-06-22','苹果','店面',1551.00); +INSERT INTO sales_data VALUES ('2019-06-23','桔子','淘宝',1985.00); +INSERT INTO sales_data VALUES ('2019-06-23','桔子','京东',1498.00); +INSERT INTO sales_data VALUES ('2019-06-23','桔子','店面',2573.00); +INSERT INTO sales_data VALUES ('2019-06-23','香蕉','淘宝',1725.00); +INSERT INTO sales_data VALUES ('2019-06-23','香蕉','京东',2034.00); +INSERT INTO sales_data VALUES ('2019-06-23','香蕉','店面',1871.00); +INSERT INTO sales_data VALUES ('2019-06-23','苹果','淘宝',1728.00); +INSERT INTO sales_data VALUES ('2019-06-23','苹果','京东',1932.00); +INSERT INTO sales_data VALUES ('2019-06-23','苹果','店面',2398.00); +INSERT INTO sales_data VALUES ('2019-06-24','桔子','淘宝',2784.00); +INSERT INTO sales_data VALUES ('2019-06-24','桔子','京东',2201.00); +INSERT INTO sales_data VALUES ('2019-06-24','桔子','店面',1398.00); +INSERT INTO sales_data VALUES ('2019-06-24','香蕉','淘宝',2571.00); +INSERT INTO sales_data VALUES ('2019-06-24','香蕉','京东',2803.00); +INSERT INTO sales_data VALUES ('2019-06-24','香蕉','店面',1507.00); +INSERT INTO sales_data VALUES ('2019-06-24','苹果','淘宝',2581.00); +INSERT INTO sales_data VALUES ('2019-06-24','苹果','京东',1431.00); +INSERT INTO sales_data VALUES ('2019-06-24','苹果','店面',2477.00); +INSERT INTO sales_data VALUES ('2019-06-25','桔子','淘宝',1620.00); +INSERT INTO sales_data VALUES ('2019-06-25','桔子','京东',2044.00); +INSERT INTO sales_data VALUES ('2019-06-25','桔子','店面',2055.00); +INSERT INTO sales_data VALUES ('2019-06-25','香蕉','淘宝',2530.00); +INSERT INTO sales_data VALUES ('2019-06-25','香蕉','京东',2593.00); +INSERT INTO sales_data VALUES ('2019-06-25','香蕉','店面',2201.00); +INSERT INTO sales_data VALUES ('2019-06-25','苹果','淘宝',2006.00); +INSERT INTO sales_data VALUES ('2019-06-25','苹果','京东',2517.00); +INSERT INTO sales_data VALUES ('2019-06-25','苹果','店面',2634.00); +INSERT INTO sales_data VALUES ('2019-06-26','桔子','淘宝',1979.00); +INSERT INTO sales_data VALUES ('2019-06-26','桔子','京东',2832.00); +INSERT INTO sales_data VALUES ('2019-06-26','桔子','店面',2166.00); +INSERT INTO sales_data VALUES ('2019-06-26','香蕉','淘宝',2170.00); +INSERT INTO sales_data VALUES ('2019-06-26','香蕉','京东',1952.00); +INSERT INTO sales_data VALUES ('2019-06-26','香蕉','店面',2299.00); +INSERT INTO sales_data VALUES ('2019-06-26','苹果','淘宝',1878.00); +INSERT INTO sales_data VALUES ('2019-06-26','苹果','京东',2312.00); +INSERT INTO sales_data VALUES ('2019-06-26','苹果','店面',1468.00); +INSERT INTO sales_data VALUES ('2019-06-27','桔子','淘宝',2389.00); +INSERT INTO sales_data VALUES ('2019-06-27','桔子','京东',2680.00); +INSERT INTO sales_data VALUES ('2019-06-27','桔子','店面',2040.00); +INSERT INTO sales_data VALUES ('2019-06-27','香蕉','淘宝',1922.00); +INSERT INTO sales_data VALUES ('2019-06-27','香蕉','京东',2594.00); +INSERT INTO sales_data VALUES ('2019-06-27','香蕉','店面',2870.00); +INSERT INTO sales_data VALUES ('2019-06-27','苹果','淘宝',1950.00); +INSERT INTO sales_data VALUES ('2019-06-27','苹果','京东',2296.00); +INSERT INTO sales_data VALUES ('2019-06-27','苹果','店面',2803.00); +INSERT INTO sales_data VALUES ('2019-06-28','桔子','淘宝',2092.00); +INSERT INTO sales_data VALUES ('2019-06-28','桔子','京东',2011.00); +INSERT INTO sales_data VALUES ('2019-06-28','桔子','店面',2869.00); +INSERT INTO sales_data VALUES ('2019-06-28','香蕉','淘宝',1699.00); +INSERT INTO sales_data VALUES ('2019-06-28','香蕉','京东',2256.00); +INSERT INTO sales_data VALUES ('2019-06-28','香蕉','店面',2038.00); +INSERT INTO sales_data VALUES ('2019-06-28','苹果','淘宝',2379.00); +INSERT INTO sales_data VALUES ('2019-06-28','苹果','京东',1911.00); +INSERT INTO sales_data VALUES ('2019-06-28','苹果','店面',1756.00); +INSERT INTO sales_data VALUES ('2019-06-29','桔子','淘宝',1709.00); +INSERT INTO sales_data VALUES ('2019-06-29','桔子','京东',2547.00); +INSERT INTO sales_data VALUES ('2019-06-29','桔子','店面',1403.00); +INSERT INTO sales_data VALUES ('2019-06-29','香蕉','淘宝',1469.00); +INSERT INTO sales_data VALUES ('2019-06-29','香蕉','京东',1646.00); +INSERT INTO sales_data VALUES ('2019-06-29','香蕉','店面',2856.00); +INSERT INTO sales_data VALUES ('2019-06-29','苹果','淘宝',2255.00); +INSERT INTO sales_data VALUES ('2019-06-29','苹果','京东',2436.00); +INSERT INTO sales_data VALUES ('2019-06-29','苹果','店面',1928.00); +INSERT INTO sales_data VALUES ('2019-06-30','桔子','淘宝',1679.00); +INSERT INTO sales_data VALUES ('2019-06-30','桔子','京东',1439.00); +INSERT INTO sales_data VALUES ('2019-06-30','桔子','店面',2865.00); +INSERT INTO sales_data VALUES ('2019-06-30','香蕉','淘宝',1767.00); +INSERT INTO sales_data VALUES ('2019-06-30','香蕉','京东',2443.00); +INSERT INTO sales_data VALUES ('2019-06-30','香蕉','店面',2660.00); +INSERT INTO sales_data VALUES ('2019-06-30','苹果','淘宝',2422.00); +INSERT INTO sales_data VALUES ('2019-06-30','苹果','京东',1481.00); +INSERT INTO sales_data VALUES ('2019-06-30','苹果','店面',2369.00); +``` + +# SQL +``` +SELECT + sd.product AS '产品', + sd.channel AS '渠道', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '1月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '2月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '3月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '4月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '5月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '6月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '7月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '8月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '9月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '10月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '11月', + SUM( + CASE + EXTRACT(MONTH FROM sd.saledate) + WHEN 1 THEN amount + END + ) + AS '12月' +FROM + sales_data sd +GROUP BY + sd.product,sd.channel; + +``` + +- 得到结果 + + ![sql-13]({{site.baseurl}}/img-post/sql-13.png) diff --git "a/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 CROSS JOIN \345\256\236\347\216\260\345\244\232\350\241\250\346\237\245\350\257\242.md" "b/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 CROSS JOIN \345\256\236\347\216\260\345\244\232\350\241\250\346\237\245\350\257\242.md" new file mode 100644 index 00000000000..31dd53215f0 --- /dev/null +++ "b/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 CROSS JOIN \345\256\236\347\216\260\345\244\232\350\241\250\346\237\245\350\257\242.md" @@ -0,0 +1,627 @@ +--- +layout: post +title: SQL:使用 CROSS JOIN 实现多表查询 +subtitle: 统计员工的出勤情况 +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + +# 创建表 +- 需求: + - 需要根据 employee、attendance、calendar 三张表,统计员工的出勤情况; +- 创建日历表 calendar + + ```aidl + CREATE TABLE calendar( + id INTEGER NOT NULL PRIMARY KEY, -- 日历编号 + calendar_date DATE NOT NULL UNIQUE, -- 日历日期 + calendar_year INTEGER NOT NULL, -- 日历年 + calendar_month INTEGER NOT NULL, -- 日历月 + calendar_day INTEGER NOT NULL, -- 日历日 + is_work_day VARCHAR(1) DEFAULT 'Y' NOT NULL -- 是否工作日 + ); + ``` +- 创建考勤记录表 attendance + ``` + CREATE TABLE attendance( + id INTEGER NOT NULL PRIMARY KEY, -- 考勤记录编号 + check_date DATE NOT NULL, -- 考勤日期 + emp_id INTEGER NOT NULL, -- 员工编号 + clock_in TIMESTAMP, -- 上班打卡时间 + clock_out TIMESTAMP, -- 下班打卡时间 + CONSTRAINT uk_attendance UNIQUE (check_date, emp_id) + ); + ``` +- 创建员工表 employee + ``` + CREATE TABLE `employee` ( + `employee_id` int NOT NULL AUTO_INCREMENT, + `employee_name` varchar(45) DEFAULT NULL, + `department_id` int DEFAULT NULL, + `job_id` int DEFAULT NULL, + PRIMARY KEY (`employee_id`), + UNIQUE KEY `employee_id_UNIQUE` (`employee_id`) + ) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + ``` + +# 写入测试数据 + +```aidl +-- 生成测试数据 +-- Oracle 需要执行以下 ALTER 语句 +-- ALTER SESSION SET nls_date_format = 'YYYY-MM-DD'; +-- ALTER SESSION SET nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS.FF'; +INSERT INTO calendar VALUES (1,'2021-01-01',2021,1,1,'N'); +INSERT INTO calendar VALUES (2,'2021-01-02',2021,1,2,'N'); +INSERT INTO calendar VALUES (3,'2021-01-03',2021,1,3,'N'); +INSERT INTO calendar VALUES (4,'2021-01-04',2021,1,4,'Y'); +INSERT INTO calendar VALUES (5,'2021-01-05',2021,1,5,'Y'); +INSERT INTO calendar VALUES (6,'2021-01-06',2021,1,6,'Y'); +INSERT INTO calendar VALUES (7,'2021-01-07',2021,1,7,'Y'); +INSERT INTO calendar VALUES (8,'2021-01-08',2021,1,8,'Y'); +INSERT INTO calendar VALUES (9,'2021-01-09',2021,1,9,'N'); +INSERT INTO calendar VALUES (10,'2021-01-10',2021,1,10,'N'); +INSERT INTO calendar VALUES (11,'2021-01-11',2021,1,11,'Y'); +INSERT INTO calendar VALUES (12,'2021-01-12',2021,1,12,'Y'); +INSERT INTO calendar VALUES (13,'2021-01-13',2021,1,13,'Y'); +INSERT INTO calendar VALUES (14,'2021-01-14',2021,1,14,'Y'); +INSERT INTO calendar VALUES (15,'2021-01-15',2021,1,15,'Y'); +INSERT INTO calendar VALUES (16,'2021-01-16',2021,1,16,'N'); +INSERT INTO calendar VALUES (17,'2021-01-17',2021,1,17,'N'); +INSERT INTO calendar VALUES (18,'2021-01-18',2021,1,18,'Y'); +INSERT INTO calendar VALUES (19,'2021-01-19',2021,1,19,'Y'); +INSERT INTO calendar VALUES (20,'2021-01-20',2021,1,20,'Y'); +INSERT INTO calendar VALUES (21,'2021-01-21',2021,1,21,'Y'); +INSERT INTO calendar VALUES (22,'2021-01-22',2021,1,22,'Y'); +INSERT INTO calendar VALUES (23,'2021-01-23',2021,1,23,'N'); +INSERT INTO calendar VALUES (24,'2021-01-24',2021,1,24,'N'); +INSERT INTO calendar VALUES (25,'2021-01-25',2021,1,25,'Y'); +INSERT INTO calendar VALUES (26,'2021-01-26',2021,1,26,'Y'); +INSERT INTO calendar VALUES (27,'2021-01-27',2021,1,27,'Y'); +INSERT INTO calendar VALUES (28,'2021-01-28',2021,1,28,'Y'); +INSERT INTO calendar VALUES (29,'2021-01-29',2021,1,29,'Y'); +INSERT INTO calendar VALUES (30,'2021-01-30',2021,1,30,'N'); +INSERT INTO calendar VALUES (31,'2021-01-31',2021,1,31,'N'); + +INSERT INTO attendance VALUES (1,'2021-01-04',1,'2021-01-04 08:34:02.374','2021-01-04 18:33:11.842'); +INSERT INTO attendance VALUES (2,'2021-01-04',2,'2021-01-04 08:10:31.367','2021-01-04 19:11:59.19'); +INSERT INTO attendance VALUES (3,'2021-01-04',3,'2021-01-04 08:54:08.807','2021-01-04 18:27:21.348'); +INSERT INTO attendance VALUES (4,'2021-01-04',4,'2021-01-04 08:27:55.39','2021-01-04 18:10:16.862'); +INSERT INTO attendance VALUES (5,'2021-01-04',5,'2021-01-04 08:39:34.557','2021-01-04 18:36:10.973'); +INSERT INTO attendance VALUES (6,'2021-01-04',6,'2021-01-04 08:32:34.859','2021-01-04 19:29:04.401'); +INSERT INTO attendance VALUES (7,'2021-01-04',7,'2021-01-04 08:22:57.576','2021-01-04 19:25:37.615'); +INSERT INTO attendance VALUES (8,'2021-01-04',8,'2021-01-04 08:45:56.07','2021-01-04 18:46:10.026'); +INSERT INTO attendance VALUES (9,'2021-01-04',9,'2021-01-04 08:13:23.886','2021-01-04 19:24:29.827'); +INSERT INTO attendance VALUES (10,'2021-01-04',10,'2021-01-04 08:45:31.543','2021-01-04 18:21:02.158'); +INSERT INTO attendance VALUES (11,'2021-01-04',11,'2021-01-04 08:35:28.413','2021-01-04 18:11:34.613'); +INSERT INTO attendance VALUES (12,'2021-01-04',12,'2021-01-04 08:03:29.148','2021-01-04 18:50:04.368'); +INSERT INTO attendance VALUES (13,'2021-01-04',13,'2021-01-04 08:02:13.397','2021-01-04 19:27:37.883'); +INSERT INTO attendance VALUES (14,'2021-01-04',14,'2021-01-04 08:26:13.715','2021-01-04 18:23:04.015'); +INSERT INTO attendance VALUES (15,'2021-01-04',15,'2021-01-04 08:13:55.707','2021-01-04 19:29:21.996'); +INSERT INTO attendance VALUES (16,'2021-01-04',16,'2021-01-04 08:15:23.972','2021-01-04 18:34:22.918'); +INSERT INTO attendance VALUES (17,'2021-01-04',17,'2021-01-04 08:04:11.176','2021-01-04 18:20:36.873'); +INSERT INTO attendance VALUES (18,'2021-01-04',18,'2021-01-04 08:41:42.695','2021-01-04 19:18:09.091'); +INSERT INTO attendance VALUES (19,'2021-01-04',19,'2021-01-04 08:34:25.891','2021-01-04 18:09:18.634'); +INSERT INTO attendance VALUES (20,'2021-01-04',20,'2021-01-04 08:31:51.219','2021-01-04 18:33:13.3'); +INSERT INTO attendance VALUES (21,'2021-01-04',21,'2021-01-04 08:18:28.941','2021-01-04 19:22:39.781'); +INSERT INTO attendance VALUES (22,'2021-01-04',22,'2021-01-04 08:04:17.153','2021-01-04 18:47:44.909'); +INSERT INTO attendance VALUES (23,'2021-01-04',23,'2021-01-04 08:46:59.726','2021-01-04 18:59:09.632'); +INSERT INTO attendance VALUES (24,'2021-01-04',24,'2021-01-04 08:18:05.634','2021-01-04 18:51:09.465'); +INSERT INTO attendance VALUES (25,'2021-01-04',25,'2021-01-04 08:45:10.735','2021-01-04 18:42:57.763'); +INSERT INTO attendance VALUES (26,'2021-01-05',1,'2021-01-05 08:35:51.082','2021-01-05 18:01:37.954'); +INSERT INTO attendance VALUES (27,'2021-01-05',2,'2021-01-05 08:01:41.689','2021-01-05 18:25:56.633'); +INSERT INTO attendance VALUES (28,'2021-01-05',3,'2021-01-05 08:08:05.755','2021-01-05 19:07:14.298'); +INSERT INTO attendance VALUES (29,'2021-01-05',4,'2021-01-05 08:10:07.258','2021-01-05 18:15:52.768'); +INSERT INTO attendance VALUES (30,'2021-01-05',5,'2021-01-05 08:47:56.777','2021-01-05 18:39:51.89'); +INSERT INTO attendance VALUES (31,'2021-01-05',6,'2021-01-05 08:55:39.959','2021-01-05 18:58:30.74'); +INSERT INTO attendance VALUES (32,'2021-01-05',7,'2021-01-05 08:56:55.547','2021-01-05 18:14:43.76'); +INSERT INTO attendance VALUES (33,'2021-01-05',8,'2021-01-05 08:21:23.269','2021-01-05 18:54:58.262'); +INSERT INTO attendance VALUES (34,'2021-01-05',9,'2021-01-05 08:13:38.294','2021-01-05 18:47:59.95'); +INSERT INTO attendance VALUES (35,'2021-01-05',10,'2021-01-05 08:22:34.44','2021-01-05 18:08:05.129'); +INSERT INTO attendance VALUES (36,'2021-01-05',11,'2021-01-05 08:57:59.632','2021-01-05 19:02:17.866'); +INSERT INTO attendance VALUES (37,'2021-01-05',12,'2021-01-05 08:32:48.528','2021-01-05 19:17:10.785'); +INSERT INTO attendance VALUES (38,'2021-01-05',13,'2021-01-05 08:19:42.181','2021-01-05 19:15:32.31'); +INSERT INTO attendance VALUES (39,'2021-01-05',14,'2021-01-05 08:41:21.615','2021-01-05 18:20:57.274'); +INSERT INTO attendance VALUES (40,'2021-01-05',15,'2021-01-05 08:32:28.488','2021-01-05 18:25:49.258'); +INSERT INTO attendance VALUES (41,'2021-01-05',16,'2021-01-05 08:36:44.328','2021-01-05 19:03:23.056'); +INSERT INTO attendance VALUES (42,'2021-01-05',17,'2021-01-05 08:26:13.336','2021-01-05 19:19:58.026'); +INSERT INTO attendance VALUES (43,'2021-01-05',18,'2021-01-05 08:05:03.891','2021-01-05 18:08:00.424'); +INSERT INTO attendance VALUES (44,'2021-01-05',19,'2021-01-05 08:29:07.198','2021-01-05 18:46:56.679'); +INSERT INTO attendance VALUES (45,'2021-01-05',20,'2021-01-05 08:48:28.622','2021-01-05 18:05:38.832'); +INSERT INTO attendance VALUES (46,'2021-01-05',21,'2021-01-05 08:48:12.368','2021-01-05 19:13:39.761'); +INSERT INTO attendance VALUES (47,'2021-01-05',22,'2021-01-05 08:37:37.291','2021-01-05 18:01:37.542'); +INSERT INTO attendance VALUES (48,'2021-01-05',23,'2021-01-05 08:11:57.007','2021-01-05 19:18:04.795'); +INSERT INTO attendance VALUES (49,'2021-01-05',25,'2021-01-05 08:37:47.171','2021-01-05 18:04:13.12'); +INSERT INTO attendance VALUES (50,'2021-01-06',1,'2021-01-06 08:45:16.057','2021-01-06 18:18:08.261'); +INSERT INTO attendance VALUES (51,'2021-01-06',2,'2021-01-06 08:21:41.834','2021-01-06 18:51:13.415'); +INSERT INTO attendance VALUES (52,'2021-01-06',3,'2021-01-06 08:25:45.385','2021-01-06 19:19:22.325'); +INSERT INTO attendance VALUES (53,'2021-01-06',4,'2021-01-06 08:49:05.149','2021-01-06 18:42:16.572'); +INSERT INTO attendance VALUES (54,'2021-01-06',5,'2021-01-06 08:00:53.742','2021-01-06 18:45:52.32'); +INSERT INTO attendance VALUES (55,'2021-01-06',6,'2021-01-06 08:24:45.381','2021-01-06 18:43:21.977'); +INSERT INTO attendance VALUES (56,'2021-01-06',7,'2021-01-06 08:42:03.426','2021-01-06 18:18:21.11'); +INSERT INTO attendance VALUES (57,'2021-01-06',8,'2021-01-06 08:58:40.471','2021-01-06 18:20:25.922'); +INSERT INTO attendance VALUES (58,'2021-01-06',9,'2021-01-06 08:36:45.124','2021-01-06 18:56:28.7'); +INSERT INTO attendance VALUES (59,'2021-01-06',10,'2021-01-06 08:54:35.033','2021-01-06 19:23:13.231'); +INSERT INTO attendance VALUES (60,'2021-01-06',11,'2021-01-06 08:05:28.988','2021-01-06 19:14:58.245'); +INSERT INTO attendance VALUES (61,'2021-01-06',12,'2021-01-06 08:22:27.423','2021-01-06 18:52:13.498'); +INSERT INTO attendance VALUES (62,'2021-01-06',13,'2021-01-06 08:59:35.511','2021-01-06 19:26:11.478'); +INSERT INTO attendance VALUES (63,'2021-01-06',14,'2021-01-06 08:02:04.065','2021-01-06 18:21:37.454'); +INSERT INTO attendance VALUES (64,'2021-01-06',15,'2021-01-06 08:38:40.04','2021-01-06 19:04:11.615'); +INSERT INTO attendance VALUES (65,'2021-01-06',16,'2021-01-06 08:40:40.106','2021-01-06 18:23:46.659'); +INSERT INTO attendance VALUES (66,'2021-01-06',17,'2021-01-06 08:03:32.269','2021-01-06 18:53:30.189'); +INSERT INTO attendance VALUES (67,'2021-01-06',18,'2021-01-06 08:01:11.163','2021-01-06 19:29:14.124'); +INSERT INTO attendance VALUES (68,'2021-01-06',19,'2021-01-06 08:16:01.192','2021-01-06 18:19:11.921'); +INSERT INTO attendance VALUES (69,'2021-01-06',20,'2021-01-06 08:41:32.83','2021-01-06 18:55:59.573'); +INSERT INTO attendance VALUES (70,'2021-01-06',21,'2021-01-06 08:19:59.225','2021-01-06 18:48:33.704'); +INSERT INTO attendance VALUES (71,'2021-01-06',22,'2021-01-06 08:29:26.286','2021-01-06 18:53:50.085'); +INSERT INTO attendance VALUES (72,'2021-01-06',23,'2021-01-06 08:14:30.847','2021-01-06 19:15:48.219'); +INSERT INTO attendance VALUES (73,'2021-01-06',24,'2021-01-06 08:29:50.292','2021-01-06 18:42:53.553'); +INSERT INTO attendance VALUES (74,'2021-01-06',25,'2021-01-06 08:11:57.989','2021-01-06 18:17:05.313'); +INSERT INTO attendance VALUES (75,'2021-01-07',1,'2021-01-07 08:50:21.479','2021-01-07 18:37:00.492'); +INSERT INTO attendance VALUES (76,'2021-01-07',2,'2021-01-07 08:49:47.402','2021-01-07 18:53:27.009'); +INSERT INTO attendance VALUES (77,'2021-01-07',3,'2021-01-07 08:51:43.025','2021-01-07 19:20:06.793'); +INSERT INTO attendance VALUES (78,'2021-01-07',4,'2021-01-07 08:22:49.782','2021-01-07 18:29:53.198'); +INSERT INTO attendance VALUES (79,'2021-01-07',5,'2021-01-07 08:36:08.252','2021-01-07 18:02:12.076'); +INSERT INTO attendance VALUES (80,'2021-01-07',6,'2021-01-07 08:43:20.858','2021-01-07 19:02:14.235'); +INSERT INTO attendance VALUES (81,'2021-01-07',7,'2021-01-07 08:18:01.76','2021-01-07 18:32:02.165'); +INSERT INTO attendance VALUES (82,'2021-01-07',8,'2021-01-07 08:40:24.227','2021-01-07 18:25:56.654'); +INSERT INTO attendance VALUES (83,'2021-01-07',9,'2021-01-07 08:16:49.757','2021-01-07 18:20:01.265'); +INSERT INTO attendance VALUES (84,'2021-01-07',10,'2021-01-07 08:43:35.134','2021-01-07 18:35:03.231'); +INSERT INTO attendance VALUES (85,'2021-01-07',11,'2021-01-07 08:50:44.161','2021-01-07 18:07:04.51'); +INSERT INTO attendance VALUES (86,'2021-01-07',12,'2021-01-07 08:35:39.053','2021-01-07 18:06:55.982'); +INSERT INTO attendance VALUES (87,'2021-01-07',13,'2021-01-07 08:45:47.697','2021-01-07 18:56:41.041'); +INSERT INTO attendance VALUES (88,'2021-01-07',14,'2021-01-07 08:13:20.802','2021-01-07 18:29:31.599'); +INSERT INTO attendance VALUES (89,'2021-01-07',15,'2021-01-07 08:30:44.08','2021-01-07 18:44:22.748'); +INSERT INTO attendance VALUES (90,'2021-01-07',16,'2021-01-07 08:32:57.825','2021-01-07 19:21:25.719'); +INSERT INTO attendance VALUES (91,'2021-01-07',17,'2021-01-07 08:33:26.599','2021-01-07 18:10:13.527'); +INSERT INTO attendance VALUES (92,'2021-01-07',18,'2021-01-07 08:15:17.918','2021-01-07 18:14:36.04'); +INSERT INTO attendance VALUES (93,'2021-01-07',19,'2021-01-07 08:59:15.325','2021-01-07 18:57:17.367'); +INSERT INTO attendance VALUES (94,'2021-01-07',20,'2021-01-07 08:18:40.677','2021-01-07 18:51:35.671'); +INSERT INTO attendance VALUES (95,'2021-01-07',21,'2021-01-07 08:30:34.002','2021-01-07 18:45:32.63'); +INSERT INTO attendance VALUES (96,'2021-01-07',22,'2021-01-07 08:50:42.612','2021-01-07 18:31:16.777'); +INSERT INTO attendance VALUES (97,'2021-01-07',23,'2021-01-07 08:33:49.24','2021-01-07 18:39:07.772'); +INSERT INTO attendance VALUES (98,'2021-01-07',24,'2021-01-07 08:26:40.186','2021-01-07 18:13:55.498'); +INSERT INTO attendance VALUES (99,'2021-01-07',25,'2021-01-07 08:01:30.599','2021-01-07 18:28:23.797'); +INSERT INTO attendance VALUES (100,'2021-01-08',1,'2021-01-10 08:00:02.771','2021-01-10 18:45:02.915'); +INSERT INTO attendance VALUES (101,'2021-01-08',2,'2021-01-10 08:18:19.509','2021-01-10 18:52:45.721'); +INSERT INTO attendance VALUES (102,'2021-01-08',3,'2021-01-10 08:21:39.68','2021-01-10 18:56:59.892'); +INSERT INTO attendance VALUES (103,'2021-01-08',4,'2021-01-10 08:46:22.282','2021-01-10 18:40:05.496'); +INSERT INTO attendance VALUES (104,'2021-01-08',5,'2021-01-10 08:40:10.87','2021-01-10 18:20:43.218'); +INSERT INTO attendance VALUES (105,'2021-01-08',6,'2021-01-10 08:02:08.985','2021-01-10 18:17:35.714'); +INSERT INTO attendance VALUES (106,'2021-01-08',7,'2021-01-10 08:56:40.194','2021-01-10 18:15:42.569'); +INSERT INTO attendance VALUES (107,'2021-01-08',8,'2021-01-10 08:20:48.469','2021-01-10 18:49:15.59'); +INSERT INTO attendance VALUES (108,'2021-01-08',9,'2021-01-10 08:17:14.243','2021-01-10 18:29:35.534'); +INSERT INTO attendance VALUES (109,'2021-01-08',10,'2021-01-10 08:18:16.177','2021-01-10 18:15:41.696'); +INSERT INTO attendance VALUES (110,'2021-01-08',11,'2021-01-10 08:29:09.5','2021-01-10 19:11:33.418'); +INSERT INTO attendance VALUES (111,'2021-01-08',12,'2021-01-10 08:21:22.473','2021-01-10 19:17:31.9'); +INSERT INTO attendance VALUES (112,'2021-01-08',13,'2021-01-10 08:20:29.483','2021-01-10 18:16:44.212'); +INSERT INTO attendance VALUES (113,'2021-01-08',14,'2021-01-10 08:51:19.459','2021-01-10 19:16:58.311'); +INSERT INTO attendance VALUES (114,'2021-01-08',15,'2021-01-10 08:32:46.01','2021-01-10 18:25:57.469'); +INSERT INTO attendance VALUES (115,'2021-01-08',16,'2021-01-10 08:05:26.84','2021-01-10 18:17:32.939'); +INSERT INTO attendance VALUES (116,'2021-01-08',17,'2021-01-10 08:19:07.136','2021-01-10 18:42:38.108'); +INSERT INTO attendance VALUES (117,'2021-01-08',18,'2021-01-10 08:11:44.637','2021-01-10 19:08:20.217'); +INSERT INTO attendance VALUES (118,'2021-01-08',19,'2021-01-10 08:15:48.439','2021-01-10 18:18:31.299'); +INSERT INTO attendance VALUES (119,'2021-01-08',20,'2021-01-10 08:40:23.455','2021-01-10 18:48:22.836'); +INSERT INTO attendance VALUES (120,'2021-01-08',21,'2021-01-10 08:30:28.016','2021-01-10 18:07:37.526'); +INSERT INTO attendance VALUES (121,'2021-01-08',22,'2021-01-10 08:08:44.252','2021-01-10 18:18:22.9'); +INSERT INTO attendance VALUES (122,'2021-01-08',23,'2021-01-10 08:40:58.678','2021-01-10 18:26:24.982'); +INSERT INTO attendance VALUES (123,'2021-01-08',24,'2021-01-10 08:13:22.774','2021-01-10 18:13:21.348'); +INSERT INTO attendance VALUES (124,'2021-01-08',25,'2021-01-10 08:21:54.26','2021-01-10 18:46:37.627'); +INSERT INTO attendance VALUES (125,'2021-01-11',1,'2021-01-11 08:57:00.945','2021-01-11 18:09:27.372'); +INSERT INTO attendance VALUES (126,'2021-01-11',2,'2021-01-11 08:34:49.107','2021-01-11 18:27:08.029'); +INSERT INTO attendance VALUES (127,'2021-01-11',3,'2021-01-11 08:40:24.455','2021-01-11 18:37:44.055'); +INSERT INTO attendance VALUES (128,'2021-01-11',4,'2021-01-11 08:10:23.142','2021-01-11 18:58:14.284'); +INSERT INTO attendance VALUES (129,'2021-01-11',5,'2021-01-11 08:50:29.087','2021-01-11 18:16:10.282'); +INSERT INTO attendance VALUES (130,'2021-01-11',6,'2021-01-11 08:13:32.339','2021-01-11 19:28:45.072'); +INSERT INTO attendance VALUES (131,'2021-01-11',7,'2021-01-11 08:13:12.828','2021-01-11 18:20:18.741'); +INSERT INTO attendance VALUES (132,'2021-01-11',8,'2021-01-11 08:20:59.383','2021-01-11 19:18:06.405'); +INSERT INTO attendance VALUES (133,'2021-01-11',9,'2021-01-11 08:16:47.614','2021-01-11 19:24:18.563'); +INSERT INTO attendance VALUES (134,'2021-01-11',10,'2021-01-11 08:13:50.422','2021-01-11 18:37:31.179'); +INSERT INTO attendance VALUES (135,'2021-01-11',12,'2021-01-11 08:21:39.938','2021-01-11 18:40:20.338'); +INSERT INTO attendance VALUES (136,'2021-01-11',13,'2021-01-11 08:47:20.85','2021-01-11 19:18:33.5'); +INSERT INTO attendance VALUES (137,'2021-01-11',14,'2021-01-11 08:44:57.965','2021-01-11 19:11:51.117'); +INSERT INTO attendance VALUES (138,'2021-01-11',15,'2021-01-11 08:15:54.76','2021-01-11 18:10:47.549'); +INSERT INTO attendance VALUES (139,'2021-01-11',16,'2021-01-11 08:22:36.486','2021-01-11 18:02:34.481'); +INSERT INTO attendance VALUES (140,'2021-01-11',17,'2021-01-11 08:45:06.179','2021-01-11 19:15:33.943'); +INSERT INTO attendance VALUES (141,'2021-01-11',18,'2021-01-11 08:41:05.886','2021-01-11 19:18:29.987'); +INSERT INTO attendance VALUES (142,'2021-01-11',19,'2021-01-11 08:11:54.073','2021-01-11 18:36:03.986'); +INSERT INTO attendance VALUES (143,'2021-01-11',20,'2021-01-11 08:04:17.52','2021-01-11 18:59:36.99'); +INSERT INTO attendance VALUES (144,'2021-01-11',21,'2021-01-11 08:45:17.274','2021-01-11 19:14:09.068'); +INSERT INTO attendance VALUES (145,'2021-01-11',22,'2021-01-11 08:08:40.692','2021-01-11 18:19:27.634'); +INSERT INTO attendance VALUES (146,'2021-01-11',23,'2021-01-11 08:29:31.58','2021-01-11 18:57:01.788'); +INSERT INTO attendance VALUES (147,'2021-01-11',24,'2021-01-11 08:46:41.935','2021-01-11 18:39:44.221'); +INSERT INTO attendance VALUES (148,'2021-01-11',25,'2021-01-11 08:29:31.073','2021-01-11 19:09:03.324'); +INSERT INTO attendance VALUES (149,'2021-01-12',1,'2021-01-12 08:27:51.502','2021-01-12 18:00:14.981'); +INSERT INTO attendance VALUES (150,'2021-01-12',2,'2021-01-12 08:19:02.394','2021-01-12 19:09:36.293'); +INSERT INTO attendance VALUES (151,'2021-01-12',3,'2021-01-12 08:10:16.364','2021-01-12 18:45:00.56'); +INSERT INTO attendance VALUES (152,'2021-01-12',4,'2021-01-12 08:44:01.316','2021-01-12 18:38:02.932'); +INSERT INTO attendance VALUES (153,'2021-01-12',5,'2021-01-12 08:35:28.988','2021-01-12 18:42:53.776'); +INSERT INTO attendance VALUES (154,'2021-01-12',6,'2021-01-12 08:38:16.505','2021-01-12 18:38:35.161'); +INSERT INTO attendance VALUES (155,'2021-01-12',7,'2021-01-12 08:47:55.921','2021-01-12 18:24:38.188'); +INSERT INTO attendance VALUES (156,'2021-01-12',8,'2021-01-12 08:33:50.104','2021-01-12 18:25:24.031'); +INSERT INTO attendance VALUES (157,'2021-01-12',9,'2021-01-12 08:20:02.087','2021-01-12 18:47:02.544'); +INSERT INTO attendance VALUES (158,'2021-01-12',10,'2021-01-12 08:22:52.628','2021-01-12 18:48:35.72'); +INSERT INTO attendance VALUES (159,'2021-01-12',11,'2021-01-12 08:46:45.411','2021-01-12 19:29:10.451'); +INSERT INTO attendance VALUES (160,'2021-01-12',12,'2021-01-12 08:27:02.773','2021-01-12 18:26:24.641'); +INSERT INTO attendance VALUES (161,'2021-01-12',13,'2021-01-12 08:14:36.183','2021-01-12 18:35:29.542'); +INSERT INTO attendance VALUES (162,'2021-01-12',14,'2021-01-12 08:15:23.594','2021-01-12 18:50:17.252'); +INSERT INTO attendance VALUES (163,'2021-01-12',15,'2021-01-12 08:46:11.919','2021-01-12 19:01:21.258'); +INSERT INTO attendance VALUES (164,'2021-01-12',16,'2021-01-12 08:54:53.972','2021-01-12 19:05:57.831'); +INSERT INTO attendance VALUES (165,'2021-01-12',17,'2021-01-12 08:55:01.843','2021-01-12 18:36:39.325'); +INSERT INTO attendance VALUES (166,'2021-01-12',18,'2021-01-12 08:21:36.517','2021-01-12 19:18:04.027'); +INSERT INTO attendance VALUES (167,'2021-01-12',19,'2021-01-12 08:53:07.749','2021-01-12 19:08:21.163'); +INSERT INTO attendance VALUES (168,'2021-01-12',20,'2021-01-12 08:08:22.908','2021-01-12 19:23:16.957'); +INSERT INTO attendance VALUES (169,'2021-01-12',21,'2021-01-12 08:00:35.768','2021-01-12 18:19:50.04'); +INSERT INTO attendance VALUES (170,'2021-01-12',22,'2021-01-12 08:15:22.994','2021-01-12 18:10:19.162'); +INSERT INTO attendance VALUES (171,'2021-01-12',23,'2021-01-12 08:49:14.259','2021-01-12 19:02:49.075'); +INSERT INTO attendance VALUES (172,'2021-01-12',24,'2021-01-12 08:47:34.259','2021-01-12 19:29:53.1'); +INSERT INTO attendance VALUES (173,'2021-01-12',25,'2021-01-12 08:33:29.381','2021-01-12 18:18:34.01'); +INSERT INTO attendance VALUES (174,'2021-01-13',1,'2021-01-13 08:43:23.796','2021-01-13 18:13:41.737'); +INSERT INTO attendance VALUES (175,'2021-01-13',2,'2021-01-13 08:20:30.036','2021-01-13 18:47:04.963'); +INSERT INTO attendance VALUES (176,'2021-01-13',3,'2021-01-13 08:47:38.705','2021-01-13 18:14:49.647'); +INSERT INTO attendance VALUES (177,'2021-01-13',4,'2021-01-13 08:28:40.134','2021-01-13 18:19:05.091'); +INSERT INTO attendance VALUES (178,'2021-01-13',5,'2021-01-13 08:59:29.427','2021-01-13 18:09:09.892'); +INSERT INTO attendance VALUES (179,'2021-01-13',6,'2021-01-13 08:08:23.63','2021-01-13 18:29:27.237'); +INSERT INTO attendance VALUES (180,'2021-01-13',7,'2021-01-13 08:02:56.181','2021-01-13 18:47:24.366'); +INSERT INTO attendance VALUES (181,'2021-01-13',8,'2021-01-13 08:45:29.247','2021-01-13 18:47:56.194'); +INSERT INTO attendance VALUES (182,'2021-01-13',9,'2021-01-13 08:25:34.331','2021-01-13 18:01:13.014'); +INSERT INTO attendance VALUES (183,'2021-01-13',10,'2021-01-13 08:21:22.966','2021-01-13 18:53:30.803'); +INSERT INTO attendance VALUES (184,'2021-01-13',11,'2021-01-13 08:50:14.01','2021-01-13 18:31:13.346'); +INSERT INTO attendance VALUES (185,'2021-01-13',12,'2021-01-13 08:49:50.759','2021-01-13 18:44:15.652'); +INSERT INTO attendance VALUES (186,'2021-01-13',13,'2021-01-13 08:38:14.802','2021-01-13 18:49:45.39'); +INSERT INTO attendance VALUES (187,'2021-01-13',14,'2021-01-13 08:05:23.458','2021-01-13 18:52:04.165'); +INSERT INTO attendance VALUES (188,'2021-01-13',15,'2021-01-13 08:45:41.343','2021-01-13 18:33:32.467'); +INSERT INTO attendance VALUES (189,'2021-01-13',16,'2021-01-13 08:34:28.19',NULL); +INSERT INTO attendance VALUES (190,'2021-01-13',17,'2021-01-13 08:59:36.18','2021-01-13 19:16:57.773'); +INSERT INTO attendance VALUES (191,'2021-01-13',18,'2021-01-13 08:29:18.256','2021-01-13 19:02:20.427'); +INSERT INTO attendance VALUES (192,'2021-01-13',19,'2021-01-13 08:55:57.874','2021-01-13 18:07:46.404'); +INSERT INTO attendance VALUES (193,'2021-01-13',20,'2021-01-13 08:00:40.237','2021-01-13 19:18:27.254'); +INSERT INTO attendance VALUES (194,'2021-01-13',21,'2021-01-13 08:26:29.17','2021-01-13 19:27:35.004'); +INSERT INTO attendance VALUES (195,'2021-01-13',22,'2021-01-13 08:10:22.418','2021-01-13 19:02:01.405'); +INSERT INTO attendance VALUES (196,'2021-01-13',23,'2021-01-13 08:41:06.836','2021-01-13 19:19:19.324'); +INSERT INTO attendance VALUES (197,'2021-01-13',24,'2021-01-13 08:06:11.435','2021-01-13 19:16:40.933'); +INSERT INTO attendance VALUES (198,'2021-01-13',25,'2021-01-13 08:04:20.755','2021-01-13 19:16:32.221'); +INSERT INTO attendance VALUES (199,'2021-01-14',1,'2021-01-14 08:07:59.481','2021-01-14 19:20:30.471'); +INSERT INTO attendance VALUES (200,'2021-01-14',2,'2021-01-14 08:08:51.482','2021-01-14 19:00:19.871'); +INSERT INTO attendance VALUES (201,'2021-01-14',3,'2021-01-14 08:50:55.823','2021-01-14 18:19:43.245'); +INSERT INTO attendance VALUES (202,'2021-01-14',4,'2021-01-14 08:04:31.95','2021-01-14 19:25:35.633'); +INSERT INTO attendance VALUES (203,'2021-01-14',5,'2021-01-14 08:18:24.478','2021-01-14 18:26:27.144'); +INSERT INTO attendance VALUES (204,'2021-01-14',6,'2021-01-14 08:53:06.897','2021-01-14 18:34:56.045'); +INSERT INTO attendance VALUES (205,'2021-01-14',7,'2021-01-14 08:45:47.688','2021-01-14 18:19:23.142'); +INSERT INTO attendance VALUES (206,'2021-01-14',8,'2021-01-14 08:43:56.96','2021-01-14 19:13:44.021'); +INSERT INTO attendance VALUES (207,'2021-01-14',9,'2021-01-14 08:02:43.684','2021-01-14 18:40:28.59'); +INSERT INTO attendance VALUES (208,'2021-01-14',10,'2021-01-14 08:27:14.309','2021-01-14 18:32:01.006'); +INSERT INTO attendance VALUES (209,'2021-01-14',11,'2021-01-14 08:06:10.046','2021-01-14 18:13:47.834'); +INSERT INTO attendance VALUES (210,'2021-01-14',12,'2021-01-14 08:06:50.674','2021-01-14 19:23:18.101'); +INSERT INTO attendance VALUES (211,'2021-01-14',13,'2021-01-14 08:22:49.264','2021-01-14 18:00:08.114'); +INSERT INTO attendance VALUES (212,'2021-01-14',14,'2021-01-14 08:47:11.363','2021-01-14 19:09:29'); +INSERT INTO attendance VALUES (213,'2021-01-14',15,'2021-01-14 08:42:08.476','2021-01-14 18:33:51.564'); +INSERT INTO attendance VALUES (214,'2021-01-14',16,'2021-01-14 08:36:32.193','2021-01-14 18:01:56.477'); +INSERT INTO attendance VALUES (215,'2021-01-14',17,'2021-01-14 08:00:30.151','2021-01-14 18:19:56.862'); +INSERT INTO attendance VALUES (216,'2021-01-14',18,'2021-01-14 08:45:17.062','2021-01-14 18:09:06.86'); +INSERT INTO attendance VALUES (217,'2021-01-14',19,'2021-01-14 08:53:36.769','2021-01-14 18:24:13.077'); +INSERT INTO attendance VALUES (218,'2021-01-14',20,'2021-01-14 08:38:43.412','2021-01-14 19:08:55.625'); +INSERT INTO attendance VALUES (219,'2021-01-14',21,'2021-01-14 08:17:08.493','2021-01-14 18:17:22.369'); +INSERT INTO attendance VALUES (220,'2021-01-14',22,'2021-01-14 08:27:09.634','2021-01-14 18:12:07.415'); +INSERT INTO attendance VALUES (221,'2021-01-14',23,'2021-01-14 08:33:48.734','2021-01-14 18:14:46.697'); +INSERT INTO attendance VALUES (222,'2021-01-14',25,'2021-01-14 08:47:36.708','2021-01-14 18:30:12.789'); +INSERT INTO attendance VALUES (223,'2021-01-15',1,'2021-01-17 08:02:42.128','2021-01-17 18:46:14.552'); +INSERT INTO attendance VALUES (224,'2021-01-15',2,'2021-01-17 08:54:53.476','2021-01-17 19:11:14.248'); +INSERT INTO attendance VALUES (225,'2021-01-15',3,'2021-01-17 08:11:58.053','2021-01-17 19:10:57.193'); +INSERT INTO attendance VALUES (226,'2021-01-15',4,'2021-01-17 08:52:07.071','2021-01-17 18:15:17.231'); +INSERT INTO attendance VALUES (227,'2021-01-15',5,'2021-01-17 08:45:36.246','2021-01-17 18:58:46.544'); +INSERT INTO attendance VALUES (228,'2021-01-15',6,'2021-01-17 08:26:13.283','2021-01-17 19:18:27.052'); +INSERT INTO attendance VALUES (229,'2021-01-15',7,'2021-01-17 08:43:35.881','2021-01-17 19:23:08.928'); +INSERT INTO attendance VALUES (230,'2021-01-15',8,'2021-01-17 08:30:45.652','2021-01-17 18:40:07.791'); +INSERT INTO attendance VALUES (231,'2021-01-15',9,'2021-01-17 08:41:25.357','2021-01-17 18:12:00.807'); +INSERT INTO attendance VALUES (232,'2021-01-15',10,'2021-01-17 08:36:30.922','2021-01-17 18:06:35.671'); +INSERT INTO attendance VALUES (233,'2021-01-15',11,'2021-01-17 08:09:19.852','2021-01-17 18:07:15.58'); +INSERT INTO attendance VALUES (234,'2021-01-15',12,'2021-01-17 08:04:49.945','2021-01-17 18:30:37.762'); +INSERT INTO attendance VALUES (235,'2021-01-15',13,'2021-01-17 08:12:35.816','2021-01-17 18:07:47.409'); +INSERT INTO attendance VALUES (236,'2021-01-15',14,'2021-01-17 08:35:05.776','2021-01-17 18:35:49.562'); +INSERT INTO attendance VALUES (237,'2021-01-15',15,'2021-01-17 08:27:33.535','2021-01-17 19:22:18.533'); +INSERT INTO attendance VALUES (238,'2021-01-15',16,'2021-01-17 08:27:22.46','2021-01-17 18:01:47.09'); +INSERT INTO attendance VALUES (239,'2021-01-15',17,'2021-01-17 08:31:46.171','2021-01-17 18:10:02.381'); +INSERT INTO attendance VALUES (240,'2021-01-15',18,'2021-01-17 08:47:49.853','2021-01-17 19:08:02.054'); +INSERT INTO attendance VALUES (241,'2021-01-15',19,'2021-01-17 08:58:11.641','2021-01-17 19:16:21.587'); +INSERT INTO attendance VALUES (242,'2021-01-15',20,'2021-01-17 08:37:30.106','2021-01-17 18:40:13.636'); +INSERT INTO attendance VALUES (243,'2021-01-15',21,'2021-01-17 08:49:04.39','2021-01-17 18:25:28.872'); +INSERT INTO attendance VALUES (244,'2021-01-15',22,'2021-01-17 08:03:35.362','2021-01-17 19:23:58.923'); +INSERT INTO attendance VALUES (245,'2021-01-15',23,'2021-01-17 08:26:51.55','2021-01-17 19:04:26.618'); +INSERT INTO attendance VALUES (246,'2021-01-15',24,'2021-01-17 08:59:31.266','2021-01-17 19:17:57.896'); +INSERT INTO attendance VALUES (247,'2021-01-15',25,'2021-01-17 08:38:15.573','2021-01-17 18:30:50.77'); +INSERT INTO attendance VALUES (248,'2021-01-18',1,'2021-01-18 08:16:34.346','2021-01-18 19:09:37.434'); +INSERT INTO attendance VALUES (249,'2021-01-18',2,'2021-01-18 08:05:46.094','2021-01-18 19:05:13.224'); +INSERT INTO attendance VALUES (250,'2021-01-18',3,'2021-01-18 08:09:20.54','2021-01-18 19:06:03.062'); +INSERT INTO attendance VALUES (251,'2021-01-18',4,'2021-01-18 08:54:25.52','2021-01-18 18:08:27.417'); +INSERT INTO attendance VALUES (252,'2021-01-18',5,'2021-01-18 08:48:34.576','2021-01-18 18:51:26.464'); +INSERT INTO attendance VALUES (253,'2021-01-18',6,'2021-01-18 08:09:17.372','2021-01-18 19:15:19.557'); +INSERT INTO attendance VALUES (254,'2021-01-18',7,'2021-01-18 08:27:41.563','2021-01-18 18:00:46.344'); +INSERT INTO attendance VALUES (255,'2021-01-18',8,'2021-01-18 08:27:32.673','2021-01-18 19:06:17.304'); +INSERT INTO attendance VALUES (256,'2021-01-18',9,'2021-01-18 08:24:14.802','2021-01-18 18:21:38.937'); +INSERT INTO attendance VALUES (257,'2021-01-18',10,'2021-01-18 08:58:08.091','2021-01-18 18:19:59.503'); +INSERT INTO attendance VALUES (258,'2021-01-18',11,'2021-01-18 08:44:49.043','2021-01-18 18:24:26.578'); +INSERT INTO attendance VALUES (259,'2021-01-18',12,'2021-01-18 08:01:48.749','2021-01-18 19:01:20.965'); +INSERT INTO attendance VALUES (260,'2021-01-18',13,'2021-01-18 08:43:43.79','2021-01-18 18:34:43.636'); +INSERT INTO attendance VALUES (261,'2021-01-18',14,'2021-01-18 08:04:58.969','2021-01-18 18:48:15.643'); +INSERT INTO attendance VALUES (262,'2021-01-18',15,'2021-01-18 08:47:45.409','2021-01-18 18:55:09.921'); +INSERT INTO attendance VALUES (263,'2021-01-18',16,'2021-01-18 08:13:08.23','2021-01-18 18:42:17.513'); +INSERT INTO attendance VALUES (264,'2021-01-18',17,'2021-01-18 08:40:16.287','2021-01-18 18:10:44.433'); +INSERT INTO attendance VALUES (265,'2021-01-18',18,'2021-01-18 08:32:50.525','2021-01-18 18:25:50.616'); +INSERT INTO attendance VALUES (266,'2021-01-18',19,'2021-01-18 08:57:09.037','2021-01-18 18:07:00.528'); +INSERT INTO attendance VALUES (267,'2021-01-18',20,'2021-01-18 08:16:41.43','2021-01-18 18:07:39.176'); +INSERT INTO attendance VALUES (268,'2021-01-18',21,'2021-01-18 08:08:02.001','2021-01-18 18:53:34.578'); +INSERT INTO attendance VALUES (269,'2021-01-18',22,'2021-01-18 08:30:21.643','2021-01-18 18:20:35.109'); +INSERT INTO attendance VALUES (270,'2021-01-18',23,'2021-01-18 08:10:33.122','2021-01-18 18:43:09.41'); +INSERT INTO attendance VALUES (271,'2021-01-18',24,'2021-01-18 08:20:42.283','2021-01-18 19:02:29.152'); +INSERT INTO attendance VALUES (272,'2021-01-18',25,'2021-01-18 08:34:11.047','2021-01-18 18:29:25.774'); +INSERT INTO attendance VALUES (273,'2021-01-19',1,'2021-01-19 08:25:28.501','2021-01-19 18:49:01.66'); +INSERT INTO attendance VALUES (274,'2021-01-19',2,'2021-01-19 08:59:12.962','2021-01-19 18:07:20.364'); +INSERT INTO attendance VALUES (275,'2021-01-19',3,'2021-01-19 08:40:43.225','2021-01-19 19:13:10.255'); +INSERT INTO attendance VALUES (276,'2021-01-19',4,'2021-01-19 08:56:14.976','2021-01-19 19:15:42.426'); +INSERT INTO attendance VALUES (277,'2021-01-19',5,'2021-01-19 08:41:14.261','2021-01-19 18:56:20.343'); +INSERT INTO attendance VALUES (278,'2021-01-19',6,'2021-01-19 08:29:15.149','2021-01-19 18:37:08.121'); +INSERT INTO attendance VALUES (279,'2021-01-19',7,'2021-01-19 08:29:08.546','2021-01-19 19:21:37.378'); +INSERT INTO attendance VALUES (280,'2021-01-19',8,'2021-01-19 08:01:58.721','2021-01-19 18:15:46.187'); +INSERT INTO attendance VALUES (281,'2021-01-19',9,'2021-01-19 08:43:07.912','2021-01-19 19:06:25.244'); +INSERT INTO attendance VALUES (282,'2021-01-19',10,'2021-01-19 08:44:04.067','2021-01-19 18:52:02.272'); +INSERT INTO attendance VALUES (283,'2021-01-19',11,'2021-01-19 08:49:45.181','2021-01-19 18:48:51.065'); +INSERT INTO attendance VALUES (284,'2021-01-19',12,'2021-01-19 08:08:21.273','2021-01-19 18:21:41.899'); +INSERT INTO attendance VALUES (285,'2021-01-19',13,'2021-01-19 08:13:33.426','2021-01-19 18:07:12.624'); +INSERT INTO attendance VALUES (286,'2021-01-19',14,'2021-01-19 08:32:59.061','2021-01-19 18:36:10.041'); +INSERT INTO attendance VALUES (287,'2021-01-19',15,'2021-01-19 08:55:29.278','2021-01-19 19:03:23.164'); +INSERT INTO attendance VALUES (288,'2021-01-19',16,'2021-01-19 08:07:16.274','2021-01-19 18:46:19.226'); +INSERT INTO attendance VALUES (289,'2021-01-19',17,'2021-01-19 08:33:27.976','2021-01-19 18:11:22.581'); +INSERT INTO attendance VALUES (290,'2021-01-19',18,'2021-01-19 08:18:43.662','2021-01-19 18:19:20.388'); +INSERT INTO attendance VALUES (291,'2021-01-19',19,'2021-01-19 08:08:54.687','2021-01-19 18:38:49.117'); +INSERT INTO attendance VALUES (292,'2021-01-19',20,'2021-01-19 08:37:58.737','2021-01-19 18:36:12.992'); +INSERT INTO attendance VALUES (293,'2021-01-19',21,'2021-01-19 08:35:49.17','2021-01-19 19:08:58.04'); +INSERT INTO attendance VALUES (294,'2021-01-19',22,'2021-01-19 08:57:43.637','2021-01-19 18:57:40.046'); +INSERT INTO attendance VALUES (295,'2021-01-19',23,'2021-01-19 08:24:31.346','2021-01-19 18:46:03.475'); +INSERT INTO attendance VALUES (296,'2021-01-19',24,'2021-01-19 08:07:30.903','2021-01-19 18:48:01.626'); +INSERT INTO attendance VALUES (297,'2021-01-19',25,'2021-01-19 08:14:16.894','2021-01-19 17:44:08.288'); +INSERT INTO attendance VALUES (298,'2021-01-20',1,'2021-01-20 08:51:31.401','2021-01-20 18:52:07.814'); +INSERT INTO attendance VALUES (299,'2021-01-20',2,'2021-01-20 08:54:56.248','2021-01-20 18:26:17.163'); +INSERT INTO attendance VALUES (300,'2021-01-20',3,'2021-01-20 08:30:17.563','2021-01-20 18:33:42.914'); +INSERT INTO attendance VALUES (301,'2021-01-20',4,'2021-01-20 08:13:46.752','2021-01-20 19:12:17.123'); +INSERT INTO attendance VALUES (302,'2021-01-20',5,'2021-01-20 08:31:58.528','2021-01-20 18:08:52.041'); +INSERT INTO attendance VALUES (303,'2021-01-20',6,'2021-01-20 08:06:24.153','2021-01-20 18:09:10.27'); +INSERT INTO attendance VALUES (304,'2021-01-20',7,'2021-01-20 08:45:44.866','2021-01-20 18:10:44.643'); +INSERT INTO attendance VALUES (305,'2021-01-20',8,'2021-01-20 08:27:25.846','2021-01-20 18:55:50.361'); +INSERT INTO attendance VALUES (306,'2021-01-20',9,'2021-01-20 08:02:13.827','2021-01-20 19:01:16.397'); +INSERT INTO attendance VALUES (307,'2021-01-20',11,'2021-01-20 08:27:36.029','2021-01-20 18:22:57.277'); +INSERT INTO attendance VALUES (308,'2021-01-20',12,'2021-01-20 08:26:44.205','2021-01-20 19:17:15.389'); +INSERT INTO attendance VALUES (309,'2021-01-20',13,'2021-01-20 08:41:45.317','2021-01-20 19:19:20.827'); +INSERT INTO attendance VALUES (310,'2021-01-20',14,'2021-01-20 08:55:35.26','2021-01-20 18:52:33.774'); +INSERT INTO attendance VALUES (311,'2021-01-20',15,'2021-01-20 08:18:52.994','2021-01-20 19:09:16.844'); +INSERT INTO attendance VALUES (312,'2021-01-20',16,'2021-01-20 08:00:08.471','2021-01-20 18:27:44.56'); +INSERT INTO attendance VALUES (313,'2021-01-20',17,'2021-01-20 08:37:13.012','2021-01-20 18:27:35.32'); +INSERT INTO attendance VALUES (314,'2021-01-20',18,'2021-01-20 08:06:56.989','2021-01-20 18:20:40.162'); +INSERT INTO attendance VALUES (315,'2021-01-20',19,'2021-01-20 08:09:56.278','2021-01-20 18:54:41.805'); +INSERT INTO attendance VALUES (316,'2021-01-20',20,'2021-01-20 08:53:18.546','2021-01-20 18:58:44.935'); +INSERT INTO attendance VALUES (317,'2021-01-20',21,'2021-01-20 08:29:27.845','2021-01-20 19:13:51.709'); +INSERT INTO attendance VALUES (318,'2021-01-20',22,'2021-01-20 08:34:03.379','2021-01-20 18:01:36.449'); +INSERT INTO attendance VALUES (319,'2021-01-20',23,'2021-01-20 08:35:35.802','2021-01-20 19:22:49.519'); +INSERT INTO attendance VALUES (320,'2021-01-20',24,'2021-01-20 08:40:29.945','2021-01-20 18:14:22.879'); +INSERT INTO attendance VALUES (321,'2021-01-20',25,'2021-01-20 08:51:38.347','2021-01-20 18:44:12.118'); +INSERT INTO attendance VALUES (322,'2021-01-21',1,'2021-01-21 08:26:17.141','2021-01-21 19:18:33.398'); +INSERT INTO attendance VALUES (323,'2021-01-21',2,'2021-01-21 08:02:47.04','2021-01-21 19:14:40.596'); +INSERT INTO attendance VALUES (324,'2021-01-21',3,'2021-01-21 08:44:09.648','2021-01-21 18:03:11.092'); +INSERT INTO attendance VALUES (325,'2021-01-21',4,'2021-01-21 08:34:49.976','2021-01-21 18:46:11.472'); +INSERT INTO attendance VALUES (326,'2021-01-21',5,'2021-01-21 08:58:57.719','2021-01-21 18:19:40.749'); +INSERT INTO attendance VALUES (327,'2021-01-21',6,'2021-01-21 08:59:46.46','2021-01-21 18:01:29.84'); +INSERT INTO attendance VALUES (328,'2021-01-21',7,'2021-01-21 08:41:13.902','2021-01-21 19:29:31.416'); +INSERT INTO attendance VALUES (329,'2021-01-21',8,'2021-01-21 08:40:45.502','2021-01-21 19:26:36.045'); +INSERT INTO attendance VALUES (330,'2021-01-21',9,'2021-01-21 08:45:14.186','2021-01-21 18:09:45.343'); +INSERT INTO attendance VALUES (331,'2021-01-21',10,'2021-01-21 08:14:55.705','2021-01-21 18:14:18.067'); +INSERT INTO attendance VALUES (332,'2021-01-21',11,'2021-01-21 08:58:39.466','2021-01-21 19:01:57.757'); +INSERT INTO attendance VALUES (333,'2021-01-21',12,'2021-01-21 08:31:19.823','2021-01-21 18:40:24.29'); +INSERT INTO attendance VALUES (334,'2021-01-21',13,'2021-01-21 08:04:22.294','2021-01-21 18:27:29.311'); +INSERT INTO attendance VALUES (335,'2021-01-21',14,'2021-01-21 08:52:19.82','2021-01-21 18:36:49.144'); +INSERT INTO attendance VALUES (336,'2021-01-21',15,'2021-01-21 08:32:43.374','2021-01-21 18:37:01.038'); +INSERT INTO attendance VALUES (337,'2021-01-21',16,'2021-01-21 08:29:02.991','2021-01-21 18:53:05.063'); +INSERT INTO attendance VALUES (338,'2021-01-21',17,'2021-01-21 08:02:52.773','2021-01-21 18:13:18.915'); +INSERT INTO attendance VALUES (339,'2021-01-21',18,'2021-01-21 08:17:56.704','2021-01-21 18:11:24.56'); +INSERT INTO attendance VALUES (340,'2021-01-21',19,'2021-01-21 08:38:14.923','2021-01-21 19:25:30.515'); +INSERT INTO attendance VALUES (341,'2021-01-21',20,'2021-01-21 08:28:35.504','2021-01-21 18:43:43.162'); +INSERT INTO attendance VALUES (342,'2021-01-21',21,'2021-01-21 08:12:19.521','2021-01-21 18:23:11.568'); +INSERT INTO attendance VALUES (343,'2021-01-21',22,'2021-01-21 08:15:38.917','2021-01-21 18:04:44.178'); +INSERT INTO attendance VALUES (344,'2021-01-21',23,'2021-01-21 08:19:13.815','2021-01-21 19:02:37.045'); +INSERT INTO attendance VALUES (345,'2021-01-21',24,'2021-01-21 08:50:13.863','2021-01-21 18:29:30.941'); +INSERT INTO attendance VALUES (346,'2021-01-21',25,'2021-01-21 08:20:52.983','2021-01-21 19:09:09.996'); +INSERT INTO attendance VALUES (347,'2021-01-22',1,'2021-01-24 08:06:02.827','2021-01-24 18:44:46.866'); +INSERT INTO attendance VALUES (348,'2021-01-22',2,'2021-01-24 08:27:04.464','2021-01-24 18:07:35.345'); +INSERT INTO attendance VALUES (349,'2021-01-22',3,'2021-01-24 08:59:45.619','2021-01-24 18:52:46.549'); +INSERT INTO attendance VALUES (350,'2021-01-22',4,'2021-01-24 08:06:31.34','2021-01-24 18:30:42.987'); +INSERT INTO attendance VALUES (351,'2021-01-22',6,'2021-01-24 08:04:44.137','2021-01-24 19:20:01.897'); +INSERT INTO attendance VALUES (352,'2021-01-22',7,'2021-01-24 08:07:25.052','2021-01-24 18:30:12.54'); +INSERT INTO attendance VALUES (353,'2021-01-22',8,'2021-01-24 08:49:54.642','2021-01-24 18:40:24.375'); +INSERT INTO attendance VALUES (354,'2021-01-22',9,'2021-01-24 08:16:58.017','2021-01-24 18:01:49.276'); +INSERT INTO attendance VALUES (355,'2021-01-22',10,'2021-01-24 08:04:06.154','2021-01-24 18:09:04.816'); +INSERT INTO attendance VALUES (356,'2021-01-22',11,'2021-01-24 08:12:37.81','2021-01-24 18:07:26.785'); +INSERT INTO attendance VALUES (357,'2021-01-22',12,'2021-01-24 08:41:50.654','2021-01-24 18:40:15.909'); +INSERT INTO attendance VALUES (358,'2021-01-22',13,'2021-01-24 08:00:18.838','2021-01-24 18:06:23.075'); +INSERT INTO attendance VALUES (359,'2021-01-22',14,'2021-01-24 08:47:35.245','2021-01-24 19:11:39.584'); +INSERT INTO attendance VALUES (360,'2021-01-22',15,'2021-01-24 08:10:52.873','2021-01-24 18:23:40.417'); +INSERT INTO attendance VALUES (361,'2021-01-22',16,'2021-01-24 08:53:28.193','2021-01-24 18:17:40.485'); +INSERT INTO attendance VALUES (362,'2021-01-22',17,'2021-01-24 08:28:21.949','2021-01-24 19:17:20.413'); +INSERT INTO attendance VALUES (363,'2021-01-22',18,'2021-01-24 08:13:45.505','2021-01-24 18:55:59.713'); +INSERT INTO attendance VALUES (364,'2021-01-22',19,'2021-01-24 08:30:31.205','2021-01-24 18:26:53.145'); +INSERT INTO attendance VALUES (365,'2021-01-22',20,'2021-01-24 08:31:06.486','2021-01-24 18:14:34.155'); +INSERT INTO attendance VALUES (366,'2021-01-22',21,'2021-01-24 08:44:12.079','2021-01-24 19:02:14.997'); +INSERT INTO attendance VALUES (367,'2021-01-22',22,'2021-01-24 08:58:19.406','2021-01-24 19:17:30.086'); +INSERT INTO attendance VALUES (368,'2021-01-22',23,'2021-01-24 08:39:54.341','2021-01-24 18:56:22.637'); +INSERT INTO attendance VALUES (369,'2021-01-22',24,'2021-01-24 08:56:45.162','2021-01-24 18:20:50.701'); +INSERT INTO attendance VALUES (370,'2021-01-22',25,'2021-01-24 08:01:51.153','2021-01-24 18:44:01.389'); +INSERT INTO attendance VALUES (371,'2021-01-25',1,'2021-01-25 08:32:50.814','2021-01-25 18:11:23.635'); +INSERT INTO attendance VALUES (372,'2021-01-25',2,'2021-01-25 08:38:20.045','2021-01-25 18:39:58.9'); +INSERT INTO attendance VALUES (373,'2021-01-25',3,'2021-01-25 08:50:47.599','2021-01-25 19:20:47.878'); +INSERT INTO attendance VALUES (374,'2021-01-25',4,'2021-01-25 08:01:25.458','2021-01-25 19:27:06.771'); +INSERT INTO attendance VALUES (375,'2021-01-25',5,'2021-01-25 08:00:38.663','2021-01-25 18:27:57.132'); +INSERT INTO attendance VALUES (376,'2021-01-25',6,'2021-01-25 08:05:53.58','2021-01-25 18:18:20.163'); +INSERT INTO attendance VALUES (377,'2021-01-25',7,'2021-01-25 08:54:08.497','2021-01-25 18:40:11.88'); +INSERT INTO attendance VALUES (378,'2021-01-25',8,'2021-01-25 08:30:22.027','2021-01-25 18:13:14.728'); +INSERT INTO attendance VALUES (379,'2021-01-25',9,'2021-01-25 08:47:43.074','2021-01-25 18:19:12.591'); +INSERT INTO attendance VALUES (380,'2021-01-25',10,'2021-01-25 08:19:19.497','2021-01-25 18:37:24.22'); +INSERT INTO attendance VALUES (381,'2021-01-25',11,'2021-01-25 08:48:58.995','2021-01-25 19:16:28.08'); +INSERT INTO attendance VALUES (382,'2021-01-25',12,'2021-01-25 08:31:04.96','2021-01-25 19:03:47.611'); +INSERT INTO attendance VALUES (383,'2021-01-25',13,'2021-01-25 08:31:59.328','2021-01-25 18:20:34.174'); +INSERT INTO attendance VALUES (384,'2021-01-25',14,'2021-01-25 08:33:50.062','2021-01-25 18:12:19.164'); +INSERT INTO attendance VALUES (385,'2021-01-25',15,'2021-01-25 08:24:16.453','2021-01-25 18:31:21.01'); +INSERT INTO attendance VALUES (386,'2021-01-25',16,'2021-01-25 08:45:49.814','2021-01-25 19:01:17.027'); +INSERT INTO attendance VALUES (387,'2021-01-25',17,'2021-01-25 08:11:41.202','2021-01-25 18:05:41.218'); +INSERT INTO attendance VALUES (388,'2021-01-25',18,'2021-01-25 08:06:33.17','2021-01-25 18:07:37.936'); +INSERT INTO attendance VALUES (389,'2021-01-25',19,'2021-01-25 08:39:14.535','2021-01-25 18:27:02.232'); +INSERT INTO attendance VALUES (390,'2021-01-25',20,'2021-01-25 08:49:16.421','2021-01-25 19:06:31.339'); +INSERT INTO attendance VALUES (391,'2021-01-25',21,'2021-01-25 08:30:08.083','2021-01-25 19:02:18.963'); +INSERT INTO attendance VALUES (392,'2021-01-25',22,'2021-01-25 08:00:32.55','2021-01-25 19:29:11.454'); +INSERT INTO attendance VALUES (393,'2021-01-25',23,'2021-01-25 08:45:58.22','2021-01-25 18:54:50.924'); +INSERT INTO attendance VALUES (394,'2021-01-25',24,'2021-01-25 08:21:36.782','2021-01-25 18:03:42.203'); +INSERT INTO attendance VALUES (395,'2021-01-25',25,'2021-01-25 08:57:49.783','2021-01-25 18:30:53.051'); +INSERT INTO attendance VALUES (396,'2021-01-26',1,'2021-01-26 08:46:54.221','2021-01-26 18:59:04.564'); +INSERT INTO attendance VALUES (397,'2021-01-26',2,'2021-01-26 08:10:46.522','2021-01-26 19:05:11.068'); +INSERT INTO attendance VALUES (398,'2021-01-26',3,'2021-01-26 08:53:01.13','2021-01-26 19:03:30.963'); +INSERT INTO attendance VALUES (399,'2021-01-26',4,'2021-01-26 08:25:45.799','2021-01-26 19:05:54.718'); +INSERT INTO attendance VALUES (400,'2021-01-26',5,'2021-01-26 08:03:29.67','2021-01-26 18:15:16.382'); +INSERT INTO attendance VALUES (401,'2021-01-26',6,'2021-01-26 08:18:10.938','2021-01-26 18:27:56.984'); +INSERT INTO attendance VALUES (402,'2021-01-26',7,'2021-01-26 08:34:20.8','2021-01-26 18:34:27.461'); +INSERT INTO attendance VALUES (403,'2021-01-26',8,'2021-01-26 08:26:33.19','2021-01-26 18:15:59.188'); +INSERT INTO attendance VALUES (404,'2021-01-26',9,'2021-01-26 08:29:11.146','2021-01-26 19:23:29.364'); +INSERT INTO attendance VALUES (405,'2021-01-26',10,'2021-01-26 08:17:39.389','2021-01-26 18:54:46.657'); +INSERT INTO attendance VALUES (406,'2021-01-26',11,'2021-01-26 08:25:53.776','2021-01-26 18:03:58.764'); +INSERT INTO attendance VALUES (407,'2021-01-26',12,'2021-01-26 08:18:56.531','2021-01-26 19:10:37.079'); +INSERT INTO attendance VALUES (408,'2021-01-26',13,'2021-01-26 08:10:32.341','2021-01-26 18:41:17.145'); +INSERT INTO attendance VALUES (409,'2021-01-26',14,'2021-01-26 08:15:09.085','2021-01-26 18:36:57.258'); +INSERT INTO attendance VALUES (410,'2021-01-26',15,'2021-01-26 08:19:54.738','2021-01-26 18:16:30.039'); +INSERT INTO attendance VALUES (411,'2021-01-26',16,'2021-01-26 08:11:11.467','2021-01-26 19:26:56.628'); +INSERT INTO attendance VALUES (412,'2021-01-26',17,'2021-01-26 08:39:24.967','2021-01-26 18:15:15.393'); +INSERT INTO attendance VALUES (413,'2021-01-26',18,'2021-01-26 08:18:26.856','2021-01-26 18:31:21.422'); +INSERT INTO attendance VALUES (414,'2021-01-26',19,'2021-01-26 08:23:31.986','2021-01-26 18:04:37.376'); +INSERT INTO attendance VALUES (415,'2021-01-26',20,'2021-01-26 08:22:12.273','2021-01-26 19:07:56.24'); +INSERT INTO attendance VALUES (416,'2021-01-26',21,'2021-01-26 08:51:02.933','2021-01-26 18:02:07.193'); +INSERT INTO attendance VALUES (417,'2021-01-26',22,'2021-01-26 08:32:47.411','2021-01-26 18:22:06.547'); +INSERT INTO attendance VALUES (418,'2021-01-26',23,'2021-01-26 08:46:23.45','2021-01-26 19:14:44.461'); +INSERT INTO attendance VALUES (419,'2021-01-26',24,'2021-01-26 08:24:02.598','2021-01-26 18:44:17.639'); +INSERT INTO attendance VALUES (420,'2021-01-26',25,'2021-01-26 08:54:34.645','2021-01-26 19:18:03.849'); +INSERT INTO attendance VALUES (421,'2021-01-27',1,'2021-01-27 08:43:27.533','2021-01-27 18:10:22.73'); +INSERT INTO attendance VALUES (422,'2021-01-27',2,'2021-01-27 08:29:46.592','2021-01-27 18:53:49.897'); +INSERT INTO attendance VALUES (423,'2021-01-27',3,'2021-01-27 08:54:39.096','2021-01-27 18:34:00.797'); +INSERT INTO attendance VALUES (424,'2021-01-27',4,'2021-01-27 08:18:29.394','2021-01-27 18:11:40.18'); +INSERT INTO attendance VALUES (425,'2021-01-27',5,'2021-01-27 08:17:22.649','2021-01-27 18:19:40.506'); +INSERT INTO attendance VALUES (426,'2021-01-27',6,'2021-01-27 08:50:20.383','2021-01-27 18:48:48.441'); +INSERT INTO attendance VALUES (427,'2021-01-27',7,'2021-01-27 08:33:38.335','2021-01-27 18:18:39.592'); +INSERT INTO attendance VALUES (428,'2021-01-27',8,'2021-01-27 08:33:30.523','2021-01-27 18:33:33.304'); +INSERT INTO attendance VALUES (429,'2021-01-27',9,'2021-01-27 08:47:43.669','2021-01-27 18:41:57.067'); +INSERT INTO attendance VALUES (430,'2021-01-27',10,'2021-01-27 08:45:31.512','2021-01-27 18:21:05.624'); +INSERT INTO attendance VALUES (431,'2021-01-27',11,'2021-01-27 08:49:08.733','2021-01-27 18:14:02.457'); +INSERT INTO attendance VALUES (432,'2021-01-27',12,'2021-01-27 08:20:06.228','2021-01-27 19:11:06.679'); +INSERT INTO attendance VALUES (433,'2021-01-27',13,'2021-01-27 08:09:38.69','2021-01-27 18:13:38.656'); +INSERT INTO attendance VALUES (434,'2021-01-27',14,'2021-01-27 09:00:11.061','2021-01-27 19:19:26.993'); +INSERT INTO attendance VALUES (435,'2021-01-27',15,'2021-01-27 08:45:58.649','2021-01-27 18:59:29.979'); +INSERT INTO attendance VALUES (436,'2021-01-27',16,'2021-01-27 08:21:01.728','2021-01-27 18:09:59.018'); +INSERT INTO attendance VALUES (437,'2021-01-27',17,'2021-01-27 08:55:44.409','2021-01-27 19:19:07.503'); +INSERT INTO attendance VALUES (438,'2021-01-27',18,'2021-01-27 08:45:31.677','2021-01-27 19:06:02.095'); +INSERT INTO attendance VALUES (439,'2021-01-27',19,'2021-01-27 08:18:21.058','2021-01-27 18:04:55.2'); +INSERT INTO attendance VALUES (440,'2021-01-27',20,'2021-01-27 08:29:18.128','2021-01-27 18:00:55.743'); +INSERT INTO attendance VALUES (441,'2021-01-27',21,'2021-01-27 08:21:42.186','2021-01-27 18:12:28.633'); +INSERT INTO attendance VALUES (442,'2021-01-27',22,'2021-01-27 08:47:23.797','2021-01-27 18:12:58.959'); +INSERT INTO attendance VALUES (443,'2021-01-27',23,'2021-01-27 08:43:29.704','2021-01-27 18:50:21.561'); +INSERT INTO attendance VALUES (444,'2021-01-27',24,'2021-01-27 08:23:36.712','2021-01-27 19:25:17.32'); +INSERT INTO attendance VALUES (445,'2021-01-27',25,'2021-01-27 08:01:22.42','2021-01-27 18:31:59.285'); +INSERT INTO attendance VALUES (446,'2021-01-28',1,'2021-01-28 08:57:34.217','2021-01-28 18:52:45.444'); +INSERT INTO attendance VALUES (447,'2021-01-28',2,'2021-01-28 08:54:54.392','2021-01-28 18:19:36.335'); +INSERT INTO attendance VALUES (448,'2021-01-28',3,'2021-01-28 08:56:33.694','2021-01-28 18:56:01.103'); +INSERT INTO attendance VALUES (449,'2021-01-28',4,'2021-01-28 08:10:45.998','2021-01-28 19:03:09.834'); +INSERT INTO attendance VALUES (450,'2021-01-28',5,'2021-01-28 08:55:04.184','2021-01-28 18:36:24.549'); +INSERT INTO attendance VALUES (451,'2021-01-28',6,'2021-01-28 08:54:28.157','2021-01-28 19:09:46.627'); +INSERT INTO attendance VALUES (452,'2021-01-28',7,'2021-01-28 08:03:25.87','2021-01-28 18:25:31.438'); +INSERT INTO attendance VALUES (453,'2021-01-28',8,'2021-01-28 08:58:03.591','2021-01-28 18:14:01.072'); +INSERT INTO attendance VALUES (454,'2021-01-28',9,'2021-01-28 08:15:15.748','2021-01-28 19:25:29.896'); +INSERT INTO attendance VALUES (455,'2021-01-28',10,'2021-01-28 08:43:34.3','2021-01-28 18:59:03.158'); +INSERT INTO attendance VALUES (456,'2021-01-28',11,'2021-01-28 08:47:07.086','2021-01-28 18:06:27.584'); +INSERT INTO attendance VALUES (457,'2021-01-28',12,'2021-01-28 08:05:16.008','2021-01-28 18:03:13.385'); +INSERT INTO attendance VALUES (458,'2021-01-28',13,'2021-01-28 08:16:48.372','2021-01-28 18:45:00.752'); +INSERT INTO attendance VALUES (459,'2021-01-28',14,'2021-01-28 08:17:33.852','2021-01-28 18:47:02.8'); +INSERT INTO attendance VALUES (460,'2021-01-28',15,'2021-01-28 08:29:15.423','2021-01-28 19:19:36.653'); +INSERT INTO attendance VALUES (461,'2021-01-28',16,'2021-01-28 08:33:35.223','2021-01-28 18:26:26.948'); +INSERT INTO attendance VALUES (462,'2021-01-28',17,'2021-01-28 08:24:49.433','2021-01-28 18:47:58.732'); +INSERT INTO attendance VALUES (463,'2021-01-28',18,'2021-01-28 08:46:49.457','2021-01-28 18:18:22.37'); +INSERT INTO attendance VALUES (464,'2021-01-28',19,'2021-01-28 08:12:05.06','2021-01-28 18:52:52.849'); +INSERT INTO attendance VALUES (465,'2021-01-28',20,'2021-01-28 08:46:25.458','2021-01-28 19:13:31.868'); +INSERT INTO attendance VALUES (466,'2021-01-28',21,'2021-01-28 08:46:49.514','2021-01-28 19:09:58.14'); +INSERT INTO attendance VALUES (467,'2021-01-28',22,'2021-01-28 08:08:34.551','2021-01-28 18:52:46.033'); +INSERT INTO attendance VALUES (468,'2021-01-28',23,'2021-01-28 08:16:19.864','2021-01-28 19:15:26.034'); +INSERT INTO attendance VALUES (469,'2021-01-28',24,'2021-01-28 08:32:09.904','2021-01-28 18:46:51.614'); +INSERT INTO attendance VALUES (470,'2021-01-28',25,'2021-01-28 08:18:32.066','2021-01-28 18:03:22.204'); +INSERT INTO attendance VALUES (471,'2021-01-29',1,'2021-01-31 08:13:33.925','2021-01-31 19:19:45.593'); +INSERT INTO attendance VALUES (472,'2021-01-29',2,'2021-01-31 08:36:15.024','2021-01-31 18:24:12.728'); +INSERT INTO attendance VALUES (473,'2021-01-29',3,'2021-01-31 08:52:14.092','2021-01-31 19:10:51.328'); +INSERT INTO attendance VALUES (474,'2021-01-29',4,'2021-01-31 08:06:29.025','2021-01-31 18:49:04.973'); +INSERT INTO attendance VALUES (475,'2021-01-29',5,'2021-01-31 08:59:44.646','2021-01-31 18:00:58.809'); +INSERT INTO attendance VALUES (476,'2021-01-29',6,'2021-01-31 08:59:25.199','2021-01-31 19:24:17.327'); +INSERT INTO attendance VALUES (477,'2021-01-29',7,'2021-01-31 08:03:35.949','2021-01-31 19:11:35.583'); +INSERT INTO attendance VALUES (478,'2021-01-29',8,'2021-01-31 08:55:41.736','2021-01-31 18:07:36.566'); +INSERT INTO attendance VALUES (479,'2021-01-29',9,'2021-01-31 08:31:09.867','2021-01-31 18:09:15.552'); +INSERT INTO attendance VALUES (480,'2021-01-29',10,'2021-01-31 08:00:47.301','2021-01-31 18:31:11.902'); +INSERT INTO attendance VALUES (481,'2021-01-29',11,'2021-01-31 08:09:57.843','2021-01-31 18:56:01.037'); +INSERT INTO attendance VALUES (482,'2021-01-29',12,'2021-01-31 08:33:13.425','2021-01-31 18:24:45.367'); +INSERT INTO attendance VALUES (483,'2021-01-29',13,'2021-01-31 08:00:17.522','2021-01-31 18:00:08.211'); +INSERT INTO attendance VALUES (484,'2021-01-29',14,'2021-01-31 08:28:42.511','2021-01-31 18:44:09.882'); +INSERT INTO attendance VALUES (485,'2021-01-29',15,'2021-01-31 08:52:53.799','2021-01-31 18:52:40.081'); +INSERT INTO attendance VALUES (486,'2021-01-29',16,'2021-01-31 08:06:40.745','2021-01-31 19:00:19.793'); +INSERT INTO attendance VALUES (487,'2021-01-29',17,'2021-01-31 08:46:41.241','2021-01-31 19:01:34.619'); +INSERT INTO attendance VALUES (488,'2021-01-29',19,'2021-01-31 08:42:15.648','2021-01-31 18:23:57.765'); +INSERT INTO attendance VALUES (489,'2021-01-29',20,'2021-01-31 08:31:06.961','2021-01-31 18:16:45.358'); +INSERT INTO attendance VALUES (490,'2021-01-29',21,'2021-01-31 08:29:01.671','2021-01-31 18:38:20.186'); +INSERT INTO attendance VALUES (491,'2021-01-29',22,'2021-01-31 08:08:36.581','2021-01-31 18:01:04.587'); +INSERT INTO attendance VALUES (492,'2021-01-29',23,'2021-01-31 08:44:07.088','2021-01-31 18:42:24.507'); +INSERT INTO attendance VALUES (493,'2021-01-29',24,'2021-01-31 08:48:33.944','2021-01-31 18:00:21.114'); +INSERT INTO attendance VALUES (494,'2021-01-29',25,'2021-01-31 08:02:05.548','2021-01-31 18:36:05.476'); +``` + + +# SQL + +```aidl +SELECT + c.calendar_date, e.employee_name,a.clock_in,a.clock_out, + CASE + WHEN a.clock_out IS NULL THEN '缺勤' + WHEN EXTRACT(HOUR FROM a.clock_in)>9 THEN '迟到' + ELSE '早退' + END + AS '考勤状态' +FROM + calendar c +CROSS JOIN + employee e +LEFT JOIN + attendance a +ON + ( + a.check_date=c.calendar_date + AND + a.emp_id=e.employee_id) +WHERE + c.calendar_year=2021 + AND + c.calendar_month=1 AND c.is_work_day='Y' +AND + ( + a.id IS NULL + OR + a.clock_out IS NULL + OR + EXTRACT(HOUR FROM a.clock_in)>9 + OR + EXTRACT(HOUR FROM a.clock_out)<18 + ) +; +``` +- 得到结果 + + ![sql-10]({{site.baseurl}}/img-post/sql-10.png) diff --git "a/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 INTERVAL + DENSE_RANK OVER \345\256\236\347\216\260\347\224\250\346\210\267\347\225\231\345\255\230\345\210\206\346\236\220.md" "b/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 INTERVAL + DENSE_RANK OVER \345\256\236\347\216\260\347\224\250\346\210\267\347\225\231\345\255\230\345\210\206\346\236\220.md" new file mode 100644 index 00000000000..cfb943b3c86 --- /dev/null +++ "b/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 INTERVAL + DENSE_RANK OVER \345\256\236\347\216\260\347\224\250\346\210\267\347\225\231\345\255\230\345\210\206\346\236\220.md" @@ -0,0 +1,113 @@ +--- +layout: post +title: SQL:使用 INTERVAL + DENSE_RANK OVER 实现用户留存分析 +subtitle: 统计用户注册后 1、3、7、30 日留存率 +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + +# 需求 + +- 给定用户注册登录时间,统计用户注册后 1、3、7、30 日留存率; + +# 创建表 + +- 创建用户表 + ``` + DROP TABLE t_user; + CREATE TABLE t_user( + id INT AUTO_INCREMENT PRIMARY KEY, + user_name VARCHAR(50) NOT NULL, + register_time DATETIME NOT NULL + ); + ``` + +- 写入用户创建时间 + ``` + SET SESSION cte_max_recursion_depth=9999999; + + INSERT INTO t_user(user_name, register_time) + WITH RECURSIVE t AS ( + SELECT 1 n, '2022-01-01 00:00:00' d + UNION ALL + SELECT n+1, d + INTERVAL '1' MINUTE + FROM t + WHERE n<10000 + ) + SELECT concat('user', n), d FROM t; + + SELECT date(register_time), count(*) FROM t_user GROUP BY date(register_time); + ``` +- 创建用户登录表 + ``` + DROP TABLE t_user_login; + CREATE TABLE t_user_login( + id INT AUTO_INCREMENT PRIMARY KEY, + uid INT NOT NULL, + login_time DATETIME NOT NULL + ); + + SET SESSION cte_max_recursion_depth=9999999; + ``` +- 写入用户登录数据 + ``` + INSERT INTO t_user_login(uid, login_time) + WITH RECURSIVE t AS ( + SELECT 1 n, rand()*10000 id, '2022-01-01 00:00:00' d + UNION ALL + SELECT n+1, rand()*10000, d + INTERVAL CEIL(n/3000) second + FROM t + WHERE n<500000 + ) + SELECT CEIL(id), d FROM t; + ``` + +# SQL + +- 多表连接方式统计 + ``` + SELECT date(u.register_time), + 100*count(DISTINCT ul1.uid)/count(DISTINCT u.id) rr1, + 100*count(DISTINCT ul2.uid)/count(DISTINCT u.id) rr3, + 100*count(DISTINCT ul3.uid)/count(DISTINCT u.id) rr7, + 100*count(DISTINCT ul4.uid)/count(DISTINCT u.id) rr30 + FROM t_user u + LEFT JOIN t_user_login ul1 ON (ul1.uid = u.id AND date(ul1.login_time) = date(u.register_time) + INTERVAL '1' DAY) + LEFT JOIN t_user_login ul2 ON (ul2.uid = u.id AND date(ul2.login_time) = date(u.register_time) + INTERVAL '3' DAY) + LEFT JOIN t_user_login ul3 ON (ul3.uid = u.id AND date(ul3.login_time) = date(u.register_time) + INTERVAL '7' DAY) + LEFT JOIN t_user_login ul4 ON (ul4.uid = u.id AND date(ul4.login_time) = date(u.register_time) + INTERVAL '30' DAY) + GROUP BY date(u.register_time); + ``` + +- 窗口函数 + ``` + WITH t1 AS ( + SELECT u.id, u.user_name, date(u.register_time) reg_date, date(l.login_time) login_date, + DENSE_RANK() OVER (PARTITION BY date(u.register_time) ORDER BY u.id) daily_reg, + DENSE_RANK() OVER (PARTITION BY date(u.register_time), date(l.login_time) ORDER BY l.uid) daily_login + FROM t_user u + LEFT JOIN t_user_login l + ON (l.uid = u.id AND date(l.login_time) BETWEEN date(u.register_time) + INTERVAL '1' DAY AND date(u.register_time) + INTERVAL '30' DAY) + ), + t2 AS ( + SELECT reg_date, max(daily_reg) daily_reg, login_date, max(daily_login) daily_login + FROM t1 + GROUP BY reg_date, login_date) + SELECT reg_date, max(daily_reg), + 100*max(CASE WHEN login_date = reg_date + INTERVAL '1' DAY THEN daily_login END)/max(daily_reg) rr1, + 100*max(CASE WHEN login_date = reg_date + INTERVAL '3' DAY THEN daily_login END)/max(daily_reg) rr3, + 100*max(CASE WHEN login_date = reg_date + INTERVAL '7' DAY THEN daily_login END)/max(daily_reg) rr7, + 100*max(CASE WHEN login_date = reg_date + INTERVAL '30' DAY THEN daily_login END)/max(daily_reg) rr30 + FROM t2 + GROUP BY reg_date; + ``` + +- 得到结果 + + ![sql-35]({{site.baseurl}}/img-post/sql-35.png) + + diff --git "a/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 JOIN \350\241\250\350\207\252\350\272\253\345\256\236\347\216\260\350\241\250\345\206\205\347\273\237\350\256\241.md" "b/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 JOIN \350\241\250\350\207\252\350\272\253\345\256\236\347\216\260\350\241\250\345\206\205\347\273\237\350\256\241.md" new file mode 100644 index 00000000000..b5b16d9eb4b --- /dev/null +++ "b/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 JOIN \350\241\250\350\207\252\350\272\253\345\256\236\347\216\260\350\241\250\345\206\205\347\273\237\350\256\241.md" @@ -0,0 +1,77 @@ +--- +layout: post +title: SQL:使用 JOIN 表自身实现表内统计 +subtitle: 统计用户第一次消费后30天内的购买数量 +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + +# 需求 + +- 给定用户 ID,统计用户第一次消费后30天内的总购买数量; + +# 创建表 + +- 创建日历表 user_sales + + ```aidl + create table user_sales( + user_id integer not null primary key, + sell_day date not null unique, + amount integer not null + ); + ``` + +# 写入测试数据 +- 写入数据; + ```aidl + -- 生成测试数据 + insert into user_sales values(1, '2021-01-01', 100); + insert into user_sales values(2, '2021-01-15', 100); + insert into user_sales values(2, '2021-01-10', 200); + insert into user_sales values(1, '2021-01-01', 300); + insert into user_sales values(1, '2021-01-20', 100); + insert into user_sales values(1, '2021-02-10', 200); + insert into user_sales values(2, '2021-02-01', 100); + insert into user_sales values(1, '2021-01-05', 100); + insert into user_sales values(3, '2021-01-20', 300); + insert into user_sales values(1, '2021-02-01', 300); + insert into user_sales values(3, '2021-02-21', 100); + ``` + +# SQL + + +```language +SELECT + s.user_id,s.first_day AS 第一次消费时间,sum(user_sales.amount) AS 30天消费 +FROM + ( + SELECT + user_id,min(sell_day) AS first_day + FROM + user_sales + GROUP BY + user_id + ) AS s +JOIN + user_sales +ON + ( + s.user_id=user_sales.user_id + AND + user_sales.sell_day BETWEEN s.first_day AND s.first_day + INTERVAL 30 DAY + ) +GROUP BY + s.user_id,s.first_day; +``` + +- 得到结果 + + ![sql-19]({{site.baseurl}}/img-post/sql-19.png) + + diff --git "a/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 RANK OVER \345\222\214 PARTION BY ORDER BY \345\256\236\347\216\260\345\210\206\347\273\204\346\216\222\345\272\217.md" "b/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 RANK OVER \345\222\214 PARTION BY ORDER BY \345\256\236\347\216\260\345\210\206\347\273\204\346\216\222\345\272\217.md" new file mode 100644 index 00000000000..53c336e4f3d --- /dev/null +++ "b/_posts/2022-01-24-SQL\357\274\232\344\275\277\347\224\250 RANK OVER \345\222\214 PARTION BY ORDER BY \345\256\236\347\216\260\345\210\206\347\273\204\346\216\222\345\272\217.md" @@ -0,0 +1,153 @@ +--- +layout: post +title: SQL:使用 RANK OVER + PARTION BY + ORDER BY 实现分组排序 +subtitle: 统计商品畅销度排行榜 +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + +# RANK OVER & PARTION BY + +- PARTION BY 进行分组,RANK OVER + ORDER BY 进行给每个分组内的记录进行排序 + +# 创建表 +- 需求: + - 按照产品的分类排名; + - 按照产品子类排名; + - 按照产品分类的飙升榜; +- 创建日历表 calendar + + ```aidl + create table products( + product_id integer not null primary key, + product_name varchar(100) not null unique, + product_subcategory varchar(100) not null, + product_category varchar(100) not null + ); + ``` + + ```language + create table sales( + product_id integer not null, + sale_time timestamp not null, + quantity integer not null + ); + ``` + +# 写入测试数据 +- 写入商品表; +```aidl +-- 生成测试数据 +insert into products values(1, 'iPhone 11', '手机', '手机通讯'); +insert into products values(2, 'HUAWEI P40', '手机', '手机通讯'); +insert into products values(3, '小米10', '手机', '手机通讯'); +insert into products values(4, 'OPPO Reno4', '手机', '手机通讯'); +insert into products values(5, 'vivo Y70s', '手机', '手机通讯'); +insert into products values(6, '海尔BCD-216STPT', '冰箱', '大家电'); +insert into products values(7, '康佳BCD-155C2GBU', '冰箱', '大家电'); +insert into products values(8, '容声BCD-529WD11HP', '冰箱', '大家电'); +insert into products values(9, '美的BCD-213TM(E)', '冰箱', '大家电'); +insert into products values(10, '格力BCD-230WETCL', '冰箱', '大家电'); +insert into products values(11, '格力KFR-35GW', '空调', '大家电'); +insert into products values(12, '美的KFR-35GW', '空调', '大家电'); +insert into products values(13, 'TCLKFRd-26GW', '空调', '大家电'); +insert into products values(14, '奥克斯KFR-35GW', '空调', '大家电'); +insert into products values(15, '海尔KFR-35GW', '空调', '大家电'); +``` +- 模拟生成销售数据 +```language +insert into sales +with recursive s(product_id, sale_time, quantity) as ( + select product_id, '2022-04-01 00:00:00', floor(10*rand(0)) from products + union all + select product_id, sale_time + interval 1 minute, floor(10*rand(0)) + from s + where sale_time < '2022-04-01 10:00:00' +) +select * from s; +``` + +# SQL + + +- 按照产品的分类,计算2022-04-01 09:00:00到2022-04-01 09:59:59一小时的销量排名 +```language +with hourly_sales(product_id, ymdh, quantity) as ( + select product_id, date_format(sale_time, '%Y%m%d%H') ymdh, sum(quantity) + from sales + where sale_time between '2022-04-01 09:00:00' and '2022-04-01 09:59:59' + group by product_id, date_format(sale_time, '%Y%m%d%H') +), +hourly_rank as( + select p.product_category, p.product_subcategory, p.product_name, s.quantity, + rank() over (partition by ymdh, p.product_category order by s.quantity desc) as rk + from hourly_sales s + join products p on (p.product_id = s.product_id) +) +select * +from hourly_rank; +``` +- 得到结果 + + ![sql-17]({{site.baseurl}}/img-post/sql-17.png) + +- 按照产品子类排名 + +``` +with hourly_sales(product_id, ymdh, quantity) as ( + select product_id, date_format(sale_time, '%Y%m%d%H') ymdh, sum(quantity) + from sales + where sale_time between '2022-04-01 09:00:00' and '2022-04-01 09:59:59' + group by product_id, date_format(sale_time, '%Y%m%d%H') +), +hourly_rank as( + select product_category, product_subcategory, product_name, quantity, + rank() over (partition by ymdh, product_category, product_subcategory order by quantity desc) as sub_rk + from hourly_sales s + join products p on (p.product_id = s.product_id) +) +select * +from hourly_rank; +``` +- 得到结果 + + ![sql-16]({{site.baseurl}}/img-post/sql-16.png) + +- 按照产品分类的飙升榜 + +``` +with hourly_sales(product_id, ymdh, quantity) as ( + select product_id, date_format(sale_time, '%y%m%d%H') ymdh, sum(quantity) + from sales + where sale_time between '2022-04-01 08:00:00' and '2022-04-01 09:59:59' + group by product_id, date_format(sale_time, '%y%m%d%H') +), +hourly_rank as( + select ymdh, product_category, product_subcategory, product_name, + rank() over (partition by ymdh, product_category order by quantity desc) as rk + from hourly_sales s + join products p on (p.product_id = s.product_id) +), +rank_gain as( + select product_category, product_subcategory, product_name, + -- lag 有三个参数,第一个参数是列名,第二个参数是偏移的 offset,第三个参数是 超出记录窗口时的默认值。 + rk, lag(rk, 1) over (partition by product_category, product_name order by ymdh) pre_rk, + 100 * (ifnull(lag(rk, 1) over (partition by product_category, product_name order by ymdh), 9999) - rk) + /rk as gain + from hourly_rank +), +top_gain as( + select *, rank() over (partition by product_category order by gain desc) gain_rk + from rank_gain + where pre_rk is not null +) +select product_category, product_subcategory, product_name, pre_rk, rk, concat(gain,'%') gain, gain_rk +from top_gain; +``` +- 得到结果 + + ![sql-18]({{site.baseurl}}/img-post/sql-18.png) diff --git "a/_posts/2022-01-24-SQL\357\274\232\346\225\260\344\273\223\344\270\255\345\270\270\347\224\250\347\232\204 SQL \350\257\255\345\217\245\346\261\207\346\200\273.md" "b/_posts/2022-01-24-SQL\357\274\232\346\225\260\344\273\223\344\270\255\345\270\270\347\224\250\347\232\204 SQL \350\257\255\345\217\245\346\261\207\346\200\273.md" new file mode 100644 index 00000000000..723a3695e5e --- /dev/null +++ "b/_posts/2022-01-24-SQL\357\274\232\346\225\260\344\273\223\344\270\255\345\270\270\347\224\250\347\232\204 SQL \350\257\255\345\217\245\346\261\207\346\200\273.md" @@ -0,0 +1,143 @@ +--- +layout: post +title: SQL:数仓中 INFORMATION_SCHEMA 应用汇总 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - SQL +--- + + +#### INFORMATION_SCHEMA 用法 + +- MySQL中有一个名为 INFORMATION_SCHEMA 的数据库,在该库中有一个 TABLES 表,这个表主要字段分别是: + - TABLE_SCHEMA : 数据库名, + - TABLE_NAME:表名, + - ENGINE:所使用的存储引擎, + -TABLES_ROWS:记录数, + - DATA_LENGTH:数据大小, + - INDEX_LENGTH:索引大小 + +#### 查询数据库表基本信息 + +- 查看数据库的表数量 + ```aidl + select table_schema as 数据库名称, count(*) as 表数量 + from INFORMATION_SCHEMA.TABLES + group by table_schema; + ``` + ![sql-17]({{site.baseurl}}/img-post/sql-29.png) + +- 查询指定数据库及其表数量 + ```aidl + SELECT table_schema as 库名 , count(*) as 表数量 + FROM information_schema.TABLES + where table_schema = 'ods' + ``` + ![sql-17]({{site.baseurl}}/img-post/sql-30.png) + +- 查询某个表中有多少字段 + ```aidl + SELECT COUNT(*) 字段数量 + FROM information_schema. COLUMNS + WHERE table_schema = 'ods' + AND table_name = 'ods_oms_stock'; + ``` + +- 查询某个数据库中所有表共有多少字段 + ```aidl + SELECT COUNT(column_name) as 总字段量 + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = 'ods'; + ``` + +- 统计某数据库中每个表的数据量 + + ```aidl + SELECT TABLE_NAME as 表名, + (DATA_LENGTH/1024/1024) as 数据大小_兆 , + (INDEX_LENGTH/1024/1024) as 索引数据大小_兆, + ((DATA_LENGTH+INDEX_LENGTH)/1024/1024) as 总数据大小_兆, + TABLE_ROWS as 总数据条数 + FROM TABLES + WHERE TABLE_SCHEMA = 'ods'; + ``` + ![sql-17]({{site.baseurl}}/img-post/sql-33.png) + +- 查询某数据库总数据大小 + ```aidl + SELECT (sum(DATA_LENGTH)/1024/1024) as 总数据量_单位M + from `TABLES` + WHERE TABLE_SCHEMA = 'ods'; + ``` + ![sql-17]({{site.baseurl}}/img-post/sql-34.png) + +- 查询某数据库中每张表数据条数 + ```aidl + select table_name as 表名 , table_rows as 数据条数 + from tables + where TABLE_SCHEMA = 'ods'; + ``` + +- 查询某数据库内所有表总数据条数 + ```aidl + SELECT sum(table_rows) as 总数据条数 + from tables + where TABLE_SCHEMA = 'ods' + ``` + +#### 查看表字段基本信息 + +- 查询某个数据库中所有字段 + ```aidl + SELECT column_name as 全部字段 + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = 'ods'; + ``` + ![sql-17]({{site.baseurl}}/img-post/sql-31.png) + +- 查询某数据库中所有表、字段、字段类型、注释等信息 + + ```aidl + SELECT TABLE_NAME 表名, column_name 字段名, DATA_TYPE 数据类型, column_comment 注释 + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = 'ods'; + ``` + ![sql-17]({{site.baseurl}}/img-post/sql-32.png) + +#### INFORMATION_SCHEMA.COLUMNS 查找指定字段 + +- 通常情况下,我们可以通过以下命令,查看指定表包含哪些字段信息: + - show columns from 表名; + - describe 表名; + - show create table 表名; + +- 但在数仓工作中,经常要查看哪些数据表包含指定字段,由于涉及的表比较多,不可能逐个的去查找表格,需要使用特殊方法 `INFORMATION_SCHEMA.COLUMNS`。 + +- 查看全部的 **库名 + 表名 + 字段名** + ```aidl + select concat(table_schema,'.',table_name,'.',column_name) from INFORMATION_SCHEMA.COLUMNS; + ``` + +- 查看【字段名】包含指定字符串: + - 精确查找: + ```aidl + select * from INFORMATION_SCHEMA.COLUMNS where column_name = '尺码'; + ``` + ![sql-17]({{site.baseurl}}/img-post/sql-27.png) + - 模糊匹配: + ```aidl + select * from INFORMATION_SCHEMA.COLUMNS where column_name rlike '尺'; + ``` + ![sql-17]({{site.baseurl}}/img-post/sql-26.png) + +- 查看【注释】包含指定字符串: + + ```aidl + select * from INFORMATION_SCHEMA.COLUMNS where column_comment rlike '尺'; + ``` + + ![sql-17]({{site.baseurl}}/img-post/sql-28.png) diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232AOP \347\274\226\347\250\213\346\200\235\346\203\263\345\217\212\345\205\266\345\234\250 Android \345\237\213\347\202\271\344\270\255\347\232\204\345\272\224\347\224\250.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232AOP \347\274\226\347\250\213\346\200\235\346\203\263\345\217\212\345\205\266\345\234\250 Android \345\237\213\347\202\271\344\270\255\347\232\204\345\272\224\347\224\250.md" new file mode 100644 index 00000000000..3af85c76db4 --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232AOP \347\274\226\347\250\213\346\200\235\346\203\263\345\217\212\345\205\266\345\234\250 Android \345\237\213\347\202\271\344\270\255\347\232\204\345\272\224\347\224\250.md" @@ -0,0 +1,163 @@ +--- +layout: post +title: 数据埋点:AOP 编程思想及其在 Android 埋点中的应用 +subtitle: ASM & AspectJ +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + +# 什么是 AOP + +#### OOP vs AOP + +- OOP 的局限: + - OOP 的精髓是把功能或问题模块化,每个模块处理自己的事情,但在现实世界中,并不是所有问题都能完美得划分到模块中。 + - 举个最简单而又常见的例子,现在想为每个模块加上日志功能,要求模块运行时候能输出日志。一般的处理都是: + - 先设计一个日志输出模块,这个模块提供日志输出 API,比如 Android 中的 Log 类。 + - 然后,其他模块需要输出日志的时候调用 Log 类的几个函数,比如 e(TAG,…),w(TAG,…),d(TAG,…),i(TAG,…)等。 + - 但是,从 OOP 角度看,除了日志模块本身,其他模块的业务动作、绝大部分情况下应该都不会包含日志输出功能。 + - 以 ActivityManagerService 为例,显然 ActivityManagerService 的功能点中不包含输出日志这一项。 + - 但实际上,软件中的众多模块确实又需要打印日志。 + - 这个日志输出功能,从整体来看,都是一个面上的。而这个面的范围,就不局限在单个模块里了,而是横跨多个模块。 + +- 在 AOP 中 + - 我们要认识到 OOP 世界中,有些功能是横跨并嵌入众多模块里的,比如打印日志,数据上报等。这些功能在各个模块里分散得很厉害,可能到处都能见到。 + - AOP 的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。 + >如果说,OOP 如果是把问题划分到单个模块的话,那么 AOP 就是把涉及到众多模块的某一类问题进行统一管理。 + - 比如我们可以设计两个 Aspects,一个是管理某个软件中所有模块的日志输出的功能,另外一个是管理该软件中一些特殊函数调用的权限检查。 + +#### AOP 实现 + +- 实现的时间点及思路 + - 编译时 + - 在 APK 打包过程中对 .clss 文件的字节码进行扫描更改,通过向编译过程添加额外的步骤来修改被编译的类; + - 加载时 + - 当目标类被 Dalvik 或者 ART 加载的时候修改才会被执行,这是对 Java 字节码文件或者 Android 的 dex 文件进行的注入操作; + - 运行时 + - Hook 某些关键方法,比如:使用动态代理(这可以说并不是真正的代码注入); + +- 实现方法 + - 切面编程库(AspectJ),案例:Hugo + - 字节码注入(ASM),案例:GrowingIO + +#### AOP 编程的具体使用场景 + +- 日志记录 +- 数据持久化 +- 行为监测 +- 组件生命周期监控 +- 方法执行时间监控 +- 用户点击事件埋点 +- 弹窗事件埋点 +- 数据验证 +- 缓存 + + +# AspectJ + +#### AspectJ 是什么 + +- AspectJ 是广泛应用于 JavaEE 开发的 AOP 方案,是面向切面编程在 Java 中的一种具体实现,简单易用、功能强大。 + - 正如面向对象编程是对常见问题的模块化一样,面向切面编程是对横向的同一问题进行模块化; + - 比如:在某个包下的所有类中的某一类方法中、都需要解决一个相似的问题,可以通过 AOP 的编程方式对此进行模块化封装,统一解决。 + - AspectJ 提供了简便的语法让我们定义切面逻辑,再通过提供的 AJC 编译器,在 Java 文件编译成 class 文件的过程里,把切面代码织入到目标业务代码里。 +>本质上,AspectJ 仍然是以代理的方式实现 AOP,我们通过 AspectJ 就能方便的在目标方法执行前后执行我们的埋点代码。 + +#### AspectJ 组成部分 + +- AspectJ 向 Java 引入了一个新的概念—— join point,它包括几个新的结构: + - pointcuts + - advice + - inter-type declarations + - aspects。 + +- 含已解释 + - join point 是在程序流中被定义好的点; + - pointcut 在那些点上选出特定的 join point 和值; + - advice 是到达 join point 时被执行的代码; + - inter-type declarations 是 AspectJ 具有的不同类型的类型间声明,允许程序员修改程序的静态结构,即其类的成员和类之间的关系。 + +- AspectJ 几个名词术语解释 + + - Cross-cutting concerns + - 即使在面向对象编程中大多数类都是执行一个单一的、特定的功能,它们也有时候需要共享一些通用的辅助功能。 + - 比如:我们想要在一个线程进入和退出一个方法时,在数据层和UI层加上输出log的功能。尽管每一个类的主要功能时不同的,但是它们所需要执行的辅助功能是相似的。 + + - Advice + - 需要被注入到.class字节码文件的代码。通常有三种:before,after和around,分别是在目标方法执行前,执行后以及替换目标代码执行。 + - 除了注入代码到方法中外,更进一步的,你还可以做一些别的修改,例如添加成员变量和接口到一个类中。 + + - Join point + - 程序中执行代码插入的点,例如方法调用时或者方法执行时。 + + - Pointcut + - 告诉代码注入工具在哪里注入特定代码的表达式(即需要在哪些Joint point应用特定的Advice)。它可以选择一个这样的点(例如,一个单一方法的执行)或者许多相似的点(例如,所有被自定义注解@DebugTrace标记的方法)。 + + - Aspect + - Aspect 将 pointcut 和 advice 联系在一起。 + - 例如:我们通过定义一个 pointcut 和给出一个准确的 advice 实现向我们的程序中添加一个打印日志功能的 aspect。 + + - Weaving + - 向目标位置(join point)注入代码(advice)的过程。 + +# ASM 字节码插桩 + +#### ASM 是什么 + +- ASM 是字节码操作库,支持对字节码进行编辑,实现类、属性和方法的增删改查; +- 字节码操作库还有 Javaassit 库可以选择,但 ASM 灵活度和效率都是最高的; +- ASM 可以直接产生二进制的 class 文件,也可以增强既有类的功能。 + - Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素,包括: + - 类名称 + - 方法 + - 属性 + - Java 字节码(指令)。 + + +#### 字节码插桩 + +- 利用操作 ASM 字节码实现方法计时,可以的做法是修改 class 文件,在目标方法开始和结束时插入本来需要手动埋点的代码,这个过程称之为字节码插桩。 + + ![]({{site.baseurl}}/img-post/数据埋点-8.png) + +- 如图所示是Android打包流程:.java 文件 -> .class 文件 -> .dex 文件; +- 只要在红圈处拦截住,拿到所有方法进行修改完再放生就可以了,而做到这一步也不难,Google 官方在Android Gradle 的 1.5.0 版本以后提供了 Transfrom API,允许第三方 Plugin 在打包 dex 文件之前的编译过程中操作 .class 文件; +- ASM 字节码插桩,要做的就是实现 Transform 进行 .class 文件遍历拿到所有方法,修改完成对原文件进行替换。 + +#### ASM 框架中的核心类 + +- ClassReader + - 该类用来解析编译过的class字节码文件。 + +- ClassWriter + - 该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。 + +- ClassViitor + - 主要负责 “拜访” 类成员信息,包括: + - 标记在类上的注解, + - 类的构造方法, + - 类的字段, + - 类的方法, + - 静态代码块。 + +- AdviceAdapter + - 实现了 MethodVisitor 接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。 + + +# AspectJ vs ASM + +#### 共同点 + +- AspectJ 和 ASM 既支持以注解作为切入点,也支持根据类方法名/类继承关系等规则来确定切入点。 + - 注解的作用是提供插入点; + +#### 不同点 + +- AspectJ 本质上仍然是以代理的方式实现 AOP; +- ASM 通过直接对 class 文件进行修改的方式,实现 AOP 埋点需求。 + diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\344\272\213\344\273\266\347\232\204\345\217\202\346\225\260\345\255\227\346\256\265.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\344\272\213\344\273\266\347\232\204\345\217\202\346\225\260\345\255\227\346\256\265.md" new file mode 100644 index 00000000000..9db9f6122cb --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\344\272\213\344\273\266\347\232\204\345\217\202\346\225\260\345\255\227\346\256\265.md" @@ -0,0 +1,265 @@ +--- +layout: post +title: 数据埋点:埋点事件的参数字段 +subtitle: 用户行为日志埋点参数示例 +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + +# 1. 常见的公共参数 + +#### 1.1. 环境信息 + +- 用户信息; +- 用户UID; +- 设备PID; +- 跳入时间; +- 跳出时间; +- 地理位置; +- 设备,品牌、型号、系统; +- 应用,应用版本; +- 渠道,应用下载渠道。 + +#### 1.2. 页面浏览记录 + +- 页面信息; +- 页面ID; +- 页面对象; + +#### 1.3. 曝光对象 + +- 广告; +- 推荐; +- 对象类型; +- 对象ID。 + +#### 1.4. 启动记录 + +- 启动时间 +- 启动类型,图标或推送; +- 开屏广告,广告ID等。 + +#### 1.5. 事件日志 + +- 点击事件 +> 事件日志,在大数据中多采用 **批处理** 方式,而不采用 **实时处理**,以减轻对于日志服务器的压力。多数企业都是做批处理,实时处理典型的用户是 **今日头条**。 +> + +#### 1.6. 错误信息 + +- 异常记录 +- 程序崩溃 +- 错误加载 + + +#### 1.7. 公共字段示例 + +- 基本的设备(手机 / PC / PAD等)都有的字段。 + + ``` + "cm": { //公共字段 + "mid": "", // (String) 设备唯一标识 + "uid": "", // (String) 用户标识 + "vc": "1", // (String) versionCode,程序版本号 + "vn": "1.0", // (String) versionName,程序版本名 + "l": "zh", // (String) 系统语言 + "sr": "", // (String) 渠道号,应用从哪个渠道来的。 + "os": "7.1.1", // (String) Android系统版本 + "ar": "CN", // (String) 区域 + "md": "BBB100-1", // (String) 手机型号 + "ba": "blackberry", // (String) 手机品牌 + "sv": "V2.2.1", // (String) sdkVersion + "g": "", // (String) gmail + "hw": "1620x1080", // (String) heightXwidth,屏幕宽高 + "t": "1506047606608", // (String) 客户端日志产生时的时间 + "nw": "WIFI", // (String) 网络模式 + "ln": 0, // (double) lng经度 + "la": 0 // (double) lat 纬度 + }, + ``` + +# 2. 全埋点事件 + +- 全埋点事件示例: + + ![]({{site.baseurl}}/img-post/埋点需求示例-2.png) + +# 3. 业务自定义事件 + +#### 3.1. 自定义参数 + +- 业务自定义参数,是为不同目的而采集的参数,这些参数在不同页面位置、上报指定类型数据,用于指定 + +#### 3.2. 自定义事件示例 + + ![]({{site.baseurl}}/img-post/埋点需求示例.png) + +#### 3.3. 列表页 loading + +``` +action 动作:开始加载=1,加载成功=2,加载失败=3 +loading_time 加载时长:计算下拉开始到接口返回数据的时间,(开始加载报 0,加载成 功或加载失败才上报时间) +loading_way 加载类型:1-读取缓存,2-从接口拉新数据 (加载成功才上报加载类型) +extend1: 扩展字段 Extend1 +extend2: 扩展字段 Extend2 +type 加载类型:自动加载=1,用户下拽加载=2,底部加载=3(底部条触发点击) +``` + +#### 3.4. 商品点击 display + +``` +action 动作:曝光商品=1,点击商品=2, +goodsid 商品 ID(服务端下发的 ID) +place 顺序(第几条商品,第一条为 0,第二条为 1,如此类推) +extend1 曝光类型:1 - 首次曝光 2-重复曝光 +category 分类 ID(服务端定义的分类 ID) +``` + +#### 3.5. 商品详情页 + +``` +entry 页面入口来源:应用首页=1、push=2、详情页相关推荐=3 +action 动作:开始加载=1,加载成功=2(pv),加载失败=3, 退出页面=4 +goodsid 商品 ID(服务端下发的 ID) +show_style 商品样式:0、无图、1、一张大图、2、两张图、3、三张小图、4、一张小图、 5、一张大图两张小图 +news_staytime 页面停留时长:从商品开始加载时开始计算,到用户关闭页面所用的时间。 若中途用跳转到其它页面了,则暂停计时,待回到详情页时恢复计时。或中 途划出的时间超过 10 分钟,则本次计时作废,不上报本次数据。如未加载成 功退出,则报空。 +loading_time 加载时长:计算页面开始加载到接口返回数据的时间 (开始加载报 0,加载 成功或加载失败才上报时间) +type1 加载失败码:把加载失败状态码报回来(报空为加载成功,没有失败) +category 分类 ID(服务端定义的分类 ID) +``` + +#### 3.6. 广告 + +``` +entry 入口:商品列表页=1 应用首页=2 商品详情页=3 +action 动作: 广告展示=1 广告点击=2 +contentType Type: 1 商品 2 营销活动 +displayMills 展示时长 毫秒数 +itemId 商品 id +activityId 营销活动 id +``` + +#### 3.7. 消息通知 + +``` +action 动作:通知产生=1,通知弹出=2,通知点击=3,常驻通知展示(不重复上 报,一天之内只报一次)=4 +type 通知 id:预警通知=1,天气预报(早=2,晚=3),常驻=4 +ap_time 客户端弹出时间 +content 备用字段 +``` + +#### 3.8. 后台活跃 + +``` +active_source 1=upgrade,2=download(下载),3=plugin_upgrade +``` + +#### 3.9. 评论 + +``` +序号 字段名称 字段描述 字段类型 长度 允许空 缺省值 +1 comment_id 评论表 int 10,0 +2 userid 用户 id int 10,0 √ 0 +3 p_comment_id 父级评论 id(为 0 则是一级评论,不 为 0 则是回复) int 10,0 √ +4 content 评论内容 string 1000 √ +5 addtime 创建时间 string √ +6 other_id 评论的相关 id int 10,0 √ +7 praise_count 点赞数量 int 10,0 √ 0 +8 reply_count 回复数量 int 10,0 √ 0 +``` + +#### 3.10. 收藏 + +``` +序号 字段名称 字段描述 字段类型 长度 允许空 缺省值 +1 id 主键 int 10,0 +2 course_id 商品 id int 10,0 √ 0 +3 userid 用户 ID int 10,0 √ 0 +4 add_time 创建时间 string √ +``` + +#### 3.11. 点赞 + +``` +序号 字段名称 字段描述 字段类型 长度 允许空 缺省值 +1 id 主键 id int 10,0 +2 userid 用户 id int 10,0 √ +3 target_id 点赞的对象 id int 10,0 √ +4 type 点赞类型 1 问答点赞 2 问答评论 点赞 3 文章点赞数 4 评论点赞 int 10,0 √ +5 add_time 添加时间 string √ +``` + +#### 3.12. 错误日志 + +``` +errorBrief 错误摘要 +errorDetail 错误详情 +``` + +#### 3.13. 启动日志 + +``` +entry 入 口 : push=1 , widget=2 , icon=3 , notification=4, lockscreen_widget =5 +open_ad_type 开屏广告类型: 开屏原生广告=1, 开屏插屏广告=2 +action 状态:成功=1 失败=2 +loading_time 加载时长:计算下拉开始到接口返回数据的时间,(开始加载报 0,加载成 功或加载失败才上报时间) +detail 失败码(没有则上报空) +extend1 失败的 +message(没有则上报空) +en 日志类型 +``` + +- 启动日志示例: + + ``` + { + "action":"1", + "ar":"MX", + "ba":"HTC", + "detail":"", + "en":"start", + "entry":"2", + "extend1":"", + "g":"43R2SEQX@gmail.com", + "hw":"640*960", + "l":"en", + "la":"20.4", + "ln":"-99.3", + "loading_time":"2", + "md":"HTC-2", + "mid":"995", + "nw":"4G", + "open_ad_type":"2", + "os":"8.1.2", + "sr":"B", + "sv":"V2.0.6", + "t":"1561472502444", + "uid":"995", + "vc":"10", + "vn":"1.3.4" + } + ``` + +#### 3.14. 事件埋点示例 + + ``` + "et": [ //事件 + { + "ett": "1506047605364", //客户端事件产生时间 + "en": "display", //事件名称 启动和事件日志是根据事件名称的不同 + "kv": { //事件结果,以key-value形式自行定义 + "goodsid": "236", + "action": "1", + "extend1": "1", + "place": "2", + "category": "75" + } + } + ] + } + ``` diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\346\262\273\347\220\206\351\235\242\345\257\271\347\232\204\346\212\200\346\234\257\351\227\256\351\242\230.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\346\262\273\347\220\206\351\235\242\345\257\271\347\232\204\346\212\200\346\234\257\351\227\256\351\242\230.md" new file mode 100644 index 00000000000..3193b669497 --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\346\262\273\347\220\206\351\235\242\345\257\271\347\232\204\346\212\200\346\234\257\351\227\256\351\242\230.md" @@ -0,0 +1,100 @@ +--- +layout: post +title: 数据埋点:埋点治理面对的技术问题 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + +# 埋点方案选择 + +#### 全埋点 or 分模块埋点,直接影响数据存储成本 + +- 如果数据结构优化不做好,每年浪费的存储成本可能会是百万级的消耗,随着周期的增加、成本浪费会更严重。 + +# 技术层面问题 + +#### 事件上报机制 + +- 例如:APP,需要做延时上报,将数据预先存储在手机上,等到有 WIFI 环境时再上报; + +#### 业务兼容的问题 + +- 前期规范执行之后,后续随着业务的拓展,已有数据字段满足不了业务的分析需求。 + +#### 前端版本兼容的问题 + +- 埋点从应用端来区分,web/ios/android,小程序,公众号; +- 然后还要区分一下是原生、还是 H5,新老版本之间肯定会带来一些模块化的差异。 + +#### 前端埋点数据丢失问题 + +- 前端请求服务端的数据大多存在 binlog 里面的,数据日志同步解析的过程里面可能会存在丢包的可能性,数仓的稳定性也会影响数据质量。 + +#### 埋点数据和后端数据不一致 + +- 后端服务信息存储的数据是存在 mysql,表字段结构化,分多表存储,需要靠主键进行关联,有大量的ETL过程。 +- 两者之间可能因为数据清洗、处理、实时技术等原因,造成数据差异化; + +#### 埋点开发技术执行不到位的问题 + +- 绝大多数情况下我们说埋点,一般都是说前端埋点,前端开发工程师在做埋点的时候又多是人为埋点,在开发过程中,会造成部分信息冗余、重复、记录不完整的情况存在; + +# 应用层面问题 + +#### 多产品之间的模块差异化问题 + +- 埋点不能够只有一套标准规范,多生态应用下,业务繁琐,在产品、技术的架构上有明显的差异,不同的产品、模块、坑位、点击事件的定义也可能有一定的区别,这时候可能需要根据场景划分不同的埋点标准; + +#### 自定义埋点信息的键对设计问题 + +- 往往会在埋点里面增加一个json的字段(bdata),在埋点的时候写入自定义的业务信息进行场景识别,譬如活动id、业务信息、用户快照的基本信息等,不同开发写入的自定义字段格式可能会有差异; + +#### 自埋点和第三方应用统计口径的问题 + +- 自埋点一般都会定义一个唯一id作为区分用户的标志,但是第三方是缺少用户属性信息的判断,一般会以设备号uuid/imse,或者IP地址段、mac地址段作为区分标志,从而造成统计数据上的差异化; +- 对于留存分析、转化分析、流失分析需要用到明细数据的场景,可兼容性不是很友好,需要进行 ID Mapping 操作。 + +# 预留字段问题 + +#### 基于业务需求、预留字段 + +- 基于业务分析框架,梳理常规分析案例中需要用到的埋点数据集,核心指标必须要有埋点; +- 基于算法模型框架,梳理算法所需要构建的数据特征需要用到的字段信息; +- 基于业务诉求,梳理非常规、当前没需求未来有应用场景的字段信息; + +#### 基于用户标签、预留字段 + +- 基于用户画像的标签建设,需要考虑画像的多层属性,社会属性、基本属性、市场属性、交易属性、行为属性等,通过画像筛选人群的时候,可能需要通过数据模型建立用户分层的过程,所需要用到的辅助数据; +- 基于智能运营的标签建设,运营策略、活动、方案的数据需求收集,哪些标签需要用到埋点中的信息; +- 基于营销系统的标签建设,涉及到渠道分配、广告投放、点击预测等,可能需要对曝光、点击、转化进行全链路的埋点建设,或者基于某一个产品使用链路,埋点数据要完备; + +#### 基于推荐系统、预留字段 + +- 推荐算法中需要用到的数据特征中包含哪些数据指标,其中埋点的部分所需要的数据格式是怎样的; +- 推荐算法的设计方案包括,基于用户、基于物品、协同过滤、基于规则、基于融合模型,不同的方案下、对数据底层的要求可能也会有一定的差异; + +# 数仓落表问题 + +#### 数仓建模设计 + +- 埋点数据落到数仓后,需要预先建立哪些表,如何做埋点数据的分层; + +#### 数仓库表的开发成本 + +- 埋点的数据体量是非常大的,TB级数据的存储本身就是一个比较大的成本,再加上调度系统、计算资源、运行性能等方面,就需要数仓团队在一开始就要把数据模型提前建立好,做好ods层到dw层、ads层的划分,维度和事实之间的建设; + +#### 数仓性能问题 + +- 因为埋点数据的体量问题,落表的时候、一定会存在大量的冗余字段,如果集群资源比较紧张,对于常规数据的统计、计算都会带来性能上的问题; + +#### 查询响应时效问题 + +- 在数据团队的架构中,有对外提供数据应用服务,对于数据的实时计算就有一定的要求,什么场景下应该是T+1,什么场景下应该是伪实时,避免数据调度任务影响前台应用产出; + + diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\346\262\273\347\220\206\351\235\242\345\257\271\347\232\204\351\241\271\347\233\256\347\256\241\347\220\206\351\227\256\351\242\230.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\346\262\273\347\220\206\351\235\242\345\257\271\347\232\204\351\241\271\347\233\256\347\256\241\347\220\206\351\227\256\351\242\230.md" new file mode 100644 index 00000000000..1840098a3ef --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\346\262\273\347\220\206\351\235\242\345\257\271\347\232\204\351\241\271\347\233\256\347\256\241\347\220\206\351\227\256\351\242\230.md" @@ -0,0 +1,118 @@ +--- +layout: post +title: 数据埋点:埋点治理面对的项目管理问题 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + + +# 埋点项目管理 + +#### 数据埋点项目流程 + +- 梳理埋点需求(分析指标体系) +- 制定埋点规则 + - 命名的规范化 + - 埋点流程统一化 +- 制定埋点数据的清洗规则 + - 数据清洗规范化 +- 维护埋点的测试、上线、下线 +- 维护埋点元数据(埋点管理系统) + +#### 埋点项目示例 + +![]({{site.baseurl}}/img-post/指标体系方法论-4.png) + + +# 项目管理问题 + +#### 内部信息障碍 + +- 开发人员疑虑 + - 埋点文档太乱,阅读体验极差; + - 没有埋点变更信息记录,有数据质量问题时,数据溯源工作非常艰难。 + +- 测试人员疑虑 + - 每次埋点测试都需要到数据库去查; + - 由于 SDK 上报时机的原因,往往不能即时看到数据,需要等一定时间,比较耗费时间; + - 如果不会写 sql,测试查询会非常困难; + - 有时候遇到大版本迭代,一次性上线的埋点比较多,测试工作量巨大。 + +- 数据产品疑虑 + - 业务经常会问这个数据有没有埋点,埋点ID是什么?有哪些可以分析的维度?但是埋点信息都是分散在各个业务产品经理手上,大数据部门没有埋点元数据。 + - 当前埋点是什么时候上线的,是不是因为刚发版,新老版本过渡期,数据还没报上来或者新版本流量较小,这时才发现,没有相关记录可以查。 + - 有时,上游业务系统发生变更,也不会通知下游,业务发现数据异常时,通常第一时间找数据产品经理,如果数据产品经理不知道上游埋点的变更,这时往往是一脸懵。 + +#### 埋点需求混乱且缺少管控 + +- 当新功能或活动上线时会提很多埋点需求,数据产品在这个环节如果对埋点需求没有明确的提需规范和把控,就会导致埋点需求爆炸,对于开发和维护成本都是压力; +- 并且后续做数据分析的时候经常会发现数据不可用或数据不准确,那其实后续排查问题的成本非常大, + +#### 未明确埋点要统计哪些指标 + +- 数据产品在评审埋点需求的时候很重要的一点就是:明确埋点要统计哪些指标; +- 埋点统计是服务于指标的,如果对埋点需求没有管控放任提需,经过几个版本的迭代就会发现埋点维护很难; + +#### 埋点需求不规范 + +- 埋点需求规范的价值是帮助业务方和数据产品拉齐对即将开发的埋点认知一致; +- 在设计埋点提需规范时,不仅仅要让业务方标明要统计哪些指标、事件如何规划、触发时机,最好能写出每个自定义参数的触发时机、参数打在哪个层级、是否需要透传等; +- 埋点提需规范越详细越好,可以帮忙拉齐各方对埋点的认知。 + +#### 业务接口人素质参差不齐 + +- 需求接口人这个角色很重要,没有接口人的话仅靠数据产品跟业务对接,在大体量和复杂业务场景的公司里是不现实的; +- 所以接口人的定位是埋点需求 master,甚至是数据需求 master,需要将业务方、开发、测试、数据产品等连接起来; +- 建议接口人可以考虑经常对接埋点需求的业务,或是有开发背景的业务方,这样沟通起来会方便一些。 + +#### 未规范录入信息 + +- 虽然提供了埋点录入工具,但是业务产品往往只在在线文档上维护埋点信息,只有用数据的时候才会来录入,导致埋点信息不全; + +#### 信息录入不完善 + +- 例如: + - 埋点管理平台仅有部分埋点 ID 和名称及所属应用,缺少埋点属性、位置归属等信息,不能提供完整的埋点元数据供数据使用者,导致数据理解困难; + +#### 只埋点、不维护 + +- 埋点变更、下线等情况无人维护,也没有通知下游进行影响评估; + - 比如: + - 某天业务突然找数据产品经理反馈,当前的数据值和埋点元数据不一致,数据产品经理需要去排查原因,有时候查了半天原因才发现是业务产品改版,所以变更了埋点导致了数据异常,但这种情况只有变更上线后才能发现,没法在事情发现,进行影响评估,也没法在事后通知相关方。 +- 甚至,埋点信息根本就无人持续维护。 + +#### 缺乏埋点生命周期管理 + +- 缺少生命周期的管理一味放任熵增,后续治理的成本实在很高,所以埋点生命周期的管理必不可缺; +- 简单来说要做好后续埋点梳理、埋点瘦身、埋点升级的工作,定期统计一段时间内低频上报的埋点和这些埋点相关指标、报表的访问量,以此为依据开展埋点生命周期管理工作; +- 对于埋点需求的提出、评审、开发、测试、验收、上架、修改、下架,必须要做持续的全流程、全生命周期的管控; + +# 规范制度层面问题 + +#### 制度层面缺少统一的流程及操作要求 + - 埋点从需求到上线的流程是怎样的,埋点需求应该在哪里创建,维护,埋点元数据应该在哪里查询等,在制度上没有相关规定, + - 有的业务产品经理认为:埋点文档不应该给业务的人看,业务用数据时,找大数据部门就好了,不应该找业务产品经理要埋点信息。 + - 现实是埋点元数据散落在各业务产品经理手上,数据产品经理也不知道埋点元数据。这样,用数据的人常常需要找多方沟通,才能拿到埋点信息,对业务来说,找数据极其困难。 + +#### 埋点定义缺少统一标准和规范 + + - 比如对于自定义参数: + - 有的开发以数组的形式存在一个字段里面, + - 有的开发是将自定义参数存在分别上报到不同的字段里面, + - 这种标准的不一致,不但会增加下游数据处理成本,也会对数据使用者造成一定的困惑; + - 比如:导致埋点数据不能通过自定以参数筛选。 + +#### 特殊场景缺少规范指导 + +- 比如新版本增加了和已有埋点相同意义的埋点,是新增一个埋点ID还是用已有的埋点ID,增加参数区分所属埋点事件的位置等等。 + +#### 埋点元数据管理混乱 + +- 比如埋点位置结构无统一维护,针对同样的页面,不同的产品经理理解不一致,导致埋点归属混乱,就像图书馆的数没有目录和检索系统一样,找起来非常困难。 + diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\347\232\204\344\272\247\345\223\201\351\234\200\346\261\202\350\256\276\350\256\241.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\347\232\204\344\272\247\345\223\201\351\234\200\346\261\202\350\256\276\350\256\241.md" new file mode 100644 index 00000000000..93b813ad038 --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\213\347\202\271\347\232\204\344\272\247\345\223\201\351\234\200\346\261\202\350\256\276\350\256\241.md" @@ -0,0 +1,214 @@ +--- +layout: post +title: 数据埋点:埋点的产品需求设计 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + +# 1. 埋点设计 + +#### 1.1. 埋点需求基本要素 + +- 上报时间 +- 涉及页面 +- 用户信息 +- 系统信息 +- 触发条件(session 回溯) +- 上报位置 + +#### 1.2. 埋点事件设计原则 + +- 驼峰命名法; +- 命名格式:模块_页面_事件; +- 区分公共事件, + - 如设备、位置、机型等,不要每次都上报,这样可以大大减少数据冗余; +- 相同的功能事件设置相同的 event_id,通过属性参数来做区分; + + +#### 1.3. 埋点设计步骤 + +- 需求梳理 + - 在设计属性和事件的时候,我们要知道哪些经常变,哪些不变,哪些是业务行为,哪些是基本属性; + - 基于基本属性事件,我们认为属性是必须采集项,只是属性里面的事件属性、根据业务不同有所调整而已。 +- 业务分解 + - 梳理确认业务流程、操作路径和不同细分场景、定义用户行为路径 +- 分析指标 + - 对特定的事件进行定义、核心业务指标需要的数据 +- 事件设计 + - APP启动,退出、页面浏览、事件曝光点击 +- 属性设计 + - 用户属性、事件属性、对象属性、环境属性 + - +#### 1.4. 埋点日志基本字段 + +- 用户 ID +- 事件名称 +- 事件 ID +- 所在页面(位置) +- 属性 +- 属性值 +- 时间 + - 设备上报时间 + - 到达服务器的时间 + +#### 1.5. 多维事件模型 + +- event 事件模型 + - 使用 event 记录用户在产品上的各种行为; + - What + - When + - Where + - Who + - How +- user 用户模型 + - user 的基础属性信息 +- 二者既可以分别分析,也可以关联分析。 + +#### 1.6. 埋点的总体要求 + +- 数据稳定准确,反映真实业务场景 +- 需要长期监控,数据需要长期存储 +- 业务属性丰富,可以做深度业务分析 +- 对于核心 KPI,指标需要全面覆盖 +- 需要设置数据权限 + + +# 2. 埋点采集的顶层设计 + +#### 2.1. 用户识别 + +- 用户识别机制的混乱会导致两个结果: + - 一是数据不准确,比如UV数据对不上; + - 二是涉及到漏斗分析环节出现异常。 +- 因此应该做到: + - a.严格规范ID的本身识别机制; + - b.跨平台用户识别 + +#### 2.2. 同类抽象 + +- 同类抽象包括事件抽象和属性抽象: + - 事件抽象即浏览事件,点击事件的聚合; + - 属性抽象,即多数复用的场景来进行合并,增加来源区分。 + +#### 2.3. 采集一致 + +- 采集一致包括两点: + - 一是跨平台页面命名一致, + - 二是按钮命名一致 +- 埋点的制定过程本身就是规范底层数据的过程,所以一致性是特别重要,只有这样才能真正的用起来 + +#### 2.4. 渠道配置 + +- 渠道主要指的是推广渠道,落地页,网页推广页面,APP推广页面等,这个落地页的配置要有统一规范和标准。 + +#### 2.5. 系统化管理埋点事件 + +- 实现统一的格式; +- 方便埋点查询、变更管理; +- 易于埋点元数据管理; +- 方便埋点生命周期管理; +- 便于进行埋点数据质量的监控。 + + +# 3. 埋点需求梳理 + +#### 3.1. 埋点事件表 + +- 事件表示例: + + ![]({{site.baseurl}}/img-post/数据埋点-3.png) + +- 埋点位置 + - 如果业务覆盖了APP、Web和小程序平台,分开管理会造成指标冗余; + - 对于多平台存在的核心指标,采用的是统一事件名定义; + - 不同平台触发时,数据上报到同一个事件名上,通过平台类型(platform_type)进行拆分; + +- 事件定义: + - 比较难处理的是【触发条件】的准确定义和描述,如页面的进度数据,触发定义成加载和加载成功完全不同; + - 比如,首页模块浏览,模块长短不一,到何种深度会触发对应模块的浏览,需要定义时想清楚,与开发沟通实现细节,避免后期踩坑; + +- 参数定义: + - 用来定义事件的参数,也可以理解为事件维度; + - 该字段决定了事件的颗粒度,直接影响到事件下钻的颗粒度; + - 平台不同位置的事件抽象后,尽可能提取出公用事件,然后通过事件变量进行区分,这样做的好处是:减少指标冗余、指标管理工作、培训成本,以及使用者的学习成本。 + - 当然这里也并不完全执着于抽象公用性,对于数据PM和开发来说,指标越精简越好,便于理解和管理,但对于运营同事来说,学习和使用成本高企,数据产生了但无法最大化应用侧价值,那就得不偿失,所以需要平衡。 + + +#### 3.2. 事件上报时机 + +- 事件埋点 + - 比如:下单成功; +- 页面埋点 + - 比如:完成页面跳转 +- 曝光埋点 + - 进入页面时,模块可见面积大于 50%,才上报; + - 手指不停的滑动,模块可见面积小于 50% 不上报,大于 50% 才上报; + - 滑动停止时,模块可见面积小于 50% 不上报,大于 50% 才上报; + +#### 3.3. 事件标识 & 参数命名规则 + +- 事件的命名规则: + - 同一类功能在不同页面或位置出现时,按照功能名称命名,页面和位置在ev参数中进行区分。 + - 仅是按钮点击时,按照按钮名称命名。 + +- 事件格式: + - 事件分为事件标识和事件参数 + - 事件标识 + - 作为埋点的唯一标识,用来区分埋点的位置和属性,不可变,不可修改。 + - 事件参数 + - 埋点需要回传的参数,ev参数顺序可变,可修改 + - 事件标识和事件参数之间用“#”连接(一级连接符) + - 事件参数和事件参数之间用“/”来连接(二级连接符) + - 参数使用 key=value 的结构,当一个 key 对应多个 value 值时,value1 与 value2 之间用“,”连接(三级连接符) + - 当埋点仅有事件标识、没有事件参数的时候,不需要带# + +- 备注: + - app埋点调整的时,事件标识不变,只修改后面的埋点参数(参数取值变化或者增加参数类型) + +#### 3.4. 通用埋点设计 + +- 在自定义埋点设计中,有一些通用的事件往往是比较复杂的,而且随着业务发展,会变得越来越复杂。 +- 比如:APP 平台的分享事件,如果按功能模块,每个功能模块都设计了自己的分享事件,则这个事件会越来越分散,如果想聚合做复合指标(如:通过分享/日活来衡量内容质量),分享事件要先聚合平台各功能模块的分享事件,太分散会产生应用上的问题。 +- 此时,建议仍然是将通用类型的埋点统一进行管理,通过变量字段进行拓展,来满足多功能模块的埋点需求,可以通过多个参数来进行区分。 + +- 通用埋点事件需求示例 + + ![]({{site.baseurl}}/img-post/数据埋点-9.png) + +#### 3.5. 数据指标地图 + +- 数据能力推广的第一个难点,是让让大家知道平台上有哪些数据。 +- 在各平台埋设的指标,采用excel的方式进行管理的问题是指标一多起来,找起来不太方便,对于定义者来说自然很容易找到,但是对于使用者来说则不太友好。 +- 即使搜中文名称,也会存在同一个地方,大家用不同的关键词去搜索,比如:模块、版块、板块。 +- 数据指标地图,将不同功能模块的数据指标进行了拆解和说明,其他同事找数据指标之前,先打开指标地图大概定位,然后再去对应的sheet表中寻找对应指标的细节定义和可下钻的维度信息。 + +#### 3.6. 指标字典 + +- 从数仓里自助取数时,会有以下的问题: + - 有哪些表; + - 表格对应的是哪块业务的数据; + - 有哪些字段; + - 字段的含义是什么; + - 等等 +- 这个需要明确具体内容,需要确认、并且约定好,在新增时及时更新对文档的解释。 + +#### 3.7. 版本迭代 + +- 随着版本迭代有新功能的埋点,或者针对之前功能的优化,所以需要对之前埋点进行调整。从埋点管理的角度,新增/修改的埋点,需要整合到之前的埋点系统里,这样能够方便使用者查阅整体的埋点明细。 +- 关于版本迭代中的埋点管理,相比于excel也有更好的工具化管理办法,如:做一个web端埋点管理平台,可以看到所有的埋点。同时,功能 PM 可以在该平台上按照字段要求提交自己的埋点需求,然后走审批流程,能够进入开发的埋点,会打上版本标记,待上线后,对应的埋点会出现在平台总表里,供使用者查看。 +- +#### 3.8. 埋点状态监控 + +- 主要负责对埋点的存活状态、未知埋点发现及数据异常进行提醒。 + +- 埋点的存活 + - 主要是针对已经确定的埋点进行监控,而是否存活主要是通过测试人员进行回归测试来判断。 +- 未知埋点发现 + - 主要是通过上报数据进行分析,如果出现了未知的埋点标识数据,则进行提醒,方便反向跟踪问题。 +- 数据异常提醒 + - 主要从数据本身的阈值异常以及上下游埋点比例的阈值进行监控。 \ No newline at end of file diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\272\344\272\216 AOP + ASM \345\256\236\347\216\260 Android \350\207\252\345\212\250\345\237\213\347\202\271.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\272\344\272\216 AOP + ASM \345\256\236\347\216\260 Android \350\207\252\345\212\250\345\237\213\347\202\271.md" new file mode 100644 index 00000000000..7fcd5c8701b --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\237\272\344\272\216 AOP + ASM \345\256\236\347\216\260 Android \350\207\252\345\212\250\345\237\213\347\202\271.md" @@ -0,0 +1,208 @@ +--- +layout: post +title: 数据埋点:基于 AOP + ASM 实现 Android 自动埋点 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + + +#### ASM 是什么 + +- ASM 是字节码操作库,支持对字节码进行编辑,实现类、属性和方法的增删改查; +- 字节码操作库还有 Javaassit 库可以选择,但 ASM 灵活度和效率都是最高的; +- ASM 可以直接产生二进制的 class 文件,也可以增强既有类的功能。 + - Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素,包括: + - 类名称 + - 方法 + - 属性 + - Java 字节码(指令)。 + +#### ASM 字节码插桩 + +- 利用操作 ASM 字节码实现方法计时,可以的做法是修改 class 文件,在目标方法开始和结束时插入本来需要手动埋点的代码,这个过程称之为字节码插桩。 + +- Gradle Transform + + ![]({{site.baseurl}}/img-post/数据埋点-8.png) + + - Gradle Transform 是 Android 官方提供给开发者在项目构建阶段即由 class 到 dex 转换期间修改 class 文件的一套 api。 + - 目前比较经典的应用是字节码插桩、代码注入技术。 + +- 上图所示是 Android 打包流程:.java 文件 -> .class 文件 -> .dex 文件; +- 只要在红圈处拦截住,拿到所有方法进行修改完再放生就可以了,而做到这一步也不难,Google 官方在Android Gradle 的 1.5.0 版本以后提供了 Transfrom API,允许第三方 Plugin 在打包 dex 文件之前的编译过程中操作 .class 文件; +- ASM 字节码插桩,要做的就是实现 Transform 进行 .class 文件遍历拿到所有方法,修改完成对原文件进行替换。 + +#### ASM 框架中的核心类 + +- ClassReader + - 该类用来解析编译过的class字节码文件。 + +- ClassWriter + - 该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。 + +- ClassViitor + - 主要负责 “拜访” 类成员信息,包括: + - 标记在类上的注解, + - 类的构造方法, + - 类的字段, + - 类的方法, + - 静态代码块。 + +- AdviceAdapter + - 实现了 MethodVisitor 接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。 + + +#### ASM 插桩实现方案 + +- 方案简介 + - class 文件是按照 JVM 规范格式存储的二进制文件,本质上是一个表,记录了类的常量池、访问标志、属性和方法等。 + - ASM 库不仅能够对 class 文件进行解读,还提供了方便的 API 进行字节码的修改,支持直接产生二进制 class 文件。 + - ASM 提供了基于事件的 API: + - ClassReader 用于读取 class 文件的二进制流; + - ClassVisitor 以事件的形式输出 class 的结构信息; + - ClassWriter 则用于把修改后的字节码生成二进制流。 + +- 示例:监听点击事件 + +- 步骤一:通过 visitMethod 拿到方法名,判断这个方法是不是要监控的方法 + + - ASM 读取分析 class 文件 + + - class 文件在工程的 `app/build/intermediates/classes` 目录下 + ``` + public static void main(String[] args) { + try { + File classFile = new File("./source/MainActivity.class"); + File dir = new File("."); + transformClassFile(dir, classFile) + } catch (Exception e){} + } + private static File transformClassFile(File dir, File sourceFile){ + String className = sourceFile.getName(); + // 得到class文件二进制流 + FileInputStream fileInputStream = new FileInputStream(sourceFile); + byte[] sourceClassBytes = IOUtils.toByteArray(fileInputStream); + // 定义classWriter,用于输出修改后的二进制流 + ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); + // 自定义ClassVisitor, 负责字节码的消费 + MyClassVisitor myClassVisitor = new MyClassVisitor(classWriter); + // ClassReader负责字节码的读取 + ClassReader classReader = new ClassReader(sourceClassBytes); + // 开始字节码处理 + classReader.accept(myClassVisitor, 0); + // 生成二进制流并保存成新的文件 + byte[] destByte = classWriter.toByteArray(); + File modified = new File(dir, className) + if (modified.exists()) { + modified.delete() + } + modified.createNewFile(); + new FileOutputStream(modified).write(destByte) + return modified; + } + + private static class MyClassVisitor extends ClassVisitor { + + public MyClassVisitor(ClassVisitor classVisitor) { + super(Opcodes.ASM6, classVisitor); + } + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + System.out.println("visit: access: " + access + " ,name: " + name + " , superName: " + superName + " ,singature: " + signature + ", interfaces: " + interfaces.join("/")); + super.visit(version, access, name, signature, superName, interfaces); + } + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + System.out.println("visitMethod: access: " + access + " ,name: " + name + " , desc: " + descriptor + " ,singature: " + signature); + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + MethodVisitor myMv = new MethodVisitor(Opcodes.ASM6, mv) { + @Override + AnnotationVisitor visitAnnotation(String desc, boolean visible) { + System.out.println("visitAnnotation: desc: " + desc); + return super.visitAnnotation(desc, visible) + } + @Override + void visitCode() { + super.visitCode() + } + } + return myMv; + } + } + ``` + + - 用 ClassReader 读取 class 文件,并用自定义的 ClassVisitor 接收事件,查看输出: + ``` + visit: access: 33 ,name: com/example/wangkai/MainActivity , superName: android/support/v7/app/AppCompatActivity ,singature: null, interfaces: + visitMethod: access: 1 ,name: , desc: ()V ,singature: null + visitMethod: access: 4 ,name: onCreate , desc: (Landroid/os/Bundle;)V ,singature: null + visitMethod: access: 1 ,name: fun , desc: ()V ,singature: null + visitAnnotation: desc: Lcom/example/wangkai/annotation/TraceTime; + ``` + - 这里通过 visit 回调可以读取到 class 的名字、父类名和接口,这样就可以判断出一个类是否是我们要插桩的白名单页面,是不是 Activity 子类以及是否实现了点击事件接口 View$onClickListener(实现对点击事件的监控) + - 通过在 visitMethod 方法里返回自定义的 MethodVisitor 对象,拿到方法上的注解,从而可以知道这个方法是否是要插桩的方法 + - 上面的输出中,visitCode 表示方法开始执行,如果能在这里插入代码,那代码就能在原始代码执行前执行。 + - 我们已经找到了切入点,下一步就是插入代码了。 + +- 步骤二:插入埋点代码 + + - 插入代码要难一些,因为我们是在字节码层面操作,插入的也只能是字节码,这就需要对字节码有一定了解。 + - 包括局部变量表和操作数栈的概念,常见指令(ALOAD, INVOKEVIRTUAL等)的含义。 + - 埋点时,我们需要插入这样的代码: + + ``` + private static class MyClickListener implements View.OnClickListener{ + @Override + public void onClick(View v) { + ClickAgent.click(v); //待插入代码,方法里获取view的ID和当前时间,实现对点击事件的记录 + Log.d(TAG, "onClick: "); + } + } + ``` + - 上面这步操作的是,通过 ASM methodVisitor 提供的 API,把 ClickAgent.click(v) 的字节码,注入到原始 onClick 方法里。 + + - 查看字节码: + ``` + L0 + LINENUMBER 27 L0 + ALOAD 1 + INVOKESTATIC com/example/wangkai/ClickAgent.click (Landroid/view/View;)V + L1 + LINENUMBER 28 L1 + LDC "MainActivity" + LDC "onClick: " + ``` + + - INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I + 可以看到ClickAgent.click(v)对应的字节码是两行; + - ALOAD 1 表示把局部变量表里索引为 1 的值,推到操作数栈上,也就是参数值 View v。 + - 对应到 ASM,是 methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); + - INVOKESTATIC com/example/wangkai/ClickAgent.click (Landroid/view/View;)V就是调用 ClickAgent 的静态方法 click。 + - 对应到 ASM,是 methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/wangkai/ClickAgent", "click", "(Landroid/view/View;)V", false) + - 当我们在 visitorMethod 回调里判断 name、desc 和 signature 和原始方法一致,并且该类实现的 interfaces 包含了 View$onClickListener 时,就可以注入了。 + - 具体的注入代码如下: + ``` + @Override + void visitCode() { + super.visitCode() + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/wangkai/ClickAgent", "click", "(Landroid/view/View;)V", false) + } + ``` + + - 修改后执行,会生成插桩后的 class 文件,可以用 JD_GUI 查看插桩后的效果。 + - 实际编码中,我们可以借助于 Bydecode Outline 插件。 + +- 以上简单介绍了无埋点插桩实现的过程。 + - 实际的插件工程要复杂,需要考虑: + - 黑白名单处理; + - Manifest文件读取; + - 插桩的统一处理等。 + - 另外,考虑到实现的复杂度和对性能的消耗,无埋点并不能完全代替手工埋点; + - 部分埋点信息仍然需要手工补全。 diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\243\260\346\230\216\345\274\217\345\237\213\347\202\271\357\274\210\346\227\240\347\227\225\345\237\213\347\202\271\357\274\211.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\243\260\346\230\216\345\274\217\345\237\213\347\202\271\357\274\210\346\227\240\347\227\225\345\237\213\347\202\271\357\274\211.md" new file mode 100644 index 00000000000..836745be80b --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\345\243\260\346\230\216\345\274\217\345\237\213\347\202\271\357\274\210\346\227\240\347\227\225\345\237\213\347\202\271\357\274\211.md" @@ -0,0 +1,138 @@ +--- +layout: post +title: 数据埋点:声明式埋点(无痕埋点) +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + +#### 声明式埋点 + +- 预先声明埋点类型(提前封装好自定义的指令),在 DOM 或者组件上,通过设置即可以完成事件的统一上报; + - 声明式埋点是基于 vue 框架的自定义指令,可以一定程度的减小代码冗余解决代码耦合的问题。 + - vue 中提供了自定义指令,在模版元素中插入自定义指令,可以跟踪元素的结构变化、跟踪数据变化,比 DOM 的 data 属性,从而更加方便抓取跟踪; + +- 理论上,声明式埋点只需要关注两个问题: + - 需要埋点的 DOM 节点; + - 所需携带的数据; + +- 声明式埋点标准写法: + ```aidl + // key表示埋点的唯一标识;act表示埋点方式 + + ``` + +- 埋点触发及数据回传: + - 声明埋点后、遍历 DOM 树,找到 [data-stat] 节点,给这个 button 绑上 click 事件; + - 当事件被触发后,把参数 111 在回调函数中通过请求发出去。 + + +#### 声明式埋点优缺点 + +- 声明式埋点优点: + - 埋点和业务代码实现了解耦; + >声明埋点是在 DOM 节点(html)上,而业务逻辑通常在 Javascript 文件中; + - 提高代码可维护和可阅读性; + - VUE 比较流行的埋点方式; + +- 声明式埋点缺点: + - 需要手动在需要埋点的节点中添加指令。 + +#### 声明式埋点需要解决的问题 + +- 遍历 DOM 树的时机 + - 例如:一个表格的行数据是通过异步加载,而表格行中的操作按钮需要埋点,那么在 DOM ready 的时候去遍历,显然是无法找到的; +- 绑定埋点事件次数的问题 + - 怎样保证埋点事件不会被重复绑定到元素上,一次操作发了N个埋点请求? +- 如何处理特有的埋点行为, + - 例如:页面展现埋点,区域展现埋点? +- 如何在解绑时,销毁已绑定的事件? + +#### 声明式埋点方案核心内容 + +- 通过声明式埋点来解耦业务代码 +- 埋点方案需要兼容 Vue 应用和 jquery 应用(甚至所有应用) +- 需要支持页面展现埋点、区域展现埋点、点击埋点等多种埋点方式 +- 极端情况下需要支持命令式埋点 + +#### 钩子函数 + +- 按照官方文档,一个指令定义对象提供如下几个钩子函数: + + - bind + - 只调用一次,指令第一次绑定到元素时调用,在这里可以进行一次性的初始化设置。 + + - inserted + - 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 + + - update + - 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。 + - 指令的值可能发生了改变,也可能没有。 + - 但是可以通过比较更新前后的值来忽略不必要的模板更新。 + + - componentUpdated + - 指令所在组件的 VNode 及其子 VNode 全部更新后调用。 + + - unbind + - 只调用一次,指令与元素解绑时调用。 + + - 其中 bind 和 update 是比较重要的两个钩子函数, + - 它能跟踪 DOM 的出现和变化,获取数据并数据上报埋点; + +- 指令钩子函数会被传入以下参数: + + - el + - 指令所绑定的元素,可以用来直接操作 DOM。 + - binding + - 一个对象,包含指令的指令名、绑定值、表达式等。 + +#### 具体实现 + +- 页面曝光 + - 对于 vue 的单页应用来说,页面的切换跟踪就是路由的变化; + - 但是在统一的路由钩子、放置每个页面不同的数据不太现实,于是就考虑在每个 view 页面根部埋下页面曝光数据,只要用户一访问该页面,就触发页面曝光; + +- 坑位曝光 + - 坑位的曝光实际就是一个 DOM 出现在用户视野范围内进行抓取; + - 需要借助全局滚动(scroll)事件,使用节流的方式检测目标坑位是否在可视范围内,在的话就触发坑位曝光 + +- 坑位点击 + - 坑位的点击需要对目标坑位进行点击事件绑定; + - 在不影响目标坑位原有事件基础上,绑定点击的埋点上报,在元素unbind的时候解除对应点击事件 + +#### 页面曝光(v-page-exposure) + +- v-page-exposure + ``` +
    + ``` + - a.b.c.d 分别为:`应用代码.页面代码.模块代码.坑位代码`,就是埋点统计需要的代码信息,把信息收集上报即可。 + - 需要注意的是指令信息是一个 js 表达式,如果不加引号会默认执行 js 变成一个对象取值报错。 + +#### 坑位曝光(v-exposure) + +- v-exposure + ```aidl +
    + 复制代码 + ``` + - 坑位曝光收集的是一个 js 对象,包含一些坑位的代码信息、位置信息、商品信息、跳转信息等。 + - 坑位曝光采用实时上报,且曝光一次就不再曝光, + - 所以每次 bind 的时候、把所有曝光坑位收集在一个数组,坑位出现在用户可视范围内一次就上报弹出数组。 + +#### 坑位点击(v-bury-click) + +- v-bury-click + ```aidl +
    + 复制代码 + ``` + - 坑位点击也是一个 js 对象,包含坑位的各种信息,在页面加载的时候绑定点击事件,用户点击的时候上报埋点。 + - 在实际使用过程中,埋点需要跟踪最新的数据信息,比如: + - 在加入购物车的环节中,加入购物车的按钮是同一个不变,但下一次选择商品的信息可能发生改变,所以每次点击按钮需要传递最新的埋点信息。 + - 由于指令信息是一个 js 表达式,对象是一个引用类型,改变对象的值不会触发 update 钩子函数,所以需要 JSON 字符串化一下。 \ No newline at end of file diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\346\226\271\345\274\217\344\270\216\347\255\226\347\225\245.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\346\226\271\345\274\217\344\270\216\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..b5386dedca9 --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\346\226\271\345\274\217\344\270\216\347\255\226\347\225\245.md" @@ -0,0 +1,159 @@ +--- +layout: post +title: 数据埋点:数据埋点方式与策略 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + + +#### 埋点方式 + +- 展现埋点 + - 通俗来说,某个页面里的内容被展现了; + - 怎么定义展现,其实就是一个服务端的触发; + - 服务端如果触发了,用户侧就会展现内容; + - 展现埋点需要记录的,是页面展现的内容信息。 + - 也就是说,服务端下发的内容都包含什么; + - 这些东西一定是当前页面主要内容,不包含一些交互信息。 +- 曝光埋点 + - 比如:广告(用于计算曝光数) + - 有效曝光 / 逻辑可见: + - 停留时间大于 2 秒; + - 曝光逻辑: + - 一个模块只曝光一次、且上报一次(如果已经上报,就不做二次上报); + - 标题曝光,即视为模块模块曝光; +- 交互埋点 + - 比如: + - 用户点击登录按钮; + - 用户给视频点赞、播放、暂停; + - 只要用户“消费”了可交互内容,都可以记录埋点。 + +#### 前端埋点 & 后端埋点 + +- 前端埋点为主,后端埋点为辅; +- 后端数据作为标准,前端数据作为参考; +- 很多时候,需要相互配合; + - 比如: + - 判定用户登录是否成功,需要后端判定登录状态,结合前端的登录行为才能判定; + - 获取用户的支付信息,前端只展示标出来的价格,但实际支付的时候可能会有优惠,需要结合后端的支付数据才能确定实际价格; + +#### 前、后端埋点的区别 + +- 在实际过程中,有些埋点是不用特意区分前后端的,用户的一个埋点事件在前端埋点或后端埋点都可以实现; +- 但是需要注意的是,在实际埋点上报、数据收集等过程中会有数据丢失的情况,从这个角度来看的话,其实后端埋点要比前端埋点更有优势; +- 前端埋点会因为一些网络问题、适配问题等等容易出现上报异常造成数据丢失且丢失后排查困难,因为前端相关的是没有记录相关操作的,只负责上报,上报成功与否没有记录 +- 而如果是后端埋点,无论是自己的数据系统、还是第三方数据系统,都是可以通过自己系统本身相关的数据库查询、或记录日志等操作进行埋点数据的校验排查; +- 所以针对一些比较重要的埋点,还是建议以后端埋点为主,必要时通过记录日志或记入数据库等方式对相关数据进行二次记录以便进行数据核实。 + +#### 代码埋点 + +- 指令式埋点: + - 命令式埋点需要开发人员在需要埋点的 dom 节点处手动加入代码。 + - 用户在埋点位置触发事件,由预先定义的函数上报事件相关数据。 + - 示例:点击事件埋点 + - 如页面按钮的点击,页面的跳转时都会加入回调函数,以这种方式埋点采集数据。 + ```aidl + // 页面加载时发送埋点请求 + $(document).ready(function(){ + // ... 这里存在一些业务逻辑 + sendRequest(params); + }); + // 按钮点击时发送埋点请求 + $('button').click(function(){ + // ... 这里存在一些业务逻辑 + sendRequest(params); + }); + ``` + - 指令式埋点缺点: + - 侵入业务代码 + - 埋点代码侵入业务代码,这使整体业务代码变得繁琐; + - 业务耦合度高 + - 每增加需求都要做版本更新迭代,代码开发的工作量比较大; + - 开发成本较高; + - 代码冗余 + - 在业务逻辑中插入埋点代码会导致代码冗余; + - 后期维护升级困难 + - 项目越到后期代码就难优化冗余度就越高,不利于项目后期维护和升级; + +- 声明式埋点: + - 预先声明埋点类型(提前封装好自定义的指令),在 DOM 或者组件上,通过设置即可以完成事件的统一上报; + - 声明式埋点是基于 vue 框架的自定义指令,可以一定程度的减小代码冗余解决代码耦合的问题。 + - 示例:购买事件埋点 + ```aidl + + + // 自定义指令 + directives:{ + focus:{ + bind:function(el,binding,vnode){ + el.addEventListener('click',()=>{sendRequest(binding.value)}) } + } + } + ``` + + - type + - 事件类型,这个事件类型、指当前元素添加的事件 click; + - key: + - 埋点标识,这里代表埋点标识位置为购买 shop; + - 埋点实现过程 + - 用户点击购买按钮时,前端页面会将当前的事件类型(click)和埋点标识(shop)传到服务端。 + + - 声明式埋点优点: + - 埋点和业务代码实现了解耦,提高代码可维护和可阅读性; + - 提高代码可维护和可阅读性; + - VUE 比较流行的埋点方式; + + - 声明式埋点缺点: + - 需要手动在需要埋点的节点中添加指令。 + +#### 全埋点(无埋点) + +- 全部免采集用户的行为; +- 如国内的神策数据等; +- 优点: + - 数据全部采集,不会有遗漏; + - 全生命周期的采集和分析; +- 缺点: + - 后端日志服务器亚历山大; + - 占用网络资源,对用户浏览体验也会有影响; + +#### 可视化埋点 + +- 通过可视化的自定义埋点; +- 如开源的 Mixpanel; +- 优点: + - 操作简单; + - 支持自定义属性; + - 适用于非技术人员进行埋点工作; +- 缺点: + - 覆盖的功能有限,不支持后端埋点; + - 可视化埋点并不能准确的加入埋点,只能通过可见的按钮添加埋点,分析较难、针对性埋点较弱。 + - 开发难度大,需要在 web 短提前解析 DOM,并将解析后的 DOM 进行可视化,并能实现准确的自定义配置,对开发要求比较高。 + +#### 第三方 SDK 埋点 + +- 神策 +- 诸葛 +- Growing IO +- 只需要集成 SDK 就可以实现埋点; +- 基本都提供私有化部署,不用担心数据安全问题。 + + ![]({{site.baseurl}}/img-post/数据埋点-2.png) + +- 第三方工具存在的问题 + - 非实时反馈 + - 数据波动难以定位 + - 与第三方沟通成本太高 + - 没有自己的核心数据存储 + - 电商时间埋点方案示例 + + ![]({{site.baseurl}}/img-post/指标体系方法论-5.png) + + + diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\346\240\241\351\252\214&\346\265\213\350\257\225.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\346\240\241\351\252\214&\346\265\213\350\257\225.md" new file mode 100644 index 00000000000..021dbc4e9ee --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\346\240\241\351\252\214&\346\265\213\350\257\225.md" @@ -0,0 +1,117 @@ +--- +layout: post +title: 数据埋点:数据埋点校验&测试 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + +# 埋点测试 + +- 开发完了以后、在测试工具上去做数据的验证,检查数据的完整性、准确性、及时性。 + +#### 埋点测试内容 + +- 模块是否做了埋点处理 +- 埋点是否被正常触发 +- 埋点是否回传成功 +- 触发时机是否正确 +- 报送内容(埋点模块回传的参数)是否准确、完整 + - 验收是否多报 + - 验收是否少报 + - 验收是否缺参数上报 + - 验收上报参数是否符合预期 + - 空日志 + - 验收上报为空日志的比例 + - 异常上报 + - 验收上报不符合预期日志的比例 +- 数据落表是否准确、完整 + +#### 测试中常见的问题 + +- 是否使用了最新版本的 SDK(有些功能所需数据只有新版本SDK才会上报) +- AppKey 是否填写正确 +- 上报地址是否配置正确,尤其是小程序数据的上报 +- 数据是否能正常发送 +- 是否选择了合适的 SDK,比如有些事件更适合在服务端上报 +- 是否覆盖了所有涉及到的页面 +- 是否覆盖了位于不同页面的相同事件 +- 是否因为页面跳转导致有些事件不能完全上报 + + +#### 埋点测试关注点 + +- 埋点测试的过程有两个比较重要的环节,埋点上报和埋点落库。 +- 埋点上报: + - 无论是前端埋点还是后端埋点,是否正常按照相关规则进行上报,相关的事件名、属性值都是否完整正确上报。 +- 埋点落库: + - 埋点上报完的数据,需要存储到数据库当中,再进行相关的数据统计、分析、归类等等,除了检查埋点上报,还要看最终数据是否正常落库,相关数据字段是否正常。 + +# 测试流程 + +![]({{site.baseurl}}/img-post/埋点测试-1.jpg) + +# 测试方法 + +#### 埋点校验方式 + +- 前端校验 + - debugger 断点 +- 后端校验 + - 数据库验证 + +#### 测试时间点 + +- 开发中测试 +- 上线前测试 +- 上线后测试 + +#### 测试方法 + +- 手工测试 +- 自动化测试 + +# 手工测试 + +#### 手工测试方法 + +- 最原始的就是去做人肉的 review,在开发工位旁边去看一下这个代码,开发触发、测试看数据。 +- 另外有 Kibana 之类的查询工具,可以协助快速的做埋点日志的查询,提供简单的可视化展示。 + +#### 手工抓包测试缺点 + +- 如果 APP 埋点加密,无法直接在抓包工具里查看; +- 阻碍开发人员自测; +- 大量埋点上报时,人工肉眼查看成本高,漏测率较高; +- 手工记录结果,费时费力; +- 老埋点数据问题遗留 + - 当老埋点数据在应用程序升级过程中被修改出问题时,由于测试人员精力有限且主要集中在新埋点的测试中,导致老埋点数据问题遗留到线上环境; + +#### 测试工具 + +- fiddler +- Android Studio +- Charles + +# 自动化测试 + +#### 自动化测试方法 + +- 用户应用层框架 + - 移动端 Appium,web 端 selenium,主要是模拟用户正常的业务操作; +- 数据 mock & 上报数据收集 + - 通过构造测试数据给到用户应用层使用,并且通过代理抓包收集上报数据,进行上报数据校验(jsonschema 校验); +- 服务端上报及落库查询 + - 通过链接数据数据库或使用相关 API,查询测试上报数据是否落库; +- Jenkins CI/CD + - 结合 Jenkins 进行持续集成,每天或每次发版前对所有埋点进行回归测试。 + +#### UI自动化测试埋点缺点: + +- 应用程序迭代较快,UI控件布局变化频繁,导致UI自动化用例稳定性差、失败率高,维护成本高; +- 学习编写UI自动化用例门槛较高,不利于广泛推广使用; + diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\346\265\213\350\257\225\345\271\263\345\217\260.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\346\265\213\350\257\225\345\271\263\345\217\260.md" new file mode 100644 index 00000000000..d8ddb1678ec --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\346\265\213\350\257\225\345\271\263\345\217\260.md" @@ -0,0 +1,102 @@ +--- +layout: post +title: 数据埋点:数据埋点测试平台 +subtitle: 基于抓包工具实现 +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + +# 测试平台功能 + +![]({{site.baseurl}}/img-post/数据埋点-7.png) + +#### 代理支持 + +- 通过对 Fiddler 开发自定义插件、Whistle 进行二次开发,能够无缝衔接抓包工具(Fiddler、Whistle等),无侵入的收集应用程序的埋点数据, +- 可以有效减少获取埋点数据的步骤,节约测试时间; + +#### 非代理环境上报 + +- 无需连接代理,便可上报埋点到测试平台,且能够实时查看埋点数据; +- 解决产品人员、开发人员等、因没有安装代理环境或不会使用代理工具等因素,而无法验收、自测埋点的问题。 + +#### 漏报性检测 + +- 自动检测埋点是否漏报; + +#### 规则自动对比 + +- 根据埋点数据的需求文档生成动态的规则库,自动对应用程序上报的埋点数据进行实时、快速的一致性检测,降低了验证埋点的成本,提高了埋点测试的准确性; + +#### 历史埋点数据存储 + +- 进行持久化存储,方便追溯历史数据; + +#### 埋点方案规范性检测 + +- 活动上报的埋点数据必须满足一定的规范,才能保证埋点提数的正确性,以及活动运营数据评估的准确性。 +- 在埋点测试之前,对埋点方案进行规范化检查,能够尽早揭露问题,实现了测试左移; + +# 测试平台设计 + +#### 测试平台设计原则 + +- 无痕获取埋点数据; +- 自动和埋点方案对比测试,失败时高亮提示; +- 记录埋点测试过程数据、一键生成报告; +- 丰富多样的规则库,能灵活配置,维护成本低; +- 图表功能直观展示,可查看实时、历史埋点数据; + +#### 测试流程 + +- 抓包工具(Fiddler、Whistle等)包含一个自定义插件,当测试人员连接抓包工具并触发应用程序时,会无感知的收集埋点数据并上报到服务器。 +- 服务器包含一个动态规则库:通用规则和自定义规则,根据动态规则库自动检测埋点数据是否正确,并生成测试报告。 + + +# 抓包工具(自定义插件) + +#### 抓包工具(自定义插件)获取埋点数据 + +- 抓包工具(Fiddler、Whistle等)安装自定义插件,无感知的获取应用程序的埋点数据; + +#### 自定义插件的具体实现方法 + +- 监听抓包工具传输的所有网络数据包的域名(HostName); +- 当网络数据包的域名(HostName)和应用程序的埋点上报的域名一致时,拷贝网络数据包的请求内容(RequestBody),并发送到服务器; + +#### 自定义插件优势 + +- 自定义插件只需开发实现一份、可重复利用; +- 测试人员不需要进行额外的操作; +- 学习成本低,有利于推广使用; +- 自定义插件自动监听埋点数据并发送到服务器,不需要对被测应用进行二次改造,达到了无侵入、无感知的收集埋点数据的目的。 + +# 服务器(动态规则库) + +#### 服务器(动态规则库)自动检测埋点数据 + +- 服务器收到埋点数据后,根据动态规则库对埋点数据进行自动检测,并生成测试报告。 + - 动态规则库包括通用规则和自定义规则。 + +#### 通用规则的实现方法 + +- 固有属性抽象通用规则 + - 将需求文档中埋点数据的固有属性抽象成通用规则; +- 例如: + - 埋点数据的类型(点击、PV、曝光); + - 埋点数据的字段是否为空; + - 埋点数据字段的拼接个数等。 +- 因通用规则具有普遍性,适用性广,所以只需生成一份,可重复使用,实现成本比较低。 + +#### 自定义规则的实现方法 + +- 正则表达式 + - 因触发应用程序的场景不同,导致实际上报的埋点数据存在差异性时,需对需求文档中的埋点数据的字段采用正则表达式生成自定义规则。 +- 自定义规则是针对某个具体埋点数据实现的,满足了个性化的需求。 +- 当埋点数据量比较庞大时,优先生成并使用通用规则,只有埋点数据的格式存在不确定性时,才利用正则表达式生成自定义规则。 + - 所以动态规则库既节约了成本,同时具有灵活性、多样性的特点。 diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\344\272\213\344\273\266\346\250\241\345\236\213.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\344\272\213\344\273\266\346\250\241\345\236\213.md" new file mode 100644 index 00000000000..465986330ae --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\344\272\213\344\273\266\346\250\241\345\236\213.md" @@ -0,0 +1,80 @@ +--- +layout: post +title: 数据埋点:数据埋点的事件模型 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + +# 事件模型 + +- 一个事件的触发,有4个因素:触发者、触发位置、触发的事件、触发的时间。 + + ![]({{site.baseurl}}/img-post/数据埋点-6.png) + + +#### 触发者 + +- 触发者即触发事件的用户; +- 需要一个唯一标识,来识别不同的用户; +- 同时,需要识别在不同平台的同一个用户。 + +#### 触发位置 + +- 为了识别一个网页里面,事件触发的位置,需要一个页面的唯一标识、和控件的唯一标识; +- 页面的唯一标识 + - 一般通过 url 标记,但要处理好url后面的参数; +- 控件的唯一标识 + - 一般通过元素在整个文档中的xpath路径来标记。xpath是能唯一标记控件在网页的唯一位置的一种标记方法。 + +#### 触发的事件 + +- 事件类型有 + - 浏览 + - 点击 + - 搜索 + - 曝光 + - 分析 + - 进入 + - 退出 + - 返回 + - 悬浮 + - 下拉 + - 滚动 + - 长按 + - 右键 + - 等等 +- 最常用的还是浏览和点击。 + +#### 触发时间 + +- 事件触发的时间一般取的是客户端时间,也就是用户的本地时间; + - 如果用户的设备是移动端,取的就是手机时间,如果是电脑,取的就是电脑的时间。 +- 但是客户端的时间不太准确,因为用户可以去更改设备时间,我们需要一个机制去校准客户端时间。 + - 一般的做法是,在上报事件时,我们会上报事件触发时间 t1 和数据发送时间 t2,服务端也会拿到一个接收数据的时间 t3; + - 如果 t3-t2>60s,则认为客户端时间不准,要对客户端时间进行修正,修正后的客户端时间是:t1+(t3-t2); + - 之所以 t3-t2>60s 会认为不准,因为数据发送到接收的时间,一般不会超过60s。 + +# 事件分类 + +- 根据所要埋的事件类型进行分类很有必要,一方面方便对需求进行优先级确认,另一方面设计埋点时,不同类型的事件需要对应各自的方法。 + +#### 核心事件 + +- 产品的核心流程及用户核心行为的分支事件,根据具体业务需求进行专门处理和参数设置。 + +#### 通用事件 + +- 对于通用的、泛化性的、活动等次要流程事件,可以进行抽象化处理。 +- 比如:在日常工作中,经常遇到市场或活动运营同事提出某次活动的埋点需求,如果每次活动都单独处理成一个事件,随着时间的推移将会导致同类事件越积越多,不利于管理,因此对于这类相关的事件,需要进行抽象化的通用事件处理。 + +#### 边缘事件 + +- 所谓边缘事件,指的是零散的只查看点击或浏览行为的事件,比如 APP 端诸如设置、客服入口等功能按钮。 +处理此类事件,有两种常见方法: + - 一种是做一个最基本的自定义事件容器,然后把相关按钮名称、所在页面及其他零散东西抽象化后放进去。 + - 另一种是手动纠正一些全埋点进行上报。之所以要手动纠正,是因为前后端的技术架构不同,没有办法 100% 地适应全埋点,当全埋点数据出现未知或无法采集时,需要手动调 SDK,纠正所要采集的页面。 \ No newline at end of file diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\345\272\224\347\224\250\345\234\272\346\231\257.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\345\272\224\347\224\250\345\234\272\346\231\257.md" new file mode 100644 index 00000000000..1732ad6eb23 --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\345\272\224\347\224\250\345\234\272\346\231\257.md" @@ -0,0 +1,81 @@ +--- +layout: post +title: 数据埋点:数据埋点的应用场景 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + +#### 1. 抽象场景 + + - 需要定期汇报考核 KPI + - 需要周期性分析 KPI 增长情况 + - 需要对业务数据进行深度挖掘 + - 需要进行归因分析 + - 需要优化产品和运行策略 + +#### 2. 业务指标分析 + + - 流量监控 + - 各功能模块使用情况分析 + - 活动效果评估 + - 用户行为日志 + - 漏斗分析 + - 路径分析 + - 留存分析 + - 活跃分析 + - 互动分析 + - 广告投放 + - 波动警报 + +#### 3. 性能指标分析 + +- 监控 App\WEB 性能 + - 比如: + - crash率 + - 指系统指定版本异常退出的次数、在该版本中所有启动次数比例; + - 次均TCP建连时间 + - 代表网络请求中TCP建立连接的平均耗时,单位毫秒; + - 次均首字节到达时间 + - 代表网络请求中首字节到达的平均耗时,单位毫秒; + - 次均请求资源大小 + - 代表网络请求完成的平均消耗资源,单位B; + - 异常数量 + - 代表发生异常的网络请求数量。 + +#### 4. 数据产品 + +- 用户画像 / 用户分群 +- A/B 测试 +- 算法推荐(千人千面) +- BI + +#### 5. 归因模型 + +- 销售归因 + - 在电商领域可以根据埋点日志进行销售归因。我们引入电商坑位归因的概念,把每一笔的成交都归给转化路径的不同的坑位。据坑位的曝光转化价值来评价坑位质量。 + - 把宝贵的流量尽可能都引导到转化率更高的坑位,以此达到精细化运营的效果。有了这个坑位价值评判的机制后,各个坑位的改版也能准确的评估,真正做到了数据驱动增长。 +- 流量归因 + - 广告效果评估 +- 退货归因 + +#### 6. OSM 模型 + +- Object + - KPI,业务目标; + - 如: + - 拉新 + - 提升留存 + - 提高转化 + - 提升客单价 + - 降低客诉 +- Strategy + - 业务策略 +- Measurement + - 指标 + diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\346\212\200\346\234\257\346\226\271\346\241\210.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\346\212\200\346\234\257\346\226\271\346\241\210.md" new file mode 100644 index 00000000000..8eae778128e --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\346\212\200\346\234\257\346\226\271\346\241\210.md" @@ -0,0 +1,89 @@ +--- +layout: post +title: 数据埋点:数据埋点的技术方案 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + +#### 开发思路 + +- 小公司: + - 开源 SDK + 通用存储 + 通用 BI +- 大公司: + - 开源 SDK + OLAP 引擎 + 自研可视化 + +#### 埋点系统搭建工作流程 + +- 梳理埋点需求(分析指标体系) +- 制定埋点规则 + - 命名的规范化 + - 埋点流程统一化 +- 制定埋点数据的清洗规则 + - 数据清洗规范化 +- 维护埋点的测试、上线、下线 +- 维护埋点元数据(埋点管理系统) + +#### 数据埋点上下游技术 + +- 数据采集 + + - 对行为数据采集部分我们一般封装为埋点采集的 SDK,然后嵌入到不同的客户端中。 + - SDK 的作用在于封装好一些公共的参数信息、上报方式以及帮助各端开发简化数据上报的过程。 + - 同时也在基础层面上支持类似可视化埋点、圈选埋点、全埋点等不同的埋点方式。 + +- 数据存储(含计算引擎) + + - 在行为分析领域里面,现在最热门的是使用 Clickhouse 做存储及计算引擎,每日千亿级别的数据增量可以比较高效从容的进行行为数据分析。 + - 对于数据并没有那么大的公司也可以根据自身情况选择诸如 Durid、Impala等计算引擎 + +- 数据可视化 + + - 分析的结果在大部分团队内部都希望能以可直观查看的方式进行展示,包括一些简单的图表、数据看板等。 + - 如果选择完全自研的话成本比较高,一般的处理方式有两种,一种采用开源的图表组件进行二次开发,例如使用范围比较广的 Echarts、AntV 等进行定制化的图表开发。 + - 另外也可以直接在上游的分计算引擎上直接接入传统意义上的 BI 可视化产品进行最终数据结果的展示。 + +#### 数据埋点技术方案选择 + +![]({{site.baseurl}}/img-post/数据埋点-4.png) + +- 采集 + - 自主开发采集 + - Nginx + - Logstash + - API + - 第三方 SDK + - 神策 SDK(鬼策 SDK) + - 易观 SDK + - 诸葛 SDK + - Mixpanel + - 阿里开源 SDK +- 存储 + - 通用存储 + - TiDB + - Hive + - ES + - OLAP(性能更佳) + - Clickhouse(今日头条、B站) + - 查询并发比较小,只支持几百并发查询 + - Impala(神策在用的引擎) + - Druid + - Kylin + - Presto +- 可视化 + - 通用 BI产品 + - PowerBI + - Tableau + - Metabase + - Superset + - 自研可视化 + - 百度 Echarts + +#### 埋点项目开发流程示例 + +![]({{site.baseurl}}/img-post/指标体系方法论-4.png) diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\347\224\250\346\210\267\346\250\241\345\236\213.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\347\224\250\346\210\267\346\250\241\345\236\213.md" new file mode 100644 index 00000000000..cf16b4b2437 --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\346\225\260\346\215\256\345\237\213\347\202\271\347\232\204\347\224\250\346\210\267\346\250\241\345\236\213.md" @@ -0,0 +1,39 @@ +--- +layout: post +title: 数据埋点:数据埋点的用户模型 +subtitle: +date: 2022-01-24 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + +#### 用户模型 + +- 每个用户都需要一个唯一标识。 +- 选择一个合适的用户标识,对于用户行为分析的准确性有很大的影响。 +- 这里说的唯一标识、是发生事件行为用户id在数据中台的标记,不是业务中的登录id。 + +#### 小程序用户 + +- 如果是小程序用户,可直接使用用户的 openid,需要上报用户 openid 和 unionid。 +- 如果同一个微信开放平台账号下,有多个小程序/公众号,就可以通过 unionid 来打通用户体系。 + +#### H5用户 + +- 如果是 h5 用户,sdk 会创建一个 uuid 来唯一标记用户; +- uuid 根据用户的浏览器类型、屏幕宽高、分辨率等特性生成。 + +#### APP用户 + +- 优先使用设备id来唯一标记用户; +- 但如果取不到设备id,则使用一个 sdk 创建一个随机的 uuid 来标记。 + +#### 游客 + +- 如果是游客,并且游客后面登录了系统,该如何把游客和已登录状态的用户id的行为数据关联起来? +- 如果用户在未登录状态下触发了事件,那么 sdk 会创建一个 uuid 来唯一标记这个用户,uuid 不仅会随着行为数据上报,也会存在本地存储中; +- 如果以后用户注册登录了,就可以拿这个 uuid 去跟登录 id 去做关联,就可以把以前用户未登录时的行为事件数据和已登录状态的行为数据关联起来。 \ No newline at end of file diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\347\254\254\344\270\211\346\226\271\345\237\213\347\202\271\345\267\245\345\205\267\344\272\247\345\223\201.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\347\254\254\344\270\211\346\226\271\345\237\213\347\202\271\345\267\245\345\205\267\344\272\247\345\223\201.md" new file mode 100644 index 00000000000..2c8051d808d --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\347\254\254\344\270\211\346\226\271\345\237\213\347\202\271\345\267\245\345\205\267\344\272\247\345\223\201.md" @@ -0,0 +1,52 @@ +--- +layout: post +title: 数据埋点:第三方埋点工具产品 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + +#### 第三方埋点工具产品 + +![]({{site.baseurl}}/img-post/数据埋点-5.png) + +- 神策 SensorData +- 字节 DataFinder +- Growing IO +- Tracking IO +- TalkingData +- 诸葛 IO +- 友盟 +- 阿里 SDK +- 百度移动分析 +- 腾讯移动分析 +- 易观 +- 阿拉丁 +- Google Analytics +- FireBase +- Fabric +- Mixpanel +- Amplitude +- Heap + +#### 选择工具要注意的问题 + + - 数据安全性(国外产品尤其注意) + - 数据隐私性 + - 访问速度 + - 数据丢失 + - 使用体验 + - 学习成本 + +#### 第三方工具存在的问题 + +- 非实时反馈 +- 数据波动难以定位 +- 与第三方沟通成本太高 +- 没有自己的核心数据存储 +- 第三方数据粒度太粗,不能适应精细化运营需求 \ No newline at end of file diff --git "a/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\350\207\252\347\240\224\345\237\213\347\202\271\347\263\273\347\273\237\347\232\204\345\212\237\350\203\275\346\250\241\345\235\227.md" "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\350\207\252\347\240\224\345\237\213\347\202\271\347\263\273\347\273\237\347\232\204\345\212\237\350\203\275\346\250\241\345\235\227.md" new file mode 100644 index 00000000000..983af49f323 --- /dev/null +++ "b/_posts/2022-01-24-\346\225\260\346\215\256\345\237\213\347\202\271\357\274\232\350\207\252\347\240\224\345\237\213\347\202\271\347\263\273\347\273\237\347\232\204\345\212\237\350\203\275\346\250\241\345\235\227.md" @@ -0,0 +1,144 @@ +--- +layout: post +title: 数据埋点:自研埋点系统的功能模块 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据埋点 +--- + + +#### 事件管理 + +- 事件 ID +- 事件名称 +- 时间中文名称 +- 事件类型 +- 事件描述 +- 所属页面 +- 页面 ID +- 属性(Key) +- 属性值(Value) +- 属性值类型 + - 字符串 + - 数值 + - 时间 + - ... +- 属性值示例 +- 埋点方式 + - 代码埋点 + - 可视化圈选埋点 + - 全埋点 + - SDK +- 触发条件 +- 上报方式 + - 实时上报 + - 延时上报 + - 延时时间 + - 缓存位置 +- 状态 + - 提交审核 + - 审核中 + - 审核完成 + - 开发中 + - 开发完成 + - 测试中 + - 测试完成 + - 待上线 + - 上线 + - 下线(逻辑删除) +- 平台 + - H5 + - 小程序 + - web + - JS + - 安卓 + - IOS +- 版本 + - 版本号 + - 埋点增减说明 +- 备注 + +#### 页面管理 + +- 功能模块 +- 页面名称 +- 页面中文名称 +- 上游页面 +- 下游页面 +- 区块(模块位置) +- 组件 + - 商品 + - 入口 + - 广告 + +#### 公共数据管理 + +- 公共数据 + - 手机号 + - UUID + - open ID + - 网络格式 + - 经纬度(坐标) + - 省份 + - 城市 +- 属性 1 + - 是否必填 +- 属性 2 + - 平台 + - 安卓 + - iOS + - 小程序 + - web +- 属性 3 + - ... + +#### 元数据管理 + +- 事件类型 + - 点击 + - 曝光 + - 播放 + - 收藏 + - 转发 + - 加购 + - 下单 + - 支付 + - 退货 + - 投诉 +- 功能模块 +- 平台 +- 版本 +- 组件 + +#### 埋点位置 + +- 页面打开 / 关闭 +- 模块曝光展示 +- 文本输入 & 搜索 +- 鼠标点击 & 拖动 & 翻页 +- 点赞、评论、分享 +- 按钮控件触发 + - 在网页不同位置,控件触发的概率是不一样的,需要注意埋点的位置; + - 网页热力图: + ![]({{site.baseurl}}/img-post/数据埋点-1.png) + +#### 埋点测试 + +- 是否正常触发 +- 触发时机是否正确 +- 报送内容是否准确、完整 +- 前端校验 + - debugger 断点 +- 后端校验 + - 数据库验证 + +#### 可视化 + +- 埋点统计 + +- 可视化分析 + diff --git "a/_posts/2022-01-26-HBase\357\274\232HBase vs RDBMS vs HDFS vs Hive.md" "b/_posts/2022-01-26-HBase\357\274\232HBase vs RDBMS vs HDFS vs Hive.md" new file mode 100644 index 00000000000..e309fce1060 --- /dev/null +++ "b/_posts/2022-01-26-HBase\357\274\232HBase vs RDBMS vs HDFS vs Hive.md" @@ -0,0 +1,98 @@ +--- +layout: post +title: HBase:HBase vs RDBMS vs HDFS vs Hive +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - HBase +--- + + +# RDBMS vs HBase + +#### 关系型数据库 + +- 结构 + - 数据库以表的形式存在 + - + - 支持FAT、NTFS、EXT、文件系统 + - 使用主键(PK) + - 通过外部中间件可以支持分库分表,但底层还是单机引擎 + - 使用行、列、单元格 +- 功能 + - 支持向上扩展(买更好的服务器) + - 使用SQL查询 + - 面向行,即每一行都是一个连续单元 + - 数据总量依赖于服务器配置 + - 具有ACID支持 + - 适合结构化数据 + - 传统关系型数据库一般都是中心化的 + - 支持事务 + - 支持Join + +#### HBase + +- 结构 + - 以表形式存在 + - 支持HDFS文件系统 + - 使用行键(row key) + - 原生支持分布式存储、计算引擎 + - 使用行、列、列蔟和单元格 +- 功能 + - 支持向外扩展 + - 使用API和MapReduce、Spark、Flink来访问HBase表数据 + - 面向列蔟,即每一个列蔟都是一个连续的单元 + - 数据总量不依赖具体某台机器,而取决于机器数量 + - HBase不支持ACID(Atomicity、Consistency、Isolation、Durability) + - 适合结构化数据和非结构化数据 + - 一般都是分布式的 + - HBase不支持事务,支持的是单行数据的事务操作 + - 不支持Join + +# HDFS vs HBase + +#### HDFS + + - HDFS是一个非常适合存储大型文件的分布式文件系统 + - HDFS它不是一个通用的文件系统,也无法在文件中快速查询某个数据 + +#### HBase + + - HBase构建在HDFS之上,并为大型表提供快速记录查找(和更新) + - HBase内部将大量数据放在HDFS中名为「StoreFiles」的索引中,以便进行高速查找 + - Hbase比较适合做快速查询等需求,而不适合做大规模的OLAP应用 + +# Hive vs Hbase + +#### Hive + - >Hive 没有存储引擎,也没有计算引擎 + - 数据仓库工具 +Hive的本质其实就相当于将HDFS中已经存储的文件在Mysql中做了一个双射关系,以方便使用HQL去管理查询 + - 用于数据分析、清洗 +Hive适用于离线的数据分析和清洗,延迟较高 + - 基于HDFS、MapReduce +Hive存储的数据依旧在DataNode上,编写的HQL语句终将是转换为MapReduce代码执行 + +#### HBase + +- NoSQL数据库 + - 是一种面向列存储的非关系型数据库。 +- 用于存储结构化和非结构化的数据 + - 适用于单表非关系型数据的存储,不适合做关联查询,类似JOIN等操作。 +- 基于HDFS + - 数据持久化存储的体现形式是Hfile,存放于DataNode中,被ResionServer以region的形式进行管理 +- 延迟较低,接入在线业务使用 + - 面对大量的企业数据,HBase可以直线单表大量数据的存储,同时提供了高效的数据访问速度 + +#### Hive vs HBase + +- Hive 和 Hbase 是两种基于 Hadoop 的不同技术 +- Hive 是一种类 SQL 的引擎,并且运行 MapReduce 任务 +- Hbase 是一种在 Hadoop 之上的 NoSQL 的 Key/value 数据库 +- 这两种工具是可以同时使用的。 + - 就像用 Google 来搜索,用 FaceBook 进行社交一样; + - Hive 可以用来进行统计查询,HBase 可以用来进行实时查询; + - 数据也可以从 Hive 写到 HBase,或者从 HBase 写回 Hive。 \ No newline at end of file diff --git "a/_posts/2022-01-26-HBase\357\274\232HBase \345\237\272\346\234\254\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-26-HBase\357\274\232HBase \345\237\272\346\234\254\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..0a0adcf335c --- /dev/null +++ "b/_posts/2022-01-26-HBase\357\274\232HBase \345\237\272\346\234\254\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,134 @@ +--- +layout: post +title: HBase:HBase 基本概念详解 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - HBase +--- + +# 1. Hbase 概念 + +#### 1.1. Hadoop 的局限 + +- 要查找数据必须搜索整个数据集,如果要进行随机读取数据,效率较低; + - Hadoop 主要是实现批量数据的处理,并且通过顺序方式访问数据; +- Hadoop 适合做一些批量数据处理,适用于吞吐量比较高的场景,但是不适用于随机查询、实时操作 + +#### 1.2. Hbase 定义 + +- HBase 建立在 Hadoop 集群 HDFS 之上,提供: + - 高可靠性 + - 高性能 + - 列存储 + - 可伸缩 + - 实时读写 + - NoSQL 的数据库系统; +- HBase 本质是一个 NoSQL; +- HBase 是 Google BigTable 的开源 java 版本; + +# 2. HBase 的特点(基于应用视角) + +- 查询 + - Hbase 查询数据功能很简单: + - HBase 不支持 join 等复杂操作; + - HBase 不支持复杂的事务(行级的事务); + - HBase 仅能通过主键(row key)和主键的 range 来检索数据; + - 相比较而言,MySQL 可以指定 where 条件限定查询范围; +- 存储 + - HBase 主要用来存储结构化和半结构化的松散数据; + - HBase 仅支持单行事务; + - 从技术上来说,HBase 更像是一个「数据存储」而不是「数据库」,因为 HBase 缺少 RDBMS 中的许多特性,例如:带类型的列、二级索引以及高级查询语言等; + - HBase 没有列类型概念; + - Hbase 中支持的数据类型:byte[]; +- 扩展 + - HBase 的主要目标是横向扩展,通过不断增加廉价的服务器,来增加存储和处理能力; + - 例如:把集群节点由 10 个增加到 20 个,存储能力和处理能力都会加倍; + +# 3. HBase 自身特性 + +- 强一致性读/写 + - HBASE 不是“最终一致的”数据存储; + - HBASE 非常适合于诸如高速计数器聚合等任务; +- 自动分块 + - HBase表通过Region分布在集群上; + - 随着数据的增长,区域被自动拆分和重新分布; +- 自动 RegionServer 故障转移 +- Hadoop/HDFS集成 + - HBase 支持 HDFS 开箱即用作为其分布式文件系统; +- MapReduce + - HBase 通过 MapReduce 支持大规模并行处理,将 HBase 用作源和接收器; +- Java Client API + - HBase 支持易于使用的 Java API 进行编程访问; +- Thrift/REST API +- 块缓存和布隆过滤器 + - HBase 支持块 Cache 和 Bloom 过滤器进行大容量查询优化; +- 运行管理 + - HBase 为业务洞察和 JMX 度量提供内置网页。 + + +# 4. HBase 局限性 + +- HBase 不支持 join 等复杂操作; +- HBase 不支持复杂的事务(行级的事务); +- HBase 仅能通过主键(row key)和主键的 range 来检索数据; + +# 5. HBase 应用场景 + +- HBase 适用场景: + - 海量数据存储; + - 快速读写; + +#### 5.1. Hbase 表特点 + +- 一般是大表 + - 一个表上十亿行、上百万列; +- 面向列存储 + - 面向列(族)存储和权限控制,列(族)独立检索; +- 稀疏 + - 对于为空(null)的列,并不占存储空间; +- 一般用在实时处理中; + +#### 5.2. 对象存储 + +- 不少的头条类、新闻类的的新闻、网页、图片存储在 HBase 之中,一些病毒公司的病毒库也是存储在 HBase 之中; + +#### 5.3. 时序存储 + +- HBase 之上有 OpenTSDB 模块,可以满足时序类场景的需求; + +#### 5.4. 推荐画像 + +- 用户标签,是一个比较大的稀疏矩阵; + - 例如:蚂蚁金服的风控就是构建在 HBase 之上; + +#### 5.5. 时空数据 + +- 主要是轨迹、气象网格之类,滴滴打车的轨迹数据主要存在 HBase 之中,另外在技术所有大一点的数据量的车联网企业,数据都是存在 HBase 之中; + +#### 5.6. Cube OLAP + +- Kylin一个cube分析工具,底层的数据就是存储在HBase之中,不少客户自己基于离线计算构建cube存储在hbase之中,满足在线报表查询的需求 + +#### 5.7. 消息/订单 + +- 在电信领域、银行领域,不少的订单查询底层的存储,另外不少通信、消息同步的应用构建在HBase之上 + +#### 5.8. Feeds 流 + +- 典型的应用就是xx朋友圈类似的应用,用户可以随时发布新内容,评论、点赞。 + +#### 5.9. NewSQL + +- 之上有Phoenix的插件,可以满足二级索引、SQL的需求,对接传统数据需要SQL非事务的需求 + +#### 5.10. 其他 + +- 存储爬虫数据 +- 海量数据备份 +- 短网址 +- 等 + diff --git "a/_posts/2022-01-26-HBase\357\274\232HBase \346\236\266\346\236\204 & \346\225\260\346\215\256\346\250\241\345\236\213.md" "b/_posts/2022-01-26-HBase\357\274\232HBase \346\236\266\346\236\204 & \346\225\260\346\215\256\346\250\241\345\236\213.md" new file mode 100644 index 00000000000..3c3be396e74 --- /dev/null +++ "b/_posts/2022-01-26-HBase\357\274\232HBase \346\236\266\346\236\204 & \346\225\260\346\215\256\346\250\241\345\236\213.md" @@ -0,0 +1,105 @@ +--- +layout: post +title: HBase:HBase 架构 & 数据模型 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - HBase +--- + + +# 1. HBase 架构 + +#### 1.1. 列式存储 + +- 关于本节,可参考文章 《OLAP:行式存储 & 列式存储》。 + +#### 1.2. HBase 架构图 + +![]({{site.baseurl}}/img-post/hbase-1.png) + +#### 1.3. Client 客户端 + + - Client 提供了访问 HBase 的接口; + - Client 维护了对应的 cache 来加速 HBase 的访问; + +#### 1.4. Zookeeper + + - 存储 HBase 的元数据(meta 表); + - 无论是读还是写数据,都是去 Zookeeper 里边拿 meta 元数据;、 + - Zookeeper 告诉给客户端去哪台机器读写数据; + +#### 1.5. HRegionServer + + - HRegionServer 处理客户端的读写请求; + - 负责与 HDFS 底层交互,是真正干活的节点。 + + - HBase 之所以能存储海量的数据,是因为 HBase 是分布式的。 + - HBase一张表的数据会分到多台机器上的。那HBase是怎么切割一张表的数据的呢?用的就是RowKey来切分,其实就是表的横向切割。 + - 一个HRegion上,存储HBase表的一部分数据。 + - + ![]({{site.baseurl}}/img-post/hbase-7.png) + +#### 1.6. HMaster + + - HMaster 会处理元数据的变更、监控 RegionServer 的状态、处理 HRegion 的分配或转移; + - 如果我们 HRegion 的数据量太大的话,HMaster 会对拆分后的 Region 重新分配 RegionServer; + - 如果发现失效的 HRegion,也会将失效的 HRegion 分配到正常的 HRegionServer 中; + +#### 1.3. HBase 数据流程 + +![]({{site.baseurl}}/img-post/hbase-2.png) + +- client 请求到 Zookeeper; +- Zookeeper 查找 meta 元数据,返回 HRegionServer 地址给 client; +- client 得到 Zookeeper 返回的地址去请求 HRegionServer; +- HRegionServer 读写数据,并返回结果给 client。 + +#### 1.4. HRegionServer + + + +# 2. HBase 数据模型 + +#### 2.1. HBase 列族 & 列 + +- HBase 里边也有表、行和列的概念 + - HBase 一行数据,由一个行键(RowKey)、一个或多个相关的列以及它的值所组成; + - 在 HBase 里边,定位一行数据会有一个唯一的值,这个叫做行键(RowKey)。 + +- HBase 的列,不是我们在关系型数据库所想象中的列 + - HBase 的列(Column)归属到列族(Column Family)中; + - 在 HBase 中用列修饰符(Column Qualifier)来标识每个列; + +- 在 HBase 里,先有列族,后有列,在列族下用列修饰符来标识一列。 + + ![]({{site.baseurl}}/img-post/hbase-3.png) + +- HBase表的每一行中,列的组成都是灵活的,行与行之间的列不需要相同。 + + ![]({{site.baseurl}}/img-post/hbase-4.png) + +- 一个列族下可以任意添加列,不受任何限制。 + + ![]({{site.baseurl}}/img-post/hbase-5.png) + +#### 2.2. HBase 数据时间戳 + +- 数据写到 HBase 的时候,都会被记录一个时间戳,这个时间戳被我们当做一个版本; + - 比如:我们修改或者删除某一条的时候,本质上是往里边新增一条数据,记录的版本加一了而已。 + + ![]({{site.baseurl}}/img-post/hbase-6.png) + +#### 2.3. HBase Key-Value 结构 + +- Key + - 由 RowKey(行键)+ColumnFamily(列族)+Column Qualifier(列修饰符)+TimeStamp(时间戳--版本)+KeyType(类型)组成 +- Value + - Value 就是实际上的值。 + + + + diff --git "a/_posts/2022-01-26-Hive\357\274\232Hive SQL DDL \345\237\272\346\234\254\346\223\215\344\275\234.md" "b/_posts/2022-01-26-Hive\357\274\232Hive SQL DDL \345\237\272\346\234\254\346\223\215\344\275\234.md" new file mode 100644 index 00000000000..6dc909b29fc --- /dev/null +++ "b/_posts/2022-01-26-Hive\357\274\232Hive SQL DDL \345\237\272\346\234\254\346\223\215\344\275\234.md" @@ -0,0 +1,164 @@ +--- +layout: post +title: Hive:Hive SQL DDL 基本操作 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hive +--- + +# 1. Hive 建库 + +- 创建 database + ``` + create database test; + ``` +- 查看库 + - 文件位置:`/user/hive/warehouse` + - 看到 test.db; + ![]({{site.baseurl}}/img-post/hive-8.png) + +# 2. Hive 表 创建 & 删除 + +#### 2.1. 建表 + +- 创建表 t_archer + ``` + create table t_archer( + id int comment "ID", + name string comment "英雄名称", + hp_max int comment "最大生命", + mp_max int comment "最大法力", + attack_max int comment "最高物攻", + defense_max int comment "最大物防", + attack_range string comment "攻击范围", + role_main string comment "主要定位", + role_assist string comment "次要定位" + ) comment "王者荣耀射手信息" + row format delimited + fields terminated by "\t"; -- 字段之间的分隔符是 tab 键 + ``` + +- 创建成功 + ![]({{site.baseurl}}/img-post/hive-9.png) + +- 上传文件至 master 服务器 + - 点击下载站内资源:archer.txt + +- put 文件至 hdfs + ``` + hadoop fs -put ./archer.txt /user/hive/warehouse/test.db/t_archer + ``` + +- 查看文件 + ![]({{site.baseurl}}/img-post/hive-10.png) + +#### 2.2. 删除表 + +- 删除表 t_archer + ``` + drop table t_archer; + ``` + +# 3. 默认分隔符使用 + +#### 3.1. Linux 文件分隔符 + +- 以 \001 作为分隔符时,下载后用 notePad++ 打开时看到的 SOH +- 以 \002 作为分隔符时,下载后用notePad++打开时看到的 STX, +- 以 \003 作为分隔符时,下载后用notePad++打开时看到的 ETX; +- "\001" “\002” "\003"分隔是程序代码中进行解析需要的。 + +#### 3.2. Hive 默认使用 \001 作为分隔符 + +- 使用 notepad++ 查看 \001 分隔符显示为 SOH + ![]({{site.baseurl}}/img-post/hive-13.png) + +#### 3.3. 替换分隔符方法 + +- notepad++ 替换 + ![]({{site.baseurl}}/img-post/hive-12.png) + +#### 3.4. 使用默认分隔符建表 + +- 创建表 t_team_ace_player + ``` + create table t_team_ace_player ( + id int, + team_name string, + ace_player_name string + ); + ``` + +- 上传文件至 master 服务器 + - 点击下载站内资源:team_ace_player.txt + +- put 文件至 hdfs + ``` + hadoop fs -put ./archer.txt /user/hive/warehouse/test.db/team_ace_player + ``` + +- 查看文件 + ![]({{site.baseurl}}/img-post/hive-14.png) + + +# 4. Hive 表查询 + +- select 查询全表 + ``` + select * from t_archer; + ``` +- 返回结果 + ![]({{site.baseurl}}/img-post/hive-11.png) + +# 5. Hive show 语句 + +- 查库 + ``` + show database; + ``` +- 查表 + ``` + show tables in test; + ``` +- 查看表结构 + ``` + use test; + desc formatted t_team_ace_player; + ``` + + ![]({{site.baseurl}}/img-post/hive-15.png) + +# 6. 中文注释乱码问题解决 + +#### 6.1. MySQL 的 Hive 元数据存储格式不支持中文 + +- Hive 元数据是在 MySQL 中保存,存储在 MySQL 中的 Hive 元数据存储格式不支持中文; +- 我们查看表结构元数据信息,发现中文注释都变成了乱码; + ![]({{site.baseurl}}/img-post/hive-16.png) + +#### 6.2. 修改 MySQL 中的 hive 元数据存储格式 + +- 连接 MySQL 做出如下修改 + + ``` + --注意 下面sql语句是需要在MySQL中执行 修改Hive存储的元数据信息(metadata) + use hive3; + show tables; + + alter table hive3.COLUMNS_V2 modify column COMMENT varchar(256) character set utf8; + alter table hive3.TABLE_PARAMS modify column PARAM_VALUE varchar(4000) character set utf8; + alter table hive3.PARTITION_PARAMS modify column PARAM_VALUE varchar(4000) character set utf8 ; + alter table hive3.PARTITION_KEYS modify column PKEY_COMMENT varchar(4000) character set utf8; + alter table hive3.INDEX_PARAMS modify column PARAM_VALUE varchar(4000) character set utf8; + ``` + +#### 6.3. 删除原来的表重新创建 + +- 删除 t_archer 后,重新建表; +- 查看表注释显示为中文; + ![]({{site.baseurl}}/img-post/hive-17.png) + diff --git "a/_posts/2022-01-26-Hive\357\274\232Hive SQL DML \345\237\272\346\234\254\346\223\215\344\275\234.md" "b/_posts/2022-01-26-Hive\357\274\232Hive SQL DML \345\237\272\346\234\254\346\223\215\344\275\234.md" new file mode 100644 index 00000000000..56ea3076096 --- /dev/null +++ "b/_posts/2022-01-26-Hive\357\274\232Hive SQL DML \345\237\272\346\234\254\346\223\215\344\275\234.md" @@ -0,0 +1,364 @@ +--- +layout: post +title: Hive:Hive SQL DML 基本操作 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hive +--- + +# 1. Hive 数据加载 + +#### 1.1. hadoop fs -put 上传数据 + +- 上传文件至 master 服务器 + - 点击下载站内资源:team_ace_player.txt + +- put 文件至 hdfs + ``` + hadoop fs -put ./archer.txt /user/hive/warehouse/test.db/team_ace_player + ``` +>官方不推荐使用这种方式,因为真个操作并未经过 hive,官方推荐 load 加载数据的方式上传。 + +#### 1.2. load 加载 + +- 加载是指将数据文件移动到与 Hive 表对应的位置,移动时是纯复制、移动操作; +- 在数据 load 加载到表中时,Hive 不会对表中的数据内容进行任何转换,任何操作。 +- filepath + - filepath 表示待移动数据的路径。可以指向文件(在这种情况下,Hive 将文件移动到表中),也可以指向目录(在这种情况下,Hive 将把该目录中的所有文件移动到表中)。 +- filepath 文件路径支持下面三种形式,要结合 LOCAL 关键字一起考虑: + - 相对路径 + - 例如:`project/data1`; + - 绝对路径 + - 例如:`/user/hive/project/data1`; + - 具有 schema 的完整 URI + - 例如:`hdfs://namenode:9000/user/hive/project/data1`。 + +#### 1.3. load 本地复制(LOCAL) + +- 从本地加载数据 数据位于HS2(hadoop102)本地文件系统; + - 执行复制操作,原始文件会继续保留; +- 本质是 `hadoop fs -put` 上传操作; +- 指定 LOCAL,表示在本地文件系统中查找文件路径; + - 若指定相对路径,将相对于用户的当前工作目录进行解释; + - 用户也可以为本地文件指定完整的URI + - 例如:file:///user/hive/project/data1。 +- 建表 student_local + ``` + create table student_local(num int,name string,sex string,age int,dept string) row format delimited fields terminated by ','; + ``` +- 上传文件至 hadoop102 `/home` 目录下 + - 点击下载站内资源:students.txt + +- 加载 + ``` + LOAD DATA LOCAL INPATH '/home/students.txt' INTO TABLE student_local; + ``` +- 查看数据 + ``` + select * from student_local; + ``` + +#### 1.4. load 移动 + +- 从 HDFS 加载数据 数据位于 HDFS 文件系统根目录下 本质是 `hadoop fs -mv` 移动操作; + - 原始本地文件移动后,就会被删除; +- 没有指定 LOCAL 关键字。 + - 如果 filepath 指向的是一个完整的 URI,会直接使用这个 URI; + - 如果没有指定 schema,Hive 会使用在 hadoop 配置文件中参数 fs.default.name 指定的(不出意外,都是 HDFS) +- 建表 student_HDFS + ``` + create external table student_HDFS(num int,name string,sex string,age int,dept string) row format delimited fields terminated by ','; + ``` +- 上传文件至 hadoop102 `/home` 目录下 + - 点击下载站内资源:students.txt +- 先把数据上传到 HDFS 上 `hadoop fs -put /root/hivedata/students.txt /` + +- 加载 + ``` + LOAD DATA INPATH '/students.txt' INTO TABLE student_HDFS; + ``` +- 查看数据 + ``` + select * from student_local; + ``` + +# 2. Hive 插入数据 + +- 创建一张目标表 student_from_insert + ``` + create table student_from_insert(num int,name string,sex string,age int,dept string); + ``` + +- 使用 `insert` + `select` 插入数据到新表中 + ``` + insert into table student_from_insert select num,name,sex,age,dept from student_local; + ``` + +# 3. Hive 查询语句 + +#### 3.1. Hive SQL select语法介绍 + +- select 基本使用 + ``` + SELECT [ALL | DISTINCT] select_expr, select_expr, ... + FROM table_reference + [WHERE where_condition] + [GROUP BY col_list] + [ORDER BY col_list] [LIMIT [offset,] rows]; + ``` + ``` + --查询所有字段或者指定字段 + select * from t_usa_covid19; + select county, cases, deaths from t_usa_covid19; + ``` +#### 3.2. select_expr、ALL DISTINCT 结果返回与去重 + +- select_expr + ``` + --返回所有匹配的行 + select state from t_usa_covid19; + + --相当于 + select all state from t_usa_covid19; + ``` +- DISTINCT + ``` + --返回所有匹配的行 去除重复的结果 + select distinct state from t_usa_covid19; + + --多个字段distinct 整体去重 + select distinct county,state from t_usa_covid19; + ``` + +#### 3.3. WHERE 过滤 + +- 在 WHERE 表达式中,可以使用 Hive 支持的任何函数和运算符; +- 但聚合函数除外,WHERE 关键字无法与聚合函数一起使用 + +- WHERE 语法 + + ``` + SELECT [ALL | DISTINCT] select_expr, select_expr, ... + FROM table_reference + [WHERE where_condition] + [GROUP BY col_list] + [ORDER BY col_list] [LIMIT [offset,] rows]; + ``` + +- 示例 + + ``` + select * from t_usa_covid19 where 1 > 2; -- 1 > 2 返回false + select * from t_usa_covid19 where 1 = 1; -- 1 = 1 返回true + + --找出来自于California州的疫情数据 + select * from t_usa_covid19 where state = "California"; + + --where条件中使用函数 找出州名字母长度超过10位的有哪些 + select * from t_usa_covid19 where length(state) >10 ; + + --注意:where条件中不能使用聚合函数 + -- --报错 SemanticException:Not yet supported place for UDAF ‘sum' + --聚合函数要使用它的前提是结果集已经确定。 + --而where子句还处于“确定”结果集的过程中,因而不能使用聚合函数。 + select state,sum(deaths) from t_usa_covid19 where sum(deaths) >100 group by state; + + --可以使用Having实现 + select state,sum(deaths) from t_usa_covid19 group by state having sum(deaths) > 100; + ``` + +#### 3.4. 聚合操作 + +- 聚合(Aggregate)操作函数,如:Count、Sum、Max、Min、Avg 等函数。 + +- 聚合函数的最大特点是不管原始数据有多少行记录,经过聚合操作只返回一条数据,这一条数据就是聚合的结果。 + +- WHERE 关键字无法与聚合函数一起使用 + +- 常见的聚合操作函数: + + ![]({{site.baseurl}}/img-post/hive-18.png) + +- 示例 + + ``` + --统计美国总共有多少个县county + select count(county) from t_usa_covid19; + + --统计美国加州有多少个县 + select count(county) from t_usa_covid19 where state = "California"; + + --统计德州总死亡病例数 + select sum(deaths) from t_usa_covid19 where state = "Texas"; + + --统计出美国最高确诊病例数是哪个县 + select max(cases) from t_usa_covid19; + ``` + +#### 3.5. GROUP BY 分组 + +- GROUP BY 语句用于结合聚合函数,根据一个或多个列对结果集进行分组; +- 如果没有group by语法,则表中的所有行数据当成一组。 + +- GROUP BY 语法 + + ``` + SELECT [ALL | DISTINCT] select_expr, select_expr, ... + FROM table_reference + [WHERE where_condition] + [GROUP BY col_list] + [ORDER BY col_list] [LIMIT [offset,] rows]; + ``` + +- 示例 + + ``` + --根据state州进行分组 统计每个州有多少个县county + select count(county) from t_usa_covid19 where count_date = "2021-01-28" group by state; + + --想看一下统计的结果是属于哪一个州的 + select state,count(county) from t_usa_covid19 where count_date = "2021-01-28" group by state; + + -- 出现在 GROUP BY 中 select_expr 的字段:要么是 GROUP BY分组的字段,要么是被聚合函数应用的字段。 + select state,count(county),sum(deaths) from t_usa_covid19 where count_date = "2021-01-28" group by state; + ``` + +#### 3.6. HAVING 分组后过滤 + +- 在 SQL 中增加 HAVING 子句原因是,WHERE 关键字无法与聚合函数一起使用。 + - 原因:**聚合计算是在 group by 分组之后进行,而 where 需要在分组前过滤。where 是一个确定结果的过程,group by 是在确定结果基础上执行。** + +- HAVING 子句可以让我们筛选分组后的各组数据,并且可以在 Having 中使用聚合函数,因为此时 where,group by 已经执行结束,结果集已经确定。 + +- HAVING 与 WHERE 区别 + - having是在分组后对数据进行过滤 + - where是在分组前对数据进行过滤 + - having后面可以使用聚合函数 + - where后面不可以使用聚合函数 +- 示例 + + ``` + --先where分组前过滤,再进行group by分组, 分组后每个分组结果集确定 再使用having过滤 + select state,sum(deaths) from t_usa_covid19 where count_date = "2021-01-28" group by state having sum(deaths) > 10000; + + --这样写更好 即在group by的时候聚合函数已经作用得出结果 having直接引用结果过滤 不需要再单独计算一次了 + select state,sum(deaths) as cnts from t_usa_covid19 where count_date = "2021-01-28" group by state having cnts> 10000; + ``` + +#### 3.7. ORDER BY 排序 + +- ORDER BY 语法 + + ``` + SELECT [ALL | DISTINCT] select_expr, select_expr, ... + FROM table_reference + [WHERE where_condition] + [GROUP BY col_list] + [ORDER BY col_list] [LIMIT [offset,] rows]; + ``` + +- 示例 + + ``` + --根据确诊病例数升序排序 查询返回结果 + select * from t_usa_covid19 order by cases; + + --不写排序规则 默认就是asc升序 + select * from t_usa_covid19 order by cases asc; + + --根据死亡病例数倒序排序 查询返回加州每个县的结果 + select * from t_usa_covid19 where state = "California" order by cases desc; + ``` + +# 4. Hive Join 使用 +- 根据数据库的三范式设计要求和日常工作习惯来说,我们通常不会设计一张大表把所有类型的数据都放在一起,而 + 是不同类型的数据设计不同的表存储。 +- 在这种情况下,有时需要基于多张表查询才能得到最终完整的结果;join语法的出现是用于根据两个或多个表中的列之间的关系,从这些表中共同组合查询数据。 +- 在Hive中,使用最多,最重要的两种join分别是: + - inner join(内连接) + - left join(左连接) + + +# 5. Hive SQL 常用函数 + +#### SQL 函数类型 + +- 使用 `show functions` 查看当下可用的所有函数; +- 通过 `describe function extended funcname` 来查看函数的使用方式。 + +- 内置函数可分为:数值类型函数、日期类型函数、字符串类型函数、集合函数、条件函数等; +- 用户定义函数根据输入输出的行数可分为3类: + - UDF(User-Defined-Function)普通函数,一进一出; + - UDAF(User-Defined Aggregation Function)聚合函数,多进一出; + - UDTF(User-Defined Table-Generating Functions)表生成函数,一进多出。 + +#### String Functions 字符串函数 + +- 字符串长度函数:length + - `select length("itcast")`; +- 字符串反转函数:reverse + - `select reverse("itcast")`; +字符串连接函数:concat + - `select concat("angela","baby")`; +- 带分隔符字符串连接函数:concat_ws(separator, [string | array(string)]+) + - `select concat_ws('.', 'www', array('itcast', 'cn'))`; +- 字符串截取函数:substr(str, pos[, len]) 或者 substring(str, pos[, len]) + - `select substr("angelababy",-2)`; --pos是从1开始的索引,如果为负数则倒着数 + - `select substr("angelababy",2,2)`; +- 分割字符串函数: split(str, regex) + - `select split('apache hive', ' ')`; + +#### Date Functions 日期函数 + +- 获取当前日期: current_date + - `select current_date();` +- 获取当前UNIX时间戳函数: unix_timestamp + - `select unix_timestamp();` +- 日期转UNIX时间戳函数: unix_timestamp + - `select unix_timestamp("2011-12-07 13:01:03");` +- 指定格式日期转UNIX时间戳函数: unix_timestamp + - `select unix_timestamp('20111207 13:01:03','yyyyMMdd HH:mm:ss');` +- UNIX时间戳转日期函数: from_unixtime + - `select from_unixtime(1618238391);` + - `select from_unixtime(0, 'yyyy-MM-dd HH:mm:ss');` +- 日期比较函数: datediff 日期格式要求'yyyy-MM-dd HH:mm:ss' or 'yyyy-MM-dd' + - `select datediff('2012-12-08','2012-05-09');` +- 日期增加函数: date_add + - `select date_add('2012-02-28',10);` +- 日期减少函数: date_sub + - `select date_sub('2012-01-1',10);` + +#### Mathematical Functions 数学函数 + +- 取整函数: round 返回double类型的整数值部分 (遵循四舍五入) + - `select round(3.1415926);` +- 指定精度取整函数: round(double a, int d) 返回指定精度d的double类型 + - `select round(3.1415926,4);` +- 取随机数函数: rand 每次执行都不一样 返回一个0到1范围内的随机数 + - `select rand();` +- 指定种子取随机数函数: rand(int seed) 得到一个稳定的随机数序列 + - `select rand(3);` + +#### Conditional Functions 条件函数 + +- 使用之前课程创建好的student表数据 + - `select * from student limit 3;` +- if条件判断: if(boolean testCondition, T valueTrue, T valueFalseOrNull) + - `select if(1=2,100,200);` + - `select if(sex ='男','M','W') from student limit 3;` +- 空值转换函数: nvl(T value, T default_value) + - `select nvl("allen","itcast");` + - `select nvl(null,"itcast");` +- 条件转换函数: CASE a WHEN b THEN c [WHEN d THEN e]* [ELSE f] END + - `select case 100 when 50 then 'tom' when 100 then 'mary' else 'tim' end;` + - `select case sex when '男' then 'male' else 'female' end from student limit 3;` + + + + + + diff --git "a/_posts/2022-01-26-Hive\357\274\232Hive SQL \345\270\270\347\224\250\344\274\230\345\214\226\346\226\271\346\263\225.md" "b/_posts/2022-01-26-Hive\357\274\232Hive SQL \345\270\270\347\224\250\344\274\230\345\214\226\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..97247157452 --- /dev/null +++ "b/_posts/2022-01-26-Hive\357\274\232Hive SQL \345\270\270\347\224\250\344\274\230\345\214\226\346\226\271\346\263\225.md" @@ -0,0 +1,51 @@ +--- +layout: post +title: Hive:Hive SQL 常用优化方法 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hive +--- + + +# 1. 影响 Hive 效率的因素 + +#### 1.1. 数据倾斜 + +#### 1.2. 数据冗余 + +#### 1.3. JOB / IO 过多 + +#### 1.4. MapReduce 分配不合理 + + +# 2. 优化思路 + +#### 2.1. 对 Hive SQL 语句的优化 + + +#### 2.2. Hive 配置项优化 + + +#### 2.3. MapReduce 配置优化 + + +# 3. 优化方法 + +#### 3.1. 列裁剪 & 分区裁剪 + +- 列裁剪,就是在查询时只读取需要的列; +- 分区裁剪,就是只读取需要的分区。 +>全列扫描和全表扫描,他们的效率都很低。 + + + + + + ![]({{site.baseurl}}/img-post/es-5.png) + +- 这表示安装成、服务已被启动; + diff --git "a/_posts/2022-01-26-Hive\357\274\232Hive \345\244\215\346\235\202\346\225\260\346\215\256\347\261\273\345\236\213\344\275\277\347\224\250\346\226\271\346\263\225.md" "b/_posts/2022-01-26-Hive\357\274\232Hive \345\244\215\346\235\202\346\225\260\346\215\256\347\261\273\345\236\213\344\275\277\347\224\250\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..784a56ae6a8 --- /dev/null +++ "b/_posts/2022-01-26-Hive\357\274\232Hive \345\244\215\346\235\202\346\225\260\346\215\256\347\261\273\345\236\213\344\275\277\347\224\250\346\226\271\346\263\225.md" @@ -0,0 +1,164 @@ +--- +layout: post +title: Hive:Hive 复杂数据类型使用方法 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hive +--- + +# 1. Hive 复杂数据类型 + +- Hive 复杂数据类型包括数组(ARRAY)、映射(MAP)和结构体(STRUCT),具体如下所示: + + ![]({{site.baseurl}}/img-post/hql-1.png) + +- ARRAY + - ARRAY 类型是由一系列相同数据类型的元素组成,这些元素可以通过下标来访问。 + - 比如:有一个 ARRAY 类型的变量 fruits,它是由 [‘apple’,‘orange’,‘mango’] 组成,那么我们可以通过 fruits[1]来访问元素 orange,因为 ARRAY 类型的下标是从 0 开始的。 + +- MAP + - MAP 包含 key->value 键值对,可以通过 key 来访问元素。 + - 比如:”userlist” 是一个 map 类型,其中 username 是 key,password 是 value;那么我们可以通过 userlist[‘username’] 来得到这个用户对应的 password。 + +- STRUCT + - STRUCT 可以包含不同数据类型的元素。 + - 这些元素可以通过”点语法”的方式来得到所需要的元素,比如 user 是一个 STRUCT 类型,那么可以通过 user.address 得到这个用户的地址。 + +- 示例: + + ``` + CREATE TABLE student( + name STRING, + favors ARRAY, + scores MAP, + address STRUCT + ) + ROW FORMAT DELIMITED + FIELDS TERMINATED BY '\t' + COLLECTION ITEMS TERMINATED BY ';' + MAP KEYS TERMINATED BY ':'; + ``` + + - 字段说明: + - 字段 name 是基本类型,favors 是数组类型,可以保存很多爱好,scores 是映射类型,可以保存多个课程的成绩,address 是结构类型,可以存储住址信息; + - ROW FORMAT DELIMITED 是指明后面的关键词是列和元素分隔符的; + - FIELDS TERMINATED BY 是字段分隔符; + - COLLECTION ITEMS TERMINATED BY 是元素分隔符(Array 中的各元素、Struct 中的各元素、Map 中的 key-value 对之间); + - MAP KEYS TERMINATED BY 是 Map 中 key 与 value 的分隔符; + - LINES TERMINATED BY 是行之间的分隔符; + - STORED AS TEXTFILE 指数据文件上传之后保存的格式。 + + - 优势 + - 在关系型数据库中,我们至少需要三张表来定义,包括学生地址信息表、爱好表、成绩表; + - 但在 Hive 中通过一张表就可以搞定了; + - 也就是说,复合数据类型把多表关系通过一张表就可以实现了。 + + +# 2. ARRAY + +- 创建表 + + ``` + create table test.person(name string,work_locations array) + row format delimited + fields terminated by ' '; + ``` + +- 数据内容 + + ``` + huangbo beijing,shanghai,tianjin,hangzhou + xuzheng changchu,chengdu,wuhan + wangbaoqiang dalian,shenyang,jilin + ``` + +- 加载数据 + + ``` + load data local inpath '/home/score.txt' into table test.score; + ``` + +- 查询数据 + + ``` + select * from test.person; + select name from test.person; + select work_locations from test.person; + select work_locations[0] from test.person; + ``` + + ![]({{site.baseurl}}/img-post/hql-2.png) + +# 3. MAP + +- 创建表 + ``` + create table test.score(name string, scores map) + row format delimited fields terminated by ' ' + collection items terminated by ',' + map keys terminated by ':'; + ``` + +- 数据内容 + ``` + huangbo yuwen:80,shuxue:89,yingyu:95 + xuzheng yuwen:70,shuxue:65,yingyu:81 + wangbaoqiang yuwen:75,shuxue:100,yingyu:75 + ``` + +- 加载数据 + ``` + load data local inpath '/home/score.txt' into table test.score; + ``` + +- 查询数据 + ``` + select * from test.score; + select name from test.score; + select scores from test.score; + select s.scores['yuwen'] from test.score s; + ``` + +# 4. STRUCT + +- 创建表 + ``` + create table test.structtable(id int,course struct) + row format delimited fields terminated by ' ' + collection items terminated by ','; + ``` + +- 数据内容 + ``` + 1 english,80 + 2 math,89 + 3 chinese,95 + ``` + +- 加载数据 + ``` + load data local inpath '/home/structtable.txt' into table test.structtable; + ``` + +- 查询数据 + ``` + select * from test.structtable; + select id from test.structtable; + select course from test.structtable; + select t.course.name from test.structtable t; + select t.course.score from test.structtable t; + ``` + + + + + + + + + + \ No newline at end of file diff --git "a/_posts/2022-01-26-Hive\357\274\232Hive \345\256\211\350\243\205\351\203\250\347\275\262\347\244\272\344\276\213.md" "b/_posts/2022-01-26-Hive\357\274\232Hive \345\256\211\350\243\205\351\203\250\347\275\262\347\244\272\344\276\213.md" new file mode 100644 index 00000000000..797aaf814da --- /dev/null +++ "b/_posts/2022-01-26-Hive\357\274\232Hive \345\256\211\350\243\205\351\203\250\347\275\262\347\244\272\344\276\213.md" @@ -0,0 +1,513 @@ +--- +layout: post +title: Hive:Hive 安装部署示例 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hive +--- + +# 1. 环境准备 + +- 由于 Apache Hive 是一款基于 Hadoop 的数据仓库软件,通常部署运行在 Linux 系统之上。因此不管使用何种方式配置 Hive Metastore,必须要先保证服务器的基础环境正常,Hadoop 集群健康可用。 +- 服务器基础环境 + - 集群时间同步、防火墙关闭、主机Host映射、免密登录、JDK安装 +- Hadoop 集群健康可用 + - 启动 Hive 之前必须先启动 Hadoop 集群。 + - 特别要注意,需等待 HDFS 安全模式关闭之后再启动运行 Hive。 + - Hive 不是分布式安装运行的软件,其分布式的特性主要借由 Hadoop 完成,包括分布式存储、分布式计算。 + +# 2. 安装 MySQL + +#### 2.1. 删除 自带mariadb + +``` +[root@hadoop102 ~]# rpm -qa|grep mariadb +mariadb-libs-5.5.60-1.el7_5.x86_64 +[root@hadoop102 ~]# rpm -e mariadb-libs-5.5.60-1.el7_5.x86_64 --nodeps +[root@hadoop102 ~]# rpm -qa|grep mariadb +[root@hadoop102 ~]# +``` + +#### 2.2. 安装软件 + +- 上传文件安装包 + +- 解压缩 + ``` + tar xvf mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar + ``` + + ``` + [root@hadoop102 mysql]# tar xvf mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar + mysql-community-embedded-devel-5.7.29-1.el7.x86_64.rpm + mysql-community-test-5.7.29-1.el7.x86_64.rpm + mysql-community-embedded-5.7.29-1.el7.x86_64.rpm + mysql-community-embedded-compat-5.7.29-1.el7.x86_64.rpm + mysql-community-libs-5.7.29-1.el7.x86_64.rpm + mysql-community-client-5.7.29-1.el7.x86_64.rpm + mysql-community-server-5.7.29-1.el7.x86_64.rpm + mysql-community-devel-5.7.29-1.el7.x86_64.rpm + mysql-community-libs-compat-5.7.29-1.el7.x86_64.rpm + mysql-community-common-5.7.29-1.el7.x86_64.rpm + [root@hadoop102 mysql]# ls + mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar mysql-community-embedded-devel-5.7.29-1.el7.x86_64.rpm + mysql-community-client-5.7.29-1.el7.x86_64.rpm mysql-community-libs-5.7.29-1.el7.x86_64.rpm + mysql-community-common-5.7.29-1.el7.x86_64.rpm mysql-community-libs-compat-5.7.29-1.el7.x86_64.rpm + mysql-community-devel-5.7.29-1.el7.x86_64.rpm mysql-community-server-5.7.29-1.el7.x86_64.rpm + mysql-community-embedded-5.7.29-1.el7.x86_64.rpm mysql-community-test-5.7.29-1.el7.x86_64.rpm + mysql-community-embedded-compat-5.7.29-1.el7.x86_64.rpm + ``` + +- 安装依赖 + + ``` + yum -y install libaio + ``` + + ``` + [root@hadoop102 mysql]# yum -y install libaio + Loaded plugins: fastestmirror + Determining fastest mirrors + base | 3.6 kB 00:00:00 + epel | 4.7 kB 00:00:00 + extras | 2.9 kB 00:00:00 + updates | 2.9 kB 00:00:00 + (1/7): base/7/x86_64/group_gz | 153 kB 00:00:00 + (2/7): epel/x86_64/group_gz | 96 kB 00:00:00 + (3/7): base/7/x86_64/primary_db | 6.1 MB 00:00:00 + (4/7): epel/x86_64/updateinfo | 1.1 MB 00:00:00 + (5/7): epel/x86_64/primary_db | 7.0 MB 00:00:00 + (6/7): extras/7/x86_64/primary_db | 247 kB 00:00:00 + (7/7): updates/7/x86_64/primary_db | 16 MB 00:00:00 + Resolving Dependencies + --> Running transaction check + ---> Package libaio.x86_64 0:0.3.109-13.el7 will be installed + --> Finished Dependency Resolution + + Dependencies Resolved + + =================================================================================================================================================== + Package Arch Version Repository Size + =================================================================================================================================================== + Installing: + libaio x86_64 0.3.109-13.el7 base 24 k + + Transaction Summary + =================================================================================================================================================== + Install 1 Package + + Total download size: 24 k + Installed size: 38 k + Downloading packages: + libaio-0.3.109-13.el7.x86_64.rpm | 24 kB 00:00:00 + Running transaction check + Running transaction test + Transaction test succeeded + Running transaction + ** Found 2 pre-existing rpmdb problem(s), 'yum check' output follows: + 2:postfix-2.10.1-6.el7.x86_64 has missing requires of libmysqlclient.so.18()(64bit) + 2:postfix-2.10.1-6.el7.x86_64 has missing requires of libmysqlclient.so.18(libmysqlclient_18)(64bit) + Installing : libaio-0.3.109-13.el7.x86_64 1/1 + Verifying : libaio-0.3.109-13.el7.x86_64 1/1 + + Installed: + libaio.x86_64 0:0.3.109-13.el7 + + Complete! + ``` +- 安装 mysql + + ``` + rpm -ivh mysql-community-common-5.7.29-1.el7.x86_64.rpm mysql-community-libs-5.7.29-1.el7.x86_64.rpm mysql-community-client-5.7.29-1.el7.x86_64.rpm mysql-community-server-5.7.29-1.el7.x86_64.rpm + ``` + + ``` + [root@hadoop102 mysql]# rpm -ivh mysql-community-common-5.7.29-1.el7.x86_64.rpm mysql-community-libs-5.7.29-1.el7.x86_64.rpm mysql-community-client-5.7.29-1.el7.x86_64.rpm mysql-community-server-5.7.29-1.el7.x86_64.rpm + warning: mysql-community-common-5.7.29-1.el7.x86_64.rpm: Header V3 DSA/SHA1 Signature, key ID 5072e1f5: NOKEY + Preparing... ################################# [100%] + Updating / installing... + 1:mysql-community-common-5.7.29-1.e################################# [ 25%] + 2:mysql-community-libs-5.7.29-1.el7################################# [ 50%] + 3:mysql-community-client-5.7.29-1.e################################# [ 75%] + 4:mysql-community-server-5.7.29-1.e################################# [100%] + ``` + +#### 初始化配置 MySQL + +- 初始化启动 + ``` + mysqld --initialize + ``` + +- 更改所属组 + ``` + chown mysql:mysql /var/lib/mysql -R + ``` + +- 启动 mysql + ``` + systemctl start mysqld.service + ``` + +- 查看生成的临时 root 密码 + + ``` + cat /var/log/mysqld.log + ... + [Note] A temporary password is generated for root@localhost: o+TU+KDOm004 + ``` + +- 修改root密码 授权远程访问 设置开机自启动 + + ```shell + [root@node2 ~]# mysql -u root -p + Enter password: #这里输入在日志中生成的临时密码 + Welcome to the MySQL monitor. Commands end with ; or \g. + Your MySQL connection id is 3 + Server version: 5.7.29 + + Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. + + Oracle is a registered trademark of Oracle Corporation and/or its + affiliates. Other names may be trademarks of their respective + owners. + + Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + + mysql> + + + #更新root密码 设置为hadoop + mysql> alter user user() identified by "hadoop"; + Query OK, 0 rows affected (0.00 sec) + + + #授权 + mysql> use mysql; + + mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'hadoop' WITH GRANT OPTION; + + mysql> FLUSH PRIVILEGES; + + #mysql的启动和关闭 状态查看 (这几个命令必须记住) + systemctl stop mysqld + systemctl status mysqld + systemctl start mysqld + + #建议设置为开机自启动服务 + [root@node2 ~]# systemctl enable mysqld + Created symlink from /etc/systemd/system/multi-user.target.wants/mysqld.service to /usr/lib/systemd/system/mysqld.service. + + #查看是否已经设置自启动成功 + [root@node2 ~]# systemctl list-unit-files | grep mysqld + mysqld.service enabled + ``` + + + +# 3. Hive 安装部署 + +#### 3.1. 部署方式 + +- 单机部署 + - Hive 不是分布式软件,但是却又分布式的能力; + - Hive 使用 HDFS 存储数据,而 HDFS 是分布式的; + - Hive 计算数据使用 MR、Spark、Tez,而这些计算引擎是分布式的。 + +#### 3.2. 上传解压 + +- 上传文件 apache-hive-3.1.2-bin.tar.gz 到服务器。 + +#### 3.3. 修改 guava 包 + +- hive 自带的 guava 包版本太低,需要替换成 hadoop 中的 guava 包; + ``` + cd hive/ + rm -rf lib/guava-19.0.jar + cp /usr/local/hadoop-3.1.3/share/hadoop/common/lib/guava-27.0-jre.jar ./lib/ + cd lib/ & ls + ``` +- 查看 guava 包已被更新至 hadoop 使用的更高版本 guava-27.0 + + ![]({{site.baseurl}}/img-post/hive-5.png) + + +#### 3.4. 修改 hive 配置文件 + +- 编辑 hive-env.sh + + ``` + vim hive-env.sh + ``` + + ``` + export HADOOP_HOME=/usr/local/hadoop-3.1.3 + export HIVE_CONF_DIR=/usr/local/hive/conf + export HIVE_AUX_JARS_PATH=/usr/local/hive/lib + ``` + +- 编辑 hive-site.xml + + ``` + vim hive-site.xml + ``` + + ``` + + + + javax.jdo.option.ConnectionURL + jdbc:mysql://hadoop102:3306/hive3?createDatabaseIfNotExist=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8 + + + + javax.jdo.option.ConnectionDriverName + com.mysql.jdbc.Driver + + + + + javax.jdo.option.ConnectionUserName + root + + + + + javax.jdo.option.ConnectionPassword + hadoop + + + + + hive.server2.thrift.bind.host + hadoop102 + + + + + hive.metastore.uris + thrift://hadoop102:9083 + + + + + hive.metastore.event.db.notification.api.auth + false + + + ``` + +#### 3.5. 修改 Hadoop 中 core-site.xml + +- 因为Hive需要把数据存储在HDFS上,并且通过MapReduce作为执行引擎处理数据; +- 因此需要在Hadoop中添加相关配置属性,以满足Hive在Hadoop上运行。 + + +- core-site.xml + + ``` + vim core-site.xml + ``` + + ``` + + + hadoop.proxyuser.root.hosts + * + + + + + hadoop.proxyuser.root.groups + * + + + ``` +- 修改文件后,需要在 Hadoop 集群同步配置文件,重启 hadoop 生效。 + +#### 3.6。 重启 hadoop 集群 + +- 略。 + +#### 3.5. 上传 mysql jar 包 + +- 上传 jar 文件 `mysql-connector-java-5.1.32.jar`; + + + +# 4. 启动 Hive + +#### 4.1. 初始化 hive 元数据 + +- schema 初始化命令 + + ``` + cd /hive/bin + ./schematool -initSchema -dbType mysql -verbos + ``` + +- 下面提示标识初始化成功 + + ``` + ... + Initialization script completed + schemaTool completed + ``` + +#### 4.2. 启动 metastore 服务 + +- 前台启动 + - 可以根据需求添加参数开启 debug 日志,获取详细日志信息,便于排错。 + - 进程会一直占据终端,`ctrl + c` 结束进程,服务关闭。 + ``` + /hive/bin/hive --service metastore + + /hive/bin/hive --service metastore --hiveconf hive.root.logger=DEBUG,console + ``` +- 后台启动 + ``` + nohup ./hive/bin/hive --service metastore & + ``` + +# 5. Hive 客户端使用 + +#### 5.1. Hive自带客户端 + +- Hive 发展至今,总共历经了两代客户端工具。 +- 第一代客户端 bin/hive(deprecated不推荐使用): + - $HIVE_HOME/bin/hive, 是一个 shellUtil。 + - 主要功能: + - 一是可用于以交互或批处理模式运行Hive查询; + - 二是用于Hive相关服务的启动,比如metastore服务。 +- 第二代客户端 bin/beeline(recommended 推荐使用): + - $HIVE_HOME/bin/beeline,是一个JDBC客户端,是官方强烈推荐使用的Hive命令行工具,和第一代客户端相比,性能加强安全性提高。 + +#### 5.2. HiveServer2 服务 + +- 远程模式下 beeline 通过 Thrift 连接到单独的 HiveServer2 服务上,这也是官方推荐在生产环境中使用的模式。 +- HiveServer2 支持多客户端的并发和身份认证,旨在为开放 API 客户端如 JDBC、ODBC 提供更好的支持。 +>HiveServer2 通过 Metastore 服务读写元数据,在远程模式下,启动 HiveServer2 之前必须先首先启动 + metastore服务。 + +- 特别注意: + - 远程模式下,Beeline 客户端只能通过 HiveServer2 服务访问 Hive,而 bin/hive 是通过Metastore服务访问的。 + +- hive 客户端和服务的具体关系,如下图: + + ![]({{site.baseurl}}/img-post/hive-6.png) + +#### 5.3. 在 hadoop103 使用 beeline 客户端 远程访问 HiveServer2 + +- scp hive 到 hadoop103 + + ``` + scp -r hive hadoop103:/usr/local + ``` + +- 启动 hiveserver2 + + ``` + nohup /hive/bin/hive --service metastore & + nohup /hive/bin/hive --service hiveserver2 & + ``` + +- 启动 beeline + + ``` + cd /hive + ./beeline + ``` + +- 处理报错:Cannot find hadoop installation + + - 报错内容:`Cannot find hadoop installation: $HADOOP_HOME or $HADOOP_PREFIX must be set or hadoop must be in the path` + +- 解决办法:执行 `source hive-env.sh` + + ``` + cd /hive/conf + source hive-env.sh + ``` + +- 再次启动 beeline + + ``` + cd /hive + ./beeline + ``` + +- 得到如下提示 + + ``` + [root@hadoop103 bin]# ./beeline + SLF4J: Class path contains multiple SLF4J bindings. + SLF4J: Found binding in [jar:file:/usr/local/hive/lib/log4j-slf4j-impl-2.10.0.jar!/org/slf4j/impl/StaticLoggerBinder.class] + SLF4J: Found binding in [jar:file:/usr/local/hadoop-3.1.3/share/hadoop/common/lib/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class] + SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation. + SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory] + SLF4J: Class path contains multiple SLF4J bindings. + SLF4J: Found binding in [jar:file:/usr/local/hive/lib/log4j-slf4j-impl-2.10.0.jar!/org/slf4j/impl/StaticLoggerBinder.class] + SLF4J: Found binding in [jar:file:/usr/local/apache-hive-3.1.2-bin/lib/log4j-slf4j-impl-2.10.0.jar!/org/slf4j/impl/StaticLoggerBinder.class] + SLF4J: Found binding in [jar:file:/usr/local/hadoop-3.1.3/share/hadoop/common/lib/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class] + SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation. + SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory] + Beeline version 3.1.2 by Apache Hive + beeline> + ``` + +- 连接 HS2 + + ``` + beeline> ! connect jdbc:hive2://hadoop102:10000 + Connecting to jdbc:hive2://hadoop102:10000 + Enter username for jdbc:hive2://hadoop102:10000: root + Enter password for jdbc:hive2://hadoop102:10000: + Connected to: Apache Hive (version 3.1.2) + Driver: Hive JDBC (version 3.1.2) + Transaction isolation: TRANSACTION_REPEATABLE_READ + 0: jdbc:hive2://hadoop102:10000> + ``` + +- 执行 show tables; + + ``` + 0: jdbc:hive2://hadoop102:10000> show tables; + INFO : Compiling command(queryId=root_20220630230711_bbf7a441-313a-4a33-b3dc-b4192c9b4fc1): show tables + INFO : Concurrency mode is disabled, not creating a lock manager + INFO : Semantic Analysis Completed (retrial = false) + INFO : Returning Hive schema: Schema(fieldSchemas:[FieldSchema(name:tab_name, type:string, comment:from deserializer)], properties:null) + INFO : Completed compiling command(queryId=root_20220630230711_bbf7a441-313a-4a33-b3dc-b4192c9b4fc1); Time taken: 1.139 seconds + INFO : Concurrency mode is disabled, not creating a lock manager + INFO : Executing command(queryId=root_20220630230711_bbf7a441-313a-4a33-b3dc-b4192c9b4fc1): show tables + INFO : Starting task [Stage-0:DDL] in serial mode + INFO : Completed executing command(queryId=root_20220630230711_bbf7a441-313a-4a33-b3dc-b4192c9b4fc1); Time taken: 0.09 seconds + INFO : OK + INFO : Concurrency mode is disabled, not creating a lock manager + +-----------+ + | tab_name | + +-----------+ + +-----------+ + No rows selected (1.576 seconds) + 0: jdbc:hive2://hadoop102:10000> + ``` + +# 6. DataGrip 连接 Hive + +- 建立连接 + + ![]({{site.baseurl}}/img-post/hive-7.png) + +- 创建 database + + ``` + create database test; + ``` +- 查看库 + - 文件位置:`/user/hive/warehouse` + - 看到 test.db; + ![]({{site.baseurl}}/img-post/hive-8.png) + diff --git "a/_posts/2022-01-26-Hive\357\274\232Hive \347\256\200\344\273\213\345\217\212\345\267\245\344\275\234\345\216\237\347\220\206.md" "b/_posts/2022-01-26-Hive\357\274\232Hive \347\256\200\344\273\213\345\217\212\345\267\245\344\275\234\345\216\237\347\220\206.md" new file mode 100644 index 00000000000..df112297821 --- /dev/null +++ "b/_posts/2022-01-26-Hive\357\274\232Hive \347\256\200\344\273\213\345\217\212\345\267\245\344\275\234\345\216\237\347\220\206.md" @@ -0,0 +1,151 @@ +--- +layout: post +title: Hive:Hive 简介及工作原理 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hive +--- + +# 1. Hive 简介 + +- Hive 是一款建立在 Hadoop 之上的开源数据仓库系统,可以将存储在 Hadoop 文件中的结构化、半结构化数据文件映射为一张数据库表,基于表提供了一种类似 SQL 的查询模型,称为 Hive 查询语言(HQL),用于访问和分析存储在 Hadoop 文件中的大型数据集。 + +#### 1.1. Hive & Hadoop + +>Hive 利用 HDFS 存储数据,利用 MapReduce 计算引擎查询分析数据。 +- Hive 核心是将 HQL 转换为 MapReduce 程序,然后将程序提交到 Hadoop 群集执行。 + +#### 1.2. Hive & MapReduce + +- 使用 Hadoop MapReduce 直接处理数据所面临的问题 + - 人员学习成本太高 需要掌握 java 语言 + - MapReduce 实现复杂,查询逻辑开发难度太大 + +- 使用Hive处理数据的好处 + - 操作接口采用类SQL语法,提供快速开发的能力(简单、容易上手) + - 避免直接写 MapReduce,减少开发人员的学习成本 + - 支持自定义函数(UDF),功能扩展很方便 + - 背靠 Hadoop,擅长存储分析海量数据集 + +- Hive 的最大的魅力在于用户专注于编写 HQL,Hive 将 HQL 转换成为 MapReduce 程序完成对数据的分析。 + +#### 1.3. Hive 优点 + +- 操作接口采用类 SQL 语法,避免了写 MapReduce 程序,简单易上手,减少开发人员学习成本; +- 在数据处理方面,Hive 语句最终会生成 MapReduce 任务去计算,常用于离线数据分析,对数据实时性要求不高的场景; +- 在数据存储方面,能够存储很大的数据集(HDFS),并且对数据完整性、格式要求并不严格(要求是结构化的就可以了); +- 在延展性方面,Hive 支持用户自定义函数,用户可以根据自己的需求来实现自己的函数; + +#### 1.4. Hive 缺点 + +- Hive 的 HQL 本身表达能力有限,不能够进行迭代式计算,在数据挖掘方面也不擅长(当需求逻辑特别复杂时还需要借助于 Map Reduce) +- Hive 操作默认基于 MapReduce 引擎,延迟高、不适用于交互式查询,因此智能化程度低,并且基于 SQL 调优困难、粒度较粗。 +- 我们也可以手动更改让 Hive 基于 Spark 引擎。 + +# 2. Hive 工作原理 + +#### 2.1. Hive 工作流程 + +![]({{site.baseurl}}/img-post/hive-1.png) + +#### 2.2. Hive 将数据文件映射成为一张表 + +- 在 hive中 能够写 sql 处理的前提是针对表,而不是针对文件,因此需要将文件和表之间的对应关系描述记录清楚; +- 映射信息专业的叫法称之为元数据信息(元数据是指用来描述数据的数据 metadata)。 + - 元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。 + + +#### 2.2. Hive 解析 SQL 编译称为 MapReduce + +- 用户写完sql之后,hive需要针对sql进行语法校验,并且根据记录的元数据信息解读sql背后的含义,制定执行计划。 +- 并且把执行计划转换成 MapReduce 程序来具体执行,把执行的结果封装返回给用户。 + + +# 3. Hive 架构 + +![]({{site.baseurl}}/img-post/hive-2.png) + +#### 3.1. 用户接口 + +- 包括 CLI、JDBC/ODBC、WebGUI。其中,CLI(command line interface)为shell命令行;Hive中的Thrift服务器允许; +- 外部客户端通过网络与Hive进行交互,类似于JDBC或ODBC协议。WebGUI是通过浏览器访问Hive。 + +#### 3.2. 元数据存储 Hive Metadata + +- Hive 中的元数据包括表的名字,表的列和分区及其属性,表的属性(是否为外部表等),表的数据所在目录等。 +- Hive Metadata 即 Hive 的元数据,包含用 Hive 创建的 database、table、表的位置、类型、属性,字段顺序类型等元信息。 +- 元数据存储在关系型数据库中。如 hive 内置的 Derby、或者第三方如 MySQL 等。 + +#### 3.4. Driver 驱动程序 + +- 包括语法解析器、计划编译器、优化器、执行器; +- Driver 完成 HQL 查询语句从词法分析、语法分析、编译、优化以及查询计划的生成; +- 生成的查询计划存储在 HDFS 中,并在随后有执行引擎调用执行。 + +#### 3.5. 执行引擎 + +- Hive 本身并不直接处理数据文件,而是通过执行引擎处理。 +- 当下 Hive支持 MapReduce、Tez、Spark 3种执行引擎。 + +# 4. Metastore + +#### 4.1. Hive Metastore + +- Metastore 即元数据服务。Metastore服务的作用是管理metadata元数据,对外暴露服务地址,让各种客户端通过连接metastore服务,由metastore再去连接MySQL数据库来存取元数据。 +- 有了metastore服务,就可以有多个客户端同时连接,而且这些客户端不需要知道MySQL数据库的用户名和密码,只需要连接metastore 服务即可。某种程度上也保证了hive元数据的安全。 + +#### 4.2. metastore 配置方式 + +- metastore服务配置有3种模式:内嵌模式、本地模式、远程模式。 +- 区分3种配置方式的关键是弄清楚两个问题: + - Metastore服务是否需要单独配置、单独启动; + - Metadata是存储在内置的derby中,还是第三方RDBMS,比如MySQL。 + +#### 4.3. metastore 远程模式 + +- 在生产环境中,建议用远程模式来配置Hive Metastore。 +- 在这种情况下,其他依赖 hive 的软件都可以通过 Metastore 访问 hive。由于还可以完全屏蔽数据库层,因此这也带来了更好的可管理性/安全性。 + +![]({{site.baseurl}}/img-post/hive-3.png) + +#### 4.4. HiveServer2 服务 + +- 远程模式下 Hive 命令行工具 beeline 通过 Thrift 连接到单独的 HiveServer2 服务上,这也是官方推荐在生产环境中使用的模式。 +- HiveServer2 支持多客户端的并发和身份认证,旨在为开放 API 客户端如 JDBC、ODBC 提供更好的支持。 + +- HiveServer2 通过 Metastore 服务读写元数据,在远程模式下,启动 HiveServer2 之前必须先首先启动 + metastore服务。 +>特别注意:远程模式下,Beeline客户端只能通过HiveServer2服务访问Hive。而bin/hive是通过Metastore服务访问的。 + +- 具体关系如下: + ![]({{site.baseurl}}/img-post/hive-4.png) + + +# 5. Hive 与 MySQL 的区别 + +- 查询语言不同 + - hive 是 hql 语言,mysql 是 sql 语言; +- 数据存储位置不同 + - hive 是把数据存储到 hdfs,而 mysql 数据存储在自己的系统中; +- 数据格式 + - hive 数据格式可以用户自定义,mysql 有自己的系统定义格式; +- 数据更新 + - hive 不支持数据更新,只可以读,不可以写,sql 支持数据的读写; +- 索引 + - hive 没有索引,因此查询数据的时候是通过 mapreduce 很暴力的把数据都查询一遍,也造成了 hive 查询数据速度很慢的原因,而 mysql 有索引; +- 延迟性 + - hive 没有索引,因此查询数据的时候通过 mapreduce 很暴力 的把数据都查询一遍,也造成了 hive 查询数据速度很慢的原因,而 mysql 有索引; +- 数据规模 + - hive 存储的数据量超级大,而 mysql 只是存储一些少量的业务数据; +- 底层执行原理 + - hive 底层是用的 mapreduce,而 mysql 是 excutor 执行器; +- 应用场景 + - Hive 只适合用来做海量离线数 据统计分析,也就是数据仓库。 + +![]({{site.baseurl}}/img-post/hive-19.png) + + diff --git "a/_posts/2022-01-26-OLAP\357\274\232\350\241\214\345\274\217\345\255\230\345\202\250 & \345\210\227\345\274\217\345\255\230\345\202\250.md" "b/_posts/2022-01-26-OLAP\357\274\232\350\241\214\345\274\217\345\255\230\345\202\250 & \345\210\227\345\274\217\345\255\230\345\202\250.md" new file mode 100644 index 00000000000..d45e806c539 --- /dev/null +++ "b/_posts/2022-01-26-OLAP\357\274\232\350\241\214\345\274\217\345\255\230\345\202\250 & \345\210\227\345\274\217\345\255\230\345\202\250.md" @@ -0,0 +1,144 @@ +--- +layout: post +title: OLAP:行式存储 & 列式存储 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - OLAP +--- + + +# 行式存储 + +- 传统的关系型数据库,如 Oracle、DB2、MySQL、SQL SERVER 等采用行式存储法(Row-based); +- 在基于行式存储的数据库中,数据是按照行数据为基础逻辑存储单元进行存储的,一行中的数据在存储介质中以连续存储形式存在。 + +#### 行式存储缺陷 + +- 行式数据库在读取数据的时候,所选择查询的目标即使只涉及少数几项属性,但由于这些目标数据埋藏在各行数据单元中,而行单元往往又特别大,应用程序必须读取每一条完整的行记录,从而使得读取效率大大降低; +- 对此,行式数据库给出的优化方案是加“索引”; +- 在 OLTP 类型的应用中,通过索引机制或给表分区等手段,可以简化查询操作步骤,并提升查询效率; +- 但针对海量数据背景的 OLAP 应用(例如分布式数据库、数据仓库等等),行式存储的数据库就有些“力不从心”了,行式数据库建立索引和物化视图,需要花费大量时间和资源; +- 因此还是得不偿失,无法从根本上解决查询性能和维护成本等问题,也不适用于数据仓库等应用场景,所以后来出现了基于列式存储的数据库。 + + +# 列式存储 + +- 列式存储(Column-based)是相对于行式存储来说的,新兴的 Hbase、HP Vertica、EMC Greenplum 等分布式数据库均采用列式存储。 +- 在基于列式存储的数据库中,数据是按照列为基础的逻辑存储单元进行存储的,一列中的数据在存储介质中以连续存储形式存在。 + + ![]({{site.baseurl}}/img-post/olap-1.png) + +#### 列存储优点 + +- 列存储在写入效率、保证数据完整性上都不如行存储,它的优势是在读取过程,不会产生冗余数据,这对数据完整性要求不高的大数据处理领域,比如互联网,犹为重要。 +- 查询过程中,可针对各列的运算并发执行(SMP),***在内存中聚合完整记录集,***可能降低查询响应时间; +- 可在数据列中高效查找数据,无需维护索引(任何列都能作为索引),查询过程中能够尽量减少无关IO,避免全表扫描; +- 因为各列独立存储,且数据类型已知,可以针对该列的数据类型、数据量大小等因素动态选择压缩算法,以提高物理存储利用率; +- 如果某一行的某一列没有数据,那在列存储时,就可以不存储该列的值,这将比行式存储更节省空间。 + +#### 数据压缩 + +- 由于同一个数据列的数据重复度很高,因此,列式数据库压缩时有很大的优势。 +- Google Bigtable 列式数据库对网页库压缩可以达到15倍以上的压缩率。 +- 另外,可以针对列式存储做专门的索引优化。 + +![]({{site.baseurl}}/img-post/olap-3.png) + +![]({{site.baseurl}}/img-post/olap-4.png) + +#### 位图索引 + +- 比如,某表性别列只有两个值,“男”和“女”,可以对这一列建立位图索引; +- 如下图所示: + - “男” 对应的位图为100101,表示第 1、4、6 行值为 “男”; + - “女” 对应的位图为011010,表示第 2、3、5 行值为 “女”; +- 如果需要查找男性或者女性的个数,只需要统计相应的位图中1出现的次数即可。 + +![]({{site.baseurl}}/img-post/olap-2.png) + +- 另外,建立位图索引后 0 和 1 的重复度高,可以采用专门的编码方式对其进行压缩。 + +#### 数据查询 + +- 查询关键步骤如下: + - 去字典表里找到字符串对应数字(只进行一次字符串比较)。 + - 用数字去列表里匹配,匹配上的位置设为 1。 + - 把不同列的匹配结果进行位运算得到符合所有条件的记录下标。 + - 使用这个下标组装出最终的结果集。 + +# 行式存储 VS 列式存储 + +- 行式存储倾向于结构固定,列式存储倾向于结构弱化。 +- 行式存储一行数据只需一份主键,列式存储一行数据需要多份主键。 +- 行式存储存的都是业务数据,列式存储除了业务数据外,还要存储列名。 +- 行式存储更像一个Java Bean,所有字段都提前定义好,且不能改变;列式存储更像一个Map,不提前定义,随意往里添加key/value。 + +#### 特性对比 + +- 传统行式数据库的特性如下: + - 数据是按行存储的; + - 没有索引的查询使用大量 I/O,一般的数据库表都会建立索引,通过索引加快查询效率; + - 建立索引和物化视图需要花费大量的时间和资源; + - 面对查询需求,数据库必须被大量膨胀才能满足需求。 + +- 列式数据库的特性如下: + - 数据按列存储,即每一列单独存放; + - 数据即索引; + - 只访问查询涉及的列,可以大量降低系统 I/O; + - 每一列由一个线程来处理,即查询的并发处理性能高; + - 数据类型一致,数据特征相似,可以高效压缩; + - 比如有增量压缩、前缀压缩算法都是基于列存储的类型定制的,所以可以大幅度提高压缩比,有利于存储和网络输出数据带宽的消耗。 + +#### 数据写入 + +- 行存储的写入是一次完成的,如果这种写入建立在操作系统的文件系统上,可以保证写入过程的成功或者失败,数据的完整性因此可以确定; + - 列存储由于需要把一行记录拆分成单列保存,写入次数明显比行存储多 + - 意味着磁头调度次数多,而磁头调度是需要时间的,一般在1ms~10ms; + - 再加上磁头需要在盘片上移动和定位花费的时间,实际消耗更大; + - 所以,行存储在写入上占很大的优势。 +- 数据修改,实际也是一次写入过程; + - 不同的是,数据修改是对磁盘上的记录做删除标记; + - 行存储是在指定位置写入一次,列存储是将磁盘定位到多个列上分别写入,这个过程仍是行存储的列数倍; + - 所以,数据修改也是以行存储占优。 + +#### 数据读取 + +- 数据读取 + - 行存储通常将一行数据完全读出,如果只需要其中几列数据的情况,就会存在冗余列,出于缩短处理时间的考量,消除冗余列的过程通常是在内存中进行的; + - 列存储每次读取的数据是集合的一段或者全部,不存在冗余性问题; + +- 数据分布 + - 由于列存储的每一列数据类型是同质的,不存在二义性问题。 + - 比如说某列数据类型为整型(int),那么它的数据集合一定是整型数据。 + - 这种情况使数据解析变得十分容易。 + - 相比之下,行存储则要复杂得多, + - 在一行记录中保存了多种类型的数据,数据解析需要在多种数据类型之间频繁转换,这个操作很消耗 CPU,增加了解析的时间。 + - 所以,列存储的解析过程更有利于分析大数据。 + +# 适用场景 + +- 行式存储的适用场景 + - 适合随机的增删改查操作; + - 需要在行中选取所有属性的查询操作; + - 需要频繁插入或更新的操作,其操作与索引和行的大小更为相关。 + +- 列式存储的适用场景 + - OLAP + - 一般来说,一个 OLAP 类型的查询可能需要访问几百万甚至几十亿个数据行,且该查询往往只关心少数几个数据列。 + - 例如,查询今年销量最高的前20个商品,这个查询只关心三个数据列:时间(date)、商品(item)以及销售量(sales amount)。 + - 商品的其他数据列,例如商品URL、商品描述、商品所属店铺,等等,对这个查询都是没有意义的。 + - 列式数据库只需要读取存储着“时间、商品、销量”的数据列,而行式数据库需要读取所有的数据列。 + - 因此,列式数据库大大地提高了 OLAP 大数据量查询的效率。 +   + - OLAP & OLTP + - 很多列式数据库还支持列族(column group,Bigtable系统中称为locality group),即将多个经常一起访问的数据列的各个值存放在一起。 + - 如果读取的数据列属于相同的列族,列式数据库可以从相同的地方一次性读取多个数据列的值,避免了多个数据列的合并。 + - 列族是一种行列混合存储模式,这种模式能够同时满足 OLTP 和 OLAP 的查询需求。 +   + - 数仓 + - 对于数据仓库和分布式数据库来说,大部分情况下它会从各个数据源汇总数据,然后进行分析和反馈,其操作大多是围绕同一列属性的数据进行的,而当查询某属性的数据记录时,列式数据库只需返回与列属性相关的值,在大数据量查询场景中,列式数据库可在内存中高效组装各列的值,最终形成关系记录集,因此可以显著减少IO消耗,并降低查询响应时间,非常适合数据仓库和分布式的应用。 + diff --git "a/_posts/2022-01-26-\346\225\260\344\273\223\350\277\220\347\273\264\357\274\232Hadoop \351\233\206\347\276\244\351\205\215\347\275\256\351\224\231\350\257\257\345\257\274\350\207\264\347\232\204\346\212\245\351\224\231\345\217\212\345\244\204\347\220\206\345\212\236\346\263\225.md" "b/_posts/2022-01-26-\346\225\260\344\273\223\350\277\220\347\273\264\357\274\232Hadoop \351\233\206\347\276\244\351\205\215\347\275\256\351\224\231\350\257\257\345\257\274\350\207\264\347\232\204\346\212\245\351\224\231\345\217\212\345\244\204\347\220\206\345\212\236\346\263\225.md" new file mode 100644 index 00000000000..cb755cd5814 --- /dev/null +++ "b/_posts/2022-01-26-\346\225\260\344\273\223\350\277\220\347\273\264\357\274\232Hadoop \351\233\206\347\276\244\351\205\215\347\275\256\351\224\231\350\257\257\345\257\274\350\207\264\347\232\204\346\212\245\351\224\231\345\217\212\345\244\204\347\220\206\345\212\236\346\263\225.md" @@ -0,0 +1,322 @@ +--- +layout: post +title: 数仓运维:Hadoop 集群配置错误导致的报错及处理办法 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓运维 +--- + + + +#### Hadoop宕机 + +(1)如果MR造成系统宕机。此时要控制Yarn同时运行的任务数,和每个任务申请的最大内存。调整参数:yarn.scheduler.maximum-allocation-mb(单个任务可申请的最多物理内存量,默认是8192MB) + +(2)如果写入文件过量造成NameNode宕机。那么调高Kafka的存储大小,控制从Kafka到HDFS的写入速度。高峰期的时候用Kafka进行缓存,高峰期过去数据同步会自动跟上。 + +#### 报错:no YARN_RESOURCEMANAGER_USER defined + +- 报错内容: + `ERROR: but there is no YARN_RESOURCEMANAGER_USER defined. Aborting operation.` +- 解决办法: + + ``` + cd /usr/local/hadoop-3.1.3/sbin/ + ``` + + ``` + vim start-dfs.sh + + # 在文件顶部添加以下参数 + HDFS_NAMENODE_USER=root + HDFS_DATANODE_USER=root + HDFS_SECONDARYNAMENODE_USER=root + YARN_RESOURCEMANAGER_USER=root + YARN_NODEMANAGER_USER=root + ``` + + ``` + vim stop-dfs.sh + + # 在文件顶部添加以下参数 + HDFS_NAMENODE_USER=root + HDFS_DATANODE_USER=root + HDFS_SECONDARYNAMENODE_USER=root + YARN_RESOURCEMANAGER_USER=root + YARN_NODEMANAGER_USER=root + ``` + + ```aidl + vim start-yarn.sh + + # 在文件顶部添加以下参数 + YARN_RESOURCEMANAGER_USER=root + HADOOP_SECURE_DN_USER=yarn + YARN_NODEMANAGER_USER=root + ``` + + ```aidl + vim stop-yarn.sh + + # 在文件顶部添加以下参数 + YARN_RESOURCEMANAGER_USER=root + HADOOP_SECURE_DN_USER=yarn + YARN_NODEMANAGER_USER=root + ``` + +#### Error: Could not find or load main class org.apache.hadoop.mapreduce.v2.app.MRAppMaster + +- 写入数据到 hive 时报错,提示 `FAILED: Execution Error, return code 2 from org.apache.hadoop.hive.ql.exec.mr.MapRedTask`,报错内容如下方所示: + + ``` + INFO : Compiling command(queryId=root_20220702214441_90f37f30-1f88-4147-bc45-832d0018fa04): insert into student_hdfs values (950122, '华少','男',30,'IS') + INFO : Concurrency mode is disabled, not creating a lock manager + INFO : Semantic Analysis Completed (retrial = false) + INFO : Returning Hive schema: Schema(fieldSchemas:[FieldSchema(name:col1, type:int, comment:null), FieldSchema(name:col2, type:string, comment:null), FieldSchema(name:col3, type:string, comment:null), FieldSchema(name:col4, type:int, comment:null), FieldSchema(name:col5, type:string, comment:null)], properties:null) + INFO : Completed compiling command(queryId=root_20220702214441_90f37f30-1f88-4147-bc45-832d0018fa04); Time taken: 0.633 seconds + INFO : Concurrency mode is disabled, not creating a lock manager + INFO : Executing command(queryId=root_20220702214441_90f37f30-1f88-4147-bc45-832d0018fa04): insert into student_hdfs values (950122, '华少','男',30,'IS') + WARN : Hive-on-MR is deprecated in Hive 2 and may not be available in the future versions. Consider using a different execution engine (i.e. spark, tez) or using Hive 1.X releases. + INFO : Query ID = root_20220702214441_90f37f30-1f88-4147-bc45-832d0018fa04 + INFO : Total jobs = 3 + INFO : Launching Job 1 out of 3 + INFO : Starting task [Stage-1:MAPRED] in serial mode + INFO : Number of reduce tasks determined at compile time: 1 + INFO : In order to change the average load for a reducer (in bytes): + INFO : set hive.exec.reducers.bytes.per.reducer= + INFO : In order to limit the maximum number of reducers: + INFO : set hive.exec.reducers.max= + INFO : In order to set a constant number of reducers: + INFO : set mapreduce.job.reduces= + INFO : number of splits:1 + INFO : Submitting tokens for job: job_1656766458051_0004 + INFO : Executing with tokens: [] + INFO : The url to track the job: http://hadoop103:8088/proxy/application_1656766458051_0004/ + INFO : Starting Job = job_1656766458051_0004, Tracking URL = http://hadoop103:8088/proxy/application_1656766458051_0004/ + INFO : Kill Command = /usr/local/hadoop-3.1.3/bin/mapred job -kill job_1656766458051_0004 + INFO : Hadoop job information for Stage-1: number of mappers: 1; number of reducers: 1 + INFO : 2022-07-02 21:45:05,409 Stage-1 map = 100%, reduce = 100% + ERROR : Ended Job = job_1656766458051_0004 with errors + ERROR : FAILED: Execution Error, return code 2 from org.apache.hadoop.hive.ql.exec.mr.MapRedTask + INFO : MapReduce Jobs Launched: + INFO : Stage-Stage-1: Map: 1 Reduce: 1 HDFS Read: 0 HDFS Write: 0 FAIL + INFO : Total MapReduce CPU Time Spent: 0 msec + INFO : Completed executing command(queryId=root_20220702214441_90f37f30-1f88-4147-bc45-832d0018fa04); Time taken: 25.682 seconds + INFO : Concurrency mode is disabled, not creating a lock manager + Error: Error while processing statement: FAILED: Execution Error, return code 2 from org.apache.hadoop.hive.ql.exec.mr.MapRedTask (state=08S01,code=2) + ``` + +- 注意:下方的报错并不是实际报错,需要到 yarn web ui 集群查看真实的错误。 + +- 真实的报错内容: + + ``` + Application application_1656759536632_0001 failed 2 times due to AM Container for appattempt_1656759536632_0001_000002 exited with exitCode: 1 + Failing this attempt.Diagnostics: [2022-07-02 19:00:15.757]Exception from container-launch. + Container id: container_1656759536632_0001_02_000001 + Exit code: 1 + [2022-07-02 19:00:15.799]Container exited with a non-zero exit code 1. Error file: prelaunch.err. + Last 4096 bytes of prelaunch.err : + Last 4096 bytes of stderr : + Error: Could not find or load main class org.apache.hadoop.mapreduce.v2.app.MRAppMaster + Please check whether your etc/hadoop/mapred-site.xml contains the below configuration: + + yarn.app.mapreduce.am.env + HADOOP_MAPRED_HOME=${full path of your hadoop distribution directory} + + + mapreduce.map.env + HADOOP_MAPRED_HOME=${full path of your hadoop distribution directory} + + + mapreduce.reduce.env + HADOOP_MAPRED_HOME=${full path of your hadoop distribution directory} + + [2022-07-02 19:00:15.801]Container exited with a non-zero exit code 1. Error file: prelaunch.err. + Last 4096 bytes of prelaunch.err : + Last 4096 bytes of stderr : + Error: Could not find or load main class org.apache.hadoop.mapreduce.v2.app.MRAppMaster + Please check whether your etc/hadoop/mapred-site.xml contains the below configuration: + + yarn.app.mapreduce.am.env + HADOOP_MAPRED_HOME=${full path of your hadoop distribution directory} + + + mapreduce.map.env + HADOOP_MAPRED_HOME=${full path of your hadoop distribution directory} + + + mapreduce.reduce.env + HADOOP_MAPRED_HOME=${full path of your hadoop distribution directory} + + For more detailed output, check the application tracking page: http://hadoop103:8088/cluster/app/application_1656759536632_0001 Then click on links to logs of each attempt. + . Failing the application. + ``` + +- 错误解决办法: + + - 下面的方法我尝试了,不可行。 + - mapred-site.xml 给 HADOOP_MAPRED_HOME 配置绝对路径 + ``` + + yarn.app.mapreduce.am.env + HADOOP_MAPRED_HOME=/opt/module/hadoop-3.1.3 + + + mapreduce.map.env + HADOOP_MAPRED_HOME=/opt/module/hadoop-3.1.3 + + + mapreduce.reduce.env + HADOOP_MAPRED_HOME=/opt/module/hadoop-3.1.3 + + + ``` + - 配置 hadoop classpath + ``` + hadoop classpath + ``` + - 获取后的路径 `:` 替换成 `,`,写入 `yarn-site.xml`,如下: + ``` + + yarn.application.classpath + /usr/local/hadoop-3.1.3/etc/hadoop,/usr/local/hadoop-3.1.3/share/hadoop/common/lib/*,/usr/local/hadoop-3.1.3/share/hadoop/common/*,/usr/local/hadoop-3.1.3/share/hadoop/hdfs,/usr/local/hadoop-3.1.3/share/hadoop/hdfs/lib/*,/usr/local/hadoop-3.1.3/share/hadoop/hdfs/*,/usr/local/hadoop-3.1.3/share/hadoop/mapreduce/lib/*,/usr/local/hadoop-3.1.3/share/hadoop/mapreduce/*,/usr/local/hadoop-3.1.3/share/hadoop/yarn,/usr/local/hadoop-3.1.3/share/hadoop/yarn/lib/*,/usr/local/hadoop-3.1.3/share/hadoop/yarn/* + + + yarn.nodemanager.env-whitelist, + JAVA_HOME,HADOOP_HOME,HADOOP_COMMON_HOME, HADOOP_ HDFS_HOME, HADOOP_ CONF_DIR, CLASSPATH_PREPEND_DISTCACHE, HADOOP_YARN_HOME, HADOOP_MAPRED_HOME + + ``` + - 上述方法我试了都不可行。 + +- 解决办法: + - 编辑 `mapred-site.xml` + ``` + + yarn.app.mapreduce.am.env + HADOOP_MAPRED_HOME=${HADOOP_CLASSPATH} + + + mapreduce.map.env + HADOOP_MAPRED_HOME=${HADOOP_CLASSPATH} + + + mapreduce.reduce.env + HADOOP_MAPRED_HOME=${HADOOP_CLASSPATH} + + ``` + - export 配置 HADOOP_MAPRED_HOME + ``` + export HADOOP_MAPRED_HOME=/usr/local/hadoop-3.1.3 + ``` + - 注意:这一步不能去掉,不然的话上面的配置会不生效。 + - 分发 `mapred-site.xml` 到集群其他节点 + - 重启集群。 + +#### The required MAP capability is more than the supported max container capability in the cluster. + +- 报错内容: + + ``` + The required MAP capability is more than the supported max container capability in the cluster. Killing the Job. mapResourceRequest: maxContainerCapability: + Job received Kill while in RUNNING state. + REDUCE capability required is more than the supported max container capability in the cluster. Killing the Job. reduceResourceRequest: maxContainerCapability: + ``` +- 解决方法: + + - 需要调整两个参数: + + - `yarn.nodemanager.resource.memory-mb` + - `yarn.scheduler.maximum-allocation-mb` + - 注意:如果只设置其中一个,可能依然存在同样的报错 + + - 编辑 `yarn-site.xml` + + ``` + + yarn.nodemanager.resource.memory-mb + 10000 + + + yarn.scheduler.maximum-allocation-mb + 10000 + + ``` + - 分发 `yarn-site.xml` 到集群其他节点 + - 重启集群。 + +#### Error: JAVA_HOME is not set and could not be found. + +- 报错内容: + ``` + Error: JAVA_HOME is not set and could not be found. + ``` +- 解决办法 1: 添加系统环境变设置 + - 检查环境变量设置 + ``` + which java + + java -version + ``` + - 如果返回异常,则需要手动添加 + ``` + vim /etc/profile + + # JAVA_HOME + export JAVA_HOME=/usr/local/bin/jdk1.8 + export PATH=$PATH:$JAVA_HOME/bin + ``` + - 启动生效 + ``` + source /etc/profile + ``` + +- 解决方法 2:在 `hadoop-env.sh` 中的环境变量设置 + ``` + cd /usr/local/hadoop-3.1.3/etc/hadoop/ + + vim hadoop-env.sh + + export JAVA_HOME=/usr/local/bin/jdk1.8 + ``` + +- 注意: + - 对于 hadoop 集群而言,前面所说的环境变量配置,必须要在 master 上进行,并在集群内部分发、而且启动生效; + - 如果按照上面的操作进行以后仍然报错,请 **仔细检查是否是在 master 上进行这些配置操作,检查集群机器是否全部配置生效**。 + +#### bash: jps: command not found + +- 报错内容: + ```aidl + bash: jps: command not found + ``` + +- 解决方法: + - 删除原有的 jps 软连接,并新建一个 + ```aidl + rm /usr/local/bin/jps + ln -s /usr/local/bin/jdk1.8/bin/jps /usr/local/bin/jps + ``` + +#### /usr/libexec/grepconf.sh:行5: grep: 未找到命令 + +- 问题: + - 在 `/etc/profile` 配置 SPARK_HOME 后,执行 `source /etc/profile` 环境变量生效,提示报错; + - 报错后命令行常规命令均无法正常使用 + +- 经排查发现路径配置错误,需要修改路径,重新启用环境变量配置文件; + +- 解决方法: + - 修改配置文件 SPARK_HOME 路径; + - cmd 窗口执行如下命令: + ``` + export PATH=/usr/bin:/usr/sbin:/bin:/sbin:/usr/X11R6/bin + ``` + - 重新启用环境变量配置文件 + ``` + source /etc/profile + ``` \ No newline at end of file diff --git "a/_posts/2022-01-26-\346\225\260\344\273\223\350\277\220\347\273\264\357\274\232\345\260\217\346\226\207\344\273\266\351\227\256\351\242\230\350\247\243\345\206\263\346\200\235\350\267\257.md" "b/_posts/2022-01-26-\346\225\260\344\273\223\350\277\220\347\273\264\357\274\232\345\260\217\346\226\207\344\273\266\351\227\256\351\242\230\350\247\243\345\206\263\346\200\235\350\267\257.md" new file mode 100644 index 00000000000..283dc20fda5 --- /dev/null +++ "b/_posts/2022-01-26-\346\225\260\344\273\223\350\277\220\347\273\264\357\274\232\345\260\217\346\226\207\344\273\266\351\227\256\351\242\230\350\247\243\345\206\263\346\200\235\350\267\257.md" @@ -0,0 +1,81 @@ +--- +layout: post +title: 数仓运维:小文件问题解决思路 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓运维 +--- + + +#### 小文件过多问题产生原因 + +- 源头的数据文件数目本身就很多 +- 一般使用动态分区会产生很多小文件(动态分区是根据某个key进行划分分区) +- reduce 个数越多,小文件数目越多。 + +#### 小文件过多造成的后果 + +- 从 Hive 的角度看,小文件会开很多 map,一个 map 开一个 JVM 去执行,所以这些任务的初始化、启动、执行会浪费大量的资源,严重影响性能; +- HDFS 存储太多小文件, 会导致 namenode 元数据特别大, 占用太多内存, 制约了集群的扩展; + +#### 解决小文件问题思路 + +- 通过调整参数进行合并 + + - 每个Map最大输入大小(这个值决定了合并后文件的数量) + - `set mapred.max.split.size=256000000;` + + - 一个节点上split的至少的大小(这个值决定了多个DataNode上的文件是否需要合并) + - `set mapred.min.split.size.per.node=100000000;` + + - 一个交换机下split的至少的大小(这个值决定了多个交换机上的文件是否需要合并) + - `set mapred.min.split.size.per.rack=100000000;` + + - 执行Map前进行小文件合并 + - `set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;` + + - 设置map输出和reduce输出进行合并的相关参数: + + - 设置map端输出进行合并,默认为true + - `set hive.merge.mapfiles = true` + + - 设置reduce端输出进行合并,默认为false + - `set hive.merge.mapredfiles = true` + + - 设置合并文件的大小 + - `set hive.merge.size.per.task = 256*1000*1000` + + - 当输出文件的平均大小小于该值时,启动一个独立的MapReduce任务进行文件merge。 + - `set hive.merge.smallfiles.avgsize=16000000` + +- 针对按分区插入数据的时候产生大量的小文件的问题, 可以使用 DISTRIBUTE BY rand() 将数据随机分配给 Reduce,这样可以使得每个 Reduce 处理的数据大体一致. + + - 设置每个reducer处理的大小为5个G + `set hive.exec.reducers.bytes.per.reducer=5120000000;` + + - 使用distribute by rand()将数据随机分配给reduce, 避免出现有的文件特别大, 有的文件特别小 + `insert overwrite table test partition(dt) + select * from iteblog_tmp + DISTRIBUTE BY rand(); + ` + - 使用 Sequencefile 作为表存储格式,不要用 textfile,在一定程度上可以减少小文件 + +- 使用 hadoop 的 archive 归档 + - Hadoop Archive或者HAR,是一个高效地将小文件放入HDFS块中的文件存档工具,它能够将多个小文件打包成一个HAR文件,这样在减少namenode内存使用的同时,仍然允许对文件进行透明的访问。 + + - 用来控制归档是否可用 + - `set hive.archive.enabled=true;` + - 通知Hive在创建归档时是否可以设置父目录 + - `set hive.archive.har.parentdir.settable=true;` + - 控制需要归档文件的大小 + - `set har.partfile.size=1099511627776;` + - 使用以下命令进行归档 + - `ALTER TABLE srcpart ARCHIVE PARTITION(ds='2008-04-08', hr='12');` + - 对已归档的分区恢复为原文件 + - `ALTER TABLE srcpart UNARCHIVE PARTITION(ds='2008-04-08', hr='12');` + + - 注意,归档的分区不能够 INSERT OVERWRITE,必须先 unarchive \ No newline at end of file diff --git "a/_posts/2022-01-26-\346\225\260\344\273\223\350\277\220\347\273\264\357\274\232\346\225\260\346\215\256\345\200\276\346\226\234\351\227\256\351\242\230\350\247\243\345\206\263\346\200\235\350\267\257.md" "b/_posts/2022-01-26-\346\225\260\344\273\223\350\277\220\347\273\264\357\274\232\346\225\260\346\215\256\345\200\276\346\226\234\351\227\256\351\242\230\350\247\243\345\206\263\346\200\235\350\267\257.md" new file mode 100644 index 00000000000..5c2dfd692d5 --- /dev/null +++ "b/_posts/2022-01-26-\346\225\260\344\273\223\350\277\220\347\273\264\357\274\232\346\225\260\346\215\256\345\200\276\346\226\234\351\227\256\351\242\230\350\247\243\345\206\263\346\200\235\350\267\257.md" @@ -0,0 +1,402 @@ +--- +layout: post +title: 数仓运维:数据倾斜问题解决思路 +subtitle: +date: 2022-01-26 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓运维 +--- + + + +# 1. 什么是数据倾斜 + +- 数据倾斜,一般是指 mapreduce 程序执行时,reduce 节点大部分执行完毕,但是有一个或者几个 reduce 节点运行很慢,导致整个程序的处理时间很长; + +> MapReduce 过程中,Reduce 阶段输入 ReduceTask 的数据流是形式,用户可以自定义 reduce()方法进行逻辑处理,最终以的形式输出。 + +- 简单来说数据倾斜就是数据的 key 的分化严重不均,造成一部分数据很多,一部分数据很少的局面。 + +# 2. 数据倾斜有哪些表现 + +- 数据倾斜会发生在数据开发的各个环节中,在不同引擎表现不一样。 + +#### 2.1. Hadoop/Hive 99.99% 问题 + +- 任务进度长时间维持在 99.99%,如果详细的看日志或者和监控界面的话,会发现: + - 有一个多几个 reduce 卡住 + - 各种 container 报错 OOM + - 读写的数据量极大,至少远远超过其它正常的 reduce + - 伴随着数据倾斜,会出现任务被 kill 等各种诡异的表现 + +#### 2.2. Spark OOM 问题 + +- Spark 中的数据倾斜也很常见,Spark 中一个 stage 的执行时间受限于最后那个执行完的 task,因此运行缓慢的任务,会拖累整个程序的运行速度; +- 过多的数据在同一个 task 中执行,将会把 executor 撑爆,造成 OOM,程序终止运行。 + +#### 2.3. Flink OOM 问题 + +- 使用 Window、GroupBy、Distinct 等聚合函数时,频繁出现反压,消费速度很慢,个别的task会出现OOM,调大资源也无济于事。 + +# 3. 那些场景会出现数据倾斜 + +#### 3.1. 业务场景 + +- 正常的数据分布理论上都是倾斜的,就是我们所说的'二八原理': + - 80% 的用户只使用 20% 的功能; + - 20% 的用户贡献了 80% 的访问量; + - 80% 的商品贡献了 20% 的销售业绩 + +- 例如: + - 在订单表中,北京和上海两个地区的订单数量,比其他地区高几个数量级,那么进行聚合的时候就会出现数据热点; + - 在商品表中,少数顶流/主营品类的商品数量很多,而周边商品的种类则比较少,在统计的时候也会出现数据热点。 + +#### 3.2. 技术场景 + +- 大表 join 小表; +- group by 维度过小,某个值的数量过多; +- count、distinct 某个值过多; + +# 4. 为什么出现数据倾斜 + +- 在做数据运算的时候(如:`count`、`distinct`、`group by`、`join`),会触发 Shuffle 动作,一旦触发所有相同 key 的值,就会拉到一个或几个节点上,发生单点过热问题。 + +- MapReduce 数据倾斜,是因为某一个 key 的数据条数比其他 key 多很多(有时是百倍或者千倍之多),这条 key 所在的 reduce 节点所处理的数据量比其他节点就大很多,从而导致某几个节点迟迟运行不完; + +#### Group by 倾斜 + +- group by 引起的倾斜主要是输入数据行按照 「group by 列分布不均匀」 引起的。 +- 比如,假设按照供应商对销售明细事实表来统计订单数,那么部分大供应商的订单量显然非常多,而多数供应商的订单量就一般,由于 group by 的时候是按照供应商的 ID 分发到每个 Reduce Task ,那么此时分配到大供应商的 Reduce Task 就分配了更多的订单,从而导致数据倾斜。 + +#### Join 倾斜 + +- join 造成的倾斜,常见情况是不能做 map join 的两个表(能做map join的话基本上可以避免倾斜),其中一个是行为表,另一个应该是属性表。 + +- 比如,我们有两个表: + - 一个用户属性表 users,还有一个用户对商品的操作行为表日志表 logs。 +- 现在需要将行为表关联用户表: + - `select * from logs a join users b on a.user_id = b.user_id;` +- 其中 logs 表里面会有一个特殊用户 user_id = 0,代表未登录用户,假如这种用户占了相当的比例,那么个别 reduce 会收到比其他 reduce 多得多的数据,因为它要接收所有 user_id = 0 的记录进行处理,使得其处理效果会非常差,其他 reduce 都跑完很久了它还在运行。 + +#### 4.3. 连接 key 设计缺陷 + +- 示例: + - 对于商品 item_id 的编码,除了本身的 id 序列,还人为的把 item 的类型、也作为编码放在最后两位; + - 例如:类型 1(电子产品)的编码是 01,类型 2(家居产品)的编码是 02, + - 由于类型 1 是主要商品类,将会造成以 01 为结尾的商品整体倾斜, + 这时,如果 reduce 的数量恰好是 100 的整数倍,会造成 partitioner 把 01 结尾的 item_id 都 hash 到同一个 reducer,从而引爆问题。 + +#### 4.4. 连接 key 为 NULL 空值 + +- 示例: + - 如日志中,常会有信息丢失的问题,比如日志中的 user_id,如果取其中的 user_id 和 用户表中的 user_id 关联,会碰到数据倾斜的问题。 + +#### 4.5. 连接 key 为不同数据类型 + +- 示例: + - 用户表中 user_id 字段为 int,log 表中 user_id 字段既有 string 类型也有 int 类型; + - 当按照 user_id 进行两个表的 Join 操作时,默认的 Hash 操作会按 int 型的 id 来进行分配,这样会导致所有 string 类型 id 的记录都分配到一个 Reducer 中。 + + +# 5. 如何解决数据倾斜 + +#### 5.1. 解决原理 + +- Hive 的执行是分阶段的,map 处理数据量的差异取决于上一个 stage 的 reduce 输出,所以如何将数据均匀的分配到各个 reduce 中,就是解决数据倾斜的根本所在。 + +- 解决数据倾斜的几个思路: + + - 业务上:避免热点key的设计或者打散热点key,例如可以把北京和上海分成地区,然后单独汇总。 + + - 技术上:在热点出现时,需要调整方案避免直接进行聚合,可以借助框架本身的能力,例如进行mapside-join。 + + - 参数上:无论是Hadoop、Spark还是Flink都提供了大量的参数可以调整。 + + +#### 5.2. 事前对连接 key 进行预处理 + +- 大多的数据倾斜都是跟业务无关的数据导致的,在读表之后第一件事就是尽可能的过滤不必要的数据,理清业务含义先过滤脏数据和业务不相关的数据。 + +- 过滤 NULL 值: + - 两个表连接 key 存在大量的 NULL 数据没有过滤,参与进了 join 的执行。 + - 其中分为主动产生与非主动产生的: + - 主动产生的空值与无效值,是值本来连接字段就存在大量空值与无效值。 + - 另一种是非主动产生的空值或无效值,比如之前的逻辑为了合并数据,用了 union all 操作把某些列设置为 null,而后面的逻辑采用该列作为 join 条件导致的数据倾斜。 + +- 过滤 “ 脏数据 ”: + - 两个表连接条件的字段数据类型不一致,不满足原有的数据类型,经过内在的逻辑处理,往往会得到与上边 NULL 相同的结果,因此在连接前,需要把连接 key 统一好数据类型。 + + +- 事前对连接 key 进行预处理最好的办法是,写 SQL 操作之前,先简单看下表的数据量级,再看 key 的空值与无效值的统计,表与表之间的关联 key,先增加空值与无效值的过滤处理; + +- 预处理方法: + - 直接去掉空值、异常值; + - 在大表关联大表出现的异常值使用 rand 函数进行随机填充。 + + +#### 5.3. map join + +- MapJoin 介绍 + - MapJoin,是在 Map 阶段进行表之间的 join 连接,而不需要进入到 Reduce 阶段才进行 join 连接,这样就节省了在 Shuffle 阶段时要进行的大量数据传输,从而起到了优化作业的作用; + - MapJoin 的 Join 操作在 Map 阶段完成,如果需要的数据在 Map 的过程中可以访问到则不再需要 Reduce。 + - MapJoin 把小表全部加载到内存,在 map 端进行join,避免 reducer 处理。 + - 用法示例: + - `select /*+ mapjoin(t)*/ f.a,f.b from A f join B t on f.a=t.a;` + +- MapJoin 原理 + - 通常情况下,要连接的各个表里面的数据,会分布在不同的 Map 中进行处理,即同一个 Key 对应的 Value 可能存在不同的 Map 中,这样就必须等到 Reduce 中去连接。 + - >要使 MapJoin 能够顺利进行,那就必须满足这样的条件:除了大表的数据分布在不同的 Map 中外,小表的数据必须在每个 Map 中有完整的拷贝。 + +- MapJoin分为两个阶段: + - 通过 `MapReduce Local Task`,将小表读入内存,生成 `HashTableFiles` 上传至 `Distributed Cache` 中,这里会 `HashTableFiles` 进行压缩; + - `MapReduce Job` 在 Map 阶段,每个 `Mapper` 从 `Distributed Cache` 读取 `HashTableFiles` 到内存中,顺序扫描大表,在 Map 阶段直接进行 Join,将数据传递给下一个MapReduce 任务。 + +- 示例: + + ``` + select c.channel_name,count(t.requesturl) PV + from ods.cms_channel c + join + (select host,requesturl from dms.tracklog_5min where day='20151111' ) t + on c.channel_name=t.host + group by c.channel_name + order by c.channel_name; + ``` + + - 上以为小表 join 大表的操作,可以使用 mapjoin 把小表放到内存中处理,语法很简单只需要增加 `/*+ MAPJOIN(pt) */`,把需要分发的表放入到内存中。 + + ``` + select /*+ MAPJOIN(c) */ + c.channel_name,count(t.requesturl) PV + from ods.cms_channel c + join + (select host,requesturl from dms.tracklog_5min where day='20151111' ) t + on c.channel_name=t.host + group by c.channel_name + order by c.channel_name; + ``` + +- mapjoin 参数 + - `hive.auto.convert.join` + - 如果是小表,自动选择 Mapjoin; + - 小表,默认25m的表是小表; + - 该参数为 true 时,Hive 自动对左边的表统计量,如果是小表就加入内存,即对小表使用Map join; + - 修改方法: + - `set hive.auto.convert.join = true; # 默认为false` + + - `hive.mapjoin.smalltable.filesize` + - 大表小表的阀值; + - 默认值是 25mb,即:`hive.mapjoin.smalltable.filesize=25000000`; + - 修改方法: + - `set hive.mapjoin.smalltable.filesize=xxxx;` + - `hive.mapjoin.followby.gby.localtask.max.memory.usage` + - 规定 map join 做group by 操作时,可以使用多大的内存来存储数据; + - 如果数据太大,则不会保存在内存里 + - 默认值:0.55; + - 修改方法: + - `set hive.mapjoin.followby.gby.localtask.max.memory.usage;` + - ` hive.mapjoin.localtask.max.memory.usage` + - 本地任务可以使用内存的百分比 + - 默认值: 0.90; + - 修改方法: + - `set hive.mapjoin.localtask.max.memory.usage;` + +- 注意:使用 mapjoin 时,一次性加载到内存中的表最多是 `8` 张; + - 如果超过 8 张小表,应该嵌套一层子循环,将多余的表在外层中写入 mapjion 里面; + +- 对于 join,在判断小表不大于 `1G` 的情况下,使用 map join; + +- MapJoin 缺陷: + - 阈值问题,导致该方案并不够通用。 + + +- skew join + - 针对前面说的 Join 倾斜问题,skew join 方案 user_id = 0 的特殊值先不在 reduce 端计算,而是先写入 hdfs,然后启动一轮 map join 专门做这个特殊值的计算,以能提高计算这部分值的处理速度。 + - skewjoin 参数: + - `hive.optimize.skewjoin` + - 告知 hive 这个 join 是 个 skew join; + - 设置方法: + - `set hive.optimize.skewjoin=true;` + - `hive.skewjoin.key` + - 告诉 hive 如何判断特殊值, + - 根据 hive.skewjoin.key 设置的数量 hive 可以知道,比如默认值是 100000,那么超过 100000 条记录的值就是特殊值。 + - 设置方法: + - `set hive.skewjoin.key=100000;` + +#### 5.5. 解决 Group By 导致的数据倾斜 + +- 对于 group by 或 distinct,启用 skewindata; +- `hive.groupby.skewindata` + - 有数据倾斜的时候进行负载均衡(默认是 false); + - 设置方法: + - `set hive.groupby.skewindata=true` +- `hive.map.aggr` + - 是否在 Map 端进行聚合,默认为True; + - 设置方法: + - `set hive.map.aggr = true` +- `hive.groupby.mapaggr.checkinterval` + - 配置在 Map 端进行聚合操作的条目数目; + - 设置方法: + - `set hive.groupby.mapaggr.checkinterval = 100000` +- 这个方案的核心实现思路就是进行两阶段聚合,Hive 在数据倾斜的时候会进行负载均衡,生成的查询计划会有两个 MapReduce Job。 + - 第一个 MapReduce Job 中,Map 的输出结果集合会随机分布到 Reduce 中,每个Reduce 做部分聚合操作并输出结果。这样处理的结果是相同的 GroupBy Key 有可能被分布到不同的 Reduce 中,从而达到负载均衡的目的; + - 第二个 MapReduce Job 再根据预处理的数据结果,按照 GroupBy Key 分布到 Reduce 中(这过程可以保证相同的GroupBy Key被分布到同一个Reduce中),最后完成最终的聚合操作。 + + +#### 5.5. 特殊值分开处理 + +- 例如:表 a 跟表 b 进行 join 操作,有数据倾斜的表是表a ,表 a 里边的倾斜的头部 key 非常少,只有 key = 1 的数据特别多,可以将 a 的数据分为两部分 a1 跟 a2,a1 中只包含 key = 1 的倾斜数据,a2 不包含数据倾斜的数据。 +- 采取 `union all + mapjoin` 的方式分而治之,把 key = a 单独抽出来作为一组用 mapjoin ,剩下的另起炉灶直接用 join,最后再把各自计算得出来的结果用 union all 拼接起来。 + +- SQL 示例: + + ```bash + select + * + from + ( + select * from logs where user_id = 0 + ) + a + join + ( + select * from users where user_id = 0 + ) + b + on + a.user_id = b.user_id + + union all + + select * from logs a join users b on a.user_id <> 0 and a.user_id = b.user_id; + ``` + +#### 5.6. 随机数分配法 + +- 数据倾斜的表头部 key 都很多,而且都是业务需要的 key,既不能过滤、也不好单拆开计算,可以考虑在头部 key 添加随机数进行打散,处理后再去掉随机数合并。 +- 最常见的倾斜,是因为数据分布本身就具有长尾性质,比如我们将日志表和商品表关联: + ```bash + select * from logs a join items b on a.item_id = b.item_id; + ``` +- 这个时候,分配到热门商品的 reducer 就会很慢,因为热门商品的行为日志肯定是最多的,而且我们也很难像上面处理特殊 user 那样去处理 item。这个时候就会用到加随机数的方法,也就是在join的时候增加一个随机数,随机数的取值范围n相当于将 item 给分散到 n 个 reducer。 +- SQL 示例: + ```bash + select + a.*, + b.* + from + ( + select *, cast(rand() * 10 as int) as r_id from logs + ) + a + join + ( + select *, r_id from items lateral view explode(range_list(1, 10)) rl as r_id + ) + b + on + a.item_id = b.item_id + and a.r_id = b.r_id + ``` + +- 上面的 SQL,对行为表的每条记录生成一个 1-10 的随机整数,对于 item 属性表,每个 item 生成 10 条记录,随机 key 分别也是 1-10,这样就能保证行为表关联上属性表。 +- 其中 range_list(1,10) 代表用 udf 实现的一个返回 1-10 整数序列的方法。 +- 这个做法是一个解决 join 倾斜比较根本性的通用思路,就是如何用随机数将 key 进行分散,具体可以根据具体的业务场景做实现上的简化或变化。 + +#### 5.7. 解决小表过大无法 map join 问题 + +- 当小表数据过大(比如大于 1G),或者小表条数过多(比如数百万行),就不再适用于之前的 map join 方案; +- 如果小表很大,map join 会出现 bug 或异常,这时就需要特别的处理; +- 例如: + - users 表有 600w+ 的记录,需要和 log 数据进行 join,把 users 分发到所有的 map 上是个不小的开销,而且 map join 不支持这么大的小表。 + - 如果用普通的 join,又会碰到数据倾斜的问题,因为 log 里 user_id 有上百万个,这就又回到原来 map join 问题。 + +- 解决: + - 思路: + - 由于每天的会员 uv 不会太多,有交易的会员不会太多,有点击的会员不会太多,有佣金的会员不会太多,所以可以先抽取 log 中出现的 distinct user_id; + - 之后将 log 有出现的 distinct user_id,再与 log 进行 join; + - 这个思路,能解决很多场景下的数据倾斜问题。 + - SQL 示例: + ```bash + select + /*+mapjoin(x)*/ + * + from + log a + left outer join + ( + select + /*+mapjoin(c)*/ + d.* + from + ( + select distinct user_id from log + ) + c + join users d + on + c.user_id = d.user_id + ) + x on a.user_id = b.user_id; + ``` + +#### 5.8. 处理连接 key 为 NULL 空值 + +- 解决方法: + - 赋与空值分新的 key 值; + +- SQL 示例: + ```bash + select + * + from + log a + left outer join users b + on + case + when a.user_id is null + then concat(‘hive’, rand()) + else a.user_id + end = b.user_id; + ``` + +#### 5.9. 处理连接 key 为不同数据类型 + +- 解决方法: + - 把不同数据类型转换为同一类型,数字类型转换成字符串类型 + +- SQL 示例: + ```bash + select + * + from + users a + left outer join logs b + on + a.usr_id = cast(b.user_id as string) + ``` + +#### 5.10. 提高 Reduce 任务的并行度 + +- 将 Reduce Task 的数量变多,就可以让每个Reduce Task分配到更少的数据量,这样可以缓解数据倾斜的问题。 +- 在调用groupByKey、countByKey、reduceByKey时,传入Reduce端的并行度参数,这样在进行Shuffle操作时,会创建指定的Reduce Task,可以让每个Reduce Task分配到更少量的数据,避免了Spark作业时OOM的情况。 + +![]({{site.baseurl}}/img-post/数据倾斜-1.png) + +#### 5.11. 其他方法 + +- 压缩文件 + +- Spark 参数 + - 使用map join 代替reduce join + - 提高shuffle并行度 + +- Flink 参数 + - MiniBatch设置 + - 并行度设置 + + + diff --git "a/_posts/2022-01-27-BI\357\274\232BI \345\273\272\350\256\276\347\232\204\346\255\245\351\252\244\345\222\214\350\246\201\346\263\250\346\204\217\347\232\204\351\227\256\351\242\230.md" "b/_posts/2022-01-27-BI\357\274\232BI \345\273\272\350\256\276\347\232\204\346\255\245\351\252\244\345\222\214\350\246\201\346\263\250\346\204\217\347\232\204\351\227\256\351\242\230.md" new file mode 100644 index 00000000000..b59e4afad10 --- /dev/null +++ "b/_posts/2022-01-27-BI\357\274\232BI \345\273\272\350\256\276\347\232\204\346\255\245\351\252\244\345\222\214\350\246\201\346\263\250\346\204\217\347\232\204\351\227\256\351\242\230.md" @@ -0,0 +1,287 @@ +[//]: # (---) + +[//]: # (layout: post ) + +[//]: # (title: BI:BI 建设的步骤和要注意的问题) + +[//]: # (subtitle: ) + +[//]: # (date: 2022-01-27) + +[//]: # (author: dex0423) + +[//]: # (header-img: img/post-bg-os-metro.jpg) + +[//]: # (catalog: true) + +[//]: # (tags:) + +[//]: # ( - BI) + +[//]: # (---) + + +BI的应用深度可划分为: + +数据可视化、数据分析、数据挖掘三个阶段。 + +在数据可视化阶段,BI的作用是将企业日常业务数据报表以可视化图表的方式予以呈现,只是单纯用可视化图表代替了Excel报表,但缺少对数据的分析。 + +在数据分析阶段,BI可以实现对可视化图表中的数据进行描述性统计分析、关联分析等,发现数据背后的原因,实现数据辅助业务决策。 + +在数据挖掘阶段,通过算法对数据进行深度挖掘和预测分析,BI和AI结合,能够对未来业务进行预测分析,实现智能业务决策。 + +# 技术选型 + +在技术选型阶段,首先需要关注的就是企业的数据量级,亿级、千万级、百万级数据,对应的技术方案是完全不同的。 + + +# 数仓技术选型 + +#### 数仓是使用第三方服务如 DataWorks,还是自己从零搭建!!! + +- 开源免费的,其实是最贵的。 + +# 核心业务指标 + +最核心的是业务和财务。 + +在以往企业信息化建设中,经常强调“业财一体化”,其核心是把业务和财务数据打通。不少企业财务管理软件,也会在信息流基础上对沉淀的数据进行分析和展示。这其中,固然在这里有一部分 BI 的影子,比如在财务管理系统中的“阿米巴”模式下,可以基于部门(团队)、产品线等,分析各个业务单元的利润情况。 + +但需要注意的是,BI需要面对的问题要远比这复杂得多,绝不是靠一套企业管理软件就能解决的。 + +先有业务指标,再有财务指标。 + +#### 业务 + 财务 + +#### 产品 + +#### 销售 + +#### 渠道 + +#### 广告 + +#### 库存 & 周转 + +#### 供应链 + +#### 物流 + +#### 财务的两个维度 + +#### 管理会计:核心是利润 + +#### 财务会计:核心是现金流 + + +# BI 要解决什么问题 + + +# 如何开展 BI 建设 + +对于很多准备或者正在规划商业智能BI项目的企业来说,业务分析需求的梳理是整个项目开始的第一步,往往也是最困难的,主要表现如下:业务部门往往提不出比较具体的分析需求,而IT部门很难深入到业务,也提不出适合业务部门的分析需求。 + +# 数据采集 + +#### 用户行为数据 + +#### 业务订单数据 + +#### 主数据 + + + + +# 数据建模 + +> MySQL 建表一般都是小表,数据冗余小,磁盘利用率高。但这种情况并不适合大数据分析,因为做 MapReduce 的时候,大量的跨表join会导致 MySQL 运行极度缓慢,查询操作性能极差。 + +#### 关系模型 + +#### 维度模型 + +数据源层:亦即各个业务系统。 + +- ODS层: + + 基本保持与数据源层一致的数据表结构,但是会按某种方式(存量、增量)某个频率(按天、按小时、按分钟)集成各个业务系统的数据到一个统一的地方。 + +- DW层: + + 会按某一个或某几个统一的字段信息(比如:订单号,保单号,账号等),整合各个业务系统的数据到一起数据的颗粒度还是比较细节级别的。 + +- DM层: + + 会面向主题分部门分模块进行数据的汇总组合。 + +- DA层: + + 面向最终的上层应用。 + +# 数据模型 + +和数据连接一样要考虑到复用性 + +数据建模可以通过多表的关联实现复杂建模,但是实现不等于最优实践。 + +BI数据建模一般一个模型中建议不超过5个表关联,这个主要出于后期维护考虑。 + +如果存在特殊场景,需要10几张表做可视化建模,建议用自定义sql 先把一些复杂的逻辑在自定义sql完成。用自定义sql在可视区域建模。 + +数据模型最后是以宽表展示,理论上一个宽表字段不要超过30个,以服务客户的经验来说,一般模型层字段在报告上使用在5-30个之间。 + +这里也要说一下性能,性能提升有很多种方式,最笨的方法就是在每个细节部分都注意到。 + +对于不需要用到的字段尽量的不要在宽表中展示,这也是复杂场景下,推荐用自定义sql的原因。 + +对于数据层面,可用范围要极尽缩减,做到用多少取多少。举个例子:如果你要分析本月的订单数据,但实际上提供的表是所有订单数据。此时建议在模型层对数据进行缩减。 + + +![]({{site.baseurl}}/img-post/bi-2-1.jpg) + +实际业务中,具体的建设实施,一般是采用自底向上的模式比较多,这个模式偏业务驱动,都是先有特定分析需求或特定报表需求开始,然后再到数据平台。针对特定部门,特定需求,先做一些部门的关键指标报表(尤其到现在的互联网公司更是采用敏捷开发,快速迭代),逐步再演变重构平台,所有这些在统一平台前都是会由一些特定的需求或报表而先使用起来,效果出来后,再统一规划重构数据中心、数据平台,这时的实施模式就会由原来的自底向上转变为站在更高角度的自顶向下模式了 + +# 流程规范 + +流程规范主要有资源创建规范,命名规范,权限分配规范。 + +1.资源创建规范:上面讲到的业务切割,也是一种资源创建规范的一种。在有数BI的项目中资源有数据连接,数据模型,报告。对于这些创建原则尽量用文件夹隔离,业务细分用子文件夹。对于一些公共业务模块可以见公共文件夹块。 + +2.命名规则规范:要做到顾名思义。在资源创建的时候,首先面临的是命名规范。 + +(1)资源创建命名规范:对于一些小型主题分析,尽量数据连接、模型、报告的文件夹命名保持一致。如图所示: + +如果在小型主题分析中有多个细分业务。由于数据连接支持1级文件夹,数据模型和报告支持多级文件夹创建。所以命名规范如下: + +为什么命名规则不采用:如一级业务-二级业务-具体业务-创建日期-创建人 这样的方式命名?原因有两个: + +逻辑虽然清晰,但是命名规范复杂。用文件分级形式有交互; + +对于创建人,创建日期等信息可以通过后台监控方式获得,所以可以不用需要在命名中体现。 + +(2)字段(指标)命名规范:在BI项目中也非常重要,可以参考数仓设计字段命名规范。但是通常在大型项目中如果存在主题交叉的时候,但又要防止烟囱式开发,指标的复用是很突出的问题。 + +具体如何对指标进行管理,这里推荐一门课程,由郭忆老师在极客时间的《数据中台实践》。该课程在第五章具体介绍了指标管理的方法。 + +3.权限分配规范。如果在中大型企业中简单的权限分配往往满足不了需求。我们需要采用权限分级策略。系统内部权限设计如下: + +对于小主题业务生产者或者业务方。一般授予的是一级或者二级权限,以及数据权限。 + +所以命名规则主要是针对一级角色和二级角色以及数据权限的命名。 + +(1)角色权限命名规则:级别_文件夹名_团队_权限 + +如:一级角色_数据BU分析_数据分析师_编辑 + +(2)数据权限命名规范 数据链接名称_表_字段 + +如:超市系统_订单表_地区 + +# 数据连接 + +如果在一个项目中业务较多,要考虑数据连接的复用性;同时对于数据权限的限制,优先考虑通过数据连接中的用户权限来控制。而不是用BI的权限控制。 + +举个例子:如一个公司的重点业务数据都存在某一数据库下面,那么不同部门,不同团队关注的表是不一样的。此时建议先在数据库中创建用户。通过用户来控制业务范围。 + + + + +# 报告展示 + +#### 展示报表 + +- 单图(chart) + + 单图主要是对指标进行某种样式的展示,例如日活的折线图、日活的表格、多平台日活对比图等,并可以对单图进行多个维度的查询操作。 + 它提供了: + - 维度:可以选择多个维度,向下进行钻取 + - 时间:可以选择昨天、过去7天、过去30天、过去90、过去180天、过去365天以及自定义天数 + - 图表样式:目前支持折线图、横向柱图、竖向柱图、表格、地图、饼图等图表 + +- 看板(dashboard) + + 看板能够帮助将相互关联的单图集合在一起,兼顾全面性与单独性,既能够从多个图表中发现关联,也可以对单个图表进行深入分析,方便每天查看相应的数据。 + 看板可以供不同的业务人员实现不同的使用场景: + - 产品经理的看板可能是项目的核心指标 + - 市场人员的看板可能是监控各个渠道来源指标 + - 销售的看板可能是潜在客户的活跃度 + +#### 自助分析 + +业务人员的需求是多种多样的,如果这些需求都让负责BI平台的产品经理来配置的话,既增加工作量,又有很大的沟通成本,这时候,业务人员就需要一个能够自己在平台上快速方便搭建报表的方式。 + +自助分析功能的核心是创建单图功能,使用人员可以选择图表样式,现在常用的图表类型有表格、折线图、柱状图(横向柱图、竖向柱图)、饼图、漏斗图、堆积图等,然后选择数据源里的数据表,把对应的数据表中的字段拖拽到时间、维度、指标栏中,然后选择查询便可以在显示区进行预览,还可以设置过滤条件,进行一些维度的过滤,并可以设置是否在前端显示。 + +#### 功能性分析工具 + +一个完善的BI平台,不仅仅是单纯展示数据的,还要能够能为数据分析师、业务人员提供一些常用的数据分析工具,例如用户行为路径、用户分群与用户详情、系统监控等工具,可以方便使用人员方便快捷的分析更精细的业务场景。 + +以用户分群和用户细查为例,日常中经常需要把满足某个或者某些条件的用户区分出来,然后查看这批用户的一些关键指标以及一些行为事件等,例如,想了解iOS平台上,最近五天内连续沉默的用户,使用人员选择这些条件组合后,就可以获取一批userid的列表,让后查看每个userid的用户属性、用户行为轨迹、用户活跃度趋势、用户阅读文章列表等信息,由于不方便透露一些用户信息,用户细查页面就以原型图的形式给予示例,见下图。当然获取某些条件下的userid对集群来说是有一定的计算压力的,要等一些时间计算完成后才能给用户显示。 + +#### 业务场景模板 + +BI数据系统是要更方便的服务于不同的业务场景进行数据分析的,每个业务场景总会沉淀下来一套固定的分析思路和分析架构,这套固定的分析架构就可以放在BI平台上来实现,例如渠道分析、用户留存分析、用户活跃分析及日常的周月报等。通过分析模板,可以方便快速的查看数分析数据,提高效率。 + +例如活跃用户分析来说,根据平时的分析习惯,一般要将活跃用户拆解为不同的活跃用户群体,进一步查看活跃用户的构成及这部分用户的变化情况,从而针对每部分的不同群体进行优化和分析。例如可以按照下图的分析框架创建一个看板(dashboard),由一下七个单图(chart)组成一个日常的分析模板。 + +# 报告 + +1个报告的模型尽量不要超过10个。如果超出,可以考虑分报告实现,模型尽量复用。 + +报告是加载当前页面的组件,所以一个报告页组件不要超过20个。能分报告页,尽量不要用tab页组件。 + +筛选器等组件尽量不要用数据量较大的数据模型。建议用维表。 + +对于开发阶段建议把报告放在私人文件夹下开发。等报告验证完成复制到公共区域。 + +对于离线分析报告,建议做预加载处理。 + +是BI的核心,一句话总结:报告是一个商业产出可以用价值衡量,所以要看性价比,因此大方向上需要做到4点。 + +# 即席查询 + +#### 实施指标分析 + +#### BI 作为企业经营管理辅助工具,是在企业信息化建设已经很完善的基础上 + +#### BI 的第一步:数据可视化 + +BI 给人的最直观的感受,就是可视化大屏上炫丽的图形。这些图形只是做了图形化的展示,尽管并未对数据做任何处理,但清晰、易懂、直观、友好的图形,本身就是企业管理的一大飞跃。 + +人是视觉动物,同时人的注意力、理解能力又是有限的,用图形来给管理运营人员展示数据,要比给他们一对密密麻麻的Excel报表,效率的提升绝对是倍数级的。 + + +#### BI 的第二步:决策智能化 + +#### BI 决策智能化,服务的是企业经营 + +BI 的核心价值,在于决策的智能化。 + + +#### BI 决策智能化,本质是企业经营决策的知识沉淀。 + + + +#### BI 决策智能化,需要能超越过往经验,发现并解决以往处理不了的问题。 + +在 BI 项目实践中,笔者不止一次遇到过业务部门提出需求,就是领导层觉察到某项业务指标存在问题,但就是不知道原因出在哪里。在这类需求中,成百上千的 Excel 报表、数十万甚至上百万条数据,把业务人员折磨的死去活来,分管的部门领导面对问题也是一头雾水,被大领导追着问、却根本给不出清晰有说服力的答案,更给不出解决方案。 + +尤其是在一些涉及到成本费用的领域,有问题却找不到原因是一个很敏感的问题。领导永远觉得钱花得太多,也不知道钱到了那里去,中层领导只能很模糊的掌握一些信息,但是给不出数据证实自己的判断。而企业规模的快速增长,又给内部的员工吃回扣等行为提供了温床,这个时候管理层面临的压力将是空前的。我想,这也是为什么很多企业,在业务相关的 BI 建设还没完成时,甚至刚刚建好销售 BI,就立马开动做财务 BI 的重要原因。 + +在这类问题中,需要 BI 团队尤其是数据分析人员,对业务进行深度介入。归因分析是个体力活,需要从海量的数据中,对问题的线索进行仔细梳理,追根溯源找到问题发生的根源,并把导致结果的路径链条理清楚,并最终用清晰地图表展示出来。这对于数据分析师的个人综合能力要求是很高的,数据分析师的数据分析能力、对于业务的学习理解能力、以及科学严谨的工作态度,都是缺一不可的,这也是 BI 项目能否有更高突破的关键。 + + +![]({{site.baseurl}}/img-post/fwq-1-1.jpg) + +在企业 BI 建设过程中, + +当然,这并不是要求一定要有数据中台才能开展 BI, + + + +,并且数据中台建设一定是要服务于 BI 建设的,而不是反过来。 + +需要注意的是,在数据中台的建设理念看来,BI + +中台的价值在于响应业务需求,这一点“阿里去中台化”的教训已经足够深刻。而对数据中台来说,其核心价值的第一要务就是服务于 BI,BI 团队明白这一点尤为重要。 \ No newline at end of file diff --git "a/_posts/2022-01-27-BI\357\274\232BI \347\232\204\345\237\272\346\234\254\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-27-BI\357\274\232BI \347\232\204\345\237\272\346\234\254\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..eca81149d55 --- /dev/null +++ "b/_posts/2022-01-27-BI\357\274\232BI \347\232\204\345\237\272\346\234\254\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,133 @@ +--- +layout: post +title: BI:关于 BI 的基本概念详解 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - BI +--- + + +# 1. BI 的定义 + +BI,是 Business Intelligence 的缩写,翻译过来为商业智能,简称 BI。 + +![]({{site.baseurl}}/img-post/bi-6.png) + +商业智能的概念在1996年最早由加特纳集团提出,加特纳集团将商业智能定义为:商业智能描述了一系列的概念和方法,通过应用基于事实的支持系统来辅助商业决策的制定。商业智能技术提供使企业迅速分析数据的技术和方法,包括收集、管理和分析数据,将这些数据转化为有用的信息,然后分发到企业各处。 + +BI提供了一套完整的解决方案,把企业不同业务系统(如OA、ERP、CRM、SCM、FMS等)的数据提取出有价值的部分进行整合清洗,在保证数据准确性的同时,进行数据分析和处理,利用查询和分析工具快速、准确地为企业提供报表展示与分析。 + + +# 2. BI 的内涵 + +#### 2.1. BI 概念的四大范畴 + +通过上面的定义,我们大致可以勾画出 BI 的概念雏形。 即 BI 大致包含下面四大范畴: +- 可视化展示; +- 数据的分析; +- 数据的管理; +- 数据的收集。 + +![]({{site.baseurl}}/img-post/bi-1.png) + +上面所说的四大范畴,其实从上往下是层层依赖的,整个链条是数据从产生到创造价值、再到展示价值的全过程。 + + +#### 2.2. 数据连接模块 + + 需要支持连接多种数据源,如关系型数据库MySQL、Oracle,通过 JDBC 的方式直接连接数据库。同时支持Excel本地文件上传以及大数据平台如ClickHouse对接等。 + +#### 2.3. 数据准备模块 + + 通常数据分组分包、可视化ETL等功能,主要是将连接到BI系统的数据进行整合、建模。将数据源内的原始工作表,按需取数放入数据包中,在可视化ETL中将工作表行列转化、合并、过滤等操作,加工成业务分析所需要的工作表。 + +#### 2.4. 仪表板模块 + + 仪表板模块是业务人员使用频率较高的模块,通常具有可视化分析、多维交互分析、可视化编排等功能。它主要是将数据字段映射到可视化图形,同时提供多维交互分析,最终呈现可视化结果。 + + 可视化分析主要是将工作表数据字段通过拖拽操作映射到图表维度、指标上,从而绘制出各类图表,常见的图表类型有趋势类、占比类、分布类、空间类、关系类等。 + + 多维交互分析(也是OLAP分析),是通过上卷、下钻、旋转、切片/切块等操作对数据进行多维度分析。 + + 可视化编排是将分析图表、页面组件等进行布局美化,让数据可视化结果更加清晰美观,同时具有更多的页面组件增加分析能力。 + +#### 2.5. 数据应用模块 + + 将数据分析的可视化结果,以不同应用的方式分发,通常有驾驶舱、数据门户、移动BI、外部链接、预警通知等功能。 + +#### 2.6. 系统管理模块 + + 系统管理模块主要是对整个系统内用户、权限以及资产的管理,能够实现对数据的行列级权限管控,保障数据稳定建设的同时,保障数据安全性。 + +![]({{site.baseurl}}/img-post/bi-7.png) + + +# 3. BI 的价值 + +#### 3.1. 数据分析和展示 + +从分析形式看,由静态到动态,BI 分析可以分为查询、报表(Reporting)、图表(Charts)、联机分析(OLAP/Analytics)、数据挖掘(Data Mining)以及预测性分析(Predictive)等类型, + +具体满足的业务分析主题,既有战略性课题,面向企业高层管理人员,例如经营战略、战略财务、营销战略等,也可以有战术性课题,面向业务专家、中低层管理人员,例如客户/商品分析、供应链效率、成本分析等等。 + +![]({{site.baseurl}}/img-post/bi-2.png) + + +#### 3.2. 数据分析的四种类型 + +按照数据使用的成熟度,数据分析可分为描述性分析、诊断性分析、预测性分析、指示性分析四种类型: + +- 描述性分析: + + 回答“发生了什么”,集成历史数据并进行报表、图表等形式的展示,可以描述业务中的关键指标,但无法解释问题发生的原因; + +- 诊断(归因)性分析: + + 回答“为什么会发生”,在描述性分析的基础上进行可视化探索,通过联动、下钻、挖掘等手段,找到影响因子和关联关系,解释问题发生的原因; + +- 预测性分析: + + 回答“将会发生什么”,借助统计学技术,利用预测模型、机器学习、数据挖掘等,基于描述性和诊断性分析的结果探索过去规律,对未来进行动态预测; + +- 指示性分析: + + 回答“最好发生什么”,通过算法实现问题的最优解,针对预测性分析的结果制定应对措施,消除未来可能发生的风险或赢得有利的机会。 + +![]({{site.baseurl}}/img-post/bi-5.png) + +#### 3.3. BI 核心需求 + +- 经营决策 + - 历史数据分析 + - 增长趋势预测 + - 跨部门、跨业务分析 + - 经营问题诊断 +- 日常管理 + - 数据日报 + - 统计日报 + +# 4. 主流的 BI 厂商 + +![]({{site.baseurl}}/img-post/bi-3.png)BI 现阶段的应用情况 + +# 5. BI 的应用情况 + +#### 6.1. 金融和零售行业 + +金融和零售两个行业在BI应用成熟度高:数据基础设施良好,且已进入数据挖掘的应用阶段。金融行业对BI的典型应用场景包含营销、风控、财富管理等方面,与业务相关程度高、预算投入比例也高;零售行业同样将BI应用到核心业务场景,如销售管理、用户营销、忠诚度管理等。但由于零售行业IT投入能力不及金融,市场规模较小。 + +#### 6.2. 制造和电力行业 + +制造和电力两个行业BI应用成熟度中等。其中制造行业企业数量庞大,但数据基础设施参差不齐,领先企业已经进入数据挖掘阶段,但总体制造业企业仍在数据分析阶段,通过BI优化成本控制、进行流程监控;电力行业则以国家电网为代表,各区域在BI应用上各自投入,总体投入规模、应用深度都有很大提升空间。 + +#### 6.3. 政务和教育行业 + +政务、教育行业的BI应用尚处于初级阶段,目前主要是对数据进行可视化展示,数据分析的程度有待加深。 + +![]({{site.baseurl}}/img-post/bi-1-1.png) + +由于各行业BI应用成熟度处于不同水平,对于BI的需求也存在差异。因此,厂商需要具备综合产品服务能力以支撑跨行业服务需求。 \ No newline at end of file diff --git "a/_posts/2022-01-27-BI\357\274\232\347\224\265\345\225\206\345\215\226\345\256\266\347\261\273\344\274\201\344\270\232 BI \345\233\242\351\230\237\345\273\272\350\256\276.md" "b/_posts/2022-01-27-BI\357\274\232\347\224\265\345\225\206\345\215\226\345\256\266\347\261\273\344\274\201\344\270\232 BI \345\233\242\351\230\237\345\273\272\350\256\276.md" new file mode 100644 index 00000000000..470a3713be3 --- /dev/null +++ "b/_posts/2022-01-27-BI\357\274\232\347\224\265\345\225\206\345\215\226\345\256\266\347\261\273\344\274\201\344\270\232 BI \345\233\242\351\230\237\345\273\272\350\256\276.md" @@ -0,0 +1,166 @@ +--- +layout: post +title: BI:电商卖家类企业 BI 团队建设 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - BI +--- + + +# 1. BI 四类岗位 + +- 针对 BI 的四个范畴,在企业组织架构中,也分别对应了一个小型 BI 团队的四个类型的岗位,分别是数据采集、数仓开发、数据分析和可视化开发; +- 这四类岗位的工作职能,会存在部分的重叠,需要做好分工和协作安排; +- 同时,岗位所需的技术栈很多也是通用的,比如 python、SQL,可以根据业务需要,调整岗位设计和人员职能,不要一上来就要求全部配齐人员,却工作量不饱和; + +>组建团队时要注意,人员的技术栈尽可能全面一些,在工作较忙时可以内部协调、相互帮助,比如数据采集就可以协助做 ETL,可视化最好也懂一点 SQL 和数据分析。 +> +> **注意:在管理上,可以专人专责,但尽量不要专人专职。** + +#### 1.1. 数据采集岗 + +- 第三方数据(部分职能与数仓工程师重叠): + - RPA; + - API; +- 业务数据: + - 手动报表上传; + - 数据库导入等; +- 埋点数据; +- 日志数据; +- 爬虫数据 + +#### 2.2. 数仓开发岗 + +- DBA: +- 数仓: + - Hadoop 生态圈; +- ETL; + - 部分工作会与数据采集职能重叠; +- OLAP; +- 数据湖: + - OSS 等; + +#### 2.3. 数据分析岗 + +- SQL; +- python +- 数据挖掘; +- 算法模型; + +#### 2.4. 数据可视化岗 + +- PowerBI; +- Tableau; +- FineBI; + +# 2. BI 团队搭建 + +#### 2.1. 常规 BI 团队 + +- 需要指出的是,在不同的行业、不同企业组织架构不尽相同,对于 BI 的需求和定义不尽相同,BI 各个模块具体职能实际可能是由不同团队负责的,并不一定都在 BI 团队独立负责。 + +- 尤其是不少企业,数据中台的建设比较完善,数据采集、数据管理的一部分工作,事实上是由多个部门来承担的,比如有专门的爬虫、RPA团队负责外采数据,有专门的的大数据团队负责数仓的统一管理。 + +#### 2.2. 电商 BI 团队 + +- 就大多数的电商卖家企业来说,数据中台建设普遍起步比较晚,且数据量、企业规模都比互联网企业低了一个量级,一般来说具体的数据采集和管理,仍是由 BI 团队负责。 + +- 要注意的是,部门间的沟通协调是非常消耗时间精力的,这无形中会增加企业 BI 建设的成本。 + +>最理想的状态,是一定要保证 BI 团队和职能的完整性,即使职能和相关部门有一定的重叠也是合理的,关键是要厘清分工与协作关系。 + +# 3. 工程师的能力要求 + +- 基本的数据处理能力 + - ETL 架构设计 + - ETL 工具操作 + - SQL 查询及调优 + - 复杂存储过程 + +- 数据仓库建模能力 + - 业务指标体系梳理 + - 维度建模设计 + - 多数据源、多层架构的设计开发能力 + +- 原型设计能力 + - 可视化报表原型设计 + +- 业务理解能力 + - 理解业务指标背后意义 + - 业务场景 + - 分析逻辑 + +- 项目管理能力 + - 项目管理规范化、流程化、制度化 + - 需求梳理 + - 计划资源 + - 交付质量 + - 文档输出 + +- 沟通能力 + - 语言表达 + - 理解能力 + - 文档撰写能力 + +# 4. 外部沟通协调机制 + +- 在多数情况下,BI 需求是企业高管、或者前台业务部门提出的,BI 部门直接面对高层领导和前台业务部门; +- 同时 BI 需要的数据、开发资源又掌握在后台开发部门,需要开发部门提供资源和信息支撑; +- 这些决定了 BI 部门需要常态化的多向沟通。 + +#### 4.1. 业务接口人 + +- BI 需求一般是由企业高层领导或者部门的领导提出,但多数时候会有一个专门的人员,和 BI 部门进行具体的细节对接; +- 接口人不一定能充分理解领导意图、或业务部门的需求,一定要注意多与领导沟通,了解真实意图; +>这些年,随着企业信息化、数字化建设的发展,越来越多的企业业务部门,会配备有专门的数据运营岗,负责处理各种数据、报表,用以支撑部门的管理运营工作。这类人员有一定的数据分析能力,同时又比较了解业务,是比较理想的业务接口人。 + +- 业务接口人职责: + - 需求整理 + - 数据源梳理 + - 统计逻辑梳理 + - 报表需求整理 + - 资源协调 + - 提供账号、权限、数据、文档等 + - 协调第三方服务商 + - 协调业务团队提供支持 + +#### 4.2. 内部数据接口人 + +由于企业信息化建设情况不一,多数企业会存在数据来源多、数据类型多等问题,需要梳理清楚各个 BI 的数据来源,厘清对应的负责人,并且要将其纳入 BI 建设项目协调机制中来,防止出现要数据找不到人、或者对方不配合的情况。 + +- IT 部门的 DBA + - DBA 主要的职责,是在数据库层面,为 BI 团队提供支撑。 + - 具体包括: + - 数据库账号密码、读取权限; + - 数据字典:数据库表关系、字段属性、元数据等; + - 数据库运维支撑; + +- 业务部门第三方系统管理人员 + - 第三方软件的系统管理工作,一般是由 IT 部门负责的,但也有的会安排在业务部门。 + +#### 4.3. 第三方服务商接口人 + +- 一般来说,电商卖家的 ERP、SCM、财务软件、OA大多是外采的产品,第三方服务商交付以后,会安排售后与 BI 对接; + +>要注意的是,如果外采的软件涉及到二次开发,那工作中真正要对接的,其实是负责开发的 DBA 或写 SQL 的开发人员,只有他们才清楚表结构设计和具体的业务逻辑。 + +# 5. 与数据中台关系 + +#### 5.1. 数据中台概念介绍 + + 数据中台的概念是最早由阿里巴巴首次提出,其最核心的是 OneData 体系。这个数据管理体系包括:全局数据仓库规划、数据规范定义、数据建模研发、数据连接萃取、数据运维监控、数据资产管理工具等。 数据中台对海量数据进行采集、计算、存储、加工,同时统一标准和口径。数据中台把数据统一之后,会形成标准数据,再进行存储,形成大数据资产,进而为企业不同部门设置外部企业提供高效服务。 + + 数据中台的本质,就是“数据仓库+数据服务中间件”,它不仅为数据分析挖掘而建,同时作为各个业务的数据源,为业务系统提供数据和计算服务。在这个过程中,数据中台不仅要解决“读”的问题,也要解决“写”的问题,需要的是一整套的数据治理规范。这一点在“主数据管理”和“交易数据管理”上,都能得到很好的体现。 + +#### 5.2. BI 与 数据中台 的关系 + +相比于数据中台,BI 要做的数据分析和展示,则主要是是围绕着“读”数据展开,BI 强调的是利用数据的能力。理解了这一点,也就清楚了数据中台对于 BI 的基础性作用。 我们也可以简单的理解为:数据中台提供数据,BI 使用数据,BI 对数据中台是有依赖的。 + +需要补充的是,BI 建设和数据中台建设,是需要对方的全程参与的,绝不能闭门造车。比如表结构设计、字段设计、数据获取逻辑等等,这些看似不起眼的问题,如果沟通不畅很容易出问题。尤其是很多企业的数据中台建设起步比较晚,很多都是先开始做 BI,后面才开始做数据中台,这就导致了一系列的问题。 + +举个最简单的例子,笔者就曾经遇到过 BI 团队为方便数据分析,建表时、表名和字段名都使用汉字,但后来数据中台推出的数据库上线后、却根本不支持汉字,这就让数据中台的建设失去了意义。 + diff --git "a/_posts/2022-01-27-FineBI\357\274\232FineBI \345\261\225\347\244\272\346\237\220\350\275\257\344\273\266\350\201\212\345\244\251\346\225\260\346\215\256.md" "b/_posts/2022-01-27-FineBI\357\274\232FineBI \345\261\225\347\244\272\346\237\220\350\275\257\344\273\266\350\201\212\345\244\251\346\225\260\346\215\256.md" new file mode 100644 index 00000000000..4fb95b67dcc --- /dev/null +++ "b/_posts/2022-01-27-FineBI\357\274\232FineBI \345\261\225\347\244\272\346\237\220\350\275\257\344\273\266\350\201\212\345\244\251\346\225\260\346\215\256.md" @@ -0,0 +1,58 @@ +--- +layout: post +title: FineBI:FineBI 展示某软件聊天数据 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - FineBI +--- + + +# 1. Fine BI 配置 + +- 添加本地 jar 包 + ![]({{site.baseurl}}/img-post/finebi-12.png) + +- 上传 Hive 插件: + + ![]({{site.baseurl}}/img-post/finebi-3.png) + + +# 2. 数据准备 + +- 连接 Hive + ![]({{site.baseurl}}/img-post/finebi-4.png) + +- 准备数据 + ![]({{site.baseurl}}/img-post/finebi-5.png) + +- 更新数据包 + ![]({{site.baseurl}}/img-post/finebi-6.png) + +# 3. Fine BI 编辑 + +![]({{site.baseurl}}/img-post/finebi-2.png) + +![]({{site.baseurl}}/img-post/finebi-7.png) + +- 地图经纬度设置 + - 注意:如果不设置经纬度,会导致地图无法正常展示! + + ![]({{site.baseurl}}/img-post/finebi-8.png) + + ![]({{site.baseurl}}/img-post/finebi-9.png) + +- 饼图绘制 + + ![]({{site.baseurl}}/img-post/finebi-10.png) + +- 折线图绘制 + ![]({{site.baseurl}}/img-post/finebi-11.png) + + +# 4. 完整效果 + +![]({{site.baseurl}}/img-post/finebi-1.png) \ No newline at end of file diff --git "a/_posts/2022-01-27-FineBI\357\274\232Hive SQL \345\210\206\346\236\220\346\237\220\350\275\257\344\273\266\350\201\212\345\244\251\346\225\260\346\215\256.md" "b/_posts/2022-01-27-FineBI\357\274\232Hive SQL \345\210\206\346\236\220\346\237\220\350\275\257\344\273\266\350\201\212\345\244\251\346\225\260\346\215\256.md" new file mode 100644 index 00000000000..ee119392fd1 --- /dev/null +++ "b/_posts/2022-01-27-FineBI\357\274\232Hive SQL \345\210\206\346\236\220\346\237\220\350\275\257\344\273\266\350\201\212\345\244\251\346\225\260\346\215\256.md" @@ -0,0 +1,343 @@ +--- +layout: post +title: FineBI:Hive SQL 分析某软件聊天数据 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - FineBI +--- + + +# 1. 结果展示 + +![]({{site.baseurl}}/img-post/finebi-1.png) + +# 2. 需求梳理 + +#### 2.1. 需求指标 + +- 基于用户聊天数据,统计以下指标信息 + + - 统计今日总消息量 + - 统计今日每小时消息量、发送和接收用户数 + - 统计今日各地区发送消息数据量 + - 统计今日发送消息和接收消息的用户数 + - 统计今日发送消息最多的Top10用户 + - 统计今日接收消息最多的Top10用户 + - 统计发送人的手机型号分布情况 + - 统计发送人的设备操作系统分布情况 + +#### 2.2. 技术方案 + +- 数据存储:Hadoop Hive +- 分析语言:Hive SQL +- 数据展示:FineBI + +# 3. 数据梳理 + +- 样例数据共两个文件 + - 文件格式 .tsv + - 制表符诶 tab 键 `\t` +- 每行数据包括以下信息: + - 消息发送时间 + - 发送人昵称 + - 发送人账号 + - 发送人性别 + - 发送人 ip 地址 + - 发送人操作系统 + - 发送人手机型号 + - 发送人网络类型 + - 发送人的 GPS 定位 + - 接收人昵称 + - 接收人 IP + - 接收人账号 + - 接收人操作系统 + - 接收人手机型号 + - 接收人网络类型 + - 接收人的 GPS 定位 + - 接收人性别 + - 消息类型 + - 双方距离 + - 消息内容 + +- 样例数据 + +``` +2021-11-01 07:44:37 郯乐游 1825138366359 男 195.188.222.255 IOS 9.0 OPPO A11X 5G 123.257181,48.807394 梁丘雨琴 136.66.109.160 1523699980735 Android 7.0 华为 荣耀9X 4G 89.332566,42.956064 女 TEXT 5.14KM 你如那出水的芙蓉,亭亭玉立[lizhigushicom]。你是那样地美,美得让我无法自拔,笑容如春天般清丽秀雅,秀发如柳丝细腻顺滑,眼眸如涟漪百媚丛生。宝贝,对我来说你是最好的。 +2021-11-01 07:29:22 牛星海 1551023222496 女 184.147.145.6 Android 7.0 小辣椒 红辣椒8X 4G 113.39623,22.371406 赖美华 186.218.176.9 1593421890833 Android 8.0 小辣椒 红辣椒8X 4G 108.980879,24.071738 女 TEXT 78.22KM 一点爱一点情,日子甜甜又蜜蜜。一个我一个你,生活平淡不嫌腻。粗茶淡饭,吃出健康身体,简单衣着,也能万种风情。幸福有你,如此心心相印! +2021-11-01 07:57:58 晁泰平 1898238221524 男 21.56.191.12 Android 6.0 一加 OnePlus 4G 117.885171,33.21035 犹妞 225.104.57.224 1331812911843 Android 6 小辣椒 红辣椒8X 5G 104.639117,37.781842 男 TEXT 98.86KM 一朵花摘了许久枯萎了也舍不得丢,一把伞撑了许久雨停了也记不起收,一条路走了许久天黑了也走不到尽头,一句话想了很久清楚了,才说出口:有你真好! +2021-11-01 07:26:15 茹鸿晖 1328514919221 男 43.227.208.47 Android 8.0 Apple iPhone XR 4G 121.785398,41.86541 肖沛 30.139.30.154 1305190543476 Android 8.0 OPPO Reno3 4G 100.886068,33.395741 男 TEXT 12.59KM 一见钟情爱上你二话不说想追你三番四次来找你五朵玫瑰送给你六神无主想泡你七次八次来烦你九颗真心打动你十分满意就是你。 +2021-11-01 07:01:10 代泰华 1896394119953 男 220.26.70.16 Android 8.0 OPPO Reno3 4G 124.361019,45.856657 邝琨瑶 247.25.68.26 1859080711480 Android 6.0 小辣椒 红辣椒8X 3G 102.725798,37.430552 男 TEXT 21.2KM 聚散两依依,聚是缘,散也是缘。聚不是开始,散就不是结束。真正的爱情是默契,是心灵的碰撞和颤栗。 +``` + +# 4. 数据存储 + +#### 4.1. 建库建表 + +- 建库 + ``` + - 如果数据库已存在就删除 + drop database if exists db_msg cascade ; + - 创建数据库 + create database db_msg ; + - 切换数据库 + use db_msg ; + - 列举数据库 + show databases ; + ``` +- 建表 + + ``` + - 如果表已存在就删除 + drop table if exists db_msg.tb_msg_source ; + - 建表 + create table db_msg.tb_msg_source( + msg_time string comment "消息发送时间" + , sender_name string comment "发送人昵称" + , sender_account string comment "发送人账号" + , sender_sex string comment "发送人性别" + , sender_ip string comment "发送人ip地址" + , sender_os string comment "发送人操作系统" + , sender_phonetype string comment "发送人手机型号" + , sender_network string comment "发送人网络类型" + , sender_gps string comment "发送人的GPS定位" + , receiver_name string comment "接收人昵称" + , receiver_ip string comment "接收人IP" + , receiver_account string comment "接收人账号" + , receiver_os string comment "接收人操作系统" + , receiver_phonetype string comment "接收人手机型号" + , receiver_network string comment "接收人网络类型" + , receiver_gps string comment "接收人的GPS定位" + , receiver_sex string comment "接收人性别" + , msg_type string comment "消息类型" + , distance string comment "双方距离" + , message string comment "消息内容" ) + - 指定分隔符为制表符 + row format delimited fields terminated by '\t' ; + ``` + +#### 4.2. 上传数据 + +- HDFS上创建目录 + `hdfs dfs -mkdir -p /momo/data` + +- 上传到HDFS + - `hdfs dfs -put /home/data1.tsv /momo/data/` + - `hdfs dfs -put /home/data2.tsv /momo/data/` + +- 查询表 验证数据文件是否映射成功 + - `select * from tb_msg_source limit 10;` + +- 统计行数 + - `select count(*) as cnt from tb_msg_source;` + - 返回结果 14 万行; + +# 5. ETL + +#### 5.1. 数据探查 + +- 问题1:当前数据中,有一些数据的字段为空,不是合法数据 + + ``` + select + msg_time, + sender_name, + sender_gps + from db_msg.tb_msg_source + where length(sender_gps) = 0 + limit 10; + ``` +- 问题2:需求中,需要统计每天、每个小时的消息量,但是数据中没有天和小时字段,只有整体时间字段,不好处理 + + ``` + select + msg_time + from db_msg.tb_msg_source + limit 10; + ``` + +- 问题3:需求中,需要对经度和维度构建地区的可视化地图,但是数据中GPS经纬度为一个字段,不好处理 + + ``` + select + sender_gps + from db_msg.tb_msg_source + limit 10; + ``` +#### 5.2. ETL 过程 + +- 如果表已存在就删除 + ``` + drop table if exists db_msg.tb_msg_etl; + ``` +- 将Select语句的结果保存到新表中 + ``` + create table db_msg.tb_msg_etl as + select + *, + substr(msg_time,0,10) as dayinfo, - 获取天 + substr(msg_time,12,2) as hourinfo, - 获取小时 + split(sender_gps,",")[0] as sender_lng, - 提取经度 + split(sender_gps,",")[1] as sender_lat - 提取纬度 + from db_msg.tb_msg_source + ``` +- 过滤字段为空的数据 + ``` + where length(sender_gps) > 0 ; + ``` + +- 验证ETL结果 + ``` + select + msg_time,dayinfo,hourinfo,sender_gps,sender_lng,sender_lat + from db_msg.tb_msg_etl + limit 10; + ``` + +# 6. 指标统计 + +#### 6.1. 统计今日总消息量 +- SQL + ``` + create table if not exists tb_rs_total_msg_cnt + comment "今日消息总量" + as + select + dayinfo, + count(*) as total_msg_cnt + from db_msg.tb_msg_etl + group by dayinfo; + + select * from tb_rs_total_msg_cnt;--结果验证 + ``` + +#### 6.2. 统计今日每小时消息量、发送和接收用户数 +- SQL + ``` + create table if not exists tb_rs_hour_msg_cnt + comment "每小时消息量趋势" + as + select + dayinfo, + hourinfo, + count(*) as total_msg_cnt, + count(distinct sender_account) as sender_usr_cnt, + count(distinct receiver_account) as receiver_usr_cnt + from db_msg.tb_msg_etl + group by dayinfo,hourinfo; + + select * from tb_rs_hour_msg_cnt;--结果验证 + ``` + +#### 6.3. 统计今日各地区发送消息数据量 +- SQL + ``` + create table if not exists tb_rs_loc_cnt + comment "今日各地区发送消息总量" + as + select + dayinfo, + sender_gps, + cast(sender_lng as double) as longitude, + cast(sender_lat as double) as latitude, + count(*) as total_msg_cnt + from db_msg.tb_msg_etl + group by dayinfo,sender_gps,sender_lng,sender_lat; + + select * from tb_rs_loc_cnt; --结果验证 + ``` + +#### 6.4. 统计今日发送消息和接收消息的用户数 +- SQL + ``` + create table if not exists tb_rs_usr_cnt + comment "今日发送消息人数、接受消息人数" + as + select + dayinfo, + count(distinct sender_account) as sender_usr_cnt, + count(distinct receiver_account) as receiver_usr_cnt + from db_msg.tb_msg_etl + group by dayinfo; + + select * from tb_rs_usr_cnt; --结果验证 + ``` + +#### 6.5. 统计今日发送消息最多的Top10用户 +- SQL + ``` + create table if not exists tb_rs_susr_top10 + comment "发送消息条数最多的Top10用户" + as + select + dayinfo, + sender_name as username, + count(*) as sender_msg_cnt + from db_msg.tb_msg_etl + group by dayinfo,sender_name + order by sender_msg_cnt desc + limit 10; + + select * from tb_rs_susr_top10; --结果验证 + ``` + +#### 6.6. 统计今日接收消息最多的Top10用户 +- SQL + ``` + create table if not exists tb_rs_rusr_top10 + comment "接受消息条数最多的Top10用户" + as + select + dayinfo, + receiver_name as username, + count(*) as receiver_msg_cnt + from db_msg.tb_msg_etl + group by dayinfo,receiver_name + order by receiver_msg_cnt desc + limit 10; + + select * from tb_rs_rusr_top10; --结果验证 + ``` + +#### 6.7. 统计发送人的手机型号分布情况 +- SQL + ``` + create table if not exists tb_rs_sender_phone + comment "发送人的手机型号分布" + as + select + dayinfo, + sender_phonetype, + count(distinct sender_account) as cnt + from tb_msg_etl + group by dayinfo,sender_phonetype; + + select * from tb_rs_sender_phone; --结果验证 + ``` + +#### 6.8. 统计发送人的设备操作系统分布情况 +- SQL + ``` + create table if not exists tb_rs_sender_os + comment "发送人的OS分布" + as + select + dayinfo, + sender_os, + count(distinct sender_account) as cnt + from tb_msg_etl + group by dayinfo,sender_os; + + select * from tb_rs_sender_os; --结果验证 + ``` + + + + + + + + diff --git "a/_posts/2022-01-27-Hadoop\357\274\232HDFS \345\210\206\345\270\203\345\274\217\346\226\207\344\273\266\345\255\230\345\202\250\347\263\273\347\273\237\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-27-Hadoop\357\274\232HDFS \345\210\206\345\270\203\345\274\217\346\226\207\344\273\266\345\255\230\345\202\250\347\263\273\347\273\237\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..c4f68d92fbb --- /dev/null +++ "b/_posts/2022-01-27-Hadoop\357\274\232HDFS \345\210\206\345\270\203\345\274\217\346\226\207\344\273\266\345\255\230\345\202\250\347\263\273\347\273\237\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,281 @@ +--- +layout: post +title: Hadoop:HDFS 分布式文件存储系统概念详解 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hadoop +--- + +# 0. 分布式存储 + +#### 0.1 分布式存储核心属性 + +- 分布式存储 + - 多机器、横向扩展存储资源 + - 相对与纵向扩展,可以分布式存储理论上无上限 +- 元数据记录(文件位置索引) + - 记录文件存储位置信息 + - 快速定位文件位置 +- 分块存储 + - 解决文件过大,单机存储不下的问题 + - 方便并行计算,提高文件处理的效率 + - 并行上传下载,提升文件传输的效率 +- 副本机制 + - 问题:部署的机器是普通机器,难免存在硬件故障,容易导致数据丢失 + - 解决:副本机制、异地备份,做数据冗余,有效保证数据安全 + +#### 0.2 常见的分布式存储方案 + +- HDFS / GFS +- Ceph + +# 1. HDFS 概念 + +#### 1.1. HDFS 工作原理 + +- HDFS 概念 + - HDFS 是Hadoop Distribute File System 的简称,HDFS 是 Hadoop 生态的核心。 + - 它允许文件通过网络在多台主机上分享的文件系统,可以让多台机器上的多个用户分享文件和存储空间。 + >HDFS只是分布式文件管理系统中的一种。 + + ![]({{site.baseurl}}/img-post/hdfs-1.png) + +- HDFS 架构 + + - HDFS 遵循主/从架构,由单个 NameNode(NN)、SecondaryNameNode 和多个 DataNode(DN) 组成: + + - NameNode : + - 负责维护和管理文件系统元数据 + - 执行有关 **文件系统命名** 空间的操作,例如打开,关闭、重命名文件和目录等; + - 同时还负责 **集群元数据** 的存储,记录着文件中各个数据块的位置信息; + >NameNode 是访问 HDFS 的唯一入口。 + - NameNode 管理元数据的方式: + - 内存 + - 磁盘文件 + - FSimage 内存元数据镜像文件 + - edits log(Journal)日志 + - NameNode,其实就是 Master,它是一个管理者承担以下职责: + - 管理HDFS的名称空间 + - 配置副本策略 + - 管理数据块的映射信息 + - 处理客户端读写请求 + + ![]({{site.baseurl}}/img-post/hdfs-5.png) + + - NameNode 是主服务器, + - NameNode 负责管理文件系统的命名空间以及客户端对文件的访问。当客户端请求数据时,仅仅从 NameNode 中获取文件的元数据信息,具体的数据传输不经过 NameNode,而是直接与具体的 DataNode 进行交互。 + - 这里文件的元数据信息,记录了文件系统中的文件名和目录名,以及它们之间的层级关系,同时也记录了每个文件目录的所有者及其权限,甚至还记录每个文件由哪些块组成,这些元数据信息记录在文件 fsimage 中,当系统初次启动时,NameNode 将读取 fsimage 中的信息并保存到内存中。 + - 这些块的位置信息是由 NameNode 启动后从每个 DataNode 获取并保存在内存当中的,这样既减少了 NameNode 的启动时间,又减少了读取数据的查询时间,提高了整个系统的效率。 + - NameNode **并不持久化文件块的存储位置信息**,这些信息会在系统启动时从 DataNode 重建。 + >NameNode 所在机器通常会配置大量内存(RAM)。 + + - DataNode: + - 负责具体的数据块存储; + - 提供来自文件系统客户端的读写请求,执行块的创建,删除等操作; + - DateNode,其实就是 Slave,NameNode 下达命令,DateNode 执行实际的操作: + - 存储实际的数据块 + - 执行数据块的读/写操作 + - DataNode 会定期发送心跳信息给 NameNode,告知 NameNode 当前节点存储的文件块信息。 + - 当客户端给 NameNode 发送读写请求时,NameNode 告知客户端每个数据块所在的 DataNode 信息,然后客户端直接与 DataNode 进行通信,减少 NameNode 的系统开销。 + - 当 DataNode 在执行块存储操作时,DataNode 还会与其他 DataNode 通信,复制这些块到其他 DataNode 上实现冗余。 + + - Secondary NameNode + - 从字面上来看,SecondaryNameNode 很容易被当作是 NameNode 的备份节点,其实不然。 + + ![]({{site.baseurl}}/img-post/hdfs-6.png) + + - NameNode 管理着元数据信息,元数据信息会定期保存到 edits 和 fsimage 文件中。 + - 其中的 edits 保存操作日志信息,在 HDFS 运行期间,新的操作日志不会立即与 fsimage 进行合并,也不会存到 NameNode 的内存中,而是会先写到 edits 中。 + - 当 edits 文件达到一定域值或间隔一段时间后触发 SecondaryNameNode 进行工作,这个时间点称为 checkpoint。 + - SecondaryNameNode 的角色就是定期地合并 edits 和 fsimage 文件,其合并步骤如下: + - 在进行合并之前,SecondaryNameNode 会通知 NameNode 停用当前的 editlog 文件, NameNode 会将新记录写入新的 editlog.new 文件中。 + - SecondaryNameNode 从 NameNode 请求并复制 fsimage 和 edits 文件。 + - SecondaryNameNode 把 fsimage 和 edits 文件合并成新的 fsimage 文件,并命名为 fsimage.ckpto + - NameNode 从 SecondaryNameNode 获取 fsimage.ckpt,并替换掉 fsimage,同时用 edits.new 文件替换旧的 edits 文件。 + - 更新 checkpoint 的时间。 + - 在紧急情况下,Secondary NameNode 可辅助恢复 NameNode + +- HDFS 设计目标 + - 故障检测 & 自动恢复 + - HDFS 运行在廉价普通机器上,硬件出现故障是常态,因此 故障检测 & 自动恢复 是常态; + - 注重数据访问的 高吞吐量 + - 不太重视数据访问的反应时间 + - HDFS 支持大文件 + - 提供很高的聚合数据带宽,一个集群支持数据百个节点,支持千万级别的文件。 + - write-one-read-many + - 一次写入,多次访问; + - 文件一旦被写入,就不再被修改; + - 跨平台移植 + +- HDFS 适用场景 + - 大文件 + - 数据流式访问 + - 高吞吐量 + - 一次写入、多次读取 + - 低成本部署(部署在廉价PC上) + - 高容错 + +- HDFS 不适合的场景 + - 大量的小文件 + - 数据交互式访问 + - 频繁任意修改 + - 实时数据处理 + - 低延迟数据访问 + - 多用户写入 + - HDFS 只有一个写入者,而且写操作总是在文件末尾,不支持多用户写入 + +#### 1.2. 分块(Block)存储机制 + +- HDFS 中的文件在 **物理上是分块 (Block) 存储的**,可以通过配置参数 ([hdfs-site.xml文件中的]dfs.blocksize) 来设置。 +- Block 块不能设置太小,也不能设置太大,**HDFS块大小的设置主要取决于磁盘的传输效率**: + - HDFS的块设置太小,会增加寻址时间 + - HDFS 的块比磁盘的块大,目的是为了最小化寻址开销 + >磁盘块一般 512 Bit,HDFS 每个块 64Mb。 + - HDFS的块如果设置的也不能太大。 + - Hadoop 中一个 map 任务一次通常只处理一个块中的数据,如果块过大,会导致整体任务数量过小,降低作业处理的速度。 + - 另外,在一次上传时,如果发生异常,需要重新传输,造成网络IO资源的浪费。 + - 在随机读取某部分内容时,不够灵活。 + +- Hadoop 2.x 默认最小分块大小是 128M + - 原因:寻址时间大约为 10ms,寻址时间为传输时间的 1% 为最佳(即为 10/0.01=1000ms=1s ),目前磁盘的传输率普遍为 100m/s,块的大小需要是 **2的n次方**,故为 128M) + +#### 1.3. 文件系统命名空间(name space) + +- HDFS 的文件系统命名空间的层次结构,支持目录和文件的创建、移动、删除和重命名等操作,支持配置用户和访问权限。 +- HDFS 不支持硬链接和软连接。 +- NameNode负责维护文件系统名称空间,记录对名称空间或其属性的任何更改。 + +#### 1.4. 副本机制 + +- 由于 Hadoop 被设计运行在廉价的机器上,这意味着硬件是不可靠的,为了保证容错性,HDFS 提供了数据复制机制。 +- HDFS 将每一个文件存储为一系列块,每个块由多个副本来保证容错,块的大小和复制因子可以自行配置(默认情况下,块大小是 128M,默认复制因子是 3)。 + + ![]({{site.baseurl}}/img-post/hdfs-2.png) + +#### 1.5. 副本机制的实现原理 + +- 大型的 HDFS 实例,通常分布在多个机架的多台服务器上,不同机架上的两台服务器之间通过交换机进行通讯。 +- 在大多数情况下,同一机架中的服务器间的网络带宽、大于不同机架中的服务器之间的带宽。 +- 因此 HDFS 采用 **机架感知** 副本放置策略,对于常见情况,当复制因子为 3 时,HDFS 的放置策略是: + - 在写入程序位于 Datanode 上时,就优先将写入文件的一个副本放置在该 Datanode 上,否则放在随机 Datanode 上,在另一个远程机架上的任意一个节点上放置另一个副本,并在该机架上的另一个节点上放置最后一个副本。 + - 此策略可以减少机架间的写入流量,从而提高写入性能。 + + ![]({{site.baseurl}}/img-post/hdfs-3.png) + +#### 1.6. 副本的选择 + +- 为了最大限度地减少带宽消耗和读取延迟,HDFS 在执行读取请求时,优先读取距离读取器最近的副本。 +>**节点距离**:两个节点到达最近的共同祖先的距离总和。 +- 如果在与读取器节点相同的机架上存在副本,则优先选择该副本。 +- 如果 HDFS 群集跨越多个数据中心,则优先选择本地数据中心上的副本。 + +# 2. HDFS 的稳定性 + +#### 2.1. 心跳机制 + +- 每个 DataNode 定期向 NameNode 发送心跳消息,如果超过指定时间没有收到心跳消息,则将 DataNode 标记为死亡。 +- NameNode 不会将任何新的 IO 请求转发给标记为死亡的 DataNode,也不会再使用这些DataNode上的数据。 +- 由于数据不再可用,可能会导致某些块的复制因子小于其指定值,NameNode会跟踪这些块,并在必要的时候进行重试复制。 + +#### 2.2. 数据的完整性 + +- 由于存储设备故障等原因,存储在 DataNode 上的数据块也会发生损坏。 +- 为了避免读取到已经损坏的数据而导致错误,HDFS提供了数据完整性校验机制来保证数据的完整性,具体操作如下: + - 当客户端创建 HDFS 文件时,它会计算文件的每个块的校验和,并将校验和存储在同一HDFS命名空间下的单独的隐藏文件中。 + - 当客户端检索文件内容时,它会验证从每个DataNode接收的数据是否与存储在关联校验和文件中的校验和匹配。 + - 如果匹配失败,则证明数据已经损坏,此时客户端会选择从其他DataNode获取该块的其他可用副本。 + +#### 2.3. 元数据的磁盘故障 + +- FsImage 和 EditLog 是 HDFS 的核心数据,这些数据的意外丢失可能会导致整个 HDFS 服务不可用。 +- 为了避免这个问题,可以配置 NameNode 使其支持 FsImage 和 EditLog 多副本同步,这样 FsImage 或 EditLog 的任何改变都会引起每个副本 FsImage 和 EditLog 的同步更新。 + +#### 2.6. 支持快照 + +- 快照支持在特定时刻存储数据副本,在数据意外损坏时,可以通过回滚操作恢复到健康的数据状态。 + +# 3. HDFS 特点 + +#### 3.1 高容错 + + - 副本机制: + 由于HDFS 采用数据的多副本方案,所以部分硬件的损坏不会导致全部数据的丢失。 + +#### 3.2 高吞吐量 + +- HDFS设计的重点是支持高吞吐量的数据访问,而不是低延迟的数据访问。 + +#### 3.3 大文件支持 + +- HDFS适合于大文件的存储,文档的大小应该是是GB到TB级别的。 + +#### 3.3 简单一致性模型 + +- HDFS 更适合于一次写入多次读取(write-once-read-many)的访问模型。 +- 支持将内容追加到文件末尾,但不支持数据的随机访问,不能从文件任意位置新增数据。 + +#### 3.4 跨平台移植性 + +- HDFS具有良好的跨平台移植性,这使得其他大数据计算框架都将其作为数据持久化存储的首选方案。 + +# 4. HDFS 文件写入流程 + +#### 4.1. Pipeline 管道 + +![]({{site.baseurl}}/img-post/hdfs-8.png) + +- 通过 pipeline 管道,客户端将数据块写入第一个数据节点,第一个节点保存后再复制到第二个节点,第二个节点保存后再复制到第三个节点。 + +>问题:DataNode 采用 pipeline 传输,而不是用拓扑式传输的原因是什么? +> +>答案:以管道的形式,顺序沿着一个方向传输,这样能够充分利用每台机器的带宽,避免网络瓶颈和高延迟的连接,最小化推送所有数据的延时。 + +#### 4.2. ACK 应答响应 + +![]({{site.baseurl}}/img-post/hdfs-9.png) + +- ACK (Acknowledge character)即是确认字符,在数据通信中,接收方发给发送方的一种传输类控制字符。表示发来的数据已确认接收无误; +- 在HDFS pipeline管道传输数据的过程中,传输的反方向会进行ACK校验,确保数据传输安全。 + +#### 4.3. 核心概念–默认 3 副本存储策略 + +- 默认副本存储策略是由 BlockPlacementPolicyDefault 指定。 +- 第一块副本:优先客户端本地,否则随机 +- 第二块副本:不同于第一块副本的不同机架。 +- 第三块副本:第二块副本相同机架不同机器。 + +#### 4.4. 文件写入流程 + +![]({{site.baseurl}}/img-post/hdfs-8.png) + +- 客户端在向 NameNode 发送写请求之前,先将数据写入本地的临时文件中。 +- 待临时文件块达到系统设置的块大小时,开始向 NameNode 请求写文件。 +- 客户端通过 Distributed FileSystem 模块向 NameNode 请求上传文件,NameNode 收到请求后会进行校验: + - 校验是否有写权限; + - 校验路径下是否有同名文件; +- NameNode 在此步骤中会检查集群中每个 DataNode 状态信息,获取空闲的节点,并在检查客户端权限后创建文件; +- NameNode 返回是否可以上传(如果校验失败,会直接报错,如果成功会给客户端返回一个信号,表示可以上传); +- 客户端请求第一个 Block 块上传到哪几个 DataNode 节点上; +- NameNode 返回 3 个 DataNode 节点,dn1、dn2、dn3; +- 客户端通过 FSDataOutputStream 模块请求 dn1 上传数据,dn1 收到请求会继续调用dn2,然后 dn2 调用 dn3,将这个通信管道建立完成; +- dn1、dn2、dn3逐级应答客户端; +- 客户端开始往 dn1 上传第一个 Block(先从磁盘读取数据放到一个本地内存缓存),以 Packet 为单位,dn1 收到一个 Packet 就会传给 dn2,dn2 传给 dn3,dn1 每传一个 Packet 会放入一个应答队列等待应答; +- 当一个 Block 传输完成之后,客户端再次请求 NameNode 上传第二个 Block 的服务器。(重复执行3-7步)。 +- 最小复制块由 `dfs.namenode.replication.min` 指定,默认值是 1。 + >如果 pipeline 传输失败,只要有一个副本上传成功,系统就会自动找其他机器继续复制。如果一个副本都没穿成功,则上传失败。 + +# 5. HDFS 读数据流程 + +- 客户端通过 Distributed FileSystem 向 NameNode 请求下载文件,NameNode 通过查询元数据信息,获取文件块所在的 DataNode 节点地址; +- 挑选一台 DataNode 服务器(就近原则),请求读取数据; +- DataNode 开始传输数据给客户端(从磁盘里读取 数据输入流,以Packet为单位来做校验); +- 客户端以 Packet 为单位接受,先在本地缓存,然后写入目标文件。 + + +# 6. 常用命令一览表 + +![]({{site.baseurl}}/img-post/hdfs-4.png) \ No newline at end of file diff --git "a/_posts/2022-01-27-Hadoop\357\274\232Hadoop \345\237\272\347\241\200\346\246\202\345\277\265\347\256\200\344\273\213.md" "b/_posts/2022-01-27-Hadoop\357\274\232Hadoop \345\237\272\347\241\200\346\246\202\345\277\265\347\256\200\344\273\213.md" new file mode 100644 index 00000000000..57ac44fe17d --- /dev/null +++ "b/_posts/2022-01-27-Hadoop\357\274\232Hadoop \345\237\272\347\241\200\346\246\202\345\277\265\347\256\200\344\273\213.md" @@ -0,0 +1,57 @@ +--- +layout: post +title: Hadoop:Hadoop 基础概念简介 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hadoop +--- + +#### 1. Hadoop 核心组件 + +- HDFS:解决海量存储; +- YARN:解决资源调度; +- MapReduce:解决海量数据计算。 + +>**Hadoop 集群 = HDFS 集群 + Yarn 集群** + +- HDFS 集群 & Yarn 集群,物理上在一起,逻辑上相分离。 + +- 物理上在一起:程序部署在同一台机器上。 +- 逻辑上相分离:两个集群之间没有依赖,互不影响。 + +![]({{site.baseurl}}/img-post/hadoop-6.png) + +- MapReduce 是第一代计算引擎,是代码层面的组件,不存在集群之说。 + +>没有 MapReduce 集群之说。 + + +#### 2. Hadoop 生态圈 + +![]({{site.baseurl}}/img-post/hadoop-7.png) + + +#### 3. Hadoop 特点 + +- 成本低廉:允许部署在廉价的通用机器上,不再要求单机性能 +- 横向扩容:方便进行横向的扩容,集群方便扩展至上千节点 +- 效率高:在集群内通过并行计算,大大提升计算效率 +- 可靠性:副本机制(备份冗余)、重试机制、推测机制 +- 通用性:与业务分离,各个行业都能很好的应用; +- 易用性:易学易懂、简单易用; + +#### 4. 版本变迁 + +- 1.x:MapReduce + HDFS +- 2.x:MapReduce + HDFS + Yarn +- 3.x:着重强调性能优化 + - EC 纠删码 + - 多 NameNode + - 任务本地优化 + - 内存参数自动推断 + + diff --git "a/_posts/2022-01-27-Hadoop\357\274\232MapReduce \350\256\241\347\256\227\345\274\225\346\223\216\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-27-Hadoop\357\274\232MapReduce \350\256\241\347\256\227\345\274\225\346\223\216\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..f029508f60f --- /dev/null +++ "b/_posts/2022-01-27-Hadoop\357\274\232MapReduce \350\256\241\347\256\227\345\274\225\346\223\216\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,362 @@ +--- +layout: post +title: Hadoop:MapReduce 计算引擎概念详解 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hadoop +--- + +# 1. MapReduce 概念简介 + +#### 1.1. MapReduce 定义 + +- MapReduce 是一个软件框架,基于该框架能够容易地编写应用程序,这些应用程序能够运行在由上千个商用机器组成的大集群上,并以一种可靠的、具有容错能力的方式并行地处理上TB级别的海量数据集。 +- 这个定义里面有着这些关键词: + - 软件框架; + - 并行处理; + - 可靠且容错; + - 大规模集群; + - 海量数据集。 +- MapReduce 核心分为两个阶段: + - 第一阶段:映射 + - MapTask 特征提取; + - 把具有相同特征的数值,提取变成 key-value 形式,经由网络发送到第二阶段的 Task 做逻辑计算; + - 第二阶段:聚合 + - ReduceTask 逻辑计算; + - 将相同特折让的数据抽象成一个逻辑计算单位,调用用户定义的逻辑执行计算。 +#### 1.2. MapReduce 工作机制 +- MapReduce 擅长处理大数据 + - Mapper 负责“分”,即把复杂的任务分解为若干个“简单的任务”来处理。 + >MapReduce的思想就是 **“分而治之”**。 + - “简单的任务”包含三层含义: + - 一是数据或计算的规模相对原任务要大大缩小; + - 二是就近计算原则,即任务会分配到存放着所需数据的节点上进行计算; + - 三是这些小任务可以并行计算,彼此间几乎没有依赖关系。 + - Map 主要是 映射、变换、过滤的过程。 + - 一条数据进入map会被处理成多条数据,也就是 1 进 N 出。 + + - Reducer 负责对 map 阶段的结果进行汇总。至于需要多少个 Reducer,用户可以根据具体问题,通过在 mapred-site.xml 配置文件里设置参数 mapred.reduce.tasks 的值,缺省值为1。 + - Reduce主要是 分解、缩小、归纳的过程。 + +#### 1.3. MapReduce 组成部分 + +![]({{site.baseurl}}/img-post/MapReduce-1.png) + +MapReduce的整个工作过程如上图所示,它包含如下4个独立的实体: + +- 实体一:客户端,用来提交 MapReduce 作业。 +- 实体二:JobTracker,用来协调作业的运行。 +- 实体三:TaskTracker,用来处理作业划分后的任务。 +- 实体四:HDFS,用来在其它实体间共享作业文件。 + +#### 1.4. MapReduce 适用场景 + +- 数据查找 :分布式Grep +- Web访问日志分析:词频统计、网站PV UV统计、Top K问题 +- 倒排索引:建立搜索引擎索引(根据值找健) +- 分布式排序 + +#### 1.5. MapReduce的优点和缺点 + +- 优点: + - 模型简单:Map + Reduce + - 高伸缩性:支持横向扩展 + - 易于编程:通过简单的接口,就可以完成分布式程序 + - 灵活:结构化和非结构化数据 + - 速度快:高吞吐离线处理数据 + - 并行处理:编程模型天然支持并行处理 + - 容错能力强:任何一个节点宕机,只需要把计算任务移到别的节点就可以 + - 适合 海量数据离线处理:可以请从处理GB、TB乃至PB级别的数据量 + +- 缺点: + - 流式数据:MapReduce 处理模型就决定了 **只能处理静态数据** + - 实时计算:不适合低延迟数据处理,需要毫秒级别响应(MapReduce处理延迟较高) + - 复杂算法:例如SVM支持向量(机器学习算法) + - 迭代计算:例如斐波那契数列(后边的计算需要前面的计算结果) + - 读写较慢:Shuffle 过程过于繁琐,读写非常的慢 + - MR 固定:一个 map 对应一个 reduce,不能多个 map 或者 reduce + +#### 1.6. 应用情况 + +- 目前,企业已经基本不再直接使用 MapReduce。 + +# 2. MapReduce 知识全景 + +![]({{site.baseurl}}/img-post/MapReduce-4.png) + +# 3. MapReduce 框架 + +#### 3.1. MapReduce 框架的组成 + +![]({{site.baseurl}}/img-post/MapReduce-2.png) + +- 一个MapReduce作业通常会把输入的数据集切分为若干独立的数据块,由Map任务以完全并行的方式去处理它们。 + +- 框架会对Map的输出先进行排序,然后把结果输入给Reduce任务。通常作业的输入和输出都会被存储在文件系统中,整个框架负责任务的调度和监控,以及重新执行已经关闭的任务。 + +- 通常,MapReduce框架和分布式文件系统是运行在一组相同的节点上,也就是说,计算节点和存储节点通常都是在一起的。这种配置允许框架在那些已经存好数据的节点上高效地调度任务,这可以使得整个集群的网络带宽被非常高效地利用。 + +- JobTracker + - JobTracker负责调度构成一个作业的所有任务,这些任务分布在不同的TaskTracker上(由上图的JobTracker可以看到2 assign map 和 3 assign reduce)。你可以将其理解为公司的项目经理,项目经理接受项目需求,并划分具体的任务给下面的开发工程师。 + +- TaskTracker + - TaskTracker负责执行由JobTracker指派的任务,这里我们就可以将其理解为开发工程师,完成项目经理安排的开发任务即可。 + +- 大部分 map task 与 reduce task 的执行是在不同的节点上。当然很多情况下 Reduce 执行时,需要跨节点去拉取其它节点上的 map task 结果。 + +#### 3.2. MapReduce 的输入输出 + +- MapReduce借鉴了函数式语言中的思想,用Map和Reduce两个函数提供了高层的并行编程抽象模型。 + - Map: 对一组数据元素进行某种重复式的处理; + - Reduce: 对Map的中间结果进行某种进一步的结果整理。 + +- MapReduce 中定义了如下的 Map 和 Reduce 两个抽象的编程接口,由用户去编程实现: + - map: [k1,v1] → [(k2,v2)] + - reduce: [k2, {v2,…}] → [k3, v3] + +- MapReduce框架运转在键值对上,也就是说,框架把作业的输入看成是一组键值对,同样也产生一组键值对作为作业的输出,这两组键值对有可能是不同的。 + +- 一个MapReduce作业的输入和输出类型如下图所示:可以看出在整个流程中,会有三组键值对类型的存在。 + +![]({{site.baseurl}}/img-post/MapReduce-3.png) + + +#### 3.3. MapReduce 优点 + +- MapReduce 最大的亮点在于,通过抽象模型和计算框架,把需要做什么(what need to do)与具体怎么做(how to do)分开了,为程序员提供一个抽象和高层的编程接口和框架。 +- 程序员仅需要关心其应用层的具体计算问题,仅需编写少量的处理应用本身计算问题的程序代码。 +- 如何具体完成这个并行计算任务所相关的诸多系统层细节被隐藏起来,从分布代码的执行、到大到数千小到单个节点集群的自动调度使用,都交给计算框架去处理。 + + +# 4. MapReduce 工作流程 + +![]({{site.baseurl}}/img-post/MapReduce-5.png) + +#### 4.1. 分片、格式化数据源 + +- 输入 Map 阶段的数据源,必须经过分片和格式化操作。 + - 分片操作: + - 指的是将源文件划分为大小相等的小数据块( Hadoop 2.x 中默认 128MB ),也就是分片( split ),Hadoop 会为每一个分片构建一个 Map 任务,并由该任务运行自定义的 map() 函数,从而处理分片里的每一条记录; + - 格式化操作: + - 将划分好的分片( split )格式化为键值对形式的数据,其中, key 代表偏移量, value 代表每一行内容。 + +#### 4.2. 执行 MapTask + +- 每个 Map 任务都有一个内存缓冲区(缓冲区大小 100MB ),输入的分片( split )数据经过 Map 任务处理后的中间结果会写入内存缓冲区中。 +- 如果写人的数据达到内存缓冲的阈值( 80MB ),会启动一个线程将内存中的溢出数据写入磁盘,同时不影响 Map 中间结果继续写入缓冲区。 +- 在溢写过程中, MapReduce 框架会对 key 进行排序,如果中间结果比较大,会形成多个溢写文件,最后的缓冲区数据也会全部溢写入磁盘形成一个溢写文件,如果是多个溢写文件,则最后合并所有的溢写文件为一个文件。 + +#### 4.3. 执行 Shuffle 过程 + +- MapReduce 工作过程中, Map 阶段处理的数据如何传递给 Reduce 阶段,这是 MapReduce 框架中关键的一个过程,这个过程叫作 Shuffle 。 +- Shuffle 会将 MapTask 输出的处理结果数据分发给 ReduceTask ,并在分发的过程中,对数据按 key 进行分区和排序。 + +#### 4.4. 执行 ReduceTask + +- 输入 ReduceTask 的数据流是形式,用户可以自定义 reduce()方法进行逻辑处理,最终以的形式输出。 + +#### 4.5. 写入文件 + +- MapReduce 框架会自动把 ReduceTask 生成的传入 OutputFormat 的 write 方法,实现文件的写入操作。 + + +# 5. MapTask + +![]({{site.baseurl}}/img-post/MapReduce-7.png) + +#### 5.1. Read 阶段 + +- MapTask 通过用户编写的 RecordReader ,从输入的 InputSplit 中解析出一个个 key / value 。 +- 在 MapReduce 概念中,MapTask 只读取 split。 + +#### 5.2. Map 阶段 + +- 将解析出的 key / value 交给用户编写的 Map ()函数处理,并产生一系列新的 key / value 。 + +#### 5.3. Collect 阶段 + +- 在用户编写的 map() 函数中,数据处理完成后,一般会调用 outputCollector.collect() 输出结果,在该函数内部,它会将生成的 key / value 分片(通过调用 partitioner ),并写入一个环形内存缓冲区中(该缓冲区默认大小是 100MB )。 + +#### 5.4. Spill 阶段 + +- 即“溢写”,当缓冲区快要溢出时(默认达到缓冲区大小的 80 %),会在本地文件系统创建一个溢出文件,将该缓冲区的数据写入这个文件。 +- 将数据写入本地磁盘前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。 +- 写入磁盘之前,线程会根据 ReduceTask 的数量,将数据分区,一个 Reduce 任务对应一个分区的数据。 +- 这样做的目的是为了避免有些 Reduce 任务分配到大量数据,而有些 Reduce 任务分到很少的数据,甚至没有分到数据的尴尬局面。 +- 如果此时设置了 Combiner ,将排序后的结果进行 Combine 操作,这样做的目的是尽可能少地执行数据写入磁盘的操作。 + +#### 5.5. Combine 阶段 + +- 当所有数据处理完成以后, MapTask 会对所有临时文件进行一次合并,以确保最终只会生成一个数据文件 + +- 合并的过程中会不断地进行排序和 Combine 操作, +其目的有两个:一是尽量减少每次写人磁盘的数据量;二是尽量减少下一复制阶段网络传输的数据量。 +- 最后合并成了一个已分区且已排序的文件。 + +# 6. ReduceTask + +![]({{site.baseurl}}/img-post/MapReduce-6.png) + +#### 6.1. Copy 阶段 + +- Reduce 会从各个 MapTask 上远程复制一片数据(每个 MapTask 传来的数据都是有序的),并针对某一片数据,如果其大小超过一定國值,则写到磁盘上,否则直接放到内存中 + +#### 6.2. Merge 阶段 + +- 在远程复制数据的同时, ReduceTask 会启动两个后台线程,分别对内存和磁盘上的文件进行合并,以防止内存使用过多或者磁盘文件过多。 + +#### 6.3. Sort 阶段 + +- 用户编写 reduce() 方法输入数据是按 key 进行聚集的一组数据。 +- 为了将 key 相同的数据聚在一起, Hadoop 采用了基于排序的策略。 +- 由于各个 MapTask 已经实现对自己的处理结果进行了局部排序,因此, ReduceTask 只需对所有数据进行一次归并排序即可。 +- 为了将 key 相同的数据聚在一起, Hadoop 采用了基于排序的策略。 +- 由于各个 MapTask 已经实现对自己的处理结果进行了局部排序,因此, ReduceTask 只需对所有数据进行一次归并排序即可。 + +#### 6.4. Reduce 阶段 + +- 对排序后的键值对调用 reduce() 方法,键相等的键值对调用一次 reduce()方法,每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到 HDFS 中 + +#### 6.5. Write 阶段 + +- reduce() 函数将计算结果写到 HDFS 上。 + +- 合并的过程中会产生许多的中间文件(写入磁盘了),但 MapReduce 会让写入磁盘的数据尽可能地少,并且最后一次合并的结果并没有写入磁盘,而是直接输入到 Reduce 函数。 +- 合并的过程中会产生许多的中间文件(写入磁盘了),但 MapReduce 会让写入磁盘的数据尽可能地少,并且最后一次合并的结果并没有写入磁盘,而是直接输入到 Reduce 函数。 + + +# 7. Shuffle + +#### 7.1. Shuffle 作用 + +- 将 map task 输出的处理结果数据,分发给 reducetask,并在分发的过程中,对数据按key进行了分区和排序,这个过程称为 shuffle。 +- 完整的 shuffle 是由 Map 端和 Reduce 端组成: + - Map 端负责数据的溢写; + - Reduce 负责将 Map 的数据拷贝到本地,并进行归并排序。 + + ![]({{site.baseurl}}/img-post/MapReduce-8.png) + +#### 7.2. Map 端 Shuffle:输出 + +- Map端把数据源源不断的写入到一个环形缓冲区(RingBuffer) +- 当达到一定阀值时会新启一个线程,将缓冲区的数据溢写到磁盘 +- 在溢写过程中,调用 Partitioner 进行分组,对于每个组按照Key进行排序,partitioner 决定 map task 的哪一个输出对应下游 reduce 的哪一个 task; +- Map 处理完毕后对磁盘的多个文件进行 Merge 操作,将大量文件合并为一个大文件(数据文件)和一个索引文件(每个partition在文件中的起始位置、长度等等) + - 一个 map 输入文件,经过 merge 后合并成一个输出文件,一个输入只会对应一个输出; + + +#### 7.3. Reduce 端的 Shuffle:输入 + +- Map 端 Shuffle 结束后,会暴露一个 Http 服务,供 Reduce 端获取数据; +- Reduce 端启动拷贝线程,从各个 Map 端拷贝数据,一边拷贝一边进行归并排序操作,便于数据的下一步处理。 +- copy 阶段: + - ReduceTask 启动 Fetcher 线程,到已经完成 MapTask 的节点上复制一份属于自己的数据; +- Merge 阶段: + - 在 ReduceTask 远程复制到数据的同时,会在后台开启两个线程,对内存到本地的数据文件进行合并; +- Sort 阶段: + - 在对数据进行合并的同时,会进行排序操作,由于 MapTask 阶段已经对数据进行了局部的排序,ReduceTask 只需要保证 Copy 的数据最终的整体有效性。 + +#### 7.4. Shuffle 机制的弊端 + +- 频繁涉及到数据内存、磁盘之间的多次往复; +>Mapreduce 相比于 Spark、Flink 计算引擎速度很慢,最关键的问题就是 Shuffle 机制。 + +# 8. MapReduce 容错性 + +#### 8.1. Task运行失败 + +- Map Task 失败:由 MRAppMaster 重启 Map Task,Map Task 没有依赖性; +- Reduce Task 失败:由 MRAppMaster 重启 Reduce Task,Map Task 的输出保存在磁盘上; +- 同一个 Task 运行多次失败(默认4次)则本次作业失败。 + +#### 8.2. 如果Task所在的Node节点挂了 + +- 在另外一个节点上重启所有在挂掉节点上曾经运行过的任务 + +#### 8.3. 如果Task运行缓慢 + +- 通常由于硬件损坏、软件 Bug 或者配置错误导致; +- 单个 task 运行缓慢会显著影响整体作业运行时间 +- 推测执行:在另外一个节点上启动相同的任务,谁先完成就 kill 掉另外一个节点上的任务 +- 无法启动推测执行的情况:写入数据库 + +#### 8.4. 数据本地性 + +- HDFS上同一份文件会有多份拷贝(默认是3份); +- MapReduce调度原则:在副本的节点上启动Map Task任务,或者在就近的节点上启动Map Task任务; +- 数据本地性有三个级别 :数据和Task在同一个节点、数据和Task在同一机架、数据和Task不在同一个节点也不在同一个机架。 + + +# 9. MapReduce 常见疑问 + +#### 9.1. Shuffle 的作用是什么? + +每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候,需要用 Shuffle 将缓冲区的数据、以一个临时文件的方式存放到磁盘,当整个 map task 结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据。 + +#### 9.2. map task与reduce task的执行是否在不同的节点上? + +大部分map task与reduce task的执行是在不同的节点上。当然很多情况下Reduce执行时需要跨节点去拉取其它节点上的map task结果。 + +#### 9.3. Shuffle产生的意义是什么? + +跨节点拉取数据时,尽可能减少对带宽的不必要消耗。减少磁盘IO对task执行的影响。 + +#### 9.4. 每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据该如何处理? + +新开启一个线程将数据溢写到磁盘。 + +#### 9.5. 在map task执行时,它是如何读取HDFS的? + +在map task执行时,它的输入数据来源于HDFS的block,当然在MapReduce概念中,map task只读取split。 + +#### 9.6. 读取的Split与block的对应关系可能是什么? + +Split与block的对应关系可能是多对一,默认是一对一。 + +#### 9.7. MapReduce提供Partitioner接口,它的作用是什么? + +根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job.set(..)。 + +#### 9.8. 溢写是在什么情况下发生? + +当map task 的输出结果大于这个内存缓冲区的阀值是(buffer size * spill percent = 100MB * 0.8 = 80MB)溢写线程启动。 + +#### 9.9. 溢写是为什么不影响往缓冲区写map结果的线程? + +1、溢写为新开启线程, + +2、溢写过程中会剩余20M继续写入,对缓存区map的结果无影响 + +#### 9.10. 哪些场景才能使用Combiner呢? + +Combiner只应该用于那种 Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner的使用一定 得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果。 + +#### 9.11. Merge的作用是什么? + +每次溢写会在磁盘上产生一个溢写文件,Map 输出结果很大时,会有多次这样的溢写文件到磁盘上,当 Map task 结束完成时,内存缓冲区的数据同样也会溢写到磁盘上,结果磁盘会有一个或多个溢出的文件,同时合并溢出的文件。(如果map输出的结果很少,map完成时,溢出的文件只有一个)合并这个过程就叫做Merge。 + +merge是将多个溢写文件合并到一个文件,所以可能也有相同的key存在,在这个过程中如果client设置过Combiner,也会使用Combiner来合并相同的key。 + +#### 9.12. reduce中Copy过程采用是什么协议? + +确认map task是否完成为RPC协议,拷贝文件为HTTP协议 + +#### 9.13. reduce中merge过程有几种方式,与map有什么相似之处? + +合并后会生成一个文件,可能在内存,也可能在磁盘,默认在磁盘。 + +这里和map中内存溢出一样,当内存中的数据达到一定的阀值,就会启动内存到磁盘的溢出....合并Merge。这个过程我们设置Combiner,也会启用的,然后在磁盘中生成很多一些文件。值到map端没有数据才结束。然后启动第三种磁盘到磁盘的merge方式生成最终的那个文件。 + +#### 9.14. 溢写过程中如果有很多个key/value对、需要发送到某个reduce端去,那么如何处理这些key/value值? + +有很多个key/value 对需要发送到某个reduce端去,那么需要将这些key/value值拼接到一块,减少与partition相关的索引记录 + +#### 9.15. 当溢写线程启动后,需要对这80MB空间内的key做排序(Sort),这里的排序也是对谁的排序? + +排序是MapReduce模型默认的行为,这里的排序也是对序列化的字节做的排序。 + + diff --git "a/_posts/2022-01-27-Hadoop\357\274\232YARN \345\212\237\350\203\275\345\217\212\345\267\245\344\275\234\346\234\272\345\210\266\345\216\237\347\220\206.md" "b/_posts/2022-01-27-Hadoop\357\274\232YARN \345\212\237\350\203\275\345\217\212\345\267\245\344\275\234\346\234\272\345\210\266\345\216\237\347\220\206.md" new file mode 100644 index 00000000000..991d08e6099 --- /dev/null +++ "b/_posts/2022-01-27-Hadoop\357\274\232YARN \345\212\237\350\203\275\345\217\212\345\267\245\344\275\234\346\234\272\345\210\266\345\216\237\347\220\206.md" @@ -0,0 +1,149 @@ +--- +layout: post +title: Hadoop:YARN 功能及工作机制原理 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hadoop +--- + + +# YARN 的概念 + +- YARN 是一个通用的资源管理系统和调度平台,可以为上层提供统一的资源管理和调度; + +- 资源管理系统 + - 集群的硬件资源,和程序运行相关,比如内存、CPU; +- 调度平台 + - 解决多个程序同时申请计算资源,如何分配、调度规则等问题; + +>YARN 不管理 磁盘,磁盘是由 HDFS 管理。 + +- 可以把 YARN 理解为一个分布式操作系统平台,而 MapReduce 等计算程序,则相当于运行操作系统之上的应用程序。 + + +- YARN 具有良好的通用性和包容性: + + - 正是因为有了 YARN,更多的计算框架可以接入到 HDFS 中,不仅是 MapReduce,Flink、Spark 都可以顺利接入 HDFS。 + - 正是因为 YARN 的包容性,是的其他的计算框架可以专注于计算性能的提升。 + + +![]({{site.baseurl}}/img-post/yarn-1.png) + +# YARN 的作用 + +YARN 使得集群具备以下优点: + +- 可扩展性: + - 可以平滑地扩展至数万节点和并发的应用 +- 可维护性: + - 保证集群软件的升级与用户应用程序完全解耦 +- 多租户: + - 支持同一集群中多个租户并存,同时支持多个租户间细颗粒度地共享单个节点 +- 位置感知: + - 将计算移至数据所在位置 +- 高集群使用率: + - 实现底层物理资源的高使用率 +- 安全和可审计的操作: + - 以安全、可审计的方式使用集群资源 +- 可靠性和可用性: + - 具有高度可靠的用户交互、并支持高可用性 +- 对编程模型多样化的支持: + - 不仅支持mapreduce,还支持其他模型 +- 灵活的资源模型: + - 支持各个节点的动态资源配置以及灵活的资源模型 +- 向后兼容: + - 保持现有的mapreduce应用程序的向后兼容性。 + + +# YARN 三大组件 + +#### ResourceManager(RM) + +- YARN 集群中的主角色,决定系统中所有应用程序之间资源分配的最终权限; + - 调度器根据容量、队列等限制条件(如每个队列分配一定的资源,最多执行一定数量的作业等),将系统中的资源分配给各个正在运行的应用程序。 + - 需要注意的是,该调度器是一个“纯调度器”,它不再从事任何与具体应用程序相关的工作,比如: + - RM 不负责监控或者跟踪应用的执行状态等, + - RM 也不负责重新启动因应用执行失败或者硬件故障而产生的失败任务, + - 这些均交由应用程序相关的 ApplicationMaster 完成。 +- RM 调度器仅根据各个应用程序的资源需求进行资源分配,而资源分配单位用一个抽象概念“资源容器”(Resource Container,简称 Container)表示,Container 是一个动态资源分配单位,它将内存、CPU、磁盘、网络等资源封装在一起,从而限定每个任务使用的资源量。 +- 此外,RM 该调度器是一个可插拔的组件,用户可根据自己的需要设计新的调度器,YARN 提供了多种直接可用的调度器,比如 Fair Scheduler和Capacity Scheduler 等。 +- 接收用户的作业提交,并通过 NodeManger 分配、管理各个机器上的计算资源; + +#### NodeManager(NM) + +- YARN 中的从角色,一台机器上一个,负责管理机器上的计算资源; +- 根据 RM 命令,启动 Container 容器、监视容器的资源使用情况,并向 RM 主角色汇报资源使用情况; + + +#### ApplicationMaster(AM) + +- 用户提交的每个应用程序,均包含一个 AM; +- 应用程序内的“老大”,负责程序内部各个阶段的资源申请,监督程序的执行情况; +- AM 与 RM 调度器协商以获取资源(资源用 Container 表示); +- 将得到的任务进一步分配给内部的任务(资源的二次分配); +- 与 NM 通信以启动 / 停止任务; +- 监控所有任务运行状态,并在任务运行失败时重新为任务申请资源以重启任务。 +- 当前 YARN 自带了两个AM实现,一个是用于演示AM编写方法的实例程序 distributedshell,它可以申请一定数目的 Container 以并行运行一个 Shell 命令或者Shell脚本;另一个是运行 MapReduce 应用程序的AM—MRAppMaster。 + +#### Container + +- Container 是 YARN 中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等,当AM向 RM 申请资源时,RM 为 AM 返回的资源便是用 Container 表示。 +- YARN 会为每个任务分配一个 Container,且该任务只能使用该 Container 中描述的资源。 + +# YARN 核心交互流程 + +#### 核心流程 + +- MR 作业提交 client -> RM +- 资源申请 MRAppMaster -> RM +- MR 作业状态汇报 Container( Map | Reduce Task ) -> Container(MRAppMaster) +- 节点的状态汇报 NM -> RM + +#### 具体步骤 + +- 当用户向yarn中提交一个应用程序后,yarn将分两个阶段运行该应用程序。 + - 第一个阶段是启动 ApplicationMaster。 + - 第二个阶段是由 Applicationmaster 刨建应用程序,为它申请资源,并监控它的整个运行过程,直到运行完成。 + +- 第1步:用户向yarn中提交应用程序,其中包括ApplicationMaster程序、启动 ApplicationMaster的命令、用户程序等。 +- 第2步:Resourcemanager为该应用程序分配第一个Container,并与对应的 NodeManager通信,要求它在这个Container中启动应用程序的ApplicationMaster。 +- 第3步:Applicationmaster首先向ResourceManager注册,这样用户可以直接通过 Resourcemanager查看应用程序的运行状态,然后它将为各个任务申请资源,并监控它的运行状态,直到运行结束。即重复步骤4~7。 +- 第4步:ApplicationMaster通过RPC协议向Resourcemanager申请和领取资源。 +- 第5步:一旦ApplicationMaster申请到资源后,便与对应的Nodemanager通信,要求它启动任务。 +- 第6步:Nodemanager为任务设置好运行环境后(包括环境变量、JAR包、二进制程序等),将任务启动命令写到一个脚本中,并通过运行该脚本启动任务。 +- 第7步:各个任务通过RPC协议向ApplicationMaster汇报自己的状态和进度,让 ApplicationMaster随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务。在应用程序运行过程中,用户可随时通过RPC向Applicationmaster查询应用程序的当前运行状态。 +- 第8步:应用程序运行完成后,ApplicationMaster向Resourcemanager注销并关闭自己。 + +# YARN 的 RM 重启机制 +- ResourceManager负责资源管理和应用的调度,是yarn的核心组件,存在单点故障的问题。 +- ResourceManager restart重启机制是使RM在重启动时能够使yarn集群继续正常工作,并且使RM出现的失败不被用户知道。 +- 重启机制需要借助zookeeper集群来存储信息实现。 +- 重启机制并不是自动帮我们重启的意思,所以不能解决单点故障问题。 + +- 不开启RM重启机制。RM故障重启后,之前集群上跑的任务信息都会消失,正在执行的作业也会失败。 +- +- RM重启机制有两种,一种是保留工作的重启机制,另一种是不保留工作的重启机制。 + - 不保留工作的RM重启机制。只保存了application提交的信息和最终执行状态,并不保存运行过程中的相关数据,所以RM重启后,会先杀死正在执行的任务,再重新提交,从零开始执行任务。 + - 保留工作的重启机制。保存了application运行中的状态数据,所以在RM重启之后,不需要杀死之前的任务,而是接着原来执行到的进度继续执行。 + +# YARN 的高可用性架构 +- ResourceManager 负责资源管理和应用的调度,是yarn的核心组件,集群的主角色,存在单点故障问题。为了解决RM的单点故障问题,yarn设计了一套Active/Standby模式的ResourceManager HA架构。 +- HA架构能够保证运行的ResourceManager挂掉后备用的ResourceManager能迅速接替工作,保证计算任务不会中断,转换过程对用户无感。 +- 该HA架构也使用zookeeper集群实现。 + +- 实现HA集群的关键是: + - 主备之间状态数据同步、主备之间顺利切换(故障转移机制)。 +- 针对数据同步问题,可以通过zookeeper来存储共享集群的状态数据,因为zookeeper本质也是一个小文件存储系统。 +- 针对主备顺利切换,也可以基于zookeeper自动实现。 + + +# YARN 的通信协议 +- 分布式环境下,需要涉及跨机器跨网络通信,yarn底层使用RPC协议实现通信。 + +- RPC是远程过程调用( Remote procedure call)的缩写形式。 +- 基于RPC进行远程调用就像本地调用一样。 +- 在RPC协议中,通信双方有一端是Client,另一端为Server,且Client总是主动连接Server的。因此,yarn实际上采用的是拉式(pull-based)通信模型。 \ No newline at end of file diff --git "a/_posts/2022-01-27-Hadoop\357\274\232\345\237\272\344\272\216 Docker \346\220\255\345\273\272\344\274\252\345\210\206\345\270\203\345\274\217 Hadoop \351\233\206\347\276\244.md" "b/_posts/2022-01-27-Hadoop\357\274\232\345\237\272\344\272\216 Docker \346\220\255\345\273\272\344\274\252\345\210\206\345\270\203\345\274\217 Hadoop \351\233\206\347\276\244.md" new file mode 100644 index 00000000000..dce0ad50e59 --- /dev/null +++ "b/_posts/2022-01-27-Hadoop\357\274\232\345\237\272\344\272\216 Docker \346\220\255\345\273\272\344\274\252\345\210\206\345\270\203\345\274\217 Hadoop \351\233\206\347\276\244.md" @@ -0,0 +1,439 @@ +--- +layout: post +title: Hadoop:基于 Docker 搭建伪分布式 Hadoop 集群 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hadoop +--- + +# 1. Windows 环境安装 docker + +#### 1.1. 更新 WSL 2 Linux 内核 + +- 下载 WSL 2 Linux 内核,下载地址:[https://docs.microsoft.com/zh-cn/windows/wsl/wsl2-kernel](https://docs.microsoft.com/zh-cn/windows/wsl/wsl2-kernel) +- 双击下载好的 wsl_update_x64.msi 文件,安装 WSL; + +#### 1.2. 安装 docker for windows 到 D 盘 + +- 在 `D:\Program Files` 目录下,新建 `Docker` 文件夹; +- 创建 mklink 链接 + ```aidl + cd "C:\Program Files" + mklink /j "C:\Program Files\Docker" "D:\Program Files\Docker" + + # 得到如下提示,标识创建成功 + C:\Program Files\Docker <<===>> D:\Program Files\Docker + ``` +- 下载 docker for windows,下载地址:[https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe](https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe) +- 双击 Docker Desktop Installer,开始安装; + +#### 1.3. 配置阿里云镜像加速器 + +- 如果没有阿里云账号,需要注册阿里云开发账号,[https://dev.aliyun.com/](https://dev.aliyun.com/); + +- 进入加速器页面获取镜像加速器地址,[https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors](https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors) + + ![]({{site.baseurl}}/img-post/docker-hadoop-3.png) + +- 打开 Docker Desktop,在桌面右下角找到 docker 鲸鱼小图标,依次点击 **鼠标右键 -> settings -> Docker Engine**,在输入框中输入加速信息; + + ``` + { + "registry-mirrors": [ + "在此处粘贴上面复制的加速器地址,注意要加引号" + ], + "insecure-registries": [], + "debug": true, + "experimental": false + } + ``` + +- 如下图所示: + ![]({{site.baseurl}}/img-post/docker-hadoop-4.png) +- 等待 docker desktop 重启,配置完成。 + +#### 1.4. WSL 2 installation is incomplete. 错误 + + ![]({{site.baseurl}}/img-post/docker-hadoop-1.png) + +- 报错提示使用的wsl2版本老了,需要我们自己手动更新一下,根据提示去微软官网下载最新版的wsl2安装后即可正常打开。 + +- 下载地址:[https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi](https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi) + +# 2. 准备 CentOS 镜像环境 + +#### 2.1. 获取一个 CnetOS 镜像 + +- 直接获取官方镜像即可,此处我是用 CentOS 8,具体步骤不再赘述。 + +#### 2.2. 替换 mirror 源 + +- 由于 CentOS8 于 2021年12月31日 停止了源的服务,所以需要手动替换 mirror 源,否则 yum 安装的时候会报错。 + - 报错内容: + ``` + #5 3.638 Error: Failed to download metadata for repo 'AppStream': Cannot prepare internal mirrorlist: No URLs in mirrorlist + ``` + +- 启动容器 +- 在容器内部,我们进行替换源操作。 + + ```aidl + cd /etc/yum.repos.d/ + sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* + sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* + yum update -y + wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo + yum clean all + yum makecache + ``` + +- 到这一步时仍然会报错,报错如下: + + ```aidl + Errors during downloading metadata for repository 'base': + - Status code: 404 for https://mirrors.aliyun.com/centos/8/BaseOS/x86_64/os/repodata/repomd.xml (IP: 202.100.222.210) + ``` + +- 这是因为阿里云改了源文件结构,但是在repo配置文件中没有同步修改,需要修改一下repo文件中的路径信息。 + + ```aidl + sed -i 's/$releasever/8-stream/g' /etc/yum.repos.d/CentOS-AppStream.repo + sed -i 's/$releasever/8-stream/g' /etc/yum.repos.d/CentOS-Base.repo + sed -i 's/$releasever/8-stream/g' /etc/yum.repos.d/CentOS-Extras.repo + ``` +- 重新执行 `yum makecache`,提示 `Metadata cache created.`。 +- docker commit 打包容器成新的镜像。 + ```docker commit d32673714e72 centos8-alimirror-env:1.0``` +- 此时我们可以 push 一下这个镜像,以方便其他项目使用。 + ```aidl + docker login -u 你的 dockerhub 账号 + + docker tag centos8-alimirror-env:1.0 你的dockerhub账号/centos8-alimirror-env:1.0 + docker push + ``` + - 注意: + - push 前一定要给上传的镜像重新打一个 tag,否则 push 的时候会报错:`denied: requested access to the resource is denied`。 + +#### 2.3. 创建 dockerfile + +- 创建 dockerfile 文件,注意没有后缀。 + + ```aidl + FROM pandong423/centos8-alimirror-env:1.0 + + # 安装必要软件 + RUN yum install -y net-tools which openssh-clients openssh-server iproute.x86_64 wget passwd vim + + # 修改root密码 + RUN echo "root:root" | chpasswd + + # 添加root用户到sudoers,允许使用sudo命令 + RUN echo "root ALL=(ALL) ALL" >> /etc/sudoers + + # ssh用的公私钥认证,需要把PAM认证中的UsePAM yes改为no + RUN sed -i 's/UsePAM yes/UsePAM no/g' /etc/ssh/sshd_config + + # 创建ssh所需的文件夹/var/run/sshd,否则ssh启动不起来 + RUN mkdir /var/run/sshd + + # 生成无密码的公私密钥对 + RUN ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa + + # 公钥添加授权文件authorized_keys中 + RUN cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys + + # 设置初次ssh登录时,无需yes确认登陆 + RUN echo "StrictHostKeyChecking no" >~/.ssh/config + RUN echo "alias ll='ls -l'" >> ~/.bashrc \ + RUN source ~/.bashrc + + # 暴露 22 端口 + EXPOSE 22 + CMD ["/usr/sbin/sshd", "-D"] + ``` + +#### 2.4. build 环境镜像 + +- 创建环境准备镜像 + ```aidl + docker build -t centos8-hadoop-env:v1.0 . --no-cache + ``` +- 此处我们将新镜像 push 到 dockerhub,以备使用。 + ```aidl + docker tag centos8-hadoop-env:v1.0 你的dockerhub账号/centos8-hadoop-env:v1.0 + docker push 你的dockerhub账号/centos8-hadoop-env:v1.0 + ``` + +# 3. 搭建 hadoop 集群 + +#### 3.1. 创建包含 Hadoop 和 JDK 的镜像 + +- 创建 dockerfile + - 刚才已经创建了一个Dockerfile了,先将他移开。 + + ```aidl + mv dockerfile dockerfile.bak + ``` + - 新建编辑 dockerfile + - **注意:添加环境变量时可能不生效,如果不生效则使用替换代码,注意修改**。 + ```aidl + FROM centos8-hadoop-env:v1.0 + + # 配置 jdk + ADD jdk-8u212-linux-x64.tar.gz /usr/local/ + RUN mv /usr/local/jdk1.8.0_212 /usr/local/jdk1.8 + + # 添加环境变量 JAVA_HOME + ENV JAVA_HOME /usr/local/jdk1.8 + ENV PATH $PATH:$JAVA_HOME/bin + + # 注意:上面的添加环境变量代码可能不生效,如果不生效可以尝试用下面的代码替换。 + RUN export JAVA_HOME=/usr/local/jdk1.8 + RUN export PATH=$PATH:$JAVA_HOME/bin:$JAVA_HOME/sbin + + # 配置 hadoop + ADD hadoop-3.1.3.tar.gz /usr/local + RUN mv /usr/local/hadoop-3.1.3 /usr/local/hadoop + + # 添加环境变量 HADOOP_HOME + ENV HADOOP_HOME /usr/local/hadoop + ENV PATH $PATH:$HADOOP_HOME/bin + + # 注意:上面的添加环境变量代码可能不生效,如果不生效可以尝试用下面的代码替换。 + RUN export HADOOP_HOME=/usr/local/hadoop + RUN export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin + ``` +- 创建镜像 + ```aidl + docker build -t centos8-jdk1.8-hadoop3.1.3:v1.0 . + ``` +- 检查端口禁用情况 + - 注意检查哪些端口是被禁用的,自行选择合适的对外暴露端口 + + ``` + netsh interface ipv4 show excludedportrange protocol=tcp + ``` + + 我的电脑端口禁用情况如下: + + ``` + 协议 tcp 端口排除范围 + + 开始端口 结束端口 + ---------- -------- + 49686 49785 + 49786 49885 + 49886 49985 + 50000 50059 * + 50060 50159 + 50160 50259 + 50262 50361 + 50362 50461 + 50462 50561 + 50562 50661 + 50662 50761 + 50762 50861 + 50862 50961 + 64571 64670 + 64671 64770 + 64771 64870 + 64871 64970 + + * - 管理的端口排除。 + ``` + - 我选择 `50260` 做为对外暴露端口。 + +- 创建 bridge 类型网络,让集群间能网络连通。 + ```aidl + docker network create --driver bridge hadoop-br + ``` + +#### 3.2. 启动 hadoop 容器 +- 启动容器 + ```aidl + docker run -itd --network hadoop-br --name hadoop1 -p 50260:50070 -p 8088:8088 centos8-jdk1.8-hadoop3.1.3:v1.0 + docker run -itd --network hadoop-br --name hadoop2 centos8-jdk1.8-hadoop3.1.3:v1.0 + docker run -itd --network hadoop-br --name hadoop3 centos8-jdk1.8-hadoop3.1.3:v1.0 + ``` + +- 检查网络 + ```aidl + [ + { + "Name": "hadoop-br", + "Id": "1ccc41f2821cd700882eca4c161f2a245c9a17c365fc808ce1ddfd0f088fb274", + "Created": "2022-05-04T10:53:24.57192Z", + "Scope": "local", + "Driver": "bridge", + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": {}, + "Config": [ + { + "Subnet": "172.18.0.0/16", + "Gateway": "172.18.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Containers": { + "08a2bb09245b86d3cbf708987740f6f65edbdae8576f2fcceca5abcdcdaa2d97": { + "Name": "hadoop3", + "EndpointID": "114f83d2fb2181d9ac446c0ac2183440c16da46c6446b292fca2825373acc0b6", + "MacAddress": "02:42:ac:12:00:04", + "IPv4Address": "172.18.0.4/16", + "IPv6Address": "" + }, + "b38a377496a3cd83bf8965365913cd83da8319cff42ca4e0cc8d4318a3306ceb": { + "Name": "hadoop1", + "EndpointID": "88a0042804a9848451928f608d33cb16300364aba84a7b108b02727882b23e69", + "MacAddress": "02:42:ac:12:00:02", + "IPv4Address": "172.18.0.2/16", + "IPv6Address": "" + }, + "f417a6f26827993d83c0d17c3a69365a6bd832d5dbd4a306da495c3f3d968532": { + "Name": "hadoop2", + "EndpointID": "6e042fac8decedf98cc8d6ca9bb7c0ddb193e13a8eb8873d445c9105373c34aa", + "MacAddress": "02:42:ac:12:00:03", + "IPv4Address": "172.18.0.3/16", + "IPv6Address": "" + } + }, + "Options": {}, + "Labels": {} + } + ] + ``` + - 查看得知三台机器的 IP 分别为: + ```aidl + hadoop1 172.18.0.2/16 + hadoop2 172.18.0.3/16 + hadoop3 172.18.0.4/16 + ``` + +- 检查是否能互相 ping 通、是否能 ssh 免密登录 + - hadoop1 + ```aidl + docker exec -it hadoop1 bash + + ping hadoop2 + ping hadoop3 + + ssh hadoop2 + ``` + - hadoop2 + - 略 + - hadoop3 + - 略 + +#### 3.3. 安装配置 Hadoop + +- 进入 $Hadoop + ```aidl + cd $HADOOP_HOME/etc/hadoop/ + ``` +- 编辑 core-site.xml + ```aidl + + fs.defaultFS + hdfs://hadoop1:9000 + + + hadoop.tmp.dir + file:/home/hadoop/tmp + + + io.file.buffer.size + 131702 + + ``` + +- 编辑 hdfs-site.xml + ```aidl + + dfs.namenode.name.dir + file:/home/hadoop/hdfs_name + + + dfs.datanode.data.dir + file:/home/hadoop/hdfs_data + + + dfs.replication + 2 + + + dfs.namenode.secondary.http-address + hadoop1:9001 + + + dfs.webhdfs.enabled + true + + ``` + +- 编辑 mapred-site.xml + ```aidl + + mapreduce.framework.name + yarn + + + mapreduce.jobhistory.address + hadoop1:10020 + + + mapreduce.jobhistory.webapp.address + hadoop1:19888 + + ``` + +- 编辑 yarn-site.xml + ```aidl + + yarn.nodemanager.aux-services + mapreduce_shuffle + + + yarn.nodemanager.auxservices.mapreduce.shuffle.class + org.apache.hadoop.mapred.ShuffleHandler + + + yarn.resourcemanager.address + hadoop1:8032 + + + yarn.resourcemanager.scheduler.address + hadoop1:8030 + + + yarn.resourcemanager.resource-tracker.address + hadoop1:8031 + + + yarn.resourcemanager.admin.address + hadoop1:8033 + + + yarn.resourcemanager.webapp.address + hadoop1:8088 + + ``` + + + + + + + diff --git "a/_posts/2022-01-27-Hadoop\357\274\232\345\237\272\344\272\216 VMware \346\220\255\345\273\272\344\274\252\345\210\206\345\270\203\345\274\217 Hadoop \351\233\206\347\276\244.md" "b/_posts/2022-01-27-Hadoop\357\274\232\345\237\272\344\272\216 VMware \346\220\255\345\273\272\344\274\252\345\210\206\345\270\203\345\274\217 Hadoop \351\233\206\347\276\244.md" new file mode 100644 index 00000000000..3cfc9ee2900 --- /dev/null +++ "b/_posts/2022-01-27-Hadoop\357\274\232\345\237\272\344\272\216 VMware \346\220\255\345\273\272\344\274\252\345\210\206\345\270\203\345\274\217 Hadoop \351\233\206\347\276\244.md" @@ -0,0 +1,394 @@ +--- +layout: post +title: Hadoop:基于 VMware 搭建伪分布式 Hadoop 集群 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hadoop +--- + +# 1. 集群配置 + +#### 1.1. 获取 root 权限 + +```aidl +su +# 输入密码 +``` + +#### 1.2. 修改主机名 + +- hadoop-1 +```aidl +hostnamectl set-hostname hadoop-1 +``` +- hadoop-2 +```aidl +hostnamectl set-hostname hadoop-2 +``` +- hadoop-3 +```aidl +hostnamectl set-hostname hadoop-3 +``` + +#### 1.3. 修改网卡配置 + +```aidl +cd /etc/sysconfig/network-scripts/ +vim ifcfg-ens33 +``` + +```aidl +PROXY_METHOD="nc" +BROWSER_ONLY="nc" +BOOTPROTO="static" +NAME="ens33" +DEVICE="ens33" +ONBOOT="yes" +IPADDR=192.168.1.101 +NETMASK=255.255.255.0 +GATEWAY=192.168.1.254 +DNS1=192.168.1.254 # 注意:这里不配置会导致虚拟机上不了网 +``` + +``` +# 启动生效 +systemctl restart network +``` + +#### 1.4. 主机名与IP映射 + +```aidl + +vi /etc/hosts + +192.168.1.101 master +192.168.1.102 slave1 +192.168.1.103 slave2 +``` + +#### 1.5. 配置SSH免密登录 + +>此处只在 master 上配置免密登录 slave + +``` + +#进入用户目录 +cd /home/用户名 + +#生成密钥,回车即可 +ssh-keygen -t rsa + +#到.ssh目录下 +cd /root/.ssh/ + +#将id_rsa.pub添加到authorized_keys目录 +cp id_rsa.pub authorized_keys + +ssh-copy-id -i slave1 + +ssh-copy-id -i slave2 +``` + +#### 1.6. NAT配置 + +![]({{site.baseurl}}/img-post/hadoop-1.png) + +![]({{site.baseurl}}/img-post/hadoop-2.png) + +![]({{site.baseurl}}/img-post/hadoop-3.png) + + + +# 2. 安装 Hadoop + +#### 2.1. 配置 jdk + +- 删除原生 java,注意 master 和 slave 机器都要删掉 + - 查找jdk 安装位置 + ``` + rpm -qa | grep java + javapackages-tools-3.4.1-11.el7.noarch + java-1.8.0-openjdk-headless-1.8.0.262.b10-1.el7.x86_64 + tzdata-java-2020a-1.el7.noarch + java-1.7.0-openjdk-headless-1.7.0.261-2.6.22.2.el7_8.x86_64 + java-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64 + python-javapackages-3.4.1-11.el7.noarch + java-1.7.0-openjdk-1.7.0.261-2.6.22.2.el7_8.x86_64 + ``` + - 删除 + + ``` + rpm -e --nodeps javapackages-tools-3.4.1-11.el7.noarch + rpm -e --nodeps java-1.8.0-openjdk-headless-1.8.0.262.b10-1.el7.x86_64 + rpm -e --nodeps tzdata-java-2020a-1.el7.noarch + rpm -e --nodeps java-1.7.0-openjdk-headless-1.7.0.261-2.6.22.2.el7_8.x86_64 + rpm -e --nodeps java-1.8.0-openjdk-1.8.0.262.b10-1.el7.x86_64 + rpm -e --nodeps python-javapackages-3.4.1-11.el7.noarch + rpm -e --nodeps java-1.7.0-openjdk-1.7.0.261-2.6.22.2.el7_8.x86_64 + ``` + + - 检查有没有删除 + ``` + java -version + bash: java: command not found... + ``` + +- 在 home 目录下,解压缩 jdk-8u162-linux-x64.tar.gz 文件,并保存到 `jdk1.8.0` 文件目录下。 + + ```aidl + tar -zxvf jdk-8u162-linux-x64.tar.gz jdk1.8.0 + ``` + +- 配置环境变量 + ```aidl + export JAVA_HOME=/home/用户名/jdk1.8.0 + export JRE_HOME=${JAVA_HOME}/jre + export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib + export PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin + + ``` + +- 环境变量生效 + ```aidl + source .bashrc + ``` + +- 检查安装情况 + ```aidl + java -version + + java version "1.8.0_162" + Java(TM) SE Runtime Environment (build 1.8.0_162-b12) + Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode) + ``` + +- 复制文件到子节点 + ```aidl + scp -r /home/用户名/jdk1.8.0 用户名@slave1:/home/用户名/ + + scp -r /home/用户名/jdk1.8.0 用户名@slave2:/home/用户名/ + ``` + +#### 2.2. 安装配置Hadoop + +- 在 home 目录下,解压缩 Hadoop 压缩文件。 + + ```aidl + tar -zxvf hadoop-3.1.3.tar.gz + ``` +- 配置环境变量 + ```aidl + vim .bashrc + ``` + + ```aidl + export HADOOP_HOME=/home/用户/hadoop-3.1.3 + export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin + export HADOOP_INSTALL=$HADOOP_HOME + export HADOOP_MAPRED_HOME=$HADOOP_HOME + export HADOOP_COMMON_HOME=$HADOOP_HOME + export HADOOP_HDFS_HOME=$HADOOP_HOME + export YARN_HOME=$HADOOP_HOME + export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native + ``` + +#### 2.3. 修改hadoop配置文件 + +- 修改 .bashrc 中的 YARN 和 HDFS 配置 + ```aidl + vim .bashrc + ``` + + ```aidl + export HDFS_NAMENODE_USER=root + export HDFS_DATANODE_USER=root + export HDFS_SECONDARYNAMENODE_USER=root + export YARN_RESOURCEMANAGER_USER=root + export YARN_NODEMANAGER_USER=root + ``` + + ```aidl + cd hadoop-3.1.3/etc/hadoop/ + ``` +- 修改 core-site.xml + + ```aidl + vim core-site.xml + ``` + + ```aidl + # 在 configuration 中添加 + + fs.defaultFS + hdfs://master:9000 + + + hadoop.tmp.dir + file:/home/pandong/hadoop-3.1.3/tmp + Abase for other temporary directories. + + ``` + +- 修改 hadoop-env.sh + ```aidl + vim hadoop-env.sh + ``` + + ```aidl + # 在 hadoop-env.sh 添加内容 + export JAVA_HOME=/home/用户名/jdk1.8.0 + ``` +- 修改 hdfs-site.xml + + ```aidl + vim hdfs-site.xml + ``` + + ```aidl + + dfs.namenode.secondary.http-address + master:50090 + + + dfs.replication + 1 + + + dfs.namenode.name.dir + file:/home/用户名/hadoop-3.1.3/tmp/dfs/name + + + dfs.datanode.data.dir + file:/home/用户名/hadoop-3.1.3/tmp/dfs/data + + ``` + +- 修改 mapred-site.xml + + ```aidl + vim mapred-site.xml + ``` + + ```aidl + + mapreduce.framework.name + yarn + + + mapreduce.jobhistory.address + master:10020 + + + mapreduce.jobhistory.webapp.address + master:19888 + + ``` + +- 修改 yarn-site.xml + + ```aidl + vim yarn-site.xml + ``` + + ```aidl + + yarn.resourcemanager.hostname + master + + + yarn.nodemanager.aux-services + mapreduce_shuffle + + ``` + +- workers + + ```aidl + vim workers + ``` + + ```aidl + slave1 + slave2 + ``` + +#### 2.4. 复制文件到子节点 + + ```aidl + scp -r /home/用户名/hadoop-3.1.3 用户名@slave1:/home/用户名/ + + scp -r /home/用户名/hadoop-3.1.3 用户名@slave2:/home/用户名/ + ``` + + ```aidl + scp -r .bashrc 用户@slave1:/home/用户/ + + scp -r .bashrc 用户@slave2:/home/用户/ + ``` + +#### 2.5. 格式化 + +- 关闭enforce + + ```aidl + vi /etc/selinux/config + ``` + +- 切换 root 权限 + + ```aidl + su + ``` + +- 修改SELINUX + + ```aidl + SELINUX=disabled + ``` + +- 退出 root,格式化 + + ```aidl + hdfs namenode -format + ``` + +#### 2.6. 启动hadoop + +- 运行全部 + + ```aidl + start-all.sh + ``` + + ```aidl + WARNING: HADOOP_SECURE_DN_USER has been replaced by HDFS_DATANODE_SECURE_USER. Using value of HADOOP_SECURE_DN_USER. + Starting namenodes on [master] + Last login: Mon Mar 7 08:10:16 PST 2022 on pts/0 + Starting datanodes + Last login: Mon Mar 7 08:10:36 PST 2022 on pts/0 + Starting secondary namenodes [master] + Last login: Mon Mar 7 08:10:39 PST 2022 on pts/0 + 2022-03-07 08:10:55,169 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable + Starting resourcemanager + Last login: Mon Mar 7 08:10:46 PST 2022 on pts/0 + Starting nodemanagers + Last login: Mon Mar 7 08:10:57 PST 2022 on pts/0 + ``` + +- mapreduce:http://master:8088/cluster + + ![]({{site.baseurl}}/img-post/hadoop-4.png) + +- NameNode and Datanode: http://master:9870 + + ![]({{site.baseurl}}/img-post/hadoop-5.png) + +- 关闭全部 + + ```aidl + stop-all.sh + ``` diff --git "a/_posts/2022-01-27-Hadoop\357\274\232\345\237\272\344\272\216 \351\230\277\351\207\214\344\272\221\346\234\215\345\212\241\345\231\250 \346\220\255\345\273\272 Hadoop \351\233\206\347\276\244.md" "b/_posts/2022-01-27-Hadoop\357\274\232\345\237\272\344\272\216 \351\230\277\351\207\214\344\272\221\346\234\215\345\212\241\345\231\250 \346\220\255\345\273\272 Hadoop \351\233\206\347\276\244.md" new file mode 100644 index 00000000000..30114517d3b --- /dev/null +++ "b/_posts/2022-01-27-Hadoop\357\274\232\345\237\272\344\272\216 \351\230\277\351\207\214\344\272\221\346\234\215\345\212\241\345\231\250 \346\220\255\345\273\272 Hadoop \351\233\206\347\276\244.md" @@ -0,0 +1,474 @@ +--- +layout: post +title: Hadoop:基于 阿里云服务器 搭建 Hadoop 集群 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Hadoop +--- + +# 1. 环境准备 + +#### 1.1. 配置主机映射 + +- 设置服务器的映射关系,在三台服务器依次操作。 + + ```aidl + vi /etc/hosts + ``` + + ```aidl + 172.xxx.xxx.xxx hadoop102 + 172.xxx.xxx.xxx hadoop104 + 172.xxx.xxx.xxx hadoop103 + ``` + +#### 1.2. 配置免密登录 + +- 生成 ssh 秘钥并复制。 + + ``` + cd ~/.ssh/ + + ssh-keygen -t rsa + + ssh-copy-id hadoop102 + ssh-copy-id hadoop103 + ssh-copy-id hadoop104 + ``` + +#### 1.3. 创建文件分发脚本 + +- 分别在三台机器上安装 rsync + + ```aidl + yum install rsync + ``` + +- 在 bin 目录下创建 xsync; + + ```aidl + cd /bin + + vim xsync + ``` + +- 编辑脚本; + + ```aidl + #!/bin/bash + #1 获取输入参数个数,如果没有参数,直接退出 + pcount=$# + if((pcount==0)); then + echo no args; + exit; + fi + + #2 获取文件名称 + p1=$1 + fname=`basename $p1` + echo fname=$fname + + #3 获取上级目录到绝对路径 + pdir=`cd -P $(dirname $p1); pwd` + echo pdir=$pdir + + #4 获取当前用户名称 + user=`whoami` + + #5 循环 + for((host=103; host<105; host++)); do + echo ------------------- hadoop$host -------------- + rsync -rvl $pdir/$fname $user@hadoop$host:$pdir + done + ``` + +- 修改权限 + + ```aidl + chmod 777 xsync + ``` + +#### 1.4. 安装 JDK + +- 上传文件 + - 上传 `jdk-8u162-linux-x64.tar.gz` 到 hadoop102 服务器 `/opt/software` 目录下; +- 解压安装 + + ```aidl + tar -zxvf jdk-8u162-linux-x64.tar.gz -C /opt/module/ + + mv /opt/module/jdk1.8.0_162/ /usr/local/bin/jdk1.8 + ``` +- 分发 jdk + ```aidl + xsync /usr/local/bin/jdk1.8/ + ``` +- 添加环境变量 + ``` + sudo vi /etc/profile + + # 在文件末尾添加 PATH 路径 + export JAVA_HOME=/usr/local/bin/jdk1.8 + export PATH=$PATH:$JAVA_HOME/bin + ``` +- 分发 `/etc/profile` + ``` + xsync /etc/profile + ``` +- 在三台服务器上,依次启用环境变量 + ``` + source /etc/profile + ``` +- 检查 jdk + ``` + java -version + ``` + +# 2. 安装 Hadoop + +#### 2.1. 安装 + +- 上传文件 + - 上传文件 `hadoop-3.1.3.tar.gz` 到 hadoop102 服务器 `/opt/software` 目录 +- 解压安装 + ``` + tar -vxf hadoop-3.1.3.tar.gz -C /opt/module/ + + mv /opt/module/hadoop-3.1.3/ /usr/local/hadoop-3.1.3 + ``` +- 分发 hadoop 文件 + ``` + xsync /usr/local/hadoop-3.1.3/ + ``` + +#### 2.2. 添加环境变量 + +- 添加环境变量 + ``` + export HADOOP_HOME=/usr/local/hadoop-3.1.3 + export PATH=$PATH:$HADOOP_HOME/bin + export PATH=$PATH:$HADOOP_HOME/sbin + export HADOOP_INSTALL=$HADOOP_HOME + export HADOOP_MAPRED_HOME=$HADOOP_HOME + export HADOOP_COMMON_HOME=$HADOOP_HOME + export HADOOP_HDFS_HOME=$HADOOP_HOME + export YARN_HOME=$HADOOP_HOME + export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native + ``` + +- 分发 `/etc/profile` + ``` + xsync /etc/profile + ``` + +- 在三台服务器上,依次启用环境变量 + ``` + source /etc/profile + ``` + +- 检查安装情况 + ``` + hadoop version + ``` + +- 查看 Hadoop 目录结构 + ```aidl + cd $HADOOP_HOME + ll + + drwxr-xr-x 2 root root 4096 May 8 22:04 bin + drwxr-xr-x 3 root root 4096 May 8 22:04 etc + drwxr-xr-x 2 root root 4096 May 8 22:04 include + drwxr-xr-x 3 root root 4096 May 8 22:04 lib + drwxr-xr-x 4 root root 4096 May 8 22:04 libexec + -rw-r--r-- 1 root root 147145 May 8 22:04 LICENSE.txt + -rw-r--r-- 1 root root 21867 May 8 22:04 NOTICE.txt + -rw-r--r-- 1 root root 1366 May 8 22:04 README.txt + drwxr-xr-x 3 root root 4096 May 8 22:04 sbin + drwxr-xr-x 4 root root 4096 May 8 22:04 share + ``` + +# 3. 配置 Hadoop 集群 + +#### 3.1. 集群规划配置 + + ![]({{site.baseurl}}/img-post/ali-hadoop-1.png) + +#### 3.2. 修改集群配置文件 + +- 注意: + - 一定要在 `hadoop102` 上执行以下操作! + +- 进入配置文件目录 + + ``` + cd /usr/local/hadoop-3.1.3/etc/hadoop + ``` + +- 修改 `core-site.xml` + + ```aidl + vi core-site.xml + ``` + + ```aidl + # 在 configuration 中添加 + + + + fs.defaultFS + hdfs://hadoop102:9000 + + + + hadoop.tmp.dir + file:/usr/local/hadoop-3.1.3/tmp + Abase for other temporary directories. + + ``` + +- 修改 `hadoop-env.sh` + + ```aidl + vi hadoop-env.sh + + # 在 hadoop-env.sh 添加内容 + export JAVA_HOME=/usr/local/bin/jdk1.8 + ``` +- 修改 `hdfs-site.xml` + + ```aidl + vi hdfs-site.xml + ``` + + ```aidl + + + + dfs.namenode.secondary.http-address + hadoop104:50090 + + + + dfs.replication + 3 + + + + dfs.namenode.name.dir + file:/usr/local/hadoop-3.1.3/tmp/dfs/name + + + + dfs.datanode.data.dir + file:/usr/local/hadoop-3.1.3/tmp/dfs/data + + + + dfs.permissions.enabled + false + + ``` + +- 修改 `mapred-env.sh` + + ```aidl + vi mapred-env.sh + + export JAVA_HOME=/usr/local/bin/jdk1.8 + ``` + +- 修改 `mapred-site.xml` + + ```aidl + vim mapred-site.xml + ``` + + ```aidl + + + mapreduce.framework.name + yarn + + + + mapreduce.jobhistory.address + hadoop102:10020 + + + + mapreduce.jobhistory.webapp.address + hadoop102:19888 + + ``` + +- 修改 `yarn-env.sh` + + ``` + vi yarn-env.sh + + export JAVA_HOME=/usr/local/bin/jdk1.8 + ``` + +- 修改 `yarn-site.xml` + + ```aidl + vim yarn-site.xml + ``` + + ```aidl + + + yarn.resourcemanager.hostname + hadoop103 + + + + yarn.nodemanager.resource.memory-mb + 4096 + + + + yarn.scheduler.minimum-allocation-mb + 512 + + + + yarn.scheduler.maximum-allocation-mb + 4096 + + + + yarn.nodemanager.vmem-check-enabled + false + + + + yarn.nodemanager.pmem-check-enabled + false + + + + yarn.nodemanager.vmem-pmem-ratio + 5 + + + + yarn.nodemanager.aux-services + mapreduce_shuffle + + + + + yarn.log-aggregation-enable + true + + + + yarn.log-aggregation.retain-seconds + 604800 + + + + yarn.log.server.url + http://hadoop102:19888/jobhistory/logs/ + + + + yarn.nodemanager.vmem-check-enabled + false + + ``` +- 创建 `slaves` + + ``` + vi slaves + + hadoop102 + hadoop103 + hadoop104 + ``` + +#### 3.3. 分发文件 + +- 使用 xsync 分发 + + ```aidl + xsync /usr/local/hadoop-3.1.3 + ``` + +#### 3.4. 初始化 + +- 在 hadoop102 上执行初始化命令 + + ```aidl + hdfs namenode -format + ``` + +#### 3.5. 启动 HDFS + +- 在 hadoop102 上启动 hdfs + ```aidl + start-dfs.sh + ``` +- 在 hadoop102 上启动 yarn + ```aidl + start-yarn.sh + ``` +- 检查启动情况 + ```aidl + javapsall + + # 下面的提示标识启动成功 + xcall jps + -------hadoop102------ + 13586 DataNode + 13994 Jps + 13933 JobHistoryServer + 13455 NameNode + -------hadoop102------ + 13586 DataNode + 14009 Jps + 13933 JobHistoryServer + 13455 NameNode + -------hadoop103------ + 5040 Jps + 4703 NodeManager + 4575 ResourceManager + -------hadoop104------ + 3408 SecondaryNameNode + 3443 Jps + ``` + +#### 3.6. 群启群停 + +- 群启 + + ```aidl + start-all.sh + ``` + +- 群停 + + ```aidl + stop-all.sh + ``` + +# 4. Web UI + +#### 4.1. 安全组设置 + + ![]({{site.baseurl}}/img-post/hadoop-8.png) + +#### 4.2. 访问地址 + +- hdfs + - http://mater节点IP:9870/ + ![]({{site.baseurl}}/img-post/hadoop-9.png) +- yarn + - http://ResourceManager节点IP:8088/ + ![]({{site.baseurl}}/img-post/hadoop-10.png) diff --git "a/_posts/2022-01-27-Spark\357\274\232Hadoop 3.1.2 + Spark 3.1.2 \351\233\206\347\276\244\345\256\211\350\243\205\351\203\250\347\275\262.md" "b/_posts/2022-01-27-Spark\357\274\232Hadoop 3.1.2 + Spark 3.1.2 \351\233\206\347\276\244\345\256\211\350\243\205\351\203\250\347\275\262.md" new file mode 100644 index 00000000000..315cf96af4d --- /dev/null +++ "b/_posts/2022-01-27-Spark\357\274\232Hadoop 3.1.2 + Spark 3.1.2 \351\233\206\347\276\244\345\256\211\350\243\205\351\203\250\347\275\262.md" @@ -0,0 +1,129 @@ +--- +layout: post +title: Spark:Hadoop 3.1.2 + Spark 3.1.2 集群安装部署 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Spark +--- + + +# 1. 规划 + +#### 1.1. 版本 + +- Hadoop 3.1.2 +- Spark 3.1.2 + +#### 1.2. 集群规划 + +- Master:Hadoop102 +- Slave1:Hadoop103 +- Slave2:Hadoop104 + +# 2. 安装软件 + +- 上传 + - 上传 spark 压缩文件 `spark-3.1.2-bin-hadoop3.2.tgz` 到 `/usr/local` +- 解压缩 + ``` + tar -vxf spark-3.1.2-bin-hadoop3.2.tgz + mv spark-3.1.2-bin-hadoop3.2 spark + ``` + +# 3. 配置环境变量 + +- 修改 `/etc/profile` + + ``` + xsync /etc/profile + ``` + + ``` + export SPARK_HOME=/usr/local/spark + export PATH=$PATH:${SPARK_HOME}/bin:${SPARK_HOME}/sbin + ``` + +- 分发文件 + + ``` + xsync /etc/profile + ``` + +- 依次启用生效 + - 在三台机器依次启用环境变量 + ``` + /etc/profile + ``` + - 注意:此时可能会报错 + +- 报错:`/usr/libexec/grepconf.sh:行5: grep: 未找到命令` + + - 问题: + - 在 `/etc/profile` 配置 SPARK_HOME 后,执行 `source /etc/profile` 环境变量生效,提示报错; + - 报错后命令行常规命令均无法正常使用 + + - 经排查发现路径配置错误,需要修改路径,重新启用环境变量配置文件; + + - 解决方法: + - 修改配置文件 SPARK_HOME 路径; + - cmd 窗口执行如下命令: + ``` + export PATH=/usr/bin:/usr/sbin:/bin:/sbin:/usr/X11R6/bin + ``` + - 重新启用环境变量配置文件 + ``` + source /etc/profile + ``` + +# 4. 修改 spark 配置 + +- 修改 `spark-env.sh` + ``` + vim /spark/conf/spark-env.sh + ``` + + ``` + export JAVA_HOME=/usr/local/bin/jdk1.8 + export SPARK_MASTER_IP=8.142.72.198 + export SPARK_WORKER_MEMORY=1g + export HADOOP_CONF_DIR=/usr/local/hadoop-3.1.3/etc/hadoop + ``` + +- 添加 slave 配置 + + ``` + hadoop103 + hadoop104 + ``` + +# 5. 分发 spark + +- 分发 spark 文件夹 + ``` + xsync spark/ + ``` + +# 6. 启动文件 + +- start-all.sh + ``` + cd /usr/local/spark + ./sbin/start-all.sh + ``` +- 启动成功 + ``` + starting org.apache.spark.deploy.master.Master, logging to /usr/local/spark/logs/spark-root-org.apache.spark.deploy.master.Master-1-hadoop102.out + hadoop104: starting org.apache.spark.deploy.worker.Worker, logging to /usr/local/spark/logs/spark-root-org.apache.spark.deploy.worker.Worker-1-hadoop104.out + hadoop103: starting org.apache.spark.deploy.worker.Worker, logging to /usr/local/spark/logs/spark-root-org.apache.spark.deploy.worker.Worker-1-hadoop103.out + ``` + +# 7. WEB UI + +- `hadoop102:8080` + + ![]({{site.baseurl}}/img-post/spark-3.png) + diff --git "a/_posts/2022-01-27-Spark\357\274\232Spark \344\270\216 MapReduce \347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" "b/_posts/2022-01-27-Spark\357\274\232Spark \344\270\216 MapReduce \347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" new file mode 100644 index 00000000000..c28a5ff7129 --- /dev/null +++ "b/_posts/2022-01-27-Spark\357\274\232Spark \344\270\216 MapReduce \347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" @@ -0,0 +1,104 @@ +--- +layout: post +title: Spark:Spark 与 MapReduce 的对比分析 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Spark +--- + + +#### 面向内存 VS 面向磁盘 + +- MapReduce 是面向磁盘的,受限于磁盘读/写性能和网络I/O性能的约束,在处理迭代计算、实时计算、交互式数据查询等方面并不高效,但是这些计算在图计算、数据挖掘和机器学习等相关应用领域中非常常见。 + - 针对 MapReduce 这一不足,将数据存储在内存中并基于内存进行计算是一个有效的解决途径。 + +- Spark 是面向内存的大数据处理引擎,这使得 Spark 能够为多个不同数据源的数据,提供近乎实时的处理性能,适用于需要多次操作特定数据集的应用场景。 + +- 在相同的实验环境下处理相同的数据: + - 若在内存中运行,Spark 要比 MapReduce 快 100 倍; + - 在磁盘中运行时,Spark 要比 MapReduce 快 10 倍; + +- 综合各种实验表明: + - 处理迭代计算问题 Spark 要比 MapReduce 快 20 多倍; + - 计算数据分析类报表的速度,Spark 可提高 40 多倍; + - Spark 能够在 5~7 秒的延时内交互式扫描 1TB 数据集。 + +![]({{site.baseurl}}/img-post/spark-2.png) + +#### 简洁易用 VS 冗长代码 + +- 在使用 MapReduce 开发应用程序时,通常用户关注的重点与难点是如何将一个需求Job(作业)拆分成 Map 和 Reduce。 + - 由于 MapReduce 中仅为数据处理提供了两个操作,即 Map 和 Reduce,因此系统开发人员需要解决的一个难题是,如何把数据处理的业务逻辑合理有效地封装在对应的两个类中。 + - 以分词统计为例,虽然 MapReduce 固定的编程模式极大地简化了并行程序开发,但是代码至少几十行; + +- 与之相对比,Spark提供了80多个针对数据处理的基本操作,如:map、flatMap、reduceByKey、filter、cache、collect、textFile 等; + - 这使得用户基于 Spark 进行应用程序开发非常简洁高效,以分词统计为例,若换成Spark,其核心代码最短仅需一行,极大地提高了应用程序开发效率。 + - 基于Spark的WordCount程序核心代码:`sc.textFile("hdfs://master:8020/xxx/wc.input").flatMap(_.split("")).map((_,1)).reduceByKey(_ + _).collect` + +#### 附加模块交互 VS Spark Sheep + +- MapReduce 自身并没有交互模式,需要借助 Hive 和 Pig 等附加模块。 +- Spark 提供了一种命令行交互模式,即 Spark Sheep,使得用户可以获取到查询和其他操作的即时反馈。 + +- 需要注意的是,在 Spark 的实际项目开发中多用 Scala 语言: + - Scala 语言,约占 70%; + - Java 语言,约占 20%; + - Python 语言,约占10%。 + - Scala 通常使用方便、简洁的工具,其内部往往封装了更为复杂的机理,因此 Scala 与 Java 等语言比较起来,学习难度要大一些。 + +#### 数据格式和内存布局 + +- MapReduce Schema on Read 处理方式会引起较大的处理开销; +- Spark RDD 能支持粗粒度写操作,对于读操作则可以精确到每条 record,因此 RDD 可以用来作为分布式索引。此外用户可以自定义分区策略,如 Hash 分区等。 + +#### 任务调度对比 + +- Hadoop MapReduce的 Map Task 和 Reduce Task 都是进程级别的; + +- Spark Task 是基于线程模型的,Spark 通过复用线程池中的线程来减少启动、关闭 task 所需要的开销。 + +#### 执行策略对比 + +- MapReduce 在 shuffle 前需要花费大量时间进行排序; + +- Spark 在 shuffle 时只有部分场景才需要排序,支持基于 Hash 的分布式聚合,更加省时; + + +#### 方案的统一性 + +- 相对于 MapReduce,Spark 在方案的统一性方面,都有着极大的优势。 +- Spark 框架包含了多个紧密集成的组件,位于底层的是Spark Core 实现了 Spark 的作业调度、内存管理、容错、与存储系统交互等基本功能,并针对弹性分布式数据集提供了丰富的操作。 +- 在 Spark Core 的基础上,Spark 提供了一系列面向不同应用需求的组件,主要有 Spark SQL、Spark Streaming、MLlib、GraphX。 + - 这些 Spark 核心组件都以 jar 包的形式提供给用户,这意味着在使用这些组件时,无需进行复杂烦琐的学习、部署、维护和测试等一系列工作,用户只要搭建好 Spark 平台便可以直接使用这些组件,从而节省了大量的系统开发与运维成本。 + - 将这些组件放在一起,就构成了一个Spark软件栈。基于这个软件栈,Spark提出并实现了大数据处理的一种理念——“一栈式解决方案(one stack to rule them all)”,即Spark可同时对大数据进行批处理、流式处理和交互式查询,如图5所示。借助于这一软件栈用户可以简单而低耗地把各种处理流程综合在一起,充分体现了Spark的通用性。 + +#### Spark 比 MapReduce 快的原因 + +- Spark 是基于内存的大数据处理框架 + + - Spark 既可以在内存中处理一切数据,也可以使用磁盘来处理未全部装入到内存中的数据。 + - 由于内存与磁盘在读/写性能上存在巨大的差距,因此 CPU 基于内存对数据进行处理的速度要快于磁盘数倍。 + - MapReduce 对数据的处理是基于磁盘展开的: + - 一方面,MapReduce 对数据进行 Map 操作后的结果要写入磁盘中,而且 Reduce 操作也是在磁盘中读取数据; + - 另一方面,分布式环境下不同物理节点间的数据通过网络进行传输,网络性能使得该缺点进一步被放大。 + - 因此,磁盘的读/写性能、网络传输性能成为了基于MapReduce大数据处理框架的瓶颈。 + + +- Spark 具有优秀的作业调度策略 + + - Spark 中使用了有向无环图(Directed Acyclic Graph,DAG)这一概念。 + - 一个 Spark 应用由若干个作业构成: + - 首先,Spark 将每个作业抽象成一个图,图中的节点是数据集,图中的边是数据集之间的转换关系; + - 然后,Spark 基于相应的策略将 DAG 划分出若干个子图,每个子图称为一个阶段,而每个阶段对应一组任务; + - 最后,每个任务交由集群中的执行器进行计算。 + - 借助于 DAG,Spark 可以对应用程序的执行进行优化,能够很好地实现循环数据流和内存计算。 + + + + + + diff --git "a/_posts/2022-01-27-Spark\357\274\232Spark \347\256\200\344\273\213\345\217\212\345\267\245\344\275\234\345\216\237\347\220\206.md" "b/_posts/2022-01-27-Spark\357\274\232Spark \347\256\200\344\273\213\345\217\212\345\267\245\344\275\234\345\216\237\347\220\206.md" new file mode 100644 index 00000000000..92b4d2c587b --- /dev/null +++ "b/_posts/2022-01-27-Spark\357\274\232Spark \347\256\200\344\273\213\345\217\212\345\267\245\344\275\234\345\216\237\347\220\206.md" @@ -0,0 +1,137 @@ +--- +layout: post +title: Spark:Spark 简介及工作原理 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Spark +--- + + + +# Spark 简介 + +- Spark 是一种通用、快速可扩展的大数据分析引擎; + +- Spark 解决了 MapReduce 读写磁盘速度过慢的问题; + +- Spark 框架下,不需要考虑数据倾斜问题,系统会自动优化; + +- Spark 可以处理 MapReduce 离线批处理、Impala 交互式分析、Storm 流失处理、机器学习、图计算; + - 离线批处理:SparkCore + - 交互式分析:SparkSQL、SparkStreaming、StructuredStreaming + - 机器学习:MLlib + - 图计算:GraphX + +- Saprk 可以计入的数据包括: + - HDFS + - JSON + - CSV + - Parquet + - RDBMS + - ES + - Redis + - Kafka + +# Spark 特点 + +#### 快速 + +- Spark 是面向内存的大数据处理引擎,这使得 Spark 能够为多个不同数据源的数据,提供近乎实时的处理性能,适用于需要多次操作特定数据集的应用场景。 + +#### 简洁易用 + +- Spark 不仅计算性能突出,在易用性方面也是其他同类产品难以比拟的。 + - 一方面,Spark 支持 Scala、Java、Python、R 等多种语言,用户开发 Spark 程序十分方便; + - 另一方面,Spark是基于 Scala 语言开发的,由于 Scala 是一种面向对象的、函数式的静态编程语言,其强大的类型推断、模式匹配、隐式转换等功能、结合丰富的描述能力,使得 Spark 应用程序代码非常简洁。 + +- Spark 的易用性还体现在,其针对数据处理提供了丰富的操作。 + - ,Spark提供了80多个针对数据处理的基本操作,如map、flatMap、reduceByKey、filter、cache、collect、textFile等,这使得用户基于Spark进行应用程序开发非常简洁高效。 + + +- 此外,MapReduce自身并没有交互模式,需要借助Hive和Pig等附加模块。Spark则提供了一种命令行交互模式,即Spark Sheep,使得用户可以获取到查询和其他操作的即时反馈。 + +- 需要注意的是,在Spark的实际项目开发中多用Scala语言,约占70%;其次是Java,约占20%;而Python约占10%。通常使用方便、简洁的工具,其内部往往封装了更为复杂的机理,因此Scala与Java等语言比较起来,学习难度要大一些。 + +#### 通用 + +- Spark 框架包含了多个紧密集成的组件,位于底层的 Spark Core 实现了 Spark 的作业调度、内存管理、容错、与存储系统交互等基本功能,并针对弹性分布式数据集提供了丰富的操作。 +- 在 Spark Core 的基础上,Spark 提供了一系列面向不同应用需求的组件,主要有Spark SQL、Spark Streaming、MLlib、GraphX。 + +# Spark软件栈 + +- Spark 核心组件以 jar 包的形式提供给用户,在使用这些组件无需进行复杂烦琐的学习、部署、维护和测试等一系列工作,用户只要搭建好Spark平台便可以直接使用这些组件,从而节省了大量的系统开发与运维成本。 +- 将这些组件放在一起,就构成了一个Spark软件栈。基于这个软件栈,Spark提出并实现了大数据处理的一种理念——“一栈式解决方案(one stack to rule them all)”,即Spark可同时对大数据进行批处理、流式处理和交互式查询。借助于这一软件栈用户可以简单而低耗地把各种处理流程综合在一起,充分体现了Spark的通用性。 + +#### Spark SQL + +- Spark SQL 是 Spark 用来操作结构化数据的组件。 +- 通过 Spark SQL,用户可以使用 SQL 或者 Apache Hive 版本的 HQL 来查询数据。 +- Spark SQL 支持多种数据源类型,例如 Hive表、Parquet 以及 JSON 等。 +- Spark SQL 不仅为 Spark 提供了一个 SQL 接口,还支持开发者将 SQL 语句融入到 Spark 应用程序开发过程中,无论是使用 Python、Java 还是 Scala,用户可以在单个的应用中同时进行 SQL 查询和复杂的数据分析。 + +#### Spark Streaming + +- 众多应用领域对实时数据的流式计算有着强烈的需求,例如网络环境中的网页服务器日志或是由用户提交的状态更新组成的消息队列等,这些都是实时数据流。 +- Spark Streaming 是 Spark 平台上针对实时数据进行流式计算的组件,提供了丰富的处理数据流的 API。由于这些 API 与 Spark Core 中的基本操作相对应,因此开发者在熟知 Spark 核心概念与编程方法之后,编写 Spark Streaming 应用程序会更加得心应手。 +- 从底层设计来看,Spark Streaming 支持与 Spark Core 同级别的容错性、吞吐量以及可伸缩性。 + +#### MLlib + +- MLlib 是 Spark 提供的一个机器学习算法库,其中包含了多种经典、常见的机器学习算法,主要有分类、回归、聚类、协同过滤等。 +- MLlib 不仅提供了模型评估、数据导入等额外的功能,还提供了一些更底层的机器学习原语,包括一个通用的梯度下降优化基础算法。所有这些方法都被设计为可以在集群上轻松伸缩的架构。 + +#### GraphX + +- GraphX 是 Spark 面向图计算提供的框架与算法库。 +- GraphX 中提出了弹性分布式属性图的概念,并在此基础上实现了图视图与表视图的有机结合与统一; +- 同时,针对图数据处理提供了丰富的操作,例如取子图操作 subgraph、顶点属性操作 mapVertices、边属性操作mapEdges 等。 +- GraphX 还实现了与 Pregel 的结合,可以直接使用一些常用图算法,如 PageRank、三角形计数等。 + +# Spark 运行模式 + +- Spark 支持多种运行模式:本地local运行模式、分布式运行模式。 + - local mode 下,最好设置 2 个 CPU Core,此时可以同时运行 2 个 Task 任务,相当于并行计算; + - 集群模式 cluster mode: + - 管理者: + - MR:AppMaster + - Spark:Driver Program + - Flink:JobManager + - 任务执行者: + - MR:Map Task、Reduce Task + - Spark:Executor + - Flink:TaskManager +- Spark 集群的底层资源可以借助于外部的框架进行管理,目前 Spark 对 Mesos 和 Yarn 提供了相对稳定的支持。在实际生产环境中,中小规模的 Spark 集群通常可满足一般企业绝大多数的业务需求,而在搭建此类集群时推荐采用 Standalone 模式(不采用外部的资源管理框架),该模式使得 Spark 集群更加轻量级。 + +#### Spark on Yarn模式 + +在这一模式下,Spark作为一个提交程序的客户端将Spark任务提交到Yarn上,然后通过Yarn来调度和管理Spark任务执行过程中所需的资源。在搭建此模式的Spark集群过程中,需要先搭建Yarn集群,然后将Spark作为Hadoop中的一个组件纳入到Yarn的调度管理下,这样将更有利于系统资源的共享。 + +#### Spark on Mesoes模式 + +Spark和资源管理框架Mesos相结合的运行模式。Apache Mesos与Yarn类似,能够将CPU、内存、存储等资源从计算机的物理硬件中抽象地隔离出来,搭建了一个高容错、弹性配置的分布式系统。Mesos同样也采用Master/Slave架构,并支持粗粒度模式和细粒度模式两种调度模式。 + +#### Spark Standalone模式 + +该模式是不借助于第三方资源管理框架的完全分布式模式。Spark使用自己的Master进程对应用程序运行过程中所需的资源进行调度和管理。对于中小规模的Spark集群首选Standalone模式。 + + +# SparkCore:RDD 弹性分布式数据集 + +- Spark 处理数据时,将数据封装到 RDD 中,RDD 里会有很多 Partition 分区,每个分区数据被一个 Task 处理; + +![]({{site.baseurl}}/img-post/spark-1.png) + +- Saprk 和 Flink 的 task 是以线程方式运行的,而 MapReduce 是进程方式运行,线程运行速度快于进程,这是 Spark 速度更快的原因之一; +- + + + + + + + + diff --git "a/_posts/2022-01-27-Tableau\357\274\232 Tableau Prep \345\237\272\346\234\254\345\212\237\350\203\275\344\275\277\347\224\250.md" "b/_posts/2022-01-27-Tableau\357\274\232 Tableau Prep \345\237\272\346\234\254\345\212\237\350\203\275\344\275\277\347\224\250.md" new file mode 100644 index 00000000000..1c11674470a --- /dev/null +++ "b/_posts/2022-01-27-Tableau\357\274\232 Tableau Prep \345\237\272\346\234\254\345\212\237\350\203\275\344\275\277\347\224\250.md" @@ -0,0 +1,93 @@ +--- +layout: post +title: Tableau: Tableau Prep 基本功能使用 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Tableau +--- + + + + +# MySQL 数据库连接 + +- 下面这种方式,并不能成功下载驱动; + +![]({{site.baseurl}}/img-post/tableau-1.png) + +- 需要自己到 MySQL 网站,自行下载安装; + +![]({{site.baseurl}}/img-post/tableau-2.png) + +- 安装好驱动以后,就可以正常连接 MySQL; + +![]({{site.baseurl}}/img-post/tableau-3.png) + + +# 多表连接 + +#### 跨库取数 + +>Tableau Prep 不支持跨库直接拖入表,如果直接拖入窗口的话,会提示报错。 + +- 一般使用自定义 SQL 跨库添加表; + +![]({{site.baseurl}}/img-post/tableau-5.png) + +#### 内联接 + +使用内联接来合并表时,生成的表将包含与两个表均匹配的值。我们可以理解为两张表的交集。 + +#### 左联接 + +使用左联接来合并表时,生成的表将包含左侧表中的所有值以及右侧表中的对应匹配项。 + +当左侧表中的值在右侧表中没有对应匹配项时,您将在数据网格中看到 null 值。 + +#### 右联接 + +使用右联接来合并表时,生成的表将包含右侧表中的所有值以及左侧表中的对应匹配项。 + +当右侧表中的值在左侧表中没有对应匹配项时,您将在数据网格中看到 null 值。 + +#### 外联接 + +使用完全外部联接来合并表时,生成的表将包含两个表中的所有值。 + +当任一表中的值在另一个表中没有匹配项时,您将在数据网格中看到 null 值。 + + +# 数据清洗 + +#### 字段拆分 + +![]({{site.baseurl}}/img-post/tableau-8.png) + +![]({{site.baseurl}}/img-post/tableau-9.png) + + +# 输出结果 + +#### 输出结果表 + +- Tableau Prep 只负责 ETL 相关工作,具体的数值计算不在 Tableau Prep 完成。 + +![]({{site.baseurl}}/img-post/tableau-4.png) + +#### 发布数据源 + +![]({{site.baseurl}}/img-post/tableau-map-4.png) + + +#### 发布流程 + +![]({{site.baseurl}}/img-post/tableau-map-6.png) + +![]({{site.baseurl}}/img-post/tableau-map-7.png) + + + diff --git "a/_posts/2022-01-27-Tableau\357\274\232 \351\200\220\350\241\214\350\256\241\347\256\227 & \350\201\232\345\220\210\350\256\241\347\256\227.md" "b/_posts/2022-01-27-Tableau\357\274\232 \351\200\220\350\241\214\350\256\241\347\256\227 & \350\201\232\345\220\210\350\256\241\347\256\227.md" new file mode 100644 index 00000000000..bc0fd104c8e --- /dev/null +++ "b/_posts/2022-01-27-Tableau\357\274\232 \351\200\220\350\241\214\350\256\241\347\256\227 & \350\201\232\345\220\210\350\256\241\347\256\227.md" @@ -0,0 +1,35 @@ +--- +layout: post +title: Tableau:逐行计算 & 聚合计算 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Tableau +--- + + +# 问题 + +- 在 tableau 计算时经常会遇到一个场景,既先需要汇总求和、再计算比率; +- 例如,下面的场景: + - 需要先汇总不同类商品的 出库、客退,之后将 客退/出库 求得客退率; + ![]({{site.baseurl}}/img-post/tableau-10.png) + +# 逐行计算 + +- 在计算上面的需求的时候,如果我们直接用 `[1季度客退]/[1季度出库]` 计算,得到的结果其实是错误的; +- 这是因为,tableau 会先计算【每一行】的 `[1季度客退]/[1季度出库]` 结果值,之后将结果加总得到总的结果; +- 这样的出的结果并不是我们想要的,其核心原因就在于 tableau 逐行计算原则; + ![]({{site.baseurl}}/img-post/tableau-11.png) + + +# 聚合计算 + +- 在处理上面的需求的时候,我们需要先使用 `SUM` 函数,计算出 出库 和 客退 的总和,之后用两个和相除,得到的才是我们想要的结果; +- 如下图: + ![]({{site.baseurl}}/img-post/tableau-12.png) + + diff --git "a/_posts/2022-01-27-\346\225\260\344\273\223\345\237\272\347\241\200\357\274\232OLTP \344\270\216 OLAP \347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\345\217\212\344\274\230\345\214\226\347\255\226\347\225\245.md" "b/_posts/2022-01-27-\346\225\260\344\273\223\345\237\272\347\241\200\357\274\232OLTP \344\270\216 OLAP \347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\345\217\212\344\274\230\345\214\226\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..1d3d2141a9d --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\344\273\223\345\237\272\347\241\200\357\274\232OLTP \344\270\216 OLAP \347\232\204\345\257\271\346\257\224\345\210\206\346\236\220\345\217\212\344\274\230\345\214\226\347\255\226\347\225\245.md" @@ -0,0 +1,164 @@ +--- +layout: post +title: 数仓基础:OLTP 与 OLAP 的对比分析及优化策略 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 数仓 +--- + + +# 1. 概念 + +#### 1.1. 定义 + +- OLTP + + OLTP(on-line transaction processing)翻译为联机事务处理; + +- OLAP + + OLAP(On-Line Analytical Processing)翻译为联机分析处理。 + +#### 1.2. 业务目的 + +- OLTP + + OLTP是做事务处理; + +- OLAP + + OLAP是做分析处理。 + +#### 1.3. 对数据库操作 + +- OLTP + + OLTP主要是对数据的增删改; + +- OLAP + + OLAP是对数据的查询。 + +# 2. 存储模型 + +#### 2.1. 数据模型 + +- OLTP + + OLTP 关系型数据库是基于业务模型构建方便查找的二维关系表,侧重描述单一实体间的关系; + +- OLAP + + OLAP 构建多维的分层的数据模型,侧重分析整体对象的特征。 + +#### 2.2. 存储方式 + +- OLTP + + OLTP 型数据库的典型代表是关系型数据库(mysql),它的数据存储在服务器本地的文件里; + +- OLAP + + OLAP 型数据库的典型代表是分布式文件系统(hive),它的数据存储在hdfs集群的文件里。 + +#### 2.3. 存储要求 + +关系型数据库中一行数据必须存在一个关系表中,是一个元组,分量必须要符合类型要求的,还通常有一个键来标记唯一性(行粒度索引,为了高效查找)。 + +分布文件系统中的数据存储的要求简单很多(数据分块,为了批量写入),一次写多次读取;数据要么数值、要么字符,就是我们常说的维度和指标。 + +# 3. 应用场景 + +#### 3.1. OLTP 多用于业务系统 + +OLTP主要用来记录某类业务事件的发生,如购买行为,当行为产生后,系统会记录是谁在何时何地做了何事,这样的一行(或多行)数据会以增删改的方式在数据库中进行数据的更新处理操作,要求实时性高、稳定性强、确保数据及时更新成功,像公司常见的业务系统如ERP,CRM,OA等系统都属于OLTP。 + +#### 3.2. OLAP 多用于数仓和分析 + +当数据积累到一定的程度,我们需要对过去发生的事情做一个总结分析时,就需要把过去一段时间内产生的数据拿出来进行统计分析,从中获取我们想要的信息,为公司做决策提供支持,这时候就是在做OLAP了。 + +因为OLTP所产生的业务数据分散在不同的业务系统中,而OLAP往往需要将不同的业务数据集中到一起进行统一综合的分析,这时候就需要根据业务分析需求做对应的数据清洗后存储在数据仓库中,然后由数据仓库来统一提供OLAP分析。所以我们常说OLTP是数据库的应用,OLAP是数据仓库的应用,下面用一张图来简要对比。 + +![]({{site.baseurl}}/img-post/oltp-1.jpg) + +# 3. 追求目标 + +#### 3.1. OLTP 追求效率最优 + +OLTP 系统追求的效率最优解,强调数据库内存效率,强调内存各种指标的命令率,强调绑定变量,强调并发操作。OLTP 迫切要求数据在端到端的流通速度越快,耗时越短、则用户在更短的时间中达成交易并创造生产性的数据,同时也为企业创造出更大的单位时间价值。 + +>OLTP系统最容易出现瓶颈的地方就是CPU与磁盘子系统。 + +#### 3.2. OLAP 追求决策最优 + +OLAP分析决策主要追求的决策最优解,决策的周期期一般都大于1天,对时间的效率要求并不高,对数据的质量要求很高。OLAP 系统则强调数据分析,强调SQL执行时长,强调磁盘I/O,强调分区等。 + +另外,OLAP 对数据质量要求很高,低质量的数据会产生低质量的业务认知,低质量的业务认知会产生低效或错误的决策,低效或错误的决策将给业务带来很大的损害。高质量的数据才能使决策者减少失误并得到符合预期的决策效果。 + +高质量的数据要求数据本身的体量要足够大足够表达业务,同时也要求数据的完整性、一致性、准确性、有效性、及时性。对大数据的清洗处理是一个本身就很耗时的ETL过程,压缩时间周期的成本很高也很难。基于OLAP的系统要求数据稳健的推动决策,重在数据质量。 + +![]({{site.baseurl}}/img-post/oltp-2.jpg) + + +# 4. 优化策略 + +#### 4.1. OLTP 优化策略 + +- 减少单个语句的逻辑读,或者是减少它们的执行次数; +- 尽量避免计算过程,建设计算型的函数,如自定义函数、decode等的频繁使用, +- 尽可能使用变量绑定技术来达到SQL 重用,减少物理I/O 和重复的SQL 解析; + +>在高可用的OLTP环境中,数据库使用越简单的功能越好。 + +#### 4.2. OLAP 优化策略 + +- 增加CPU 处理速度; +- 提升磁盘I/O 速度; +- 增加磁盘的个数; +- 加大带宽; +- 分区技术; +- 并行技术; +- 使用位图索引; +- MV技术; + +# 5. ROLAP & MOLAP + +#### 5.1. ROLAP + +以ROLAP为代表的有传统关系型数据库、MPP分布式数据库以及基于Hadoop的Spark/Impala,特点是能同时连接明细数据和汇总数据,实时根据用户提出的需求对数据进行计算后返回给用户,所以用户使用相对比较灵活,可以随意选择维度组合来进行实时计算。 + +正因为采用的实时计算技术,所以ROLAP的缺点也比较明显——当计算的数据量达到一定级别或并发数达到一定级别的时候,一定会出现性能问题。 + +- 关系型数据库 + + 传统关系型数据库为代表的如Teradata、Oracle等,由于传统架构可扩展性较差,所以对硬件的要求非常高,当计算的数据量达到千万,亿级别时,数据库的计算就会出现延时,使得用户不能及时得到响应,更别提高并发了。 + +- MPP分布式数据库 + + MPP分布式数据库(GreenPlum/GBase/Vertica)则解决了一部分可扩展性问题,对硬件设备的要求也稍稍下降了(还是有一定的硬件要求),在支持的数据体量(GB,TB级别)上有了很大的提升。当集群有几百、上千节点时,会出现性能瓶颈(增加再多节点,性能提升也不会很明显),扩容成本同样不菲。 + +- 基于Hadoop的Spark/Impala + + 基于Hadoop的Spark/Impala,则对部署硬件的要求很低(常见服务器即可,只是其主要依靠内存计算来缩短响应时间,所以对内存要求较高),在节点扩容上成本上相对较低,但当计算量达到一定级别或并发达到一定级别后,无法秒级响应,且容易出现内存溢出等问题。 + +#### 5.2. MOLAP + +以MOLAP分析为代表的有Cognos,SSAS,Kylin等,设计理念是预先将客户的需求计算好以结果的形式存下来(比如一张表分为10个维度,5个度量,那客户提出的需求会有2的10次方种可能,然后将这么多种可能提前计算好存储下来)。 + +当客户提出需求后,找到对应结果返回即可,特点是当命中需求后返回非常快(所以MOLAP非常适合常见固定的分析场景),同等资源下支持的数据体量更大,支持的并发更多,不足则是当表的维度越多,越复杂,其所需的磁盘存储空间则越大,构建cube也需要一定的时间。 + +Apache Kylin 基于hadoop框架,Cube以分片的形式存储在不同节点上,Cube大小不受服务器配置限制,所以具备很好的可扩展性和对服务器要求很低,在扩容成本上就非常低廉。另外为了控制整体Cube的大小,Kylin给客户提供了建模的能力,即用户可以根据自身需要,对模型种的维度以及维度组合进行预先的构建,把一些不需要的维度和组合筛选掉,从而达到降低维度的目的,减少磁盘空间的占用。 + +# 6. 对比图 + +![]({{site.baseurl}}/img-post/oltp-3.png) + + +# 参考文献: +- 《 [知乎:OLTP与OLAP的关系是什么?](https://www.zhihu.com/question/24110442/answer/851671343)》; +- 《[简书:数据库 OLAP、OLTP的介绍和比较](https://www.jianshu.com/p/b1d7ca178691)》 + diff --git "a/_posts/2022-01-27-\346\225\260\344\273\223\345\237\272\347\241\200\357\274\232\346\225\260\344\273\223\346\212\200\346\234\257\351\200\211\345\236\213\345\217\212\346\226\271\346\241\210\350\256\276\350\256\241.md" "b/_posts/2022-01-27-\346\225\260\344\273\223\345\237\272\347\241\200\357\274\232\346\225\260\344\273\223\346\212\200\346\234\257\351\200\211\345\236\213\345\217\212\346\226\271\346\241\210\350\256\276\350\256\241.md" new file mode 100644 index 00000000000..48f9b01c2a2 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\344\273\223\345\237\272\347\241\200\357\274\232\346\225\260\344\273\223\346\212\200\346\234\257\351\200\211\345\236\213\345\217\212\346\226\271\346\241\210\350\256\276\350\256\241.md" @@ -0,0 +1,227 @@ +--- +layout: post +title: 数仓基础:数仓技术选型及方案设计 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓 +--- + + +# 1. 大数据技术生态 + +#### 1.1. 大数据技术生态体系图 + +![]({{site.baseurl}}/img-post/数仓-1.png) + +#### 1.2. 数据流程图 + +![]({{site.baseurl}}/img-post/数仓-2.png) + +#### 1.3. 大数据框架 + +- Apache + - 使用最广泛的框架; + - 需要专业的运维人员; + - 组件兼容性需要仔细调研(Hive v2.3+); + - 尽量不要选新框架,容易出问题,最好选择最新框架半年前左右的稳定版本; + - Apache 常用版本对应情况 + - JAVA: 1.8 + - Hadoop: 3.1.3 + - Hive: 3.1.2 + - Flume: 1.9.0 + - Zookeeper: 3.5.7 + - Kafka: 2.4.1 + - DataX: 3.0 + - MaxWell: 1.29.2 + +- CDH + - 收费较贵; + +#### 1.4. 服务器 + +- 物理机 + - 品牌:戴尔; + - 内存:**128G**; + - 内核:**20 核** 物理 CPU,**40 线程**(一般 1 核 CPU 对应 2 线程); + - 机械硬盘:**8T HDD** 机械硬盘; + - 固态硬盘:**2T SSD** 固态硬盘; + - 价格: **4W** 左右; + - 寿命: 5 年左右; +- 云主机 + - 阿里云、腾讯云,5W / 年; + +> 需要根据 **业务场景、集群规模** 综合考虑。 + +#### 1.5. 数据存储 + +- MySQL + - 存储业务数据; + - 存储数据分析的结果数据; +- HDFS + - 与 Hive 配套,存储海量数据; +- HBase + - 存储快速查表数据; +- Redis + - 缓存; +- MongoDB + - 爬虫数据存储; + +#### 1.6. 数据采集传输 + +- 用户行为数据: + - Flume,读取日志文件; + - Kafka,削峰; + - Logstash,Flume 替代品,专门处理日志(ELK); +- 业务数据(处理 MySQL): + - Sqoop,老外写的,轻量简洁,但功能单一,基本被替代了; + - DataX,Sqoop 的替代品,阿里出品的处理 MySQL 的工具,拥有很多插件,功能强大; + - MaxWell,实时监控 MySQL binlog,一旦发现 MySQL 有增删改查操作立刻同步到 Kafka; + - Canal,阿里开发的工具,功能与 MaxWell 类似。 + +#### 1.7. 数据计算 + +- 实时计算:处理非实时问题,比如用于统计日活、周活、月活等; + - Hive(SQL):基于 MapReduce,做数据查询; + - Mahaout: 数据挖掘; + - Tez:基于内存,计算速度快; + - Spark Core:基于内存,计算速度快,掉电很麻烦; + - Spark Mlib:数据挖掘 + - Spark R:数据分析 + - Spark SQL:数据查询 + +- 实时计算:处理实时数据,比如天猫双十一实时数据计算; + - Spark Streaming:**准实时** 计算,实际采用批处理; + - Flink:TODO + - Storm:实时计算,正在被抛弃; + +- 数据处理 + - 批处理,触发式单次执行,比如电商交易订单; + - 流处理,流式不间断执行,比如自动驾驶数据; + +#### 1.8. 数据查询 + +- Presto + - 快速查询 + - 支持 Redis、Kafka、MySQL + - 与 Apache 框架配合使用,安装包使用较方便; +- Druid + - 实时处理、批处理、流处理 +- Impala + - Presto 替代品,速度快叫 Presto 速度更快,但是多数据源支持范围较 Presto 更窄; + - 与 CDH 框架配合,CDH 默认集成 Impala; + - Apache 安装 Impala 极为困难 +- Kylin + - 多维度数据处理; + +#### 1.9. 数据可视化 + +- Echarts + - 百度开发的平台,需要 JavaScript 支持; +- Superset + - 免费 +- QuickBI + - TODO +- DataV + - TODO + +#### 1.10. 任务调度 + +- DolphinScheduler +- Azkaban +- Oozie + +#### 1.11. 集群监控 + +- Zabbix + +#### 1.12. 元数据管理 + +- Atlas + +#### 1.13. 数据质量监控 + +- Griffin +- Shell +- Python + +#### 1.14. 数据平台和配置 + +- ZooKeeper +- Yarn + +# 2. 技术选型考虑因素 + +#### 2.1. 数据量大小 \ 集群规模 + +- 万级 \ 十万级:MySQL; +- 百万级 \ 千万级:; +- 亿级 \ 十亿级 \ 百亿级:HDFS; + +#### 2.2. 数据类型 + +- 数据库(结构化数据) +- 文件日志(半结构化数据) +- 视频、文本、图片文件等(非结构化数据) + +#### 2.3. 业务需求 + +- 用户行为数据:日志文件; +- 业务数据:MySQL; + +#### 2.4. 行业内经验 + +- 同行业(竞争对手)参考; + +#### 2.5. 技术成熟度 + +- 尽可能使用较稳定版本的技术; + +#### 2.6. 开发维护成本 + +- 开发难度 & 维护难度,需要作出长远规划、做好动态平衡; + +#### 2.7. 预算 + +- 费用预算应结合业务实际需求,不盲目求新求大; + +# 3. 集群规模参考值 + +#### 3.1. 日数据量: + +- 日活 100万,每人 100 条日志,共计 100万 X 100 = 1 亿条; + +#### 3.2. 日存储量: + +- 每条日志 1K 大小,每天:1亿条 / 1024 / 1024 = 100G; + +#### 3.3. 半年不扩容: + +- 100G/天 X 180天 = 18T; + +#### 3.4. 保存 3 副本: + +- 18 t x 3 = 54T; + +#### 3.5. 预留 Buff: + +- 预留 20% ~ 30% Buff = 54T / 0.7 = 77T; + +#### 3.6. 服务器需求 + +- 8T * 10 台标准服务器(20核 / 128G / 8T HDD / 2T SSD); + +>注意:此方案未考虑 **数仓分层 和 数据压缩** ! + +# 4. 服务器规划注意事项 + +- ResourceManager 和 NameNode 不能放在同一台服务器; +- ResourceManager 需要做高可用; +- ZooKeeper,安装台数为 **奇数**,最少 3 台; +- Kafka,与 ZooKeeper 安装在一起,Kafka 与 ZooKeeper 有大量的数据通信; +- Flume,与 Kafka 安装在一起,Flume 采集完的数据需要快速写入 Kafka; + - 如果有专门的额日志服务器,一般单独部署 Flume 到日志服务器; + diff --git "a/_posts/2022-01-27-\346\225\260\344\273\223\345\237\272\347\241\200\357\274\232\346\225\260\344\273\223\347\232\204\346\246\202\345\277\265\345\220\215\350\257\215\350\257\246\350\247\243.md" "b/_posts/2022-01-27-\346\225\260\344\273\223\345\237\272\347\241\200\357\274\232\346\225\260\344\273\223\347\232\204\346\246\202\345\277\265\345\220\215\350\257\215\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..57a37d1d4ca --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\344\273\223\345\237\272\347\241\200\357\274\232\346\225\260\344\273\223\347\232\204\346\246\202\345\277\265\345\220\215\350\257\215\350\257\246\350\247\243.md" @@ -0,0 +1,274 @@ +--- +layout: post +title: 数仓基础:数仓的概念名词详解 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓 +--- + + +# 1. 数仓的特点 + +#### 1.1. 面向主题 + +- 数仓的价值,是为数据分析提供数据支撑,数据处理过程是面向分析的 OLAP 过程。 + +#### 1.2. 集成 + +- 跨部门 +- 跨系统 +- 多源 +- 异构 + +#### 1.3. 稳定 + +- 非事务性,不经常变化; + > 数仓有大量的查询操作,但是修改和删除很少。 +- 非易失性 + - 数据一旦进入数仓,基本上都会被永久保留; + +#### 1.4. 反映历史变化 + +- 数仓存储大量原始数据,并且是永久保存,能够反应历史变化情况。 +- 多数的数据,必然会存在时间维度。 + +#### 1.5. 数仓架构设计原则 + +- 业务驱动 & 数据驱动 + - 底层业务的数据驱动为导向同时结合业务需求驱动; +- 便于数据分析 + - 屏蔽底层复杂业务,将简单、完整、集成的数据暴露给分析层; +- 底层业务变动与上层需求变动对模型冲击最小化 + - 业务系统变化影响削弱,在基础数据层(资金订单改造)结合自上而下的建设方法、削弱需求变动对模型的影响,数据水平层次清晰化 +- 高内聚、松耦合 + - 主题之内或各个完整意义的系统内数据的高内聚,主题之间或各个完整意义的系统间数据的松耦合 +- 底层数据与上层分析应用隔离 + - 构建仓库基础数据层,使得底层业务数据整合工作与上层应用开发工作相隔离,为仓库大规模开发奠定基础仓库层次更加清晰,对外暴露数据更加统一; + +#### 1.6. 数仓与数据库的区别 + +- 特性区别 + ![]({{site.baseurl}}/img-post/dw-1.png) + +- 差异对比 + ![]({{site.baseurl}}/img-post/dw-1.png) + +# 2. 数仓常见术语 + +#### 2.1. 数据仓库 + +数据仓库是一个支持管理决策的数据集合。数据是面向主题的、集成的、不易丢失的并且是时变的。数据仓库是所有操作环境和外部数据源的快照集合。它并不需要非常精确,因为它必须在特定的时间基础上从操作环境中提取出来。 + +#### 2.2. 数据集市 + +数据仓库只限于单个主题的区域,例如顾客、部门、地点等。数据集市在从数据仓库获取数据时可以依赖于数据仓库,或者当它们从操作系统中获取数据时就不依赖于数据仓库。 + +#### 2.3. 事实 + +事实是数据仓库中的信息单元,也是多维空间中的一个单元,受分析单元的限制。事实存储于一张表中(当使用关系数据库时)或者是多维数据库中的一个单元。每个事实包括关于事实(销售额,销售量,成本,毛利,毛利率等)的基本信息,并且与维度相关。在某些情况下,当所有的必要信息都存储于维度中时,单纯的事实出现就是对于数据仓库足够的信息。 + +#### 2.4. 维度 + +维度是用来反映业务的一类属性,这类属性的集合构成一个维度。例如,某个地理维度可能包括国家、地区、省以及城市的级别。一个时间维度可能包括年、季、月、周、日的级别。 + +#### 2.5. 级别 + +维度层次结构的一个元素。级别描述了数据的层次结构,从数据的最高(汇总程度最大)级别直到最低(最详细)级别(如大分类-中分类-小分类-细分类)。级别仅存在于维度内。级别基于维度表中的列或维度中的成员属性。 + +#### 2.6. 数据清洗 + +对数据仓库系统无用的或者不符合数据格式规范的数据称之为脏数据。清洗的过程就是清除脏数据的过程。 + +#### 2.7. 数据采集 + +数据仓库系统中后端处理的一部分。数据采集过程是指从业务系统中收集与数据仓库各指标有关的数据。 + +#### 2.8. 数据转换 + +解释业务数据并修改其内容,使之符合数据仓库数据格式规范,并放入数据仓库的数据存储介质中。数据转换包括数据存储格式的转换以及数据表示符的转换(如产品代码到产品名称的转换)。 + +#### 2.9. 联机分析处理(OLAP) + +OLAP 是一种多维分析技术,用来满足决策用户在大量的业务数据中,从多角度探索业务活动的规律性、市场的运作趋势的分析需求,并辅助他们进行战略发展决策的制定。 +按照数据的存储方式分 OLAP 又分为 ROLAP、MOLAP 和 HOLAP。在客户信息数据仓库 CCDW 的数据环境下,OLAP 提供上钻、下钻、切片、旋转等在线分析机制。完成的功能包括多角度实时查询、简单的数据分析,并辅之于各种图形展示分析结果。 + +#### 2.10. 数据挖掘 + +在数据仓库的数据中发现新信息的过程被称为数据挖掘,这些新信息不会从操作系统中获得。 + +#### 2.11. 数据魔方 + + ![]({{site.baseurl}}/img-post/dw-2.png) + +多维数据抽象的数据立方体概念,(OLAP的基本操作 )面向分析的多维查询操作。 + +#### 2.12. 钻取 & 上卷 & 切片 & 切块 & 旋转 + +钻取:是指将某些维度进行细分; +上卷:理解为”无视”某些维度; +切片:在数据立方体的某一维度上选定一个维成员的操作叫切片; +切块:对两个或多个维执行选择则叫做切块; +旋转:指改变报表或页面的展示方向。 + + ![]({{site.baseurl}}/img-post/dw-3.png) + + ![]({{site.baseurl}}/img-post/dw-4.png) + + ![]({{site.baseurl}}/img-post/dw-5.png) + + +#### 2.13. 星型模型 + +是数据仓库应用程序的最佳设计模式。它的命名是因其在物理上表现为中心实体,典型内容包括指标数据、辐射数据,通常是有助于浏览和聚集指标数据的维度。星形图模型得到的结果常常是查询式数据结构,能够为快速响应用户的查询要求提供最优的数据结构。星形图还常常产生一种包含维度数据和指标数据的两层模型。 + +#### 2.14. 雪花模型 + +指一种扩展的星形图。星形图通常生成一个两层结构,即只有维度和指标,雪花图生成了附加层。实际数据仓库系统建设过程中,通常只扩展三层:维度(维度实体)、指标(指标实体)和相关的描述数据(类目细节实体);超过三层的雪花图模型在数据仓库系统中应该避免。因为它们开始像更倾向于支持OLTP应用程序的规格化结构,而不是为数据仓库和OLAP应用程序而优化的非格式化结构。 + +#### 2.15. 粒度 + +粒度将直接决定所构建仓库系统能够提供决策支持的细节级别。粒度越高表示仓库中的数据较粗,反之,较细。粒度是与具体指标相关的,具体表现在描述此指标的某些可分层次维的维值上。例如,时间维度,时间可以分成年、季、月、周、日等。 +数据仓库模型中所存储的数据的粒度将对信息系统的多方面产生影响。事实表中以各种维度的什么层次作为最细粒度,将决定存储的数据能否满足信息分析的功能需求,而粒度的层次划分、以及聚合表中粒度的选择将直接影响查询的响应时间。 + +#### 2.16. 度量值 + +在多维数据集中,度量值是一组值,这些值基于多维数据集的事实数据表中的一列,而且通常为数字。此外,度量值是所分析的多维数据集的中心值。即,度量值是最终用户浏览多维数据集时重点查看的数字数据(如销售、毛利、成本)。 + +#### 2.17. 冰山查询 + +在数据仓库领域有一个概念叫Iceberg query,中文一般翻译为“冰山查询”。冰山查询在一个属性或属性集上计算一个聚集函数,以找出大于某个指定阈值的聚集值。以销售数据为例,你想产生这样的一个顾客-商品对的列表,这些顾客购买商品的数量达到3件或更多。这可以用下面的冰山查询表示: +Select P.cust_ID, P.item_ID, SUM(P.qty) +From Purchase P +Group by P.cust_ID, P.item_ID +Having SUM(P.qty)>=3 +这种在给出大量输入数据元组的情况下,使用having字句中的阈值来进行过滤的查询方法就叫做冰山查询。输出结果可以看作“冰山顶”,而“冰山”是输入数据。这种冰山查询在数据仓库的数据概况分析阶段、数据质量检查阶段和数据挖掘的购物篮分析中都经常使用。而且,冰山查询也是面试中出现频率非常高的一道题,经常用来检测SQL能力。 + +#### 2.18. 操作集市--oper mart + +在数据仓库领域有一个概念叫Oper Mart,中文一般翻译为“操作集市”。操作集市是为了企业战术性的分析提供支持,它的数据来源是操作数据存储(ODS)。它是ODS在分析功能上的扩展,使用户可以对操作型数据进行多维分析。 +一个操作集市应该有如下特征: +①操作集市是ODS的子集,数据来源于ODS,用于战略分析和报表。 +②操作集市中的数据和ODS中的数据同步更新。 +③操作集市以多维技术进行建模,即星型结构。 +④操作集市是一个临时的结构,当不在需要时会清掉所有数据,即不保存历史数据。 +操作集市和数据集市很相似,但是它不能用来取代用于战略性分析的数据集市。由于操作集市的数据来源于ODS,所以它的数据比数据集市的数据要新。但是出于容量的考虑,操作集市中不保存历史数据,是一个临时的结构。 + +#### 2.19. 操作数据存储--operational data store + +Kimball对操作数据存储的定义是,面向主题的、集成的、经常更新的细节数据存储,用集成的数据来支持事务系统。Kimball也认可Inmon对ODS的分类,但是他认为ODS应该以星型结构来进行建模。虽然Kimball对操作数据存储(ODS)的定义和Inmon基本上一样,但是他对操作数据存储的理解、作用与实现和Inmon有着较大的不同。Kimball认为ODS在两种情况下是需要的: +第一种情况是提供操作型报表,这些报表需要提供面向主题的、集成的数据,所以操作型的源系统无法提供;这些报表和数据仓库中的报表也不相同,因为它们可以是一些定制好的,写死在程序中的报表。 +第二种情况是需要提供实时的信息时,由于数据仓库的更新频率一般都是24小时,而用户会有更急切的需求来了解数据源的信息,这时,建立操作数据存储是很有必要的。对于ODS是保存最细粒度数据的地方的说法,Kimball认为对于最细粒度数据,即原子数据层,应该保存在数据仓库中,而且应该置于维度框架和总线架构中。 + +#### 2.20. 代理键--surrogate key + +代理关键字一般是指维度表中使用顺序分配的整数值作为主键,也称为“代理键”。代理关键字用于维度表和事实表的连接。代理关键字的称呼有surrogate keys,meaningless keys,integer keys,nonnatural keys,artificial keys,synthetic keys等。与之相对的自然关键字的称呼有natural keys,samat keys等。在Kimball的维度建模领域里,是强烈推荐使用代理关键字的。在维度表和事实表的每一个联接中都应该使用代理关键字,而不应该使用自然关键字或者智能关键字(Smart Keys)。数据仓库中的主键不应该是智能的,也就是说,要避免通过主键的值就可以了解一些业务信息。当然,退化维度作为事实表的复合主键之一时例外。 +使用代理关键字,有很多优点: +①使用代理关键字能够使数据仓库环境对操作型环境的变化进行缓冲。也就是说,当数据仓库需要对来在多个 +操作型系统的数据进行整合时,这些系统中的数据有可能缺乏一致的关键字编码,即有可能出现重复,这时代 +理关键字可以解决这个问题。 +②使用代理关键字可以带来性能上的优势。和自然关键字相比,代理关键字很小,是整型的,可以减小事实表 +中记录的长度。这样,同样的IO就可以读取更多的事实表记录。另外,整型字段作为外键联接的效率也很高。 +③使用代理关键字可以建立一些不存在的维度记录,例如“不在促销之列”,“日期待定”,“日期不可用”等 +维度记录。 +④使用代理关键字可以用来处理缓慢变化维。维度表数据的历史变化信息的保存是数据仓库设计的实施中非常重 +要的一部分。Kimball的缓慢变化维处理策略的核心就是使用代理关键字。 +使用代理关键字,当然也有缺点: +代理关键字的使用使数据加载变得非常复杂。有关使用代理关键字的维度表和事实表的加载方法在ETL Toolkit +中有详细的描述。使用代理关键字是一个从长远考虑的策略。 + +#### 2.21. 多值维度--multivalue dimension + +多值维度有两种情况: +第一种情况是指维度表中的某个属性字段同时有多个值。举例来说,一个帐户维度表中,帐户持有人姓名,可能会有多个顾客。这样,一个帐户对应多个顾客姓名,一个顾客也可以有多个帐户,它们之间是多对多的关系。正因为一个帐户可能会有多个对应的顾客,所以不能直接将顾客ID放入帐户维度表中。而帐户维度表中的这种情况就叫做多值维度。 +第二种情况是事实表在某个维度表中有多条对应记录。举例来说,对于一个健康护理单分列项事实表来说,它的粒度是一个健康护理单,但是该护理单却有可能有多次诊断,即该事实表与诊断维度的是一对多的关系。这个与事实表粒度不匹配的诊断维度也称之为多值维度。处理多值维度最好的办法是降低事实表的粒度。如第二种情况中,将健康护理单分列项事实表的粒度降低到具体的诊断粒度上,这样就避免了多值维度的出现。这种处理方式也是维度建模的一个原则,即事实表应该建立在最细粒度上。这样的处理,需要对事实表的事实进行分摊。但是有些时候,事实表的粒度是不能降低的,多值维度的出现是无法避免的。如第一种情况中,事实表是月帐户快照事实表,这张事实表与顾客维度没有直接的关系,不能将数据粒度进行细分,即使细分的话帐户余额也很难分摊。这时,可以采用桥接表技术进行处理。在帐户维度表和顾客维度表之间建立个帐户-顾客桥接表。这个桥接表可以解决掉帐户维度和顾客维度之间的多对多关系,也解决掉的帐户维度表的多值维度问题。总之,多值维度是应该尽量避免的,它给数据处理带来了很大的麻烦。如果多值维度不能避免的话,应该建立桥接表来进行处理。 + +##### 2.22. 非事实型事实表--factless fact table + +在事实表中,通常会保存十个左右的维度外键和多个度量事实,度量事实是事实表的关键所在。在非事实型事实表中没有这些度量事实,只有多个维度外键。非事实型事实表通常用来跟踪一些事件或者说明某些活动的范围。下面举例来进行说明。第一类非事实型事实表是用来跟踪事件的事实表:学生注册事件,学校需要对学生按学期进行跟踪。维度表包括学期维度、课程维度、系维度、学生维度、注册专业维度和取得学分维度,而事实表是由这些维度的主键组成,事实只有注册数,并且恒为1。这样的事实表可以回答大量关于大学开课注册方面的问题,主要是回答各种情况下的注册数。 +第二类非事实型事实表是用来说明某些活动范围的事实表: +促销范围事实表。通常销售事实表可以回答如促销商品的销售情况,但是对于那些没有销售出去的促销商品没法回答。这时,通过建立促销范围事实表,将商场需要促销的商品单独建立事实表保存。然后,通过这个促销范围事实表和销售事实表即可得出哪些促销商品没有销售出去。这样的促销范围事实表只是用来说明促销活动的范围,其中没有任何事实度量。 + +##### 2.23. 合并事实表--consolidated + +合并事实表是将不同事实表的事实合并到同一张事实表的建模方法,合并的事实要保证在相同的粒度。这种建模方法通常被用来横跨多个业务主题域来建立数据集市,Kimball将这样的数据集市称为第二级的数据集市。使用合并事实表技术,☆可以避免性能较差的交叉探察操作☆。但是,这种合并事实表和使用交叉探察操作还有着细微的不同,在一些基础表中没有记录的时候,合并事实表中可能会存储一条记录,字段值保存为零。合并事实表可以给数据仓库带来很大的性能提升,提供的跨主题的事实数据也给用户带来了很大的方便。但是,合并事实表给ETL工作带来了较大的麻烦。对于合并事实表中涉及到的维度,需要在数据准备区保证它们是一致性维度。 + +##### 2.24. 缓慢变化维--slowly changing dimension + +缓慢变化维,经常被简写为SCD。缓慢变化维的提出是因为在现实世界中,维度的属性并不是静态的,它会随着时间的流失发生缓慢的变化。这种随时间发生变化的维度我们一般称之为缓慢变化维,并且把处理维度表的历史变化信息的问题称为处理缓慢变化维的问题,有时也简称为处理SCD的问题。处理缓慢变化维的方法通常分为三种方式: +①直接覆盖原值。这样处理,最容易实现,但是没有保留历史数据,无法分析历史变化信息。第一种方式通常简 +称为“TYPE 1”。 +②添加维度行。这样处理,需要代理键的支持。实现方式是当有维度属性发生变化时,生成一条新的维度记录, +主键是新分配的代理键,通过自然键可以和原维度记录保持关联。第二种方式通常简称为“TYPE 2”。 +③添加属性列。这种处理的实现方式是对于需要分析历史信息的属性添加一列,来记录该属性变化前的值,而本 +属性字段使用TYPE 1来直接覆盖。这种方式的优点是可以同时分析当前及前一次变化的属性值,缺点是只保留了 +最后一次变化信息。第三种方式通常简称为“TYPE 3”。 +在实际建模中,我们可以联合使用三种方式,也可以对一个维度表中的不同属性使用不同的方式,这些,都需要 +根据实际情况来决定,但目的都是一样的,就是能够支持方便的分析历史变化情况。 + +##### 2.25. 即席查询--ad hoc queries + +即席查询是指那些用户在使用系统时,根据自己当时的需求定义的查询。即席查询生成的方式很多,最常见的就是使用即席查询工具。一般的数据展现工具都会提供即席查询的功能。通常的方式是,将数据仓库中的维度表和事实表映射到语义层,用户可以通过语义层选择表,建立表间的关联,最终生成SQL语句。即席查询与通常查询从SQL语句上来说,并没有本质的差别。它们之间的差别在于,通常的查询在系统设计和实施时是已知的,所有我们可以在系统实施时通过建立索引、分区等技术来优化这些查询,使这些查询的效率很高。而即席查询是用户在使用时临时生产的,系统无法预先优化这些查询,所以即席查询也是评估数据仓库的一个重要指标。在一个数据仓库系统中,即席查询使用的越多,对数据仓库的要求就越高,对数据模型的对称性的要求也越高。对称性的数据模型对所有的查询都是相同的,这也是维度建模的一个优点。 + +##### 2.26. 交叉探察--drill across + +在基于总线架构(Bus Architecture)的维度建模中,大部分的维度表是由事实表共有的。比如“营销事务事实表”和“库存快照事实表”就会有相同的维度表,“日期维度”、“产品维度”和“商场维度”。这时,如果有个需求是想按共有维度来对比查看销售和库存的事实,这时就需要发出两个SQL,分别查出按维度统计出的销售数据和库存数据。然后再基于共有的维度进行外连接,将数据合并。这种发出多路SQL再进行合并的操作就是交叉探查。当这种交叉探查的需求很常用时,有一种建模方法可以避免交叉探查,就是合并事实表(Consolidated Fact Table)。合并事实表是指将位于不同事实表中处于相同粒度的事实进行组合的一种建模方法。即新建立一个事实表,它的维度是两个或多个事实表的相同维度的集合,事实是几个事实表中感兴趣的事实。这个事实表的数据和其他事实表的数据一样来自Staging Area。合并事实表在性能和易用性上都比交叉探查要好,但是被组合的事实表必须处于相同的粒度和维度层次上。 + +##### 2.27. 角色模仿维度--role-playing dimensions + +角色模仿维度是为了处理一个维度在一个事实表中同时出现多次而使用的一种技术处理手段。在建立了角色模仿维度以后,在底层只有一个物理表存在,但是针对这个物理表会建立多个角色提供给数据访问工具,而且对数据访问工具来说这多个角色是不同的。例如对与累计快照事实表中会出现多个日期字段联接到日期维度。这时就可以针对日期维度建立多个角色模仿维度。角色模仿维度的建立方法通常是使用视图来完成。例如订单日期维度表如下所示:CREATE VIEW order_date(order_date_key, order_day_of_week, order_month, … )AS SELECT data_key, day_of_week, month, … FROM DATA使用同样的方式还可以建立多个不同日期的角色模仿维度。聚集事实表--aggregated fact table、累计快照事实表--accumulating snapshot fact table桥接表--bridge table + +##### 2.28. 切片事实表--sliced fact table + +切片事实表中的字段结构和相应的基础表完全相同,差别在于存储的记录的范围。切片事实表中保存记录的是相应基础表中记录的子集,记录数通常与某个维度记录数相同。这种建模方法一般用来满足特殊需要,如需要分析某些特殊问题时,可以将与之相关的数据切片出来。相反,这种方法也常用于合并存储在不同地区的数据,即各个地区都保存自己地区的数据,总部和所有地区的表结构都相同,然后总部将所有地区的数据合并在一起。切片事实表的结构与相对应的基础表相同,数据来源于相对应的基础表。切片事实表由于缩小了表中数据的记录数,所以查询的效率得到了很大的提高。 + +##### 2.29. 事实表--fact table + +在维度建模的数据仓库中,事实表是指其中保存了大量业务度量数据的表。事实表中的度量值一般称为事实。在事实表中最有用的事实就是数字类型的事实和可加类型的事实。事实表的粒度决定了数据仓库中数据的详细程度。以粒度作为化分依据,主要有三种事实表,分别是事务粒度事实表(Transaction Grain Fact Table),周期快照粒度事实表(Periodic Snapshot Grain Fact Table)和累积快照粒度事实表(Accumulating Snapshot Grain Fact Table)。事务粒度事实表中的一条记录代表了业务系统中的一个事件。事务出现以后,就会在事实中出现一条记录。事务粒度事实表也称为原子粒度。典型的例子是销售单分列项事实表。周期快照粒度事实表用来记录有规律的,可预见时间间隔的业务累计数据。通常的时间间隔可以是每天、每周或者每月。典型的例子是库存日快照事实表。累积快照事实表一般用来涵盖一个事务的生命周期内的不确定的时间跨度。典型的例子是KDT#2中描述的具有多个日期字段的发货事实表。通常来说,事务和快照是建模中的两个非常重要的特点,将两者相结合可以使模型建立的更完整。从用途的不同来说,事实表可以分为三类,分别是原子事实表,聚集事实表和合并事实表: +①原子事实表(Atom Fact Table)是保存最细粒度数据的事实表,也是数据仓库中保存原子信息的场所。 +②聚集事实表(Aggregated Fact Table)是原子事实表上的汇总数据,也称为汇总事实表。即新建立一个事实表,它的维度表是比原维度表要少,或者某些维度表是原维度表的子集,如用月份维度表代替日期维度表;事实数据是相应事实的汇总,即求和或求平均值等。在做数据迁移时,当相关的维度数据和事实数据发生变化时,聚集事实表需要做相应的刷新。物化视图是实现聚集事实表的一种有效方式,可以设定刷新方式,具体功能由DBMS来实现。 +③合并事实表(Consolidated Fact Table)是指将位于不同事实表中处于相同粒度的事实进行组合建模而成的一种事实表。即新建立一个事实表,它的维度是两个或多个事实表的相同维度的集合;事实是几个事实表中感兴趣的事实。在Kimball的总线架构中,由合并事实表为主组成的合并数据集市称为二级数据集市。合并事实表的粒度可以是原子粒度也可以是聚集粒度。在做数据迁移时,当相关的原子事实表的数据有改变时,合并事实表的数据需要重新刷新。合并事实表和交叉探察是两个互补的操作。聚集事实表和合并事实表的主要差别是合并事实表一般是从多个事实表合并而来。但是它们的差别不是绝对的,一个事实表既是聚集事实表又是合并事实表是很有可能的。因为一般合并事实表需要按相同的维度合并,所以很可能在做合并的同时需要进行聚集,即粒度变粗。 + +##### 2.30. 数据世系--data lineage + +数据世系描述的是从源系统抽取数据开始,经过数据转换到最终的数据加载的整个过程中各种信息。数据世系信息需要留下详细的文档记载。数据世系包括源系统的数据库中数据定义以及该数据在数据仓库中的最终位置等信息。 +数据世系是数据仓库的元数据中最重要的一部分。这部分元数据的产生位置是在ETL的处理过程中。如果在ETL的处理过程中使用的ETL工具的话,ETL工具可以记录下元数据的一部分,但是这部分一般都是数据的属性描述,而不是完全的数据世系。换一句说,完全依靠ETL工具来维护元数据是不够的。 + +##### 2.31. 退化维度--degenerate dimension + +退化维度一般都是事务的编号,如订单编号、发票编号等。这类编号需要保存到事实表中,但是不需要对应的维度表,所以称为退化维度。退化维度是维度建模领域中的一个非常重要的概念,它对理解维度建模有着非常重要的作用,尤其是对维度建模的入门者。退化维度经常会和其他一些维度一起组合成事实表的主键。在Kimball提出的维度建模中,事实表应该保存最细粒度的数据。所以对于象销售单这样的事实表来说,需要销售单编号和产品来共同作为主键,而不能用销售日期、商场、产品等用来分析的维度共同作为主键。退化维度在分析中可以用来做分组使用。它可以将同一个事务中销售的产品集中在一起。 + +##### 2.32. 微型维度--minidimension + +微型维度的提出主要是为了解决快变超大维度(rapidly changing monster dimension)。以客户维度举例来说,如果维度表中有数百万行记录或者还要多,而且这些记录中的字段又经常变化,这样的维度表一般称之为快变超大维度。对于快变超大维度,设计人员一般不会使用TYPE 2的缓慢变化维处理方法,因为大家都不愿意向本来就 +有几百万行的维度表中添加更多的行。这时,有一项技术可以解决这个问题。解决的方法是,将分析频率比较高或者变化频率比较大的字段提取出来,建立一个单独的维度表。这个单独的维度表就是微型维度表。微型维度表有自己的关键字,这个关键字和原客户维度表的关键字一起进入事实表。有时为了分析的方便,可以把微型维度的关键字的最新值作为外关键字进入客户维度表。这时一定要注意,这个外关键字必须做TYPE 1型处理。在微型维度表中如果有像收入这样分布范围较广的属性时,应该将它分段处理。比如,存储¥31257.98这样过于分散的数值就不如存储¥30000-¥34999这样的范围。这样可以极大的减少微型维度中的记录数目,也给分析带来方便。 + +##### 2.33. 蜈蚣事实表--centipede fact table + +蜈蚣事实表是指那些一张事实表中有太多维度的事实表。连接在事实表两边的维度表过多,看起来就像蜈蚣一样,所以称为“蜈蚣事实表”。通常来说,蜈蚣事实表的出现是由于建模师对事实表和维度表逆规范化过了头。不单将产品主键放入事实表中,对于产品层级结构中的每一层的主键都放入事实表中,这样事实表中与产品相关的就会有产品ID、商标ID、子类ID、类别ID等多个外键。同样,也有建模师将日期相关的日期ID、月ID、年ID都放入事实表中。这些都将产生蜈蚣事实表,使自己落入维度过多的陷阱。蜈蚣事实表虽然使查询效率有所提高,但是伴之而来的是存储空间的大量增长。在维度建模的数据仓库中,维度表的字段个数可以尽可能的增加,但是事实表的字段要尽量减少,因为相比而言,事实表的记录数要远远大于维度表的记录数。一般来说,事实表相关的维度在15个以下为正常,如果维度个数超过25个,就出现了维度过多的蜈蚣事实表。这时,需要做的事情是自己核查,将相关的维度进行合并,减少维度的个数。 + +##### 2.34. 旋转事实表--pivoted fact table + +旋转事实表是将一条记录中的多个事实字段转化为多条记录,其中每条记录保存一个事实字段的一种建模方法。或者反过来,也可以由多条记录转化为一条记录。旋转事实表建模方法的使用通常是为了简化前端数据展现的查询。它通过改变后端的事实记录存储方式,使相应的查询需求的性能得到的极大的提高。如果在SQL或者查询工具中进行这种转换会非常麻烦,效率也很差。和合并事实表类似,有时当基础表中没有记录时,旋转事实表也要存储一些零值在里面。 + +##### 2.35. 一致性事实--comformed fact + +一致性事实是Kimball的多维体系结构(MD)中的三个关键性概念之一,另两个是总线架构(Bus Architecture)和一致性维度(Conformed Dimension)。在建立多个数据集市时,完成一致性维度的工作就已经完成了一致性的80%-90%的工作量。余下的工作就是建立一致性事实。一致性事实和一致性维度有些不同,一致性维度是由专人维护在后台(Back Room),发生修改时同步复制到每个数据集市,而事实表一般不会在多个数据集市间复制。需要查询多个数据集市中的事实时,一般通过交叉探查(drill across)来实现。为了能在多个数据集市间进行交叉探查,一致性事实主要需要保证两点。第一个是KPI的定义及计算方法要一致,第二个是事实的单位要一致性。如果业务要求或事实上就不能保持一致的话,建议不同单位的事实分开建立字段保存。一致性维度将多个数据集市结合在一起,一致性事实保证不同数据集市间的事实数据可以交叉探查,一个分布式的数据仓库就建成了。 + +##### 2.36. 一致性维度--comformed dimension + +在多维体系结构中,没有物理上的数据仓库,由物理上的数据集市组合成逻辑上的数据仓库。而且数据集市的建立是可以逐步完成的,最终组合在一起,成为一个数据仓库。如果分步建立数据集市的过程出现了问题,数据集市就会变成孤立的集市,不能组合成数据仓库,而一致性维度的提出正式为了解决这个问题。一致性维度的范围是总线架构中的维度,即可能会在多个数据集市中都存在的维度,这个范围的选取需要架构师来决定。一致性维度的内容和普通维度并没有本质上区别,都是经过数据清洗和整合后的结果。一致性维度建立的地点是多维体系结构的后台(Back Room),即数据准备区。在多维体系结构的数据仓库项目组内需要有专门的维度设计师,他的职责就是建立维度和维护维度的一致性。在后台建立好的维度同步复制到各个数据集市。这样所有数据集市的这部分维度都是完全相同的。建立新的数据集市时,需要在后台进行一致性维度处理,根据情况来决定是否新增和修改一致性维度,然后同步复制到各个数据集市。这是不同数据集市维度保持一致的要点。在同一个集市内,一致性维度的意思是两个维度如果有关系,要么就是完全一样的,要么就是一个维度在数学意义上是另一个维度的子集。例如,如果建立月维度话,月维度的各种描述必须与日期维度中的完全一致,最常用的做法就是在日期维度上建立视图生成月维度。这样月维度就可以是日期维度的子集,在后续钻取等操作时可以保持一致。如果维度表中的数据量较大,出于效率的考虑,应该建立物化视图或者实际的物理表。维度保持一致后,事实就可以保存在各个数据集市中。虽然在物理上是独立的,但在逻辑上由一致性维度使所有的数据集市是联系在一起,随时可以进行交叉探察等操作,也就组成了数据仓库。☆☆☆☆ + +##### 2.37. 预连接聚集表--pre-joined aggregate table + +预连接聚集表是通过对事实表和维度表的联合查询而生成的一类汇总表。在预连接聚集表中,保存有维度表中的描述信息和事实表的事实值。通过预连接,可以避免在用户查询时RDBMS的连接操作,所以预连接聚集表的查询效率要高很多。典型的预连接聚集表如下例所示的销售事实表: +产品名称、商标名称、年份、月份、销售人员名称、销售量、销售金额在这个销售事实表,前五个字段都来自于维度表的描述字段,后两个字段来自于事实表的事实字段。这样在用户提交查询后,RDBMS就不需要连接维度表和事实表了,只需直接在该表中查询即可。预连接聚集表有一个很大的缺点,它需要占用大量的存储空间。预连接事实表的记录和事实表一样多,每条记录的长度和维度表一样长,所以对存储空间的需求是非常大的。除非情况特殊,或者该表是高度汇总的,否则不建议建立预连接聚集表。在建立预连接聚集表时需要平衡效率和存储空间的矛盾。预连接聚集表的生成方式较为简单,直接使用SQL查询即可生成。如果聚集导航器的功能很强大的话,也可以处理预连接聚集表。否则,需要用户理解预连接聚集表,并在SQL中直接使用该表。预连接聚集表在数据仓库领域有着很重要的作用,是汇总表的一种。它的优点和缺点都很明显,在使用时需要综合考虑。 + +##### 2.38. 杂项维度--junk dimension + +杂项维度是由操作系统中的指示符或者标志字段组合而成,一般不在一致性维度之列。在操作系统中,我们定义好各种维度后,通常还会剩下一些在小范围内取离散值的。 + + diff --git "a/_posts/2022-01-27-\346\225\260\344\273\223\345\267\245\345\205\267\357\274\232\346\225\260\344\273\223\351\203\250\347\275\262&\350\277\220\347\273\264\345\270\270\347\224\250\350\204\232\346\234\254.md" "b/_posts/2022-01-27-\346\225\260\344\273\223\345\267\245\345\205\267\357\274\232\346\225\260\344\273\223\351\203\250\347\275\262&\350\277\220\347\273\264\345\270\270\347\224\250\350\204\232\346\234\254.md" new file mode 100644 index 00000000000..8701f542347 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\344\273\223\345\267\245\345\205\267\357\274\232\346\225\260\344\273\223\351\203\250\347\275\262&\350\277\220\347\273\264\345\270\270\347\224\250\350\204\232\346\234\254.md" @@ -0,0 +1,284 @@ +--- +layout: post +title: 数仓工具:数仓部署&运维常用脚本 +subtitle: rsync递归数据分发 +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数仓 +--- + + +# 1. 脚本编写注意事项 + +#### 1.1. 使用 linux vim 编辑器 +- 脚本编写,最好先在 Windows 环境下,编辑好,再粘贴到 linux 的 vim 编辑器; +- 不能直接将脚本推送到 linux 中,否则执行的时候会因为回车问题,导致脚本无法正常运行。 + +#### 1.2. 脚本存放到 bin 目录下 +- 写好的脚本放到 /bin 目录下,可以直接执行; + +#### 1.3. 修改 777 权限 +- `chmod 777 脚本名称` + +#### 1.4. 注意脚本后缀 +- 不要后缀的脚本要注意; + +# 2. xsync + +#### 2.1. xsync 简介 + +改脚本基于 rsync 而写的,rsync 与 scp 的运行机制不一样,rsync 会对比集群内同目录下是否有不一致,如果有不一样的文件则同步文件过去,如果一致则不更新。 + +#### 2.2. xsync 脚本 + +- 在 bin 目录下创建 xsync; + + ```aidl + cd /bin & vim xsync + ``` + +- 编辑脚本; + + ```aidl + #!/bin/sh + + # 获取输入参数个数,如果没有参数,直接退出 + pcount=$# + if((pcount!=4)); then + echo Usage: $0 filename servername startno endno + exit; + fi + + + # 获取文件名称 + p1=$1 + fname=`basename $p1` + echo fname=$fname + + # 获取上级目录到绝对路径 + pdir=`cd -P $(dirname $p1); pwd` + echo pdir=$pdir + # 获取当前用户名称 + user=`whoami` + # 获取hostname及起止号 + slave=$2 + startline=$3 + endline=$4 + + # 循环 + for((host=$startline; host<=$endline; host++)); do + echo $pdir/$fname $user@$slave$host:$pdir + echo ==================$slave$host================== + rsync -rvl $pdir/$fname $user@$slave$host:$pdir + done + ``` + +- 修改权限 + + ```aidl + chmod 777 xsync + ``` + +#### 2.3. xsync 使用 + +- 使用示例: + + ```aidl + [root@hadoop-1 home]# sudo xsync 1.txt slave 1 2 + + fname=1.txt + pdir=/home + /home/1.txt root@slave1:/home + ==================slave1================== + sending incremental file list + 1.txt + + sent 90 bytes received 35 bytes 83.33 bytes/sec + total size is 2 speedup is 0.02 + /home/1.txt root@slave2:/home + ==================slave2================== + sending incremental file list + 1.txt + + sent 90 bytes received 35 bytes 250.00 bytes/sec + total size is 2 speedup is 0.02 + ``` + +# 3. javapsall 脚本 + +- 建立 jps 软链接 + + ``` + ln -s /usr/local/jdk1.8/bin/jps /usr/local/bin/jps + ``` + +- 在 `/usr/local/bin` 路径下编辑 `javapsall` 脚本 + + ``` + for host in hadoop102 hadoop103 hadoop104 + do + echo "========== $host =========" + ssh $host "jps" | grep -v Jps + done + ``` +- 修改 javapsall 权限 + + ``` + chmod 777 javapsall + ``` +- 测试结果 + + ``` + [root@hadoop102 bin]# javapsall + ========== hadoop102 ========= + ========== hadoop103 ========= + ========== hadoop104 ========= + ``` + +# 4. xcall 集群内执行命令 + +#### 4.1. 编辑 xcall +``` +cd /usr/local/bin +vim xcall +``` + +```aidl +#!/bin/bash +pcount=$# +if((pcount==0));then + echo "没有参数!"; + exit; +fi +echo -------hadoop102------ +$@ +for((host=2; host<=4; host++)); do + echo -------hadoop10$host------ + ssh hadoop10$host $@ +done +``` + +```aidl +chmod 777 xcall +``` + +#### 4.2. xcall 使用示例 + +```aidl +xcall jps + + +-------hadoop102------ +13586 DataNode +13994 Jps +13933 JobHistoryServer +13455 NameNode +-------hadoop102------ +13586 DataNode +14009 Jps +13933 JobHistoryServer +13455 NameNode +-------hadoop103------ +5040 Jps +4703 NodeManager +4575 ResourceManager +-------hadoop104------ +3408 SecondaryNameNode +3443 Jps +``` + +# 5. Hadoop 集群启动命令 + +#### 5.1. 编辑 myhadoop.sh 文件 + +- 在 `/usr/local/bin` 下创建文件 + + ```aidl + #!/bin/bash + # 判断参数个数 + if [ $# -ne 1 ];then + echo "need one param, but given $#" + fi + + # 操作hadoop + case $1 in + "start") + echo " ========== 启动hadoop集群 ========== " + echo ' ---------- 启动 hdfs ---------- ' + ssh hadoop102 "/usr/local/hadoop-3.1.3/sbin/start-dfs.sh" + echo ' ---------- 启动 yarn ---------- ' + ssh hadoop103 "/usr/local/hadoop-3.1.3/sbin/start-yarn.sh" + echo ' ---------- 启动 historyserver ---------- ' + ssh hadoop102 "/usr/local/hadoop-3.1.3/sbin/mr-jobhistory-daemon.sh start historyserver" + ;; + "stop") + echo " ========== 关闭hadoop集群 ========== " + echo ' ---------- 关闭 historyserver ---------- ' + ssh hadoop102 "/usr/local/hadoop-3.1.3/sbin/mr-jobhistory-daemon.sh stop historyserver" + echo ' ---------- 关闭 hdfs ---------- ' + ssh hadoop102 "/usr/local/hadoop-3.1.3/sbin/stop-dfs.sh" + echo ' ---------- 关闭 yarn ---------- ' + ssh hadoop103 "/usr/local/hadoop-3.1.3/sbin/stop-yarn.sh" + ;; + *) + echo "Input Param Error ..." + ;; + ``` + +#### 5.2. 集群启动关闭使用示例 + +- 启动集群 + + ``` + myhadoop.sh start + + + ========== 启动hadoop集群 ========== + ---------- 启动 hdfs ---------- + Starting namenodes on [hadoop102] + Last login: Fri May 13 23:23:13 CST 2022 + Starting datanodes + Last login: Fri May 13 23:23:25 CST 2022 + Starting secondary namenodes [hadoop102] + Last login: Fri May 13 23:23:28 CST 2022 + ---------- 启动 yarn ---------- + Starting resourcemanager + Last login: Fri May 13 23:23:18 CST 2022 + Starting nodemanagers + Last login: Fri May 13 23:23:38 CST 2022 + ---------- 启动 historyserver ---------- + WARNING: Use of this script to start the MR JobHistory daemon is deprecated. + WARNING: Attempting to execute replacement "mapred --daemon start" instead. + ``` + +- 关闭集群 + + ```aidl + myhadoop.sh stop + + + ========== 关闭hadoop集群 ========== + ---------- 关闭 historyserver ---------- + WARNING: Use of this script to stop the MR JobHistory daemon is deprecated. + WARNING: Attempting to execute replacement "mapred --daemon stop" instead. + ---------- 关闭 hdfs ---------- + Stopping namenodes on [hadoop102] + Last login: Fri May 13 23:07:27 CST 2022 from 183.14.30.31 on pts/1 + Stopping datanodes + Last login: Fri May 13 23:23:12 CST 2022 + Stopping secondary namenodes [hadoop102] + Last login: Fri May 13 23:23:12 CST 2022 + ---------- 关闭 yarn ---------- + Stopping nodemanagers + Last login: Fri May 13 22:24:28 CST 2022 from 183.14.30.31 on pts/0 + Stopping resourcemanager + Last login: Fri May 13 23:23:16 CST 2022 + ``` + + + + + diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232\346\225\260\346\215\256\344\270\255\345\217\260\345\273\272\350\256\276\346\226\271\346\263\225\350\256\272.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232\346\225\260\346\215\256\344\270\255\345\217\260\345\273\272\350\256\276\346\226\271\346\263\225\350\256\272.md" new file mode 100644 index 00000000000..96a4773034b --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232\346\225\260\346\215\256\344\270\255\345\217\260\345\273\272\350\256\276\346\226\271\346\263\225\350\256\272.md" @@ -0,0 +1,187 @@ +--- +layout: post +title: 数据中台:数据中台建设方法论 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据中台 +--- + + + +# 1. 数据中台建设的基本流程 + + +![]({{site.baseurl}}/img-post/数据中台-2.png) + + +# 2. 数据中台建设的常见问题 + +- 企业在数据中台建设过程中,通常以IT条线人员作为产品经理主导,虽然能够做到技术架构的先进性,围绕“ODS、DWD、DWS”大规模存储及计算性能展开投入,但业务参与度太弱,导致系统对业务响应的敏捷度较差,业务通用性低。 +- 往往企业新上一个业务,在业务看来很简单的“接入数据、生成标签、报表统计、提取数据”业务需求,在数据中台从需求分析到上线支持以月为周期。 +>2020年底,传出阿里掌门逍遥子要拆掉亲自搭建的大中台,核心原因对业务一线响应效率较差。 + +#### 2.1. 数据标准建立和协调困难 + +- 业务板块和业务线众多,数据标准难建立,协调扩展困难。 + +#### 2.2. 技术选型困难 + +- 技术选型众多,不同业务方有不同的数据需求,技术选型时依据这些客观需求及主观偏好,会选择不同的计算框架和数据组件。 + +#### 2.3. 数据需求多样 + +- 业务部门需求多样化,包括:报表计算、可视化看板、数据探索、数据服务、结果推送、数据采集及迁移、A/B测试、标签体系、用户触达、数据应用等。 + +#### 2.4. 数据需求多变 + +- 为应对市场的快速变化,业务方的数据需求也是多变的,看板必须能够按需调整,标签必须能自主配置,诸如此类的需求需要有大量自助工具来支撑。 + +#### 2.5. 数据正确性难以确定 + +- 随着数据的复杂度越来越高,数据链条越来越长,数据源越来越多,保证数据的正确性及验证数据将成为一个很耗时的问题。 + +#### 2.6. 数据管理复杂 + +- 业界对数据的可解释性、可管理性要求越来越高,各种新存储架构的加入,使得元数据管理和数据流程标准化更加复杂。 + +#### 2.7. 数据安全管理复杂 + +- 如果无法保证数据安全,数据就是不可用的,数据合规使得数据安全成为刚需,这里要求能够支撑多级数据安全策略、数据链路可追溯、敏感数据可加密。 + +#### 2.8. 数据权限管理 + +- 在数据赋能的体系中权限控制是很关键的功能,需要实现各种级别的数据权限,组织架构、角色、权限策略自动化,以及对新的计算架构的权限管理。 + +#### 2.9. 数据成本高,难以量化 + +- 数据成本包括集群成本、运维成本、人力成本、时间成本等,持续系统地计算这些成本需要在系统架构中加入相应的统计接口,而现有的大多数平台并没有将这些接口考虑在内。 + +# 3. 数据中台建设的内容 + +#### 3.1. 底层服务 + +- 底层服务层重点为整个数据中台,从数据接入层到数据安全管控全流程的数据活动,提供统一的数据存储资源、计算引擎、数据处理中间件服务,增强了服务器资源的有效调度和统一管理。 + +#### 3.2. 数据接入 + +- 数据接入层提供统一的数据接入平台,根据数据采集的业务场景,平台提供了数据收集的工具及解决方案,让数据采集》数据传输》数据存储》数据资源管理全链路都可自动化完成,并实现对活动任务的自动化监控。 + +#### 3.3. 数据整合 + +- 数据整合层提供统一数据处理及标签/模型开发服务。在整合层的数据需求应当来自企业的数据使用场景对数据进行建模,生成如标签管理、数据仓库这样的服务平台,为企业的数据团队/业务团队使用数据提供一个高效的整合后的数据源; +- 并在这一层对数据进行治理,例如编码规范、主题域划分,表模型规划、数据质量校验规则设计等都在这一层完成,通过这些模型、规范及平台的协同作用,为企业提供可高效获取并质量可靠的数据。 + +#### 3.4. 数据挖掘分析 + +- 数据挖掘分析层整合了企业存在的几大数据挖掘及分析工作场景,比如对用户行为数据进行数据分析、通过算法模型挖掘用户潜在的商业价值、或者多个BU之间进行数据加密碰撞发现新合作场景等,这一层提供的平台及工具,基本覆盖了大部分数据挖掘的工作场景; +- 通过数据中台实现共享这些数据组件,使得各部门和团队都可以通过工具高效完成挖掘分析工作。 + +#### 3.5. 业务应用 + +- 业务应用层是指可以直接提供给业务端使用的数据产品,业务可以直接使用这些数据产品高效获得满足业务需求的目标数据,甚至这些数据可以直接打通到业务系统; +- 比如 DMP 平台,让用户从 `产生数据需求 > 数据加工 > 数据使用` 的整个数据获取周期大幅度缩短。 +- 此外,也可以通过提满足特定具体业务场景的数据应用来给业务赋能。 + +#### 3.6. 数据服务管理 + +- 数据服务管理层提供统一的数据服务出口,目标在帮助企业提升数据资产的应用价值,同时要保证数据的安全性和有效性。 +- 统一服务通过行业成熟的ONESERVICE解决方案,构建API和数据服务接口来满足不同数据使用场景的需要,同时降低了数据的开发门槛。 + +# 4. 数据中台的设计原则 + +#### 4.1. 数据通用性 + +- 数据中台的核心价值就是共享,因此数据中台的数据标准、数据接口、数据规则是否具备通用性衡量了数据中台的成功与否的标准; +- 如果每一个新接入的数据源需要重新设计接口,设计数据处理流程、更新宽表、开发新的标签,那么数据中台不过是把数据烟囱平移到新的平台而已。 + +#### 4.2. 数据标准化 + +- 数据中台具有沉淀数据的价值使命,数据通用性、可用性主要依赖于“数据的标准化”。 + +#### 4.3. 服务敏捷性 + +- 数据中台强调复用,复用最大的效果之一响应业务需求“快速敏捷”; +- 如果一个数据需求中台响应时效不如过去的数据集市,那么中台难以满足日益提速的业务节奏。 + + +# 5. 数据中台建设涉及的角色 + +#### 5.1. 业务部门主管 / 接口人 + +- 了解业务流程和优先级,能够将业务场景与数据对应,指导建模的流程。 +- 了解企业的系统架构、技术框架。 +- 对业务流程非常熟悉,通常是技术部门与业务部门的纽带。 + +#### 5.2. 数据工程师 + +- 数据平台工程师: + - 通常有系统工程师背景,负责建设和运维数据平台,安装和运维各种大数据组件,以及保证数据平台的性能和稳定性。 +- 数据开发工程师: + - 以数据仓库技能为背景,懂业务,负责建模、数据清洗和编写ETL程序。 +- 数据应用开发工程师: + - 以应用开发为背景,开发服务于业务部门的数据应用。 + +#### 5.3. 数据中台架构师 + +- 全面掌握数据平台的功能,对公司的产品提出数据的支持和要求,负责公司产品与数据平台的集成、与业务系统进行衔接的架构规划以及公司的数据标准推动和把控。 + +#### 5.4. 数据分析师 + +- 以统计学背景为主,能够从数据中产生合理、准确的商业智能报表。 + +#### 5.5. 数据科学家 + +- 以机器学习为背景,提供基于机器学习和人工智能的数据分析产品和结果。 + +#### 5.6. 数据产品经理 + +- 5负责公司内部数据能力的规划和开发流程的协调,有时这个角色由数据架构师承担。 + +![]({{site.baseurl}}/img-post/数据中台-1.png) + +# 6. 数据中台建设路径 + +#### 6.1. 路径一:业务中台与数据中台并行建设 + +- 数据中台与业务中台并行建设,复杂度可想而知,也是挑战难度最大的路径。因为建设业务中台的过程需要对业务进行梳理和规划; +- 这个过程会反复出现多次对流程调整,这给数据中台建设带来了非常多的不确定性,这也进一步增加了数据中台建设的难度。 +- 在数字化营销成熟发展的今天,其实业务与数据早已不能完全分割,业务数据化和数据业务化几乎是同时完成的。 + +#### 6.2. 路径二: 业务中台先行建设,数据中台跟进 + +- 相对比数据中台与业务中台并行的复杂度和挑战度更高,路径二显得更加稳健,也是被企业采用最多的一种路径。 +- 业务中台先行,数据中台跟进,这种模式吸取了第一种模式下的业务不确定给数据中台带来了多种不确定的教训。 + +# 7. 案例:用户行为事件-数据接口标准化-建设实践 + +#### 7.1 接口的通用化 + +- 实现接口的通用化,对实时接口、批量接口、文件上传接口中字段的定义配置化与模块化。 +- 例如:90% 的事件数据可以进行通用化定义: + - 事件来源(枚举值/文本) + - 事件对象(枚举值/文本) + - 事件开始时间(日期) + - 结束时间(日期) + - 事件内容(枚举值/文件/数值) + - 事件特殊标记(枚举值/文件/数值/日期), +- 通用化标准化的接口,可以节约数据接入需求分析及接口重复设计工作量,仅需维护数据源的定义表。 +- 同时,对于非分析数据应用场景,允许数据应用层可以跨层调用接入的数据(避免ODS层、DWD层处理流程消耗的时效),根据需要设置接入数据临时表(只存储一定周期接入原始数据),直接供应用层调用(实际业务中很多事件数据无需DWS或DWD层进行加工),可大幅提高响应业务时效。 + +#### 7.2. 寿险领域实践案例 + +- 在寿险保险领域,客户的投保、保全、理赔、咨诉事件数据都可以纳入通用化接口: + - 事件类型:设置3个数值字段(如应用到咨诉-投诉-销售投诉) + - 事件名称:设置2个文本字段(XX活动) + - 事件对象:设置6个文本字段(客户号、手机号、保单号) + - 事件开始与结束时间:各设置两个日期字段(客户投诉时间、投诉事件发生时间、实际结案时间、客户要求结案时间) + - 事件内容:设置三个文本字段(投诉内容、客服备注内容) + - 特殊标记:设置2个数值、2个文本、2个日期(如投诉是否升级等)。 +- 实践证明通用化接口兼容 80% 以上保险领域事件数据,减少需求分析及接口设计工作。 +- 寿险实际业务中,“作业报表、触点推送”两类应用中,很多数据无需在DWS或DWD层汇总加工。 +- 因此,增加设置接入数据临时表,供中台的报表、触点两两大应用模块取用进行快速增量更新,实现与数据写入ODS层流程解耦,有效解决排期不一致各种问题。 + + diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232\346\225\260\346\215\256\344\270\255\345\217\260\347\232\204\345\212\237\350\203\275\346\250\241\345\235\227.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232\346\225\260\346\215\256\344\270\255\345\217\260\347\232\204\345\212\237\350\203\275\346\250\241\345\235\227.md" new file mode 100644 index 00000000000..47123c4e959 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232\346\225\260\346\215\256\344\270\255\345\217\260\347\232\204\345\212\237\350\203\275\346\250\241\345\235\227.md" @@ -0,0 +1,74 @@ +--- +layout: post +title: 数据中台:数据中台的功能模块 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据中台 +--- + + + +# 1. 工具平台模块 + + +工具平台层是数据中台的载体,包含大数据处理的基础能力技术,如集数据采集、数据存储、数据计算、数据安全等于一体的大数据平台;还包含建设数据中台的一系列工具,如离线或实时数据研发 工具、数据联通工具、标签计算工具、算法平台工具、数据服务工具及自助分析工具。 + +![]({{site.baseurl}}/img-post/数据中台-4.png) + + +# 2. 数据资产模块 + +#### 2.1. 主题域模型 + 主题域模型是指面向业务分析,将业务过程或维度进行抽象的集合。业务过程可以概括为一个个不可拆分的行为事件,如订单、合同、营销等。 + + 为了保障整个体系的生命力,主题域即数据域需要抽象提炼,并且长期维护和更新,但是不轻易变动。在划分数据域时,既要涵盖当前所有业务的需求,又要保证新业务能够无影响地被包含进已有的数据域中或者很容易扩展新的数据域. + +#### 2.2. 标签模型 + 标签模型的设计与主题域模型方法大同小异,同样需要结合业务过程进行设计,需要充分理解业务过程。 + + 标签一般会涉及企业经营过程中的实体对象,如会员、商品、门店、经销商等。这些主体一般来说都穿插在各个业务流程中,比如会员一般都穿插在关注、注册、浏览、下单、评价、服务等环节。那么在设计标签的时候就需要充分理解这些业务流程,在流程中发现标签的应用点,结合这些应用点来搭建企业的标签体系。标签模型按计算模式一般分为客观标签和主观标签。 + + 设计标签模型时非常关键的要素是标签模型一定要具有可扩展性。毕竟标签这种数据资产是需要持续运营的,也是有生命周期的,在运营的过程中随时可能增加新的标签。 + +#### 2.2. 算法模型 + 算法模型更加贴近业务场景。在设计算法模型的时候要反复推演算法模型使用的场景,包括模型的冷启动等问题。整个模型搭建过程包含定场景、数据源准备、特征工程、模型设计、模型训练、正式上线、参数调整7个环节。 + + 以新零售企业为例,常用的机器学习算法有决策树、神经网络、关联规则、聚类、贝叶斯、支持向量机等。这些算法已经非常成熟,可以用来实现商品个性化推荐、销量预测、流失预测、商品组货优化等新零售场景的算法模型。 + +# 3. 数据应用模块 + +数据应用层严格来说不属于数据中台的范畴,但数据中台的使命就是为业务赋能,几乎所有企业在建设数据中台的同时都已规划好数据应用。数据应用可按数据使用场景来划分为以下多个使用领域:分析与决策应用、标签应用、智能应用。 + +#### 3.1. 底层服务 + +- 底层服务层重点为整个数据中台,从数据接入层到数据安全管控全流程的数据活动,提供统一的数据存储资源、计算引擎、数据处理中间件服务,增强了服务器资源的有效调度和统一管理。 + +#### 3.2. 数据接入 + +- 数据接入层提供统一的数据接入平台,根据数据采集的业务场景,平台提供了数据收集的工具及解决方案,让数据采集》数据传输》数据存储》数据资源管理全链路都可自动化完成,并实现对活动任务的自动化监控。 + +#### 3.3. 数据整合 + +- 数据整合层提供统一数据处理及标签/模型开发服务。在整合层的数据需求应当来自企业的数据使用场景对数据进行建模,生成如标签管理、数据仓库这样的服务平台,为企业的数据团队/业务团队使用数据提供一个高效的整合后的数据源; +- 并在这一层对数据进行治理,例如编码规范、主题域划分,表模型规划、数据质量校验规则设计等都在这一层完成,通过这些模型、规范及平台的协同作用,为企业提供可高效获取并质量可靠的数据。 + +#### 3.4. 数据挖掘分析 + +- 数据挖掘分析层整合了企业存在的几大数据挖掘及分析工作场景,比如对用户行为数据进行数据分析、通过算法模型挖掘用户潜在的商业价值、或者多个BU之间进行数据加密碰撞发现新合作场景等,这一层提供的平台及工具,基本覆盖了大部分数据挖掘的工作场景; +- 通过数据中台实现共享这些数据组件,使得各部门和团队都可以通过工具高效完成挖掘分析工作。 + +#### 3.5. 业务应用 + +- 业务应用层是指可以直接提供给业务端使用的数据产品,业务可以直接使用这些数据产品高效获得满足业务需求的目标数据,甚至这些数据可以直接打通到业务系统; +- 比如 DMP 平台,让用户从 `产生数据需求 > 数据加工 > 数据使用` 的整个数据获取周期大幅度缩短。 +- 此外,也可以通过提满足特定具体业务场景的数据应用来给业务赋能。 + +#### 3.6. 数据服务管理 + +- 数据服务管理层提供统一的数据服务出口,目标在帮助企业提升数据资产的应用价值,同时要保证数据的安全性和有效性。 +- 统一服务通过行业成熟的ONESERVICE解决方案,构建API和数据服务接口来满足不同数据使用场景的需要,同时降低了数据的开发门槛。 + diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232\346\225\260\346\215\256\344\270\255\345\217\260\347\232\204\345\237\272\346\234\254\346\246\202\345\277\265.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232\346\225\260\346\215\256\344\270\255\345\217\260\347\232\204\345\237\272\346\234\254\346\246\202\345\277\265.md" new file mode 100644 index 00000000000..fa851bf6750 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\344\270\255\345\217\260\357\274\232\346\225\260\346\215\256\344\270\255\345\217\260\347\232\204\345\237\272\346\234\254\346\246\202\345\277\265.md" @@ -0,0 +1,151 @@ +--- +layout: post +title: 数据中台:数据中台的基本概念 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据中台 +--- + + + +# 1. 数据中台概念 + +#### 1.1. 数据中台的定义 + +- 数据中台的概念是最早由阿里巴巴首次提出,其最核心的是 OneData 体系。 +- 这个数据管理体系包括: + - 全局数据仓库规划 + - 数据规范定义 + - 数据建模研发 + - 数据连接萃取 + - 数据运维监控 + - 数据资产管理工具等 +- 数据中台对海量数据进行采集、计算、存储、加工,同时统一标准和口径。 +- 数据中台把数据统一之后,会形成标准数据,再进行存储,形成大数据资产,进而为企业不同部门设置外部企业提供高效服务。 + +- 从技术视角: + - 数据中台是一种数据管理体系,最重要的目标是支持各部门业务数据和提供计算服务。数据中台的本质就是“数据仓库+数据服务中间件”。 + +- 从业务视角: + - 数据中台是指通过完成企业内外部多源异构的数据采集、治理、建模、分析、应用,打通数据孤岛实现数据集中管理应用,成为企业数据资产管理中枢。 + +#### 1.2. 数据中台的本质 + +- 数据中台的本质,就是“数据仓库+数据服务中间件”,它不仅为数据分析挖掘而建,同时作为各个业务的数据源,为业务系统提供数据和计算服务。 +- 在这个过程中,数据中台不仅要解决“读”的问题,也要解决“写”的问题,需要的是一整套的数据治理规范。这一点在“主数据管理”和“交易数据管理”上,都能得到很好的体现。 + + +# 2. 数据中台要解决的问题 + +#### 2.1. 数据的统一管理 + +- 通过大数据的技术,实现对海量的、多类型的、多来源的数据统一管理,沉淀并形成数据资产; +- 具体包括: + - 数据接口管理 + - 数据模型管理 + - One ID(ID Mapping) + - 数据资产管理 + - 数据质量管理 + - 数据权限管理 + - 数据安全管理 + +#### 2.2. 多源、异构数据融合 + +- 在数字化时代,企业融入了整个互联网生态,企业内部、企业之间的联系在新的技术和工具下,业务连接更加紧密,这催生了企业之间的数据通过开放、共享,企业和社会级数据的连接服务,实现企业内、企业间、企业与社会公共服务的数据之间的融合共享; +- 数据来源体系包括: + - 业务系统 + - CRM + - BPM + - OA + - WMS + - TMS + - OMS + - 埋点数据 + - 前端埋点 + - 后端埋点 + - 第三方数据 + - 第三方企业提供的数据 + - 如:阿里妈妈、巨量引擎; + - 手工数据 + - 人工手动处理的表格文档; + - 爬虫数据 + - 通过爬虫技术获取的数据 + +#### 2.3. 数据服务体系 + +- 数据最终是为了服务于应用,通过面向智能的方法和工具,可大幅的提升数据运用的效率,帮助企业实现业务的实时响应。 +- 数据服务体系,具体包括: + - BI 报表 + - 用户分析 + - 商品分析 + - 流量分析 + - 交易分析 + - 自助分析平台 + - 自动化营销平台 + - 推荐系统 / 用户画像 + - 欺诈行为分析 + - ... + +# 3. 数据中台与传统数据仓库的差别 + +#### 3.1. 数据存储差异 + +- 与存储“已知”结构化数据,解决“已知问题”的传统数据仓库(Data Warehouse)相比,数据中台存储了大量“未知”的原始数据,利用数据科学(Data Science)可在应用层面进行更多探索,帮助企业解决更多“未知”的商业问题。 + +#### 3.2. 数据量级差异 +- 数字技术的革命,使得可收集的数据在量级上产生了爆发,因为数据的“量变”,催生了数据管理和应用的“质变”,这是数据中台出现的主要原因。 +- 如果说传统的“数据仓库”面对的是“小数据”,数据中台处理的则是真正的“大数据”。 + +#### 3.3. 服务对象差异 +- 传统的数据仓库只是满足领导数据决策的需要,因此更多地体现在报表输出,使用者以小部分的业务人员和决策层为主,新需求的开发周期以月甚至到年为计。 +- 而数据中台由于起家于互联网企业,其使用对象扩大到一线服务人员和商家企业,其业务需求更繁杂,很难用一套报表系统满足需求,因此催生出一个生态的数据服务。 + +#### 3.4. 在服务表现形式差异 + +- 数据中台体现的更多样化,数据中台不仅能提供报表基础服务功能,而且为了满足各个业务部门不同需求,会提供领导决策系统、行业分析、业务洞察、业务重塑、自助查询等多个功能,满足从领导层、PD、业务人员、开发人员等各个层级的需求。 +- 在继承性方面,数据中台采用传统的数据仓库Kimball维度建模法,按照事实表、维表来构建数据中台的数据模型。 + + +# 4. 数据中台的三种形式 + +#### 4.1. Data Lake(数据湖) + +- 技术难度最重的一种,定位是企业业务层面的数据大集市,会整合全公司各种数据源,支撑的不只是营销场景,还包括企业个性化的业务场景; +- 往往由企业的最高层直接领导,目标是帮助企业进行数字化转型。由于在数据对接和数据处理层面需要处理大量定制化数据源,因此构建过程往往以年为时间单位; + +#### 4.2. CDP(Customer Data Platform) + +- 技术难度稍低的数据中台,定位是营销层面的数据大集市,目标是支撑各种利用企业自有数据的营销场景。 +- 因为CDP通常只对接标准化数据源(例如两个企业用的是同一款标准化CRM,他们的底层数据结构都是一样的),数据治理和数据管理相对容易,因此实施周期以月为单位; + +#### 4.3. DMP(Data Management Platform) + +- 定位是支撑以程序化广告为主的实时营销场景,和Data Lake,CDP的最大不同是毫秒级数据输出。因为DMP主要用到的是广告监测数据、网站分析数据和第三方大数据,数据格式相对固定,因此实施难度最低。 + +# 5. 数据中台在营销中的应用 + +- 赋予企业数字营销的精细化操作能力: + - 当市场部们承接的数字营销预算大到一定程度时,便无法仅凭借营销人员的个人经验对营销活动进行微观操作。 + - 而在拥有数据中台后,便可依靠数据+技术,驱动整个营销体系的精细化操作; + +- 提升营销执行的ROI: + - 这是企业最常规的诉求,市场部绝大部分预算都分配在营销执行层面。按照每年1亿的营销投入计算,如果能通过数据提升1%的精准度,就能为企业节省100万的成本,这是能最直接看到的真金白银; + +- 战略视角的营销策略: + - 在打通生产、销售、电商、服务等数据后,市场部就能看到更加连贯的全局数据,可以站在更高维度审视营销在公司战略布局中的定位和作用; + +- 提升市场部内部运营的整合度: + - 当市场部内部职能划分过细,便需要通过数据来串接营销运营过程中的市场研究->市场策略->营销执行->效果考核,避免内部信息不对称,提升运营效率 ; + +- 加强市场部和其他部门间协作: + - 当企业内部组织架构达到一定复杂度,市场部需要通过数据对接其他部门的运作,在企业统一的考核体系下,于企业内部证明自身价值,争取更多资源; + +- 支撑业务的数字化转型: + - “数字营销”已不再只是营销词汇,数据中台所拥有的资源(数据/IT设施/考核规则/运营人员),除了支持营销场景,还可用于构建各种数字化转型的业务场景,作为CMO和CEO/CGO/CDO对话的核心资本。 + - 今天讨论建立营销数据中台的,除了市场部和IT部门,很多需求是来自更高层的CEO、COO(首席运营官)、CGO(首席增长官),这些高层的诉求通过“数据中台”来解决业务问题(例如产能过剩、人员效能、获客),支持企业的创新业务(例如新零售、金融科技、数字化管理)。 + + diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232ETL \344\273\273\345\212\241\347\233\221\346\216\247\344\270\216\345\221\212\350\255\246.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232ETL \344\273\273\345\212\241\347\233\221\346\216\247\344\270\216\345\221\212\350\255\246.md" new file mode 100644 index 00000000000..4bf913acd8a --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232ETL \344\273\273\345\212\241\347\233\221\346\216\247\344\270\216\345\221\212\350\255\246.md" @@ -0,0 +1,212 @@ +--- +layout: post +title: 数据治理:ETL 任务监控与告警 +subtitle: 钉钉机器人 & 邮件自动告警 +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据治理 +--- + + +#### 监控系统 + +- 监控系统,一般是对大数据整个架构、各个数据的输入输出流、中间件的稳定性、数据的准确性、资源的使用情况、任务的执行情况进行监控; +- 一般的监控告警通过采集告警日志、错误数据、关键词匹配等,获取错误的数据进行实时展现并告警; +- 常见的监控系统以 Grafana 为基础,主要功能是将收集存储的数据按照不同维度、不同应用、不同用户进行配置化的展示; +- 为了保证数据安全,每个团队只能看到自己的应用数据。 +- 同时,对不同维度的数据,可以进行报警配置,根据最常用的报警方式,提供了钉钉报警、邮件报警两方式。 + +#### 监控对象 + +- binlog +- Kafka +- 同步任务 +- ETL 任务 + +#### 监控方式 + +- 监控库表 + - 数据总量 + - 数据增量 +- 监控字段 + - 字段空值缺失情况 + - NULL 占比超出阈值 + - 字段异常情况 + - INT 型字段值超出范围 + - 枚举 型字段值超出取值范围 + +#### 监控示例 + +- 以监控表数据总量为例 + +- 表设计 + - 创建配置表 + - 将配置文件生成一张表,每个人可以通过数据库 insert 的操作去添加自己需要监控的表。 + + ```aidl + CREATE TABLE `warehouse.dj_rpt_check_conf` ( + `db` varchar(8) NOT NULL COMMENT '数据库别名,例如bi,online,warehouse结果库)' , + `tbl` varchar(64) NOT NULL COMMENT '表名', + `condition` varchar(256) NOT NULL COMMENT '筛选条件', + `threshold` bigint(20) NOT NULL DEFAULT 0 COMMENT '阈值', + `owner` varchar(16) NOT NULL default 'nobody' COMMENT '负责人:每个人自己固定用一个名字', + `ptype` varchar(8) NOT NULL COMMENT '检查周期,例如:d(天),w(周,周一),m(月,1号)', + unique index tbl_db (tbl,db) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; + + 插入数据: + insert into dj_rpt_check_conf values ('bi','dj_share_category_di','stat_date="${ds}"',20,'dy','d'); + ``` +- 监控脚本 + ```aidl + # coding: utf-8 + + from djtool import * + import pandas as pd + import sqlalchemy as sq + from sqlalchemy import exc + import sys + import requests + import json + import copy + + owner_mobile={'gw':'123456789'} + + def check_table(conn,table,condition,threshold): + try: + cursor = conn.cursor() + check_sql= 'select count(1) from ' + table + ' where ' + condition + cursor.execute(check_sql) + data = cursor.fetchone() + if(data[0]>>>>>>>>>>>>", code, message) + return -1 + + ds= sys.argv[1] + check_conf = pd.read_sql_table('dj_rpt_check_conf',get_sqlalchemy_conn('mysql','bg')) + + check_conf['condition'] = check_conf.condition.str.replace('\$\{ds\}',ds) + check_conf['real_cnt'] = 0 + check_conf['failed'] = 0 + + for db in check_conf.db.unique(): + db_conn=get_pymysql_conn("mysql_"+db) + for index,row in check_conf[(check_conf.db==db) & (check_conf.ptype=='d')].iterrows(): + real_cnt = check_table(db_conn,row['tbl'],row['condition'],row['threshold']) + if(real_cnt is not None): + check_conf.loc[index,'real_cnt']=real_cnt + check_conf.loc[index,'failed']=1 + + mail_text = ''' + 配置表:`warehouse.dj_rpt_check_conf` + ''' + fail_conf = check_conf[(check_conf.failed==1)] + + if(fail_conf.shape[0]>0): + send_mail(['data@idongjia.cn'],[],ds+'--BI任务失败列表',mail_text + fail_conf.to_html()) + else: + send_mail(['data@idongjia.cn'],[],ds+'--已经加入监控的BI任务完成:)',mail_text) + + headers = {'Content-Type': 'application/json'} + ding_url = 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxx' + msg={ + "msgtype": "markdown", + "markdown": {"title":"BI任务失败了:"+ds, + "text":"#### BI任务失败了:"+ds+" \n @mobile 失败任务:\n- fail_task " + }, + "at": { + "atMobiles": [ + "88888" + ] + } + } + + for owner in fail_conf.owner.unique(): + tmp_msg = copy.deepcopy(msg) + tmp_msg['at']['atMobiles']=[owner_mobile[owner]] + tmp_msg['markdown']['text']=tmp_msg['markdown']['text'].replace('mobile',owner_mobile[owner]) + tmp_msg['markdown']['text']=tmp_msg['markdown']['text'].replace('fail_task',"\n- ".join(fail_conf[(fail_conf.owner==owner)].tbl.values.tolist())) + requests.post(ding_url, headers=headers,data=json.dumps(tmp_msg)) + ``` +- 告警 + - 钉钉告警 + - 钉钉开发文档:https://link.zhihu.com/?target=https%3A//ding-doc.dingtalk.com/doc%23/serverapi2/qf2nxq + + ![]({{site.baseurl}}/img-post/ETL监控告警-1.png) + + - 发邮件 + + ```aidl + #!/usr/bin/python + # -*- coding: UTF-8 -*- + + import smtplib + import sys + from email.mime.text import MIMEText + from email.mime.multipart import MIMEMultipart + from email.header import Header + from email.utils import formataddr + from sys import argv + + def main(): + sender = 'test' + receivers = ['gaowei@test.cn'] + password = 'xxxxxxxx' + message = MIMEMultipart() + message['From'] = formataddr(["数据组",sender]) + message['To'] = formataddr(["数据组成员",receivers]) + subject = 'rest' + message['Subject'] = Header(subject, 'utf-8') + message.attach(MIMEText(sys.argv[1]+'数据见附件\n', 'plain', 'utf-8')) + att1 = MIMEText(open("aa.csv", 'rb').read(), 'base64', 'utf-8') + att1["Content-Type"] = 'application/octet-stream' + att1["Content-Disposition"] = 'attachment; '+'filename='+sys.argv[1]+'.csv' + message.attach(att1) + try: + server=smtplib.SMTP_SSL("smtp.exmail.qq.com", 465) + server.login(sender, password) + server.sendmail(sender,receivers,message.as_string()) + print "邮件发送成功" + except smtplib.SMTPException: + print "Error: 无法发送邮件" + + if __name__ == '__main__': + main() + ``` + - shell 脚本 + + ```aidl + #!/usr/bin/env bash + export JAVA_HOME=/opt/jdk1.8.0_121 + export JRE_HOME=${JAVA_HOME}/jre + export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib + export PATH=$PATH:${JAVA_HOME}/bin:{JRE_HOME}/bin:$PATH + export PATH=$PATH:/opt/mysql/bin + dateStrDay=$1 + if [ -z "$1" ] ; then + dateStrDay=`date +%Y-%m-%d` + fi + echo $dateStrDay + dateStr=`date +%Y-%m-%d-%M-%S` + + hadoop fs -rm -r /tmp/gaowei/test + + cd /opt/task/gaowei/warehouse/test + rm *.csv + + spark2-submit --class cn.idongjia.data.auction.AuctionOrder --master yarn --deploy-mode cluster /opt/task/gaowei/warehouse/test/datawarehouse_2.11-1.0.jar + + hadoop fs -getmerge /tmp/gaowei/test/* /opt/task/gaowei/warehouse/test/bb.csv + + sed '1i\用户id,拍卖订单数,跑单数,异常订单数,是否禁言禁拍(0表示否,1表示是),是否在白名单(0表示否,1表示是),是否屏蔽(0表示否,1表示是,时间)' /opt/task/gaowei/warehouse/test/bb.csv > /opt/task/gaowei/warehouse/test/aa.csv + + python Email.py ${dateStrDay} + ``` diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\345\205\203\346\225\260\346\215\256\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\345\205\203\346\225\260\346\215\256\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..eda2830d572 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\345\205\203\346\225\260\346\215\256\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,159 @@ +--- +layout: post +title: 数据治理:元数据的概念详解 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据治理 +--- + +# 1. 元数据的概念 + +- 元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。 +- 元数据算是一种电子式目录,为了达到编制目录的目的,必须在描述并收藏数据的内容或特色,进而达成协助数据检索的目的。 +- 元数据,可以理解为比一般意义的数据范畴更加广泛的数据,不再仅仅表示数据的类型、名称、值等信息,它可以进一步提供数据的上下文描述信息,比如: + - 数据的所属域 + - 数据取值范围 + - 数据间的关系 + - 业务规则 + - 数据的来源。 +- 元数据相当于企业数据的DNA,它可以告诉开发者,有用的数据在哪里,能提供一份数据结构定义和元素的详细示意图,数据来龙去脉、关系,使应用开发过程更有效,提供数据的参照性、引用性、血缘分析、影响分析、变化分析等。 +- 元数据,是一种进行数据资产的梳理和盘点的有效工具,方便企业对自身内部的数据资产进行管理; + - 对于 RMDB 库来说,元数据(字典)是属于数据库本身的一些数据,包含数据库名、数据库字符集、表名、表的大小、表的记录行数、表的字符集、表的字段、表的索引、表的描述、字段的类型、字段的精度、字段的描述等。 + - 但要注意:对于来自 information_schema 的数据库内部数据,如表的大小、表的行数可能不是非常精准,仅做数量级的参考。 +>注意:业务数据才会有元数据这个概念,系统日志、备份数据是没有元数据这个概念的。 + +元数据是企业数据资源的应用字典和操作指南,元数据管理有利于统一数据口径、标明数据方位、分析数据关系、管理数据变更,为企业级的数据战略规划、数据模型设计、数据标准管理、主数据管理、数据质量管理、数据安全管理以及数据的全生命周期管理提供支持,是企业实现数据自服务、推动企业数据化运营的可行路线。企业以元数据为抓手进行数据治理,帮助企业更好地对数据资产进行管理,理清数据之间的关系,实现精准高效的分析和决策。 + +# 2. 元数据的种类 + +- 技术元数据 +- 业务元数据 +- 操作元数据 +- 管理元数据 + +![]({{site.baseurl}}/img-post/元数据-1.png) + +# 3. 技术元数据 + +#### 3.1. 数据结构 + +- 库 + - 名称 + - 字符集 + +- 表 + - 名称 + - 描述 + - 字符集 + - 行数 + - 容量 + +- 字段 + - 名字 + - 类型 + - 是否可空 + - 长度 + - 精度 + - 描述 + +- 索引 + - 名字 + - 类型 + - 索引字段 + +- Key + - primary key + - unique key + +- 可编程对象 + - 名称 + - 类型 + +- SQL_MODE + - SQL_MODE 更多信息,可以参考文章 《 MySQL SQL Mode(模式)介绍 》。 + + +#### 3.2. 数据部署 +- 数据集的物理位置; + +#### 3.3. 存储过程 +- SQL 语句集 + +#### 3.4. 数据依赖 +- 数据流向,ETL规则,数据集之间的依赖关系; + +#### 3.5. 质量度量 +- 数据集上可以计算的度量; + +#### 3.6. 度量逻辑关系 +- 数据集度量之间的逻辑关系; + +#### 3.7. ETL 过程 +- ETL 顺序、并行&串行; + +#### 3.8. 数据集快照 +- 某一时间点上,数据在所有数据集上的分布情况; + +#### 3.9. 数据模型元数据 +- 事实表、维度、属性、层次; + +#### 3.10. 报表语义 +- 报表的指标体系、规则、过滤条件; +- 物理名称 & 业务名称对应关系; + +#### 3.11. 数据访问日志 +- 数据被用户访问的情况; + +#### 3.12. 质量稽核日志 +- 何时、哪个度量被稽核; + +#### 3.13. 数据装载日志 +- 那些数据、何时、被谁装载; + +# 4. 业务元数据 + +#### 4.1. 业务域 + +- 数据库表的业务域 + - 用户画像标签元数据 + - TODO +- 所在的项目 +- 所在的集群 + +#### 4.2. 业务规则 + +- 转换规则 +- 计算公式 +- 推导公式等(更多是文档); + +#### 4.3. 数据模型 + +#### 4.4. 数据质量规则和核检结果 + +#### 4.5. 数据标准 + +#### 4.6. 数据的安全 & 隐私级别 + +#### 4.7. 数据使用说明等 + +# 5. 操作元数据 + +- 批处理的执行日志; +- 调度异常记录及处理; +- 报表和查询的访问模式,访问频率和执行时间; +- 数据产生; +- 表的访问(查询,关联,聚合等); +- 字段的访问; +- 物理表的创建时间,创建人,更新时间,更新人 + +# 6. 管理元数据 + +- 人员 +- 流程 +- 职责、岗位 +- 组织、部门; + diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\345\205\203\346\225\260\346\215\256\347\256\241\347\220\206\346\236\266\346\236\204\345\217\212\351\241\271\347\233\256\345\256\236\346\226\275\346\255\245\351\252\244.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\345\205\203\346\225\260\346\215\256\347\256\241\347\220\206\346\236\266\346\236\204\345\217\212\351\241\271\347\233\256\345\256\236\346\226\275\346\255\245\351\252\244.md" new file mode 100644 index 00000000000..530d1143ee4 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\345\205\203\346\225\260\346\215\256\347\256\241\347\220\206\346\236\266\346\236\204\345\217\212\351\241\271\347\233\256\345\256\236\346\226\275\346\255\245\351\252\244.md" @@ -0,0 +1,260 @@ +--- +layout: post +title: 数据治理:元数据管理架构及项目实施步骤 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据治理 +--- + + +# 1. 元数据管理 + +#### 1.1. 元数据管理 + +- 元数据管理,记录数据仓库中模型的定义,各层级间的映射关系,监控数据仓库的数据状态及ETL的任务运行状态; +- 一般会通过元数据资料库(Metadata Repository)来统一地存储和管理元数据,其主要目的是使数据仓库的设计、部署、操作和管理能达成协同和一致; +- 元数据管理应具备的功能: + - 搜索和发现: + - 数据表、字段、标签、使用信息; + - 访问控制: + - 访问控制组、用户、策略; + - 数据血缘: + - 管道执行、查询; + - 合规性: + - 数据隐私/合规性注释类型的分类; + - 数据管理: + - 数据源配置、摄取配置、保留配置、数据清除策略; + - AI 可解释性、再现性: + - 特征定义、模型定义、训练运行执行、问题陈述; + - 数据操作: + - 管道执行、处理的数据分区、数据统计; + - 数据质量: + - 数据质量规则定义、规则执行结果、数据统计; + +#### 1.2. 元数据管理的价值 + +- 数据资源管理 + - 对数据资产进行有效的组织、管理,可以帮助数据专业人员收集、组织、访问和丰富元数据,以支持数据治理; +- 开发协同 + - 通过元数据管理,可以方便的进行数据调研成果的落地、共享,支撑数仓螺旋开发; + - 方便数据在企业内横向、纵向传递,这是团队协作的要求,也是项目经验的知识沉淀; +- 动态的数据字典,方便元数据查询、审计 + - 支撑各类数据报表、运营统计,为后续数据处理提供基础元数据查询。 + - 比如:统计接入了多少个系统、每个系统有多少张表,数据清洗需要知道每张表的字段信息。 + - 元数据还可以通过 OpenAPI 实现导出,快速支持保障合规审计等工作。 +- 确保数据的规范化、标准化,确保数据准确性、可用性; +- 数据生命周期管理; +- 最重要的意义就是数据资源的可管、可控、可查。 + +>元数据管理回答的问题: +> - 都有哪些数据? +> - 数据在哪里用? +> - 数据的业务定义是什么? +> - 这个数据有没有其他名称? +> - 这个数据与其他数据有什么关系? +> - 谁在用这个数据? +> - 为什么要使用这个数据? +> - 这个数据什么时候、被谁、做过什么样的修改? +> - 数据是否准确、可靠? + +# 2. 元数据管理架构 + +#### 2.1. 元数据管理平台架构 + +![]({{site.baseurl}}/img-post/元数据-2.png) + +- 接入层 + - 适配不同元数据生产方,转换成标准定义,输出全种类实体、关系变更消息。 +- 存储层(元数据库) + - 基于元模型的实体、关系的存储与查询,支持统计与分析能力。 +- 功能层 + - 提供元模型管理、元数据分析应用、元数据管理、元数据检核等功能。 +- 应用层 + - 基于定板元数据提供单点、复杂查询服务,基于分析引擎提供面向不同角色的元数据分析服务。 + +#### 2.2. 元数据管理架构的类型 + +- 集中式的架构 + - 指的是采集多种数据源的元数据到元数据自己的存储中来,再集中加工给其他场景提供服务; + +- 分散式的架构 + - 没有自己的元数据存储,而是在使用的时候,去即时的查询其他数据源的元数据。 + +- 集中式的架构 VS 分散式的架构 + - 集中式的架构 + - 优点:可以快速的检索元数据,抽取的时候,也可以自由的转换,自定义补充,提升了元数据的质量; + - 缺点:需要保证自身存储和其他源数据的一致性,增加了流程复杂度和工作量。 + + - 分散式架构 + - 优点:元数据总能够保持最新,查询更加的简单; + - 缺点:无法自定义或修改元数据项,查询也受源系统可用性的影响。 + + >一般我们推荐使用集中式架构,定时采集源系统的元数据,通过 hook 方式捕捉各种引擎运行时数据血缘关系,并且定义通用的数据模型提供给第三方接入使用。 + + +# 3. 元数据管理平台设计要点 + +#### 3.1. Hook 方式采集数据血缘 + +当前众多大数据组件都提供了 Hook 钩子的方式,相当于以插件的方式实时的捕捉执行计划。解析之后,推送到 Kafka,再去解析到分布式的图数据库中。 + +#### 3.2. 通用的数据源模块 + +一般公司肯定是存在多种不同类型的数据源的,比如 Mysql,Oracle,Hive 等,可以制作一个通用的模块,提供统一的接口,来对接这些不同的数据源。 + +数据源模块则提供三方接口供采集模块定时采集数据源的元数据信息。 + +核心的技术点,就是要隔离不同数据源的驱动,这些驱动也需要以插件化来集成到数据源模块中。 + +#### 3.3. 个性化的 SDK 接入 + +如果公司的核心业务部门比较多,公司的数据平台产品比较多,那么势必会产生一些其他的元数据。比如监控平台监控的资源使用情况、任务调度的任务运行情况等。 + +这种 SDK 接入,需要考虑接入时的安全校验,限流(可定时消费一批Kafka数据来实现)等问题。 + +#### 3.4. 后端存储的统一模型 + +元数据类型纷繁杂乱,需要统一整理抽象,再分类存储,并且设计之初,就要尽可能的详尽所有情况,设计出统一的表模型,预留扩展字段。 + +有一套模型是专门解决元数据模型通用性问题的,叫做 CWM (Common Warehouse Metamodel)标准,翻译过来是公共仓库元模型,这里提供了三层元模型来存储一切不同类型的元数据,当然设计起来比较复杂,一般超大型企业会采用这种模型。 + +如果可以详尽公司未来的元数据种类,可以分门别类建不同类型的元数据模型表来解决。 + +# 4. 元数据管理服务 + +#### 4.1. 元数据查询 + +#### 4.2. 元模型管理 + +- 元模型,一些是关系型的,一些是非关系型的,在概念层面它们都代表相同的实体,如:如数据集、数据表、数据字段、数据系统、应用程序、分类、业务术语表、数据血缘等。 +- 对于业务元数据,以系统的方式存储所有元数据而不是维护电子表格也非常有必要。 + +#### 4.3. 元数据维护 + +#### 4.4. 元数据版本管理 + +#### 4.5. 元数据对比分析 + +#### 4.6. 元数据适配器 + +#### 4.7. 元数据同步管理 + +#### 4.8. 元数据生命周期管理 + +# 5. 元数据分析服务 + +#### 5.1. 血缘分析 +- 血缘分析是一种技术手段,用于对数据处理过程的全面追踪,从而找到某个数据对象为起点的所有相关元数据对象以及这些元数据对象之间的关系。 + - 元数据对象之间的关系特指表示这些元数据对象的数据流输入输出关系。 +- 通过血缘分析,可以告诉开发者数据来自哪里,都经过了哪些加工。 +- 血缘分析的价值,在于当发现数据问题时、可以通过数据的血缘关系、追根溯源,快速地定位到问题数据的来源和加工过程,减少数据问题排查分析的时间和难度。 + - 这个功能常用于数据分析发现数据问题时,快速定位和找到数据问题的原因。 +- 在元数据管理系统成型后,我们便可以通过血缘分析来对数据仓库中的数据健康、数据分布、集中度、数据热度等进行分析。 + +- 数据血缘分析还有其它的积极意义,比如: + - 问题定位分析 + - 类似于影响分析,当程序运行出错时,可以方便找到问题的节点,并判断出问题的原因以及后续的影响 + - 指标波动分析 + - 当某个指标出现较大的波动时,可进行溯源分析,判断是由哪条数据发生变化所导致的 + - 数据体检 + - 判定系统和数据的健康情况,是否存在大量的冗余数据、无效数据、无来源数据、重复计算、系统资源浪费等问题 + - 数据评估 + - 通过血缘分析和元数据,可以从数据的集中度、分布、冗余度、数据热度、重要性等多角度进行评估分析,从而初步判断数据的价值 + +#### 5.2. 影响分析 + +- 影响分析,告诉开发者数据都去了哪里,经过了哪些加工。 +- 影响分析的价值,在于当发现数据问题时,可以通过数据的关联关系、向下追踪,快速找到都哪些应用或数据库使用了这个数据,从而避免或降低数据问题带来的更大的影响。 + - 这个功能常用于数据源的元数据变更对下游ETL、ODS、DW等应用应用的影响分析。 +- 在开发中,我们经常会遇到这个问题:如果我要改动某个表、ETL,会造成怎样的影响。 + - 如果没有元数据,那我们可能需要遍历所有的脚本、数据,才能得到想要的答案; + - 而如果有成熟的元数据管理,那我们就可以直接得到答案,节省大量时间。 + +#### 5.3. 冷热度分析 + +- 冷热度分析,是告诉开发者哪些数据是企业常用数据,哪些数据属于“僵尸数据”。、 +- 冷热分析的价值,在于让数据活跃程度可视化,让企业中的业务人员、管理人员都能够清晰的看到数据的活跃程度,以便更好的驾驭数据,激活或处置“僵死数据”,从而为实现数据的自助式分析提供支撑。 + +#### 5.4. 关联度分析 + +- 关联度分析,是告诉你数据和其他数据的关系、以及它们的关系是怎样建立的。 +- 关联度分析,是从某一实体关联的其它实体、和其参与的处理过程,两个角度来查看具体数据的使用情况,形成一张实体和所参与处理过程的网络,从而进一步了解该实体的重要程度。 + - 如:表与ETL 程序、表与分析应用、表与其他表的关联情况等。 +>本功能可以用来支撑需求变更的影响评估。 + +#### 5.5. 数据资产地图 + +- 数据资产地图,是告诉你有哪些数据,在哪里可以找到这些数据,能用这些数据干什么。 +- 通过元数据,可以对企业数据进行完整的梳理、采集和整合,从而形成企业完整的数据资产地图。 +- 数据资产地图,支持以拓扑图的形式进行、可视化展示各类元数据和数据处理过程,通过不同层次的图形展现粒度控制,满足业务上不同应用场景的数据查询和辅助分析需要。 +- 数据地图包括了数据资源的基本信息,存储位置信息、数据结构信息、各数据之间关系信息,数据和人之间的关系信息,数据使用情况信息等,使数据资源信息详细、统一、透明,降低“找数据”的沟通成本,为数据的使用和大数据挖掘提供支撑。 +- 通过元数据以企业全局视角对企业各业务域的数据资产进行盘点,实现企业数据资源的统一梳理和盘查,有助于发现分布在不同系统、位置或个人电脑的数据,让隐匿的数据显性化。 + +#### 5.6. ETL 自动化管理 + +- 在数仓中,很大一部分 ETL 都是枯燥重复的步骤。 + - 例如: + - 源系统-ODS 层的:表输入——表输出。 + - 又比如 ODS-DW:SQL 输入——数据清洗——数据处理——表输出。 + - 以上的规则其实就属于一部分元数据。 +- 那理论上完全可以实现,写好固定脚本,然后通过前端选择——或 api 接口。 +- 进而对重复的 ETL 实现自动化管理,降低 ETL 开发的时间成本。 + +#### 5.7. 数据安全管理 + +- 在数据中台,一切数据接口指标,都会从数据仓库中出口。 +- 因此理论上,只需在此处的元数据中对管理元数据的权限进行配置,即可实现全公司的数据安全管理。 + +# 6. 元数据采集服务 + +- 采集服务,需要能够适应异构环境: + - 支持从传统关系型数据库、大数据平台中采集; + - 支持从数据产生系统到数据加工处理、从系统到数据应用报表系统的全量元数据,包括过程中的数据实体(系统、库、表、字段的描述)以及数据实体加工处理过程中的逻辑。 +- 需要多种采集适配器: + - 支持多种存储格式的元数据自动获取,如:数据库、报表工具、ETL工具、文件系统等; + - 对于无法完成自动获取的元数据,需要可自定义的元数据采集模版,以完成元数据的批量导入。 + +# 7. 元数据访问服务 + +- 元数据访问服务是元数据管理软件提供的元数据访问的接口服务,一般支持REST或Webservice等接口协议。 +- 通过元数据访问服务支持企业元数据的共享,是企业数据治理的基础。 + +# 8. 元数据项目的实施步骤 + +#### 8.1. 划定元数据范围 + +首先确定元数据来源范围,在实际的工作中,不是所有数据都是要做元数据管理,通常我们会选择业务数据做元数据管理,非业务数据(例如:备份数据、系统日志等)是不会纳入管理范围内,主要还是因为元数据管理是提供业务和开发人员快速掌握业务数据。 + +确定规则后,就要结合公司的实际情况去梳理出哪些业务系统、哪些数据库、哪些数据库用户、哪些表需要做元数据管理。当然也可以支持非结构化数据的元数据抽取,例如:word、pdf等。 + +#### 8.2. 元数据标准 + +在梳理的过程中可能会出现有些数据库或者有些数据定义不规范的情况,导致元数据管理无法进行下去。那接下来需要建立元数据的管理规范,去反推前端的源数据进行整改,主要是保证元数据的完整性和一致性。 + +针对不同的类型的公司要求,元数据会开放给不同的人群,所以要对元数据进行权限管理,规范里面就需定义权限的管理流程:元数据的权限分层、元数据权限申请流程、元数据的发布流程、元数据的审核流程。 + +我的公司将元数据分为业务和技术两个管理属性,技术人员可以查看全域元数据,业务人员只能查看自己所对应业务流程的元数据,如要查看其他业务流程的元数据,需进行申请,申请流程要过元数据对应的业务和技术属主。 + + +#### 8.3. 元数据接入 + +元数据从哪接入,一般都是从源系统接入,假如公司已经存在数仓或者实时性要求不高,为了节约开发工作量,对于已有的元数据会从数仓接入,还未接入的会从源系统进行接入。 + +但这种方案也是存在风险,假如数仓的数据和源系统出现不一致,就会导致元数据出错。现在大部分的元数据抽取都是采用配置自动化的方式进行。 + + +#### 8.4. 元数据维护 + +元数据维护主要是对已经发布的元数据进行维护管理,已经发布上线的元数据,如需调整、优化则必须重新走元数据发布流程,不准许对元数据进行直接修改。为了安全,元数据所有操作行为都要记录到元数据操作日志里面。 + +可以对元数据创建目录将不同的元数据挂在对应的目录下,按照业务流程、业务主题域、开发流程设计对应的目录,主要还是根据公司要求设计。 + +#### 8.5. 元数据查找、分析、报告 + +有单独的页面支持元数据的模糊或精准快速查找,通过输入关键信息查找对应的元数据。我所在的公司将元数据作为数据资产的一类,因此我们需要产出元数据资产报告,从报告中能够快速的了解元数据访问热度、数据价值、数据成本、数据分布等相关信息。 + +分析这块上一篇文章就有提到,主要是血缘分析,做血缘分析的两种方法。血缘分析对做关联影响分析很重要,尤其是刚进来的开发或者业务不了解数据,通过血缘分析能够快速的定位、分析数据。 \ No newline at end of file diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\346\225\260\346\215\256\346\240\207\345\207\206\345\214\226\346\246\202\345\277\265\345\217\212\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\346\225\260\346\215\256\346\240\207\345\207\206\345\214\226\346\246\202\345\277\265\345\217\212\345\270\270\350\247\201\351\227\256\351\242\230.md" new file mode 100644 index 00000000000..715226a9d2d --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\346\225\260\346\215\256\346\240\207\345\207\206\345\214\226\346\246\202\345\277\265\345\217\212\345\270\270\350\247\201\351\227\256\351\242\230.md" @@ -0,0 +1,256 @@ +--- +layout: post +title: 数据治理:数据标准化概念及常见问题 +subtitle: +date: 2022-01-31 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据治理 +--- + + +# 1. 数据标准化 + +#### 1.1. 数据标准定义 + +- 数据标准,是一套由管理制度、管控流程、技术工具共同组成的体系,是通过这套体系的推广,应用统一的数据定义、数据分类、记录格式和转换、编码等实现数据的标准化。 +- 企业数据标准体系,一般是从数据域、数据分类、数据实体、数据属性四个层次构建。 + + +#### 1.2. 数据域 + +- 数据域是按业务领域划分的顶级数据分类; +- 例如:财务域、人力域、生产域、营销域等。 + +#### 1.3. 数据分类 + +- 数据分类是按照业务主题对数据域的细分; +- 例如:财务域又可以分为盈利能力主题、企业发展主题、资金管理主题、运营管控主题等。 + +#### 1.4. 数据实体 + +- 数据实体是每一类数据对象的个体,是数据标准化的主体; +- 需要规范数据实体的定义、标识、表示和允许值的数据单元。 + +#### 1.5. 数据属性 + +- 数据一般有业务属性、技术属性和管理属性组成,例如: + - 业务属性:数据项的业务定义、业务规则、质量规则; + - 技术属性:数据项的名称、编码、类型、长度等; + - 管理属性:数据的存储位置、管理部门、管理人员为该数据的。 +- 数据标准管理的过程,就是对数据以及数据的属性信息的标准化定义和应用的过程。 + + +# 2. 数据标准化要解决的问题 + +#### 2.1. 数据异构问题 + +- 数据存储结构不一致,调用多系统的数据时,由于某些数据在不同系统中数据存储结构不同,导致数据无法直接关联,影响不同系统之间的数据共享。 + +#### 2.2. 数据名称含义混淆问题 + +- 数据定义不一致,不同系统对数据的命名、业务含义、取值范围等定义不同; +- 比如: + - 同名不同义 + - 同义不同名 + +#### 2.3. 沟通成本问题 + +- 数据理解不一致,不同人员对数据的理解不一致,导致在数据使用时浪费很多时间来进行沟通。 + +#### 2.4. 数据来源不明问题 + +- 数据来源不一致,数据存在多个来源,在使用数据时,不清楚应该取哪个系统的数据; +- 比如: + - 名称问题: + - 同名不同源; + - 同源不同名; + - 含义问题: + - 同源不同义; + - 同义不同源; + +# 3. 数据标准化的价值 + +#### 3.1. 方便数据共享 + +- 数据标准的统一制定与管理,可保证数据定义和使用的一致性,促进企业级单一数据视图的形成,促进信息资源共享。 + +#### 3.2. 提升数据质量 + +- 通过评估已有系统标准建设情况,可及时发现现有系统标准问题,支撑系统改造,减少数据转换,促进系统集成,提高数据质量。 +- 没有标准化就没有信息化,那就更谈不上数据质量了。 +- 通过对数据标准的统一定义,明确数据的归口部门和责任主体,为企业的数据质量和数据安全提供了一个基础的保障。 + +#### 3.3. 保证数据规范化 + +- 数据标准可作为新建系统参考依据,为企业系统建设整体规划打好基础,减少系统建设工作量,保障新建系统完全符合标准。 +- 通过对数据实体、数据关系以及数据处理阶段,定义统一的标准、数据映射关系和数据质量规则,使得数据的质量校验有据可依、有规可循,为企业数据质量的提升和优化提供支持。 + +#### 3.4. 服务业务人员 + +- 对业务人员而言,数据标准建设可提升业务规范性,保障人员对数据业务含义理解一致,支撑业务数据分析、挖掘及信息共享。 +- 对技术人员而言,有数据标准作为支撑,可提升系统实施工作效率,保障系统建设符合规范,同时降低出错率,提升数据质量。 +- 对管理人员而言,数据标准建设可提供更加完整、准确的数据,更好的支撑经营决策、精细化管理。 + +# 4. 数据标准化的分类 + +#### 4.1. 数据结构视角 + +- 结构化数据标准: + - 针对结构化数据制定的标准; + - 通常包括:信息项分类、类型、长度、定义、值域等。 + +- 非结构化数据标准: + - 针对非结构化数据制定的标准; + - 通常包括:文件名称、格式、分辨率等。 + +#### 4.2. 数据内容来源视角 + +- 基础类数据标准: + - 指业务系统直接产生的明细数据和相关代码数据; + - 保障业务活动相关数据的一致性和准确性。 + +- 衍生类数据标准: + - 根据管理运营的需求、加工计算而派生出来的数据; + - 例如:统计指标、实体标签等。 + +#### 4.3. 技术业务视角 + +- 业务数据标准: + - 为实现业务沟通而制定的标准; + - 通常包括:业务定义和管理部门,业务主题等。 + +- 技术数据标准: + - 从信息技术的角度对数据标准的统一规范和定义; + - 通常包括:数据类型、字段长度、精度、数据格式等。 + +# 5. 数据标准化涉及的内容 + +- 从范围上看,数据标准包括数据元数据标准、主数据标准、数据指标标准和其他数据元标准, + +#### 5.1. 元数据标准化 + +>数据模型标准是元数据管理的主要内容,是企业数据治理的基础。 + +- 元数据是数据标准的基础,企业在制定数据标准的时候最先需要明确的就是数据业务属性、技术属性和管理属性,而这三类属性就是我们所说的业务元数据、技术元数据和管理元数据。 +- 基于元数据的数据标准管理,为业务实体的定义、关系和业务规则到IT实现之间提供清晰、标准的语义转换,提高业务和IT之间的一致性,保障IT系统能够真实反映业务事实。 +- 并为数据标准系统与其他业务系统的集成,提供有关数据标准、数据映射关系和数据规则的描述,为业务系统的集成提供支撑。 + +#### 5.2. 主数据(参考数据)标准化 + +- 主数据是用来描述企业核心业务实体的数据; + - 比如:客户、供应商、员工、产品、物料等; +- 主数据是具有高业务价值的、可以在企业内跨越各个业务部门被重复使用的数据,被誉为企业的“黄金数据”。 +- 参考数据是用于将其他数据进行分类或目录整编的数据,是规定数据元的域值范围。参照数据一般是有国标可以参照的,固定不变的,或者是用于企业内部数据分类的,基本固定不变的数据。 + - 例如:企业 SKU_CODE。 +- 企业在数据治理项目中: + - 有整体建设的,包含了:元数据、主数据、数据标准等领域; + - 也有分开建设的,例如:主数据项目单独立项,数据标准管理和数据仓库放在一起实施; +- 企业应根据自身的实际情况和需求,明确实施范围和内容,制定适合企业发展需要的数据治理路线图。 +- 主数据与参照数据的标准化是企业数据标准化的核心。 + +#### 5.3. 指标数据标准化 + +- 指标数据是在实体数据基础之上,增加了统计维度、计算方式、分析规则等信息加工后的数据。 +- 指标数据标准是对企业业务指标所涉及的指标项的统一定义和管理。 +- 企业的财务、销售、采购、生产、质量、售后等各业务域均分布都有其相应的业务指标。 +- 这些指标不仅需要在业务系统中统计和展现还需要在数据分析系统中展现,有的指标数据需要多个从不同的业务系统中进行获取。 + +# 6. 数据标准化覆盖的范围 + +#### 6.1. 组织范围 + +- 数据标准适用的组织范围; +- 例如:部门级、公司级,集团级还是行业级。 + +#### 6.2. 业务应用范围 + +- 数据标准都哪些业务部门会使用; +- 例如:一个“客户”数据标准,就会被市场、销售、生产、采购、仓储、物流、售后等多个部门使用。 + +#### 6.3. 落地系统范围 + +- 该标准需要在哪些系统中贯彻执行; +- 例如:我们上边举的“客户”数据标准,落地系统范围可能包括ERP、CRM、WMS等。 + + +# 7. 数据属性标准化 + +#### 7.1. 业务属性定义 + +- 业务属性,定义数据与企业业务相关联的特性和用途,统一业务描述和理解; + - 包括:命名规则、编码规则、业务定义、业务规则、值集、维度、粒度等。 +- 通过对实体数据的标准化定义,解决数据不一致、不完整、不准确等问题,消除数据的二义性,使得数据在企业有一个全局的定义,减少了各部门、各系统的沟通成本,提升企业业务处理的效率; +- 标准统一的数据指标体系,让业务人员也能够轻松获取数据,并能够自助式的进行数据分析,为基于数据的业务创新提供可能。 + +#### 7.2. 技术属性定义 + +- 技术属性,定义数据与IT技术实现相关联的特性,对IT实施形成必要的指引和约束; + - 包括:字段名称、数据类型、数据格式、数据长度、度量单位、枚举值的限定等。 +- 统一、标准的数据及数据结构是企业信息共享的基础; +- 标准的数据模型和标准数据元为新建系统提供支撑,提升应用系统的开发实施效率; +- 数据标准化清晰定义数据质量规则、数据的来源和去向、校验规则,提升数据质量。 + +#### 7.3. 管理属性定义 + +- 管理属性,定义数据标准在管理和使用方面各部门承担的责任,对数据归属进行确权认知,明确数据所属部门、数据管理部门、数据使用部门、标准发布日期等管理属性做出规范; +- 通过数据的标准化定义,明确数据的责任主体,为数据安全、数据质量提供保障; +- 统一、标准的数据指标体系为各主题的数据分析提供支持,提升数据处理和分析效率,提供业务指标的事前提示、事中预警、事后提醒,实现数据驱动管理,让领导能够第一时间获取决策信息。 + + +# 8. 数据标准化设计 + +#### 8.1. 标准设计的定义 + +- 数据标准设计,是对数据标准的主题、信息大类、信息小类、信息项、数据类型、数据长度、数据定义、数据规则等进行规划设计。 +- 在方法论指导下,完成数据标准设计和定义工作,包括数据业务描述定义(业务属性)、类型长度定义(技术属性)、其他标准信息定义。 + +#### 8.2. 业务属性规范 + +- 主题 +- 分类 + - 大类 + - 小类 +- 名称 +- 业务含义 +- 数据来源 +- (指标)计算公式 +- 应用场景 + +#### 8.3. 技术属性规范 + +- 类型 +- 长度 +- 域值范围 +- 编码 +- 数据源表 +- 质量规则 +- (指标)统计周期 +- (指标)统计维度 +- (指标)计算精度 + +#### 8.4. 管理属性规范 + +- 归口部门 +- 维护部门 +- 安全级别 +- 接口人 +- 生成数据的业务系统 +- 等 + +# 9. 数据标准实施 + +#### 9.1 数据映射 + +- 明确需要映射内容的系统范围、应用领域、数据库表、数据字典、数据字段等。 +- 将已定义的数据标准与业务系统、业务应用进行映射,表明标准和现状的关系以及可能影响到的应用。 + +#### 9.2. 规范业务流程 + +- 数据治理过程中,有一个比较常见的问题:企业花费了大量精力建立起来的数据标准,在实际业务中没有很难使用起来。 +- 当然,造成这个问题的因素有很多,例如:标准本身制定的不合理,历史系统的改造难度大,标准没有得到广泛的普及等等。 +- 在众多的因素中,有一个因素我们不能忽略,那就是“业务流程的优化”,只有将数据标准与业务流程进行深度融合,融为一体的时候,才是真正意义上实现了数据标准的落地。 +- 不能只关注数据而忽视了业务流程,事实上数据标准和流程优化,相互依存、互为支撑,没有规范化的业务流程,就不会有标准化的数据。 + diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\346\225\260\346\215\256\346\262\273\347\220\206\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\346\225\260\346\215\256\346\262\273\347\220\206\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..39a27e2ad43 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\346\225\260\346\215\256\346\262\273\347\220\206\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,142 @@ +--- +layout: post +title: 数据治理:数据治理的概念详解 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据治理 +--- + +# 1. 什么是数据治理 + +数据治理(Data Governance)是组织中涉及数据使用的一整套管理行为。由企业数据治理部门发起并推行,关于如何制定和实施针对整个企业内部数据的商业应用和技术管理的一系列政策和流程。 + +- 数据治理是一套管理机制,技术工具只是实现这套机制的载体。 + +- 数据架构组织; +- 数据模型; +- 制度规范; +- 技术工具; +- 数据标准; +- 数据质量; +- 影响度分析; +- 作业流程; +- 监督考核; + +# 2. 为什么需要数据治理 + +- 表数量越来越多; +- 历史数据庞大、复杂; + - 表关系不清晰; + - 相似的表越来越多; + - 数据模型扩展性差,新增一个业务就要建新表; +- 数据处理过程,越来越复杂; +- 缺乏数据标准; + - 表字段命名随意,定义混乱; + - 同一个英文字段对应多个中文名; + - 同一个中文名对应多个英文字段; + - 同一个业务元素,对应多重数据类型、长度; +- 数据质量差 + - 无法规避错误数据出现; + - 出现错误后发现不及时; + - 数据处理流程没有检测机制,导致更多连锁性错误; +- 数据资产分散,共享性差; + - DB、数据模型、应用程序、数据标准、数据质量信息分散管理; + - 企业数据资产无法集中管理和展现,数据共享性差; +- 管理体系不完善,落地实施差; + - 管理角色分工不清晰; + - 管理工具易用性差; + - 管理和运维操作复杂; +- 表结构变更、系统改造时,对应用的影响无法评估; + +# 3. 数据治理包含哪些内容 + +- 元数据管理 + - 血缘关系 + - 影响分析 + +- 主数据管理 + +- 数据统一存储 + - 数据库管理 + - 数据地图 + +- 主数据管理 + - 核心是人、财、物; + - 主数据是企业最关系的、最核心的数据,对数据准确性要求极高; + - 在系统之间的关系图中,主数据通常处于正中位置,外围系统通过主数据进行共享和交互; + +- 数据集成管理 + +- 数据标准管理 + - 数据标准化; + - 单词; + - 域; + - 用语; + - 标准编码; + - 数据字典 + - 标准用语 + - 命名规则 + - 流程标准化; + +- 数据质量管理 + - 完整性 + - 关注数据不丢失、数据可用; + - 规范性 + - 统一存储格式; + - 一致性 + - 数据的值在信息含义上不能有冲突; + - 准确性 + - 业务规则,度量哪些数据和信息是不正确的,或者超预期; + - 唯一性 + - 度量哪些数据是重复的、或者数据的那些属性是重复的; + - 关联性 + - 度量哪些关联的数据确实、或未建立索引; + +- 数据资产管理 +- 数据开放共享 +- 监控与报告 +- 数据权限 & 安全 + +# 4. 数据治理涉及的范畴 + +#### 4.1. 组织架构 + +- 从理论和国外实践来看,大型企业会建立企业级数据治理委员会,业务部门领导、IT部门领导共同参与。 +- 在企业级之下,还可以有部门级、项目级的委员会,负责某些局部的数据治理; +- 在最基层面向某一个业务领域,应该有相应的数据管理专员(Data Steward)。 + +#### 4.2. 数据架构 + +- 数据架构,包括了数据模型(概念模型、逻辑模型)以及数据的流转关系; +- 一般在企业级和系统级会谈数据架构,主要对企业数据的分类、分布和流转进行规划、设计; +- 目的是确保新建系统、新建应用能够与现有系统保持一致和融合,避免产生信息孤岛,或者带来重复不必要的数据集成、数据转换。 + +#### 4.3. 数据标准 + +- 数据标准,包括了数据项、参考数据、指标等不同形式的标准; +- 举例来说,“客户类型”是一个数据项,应该有统一的业务含义,将客户归类为大客户、一般客户的规则是什么,数据项的取值是几位长度,有哪些有效值(如 01,02,03)等。 + +#### 4.4. 数据质量 + +- 数据质量,包括数据质量规则以及稽核模型(即规则的组合应用); +- 准确性: + - 数据不正确或描述对象过期 +- 合规性: + - 数据是否以非标准格式存储 +- 完备性: + - 数据不存在 +- 及时性: + - 关键数据是否能够及时传递到目标位置 +- 一致性: + - 数据冲突 +- 重复性: + - 记录了重复数据 + +#### 4.5. 治理工具 + +- 数据中台 / 数据管理平台 + diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\346\225\260\346\215\256\350\264\250\351\207\217\347\256\241\347\220\206.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\346\225\260\346\215\256\350\264\250\351\207\217\347\256\241\347\220\206.md" new file mode 100644 index 00000000000..9b03ce1a90a --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\346\262\273\347\220\206\357\274\232\346\225\260\346\215\256\350\264\250\351\207\217\347\256\241\347\220\206.md" @@ -0,0 +1,107 @@ +--- +layout: post +title: 数据治理:数据质量管理 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据治理 +--- + +# 数据质量指标 + +- 准确性: + - 数据不正确或描述对象过期 +- 合规性: + - 数据是否以非标准格式存储 +- 完备性: + - 数据不存在 +- 及时性: + - 关键数据是否能够及时传递到目标位置 +- 一致性: + - 数据冲突 +- 重复性: + - 记录了重复数据 + +# 质量监控对象 + +- 离线报表 +- 准实时报表 +- 实时大盘 +- 数据服务推送 +- 算法依赖 +- 数据分析依赖 +- 其他(ftp file推送、olap下载,业务库数据推送) + +# 数据质量关注的问题 + +#### 缺省值分析 + +- 产生原因: + - 有些信息暂时无法获取,或者获取信息的代价太大 + - 有些信息是被遗漏的,人为或者信息采集机器故障 + - 属性值不存在,比如一个未婚者配偶的姓名、一个儿童的固定收入 +- 影响: + - 会丢失大量的有用信息 + - 数据额挖掘模型表现出的不确定性更加显著,模型中蕴含的规律更加难以把握 + - 包含空值的数据回事建模过程陷入混乱,导致不可靠输出 +- 解决办法: + - 通过简单的统计分析,可以得到含有缺失值的属性个数,以及每个属性的未缺失数、缺失数和缺失率。删除含有缺失值的记录、对可能值进行插补和不处理三种情况。 + +#### 异常值分析 + +- 产生原因: + - 业务系统检查不充分,导致异常数据输入数据库 +- 影响: + - 不对异常值进行处理会导致整个分析过程的结果出现很大偏差 +- 解决办法: + - 可以先对变量做一个描述性统计,进而查看哪些数据是不合理的。最常用的统计量是最大值和最小值,用力啊判断这个变量是否超出了合理的范围。 + - 如果数据是符合正态分布,在原则下,异常值被定义为一组测定值中与平均值的偏差超过3倍标准差的值,如果不符合正态分布,也可以用原理平均值的多少倍标准差来描述。 + +#### 不一致值分析 +- 产生原因: + - 不一致的数据产生主要发生在数据集成过程中,这可能是由于被挖掘的数据是来自不同的数据源、对于重复性存放的数据未能进行一致性更新造成。 + - 例如,两张表中都存储了用户的电话号码,但在用户的号码发生改变时只更新了一张表中的数据,那么两张表中就有了不一致的数据。 +- 影响: + - 直接对不一致的数据进行数据挖掘,可能会产生与实际相悖的数据挖掘结果。 +- 解决办法: + - 注意数据抽取的规则,对于业务系统数据变动的控制应该保证数据仓库中数据抽取最新数据 + +#### 重复数据及特殊数据 + +- 产生原因: + - 业务系统中未进行检查,用户在录入数据时多次保存。或者因为年度数据清理导致。特殊字符主要在输入时携带进入数据库系统。 +- 影响: + - 统计结果不准确,造成数据仓库中无法统计数据 +- 解决办法: + - 在ETL过程中过滤这一部分数据,特殊数据进行数据转换 + +# 数据质量监控规则 + +![]({{site.baseurl}}/img-post/数据质量管理-2.png) + +- 质量监控规则示例 + - 易于漏数据的,做记录条数多少验证或记录数波动大小告警; + - 稳定的枚举值个数、内容做完整性、包含、不包含等具体枚举值验证; + - 门店清单完整性比较,如不按时日结完成,打电话告警门店负责人及全集图曝晒; + - 维度表剔除重复处理; + - 上游系统 IP 不通配置上游业务系统负责人及 dba 告警电话; + - 关键字段非空告警监控。 + +- 维度表 & 事实表 监控规则示例 + + ![]({{site.baseurl}}/img-post/数据质量管理-3.png) + +# 数据治理常规检查项 + +![]({{site.baseurl}}/img-post/数据质量管理-1.png) + + +# 数据异常告警 + +- 任务失败电话告警 +- 任务延迟启动电话告警 +- 任务延迟完成电话告警 + diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\346\271\226\357\274\232\346\225\260\346\215\256\346\271\226\347\232\204\346\200\247\350\203\275\350\246\201\346\261\202\345\222\214\350\256\276\350\256\241\345\216\237\345\210\231.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\346\271\226\357\274\232\346\225\260\346\215\256\346\271\226\347\232\204\346\200\247\350\203\275\350\246\201\346\261\202\345\222\214\350\256\276\350\256\241\345\216\237\345\210\231.md" new file mode 100644 index 00000000000..6e55850eed9 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\346\271\226\357\274\232\346\225\260\346\215\256\346\271\226\347\232\204\346\200\247\350\203\275\350\246\201\346\261\202\345\222\214\350\256\276\350\256\241\345\216\237\345\210\231.md" @@ -0,0 +1,125 @@ +--- +layout: post +title: 数据湖:数据湖的性能要求和设计原则 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据湖 +--- + +# 1. 数据湖的性能要求 + +#### 1.1. 安全 + +- 数据集中存储,要求有严格的权限管控。 + +#### 1.2. 可扩展 + +- 数据湖应随需求扩展的能力。 + +#### 1.3. 吞吐量 + +- 较高的数据吞吐量。 + +#### 1.4. 可靠性 + +- 必须稳定可靠。 + +#### 1.5. 原始格式存储 + +- 数据必须是原始数据,不能有任何修饰、加工。 + +#### 1.6. 支持多数据源、异构数据写入 + +- 不限制数据类型。 + +#### 1.7. 支持多种处理和分析框架 + +- 支持的数据处理和分析架构应该是多样的。 + +# 2. 数据湖的设计原则 + +#### 2.1. 数据和业务分离 + +- 数据湖的架构设计,不考虑业务,只考虑数据。 + +>业务适配,是数仓应该考虑的,而不是数据湖。 + +#### 2.2. 存储和计算分离(比较适合云平台) + +- Hadoop 中计算的扩容受到存储制约: + - 一般来说,计算容量不够的时候,需要对计算进行扩容; + - Hadoop 中,计算和存储是一起的,DataNode 和 计算节点是复用的,这样计算节点可以直接访问本地的数据; + - 但是这样就会导致一个问题,计算扩容的时候、存储也跟着扩容,存储的 rebalance 会造成存储的资源消耗。 + - 总结就是,计算的扩容收到存储制约,无法灵活的扩容和缩容。 + +- 存储和计算分离对网络要求很高: + - 存储和计算分离,数据是走网络的,这就对网络有很高的要求; + - 集群内网交换机的传输速度要很快才行,不然就会制约性能; + +- 云平台天生支持存储和计算分离 + - AWS 使用 S3 作为存储,计算是分离的; + - Azure 使用 blob 存储,计算和 blob 无关; + - 阿里云使用 OSS 存储,和计算也没有关系; + - 在云平台上,计算和存储天生就是分离的。 + +#### 2.3. 支持多种数据处理架构 + +- lambda 架构; +- Kappa 架构; +- IOTA 架构; + +#### 2.4. 管理服务以及工具使用要求 + +- 数据安全管理; +- 访问权限管理; +- ETL 数据汇入; +- 合适的批处理、流处理工具; +- 前端工具,包括 BI、API 等; + +# 3. Lambda 架构 + +#### 3.1. Lambda 架构概念 + +- 分为离线处理和实时处理两种路径; +- 离线处理和实时处理,都会产生中间数据,离线结果根据执行间隔不停的执更新,实时结果不断的用新数据迭代; +- 离线数据和实时数据合并以后,抽取到数据库、缓存系统中,作为服务供用户查询; + +![]({{site.baseurl}}/img-post/data-lake-3.jpg) + +#### 3.2. Batch Layer + +- MR、Spark、SparkSQL 等计算引擎; + +#### 3.3. Speed Layer + +- Storm、Flink、Spark Streaming; + +#### 3.4. Serving Layer + +- MySQL、Redis、HBASE 等缓存数据库货缓存系统,将两个 view 的合并结果导入供查询; + +#### 3.5. Lambda 的优点 + +- 对于需要全量数据才能计算的结果,90% 的数据计算已经由离线完成,剩下的 10% 是实时计算结果,节省了计算的资源消耗、提升了计算响应速度; +- 实时计算部分成本可控; +- 批处理部分可以利用晚上等空闲时间进行,这样就会把实时计算和离线处理的高峰错开; + +#### 3.6. Lambda 的缺陷 + +- 实时和批处理结果不一致问题: + - 实际生产过程中,多种原因会导致实时处理和批处理时间对不上问题,导致衔接处出现数据遗漏或者数据重复,造成结果不准确; + >时间对不上的原因包括: + > + > todo + + +- 批量计算无法在限定时间内处理完问题: + - 随着数据了越来越多,计算资源如果跟不上就会出现数据处理不完的问题; +- 系统维护问题: + - 实时处理和批处理是两套代码,维护起来会比较麻烦; +- 服务器存储开销大: + - 计算出来的中间数据用于支撑业务,会加大存储压力,不过这个情况渐渐不太重要了。 \ No newline at end of file diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\346\271\226\357\274\232\346\225\260\346\215\256\346\271\226\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\346\271\226\357\274\232\346\225\260\346\215\256\346\271\226\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..ad6567ea069 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\346\271\226\357\274\232\346\225\260\346\215\256\346\271\226\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,154 @@ +--- +layout: post +title: 数据湖:数据湖的概念详解 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据湖 +--- + + +# 1. 企业对数据湖的需求 + +#### 1.1. 过往的数据管理 + +- 数据库:数据的存储和查询; +- 数仓:数据的集中存储和分析; +- 消息队列:数据的转移通道; +- 流式计算:高效的数据加工和分析; +- 缓存系统:数据的快速加载。 + +#### 1.2. 数仓主要解决数据孤岛问题 + +- 数据孤岛,不同业务、不同部门、不同来源、不同标准的数据,分散在各个地方; +- 数仓通过ETL、数据管道等,将分散的数据统一存储到数仓中,可以部分解决数据孤岛问题; + +#### 1.3. 数仓无法适应快速增长的非结构化数据分析需求 + +- 数据增长速度太快,导致数仓逐渐力不从心; + - 数仓建模是非常严格的,次开发数仓应用流程都很长,无法适应数据快速分析、快速处理的要求; +- 非结构化数据的价值越来越高,但是数仓并不适合分析非结构化数据; + - 数仓建模是非常严禁的,更适合处理结构化数据; + +>数仓的开发,是以需求为导向的,一旦设计好以后就不好在修改。 + +#### 1.4. 企业需要保留海量的原始数据 + +- 过往企业存储数据,大都是存储的精简数据; +- 但是随着企业发展和技术进步,越来越多的企业需要存储海量的原始数据; + +#### 1.5. 企业需求的总结 + +- 数据的集中存储,成本可控、维护简单; +- 可以存储任意格式的数据(半结构化、非结构化); +- 能够支持大多数的分析框架; + +# 2. 数据湖的概念理解 + +> 大数据技术的发展,是数据湖能够落地的前提下。 + +#### 2.1. 数据湖包含的功能 + +- 大规模存储: + - 集中存储; + - 保留原始数据; + - 支持任意格式; +- 支持海量数据分析; + +#### 2.2. 数据湖的定义 + +**一种支持任意格式数据、并能保留原始数据内容的、大规模数据存储系统架构,并且支持海量数据的分析处理。** + +#### 2.3. 数据湖的特点 + +- 不限数据格式,均可流入; +- 集中存储,任意访问; +- 高性能分析能力; +- 存储原始数据; + +# 3. 数据湖架构图 + +![]({{site.baseurl}}/img-post/data-lake-2.png) + +# 4. 写时模式 & 读时模式 + +#### 5.1. 写时模式 + +- 数据在写入之前,就需要定义好 schema,按照 schema 定义写入; +- 数据库、数仓、数据集市,都是写时模式,需要先建表、再写入数据; +- 写时模式下,数据写入后修改 schema 成本是很高的。 + +#### 4.2. 读时模式 + +- 数据在写入的时候,不要定义 schema,在需要的时候使用 schema 定义它; +- 读时模式下,数据模型定义很灵活; +- 同一套数据可以根据业务、使用不同的 schema,以满足不同的上层业务的高效分析需求; + +# 5. 数据湖 VS 数仓 VS 数据集市 + +![]({{site.baseurl}}/img-post/data-lake-1.png) + +- 数据格式 + - 数仓:结构化数据; + - 数据湖:任何格式数据均可。 + +- 模式区别 + - 数仓:写时模式; + - 数据湖:读时模式; + - 典型例子如 SparksSQL。 + +- 使用思维 + - 数仓:先有需求、再准备数据; + - 先有报表需求,然后设计 schema,之后 ETL 导入数据; + - 数据湖:先有数据,再按需利用; + - 数据先存储,需要的时候再设计 schema,再根据业务使用数据; + +# 6. 数据湖的优势 + +#### 6.1. 读时模式:可以更轻松的收集数据 + +- 不需要提前设计 schema; +- 对数据写入没有限制,数据收集很方便。 + +#### 6.2. 异构存储:不需要关心数据结构 + +- 存储数据无限制; +- 任意格式都能存储。 + +#### 6.3. 集中存储:集中存储方便任意访问 + +- 多个业务单元的人员,可以使用全部的数据; +- 省去了数据的聚合汇总。 + +#### 6.4. 分析能力:能发现更多而数据价值 + +- 数仓:只能存储精简数据,而不是原始数据,这样会损失原始数据中的大量信息,只能回答定义好的问题,无法发掘更多数据价值。 +- 数据湖:允许使用各种工具,对数据进行多样化、多维度的分析,从而容发掘更多的数据价值。 + +#### 6.5. 更好的扩展性和开发敏捷性 + +- 数据湖使用分布式文件系统存储数据,具有很高的扩展能力; +- 开源技术降低了存储成本; +- 数据湖的数据结构不严格,具有很高的灵活性,从而有助于敏捷开发。 + +# 7. 数据湖的实施方案 + +> 数据湖是一种系统架构设计思想,既不是数据库、也不是技术框架。 + +#### 7.1. 基于 Hadoop 的数据湖实施方案 + +- 常见的企业应用: + - 使用 HDFS 作为存储层,存储各类原始数据,包括架构华、非结构化、半结构化的数据; + - 使用 Spark、SparkSQL、MR 等计算框架作为分析引擎,对原始数据进行分析、抽取、计算、利用; + - 使用 Flume、Storm 等实时分析 HDFS 的数据; + +#### 7.2. 商业产品 + +- AWS S3 +- Zaloni + + + diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\351\233\206\345\270\202\357\274\232\345\256\275\350\241\250\350\256\276\350\256\241\346\263\250\346\204\217\344\272\213\351\241\271.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\351\233\206\345\270\202\357\274\232\345\256\275\350\241\250\350\256\276\350\256\241\346\263\250\346\204\217\344\272\213\351\241\271.md" new file mode 100644 index 00000000000..b79512647f8 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\351\233\206\345\270\202\357\274\232\345\256\275\350\241\250\350\256\276\350\256\241\346\263\250\346\204\217\344\272\213\351\241\271.md" @@ -0,0 +1,91 @@ +--- +layout: post +title: 数据集市:宽表设计注意事项 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 数据集市 +--- + + +# 1. 宽表设计原理 + +- 宽表设计其实体现的是架构设计的思想,即高内聚松耦合,只不过发生在数据领域而已。 +- 之所以要建设宽表,更多地是要利用大数据强大的计算能力,避免范式建模带来过多的关联操作,可以实现计算效率的高度并行化。 + - 在大数据数仓中更多地是使用星型模型构建hive表,通过大量的冗余来提升查询效率,同时OLAP的数据计算引擎也能更好地支持星型模型的开发。 + +- 因为是把许多的字段内容集中存储在同一张表里,宽表很明显已经不符合范式模型设计的规范,并且在目前的数仓体系中,也早已抛弃了范式建模思路,带来的好处是高效与便捷的查询,当然也带来了宽表的脆弱性,如果有太多的底层表与宽表产生联系,同时又有许多上层应用被宽表所绑架,那么宽表的出错就会是数据体系的致命一环,不得不面临拆分的下场。 + +- 宽表带来便利的同时也带来了脆弱性。底层每张表都跟宽表的生成有千丝万缕的逻辑关系。这意味着宽表出错、延时的概率大幅增加。这个时候,大量具体应用都被宽表落库的时间、质量和数量绑架了,即使它仍然遵循高内聚松耦合的特点,也不得不面临拆分的下场。 + +# 2. 宽表模型优点 + +- 建模方法论中维度模型非强范式,就可以更好的利用大数据处理框架的并行处理能力,避免单一范式操作的过多线性操作,可以实现高度的并行化。 +- 数据仓库大多数时候,比较适用星型模型,构建底层数据Hive表,通过大量的冗余来提升查询效率。星型模型对OLAP的分析引擎支持比较友好,这一点在Kylin、Guess中比较能体现。 + +#### 2.1 更好的发挥大数据框架的能力 + +- 宽表可以更好地利用大数据框架,宽表用数据数据冗余避免很多的“额外”关联、节点服务器、集群之间的通讯资源、负载均衡软硬请求。 + +#### 2.2 提高开发效率 + +一般情况下,宽表包含了很多相关的数据。如果在宽表的基础上做一些业务场景的数据应用开发,就很方便。可以直接从宽表里面取数据,避免每次从ODS层开始,从头计算。 + +#### 2.3 提高数据质量 + +- 宽表的准确性,一般都经历了业务管理系统实践、时间检验,逻辑错误的可能性很小,可以直接使用。如果是从头开发,在这个过程中,有极大可能因为对业务理解不透彻,或者书写的逻辑不正确,导致结果数据驴头不对马嘴的交付问题。 + +#### 2.4 统一指标口径 + +- 如果报表都能从底层宽表出,那么报表指标肯定是一样的。在数仓建设过程中,经常会遇到同一个指标口径不一致,导致抽数据在不同的出口不一样,这是业务部门经常诟病的一个数据口径不一致问题。 + +#### 3. 宽表模型的不足 + +#### 3.1 数据冗余较多 + +由于把不同的内容都放在同一张表存储,宽表已经不符合三范式模型设计规范,随之带来的主要坏处就是数据的大量、重复冗余和成本投入。 + +#### 3.2 性能不高 + +由于宽表计算逻辑往往很复杂,再加上宽表数据输入有大量依赖。需要处理的数据量很大,在负载逻辑+数据存储量大的情景下,导致宽表往往运行效率不高,资源常态占用很高,尤其是在数据重跑的时候。 + +#### 3.3 稳定性不高 + +- 一般一个系统的稳定性取决于最差的一个环节或部件,遵循木桶理论。 +- 宽表的稳定性很差,主要是因为宽表依赖“源表”、“中间表”太多,每一个表的不稳定性都会传到宽表。 + +#### 3.3 开发难度大/维护成本高 + +- 宽表的可用性与业务的变化速度成反比,宽表越复杂,越没人愿意去优化那些可能带来极大业务风险的宽表。因为业务本身逻辑繁多,异源异构的库表字段映射的逻辑很复杂,对于不懂业务的数据工程师来说,数据平台设计挑战巨大。并且,由于业务逻辑的随时变更,也需要随时响应跟进、持续维护、映射字段之间复杂的物理逻辑。 +- 业内的普遍做法是基于宽表做顶层的报表设计,但是宽表也是由底层的开发人员开发,本身的逻辑也很复杂,所以一旦涉及到业务逻辑的变更也就是要去维护复杂的逻辑,宽表的内部sql逻辑可能涉及到几千行的代码,可能是由数个开发人员协调完成,人员的变动和离职都以为着维护成本的加大,甚至上层业务逻辑的改变,也会影响到最后的数据质量,这时候我们只能祈祷完备的代码注释能够帮助我们第一时间发现问题。 + +# 4. 宽表模型设计原则 + +>核心原则:宽表不应该包含不属于它所在域的数据。 + +#### 4.1 主次分离 +- 主次分离式的拆分,可以确保聚焦表主题域,确保单一主题域不随着项目推进而产生走形变样。 +- 对于数仓开发人员而言,可以简单、可靠、敏捷的智能维护、对于终端用户,也可以更加清楚的理解这张宽表的业务观点和视角。 +- 例如: + - 做一张会员域下会员基本信息宽表,专注的肯定就是基本信息,作为主数据管理,将不同应用调用的会员信息做强一致性打通。 + +- 当然,事实宽表可能还会冗余其他信息进来,当这样的信息越来越多的时候,这张表的业务域、数据域边界就越来越弱化,所以需要继续拆分。 + +#### 4.2 冷热分离 + +- 假设有一张宽表,里面有200个字段,有30张报表在使用它,但是发现前面150个经常字段经常被使用,后面 50个字段只有一两张报表偶尔使用到。那么就可以做一个冷热分离,将宽表拆分。 + +#### 4.3 稳定与不稳定分离 + +- 上面说的主次分离、冷热分离都可以提高稳定性,但并不是为了稳定性而分离的。 +- 例如: + - 经常有这样的宽表,例如对金融客户在移动终端设备上的行为分析,通常,它依赖埋点数据。 +但是埋点数据的特点就是高并发、高通量,单点业务价值密度低。量大导致“全量遍历”计算经常延迟。那么,宽表落库的及时性就会受影响,从而导致报表出表时间点设计受影响。但是,很多时候发现要出的报表,根本没有用过埋点计算出来的指标,或者是只用了一点点。 + - 这时候,可以将其拆分,如果没有用到最好,如果用到了,那就后推,在报表主外键层面上做逻辑关联,这样,我们的埋点数据即使出不来,我们的报表数据、模型算法实现结果还可以看到。这也是一种用冗余确保稳定性的设计。 + + + + diff --git "a/_posts/2022-01-27-\346\225\260\346\215\256\351\233\206\345\270\202\357\274\232\346\225\260\346\215\256\351\233\206\345\270\202\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-27-\346\225\260\346\215\256\351\233\206\345\270\202\357\274\232\346\225\260\346\215\256\351\233\206\345\270\202\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..ecce56ecd50 --- /dev/null +++ "b/_posts/2022-01-27-\346\225\260\346\215\256\351\233\206\345\270\202\357\274\232\346\225\260\346\215\256\351\233\206\345\270\202\347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,134 @@ +--- +layout: post +title: 数据集市:数据集市的概念详解 +subtitle: +date: 2022-01-27 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 数据集市 +--- + + +# 1. 数据集市概念 + +- 数据集市(Data Mart) ,也叫数据市场,数据集市就是满足特定的部门或者用户的需求,按照多维的方式进行存储,包括定义维度、需要计算的指标、维度的层次等,生成面向决策分析需求的数据立方体。 + +- 数据集市主要面向部门级业务,并且只面向某个特定的主题,存储为特定用户预先计算好的数据,从而满足用户对性能的需求。主要特征是规模小、面向部门,由业务部门定义、设计和开发,业务部门管理和维护,能快速实现需求。 + +- 数据集市本质上是一个数据交易市场,既然是市场就存在供需,需求即终端用户对数据的直接需求,供应即满足用户需求特点的数据立方体。集市数据来源是全企业范围的数据库、数据仓库中抽取出来的体系化数据。 + +- 数据集市低延时和高并发查询的情况下具备足够的支撑能力,可以7×24对外提供数据服务,且不影响原有数据仓库统计分析应用的正常运行。大量生产数据的预处理在数仓进行,数据集市接收数仓预处理后的数据。 + +- 数据集市是对数仓的补充,灵活、快速响应业务,支持用户部门自行定制数据进行统计分析,支持高并发、性价比较高。 + +# 2. 数据集市的价值 + +#### 2.1. 为什么要建立数据集市 + + - 因为我们需要创建一个静态副本(副本中含有几个事实表及必要的维度),不像仓库那样每天都在变化,方便于分析。集市中会有很多副本只是暂时性应用的。用户在一个持续时间不长的项目中观测集市中的数据,比如几天到几个星期。之后,在项目结束后,集市中的数据就可以被删除。 + - 因为需要一个集市进行数据挖掘。在数仓中处理数据挖掘模型(训练、预测分析)会产生很繁重的处理压力,我们不希望这类工作影响到中央/核心仓库的性能。 + - 减轻数仓的查询工作量。假设有50%的数据报告都需要查询某个特定的事实表(以及它常用的必要维度)。为了减轻仓库的负担,我们可以针对这个事实表及使用维度创建一份副本,将副本指向某些数据报表。并且保持这个集市数据每天定时更新,达到减轻数仓的即席查询压力的目的。 + - 有些业务想改变一部分数据来模拟一些业务场景。这个时候他们不能改变核心数据仓库中的底层原始数据(这样会影响到所有人),所以我们可以为他们提供一个数据集市,满足这类人群的的特殊需求,去帮助他们应用他们的分析场景。 + - 提高报表的查询性能,集市中的数据相比于数仓的结构不同,索引不同。集市中会有许多额外生成的表(汇总表、高粒度聚合表)。集市的数据通常是只读的,每天定时刷新。 + - 因为数仓是有一定规范化的格式的,所以我们需要为分析或OLAP构建一个维度集市来获取数据。数据仓库中同一个表能够创建多个数据集市——同一个实体可以为不同的分析生成不同的维度。 + +#### 2.2. 数据集市的优势和好处 + + 高效访问---数据集市是一种节省时间的解决方案,用于访问特定的数据集以实现商业智能(关于商业智能,我们后续再讲)。 + + 廉价的数据仓库替代品---数据集市可以是开发企业数据仓库的廉价替代品,在这种情况下,所需的数据集较小。一个独立的数据集市可以在一周或更短的时间内启动并运行。 + + 提高数据仓库的性能---依赖性和混合数据集市可以通过承担数据仓库的处理负担来提高数据仓库的性能,以满足分析员的需求。当依赖数据集市被放置在单独的处理设施中时,它们也显著降低了分析处理成本 + + 数据维护-不同的部门可以拥有和控制他们的数据。 + + 简单的设置-简单的设计需要较少的技术技能来设置。 + + 分析-关键绩效指标(KPI)可以很容易地跟踪。 + + 容易进入-数据集市可以成为未来企业数据仓库项目的构建块。 + +# 3. 数据集市的特征 + +- 数据集市的特征主要有: + - 面向主题 / 面向部门; + - 有特定的应用主题; + - 由业务部门定义、设计和开发; + - 业务部门管理和维护; + - 规模小 + - 数据仓库是面向企业的,数据集市是面向部门或者特定业务的; + - 直接面向用户 + - 个性化高 + +# 4. 数据集市的服务方式 + +- 数据集市的服务方式主要有3种: + - 基础数据及数据产品交易,如数据堂、数多多等; + - 满足在线数据调用需求的 API 云服务,如千数堂、聚合数据等; + - 大数据分析结果的交易,如贵阳大数据交易所等。 + +# 5. 数据集市的组成部分 + + - 数据集市的数据,包括基础数据和衍生数据 + ![]({{site.baseurl}}/img-post/数据集市-1.png) + +# 6. 数据治理 & 数据集市 + + - 在数据治理方面,公司主要以数据标准、数据质量、数据安全、元数据、主数据以及生命周期的管理作为重点领域,通过有效的数据资源控制手段,在保证数据安全的情况下,对数据进行有效的管理和控制,以提升数据质量进而提升数据变现的能力,为公司的业务和管理提供多样化、统一的数据服务调用,在公司数字化转型的过程中扮演极其重要的角色。 + + - 数据集市以数据治理为基础,利用数据治理工具治理过的企业数据,按照公司各种业务需求主体,建设单独的应用数据集市,确保满足数据的完整性、及时性、准确性和一致性要求,同时为各类系统提供数据服务。经过治理的数据能提高数据集市建设进度,降低数据集市建设难度,统一不同集市的数据标准,提高数据集市的数据质量。 + +# 7. 数仓 VS 数据集市 + +- 数据仓库中数据结构采用的规范化模式(关系数据库设计理论),数据集市的数据结构采用的星型模式(多维数据库设计理论)。 +- 数据仓库中数据的粒度比数据集市的细。数据集市的数据来源于数据仓库,主要是经过重新组织的汇总数据。 +- 因此,多个数据集市不能构成一个企业级的数据仓库,就好比不可能把大海里的小鱼堆在一起构成一头大鲸鱼。这也说明了数据仓库和数据集市有本质的不同。 + +- 数仓和数据集市的关系类似于工厂和门店的关系。存在三大差异: + - 定位差异 + - 数据仓库在底层,涵盖企业范围内的各类领域数据,能为整个企业各个部门的运行提供决策支持手段,是数据统一整洁仓管; + - 而数据集市在服务层,它要与用户需求进行直接交互聚焦于、各类主题服务区域,服务于特定的数据需求。 + + - 服务差异 + - 数据集市可以在一定程度上缓解访问数据仓库的瓶颈、面向客制化的数据服务应用,因此是广义数仓中被抽离出的一部分,面向实时的、高并发分析;数仓则重点解决海量数据统计分析、低用户并发、大量计算。 + - 数据集市聚焦具体领域,要满足低延时、高并发的数据查询要求,可以7×24 对外提供数据服务,且不影响原有数据仓库统计分析应用的正常运行。大量生产数据的预处理在数仓进行,数据集市接收数仓预处理后的数据。 + - 数据集市是对数仓的补充,灵活、快速响应业务,支持用户部门自行定制数据进行统计分析,支持高并发、性价比较高。 + + - 建模差异 + - 数据仓库的建模 + - 确保提供的数据简单性以及历史数据的集成和联合性是数仓建模应具备的关键原则。要由企业IT部门或者DT部门的数据专家进行统一组织设计。 + + - 数据集市的数据建模 + + - 数据集市的建模是由业务需求驱动的。数据集市模型对于捕获业务需求十分有用,数集提供的数据服务必须是有业务价值的,否则不应该存在。 + + - 数据集市的建模是终端用户发起的。终端用户必须参与数据集市的建模过程,他们是数据集市的需求发起方,也即是否有价值的关键评判者,而不是IT或DT部门自己。 + + - 数据集市的建模不是数据技术专家依据相关数据标准收集的,它受行业业务经验以及数据分析技术的影响。数据分析技术可以影响所选择的数据模型的类型及其内容。目前,有几种常用的数据分析技术:查询和报表制作、多维分析以及数据挖掘。 + + +# 8. 数据集市建设关键点 + +- 集市与其它系统的边界及关系: + - 需要明确集市与基础数据平台的边界及定位,同时要梳理清楚与各应用系统在数据上的关系。 +- 支持数据不定期重跑: + - 数据保存完整,合理利用业务特征和日期属性建立分区表,批量采用流程式配置,做到支持快速重跑。 +- 批处理性能: + - 批处理时间直接影响业务对数据的应用,需要从业务以及技术层面进行梳理,提高整体运行效率。 + +- 历史数据存储方式: + - 明细区数据采用拉链形式,汇总区则保留一段时间的切片数据,指标区数据则根据实际需要存储。 +- 指标体系建设: + - 为了最大限度复用与灵活管理集市数据,以指标方式提供数据服务可大大减少建设与运维成本。 + + +# 9. ETL & 数据集市 + +![]({{site.baseurl}}/img-post/数据集市-1.png) + +在决策引擎的输入/输出时,结果由于数据量较大,一般都是存储为横表(一笔合同多行),而很多指标是风控数据分析人员比较关注的(如:外部数据源返回的结果),为了便于分析需要将横表转竖表(一笔合同一行)。 + + + diff --git "a/_posts/2022-01-28-Airflow\357\274\232Airflow \344\275\277\347\224\250\346\226\271\346\263\225\345\217\212\346\263\250\346\204\217\351\227\256\351\242\230.md" "b/_posts/2022-01-28-Airflow\357\274\232Airflow \344\275\277\347\224\250\346\226\271\346\263\225\345\217\212\346\263\250\346\204\217\351\227\256\351\242\230.md" new file mode 100644 index 00000000000..3e82654c111 --- /dev/null +++ "b/_posts/2022-01-28-Airflow\357\274\232Airflow \344\275\277\347\224\250\346\226\271\346\263\225\345\217\212\346\263\250\346\204\217\351\227\256\351\242\230.md" @@ -0,0 +1,214 @@ +--- +layout: post +title: Airflow:Airflow 使用方法及注意问题 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Airflow +--- + +> 关于 Airflow 的介绍,请参考 《ETL:ETL的基本概念》 中 ***5.3. Airflow*** 一节的内容。 + +# 1. 安装配置 + +#### 1.1. 安装步骤 + +``` +sudo apt update +sudo apt install python3-pip +pip3 install --upgrade pip +``` + +#### 1.2. 配置文件安装路径及环境变量 + +``` +export AIRFLOW_HOME=~/airflow +pip3 install apache-airflow +export AIRFLOW_HOME=/home/work/airflow +echo 'export PATH=/usr/local/bin:$PATH' >>~/.bashrc +source ~/.bashrc +``` + +#### 1.3. 初始化 airflow + +``` +airflow db init +``` + +#### 1.4. 配置 webserver + +``` +vim /home/work/airflow/airflow.cfg +``` +- 修改 webserver 配置 +``` +[webserver] +security = Flask AppBuilder +secure_mode = True +rbac=True +``` + +#### 1.5. 重置 airflow + +``` +airflow db reset +``` + +#### 1.6. 创建账号密码 + +``` +airflow users create --lastname user --firstname admin --username name --email xxx@xxx.com --role Admin --password password +``` + +# 2. 启动访问 + +#### 2.1. 启动服务 + +``` +airflow webserver --port 8080 --hostname 0.0.0.0 # 注意:ubuntu如果有开启防火墙,则需要允许端口8080通过 +``` +#### 2.2. web 访问 +``` +http://xxx.xxx.xxx.xxx:8080/home +账号:xxxx +密码:xxxx +``` + +# 3. DAGS + +#### 3.1. DAG 概念简介 + +- dags 是用 python 编写的任务调度文件,用于给 airflow 要执行的 task 做任务计划。 + +#### 3.2. 文件位置 + +- dags 文件保存在该目录下 + ``` + /home/work/airflow/dags + ``` + +#### 3.3. DAGS 样例文件 + +- sample.py + + ```aidl + # coding: utf-8 + + from airflow import DAG + # from airflow.operators.python_operator import PythonOperator + from airflow.operators.bash_operator import BashOperator + from datetime import datetime, timedelta + + + default_args = { + 'owner': 'admin', + 'depends_on_past': False, + 'start_date': datetime(2022, 3, 24), + # 'email': ['xxx@xxx.com'], + # 'email_on_failure': True, + # 'email_on_retry': True, + 'retries': 3, + 'retry_delay': timedelta(seconds=5) + } + + + dag = DAG( + dag_id='dws_delivery_reach_result_test', + default_args=default_args, + # schedule_interval="00, *, *, *, *" + schedule_interval=timedelta(minutes=1) + ) + + + first_task = BashOperator( + task_id='dws_delivery_reach_result_test_1', + depends_on_past=False, + bash_command='python /home/work/airflow/tasks/xxxxx.py', + dag=dag + ) + + second_task = BashOperator( + task_id='dws_delivery_reach_result_test_2', + depends_on_past=False, + bash_command='python /home/work/airflow/tasks/xxxxx.py', + dag=dag + ) + + first_task >> second_task + + ``` + +#### 3.4. 执行 dags 文件 + +```aidl +python sample.py +``` +如果不报错就是执行成功。 + +#### #、3.5. 检查 dags 是否生效 + +```aidl +airflow dags list +``` + +如果 `paused` 值为 `None`,则表示 scheduler 中并未引入 dag。 + +```aidl +ag_id | filepath | owner | paused +=================================+==========+=======+======= +dws_delivery_reach_result_test_1 | test.py | admin | False +``` +如果 `paused` 值为 `True`,则表示 dags 生效,在webUI中可以查看到该 dag。 + +```aidl +dag_id | filepath | owner | paused +=================================+==========+=======+======= +dws_delivery_reach_result_test_1 | test.py | admin | True +``` + +# 4. TASKS + +#### 4.1. 概念简介 + +- task 是用来做数据清洗同步的具体任务,内容是用 python 编写的 SQL 相关语句,数据清洗工作在此处完成; + +#### 4.2. 文件位置 + +- task 文件存放在该目录下: + + `/home/work/airflow/tasks/` + +#### 4.3. dags 调度任务 + +>注意: +>- 使用 BashOperator,以命令行的形式执行 `python /home/work/airflow/tasks/xxxxx.py`; + +#### 4.4. 频率设置 + +以下两种方式二选一: + +- 定时任务: + - `schedule_interval="00, *, *, *, *"` + +- 时间间隔 + - `schedule_interval=timedelta(minutes=1)` + + +# 5. 注意事项 + +#### 5.1. dags 文件禁止汉字 + +- dag 文件中不能出现汉字,否则 dags 不会生效。 + +#### 5.2. 注意 cfg 文件中默认时间 + +`default_timezone = Asia/Shanghai` + +#### 5.3. 推荐使用 BashOperator,不要使用 PythonOperator。 + +- 尽量不要使用 PythonOperator,不然 task 脚本会一直出现 import 导入路径报错。 + + diff --git "a/_posts/2022-01-28-DataX\357\274\232DataX \345\270\270\350\247\201\345\274\202\345\270\270\345\217\212\345\244\204\347\220\206\346\226\271\346\263\225.md" "b/_posts/2022-01-28-DataX\357\274\232DataX \345\270\270\350\247\201\345\274\202\345\270\270\345\217\212\345\244\204\347\220\206\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..92ddb9ae60d --- /dev/null +++ "b/_posts/2022-01-28-DataX\357\274\232DataX \345\270\270\350\247\201\345\274\202\345\270\270\345\217\212\345\244\204\347\220\206\346\226\271\346\263\225.md" @@ -0,0 +1,168 @@ +--- +layout: post +title: DataX:DataX 常见异常及处理方法 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - DataX +--- + + +# 1. 无法访问 datax-web + +- 问题现象: + - 启动无报错,但无法访问 datax-web 网页 + +- 处理方法:重新安装初始化 datax + + ``` + cd /home/work/datax-web/build/datax-web-2.1.2/bin & ./install.sh + ``` + +# 2. 初始化 datax 时 MySQL 拒绝访问 + +- 报错内容: + + `ERROR 1045 (28000): Access denied for user 'root'@'xxx.xxx.xxx.xxx' (using password: YES)` + +- 处理方法:不要使用 root 账号连接 MySQL,使用 jianzi 账号密码连接 MySQL + + ``` + Please input the db username(default: root): jianzi + Please input the db password(default: ): 4ylnkL6lRT + ``` + +# 3. 启动 DataX 不成功 + +- 报错内容: + + `DATAX-EXEXUTOR didn't start successfully, not found in the java process table` + +- 处理方法: + ``` + ./start-all.sh start -v + ``` + +# 4. 无法连接数据源 + +- 报错内容: + + `Code:[DBUtilErrorCode-10], Description:[连接数据库失败. 请检查您的 账号、密码、数据库名称、IP、Port或者向 DBA 寻求帮助(注意网络环境).]. - 具体错误信息为:com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create connection to database server.` + +- 处理方法: + + - 检查要连接的 MySQL 账号密码; + - 检查要连接的 MySQL 服务器端口是否开放; + - 检查要连接的 MySQL 版本,如果是 MySQL8,修改 jdbc 驱动类: + ``` + com.mysql.cj.jdbc.Driver + ``` + +# 5. datax-web 创建任务报错 + + - 报错内容: + + `### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 't.user_id' in 'field list' ### The error may exist in file [/home/work/datax-web/build/datax-web-2.1.2/modules/datax-admin/conf/mybatis-mapper/JobInfoMapper.xml] ### The error may involve com.wugui.datax.admin.mapper.JobInfoMapper.findAll-Inline ### The error occurred while setting parameters ### SQL: SELECT t.id, t.job_group, t.job_cron, t.job_desc, t.add_time, t.update_time, t.user_id, t.alarm_email, t.executor_route_strategy, t.executor_handler, t.executor_param, t.executor_block_strategy, t.executor_timeout, t.executor_fail_retry_count, t.glue_type, t.glue_source, t.glue_remark, t.glue_updatetime, t.child_jobid, t.trigger_status, t.trigger_last_time, t.trigger_next_time, t.job_json, t.replace_param, t.jvm_param, t.inc_start_time, t.partition_info, t.last_handle_code, t.replace_param_type, t.project_id, t.reader_table, t.primary_key, t.inc_start_id, t.increment_type, t.datasource_id FROM job_info AS t ORDER BY job_desc ASC ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 't.user_id' in 'field list' ; bad SQL grammar []; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 't.user_id' in 'field list'` + + ![]({{site.baseurl}}/img-post/datax-1.png) + + 其中的关键错误是:`Unknown column 't.user_id' in 'field list' `。 + + - 处理方法: + + 连接 dataX 服务器上 MySQL,进入 `dataxweb > jobinfo`,修改 `author(VARCHAR(50))` 字段名为 `user_id(INT(11))`; + + 修改后重新创建任务; + + 注意:这是个临时解决方案,详细原因并未知晓!!! + +# 6. 调度失败 + +- 报错内容: + + ``` + 任务触发类型:手动触发 + 调度机器:172.16.0.157 + 执行器-注册方式:自动注册 + 执行器-地址列表:null + 路由策略:随机 + 阻塞处理策略:单机串行 + 任务超时时间:0 + 失败重试次数:0 + + >>>>>>>>>>>触发调度<<<<<<<<<<< + 调度失败:执行器地址为空 + ``` +- 处理方法: + + - 十分钟后再次执行任务。 + +# 7. `can't find '__main__' module in ''` + +- 报错内容: + + ``` + 2022-04-20 20:18:57 [JobThread.run-130]
    ----------- datax-web job execute start -----------
    ----------- Param: + 2022-04-20 20:18:57 [BuildCommand.buildDataXParam-100] ------------------Command parameters: + 2022-04-20 20:18:57 [ExecutorJobHandler.execute-57] ------------------DataX process id: -1 + 2022-04-20 20:18:57 [ProcessCallbackThread.callbackLog-186]
    ----------- datax-web job callback finish. + 2022-04-20 20:18:57 [AnalysisStatistics.analysisStatisticsLog-53] /usr/bin/python: can't find '__main__' module in '' + 2022-04-20 20:18:57 [JobThread.run-165]
    ----------- datax-web job execute end(finish) -----------
    ----------- ReturnT:ReturnT [code=500, msg=command exit value(1) is failed, content=null] + 2022-04-20 20:18:57 [TriggerCallbackThread.callbackLog-186]
    ----------- datax-web job callback finish. + ``` + +- 处理方法: + + - 原因分析:datax-web找不到datax启动文件(datax.py) + + - 修改 datax-executor.sh 文件 + + ``` + cd /home/work/datax-web/modules/datax-executor/bin + vim datax-executor.sh + ``` + 找到下面的代码并注释掉。 + ``` + # JAVA_OPTS=${JAVA_OPTS}" -Dserver.port="${SERVER_PORT}" -Ddata.path="${DATA_PATH}" -Dexecutor.port="${EXECUTOR_PORT}" -Djson.path="${JSON_PATH}" -Dpython.path="${PYTHON_PATH}" -Ddatax.admin.port="${DATAX_ADMIN_PORT} + ``` + 写入成下面的代码。 + ``` + JAVA_OPTS=${JAVA_OPTS}" -Dserver.port="${SERVER_PORT}" -Ddata.path="${DATA_PATH}" -Dexecutor.port="${EXECUTOR_PORT}" -Djson.path="${JSON_PATH}" -Dpython.path="/home/work/datax/bin/datax.py" -Ddatax.admin.port="${DATAX_ADMIN_PORT} + ``` + - 保存退出,重启 datax-web。 + +# 8. `您提供的配置文件存在错误信息,请检查您的作业配置。` + +- 报错内容: + + ` + 插件[odpsreader,mysqlwriter]加载失败,1s后重试... Exception:Code:[Common-00], Describe:[您提供的配置文件存在错误信息,请检查您的作业配置 .] - 配置信息错误, + 您提供的配置文件[/home/work/datax/plugin/reader/._hbase11xreader/plugin.json]不存在. 请检查您的配置文件. + 经DataX智能分析,该任务最可能的错误原因是: + com.alibaba.datax.common.exception.DataXException: Code:[Common-00], Description:[您提供的配置文件存在错误信息,请检查您的作业配置。]. + -配置信息错误 ,您提供的配置文件:/home/work/datax/plugin/reader/._hbase11xreader/plugin.json]不存在,请检查您的配置文件。 + ` + +- 原因分析:操作过 reader 目录的内容,系统会自动生成一个.DS_Store 的系统文件,dataX默认plugin/render都是文件夹,没有做操作系统这种层面的差异处理所以需要手动把错误路径下的._hbase11xreader文件删除即可。 + +- 处理方法: + + ``` + find . -name '._*'|xargs rm -rf + ``` + +# 9. 回滚此次写入, 采用每次写入一行方式提交 + +- 报错内容: + + ` + WARN CommonRdbmsWriter$Task - 回滚此次写入, 采用每次写入一行方式提交. 因为:[9001, 2022052000014319216820803603453360741] unsupport packet=>030000001B0100, packet_name=mysql_set_server_option + ` + +- 解决方法: + + - 删除低版本 `mysql-connector-java-xxx.jar` jar 包。 + \ No newline at end of file diff --git "a/_posts/2022-01-28-DataX\357\274\232DataX \346\225\260\346\215\256\345\220\214\346\255\245\345\267\245\345\205\267\344\275\277\347\224\250\346\226\271\346\263\225.md" "b/_posts/2022-01-28-DataX\357\274\232DataX \346\225\260\346\215\256\345\220\214\346\255\245\345\267\245\345\205\267\344\275\277\347\224\250\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..5bbb915cdcd --- /dev/null +++ "b/_posts/2022-01-28-DataX\357\274\232DataX \346\225\260\346\215\256\345\220\214\346\255\245\345\267\245\345\205\267\344\275\277\347\224\250\346\226\271\346\263\225.md" @@ -0,0 +1,306 @@ +--- +layout: post +title: DataX:DataX 部署使用方法及注意问题 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - DataX +--- + +> 关于 DataX 的介绍,请参考 《ETL:ETL的基本概念》 中 ***5.2. DataX*** 一节的内容。 + +# 1. 环境准备 + +#### 1.1. python + +- 网上文章推荐 python 2.6,但实际 python 3.7 一样支持,自行根据需要选择; +- 注意安装好以后配置环境变量; +- 安装配置细节,此处不再赘述; + +#### 1.2. JDK + +- JDK 需要 1.6 以上,推荐使用 1.6; +- 细节此处不再赘述; + +#### 1.3. Apache Maven + +- 安装 Apache Maven 3.x (Compile DataX); +- 细节此处不再赘述; + +# 2. DataX 安装部署 + +#### 2.1. 安装步骤 + +- 环境准备 + + - python + + 服务器自带 Python2 和 Python3,不需要重新安装; + + - JAVA_HOME + + ``` + sudo apt install -y default-jdk + ``` + +- 下载安装 + - 在 /home 下新建并进入 work 目录 + ``` + sudo mkdir /home/work & cd /home/work + ``` + + - 下载解压 datax + ``` + wget http://datax-opensource.oss-cn-hangzhou.aliyuncs.com/datax.tar.gz + tar -zxvf datax.tar.gz + + ``` + - 下载解压 data-web + - 百度网盘:https://pan.baidu.com/s/13yoqhGpD00I82K4lOYtQhg,密码:cpsk; + - 手动上传到 /home/work 目录中,并解压缩; + ``` + tar -zxvf datax-web-2.1.2.tar.gz + ``` +- 安装 & 初始化 + + ``` + cd /home/work/datax-web-2.1.2/bin + ./install.sh + + ... + Do you want to initalize database with sql: [/home/work/datax-web/build/datax-web-2.1.2/bin/db/datax_web.sql]? (Y/N)y + Please input the db host(default: 127.0.0.1): + Please input the db port(default: 3306): + Please input the db username(default: root): jianzi + Please input the db password(default: ): 4ylnkL6lRT + Please input the db name(default: dataxweb) + ... + ``` + +#### 2.2. 修改 DB 配置 + +- 修改 datax 服务器本地 MySQL 数据库,用于存储 datax 账号密码、同步任务、数据源连接信息等; + + ``` + vim /home/work/datax-web/build/datax-web-2.1.2/modules/datax-admin/conf/bootstrap.properties + + #Database + DB_HOST=xxx.xxx.xxx.xxx + DB_PORT=3306 + DB_USERNAME=xxx + DB_PASSWORD=xxx + DB_DATABASE=dataxweb + ``` + +#### 2.3. 初始化 + +- 初始化操作时,会重新在 MySQL 中新建用户和任务信息,如果是重新初始化,记得将 MySQL dataxweb 库中的数据做备份。 + + ``` + cd /home/work/datax-web/build/datax-web-2.1.2/bin & ./install.sh + + ... + Do you want to initalize database with sql: [/home/work/datax-web/build/datax-web-2.1.2/bin/db/datax_web.sql]? (Y/N)y + Please input the db host(default: 127.0.0.1): + Please input the db port(default: 3306): + Please input the db username(default: root): xxxx + Please input the db password(default: ): xxxx + Please input the db name(default: dataxweb) + ... + ``` + +#### 2.4. 添加 MySQL8 需要的 jar 包 + +- mysql-connector-java-8.0.28.jar 文件下载地址 + + - 点击下载站内资源:mysql-connector-java-8.0.28.jar + +- 查找文件 mysql-connector-java-5.1.47.jar 所在位置 + ``` + find -name "*mysql-con*" + ``` + +- 下面返回的是文件路径示例,这些目录全部都要进行添加 + ``` + /home/work/datax-web-2.1.2/modules/datax-admin/lib/mysql-connector-java-5.1.47.jar + /home/work/datax/plugin/writer/drdswriter/libs/mysql-connector-java-5.1.34.jar + /home/work/datax/plugin/writer/drdswriter/libs/._mysql-connector-java-5.1.34.jar + /home/work/datax/plugin/writer/mysqlwriter/libs/mysql-connector-java-5.1.34.jar + /home/work/datax/plugin/writer/mysqlwriter/libs/._mysql-connector-java-5.1.34.jar + /home/work/datax/plugin/writer/adswriter/libs/._mysql-connector-java-5.1.31.jar + /home/work/datax/plugin/writer/adswriter/libs/mysql-connector-java-5.1.31.jar + /home/work/datax/plugin/reader/drdsreader/libs/mysql-connector-java-5.1.34.jar + /home/work/datax/plugin/reader/drdsreader/libs/._mysql-connector-java-5.1.34.jar + /home/work/datax/plugin/reader/oceanbasev10reader/libs/mysql-connector-java-5.1.40.jar + /home/work/datax/plugin/reader/mysqlreader/libs/mysql-connector-java-5.1.34.jar + /home/work/datax/plugin/reader/mysqlreader/libs/._mysql-connector-java-5.1.34.jar + ``` +- 将 mysql-connector-java-8.0.28.jar 文件放到上面返回的文件所在目录 + + +# 3. DataX 使用 + +#### 3.1. DataX 启动命令 + +- 在启动前,记得先执行关闭操作; + + ``` + cd /home/work/datax-web/build/datax-web-2.1.2/bin + bash stop-all.sh + ./start-all.sh + ``` + +#### 3.2. 访问 datax-web + +- 访问地址:http://xxx.xxx.xxx.xxx:9527/index.html + +- 访问datax-web 记住务必加/index.html,不加会提示 `Whitelabel Error Page` 报错! + +#### 3.3. DataX 用户管理 + +- 注意:首次启动,一定要修改 admin 密码! + + 修改路径: `用户管理 > admin > 编辑 > 密码` + + +#### 3.4. MySQL8 修改 jdbc 驱动类 + +- EDS 如果连接 MySQL8,数据源连接需要修改 jdbc 驱动类 + + jdbc 驱动类: + + ``` + com.mysql.cj.jdbc.Driver + ``` + +# 4. DataX 任务管理 + +#### 4.1. DataX 任务模板 + +- 阻塞处理 + + - 选择:**单机串行**; + + ![]({{site.baseurl}}/img-post/datax-2.png) + + +#### 4.2. 创建任务:reader 增量同步 + +- 增量同步 where 条件: + + 更新指定时间内的数据。 + + ``` + "where": "last_update_time>=subdate(current_date, 1)", # 更新昨天的数据 + + "where": "last_update_time>=subdate(current_date, 7)", # 更新七天内的数据 + ``` + +#### 4.3. 创建任务:writer 替换更新 + +- 写入模式使用 replace; + + replace 在写入数据的时候,会对主键相同的数据进行 update,而不是 insert。 + + ``` + "writeMode": "replace", + ``` + +#### 4.4. 切分字段 + +- 如果要同步的数据量比较大,需要在创建任务的时候,添加主键作为切分字段。 + + ``` + "splitPk": "sales_order_id" + ``` + +#### 4.5. 任务示例 + +``` +{ + "job": { + "setting": { + "speed": { + "channel": 3, + "byte": 1048576 + }, + "errorLimit": { + "record": 0, + "percentage": 0.02 + } + }, + "content": [ + { + "reader": { + "name": "mysqlreader", + "parameter": { + "where": "last_update_time>=subdate(current_date, 7)", + "username": "xxxxx", + "password": "xxxxx", + "column": [ + "`add_time`", + "`last_update_time`", + "`日期`", + "`店铺_系统或分销商`", + "`规格编码`", + "`发货数量`", + "`退货数量`", + "`销售数量`", + "`发货金额`", + "`退货金额`", + "`实际销售额`", + "`数据来源表`", + "`发退货类型`" + ], + "splitPk": "xxx_id", + "connection": [ + { + "table": [ + "xxxxxx" + ], + "jdbcUrl": [ + "jdbc:mysql://xxx.xxx.xxx.xxx:3306/xxx" + ] + } + ] + } + }, + "writer": { + "name": "mysqlwriter", + "parameter": { + "writeMode": "replace", + "username": "xxxxx", + "password": "xxxxx", + "column": [ + "`source_add_time`", + "`source_last_update_time`", + "`日期`", + "`店铺_系统或分销商`", + "`规格编码`", + "`发货数量`", + "`退货数量`", + "`销售数量`", + "`发货金额`", + "`退货金额`", + "`实际销售额`", + "`数据来源表`", + "`发退货类型`" + ], + "connection": [ + { + "table": [ + "ods_xxxxxx" + ], + "jdbcUrl": "jdbc:mysql://xxx.xxx.xxx.xxx:3306/ods" + } + ] + } + } + } + ] + } +} +``` diff --git "a/_posts/2022-01-28-Dolphinscheduler\357\274\232Dolphinscheduler \344\273\213\347\273\215.md" "b/_posts/2022-01-28-Dolphinscheduler\357\274\232Dolphinscheduler \344\273\213\347\273\215.md" new file mode 100644 index 00000000000..e95e374ea9d --- /dev/null +++ "b/_posts/2022-01-28-Dolphinscheduler\357\274\232Dolphinscheduler \344\273\213\347\273\215.md" @@ -0,0 +1,132 @@ +--- +layout: post +title: DolphinScheduler:DolphinScheduler 介绍 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - DolphinScheduler +--- + +# DolphinScheduler 概念 + +#### DAG + +- 全称 Directed Acyclic Graph,简称DAG。 +- 工作流中的Task任务以有向无环图的形式组装起来,从入度为零的节点进行拓扑遍历,直到无后继节点为止。 + +#### 流程定义 + +- 通过拖拽任务节点并建立任务节点的关联所形成的可视化DAG; + +#### 流程实例 + +- 流程实例是流程定义的实例化,可以通过手动启动或定时调度生成,流程定义每运行一次,产生一个流程实例; + +#### 任务实例 + +- 任务实例是流程定义中任务节点的实例化,标识着具体的任务执行状态; + +#### 任务类型 + +- 目前支持有SHELL、SQL、SUB_PROCESS(子流程)、PROCEDURE、MR、SPARK、PYTHON、DEPENDENT(依赖),同时计划支持动态插件扩展,注意:其中子 SUB_PROCESS 也是一个单独的流程定义,是可以单独启动执行的; + +#### 调度方式 + +- 支持基于 cron表 达式的定时调度和手动调度。命令类型支持: + - 启动工作流 + - 从当前节点开始执行 + - 恢复被容错的工作流 + - 恢复暂停流程 + - 从失败节点开始执行 + - 补数 + - 定时 + - 重跑 + - 暂停 + - 停止 + - 恢复等待线程。 +- 其中 恢复被容错的工作流 和 恢复等待线程 两种命令类型是由调度内部控制使用,外部无法调用; + +#### 定时调度 + +- 采用 quartz 分布式调度器,并同时支持cron表达式可视化的生成; + +#### 依赖 + +- 不单单支持 DAG 简单的前驱和后继节点之间的依赖,同时还提供任务依赖节点,支持流程间的自定义任务依赖; + +#### 优先级 + +- 支持流程实例和任务实例的优先级,如果流程实例和任务实例的优先级不设置,则默认是先进先出; + +#### 邮件告警 + +- 支持 SQL任务 查询结果邮件发送,流程实例运行结果邮件告警及容错告警通知; + +#### 失败策略 + +- 对于并行运行的任务,如果有任务失败,提供两种失败策略处理方式,继续是指不管并行运行任务的状态,直到流程失败结束。结束是指一旦发现失败任务,则同时Kill掉正在运行的并行任务,流程失败结束; + +#### 补数 + +- 补历史数据,支持区间并行和串行两种补数方式; + +# DolphinScheduler 架构 + +#### DolphinScheduler 架构图 + +![]({{site.baseurl}}/img-post/dolphinscheduler-2.png) + +#### MasterServer + +- MasterServer 采用分布式无中心设计理念,MasterServer 主要负责 DAG 任务切分、任务提交监控,并同时监听其它 MasterServer 和 WorkerServer 的健康状态。 + +- MasterServer 服务启动时向 Zookeeper 注册临时节点,通过监听 Zookeeper 临时节点变化来进行容错处理。 + +- MasterServer 服务内主要包含: + - Distributed Quartz 分布式调度组件:主要负责定时任务的启停操作,当 quartz 调起任务后, Master 内部会有线程池具体负责处理任务的后续操作; + - MasterSchedulerThread:是一个扫描线程定时扫描数据库中的 command 表,根据不同的命令类型进行不同的业务操作; + - MasterExecThread:主要是负责DAG任务切分、任务提交监控、各种不同命令类型的逻辑处理; + - MasterTaskExecThread:主要负责任务的持久化; + +#### WorkerServer + +- WorkerServer 也采用分布式无中心设计理念,WorkerServer 主要负责任务的执行和提供日志服务。 WorkerServer 服务启动时向 Zookeeper 注册临时节点,并维持心跳。 + +- 该服务包含: + - FetchTaskThread:主要负责不断从 Task Queue 中领取任务,并根据不同任务类型调用 TaskScheduleThread 对应执行器; + - LoggerServer:是一个RPC服务,提供日志分片查看、刷新和下载等功能。 + +#### ZooKeeper + +- ZooKeeper 服务,系统中的 MasterServer 和 WorkerServer 节点都通过 ZooKeeper 来进行集群管理和容错。 +- 另外系统还基于 ZooKeeper 进行事件监听和分布式锁。 + +#### Task Queue + +- 提供任务队列的操作,目前队列也是基于 Zookeeper 来实现。 +- 由于队列中存的信息较少,不必担心队列里数据过多的情况,实际上压测过百万级数据存队列,对系统稳定性和性能没影响。 + +#### Alert + +- 提供告警相关接口,接口主要包括告警两种类型的告警数据的存储、查询和通知功能。其中通知功能又有邮件通知和 SNMP(暂未实现) 两种。 + +#### API + +- API接口层,主要负责处理前端UI层的请求。 +- 该服务统一提供 RESTful api 向外部提供请求服务。 接口包括工作流的创建、定义、查询、修改、发布、下线、手工启动、停止、暂停、恢复、从该节点开始执行等等。 + +#### UI + +- 系统的前端页面,提供系统的各种可视化操作界面。 + +# DolphinScheduler 工作流程 + +![]({{site.baseurl}}/img-post/dolphinscheduler-1.png) + + + + + diff --git "a/_posts/2022-01-28-Dolphinscheduler\357\274\232Dolphinscheduler \344\275\277\347\224\250\346\263\250\346\204\217\344\272\213\351\241\271.md" "b/_posts/2022-01-28-Dolphinscheduler\357\274\232Dolphinscheduler \344\275\277\347\224\250\346\263\250\346\204\217\344\272\213\351\241\271.md" new file mode 100644 index 00000000000..f9fe38b2913 --- /dev/null +++ "b/_posts/2022-01-28-Dolphinscheduler\357\274\232Dolphinscheduler \344\275\277\347\224\250\346\263\250\346\204\217\344\272\213\351\241\271.md" @@ -0,0 +1,70 @@ +--- +layout: post +title: DolphinScheduler:DolphinScheduler 使用注意事项 +subtitle: 调度方式 & 参数 & 补数据 +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - DolphinScheduler +--- + + + +#### DolphinScheduler 调度方式 + +- 系统支持基于 cron 表达式的定时调度和手动调度。 +- 命令类型支持:启动工作流、从当前节点开始执行、恢复被容错的工作流、恢复暂停流程、从失败节点开始执行、补数、定时、重跑、暂停、停止、恢复等待线程。其中 恢复被容错的工作流 和 恢复等待线程 两种命令类型是由调度内部控制使用,外部无法调用。 + +#### DolphinScheduler 参数关键问题 + +- 不支持上下游传递 + - 即不支持上游的 out 给下游作为 in 输入 +- 不支持在启动流程时候配置参数 +- 有全局参数,可以在每个task使用 +- OUT 参数只用于存储过程输出 + +#### DolphinScheduler 补数据(数据回填) + +- 示例:基于日期的增量更新 + + ``` + select + 字段一, + 字段二, + + ..... + + 字段N, + from 表名 + WHERE DATE_FORMAT(update_time, '%Y-%m-%d') + >= ${start_time} AND DATE_FORMAT(update_time, '%Y-%m-%d') + < ${end_time}; + ``` + +- 自定义参数 + + ``` + prop: start_time, value: ${global_start_time} + prop: end_time, value: ${global_end_time} + ``` + +- 正常增量同步时,全局变量赋值 + + ``` + prop: global_start_time, value: date_sub(current_date(),interval 1 day) + prop: global_end_time, value: current_date() + ``` + +- 补数据时,全局变量赋值 + + ``` + prop: global_start_time, value: 'xxxx-xx-xx' + prop: global_end_time, value: 'xxxx-xx-xx' + ``` + + + + + diff --git "a/_posts/2022-01-28-Flume\357\274\232Flume TailDirSource + Kafka Channel + Hdfs sink \346\227\245\345\277\227\351\207\207\351\233\206\347\244\272\344\276\213.md" "b/_posts/2022-01-28-Flume\357\274\232Flume TailDirSource + Kafka Channel + Hdfs sink \346\227\245\345\277\227\351\207\207\351\233\206\347\244\272\344\276\213.md" new file mode 100644 index 00000000000..a4ece8a6574 --- /dev/null +++ "b/_posts/2022-01-28-Flume\357\274\232Flume TailDirSource + Kafka Channel + Hdfs sink \346\227\245\345\277\227\351\207\207\351\233\206\347\244\272\344\276\213.md" @@ -0,0 +1,399 @@ +--- +layout: post +title: Flume:Flume TailDirSource + Kafka Channel + Hdfs sink 日志采集示例 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Flume +--- + + +# 1. 模拟生成日志 + +#### 1.1. 日志生成脚本 + +- 下载脚本 + - 链接:https://pan.baidu.com/s/1OqriA6Yr5B-W90vR71RPkg?pwd=0qnm + - 提取码:0qnm + +- 将脚本复制到服务器; +- 修改日志存放路径 + ``` + vim logback.xml + + # 修改下面的路径到指定目录 + + ``` + +#### 1.2. 修改日志的日期 + +- 修改要生成的日志的日期,具体的日期可以自行修改。 + + ```aidl + vim application.yml + ``` + + ```aidl + #业务日期 + mock.date: "2021-06-08" + ``` + +#### 1.3. 生成日志 + +- 后台运行 + + ```aidl + nohup java -jar gmall2020-mock-log-2021-01-22.jar 1>/dev/null 2>/dev/null & + ``` + +#### 1.4. 群启日志生成脚本 + +- 在 `/usr/local/bin` 目录下编辑 `start_applog.sh` + + ```aidl + #!/bin/bash + for i in hadoop102 hadoop103 hadoop104;do + echo ============ $i start applog =================== + ssh $i "cd /usr/local/applog; nohup java -jar gmall2020-mock-log-2021-01-22.jar 1>/dev/null 2>/dev/null &" + done + ``` + +- 修改权限 + ```aidl + chmod 777 start_applog.sh + ``` + +- 分发日志生成脚本到 hadoop102 hadoop103 hadoop104 + ``` + xsync applog/ + ``` + +- 群启动 + ```aidl + start_applog.sh + + ============ hadoop102 start applog =================== + ============ hadoop103 start applog =================== + ============ hadoop104 start applog =================== + ``` + +# 2. 采集日志 + +#### 2.1. 编辑 Flume 配置文件 + +- 在 `/flume/job` 目录下编辑 `file_to_kafka.conf` + + ```aidl + a1.sources=r1 + a1.channels=c1 c2 + + # configure source + a1.sources.r1.type = TAILDIR + a1.sources.r1.positionFile = /usr/local/flume/log_position.json + a1.sources.r1.filegroups = f1 + a1.sources.r1.filegroups.f1 = /usr/local/applog/log/app.* + a1.sources.r1.fileHeader = true + a1.sources.r1.channels = c1 c2 + + #interceptor + a1.sources.r1.interceptors = i1 i2 + a1.sources.r1.interceptors.i1.type = com.dex0423.flume.interceptor.ETLInterceptor$Builder + a1.sources.r1.interceptors.i2.type = com.dex0423.flume.interceptor.ETLInterceptor$Builder + + a1.sources.r1.selector.type = multiplexing + a1.sources.r1.selector.header = topic + a1.sources.r1.selector.mapping.topic_start = c1 + a1.sources.r1.selector.mapping.topic_event = c2 + + # configure channel + a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel + a1.channels.c1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092 + a1.channels.c1.kafka.topic = topic_start + a1.channels.c1.parseAsFlumeEvent = false + a1.channels.c1.kafka.consumer.group.id = flume-consumer + + a1.channels.c2.type = org.apache.flume.channel.kafka.KafkaChannel + a1.channels.c2.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092 + a1.channels.c2.kafka.topic = topic_event + a1.channels.c2.parseAsFlumeEvent = false + a1.channels.c2.kafka.consumer.group.id = flume-consumer + ``` + +#### 2.2. 编辑 Flume 拦截器 + +- 具体 JAVA 工程步骤,这里不再赘述,下面是两个核心文件。 + + - 点击下载站内资源:flume-interceptor.rar + +- `pom.xml` + + ```aidl + + + 4.0.0 + + org.example + flume-interceptor + 1.0-SNAPSHOT + + + 8 + 8 + + + + + org.apache.flume + flume-ng-core + 1.9.0 + provided + + + + com.alibaba + fastjson + 1.2.62 + + + + + + + maven-compiler-plugin + 2.3.2 + + 1.8 + 1.8 + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + + + ``` + +- `ETLInterceptor.java`,我们在这里自己创建拦截器,根据业务不同可以创建不同的拦截器。 + + ```aidl + package com.dex0423.flume.interceptor; + + import org.apache.flume.Context; + import org.apache.flume.Event; + import org.apache.flume.interceptor.Interceptor; + + import java.nio.charset.StandardCharsets; + import java.util.Iterator; + import java.util.List; + + public class ETLInterceptor implements Interceptor { + + + @Override + public void initialize() { + + } + + @Override + public Event intercept(Event event) { + + byte[] body = event.getBody(); + + String log = new String(body, StandardCharsets.UTF_8); + + // 过滤 event 中的数据是否是 JSON 格式 + if (JSONUtils.isJSONValidate(log)){ + return event; + }else { + return null; + } + } + + @Override + public List intercept(List list) { + + Iterator iterator = list.iterator(); + + while (iterator.hasNext()) { + Event next = iterator.next(); + + if (intercept(next) == null){ + iterator.remove(); + } + } + + return list; + } + + @Override + public void close() { + + } + + public static class Builder implements Interceptor.Builder{ + + @Override + public Interceptor build() { + return new ETLInterceptor(); + } + + @Override + public void configure(Context context) { + + } + } + + } + ``` + +- build jar 包,推送到 `/usr/local/flume/lib` 目录下。 + +#### 2.3. 采集日志 + +- 确保 Hadoop 集群和 Kafka 正常运行。 + +- 在 hadoop103 `kafka/bin` 目录下创建 topic,挂一个消费者。 + + ```aidl + kafka-topics.sh --bootstrap-server hadoop102:9092 --alter --topic topic_log --partitions 3 + ``` + +- 在 hadoop102 启动 flume 将日志写入 kafka + + ```aidl + bin/flume-ng agent --name a1 --conf-file /usr/local/flume/job/file_to_kafka.conf -Dflume.root.logger=info,console + ``` + +[//]: # (- 如下即表示日志同步成功) + +[//]: # ( ```aidl) + +[//]: # ( ...) + +[//]: # ( 2022-05-21 18:02:22,016 INFO taildir.TaildirSource: r1 TaildirSource source starting with directory: {f1=/usr/local/applog/log/app.*}) + +[//]: # ( 2022-05-21 18:02:22,020 INFO taildir.ReliableTaildirEventReader: taildirCache: [{filegroup='f1', filePattern='/usr/local/applog/log/app.*', cached=true}]) + +[//]: # ( 2022-05-21 18:02:22,023 INFO taildir.ReliableTaildirEventReader: headerTable: {}) + +[//]: # ( 2022-05-21 18:02:22,029 INFO taildir.ReliableTaildirEventReader: Opening file: /usr/local/applog/log/app.2022-05-17.log, inode: 1453474, pos: 0) + +[//]: # ( 2022-05-21 18:02:22,031 INFO taildir.ReliableTaildirEventReader: Updating position from position file: /usr/local/flume/log_position.json) + +[//]: # ( 2022-05-21 18:02:22,037 INFO taildir.TailFile: Updated position, file: /usr/local/applog/log/app.2022-05-17.log, inode: 1453474, pos: 3192719) + +[//]: # ( 2022-05-21 18:02:22,040 INFO instrumentation.MonitoredCounterGroup: Monitored counter group for type: SOURCE, name: r1: Successfully registered new MBean.) + +[//]: # ( 2022-05-21 18:02:22,040 INFO instrumentation.MonitoredCounterGroup: Component type: SOURCE, name: r1 started) + +[//]: # ( 2022-05-21 18:04:22,052 INFO taildir.TaildirSource: Closed file: /usr/local/applog/log/app.2022-05-17.log, inode: 1453474, pos: 3192719) + +[//]: # ( ```) + +# 3. 消费日志 + +- kafka_to_hdfs.conf + + ```aidl + ## 组件 + a1.sources=r1 r2 + a1.channels=c1 c2 + a1.sinks=k1 k2 + + ## source1 + a1.sources.r1.type = org.apache.flume.source.kafka.KafkaSource + a1.sources.r1.batchSize = 5000 + a1.sources.r1.batchDurationMillis = 2000 + a1.sources.r1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092 + a1.sources.r1.kafka.topics=topic_start + + ## source2 + a1.sources.r2.type = org.apache.flume.source.kafka.KafkaSource + a1.sources.r2.batchSize = 5000 + a1.sources.r2.batchDurationMillis = 2000 + a1.sources.r2.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092 + a1.sources.r2.kafka.topics=topic_event + + ## channel1 + a1.channels.c1.type = file + a1.channels.c1.checkpointDir = /usr/local/flume/checkpoint/behavior1 + a1.channels.c1.dataDirs = /usr/local/flume/data/behavior1/ + a1.channels.c1.maxFileSize = 2146435071 + a1.channels.c1.capacity = 1000000 + a1.channels.c1.keep-alive = 6 + + ## channel2 + a1.channels.c2.type = file + a1.channels.c2.checkpointDir = /usr/local/flume/checkpoint/behavior2 + a1.channels.c2.dataDirs = /usr/local/flume/data/behavior2/ + a1.channels.c2.maxFileSize = 2146435071 + a1.channels.c2.capacity = 1000000 + a1.channels.c2.keep-alive = 6 + + ## sink1 + a1.sinks.k1.type = hdfs + a1.sinks.k1.hdfs.path = /origin_data/gmall/log/topic_start/%Y-%m-%d + a1.sinks.k1.hdfs.filePrefix = logstart- + a1.sinks.k1.hdfs.round = true + a1.sinks.k1.hdfs.roundValue = 10 + a1.sinks.k1.hdfs.roundUnit = second + + ## sink2 + a1.sinks.k2.type = hdfs + a1.sinks.k2.hdfs.path = /origin_data/gmall/log/topic_event/%Y-%m-%d + a1.sinks.k2.hdfs.filePrefix = logevent- + a1.sinks.k2.hdfs.round = true + a1.sinks.k2.hdfs.roundValue = 10 + a1.sinks.k2.hdfs.roundUnit = second + + ## 不要产生大量小文件 + a1.sinks.k1.hdfs.rollInterval = 10 + a1.sinks.k1.hdfs.rollSize = 134217728 + a1.sinks.k1.hdfs.rollCount = 0 + + a1.sinks.k2.hdfs.rollInterval = 10 + a1.sinks.k2.hdfs.rollSize = 134217728 + a1.sinks.k2.hdfs.rollCount = 0 + + ## 控制输出文件是原生文件。 + a1.sinks.k1.hdfs.fileType = CompressedStream + a1.sinks.k2.hdfs.fileType = CompressedStream + + a1.sinks.k1.hdfs.codeC = lzop + a1.sinks.k2.hdfs.codeC = lzop + + ## 拼装 + a1.sources.r1.channels = c1 + a1.sinks.k1.channel= c1 + + a1.sources.r2.channels = c2 + a1.sinks.k2.channel= c2 + ``` + +# 4. 存储日志 + +- 日志处理一般采用批处理方式,而不是绝对的实时处理; +- 日志的存储,根据处理时间间隔,可以按 天\小时\分钟 为单位创建目录,将日志文件存到对应的目录中,每次批处理的时候、处理一整个文件夹的日志文件; +- 文件夹名称,与 `kafka_to_hdfs.conf` 中的 `a1.sinks.k1.hdfs.path` 的值相对应。 \ No newline at end of file diff --git "a/_posts/2022-01-28-Flume\357\274\232Flume \345\256\211\350\243\205\346\255\245\351\252\244\347\244\272\344\276\213.md" "b/_posts/2022-01-28-Flume\357\274\232Flume \345\256\211\350\243\205\346\255\245\351\252\244\347\244\272\344\276\213.md" new file mode 100644 index 00000000000..2c25fab3247 --- /dev/null +++ "b/_posts/2022-01-28-Flume\357\274\232Flume \345\256\211\350\243\205\346\255\245\351\252\244\347\244\272\344\276\213.md" @@ -0,0 +1,84 @@ +--- +layout: post +title: Flume:Flume 安装步骤示例 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Flume +--- + +# 1. 准备工作 + +#### 1.1. 集群规划 + + ![]({{site.baseurl}}/img-post/flume-1.png) + + +#### 1.2. 下载 + +
  • 点击此处下载:https://dlcdn.apache.org/flume/1.9.0/apache-flume-1.9.0-bin.tar.gz
  • + +#### 1.3. 安装 + +- 解压缩文件,到常用软件安装位置。 + + ```aidl + tar tar -zxf apache-flume-1.9.0-bin.tar.gz + + # 移动文件到指定位置 + mv apache-flume-1.9.0-bin/ flume + ``` + +# 2. 修改配置 + +#### 2.1. 删除低版本 guava 包 + +- 在 flume 文件夹 `/lib` 目录下执行 + + ```aidl + cd /lib + mv guava-11.0.2.jar guava-11.0.2.jar_bak + ``` + +#### 2.2. 修改日志配置 + +- 在 flume 文件夹 `/conf` 目录下修改 `log4j.properties` + + ```aidl + # 指定日志文件的绝对路径 + flume.log.dir=/usr/local/flume/logs + ``` + +#### 2.3. 调整堆内存 + +- 在 `flume/conf/` 目录下编辑 `flume-env.sh` + + ``` + cd flume/conf/ + mv flume-env.sh.template flume-env.sh + + vim flume-env.sh + ``` + +- 修改内存: + - 最小 1G、最大 4G; + - 根据实际需要,尽可能多配一点防止挂掉; + + ``` + export JAVA_OPTS="-Xms1000m -Xmx4000m -Dcom.sun.management.jmxremote" + ``` + + - Xms,表示 JVM Heap(堆内存)最小尺寸,初始分配; + - Xmx,表示 JVM Heap(堆内存)最大允许的尺寸,按需进行分配。 + +#### 2.4. 分发文件 + +- xsync 分发文件 + + ```aidl + xsync flume/ + ``` + diff --git "a/_posts/2022-01-28-Flume\357\274\232Flume \351\233\266\347\202\271\346\274\202\347\247\273\351\227\256\351\242\230\345\217\212\350\247\243\345\206\263\346\226\271\346\263\225\347\244\272\344\276\213.md" "b/_posts/2022-01-28-Flume\357\274\232Flume \351\233\266\347\202\271\346\274\202\347\247\273\351\227\256\351\242\230\345\217\212\350\247\243\345\206\263\346\226\271\346\263\225\347\244\272\344\276\213.md" new file mode 100644 index 00000000000..dbf3ea6a65e --- /dev/null +++ "b/_posts/2022-01-28-Flume\357\274\232Flume \351\233\266\347\202\271\346\274\202\347\247\273\351\227\256\351\242\230\345\217\212\350\247\243\345\206\263\346\226\271\346\263\225\347\244\272\344\276\213.md" @@ -0,0 +1,94 @@ +--- +layout: post +title: Flume:Flume 零点漂移问题及解决方法示例 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Flume +--- + +#### 数据漂移问题 + +- 所谓零点漂移,就是在按天生成日志文件的情况下,一条23:59:59左右生成的日志发送到服务器后可能已经是第二天了,如果没有指定时间,会被写入第二天对应的文件中,这就是所谓的零点漂移。 + +- flume 写入日志到 HDFS 时,如果按照时间生成文件,在没有明确指定时间的情况下,会读取服务器时间作为创建文件的依据,这会导致日志的实际生成日期与文件不符。 + +#### 解决办法 + +- 这种情况下,可以通过拦截器在flume事件头指定timestamp作为文件的创建依据。 + +- 具体的通常是将日志中记录的日志创建时间提取出来,写入flume事件头的timestamp字段,有了这个字段,flume创建文件时,会依据这个字段创建文件,这种场景很类似spark、flink的事件事件和处理事件。 + + + +```aidl +@Override + public Event intercept(Event event) { + + Map headers = event.getHeaders(); + String log = new String(event.getBody(), StandardCharsets.UTF_8); + + JSONObject jsonObject = JSONObject.parseObject(log); + + String ts = jsonObject.getString("ts"); + headers.put("timestamp", ts); + + return event; + } +``` + +```aidl +import com.alibaba.fastjson.JSONObject; +import org.apache.flume.Context; +import org.apache.flume.Event; +import org.apache.flume.interceptor.Interceptor; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +public class TimeStampInterceptor implements Interceptor { + @Override + public void initialize() { + + } + @Override + public Event intercept(Event event) { + + Map headers = event.getHeaders(); + String log = new String(event.getBody(), StandardCharsets.UTF_8); + + JSONObject jsonObject = JSONObject.parseObject(log); + + String ts = jsonObject.getString("ts"); + headers.put("timestamp", ts); + + return event; + } + @Override + public List intercept(List events) { + + for (Event event : events) { + intercept(event); + } + + return events; + } + @Override + public void close() { + + } + public static class Builder implements Interceptor.Builder { + @Override + public Interceptor build() { + return new TimeStampInterceptor(); + } + @Override + public void configure(Context context) { + } + } +} +``` \ No newline at end of file diff --git "a/_posts/2022-01-28-Kafka\357\274\232Kafka \345\256\211\350\243\205\346\255\245\351\252\244\347\244\272\344\276\213.md" "b/_posts/2022-01-28-Kafka\357\274\232Kafka \345\256\211\350\243\205\346\255\245\351\252\244\347\244\272\344\276\213.md" new file mode 100644 index 00000000000..10956acd3bd --- /dev/null +++ "b/_posts/2022-01-28-Kafka\357\274\232Kafka \345\256\211\350\243\205\346\255\245\351\252\244\347\244\272\344\276\213.md" @@ -0,0 +1,215 @@ +--- +layout: post +title: Kafka:Kafka 安装步骤示例 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Kafka +--- + +# 1. 准备工作 + +#### 1.1. 集群规划 + + ![]({{site.baseurl}}/img-post/kafka.png) + +#### 1.2. 下载 + +
  • 点击此处下载:https://archive.apache.org/dist/kafka/2.4.1/kafka_2.12-2.4.1.tgz
  • + +#### 1.3. 安装 + +- 解压缩文件,到常用软件安装位置。 + + ```aidl + tar -zxvf kafka_2.12-2.4.1.tgz + + # 重命名为 kafka + mv kafka_2.12-2.4.1 kafka + ``` + +# 2. 修改配置 + +#### 2.1. 修改配置文件 + +- 修改 `server.properties` + + ```aidl + cd /kafka/config + vi server.properties + ``` + + ```aidl + # broker的全局唯一编号,不能重复 + broker.id=0 + + # 删除topic功能使能 + delete.topic.enable=true + + # 处理网络请求的线程数量 + num.network.threads=3 + + # 用来处理磁盘IO的现成数量 + num.io.threads=8 + + # 发送套接字的缓冲区大小 + socket.send.buffer.bytes=102400 + + # 接收套接字的缓冲区大小 + socket.receive.buffer.bytes=102400 + + # 请求套接字的缓冲区大小 + socket.request.max.bytes=104857600 + + # kafka运行日志存放的路径 + log.dirs=/usr/local/kafka/kafka-logs + + # topic在当前broker上的分区个数 + num.partitions=1 + + # 用来恢复和清理data下数据的线程数量 + num.recovery.threads.per.data.dir=1 + + # segment文件保留的最长时间,超时将被删除 + log.retention.hours=168 + + # 配置连接Zookeeper集群地址 + zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181/kafka + ``` + +#### 2.2. 配置环境变量 + +- 编辑 `/etc/profile` + + ```aidl + sudo vi /etc/profile + + #KAFKA_HOME + export KAFKA_HOME=/usr/local/kafka + export PATH=$PATH:$KAFKA_HOME/bin + ``` + +- 启动生效 + + ``` + source /etc/profile + ``` + +#### 2.3. 分发 kafka + +- 分发 kafka + + ```aidl + xsync kafka/ + ``` + +- 依次修改 hadoop103、hadoop104 的 `server.properties` + - hadoop103 + ```aidl + broker.id=1 + ``` + - hadoop104 + ```aidl + broker.id=2 + ``` + - 注:broker.id 不得重复。 + +#### 2.4. 分发 `/etc/profile` + +- 分发文件 + + ```aidl + xsync /etc/profile + ``` + +- 依次在 hadoop103、hadoop104 启动生效 + + ``` + source /etc/profile + ``` + + +# 3. 运行 + +#### 3.1. 依次启动 + +- 注意: + - **kafka 依赖于 zookeeper,需要先启动 zookeeper**。 + +- 在 hadoop102、hadoop103、hadoop104 上逐个启动 + + ```aidl + bin/kafka-server-start.sh config/server.properties & + ``` + +- 查看运行情况 + + ```aidl + javapsall + + + ========== hadoop102 ========= + 3222 QuorumPeerMain + 2470 DataNode + 3017 NodeManager + 2330 NameNode + 3325 Kafka + ========== hadoop103 ========= + 1490 Kafka + 1383 QuorumPeerMain + ========== hadoop104 ========= + 1588 Kafka + 1396 SecondaryNameNode + 1476 QuorumPeerMain + ``` + +#### 3.2. 依次关闭 + +- 逐个关闭 + ``` + bin/kafka-server-stop.sh stop + + + ... + [2022-05-16 22:50:58,864] INFO [SocketServer brokerId=0] Shutdown completed (kafka.network.SocketServer) + [2022-05-16 22:50:58,868] INFO [KafkaServer id=0] shut down completed (kafka.server.KafkaServer) + ``` + +- 注意: + - **关闭 kafaka 之后,需要等一会才能关闭,如果关闭 kafka 以后立刻关闭 zookeeper 可能会无法关闭成功**。 + +#### 3.3. kafka 群启群停 + +- 进入 `/usr/local/apache-zookeeper-3.5.7/bin` 目录 + + ``` + #!/bin/bash + + case $1 in + "start" ){ + for(( i = 2;i <= 4;i = $i +1));do + echo ============ hadoop10$i kafka $1 =================== + ssh hadoop10$i "source /etc/profile;nohup /usr/local/kafka/bin/kafka-server-start.sh /usr/local/kafka/config/server.properties" + done + };; + "stop" ){ + for(( i = 2;i <= 4;i = $i +1));do + echo ============ hadoop10$i kafka $1 =================== + ssh hadoop10$i "source /etc/profile;kafka-server-stop.sh" + done + };; + esac + ``` + +- 集群启动 + ``` + kafka.sh start + ``` +- 集群关闭 + ``` + kafka.sh stop + ``` + diff --git "a/_posts/2022-01-28-Maxwell\357\274\232Maxwell \345\212\237\350\203\275\344\273\213\347\273\215\345\217\212\344\275\277\347\224\250\347\244\272\344\276\213.md" "b/_posts/2022-01-28-Maxwell\357\274\232Maxwell \345\212\237\350\203\275\344\273\213\347\273\215\345\217\212\344\275\277\347\224\250\347\244\272\344\276\213.md" new file mode 100644 index 00000000000..6148a59d4f5 --- /dev/null +++ "b/_posts/2022-01-28-Maxwell\357\274\232Maxwell \345\212\237\350\203\275\344\273\213\347\273\215\345\217\212\344\275\277\347\224\250\347\244\272\344\276\213.md" @@ -0,0 +1,278 @@ +--- +layout: post +title: Maxwell:Maxwell 功能介绍及使用示例 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Maxwell +--- + + +# 1. Maxwell 介绍 + +- Maxwell 是由美国 Zendesk 开源,使用 Java 编写的 MySQL 实时抓取工具,可以实时读取 MySQL 二进制日志 binlog,并生成 JSON 格式的消息,作为生产者发送给 Kafka,Kinesis、RabbitMQ、Redis、Google Cloud Pub/Sub、文件或其它平台的应用程序。 +- Maxwell 设计的初衷是监听 Mysql 的 binlog 日志,实时采集 Mysql 数据,并且把解析的 json 格式数据发送到到 Kafka。 +- Maxwell 支持全表 load 数据,支持自动断点还原,支持按照列将数据发送到 Kafka 不同分区。 + + +# 2. Maxwell工作原理 + +#### 2.1. Maxwell 把自己伪装成 MySQL 的 slave 从库 + +- Maxwell 工作原理与 Canal 工作原理一样,都是把自己伪装成 MySQL 的 slave 从库,同步 binlog 数据,来达到同步 MySQL 数据,与 Canal 相比,更加轻量。 + +- 使用 Maxwell,需要开启 MySQL binlog 日志。 + + - 二进制日志(Binlog)是MySQL服务端非常重要的一种日志,它会保存MySQL数据库的所有数据变更记录。Binlog的主要作用包括主从复制和数据恢复。 + +#### 2.2. Maxwell & MySQL 主从复制 + +- Maxwell 的工作原理和主从复制密切相关。 + +- MySQL的主从复制,就是用来建立一个和主数据库完全一样的数据库环境,这个数据库称为从数据库。 + +- 主从复制的应用场景如下: + - 做数据库的热备: + - 主数据库服务器故障后,可切换到从数据库继续工作。 + - 读写分离: + - 主数据库只负责业务数据的写入操作,而多个从数据库只负责业务数据的查询工作,在读多写少场景下,可以提高数据库工作效率。 + +- 主从复制的工作原理如下: + - Master主库将数据变更记录,写到二进制日志(binary log)中 + - Slave从库向mysql master发送dump协议,将master主库的binary log events拷贝到它的中继日志(relay log) + - Slave从库读取并回放中继日志中的事件,将改变的数据同步到自己的数据库。 + +# 3. Maxwell 使用示例 + +#### 3.1. 环境 & 版本准备 + +- 版本 + - maxwells : 1.2.X 以上 + - mysql : 5.1,5.5,5.6,5.7 + - jdk : 1.8 以上 + - 操作系统 : centos7 + +#### 3.2. Mysql 配置 + +- 编辑 conf + + ```aidl + [mysqld] + + #数据库id + server-id = 1 + #启动binlog,该参数的值会作为binlog的文件名 + log-bin=mysql-bin + #binlog类型,maxwell要求为row类型 + binlog_format=row + #启用binlog的数据库,需根据实际情况作出修改 + binlog-do-db=gmall + ``` +- 创建库和用户 + + ``` + 创建库和用户以及相应的权限 + msyql> CREATE DATABASE maxwell; + mysql> CREATE USER 'maxwell'@'%' IDENTIFIED BY 'maxwell'; + mysql> GRANT ALL ON maxwell.* TO 'maxwell'@'%'; + mysql> GRANT SELECT, REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO 'maxwell'@'%'; + ``` + +#### 3.3. 配置 Maxwell + +- 配置 `config.properties` + ``` + cp config.properties.example config.properties + ``` + + ``` + # Maxwell数据发送目的地,可选配置有stdout|file|kafka|kinesis|pubsub|sqs|rabbitmq|redis + producer=kafka + #目标Kafka集群地址 + kafka.bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 + #目标Kafka topic,可静态配置,例如:maxwell,也可动态配置,例如:%{database}_%{table} + kafka_topic=maxwell + + # MySQL相关配置 + host=hadoop102 + user=maxwell + password=maxwell + jdbc_options=useSSL=false&serverTimezone=Asia/Shanghai + ``` + +- `mysql options` + - `host` + - 指定从哪个地址的mysql获取binlog + +- `replication_host` + - 如果指定了 `replication_host`,那么它是真正的 `binlog` 来源的 `mysql server` 地址,而那么上面的 `host` 用于存放 `maxwell` 表结构和 `binlog` 位置的地址。 + - 将两者分开,可以避免 replication_user 往生产库里写数据。 +- `schema_host` + - 从哪个 host 获取表结构。 + - binlog 里面没有字段信息,所以 maxwell 需要从数据库查出 schema,存起来。 + - schema_host 一般用不到,但在 binlog-proxy 场景下就很实用。 + - 比如:要将已经离线的 binlog 通过 maxwell 生成 json 流,由于 mysql server 里面没有结构,只用于发送 binlog,此时表机构就可以制动从 schema_host 获取。 +- `gtid_mode` + - 如果 mysql server 启用了 GTID,maxwell 也可以基于 gtid 取 event。 + - 如果 mysql server 发生 failover,maxwell 不需要手动指定 newfile:postion + +>正常情况下,replication_host 和 schema_host都不需要指定,只有一个 --host。 + +- `schema_database` + - 使用这个 db 来存放 maxwell 需要的表,比如要复制的 databases, tables, columns, postions, heartbeats。 + +- `filtering` + - `include_dbs` + - 只发送 binlog 里面这些 databases 的变更,以,号分隔,中间不要包含空格。 + - 也支持 java 风格的正则,如 include_tables=db1,/db\\d+/,表示 db1, db2, db3…这样的。(下面的filter都支持这种regex) + - 提示:这里的dbs指定的是真实db。 + - 比如: + - binlog里面可能 use db1 但 update db2.ttt,那么maxwell生成的json database 内容是db2。 + - `exclude_dbs` + - 排除指定的这些 databbases + - `include_tables` + - 只发送这些表的数据变更。不只需要指定 database. + - `exclude_tables` + - 排除指定的这些表 + - `exclude_columns` + - 不输出这些字段。 + - 如果字段名在 row 中不存在,则忽略这个 filter。 + - `include_column_values` + - 1.12.0新引入的过滤项,只输出满足 `column=values` 的行; + - 比如: + - `include_column_values=bar=x,foo=y`,如果有 `bar` 字段,那么只输出值为 `x` 的行,如果有 `foo` 字段,那么只输出值为 `y` 的行。 + 如果没有对应字段,如只有bar=x没有foo字段,那么也成立。(即不是 或,也不是 与) + + - `blacklist_dbs` + - 一般不用。 + - `blacklist_dbs` 字面上难以与 `exclude_dbs` 分开,官网的说明也是模棱两可。 + + - 注意这些 `include` 与 `exclude` 的关系,记住三点: + - 只要 include 有值,那么不在include里面的都排除 + - 只要在 exclude 里面的,都排除 + - 其它都正常输出 + +- `formatting` + - `output_ddl` + - 是否在输出的 json 流中,包含 ddl 语句,默认 false。 + - `output_binlog_position` + - 是否在输出的 json 流中,包含 binlog filename:postion,默认 false。 + - `output_commit_info` + - 是否在输出的 json 流里面,包含 commit 和 xid 信息,默认 true。 + - 比如一个事物里,包含多个表的变更,或一个表上多条数据的变更,那么他们都具有相同的 xid,最后一个row event输出 commit:true 字段。这有利于消费者实现 事务回放,而不仅仅是行级别的回放。 + - `output_thread_id` + - binlog 里面也包含了 thread_id ,可以包含在输出中,默认 false。 + - 消费者可以用它来实现更粗粒度的事务回放。还有一个场景是用户审计,用户每次登陆之后将登陆ip、登陆时间、用户名、thread_id记录到一个表中,可轻松根据thread_id关联到binlog里面这条记录是哪个用户修改的。 + - `monitoring` + - 如果是长时间运行的 maxwell,添加 monitor 配置,maxwell 提供了 http api 返回监控数据。 + +- `init_position` + - 手动指定 maxwell 要从哪个 binlog,哪个位置开始。 + - 指定的格式 `FILE:POSITION:HEARTBEAT`。 + - 只支持在启动maxwell的命令指定,比如 `--init_postion=mysql-bin.0000456:4:0`。 + - maxwell 默认从连接上 mysql server 的当前位置开始解析,如果指定 init_postion,要确保文件确实存在,如果 binlog 已经被 purge 掉了,可能需要想其它办法。 + +#### 3.4. Maxwell 启动关闭 + +- 启动关闭 .sh 脚本 + + ```aidl + #!/bin/bash + + MAXWELL_HOME=/usr/local/maxwell + + status_maxwell(){ + result=`ps -ef | grep maxwell | grep -v grep | wc -l` + return $result + } + start_maxwell(){ + status_maxwell + if [[ $? -eq 0 ]]; then + echo "启动maxwell" + $MAXWELL_HOME/bin/maxwell --config $MAXWELL_HOME/config.properties --daemon + else + echo "maxwell正在运行" + fi + } + + stop_maxwell(){ + status_maxwell + if [[ $? -eq 1 ]]; then + echo "关闭maxwell" + ps -ef | grep maxwell | grep -v grep | awk '{print $2}' | xargs kill -9 + else + echo "maxwell已经关闭" + fi + } + case $1 in + start ) + start_maxwell + ;; + stop ) + stop_maxwell + ;; + restart ) + stop_maxwell + start_maxwell + ;; + esac + ``` + +#### 3.5. 使用 kafka 作为消息队列 + +- kafka是maxwell支持最完善的一个producer,并且内置了 多个版本的 kafka client(0.8.2.2, 0.9.0.1, 0.10.0.1, 0.10.2.1 or 0.11.0.1),默认 kafka_version=0.11.0.1 + +- 开启 kafka consumer + + ```aidl + bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic maxwell + ``` + +- Maxwell提供了 bootstrap 功能来进行历史数据的全量同步 + + ```aidl + { + "database": "fooDB", + "table": "barTable", + "type": "bootstrap-start", + "ts": 1450557744, + "data": {} + } + { + "database": "fooDB", + "table": "barTable", + "type": "bootstrap-insert", + "ts": 1450557744, + "data": { + "txt": "hello" + } + } + { + "database": "fooDB", + "table": "barTable", + "type": "bootstrap-insert", + "ts": 1450557744, + "data": { + "txt": "bootstrap!" + } + } + { + "database": "fooDB", + "table": "barTable", + "type": "bootstrap-complete", + "ts": 1450557744, + "data": {} + } + ``` +- 注意: + - 第一条 `type` 为 `bootstrap-start` 和最后一条 `type` 为 `bootstrap-complete` 的数据,是 `bootstrap` 开始和结束的标志,不包含数据,中间的 `type` 为 `bootstrap-insert` 的数据才包含数据。 + - bootstrap 输出的所有记录的ts都相同,为 bootstrap 开始的时间。 + +- maxwell 的json数据说明 + - http://maxwells-daemon.io/dataformat/ + +- 参考过滤配置: + - http://maxwells-daemon.io/filtering/ diff --git "a/_posts/2022-01-28-Zookeeper\357\274\232Zookeeper\345\256\211\350\243\205\346\255\245\351\252\244\347\244\272\344\276\213.md" "b/_posts/2022-01-28-Zookeeper\357\274\232Zookeeper\345\256\211\350\243\205\346\255\245\351\252\244\347\244\272\344\276\213.md" new file mode 100644 index 00000000000..4e76316791d --- /dev/null +++ "b/_posts/2022-01-28-Zookeeper\357\274\232Zookeeper\345\256\211\350\243\205\346\255\245\351\252\244\347\244\272\344\276\213.md" @@ -0,0 +1,340 @@ +--- +layout: post +title: Zookeeper:Zookeeper安装步骤示例 +subtitle: +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Zookeeper +--- + +# 1. 准备工作 + +#### 1.1. 集群规划 + + ![]({{site.baseurl}}/img-post/zookeeper.png) + +#### 1.2. 下载 + +
  • 点击此处下载:apache-zookeeper-3.5.7-bin.tar.gz
  • + +#### 1.3. 安装 + +- 解压缩文件,到常用软件安装位置。 + + ```aidl + tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz + # 移动文件到指定位置 + cp -r ./apache-zookeeper-3.5.7-bin/* /指定目录/zookeeper-3.5.7 & rm -rf apache-zookeeper-3.5.7-bin + ``` + +# 2. 修改配置 + +#### 2.1. 配置 dataDir 和 myid + +- 创建 `zkData` 目录 + + ```aidl + cd /指定目录/zookeeper-3.5.7 + mkdir zkData + cd zkData + ``` + +- 修改 myid 为 1 + + ```aidl + vim myid + ``` + ```aidl + 1 + ``` +- 保存退出 + +- 编辑 cfg 文件 + + ```aidl + cd ../conf + mv zoo_sample.cfg zoo.cfg + vim zoo.cfg + ``` + +- 修改以下内容: + + ```aidl + dataDir=/指定目录/zookeeper-3.5.7/zkData + ``` + + ```aidl + #######################cluster########################## + + server.1=hadoop102:2888:3888 + + server.2=hadoop103:2888:3888 + + server.3=hadoop104:2888:3888 + ``` + +- 代码含义: + - `server.A=B:C:D` + - A,第几号服务器; + - B,服务器 ip 地址; + - C,服务器与集群 Leader 交换信息的端口; + - D,执行选举时服务器相互通信的端口。` + + +#### 2.2. 分发到 hadoop103 和 hadoop104 + +- xsync 分发 + + ```aidl + sudo xsync zookeeper-3.5.7/ + ``` + +#### 2.3. 修改 hadoop103 和 hadoop104 的设置 + +- hadoop103 + + ```aidl + # 链接 hadoop103 + ssh hadoop103 + + # 获取 root 权限 + su + + # 编辑 myid + vim /指定目录/zookeeper-3.5.7/zkData/myid + + 2 + + # 保存退出 + ``` +- hadoop104 + + ```aidl + # 链接 hadoop104 + ssh hadoop104 + + # 获取 root 权限 + su + + # 编辑 myid + vim /指定目录/zookeeper-3.5.7/zkData/myid + + 3 + + # 保存退出 + ``` +# 3. 运行 + +#### 3.1. 启动 + +- 在 hadoop102、hadoop103、hadoop104 上逐个启动 + + ```aidl + cd /zookeeper-3.5.7/bin + ./zkServer.sh start + + # 启动成功 + ZooKeeper JMX enabled by default + Using config: /usr/local/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg + Starting zookeeper ... STARTED + ``` + +#### 3.2. 关闭 + +- 逐个关闭 + ``` + cd /bin + ./zkServer.sh stop + + # 关闭成功 + ZooKeeper JMX enabled by default + Using config: /usr/local/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg + Stopping zookeeper ... STOPPED + ``` + +#### 3.3. Zookeeper 群启群停 + +- 进入 `/usr/local/apache-zookeeper-3.5.7/bin` 目录 + ``` + cd /usr/local/apache-zookeeper-3.5.7/bin + ``` +- 编辑 zkEnv.sh + ``` + vim zkEnv.sh + ``` + + 找到下面这段代码: + + ``` + if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then + JAVA="$JAVA_HOME/bin/java" + elif type -p java; then + JAVA=java + else + echo "Error: JAVA_HOME is not set and java could not be found in PATH." 1>&2 + exit 1 + fi + ``` + + 在上面这段代码前,添加 `JAVA_HOME` + + ```aidl + JAVA_HOME="/usr/local/bin/jdk1.8" + ``` +- 分发 `zkEnv.sh` + ``` + xsync ./zkEnv.sh + ``` +- 编辑 `zk.sh` + + ``` + vim zk.sh + ``` + + ```aidl + #!/bin/bash + + case $1 in + "start"){ + for i in hadoop102 hadoop103 hadoop104 + do + ssh $i "/usr/local/apache-zookeeper-3.5.7/bin/zkServer.sh start" + done + };; + "stop"){ + for i in hadoop102 hadoop103 hadoop104 + do + ssh $i "/usr/local/apache-zookeeper-3.5.7/bin/zkServer.sh stop" + done + };; + "status"){ + for i in hadoop102 hadoop103 hadoop104 + do + ssh $i "/usr/local/apache-zookeeper-3.5.7/bin/zkServer.sh status" + done + };; + esac + ``` + + ```aidl + # 修改权限 + chmod 777 zk.sh + ``` +- 集群启动 Zookeeper + + ``` + ./zk.sh start + + + ZooKeeper JMX enabled by default + Using config: /usr/local/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg + Starting zookeeper ... STARTED + ZooKeeper JMX enabled by default + Using config: /usr/local/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg + Starting zookeeper ... already running as process 7970. + ZooKeeper JMX enabled by default + Using config: /usr/local/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg + Starting zookeeper ... STARTED + ``` + +# 4. Zookeeper 常用命令 + +- 命令基本语法及功能描述 + + - `bin/zkCli.sh help`,显示所有操作命令 + + - `bin/zkCli.sh ls path [watch]`,使用 ls 命令来查看当前znode中所包含的内容 + + - `bin/zkCli.sh ls2 path [watch]`,查看当前节点数据并能看到更新次数等数据 + + - `bin/zkCli.sh create`,普通创建 + + - `bin/zkCli.sh -s`,含有序列 + + - `bin/zkCli.sh -e`,临时(重启或者超时消失) + + - `bin/zkCli.sh get path [watch]`,获得节点的值 + + - `bin/zkCli.sh set`,设置节点的具体值 + + - `bin/zkCli.sh stat`,查看节点状态 + + - `bin/zkCli.sh delete`,删除节点 + + - `bin/zkCli.sh rmr`,递归删除节点 + +# 5. 常见错误 + +#### 5.1. 启动 zookeeper 时报错:`Error contacting service. It is probably not running` + +- 原因分析: + - myid 配置错误。 + +- 解决方法: + + - 重新编辑 `vim zoo.cfg` + + ```aidl + vim zoo.cfg + ``` + + ``` + #######################cluster########################## + + server.1=hadoop102:2888:3888 + + server.2=hadoop103:2888:3888 + + server.3=hadoop104:2888:3888 + ``` + + - 重新编辑 `/zkData` 目录下的 `myid` 文件: + + - hadoop102: 1; + - hadoop103: 2; + - hadoop104: 3。 + + - 保存退出后重新启动 zookeeper。 + +#### 5.2. `zk.sh start` 时报错:`Zookeeper JAVA_HOME is not set and java could not be found in PATH` + +- 原因分析: + - `zkEnv.sh` 中未配置 JAVA_HOME。 + +- 解决方法: + - 进入 `/usr/local/apache-zookeeper-3.5.7/bin` 目录 + ``` + cd /usr/local/apache-zookeeper-3.5.7/bin + ``` + - 编辑 zkEnv.sh + + ``` + vim zkEnv.sh + ``` + + - 找到下面这段代码: + + ``` + if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then + JAVA="$JAVA_HOME/bin/java" + elif type -p java; then + JAVA=java + else + echo "Error: JAVA_HOME is not set and java could not be found in PATH." 1>&2 + exit 1 + fi + ``` + + - 在上面这段代码前,添加 `JAVA_HOME` + + ```aidl + JAVA_HOME="/usr/local/bin/jdk1.8" + ``` + - 分发 `zkEnv.sh` + + ``` + xsync ./zkEnv.sh + ``` diff --git "a/_posts/2022-01-28-\344\273\273\345\212\241\350\260\203\345\272\246\357\274\232\344\273\273\345\212\241\350\260\203\345\272\246\345\267\245\345\205\267\345\257\271\346\257\224.md" "b/_posts/2022-01-28-\344\273\273\345\212\241\350\260\203\345\272\246\357\274\232\344\273\273\345\212\241\350\260\203\345\272\246\345\267\245\345\205\267\345\257\271\346\257\224.md" new file mode 100644 index 00000000000..cc081726dc9 --- /dev/null +++ "b/_posts/2022-01-28-\344\273\273\345\212\241\350\260\203\345\272\246\357\274\232\344\273\273\345\212\241\350\260\203\345\272\246\345\267\245\345\205\267\345\257\271\346\257\224.md" @@ -0,0 +1,100 @@ +--- +layout: post +title: 任务调度:任务调度工具对比 +subtitle: DolphinScheduler vs Airflow vs Azkaban vs Oozie +date: 2022-01-28 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 任务调度 +--- + + +#### 综合对比 + +![]({{site.baseurl}}/img-post/任务调度-2.jpg) + + +#### DolphinScheduler + +- Apache DolphinScheduler 是一个分布式去中心化,易扩展的可视化DAG工作流任务调度系统。致力于解决数据处理流程中错综复杂的依赖关系,使调度系统在数据处理流程中开箱即用。 + +- DolphinScheduler 特点 + + - 高可靠性 + - 去中心化的多 Master 和多 Worker, 自身支持 HA 功能, 采用任务队列来避免过载,不会造成机器卡死 + + - 简单易用 + - DAG 监控界面,所有流程定义都是可视化,通过拖拽任务定制DAG; + - 通过 API 方式与第三方系统对接,方便一键部署; + + - 丰富的使用场景 + - 支持暂停恢复操作. 支持多租户,更好的应对大数据的使用场景. 支持更多的任务类型,如 spark, hive, mr, python, sub_process, shell + + - 高扩展性 + - 支持自定义任务类型,调度器使用分布式调度,调度能力随集群线性增长,Master和Worker支持动态上下线 + +- 主要能力 + + - Task 以 DAG 形式关联,实时监控任务的状态; + - 支持 Shell、MR、Spark、SQL、依赖等 10 多种任务类型; + - 工作流优先级、任务优先级,全局参数及局部自定义参数; + - 工作流可定时、依赖、手动、暂停/停止/恢复; + - 支持补数、多租户、日志在线查看及资源在线管理; + - 完善的系统服务监控,任务超时告警/失败; + - 去中心化设计确保系统的稳定、高可用; + - 支持每日十万数据量级任务稳定运行。 + +#### Airflow + +- Airflow 优点 + - 与所有其他解决方案相比,Airflow 是一种功能超强的引擎,可以使用插件来支持各种作业,包括数据处理作业:Hive、Pig、以及通过文件/ db entry / s3来触发的一般流程管理,或者等待来自Web端点的预期输出, + - Airflow 提供了一个很好的 UI,允许: + - 查看 log + - 重跑历史 task + - 查看 task 代码 + - 监视作业的实时执行 + - Airflow 易于实现分布式任务分发的扩展; + - 可以使用本地执行程序通过单个节点运行所有作业,或通过 Celery / Dask / Mesos 编排将它们分发到一组工作节点。 + +- 缺点 + - Airflow 本身仍然不是很成熟(实际上Oozie可能是这里唯一的“成熟”引擎),调度程序需要定期轮询调度计划并将作业发送给执行程序; + - 由于有一个集中式调度程序,如果它出现故障或卡住,的正在运行的作业将不会像执行程序的作业那样受到影响,但是不会安排新的作业了。 + - 当使用 HA 设置运行时,这尤其令人困惑,其中有多个 Web 节点,调度程序,代理(通常是Celery案例中的消息队列),多个执行程序。 + - 当调度程序因任何原因而卡住时,在Web UI中看到的所有任务都在运行,但实际上它们实际上并没有向前运行,而执行程序却高兴地报告它们没问题。 + - 分布式系统中代码同步问题 + - Airflow 是分布式任务分发的系统, master 和 worker 会部署在不同的机器上,并且 worker 可以有很多的类型和节点。 + - 当 master 与 worker code 不一致时,会引入一些奇怪的问题,所以需要解决分布式系统中代码升级与同步的问题。 + + + +#### Azkaban + +- 优点 + - 在所有引擎中,Azkaban 可能是最容易开箱即用的; + - Azkaban UI 非常直观且易于使用; + - Azkaban 调度和 REST API 工作得很好; + - 有限的 HA 设置开箱即用; + - 不需要负载均衡器,因为只能有一个Web节点; + - 可以配置如何选择执行程序节点,只要有足够的容量来执行程序节点,就可以轻松运行数万个作业。 + +- 缺点 + - 作为通用编排引擎,Azkaban 没有非常丰富的功能; + - Azkaban 的优势在于对 Hadoop / Pig / Hive 的原生支持; + - 尽管也可以使用命令行实现这些功能,但 Azkaban 本身不能通过 Airflow 等外部资源触发工作,也不支持工作等待模式; + - Azkaban 文档和配置通常有点混乱,不应该推荐为初学者使用,设计很好但是最好有一个大型数据中心来运行执行程序,因为当执行程序耗尽资源而没有额外的监视功能时,调度会停止。 + - Azkaban 整体代码质量有点朝向低端,所以它通常只有在资源不成问题时才能很好地扩展。 + + +#### Oozie + +- 优点 + - Oozie 通过 db 设置提供了一个看似可靠的 HA 模型(貌似b / c我没有看到它),它为 Hadoop 相关工作提供本机支持,因为它是为该生态系统构建的。 + +- 缺点 + - 对于通用流程调度而言,不是一个非常好的候选者,因为XML定义对于定义轻量级作业非常冗长和繁琐; + - Oozie 还需要相当多的外设设置: + - 需要一个 zookeeper 集群,一个db,一个负载均衡器; + - 每个节点都需要运行像 Tomcat 这样的 Web 应用程序容器; + - 初始设置也需要一些时间,这对初次使用的用户来说是不友好的。 \ No newline at end of file diff --git "a/_posts/2022-01-29-Elasticsearch\357\274\232ES \345\200\222\346\216\222\347\264\242\345\274\225\344\270\272\344\273\200\344\271\210\346\237\245\350\257\242\351\200\237\345\272\246\344\274\232\350\277\231\344\271\210\345\277\253.md" "b/_posts/2022-01-29-Elasticsearch\357\274\232ES \345\200\222\346\216\222\347\264\242\345\274\225\344\270\272\344\273\200\344\271\210\346\237\245\350\257\242\351\200\237\345\272\246\344\274\232\350\277\231\344\271\210\345\277\253.md" new file mode 100644 index 00000000000..52e0aa3bd37 --- /dev/null +++ "b/_posts/2022-01-29-Elasticsearch\357\274\232ES \345\200\222\346\216\222\347\264\242\345\274\225\344\270\272\344\273\200\344\271\210\346\237\245\350\257\242\351\200\237\345\272\246\344\274\232\350\277\231\344\271\210\345\277\253.md" @@ -0,0 +1,145 @@ +--- +layout: post +title: Elasticsearch:ES 倒排索引为什么查询速度会这么快 +subtitle: +date: 2022-01-29 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Elasticsearch +--- + + +# 0. 前言 + +Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,它建立在全文搜索引擎 Apache Lucene™ 的基础上。Elasticsearch 之所以可以实现近乎实时的检索,依靠的技术手段是非常多的,本文将从 反向索引、Term Index 两块知识点入手,分析 Elasticsearch 之所以那么快的原因。 + +# 1. 反向索引 + +#### 1.1. 正向索引 + +- 什么是正向索引 + - 在解释反向索引之前,需要先了解一下什么是正向索引,这也将解释倒排索引之所以会出现的背景原因。 + - 所谓正向索引,其结构就是每个文档和关键字做关联,每个文档都有与之对应的关键字,记录关键字在文档中出现的位置和次数,用户查询的时候是根据关键字去查询的。 + - 例如,搜索引擎在处理网页文档的时候,会从网页中提取出来一些特殊关键词,比如文章标题、文本内容、作者、单位、网页阅读数、点赞数、评论数等等,同时统计关键词出现的次数、位置、格式等信息,之后将这些提取的词组成一个集关键词合。 + - 这样,网页文档与关键词之间的映射关系是 **一个网页文件对应多个关键词**,被存储进搜索引擎的索引库,其所实现的是一个 **“文档-关键词”** 矩阵, 如下表展示: + + | 网页 | 关键词 | + | --- | --- | + | web_1 | 马云, 马云, 马化腾,... | + | web_2 | 马云, 汽车, 飞机, ... | + | ... | ... | + | web_x_1 | 火星, 金星, ... | + | ... | ... | + | web_x_2 | 火星, 火星, 木星, ... | + | ... | ... | + | web_n | ... | + +在上表所展示的 **“文档-关键词”** 矩阵索引中,如果用户使用搜索引擎查找目标关键字(比如 **火星**),搜索引擎就会从索引库中所有的关键字包含 **火星** 的文档,也就是 **web_x_1、web_x_2**,并根据网页文件自身的价值评分高低(比如关键词出现的次数)按顺序展示给用户,用户得到的就是 **按顺序** 展示的 **web_x_2**、**web_x_1** 两个网页。这就是正向索引实现的大致流程。 +- 正向索引的问题 + - 这个过程存在一个致命的问题,那就是在海量的网页里找到所有的关键词包含 **火星** 的网页,将是一个漫长的过程,在上面的例子中,如果总共有 1000亿个网页,并且有100万个包含火星,那么需要的计算量是惊人的。 + - 实际上,受制于时间、内存、处理器等等资源的限制,技术上正向索引是不能实现的。 + - 在这种背景下,倒排索引出现了。 + +#### 1.2. 反向索引 + +- 什么是反向索引 + 反向索引(Inverted Index),也叫倒排索引,相比于正向索引,其采用的是 **“关键词-文档”** 矩阵,关键词与网页文档之间的映射关系是 **一个关键词对应多个网页文档**,如下图所示: + + | 关键词 | 网页 | + | --- | --- | + | 马云 | web_1, web_2 | + | 马化腾 | web_1 | + | 汽车 | web_2 | + | 飞机 | web_2 | + | ... | ... | + | 火星 | web_x_2, web_x_1 | + | ... | ... | + | 金星 | web_x_1| + | 木星 | web_x_2 | + | ... | ... | + +- 在这个矩阵中,**火星** 关键词对应的所有网页都被提前找到,甚至网页文档的权重都被提前计算好并排序,当用户输入 **火星** 关键词时,就会立刻到 web_x_2, web_x_1 的反馈结果。 + +- 这里有些人会有疑问,关键词数量会不会太多,以至于超过网页问的数量,这样效率不会反而变低了么,其实不然,人类的语言词汇数量是相对有限、且固定的,但网页数量却没有上限。比如汉语中,汉字30000个、词汇大概40万,但汉语网站数量却远远不止这么些。 + +- 需要注意的是,由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂,但是在查询的时候由于可以一次得到查询关键字所对应的所有文档,所以效率高于正排表。 + +# 2. Term Index + +#### 2.1. Term & Posting List + +- 在上面的例子中,创建的是一个简单的反向索引,但 Elasticsearch 的索引结构实际上并不表现为这种形态,首先网页文档在索引中只表现为一个 int 类型的 **文档di**,而且索引中用于查找网页文档的只有 **关键词** 一项,实际我们在做业务中需要面对的情况要复杂得多。 + 下面展示了一个商品信息的二维数据表样例: + + | id | Name | Color | Rate | + | --- | --- | --- | --- | + | 1 | iphone 666 plus | black | high | + | 2 | Huawei mate 98k | blue | high | + | 3 | Chuzi game over | black | middle | + +- 针对这个表,Elasticsearch 会创建如下的索引: + + - 索引一:**Name** + + | Term | Posting List | + | --- | --- | + | iphone 666 plus | 1 | + | Hauwei mate 98k | 2 | + | Chuzi game over | 3 | + + - 索引二:**Color** + + | Term | Posting List | + | --- | --- | + | black | [1, 3] | + | blue | 2 | + + - 索引三:**Rate** + + | Term | Posting List | + | --- | --- | + | high | [1, 2] | + | middle | 3 | + +- 在这个索引中,Name、Color、Rate 这些字段被称为 **filed**, iphone 666 plus、blue、middle 这些被称作 **Term**,而 Term 对应的所有商品的 id 比如 [1, 3] 就是 **Posting List**。 +- 当用户要查找 **Color=blue** 的商品时,通过索引三的 Term 和 Posting List 很快就可以找到,目标是 id 为 2 的商品,进而通过索引一找到商品 Name 为 华为 mate 98k。 + +#### 2.2. Term Dictionary + +- 上面简单解释了 Term 和 Posting List,但实际生产中 Elasticsearch 需要面对的是数以亿计的数据记录,数据的 Term 的数量是惊人的,这样往往需要花费大量时间才能命中,而且多数时候查找是多条件查找,这就需要多次进行重复查找,效率仍然不高。 +- 这时就需要对 Term 进行优化排序,即使用 [二分查找](https://baike.baidu.com/item/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/10628618?fr=aladdin) 查找 Term,这种查找方法类似于通过字典查找,被称为 Term Dictionary 。 +- 同样是上面的例子,Name、Color、Rate 三个索引下所有的 Term,按照 **首字母在英语字母表中位置** 排序后如下: + + | Term | Posting List| + | --- | --- | + | black | [1, 3] | + | blue | 2 | + | Chuizi game over | 3| + | high | [1, 2] | + | Huawei mate 98k | 2 | + | iphone 666 plus| 1 | + | middle| 3 | + +当用户想要查找 rate 为 high 的商品时,通过二分法很快就可以查到,查找过程的时间复杂度为 log N,这样就大大提高了查找的速度。关于二分查找,细节这里就不做赘述了,如果不清楚的朋友们可以自行百度,或点击 [二分查找](https://baike.baidu.com/item/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/10628618?fr=aladdin) 获取更多信息。 + +#### 2.3. Term Index + +- 到这里很多人会有疑问,那这和传统的 B-tree 有什么区别呢,这就需要引入另一个概念 Term Index。 +- Term Index 其实也可以理解为一个树形结构,从 Term 的第一个字母开始进行第一层排序,如果有多个 Term 首字母相同,则从该字母为起始点进行第二层排序,如果以该字母为首的只有一个 Term,则不再进行第二次排序。 +- 同样是上面的例子,其 Term Index 如下图所示: + + ![]({{site.baseurl}}/img-post/es-1.png) + +- 在上图中,字母 b 为首的 Term 有两个,分别为 blue 和 black,这时就需要进行第二层排序,即对第二位字母进行排序,这时我们发现两个 Term 的第二位字母都为 l,于是进行第三层排序,第三层排序的结果是 bla、blu ,分别对应 black、blue 两个 Term,并对应 [1, 3]、2 两个 Posting List。对应关系如下图所示: + + ![]({{site.baseurl}}/img-post/es-2.png) + +- 在 Term Index 中需要保存的是 Term 的前面部分字段,以及与 Term Dictionary 之间的映射关系,这使得存储的信息量减少。再结合 FST(Finite State Transducer)压缩技术,Term Iindex 可以被压缩到足够小,以至于可以被缓存进服务器内存中。这样,在用户查找的时候,先在内存里从 Term Index 找到 Term Dictionary 中的位置映射关系,然后再去磁盘上找对应的 Term,进而查找对应的 Posting List,这就大大减少了磁盘的读取次数,也就提高了效率和速度。 + +#### 2.4. FST(Finite State Transducer) + +- 关于 FST 压缩技术,请参考这篇文章:[https://www.shenyanchao.cn/blog/2018/12/04/lucene-fst/](https://www.shenyanchao.cn/blog/2018/12/04/lucene-fst/),英语好的可以看下这篇论文[https://cs.nyu.edu/~mohri/pub/fla.pdf](https://cs.nyu.edu/~mohri/pub/fla.pdf),里面对FST有详细的解释。 + + diff --git "a/_posts/2022-01-29-Elasticsearch\357\274\232ES \345\270\270\350\247\201\346\212\245\351\224\231\345\217\212\350\247\243\345\206\263\345\212\236\346\263\225.md" "b/_posts/2022-01-29-Elasticsearch\357\274\232ES \345\270\270\350\247\201\346\212\245\351\224\231\345\217\212\350\247\243\345\206\263\345\212\236\346\263\225.md" new file mode 100644 index 00000000000..ea7c64415ed --- /dev/null +++ "b/_posts/2022-01-29-Elasticsearch\357\274\232ES \345\270\270\350\247\201\346\212\245\351\224\231\345\217\212\350\247\243\345\206\263\345\212\236\346\263\225.md" @@ -0,0 +1,155 @@ +--- +layout: post +title: Elasticsearch:ES 常见报错及解决办法 +subtitle: +date: 2022-01-29 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Elasticsearch +--- + + +#### 处理 AttributeError: 'Response' object has no attribute 'my_suggest' 异常 + +- 环境 + - windows 10; + - elasticsearch 7.8.0; + - Django + +- 问题 + - 使用 elasticsearch + Django 搭建搜索引擎,在获取 suggest 时遇到 **AttributeError: 'Response' object has no attribute 'my_suggest'** 报错, + - 问题代码如下: + ``` + from SEARCH.models import ArticleType # 注意:ArticleType 是在另一个文件单独定义的! + from django.http import HttpResponse + from elasticsearch import Elasticsearch + from datetime import datetime + + client = Elasticsearch(hosts=["127.0.0.1"]) + + s = ArticleType.search() + s = s.suggest( + 'my_suggest', + "xx", + completion={ + "field": "suggest", + "fuzzy": { + "fuzziness": 2 + }, + "size": 10 + } + ) + suggestions = s.execute() + ``` +- 报错如下: + >KeyError: 'my_suggest' + > + >During handling of the above exception, another exception occurred: + > + >Traceback (most recent call last): + File "C:\Users\Administrator\AppData\Local\Programs\Python\Python37\lib\site-packages\django\core\handlers\exception.py", line 34, in inner + response = get_response(request) + File "C:\Users\Administrator\AppData\Local\Programs\Python\Python37\lib\site-packages\django\core\handlers\base.py", line 115, in _get_response + response = self.process_exception_by_middleware(e, request) + File "C:\Users\Administrator\AppData\Local\Programs\Python\Python37\lib\site-packages\django\core\handlers\base.py", line 113, in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + File "C:\Users\Administrator\AppData\Local\Programs\Python\Python37\lib\site-packages\django\views\generic\base.py", line 71, in view + return self.dispatch(request, *args, **kwargs) + File "C:\Users\Administrator\AppData\Local\Programs\Python\Python37\lib\site-packages\django\views\generic\base.py", line 97, in dispatch + return handler(request, *args, **kwargs) + File "C:\Users\Administrator\Desktop\E_engine\E_search\SEARCH\views.py", line 36, in get + for match in suggestions.my_suggest[0].options: + File "C:\Users\Administrator\AppData\Local\Programs\Python\Python37\lib\site-packages\elasticsearch_dsl\utils.py", line 130, in __getattr__ + '{!r} object has no attribute {!r}'.format(self.__class__.__name__, attr_name)) + AttributeError: 'Response' object has no attribute 'my_suggest' + + +- 原因 + - elasticsearch 版本更新导致用法变更,问题代码是旧的写法,需要将 **s.execute_suggest()** 变更为 **s.execute().to_dict()**; + +- 解决: + - 更新代码 + + ``` + from django.http import HttpResponse + from elasticsearch import Elasticsearch + from datetime import datetime + + client = Elasticsearch(hosts=["127.0.0.1"]) + + s = ArticleType.search() + s = s.suggest( + 'my_suggest', + "九州", + completion={ + "field": "suggest", + "fuzzy": { + "fuzziness": 2 + }, + "size": 10 + } + ) + # 调用 execute_suggest 方法 + # suggestions = s.execute() + suggestions = s.execute().to_dict() # 此处将 s.execute_suggest() 变更为 s.execute().to_dict() + print("#" * 50) + print(suggestions) + print("#" * 50) + ``` + + +#### 处理 analyzer [ik_max_word] not found for field [name] 异常 + +- 环境 + - windows 10; + - elasticsearch 7.8.0; + +- 问题 + - elasticsearch 创建索引时遇到 analyzer [ik_max_word] not found for field [name] 报错; + - 报错: + >org.elasticsearch.index.mapper.MapperParsingException: analyzer [ik_max_word] not found for field [name] + +- 原因 + - IK分词器插件 未安装; + +- 解决 + - 进入 elasticsearch 安装目录下的 bin\ 目录,在此目录打开命令行窗口,输入下面代码安装IK分词器插件; + ``` + elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/vX.X.X/elasticsearch-analysis-ik-X.X.X.zip + ``` + - 安装成功,如下图: + + ![]({{site.baseurl}}/img-post/es-13.png) + + - 重启 elasticsearch,重新创建索引,不在报错、创建成功; + + +#### elasticsearch-head 连接不上 Elasticsearch,localhost:9100 显示集群健康值:未连接 + +- 环境 + - windows 10; + - elasticsearch 7.8.0; + - elasticsearch-head; + +- 问题 + - 启动 elasticsearch 和 elasticsearch-head 以后,[http://localhost:9100/](http://localhost:9100/) 页面显示 **集群健康值:未连接**,点击 连接 按钮以后没有任何反应; + + ![]({{site.baseurl}}/img-post/es-11.png) + +- 解决 + - 原因:config/elasticsearch.yml 文件的配置问题; + - 进入 elasticsearch/config 文件夹,打开 elasticsearch.yml 文件,在文件尾部添加如下代码; + ``` + # 开启跨域访问支持,默认为false + http.cors.enabled: true + # 跨域访问允许的域名地址 + http.cors.allow-origin: "*" + # 通过为 cluster.initial_master_nodes 参数设置符合主节点条件的节点的 IP 地址来引导启动集群 + cluster.initial_master_nodes: ["node-1"] + ``` + - 保存文件后重启 elasticsearch 和 elasticsearch-head; + - 问题解决,如下图: + + ![]({{site.baseurl}}/img-post/es-12.png) \ No newline at end of file diff --git "a/_posts/2022-01-30-Elasticsearch\357\274\232ES \347\232\204\347\256\200\344\273\213\343\200\201\347\211\271\347\202\271\345\217\212\344\275\277\347\224\250\345\234\272\346\231\257.md" "b/_posts/2022-01-30-Elasticsearch\357\274\232ES \347\232\204\347\256\200\344\273\213\343\200\201\347\211\271\347\202\271\345\217\212\344\275\277\347\224\250\345\234\272\346\231\257.md" new file mode 100644 index 00000000000..9d838e949ca --- /dev/null +++ "b/_posts/2022-01-30-Elasticsearch\357\274\232ES \347\232\204\347\256\200\344\273\213\343\200\201\347\211\271\347\202\271\345\217\212\344\275\277\347\224\250\345\234\272\346\231\257.md" @@ -0,0 +1,68 @@ +--- +layout: post +title: Elasticsearch:ES 的简介、特点及使用场景 +subtitle: +date: 2022-01-30 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Elasticsearch +--- + + +# 1. ES 简介 + +- ES,是一个开源的高扩展分布式全文检索引擎; +- ES,基于Lucene; +- 是当前最流行的企业级搜索引擎; +- ES 是面向文档的; + +# 2. ES 特点: + +- 大型分布式集群,处理PB级别数据; +- 近乎实时的存储、检索数据; +- 开箱即用,部署简单,操作简单; +- 可扩展性好,可扩展至上百台服务器; +- 使用 restful 风格的 API; +- 对条件的过滤支持非常好; +- **ES 最核心的功能,检索、统计**; +- **ES 存储的数据是静态数据**,不同于 MySQL 这类需要定期 CRUD 事务操作的数据库; +- ES 不支持频繁的更新; +- ES 不支持事务; +- ES不是很适合存储原始数据,尤其是需要 CRUD 的数据; + +# 3. ES 与 MySQL 等 RDB 的区别: + +- MySQL 属于 RDB,索引基于 b-tree,而 b-tree 是为写入优化而设计的索引结构; +- ES 存储的多是静态数据,不需要频繁的更新,可以采用预先排序的方式换区更快的检索速度,代价是更新很慢; + +# 4. ES 三种部署模式: + +- 使用 ES 作为主要存储; + -- 对于静态数据,推荐使用 elasticsearch 作为唯一数据存储,慎重权衡是否需要增加额外的存储; +- 在现有 SQL database 上增加 ES 作为索引; + -- 该模式下,MySQL来做原始数据的存储,然后基于MySQL在上层部署我们的ES中间件来实现我们的搜索引擎; + -- MySQL 不适用于检索,但支持事务,可以保证不会出现脏数据,ES 不支持事务,不是很适合存储原始数据; + -- 很多业务场景的现实条件是,海量数据存储在 MySQL 上,迁移到 ES 之上的难度和风险很大; +>补充: +>MySQL、Oracle 使用 logstash-input-jdbc 作为 ES 中间件; +>MongoDB 使用 mongo-connector 作为 ES 中间件; + +- 使用 ES 和现有的工具; + +# 5. ES 典型使用场景: + +- 排名; +- 智能分词 + 模糊匹配查询; +- 用户浏览行为日志追踪,推荐期望商品或内容; +- 搜索引擎; +- 全文检索; +- 相关度排名; +- 文本高亮; +- 搜索推荐; +- 电商网站,商品检索; +- ELK; +- 商品价格监控抢购; +- 海量数据 BI; +- 站内搜索(电商、招聘、门户、OA); diff --git "a/_posts/2022-01-30-Elasticsearch\357\274\232Windows \345\256\211\350\243\205 Elasticsearch \345\217\212 \345\217\257\350\247\206\345\214\226\345\267\245\345\205\267 elasticsear-head.md" "b/_posts/2022-01-30-Elasticsearch\357\274\232Windows \345\256\211\350\243\205 Elasticsearch \345\217\212 \345\217\257\350\247\206\345\214\226\345\267\245\345\205\267 elasticsear-head.md" new file mode 100644 index 00000000000..7e7c08b9e44 --- /dev/null +++ "b/_posts/2022-01-30-Elasticsearch\357\274\232Windows \345\256\211\350\243\205 Elasticsearch \345\217\212 \345\217\257\350\247\206\345\214\226\345\267\245\345\205\267 elasticsear-head.md" @@ -0,0 +1,94 @@ +--- +layout: post +title: Elasticsearch:Windows 安装 ES 及 elasticsear-head +subtitle: +date: 2022-01-30 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Elasticsearch +--- + + +# 1. 安装 Elasticsearch + +- 下载 Elasticsearch; +- 下载地址:[https://www.elastic.co/cn/downloads/elasticsearch](https://www.elastic.co/cn/downloads/elasticsearch); + +- 下载后解压缩; + +- 打开压缩后的文件,打开 \bin 目录, + - 如下: + + ![]({{site.baseurl}}/img-post/es-3.png) + +- 双击 图中红色框标记的 elasticsearch 文件,系统自动启动命令行窗口,得到下图: + - 注意这个窗口不要关闭! + + ![]({{site.baseurl}}/img-post/es-4.png) + +- 如图,在红框中我们看到 127.0.0.1:9200; + +- 我们打开浏览器,输入127.0.0.1:9200 回车,得到下图: + + ![]({{site.baseurl}}/img-post/es-5.png) + +- 这表示安装成、服务已被启动; + +# 2. 安装可视化工具 elasticsear-head + +- 参考下载页面:[https://github.com/mobz/elasticsearch-head](https://github.com/mobz/elasticsearch-head); + + ``` + git clone git://github.com/mobz/elasticsearch-head.git + cd elasticsearch-head + npm install + npm run start + ``` + +- 得到下图: + + ![]({{site.baseurl}}/img-post/es-6.png) + +- 打开浏览器,输入 http://localhost:9100,得到下图: + + ![]({{site.baseurl}}/img-post/es-7.png) + +- 完成这一步后,我们发下右侧灰色提示 **集群健康值:未连接**,我们点击 【连接】按钮发现并没有任何反应,这是因为 ES 服务 和 elasticsearch-head 之间的通信尚未建立,我们需要进行下一步操作; + +# 3. 修改 YML 配置 + +- 打开 ES 文件包找到 \config 目录下的 elasticsearch.yml 文件,如下图: + + ![]({{site.baseurl}}/img-post/es-8.png) + +- 使用文本阅读程序打开 elasticsearch.yml 文件,在最底部添加下面两行内容: + + ``` + http.cors.enabled: true # 开启跨域访问支持,默认为false + http.cors.allow-origin: "*" # 跨域访问允许的域名地址 + ``` + +- 找到 ES 服务重启后跳出的命令行窗口,ctrl + C 终止服务; + +- 重新双击 ES 文件夹 \bin 目录下的 elasticsearch,重启 ES 服务; + +- 刷新 http://localhost:9100 页面,得到下图: + + ![]({{site.baseurl}}/img-post/es-9.png) + +- 至此安装全部完成。 + +# 4. 补充知识: + +- 下面是 elasticsearch.yml 文件中配置项的用途和用法,请参考: + + ``` + http.cors.enabled 是否支持跨域,默认为false + http.cors.allow-origin 当设置允许跨域,默认为*,表示支持所有域名,如果我们只是允许某些网站能访问,那么可以使用正则表达式。比如只允许本地地址。 /https?:\/\/localhost(:[0-9]+)?/ + http.cors.max-age 浏览器发送一个“预检”OPTIONS请求,以确定CORS设置。最大年龄定义多久的结果应该缓存。默认为1728000(20天) + http.cors.allow-methods 允许跨域的请求方式,默认OPTIONS,HEAD,GET,POST,PUT,DELETE + http.cors.allow-headers 跨域允许设置的头信息,默认为X-Requested-With,Content-Type,Content-Length + http.cors.allow-credentials 是否返回设置的跨域Access-Control-Allow-Credentials头,如果设置为true,那么会返回给客户端。 + ``` diff --git "a/_posts/2022-01-30-Elasticsearch\357\274\232\345\237\272\346\234\254\346\246\202\345\277\265\345\220\215\350\257\215\345\205\250\350\247\243\350\257\273.md" "b/_posts/2022-01-30-Elasticsearch\357\274\232\345\237\272\346\234\254\346\246\202\345\277\265\345\220\215\350\257\215\345\205\250\350\247\243\350\257\273.md" new file mode 100644 index 00000000000..61f03c5d9a0 --- /dev/null +++ "b/_posts/2022-01-30-Elasticsearch\357\274\232\345\237\272\346\234\254\346\246\202\345\277\265\345\220\215\350\257\215\345\205\250\350\247\243\350\257\273.md" @@ -0,0 +1,125 @@ +--- +layout: post +title: Elasticsearch:基本概念名词全解读 +subtitle: 索引、节点、集群、分片 +date: 2022-01-30 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - Elasticsearch +--- + + +# 文档 + +- elasticsearch 是面向文档的,文档是所有可搜索数据的最小单位,对应 RDB 中的一条记录,比如一条日志、一部电影、一篇文章; +- 文档在保存到 elasticsearch 前会被序列化成 json 格式,json 对象有字段组成(字段类型包括:字符串 / 数值 / 布尔 / 日期 / 二进制 / 范围类型); +- 每个文档都有一个 unique ID,这个 ID 可以自己指定也可以有 elasticsearch 自动生成; + +# 元数据 + +- 在Elasticsearch下,一个文档除了有数据之外,它还包含了元数据(Metadata); +- 每创建一条数据时,都会对元数据进行写入等操作,元数据定义了每个添加的doc的处理方式;类似于数据库的表结构数据; +- 有些元数据是在创建mapping的时候就会设置; +- 文档有三个必须的元数据元素: + - _index,文档在哪存放 + - _type,文档表示的对象类别 + - _id,文档唯一标识 + - 注意:elasticsearch 中这三个元数据加在一起,构成了文档在 Elasticsearch 中的唯一标识; + +# 索引 index + +- 一个索引是拥有相似特征的文档的集合; +- index V.S. shard + - index,体现逻辑空间概念,每个所以都有自己的 mapping 定义; + - shard,体现了物理空间概念,索引中的数据分散在 shard 上; +- 索引的 mapping V.S. settings + - mapping 定义文档字段的类型; + - settings 定义不同的数据分布; +- 索引库是多个 type 的集合; +- 对文档进行 CRUD 时需要使用索引名称; +- mysql:一个应用对应一个数据库,一对一; +- ES:一个应用可以对多个具有相似特征的对象构建索引,如用户索引、订单索引等,一对多; +- 类型 type + - 相当于 RDB 的表; + - 一个类型是索引的一个逻辑上的额分区; + - ES 5.x 一个索引中可以创建多个 type; + - ES 6.x 兼容之前的一个索引对多个 type,但是不能创建多个 type; + - ES 7.x 一个索引不能创建多个 type,只能创建一个 type; +- 字段 field + - 相当于 RDB 数据表中的字段; + - 对同一个文档,根据不同属性(字段)进行分类; +- 映射 mapping + - 对应 RDB 的表结构定义; + - mapping 定义了映射关系; + - mapping 定义每个 type 中有哪些 field 字段、字段名称、是否分词、是否索引、是否存储等; +- 文档 document + - 文档是一个被索引的信息单元; + - 文档以 json 格式表示; + - 一个 index / type 里面可以存储任意多的文档; +- 接近实时 NRT + - 从开始索引一个文档,到文档被搜索到,延时通常在 1s 以内; +- 集群 cluster + - 多个节点组织在一起,共同持有整个数据,提供索引和搜索功能; + - 一个集群有一个唯一的名字标识,一个节点只能通过指定某个集群的名字,加入这个集群; + +# 节点 + +- 概念 + - 一个节点就是一个 elasticsearch 实例,本质是一个 Java 进程; + - 一台机器上可以运行多个 elasticsearch 进程,生产环境一般建议一台服务器上只运行一个 elasticsearch 实例; + - 每个节点在启动之后,都会被分配一个 UID,保存在 data 目录之下; + +- 节点类型 + - master-eligible nodes & master node; + - data node; + - coordinate node; + - hot & warm node; + - machine learnning node; + - trible node; + +- 节点 & 集群 + - 一个集群可以拥有任意多个节点; + +- 每个节点都有自己的名称,一个节点可以通过配置节点名称的方式,加入一个集群; +- 任意一个节点启动以后,都会创建并加入一个叫做 elasticsearch 的集群中; + + ![]({{site.baseurl}}/img-post/es-10.png) + +- master-eligible nodes & master node + - master-eligible,表示有资格参加选举成为 master node; + - 每个节点启动后默认就是 master-eligibel node,有资格参加选举成为 master-node; + - 集群中的第一个节点启动后,会默认选举自己作为 master node; + - 集群中每个节点都保存了集群状态信息; + - 只有 master node 才能修改集群的状态信息(cluster state),如创建或删除索引,跟踪哪些节点是集群的一部分,以及决定将哪些分片分配给哪些节点; + - 集群状态(cluster state),维护了一个集群中必要的状态信息,包括所有的节点信息、所有的索引和相关的 mapping 与 setting 信息,以及分片的路由信息; + - 集群中如果任意节点都能修改信息,则会导致数据不一致性; +- data node + - 用于保存数据的节点成为 data 节点; + - 一个节点可以是 master/master-eligible 节点,也可以是 data 节点,也可以同时为 master/master-eligible 和 data 节点; + +- 节点配置 + - **开发环境中,一个节点最好只承担一种角色**; + - 职责明确、管理方便; + - 不同角色的节点,配置不同的硬件,达到性能最优;【TODO】 + - 通过节点参数配置节点角色,如 node.master = true、node.data=fales; + +# 分片 + +- 分片类型 + - 主分片(primary shard) & 分片副本(replica shard); +- 主分片(primary shard) + - 主分片,解决数据水平扩展问题,通过主分片可以将数据分散到集群内的所有节点; + - 主分片数量在索引创建时指定,后续不允许修改,除非 reindex; +- 副本(replica shard) + - 副本,解决的是数据的高可用问题; + - 副本是主分片的拷贝; + - 副本数量可以动态调整; + - 增加副本,在一定程度可以提高服务的可用性,提高数据读取吞吐量; +- 分片设定 + - 如果分片数设置过小,会导致后续无法增加节点,进而无法进行水平扩展; + - 吐过分片数设置过大,会导致数据重新分配,耗费大量时间,同时会影响排名、打分等数据的统计; + - 默认情况下,elasticsearch 中的每个索引会被分成 5 个主分片和 1 个复制,如果集群中有 2 个节点,那么集群将会有 5 个主分片、5 个复制分片,每个索引共 10 个分片; + - + diff --git "a/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232ETL \345\274\200\345\217\221\350\246\201\346\263\250\346\204\217\347\232\204\351\227\256\351\242\230.md" "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232ETL \345\274\200\345\217\221\350\246\201\346\263\250\346\204\217\347\232\204\351\227\256\351\242\230.md" new file mode 100644 index 00000000000..ca2bf633aca --- /dev/null +++ "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232ETL \345\274\200\345\217\221\350\246\201\346\263\250\346\204\217\347\232\204\351\227\256\351\242\230.md" @@ -0,0 +1,118 @@ +--- +layout: post +title: ETL方法论:ETL 开发要注意的问题 +subtitle: +date: 2022-01-31 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - ETL +--- + + +# 设计规范 + +#### 抽取加载策略设计文档 + +- 节点名称,与目标表一致 +- 负责人 +- 源表、目标表 +- 抽取方式、加载策略 +- 增量新增数据的判断条件 +- 是否支持重复执行 + +#### ETL 映射设计文档 + +- 节点名称,与目标表一致 +- 所在层级、所属 Job +- Job 中的位置、上游节点名称 +- 负责人 +- 参数列表 +- 源表、目标表 +- 字段映射转换关系 +- 是否支持重复执行 + +#### 调度设计文档 + +- 顶层调度有几个 +- 顶层 Job 调度时机 +- 顶层调度参数列表 +- 每个 Job 内的任务调度顺序编排 +- 调度是否支持重复执行 +- 调度是否支持并行 + +# 开发规范 + +#### 命名规范 + +- Job 命名规范:J_层次名_内容描述 +- 节点命名规范:跟目标表一致 +- 参数命名规范:统一命名,禁止私自创造 + +#### 代码编写原则 + +- 代码清晰、整齐,易读性高。 +- 代码编写要充分考虑执行速度最优原则。 +- 代码行整体层次分明、结构化强。 +- 代码中应有必要的注释以增强代码的可读性。 +- 规范要求非强制性地约束代码开发人员的代码编写行为。在实际应用中,只要不违反常规要求,允许存在可理解的偏差。 + + +#### 代码开发规范 + +- 脚本是否有备注、复杂计算逻辑是否有注释。 +- 任务是否支持多次重跑而输出不变,不能有直接 insert into 语句。 +- 分区表是否使用分区键过滤并且有有效裁剪。 +- 外连接的过逑条件是否使用正确,例如在左连接的 where 语句存在右表的过滤条件。 +- 关联小表,是否使用/*+ map join * /。 +- 不允许引用别的计算任务临时表。 +- 原则上不允许存在一个任务更新多个目标表。 +- 是否存在笛卡尔积。 +- 禁止在代码里面使用 drop、create、rename 等 DDL 语句。 +- 使用动态分区时,有没有检查分区键值为 NULL 的情况。 +- 对于重要的任务 DQC 质量监控规则是否配置,严禁裸奔。 +- 代码中有没有进行适当的规避数据倾斜语句。 + +#### 日志输出规范 + +- 每个任务节点,都需要输出成功/失败标志,如果失败最好输出错误信息。 +- 每个 Job 也要输出成功/失败标志,如果失败必须输出失败任务节点名称。 + + +# 部署规范 + +#### 调度部署 + +- 需要定义清楚,每个工作流应该部署到哪台服务器的哪个目录下边; +- 需要定义清楚,ETL 服务器的目录结构; +- 调度其实不需要过多了解工作流的内部情况,只需根据工作流的使用说明书,做好如下二件事情即可: + - 在合适的时间调度执行; + - 监控工作流的执行情况,报错或超时的时候发出告警,开始或者结束的时候发出通知; + +#### 流程依赖 + +- 根据节点间的依赖关系、每个节点的资源消耗和每个节点的执行耗时,去合理编排流程; +- 有依赖的必须串行,无依赖可以并行用于降低总执行时长、 + - 同时也要注意任务并行会增加瞬时资源消耗; +- 注意:流程依赖里配置的执行先后顺序,后执行的不一定就真的依赖于先执行的任务节点; + +#### 调度配置 + +- 具体到调度层面,需要把每个工作流都打上如下标签(使用说明书): + - 允许的最早调度时间(需要考虑前置依赖或数据源就位时间); + - 允许的最晚结束时间(如有); + - 参数列表以及默认值; + - 告警通知人列表; + - 是否支持重复跑; + - 是否支持并行补多个日期数据; + +#### 经验之谈 + +- 注意保留执行过程中的日志记录; +- 补数、重跑情况下的参数传递问题; + - 内层工作流或底层节点绝对不能把参数(主要是日期)写死; + - 需要在调度工作流的时候,工作流内的所有节点都能接受到来自外层传入的参数; +- 尽量的将可以重复跑的、和不能重复跑的节点,放到不同的工作流; +- 尽量的将可并行补多个日期数据的节点、跟不能并行补数的节点放到不同的工作流; + diff --git "a/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232ETL \347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232ETL \347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..559dd15c22e --- /dev/null +++ "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232ETL \347\232\204\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,165 @@ +--- +layout: post +title: ETL方法论:ETL 的概念详解 +subtitle: +date: 2022-01-31 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - ETL +--- + + +# 1. ETL 定义 + +ETL,是数据抽取(Extractor)、清洗转换(Transform)、数据加载(Load)的过程,是构建数据仓库的重要环节之一。 + + ![]({{site.baseurl}}/img-post/etl-1-2.png) + +ETL,负责将分布、异构的数据源中的数据,如关系数据、平面数据、文件等取到临时中间层,进行清洗、转换、集成,最后加载到数据仓库或数据集市,成为联机分析处理、数据挖掘的基础。 + +ETL 的目的,是将其中分散、零乱、标准不统一的数据,整合到一起,为企业的决策提供分析依据。ETL 是 BI 项目的重要环节,一般要花掉整个 BI 项目的三分之一的时间精力,ETL 设计的好坏直接关系到 BI 项目的成败。 + +其中,ETL三个部分中: +- Extractor,是从不同的数据源抽取到 ODS(operational data store),这部分尽可能的选用高效的抽取方法,以提升 ETL 的性能; +- Transform,数据清洗和转换,最耗费时间,一般这部分工作占了整个 ETL 的三分之二。 +- Load,清洗完的数据一般直接写入数仓 DW(data warehouse)中。 + +# 2. Extractor + +在数据抽取环节,需要做大量的数据调研工作。比如:要搞清楚数据是从几个业务系统来的,各个业务系统的数据库服务器是什么样的,连接方式如何选择;有多少手工数据,手工数据如何获取;是否存在非结构化数据,非结构化数据如何处理。 + +这里需要注意的是,数据抽取的方式是多种多样的,对于关系型数据库可以通过 ODBC 和 JDBC,对于文件型数据可以通过同步工具等上传。 + +另外,在这步抽取环节,其实就可以做一部分的清洗过滤和转换工作。比如:数据库中的无效数据、缺失数据、重复数据、异常数据等要处理;不同的数据格式需要统一;半结构化数据、非结构化数据,有的如日志要进行信息提取并汇聚成结构化数据保存,有的如音视频除了提取信息,也要保留原始数据。 + +还有数据抽取的方式、策略,也需根据业务需求进行设计,是全量提取、还是增量提取,是实时同步、还是半实时同步、还是离线同步,需要结合业务需求进行针对性的设计。 + +# 3. Transform + +数据的清洗转换,是 ETL 最重要的工作内容。数据清洗转换的核心目标,是确保数据的可用性。而要确保可用性,则需要做到数据的真实、有效、规范、统一。 同时,一个好的 ETL 系统,也可以反向提升业务系统。及时发现问题,并提交给来源系统做问题定位和整改,进而提升数据的可用性。 + +#### 3.1. 数据清洗 + +业务系统→ODS的过程,过滤那些不符合要求的数据,将过滤的结果交给业务主管部门,确认是否过滤掉还是由业务单位修正之后再进行抽取。 + +#### 3.2. 数据转换 + +ODS→DS的过程,主要进行不同维度的数据转换、数据颗粒度的转换,以及一些业务规则的计算。 + +- 不同维度数据转换 + + 将不同业务系统的相同类型的数据进行统一,例如编码转化:不同供应商在不同业务系统的编码不同;字段转换;度量单位的转换等。 + +- 数据颗粒度的转换 + 业务系统存储着颗粒度较细的数据,而数据仓库的数据时用来分析的,不需要颗粒度很细的数据,所以会将业务系统数据按照数据仓库的颗粒度进行转换。 + +- 业务规则的计算 + 企业有不同的数据指标以及业务规则,此时需要将这些数据指标计算好后存储在数据仓库中,供数据分析使用。 + +#### 3.3. 校验规则 & 数据标准化 + +- 在数据清洗转换前,需要制定一套系统、全面、规范的数据校验规则,以及一套完整、统一的输出数据的标准化规范; +- 校验规则包括合规性校验、值域范围校验、数据格式较严、空值校验、准确性校验、完整性校验等,同时针对不同问题对应的处理策略; +- 在明确了数据的校验规则之后,在进行过滤、去重、格式转换、数据置空、丢弃、赋值等操作。 + +#### 3.4. 具体内容 + +- 数据采样: + - 通过随机、加权、分层、下采样四种方式对数据源进行抽取 +- 数据拆分: + - 将数据进行横向或者纵向的拆分 +- 数据过滤: + - 按照用户需求,通过写SQL语句,对数据按照过滤表达式进行筛选 +- 数据合并: + - 将两张表按行或列的方式进行合并 +- 数据关联: + - 通过内连接、左右连接、全连接的方式对两个表格进行关联 +- 空值处理、去除重复值、聚合等。 + +# 4. Load + +- 将清洗和转换好的数据,直接加载到数据库对应表中; +- 全量更新: + - 如果是全量方式则采用覆盖的方式, +- 增量更新: + - 如果是增量则选择追加的方式。 + + +# 5. ETL 工具 + +#### 5.1. sqoop + +- sqoop 的缺点: + - 由于mysql的表结构变更,引起的数据抽取失败。(目前添加监控,自动更改还需要开发) + - 抽取速度有待提高,对于大表,指定多个map,可能会导致数据重复,需要单独做处理。 + - 不支持 mongoDB + - 启动的速度比较慢 + + +#### 5.2. DataX + +- DataX 简介 + + DataX 是阿里开源的一个异构数据源离线同步工具,致力于实现包括关系型数据库(MySQL、Oracle等)、HDFS、Hive、ODPS、HBase、FTP等各种异构数据源之间稳定高效的数据同步功能。 + + DataX本身作为数据同步框架,将不同数据源的同步抽象为从源头数据源读取数据的Reader插件,以及向目标端写入数据的Writer插件,理论上DataX框架可以支持任意数据源类型的数据同步工作。同时DataX插件体系作为一套生态系统, 每接入一套新数据源该新加入的数据源即可实现和现有的数据源互通。 + +![]({{site.baseurl}}/img-post/etl-8.png) + +- DataX框架设计 + + ![]({{site.baseurl}}/img-post/etl-9.png) + + DataX本身作为离线数据同步框架,采用Framework + plugin架构构建。将数据源读取和写入抽象成为Reader/Writer插件,纳入到整个同步框架中。 + + - Reader:Reader为数据采集模块,负责采集数据源的数据,将数据发送给Framework。 + - Writer: Writer为数据写入模块,负责不断向Framework取数据,并将数据写入到目的端。 + - Framework:Framework用于连接reader和writer,作为两者的数据传输通道,并处理缓冲,流控,并发,数据转换等核心技术问题。 + +- DataX 与 sqoop 的对比: + + ![]({{site.baseurl}}/img-post/etl-7.png) + +- DataX 的问题: + - 目前公司表中基本上没有自增主键,对于数据量大的表(目前数据量还有待测试),抽取速度慢(6千万的表7116rec/s,两千万的速度在7902rec/s,1千万的表在19307rec/s 左右),如果有自增主键或者整型的索引字段,速度是56716rec/s ,使用uuid生成的主键,会存在主键切分不均匀现象(可以修改源码); + - 目前开源版本只支持单机模式,需要依赖调度系统(在每个节点上部署客户端); + - 不支持自动创建表和分区,写入的hdfs路径必须存在(可以后期修改源代码,或者使用脚本生成); + - 生成配置文件比较繁琐(每张表需要生成一张配置文件,可以使用代码生成)。 + +#### 5.3. Airflow + +- Airflow是什么 + + airflow 是一个编排、调度和监控 workflow 的平台,由 Airbnb 开源,现在在 Apache Software Foundation 孵化。airflow 将workflow编排为由tasks组成的DAGs(有向无环图),调度器在一组workers上按照指定的依赖关系执行tasks。同时,airflow 提供了丰富的命令行工具和简单易用的用户界面以便用户查看和操作,并且airflow提供了监控和报警系统。 + +- Airflow 的基础概念 + + - DAGs: + + 即有向无环图(Directed Acyclic Graph),将所有需要运行的 tasks 按照依赖关系组织起来,描述的是所有tasks执行的顺序。 + + - Operators: + + airflow 内置了很多 operators,如 BashOperator 执行一个 bash 命令,PythonOperator 调用任意的 Python 函数,EmailOperator 用于发送邮件,HTTPOperator 用于发送HTTP请求, SqlOperator 用于执行SQL命令. + + 同时,用户可以自定义Operator,这给用户提供了极大的便利性。可以理解为用户需要的一个操作,是Airflow提供的类。 + + - Tasks: + + Task 是 Operator的一个实例 + + - Task Instance: + + 由于Task会被重复调度,每次task的运行就是不同的task instance了。Task instance 有自己的状态,包括"running", "success", "failed", "skipped", "up for retry"等。 + + - Task Relationships: + + DAGs中的不同Tasks之间可以有依赖关系 + +- Airflow与同类产品的对比: + + ![]({{site.baseurl}}/img-post/airflow-1.png) + +#### 5.4. \ No newline at end of file diff --git "a/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\345\257\271\350\264\246.md" "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\345\257\271\350\264\246.md" new file mode 100644 index 00000000000..ac02f04ffb7 --- /dev/null +++ "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\345\257\271\350\264\246.md" @@ -0,0 +1,134 @@ +--- +layout: post +title: ETL方法论:数据对账 +subtitle: +date: 2022-01-31 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - ETL +--- + + +# 1. 数据对账 + +#### 1.1. 概念 + +数据一致性:数据对账要解决的,是数据同步任务执行一段时间后、源表和目标表数据不一致的问题。 + +> 项目经验: +> +> 1、来源表的历史数据,必须保留以防止出现问题; +> +> 2、来源表必须有主键字段,确保一条数据的唯一性。比如:账单信息表的账单 ID、用户信息表的用户 ID。 + +#### 1.2. 增量同步 + + 上表新增用户小华、同时李四昵称发生变更。 + + - 插入更新: + + ![]({{site.baseurl}}/img-post/etl-4-3.png) + + - 拉链维护: + + ![]({{site.baseurl}}/img-post/etl-4-4.png) + +#### 1.3. 全量同步 + + - 不需要考虑数据对账问题; + - 全量同步,只会在第一次【初始化】、以及同步【对账表】的时候才会应用到。 + +# 2. 数据不一致的原因 + +#### 2.1. 来源表物理删除 + +- 异常结果:来源表数据量比目标表少。 + +- 解决办法:【逻辑删除】维护数据状态标识,进行逻辑删除。比如:数据状态:值为 1 表示可用,值为 0 表示不可用。 + +#### 2.2. 历史数据更新未同步更新增量字段 + +- 异常结果:相同主键数据内容不一致。 + - 比如:数据有变动,却未修改更新时间,导致增量同步时、无法感知数据变化,出现相同主键数据内容不一致。 + +- 解决办法:【增量字段】随数据变化及时更新。 + +#### 2.3. 增量字段不规范 + +- 异常结果:相同主键数据内容不一致。 + - 比如:数据有变动,但更新时间未能正常修改,导致增量同步时、无法感知数据变化,出现相同主键数据内容不一致。 + +- 解决办法:【增量字段】需要符合规范。 + +#### 2.5. 增量字段不合理 + +- 异常结果:来源表数据比目标表多,相同主键数据内容不一致。 + - 比如:增量字段设置为用户注册时间,但注册后需要时间审核,而审核时间不可控,数据同步记录的偏移量是最近时间,但是在审核这段时间可能还会有新的账号注册,这就会导致数据不一致问题。 + +- 解决办法:选择正确的增量字段。 + +#### 2.6. 任务异常 + +- 异常结果:来源表数据比目标表多,相同主键数据内容不一致。 + - 比如:某一天任务运行失败,导致数据未能同步,之后又未能及时补录,导致数据不一致。 +- 解决办法:异常重试机制、任务监控、及时补录数据。 + +# 3. 数据对账策略 + +#### 3.1. 目标表公共字段 + +- 入库时间 + - 记录创建时间; +- 更新时间 + - 作为【增量字段】,用于判断那些数据近期有更新; +- 业务数据 MD5 值 + - 用于判断该条数据全部字段值、是否有变动。 + +#### 3.2. 对账表 + +![]({{site.baseurl}}/img-post/etl-4-5.png) + +- 对账表内容: + - 业务表主键字段,一般为 ID; + - 业务表增量字段,一般为 最后更新时间; + - 业务表记录全字段 MD5 值; + +- 全量同步对账表 + - 对账表需要采用全量同步,才能保证对账的准确性。 + > 注意:对账表字段少,数据量小,同步的速度快,资源占用小,所以可以进行全量同步。 + +- 对账表作用 + - 通过将目标表和对账表进行联合分析,对标差异,进而排查数据差异。 + +#### 3.3. 数据同步初始化 +- 数据同步初始化,也就是第一次同步时、需要全量同步所有历史数据; + +![]({{site.baseurl}}/img-post/etl-4-1.png) + +首次全量同步,记录数据偏移量(上次同步位置,即最新的同步位置)。 + +![]({{site.baseurl}}/img-post/etl-4-2.png) + +> 注意:信息入库时间和信息更新时间,实际上是不一致的,最好全部记录下来。 + +#### 3.4. 对账分析内容 + +- 数据量一致性: + - 目标表是否存在数据缺失; +- 数据内容一致性: + - 目标表数据未及时更新; + +#### 3.5. 对账周期 + +- 根据业务实际,决定多久进行一次对账表的全量同步; +- 不同量级、不同类型的数据,可以按不同周期进行对账; + +#### 3.6. 注意事项 + +- 当数据量达到百亿、千亿级别时,全量同步对账表会非常考验系统性能; +- 需要根据业务实际,分析是否有必要针对全部历史数据、全量同步对账表; +- 实际工作中,需要根据业务实际,进行分级、分类处理。 + + diff --git "a/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\346\216\242\346\237\245.md" "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\346\216\242\346\237\245.md" new file mode 100644 index 00000000000..e015ecd8c98 --- /dev/null +++ "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\346\216\242\346\237\245.md" @@ -0,0 +1,146 @@ +--- +layout: post +title: ETL方法论:数据探查 +subtitle: +date: 2022-01-31 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - ETL +--- + +# 1. 业务探查 + +#### 1.1. 业务流程调研 + +- 业务流程: + - 明确具体的业务流程; +- 业务数据: + - 将数据与业务过程联系起来; + - 明确每一步业务,会对那些数据、产生哪些影响; + +#### 1.2. 业务系统调研 + +>业务系统探查,最重要的是明确业务提供方的信息。 + +- 部门信息 + +- 名称信息 + +- 系统级别 + +- 地域信息 + +- 上线时间 + +- 系统状态 + + - 如当前处在试运行阶段,需要考虑系统后面会不会变动,数据结构和内容后面会不会变动。 + + - 如系统已不再运行,则需要考虑系统有没有接入的价值。 + +- 业务联系人 + - 需要有清晰的接口人,协助梳理业务规则。 + +# 2. 数据源探查 + +#### 2.1. 存储介质探查 + +- 数据库类型 + - 是关系型数据库、还是非关系型数据库, 是 MySQL、Oracle、还是 redis、HBASE + +- 文件服务器类型 + - 存在FTP服务器、还是 HDFS 文件系统。 + +- 手工数据 + +#### 2.2. 访问方式探查 + +- IP、端口号、账号密码、用户名、查询用户、存储路径、字符集。 + + +# 3. 数据集探查 + +#### 3.1. 基础信息探查 + +- 表标识 +- 表名称 + +#### 3.2. 规模信息探查 + +- 事实表 +- 维度表 +- 业务分类 + +#### 3.3. 规模信息探查 + +- 总量 +- 增量 +- 大小 +- 存储周期 + +#### 3.4. 状态信息探查 + +- 状态(启用 / 停用) +- 更新频率 +- 更新方式 + +# 4. 数据项探查 + +#### 4.1. 基础信息探查 + +- 表示 +- 名称 +- 类型 +- 长度 +- 精度 + +#### 4.2. 属性信息探查 + +- 字典项 +- 标准数据元 +- 增量字段 +- 主键字段 +- 外键字段 +- 唯一主键 +- 关联信息 + - 比如,事实表的外键字段、关联哪个维度表的主键字段。 + +#### 4.3. 问题信息探查 + +- 有效性 +- 合规性 +- 空置率 + +# 5. 数据探查步骤 + +- 简单看下表的元数据信息: + - 重点关注命名、备注、字段类型、最后更新时间等; + - 数据量级、数据增量; + +- 简单拿几条数据出来看看: + - 需要用到的列是查看的重点; + - 同时要确认真实数据跟元数据信息是否一致, + - 不一致的要进一步求证,是自己理解错误?还是脏数据影响?还是元数据错了? + +- 对关键字段需要查询下分布情况: + - 如果发现跟常识或者跟自己对业务的理解存在偏差,就需要进一步求证,看是数据问题?还是自己认知问题?还是单位或计算口径问题? + - 空值率、异常数据等在这一步也需要被发现。 + +- 着重看下数据的完整性: + - 比如数据内容(或列)缺失、数据条数缺失等。 + +- 看看有没有其它数据质量问题: + - 比如重复数据、脏数据、编码映射问题等。 + +- 确认好 join 字段: + - 需要使用多张表的时候,要确认好 join 字段,并确保两表之间是 1:m 的关系; + - m:n 的关系很少碰到,并且多数情况下是自己用错了。 + +- 大致评估出实际的开发工时: + - 需要参考的因素如下: + - 需求的复杂度; + - 数据源的理解探查难度; + - 数据源的数据质量(是否需要特别的清洗、编码映射、缺失数据补全) + - 数据存储结构转换成最终需求的复杂度(有的需要分多步落中间表完成,有的源表数据直接就是最终需要的数值) diff --git "a/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\346\270\205\346\264\227.md" "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\346\270\205\346\264\227.md" new file mode 100644 index 00000000000..0c5eb039a5c --- /dev/null +++ "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\346\270\205\346\264\227.md" @@ -0,0 +1,259 @@ +--- +layout: post +title: ETL方法论:数据清洗 +subtitle: +date: 2022-01-31 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - ETL +--- + + +# 1. 数据流转过程 + +![]({{site.baseurl}}/img-post/etl-6-1.png) + +>一般意义上,ETL 数据清洗结束后,输出的数据会放到 DWD 层。 + +# 2. 源数据的问题 + +#### 2.1. 数据本身问题 + - 数据值录入错误 + - 数据缺失 + - 数据重复 + +#### 2.2. 表设计问题 + - 建表命名不规范 + - 数据不规范 + - 字段属性错误 + - 格式不统一 + - 缺少宽表 + - 业务数据一般字段较少,但数据分析却需要宽表; + - 字段组合问题 + - 业务数据表内的字段只服务于业务,但是数据分析时需要多个表拆分出的部分字段合并成宽表; + - 表之间缺少关联 + - 没有面向数据分析的外键设计 +- 需要看到数据,才能知道如何制定规则; +- 有些数据,需要跨部门联合检验; + +# 3. 数据清洗的作用 + +- 确保数据的可用性 + - 真实 + - 有效 + - 规范 + - 统一 +- 反向提升业务系统 + - 及时发现问题,并提交给来源系统做问题定位和整改,进而提升数据的可用性。 + +# 4. 数据清洗的目标 + +#### 4.1. 完整性 + +单条数据是否存在空值,统计的字段是否完善。 + +#### 4.2. 全面性 + +通过比较最大值,最小值,平均值,数据定义等来判断数据是否全面。 + +#### 4.3. 合法性 + +数值的类型、内容、大小是否符合我们设定时候的预想。 + +#### 4.4. 唯一性 + +数据是否重复记录。 + +#### 4.5. 权威性 + +同一个指标出现多个来源的数据,且数值不一样。 + +# 5. 数据校验 + +#### 5.1. 合规性校验:基于规则 +- 身份证合规 + - 行政区划、年份、月份、日期,需要符合规则 +- 手机号合规 + - 位数 + - 号段 + - 事实表多表联合校验 +- 邮箱地址合规 +- 组织结构代码证合规 +- 地址信息合规 +- 一致性校验: + - 属性一致性 + - 关系一致性 + +#### 5.2. 值域范围校验 + - 一般需要与维度表联合校验 + - 比如: + - ID:某订单数据的商品ID为10001,但商品表ID字段值域范围最大是10000,超出了值域范围; + - 性别:性别为 3,不在性别维度表 0(未填写)、1(男)、2(女)值域范围内; + +#### 5.3. 数据格式校验 + - 比如: + - 概率:概率值应该为 0~1,但实际确实 2、3、4 等; + - 年龄:应该为整数值,并且在0~120之间。 + +#### 5.4, 空值校验 + - 非空字段,比如:年龄、性别 + - 可为空字段,比如:爱好 + +#### 5.5. 准确性校验 + - 比如:业务人员一、二级部门归属; + +#### 5.6. 完整性校验 + - 比如:地址信息缺失街道门牌号 + +# 6. 清洗方法 + +#### 6.1. 数据过滤 + +- 基于样本数据过滤 + - 主要用于处理垃圾数据: + - 比如:垃圾样本库 + - 垃圾短信 + - 垃圾邮件 +- 基于业务规则过滤 + - 比如:无效日志 + - 网络攻击 + - 爬虫 +- 基于逻辑规则过滤 + - 不合理值 + - 比如:年龄为1000岁; + - 矛盾内容 + - 比如:收获地址为美国,取件人号码却是+86开头; + +#### 6.2. 异常值清洗 + +- 异常值检查方法: + - 统计分析 + - 箱线图分析 + - 基于模型检测 + - 同数据模型不能拟合 + >- 簇集合: 异常值不属于任何簇 + >- 回归模型:异常值相对远离预测值 + - 基于距离分析 + - 近邻性度量:异常值远离其他对象 + - 基于密度分析 + - 异常点的局部密度,显著大于大部分近邻 + - 基于聚类 + - 不强属于任何簇的离群点 + +- 异常值的处理方法: + - 数据光滑处理 + - 丢弃数据 + - 视为缺失值 + - 中位数修正 + - 众数修正 + - 均值修正 + - 不处理 + +#### 6.3. 数据去重 + +- 全业务字段重复 + - 一般出现在数据同步过程中, + - 比如同步任务异常、或者 kafka 同步的数据本身就存在重复; + - 需要设定唯一主键,防止出现重复。 +- 业务规则重复 + - 根绝业务规则判定需要保留的数据 + - 比如:一个身份证号对应多个用户ID + +#### 6.4. 数据规范化 & 格式统一 + +- 时间、日期格式统一 +- 数值格式统一 +- 全角半角显示格式统一 +- 大小写格式统一 +- 经纬度格式统一 +- 设备 Mac 地址格式统一 + +#### 6.5. 数据填充 + +- 人工填充 +- 特殊填充 +- 统计量填充 +- 模型预测填充 +- 插值法填充 +- 哑变量填充 +- 热卡填充 +- 期望值最大化填充 + +#### 6.6. 数据置空 +- 置空不合规字段: + - 比如: + - 在BI分析时,尽管有部分字段不合规,但字段值置空并不影响数据分析,所以就可以将数据置空 + +#### 6.7. 数据丢弃 + +- 丢弃问题数据 + - 需要分析对看结果造成的影响大小。 + +#### 6.8. 数据合并 + +- 多表合并 + - 横向合并 + - 纵向合并 + - 多字段合并成新表 + - 多表合并成宽表 + +#### 6.9. 数据拆分 + +- 横向拆分 + - 按照也无需求,将表内的不同字段拆分出来与其他表字段合并,或者单独成表; +- 纵向拆分 + - 数据量太大的单表,纵向拆分成多个表,以提升SQL性能; +- 按业务拆分 + - 根据需求,将不同数据拆分成多个表 +- 按功能拆分 + - 拆分成训练集、测试集 + +#### 6.10. 数据关联 + +- 内连接 +- 左右连接 +- 全连接 +- 外键关联 + +#### 6.11. 数据切割 + +- 数据切割,主要是针对时间跨度较长的数据,进行横向的切分。 +- 比如: + - 有些公司还会将数据 session 做切割,这个一般是 app 的日志数据,其他业务场景不一定适合。这是因为 app 有进入后台模式,例如用户上午打开 app 用了10分钟,然后 app 切入后台、晚上再打开,这时候 session 还是一个,实际上应该做切割才对。 + - 也有公司会记录app进入后台,再度进入前台的记录,这样来做 session 切割。 + +#### 6.12. 数据映射 + +- 将GPS经纬度转换为省市区详细地址; + - 业界常见 GPS 快速查询,一般将地理位置知识库使用 geohash 映射,然后将需要比对的 GPS 转换为 geohash 后跟知识库中 geohash 比对,查找出地理位置信息。 +- 将 IP 地址也转换为省市区详细地址; + - 业界有很多快速查找库,基本原理都是二分查找,因为 ip 地址可以转换为长整数; + - 典型的如 ip2region 库; +- 将时间转换为年、月、日甚至周、季度等维度信息; + +>注意:数据映射,一般只映射常见指标、以及明确的企业开发中后续会用到的指标。因为数据量较大,如映射的指标后续用不到,只会平白增加开发、维护成本。 + +# 7. ETL 工具 + +#### 7.1. Kettle + +- Kettle是一款国外免费开源的、可视化的、功能强大的ETL工具,可以在Windows、Linux、Unix上运行,数据抽取高效稳定。 +- 缺点: + - 打开时速度慢 + - 性能较差 + - 存在着bug + - 用户的体验不好 + +#### 7.2. Datastage + +- Datastage是一款非常专业的ETL处理工具,为整个 ETL 过程提供了一个图形化的开发环境,它是一套专门对多种操作数据源的数据抽取、转换和维护过程进行简化和自动化,并将其输入数据集或数据仓库的集成工具。 +- 缺点: + - 价格比较昂贵,企业版的花费每月需好几万的人民币。 + +#### 7.3. Informatica + +- Informatica与Datastage旗鼓相当,也是一款专业的商业ETL处理工具,依靠图形化的操作界面,无需编程语言便可以完成ETL过程的操作。 +- 缺点: + - 虽然价格比Datastage略低,但要部署的话也需要不少的预算。 \ No newline at end of file diff --git "a/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\347\261\273\345\236\213.md" new file mode 100644 index 00000000000..ab618edc2b4 --- /dev/null +++ "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -0,0 +1,87 @@ +--- +layout: post +title: ETL方法论:数据类型 +subtitle: +date: 2022-01-31 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - ETL +--- + +# 1. 多种类型的数据 + +#### 1.1. 结构化数据 + +有完整的结构规则,如 MySQL、Oracle、Excel 表等。 + +#### 1.2. 半结构化数据 + +由基本固定结构模式的数据,比如日志文件、XML文档、JSON文档、email等。 + +#### 1.3. 非结构化数据 + +无固定格式的数据,如Word、PDF、PPT、图片、音频、视频等。 + +# 2. 数据提取 + +#### 2.1. 提取结构化数据 + +可以通过关系型数据库表形式进行存储。 + +#### 2.2. 提取半结构化数据 + +可以通过固定规则提取数据信息,实时数据经常遇到这类数据,比如通过 kafka 传送 JSON 数据流。 + +一般情况下,在数据接入环节、就需要提取结构化的信息。 + +#### 2.3. 提取非结构化数据 + +需要通过数据挖掘、机器学习等算法,提取结构化信息。 + +一般不在数据接入过程中,进行信息提取。因为非结构化数据没有明显特征,提取的时候需要大量样本进行学习、训练,如果算法模型有问题、这会导致源数据的大量损失。 + +多数时候会存在 HDFS、FTP、华为云obs、阿里云oss等,然后将文件路径存储为结构化数据,在使用的时候通过文件路径找到文件。 + +![]({{site.baseurl}}/img-post/etl-5-1.png) + + +# 3.非结构化数据 + +#### 3.1. 文本信息提取 + +- 要素信息: + - 姓名 + - 身份证号码 + - 电话 + - 地址 + - 账号 + - 性别 + - 年龄等 + +- 关键词摘要: + - 关键词 + - 段落 + - 摘要文本 + +- 关系提取: + - 人员关系 + +#### 3.2. 音频信息提取 + +- 特征信息: + - 声纹特征、 + - 语种特征 + +- 语音转文本: + - 文本信息提取 + +#### 3.3. 视频图片信息提取 + +- 特征信息: + - 人像信息、 + - 物品信息、 + - 场景信息、 + - 字幕信息 + \ No newline at end of file diff --git "a/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\350\257\273\345\217\226.md" "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\350\257\273\345\217\226.md" new file mode 100644 index 00000000000..eb10a00293d --- /dev/null +++ "b/_posts/2022-01-31-ETL\346\226\271\346\263\225\350\256\272\357\274\232\346\225\260\346\215\256\350\257\273\345\217\226.md" @@ -0,0 +1,108 @@ +--- +layout: post +title: ETL方法论:数据读取 +subtitle: +date: 2022-01-31 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - ETL +--- + + +# 1. 业务表属性分类 + +> 事实表与维度表相结合,可以有效解决海量数据的冗余问题。 + +#### 1.1. 字典表 + +字典表,存储的是每个字段的映射关系。 + +> 字典表特性: +> +> - 结构简单; +> - 数据规模小,数据值有限、可穷举; +> - 更新频率低。 + +#### 1.2. 事实表 + +- 描述某一事务的活动信息,数据规模大。 +- 比如:账单信息表: + - 比如:账单ID、创建时间、金额、商品ID、账单状态等; +- 分类: + - 事务型事实表:一旦发生就不会改变,比如用户日志; + - 周期型事实表:比如待支付订单,在完成支付后,这条数据就会发生变更; + +#### 1.3. 维度表 + +- 描述实时表某一维度的特性信息,数据结构复杂; +- 商品维度表: + - 商品ID、商品名称、商品类型、商品规格、商品单价; +- 用户维度表: + - 用户ID、用户姓名、用户性别、用户年龄、用户电话。 + +# 2. 数据同步策略 + +#### 2.1. 字典表 + +- 同步方式:全量同步; +- 同步频率:一周一次、一个月一次、一年一次; + +#### 2.2. 事实表 + +- 同步方式:增量更新、增量追加 +- 同步频率:结合业务要求制定,实时性、准确性; + > 注意:事实表会涉及到更新,比如订单的支付状态,需考虑数据一致性、进行更新维护。 + +#### 2.3. 维度表 + +- 同步方式:根据数据规模制定,规模大的增量同步,规模小的全量同步; +- 同步频率:实时同步、准实时同步、离线同步; + > 注意一定要结合业务考虑,比如地震数据虽然很少,但是对实时性要求特别高; + +# 3. 数据同步方式 + +#### 3.1. 全量同步 + +- 全量同步,对应的是【批处理】; +- 库对库,或直接对文件拉取; +- 写入目标表前先清空目标表; +- 注意来源表、目标表顺序; + - 正常情况下,来源表、目标表是有读写权限限制的,以防止出现来源表、目标表顺序颠倒问题; +- 拉链表 + - TODO + +#### 3.2. 增量同步 + +- 增量同步,对应的是【流式处理】。 + +> 增量同步,要求表中一定要有增量字段,以判定数据是否为增量数据。 +> + +- 实时同步手段: + - binlog + - cdc + - kafka + - flink + - sparkstreaming + +- 准实时同步 / 离线同步手段 + - 固定变量:T+1 + - 数据偏移量:记录上次同步位置 + +#### 3.3. 其他信息 + +- 公共字段: + - 入库时间 + - 更新时间 + - 业务数据 MD5 值 + +# 4. 数据读取技术 + +#### 4.1. ODBC + + + +#### 4.2. JDBC + diff --git "a/_posts/2022-02-01-MySQL 5.7 \344\270\216 8.0 \347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" "b/_posts/2022-02-01-MySQL 5.7 \344\270\216 8.0 \347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" new file mode 100644 index 00000000000..9df80f51894 --- /dev/null +++ "b/_posts/2022-02-01-MySQL 5.7 \344\270\216 8.0 \347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" @@ -0,0 +1,129 @@ +--- +layout: post +title: MySQL 5.7 与 8.0 的对比分析 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 数据库 +--- + + +# 功能对比 + +#### NoSQL支持: + +MySQL 从 5.7 版本开始提供 NoSQL 存储功能,在 8.0 版本中这部分功能也得到了更大的改进。 + +#### JSON: + +MySQL 8 大幅改进了对 JSON 的支持,添加了基于路径查询参数从 JSON 字段中抽取数据的 JSON_EXTRACT() 函数,以及用于将数据分别组合到 JSON 数组和对象中的 JSON_ARRAYAGG() 和 JSON_OBJECTAGG() 聚合函数。 + + +- Varchar VS JSON: + - 大家都应该知道,在没有JSON数据类型时,还不是一样可以使用varchar类型或text类型来存储就JSON格式的字符串,那么,JSON格式到底哪些“过人”之处呢?好处还真不少呢。 + - JSON数据类型,会自动校验数据是否为JSON格式,如果不是JSON格式数据,则会报错。 + - MySQL提供了一组操作JSON数据的内置函数。 + - 优化的存储格式,存储在JSON列中的JSON数据被转换成内部的存储格式。其允许快速读取。 + - 可以修改特定的键值,(如果之前在MySQL中存储过JSON格式字符串的小伙伴,应该经历过每次修改一个值,都要将整个JSON字符串更新一遍的尴尬。) + +#### 窗口函数: + +从 MySQL 8.0 开始,新增了一个叫窗口函数的概念,它可以用来实现若干新的查询方式。 + +窗口函数与 SUM()、COUNT() 这种集合函数类似,但它不会将多行查询结果合并为一行,而是将结果放回多行当中。即窗口函数不需要 GROUP BY。 + +#### 隐藏索引: + +在 MySQL 8.0 中,索引可以被“隐藏”和“显示”。当对索引进行隐藏时,它不会被查询优化器所使用,我们可以使用这个特性用于性能调试。 + +在创建完索引后,我们先将其隐藏,然后观察其对数据库的影响。如果数据库性能有所下降,说明这个索引是有用的,然后将其“恢复显示”即可。如果数据库性能看不出变化,说明这个索引是多余的,可以考虑删掉。 + +#### 降序索引: + +MySQL 8.0 为索引提供按降序方式进行排序的支持,在这种索引中的值也会按降序的方式进行排序。 + +#### 通用表表达式式(Common Table Expressions CTE): + +在复杂的查询中使用嵌入式表时,使用 CTE 使得查询语句更清晰。 + +#### UTF-8 编码: + +从 MySQL 8 开始,使用 utf8mb4 作为 MySQL 的默认字符集。 + +#### 支持表 DDL 的原子性: + +InnoDB 现在支持表 DDL 的原子性,也就是 InnoDB 表上的 DDL 也可以实现事务完整性,要么失败回滚,要么成功提交,不至于出现 DDL 时部分成功的问题,此外还支持 crash-safe 特性,元数据存储在单个事务数据字典中。 + +#### 支持 crash-safe 特性: + +元数据存储在单个事务数据字典中。 + +#### 高可用性: + +InnoDB 集群为数据库提供集成的原生 HA 解决方案。 + +#### caching_sha2_password 身份验证插件: + +caching_sha2_password是MySQL 8.0中的默认身份验证插件,替换了mysql 5.7的mysql_native_password,身份验证安全性能提升。 + +#### 授权 + +与帐户管理相关的授权语法略有差异: + +MySQL5.7创建用户和用户授权命令可以同时执行 + +`grant all privileges on . to 'Gary'@'%' identified by 'Gary@2019'` + +MySQL8.0创建用户和用户授权的命令需要分开执行 + +#### 创建用户 + +`create user 'Gary'@'%' identified by 'Gary@2019';` + +#### 用户授权【给予所有权限】 + +`grant all privileges on . to 'Gary'@'%'` + +# 性能对比 + +根据官方说法,MySQL 8 要比 MySQL 5.7 快 2 倍。 + +性能提升主要集中在三个领域: +- 读写工作负载 +- I/O密集型工作负载 +- 热点竞争问题工作负载。 + +以下几张图,是腾讯团队测试的MySQL 5.7 与 8.0 的对比分析数据,可以作为参考。 + + +![]({{site.baseurl}}/img-post/mysql5.7vs8.0-1.png) + +![]({{site.baseurl}}/img-post/mysql5.7vs8.0-1.png) + +![]({{site.baseurl}}/img-post/mysql5.7vs8.0-1.png) + +![]({{site.baseurl}}/img-post/mysql5.7vs8.0-1.png) + + +#### 当线程数量增加时,MySQL 8.0明显优于MySQL 5.7! + + +#### 降序索引 + +#### MySQL 8.0 降序索引 避免了“filesort”,显著提升了读取性能。 + +在8.0版本中,影响MySQL读取性能的重要新增支持是:可以按降序(或正向索引扫描)创建索引的能力。从语法上,MySQL 4就支持了降序索引,但正如官方文档所言,"they are parsed but ignored",实际创建的还是升序索引。 +以前的版本只有升序或 **反向索引扫描**,如果需要降序,MySQL必须执行filesort(如果需要filesort,需要检查max_length_for_sort_data的值)。当最有效的扫描顺序混合某些列的升序和其他列的降序时,降序索引还使优化器可以使用多列索引。 +如果只对单个列进行排序,降序索引的意义不是太大,无论是升序还是降序,升序索引完全可以应付。**如果一个查询,需要对多个列进行排序,且顺序要求不一致。在这种场景下,要想避免数据库额外的排序-“filesort”,最好的方法就是使用降序索引。** + +#### 避免隐式排序 + +由于降序索引的引入,MySQL 8.0再也不会对group by操作进行隐式排序。 + +# 参考文献: +- 《 [mysql 5.7与8.0区别](http://www.vivianwei808.top/archives/mysql5780)》; +- 《[MySQL性能基准测试对比:MySQL 5.7与MySQL 8.0](https://zhuanlan.zhihu.com/p/58706113)》 + diff --git "a/_posts/2022-02-01-MySQL B+ \346\240\221\347\264\242\345\274\225\345\216\237\347\220\206.md" "b/_posts/2022-02-01-MySQL B+ \346\240\221\347\264\242\345\274\225\345\216\237\347\220\206.md" new file mode 100644 index 00000000000..5f096a981ce --- /dev/null +++ "b/_posts/2022-02-01-MySQL B+ \346\240\221\347\264\242\345\274\225\345\216\237\347\220\206.md" @@ -0,0 +1,139 @@ +--- +layout: post +title: MySQL B+ 树索引原理 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 数据库 +--- + + +# 1. MySQL 索引 + +- MySQL 的数据是持久化的,意味着数据(索引+记录)是保存到磁盘上的,因为这样即使设备断电了,数据也不会丢失。 + +- 设计存储的时候,不单单要从数据结构的角度出发,还要考虑磁盘 I/O 操作次数。 + - 内存的访问速度是纳秒级别的,而磁盘访问的速度是毫秒级别的; + - 也就是说读取同样大小的数据,磁盘中读取的速度比从内存中读取的速度要慢上万倍,甚至几十万倍; + +- 磁盘读写的最小单位是扇区,扇区的大小只有 512B 大小,操作系统一次会读写多个扇区,操作系统的最小读写单位是块(Block)。 + - Linux 中的块大小为 4KB; + - 也就是一次磁盘 I/O 操作会直接读写 8 个扇区; + +- 由于数据库的索引是保存到磁盘上的,因此当我们通过索引查找某行数据的时候,就需要先从磁盘读取索引到内存,再通过索引从磁盘中找到某行数据,然后读入到内存; + - 也就是说查询过程中会发生多次磁盘 I/O; + - 而磁盘 I/O 次数越多,所消耗的时间也就越大; + +- 索引的数据结构,要能在尽可能少的磁盘的 I/O 操作中完成查询工作; + - 因为磁盘 I/O 操作越少,所消耗的时间也就越小。 + +- MySQL 支持范围查找 + - 索引的数据结构不仅要能高效地查询某一个记录,而且也要能高效地执行范围查找。 + + +# 2. 二叉树 + +![]({{site.baseurl}}/img-post/mysql-1.gif) + +- 二叉查找树的特点是: + - 节点左子树的所有节点、都小于这个节点; + - 节点右子树的所有节点、都大于这个节点 + - 这样我们在查询数据时,不需要计算中间节点的位置了,只需将查找的数据与节点的数据进行比较。 + +- 二叉树解决了插入新节点的问题: + - 因为二叉查找树是一个跳跃结构,不必连续排列; + - 这样,在插入的时候,新节点可以放在任何位置,不会像线性结构那样插入一个元素,所有元素都需要向后排列。 + +# 3. 平衡二叉树 + +- 二叉查找树存在一个极端情况: + - 当每次插入的元素都是二叉查找树中最大的元素,二叉查找树就会退化成了一条链表,查找数据的时间复杂度变成了 O(n)。 + + ![]({{site.baseurl}}/img-post/mysql-1.gif) + +- 为了解决在极端情况下退化成链表的问题,后面就有人提出平衡二叉树(AVL 树)。 + +- 平衡二叉树主要是在二叉查找树的基础上,增加了一些条件约束: + - 每个节点的左子树和右子树的高度差不能超过 1; + - 也就是说,节点的左子树和右子树仍然为平衡二叉树,这样查询操作的时间复杂度就会一直维持在 O(logn) 。 + + ![]({{site.baseurl}}/img-post/mysql-3.gif) + +# 4. 三叉树 + +- 随着插入的元素增多,平衡二叉树的高度变高,这就意味着磁盘 I/O 操作次数多,会影响整体数据查询的效率。 +- 比如: + - 下面这个平衡二叉查找树的高度为 5,那么在访问最底部的节点时,就需要磁盘 5 次 I/O 操作。 + + ![]({{site.baseurl}}/img-post/mysql-2.png) + + - 原因是因为它们都是二叉树,也就是每个节点只能保存 2 个子节点,如果我们把二叉树改成 M 叉树(M>2)呢? + +- 比如: + - 当 M=3 时,在同样的节点个数情况下,三叉树比二叉树的树高要矮。 + + ![]({{site.baseurl}}/img-post/mysql-3.png) + +# 5. B 树 + +- 平衡二叉树不足: + - 平衡二叉树虽然能保持查询操作的时间复杂度在O(logn),但是因为它本质上是一个二叉树,每个节点只能有 2 个子节点; + - 当节点个数越多的时候,树的高度也会相应变高,这样就会增加磁盘的 I/O 次数,从而影响数据查询的效率。 + +- B 数概念: + - 为了解决降低树的高度的问题,后面就出来了 B 树,它不再限制一个节点就只能有 2 个子节点,而是允许 M 个子节点 (M>2),从而降低树的高度。 + - B 树的每一个节点最多可以包括 M 个子节点,M 称为 B 树的阶,所以 B 树就是一个多叉树。 + - 假设 M = 3,那么就是一棵 3 阶的 B 树,特点就是每个节点最多有 2 个(M-1个)数据和最多有 3 个(M个)子节点,超过这些要求的话,就会分裂节点, + +- 比如:下面的的动图: + + ![]({{site.baseurl}}/img-post/mysql-4.gif) + + - 可以看到: + - 一棵 3 阶的 B 树在查询叶子节点中的数据时,由于树的高度是 3 ,所以在查询过程中会发生 3 次磁盘 I/O 操作。 + - 而如果同样的节点数量,在平衡二叉树的场景下,树的高度就会很高,意味着磁盘 I/O 操作会更多。所以,B 树在数据查询中比平衡二叉树效率要高。 + +# 6. B+ 树 + +- B 数的不足: + + - B 树的每个节点都包含数据(索引+记录),而用户的记录数据的大小、很有可能远远超过了索引数据,这就需要花费更多的磁盘 I/O 操作次数来读到有用的索引数据。 + + - 而且,在我们查询位于底层的某个节点(比如 A 记录)过程中,非 A 记录节点里的记录数据会从磁盘加载到内存,但是这些记录数据是没用的,我们只是想读取这些节点的索引数据来做比较查询,而非 A 记录节点里的记录数据对我们是没用的,这样不仅增多磁盘 I/O 操作次数,也占用内存资源。 + + - 另外,如果使用 B 树来做范围查询的话,需要使用中序遍历,这会涉及多个节点的磁盘 I/O 问题,从而导致整体速度下降。 + + ![]({{site.baseurl}}/img-post/mysql-4.png) + + - 这个时候,就需要使用到 B+ 树; + +- B+ 树的特点: + - B+ 树一个节点存储多个元素,可以使得树高不会太高; + - MySQL 中一个 Innodb 页就是一个 B+ 树节点,一个 Innodb 页默认为 16kb; + - 一般情况下,一颗两层的 B+ 树可以存 2000万 行左右的数据; + - B+ 树叶子节点存储全部数据,叶子结点之间有指针,可以很好的支持全表扫描、范围查找等 SQL 操作; + +- MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有: + - B+ 树的非叶子节点不存放实际的记录数据,仅存放索引; + - 因此,数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+ 树的非叶子节点可以存放更多的索引; + - 因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。 + - B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引); + - 这些冗余索引让 B+ 树在插入、删除的效率都更高; + - 比如: + - 删除根节点的时候,不会像 B 树那样会发生复杂的树的变化; + - B+ 树叶子节点之间用链表连接了起来,有利于范围查询; + - 而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询; + - 这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。 + +# 7. B+ 树与 B 树差异的点 + +- 主要差异点: + - B+ 树叶子节点(最底部的节点)才会存放实际数据(索引+记录),非叶子节点只会存放索引; + - B+ 树所有索引都会在叶子节点出现,叶子节点之间构成一个有序链表; + - B+ 树非叶子节点的索引也会同时存在在子节点中,并且是在子节点中所有索引的最大(或最小)值。 + - B+ 树非叶子节点中有多少个子节点,就有多少个索引; + + diff --git "a/_posts/2022-02-01-MySQL \345\255\230\345\202\250\350\277\207\347\250\213\346\246\202\345\277\265\345\217\212\345\256\236\344\276\213.md" "b/_posts/2022-02-01-MySQL \345\255\230\345\202\250\350\277\207\347\250\213\346\246\202\345\277\265\345\217\212\345\256\236\344\276\213.md" new file mode 100644 index 00000000000..82cf73e6f5d --- /dev/null +++ "b/_posts/2022-02-01-MySQL \345\255\230\345\202\250\350\277\207\347\250\213\346\246\202\345\277\265\345\217\212\345\256\236\344\276\213.md" @@ -0,0 +1,129 @@ +--- +layout: post +title: MySQL 存储过程概念及实例 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 数据库 +--- + + +# 1. 存储过程的概念 + +存储过程(Stored Procedure)是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来调用存储过程。 + + +# 2. 存储过程的价值 + +#### 2.1. 效率高 +存储过程编译一次后,就会存到数据库,每次调用时都直接执行。而普通的sql语句我们要保存到其他地方(例如:记事本上),都要先分析编译才会执行。所以想对而言存储过程效率更高。 + +#### 2.2. 降低网络流量 + +存储过程编译好会放在数据库,我们在远程调用时,不会传输大量的字符串类型的sql语句。 + +#### 2.3. 复用性高 + +存储过程往往是针对一个特定的功能编写的,当再需要完成这个特定的功能时,可以再次调用该存储过程。 + +#### 2.4. 可维护性高 + +当功能要求发生小的变化时,修改之前的存储过程比较容易,花费精力少。 + +#### 2.5. 安全性高 + +完成某个特定功能的存储过程一般只有特定的用户可以使用,具有使用身份限制,更安全。 + +# 3. 创建存储过程 + +>在创建存储过程时,必须具有 CREATE ROUTINE 权限。 + +#### 3.1. CREATE PROCEDURE 语法 + +```aidl +CREATE PROCEDURE <过程名> ( [过程参数[,…] ] ) +<过程体> +[过程参数[,…] ] 格式 +[ IN | OUT | INOUT ] <参数名> <类型> +``` +- 过程名 + - 存储过程的名称,默认在当前数据库中创建。 + 若需要在特定数据库中创建存储过程,则要在名称前面加上数据库的名称,即 db_name.sp_name。 +- 过程参数 + - 存储过程的参数列表。 + 其中,<参数名>为参数名,<类型>为参数的类型(可以是任何有效的 MySQL 数据类型)。 + 当有多个参数时,参数列表中彼此间用逗号分隔。 + 存储过程可以没有参数(此时存储过程的名称后仍需加上一对括号),也可以有 1 个或多个参数。 + >MySQL 存储过程支持三种类型的参数,即输入参数、输出参数和输入/输出参数,分别用 IN、OUT 和 INOUT 三个关键字标识。其中,输入参数可以传递给一个存储过程,输出参数用于存储过程需要返回一个操作结果的情形,而输入/输出参数既可以充当输入参数也可以充当输出参数。 + - 注意的是,参数的取名不要与数据表的列名相同,否则尽管不会返回出错信息,但是存储过程的 SQL 语句会将参数名看作列名,从而引发不可预知的结果。 +- 过程体 + - 存储过程的主体部分,也称为存储过程体,包含在过程调用的时候必须执行的 SQL 语句。 + - `BEGIN` 和 `END` 关键字 + - 这个部分以关键字 `BEGIN` 开始,以关键字 `END` 结束。 + - 若存储过程体中只有一条 SQL 语句,则可以省略 `BEGIN-END` 标志。 + - 在存储过程的创建中,经常会用到一个十分重要的 MySQL 命令,即 `DELIMITER` 命令。 + - `DELIMITER` 关键字 + - 在 MySQL 中,服务器处理 SQL 语句默认是以分号作为语句结束标志的。 + - 然而,在创建存储过程时,存储过程体可能包含有多条 SQL 语句,这些 SQL 语句如果仍以分号作为语句结束符,那么 MySQL 服务器在处理时会以遇到的第一条 SQL 语句结尾处的分号作为整个程序的结束符,而不再去处理存储过程体中后面的 SQL 语句,这样显然不行。 + - 因此,通常使用 DELIMITER 命令将结束命令修改为其他字符。 + - 语法格式如下: + ``` + DELIMITER $$ + ``` + >当使用 DELIMITER 命令时,应该避免使用反斜杠“\”字符,因为它是 MySQL 的转义字符。 + +# 4. 存储过程实例 + +#### 4.1. 获取结果 + +``` +delimiter $$ +create procedure testa() +begin + select * from test_table; +end $$ +delimiter ; +-- 调用存储过程 + +call testa; +``` + +#### 4.2. 加减法计算 + +```aidl +# 创建存储过程 +create procedure testpro(in a int,in b int,out sum int) +begin + set sum = a+b; +end; + + +# 调用存储过程 +call testpro(1,2,@s); -- 调用存储过程 +select @s; -- 显示过程输出结果 +``` + +#### 4.3. 变量赋值 + +```aidl +# 变量赋值 +SET 变量名 = 表达式值 [,variable_name = expression ...] + + +# 使用变量 +use schooldb; -- 使用 schooldb 数据库 + +create procedure testpro() +begin + declare name varchar(20); + set name = '小明'; + select * from studentinfo where studentname = name; +end; + + +# 调用过程 +call testpro(); +``` \ No newline at end of file diff --git "a/_posts/2022-02-01-MySQL \345\270\270\347\224\250\345\221\275\344\273\244\346\261\207\346\200\273.md" "b/_posts/2022-02-01-MySQL \345\270\270\347\224\250\345\221\275\344\273\244\346\261\207\346\200\273.md" new file mode 100644 index 00000000000..e730ba7f54d --- /dev/null +++ "b/_posts/2022-02-01-MySQL \345\270\270\347\224\250\345\221\275\344\273\244\346\261\207\346\200\273.md" @@ -0,0 +1,351 @@ +--- +layout: post +title: MySQL 常用命令汇总 +subtitle: 表维护 & DB运维 +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: +- 数据库 +--- + + +# 1. 表结构创建维护 + +- 修改 primary key + + ```aidl + # 删除原有主键 + alter table tablename drop PRIMARY KEY + + # 重新创建主键 + alter table tablename ADD PRIMARY KEY('col_name') + ``` + +- 添加索引 + + ```aidl + create index `idx_add_time` on ods.ods_oms_product (add_time); + + ALTER TABLE ods.ods_ad原始表 ADD KEY INDEX idx_date(`日期`); + + ``` + +- 添加多列索引 + + ```aidl + ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` ) + ``` + +- 添加全文索引 + + ```aidl + ALTER TABLE `table_name` ADD FULLTEXT ( `column`) + ``` + +- 添加字段 + + ```aidl + ALTER TABLE table_name ADD COLUMN column_name + VARCHAR(100) DEFAULT NULL COMMENT '新加字段' AFTER old_column; + ``` + +- 添加唯一键 + + ```aidl + alter table question_tag_map add unique key `qtag2` (`question_id`,`question_tag_id`); + ``` + +- 删除唯一键 + + ```aidl + alter table question_tag_map drop index `qtag2`; + ``` + +- 复制表: + + ``` + create table tb_test01 like tb_test02; + ``` + +- 复制表数据: + + ```aidl + insert into tb_test01 select * from tb_test02; + ``` + +- 修改表名 + + ```aidl + alter table ods.`ods_oms_b2b_stock_out_order_detail_new` rename [to|as] ods.`ods_oms_b2b_stock_out_order_detail` + ``` + +- 修改列名 / 重命名字段 + + ```aidl + ALTER TABLE 表名 CHANGE 旧字段名 新字段名 字段类型(长度); + ``` + +- 添加时间戳字段 + + ```aidl + alter table quant_stk_calc_d_wxcp add update_time timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; + ``` + +# 2. 增删改查 + +#### 2.1. 联合查询 + +- 多表联查 + + ``` + SELECT * FROM oms_biz.oms_stock_loan_out st1, oms_biz.oms_stock_loan_out_detail st2 + where st1.stock_loan_out_id = st2.stock_loan_out_id; + ``` + +- 多表查询相同字段名 + + ```aidl + SELECT st1.modified_time, st2.modified_time FROM oms_biz.oms_stock_loan_out st1, oms_biz.oms_stock_loan_out_detail st2; + ``` + +#### 2.2. CAST 函数 + +```aidl +select cast('01-11-11' as DATE); +``` + +![]({{site.baseurl}}/img-post/mysql-1.png) + + +# 3. 清洗转换 + +#### 3.1. 数据探查 + +- 查看某字段不重复的全部值 + + ``` + SELECT DISTINCT(字段) FROM test.test; + ``` + + +#### 3.2. 数据校验 + +- 正则表达式:检测科学计数法 + + ``` + SELECT * FROM test.test WHERE 日期 regexp '[E|e]+'; + ``` + +- 检测空值 + + ``` + SELECT * FROM test.test WHERE ISNULL(非空字段)=1; + ``` + +#### 3.3. 数据转换 + +- SQL 替换字符串 + + ```aidl + update [表名] set 字段名 = replace(与前面一样的字段名,'原本内容','想要替换成什么') + ``` + +#### 3.4. 日期计算 + +- 对比两表的 datetime 字段是否在同一天 + ``` + SELECT * FROM oms_biz.oms_stock_loan_out st1, oms_biz.oms_stock_loan_out_detail st2 + where TO_DAYS(st1.modified_time) != TO_DAYS(st2.modified_time); + ``` +- 日期时间字段类型 datetime 转换为日期(年-月-日)的方法 + - 方法1,使用DATE_FORMAT + + ``` + DATE_FORMAT(time, '%Y-%m-%d') + ``` + + - 方法2,使用 CAST + + ``` + CAST(time AS DATE) + ``` + +- 查询指定日期 + + – 今天 + + ``` + SELECT * FROM 表名 WHERE TO_DAYS(时间字段名) = TO_DAYS(NOW()); + ``` + + – 昨天 + + ``` + SELECT * FROM 表名 WHERE TO_DAYS(NOW()) - TO_DAYS( 时间字段名) <= 1; + ``` + + – 近7天 + + ``` + SELECT * FROM 表名 where DATE_SUB(CURDATE(), INTERVAL 7 DAY) <= date(时间字段名); + ``` + + – 近30天 + + ``` + SELECT * FROM 表名 where DATE_SUB(CURDATE(), INTERVAL 30 DAY) <= date(时间字段名); + ``` + + – 本月 + + ``` + SELECT * FROM 表名 WHERE DATE_FORMAT( 时间字段名, ‘%Y%m’ ) = DATE_FORMAT( CURDATE( ) , ‘%Y%m’ ); + ``` + + – 上一月 + + ``` + SELECT * FROM 表名 WHERE PERIOD_DIFF( date_format( now( ) , ‘%Y%m’ ) , date_format( 时间字段名, ‘%Y%m’ ) ) =1; + ``` + + – 查询本季度数据 + + ``` + Select * from 表名 where QUARTER(时间字段名)=QUARTER(now()); + ``` + + – 查询上季度数据 + + ``` + Select * from 表名 where QUARTER(时间字段名)=QUARTER(DATE_SUB(now(),interval 1 QUARTER)); + ``` + + – 查询本年数据 + + ``` + Select * from 表名 where YEAR(时间字段名)=YEAR(NOW()); + ``` + + – 查询上年数据 + + ``` + Select * from 表名 where year(时间字段名)=year(date_sub(now(),interval 1 year)); + ``` + + – 查询当前这周的数据 + + ``` + SELECT * FROM 表名 WHERE YEARWEEK(date_format(时间字段名,’%Y-%m-%d’)) = YEARWEEK(now()); + ``` + + – 查询上周的数据 + + ``` + SELECT * FROM 表名 WHERE YEARWEEK(date_format(时间字段名,’%Y-%m-%d’)) = YEARWEEK(now())-1; + ``` + + – 查询上个月的数据 + + ``` + Select * from 表名 where date_format(时间字段名,’%Y-%m’)=date_format(DATE_SUB(curdate(), INTERVAL 1 MONTH),’%Y-%m’); + ``` + + ``` + Select * from 表名 where DATE_FORMAT(时间字段名,’%Y%m’) = DATE_FORMAT(CURDATE(),’%Y%m’); + ``` + + ``` + Select * from 表名 where WEEKOFYEAR(FROM_UNIXTIME(时间字段名,’%y-%m-%d’)) = WEEKOFYEAR(now()); + ``` + + ``` + Select * from 表名 where MONTH(FROM_UNIXTIME(时间字段名,’%y-%m-%d’)) = MONTH(now()); + ``` + + ``` + Select * from 表名 where YEAR(FROM_UNIXTIME(时间字段名,’%y-%m-%d’)) = YEAR(now()) and MONTH(FROM_UNIXTIME(pudate,’%y-%m-%d’)) = MONTH(now()); + ``` + + ``` + Select * from 表名 where 时间字段名 between 上月最后一天 and 下月第一天; + ``` + + – 查询当前月份的数据 + + ``` + Select * from 表名 where date_format(时间字段名,’%Y-%m’)=date_format(now(),’%Y-%m’); + ``` + + – 查询距离当前现在6个月的数据 + + ``` + Select * from 表名 where 时间字段名 between date_sub(now(),interval 6 month) and now(); + ``` + + – 查询某个月的数据(查询18年10月份数据) + + ``` + Select * from 表名 where date_format(时间字段名,’%Y-%m’)=‘2018-10’; + ``` + + ``` + Select * from 表名 where date_format(时间字段名,’%Y-%m’)=date_format(‘2018-10-05’,’%Y-%m’); + ``` + + +# 4. 管理配置 + +- 卸载&清除残留文件 + + ```aidl + sudo apt purge mysql-* + + sudo rm -rf /etc/mysql/ /var/lib/mysql + + sudo apt autoremove + + sudo apt autoclean + + 或者: + + sudo apt-get remove mysql-* + + dpkg -l |grep ^rc|awk '{print $2}' |sudo xargs dpkg -P + + ``` + +- 修改密码 + + ```aidl + use mysql; + + GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION; --创建远程root用户并设置密码 + + flush privileges; + ``` + +- 查看端口号 + + ```aidl + show global variables like 'port'; + ``` + +# 4. 异常处理 + +- Invalid for this platform protocol requested + + - 问题: + - 使用 workbench 连接 MySQL 数据库,workbench 客户端连接失败, + + - 报错: + ```aidl + Invalid for this platform protocol requested(MYSQL_PROTOCOL_SOCKET) + ``` + + - 处理方法: + - 删除现有的连接,从头开始、重新创建一个连接。 + + +` \ No newline at end of file diff --git "a/_posts/2022-02-01-MySQL \350\241\250\350\247\206\345\233\276\347\232\204\344\275\277\347\224\250.md" "b/_posts/2022-02-01-MySQL \350\241\250\350\247\206\345\233\276\347\232\204\344\275\277\347\224\250.md" new file mode 100644 index 00000000000..f891480a0db --- /dev/null +++ "b/_posts/2022-02-01-MySQL \350\241\250\350\247\206\345\233\276\347\232\204\344\275\277\347\224\250.md" @@ -0,0 +1,137 @@ +--- +layout: post +title: MySQL 表视图的使用 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据库 +--- + + +# 1. 表视图 + +- 视图是一种 虚拟表 ,本身是 不具有数据 的,占用很少的内存空间,它是 SQL 中的一个重要概念。 +- 视图建立在已有表的基础上, 视图赖以建立的这些表称为基表。 + + ![]({{site.baseurl}}/img-post/mysql-6.png) + +- 视图的创建和删除只影响视图本身,不影响对应的基表。但是当对视图中的数据进行增加、删除和修改操作时,数据表中的数据会相应地发生变化,反之亦然。 + +- 向视图提供数据内容的语句为 SELECT 语句, 可以将视图理解为存储起来的 SELECT 语句。 + +- 在数据库中,视图不会保存数据,数据真正保存在数据表中。当对视图中的数据进行增加、删除和修改操作时,数据表中的数据会相应地发生变化;反之亦然。 + +- 视图,是向用户提供基表数据的另一种表现形式。通常情况下,小型项目的数据库可以不使用视图,但是在大型项目中,以及数据表比较复杂的情况下,视图的价值就凸显出来了,它可以帮助我们把经常查询的结果集放到虚拟表中,提升使用效率。 + +# 2. 视图使用 + +- 建表 SQL + + ``` + CREATE VIEW 视图名称 AS 查询语句 + ``` + +- 示例 + ``` + CREATE VIEW empvu80 + AS + SELECT employee_id, last_name, salary + FROM employees + WHERE department_id = 80; + ``` + +- 利用视图对数据进行计算处理 + ``` + CREATE VIEW emp_year_salary (ename,year_salary) + AS + SELECT ename,salary*12*(1+IFNULL(commission_pct,0)) + FROM t_employee; + ``` + +- 利用视图对数据进行格式化 + ``` + CREATE VIEW emp_depart + AS + SELECT CONCAT(last_name,'(',department_name,')') AS emp_dept + FROM employees e JOIN departments d + WHERE e.department_id = d.department_id + ``` + +- 基于视图创建视图 + ``` + CREATE VIEW emp_dept_ysalary + AS + SELECT emp_dept.ename,dname,year_salary + FROM emp_dept INNER JOIN emp_year_salary + ON emp_dept.ename = emp_year_salary.ename; + ``` + +# 3. 视图增删改 + +#### 3.1. 视图更新 + +- MySQL支持使用 INSERT、UPDATE 和 DELETE 语句对视图中的数据进行插入、更新和删除操作。 +- 当视图中的数据发生变化时,数据表中的数据也会发生变化,反之亦然。 +- 要使视图可更新,视图中的行和底层基本表中的行之间必须存在 **一对一** 的关系。 + +- 当视图定义出现如下情况时,视图不支持更新操作: + - 在定义视图的时候指定了 “ALGORITHM = TEMPTABLE”,视图将不支持 INSERT 和 DELETE 操作; + - 视图中不包含基表中所有被定义为非空又未指定默认值的列,视图将不支持 INSERT 操作; + - 在定义视图的 SELECT 语句中使用了 JOIN 联合查询 ,视图将不支持 INSERT 和 DELETE 操作; + - 在定义视图的 SELECT 语句后的字段列表中使用了 数学表达式 或 子查询 ,视图将不支持 INSERT,也不支持 UPDATE 使用了数学表达式、子查询的字段值; + - 在定义视图的 SELECT 语句后的字段列表中使用 DISTINCT 、 聚合函数 、 GROUP BY 、 HAVING 、 UNION 等,视图将不支持 INSERT、UPDATE、DELETE; + - 在定义视图的 SELECT 语句中包含了子查询,而子查询中引用了 FROM 后面的表,视图将不支持 INSERT、UPDATE、DELETE; + - 视图定义基于一个 不可更新视图 ; + - 常量视图 + +#### 3.2. 视图修改 + +- CREATE OR REPLACE VIEW + ``` + CREATE OR REPLACE VIEW empvu80 + (id_number, name, sal, department_id) + AS + SELECT employee_id, first_name || ' ' || last_name, salary, department_id + FROM employees + WHERE department_id = 80; + ``` + +- ALTER VIEW + ``` + ALTER VIEW 视图名称 + AS查询语句 + ``` + +# 4. 视图优缺点 + +#### 4.1. 试图优点 + +- 操作简单 + - 将经常使用的查询操作定义为视图,可以使开发人员不需要关心视图对应的数据表的结构、表与表之间的关联关系,也不需要关心数据表之间的业务逻辑和查询条件,而只需要简单地操作视图即可,极大简化了开发人员对数据库的操作。 + +- 减少数据冗余 + - 视图跟实际数据表不一样,它存储的是查询语句。所以,在使用的时候,我们要通过定义视图的查询语句来获取结果集。而视图本身不存储数据,不占用数据存储的资源,减少了数据冗余。 + +- 数据隔离 + - MySQL将用户对数据的 访问限制 在某些数据的结果集上,而这些数据的结果集可以使用视图来实现。用户不必直接查询或操作数据表。这也可以理解为视图具有 隔离性 。视图相当于在用户和实际的数据表之间加了一层虚拟表 + - 同时,MySQL可以根据权限将用户对数据的访问限制在某些视图上,用户不需要查询数据表,可以直接 通过视图获取数据表中的信息。这在一定程度上保障了数据表中数据的安全性。 + +- 适应灵活多变的需求 + - 当业务系统的需求发生变化后,如果需要改动数据表的结构,则工作量相对较大,可以使用视图来减少改动的工作量。这种方式在实际工作中使用得比较多。 + +- 能够分解复杂的查询逻辑 + - 数据库中如果存在复杂的查询逻辑,则可以将问题进行分解,创建多个视图获取数据,再将创建的多个视图结合起来,完成复杂的查询逻辑。 + +#### 4.2. 视图不足 + +- 如果我们在实际数据表的基础上创建了视图,那么,如果实际数据表的结构变更了,我们就需要及时对 相关的视图进行相应的维护。 + - 特别是嵌套的视图(就是在视图的基础上创建视图),维护会变得比较复杂, 可读性不好 ,容易变成系统的潜在隐患。因为创建视图的 SQL 查询可能会对字段重命名,也可能包含复杂的逻辑,这些都会增加维护的成本。 + +- 实际项目中,如果视图过多,会导致数据库维护成本的问题。 + +- 在创建视图的时候,你要结合实际项目需求,综合考虑视图的优点和不足,这样才能正确使用视图,使系统整体达到最优。 + + diff --git "a/_posts/2022-02-01-MySQL \350\241\250\350\256\276\350\256\241\345\222\214\344\275\277\347\224\250\350\247\204\350\214\203.md" "b/_posts/2022-02-01-MySQL \350\241\250\350\256\276\350\256\241\345\222\214\344\275\277\347\224\250\350\247\204\350\214\203.md" new file mode 100644 index 00000000000..2c376f72216 --- /dev/null +++ "b/_posts/2022-02-01-MySQL \350\241\250\350\256\276\350\256\241\345\222\214\344\275\277\347\224\250\350\247\204\350\214\203.md" @@ -0,0 +1,627 @@ +--- +layout: post +title: MySQL 表设计和使用规范 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: true +tags: + - 数据库 +--- + + +# 通用命名规范 + +#### 所有数据库对象名称,必须使用小写字母、并用下划线分割 + +#### 所有数据库对象名称,禁止使用 MySQL 保留关键字 + - 如果表名中包含关键字查询时,需要将其用单引号括起来; + +#### 所有数据库对象名称,必须见名识意,最好不要超过 32 个字符 + +#### 字段名尽量不超过 30 个字符 + +#### 临时库&表:必须以 tmp_为前缀并以日期为后缀,并加时间后缀 + +- 如:`temp前缀+模块+表+日期后缀:temp_user_eduinfo_20210719` + +#### 备份表:必须以 bak_为前缀,并以日期 (时间戳) 为后缀 + +- 如:`bak前缀+模块+表+日期后缀:bak_user_eduinfo_20210719` + +#### unique key 以 `uk_` 作为前缀 + +#### primary key 以 `pk_` 作为前缀 + +#### 普通索引一般以 `idx_` 作为前缀 + +#### 对于所有存储相同数据字段,的列名和列类型必须一致 + +- 一般作为关联列,如果查询时关联列类型不一致、会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低; + +#### 索引名尽量不超过 50 个字符 + +# 表设计规范 + +#### 表设计应至少满足第三范式 + +- 一些特殊场景允许反范式化设计,但需要对冗余字段的设计给出解释。 + +#### 不同应用的表之间应尽可能减少关联 + +- 尽量不使用外键对表之间进行关联,一般是由程序控制参照完整性,确保组件对应的表之间的独立性; +- 这样会降低系统耦合度,提高灵活性和可扩展性,方便系统或表结构的重构。 + +#### 表一般使用 Innodb 存储引擎 + +- 没有特殊要求的情况下,所有表必须使用 Innodb 存储引擎(MySQL5.5 之前默认使用 Myisam,5.6 以后默认的为 Innodb)。 + - 如果是 Innodb 无法满足的功能如:列存储,存储空间数据等,另当别论。 +- Innodb 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。 + +#### 尽可能别使用混合存储引擎 + +- 绝对禁止使用混合存储引擎。 + +#### 数据库和表的字符集统一使用 UTF8 / utf8mb4 + +- UTF8 兼容性更好; +- 统一字符集可以避免由于字符集转换产生的乱码; +- 不同的字符集进行比较前需要进行转换会造成索引失效; +- 如果数据库中有存储 emoji 表情的需要,字符集需要采用 utf8mb4 字符集。 + +#### 主键选择必须满足以下条件 + +- 唯一性 +- 非空性 +- 有序性 +- 可读性 +- 可扩展性 + +- id,建议使 8字节 unsigned bigint(20) 作为主键的数据类型; + >**无符号与有符号的区别**: 有符号允许存储负数,无符号只允许从年初正数,无符号最小值为 0,最大值根据类型不同而不同。 +- 主外键的数据类型一定要一致; +- 每个表中的主键命名保持一致; +- 一般不使用varchar类型作主键; + - varchar 不是有序的,可读性也不好; + +#### 不建议在数据库表中加外键约束 + +- 在数据表中添加外键约束,会影响性能; + - 例如: 每一次修改数据时,都要在另外的一张表中执行查询。 +- 在数据表中添加外键约束,耦合度太高,不利于解耦和扩展; +- 建议在应用层,也就是代码层面,来维持外键关系。 + +#### 选择合适的类型作为键 + +- 状态类型用 tinyint,例如:性别等; +- 时间日期使用 datetime,可读性高些; +- 不要使用 text 和 blob 数据类型,特别是 blob 必须绝对禁止。 + +#### 所有表和字段都需要添加注释 + +- 使用 comment 从句添加表和列的备注; +- 从一开始就进行数据字典的维护。 + +#### 控制单表数据量在 500 万以内 + +- 500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。 +- 可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。 + +#### 单表字段数最多不要大于50个 + +- 字段数过多的宽表,对性能影响很大。 + +#### 谨慎使用 MySQL 分区表 + +- 分区表在物理上表现为多个文件,在逻辑上表现为一个表; +- 谨慎选择分区键,跨分区查询效率可能更低; +- 建议采用物理分表的方式管理大数据。 + +#### 尽量做到冷热数据分离,减小表的宽度 + +- 减少磁盘 IO; + - 保证热数据的内存缓存命中率(表越宽,把表装载进内存缓冲池时所占用的内存也就越大,也会消耗更多的 IO); +- 更有效的利用缓存,避免读入无用的冷数据; +- 经常一起使用的列放到一个表中(避免更多的关联操作)。 + +#### 禁止在表中建立预留字段 + +- 预留字段的命名,很难做到见名识义。 +- 预留字段无法确认存储的数据类型,所以无法选择合适的类型。 +- 对预留字段类型的修改,会对表进行锁定。 + +#### 禁止在数据库中存储图片、文件等大的二进制数据 + +- 这类文件通常很大,会短时间内造成数据量快速增长; +- 数据库进行数据库读取时,通常会进行大量的随机 IO 操作,文件很大时,IO 操作很耗时。 +- 一般情况下,存储于文件服务器,数据库只存储文件地址信息。 + +#### 定期删除(或者转移)过期数据的表 + +- 一般通过分表解决,常规做法是按照2/8法则,将操作频率较低的历史数据迁移到历史表中,按照时间或者则曾Id做切割点。 + +# 字段设计规范 + +#### 优先选择符合存储需要的最小的数据类型 +- 在进行排序和创建临时表一类的内存操作时,会使用字段的长度申请内存。 +- 列的字段长度越大,建立索引时所需要的空间也就越大; + - 这样,一页中所能存储的索引节点的数量、也就越少也越少; + - 在遍历时所需要的 IO 次数也就越多,索引的性能也就越差。 + +- 示例: + - 将字符串转换成数字类型存储,如:将 IP 地址转换成整形数据 + - MySQL 提供了两个方法来处理 ip 地址: + - inet_aton,把 ip 转为无符号整型 (4-8 位) + - inet_ntoa,把整型的 ip 转为地址 + >插入数据前,先用 inet_aton 把 ip 地址转为整型,可以节省空间,显示数据时,使用 inet_ntoa 把整型的 ip 地址转为地址显示即可。 + +#### 对于非负型的数据 (如自增 ID,整型 IP) 来说,要优先使用无符号整型 UNSIGNED INT 来存储 + + - 这是因为,**无符号 UNSIGNED INT 相对于有符号可以多出一倍的存储空间**; + ``` + SIGNED INT -2147483648~2147483647 + UNSIGNED INT 0~4294967295 + ``` + +#### VARCHAR(N) 中的 N 代表的是字符数,而不是字节数 + +- 使用 UTF8 存储 255 个汉字 Varchar(255)=765 个字节; +- 过大的长度会消耗更多的内存。 + +#### UTF8 存储一个字符最大要 3 个字节 + +- UTF-8编码是变长编码,通常汉字占3个字节; +- Unicode编码一个英文等于两个字节,一个中文(含繁体)等于两个字节; + +>**字符与字节的关系:** +> +>1个字符 = 1个字节 = 8bit(ACSII码下) +> +>1个字符 = 2个字节 = 16bit(Unicode码下) + +#### 如无特殊需要,原则上单个 VARCHAR 型字段不允许超过 255 个字符 + +#### 如无特殊需要,不使用MEDIUMTEXT、TEXT、LONGTEXT类型 + +#### 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据 + +- 建议把 BLOB 或是 TEXT 列分离到单独的扩展表中,这是因为: + - MySQL 内存临时表不支持 TEXT、BLOB 这样的大数据类型,如果查询中包含这样的数据,在排序等操作时,就不能使用内存临时表,必须使用磁盘临时表进行。 + - 而且对于这种数据,MySQL 还是要进行二次查询,会使 sql 性能变得很差,但是不是说一定不能使用这样的数据类型。 +- 如果一定要使用,建议把 BLOB 或是 TEXT 列分离到单独的扩展表中; + - 注意:查询时一定不要使用 select * 而只需要取出必要的列,不需要 TEXT 列的数据时不要对该列进行查询。 + +#### TEXT 或 BLOB 类型只能使用前缀索引 + +- 因为MySQL 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引, + +#### TEXT 列上是不能有默认值 + +#### 当字符数量可能超过 20000 个的时候,才可以使用 TEXT 类型来存放字符类数据 + +#### 所有使用TEXT类型的字段必须和原表进行分拆,与原表主键单独组成另外一个表进行存放,与大文本字段的隔离 + +#### 避免使用 ENUM 类型 + +- 修改 ENUM 值需要使用 ALTER 语句; +- ENUM 类型的 ORDER BY 操作效率低,需要额外操作; +- 禁止使用数值作为 ENUM 的枚举值。 + +#### 如果业务允许,尽可能把所有列定义为 NOT NULL + +- 这是因为索引 NULL 列,需要额外的空间来保存,所以要占用更多的空间; +- 进行比较和计算时要对 NULL 值做特别的处理 + +#### NULL 值处理规范 + +- NULL 不代表 0,也不代表 0 长度的字符串; +- NULL 含义: + - 真未知; + - 尚未知; + - 不适用; + +> NULL 的长度不是 0 ! + +- 同一个数据库,对于 Null 值的处理应有统一的原则; +- 数据质量要求不高的,默认值可以为 Null,然后对特殊字段特殊处理; +- 数据质量要求较高的,默认值均为 Not Null,然后特殊字段特殊处理; + +#### 如无特殊需要,所有字段必须有默认值 + +- 默认值的设置,需要根据业务来确定。 + +#### 使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间 + +- TIMESTAMP 存储的时间范围 1970-01-01 00:00:01 ~ 2038-01-19-03:14:07 +- TIMESTAMP 占用 4 字节和 INT 相同,但比 INT 可读性高 +- 超出 TIMESTAMP 取值范围的使用 DATETIME 类型存储 + +#### 不要用字符串存储日期型的数据 + +- 缺点: + - 无法用日期函数进行计算和比较,查询时性能会极慢 + - 用字符串存储日期要占用更多的空间 + +#### 同财务相关的金额类数据必须使用 DECIMAL 类型,严禁使用 FLOAT 和 DOUBLE + +- 不要使用非精准浮点:float,double,必须使用精准浮点:decimal +- Decimal 类型为精准浮点数,在计算时不会丢失精度 +- Decimal 占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节 +- 可用于存储比 bigint 更大的整型数据 + +# 索引设计规范 + +#### 索引的设计目的 + +- 建立索引的目的,是希望通过索引进行数据查找,减少随机 IO,增加查询性能; + - 索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。 + +#### 限制每张表上的索引数量,建议单张表索引不超过 5 个 + +- 过多索引会降低查询速度 + - 索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。 + - 每个索引都需要占⽤用磁盘空间,索引越多,需要的磁盘空间就越大。 + - 原因是: + - MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划; + - 如果同时有很多个索引都可以用于查询,就会 **增加 MySQL 优化器生成执行计划的时间**,同样会降低查询性能。 +- 修改表时,索引过多会很麻烦 + - 修改表时,对索引的重构和更新很麻烦 + - 越多的索引,会使更新表变得很浪费时间 + +#### 禁止给表中的每一列都建立单独的索引 + +- 5.6 版本之前,一个 sql 只能使用到一个表中的一个索引; +- 5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。 + +#### 每个 Innodb 表必须有个主键 + +- Innodb 是一种索引组织表:数据的存储的逻辑顺序和索引的顺序是相同的。 +- Innodb 是按照主键索引的顺序来组织表的,每个表都可以有多个索引,但是表的存储顺序只能有一种。 + +- 注意: + - 不要使用更新频繁的列作为主键,不适用多列主键(相当于联合索引) + - 不要使用 UUID,MD5,HASH,字符串列作为主键(无法保证数据的顺序增长) + - 主键建议使用自增 ID 值 + +#### 为经常需要排序、分组和联合操作的字段建立索引 + +- 经常需要order by、group by、distinct和union等操作的字段,排序操作会浪费很多时间。如果为其建立索引,可以有效的避免排序操作 +- 字段选择: + - 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的字段; + - 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段; +>注意:并不要将符合前面两条的字段每一个都建立索引,通常将这些字段建立联合索引效果更好; +- 多表 join 的关联列; + +#### 经常被用来过滤记录的字段 + +- primary key 字段,系统自动创建主键的索引 +- unique key 字段,系统自动创建对应的索引 +- foreign key 约束所定义的作为外键的字段 +- 在查询中用来连接表的字段 + +#### 尽量使用数据量少的索引 + +- 如果索引的值很长,那么查询的速度会受到影响 +- 例如:对一个char(100)类型的字段进行全文检索,需要的时间肯定比对char(10)类型的字段需要的时间更多 + +#### 区分度最高的列放在联合索引的最左侧, + +- 区分度=列中不同值的数量/列的总行数; + +#### 字段长度小的列放在联合索引的最左侧 + +- 因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好 + +#### 使用最频繁的列放到联合索引的左侧 + +- 这样可以比较少的建立一些索引 + +#### 避免建立冗余索引和重复索引 + +- 冗余索引和重复索引,增加了查询优化器生成执行计划的时间 +- 重复索引示例: + - primary key(id)、index(id)、unique index(id) +- 冗余索引示例: + - index(a,b,c)、index(a,b)、index(a) + +#### 对于频繁的查询,优先考虑使用覆盖索引 + +- 覆盖索引:就是包含了所有查询字段 (where,select,ordery by,group by 包含的字段) 的索引 + +- 覆盖索引的好处: + - 避免 Innodb 表进行索引的二次查询: + - Innodb 是以聚集索引的顺序来存储的,对于 Innodb 来说,二级索引在叶子节点中所保存的是行的主键信息; + - 如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询、才能获取我们真实所需要的数据。 + - 而在覆盖索引中,二级索引的键值中可以获取所有的数据,**避免了对主键的二次查询**,减少了 IO 操作,提升了查询效率。 + - 可以把随机 IO 变成顺序 IO 加快查询效率: + - 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多; + - 因此,利用覆盖索引访问时、也可以 **把磁盘的随机读取的 IO 转变成索引查找的顺序 IO**。 + +#### text 和 blog 尽量使用前缀索引 + +- 如果索引字段的值很长,最好使用值的前缀来索引 +- 例如:text和blog类型的字段,进行全文检索会浪费时间。如果只检索字段的前面的若干个字符,这样可以提高检索速度 + +#### 删除不再使用或者很少使用的索引 + +- 表中数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。 +- 库管理理员应当定期找出这些索引,将他们删除,从而减少索引对更新操作的影响。 + +#### 有些情况不应该创建索引 + +- 对于那些在查询中很少使用或者参考的列不应该创建索引。 +- 对于那些取值很少的列,如性别等等。 +- 对于那些定义为text、image和bit数据类型的列不应该增加索引。 + +#### 使用排序字段做索引,可以减少排序步骤,提升查询效率 + +- 索引覆盖排序字段,这样可以减少排序步骤,提升查询效率 + +#### 索引 SET 规范 + +- 尽量避免使用外键约束 + - 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引 + - 外键可用于保证数据的参照完整性,但建议在业务端实现 + - 外键会影响父表和子表的写操作,从而降低性能 + + +# 键设计注意问题 + +#### primary key 一般使用自增代理键 + +- primary key 应该是有序并且无意义的,由开发人员自定义,尽可能简短,并且是自增序列。 + +#### primary key 字段不允许更新 + +- primary key 一旦写入,就不能再更新; + +#### unique key 一般以 `uk_` 作为前缀 + +- 表中除 primary key 以外,还存在唯一性约束的,可以在数据库中创建以“uk_”作为前缀的唯一约束索引。 + +#### 主键应该尽可能的小 + +- 以降低索引的空间占用。 + +#### 主键应该顺序增长(自增主键) + +- 相比于 UUID 的无序性,顺序主键不会对 innodb 造成过大的 IO 压力。 + - 顺序插入,不存在随机插入会导致的分页问题,性能很低。 + - 顺序读取,性能 + +#### AUTO_INCREMENT 约束的字段必须具备 NOT NULL 属性。 + +#### AUTO_INCREMENT 约束的字段只能是整数类型(TINYINT、SMALLINT、INT、BIGINT 等)。 + +#### 提起预估 AUTO_INCREMENT 约束字段的最大值 + +- 最大值受该字段的数据类型约束,如果达到上限,AUTO_INCREMENT 就会失效。 + +#### 不要使用外键约束,外键约束由程序控制。 + +- 在数据表中添加外键约束,会影响性能; +- 外键约束由程序控制,可以降低耦合度,提升灵活性和可扩展性; + +#### 状态类型如 性别 用 tinyint + +- 尽可能选择小的数据类型,这样会有很多好处,比如服务端处理效率,传输等都会快些。 + +#### 键的字段值应该避免 NULL + +- Null 字段存在巨大隐患; + +#### 表更新的复杂性 + - 多系统合并、需要多个自然键组合做主键时,尽量不要使用复合键; + - 可以使用 Checksum key,即对多系统的自然键进行 MD5 计算,用 MD5 结果值作为单键主键; + +#### SQL 语句复杂性 + - 当 SQL 需要多表关联的时候,SQL 复杂性会迅速增加。 + +#### 空间占用 + - 自然键: + - 维度表,需要空间比较少; + - 事实表,通常需要更多空间; + - 整体来说,需要更多空间; + - 代理键: + - 维度表,需要更多空间; + - 事实表,需要更少空间; + - 整体来说,需要更少空间; + +#### SQL 语句资源消耗 + - 代理键为整型,查询性能较好; + - 自然键如是 VARCHAR,性能比较差; + - 单键的性能较好; + - 复合键的性能差; + - 当数据量达到百万级的时候,性能差异会凸显; + +#### 数据加载速度 + - 自然键: + - 维度表,直接从源系统过数据,额外开销比较少; + - 代理键: + - 维度表,代理键插入必须计算最新插入的代理键,需要时间,但和自然键差别不大; + +# SQL 开发规范 + +#### 建议使用预编译语句进行数据库操作 + +- 预编译语句,可以重复使用这些计划,减少 SQL 编译所需要的时间,还可以解决动态 SQL 所带来的 SQL 注入的问题。 +- 只传参数,比传递 SQL 语句更高效。 +- 相同语句可以一次解析,多次使用,提高处理效率。 + +#### 避免数据类型的隐式转换 + +- 隐式转换会导致索引失效, +- 例如: + ``` + select name,phone from customer where id = '111'; + ``` + +#### 禁止对索引字段使用函数、运算符操作,会使索引失效 + +- 避免索引失效的原则,实际上就是需要保证索引所对应字段的”干净度“。 + +#### 充分利用表上已经存在的索引 + +- 避免使用“双%号”的查询条件。 + - 如: `a like '%123%'` + - 如果无前置%,只有后置%,是可以用到列上的索引的; + +- 一个 SQL 只能利用到复合索引中的一列进行范围查询。 + - 如:有 a,b,c 列的联合索引,在查询条件中有 a 列的范围查询,则在 b,c 列上的索引将不会被用到。 + +- 在定义联合索引时,如果 a 列要用到范围查找的话,就要把 a 列放到联合索引的右侧; + +- 使用 left join 或 not exists 来优化 not in 操作,因为 not in 也通常会使用索引失效。 + + +#### 程序连接不同的数据库使用不同的账号,禁止跨库查询 + +- 为数据库迁移和分库分表留出余地 +- 降低业务耦合度 +- 避免权限过大而产生的安全风险 + +#### 不要使用 SELECT * 必须使用 SELECT <字段列表> 查询 + +- 原因: + - 消耗更多的 CPU 和 IO 以网络带宽资源 + - 无法使用覆盖索引 + - 可减少表结构变更带来的影响 + +#### 避免使用子查询,可以把子查询优化为 join 操作 + +- 子查询性能差的原因: + - 这是因为 **子查询的结果集无法使用索引**; + - 通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响; + - 特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。 + - 由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。 + +- 通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。 + +#### 避免使用 JOIN 关联太多的表 + +- 在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。 + - MySQL 是存在关联缓存的,缓存的大小可以由 join_buffer_size 参数进行设置。 + - 如果程序中大量的使用了多表关联的操作,同时 join_buffer_size 设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。 +- 同时对于关联操作来说,会产生临时表操作,影响查询效率,MySQL 最多允许关联 61 个表,建议不超过 5 个。 + +#### 减少同数据库的交互次数 + +- 数据库更适合处理批量操作,合并多个相同的操作到一起,可以提高处理效率。 + +#### 对应同一列进行 or 判断时,使用 in 代替 or + +- in 的值不要超过 500 个; +- in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。 + +#### 禁止使用 order by rand() 进行随机排序 + +- order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值; + - 如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。 +- 推荐在程序中获取一个随机值,然后从数据库中获取数据的方式。 + +#### 模糊查询 '%value%' 会使索引无效,但是 'value%' 可以有效利用索引 + +- 因为无法判断扫描的区间,SQL 变为全表扫描,但是:**'value%'是可以有效利用索引**。 + +#### WHERE 从句中禁止对列进行函数转换和计算 + +- 对列进行函数转换或计算时,会导致无法使用索引 +- 不推荐: + ``` + where date(create_time)='20190101' + ``` +- 推荐: + ``` + where create_time >= '20190101' and create_time < '20190102' + ``` + +#### 在明显不会有重复值时使用 UNION ALL 而不是 UNION + +- UNION 会把两个结果集的所有数据,放到临时表中后再进行去重操作; +- UNION ALL 不会再对结果集进行去重操作。 + +#### 分页查询语句全部都需要带有排序条件,否则很容易引起乱序 + +#### 使用逻辑删除而不是物理删除 + +- 逻辑删除数据可用数据分析等,物理删除一旦删除,即不可恢复。 + +#### 添加审计字段 add_time、last_update_time + +- 每个表中都应该有 `add_time`、`last_update_time`,在查询、以及问题查找定位时有诸多好处。 + + ```aidl + `add_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间', + + `last_update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', + ``` + +#### 拆分复杂的大 SQL 为多个小 SQL + +- 大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL +- MySQL 中,一个 SQL 只能使用一个 CPU 进行计算 +- SQL 拆分后可以通过并行执行来提高处理效率 + +#### 避免使用子查询,可以把子查询优化为join操作 + +- 通常子查询在in子句中,且子查询中为简单SQL(不包含union、group by、order by、limit从句)时,才可以把子查询转化为关联查询进行优化。 + +#### 子查询性能差的原因 + +- 子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能 会受到一定的影响; + +- 对于返回结果集比较大的子查询,其对查询性能的影响也就越大; + +- 由于子查询会产生大量的临时表也没有索引,所以会消耗过多的CPU和IO资源,产生大量的慢查询。 + +# 操作行为规范 + +#### 超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作 + +- 大批量操作可能会造成严重的主从延迟 + - 主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间; + - 而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况 + +- binlog 日志为 row 格式时会产生大量的日志 + - 大批量写操作会产生大量日志; + - 特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因。 + +- 避免产生大事务操作 + - 大批量修改数据,一定是在一个事务中进行的; + - 这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对 MySQL 的性能产生非常大的影响。 + - 特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批 + +#### 大表修改表结构一定要慎重,推荐使用 pt-online-schema-change + +- 对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。 + +- pt-online-schema-change 工作原理: + - pt-online-schema-change,首先会建立一个与原表结构相同的新表,并且在新表上进行表结构的修改; + - 然后再把原表中的数据复制到新表中,并在原表中增加一些触发器,把原表中新增的数据也复制到新表中; + - 在行所有数据复制完成之后,会把新表命名成原表,并把原来的表删除掉; + - 这样做的核心原理,就是把原来一个 DDL 操作,分解成多个小的批次进行。 + +- pt-online-schema-change 的有点: + - 避免大表修改产生的主从延迟 + - 避免在对表字段进行修改时进行锁表 + +#### 禁止为程序使用的账号赋予 super 权限 + +- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接,这个权限是用来做 DBA 问题处理的 +- super 权限只能留给 DBA 处理问题的账号使用 + +#### 对于程序连接数据库账号,遵循权限最小原则 + +- 程序使用数据库账号只能在一个 DB 下使用,不准跨库 +- 程序使用的账号原则上不准有 drop 权限 + +#### 禁止从开发环境,测试环境直接连接生产环境数据库 + +- 防止开发测试误操作,导致生产事故; +- 防止开发测试,影响数据库性能表现; + +#### 大批量写操作(UPDATE、DELETE、INSERT),需要分批多次进行操作 + +- 大批量操作可能会造成严重的主从延迟,特别是主从模式下,大批量操作可能会造成严重的主从延迟,因为需要slave从master的binlog中读取日志来进行数据同步。 +- binlog日志为row格式时会产生大量的日志 \ No newline at end of file diff --git "a/_posts/2022-02-01-MySQL\357\274\232Explain \350\257\255\345\217\245\350\277\224\345\233\236\347\273\223\346\236\234\351\207\212\344\271\211.md" "b/_posts/2022-02-01-MySQL\357\274\232Explain \350\257\255\345\217\245\350\277\224\345\233\236\347\273\223\346\236\234\351\207\212\344\271\211.md" new file mode 100644 index 00000000000..1154be69c95 --- /dev/null +++ "b/_posts/2022-02-01-MySQL\357\274\232Explain \350\257\255\345\217\245\350\277\224\345\233\236\347\273\223\346\236\234\351\207\212\344\271\211.md" @@ -0,0 +1,120 @@ +--- +layout: post +title: MySQL:Explain 语句返回结果释义 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - MySQL +--- + + +# explain 关键字分析 + +- 通过 explain 我们可以获得以下信息: + - 表的读取顺序 + - 数据读取操作的操作类型 + - 哪些索引可以使用 + - 哪些索引被实际使用 + - 表之间的引用 + - 每张表有多少行被优化器查询 + +![]({{site.baseurl}}/img-post/sql-20.png) + +#### id + +- id 可以认为是查询序列号: + - 每一个 id 代表一个 select,一句 sql 有两个 select,就会有两列、两个id; + - 不同的 id 代表不同子查询,id 越大优先级越高,越先被解析,如果 id 相同、则按照从上到下的顺序查找。 + +#### select_type + +- select_type:表示查询的类型 + - SIMPLE(简单SELECT,不使用UNION或子查询等); + - PRIMARY(子查询中最外层查询,查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY); + - UNION(UNION中的第二个或后面的SELECT语句); + - DEPENDENT UNION(UNION中的第二个或后面的SELECT语句,取决于外面的查询); + - UNION RESULT(UNION的结果,union语句中第二个select开始后面所有select); + - SUBQUERY(子查询中的第一个SELECT,结果不依赖于外部查询); + - DEPENDENT SUBQUERY(子查询中的第一个SELECT,依赖于外部查询); + - DERIVED(派生表的SELECT, FROM子句的子查询); + - UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行); + +#### table + +- table:输出结果集的表 + - 显示这一行的数据是关于哪张表的,有时不是真实的表名字,可能是简称; + +#### type + +- type:表示表的连接类型 + - 对表访问方式,表示 MySQL 在表中找到所需行的方式,又称“访问类型”; + - 常用的类型有: ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好); + + - ALL: + - Full Table Scan, MySQL将遍历全表以找到匹配的行 + - index: + - Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树; + - range: + - 只检索给定范围的行,使用一个索引来选择行; + - ref: + - 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值; + - eq_ref: + - 类似 ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件 + - const、system: + - 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用这些类型访问; + - 如将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量,system 是 const 类型的特例,当查询的表只有一行的情况下,使用system; + - NULL: + - MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。 + +#### possible_keys + +- possible_keys:表示查询时,可能使用的索引 + - 指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用(该查询可以利用的索引,如果没有任何索引显示 null) + +- 该列完全独立于 EXPLAIN 输出所示的表的次序。这意味着在 possible_keys 中的某些键实际上不能按生成的表次序使用。 +- 如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查 WHERE 子句看是否它引用某些列或适合索引的列来提高查询性能。 + +#### key + +- key:表示实际使用的索引 + - key 列显示 MySQL 实际决定使用的键(索引),必然包含在 possible_keys 中; + - 如果没有选择索引,键是 NULL。要想强制 MySQL 使用或忽视 possible_keys 列中的索引,在查询中使用FORCE INDEX、USE INDEX 或者 IGNORE INDEX。 + +#### key_len + +- key_len:索引字段的长度 + - 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的) + - 不损失精确性的情况下,长度越短越好 + +#### ref + +- ref:列与索引的比较 + - 列与索引的比较,表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 + +#### rows + +- rows:扫描出的行数(估算的行数) + - 估算出结果集行数,表示 MySQL 根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数 + +#### Extra + +- Extra:执行情况的描述和说明 + - 该列包含 MySQL 解决查询的详细信息; + - Using where: + - 不用读取表中所有信息,仅通过索引就可以获取所需数据,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示 mysql 服务器将在存储引擎检索行后再进行过滤 + - Using temporary: + - 表示MySQL 需要使用临时表来存储结果集,常见于排序和分组查询,常见 group by、order by; + - Using filesort:当Query中包含 order by 操作,而且无法利用索引完成的排序操作称为“文件排序” + - Using join buffer: + - 改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。 + - Impossible where: + - 这个值强调了where语句会导致没有符合条件的行(通过收集统计信息不可能存在结果)。 + - Select tables optimized away: + - 这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 + - No tables used: + - Query语句中使用from dual 或不含任何from子句 + + diff --git "a/_posts/2022-02-01-MySQL\357\274\232MySQL \346\200\247\350\203\275\344\274\230\345\214\226\346\200\235\350\267\257.md" "b/_posts/2022-02-01-MySQL\357\274\232MySQL \346\200\247\350\203\275\344\274\230\345\214\226\346\200\235\350\267\257.md" new file mode 100644 index 00000000000..aed7712fe65 --- /dev/null +++ "b/_posts/2022-02-01-MySQL\357\274\232MySQL \346\200\247\350\203\275\344\274\230\345\214\226\346\200\235\350\267\257.md" @@ -0,0 +1,443 @@ +--- +layout: post +title: MySQL:MySQL 性能优化思路 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 数据库 +--- + + +# 影响 MySQL 性能的几个方面 + +- 服务器硬件 & 服务器系统(硬件 & 系统参数优化) + - 一般是运维来负责 + +- 存储引擎 + - MyISAM: 不支持事务,表级锁。 + - InnoDB: 支持事务,支持行级锁,事务 ACID。 + +- 数据库参数配置 + +- 数据库 结构设计 & SQL 语句优化(重点优化方向) + + +# 读写分离 & 一主多从 + +#### 工作原理 + +* 在实际的应用中,绝大部分情况都是读远大于写; +* Mysql提供了读写分离的机制,所有的写操作都必须对应到Master,读操作可以在 Master和Slave机器上进行; +* Slave与Master的结构完全一样,一个Master可以有多个Slave,甚至Slave下还可以挂 Slave,通过此方式可以有效的提高DB集群的 QPS; +* 所有的写操作都是先在Master上操作,然后同步更新到Slave上; +* 所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。 + +#### 应用实践 + +* 当读压力很大的时候,可以考虑添加 Slave 机器的分式解决, + * 但是当 Slave 机器达到一定的数量就得考虑分库了。 +* 当写压力很大的时候,就必须得进行分库操作。 + + +# MySQL引擎选择 + +#### InnoDB + +- 特点 + - 支持事务,行级锁,外键,崩溃修复,多版本并发控制; + - 读写效率相对较差,内存使用相对较高,占用数据空间相对较大。 + +- 应用场景 + - 依赖于 事务,回滚,并发,大数据量,外键,行级锁 的场景。 + +#### MyISAM + +- 特点 + + * 不支持事务,不支持外键,仅支持非聚集索引; + + * 支持全文索引,仅支持到表级锁; + + * 支持数据压缩,占用空间相对小,内存使用相对较低; + + * 读写性能相对极佳。 + +- 应用场景 + + * 过多的大数据量的频繁的查询优势。 + +#### Memory + +- 特点 + + * 依赖于内存空间,数据处理速度快,仅支持到表级锁。 + +- 应用场景 + + * 临时性的,大数据量表的查询优势。 + +# MySQL分库分表 + +#### 分库分表的基本原理 + +- 为什么要分库分表 + - 用户请求量太,但是单服务器 TPS,内存,IO 都是有限的; + - 单库太大,单库所在服务器上磁盘空间不足,单库处理能力有限,存在 IO 瓶颈; + - 单表太大,索引膨胀,查询超时; +- 分库分表的目标 + - 分表,可以解决单表海量数据的读写性能问题; + - 分库,可以解决单台数据库的并发访问压力问题。 +- 拆分思路 + - 垂直拆分,垂直分库、垂直分表 + - 水平拆分,水平分库、水平分表 +>垂直拆分:是指按功能模块拆分,**垂直拆分解决表与表之间的 I/O 竞争**。比如分为订单库、商品库、用户库...这种方式多个数据库之间的表结构不同。 +水平拆分:将同一个表的数据进行分块保存到不同的数据库中,**水平拆分解决单表中数据量增长出现的压力**。这些数据库中的表结构完全相同。 +- 分库分表的顺序 + **分库分表的顺序应该是先垂直分,后水平分**。因为垂直分更简单,更符合我们处理现实世界问题的方式。 + +#### 垂直分表 +垂直分表,是基于列(字段)对表格进行横向拆分。 +- 垂直分表规则: + - 数据较大的字段,单独存表; + - 不常用的的字段,单独存表; + - 经常一起使用的字段,放在一起存表; + - 长度较长(比如text类型字段)的字段,单独存表; +>垂直分表,一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。 + +分表能够解决单表数据量过大带来的查询效率下降的问题,但是,却无法给数据库的并发处理能力带来质的提升。面对高并发的读写访问,当数据库master服务器无法承载写操作压力时,不管如何扩展slave服务器,此时都没有意义了。因此,我们必须换一种思路,对数据库进行拆分,从而提高数据库写入能力,这就是所谓的分库。 + +#### 垂直分库 +垂直分库,是针对一个系统中的不同业务进行拆分,比如用户User一个库,商品Producet一个库,订单Order一个库。 +>**垂直切分后,一般放在多个服务器上,而不是一个服务器上**。这是因为,一个购物网站对外提供服务,会有用户,商品,订单等的CRUD。没拆分之前, 全部都是落到单一的库上的,这会让数据库的单库处理能力成为瓶颈。 + +按垂直分库后,如果还是放在一个数据库服务器上, 随着用户量增大,这会让单个数据库的处理能力成为瓶颈,还有单个服务器的磁盘空间,内存,tps等非常吃紧。 所以我们要拆分到多个服务器上,这样上面的问题都解决了,以后也不会面对单机资源问题。 +数据库往往最容易成为应用系统的瓶颈,而数据库本身属于“有状态”的,相对于Web和应用服务器来讲,是比较难实现“横向扩展”的。 数据库的连接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈。数据库业务层面的拆分,能对不同业务的数据分别的进行管理,维护,监控,扩展等。 + +#### 水平分表(不单独使用) +针对数据量巨大的单张表(比如订单表),按照某种规则(RANGE,HASH取模等),切分到多张表里面去。 +- 水平分表的问题: + - 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。 + - 无法进行跨表直连连接查询; + - 统计数据不方便; + - 如果数据持续增长,达到现有分表的瓶颈,需要增加分表时、会出现数据重新排列的情况。 +> 生产环境中,水平分表一般不单独使用,而是和水平分库一起使用,做水平分库分表。 + +#### 水平分库(一般与水平分表同时使用) +将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。 +- 水平分库分表策略: + - 用户 id 取模; + - 如果不是整数,可以进行 hash 获取到整数; + - 按照地理区域拆分; + - 按照时间拆分; + - 按照【冷热数据】拆分; + +#### 分库分表的标准 +- 什么样的情况需要分库分表: + - 表体积大于 2G,行数大于 1000 万; + - 表中含有 text、blob、varchar(1000); + - 数据有时效性,可以单独拿出来归档; +- 分库分表的大小多少合适 + 分表最大分1024,一般分100左右比较适合。 + +#### 分库分表存在的问题 +- 维度的问题 + 假如用户购买了商品,需要将交易记录保存取来,如果按照用户的纬度分表,则每个用户的交易记录都保存在同一表中,所以很快很方便的查找到某用户的 购买情况,但是某商品被购买的情况则很有可能分布在多张表中,查找起来比较麻烦。反之,按照商品维度分表,可以很方便的查找到此商品的购买情况,但要查找 到买人的交易记录比较麻烦。 +>维度的问题解决办法:记录【两份数据】,一份按照用户纬度分表,一份按照商品维度分表。 +- 联合查询的问题 + 联合查询基本不可能,因为关联的表有可能不在同一数据库中。这是因为分库分表后表之间的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表, 结果原本一次查询能够完成的业务,可能需要多次查询才能完成。 + 联合查询问题的解决方法: + - 全局表:基础数据,所有库都拷贝一份; + - 字段冗余:这样有些字段就不用join去查询了; + - 系统层组装:分别查询出所有,然后组装起来,较复杂。 +- 跨库事务问题 + **避免在一个事务中修改db0中的表的时候、同时修改db1中的表**,一个是操作起来更复杂,效率也会有一定影响。 + 分库分表后,就成了分布式事务了。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价; 如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。 +- 数据依赖问题 + 例如卖家a的商品和交易信息都放到db0中,当db1挂了的时候,卖家a相关的东西可以正常使用。**尽量把同一组数据放到同一DB服务器上**,避免数据库中的数据依赖另一数据库中的数据。 + +#### 分库分表产品方案 +- MySQL Proxy; +- Amoeba; +- Hibernate Shards; +- sharding-jdbc; + + +# MySQL分区分片 + +#### 分区原理 +- 表分区,就是将数据量比较大的表,在物理上分成若干个小表,从逻辑来看还是一个大表。MySQL 5 之后才有了数据表分区功能。 +- 数据库的应用分为两类:一类是OLTP(在线事务处理),如Blog、电子商务、网络游戏等;另一类是OLAP(在线分析处理),如数据仓库、数据集市。 +- 对于OLAP的应用,分区的确是可以很好地提高查询的性能。 + 因为OLAP应用大多数查询需要频繁地扫描一张很大的表。假设有一张1亿行的表,其中有一个时间戳属性列。用户的查询需要从这张表中获取一年的数据。如果按时间戳进行分区,则只需要扫描相应的分区即可。 +- 对于OLTP的应用,分区应该非常小心。 + 在这种应用下,通常不可能会获取一张大表10%的数据,大部分都是通过索引返回几条记录即可。而根据B+树索引的原理可知,对于一张大表,一般的B+树需要2~3次的磁盘IO。因此B+树可以很好地完成操作,不需要分区的帮助,并且设计不好的分区会带来严重的性能问题。 + >MySQL的分区字段,必须包含在主键字段内。 + +#### 分区策略 +- RANGE分区 + - 最常用的一种分区类型,基于属于一个给定连续区间的列值,把多行分配给分区。这些区间要连续且不能相互重叠,使用VALUES LESS THAN操作符来进行定义。 +- LIST分区 + - LIST分区和RANGE分区类似,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择,而非连续的。 + - LIST分区通过使用“PARTITION BY LIST(expr)”来实现,其中“expr” 是某列值或一个基于某个列值、并返回一个整数值的表达式,然后通过“VALUES IN (value_list)”的方式来定义每个分区,其中“value_list”是一个通过逗号分隔的整数列表。 + - 不同于RANGE分区中定义的VALUES LESS THAN语句,LIST分区使用VALUES IN,因为每个分区的值是离散的,因此只能定义值。 +- HASH分区 + - HASH分区的目的是将数据均匀地分布到预先定义的各个分区中,保证各分区的数据量大致都是一样的。在RANGE和LIST分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区中;而在HASH分区中,MySQL自动完成这些工作,用户所要做的只是基于将要进行哈希分区的列值指定一个列值或表达式,以及指定被分区的表将要被分隔成的分区数量。 + - 要使用HASH分区来分割一个表,要在CREATE TABLE 语句上添加一个“PARTITION BY HASH (expr)”子句,其中“expr”是一个返回一个整数的表达式。它可以仅仅是字段类型为MySQL 整型的一列的名字。此外,你很可能需要在后面再添加一个“PARTITIONS num”子句,其中num是一个非负的整数,它表示表将要被分割成分区的数量,如果没有包括一个PARTITIONS子句,那么分区的数量将默认为1。 + +#### 分区使用场景 + +- 一张表的查询速度已经慢到影响使用的时候; +- 表中的数据是分段的; +- 对数据的操作往往只涉及一部分数据,而不是所有的数据; +- 数据库比较多、并发不多,可以采用表分区; +- 数据量比较大、并发较高,可以采用分库分表和分区相结合; + +#### 谨慎使用 MySQL 分区表 + +- 分区表在物理上表现为多个文件,在逻辑上表现为一个表; +- 谨慎选择分区键,跨分区查询效率可能更低; +- 建议采用物理分表的方式管理大数据。 + +#### 分表和分区的区别 +- 实现方式 + - mysql的分表是真正的分表,一张表分成很多表后,每一个小表都是完正的一张表,都对应三个文件(MyISAM引擎:一个.MYD数据文件,.MYI索引文件,.frm表结构文件)。 +- 数据处理 + - 分表后数据都是存放在分表里,总表只是一个外壳,存取数据发生在一个一个的分表里面。分区则不存在分表的概念,分区只不过把存放数据的文件分成了许多小块,分区后的表还是一张表,数据处理还是由自己来完成。 +- 提高性能 + - 分表后,单表的并发能力提高了,磁盘I/O性能也提高了。分区突破了磁盘I/O瓶颈,想提高磁盘的读写能力,来增加mysql性能。 在这一点上,分区和分表的测重点不同,分表重点是存取数据时,如何提高mysql并发能力上;而分区呢,如何突破磁盘的读写能力,从而达到提高mysql性能的目的。 +- 实现的难易度 + - 分表的方法有很多,用merge来分表,是最简单的一种方式。这种方式和分区难易度差不多,并且对程序代码来说可以做到透明的。如果是用其他分表方式就比分区麻烦了。 分区实现是比较简单的,建立分区表,跟建平常的表没什么区别,并且对代码端来说是透明的。 + +#### 分片策略 +- 分片,是把数据库横向扩展(Scale Out)到多个物理节点上的一种有效的方式。 + - 分片的主要目的,是为突破单节点数据库服务器的 I/O 能力限制,解决数据库扩展性问题。Shard这个词的意思是“碎片”。如果将一个数据库当作一块大玻璃,将这块玻璃打碎,那么每一小块都称为数据库的碎片(DatabaseShard)。将整个数据库打碎的过程就叫做分片,可以翻译为分片。 + +- 形式上,分片可以简单定义为将大数据库分布到多个物理节点上的一个分区方案。 + - 每一个分区包含数据库的某一部分,称为一个片,分区方式可以是任意的,并不局限于传统的水平分区和垂直分区。 + - 一个分片可以包含多个表的内容甚至可以包含多个数据库实例中的内容。每个分片被放置在一个数据库服务器上。一个数据库服务器可以处理一个或多个分片的数据。系统中需要有服务器进行查询路由转发,负责将查询转发到包含该查询所访问数据的分片或分片集合节点上去执行。 + +#### 分片和分区的区别 +- 分片和分区,有很多的相似之处。有的时候,分片(Sharding) 也被近似等同于水平分区(Horizontal Partitioning),网上很多地方也用水平分区来指代 分片(Sharding)。 + + ![分片和分区的区别]({{site.baseurl}}/img-post/分片和分区的区别.png) + +# MySQL锁优化 + +#### innodb 锁优化 + +Innodb 存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM 的表级锁定的。 +- 尽可能让所有的数据检索都通过索引来完成,从而避免Innodb 因为无法通过索引键加锁而升级为表级锁定; +- 合理设计索引,让Innodb 在索引键上面加锁的时候尽可能准确,尽可能的缩小锁定范围,避免造成不必要的锁定而影响其他Query 的执行; +- 尽可能减少基于范围的数据检索过滤条件,避免因为间隙锁带来的负面影响而锁定了不该锁定的记录; +- 尽量控制事务的大小,减少锁定的资源量和锁定时间长度; +- 在业务环境允许的情况下,尽量使用较低级别的事务隔离,以减少MySQL 因为实现事务隔离级别所带来的附加成本; + +#### 减少 innodb 死锁产生概率的建议 + +- 类似业务模块中,尽可能按照相同的访问顺序来访问,防止产生死锁; +- 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率; +- 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率; + + +#### MyISAM 锁优化 + +- MyISAM 表锁优化建议优化MyISAM 存储引擎锁定问题的时候,最关键的就是如何让其提高并发度。由于锁定级别是不可能改变的了,所以我们首先需要尽可能让锁定的时间变短,然后就是让可能并发进行的操作尽可能的并发。 + +#### 缩短锁定时间 + +- 尽两减少大的复杂Query,将复杂Query 分拆成几个小的Query 分布进行; +- 尽可能的建立足够高效的索引,让数据检索更迅速; +- 尽量让MyISAM 存储引擎的表只存放必要的信息,控制字段类型; +- 利用合适的机会优化MyISAM 表数据文件; + +#### 分离能并行的操作 + +配置是Concurrent Insert(并发插入),MyISAM 存储引擎有一个控制是否打开Concurrent Insert 功能的参数选项:concurrent_insert,可以设置为0,1 或者2。三个值的具体说明如下: +- `set global concurrent_insert=2(always)`,无论MyISAM 存储引擎的表数据文件的中间部分是否存在因为删除数据而留下的空闲空间,都允许在数据文件尾部进行Concurrent Insert; +- `set global  concurrent_insert=1(auto)`,当MyISAM 存储引擎表数据文件中间不存在空闲空间的时候,可以从文件尾部进行Concurrent Insert; +- `set global  concurrent_insert=0(never)`,无论MyISAM 存储引擎的表数据文件的中间部分是否存在因为删除数据而留下的空闲空间,都不允许Concurrent Insert。 + +#### 合理利用读写优先级 + +- 如果我们的系统是一个以读为主,而且要优先保证查询性能的话,我们可以通过设置系统参数选项 `low_priority_updates=1`,如果我们的系统需要有限保证数据写入的性能的话,则可以不用设置low_priority_updates参数; + + +# MySQL优化数据库连接 + +#### 优化连接池 + +- 连接池运行机制 + - MySQL连接器中的连接池,用以提高数据库密集型应用程序的性能和可扩展性,默认启用。MySQL连接器负责管理连接池中的多个连接,自动创建、打开、关闭和破坏连接,多个连接的创建,可满足多客户端的频繁连接,连接的重复使用获得最佳性能。 + - MySQL连接器 每三分钟运行一次后台作业,并从池中删除闲置(未使用)超过三分钟的连接。池清理释放客户端和服务器端的资源。这是因为在客户端每个连接都使用一个Socket,而在服务器端每个连接都使用一个Socket和一个线程。 + +- max_connections,MySQL最大并发连接数,默认值是151,最大连接数上限是16384; + - **经验:实际连接数是最大连接数的 `85%` 较为合适**。 + +- 设置 max_used_connections 方法: + - 查询数据库目前设置的最大并发连接数是多少 + ``` + SHOW VARIABLES LIKE 'max_connections'; + ``` + - 查询数据库目前实际连接的并发数是多少 + ``` + SHOW STATUS LIKE 'max_used_connections'; + ``` + - 在MySQL配置文件 `/etc/my.cnf` 中设置 `max_connections=3000`,表示修改最大连接数为3000。 + >注意:需要重启 MySQL 才能生效。 + + - MySQL为每个连接创建缓冲区,所以不应该盲目上调最大连接数。 + >如果最大连接数达到了上面设置的 3000,会消耗大约 800M 内存。 +- 其他连接池设置: + ``` + 开启连接池: Pooling=true,默认开启 + + 复用时重置连接状态: ConnectionReset=True + + 保持连接设置: CacheServerProperties=True + + 连接超时回收(秒): ConnectionLifeTime=300 + + 支持的最大连接数量: Max Pool Size=100 + + 保持最小的连接数量: Min Pool Size=10 + ``` +#### 优化请求堆栈 + +- back_log,存放执行请求的堆栈大小,默认值是50。 + - **该值设置为最大并发连接数的 `20%~30%` 较为合适**。 +- 设置 back_log 方法: + - 在MySQL配置文件 `/etc/my.cnf` 中,设置 `back_log=600` + - 修改后需要重启 MySQL 才能生效。 + +#### 修改连接超时时间 + +- wait-timeout,超时时间,单位是秒,连接默认超时为8小时,连接长期不用不销毁,比较浪费资源。 + - **经验:设置超时时间为 `10` 分钟 `wait-timeout=600`。** + +#### 优化内存缓冲池 + +- 缓冲池运行机制 + - 在MySQL5.5之前,广泛使用的和默认的存储引擎是MyISAM。MyISAM使用操作系统缓存来缓存数据。InnoDB需要innodb buffer pool中处理缓存,所以非常需要有足够的InnoDB buffer pool空间。 + - 缓冲区分为 热数据区 / 冷数据区,两者空间占比约为 7/3,每区中的数据集依使用频率按顺序依次排列。 + 当一个新的查询结果出现后,首先考虑存放到冷数据区,当冷数据区的结果集使用达到一定频率,会被改存到热数据区,使用频率最好的数据集会被存放到热区的首位,当然也有热区转到冷区的状况。 +- InnoDB 缓冲池不仅仅是一个缓存,MySQL InnoDB buffer pool 包含四部分: + - 数据缓存,InnoDB 数据页面; + - 索引缓存,索引数据; + - 缓冲数据,脏页(在内存中修改尚未写入到磁盘的数据); + - 内部结构,如自适应哈希索引,行锁等。 +- innodb_buffer_pool_instances,内存缓冲池。 + - buffer_pool 把需要缓冲的数据 hash 到不同的缓冲池中,这样可以并行的内存读写。通过减少争用不同线程对缓存页面进行读写的争用,将缓冲池划分为多个单独的实例可以提高并发性。 + - MySQL 5.7、MySQL 8.0 下 innodb_buffer_pool_instances 默认为 1,若 MySQL 存在高并发和高负载访问,设置为 1 则会造成大量线程对 buffer_pool 的单实例互斥锁竞争,这样会消耗一定量的性能的。 + - **innodb_buffer_pool_instances 建议设置为 `cpu核心数`**。 +- innodb_buffer_pool_chunk_size,缓冲池每块大小,默认128M。 + - pool_chunk_size 一般不做改动,使用默认值就可以。 +- innodb_buffer_pool_size,缓冲池的承载总量。 + - innodb_buffer_pool_size 可以缓存索引和行数据,值越大、IO读写就越少; + - 设置规则:`innodb_buffer_pool_size = (innodb_buffer_pool_chunk_size * {N}块 )* innodb_buffer_pool_instances` + - **如果单纯的做数据库服务,该参数可以设置到电脑物理内存的80%**; + - **为了更好的配合 pool_instance,pool_size 需要设置为 pool_instance 和 pool_chunk_size 的整数倍**,这样可以被 pool_instance 整除,为每个 buffer pool 实例平均分配内存。如果设置的值不是倍数,MySQL会自动将 pool_size 调整为 pool_chunk_size 的倍数。 + +#### 优化并发线程数 +- innodb_thread_concurrency,代表并发线程数。 + - 默认是0,表示没有设置线程数量的上限。 + - 不是分配给 MySQL 的线程越多越好,线程多反而会损耗cpu性能,导致速度变慢。 + - **经验:并发线程数应该设置为 `cpu 核心数的两倍`**。 + - 注意:这个变量特定于Solaris 8和更早的系统,MySQL 5.7.2中删除了这个变量。 +- 设置 innodb_thread_concurrency 方法: + - 在MySQL配置文件 `/etc/my.cnf` 中,设置 `innodb_thread_concurrency=8`。 + - 查看cpu型号 + ``` + cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c + ``` + - 查看cpu核心数 + + ``` + cat /proc/cpuinfo | grep "cores"|uniq + ``` +#### 优化线程池 + +- 为什么要优化线程池 + - 客户端发起连接到 MySQL Server 后,MySQL Server监听进程监听到新的请求,然后 Sever 会为其分配一个新的 thread去处理此请求。 + - 从建立连接之开始,CPU要给它划分一定的 thread stack,然后进行用户身份认证,建立上下文信息,最后请求完成,关闭连接,同时释放资源。 + - 在高并发的情况下,这个过程将给系统带来巨大的压力,不能保证性能。MySQL服务器的线程数需要在一个合理的范围之内,这样才能保证MySQL服务器健康平稳地运行。 + +- 查看线程池的状态: + ``` + mysql> show variables like 'thread%'; + +--------------------+---------------------------+ + | Variable_name | Value | + +--------------------+---------------------------+ + | thread_cache_size | 64 | + | thread_concurrency | 10 | + | thread_handling | one-thread-per-connection | + | thread_stack | 262144 | + +--------------------+---------------------------+ + ``` +- thread_cache_size + - thread_cache_size,Threads_cached 中存放的最大连接线程数; + - 在短连接的应用中,Threads_cached 的功效非常明显,因为在应用中数据库的连接和创建是非常频繁的。如果不使用 Threads_cached,那么消耗的资源是非常可观的。 、 + - 在长连接中虽然带来的改善没有短连接的那么明显,但是好处是显而易见的。但并不是越大越好,大了反而浪费资源,这个的确定一般认为和物理内存有一定关系。 + - Mysql默认值为9。 + +- 设置 thread_cache_size 方法: + + - **参考下面额对照表,根据物理内存设置对应的 thread_cache_size 数值**:、 + ``` + 1G —> 8 + 2G —> 16 + 3G —> 32 + >3G —> 64 + ``` + - 在 mysql 命令行中设置: + ``` + mysql> set global thread_cache_size=64; + ``` + +- thread_concurrency + - **thread_concurrency 应设为 `CPU核数的2倍`**。 + 比如有一个双核的CPU,那么thread_concurrency的应该为4。这个变量是针对Solaris系统的,如果设置这个变量的话,mysqld就会调用thr_setconcurrency()。 + - 这个函数使应用程序给同一时间运行的线程系统提供期望的线程数目,但是在5.7以后就已经抛弃了。 + - 设置 thread_concurrency 方法: + - 在 mysql 命令行中设置: + ``` + mysql> set global thread_concurrency=4; + ``` +- thread_handling + - thread_handling 运用 Thread_Cache 处理连接的方式,从 5.1.19 添加的新特性,有两个值可选 `no-threads`、`one-thread-per-connection`。 + - no-threads :服务器使用一个线程 + - one-thread-per-connection :服务器为每个客户端请求使用一个线程 +- thread_stack + - thread_stack + 每个连接被创建的时候,mysql分配给它的内存。这个值一般认为默认就可以应用于大部分场景了,除非必要非则不要动它。上面表示是256kb。 + +- 查看线程使用情况 + + ``` + mysql> show global status like 'Thread%'; + +-------------------+-------+ + | Variable_name | Value | + +-------------------+-------+ + | Threads_cached | 41 | + | Threads_connected | 53 | + | Threads_created | 541 | + | Threads_running | 4 | + +-------------------+-------+ + ``` +- Threads_cached + - MySQL里面为了提高客户端请求创建连接过程的性能,提供了一个连接池也就是 Thread_cache 池(大小是thread_cache_size),将空闲的连接线程放在连接池中,而不是立即销毁。 + - 这样的好处就是,当又有一个新的请求的时候,mysql不会立即去创建连接 线程,而是先去 Thread_Cache 中去查找空闲的连接线程,如果存在则直接使用,不存在才创建新的连接线程。Thread_cache 值表示已经被线程缓存池缓存的线程个数。 + +- Threads_connected + - 当前处于连接状态的线程个数,等于 show processlist。 + +- Threads_created + - Threads_created 表示创建过的线程数,如果发现 Threads_created 值过大的话,表明MySQL服务器一直在创建线程,这也是比较耗资源,可以适当增加配置文件中 thread_cache_size 值。 + +- Threads_running + - 处于激活状态的线程的个数,这个一般都是远小于Threads_connected的。 + + + diff --git "a/_posts/2022-02-01-MySQL\357\274\232MySQL \346\200\247\350\203\275\351\227\256\351\242\230\346\216\222\346\237\245\346\200\235\350\267\257.md" "b/_posts/2022-02-01-MySQL\357\274\232MySQL \346\200\247\350\203\275\351\227\256\351\242\230\346\216\222\346\237\245\346\200\235\350\267\257.md" new file mode 100644 index 00000000000..35c51b23872 --- /dev/null +++ "b/_posts/2022-02-01-MySQL\357\274\232MySQL \346\200\247\350\203\275\351\227\256\351\242\230\346\216\222\346\237\245\346\200\235\350\267\257.md" @@ -0,0 +1,118 @@ +--- +layout: post +title: MySQL:MySQL 性能问题排查思路 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 数据库 +--- + + +# 数据库性能指标 + +#### QPS + +- QPS,Queries Per Second,意思是“每秒查询率”; +- 是一台服务器每秒可以相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 + +#### TPS + +- TPS,Transactions Per Second,也就是事务数/秒。 +- TPS 是软件测试结果的测量单位; +- client 在发送请求时开始计时,收到 server 响应后结束计时,以此来计算使用的时间和完成的事务个数。 + +# 数据库性能差的表现 + +#### 效率低下的 SQL + +- 超高的 QPS 与 TPS + +#### 大量的并发 + +- 数据链接数被占满(max_connection 默认100,通常把链接数设置得大一些)。 + - 并发量,表示同一时刻数据库服务器处理的请求数量数据库。 + +#### 超高的CPU使用率 + +- CPU资源耗尽出现宕机。 + +#### 磁盘IO性能忽然降低 + +- 大量消耗磁盘性能的计划任务。 + - 解决:更快磁盘设备、调整计划任务、作好磁盘维护。 + +#### 避免连接不了数据库问题 + +- 减小从服务器的数量(从服务器会从主服务器复制日志)服务器 + +- 进行分级缓存(避免前端大量缓存失效)网络 + +- 避免使用 select * 进行查询session + +- 分离业务网络和服务器网络多 + +# 影响 MySQL 性能的几个方面 + +- 服务器硬件。 + +- 服务器系统(系统参数优化)。 + +- 存储引擎。 + - MyISAM: 不支持事务,表级锁。 + - InnoDB: 支持事务,支持行级锁,事务 ACID。 + +- 数据库参数配置。 + +- 数据库结构设计和SQL语句。(重点优化) + +# 大表问题 + +#### 大表带来的问题 + +- 大表: + - 记录行数巨大,单表超 1000万; + - 表数据文件巨大,超过 10G; + +- 大表的危害 + + - 慢查询: + - 很难在短期内过滤出须要的数据; + - 查询字区分度低: + - 在大数据量的表中筛选一部分数据,会产生大量的磁盘 I/O,导致下降磁盘效率; + +- 大表对 DDL 的影响 + - 创建索引须要很长时间: + - MySQL -v<5.5 创建索引会锁表 + - MySQL -v>=5.5 创建索引会形成主从延迟(mysql创建索引,先在组上执行,再在库上执行) + - 修改表结构须要长时间的锁表: + - 会形成长时间的主从延迟('480秒延迟') + + +#### 如何处理数据库上的大表 + +- 分库分表把一张大表分红多个小表 +- 难点: + - 分表主键的选择 + - 分表后跨分区数据的查询和统计 + +# 大事务问题 + +- 大事务的定义:运行时间长,操做数据比较多的事务; + +#### 大事务的危害 + +- 锁定数据太多,回滚时间长,执行时间长。 +- 锁定太多数据,形成大量阻塞和锁超时; +- 回滚时所需时间比较长,且数据仍然会处于锁定; +- 若是执行时间长,将形成主从延迟,由于只有当主服务器所有执行完写入日志时,从服务器才会开始进行同步,形成延迟。 + +#### 大事务问题解决思路 + +- 避免一次处理太多数据,能够分批次处理; +- 移出没必要要的 SELECT 操做,保证事务中只有必要的写操做。 + + + diff --git "a/_posts/2022-02-01-MySQL\357\274\232SQL \346\237\245\350\257\242\350\257\255\345\217\245\346\211\247\350\241\214\350\277\207\347\250\213.md" "b/_posts/2022-02-01-MySQL\357\274\232SQL \346\237\245\350\257\242\350\257\255\345\217\245\346\211\247\350\241\214\350\277\207\347\250\213.md" new file mode 100644 index 00000000000..dc3ac668bf6 --- /dev/null +++ "b/_posts/2022-02-01-MySQL\357\274\232SQL \346\237\245\350\257\242\350\257\255\345\217\245\346\211\247\350\241\214\350\277\207\347\250\213.md" @@ -0,0 +1,150 @@ +--- +layout: post +title: MySQL:SQL 查询语句执行过程 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 数据库 +--- + + +# 执行过程流程图 + +![]({{site.baseurl}}/img-post/mysql-5.png) + +# 连接器:客户端访问服务器 + +- 客户端需要通过连接器访问 MySQL Server,连接器主要负责身份认证和权限鉴别的工作。也就是负责用户登录数据库的相关认证操作。 +- 例如:校验账户密码,权限等。在用户名密码合法的前提下,会在权限表中查询用户对应的权限,并且将该权限分配给用户。 +- 在连接完成以后可以查看连接状态,通过命令行“show processlist”生成下图的查询结果。其中“Command”列返回的内容中,“Sleep”表示MySQL相同中对应一个空闲连接。而“Query”表示正在查询的连接。 + +- MySQL将连接器中的连接分为长连接和短连接: + - 长连接是指连接成功后,客户端请求一直使用是同一个连接。 + - 短连接是指每次执行完SQL请求的操作之后会断开连接,如果再有SQL请求会重新建立连接。由于短连接会反复创建连接消耗相同资源,因此多数情况下会选择长连接。但是为了保持长连接,会占用系统内存,而这些被占用的内存知道连接断开以后才会释放。这里提出了两个解决方案: + 定期断开长连接,每隔一段时间或者执行一个占用内存的大查询以后断开连接,从而释放内存,当查询的时候再重新创建连接。 +- MySQL 5.7 或者更高的版本,通过执行 mysql_reset_connection 来重新初始化连接。此过程不会重新建立连接,但是会释放占用的内存,将连接恢复到刚刚创立连接的状态。 + +# 提交 SQL 语句 + +- 客户端先发送查询语句给服务器 + +# 检查缓存 + +- 服务器检查缓存,如果存在则返回。 +- 缓存的查询在sql解析之前进行。 +- 缓存的查找通过一个 对大小写敏感的哈希表实现,即直接比对sql字符串。因此只要有一个字节不同,都不会匹配中。 + +# 分析器:解析 SQL + +- Mysql 没有命中查询缓存,那么就会进入分析器; +- 分析器主要是用来分析 SQL 语句是来干嘛的,这里就是把sql做解析,变成一个解析树; +- 解析时会做mysql语法规则验证。 + +- 分析器主要分为以下两步: + + - 词法分析: + - 一条SQL语句有多个字符串组成,首先要提取关键字; + - 比如:select,提出查询的表,提出字段名,提出查询条件等等。 + + - 语法分析: + - 根据词法分析的结果,语法分析主要就是判断你输入的SQL语句是否正确,检查关键字错误、关键字顺序、引号匹配是否符合 MYSQL 语法; + - 如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒。 + + - 注意: + - 词法分析程序将整个查询语句分解成各类标志,语法分析根据定义的系统语言将“各类标志”转为对MySQL有意义的组合。 + - 最后系统生成一个语法树(AST),语法树便是优化器依赖的数据结构。 + +- 预处理 + - 和元数据关联校验,检查数据表和列是否存在,解析名字和别名。 + +# 优化器:生成真正的执行计划 + +- mysql 会生成多种计划: + - 系统会分别计算一个预测成本值,然后选一个成本最小的计划。 + - 计算信息来自于表的页面个数、索引分布、长度、个数、数据行长度。 + - 因为多种原因,可能不会选择到最优的计划。 + - 优化方法包括静态优化和动态优化。 + +- 静态优化和动态优化的区别: + - 静态优化类似“编译期优化”,只和语句结构有关,和具体值无关 + - 动态优化是在运行中去优化的,需要依赖索引行数、where取值,执行次数可能比静态优化要多。 + +#### mysql 的优化类型 + +- join 顺序 + - 关联表(join)的顺序可能会变 +- join 连接 + - outer join 可能会变成内连接 +- 优化条件表达式 + - 例如:5=5 AND a>5 被简化成 a>5 +- 优化 MAX\MIN + - 如果是MAX(索引),那么直接拿 B+ 树的第一条或者最后一条即可 +- 计算常数 + - 当发现某个查询或者表达式的结果、是可以提前计算出来的时候,就会优化成常数 +- 索引覆盖 + - 如果只要返回索引列,就不会走到最底层去。 +- 子查询优化 +- 提前终止查询 + - 例如:LIMIT +- 等值传播: + - join 中可能把左表的 where 拿给右表一起用 +- 排序: + - 例如:where xx in(1,2,3,4,5,6)这个条件,并不是简单遍历判断,会先排序、然后用二分去判断是否存在。 + + +#### 数据和索引的统计信息 + +- 统计信息是存储引擎去计算的,不同的存储引擎有不同的统计信息 +- 服务器层生成查询计划时,会向存储引擎获取这些信息。 + +#### MYSQL对关联查询的执行 + +- join 查询的本质其实是读取临时表做关联 + - 例如: + - `a inner join b on a.id=b.id where a.xx=y` + - 系统遍历 a 的每一行(此时 a 表本质上是 select * from a where a.xx=y) + - 在哪一行中 a 的 id 被定下来, 那么就会去获取一个临时表,临时表为(select * from b where a.id = id) + - 接着用这个临时表和a那一行拼接,输出多行。 + - 然后再用这里的结果作为临时表,给更上层的关联去用(嵌套查询的含义) + - 如果是 left join,则就是临时表如果为空,则给 a 那一行拼接一个null。 + + +#### 关联查询优化器 + +- join 实际执行的顺序会关系到性能 +- 例如: + - a\b\c 三个表关联,可能先让 a 和 b 关联得到的临时表里的记录只有10条; + - 如果让a和c先关联,会有10000条, 那么后面的效率就会截然不同 +- EXPLAIN EXTENDED 可以展示关联的顺序 +- STRAIGHT_JOIN可以手动指定关联顺序 +- mysql 自己会评估搜索一个最优的顺序,但如果 join 表太多,则无法搜完所有结果(O(n!)), 那时候就会采用贪心。 +- 是否使用贪心算法的边界值可以根据 `optimizer_seartch_depth` 去指定。 + +#### 排序优化 + +- 如果排序的量小,就用内存快速排序;如果排序的量大,就用文件排序 +- mysql有2种取排序数据的方式: + - 两次传输排序: 先取要排序的字段加行序号,按照字段排序好之后,再根据行索引一条条取读 + - 优点: 排序时占用内存小。 + - 缺点: 排序之后读的过程会很慢,根据行序号取读不是很方便 + - 单次传输排序: 直接把行读出来(行里只有需要用的列,不一定是整行) ,然后排序 + - 优点: 把全部行读出来相当于顺序IO,读取速度快 + - 缺点: 可能会很大导致需要文件排序 + - 关联查询order by的注意事项 + - 如果order by的列 都 来自关联的 第一张 表,则直接第一张表join的时候就排序了。 + - 全部join完再排序,就算用了limit,也是全部join+排序后,再limit。 + +# 执行器:根据执行计划,调用存储引擎的API来执行查询 + +- 当分析器生成查询计划,并且经过优化器以后,就到了执行器。 +- 执行器会选择执行计划开始执行,但在执行之前会校验请求用户是否拥有查询的权限, + - 如果没有权限,就会返回错误信息 + - 否则将会去调用 MySQL 引擎层的接口 +- 执行对应的SQL语句并且返回结果。 + +# 返回结果 + +- 将结果返回给客户端 diff --git "a/_posts/2022-02-01-MySQL\357\274\232\345\233\236\350\241\250\351\227\256\351\242\230\345\217\212\345\244\204\347\220\206\346\226\271\346\263\225.md" "b/_posts/2022-02-01-MySQL\357\274\232\345\233\236\350\241\250\351\227\256\351\242\230\345\217\212\345\244\204\347\220\206\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..e6d97c03d37 --- /dev/null +++ "b/_posts/2022-02-01-MySQL\357\274\232\345\233\236\350\241\250\351\227\256\351\242\230\345\217\212\345\244\204\347\220\206\346\226\271\346\263\225.md" @@ -0,0 +1,94 @@ +--- +layout: post +title: MySQL:回表问题及处理方法 +subtitle: 回表问题 & 索引覆盖 & 联合索引 +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 数据库 +--- + + +# 两类索引 + +- MySQL 中的索引按照物理存储方式,可以分为聚簇索引和非聚簇索引: + - 日常所说的主键索引,其实就是聚簇索引(Clustered Index); + - 主键索引之外,其他的都称之为非主键索引, + - 非主键索引也被称为二级索引(Secondary Index),或者叫作辅助索引。 + +- 对于主键索引和非主键索引,使用的数据结构都是 B+Tree,唯一的区别在于叶子结点中存储的内容不同: + - 主键索引的叶子节点,存储的是一行完整的数据; + - 非主键索引的叶子节点,存储的则是主键值,叶子结点不包含行记录的全部数据; + +# MySQL 回表问题 + +- 当我们需要 SQL 查询的时候: + - 如果是通过主键索引来查询数据 + - 例如: `select * from user where id=02`,那么此时只需要搜索主键索引的 B+Tree 就可以找到数据; + - 如果是通过非主键索引来查询数据 + - 例如:`select * from user where username='小王'`,那么此时需要 **先搜索 username 这一列索引的 B+Tree,搜索完成后得到主键的值,然后再去搜索主键索引的 B+Tree,就可以获取到一行完整的数据**。 + - 对于第二种查询方式而言,一共搜索了两棵 B+Tree,第一次搜索 B+Tree 拿到主键值后再去搜索主键索引的 B+Tree,这个过程就是所谓的回表。 + +- 两种查询方式差异: + - 通过非主键索引查询要扫描两棵 B+Tree,而通过主键索引查询只需要扫描一棵 B+Tree; + - 所以,如果条件允许,还是建议在查询中优先选择通过主键索引进行搜索。 + - 例如: + - 对于 `student` 表,其中包含字段 `id` 设置主键索引、`name` 设置普通索引、`age`(无索引),并向数据库中插入4条数据:("小赵", 10)("小王", 11)("小李", 12)("小陈", 13) + + ![]({{site.baseurl}}/img-post/mysql-7.png) + + - 分析发现: + - 在叶子节点中主键索引存储了整行数据; + - 而非主键索引中存储的值为主键 id; + +- 执行如下 sql `SELECT age FROM student WHERE name = '小李'`,流程为: + - 在 name 索引树上找到名称为小李的节点 id 为 03; + - 从 id 索引树上找到 id 为 03 的节点,并获取所有数据; + - 从数据中获取字段名为 age 的值返回 12; + - 在流程中,从非主键索引树搜索回到主键索引树搜索的过程,就是回表。 + +# 索引覆盖 + +- 回表问题: + - 在前面的查询中,因为查询结果只存在主键索引树中,必须回表才能查询到结果; + - 因为需要两次查询,整个过程性能低下、耗费资源、浪费时间。 + - 那么如何优化这个过程呢?这就需要使用覆盖索引。 + +- 索引覆盖: + - 就是把单列的 **非主键索引** 修改为 **多字段** 的联合索引; + - 这样在一棵索引数上,就找到了想要的数据,不需要去主键索引树上、再检索一遍; + - 这个现象,称之为 **索引覆盖**。 + +- 索引覆盖避免了回表: + - 索引覆盖从非主键索引中就能查到的记录,而不需要查询主键索引中的记录,避免了回表的产生减少了树的搜索次数,显著提升性能。 + +# 使用联合索引实现索引覆盖 + +- 前面介绍了覆盖索引,但具体如何实现呢?这就要使用到 **联合索引**。 +- 示例: + - 对于之前已经建立的表 `student`,业务需求是根据名称获取学生的年龄,并且该搜索场景非常频繁; + - 这时就要: + - 先删除掉之前以字段 `name` 建立的普通索引; + - 再以 `name` 和 `age` 两个字段建立联合索引; + - 此时,sql 命令与建立后的索引树结构如下: + + ![]({{site.baseurl}}/img-post/mysql-8.png) + +- 执行如下sql:`select age from student where name = '小李'`,执行流程为: + - 在 `name,age` 联合索引树上找到名称为小李的节点; + - 此时,节点索引(非主键索引)里包含信息 age 直接返回 12; + +- 如何确定数据库成功使用了覆盖索引呢? + - 当发起一个索引覆盖查询时,在 `explain` 的 `extra` 列可以看到 `using index` 的信息,这就表明成功使用了覆盖索引。 + + ![]({{site.baseurl}}/img-post/mysql-9.png) + +- 联合索引,实现了覆盖索引,避免了回表现象的产生,从而减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是性能优化的一种手段。 + +- 哪些场景需要覆盖索引 + - 全表 count 查询 + - 列查询回表优化 + - 分页查询 + diff --git "a/_posts/2022-02-01-Python\357\274\232Python SQLalchemy \344\275\277\347\224\250.md" "b/_posts/2022-02-01-Python\357\274\232Python SQLalchemy \344\275\277\347\224\250.md" new file mode 100644 index 00000000000..1f9f23d2afb --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232Python SQLalchemy \344\275\277\347\224\250.md" @@ -0,0 +1,86 @@ +--- +layout: post +title: Python:Python SQLalchemy 使用 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. sqlalchemy 中 create_engine 中的参数 +- pool_size + - 设置连接池中,保持的连接数; + - 初始化时并不产生连接,只有慢慢需要连接时,才会产生连接; + - 例如我们的连接数设置成 pool_size=10,如果我们的并发量一直最高是 5,那么我们的连接池里的连接数也就是5,当我们有一次并发量达到了 10,以后并发量虽然下去了,连接池中也会保持 10 个连接。 + +- max_overflow + - 当连接池里的连接数已达到 pool_size 且都被使用时,max_overflow就是允许再新建的连接数; + - 例如pool_size=10,max_overlfow=5,当我们的并发量达到 12 时,当第 11 个并发到来后,就会去再建一个连接,第 12 个同样,当第11个连接处理完回收后,若没有在等待进程获取连接,这个连接将会被立即释放。 + +- pool_timeout + - 从连接池里获取连接,如果此时无空闲的连接,且连接数已经到达了 pool_size+max_overflow,此时获取连接的进程会等待pool_timeout秒,如果超过这个时间,还没有获得将会抛出异常; + - sqlalchemy 默认 30 秒; + +- pool_recycle + - pool_recycle 指一个数据库连接的生存时间; + - 例如 pool_recycle=3600,也就是当这个连接产生 1 小时后,再获得这个连接时,会丢弃这个连接,重新创建一个新的连接; + - 当 pool_recycle 设置为 -1 时,也就是连接池不会主动丢弃这个连接、永久可用,但是有可能数据库 server 设置了连接超时时间,例如 mysql 设置的有 wait_timeout 默认为 28800、即 8 小时,当连接空闲 8 小时时会自动断开,8 小时后再用这个连接也会被重置。 + +# 2. 连接实例 +- sqlalchemy + mysql 应用示例: +``` +from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session +from models import Student,Course,Student2Course +from threading import Thread + + +engine = create_engine( + "mysql+pymysql://root:123456@127.0.0.1:3306/s9day120?charset=utf8", + max_overflow=0, # 超过连接池大小外最多创建的连接 + pool_size=5, # 连接池大小 + pool_timeout=30, # 池中没有线程最多等待的时间,否则报错 + pool_recycle=-1 # 多久之后对线程池中的线程进行一次连接的回收(重置) + ) +SessionFactory = sessionmaker(bind=engine) +session = scoped_session(SessionFactory) + + +def task(): + """""" + # 方式一: + """ + # 查询 + # cursor = session.execute('select * from users') + # result = cursor.fetchall() + + # 添加 + cursor = session.execute('INSERT INTO users(name) VALUES(:value)', params={"value": 'wupeiqi'}) + session.commit() + print(cursor.lastrowid) + """ + # 方式二: + """ + # conn = engine.raw_connection() + # cursor = conn.cursor() + # cursor.execute( + # "select * from t1" + # ) + # result = cursor.fetchall() + # cursor.close() + # conn.close() + """ + + # 将连接交还给连接池 + session.remove() + + +for i in range(20): + t = Thread(target=task) + t.start() +``` \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232Python \344\270\255\347\232\204\345\217\230\351\207\217\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210.md" "b/_posts/2022-02-01-Python\357\274\232Python \344\270\255\347\232\204\345\217\230\351\207\217\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210.md" new file mode 100644 index 00000000000..93c0eda70b9 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232Python \344\270\255\347\232\204\345\217\230\351\207\217\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210.md" @@ -0,0 +1,70 @@ +--- +layout: post +title: Python:Python 中的变量到底是什么 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. 变量的概念 +- 用标识符命名的存储单元的地址称为变量; +- 变量是用来存储数据的,通过标识符可以获取变量的值,也可以对变量进行赋值; +# 2. 变量三要素 +- 变量是有三部分构成,变量名、赋值符号、变量值,示例如下: +``` +# 变量名 赋值符号 变量值 + name = "Tom" +``` +- 变量名 + - 即 **name**; + - 当变量定义的时候,会在内存中申请一块空间专门用来存放变量值,而变量名就是这个空间的 **门牌号**,能方便的找到这块内存空间; +- 赋值符号 + - 即 **==** 号; + - 它的意义是将右侧的变量值内存地址绑给左侧的变量名,当赋值动作完成后,变量名所指向的存储单元存储了被赋的值; + - 在Pyhton语言中赋值操作符为“=、+=、-=、*=、/=、%=、**=、//=”; +- 变量值 + - 即 **"Tom"** 这个字符串; + - 变量值就是内存区域的状态,被修改为 **"Tom"** 这个字符串,定义之后可以用变量名来访问这个字符串; + - 变量值有一个 **引用次数**,一旦这块空间被引用次数为 0,就意味着我们没有途径能找到这块空间了,此时如果不对其进行清理,就会导致内存被占用; + - 系统找不到这块空间时,就会触发Python的内存管理 **垃圾回收机制**,对该空间进行回收在利用; +# 3. 变量三特性 +- id + - 就是变量值得内存空间地址,每一个值都有唯一一个id; + - 内存地址不同,id就不相同; + - 可以使用 id() 来查看变量的内存地址。 +- type + - 不同类型的值记录事物的状态有所不同,这就是Python的数据类型,如 str、int 等; + - 可以使用type()来查看。 +- 变量值 + - 变量值就是存储值的本身。 +- Python 和 Java 中 变量的区别 + - Java 中声明变量的时候,会在内存中开辟一个内存区域,这个区域的大小根据变量的类型不同而不同; + - Python 中变量实质上是一个指针,先把变量的赋值(入一个整数)创建好,然后把变量的指针指向创建好的区域。这类似于 **便利贴**,变量本身没有类型,赋值才有类型,变量只是贴在赋值上的一个 “便利贴” 而已; + +# 4. 变量操作示例 +- 查看变量三特性 +``` +>>> a = "hello" # 赋值 +>>> id(a) # 查看 id +2280593916592 +>>> type(a) # 查看 type + +>>> print(a) # 查看值 +hello +``` +- 多变量指向同一内存区域 + - 有如下代码和执行结果; +``` +>>> a = [1, 2, 3] +>>> b = a +>>> b.append(4) +>>> print(a) +[1, 2, 3, 4] +``` +- 通过上面的示例我们发现,我们将 a 赋值给 b 以后,对 b 的 append 操作直接影响了 a,导致 a 也跟着 append 一个 4 元素; +- 这从侧面说明 a 和 b 两个变量指向同一块内存区域。 \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232Python \345\244\204\347\220\206\346\226\207\344\273\266\346\223\215\344\275\234\345\221\275\344\273\244\346\261\207\346\200\273.md" "b/_posts/2022-02-01-Python\357\274\232Python \345\244\204\347\220\206\346\226\207\344\273\266\346\223\215\344\275\234\345\221\275\344\273\244\346\261\207\346\200\273.md" new file mode 100644 index 00000000000..1fa3233b269 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232Python \345\244\204\347\220\206\346\226\207\344\273\266\346\223\215\344\275\234\345\221\275\344\273\244\346\261\207\346\200\273.md" @@ -0,0 +1,212 @@ +--- +layout: post +title: Python:Python 处理文件操作命令汇总 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. python 中对文件、文件夹的操作 +- 得到当前工作目录,即当前Python脚本工作的目录路径 +``` +os.getcwd() +``` + +- 返回指定目录下的所有文件和目录名 +``` +os.listdir() +``` + +- 函数用来删除一个文件 +``` +os.remove() +``` + +- 删除多个目录 +``` +os.removedirs(r“c:\python”) +``` + +- 检验给出的路径是否是一个文件 +``` +os.path.isfile() +``` + +- 检验给出的路径是否是一个目录 +``` +os.path.isdir() +``` + +- 判断是否是绝对路径 +``` +os.path.isabs() +``` + +- 检验给出的路径是否真地存 +``` +os.path.exists() +``` +- 判断文件夹是否存在 +``` +os.path.exists(test_dir) +``` +- 返回一个路径的目录名和文件名 +``` +os.path.split() eg os.path.split('/home/swaroop/byte/code/poem.txt') 结果:('/home/swaroop/byte/code', 'poem.txt') +``` + +- 分离扩展名 +``` +os.path.splitext() +``` + +- 获取路径名 +``` +os.path.dirname() +``` + +- 只检查文件 +``` +os.path.isfile("test-data") +``` +- 获取文件名 +``` +os.path.basename() +``` + +- 运行shell命令: +``` +os.system() +``` + +- 读取和设置环境变量 +``` +os.getenv() 与os.putenv() +``` + +- 给出当前平台使用的行终止符 +``` +os.linesep Windows使用'\r\n',Linux使用'\n'而Mac使用'\r' +``` + +- 指示你正在使用的平台 +``` +os.name 对于Windows,它是'nt',而对于Linux/Unix用户,它是'posix' +``` +- 重命名 +``` +os.rename(old, new) +``` + +- 创建多级目录 +``` +os.makedirs(r“c:\python\test”) +``` + +- 创建单个目录 +``` +os.mkdir(“test”) +``` + +- 获取文件属性 +``` +os.stat(file) +``` + +- 修改文件权限与时间戳 +``` +os.chmod(file) +``` + +- 终止当前进程 +``` +os.exit() +``` + +- 获取文件大小 +``` +os.path.getsize(filename) +``` +- 文件操作 +``` +os.mknod("test.txt") 创建空文件 +fp = open("test.txt",w) 直接打开一个文件,如果文件不存在则创建文件 +``` + +# 2. 目录操作 + +``` +os.mkdir("file") 创建目录 +``` +- 复制文件 + +``` +shutil.copyfile("oldfile","newfile") oldfile和newfile都只能是文件 +shutil.copy("oldfile","newfile") oldfile只能是文件夹,newfile可以是文件,也可以是目标目录 +``` +- 复制文件夹 + +``` +shutil.copytree("olddir","newdir") olddir和newdir都只能是目录,且newdir必须不存在 +``` +- 重命名文件(目录) + +``` +os.rename("oldname","newname") 文件或目录都是使用这条命令 +``` +- 移动文件(目录) + +``` +shutil.move("oldpos","newpos") +``` +- 删除文件 + +``` +os.remove("file") +``` +- 删除目录 + +``` +os.rmdir("dir")只能删除空目录 +shutil.rmtree("dir") 空目录、有内容的目录都可以删 +``` +- 转换目录 + +``` +os.chdir("path") 换路径 +``` + +# 3. open 模式相关操作 + +``` +w 以写方式打开, +a 以追加模式打开 (从 EOF 开始, 必要时创建新文件) +r+ 以读写模式打开 +w+ 以读写模式打开 (参见 w ) +a+ 以读写模式打开 (参见 a ) +rb 以二进制读模式打开 +wb 以二进制写模式打开 (参见 w ) +ab 以二进制追加模式打开 (参见 a ) +rb+ 以二进制读写模式打开 (参见 r+ ) +wb+ 以二进制读写模式打开 (参见 w+ ) +ab+ 以二进制读写模式打开 (参见 a+ ) + +fp.read([size]) #size为读取的长度,以byte为单位 +fp.readline([size]) #读一行,如果定义了size,有可能返回的只是一行的一部分 +fp.readlines([size]) #把文件每一行作为一个list的一个成员,并返回这个list。其实它的内部是通过循环调用readline()来实现的。如果提供size参数,size是表示读取内容的总长,也就是说可能只读到文件的一部分。 +fp.write(str) #把str写到文件中,write()并不会在str后加上一个换行符 +fp.writelines(seq) #把seq的内容全部写到文件中(多行一次性写入)。这个函数也只是忠实地写入,不会在每行后面加上任何东西。 +fp.close() #关闭文件。python会在一个文件不用后自动关闭文件,不过这一功能没有保证,最好还是养成自己关闭的习惯。 如果一个文件在关闭后还对其进行操作会产生ValueError +fp.flush() #把缓冲区的内容写入硬盘 +fp.fileno() #返回一个长整型的”文件标签“ +fp.isatty() #文件是否是一个终端设备文件(unix系统中的) +fp.tell() #返回文件操作标记的当前位置,以文件的开头为原点 +fp.next() #返回下一行,并将文件操作标记位移到下一行。把一个file用于for … in file这样的语句时,就是调用next()函数来实现遍历的。 +fp.seek(offset[,whence]) #将文件打操作标记移到offset的位置。这个offset一般是相对于文件的开头来计算的,一般为正数。但如果提供了whence参数就不一定了,whence可以为0表示从头开始计算,1表示以当前位置为原点计算。2表示以文件末尾为原点进行计算。需要注意,如果文件以a或a+的模式打开,每次进行写操作时,文件操作标记会自动返回到文件末尾。 +fp.truncate([size]) #把文件裁成规定的大小,默认的是裁到当前文件操作标记的位置。如果size比文件的大小还要大,依据系统的不同可能是不改变文件,也可能是用0把文件补到相应的大小,也可能是以一些随机的内容加上去。 +``` \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232Python \345\244\232\347\272\277\347\250\213 & \345\244\232\350\277\233\347\250\213 & \345\215\217\347\250\213.md" "b/_posts/2022-02-01-Python\357\274\232Python \345\244\232\347\272\277\347\250\213 & \345\244\232\350\277\233\347\250\213 & \345\215\217\347\250\213.md" new file mode 100644 index 00000000000..7cc1a26b3df --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232Python \345\244\232\347\272\277\347\250\213 & \345\244\232\350\277\233\347\250\213 & \345\215\217\347\250\213.md" @@ -0,0 +1,241 @@ +--- +layout: post +title: Python:Python 多线程 & 多进程 & 协程 +subtitle: 异同对比、使用示例 +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. 多进程 + +#### 1.1. 什么是进程 + +- 进程(Process),是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位; +- 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义; +- 进程由程序、数据集合和进程控制块三部分组成: + -- 程序用于描述进程要完成的功能,是控制进程执行的指令集; + -- 数据集合是程序在执行时所需要的数据和工作区; + -- 程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。 + +#### 1.2. 进程的特点 + +- 进程是一个实体; +- 对操作系统来说,**进程是最小的资源管理单元**; +- 进程之间相互独立,不能共享变量,进程内的线程对其他进程不可见; +- 任何进程都可以同其他进程一起并发执行; +- 任何进程都可以同其他进程一起并发执行; + + + +# 2. 多线程 + +#### 2.1. 什么是线程 + +- 线程(Thread),是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位; +- 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程; +- 线程不拥有系统资源,只有运行必须的一些数据结构,线程与父进程的其它线程共享该进程所拥有的全部资源。 +- 线程可以创建和撤消程,从而实现程序的并发执行; +- 一般,线程具有就绪、阻塞和运行三种基本状态。 + +#### 2.2. 线程的特点 + +- 对操作系统来说,**线程是最小的任务执行单元**; +- 线程运行在同一个进程下,线程间可以共享内存空间; +- 线程是一个实体; +- 线程上下文切换比进程更频繁; + +#### 2.3. 线程的工作原理 + +- 根据计算机多核 CPU 处理机制,当线程的数量小于处理器的数量时,线程的并发是真正的并发,不同的线程运行在不同的处理器上; +- 但当线程的数量大于 CPU 处理器的数量时,线程的并发会受到一些阻碍,此时并不是真正的并发,因为此时至少有一个处理器会运行多个线程; +- 在单个处理器运行多个线程时,并发是一种模拟出来的状态,几乎所有的操作系统都是采用时间片轮转的方式抢占调度、轮流执行每一个线程,如我们熟悉的Unix、Linux、Windows及macOS等流行的操作系统; + +#### 2.4. 线程的任务调度 + +- 在一个进程中,当一个线程任务执行几毫秒后,会由操作系统的内核(负责管理各个任务)进行调度,通过硬件的计数器中断处理器,让该线程强制暂停并将该线程的寄存器放入内存中,通过查看线程列表决定接下来执行哪一个线程,并从内存中恢复该线程的寄存器,最后恢复该线程的执行,从而去执行下一个任务。 +- 上述过程中,任务执行的那一小段时间叫做时间片,任务正在执行时的状态叫运行状态,被暂停的线程任务状态叫做就绪状态,意为等待下一个属于它的时间片的到来。 +- 这种方式保证了每个线程轮流执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。 + +#### 2.5. 线程与进程的区别 + +- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位; +- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线; +- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见; +- **线程上下文切换比进程上下文切换要快得多**。 + +#### 2.6. 多线程实例 + +- threading.Thread + + ```aidl + import time + import threading + + + def test1(): + print('task 1 start...') + time.sleep(1) + print('task 1 end') + + + def test2(): + print('task 2 start...') + time.sleep(2) + print('task 2 end') + + + def test3(): + print('task 3 start...') + time.sleep(3) + print('task 3 end') + + + def test4(): + print('task 4 start...') + time.sleep(4) + print('task 4 end') + + + def test5(): + print('task 5 start...') + time.sleep(5) + print('task 5 end') + + + def test(): + tasks = [ + threading.Thread(target=test1,), + threading.Thread(target=test2,), + threading.Thread(target=test3,), + threading.Thread(target=test4,), + threading.Thread(target=test5,), + ] + for t in tasks: + t.start() + for t in tasks: + t.join() + + + if __name__ == '__main__': + test() + ``` + +- ThreadPoolExecutor + + ```aidl + import time + from concurrent.futures import ThreadPoolExecutor + from concurrent.futures import wait, ALL_COMPLETED + + + def test1(): + print('task 1 start...') + time.sleep(1) + print('task 1 end') + + + def test2(): + print('task 2 start...') + time.sleep(2) + print('task 2 end') + + + def test3(): + print('task 3 start...') + time.sleep(3) + print('task 3 end') + + + def test4(): + print('task 4 start...') + time.sleep(4) + print('task 4 end') + + + def test5(): + print('task 5 start...') + time.sleep(5) + print('task 5 end') + + + def test(): + thread_pool = ThreadPoolExecutor(max_workers=5) + tasks = [] + tasks.append(thread_pool.submit(test1)) + tasks.append(thread_pool.submit(test2)) + tasks.append(thread_pool.submit(test3)) + tasks.append(thread_pool.submit(test4)) + tasks.append(thread_pool.submit(test5)) + + wait(tasks, return_when=ALL_COMPLETED) + + + if __name__ == '__main__': + test() + ``` + + +# 3. 协程 + +#### 3.1. 什么是协程 + +- 协程,又称微线程,纤程; +- 协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的; +- 基于单线程来实现并发,在一个线程中可以开启很多协程; +- 协程并不是实际存在的实体,本质上是一个线程的调度控制机制; +- 在执行程序的过程中,遇到 IO 操作就冻结当前位置的状态,去执行其他任务,在执行其他任务的过程中,会不断地检测上一个冻结的任务是否 IO 结束,如果 IO 结束了,就继续从冻结的位置开始执行。 + +#### 3.2 为什么需要协程 + +- 在传统的J2EE系统中,都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时; +- 如果 **遇到很耗时的 I/O 行为,这个时候线程一直处于阻塞状态**,则整个系统的吞吐立刻下降,如果线程很多的时候,会存在很多线程处于空闲状态(等待该线程执行完才能执行),造成了资源应用不彻底; +- 协程的目的就是当出现长时间的 I/O 操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除 ContextSwitch 上的开销; + +#### 3.3. 协程的工作原理 + +- **当出现 I/O 阻塞的时候,由协程的调度器进行调度,通过将数据流立刻 yield 掉(主动让出),并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去跑**; +- 这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为 coroutine,而跑在由 coroutine 负责调度的线程称为 Fiber。由于协程的暂停完全由程序控制,发生在用户层;而线程的阻塞状态是由操作系统内核来进行切换,发生在内核层。因此,协程的开销远远小于线程的开销,也就没有了ContextSwitch上的开销; +- 协程比线程之间的切换和线程的创建销毁所花费的时间,空间开销要小的多; + +#### 3.4. 协程的特点 + +- 协程并不是一个实体; +- 协程在一个线程里实现并发; +- 协程工作在单个线程内,只有挂起、恢复和销毁三种状态 +- 用户程序里自己保存多个控制流的上下文栈; +- 一个协程遇到 I/O 操作自动切换到其他协程; +- 线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率; +- 线程的默认 Stack 大小是 1M,而协程更轻量,接近 1K,因此可以在相同的内存中开启更多的协程; +- **协程工作在在同一个线程上,因此可以避免竞争关系而使用锁**,因此协程修改共享数据不需要加锁; +- 协程适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决。 + +#### 3.5. 协程的有点 + +- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级。 +- 单线程内就可以实现并发的效果,最大限度地利用cpu. + +#### 3.6. 协程的缺点 + +- 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启多个协程。 +- 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程。 + +# 4. 协程 & 进程 & 线程 的区别 + +- 协程,是函数级别的调度; +- 进程 & 线程,都是内核级别的程序调度; + + +| 对比维度 | 进程 | 线程 | 协程 | +|:----:|:----:|:----:|:----:| +| 调度 | 系统内核 | 系统内核 | 用户 | +| 创建、销毁 | 一次创建、一次销毁 | 多线程频繁创建、切换,耗费 CPU 资源 | 一次创建、一次销毁 | +| 内存、CPU | 占用内存多、切换复杂,CPU 利用率低 | 占用内存少、CPU 利用率高 |资源占用小 | +| 通信 | 多进程 不能 共享变量 | 多线程 可以 共享变量 | 协程可以共享变量 | +| 同步 | Queue、Pipe | 共享变量 & 锁 | 不需要锁 | +| 应用 | 多进程适用于 CPU 密集型操作(计算)| 多线程适用于 IO 密集型操作(爬虫) | 协程适用于高并发需求场景,如爬虫并发、服务器并发 + diff --git "a/_posts/2022-02-01-Python\357\274\232Python \345\270\270\347\224\250\344\273\243\347\240\201\346\256\265\346\200\273\347\273\223.md" "b/_posts/2022-02-01-Python\357\274\232Python \345\270\270\347\224\250\344\273\243\347\240\201\346\256\265\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..00a23f195f9 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232Python \345\270\270\347\224\250\344\273\243\347\240\201\346\256\265\346\200\273\347\273\223.md" @@ -0,0 +1,184 @@ +--- +layout: post +title: Python:Python 常用代码段总结 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 使用 try except import 第三方工具包 + +#### 需求 +- 在 import 第三方工具包时,使用 try ... except ... 进行导入,以提高代码健壮性。 + +#### 实现 + +- 此处以导入 pandas 为例进行演示 + ``` + import os # 注意:此包必须提前直接导入 + try: + import pandas as pd + except: + os.system('pip install pandas') # 注意 pip 和 pip3 在使用时的区别 + import pandas as pd + + frame = pd.read_csv('file.csv', engine='python') + ... + ``` + +# 统计程序执行时间 + +#### 需求 + +- 在 python 程序中,记录程序开始结束时间,用于统计程序运行时长。 + +#### 实现 + +- 方法一:普通方法 + ``` + import datetime + + def main(): + ... # 程序运行函数 + + if __name__ == '__main__': + starttime = datetime.datetime.now() # 记录开始时间 + + main() # 运行主程序 + + endtime = datetime.datetime.now() # 记录结束时间 + print (endtime - starttime).seconds # 以秒为单位,计算运行时长 + ``` +- 方法二:装饰器 + ``` + import time + from functools import wraps + + def fn_timer(function): + @wraps(function) + def function_timer(*args, **kwargs): + t0 = time.time() + result = function(*args, **kwargs) + t1 = time.time() + print ("Total time running %s: %s seconds" % + (function.func_name, str(t1-t0)) + ) + return result + return function_timer + + @fn_timer + def main(): + ... # 程序运行函数 + + if __name__ == '__main__': + main() # 运行主程序 + ``` + +# 以追加方式写入数据到 csv 文件中 + +#### 需求 + +- python 打开文件,并以追加方式写入数据到 csv 文件中。 + +#### 解决 +- 代码 + + ``` + import csv + + def write_csv(path, data_row): + with open(path,'a+') as f: + csv_write = csv.writer(f) + csv_write.writerow(data_row) + + path = "file_name.csv" + data_row = ["1","2"] + write_csv(path, data_row) + + ``` +#### 补充 + +>w:以写方式打开,  +a:以追加模式打开 (从 EOF 开始, 必要时创建新文件)  +r+:以读写模式打开  +w+:以读写模式打开 (参见 w )  +a+:以读写模式打开 (参见 a )  +rb:以二进制读模式打开  +wb:以二进制写模式打开 (参见 w )  +ab:以二进制追加模式打开 (参见 a )  +rb+:以二进制读写模式打开 (参见 r+ )  +wb+:以二进制读写模式打开 (参见 w+ )  +ab+:以二进制读写模式打开 (参见 a+ ) + + +# 新建 csv 文件写入第一行标题,并跳过第一行追加写入数据 + +#### 需求 + +- 如题,本文的目标是新建 csv 文件,在第一行写入标题,并追加写入数据。这里需要注意,标题写入第一行以后,后续就会自动跳过。 + +#### 解决 + +- 代码 + ``` + import csv + + + def write_to_csv(file_name, item): + ''' + :params file_name: 保存的文件名 + :params item: 要保存的额数据 # 此处保存的数据是 dict 字典格式的 + ''' + with open("{}.csv".format(file_name), "a+", newline='') as csv_writer: + writer = csv.writer(csv_writer) + #以读的方式打开csv 用csv.reader方式判断是否存在标题。 + with open("test.csv", "r", newline="") as csv_reader: + reader = csv.reader(csv_reader) + if not [row for row in reader]: + writer.writerow(item.keys()) # keys 作为第一行标题 + writer.writerows(item.values()) # 追加写入 values 值 + else: + writer.writerows(item.values()) # 追加写入 values 值 + + + def main(): + file_name = 'csv_file_name' + item = { + 'key_1': 'value_1', + 'key_2': 'value_2' + } + write_to_csv(file_name, item) + + if __name__ == '__main__': + main() + ``` + +# 按 key 首字母位置对字典键值对进行重新排序 + +#### 需求 + +- 现有一个字典,如下: + ``` + dict_1 = { + 'c_key': 'c_value', + 'a_key': 'a_value', + 'd_key': 'd_value', + 'b_key': 'b_value', + } + ``` +- 需要将其按照 ‘a_key’, 'b_key', 'c_key', 'd_key' 的顺序,对 dict_1 中的键值对进行重新排序。 +#### 实现 +- 代码 + ``` + dict_2 = dict(sorted(dict_1.items(), key=lambda i:i[0])) + print(dict_2) + ``` + 得到结果如下: + ``` + {'a_key': 'a_value', 'b_key': 'b_value', 'c_key': 'c_value', 'd_key': 'd_value'} + ``` \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232Python \347\224\237\346\210\220\345\231\250\345\216\237\347\220\206\345\217\212\345\272\224\347\224\250\345\256\236\350\267\265.md" "b/_posts/2022-02-01-Python\357\274\232Python \347\224\237\346\210\220\345\231\250\345\216\237\347\220\206\345\217\212\345\272\224\347\224\250\345\256\236\350\267\265.md" new file mode 100644 index 00000000000..f58d7544f07 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232Python \347\224\237\346\210\220\345\231\250\345\216\237\347\220\206\345\217\212\345\272\224\347\224\250\345\256\236\350\267\265.md" @@ -0,0 +1,414 @@ +--- +layout: post +title: Python:Python 生成器原理及应用实践 +subtitle: 用生成器构建斐波那契数列和杨辉三角 +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + +## 生成器函数 + +#### 什么是生成器 + +- 在 Python 中,一边循环一边计算的机制,称为生成器(Generator); +- 生成器是一个返回迭代器的函数,只能用于迭代操作; +- 生成器可以通过生成器表达式和生成器函数获取到; +- 生成器是 Python 中的一个对象,对这个对象进行操作,可以依次生产出按生成器内部运算规则产生的数据; +- **生成器,最大的特点,就是可以 暂停 \ 恢复**; + +#### 迭代器与生成器的关系 + +> 【说明】:很多文章在介绍生成器的时候,上来就引入 ```yield```,说使用 ```yiled``` 关键字的函数就是生成器, 但很多人并不能理解 ```yield``` 的含义,这种用新概念解释新概念的方式,对于新人来说很不友好,这里我先从最基本的迭代器开始,介绍如何用迭代器构造一个生成器,进而引入 ```yiled``` 概念。 + +- ```生成器,本质上仍然是一个迭代器```,它是对迭代器的调用,同时自身也是一个迭代器,符合迭代器的构造规范; + - 不了解迭代器的朋友,可以参考我的另一篇文章《[Python:理解迭代器,并用迭代器生成斐波那契数列](https://www.jianshu.com/p/5dc09fcee57a)》; + +- 生成器是一种特殊的迭代器,与迭代器不同的是,```生成器是被动生成```,每被调用一次则生成一次; + +- 下面,我们用迭代器实现了一个生成器函数,: + - 其中 ```IteraleSample``` 是一个可迭代类,```Generator``` 是一个 生成器类; + - ```Generator``` 通过 ```iterator_sample``` 属性调用 ```IteraleSample``` 类; + - 同时 ```Generator``` 将自身的 ```max_generate_num``` 属性值赋值给 ```IteraleSample``` 的 ```max_iter_num```: + + ``` + class IteraleSample(object): + ''' + 创建一个类对象,并定义 __iter__、__next__ 两个方法 + ''' + def __init__(self, max_iter_num): + ''' + max_iter_num: 迭代器最大迭代次数 + ''' + self.current = 0 + self.max_iter_num = max_iter_num + + def __iter__(self): + return self + + def __next__(self): + # pass + if self.current < self.max_iter_num: + self.current += 1 + else: + raise StopIteration + return self.current + + + class Generator(object): + def __init__(self, max_generate_num): + ''' + max_generate_num: 生成器最大生成次数 + ''' + self.max_generate_num = max_generate_num + self.iterator_sample = IteraleSample(self.max_generate_num) + + def __iter__(self): + return self.iterator_sample + + def __next__(self): + return self.iterator_sample.__next__() * 10 + + + gen = Generator(10) # 实例化生成器,并赋值最大迭代次数为 10 + + print(next(gen)) # 使用 next 函数迭代元素 + print(next(gen)) + print(next(gen)) + print(next(gen)) + print(next(gen)) + print(next(gen)) + print(next(gen)) + print(next(gen)) + print(next(gen)) + print(next(gen)) + print(next(gen)) + print(next(gen)) # 此处,共计 next 迭代 11 次,超出了生成器的最大迭代次 10 + ``` + - 上面代码的执行结果如下,分析发现每次 ```next(gen)``` 都会迭代出一个结果值,这个结果值就是 ```IteraleSample``` 迭代出的元素(从1到10)乘以 10 的结果,而第十一次迭代时则报 ```StopIteration``` 异常; + ``` + 10 + 20 + 30 + 40 + 50 + 60 + 70 + 80 + 90 + 100 + Traceback (most recent call last): + File "C:\Users\Administrator\Desktop\iterator_test.py", line 94, in + print(next(gen)) + File "C:\Users\Administrator\Desktop\iterator_test.py", line 76, in __next__ + return self.iterator_sample.__next__() * 10 + File "C:\Users\Administrator\Desktop\iterator_test.py", line 63, in __next__ + raise StopIteration + StopIteration + ``` +#### 如何构造生成器 + +- 前面如果能看懂,其实生成器就已经理解的产不多了,```yield``` 关键字构造生成器函数只是一种比较简洁的写法罢了; +- 如果前面看不懂也没关系,只要记住一点就好了:**只要函数里面有 ```yield``` 关键字,就是生成器函数**; + -- 很多人看到这里时有点晕,因为人不懂什么是 ```yield```,我们 +- 生成器函数保存的是函数内部的算法,每次调用就通过算法计算出下一个元素的值,直到计算到最后一个元素; +- 生成器函数使用 **next()** 调用生成器函数的赋值对象(不是函数本身)返回 yiled 值; + + ``` + >>> def fun_gen(): + ... print("第一次 yield") + ... yield 1 + ... print("第二次 yield") + ... yield 2 + ... print("第三次 yield") + ... yield 3 + ... + >>> result = fun_gen() # 注意:必须将生成器函数赋值于某个变量!!! + >>> next(result) # next 调用的是被赋值后的变量,而不是函数本身; + 第一次 next + 1 + >>> next(result) # next 调用的是被赋值后的变量,而不是函数本身; + 第二次 next + 2 + >>> next(result) # next 调用的是被赋值后的变量,而不是函数本身; + 第三次 next + 3 + ``` + >注意:生成器函在被 next 调用之前,必须将生成器函数赋值于某个变量,next 调用的是被赋值后的变量,而不是函数本身,如:下面代码中的 result = fun_gen(),这一步一定不能省,next 调用的是 result,不是 fun_gen(); + +- 与迭代器一样,生成器中没有更多的元素时,next() 动作会抛出 **StopIteration** 的错误; + + ``` + >>> def fun_gen(): + ... print("第一次 yield") + ... yield 1 + ... print("第二次 yield") + ... yield 2 + ... print("第三次 yield") + ... yield 3 + ... + >>> + >>> result = fun_gen() + >>> next(result) + 第一次 yield + 1 + >>> next(result) + 第二次 yield + 2 + >>> next(result) + 第三次 yield + 3 + >>> next(result) + Traceback (most recent call last): + File "", line 1, in + StopIteration # 没有更多的元素,next() 动作抛出 StopIteration 的错误 + ``` + +- 实际使用生成器函数的时候,不必依次进行 next 调用,一般使用 for...in...,这样会更简洁、合理; + ``` + >>> def fun_gen(): + ... print("第一次 yield") + ... yield 1 + ... print("第二次 yield") + ... yield 2 + ... print("第三次 yield") + ... yield 3 + ... + >>> + >>> for i in fun_gen(): + ... print(i) + ... + 第一次 yield + 1 + 第二次 yield + 2 + 第三次 yield + 3 + ``` + +#### 生成器函数 与 普通函数 的区别 + +- 生成器函数可以多次 yield 结果值,普通函数只能返回一次结果值; +- 生成器函数需要结果值的时候就直接生成一个,不需要直接占用很大的内存空间,而普通函数执行时需要足够的内存空间,尤其是需要生成元素很多的列表、巨大的数值的时候,需要占用巨大的内存空间; +- 生成器函数仅仅是需要的时候才使用,有点像数据库操作单条记录使用的游标; +- 如果要读取并使用的内容远远超过内存,但是需要对所有的流中的内容进行处理,那么生成器是一个很好的选择,可以让生成器返回当前的处理状态,由于它可以保存状态,那么下一次直接处理即可; +- 相比于普通函数,生成器函数最大的优点在于: ```生成器函数处理集合的效率远远优于普通函数,内存占用低、耗时短```; + +## 用生成器构造斐波那契数列 + +#### 斐波那契数列简介 +- 斐波那契数列,是有一系列整数组成的数列; +- 这一数列任意位置上的数值与后一位相加,等于第三位数值; +- 示例: + ``` + 0 , 1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , ... + ``` +#### 普通方法构造斐波那契数列 +- 普通方法一: + - 如下面代码,使用递归构造斐波那契数列生成函数,并记录运行时间; + ``` + def feb_1(index): + """ + 斐波那契数列生成函数 + """ + if index <= 2: + return 1 + elif index > 2 and index < 101: + return feb_1(index-1) + feb_1(index-2) + + def test_time(max_index): + """ + 测试上面 斐波那契数列函数的执行情况 + """ + starttime = datetime.datetime.now() # 记录开始时间 + print("最大值为 : {}\n结果值为:{}".format(max_index, feb_1(max_index))) + endtime = datetime.datetime.now() # 记录结束时间 + print("运行时间为:{}".format(endtime - starttime)) # 以秒为单位,计算运行时长 + ``` + + - 方法缺陷: + - 每次只能获取一个末位的数值,而不是整个数列; + - 如果数值太大,运行时间会变得非常的长; + ``` + print(test_time(10)) + + print(test_time(20)) + + print(test_time(30)) + + print(test_time(40)) + + 最大值为 : 10 + 结果值为:55 + 运行时间为:0:00:00 + None + + 最大值为 : 20 + 结果值为:6765 + 运行时间为:0:00:00.003998 + None + + 最大值为 : 30 + 结果值为:832040 + 运行时间为:0:00:00.469920 + None + + 最大值为 : 40 + 结果值为:102334155 + 运行时间为:0:00:57.805053 # 此处,max_index=40,运行时间就已经需要 57 秒,相比前面的时间花费增速是几何式的, + None + ``` + + - 分析上面的例子: + - max_index = 30 的时候程序运行时常只需要 0:00:00.749871 秒; + - 到了 max_index = 40 的时候就已经需要 0:00:57.805053 秒,这种耗时的增速在实际生产中是不能接受的; + +- 普通方法二: + ``` + def feb_2(max_index): + """ + 斐波那契数列生成函数 + """ + result_list = [] # 此处使用 list 作为一个容器存储生成的数值,如果数值足够大这个 list 将会占用大量的的内存 + n, a, b = 0, 0, 1 + while n < max_index: + result_list.append(b) + a,b = b, a+b + n += 1 + return "最大值为 : {}\n结果值为:{}".format(max_index, result_list) + + def test_time(max_index): + """ + 测试上面 斐波那契数列函数的执行情况 + """ + starttime = datetime.datetime.now() # 记录开始时间 + print(feb_2(max_index)) + endtime = datetime.datetime.now() # 记录结束时间 + print("运行时间为 : {}".format(endtime - starttime)) # 以秒为单位,计算运行时长 + ``` + - 该方法的优点:相比与前面的方法,程序运行耗费的时间相对较快; + ``` + test_time(10) + + test_time(20) + + test_time(30) + + test_time(40) + + 最大值为 : 10 + 结果值为:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55] + 运行时间为 : 0:00:00.001000 + + 最大值为 : 20 + 结果值为:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765] + 运行时间为 : 0:00:00 + + 最大值为 : 30 + 结果值为:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040] + 运行时间为 : 0:00:00 + + 最大值为 : 40 + 结果值为:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155] + 运行时间为 : 0:00:00 + ``` + + - 该方法的缺陷:该方法使用 result_list = [] 作为一个容器存储生成的数值,如果数值足够大这个 list 将会占用大量内存空间,简言之:**如果数值过大,会占用大量内存**; + - 有兴趣的小伙伴可以尝试 max_index=10000 时程序的执行情况,看一看程序内存占用情况; + - 请注意:你的电脑可能会被卡死!!! + ``` + test_time(10000) # 注意:你的电脑可能会被卡死 + ``` + +#### 生成器构造斐波那契数列 + +- 如下面的代码: + - fib_3(max_index) 是用生成器函数构成的斐波那契数列生成函数; + - 在 test_time(max_index) 测试函数中使用 for i in fib_3(max_index) print(i) 打印所有的生成结果; + + ``` + import datetime + + + def fib_3(max_index): + """ + 使用 yiled 生成器构造的斐波那契数列生成函数 + """ + n, a, b = 0, 0, 1 + while n < max_index: + yield b # 此处使用 yiedl b + a,b = b, a+b + n += 1 + + + def test_time(max_index): + """ + 测试上面 斐波那契数列函数的执行情况 + """ + starttime = datetime.datetime.now() + for i in fib_3(max_index): # 使用 for ... in ... 遍历打印所有的生成结果 + print(i) + endtime = datetime.datetime.now() + print("运行时间为 : {}".format(endtime - starttime)) + + + test_time(10) + test_time(100) + test_time(1000) + test_time(10000) + ``` + +- 下面的结果展示了运行时间结果,; +``` +# 运行的时间如下所示: + +运行时间为 : 0:00:00 +运行时间为 : 0:00:00 +运行时间为 : 0:00:00.001000 +运行时间为 : 0:00:00.004998 + +``` + +#### 生成器函数构建杨辉三角 + +- 杨辉三角简介: + - 杨辉三角,是二项式系数在三角形中的一种几何排列,中国南宋数学家杨辉1261年所著的《详解九章算法》一书中出现; + - 在欧洲,帕斯卡在1654年发现这一规律,所以这个表又叫做[帕斯卡三角形; + - 杨辉三角示例如下图: + +![20190307104242486.png](https://upload-images.jianshu.io/upload_images/14502986-8f1d1a6652aa0be0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 杨辉三角特性: + - 每个数等于它上方两数之和; + - 每行数字左右对称,由1开始逐渐变大; +- 代码实现: + ``` + def triangles(max_index): # max_index 表示最大行数 + L = [1] + i = 1 + while i <= max_index: # 进入持续循环 + yield L[:len(L)] # 输出数组 + L.append(0) # 为数组添加最后一位 + L = [L[j - 1] + L[j] for j in range(len(L))] + i += 1 + + for i in triangles(10): # 使用 for 循环打印结果值 + print(i) + + ``` + - 运行结果 + ``` + [1] + [1, 1] + [1, 2, 1] + [1, 3, 3, 1] + [1, 4, 6, 4, 1] + [1, 5, 10, 10, 5, 1] + [1, 6, 15, 20, 15, 6, 1] + [1, 7, 21, 35, 35, 21, 7, 1] + [1, 8, 28, 56, 70, 56, 28, 8, 1] + [1, 9, 36, 84, 126, 126, 84, 36, 9, 1] + ``` \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232Python \350\257\255\350\250\200\344\270\255\347\232\204\345\272\217\345\210\227.md" "b/_posts/2022-02-01-Python\357\274\232Python \350\257\255\350\250\200\344\270\255\347\232\204\345\272\217\345\210\227.md" new file mode 100644 index 00000000000..e5e7043e0eb --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232Python \350\257\255\350\250\200\344\270\255\347\232\204\345\272\217\345\210\227.md" @@ -0,0 +1,182 @@ +--- +layout: post +title: Python:Python 语言中的序列 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. Python 序列分类 + +#### 1.1. 按存储数据类型分类 + +- 容器序列, + -- 定义:容器序列可以存放不同类型的数据,即可以存放任意类型对象的引用; + -- 包括:list,tuple,deque; +- 扁平序列 + -- 定义:扁平序列只能容纳一种类型,也就是说其存放的是值而不是引用,扁平序列其实是一段连续的内存空间,由此可见扁平序列其实更加紧凑。但是它里面只能存放诸如字符、字节和数值这种基础类型。 + -- 包括:str,bytes,bytearray,array.array; + +#### 1.2. 按是否可变分类 + +- 可变序列 + -- list,deque,bytearray,array; +- 不可变序列 + -- str,tuple,bytes; + +# 2. append & extend + +- append + -- append 对不同类型的序列进行新增操作,是把新增序列作为一个元素添入; +``` +>>> a = [1, 2, 3] +>>> b = (4, 5) +>>> +>>> a.append(b) +>>> print(a) +[1, 2, 3, (4, 5)] # 注意:(4, 5) 是一个元组 +``` +- extend + -- extend 对不同类型的序列进行新增操作,会先 for 循环遍历新增序列中的元素,然后把这些元素逐个添入; +``` +>>> a = [1, 2, 3] +>>> b = (4, 5) +>>> +>>> a.extend(b) +>>> print(a) +[1, 2, 3, 4, 5] # 注意:4, 5 作为单独的元素出现在新列表 +``` + +# 3. 序列切片 + +- 列表全部元素组成新列表 +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> +>>> print(sample_list[::]) +[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +``` +- 列表全部元素组成 **倒序** 新列表 +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> +>>> print(sample_list[::-1]) +[10, 9, 8, 7, 6, 5, 4, 3, 2, 1] +``` +- **奇数位** 元素组成新列表 +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> +>>> print(sample_list[::2]) # 2 为步长,表示每隔一位取一个元素 +[1, 3, 5, 7, 9] +``` +- **偶数位** 元素组成新列表 +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> +>>> print(sample_list[1::2]) +[2, 4, 6, 8, 10] +``` +- **指定起始结束位置** 截取列表 +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> +>>> print(sample_list[3:6]) +[4, 5, 6] +``` +- 切片 **结束位大于列表长度** +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> +>>> print(sample_list[0:100]) +[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +``` +- 切片 **初始位大于列表长度** +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> +>>> print(sample_list[100:]) +[] +``` +- **尾部增加** 新元素 +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> sample_list[len(sample_list):] = ["new"] +>>> print(sample_list) +[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'new'] +``` +- **头部增加** 新元素 +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> sample_list[:0] = ["new_1", "new_2"] +>>> print(sample_list) +['new_1', 'new_2', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +``` +- **指定位置** 增加元素 +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> sample_list[3:3] = ["new_1"] +>>> print(sample_list) +[1, 2, 3, 'new_1', 4, 5, 6, 7, 8, 9, 10] +``` +- 替换 **指定起始位置** 多位元素,= 号两边元素 **数量相同** +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> sample_list[:3] = ["new_1", "new_2", "new_3"] +>>> print(sample_list) +['new_1', 'new_2', 'new_3', 4, 5, 6, 7, 8, 9, 10] +``` +- 替换 **指定起始位置** 多位元素,= 号两边元素 **数量不同** +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> sample_list[:3] = ["new_1", "new_2"] # =号左侧 3 个元素,=号右侧 2 个元素 +>>> print(sample_list) +['new_1', 'new_2', 4, 5, 6, 7, 8, 9, 10] # 注意:3 没有了,但并没有被替换成新元素 +``` +- **奇数位** 替换 **同一元素**,= 号两边元素 **数量相同** +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> sample_list[::2] = [0] * 5 +>>> print(sample_list) +[0, 2, 0, 4, 0, 6, 0, 8, 0, 10] +``` +- **奇数位** 替换 **同一元素**,= 号两边元素 **数量不同**,会报错! +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> sample_list[::2] = [0] * 3 # = 号左侧 5 个元素,=号右侧 3 个元素 +Traceback (most recent call last): + File "", line 1, in +ValueError: attempt to assign sequence of size 3 to extended slice of size 5 +``` +- **奇数位** 替换 **不同元素**,= 号两边元素 **数量相同** +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> sample_list[::2] = ["a", "b", "c", "d", "e"] +>>> print(sample_list) +['a', 2, 'b', 4, 'c', 6, 'd', 8, 'e', 10] +``` +- **奇数位** 替换 **不同元素**,= 号两边元素 **数量不同** +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> sample_list[::2] = ["a", "b", "c"] # = 号左侧 5 个元素,=号右侧 3 个元素 +Traceback (most recent call last): + File "", line 1, in +ValueError: attempt to assign sequence of size 3 to extended slice of size 5 +``` +- **删除指定起始位置** 元素 +``` +>>> sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +>>> sample_list[:3] = [] # 删除前三位 +>>> print(sample_list) +[4, 5, 6, 7, 8, 9, 10] +``` + +# 4. array 数组 + +- 与 list 不同,array 只能存放指定的数据类型; +- array 的性能要比 list 高很多; diff --git "a/_posts/2022-02-01-Python\357\274\232Python \350\277\255\344\273\243\345\231\250\345\216\237\347\220\206\345\217\212\345\272\224\347\224\250\345\256\236\350\267\265.md" "b/_posts/2022-02-01-Python\357\274\232Python \350\277\255\344\273\243\345\231\250\345\216\237\347\220\206\345\217\212\345\272\224\347\224\250\345\256\236\350\267\265.md" new file mode 100644 index 00000000000..659ddc18b7b --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232Python \350\277\255\344\273\243\345\231\250\345\216\237\347\220\206\345\217\212\345\272\224\347\224\250\345\256\236\350\267\265.md" @@ -0,0 +1,339 @@ +--- +layout: post +title: Python:Python 迭代器原理及应用实践 +subtitle: 使用迭代器生成斐波那契数列 +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + +# 1. 什么是迭代器 + +- 迭代是Python最强大的功能之一,是访问集合元素的一种方式; +- 迭代器是一个可以记住当前遍历的位置的对象; +- 迭代器获基于 **迭代协议** 取值,背后是 ```__iter()__``` 方法; + +# 2. 迭代器如何工作 + +- 迭代器有两个基本的方法:```__iter()__``` 和 ```__next()__```; + -- 注意:如果仅仅实现 ```__iter()__``` 方法,只能通过 ```for``` 循环来进行迭代,而如果想要通过 ```next``` 方法迭代的话则需要使用 ```__next()__```方法: +- 迭代器对象从集合的第一个元素开始访问,每访问一个元素就记录当前位置,然后当下一次访问元素的时候,就从当前位置继续访问下一个,直到所有的元素被访问完结束; +- 当所有元素都被迭代出来以后,迭代器中没有更多的元素,```__next()__``` 动作会抛出 ```StopIteration``` 的错误; +- 下面的例子中,我们引入 ```iter()``` 这个内置的迭代函数做一个演示,```iter()``` 是 Python 内置的可迭代函数,可以将 list、set、str 等类型对象转化成可迭代对象; +``` +>>> list=[1,2,3,4] +>>> it = iter(list) # 创建迭代器对象 +>>> print (next(it)) # 输出迭代器的下一个元素 +1 +>>> print (next(it)) +2 +>>> print (next(it)) +3 +>>> print (next(it)) +4 +>>> +>>> print (next(it)) +Traceback (most recent call last): + File "", line 1, in +StopIteration # 迭代器中没有更多的元素时,next() 动作会抛出 **StopIteration** 的错误 +``` + +# 3. 迭代器与列表的区别 + +- 与 list 序列不同,迭代器不能按位置下标返回; + -- list 下标获取值,背后的原理是 ```__getitem__```,而迭代器获取值用到的 **迭代协议**,背后是 ```__iter__``` 方法。 +- 我们举一个例子: +>-- 有一只兔子,它在菜园里种了三十个胡萝卜,这只兔子每天都要吃一个胡萝卜,它如何为接下来的一个月准备口粮呢?分析一下我们发现,其实这只兔子有两种方法可以获取食物: +> +> 第一种方法,是直接准备一个月的食物,即把全部三十个胡萝卜拖进自己的洞里,然后每天吃一个,这种是常规方法,但是有个问题就是兔子洞不一定能放得下三十个胡萝卜; +> +>第二种方法,是每天从菜园里挖一个胡萝卜出来吃,每次吃完以后到记住胡萝卜坑的位置,第二天来的时候从这个萝卜坑的位置继续往下找,直到吃完三十个胡萝卜为止; +- 上面的例子中: + -- 第一种方法其实对应的就是列表存储数据的方式,简单易懂但问题是三十根胡萝卜太占地方,也就是我们说的 **占用内存空间**; + -- 第二种方法,其核心思想就是迭代器的设计思路,即迭代器中存储的不是元素数据,而是获取数据的方式(即数据存放的位置),每次获取一个数据然后记录位置,下一次获取数据的时候接着往下找; + +# 4. 如何构造一个迭代器 + +- 如下所示,```IteraleSample``` 类中内置了 ```__iter__```、```__next__``` 两个方法,所以后面的判断 ```isinstance(sample, Iterator)``` 返回的结果是 ```True```; +``` +from collections import Iterable, Iterator + + +class IteraleSample(object): + ''' + 创建一个类对象,并定义 __iter__、__next__ 两个方法 + ''' + def __init__(self): + pass + + def __iter__(self): + return self + + def __next__(self): + pass + +sample = IteraleSample() +print(isinstance(sample, Iterator)) +``` +- 打印结果如下: +```json +True +``` +# 5. __ iter __ 方法一定要 return 可迭代对象 + +- 需要指出的是,迭代器中 ```__iter__``` 方法一定要 ```return 一个可迭代对象```; + -- 上面例子 ```__iter__``` 方法 ```return``` 的 ```self```,表示的是 ```IteraleSample``` 类自己,这个类是可迭代的; +- 如果 ```__iter__``` 方法没有 return 可迭代对象的话,会怎么样呢? + -- 下面,我们构造了一个 ```WrongIteraleSample``` 类,该类的 ```__iter__``` 方法下只写一个 ```pass``` 占位符; +```json +# 错误的示例 + +from collections import Iterable, Iterator + + +class WrongIteraleSample(object): + ''' + 创建一个类对象,并定义 __iter__、__next__ 两个方法 + ''' + def __init__(self, max_num): + self.current = 0 + self.max_num = max_num + + def __iter__(self): + pass # 【注意】__iter__ 方法没有 return 可迭代对象,这种写法是【错误的】!!! + + def __next__(self): + # pass + if self.current < self.max_num: + self.current += 10 + return self.current + +sample = IteraleSample(5) +print(isinstance(sample, Iterator)) + +for num in sample: + print(num) +``` +- 运行后发现,尽管 ```isinstance(sample, Iterator)``` 的返回值也是 ```True```,但在实例化该类后执行 ```for num in sample: + print(num)``` 的时候,会报错 ```TypeError: iter() returned non-iterator of type 'NoneType'```; +```json +True +Traceback (most recent call last): + File "C:\Users\Administrator\Desktop\iterator_test.py", line 52, in + for num in sample: +TypeError: iter() returned non-iterator of type 'NoneType' +``` + +# 6. 使用迭代器构造斐波那契数列 +#### 6.1. 斐波那契数列简介 +-- 斐波那契数列,是有一系列整数组成的数列; +-- 这一数列任意位置上的数值与后一位相加,等于第三位数值; +-- 示例: +```json +0 , 1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , ... +``` +##6.2. 普通方法构造斐波那契数列 +- 普通方法一: + -- 如下面代码,使用递归构造斐波那契数列生成函数,并记录运行时间; +``` +def feb_1(index): + """ + 斐波那契数列生成函数 + """ + if index <= 2: + return 1 + elif index > 2 and index < 101: + return feb_1(index-1) + feb_1(index-2) + +def test_time(max_index): + """ + 测试上面 斐波那契数列函数的执行情况 + """ + starttime = datetime.datetime.now() # 记录开始时间 + print("最大值为 : {}\n结果值为:{}".format(max_index, feb_1(max_index))) + endtime = datetime.datetime.now() # 记录结束时间 + print("运行时间为:{}".format(endtime - starttime)) # 以秒为单位,计算运行时长 +``` +-- 方法缺陷: +(1)每次只能获取一个末位的数值,而不是整个数列; +(2)如果数值太大,运行时间会变得非常的长; +``` +print(test_time(10)) + +print(test_time(20)) + +print(test_time(30)) + +print(test_time(40)) + +最大值为 : 10 +结果值为:55 +运行时间为:0:00:00 +None + +最大值为 : 20 +结果值为:6765 +运行时间为:0:00:00.003998 +None + +最大值为 : 30 +结果值为:832040 +运行时间为:0:00:00.469920 +None + +最大值为 : 40 +结果值为:102334155 +运行时间为:0:00:57.805053 # 此处,max_index=40,运行时间就已经需要 57 秒,相比前面的时间花费增速是几何式的, +None + +``` +-- 如上面的例子,max_index = 30 的时候程序运行时常只需要 0:00:00.749871 秒,到了 max_index = 40 的时候就已经需要 0:00:57.805053 秒,这种耗时的增速在实际生产中是不能接受的; +- 普通方法二: +``` +def feb_2(max_index): + """ + 斐波那契数列生成函数 + """ + result_list = [] # 此处使用 list 作为一个容器存储生成的数值,如果数值足够大这个 list 将会占用大量的的内存 + n, a, b = 0, 0, 1 + while n < max_index: + result_list.append(b) + a,b = b, a+b + n += 1 + return "最大值为 : {}\n结果值为:{}".format(max_index, result_list) + +def test_time(max_index): + """ + 测试上面 斐波那契数列函数的执行情况 + """ + starttime = datetime.datetime.now() # 记录开始时间 + print(feb_2(max_index)) + endtime = datetime.datetime.now() # 记录结束时间 + print("运行时间为 : {}".format(endtime - starttime)) # 以秒为单位,计算运行时长 +``` +-- 该方法的优点:相比与前面的方法,程序运行耗费的时间相对较快; +``` +test_time(10) + +test_time(20) + +test_time(30) + +test_time(40) + +最大值为 : 10 +结果值为:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55] +运行时间为 : 0:00:00.001000 + +最大值为 : 20 +结果值为:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765] +运行时间为 : 0:00:00 + +最大值为 : 30 +结果值为:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040] +运行时间为 : 0:00:00 + +最大值为 : 40 +结果值为:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155] +运行时间为 : 0:00:00 +``` + +-- 该方法的缺陷:该方法使用 result_list = [] 作为一个容器存储生成的数值,如果数值足够大这个 list 将会占用大量内存空间,简言之:**如果数值过大,会占用大量内存**; +-- 有兴趣的小伙伴可以尝试 max_index=10000 时程序的执行情况,看一看程序内存占用情况; +-- 请注意:你的电脑可能会被卡死!!! +``` +test_time(10000) # 注意:你的电脑可能会被卡死 +``` +#### 6.3. 使用迭代器构造斐波那契数列 +- 如下,我们使用迭代器实现一个斐波那契数列: +``` +class Fibonacci(object): + def __init__(self, max_num): + self.max_num = max_num + self.current = 0 + self.a = 0 + self.b = 1 + + def __iter__(self): + ''' + 【注意】此处一定要 return self,否则会报错 TypeError: iter() returned non-iterator of type NoneType !!! + ''' + return self + + def __next__(self): + if self.current < self.max_num: + self.a, self.b = self.b, self.a + self.b + self.current += 1 + return self.a + else: + raise StopIteration + + +fib = Fibonacci(10) +for num in fib: + print(num) +``` +- 下面打印的结果正是我们想要的: +```json +1 +1 +2 +3 +5 +8 +13 +21 +34 +55 +``` +- 我们来测试其执行效率 +``` +import datetime + + +class Fibonacci(object): + def __init__(self, max_num): + self.max_num = max_num + self.current = 0 + self.a = 0 + self.b = 1 + + def __iter__(self): + return self + + def __next__(self): + if self.current < self.max_num: + self.a, self.b = self.b, self.a + self.b + self.current += 1 + return self.a + else: + raise StopIteration + + +def test_time(max_num): + """ + 测试上面 斐波那契数列函数的执行情况 + """ + starttime = datetime.datetime.now() + for i in Fibonacci(max_num): # 使用 for ... in ... 遍历打印所有的生成结果 + print(i) + endtime = datetime.datetime.now() + print("运行时间为 : {}".format(endtime - starttime)) + + +test_time(10) +test_time(100) +test_time(1000) +test_time(10000) +``` +- 运行耗时、内存占用都被大大缩减,下面的结果展示了运行时间结果,和前面的对比这种方法的效率是惊人的; +``` +# 运行的时间如下所示: + +运行时间为 : 0:00:00 +运行时间为 : 0:00:00.004000 +运行时间为 : 0:00:00.574901 +运行时间为 : 0:00:04.990133 diff --git "a/_posts/2022-02-01-Python\357\274\232Python \351\255\224\346\263\225\345\207\275\346\225\260.md" "b/_posts/2022-02-01-Python\357\274\232Python \351\255\224\346\263\225\345\207\275\346\225\260.md" new file mode 100644 index 00000000000..206ee3a056e --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232Python \351\255\224\346\263\225\345\207\275\346\225\260.md" @@ -0,0 +1,74 @@ +--- +layout: post +title: Python:Python 魔法函数 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. 简介 +#### 1.1. 什么是魔法方法 +- Python是一门追求简单、高效的语言,它并不像某些语言如Java、C++等,需要让类实现接口并逐一实现接口中的方法。Python采用了一种约定的机制,在基类中以特殊名称的方法、属性(类似 __ init __ 这样的属性和方法)来提供指定的功能; +- 在Python中有些方法名、属性名前后加上了双下划线,这种方法、属性通常属于Python类的特殊方法和属性,有的书中成为“魔法方法”; +#### 1.2. 调用方式 +- 隐式调用 + - 魔法方法是 **隐式调用** 的,在使用的时候不需要调用方法 +- 显示调用 + -- +# 2. 简单实例 +- __ init __(self, [...]) + - 类的初始化方法。它获取任何传给构造器的参数(比如我们调用 x = SomeClass(10, ‘foo’) , __init__ 就会接到参数 10 和 ‘foo’ ,__init__ 在Python的类定义中用的最多; +- __ getitem __(self, key) + - 定义对容器中某一项使用 self[key] 的方式进行读取操作时的行为。这也是可变和不可变容器类型都需要实现的一个方法; +- 示例: +``` +class Compalny(object): + # 类的初始化方法,获取参数 employee_list 这个列表 + def __init__(self, employee_list): + self.employee = employee_list + + # 对 self.employee 这个 list 使用 self[key] 的方式进行读取操作 + def __getitem__(self, item): + return self.employee[item] + +company = Compalny(["tom", "bob", "jane"]) + +# 遍历 company 中所有员工 +for em in company: + print(em) + +print('=' * 50) + +# 获取 company 中的第三个元素 +print(company[2]) +``` +# 3. 常用魔法方法 +- 常用魔法方法 + - __ init __:类实例化会触发; +- __ repr __ + - 和 __ init __(self) 的性质一样,Python 中每个类都包含 __ repr __() 方法,因为 object 类包含 __ reper __() 方法,而 Python 中所有的类都直接或间接继承自 object 类; + - 示例: +``` +>>> class User(): +... def __init__(self, name): +... self.name = name +... def __repr__(self): +... return "user with name : "+ self.name +... +>>> user = User(name="zhangsan") +>>> print(user) +user with name : zhangsan # 自动打印出 __repr__ 方法 return 的文字 +``` + +- __ str __:打印对象会触发; +- __ call __:对象()触发,类也是对象 类(),类的实例化过程调用元类的__call__; +- __ new __:在类实例化会触发,它比__init__早(造出裸体的人,__init__穿衣服); +- __ del __:del 对象,对象回收的时候触发; +- __ setattr __ , __ getattr __:(.拦截方法),当对象.属性--》赋值会调用setattr,如果是取值会调用getattr; +- __ getitem __ , __ setitem __:([]拦截); +- __ enter __ 和 __ exit __ 上下文管理器; \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232args \345\222\214 kargs \347\232\204\347\224\250\346\263\225\345\222\214\345\214\272\345\210\253.md" "b/_posts/2022-02-01-Python\357\274\232args \345\222\214 kargs \347\232\204\347\224\250\346\263\225\345\222\214\345\214\272\345\210\253.md" new file mode 100644 index 00000000000..726d1f26f25 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232args \345\222\214 kargs \347\232\204\347\224\250\346\263\225\345\222\214\345\214\272\345\210\253.md" @@ -0,0 +1,208 @@ +--- +layout: post +title: Python:*args 和 **kargs 的用法和区别 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. *args 和 **kargs 到底有什么用 + +>在 python 中,```*args``` 和 ```**kwargs``` 都用作向函数传递 **可变参数**。 +所谓 **可变**,其实就是参数的 **数量**、**位置**、**形式** 是不固定的。 +之所以使用可变参数,目的是为了在构造函数的时候,增加函数功能的灵活性,同时函数写法会更加优雅。 + +在编程工作中,经常需要传递很多参数到函数中,但这些参数 **有的时候需要用到、有时候则不需要用到**,甚至有时候参数根本不存在,最典型的例子就是 **构造装饰器**。 + +注意:如果看不懂可以跳过下面这一部分! + +下面是一个 ```timer``` 装饰器,用来计算并打印函数执行花费的时间,在构造参数的时候用到了 ```*args``` 和 ```**kwargs``` 两个参数。 +``` +def timer(func): +... def call_func(*args, **kwargs): +... print(f"计时开始 [{time.strftime('%Y-%m-%d %X', time.localtime())}]") +... start_time = time.time() +... try: +... func(*args, **kwargs) +... except KeyboardInterrupt: +... pass +... finally: +... end_time = time.time() +... total_time = end_time - start_time +... print("计时结束") +... print(f"程序用时{int(total_time // 60)}分{total_time % 60:.2f}秒") +... return call_func +... +@timer +... def test(arg_1, arg_2, arg_3): +... print("开始 执行 test 函数") +... time.sleep(3) +... print(f"打印参数 arg_1: {arg_1} arg_1: {arg_1} arg_1: {arg_1}") +... print("执行 test 函数 结束") +... +test(1, 2, 3) +计时开始 [2020-10-30 17:50:23] +开始 执行 test 函数 +打印参数 arg_1: 1 arg_1: 1 arg_1: 1 +执行 test 函数 结束 +计时结束 +程序用时0分3.01秒 + +``` +上面的装饰器之所以传递 **可变参数**,是因为装饰器需要装饰的函数的数量是不固定的,如果传递固定的参数,那么被装饰的函数在传参的时候会报错。 + +# 2. *args 和 **kargs 用法有什么区别 + +##2.1. 可变位置参数 *args +- python 中规定参数前带 ```*``` 的称为可变位置参数,只是我们通常称这个可变位置参数为 ```*args```,我们也可以写成 ```*any``` 等形式。 +- ```*args``` 是一个 **元组**,传入的参数会被放进元组里。 + +**场景一:** +- ```*args``` 将多个参数存储在 **元组** 类型的变量 ```args``` 当中,当我们在函数内部使用 ```args``` 获取参数的时候,会得到一个 **元组**。 +>注意:如果在函数内部使用 ```*args``` 获取参数, ```*args``` 得到的结果 **并不是元组**,而是分开的多个参数值,这与 ```args``` 获取的结果是不一样的。 +``` +>>> def func_args(arg_1, arg_2, *args): +... print(f"arg_1 : {arg_1}") +... print(f"arg_2 : {arg_2}") +... print(f"args : {args}") +... print(f"*args : {*args}") # 注意这里获取的是 *args +... +>>> func_args('a', 'b', 'c', 'd', 'e') +arg_1 : a +arg_2 : b +args : ('c', 'd', 'e') +*args : 'c', 'd', 'e' # 注意这里打印的 并不是元组 +>>> +``` +- 上面代码中,```arg_1```、```arg_2``` 是固定参数,传参的时候函数会将第一、二个参数按位置赋给 ```arg_1```、```arg_2```,就是代码中的 ```a```、```b```。 +- 而后面 ```c```、```d```、```e```,则一并存储为 **元祖** 传给 ```args```,所以才会在打印的结果中看到打印的返回值:```args : ('c', 'd', 'e')```。 + +**场景二:** +- 用于容器传参,即函数被调用时传入了一个容器(如 list),容器中包含多个参数,而这些参数分别对应函数的参数。 +``` +def func(arg1, arg2, arg3): +... print(arg1, arg2, arg3) +... +... arg_list=["second", 3] +... func(1, *arg_list) # 此处用 *arg_list 这个 list 传入 "second" 和 3 两个参数 +1 second 3 +``` +- 在上面的例子中,```func(1, *arg_list) ``` 其实就等效于 ```func(1, "second", 3) ```,这种写法其实并没有特别的作用,只是让代码看起来更优雅。 +>**注意**:用的时候注意,调用时 **传入的参数总量**(即容器外参数和容器内元素个数的总和),必须和 **构造函数所需的参数数量** 保持一致。以上面的例子为例,构造函数时 ```arg1, arg2, arg3``` 共计 **三个** 参数,调用函数时传入 ```1, "second", 3``` 加起来也是 **三个**。 如果数量不一样,传入的太多或者太少都会报错。 + +下面是一个例子: +``` + +... arg_list=["second", 3, 4, 5] # 传递的参数 太多 +... func(1, *arg_list) +Traceback (most recent call last): + File "", line 5, in +TypeError: func() takes 3 positional arguments but 5 were given +... +... arg_list=["second",] # 传递的参数 太少 +... func(1, *arg_list) +Traceback (most recent call last): + File "", line 6, in +TypeError: func() missing 1 required positional argument: 'arg3' +``` + +##2.2. 可变关键字参数 **kargs +- python 中规定参数前 带 ```**``` 的,称为可变关键字参数,通常用 ```**kwargs``` 表示。 +- ** kwargs:是一个字典,传入的参数以 **键值对** 的形式存放到字典里,相比较而言,*args 是一个 **元组**,到这里读者应该明白二者的区别了吧。 + +**场景一:** +- **kargs 实质就是将函数的参数和值,存储在 **字典类型** 的 kargs 变量中。 +``` +def func_kwargs(arg_1, **kwargs): +... print(f"第一个参数 arg_1 : {arg_1}") +... print(f"后面的参数 kwargs : {kwargs}") +... +func_kwargs('a', arg_2='b', arg_3='c', arg_4='d') # 注意参数格式一定写成 key=value 形式 +第一个参数 arg_1 : a +后面的参数 kwargs : {'arg_2': 'b', 'arg_3': 'c', 'arg_4': 'd'} +``` +在上面的例子中,``` arg_2='b', arg_3='c', arg_4='d'```的含义是:```arg_2、arg_3、arg_4``` 三个参数的值分别为 ```'b'、'c'、'd'```,在参数传入函数后,他们一起被构造成一个字典,即打印出的 ```kwargs : {'arg_2': 'b', 'arg_3': 'c', 'arg_4': 'd'}```。 + +>**注意**:传入的可变参数一定要写成 **```key=value```** 的形式,否则会报 **赋参太多** 错误。 + +详见下面的赋参太多错误的示例: +``` +def func_kwargs(arg_1, **kwargs): +... print(f"第一个参数 arg_1 : {arg_1}") +... print(f"后面的参数 kwargs : {kwargs}") +... +func_kwargs('a', 'b', 'c', 'd') # 直接传递参数 +Traceback (most recent call last): + File "", line 1, in +TypeError: func_kwargs() takes 1 positional argument but 4 were given +``` +**场景二:** +- **kargs的第二种使用场景,与 *args 第二种使用场景类似,也是用于传递参数到函数中,只不过区别是 *args 可以传入的是列表、元组等,而 **kargs 传入的是字典。 + +``` +def func(arg_1, arg_2=None, arg_3=None): +... print(arg_1, arg_2, arg_3) +... +arg_dic={"arg_2": "第二个参数的值", "arg_3": "第三个参数的值"} # 需要传入的字典 +func("第一个参数", **arg_dic) +第一个参数 第二个参数的值 第三个参数的值 +``` +在上面的例子中,```arg_dic={"arg_2": "第二个参数的值", "arg_3": "第三个参数的值"}``` 以字典形式传入函数 ```func```,在传入后自动匹配赋值: ```arg_2="第二个参数的值", arg_3="第三个参数的值"```。 +>**注意**:在构造函数的时候,参数 **不一定** 要写成 **```key=value```** 的形式,按照普通函数的形式来写,也能正常接收传入的字典形式的参数。 + +下面是普通的函数构造写法的例子: +``` +def func_1(arg_1, arg_2, arg_3): # 普通的函数参数写法 +... print(arg_1, arg_2, arg_3) +... +arg_dic={"arg_2": "第二个参数的值", "arg_3": "第三个参数的值"} +func_1("第一个参数", **arg_dic) +第一个参数 第二个参数的值 第三个参数的值 +``` +- 至于参数的数量,情况和 ```*args``` 一样,传入字典的元素数量和函数所需的参数数量需要保持一致,否则会报错。 + 下面是一个例子: +``` +def func(arg_1, arg_2=None, arg_3=None): +... print(arg_1, arg_2, arg_3) +... +arg_dic={"arg_2": "第二个参数的值", "arg_3": "第三个参数的值", "arg_4": "第四个参数的值"} +func("第一个参数", **arg_dic) +Traceback (most recent call last): + File "", line 1, in +TypeError: func() got an unexpected keyword argument 'arg_4' +``` + +# 3. *args 和 **kargs 一起使用 + +- 这里参考网上的帖子,给出了一个比较经典的例子。 +- 有兴趣的朋友可以好好看一下其中的细节,尤其是 **仔细看一下注释** 内容。 +``` +def func(arg_1, arg_2="第二个参数的值", *args, arg_3="第三个参数的值", arg_4, **kwargs): +... print(arg_1) # [0, 1, 2] +... print(arg_2) # 3 # 注意:这里的 3,对应构造函数的 arg_2,它是参数 *[3, 4, 5] 传进来的!!! +... print(args) # (4, 5) +... print(*args) # 4 5 # 注意:这里返回的并不是元组 +... print(arg_3) # 6 +... print(arg_4) # 7 +... print(kwargs) # {'key_1': '第一个字典参数', 'key_2': '第二个字典参数'} +... # print(**kargs) +... print(kwargs["key_1"]) # 第一个字典参数 +... print(kwargs["key_2"]) # 第二个字典参数 + +func([0, 1, 2], *[3, 4, 5], arg_3=6, arg_4=7, key_1="第一个字典参数", key_2="第二个字典参数") + +[0, 1, 2] +3 +(4, 5) +4 5 +6 +7 +{'key_1': '第一个字典参数', 'key_2': '第二个字典参数'} +第一个字典参数 +第二个字典参数 +``` \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232asyncio \346\246\202\345\277\265\345\222\214\347\224\250\346\263\225\350\257\246\350\247\243.md" "b/_posts/2022-02-01-Python\357\274\232asyncio \346\246\202\345\277\265\345\222\214\347\224\250\346\263\225\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..59b21eac7ce --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232asyncio \346\246\202\345\277\265\345\222\214\347\224\250\346\263\225\350\257\246\350\247\243.md" @@ -0,0 +1,503 @@ +--- +layout: post +title: Python:asyncio 概念和用法详解 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 0. 前言 +- 在执行一些 IO 密集型任务的时候,程序常常会因为等待 IO 而阻塞。比如在网络爬虫中,如果我们使用 requests 库来进行请求的话,如果网站响应速度过慢,程序一直在等待网站响应,最后导致其爬取效率是非常非常低的。 +- 为了解决这类问题,本文就来探讨一下 Python 中异步协程来加速的方法,此种方法对于 IO 密集型任务非常有效。如将其应用到网络爬虫中,爬取效率甚至可以成百倍地提升; +# 1. 理解协程 +#### 1.1. 协程定义 +- 协程,英文叫做 Coroutine,又称微线程,纤程,协程是一种用户态的轻量级线程; +- 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重入时,就相当于进入上一次调用的状态。 +- 协程本质上是个单进程,协程相对于多进程来说,无需线程上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单; +- 我们可以使用协程来实现异步操作,比如在网络爬虫场景下,我们发出一个请求之后,需要等待一定的时间才能得到响应,但其实在这个等待过程中,程序可以干许多其他的事情,等到响应得到之后才切换回来继续处理,这样可以充分利用 CPU 和其他资源,这就是异步协程的优势。 +- 其实在其他语言中,协程的其实是意义不大的多线程即可已解决I/O的问题,但是在python因为他有GIL(Global Interpreter Lock 全局解释器锁 )在同一时间只有一个线程在工作,所以:如果一个线程里面I/O操作特别多,协程就比较适用; +#### 1.2. 协程的优势 +- 无需线程上下文切换的开销; +- 无需原子操作锁定及同步的开销; +- 方便切换控制流,简化编程模型 +- 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。 +#### 1.3. 协程缺点 +- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用; +- 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序 + +# 3. 协程关键字 +#### 3.1. async +- **async def func(): ...** + - async 作为一个关键字放在函数的前面,表示该函数是一个异步函数; + - async 定义后的异步函数,在执行时不会阻塞后面代码的执行; + - 异步函数 func() 同样可以单独、或者在 class 类中被调用,与普通函数的调用方法是一样的; +- **async for** +- **async with** +- **task** + - 任务,它是对协程对象的进一步封装,包含了任务的各个状态; +- **future** + - 代表将来执行或没有执行的任务的结果,实际上和 task 没有本质区别。 +- **gather** + - gather 可以将任务分组 + +#### 3.2. await +- await 用来用来声明程序挂起,比如异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序; +- await 后面只能跟异步程序或有 __ await __ 属性的对象,否则会报错; +- 假设有两个异步函数 async a,async b,a 中的某一步有 await,当程序碰到关键字 await b() 后,异步程序挂起后去执行另一个异步 b 程序,就是从函数内部跳出去执行其他函数,当挂起条件消失后,不管 b 是否执行完,要马上从 b 程序中跳出来,回到原程序执行原来的操作; +- 如果 await 后面跟的 b 函数不是异步函数,那么操作就只能等b执行完再返回,无法在 b 执行的过程中返回。如果要在 b 执行完才返回,也就不需要用 await 关键字了,直接调用 b 函数就行,所以这就需要 await 后面跟的是异步函数了; +- 在一个异步函数中,可以不止一次挂起,也就是可以用多个 await; +- **await 的用法和 yield from 用法类似**,但是 await 后面只能跟 Awaitable 的对象(实现了 __await __ 魔法方法),而 yield from 后面可以跟生成器、协程等; +- await asyncio.sleep() + - 需要 sleep 的时候用 await asyncio.sleep(xx), time.sleep(xx) 是同步阻塞接口,不能用于协程中; +#### 3.3. asyncio +- **asyncio.get_event_loop()** + +#### 3.4. loop 事件循环 +- **loop.run_until_complete(asyncio.wait(tasks))** + +# 4. 协程用法 +# 4.1. 同步执行函数示例 +- 下面的代码是常规的同步执行代码,用于和异步执行方法作对比; +``` +import time + + +def timer(func): + """ + 定义一个装饰器,用于统计程序执行所花费的时间 + :param func: 被统计的函数 + :return: + """ + def call_func(*args, **kwargs): + print("计时开始") + start_time = time.time() + try: + func(*args, **kwargs) + except KeyboardInterrupt: + pass + finally: + end_time = time.time() + total_time = end_time - start_time + print("计时结束") + print(f"程序用时{int(total_time // 60)}分{total_time % 60:.2f}秒") + return call_func + + +def task(i): + print(f'task {i} start : {time.strftime("%X", time.localtime())}') + time.sleep(1) + print(f'task {i} finish : {time.strftime("%X", time.localtime())}') + + +@timer +def main(): + for i in range(5): + task(i) + + +if __name__ == '__main__': + main() +``` +- 执行结果 +``` +计时开始 +task 0 start : 10:59:58 +task 0 finish : 10:59:59 +task 1 start : 10:59:59 +task 1 finish : 11:00:00 +task 2 start : 11:00:00 +task 2 finish : 11:00:01 +task 3 start : 11:00:01 +task 3 finish : 11:00:02 +task 4 start : 11:00:02 +task 4 finish : 11:00:03 +计时结束 +程序用时0分5.03秒 +``` +#### 4.2. asyncio 用法示例 +- 下面的代码展示了 asyncio 的基础用法 +``` +import time +import asyncio + + +def timer(func): + """ + 定义一个装饰器,用于统计程序执行所花费的时间 + :param func: 被统计的函数 + :return: + """ + def call_func(*args, **kwargs): + print("计时开始") + start_time = time.time() + try: + func(*args, **kwargs) + except KeyboardInterrupt: + pass + finally: + end_time = time.time() + total_time = end_time - start_time + print("计时结束") + print(f"程序用时{int(total_time // 60)}分{total_time % 60:.2f}秒") + return call_func + + +async def task(i): + print(f'task {i} start : {time.strftime("%X", time.localtime())}') + await asyncio.sleep(1) # 异步函数中 需要 sleep 的时候用 await asyncio.sleep() + print(f'task {i} finish : {time.strftime("%X", time.localtime())}') + + +@timer +def run(): + loop = asyncio.get_event_loop() + # 这个 tasks 可以是不同的协程 + tasks = [task(i) for i in range(5)] + # asyncio.wait()函数会接收一个可迭代对象 + loop.run_until_complete(asyncio.wait(tasks)) + + +if __name__ =='__main__': + run() +``` +- 运行结果 + - 通过和前面的对比,我们发现 asyncio 所花的时间只有同步执行的 1/5,也可以说执行的效率要提升了 5 倍; +``` +计时开始 +task 4 start : 11:11:09 +task 2 start : 11:11:09 +task 0 start : 11:11:09 +task 1 start : 11:11:09 +task 3 start : 11:11:09 +task 4 finish : 11:11:10 +task 0 finish : 11:11:10 +task 3 finish : 11:11:10 +task 2 finish : 11:11:10 +task 1 finish : 11:11:10 +计时结束 +程序用时0分1.00秒 +``` + +- **使用 asyncio 注意事项** +>注意:做数据库驱动、网络请求驱动,一定要有对应的异步库,不能使用传统的阻塞接口; +例如,pymysql 不能满足 asyncio 异步需求,因为 pymysql 接口是阻塞的,而 asyncio 是单线程的,只要一个地方阻塞其他的都会被阻塞; + + +#### 3.4. asyncio 用法示例 +- run_until_complete() 用法 +``` + +import asyncio +import time + + +async def parse_list(page_url): + print(f"start parse list : {page_url}") + # 解析网页的过程此处省略 + await asyncio.sleep(2) # 注意:不能使用 time.sleep(),因为 time.sleep() 是同步阻塞接口,不能用于异步编程 + print(f"parse list finished : {page_url}") + + +def main(): + start_time = time.time() + loop = asyncio.get_event_loop() + page_urls = [f"https://static1.scrape.cuiqingcai.com/page/{page}" for page in range(0, 11)] + tasks = [parse_list(page_url) for page_url in page_urls] + loop.run_until_complete(asyncio.wait(tasks)) + end_time = time.time() + print("SPEND TIME : {}".format(end_time-start_time)) + + +if __name__ == '__main__': + main() + +``` +- run_forever() & task.cancel() 用法; +``` + +import asyncio +import time +from concurrent.futures import ThreadPoolExecutor + + +async def parse_list(page_url): + print(f"start parse list : {page_url}") + # 解析网页的过程此处省略 + await asyncio.sleep(2) # 注意:不能使用 time.sleep(),因为 time.sleep() 是同步阻塞接口,不能用于异步编程 + print(f"parse list finished : {page_url}") + parse_list_result = f"{page_url} result" + return parse_list_result + + +def main(): + start_time = time.time() + loop = asyncio.get_event_loop() + + tasks_1 = [parse_list(f"https://static1.scrape.cuiqingcai.com/page/{page}") for page in range(0, 11)] + tasks_2 = [parse_list(f"https://static1.scrape.cuiqingcai.com/page/{page}") for page in range(11, 21)] + tasks_3 = [parse_list(f"https://static1.scrape.cuiqingcai.com/page/{page}") for page in range(21, 31)] + + tasks = tasks_1 + tasks_2 + tasks_3 + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(asyncio.wait(tasks)) + except KeyboardInterrupt as e: + all_tasks = asyncio.Task.all_tasks() + for task in all_tasks: + task.cancel() + print(f"task {task} called") + loop.stop() + loop.run_forever() + finally: + loop.close() + + +if __name__ == '__main__': + main() + +``` +- ask.add_done_callback() & task.result() 用法 + - task.add_done_callback(call_back_func),添加回调函数; + - task.result(),获取协程的返回值; +``` +import asyncio +import time + + +async def parse_list(page_url): + print(f"start parse list : {page_url}") + # 解析网页的过程此处省略 + await asyncio.sleep(2) # 注意:不能使用 time.sleep(),因为 time.sleep() 是同步阻塞接口,不能用于异步编程 + print(f"parse list finished : {page_url}") + parse_list_result = f"{page_url} result" + return parse_list_result + + +def call_back_func(object): + parse_list_result = object.result() # object.result(),用于获取 parse_list 的返回值 + print(f"call back {parse_list_result}") + + +def main(): + start_time = time.time() + loop = asyncio.get_event_loop() + + page = 1 + page_url = f"https://static1.scrape.cuiqingcai.com/page/{page}" + coroutine = parse_list(page_url) + task = loop.create_task(coroutine) + # task = asyncio.ensure_future(coroutine) # 这种写法和上面一句作用是一样的 + + task.add_done_callback(call_back_func) # task.add_done_callback 用于添加回调函数,表示函数执行完毕后调用此函数 + loop.run_until_complete(task) + print(f"get parse list result : {task.result()}") + end_time = time.time() + print("SPEND TIME : {}".format(end_time-start_time)) + +if __name__ == '__main__': + main() + +``` + +- gather() 用法; +``` +import asyncio +import time +import concurrent + + +async def parse_list(page_url): + print(f"start parse list : {page_url}") + # 解析网页的过程此处省略 + await asyncio.sleep(2) # 注意:不能使用 time.sleep(),因为 time.sleep() 是同步阻塞接口,不能用于异步编程 + print(f"parse list finished : {page_url}") + parse_list_result = f"{page_url} result" + return parse_list_result + + +def main(): + start_time = time.time() + loop = asyncio.get_event_loop() + + tasks_1 = [parse_list(f"https://static1.scrape.cuiqingcai.com/page/{page}") for page in range(0, 11)] + tasks_2 = [parse_list(f"https://static1.scrape.cuiqingcai.com/page/{page}") for page in range(11, 21)] + tasks_3 = [parse_list(f"https://static1.scrape.cuiqingcai.com/page/{page}") for page in range(21, 31)] + + tasks_1 = asyncio.gather(*tasks_1) + tasks_2 = asyncio.gather(*tasks_2) + tasks_3 = asyncio.gather(*tasks_3) + + tasks_2.cancel() # 取消任务 + + # loop.run_until_complete(asyncio.gather(tasks_1, tasks_2, tasks_3)) # 直接 run 执行 task_2 时会报错,因为已经被取消 + + try: + loop.run_until_complete(asyncio.gather(tasks_1, tasks_2, tasks_3)) + except concurrent.futures._base.CancelledError as e: # task_2 执行报错会在这里被捕获 + print("task cancell") + + end_time = time.time() + print("SPEND TIME : {} ".format(end_time-start_time)) + + +if __name__ == '__main__': + main() + +``` +- 使用 ThreadPoolExecutor 在协程中集成阻塞 I/O; + - 在协程中集成阻塞 I/O,必须要用到 **loop.run_in_executor(None, func, args)**,其中 None 是用于存放线程(也可以是进程)的 ppol 池,func 是阻塞的函数,args 是阻塞函数的参数; +``` + +import asyncio +from concurrent.futures import ThreadPoolExecutor +import socket +from urllib.parse import urlparse + + +def parse_list(page_url): + print(f"start parse list : {page_url}") + # 解析网页的过程此处省略 + # asyncio.sleep(2) # asyncio.sleep 用于异步编程,此处为演示阻塞接口,所以没有使用 + time.sleep(2) # 注意:此处故意使用 time.sleep(),因为 time.sleep() 是同步阻塞接口, + print(f"parse list finished : {page_url}") + parse_list_result = f"{page_url} result" + return parse_list_result + + +def main(): + start_time = time.time() + loop = asyncio.get_event_loop() + executor = ThreadPoolExecutor() + tasks = [] + for page in range(1, 31): + page_url = f"http://shop.projectsedu.com/goods/{page}/" + task = loop.run_in_executor(executor, parse_list, page_url) + tasks.append(task) + loop.run_until_complete(asyncio.wait(tasks)) + end_time = time.time() + print("SPEND TIME : {}".format(end_time-start_time)) + + +if __name__ == '__main__': + main() + +``` +- asyncio 模拟 http 请求; +``` + +# 使用多线程:在携程中集成阻塞io +import asyncio +import socket +from urllib.parse import urlparse + + +async def get_url(url): + #通过socket请求html + url = urlparse(url) + host = url.netloc + path = url.path + if path == "": + path = "/" + + #建立socket连接 + reader, writer = await asyncio.open_connection(host,80) + writer.write("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8")) + all_lines = [] + async for raw_line in reader: + data = raw_line.decode("utf8") + all_lines.append(data) + html = "\n".join(all_lines) + return html + +async def main(): + tasks = [] + for url in range(20): + url = "http://shop.projectsedu.com/goods/{}/".format(url) + tasks.append(asyncio.ensure_future(get_url(url))) + for task in asyncio.as_completed(tasks): + result = await task + print(result) +# +if __name__ == "__main__": + import time + start_time = time.time() + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + print('last time:{}'.format(time.time()-start_time)) + +``` +- 使用 Semaphore 信号量同步机制限制并发量; +``` +from aiohttp import ClientSession +import asyncio + +###################### +# 限制协程并发量 +###################### +async def hello(sem, url): + async with sem: + async with ClientSession() as session: + async with session.get(f'http://localhost:8080/{url}') as response: + r = await response.read() + print(r) + await asyncio.sleep(1) + + +def main(): + loop = asyncio.get_event_loop() + tasks = [] + sem = asyncio.Semaphore(5) # this + for i in range(100000): + task = asyncio.ensure_future(hello(sem, i)) + tasks.append(task) + + + feature = asyncio.ensure_future(asyncio.gather(*tasks)) + loop.run_until_complete(feature) + +if __name__ == "__main__": + main() +``` +- asyncio 的 Semaphore 和 aiohttp 限制并发数量; +``` +import time +from aiohttp import ClientSession +import asyncio + + +URL = 'http://httpbin.org/get?a={}' + + +async def fetch_async(num): + async with ClientSession() as session: + async with session.get(URL.format(num)) as response: + data = await response.json() + return data['args']['a'] + + +async def print_result(num, semaphore): + async with semaphore: + r = await fetch_async(num) + print(f'fetch({num}) = {r}') + + +async def main(loop): + now = time.time() + semaphore = asyncio.Semaphore(3) + tasks = [print_result(num, semaphore) for num in range(10)] + await asyncio.wait(tasks) + print("总用时", time.time() - now) + + +if __name__ == '__main__': + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(main(loop)) + finally: + loop.close() +``` diff --git "a/_posts/2022-02-01-Python\357\274\232asyncio.wait \345\222\214 asyncio.gather \347\232\204\345\274\202\345\220\214.md" "b/_posts/2022-02-01-Python\357\274\232asyncio.wait \345\222\214 asyncio.gather \347\232\204\345\274\202\345\220\214.md" new file mode 100644 index 00000000000..b7d893faa11 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232asyncio.wait \345\222\214 asyncio.gather \347\232\204\345\274\202\345\220\214.md" @@ -0,0 +1,143 @@ +--- +layout: post +title: Python:asyncio.wait 和 asyncio.gather 的异同 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. 异同点综述 +- **相同**: + - 从功能上看,```asyncio.wait``` 和 ```asyncio.gather``` 实现的效果是相同的,都是把所有 Task 任务结果收集起来。 + +- **不同**: + - ```asyncio.wait``` 会返回两个值:```done``` 和 ```pending```,```done``` 为已完成的协程 ```Task```,```pending``` 为超时未完成的协程 ```Task```,需通过 ```future.result``` 调用 ```Task``` 的 ```result```; + - 而```asyncio.gather``` 返回的是所有已完成 ```Task``` 的 ```result```,不需要再进行调用或其他操作,就可以得到全部结果。 + +# 2. asyncio.wait 用法: +最常见的写法是:```await asyncio.wait(task_list)```。 + +``` +import asyncio +import arrow + + +def current_time(): + ''' + 获取当前时间 + :return: + ''' + cur_time = arrow.now().to('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss') + return cur_time + + +async def func(sleep_time): + func_name_suffix = sleep_time # 使用 sleep_time(函数 I/O 等待时长)作为函数名后缀,以区分任务对象 + print(f"[{current_time()}] 执行异步函数 {func.__name__}-{func_name_suffix}") + await asyncio.sleep(sleep_time) + print(f"[{current_time()}] 函数 {func.__name__}-{func_name_suffix} 执行完毕") + return f"【[{current_time()}] 得到函数 {func.__name__}-{func_name_suffix} 执行结果】" + + +async def run(): + task_list = [] + for i in range(5): + task = asyncio.create_task(async_func(i)) + task_list.append(task) + + done, pending = await asyncio.wait(task_list, timeout=None) + for done_task in done: + print((f"[{current_time()}] 得到执行结果 {done_task.result()}")) + +def main(): + loop = asyncio.get_event_loop() + loop.run_until_complete(run()) + + +if __name__ == '__main__': + main() +``` +代码执行结果如下: +```json +[2020-11-03 22:45:53] 执行异步函数 func-0 +[2020-11-03 22:45:53] 执行异步函数 func-1 +[2020-11-03 22:45:53] 执行异步函数 func-2 +[2020-11-03 22:45:53] 执行异步函数 func-3 +[2020-11-03 22:45:53] 执行异步函数 func-4 +[2020-11-03 22:45:53] 函数 func-0 执行完毕 +[2020-11-03 22:45:54] 函数 func-1 执行完毕 +[2020-11-03 22:45:55] 函数 func-2 执行完毕 +[2020-11-03 22:45:56] 函数 func-3 执行完毕 +[2020-11-03 22:45:57] 函数 func-4 执行完毕 +[2020-11-03 22:45:57] 得到执行结果 【[2020-11-03 22:45:57] 得到函数 func-4 执行结果】 +[2020-11-03 22:45:57] 得到执行结果 【[2020-11-03 22:45:55] 得到函数 func-2 执行结果】 +[2020-11-03 22:45:57] 得到执行结果 【[2020-11-03 22:45:53] 得到函数 func-0 执行结果】 +[2020-11-03 22:45:57] 得到执行结果 【[2020-11-03 22:45:56] 得到函数 func-3 执行结果】 +[2020-11-03 22:45:57] 得到执行结果 【[2020-11-03 22:45:54] 得到函数 func-1 执行结果】 +``` +# 3. asyncio.gather 用法: +最常见的用法是:```await asyncio.gather(*task_list)```,注意这里 ```task_list``` 前面有一个 ```*```。 +``` +import asyncio +import arrow + + +def current_time(): + ''' + 获取当前时间 + :return: + ''' + cur_time = arrow.now().to('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss') + return cur_time + + +async def func(sleep_time): + func_name_suffix = sleep_time # 使用 sleep_time(函数 I/O 等待时长)作为函数名后缀,以区分任务对象 + print(f"[{current_time()}] 执行异步函数 {func.__name__}-{func_name_suffix}") + await asyncio.sleep(sleep_time) + print(f"[{current_time()}] 函数 {func.__name__}-{func_name_suffix} 执行完毕") + return f"【[{current_time()}] 得到函数 {func.__name__}-{func_name_suffix} 执行结果】" + + +async def run(): + task_list = [] + for i in range(5): + task = asyncio.create_task(func(i)) + task_list.append(task) + results = await asyncio.gather(*task_list) + for result in results: + print((f"[{current_time()}] 得到执行结果 {result}")) + + +def main(): + loop = asyncio.get_event_loop() + loop.run_until_complete(run()) + + +if __name__ == '__main__': + main() + +``` +代码执行结果如下: +```json +[2020-11-03 22:35:54] 执行异步函数 func-0 +[2020-11-03 22:35:54] 执行异步函数 func-1 +[2020-11-03 22:35:54] 执行异步函数 func-2 +[2020-11-03 22:35:54] 执行异步函数 func-3 +[2020-11-03 22:35:54] 执行异步函数 func-4 +[2020-11-03 22:35:54] 函数 func-0 执行完毕 +[2020-11-03 22:35:55] 函数 func-1 执行完毕 +[2020-11-03 22:35:56] 函数 func-2 执行完毕 +[2020-11-03 22:35:57] 函数 func-3 执行完毕 +[2020-11-03 22:35:58] 函数 func-4 执行完毕 +[2020-11-03 22:35:58] 得到执行结果 【[2020-11-03 22:35:54] 得到函数 func-0 执行结果】 +[2020-11-03 22:35:58] 得到执行结果 【[2020-11-03 22:35:55] 得到函数 func-1 执行结果】 +[2020-11-03 22:35:58] 得到执行结果 【[2020-11-03 22:35:56] 得到函数 func-2 执行结果】 +[2020-11-03 22:35:58] 得到执行结果 【[2020-11-03 22:35:57] 得到函数 func-3 执行结果】 +[2020-11-03 22:35:58] 得到执行结果 【[2020-11-03 22:35:58] 得到函数 func-4 执行结果】 +``` \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232yield \351\253\230\347\272\247\347\224\250\346\263\225 send\343\200\201yiled from.md" "b/_posts/2022-02-01-Python\357\274\232yield \351\253\230\347\272\247\347\224\250\346\263\225 send\343\200\201yiled from.md" new file mode 100644 index 00000000000..97ed348e659 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232yield \351\253\230\347\272\247\347\224\250\346\263\225 send\343\200\201yiled from.md" @@ -0,0 +1,120 @@ +--- +layout: post +title: Python:yield 高级用法 send、yiled from +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. yield 中 send 用法 +- send 作用 + -- 生成器函数返回值给调用方; + -- 调用方通过 send 传入值给生成器函数; +- 示例: + +``` +>>> def name_func(): +... yield_result = 0 +... while 1: +... yield_result += 1 +... send_value = yield yield_result # 这一句是关键!!! +... print(f"func has been sent value : {send_value}") +... +>>> +>>> gen = name_func() +>>> # print(next(gen)) +... print("yield_result : ", gen.send(None)) # 第一次调用, send 传入 None +yield_result : 1 +>>> print("yield_result : ", gen.send("lavra")) +func has been sent value : lavra +yield_result : 2 +>>> print("yield_result : ", gen.send("hiton")) +func has been sent value : hiton +yield_result : 3 +``` +> 上面的代码示例中,最关键的一句是:send_value = yield yield_result,其中: +-- send_value 是被 send 传入函数的值; +-- yield_result 是返回给调用方 gen 的值; +- next 和 send 的区别: + -- next 只能取值; + -- send 能取本次yield的返回值,也能给上次yield传入值; + +- send 注意点: + -- 第一个 send 不能给 yield 传值,必须要 send None,这一步不能省! + +- 应用示例 +``` +>>> def url_repository(): +... url_list = [] +... while True: +... # _url 是 send 发送给 yield 的参数值,在接下来的语句中使用, +... _url = yield url_list # url_list是 yield 的返回值 +... url_list.append(_url) +... print(f"ADD URL : {_url}") +... +>>> +>>> g = url_repository() # 实例化生成器函数 +>>> g.send(None) # 第一次调用, send 传入 None +[] +>>> print("URL REPO : ", g.send('url_1')) # g.send() 返回 url_list +ADD URL : url_1 +URL REPO : ['url_1'] # 打印 yield 返回的 url_list +>>> print("URL REPO : ", g.send('url_2')) +ADD URL : url_2 +URL REPO : ['url_1', 'url_2'] +>>> print("URL REPO : ", g.send('url_3')) +ADD URL : url_3 +URL REPO : ['url_1', 'url_2', 'url_3'] +``` +# 2. yiled from 用法 +- 下面是一个 yiled 生成器: +``` +>>> def func(): +... for num in range(0, 5): +... yield num +... +>>> +>>> g = func() +>>> print(next(g)) +0 +>>> print(next(g)) +1 +>>> print(next(g)) +2 +>>> print(next(g)) +3 +>>> print(next(g)) +4 +>>> print(next(g)) +Traceback (most recent call last): + File "", line 1, in +StopIteration +``` + +- 我们可以将其改成如下代码: +``` +>>> def func(): +... yield from range(5) +... +>>> +>>> g = func() +>>> print(next(g)) +0 +>>> print(next(g)) +1 +>>> print(next(g)) +2 +>>> print(next(g)) +3 +>>> print(next(g)) +4 +>>> print(next(g)) +Traceback (most recent call last): + File "", line 1, in +StopIteration +``` diff --git "a/_posts/2022-02-01-Python\357\274\232\344\275\277\347\224\250 configparser \346\250\241\345\235\227\346\223\215\344\275\234 conf \351\205\215\347\275\256\346\226\207\344\273\266.md" "b/_posts/2022-02-01-Python\357\274\232\344\275\277\347\224\250 configparser \346\250\241\345\235\227\346\223\215\344\275\234 conf \351\205\215\347\275\256\346\226\207\344\273\266.md" new file mode 100644 index 00000000000..3b901e73277 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232\344\275\277\347\224\250 configparser \346\250\241\345\235\227\346\223\215\344\275\234 conf \351\205\215\347\275\256\346\226\207\344\273\266.md" @@ -0,0 +1,68 @@ +--- +layout: post +title: Python:使用 configparser 模块操作 conf 配置文件 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. configparser + +- configparser 模块在 python 中是用来读取配置文件; +- configparser 配置文件可以包含一个或多个节(section),每个节可以有多个参数(键=值); +- configparser 配置文件通常以 .conf 为后缀,作为单独文件进行保存; + +# 2. 示例 + +- 下面为 unitConf.conf 配置文件示例: + +``` + +[db] +db_host = localhost +db_port = 3306 +db_user = xxx +db_passwd = xxxxxx +db_name = xxx + +[concurrent] +thread= 10 + +``` +- 下面为 .py 程序获取配置的示例: +``` +import configparser + + +cf = configparser.ConfigParser() +cf.read("unitConf.conf") + +# return all section +secs = cf.sections() +print('sections:', secs) # sections: ['db', 'concurrent'] + +opts = cf.options("db") +print('options:', opts) # options: ['db_host', 'db_port', 'db_user', 'db_passwd'] + +kvs = cf.items("db") +print('db:', kvs) # db: [('db_host', 'localhost'), ('db_port', '3306'), ('db_user', 'xxx'), ('db_passwd', 'xxxxxx')] + +# read by type +host = cf.get("db", "db_host") +port = cf.get("db", "db_port") +dbname = cf.get("db", "db_name") +usernm = cf.get("db", "db_user") +passwd = cf.get("db", "db_passwd") + +print(host) # localhost +print(port) # 3306 +print(dbname) # xxx +print(usernm) # xxx +print(passwd) # xxxxxx + +``` \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232\344\275\277\347\224\250\345\205\203\347\261\273\345\256\236\347\216\260 orm & sqlalchemy \350\277\236\346\216\245\346\261\240.md" "b/_posts/2022-02-01-Python\357\274\232\344\275\277\347\224\250\345\205\203\347\261\273\345\256\236\347\216\260 orm & sqlalchemy \350\277\236\346\216\245\346\261\240.md" new file mode 100644 index 00000000000..ac50a65462c --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232\344\275\277\347\224\250\345\205\203\347\261\273\345\256\236\347\216\260 orm & sqlalchemy \350\277\236\346\216\245\346\261\240.md" @@ -0,0 +1,276 @@ +--- +layout: post +title: Python:使用元类实现 orm & sqlalchemy 连接池 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. orm +- 什么是 orm + - [对象关系映射](http://www.itisedu.com/phrase/200603051342455.html)([Object Relational Mapping](http://www.itisedu.com/phrase/200604231312415.html),简称[ORM](http://www.itisedu.com/phrase/200604231312115.html))是一种为了解决[面向对象](http://www.itisedu.com/phrase/200603101726185.html)与关系[数据库](http://www.itisedu.com/phrase/200602271218062.html)存在的互不匹配的现象的技术,简单的说 ORM 是通过使用描述[对象](http://www.itisedu.com/phrase/200603090845215.html)和数据库之间映射的[元数据](http://www.itisedu.com/phrase/200603141328355.html),将程序中的对象自动持久化到关系数据库中,本质上就是将数据从一种形式转换到另外一种形式,这也同时暗示者额外的执行开销; + - ORM 的全程是对象关系映射,这种思想的要点在于要把后台对数据库的各种操作如增删改查中的一些步骤给抽象出来,把数据表映射成一个类,表中的行给作为一个实例,行中的每个字段作为属性,这样以来以前对数据库插入一行数据的操作、就可以简化为类似下面代码: +``` +user=User(id="100001",name="Andy",password="*****") +user.save() //保存到数据库 +``` +- 简单理解:**表格就是类,字段名就是属性,一行记录就是一个对象;** +- orm 的优势是什么 + - ORM将数据与SQL独立开来,让使用时只需要传递数据,调用方法,而不去一遍一遍的拼接sql语句,如果ORM作为一种[中间件](http://www.itisedu.com/phrase/200604241155005.html)实现,则会有很多机会做优化,更重要的是用于控制转换的元数据需要提供和管理,但是同样这些花费要比维护手写的方案要少;而且就算是遵守ODMG规范的对象数据库依然需要[类](http://www.itisedu.com/phrase/200603090857555.html)级别的元数据; +- 为什么需要 orm + - ORM,是随着面向对象的[软件开发](http://www.itisedu.com/phrase/200603282233345.html)方法发展而产生的,面向对象的开发方法是当今企业级应用开发环境中的主流开发方法,关系数据库是企业级应用环境中永久存放数据的主流数据存储系统; + - 对象和关系数据是[业务实体](http://www.itisedu.com/phrase/200603101513085.html)的两种表现形式,业务实体在内存中表现为对象,在数据库中表现为关系数据。内存中的对象之间存在关联和继承关系,而在数据库中,关系数据无法直接表达多对多关联和继承关系,ORM 一般以中间件的形式存在,主要实现程序对象到关系数据库数据的映射; +- 示例: + - 下面的代码,演示了如何实现 orm: +``` + +class ModelMetaclass(type): + """ + 元类,被 Model 类继承,提供最基本的 + """ + def __new__(cls, name, bases, attrs): + """ + :param name: 子类的名称,继承该元类的子类的名称; + :param bases: 父类,以tuple的形式传入,没有也要传入控tuple:(); + :param attrs: 绑定的方法或属性,以dict的形式传入; + :return: + """ + mappings = dict() + # 判断是否需要保存 + for key, value in attrs.items(): + # 判断是否是指定的字符串或者数字的实例对象 + if isinstance(value, tuple): + mappings[key] = value + + for key in mappings.keys(): + attrs.pop(key) + + attrs["__mappings__"] = mappings + attrs["__table__"] = name.lower() + return type.__new__(cls, name, bases, attrs) + + +class Model(object, metaclass=ModelMetaclass): + """ + Model 类,被 Field 类(如本文中的 User 类)继承 + """ + def __init__(self, **kwargs): + for name, value in kwargs.items(): + setattr(self, name, value) + + def save(self): + fileds = [] + args = [] + for key, value in self.__mappings__.items(): + fileds.append(value[0]) + args.append(getattr(self, key, None)) + + args_temp = list() + for temp in args: + if isinstance(temp, int): + args_temp.append(str(temp)) + elif isinstance(temp, str): + args_temp.append(f"""'{temp}'""") + sql = f"""INSERT INTO {self.__table__} ({','.join(fileds)}) VALUES ({','.join(args_temp)})""" + print("SQL : ", sql) + + +class User(Model): + user_id = ("user_id", "int usigined") # 类属性,传入元类 attrs + user_name = ("username", "varchar(30)") # 类属性,传入元类 attrs + user_email = ("email", "varchar(30)") # 类属性,传入元类 attrs + + +user = User(user_id=123, user_name="pan", user_email="xxx@xx.com") +user.save() + +``` +- sqlalchemy + +![1431912884-5a5b6a87e85dc_articlex.png](https://upload-images.jianshu.io/upload_images/14502986-55e004a93977cc5d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +- 创建 database; + - 在 MySQL 中创建 databse user ; +``` +mysql> create database userdb; +Query OK, 1 row affected (0.00 sec) +``` +- 创建 **配置文件 unitConf.conf**: +``` + +[db] +db_host = localhost +db_port = 3306 +db_user = root +db_passwd = Linux423 +db_name = userdb + +[concurrent] +thread= 10 + + +``` +- 使用 sqlalchemy 连接 mysql: +``` + +from sqlalchemy import exists, Column, Integer, String, ForeignKey, DateTime, Text, func +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import scoped_session +import configparser + + +cf = configparser.ConfigParser() +cf.read("unitConf.conf") +# 从配置文件中获取数据库信息 +host = cf.get("db", "db_host") +port = cf.get("db", "db_port") +dbname = cf.get("db", "db_name") +usernm = cf.get("db", "db_user") +passwd = cf.get("db", "db_passwd") + +# 连接数据库 +engine_str = "mysql+mysqlconnector://{0}:{1}@{2}:{3}/{4}?auth_plugin=mysql_native_password".format( + usernm, passwd, host, port, dbname) +engine = create_engine( + engine_str, + encoding='utf-8', + pool_size=10, # 连接池中,保持的连接数 + max_overflow=5 # 当连接数已达到10 且都被使用时,max_overflow就是允许再新建的连接数 +) + +# 创建DBSession类型 +session = sessionmaker(bind=engine) +# declarative_base 函数实例化一个数据库表的基类,之后所有的数据库表都要继承这个基类。 +Base = declarative_base() # + + +# 必须继承前面由 declaraive_base 得到的 Base 基类,Base 会通过引擎初始化数据库结构,不继承Base就没有办法连接数据库 +class Users(Base): + # 必须要有__tablename__来指出这个类对应什么表,这个表可以暂时在库中不存在,SQLAlchemy 会帮我们创建这个表 + __tablename__ = "user" + + id = Column( # Column 类创建一个字段 + Integer, + autoincrement=True, + primary_key=True + ) + user_no = Column( + String(20), # user_no 员工编号额外生成,作为员工的身份识别编号,每一个编号都是唯一的 + nullable=False, # nullable 就是决定是否 not null, + unique=True, # unique 就是决定是否 unique,员工的身份识别编号是唯一的,不能重复 + index=True, # 设置 index 可以让系统自动根据这个字段为基础建立索引 + comment='用户编号' + ) + name = Column( + String(20), + nullable=False, # 姓名不能为空 + comment='姓名' + ) + gender = Column( + Integer, # 1 表示男,0 表示 女 + nullable=False, # 性别不能为空 + comment='性别' + ) + age = Column( + Integer, + nullable=False, # 年龄不能为空 + comment = '年龄' + ) + dept = Column( + String(20), + nullable=False, # 部门不能为空 + comment='部门' + ) + createtime = Column( + DateTime, + server_default=func.now(), # 系统默认时间 + comment='创建时间' + ) + updatetime = Column( + DateTime, + server_default=func.now(), # 系统默认时间 + onupdate=func.now(), # onupdate 用于记录【更新时间】 + comment='修改时间' + ) + + def __repr__(self): + return " {} : {}".format(self.Sname, self.Sno) + + +def create_table(): + """ + 创建表 + :return: + """ + Base.metadata.create_all(engine) + +def delete_table(): + """ + 删除表 + """ + Base.metadata.drop_all(engine) + +``` + +- 创建表 + +- 从上面代码调用 create_table() 方法; +``` +create_table() +``` +- 查看 mysql 中 user 表结构; +``` +mysql> use userdb; +Database changed +mysql> desc user; ++- - - - - - +- - - - - - -+- - - +- - -+- - - - - - - - - -+- - - - - - - - - -+ +| Field | Type | Null | Key | Default | Extra | ++- - - - - - +- - - - - - -+- - - +- - -+- - - - - - - - - -+- - - - - - - - - -+ +| id | int | NO | PRI | NULL | auto_increment | +| user_no | varchar(20) | NO | UNI | NULL | | +| name | varchar(20) | NO | | NULL | | +| gender | varchar(2) | NO | | NULL | | +| age | int | NO | | NULL | | +| dept | varchar(20) | NO | | NULL | | +| createtime | datetime | YES | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | +| updatetime | datetime | YES | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | ++- - - - - - +- - - - - - -+- - - +- - -+- - - - - - - - - -+- - - - - - - - - -+ +8 rows in set (0.00 sec) +mysql> +``` +- user 表创建成功; + +- 删除表 + +- 从上面代码调用 delete_table() 方法; +``` +# 删除表 +delete_table() +``` +- 在 mysql 中查看 user 表 +``` +mysql> desc user; +ERROR 1146 (42S02): Table 'userdb.user' doesn't exist +``` +- +- pool_size + 设置连接池中,保持的连接数。初始化时,并不产生连接。只有慢慢需要连接时,才会产生连接。例如我们的连接数设置成pool_size=10。如果我们的并发量一直最高是5。那么我们的连接池里的连接数也就是5。当我们有一次并发量达到了10。以后并发量虽然下去了,连接池中也会保持10个连接。 + +- max_overflow + 当连接池里的连接数已达到,pool_size时,且都被使用时。又要求从连接池里获取连接时,max_overflow就是允许再新建的连接数。 + 例如pool_size=10,max_overlfow=5。当我们的并发量达到12时,当第11个并发到来后,就会去再建一个连接,第12个同样。当第11个连接处理完回收后,若没有在等待进程获取连接,这个连接将会被立即释放。 + +- pool_timeout + 从连接池里获取连接,如果此时无空闲的连接。且连接数已经到达了pool_size+max_overflow。此时获取连接的进程会等待pool_timeout秒。如果超过这个时间,还没有获得将会抛出异常。sqlalchemy默认30秒 + +- pool_recycle + 这个指,一个数据库连接的生存时间。例如pool_recycle=3600。也就是当这个连接产生1小时后,再获得这个连接时,会丢弃这个连接,重新创建一个新的连接。 + 当pool_recycle设置为-1时,也就是连接池不会主动丢弃这个连接。永久可用。但是有可能数据库server设置了连接超时时间。例如mysql,设置的有wait_timeout默认为28800,8小时。当连接空闲8小时时会自动断开。8小时后再用这个连接也会被重置。 diff --git "a/_posts/2022-02-01-Python\357\274\232\345\212\250\346\200\201\345\261\236\346\200\247 property & setter \344\273\245\345\217\212 __getattr__ \345\261\236\346\200\247.md" "b/_posts/2022-02-01-Python\357\274\232\345\212\250\346\200\201\345\261\236\346\200\247 property & setter \344\273\245\345\217\212 __getattr__ \345\261\236\346\200\247.md" new file mode 100644 index 00000000000..b0684327516 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232\345\212\250\346\200\201\345\261\236\346\200\247 property & setter \344\273\245\345\217\212 __getattr__ \345\261\236\346\200\247.md" @@ -0,0 +1,145 @@ +--- +layout: post +title: Python:动态属性 property & setter 以及 __getattr__ 属性 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. property +- 引言: + - 假设有这样一个需求,我们需要创建一个 User 类,并初始化 birthday 参数,之后根据 birthday 计算得到年龄; + - 我们设计下面的代码实现该需求: +``` +>>> from datetime import date, datetime +>>> +>>> +>>> class User(): +... def __init__(self, nbirthday): +... self.birthday = birthday +... def get_age(self): +... return datetime.now().year - self.birthday.year +... +>>> +>>> user = User(date(year=1990, month=3, day=5)) +>>> age = user.get_age() +>>> print(age) +30 +``` +- 上面的代码中使用 user 实例调用 get_age() 方法获取 age,但是这种方法并不是常规方法,我们习惯用类似于 user.age 这样的方式获取 age; +- property + +- 于是我们引入 property 属性,将 age() 方法变为实例的一个 age 属性,这样就可以通过 user.age 直接调用,代码如下: +``` +>>> from datetime import date, datetime +>>> +>>> +>>> class User(): +... def __init__(self, birthday): +... self.birthday = birthday +... @property +... def age(self): +... return datetime.now().year - self.birthday.year +... +>>> +>>> user = User(date(year=1990, month=3, day=5)) +>>> print(user.age) +30 +``` +# 2. setter +- 引言 + - 假设有这样一个需求,我们需要创建一个 User 类,初始化属性 age 默认为 0,之后改变 age 的值,并获取该值; + - 我们设计下面的代码实现该需求: +``` +>>> class User(): +... def __init__(self): +... self.age = 0 +... def set_age(self, age): +... self.age = age +... +>>> +>>> user = User("zhang") +>>> user.set_age(30) +>>> print(user.age) +30 +``` +- 上面的代码中使用 user 实例调用 set_age() 方法赋值于 age,但是这种方法并不是常规方法,我们习惯用类似于 user.age = xxx 这样的方式赋值 age; +- setter + +- 于是我们引入 setter 属性,这样就可以通过 user.age 直接赋值,代码如下: +``` +>>> class User(): +... def __init__(self,): +... self._age = 0 +... @ property +... def age(self): +... return self._age +... @age.setter +... def age(self, age_value): +... self._age = age_value +... +>>> +>>> user = User() +>>> user.age = 30 +>>> print(user.age) +30 +``` + +# 3. __ getattr __ +- 引言 + - 在开发过程中,我们经常需要设计并获取类的实例对象的属性,但有时会因为疏忽遗漏没有为类设计某个属性,这就导致获取属性值的实收出现报错,故而造成程序异常甚至崩溃; + - 如下面的代码: +``` +>>> class User(): +... def __init__(self, name): +... self.name = name +... +>>> +>>> user = User(name="zhangsan") +>>> print(user.age) +Traceback (most recent call last): + File "", line 1, in +AttributeError: 'User' object has no attribute 'age' +``` +- __ getattr __ **处理 object has no attribute 报错** + - 如上面的代码,User 类并没有 age 属性,所以在实例 user 获取 age 属性时遇到报错,这个时候需要用到 __ getattr __ 解决此问题; + +``` +>>> class User(): +... def __init__(self, name): +... self.name = name +... def __getattr__(self, item): # 注意:此处 item 不能少 +... pass +... +>>> +>>> user = User(name="zhangsan") +>>> print(user.age) +None +``` +- 在上面的代码中,User 类并没有 age 属性,所以在实例 user 获取 age 属性时并未报错,而只是反悔了一个 None,这就有效避免了报错; +- __ getattr __ **获取 dict 类型属性的 value 值** + - 除了上面的用法,__ getattr __ 还可以用来更好的获取 dict 类型属性的 value 值; + - 比如我们初始化类时给与其一个 dict 字典属性参数,我们希望可以通过 实例. 属性(比如 user.age)的方式直接获取 dict 字典中 key 为 age 的 value 值; + - 以下面的代码为例: +``` +>>> class User(): +... def __init__(self, info={}): +... self.info = info +... def __getattr__(self, item): +... if not item in self.info.keys(): +... return None +... return self.info[item] +... +>>> +>>> user = User({"name":"zhangsan", "age":23}) +>>> print(user.age) +23 +>>> print(user.gender) +None +``` +- 代码中 user.age 的方式直接获取初始化参数 dict 字典中 key 为 age 的 value 值 23,同时 dict 中并未有 key 为 gender 的键值对,user.gender 直接返回 None 而并未出现报错; \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232\345\215\217\347\250\213\344\270\255 Task \345\222\214 Future \347\232\204\347\220\206\350\247\243\345\217\212\344\275\277\347\224\250.md" "b/_posts/2022-02-01-Python\357\274\232\345\215\217\347\250\213\344\270\255 Task \345\222\214 Future \347\232\204\347\220\206\350\247\243\345\217\212\344\275\277\347\224\250.md" new file mode 100644 index 00000000000..6080ad88393 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232\345\215\217\347\250\213\344\270\255 Task \345\222\214 Future \347\232\204\347\220\206\350\247\243\345\217\212\344\275\277\347\224\250.md" @@ -0,0 +1,371 @@ +--- +layout: post +title: Python:协程中 Task 和 Future 的理解及使用 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. Task 概念及用法 +- Task,是 python 中与事件循环进行交互的一种主要方式。 + 创建 Task,意思就是把协程封装成 Task 实例,并追踪协程的 **运行 / 完成状态**,用于未来获取协程的结果。 +- Task 核心作用:**在事件循环中添加多个并发任务**; + 具体来说,是通过 ```asyncio.create_task()``` 创建 Task,让协程对象加入时事件循环中,等待被调度执行。 +>注意:Python 3.7 以后的版本支持 ```asyncio.create_task()```,在此之前的写法为 ```loop.create_task()```,开发过程中需要注意代码写法对不同版本 python 的兼容性。 +- 需要指出的是,协程封装为 Task 后不会立马启动,当某个代码 ```await``` 这个 Task 的时候才会被执行。 +>当多个 Task 被加入一个 task_list 的时候,添加 Task 的过程中 Task 不会执行,必须要用 ```await asyncio.wait()``` 或 ```await asyncio.gather()``` 将 Task 对象加入事件循环中异步执行。 +- 一般在开发中,常用的写法是这样的: + -- 先创建 ```task_list``` 空列表; + -- 然后用 ```asyncio.create_task()``` 创建 Task; + -- 再把 Task 对象加入 ```task_list```; + -- 最后使用 ```await asyncio.wait``` 或 ```await asyncio.gather``` 将 Task 对象加入事件循环中异步执行。 +>注意:创建 Task 对象时,除了可以使用 ```asyncio.create_task()``` 之外,还可以用最低层级的 ```loop.create_task()``` 或 ```asyncio.ensure_future()```,他们都可以用来创建 Task 对象,其中关于 ```ensure_future``` 相关内容本文接下来会一起讲。 + + +- Task 用法代码示例: +``` +import asyncio +import arrow + + +def current_time(): + ''' + 获取当前时间 + :return: + ''' + cur_time = arrow.now().to('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss') + return cur_time + + +async def func(sleep_time): + func_name_suffix = sleep_time # 使用 sleep_time(函数 I/O 等待时长)作为函数名后缀,以区分任务对象 + print(f"[{current_time()}] 执行异步函数 {func.__name__}-{func_name_suffix}") + await asyncio.sleep(sleep_time) + print(f"[{current_time()}] 函数 {func.__name__}-{func_name_suffix} 执行完毕") + return f"【[{current_time()}] 得到函数 {func.__name__}-{func_name_suffix} 执行结果】" + + +async def run(): + task_list = [] + for i in range(5): + task = asyncio.create_task(async_func(i)) + task_list.append(task) + + done, pending = await asyncio.wait(task_list, timeout=None) + for done_task in done: + print((f"[{current_time()}] 得到执行结果 {done_task.result()}")) + +def main(): + loop = asyncio.get_event_loop() + loop.run_until_complete(run()) + + +if __name__ == '__main__': + main() +``` +代码执行结果如下: +```json +[2020-11-03 22:45:53] 执行异步函数 func-0 +[2020-11-03 22:45:53] 执行异步函数 func-1 +[2020-11-03 22:45:53] 执行异步函数 func-2 +[2020-11-03 22:45:53] 执行异步函数 func-3 +[2020-11-03 22:45:53] 执行异步函数 func-4 +[2020-11-03 22:45:53] 函数 func-0 执行完毕 +[2020-11-03 22:45:54] 函数 func-1 执行完毕 +[2020-11-03 22:45:55] 函数 func-2 执行完毕 +[2020-11-03 22:45:56] 函数 func-3 执行完毕 +[2020-11-03 22:45:57] 函数 func-4 执行完毕 +[2020-11-03 22:45:57] 得到执行结果 【[2020-11-03 22:45:57] 得到函数 func-4 执行结果】 +[2020-11-03 22:45:57] 得到执行结果 【[2020-11-03 22:45:55] 得到函数 func-2 执行结果】 +[2020-11-03 22:45:57] 得到执行结果 【[2020-11-03 22:45:53] 得到函数 func-0 执行结果】 +[2020-11-03 22:45:57] 得到执行结果 【[2020-11-03 22:45:56] 得到函数 func-3 执行结果】 +[2020-11-03 22:45:57] 得到执行结果 【[2020-11-03 22:45:54] 得到函数 func-1 执行结果】 +``` + +# 2. Future 概念解读 +- 在介绍 Future 之前有两点问题需要先说明: + 1、Future 相较于 Task 属于更底层的概念,在开发过程中用到的并不多,这里介绍 Future 主要是为了加深对于 Task 的理解; + 2、这里指的是 ```asyncio.Future``` 而不是 ```coroutines.futures.Future```,```coroutines.futures.Future``` 常用于 **多进程、多线程实现并发**。 +- Future,又称 **未来对象、期程对象**,其本质上是一个容器,用于接受异步执行的结果; +- 我们前面讲的 **Task 是继承自 Future** ! +- Furture 对象内部封装了一个 ```_state```,这个 ```_state``` 维护着四种状态:**Pending**、**Running**、**Done**,**Cancelled**,如果变成 ```Done``` 完成,就不再等待,而是往后执行,这四种状态的存在其实类似与进程的 运行态、就绪态、阻塞态,事件循环凭借着四种状态对 Future\协程对象 进行调度。 +- 在开发中,如果直接创建 Future 需要使用 ```asyncio.ensure_future()``` 函数,下面是 ```ensure_future``` 函数的源码,仔细阅读源码我们会发现,```ensure_future``` 函数最后返回的一定是一个 ```awaitable``` 对象,即满足 Awaitable 协议。 +>正因为 ```ensure_future``` 函数最后返回的一定是一个 ```awaitable``` 对象,所以才保证了继承自 Future 的 Task 是 ```awaitable``` 的。 +同时,协程对象一位内无法自己执行,需要将其注册到事件循环中转变为一个 Task 对象才会被执行,所以协程对象一定 ```awaitable``` 的。 + +- 一般只有在一定要确保需要创建一个 ```awaitable``` 对象的时候,才会使用 ```ensure_future``` 函数。 +``` +def ensure_future(coro_or_future, *, loop=None): + """Wrap a coroutine or an awaitable in a future. + + If the argument is a Future, it is returned directly. + """ + if coroutines.iscoroutine(coro_or_future): + if loop is None: + loop = events.get_event_loop() + task = loop.create_task(coro_or_future) + if task._source_traceback: + del task._source_traceback[-1] + return task + elif futures.isfuture(coro_or_future): + if loop is not None and loop is not futures._get_loop(coro_or_future): + raise ValueError('loop argument must agree with Future') + return coro_or_future + elif inspect.isawaitable(coro_or_future): + return ensure_future(_wrap_awaitable(coro_or_future), loop=loop) + else: + raise TypeError('An asyncio.Future, a coroutine or an awaitable is ' + 'required') + +@coroutine +def _wrap_awaitable(awaitable): + """Helper for asyncio.ensure_future(). + + Wraps awaitable (an object with __await__) into a coroutine + that will later be wrapped in a Task by ensure_future(). + """ + return (yield from awaitable.__await__()) +``` +- 下面是 Future 类的源码,感兴趣的小伙伴可以研究一下。 +``` +class Future: + """This class is *almost* compatible with concurrent.futures.Future. + + Differences: + + - This class is not thread-safe. + + - result() and exception() do not take a timeout argument and + raise an exception when the future isn't done yet. + + - Callbacks registered with add_done_callback() are always called + via the event loop's call_soon(). + + - This class is not compatible with the wait() and as_completed() + methods in the concurrent.futures package. + + (In Python 3.4 or later we may be able to unify the implementations.) + """ + + # Class variables serving as defaults for instance variables. + _state = _PENDING + _result = None + _exception = None + _loop = None + _source_traceback = None + + # This field is used for a dual purpose: + # - Its presence is a marker to declare that a class implements + # the Future protocol (i.e. is intended to be duck-type compatible). + # The value must also be not-None, to enable a subclass to declare + # that it is not compatible by setting this to None. + # - It is set by __iter__() below so that Task._step() can tell + # the difference between + # `await Future()` or`yield from Future()` (correct) vs. + # `yield Future()` (incorrect). + _asyncio_future_blocking = False + + __log_traceback = False + + def __init__(self, *, loop=None): + """Initialize the future. + + The optional event_loop argument allows explicitly setting the event + loop object used by the future. If it's not provided, the future uses + the default event loop. + """ + if loop is None: + self._loop = events.get_event_loop() + else: + self._loop = loop + self._callbacks = [] + if self._loop.get_debug(): + self._source_traceback = format_helpers.extract_stack( + sys._getframe(1)) + + _repr_info = base_futures._future_repr_info + + def __repr__(self): + return '<{} {}>'.format(self.__class__.__name__, + ' '.join(self._repr_info())) + + def __del__(self): + if not self.__log_traceback: + # set_exception() was not called, or result() or exception() + # has consumed the exception + return + exc = self._exception + context = { + 'message': + f'{self.__class__.__name__} exception was never retrieved', + 'exception': exc, + 'future': self, + } + if self._source_traceback: + context['source_traceback'] = self._source_traceback + self._loop.call_exception_handler(context) + + @property + def _log_traceback(self): + return self.__log_traceback + + @_log_traceback.setter + def _log_traceback(self, val): + if bool(val): + raise ValueError('_log_traceback can only be set to False') + self.__log_traceback = False + + def get_loop(self): + """Return the event loop the Future is bound to.""" + return self._loop + + def cancel(self): + """Cancel the future and schedule callbacks. + + If the future is already done or cancelled, return False. Otherwise, + change the future's state to cancelled, schedule the callbacks and + return True. + """ + self.__log_traceback = False + if self._state != _PENDING: + return False + self._state = _CANCELLED + self.__schedule_callbacks() + return True + + def __schedule_callbacks(self): + """Internal: Ask the event loop to call all callbacks. + + The callbacks are scheduled to be called as soon as possible. Also + clears the callback list. + """ + callbacks = self._callbacks[:] + if not callbacks: + return + + self._callbacks[:] = [] + for callback, ctx in callbacks: + self._loop.call_soon(callback, self, context=ctx) + + def cancelled(self): + """Return True if the future was cancelled.""" + return self._state == _CANCELLED + + # Don't implement running(); see http://bugs.python.org/issue18699 + + def done(self): + """Return True if the future is done. + + Done means either that a result / exception are available, or that the + future was cancelled. + """ + return self._state != _PENDING + + def result(self): + """Return the result this future represents. + + If the future has been cancelled, raises CancelledError. If the + future's result isn't yet available, raises InvalidStateError. If + the future is done and has an exception set, this exception is raised. + """ + if self._state == _CANCELLED: + raise CancelledError + if self._state != _FINISHED: + raise InvalidStateError('Result is not ready.') + self.__log_traceback = False + if self._exception is not None: + raise self._exception + return self._result + + def exception(self): + """Return the exception that was set on this future. + + The exception (or None if no exception was set) is returned only if + the future is done. If the future has been cancelled, raises + CancelledError. If the future isn't done yet, raises + InvalidStateError. + """ + if self._state == _CANCELLED: + raise CancelledError + if self._state != _FINISHED: + raise InvalidStateError('Exception is not set.') + self.__log_traceback = False + return self._exception + + def add_done_callback(self, fn, *, context=None): + """Add a callback to be run when the future becomes done. + + The callback is called with a single argument - the future object. If + the future is already done when this is called, the callback is + scheduled with call_soon. + """ + if self._state != _PENDING: + self._loop.call_soon(fn, self, context=context) + else: + if context is None: + context = contextvars.copy_context() + self._callbacks.append((fn, context)) + + # New method not in PEP 3148. + + def remove_done_callback(self, fn): + """Remove all instances of a callback from the "call when done" list. + + Returns the number of callbacks removed. + """ + filtered_callbacks = [(f, ctx) + for (f, ctx) in self._callbacks + if f != fn] + removed_count = len(self._callbacks) - len(filtered_callbacks) + if removed_count: + self._callbacks[:] = filtered_callbacks + return removed_count + + # So-called internal methods (note: no set_running_or_notify_cancel()). + + def set_result(self, result): + """Mark the future done and set its result. + + If the future is already done when this method is called, raises + InvalidStateError. + """ + if self._state != _PENDING: + raise InvalidStateError('{}: {!r}'.format(self._state, self)) + self._result = result + self._state = _FINISHED + self.__schedule_callbacks() + + def set_exception(self, exception): + """Mark the future done and set an exception. + + If the future is already done when this method is called, raises + InvalidStateError. + """ + if self._state != _PENDING: + raise InvalidStateError('{}: {!r}'.format(self._state, self)) + if isinstance(exception, type): + exception = exception() + if type(exception) is StopIteration: + raise TypeError("StopIteration interacts badly with generators " + "and cannot be raised into a Future") + self._exception = exception + self._state = _FINISHED + self.__schedule_callbacks() + self.__log_traceback = True + + def __await__(self): + if not self.done(): + self._asyncio_future_blocking = True + yield self # This tells Task to wait for completion. + if not self.done(): + raise RuntimeError("await wasn't used with future") + return self.result() # May raise too. + + __iter__ = __await__ # make compatible with 'yield from'. +``` + diff --git "a/_posts/2022-02-01-Python\357\274\232\345\244\232\346\200\201\343\200\201\351\270\255\345\255\220\346\250\241\345\236\213\345\222\214\346\212\275\350\261\241\345\237\272\347\261\273.md" "b/_posts/2022-02-01-Python\357\274\232\345\244\232\346\200\201\343\200\201\351\270\255\345\255\220\346\250\241\345\236\213\345\222\214\346\212\275\350\261\241\345\237\272\347\261\273.md" new file mode 100644 index 00000000000..a96ea9d4e7f --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232\345\244\232\346\200\201\343\200\201\351\270\255\345\255\220\346\250\241\345\236\213\345\222\214\346\212\275\350\261\241\345\237\272\347\261\273.md" @@ -0,0 +1,189 @@ +--- +layout: post +title: Python:多态、鸭子模型和抽象基类 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. 多态 +- 什么是多态 + - 多态,指的是一种事务具有多种形态; + - python是一种动态语言,默认支持多态,**同一个方法** 调用 **不同的类对象** ,执行的 **结果各不相同**; +- 多态实现 + - 继承:**不同子类** 继承 **同一父类**; + - 重写:子类重写 **同一个方法**,保证执行结果各不相同; + +- 示例 + - 有如下代码: +``` + +>>> class Animals(): +... def talk(self): +... print("Animal talk") +... +>>> +>>> class People(Animals): # 继承 Animals 类 +... def talk(self): +... print('People speak language') +... +>>> +>>> class Cat(Animals): # 继承 Animals 类 +... def talk(self): +... print('Cat say miaomiao') +... +>>> +>>> cat = Cat() +>>> peo = People() +>>> +>>> cat.talk() # 调用 talk 方法 +Cat say miaomiao +>>> peo.talk() # 调用 talk 方法 +People speak language + +``` +- 如上所示: + - cat 和 peo 两个对象调用同一个 talk() 方法; + - 最后得到两种不同的结果; +- 多态的优点: + - 多态可以增加代码的灵活度; + - 是调用方法的技巧,不会影响到类的内部设计; + - 多态可以看做 **接口函数的重用**,**同一种接口方法** 通过 **接收不同的类** 对象,从而实现不同的功能; +- 多态使用场景: + - 方法参数接收同一父类的不同子类对象。 + +# 2. 鸭子模型 +- 什么是鸭子模型 + - 当看到一只鸟走起来像鸭子,游泳起来也像鸭子,叫起来也像鸭子,那么这只鸟就可以被称为鸭子; + - 鸭子模型和多态一样,都是接受不同的类对象,并调用相同的方法(即:鸭子的 游泳 和 叫 方法); + - 对于一个鸭子模型来说,我们并 **不关心接收的类对象是否真的是鸭子类**,只关心这个类是如何被使用的; + - 注意:如果这些需要被调用的方法不存在,那么将引发一个运行时错误。 + +- 示例 + - 有如下代码: +``` + +>>> class Duck: +... def quack(self): +... print("duck quack") +... +>>> +>>> class Bird: # Bird 类与 Duck 类无继承关系 +... def quack(self): +... print("bird quack") +... +>>> +>>> class Dog: # Dog类与 Duck 类无继承关系 +... def quack(self): +... print("dog quack") +... +>>> +>>> def animal_quack(animal): # animal_quack 方法可以调用任何对象的 quack() 方法,不关心对象是谁 +... animal.quack() +... +>>> +>>> duck = Duck() +>>> bird = Bird() # bird 实例与 duck 实例无任何关系 +>>> dog = Dog() # dog 实例与 duck 实例无任何关系 +>>> +>>> for animal in [duck, bird, dog]: +... animal_quack(animal) +... +duck quack +bird quack +dog quack + +``` +- 如上所示: +- duck、bird、dog 分别来自三个不同的类,而且类之间是 **没有继承关系** 的; +- duck、bird、dog 调用 animal_quack 方法,得到三种不同的结果,符合多态的特征; +- 鸭子模型的优点: + - 鸭子模型不关关心类对象是什么,不需要类之间具有继承关系; + - 鸭子模型让代码比多态更加灵活度; +- 多态使用场景: + - 鸭子模型中,接收不同的类将会产生不同的行为,而无须明确知道这个类实际上是什么,这是多态的重要应用场景; + - 实际生产环境中,主要用于 **接口开发**,即用同一个函数接收不同的类对象,从而实现不同的功能,而且无需关注对象之间的继承关系; +# 3. 抽象基类 +- 什么是抽象基类 + - 抽象基类,这个词可能听着比较"深奥",其实 抽象 就是 假 的意思,基类 就是 父类,抽象基类 就是 假父类; + - 具体来说,由 abc.ABCMeta 这个元类实现的类,就是抽象基类; +- 示例: + - 如下代码中的 AbstractClass 类继承自 abc.ABCMeta,AbstractClass 就是抽象基类; +``` + +class AbstractClass(metaclass=abc.ABCMeta): + pass + +``` + +- 抽象基类的作用 + - 判断是否为某个对象的实例 +``` + +>>> class MyList(object): +... def __init__(self, my_list): +... self.my_list= my_list +... def __len__(self): +... return len(self.my_list) +... +>>> +>>> class NewList(MyList): # NewList 继承自 MyList +... pass +... +>>> ml = MyList(["a", "b", "c"]) +>>> +>>> from collections.abc import Sized, Iterable +>>> +>>> print(isinstance(ml, Sized)) +True # 返回 True,因为这里会检查实例对象中有没有__len__方法,有即输出True +>>> nl = NewList([1, 2, 3]) +>>> print(isinstance(nl, MyList)) +True # 返回 True,因为 nl 实例化的类 NewList 同时也是 MyList 的子类 + +``` +- 强制要求父类被子类继承,并在子类实现某个方法,否则子类初始化时就会报错; +``` + +>>> from abc import ABCMeta,abstractmethod +>>> +>>> +>>> class Source(metaclass=ABCMeta): # 创建抽象基类 Source +... @abstractmethod # 表示装饰的方法必须被子类所实现,否则会报错 +... def get(self,key): +... pass +... +>>> +>>> class Mysource(Source): # 子类 Mysource 继承自 抽象基类 Source +... def get(self,key): # 实现 get 方法,这个方法是 抽象基类 Source 强制要求实现的 +... pass +... +>>> +>>> class Mysource1(Source): # 子类 Mysource1 没有实现 抽象基类 Source 强制要求实现的 get 方法 +... pass +... +>>> test = Source() # test 直接实例化 Source 父类 +Traceback (most recent call last): # 此处报错,因为抽象类无法实现实例化 + File "", line 1, in +TypeError: Can't instantiate abstract class Source with abstract methods get +>>> +>>> test = Mysource() # 此处实例化 Mysource,未报错 +>>> +>>> test = Mysource1() +Traceback (most recent call last): # 报错,继承类必须实现抽象类的方法 + File "", line 1, in +TypeError: Can't instantiate abstract class Mysource1 with abstract methods get + +``` +- 抽象基类使用场景 + - 接口强制规定,主要是 **强制子类实现某个方法**,否则就提示报错; +- 抽象基类的有点: + - 处理继承问题方面更加规范、系统; + - 明确调用之间的相互关系,使得继承层次更加清晰; +- 抽象基类的缺点: + - 抽象基类在 python 并非在于用来继承,主要用来理解 python继承 的定义,应该 **尽量使用鸭子模型**; + - 如果一定要继承接口的话,比较 **推荐多继承**,抽象基类容易 **设计过度**; \ No newline at end of file diff --git "a/_posts/2022-02-01-Python\357\274\232\345\256\236\344\276\213\346\226\271\346\263\225\343\200\201\351\235\231\346\200\201\346\226\271\346\263\225\345\222\214\347\261\273\346\226\271\346\263\225.md" "b/_posts/2022-02-01-Python\357\274\232\345\256\236\344\276\213\346\226\271\346\263\225\343\200\201\351\235\231\346\200\201\346\226\271\346\263\225\345\222\214\347\261\273\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..01500f82914 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232\345\256\236\344\276\213\346\226\271\346\263\225\343\200\201\351\235\231\346\200\201\346\226\271\346\263\225\345\222\214\347\261\273\346\226\271\346\263\225.md" @@ -0,0 +1,138 @@ +--- +layout: post +title: Python:实例方法、静态方法和类方法 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. 实例方法 +- 什么是实例方法 + -- 只有实例化对象之后才可以使用的方法,称为 类方法; + -- 一般在类中定义的方法,都是 类方法; + -- 该方法的第一个形参接收的一定是对象本身,即 self; +- 示例: +``` + +>>> class Animal(): +... def eat(self): +... print("animal eat") +... +... +>>> cat = Animal() +>>> cat.eat() +animal eat + +``` +-- 如上所示,eat() 方法就是实例方法,只有在 cat 对 Animal 类进行实例化之后,eat() 方法才能够被调用; + +# 2. 静态方法 +- 什么是静态方法 + -- 类对象或实例对象都可以调用的方法,称为静态方法; + -- 静态方法使用装饰器 @staticmethod进行装饰; + -- 静态方法的参数随意,没有 self 和 cls 参数; + -- 静态方只能能使用 类 属性,不能调用 实例 的任何属性和方法; +- 静态方法调用 + -- 类对象或实例对象都可以调用; +- 示例: +``` + +>>> class Animal(): +... color = "blue" # 创建 Animal 类属性 color,并赋值 "blue" +... def __init__(self, language): +... self.language = language +... @staticmethod +... def drink(sth): # 静态方法 drink 操作 sth,sth 与 Animal 类没有任何关系 +... print("animal drink {}".format(sth)) +... @staticmethod +... def has_color(): +... print("animal has {} color".format(Animal.color)) # 静态方法 has_color 调用 Animal 类属性 color +... @staticmethod +... def speak(): +... print("animal speak {}".format(Animal().language)) # 静态方法 speak 调用实例属性 Animal().language +... +>>> +>>> cat = Animal(language="miaomiao") +>>> cat.drink("water") # 成功调用 静态方法 drink 操作对象 "water" +animal drink water +>>> cat.has_color() # 成功调用 Animal 类属性 color,但是这种操作很鸡肋,因为不可能所有实例的 color 都是 "blue" +animal has blue color +>>> cat.speak() # 调用实例属性 Animal().language 失败,这种操作不符合规范 +Traceback (most recent call last): + File "", line 1, in + File "", line 13, in speak +TypeError: __init__() missing 1 required positional argument: 'language' + +``` +- 静态方法使用场景 + -- 静态方法参数不涉及类内部的变量; + -- 静态方法参数与类没有任何关系; + -- 静态方法不关心类中的属性和方法的变化; + -- **可以将静态方法当成一个普通的函数使用**; +# 3. 类方法 +- 什么是类方法 + -- 类方法 使用装饰器@classmethod 进行装饰; + -- 它的第一个参数不是 self,而是 cls,它表示这个类本身; + -- 类方法的第一个参数约定名为 cls; +- 类方法的调用 + -- 和实例方法调用一样,先实例化再调用; +``` + +>>> class Animal(): +... def __init__(self, language): # 初始化参数为 language +... self.language = language +... @classmethod +... def speak(cls, language): # 创建类方法 speak,其返回的是重新被 cls 即类本身处理过的 cls(language) +... return cls(language) +... def __str__(self): # 这个__str__是魔法方法,其返回的字符串结果是类默认属性,如果去掉这段代码会报错 +... return "animal speak " + self.language +... +>>> cat = Animal(language="miaomiao") # 实例化一个 cat,并赋予 language 参数 "miaomiao" +>>> +>>> cat_word = cat.speak(language="wangwang gaga") # cat 调用了类方法 speak(),并重新赋予 language 参数 "wangwang gaga" +>>> print(cat_word) +animal speak wangwang gaga # 打印 cat_word,看看小猫说了什么,神奇的一幕出现了,小猫 wangwang gaga 乱叫 + +``` +-- 在上面的代码中我们发现,尽管我们已经给 cat 实例的 language 参数赋值 "miaomiao",但是由于 speak() 作为静态方法通过 cls(laguage) 对类进行了二次处理,导致 cat.speak(language="wangwang gaga") 的打印结果是 animal speak wangwang gaga,我们得到一个 汪汪嘎嘎 乱叫的 猫。 +- 类方法的应用场景 + -- 类方法的一个应用场景,是上面代码中展示的,改变 实例化类 的 __ init __ 初始参数; + -- 类方法的另一个价值是:**“类方法让类模板具有记忆力”**,具体来说,在普通情况下不使用类方法对类进行实例化,类本身是不具有记忆性的,只是当一个静态模板被套用多次而已。如果我们想让类在每一次实例化之后,都能记载一些记忆,那么就需要用到类方法; + -- 下面的示例展示 **类方法如何让类模板具有记忆力**: +``` + +>>> class Count(): # 创建一个计数器 Cout 类 +... current_count = 10000 # 类变量 current_count 代表类当前的 【数值】,初始值为 10000 +... def __init__(self, num): # num 参数记录的是当前的 【次数】 +... self.num = num +... self.current_count = self.count() +... @classmethod +... def count(cls): # 此处定义静态方法 count +... cls.current_count += 1 # 每次调用当前数值都会 +1 +... return cls.current_count +... def __str__(self): # 这个__str__是魔法方法,其返回的字符串结果是类默认属性,如果去掉这段代码会报错 +... return f"第 {self.num} 次计数:当前数值为 {self.current_count}" +... +>>> +>>> num = 0 +>>> while num < 10: +... print(Count(num)) +... num += 1 +... +第 0 次计数:当前数值为 10001 +第 1 次计数:当前数值为 10002 +第 2 次计数:当前数值为 10003 +第 3 次计数:当前数值为 10004 +第 4 次计数:当前数值为 10005 +第 5 次计数:当前数值为 10006 +第 6 次计数:当前数值为 10007 +第 7 次计数:当前数值为 10008 +第 8 次计数:当前数值为 10009 +第 9 次计数:当前数值为 10010 + +``` diff --git "a/_posts/2022-02-01-Python\357\274\232\345\271\266\345\217\221 & \345\271\266\350\241\214\343\200\201\345\220\214\346\255\245 & \345\274\202\346\255\245\343\200\201\351\230\273\345\241\236 & \351\235\236\351\230\273\345\241\236.md" "b/_posts/2022-02-01-Python\357\274\232\345\271\266\345\217\221 & \345\271\266\350\241\214\343\200\201\345\220\214\346\255\245 & \345\274\202\346\255\245\343\200\201\351\230\273\345\241\236 & \351\235\236\351\230\273\345\241\236.md" new file mode 100644 index 00000000000..cc92221b54e --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232\345\271\266\345\217\221 & \345\271\266\350\241\214\343\200\201\345\220\214\346\255\245 & \345\274\202\346\255\245\343\200\201\351\230\273\345\241\236 & \351\235\236\351\230\273\345\241\236.md" @@ -0,0 +1,61 @@ +--- +layout: post +title: Python:并发 & 并行、同步 & 异步、阻塞 & 非阻塞 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. 并发 & 并行 + +#### 1.1. 并发 + +- 在一个 **时间段内**,**多个程序** 在 **同一个 CPU 上** 运行; + -- 但实际任一个时刻点上只有一个程序在处理机上运行; + +#### 1.2. 并行 + +- 在一个 **时间点上**,**多个程序** 在 **多个 CPU 上** 运行; + +# 2. 同步 & 异步 + +> 同步 & 异步 概念属于 I/O 操作范畴, + +#### 2.1. 同步 + +- 代码调用 I/O 操作时,**必须等到 I/O 操作完成** 才返回; + +#### 2.2. 异步 + +- 代码调用 I/O 操作时,**不用等到 I/O 操作完成** 就可以返回; +- 同步 & 异步 任务执行的顺序; + ##2.3. 同步& 异步 内涵 +>同步 和 异步,关注的是消息通信机制 (synchronous communication/ asynchronous communication); +-- 异步程序执行完毕以后,会通过 **消息机制** 告知相关程序; +- 所谓同步,就是在发出一个"调用"时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。 +- 而异步则是相反,"调用"在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在"调用"发出后,"被调用者"通过状态、通知来通知调用者,或通过回调函数处理这个调用。 + +# 3. 阻塞 & 非阻塞 + +#### 3.1. 阻塞 + +- 调用函数时,当前线程自身会被挂起; +- 常见的阻塞形式: + -- 网络 I/O 阻塞; + -- 磁盘 I/O 阻塞; + -- 用户输入阻塞; + +#### 3.2. 非阻塞 + +- 调用函数时当前线程自身不会被挂起,而是立即返回; + +#### 3.3. 阻塞 & 非阻塞 内涵 + +> 阻塞和非阻塞 强调的是程序在等待调用结果(消息,返回值)时的状态。 +- 阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回; +- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。 对于同步调用来说,很多时候当前线程还是激活的状态,只是从逻辑上当前函数没有返回而已,即同步等待时什么都不干,白白占用着资源。 diff --git "a/_posts/2022-02-01-Python\357\274\232\347\273\247\346\211\277\343\200\201\345\215\225\347\273\247\346\211\277 \345\222\214 \345\244\232\347\273\247\346\211\277.md" "b/_posts/2022-02-01-Python\357\274\232\347\273\247\346\211\277\343\200\201\345\215\225\347\273\247\346\211\277 \345\222\214 \345\244\232\347\273\247\346\211\277.md" new file mode 100644 index 00000000000..d5a7ddf4cb6 --- /dev/null +++ "b/_posts/2022-02-01-Python\357\274\232\347\273\247\346\211\277\343\200\201\345\215\225\347\273\247\346\211\277 \345\222\214 \345\244\232\347\273\247\346\211\277.md" @@ -0,0 +1,198 @@ +--- +layout: post +title: Python:继承、单继承 和 多继承 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Python +--- + + +# 1. 继承 +- 什么是继承 + - 继承是一种创建新的类的方式; + - 新创建的叫子类; + - 被继承的叫父类、超类、基类; +- 示例: +``` + +>>> class Animal(): # 创建一个父类 Animal +... def __init__(self): +... print("this is an animal") +... def swim(self): # 创建 swim 方法 +... print("animal swim") +... +>>> class Cat(Animal): # 创建一个子类 Cat,继承自 Animal 父类 +... def __init__(self): +... super().__init__() # 使用 super().__init__() 继承 Animal 父类的所有方法,包括__init__ 方法和 swim 方法 +... def walk(self): # 创建 walk 方法,该方法在 Cat 子类内部创建,与 Animal 父类无关 +... print("cat walk") +... +>>> cat = Cat() # 实例化 Cat 子类 +this is an animal # Cat 子类继承了 Animal 父类的所有属性和方法,所以才会打印 this is an animal +>>> cat.walk() # cat 实力调用 Cat 子类独有的 walk 方法 +cat walk +>>> cat.swim() # 注意:cat 实例可以调用 Animal 父类的 swim 方法,而这个方法 Cat 子类中并未创建 +animal swim + +``` +- 在上面的代码中,Cat 就是子类,Animal 就是父类,Cat 继承了 Animal 的 **【全部】** 属性和方法; +- Cat 子类继承了 Animal 父类的 __ init __() 方法,所以在用 cat 对 Cat 类进行实例化时,才会打印 this is an animal; +- Cat 子类继承了 Animal 父类的 swim() 方法,所以尽管 Cat 子类中没有 swim() 方法,但是 Cat 的实例 cat 仍然可以调用 swim() 方法; +- 继承的作用 + - 子类可以使用父类的属性(特征、方法); + - 减少代码冗余、提高重用性。 +- 继承的缺陷: + - 如上面的代码,Cat 子类中没有 swim() 方法,因为猫不会游泳,但是 Cat 子类继承自 Animal父类,Cat 子类的实例 cat 仍然可以调用 swim() 方法,这其实存在巨大的隐患; + - 实际上在生产环境中,我们并不希望 Cat 子类继承 Animal 父类的 swim() 方法,这种情形会导致可能会导致 bug; + - 由于 swim() 这类本不该继承的方法的存在,继承关系会变得混乱而复杂,不利于问题的溯源。 + +# 2. 单继承 +- 什么是单继承 + - 单继承,一个类只能继承一个父类的继承方式; +- 示例: +``` + +>>> class Animal(): # 创建父类 Animal +... def __init__(self): # 初始化,打印 this is an animal +... print("this is an animal") +... def swim(self): +... print("animal swim") +... def walk(self): +... print("animal walk") +... def fly(self): +... print("animal fly") +... +>>> +>>> class Cat(Animal): +... def walk(self): # 重写 walk 方法 +... print("cat walk") +... +>>> +>>> class Fish(Animal): +... def swim(self): # 重写 swim 方法 +... print("fish swim") +... +>>> +>>> class Bird(Animal): +... def swim(self): # 重写 swim 方法 +... print("bird swim") +... def walk(self): # 重写 walk 方法 +... print("bird walk") +... def fly(self): # 重写 fly方法 +... print("bird fly") +... +>>> +>>> white_cat = Cat() +this is an animal # 实例化 Cat 类时,打印 this is an animal,这是继承自 Animal 父类的初始化动作 +>>> white_cat.walk() +cat walk +>>> +>>> +>>> golden_fish = Fish() +this is an animal # 实例化 Fish 类时,打印 this is an animal,这是继承自 Animal 父类的初始化动作 +>>> golden_fish.swim() +fish swim +>>> +>>> +>>> blue_bird = Bird() +this is an animal # 实例化 Bird 类时,打印 this is an animal,这是继承自 Animal 父类的初始化动作 +>>> blue_bird.walk() +bird walk +>>> blue_bird.swim() +bird swim +>>> blue_bird.fly() +bird fly +``` +- 单继承缺陷: + - Cat、Fish、Bird 三个子类继承自 Animal 父类,需要重写各自调用的方法,重写方法使得代码重复、臃肿; + - Animal 父类中的方法比较多,总共有三个方法,但是其子类并未全部继承,而且由于没有严格的规则定义什么类型的子类继承父类中的那个方法,导致继承关系比较散乱,结构不够严谨、不利于整体的管理; +# 3. 多继承 +- 什么是多继承 + - 多继承,一个类可以继承多个父类的继承方式; +- 示例,上面的代码可以改成如下: +``` + +>>> class Animal(): +... def __init__(self): +... print("this is an animal") +... +>>> class SwimAnimal(Animal): +... def __init__(self): +... super().__init__() +... print("this is swim animal") +... def swim(self): +... print("animal swim") +... +>>> +>>> class WalkAnimal(Animal): +... def __init__(self): +... super().__init__() +... print("this is walk animal") +... def walk(self): +... print("animal walk") +... +>>> +>>> class FlyAnimal(Animal): +... def __init__(self): +... super().__init__() +... print("this is fly animal") +... def fly(self): +... print("animal fly") +... +>>> +>>> class Cat(WalkAnimal): +... def __init__(self): +... super().__init__() +... +>>> +>>> class Fish(SwimAnimal): +... def __init__(self): +... super().__init__() +... +>>> +>>> class Bird(SwimAnimal, WalkAnimal, FlyAnimal): +... def __init__(self): +... super().__init__() +... +>>> +>>> white_cat = Cat() +this is an animal +this is walk animal +>>> white_cat.walk() +animal walk +>>> +>>> +>>> golden_fish = Fish() +this is an animal +this is swim animal +>>> golden_fish.swim() +animal swim +>>> +>>> +>>> blue_bird = Bird() +this is an animal +this is fly animal +this is walk animal +this is swim animal +>>> blue_bird.walk() +animal walk +>>> blue_bird.swim() +animal swim +>>> blue_bird.fly() +animal fly + +``` +- 上面的代码中,SwimAnimal、WalkAnimal、FlyAnimal 三个类继承自 Animal 父类,Cat 类继承自 WalkAnimal,Fish 类继承自 SwimAnimal,Bird 类继承自 SwimAnimal, WalkAnimal, FlyAnimal 三个类; +- 图示如下: + + ![]({{site.baseurl}}/img-post/多继承.png) + +- 其中,Bird 类继承自 SwimAnimal, WalkAnimal, FlyAnimal 三个类,而这三个类又共同继承自 Animal 父类,所以实例化 Bird 类时,打印了 this is an animal、this is fly animal、this is walk animal、this is swim animal 四句话; + +- 多继承优点: + - 使用多继承,可以使得继承关系更加清晰,代码结构构架严谨、整洁,更易于代码设计和管理; + - 使用多继承,在遇到报错时可以更容易通过继承关系进行问题定位,代码可维护性会更高; \ No newline at end of file diff --git "a/_posts/2022-02-01-Redis\357\274\232Redis \346\227\240\345\272\217\351\233\206\345\220\210 Set \345\270\270\347\224\250\345\221\275\344\273\244\350\247\243\350\257\273.md" "b/_posts/2022-02-01-Redis\357\274\232Redis \346\227\240\345\272\217\351\233\206\345\220\210 Set \345\270\270\347\224\250\345\221\275\344\273\244\350\247\243\350\257\273.md" new file mode 100644 index 00000000000..4a147467ebb --- /dev/null +++ "b/_posts/2022-02-01-Redis\357\274\232Redis \346\227\240\345\272\217\351\233\206\345\220\210 Set \345\270\270\347\224\250\345\221\275\344\273\244\350\247\243\350\257\273.md" @@ -0,0 +1,431 @@ +--- +layout: post +title: Redis:Redis 无序集合 Set 常用命令解读 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Redis +--- + +#### 前言 + +在Redis中,集合(Set)是一个无序的字符串数据集,且该数据集中的元素具有唯一性(即不存在重复的元素)。 + +在下面的文章中,列举了日常工作中我们常用的 Set 集合命令: + +#### SADD + +`SADD`命令用于将指定元素添加到集合中,并返回实际添加的元素个数(即不包括已经存在的元素个数)。当指定的元素在集合中已经存在时,将忽略该元素。若指定的键不存在,在执行操作前将创建一个新的空集合。 + +``` +SADD key member [member ...] + +``` + +在Redis 2.4及以上版本中,`SADD`命令可用于一次添加多个元素。 + +###### 示例 + +``` +redis> SADD numbers 1 +(integer) 1 +# 元素1已存在,第二次执行时将被忽略 +redis> SADD numbers 1 2 3 +(integer) 2 +redis> SMEMBERS numbers +1) "1" +2) "2" +3) "3" + +``` + +#### SISMEMBER + +`SISMEMBER`命令用于指示集合中是否存在指定元素,若存在则返回`1`,否则返回`0`(若集合不存在则视为空集合,也将返回`0`)。 + +``` +SISMEMBER key member + +``` + +###### 示例 + +``` +redis> SADD numbers 1 +(integer) 1 +redis> SISMEMBER numbers 1 +(integer) 1 +redis> SISMEMBER numbers 2 +(integer) 0 + +``` + +#### SREM + +`SREM`命令用于移除集合中的指定元素,并返回实际移除的元素个数。当集合中不存在指定的元素时将忽略对应的移除操作;若集合不存在,则将视为空集合,并返回`0`。 + +``` +SREM key member [member ...] + +``` + +在Redis 2.4及以上版本中,`SREM`命令可用于一次移除多个元素。 + +###### 示例 + +``` +redis> SADD numbers 1 2 3 4 +(integer) 4 +redis> SREM numbers 3 4 5 +(integer) 2 +redis> SMEMBERS numbers +1) "1" +2) "2" + +``` + +#### SCARD + +`SCARD`命令用于返回集合的基数(cardinality,即集合中元素的个数),若集合不存在则将返回`0`。 + +``` +SCARD key + +``` + +###### 示例 + +``` +redis> SADD numbers 1 2 3 +(integer) 3 +redis> SADD numbers 4 +(integer) 1 +redis> SCARD numbers +(integer) 4 + +``` + +#### SMEMBERS + +`SMEMBERS`命令用于已数组的形式返回集合中的所有元素,若集合不能存在则视为空集合。 + +``` +SMEMBERS key + +``` + +###### 示例 + +``` +# 键numbers不存在 +redis> SMEMBERS numbers +(empty array) +redis> SADD numbers 1 2 3 +(integer) 3 +redis> SMEMBERS numbers +1) "1" +2) "2" +3) "3" + +``` + +#### SSCAN + +`SSCAN`命令与前文中介绍过的[`SCAN`](https://www.ghosind.com/2020/06/30/redis-keys#scan)命令类似,它用于增量式的迭代获取集合中的所有元素。同样,`SSCAN`命令是一个基于游标`cursor`的迭代器,每次执行后将会返回一个新的游标,以作为下一轮迭代的游标参数。关于更多`SSCAN`命令的用法,可参考[`SCAN`](https://www.ghosind.com/2020/06/30/redis-keys#scan)命令。 + +``` +SSCAN key cursor [MATCH pattern] [COUNT count] + +``` + +###### 示例 + +``` +redis> SSCAN numbers +1) "0" +2) (empty array) +redis> SADD numbers 1 2 3 +(integer) 3 +redis> SSCAN numbers 0 +1) "0" +2) 1) "1" + 2) "2" + 3) "3" + +``` + +#### SRANDMEMBER + +`SRANDMEMBERS`命令用于随机获取元素,并在Redis 2.6及以上版本中支持通过`count`参数指定获取的元素个数。 + +``` +SRANDMEMBER key [count] + +``` + +对于`count`参数,不同的值将有以下几种情况: + +* 若其值为正数则随机返回集合中指定数量的元素,且所有元素不重复; +* 若其值大于集合的大小则返回集合中的全部元素; +* 若其值为负数,则将随机获取该值绝对值数量的元素,且可能存在重复的元素; +* 若其值为0,则返回空数组。 + +###### 返回值 + +当未指定`count`参数时: + +* 随机返回集合内的一个元素; +* 若集合不存在则返回`nil`。 + +若通过`count`参数指定获取的元素个数时: + +* 返回随机获取的元素数组; +* 若集合不存在则返回空数组。 + +###### 示例 + +``` +redis> SADD numbers 1 2 3 4 5 +(integer) 5 +redis> SRANDMEMBER numbers +"4" +redis> SRANDMEMBER numbers 2 +1) "2" +2) "3" + +``` + +#### SPOP + +`SPOP`命令用于随机地从集合中移除并返回元素,若集合不存在则返回`nil`。在Redis 3.2及以上的版本中,`SPOP`命令支持通过`count`参数指定获取的元素个数。若`count`的值大于集合的大小,将移除并返回集合中的全部元素。 + +``` +SPOP key [count] +``` + +###### 示例 + +``` +127.0.0.1:6379> SADD numbers 1 2 3 4 5 +(integer) 5 +127.0.0.1:6379> SPOP numbers +"3" +127.0.0.1:6379> SPOP numbers +"2" +127.0.0.1:6379> SPOP numbers +"5" +127.0.0.1:6379> SPOP numbers +"4" +127.0.0.1:6379> SPOP numbers +"1" +127.0.0.1:6379> SPOP numbers +(nil) +127.0.0.1:6379> SCARD numbers +(integer) 0 +127.0.0.1:6379> + +``` + +#### SMOVE + +`SMOVE`命令用于将源集合中的指定元素移至目标集合中,即将该元素从源集合中移除并在目标集合中添加,并返回`1`。当源集合中不存在指定的元素时,将不执行操作并返回`0`。 + +``` +SMOVE source destination member + +``` + +`SMOVE`命令具备原子性,即在执行时其它客户端的连接只会在源集合或目标集合中获取到该元素。 + +###### 示例 + +``` +redis> SADD set1 1 2 +(integer) 2 +redis> SADD set2 3 +(integer) 1 +redis> SMOVE set1 set2 2 +(integer) 1 +redis> SMEMBERS set1 +1) "1" +redis> SMEMBERS set2 +1) "2" +2) "3" + +``` + +#### SINTER + +`SINTER`命令用于获取指定集合的交集。当集合不存在将被视为空集合,若参数中包含空集合,返回的结果也为空(任何集合与空集合的交集为空集)。 + +当只指定一个集合作为参数时,执行该命令等同于执行`SMEMBERS`命令。 + +``` +SINTER key [key ...] + +``` + +官方文档中给出了下列示例: + +``` +key1 = {a,b,c,d} +key2 = {c} +key3 = {a,c,e} +SINTER key1 key2 key3 = {c} + +``` + +###### 示例 + +``` +redis> SADD set1 1 2 3 +(integer) 3 +redis> SADD set2 3 4 5 +(integer) 3 +redis> SINTER set1 set2 +1) "3" + +``` + +#### SINTERSTORE + +`SINTERSTORE`命令与`SINTER`命令相似,区别在于`SINTERSTORE`命令不直接返回其交集,而是保存到`destination`参数指定的集合中,并返回结果的数量。若`destination`参数指定的集合已存在将会被覆盖。 + +``` +SINTERSTORE destination key [key ...] + +``` + +###### 示例 + +``` +redis> SADD set1 1 2 3 +(integer) 3 +redis> SADD set2 3 4 5 +(integer) 3 +redis> SINTERSTORE set3 set1 set2 +(integer) 1 +redis> SMEMBERS set3 +1) "3" + +``` + +#### SUNION + +`SUNION`命令用于获取指定集合的并集。若集合不存在将被视为空集合。 + +``` +SUNION key [key ...] + +``` + +官方文档中给出了下列示例: + +``` +key1 = {a,b,c,d} +key2 = {c} +key3 = {a,c,e} +SUNION key1 key2 key3 = {a,b,c,d,e} + +``` + +###### 示例 + +``` +redis> SADD set1 1 2 3 +(integer) 3 +redis> SADD set2 3 4 5 +(integer) 3 +redis> SUNION set1 set2 +1) "1" +2) "2" +3) "3" +4) "4" +5) "5" + +``` + +#### SUNIONSTORE + +`SUNIONSTORE`命令与`SUNION`命令相似,区别在于`SUNIONSTORE`命令不直接返回其并集,而是保存到`destination`参数指定的集合中,并返回结果的数量。若`destination`参数指定的集合已存在将会被覆盖。 + +``` +SUNION key [key ...] + +``` + +###### 示例 + +``` +redis> SADD set1 1 2 3 +(integer) 3 +redis> SADD set2 3 4 5 +(integer) 3 +redis> SUNIONSTORE set3 set1 set2 +(integer) 5 +redis> SMEMBERS set3 +1) "1" +2) "2" +3) "3" +4) "4" +5) "5" + +``` + +#### SDIFF + +`SDIFF`命令用于获取指定集合(即第一个参数指定)与其它集合的差集。需要注意的是并非获取全部集合的差集,二者存在部分差异。若集合不存在将被视为空集合。 + +``` +SDIFF key [key ...] + +``` + +官方文档中给出了下列示例: + +``` +key1 = {a,b,c,d} +key2 = {c} +key3 = {a,c,e} +SDIFF key1 key2 key3 = {b,d} + +``` + +###### 示例 + +``` +redis> SADD set1 1 2 3 +(integer) 3 +redis> SADD set2 3 4 5 +(integer) 3 +redis> SDIFF set1 set2 +1) "1" +2) "2" + +``` + +#### SDIFFSTORE + +`SDIFFSTORE`命令与`SDIFF`命令相似,区别在于`SDIFFSTORE`命令不直接返回其差集,而是保存到`destination`参数指定的集合中,并返回结果的数量。若`destination`参数指定的集合已存在将会被覆盖。 + +``` +SDIFFSTORE destination key [key ...] + +``` + +###### 示例 + +``` +redis> SADD set1 1 2 3 +(integer) 3 +redis> SADD set2 3 4 5 +(integer) 3 +redis> SDIFFSTORE set3 set1 set2 +(integer) 2 +redis> SMEMBERS set3 +1) "1" +2) "2" +``` diff --git "a/_posts/2022-02-01-Redis\357\274\232Redis\344\275\277\347\224\250\346\263\250\346\204\217\344\272\213\351\241\271.md" "b/_posts/2022-02-01-Redis\357\274\232Redis\344\275\277\347\224\250\346\263\250\346\204\217\344\272\213\351\241\271.md" new file mode 100644 index 00000000000..4db465b7ef3 --- /dev/null +++ "b/_posts/2022-02-01-Redis\357\274\232Redis\344\275\277\347\224\250\346\263\250\346\204\217\344\272\213\351\241\271.md" @@ -0,0 +1,139 @@ +--- +layout: post +title: Redis:Redis 使用注意事项 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - Redis +--- + + + + +#### 修改 redis.conf 前先做备份 +- 修改之前先备份一份默认的初始化配置文件,以备不时之需。 + + +#### Redis 字符串 +- 最大字符串为512M,但是生产环境一般不用大字符串。 + + +#### Redis 添加元素 +- 江湖规矩一般从左端Push,右端Pop,即LPush/RPop。 + + +#### set 无序集合 +- set 无序集合,主要应用于 ```去重、过滤```; +- SPOP: + -- spop + +#### 从集合 ```随机移除一个成员,并将其返回```; +- SREM: + -- srem + +#### 在name对应的集合中 ```删除某些值```; +- SCARD: + -- scard 获取name对应的集合中 ```元素个数```; +- SDIFF: + -- sdiff 在第一个name对应的集合中且不在其他name对应的集合的元素集合 +- SDIFFSTORE: + -- sdiffstore  + +#### 获取第一个name对应的集合中且不在其他name对应的集合,再将其新加入到dest对应的集合中 +- SINTER: + -- sinter + +#### 获取多个name对应集合的并集 +- SINTERSTORE: + -- sinterstore + +#### 获取多一个name对应集合的并集,再讲其加入到dest对应的集合中 +- SISMEMBER: + -- sismember + +#### 检查value是否是name对应的集合的成员 +- SMEMBERS: + -- smembers + +#### 获取name对应的集合的所有成员 +- SMOVE: + -- smove + +#### 将某个成员从一个集合中移动到另外一个集合 +- SRANDMEMBER: + --srandmember + +#### 从name对应的集合中 ```随机获取 numbers 个元素```,不会删除 +- SUNIONSTORE: + --sunionstore + +#### 获取多一个name对应的集合的并集,并将结果保存到dest对应的集合中 +- SSCAN: + --sscan + +#### 同字符串的操作,用于 ```增量迭代分批获取元素```,避免内存消耗太大 + + +#### zset 有序集合 +- zset 有序集合,主要应用于 ```排名、排序```; + + +#### Redis 设置密码 +- 在 redis 文件夹打开 ```conf 配置文件``` ,查找 ```requirepass foobared```,找到后将这一行注释掉,在下方添加 ```requirepass XXX(你要设置的密码)```; +- 重启 redis 密码即可生效; + + +#### Redis 服务化运行 +- 以 Windows 为例,cmd 命令行进入 redis 文件夹,执行下面的命令即可安装 redis 服务: +``` +redis-server --service-install redis.windows-service.conf --loglevel verbose +``` +- 启动 redis 服务 +``` +redis-server --service-start +``` +- 关闭 redis 服务 +``` +redis-server --service-stop +``` + + +#### Redis 设置远程连接 +在 Redis 文件夹打开 ```.conf``` 配置文件; +查找 ```bind 127.0.0.1```,找到后将这一行注释掉,在下方添加 ```bind 0.0.0.0```; +查找 ```protected-mode yes```,找到活将这一行注释掉,在下方添加 ```protected-mode no```; +重启 redis 即开启远程访问; + + +#### redis 数据持久化策略 +- RDB +- + + +#### redis 内存空间设置 +- 进入 redis 文件夹,打开 ```.conf``` 配置文件; +- 查找 ```maxmemory```,找到后修改 ```maxmemory``` 后面的数值,例如:```2000mb``` 表示 ```2G```; + + +#### redis 最大连接数 + + +#### redis key 超时设置 +- 当client主动访问key会先对key进行超时判断,过时的key会立刻删除。如果clien永远都不再get那条key呢? 它会在Master的后台,每秒10次的执行如下操作: 随机选取100个key校验是否过期,如果有25个以上的key过期了,立刻额外随机选取下100个key(不计算在10次之内)。可见,如果过期的key不多,它最多每秒回收200条左右,如果有超过25%的key过期了,它就会做得更多,但只要key不被主动get,它占用的内存什么时候最终被清理掉只有天知道。在主从复制环境中,由于上述原因存在已经过期但是没有删除的key,在主snapshot时并不包含这些key,因此在slave环境中我们往往看到dbsize较master是更小的。 + + +#### redis 出现异常时的持久化配置 +- 默认情况下,redis 出现异常时就会中断持久化写入操作,```.conf``` 配置文件中配置项 ```stop-writes-on-bgsave-error``` 默认设置为 ```yes```,表示 ```redis 遇到错误时终止写入```; +- 这种设置下,当 redis 出现异常的时候,就会出现如下报错: +```json +(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on + disk. Commands that may modify the data set are disabled. Please check Redis logs for details +about the error. +``` +- 解决这个报错有两种思路: + -- 第一种是 ```回溯分析 redis 到底出现了什么问题,从而在应用层面予以解决```,但这会影响应用的执行,如果问题没有查找清楚这个报错就会一直阻断程序运行; + -- 第二种是 打开 ```conf 配置文件```,修改 ```stop-writes-on-bgsave-error``` 为 ```no```,之后重启 redis,这可以看做是一种临时解决方案,好处是不会影响程序的执行; +- 最好是在配置 redis 的时候,就在 conf 配置文件中把 ```stop-writes-on-bgsave-error``` 的值修改为 ```no```,毕竟应用程序中断是最不愿意看到的情况。 diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232Hook \346\212\200\346\234\257\345\216\237\347\220\206\344\273\245\345\217\212\345\234\250 JS \351\200\206\345\220\221\344\270\255\347\232\204\347\233\270\345\205\263\345\272\224\347\224\250.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232Hook \346\212\200\346\234\257\345\216\237\347\220\206\344\273\245\345\217\212\345\234\250 JS \351\200\206\345\220\221\344\270\255\347\232\204\347\233\270\345\205\263\345\272\224\347\224\250.md" new file mode 100644 index 00000000000..0c1d9aead13 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232Hook \346\212\200\346\234\257\345\216\237\347\220\206\344\273\245\345\217\212\345\234\250 JS \351\200\206\345\220\221\344\270\255\347\232\204\347\233\270\345\205\263\345\272\224\347\224\250.md" @@ -0,0 +1,347 @@ +--- +layout: post +title: JS逆向:Hook 技术原理以及在 JS 逆向中的相关应用 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + +## Hook 技术原理 + +Hook 是一种钩子技术,在系统没有调用函数之前,钩子程序就先得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,也可以强制结束消息的传递。 +简单来说,修改原有的 JS 代码就是 Hook。 + +#### Hook 技术之所以能够实现有两个条件: + +- 客户端拥有 JS 的最高解释权,可以决定在任何时候注入 JS,而服务器无法阻止或干预。服务端只能通过检测和混淆的手段,另 Hook 难度加大,但是无法直接阻止。 +- 除了上面的必要条件之外,还有一个条件。就是 JS 是一种弱类型语言,同一个变量可以多次定义、根据需要进行不同的赋值,而这种情况如果在其他强类型语言中则可能会报错,导致代码无法执行。js 的这种特性,为我们 Hook 代码提供了便利。 + +#### JS 作用域问题 1:自执行函数的 Hook 问题 + +JS 变量是有作用域的,**只有当被 hook 函数和 debugger 断点在同一个作用域的时候,才能 hook 成功**。 +对于下面的自执行函数,在执行完之后我们实际上是无法 hook test 函数的。因为 test 是在自执行函数的作用域,而不是在全局作用域。而此时,自执行函数已经执行完了,test 函数已经被内存清空无法 hook。 +``` +!(function(){ + var arg = 1; + var test = function(){ + console.log(arg); + } +})() +debugger; +在此处 hook test(); +``` +争取的写法应该是下面这样,这样当程序被断下来以后才能 hook test 函数。 +``` +!(function(){ + var arg = 1; + var test = function(){ + console.log(arg); + } +debugger; +})() +``` + +#### JS 作用域问题 2:局部变量污染全局变量 + +在 Hook 的时候要注意一个 JS 代码的特性,就是 JS 在函数赋值的时候,会遵循一个原则: +>**当前作用域有变量则赋值该变量,当前作用域没有该变量则赋值在全局作用域定义该变量并赋值**。 + +比如: +``` +var arg1 = null; +function test1(){ + arg1 = 1; // 注意这里没有 var +}; +function test2(){ + console.log(arg1) +}; +test1(); +test2(); +``` +上述代码的执行结果 `test2();` 打印出 `1`,而不是 `null`。只有当 `test1` 函数中得赋值语句是 `var arg1 = 1;` 的时候,才会 返回 `null` 而不是 `1`。 + +#### JS 作用域问题 3:this 的指向问题 + +在不同的作用域中,同样的变量指向是不一样的。每个函数在定义被解析器解析时,都会创建两个特殊变量: this 和 arguments 。 每个函数都有属于自己的 `this` 对象, 这个 `this` 对象是在运行时基于函数的执行环境绑定的。 + +在 Hook 的时候,`this` 的指向遵循下面的原则: +>全局作用域中,`this = window`; +方法作用域中,`this = 调用者`; +在浅滩函数中,`this = 调用外部函数或内部函数的执行环境对象`; +在类方法里面,`this = 类自己`; + +总结来说就是:**谁调用这个函数对象,this就指向谁(this指向的是调用执行函数的那个对象)**。 +在 JS 中,如果需要改变 this 的指向,可以通过使用 `call()` 和 `apply()` 改变函数执行环境的情况,以改变this 指向。 +具体可以参考文章《 [call/apply以及this指向的理解](https://www.cnblogs.com/yangshifu/p/9761388.html)》,问占中有详细的解释。 + +## Hook 的实现 + +Hook 实现有两种方式,一种是直接替换函数,另一种是 `Object.defineProperty` 通过为对象的属性赋值的方式进行 Hook。 + +**两种方式的区别**: +- 函数 hook,一般不会 hook 失败,除非 __proto__ 模拟的不好会被检测到。 +- 属性 hook,当网站所有的逻辑都采用 `Object.defineProperty` 绑定时,属性 hook 就会失效。同时, `Object.defineProperty` 无法进行二次 Hook。 + +从日常的实际应用层面来说,上面的问题并不需要过度关注。需要注意的是,第一种方式简单、但是太粗暴,容易影响原有代码的正常执行,也容易被检测到,而第二种方式会更优雅一些,具体需要结合具体需求选择合适的 Hook 方式。 +- 方法一:直接替换原有函数。 + +``` +old_func = 被 hook 函数 +被 hook 函数 = function(arguments){ + if 判断条件: + my_task; + return old_func.apply(arguments) +} +func.prototype.xxx = xxxx +``` + +- 方法二:Object.defineProperty 为对象的属性赋值。 + +``` +odl_attr = obj.attr +Object.defineProperty(obj, 'attr', { + get: function(){ + debugger; + if 判断条件: + my_task; + return old_attr + }, + set: function(val){ + debugger; + if 判断条件: + my_task; + return 自定义内容 + } +}) +``` + + +## Hook 的应用 + +#### Hook http请求 + +http请求包括 ajax、src、href、表单等。 +``` +// 代码来源:https://www.cnblogs.com/amiezhang/p/9984690.html + +function hookAJAX() { + XMLHttpRequest.prototype.nativeOpen = XMLHttpRequest.prototype.open; + var customizeOpen = function (method, url, async, user, password) { + // do something + this.nativeOpen(method, url, async, user, password); + }; + + XMLHttpRequest.prototype.open = customizeOpen; + } + + /** + *全局拦截Image的图片请求添加token + * + */ + function hookImg() { + const property = Object.getOwnPropertyDescriptor(Image.prototype, 'src'); + const nativeSet = property.set; + + function customiseSrcSet(url) { + // do something + nativeSet.call(this, url); + } + Object.defineProperty(Image.prototype, 'src', { + set: customiseSrcSet, + }); + } + + /** + * 拦截全局open的url添加token + * + */ + function hookOpen() { + const nativeOpen = window.open; + window.open = function (url) { + // do something + nativeOpen.call(this, url); + }; + } + + function hookFetch() { + var fet = Object.getOwnPropertyDescriptor(window, 'fetch') + Object.defineProperty(window, 'fetch', { + value: function (a, b, c) { + // do something + return fet.value.apply(this, args) + } + }) + } +// 代码来源:https://www.cnblogs.com/amiezhang/p/9984690.html +``` + +#### 修改返回的 response,干扰 JS 原有代码的运行。 + +在处理无限反 debugger 的时候,常用的一种方法就是 Fiddler + AutoResponse,然后删除 js 代码中的 debugger,或者直接修改成本地代码,都是在浏览器接收服务器返回的资源的时候,基于 Hook 思想完成的。 +详细参见:《[Fiddler + AutoResponder 篡改 js 破解企查查无限 debugger 问题](https://dex0423.github.io/2022/01/27/Fiddler-+-AutoResponder-%E7%AF%A1%E6%94%B9-js-%E7%A0%B4%E8%A7%A3%E4%BC%81%E6%9F%A5%E6%9F%A5%E6%97%A0%E9%99%90-debugger-%E9%97%AE%E9%A2%98/)》。 + +#### 修改常用变量、函数,方便进行加密函数定位。 + +比如,如果推测加密过程中用到了 md5 加密,那么通过 Hook `md5` 函数,在其中打 debugger 断点的方式,就可以很快定位加密函数的位置。 + +#### 导出加密函数的参数、结果值。 + +通过定义全局变量、window属性等方式,导出加密函数中的参数或者结果值。 + +#### cookie钩子:查找 cookie 生成入口 + +打 `script` 断点,在 `js` 刚运行时就把网页断住。 +在 console 中输入下面的代码。 +``` +document.cookie_bak = document.cookie +Object.defineProperty(document, 'doockie',{ + get: function(){ + debugger; + return document.cookie_bak; + }, + set: function(val){ + debugger; + return; + } +}) +``` + +#### cookie钩子:用于定位cookie中关键参数生成位置 + +当cookie中匹配到了 `目标cookie字符串`, 则插入断点。 + +``` +(function () { + 'use strict'; + var cookieTemp = ''; + Object.defineProperty(document, 'cookie', { + set: function (val) { + if (val.indexOf('目标cookie字符串') != -1) { + debugger; + } + console.log('Hook捕获到cookie设置->', val); + cookieTemp = val; + return val; + }, + get: function () { + return cookieTemp; + }, + }); +})(); +``` + +#### 请求钩子 + +用于定位请求中关键参数生成位置。 +``` +(function () { + 'use strict'; + var cookieTemp = ''; + Object.defineProperty(document, 'cookie', { + set: function (val) { + if (val.indexOf('关键参数') != -1) { + debugger; + } + console.log('Hook捕获到关键参数设置->', val); + cookieTemp = val; + return val; + }, + get: function () { + return cookieTemp; + }, + }); +})(); +``` + +#### header钩子:用于定位header中关键参数生成位置 + +``` +TODO +``` + +#### 自己写插件 + +插件 js 文件,inject.js +``` +// hook 代码 +// 略 +``` +插件的配置文件:manifest.json +``` +{ + "name": "Injection", + "version": "2.0", + "description": "RequestHeader钩子", + "manifest_version": 2, + "content_scripts": [ + { + "matches": [ + "" + ], + "js": [ + "inject.js" + ], + "all_frames": true, + "permissions": [ + "tabs" + ], + "run_at": "document_start" + } + ] +} +``` + +#### 将无限 debugger 方法直接置空 + +``` +debugger函数 = function(){}; +``` + +#### Hook eval 绕过无限 debugger + +``` +eval_bak = eval +eval = function(val){ + debugger; + return eval_bak(val); +} +eval.toString = function(){ + return "function eval() { [native code] }" +} +``` + +#### 干掉定时器类触发的无限 debugger + +``` +for (var i = 1; i < 99999; i++)window.clearInterval(i); +``` + +#### 利用 Hook 将 eval 函数替换成 console.log + +当 js 代码中有 `eval` 函数执行了某种操作,逆向过程中需要分析操作的具体内容时,可以通过 Hook eval 替换成 console.log 的方式,将 `eval` 执行的代码打印出来。 +``` +eval_bak = eval; +eval = console.log; +``` + +## 风控如何检测 Hook + +#### toString 检测识别 Hook + +toString 检测,指的是风控通过检测被 Hook 的函数 `toString()` 结果是否变化,来判断该函数是否被 Hook 的一种检测方法。当风控监测到 Hook 以后,可以返回假数据误导逆向工程师,也可以配合内存爆破进行反 debugger。 +比如我们 Hook 了 `eval` 函数,这时风控就可以通过检测 eval.toString() 的返回值是否是 `"function eval() { [native code] }"` 来识别该函数是否被 Hook 了。 + +#### 如何解决 toString 检测 +如何解决 `toString` 检测呢?其实方法很简单,仍然需要使用 Hook 技术。我们只需要修改目标函数的 `toString` 方法。 +``` +eval.toString = function(){ + return "function eval() { [native code] }" +} +``` + +#### 检测原型链 +TODO \ No newline at end of file diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232JS \344\270\255\345\270\270\350\247\201\347\232\204\345\212\240\345\257\206\347\256\227\346\263\225\345\217\212\351\200\206\345\220\221\347\211\271\345\276\201.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232JS \344\270\255\345\270\270\350\247\201\347\232\204\345\212\240\345\257\206\347\256\227\346\263\225\345\217\212\351\200\206\345\220\221\347\211\271\345\276\201.md" new file mode 100644 index 00000000000..251ac734520 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232JS \344\270\255\345\270\270\350\247\201\347\232\204\345\212\240\345\257\206\347\256\227\346\263\225\345\217\212\351\200\206\345\220\221\347\211\271\345\276\201.md" @@ -0,0 +1,80 @@ +--- +layout: post +title: JS逆向:JS 中常见的加密算法及逆向特征 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +## 1. 取盐算法 + +**取盐** 算法,也叫 **摘要算法**,是对数据进行一系列运算后,截取一部分关键值进行校验。因此运算过程 **不可逆**,无法还原出加密前的 **初始文本**。 +取盐算法得到的结果长度一般是固定的,无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。一般地,只要输入的文本不同,对其进行摘要以后产生的摘要消息也必不相同,但相同的文本输入必会产生相同的输出。 + +#### 1.1. MD5 逆向特征 + +- 字符串为 16 进制,即数字英文组合,而且 英文最大是字母 f 。 +- 位数为 16、32。 +- 搜索关键字: + -- 关键词:`md5`、`MD5` + -- 默认的 key 值:```0123456789abcdef、0123456789ABCDEF``` + -- 原始MD5的魔法值(16进制):```0x67452301、0xefcdab89、0x98badcfe、0x10325476``` + -- 原始MD5的魔法值(10进制):```1732584193、271733879、1732584194、271733878``` +- `123456` 计算结果值: + **16 位**,结果值 **49** 开头。 + -- 16位小写计算结果:`49ba59abbe56e057` + -- 16位大写计算结果:`49BA59ABBE56E057` + **32 位**,结果值 **e10、E10** 开头。 + -- 32位小写计算结果:`e10adc3949ba59abbe56e057f20f883e` + -- 32位小写计算结果:`E10ADC3949BA59ABBE56E057F20F883E` + +>注意:**16位** 的结果值是 **32位** 的结果值的一部分。 + +#### 1.2. SHA 逆向特征 + +- 字符串为 16 进制,即数字英文组合,而且 英文最大是字母 f 。 +- 位数为 40、64、96、128 等,**位数均是 `8` 的倍数**。 +- `123456` 计算结果值: + **SHA1:40 位**:`7c4a8d09ca3762af61e59520943dc26494f8941b` + **SHA256:64 位**:`8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92` + **SHA384:96 位**:`0a989ebc4a77b56a6e2bb7b19d995d185ce44090c13e2984b7ecc6d446d4b61ea9991b76a4c2f04b1b4d244841449454` + **SHA512:128 位**: + `ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413` + +## 2. 可还原加密算法 + +>**特征:** +>1、字符串为 **16 进制**,即数字英文组合,而且 **英文最大是字母 `f`** 。 +>2、字符串为 **base64** 编码形式,由数字 `0-9`、小写字母 `a-z`、大写字母 `A-Z` 以及字符 `+`、`_`、`=` 组成,且最后一个或最后两个字符为 `=`。通常而言 Base64 的识别特征为索引表,当我们能找到 `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/` 这样索引表,再经过简单的分析基本就能判定是 Base64 编码。 + +#### 2.1. RSA 逆向特征 + +RSA 是典型的 **非对称加密**,拥有一个公钥和一个私钥。 + +>其中: +-- **客户端(浏览器)拥有公钥,服务器同时拥有公钥和私钥**。 +-- 同一个明文可以生成不同的密文。 +-- 必须先 `new JSEncrypt` 加密对象,然后 `setPublicKey`。 +- 加密后的数据长度不可能是 `8` 的倍数。 +- 搜索关键词:`new JSEncrypt`、`setpublickey`、`ABCDEFG`、`abcdefg`。 + +#### 2.2. AES 逆向特征 + +AES 是 **对称加密** 的一种。 +- 一般AES加密出来的数据是128 或 256 的整倍数。 +- 搜索关键词:`cryptojs.aes`、`encryptedString`、`010001`(或类似二进制模值)。 + +#### 2.3. DES 逆向特征 +- 搜索关键词:`cryptojs.des.encrypt`。 + +## 其他编码算法 + +#### 3.1. Base64 逆向特征 + +- 字符串的长度为4的整数倍。 +- 字符串的符号取值只能在 `A-Z, a-z, 0-9, +, /, =` 共计65个字符中,且 `=` 如果出现就必须在结尾出现。 \ No newline at end of file diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232JS \351\200\206\345\220\221\346\226\271\345\274\217\347\240\264\350\247\243\346\237\220\351\252\214\346\273\221\345\235\227\351\252\214\350\257\201\347\240\201.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232JS \351\200\206\345\220\221\346\226\271\345\274\217\347\240\264\350\247\243\346\237\220\351\252\214\346\273\221\345\235\227\351\252\214\350\257\201\347\240\201.md" new file mode 100644 index 00000000000..da8b5942363 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232JS \351\200\206\345\220\221\346\226\271\345\274\217\347\240\264\350\247\243\346\237\220\351\252\214\346\273\221\345\235\227\351\252\214\350\257\201\347\240\201.md" @@ -0,0 +1,352 @@ +--- +layout: post +title: JS逆向:JS 逆向方式破解某验滑块验证码 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + + +## 1. 抓包分析 + +打开某验的 demo,点出验证码图片,分析 network 中的请求以及参数变化 +请求与参数: +``` +1、 Request URL: https://www.geetest.com/demo/gt/register-slide?t=1640096834809 + 返回: challenge: "5bd76b0b1c9388a667bba39af5cfd71e" + gt: "019924a82c70bb123aae90d483087f94" + +2、 Request URL: https://apiv6.geetest.com/gettype.php?gt=019924a82c70bb123aae90d483087f94&callback=geetest_1640096837076 + 返回: fullpage: "/static/js/fullpage.9.0.8.js" + slide: "/static/js/slide.7.8.6.js" + +3、 Request URL: https://apiv6.geetest.com/get.php?gt=019924a82c70bb123aae90d483087f94&challenge=5bd76b0b1c9388a667bba39af5cfd71e&lang=zh-cn&pt=0&client_type=web&w=7(Pr5uTDzYQIrCym5Psn(W7fvv7K3ji2dFDhp... + 提交: gt: 019924a82c70bb123aae90d483087f94 + challenge: 5bd76b0b1c9388a667bba39af5cfd71e + w: 7(Pr5uTDzYQIrCym5Psn(W7fvv7K3ji2dFDhpCxfIsWhd... + 返回: status: "success" + +4、 Request URL: https://api.geetest.com/ajax.php?gt=019924a82c70bb123aae90d483087f94&challenge=5bd76b0b1c9388a667bba39af5cfd71e&lang=zh-cn&pt=0&client_type=web&w=7ZMNp(76n(MlO6aTxrUCxNw(7Jj5kEFnmodPZj2Nid... + 提交: w: 7ZMNp(76n(MlO6aTxrUCxNw(7Jj5kEFnmodPZj2NidffUg3PXYPHS(65Up0jO2X... + callback: geetest_1640096847867 + +5、 Request URL: https://api.geetest.com/get.php?is_next=true&type=slide3>=019924a82c70bb123aae90d483087f94&challenge=5bd76b0b1c9388a667bba39af5cfd71e&lang=zh-cn&https=true&protocol=https%3A%2F%2F&offline=false&product=embed&api_server=api.geetest.com&isPC=true&autoReset=true&width=100%25&callback=geetest_1640096847383 + 提交: gt: 019924a82c70bb123aae90d483087f94 + challenge: 5bd76b0b1c9388a667bba39af5cfd71e + 返回: fullbg: "pictures/gt/b9694f3e8/b9694f3e8.jpg" + slice: "pictures/gt/b9694f3e8/slice/f35f84584.png" + bg: "pictures/gt/b9694f3e8/bg/f35f84584.jpg" +``` +拖动滑块,完成验证,继续分析请求和参数。 +``` +6、 Request URL: https://api.geetest.com/ajax.php?gt=019924a82c70bb123aae90d483087f94&challenge=5bd76b0b1c9388a667bba39af5cfd71eaj&lang=zh-cn&%24_BBF=0&client_type=web&w=(x5U5)0n0zt1KeeU1X7ZseyXl)fhHOh539)Bm2(6... + 提交: w: (x5U5)0n0zt1KeeU1X7ZseyXl)fhHOh539)Bm2(6bQrrmShGp9v27nhkmGV... + gt: 019924a82c70bb123aae90d483087f94 + challenge: 5bd76b0b1c9388a667bba39af5cfd71eaj + + 返回: message: "success" + validate: "00c14c420b44ade8cd6d3ddd5c916010" +``` + +>需要明确的问题: +1、还原底图(处理底图乱序问题); +2、获取 w 值,生成轨迹; + +## 2. 还原底图 + +- 通过分析页面元素,我们发现验证码是一个 canvas。 + +![]({{site.baseurl}}/img-post/geetcode-1.png) + + +- 我们打上 canvas 断点,然后刷新重新获取验证码。 + +![]({{site.baseurl}}/img-post/geetcode-2.png) +![]({{site.baseurl}}/img-post/geetcode-3.png) + +- 此时注意提示 `https://static.geetest.com/pictures/gt/b9694f3e8/b9694f3e8.webp` 打开以后就是乱序验证码, `e = canvas.geetest_canvas_bg.geetest_absolute {width: 260, height: 160, title: '', toDataURL: ƒ, toBlob: ƒ, …}` 的就是验证码的宽度和高度。 +同时,仔细观察我们发现,验证码混淆后分成了上下两部分。 + +![]({{site.baseurl}}/img-post/geetcode-4.png) + +- 单步调试,集合验证码图片、分析代码; + +![]({{site.baseurl}}/img-post/geetcode-5.png) + +根据提示我们发现,有一段代码的提示是 `l = ImageData {data: Uint8ClampedArray(3200), width: 10, height: 80, colorSpace: 'srgb'}, o = CanvasRenderingContext2D {canvas: canvas, globalAlpha: 1`,推测 `ImageData` 是画图动作,进而推测验证码还原的关键步骤,在 `l = o[$_CJET(69)](c, u, 10, a)` 这一步。 +我们仔细分析一下这一段 for 循环代码。 +``` + for (var a = r / 2, _ = 0; _ < 52; _ += 1) { + var c = Ut[_] % 26 * 12 + 1 + , u = 25 < Ut[_] ? a : 0 + , l = o[$_CJET(69)](c, u, 10, a); + s[$_CJET(66)](l, _ % 26 * 10, 25 < _ ? a : 0); + } +``` +对上面的代码进一步分析,我们发现这里进行了 **52** 次循环,而代码中的 **26** 刚好是 52 的一半,这个与前面验证码被分成上下两部分呼应。 +我们推测验证码被分成了52个小块,上下两个部分各有26,每个小块是的宽度是 10,加在一起正好是 260,刚好是前面分析发现的整个验证码图片的宽度。 + +![]({{site.baseurl}}/img-post/geetcode-6.png) + + +仔细分析上面的代码,我们发现这个循环,会根据 `Ut[_]` 的变化而进行不同的操作。`_` 是一个每次自增1且小于52的整数,那 `Ut` 是什么呢? +我们在 console 中获取,发现他是一个52位的数组,` [39, 38, 48, 49, 41, 40, 46, 47, 35, 34, 50, 51, 33, 32, 28, 29, 27, 26, 36, 37, 31, 30, 44, 45, 43, 42, 12, 13, 23, 22, 14, 15, 21, 20, 8, 9, 25, 24, 6, 7, 3, 2, 0, 1, 11, 10, 4, 5, 19, 18, 16, 17]`。 +这个时候,就可以断定这个数组就是验证码混淆的顺序,按照这个顺序反向操作就能还原验证码。而上面的 for 循环,实际就是在执行这个还原验证码的操作。 +>**注意**: +这个数组可能是动态的,也可能是静态的,为了验证是静态还是动态,我们需要进行多次调试,对比该数组是否发生变化。这里我们通过分析,确认该数组是静态的。 + +至此,底图还原的逆向工作完成。 + +## 3. 获取 w 值 & 还原推动轨迹 + +#### 3.1. 获取 w 值 + +w 值在这里曾经多次生成,这里只分析其中的一次。 +w 是一个数字单字符,这里通过直接搜索无法直接搜到。这里我们采用跟栈的方式一步步回溯。 + +#### 跟栈技巧 + +跟栈的时候,注意我们的重点关注 w 值有没有在提示中出现,如果又出现就说明它在代码之前或者上一步,就已经被生成了。我们就继续往前找,一直找到提示中没有 w 值的时候,我们再分析 w 值到底是在哪里生成的。 +>注意:在开始跟栈的时候,如果找不到 w 值,就往前多跟几步。 + +![]({{site.baseurl}}/img-post/geetcode-7.png) + +继续上面的思路往前跟栈,很快就会发现遇到一个平坦流,如下图: + +![]({{site.baseurl}}/img-post/geetcode-8.png) + +#### 平坦流的跟栈技巧 + +当遇到平坦流的时候,首先调试分析平坦流有没有对目标参数做修改,如果没有就不用关注控制流,直接跳过就好。 +很多时候,我们可以直接跳过平坦流,继续往前找,看看能不能找到 w 值,如果实在找不到再回来分析平坦流。这里其实可以先在函数内部、平坦流前打一个断点,然后刷新货重新请求、让浏览器在此处被断掉,继而进行跟栈分析。 +分析发现,w 值在 `R` 这层已经出现,那我们就没必要关注之前的平坦流,接着跟栈分析就可以。如下图: + +![]({{site.baseurl}}/img-post/geetcode-9.png) + +我们接着进行跟栈分析,发现调试工具已经把 `"w"` 的生成逻辑都提示出来了。 + +![]({{site.baseurl}}/img-post/geetcode-10.png) + +w 值的生成代码就是下面这行。 +``` + "\u0077": h + u +``` +到这一步,就进入你想的关键步骤了。 +下面附上比较关键的一段加密代码,供大家参考。 +``` + var rt = function() { + var $_BFBDL = lTloj.$_CX + , $_BFBCi = ['$_BFBGO'].concat($_BFBDL) + , $_BFBEt = $_BFBCi[1]; + $_BFBCi.shift(); + var $_BFBFk = $_BFBCi[0]; + function t() { + var $_DBFAh = lTloj.$_DP()[0][4]; + for (; $_DBFAh !== lTloj.$_DP()[2][3]; ) { + switch ($_DBFAh) { + case lTloj.$_DP()[0][4]: + return (65536 * (1 + Math[$_BFBDL(75)]()) | 0)[$_BFBDL(396)](16)[$_BFBDL(476)](1); + break; + } + } + } + return function() { + var $_BFBIl = lTloj.$_CX + , $_BFBHs = ['$_BFCBY'].concat($_BFBIl) + , $_BFBJq = $_BFBHs[1]; + $_BFBHs.shift(); + var $_BFCAQ = $_BFBHs[0]; + return t() + t() + t() + t(); + } + ; + }(); +``` +#### 3.2. 轨迹还原 + +当分析到前面代码处位置时,我们发现其中一个很重要的参数 `o`,这个 `o` 是一个对象,对象中其中包括了下面的内容。 +``` +aa: "M/-821./0(!!Mty!)(!)!)!)!K)(!)ysttststsss(!!(f011/11x1111/19111o6,S/$)ZC" +ep: {v: '7.8.6', $_BHR: false, me: true, tm: {…}, td: -1} +imgload: 813 +lang: "zh-cn" +passtime: 337 +rp: "a110d6f451e2b042883f4ae191a89272" +userresponse: "99e999e71" +xb3y: "1140037174" +[[Prototype]]: Object +``` +通过多次调试,我们发现这里的 `aa` 和 `rp` 值,会随着每次拖动滑块验证码而变化,我们猜测这里可能是滑动的轨迹。通过想上查找和跟栈,我们找到了这个地方: + +![]({{site.baseurl}}/img-post/geetcode-11.png) + +``` +l = n[$_CJJIW(1078)][$_CJJJd(1069)](n[$_CJJIW(1078)][$_CJJJd(1051)](), n[$_CJJJd(13)][$_CJJIW(1045)], n[$_CJJIW(13)][$_CJJIW(307)]) +``` +通过还原,得到以下代码: +``` +l = n['$_CIBw']['$_BBCA'](n['$_CIBw']['$_GEy'](), n['$_CIY']['c'], n['$_CIY']['s']) +``` +通过 console 控制台,我们分析得到下面的内容: +``` +n['$_CIBw']['$_GEy']() : 'T,0./53/220-,(!!Mty*)))*)))(y((t)tssssswssssswsssssvvsssswsss(!!($)39/112011100120112.9191/:8Fp/01Ei:-:901C3/_$)J$*r' +n['$_CIY']['c'] : [12, 58, 98, 36, 43, 95, 62, 15, 12] +n['$_CIY']['s'] : '6f404c79' +``` +那上面的数据都是从哪里拿到的呢?我们将 `n['$_CIY']` 还原,得到下面的内容: + +![]({{site.baseurl}}/img-post/geetcode-12.png) + +通过分析,可以知道 `n['$_CIY']['c']`、`n['$_CIY']['s']` 是前面通过请求返回的,可以直接拿来使用。 +这里比较重要的是 `n['$_CIBw']['$_GEy']() `,我们进入函数找到下面这段代码。 +``` +"\u0024\u005f\u0047\u0045\u0079": function() { + var $_BEGIH = lTloj.$_CX + , $_BEGHL = ['$_BEHBT'].concat($_BEGIH) + , $_BEGJJ = $_BEGHL[1]; + $_BEGHL.shift(); + var $_BEHAr = $_BEGHL[0]; + function n(t) { + var $_DBEIz = lTloj.$_DP()[0][4]; + for (; $_DBEIz !== lTloj.$_DP()[2][3]; ) { + switch ($_DBEIz) { + case lTloj.$_DP()[0][4]: + var e = $_BEGJJ(430) + , n = e[$_BEGJJ(182)] + , r = $_BEGIH(33) + , i = Math[$_BEGIH(383)](t) + , o = parseInt(i / n); + n <= o && (o = n - 1), + o && (r = e[$_BEGIH(122)](o)); + var s = $_BEGIH(33); + return t < 0 && (s += $_BEGJJ(456)), + r && (s += $_BEGJJ(459)), + s + r + e[$_BEGIH(122)](i %= n); + break; + } + } + } + var t = function(t) { + var $_BEHDi = lTloj.$_CX + , $_BEHCK = ['$_BEHGM'].concat($_BEHDi) + , $_BEHEF = $_BEHCK[1]; + $_BEHCK.shift(); + var $_BEHFx = $_BEHCK[0]; + for (var e, n, r, i = [], o = 0, s = 0, a = t[$_BEHEF(182)] - 1; s < a; s++) + e = Math[$_BEHEF(156)](t[s + 1][0] - t[s][0]), + n = Math[$_BEHDi(156)](t[s + 1][1] - t[s][1]), + r = Math[$_BEHDi(156)](t[s + 1][2] - t[s][2]), + 0 == e && 0 == n && 0 == r || (0 == e && 0 == n ? o += r : (i[$_BEHEF(140)]([e, n, r + o]), + o = 0)); + return 0 !== o && i[$_BEHDi(140)]([e, n, o]), + i; + }(this[$_BEGJJ(361)]) + , r = [] + , i = [] + , o = []; + return new ct(t)[$_BEGIH(84)](function(t) { + var $_BEHIs = lTloj.$_CX + , $_BEHHw = ['$_BEIBE'].concat($_BEHIs) + , $_BEHJy = $_BEHHw[1]; + $_BEHHw.shift(); + var $_BEIAO = $_BEHHw[0]; + var e = function(t) { + var $_BEIDv = lTloj.$_CX + , $_BEICk = ['$_BEIGW'].concat($_BEIDv) + , $_BEIEU = $_BEICk[1]; + $_BEICk.shift(); + var $_BEIFh = $_BEICk[0]; + for (var e = [[1, 0], [2, 0], [1, -1], [1, 1], [0, 1], [0, -1], [3, 0], [2, -1], [2, 1]], n = 0, r = e[$_BEIDv(182)]; n < r; n++) + if (t[0] == e[n][0] && t[1] == e[n][1]) + return $_BEIEU(413)[n]; + return 0; + }(t); + e ? i[$_BEHIs(140)](e) : (r[$_BEHJy(140)](n(t[0])), + i[$_BEHJy(140)](n(t[1]))), + o[$_BEHJy(140)](n(t[2])); + }), + r[$_BEGJJ(444)]($_BEGIH(33)) + $_BEGIH(407) + i[$_BEGIH(444)]($_BEGJJ(33)) + $_BEGIH(407) + o[$_BEGIH(444)]($_BEGIH(33)); + } +``` +其中最后一行代码,经过 console 还原,可以还原呈现面的代码: +``` +r['join']('') + '!!' + i['join']('') + '!!' + o['join']('') +``` +这里的 `r`、`i`、`o` 是三个数组,加密的核心逻辑,就是生成这三个数组。而这三个数组的生成过程,其实也就是对滑动轨迹的处理过程。那轨迹是什么呢? +通过分析 `return new ct(t)[$_BEGIH(84)]` 我们发现,整个处理过程都是围绕 `t` 展开的。 + +![]({{site.baseurl}}/img-post/geetcode-13.png) + +那 `t` 又是什么东西呢?调试工具已经提示给我们了,它是一个数组,分析数组内容我们推测,这就是滑块运行轨迹,数组中的每个小数组有三个值。 +经验判断三个值分别是:横向滑动距离、纵向滑动距离、以及滑动时间。 +``` +0: (3) [35, 23, 0] +1: (3) [1, 0, 83] +2: (3) [1, 0, 8] +3: (3) [2, 1, 8] +4: (3) [2, 0, 8] +5: (3) [2, 0, 6] +6: (3) [2, 0, 8] +7: (3) [2, 0, 8] +8: (3) [2, 0, 8] +9: (3) [0, 1, 8] +10: (3) [1, 0, 8] +11: (3) [1, 1, 22] +12: (3) [1, 0, 8] +13: (3) [1, 0, 16] +14: (3) [0, 1, 8] +15: (3) [1, 0, 8] +16: (3) [2, 0, 8] +17: (3) [1, 0, 6] +18: (3) [1, 0, 8] +19: (3) [2, 0, 10] +20: (3) [1, 0, 6] +21: (3) [1, 0, 24] +22: (3) [1, 0, 8] +23: (3) [1, 0, 14] +24: (3) [1, 0, 17] +25: (3) [1, 0, 23] +26: (3) [1, 0, 11] +27: (3) [1, 0, 19] +28: (3) [1, 0, 16] +29: (3) [1, 0, 8] +30: (3) [1, 0, 7] +31: (3) [1, 0, 8] +32: (3) [2, 0, 8] +33: (3) [2, 0, 15] +34: (3) [1, 0, 24] +35: (3) [1, 0, 22] +36: (3) [1, 0, 16] +37: (3) [1, 0, 8] +38: (3) [1, 0, 8] +39: (3) [0, 0, 116] +length: 40 +[[Prototype]]: Array(0) +``` +这里需要注意的是,`t` 的值在经过 `ct(t)[$_BEGIH(84)]` 函数处理时,会发生变化,如果忽略了这一点可能会导致轨迹加密后,依然无法成功请求。 +到这里关键的逆向工作就差不多完成了,不过这里要注意加密的入口是 `l = n[$_CJJIW(1078)][$_CJJJd(1069)](n[$_CJJIW(1078)][$_CJJJd(1051)](), n[$_CJJJd(13)][$_CJJIW(1045)], n[$_CJJIW(13)][$_CJJIW(307)])`,我们前面的逆向完成的是传入的三个参数,外面的函数也需抠出来。我们下断追进去,发现下面这段代码,直接拿过来用就可以。 +``` +"\u0024\u005f\u0042\u0042\u0043\u0041": function(t, e, n) { + var $_BEIIp = lTloj.$_CX + , $_BEIHl = ['$_BEJBo'].concat($_BEIIp) + , $_BEIJO = $_BEIHl[1]; + $_BEIHl.shift(); + var $_BEJAQ = $_BEIHl[0]; + if (!e || !n) + return t; + var r, i = 0, o = t, s = e[0], a = e[2], _ = e[4]; + while (r = n[$_BEIIp(373)](i, 2)) { + i += 2; + var c = parseInt(r, 16) + , u = String[$_BEIIp(206)](c) + , l = (s * c * c + a * c + _) % t[$_BEIIp(182)]; + o = o[$_BEIIp(373)](0, l) + u + o[$_BEIJO(373)](l); + } + return o; + } +``` + diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\344\274\201\346\237\245\346\237\245\346\227\240\351\231\220 debugger \345\217\215\347\210\254\347\255\226\347\225\245.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\344\274\201\346\237\245\346\237\245\346\227\240\351\231\220 debugger \345\217\215\347\210\254\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..ce5f00de55f --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\344\274\201\346\237\245\346\237\245\346\227\240\351\231\220 debugger \345\217\215\347\210\254\347\255\226\347\225\245.md" @@ -0,0 +1,48 @@ +--- +layout: post +title: JS逆向:破解企查查无限 debugger 反爬策略 +subtitle: Fiddler + AutoResponder 篡改 js 代码 +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +# 1. 前言 +今天在搞企查查的时候,发现企查查开启了无限 debugger,右键检查打开调试窗口后,无论在企查查页面进行任何操作,都会进入 debugger 调试状态。 +如下图: + +![]({{site.baseurl}}/img-post/debugger-1.png) + +按照常规的思路,找到 debugger 关键字,之后禁用调试或者添加 false 条件,不执行 debugger 就可以了,但是当我全局搜索 debugger 关键字的时候,却发现 js 文件中的 debugger 竟然有三千多个,瞬间心里三千只草泥马奔腾而过。 +![]({{site.baseurl}}/img-post/debugger-2.png) + +此时,我尝试分析 js 文件,搞清楚 debugger 函数的执行逻辑,但是经过分析发现,debugger 的执行逻辑全部封装在一个匿名函数里,当请求返回的时候函数会自动执行,不要监听任何事件即可完成 debugger 调用。 +![]({{site.baseurl}}/img-post/debugger-2-1.png) + +# 2. Fiddler 拦截思路 +这个时候只能祭出大杀器 fiddler,采用中间人攻击的方式篡改 js 文件。 +这里再简单介绍一下 Fiddler,可能很多人知道 Fiddler 是一个抓包工具,但其实 Fiddler 的作用远不止于此。 +Fiddler 是一个 http 协议调试工具,Fiddler 数据抓包软件能够记录并检查所有你的电脑和互联网之间的http通讯,并可以设置断点、查看所有的“进出”Fiddler的数据,还可以自定义修改 cookie、html、js、css 等文件。 +这里我将拦截企查查返回的 js 文件,并用篡改后的 js 文件取而代之,从而实现规避无限 debugger 的目的。 + +# 3. 获取 js 路径 +如下图所示,获取 js 文件的 url 路径。 +![]({{site.baseurl}}/img-post/debugger-3.png) + +# 4. 篡改 js 文件 +这里,首先将 js 文件保存到本地,之后将 ``` debugger```字符串替换成``` false```。 +>注意:**debugger 前面有空格**,替换时最好带上空格,因为有些位置也出现了 ```debugger``` 字符串,但是并不是我们需要替换的函数。 + +![]({{site.baseurl}}/img-post/debugger-4.png) + +# 5. Fiddler 拦截 +在 ```AutoResponder```中添加规则,如下图所示。 +![]({{site.baseurl}}/img-post/debugger-5.png) + +# 6. 重新请求 +此时,无限 debugger 已经不再出现。 +![]({{site.baseurl}}/img-post/debugger-6.png) diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232jsfuck \346\212\200\346\234\257\345\216\237\347\220\206\350\247\243\346\236\220.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232jsfuck \346\212\200\346\234\257\345\216\237\347\220\206\350\247\243\346\236\220.md" new file mode 100644 index 00000000000..3bd576a7088 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232jsfuck \346\212\200\346\234\257\345\216\237\347\220\206\350\247\243\346\236\220.md" @@ -0,0 +1,47 @@ +--- +layout: post +title: JS逆向:jsfuck 技术原理解析 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +## jsfuck 简介 +- jsfuck 源于一门编程语言 brainfuck,其主要的思想就是只使用8种特定的符号来编写代码; +- jsfuck 也是沿用了这个思想,它仅仅使用6种符号来编写代码,它们分别是 **(、)、+、[、]、!**; +- github:[https://github.com/aemkei/jsfuck](https://github.com/aemkei/jsfuck) + +- 下面这串代码,复制黏贴到 consle 中执行: +``` +[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+(![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+[+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]])() +``` +- 执行的结果如下图所示,窗口弹出数字 “1”: + ![]({{site.baseurl}}/img-post/jsfuck-1.png) + +## 生成 jsfuck 代码 +- 我们也可以在 jsfuck 调试网站,将普通的 js 代码转换成 jsfuck 代码; +>调试网站:[http://www.jsfuck.com/](http://www.jsfuck.com/)  + +![]({{site.baseurl}}/img-post/jsfuck-2.png) + + +## jsfuck 调试 +- 复制下面的代码,在 console 中执行; +``` +[][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (+[![]] + [[!![]][+(![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![] + [+[]]]]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![] + [+[]]] + (![] + [])[+!![]] + (![] + [])[!![] + !![]])()((!![] + !![] + [!![] + !![]] + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + ![] + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + ![] + ![] + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (+!![]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + ![] + (+!![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + ![] + (+!![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + (!![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]))[(![] + [])[!![] + !![] + !![]] + (+[![]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + ([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (![] + [])[+!![]])()([][[]]))[+!![] + [+[]]] + (![] + [])[!![] + !![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[+[]]](![])[(+(!![] + !![] + [+[]] + (+!![]) + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![])))[(!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([] + [])[([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]][([][[]] + [])[+!![]] + (![] + [])[+!![]] + ([] + (+[])[([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]])[+!![] + [+!![]]] + (!![] + [])[!![] + !![] + !![]]]](!![] + !![] + !![] + [+[]])](([] + [])[([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + ([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (![] + [])[+!![]])()(+[] + [![]])[!![] + !![] + !![]] + (![] + [])[+!![]] + (!![] + [])[+!![]] + (+[![]] + [][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]())[+!![] + [+!![]]] + (!![] + [])[+[]]][([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]] + ([![]] + [][[]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + ([][[]] + [])[!![] + !![]]]([][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (![] + [])[+!![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (![] + [])[!![] + !![] + !![]] + (+(+!![] + [+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + !![]) + (+[])) + [])[+!![]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]] + (+(+!![] + [+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + !![]) + (+[])) + [])[+!![]] + (![] + [])[+[]] + (!![] + [])[+!![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([] + (+[])[([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]])[+!![] + [+!![]]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + (![] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]])()(+[] + [+[]] + (+[![]]) + ![])[+!![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[!![] + !![]] + (!![] + [])[!![] + !![] + !![]] + ([][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (![] + [])[+!![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (![] + [])[!![] + !![] + !![]] + ([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]] + (![] + [])[+!![]] + (!![] + [])[+!![]])() + [])[+!![] + [+!![]]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([![]] + [][[]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [!![] + !![] + !![]]] + (!![] + !![] + !![] + [!![] + !![]] + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (+[]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + (+[]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (+!![]) + ![] + (+!![]) + (+[]) + (+[]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![] + !![]))[(![] + [])[!![] + !![] + !![]] + (+[![]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + ([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (![] + [])[+!![]])()([][[]]))[+!![] + [+[]]] + (![] + [])[!![] + !![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[+[]]](![]) + (+[] + [![]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]])()))[([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([![]] + [][[]])[+!![] + [+[]]] + ([][[]] + [])[+!![]]]([])) +``` +- 发现 console 提示下面的错误; + ![]({{site.baseurl}}/img-post/jsfuck-3.png) +- 此时我们需要把代码复制到 notepad++ 编辑器,鼠标放在代码最后,如下图: + ![]({{site.baseurl}}/img-post/jsfuck-4.png) +- 之后往上翻,查找对应的左半边括号,如下图: + ![]({{site.baseurl}}/img-post/jsfuck-5.png) +- 将【括号内部】的代码,复制到 console 中执行,得到下面的代码: + +![]({{site.baseurl}}/img-post/jsfuck-6.png) + diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\344\275\277\347\224\250 jsnice \346\217\220\351\253\230\346\267\267\346\267\206\344\273\243\347\240\201\347\232\204\345\217\257\350\257\273\346\200\247.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\344\275\277\347\224\250 jsnice \346\217\220\351\253\230\346\267\267\346\267\206\344\273\243\347\240\201\347\232\204\345\217\257\350\257\273\346\200\247.md" new file mode 100644 index 00000000000..d87637a7df8 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\344\275\277\347\224\250 jsnice \346\217\220\351\253\230\346\267\267\346\267\206\344\273\243\347\240\201\347\232\204\345\217\257\350\257\273\346\200\247.md" @@ -0,0 +1,85 @@ +--- +layout: post +title: JS逆向:使用 jsnice 提高混淆代码的可读性 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +#### 1、jsnice 简介 + +- jsnice 是一个反混淆利器之一,可以将混淆后的代码进行更加有好的展示,从而提升代码的可读性; +- jsnice 在元素关系的建立上大部分来自于 AST 语法树,同时采用了概率图模型进行 **推理** 和 **联想**,通过样本学习推测出未混淆JS脚本的 **概率图**; +> **jsnice** 本质上是一种 **机器学习**。 + +![]({{site.baseurl}}/img-post/jsnice.jpg) + +- 详细的工作原理,强烈推荐阅读一下这篇文章:《 代码理解之代码可读性:代码反混淆(https://zhuanlan.zhihu.com/p/311907878)》,以加强对其的理解; + +#### 2、jsnice 使用实例 + +- 我们以 jsnice 网站的实例进行说明, + + 下面的代码,是我们常见的经过混淆处理的代码: + + ``` + // Put your JavaScript here that you want to rename, deobfuscate, + // or infer types for: + function chunkData(e, t) { + var n = []; + var r = e.length; + var i = 0; + for (; i < r; i += t) { + if (i + t < r) { + n.push(e.substring(i, i + t)); + } else { + n.push(e.substring(i, r)); + } + } + return n; + } + // You can also use some ES6 features. + const get = (a,b) => a.getElementById(b); + ``` + +- 使用 jsnice 进行 nicefy 操作,得到下面的代码,代码的变量名、参数名等都被以很友好的方式进行了解读,代码可读性大大提高。 + + 如下所示: + + ``` + 'use strict'; + /** + * @param {string} bin + * @param {number} size + * @return {?} + */ + function chunkData(bin, size) { + /** @type {!Array} */ + var results = []; + var length = bin.length; + /** @type {number} */ + var i = 0; + for (; i < length; i = i + size) { + if (i + size < length) { + results.push(bin.substring(i, i + size)); + } else { + results.push(bin.substring(i, length)); + } + } + return results; + } + const get = (doc, key) => { + return doc.getElementById(key); + }; + ``` + +#### 3、jsnice 只能处理普通混淆 + +- jsnice 实际上只能处理 80% 的混淆代码,如果代码经过 **加密压缩** 等方式混淆后, jsnice 反混淆也会失败,这种情况下我们除非知道其加密算法,否则无法反混淆出来; +> jsnice 处理不了经过 **复杂加密压缩混淆** 的代码! +- 复杂的混淆如何处理呢,请参考文章《JS逆向:复杂混淆代码的处理策略(https://www.jianshu.com/p/318b2da7e75a)》 \ No newline at end of file diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\345\270\270\350\247\201\347\232\204JS\344\273\243\347\240\201\346\267\267\346\267\206\347\255\226\347\225\245\347\232\204\351\200\206\345\220\221\347\211\271\345\276\201.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\345\270\270\350\247\201\347\232\204JS\344\273\243\347\240\201\346\267\267\346\267\206\347\255\226\347\225\245\347\232\204\351\200\206\345\220\221\347\211\271\345\276\201.md" new file mode 100644 index 00000000000..8cd402529c3 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\345\270\270\350\247\201\347\232\204JS\344\273\243\347\240\201\346\267\267\346\267\206\347\255\226\347\225\245\347\232\204\351\200\206\345\220\221\347\211\271\345\276\201.md" @@ -0,0 +1,190 @@ +--- +layout: post +title: JS逆向:常见的JS代码混淆策略的逆向特征 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +## eval 混淆 + +Javascript 提供了将字符串当做代码执行``(evaluate)``的能力,可以通过 `Function` 构造器、``eval``、``setTimeout``、``setInterval`` 将字符串传递给 js 引擎进行解析执行。其最明显的特征是生成的代码以 `eval(function(p,a,c,k,...){...})` 开头。 +这类混淆的关键思想在于将需要执行的代码进行一次编码,在执行的时候还原出浏览器可执行的合法的脚本,然后执行之,看上去和可执行文件的加壳有那么点类似。 + +#### 逆向特征 + +- 无论代码如何进行变形,其最终都要调用一次 ``eval``、``throw`` 等函数。 +- 解密的方法不需要对其算法做任何分析,只需要简单地找到这个最终的调用,改为 `console.log` 或者其他方式,将程序解码后的结果按照字符串输出即可。 +- 关键字:`eval`、`throw`; + +## AA 混淆 + +有如下 js 代码: +``` +var test = function(x){console.log(x)} +test('1') +``` +使用 `https://www.jsjiami.com/aaencode.html` 工具,经过 AA 混淆后得到: +``` +゚ω゚ノ= /`m´)ノ ~┻━┻ //*´∇`*/ ['_']; o=(゚ー゚) =_=3; c=(゚Θ゚) =(゚ー゚)-(゚ー゚); (゚Д゚) =(゚Θ゚)= (o^_^o)/ (o^_^o);(゚Д゚)={゚Θ゚: '_' ,゚ω゚ノ : ((゚ω゚ノ==3) +'_') [゚Θ゚] ,゚ー゚ノ :(゚ω゚ノ+ '_')[o^_^o -(゚Θ゚)] ,゚Д゚ノ:((゚ー゚==3) +'_')[゚ー゚] }; (゚Д゚) [゚Θ゚] =((゚ω゚ノ==3) +'_') [c^_^o];(゚Д゚) ['c'] = ((゚Д゚)+'_') [ (゚ー゚)+(゚ー゚)-(゚Θ゚) ];(゚Д゚) ['o'] = ((゚Д゚)+'_') [゚Θ゚];(゚o゚)=(゚Д゚) ['c']+(゚Д゚) ['o']+(゚ω゚ノ +'_')[゚Θ゚]+ ((゚ω゚ノ==3) +'_') [゚ー゚] + ((゚Д゚) +'_') [(゚ー゚)+(゚ー゚)]+ ((゚ー゚==3) +'_') [゚Θ゚]+((゚ー゚==3) +'_') [(゚ー゚) - (゚Θ゚)]+(゚Д゚) ['c']+((゚Д゚)+'_') [(゚ー゚)+(゚ー゚)]+ (゚Д゚) ['o']+((゚ー゚==3) +'_') [゚Θ゚];(゚Д゚) ['_'] =(o^_^o) [゚o゚] [゚o゚];(゚ε゚)=((゚ー゚==3) +'_') [゚Θ゚]+ (゚Д゚) .゚Д゚ノ+((゚Д゚)+'_') [(゚ー゚) + (゚ー゚)]+((゚ー゚==3) +'_') [o^_^o -゚Θ゚]+((゚ー゚==3) +'_') [゚Θ゚]+ (゚ω゚ノ +'_') [゚Θ゚]; (゚ー゚)+=(゚Θ゚); (゚Д゚)[゚ε゚]='\\'; (゚Д゚).゚Θ゚ノ=(゚Д゚+ ゚ー゚)[o^_^o -(゚Θ゚)];(o゚ー゚o)=(゚ω゚ノ +'_')[c^_^o];(゚Д゚) [゚o゚]='\"';(゚Д゚) ['_'] ( (゚Д゚) ['_'] (゚ε゚+(゚Д゚)[゚o゚]+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚ー゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+((゚ー゚) + (o^_^o))+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚ー゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (o^_^o))+ (c^_^o)+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (o^_^o))+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (o^_^o))+ (c^_^o)+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (o^_^o))+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+((o^_^o) +(o^_^o))+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚o゚]) (゚Θ゚)) ('_'); +``` +经过美化后,展示如下: +``` +゚ω゚ノ = /`m´)ノ ~┻━┻ //*´∇`*/ +['_']; +o = (゚ー゚) = _ = 3; +c = (゚Θ゚) = (゚ー゚) - (゚ー゚); +(゚Д゚) = (゚Θ゚) = (o ^ _ ^ o) / (o ^ _ ^ o); +(゚Д゚) = { + ゚Θ゚: '_', + ゚ω゚ノ: ((゚ω゚ノ == 3) + '_')[゚Θ゚], + ゚ー゚ノ: (゚ω゚ノ + '_')[o ^ _ ^ o - (゚Θ゚)], + ゚Д゚ノ: ((゚ー゚ == 3) + '_')[゚ー゚] +}; +(゚Д゚)[゚Θ゚] = ((゚ω゚ノ == 3) + '_')[c ^ _ ^ o]; +(゚Д゚)['c'] = ((゚Д゚) + '_')[(゚ー゚) + (゚ー゚) - (゚Θ゚)]; +(゚Д゚)['o'] = ((゚Д゚) + '_')[゚Θ゚]; +(゚o゚) = (゚Д゚)['c'] + (゚Д゚)['o'] + (゚ω゚ノ + '_')[゚Θ゚] + ((゚ω゚ノ == 3) + '_')[゚ー゚] + ((゚Д゚) + '_')[(゚ー゚) + (゚ー゚)] + ((゚ー゚ == 3) + '_')[゚Θ゚] + ((゚ー゚ == 3) + '_')[(゚ー゚) - (゚Θ゚)] + (゚Д゚)['c'] + ((゚Д゚) + '_')[(゚ー゚) + (゚ー゚)] + (゚Д゚)['o'] + ((゚ー゚ == 3) + '_')[゚Θ゚]; +(゚Д゚)['_'] = (o ^ _ ^ o)[゚o゚][゚o゚]; +(゚ε゚) = ((゚ー゚ == 3) + '_')[゚Θ゚] + (゚Д゚).゚Д゚ノ + ((゚Д゚) + '_')[(゚ー゚) + (゚ー゚)] + ((゚ー゚ == 3) + '_')[o ^ _ ^ o - ゚Θ゚] + ((゚ー゚ == 3) + '_')[゚Θ゚] + (゚ω゚ノ + '_')[゚Θ゚]; +(゚ー゚) += (゚Θ゚); +(゚Д゚)[゚ε゚] = '\\'; +(゚Д゚).゚Θ゚ノ = (゚Д゚ + ゚ー゚)[o ^ _ ^ o - (゚Θ゚)]; +(o゚ー゚o) = (゚ω゚ノ + '_')[c ^ _ ^ o]; +(゚Д゚)[゚o゚] = '\"'; +(゚Д゚)['_']((゚Д゚)['_'](゚ε゚ + (゚Д゚)[゚o゚] + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + (゚ー゚) + (゚Θ゚) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + ((o ^ _ ^ o) - (゚Θ゚)) + (゚Д゚)[゚ε゚] + (゚ー゚) + (c ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚ー゚) + (゚Д゚)[゚ε゚] + (゚Θ゚) + (゚ー゚) + ((゚ー゚) + (゚Θ゚)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (o ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚ー゚) + (゚Д゚)[゚ε゚] + (゚ー゚) + (c ^ _ ^ o) + (゚Д゚)[゚ε゚] + ((゚ー゚) + (o ^ _ ^ o)) + ((゚ー゚) + (゚Θ゚)) + (゚Д゚)[゚ε゚] + (゚ー゚) + (c ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚Θ゚) + (゚ー゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + ((゚ー゚) + (゚Θ゚)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (゚Θ゚)) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + (゚ー゚) + (o ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚ー゚) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (゚Θ゚)) + (゚Θ゚) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (゚Θ゚)) + ((゚ー゚) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (゚Θ゚)) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + ((゚ー゚) + (゚Θ゚)) + (c ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (o ^ _ ^ o)) + (c ^ _ ^ o) + (゚Д゚)[゚ε゚] + ((゚ー゚) + (゚Θ゚)) + (゚Θ゚) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (o ^ _ ^ o)) + (o ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚Θ゚) + (゚ー゚) + (o ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (゚Θ゚)) + ((゚ー゚) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (゚Θ゚)) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (o ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (゚Θ゚)) + ((゚ー゚) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (゚Θ゚)) + (゚ー゚) + (゚Д゚)[゚ε゚] + (゚Θ゚) + (゚ー゚) + ((゚ー゚) + (゚Θ゚)) + (゚Д゚)[゚ε゚] + ((゚ー゚) + (゚Θ゚)) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (゚Θ゚)) + (゚ー゚) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (゚Θ゚)) + ((゚ー゚) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + (゚ー゚) + ((゚ー゚) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + ((゚ー゚) + (゚Θ゚)) + (c ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (o ^ _ ^ o)) + (c ^ _ ^ o) + (゚Д゚)[゚ε゚] + ((゚ー゚) + (゚Θ゚)) + (゚Θ゚) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((゚ー゚) + (o ^ _ ^ o)) + ((゚ー゚) + (゚Θ゚)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) - (゚Θ゚)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚ー゚) + (゚Д゚)[゚ε゚] + (゚Θ゚) + (゚ー゚) + ((゚ー゚) + (゚Θ゚)) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (o ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚Θ゚) + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚ー゚) + (゚Д゚)[゚ε゚] + ((゚ー゚) + (゚Θ゚)) + (c ^ _ ^ o) + (゚Д゚)[゚ε゚] + (゚ー゚) + ((゚ー゚) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + ((o ^ _ ^ o) + (o ^ _ ^ o)) + (゚Θ゚) + (゚Д゚)[゚ε゚] + (゚ー゚) + ((゚ー゚) + (o ^ _ ^ o)) + (゚Д゚)[゚ε゚] + ((゚ー゚) + (゚Θ゚)) + (゚Θ゚) + (゚Д゚)[゚o゚])(゚Θ゚))('_'); + +``` +#### 反混淆工具 + +>注意:不同站点的混淆工具,得到的混淆结果不尽相同,在一个站点生成的混淆代码,在另一站点不一定能反混淆成功。 +- `https://www.qtool.net/decode` +- `https://cat-in-136.github.io/2010/12/aadecode-decode-encoded-as-aaencode.html` +- `http://www.hiencode.com/jjencode.html` + +## JJ 混淆 + +有如下 js 代码: +``` +var test = function(x){console.log(x)} +test('1'); +``` +设全局变量 `$`,经过 jj 混淆后将代码如下: +``` +$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+"\\"+$.__$+$.$$_+$.$$_+$.$_$_+"\\"+$.__$+$.$$_+$._$_+"\\"+$.$__+$.___+$.__+$.$$$_+"\\"+$.__$+$.$$_+$._$$+$.__+"\\"+$.$__+$.___+"=\\"+$.$__+$.___+$.$$$$+$._+"\\"+$.__$+$.$_$+$.$$_+$.$$__+$.__+"\\"+$.__$+$.$_$+$.__$+$._$+"\\"+$.__$+$.$_$+$.$$_+"(\\"+$.__$+$.$$$+$.___+"){"+$.$$__+$._$+"\\"+$.__$+$.$_$+$.$$_+"\\"+$.__$+$.$$_+$._$$+$._$+(![]+"")[$._$_]+$.$$$_+"."+(![]+"")[$._$_]+$._$+"\\"+$.__$+$.$__+$.$$$+"(\\"+$.__$+$.$$$+$.___+")}\\"+$.__$+$._$_+$.__+$.$$$_+"\\"+$.__$+$.$$_+$._$$+$.__+"('"+$.__$+"');"+"\"")())(); +``` + +美化展示,得到下面的代码: + +``` +$ = ~[]; +$ = { + ___: ++$, + $$$$: (![] + "")[$], + __$: ++$, + $_$_: (![] + "")[$], + _$_: ++$, + $_$$: ({} + "")[$], + $$_$: ($[$] + "")[$], + _$$: ++$, + $$$_: (!"" + "")[$], + $__: ++$, + $_$: ++$, + $$__: ({} + "")[$], + $$_: ++$, + $$$: ++$, + $___: ++$, + $__$: ++$ +}; +$.$_ = ($.$_ = $ + "")[$.$_$] + ($._$ = $.$_[$.__$]) + ($.$$ = ($.$ + "")[$.__$]) + ((!$) + "")[$._$$] + ($.__ = $.$_[$.$$_]) + ($.$ = (!"" + "")[$.__$]) + ($._ = (!"" + "")[$._$_]) + $.$_[$.$_$] + $.__ + $._$ + $.$; +$.$$ = $.$ + (!"" + "")[$._$$] + $.__ + $._ + $.$ + $.$$; +$.$ = ($.___)[$.$_][$.$_]; +$.$($.$($.$$ + "\"" + "\\" + $.__$ + $.$$_ + $.$$_ + $.$_$_ + "\\" + $.__$ + $.$$_ + $._$_ + "\\" + $.$__ + $.___ + $.__ + $.$$$_ + "\\" + $.__$ + $.$$_ + $._$$ + $.__ + "\\" + $.$__ + $.___ + "=\\" + $.$__ + $.___ + $.$$$$ + $._ + "\\" + $.__$ + $.$_$ + $.$$_ + $.$$__ + $.__ + "\\" + $.__$ + $.$_$ + $.__$ + $._$ + "\\" + $.__$ + $.$_$ + $.$$_ + "(\\" + $.__$ + $.$$$ + $.___ + "){" + $.$$__ + $._$ + "\\" + $.__$ + $.$_$ + $.$$_ + "\\" + $.__$ + $.$$_ + $._$$ + $._$ + (![] + "")[$._$_] + $.$$$_ + "." + (![] + "")[$._$_] + $._$ + "\\" + $.__$ + $.$__ + $.$$$ + "(\\" + $.__$ + $.$$$ + $.___ + ")}\\" + $.__$ + $._$_ + $.__ + $.$$$_ + "\\" + $.__$ + $.$$_ + $._$$ + $.__ + "('" + $.__$ + "');" + "\"")())(); + +``` +> **注意:** 美化后的 jj 混淆代码可能无法进行直接解码。 + +使用 `http://www.hiencode.com/jjencode.html` 解码工具,将美化后的 jj 混淆代码进行 decode 解码时,会有 `no match` 报错,如下图: + +![]({{site.baseurl}}/img/jjencode-1.png) + +而如果 jj 混淆代码是未经美化的,那就可以顺利的解码,如下图: + +![]({{site.baseurl}}/img/jjencode-1.png) + +## jsfuck + +- jsfuck 源于一门编程语言 brainfuck,其主要的思想就是只使用8种特定的符号来编写代码; +- jsfuck 也是沿用了这个思想,它仅仅使用6种符号来编写代码,它们分别是 **(、)、+、[、]、!**; +- github:[https://github.com/aemkei/jsfuck](https://github.com/aemkei/jsfuck) + +- 下面这串代码,复制黏贴到 consle 中执行: + +``` +[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+(![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+[+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]])() +``` +- 执行的结果如下图所示,窗口弹出数字 “1”: + +![]({{site.baseurl}}/img/jsfuck-1.jpg) + +#### 生成 jsfuck 代码 + +- 我们也可以在 jsfuck 调试网站,将普通的 js 代码转换成 jsfuck 代码; +>调试网站:[http://www.jsfuck.com/](http://www.jsfuck.com/)  + +![]({{site.baseurl}}/img/jsfuck-2.jpg) + +#### jsfuck 调试 + +- 复制下面的代码,在 console 中执行; + +``` +[][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (+[![]] + [[!![]][+(![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![] + [+[]]]]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![] + [+[]]] + (![] + [])[+!![]] + (![] + [])[!![] + !![]])()((!![] + !![] + [!![] + !![]] + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + ![] + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + ![] + ![] + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (+!![]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + ![] + (+!![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + ![] + (+!![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + ![] + (+!![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+!![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + (!![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (+[]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![] + !![]) + (+[]) + ![] + (+!![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (+!![]) + (!![] + !![]) + ![] + (!![] + !![]) + (!![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + ![] + (+!![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + ![] + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![]) + ![] + (+!![]) + (+!![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + ![] + ![] + ![] + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]) + ![] + (!![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![]) + (+!![]))[(![] + [])[!![] + !![] + !![]] + (+[![]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + ([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (![] + [])[+!![]])()([][[]]))[+!![] + [+[]]] + (![] + [])[!![] + !![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[+[]]](![])[(+(!![] + !![] + [+[]] + (+!![]) + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![])))[(!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([] + [])[([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]][([][[]] + [])[+!![]] + (![] + [])[+!![]] + ([] + (+[])[([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]])[+!![] + [+!![]]] + (!![] + [])[!![] + !![] + !![]]]](!![] + !![] + !![] + [+[]])](([] + [])[([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + ([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (![] + [])[+!![]])()(+[] + [![]])[!![] + !![] + !![]] + (![] + [])[+!![]] + (!![] + [])[+!![]] + (+[![]] + [][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]())[+!![] + [+!![]]] + (!![] + [])[+[]]][([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]] + ([![]] + [][[]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + ([][[]] + [])[!![] + !![]]]([][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (![] + [])[+!![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (![] + [])[!![] + !![] + !![]] + (+(+!![] + [+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + !![]) + (+[])) + [])[+!![]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]] + (+(+!![] + [+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + !![]) + (+[])) + [])[+!![]] + (![] + [])[+[]] + (!![] + [])[+!![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([] + (+[])[([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]])[+!![] + [+!![]]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + (![] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]])()(+[] + [+[]] + (+[![]]) + ![])[+!![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[!![] + !![]] + (!![] + [])[!![] + !![] + !![]] + ([][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (![] + [])[+!![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (![] + [])[!![] + !![] + !![]] + ([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]] + (![] + [])[+!![]] + (!![] + [])[+!![]])() + [])[+!![] + [+!![]]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([![]] + [][[]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [!![] + !![] + !![]]] + (!![] + !![] + !![] + [!![] + !![]] + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (+[]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + (+!![]) + ![] + (!![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (+!![]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + ![] + (+!![]) + (+[]) + (!![] + !![]) + ![] + (!![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (+!![]) + ![] + (+!![]) + (+[]) + (+[]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + (+!![]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (!![] + !![]) + (!![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (+[]) + ![] + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + (!![] + !![] + !![]) + ![] + (+!![]) + (+!![]) + (!![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (!![] + !![] + !![] + !![]) + (!![] + !![] + !![] + !![] + !![] + !![] + !![] + !![] + !![]) + ![] + (+!![]) + (+[]) + (!![] + !![] + !![] + !![]))[(![] + [])[!![] + !![] + !![]] + (+[![]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]][([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([][[]] + [])[+!![]] + (![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+[]] + ([] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (!![] + [])[+!![]]]((!![] + [])[+!![]] + (!![] + [])[!![] + !![] + !![]] + (!![] + [])[+[]] + ([][[]] + [])[+[]] + (!![] + [])[+!![]] + ([][[]] + [])[+!![]] + (![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]] + ([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![]] + (!![] + [])[+[]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + (![] + [])[+!![]])()([][[]]))[+!![] + [+[]]] + (![] + [])[!![] + !![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[+[]]](![]) + (+[] + [![]] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[!![] + !![] + [+[]]])()))[([][(!![] + [])[!![] + !![] + !![]] + ([][[]] + [])[+!![]] + (!![] + [])[+[]] + (!![] + [])[+!![]] + ([![]] + [][[]])[+!![] + [+[]]] + (!![] + [])[!![] + !![] + !![]] + (![] + [])[!![] + !![] + !![]]]() + [])[!![] + !![] + !![]] + (!![] + [][(![] + [])[+[]] + ([![]] + [][[]])[+!![] + [+[]]] + (![] + [])[!![] + !![]] + (![] + [])[!![] + !![]]])[+!![] + [+[]]] + ([![]] + [][[]])[+!![] + [+[]]] + ([][[]] + [])[+!![]]]([])) +``` + +- 发现 console 提示下面的错误; + + ![]({{site.baseurl}}/img/jsfuck-1.jpg) + +- 此时我们需要把代码复制到 notepad++ 编辑器,鼠标放在代码最后,如下图: +- + ![]({{site.baseurl}}/img/jsfuck-2.jpg) + +- 之后往上翻,查找对应的左半边括号,如下图: + + ![]({{site.baseurl}}/img/jsfuck-3.jpg) + +- 将【括号内部】的代码,复制到 console 中执行,得到下面的代码: + + ![]({{site.baseurl}}/img/jsfuck-4.jpg) + +## 隐写术 + +- 严格说这不能称之为混淆,只是将 js 代码隐藏到了特定的介质当中。如通过最低有效位(LSB)算法嵌入到图片的 RGB 通道、隐藏在图片 EXIF 元数据、隐藏在 HTML 空白字符等。 + +- 比如这个耸人听闻的议题:《一张图片黑掉你》在图片中嵌入恶意程序,正是使用了最低有效位平面算法。结合 HTML5 的 canvas 或者处理二进制数据的 TypeArray,脚本可以抽取出载体中隐藏的数据(如代码)。 + +## 复杂表达式 + +代码混淆不一定会调用 eval,也可以通过在代码中填充无效的指令来增加代码复杂度,极大地降低可读性。Javascript 中存在许多称得上丧心病狂的特性,这些特性组合起来,可以把原本简单的字面量(Literal)、成员访问(MemberExpression)、函数调用(CallExpression)等代码片段变得难以阅读。 + +举个简单的例子,可能会更好理解: + +1. 访问一个对象的成员有两种方法——点运算符和下标运算符。调用 ``window`` 的 ``eval`` 方法,既可以写成 ``window.eval()``,也可以 ``window[‘eval’]``; +2. 为了让代码更变态一些,混淆器选用第二种写法,然后再在字符串字面量上做文章。先把字符串拆成几个部分:``’e’ + ‘v’ + ‘al’``; +3. 这样看上去还是很明显,再利用一个数字进制转换的技巧:``14..toString(15) + 31..toString(32) + 0xf1.toString(22)``; +4. 一不做二不休,把数字也展开:``(0b1110).toString(4<<2) + (‘ ‘.charCodeAt() - 1).toString(Math.log(0x100000000) / Math.log(2)) + 0xf1.toString(11 << 1)``; +5. 最后的效果:``window[(2*7).toString(4<<2) + (‘ ‘.charCodeAt() - 1).toString(Math.log(0x100000000) / Math.log(2)) + 0xf1.toString(11 << 1)](http://jartto.wang/2017/10/31/js-anti-aliasing/'alert(1)‘)`` + +在 js 中可以找到许多这样互逆的运算,通过使用随机生成的方式将其组合使用,可以把简单的表达式无限复杂化。 + +- 隐写的方式同样需要解码程序和动态执行,所以破解的方式和前者相同,在浏览器上下文中劫持替换关键函数调用的行为,改为文本输出即可得到载体中隐藏的代码。 + +## ob 混淆 + +TODO diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\345\270\270\350\247\201\347\232\204\346\227\240\351\231\220debugger\345\217\215\350\260\203\350\257\225\347\255\226\347\225\245\344\273\245\345\217\212\345\272\224\345\257\271\346\226\271\346\263\225.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\345\270\270\350\247\201\347\232\204\346\227\240\351\231\220debugger\345\217\215\350\260\203\350\257\225\347\255\226\347\225\245\344\273\245\345\217\212\345\272\224\345\257\271\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..e64190a836c --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\345\270\270\350\247\201\347\232\204\346\227\240\351\231\220debugger\345\217\215\350\260\203\350\257\225\347\255\226\347\225\245\344\273\245\345\217\212\345\272\224\345\257\271\346\226\271\346\263\225.md" @@ -0,0 +1,195 @@ +--- +layout: post +title: JS逆向:常见的无限debugger反调试策略以及应对方法 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +# 种类划分 + +### 按代码逻辑划分 + +#### 无限循环 + +- for 循环 +- while 循环 + +> 注意:使用 while 循环一定会有次数上限,否则浏览器会卡死掉。 + +#### 无限递归 + +- 调用自身 + +#### 两个方法循环互调 + +- 顾名思义 + +#### 计时器 + +`setInterval` 这个是 JS 语言当中的 `定时器`,它有两个参数:第一个是要执行的代码,第二个执行时间。 +下方是 sojson 混淆后的一段代码,其中使用了 `setInterval` 定时器进行了反 debugger 操作。 + +这个一段代码执行的话会一直断住,不管你怎么跳转到下一个断点,它都还是会一直断,其实这个就是一个无限debugger的代码。 + +### 按是否可混淆划分 + +#### 不可混淆 + +直接使用代码, + +``` +debugger; +``` + +#### 可混淆 + +``` +eval("debugger;") +``` + +#### 可重度混淆 + +``` +Function("debugger").call()/apply() 或赋值 bind() +``` + +``` +xxx.constructor("debugger").call("action") +``` + +``` +Fuction.constructor("debugger").call("action") +``` + +``` +(function(){return !![];}["constructor"]("debugger")["call"]("action")) +``` + +### 按设置节点划分 + +- 无限 debugger 贯穿全局 + 这种情况,多是由 `setInterval` 定时器实现的。针对这种情况,我们需要将定时器函数置空或者重写。 +- 在加密业务逻辑之前设置 +- 在加密业务逻辑之后设置 + +# 应对方法 + +### 针对静态文件 + +#### conditional breakpoint + +在 JS 代码 debugger 行数位置,鼠标右键添加 conditional breakpoint,其中条件 condition 设为 false; + +#### Fiddler AutoResponder 篡改 JS 代码 +这种方式的核心思路,是替换 JS 文件中的 debugger 关键字,并保存为本地文件,在请求返回的时候、通过正则匹配等方式、拦截并替换返回的 JS 代码,已达到绕过 debugger 的目的。 +关于这种方法,请参考我的另一篇文章:《[JS逆向:fiddler 篡改 js 破解企查查无限 debugger 问题](https://www.jianshu.com/p/9f72c4e0fd34)》,文中有详细的描述。 + +### 针对动态文件 + +#### 手动在浏览器中 Hook + +#### 第一步、打 script 断点 + +这一步的目的,是为了让浏览器在刚运行时就被断下来,以方便进行 Hook + +#### 第二步、Hook 无限 debugger 函数 + +- 手动置空 debugger 函数。 + 打 `script` 断点,使网页在 debugger 之前下断,手动将debugger所在函数置空。这种方法简单粗暴,但是会影响原有的业务逻辑。 +``` +包含debugger函数 = function (){}; +``` + +- 手动重写 debugger 函数。 + 打 `script` 断点,然后在 debugger 函数被调用的地方下断,手动将debugger所在函数中的 debugger 逻辑删除,然后 **把 debugger 函数赋值给 调用函数**。 +>注意:这一步赋值不能省,否则无法调用。 + +- Hook Funciton + + ``` + F_ = Function + Function = function(a){ + if (a!=='debugger'){return F_(a)} + } + ``` +- Hook Function 构造器函数 + ``` + Function.prototype.constructor_ = Function.prototype.constructor; + Function.prototype.constructor = function(x){ + if (x!=='debugger'){return Function.prototype.constructor_(x)} + } + ``` +如果觉得上面的代码太粗暴,也可使用下方代码。 + ``` + Function.prototype.__constructor_back = Function.prototype.constructor; + Function.prototype.constructor = function() { + if(arguments && typeof arguments[0]==='string'){ + //alert("new function: "+ arguments[0]); + if("debugger" === arguments[0]){ + //arguments[0]="console.log(\"anti debugger\");"; + //arguments[0]=";"; + return + } + } + return Function.prototype.__constructor_back.apply(this,arguments); + } + ``` +- Hook eval 函数 + ``` + eval_ = eval; + //下面这样写,是为了过瑞数的 eval.toString 检测 + eval = function(a){if(a=='debugger'){return ''}else{return eval_(a)}} + ``` +- Hook conole.log,为了防止调试过程中,console.log 被重写,也可以在此时对 console.log 进行 hook,之后在 console.log 无法正常打印的位置,再将 console.log 进行复原; + +Hook console.log: + ``` + console.log_ = console.log + ``` +将 console.log 复原: + ``` + console.log = console.log_ + ``` +- Hook setInterval 函数 + - 业务代码和 setInterval 无关 + ``` + setInterval = function(){} + ``` + - 业务代码和 setInterval 有关 + ``` + setInterval_back = setInterval + setInterval = function(a,b){ + if(a.toString().indexOf('debugger') == -1){ + return null; + } + setInterval_back(a, b) + } + ``` + +#### Fiddler + 编程猫插件 + Hook + +使用这种方法,就不需要再打 `script` 断点。 +>注意:如果 `script` 断点无法在 debugger 函数之前断下来,那就只能用这种方式进行拦截 Hook,才能过掉 debugger。 + +使用编程猫插件用到的代码,和在浏览器中手动 Hook 基本一致。在 Hook `eval` 过瑞数 debugger 的时候,可以用到下面的代码。 + +``` +//配合编程猫专用工具进行hook +(function() { +'use strict'; + +//过 瑞数 debuger +var eval_ = window.eval; +window.eval = function(x){ + eval_(x.replace("debugger;"," ; ")); +}; +//过 瑞数 debuger检测 +window.eval.toString = eval_.toString; +})(); +``` \ No newline at end of file diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\345\270\270\350\247\201\347\232\204\351\233\252\347\242\247\345\233\276\345\217\215\347\210\254\347\255\226\347\225\245\344\270\216\347\240\264\350\247\243.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\345\270\270\350\247\201\347\232\204\351\233\252\347\242\247\345\233\276\345\217\215\347\210\254\347\255\226\347\225\245\344\270\216\347\240\264\350\247\243.md" new file mode 100644 index 00000000000..7cd7d2263bc --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\345\270\270\350\247\201\347\232\204\351\233\252\347\242\247\345\233\276\345\217\215\347\210\254\347\255\226\347\225\245\344\270\216\347\240\264\350\247\243.md" @@ -0,0 +1,49 @@ +--- +layout: post +title: JS逆向:常见的雪碧图反爬策略与破解 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + +## 1. 雪碧图 + +#### 1.1. 雪碧图 + +- 雪碧图(sprite)也叫CSS精灵, 是一CSS图像合成技术; +- 该方法是将小图标和背景图像合并到一张图片上,然后利用css的背景定位来显示需要显示的图片部分;简单说就是在一整张图片中分割出自己想要的部分,也可理解为图片截取显示(坐标的移动) ; + +#### 1.2. 雪碧图的优点 + +- 减少加载网页图片时对服务器的请求次数 + 可以合并多数背景图片和小图标,方便在任何位置使用,这样不同位置的请求只需要调用一个图片,从而减少对服务器的请求次数,降低服务器压力,同时提高了页面的加载速度,节约服务器的流量。 +- 提高页面的加载速度 + 由所需图片拼成的一张 GIF 图片的尺寸,会明显小于所有图片拼合前的大小。单张的 GIF 只有相关的一个色表,而单独分割的每一张 GIF 都有自己的一个色表,这就增加了总体的大小。因此,单独的一张 JPEG 或者 PNG sprite 在大小上非常可能比把一张图分成多张得来的图片总尺寸小。 + +## 2. 雪碧图反爬策略破解 + +#### 2.1. 反爬策略分析 + +- 目前,雪碧图在前端在反爬策略中,仍然有着普遍的应用,这里我们以某租房平台为例进行解说。 +- 如下图所示,当我们鼠标右键检查价格的时候,发现数据已经被隐藏; + + ![]({{site.baseurl}}/img-post/雪碧图-1.png) + +- 分析元素代码发现,```element.style``` 中 ```background-position``` 值为 ```-156.2px```,```background-image``` 值为 ```url(//static8.ziroom.com/phoenix/pc/images/2019/price/7ce54f64c5c0a425872683e3d1df36f4.png)```,点击 .png 地址得到下面的网页; + + ![]({{site.baseurl}}/img-post/雪碧图-2.png) + +- 通过上面的分析我们推断,**数字 6** 和上面的图片之间存在着某种映射关系,而这个关系就是 background-position 的值。为了证实这种假设,我们分析价格 **1330** 中,两个 **数字 3** 的 background-position 值是一样的; +- 据此我们基本可以断定,此网站采用雪碧图的方式,将价格数字隐藏在 + background-image 上,并通过元素的 background-position 值提取对应的数字,展示在网页上成为用户看到的价格; +- 那具体的映射关系是什么呢?我们分析数值的位置,和 background-image 图,发现每个相邻的数字之间相差 ``31`` ,最后一位的 background-position 为 ``-281``,也就是说 ``1、2、3、4、5、6、7、8、9、0`` 共计 10 个数字分别占据 background-image 图的 10 个位置,每个数字的位置相差 30 个像素; +- 此时,我们还要注意一个问题,那就是这个 background-image 是不是固定的,映射关系是不是也是固定的,我们打开多个同类的 url,发现 background-image 是动态更新的,也就是说每次请求都要去获取 background-image 图片,然后根据元素位置得到对应的数字; +- 至此,破解分析工作就已完成; + +#### 2.2. 代码实现 +- 略。 + diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\345\233\275\345\256\266\346\237\220\351\203\250\345\256\230\347\275\221\346\237\220\351\200\237\344\271\220\345\212\240\345\257\206\347\255\226\347\225\245.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\345\233\275\345\256\266\346\237\220\351\203\250\345\256\230\347\275\221\346\237\220\351\200\237\344\271\220\345\212\240\345\257\206\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..ea8df3895cb --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\345\233\275\345\256\266\346\237\220\351\203\250\345\256\230\347\275\221\346\237\220\351\200\237\344\271\220\345\212\240\345\257\206\347\255\226\347\225\245.md" @@ -0,0 +1,72 @@ +--- +layout: post +title: JS逆向:破解国家某部官网某速乐加密策略 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +本文我们以某安部的官网为例,破解某速乐的加密策略。本站的加密技术比较简单,适合小白进行练手。 + +## 1. 抓包分析 + +打开调试面板,分析请求,发现成功请求的 cookie 中有 `__jsl_s=...`,同时前三个请求返回了 521,这是某速乐的典型特征。前面的三个请求其实是 **状态码欺骗**,服务器返回的东西被浏览器请掉了。 + +![]({{site.baseurl}}/img-post/某安部-1.png) + +![]({{site.baseurl}}/img-post/某安部-2.png) + +我们想要进行逆向分析,要借助于 Fiddler 抓包工具,使用抓包工具不会出现请求返回内容被清掉的情况。如果不想使用抓包工具,也可以直接打 script 断点,然后进行断点调试,也能找到函数加密的入口,但是会比较麻烦。 +我们使用 Fiddler 进行抓包,通过分析多次请求的变化,发现在成功请求之前,浏览器向服务器发送了两次, + +#### 第一次请求: + +返回 cookie 中的 `__jsluid_s`,同时在返回的 script 中隐藏返回了 `__jsl_clearance_s`。 + +![]({{site.baseurl}}/img-post/某安部-3.png) + +执行返回的 JS 代码,可以直接得到结果,如果不嫌麻烦也可以正则替换。 + +![]({{site.baseurl}}/img-post/某安部-4.png) + +#### 第二次请求: + +前面的得到的 cookie 中的 `__jsluid_s` 和 `__jsl_clearance_s` 都被请求作为参数携带,之后服务器返回了一段新的代码。 + +![]({{site.baseurl}}/img-post/某安部-5.png) + +#### 第三次请求: + +分析发现,第三次请求的 cookie 中,`__jsl_clearance_s` 被修改了,这个修改应该是在第二次请求返回后完成的,我们继续分析第二次请求的返回内容。 + +![]({{site.baseurl}}/img-post/某安部-6.png) + +通过分析发现返回的 HTML 中有一段 script, 分析后发现这是一段 JS代码。 + +![]({{site.baseurl}}/img-post/某安部-7.png) + +返回的 JS 代码为: + +``` +var _0x44e4=['R8KjGX4=','wrlRWTI=','bsOAGhI=','FsOHwrQ=','w40ja8Ow','wphYRcKF','wpHDgz3CsA==','w7YAZMOi','wrwqw5bClA==','w6gbIFA=','w5LDhCXCkg==','bX1hVw==','wpPCicOBFg==','w5zCvkdN','UcOwdcK6','w4QxNUY=','w400c2g=','woHCp3U5','wpp5w6/Cjw==','w4IYazg=','wpwLw4Y=','ZMOZPA==','w5cZY8OR','dW4Yw7o=','ZcOHNzs=','PcKow5A8','HhHDhcKk','wpbCnVzChg==','w7UIfkA=','T0ZWw4c=','wqzCrsO0','w4wtwpc8','CMKCNgg=','w7MbYcOz','wqJ7w6nCrQ==','w6rDgEDDlw==','BcKHJQc=','OD9Swq0=','wpZXVcKP','LcOiKEk=','GMKUwqQf','woTDiXvDsg==','wqzCrUs4','w7DDuCHCrg==','w6jCmEJN','Em8Zwrg=','w6s0RMOL','wr/CtsOIHA==','V8OcwozCow==','GsKLasOz','flBCUg==','w7M7R8O2','wqfDimPDuA==','aMKgfwg=','w7rCpUzDvQ==','K8KjUcOx','GsKvwrdM','M2czwpA=','W1lXWQ==','GcKUV8O8','O1g1woE=','VsOjGxI=','w58gUgo=','HsKmwqJR','wpAew5gK','OcKGNgA=','w4s8SsOE','VMKRw7LDuQ==','wodTU8K/','w7UfQnI=','wpc/TyE=','L8Kvwo8m','bU5ww7U=','w707wrcm','wrrDoWPDtg==','FcONwrsq','w5EnMGU=','AQrDp8K5','w69Fw6sm','YMOha8Ky','QnYOwpA=','wp9CTsKN','FcKGIRM=','D8Oawo02','wpYodT4=','w7UMdhk=','w4ogWTI=','woF5bAw=','McK5LDE=','w47CsWJe','wp5BWw==','wr0Zw5PCrA==','IDLDlMKb','L8OqYsOn','w5goasOE','KH8Kwqw=','woVhUis=','w5Fjw4Iy','wrhFw6vCsw==','HVcKwpA=','YMOpwqDCrA==','w4obVVc=','WsKQw4bDhw==','wrTCpMO3Nw==','wprCq03Ckg==','wp7CgMOgPg==','acOPNDk=','wotzw4XCvA==','wqN1bMKD','esOvcA==','LXgTwos=','wqMvXhY=','w685wqc1','wo59aDA=','w5I6QXQ=','Y09sw7s=','wrNUd8Kv','wrNJw4XCig==','wr3ClEM=','aMOqDBU=','wq3CqHDCgw==','IWQJwqY=','asKhEnc=','w4TCkXhx','CMORwq8p','F1oZwoE=','wpMNw6EX','w7EBwoYK','acOhacK0','wr/CpMO9FA==','E8KeYBw=','wpV3w6LCtA==','w4E8X0M=','Zkpqw70=','eMOENCs=','w63CnURL','wonDqnTDog==','woPDtHDDog==','w7wITMOr','w70pwrUU','fsOXJyQ=','CMKew5c/','wq55XhY=','w6DDuMO4Mg==','wq1+SxI=','w7HDgFHDgA==','wrQ2w4cp','w4LCoH/Dqg==','wqjCoMOtOA==','RlQBw7g=','UUN/w7w=','w7vDqFU+','bHAWw7E=','w7Rkw7A5','w5wlb8Oy','wrM2w5TCtA==','w5DDncOtVA==','P8OwOVQ=','N8KFS8Ox','w68GYh4=','w47ClX3DlQ==','w4nCk1RB','QcKgLn0=','LcKTNjM=','Q3YBw7o=','OsK1K8Os','wo8+fis=','w4fClX/Dkw==','HMKdwrEC','DMOcwrYj','wrHDjk3Djg==','w6YOQ8OQ','wr/CkEYa','wrXCtnPCgg==','w5wsYsOs','A8Kcw6cH','w5fDhcO9Rw==','w6HDoD7ClQ==','EsOFwo8/','CcOVKH8=','6K6M5rGT6au26Kyi','M8OFO2s=','w4/DksOtVQ==','wrU3w60p','w4XDjW/DpA==','bVU9w7I=','JiwdYw==','KcKcwolg','w73DkcOtUA==','w6XDtgPCtQ==','LMO/dcOR','w5bDrXrDtQ==','P8KgJhY=','Tkx9w74=','wqtDUsKl','wqdpWC4=','w5Nww4ca','w5w4MEs=','w6k+WGs=','w7bDsVTDow=='];(function(_0x4576a7,_0x44e404){var _0x401b38=function(_0x4a4191){while(--_0x4a4191){_0x4576a7['push'](_0x4576a7['shift']());}};_0x401b38(++_0x44e404);}(_0x44e4,0x188));var _0x401b=function(_0x4576a7,_0x44e404){_0x4576a7=_0x4576a7-0x0;var _0x401b38=_0x44e4[_0x4576a7];if(_0x401b['OTSYjC']===undefined){(function(){var _0x45325a;try{var _0x24593f=Function('return\x20(function()\x20'+'{}.constructor(\x22return\x20this\x22)(\x20)'+');');_0x45325a=_0x24593f();}catch(_0x19ea10){_0x45325a=window;}var _0x4d2b89='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';_0x45325a['atob']||(_0x45325a['atob']=function(_0x39b179){var _0x22945a=String(_0x39b179)['replace'](/=+$/,'');var _0x5b972c='';for(var _0x33b518=0x0,_0x306ddd,_0x5235c4,_0xf3fccd=0x0;_0x5235c4=_0x22945a['charAt'](_0xf3fccd++);~_0x5235c4&&(_0x306ddd=_0x33b518%0x4?_0x306ddd*0x40+_0x5235c4:_0x5235c4,_0x33b518++%0x4)?_0x5b972c+=String['fromCharCode'](0xff&_0x306ddd>>(-0x2*_0x33b518&0x6)):0x0){_0x5235c4=_0x4d2b89['indexOf'](_0x5235c4);}return _0x5b972c;});}());var _0x196aa6=function(_0xf6982,_0x2224e7){var _0x5bf124=[],_0x4ca4f6=0x0,_0x34c181,_0x5e3618='',_0x38fc61='';_0xf6982=atob(_0xf6982);for(var _0x3f6d4b=0x0,_0x3cd74e=_0xf6982['length'];_0x3f6d4b<_0x3cd74e;_0x3f6d4b++){_0x38fc61+='%'+('00'+_0xf6982['charCodeAt'](_0x3f6d4b)['toString'](0x10))['slice'](-0x2);}_0xf6982=decodeURIComponent(_0x38fc61);var _0x37b560;for(_0x37b560=0x0;_0x37b560<0x100;_0x37b560++){_0x5bf124[_0x37b560]=_0x37b560;}for(_0x37b560=0x0;_0x37b560<0x100;_0x37b560++){_0x4ca4f6=(_0x4ca4f6+_0x5bf124[_0x37b560]+_0x2224e7['charCodeAt'](_0x37b560%_0x2224e7['length']))%0x100;_0x34c181=_0x5bf124[_0x37b560];_0x5bf124[_0x37b560]=_0x5bf124[_0x4ca4f6];_0x5bf124[_0x4ca4f6]=_0x34c181;}_0x37b560=0x0;_0x4ca4f6=0x0;for(var _0xdb9f26=0x0;_0xdb9f26<_0xf6982['length'];_0xdb9f26++){_0x37b560=(_0x37b560+0x1)%0x100;_0x4ca4f6=(_0x4ca4f6+_0x5bf124[_0x37b560])%0x100;_0x34c181=_0x5bf124[_0x37b560];_0x5bf124[_0x37b560]=_0x5bf124[_0x4ca4f6];_0x5bf124[_0x4ca4f6]=_0x34c181;_0x5e3618+=String['fromCharCode'](_0xf6982['charCodeAt'](_0xdb9f26)^_0x5bf124[(_0x5bf124[_0x37b560]+_0x5bf124[_0x4ca4f6])%0x100]);}return _0x5e3618;};_0x401b['eKkzWd']=_0x196aa6;_0x401b['UWlWwl']={};_0x401b['OTSYjC']=!![];}var _0x4a4191=_0x401b['UWlWwl'][_0x4576a7];if(_0x4a4191===undefined){if(_0x401b['OUtcIL']===undefined){_0x401b['OUtcIL']=!![];}_0x401b38=_0x401b['eKkzWd'](_0x401b38,_0x44e404);_0x401b['UWlWwl'][_0x4576a7]=_0x401b38;}else{_0x401b38=_0x4a4191;}return _0x401b38;};function hash(_0x2d6f49){var _0x23943a={};_0x23943a[_0x401b('0xa1','CCc&')+'J']=function(_0x48678e,_0x208f8f){return _0x48678e+_0x208f8f;};_0x23943a[_0x401b('0x24','R73i')+'n']=function(_0x546f8b,_0x176ca1){return _0x546f8b&_0x176ca1;};_0x23943a[_0x401b('0x54','O6@t')+'L']=_0x401b('0x3d','i[he')+_0x401b('0xad','f]NG')+_0x401b('0x97','uZ3e')+_0x401b('0x2c','DFKj');_0x23943a[_0x401b('0x78','$5p*')+'C']=function(_0x4304cb,_0x4244d0){return _0x4304cb>>_0x4244d0;};_0x23943a[_0x401b('0xc7','aTiX')+'n']=function(_0x48f640,_0x368d5f){return _0x48f640*_0x368d5f;};_0x23943a[_0x401b('0x10','[@8c')+'E']=function(_0x18d799,_0x411000){return _0x18d799*_0x411000;};_0x23943a[_0x401b('0x3b','REkM')+'O']=function(_0x127030,_0x94cc1c){return _0x127030>>_0x94cc1c;};_0x23943a[_0x401b('0x63','[@8c')+'D']=function(_0x262640,_0x4e1fd4){return _0x262640&_0x4e1fd4;};_0x23943a[_0x401b('0xba','b7ZU')+'s']=function(_0x29bb6a,_0x26fdbe){return _0x29bb6a-_0x26fdbe;};_0x23943a[_0x401b('0x21','m]7y')+'X']=function(_0x13a082,_0x3ec0ee){return _0x13a082*_0x3ec0ee;};_0x23943a[_0x401b('0xc1','G2qO')+'W']=function(_0xb7af84,_0x31f46c){return _0xb7af84*_0x31f46c;};_0x23943a[_0x401b('0x65','U$wh')+'k']=function(_0xc13949,_0xf5c8dc){return _0xc13949|_0xf5c8dc;};_0x23943a[_0x401b('0x18','$[h*')+'P']=function(_0x1ff6d2,_0x4e47a0){return _0x1ff6d2-_0x4e47a0;};_0x23943a[_0x401b('0x61','6I7g')+'Y']=function(_0x527f7e,_0x32567d){return _0x527f7e<_0x32567d;};_0x23943a[_0x401b('0x4a','K56X')+'n']=function(_0x49b82d,_0x985bc0){return _0x49b82d&_0x985bc0;};_0x23943a[_0x401b('0x4e','BXge')+'N']=function(_0x23567d,_0x503524){return _0x23567d^_0x503524;};_0x23943a[_0x401b('0x1','qZ*J')+'g']=function(_0x12985f,_0x388fe5){return _0x12985f&_0x388fe5;};_0x23943a[_0x401b('0x51','$5p*')+'R']=function(_0x44b66a,_0xfeb37e){return _0x44b66a<_0xfeb37e;};_0x23943a[_0x401b('0x27','$5p*')+'m']=function(_0x456daa,_0xed8d37){return _0x456daa<_0xed8d37;};_0x23943a[_0x401b('0x33','jfD(')+'B']=_0x401b('0x3a','0o5]')+_0x401b('0xb','b7ZU');_0x23943a[_0x401b('0x1b','6*F9')+'Q']=function(_0x4ebf50,_0x5758e2){return _0x4ebf50<_0x5758e2;};_0x23943a[_0x401b('0x71','uZ3e')+'g']=function(_0x518f1f,_0x32d4a0){return _0x518f1f!==_0x32d4a0;};_0x23943a[_0x401b('0x39','#ulG')+'N']=_0x401b('0xb3','REkM')+'g';_0x23943a[_0x401b('0x43','#ulG')+'g']=function(_0x25ca1c,_0x3cee16,_0x2d520b){return _0x25ca1c(_0x3cee16,_0x2d520b);};_0x23943a[_0x401b('0xbb','i[he')+'A']=function(_0x26644a,_0x230625){return _0x26644a^_0x230625;};_0x23943a[_0x401b('0xbe','N3U2')+'l']=function(_0xefb59a,_0x58a7d7){return _0xefb59a-_0x58a7d7;};_0x23943a[_0x401b('0x83','6I7g')+'f']=function(_0x112912,_0x4c88c3){return _0x112912-_0x4c88c3;};_0x23943a[_0x401b('0x5f','cYgH')+'N']=function(_0x408461,_0x1fa261,_0x342874){return _0x408461(_0x1fa261,_0x342874);};_0x23943a[_0x401b('0x66','REkM')+'B']=function(_0x211d89,_0x2be022){return _0x211d89(_0x2be022);};_0x23943a[_0x401b('0x20','@mcJ')+'D']=function(_0x4ebe5d,_0x348e53,_0x53b4ff){return _0x4ebe5d(_0x348e53,_0x53b4ff);};_0x23943a[_0x401b('0xac','G2qO')+'Z']=function(_0x491eb4,_0x42f16a){return _0x491eb4+_0x42f16a;};_0x23943a[_0x401b('0x4d','R73i')+'E']=function(_0x10ae28,_0x2ea3c5){return _0x10ae28+_0x2ea3c5;};var _0x4c9fc4=_0x23943a;function _0x823cc5(_0x4e066a,_0x2ab238){return _0x4c9fc4[_0x401b('0x7c','T3D4')+'J'](_0x4e066a&0x7fffffff,_0x4c9fc4[_0x401b('0x64','IpfQ')+'n'](_0x2ab238,0x7fffffff))^_0x4c9fc4[_0x401b('0x24','R73i')+'n'](_0x4e066a,0x80000000)^_0x2ab238&0x80000000;}function _0x108d92(_0x4348f4){var _0x2cff27=_0x4c9fc4[_0x401b('0x9a','[JgK')+'L'];var _0x1c8ce8='';for(var _0x343cb4=0x7;_0x343cb4>=0x0;_0x343cb4--){_0x1c8ce8+=_0x2cff27[_0x401b('0x47','M%y&')+'At'](_0x4c9fc4[_0x401b('0x3e','[ueK')+'n'](_0x4c9fc4[_0x401b('0x81','KObJ')+'C'](_0x4348f4,_0x4c9fc4[_0x401b('0x14','uZ3e')+'n'](_0x343cb4,0x4)),0xf));}return _0x1c8ce8;}function _0x1c669f(_0x145107){var _0x18fbe3=_0x4c9fc4[_0x401b('0x2','T3D4')+'C'](_0x145107[_0x401b('0xe','KGNL')+'th']+0x8,0x6)+0x1,_0x46045c=new Array(_0x4c9fc4[_0x401b('0xbf','[JgK')+'n'](_0x18fbe3,0x10));for(var _0x257f0c=0x0;_0x257f0c<_0x4c9fc4[_0x401b('0x76','KObJ')+'E'](_0x18fbe3,0x10);_0x257f0c++){_0x46045c[_0x257f0c]=0x0;}for(_0x257f0c=0x0;_0x257f0c<_0x145107[_0x401b('0x6f','uZ3e')+'th'];_0x257f0c++){_0x46045c[_0x4c9fc4[_0x401b('0x44','iJrI')+'O'](_0x257f0c,0x2)]|=_0x145107[_0x401b('0x30','MuIU')+_0x401b('0xa9','6I7g')+'At'](_0x257f0c)<<0x18-_0x4c9fc4[_0x401b('0x8a','HDl4')+'E'](_0x4c9fc4[_0x401b('0xb4','0d&0')+'n'](_0x257f0c,0x3),0x8);}_0x46045c[_0x257f0c>>0x2]|=0x80<<0x18-_0x4c9fc4[_0x401b('0x60','DFKj')+'D'](_0x257f0c,0x3)*0x8;_0x46045c[_0x4c9fc4[_0x401b('0xb5','XUXo')+'s'](_0x4c9fc4[_0x401b('0x0','DFKj')+'X'](_0x18fbe3,0x10),0x1)]=_0x4c9fc4[_0x401b('0xaa','KlHD')+'W'](_0x145107[_0x401b('0x5c','BXge')+'th'],0x8);return _0x46045c;}function _0x139bf6(_0x2d7286,_0x119b5b){return _0x4c9fc4[_0x401b('0xab','DFKj')+'k'](_0x2d7286<<_0x119b5b,_0x2d7286>>>_0x4c9fc4[_0x401b('0x36','K56X')+'P'](0x20,_0x119b5b));}function _0x1be9d8(_0x146cfb,_0x368fc4,_0x2c0adb,_0x33ca55){if(_0x4c9fc4[_0x401b('0x46','6*F9')+'Y'](_0x146cfb,0x14))return _0x368fc4&_0x2c0adb|_0x4c9fc4[_0x401b('0xc3','M%y&')+'n'](~_0x368fc4,_0x33ca55);if(_0x4c9fc4[_0x401b('0x87','$5p*')+'Y'](_0x146cfb,0x28))return _0x4c9fc4[_0x401b('0x5b','b7ZU')+'N'](_0x4c9fc4[_0x401b('0x22','IpfQ')+'N'](_0x368fc4,_0x2c0adb),_0x33ca55);if(_0x146cfb<0x3c)return _0x4c9fc4[_0x401b('0x98','cYgH')+'k'](_0x368fc4&_0x2c0adb,_0x4c9fc4[_0x401b('0x6','R73i')+'g'](_0x368fc4,_0x33ca55))|_0x2c0adb&_0x33ca55;return _0x368fc4^_0x2c0adb^_0x33ca55;}function _0xd86c62(_0x16001e){return _0x4c9fc4[_0x401b('0x1e','K56X')+'R'](_0x16001e,0x14)?0x5a827999:_0x4c9fc4[_0x401b('0x96','cYgH')+'R'](_0x16001e,0x28)?0x6ed9eba1:_0x4c9fc4[_0x401b('0x8b','o13F')+'m'](_0x16001e,0x3c)?-0x70e44324:-0x359d3e2a;}var _0x23fff8=_0x1c669f(_0x2d6f49);var _0x6017f0=new Array(0x50);var _0x3ecb42=0x67452301;var _0x212bba=-0x10325477;var _0x46c23e=-0x67452302;var _0x29c94c=0x10325476;var _0x162bfb=-0x3c2d1e10;for(var _0xdf787a=0x0;_0xdf787a<_0x23fff8[_0x401b('0x9f','8atC')+'th'];_0xdf787a+=0x10){var _0x4b219b=_0x3ecb42;var _0x274b5a=_0x212bba;var _0x1eff1d=_0x46c23e;var _0x2f1c66=_0x29c94c;var _0x3a19c6=_0x162bfb;for(var _0xfc52a8=0x0;_0xfc52a8<0x50;_0xfc52a8++){if(_0x4c9fc4[_0x401b('0xa3','[@8c')+'Q'](_0xfc52a8,0x10)){_0x6017f0[_0xfc52a8]=_0x23fff8[_0xdf787a+_0xfc52a8];}else{if(_0x4c9fc4[_0x401b('0xa0','G2qO')+'g'](_0x401b('0x56','CCc&')+'g',_0x4c9fc4[_0x401b('0x41','iJrI')+'N'])){var _0x3d56a5=window[_0x401b('0x2d','n8g5')+_0x401b('0x2e','T3D4')+'r'][_0x401b('0xae','BXge')+_0x401b('0x3','cYgH')+'t'],_0x20cf0c=[_0x4c9fc4[_0x401b('0x1a','o13F')+'B']];for(var _0x5e1d72=0x0;_0x5e1d72<_0x20cf0c[_0x401b('0x50','qZ*J')+'th'];_0x5e1d72++){if(_0x3d56a5[_0x401b('0xd','T3D4')+_0x401b('0x7e','XUXo')](_0x20cf0c[_0x5e1d72])!=-0x1){return!![];}}if(window[_0x401b('0xb6','REkM')+_0x401b('0x9e','qZ*J')+_0x401b('0x1d','@mcJ')]||window[_0x401b('0x70','XUXo')+_0x401b('0x59','T3D4')]||window[_0x401b('0x93','$5p*')+_0x401b('0x31','!7Hr')]||window[_0x401b('0x57','f]NG')+_0x401b('0x88','f]NG')+'r'][_0x401b('0x2b','aTiX')+_0x401b('0xa4','N3U2')+'r']||window[_0x401b('0x28','DFKj')+_0x401b('0x40','M%y&')+'r'][_0x401b('0x9','cYgH')+_0x401b('0x90','QEj2')+_0x401b('0x49','DFKj')+_0x401b('0x29','K56X')+'e']||window[_0x401b('0xaf','[ueK')+_0x401b('0x8d','qZ*J')+'r'][_0x401b('0x17','O6@t')+_0x401b('0x7b','qZ*J')+_0x401b('0x4c','T3D4')+_0x401b('0x4','CCc&')+_0x401b('0x42','0o5]')]){return!![];}}else{_0x6017f0[_0xfc52a8]=_0x4c9fc4[_0x401b('0x3c','QEj2')+'g'](_0x139bf6,_0x4c9fc4[_0x401b('0x86','[JgK')+'N'](_0x4c9fc4[_0x401b('0x67','dxjJ')+'A'](_0x6017f0[_0x4c9fc4[_0x401b('0x19','0d&0')+'l'](_0xfc52a8,0x3)],_0x6017f0[_0x4c9fc4[_0x401b('0x2a','o13F')+'f'](_0xfc52a8,0x8)]),_0x6017f0[_0xfc52a8-0xe])^_0x6017f0[_0x4c9fc4[_0x401b('0x25','qZ*J')+'f'](_0xfc52a8,0x10)],0x1);}}t=_0x823cc5(_0x823cc5(_0x4c9fc4[_0x401b('0x5','O6@t')+'g'](_0x139bf6,_0x3ecb42,0x5),_0x1be9d8(_0xfc52a8,_0x212bba,_0x46c23e,_0x29c94c)),_0x823cc5(_0x4c9fc4[_0x401b('0x37','uZ3e')+'N'](_0x823cc5,_0x162bfb,_0x6017f0[_0xfc52a8]),_0x4c9fc4[_0x401b('0x69','CCc&')+'B'](_0xd86c62,_0xfc52a8)));_0x162bfb=_0x29c94c;_0x29c94c=_0x46c23e;_0x46c23e=_0x4c9fc4[_0x401b('0xb9','jfD(')+'N'](_0x139bf6,_0x212bba,0x1e);_0x212bba=_0x3ecb42;_0x3ecb42=t;}_0x3ecb42=_0x4c9fc4[_0x401b('0x4b','UHd8')+'N'](_0x823cc5,_0x3ecb42,_0x4b219b);_0x212bba=_0x4c9fc4[_0x401b('0xb7','m]7y')+'N'](_0x823cc5,_0x212bba,_0x274b5a);_0x46c23e=_0x823cc5(_0x46c23e,_0x1eff1d);_0x29c94c=_0x823cc5(_0x29c94c,_0x2f1c66);_0x162bfb=_0x4c9fc4[_0x401b('0xb8','N3U2')+'D'](_0x823cc5,_0x162bfb,_0x3a19c6);}return _0x4c9fc4[_0x401b('0xa6','#ulG')+'Z'](_0x4c9fc4[_0x401b('0x23','XUXo')+'E'](_0x4c9fc4[_0x401b('0x8c','R73i')+'B'](_0x108d92,_0x3ecb42),_0x108d92(_0x212bba))+_0x4c9fc4[_0x401b('0x89','uZ3e')+'B'](_0x108d92,_0x46c23e)+_0x4c9fc4[_0x401b('0x73','o13F')+'B'](_0x108d92,_0x29c94c),_0x108d92(_0x162bfb));}function go(_0x5048ef){var _0x15516d={};_0x15516d[_0x401b('0x72','HkAk')+'K']=_0x401b('0x9e','qZ*J')+_0x401b('0x26','uZ3e');_0x15516d[_0x401b('0xc','K56X')+'X']=function(_0x5057a9,_0x367059){return _0x5057a9!=_0x367059;};_0x15516d[_0x401b('0x84','b7ZU')+'n']=function(_0x4da8d9,_0x4f0417){return _0x4da8d9<_0x4f0417;};_0x15516d[_0x401b('0x9b','[ueK')+'N']=function(_0x3fb843,_0x1acc8b){return _0x3fb843+_0x1acc8b;};_0x15516d[_0x401b('0x92','REkM')+'q']=function(_0x56cc2b,_0x5cb583){return _0x56cc2b(_0x5cb583);};_0x15516d[_0x401b('0x15','6I7g')+'e']=_0x401b('0x58','dxjJ')+_0x401b('0xc0','!7Hr')+'=';_0x15516d[_0x401b('0x4f','MuIU')+'S']=function(_0x28e7e5,_0x1303a3,_0x1164dd){return _0x28e7e5(_0x1303a3,_0x1164dd);};_0x15516d[_0x401b('0x79','cYgH')+'i']=function(_0x30f796,_0x1163c2){return _0x30f796>_0x1163c2;};_0x15516d[_0x401b('0xb2','!7Hr')+'M']=function(_0x46612f,_0x524547){return _0x46612f-_0x524547;};_0x15516d[_0x401b('0x6c','o1mN')+'t']=_0x401b('0xbc','KObJ')+'失败';var _0xc404ee=_0x15516d;function _0x47329e(){var _0x481558=window[_0x401b('0x94','@mcJ')+_0x401b('0xa5','i[he')+'r'][_0x401b('0x2f','i[he')+_0x401b('0x8','KlHD')+'t'],_0x34b55a=[_0xc404ee[_0x401b('0x5e','6*F9')+'K']];for(var _0x2f5b7a=0x0;_0x2f5b7a<_0x34b55a[_0x401b('0x82','KlHD')+'th'];_0x2f5b7a++){if(_0xc404ee[_0x401b('0x52','!7Hr')+'X'](_0x481558[_0x401b('0xf','K56X')+_0x401b('0x62','cYgH')](_0x34b55a[_0x2f5b7a]),-0x1)){return!![];}}if(window[_0x401b('0xc5','jfD(')+_0x401b('0x7f','HkAk')+_0x401b('0x75','f]NG')]||window[_0x401b('0x16','f]NG')+_0x401b('0xb1','b7ZU')]||window[_0x401b('0xc6','U$wh')+_0x401b('0x99','aTiX')]||window[_0x401b('0x45','HkAk')+_0x401b('0xa8','[ueK')+'r'][_0x401b('0x53','b7ZU')+_0x401b('0x55','IpfQ')+'r']||window[_0x401b('0x5d','6*F9')+_0x401b('0xb0','MuIU')+'r'][_0x401b('0x7d','o13F')+_0x401b('0x6e','UHd8')+_0x401b('0xc4','N3U2')+_0x401b('0x95','m]7y')+'e']||window[_0x401b('0x85','dxjJ')+_0x401b('0x8f','6I7g')+'r'][_0x401b('0x1f','G2qO')+_0x401b('0x34','6I7g')+_0x401b('0xc2','D]Y!')+_0x401b('0x38','o1mN')+_0x401b('0x3f','#ulG')]){return!![];}};if(_0x47329e()){return;}var _0x4fc19c=new Date();function _0x3f7a6d(_0x175568,_0xfe0b72){var _0x3b19c1=_0x5048ef[_0x401b('0x91','QEj2')+'s'][_0x401b('0x9d','G2qO')+'th'];for(var _0x5a84af=0x0;_0x5a84af<_0x3b19c1;_0x5a84af++){for(var _0x4c66d9=0x0;_0xc404ee[_0x401b('0x6b','iJrI')+'n'](_0x4c66d9,_0x3b19c1);_0x4c66d9++){var _0x28a628=_0xc404ee[_0x401b('0x7a','$[h*')+'N'](_0xc404ee[_0x401b('0xbd','i[he')+'N'](_0xfe0b72[0x0]+_0x5048ef[_0x401b('0xa2','REkM')+'s'][_0x401b('0x6d','$[h*')+'tr'](_0x5a84af,0x1),_0x5048ef[_0x401b('0xa2','REkM')+'s'][_0x401b('0x11','O6@t')+'tr'](_0x4c66d9,0x1)),_0xfe0b72[0x1]);if(_0xc404ee[_0x401b('0x13','0o5]')+'q'](hash,_0x28a628)==_0x175568){return[_0x28a628,new Date()-_0x4fc19c];}}}};var _0x39fec4=_0xc404ee[_0x401b('0x12','jfD(')+'S'](_0x3f7a6d,_0x5048ef['ct'],_0x5048ef[_0x401b('0x1c','[JgK')]);if(_0x39fec4){var _0x1dc236;if(_0x5048ef['wt']){_0x1dc236=_0xc404ee[_0x401b('0x80','XUXo')+'i'](parseInt(_0x5048ef['wt']),_0x39fec4[0x1])?_0xc404ee[_0x401b('0x77','BXge')+'M'](parseInt(_0x5048ef['wt']),_0x39fec4[0x1]):0x1f4;}else{_0x1dc236=0x5dc;}_0xc404ee[_0x401b('0x6a','o13F')+'S'](setTimeout,function(){document[_0x401b('0x68','cYgH')+'ie']=_0xc404ee[_0x401b('0x35','dxjJ')+'N'](_0xc404ee[_0x401b('0x7','aTiX')+'N'](_0x5048ef['tn']+'='+_0x39fec4[0x0],_0xc404ee[_0x401b('0xa','HkAk')+'e']),_0x5048ef['vt'])+(_0x401b('0x32','8atC')+_0x401b('0xa7','O6@t')+'\x20/');location[_0x401b('0x8e','@mcJ')]=location[_0x401b('0x9c','uZ3e')+_0x401b('0x48','[JgK')]+location[_0x401b('0x5a','DFKj')+'ch'];},_0x1dc236);}else{alert(_0xc404ee[_0x401b('0x74','T3D4')+'t']);}};go({"bts":["1640352135.056|0|09R","YO0nUDN4n9GAdr1E0z1QbI%3D"],"chars":"BlfYQTtCkItHcOOLSaFyqB","ct":"b6d22860a990b7757d364de7228e04e4bfc18429","ha":"sha1","tn":"__jsl_clearance_s","vt":"3600","wt":"1500"}) +``` + +## 2. 逆向分析 + +格式美化后复制到 Notepad 并折叠展示。 + +![]({{site.baseurl}}/img-post/某安部-8.png) + +观察发现代码都已经被做了混淆,根据经验判断是 ob 混淆,我们用反混淆工具 http://www.jsnice.org/ 进行反混淆操作。 + +![]({{site.baseurl}}/img-post/某安部-9.png) + +打开乐易调试工具,将反混淆后的 JS 代码复制进去。根据报错,我们依次添加 `var targetLocale = UA值`、`document={};`、注释掉 `if (window["callPhantom"] || window["_phantom"]`、注释 `location["href"]=...`、注释 `setTimeout(function() {...`、导出 `document["cookie"]`、赋值 `arg={"bts" : ["16403 ...`、执行 `go(arg)`。 +最后得到返回值:`__jsl_clearance_s=1640352135.056|0|09RqBYO0nUDN4n9GAdr1E0z1QbI%3D;Max-age=3600; path = /`。 + +![]({{site.baseurl}}/img-post/某安部-10.png) diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\344\270\223\345\210\251\346\243\200\347\264\242\347\275\221\346\227\240\351\231\220\345\217\215 debug \351\227\256\351\242\230.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\344\270\223\345\210\251\346\243\200\347\264\242\347\275\221\346\227\240\351\231\220\345\217\215 debug \351\227\256\351\242\230.md" new file mode 100644 index 00000000000..c8351012e70 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\344\270\223\345\210\251\346\243\200\347\264\242\347\275\221\346\227\240\351\231\220\345\217\215 debug \351\227\256\351\242\230.md" @@ -0,0 +1,89 @@ +--- +layout: post +title: JS逆向:破解某专利检索网无限反 debug 问题 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + +# 1. 网页反 debug 机制分析 +- 反 debug 调试措施,是通过在代码中 **添加 debugger 代码** 实现,通过 debugger 阻止非法用户调试代码,一般会同时通过内部死循环逻辑让网页呈现无限 debug 状态; +- 打开 **国家知识产权局专利检索网页**( [http://cpquery.cnipa.gov.cn/](http://cpquery.cnipa.gov.cn/) ),打开网页调试模式,随机出现下面的状况; + +![]({{site.baseurl}}/img-post/zscq-1.png) + +- 将跳出的代码格式化,我们发现 **debugger** 字符; + +![]({{site.baseurl}}/img-post/zscq-2.png) +- 全局搜索 debugger,我们发现如下代码逻辑,可见使用了递归循环调用 debugger,以实现无限 debugger 的效果; + +![]({{site.baseurl}}/img-post/zscq-3.png) + +# 2. 解决方法 + +## 2.1. 禁用浏览器断点 +- 如下图,点击箭头指示位置,禁用浏览器断点; + ![]({{site.baseurl}}/img-post/zscq-4.png) +- 之后按键盘 F8,问题解决; + ![]({{site.baseurl}}/img-post/zscq-5.png) + +- 问题: + -- **禁用浏览器断点后将无法下断点、调试代码,所以此方法并不理想;** + -- **这种方法一般用于 **获取网页元素**,不能用于调试过程; +## 2.2. Fiddler 绕过 debbuger +- 有两个断点位置: + -- before request:也就是发送请求之后,但是Fiddler代理中转之前,这时可以修改请求的数据; + -- after response:也就是服务器响应之后,但是在Fiddler将响应中转给客户端之前,这时可以修改响应的结果; +### 2.2.1. 下断方法一:为指定网站的请求下断点 + +-- 打开 fiddler,找到下图箭头指向位置: + +![]({{site.baseurl}}/img-post/zscq-6.png) +-- before request +``` +bpu cpquery.cnipa.gov.cn +``` +-- after response +``` +bpafter cpquery.cnipa.gov.cn +``` +-- 消除 before request 断点 +``` +bpu +``` +-- 消除 after response 断点 +``` +bpafter +``` +### 2.2.2. 下断方法二:设置全局断点 +- 打开 fiddler,依次点击 **Rules -> Automatic Breakpoints -> Before Reuqests** ; + +![]({{site.baseurl}}/img-post/zscq-7.png) + +- 刷新网页,得到下图所示红色提示,表示下断成功(点击 **Go** 按钮可以进行下一步); + +![]({{site.baseurl}}/img-post/zscq-8.png) + +- 修改参数,发起请求; + +![]({{site.baseurl}}/img-post/zscq-9.png) + +- 清除断点,依次点击:**rules -> automatic breakpoint -> disabled**; + +## 2.3. 条件断点(conditional breakpoint) +- 找到 debugger 代码所在位置,在代码行天剑条件断点,设置一个 **结果值为 False 的条件**,这样就不会调用 debugger; + +![]({{site.baseurl}}/img-post/zscq-10.png) +- 在上面的位置输入下面的条件,然后 enter 输入 +``` +1===0 +``` +![]({{site.baseurl}}/img-post/zscq-11.png) +- 由于 **debugger 代码出现在多个位置**,所以需要多次添加上面的 1===0 条件断点; + + + diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\346\211\213\346\234\272\346\270\270\346\210\217\345\225\206\345\223\201\345\206\205\350\264\255\345\212\237\350\203\275.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\346\211\213\346\234\272\346\270\270\346\210\217\345\225\206\345\223\201\345\206\205\350\264\255\345\212\237\350\203\275.md" new file mode 100644 index 00000000000..4a85193d4a2 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\346\211\213\346\234\272\346\270\270\346\210\217\345\225\206\345\223\201\345\206\205\350\264\255\345\212\237\350\203\275.md" @@ -0,0 +1,96 @@ +--- +layout: post +title: 安卓逆向:破解某手机游戏商品内购功能 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 安卓逆向 +--- + +# 0x00. 前言 + +- 本文介绍的内容,是对某手机游戏进行逆向分析,破解商品内购功能,从而实现不用实际支付就可以完成商品购买; +- **法律风险提示:本文内容主要为交流和分享安卓逆向技术,严禁用于任何商业用途!** + +# 0x01. 环境 + +- Windows 10; +- JEB 版本 3.0.0.201808031948 Beta; +- 雷电模拟器 3.94; +- Android Killer V1.3.1.0; +- Android Device Monitor 25.2.5; + +# 0x02. 下载 +- 下载游戏 + -- 链接:https://pan.baidu.com/s/1gYYND0R29xYCxoddM1E8Lw,提取码:m1qu; +- 下载 JEB + -- 链接:https://pan.baidu.com/s/1pQWDjA3_rChRzvVWMEtzgA,提取码:wbfw; +- 下载 Android Killer + -- 链接:https://pan.baidu.com/s/15kWi_-9HCHceoJyrZCswpA,提取码:4bos; +- Android Device Monitor + -- 在本机 SDK 目录下 sdk\tools\lib\monitor-x86_64 文件夹中; +- 雷电模拟器,请自行下载; + +# 0x03. 分析 +- 启动雷电模拟器; +- 安装 apk +``` +adb install apk绝对路径 +``` + +- 在模拟器打开app; +- 打开 cmd 命令行,输入下面的命令; +``` +adb shell dumpsys activity activities +``` +- 获取 app 的 packageName,结果在下图红框位置; + + ![]({{site.baseurl}}/img-post/手机内购-1.png) + +- 打开 Android Device Monitor,找到 packageName 对应的 PID; + + ![]({{site.baseurl}}/img-post/手机内购-2.png) + +- 添加 LogCat 过滤器; + + ![]({{site.baseurl}}/img-post/手机内购-3.png) + +- 在模拟器上,按照下图所示点击操作 APP: + + ![]({{site.baseurl}}/img-post/手机内购-4.png) + +- 在 Android Device Monitor 查看输出日志,从日志输出中获取可以入手分析的字符串,如下图; + + ![]({{site.baseurl}}/img-post/手机内购-5.png) + +- 打开 JEB,Ctrl + F 弹出查找弹窗,输入 **请求支付,计费id:**,找到目标代码位置; + + ![]({{site.baseurl}}/img-post/手机内购-6.png) + +- 按 Tab 键,切换为 JAVA 代码视图,分析代码发现 **payCancel**、**payFailed**、**paySuccess** 三个方法都调用了 **onResult** 方法,如下图; + + ![]({{site.baseurl}}/img-post/手机内购-7.png) + +- 鼠标双击 **onResult**,跳至 **onResult** 方法代码段,通过分析发下如果 **onResult** 方法第二个参数为 **0** 则支付成功; + + ![]({{site.baseurl}}/img-post/手机内购-8.png) + +# 0x04. 修改源码 +- 确定思路:即在源代码中修改 **onResult** 代码的第二个参数,使之始终为 **0**,则可以实现不支付就能完成购买; +- 在 JEB 中找到下图红框位置,**org/cocos2dx/cpp/AppActivity** 就是源码所在的文件位置; + + ![]({{site.baseurl}}/img-post/手机内购-9.png) + +- 打开 Android Killer,找到 **org/cocos2dx/cpp/AppActivity** 文件并打开; +- 找到 onResult 方法,按下图所示添加代码 **const/4 p1, 0**,这里的 **p1** 表示 onResult 方法的第二个变量,这句代码表示 第二个变量恒为 **0**; + + ![]({{site.baseurl}}/img-post/手机内购-10.png) + +# 0x05. 回编译并安装运行 +- 回编译 apk,并安装到模拟器上; +- 重新打开 app,点击要购买的道具任务,在最后一步支付时、点击叉号取消支付,返回主页发现内购成功; + + ![]({{site.baseurl}}/img-post/手机内购-11.png) diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\346\237\220\346\226\207\344\271\246\347\275\221\345\217\215\347\210\254\350\231\253JS\345\212\240\345\257\206\347\255\226\347\225\245.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\346\237\220\346\226\207\344\271\246\347\275\221\345\217\215\347\210\254\350\231\253JS\345\212\240\345\257\206\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..fd2396ec903 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\346\237\220\346\226\207\344\271\246\347\275\221\345\217\215\347\210\254\350\231\253JS\345\212\240\345\257\206\347\255\226\347\225\245.md" @@ -0,0 +1,1479 @@ +--- +layout: post +title: JS逆向:破解某某文书网反爬虫JS加密策略 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +## 1. 破解返回结果 + +#### 1.1. 分析 response + +- 打开网站,在输入框输入查询关键字【吴*凡】,分析 response 发现找不到返回的数据,但是却能找到以下内容: + + ![]({{site.baseurl}}/img-post/文书网-1.png) + +- 根据经验推测,这里的 result 结果就是我们要抓取的数据,只不过被某种加密方式加密了,我们无法直接获取。 +- 这里我们注意到,返回的 response 中包含一个 secretKey,我们猜测这个 secretKey 就是解密 result 的关键钥匙。 + +#### 1.2. 逆向过程 + +- 全局搜搜 secretKey,找到下面的代码,通过分析代码我们推测这里就是加密代码; + + ![]({{site.baseurl}}/img-post/文书网-2.png) + +- 在此处打上断点,进行翻页调试,发现该段代码最后返回的结果 ```data.result``` 的值就是 ```result```; + + ![]({{site.baseurl}}/img-post/文书网-3.png) + +- 在调试的过程中,我们注意到此处加密的函数为 ``` DES3.decrypt```,那这个函数具体执行步骤是什么呢?我们进行回溯分析,并找到下面的代码: + + ![]({{site.baseurl}}/img-post/文书网-4.png) + +#### 1.3. DES3.decrypt 代码逻辑 + +- 通过下段调试,得到解密的 js 代码。 + +``` +'use strict'; +var CryptoJS = CryptoJS || function(e, m) { + var p = {}; + var j = p.lib = {}; + /** + * @return {undefined} + */ + var l = function() { + }; + var Base = j.Base = { + extend : function(opts) { + l.prototype = this; + var c = new l; + if (opts) { + c.mixIn(opts); + } + if (!c.hasOwnProperty("init")) { + /** + * @return {undefined} + */ + c.init = function() { + c.$super.init.apply(this, arguments); + }; + } + c.init.prototype = c; + c.$super = this; + return c; + }, + create : function() { + var instance = this.extend(); + instance.init.apply(instance, arguments); + return instance; + }, + init : function() { + }, + mixIn : function(properties) { + var property; + for (property in properties) { + if (properties.hasOwnProperty(property)) { + this[property] = properties[property]; + } + } + if (properties.hasOwnProperty("toString")) { + this.toString = properties.toString; + } + }, + clone : function() { + return this.init.prototype.extend(this); + } + }; + var n = j.WordArray = Base.extend({ + init : function(a, c) { + a = this.words = a || []; + this.sigBytes = c != m ? c : 4 * a.length; + }, + toString : function(encoder) { + return (encoder || Hex).stringify(this); + }, + concat : function(a) { + var c = this.words; + var q = a.words; + var d = this.sigBytes; + a = a.sigBytes; + this.clamp(); + if (d % 4) { + /** @type {number} */ + var b = 0; + for (; b < a; b++) { + c[d + b >>> 2] |= (q[b >>> 2] >>> 24 - 8 * (b % 4) & 255) << 24 - 8 * ((d + b) % 4); + } + } else { + if (65535 < q.length) { + /** @type {number} */ + b = 0; + for (; b < a; b = b + 4) { + c[d + b >>> 2] = q[b >>> 2]; + } + } else { + c.push.apply(c, q); + } + } + this.sigBytes += a; + return this; + }, + clamp : function() { + var a = this.words; + var c = this.sigBytes; + a[c >>> 2] &= 4294967295 << 32 - 8 * (c % 4); + /** @type {number} */ + a.length = e.ceil(c / 4); + }, + clone : function() { + var c = Base.clone.call(this); + c.words = this.words.slice(0); + return c; + }, + random : function(a) { + /** @type {!Array} */ + var b = []; + /** @type {number} */ + var d = 0; + for (; d < a; d = d + 4) { + b.push(4294967296 * e.random() | 0); + } + return new n.init(b, a); + } + }); + var b = p.enc = {}; + var Hex = b.Hex = { + stringify : function(a) { + var c = a.words; + a = a.sigBytes; + /** @type {!Array} */ + var outChance = []; + /** @type {number} */ + var d = 0; + for (; d < a; d++) { + /** @type {number} */ + var f = c[d >>> 2] >>> 24 - 8 * (d % 4) & 255; + outChance.push((f >>> 4).toString(16)); + outChance.push((f & 15).toString(16)); + } + return outChance.join(""); + }, + parse : function(a) { + var c = a.length; + /** @type {!Array} */ + var b = []; + /** @type {number} */ + var d = 0; + for (; d < c; d = d + 2) { + b[d >>> 3] |= parseInt(a.substr(d, 2), 16) << 24 - 4 * (d % 8); + } + return new n.init(b, c / 2); + } + }; + var g = b.Latin1 = { + stringify : function(a) { + var c = a.words; + a = a.sigBytes; + /** @type {!Array} */ + var outChance = []; + /** @type {number} */ + var d = 0; + for (; d < a; d++) { + outChance.push(String.fromCharCode(c[d >>> 2] >>> 24 - 8 * (d % 4) & 255)); + } + return outChance.join(""); + }, + parse : function(message) { + var d = message.length; + /** @type {!Array} */ + var c = []; + /** @type {number} */ + var b = 0; + for (; b < d; b++) { + c[b >>> 2] |= (message.charCodeAt(b) & 255) << 24 - 8 * (b % 4); + } + return new n.init(c, d); + } + }; + var f = b.Utf8 = { + stringify : function(a) { + try { + return decodeURIComponent(escape(g.stringify(a))); + } catch (d) { + throw Error("Malformed UTF-8 data"); + } + }, + parse : function(string) { + return g.parse(unescape(encodeURIComponent(string))); + } + }; + var k = j.BufferedBlockAlgorithm = Base.extend({ + reset : function() { + this._data = new n.init; + /** @type {number} */ + this._nDataBytes = 0; + }, + _append : function(a) { + if ("string" == typeof a) { + a = f.parse(a); + } + this._data.concat(a); + this._nDataBytes += a.sigBytes; + }, + _process : function(a) { + var b = this._data; + var c = b.words; + var d = b.sigBytes; + var f = this.blockSize; + /** @type {number} */ + var h = d / (4 * f); + /** @type {number} */ + h = a ? e.ceil(h) : e.max((h | 0) - this._minBufferSize, 0); + /** @type {number} */ + a = h * f; + /** @type {number} */ + d = e.min(4 * a, d); + if (a) { + /** @type {number} */ + var g = 0; + for (; g < a; g = g + f) { + this._doProcessBlock(c, g); + } + g = c.splice(0, a); + b.sigBytes -= d; + } + return new n.init(g, d); + }, + clone : function() { + var polygon = Base.clone.call(this); + polygon._data = this._data.clone(); + return polygon; + }, + _minBufferSize : 0 + }); + j.Hasher = k.extend({ + cfg : Base.extend(), + init : function(cfg) { + this.cfg = this.cfg.extend(cfg); + this.reset(); + }, + reset : function() { + k.reset.call(this); + this._doReset(); + }, + update : function(a) { + this._append(a); + this._process(); + return this; + }, + finalize : function(a) { + if (a) { + this._append(a); + } + return this._doFinalize(); + }, + blockSize : 16, + _createHelper : function(hasher) { + return function(b, cfg) { + return (new hasher.init(cfg)).finalize(b); + }; + }, + _createHmacHelper : function(a) { + return function(b, f) { + return (new s.HMAC.init(a, f)).finalize(b); + }; + } + }); + var s = p.algo = {}; + return p; +}(Math); + +(function() { + var b = CryptoJS + , a = b.lib.WordArray; + b.enc.Base64 = { + stringify: function(i) { + var j = i.words + , e = i.sigBytes + , g = this._map; + i.clamp(); + i = []; + for (var h = 0; h < e; h += 3) { + for (var c = (j[h >>> 2] >>> 24 - 8 * (h % 4) & 255) << 16 | (j[h + 1 >>> 2] >>> 24 - 8 * ((h + 1) % 4) & 255) << 8 | j[h + 2 >>> 2] >>> 24 - 8 * ((h + 2) % 4) & 255, f = 0; 4 > f && h + 0.75 * f < e; f++) { + i.push(g.charAt(c >>> 6 * (3 - f) & 63)) + } + } + if (j = g.charAt(64)) { + for (; i.length % 4; ) { + i.push(j) + } + } + return i.join("") + }, + parse: function(j) { + var k = j.length + , i = this._map + , g = i.charAt(64); + g && (g = j.indexOf(g), + -1 != g && (k = g)); + for (var g = [], h = 0, e = 0; e < k; e++) { + if (e % 4) { + var f = i.indexOf(j.charAt(e - 1)) << 2 * (e % 4) + , c = i.indexOf(j.charAt(e)) >>> 6 - 2 * (e % 4); + g[h >>> 2] |= (f | c) << 24 - 8 * (h % 4); + h++ + } + } + return a.create(g, h) + }, + _map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" + } +} +)(); + +(function(m) { + function f(l, r, n, s, d, q, p) { + l = l + (r & n | ~r & s) + d + p; + return (l << q | l >>> 32 - q) + r + } + function g(l, r, n, s, d, q, p) { + l = l + (r & s | n & ~s) + d + p; + return (l << q | l >>> 32 - q) + r + } + function e(l, r, n, s, d, q, p) { + l = l + (r ^ n ^ s) + d + p; + return (l << q | l >>> 32 - q) + r + } + function c(l, r, n, s, d, q, p) { + l = l + (n ^ (r | ~s)) + d + p; + return (l << q | l >>> 32 - q) + r + } + for (var o = CryptoJS, a = o.lib, j = a.WordArray, k = a.Hasher, a = o.algo, h = [], i = 0; 64 > i; i++) { + h[i] = 4294967296 * m.abs(m.sin(i + 1)) | 0 + } + a = a.MD5 = k.extend({ + _doReset: function() { + this._hash = new j.init([1732584193, 4023233417, 2562383102, 271733878]) + }, + _doProcessBlock: function(J, T) { + for (var V = 0; 16 > V; V++) { + var U = T + V + , N = J[U]; + J[U] = (N << 8 | N >>> 24) & 16711935 | (N << 24 | N >>> 8) & 4278255360 + } + var V = this._hash.words + , U = J[T + 0] + , N = J[T + 1] + , S = J[T + 2] + , F = J[T + 3] + , d = J[T + 4] + , L = J[T + 5] + , H = J[T + 6] + , n = J[T + 7] + , p = J[T + 8] + , E = J[T + 9] + , l = J[T + 10] + , b = J[T + 11] + , M = J[T + 12] + , K = J[T + 13] + , I = J[T + 14] + , G = J[T + 15] + , R = V[0] + , Q = V[1] + , P = V[2] + , O = V[3] + , R = f(R, Q, P, O, U, 7, h[0]) + , O = f(O, R, Q, P, N, 12, h[1]) + , P = f(P, O, R, Q, S, 17, h[2]) + , Q = f(Q, P, O, R, F, 22, h[3]) + , R = f(R, Q, P, O, d, 7, h[4]) + , O = f(O, R, Q, P, L, 12, h[5]) + , P = f(P, O, R, Q, H, 17, h[6]) + , Q = f(Q, P, O, R, n, 22, h[7]) + , R = f(R, Q, P, O, p, 7, h[8]) + , O = f(O, R, Q, P, E, 12, h[9]) + , P = f(P, O, R, Q, l, 17, h[10]) + , Q = f(Q, P, O, R, b, 22, h[11]) + , R = f(R, Q, P, O, M, 7, h[12]) + , O = f(O, R, Q, P, K, 12, h[13]) + , P = f(P, O, R, Q, I, 17, h[14]) + , Q = f(Q, P, O, R, G, 22, h[15]) + , R = g(R, Q, P, O, N, 5, h[16]) + , O = g(O, R, Q, P, H, 9, h[17]) + , P = g(P, O, R, Q, b, 14, h[18]) + , Q = g(Q, P, O, R, U, 20, h[19]) + , R = g(R, Q, P, O, L, 5, h[20]) + , O = g(O, R, Q, P, l, 9, h[21]) + , P = g(P, O, R, Q, G, 14, h[22]) + , Q = g(Q, P, O, R, d, 20, h[23]) + , R = g(R, Q, P, O, E, 5, h[24]) + , O = g(O, R, Q, P, I, 9, h[25]) + , P = g(P, O, R, Q, F, 14, h[26]) + , Q = g(Q, P, O, R, p, 20, h[27]) + , R = g(R, Q, P, O, K, 5, h[28]) + , O = g(O, R, Q, P, S, 9, h[29]) + , P = g(P, O, R, Q, n, 14, h[30]) + , Q = g(Q, P, O, R, M, 20, h[31]) + , R = e(R, Q, P, O, L, 4, h[32]) + , O = e(O, R, Q, P, p, 11, h[33]) + , P = e(P, O, R, Q, b, 16, h[34]) + , Q = e(Q, P, O, R, I, 23, h[35]) + , R = e(R, Q, P, O, N, 4, h[36]) + , O = e(O, R, Q, P, d, 11, h[37]) + , P = e(P, O, R, Q, n, 16, h[38]) + , Q = e(Q, P, O, R, l, 23, h[39]) + , R = e(R, Q, P, O, K, 4, h[40]) + , O = e(O, R, Q, P, U, 11, h[41]) + , P = e(P, O, R, Q, F, 16, h[42]) + , Q = e(Q, P, O, R, H, 23, h[43]) + , R = e(R, Q, P, O, E, 4, h[44]) + , O = e(O, R, Q, P, M, 11, h[45]) + , P = e(P, O, R, Q, G, 16, h[46]) + , Q = e(Q, P, O, R, S, 23, h[47]) + , R = c(R, Q, P, O, U, 6, h[48]) + , O = c(O, R, Q, P, n, 10, h[49]) + , P = c(P, O, R, Q, I, 15, h[50]) + , Q = c(Q, P, O, R, L, 21, h[51]) + , R = c(R, Q, P, O, M, 6, h[52]) + , O = c(O, R, Q, P, F, 10, h[53]) + , P = c(P, O, R, Q, l, 15, h[54]) + , Q = c(Q, P, O, R, N, 21, h[55]) + , R = c(R, Q, P, O, p, 6, h[56]) + , O = c(O, R, Q, P, G, 10, h[57]) + , P = c(P, O, R, Q, H, 15, h[58]) + , Q = c(Q, P, O, R, K, 21, h[59]) + , R = c(R, Q, P, O, d, 6, h[60]) + , O = c(O, R, Q, P, b, 10, h[61]) + , P = c(P, O, R, Q, S, 15, h[62]) + , Q = c(Q, P, O, R, E, 21, h[63]); + V[0] = V[0] + R | 0; + V[1] = V[1] + Q | 0; + V[2] = V[2] + P | 0; + V[3] = V[3] + O | 0 + }, + _doFinalize: function() { + var l = this._data + , p = l.words + , n = 8 * this._nDataBytes + , q = 8 * l.sigBytes; + p[q >>> 5] |= 128 << 24 - q % 32; + var d = m.floor(n / 4294967296); + p[(q + 64 >>> 9 << 4) + 15] = (d << 8 | d >>> 24) & 16711935 | (d << 24 | d >>> 8) & 4278255360; + p[(q + 64 >>> 9 << 4) + 14] = (n << 8 | n >>> 24) & 16711935 | (n << 24 | n >>> 8) & 4278255360; + l.sigBytes = 4 * (p.length + 1); + this._process(); + l = this._hash; + p = l.words; + for (n = 0; 4 > n; n++) { + q = p[n], + p[n] = (q << 8 | q >>> 24) & 16711935 | (q << 24 | q >>> 8) & 4278255360 + } + return l + }, + clone: function() { + var d = k.clone.call(this); + d._hash = this._hash.clone(); + return d + } + }); + o.MD5 = k._createHelper(a); + o.HmacMD5 = k._createHmacHelper(a) +} +)(Math); + +CryptoJS.lib.Cipher || function(C) { + var j = CryptoJS + , m = j.lib + , i = m.Base + , h = m.WordArray + , D = m.BufferedBlockAlgorithm + , g = j.enc.Base64 + , A = j.algo.EvpKDF + , B = m.Cipher = D.extend({ + cfg: i.extend(), + createEncryptor: function(b, c) { + return this.create(this._ENC_XFORM_MODE, b, c) + }, + createDecryptor: function(b, c) { + return this.create(this._DEC_XFORM_MODE, b, c) + }, + init: function(d, e, c) { + this.cfg = this.cfg.extend(c); + this._xformMode = d; + this._key = e; + this.reset() + }, + reset: function() { + D.reset.call(this); + this._doReset() + }, + process: function(b) { + this._append(b); + return this._process() + }, + finalize: function(b) { + b && this._append(b); + return this._doFinalize() + }, + keySize: 4, + ivSize: 4, + _ENC_XFORM_MODE: 1, + _DEC_XFORM_MODE: 2, + _createHelper: function(a) { + return { + encrypt: function(d, c, l) { + return ("string" == typeof c ? o : y).encrypt(a, d, c, l) + }, + decrypt: function(d, c, l) { + return ("string" == typeof c ? o : y).decrypt(a, d, c, l) + } + } + } + }); + m.StreamCipher = B.extend({ + _doFinalize: function() { + return this._process(!0) + }, + blockSize: 1 + }); + var t = j.mode = {} + , z = function(l, n, d) { + var q = this._iv; + q ? this._iv = C : q = this._prevBlock; + for (var p = 0; p < d; p++) { + l[n + p] ^= q[p] + } + } + , f = (m.BlockCipherMode = i.extend({ + createEncryptor: function(b, c) { + return this.Encryptor.create(b, c) + }, + createDecryptor: function(b, c) { + return this.Decryptor.create(b, c) + }, + init: function(b, c) { + this._cipher = b; + this._iv = c + } + })).extend(); + f.Encryptor = f.extend({ + processBlock: function(e, l) { + var d = this._cipher + , n = d.blockSize; + z.call(this, e, l, n); + d.encryptBlock(e, l); + this._prevBlock = e.slice(l, l + n) + } + }); + f.Decryptor = f.extend({ + processBlock: function(n, l) { + var r = this._cipher + , p = r.blockSize + , q = n.slice(l, l + p); + r.decryptBlock(n, l); + z.call(this, n, l, p); + this._prevBlock = q + } + }); + t = t.CBC = f; + f = (j.pad = {}).Pkcs7 = { + pad: function(q, n) { + for (var u = 4 * n, u = u - q.sigBytes % u, r = u << 24 | u << 16 | u << 8 | u, s = [], p = 0; p < u; p += 4) { + s.push(r) + } + u = h.create(s, u); + q.concat(u) + }, + unpad: function(b) { + b.sigBytes -= b.words[b.sigBytes - 1 >>> 2] & 255 + } + }; + m.BlockCipher = B.extend({ + cfg: B.cfg.extend({ + mode: t, + padding: f + }), + reset: function() { + B.reset.call(this); + var e = this.cfg + , l = e.iv + , e = e.mode; + if (this._xformMode == this._ENC_XFORM_MODE) { + var d = e.createEncryptor + } else { + d = e.createDecryptor, + this._minBufferSize = 1 + } + this._mode = d.call(e, this, l && l.words) + }, + _doProcessBlock: function(b, d) { + this._mode.processBlock(b, d) + }, + _doFinalize: function() { + var b = this.cfg.padding; + if (this._xformMode == this._ENC_XFORM_MODE) { + b.pad(this._data, this.blockSize); + var d = this._process(!0) + } else { + d = this._process(!0), + b.unpad(d) + } + return d + }, + blockSize: 4 + }); + var k = m.CipherParams = i.extend({ + init: function(b) { + this.mixIn(b) + }, + toString: function(b) { + return (b || this.formatter).stringify(this) + } + }) + , t = (j.format = {}).OpenSSL = { + stringify: function(b) { + var d = b.ciphertext; + b = b.salt; + return (b ? h.create([1398893684, 1701076831]).concat(b).concat(d) : d).toString(g) + }, + parse: function(e) { + e = g.parse(e); + var l = e.words; + if (1398893684 == l[0] && 1701076831 == l[1]) { + var d = h.create(l.slice(2, 4)); + l.splice(0, 4); + e.sigBytes -= 16 + } + return k.create({ + ciphertext: e, + salt: d + }) + } + } + , y = m.SerializableCipher = i.extend({ + cfg: i.extend({ + format: t + }), + encrypt: function(p, r, e, q) { + q = this.cfg.extend(q); + var n = p.createEncryptor(e, q); + r = n.finalize(r); + n = n.cfg; + return k.create({ + ciphertext: r, + key: e, + iv: n.iv, + algorithm: p, + mode: n.mode, + padding: n.padding, + blockSize: p.blockSize, + formatter: q.format + }) + }, + decrypt: function(l, p, d, n) { + n = this.cfg.extend(n); + p = this._parse(p, n.format); + return l.createDecryptor(d, n).finalize(p.ciphertext) + }, + _parse: function(b, d) { + return "string" == typeof b ? d.parse(b, this) : b + } + }) + , j = (j.kdf = {}).OpenSSL = { + execute: function(l, p, e, n) { + n || (n = h.random(8)); + l = A.create({ + keySize: p + e + }).compute(l, n); + e = h.create(l.words.slice(p), 4 * e); + l.sigBytes = 4 * p; + return k.create({ + key: l, + iv: e, + salt: n + }) + } + } + , o = m.PasswordBasedCipher = y.extend({ + cfg: y.cfg.extend({ + kdf: j + }), + encrypt: function(p, a, l, n) { + n = this.cfg.extend(n); + l = n.kdf.execute(l, p.keySize, p.ivSize); + n.iv = l.iv; + p = y.encrypt.call(this, p, a, l.key, n); + p.mixIn(l); + return p + }, + decrypt: function(p, a, l, n) { + n = this.cfg.extend(n); + a = this._parse(a, n.format); + l = n.kdf.execute(l, p.keySize, p.ivSize, a.salt); + n.iv = l.iv; + return y.decrypt.call(this, p, a, l.key, n) + } + }) +}(); + +(function() { + function o(d, l) { + var n = (this._lBlock >>> d ^ this._rBlock) & l; + this._rBlock ^= n; + this._lBlock ^= n << d + } + function g(d, l) { + var n = (this._rBlock >>> d ^ this._lBlock) & l; + this._lBlock ^= n; + this._rBlock ^= n << d + } + var h = CryptoJS + , f = h.lib + , e = f.WordArray + , f = f.BlockCipher + , t = h.algo + , c = [57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36, 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4] + , k = [14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10, 23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2, 41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48, 44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32] + , m = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28] + , i = [{ + "0": 8421888, + 268435456: 32768, + 536870912: 8421378, + 805306368: 2, + 1073741824: 512, + 1342177280: 8421890, + 1610612736: 8389122, + 1879048192: 8388608, + 2147483648: 514, + 2415919104: 8389120, + 2684354560: 33280, + 2952790016: 8421376, + 3221225472: 32770, + 3489660928: 8388610, + 3758096384: 0, + 4026531840: 33282, + 134217728: 0, + 402653184: 8421890, + 671088640: 33282, + 939524096: 32768, + 1207959552: 8421888, + 1476395008: 512, + 1744830464: 8421378, + 2013265920: 2, + 2281701376: 8389120, + 2550136832: 33280, + 2818572288: 8421376, + 3087007744: 8389122, + 3355443200: 8388610, + 3623878656: 32770, + 3892314112: 514, + 4160749568: 8388608, + 1: 32768, + 268435457: 2, + 536870913: 8421888, + 805306369: 8388608, + 1073741825: 8421378, + 1342177281: 33280, + 1610612737: 512, + 1879048193: 8389122, + 2147483649: 8421890, + 2415919105: 8421376, + 2684354561: 8388610, + 2952790017: 33282, + 3221225473: 514, + 3489660929: 8389120, + 3758096385: 32770, + 4026531841: 0, + 134217729: 8421890, + 402653185: 8421376, + 671088641: 8388608, + 939524097: 512, + 1207959553: 32768, + 1476395009: 8388610, + 1744830465: 2, + 2013265921: 33282, + 2281701377: 32770, + 2550136833: 8389122, + 2818572289: 514, + 3087007745: 8421888, + 3355443201: 8389120, + 3623878657: 0, + 3892314113: 33280, + 4160749569: 8421378 + }, { + "0": 1074282512, + 16777216: 16384, + 33554432: 524288, + 50331648: 1074266128, + 67108864: 1073741840, + 83886080: 1074282496, + 100663296: 1073758208, + 117440512: 16, + 134217728: 540672, + 150994944: 1073758224, + 167772160: 1073741824, + 184549376: 540688, + 201326592: 524304, + 218103808: 0, + 234881024: 16400, + 251658240: 1074266112, + 8388608: 1073758208, + 25165824: 540688, + 41943040: 16, + 58720256: 1073758224, + 75497472: 1074282512, + 92274688: 1073741824, + 109051904: 524288, + 125829120: 1074266128, + 142606336: 524304, + 159383552: 0, + 176160768: 16384, + 192937984: 1074266112, + 209715200: 1073741840, + 226492416: 540672, + 243269632: 1074282496, + 260046848: 16400, + 268435456: 0, + 285212672: 1074266128, + 301989888: 1073758224, + 318767104: 1074282496, + 335544320: 1074266112, + 352321536: 16, + 369098752: 540688, + 385875968: 16384, + 402653184: 16400, + 419430400: 524288, + 436207616: 524304, + 452984832: 1073741840, + 469762048: 540672, + 486539264: 1073758208, + 503316480: 1073741824, + 520093696: 1074282512, + 276824064: 540688, + 293601280: 524288, + 310378496: 1074266112, + 327155712: 16384, + 343932928: 1073758208, + 360710144: 1074282512, + 377487360: 16, + 394264576: 1073741824, + 411041792: 1074282496, + 427819008: 1073741840, + 444596224: 1073758224, + 461373440: 524304, + 478150656: 0, + 494927872: 16400, + 511705088: 1074266128, + 528482304: 540672 + }, { + "0": 260, + 1048576: 0, + 2097152: 67109120, + 3145728: 65796, + 4194304: 65540, + 5242880: 67108868, + 6291456: 67174660, + 7340032: 67174400, + 8388608: 67108864, + 9437184: 67174656, + 10485760: 65792, + 11534336: 67174404, + 12582912: 67109124, + 13631488: 65536, + 14680064: 4, + 15728640: 256, + 524288: 67174656, + 1572864: 67174404, + 2621440: 0, + 3670016: 67109120, + 4718592: 67108868, + 5767168: 65536, + 6815744: 65540, + 7864320: 260, + 8912896: 4, + 9961472: 256, + 11010048: 67174400, + 12058624: 65796, + 13107200: 65792, + 14155776: 67109124, + 15204352: 67174660, + 16252928: 67108864, + 16777216: 67174656, + 17825792: 65540, + 18874368: 65536, + 19922944: 67109120, + 20971520: 256, + 22020096: 67174660, + 23068672: 67108868, + 24117248: 0, + 25165824: 67109124, + 26214400: 67108864, + 27262976: 4, + 28311552: 65792, + 29360128: 67174400, + 30408704: 260, + 31457280: 65796, + 32505856: 67174404, + 17301504: 67108864, + 18350080: 260, + 19398656: 67174656, + 20447232: 0, + 21495808: 65540, + 22544384: 67109120, + 23592960: 256, + 24641536: 67174404, + 25690112: 65536, + 26738688: 67174660, + 27787264: 65796, + 28835840: 67108868, + 29884416: 67109124, + 30932992: 67174400, + 31981568: 4, + 33030144: 65792 + }, { + "0": 2151682048, + 65536: 2147487808, + 131072: 4198464, + 196608: 2151677952, + 262144: 0, + 327680: 4198400, + 393216: 2147483712, + 458752: 4194368, + 524288: 2147483648, + 589824: 4194304, + 655360: 64, + 720896: 2147487744, + 786432: 2151678016, + 851968: 4160, + 917504: 4096, + 983040: 2151682112, + 32768: 2147487808, + 98304: 64, + 163840: 2151678016, + 229376: 2147487744, + 294912: 4198400, + 360448: 2151682112, + 425984: 0, + 491520: 2151677952, + 557056: 4096, + 622592: 2151682048, + 688128: 4194304, + 753664: 4160, + 819200: 2147483648, + 884736: 4194368, + 950272: 4198464, + 1015808: 2147483712, + 1048576: 4194368, + 1114112: 4198400, + 1179648: 2147483712, + 1245184: 0, + 1310720: 4160, + 1376256: 2151678016, + 1441792: 2151682048, + 1507328: 2147487808, + 1572864: 2151682112, + 1638400: 2147483648, + 1703936: 2151677952, + 1769472: 4198464, + 1835008: 2147487744, + 1900544: 4194304, + 1966080: 64, + 2031616: 4096, + 1081344: 2151677952, + 1146880: 2151682112, + 1212416: 0, + 1277952: 4198400, + 1343488: 4194368, + 1409024: 2147483648, + 1474560: 2147487808, + 1540096: 64, + 1605632: 2147483712, + 1671168: 4096, + 1736704: 2147487744, + 1802240: 2151678016, + 1867776: 4160, + 1933312: 2151682048, + 1998848: 4194304, + 2064384: 4198464 + }, { + "0": 128, + 4096: 17039360, + 8192: 262144, + 12288: 536870912, + 16384: 537133184, + 20480: 16777344, + 24576: 553648256, + 28672: 262272, + 32768: 16777216, + 36864: 537133056, + 40960: 536871040, + 45056: 553910400, + 49152: 553910272, + 53248: 0, + 57344: 17039488, + 61440: 553648128, + 2048: 17039488, + 6144: 553648256, + 10240: 128, + 14336: 17039360, + 18432: 262144, + 22528: 537133184, + 26624: 553910272, + 30720: 536870912, + 34816: 537133056, + 38912: 0, + 43008: 553910400, + 47104: 16777344, + 51200: 536871040, + 55296: 553648128, + 59392: 16777216, + 63488: 262272, + 65536: 262144, + 69632: 128, + 73728: 536870912, + 77824: 553648256, + 81920: 16777344, + 86016: 553910272, + 90112: 537133184, + 94208: 16777216, + 98304: 553910400, + 102400: 553648128, + 106496: 17039360, + 110592: 537133056, + 114688: 262272, + 118784: 536871040, + 122880: 0, + 126976: 17039488, + 67584: 553648256, + 71680: 16777216, + 75776: 17039360, + 79872: 537133184, + 83968: 536870912, + 88064: 17039488, + 92160: 128, + 96256: 553910272, + 100352: 262272, + 104448: 553910400, + 108544: 0, + 112640: 553648128, + 116736: 16777344, + 120832: 262144, + 124928: 537133056, + 129024: 536871040 + }, { + "0": 268435464, + 256: 8192, + 512: 270532608, + 768: 270540808, + 1024: 268443648, + 1280: 2097152, + 1536: 2097160, + 1792: 268435456, + 2048: 0, + 2304: 268443656, + 2560: 2105344, + 2816: 8, + 3072: 270532616, + 3328: 2105352, + 3584: 8200, + 3840: 270540800, + 128: 270532608, + 384: 270540808, + 640: 8, + 896: 2097152, + 1152: 2105352, + 1408: 268435464, + 1664: 268443648, + 1920: 8200, + 2176: 2097160, + 2432: 8192, + 2688: 268443656, + 2944: 270532616, + 3200: 0, + 3456: 270540800, + 3712: 2105344, + 3968: 268435456, + 4096: 268443648, + 4352: 270532616, + 4608: 270540808, + 4864: 8200, + 5120: 2097152, + 5376: 268435456, + 5632: 268435464, + 5888: 2105344, + 6144: 2105352, + 6400: 0, + 6656: 8, + 6912: 270532608, + 7168: 8192, + 7424: 268443656, + 7680: 270540800, + 7936: 2097160, + 4224: 8, + 4480: 2105344, + 4736: 2097152, + 4992: 268435464, + 5248: 268443648, + 5504: 8200, + 5760: 270540808, + 6016: 270532608, + 6272: 270540800, + 6528: 270532616, + 6784: 8192, + 7040: 2105352, + 7296: 2097160, + 7552: 0, + 7808: 268435456, + 8064: 268443656 + }, { + "0": 1048576, + 16: 33555457, + 32: 1024, + 48: 1049601, + 64: 34604033, + 80: 0, + 96: 1, + 112: 34603009, + 128: 33555456, + 144: 1048577, + 160: 33554433, + 176: 34604032, + 192: 34603008, + 208: 1025, + 224: 1049600, + 240: 33554432, + 8: 34603009, + 24: 0, + 40: 33555457, + 56: 34604032, + 72: 1048576, + 88: 33554433, + 104: 33554432, + 120: 1025, + 136: 1049601, + 152: 33555456, + 168: 34603008, + 184: 1048577, + 200: 1024, + 216: 34604033, + 232: 1, + 248: 1049600, + 256: 33554432, + 272: 1048576, + 288: 33555457, + 304: 34603009, + 320: 1048577, + 336: 33555456, + 352: 34604032, + 368: 1049601, + 384: 1025, + 400: 34604033, + 416: 1049600, + 432: 1, + 448: 0, + 464: 34603008, + 480: 33554433, + 496: 1024, + 264: 1049600, + 280: 33555457, + 296: 34603009, + 312: 1, + 328: 33554432, + 344: 1048576, + 360: 1025, + 376: 34604032, + 392: 33554433, + 408: 34603008, + 424: 0, + 440: 34604033, + 456: 1049601, + 472: 1024, + 488: 33555456, + 504: 1048577 + }, { + "0": 134219808, + 1: 131072, + 2: 134217728, + 3: 32, + 4: 131104, + 5: 134350880, + 6: 134350848, + 7: 2048, + 8: 134348800, + 9: 134219776, + 10: 133120, + 11: 134348832, + 12: 2080, + 13: 0, + 14: 134217760, + 15: 133152, + 2147483648: 2048, + 2147483649: 134350880, + 2147483650: 134219808, + 2147483651: 134217728, + 2147483652: 134348800, + 2147483653: 133120, + 2147483654: 133152, + 2147483655: 32, + 2147483656: 134217760, + 2147483657: 2080, + 2147483658: 131104, + 2147483659: 134350848, + 2147483660: 0, + 2147483661: 134348832, + 2147483662: 134219776, + 2147483663: 131072, + 16: 133152, + 17: 134350848, + 18: 32, + 19: 2048, + 20: 134219776, + 21: 134217760, + 22: 134348832, + 23: 131072, + 24: 0, + 25: 131104, + 26: 134348800, + 27: 134219808, + 28: 134350880, + 29: 133120, + 30: 2080, + 31: 134217728, + 2147483664: 131072, + 2147483665: 2048, + 2147483666: 134348832, + 2147483667: 133152, + 2147483668: 32, + 2147483669: 134348800, + 2147483670: 134217728, + 2147483671: 134219808, + 2147483672: 134350880, + 2147483673: 134217760, + 2147483674: 134219776, + 2147483675: 0, + 2147483676: 133120, + 2147483677: 2080, + 2147483678: 131104, + 2147483679: 134350848 + }] + , j = [4160749569, 528482304, 33030144, 2064384, 129024, 8064, 504, 2147483679] + , a = t.DES = f.extend({ + _doReset: function() { + for (var n = this._key.words, q = [], u = 0; 56 > u; u++) { + var s = c[u] - 1; + q[u] = n[s >>> 5] >>> 31 - s % 32 & 1 + } + n = this._subKeys = []; + for (s = 0; 16 > s; s++) { + for (var r = n[s] = [], p = m[s], u = 0; 24 > u; u++) { + r[u / 6 | 0] |= q[(k[u] - 1 + p) % 28] << 31 - u % 6, + r[4 + (u / 6 | 0)] |= q[28 + (k[u + 24] - 1 + p) % 28] << 31 - u % 6 + } + r[0] = r[0] << 1 | r[0] >>> 31; + for (u = 1; 7 > u; u++) { + r[u] >>>= 4 * (u - 1) + 3 + } + r[7] = r[7] << 5 | r[7] >>> 27 + } + q = this._invSubKeys = []; + for (u = 0; 16 > u; u++) { + q[u] = n[15 - u] + } + }, + encryptBlock: function(d, l) { + this._doCryptBlock(d, l, this._subKeys) + }, + decryptBlock: function(d, l) { + this._doCryptBlock(d, l, this._invSubKeys) + }, + _doCryptBlock: function(w, z, y) { + this._lBlock = w[z]; + this._rBlock = w[z + 1]; + o.call(this, 4, 252645135); + o.call(this, 16, 65535); + g.call(this, 2, 858993459); + g.call(this, 8, 16711935); + o.call(this, 1, 1431655765); + for (var x = 0; 16 > x; x++) { + for (var v = y[x], u = this._lBlock, s = this._rBlock, l = 0, b = 0; 8 > b; b++) { + l |= i[b][((s ^ v[b]) & j[b]) >>> 0] + } + this._lBlock = s; + this._rBlock = u ^ l + } + y = this._lBlock; + this._lBlock = this._rBlock; + this._rBlock = y; + o.call(this, 1, 1431655765); + g.call(this, 8, 16711935); + g.call(this, 2, 858993459); + o.call(this, 16, 65535); + o.call(this, 4, 252645135); + w[z] = this._lBlock; + w[z + 1] = this._rBlock + }, + keySize: 2, + ivSize: 2, + blockSize: 2 + }); + h.DES = f._createHelper(a); + t = t.TripleDES = f.extend({ + _doReset: function() { + var d = this._key.words; + this._des1 = a.createEncryptor(e.create(d.slice(0, 2))); + this._des2 = a.createEncryptor(e.create(d.slice(2, 4))); + this._des3 = a.createEncryptor(e.create(d.slice(4, 6))) + }, + encryptBlock: function(d, l) { + this._des1.encryptBlock(d, l); + this._des2.decryptBlock(d, l); + this._des3.encryptBlock(d, l) + }, + decryptBlock: function(d, l) { + this._des3.decryptBlock(d, l); + this._des2.encryptBlock(d, l); + this._des1.decryptBlock(d, l) + }, + keySize: 6, + ivSize: 2, + blockSize: 2 + }); + h.TripleDES = f._createHelper(t) +} +)(); + +function myformatDate(value, format) { + if (!value) { + return ""; + } + /** @type {!Object} */ + var date = value; + if (typeof value === "string") { + if (value.indexOf("/Date(") > -1) { + /** @type {!Date} */ + date = new Date(parseInt(value.replace("/Date(", "").replace(")/", ""), 10)); + } else { + /** @type {!Date} */ + date = new Date(Date.parse(value.replace(/-/g, "/").replace("T", " ").split(".")[0])); + } + } else { + if (typeof value === "number") { + /** @type {!Date} */ + date = new Date(value); + } + } + var o = { + "M+" : date.getMonth() + 1, + "d+" : date.getDate(), + "h+" : date.getHours(), + "m+" : date.getMinutes(), + "s+" : date.getSeconds(), + "q+" : Math.floor((date.getMonth() + 3) / 3), + "S" : date.getMilliseconds() + }; + format = format || "yyyy-MM-dd"; + if (/(y+)/.test(format)) { + format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)); + } + var str; + for (str in o) { + if ((new RegExp("(" + str + ")")).test(format)) { + format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[str] : ("00" + o[str]).substr(("" + o[str]).length)); + } + } + return format; +}; + +var DES3 = { + iv : function() { + return myformatDate(new Date, "yyyyMMdd"); + }, + encrypt : function(message, data, text) { + if (data) { + return CryptoJS.TripleDES.encrypt(message, CryptoJS.enc.Utf8.parse(data), { + iv : CryptoJS.enc.Utf8.parse(text || DES3.iv()), + mode : CryptoJS.mode.CBC, + padding : CryptoJS.pad.Pkcs7 + }).toString(); + } + return ""; + }, + decrypt : function(key, ciphertext, options) { + if (ciphertext) { + return CryptoJS.enc.Utf8.stringify(CryptoJS.TripleDES.decrypt(key, CryptoJS.enc.Utf8.parse(ciphertext), { + iv : CryptoJS.enc.Utf8.parse(options || DES3.iv()), + mode : CryptoJS.mode.CBC, + padding : CryptoJS.pad.Pkcs7 + })).toString(); + } + return ""; + } +}; +``` +- 使用 js 调试工具,对 response 数据进行解密,得到下面的结果,破解成功: + + ![]({{site.baseurl}}/img-post/文书网-5.png) + +## 2. 破解请求参数 + +#### 2.1. 分析参数生成逻辑 +- 分析请的参数,发现 Form Data 里的参数 + + ![]({{site.baseurl}}/img-post/文书网-6.png) + +#### 2.2. pageId + +```pageId: 953feda22cf2bf18b9548aaa05152120``` +- 通过分析回溯,找到 pageId 的生成函数为 ```uuid()```; + ![]({{site.baseurl}}/img-post/文书网-7.png) + +- 继续查找得到 uuid 函数的生成逻辑如下: + ![]({{site.baseurl}}/img-post/文书网-8.png) + +#### 2.3. s21 + +- 此参数为输入的查找对象; + +#### 2.4. sortFields + +- 不做处理; + +#### 2.5. ciphertext + +>110100 1100111 1000110 1100001 110111 1010001 1111001 110001 1111000 1000100 1010010 1111001 1010001 1000010 1100100 110101 1010101 110100 1010101 1010101 1110011 1111000 1100011 1000011 110010 110000 110010 110001 110000 111000 110000 110011 110100 1110000 1001110 1110101 1010011 1010000 1101101 1111010 101011 1011000 1110010 1001001 110001 110111 1000110 1000101 1001101 110111 1100001 1110011 1000011 1110111 111101 111101 +- 通过分析我们发现这里的数值都是二进制的,查找关键字 ```strTobinary``` 或者查找 ```ciphertext``` 然后断点调试得到参数生成逻辑,如下图: + ![]({{site.baseurl}}/img-post/文书网-9.png) + +#### 2.6. pageNum + +- 页码数; + +#### 2.7. queryCondition + +- 分析 [{"key":"s21","value":"吴*凡"}] + ```key``` 的值为 ```s21``` 表示案件类型为 ```全文```, + 如下图: + ![]({{site.baseurl}}/img-post/文书网-10.png) + ```value``` 表示查找对象 + +#### 2.8. cfg + +- 值为 ```com.lawyee.judge.dc.parse.dto.SearchDataDsoDTO@queryDoc```,不做处理; + +#### 2.9. __RequestVerificationToken + +- 通过分析,得到生成逻辑如下: + ![]({{site.baseurl}}/img-post/文书网-11.png) + + ![]({{site.baseurl}}/img-post/文书网-12.png) + +#### 2.10.参数 js 代码 + +- 略。 \ No newline at end of file diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\346\260\221\345\256\277\345\255\227\344\275\223\345\217\215\347\210\254\347\255\226\347\225\245.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\346\260\221\345\256\277\345\255\227\344\275\223\345\217\215\347\210\254\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..b834407db01 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\346\260\221\345\256\277\345\255\227\344\275\223\345\217\215\347\210\254\347\255\226\347\225\245.md" @@ -0,0 +1,40 @@ +--- +layout: post +title: JS逆向:破解某民宿字体反爬策略 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +## 1. 字体反*爬原理 + +- 字体反爬原理,参见:https://www.jianshu.com/writer#/notebooks/46497835/notes/89368581,此处不再赘述: + +## 2. 某民宿字体反爬策略 + +- 检查元素查看价格、评论数,发现数值被加**密过了,如下图: + + ![]({{site.baseurl}}/img-post/民宿-1.png) + +- 这里我们发现 **clsss** 值为 **abfzdp8**,通过分析我们发现,这种情况下我们后面没有办法进行映射; +- 此时我们准换思路,在 **Network** 中分析 **response**,查看 **response** 我们可以发现,price 值是 ** ; ; ;**,这就符合我们一般的映射表习惯了,同时我们还注意到在 **response** 中有 .woff 文件的地址,据此我们可以推断, woff 文件是动态变化的; + + ![]({{site.baseurl}}/img-post/民宿-2.png) + +- 我们下载 .woff 文件,拖入 **https://font.qqe2.com/**,我们发现数值和 unicode 编码对应上了,得到下图: + + ![]({{site.baseurl}}/img-post/民宿-3.png) + +- 至此,破**解完毕。 + +## 3. 代码实现 + +- 发送请求,获取 woff 文件、prices 数据; +- 下载 woff 文件,TTFont 读取并转换成 xml 文件; +- 建立映射表,并替换得出结果; +- 具体代码,此处省略。 \ No newline at end of file diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\347\202\271\350\257\204\347\275\221\347\253\231\345\255\227\344\275\223\345\217\215\347\210\254\347\255\226\347\225\245.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\347\202\271\350\257\204\347\275\221\347\253\231\345\255\227\344\275\223\345\217\215\347\210\254\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..51678a366ed --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\347\202\271\350\257\204\347\275\221\347\253\231\345\255\227\344\275\223\345\217\215\347\210\254\347\255\226\347\225\245.md" @@ -0,0 +1,69 @@ +--- +layout: post +title: JS逆向:破解某点评网站字体反爬策略 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +## 1、字体反爬原理 + +- 在CSS3之前,Web开发者必须使用用户计算机上已有的字体。但是在CSS3时代,开发者可以使用@font-face为网页指定字体,开发者可以将心仪的字体文件放在Web服务器上,并在Css样式中使用它。用户使用浏览器访问Web应用时,对应的字体会被浏览器下载到用户的计算上。 +- CSS的作用是修饰HTML,所以在页面渲染的时候不会改变HTML文档内容。由于字体的加载和映射工作是由CSS完成的,所以即使我们借助Splash、Selenium和Pypeeteer工具也无法获得对应的文字内容。字体反爬正式利用了这个特点,将自定义字体应用到网页中重要的数据上,使得爬虫程序无法获得正确的数据。 + +## 2、 某点评网站字体反爬介绍 + +- 打开网站某店铺页面,邮件检查店铺评论数、地址,发现想要查找的数据变成了下面这个样子: + + ![]({{site.baseurl}}/img-post/字体-1.png) + +- 像这种情况就是字体反爬,对于这类点评网站来说,**评分、评论数、金额、店换号码、店铺地址、特色菜品、用户评分、用户评论、用户消费金额** 等等这些都属于核心资产,都在加密的范围内; +- 我们查看右侧的 style,发现 **地址** 对应 style 中 **.adress** 下 **font-family** 值为 **PingFangSC-Regular-address**,而 **电话号码** 对应的 style 中 **.num** 下 **font-family** 值为 **PingFangSC-Regular-num**,由此我们也可以推断,不同的网页内容,加载的字体文件是动态变化的; + + ![]({{site.baseurl}}/img-post/字体-2.png) + ![]({{site.baseurl}}/img-post/字体-3.png) + +- 那具体什么内容,对应什么字体文件呢?我们打开 **全局 search**,搜索 **@font-face**,找到下面的 .css 文件,打开以后通过分析发现, **address、dishname、hours、review、num、shopdesc**,分别对应了不同的 **.woff** 文件,这里的 .woff 文件就是字体存放的地方; + + ![]({{site.baseurl}}/img-post/字体-4.png) + +- 除了通过全局搜索的方式获取字体文件的 url 地址,也可以鼠标放在 .css 文件名上面,就能看到 .css 文件的 url; + + ![]({{site.baseurl}}/img-post/字体-5.png) + +- 在实际爬虫开发的时候一般访问网页后,在网页源代码中获取 .css 文件的 url,具体可以通过正则表达式的方式匹配提取; + + ![]({{site.baseurl}}/img-post/字体-6.png) + +## 3、反反爬策略 +- 我们这里以 review 的 woff 文件为例子,访问并下载该 woff 文件,之后将下载的文件拖入 https://font.qqe2.com/ ,得到下图: + + ![]({{site.baseurl}}/img-post/字体-7.png) + +- 此时我们发现,每个字符都对应了一个编码,这里的编码代表什么意思呢?我们打开调试面板,在 Network 中查找 **review** 相关的 **response**,通过分析我们发现,返货的数据中 ** ;** 对应实际的汉字 **味**,查找字体文件我们发现,**e255** 和字体编码 **unie255** 相对应; + + ![]({{site.baseurl}}/img-post/字体-8.png) + +- 至此,我们就已经完成了字体反爬的破解工作。 + +## 4、代码实现 + +- 核心步骤包括: + + - 访问网页,获取页面源代码; + - 正则匹配查找 .css 文件的 url; + - 查找 .woff 文件并下载保存; + - TTFont 模块读取 woff 文件,转换成 xml 保存; + - 读取 xml 文件创建映射表; + - 发起请求获取 review 等数据; + - 通过映射表,替换被加密的数据; + +- 代码: + -- 略:参考:https://github.com/downdawn/dzdp/tree/595fbcab21f7e342c51c521c102e8e08ecb6d64f。 + + diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\347\253\231 password \345\217\202\346\225\260\345\212\240\345\257\206\351\200\273\350\276\221.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\347\253\231 password \345\217\202\346\225\260\345\212\240\345\257\206\351\200\273\350\276\221.md" new file mode 100644 index 00000000000..015dda5a896 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\347\253\231 password \345\217\202\346\225\260\345\212\240\345\257\206\351\200\273\350\276\221.md" @@ -0,0 +1,78 @@ +--- +layout: post +title: JS逆向:破解某站 password 参数加密逻辑 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + +#### 目标站点:`aHR0cDovL2VpcC5jaGFuZmluZS5jb20vbG9naW4uanNw` + +>说明: 该站点较简单,可以作为小白练手使用,方便理解 js 逆向常规步骤。 + +#1. 定位代码 +- 打开调试面板,输入手机号、密码,点击登录,检查 Network,分析后发现箭头指向的请求,分析发现参数 `j_password` 已经被加密; + +![]({{site.baseurl}}/img-post/password-1.png) +- 全局搜索 `j_password`,下断点,重新输入账号密码,网页在下图位置被断住,分析发现 `desEncrypt` 函数返回的结果就是被解密后的数据; + +![]({{site.baseurl}}/img-post/password-2.png) +# 2. 分析调试 +- 回溯,进入 `function desEncrypt(value, xForm, type)` 中,分析发现 `_0(xForm)` 结果为 `false`,在 `else` 代码 `keyObj = SECURITYKEY.get();` 处下断,如下图; + +![]({{site.baseurl}}/img-post/password-3.png) +- 回溯分析 ` SECURITYKEY.get()` 函数,在代码 `str = SECURITYKEY._2();` 处下断,回溯分析 `SECURITYKEY._2()` 函数,发现该函数是通过 ajax 发送请求得到返回结果的,请求的 url 中包含 `js/session.jsp?_=`,并且还跟了一个时间戳后缀,通过搜索过滤请求,找到了目标 url: `http://eip.chanfine.com/resource/js/session.jsp?_=1637761979345&s_ajax=true`,该 url 尾部也跟了一个时间戳; + +![]({{site.baseurl}}/img-post/password-4.png) +- 访问上面拿到的 url,得到如下返回值,正是我们想要的。 +``` +function getSessionId(){ + return "3B9B93AF595C0F909356C09468D9F041"; +} +``` +- 回退,继续单步调试,发现 `encodeType == null || encodeType == 'aes'` 结果为 `true`,此处 else 条件分支可以忽略。单步调试后发现,前述的返回值被分割成多个部分并赋值给了一个对象 key,最后返回了一个对象; + +![]({{site.baseurl}}/img-post/password-5.png) + +- 再次回退,分析 `CryptoJS.AES.encrypt(value, CryptoJS.enc.Utf8.parse(keyObj.key)`, + +**分析发现**: +-- `keyObj.key` 就是前面得到的 `key` 对象的 `key` 属性值, +-- `CryptoJS.enc.Utf8.parse()` 函数对应的函数在122行: +``` +parse: function(a) { + return b.parse(unescape(encodeURIComponent(a))) + } +``` +-- 分析上面的代码,`encodeURIComponent` 是内生的自带函数,`unescape()` 也是内生的自带函数,而且两个函数都为对传入的参数 `a` 做任何修改。需要继续回溯分析的 `b.parse()` 函数,对应函数代码为: +``` +parse: function(a) { + for (var c = a.length, e = [], j = 0; j < c; j++) + e[j >>> 2] |= (a.charCodeAt(j) & 255) << 24 - 8 * (j % 4); + return new r.init(e,c) + } +``` +-- 分析上面的代码,发现其执行过程包括一个 for 循环,单步调试发现并不需要进一步回溯,需要回溯的是return 的 `new r.init(e,c)`,继续回溯得到如下代码: +``` +init: function(a, c) { + a = this.words = a || []; + this.sigBytes = c != p ? c : 4 * a.length + }, +``` +- 到这一步,其实已经没有必要继续在扣代码了,结合前面的上下文,可以知道整个js代码其实可以全部扣下来,直接进行调用的,因为 `value = CryptoJS.AES.encrypt(value, CryptoJS.enc.Utf8.parse(keyObj.key), {iv:CryptoJS.enc.Utf8.parse(keyObj.iv)}).toString()` 这段加密代码,全部是对 `CryptoJS` 的调用。 + +![]({{site.baseurl}}/img-post/password-5.png) +- 打开 console,输出前面分析的结果; +![]({{site.baseurl}}/img-post/password-5.png) + +- 如上图,得到加密后的内容,至此调试分析结束。 +# 3. js代码实现 +- 通过上面的分析,知道核心加密步骤分为两步: +>第一步,是通过 `SECURITYKEY.get()` 生成一个对象,在此过程中还需要 ajax 发送一个带时间戳的请求; +第二步,是通过 `CryptoJS` 中的 `AES` 和 `enc` 进行加密处理; +- js 代码: + 略。 \ No newline at end of file diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\347\253\231 sign \345\217\202\346\225\260\345\212\240\345\257\206\351\200\273\350\276\221.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\347\253\231 sign \345\217\202\346\225\260\345\212\240\345\257\206\351\200\273\350\276\221.md" new file mode 100644 index 00000000000..58d7f5fbee9 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\347\253\231 sign \345\217\202\346\225\260\345\212\240\345\257\206\351\200\273\350\276\221.md" @@ -0,0 +1,247 @@ +--- +layout: post +title: JS逆向:破解某站 sign 参数加密逻辑 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +#### 目标站点:`aHR0cHM6Ly9tLndjYmNoaW5hLmNvbS9pbnZpdGUvaW52aXRlLmh0bWw=` + +>说明: 该站点较简单,可以作为小白练手使用,方便理解 js 逆向常规步骤。 + +# 1. 定位代码 +- 打开调试面板,输入手机号、密码,检查 Network,分析后发现箭头指向的请求; + + ![]({{site.baseurl}}/img-post/js-sign-1.png) + + +- 分析参数,发现 sign 参数被加密了; + + ![]({{site.baseurl}}/img-post/js-sign-2.png) + +- 打开全局搜索,搜索 `sign:`,找到如下js代码文件 + + ![]({{site.baseurl}}/img-post/js-sign-3.png) + +# 2. 分析调试 + +- 分析代码,加密函数关键步骤为 `N = h.hex_md5((v || "") + (g || "") + c).toUpperCase()`; +> **此处分析**: +未知内容:`h.hex_md5() `方法、以及参数 `(v || "") + (g || "") + c` 生成逻辑; +- 通过分析我们发现 `(v || "") + (g || "") + c` 等同于 `c+""`,而 `c` 其实就是一个时间戳 `$.now()`; + + ![]({{site.baseurl}}/img-post/js-sign-4.png) + +- 到这里,依次得到图中所示顺序疑问; + + ![]({{site.baseurl}}/img-post/js-sign-5.png) + +- 下断点; + + ![]({{site.baseurl}}/img-post/js-sign-6.png) + +- F5 强刷网页,发现断点已被断掉,位置如下图; + + ![]({{site.baseurl}}/img-post/js-sign-7.png) + +- 分析发现 `require('md5')` 得到的结果是一个 `Object` 对象,该对象中包括了 `hex_md5` 方法,推测这个 `hex_md5` 方法就是生成 `N` 用到的方法; + + ![]({{site.baseurl}}/img-post/js-sign-8.png) + +- 点击上图中 `[[FunctionLocation]]` 指向的 `md5.js?v=68:1`,得到下面的代码,在代码处下断; + + ![]({{site.baseurl}}/img-post/js-sign-9.png) + +- 输入账号、密码,过断点调试,发现 js 在断点处被断下; +>**此处分析**: +未知内容:`D`方法、`c`方法、`b`方法、`s` 变量; +已知内容:`.length` 方法、`U` 变量(`=8`)、`toUpperCase` 方法; + + ![]({{site.baseurl}}/img-post/js-sign-10.png) + +- 通过分析,发现 未知内容均位于 `md5.js?v=68:` 这个 js 文件中; + + ![]({{site.baseurl}}/img-post/js-sign-11.png) + +- 改写代码; +``` +var _j = {}; +(function(require, exports, module) { //此处为了方便调试,直接将代码改成自执行函数 + function c(x, c) { + x[c >> 5] |= 128 << c % 32, + x[(c + 64 >>> 9 << 4) + 14] = c; + for (var a = 1732584193, _ = -271733879, y = -1732584194, d = 271733878, i = 0; i < x.length; i += 16) { + var b = a + , B = _ + , D = y + , E = d; + a = h(a, _, y, d, x[i + 0], 7, -680876936), + d = h(d, a, _, y, x[i + 1], 12, -389564586), + y = h(y, d, a, _, x[i + 2], 17, 606105819), + _ = h(_, y, d, a, x[i + 3], 22, -1044525330), + a = h(a, _, y, d, x[i + 4], 7, -176418897), + d = h(d, a, _, y, x[i + 5], 12, 1200080426), + y = h(y, d, a, _, x[i + 6], 17, -1473231341), + _ = h(_, y, d, a, x[i + 7], 22, -45705983), + a = h(a, _, y, d, x[i + 8], 7, 1770035416), + d = h(d, a, _, y, x[i + 9], 12, -1958414417), + y = h(y, d, a, _, x[i + 10], 17, -42063), + _ = h(_, y, d, a, x[i + 11], 22, -1990404162), + a = h(a, _, y, d, x[i + 12], 7, 1804603682), + d = h(d, a, _, y, x[i + 13], 12, -40341101), + y = h(y, d, a, _, x[i + 14], 17, -1502002290), + _ = h(_, y, d, a, x[i + 15], 22, 1236535329), + a = g(a, _, y, d, x[i + 1], 5, -165796510), + d = g(d, a, _, y, x[i + 6], 9, -1069501632), + y = g(y, d, a, _, x[i + 11], 14, 643717713), + _ = g(_, y, d, a, x[i + 0], 20, -373897302), + a = g(a, _, y, d, x[i + 5], 5, -701558691), + d = g(d, a, _, y, x[i + 10], 9, 38016083), + y = g(y, d, a, _, x[i + 15], 14, -660478335), + _ = g(_, y, d, a, x[i + 4], 20, -405537848), + a = g(a, _, y, d, x[i + 9], 5, 568446438), + d = g(d, a, _, y, x[i + 14], 9, -1019803690), + y = g(y, d, a, _, x[i + 3], 14, -187363961), + _ = g(_, y, d, a, x[i + 8], 20, 1163531501), + a = g(a, _, y, d, x[i + 13], 5, -1444681467), + d = g(d, a, _, y, x[i + 2], 9, -51403784), + y = g(y, d, a, _, x[i + 7], 14, 1735328473), + _ = g(_, y, d, a, x[i + 12], 20, -1926607734), + a = v(a, _, y, d, x[i + 5], 4, -378558), + d = v(d, a, _, y, x[i + 8], 11, -2022574463), + y = v(y, d, a, _, x[i + 11], 16, 1839030562), + _ = v(_, y, d, a, x[i + 14], 23, -35309556), + a = v(a, _, y, d, x[i + 1], 4, -1530992060), + d = v(d, a, _, y, x[i + 4], 11, 1272893353), + y = v(y, d, a, _, x[i + 7], 16, -155497632), + _ = v(_, y, d, a, x[i + 10], 23, -1094730640), + a = v(a, _, y, d, x[i + 13], 4, 681279174), + d = v(d, a, _, y, x[i + 0], 11, -358537222), + y = v(y, d, a, _, x[i + 3], 16, -722521979), + _ = v(_, y, d, a, x[i + 6], 23, 76029189), + a = v(a, _, y, d, x[i + 9], 4, -640364487), + d = v(d, a, _, y, x[i + 12], 11, -421815835), + y = v(y, d, a, _, x[i + 15], 16, 530742520), + _ = v(_, y, d, a, x[i + 2], 23, -995338651), + a = A(a, _, y, d, x[i + 0], 6, -198630844), + d = A(d, a, _, y, x[i + 7], 10, 1126891415), + y = A(y, d, a, _, x[i + 14], 15, -1416354905), + _ = A(_, y, d, a, x[i + 5], 21, -57434055), + a = A(a, _, y, d, x[i + 12], 6, 1700485571), + d = A(d, a, _, y, x[i + 3], 10, -1894986606), + y = A(y, d, a, _, x[i + 10], 15, -1051523), + _ = A(_, y, d, a, x[i + 1], 21, -2054922799), + a = A(a, _, y, d, x[i + 8], 6, 1873313359), + d = A(d, a, _, y, x[i + 15], 10, -30611744), + y = A(y, d, a, _, x[i + 6], 15, -1560198380), + _ = A(_, y, d, a, x[i + 13], 21, 1309151649), + a = A(a, _, y, d, x[i + 4], 6, -145523070), + d = A(d, a, _, y, x[i + 11], 10, -1120210379), + y = A(y, d, a, _, x[i + 2], 15, 718787259), + _ = A(_, y, d, a, x[i + 9], 21, -343485551), + a = C(a, b), + _ = C(_, B), + y = C(y, D), + d = C(d, E) + } + return Array(a, _, y, d) + } + function a(q, c, a, x, s, t) { + return C(y(C(C(c, q), C(x, t)), s), a) + } + function h(c, h, g, d, x, s, t) { + return a(h & g | ~h & d, c, h, x, s, t) + } + function g(c, h, g, d, x, s, t) { + return a(h & d | g & ~d, c, h, x, s, t) + } + function v(c, h, g, d, x, s, t) { + return a(h ^ g ^ d, c, h, x, s, t) + } + function A(c, h, g, d, x, s, t) { + return a(g ^ (h | ~d), c, h, x, s, t) + } + function _(a, h) { + var g = b(a); + g.length > 16 && (g = c(g, a.length * U)); + for (var v = Array(16), A = Array(16), i = 0; 16 > i; i++) + v[i] = 909522486 ^ g[i], + A[i] = 1549556828 ^ g[i]; + var _ = c(v.concat(b(h)), 512 + h.length * U); + return c(A.concat(_), 640) + } + function C(x, c) { + var a = (65535 & x) + (65535 & c) + , h = (x >> 16) + (c >> 16) + (a >> 16); + return h << 16 | 65535 & a + } + function y(c, a) { + return c << a | c >>> 32 - a + } + function b(c) { + for (var a = Array(), h = (1 << U) - 1, i = 0; i < c.length * U; i += U) + a[i >> 5] |= (c.charCodeAt(i / U) & h) << i % 32; + return a + } + function B(c) { + for (var a = "", h = (1 << U) - 1, i = 0; i < 32 * c.length; i += U) + a += String.fromCharCode(c[i >> 5] >>> i % 32 & h); + return a + } + function D(c) { + for (var a = F ? "0123456789ABCDEF" : "0123456789abcdef", h = "", i = 0; i < 4 * c.length; i++) + h += a.charAt(c[i >> 2] >> i % 4 * 8 + 4 & 15) + a.charAt(c[i >> 2] >> i % 4 * 8 & 15); + return h + } + function E(c) { + for (var a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", h = "", i = 0; i < 4 * c.length; i += 3) + for (var g = (c[i >> 2] >> 8 * (i % 4) & 255) << 16 | (c[i + 1 >> 2] >> 8 * ((i + 1) % 4) & 255) << 8 | c[i + 2 >> 2] >> 8 * ((i + 2) % 4) & 255, v = 0; 4 > v; v++) + h += 8 * i + 6 * v > 32 * c.length ? S : a.charAt(g >> 6 * (3 - v) & 63); + return h + } + var F = 0 + , S = "" + , U = 8 + , j = { + hex_md5: function(s) { + return D(c(b(s), s.length * U)).toUpperCase() + }, + b64_md5: function(s) { + return E(c(b(s), s.length * U)) + }, + str_md5: function(s) { + return B(c(b(s), s.length * U)) + }, + hex_hmac_md5: function(c, a) { + return D(_(c, a)) + }, + b64_hmac_md5: function(c, a) { + return E(_(c, a)) + }, + str_hmac_md5: function(c, a) { + return B(_(c, a)) + } + }; + //module.exports = j //注意:此处不能直接用 module.exports,否则会报错 Uncaught TypeError: Cannot set properties of undefined (setting 'exports') + _j = j +})(); +``` +> **注意**: +>- 此处为了方便调试,直接将代码改成自执行函数; +>- 不能直接用 `module.exports`,否则会报错 `Uncaught TypeError: Cannot set properties of undefined (setting 'exports')` +- 测试代码,发现加密函数是我们想要的。 + + ![]({{site.baseurl}}/img-post/js-sign-12.png) + +# 3. js 代码 + +- 通过上面的分析,得到 `(v || "") + (g || "") + c` 是字符串格式的时间戳,同时扣取并改写了 `h.hex_md5()` 函数,接下来就可以进行 js 代码实现了。 +- 具体代码: + 略 + diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\350\247\206\351\242\221\347\275\221\347\253\231\347\231\273\345\275\225\345\217\202\346\225\260\347\224\237\344\272\247\346\226\271\346\263\225.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\350\247\206\351\242\221\347\275\221\347\253\231\347\231\273\345\275\225\345\217\202\346\225\260\347\224\237\344\272\247\346\226\271\346\263\225.md" new file mode 100644 index 00000000000..758cd9070b9 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\237\220\350\247\206\351\242\221\347\275\221\347\253\231\347\231\273\345\275\225\345\217\202\346\225\260\347\224\237\344\272\247\346\226\271\346\263\225.md" @@ -0,0 +1,81 @@ +--- +layout: post +title: JS逆向:破解某视频网站登录参数生产方法 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +# 1. 前言 +- 本文的主要内容,是分析得到某视频网站账号登录时、密码参数的 JS 加密方法。 + +# 2. 抓包 +- 打开网页,右键调出调试窗口的 Network 工具; +- 点击右上角登录按钮,弹出登录 frame 之后,切换成账号密码登录; +- 随便输入账号(这里输入12345678@qq.com)、密码(这里输入 **111111**),点击登录按钮,查看网络抓包情况; + - 注意:这里我们找到登录窗口的 url,转到此窗口可以直接访问登录窗口; + ![]({{site.baseurl}}/img-post/视频-1.png) + +- 打开登录页面后,调出浏览器抓包工具,随便输入账号(这里输入12345678@qq.com)、密码(这里输入 **111111**),点击登录按钮,再次抓包; + -- 注意:点击登录后弹出验证码窗口,本文不涉及验证码内容,此处忽略不用管; +# 3. 分析 +- 通过分析抓包数据,发现 login 的 POST 请求中的 **passwd** 参数值不固定,生成方式未知; + - 另外请注意:通过多次抓包,发现 **ptid**、**dfp** 参数值是固定的; + ![]({{site.baseurl}}/img-post/视频-2.png) + +- 打开全局搜索,查找生成 **passwd** 参数的 JS 代码,并在代码位置下断点; + + ![]({{site.baseurl}}/img-post/视频-3.png) + +- 刷线输入页面,重新输入账号、密码,点击登录按钮,发现被断下来; +- 通过分析发现,下面这段代码是我们要查找的代码,按照下图所示,进入查看 r.rsaFun 函数代码; + ![]({{site.baseurl}}/img-post/视频-4.png) + +- 如下图红框所示, r.rsaFun 函数代码就是生成 passwd 参数的代码; + ![]({{site.baseurl}}/img-post/视频-5.png) + +- 按照前面的方法,回溯分析代码中调用的函数 **getKeyPair**、**encryptedString**,发现两个函数都位于一个 js 文件下的同一个外部函数 c = function(a, b) 中; + + ![]({{site.baseurl}}/img-post/视频-6.png) + +- 按照下图所示,首先找到我们需要的 js 代码的开始和结束位置; + - 注意:因为 **getKeyPair**、**encryptedString** 两个函数位于同一个 外部函数 c = function(a, b),我们只需要把 function(a, b) 这个外部函数的代码复制出来就可以,即图中 **{** 和 **}** 中间的代码; + ![]({{site.baseurl}}/img-post/视频-7.png) + + ![]({{site.baseurl}}/img-post/视频-8.png) + +- # 4. 调试 JS +- 打开发条调试工具,如果没有此工具可以点击链接:https://pan.baidu.com/s/13APz4SiiY2dYCSrYfSlpNw 提取码:numa,进行下载; +- 将前面复制的代码粘贴进调试工具输入框,格式化以后加载代码; +- 加载后发现抱错 **-->错误描述:'b' 未定义**,于是在最前面加上代码 **b = {};**; +- 再次加载发现报错 **-->错误描述:'a' 未定义**,于是在最前面加上代码 **a = {};**; +- 在尾部重写 rsaFun 函数,并重新命名为 **getPwd**,代码如下所示; +``` +function getPwd(e) { +var t = "ab86b6371b5318aaa1d3c9e612a9f1264f372323c8c0f19875b5fc3b3fd3afcc1e5bec527aa94bfa85bffc157e4245aebda05389a5357b75115ac94f074aefcd", +a = "10001", +n = f.getKeyPair(a, "", t), +i = f.encryptedString(n, encodeURIComponent(e)).replace(/\s/g, "-"); +return i +} +``` +- 注意: + -- 上面代码中,**f.getKeyPair** 对应原始代码的 **Q.crypto.rsa.RSAUtils.getKeyPair**; + -- 上面代码中,**f.encryptedString** 对应原始代码的 **Q.crypto.rsa.RSAUtils.encryptedString**; +- 加载代码,尝试计算 **getpwd('666666')** 的结果值,得到的结果与调试器中的额参数值做对比,发现结果是一样的; + + ![]({{site.baseurl}}/img-post/视频-9.png) + +- 破解成功。 + +# 5. 构造爬虫 +- 编辑 py 调用 js,构造请求参数; + -- 此处具体代码省略; + + + diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\267\230\345\256\235\346\265\217\350\247\210\345\231\250\346\214\207\347\272\271\350\257\206\345\210\253\351\243\216\346\216\247\347\255\226\347\225\245.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\267\230\345\256\235\346\265\217\350\247\210\345\231\250\346\214\207\347\272\271\350\257\206\345\210\253\351\243\216\346\216\247\347\255\226\347\225\245.md" new file mode 100644 index 00000000000..ed51c578c65 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\346\267\230\345\256\235\346\265\217\350\247\210\345\231\250\346\214\207\347\272\271\350\257\206\345\210\253\351\243\216\346\216\247\347\255\226\347\225\245.md" @@ -0,0 +1,97 @@ +--- +layout: post +title: JS逆向:破解淘宝浏览器指纹识别风控策略 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + +#### 指纹识别原理 +- 在 selenium 抓取数据的时候,会暴露一些预定义的 JavaScript 变量,通过这些变量可以识别到用户是否使用了 selenium 驱动; +- 比较典型的例子,是 "window.navigator.webdriver",在非selenium环境下其值为undefined,而在selenium环境下,其值为true; + +![]({{site.baseurl}}/img-post/taobao-fingerprint.png) + +- 除了 navigator,还有一些其它的标志性字符串(不同的浏览器可能会有所不同),常见的特征串如下所示: + +``` + +webdriver +__driver_evaluate +__webdriver_evaluate +__selenium_evaluate +__fxdriver_evaluate + +__webdriver_unwrapped +__selenium_unwrapped +__fxdriver_unwrapped +_Selenium_IDE_Recorder +_selenium calledSelenium +_WEBDRIVER_ELEM_CACHE +ChromeDriverw +driver-evaluate +webdriver-evaluate +selenium-evaluate +webdriverCommand +webdriver-evaluate-response +__webdriverFunc +__webdriver_script_fn +__$webdriverAsyncExecutor +__lastWatirAlert +__lastWatirConfirm +__lastWatirPrompt +$chrome_asyncScriptInfo +$cdc_asdjflasutopfhvcZLmcfl_ +``` + +#### 反指纹识别方法 + +- webdriver 配置 +``` +options = webdriver.ChromeOptions() +options.add_experimental_option("excludeSwitches", ["enable-automation"]) +browser = webdriver.Chrome(chrome_options=options) +driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { + "source": """Object.defineProperty(navigator, 'webdriver', {get: () => undefined})""", +}) +``` + +- mitmproxy 篡改参数 + +``` +# coding: utf-8 +# modify_response.py + +from mitmproxy import ctx + + +def response(flow): + """Modify response data + """ + if '/js/yoda.' in flow.request.url: + # Screening selenium detection + for webdriver_key in ['webdriver', '__driver_evaluate', '__webdriver_evaluate', '__selenium_evaluate', + '__fxdriver_evaluate', '__driver_unwrapped', '__webdriver_unwrapped', + '__selenium_unwrapped', '__fxdriver_unwrapped', '_Selenium_IDE_Recorder', '_selenium', + 'calledSelenium', '_WEBDRIVER_ELEM_CACHE', 'ChromeDriverw', 'driver-evaluate', + 'webdriver-evaluate', 'selenium-evaluate', 'webdriverCommand', + 'webdriver-evaluate-response', '__webdriverFunc', '__webdriver_script_fn', + '__$webdriverAsyncExecutor', '__lastWatirAlert', '__lastWatirConfirm', + '__lastWatirPrompt', '$chrome_asyncScriptInfo', '$cdc_asdjflasutopfhvcZLmcfl_']: + ctx.log.info('Remove "{}" from {}.'.format(webdriver_key, flow.request.url)) + flow.response.text = flow.response.text.replace('"{}"'.format(webdriver_key), '"NO-SUCH-ATTR"') + print(webdriver_key) + flow.response.text = flow.response.text.replace('t.webdriver', 'false') + flow.response.text = flow.response.text.replace('ChromeDriver', '') +``` + +``` +mitmdump.exe -p Port number -s modify_response.py +``` + +![]({{site.baseurl}}/img-post/taobao-fingerprint-1.png) + diff --git "a/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\347\214\277\344\272\272\345\255\246\346\257\224\350\265\233\347\254\25416\351\242\230.md" "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\347\214\277\344\272\272\345\255\246\346\257\224\350\265\233\347\254\25416\351\242\230.md" new file mode 100644 index 00000000000..026702b98d2 --- /dev/null +++ "b/_posts/2022-10-01-JS\351\200\206\345\220\221\357\274\232\347\240\264\350\247\243\347\214\277\344\272\272\345\255\246\346\257\224\350\265\233\347\254\25416\351\242\230.md" @@ -0,0 +1,1043 @@ +--- +layout: post +title: JS逆向:破解猿人学比赛第16题 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - JS逆向 +--- + + +## 1. 分析加密参数 +- 通过分析请求,发现需要处理的加密参数是 `m`,另一个参数 `t` 为时间戳。 + +![]({{site.baseurl}}/img-post/猿人学-1.png) + +## 2. 定位函数入口 + +此处可以使用两种方法: +- 直接全局搜索; +- XHR 断点 + 跟栈; + +#### 2.1. 直接全局搜索 + +由于目标函数是一个英文字母,所以不能直接查找单字母 `m`,而是需要加一下辅助行的字符。 +本题中,可以尝试的搜索关键字包括: +- `m=` +- `m =` +- `.m=` +- `.m =` +- `['m']=` +- `['m'] = ` +- `["m"]=` +- `["m"] =` +- `m:` +- `.m:` + +此处,通过搜索 `.m =` 可以直接定位到函数入口。 + +如下图: + +![]({{site.baseurl}}/img-post/猿人学-2.png) + +#### 2.2. XHR 断点 + 跟栈 + +我们也可以打一个 `Any XHR or fetch` 断点,然后在翻页的时候让网页断下来。 + +![]({{site.baseurl}}/img-post/猿人学-3.png) + +之后在 `Call Stack` 中跟栈分析,通过分析发现在 `window.request` 位置 `m` 出现了。 + +如下图: + +![]({{site.baseurl}}/img-post/猿人学-4.png) + +经过分析代码,可以找到代码入口:`r.m = n[e(528)](btoa, p_s),`。 + +## 3. 扣代码 + 补环境 + +|js代码|实际意义| +| :----: | :----: | +|`n[e(528)]`|`ƒ (e,t){return e(t)}`| +|`e(496)`|`'parse'`| +|`e(517)`|`'toString'`| +|`u(208)`|`'btoa'`| + +基于前面的分析,扣出并改写得到下面的 js 代码。 + +``` +p_s = Date['parse'](new Date)['toString'](); +m = btoa(p_s); +console.log(m); +``` + +之后开始扣代码以及补环境操作。 +>**注意**:在处理 `n` 函数 `_0x4c28` 数组的时候需要注意,这个数组在初始化以后有被做混淆操作处理,如果只是扣出数组初始化代码,后面是会报错的。 + +通过扣代码和补环境操作,我们得到了可执行的 js 文件。 +在本地执行后,我们发现 js 代码成功返回了与目标参数相似的内容。 +如下图: + +![]({{site.baseurl}}/img-post/猿人学-5.png) + +接下来,使用此参数发送请求,尝试获取数据,但是却一直无法成功返回数据。 +使用 fiddler 抓包,发现此时服务器返回了 `301错误`。 +如下图: + +![]({{site.baseurl}}/img-post/猿人学-6.png) + +## 4. 投毒处理策略 +**投毒的定义**:投毒,指的是js代码中会对环境进行很多检测,如果识别为非正常浏览器,就会对某些参数进行修改,造成最终结果错误,这种过程称为 **投毒**。 +#### 4.1. 常见的投毒位置 +- `try ... cach ...` +- `if ... else ...` +- 判断表达式:`||` 或 `&&` 或 `?` +#### 4.2. 插桩定位 +在本地 js 文件可能投毒的位置,使用 `console.log` 打印出值; +在浏览器相同位置打上断点,查看运行的结果值; +进而对比二者是否相同,如果不同就是此处被投毒了。 + +基于上面的投毒和反投毒思想,我们逐个分析,发现 `n.g` 在本地执行的结果和在浏览器执行的结果不同。本地执行结果为 `undefined`,而在浏览器中则为 `window` 对象。 + +![]({{site.baseurl}}/img-post/猿人学-7.png) + +#### 4.3. 修改代码 +此处,将 `n.g` 修改为 `window`。 +重新执行本地文件,并发送请求,发现成功获取返回值。 +如下图: + +![]({{site.baseurl}}/img-post/猿人学-8.png) + + +## 5. 完整 js 代码 +``` +sdk = function (timeString) { + var window = global; + var _0x34e7 = ['split', 'ABHICESQWK', 'FKByN', 'U987654321', 'lmHcG', 'dICfr', 'Szksx', 'Bgrij', 'iwnNJ', 'jihgfdecba', 'GfTek', 'gfdecbaZXY', 'constructo', 'QIoXW', 'jLRMs', 'AqLWq', '0zyxwvutsr', 'TKgNw', 'eMnqD', 'thjIz', 'btoa', 'MNPQRSTWXY', 'oPsqh', 'niIlq', 'evetF', 'LVZVH', 'fYWEX', 'kmnprstwxy', 'aYkvo', 'tsrqpomnlk', 'HfLqY', 'aQCDK', 'lGBLj', 'test', '3210zyxwvu', 'QWK2Fi', 'return /" ', 'hsJtK', 'jdwcO', 'SlFsj', 'OWUOc', 'LCaAn', '[^ ]+)+)+[', 'FAVYf', '2Fi+987654', 'floor', 'join', 'EuwBW', 'OXYrZ', 'charCodeAt', 'SkkHG', 'iYuJr', 'GwoYF', 'kPdGe', 'cVCcp', 'INQRH', 'INVALID_CH', 'charAt', 'push', 'apply', 'lalCJ', 'kTcRS', '+ this + "', 'ykpOn', 'gLnjm', 'gmBaq', 'kukBH', 'dvEWE', 'SFKLi', '^([^ ]+( +', 'qpomnlkjih', '^ ]}', 'pHtmC', 'length']; + var l = function (e, t) { + return _0x34e7[e -= 188] + }; + var u = l; + var f = u(191) + u(204) + u(258) + u(199) + "WVUTSRQPON" + u(189) + u(232) + u(222) + u(217) + u(197) + "ZXYWVUTSRQPONABHICES" + u(223); + // console.log(u(208)); + var d = function (e) { + var t = u + , n = {}; + n[t(214)] = function (e, t) { + return e || t + } + , + n.bWcgB = function (e, t) { + return e * t + } + , + n[t(227)] = "ABCDEFGHJK" + t(209) + "Zabcdefhij" + t(215) + "z2345678"; + for (var r = n, o = "1|3|0|4|2|5"[t(188)]("|"), a = 0; ;) { + switch (o[a++]) { + case "0": + var s = l[t(261)]; + continue; + case "1": + e = r[t(214)](e, 32); + continue; + case "2": + for (i = 0; i < e; i++) + c += l[t(245)](Math[t(233)](r.bWcgB(Math.random(), s))); + continue; + case "3": + var l = r[t(227)]; + continue; + case "4": + var c = ""; + continue; + case "5": + return c + } + break + } + }; + var e, t; + _0x4c28 = ["18|38|15|2", "ucisR", "wWwRM", "LzcOo", "yWGcu", "PlAEw", "ihcci", "hBKtU", "rvloG", "xcQTI", "uhJgH", "vRqUp", "EQEzR", "abc", "QgSUn", "0|45|44|19", "WMqBp", "koePJ", "jGSEC", "IKbhW", "wEOgn", "|49|71|11|", "xgzfr", "ABCDEF", "DdHPB", "aFxRD", "sFtiw", "concat", "YhaCC", "YVBwM", "abYok", "2|28|6|36|", "NLOsy", "bRLIN", "xGAWc", "length", "zYRlD", "14|67|61|3", "bolvy", "pagBT", "mdsJQ", "4|69|41|26", "kaXPV", "IWxBE", "pviAr", "5|0|2", "lvwPz", "YcDFe", "yGmJD", "FcYqi", "AAZoR", "|46|5|3|50", "PnITs", "ABCDEFGHIJ", "charCodeAt", "KLMNOPQRST", "prrXX", "FDiNG", "split", "oBesn", "9|24|10|56", "VaXsK", "fromCharCo", "FDfcp", "rrdPR", "HHkBN", "89+/", "mfuQZ", "PbrnX", "FcXlo", "rNapo", "fEXNi", "qtIDJ", "60|53|21|5", "Rtsed", "SUrST", "nsaps", "vyNVU", "2|29|23|64", "0|43|57|4|", "NNXUu", "nCrbn", "wQPIq", "XBcOb", "39|40|47|6", "ljkOt", "yMPhx", "TXzzv", "0123456789", "fmdcS", "iXQwu", "grCxb", "3|6|1|4|7|", "wKeAM", "Iekey", "opqrstuvwx", "|7|17", "BQgZQ", "BtzmV", "jZUAt", "HYhpy", "Yvoqt", "VyzBI", "NNVLf", "dbmfK", "0|58|16|32", "UAFHv", "WNIsZ", "2|1|4|3|5|", "JFqRJ", "zObVA", "d24fb0d696", "XfWkD", "MFmWH", "lZISZ", "WzbFA", "kaQlD", "3f7d28e17f", "eSwEi", "YpeFX", "kZhzK", "KxKIe", "LAIPf", "LjyKQ", "YLwOK", "iqfMz", "51|8|0|65|", "JRihE", "nqEyg", "|37|22|27|", "ZXsFi", "goEwl", "|31|63|48|", "wvVCN", "wnDlW", "Myvqp", "UlhBp", "fwCDC", "charAt", "Lmhlz", "WQCAS", "UXeVn", "KIXRL", "HiEZt", "WNzfT", "lNWda", "tsNzQ"], + e = _0x4c28, + t = 368, + function (t) { + for (; --t;) + e.push(e.shift()) + }(++t); + var n = function (e, t) { + return _0x4c28[e -= 0] + }; + var n = function (e, t) { + return _0x4c28[e -= 0] + }; + window.md5 = function (e) { + var t = n + , r = { + fEXNi: function (e, t) { + return e(t) + }, + LzcOo: function (e, t, n) { + return e(t, n) + } + }; + r[t(3)] = function (e, t) { + return e(t) + } + , + r.wEOgn = function (e, t, n) { + return e(t, n) + } + , + r[t(120)] = function (e, t, n) { + return e(t, n) + } + , + r[t(69)] = function (e, t) { + return e == t + } + , + r[t(109)] = function (e, t) { + return e(t) + } + , + r[t(112)] = t(86), + r.oBesn = "900150983c" + t(37) + t(43) + "72", + r[t(70)] = t(18) + t(118), + r[t(16)] = function (e, t) { + return e < t + } + , + r[t(2)] = t(110) + t(5) + t(133) + "|55|13|12|" + t(146) + t(114) + t(94) + "35|68|33|4" + t(104) + t(52) + t(73) + t(88) + t(55) + "25|34|1|2|" + t(10) + t(4) + t(124) + t(58) + "52|59|66|7" + t(31) + t(22), + r[t(53)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(35)] = function (e, t) { + return e + t + } + , + r[t(141)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(91)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(65)] = function (e, t) { + return e + t + } + , + r[t(38)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(19)] = function (e, t) { + return e + t + } + , + r[t(117)] = function (e, t, n) { + return e(t, n) + } + , + r[t(92)] = function (e, t) { + return e + t + } + , + r[t(82)] = function (e, t) { + return e + t + } + , + r[t(111)] = function (e, t, n) { + return e(t, n) + } + , + r[t(78)] = function (e, t) { + return e + t + } + , + r.lZISZ = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r.Iekey = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r.AAZoR = function (e, t) { + return e + t + } + , + r[t(67)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r.UlhBp = function (e, t) { + return e + t + } + , + r.yMPhx = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(138)] = function (e, t) { + return e + t + } + , + r[t(121)] = function (e, t) { + return e + t + } + , + r[t(98)] = function (e, t, n) { + return e(t, n) + } + , + r.kHuTw = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(50)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(142)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(87)] = function (e, t) { + return e + t + } + , + r[t(90)] = function (e, t) { + return e + t + } + , + r[t(59)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(28)] = function (e, t) { + return e + t + } + , + r[t(119)] = function (e, t) { + return e + t + } + , + r.YpeFX = function (e, t) { + return e + t + } + , + r[t(7)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r.prrXX = function (e, t) { + return e + t + } + , + r.kaQlD = function (e, t) { + return e + t + } + , + r.qtIDJ = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r.xGAWc = function (e, t) { + return e + t + } + , + r[t(134)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(89)] = function (e, t) { + return e + t + } + , + r[t(15)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(9)] = function (e, t) { + return e + t + } + , + r[t(56)] = function (e, t) { + return e + t + } + , + r[t(6)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(32)] = function (e, t) { + return e + t + } + , + r[t(99)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(39)] = function (e, t) { + return e + t + } + , + r[t(113)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(106)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(66)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r.TXzzv = function (e, t) { + return e + t + } + , + r.NNVLf = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(79)] = function (e, t) { + return e + t + } + , + r[t(1)] = function (e, t, n, r, i, o, a, s) { + return e(t, n, r, i, o, a, s) + } + , + r[t(81)] = function (e, t) { + return e + t + } + , + r.MXnIN = function (e, t) { + return e >> t + } + , + r[t(23)] = function (e, t) { + return e << t + } + , + r.nqEyg = function (e, t) { + return e % t + } + , + r.kaXPV = function (e, t) { + return e >>> t + } + , + r[t(24)] = function (e, t, n) { + return e(t, n) + } + , + r[t(44)] = function (e, t, n) { + return e(t, n) + } + , + r[t(30)] = function (e, t, n) { + return e(t, n) + } + , + r[t(143)] = function (e, t) { + return e | t + } + , + r[t(101)] = function (e, t) { + return e & t + } + , + r[t(122)] = function (e, t, n, r, i, o, a) { + return e(t, n, r, i, o, a) + } + , + r.ZpUiH = function (e, t) { + return e & t + } + , + r[t(72)] = function (e, t) { + return e ^ t + } + , + r[t(130)] = function (e, t) { + return e ^ t + } + , + r[t(41)] = function (e, t) { + return e | t + } + , + r[t(116)] = function (e, t) { + return e > t + } + , + r[t(80)] = function (e, t) { + return e(t) + } + , + r[t(33)] = function (e, t, n) { + return e(t, n) + } + , + r[t(83)] = function (e, t) { + return e(t) + } + , + r[t(60)] = function (e, t) { + return e + t + } + , + r.FDfcp = function (e, t) { + return e * t + } + , + r[t(95)] = function (e, t) { + return e + t + } + , + r[t(51)] = function (e, t) { + return e & t + } + , + r.DdHPB = function (e, t) { + return e >> t + } + , + r.abYok = function (e, t) { + return e | t + } + , + r[t(84)] = function (e, t) { + return e << t + } + , + r[t(105)] = function (e, t) { + return e & t + } + , + r[t(8)] = function (e, t) { + return e - t + } + , + r[t(137)] = function (e) { + return e() + } + , + r.YVBwM = function (e, t) { + return e << t + } + , + r[t(27)] = function (e, t) { + return e & t + } + , + r[t(26)] = function (e, t) { + return e / t + } + , + r[t(74)] = function (e, t) { + return e * t + } + , + r[t(49)] = t(14) + "abcdef", + r[t(36)] = function (e, t) { + return e >> t + } + , + r[t(46)] = function (e, t) { + return e + t + } + , + r[t(75)] = function (e, t) { + return e >> t + } + , + r[t(47)] = function (e, t) { + return e * t + } + , + r[t(11)] = t(126) + t(128) + "UVWXYZabcdefghijklmn" + t(21) + "yz01234567" + t(139), + r[t(63)] = function (e, t) { + return e * t + } + , + r.KIXRL = function (e, t) { + return e << t + } + , + r[t(57)] = function (e, t) { + return e % t + } + , + r[t(77)] = function (e, t) { + return e << t + } + , + r[t(71)] = function (e, t) { + return e >> t + } + , + r.jZUAt = function (e, t) { + return e >> t + } + , + r[t(48)] = function (e, t) { + return e + t + } + , + r[t(17)] = function (e, t) { + return e % t + } + , + r[t(85)] = function (e, t) { + return e * t + } + , + r[t(61)] = function (e, t) { + return e < t + } + , + r.mfuQZ = function (e, t) { + return e + t + } + , + r[t(125)] = function (e, t) { + return e * t + } + , + r[t(0)] = function (e, t) { + return e(t) + } + ; + var i = r; + + function o(e, n) { + for (var r = t, o = i.WNzfT[r(131)]("|"), a = 0; ;) { + switch (o[a++]) { + case "0": + for (var d = 0; i.iXQwu(d, e.length); d += 16) + for (var p = i[r(2)][r(131)]("|"), h = 0; ;) { + switch (p[h++]) { + case "0": + w = i[r(53)](l, w, b, x, T, e[d + 2], 9, -51403784); + continue; + case "1": + x = u(x, T, w, b, e[d + 6], 23, 76029189); + continue; + case "2": + b = i[r(53)](u, b, x, T, w, e[i.JFqRJ(d, 9)], 4, -640364487); + continue; + case "3": + T = i[r(141)](c, T, w, b, x, e[d + 10], 15, -1051523); + continue; + case "4": + T = s(T, w, b, x, e[i.JFqRJ(d, 2)], 17, 606105819); + continue; + case "5": + w = i[r(91)](c, w, b, x, T, e[i[r(65)](d, 3)], 10, -1894446606); + continue; + case "6": + w = i.XfWkD(l, w, b, x, T, e[i.wKeAM(d, 14)], 9, -1019803690); + continue; + case "7": + T = i.pviAr(f, T, v); + continue; + case "8": + b = i.XfWkD(l, b, x, T, w, e[i[r(92)](d, 13)], 5, -1444681467); + continue; + case "9": + x = i[r(38)](s, x, T, w, b, e[i[r(82)](d, 3)], 22, -1044525330); + continue; + case "10": + w = s(w, b, x, T, e[i[r(82)](d, 5)], 12, 1200080426); + continue; + case "11": + x = i[r(38)](l, x, T, w, b, e[i[r(82)](d, 0)], 20, -373897302); + continue; + case "12": + w = i[r(38)](s, w, b, x, T, e[i[r(82)](d, 9)], 12, -1958435417); + continue; + case "13": + b = i.XfWkD(s, b, x, T, w, e[i.xcQTI(d, 8)], 7, 1770035416); + continue; + case "14": + var m = b; + continue; + case "15": + w = i[r(38)](u, w, b, x, T, e[i.xcQTI(d, 8)], 11, -2022574463); + continue; + case "16": + b = f(b, m); + continue; + case "17": + w = i[r(111)](f, w, g); + continue; + case "18": + x = l(x, T, w, b, e[i[r(78)](d, 12)], 20, -1921207734); + continue; + case "19": + w = i[r(40)](u, w, b, x, T, e[d + 4], 11, 1272893353); + continue; + case "20": + T = i[r(20)](u, T, w, b, x, e[i.PlAEw(d, 11)], 16, 1839030562); + continue; + case "21": + b = s(b, x, T, w, e[i[r(123)](d, 12)], 7, 1804550682); + continue; + case "22": + x = u(x, T, w, b, e[i[r(123)](d, 10)], 23, -1094730640); + continue; + case "23": + T = i[r(67)](c, T, w, b, x, e[d + 14], 15, -1416354905); + continue; + case "24": + b = s(b, x, T, w, e[i[r(123)](d, 4)], 7, -176418897); + continue; + case "25": + w = i.UXeVn(u, w, b, x, T, e[d + 0], 11, -358537222); + continue; + case "26": + b = i.UXeVn(l, b, x, T, w, e[i[r(62)](d, 1)], 5, -165796510); + continue; + case "27": + b = i.UXeVn(u, b, x, T, w, e[i[r(62)](d, 13)], 4, 681279174); + continue; + case "28": + b = i[r(12)](l, b, x, T, w, e[i[r(138)](d, 9)], 5, 568446438); + continue; + case "29": + w = i.yMPhx(c, w, b, x, T, e[d + 7], 10, 11261161415); + continue; + case "30": + var g = w; + continue; + case "31": + b = c(b, x, T, w, e[i.yGmJD(d, 8)], 6, 1873313359); + continue; + case "32": + x = i.aFxRD(f, x, y); + continue; + case "33": + T = i[r(12)](l, T, w, b, x, e[i[r(121)](d, 15)], 14, -660478335); + continue; + case "34": + T = i.kHuTw(u, T, w, b, x, e[d + 3], 16, -722881979); + continue; + case "35": + b = i[r(50)](l, b, x, T, w, e[i[r(121)](d, 5)], 5, -701520691); + continue; + case "36": + T = l(T, w, b, x, e[i[r(121)](d, 3)], 14, -187363961); + continue; + case "37": + T = i[r(142)](u, T, w, b, x, e[i.QgSUn(d, 7)], 16, -155497632); + continue; + case "38": + b = i.FcXlo(u, b, x, T, w, e[i.koePJ(d, 5)], 4, -378558); + continue; + case "39": + w = i[r(142)](u, w, b, x, T, e[i[r(90)](d, 12)], 11, -421815835); + continue; + case "40": + T = i[r(59)](u, T, w, b, x, e[i[r(28)](d, 15)], 16, 530742520); + continue; + case "41": + x = i.wvVCN(s, x, T, w, b, e[d + 15], 22, 1236531029); + continue; + case "42": + x = i[r(59)](l, x, T, w, b, e[i[r(119)](d, 4)], 20, -405537848); + continue; + case "43": + b = i[r(59)](s, b, x, T, w, e[i.lvwPz(d, 0)], 7, -680976936); + continue; + case "44": + b = i[r(59)](u, b, x, T, w, e[i[r(45)](d, 1)], 4, -1530992060); + continue; + case "45": + x = i.nCrbn(u, x, T, w, b, e[i[r(129)](d, 14)], 23, -35311556); + continue; + case "46": + b = c(b, x, T, w, e[i[r(42)](d, 12)], 6, 1700485571); + continue; + case "47": + x = i[r(7)](u, x, T, w, b, e[i.kaQlD(d, 2)], 23, -995338651); + continue; + case "48": + T = c(T, w, b, x, e[d + 6], 15, -1560198380); + continue; + case "49": + w = i[r(145)](l, w, b, x, T, e[i[r(107)](d, 6)], 9, -1069501632); + continue; + case "50": + x = i[r(134)](c, x, T, w, b, e[i[r(89)](d, 1)], 21, -2054922799); + continue; + case "51": + x = i.fmdcS(l, x, T, w, b, e[d + 8], 20, 1163531501); + continue; + case "52": + x = i[r(15)](c, x, T, w, b, e[i[r(9)](d, 13)], 21, 1309151649); + continue; + case "53": + x = i[r(15)](s, x, T, w, b, e[i[r(56)](d, 11)], 22, -1990404162); + continue; + case "54": + w = i[r(6)](s, w, b, x, T, e[i[r(32)](d, 13)], 12, -40341101); + continue; + case "55": + x = i.sFtiw(s, x, T, w, b, e[i.UAFHv(d, 7)], 22, -45705983); + continue; + case "56": + T = i.sFtiw(s, T, w, b, x, e[i.MFmWH(d, 6)], 17, -1473231341); + continue; + case "57": + w = i[r(99)](s, w, b, x, T, e[i.MFmWH(d, 1)], 12, -389564586); + continue; + case "58": + x = c(x, T, w, b, e[i[r(39)](d, 9)], 21, -343485551); + continue; + case "59": + b = i[r(113)](c, b, x, T, w, e[i[r(39)](d, 4)], 6, -145523070); + continue; + case "60": + T = i.bRLIN(s, T, w, b, x, e[i[r(39)](d, 10)], 17, -42063); + continue; + case "61": + var v = T; + continue; + case "62": + b = i[r(66)](c, b, x, T, w, e[d + 0], 6, -198630844); + continue; + case "63": + w = i[r(66)](c, w, b, x, T, e[i[r(13)](d, 15)], 10, -30611744); + continue; + case "64": + x = c(x, T, w, b, e[d + 5], 21, -57434055); + continue; + case "65": + T = i[r(29)](l, T, w, b, x, e[i[r(13)](d, 7)], 14, 1735328473); + continue; + case "66": + w = i[r(29)](c, w, b, x, T, e[i[r(79)](d, 11)], 10, -1120210379); + continue; + case "67": + var y = x; + continue; + case "68": + w = i[r(1)](l, w, b, x, T, e[d + 10], 9, 38016083); + continue; + case "69": + T = i[r(1)](s, T, w, b, x, e[i[r(79)](d, 14)], 17, -1502002290); + continue; + case "70": + T = i.SUrST(c, T, w, b, x, e[i[r(79)](d, 2)], 15, 718787259); + continue; + case "71": + T = l(T, w, b, x, e[i[r(81)](d, 11)], 14, 643717713); + continue + } + break + } + continue; + case "1": + var b = 1732584193; + continue; + case "2": + return Array(b, x, T, w); + case "3": + e[i.MXnIN(n, 5)] |= i[r(23)](128, i[r(54)](n, 32)); + continue; + case "4": + var x = -271733879; + continue; + case "5": + var w = 271733878; + continue; + case "6": + e[i.BQgZQ(i[r(115)](n + 64, 9), 4) + 14] = n; + continue; + case "7": + var T = -1732584194; + continue + } + break + } + } + + function a(e, n, r, o, a, s) { + var l = t; + return f(i.BtzmV(d, i[l(44)](f, i.dbmfK(f, n, e), i[l(30)](f, o, s)), a), r) + } + + function s(e, n, r, o, s, l, u) { + var c = t; + return a(i[c(143)](i[c(101)](n, r), i[c(101)](~n, o)), e, n, s, l, u) + } + + function l(e, n, r, o, s, l, u) { + var c = t; + return i[c(122)](a, i[c(143)](i.ZpUiH(n, o), i.ZpUiH(r, ~o)), e, n, s, l, u) + } + + function u(e, n, r, o, s, l, u) { + return i[t(122)](a, i.tsNzQ(n ^ r, o), e, n, s, l, u) + } + + function c(e, n, r, o, s, l, u) { + var c = t; + return i[c(122)](a, i[c(130)](r, i[c(41)](n, ~o)), e, n, s, l, u) + } + + function f(e, n) { + var r = t + , o = i[r(95)](65535 & e, i.iqfMz(n, 65535)) + , a = i[r(95)](e >> 16, i[r(97)](n, 16)) + i[r(97)](o, 16); + return i[r(103)](i[r(84)](a, 16), i[r(105)](o, 65535)) + } + + function d(e, n) { + var r = t; + return i.abYok(e << n, e >>> i[r(8)](32, n)) + } + + function p(e) { + for (var n = t, r = i[n(137)](Array), o = i[n(8)](i.vRqUp(1, 16), 1), a = 0; a < i.FDfcp(e[n(108)], 16); a += 16) + r[i[n(97)](a, 5)] |= i[n(102)](i[n(27)](e[n(127)](i[n(26)](a, 16)), o), i[n(54)](a, 32)); + return r + } + + function h(e) { + for (var n = t, r = i[n(49)], o = "", a = 0; i.iXQwu(a, i[n(74)](e[n(108)], 4)); a++) + o += i.xgzfr(r[n(64)](15 & i[n(36)](e[i[n(36)](a, 2)], i[n(46)](i[n(74)](a % 4, 8), 4))), r[n(64)](15 & i.wWwRM(e[a >> 2], i[n(47)](a % 4, 8)))); + return o + } + + return i[t(0)]((function (e) { + var n = t; + return i[n(144)](h, i[n(76)](o, i.vyNVU(p, e), 16 * e[n(108)])) + } + ), e) + }; + window[u(208)] = function (e) { + var t = u + , r = {}; + r.TGmSp = t(244) + "ARACTER_ERR", + r[t(238)] = t(224) + t(250) + "/", + r[t(205)] = "^([^ ]+( +" + t(230) + t(259), + r.aYkvo = function (e) { + return e() + } + , + r[t(254)] = function (e, t) { + return e % t + } + , + r.evetF = function (e, t) { + return e >> t + } + , + r.GfTek = t(196), + r[t(260)] = function (e, t) { + return e << t + } + , + r[t(229)] = function (e, t) { + return e | t + } + , + r[t(242)] = function (e, t) { + return e << t + } + , + r[t(228)] = function (e, t) { + return e & t + } + , + r[t(207)] = function (e, t) { + return e << t + } + , + r[t(202)] = function (e, t) { + return e & t + } + , + r.jdwcO = function (e, t) { + return e === t + } + , + r.kPdGe = t(231), + r[t(195)] = t(213), + r[t(201)] = function (e, t) { + return e & t + } + , + r[t(206)] = function (e, t) { + return e == t + } + , + r[t(219)] = function (e, t) { + return e + t + } + , + r[t(220)] = function (e, t) { + return e(t) + } + ; + var i = r; + if (/([^\u0000-\u00ff])/.test(e)) + throw new Error(i.TGmSp); + for (var o, a, s, l = 0, c = []; l < e[t(261)];) { + switch (a = e[t(237)](l), + s = i.kukBH(l, 6)) { + case 0: + delete window, + delete document, + c[t(246)](f[t(245)](i[t(212)](a, 2))); + break; + case 1: + // console.log('"WhHMm" === i[t(198)] :',"WhHMm" === i[t(198)]) + // console.log('n.g :', n.g) + // console.log('c[t(246)](f[t(245)](i.pHtmC(2 & o, 3) | i.evetF(a, 4))) :', c[t(246)](f[t(245)](i.pHtmC(2 & o, 3) | i.evetF(a, 4)))) + // console.log('-----------------') + try { + "WhHMm" === i[t(198)] || window && c[t(246)](f[t(245)](i.pHtmC(2 & o, 3) | i.evetF(a, 4))) + } catch (e) { + c[t(246)](f[t(245)](i[t(229)](i.cVCcp(3 & o, 4), a >> 4))) + } + break; + case 2: + c[t(246)](f[t(245)](i[t(229)](i[t(242)](15 & o, 2), i.evetF(a, 6)))), + c[t(246)](f[t(245)](i[t(228)](a, 63))); + break; + case 3: + c[t(246)](f[t(245)](i[t(212)](a, 3))); + break; + case 4: + c.push(f[t(245)](i[t(229)](i[t(207)](i.OWUOc(o, 4), 6), i[t(212)](a, 6)))); + break; + case 5: + c[t(246)](f[t(245)](i[t(229)](i[t(207)](i[t(202)](o, 15), 4), a >> 8))), + c.push(f.charAt(i[t(202)](a, 63))) + } + o = a, + l++ + } + return 0 == s ? i[t(226)](i[t(241)], i[t(195)]) || (c[t(246)](f[t(245)](i[t(201)](o, 3) << 4)), + c.push("FM")) : i.eMnqD(s, 1) && (c[t(246)](f[t(245)]((15 & o) << 2)), + c[t(246)]("K")), + i[t(219)](i.aQCDK(d(15), window.md5(c[t(234)](""))), i[t(220)](d, 10)) + } + // p_s = Date['parse'](new Date)['toString'](); + return btoa(timeString); +}; + +console.log(sdk('165343333')) +``` + diff --git "a/_posts/2022-10-21-\347\210\254\350\231\253\357\274\232Grafana+InfluxDB+MongoDB\345\256\236\347\216\260\347\210\254\350\231\253\345\217\257\350\247\206\345\214\226\345\212\250\346\200\201\347\233\221\346\216\247.md" "b/_posts/2022-10-21-\347\210\254\350\231\253\357\274\232Grafana+InfluxDB+MongoDB\345\256\236\347\216\260\347\210\254\350\231\253\345\217\257\350\247\206\345\214\226\345\212\250\346\200\201\347\233\221\346\216\247.md" new file mode 100644 index 00000000000..e80b4654b12 --- /dev/null +++ "b/_posts/2022-10-21-\347\210\254\350\231\253\357\274\232Grafana+InfluxDB+MongoDB\345\256\236\347\216\260\347\210\254\350\231\253\345\217\257\350\247\206\345\214\226\345\212\250\346\200\201\347\233\221\346\216\247.md" @@ -0,0 +1,334 @@ +--- +layout: post +title: 爬虫:Grafana+InfluxDB+MongoDB 实现爬虫可视化动态监控 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: false +tags: + - 爬虫 +--- + +# 1.前言 +- 本文介绍的方法,是使用 Grafana 和 InfluxDB 对爬虫进行可视化监控。 +- Grafana 是一个开源的分析和监控系统,拥有精美的web UI,支持多种图表,可以展示influxdb中存储的数据,并且有报警的功能。 +- Influxdb 是一款开源的时间序列数据库,专门用来存储和时间相关的数据(比如我用它存储某个时间点爬虫抓取信息的数量)。 +>设计原理:***爬虫将抓取的数据写入MongoDB,InfluxDB从MongoDB获取数据抓取情况,Grafana 从 InfluxDB 中获取爬虫抓取数据情况并做图形化展示。*** +系统环境:***MacOS High Sierra 10.12.6*** +- 效果展示: + 如下图: + ![]({{site.baseurl}}/img-post/grafana-1.png) + +# 2.Grafana介绍 +- Grafana简介: + - Grafana 是一款采用 go 语言编写的开源应用; + - Grafana 主要用于大规模指标数据的可视化展现; + - Grafana有着非常漂亮的图表和布局展示,功能齐全的度量仪表盘和图形编辑器。 +- Grafana支持数据源: + - Graphite; + - Zabbix; + - InfluxDB; + - Prometheus; + - OpenTSDB; + - 最新版本4.3.1已经支持 MySQL 数据源。 +- Grafana 主要特性: + - 灵活丰富的图形化选项; + - 可以混合多种风格; + - 支持多个数据源; + - 拥有丰富的插件扩展; + - 支持自动告警功能; + - 支持用户权限管理。 +# 3.InfluxDB介绍 +- InfluxDB 简介 + - InfluxDB 是一个当下比较流行的时序数据库; + - InfluxDB 使用 Go 语言编写; + - InfluxDB 无需外部依赖; + - InfluxDB 适合构建大型分布式系统的监控系统。 +- 主要特色功能: + - 基于时间序列:支持与时间有关的相关函数(如最大,最小,求和等); + - 可度量性:可以实时对大量数据进行计算; + - 基于事件:它支持任意的事件数据; + +# 4.安装&配置Grafana、InfluxDB + +Grafana 和 InfluxDB 安装非常方便,这一点可以和 Graphite 做一个鲜明的对比。 + +#### 4.1.安装 InfluxDB + +- 安装 InfluxDB + +``` +brew update +brew install influxdb +``` + +#### 4.2.安装 & 配置 Grafana + +- 安装 Grafana + +``` +brew update +brew install grafana +``` + +- 配置 Grafana + - 使用 vi 命令修改 Grafana 配置文件 + +``` +vi /usr/local/etc/grafana/grafana.ini +``` + +修改 内容如下: + +``` +[server] +# Protocol (http, https, socket) +;protocol = http + +# The ip address to bind to, empty will bind to all interfaces +;http_addr = + +# 此处修改端口号 +# The http port to use +;http_port = 3000 # 【 注意 】这里我用默认 3000 端口,可以根据需要修改。 + +# 此处修改界面访问地址 +# The public facing domain name used to access grafana from a browser +;domain = localhost # 【 注意 】这里我用默认 localhost 地址,可以根据需要修改。 + +# Redirect to correct domain if host header does not match domain +# Prevents DNS rebinding attacks +;enforce_domain = false + +# The full public facing url you use in browser, used for redirects and emails +# If you use reverse proxy and sub path specify full url (with sub path) +;root_url = http://localhost:3000 +``` + +# 5.编写爬虫代码 + +这里我使用的是以前写的一个爬取豆瓣电影的的爬虫代码。 +> 注意: +> -- 爬虫使用的 MongoDB 的 database 名称为 learn_selenium_doubandianying; +> -- 爬虫使用的 MongoDB 的 table 名称为 movie_info; + +爬虫代码,此处省略。 + + +# 6.编写监控脚本 + +>*考虑到可能要增加爬虫到监控中,因此这里使用了热更新对监控进行动态配置*。 + +#### 6.1.监控脚本 *influx_monitor.py* + +``` +import ast +import time +import pymongo +import traceback +from configparser import ConfigParser +from influxdb import InfluxDBClient +from datetime import datetime +from os.path import getmtime + +# 配置 influxdb +client = InfluxDBClient(host='localhost', port=8086) # influxdb默认端口为8086 + +# 创建 database +client.create_database('Spider') +# switch 到 database +client.switch_database('Spider') + +# 设定配置文件 +config_name = 'influx_settings.conf' + +WATCHED_FILES = [config_name] +WATCHED_FILES_MTIMES = [(f, getmtime(f)) for f in WATCHED_FILES] + +_count_dict = {} +_size_dict = {} + +# 获取配置文件中的设置 +def parse_config(file_name): + + try: + # 创建一个配置文件对象 + cf = ConfigParser() + + # 打开配置文件 + cf.read(file_name) + + # 获取配置文件中的统计频率 + interval = cf.getint('time', 'interval') + + # 获取配置文件中要监控的 dbs 和 collection + dbs_and_collections = ast.literal_eval(cf.get('db', 'db_collection_dict')) + + return interval, dbs_and_collections + + except: + print(traceback.print_exc()) + return None + + +# 从 MongoDB 获取数据,并写入 InfluxDB +def insert_data(dbs_and_collections): + + # 连接 MongoDB 数据库 + mongodb_client = pymongo.MongoClient(host='127.0.0.1',port=27017) # 直接使用默认地址端口连接 MongoDB + + for db_name, collection_name in dbs_and_collections.items(): + + # 数据库操作,创建 collection 集合对象 + db = mongodb_client[db_name] + collection = db[collection_name] + + # 获取 collection 集合大小 + collection_size = round(float(db.command("collstats", collection_name).get('size')) / 1024 / 1024, 2) + + # 获取 collection 集合内数据条数 + current_count = collection.count() + + # 初始化数据条数,当程序刚执行时,条数初始量就设置为第一次执行时获取的数据 + init_count = _count_dict.get(collection_name, current_count) + # 初始化数据大小,当程序刚执行时,大小初始量就设置为第一次执行时获取的数据大小 + init_size = _size_dict.get(collection_name, collection_size) + + # 得到数据条数增长量 + increase_amount = current_count - init_count + # 得到数据大小增长量 + increase_collection_size = collection_size - init_size + + # 得到当前时间 + current_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + + # 赋值 + _count_dict[collection_name] = current_count + _size_dict[collection_name] = collection_size + + # 构建 + json_body = [ + { + "measurement": "crawler", + "time": current_time, + "tags": { + "spider_name": collection_name + }, + "fields": { + "count": current_count, + "increase_count": increase_amount, + "size": collection_size, + "increase_size": increase_collection_size + + } + } + ] + # 将获取 + if client.write_points(json_body): + print('成功写入influxdb!',json_body) + + +def main(): + # 获取配置文件中的监控频率和MongoDB数据库设置 + interval, dbs_and_collexctions = parse_config(config_name) + + # 如果配置有问题则报错 + if (interval or dbs_and_collexctions) is None: + raise ValueError('配置有问题,请打开配置文件重新设置!') + + print('设置监控频率:', interval) + print('设置要监控的MongoDB数据库和集合:', dbs_and_collexctions) + + last_interval = interval + last_dbs_and_collexctions = dbs_and_collexctions + + # 这里实现配置文件热更新 + for f, mtime in WATCHED_FILES_MTIMES: + while True: + # 检查配置更新情况,如果文件有被修改,则重新获取配置内容 + if getmtime(f) != mtime: + # 获取配置信息 + interval, dbs_and_collections = parse_config(config_name) + print('提示:配置文件于 %s 更新!' % (time.strftime("%Y-%m-%d %H:%M:%S"))) + + # 如果配置有问题,则使用上一次的配置 + if (interval or dbs_and_collexctions) is None: + interval = last_interval + dbs_and_collexctions = last_dbs_and_collexctions + + else: + print('使用新配置!') + print('新配置内容:', interval, dbs_and_collexctions) + mtime = getmtime(f) + + # 写入 influxdb 数据库 + insert_data(dbs_and_collexctions) + + # 使用 sleep 设置每次写入的时间间隔 + time.sleep(interval) + +if __name__ == '__main__': + main() +``` + +#### 6.2.配置文件 *influx_settings.conf* + +>*配置文件主要用于热更新相关设置* 。 + +``` +# [需要监控的 MongoDB 数据的 数据库名 和 集合名] +[db] +db_collection_dict = { + 'learn_selenium_doubandianying': 'movie_info', + } + +# [设置循环间隔时间] +[time] +interval = 8 +``` + +# 7.配置 Grafana + +#### 7.1. 运行 influxDB + +``` +python3 influx_monitor.py +``` + +新建一个 terminal 窗口,使用 vi 命令修改配置文件 *influx_settings.conf* 。 + +``` +vi influx_settings.conf +``` + +修改间隔时间为8秒,并保存退出。 +这时运行 influxDB 的窗口,提示配置更新,说明配置热更新可用。 + +#### 7.2. 启动 grafana + +``` +brew services start grafana +``` + +#### 7.3. 运行爬虫文件 + +启动 MongoDB 数据库服务。 + +``` +brew services mongodb start +``` + +#### 7.4. Grafana web窗口设置 +- 打开 Chrome 浏览器,输入 http://127.0.0.1:3000 登录 grafana 页面。 +- 连接本地 influxDB 数据库。 +- 设置监控的数据对象 + -- 在监控脚本中,写入influxDB的代码如下,其中 "measurement" 对应 表名,"fields" 对应写入的字段; +>"measurement": "crawler", +> "fields": { +> "count": current_count, +> "increase_count": increase_amount, +> "size": collection_size, +> "increase_size": increase_collection_size +> } + diff --git "a/_posts/2022-10-21-\347\210\254\350\231\253\357\274\232\347\210\254\350\231\253\345\270\270\347\224\250\346\265\217\350\247\210\345\231\250USER-AGENT\346\225\264\347\220\206\346\261\207\346\200\273.md" "b/_posts/2022-10-21-\347\210\254\350\231\253\357\274\232\347\210\254\350\231\253\345\270\270\347\224\250\346\265\217\350\247\210\345\231\250USER-AGENT\346\225\264\347\220\206\346\261\207\346\200\273.md" new file mode 100644 index 00000000000..827cb9bc688 --- /dev/null +++ "b/_posts/2022-10-21-\347\210\254\350\231\253\357\274\232\347\210\254\350\231\253\345\270\270\347\224\250\346\265\217\350\247\210\345\231\250USER-AGENT\346\225\264\347\220\206\346\261\207\346\200\273.md" @@ -0,0 +1,567 @@ +--- +layout: post +title: 爬虫:爬虫常用浏览器USER-AGENT整理汇总 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: false +tags: + - 爬虫 +--- + + +##说明 + +- 下为整理汇总的浏览器头清单,主要是火狐浏览器, +- 既有PC端也有移动端,使用的时候请注意; + +``` +agents = [ +"Mozilla/5.0 (compatible; U; ABrowse 0.6; Syllable) AppleWebKit/420+ (KHTML, like Gecko)", +"Mozilla/5.0 (compatible; U; ABrowse 0.6; Syllable) AppleWebKit/420+ (KHTML, like Gecko)", +"Mozilla/5.0 (compatible; ABrowse 0.4; Syllable)", +"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; Acoo Browser 1.98.744; .NET CLR 3.5.30729)", +"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; Acoo Browser 1.98.744; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; Acoo Browser; GTB5; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; InfoPath.1; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SV1; Acoo Browser; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; Avant Browser)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; GTB5; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; Maxthon; InfoPath.1; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; GTB5;", +"Mozilla/4.0 (compatible; Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; Acoo Browser 1.98.744; .NET CLR 3.5.30729); Windows NT 5.1; Trident/4.0)", +"Mozilla/4.0 (compatible; Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB6; Acoo Browser; .NET CLR 1.1.4322; .NET CLR 2.0.50727); Windows NT 5.1; Trident/4.0; Maxthon; .NET CLR 2.0.50727; .NET CLR 1.1.4322; InfoPath.2)", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; Acoo Browser; GTB6; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; InfoPath.1; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; Acoo Browser; GTB5; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; InfoPath.1; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB6; Acoo Browser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0; Acoo Browser; GTB5; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; InfoPath.1; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; GTB5; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; GTB5; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; InfoPath.1; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Acoo Browser; InfoPath.2; .NET CLR 2.0.50727; Alexa Toolbar)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Acoo Browser; .NET CLR 2.0.50727; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Acoo Browser; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.50727; FDM; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; InfoPath.2)", +"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Acoo Browser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; America Online Browser 1.1; Windows NT 5.1; (R1 1.5); .NET CLR 2.0.50727; InfoPath.1)", +"Mozilla/4.0 (compatible; MSIE 7.0; America Online Browser 1.1; rev1.5; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; America Online Browser 1.1; rev1.5; Windows NT 5.1; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; America Online Browser 1.1; rev1.5; Windows NT 5.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 4.0; InfoPath.1; .NET CLR 2.0.50727; Media Center PC 3.0; InfoPath.2)", +"Mozilla/4.0 (compatible; MSIE 7.0; America Online Browser 1.1; rev1.2; Windows NT 5.1; SV1; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1; SV1; HbTools 4.7.0)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1; SV1; FunWebProducts; .NET CLR 1.1.4322; InfoPath.1; HbTools 4.8.0)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1; SV1; FunWebProducts; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 3.1)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1; SV1; .NET CLR 1.1.4322; HbTools 4.7.1)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1; SV1; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1; SV1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 3.1)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1; SV1; .NET CLR 1.0.3705; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1; SV1)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1; FunWebProducts; (R1 1.5); HbTools 4.7.7)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1; FunWebProducts)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.1)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows NT 5.0)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; Windows 98)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; rev1.5; Windows NT 5.1; SV1; FunWebProducts; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; rev1.5; Windows NT 5.1; SV1; .NET CLR 1.1.4322; InfoPath.1)", +"AmigaVoyager/3.2 (AmigaOS/MC680x0)", +"AmigaVoyager/2.95 (compatible; MC680x0; AmigaOS; SV1)", +"AmigaVoyager/2.95 (compatible; MC680x0; AmigaOS)", +"Mozilla/5.0 (compatible; MSIE 9.0; AOL 9.7; AOLBuild 4343.19; Windows NT 6.1; WOW64; Trident/5.0; FunWebProducts)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.7; AOLBuild 4343.27; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.7; AOLBuild 4343.21; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; .NET4.0E)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.7; AOLBuild 4343.19; Windows NT 5.1; Trident/4.0; GTB7.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.7; AOLBuild 4343.19; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; .NET4.0E)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.7; AOLBuild 4343.19; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; .NET4.0E)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.5004; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.5001; Windows NT 5.1; Trident/4.0)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.5000; Windows NT 5.1; Trident/4.0; FunWebProducts)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.5000; Windows NT 5.1; Trident/4.0; .NET4.0C; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.27; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.27; Windows NT 5.1; Trident/4.0; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 4.0; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; InfoPath.2)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.17; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.168; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.168; Windows NT 5.1; Trident/4.0; GTB7.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.130; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.130; Windows NT 5.1; Trident/4.0; FunWebProducts; GTB6.6; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; yie8)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.12; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.12; Windows NT 5.1; Trident/4.0; GTB6.3)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.124; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.122; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.122; Windows NT 5.1; Trident/4.0; FunWebProducts)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.111; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; .NET4.0E)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.110; Windows NT 5.1; Trident/4.0; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.6; AOLBuild 4340.104; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.6; AOLBuild 4340.128; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.5; AOLBuild 4337.43; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.21022; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.5; AOLBuild 4337.29; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.21022; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.93; Windows NT 5.1; Trident/4.0; DigExt; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.89; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.81; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.81; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30618) (Compatible; ; ; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET ", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.81; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.80; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.53; Windows NT 6.0; FunWebProducts; GTB6; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.43; Windows NT 6.0; WOW64; GTB5; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.43; Windows NT 5.1; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.43; Windows NT 5.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.42; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.40; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.40; Windows NT 6.0; FunWebProducts; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.40; Windows NT 5.1; Trident/4.0; GTB6; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.40; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.36; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.30618; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.36; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30618; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.36; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/5.0 (compatible; MSIE 9.0; AOL 9.1; AOLBuild 4334.5012; Windows NT 6.0; WOW64; Trident/5.0)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.1; AOLBuild 4334.5011; Windows NT 6.1; WOW64; Trident/4.0; GTB7.2; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.5010; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.30729; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.5009; Windows NT 5.1; GTB5; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.5006; Windows NT 5.1; Trident/4.0; DigExt; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.5006; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.5006; Windows NT 5.1; GTB5; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.5006; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 1.0.3705; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.5000; Windows NT 5.1; Trident/4.0)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.5000; Windows NT 5.1; Media Center PC 3.0; .NET CLR 1.0.3705; .NET CLR 1.1.4322; InfoPath.1)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.36; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.34; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.34; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.34; Windows NT 5.1; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.34; Windows NT 5.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.27; Windows NT 6.0; WOW64; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506; Media Center PC 5.0); UnAuth-State", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.27; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506); UnAuth-State", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4334.27; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; InfoPath.1); UnAuth-State", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.1; AOLBuild 4327.65535; Windows NT 5.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.50727); UnAuth-State", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 9.1; AOLBuild 4334.5006; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30)", +"Mozilla/5.0 (compatible; MSIE 9.0; AOL 9.0; Windows NT 6.0; Trident/5.0)", +"Mozilla/4.0 (compatible; MSIE 8.0; AOL 9.0; AOLBuild 4327.5201; Windows NT 6.0; WOW64; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.30729; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; InfoPath.2; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 6.0; Trident/4.0; FunWebProducts; GTB6.4; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 1.1.4322; .NET CLR 3.5.30729; OfficeLiveConnector.1.3; OfficeLivePatch.0.0; .NET CLR 3.0.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; Seekmo 10.0.406.0)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 6.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 6.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 6.0; FunWebProducts; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; InfoPath.2; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 6.0; FunWebProducts; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; Seekmo 10.0.341.0)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 6.0; FunWebProducts; GTB5; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 5.1; Trident/4.0; GTB6; FunWebProducts; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 5.1; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 5.1; InfoPath.1)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 5.1; GTB5; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 4.0)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 5.1; GTB5; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 5.1; GTB5; .NET CLR 1.1.4322; .NET CLR 2.0.50727; OfficeLiveConnector.1.3; OfficeLivePatch.0.0)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 5.1; GTB5; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 5.1; GTB5; .NET CLR 1.0.3705; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.0; Windows NT 5.1; FunWebProducts; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) )", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 8.0; Windows NT 5.1; GTB5; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 8.0; Windows NT 5.1; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 8.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.1; .NET CLR 3.0.04506.30)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 8.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 8.0; Windows NT 5.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 8.0; Windows NT 5.1; .NET CLR 1.0.3705)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 8.0; Windows NT 5.1)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; YComp 5.0.0.0; .NET CLR 1.0.3705)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; SV1; (R1 1.3); .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; SV1)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; Q312461)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; FunWebProducts; SV1; .NET CLR 1.0.3705)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; FunWebProducts; SV1)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; FunWebProducts)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; .NET CLR 1.0.3705)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1; (R1 1.3))", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.1)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 8.0; Windows NT 5.0)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 7.0; Windows NT 5.1; FunWebProducts)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 7.0; Windows NT 5.1; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 7.0; Windows NT 5.1) (Compatible; ; ; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 7.0; AOL 7.0; Windows NT 5.1)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; YComp 5.0.2.6; Hotbar 4.2.8.0)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; YComp 5.0.2.4)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; YComp 5.0.0.0)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; SV1; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; SV1; .NET CLR 1.0.3705)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; SV1)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; Q312461; YComp 5.0.0.0)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; Q312461)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; Hotbar 4.2.8.0)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; Hotbar 4.1.7.0)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1; .NET CLR 1.0.3705)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.1)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows NT 5.0)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows 98; Win 9x 4.90; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows 98; Win 9x 4.90; (R1 1.3))", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 7.0; Windows 98; Win 9x 4.90)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 6.0; Windows NT 5.1)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 6.0; Windows 98; Win 9x 4.90)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 6.0; Windows 98)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 6.0; Windows 95)", +"Mozilla/4.0 (compatible; MSIE 5.0; AOL 6.0; Windows 98; DigExt; YComp 5.0.2.5)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 5.0; Windows NT 5.1)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 5.0; Windows 98; .NET CLR 1.1.4322)", +"Mozilla/4.0 (compatible; MSIE 6.0; AOL 5.0; Windows 98)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 5.0; Windows NT 5.0)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 5.0; Windows 98; YComp 5.0.0.0)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 5.0; Windows 98; Win 9x 4.90)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 5.0; Windows 98)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 5.0; Windows 95)", +"Mozilla/4.0 (compatible; MSIE 5.0; AOL 5.0; Windows 98; DigExt)", +"Mozilla/4.0 (compatible; MSIE 5.0; AOL 5.0; Windows 95; DigExt)", +"Mozilla/4.0 (compatible; MSIE 5.0; AOL 5.0; Windows 95)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 5.0; Windows NT 5.0)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 4.0; Windows 98)", +"Mozilla/4.0 (compatible; MSIE 5.5; AOL 4.0; Windows 95)", +"Mozilla/4.0 (compatible; MSIE 5.0; AOL 4.0; Windows 98; DigExt)", +"Mozilla/4.0 (compatible; MSIE 5.01; AOL 4.0; Windows 98)", +"Mozilla/4.0 (compatible; MSIE 4.01; AOL 4.0; Windows 98)", +"Mozilla/4.0 (compatible; MSIE 4.01; AOL 4.0; Windows 95)", +"Mozilla/4.0 (compatible; MSIE 4.01; AOL 4.0; Mac_68K)", +"Mozilla/5.0 (X11; U; UNICOS lcLinux; en-US) Gecko/20140730 (KHTML, like Gecko, Safari/419.3) Arora/0.8.0", +"Mozilla/5.0 (X11; U; Linux; de-DE) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.8.0", +"Mozilla/5.0 (Windows; U; ; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.8.0", +"Mozilla/5.0 (Windows; U; ; en-NZ) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.8.0", +"Mozilla/5.0 (Windows; U; ; en-EN) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.8.0", +"Mozilla/5.0 (X11; U; Linux; ru-RU) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6 (Change: 802 025a17d)", +"Mozilla/5.0 (X11; U; Linux; fi-FI) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6 (Change: 754 46b659a)", +"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6", +"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6", +"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6 (Change: )", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6 (Change: )", +"Mozilla/5.0 (X11; U; Linux; pt-PT) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4", +"Mozilla/5.0 (X11; U; Linux; nb-NO) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.4", +"Mozilla/5.0 (X11; U; Linux; it-IT) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.4 (Change: 413 12f13f8)", +"Mozilla/5.0 (X11; U; Linux; it-IT) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4", +"Mozilla/5.0 (X11; U; Linux; hu-HU) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4 (Change: 388 835b3b6)", +"Mozilla/5.0 (X11; U; Linux; hu-HU) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4", +"Mozilla/5.0 (X11; U; Linux; fr-FR) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4", +"Mozilla/5.0 (X11; U; Linux; es-ES) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4 (Change: 388 835b3b6)", +"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4", +"Mozilla/5.0 (X11; U; Linux; en-GB) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4 (Change: 388 835b3b6)", +"Mozilla/5.0 (X11; U; Linux; en-GB) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4", +"Mozilla/5.0 (X11; U; Linux; de-DE) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4", +"Mozilla/5.0 (X11; U; Linux; cs-CZ) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.4 (Change: 333 41e3bc6)", +"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.4 (Change: )", +"Mozilla/5.0 (Windows; U; Windows NT 6.0; de-DE) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.4 (Change: )", +"Mozilla/5.0 (Windows; U; Windows NT 5.2; pt-BR) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.4 (Change: )", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.4 (Change: )", +"Mozilla/5.0 (X11; U; Linux; en-GB) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 239 52c6958)", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-BE) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)", +"Mozilla/5.0 (X11; U; Linux; sk-SK) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2 (Change: 0 )", +"Mozilla/5.0 (X11; U; Linux; nb-NO) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2 (Change: 0 )", +"Mozilla/5.0 (X11; U; Linux; es-CR) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2 (Change: 0 )", +"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2 (Change: 189 35c14e0)", +"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2 (Change: 0 )", +"Mozilla/5.0 (X11; U; Linux; de-DE) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2 (Change: 0 )", +"Mozilla/5.0 (Windows; U; Windows NT 6.0; de-DE) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; nl-NL) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; de-CH) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2", +"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Arora/0.11.0 Safari/533.3", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.34 (KHTML, like Gecko) Arora/0.11.0 Safari/534.34", +"Mozilla/5.0 (X11; U; Linux; pl-PL) AppleWebKit/532.4 (KHTML, like Gecko) Arora/0.10.2 Safari/532.4", +"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.34 (KHTML, like Gecko) Arora/0.10.2 Safari/534.34", +"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527 (KHTML, like Gecko, Safari/419.3) Arora/0.10.1", +"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-MY) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.10.0", +"Mozilla/5.0 (Windows; U; ; hu-HU) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.10.0", +"Mozilla/5.0 (Windows; U; ; hu-HU) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.10.0", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; Avant Browser; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; Avant Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; .NET CLR 3.5.21022; InfoPath.2)", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; Avant Browser; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30618; InfoPath.1)", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB6.4; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; chromeframe; Avant Browser; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; InfoPath.1; .NET CLR 3.0.4506.", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB5; Avant Browser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; Avant Browser; Avant Browser; .NET CLR 2.0.50727)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT; Avant Browser; Avant Browser; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET4.0C; .NET4.0E; Avant Browser)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/4.0; Avant Browser; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; Avant Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; InfoPath.1; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; Avant Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; .NET CLR 3.5.21022; InfoPath.2)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0; GTB6.3; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; Avant Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 1.1.4322; .NET CLR 3.5.30729; .NET CLR 3.0.30729", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0; Avant Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0; Avant Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; .NET CLR 3.5.21022; InfoPath.2)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0; Avant Browser; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30618; InfoPath.1)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; Avant Browser; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Avant Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; .NET CLR 1.1.4322; InfoPath.2)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Avant Browser; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30618; InfoPath.2; OfficeLiveConnector.1.3; OfficeLivePatch.0.0)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Avant Browser; Avant Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; Tablet PC 2.0)", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Avant Browser; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", +"Mozilla/5.0 (Windows; U; WinNT; en; rv:1.0.2) Gecko/20030311 Beonex/0.8.2-stable", +"Mozilla/5.0 (Windows; U; WinNT; en; Preview) Gecko/20020603 Beonex/0.8-stable", +"Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.8.1b2) Gecko/20060821 BonEcho/2.0b2 (Debian-1.99+2.0b2+dfsg-1)", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1b2) Gecko/20060821 BonEcho/2.0b2", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1b2) Gecko/20060826 BonEcho/2.0b2", +"Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.8.1b2) Gecko/20060831 BonEcho/2.0b2", +"Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.8.1b1) Gecko/20060601 BonEcho/2.0b1 (Ubuntu-edgy)", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1a3) Gecko/20060526 BonEcho/2.0a3", +"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.8.1a2) Gecko/20060512 BonEcho/2.0a2", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1a2) Gecko/20060512 BonEcho/2.0a2", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1a2) Gecko/20060512 BonEcho/2.0a2", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-GB; rv:1.8.1a2) Gecko/20060512 BonEcho/2.0a2", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X Mach-O; en-US; rv:1.8.1a2) Gecko/20060512 BonEcho/2.0a2", +"Mozilla/5.0 (X11; U; OpenBSD ppc; en-US; rv:1.8.1.9) Gecko/20070223 BonEcho/2.0.0.9", +"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.9) Gecko/20071103 BonEcho/2.0.0.9", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.9) Gecko/20071113 BonEcho/2.0.0.9", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.8pre) Gecko/20071012 BonEcho/2.0.0.8pre", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.7pre) Gecko/20070901 BonEcho/2.0.0.7pre", +"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.7) Gecko/20070918 BonEcho/2.0.0.7", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.7) Gecko/20071018 BonEcho/2.0.0.7", +"Mozilla/5.0 (BeOS; U; BeOS BePC; en-US; rv:1.8.1.7) Gecko/20070917 BonEcho/2.0.0.7", +"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.6) Gecko/20070812 BonEcho/2.0.0.6", +"Mozilla/5.0 (BeOS; U; BeOS BePC; en-US; rv:1.8.1.6) Gecko/20070731 BonEcho/2.0.0.6", +"Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.8.1.5pre) Gecko/20070604 BonEcho/2.0.0.5pre", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.5pre) Gecko/20070622 BonEcho/2.0.0.5pre", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.4pre) Gecko/20070414 BonEcho/2.0.0.4pre", +"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.8.1.4pre) Gecko/20070510 BonEcho/2.0.0.4pre", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4pre) Gecko/20070416 BonEcho/2.0.0.4pre", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4pre) Gecko/20070410 BonEcho/2.0.0.4pre", +"Mozilla/5.0 (X11; U; OpenBSD ppc; en-US; rv:1.8.1.4) Gecko/20070223 BonEcho/2.0.0.4", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.4) Gecko/20070531 BonEcho/2.0.0.4", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070416 BonEcho/2.0.0.4", +"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.8.1.3pre) Gecko/20070302 BonEcho/2.0.0.3pre", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.3pre) Gecko/20070302 BonEcho/2.0.0.3pre", +"Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.8.1.3pre) Gecko/20070301 BonEcho/2.0.0.3pre", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.3) Gecko/20070517 BonEcho/2.0.0.3", +"Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.9a3) Gecko/20070409 BonEcho/2.0.0.3", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1.3) Gecko/20070329 BonEcho/2.0.0.3", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.3) Gecko/20070322 BonEcho/2.0.0.3", +"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.8.1.2pre) Gecko/20070226 BonEcho/2.0.0.2pre", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070213 BonEcho/2.0.0.2pre", +"Mozilla/5.0 (BeOS; U; Haiku BePC; en-US; rv:1.8.1.21pre) Gecko/20090218 BonEcho/2.0.0.21pre", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.2) Gecko/20070302 BonEcho/2.0.0.2", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.2) Gecko/20070224 BonEcho/2.0.0.2", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2) Gecko/20070227 BonEcho/2.0.0.2", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1.2) Gecko/20070223 BonEcho/2.0.0.2", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.1pre) Gecko/20061203 BonEcho/2.0.0.1pre", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.1pre) Gecko/20061202 BonEcho/2.0.0.1pre", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.1pre) Gecko/20061122 BonEcho/2.0.0.1pre", +"Mozilla/5.0 (BeOS; U; Haiku BePC; en-US; rv:1.8.1.18) Gecko/20081114 BonEcho/2.0.0.18", +"Mozilla/5.0 (BeOS; U; Haiku BePC; en-US; rv:1.8.1.17) Gecko/20080831 BonEcho/2.0.0.17", +"Mozilla/5.0 (BeOS; U; BeOS BePC; en-US; rv:1.8.1.17) Gecko/20080831 BonEcho/2.0.0.17", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.14) Gecko/20080417 BonEcho/2.0.0.14", +"Mozilla/5.0 (BeOS; U; Haiku BePC; en-US; rv:1.8.1.14) Gecko/20080429 BonEcho/2.0.0.14", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.13) Gecko/20080401 BonEcho/2.0.0.13", +"Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.12pre) Gecko/20080103 BonEcho/2.0.0.12pre", +"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.12) Gecko/20080208 BonEcho/2.0.0.12", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080321 BonEcho/2.0.0.12 (SliTaz GNU/Linux)", +"Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1.11) Gecko/20080208 BonEcho/2.0.0.11", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.11) Gecko/20071204 BonEcho/2.0.0.11", +"Mozilla/5.0 (BeOS; U; BeOS BePC; en-US; rv:1.8.1.10) Gecko/20071128 BonEcho/2.0.0.10", +"Mozilla/5.0 (X11; U; Linux ppc; en-US; rv:1.8.1.1) Gecko/20061219 BonEcho/2.0.0.1", +"Mozilla/5.0 (X11; U; Linux mips; en-US; rv:1.8.1.1) Gecko/20070628 BonEcho/2.0.0.1", +"Mozilla/5.0 (X11; U; Linux i686; en; rv:1.8.1.1) Gecko/20070117 Epiphany/2.16 BonEcho/2.0.0.1", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20070222 BonEcho/2.0.0.1", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20070220 BonEcho/2.0.0.1", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20070217 BonEcho/2.0.0.1", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20070215 BonEcho/2.0.0.1", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20070115 BonEcho/2.0.0.1", +"Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.1) Gecko/20070110 BonEcho/2.0.0.1", +"Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.1) Gecko/20070131 BonEcho/2.0.0.1", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.1) Gecko/20061222 BonEcho/2.0.0.1", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.1) Gecko/20061230 BonEcho/2.0.0.1", +"Mozilla/5.0 (BeOS; U; BeOS BePC; en-US; rv:1.8.1.1) Gecko/20061220 BonEcho/2.0.0.1", +"Mozilla/5.0 (X11; U; Win95; en-US; rv:1.8.1) Gecko/20061125 BonEcho/2.0", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061129 BonEcho/2.0", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061031 BonEcho/2.0", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061026 BonEcho/2.0", +"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061003 BonEcho/2.0", +"Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1) Gecko/20061031 BonEcho/2.0", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20061210 BonEcho/2.0", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20061209 BonEcho/2.0", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20061121 BonEcho/2.0", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20061113 BonEcho/2.0", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20061112 BonEcho/2.0", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20060930 BonEcho/2.0", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1) Gecko/20061026 BonEcho/2.0", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1) Gecko/20061025 BonEcho/2.0", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061024 BonEcho/2.0", +"Mozilla/5.0 (BeOS; U; BeOS BeBox; fr; rv:1.9) Gecko/2008052906 BonEcho/2.0", +"Mozilla/5.0 (Windows; U; Windows NT 5.1; en; rv:1.9a1) Gecko/20061128 BonEcho/0.7b1", +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 1.1.4322; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; Browzar)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; XH; rv:8.578.498) fr, Gecko/20121021 Camino/8.723+ (Firefox compatible)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; XH; rv:8.578.498) fr, Gecko/20121021 Camino/8.443+ (Firefox compatible)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.8; it; rv:1.9.2.28) Gecko/20130628 Camino/3.245.226 (MultiLang) (like Firefox/3.621.218)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.8; it; rv:1.93.26.2658) Gecko/20141026 Camino/2.176.223 (MultiLang) (like Firefox/3.64.2268)0", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en; rv:1.9.2.14pre) Gecko/20101212 Camino/2.1a1pre (like Firefox/3.6.14pre)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en; rv:1.9.2.14pre) Gecko/20101212 Camino/2.1a1pre (like Firefox/3.6.14pre)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en; rv:1.9.2.29pre) Gecko/20130101 Camino/2.1.3pre (like Firefox/3.6.29pre)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.5; de; rv:1.9.2.28) Gecko/20120308 Camino/2.1.2 (MultiLang) (like Firefox/3.6.28)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.8; it; rv:1.9.2.28) Gecko/20120308 Camino/2.1.2 (MultiLang) (like Firefox/3.6.28)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; fr; rv:1.9.2.28) Gecko/20120308 Camino/2.1.2 (MultiLang) (like Firefox/3.6.28)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; en; rv:1.9.2.24) Gecko/20111114 Camino/2.1 (like Firefox/3.6.24)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en; rv:1.9.0.8pre) Gecko/2009022800 Camino/2.0b3pre", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en; rv:1.9.0.10pre) Gecko/2009041800 Camino/2.0b3pre (like Firefox/3.0.10pre)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.5; it; rv:1.9.0.19) Gecko/2010111021 Camino/2.0.6 (MultiLang) (like Firefox/3.0.19)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en; rv:1.9.0.19) Gecko/2010111021 Camino/2.0.6 (MultiLang) (like Firefox/3.0.19)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; en; rv:1.9.0.19) Gecko/2010051911 Camino/2.0.3 (like Firefox/3.0.19)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; nl; rv:1.9.0.19) Gecko/2010051911 Camino/2.0.3 (MultiLang) (like Firefox/3.0.19)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en; rv:1.9.0.18) Gecko/2010021619 Camino/2.0.2 (like Firefox/3.0.18)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en; rv:1.8.1.4pre) Gecko/20070511 Camino/1.6pre", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; de; rv:1.8.1.5pre) Gecko/20070605 Camino/1.6a1pre", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.4pre) Gecko/20070526 Camino/1.6a1pre", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.4pre) Gecko/20070521 Camino/1.6a1pre", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; it; rv:1.8.1.21) Gecko/20090327 Camino/1.6.7 (MultiLang) (like Firefox/2.0.0.21pre)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; fr; rv:1.8.1.21) Gecko/20090327 Camino/1.6.7 (MultiLang) (like Firefox/2.0.0.21pre)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en; rv:1.8.1.21) Gecko/20090327 Camino/1.6.7 (like Firefox/2.0.0.21pre)", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.24) Gecko/20100305 Camino/1.6.11 (like Firefox/2.0.0.24)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en; rv:1.8.1.12) Gecko/20080206 Camino/1.5.5", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X Mach-O; en; rv:1.8.1.12) Gecko/20080206 Camino/1.5.5", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.11) Gecko/20071128 Camino/1.5.4", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en; rv:1.8.1.6) Gecko/20070809 Camino/1.5.1", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.6) Gecko/20070809 Firefox/2.0.0.6 Camino/1.5.1", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.6) Gecko/20070809 Camino/1.5.1", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6 Camino/1.5.1", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en; rv:1.8.1.4) Gecko/20070509 Camino/1.5", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.4) Gecko/20070609 Camino/1.5", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.4) Gecko/20070607 Camino/1.5", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.4) Gecko/20070509 Camino/1.5", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en; rv:1.9a4pre) Gecko/20070404 Camino/1.2+", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.4pre) Gecko/20070417 Camino/1.1b+", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en; rv:1.8.1.2pre) Gecko/20070227 Camino/1.1b", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.2pre) Gecko/20070223 Camino/1.1b", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.2pre) Gecko/20070108 Camino/1.1a2", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.1pre) Gecko/20061126 Camino/1.1a1+", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061018 Camino/1.1a1", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.1) Gecko/20060203 Camino/1.0rc1", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.1) Gecko/20060119 Camino/1.0b2+", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8) Gecko/20051229 Camino/1.0b2", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8) Gecko/20051228 Camino/1.0b1", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8) Gecko/20051107 Camino/1.0b1", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8b4) Gecko/20050914 Camino/1.0a1", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.10) Gecko/20070228 Camino/1.0.4", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.10) Gecko/20070228 Camino/1.0.4", +"Mozilla/5.0 (Macintosh; U; PPC Max OS X Mach-O; it-IT; rv:1.8.0.7) Gecko/200609211 Camino/1.0.3", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.7) Gecko/20060911 Camino/1.0.3 (MultiLang)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.7) Gecko/20060911 Camino/1.0.3", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.7) Gecko/20060911 Camino/1.0.3", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.4) Gecko/20060613 Camino/1.0.2", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.4) Gecko/20060613 Camino/1.0.2", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.3) Gecko/20060503 Camino/1.0.1", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.3) Gecko/20060427 Camino/1.0.1", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1b1) Gecko/20060807 Camino/1.0+", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1b1) Gecko/20060721 Camino/1.0+", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1a3) Gecko/20060528 Camino/1.0+", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1) Gecko/20061013 Camino/1.0+ (Firefox compatible)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1) Gecko/20061013 Camino/1.0+", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1a3) Gecko/20060601 Camino/1.0+", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.1) Gecko/20060307 Camino/1.0", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.1) Gecko/20060214 Camino/1.0", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8b5) Gecko/20051021 Camino/1.0+", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.1) Gecko/20060214 Camino/1.0", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8b2) Gecko Camino/0.9+", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.7) Gecko/20040517 Camino/0.8b", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.7.8) Gecko/20050427 Camino/0.8.4", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.7.2) Gecko/20040825 Camino/0.8.1", +"Mozilla/5.0 Gecko/20030306 Camino/0.7", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.0.1) Gecko/20030306 Camino/0.7", +"Mozilla/4.08 (Charon; Inferno)", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.8 (KHTML, like Gecko, Safari) Cheshire/1.0.UNOFFICIAL", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/418.9 (KHTML, like Gecko, Safari) Cheshire/1.0.UNOFFICIAL", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/419 (KHTML, like Gecko, Safari/419.3) Cheshire/1.0.ALPHA", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9 (KHTML, like Safari) Cheshire/1.0.ALPHA", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9 (KHTML, like Gecko, Safari/111) Cheshire/1.0.ALPHA", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9 (KHTML, like Gecko, Safari) Safari/419.3 Cheshire/1.0.ALPHA", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3 Cheshire/1.0.ALPHA", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9 (KHTML, like Gecko) AppleWebKit/418.9 Cheshire/1.0.ALPHA", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/419 (KHTML, like Gecko, Safari/419.3) Cheshire/1.0.ALPHA", +"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/419 (KHTML, like Gecko, Safari/125) Cheshire/1.0.ALPHA", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; pl-PL; rv:1.0.1) Gecko/20021111 Chimera/0.6", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; pl-PL; rv:1.0.1) Gecko/20021111 Chimera/0.6", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US; rv:1.0.1) Gecko/20021111 Chimera/0.6", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US; rv:1.0.1) Gecko/20021104 Chimera/0.6", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.0.1) Gecko/20030111 Chimera/0.6", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.0.1) Gecko/20030109 Chimera/0.6", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.0.1) Gecko/20021220 Chimera/0.6", +"Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.0.1) Gecko/20021216 Chimera/0.6", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/44.0.2403.155 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36", +"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2226.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.4; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36", +"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 4.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", +"Mozilla/5.0 (X11; OpenBSD i386) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.3319.102 Safari/537.36", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2309.372 Safari/537.36", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2117.157 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1866.237 Safari/537.36", +"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/4E423F", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36 Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10", +"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.517 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36", +"Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1467.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1464.0 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1500.55 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.90 Safari/537.36", +"Mozilla/5.0 (X11; NetBSD) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36", +"Mozilla/5.0 (X11; CrOS i686 3912.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.60 Safari/537.17", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.15 (KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.14 (KHTML, like Gecko) Chrome/24.0.1292.0 Safari/537.14", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13", +"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13", +"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1284.0 Safari/537.13", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.6 Safari/537.11", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.6 Safari/537.11", +"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.26 Safari/537.11", +"Mozilla/5.0 (Windows NT 6.0) yi; AppleWebKit/345667.12221 (KHTML, like Gecko) Chrome/23.0.1271.26 Safari/453667.1221", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.17 Safari/537.11", +"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_0) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.79 Safari/537.4", +"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1", +"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", +"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1", +"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5", +"Mozilla/5.0 (X11; FreeBSD amd64) AppleWebKit/536.5 (KHTML like Gecko) Chrome/19.0.1084.56 Safari/1EA69", +"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", +"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", +"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", +"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", +"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", +"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", +"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3", +"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", +"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.22 (KHTML, like Gecko) Chrome/19.0.1047.0 Safari/535.22", +"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.21 (KHTML, like Gecko) Chrome/19.0.1042.0 Safari/535.21", +"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.21 (KHTML, like Gecko) Chrome/19.0.1041.0 Safari/535.21", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20", +"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/18.6.872.0 Safari/535.2 UNTRUSTED/1.0 3gpp-gba UNTRUSTED/1.0", +] +``` + +- 更多 User-Agent 头,请参考这个链接,有全网最完整的Uer-Agent:[http://www.useragentstring.com/pages/useragentstring.php?typ=Browser](http://www.useragentstring.com/pages/useragentstring.php?typ=Browser) diff --git "a/_posts/2022-10-21-\347\210\254\350\231\253\357\274\232\347\210\254\350\231\253\347\232\204\346\263\225\345\276\213\351\243\216\351\231\251.md" "b/_posts/2022-10-21-\347\210\254\350\231\253\357\274\232\347\210\254\350\231\253\347\232\204\346\263\225\345\276\213\351\243\216\351\231\251.md" new file mode 100644 index 00000000000..fe184af7545 --- /dev/null +++ "b/_posts/2022-10-21-\347\210\254\350\231\253\357\274\232\347\210\254\350\231\253\347\232\204\346\263\225\345\276\213\351\243\216\351\231\251.md" @@ -0,0 +1,37 @@ +--- +layout: post +title: 爬虫:爬虫的法律风险 +subtitle: +date: 2022-02-01 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: false +tags: + - 爬虫 +--- + + +## 1. 扰乱企业经营 +- 刷单 +- 点赞 +- 抢票 +## 2. 非法获取个人隐私 +- 姓名 +- 电话 +- 身份 +- 地址 +- 邮箱 +## 3. 非法侵入接口 +- 抓取、诱探非公开接口。 +## 4. 不正当竞争 +- 抓取竞争对手数据直接使用(比如,盗用别人平台商品listing直接挂在自己网站)。 +## 5. 侵犯版权 +- 抓取版权数据直接使用。 +## 6. 外挂&插件 +- 直接在APP、网站上开发外挂或插件程序。 +## 7. 倒卖数据 +- 直接抓取数据,向外公开出售。 +## 8. 提供对外数据接口 +- 抓取数据,提供接口给第三方。 + +>总之,不要让抓取对象领导难受、工程师难堪,不要影响抓取对象正常经营。 \ No newline at end of file diff --git "a/_posts/2022-10-27-HTTP\357\274\232Cookie & Session \347\232\204\346\246\202\345\277\265\345\214\272\345\210\253\344\273\245\345\217\212\345\256\236\351\231\205\345\272\224\347\224\250.md" "b/_posts/2022-10-27-HTTP\357\274\232Cookie & Session \347\232\204\346\246\202\345\277\265\345\214\272\345\210\253\344\273\245\345\217\212\345\256\236\351\231\205\345\272\224\347\224\250.md" new file mode 100644 index 00000000000..1e5062bb7d6 --- /dev/null +++ "b/_posts/2022-10-27-HTTP\357\274\232Cookie & Session \347\232\204\346\246\202\345\277\265\345\214\272\345\210\253\344\273\245\345\217\212\345\256\236\351\231\205\345\272\224\347\224\250.md" @@ -0,0 +1,91 @@ +--- +layout: post +title: HTTP:Cookie & Session 的概念区别以及实际应用 +subtitle: +date: 2022-02-07 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: false +tags: + - HTTP +--- + + +#1.前言 + +- 在爬虫面试过程中,Cookie 和 Session 相关的知识点是经常遇到的问题之一; +- Cookie & Session 配合 Http 协议,是最基础、最广泛的用户识别认证实现方式。 + +# 2. Cookie + +#### 2.1. Cookie 的基本概念 + +- Cookie 是一段键值对格式的文本信息; +- 用户通过 Http 协议访问服务器的时候,服务器会将 Cookie 返回给客户端浏览器,并将这些数据加上一些限制条件,例如有效期(Expire Time); +- 当用户下次访问的时候,Cookie 又被完整的带给服务器,服务器凭借 Cookie 即可辨认用户的状态; + +### 2.2. Cookie 的优缺点 + +- 优点:数据存储在客户端,减轻服务器的压力,提高网站性能; +- 缺点:安全性不高,在客户端很容易被查看,也有可能会被截获并破解; + +# 3. Session 基本概念 + +#### 3.1. Session 的基本概念 + +- Session 又称会话控制,是一种网络通讯会话机制; +- 用户通过 Http 协议访问服务器的时候,服务器会在内存区域保存一个 Session 对象,并生成 session id; +- 服务器将 session id 返回给客户端,客户端将其保存在 Cookie 中; +- 当用户下次访问的时候,服务器凭借访问请求所带 Cookie 中的 session id,即可找到对应的 Session 验证用户身份,并服务于访问请求处理; + +#### 3.2. Session 的优缺点 + +- 优点:安全性较高,登陆验证后账号密码等信息均保存于服务器,降低了暴露的几率; +- 缺点:占用服务器资源,如果并发访问过多,Session 大量积聚会导致内存溢出; + +# 4. Cookie & Session 的区别 + +#### 4.1. 存取方式: + +- Cookie 中只能保存 ASCII 字符串,如果需要存取 Unicode 或者二进制数据,需要先进行编码; +- Session 中能存取任何类型的数据,包括 str、int、list、map等; + +#### 4.2. 隐私策略 + +- Cookie 存储在客户端,对用户是可见的,客户端上的一些程序可能会窥探、复制、修正 Cookie 中的内容; +- Session 存储于服务器上,对客户端是透明的,不存在敏感信息泄露风险; + +#### 4.3. 有效期 + +- Cookie 可以是设置一个数字很大的过期时间属性,以保证 Cookie 长期有效; +- Seesion 只要浏览器关闭就会失效,且如果 Session 设置的时间超长,服务器积累的 Seesion 越来越多会导致内存溢出; + +#### 4.4.服务器压力不同 + +- Cookie 保存在客户端,不占用服务器资源,如果并发访问的用户比较多,Cookie 是最优选择; +- Seesion 是保管在服务器端的,每个用户都会产生一个 Session,如果并发访问 的用户过多,会耗费大量的服务器资源; + +#### 4.5.浏览器支持 + +- Cookie 需要浏览器支持,如果浏览器禁用或不支持 Cookie,则会话跟踪就会失效; +- 如果浏览器禁用 Cookie,则需要 Session + url 重写,即在请求 url 后面加上 session id; + +#### 4.6. 跨域支持 + +- Cookie 支持跨域名访问; +- Seesion 只在域名内有效; + +# 5. Cookie & Session 的联系 + +- Session 需要基于 Cookie 实现,即服务器生成 session 之后将 session id 发送给客户端,客户端下次访问的时候通过 session id 找到对应的 Session ,验证用户身份并服务于访问请求处理; +- 如果浏览器禁用 Cookie,则需要 Session + url 重写,即在请求 url 后面加上 session id; + +# 6. 实际应用 + +#### 6.1. Cookie + +- 登录 Github 账号的时候,Github 服务器会在登录成功后返回 Cookie 给客户端,当用户下次请求的时候就不用再进行登录操作; + +#### 6.2. Session + +- 用户登录购物商城的时候,服务器会生成 Session 对象,并分配 session id 给用户,之后用户在整个商城添加商品到购物车的时候,添加商品的行为都会与 session id 关联起来,用户就可以在购物车上看到自己添加的所有商品。 diff --git "a/_posts/2022-10-27-HTTP\357\274\232Http \345\215\217\350\256\256 \346\227\240\350\277\236\346\216\245 & \346\227\240\347\212\266\346\200\201 \346\246\202\345\277\265\350\247\243\346\236\220.md" "b/_posts/2022-10-27-HTTP\357\274\232Http \345\215\217\350\256\256 \346\227\240\350\277\236\346\216\245 & \346\227\240\347\212\266\346\200\201 \346\246\202\345\277\265\350\247\243\346\236\220.md" new file mode 100644 index 00000000000..77f858bad6c --- /dev/null +++ "b/_posts/2022-10-27-HTTP\357\274\232Http \345\215\217\350\256\256 \346\227\240\350\277\236\346\216\245 & \346\227\240\347\212\266\346\200\201 \346\246\202\345\277\265\350\247\243\346\236\220.md" @@ -0,0 +1,75 @@ +--- +layout: post +title: HTTP:Http 协议 无连接 & 无状态 概念解析 +subtitle: +date: 2022-02-07 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: false +tags: + - HTTP +--- + + +# 1. 无连接概念 + +#### 1.1. 无连接 + +- 通信双方不需要事先建立一条通信线路; +- 在无连接方法中,网络把分组数据传送到目的地之外不做任何事情; +- 如果分组数据传送中丢失,接收方必须检测到错误并请求重发; +- 如果分组因采用不同路径而未按顺序到达,接收方需要将数据重新排序; +- ***无连接不保证传输质量、不按顺序、不进行流量控制、不可恢复错误***; +- 例如,IP、UDP、HTTP 协议是无连接的; + +#### 1.2. 面向连接 + +- 通信双方在通信时需要建立一条通信线路; +- 面向连接的通信,过程包括建立连接、使用连接、释放连接三个过程; +- 在分组交换通信过程中,信道理论上永远不会断; +- ***面向连接保证传输质量、按顺序传送、有流量控制、可恢复错误***; +- 例如 TCP 协议、电话系统是面向连接的可靠传输。 + +#### 1.3. Http 协议无连接 + +- Http 协议无连接,指的是服务器限制每次连接客户端 **只处理一个请求**。服务器处理完用户请求,并收到客户端应答以后,随即断开连接; +- Http 协议之所以设计为无连接,是为了将 Http 协议设计成请求时建立连接、请求完释放连接,以尽快将资源释放出来服务器其他客户端; + +#### 1.4. Http 无连接与 Keep-Alive 功能 + +- 随着网页越来越复杂,无连接模式越来越低效,Keep-Alive 被踢出来用于解决这个问题; +- Keep-Alive 功能使客户端到服务器的连接长期有效,当客户端后粗访问服务器的时候,Keep-Alive 避免了重建连接; +> 注意: +> Keep-Alive 影响了服务器的性能,因为在处理暂停期间,服务器上本来可以被释放资源仍旧被占用,当 Web 服务器和 Application 服务器在同一台机器上运行的时候,Keep-Alive 对功能资源的占用影响尤为突出。 + + +# 2. 无状态概念 + +#### 2.1. 状态的含义 + +- 服务器是否具有数据储存功能; +- 判断依据是指两个来自相同发起者的请求在服务器端是否具有上下文关系。 + +#### 2.2. 无状态的含义 + +- 同一个 URL 请求没有上下文关系; +- 协议对事务处理没有记忆能力; +- 每次请求都是相互独立的,它的执行结果与之前的请求和之后的请求是没有直接关系的,不会受之前的请求应答结果直接影响,也不会影响到后面的请求的应答; +- 服务器中没有保存客户端的状态,客户端每次必须带上自己的额状态区请求服务器。 + +#### 2.3. 网络协议状态 + +- IP 是无状态的,IP 协议只负责把数据包发送到指定 IP,不会考虑前面是否已经发生过数据包,也不考虑后面还会不会发送数据包; +- TCP 是有状态的,通过包头中的控制字段表明各个包之间的关系,从而做到可靠传输。同时,TCP 是面向连接的,TCP 通过“三次握手”确保对方存在,并未有状态的连接做准备; +- UDP 是无状态的,做不到可靠传输,也不需要建立连接; +- HTTP 是无状态的,HTTP 底层是 TCP(有状态),但是每次 HTTP 请求之间是没有任何关系的; + +#### 2.4. Http 无状态 + +- 服务器没有保存客户端的状态信息; +- 客户端每次必须带上去哪个自己的状态向服务器发送请求,比如借助于 cookie 或 session 发起访问。 +>注意: +>HTTP 之所以设计出无状态的,原因是: +-- HTTP 协议下同一个 URL 对应同一个超文本,这对所有的访问请求来说都是一样的,没有必要设计成有状态的; +-- 另外,无状态下服务器的资源很快被释放,应答速度会更快; +- 随着用户需求和技术的发展,静态网页上逐渐增加了许多动态生成的内容,HTML 语法增加了 Form 表单等,浏览器也增加了 Dom 等功能,HTTP 协议无状态属性严 diff --git "a/_posts/2022-10-27-HTTP\357\274\232http \344\270\216 https \347\232\204\346\246\202\345\277\265\345\214\272\345\210\253\344\273\245\345\217\212\345\256\236\351\231\205\345\272\224\347\224\250.md" "b/_posts/2022-10-27-HTTP\357\274\232http \344\270\216 https \347\232\204\346\246\202\345\277\265\345\214\272\345\210\253\344\273\245\345\217\212\345\256\236\351\231\205\345\272\224\347\224\250.md" new file mode 100644 index 00000000000..1da6f0b217b --- /dev/null +++ "b/_posts/2022-10-27-HTTP\357\274\232http \344\270\216 https \347\232\204\346\246\202\345\277\265\345\214\272\345\210\253\344\273\245\345\217\212\345\256\236\351\231\205\345\272\224\347\224\250.md" @@ -0,0 +1,52 @@ +--- +layout: post +title: HTTP:http 与 https 的概念区别以及实际应用 +subtitle: +date: 2022-02-07 +author: dex0423 +header-img: img/post-bg-os-metro.jpg +catalog: false +tags: + - HTTP +--- + + +# 1. 前言 +在爬虫岗位面试过程中,经常会被问到一个问题,就是 http/https 协议的工作原理,以及 http 和 https 的区别。 +作为爬虫工程师需要具备的基本知识,对于 Http/Https 的概念以及区别的理解是爬虫工程师必须要掌握的。 + +# 2. Http & Https 协议基本概念 +#### 2.1. Http 协议 +- Http 协议是 Hyper Text Transfer Protocal (超文本传输协议)的缩写,适用于从万维网服务器传输超文本协议到本地浏览器的传输协议; + + ![]({{site.baseurl}}/img-post/http-1.png) + +#### 2.2. Https 协议 +- Http 协议是 Hyper Text Transfer Protocal over Secure Socket Layer(基于安全层套接的超文本传输协议)的缩写,是在 Http 基础上加了 SSL(Secure Socket Layer)层; +- SSL 主要用于 web 的安全传输协议,在传输层对网络连接进行加密,保障在网络上的数据传输安全; +- CA(Certificate Authority)证书被应用于 Https 加密操作中,该证书由证书认证机构颁布,里面包含了公钥; +- Https 使用的加密技术: +>-- **对称密钥加密**: +>概念:发送端采用已知的算法对信息进行加密,比如 MD5 或者 Base64,之后秘钥被加密传输给接收端,接收端使用秘钥对接收的信息进行解密; +>缺点:一旦被窃听,或者信息被劫持,秘钥皆可能被破解; +> +>-- **非对称密钥加密**: +>概念:服务器告诉客户端,按照给定的公开密钥进行加密,服务器接收到秘钥后,再按照自己的秘钥进行解密; +>缺点:可能有人模仿服务器向客户端发送公共秘钥,而且非对称加密的效率比较低; +> +>-- **证书加密**: +>服务器向数字证书认证机构提出公开密钥申请,证书机构识别申请宁者的身份后将公开密钥做数字签名,并将公开密钥放入证书,之后服务器将证书发送给客户端,客户端通过证书中的数字签名验证证书的真伪,以确保公钥为真。 +- 现在,越来越多的网站支持 Http 而不是 Https,不过仍然有部分站点使用 Http 协议。 +> 2017年1月发布的Chrome 56浏览器,开始把收集密码或信用卡数据的HTTP页面标记为“不安全”,若用户使用2017年10月推出的Chrome 62,带有输入数据的HTTP页面和所有以无痕模式浏览的HTTP页面都会被标记为“不安全”。此外,苹果公司强制所有iOS App在2017年1月1日前使用HTTPS加密。 +>如下图: + +![]({{site.baseurl}}/img-post/http-2.png) + +# 3. Http 与 Https 协议的区别 +- Http 是明文传输,Https 使用 SSL 进行了加密; +- Http 是无状态的连接,Https 使用了信息加密和安全认证; +- Http 端口号为 80, Https 端口号为 443; +- ***使用 Http 协议时数据获取的会更齐全,而使用 Https 获取数据则会有缺失***。 +# 4. 实践中爬取 Http 与 Https 的区别 +- 在爬虫开发中,确定了 url 地址以后,尽可能采用 Http,以获取更多数据,而不是采用 Https 协议获取。 +- 不过现在很多站点都不支持 Http 了,所以在开发时需要做好测试工作,确定网页是否同时支持 Http 和 Https。 diff --git "a/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232Frida\345\256\211\350\243\205\351\205\215\347\275\256\345\217\212\344\275\277\347\224\250.md" "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232Frida\345\256\211\350\243\205\351\205\215\347\275\256\345\217\212\344\275\277\347\224\250.md" new file mode 100644 index 00000000000..0a1b40b9bde --- /dev/null +++ "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232Frida\345\256\211\350\243\205\351\205\215\347\275\256\345\217\212\344\275\277\347\224\250.md" @@ -0,0 +1,63 @@ +--- +layout: post +title: 安卓逆向:Frida安装配置及使用 +subtitle: +date: 2022-02-07 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 安卓逆向 +--- + +## 1. Frida 组成部分 + +- Frida-server: 运行在设备上 +- Frida :Python模块 +- Frida-tools:提供cli工具命令 跟Frida-server交互 + +## 2. Frida 官方文档 + +- [https://www.frida.re/docs/javascript-api](https://www.frida.re/docs/javascript-api) + +## 3. Frida安装配置 + +#### 电脑安装 frida & frida-tools + +``` +pip install frida +pip install frida-tools +``` + +> 注意:frida 版本与 python 版本之间的对应关系,可以参考:[https://pypi.org/project/frida/#files](https://pypi.org/project/frida/#files)。 + +#### 下载 frida-server + +- 下载地址:[https://github.com/frida/frida/releases](https://github.com/frida/frida/releases)。 + +#### 手机运行 frida-server + +``` +adb push frida-server-15.1.14-android-x86 /data/local/tmp/ + +adb shell + +cd /data/local/tmp + +# 确定手机当前用户是root用户或拥有root权限 +chmod 777 frida-server-15.1.14-android-x86 + +./frida-server-15.1.14-android-x86 + +``` + +#### 电脑上运行 + +``` +frida-ps -U +``` + +- 如果运行成功,命令行窗口会打印出手机上正在运行的程序。 + + + diff --git "a/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232Kali\351\205\215\347\275\256v2ray\350\277\233\350\241\214\347\277\273\345\242\231\350\277\207\347\250\213\345\205\250\347\272\252\345\275\225.md" "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232Kali\351\205\215\347\275\256v2ray\350\277\233\350\241\214\347\277\273\345\242\231\350\277\207\347\250\213\345\205\250\347\272\252\345\275\225.md" new file mode 100644 index 00000000000..c58e607ca54 --- /dev/null +++ "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232Kali\351\205\215\347\275\256v2ray\350\277\233\350\241\214\347\277\273\345\242\231\350\277\207\347\250\213\345\205\250\347\272\252\345\275\225.md" @@ -0,0 +1,67 @@ +--- +layout: post +title: 安卓逆向:Kali配置v2ray进行翻墙过程全纪录 +subtitle: +date: 2022-02-07 +author: dex0423 +header-img: img/post-bg-hacker.jpg +catalog: true +tags: + - 安卓逆向 +--- + + +## 1. 下载 v2ray + +#### 下载 AppImage + +下载地址:[https://github.com/Qv2ray/Qv2ray/releases/download/v1.99.6/Qv2ray-refs.tags.v1.99.6-linux.AppImage](https://github.com/Qv2ray/Qv2ray/releases/download/v1.99.6/Qv2ray-refs.tags.v1.99.6-linux.AppImage) + +#### 下载 v2ray 核心文件 + +下载地址:[https://github.com/v2ray/v2ray-core/releases/download/v4.22.1/v2ray-linux-64.zip](https://github.com/v2ray/v2ray-core/releases/download/v4.22.1/v2ray-linux-64.zip) + +下载后解压缩。 + +#### Objection 安装 + +``` +pip install -U objection + +``` + +## 2. 启动 + +#### 启动前设置 + +新建 v2ray 文件夹,将 AppImage 和解压后的 v2ray-linux-64 文件夹,放到 v2ray 文件夹中。 + +修改 AppImage 的 Property > Permission,在 Allow this file to run as a program 前的复选款打钩。 + +![配置 AppImage]({{site.baseurl}}/img-post/v2ray-kali-1.png) + + +#### 启动 AppImage + +按照下面的顺序,依次完成配置。 + +![配置 AppImage]({{site.baseurl}}/img-post/v2ray-kali-2.png) + +![配置 AppImage]({{site.baseurl}}/img-post/v2ray-kali-3.png) + +![配置 AppImage]({{site.baseurl}}/img-post/v2ray-kali-4.png) + +![配置 AppImage]({{site.baseurl}}/img-post/v2ray-kali-5.png) + +![配置 AppImage]({{site.baseurl}}/img-post/v2ray-kali-6.png) + +![配置 AppImage]({{site.baseurl}}/img-post/v2ray-kali-7.png) + + +#### 导入 VMess + +具体资源请自行查找,导入方式可以直接黏贴url,也可以截屏扫描二维码等,具体步骤此处不再赘述。 + +#### 配置成功 + +![配置 AppImage]({{site.baseurl}}/img-post/v2ray-kali-8.png) diff --git "a/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232adb\345\270\270\347\224\250\345\221\275\344\273\244\346\261\207\346\200\273\345\217\212\345\270\270\350\247\201\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232adb\345\270\270\347\224\250\345\221\275\344\273\244\346\261\207\346\200\273\345\217\212\345\270\270\350\247\201\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000000..8cb02de1dac --- /dev/null +++ "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232adb\345\270\270\347\224\250\345\221\275\344\273\244\346\261\207\346\200\273\345\217\212\345\270\270\350\247\201\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,166 @@ +--- +layout: post +title: 安卓逆向:adb常用命令汇总及常见问题汇总 +subtitle: +date: 2022-02-07 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 安卓逆向 +--- + +# 基本操作 + +#### 查看 adb 版本 + +``` +adb version +``` + +#### 启动关闭服务 + +``` +adb start-server +adb kill-server +``` + +#### 检查5037端口是否被占用 + +>若启动之后用USB连接手机之后,找不到设备,我们使用这个命令检查5037端口是否被占用,如果被占用,则提示 `cannot bind 'tcp:5037'`。 + +``` +adb nodaemon server +``` + +#### 查看连接设备 + +``` +adb devices +``` + +# 操作设备 + +#### 进入设备 + +``` +adb shell +``` + +#### 进入多个设备 + +``` +adb -s shell +``` + +#### 获取手机的序列号 + +``` +adb get-serialno +``` + +#### 查看手机屏幕分辨率 + +``` +adb shell wm size +``` + +#### 显示手机的cpu信息 + +``` +adb shell cat /proc/cpuinfo +``` + +#### 查看手机的设备型号 + +``` +adb shell getprop ro.product.model +``` + +#### 查看 Android 系统版本 + +``` +adb shell getprop ro.build.version.release +``` + +#### 查看手机 cpu类型是 ARM 还是 x86 + +```adb shell getprop ro.product.cpu.abi``` + +#### 获取手机的 mac 地址 + +``` +adb shell cat /sys/class/net/wlan0/address +``` + +#### 查看 IP地址 + +```adb shell ifconfig | grep Mask``` + +#### 显示手机里面所有应用的包名 + +``` +adb shell pm list packages +``` + +#### 查看内存信息 + +```adb shell cat /proc/meminfo``` + +#### 查看硬件与系统属性 + +```adb shell cat /system/build.prop``` + + +# 操作应用 + +#### 安装 apk + +``` +adb install -r `` +``` + +#### 卸载程序 + +``` +adb uninstall +``` + +>`` 为程序的 package。 +#### 获取某个应用的包名和这个应用的启动入口 + +``` +adb shell dumpsys activity | grep mFocusedActivity +``` +>其中, `com.xxxxxx` 即为这个应用的包名,`xxx.xxx.xxx.xxx.MainActivity` 即为这个应用的启动入口。 + +#### 通过ADB命令来启动应用 + +``` +adb shell am start -n /. +``` +>其中,`` 代表应用的包名,`.` 代表这个包的启动入口。 + +# 操作文件 + +#### 从安卓机器 pull 文件到电脑 + +``` +adb pull +``` + +#### 推送本地文件到安卓指定位置 + +``` +adb push /storage/emulated/0/Download/ +``` + +# 常见问题 + +#### adb 版本不匹配 + +>adb server version (36) doesn't match this client (41); killing... + +原因:当前夜神模拟器的adb版本是36,android-sdk里面的adb版本是41,不匹配导致的。 + +解决:将 \android-sdk-windows\platform-tools 中 adb.exe 替换到模拟器根目录下的 nox_adb.exe。 \ No newline at end of file diff --git "a/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232mitmproxy\344\275\277\347\224\250\346\226\271\346\263\225\345\256\236\350\267\265.md" "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232mitmproxy\344\275\277\347\224\250\346\226\271\346\263\225\345\256\236\350\267\265.md" new file mode 100644 index 00000000000..4d615b4e5c3 --- /dev/null +++ "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232mitmproxy\344\275\277\347\224\250\346\226\271\346\263\225\345\256\236\350\267\265.md" @@ -0,0 +1,154 @@ +--- +layout: post +title: 安卓逆向:mitmproxy使用方法实践 +subtitle: 使用 mitmproxy 抓取 京东APP 数据 +date: 2022-02-07 +author: dex0423 +header-img: img/post-bg-hacker.jpg +catalog: true +tags: + - 安卓逆向 +--- + + +## 1. Mitmproxy 简介 + +- Mitmproxy本意为中间人攻击代理; +- 在爬虫中主要用作抓取 http_connect、request、response 等数据; +- mitmproxy官方文档地址:https://docs.mitmproxy.org/stable/; +>【提示】 +**Mitmproxy 相较于 fillder、wireshark 的不同:** +-- *mitmproxy 不仅可以截获请求、帮助开发者查看、分析、更可以通过自定义脚本进行二次开发,修改爬虫行为*; + +## 2. 配置 Mitmproxy + +#### 2.1 安装 mitmproxy +``` +brew install mitmproxy +``` +安装好后检查安装情况。 +``` +mitmdump --version +``` +得到下图所示内容,表示安装成功。 + +![安装成功后的界面]({{site.baseurl}}/img/mitm-1.jpg) + +#### 2.2. 启动 mitmproxy + +``` +mitmweb # mitmproxy有三种启动方式,此处使用的命令可以提供一个web交互界面 +``` +>【提示】 +**mitmproxy有三种启动命令:** +(1) mitmweb +-- 提供一个web界面; +-- 代理端口:绑定了 *:8080作为代理端口; +-- 交互界面地址:localhost:8081; +(2) mitmproxy +-- 提供命令行界面; +-- 可以通过命令过滤请求; +(3) mitmdump +-- 【TODO】 + +得到下图,表示启动成功,端口号默认为 8080。 +![mitmproxy服务器绑定了 *:8080作为代理端口]({{site.baseurl}}/img/mitm-2.jpg) + +此时,浏览器自动打开下图页面,这是 mitmproxy 提供的 web 交互界面。 +![上图为mitmproxy自动打开的web交互界面]({{site.baseurl}}/img/mitm-3.jpg) + +#### 2.3. 安装CA证书 + +第一步,将电脑和手机连到同一个 WiFi 中; + +第二步,获取本机的内网IP地址; +``` +ifconfig +``` +得到电脑本机的内网IP地址为 192.168.1.102。 +![WeChatd8d30030a2bf45bb1071f0f136698471.png]({{site.baseurl}}/img/mitm-4.jpg) +第三步,给手机WiFi配置代理 +- 服务器地址为电脑内网IP地址,端口为8080 + ![服务器地址为电脑内网IP地址,端口为8080]({{site.baseurl}}/img/mitm-5.jpg) + 第四步,安装证书 + +使用手机浏览器访问 mitm.it,得到下图。 + +![选择自己的手机系统]({{site.baseurl}}/img/mitm-6.jpg) + +我的手机为iPhone,点击 Apple 后得到下图。 + +![31543663396_.pic.jpg]({{site.baseurl}}/img/mitm-7.jpg) + +点击允许,开始安装,安装完成后,得到已验证的提示。 + +![61543663399_.pic.jpg]({{site.baseurl}}/img/mitm-8.jpg) + +#### 2.4. 开启证书 + +手机依次点击:**设置** -> **通用** -> **关于本机** -> **证书信任设置**,开启 mitmproxy 证书。 + +![微信图片编辑_20200207211352.jpg]({{site.baseurl}}/img/mitm-9.jpg) + +> **注意**: +>- 有小伙伴反馈,安卓 7.0 以上版本不再信任证书,需要将证书安装到 root 路径下,这导致数据抓包不成功、手机应用网络不通。 +>- 博主还没有做过研究,想提示一下安卓 7.0 用户如果遇到这类问题,可以尝试使用模拟器降低安卓版本,看是否能解决问题,有时间的话麻烦把结果给博主反馈一下,谢谢! + +#### 2.5 配置完成 + +此时,mitmweb 页面出现下图内容,红框中的为 mitmproxy 抓取的手机的请求。 +![红框中的为 mitmproxy 抓取的手机的请求]({{site.baseurl}}/img/mitm-10.jpg) + +到这里,就成功完成了 mitmproxy配置。 + +## 3. mitmproxy 获取APP数据 + +#### 3.1. 编辑 script.py 文件 + +- script.py 是用来处理 mitmproxy 获取到的 request 和 response 的 .py 脚本; +- 用户根据业务需求,在该文件中 筛选、处理 request 和 response ; +- 本文模拟的获取 **京东APP** 数据,需要筛选出 url 包含 **jd** 字符的 请求和响应; + + ``` + import json + from urllib.parse import unquote + import re + + def response(flow): + # 提取请求的 url 地址 + request_url = flow.request.url + # 通过 jd 字符串,过滤出 京东APP 的请求和返回数据 + if bool(re.search(r"jd", request_url)): + print("request_url >>> ", request_url) + response_body = flow.response.text + response_url = flow.request.url + print("response_url >>> ", response_url) + data = json.loads(response_body) + ware_infos = data.get("wareInfo") + goods_info = {} + if ware_infos is not None: + for ware_info in ware_infos: + goods_info["wareId"] = ware_info.get("wareId") + goods_info["wname"] = ware_info.get("wname") + goods_info["jdPrice"] = ware_info.get("jdPrice") + goods_info["goodrate"] = ware_info.get("good") + goods_info["reviews"] = ware_info.get("reviews") + goods_info["shopId"] = ware_info.get("shopId") + goods_info["ShopName"] = ware_info.get("goodShop").get("goodShopName") + print(goods_info) + + ``` + +#### 3.2. 运行 script.py 文件 + +- 进入 script.py 文件所在文件夹 + ``` + cd /XXX # 进入 script.py 文件所在文件夹 + mitmdump -s script.py + ``` + +#### 3.3. 启动 京东APP,抓取数据 + +- 启动 APP + 启动 京东APP 后,命令行窗口得到下图所示返回信息,数据获取成功。 + ![image.png]({{site.baseurl}}/img/mitm-11.jpg) diff --git "a/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232\345\244\204\347\220\206 AndroidKiller \345\217\215\347\274\226\350\257\221\345\244\261\350\264\245\351\227\256\351\242\230.md" "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232\345\244\204\347\220\206 AndroidKiller \345\217\215\347\274\226\350\257\221\345\244\261\350\264\245\351\227\256\351\242\230.md" new file mode 100644 index 00000000000..084318fd059 --- /dev/null +++ "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232\345\244\204\347\220\206 AndroidKiller \345\217\215\347\274\226\350\257\221\345\244\261\350\264\245\351\227\256\351\242\230.md" @@ -0,0 +1,94 @@ +--- +layout: post +title: 安卓逆向:处理 AndroidKiller 反编译失败问题 +subtitle: 报错内容:AndroidKiller 反编译失败,无法继续下一步源码反编译! +date: 2022-02-07 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 安卓逆向 +--- + + +## 1. 问题 + +- 使用 Android Killer 进行 apk 文件反编译时,遇到“反编译失败,无法继续下一步源码反编译!” 报错。 + +> **报错内容:** +>当前 Apktool 使用版本:android 2.4.1 +正在反编译 APK,请稍等... +>I: Using Apktool 2.4.1 on chelaile_app.apk +>I: Loading resource table... +>I: Decoding AndroidManifest.xml with resources... +>I: Loading resource table from file: C:\Users\Administrator\AppData\Local\apktool\framework\1.apk +>I: Regular manifest package... +>I: Decoding file-resources... +>I: Decoding values */* XMLs... +>I: Baksmaling classes.dex... +>I: Baksmaling classes2.dex... +>I: Baksmaling classes3.dex... +>I: Baksmaling classes4.dex... +>I: Baksmaling assets/A3AEECD8.dex... +>Exception in thread "main" org.jf.dexlib2.dexbacked.DexBackedDexFile$NotADexFile: Not a valid dex magic value: cf 77 4c c7 9b 21 01 cd +> at org.jf.dexlib2.util.DexUtil.verifyDexHeader(DexUtil.java:93) +> at org.jf.dexlib2.dexbacked.DexBackedDexFile.getVersion(DexBackedDexFile.java:111) +> at org.jf.dexlib2.dexbacked.DexBackedDexFile.(DexBackedDexFile.java:78) +> at org.jf.dexlib2.dexbacked.DexBackedDexFile.(DexBackedDexFile.java:138) +> at org.jf.dexlib2.dexbacked.ZipDexContainer$1.getDexFile(ZipDexContainer.java:181) +> at brut.androlib.src.SmaliDecoder.decode(SmaliDecoder.java:90) +> at brut.androlib.src.SmaliDecoder.decode(SmaliDecoder.java:39) +> at brut.androlib.Androlib.decodeSourcesSmali(Androlib.java:96) +> at brut.androlib.ApkDecoder.decode(ApkDecoder.java:164) +> at brut.apktool.Main.cmdDecode(Main.java:170) +> at brut.apktool.Main.main(Main.java:76) +>APK 反编译失败,无法继续下一步源码反编译! + + +## 2. 更新 APKTOOL + +- 下载地址:[https://connortumbleson.com/apktool/](https://connortumbleson.com/apktool/),此处下载最新版本 **apktool_2.4.1.jar**; + +>***注意:如果 APKTOOL 已经是最新版本,则直接跳到第三步!*** + +## 3. Frida安装配置 + +#### 电脑安装 frida & frida-tools + +``` +pip install frida +pip install frida-tools +``` + +> 注意:frida 版本与 python 版本之间的对应关系,可以参考:[https://pypi.org/project/frida/#files](https://pypi.org/project/frida/#files)。 + +#### 下载 frida-server + +- 下载地址:[https://github.com/frida/frida/releases](https://github.com/frida/frida/releases)。 + +#### 手机运行 frida-server + +``` +adb push frida-server-15.1.14-android-x86 /data/local/tmp/ + +adb shell + +cd /data/local/tmp + +# 确定手机当前用户是root用户或拥有root权限 +chmod 777 frida-server-15.1.14-android-x86 + +./frida-server-15.1.14-android-x86 + +``` + +#### 电脑上运行 + +``` +frida-ps -U +``` + +- 如果运行成功,命令行窗口会打印出手机上正在运行的程序。 + + + diff --git "a/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232\345\244\234\347\245\236\346\250\241\346\213\237\345\231\250+JustTrustMe+Xposed\350\267\263\350\277\207SSL Pinning\346\243\200\346\265\213.md" "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232\345\244\234\347\245\236\346\250\241\346\213\237\345\231\250+JustTrustMe+Xposed\350\267\263\350\277\207SSL Pinning\346\243\200\346\265\213.md" new file mode 100644 index 00000000000..6b576bc7bc0 --- /dev/null +++ "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232\345\244\234\347\245\236\346\250\241\346\213\237\345\231\250+JustTrustMe+Xposed\350\267\263\350\277\207SSL Pinning\346\243\200\346\265\213.md" @@ -0,0 +1,54 @@ +--- +layout: post +title: 安卓逆向:夜神模拟器+JustTrustMe+Xposed跳过SSL Pinning检测 +subtitle: +date: 2022-02-07 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 安卓逆向 +--- + +# Android SSL pinning + +#### SSL 协议 + +SSL(安全套接字层)又被称为TLS(数据层安全协议),是一种为网络通信提供数据完整性的一种安全协议。它位于TCP/IP协议与各种应用协议之间。SSL协议主要分为两个部分:Handshake Protocol和Record Protocol。 + +Handshake protocol是用来协商加密通信数据的密钥,Record Protocol定义传输内容的格式。想了解handshake Protocol以及Record protocol的具体细节可以自己去查阅资料,这里就不详细介绍了。 + +在SSL/TLS通信中,客户端通过数字证书判断服务器是否可信,并采用证书的公钥与服务器进行加密通信。然而,在开发者在代码中不检查服务器证书的有效性,或选择接受所有的证书时,这种做法可能导致的问题是中间人攻击。 + +#### Android SSL Certificate Pinning + +SSL Pinning技术指的是在应用程序中只信任固定证书或是公钥。应用程序开发人员在程序中使用SSL pinning技术作为应用流量的附加安全层。 + +通常我们在测试一个应用的时候会设置代理,从而获取应用在运行过程中的流量。我们使用代理工具获取流量的话,需要在设备中安装我们自己的证书到可信任的根证书中,这样应用程序才会将我们的证书视为可信任的有效证书,允许代理工具拦截应用的流量。 + +但是使用的SSL pinning技术的应用程序,只信任指定的证书,那么我们就算把我们的证书安装到设备中,应用程序也不会信任我们的证书,这样的话我们就不能通过代理的方式来拦截应用的流量了。 + +想要绕过SSL pinning的话,只需要将证书校验的过程绕过就可以。那么我们可以通过hook的方式来修改证书校验过程,这样就能成功的绕过SSL pinning了。比较常见的hook工具有两种,分别是frida和Xposed,这两种工具中都有hook所有https证书校验方法的模块。 + +# 安装模拟器 + Xposed + JustTrustMe + +此处使用早期的夜神模拟器,[点此下载 nox_setup_v5.1.0.0_android_4.4.2](https://pan.baidu.com/s/1LWnsnvH8h41Jp9KHuIP7rg?pwd=mv00)。 + +#### 安装 Xposed 并更新 + +[点击此处下载 de.robv.android.xposed.installer_v33_36570c.apk](https://pan.baidu.com/s/1fsPidu_IjP-fS1d0OZP7iw?pwd=v7tm),下载成功够直接拖进模拟器安装。 + +下载后直接模拟器安装,安装成功后依次打开 `Xposed Installer > 框架 > 安装/更新`,之后 `重启模拟器`,重新打开 `Xposed Installer`,此时发现 `激活` 下方连个数字已经变 `绿色`,表示安装成功。 + +![安装成功后的界面]({{site.baseurl}}/img-post/xposed-installler.png) + +>注意:在安装好 Xposed Installer 以后,就要完成更新,否则安装 JustTrustMe 会失败! + +#### 安装 JustTrustMe + +[点击此处下载 JustTrustMe.apk](https://pan.baidu.com/s/1lB7ON43SHXUQ-3yTS1mq7g?pwd=kin2),下载成功后直接拖进模拟器进行安装。 + +安装好以后启动 Xposed点击 `模块`,进入以后勾选JustTrustMe尾部的复选框,即完成了配置。 + +![配置成功后的界面]({{site.baseurl}}/img-post/just-trus-me.png) + diff --git "a/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232\345\256\211\345\215\223\351\200\206\345\220\221\347\216\257\345\242\203\346\220\255\345\273\272\345\217\212\345\267\245\345\205\267\345\256\211\350\243\205.md" "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232\345\256\211\345\215\223\351\200\206\345\220\221\347\216\257\345\242\203\346\220\255\345\273\272\345\217\212\345\267\245\345\205\267\345\256\211\350\243\205.md" new file mode 100644 index 00000000000..99cad9ad127 --- /dev/null +++ "b/_posts/2022-10-27-\345\256\211\345\215\223\351\200\206\345\220\221\357\274\232\345\256\211\345\215\223\351\200\206\345\220\221\347\216\257\345\242\203\346\220\255\345\273\272\345\217\212\345\267\245\345\205\267\345\256\211\350\243\205.md" @@ -0,0 +1,57 @@ +--- +layout: post +title: 安卓逆向:安卓逆向环境搭建及工具安装 +subtitle: +date: 2022-02-07 +author: dex0423 +header-img: img/post-bg-universe.jpg +catalog: true +tags: + - 安卓逆向 +--- + + + +#### 1. 安卓手机配置 + +- 推荐使用谷歌原生的 Nexus 6P,刷机程序自行参考,也可以直接到淘宝购买已经刷机的设备。 + +#### 2. Android Studio 下载 + +- 下载过程此处略过,注意安装好以后,首次启动时需要配置代理,具体代理怎么配置请自行百度。 + +#### 3. adb 工具 + +将 `Android\Sdk\platform-tools` 路径添加到环境变量,之后打开 cmd 命令行工具,输入 `adb devices` 即可查看连接的设备。 +``` +C:\Users\pando>adb devices +List of devices attached +emulator-5554 device +``` + +#### 4. JAVA 安装 + + - JAVA 安装步骤自行百度,安装完以后 cmd 输入 `java -version` 得到下面的提示,表示安装成功。 +``` +C:\Users\pando>java -version +java version "17.0.2" 2022-01-18 LTS +Java(TM) SE Runtime Environment (build 17.0.2+8-LTS-86) +Java HotSpot(TM) 64-Bit Server VM (build 17.0.2+8-LTS-86, mixed mode, sharing) +``` + +#### 5. 反编译工具 apktool + +- 访问[https://ibotpeaches.github.io/Apktool/](https://ibotpeaches.github.io/Apktool/),下载 apktool.jar; +- 访问 [https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/windows/apktool.bat](https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/windows/apktool.bat),得到的文本另存为 `apktool.bat` 文件,和 apktool.jar 保存到同一个目录中; +- 添加文件夹路径到系统环境变量 PATH 中,apktool 即可生效。 + +#### 6. JADX + +- 访问本站[https://dex0423.github.io/resources/](https://dex0423.github.io/resources/),即可下载。 +- 也可访问[https://github.com/skylot/jadx/tree/v1.0.0](https://github.com/skylot/jadx/tree/v1.0.0)下载最新版。 + +#### 7. JEB + +- 访问本站[https://dex0423.github.io/resources/](https://dex0423.github.io/resources/),即可下载(注意参考提示的安装以及使用方法)。 + + diff --git a/_posts/img.png b/_posts/img.png new file mode 100644 index 00000000000..04dba79205c Binary files /dev/null and b/_posts/img.png differ diff --git a/_posts/img_1.png b/_posts/img_1.png new file mode 100644 index 00000000000..a0858ee0df1 Binary files /dev/null and b/_posts/img_1.png differ diff --git a/_posts/img_2.png b/_posts/img_2.png new file mode 100644 index 00000000000..05639a8fd15 Binary files /dev/null and b/_posts/img_2.png differ diff --git a/3-about.html b/about.html similarity index 63% rename from 3-about.html rename to about.html index 14d843f5862..0f2fd35165b 100644 --- a/3-about.html +++ b/about.html @@ -1,8 +1,8 @@ --- layout: page title: "About" -description: "为了让更多中国人能融入国际语运动而作" -header-img: "img/post-bg-rwd.jpg" +description: "一个野生程序员的碎碎念。" +header-img: "img/tag-bg.jpg" --- @@ -10,28 +10,33 @@ --> -
    -

    冰冻三尺 非一日之寒
    - 积土成山 非斯须之作

    +
    +

    + 座右铭:好记性,不如烂笔头。 + +

    +
    + +

    尊敬的读者,你好!

    + +

    我是dex0423,一个非计算机专业出身的程序员。

    + +

    之所以建立这个博客站,是为了用来记录自己在工作、学习中的收获,同时也在这里分享出来和各位一起交流探讨。

    -

    Hi,我是Dr. Henri Jambo,现旅居世界上著名的多元文化城市——蒙特利尔。在魁北克,我们不仅感受到英语、法语的强烈冲突,甚至也能感受到阿拉伯语、中文等多种语言的相互碰撞。

    +

    这里记录的文章内容比较杂,包含了Python、爬虫、JS逆向、安卓逆向、数据库、数仓、数据湖、BI、数据治理、用户画像等。

    -

    同时我深知中国也许是世界上英语学习人均投入最多的国家,但中国人学好英语的不多,社会资源浪费很大。

    - -

    为了让全世界都有一门共同的、简单的外语可以学习,降低外语学习的成本并让大家相互沟通更容易,我设计了这门中西融合的人造国际语。

    - -

    她是对世界语和其他国际语的传承,也希望她能超越世界语。

    - -

    希望更多的人加入到这个社群中,共同成长。这是新的希望!

    +

    因为技术水平有限,文章不免有错漏的地方,欢迎各位读者批评指正。

    -

    本网站为三叶语官方网站。您也可关注知乎专栏👉人造国际语。如果您有什么建议,欢迎提出探讨~

    +

    我的邮箱地址是 pandong423@icloud.com,欢迎来信!

    + +

    - +
    @@ -42,7 +47,7 @@

    Hi, I am Baiying Qiu,you can call me BY. I am an iOS software engineer and currently working in Xiamen

    This is my personal blog, through making Github Pages and Jekyll.My GitHub 👉 Github·BY.

    - +

    I am a sports enthusiast, I like fitness, running and boxing.

    Talks
    @@ -54,9 +59,8 @@
    Talks
  • BY
  • --> - ---> -{% if site.gitalk.enable %} + - + - - -{% if site.disqus.enable %} +Gitalk end --> + + -
    -
    -
    -
    - - - --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endif %} +