[TOC]
南川,2021年4月
- 增加本地上传功能(以对OpenAPI进行测试)
- 研究mongoose -> swagger的实现
- 对分类结果与图片的本身标注做对比分析
- 进一步解决只识别出部分条目的图片问题(打算先以识别出小于3条的流水类图片着手分析)
- 进一步解决未分类成功的问题
- 对详情类图片做进一步的检验(因为这个相较于流水类更难捕捉出错误)
- 对分类的所有结果进行统计分析
- 增加对其他类型,例如图片等文件的导入支持(暂无需求)
- 解决
react的热更新问题(已基于webpack serve实现) - 解决
electron的热更新问题(已基于electron-reloader实现) - 增加对
css、less等文件的导入支持(已基于style-loader、css-loader、les-loader在webpack中实现) - 修复由于
redux-thunk导致的redux在createStore时出现typescript警告的问题(这个问题最令我惊奇的是使用//@ts-ignore都无法屏蔽……不过本质上是因为combinedReducers用到了一个特有Symbol叫$CombinedState,反正挺蛋疼,我暂时先在tsconfig.json配置里把declaration和declarationMap关了,就没问题了,这个ts选项我暂时也用不到。) - 基于以上对
redux-thunk的修复,进一步寻找一个优秀的typescipt+redux的编码约定(目前我把所有的actionTypes写在一个文件里,然后在actions文件里写同步(直接返回Action)或者异步(thunk)代码,其中thunk不走reducer,同时还要把thunk中间件放在logger中间件的前面。另外,我不再考虑既声明ActionType又声明Action的问题了,对于Action直接使用AnyAction(默认自带type属性),对于ActionType直接使用switch(action.type as XXActionType)代码约定,我觉得挺好的。日后再研究其他大佬的代码吧~)
- Electron
- React
- Redux
- TypeScript
- Webpack
官方lean描述:
而将lean结果中的ObjectId转成string,可以通过使用以下插件:
- 完成云端数据库的重新安装、配置
彻底重构了代码,所有接口均重新统一定义,接口、视图、算法、配置、自动脚本高度模块化。
按照准确度标准,目前成功分类情况如下,错误率在20%左右:
现一一进行错误修复。
根据程序识别,另一种有两行(讨论过)的情况,就是当logo里有字的时候,比如如下:
所以程序可以设置两条逻辑,即当金额与详情隔着两行的时候,通过判定两行的长度去分析属于哪一类,尤其对于第二类,考虑限制在字数五个以下。
经过如上处理后,已经只有10%左右错误率了。
根据位置计算:图标位170,图宽965,比例为0.176,以上下浮动2个点算,如果我们发现低于屏幕宽度15%的文字识别,则可以视为非流水解析部分。
目前来看,应该没此类问题了,不过错误更多了,应该是发现了之前没发现的错误。目前错误率在15%左右。
这张图至少暴露了两个问题,一个是有一些字没有匹配,比如“其他”。(下一个场景说明)
另一个就是带千分位金额没有匹配成功。
第二个问题较为简单,可以先处理一下。
第一种问题之后统一处理(比如用聚类)
尽管这个“其他”没有匹配出来让我们很头疼,但是仔细分析,依旧有转机。
可以看到,这张截图里,匹配出来4条,一共6,剩余2条就是“其他”,然后确信度为三分之二,正是4除以6。
造成这样的原因,主要是因为,我们支付宝的匹配依据是日期与金额相距为2,而微信是相距为1,当“其他”未匹配成功时,就被微信模式匹配上了,于是最终出来匹配出4个支付宝、2个微信的局面。
解决的办法也就呼之欲出了,我们对于没有达到百分之百确信度的流水匹配,将他们全部转成支付宝就好。
也许会有误判,但可能性不高,我们可以做如下判断,如果支付宝匹配数大于微信,则将微信部分全部转成支付宝,否则程序报错,我们检查一下。
我们可以通过识别框的宽度(是否超过屏幕宽度的百分之80),并且有省略号、最后是数字,等综合确定,以进行分割预处理。
经过对以上问题的分析与处理后,目前已经去除了以上类型的问题。
当然,这肯定不能表示已经没有错误了,事实上还有一些隐藏的错误,比如实际有多条,但只识别出一两条,并且还报对。这些留待下一版解决。
最后,再给出一下,经过标注系统处理后,剩余的一些未能进入分类算法的图片集合:
文字版如下,如有必要可以检验,大部分是由于截图不全,少部分是因为浮窗之类无需深入解决的问题:
{
"微信/商户消费/详情页无商户全称/美团点评&京东&拼多多平台商户、理财产品/有优惠/支出/交易名称:对方名称/当前状态:支付成功/xq1.jpg": "有浮窗(跳过)",
"微信/商户消费/详情页内容齐全/付款码、乘车码、微信小程序、第三方APP、手机充值、公众号打赏/无优惠/退款/当前状态:已全额退款/ls1.jpg": "未分类成功(跳过)",
"微信/商户消费/详情页内容齐全/付款码、乘车码、微信小程序、第三方APP、手机充值、公众号打赏/无优惠/退款/退款状态:已退款/ls3.jpg": "未分类成功(跳过)",
"微信/商户消费/详情页内容齐全/付款码、乘车码、微信小程序、第三方APP、手机充值、公众号打赏/有优惠/支出/当前状态:支付成功/ls3.jpg": "未分类成功(跳过)",
"支付宝/余利宝/转入/交易名称:商品说明/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)",
"支付宝/余利宝/转入/交易名称:转出说明/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)",
"支付宝/余利宝/转出/交易名称:转出说明/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)",
"支付宝/余利宝/转出/交易名称:转出说明/当前状态:交易成功/ls2.jpg": "未分类成功(跳过)",
"支付宝/余额/充值/交易名称:余额充值/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)",
"支付宝/余额/提现/交易名称:余额提现/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)",
"支付宝/余额宝/收益/交易名称:商品说明主要部分/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)",
"支付宝/余额宝/转入/交易名称:商品说明/当前状态:交易关闭/ls1.jpg": "未分类成功(跳过)",
"支付宝/余额宝/转入/交易名称:商品说明/当前状态:交易关闭/ls2.jpg": "未分类成功(跳过)",
"支付宝/余额宝/转入/交易名称:商品说明/当前状态:交易成功/ls4.jpg": "未分类成功(跳过)",
"支付宝/余额宝/转出/交易名称:商品说明/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)",
"支付宝/余额宝/转出/交易名称:转出说明/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)",
"支付宝/小钱袋/转入/交易名称:商品说明/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)",
"支付宝/小钱袋/转入/交易名称:商品说明/当前状态:等待付款/ls1.jpg": "未分类成功(跳过)",
"支付宝/小钱袋/转出/交易名称:商品说明/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)",
"支付宝/线上交易/多出汇率(国外网站交易)/支出/交易名称:商品说明/当前状态:交易成功/ls1.jpg": "未分类成功(跳过)"
}此外,目前系统预设的流程相关的图片标注分类如下,未来将继续补充完善:
export const PRESET_MARK_TYPES = [
"已完全识别",
"未分类成功(跳过)",
"截图不全(跳过)",
"有浮窗(跳过)",
"延后处理",
];直接按Y确认即可,或者提前在本地全局安装好:
npm install -g webpack-dev-server可能是因为网络原因,可以重新安装一遍,或者继续安装未安装成功的那个包
重新配置WebStorm里typescript所使用的的位置与版本:
WebStorm > Preference > Language & FrameWorks > Typescript > Typescript
选择自己本地全局安装的Typescript(注意先更新到最新版,目前是4.2.4+),不要选用项目初始化的内置Typescript
webpack的交互对象是js文件,需要通过其他loader将特定格式的文件转成js文件处理。
对于css文件,可以先通过css-loader转成css信息的js数组格式,这个时候css其实已经导入了,但是尚未能够生效在目标网页上。
而实现后一步操作的,就是style-loader,它们将这些编译后的js格式的css信息进一步插入到网页的style标签内(这也就是style-loader取名之意)。
而对于其他格式,比如less、sass等,则可以先将它们转成css格式,要么直接用特定的loader,比如对于less文件可以使用lees-loader,要么可以用postcss处理(此项目只用了less-loader,[todo]: postcss有点复杂,日后再更)。
其配置如下:
[
{
test: /\.css$/,
use: [{ loader: "style-loader" }, { loader: "css-loader" }],
},
{
test: /\.less$/,
use: [
{ loader: "style-loader" },
{ loader: "css-loader" },
{ loader: "less-loader" },
],
}
]在学习一个好项目:https://github.com/electron-react-boilerplate/electron-react-boilerplate 的时候,发现他里面的所有webpack配置文件都用了ESM,非常好奇他的实现,因为我把自己的webpack配置文件从CMJ改成ESM后就无法运行了。
后来经过大量测试,以及结合这篇QA:https://stackoverflow.com/questions/31903692/how-can-i-use-es6-in-webpack-config-js/67136670#67136670 ,终于找到了自己的解决方案,那就是 echo '{"presets": ["@babel/preset-env"]}' > .babelrc
g
此外,还得安装一下babel的相关依赖:
yarn add -D @babel/core @babel/register @babel/preset-env
最后,将webpack.config.xx.js改成webpack.config.xx.babel.js即可。
这是一项很有收获的经验。
结合一些资料,可以知道有两种办法。
第一种,是使用tsc进行预编译,然后再用electron运行。
其具体命令如下:
tsc ./src/main/index.ts && electron ./src/main/index.js
这里比较关键的是,tsc执行完成后会生成.js文件,然后electron运行这个.js文件即可。
但也存在问题,如果使用tsc -w命令监视index.ts文件,则当我们修改源代码时,虽然会实时地重新编译,但是由于我们electron已经加载了原来的.js文件,因此无法做到electron的热更新。
此外,有资料指出,这种先编译的开销较大。
第二种,也是本项目(参考的electron-react-boilerplate)所选用的方法,就是将.ts文件交给babel处理,babel只处理其中涉及到ts相关的部分,编译转换的开销较小。
在上一个章节,我介绍了如何在webpack配置中使用ESM的办法,具体就是使用babel,结合.babelrc文件。不过那次我们只需要处理ESM的import和export,因此只需要@babel/preset-env即可,但这次我们要处理typscript,因此还需加上@babel/preset-typescript(顺便还要安装一下)。
不过还没完,还得加上一个babel_register的脚本,在运行renderer中不需要加是因为有webpack serve帮我们搞定了,但运行electron我们是纯babel,所以需要自己预处理.ts相关文件。
另外有趣的一点是,基于tsc编译后的每个.ts文件都有一个对应的.js文件,而通过babel编译后的每个.ts文件都对应两个额外文件:.d.ts和.d.ts.map。
第一种方法,是将main和renderer独立开来。
先用webpack serve将react组件渲染到localhost,然后再用electron里的loadUrl函数渲染main。实践证明,由于编译、渲染需要耗时,以及webpack serve是一个堵塞操作,不方便在script里进行顺序控制,所以我需要在loadUrl函数里写一个轮询,直到渲染成功后才正常加载。
这个方法比较笨拙,但是容易理解。基于这个理解之后,再来看第二种方法,它的本质也是两个独立进行,但是基于webpack的一些默认参数,做到了更好的进程间通信(于是就不用轮询了,降低了对业务代码的入侵)。
核心就是直接启动renderer进行,但是在renderer的配置文件里,加上devServer选项,核心配置如下,在before参数里使用spawn开启了一个新的进程,实在awesome~:
devServer: {
before()
{
console.log('Starting Main Process...');
spawn('npm', ['run', 'start:main'], {
shell: true,
env: process.env,
stdio: 'inherit',
})
.on('close', (code) => process.exit(code))
.on('error', (spawnError) => console.error(spawnError));
}
}另外,这里的.on('close')回调函数,也解决了如果独立运行两个进行,则关闭electron进程后webpack serve进程仍然会无意义进行的问题。
index.html是必须要有的,至少有一个id=app之类的结点,问题是如何加载、编译与转换。
第一种方案,我们把index.html里要引用的script都写死,比如一般来说我们会将bundle.js打包到根目录下的dist/中,如果我们index.html在根目录下,则直接写死<script src="./dist/bundle.js"></script>即可。
这样,当webpack把react组件打包到dist/目录下后,这个index.html页面就可以展示react组件了(同时不要忘了引入react和react-dom的脚本文件)。最后,再由electron读取这个index.html文件,就能做到在electron中渲染。
但这种方案有个明显的缺点,那就是生成的dist文件中不含有index.html文件,这不适合项目的发布。所以,我个人还是推荐index.html得提前拷贝到目标路径吧dist/下,然后对bundle.js文件的引用就是./bundle.js了。
那么要实现这个拷贝,也至少有两个方法,第一种比较朴素,那就是每次运行renderer进程时将index.html用cp(linux、mac)或者copy_file(需要安装,全平台)拷贝到dist/目录下(同样要包含react、react-dom脚本)。
还有一种办法就比较智能,那就是使用webpack的html-webpack-plugin插件,它会自动将index.html拷贝到目标位置,并且还会自动加上其依赖(也就是说,不需要手动写引入bundle.js、react.js、react-dom.js等)。此外,它还需要热更新功能,可谓一举双得,不过这会覆盖devServer里的hot选项。
- 即使是
electron-react-boilerplate也不支持electron的热更新,hh,得找其他库看看实现原理,估计都用到了electron-reload吧,不过这个也不重要,重要的还是renderer部分的热更新。
然而很简单,只需要安装electron-relaoder后在electron的主程序里加三句话即可:
try {
require('electron-reloader')(module);
} catch {}它的原理,就是会自动寻找入口文件的依赖图,然后对其进行文件修改监测,一旦有确认修改就重启。
不过副作用就出现在这里,我们之前配置renderer的webpack部分时,加了一句:
on('close', (code) => process.exit(code))这一句代码会导致,当我们的electron程序关闭后自动关闭渲染进程,所以我们要删掉这一行。
(理论上renderer进程是electron进程的子进程,但是我们的webpack配置结果就是electron进程是renderer进程的子进程,目前来看没有太大问题,我不确定如果我们反过来是否就是把before改成after就完事了,但好像改的意义也不是特别大)
关于electron-reloader可以参考:
第一种方法,也是一开始采取的方法,比较笨拙。
那就是先求出预定的缩放比率,然后将图片按比例放缩,再讲OCR的矩形坐标按照比率进行计算再叠加渲染。
我一直觉得这个方法很笨拙,但当时时间有限、技术有限,所以只好用了这个方案。
第二种方法,图形本身不做缩放,OCR结果直接叠加渲染到图片上,最后将这个整体使用以下CSS进行缩放:
.img-with-ocr {
transform: scale(SCALE);
transform-origin: top left;
height: IMG_HEIGHT * SCALE;
}这里的IMG_HEIGHT就是图片的实际高度,而SCALE就是目标缩放比率,由于我需要的图片布局就是统一宽度,高度按比率缩放,所以SCALE这么设计。这里之所以还要对这个形状的高度本身做限制,是因为我这个容器就是顶层容器,但是缩放效果是不改变容器大小的,因此会出现缩放后的效果与容器不贴合的副作用,因此还需要重设容器大小。
这样,通过一个统一的缩放参数,就优化了第一种方案中需要计算2 + 4 * N遍的问题。此外,这种通过CSS里的scale参数去控制显示的缩放比率,还是非常有用的!(在此,感谢威盛项目,这个技巧便是在这个项目中摸索出来的。)
很奇怪,当我把数据缓存到和main、renderer同级的data文件下时,会导致renderer重启,但当我把它放进main里面则不会,实在不清楚webpack热重启到底依赖了哪些,它是整个src文件夹都做了监控吗?
最后终于找到问题了,原来不是webpack的锅,而是electron-reloader,具体见:
// 开发模式使用electron热重载,它会自动构建依赖图
// 一定要小心这里要监测的范围,由于我们修改electron主要是一些程序部分,所以千万不要把需要不断覆写的数据文件也加进来,不然会一直重载
// 这里的启示是要把数据文件独立出去,比如放在与`main`同级的`data`文件夹下,然后只监控`main`文件夹
try {
require("electron-reloader")(__dirname);
} catch {}从相对路径到绝对路径,我们已经很熟悉了,比如使用path.resolve之类的函数。
然而当我们的程序涉及到了打包、移动,那么问题就蹊跷起来了。
事实上,我通过这种方式将数据文件夹的相对路径指定成绝对路径之后,打包完直接到了electron/dist下的未知名路径,所以事实上产生了错误。
有趣的是,在renderer里会有这样的错误,但在main里没有,毕竟,其实只有renderer里的资源被打包了,main程序里的路径是不变的。
基于此,有两种方案,一种是所有的本地资源读取都在main里,另一种就是把路径写死这样两个进程里都可以使用了,毕竟,所谓的web secruity有时间再研究吧,我现在就是要在renderer里大用特用node!将业务效率MAX MAX MAX!
export const _initTokenLS: TokenLS = {
items: [],
tokens: [],
scenario: Scenario.unknown,
cntWxTokens: 0,
cntZfbTokens: 0,
confidence: 0,
}这是一个常量对象,用于初始化,然而,却隐藏着难以预估的bug。
问题就在于,虽然我们用const修饰符固定住了变量,所以不能用赋值语句修改_initTokenLS这个变量,然而,这并不能阻止我们尝试修改它子属性的行为(用C++术语理解,就是指针是常量,但是指针指向的变量不是常量)。
于是乎,当我用这个常量作为很多其他变量的初始值,并在其上做修改,例如如下:
let tokenLS: TokenLS = _initTokenLS;
tokenLS.confidence = 1; // _initTokenLS.confidence = 1这样,对tokenLS的修改实质上等价于对_initTokenLS的修改,问题就连锁式的、反复的产生了。
所以,要么,我们铭记,初始化的常量,不能直接赋值,比如可以用解构语法,写成这样(等价于C++中的复制构造):
let tokenLS: TokenLS = {..._initTokenLS}但是我们也许当时记得,后续难免会疏忽,那有没有规避对对象的修改操作的办法呢?倒也不是没有,比如可以用typscript进行对象的键值限制:
然而,这样的问题有三:
- 虽然对象里面的每个键
items、confidence等不可以被修改(重新赋值)了,但是依旧可以使用一些原型操作(比如当键是一个对象的时候),所以这个解决方案并没有本质上解决问题 - 尽管我们可以通过种种复杂的手段,限定初始化对象的修改操作,但是我们依旧会难免不甚使用赋值操作以初始化另一个变量,当我们主动或被动(IDE提醒)意识到这个对象不可以直接修改(而是需要解构赋值)后为时已晚
- 为了这个初始化对象,我们还需要多写一个一次性的接口,简直浪费。
所以另一种绝佳的办法就是写成一个初始化的函数:
export const initTokenLS = (): TokenLS => ({
items: [],
tokens: [],
scenario: Scenario.unknown,
cntWxTokens: 0,
cntZfbTokens: 0,
confidence: 0,
});这样,我们每次直接调用,赋值、修改都不会影响初始化的结果:
let tokenLS: TokenLS = initTokenLS();
tokenLS.confidence = 1; // next time, the initial tokenLS2.confidence = 0 still写到此,使用函数的优越性就彰显出来了,这似乎,从另一个角度理解了,为什么在redux里把各种action写成函数要比写成对象方便的多了,或许,问题的关键就在此文中。
问题貌似产生于,npm和yarn混用。
昨晚(2021年04月20日)我用yarn安装chai最后总是卡住,不断retry,最后用npm安装好了。
我观察到,这个包貌似有很多依赖相关,这可能导致了我的环境发生了变化,在那之后,貌似我在运行webpack的打包程序时,就会出现这样的一个警告:
Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with
最后参考这篇文章: 我想问一下这个问题怎么解决,谢谢!_慕课猿问 得到了解决,方案就是运行如下:
yarn config set scripts-prepend-node-path true{
"build:api": "tsoa spec-and-routes && tsc --experimentalDecorators --esModuleInterop --resolveJsonModule --outDir build src/api/server.ts",
}比如运行A函数,结果完全不符合预期,甚至是其他函数的结果,这种情况一定是map出问题了!
用我们的ts-node scripts/clear_non-ts-files.ts脚本清除一下这些map就正常了!
当我们封装了一些CURD的接口后,返回的字段是否包含populate的信息,具体的说,是返回了一个_id的string还是一个object,这是个问题,需要脑子清醒!
不然会报can't cast xx to .... @path之类的错误,值得注意。
另外,集成swagger时发现无法识别OID类型(即mongoose.Schema.Types.ObjectID),所以后来就把ObjectID都改成string了。
sudo yum erase $(rpm -qa | grep mongodb-org) #卸载MongoDB
sudo rm -r /var/log/mongodb #删除日志文件
sudo rm -r /var/lib/mongo #删除数据文件参考(red-hat):official tutorial of installing mongodb
# 1. 配置`mongo`的仓库信息
cat > /etc/yum.repos.d/mongodb-org-4.4.repo << EOF
[mongodb-org-4.4]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/4.4/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-4.4.asc
EOF
# 2. 通过`yum`安装
sudo yum install -y mongodb-org
# 3. 升级权限,比如
# sudo chown -R mongod:mongod <directory>
# 比如这是我之前的遗留mongo配置文件,需要升级权限,否则会出现`ExecStart=/usr/bin/mongod $OPTIONS (code=exited, status=14)`:
# sudo chown mongod:mongod /tmp/mongodb-27017.sock
# 4. 启动
systemctl start mongod默认配置文件路径:/etc/mongod.conf
配置文件的具体信息参考:https://docs.mongodb.com/manual/reference/configuration-options/#security-options
修改其中的net.bindIp为0.0.0.0(并重启)就可以给外网访问了(无需密码)。


















