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

背景

当知道要上传的视频资料从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 条评论
登录 后参与评论

相关文章

来自专栏花叔的专栏

解读10.13发布的小程序新功能

距离上次更新已经有一个月了,小程序终于又更新了,但其实所更新的内容并不太多,这有点违背微信团队的快速迭代的习惯,难道在酝酿更大的迭代吧?嘿嘿~~~ 回归正题,先...

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

【计算机基本概念】中央处理器

中央处理器(CPU,Central Processing Unit)是一块超大规模的集成电路,是一台计算机的运算核心(Core)和控制核心( Control U...

3365
来自专栏腾讯移动品质中心TMQ的专栏

测试建模兵器谱

0.引子 有人的地方就有江湖,有测试的地方就有建模。 每个产品都是一片江湖,每一次迭代就是一场武林大会,而一个个的需求,就是一封封战书。 测试同学在面对复杂的...

2516
来自专栏SDNLAB

OpenFlow 1.3 学习笔记

因为7、8月我跟小伙伴们在备战今年的全国大学生SDN大赛,9月在全身心投入准备特殊人才保研的专家答辩,所以好久没有新文章跟大家见面啦。现在一切尘埃落定,继续卯足...

3497
来自专栏.NET技术

整理自己的.net工具库

  今天我会把自己平日整理的工具库给开放出来,提供给有需要的朋友,如果有朋友平常也在积累欢迎提意见,我会乐意采纳并补充完整。按照惯例在文章结尾给出地址^_^。

562
来自专栏WeTest质量开放平台团队的专栏

浅谈软件工程师的代码素养

“程序是写给人读的,只是偶尔让计算机执行一下。” ——Donald Ervin Knuth(高德纳)

57313
来自专栏前端工程

浅谈前端/软件工程师的代码素养

“程序是写给人读的,只是偶尔让计算机执行一下。”

1856
来自专栏架构师之路

一分钟了解两阶段提交协议/算法(分布式理论基础)

两阶段提交协议/算法(2PC) 概念 二阶段提交2PC(Two phase Commit)是指,在分布式系统里,为了保证所有节点在进行事务提交时保持一致性的一种...

3745
来自专栏程序人生

分布式系统中的监工:Overseer

最近从无趣的工作中发现了有趣的事情,工作和业余时间都扑了些精力上去,本待上周末最终的成果出来后再写文章的,无奈事情太多,代码还没写完,二月上旬已过,再不写文章春...

3157
来自专栏程序员的诗和远方

20180728_ARTS_week05

这题有点犯难,上面是 Discuss 中的一个解法,看了之后挺好理解的,要找回环字符串,就从 a 和 aa 一阶和二阶这两种形式往两边找,感觉特别巧妙。

852

扫码关注云+社区