【趣味连载】攻城狮上传视频与普通人上传视频:(一)生成结构化数据

背景

当知道要上传的视频资料从20条变成100条时,我就明白,绝对不能再人工处理了。他们总是想当然的认为,录入一条数据需要1分钟,那录入20条数据就是20分钟,录入100条数据,不就是100分钟吗?我有时候,真的很想问问他们,没有考虑过人是会犯错的吗?数据越多,出错的可能就越大;但是数据本身,又是不允许出现纰漏的。那拿什么去保证数据的正确性?刷脸?可能吗?

大多数时候,类似的争论,最终几乎总是会以他们的一句“我不懂技术,你们看着办吧”结束。所以,也懒得去做口舌之争。我尽力尽快做;但是你承不承认事情本身的复杂度,并不会影响事情本身的复杂度。

回到问题本身,究竟如何处理新到来的100条数据以及以后更多的数据,确实是一个必须想办法彻底解决下的问题。

我拿到的原始数据

此处适当象征性的描述下我拿到的数据。以下讨论,单以 10 条数据为例。

一个 word 文档,是一组问题。

内容假定是:

1.【smart-transform】取自 Atom 的 babeljs&coffeescript&typescript 智能转 es5 库
2.【YFMemoryLeakDetector】人人都能理解的 iOS 内存泄露检测工具类
3.【玩转树莓派】使用 sinopia 搭建私有 npm 服务器
4.【小技巧解决大问题】使用 frp 突破阿里云主机无弹性公网 IP 不能用作 Web 服务器的限制
5.【树莓派自动化应用实例】整点提醒自己休息五分钟
6. 借助 frp 随时随地访问自己的树莓派
7.【LuaJIT版】从零开始在 macOS 上配置 Lua 开发环境
8.【最新版】从零开始在 macOS 上配置 Lua 开发环境
9. 关于混合应用开发的未来的一些思考
10.记录我发现的第一个关于 Google 的 Bug

是的,内容中还有各种中文标点。他们有相当一部分人不理解攻城狮为什么喜欢用英文标点,甚至还有人以此为由说我们小学标点符号没学好。懒得解释那么多,但是既然给出来了,作为纯文本,也不用管这么多,照单全收就行了。符号习惯问题本身,也是一个无伤大雅的问题。

另一个 word 文档,是一组问题对应的 Luis 语义分析结果

微软的 Luis 语义分析服务,勉强算是和人工智能沾点边吧,感兴趣的请自行了解下。从客户端角度来说,你给它一个文本字符串,他们分析出来和这个字符串匹配度最高的某个预录入的答案的唯一标记。每个唯一标记 ID,被称作一个 intent。每次请求,最多只有一个匹配度最高的 intent。

感觉已经有的 word 问题,我们的后端小伙伴,送来了另一个 word 文档:

1. smart_transform
2. memory_leakDetector
3. sinopia_npm
4. frp_ip
5. tip_rest
6. frp_anywhere
7. luajit_macos
8. lua_macos
9. app_future
10. google_bug

又是非结构化的数据。显而易见,我们可爱的后端同学,只是简单完成了录入,自己没有做必要的单元测试。这是在等着我去发现问题啊。很久很久以前,我总是幻想着,所有的攻城狮,必然都是各种自动化测试用例,就像树上写的各种敏捷,各种快速迭代。事实上,我见到的许多所谓的敏捷式开发,最终其实只是把成本后置,各种技术债。出来混,真的迟早是要换的。100个问题,逐一去验证,真的是很耗费时间的,而且最终有问题的,数量也不会太多。也就说说,如果手动去做,很有可能寻找问题的时间,要远远大于发现问题的时间。所以,自动化批量测试,是显而易见的。根据不同的场景和需要,快速构建基本够用的批量自动化测试工具链,应该成为每个攻城狮的必修课。

一组勉强算是有规律的分文件夹放置的视频

我依然是象征性的描述下,结构类似于:

/videos/树莓派/【smart-transform】取自 Atom 的 babeljs&coffeescript&typescript 智能转 es5 库.mp4
/videos/树莓派/【YFMemoryLeakDetector】人人都能理解的 iOS 内存泄露检测工具类.mp4
/videos/树莓派/【玩转树莓派】使用 sinopia 搭建私有 npm 服务器.mp4
/videos/树莓派/【小技巧解决大问题】使用 frp 突破阿里云主机无弹性公网 IP 不能用作 Web 服务器的限制.mp4
/videos/frp/【树莓派自动化应用实例】整点提醒自己休息五分钟.mp4
/videos/frp/借助 frp 随时随地访问自己的树莓派.mp4
/videos/Lua/【LuaJIT版】从零开始在 macOS 上配置 Lua 开发环境.mp4
/videos/Lua/【最新版】从零开始在 macOS 上配置 Lua 开发环境.mp4
/videos/Lua/关于混合应用开发的未来的一些思考.mp4
/videos/Lua/记录我发现的第一个关于 Google 的 Bug.mp4

目标数据要求

intent 必须和问题关联起来

显而易见,应该使用 intent 作为数据的唯一 id。为了便于处理,索性写成了一个 JS 模块。之所以不直接用 JSON,是因为模块比 JSON 文件,更灵活性,后期扩展方便,如果有的话。

这一步是必须手动做的,或者说总是需要有一个人手动去做的。为了效率,团队内总是需要有一个人必须要充当这个角色。

大致处理下,初版结构 intent_info.js 大概类似这样:

module.exports = {
  /* 树莓派 */
  "smart_transform":"【smart-transform】取自 Atom 的 babeljs&coffeescript&typescript 智能转 es5 库",
  "memory_leakDetector":"【YFMemoryLeakDetector】人人都能理解的 iOS 内存泄露检测工具类",
  "sinopia_npm":"【玩转树莓派】使用 sinopia 搭建私有 npm 服务器",
  "frp_ip":"【小技巧解决大问题】使用 frp 突破阿里云主机无弹性公网 IP 不能用作 Web 服务器的限制",
  /* frp */
  "tip_rest":"【树莓派自动化应用实例】整点提醒自己休息五分钟",
  "frp_anywhere":"借助 frp 随时随地访问自己的树莓派",
  /* Lua */
  "luajit_macos":"【LuaJIT版】从零开始在 macOS 上配置 Lua 开发环境",
  "lua_macos":"【最新版】从零开始在 macOS 上配置 Lua 开发环境",
  "app_future":"关于混合应用开发的未来的一些思考",
  "google_bug":"记录我发现的第一个关于 Google 的 Bug",
}

排序

排序,是需要增加一个新的字段 order。不过,我就直接上面的类似 JSON 的结构来排序的。因为排序是由另外一个人做,懂技术,操作很简单些。

经过对方排序后,intent_info.js,可能变成了这样:

module.exports = {
  /* 树莓派 */
  "smart_transform":"【smart-transform】取自 Atom 的 babeljs&coffeescript&typescript 智能转 es5 库",
  "memory_leakDetector":"【YFMemoryLeakDetector】人人都能理解的 iOS 内存泄露检测工具类",
  "sinopia_npm":"【玩转树莓派】使用 sinopia 搭建私有 npm 服务器",
  "frp_ip":"【小技巧解决大问题】使用 frp 突破阿里云主机无弹性公网 IP 不能用作 Web 服务器的限制",
  /* Lua */
  "luajit_macos":"【LuaJIT版】从零开始在 macOS 上配置 Lua 开发环境",
  "lua_macos":"【最新版】从零开始在 macOS 上配置 Lua 开发环境",
  "app_future":"关于混合应用开发的未来的一些思考",
  "google_bug":"记录我发现的第一个关于 Google 的 Bug",
  /* frp */
  "tip_rest":"【树莓派自动化应用实例】整点提醒自己休息五分钟",
  "frp_anywhere":"借助 frp 随时随地访问自己的树莓派",
}

在上面的优先显示。在真正生成 order 字段时,是借助 Node 一个不太可靠的特性: 字典遍历时,会基于key的书写顺序来遍历。这一点,在 Node 和 Android 浏览器上都是成立的,在 safari 上,无效。一般开发时,不应依赖于这一点,不过目前,我只是需要一个够用的东西。Node 的这个特性,在短时间内,应该是不会有改变的。

分类

没过几天,果然又加了新需求,说是视频太多了,太杂乱,想给每个视频加个分类,然后可以按分类查看视频。

好,那我给你加个分类:

module.exports = {
  /* 树莓派 */
  "树莓派":"_category",
  "smart_transform":"【smart-transform】取自 Atom 的 babeljs&coffeescript&typescript 智能转 es5 库",
  "memory_leakDetector":"【YFMemoryLeakDetector】人人都能理解的 iOS 内存泄露检测工具类",
  "sinopia_npm":"【玩转树莓派】使用 sinopia 搭建私有 npm 服务器",
  "frp_ip":"【小技巧解决大问题】使用 frp 突破阿里云主机无弹性公网 IP 不能用作 Web 服务器的限制",
  /* Lua */
  "Lua":"_category",
  "luajit_macos":"【LuaJIT版】从零开始在 macOS 上配置 Lua 开发环境",
  "lua_macos":"【最新版】从零开始在 macOS 上配置 Lua 开发环境",
  "app_future":"关于混合应用开发的未来的一些思考",
  "google_bug":"记录我发现的第一个关于 Google 的 Bug",
  /* frp */
  "frp":"_category",
  "tip_rest":"【树莓派自动化应用实例】整点提醒自己休息五分钟",
  "frp_anywhere":"借助 frp 随时随地访问自己的树莓派",
}

新加了几个值为 **_category** 的字段。当检测到值为 **_category** 时,就自动判定为是一个分类。我这种处理方式,免不了引来一阵唏嘘。但是,许多时候,你选择的技术策略,都必须根据项目所处的状态和各种条件,去综合权衡。我只有几十分钟时间去重新规划和整理100条数据。可能真的没法想太多。需求总是变化的,不知道明天又会变成什么样,可能再进一步,就变成”过度设计“了。另外,项目本身, intent 本身约定了自己特有命名规律,是可以安全认为 intent 和 分类一定不会重复的。

问题和视频关联

在读取 intent_info.js 中的足够可信的结构化数据后,我会动态建立问题和视频的关联。这个过程中,可能需要适当修改问题和视频的标题。为了避免遗漏,一个标题,如果没有对应的视频或对应多个视频,就直接crash。有些霸道,但总比后期一个一个比对排查,省太多事了。结合问题和视频标题的特点,我专门封装了一个方法:

/* 获取某个标题对应的本地路径.
为了避免未知错误,如果找不到或找到多个,就直接 crash.

@return  本地视频的相对路径.
 */
function localVideoPath(title)
{
  let path = require("path")
  let fs = require ('fs-plus')
  let fse = require('fs-extra')
  let os = require("os")
  let {execSync} = require("child_process")

  let videoDir = path.resolve(__dirname,"./videos")

  let videos = fs.listTreeSync(videoDir)
                  .filter(item=>{
                    return [".mov",".mp4"].includes(path.extname(item))
                  })
                  .map(item=>{
                    return path.relative(__dirname,item)
                  })

  /* 一个标题,能且只能对应一个视频,否则就抛出异常. */
  let localVideoPath = null

  for (let item of videos) {
    if (item.includes(title)) {
      if (localVideoPath) {
        const tip = `致命异常: ${title} 对应的视频重复:
        ${localVideoPath}
        ${item}`

        throw new Error(tip)
      }

      localVideoPath = item
    }
  }

  if (!localVideoPath) {
    const tip = `致命异常!这个标题竟然没有对应的视频:\n${title}`

    throw new Error(tip)
  }

  return localVideoPath
}

见码如唔

完整的自动化处理成结构数据的逻辑如下,都集中在 make_data.js 中。

/* 生成带有排序等信息的文件. */

/* 支持自动生成数据. */
makeDataWithOrder()
function makeDataWithOrder()
{
  const fs = require('fs-extra')
  const path = require('path')

  const intentInfo = require("./intent_info.js")

  let intentInfoNew = []
  let index = 1

  /* 在node中遍历时,key的顺序是和原始key的顺序对应的.
  这个特性,并不总是有效,比如在 ios 浏览器中.
  目前,仅仅是够用. */
  let category = ""
  for (let intent in intentInfo) {
    if (intentInfo[intent] == "_category") { /* 说明是一个分类标记. */
      category = intent
      continue
    }
    let title = intentInfo[intent]
    const local_path = localVideoPath(title)
    intentInfoNew.push({
      "type":"video",
      "content":"",
      "intent": intent,
      "title": title,
      "order": index,
      "local_video_path": local_path,
      "ext": path.extname(local_path),
      "category":category,
    })

    ++ index
  }

  localVideoLoseCheck(intentInfoNew)
  const dataPath = path.resolve(__dirname, "./data.json")
  fs.writeJsonSync(dataPath, intentInfoNew)
  console.log(`恭喜!数据已写入 ${dataPath}`)
}

/* 确保视频总数与intent总数是对应的,防止有视频遗漏.
有视频没有对应问题时,会直接抛出异常.
 */
function localVideoLoseCheck(intents)
{
  /* 先把视频信息处理成 key-value. */
  let path = require("path")
  let fs = require ('fs-plus')
  let fse = require('fs-extra')
  let os = require("os")
  let {execSync} = require("child_process")

  let videoDir = path.resolve(__dirname,"./videos")
  let videoDict = fs.listTreeSync(videoDir)
                  .filter(item=>{
                    return [".mov",".mp4"].includes(path.extname(item))
                  })
                  .map(item=>{
                    return path.relative(__dirname,item)
                  })
                  .reduce((sum,item,idx)=>{
                    sum[item] = false
                    return sum
                  },{})

  for (let item of intents) {
    videoDict[item.local_video_path] = true
  }

  /* 寻找缺失的. */
  let loses = []
  for (let item in videoDict) {
    if (!videoDict[item]) {
      loses.push(item)
    }
  }

  if (loses.length) {
    const tip = `一下 ${loses.length} 个视频没有对应的问题:
    ${JSON.stringify(loses)}`
    throw new Error(tip)
  }
}

/* 获取某个标题对应的本地路径.
为了避免未知错误,如果找不到或找到多个,就直接 crash.

@return  本地视频的相对路径.
 */
function localVideoPath(title)
{
  let path = require("path")
  let fs = require ('fs-plus')
  let fse = require('fs-extra')
  let os = require("os")
  let {execSync} = require("child_process")

  let videoDir = path.resolve(__dirname,"./videos")

  let videos = fs.listTreeSync(videoDir)
                  .filter(item=>{
                    return [".mov",".mp4"].includes(path.extname(item))
                  })
                  .map(item=>{
                    return path.relative(__dirname,item)
                  })

  /* 一个标题,能且只能对应一个视频,否则就抛出异常. */
  let localVideoPath = null

  for (let item of videos) {
    if (item.includes(title)) {
      if (localVideoPath) {
        const tip = `致命异常: ${title} 对应的视频重复:
        ${localVideoPath}
        ${item}`

        throw new Error(tip)
      }

      localVideoPath = item
    }
  }

  if (!localVideoPath) {
    const tip = `致命异常!这个标题竟然没有对应的视频:\n${title}`

    throw new Error(tip)
  }

  return localVideoPath
}

我们在项目目录执行

node ./make_data.js

就可以得到我们想要的结构化的数据:

[
  {
    "type": "video",
    "content": "",
    "intent": "smart_transform",
    "title": "【smart-transform】取自 Atom 的 babeljs:coffeescript:typescript 智能转 es5 库",
    "order": 1,
    "local_video_path": "videos/树莓派/【smart-transform】取自 Atom 的 babeljs:coffeescript:typescript 智能转 es5 库.mp4",
    "ext": ".mp4",
    "category": "树莓派"
  },
  {
    "type": "video",
    "content": "",
    "intent": "memory_leakDetector",
    "title": "【YFMemoryLeakDetector】人人都能理解的 iOS 内存泄露检测工具类",
    "order": 2,
    "local_video_path": "videos/树莓派/【YFMemoryLeakDetector】人人都能理解的 iOS 内存泄露检测工具类.mp4",
    "ext": ".mp4",
    "category": "树莓派"
  },
  {
    "type": "video",
    "content": "",
    "intent": "sinopia_npm",
    "title": "【玩转树莓派】使用 sinopia 搭建私有 npm 服务器",
    "order": 3,
    "local_video_path": "videos/树莓派/【玩转树莓派】使用 sinopia 搭建私有 npm 服务器.mp4",
    "ext": ".mp4",
    "category": "树莓派"
  },
  {
    "type": "video",
    "content": "",
    "intent": "frp_ip",
    "title": "【小技巧解决大问题】使用 frp 突破阿里云主机无弹性公网 IP 不能用作 Web 服务器的限制",
    "order": 4,
    "local_video_path": "videos/树莓派/【小技巧解决大问题】使用 frp 突破阿里云主机无弹性公网 IP 不能用作 Web 服务器的限制.mp4",
    "ext": ".mp4",
    "category": "树莓派"
  },
  {
    "type": "video",
    "content": "",
    "intent": "luajit_macos",
    "title": "【LuaJIT版】从零开始在 macOS 上配置 Lua 开发环境",
    "order": 5,
    "local_video_path": "videos/Lua/【LuaJIT版】从零开始在 macOS 上配置 Lua 开发环境.mp4",
    "ext": ".mp4",
    "category": "Lua"
  },
  {
    "type": "video",
    "content": "",
    "intent": "lua_macos",
    "title": "【最新版】从零开始在 macOS 上配置 Lua 开发环境",
    "order": 6,
    "local_video_path": "videos/Lua/【最新版】从零开始在 macOS 上配置 Lua 开发环境.mp4",
    "ext": ".mp4",
    "category": "Lua"
  },
  {
    "type": "video",
    "content": "",
    "intent": "app_future",
    "title": "关于混合应用开发的未来的一些思考",
    "order": 7,
    "local_video_path": "videos/Lua/关于混合应用开发的未来的一些思考.mp4",
    "ext": ".mp4",
    "category": "Lua"
  },
  {
    "type": "video",
    "content": "",
    "intent": "google_bug",
    "title": "记录我发现的第一个关于 Google 的 Bug",
    "order": 8,
    "local_video_path": "videos/Lua/记录我发现的第一个关于 Google 的 Bug.mp4",
    "ext": ".mp4",
    "category": "Lua"
  },
  {
    "type": "video",
    "content": "",
    "intent": "tip_rest",
    "title": "【树莓派自动化应用实例】整点提醒自己休息五分钟",
    "order": 9,
    "local_video_path": "videos/frp/【树莓派自动化应用实例】整点提醒自己休息五分钟.mp4",
    "ext": ".mp4",
    "category": "frp"
  },
  {
    "type": "video",
    "content": "",
    "intent": "frp_anywhere",
    "title": "借助 frp 随时随地访问自己的树莓派",
    "order": 10,
    "local_video_path": "videos/frp/借助 frp 随时随地访问自己的树莓派.mp4",
    "ext": ".mp4",
    "category": "frp"
  }
]

参考文章

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员宝库

现代前端开发路线图:从零开始,一步步成为前端工程师

编者按:很多人都想学编程。但是苦于没有具体的步骤和指导。比如想找份前端开发的工作,却不知道应该先学习什么再学习什么,也不知道该选择什么样的工具。因为经常被人问到...

1256
来自专栏编程微刊

进阶攻略|前端完整的学习路线

1112
来自专栏Jerry的SAP技术分享

想要阅读一些开源项目或框架的源代码,不知道从哪里入手?

我的建议是从Spring的源代码入手。至少我自己也是这么做的,毕竟Spring里有大量的设计模式和OO编程的最佳实践。我之前在SAP使用SAP自己的开发语言AB...

2039
来自专栏LET

谈谈3D Tiles(3):个人总结

30511
来自专栏JAVA高级架构

有哪些 Java 源代码看了后让你收获很多,代码思维和能力有较大的提升?

最早看的 架构探险 从零开始写Java Web框架,黄勇写的,算是一种启蒙,作者自己写了一套ioc和aop框架以及mvc请求分发框架。 跟着写了一遍,基本明白了...

35210
来自专栏程序员互动联盟

我在苹果公司学到的编程技巧

当我还在苹果在线商店工作的时候,我们从来没有对在线网站做过负载测试。我们也不觉得需要这么做。然而,当每次史蒂夫·乔布斯在演示某个幻灯片过程中切换到在线商店时,会...

31412
来自专栏恰同学骚年

《大型网站技术架构》读书笔记之八:固若金汤之网站的安全性架构

此篇已收录至《大型网站技术架构》读书笔记系列目录贴,点击访问该目录可获取更多内容。

613
来自专栏阮一峰的网络日志

如何降低软件的复杂性?

John Ousterhout 是斯坦福大学计算机系教授,也是 Tcl 语言的创造者。

673
来自专栏跨界架构师

如何一步一步用DDD设计一个电商网站(二)—— 项目架构

    上一篇我们讲了DDD的核心概念(附上链接),并且设计了我们的上下文映射图,那么接下来就准备开始立项了,本篇文章的部分知识点可能对一部分人来说比较基础,可...

701
来自专栏编程微刊

进阶攻略|前端完整的学习路线

1645

扫码关注云+社区