[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
了。
参考:https://m.yisu.com/zixun/146420.html
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
修改其中的net.bindIp
为0.0.0.0
(并重启)就可以给外网访问了(无需密码)。