Blog logoJustZht

Blog logo

Google Sheet + Apps Script + Glide

 • 

背 GRE 单词的时候弄的一套自动化流程,可以批量查词修改 Spreadsheet,配合 GlideApp 生成 PWA 在手机上用,效果可以参考这里

1) 导入 Apps Script

这个脚本基本上就两个功能,一个是触发器,负责在修改文档的时候更新那一行的内容,第二个是一些数值函数,可以直接在 Sheet 的 f(x) 框里面用。所有查询的词都做了 lowercase 和 cache,毕竟 UrlFetchApp 有配额一天只能跑上几千次,要省着点用,而且如果用了 f(x) 表达式的话,每次打开页面都请求爱词霸也挺拖累别人服务器的。

appsscript.json

{
  "timeZone": "Asia/Hong_Kong",
  "dependencies": {
  },
  "webapp": {
    "access": "ANYONE",
    "executeAs": "USER_ACCESSING"
  },
  "exceptionLogging": "STACKDRIVER",
  "oauthScopes": ["https://www.googleapis.com/auth/documents", "https://www.googleapis.com/auth/script.external_request", "https://www.googleapis.com/auth/spreadsheets"]
}

code.gs

function onOpen() {
  var ui = SpreadsheetApp.getUi();
  // Or DocumentApp or FormApp.
  ui.createMenu('Words')
      .addItem('Reset Cache', 'ResetCache')
      .addToUi();
}

function onEditInstalledTrigger(e) {
  var range = e.range;
  var sheet = SpreadsheetApp.getActiveSheet();
  
  Logger.log("row = " + range.getRow() + " column = " + range.getColumn);
  Logger.log("row Number = " + range.getNumRows() + " column Number = " + range.getNumColumns());
  
  for (var i = range.getRow(); i <= range.getRow() + range.getNumRows() - 1; i++) {
    for (var j = range.getColumn(); j <= range.getColumn() + range.getNumColumns() - 1; j++) 
    {
      if (j == 1)
      {
        var currentValue = sheet.getRange(i,j).getValue();
        var wordLowCase = GetWordLowerCase(currentValue);
        var empty = IsEmpty(wordLowCase);
        var cache = IsCached(wordLowCase);
        
        Logger.log("row i = " + i + " column j = " + i + " word = " + wordLowCase);
        var attribute = {
          cache:cache,
          value:wordLowCase,
          empty:empty
        }
        sheet.getRange(i,j).setNote(empty ? "" : JSON.stringify(attribute));
        sheet.getRange(i,j+1).setValue(empty ? "" : "LOADING");
        sheet.getRange(i,j+2).setValue(empty ? "" : "LOADING");
        sheet.getRange(i,j+3).setValue(empty ? "" : "LOADING");
        sheet.getRange(i,j+4).setValue(empty ? "" : "LOADING");
        sheet.getRange(i,j+5).setValue(empty ? "" : "LOADING");
        
        var wordJsonObject = empty ? {} : GetWord(wordLowCase);
        attribute.json = wordJsonObject;
        sheet.getRange(i,j).setNote(empty ? "" : JSON.stringify(attribute));
        Logger.log(JSON.stringify(attribute));
        
        if (wordJsonObject.status == 0)
        {
          var wordMeaning = empty ? "" : wordJsonObject.content.word_mean.join("\r\n");
          sheet.getRange(i,j+1).setValue(wordMeaning);
          
          var worldPronunciationUS = empty ? "" : "/" + wordJsonObject.content.ph_am + "/";
          sheet.getRange(i,j+2).setValue(worldPronunciationUS);
          
          var worldPronunciationURLUS = empty ? "" : wordJsonObject.content.ph_am_mp3;
          sheet.getRange(i,j+3).setValue(worldPronunciationURLUS);
          
          var cachedString = empty ? "" : (cache ? "YES" : "NO");
          sheet.getRange(i,j+4).setValue(cachedString);
      
        }
        
        var updateTime = empty ? "" : new Date().toISOString();
        sheet.getRange(i,j+5).setValue(updateTime);
       
      }
    }
  }
}

function ResetCache() {
  var cache = CacheService.getScriptCache();
  var selection = SpreadsheetApp.getActiveSheet();
  var data = selection.getSelection().getActiveRange().getValues()
  for (var i = 0; i < data.length; i++) {
    var wordLowCase = GetWordLowerCase(data[i][0]);
    Logger.log('Cache remove: ' + wordLowCase);
    cache.remove(wordLowCase);
  }
}

function GetWordLowerCase(word)
{
  return String(word).toLowerCase();
}

function GetWordUrl(word)
{
  var wordLowCase = GetWordLowerCase(word);
  var url = "http://fy.iciba.com/ajax.php?a=fy&f=auto&t=zh&w=".concat(encodeURIComponent(wordLowCase));
  return url;
}

function IsCached(word)
{
  if (IsEmpty(word))
  {
    return false;
  }
  var wordLowCase = GetWordLowerCase(word);
  var cache = CacheService.getScriptCache();
  var cached = cache.get(wordLowCase);
  return cached != null;
}

function IsEmpty(str) {
    return (!str || 0 === str.length);
}

/**
 * Get Word JSON Object.
 *
 * @param {word} the word.
 * @return JSON returned from iciba.
 * @customfunction
 */
function GetWord(word) {
  if (IsEmpty(word))
  {
    Logger.log("word == null");
    return {};
  }
  var wordLowCase = GetWordLowerCase(word);
  var cache = CacheService.getScriptCache();
  var cached = cache.get(wordLowCase);
  if (cached != null) {
    return JSON.parse(cached);
  }
  var url = GetWordUrl(wordLowCase);
  var jsonData = UrlFetchApp.fetch(url);
  var jsonContent = jsonData.getContentText();
  cache.put(wordLowCase, jsonContent, 604800); // cache for random minutes
  var object   = JSON.parse(jsonContent);
  return object;
}
/**
 * @customfunction
 */
function GetWordMeaning(word)
{
  var object = GetWord(word);
  return object.content.word_mean.join("\r\n");
}
/**
 * @customfunction
 */
function GetWordPronunciationUS(word)
{
  var object = GetWord(word);
  return object.content.ph_am;
}
/**
 * @customfunction
 */
function GetWordPronunciationUSURL(word)
{
  var object = GetWord(word);
  return object.content.ph_am_mp3;
}
/**
 * @customfunction
 */
function GetWordJSON(word)
{
  var object = GetWord(word);
  return JSON.stringify(object);
}

然后部署下,允许下文件访问

2) 安装 Trigger

表格 - 工具 - 脚本触发器 - 修改 - 当前项目触发器 - 添加:

  • 选择要运行的功能 OnEditInstalledTrigger
  • 选择活动来源 基于电子表格
  • 选择活动来源 编辑时
3) 输入数据

第一行最好 freeze 住,然后在第二行开始输入英文,Trigger 会自动补全后面几列
Screenshot-2019-10-24-at-12.44.30-AM

4) 同步到 Glide 里

Glide 里新建一个项目绑定这个表格。
Glide 比较好的地方在于,他们的 Detail Page 可以设定 Audio 组件绑定 URL,刚好可以对应爱词霸 API 里的音标地址,基本配置完可以当一个自定义单词本来用了。

Screenshot-2019-10-24-at-12.49.27-AM-2
Screenshot-2019-10-24-at-12.50.28-AM

2019.10.21

 • 

Hackathon 入围第一轮了,接下来下个月会去东京参加 Final。自己的作品原本就只是三四天完成的,因此选题和实现上都是典型的求快的方式,在长期改进方面相比其他队伍就会比较被动,加上自己还有语言考试没有结束,只能抽时间做了。
回家休息了一周,心态懒散了不少。
看了 Hayao Miyazaki's AirshipsTrevor Noah: Afraid of the Dark

2019.10.4

 • 

9.21 的托福成绩出来了,连续每两周一考的最后一次,目前看来三次下来分别是 102/107/108,目前四科加起来的 best score 总算是上 110 了,虽然单次考试没过线,但申请总算是有一个大致的保障了,接下来会去考 GRE。

6CD8BD79-EF75-462F-9A8E-BD98688D61D9

顺带一提,浙大的考点是六边形的开放桌子,会比较影响人发挥,还是科大的封闭桌子好用。


国庆也没有出去玩,也没有看电影,而是窝在住处给将要参加的一个 Online Hackathon 写了三四天的 Unity,和 ARKit 相关。ARKit 3 的 Motion Tracking 其实还是挺不稳定的,有的时候极其精确,有的时候又都连叉开的左右脚都分不清。一开始想做成 Unity Editor 的样式跑在 iPad 上,因此买了 Runtime Editor,效果还行,但对移动设备的支持不太好,加上多个 RenderTarget 也挺吃内存的最后就放弃了,直接用 UGUI 做 World Space 的 3D UI。

iPad 的效果
算下来也是这几个月第一次花了全天的时间在写代码了,GitHub 上也少有地出现了绿格,虽然过几天还有 GRE 考试因此心里还是挺担心因为光顾着代码没时间准备考试,但总的来说还是挺高兴的,有点类似于阅兵一样,算是在检验自己的快速出原型的能力。


之前趁着降价买了 XCOM,而且鬼使神差地 Google Play 和 App Store 上都买了,结果 Easy 模式对于我来说都有点够呛,中期经常全员覆灭,到中后期打完神秘组织后靠着机器坦克慢慢缓过来,又觉得没有挑战了。不过总归来说这个游戏太让人入迷了,因此最后被我被迫删了。
之前貌似又玩了 EVE 手游版的 testflight,但是后来貌似测试服务器关了,因此也删了。总之貌似还在准备语言考试的阶段,因此自己也没办法过的太懒散。

Unity 实例在 iOS 13.1 上无法连接 Profiler

 • 

Workaround:Xcode 项目里 Add Capability 'Access WIFI Information' && 申请地理位置权限
PostBuildCapabilityChanger.cs

using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using UnityEngine;

namespace FinGameWorks.Scripts.Editor.Modifer
{
    public static class PostBuildCapabilityChanger 
    {
        [PostProcessBuild(999)]
        public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject)
        {
            string path = pathToBuiltProject + "/Unity-iPhone.xcodeproj/project.pbxproj";
            Debug.Log(path);
            Debug.Log(PBXProject.GetUnityTargetName());
            ProjectCapabilityManager capabilityManager = new ProjectCapabilityManager(path, "InkCloth.entitlements", PBXProject.GetUnityTargetName());
            capabilityManager.AddAccessWiFiInformation();;
            capabilityManager.WriteToFile();
        }
    }
}

PermissionManager.cs

using System.Collections;
using UnityEngine;

namespace FinGameWorks.Scripts.Manager
{
    public class PermissionManager : Singleton<PermissionManager>
    {
        private LocationService m_LocationService;
        protected override void OnAwake()
        {
            base.OnAwake();
            m_LocationService = new LocationService();
            m_LocationService.Start();
        }
    }
}

2019.8.29

 • 

连着上了两个月课,也有些懈怠了。
上英语课的最大发现倒不是语言上的什么诀窍,而是发现自己在表达看法上的匮乏。有些话题给我我就没话说也没什么想写,因为“感觉一句话就说完了”。换言之,一直以来是刻意地训练自己把看法内化,而突然被要求对这些看法重新评估的时候就已经有预设了,自然感觉没什么好说。这件事倒是没什么好或不好的,只是在没有足够的理论方法却又建立了太多个人观点的尴尬境地里徘徊一会。
因为上课也没有太多时间,很多想法也没有在 Ghost 上写了,转而偶尔在 Day One 里记一下。之前花了很大功夫从 Day One 迁移到 Journey,结果 Journey 的 Chrome App 说关就关,电脑上只能用桌面客户端,虽然是一次性购买但软件质量真的不值几十美元,因此又转而续了 Day One 的订阅。
有了几个新的想法,比如闲暇时有在慢慢写一个 iOS 上基于 Files 的 DocumentPickerUI 的背单词软件。此外还有一些 Unity 项目和 Unity Asset 的计划,但是 Unity 项目的调试周期都太长,目前是没有可能做了。目前看来语言考试的排期已经到了一两个月后,如此看来今年下半年是不太可能有时间去 Unity Shanghai 写喜欢的项目了,其实还挺可惜的。如果年底语言考试完结,估计也不打算去什么公司了,而是直接重写 Epoch,希望明年动身前能差不多做到核心玩法都完成的地步,后面再走一步看一步。
看了好几部电影,包括 Shaft 1 & 2,哪吒和速度与激情,记得之前还重新看了遍星银岛。
密集地修了两次电脑,一次是 MBP 18 的键盘,一次是 MBP 15 的电池召回。两次都是在南京修的,每次去都要顺路去买泡芙,因此和背单词记含义一样现在建立起来了苹果店和泡芙店的奇怪联系。
博客写的越来越少了,各种平台也不怎么说话了,除去上课的原因,自己相比以前也更没话说了。有的时候看以前的推发觉还挺好玩,那种心态倒是挺难得的,一种急切又乐观的状态,但是也因此很难维持下去。自己倒是逐渐在适应沉住气的节奏,因此和都在做好玩的事的大家相比显得就更为无话了,默默围观下大公司雇员,独立开发者群,设计群和游戏开发们的动静。有的时候大家又都在发看法,我想说些什么却似乎也没有发表看法的必要,因为某种意义上来说,不暴露自己的政治倾向也是政治倾向的一部分。

2019.8.10

 • 

做了万全的准备 来杭州首考托福 结果考试因为台风取消了 真是哭笑不得
现在在考虑要怎么回去

lekima

2019.7.12

 • 

六月底拿到了毕业证。之后就一直在上英语课,也算是回到了学生的日常生活。之前翘课了那么多年,这次反而头一回在主动地去机构学习东西,倒也显得有趣。
看了 blank vhs covers were kinda beautiful

2019.6.20

 • 

Macbook 键盘出问题了,因此最近用起来多了很多双击的困扰,比如敲击 hao(好)会经常粘连成 haoo(好喔)。虽然这么一说还挺可爱的,但编程的时候也总是变量名敲错地烦躁,因此约了周末的 Genius Bar 看什么情况,还好一般的任务我可以用 Pixelbook 来代替。最近还在 Pixelbook 上装了 Idea 和 Webstorm,只不过需要手动编辑 desktop 文件把图标路径从 svg 指向 png,其余都大体能用。
个人项目再一次往后拖了,也不知道暑假的时候还有没有空完成。此外在考虑暑假之后到明年六月份是:去上海 Unity 入职 / 重新面一次微软苏州的 iOS 岗 / 留在北京工作,还是在家把 Epoch 做完发布。我到现在也没有一个具体的想法,Unity 的话 offer 应该还算稳妥,好处就是也许能跟着我很喜欢的框架的项目组开发,还是少有的上海本地有独立自主开发权的项目,真做好了也就可以说是 Dogfooding 了;微软苏州如果暑假忙完刷算法应该可以再试试,只不过不确定自己的水平,也还不清楚和下半年的几场考试冲不冲突;在家的话就是可以完全准备语言考试,也有时间宅在家,如果充分投入的话也许明年六月份就能做出来 Epoch,意义和潜在的后续发展可能都挺大的。
毕业证去长沙问了一次,说还没印出来,要排在今年毕业的学生后面印了,也不知道我到底什么时候才能真正毕业。

2019.6.6

 • 

macOS 升级到了 10.15,跑了下 Marzipan 完成度出奇地高。然后发现 Unity 导出 il2cpp 项目不正常,于是又降级了。今年 WWDC 还是有很多新东西的,比如 SwiftUI 和 BackgroundTask。但是令人迷惑的东西也多,比如 iPad 所谓的新多窗口多任务交互感觉更加难用了,以及新 Mac Pro 似乎是很强,但定位也并不是给一般的 pro,而是土豪 pro 们了,因此目前为止苹果产品线上还是没有一个能满足一般的多媒体创意需求(软件开发,游戏开发,建模和渲染之类)的中等价位的台式主机/笔记本产品,也没有 N 卡。
Skyline 的 Dark Mode 支持已经推到 Stable Channel 了,顺便解决了 Android Q block background activity 启动的警告。估计会在 6.10 - 6.16 开一个小的 0.99 刀促销活动来庆祝下。
看了 Minority Report — Dismantling Precrime

2019.5.31

 • 

坐上了离开长沙回北京的车。
来长沙这几天主要就是办毕业证的事情。事出突然,看到消息的当天就买了晚上的车票。在去长沙的火车上又写了几封邮件 spam 了一圈科技媒体推荐我的 Vortex,结果今天收到了好几个 Vortex 的崩溃报告,恐怕是编辑们的手机,自觉推荐是无望了。
学校的事情没什么要说的,就是提交一个材料申请正式毕业,一切无误的话两周后会签发毕业证书。和辅导员聊的倒是异常愉快,聊了计科 14 级毕业后这一年大家都如何。
听了 Podcast 才知道一个朋友严格来说并没有上完大学,而这算是我所认识的第三位大学没有完成的人了,而我以前经常拿别人的例子(比如 Randy Lu 的大学经历)来试图说服我妈,特别是大二十分想辍学的时候。现在已经要真正毕业了,我妈也对我的惊人举动也见惯不惯,倒也并没有再举例说服她的必要了,但我对这个发现还是颇感惊奇。我妈因此问我,要是我回到大二那个特别想辍学的时候还会怎么选择。我说现在我已经没有选择了,但我如果以后有了下一代,有着独特的想法而学校挡住了他的路,那我就会让他休学一年,让他好好的去做自己的事情。如果他受到了社会的打击认清了现实,那是个好事,因为他提早体验了社会。如果他受到了社会认可被推到了前台,那也是个好事,因为从此他多了一条路,多了个选择。对于我来说,我觉得选择的结果好与坏倒没有以前看的那么严重了,但多几个选择,特别是本身就有想法的人来说,是会有很大的好处和幸运的,因为有想法的人终究不会过的太差,但提早知道自己是有选择的,并且明白自己选择的后果,会让人更早地考虑清楚很多事情。
大一的时候,因为尝试了几次校内比赛被只会 PPT 瞎吹的学长们压制的不行,愤然就去做外包为主,接着大二就去公司实习了,实习的这家公司也符合了我对外界的一切想象,因此更加觉得公司更能比学校实现自己的价值,和上一位辅导员关系闹的特别僵。现在让我重新来一次的话,我倒是会有所改变,也许初期会跟着老师走几个比赛项目,有老师的影响力能保证校赛不被打压,接着湖南也没几所大学,自然靠着学校的声誉也能保证省赛不被打压,接下来国赛再真的发挥自己的能力也不迟。这么做下去对自己和学校的关系会很有好处,也方便自己有个借口不被学校的条条框框所束缚,进而继续做自己想做的东西,比如混开源社区攒 star,比如学点建模和 VFX。等到自己在校内混到了一定知名度的时候再离开实验室,以比赛或其他的借口出去实习,就不会有那么阻力了。然而当时自己心态比较傲,也太早有其他的经历来作为资本让自己傲起来了,就没有选择这条路,而是直接翘了课走人。
翘课是一件当时对我来说很有吸引力的事情,前提条件自然是我是一个计算机偏软件的学生,如果我是医学生自然是没有这种可能性的,因为本科医学知识是靠大量的观察积累起来的,而计算机则没有这种忧虑,特别是软件相关更是几个月就更新一轮框架,每年就更新一轮系统,总有老师和教材跟不上的情况,而且教材在这种情况下也失去了跟随新版本的意义,这也是为什么大学一般都只教学语言但不教学框架使用的原因。回到翘课这件事情上,老师自然是痛恨的,毕竟对于老师来说,这个职业的意义也就是有人在听,但尴尬的是很多课的确没有在公司实习来的让人觉得有意义,因为前面也说了:学校是不会真正教你怎么去用最新的工具链编写 GUI 工具的,而对于学生来说,如果在学校学习了好几门语言,都只知道如何在命令行里输出 x + y = z,那学生如何知道这门语言有什么用处,又和他们日常使用的电子产品有什么关系的?而在公司实习是实打实的在做看得见摸得着的东西,虽然会有人嗤之以鼻觉得公司的业务是最没价值的内容,但很少会有人一开始就对计算机和软件能做什么有清晰的认识的,如果没有一个具有实体感的东西让他们入门,很多人最后就去打游戏去了。
最后,延期毕业也一年了,虽然这一年并没有像我的很多同学真正在去做全年全职的工作,也没有 996 过,但我自己也明显地感受到了可支配时间相比大学时代的减少,而这更加让我觉得大学就是一个寻求可能性的地方。对于很多人来说,入学时是没有目的的也没有意识去寻找目的的,而我的问题倒是太有目的了,因此结果都并不是最理想的。大三做经验分享会的时候,我对学弟说最好的方式就是找到喜欢的事情然后去做生产相关的事情,喜欢电影就去写影评,喜欢玩游戏就去做游戏,最后这些生产的内容都会有价值。但现在看来,当时的我反而只盯着生产价值这件事情在去做,因此反而有种苦行僧的感觉,忘了价值也是要分好几个篮子装来分散风险的。如果当时能多分点时间在其他个人爱好上,现在也许会有更多实现价值的门路可以走,自己也更镇定和自信,遇到打击性的事件也不会徘徊那么久。