> php教程 > php手册 > 본문

PHP词法解析源码分析之引号及其内容

WBOY
풀어 주다: 2016-06-06 19:42:16
원래의
1144명이 탐색했습니다.

在 词法 解析 的代码中, 引号 里面 内容 的 解析 就占了所有代码的一半以上,可见其情况是比较多的,这次就来简单 分析 一下。由于lex_scan开头的一段整个函数都用得到,所以这里就再贴一下: int lex _scan(zval *zendlval TSRMLS_ DC){//设置当前token的

词法解析的代码中,引号里面内容解析就占了所有代码的一半以上,可见其情况是比较多的,这次就来简单分析一下。由于lex_scan开头的一段整个函数都用得到,所以这里就再贴一下:

<code>int lex<span>_scan(zval *zendlval TSRMLS_</span>DC)
{
//设置当前token的首位置为当前位置
restart:
<span>    SCNG(yy_text) = YYCURSOR;</span>

yymore_restart:
//这段注释定义了各个类型的正则表达式匹配,在<strong>词法</strong><strong>解析</strong>程序(如bison、re2c等)程序将本文件转化为c代码时会用到
/*!re2c
re2c:yyfill:check = 0;
LNUM    [0-9]+
DNUM    ([0-9]<span>*"."[0-9]+)|([0-9]+"."[0-9]*</span>)
EXPONENT_DNUM    (({LNUM}|{DNUM})[<span>eE</span>][<span>+-</span>]?{LNUM})
HNUM    "0x"[0-9a-fA-F]+
BNUM    "0b"[01]+
LABEL    [<span>a-zA-Z_\x7f-\xff</span>][<span>a-zA-Z0-9_\x7f-\xff</span>]*
WHITESPACE [ \n\r\t]+
TABS<span>_AND_</span>SPACES [ \t]*
TOKENS [<span>;:,.\[\</span>]()|^&+-/*=%!~$<span></span>?@]
ANY_CHAR [^]
NEWLINE ("\r"|"\n"|"\r\n")

/<span>* compute yyleng before each rule *</span>/
<span>!*</span>> := yyleng = YYCURSOR - SCNG(yy_text);
</code>
로그인 후 복사

1.引号开始的处理

1.1 单引号的处理

可以看到整个的处理流程看似比较复杂,其实上就是单引号内只会转义诸如'\'、'\''、'\n'等转义字符,并不会对诸如\x65、0123之类的十六进制及八进制数进行转义(这里只是举个例子,更多不转义的情况下面会介绍)。同时它还会保证在只有一个单引号的情况下不会报错,而只是返回T_ENCAPSED_AND_WHITESPACE(当然也没有转义)。

<code><st_in_scripting>b?[<span>'] {
    register char *s, *t;
    char *end;
    int bprefix = (yytext[0] != '</span>\<span>''</span>) ? <span>1</span> : <span>0</span>;

    <span>while</span> (<span>1</span>) {
        <span>if</span> (YYCURSOR if (<span>*YYCURSOR</span> == <span>'\''</span>) {
                YYCURSOR++;
                yyleng = YYCURSOR - SCNG(yy_text);

                <span>break</span>;
            } <span>else</span> <span>if</span> (<span>*YYCURSOR</span>++ == <span>'\\'</span> && YYCURSOR else {
            yyleng = YYLIMIT - SCNG(yy_text);

            <span>/* Unclosed single quotes; treat similar to double quotes, but without a separate token
             * for ' (unrecognized by parser), instead of old flex fallback to "Unexpected character..."
             * rule, which continued in ST_IN_SCRIPTING state after the quote */</span>
            <span>return</span> T_ENCAPSED_AND_WHITESPACE;
        }
    }

    ZVAL_STRINGL(zendlval, yytext+bprefix+<span>1</span>, yyleng-bprefix-<span>2</span>, <span>1</span>);

    <span>/* convert escape sequences */</span>
    <span>s</span> = t = Z_STRVAL_P(zendlval);
    end = <span>s</span>+Z_STRLEN_P(zendlval);
    <span>while</span> (<span>s</span><end>if (<span>*s</span>==<span>'\\'</span>) {
            <span>s</span>++;

            switch(<span>*s</span>) {
                case <span>'\\'</span>:
                case <span>'\''</span>:
                    <span>*t</span>++ = <span>*s</span>;
                    Z_STRLEN_P(zendlval)--;
                    <span>break</span>;
                default:
                    <span>*t</span>++ = <span>'\\'</span>;
                    <span>*t</span>++ = <span>*s</span>;
                    <span>break</span>;
            }
        } <span>else</span> {
            <span>*t</span>++ = <span>*s</span>;
        }

        <span>if</span> (<span>*s</span> == <span>'\n'</span> || (<span>*s</span> == <span>'\r'</span> && (<span>*(</span><span>s</span>+<span>1</span>) != <span>'\n'</span>))) {
            CG(zend_lineno)++;
        }
        <span>s</span>++;
    }
    <span>*t</span> = <span>0</span>;

    <span>if</span> (SCNG(output_filter)) {
        size_t sz = <span>0</span>;
        <span>s</span> = Z_STRVAL_P(zendlval);
        SCNG(output_filter)((unsigned char <span>**</span>)&Z_STRVAL_P(zendlval), &sz, (unsigned char <span>*)</span><span>s</span>, (size_t)Z_STRLEN_P(zendlval) TSRMLS_CC);
        Z_STRLEN_P(zendlval) = sz;
        efree(<span>s</span>);
    }
    <span>return</span> T_CONSTANT_ENCAPSED_STRING;
}
</end></st_in_scripting></code>
로그인 후 복사

1.2 双引号的处理

引号的处理相对单引号来说容易理解很多。在ST_IN_SCRIPTING状态下遇到双引号开始往后扫描,若是遇到了另一个双引号则直接调用zend_scan_escape_string转义里面的字符串并进行保存然后返回T_CONSTANT_ENCAPSED_STRING表明这是一个常量字符串;若是遇到了诸如'$LABEL','${','{$'这三种情况(字符串中的变量)则停止扫描,保存当前已经扫描的字符并将当前状态改为ST_DOUBLE_QUOTES表明正处于双引号字符串中,然后只返回一个'"'并进入下一轮的扫描;若是遇到了'\'则跳过该字符也进入下一轮处理:

<code><st_in_scripting>b?[<span>"] {
    int bprefix = (yytext[0] != '"</span><span>') ? 1 : 0;

    while (YYCURSOR <span>"':
                yyleng = YYCURSOR - SCNG(yy_text);
                zend_scan_escape_string(zendlval, yytext+bprefix+1, yyleng-bprefix-2, '"</span><span>' TSRMLS_CC);
                return T_CONSTANT_ENCAPSED_STRING;
            case '</span><span>$'</span>:
                <span>if</span> (IS_LABEL_START(<span>*YYCURSOR</span>) || <span>*YYCURSOR</span> == <span>'{'</span>) {
                    <span>break</span>;
                }
                <span>continue</span>;
            case <span>'{'</span>:
                <span>if</span> (<span>*YYCURSOR</span> == <span>'$'</span>) {
                    <span>break</span>;
                }
                <span>continue</span>;
            case <span>'\\'</span>:
                <span>if</span> (YYCURSOR */</span>
            default:
                <span>continue</span>;
        }

        YYCURSOR--;
        <span>break</span>;
    }

    /* Remember how much was scanned to save rescanning <span>*/</span>
    SET_DOUBLE_QUOTES_SCANNED_LENGTH(YYCURSOR - SCNG(yy_text) - yyleng);

    YYCURSOR = SCNG(yy_text) + yyleng;

    BEGIN(ST_DOUBLE_QUOTES);
    <span>return</span> <span>'"'</span>;
}
</st_in_scripting></code>
로그인 후 복사

1.3 Heredoc和Nowdoc的处理

可以看到heredoc的形式,首先是'内容时首先把开头的空格和tab字符删去,随后若出现了单引号则将当前状态改为ST_NOWDOC,否则则为ST_HEREDOC。然后就是根据计算出的长度来确定是否到达末尾,如果到了末尾则直接将状态改为ST_END_HEREDOC以便下一次扫描结束heredoc的处理:

<code><st_in_scripting>b?<span>"<span>{TABS_AND_SPACES}</span>(<span>{LABEL}</span>|([<span>']{LABEL}['</span>])|([<span>"]{LABEL}["</span>]))<span>{NEWLINE}</span> {
    char <span>*s</span>;
    <span>int</span> bprefix = (yytext[<span>0</span>] != <span>') ? <span>1</span> : <span>0</span>;
    zend_heredoc_label <span>*heredoc_label</span> = emalloc(sizeof(zend_heredoc_label));

    CG(zend_lineno)++;
    heredoc_label-><span>length</span> = yyleng-bprefix-<span>3</span>-<span>1</span>-(yytext[yyleng-<span>2</span>]==<span>'\r'</span>?<span>1</span>:<span>0</span>);
    <span>s</span> = yytext+bprefix+<span>3</span>;
    <span>while</span> ((<span>*s</span> == <span>' '</span>) || (<span>*s</span> == <span>'\t'</span>)) {
        <span>s</span>++;
        heredoc_label-><span>length</span>--;
    }

    <span>if</span> (<span>*s</span> == <span>'\''</span>) {
        <span>s</span>++;
        heredoc_label-><span>length</span> -= <span>2</span>;

        BEGIN(ST_NOWDOC);
    } <span>else</span> {
        <span>if</span> (<span>*s</span> == <span>'"'</span>) {
            <span>s</span>++;
            heredoc_label-><span>length</span> -= <span>2</span>;
        }

        BEGIN(ST_HEREDOC);
    }

    heredoc_label->label = estrndup(<span>s</span>, heredoc_label-><span>length</span>);

    <span>/* Check for ending label on the next line */</span>
    <span>if</span> (heredoc_label-><span>length</span> s</span>, heredoc_label-><span>length</span>)) {
        YYCTYPE <span>*end</span> = YYCURSOR + heredoc_label-><span>length</span>;

        <span>if</span> (<span>*end</span> == <span>';'</span>) {
            end++;
        }

        <span>if</span> (<span>*end</span> == <span>'\n'</span> || <span>*end</span> == <span>'\r'</span>) {
            BEGIN(ST_END_HEREDOC);
        }
    }

    zend_ptr_stack_push(&SCNG(heredoc_label_stack), (void <span>*)</span> heredoc_label);

    <span>return</span> T_START_HEREDOC;
}
</span></st_in_scripting></code>
로그인 후 복사

1.4 反引号处理

引号处理比较简单,直接将状态改为ST_BACKQUOTE然后返回'`':

<code><st_in_scripting>[`] {
    BEGIN(ST_BACKQUOTE);
    <span>return</span> <span>'`'</span>;
}
</st_in_scripting></code>
로그인 후 복사

2.引号内容的处理

前面了解了在ST_IN_SCRIPTING状态下遇到引号类字符的处理过程,后面便是在引号中的各种处理了。引号的状态包括ST_DOUBLE_QUOTES,ST_BACKQUOTE,ST_HEREDOC和ST_NOWDOC,除了ST_NOWDOC之中的内容无法转义(相当于单引号,只能转义\等极少数的字符)之外,其他几个状态中的内容都是可以进行转义的,至于单引号中的内容也是不需要转义的,这里也没有单独的状态来表示进入单引号,而只是T_ENCAPSED_AND_WHITESPACE表示单引号里面的内容

2.1 引号中的变量

首先是是字符串中有'${'的情况,保存状态之后返回T_DOLLAR_OPEN_CURLY_BRACES表示是'$'与'{'的组合:

<code><st_double_quotes><span>"${"</span> {
    yy_push_state(ST_LOOKING_FOR_VARNAME TSRMLS_CC);
    <span>return</span> T_DOLLAR_OPEN_CURLY_BRACES;
}
</st_double_quotes></code>
로그인 후 복사

在下一次扫描时的状态便会改为ST_LOOKING_FOR_VARNAME,将变量名保存并恢复状态为ST_IN_SCRIPTING:

<code><st_looking_for_varname>{LABEL}[[}] {
    yyless(yyleng - <span>1</span>);
    zend_copy_value(zendlval, yytext, yyleng);
    zendlval-><span>type</span> = IS_STRING;
    yy_pop_state(TSRMLS_C);
    yy_push_state(ST_IN_SCRIPTING TSRMLS_CC);
    <span>return</span> T_STRING_VARNAME;
}
</st_looking_for_varname></code>
로그인 후 복사

然后是匹配'$LABEL':

<code><st_in_scripting><span>"$"</span>{LABEL} {
    zend_copy_value(zendlval, (yytext+<span>1</span>), (yyleng-<span>1</span>));
    zendlval-><span>type</span> = IS_STRING;
    <span>return</span> T_VARIABLE;
}
</st_in_scripting></code>
로그인 후 복사

2.2 引号中的类、结构体

类和结构体的调用形式是'$LABEL->var',本次扫描只会保存LABEL并不会对后面var进行处理,并将状态变为ST_LOOKING_FOR_PROPERTY等待下一次进行处理,至于寻找属性就是上面3.1的集中情况了:

<code>/* Make sure a label character follows <span>"->"</span>, otherwise there <span>is</span> <span>no</span> property
 * <span>and</span> <span>"->"</span> will be taken literally
 */
<st_double_quotes><span>"$"</span>{LABEL}<span>"->"</span>[a-zA-Z_\x7f-\xff] {
    yyless(yyleng - <span>3</span>);
    yy_push_state(ST_LOOKING_FOR_PROPERTY TSRMLS_CC);
    zend_copy_value(zendlval, (yytext+<span>1</span>), (yyleng-<span>1</span>));
    zendlval->type = IS_STRING;
    <span>return</span> T_VARIABLE;
}
</st_double_quotes></code>
로그인 후 복사

2.3 引号中的数组等

数组就是'$LABEL[var]'这个情况,同样本次扫描也不处理var而只是设置状态为ST_VAR_OFFSET等待下一次处理:

<code><span>/* A [ always designates a variable offset, regardless of what follows
 */</span>
<st_double_quotes><span>"$"</span>{LABEL}<span>"["</span> {
    yyless(yyleng - <span>1</span>);
    yy_push_state(ST_VAR_OFFSET TSRMLS_CC);
    zend_copy_value(zendlval, (yytext+<span>1</span>), (yyleng-<span>1</span>));
    zendlval->type = IS_STRING;
    <span>return</span> T_VARIABLE;
}
</st_double_quotes></code>
로그인 후 복사

下一次进行扫描时便会匹配ST_VAR_OFFSET状态的下值,[]中可以是数字,若是十进制数,超过表示范围则直接保存为字符串否则转化为十进制整数,若是八进制、十六进制和二进制数字,都是直接作为字符串处理:

<code><span>ST_VAR_OFFSET</span>>[<span>0</span>]|([<span>1-9</span>][<span>0-9</span>]<span>*) { /*</span> Offset could be treated as a long */
<span>    if (yyleng 
<span>        ZVAL_LONG(zendlval, strtol(yytext, NULL, 10));</span>
<span>    } else {</span>
<span>        ZVAL_STRINGL(zendlval, yytext, yyleng, 1);</span>
<span>    }</span>
<span>    return T_NUM_STRING;</span>
}

<span>ST_VAR_OFFSET</span>></span>{LNUM}|{HNUM}|{BNUM} { /<span>* Offset must be treated as a string *</span>/
<span>    ZVAL_STRINGL(zendlval, yytext, yyleng, 1);</span>
<span>    return T_NUM_STRING;</span>
}
</code>
로그인 후 복사

同样在[]中还可以包括一些运算符以及其他的符号:

<code>//可以包含TOKENS和大括号双<strong>引号</strong>和反<strong>引号</strong>
<span>ST_VAR_OFFSET</span>>{TOKENS}|[{}"`] {
    /* Only '[' can be valid, but returning other tokens will allow a more explicit parse error */
    return yytext[0];
}
//还可以包括空白字符、反斜杠、单<strong>引号</strong>以及#,但这些字符否会被略去
<span>ST_VAR_OFFSET</span>>[ \n\r\t\\'#] {
    /* Invalid rule to return a more explicit parse error with proper line number */
    yyless(0);
    yy_pop_state(TSRMLS_C);
    return T_ENCAPSED_AND_WHITESPACE;
}
</code>
로그인 후 복사

还有就是不带'$'的LABEL,直接作为字符串处理:

<code><st_in_scripting>{LABEL} {
    zend_copy_value(zendlval, yytext, yyleng);
    zendlval-><span>type</span> = IS_STRING;
    <span>return</span> T_STRING;
}
</st_in_scripting></code>
로그인 후 복사

3.引号中ANY_CHAR的处理

当处于ST_DOUBLE_QUOTES、ST_BACKQUOTE等引号状态时,如果没有匹配到第二节的情况便会到匹配ANY_CHAR的流程中。

3.1 双引号ST_DOUBLE_QUOTES

首先是判断有没有已经扫描过的字符,有的话直接保存并重新设置扫描的长度然后返回进行下一次的扫描,如果已扫描字符为空则后面的流程和第一次扫描到双引号时的差不多,都是碰到'\\'跳过碰到'"''\$''{\$'等就停止本次扫描,保存当前扫描过的字符然后返回并进行下一次的扫描,总体流程比较简单:

<code><st_double_quotes>{ANY_CHAR} {
    <span>if</span> (GET_DOUBLE_QUOTES_SCANNED_LENGTH()) {
        YYCURSOR += GET_DOUBLE_QUOTES_SCANNED_LENGTH() - <span>1</span>;
        SET_DOUBLE_QUOTES_SCANNED_LENGTH(<span>0</span>);

        <span>goto</span> double_quotes_scan_done;
    }

    <span>if</span> (YYCURSOR > YYLIMIT) {
        <span>return</span> <span>0</span>;
    }
    <span>if</span> (yytext[<span>0</span>] == <span>'\\'</span> && YYCURSOR while (YYCURSOR switch (*YYCURSOR++) {
            <span>case</span> <span>'"'</span>:
                <span>break</span>;
            <span>case</span> <span>'$'</span>:
                <span>if</span> (IS_LABEL_START(*YYCURSOR) || *YYCURSOR == <span>'{'</span>) {
                    <span>break</span>;
                }
                <span>continue</span>;
            <span>case</span> <span>'{'</span>:
                <span>if</span> (*YYCURSOR == <span>'$'</span>) {
                    <span>break</span>;
                }
                <span>continue</span>;
            <span>case</span> <span>'\\'</span>:
                <span>if</span> (YYCURSOR /* fall through */
            <span>default</span>:
                <span>continue</span>;
        }

        YYCURSOR--;
        <span>break</span>;
    }

double_quotes_scan_done:
    yyleng = YYCURSOR - SCNG(yy_text);

    zend_scan_escape_string(zendlval, yytext, yyleng, <span>'"'</span> TSRMLS_CC);
    <span>return</span> T_ENCAPSED_AND_WHITESPACE;
}
</st_double_quotes></code>
로그인 후 복사

3.2 反引号ST_BACKQUOTE

可以看到除了没有最开始判断已经扫描的长度之外其余的的操作都和双引号几乎相同,所以在词法解析这一层面双引号和反引号可以说非常相似:

<code><st_backquote>{ANY_CHAR} {
    <span>if</span> (YYCURSOR > YYLIMIT) {
        <span>return</span> <span>0</span>;
    }
    <span>if</span> (yytext[<span>0</span>] == <span>'\\'</span> && YYCURSOR while (YYCURSOR switch (*YYCURSOR++) {
            <span>case</span> <span>'`'</span>:
                <span>break</span>;
            <span>case</span> <span>'$'</span>:
                <span>if</span> (IS_LABEL_START(*YYCURSOR) || *YYCURSOR == <span>'{'</span>) {
                    <span>break</span>;
                }
                <span>continue</span>;
            <span>case</span> <span>'{'</span>:
                <span>if</span> (*YYCURSOR == <span>'$'</span>) {
                    <span>break</span>;
                }
                <span>continue</span>;
            <span>case</span> <span>'\\'</span>:
                <span>if</span> (YYCURSOR /* fall through */
            <span>default</span>:
                <span>continue</span>;
        }

        YYCURSOR--;
        <span>break</span>;
    }

    yyleng = YYCURSOR - SCNG(yy_text);

    zend_scan_escape_string(zendlval, yytext, yyleng, <span>'`'</span> TSRMLS_CC);
    <span>return</span> T_ENCAPSED_AND_WHITESPACE;
}
</st_backquote></code>
로그인 후 복사

3.3 ST_HEREDOC

heredoc的其实和双引号差不多,所以在解析的过程中只是在双引号的基础上增加了对heredoc结尾的判断和处理:

<code><st_heredoc><span>{ANY_CHAR}</span> {
    <span>int</span> newline = <span>0</span>;

    zend_heredoc_label <span>*heredoc_label</span> = zend_ptr_stack_top(&SCNG(heredoc_label_stack));

    <span>if</span> (YYCURSOR > YYLIMIT) {
        <span>return</span> <span>0</span>;
    }

    YYCURSOR--;

    <span>while</span> (YYCURSOR *YYCURSOR++) {
            case <span>'\r'</span>:
                <span>if</span> (<span>*YYCURSOR</span> == <span>'\n'</span>) {
                    YYCURSOR++;
                }
                /* fall through <span>*/</span>
            case <span>'\n'</span>:
                <span>//</span>这个分支判断是否到了heredoc的结尾
                /* Check <span>for</span> ending label on the <span>next</span> line <span>*/</span>
                <span>if</span> (IS_LABEL_START(<span>*YYCURSOR</span>) && heredoc_label-><span>length</span> label, heredoc_label-><span>length</span>)) {
                    YYCTYPE <span>*end</span> = YYCURSOR + heredoc_label-><span>length</span>;

                    <span>if</span> (<span>*end</span> == <span>';'</span>) {
                        end++;
                    }

                    <span>if</span> (<span>*end</span> == <span>'\n'</span> || <span>*end</span> == <span>'\r'</span>) {
                        <span>/* newline before label will be subtracted from returned text, but
                         * yyleng/yytext</span> will include it, <span>for</span> zend_highlight/strip, tokenizer, etc. <span>*/</span>
                        <span>if</span> (YYCURSOR[-<span>2</span>] == <span>'\r'</span> && YYCURSOR[-<span>1</span>] == <span>'\n'</span>) {
                            newline = <span>2</span>; <span>/* Windows newline */</span>
                        } <span>else</span> {
                            newline = <span>1</span>;
                        }

                        CG(increment_lineno) = <span>1</span>; <span>/* For newline before label */</span>
                        BEGIN(ST_END_HEREDOC);

                        <span>goto</span> heredoc_scan_done;
                    }
                }
                <span>continue</span>;
            case <span>'$'</span>:
                <span>if</span> (IS_LABEL_START(<span>*YYCURSOR</span>) || <span>*YYCURSOR</span> == <span>'{'</span>) {
                    <span>break</span>;
                }
                <span>continue</span>;
            case <span>'{'</span>:
                <span>if</span> (<span>*YYCURSOR</span> == <span>'$'</span>) {
                    <span>break</span>;
                }
                <span>continue</span>;
            case <span>'\\'</span>:
                <span>if</span> (YYCURSOR *YYCURSOR != <span>'\n'</span> && <span>*YYCURSOR</span> != <span>'\r'</span>) {
                    YYCURSOR++;
                }
                /* fall through <span>*/</span>
            default:
                <span>continue</span>;
        }

        YYCURSOR--;
        <span>break</span>;
    }

heredoc_scan_done:
    yyleng = YYCURSOR - SCNG(yy_text);

    zend_scan_escape_string(zendlval, yytext, yyleng - newline, <span>0</span> TSRMLS_CC);
    <span>return</span> T_ENCAPSED_AND_WHITESPACE;
}
</st_heredoc></code>
로그인 후 복사

之前说过nowdoc相对heredoc就相当于单引号相对于双引号,所以在heredoc的基础上删除对'\$'、'{'等的处理就是nowdoc的处理流程了。这里就不贴出对应的处理代码了。

4.引号结束的处理

因为heredoc和nowdoc在处理ANY_CHAR的时候就包括对末尾的处理,并且单引号字符串的处理都在遇到单引号时就搞定了,所以这里只有双引号和反引号结束的处理了。

4.1 双引号结束

将状态设置回ST_IN_SCRIPTING并返回'"'

<code><span>ST_DOUBLE_QUOTES</span>>["] {
    BEGIN(ST_IN_SCRIPTING);
    return '"';
}
</code>
로그인 후 복사

4.2 反引号的处理

与双引号类似:

<code><st_backquote>[`] {
    BEGIN(ST_IN_SCRIPTING);
    <span>return</span> <span>'`'</span>;
}
</st_backquote></code>
로그인 후 복사

词法分析阶段对引号及其里面内容的处理差不多就是这些了,从匹配引号开头到引号中的变量,再到引号中的ANY_CHAR,最后则是引号结束的处理。

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 추천
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿