Java应用基础微专业-设计篇
第1章--抽象与接口
1.1 抽象
An abstract class can be created without abstract methods, the purpose of doing this is to prevent instantiating the class.
A class with any abstract method must be abstract.
1.2 数据与表现分离
separation of the logical layer and the graphical interface.
责任驱动:whose responsibility?
1.3 接口
接口所有的方法都是没有方法体的,而且都是public abstract,即使你没有这样声明。
接口中的所有成员变量都是public static final的变量,并且必须有定义初始化,因为所有变量都必须在编译的时候有确定值。
Java不允许多继承(一个类有两个父类),但是允许一个类实现多个接口,也允许一个接口从多个接口得到继承,但是不允许接口从类继承。
接口--任何需要传入传出的一定是接口而不是具体的类
pro: 适合多人同时写一个大程序
con: 代码量膨胀起来很快
第2章--异常
2.1 异常的捕捉
// Question:
// how is the ArrayIndexOutOfBoundsException thrown by f() ? public class ExceptionThrown { public static void f() {
int[] a = new int[10];
a[10] = 10;
System.out.println("hello");
} public static void g() {
f();
} public static void h() {
int i = 10;
if (i < 100) {
g();
}
} public static void k() {
try {
g();
} catch (NullPointerException e) {
System.out.println("k()");
}
} public static void main (String[] args) {
try {
k();
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Exception Caught");
System.out.println(e);
e.printStackTrace();
}
}
}
捕捉异常后 可以处理异常,也可再次抛出
catch(Exception e) {
...
throw e;
}
finally{} -- execute finally block after try/catch.
2.2 异常的抛出和声明
若函数可能抛出异常,就必须在函数头部声明 throws ...
若调用一个声明了会抛出异常的函数,这需要try/catch来解决异常或重新抛出异常。
所有异常都是Exception类的子类。
捕捉异常时可以catch到异常的子类。
多句catch语句时会按照先后顺序匹配,若匹配到则不会继续匹配。
若同时有两句catch捕捉子类和父类异常,
注意"Unreachable catch block for child Exception" if child exception is put after the catch parent exception.
当覆盖一个函数的时候,子类的函数声明抛出的异常不能比被覆盖的父类函数声明抛出的异常多。
因为有可能拿着子类的对象当成父类的对象来看待: upper cast
public class A {
public void f() throws AException {}
} public class A1 extends A {
public void f() {} // works ok
public void f() throws AException {} // works ok
public void f() throws AException, BException {} // Error: Exception BException is not compatible with throws clause in A.f()
}
在子类的构造函数中,必须声明父类可能抛出的全部异常。
因为子类的构造器会自动调用父类的构造器。
第3章--IO
3.1 流
处理输入输出的方法:stream -- unidirectional; one-dimensional.
java.io: Package
InputStream & OutputStream -- abstract class in java.io
InputStream:
read(); read(byte b[]); read(byte[], int off, int len); --stream of bytes
int available(); mark(); reset(); markSupported(); close();
public class InputStreamTest { public static void main(String[] args) {
System.out.println("Ready to read in: ");
byte[] buffer = new byte[1024];
try {
int len = System.in.read(buffer); // how many bytes read in
String s = new String(buffer, 0 ,len); // start at 0, with length len
System.out.println("read in " + len + " bytes.");
System.out.println(s);
System.out.println("length of s: " + s.length());
} catch (IOException e) {
e.printStackTrace();
}
}
}
when the input stream contains Chinese characters, the value of len would be different from the value of s.length()
OutputStream:
write(); write(int b); write(byte b[]); write(byte b[], int off, int len);
flush()--; close();
File stream 文件流:
FileInputStream & FileOutputStream -- 实际工程中较少使用:常用在内存数据或通信数据上建立的流;具体的文件读写有更专业的类如配置文件和日志文件.
public static void main(String[] args) {
System.out.println("prompt");
byte[] buffer = new byte[10];
for(int i = 0; i < buffer.length; i++) {
buffer[i] = (byte) i;
}
try {
FileOutputStream out = new FileOutputStream("a.dat");
out.write(buffer);
// a file called a.dat will be created of size 10 bytes
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
流过滤器:
以一个介质流对象为基础,层层构建过滤器流,最终形成的流对象能在数据的输入输出过程中,逐层使用过滤器流的方法来读写数据。
public static void main(String[] args) {
System.out.println("prompt"); // data stream: input/output the primitive types value
DataOutputStream out;
try {
out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("a.dat")));
// nested stream -- bytes from FileOutputStream to BufferedOutputStream, to DataOutputStream
// int i = 0xcafebabe; // hex
int i = 123456; // what is written in the file is binary
out.writeInt(i);
out.close(); DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("a.dat")));
int j = in.readInt();
in.close();
System.out.println(j); } catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
3.2 文本流
The class we use previously (Data stream) is only available for dealing with data of primitive types (binary data).
What if we wanna handle text? -- Reader & Writer.
public static void main(String[] args) {
System.out.println("prompt"); try {
PrintWriter out = new PrintWriter (
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("a.txt"))));
// FileOutputStream: bytes
// OutputStreamWriter: the bridge between stream and writer
int i = 123456;
out.println(i);
out.close(); // BufferedReader
BufferedReader in = new BufferedReader(
new InputStreamReader(
new FileInputStream("src/ReaderWriter.java")));
String line;
while ((line = in.readLine()) != null) {
// read in line by line
System.out.println(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
Other classes:
LineNumberReader -- is able to get the line number and switch between lines
FileReader -- is the child class of InputStreamReader.
FileReader(File file);/ FileReader(String fileName); -- create a reader on a file directly.
文件编码方式的影响:
国标GB码:2bytes一个汉字
Unicode编码:所有平台上通用。
UTF-8:如果全部是英文字符,一个byte就能表示一个英文字符;如果有汉字(对他们来说不常见),这需要(可能)3个字节表示
若:上述程序读入的是UTF-8编码的文件,则会输出乱码--FileInputStream处理二进制数据后交给InputStreamReader处理文本(二进制转换为文本:default方法:系统默认方式)
程序改进--在InputStreamReader中给定处理方式new InputStreamReader(new FileInputStream("utf8.txt"), "utf8"));
(创建使用给定字符集的InputStreamReader: InputStreamReader(InputStream in, Charset cs);
创建使用给定字符集解码器的InputStreamReader: InputStreamReader(InputStream in, CharsetDecoder dec);
创建使用指定字符集的InputStreamReader: InputStreamReader(InputStream in, String charsetName);)
面对繁多的输入输出类,如何选择呢?
是二进制数据?-- InputStream
否 -- 表达的是文本? -- Reader (readLine...)
表达的是基本数据类型? -- Scanner(nextInt\nextDouble...)
3.3 流的应用 (instances)
network socket
public static void main(String[] args) { try {
// construct connection
Socket socket = new Socket(InetAddress.getByName("localhost"), 12345);
PrintWriter out = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())));
out.println("Hello"); // write Hello
out.flush(); // kinda like refresh // Read in
BufferedReader in = new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
String line;
line = in.readLine();
System.out.println(line); out.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
} }
在terminal中打开nc -l 12345端口后,运行程序。
read()函数是阻塞的,在读到所需的内容之前会停下来等(基于read()的函数也是如此,如nextInt(), readLine())
常用单独的线程来做socket读入的等待,或使用nio的channel选择机制
对于socket,可以设置SO时间:setSoTimeout(int timeOut);
对象串行化:将类整个写到流里去 (Interface Serialization)
ObjectInputStream & ObjectOutputStream
public static void main(String[] args) {
Student s1 = new Student("John", 18, 5);
System.out.println("s1" + s1); try {
ObjectOutputStream out;
out = new ObjectOutputStream (
new FileOutputStream("obj.dat"));
out.writeObject(s1);
out.close();
} catch (IOException e) {
e.printStackTrace();
} ObjectInputStream in;
try {
in = new ObjectInputStream(
new FileInputStream("obj.dat"));
Student s2 = (Student)in.readObject();
System.out.println("s2" + s2);
in.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} }
第4章--设计原则
4.1 城堡游戏(例子)
package castle; public class Room {
public String description;
public Room northExit;
public Room southExit;
public Room eastExit;
public Room westExit; public Room(String description)
{
this.description = description;
} public void setExits(Room north, Room east, Room south, Room west)
{
if(north != null)
northExit = north;
if(east != null)
eastExit = east;
if(south != null)
southExit = south;
if(west != null)
westExit = west;
} @Override
public String toString()
{
return description;
}
}
package castle; import java.util.Scanner; public class Game {
private Room currentRoom; public Game()
{
createRooms();
} private void createRooms()
{
Room outside, lobby, pub, study, bedroom; // 制造房间
outside = new Room("城堡外");
lobby = new Room("大堂");
pub = new Room("小酒吧");
study = new Room("书房");
bedroom = new Room("卧室"); // 初始化房间的出口
outside.setExits(null, lobby, study, pub);
lobby.setExits(null, null, null, outside);
pub.setExits(null, outside, null, null);
study.setExits(outside, bedroom, null, null);
bedroom.setExits(null, null, null, study); currentRoom = outside; // 从城堡门外开始
} private void printWelcome() {
System.out.println();
System.out.println("欢迎来到城堡!");
System.out.println("这是一个超级无聊的游戏。");
System.out.println("如果需要帮助,请输入 'help' 。");
System.out.println();
System.out.println("现在你在" + currentRoom);
System.out.print("出口有:");
if(currentRoom.northExit != null)
System.out.print("north ");
if(currentRoom.eastExit != null)
System.out.print("east ");
if(currentRoom.southExit != null)
System.out.print("south ");
if(currentRoom.westExit != null)
System.out.print("west ");
System.out.println();
} // 以下为用户命令 private void printHelp()
{
System.out.print("迷路了吗?你可以做的命令有:go bye help");
System.out.println("如:\tgo east");
} private void goRoom(String direction)
{
Room nextRoom = null;
if(direction.equals("north")) {
nextRoom = currentRoom.northExit;
}
if(direction.equals("east")) {
nextRoom = currentRoom.eastExit;
}
if(direction.equals("south")) {
nextRoom = currentRoom.southExit;
}
if(direction.equals("west")) {
nextRoom = currentRoom.westExit;
} if (nextRoom == null) {
System.out.println("那里没有门!");
}
else {
currentRoom = nextRoom;
System.out.println("你在" + currentRoom);
System.out.print("出口有: ");
if(currentRoom.northExit != null)
System.out.print("north ");
if(currentRoom.eastExit != null)
System.out.print("east ");
if(currentRoom.southExit != null)
System.out.print("south ");
if(currentRoom.westExit != null)
System.out.print("west ");
System.out.println();
}
} public static void main(String[] args) {
Scanner in = new Scanner(System.in);
Game game = new Game();
game.printWelcome(); while ( true ) {
String line = in.nextLine();
String[] words = line.split(" ");
if ( words[0].equals("help") ) {
game.printHelp();
} else if (words[0].equals("go") ) {
game.goRoom(words[1]);
} else if ( words[0].equals("bye") ) {
break;
}
} System.out.println("感谢您的光临。再见!");
in.close();
} }
4.2 消除代码复制
duplication -- bad for maintenance.
solution--using methods; inheritance.
System.out.println("你在" + currentRoom);
System.out.print("出口有: ");
if(currentRoom.northExit != null)
System.out.print("north ");
if(currentRoom.eastExit != null)
System.out.print("east ");
if(currentRoom.southExit != null)
System.out.print("south ");
if(currentRoom.westExit != null)
System.out.print("west ");
System.out.println();
duplications in printWelcome() and goRoom();
4.3 封装
encapsulation.
good design: low coupling & high cohesion.
for code modification & code reuse.
Lower coupling by using encapsulation
bad design: variables in Room set as public
solution: private visualization should be used
but how to access these variables? add getters? Not an OOP solution.
-- coupling: what does these exits used for?
We can make Room class tell Game about its exits, rather than figure it out in Game class.
getExitDesc() in Room
public String getExitSDesc() {
StringBuffer sb = new StringBuffer(); if(northExit != null)
sb.append("north ");
if(eastExit != null)
sb.append("east ");
if(southExit != null)
sb.append("south ");
if(westExit != null)
sb.append("west "); return sb.toString();
}
the reason why not using String s = ""; s+="north ";... -- immutable -> new and replace -> to much cost.
an efficient way to do this: StringBuffer.
-- coupling: same for move operation in Game.goRoom()
public Room moveToNextRoom(String direction) {
Room nextRoom = null;
if(direction.equals("north")) {
nextRoom = northExit;
}
if(direction.equals("east")) {
nextRoom = eastExit;
}
if(direction.equals("south")) {
nextRoom = southExit;
}
if(direction.equals("west")) {
nextRoom = westExit;
}
return nextRoom;
}
4.4 可扩展性
可扩展性的意思就是代码的某些部分不需要经过修改就能适应将来可能的变化。
add functions: go up/down:
用容器来实现灵活性--原本: 用成员变量来表示Room的方向,增减方向需要改变代码。
solution: 用hash table来表示方向,那么方向就不是“硬编码”的了。
four private Room variables should not be created in Room class directly -- changing to use HashMap<String, Room> instead.
adjust the method setExits();
public void setExit(String dir, Room room) {
exits.put(dir, room);
}
change the initialization of the exits to:
outside.setExit("east", lobby);
outside.setExit("south", study);
outside.setExit("west", pub);
lobby.setExit("west", outside);
pub.setExit("east", outside);
study.setExit("north", outside);
study.setExit("east", bedroom);
bedroom.setExit("west", study); // extended exits
lobby.setExit("up", pub);
pub.setExit("down", lobby);
4.5 框架加数据
进一步加强可拓展性:从程序中识别出框架和数据,以代码实现框架,将部分功能以数据的方式加载。
上一小节中的exits改动就是框架的使用。
还有一个地方也可以使用到这个理论来改良代码质量:读取指令的部分
while ( true ) {
String line = in.nextLine();
String[] words = line.split(" ");
if ( words[0].equals("help") ) {
game.printHelp();
} else if (words[0].equals("go") ) {
game.goRoom(words[1]);
} else if (words[0].equals("bye") ) {
break;
}
readInCmd();
}
while loop中的String-method pair可不可以也用HashMap的框架表示呢?
借助子类class中的override method来实现。
public void play() {
Scanner in = new Scanner(System.in);
while ( true ) {
String line = in.nextLine();
String[] words = line.split(" "); CmdHandler handler = handlers.get(words[0]);
if (handler != null) {
handler.cmdHandling();
if (handler instanceof ByeHandler) {
break;
} else if (handler instanceof GoHandler) {
if (words.length == 2) {
((GoHandler)handler).cmdHandling(words[1]);
} else {
System.out.println("Invalid Command (Usage: go dir)\n");
}
}
} else {
System.out.println("Invalid Command\n");
}
}
in.close();
}
public class CmdHandler { public void cmdHandling() { }
} public class GoHandler extends CmdHandler{ private Game game; public GoHandler(Game game) {
this.game = game;
} public void cmdHandling(String dir) {
game.goRoom(dir);
}
} public class HelpHandler extends CmdHandler{ public void cmdHandling() {
System.out.println("迷路了吗?你可以做的命令有:go bye help");
System.out.println("如:\tgo east");
}
} public class ByeHandler extends CmdHandler{ public void cmdHandling() {
System.out.println("感谢您的光临。再见!");
}
}
之后,若是想要加入新的cmd,添加Handler的子类即可。
第5章--设计模式
5.1 注入控制反转
GUI(Graphic User Interface):
部件:创建GUI的独立部分,比如按钮、菜单、菜单项、选择条等
布局:如何在屏幕上放置组件 -- 通过layout manager布局管理器实现
容器:可以在容器frame中放置部件(需要指定位置BorderLayout)(容器本身也是部件)
事件处理:用来响应用户的输入。用户激活一个部件,系统就会产生一个事件,应用程序就可以收到关于这个时间的通知(调用一个方法)。
BorderLayout:NORTH/ SOUTH/ EAST/ WEST/ CENTER. (default-CENTER) (overwrite if the position is the same)
Swing:event handling处理用户输入。
动作事件:点击按钮或选中菜单项
鼠标事件:点击或移动鼠标
窗口事件:框架被关闭或最小化
等等。
Q: How does a JButton object invoke the methods in my class?
A: One way can be done on our own: have a class MyButton extends JButton and override the doPressed() method to invoke myMethod.
A: What Swing does: 注入反转(反转控制)
JButton class; ActionListener interface;
JButton.addActionListener()/JButton.removeActionListener().
ActionListener.actionPerformed();
implement actionPerformed()
when click the JButton, check any actionListener related
JButton knows its registered ActionListener
invoke ActionListener.actionPerformed();
// add button
JButton btnStep = new JButton("下一步");
frame.add(btnStep, BorderLayout.NORTH); // which component, and where
btnStep.addActionListener(new ActionListener() { @Override
public void actionPerformed(ActionEvent e) {
step();
frame.repaint();
}
});
上例中:ActionListener作为一个内部类(一个类定义在另一个类的内部,从而成为外部类的一个成员,具有和成员变量成员方法相同的性质)
内部类的最重要的特点就是能够访问外部类的所有成员(包括私有成员);当外部是函数时,只能访问那个函数里的final变量(?)。
此外:ActionListener也是一个匿名类(在new对象的时候给出的类的定义),匿名类可以继承某类,也可以实现某接口
5.2 MVC设计模式
JTable: separating the data and representation.
public static void main(String[] args) {
JFrame frame = new JFrame();
JTable table = new JTable(new CurriculumData());
// JTable cannot display all of its content
JScrollPane panel = new JScrollPane(table);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(panel);
frame.setSize(400, 400);
frame.setVisible(true);
}
CurriculumData is a class that stores the data
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel; public class CurriculumData implements TableModel { private String[] title = {
"周一","周二","周三","周四","周五","周六","周日"
}; private String[][] curriculum = new String[8][7]; @Override
public int getRowCount() {
return 8;
} @Override
public int getColumnCount() {
return 7;
} @Override
public String getColumnName(int columnIndex) {
// the name of each column
return title[columnIndex];
} @Override
public Class<?> getColumnClass(int columnIndex) {
// the data type of each column
return String.class;
} @Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return true;
} @Override
public Object getValueAt(int rowIndex, int columnIndex) {
// get the data value at a particular cell
return curriculum[rowIndex][columnIndex];
} @Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
// set the data value at a particular cell
curriculum[rowIndex][columnIndex] = (String) aValue;
} @Override
public void addTableModelListener(TableModelListener l) {
// TODO Auto-generated method stub
} @Override
public void removeTableModelListener(TableModelListener l) {
// TODO Auto-generated method stub
}
}
what happens inside is: There is a Table Model object between JTable and JTableData as the interface.
|
\|/
MVC (Model, View, Control)
模型:保存和维护数据,提供接口让外部(Control)修改数据,notify表现(View)刷新呈现的数据
表现:从模型获得数据,根据数据画出表现
控制:从用户得到输入,根据输入调用Model的接口来调整数据
NB: Control和View没有直接的联系
好处:每一个部分的实现都很单纯,比如View只需知道每次全部重画一遍,无需在意修改的细节
上例中:Model (TableModel); View&Control (JTable) -- 常见MVC实现方式,将View&Control合并(都是显示部分)
Java应用基础微专业-设计篇的更多相关文章
- Java应用基础微专业-工程篇
第1章-命令行 1.1 命令行基础 ls -a: list all files (including hidden files) .DS_Store: files detailed informati ...
- Java应用基础微专业-进阶篇
第1章--使用对象 1.1 字符类型 char c = 65; // char --> int char c = '\u0041'; // \u: unicode + (Hex 41--> ...
- Java应用基础微专业-入门篇
第1章--用程序来做计算 1.1 第一个Java程序 Mac version: Preference -> General -> Keys -> Search "Conte ...
- 《手把手教你》系列基础篇(九十六)-java+ selenium自动化测试-框架之设计篇-跨浏览器(详解教程)
1.简介 从这一篇开始介绍和分享Java+Selenium+POM的简单自动化测试框架设计.第一个设计点,就是支持跨浏览器测试. 宏哥自己认为的支持跨浏览器测试就是:同一个测试用例,支持用不同浏览器去 ...
- 总结删除文件或文件夹的7种方法-JAVA IO基础总结第4篇
本文是Java IO总结系列篇的第4篇,前篇的访问地址如下: 总结java中创建并写文件的5种方式-JAVA IO基础总结第一篇 总结java从文件中读取数据的6种方法-JAVA IO基础总结第二篇 ...
- 总结java中文件拷贝剪切的5种方式-JAVA IO基础总结第五篇
本文是Java IO总结系列篇的第5篇,前篇的访问地址如下: 总结java中创建并写文件的5种方式-JAVA IO基础总结第一篇 总结java从文件中读取数据的6种方法-JAVA IO基础总结第二篇 ...
- 归纳从文件中读取数据的六种方法-JAVA IO基础总结第2篇
在上一篇文章中,我为大家介绍了<5种创建文件并写入文件数据的方法>,本节我们为大家来介绍6种从文件中读取数据的方法. 另外为了方便大家理解,我为这一篇文章录制了对应的视频:总结java从文 ...
- 总结java创建文件夹的4种方法及其优缺点-JAVA IO基础总结第三篇
本文是Java IO总结系列篇的第3篇,前篇的访问地址如下: 总结java中创建并写文件的5种方式-JAVA IO基础总结第一篇 总结java从文件中读取数据的6种方法-JAVA IO基础总结第二篇 ...
- Java Web基础 --- Servlet 综述(理论篇)
摘要: Web 技术成为当今主流的互联网 Web 应用技术之一,而 Servlet 是 Java Web 技术的核心基础.本文首先从请求/响应架构应用的大背景谈起 Servlet 的由来,明确 Ser ...
随机推荐
- linux下构建SVN
1. 安装subversion#yum -y install subversion2. 安装好了之后 新建一个svn目录#mkdir /home/svn3. 新建两个版本仓库#svnadmin cre ...
- 实例:接口并发限流RateLimiter
需求:接口每秒最多只能相应1个请求 1.创建 全局类对象 import com.google.common.util.concurrent.RateLimiter; import org.spring ...
- CSU-ACM2018暑期训练7-贪心
A:合并果子(贪心+优先队列) B:HDU 1789 Doing Homework again(非常经典的贪心) C:11572 - Unique Snowflakes(贪心,两指针滑动保存子段最大长 ...
- The App Life Cycle & The Main Function
The App Life Cycle Apps are a sophisticated interplay between your custom code and the system framew ...
- iOS视频倒放
iOS视频倒放 视频的倒放就是视频从后往前播放,这个只适应于视频图像,对声音来说倒放只是噪音,没什么意义,所以倒放的时候声音都是去除的. 倒放实现 一般对H264编码的视频进行解码,都是从头至尾进行的 ...
- 可能是catalan数吧
What's the number of distinct BSTs containing nodes {1, 2, 3 ,4}? 包含节点{1,2,3,4}的不同二叉搜索树有多少棵? int Num ...
- anyconnect connection attempt has failed
anyconnect connection attempt has failed 在控制面板-网络与Internet-网络连接,右键AnyConnect secure连接适配器,点击属性 在连接项目中 ...
- 关于iconfont symbol引入字体的方式
1,下载想要使用的图标集合 2,下载的压缩包解压到将要使用的目录下: 3,使用: 4,效果
- 20190120-自定义实现split方法
1. 实现字符串的split方法Python split() 通过指定分隔符对字符串进行切片,如果参数 num 有指定值,则分隔 num+1 个子字符串 思路同自定义实现replace方法类型: 1. ...
- Python 爬虫 七夕福利
祝大家七夕愉快 妹子图 import requests from lxml import etree import os def headers(refere):#图片的下载可能和头部的referer ...