• Java面向对象入门


    面向对象

    面向对象是一种思维方式,是相对于面向过程来说的。面向过程需要注重流程中的每一个细节,面向对象注重的是对象,只要找到这个对象,就能够拥有对象身上的一切功能。以做饭为例,如果是面向过程需要知道做饭的每一个步骤才能做饭,如果是面向对象只要找到做饭的厨师(厨师就是对象),让厨师就可以完成做饭。面向对象是基于面向过程的,面向过程里可能又有面向对象的使用。

    面向对象不一定比面向过程好,如果是相对复杂的事物可以使用面向对象,相对简单的直接面向过程就行。

    类与对象的关系

    根据同类对象进行概括,将对象的特征概括为属性,将对象的行为概括为方法,将这一类对象统一就是用一个类表示,类即对象的抽取,如人,车都是类,具体的一个人张三李四就是对象,具体的摩托汽车就是对象。

    对象在内存中的保存方式

    new出来的对象保存在堆中,栈中保存的是对象的内存地址,如Person p=new Person(),这个新new出来的对象保存在堆中,p保存的只是这个对象的内存地址。

    package com.boe.object;
    
    public class PersonDemo {
    	
    	public static void main(String[] args) {
    		//创建对象
    		Person p=new Person();
    		//使用.属性 可以查看属性,并修改里面的值
    		p.name="zhangsan";
    		p.age=23;
    		//使用.方法 可以调用方法 
    		p.eat("草");
    	}
    }
    //创建Person对象
    class Person{
    	//定义属性
    	//定义在类中的变量叫做成员变量
    	String name;
    	int age;
    	char gender;
    	double height;
    	double weight;
    	//定义方法
    	//定义在方法中的变量叫做局部变量
    	public void eat(String food){
    		System.out.println(name+"在吃"+food);
    	}
    }
    
    

    成员变量和局部变量

    定义位置不一样:成员变量定义在类中,局部变量定义在方法中

    作用范围:成员变量作用在整个类中,局部变量只能在定义它的方法中使用

    内存位置:成员变量是跟对象一样储存在堆中,会赋予默认值,局部变量是存储在栈中,不会自动给与默认值

    生命周期:成员变量在对象创建时储存在堆内存中,直到对象被回收才销毁,局部变量在方法执行的时候创建,方法执行完即销毁。

    上面的例子,food就是局部变量,name、age等就是成员变量。

    构造方法

    与类同名,没有返回值类型的方法,如果类中没有写构造方法,编译时会默认添加一个无参数构造方法,否则需要手动添加无参构造方法,创建对象根据参数的不同调用不同的构造方法,因此构造方法是可以重载的。

    this关键字

    可以使用this关键字来代替本类调用里面的属性和方法,this关键字代表的是当前活动的对象。

    package com.boe.object;
    
    public class StudentDemo {
    	public static void main(String[] args) {
    		//使用无参构造方法创建对象
    		Student s=new Student();
    		Student s1=new Student("张三",23);
    		//如果有参数构造方法不加this关键字,后面输出name为null,age为0,都为默认值
    		//即name和age根据java就近原则都赋值给局部变量name和age,没有赋值到成员变量
    		System.out.println(s1);
    			
    	}
    }
    class Student{
    	String name;
    	int age;
    	int number;
    	//定义有参数构造方法后,需要手动添加无参构造方法
    	public Student(String name,int age){
             //如果这里不加this关键字,根据java就近原则就不会赋值给成员变量
    		this.name=name;
    		this.age=age;
    		//this代表当前活动的对象的引用
    		System.out.println(this);
    	}
    	//补加无参构造方法
    	public Student(){
    		
    	}
    	public void study(){
    		System.out.println(name+"在学习");
    	}
    	/*@Override
    	public String toString() {
    		return "Student [name=" + name + ", age=" + age + ", number=" + number
    				+ "]";
    	}*/
    }
    
    

    测试结果,发现两次输出的对象地址一样,因此this关键字代表着当前活动的对象。

    可以使用this(参数)调用本类的对应形式的构造方法,如果使用this.方法(参数),调用的是对象的其他普通方法而不是构造方法。

    package com.boe.object;
    
    public class DriverDemo {
    
    	public static void main(String[] args) {
    		Driver d=new Driver("张三",23,5,180);
    		System.out.println(d);
    	}
    }
    class Driver{
    	String name;
    	int age;
    	int driverAge;
    	double height;
    	double weight;
    	//无参构造方法
    	public Driver(){
    		
    	}
    	//构造方法1
    	public Driver(String name,int age){
    		this.name=name;
    		this.age=age;
    	}
    	//构造方法2
    	public Driver(String name,int age,int driverAge,double height){
    		/*this.name=name;
    		this.age=age;*/
    		//上面代码有重复,想调用上面构造方法1的代码
    		//this.Driver(name,age); 会报错,这样调用只能调用普通方法
    		this(name,age);//this表达式,会根据参数形式调用对应构造方法,注意需要写在开头
    		this.driverAge=driverAge;
    		this.height=height;
    	}
    }
    
    

    构造代码块

    构造代码块也叫做初始化代码块,是定义在类中的,无论对象创建调用的是哪个构造方法,都会执行的代码,并且在构造方法之前执行,跟代码块的位置无关。

    package com.boe.object;
    
    public class BabyDemo {
    
    	public static void main(String[] args) {
    		Baby b1=new Baby();
    		Baby b2=new Baby("天王");
    	}
    }
    class Baby{
    	String name;
    	int gender;
    	
    	//针对构造方法中重复的代码,当构造方法多时,this关键字用起来会不太方便
    	//直接使用构造代码块
    	{
    		cry();
    		eat();
    	}
    	
    	//构造方法1 调用同样方法
    	public Baby(){
    		/*cry();
    		eat();*/
    		System.out.println("构造方法执行了");
    	}
    	//构造方法2 调用同样方法
    	public Baby(String name){
    		this.name=name;
    		/*cry();
    		eat();*/
    	}
    	public void cry(){
    		System.out.println("baby哭了");
    	}
    	public void eat(){
    		System.out.println("baby想喝奶");
    	}
    }
    

    测试结果发现,先执行了代码块中的方法,才执行构造方法

    局部代码块

    局部代码块是定义在方法中的,它的主要作用为限制变量的使用范围,以及限制了变量的生命周期,可以提高栈内存的利用率。

    package com.boe.object;
    
    public class LocalDemo {
    
    	public static void main(String[] args) {
    		int i=5;
    		//局部代码块
    		{
    			int j=6;
    			System.out.println(i+j);
    		}
    		//System.out.println(j); 打印j会编译错误
    	}
    }
    

    属性的私有化-private

    参考案例计算矩形的面积,如果我们给长和宽传递的参数是负数发现也可以计算出结果,这样是不允许的,可以在计算面积和周长的方法里添加条件判断,但是这样只是限定了方法执行不允许长和宽为负数,如果直接给属性赋值发现依然可以使用负数,这样就限制不了了,这个时候属性的private属性就出现了,并配合get和set方法可以解决上面的问题。当设置属性为private,则外部就看不到了,只能通过get和set方法访问并能限定。

    面向对象的特征

    封装、继承、多态和抽象是面向对象的特征,不仅仅是Java的特征。

    封装

    体现形式:方法、属性的私有化以及内部类

    属性的私有化:将属性用private修饰,只能通过get和set方法来进行访问和设置,在方法里可以限定数据的合法性,使得属性更加的符合场景要求。

    封装的优势:提高代码的可重用性,保证数据的合法性。

    继承

    参考猫和狗的类,其属性类似,可以将属性和方法提取写到一个animal类中,然后通过extends关键字让猫和狗和animal类产生联系,这就是继承。子类通过继承可以得到父类全部的数据域(属性和方法),但是只能使用父类一部分的方法和属性,还有一部分是不可见的(因为权限修饰符的使用?),这样可以提高代码的复用性。

    注意Java中的继承是单继承(C和C++则是多继承,多继承足够多的情况下,子类甚至可以不用写方法),即一个子类只能有一个父类,但是一个父类却可以有多个子类。单继承也可以提高代码的复用性,并且可以避免方法调用的混乱(如果也是多继承,则父类可能存在方法签名的同名方法,调用会有二义性)

    super关键字

    在子类中,可以通过super关键字表示父类对象的引用,通过它可以调用父类的属性和方法。super语句在子类构造方法中,代表会调用父类对应形式的构造方法来创建父类对象,如果没有手动指定super语句,编译时会默认添加。如果父类只提供含参数构造方法,则子类super中也必须传入参数,并且super语句需写在子类构造方法的第一行。

    package com.boe.extend;
    
    public class ExtendDemo {
    
    	public static void main(String[] args) {
    		Cat c=new Cat();
    		Dog d=new Dog();
    		d.eat();//调用父类方法
    	}
    
    }
    //父类/超类/基类
    class Animal{
    	String type;
    	String color;			
    	public Animal() {
    	}
    	public Animal(String type, String color) {
    		this.type = type;
    		this.color = color;
    	}	
    	//添加有参数构造方法	
    	public void eat(){
    		System.out.println("要吃东西");
    	}
    }
    //猫类 子类/派生类
    class Cat extends Animal{
    	//如果没写,默认情况下编译后会加上下面的构造方法,通过super()会调用父类对应形式的构造方法
    	//如果父类只提供了有参构造方法,则super中相对应也需要传入参数
    	public Cat(){
    		super("波斯猫","灰色");//super语句必须在子类构造方法的第一行
    		super.eat();
    	}
    	/*String type;
    	String color;	
    	public void eat(){
    		System.out.println("要吃东西");
    	}*/	
    }
    //狗类 子类/派生类
    class Dog extends Animal{
    	/*String type;
    	String color;	
    	public void eat(){
    		System.out.println("要吃东西");
    	}*/	
    }
    

    测试结果发现,输出了两次要吃东西,第一个是在创建Cat对象时调用了父类有参构造方法,并在Cat构造方法中又调用了父类的eat方法,因此输出。第二次输出是Dog对象创建时,由于类中没有添加构造方法,编译后会默认添加构造方法,并且在构造方法里面有super()方法,这样会调用父类无参数构造方法调用父类,正好父类也写了无参构造方法,因此可以编译通过。

    多态

    分为编译时多态(方法的重载,编译期就可以看出来要执行那个方法)和运行时多态(只有运行时才能确定具体执行的是哪个子类的方法),并且多态是基于继承和方法重写的。

    package com.boe.multimode;
    
    public class MultiModeDemo {
    
    	public static void main(String[] args) {
    		//向上造型,编译期能执行什么方法看左边父类,运行期具体执行什么看右边子类
    		//编译期不能确定c和d是什么类,编译期只要满足右边是Animal的子类就行
    		Animal c=new Cat();
    		c.eat();
    		Animal d=new Dog();
    		d.eat();
    	}
    
    }
    //父类
    class Animal{
    	String type;
    	String color;			
    	public Animal() {
    	}
    	public Animal(String type, String color) {
    		this.type = type;
    		this.color = color;
    	}	
    	//添加有参数构造方法	
    	public void eat(){
    		System.out.println("要吃东西");
    	}
    }
    //猫类 
    class Cat extends Animal{
    	//重写后方法
    	public void eat() {
    		System.out.println("猫在吃东西");
    	}
    	//子类添加一个方法
    	public void catchMouse() {
    		System.out.println("猫在捉老鼠");
    	}
    
    }
    //狗类 
    class Dog extends Animal{
    	//重写后方法
    	@Override
    	public void eat() {
    		System.out.println("狗在吃东西");
    	}
    	//子类添加方法
    	public void watchBadMan() {
    		System.out.println("狗在监视坏人");
    	}
    }
    

    权限修饰符

    Java中一共有四种常见修饰符,如下表,权限范围从大到小,面试常问。

    本类 子类中 同包 其他类中
    public
    protected ×
    默认 同包子类可以 ×
    private × × ×

    参考A,B,C类调用m方法。
    A类一个包

    package com.boe.extendsa;
    
    public class A {
    	
    	protected	void m() {
             System.out.println("A类的方法执行了");
    	}
    }
    

    B类一个包,并继承自A类

    package com.boe.extendsb;
    
    import com.boe.extendsa.A;
    
    public class B extends A{
    	public static void main(String[] args) {
    		B b=new B();
    		b.m();
    	}
    }
    

    C类一个包,和A类没有父子关系,里面调用B类对象方法。

    package com.boe.extendsc;
    
    import com.boe.extendsb.B;
    
    public class C {
    
    	public static void main(String[] args) {
    		B b=new B();
    		//b.m(); 编译报错方法不可见
    	}
    }
    
    

    方法的重写

    在父子类中存在方法签名一样的非静态方法,称之为方法的重写,调用的也是子类重写后的方法。方法重写有"两等两小一大''的限定

    1 子类方法权限修饰符的范围要大于等于父类方法权限修饰符的范围

    class A{
    	protected void method(){}
    }
    class B extends A{
    	protected void method(){}//权限修饰符只能大于等于protected
    }
    

    这个可以用多态向上造型案例来理解,假设子类方法权限修饰符比父类小,我们如果定义一个A a=new B()并调用a.method方法,如果B类中重写的方法是比protected小的private属性,则使用范围缩小可能造成方法不可用,所以才有上面结论。

    2 子类重写父类方法,方法签名要相等

    3 如果父类方法的返回值类型是基本类型或void,则子类重写方法的返回值类型和父类相等。

    4 如果父类方法的返回值类型是引用类型,子类重写方法的返回值类型要么和父类方法返回值一致要么返回类型是子类型。

    class A{
    
    }
    class B extends A{
    	public void m(){}//子类B中添加m方法
    }
    class C{
    	public A method(){return null}
    }
    class D extends C{
    	public A method(){return null}//返回值类型要么是A,要么是A的子类B
    }
    

    这个同样可以用多态向上造型案例来理解,如果C类的返回值类型是B类,D类方法的返回值类型是父类A,则在编译期调用 c.method()时还可以调用m方法,但是运行期 c.method()执行后返回的是A类对象,其并没有m方法因而会报错,所以才有上面的结论。

    class A{
    
    }
    class B extends A{
    	public void m(){}//子类B中添加m方法
    }
    class C{
    	public B method(){return new B()}
    }
    class D extends C{
    	public A method(){return new A()}
    }
    public staic void main(String[] args){
        C c=new D();
        c.method().m();//执行时method方法返回A类对象,而A类对象并没有m方法
    }
    

    static关键字

    static是一个修饰符,可以修饰变量、方法、代码块和内部类。

    修饰变量

    static修饰的变量是静态变量(也叫做类变量),静态变量在类加载的时候加载到了方法区,并且在方法区中赋予了默认值和地址,静态变量不需要依赖创建对象而存在,其优先于对象出现,可以通过类名来调用静态变量,参考萧峰的功夫和令狐冲的功夫案例。

    package com.boe.staticdemo;
    /**
     * 静态变量
     * @author clyang
     */
    public class StaticDemo {
    
    	public static void main(String[] args) {
    		//创建一个对象
    		Person p1=new Person();
    		p1.name="萧峰";
    		p1.age=45;
    		p1.gender='男';
    		p1.kungfu="降龙十八掌";
    		//创建第二个对象
    		Person p2=new Person();
    		p2.name="令狐冲";
    		p2.age=28;
    		p2.gender='男';
    		p2.kungfu="独孤九剑";
    		//打印两个大侠
    		System.out.println(p1);
    		System.out.println(p2);
    	}
    }
    class Person{
    	String name;
    	int age;
    	char gender;
    	static String kungfu;//静态变量,功夫
    	@Override
    	public String toString() {
    		return "Person [name=" + name + ", age=" + age + ", gender=" + gender
    				+ ", kungfu="+kungfu+"]";
    	}	
    }
    
    

    测试结果发现,功夫都变成了‘独孤九剑’,‘降龙十八掌’不见了,这是为什么呢,这个需要从类加载到创建对象的过程中,内存中的栈、堆和方法区中发生的事情去理解。

    待补充...

    上面案例总结一下就是,类是加载到方法区中的,.java文件编译成.class文件后会加载到方法区,并且第一次加载到方法区中后不再移除,使用了静态变量的类创建对象后,对象中静态变量保存的也是方法区中的地址,所有这个类创建的对象都将共享这个静态变量。

    注意静态变量不能定义在构造方法中,也不能定义在构造代码块中,只能定义在类中。

    修饰方法

    用static修饰的方法,也叫做类方法,随着类的加载而存储到方法区,静态方法也是优于对象存在的,可以直接通过类名.方法名来调用。在静态方法中不能直接使用本类中的非静态属性和非静态方法。

    class StaticMethod{
    	int i;
        public void m(){}
        //静态方法不能直接调用本类的非静态属性和非静态方法
        public static void method(){
            i=123;//本质上是this.i=123
            m();//本质上是this.m(),但是this是当前对象,但是静态方法是优先于对象存在的,这里却依赖对象,时间上不符合,无法使用
        }
    }
    

    静态方法中也不能定义静态变量,首先静态变量只能定义在类中,在类加载到方法区后就创建了静态变量,而静态方法加载存储到方法区后不一定执行,只有调用时才在栈中执行,如果静态变量定义在静态方法中,需要等静态方法被调用才能创建,时间上是不合理的,另外静态方法执行在栈中,定义的变量也在栈中,空间上也是不符合的。

    静态方法可以重载

    package com.boe.staticdemo;
    
    public class StaticMethod1 {
    	//静态方法可以重载
    	public static void main(String[] args) {
    		A.method();
    		A.method(5);
    	}
    }
    class A{
    	public static void method() {
    		System.out.println("m1");
    	}
    	public static void method(int i) {
    		System.out.println(i);
    	}
    }
    

    静态方法可以被继承

    package com.boe.staticdemo;
    
    public class StaticMethod2 {
    	//静态方法可以继承
    	public static void main(String[] args) {
    		D.method();
    	}
    
    }
    class C{
    	public static void method() {
    		System.out.println("C's method");
    	}
    }
    class D extends C{
    	
    }
    

    静态方法不可以被重写,但是能构成隐藏,注意下面采用向上造型创建一个e对象,调用静态方法执行的是父类,而不是子类。

    package com.boe.staticdemo;
    
    public class StaticMethod3 {
    	//静态方法不能被重写,下面这种情况构成了父子类同名签名静态方法的隐藏
    	public static void main(String[] args) {
    		E e=new F();
    		e.method();//E's method 调用的父类的静态方法
    	}
    }
    class E{
    	public static void method() {
    		System.out.println("E's method");
    	}
    }
    class F extends E{
    	/*
    	 * @Override 
    	 * public static void method() { System.out.println("F's method"); }
    	 */
    	//编译报错:The method method() of type F must override or implement a supertype method
    	public static void method() { System.out.println("F's method"); }
    }
    

    父子类中可以存在方法签名一样的静态方法,叫做方法的隐藏(hide),父子类中如果存在方法签名一样的方法,要么是重写的非静态方法,要么是隐藏的静态方法。

    修饰代码块

    static{
     代码块
    }
    

    静态代码块中内容只在类第一次加载的时候执行一次。

    package com.boe.staticdemo;
    
    public class StaticBlock1 {
    	static {
    		System.out.println("我是主函数静态代码块");
    	}
    	//静态代码块
    	public static void main(String[] args) {
    		SD s=new SD();
    		SD.method();
    	}
    
    }
    class SD{
    	static {
    		System.out.println("我是静态代码块");
    	}
    	public static void method() {
    		System.out.println("我是静态方法");
    	}
    }
    

    静态代码块执行次序问题,参考如下代码。

    package com.boe.staticdemo;
    
    public class StaticBlock2 {
    	//静态代码块执行顺序
    	public static void main(String[] args) {
    		//new SA();
    		new SB();
    		System.out.println("------分割线------");
    		new SB();
    	}
    
    }
    class SA{
    	SE se=new SE();//比构造代码块先执行
    	//静态代码块
    	static {
    		System.out.println("SA 1"); 
    	}
    	//构造代码块
    	{
    		System.out.println("SA 2");
    		//se=new SE();
    	}
    	//构造方法
    	public SA() {
    		System.out.println("SA 3");
    	}
    }
    class SB extends SA{
    	//静态变量
    	static SC c=new SC();
    	//静态代码块
    	static {
    		System.out.println("SB 1"); 
    	}
    	//同时有静态变量和静态代码块,谁在前面谁先执行
    	//构造代码块
    	{
    		System.out.println("SB 2");
    	}
    	//构造方法
    	public SB() {
    		System.out.println("SB 3");
    	}	
    }
    class SC{
    	public SC() {
    		System.out.println("SC");
    	}
    }
    class SE extends SC{
    	public SE() {
    		System.out.println("SE");
    	}
    }
    

    根据控制台的结果,大致分析出执行次序,先执行父类静态代码块,再执行子类静态代码块,然后执行父类其他代码,最后执行子类其他代码。

    step1 发现SB继承自父类SA,并且SB中没有构造方法,会默认执行如下代码,先创建父类对象,并发现父类中有静态代码块,因此先执行后得到结果SA 1

    public SB(){
    	super();
    }
    

    step2 然后执行子类静态代码块,先执行static SC c=new SC(),因此先打印出SC,然后执行下面一个静态代码块,打印出SB 1

    step3 父类和子类静态代码块执行完后,先执行父类其他代码,首先执行SE se=new SE(),而SE继承自SC,因此依次打印出SC和SE,然后构造代码块优先于构造方法执行,因此再打印出SA 2,最后再打印出SA 3

    step4 最后执行子类的其他方法,也是先执行构造代码块,然后执行构造方法,因此依次输出SB 2和SB 3

    静态代码块中定义变量

    案例1

    package com.boe.staticdemo;
    
    public class StaticBlock3 {
    
    	public static void main(String[] args) {
    		System.out.println(Demo.i);
    		/*Demo d=new Demo();
    		System.out.println(d.i);*/
    	}
    
    }
    class Demo{
    //	static int i=5;
    	static {
    		i=7;//只是标记为7
    		//i+=7;报错Cannot reference a field before it is defined
    		//System.out.println("此时i为"+Demo.i);
    	}
    	static int i=5;
    }
     
    

    案例2

    package com.boe.staticdemo;
    
    public class StaticBlock4 {
    
    	public static void main(String[] args) {
    		new SDemo();//1
    		System.out.println(SDemo.i);//5
    	}
    
    }
    class SDemo{
    //	static int i=5;
    	static {
    		System.out.println(++SDemo.i);//打印i的默认标记值
    	}
    	static int i=5;
    }
    

    案例3

    package com.boe.staticdemo;
    
    public class StaticBlock5 {
    
    	public static void main(String[] args) {
    		System.out.println(S.i);//5
    		System.out.println(S.j);//2
    	}
    }
    class S{
    	//情况0 不加static静态块
    	
    	//情况1
    	static{
    		S.i++;
    		S.j++;
    	}
    	
    	static S e=new S();
    	static  int i=5;
    	static int j;
    	
    	//情况2
    	/*static{
    	S.i++;
    	S.j++;
        }*/
    	
    	public S(){
    		i++;
    		j++;
    	}
    }
    
    

    类的加载分为7个阶段,JVM虚拟机执行.class字节码的过程可以分为加载、验证、准备、解析、初始化、使用、卸载7个过程,上面案例执行结果需要使用这个来理解。

    在初始化阶段之前,有一个准备阶段,准备阶段给静态变量(类变量)在方法区开辟内存空间,并设置初始值(一般为Java中该数据类型的初始零值,如int就是0,float就是0.0f),另外它可能还有标记值(就是用户想赋予的值,用户没有赋予就是默认数据类型的初始值),在准备阶段标记值是不赋予静态变量的(除了final修饰的静态变量),进入初始化阶段(代码中new一个实例,调用类的静态变量)后就会将标记值赋值给静态变量。

    参考如下Book类中案例。

    package com.boe.staticdemo;
    
    //复制博客https://blog.csdn.net/WeInfinite/article/details/84028445
    public class Book {
    	public static void main(String[] args) {
    		System.out.println("书的main方法");
    		Book b=new Book();
    	}
    
    	Book() {
    		System.out.println("书的构造方法");
    		System.out.println("price=" + price + ",amount=" + amount);
    	}
    
    	{
    		System.out.println("书的普通代码块");
    	}
    	int price = 110;
    	
    	static {
    		System.out.println("书的静态代码块");
    	}
    	static int amount = 112;
    }
    
    

    参考博文:

    (1)https://www.jianshu.com/p/f677f6a5b549

    (2)https://www.cnblogs.com/lubocsu/p/5099558.html

    (3)https://www.jb51.net/article/86629.htm

    (4)http://java-decompiler.github.io/ 下载反编译软件和插件

    (5)https://blog.csdn.net/WeInfinite/article/details/84028445 类在JVM中的加载过程

  • 相关阅读:
    Python基础-博客
    已开发程序查询
    新语法
    html基础
    python操作excel
    Selenium
    练习2---(request,ajax爬取头条数据)
    练习1---request+正则(猫眼电影)
    框架---scrapy(基本用法)
    框架---Scrapy (框架安装)
  • 原文地址:https://www.cnblogs.com/youngchaolin/p/11406717.html
Copyright © 2020-2023  润新知