网络爬虫全解析——技术、原理与实践(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-20 06:18:54

点击下载

作者:罗刚

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

网络爬虫全解析——技术、原理与实践

网络爬虫全解析——技术、原理与实践试读:

前言

现代社会,有效信息对人来说就像氧气一样不可或缺。互联网让有效信息的收集工作变得更容易。当你在网上冲浪时,网络爬虫也在网络中穿梭,自动收集互联网上有用的信息。

自动收集和筛选信息的网络爬虫让有效信息的流动性增强,让我们更加高效地获取信息。随着越来越多的信息显现于网络,网络爬虫也越来越有用。

各行业都离不开对信息的采集和加工处理。例如,农业需要抓取气象数据、农产品行情数据等实现精准农业。机械行业需要抓取零件、图纸信息作为设计参考。医药行业需要抓取一些疾病的治疗方法信息。金融行业需要抓取上市公司基本面和技术面等相关信息作为股市涨跌的参考,例如,太钢生产出圆珠笔头,导致它的股票“太钢不锈”上涨。此外,金融行业也需要抓取股民对市场的参与度,作为市场大势判断的依据。

每个人都可以用网络爬虫技术获得更好的生存策略,避免一些糟糕的情况出现,让自己生活得更加幸福和快乐。例如,网络爬虫可以收集到二甲双胍等可能抗衰老的药物,从而让人生活得更加健康。

本书的很多内容来源于搜索引擎、自然语言处理、金融等领域的项目开发和教学实践。感谢开源软件的开发者们,他们无私的工作丰富了本书的内容。

本书从开发网络爬虫所需要的Java语法开始讲解,然后介绍基本的爬虫原理。通过介绍优先级队列、宽度优先搜索等内容,引领读者入门,之后根据当前风起云涌的云计算热潮,重点讲述了云计算的相关内容及其在爬虫中的应用,以及信息抽取、链接分析等内容。接下来介绍了有关爬虫的Web数据挖掘等内容。为了让读者更深入地了解爬虫的实际应用,最后一章是案例分析。本书相关的代码在读者QQ群(294737705)的共享文件中可以找到。

本书适合需要具体实现网络爬虫的程序员使用,对于信息检索等相关领域的研究人员也有一定的参考价值,同时猎兔搜索技术团队已经开发出以本书为基础的专门培训课程和商业软件。目前的一些网络爬虫软件仍有很多功能有待完善,作者真诚地希望通过本书把读者带入网络爬虫开发的大门并认识更多的朋友。

感谢早期合著者、合作伙伴、员工、学员、家人的支持,他们给我们提供了良好的工作基础,这是一个持久可用的工作基础。在将来,希望我们的网络爬虫代码和技术能够像植物一样快速生长。

参与本书编写的还有崔智杰、石天盈、张继红、张进威、刘宇、何淑琴、任通通、高丹丹、徐友峰、孙宽,在此一并表示感谢。罗刚2017年2月

轻松注册成为博文视点社区用户(www.broadview.com.cn),您即可享受以下服务:● 下载资源:本书所提供的示例代码及资源文件均可在【下载资

源】处下载。● 提交勘误:您对书中内容的修改意见可在【提交勘误】处提交,

若被采纳,将获赠博文视点社区积分(在您购买电子书时,积分

可用来抵扣相应金额)。● 与作者交流:在页面下方【读者评论】处留下您的疑问或观点,

与作者和其他读者一同学习交流。

页面入口:http://www.broadview.com.cn/31071

二维码:第1章技术基础

很多种编程语言都可以用来开发爬虫。相对于Python,Java由于严谨的语法结构和体系结构,所以在开发爬虫方面有后发优势。

很多网络爬虫是使用Java或者C#语言开发的。如果是开发采集器那样的客户端爬虫,那么可以使用C#开发爬虫。如果是运行在服务器端的爬虫,则可以用Java开发。

只要有目标,你可以做到很多从来没有做过的事情。没有基础也可以学习开发网络爬虫,本章是专门为开发爬虫写的Java基础介绍。1.1 第一个程序

Java程序都运行在虚拟机上。为什么要用虚拟机,而不是直接运行在本机的操作系统上?因为Windows是收费的,而Linux可以免费使用。可以把Windows当作开发环境使用,而把程序部署在Linux上。因为运行在指令集相同的虚拟机上,所以Java程序可以不经修改地在不同操作系统之间切换。

并不一定要自己买房子以后才有地方住。并不一定要在本机安装开发环境以后,才能运行第一个Java程序。有一些在线的开发环境可运行Java程序,例如http://ideone.com/。

第一个Java程序是从一个类中定义的main方法开始执行的。public class Crawler{ public static void main (String args[]) { System.out.println("Hello Crawler!"); } }

底层到底做了些什么?源代码定义了一个叫作Crawler的类,虚拟机执行其中的main方法。1.2 准备开发环境

Eclipse也是使用Java开发的,所以先准备基本的Java开发环境(简称JDK),然后准备运行在JDK上的Eclipse。1.2.1 JDK

JDK可以从Java官方网站http://java.sun.com下载得到。注意,不是从http://www.java.com下的Java虚拟机。

下载Java SE,也就是标准版本。Latest Release是最新发布的安装程序。因为可以在Windows或Linux等多种操作系统环境下开发Java程序,所以有多个操作系统的JDK版本供选择。

因为JDK是有版权的,所以需要接受许可协议(Accept License Agreement)后才能下载。下载完毕后,使用默认方式安装JDK即可。JDK相关的文件都放在一个叫作JAVA_HOME的根目录下。JDK根目录的命名格式是C:\Program Files\Java\jdk1.6.0_,最后以一个数字类型的版本号结尾,例如10或者21。

因为一台机器可以安装多个JDK和JVM,为了避免混乱,可以新增环境变量JAVA_HOME,指定一个默认使用的JDK。

使用echo命令检查环境变量JAVA_HOME。>echo %JAVA_HOME% C:\Program Files\Java\jdk1.6.0_10

如果只需要使用集成开发环境,配置JAVA_HOME环境变量就可以了。为了检查JAVA_HOME是否已经正确设置,在任何路径输入Java命令“>java-version”显示虚拟机的版本号就可以了。java version "1.6.0_10-rc" Java(TM) SE Runtime Environment (build 1.6.0_10-rc-b28) Java HotSpot(TM) Client VM (build 11.0-b15, mixed mode, sharing)

如果还需要在控制台下执行,则需要访问编译程序的javac.exe或者执行Java类的java.exe。环境变量PATH指定了从哪里找java.exe这样的可执行文件。可以从多个路径查找可执行文件,这些路径以分号隔开。如果想在命令行运行Java程序,还可以修改已有的环境变量PATH,增加Java程序所在的路径,例如C:\Program Files\Java\jdk1.6.0_10\bin。

然后检查环境变量PATH。>echo %PATH%

为了检查PATH是否已经正确设置,在任何路径输入javac命令显示javac的用法就可以了,也可以用第一个Java程序试验下。>javac Crawler.java >java Crawler

运行看是否显示“Hello Crawler!”。1.2.2 Eclipse

就好像理发有推子等专门的理发用具,开发软件也有专门的集成开发环境。开发Java程序最流行的工具叫作Eclipse(http://www.eclipse.org)。

Eclipse也有很多版本,可以选择最简单的一个版本——Eclipse IDE for Java Developers。Eclipse是绿色软件,无须安装,解压后就可以直接使用。在Windows下,双击就可以解压文件。如果需要专门的解压软件,推荐使用7z(http://www.7-zip.org/)。

Eclipse默认是英文界面,如果习惯用中文界面,可以从http://www.eclipse.org/babel/downloads.php上下载支持中文的语言包。

Eclipse把软件按项目管理,每个项目都有自己的.classpath文件,指定了源代码路径、编译后输出文件的路径以及这个项目引用的jar包的路径。1.3 类和对象

世界上有各种各样的生物,每个生物都属于某一个物种。蜘蛛是一个物种,每个蜘蛛都是一个对象。同样,Java虚拟机的内存中也有很多各种各样的对象。

使用类可以创建具有相同结构和行为的对象。打印Hello World的例子并没有创建对象,因为main是一个静态方法,不属于任何一个类。现在创建一个属于对象的方法。public class Spider{ public void hello(){ System.out.println("Hello World!"); } public static void main (String args[]) { Spider p = new Spider(); //新建一个对象 p.hello(); //调用hello方法 } }

Java源文件的扩展名为.java,而且必须与类名相同。上面这个Spider类必须放在Spider.java文件中。

每个人都由不同的原子构成。每个对象都占据不同的内存空间。使用关键字new来为对象分配空间,就是实例化对象。关键字new声明了对象的诞生,但是不是所有的数据类型都是对象。一些基本的数据类型,例如int、boolean等都不是对象,不能用new的方式实例化。

toString方法返回一个描述对象内部状态的字符串。所有的对象都有toString方法,这些共同的方法在Object类中定义,Object是所有对象的共同祖先。

有的对象专门用来存储数据,叫作POJO类,还有些用来执行任务,例如,爬虫类或者搜索类。

在接口中只能定义一些方法和常量。定义一个接口Visitor处理碰到的网址:String seed = " http://wallstreetcn.com/"; //抓取的种子:华尔街新闻网 void expand(URL seed, visitor){ }

Visitor的具体实现类处理抓下来的网页内容。1.4 常量

一般把常量值声明成final类型,表示以后不会再修改它。如果需要给这个常量动态赋值,则可以放在static{}程序块中。而且最好放在一个单独的类中,这样其他的类就可以把它当成静态常量访问。public final class Constants { public static final String OS_ARCH = System.getProperty("os.arch"); public static final boolean JRE_IS_64BIT; //常量 static { String x = System.getProperty("sun.arch.data.model"); if (x != null) { JRE_IS_64BIT = x.indexOf("64") != -1; } else { if (OS_ARCH != null && OS_ARCH.indexOf("64") != -1) { JRE_IS_64BIT = true; } else { JRE_IS_64BIT = false; } } } } 1.5 命名规范

类的首字母大写,例如,新闻爬虫叫作NewsSpider,论坛爬虫叫作ForumSpider。

最好用英语命名,不要用拼音,因为拼音容易有歧义。Libai可能表示李白,也可能表示立白。为了和国际接轨,程序最好能让讲英语的程序员看懂。Java中的关键词都是英文,既然不能把if写成ruguo(如果),所以类名、变量名、方法名也应该用英语命名。

可以使用Google机器翻译(http://translate.google.cn)。虽然机器翻译可能把“公共卫生间”翻译成“Between public health”,但是大部分词的翻译结果还是比较靠谱的。

有很多种不同的名称,例如类的名称、变量的名称,它们的命名方式各不一样。爬虫类名Crawler,以大写字母开头,其中的方法名为getURLs,以小写字母开头。总的来说,有两种命名方式:以小写字母开头的命名方式和以大写字母开头的命名方式。

一个名称由多个单词组成,因为单词之间不允许有空格,所以用大小写的方式来区分单词间隔,如果都是大写字母,则单词之间用下画线隔开。

类名的单词首字母要大写,而常量名所有字母都要大写。public static final double PI=3.14; //圆周率 public static final double NEGATIVE_INFINITY = -1.0 / 0.0; //最大的负数

仅处理网址的类叫作Handler。从网址提取内容的类叫作Extractor。后续的代码实现会遵循一致的命名规范。1.6 基本语法

从语法上讲,Java语言和C语言非常相似,只是在细节上有一些差别。实际上,C语言和Java语言的主要差别不在语言本身,而是在它们所执行的平台上。

Java程序需要JRE(Java Runtime Environment)运行环境来执行代码,但是JRE只限于在Java这一门语言中使用。

Java源代码可以被编译成字节代码的一种中间状态,然后由已提供的虚拟机来执行这些字节代码。

和C一样,Java的每一个应用程序都应该有一个入口点,表明该程序从哪里开始执行。为了让系统能找到入口点,入口方法名规定为main。class HelloWorld{ public static void main() { System.out.println ("Hello World"); } }

Java的每个类都可以有一个main方法,因此可以执行,但是往往需要有很多可以直接执行的测试代码。可以把这些功能不同的代码封装在不同的方法中,这样至少避免了大量的注释代码。

有些方法处理的方式相同,只是输入参数不一样,可以把这些方法命名成同一个名称,这叫作方法重载。例如,分词类可以切分一个字符串,或者一个文件。Public class Segmenter{ Public Split(String sentence){} Public Split(File file){ //内部调用Split(String) } } 1.7 条件判断

判断一个网址是否是详细页,如果是详细页,就从这个网址提取正文。if(isDetail(url)){ //判断是否是详细页 extractContent(url); //提取正文 } 1.8 循环

for循环总是可以写成等价的while循环。

for语句中有三个子句。● 在循环开始之前执行init-stmt语句,通常在这里初始化迭代变

量。● 在每次循环之前,测试condition表达式。如果布尔表达式是false,

就不会执行循环(和while循环一样)。● 在body执行后,执行next-stmt语句。一般在这里增加迭代变量的

值。

例如,使用for循环生成出要遍历的网址。for(int pageNum = 2;pageNum<20;++pageNum){ String pattern = "http://roll.finance.sina.com.cn/finance/lc1/cfgs/index_%d.shtml"; String url = String.format(pattern, pageNum ); System.out.println(url); }

for循环的一种特殊写法:for(;;){ //代码 }

把for循环当成一个永远为真的循环来使用时,它等价于:while(true){ //代码 }

for-earch循环,看起来简洁,但是中间的元素一般是如何定义的不是那么容易理解,如下所示。for (double[] pArray : prob) for (double p : pArray) System.out.println(p);

元素的名字随便起,根据iterator接口返回值的类型定义,要实现iterator接口的对象才能够用for-earch循环遍历。1.9 数组

军训中的8×8方阵可以表示成二维数组。Person[][] matrix = new Person[8][8];

matrix是二维数组的名字,而从matrix[0]到matrix[7]都是一维数组,matrix[0]包含8个对象。所以可以把二维数组看成由一维数组组成的一维数组。

使用new关键字创建数组时,可以使用变量声明数组的长度。int num = 8; Person[] personArray = new Person[num];

对于使用变量长度创建数组的数组,在创建数组的时候需要知道数组的长度。水浒故事的原型是以宋江为首的36人,到后来的小说中发展成为108人。如果用一个数组表示其中的每个人,在创建时仍然不知道后来需要存储多少元素,所以需要真正意义上的动态数组。实现长度可变的动态数组时需要把数据从长度小的数组复制到新的更长的数组上。

复制数组实现起来并不困难,但是如果数组长度很大,则需要考虑移动的性能。高速铁路有专门的客运专线,能把大量的人从一个地方快速运到另外一个地方。System.arraycopy()方法专门用来快速移动数组中的数据,它把指定个数的元素从源数组复制到目的数组。源数组和目的数组中的元素类型必须相同。

这个方法有5个参数。● src:源数组。● srcPos:源数组要复制的起始位置。● dest:目的数组。● destPos:目的数组放置的起始位置。● length:复制的长度。

在动态数组中使用System.arraycopy方法。public class DynamicArrayOfInt { private int[] data; //保存数据的数组 public DynamicArrayOfInt() { //构造器 data = new int[1]; //按需增长的数组 } public int get(int position) { //得到数组中指定位置的值 return data[position]; } public void put(int position, int value) { //把值存储到数组中指定位置 //为了包含这个位置,如果需要,数据数组大小会增长 if (position >= data.length) { //如果指定的位置超出了数据数组的实际大小,则把数组的大小翻倍 //如果仍然不包含指定的位置,则新的大小设置成2*position int newSize = 2 * data.length; if (position >= newSize) newSize = 2 * position; int[] newData = new int[newSize]; System.arraycopy(data, 0, newData, 0, data.length); //复制内容到新的数组 data = newData; //用新数组代替原来的数组 } data[position] = value; } }

例如,使用单个元素赋值的方法复制一个二维数组。static int[][] copy2D(int[][] in){ int[][] ret = new int[in.length][in[0].length]; for(int i = 0;i < in.length;i++) { for(int j = 0;j < in[0].length;j++) { ret[i][j] = in[i][j]; } } return ret; }

使用System.arraycopy方法快速复制二维数组。static int[][] fastCopy2D(int[][] in){ int[][] ret = new int[in.length][in[0].length]; for(int i = 0;i < in.length;i++) { System.arraycopy( in[i], 0, ret[i], 0, in[0].length ); } return ret; }

如果把一个二维数组作为System.arraycopy的参数,则会把这个二维数组看成一维数组,只不过其中的每个元素都是一个一维数组。所以下面这样的写法不是深度复制,只是复制一维数组的引用。static int[][] fastCopy2D(int[][] in){ int[][] ret = new int[in.length][in[0].length]; System.arraycopy( in, 0, ret, 0, in.length ); //不是深度复制 return ret; }

有些动态规划算法把计算的中间结果存储在二维数组中。例如,创建一个存储概率的二维数组。int stageLength = 10; //第一维的长度 int types = 20; double[][] prob = new double[stageLength][types];//二维数组 1.10 位运算

计算机只认识0和1序列,也就是二进制。例如,农历七月七日的七夕,写成7.7,转化成二进制就是111.111。

二进制数组是最底层的数据结构。很多快速计算都会用到它,可以在二进制数组上进行位运算。

32位或者64位的二进制数组可以用一个int或者long类型的数表示。很长的二进制位数组可以用BitSet类型的数表示。

作为常量的二进制数组写起来会很长,所以经常用到十六进制表示常量,十六进制以0x开头,如0xff就是11111111的十六进制表示,注意,这里是零X,不是OX。

位的逻辑运算有与、或、非、异或四种。例如,与运算取得低8位值。uint low = (uint)(j) & 0xff; //取得变量j的低8位值

通过位移取得中间8位的值。((uint)(j) >> 8) & 0xff;

异或运算取得二进制数组有差别的位。long x = 0xfe07; long y = 0x07; long val = x ^ y; //异或运算 System.out.println(Long.toBinaryString(val)); //输出1111111000000000

海明距离(HammingDistance)是针对长度相同的字符串或二进制数组而言的。对于二进制数组s和t,H(s, t)是两个数组对应位有差别的数量。例如,1011101和1001001的海明距离是2。

可以把两个无符号整型数按位异或(XOR),统计val中1的个数,结果就是海明距离,如表1-1所示。表1-1 计算海明距离数字二进制表示20000001050000010100000111

计算两个数的海明距离。public static int hammingDistance(int x, int y){ int dist = 0; //海明距离 int val = x ^ y; //异或结果 //统计val中1的个数 while (val>0) { ++dist; val &= val - 1; //去掉val中最右边的一个1 } return dist; } 1.11 枚举类型

为了比输赢,我们经常使用猜拳游戏:石头、剪刀、布,每次只能出其中的一种。当一个对象的取值范围是固定的一些值,往往使用枚举类型。public enum GameValue{ stone, //石头 scissors, //剪刀 cloth //布 }

词有名词或动词等类别,句子有陈述句或疑问句等类型。使用枚举比使用无格式的整数来描述这些类型至少有如下三个优势。● 枚举可以使代码更易于维护,有助于确保给变量指定合法的、期

望的值。● 枚举使代码更清晰,允许用描述性的名称表示整数值,而不是用

含义模糊的数来表示。● 枚举使代码更易于键入。在给枚举类型的实例赋值时,集成开发

环境Eclipse会通过智能感知功能弹出一个包含可接受值的列表

框,减少了按键次数,并能够让我们回忆起可能的值。

我们可以把词的类型或句子的类型定义成枚举类型。public enum PartOfSpeech{ //词的类别 a,//形容词 n,//名词 v//动词 } 1.12 比较器

字符串可以比较大小,方法就是从前往后逐个比较字符的编码值,但是不能用操作符“>”或者“<”比较大小。调用字符串对象的compareTo方法,代码如下所示。String word = "yelp"; String anotherWord = "learn"; word.compareTo(anotherWord);

因为String类实现了Comparable接口,所以可以调用字符串对象的compareTo方法。Comparable接口只定义了一个compareTo方法。public interface Comparable { public int compareTo(T o); //比较当前对象this和传入对象 }

compareTo方法返回一个整数值,用来表示当前对象和传入的对象比较大小的结果。如果返回正数,则表示当前对象大于传入对象。如果返回负数,则表示当前对象小于传入对象。如果返回0,则表示当前对象和传入对象相等。1.13 方法

网页源代码中的URL可能是相对于当前网址本身的相对地址。定义一个叫作resolveUrl的方法用于把相对路径转为绝对路径。public static String resolveUrl(final String baseUrl, final String relativeUrl) { }

为了减轻记忆负担,同样功能的方法最好用相同的名字命名,哪怕需要传入的参数形式不一样。例如resolveUrl的另外一种实现。private static Url resolveUrl(final Url baseUrl, final String relativeUrl) { //完成功能 } public static String resolveUrl(final String baseUrl, final String relativeUrl) { //调用 resolveUrl(url,string)完成功能 }

如果相对URL以http://开头,则直接返回这个URL。实现resolveUrl方法基本上可以看成由一个字符串得到另外一个字符串。可以用有限状态转换的方法得到完整的URL地址。1.14 集合类

把遍历过的网址放入一个集合:HashSet urlSeen = new HashSet(); urlSeen.put("http://www.lietu.com");

文档是单词的集合。搜索结果集也是文档的集合。所以在搜索引擎开发中,集合类必不可少。集合类有动态数组ArrayList、队列Queue、堆栈Stack,以及存储键/值对的HashMap。HashMap是散列表的实现。1.14.1 动态数组

把所有的网址放入动态数组中:ArrayList urls = new ArrayList(); urls.add("http://www.lietu.com"); List ipList = new ArrayList();

用到的是泛型吗?是的。

List是个抽象的接口。ArrayList是具体的实现类。因为是列表,所以不确定数据的类型。1.14.2 散列表

爬虫发送给Web服务器的网页请求中有若干项说明。User-Agent和对应的值叫作键/值对。

把键/值对存储在HashMap中,就可以通过英文单词找到中文单词了。//创建一个存储键/值对的散列表 HashMap heads = new HashMap(); heads.put("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"); //放入一个键/值对

通过User-Agent找到对应的值,也就是通过HashMap.get方法取得键对应的值。System.out.println(heads.get("User-Agent")); //输出:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)

如果要找的键不在HashMap中,则get方法返回空。System.out.println(heads.get("hello")); //输出:null

遍历HashMap。//用foreach循环遍历元素 for (Entry e : heads.entrySet()) { System.out.println(e.getKey() + "->" +e.getValue()); }

如果把自定义对象作为键对象,需要重写hashCode和equals方法。HttpHost类重写了hashCode和equals方法。public final class HttpHost{ /** The default scheme is "http". */ public static final String DEFAULT_SCHEME_NAME = "http"; /** The host to use. */ protected final String hostname; /** The lowercase host, for {@link #equals} and {@link #hashCode}. */ protected final String lcHostname; /** The port to use. */ protected final int port; /** The scheme (lowercased) */ protected final String schemeName; public boolean equals(final Object obj) { if (this == obj) return true; if (obj instanceof HttpHost) { HttpHost that = (HttpHost) obj; return this.lcHostname.equals(that.lcHostname) && this.port == that.port && this.schemeName.equals(that.schemeName); } else { return false; } } /** * @see java.lang.Object#hashCode() */ public int hashCode() { int hash = LangUtils.HASH_SEED; hash = LangUtils.hashCode(hash, this.lcHostname); hash = LangUtils.hashCode(hash, this.port); hash = LangUtils.hashCode(hash, this.schemeName); return hash; } }

使用HttpHost对象作为键,对应的评分作为值。HashMap rankMap = new HashMap(); rankMap.put(new HttpHost("lietu.com",8);

HashSet用来存储一个元素的集合。从名称可以看出,它是基于散列表的。但是只存储键,而不存储值。HashSet中的Set是数学意义上的集合,而Java中的集合类则是一种更广义的称呼。

HashSet其实使用HashMap来实现。为了判断某个键是否存在,它定义了一个叫作PRESENT的特殊对象。static final Object PRESENT = new Object();

所有的键统一都对应PRESENT这一个对象。

HashMap是散列表的实现。它存储了一个元素的集合。文档是单词的集合,搜索结果集也是文档的集合,所以在搜索引擎开发中,集合类必不可少。集合类除了HashMap,还有动态数组ArrayList、队列Queue和堆栈Stack等。增加一个元素到集合类调用add方法,增加键/值对调用put方法。

存放在集合类里的元素都必须是对象。但是一些基本的数据类型如int、boolean等都不是对象。为了存放这些基本的数据类型,需要把它们封装成类,比如int封装成Integer类,boolean封装成Boolean类。定义这些对象就是为了能够把基本数据类型当作对象来使用。Integer对象比较值是否相等也是调用equals方法。

把基本类型用相应的引用类型包装起来,使其具有对象的性质。例如,int包装成Integer、float包装成Float,包装这个步骤叫作装箱。可以对Character类型的变量直接赋char类型的值,不用加强制类型转换。装箱约定的例子如下所示。Character c = 'a'; //char类型可以自动装箱成Character类型 Integer a = 100; //这也是自动装箱

编译器调用Integer.valueOf(int i)方法实现自动装箱。

和装箱相反,将引用类型的对象简化成值类型的数据叫作拆箱。int b = new Integer(100); //这是自动拆箱

对于Double类型,可以调用doubleValue方法拆箱。Double d = new Double(0); //装箱 double x = d.doubleValue(); //拆箱

要知道集合中有多少个元素,可以使用Collection中定义的size方法。所有集合类都实现了这个接口。动态数组ArrayList是最常见的集合类,可以通过ArrayList.size()知道数组的长度。用ArrayList.clear()方法清空其中的元素,但是数组仍在,这样可以避免重复分配内存。集合类之间的关系如图1-1所示。图1-1 集合类之间的关系1.15 文件

一本电子书往往就是操作系统中的一个文件。文件都是二进制格式的。但是也可以专门存储字符串,这样的文件叫文本文件。例如,网页往往以文本文件的形式存放在Web服务器中。文本文件可以直接用记事本编辑。大的文本文件如果用记事本打开需要很长时间,所以最好用写字板打开超过几兆以上的文件。可以用UltraEdit打开二进制格式的文件。

一般使用串行方式读出或者写入文件。总的来说,使用输入流把文件内容读入内存,使用输出流把内存中的信息写出到文件。这些类位于java.io包下。输入和输出的类和方法往往是对应的,例如,Reader和Writer类对应。

Windows系统文件大小经常以字节为单位。文件大小往往用MB或者GB衡量。1K表示1024,而1M表示1024K,1G表示1024M。大约的计算方法是:1K是3个零,1M是6个零,1G是9个零。1.15.1 文本文件

先了解如何读写文本文件,然后看如何读写二进制文件。java.io.Reader用来读取字符,它的子类FileReader用来读取文本文件。

FileReader打开指定路径下的文件。文件的路径分隔符可以用“\\”或者“/”表示。“\\”是Windows风格的写法,因为字符串中的特殊字符要转义,所以用两个斜线表示一个斜线。FileReader fr = new FileReader("c:\\autoexec.bat"); //打开文本文件 “/”是Linux风格的路径写法,因为不需要转义,所以正斜线只需要写一个就可以了。FileReader fr = new FileReader("c:/autoexec.bat"); //打开文本文件

Linux风格的和Windows风格的写法是等价的。

如果有一堆砖要搬,一次取不完,不会一次只拿一块砖,会尽量多拿几块。如果有很多内容要读,不会一次只读一个字节,而是一次尽量多读一些字节到缓存。FileReader fr = new FileReader("c:/autoexec.bat"); //打开文本文件 BufferedReader br = new BufferedReader(fr); //缓存读 String line; while((line = br.readLine()) != null) { //按行读入文件 System.out.println(line); } fr.close(); //关闭文本文件

输入流把数据从硬盘读入随机访问存储器(Random Access Memory,简称RAM)。可以根据输入流构建BufferedReader,实现代码如下所示。String fileName = "SDIC.txt"; //文件名 InputStream file = new FileInputStream(new File(fileName)); //打开输入流 //缓存读入数据 BufferedReader in = new BufferedReader(new InputStreamReader(file,"GBK"));

使用for循环按行读入一个文件。String fileName = "SDIC.txt"; //文件名 InputStream file = new FileInputStream(new File(fileName)); //打开输入流 //缓存读入数据 BufferedReader in = new BufferedReader(new InputStreamReader(file,"GBK")); for (String line = in.readLine();line != null; line = in.readLine()) { System.out.println(line); } in.close();

它等价于下面这个while循环。String fileName = "SDIC.txt"; //文件名 InputStream file = new FileInputStream(new File(fileName)); //打开输入流 //缓存读入数据 BufferedReader in = new BufferedReader(new InputStreamReader(file,"GBK")); String line = in.readLine(); while (line != null) { System.out.println(line); line =in.readLine(); } in.close();

通过把赋值语句写在while循环的布尔表达式里面,中间的while循环可以简写成如下所示的代码。String line; while ((line = in.readLine()) != null) { //合并赋值语句和判断条件 System.out.println(line); }

读入的字符串在Eclipse控制台中显示正常不能保证读入的字符本身不是乱码。读入文件可以指定字符集编码。中文文本文件一般使用GBK编码。如果要把其他格式的文件转码成GBK编码,可以先用记事本打开文件,然后另存为编码是ANSI格式的文本文件。

读入文件时,可以在InputStreamReader的构造方法中指定字符集。读入GBK编码的文本文件的代码如下所示。InputStream file = new FileInputStream(new File(path)); //创建使用GBK字符集的InputStreamReader BufferedReader read = new BufferedReader(new InputStreamReader(file,"GBK"));

为了支持多种语言,往往采用UTF-8格式编码的文件,把文件存成UTF-8格式的,然后用类似下面的代码读入。String file = "D:/dict.txt"; InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8"); BufferedReader read = new BufferedReader(isr); String line; while ((line = read.readLine()) != null) { System.out.println(line); }

java.io.Writer用于输出字符流,FileWriter类是Writer类的一个子类。使用FileWriter写入文本文件的例子如下所示。String fileName = "c:/story.txt" ; FileWriter writer = new FileWriter( fileName ); //写入四行,可以用写字板打开这个文件 writer.write( "从前有座山,\n" ); writer.write( "山上有座庙。\n" ); writer.write( "庙里有一个老和尚,\n" ); writer.write( "一个小和尚。\n" ); writer.close(); //关闭文件

一般来说,Writer是把内容立即写到硬盘。如果要多次调用write方法,则批量写入效率会更高。类似于团购,团购的价格往往比单件购买的价格低。可以使用缓存加快文件写入速度。//使用缺省的缓存大小 BufferedWriter bw = new BufferedWriter(new FileWriter(fileName)); bw.write("Hello,China!"); //写入一个字符串 bw.write("\n"); //写入换行符 bw.write("Hello,World!"); bw.close(); //把缓存中的内容写入文件

使用BufferedWriter写入数据时,最后需要调用BufferedWriter的close方法。如果不关闭文件,可能导致缓存中的数据丢失,写入文件的数据不完整。例如,把集合中的元素写入到文件。ArrayList words = getLexiconEntry(); //得到词表 String fileName="C:/wordlist.txt"; //要写入的文件 BufferedWriter bw = new BufferedWriter(new FileWriter(fileName)); for(String w: words){ bw.write(w); bw.write("\r\n"); } bw.close();

如果要写入一个UTF-8编码的文本文件,则可以在OutputStreamWriter的构造方法中指定字符集。File file = new File("c:/temp/test.txt"); //创建一个文件对象 BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file),"UTF8"));

按指定编码写入文本的完整代码如下所示。/** * 向文件写入字符串 * @param content 字符串 * @param fileName 文件名 * @param encoding 编码 */ public static void writeToFile(String content,String fileName,String encoding){ try { FileOutputStream fos =new FileOutputStream(fileName); OutputStreamWriter osw=new OutputStreamWriter(fos,encoding); BufferedWriter bw=new BufferedWriter(osw); bw.write(content); bw.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }

如果黑板上已经有字,可以选择抹去黑板上已有的字重新写,也可以在原来的文字后继续写。如果一个文件已经存在,可以把新的内容追加写到最后,也可以从头写入新内容,也就是覆盖写。FileWriter的构造方法区别于这两种写入方式。//FileWriter构造方法 FileWriter( String fileName, boolean mode) throws IOException

其中的mode=false表示覆盖写,mode=true表示追加写。为了避免冲突,在一个时刻只能有一个线程写文件。

打开大的文本文件可以使用Gvim(http://www.vim.org/download.php),它是vim的Windows移植版本,或者使用收费的UltraEdit。1.15.2 二进制文件

FileWriter只能接受字符串形式的参数,也就是说只能把内容存到文本文件。相对于文本文件,采用二进制格式的文件存储更省空间。例如,生物中的碱基可以用英文字符A G C T表示,也可以采用二进制格式表示,A用00表示,G用01表示,C用10表示,T用11表示。这样,二进制中的8位压缩成了2位。

读写二进制文件和文本文件使用不同的类。例如,搜索引擎中的索引库格式就是二进制文件。

InputStream用于按字节从输入流读取数据。其中的int read()方法读取一个字节,这个字节以整数形式返回0到255之间的一个值。为什么读一个字节,而不直接返回一个byte类型的值?因为byte类型最高位是符号位,它所能表示的最大的正整数是127。如果read()方法返回−1,则表示已到输入流的末尾。

InputStream只是一个抽象类,不能实例化。FileInputStream是InputStream的子类,用于从文件中按字节读取。public static void main(String[] args) throws IOException { String filePath = "d:/test.txt"; File file = new File (filePath); //根据文件路径创建一个文件对象 //如果找不到文件,会抛出FileNotFoundException异常 FileInputStream fileInput = new FileInputStream(file); fileInput.close(); //关闭文件输入流,如果无法正常关闭,会抛出IOException异常 }

OutputStream中的write(int b)方法用于按字节写出数据。FileOutputStream用于按字节把数据写到文件。例如,按字节把内容从一个文件读出来,并写入另外一个新文件,也就是文件复制功能。File fileIn = new File("source.txt"); //打开源文件 File fileOut = new File("target.txt"); //打开写入文件,也就是目标文件 FileInputStream streamIn = new FileInputStream(fileIn); //根据源文件构建输入流 FileOutputStream streamOut = new FileOutputStream(fileOut); //根据目标文件构建输出流 int c; //从源文件中按字节读入数据,如果内容还没读完,则继续

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载