MapReduce--Input與Output規則
1、MR五大階段
- Input
- 如果讓你寫一個負責讀取的類?
- 實現讀取Mysql的資料:JDBC
- 實現讀取HDFS:HDFS JavaAPI
- Map:自定義
- Shuffle:分割槽、排序、分組、Combiner
- Reduce:自定義
- Output
- 如果讓你寫一個負責輸出的類?
- 將Reduce處理好的資料寫入Mysql:JDBC
- 將Reduce處理好的資料寫入HDFS:HDFS Java API
2、Input規則
所有的輸入都由Input的類來決定
job.setInputFormatClass(TextInputFormat.class)
- 預設的Input類:TextInputFormat
所有的Input類都要繼承自InputFormat
本質上:就是將讀取的API進行了封裝
- 如果我們要自定義,就要基於封裝的結構來填充API而已
MapReduce預設提供了一些封裝好的用於輸入的類
- TextInputFormat:用於讀取HDFS檔案,返回一個KV
- key:檔案的每一行對應個的偏移量
- Value:這一行的內容
- DBInputFormat:用於讀取MySQL中的資料,返回一個KV
- Sqoop:用於實現基於MapReduce讀寫MySQL
- TableInputFormat:用於讀取Hbase表中的資料
- TextInputFormat:用於讀取HDFS檔案,返回一個KV
- 功能:所有的InputFormat都要實現這兩個功能
- 1-將讀取到的資料進行分片
- 2-將每個分片的資料轉換為KeyValue
以TextInpuFomat為例
自帶的一些方法
createRecordReader:建立一個讀取器
- 所有的InputFormat必須呼叫讀取器來真正實現資料的讀取轉為KV
- 讀取器:真正負責讀取資料的類
- LineRecordReader:一行一行的讀取檔案,並轉換為KV
isSplitable:當前讀取的資料是否可以分割【決定壓縮型別是否可以分割構建多個分片】
資料是如何分片的?
- getSplits:TextInpuFormat呼叫父類FileInputFormat來實現了分片
- splitSize:決定了分割的大小
計算公式:computeSplitSize(blockSize, minSize, maxSize)
return Math.max(minSize, Math.min(maxSize, blockSize));
Max(最小分片數,Min(最大分片數,塊的大小))
|
Max(1,Min(256M,128M)) = 128M
最終的常見的規律:HDFS上一個檔案塊 = 一個分片 = 啟動一個MapTask
- 假設要處理的檔案是300M
HDFS : 128M 128M 44M
Input: split1 split2 split3
Map: MapTask1 MapTask2 MapTask3
- 設計的目的
- 因為在HDFS中已經分好了
- 只需要讓MapReduce根據分塊的大小,每個MapTask處理一個分塊的大小
- 避免了HDFS上資料的再合併再分割
特殊情況:假設檔案130M
- HDFS: 128M 2M
- 允許有10%的溢位:如果檔案大小超過分片大小10%以內,作為一個分片
- 130 / 128 > 1.1
- 只要檔案小於140M,就會作為1個分片進行處理
- Input: split1
- Map: MapTask
- 設計目的:避免一個MapTask處理的資料太小
- minSize:最小分片數
- 屬性: mapreduce.input.fileinputformat.split.minsize = 0
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
|
minSize = 1
- maxSize:最大分片數
- 屬性:mapreduce.input.fileinputformat.split.maxsize = 256M對應的位元組數
long maxSize = getMaxSplitSize(job);
|
maxSize = 256M
每個分片的資料如何轉換為KeyValue?
- 每種InputFormat都要構建一個讀取器
- 每個讀取器中:都有一個nextKeyValue,用於將讀取到分片的資料轉為KV
3、Output規則
- 所有輸出都由Output的類來決定
job.setOutputFormatClass(TextOutputFormat.class);
- 預設的Output類:TextOutputFormat
- 所有的Output的類都要繼承自OutputFormat
- 本質上:就是將寫入API進行了封裝而已
- 如果我們要自定義,就要基於封裝的結構來填充API而已
- MapReduce預設提供了一些封裝好的用於輸出的類
- TextOutputFormat:用於將結果寫入HDFS
- DBOutputFormat:用於將結果寫入MySQL
- TableOutputFormat:用於將結果寫入Hbase
- 功能
將結果儲存
4、自定義一個Input
問題:如果我們要處理的資料都是很多小檔案怎麼辦?
- MapReduce是不適合於處理小檔案
- file1:2KB
- file2:2KB
- 一個檔案就會作為一個分片,兩個小檔案就有兩個分片,就會啟動兩個MapTask
- 這樣是及其浪費資源,可能處理的時間還沒啟動的時間長
- 解決:自定義一個讀取器,將每個檔案的內容作為一個KV傳遞給Map,將所有檔案合併輸出成為一個檔案
- 需求:自定義一個InputFormat:用於將每個小檔案的內容作為一個KV
- 將每個檔案和內容合併為一個檔案:構建一個檔案集合
- SequenceFIle:這種檔案用於儲存多個檔案,裡面的每條資料KV就是一個檔案,記錄檔案的名稱檔案的內容
- MapReduce中的有SquenceOutputFormat
- 要求輸出的Key必須為Text型別的檔名
- 要求輸出的Value必須為Byteswritable型別的檔案資料
- 每一個檔案在SequenceFIle中都是一個KV
實現
- 自定義InputFormat
package bigdata.hanjiaxiaozhi.cn.mr.input;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
* @ClassName MRUserInputFormat
* @Description TODO 自定義的輸入的類,用於將每個檔案的資料變成一個KV
* K;不儲存任何東西
* V:每個檔案的資料作為一個Value
* @Date 2020/6/2 15:28
* @Create By hanjiaxiaozhi
*/
public class MRUserInputFormat extends FileInputFormat<NullWritable, BytesWritable> {
/**
* 返回一個讀取器,實現資料的轉換
* @param split
* @param context
* @return
* @throws IOException
* @throws InterruptedException
*/
@Override
public RecordReader<NullWritable, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
//構建讀取器
MRUserRecordReader mrUserRecordReader = new MRUserRecordReader();
//呼叫初始化方法
mrUserRecordReader.initialize(split,context);
//返回讀取器
return mrUserRecordReader;
}
/**
* 是否可分割
* @param context
* @param filename
* @return
*/
@Override
protected boolean isSplitable(JobContext context, Path filename) {
//一個檔案作為一個KV ,不分割
return false;
}
}
- 自定義讀取器
package bigdata.hanjiaxiaozhi.cn.mr.input;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
/**
* @ClassName MRUserRecordReader
* @Description TODO 自定義的讀取器,用於實現將每個分片【就是每個檔案】變成一個KV返回
*
* 通過HDFSAPI,讀取檔案資料,封裝成KV
* 將每個檔案的內容做Value返回
*
* @Date 2020/6/2 15:34
* @Create By hanjiaxiaozhi
*/
public class MRUserRecordReader extends RecordReader<NullWritable, BytesWritable> {
//構建需要返回的KV
NullWritable key = NullWritable.get();
BytesWritable value = new BytesWritable();//將每個檔案的內容作為這個物件的值
//構建全域性的Conf物件
Configuration conf = null;
//構建全域性的分片資訊
FileSplit fileSplit = null;
//設定標誌變數
boolean flag = false;
/**
* 初始化方法,只被呼叫一次
* @param split:分片中記錄了這個分片的對應的檔案資訊
* @param context:上下文物件,獲取到當前程式的conf物件
* @throws IOException
* @throws InterruptedException
*/
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
conf = context.getConfiguration();
fileSplit = (FileSplit) split;
}
/**
* 這個方法,用於獲取分片【就是一個檔案】中的資料,將分片的資料變成KV
* 這個方法的返回值
* true:還有下一條,儲存當前條,繼續對下一條進行處理
* false:沒有下一條了
* 注意:每個分片第一次被呼叫 時,必須返回true,不然當前KV會被丟掉
* 由於我們一個分片就是一個檔案,只構建一個KV,第二次必須返回false
* @return
* @throws IOException
* @throws InterruptedException
*/
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
//todo:核心的目標,讀取當前分片就是檔案的內容,塞到BytesWritable中,讓value有值
if(!flag){
//構建一個HDFS檔案系統
FileSystem hdfs = FileSystem.get(conf);
//開啟當前這個分片的檔案
FSDataInputStream open = hdfs.open(fileSplit.getPath());
//將這個輸入流【這個小檔案的資料】放入位元組陣列中
byte[] bytes = new byte[(int) fileSplit.getLength()];
IOUtils.readFully(open,bytes,0, (int) fileSplit.getLength());
//給Value賦值:讓Value得到這個檔案的所有資料
this.value.set(bytes,0, (int) fileSplit.getLength());
//關閉資源
open.close();
hdfs.close();
//修改標記
flag = true;
//返回
return true;
}
//第二次返回false,表示整個分片讀取完畢
return false;
}
//返回Key
@Override
public NullWritable getCurrentKey() throws IOException, InterruptedException {
return key;
}
//返回Value
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return value;
}
//獲取進度方法
@Override
public float getProgress() throws IOException, InterruptedException {
return 0;
}
//用於關閉釋放資源
@Override
public void close() throws IOException {
}
}
- MR
package bigdata.hanjiaxiaozhi.cn.mr.input;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import java.io.IOException;
/**
* @ClassName MRDriver
* @Description TODO 實現將多個小檔案合併為一個SequenceFIle
* @Date 2020/5/30 10:34
* @Create By hanjiaxiaozhi
*/
public class MRUserInput extends Configured implements Tool {
/**
* 用於將Job的程式碼封裝
* @param args
* @return
* @throws Exception
*/
@Override
public int run(String[] args) throws Exception {
//todo:1-構建一個Job
Job job = Job.getInstance(this.getConf(),"model");//構建Job物件,呼叫父類的getconf獲取屬性的配置
job.setJarByClass(MRUserInput.class);//指定可以執行的型別
//todo:2-配置這個Job
//input
job.setInputFormatClass(MRUserInputFormat.class);//設定輸入的類的型別,預設就是TextInputFormat
Path inputPath = new Path("datas/inputformat");//用程式的第一個引數做為第一個輸入路徑
//設定的路徑可以給目錄,也可以給定檔案,如果給定目錄,會將目錄中所有檔案作為輸入,但是目錄中不能包含子目錄
MRUserInputFormat.setInputPaths(job,inputPath);//為當前job設定輸入的路徑
//map
job.setMapperClass(MRMapper.class);//設定Mapper的類,需要呼叫對應的map方法
job.setMapOutputKeyClass(Text.class);//設定Mapper輸出的key型別
job.setMapOutputValueClass(BytesWritable.class);//設定Mapper輸出的value型別
//shuffle
// job.setPartitionerClass(HashPartitioner.class);//自定義分割槽
// job.setGroupingComparatorClass(null);//自定義分組的方式
// job.setSortComparatorClass(null);//自定義排序的方式
//reduce
// job.setReducerClass(MRReducer.class);//設定Reduce的類,需要呼叫對應的reduce方法
job.setOutputKeyClass(Text.class);//檔名
job.setOutputValueClass(BytesWritable.class);//檔案內容
job.setNumReduceTasks(1);//設定ReduceTask的個數,預設為1
//output:輸出目錄預設不能提前存在
job.setOutputFormatClass(SequenceFileOutputFormat.class);//設定輸出的類,預設我誒TextOutputFormat
Path outputPath = new Path("datas/output/inputformat");//用程式的第三個引數作為輸出
//解決輸出目錄提前存在,不能執行的問題,提前將目前刪掉
//構建一個HDFS的檔案系統
FileSystem hdfs = FileSystem.get(this.getConf());
//判斷輸出目錄是否存在,如果存在就刪除
if(hdfs.exists(outputPath)){
hdfs.delete(outputPath,true);
}
SequenceFileOutputFormat.setOutputPath(job,outputPath);//為當前Job設定輸出的路徑
//todo:3-提交執行Job
return job.waitForCompletion(true) ? 0:-1;
}
/**
* 程式的入口,呼叫run方法
* @param args
*/
public static void main(String[] args) throws Exception {
//構建一個Configuration物件,用於管理這個程式所有配置,工作會定義很多自己的配置
Configuration conf = new Configuration();
//t通過Toolruner的run方法呼叫當前類的run方法
int status = ToolRunner.run(conf, new MRUserInput(), args);
//退出程式
System.exit(status);
}
/**
* @ClassName MRMapper
* @Description TODO 這是MapReduce模板的Map類
* 輸入的KV型別:由inputformat決定,預設是TextInputFormat
* 輸出的KV型別:由map方法中誰作為key,誰作為Value決定
*/
public static class MRMapper extends Mapper<NullWritable, BytesWritable, Text,BytesWritable> {
Text outputKey = new Text();
/**
* 通過自定義的InputFormat返回的型別
* @param key
* @param value:每一個Value,就是每一個檔案的所有內容
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void map(NullWritable key, BytesWritable value, Context context) throws IOException, InterruptedException {
//檔名作為Key
FileSplit fileSplit = (FileSplit) context.getInputSplit();
//獲取這條資料對應的檔名
String name = fileSplit.getPath().getName();
this.outputKey.set(name);
//檔案的內容作為value
//輸出
context.write(this.outputKey,value);
}
}
/**
* @ClassName MRReducer
* @Description TODO MapReduce模板的Reducer的類
* 輸入的KV型別:由Map的輸出決定,保持一致
* 輸出的KV型別:由reduce方法中誰作為key,誰作為Value決定
*/
public static class MRReducer extends Reducer<NullWritable,NullWritable,NullWritable,NullWritable> {
@Override
protected void reduce(NullWritable key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
/**
* 實現reduce處理的邏輯
*/
}
}
}
5、自定義一個Output
- 需求:將給定的資料按照差評和好評拆分到不同的檔案中
- 自定義分割槽能不能實現?
- 可以
- 自定義OutputFormat
package bigdata.hanjiaxiaozhi.cn.mr.output;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
* @ClassName MRUserOutputFormat
* @Description TODO 自定義一個輸出的類
* @Date 2020/6/2 16:16
* @Create By hanjiaxiaozhi
*/
public class MRUserOutputFormat extends FileOutputFormat<Text, NullWritable> {
/**
* 返回一個輸出器物件
* @param context
* @return
* @throws IOException
* @throws InterruptedException
*/
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext context) throws IOException, InterruptedException {
//構建HDFS物件
FileSystem hdfs = FileSystem.get(context.getConfiguration());
//構建兩個輸出流
FSDataOutputStream badContent = hdfs.create(new Path("datas/output/badcontent/bad.txt"));
FSDataOutputStream goodContent = hdfs.create(new Path("datas/output/goodcontent/good.txt"));
//構建輸出器物件
MrUserRecordWriter mrUserRecordWriter = new MrUserRecordWriter(badContent,goodContent);
return mrUserRecordWriter;
}
}
- 自定義一個輸出器:RecordWrite
package bigdata.hanjiaxiaozhi.cn.mr.output;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
/**
* @ClassName MrUserRecordWriter
* @Description TODO 負責真正實現將資料進行儲存
* @Date 2020/6/2 16:17
* @Create By hanjiaxiaozhi
*/
public class MrUserRecordWriter extends RecordWriter<Text, NullWritable> {
FSDataOutputStream badContent = null;
FSDataOutputStream goodContent = null;
public MrUserRecordWriter(FSDataOutputStream badContent, FSDataOutputStream goodContent) {
this.badContent = badContent;
this.goodContent = goodContent;
}
/**
* 真正負責將每條keyvalue輸出的方法
* 只需要在這個方法中構建輸出的API,將每條KeyValue輸出即可
* @param key
* @param value
* @throws IOException
* @throws InterruptedException
*/
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
//獲取資料中的評價
String content = key.toString().split("\t")[9];
//如果是差評,就寫入一個檔案
if("2".equals(content)){
badContent.write(key.toString().getBytes());
badContent.write("\r\n".getBytes());//新增換行
}else {
//如果不是差評,寫入另外一個檔案
goodContent.write(key.toString().getBytes());
goodContent.write("\r\n".getBytes());
}
}
/**
* 釋放資源的方法,最後執行的方法
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
badContent.close();
goodContent.close();
}
}
- MR實現
package bigdata.hanjiaxiaozhi.cn.mr.output;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import java.io.IOException;
/**
* @ClassName MRDriver
* @Description TODO 實現使用者自定義輸出
* @Date 2020/5/30 10:34
* @Create By hanjiaxiaozhi
*/
public class MRUserOutput extends Configured implements Tool {
/**
* 用於將Job的程式碼封裝
* @param args
* @return
* @throws Exception
*/
@Override
public int run(String[] args) throws Exception {
//todo:1-構建一個Job
Job job = Job.getInstance(this.getConf(),"model");//構建Job物件,呼叫父類的getconf獲取屬性的配置
job.setJarByClass(MRUserOutput.class);//指定可以執行的型別
//todo:2-配置這個Job
//input
// job.setInputFormatClass(TextInputFormat.class);//設定輸入的類的型別,預設就是TextInputFormat
Path inputPath = new Path("datas/outputformat/ordercomment.csv");//用程式的第一個引數做為第一個輸入路徑
//設定的路徑可以給目錄,也可以給定檔案,如果給定目錄,會將目錄中所有檔案作為輸入,但是目錄中不能包含子目錄
TextInputFormat.setInputPaths(job,inputPath);//為當前job設定輸入的路徑
//map
job.setMapperClass(MRMapper.class);//設定Mapper的類,需要呼叫對應的map方法
job.setMapOutputKeyClass(Text.class);//設定Mapper輸出的key型別
job.setMapOutputValueClass(NullWritable.class);//設定Mapper輸出的value型別
//shuffle
// job.setPartitionerClass(HashPartitioner.class);//自定義分割槽
// job.setGroupingComparatorClass(null);//自定義分組的方式
// job.setSortComparatorClass(null);//自定義排序的方式
//reduce
job.setReducerClass(MRReducer.class);//設定Reduce的類,需要呼叫對應的reduce方法
job.setOutputKeyClass(Text.class);//設定Reduce輸出的Key型別
job.setOutputValueClass(NullWritable.class);//設定Reduce輸出的Value型別
job.setNumReduceTasks(1);//設定ReduceTask的個數,預設為1
//output:輸出目錄預設不能提前存在
job.setOutputFormatClass(MRUserOutputFormat.class);//設定輸出的類,預設我誒TextOutputFormat
Path outputPath = new Path("datas/output/outputformat");//用程式的第三個引數作為輸出
//解決輸出目錄提前存在,不能執行的問題,提前將目前刪掉
//構建一個HDFS的檔案系統
FileSystem hdfs = FileSystem.get(this.getConf());
//判斷輸出目錄是否存在,如果存在就刪除
if(hdfs.exists(outputPath)){
hdfs.delete(outputPath,true);
}
MRUserOutputFormat.setOutputPath(job,outputPath);//為當前Job設定輸出的路徑
//todo:3-提交執行Job
return job.waitForCompletion(true) ? 0:-1;
}
/**
* 程式的入口,呼叫run方法
* @param args
*/
public static void main(String[] args) throws Exception {
//構建一個Configuration物件,用於管理這個程式所有配置,工作會定義很多自己的配置
Configuration conf = new Configuration();
//t通過Toolruner的run方法呼叫當前類的run方法
int status = ToolRunner.run(conf, new MRUserOutput(), args);
//退出程式
System.exit(status);
}
/**
* @ClassName MRMapper
* @Description TODO 這是MapReduce模板的Map類
* 輸入的KV型別:由inputformat決定,預設是TextInputFormat
* 輸出的KV型別:由map方法中誰作為key,誰作為Value決定
*/
public static class MRMapper extends Mapper<LongWritable, Text, Text,NullWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//將每一行的每條資料作為Key
context.write(value,NullWritable.get());
}
}
/**
* @ClassName MRReducer
* @Description TODO MapReduce模板的Reducer的類
* 輸入的KV型別:由Map的輸出決定,保持一致
* 輸出的KV型別:由reduce方法中誰作為key,誰作為Value決定
*/
public static class MRReducer extends Reducer<Text,NullWritable,Text,NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
context.write(key,NullWritable.get());
}
}
}
相關文章
- canvas非零繞組規則與奇偶規則Canvas
- id與class 命名規則
- Mysql-基本的規則與規範MySql
- 矛盾與規則的結算
- 配置ModSecurity防火牆與OWASP規則防火牆
- 規則
- 【原始碼解析】AsyncTask的用法與規則原始碼
- 開放封閉原則與規則引擎設計模式 - devgenius設計模式dev
- 正則匹配規則2
- 設計模式 基本規範與基本原則設計模式
- javascript ==與!=的比較規則(加踩坑)JavaScript
- Drools與動態載入規則檔案
- 普通函式與函式模板呼叫規則函式
- 規則引擎與ML模型的比較 - xLaszlo模型
- 學習Sass 巢狀規則與屬性巢狀
- 前端工程程式碼規範(一)——命名規則與工程約定前端
- ESlint規則EsLint
- url規則
- makefile規則
- 規則引擎與機器學習比較與結合機器學習
- 遊戲規則的制訂者就真的懂得“規則”與“樂趣”間的關係嗎?遊戲
- 正則匹配規則記錄
- 人工智慧配色系列(一)方案與規則人工智慧
- 核範數與規則項引數選擇
- 普通函式與函式模板呼叫規則2函式
- [C++]變數宣告與定義的規則C++變數
- Python小白必備:字串基礎,規則與案例Python字串
- Structured OutputStruct
- git提交規則Git
- 1、基本規則
- 任務規則
- firewalld:direct規則
- URule規則引擎
- IT職場規則
- 正規表示式基本規則
- C++程式設計規範-101條規則準則與最佳實踐電子書pdf下載C++程式設計
- 萬智牌規則設計:法術與瞬間
- 如何權衡業務規則的遵守與違反?