Java爬虫初体验

年关将近,工作上该完成的都差不多了,就剩些测试完改改的活,上午闲着就接触学习了一下爬虫,收益还是有的,纠结了很久的正则表达式终于搞得差不多了,Java的Regex和JS上还是有区别的,JS上的"\w"Java得写成"\\w",因为Java会对字符串中的"\"做转义,还有JS中"\S\s"的写法(指任意多的任意字符),Java可以写成".*"

博主刚接触爬虫,参考了许多博客和问答贴,先写个爬虫的Overview让朋友们对其有些印象,之后我们再展示代码.

网络爬虫的基本原理:

网络爬虫的基本工作流程如下:

1.首先选取一部分精心挑选的种子URL;

2.将这些URL放入待抓取URL队列;

3.从待抓取URL队列中取出待抓取在URL,解析DNS,并且得到主机的ip,并将URL对应的网页下载下来,存储进已下载网页库中。此外,将这些URL放进已抓取URL队列。

4.分析已抓取URL队列中的URL,分析其中的其他URL,并且将URL放入待抓取URL队列,从而进入下一个循环。

网络爬虫的抓取策略有:深度优先遍历,广度优先遍历(是不是想到了图的深度和广度优先遍历?),Partial PageRank,OPIC策略,大站优先等;

博主采用的是实现起来比较简单的广度优先遍历,即获取一个页面中所有的URL后,将之塞进URL队列,于是循环条件就是该队列非空.

网络爬虫(HelloWorld版)

入口类:

package com.example.spiderman.page;

/**
 * @author YHW
 * @ClassName: MyCrawler
 * @Description:
 */
import com.example.spiderman.link.LinkFilter;
import com.example.spiderman.link.Links;
import com.example.spiderman.page.Page;
import com.example.spiderman.page.PageParserTool;
import com.example.spiderman.page.RequestAndResponseTool;
import com.example.spiderman.utils.FileTool;
import com.example.spiderman.utils.RegexRule;
import org.jsoup.select.Elements;

import java.util.Set;

public class MyCrawler {

    /**
     * 使用种子初始化 URL 队列
     *
     * @param seeds 种子 URL
     * @return
     */
    private void initCrawlerWithSeeds(String[] seeds) {
        for (int i = 0; i < seeds.length; i++){
            Links.addUnvisitedUrlQueue(seeds[i]);
        }
    }

    /**
     * 抓取过程
     *
     * @param seeds
     * @return
     */
    public void crawling(String[] seeds) {

        //初始化 URL 队列
        initCrawlerWithSeeds(seeds);

        //定义过滤器,提取以 变量url 开头的链接
        LinkFilter filter = new LinkFilter() {
            @Override
            public boolean accept(String url) {
                if (url.startsWith("https://www.cnblogs.com/Joey44/"))
                    return true;
                else
                    return false;
            }
        };

        //循环条件:待抓取的链接不空且抓取的网页不多于 1000
        while (!Links.unVisitedUrlQueueIsEmpty()  && Links.getVisitedUrlNum() <= 1000) {

            //先从待访问的序列中取出第一个;
            String visitUrl = (String) Links.removeHeadOfUnVisitedUrlQueue();
            if (visitUrl == null){
                continue;
            }

            //根据URL得到page;
            Page page = RequestAndResponseTool.sendRequstAndGetResponse(visitUrl);

            //对page进行处理: 访问DOM的某个标签
            Elements es = PageParserTool.select(page,"a");
            if(!es.isEmpty()){
                System.out.println("下面将打印所有a标签: ");
                System.out.println(es);
            }

            //将保存文件
            FileTool.saveToLocal(page);

            //将已经访问过的链接放入已访问的链接中;
            Links.addVisitedUrlSet(visitUrl);

            //得到超链接
            Set<String> links = PageParserTool.getLinks(page,"a");
            for (String link : links) {
                RegexRule regexRule = new RegexRule();
                regexRule.addPositive("http.*/Joey44/.*html.*");if(regexRule.satisfy(link)){
                    Links.addUnvisitedUrlQueue(link);
                    System.out.println("新增爬取路径: " + link);
                }

            }
        }
    }

    //main 方法入口
    public static void main(String[] args) {
        MyCrawler crawler = new MyCrawler();
        crawler.crawling(new String[]{"https://www.cnblogs.com/Joey44/"});
    }
}

网络编程自然少不了Http访问,直接用apache大佬的httpclient包就行:

package com.example.spiderman.page;

/**
 * @author YHW
 * @ClassName: RequestAndResponseTool
 * @Description:
 */

import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;

import java.io.IOException;

public class RequestAndResponseTool {

    public static Page  sendRequstAndGetResponse(String url) {
        Page page = null;
        // 1.生成 HttpClinet 对象并设置参数
        HttpClient httpClient = new HttpClient();
        // 设置 HTTP 连接超时 5s
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
        // 2.生成 GetMethod 对象并设置参数
        GetMethod getMethod = new GetMethod(url);
        // 设置 get 请求超时 5s
        getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
        // 设置请求重试处理
        getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
        // 3.执行 HTTP GET 请求
        try {
            int statusCode = httpClient.executeMethod(getMethod);
            // 判断访问的状态码
            if (statusCode != HttpStatus.SC_OK) {
                System.err.println("Method failed: " + getMethod.getStatusLine());
            }
            // 4.处理 HTTP 响应内容
            byte[] responseBody = getMethod.getResponseBody();// 读取为字节 数组
            String contentType = getMethod.getResponseHeader("Content-Type").getValue(); // 得到当前返回类型
            page = new Page(responseBody,url,contentType); //封装成为页面
        } catch (HttpException e) {
            // 发生致命的异常,可能是协议不对或者返回的内容有问题
            System.out.println("Please check your provided http address!");
            e.printStackTrace();
        } catch (IOException e) {
            // 发生网络异常
            e.printStackTrace();
        } finally {
            // 释放连接
            getMethod.releaseConnection();
        }
        return page;
    }
}

别忘了导入Maven依赖:

<!-- https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient -->
        <dependency>
            <groupId>commons-httpclient</groupId>
            <artifactId>commons-httpclient</artifactId>
            <version>3.0</version>
        </dependency>

接下来要让我们的程序可以存儲页面,新建Page实体类:

package com.example.spiderman.page;

/**
 * @author YHW
 * @ClassName: Page
 * @Description:
 */
import com.example.spiderman.utils.CharsetDetector;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

import java.io.UnsupportedEncodingException;

/*
 * page
 *   1: 保存获取到的响应的相关内容;
 * */
public class Page {

    private byte[] content ;
    private String html ;  //网页源码字符串
    private Document doc  ;//网页Dom文档
    private String charset ;//字符编码
    private String url ;//url路径
    private String contentType ;// 内容类型

    public Page(byte[] content , String url , String contentType){
        this.content = content ;
        this.url = url ;
        this.contentType = contentType ;
    }

    public String getCharset() {
        return charset;
    }
    public String getUrl(){return url ;}
    public String getContentType(){ return contentType ;}
    public byte[] getContent(){ return content ;}

    /**
     * 返回网页的源码字符串
     *
     * @return 网页的源码字符串
     */
    public String getHtml() {
        if (html != null) {
            return html;
        }
        if (content == null) {
            return null;
        }
        if(charset==null){
            charset = CharsetDetector.guessEncoding(content); // 根据内容来猜测 字符编码
        }
        try {
            this.html = new String(content, charset);
            return html;
        } catch (UnsupportedEncodingException ex) {
            ex.printStackTrace();
            return null;
        }
    }

    /*
     *  得到文档
     * */
    public Document getDoc(){
        if (doc != null) {
            return doc;
        }
        try {
            this.doc = Jsoup.parse(getHtml(), url);
            return doc;
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

}

然后要有页面的解析功能类:

package com.example.spiderman.page;

/**
 * @author YHW
 * @ClassName: PageParserTool
 * @Description:
 */
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class PageParserTool {

    /* 通过选择器来选取页面的 */
    public static Elements select(Page page , String cssSelector) {
        return page.getDoc().select(cssSelector);
    }

    /*
     *  通过css选择器来得到指定元素;
     *
     *  */
    public static Element select(Page page , String cssSelector, int index) {
        Elements eles = select(page , cssSelector);
        int realIndex = index;
        if (index < 0) {
            realIndex = eles.size() + index;
        }
        return eles.get(realIndex);
    }

    /**
     * 获取满足选择器的元素中的链接 选择器cssSelector必须定位到具体的超链接
     * 例如我们想抽取id为content的div中的所有超链接,这里
     * 就要将cssSelector定义为div[id=content] a
     *  放入set 中 防止重复;
     * @param cssSelector
     * @return
     */
    public static  Set<String> getLinks(Page page ,String cssSelector) {
        Set<String> links  = new HashSet<String>() ;
        Elements es = select(page , cssSelector);
        Iterator iterator  = es.iterator();
        while(iterator.hasNext()) {
            Element element = (Element) iterator.next();
            if ( element.hasAttr("href") ) {
                links.add(element.attr("abs:href"));
            }else if( element.hasAttr("src") ){
                links.add(element.attr("abs:src"));
            }
        }
        return links;
    }

    /**
     * 获取网页中满足指定css选择器的所有元素的指定属性的集合
     * 例如通过getAttrs("img[src]","abs:src")可获取网页中所有图片的链接
     * @param cssSelector
     * @param attrName
     * @return
     */
    public static ArrayList<String> getAttrs(Page page , String cssSelector, String attrName) {
        ArrayList<String> result = new ArrayList<String>();
        Elements eles = select(page ,cssSelector);
        for (Element ele : eles) {
            if (ele.hasAttr(attrName)) {
                result.add(ele.attr(attrName));
            }
        }
        return result;
    }
}

别忘了导入Maven依赖:

<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.11.3</version>
        </dependency>

最后就是额外需要的一些工具类:正则匹配工具,页面编码侦测和存儲页面工具

package com.example.spiderman.utils;

/**
 * @author YHW
 * @ClassName: RegexRule
 * @Description:
 */
import java.util.ArrayList;
import java.util.regex.Pattern;

public class RegexRule {

    public RegexRule(){

    }
    public RegexRule(String rule){
        addRule(rule);
    }

    public RegexRule(ArrayList<String> rules){
        for (String rule : rules) {
            addRule(rule);
        }
    }

    public boolean isEmpty(){
        return positive.isEmpty();
    }

    private ArrayList<String> positive = new ArrayList<String>();
    private ArrayList<String> negative = new ArrayList<String>();

    /**
     * 添加一个正则规则 正则规则有两种,正正则和反正则
     * URL符合正则规则需要满足下面条件: 1.至少能匹配一条正正则 2.不能和任何反正则匹配
     * 正正则示例:+a.*c是一条正正则,正则的内容为a.*c,起始加号表示正正则
     * 反正则示例:-a.*c时一条反正则,正则的内容为a.*c,起始减号表示反正则
     * 如果一个规则的起始字符不为加号且不为减号,则该正则为正正则,正则的内容为自身
     * 例如a.*c是一条正正则,正则的内容为a.*c
     * @param rule 正则规则
     * @return 自身
     */
    public RegexRule addRule(String rule) {
        if (rule.length() == 0) {
            return this;
        }
        char pn = rule.charAt(0);
        String realrule = rule.substring(1);
        if (pn == ‘+‘) {
            addPositive(realrule);
        } else if (pn == ‘-‘) {
            addNegative(realrule);
        } else {
            addPositive(rule);
        }
        return this;
    }

    /**
     * 添加一个正正则规则
     * @param positiveregex
     * @return 自身
     */
    public RegexRule addPositive(String positiveregex) {
        positive.add(positiveregex);
        return this;
    }

    /**
     * 添加一个反正则规则
     * @param negativeregex
     * @return 自身
     */
    public RegexRule addNegative(String negativeregex) {
        negative.add(negativeregex);
        return this;
    }

    /**
     * 判断输入字符串是否符合正则规则
     * @param str 输入的字符串
     * @return 输入字符串是否符合正则规则
     */
    public boolean satisfy(String str) {

        int state = 0;
        for (String nregex : negative) {
            if (Pattern.matches(nregex, str)) {
                return false;
            }
        }

        int count = 0;
        for (String pregex : positive) {
            if (Pattern.matches(pregex, str)) {
                count++;
            }
        }
        if (count == 0) {
            return false;
        } else {
            return true;
        }

    }
}
package com.example.spiderman.utils;

/**
 * @author YHW
 * @ClassName: CharsetDetector
 * @Description:
 */

import org.mozilla.universalchardet.UniversalDetector;

import java.io.UnsupportedEncodingException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 字符集自动检测
 **/
public class CharsetDetector {

    //从Nutch借鉴的网页编码检测代码
    private static final int CHUNK_SIZE = 2000;

    private static Pattern metaPattern = Pattern.compile(
            "<meta\\s+([^>]*http-equiv=(\"|‘)?content-type(\"|‘)?[^>]*)>",
            Pattern.CASE_INSENSITIVE);
    private static Pattern charsetPattern = Pattern.compile(
            "charset=\\s*([a-z][_\\-0-9a-z]*)", Pattern.CASE_INSENSITIVE);
    private static Pattern charsetPatternHTML5 = Pattern.compile(
            "<meta\\s+charset\\s*=\\s*[\"‘]?([a-z][_\\-0-9a-z]*)[^>]*>",
            Pattern.CASE_INSENSITIVE);

    //从Nutch借鉴的网页编码检测代码
    private static String guessEncodingByNutch(byte[] content) {
        int length = Math.min(content.length, CHUNK_SIZE);

        String str = "";
        try {
            str = new String(content, "ascii");
        } catch (UnsupportedEncodingException e) {
            return null;
        }

        Matcher metaMatcher = metaPattern.matcher(str);
        String encoding = null;
        if (metaMatcher.find()) {
            Matcher charsetMatcher = charsetPattern.matcher(metaMatcher.group(1));
            if (charsetMatcher.find()) {
                encoding = new String(charsetMatcher.group(1));
            }
        }
        if (encoding == null) {
            metaMatcher = charsetPatternHTML5.matcher(str);
            if (metaMatcher.find()) {
                encoding = new String(metaMatcher.group(1));
            }
        }
        if (encoding == null) {
            if (length >= 3 && content[0] == (byte) 0xEF
                    && content[1] == (byte) 0xBB && content[2] == (byte) 0xBF) {
                encoding = "UTF-8";
            } else if (length >= 2) {
                if (content[0] == (byte) 0xFF && content[1] == (byte) 0xFE) {
                    encoding = "UTF-16LE";
                } else if (content[0] == (byte) 0xFE
                        && content[1] == (byte) 0xFF) {
                    encoding = "UTF-16BE";
                }
            }
        }

        return encoding;
    }

    /**
     * 根据字节数组,猜测可能的字符集,如果检测失败,返回utf-8
     *
     * @param bytes 待检测的字节数组
     * @return 可能的字符集,如果检测失败,返回utf-8
     */
    public static String guessEncodingByMozilla(byte[] bytes) {
        String DEFAULT_ENCODING = "UTF-8";
        UniversalDetector detector = new UniversalDetector(null);
        detector.handleData(bytes, 0, bytes.length);
        detector.dataEnd();
        String encoding = detector.getDetectedCharset();
        detector.reset();
        if (encoding == null) {
            encoding = DEFAULT_ENCODING;
        }
        return encoding;
    }

    /**
     * 根据字节数组,猜测可能的字符集,如果检测失败,返回utf-8
     * @param content 待检测的字节数组
     * @return 可能的字符集,如果检测失败,返回utf-8
     */
    public static String guessEncoding(byte[] content) {
        String encoding;
        try {
            encoding = guessEncodingByNutch(content);
        } catch (Exception ex) {
            return guessEncodingByMozilla(content);
        }

        if (encoding == null) {
            encoding = guessEncodingByMozilla(content);
            return encoding;
        } else {
            return encoding;
        }
    }
}
package com.example.spiderman.utils;

/**
 * @author YHW
 * @ClassName: FileTool
 * @Description:
 */
import com.example.spiderman.page.Page;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/*  本类主要是 下载那些已经访问过的文件*/
public class FileTool {

    private static String dirPath;

    /**
     * getMethod.getResponseHeader("Content-Type").getValue()
     * 根据 URL 和网页类型生成需要保存的网页的文件名,去除 URL 中的非文件名字符
     */
    private static String getFileNameByUrl(String url, String contentType) {
        //去除 http://
        url = url.substring(7);
        //text/html 类型
        if (contentType.indexOf("html") != -1) {
            url = url.replaceAll("[\\?/:*|<>\"]", "_") + ".html";
            return url;
        }
        //如 application/pdf 类型
        else {
            return url.replaceAll("[\\?/:*|<>\"]", "_") + "." +
                    contentType.substring(contentType.lastIndexOf("/") + 1);
        }
    }

    /*
     *  生成目录
     * */
    private static void mkdir() {
        if (dirPath == null) {
            dirPath = Class.class.getClass().getResource("/").getPath() + "temp\\";
        }
        File fileDir = new File(dirPath);
        if (!fileDir.exists()) {
            fileDir.mkdir();
        }
    }

    /**
     * 保存网页字节数组到本地文件,filePath 为要保存的文件的相对地址
     */

    public static void saveToLocal(Page page) {
        mkdir();
        String fileName =  getFileNameByUrl(page.getUrl(), page.getContentType()) ;
        String filePath = dirPath + fileName ;
        byte[] data = page.getContent();
        try {
            //Files.lines(Paths.get("D:\\jd.txt"), StandardCharsets.UTF_8).forEach(System.out::println);
            DataOutputStream out = new DataOutputStream(new FileOutputStream(new File(filePath)));
            for (int i = 0; i < data.length; i++) {
                out.write(data[i]);
            }
            out.flush();
            out.close();
            System.out.println("文件:"+ fileName + "已经被存储在"+ filePath  );
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

为了拓展性,增加一个LinkFilter接口:

package com.example.spiderman.link;

/**
 * @author YHW
 * @ClassName: LinkFilter
 * @Description:
 */
public interface LinkFilter {
    public boolean accept(String url);
}

整体项目结构:

运行结果(部分):

网络爬虫的Helloworld版本就完成了,当然在我们获取网站源码之后,就可以利用解析工具获取该页面任意的元素标签,再进行正则表达式过滤出我们想要的数据了,可以将数据存储到数据库里,还可以根据需要使用分布式爬虫提高爬取速率,博主以后也会推出更有使用价值的爬虫程序,届时会上传到开源社区...

参考文章:https://www.cnblogs.com/wawlian/archive/2012/06/18/2553061.html

    https://www.cnblogs.com/sanmubird/p/7857474.html

      

原文地址:https://www.cnblogs.com/Joey44/p/10320377.html

时间: 2024-12-01 17:20:30

Java爬虫初体验的相关文章

Node.js 网页瘸腿爬虫初体验

延续上一篇,想把自己博客的文档标题利用Node.js的request全提取出来,于是有了下面的初哥爬虫,水平有限,这只爬虫目前还有点瘸腿,请看官你指正了. // 内置http模块,提供了http服务器和客户端功能 var http=require("http"); // 内置文件处理模块 var fs=require('fs'); // 创建一个将流数据写入文件的WriteStream对象 var outstream=fs.createWriteStream('./1.txt'); /

【Python3爬虫】爬取美女图新姿势--Redis分布式爬虫初体验

一.写在前面 之前写的爬虫都是单机爬虫,还没有尝试过分布式爬虫,这次就是一个分布式爬虫的初体验.所谓分布式爬虫,就是要用多台电脑同时爬取数据,相比于单机爬虫,分布式爬虫的爬取速度更快,也能更好地应对IP的检测.本文介绍的是利用Redis数据库实现的分布式爬虫,Redis是一种常用的菲关系型数据库,常用数据类型包括String.Hash.Set.List和Sorted Set,重要的是Redis支持主从复制,主机能将数据同步到从机,也就能够实现读写分离.因此我们可以利用Redis的特性,借助req

【Python3爬虫】学习分布式爬虫第一步--Redis分布式爬虫初体验

一.写在前面 之前写的爬虫都是单机爬虫,还没有尝试过分布式爬虫,这次就是一个分布式爬虫的初体验.所谓分布式爬虫,就是要用多台电脑同时爬取数据,相比于单机爬虫,分布式爬虫的爬取速度更快,也能更好地应对IP的检测.本文介绍的是利用Redis数据库实现的分布式爬虫,Redis是一种常用的菲关系型数据库,常用数据类型包括String.Hash.Set.List和Sorted Set,重要的是Redis支持主从复制,主机能将数据同步到从机,也就能够实现读写分离.因此我们可以利用Redis的特性,借助req

python2.7 爬虫初体验爬取新浪国内新闻_20161130

python2.7 爬虫初学习 模块:BeautifulSoup requests 1.获取新浪国内新闻标题 2.获取新闻url 3.还没想好,想法是把第2步的url 获取到下载网页源代码 再去分析源代码 获取新闻详情页 发表时间 新闻来源等数据 结合MySQLdb模块导入到数据库 4.疑惑:期望是整体获取这些字段 发表时间 发布标题 新闻详情内容 新闻来源 任重而道远..都想拜个老师带带了.. #coding:utf-8 import requests from bs4 import Beau

JAVA 8 初体验

Java 8 发布两个多月了,更新了一些特性,其中有接口中可以有默认的实现,lambda表达式.重复注解.时间等等. 今天先探究一下这新增的lambda 表达式功能特性,其他的日后待解. 先来看一个例子,以往版本的实现方法: Runnable oldRunnable = new Runnable() { @Override public void run() { System.out.println("hello,I am panpan"); } }; oldRunnable.run(

scrapy爬虫初体验

import scrapy class StackOverflowSpider(scrapy.Spider): name="stackoverflow" start_urls=["http://stackoverflow.com/questions?sort=votes"] def parse(self,response): for href in response.css('.question-summary h3 a::attr(href)'): #得到爬取的地

新兵日记---java语法初体验

刚刚结束了java语法的学习,和之前学习的c++做一下对比. 1. 主要用途方面 java 适合待在服务器里,适合于后端开发,尤其是Web后端 ,有各种成熟的框架 , 有健全的生态环境. 手机领域,大数据领域等等 c++ 原本是想着做GUI, 结果发现MFC早就衰落,  GUI逐渐脚本化. c++用于底层开发,高性能领域 ,如游戏服务器后台,游戏开发,图像,流视频,引擎,底层通信等等 2.初始java语法 java语法比c++简单,提供了健全的内置类,如:String,StringBuffer.

JAVA的初体验

jdk的地址:https://www.oracle.com/technetwork/java/javase/downloads/index.html 第一步:找到相应的JAVA SE版本,点击download 第二步:同意,并下载相应版本 下载慢的请到我百度云下载: 链接:https://pan.baidu.com/s/1_rdWVB0G8b0tvcpsrXcq8Q 提取码:d8pc 第三步:jdk的安装 1.运行,下一步 2.目标文件夹,选择安装目录,下一步 3.安装完成 4.添加系统环境变量

Protocol Buffers(protobuf)java初体验

由于项目需要所以简单的研究了下protobuf.我也是参照网上的博客,所以大部分内容我也就不重复造轮子了.首先protobuf介绍点击这里,使用介绍点击这里,使用demo看这里.我个人的第一个例子也是参照这个demo来的,不过其中我有遇到一些问题,所以揪出来说说,也就给自己做个笔记,方便查阅. 基本的东西相信大家也了解了,直接步入主题了: 1.限定修饰符介绍 required\optional\repeated,之前给定的博客已经有这个介绍了我也不多说,这里把一些小玩儿拿出来讲讲 ①.requi