使用正则表达式解析SQL语句

代码:

本文乃原创,转载请注明出处。

先看要解析的样例SQL语句:

select   *   from  dual
SELECT   *   frOm  dual
Select  C1,c2  From  tb
select  c1,c2  from  tb
select   count ( * from  t1
select  c1,c2,c3  from  t1  where  condi1 = 1  
Select  c1,c2,c3  From  t1  Where  condi1 = 1  
select  c1,c2,c3  from  t1,t2  where  condi3 = 3   or  condi4 = 5   order     by  o1,o2
Select  c1,c2,c3  from  t1,t2  Where  condi3 = 3   or  condi4 = 5   Order     by  o1,o2
select  c1,c2,c3  from  t1,t2,t3  where  condi1 = 5   and  condi6 = 6   or  condi7 = 7   group    by  g1,g2
Select  c1,c2,c3  From  t1,t2,t3  Where  condi1 = 5   and  condi6 = 6   or  condi7 = 7   Group    by  g1,g2
Select  c1,c2,c3  From  t1,t2,t3  Where  condi1 = 5   and  condi6 = 6   or  condi7 = 7   Group    by  g1,g2,g3  order    by  g2,g3


解析效果之一(isSingleLine=false):

原SQL为select  *   from  dual
解析后的SQL为
select
     
*   
from
     dual

原SQL为SELECT 
*   frOm  dual
解析后的SQL为
select
     
*   
from
     dual

原SQL为Select C1,c2 
From  tb
解析后的SQL为
select
     C1,c2  
from
     tb

原SQL为select c1,c2 
from  tb
解析后的SQL为
select
     c1,c2  
from
     tb

原SQL为select 
count ( * from  t1
解析后的SQL为
select
     
count ( * )  
from
     t1

原SQL为select c1,c2,c3 
from  t1  where  condi1 = 1
解析后的SQL为
select
     c1,c2,c3  
from
     t1  
where
     condi1
= 1

原SQL为Select c1,c2,c3 
From  t1  Where  condi1 = 1
解析后的SQL为
select
     c1,c2,c3  
from
     t1  
where
     condi1
= 1

原SQL为select c1,c2,c3 
from  t1,t2  where  condi3 = 3   or  condi4 = 5   order     by  o1,o2
解析后的SQL为
select
     c1,c2,c3  
from
     t1,t2  
where
     condi3
= 3   or  condi4 = 5   
order   by
     o1,o2

原SQL为Select c1,c2,c3 
from  t1,t2  Where  condi3 = 3   or  condi4 = 5   Order     by  o1,o2
解析后的SQL为
select
     c1,c2,c3  
from
     t1,t2  
where
     condi3
= 3   or  condi4 = 5   
order   by
     o1,o2

原SQL为select c1,c2,c3 
from  t1,t2,t3  where  condi1 = 5   and  condi6 = 6   or  condi7 = 7   group    by  g1,g2
解析后的SQL为
select
     c1,c2,c3  
from
     t1,t2,t3  
where
     condi1
= 5   and  condi6 = 6   or  condi7 = 7   
group   by
     g1,g2

原SQL为Select c1,c2,c3 
From  t1,t2,t3  Where  condi1 = 5   and  condi6 = 6   or  condi7 = 7   Group    by  g1,g2
解析后的SQL为
select
     c1,c2,c3  
from
     t1,t2,t3  
where
     condi1
= 5   and  condi6 = 6   or  condi7 = 7   
group   by
     g1,g2

原SQL为Select c1,c2,c3 
From  t1,t2,t3  Where  condi1 = 5   and  condi6 = 6   or  condi7 = 7   Group    by  g1,g2,g3  order    by  g2,g3
解析后的SQL为
select
     c1,c2,c3  
from
     t1,t2,t3  
where
     condi1
= 5   and  condi6 = 6   or  condi7 = 7   
group   by
     g1,g2,g3  
order   by
     g2,g3


解析效果之二(isSingleLine=true):

原SQL为select  *   from  dual
解析后的SQL为
select
     
*   
from
     dual

原SQL为SELECT 
*   frOm  dual
解析后的SQL为
select
     
*   
from
     dual

原SQL为Select C1,c2 
From  tb
解析后的SQL为
select
     C1,
     c2  
from
     tb

原SQL为select c1,c2 
from  tb
解析后的SQL为
select
     c1,
     c2  
from
     tb

原SQL为select 
count ( * from  t1
解析后的SQL为
select
     
count ( * )  
from
     t1

原SQL为select c1,c2,c3 
from  t1  where  condi1 = 1
解析后的SQL为
select
     c1,
     c2,
     c3  
from
     t1  
where
     condi1
= 1

原SQL为Select c1,c2,c3 
From  t1  Where  condi1 = 1
解析后的SQL为
select
     c1,
     c2,
     c3  
from
     t1  
where
     condi1
= 1

原SQL为select c1,c2,c3 
from  t1,t2  where  condi3 = 3   or  condi4 = 5   order     by  o1,o2
解析后的SQL为
select
     c1,
     c2,
     c3  
from
     t1,
     t2  
where
     condi3
= 3   or
      condi4
= 5   
order   by
     o1,
     o2

原SQL为Select c1,c2,c3 
from  t1,t2  Where  condi3 = 3   or  condi4 = 5   Order     by  o1,o2
解析后的SQL为
select
     c1,
     c2,
     c3  
from
     t1,
     t2  
where
     condi3
= 3   or
      condi4
= 5   
order   by
     o1,
     o2

原SQL为select c1,c2,c3 
from  t1,t2,t3  where  condi1 = 5   and  condi6 = 6   or  condi7 = 7   group    by  g1,g2
解析后的SQL为
select
     c1,
     c2,
     c3  
from
     t1,
     t2,
     t3  
where
     condi1
= 5   and
      condi6
= 6   or
      condi7
= 7   
group   by
     g1,
     g2

原SQL为Select c1,c2,c3 
From  t1,t2,t3  Where  condi1 = 5   and  condi6 = 6   or  condi7 = 7   Group    by  g1,g2
解析后的SQL为
select
     c1,
     c2,
     c3  
from
     t1,
     t2,
     t3  
where
     condi1
= 5   and
      condi6
= 6   or
      condi7
= 7   
group   by
     g1,
     g2

原SQL为Select c1,c2,c3 
From  t1,t2,t3  Where  condi1 = 5   and  condi6 = 6   or  condi7 = 7   Group    by  g1,g2,g3  order    by  g2,g3
解析后的SQL为
select
     c1,
     c2,
     c3  
from
     t1,
     t2,
     t3  
where
     condi1
= 5   and
      condi6
= 6   or
      condi7
= 7   
group   by
     g1,
     g2,
     g3  
order   by
     g2,
     g3


使用的类SqlParser,你可以拷贝下来使用之:

package  com.sitinspring.common.sqlFormatter;

import  java.util.ArrayList;
import  java.util.List;
import  java.util.regex.Matcher;
import  java.util.regex.Pattern;

/**
 * SQL语句解析器类
 * 
@author : sitinspring(junglesong@gmail.com)
 * @date: 2008-3-12
 
*/
public   class  SqlParser{
    
/**
     * 逗号
     
*/
    
private   static   final  String Comma  =   " , " ;
    
    
/**
     * 四个空格
     
*/
    
private   static   final  String FourSpace  =   "      " ;
    
    
/**
     * 是否单行显示字段,表,条件的标识量
     
*/
    
private   static   boolean  isSingleLine = true ;
    
    
/**
     * 待解析的SQL语句
     
*/
    
private  String sql;
    
    
/**
     * SQL中选择的列
     
*/
    
private  String cols;
    
    
/**
     * SQL中查找的表
     
*/
    
private  String tables;
    
    
/**
     * 查找条件
     
*/
    
private  String conditions;
    
    
/**
     * Group By的字段
     
*/
    
private  String groupCols;
    
    
/**
     * Order by的字段
     
*/
    
private  String orderCols;
    
    
/**
     * 构造函数
     * 功能:传入构造函数,解析成字段,表,条件等
     * 
@param  sql:传入的SQL语句
     
*/
    
public  SqlParser(String sql){
        
this .sql = sql.trim();
        
        parseCols();
        parseTables();
        parseConditions();
        parseGroupCols();
        parseOrderCols();
    }
    
    
/**
     * 解析选择的列
     *
     
*/
    
private   void  parseCols(){
        String regex
= " (select)(.+)(from) " ;   
        cols
= getMatchedString(regex,sql);
    }
    
    
/**
     * 解析选择的表
     *
     
*/
    
private   void  parseTables(){
        String regex
= "" ;   
        
        
if (isContains(sql, " //s+where//s+ " )){
            regex
= " (from)(.+)(where) " ;   
        }
        
else {
            regex
= " (from)(.+)($) " ;   
        }
        
        tables
= getMatchedString(regex,sql);
    }
    
    
/**
     * 解析查找条件
     *
     
*/
    
private   void  parseConditions(){
        String regex
= "" ;   
        
        
if (isContains(sql, " //s+where//s+ " )){
            
//  包括Where,有条件
            
            
if (isContains(sql, " group//s+by " )){
                
//  条件在where和group by之间
                regex = " (where)(.+)(group//s+by) " ;  
            }
            
else   if (isContains(sql, " order//s+by " )){
                
//  条件在where和order by之间
                regex = " (where)(.+)(order//s+by) " ;  
            }
            
else {
                
//  条件在where到字符串末尾
                regex = " (where)(.+)($) " ;  
            }             
        }
        
else {
            
//  不包括where则条件无从谈起,返回即可
             return ;
        }
        
        conditions
= getMatchedString(regex,sql);
    }
    
    
/**
     * 解析GroupBy的字段
     *
     
*/
    
private   void  parseGroupCols(){
        String regex
= "" ;   
        
        
if (isContains(sql, " group//s+by " )){
            
//  包括GroupBy,有分组字段

            
if (isContains(sql, " order//s+by " )){
                
//  group by 后有order by
                regex = " (group//s+by)(.+)(order//s+by) " ;  
            }
            
else {
                
//  group by 后无order by
                regex = " (group//s+by)(.+)($) " ;  
            }           
        }
        
else {
            
//  不包括GroupBy则分组字段无从谈起,返回即可
             return ;
        }
        
        groupCols
= getMatchedString(regex,sql);
    }
    
    
/**
     * 解析OrderBy的字段
     *
     
*/
    
private   void  parseOrderCols(){
        String regex
= "" ;   
        
        
if (isContains(sql, " order//s+by " )){
            
//  包括GroupBy,有分组字段
            regex = " (order//s+by)(.+)($) " ;                           
        }
        
else {
            
//  不包括GroupBy则分组字段无从谈起,返回即可
             return ;
        }
            
        orderCols
= getMatchedString(regex,sql);
    }
    
    
    
/**
     * 从文本text中找到regex首次匹配的字符串,不区分大小写
     * 
@param  regex: 正则表达式
     * 
@param  text:欲查找的字符串
     * 
@return  regex首次匹配的字符串,如未匹配返回空
     
*/
    
private   static  String getMatchedString(String regex,String text){
        Pattern pattern
= Pattern.compile(regex,Pattern.CASE_INSENSITIVE);
        
        Matcher matcher
= pattern.matcher(text);
 
        
while (matcher.find()){
            
return  matcher.group( 2 );
        }
        
        
return   null ;
    }
    
    
/**
     * 看word是否在lineText中存在,支持正则表达式
     * 
@param  lineText
     * 
@param  word
     * 
@return
     
*/
    
private   static   boolean  isContains(String lineText,String word){
        Pattern pattern
= Pattern.compile(word,Pattern.CASE_INSENSITIVE);
        Matcher matcher
= pattern.matcher(lineText);
        
return  matcher.find();
    }
    
    
    
public  String toString(){        
        
//  无法解析则原样返回
         if (cols == null   &&  tables == null   &&  conditions == null   &&  groupCols == null   &&  orderCols == null  ){
            
return  sql;
        }
        
        StringBuffer sb
= new  StringBuffer();
        sb.append(
" 原SQL为 " + sql + " /n " );
        sb.append(
" 解析后的SQL为/n " );
        
        
        
for (String str:getParsedSqlList()){
            sb.append(str);
        }
        
        sb.append(
" /n " );
        
        
return  sb.toString();
    }
    
    
/**
     * 在分隔符后加上回车
     * 
@param  str
     * 
@param  splitStr
     * 
@return
     
*/
    
private   static  String getAddEnterStr(String str,String splitStr){
        Pattern p 
=  Pattern.compile(splitStr,Pattern.CASE_INSENSITIVE);

        
//  用Pattern类的matcher()方法生成一个Matcher对象
        Matcher m  =  p.matcher(str);
        StringBuffer sb 
=   new  StringBuffer();

        
//  使用find()方法查找第一个匹配的对象
         boolean  result  =  m.find();

        
//  使用循环找出模式匹配的内容替换之,再将内容加到sb里
         while  (result) {
            m.appendReplacement(sb, m.group(
0 +   " /n      " );
            result 
=  m.find();
        }
        
//  最后调用appendTail()方法将最后一次匹配后的剩余字符串加到sb里;
        m.appendTail(sb);
        
        
return  FourSpace + sb.toString();
    }
    
    
/**
     * 取得解析的SQL字符串列表
     * 
@return
     
*/
    
public  List < String >  getParsedSqlList(){
        List
< String >  sqlList = new  ArrayList < String > ();
        
        
//  无法解析则原样返回
         if (cols == null   &&  tables == null   &&  conditions == null   &&  groupCols == null   &&  orderCols == null  ){
            sqlList.add(sql);
            
return  sqlList;
        }
        
        
if (cols != null ){
            sqlList.add(
" select/n " );
            
if (isSingleLine){
                sqlList.add(getAddEnterStr(cols,Comma));
            }
            
else {
                sqlList.add(FourSpace
+ cols);
            }
        }
        
        
if (tables != null ){
            sqlList.add(
"  /nfrom/n " );

            
if (isSingleLine){
                sqlList.add(getAddEnterStr(tables,Comma));
            }
            
else {
                sqlList.add(FourSpace
+ tables);
            }
        }
        
        
if (conditions != null ){
            sqlList.add(
"  /nwhere/n " );

            
if (isSingleLine){
                sqlList.add(getAddEnterStr(conditions,
" (and|or) " ));
            }
            
else {
                sqlList.add(FourSpace
+ conditions);
            }
        }
        
        
if (groupCols != null ){
            sqlList.add(
"  /ngroup by/n " );

            
if (isSingleLine){
                sqlList.add(getAddEnterStr(groupCols,Comma));
            }
            
else {
                sqlList.add(FourSpace
+ groupCols);
            }
        }
        
        
if (orderCols != null ){
            sqlList.add(
"  /norder by/n " );

            
if (isSingleLine){
                sqlList.add(getAddEnterStr(orderCols,Comma));
            }
            
else {
                sqlList.add(FourSpace
+ orderCols);
            }
        }
        
        
return  sqlList;
    }
    
    
/**
     * 设置是否单行显示表,字段,条件等
     * 
@param  isSingleLine
     
*/
    
public   static   void  setSingleLine( boolean  isSingleLine) {
        SqlParser.isSingleLine 
=  isSingleLine;
    }
    
    
/**
     * 测试
     * 
@param  args
     
*/
    
public   static   void  main(String[] args){
        List
< String >  ls = new  ArrayList < String > ();
        ls.add(
" select * from dual " );    
        ls.add(
" SELECT * frOm dual " );
        ls.add(
" Select C1,c2 From tb " );
        ls.add(
" select c1,c2 from tb " );
        ls.add(
" select count(*) from t1 " );
        ls.add(
" select c1,c2,c3 from t1 where condi1=1  " );
        ls.add(
" Select c1,c2,c3 From t1 Where condi1=1  " );
        ls.add(
" select c1,c2,c3 from t1,t2 where condi3=3 or condi4=5 order   by o1,o2 " );
        ls.add(
" Select c1,c2,c3 from t1,t2 Where condi3=3 or condi4=5 Order   by o1,o2 " );
        ls.add(
" select c1,c2,c3 from t1,t2,t3 where condi1=5 and condi6=6 or condi7=7 group  by g1,g2 " );
        ls.add(
" Select c1,c2,c3 From t1,t2,t3 Where condi1=5 and condi6=6 or condi7=7 Group  by g1,g2 " );
        ls.add(
" Select c1,c2,c3 From t1,t2,t3 Where condi1=5 and condi6=6 or condi7=7 Group  by g1,g2,g3 order  by g2,g3 " );
        
        
for (String sql:ls){
            System.out.println(
new  SqlParser(sql));
            
// System.out.println(sql);
        }
    }
}

 

分析:

问题:将左边的SQL语句解析成右边的形式

Select c1,c2,c3 From t1,t2,t3 Where condi1=5 and condi6=6 or condi7=7 Group  by g1,g2,g3 order  by g2,g3

select
     c1,
    c2,
    c3
from
     t1,
    t2,
    t3
where
     condi1=5 and
     condi6=6 or
     condi7=7
group by
     g1,
    g2,
    g3
order by
     g2,
    g3

按关键字找出SQL语句中各部分

我们阅读SQL语句会把整句分来成列,表,条件,分组字段,排序字段来理解,解析SQL的目的也是这样.
分解SQL语句有规律可循,以列为例,它必定包含在select和from之间,我们只要能找到SQL语句中的关键字select和from,就能找到查询的列.
怎么找到select和from之间的文字呢?其实一个正则表达式就能解决:(select)(.+)(from),其中第二组(.+)代表的文字就是select和from之间的文字.
程序见右边.

/**
 * 从文本text中找到regex首次匹配的字符串,不区分大小写
 * @param regex: 正则表达式
 * @param text:欲查找的字符串
 * @return regex首次匹配的字符串,如未匹配返回空
 */
private static String getMatchedString(String regex,String text){
  Pattern pattern=Pattern.compile(regex,Pattern.CASE_INSENSITIVE);
   
    Matcher matcher=pattern.matcher(text);

    while(matcher.find()){
      return matcher.group(2);
    }
   
    return null;
}

解析函数分析

private static String getMatchedString(String regex,String text){
  Pattern pattern=Pattern.compile(regex,Pattern.CASE_INSENSITIVE);
   
    Matcher matcher=pattern.matcher(text);

    while(matcher.find()){
      return matcher.group(2);
    }
   
    return null;
}

左边的这个函数,第一个参数是拟定的正则表达式,第二个是整个SQL语句.
当正则表达式为(select)(.+)(from)时,程序将在SQL中查找第一次匹配的地方(有Pattern.CASE_INSENSITIVE的设置,查找不区分大小写),如果找到了则返回模式中的第二组代表的文字.
如果sql是select a,b from tc,则返回的文字是a,b.

选择的表对应的查找正则表达式

选择的表比较特殊,它不想选择的列一样固定处于select和from之间,当没有查找条件存在时,它处于from和结束之间;当有查找条件存在时,它处于from和where之间.
因此查询函数写为右边的形式:

/**
 * 解析选择的表
 *
 */
private void parseTables(){
    String regex="";  
   
    if(isContains(sql,"//s+where//s+")){
      regex="(from)(.+)(where)";  
    }
    else{
      regex="(from)(.+)($)";  
    }
   
    tables=getMatchedString(regex,sql);
}


isContains函数

isContains函数用于在lineText中查找word,其中不区分大小些,只要找到了即返回真.

/**
 * 看word是否在lineText中存在,支持正则表达式
 * @param lineText
 * @param word
 * @return
 */
private static boolean isContains(String lineText,String word){
  Pattern pattern=Pattern.compile(word,Pattern.CASE_INSENSITIVE);
  Matcher matcher=pattern.matcher(lineText);
  return matcher.find();
}

解析查找条件的函数

private void parseConditions(){
    String regex="";  
   
    if(isContains(sql,"//s+where//s+")){
      // 包括Where,有条件
     
      if(isContains(sql,"group//s+by")){
        // 条件在where和group by之间
        regex="(where)(.+)(group//s+by)"; 
      }
      else if(isContains(sql,"order//s+by")){
        // 条件在where和order by之间
        regex="(where)(.+)(order//s+by)"; 
      }
      else{
        // 条件在where到字符串末尾
        regex="(where)(.+)($)"; 
      }       
    }
    else{
      // 不包括where则条件无从谈起,返回即可
      return;
    }
   
    conditions=getMatchedString(regex,sql);
}

解析GroupBy的字段

private void parseGroupCols(){
    String regex="";  
   
    if(isContains(sql,"group//s+by")){
      // 包括GroupBy,有分组字段

      if(isContains(sql,"order//s+by")){
        // group by 后有order by
        regex="(group//s+by)(.+)(order//s+by)"; 
      }
      else{
        // group by 后无order by
        regex="(group//s+by)(.+)($)"; 
      }     
    }
    else{
      // 不包括GroupBy则分组字段无从谈起,返回即可
      return;
    }
   
    groupCols=getMatchedString(regex,sql);
}


解析OrderBy的字段

private void parseOrderCols(){
    String regex="";  
   
    if(isContains(sql,"order//s+by")){
      // 包括order by,有分组字段
      regex="(order//s+by)(.+)($)";                 
    }
    else{
      // 不包括GroupBy则分组字段无从谈起,返回即可
      return;
    }
     
    orderCols=getMatchedString(regex,sql);
}

得到解析后的各部分

按以上解析方法获得了列,表,条件,分组条件,排序条件各部分之后,它们会存储到各个成员变量中.
注意这些成员变量的原值都是null,如果在SQL语句中能够找到对应的部分的话它们将借助getMatchedString获得值,否则还是null.我们通过判断这些成员变量是否为空就能知道它对应的部分是否被解析出来.

 /**
   * 待解析的SQL语句
   */
  private String sql;
 
  /**
   * SQL中选择的列
   */
  private String cols;
 
  /**
   * SQL中查找的表
   */
  private String tables;
 
  /**
   * 查找条件
   */
  private String conditions;
 
  /**
   * Group By的字段
   */
  private String groupCols;
 
  /**
   * Order by的字段
   */
  private String orderCols;

取得不需要单行显示时的SQL语句

进展到这一步,SQL语句中列,表,条件,分组条件,排序条件各部分都被获取了出来,这时把它们重新组合一下就能得到整理后的SQL语句.
如下面的SQL语句将变成右边的部分(先使静态成员isSingleLine=false):
Select c1,c2,c3 From t1,t2,t3 Where condi1=5 and condi6=6 or condi7=7 Group  by g1,g2,g3 order  by g2,g3

select
     c1,c2,c3
from
     t1,t2,t3
where
     condi1=5 and condi6=6 or condi7=7
group by
     g1,g2,g3
order by
     g2,g3


进一步解析

有时我们需要把列,表,条件,分组条件,排序条件单行显示以方便查看或加上注释,这就要求我们对列,表,条件,分组条件,排序条件等进行进一步解析.
初看解析很方便,以固定的分隔符劈分即可,但需要注意的是查询条件中分隔符有and和or两种,如果贸然分隔会使重新组合时使SQL失真.
推荐一种做法,我们可以在分隔符后加上一个标志如空行,然后再以这个标志来劈分.这样就不会使SQL失真了.
请见下页的getSplitedParagraph函数.

getSplitedParagraph函数

private static List<String> getSplitedParagraph(String paragraph,String splitStr){
  List<String> ls=new ArrayList<String>();   
 
  // 先在分隔符后加空格
  Pattern p = Pattern.compile(splitStr,Pattern.CASE_INSENSITIVE);

  Matcher m = p.matcher(paragraph);
  StringBuffer sb = new StringBuffer();

  boolean result = m.find();
  while (result) {
    m.appendReplacement(sb, m.group(0) + Crlf);
    result = m.find();
  }
  m.appendTail(sb);
 
  // 再按空格断行
  String[] arr=sb.toString().split("[/n]+");
  for(String temp:arr){
    ls.add(FourSpace+temp+Crlf);
  }
 
  return ls;
}

处理结果

把静态成员变量isSingleLine=true后我们来看看执行结果:
select
     c1,
    c2,
    c3
from
     t1,
    t2,
    t3
where
     condi1=5 and
     condi6=6 or
     condi7=7
group by
     g1,
    g2,
    g3
order by
     g2,
    g3

小结

从这个例子中我们体会了分治的思想:分治是把一个大问题分解成小问题,然后分别解决小问题,再组合起来大问题的解决方法就差不多了.这种思想在工程领域解决问题时很普遍,我们要学会使用这种思想来看待,分析和解决问题,不要贪多求大,结果导致在大问题面前一筹莫展.
其次我们可以从这个例子中学习找规律,然后借助规律的过程,现实世界千变万化,但都有规律可循,只要我们找到了规律,就等于找到了事物之门的钥匙.
接下了我们复习了正则表达式用于查找的方法,以前的正则表达式学习多用于验证匹配,其实这只是正则表达式的一部分功能.
最后从解析条件成单行的过程中,我们可以学习到一种解决问题的技巧,即当现实中的规律存在变数时加入人为设置的规律,这有时能使我们更好更快的解决问题.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值