.4-浅析webpack源码之convert-argv模块
上一节看了一眼预编译的总体代码,这一节分析convert-argv模块。
这个模块主要是对命令参数的解析,也是yargs框架的核心用处。
生成默认配置文件名数组
module.exports = function(yargs, argv, convertOptions) {
var options = [];
// webapck -d
// 生成map映射文件,告知模块打包地点
if(argv.d) { /* ... */ }
// webpack -p
// 压缩文件
if(argv.p) { /* ... */ }
// 配置文件加载标记
var configFileLoaded = false;
// 配置文件加载后的载体
var configFiles = [];
// 排序
var extensions = Object.keys(interpret.extensions).sort(function(a, b) {
return a === ".js" ? -1 : b === ".js" ? 1 : a.length - b.length;
});
// 指定所有默认配置文件名
var defaultConfigFiles = ["webpack.config", "webpackfile"].map(function(filename) {
return extensions.map(function(ext) {
return {
path: path.resolve(filename + ext),
ext: ext
};
});
}).reduce(function(a, i) {
return a.concat(i);
}, []); // more code...
}
函数内部,首先判断了argv.d与argv.p属性是否存在,这个属性来源于参数d与p,即webpack -d -p,测试如图:
因为懒得加,所以直接跳过,进入到第二阶段,生成默认配置文件名数组。
这里引入了一个小的模块interpret,调用Object.keys(interpret.extensions)返回一系列文件扩展名的数组,如图:
由于获取到的数组为乱序,所以这里首先进行排序,规则为.js放在第一位,后面的按长度从小到大,结果是这样:
接下来是两个map与一个reduce的调用,首先两个map会返回一个数组,包含两个对象数组,对象包含path、ext两个属性,path代表路径+文件名+后缀,ext就是后缀,调用map后会得到如下数组 (截取部分):
最后调用reduce方法将二维数组扁平化为一维数组,图就不截了。
定义配置文件路径与后缀
有了默认列表,第二步就是尝试获取对应的配置文件:
var i;
// 从命令行读取--config
// argv.config => config.js
if(argv.config) {
var getConfigExtension = function getConfigExtension(configPath) {
for(i = extensions.length - 1; i >= 0; i--) {
var tmpExt = extensions[i];
if(configPath.indexOf(tmpExt, configPath.length - tmpExt.length) > -1) {
return tmpExt;
}
}
return path.extname(configPath);
}; var mapConfigArg = function mapConfigArg(configArg) {
// 获取文件绝对路径
var resolvedPath = path.resolve(configArg);
// 获取文件后缀
var extension = getConfigExtension(resolvedPath);
return {
path: resolvedPath,
ext: extension
};
};
// 包装成数组 统一处理单、多配置文件情况
var configArgList = Array.isArray(argv.config) ? argv.config : [argv.config];
configFiles = configArgList.map(mapConfigArg);
}
// 如果未指定配置文件 尝试匹配默认文件名
else {
for(i = 0; i < defaultConfigFiles.length; i++) {
var webpackConfig = defaultConfigFiles[i].path;
// 检测路径中是否存在对应文件
if(fs.existsSync(webpackConfig)) {
configFiles.push({
path: webpackConfig,
ext: defaultConfigFiles[i].ext
});
break;
}
}
}
这里的代码比较简单,如果调用了--config自定义配置文件,该指令后面的会被当成参数传给argv.config。
存在argv.config则会对文件名与合法后缀数组进行匹配,检测出配置文件的后缀包装成对象返回。
如果不指定配置文件,会进入else代码段开始遍历默认配置文件数组,fs.existsSync检测当前路径是否存在该文件,有就当成配置文件包装返回。
获取配置文件输出模块并做简单处理
上一步只是代表接确定了配置文件的绝对路径,这个文件并不一定是有效且存在的。
这一步会获取到配置文件的输出并简单处理:
if(configFiles.length > 0) {
var registerCompiler = function registerCompiler(moduleDescriptor) {
// ...
}; var requireConfig = function requireConfig(configPath) {
// 获取到modules.exports输出的内容
var options = require(configPath);
// 二次处理
options = prepareOptions(options, argv);
return options;
};
// 本例中configFiles => [{path:'d:\\workspace\\node_modules\\webpack\\bin\\config.js',ext:'.js'}]
configFiles.forEach(function(file) {
// interpret.extensions[.js]为null
// 这里直接跳出
registerCompiler(interpret.extensions[file.ext]);
// 这里的options是convert-argv.js开头声明的数组
options.push(requireConfig(file.path));
});
// 代表配置文件成功加载
configFileLoaded = true;
}
这里的处理情况有两个:
1、根据后缀名二次处理
2、将路径传进一个prepareOptions模块处理
这个模块内容十分简单,可以看一下:
"use strict"; module.exports = function prepareOptions(options, argv) {
argv = argv || {};
// 判断是否通过export default输出
options = handleExport(options);
// 非数组
if(Array.isArray(options)) {
options = options.map(_options => handleFunction(_options, argv));
} else {
// 当options为函数时
options = handleFunction(options, argv);
}
return options;
}; function handleExport(options) {
const isES6DefaultExported = (
typeof options === "object" && options !== null && typeof options.default !== "undefined"
);
options = isES6DefaultExported ? options.default : options;
return options;
} function handleFunction(options, argv) {
if(typeof options === "function") {
options = options(argv.env, argv);
}
return options;
}
这里针对多配置(数组)与单配置进行了处理,判断了模块输出的方式(ES6、CMD)以及输出的类型(对象、函数),最后返回处理后的配置对象并标记配置文件已被加载。
终极处理函数
接下来就是最后一个阶段:
if(!configFileLoaded) {
return processConfiguredOptions({});
} else if(options.length === 1) {
return processConfiguredOptions(options[0]);
} else {
return processConfiguredOptions(options);
} function processConfiguredOptions(options) {
// 非法输出类型
if(options === null || typeof options !== "object") {
console.error("Config did not export an object or a function returning an object.");
process.exit(-1); // eslint-disable-line
}
// promise检测
if(typeof options.then === "function") {
return options.then(processConfiguredOptions);
}
// export default检测
if(typeof options === "object" && typeof options.default === "object") {
return processConfiguredOptions(options.default);
}
// 数组
if(Array.isArray(options) && argv["config-name"]) { /* ... */ }
// 数组
if(Array.isArray(options)) { /* ... */ }
else {
// 单配置
processOptions(options);
} if(argv.context) {
options.context = path.resolve(argv.context);
}
// 设置默认上下文为进程当前绝对路径
if(!options.context) {
options.context = process.cwd();
}
// 跳过
if(argv.watch) { /* ... */ }
if(argv["watch-aggregate-timeout"]) { /* ... */ }
if(typeof argv["watch-poll"] !== "undefined") { /* ... */ }
if(argv["watch-stdin"]) { /* ... */ }
return options;
}
这里根据不同的情况传入空对象、单配置对象、多配置数组。
在函数的开头又再次检测了合法性、promise、ES6模块输出方法,由于本例只有一个配置对象,所以直接进processOptions函数,这个函数很长,简化后源码如下:
function processOptions(options) {
// 是否存在output.filename
var noOutputFilenameDefined = !options.output || !options.output.filename; function ifArg(name, fn, init, finalize) { /* ... */ }
function ifArgPair(name, fn, init, finalize) { /* ... */ }
function ifBooleanArg(name, fn) { /* ... */ }
function mapArgToBoolean(name, optionName) { /* ... */ }
function loadPlugin(name) { /* ... */ }
function ensureObject(parent, name) { /* ... */ }
function ensureArray(parent, name) { /* ... */ }function bindRules(arg) { /* ... */ }var defineObject; // 中间穿插大量ifArgPair、ifArg、ifBooleanArg等 mapArgToBoolean("cache"); function processResolveAlias(arg, key) { /* ... */ }
processResolveAlias("resolve-alias", "resolve");
processResolveAlias("resolve-loader-alias", "resolveLoader"); mapArgToBoolean("bail"); mapArgToBoolean("profile");
// 无输出文件名配置
if (noOutputFilenameDefined) { /* ... */ }
// 处理命令参数
if (argv._.length > 0) { /* ... */ }
// 无入口文件配置
if (!options.entry) { /* ... */ }
}
首先看一下里面的工具函数,区别了不同参数类型的命令。
指令分类如下:
ifArg:基本处理函数
ifArgpair:命令参数存在键值对形式
ifBooleanArg:无参命令
mapArgToBoolean:命令参数为布尔类型
(这里面的argv[name]均代表一个对应的指令,如:argv["entry"]代表--entry。)
1、ifArgpair、ifArg
function ifArgPair(name, fn, init, finalize) {
// 直接进入ifArg函数
// content => argv[name]的数组元素
// idx => 索引
ifArg(name, function(content, idx) {
// 字符"="索引
var i = content.indexOf("=");
if (i < 0) {
// 无等号的字符
return fn(null, content, idx);
} else {
// 传入=号左边与右边的字符
return fn(content.substr(0, i), content.substr(i + 1), idx);
}
}, init, finalize);
} // init => 构造函数
// finalize => 析构函数
function ifArg(name, fn, init, finalize) {
if (Array.isArray(argv[name])) {
if (init) { init(); }
argv[name].forEach(fn);
if (finalize) { finalize(); }
} else if (typeof argv[name] !== "undefined" && argv[name] !== null) {
if (init) { init(); }
fn(argv[name], -1);
if (finalize) { finalize(); }
}
}
2、ifBooleanArg
// 当argv[name]不为false时才执行fn函数
function ifBooleanArg(name, fn) {
ifArg(name, function(bool) {
if (bool) { fn(); }
});
}
3、mapArgToBoolean
// 处理布尔值指令
function mapArgToBoolean(name, optionName) {
ifArg(name, function(bool) {
if (bool === true)
options[optionName || name] = true;
else if (bool === false)
options[optionName || name] = false;
});
}
4、ensureObject、ensureArray
// 保证指定属性为对象
function ensureObject(parent, name) {
if (typeof parent[name] !== "object" || parent[name] === null) {
parent[name] = {};
}
}
// 保证指定属性为数组
function ensureArray(parent, name) {
if (!Array.isArray(parent[name])) {
parent[name] = [];
}
}
5、bindRules
function bindRules(arg) {
// 指令可以是a=b 也可以是单独的a
ifArgPair(arg, function(name, binding) {
// 没有等号的时候
if(name === null) {
name = binding;
binding += "-loader";
}
// 生成对应的test正则与loader
var rule = {
test: new RegExp("\\." + name.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") + "$"), // eslint-disable-line no-useless-escape
loader: binding
};
// 生成前置或后置loader
if(arg === "module-bind-pre") {
rule.enforce = "pre";
} else if(arg === "module-bind-post") {
rule.enforce = "post";
}
options.module.rules.push(rule);
}, function() {
ensureObject(options, "module");
ensureArray(options.module, "rules");
});
}
bindRules("module-bind");
bindRules("module-bind-pre");
bindRules("module-bind-post");
后面的bindRules可以看出如果要在命令中引入loader,可以使用module-bind、module-bind-pre、module-bind-post三个参数。
该指令参数一般用“=”号连接需要转换的文件类型与对应的loader,测试案例如下:
等号两侧的字符串会变成name与binding传入函数中,并自动生成对应的test、loader并push进module.rules中。
也可以用没有等号的字符串,此时name默认为该字符串,loader会在后面加一个-loader,测试代码如下:
至于其余两个pre、post没啥讲的。
6、loadPlugin
function loadPlugin(name) {
var loadUtils = require("loader-utils");
var args;
try {
var p = name && name.indexOf("?");
if(p > -1) {
// 解析参数
args = loadUtils.parseQuery(name.substring(p));
name = name.substring(0, p);
}
} catch(e) {
console.log("Invalid plugin arguments " + name + " (" + e + ").");
process.exit(-1); // eslint-disable-line
} var path;
try {
var resolve = require("enhanced-resolve");
// 尝试获取插件模块的绝对路径
path = resolve.sync(process.cwd(), name);
} catch(e) {
console.log("Cannot resolve plugin " + name + ".");
process.exit(-1); // eslint-disable-line
}
var Plugin;
try {
// 加载模块
Plugin = require(path);
} catch(e) {
console.log("Cannot load plugin " + name + ". (" + path + ")");
throw e;
}
try {
// 返回插件实例
return new Plugin(args);
} catch(e) {
console.log("Cannot instantiate plugin " + name + ". (" + path + ")");
throw e;
}
}
这里的步骤比较清晰,如下:
1、判断传入参数是否形式类似于pluginname?params,对后面的参数进行解析
2、尝试获取插件的绝对路径
3、尝试加载模块
4、尝试调用new方法并返回模块实例
参数解析用到了loadUtils模块的parseQuery方法,这里进去看一下源码:
const specialValues = {
"null": null,
"true": true,
"false": false
}; function parseQuery(query) {
// 传入的query字符串必须以?开头
if(query.substr(0, 1) !== "?") {
throw new Error("A valid query string passed to parseQuery should begin with '?'");
}
query = query.substr(1);
// 如果只传一个问号返回空对象
if(!query) {
return {};
}
// ?{...}的情况
// 调用JSON5尝试进行对象解析
// JSON5是对JSON的扩展
if(query.substr(0, 1) === "{" && query.substr(-1) === "}") {
return JSON5.parse(query);
}
// 其余情况切割,或&符号
const queryArgs = query.split(/[,&]/g);
const result = {};
queryArgs.forEach(arg => {
const idx = arg.indexOf("=");
// 类似于处理get请求的参数 例如:?a=1&b=2
if(idx >= 0) {
let name = arg.substr(0, idx);
// decodeURIComponent对URI进行解码
let value = decodeURIComponent(arg.substr(idx + 1));
// 将null、true、false字符串转换为值
if(specialValues.hasOwnProperty(value)) {
value = specialValues[value];
}
// key以[]结尾
if(name.substr(-2) === "[]") {
// 截取key并设置值为数组
name = decodeURIComponent(name.substr(0, name.length - 2));
if(!Array.isArray(result[name]))
result[name] = [];
result[name].push(value);
}
// 正常情况直接在result对象上添加属性
else {
name = decodeURIComponent(name);
result[name] = value;
}
} else {
// ?-a&+b&c => result = {a:false,b:true,c:true}
if(arg.substr(0, 1) === "-") {
result[decodeURIComponent(arg.substr(1))] = false;
} else if(arg.substr(0, 1) === "+") {
result[decodeURIComponent(arg.substr(1))] = true;
} else {
result[decodeURIComponent(arg)] = true;
}
}
});
return result;
}
除去不合理的传参,可以用两种模式进行传参:
1、正常模式:?a&a=1&-a&+b&a[]=1
前缀为"-"、"+"会在else被处理,"-"符号开头值会被视为false,无前缀或者为"+"会被视为true。
类似于get请求参数会被一样处理,进行字符串切割并依次添加进result对象。
最后一种比较特殊,代表参数a是一个数组,学过JAVA或者C++应该会熟悉这种声明方式。
2、JSON模式:?{...}
以"{"开头"}"结尾会被进行JSON解析,注意这里不是普通的JSON.parse,而是引入了一个JSON的扩展JSON5,该工具相对于JSON扩展了多项功能,例如:
(1)JSON不允许有注释
(2)JSON中的key必须要用双引号包起来
(3)JSON对象、数组尾部不允许出现多余的逗号
等等。
详情可见:https://www.npmjs.com/package/json5
测试代码如下:
普通模式:
JSON模式:
7、processResolveAlias
function processResolveAlias(arg, key) {
ifArgPair(arg, function(name, value) {
// 必须以a=1这种键值对形式进行传参
if(!name) {
throw new Error("--" + arg + " <string>=<string>");
}
/**
* resolve:{
* alias:{
*
* }
* }
*/
ensureObject(options, key);
ensureObject(options[key], "alias");
options[key].alias[name] = value;
});
}
processResolveAlias("resolve-alias", "resolve");
processResolveAlias("resolve-loader-alias", "resolveLoader");
这里处理--resolve-alias指令与resolve-loader-alias指令,该指令参数必须严格按照a=b形式。
测试代码如下:
因为配置文件只有entry和output,所以属性都是undefined或false,都会跳过。
这里简单看几个常用的:
// 热重载
ifBooleanArg("hot", function() {
ensureArray(options, "plugins");
var HotModuleReplacementPlugin = require("../lib/HotModuleReplacementPlugin");
options.plugins.push(new HotModuleReplacementPlugin());
});
// loaderOptionsPlugin插件
ifBooleanArg("debug", function() {
ensureArray(options, "plugins");
var LoaderOptionsPlugin = require("../lib/LoaderOptionsPlugin");
options.plugins.push(new LoaderOptionsPlugin({
debug: true
}));
});
// 代码压缩插件
ifBooleanArg("optimize-minimize", function() {
ensureArray(options, "plugins");
var UglifyJsPlugin = require("../lib/optimize/UglifyJsPlugin");
var LoaderOptionsPlugin = require("../lib/LoaderOptionsPlugin");
options.plugins.push(new UglifyJsPlugin({
// devtool参数
sourceMap: options.devtool && (options.devtool.indexOf("sourcemap") >= 0 || options.devtool.indexOf("source-map") >= 0)
}));
options.plugins.push(new LoaderOptionsPlugin({
minimize: true
}));
});
可以看到,使用--hot、--debug、--optimize-minimize指令会分别加载3个插件,一个是处理loader中Options属性的LoaderOptionsPlugin插件,一个是代码压缩插件UglifyJsPlugin,还有一个就是热重载插件,3个插件后面的章节有空再讲。所有属性在之前的config-yargs中被配置,但是默认值为false,而ifBooleanArg在传入值为false时不会执行回调,所以这里并不是加载任何东西。
其他还有很多指令类似于--output-path可以设置output.path参数等等,有兴趣的可以自己去源码看。
最后剩下3个代码块:
// 无输出文件名配置
if (noOutputFilenameDefined) { /* ... */ }
// 处理命令参数
if (argv._.length > 0) { /* ... */ }
// 无入口文件配置
if (!options.entry) { /* ... */ }
由于指令没有传任何额外参数,所以argv._是一个空数组,中间的可以跳过。
所以只需要看其余两个,首先看简单的无入口文件配置的情况,即配置文件没有entry属性:
if (!options.entry) {
// 存在配置文件 但是没有入口函数
if (configFileLoaded) {
console.error("Configuration file found but no entry configured.");
}
// 未找到配置文件
else {
console.error("No configuration file found and no entry configured via CLI option.");
console.error("When using the CLI you need to provide at least two arguments: entry and output.");
console.error("A configuration file could be named 'webpack.config.js' in the current directory.");
}
console.error("Use --help to display the CLI options.");
// 退出进程
process.exit(-1); // eslint-disable-line
}
可以看出这是必传参数,根据是否找到对应的配置文件报不同的错误。
另一种情况是不存在ouput或output.filename属性:
if (noOutputFilenameDefined) {
ensureObject(options, "output");
// convertOptions来源于第三个参数
// module.exports = function(yargs, argv, convertOptions) {...}
// var options = require("./convert-argv")(yargs, argv)
// 只传了两个参数 所以跳过
if (convertOptions && convertOptions.outputFilename) {
options.output.path = path.resolve(path.dirname(convertOptions.outputFilename));
options.output.filename = path.basename(convertOptions.outputFilename);
}
// 尝试从命令参数获取output.filename
// 命令的最后一个参数会被当成入口文件名
else if (argv._.length > 0) {
options.output.filename = argv._.pop();
options.output.path = path.resolve(path.dirname(options.output.filename));
options.output.filename = path.basename(options.output.filename);
}
// 老套的报错 不解释
else if (configFileLoaded) {
throw new Error("'output.filename' is required, either in config file or as --output-filename");
} else {
console.error("No configuration file found and no output filename configured via CLI option.");
console.error("A configuration file could be named 'webpack.config.js' in the current directory.");
console.error("Use --help to display the CLI options.");
process.exit(-1); // eslint-disable-line
}
}
可以看出,output.filename也是必须的,但是不一定需要在配置文件中,有两个方式可以传入。
一个是作为convert-argv.js的第三个参数传入,由于在之前解析时默认只传了两个,这里会跳过,暂时不清楚传入地点。
另外一个是在命令中传入,测试代码:
至此,模块全部解析完毕,输出options如图所示:
真是累……
.4-浅析webpack源码之convert-argv模块的更多相关文章
- .3-浅析webpack源码之预编译总览
写在前面: 本来一开始想沿用之前vue源码的标题:webpack源码之***,但是这个工具比较巨大,所以为防止有人觉得我装逼跑来喷我(或者随时鸽),加上浅析二字,以示怂. 既然是浅析,那么案例就不必太 ...
- .30-浅析webpack源码之doResolve事件流(1)
这里所有的插件都对应着一个小功能,画个图整理下目前流程: 上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下: ParsePlugin.prototype ...
- .17-浅析webpack源码之compile流程-入口函数run
本节流程如图: 现在正式进入打包流程,起步方法为run: Compiler.prototype.run = (callback) => { const startTime = Date.now( ...
- .34-浅析webpack源码之事件流make(3)
新年好呀~过个年光打游戏,function都写不顺溜了. 上一节的代码到这里了: // NormalModuleFactory的resolver事件流 this.plugin("resolv ...
- .30-浅析webpack源码之doResolve事件流(2)
这里所有的插件都对应着一个小功能,画个图整理下目前流程: 上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下: ParsePlugin.prototype ...
- 浅析libuv源码-node事件轮询解析(3)
好像博客有观众,那每一篇都画个图吧! 本节简图如下. 上一篇其实啥也没讲,不过node本身就是这么复杂,走流程就要走全套.就像曾经看webpack源码,读了300行代码最后就为了取package.js ...
- 从Webpack源码探究打包流程,萌新也能看懂~
简介 上一篇讲述了如何理解tapable这个钩子机制,因为这个是webpack程序的灵魂.虽然钩子机制很灵活,而然却变成了我们读懂webpack道路上的阻碍.每当webpack运行起来的时候,我的心态 ...
- webpack源码-依赖收集
webpack源码-依赖收集 version:3.12.0 程序主要流程: 触发make钩子 Compilation.js 执行EntryOptionPlugin 中注册的make钩子 执行compi ...
- ADB 源码分析(一) ——ADB模块简述【转】
ADB源码分析(一)——ADB模块简述 1.Adb 源码路径(system/core/adb). 2.要想很快的了解一个模块的基本情况,最直接的就是查看该模块的Android.mk文件,下面就来看看a ...
- 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入
使用react全家桶制作博客后台管理系统 前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...
随机推荐
- I - Dividing Stones
Description There are N stones, which can be divided into some piles arbitrarily. Let the value of e ...
- unigui验证微信服务器的有效性
UNIGUI验证微信服务器的有效性: //////////////////////////////////////////// //UniGUIServerModuleHTTPCommand //公众 ...
- python- 动态加载目录下所有的类
# 背景 自动化测试框架中model层下有很多类,用来操作mysql的,使用的时候需要把全部的类加载进来,需要使用到动态加载类 # 解决方法 使用pkgutil,内置的方法,常用的话有两个方法 ite ...
- C# webservice服务跟踪调试方法(转)
1.新建网站,添加服务,并创建服务. 2.打开internet 信息服务管理器,添加网站,映射到创建的服务所在网站的目录. 3.打开服务所在网站的解决方案,进行配置. 1) 设置启动选项 选择启动操作 ...
- NPOI 操作笔记
public static class ExcelUtil { // Methods public static DataTable GetDataTable(Stream stream) { HSS ...
- js 将一个数组插入到另一个数组的方法
JavaScript将一个数组插入到另一个数组的方法.分享给大家供大家参考.具体分析如下: 1.通过Array.prototype.push.apply方法将一个数组插入到另外一个数组,下面的代码将数 ...
- Markdown的学习笔记一
之前学习看些书籍.学些技术都喜欢用xmind做思维导图的笔记,慢慢的发现想把一些笔记做的详细一些就会变得很复杂,个人觉得误了思维导图本意,而且用手机查看的时候也各种不方便.所以开始学习使用markdo ...
- 正则表达式中 re.match与re.search的区别
标签: 本文和大家分享的主要是python正则表达式中re.match函数与re.search方法的相关用法及异同点,希望通过本文的分享,能对大家有所帮助. re.match函数 re.match 尝 ...
- [WC2005]双面棋盘(线段树+并查集)
线段树+并查集维护连通性. 好像 \(700ms\) 的时限把我的常数超级大的做法卡掉了, 必须要开 \(O_2\) 才行. 对于线段树的每一个结点都开左边的并查集,右边的并查集,然后合并. \(Co ...
- 我的AI之路 —— OCR文字识别快速体验版
OCR的全称是Optical Character Recoginition,光学字符识别技术.目前应用于各个领域方向,甚至这些应用就在我们的身边,比如身份证的识别.交通路牌的识别.车牌的自动识别等等. ...