热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Webkit一:Dom转码和解析

因为真正的数据的处理是由DocumentParser::appendBytes以及DocumentParser::finish后续调用来完成,所以咱们重点关注这两块数据接收和解码TextRes

因为真正的数据的处理是由DocumentParser::appendBytes以及DocumentParser::finish后续调用来完成,所以咱们重点关注这两块

数据接收和解码

TextResourceDecoder

TextResourceDecoder::decode()

该函数中有个重要的操作是把收到的字符串转存到TextResourceDecoder:: m_buffer中。

这里先调用了TextResourceDecoder::checkForHeadCharset,该函数是个检查HTML头信息中是否有编码的信息,一般HTML的页面中如果指定了编码信息,那么编码信息会放在标签中。该函数就是做这样的检查的。

每次新收到的字符串数据都会追加到这个TextResourceDecoder:: m_buffer中,用于TextResourceDecoder的处理。

之后会创建一个HTMLMetaCharsetParser,并赋值给TextResourceDecoder::m_charsetParser通过HTMLMetaCharsetParser::checkForMetaCharset方法来执行对编码的检测,如果检测到,则把获取到的编码TextEncoding类型设置给TextResourceDecoder。

在TextResourceDecoder中有成员TextEncoding m_encoding;和EncodingSource m_source;分别记录了Encodeing具体的类型和来源。

TextResourceDecoder中有成员OwnPtrm_codec;它是负责真正的解码操作的,通过TextCodec:: decode进行解码,把TextResourceDecoder::m_buffer的数据传入,解码后会得到一个String类型的数据

 

DecodedDataDocumentParser

DecodedDataDocumentParser::appendBytes()

通过传入参数DocumentWriter 和 char* 调用writer->createDecoderIfNeeded()->decode 将网络数据转码,最后调用append

 

 

HTMLDocumentParser

HTMLDocumentParser::append

在DecodedDataDocumentParser::appendBytes中,当执行TextResourceDecoder::decode得到数据后,会执行append操作。该函数在HTMLDocumentParser中有实现,所以这里调用的是HTMLDocumentParser::append。并把解码后的String作为参数传入。在HTMLDocumentParser中有成员HTMLInputStream m_input;此处,把参数传入的String数据追加到HTMLDocumentParser::m_input,这样HTMLDocumentParser中已经保存了解码后的字符串了。

 

DocumentWriter

DocumentWriter::createDecoderIfNeed()

如果m_decoder为空,则创建一个实例,如果非空,则直接返回。

创建时会传入MimeType,TextEncoding等信息。创建的TextResourceDecoder会赋值给DocumentWriter::m_decoder。之后又做了些设置Encoding的操作。

再之后把这个新创建的TextResourceDecoder又设置给了Document(通过Frame找到Document)。看下Document的成员,也有一个RefPtr m_decoder;这里把新创建的TextResourceDecoder又赋值给了Document::decoder。最后将创建的TextResourceDecoder返回,

DocumentWriter::begin()

//创建Document对象,创建DocumentParser对象

DocumentWriter::addData()

DocumentWriter::end()

 

解码过程已经完毕,我们又处于HTMLDocumentParser中,并且已经保存了解码后的输入数据

 

 

解析

前提:令牌(token)通常代表关键字、变量名、字符串、直接量和大括号等 语法标点。token:令牌,tokenize:令牌化,tokenizer:令牌解析器。

有效的词是token,这个过程是tokenizing,处理这个过程的工具是tokenizer。

调用 HTMLDocumentParser finish();

调用 HTMLDocumentParser attemptToEnd();

调用 HTMLDocumentParser prepareToStopParsing();

调用 HTMLDocumentParser pumpTokenizerIfPossible();

调用 HTMLDocumentParser pumpTokenizer();  

//真正页面元素解析的地方,这里首先创建了一个PumpSession。

然后while循环,通过检查PumpSession的情况不停向下查找token来迭代所有元素进行解析,查到的token通过HTMLTokenizer nextToken

 

 

调用 HTMLTreeBuilder constructTreeFromToken();

//先利用参数传入的HTMLToken创建一个AtomicHTMLToken。这个AtomicHTMLToken跟HTMLToken成员非常类似,只是HTMLToken中一些信息如m_data,m_attributes中存的都是该信息在输入流数据中的范围(起始位置和终止位置),而在AtomicHTMLToken中存的都是跟输入流数据无关的数据类型了,并且根据HTMLToken把一些数据转换成有具体含义的成员,比如类型是StartTag,那么它的m_data就转换成AtomicHTMLToken:: m_name的值,即HTMLToken::m_data在该类型下是标签名称的含义。

在转换完AtomicHTMLToken后会做这样的判断,如果参数HTMLToken的类型不是Character则对其做clear操作即重置其成员

 

调用 HTMLTreeBuilder::constructTreeFromAtomicToken();

//该函数通过HTMLTreeBuilder::processToken来处理AtomicHTMLToken。

调用 HTMLTreeBuilder processToken();

//该函数对每种类型的AtomicHTMLToken做不同的分发处理。根据AtomicHTMLToken的类型,做转发处理,即调用相应的processXXX函数来处理与之对应的类型的AtomicHTMLToken,如当前是StartTag,则进入HTMLTreeBuilder ::processStartTag。

 

 

HTMLTreeBuilder中有成员InsertionMode  m_insertionMode;这个模式是保存当前插入模式,插入模式是什么.他其实还是实现一个有穷自动状态机,他根据输入的Token,来转换自身的状态,在状态转换函数中完成对Token的语法分析。在执行了状态转换后,开始利用Token进行DOM的构建时,调用了HTMLConstructionSite::insertHTMLXXX函数。

在HTMLTreeBuilder中有成员HTMLConstructionSite m_tree; HTMLTreeBuilder实际上完成的是对Token的识别,状态机的维护。根据传入的Token来运转状态机,通过状态机的转换函数,来找到需要执行哪种节点的创建,具体的节点的创建则是通过HTMLConstructionSite来完成的

如果当前的Token类型是StartTag,通过类型的识别,进入HTMLTreeBuilder::processStartTag的处理。通过状态机的处理,使得状态机的状态变成BeforeHTMLMode,在该状态下对StartTag类型的Token处理,就是执行HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML。

Node的创建

HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML

函数通过HTMLHtmlElement::create创建了一个HTMLHtmlElement,把HTMLConstructionSite::m_document作为参数传入,回忆一下Node中有成员Document*m_document;用于指明该Node属于哪个Document中。所以这里创建Node时,通过参数告诉它它的Document是谁。

看下HTMLHtmlElement的继承体系:

Node

ContainerNode

Element

StyledElement

HTMLElement

HTMLHtmlElement

其中在Node中定义了成员Document* m_document;和RenderObject* m_renderer;

在Element中定义了成员QualifiedName m_tagName;和mutableRefPtr m_attributeMap;

这几个都是很重要的成员,Document标识了该Node位于哪个Document下,每个Node只能在一个Document下,Document是该DOM树的根。

RenderObject标识了跟该Node对应的RenderObject是哪个,每个Node有一个跟其一一对应的RenderObject。

QualifiedName m_tagName标识了该Element的类型。

NamedNodeMap m_attributeMap标识了该Element的属性。

另外Node中还有成员负责构造树结构。

了解了这些成员信息后,继续看HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML。

在创建了HTMLHtmlElement后,把AtomicHTMLToken中的属性设置给HTMLHtmlElement。

之后把该HTMLHtmlElement压入一个HTMLElementStack中。

该函数结束。经过上述的过程,从输入流中找到了一个标签,该标签被识别成一个StartTag的HTMLToken,利用该HTMLToken在HTMLConstructionSite的处理中创建了一个HTMLHtmlElement,并把它压入栈HTMLConstructionSite:: m_openElements中。

 

HTMLToken中定义的几种HTMLToken的类型。

enum Type {

       Uninitialized,  //未定义,默认

       DOCTYPE,    //文档类型

       StartTag,     //起始标签

       EndTag,      //结束标签

       Comment,    //注释

       Character,    //元素内容

       EndOfFile,    //文档结束

};

 

  

    

  

  

    

test content

  

 

树构建阶段的输入是一个来自标记化阶段的标记序列。第一个模式是“initial mode”。接收 HTML 标记后转为“before html”模式,并在这个模式下重新处理此标记。这样会创建一个 HTMLHtmlElement 元素,并将其附加到 Document 根对象上。

然后状态将改为“before head”。此时我们接收“body”标记。即使我们的示例中没有“head”标记,系统也会隐式创建一个 HTMLHeadElement,并将其添加到树中。

现在我们进入了“in head”模式,然后转入“after head”模式。系统对 body 标记进行重新处理,创建并插入 HTMLBodyElement,同时模式转变为“body”

现在,接收由“Hello world”字符串生成的一系列字符标记。接收第一个字符时会创建并插入“Text”节点,而其他字符也将附加到该节点。

接收 body 结束标记会触发“after body”模式。现在我们将接收 HTML 结束标记,然后进入“after after body”模式。接收到文件结束标记后,解析过程就此结束

 

在此阶段,浏览器会将文档标注为交互状态,并开始解析那些处于“deferred”模式的脚本,也就是那些应在文档解析完成后才执行的脚本。然后,文档状态将设置为“完成”,一个“加载”事件将随之触发。

 

 

的解析

在HTMLTokenizer::nextToken中会把HTMLDocumentParser::m_token传入,这里是引用方式传的,即他们使用的是同一个HTMLToken。

在初始时,HTMLTokenizer::m_state为DataState。该HTMLToken类型为Uninitialized。

读取了’<’时,HTMLTokenizer::m_state变为TagOpenState。HTMLToken类型Uninitialized。

读取了’h’时,HTMLTokenizer::m_state变为TagNameState。HTMLToken类型变为StartTag,并清除了属性列表和当前属性,把当前字符’h’加入到HTMLToken::m_data中。

读取了’t’时,HTMLTokenizer::m_state为TagNameState。HTMLToken类型为StartTag,把当前字符’t’加入到HTMLToken::m_data中。

读取了’m’,’l’时,同上。

读取了’>’时,HTMLTokenizer::m_state变为DataState。HTMLToken类型为StartTag 。HTMLTokenizer::nextToken函数执行了return操作。

经过上述,HTMLTokenizer::nextToken完成了一个“词”的分析,即经过对””的读取后,得到了一个类型为StartTag的HTMLToken,并且其m_data为”html”。

之后通过HTMLTreeBuilder:: constructTreeFromToken把该HTMLToken进行语法分析。

HTMLTokenizer::nextToken是在HTMLDocumentParser::pumpTokenizer中运行的,HTMLDocumentParser有成员OwnPtrm_treeBuilder;该成员是在HTMLDocumentParser构造时一并创建的。

这里回顾一下,Document类有成员DocumentParser,而HTMLDocument类是Document的子类,HTMLDocumentParser是DocumentParser的子类。即HTMLDocument类中有HTMLDocumentParser成员。这两个是相互对应的。在HTMLDocumentParser创建时,HTMLDocument把自身作为参数传入。HTMLDocumentParser的构造函数中会同时创建HTMLTreeBuilder,HTMLTreeBuilder会收到HTMLDocument的指针,并记录在其成员HTMLTreeBuilder:: m_document。另外还有成员HTMLTreeBuilder::m_parser记录了HTMLDocumentParser的指针。

 

后面的换行符的解析

回到HTMLDocumentParser::pumpTokenizer中。

在While循环中再次进入HTMLTokenizer::nextToken。

此时, HTMLTokenizer::m_state为DataState。该HTMLToken类型为Uninitialized。

读取了’换行符’时,HTMLTokenizer::m_state为DataState。HTMLToken类型变为Character。把当前字符’换行符’加入到HTMLToken::m_data中。

读取了’<’时,HTMLTokenizer::m_state为DataState。HTMLToken类型为Character。HTMLTokenizer::nextToken返回。

在HTMLDocumentParser::pumpTokenizer中进入HTMLTreeBuilder::constructTreeFromToken

在HTMLTreeBuilder::processCharacterBuffer中,判断m_insertionMode为BeforeHeadMode后,在处理流程中,会对字符串进行检测,发现除了空白字符没有其他字符后,直接返回。

 

调用 HTMLTreeBuilder::constructTreeFromAtomicToken

调用 HTMLTreeBuilder::processToken

调用 HTMLTreeBuilder::processCharacter

调用 HTMLTreeBuilder::processCharacterBuffer

在HTMLTreeBuilder::processCharacterBuffer中,判断m_insertionMode为BeforeHeadMode后,在处理流程中,会对字符串进行检测,发现除了空白字符没有其他字符后,直接返回。

 

的解析

回到HTMLDocumentParser::pumpTokenizer中。

此时, HTMLTokenizer::m_state为DataState。该HTMLToken类型为Uninitialized。

状态机的处理类似对的处理

这里生成的Token的名字是headTag。用该Token创建了一个HTMLElement。

之后调用了一个HTMLConstructionSite::attachToCurrent。这个Current是谁呢?它就是HTMLConstructionSite::m_openElements这个栈的栈顶元素,记得刚才创建HTMLHtmlElement的过程中,把HTMLHtmlElement压入栈了,现在获取到的栈顶元素就是刚才的HTMLHtmlElement。这个attach的过程就是把两个 Node建立成父子关系。

再之后把新的HTMLElement压入栈。

由此可见,每次收到一个开始标签时,创建一个新的HTMLElement后,会把这个新的HTMLElement入栈,这样通过栈情况就能够找到待插入节点的父节点(栈顶元素)。即新的元素都是属于这个最近的开始标签的子Node。由此可知,当收到结束标签时,应该会有出栈的操作,这样栈顶元素还是标识这新Node的父节点。即栈维护了标签的开关情况,这个栈类似于函数调用,当进入某个函数时,函数入栈,当继续进入子函数时,子函数入栈。当函数返回时,该函数出栈。通过函数栈能够识别某个函数的嵌套位置,同样通过这个标签栈可以识别某个标签的嵌套位置。

 

 

的解析

回到HTMLDocumentParser::pumpTokenizer中。

在处理

读取了’空格’时,HTMLTokenizer::m_state变为BeforeAttributeNameState。HTMLToken类型为StartTag。

读取了’h’时,HTMLTokenizer::m_state为BeforeAttributeNameState。HTMLToken类型为StartTag。通过HTMLToken::addNewAttribute让HTMLToken::m_attributes中开辟一个新的属性空间,HTMLToken中有个成员Attribute* m_currentAttribute;用于指向当前的属性的,即它现在指向新开辟的属性空间的地址。通过HTMLToken:: beginAttributeName设置该属性名字的起始范围,通过HTMLToken::appendToAttributeName把字符’h’加入。可见在HTMLToken中有一些专门用来维护属性信息的函数,在HTMLTokenizer解析到属性信息时,他的HTMLToken没有发生变化,仍然是StartTag,但是状态机的状态变成了BeforeAttributeNameState,并且状态处理函数中,对数据的解析会导致HTMLToken属性的设置。

读取了’t’时,HTMLTokenizer::m_state变为AttributeNameState。HTMLToken类型为StartTag。把当前字符’t’加入到HTMLToken的当前属性名中。

读取”tp-equiv”跟读取上面的’t’字符流程一样。

读取了’=’时,HTMLTokenizer::m_state变为BeforeAttributeValueState。HTMLToken类型为StartTag。通过HTMLToken::endAttributeName设置属性名字的结束偏移和属性值的开始及结束便宜。这里属性值还没有开始也没结束呢,这里设置可能是为了避免html页面中没写属性值的情况吧。

读取了’双引号’时,HTMLTokenizer::m_state变为AttributeValueDoubleQuotedState。HTMLToken类型为StartTag。通过HTMLToken::beginAttributeValue设置属性开始偏移。

读取了’c’时,HTMLTokenizer::m_state为AttributeValueDoubleQuotedState。HTMLToken类型为StartTag。把当前字符’c’加入到HTMLToken的当前属性值中。

读取”ontent-type”跟读取上面的’c’字符流程一样。

读取了’双引号’时,HTMLTokenizer::m_state变为AfterAttributeValueDoubleQuotedState。HTMLToken类型为StartTag。通过HTMLToken::endAttributeValue设置属性结束偏移。

读取了’空格’时,HTMLTokenizer::m_state变为BeforeAttributeValueState。HTMLToken类型为StartTag。

读取了’c’时,HTMLTokenizer::m_state变为AttributeValueState。HTMLToken类型为StartTag。执行跟上面读取了’h’时同样的创建属性的流程。

读取了”Ontent="text/html; charset=UTF-8"”的流程跟上述对应的流程一样。

读取了’空格’时,HTMLTokenizer::m_state变为BeforeAttributeValueState。HTMLToken类型为StartTag。

读取了’/’时,HTMLTokenizer::m_state变为SelfClosingStartTagState。HTMLToken类型为StartTag。

读取了’>’时,通过HTMLToken::setSelfClosing设置其自关闭性质。之后执行返回,退出HTMLTokenizer::nextToken。

这里生成的Token的名字是metaTag。用该Token创建了一个HTMLElement。设置他的属性,把他attach到之前的标签创建的HTMLElement上。

 

的处理

回到HTMLDocumentParser::pumpTokenizer中。

有了上述的基础,看这个标签就比较容易了,跟非常类似,只是对这个标签的处理时,HTMLToken类型为EndTag。

在HTMLTreeBuilder对该HTMLToken的处理时,会对HTMLConstructionStie::m_openElements执行出栈操作,即之前所讲到的,并更新HTMLTreeBuilder的状态机情况。这里没有再创建新的Node了。

之后的其他标签的处理都是类似的,以下显示几个处理标签的对应的HTMLToken的栈情况:

 

 

 

通过上述对Token的处理可知,在HTMLTokenizer::nextToken中根据状态机进行词法分析能够分离出一个个的HTMLToken,这些利用HTMLTreeBuilder::constructTreeFromToken进行语法分析能够识别它在当前的状态下要创建什么类型的Node。

HTMLTreeBuilder用于做这期间的语法分析,他会先根据HTMLToken的类型做分发处理(即通过processXXX),在某个分支处理中,又会根据HTMLTreeBuilder自身维护的状态机的状态情况做判断,进一步分发处理。也就是说HTMLTreeBuilder即会考虑当前的状态,也会考虑HTMLToken的类型,他的状态相当于该HTMLToken当前所处的环境。

HTMLTreeBuilder最终会通过HTMLConstructionSite来执行相应Node的创建(即通过insertXXX)。

另外注意在HTMLConstructionSite中有成员mutable HTMLElementStack m_openElements;用于维护一个HTMLElement的栈,这个栈用户维护标签的开始和结束的情况,即维护标签的层级关系,用于确定某个Node插入到哪个Node下面,即确定相互的父子关系。

而这里创建的所有的节点,都有一个公共的Document,即HTMLDocument中的成员HTMLDocumentParser中的成员HTMLTreeBuilder中的Document* m_document;这个Document就对应开始的HTMLDocument。

由此可见,后续创建的所有的Node都是HTMLDocument的后代节点。

 

解析过程

 


推荐阅读
  • DVWA学习笔记系列:深入理解CSRF攻击机制
    DVWA学习笔记系列:深入理解CSRF攻击机制 ... [详细]
  • 在JavaWeb开发中,文件上传是一个常见的需求。无论是通过表单还是其他方式上传文件,都必须使用POST请求。前端部分通常采用HTML表单来实现文件选择和提交功能。后端则利用Apache Commons FileUpload库来处理上传的文件,该库提供了强大的文件解析和存储能力,能够高效地处理各种文件类型。此外,为了提高系统的安全性和稳定性,还需要对上传文件的大小、格式等进行严格的校验和限制。 ... [详细]
  • 大类|电阻器_使用Requests、Etree、BeautifulSoup、Pandas和Path库进行数据抓取与处理 | 将指定区域内容保存为HTML和Excel格式
    大类|电阻器_使用Requests、Etree、BeautifulSoup、Pandas和Path库进行数据抓取与处理 | 将指定区域内容保存为HTML和Excel格式 ... [详细]
  • Flowable 流程图路径与节点展示:已执行节点高亮红色标记,增强可视化效果
    在Flowable流程图中,通常仅显示当前节点,而路径则需自行获取。特别是在多次驳回的情况下,节点可能会出现混乱。本文重点探讨了如何准确地展示流程图效果,包括已结束的流程和正在执行的流程。具体实现方法包括生成带有高亮红色标记的图片,以增强可视化效果,确保用户能够清晰地了解每个节点的状态。 ... [详细]
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • 字节流(InputStream和OutputStream),字节流读写文件,字节流的缓冲区,字节缓冲流
    字节流抽象类InputStream和OutputStream是字节流的顶级父类所有的字节输入流都继承自InputStream,所有的输出流都继承子OutputStreamInput ... [详细]
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • 本地存储组件实现对IE低版本浏览器的兼容性支持 ... [详细]
  • 本文详细解析了客户端与服务器之间的交互过程,重点介绍了Socket通信机制。IP地址由32位的4个8位二进制数组成,分为网络地址和主机地址两部分。通过使用 `ipconfig /all` 命令,用户可以查看详细的IP配置信息。此外,文章还介绍了如何使用 `ping` 命令测试网络连通性,例如 `ping 127.0.0.1` 可以检测本机网络是否正常。这些技术细节对于理解网络通信的基本原理具有重要意义。 ... [详细]
  • 深入解析Struts、Spring与Hibernate三大框架的面试要点与技巧 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 本指南介绍了如何在ASP.NET Web应用程序中利用C#和JavaScript实现基于指纹识别的登录系统。通过集成指纹识别技术,用户无需输入传统的登录ID即可完成身份验证,从而提升用户体验和安全性。我们将详细探讨如何配置和部署这一功能,确保系统的稳定性和可靠性。 ... [详细]
  • 本文介绍了如何利用 Delphi 中的 IdTCPServer 和 IdTCPClient 控件实现高效的文件传输。这些控件在默认情况下采用阻塞模式,并且服务器端已经集成了多线程处理,能够支持任意大小的文件传输,无需担心数据包大小的限制。与传统的 ClientSocket 相比,Indy 控件提供了更为简洁和可靠的解决方案,特别适用于开发高性能的网络文件传输应用程序。 ... [详细]
  • 本文以 www.域名.com 为例,详细介绍如何为每个注册用户提供独立的二级域名,如 abc.域名.com。实现这一功能的核心步骤包括:首先,确保域名支持泛解析,即将 A 记录设置为 *.域名.com,以便将所有二级域名请求指向同一服务器。接着,在服务器端使用 ASP.NET 2.0 进行配置,通过解析 HTTP 请求中的主机头信息,动态识别并处理不同的二级域名,从而实现个性化内容展示。此外,还需在数据库中维护用户与二级域名的对应关系,确保每个用户的二级域名都能正确映射到其专属内容。 ... [详细]
author-avatar
小白菜
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有