0%

Python

2022 年 Python3 网络爬虫教程

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

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

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

教程请移步:

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

2018 年 Python3 爬虫系列教程

以下为 2018 年版 Python3 网络爬虫系列教程

本内容来自于《Python3 网络爬虫开发实战》一书。 书籍购买地址: https://item.jd.com/12333540.html

本书通过多个实战案例详细介绍了 Python3 网络爬虫的知识,本书由图灵教育-人民邮电出版社出版发行,版权所有,禁止转载。

Python

前几天,大才发了一个自己写的框架,介绍地址在这里, GIT 地址在这里

今天在阿里云上试用了一下,在这里做一个简单的说明。

1、配置环境

阿里云的版本是 2.7.5,所以用 pyenv 新安装了一个 3.6.4 的环境,安装后使用 pyenv global 3.6.4 即可使用 3.6.4 的环境,我个人比较喜欢这样,切换自如,互不影响。 如下图: 接下来按照大才的文章,pip install gerapy 即可,这一步没有遇到什么问题。有问题的同学可以向大才提 issue。

2. 开启服务

首先去阿里云的后台设置安全组 ,我的是这样: 然后到命令窗口对 8000 和 6800 端口放行即可。 接着执行

gerapy init cd gerapy gerapy migrate # 注意下一步 gerapy runserver 0.0.0.0:8000 【如果你是在本地,执行 gerapy runserver 即可,如果你是在阿里云上,你就要改成前面这样来执行】

现在在浏览器里访问:ip:8000 应该就可以看到主界面了 里面的各个的含义见大才的文章。

3.创建项目

在 gerapy 下的 projects 里面新建一个 scrapy 爬虫,在这里我搞的是最简单的:

scrapy startproject gerapy_test cd gerapy_test scrapy genspider baidu www.baidu.com

这样就是一个最简单的爬虫了,修改一个 settings.py 中的 ROBOTSTXT_OBEY=False, 然后修改一个 spiders 下面的 baidu.py, 这里随意,我这里设置的是输出返回的 response.url

4.安装 scrapyd

pip install scrapyd

安装好以后,命令行执行

scrapyd

然后浏览器中打开 ip:6800,如果你没有修改配置,应该这里会打不开,clients 那里配置的时候,也应该会显示为 error,就像这样: 后来找了一下原因发现 scrapyd 默认打开的也是 127.0.0.1 所以这个时候就要改一下配置,具体可以参考这里, 我是这么修改:

vim ~/.scrapyd.conf [scrapyd] bind_address = 0.0.0.0

在刷新一下,就会看到前面 error 变成了 normal

5. 打包,部署,调度

这几步大才的文章里都有详细说明,打包完,部署,在进入 clients 的调度界面,点击 run 按钮即可跑爬虫了 可以看到输出的结果了。

6.结语

建议大家可以试着用一下,很方便,我这里只是很简单的使用了一下。

Python

本节我们来尝试使用 TensorFlow 搭建一个双向 LSTM (Bi-LSTM) 深度学习模型来处理序列标注问题,主要目的是学习 Bi-LSTM 的用法。

Bi-LSTM

我们知道 RNN 是可以学习到文本上下文之间的联系的,输入是上文,输出是下文,但这样的结果是模型可以根据上文推出下文,而如果输入下文,想要推出上文就没有那么简单了,为了弥补这个缺陷,我们可以让模型从两个方向来学习,这就构成了双向 RNN。在某些任务中,双向 RNN 的表现比单向 RNN 要好,本文要实现的文本分词就是其中之一。不过本文使用的模型不是简单的双向 RNN,而是 RNN 的变种 — LSTM。 如图所示为 Bi-LSTM 的基本原理,输入层的数据会经过向前和向后两个方向推算,最后输出的隐含状态再进行 concat,再作为下一层的输入,原理其实和 LSTM 是类似的,就是多了双向计算和 concat 过程。

数据处理

本文的训练和测试数据使用的是已经做好序列标注的中文文本数据。序列标注,就是给一个汉语句子作为输入,以“BEMS”组成的序列串作为输出,然后再进行切词,进而得到输入句子的划分。其中,B 代表该字是词语中的起始字,M 代表是词语中的中间字,E 代表是词语中的结束字,S 则代表是单字成词。 这里的原始数据样例如下:

1
/b/e/s/s/b/e/s/s/s/b/m/e

这里一个字对应一个标注,我们首先需要对数据进行预处理,预处理的流程如下:

  • 将句子切分
  • 将句子的的标点符号去掉
  • 将每个字及对应的标注切分
  • 去掉长度为 0 的无效句子

首先我们将句子切分开来并去掉标点符号,代码实现如下:

1
2
3
4
5
6
7
8
# Read origin data
text = open('data/data.txt', encoding='utf-8').read()
# Get split sentences
sentences = re.split('[,。!?、‘’“”]/[bems]', text)
# Filter sentences whose length is 0
sentences = list(filter(lambda x: x.strip(), sentences))
# Strip sentences
sentences = list(map(lambda x: x.strip(), sentences))

这样我们就可以将句子切分开来并做好了清洗,接下来我们还需要把每个句子中的字及标注转为 Numpy 数组,便于下一步制作词表和数据集,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
import re
# To numpy array
words, labels = [], []
print('Start creating words and labels...')
for sentence in sentences:
groups = re.findall('(.)/(.)', sentence)
arrays = np.asarray(groups)
words.append(arrays[:, 0])
labels.append(arrays[:, 1])
print('Words Length', len(words), 'Labels Length', len(labels))
print('Words Example', words[0])
print('Labels Example', labels[0])

这里我们利用正则 re 库的 findall() 方法将字及标注分开,并分别添加到 words 和 labels 数组中,运行效果如下:

1
2
3
Words Length 321533 Labels Length 321533
Words Example ['人' '们' '常' '说' '生' '活' '是' '一' '部' '教' '科' '书']
Labels Example ['b' 'e' 's' 's' 'b' 'e' 's' 's' 's' 'b' 'm' 'e']

接下来我们有了这些数据就要开始制作词表了,词表制作起来无非就是输入词表和输出词表的不重复的正逆对应,制作词表的目的就是将输入的文字或标注转为 index,同时还能反向根据 index 获取对应的文字或标注,所以我们这里需要制作 word2id、id2word、tag2id、id2tag 四个字典。 为了解决 OOV 问题,我们还需要将无效字符也进行标注,这里我们统一取 0。制作时我们借助于 pandas 库的 Series 进行了去重和转换,另外还限制了每一句的最大长度,这里设置为 32,如果大于32,则截断,否则进行 padding,代码如下:

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
from itertools import chain
import pandas as pd
import numpy as np
# Merge all words
all_words = list(chain(*words))
# All words to Series
all_words_sr = pd.Series(all_words)
# Get value count, index changed to set
all_words_counts = all_words_sr.value_counts()
# Get words set
all_words_set = all_words_counts.index
# Get words ids
all_words_ids = range(1, len(all_words_set) + 1)

# Dict to transform
word2id = pd.Series(all_words_ids, index=all_words_set)
id2word = pd.Series(all_words_set, index=all_words_ids)

# Tag set and ids
tags_set = ['x', 's', 'b', 'm', 'e']
tags_ids = range(len(tags_set))

# Dict to transform
tag2id = pd.Series(tags_ids, index=tags_set)
id2tag = pd.Series(tags_set, index=tag2id)

max_length = 32

def x_transform(words):
ids = list(word2id[words])
if len(ids) >= max_length:
ids = ids[:max_length]
ids.extend([0] * (max_length - len(ids)))
return ids

def y_transform(tags):
ids = list(tag2id[tags])
if len(ids) >= max_length:
ids = ids[:max_length]
ids.extend([0] * (max_length - len(ids)))
return ids

print('Starting transform...')
data_x = list(map(lambda x: x_transform(x), words))
data_y = list(map(lambda y: y_transform(y), labels))
data_x = np.asarray(data_x)
data_y = np.asarray(data_y)

这样我们就完成了 word2id、id2word、tag2id、id2tag 四个字典的制作,并制作好了 Numpy 数组类型的 data_x 和 data_y,这里 data_x 和 data_y 单句示例如下:

1
2
Data X Example: [8, 43, 320, 88, 36, 198, 7, 2, 41, 163, 124, 245, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Data Y Example: [2, 4, 1, 1, 2, 4, 1, 1, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

可以看到数据的 x 部分,原始文字和标注结果都转化成了词表中的 index,同时不够 32 个字符就以 0 补全。 接下来我们将其保存成 pickle 文件,以备训练和测试使用:

1
2
3
4
5
6
7
8
9
print('Starting pickle to file...')
with open(join(path, 'data.pkl'), 'wb') as f:
pickle.dump(data_x, f)
pickle.dump(data_y, f)
pickle.dump(word2id, f)
pickle.dump(id2word, f)
pickle.dump(tag2id, f)
pickle.dump(id2tag, f)
print('Pickle finished')

好,现在数据预处理部分就完成了。

构造模型

接下来我们就需要利用 pickle 文件中的数据来构建模型了,首先进行 pickle 文件的读取,然后将数据分为训练集、开发集、测试集,详细流程不再赘述,赋值为如下变量:

1
2
3
4
# Load data
data_x, data_y, word2id, id2word, tag2id, id2tag = load_data()
# Split data
train_x, train_y, dev_x, dev_y, test_x, test_y = get_data(data_x, data_y)

接下来我们使用 TensorFlow 自带的 Dataset 数据结构构造输入输出,利用 Dataset 我们可以构造一个 iterator 迭代器,每调用一次 get_next() 方法,我们就可以得到一个 batch,这里 Dataset 的初始化我们使用 from_tensor_slices() 方法,然后调用其 batch() 方法来初始化每个数据集的 batch_size,接着初始化同一个 iterator,并绑定到三个数据集上声明为三个 initializer,这样每调用 initializer,就会将 iterator 切换到对应的数据集上,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Train and dev dataset
train_dataset = tf.data.Dataset.from_tensor_slices((train_x, train_y))
train_dataset = train_dataset.batch(FLAGS.train_batch_size)

dev_dataset = tf.data.Dataset.from_tensor_slices((dev_x, dev_y))
dev_dataset = dev_dataset.batch(FLAGS.dev_batch_size)

test_dataset = tf.data.Dataset.from_tensor_slices((test_x, test_y))
test_dataset = test_dataset.batch(FLAGS.test_batch_size)

# A reinitializable iterator
iterator = tf.data.Iterator.from_structure(train_dataset.output_types, train_dataset.output_shapes)

train_initializer = iterator.make_initializer(train_dataset)
dev_initializer = iterator.make_initializer(dev_dataset)
test_initializer = iterator.make_initializer(test_dataset)

有了 Dataset 的 iterator,我们只需要调用一次 get_next() 方法即可得到 x 和 y_label 了,就不需要使用 placeholder 来声明了,代码如下:

1
2
3
# Input Layer
with tf.variable_scope('inputs'):
x, y_label = iterator.get_next()

接下来我们需要实现 embedding 层,调用 TensorFlow 的 embedding_lookup 即可实现,这里没有使用 Pre Train 的 embedding,代码实现如下:

1
2
3
4
# Embedding Layer
with tf.variable_scope('embedding'):
embedding = tf.Variable(tf.random_normal([vocab_size, FLAGS.embedding_size]), dtype=tf.float32)
inputs = tf.nn.embedding_lookup(embedding, x)

接下来我们就需要实现双向 LSTM 了,这里我们要构造一个 2 层的 Bi-LSTM 网络,实现的时候我们首先需要声明 LSTM Cell 的列表,然后调用 stack_bidirectional_rnn() 方法即可:

1
2
3
4
cell_fw = [lstm_cell(FLAGS.num_units, keep_prob) for _ in range(FLAGS.num_layer)]
cell_bw = [lstm_cell(FLAGS.num_units, keep_prob) for _ in range(FLAGS.num_layer)]
inputs = tf.unstack(inputs, FLAGS.time_step, axis=1)
output, _, _ = tf.contrib.rnn.stack_bidirectional_rnn(cell_fw, cell_bw, inputs=inputs, dtype=tf.float32)

这个方法内部是首先对每一层的 LSTM 进行正反向计算,然后对输出隐层进行 concat,然后输入下一层再进行计算,这里值得注意的地方是,我们不能把 LSTM Cell 提前组合成 MultiRNNCell 再调用 bidirectional_dynamic_rnn() 进行计算,这样相当于只有最后一层才进行 concat,是错误的。 现在我们得到的 output 就是 Bi-LSTM 的最后输出结果了。 接下来我们需要对输出结果进行一下 stack() 操作转化为一个 Tensor,然后将其 reshape() 一下,转化为 [-1, num_units * 2] 的 shape:

1
2
output = tf.stack(output, axis=1)
output = tf.reshape(output, [-1, FLAGS.num_units * 2])

这样我们再经过一层全连接网络将维度进行转换:

1
2
3
4
5
6
7
# Output Layer
with tf.variable_scope('outputs'):
w = weight([FLAGS.num_units * 2, FLAGS.category_num])
b = bias([FLAGS.category_num])
y = tf.matmul(output, w) + b
y_predict = tf.cast(tf.argmax(y, axis=1), tf.int32)
print('Output Y', y_predict)

这样得到的最后的 y_predict 即为预测结果,shape 为 [batch_size],即每一句都得到了一个最可能的结果标注。 接下来我们需要计算一下准确率和 Loss,准确率其实就是比较 y_predict 和 y_label 的相似度,Loss 即为二者交叉熵:

1
2
3
4
5
6
7
8
9
# Reshape y_label
y_label_reshape = tf.cast(tf.reshape(y_label, [-1]), tf.int32)
# Prediction
correct_prediction = tf.equal(y_predict, y_label_reshape)
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# Loss
cross_entropy = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y_label_reshape, logits=tf.cast(y, tf.float32)))
# Train
train = tf.train.AdamOptimizer(FLAGS.learning_rate).minimize(cross_entropy, global_step=global_step)

这里计算交叉熵使用的是 sparse_softmax_cross_entropy_with_logits() 方法,Optimizer 使用的是 Adam。 最后指定训练过程和测试过程即可,训练过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for epoch in range(FLAGS.epoch_num):
tf.train.global_step(sess, global_step_tensor=global_step)
# Train
sess.run(train_initializer)
for step in range(int(train_steps)):
smrs, loss, acc, gstep, _ = sess.run([summaries, cross_entropy, accuracy, global_step, train], feed_dict={keep_prob: FLAGS.keep_prob})
# Print log
if step % FLAGS.steps_per_print == 0:
print('Global Step', gstep, 'Step', step, 'Train Loss', loss, 'Accuracy', acc)

if epoch % FLAGS.epochs_per_dev == 0:
# Dev
sess.run(dev_initializer)
for step in range(int(dev_steps)):
if step % FLAGS.steps_per_print == 0:
print('Dev Accuracy', sess.run(accuracy, feed_dict={keep_prob: 1}), 'Step', step)

这里训练时首先调用了 train_initializer,将 iterator 指向训练数据,这样每调用一次 get_next(),x 和 y_label 就会被赋值为训练数据的一个 batch,接下来打印输出了 Loss,Accuracy 等内容。另外对于开发集来说,每次进行验证的时候也需要重新调用 dev_initializer,这样 iterator 会再次指向开发集,这样每调用一次 get_next(),x 和 y_label 就会被赋值为开发集的一个 batch,然后进行验证。 对于测试来说,我们可以计算其准确率,然后将测试的结果输出出来,代码实现如下:

1
2
3
4
5
6
7
8
9
10
sess.run(test_initializer)
for step in range(int(test_steps)):
x_results, y_predict_results, acc = sess.run([x, y_predict, accuracy], feed_dict={keep_prob: 1})
print('Test step', step, 'Accuracy', acc)
y_predict_results = np.reshape(y_predict_results, x_results.shape)
for i in range(len(x_results)):
x_result, y_predict_result = list(filter(lambda x: x, x_results[i])), list(
filter(lambda x: x, y_predict_results[i]))
x_text, y_predict_text = ''.join(id2word[x_result].values), ''.join(id2tag[y_predict_result].values)
print(x_text, y_predict_text)

这里打印输出了当前测试的准确率,然后得到了测试结果,然后再结合词表将测试的真正结果打印出来即可。

运行结果

在训练过程中,我们需要构建模型图,然后调用训练部分的代码进行训练,输出结果类似如下:

1
2
3
4
5
6
7
8
9
Global Step 0 Step 0 Train Loss 1.67181 Accuracy 0.1475
Global Step 100 Step 100 Train Loss 0.210423 Accuracy 0.928125
Global Step 200 Step 200 Train Loss 0.208561 Accuracy 0.920625
Global Step 300 Step 300 Train Loss 0.185281 Accuracy 0.939375
Global Step 400 Step 400 Train Loss 0.186069 Accuracy 0.938125
Global Step 500 Step 500 Train Loss 0.165667 Accuracy 0.94375
Global Step 600 Step 600 Train Loss 0.201692 Accuracy 0.9275
Global Step 700 Step 700 Train Loss 0.13299 Accuracy 0.954375
...

随着训练的进行,准确率可以达到 96% 左右。 在测试阶段,输出了当前模型的准确率及真实测试输出结果,输出结果类似如下:

1
2
3
4
Test step 0 Accuracy 0.946125
据新华社北京7月9日电连日来 sbmebebmmesbes
董新辉为自己此生不能侍奉母亲而难过 bmesbebebebmmesbe
...

可见测试准确率在 95% 左右,对于测试数据,此处还输出了每句话的序列标注结果,如第一行结果中,“据”字对应的标注就是 s,代表单字成词,“新”字对应的标注是 b,代表词的起始,“华”字对应标注是 m,代表词的中间,“社”字对应的标注是 e,代表结束,这样 “据”、“新华社” 就可以被分成两个词了,可见还是有一定效果的。

结语

本节通过搭建一个 Bi-LSTM 网络实现了序列标注,并可实现分词,准确率可达到 95% 左右,但是最主要的还是学习 Bi-LSTM 的用法,本实例代码较多,部分代码已经省略,完整代码见:https://github.com/AIDeepLearning/BiLSTMWordBreaker

参考来源

Python

Ansible简介

Ansible是由Python开发的一个运维工具,因为工作需要接触到Ansible,经常会集成一些东西到Ansible,所以对Ansible的了解越来越多。 那Ansible到底是什么呢?在我的理解中,原来需要登录到服务器上,然后执行一堆命令才能完成一些操作。而Ansible就是来代替我们去执行那些命令。并且可以通过Ansible控制多台机器,在机器上进行任务的编排和执行,在Ansible中称为playbook。 那Ansible是如何做到的呢?简单点说,就是Ansible将我们要执行的命令生成一个脚本,然后通过sftp将脚本上传到要执行命令的服务器上,然后在通过ssh协议,执行这个脚本并将执行结果返回。 那Ansible具体是怎么做到的呢?下面从模块和插件来看一下Ansible是如何完成一个模块的执行 PS:下面的分析都是在对Ansible有一些具体使用经验之后,通过阅读源代码进一步得出的执行结论,所以希望在看本文时,是建立在对Ansible有一定了解的基础上,最起码对于Ansible的一些概念有了解,例如inventory,module,playbooks等

Ansible模块

模块是Ansible执行的最小单位,可以是由Python编写,也可以是Shell编写,也可以是由其他语言编写。模块中定义了具体的操作步骤以及实际使用过程中所需要的参数 执行的脚本就是根据模块生成一个可执行的脚本。 那Ansible是怎么样将这个脚本上传到服务器上,然后执行获取结果的呢?

Ansible插件

connection插件

连接插件,根据指定的ssh参数连接指定的服务器,并切提供实际执行命令的接口

shell插件

命令插件,根据sh类型,来生成用于connection时要执行的命令

strategy插件

执行策略插件,默认情况下是线性插件,就是一个任务接着一个任务的向下执行,此插件将任务丢到执行器去执行。

action插件

动作插件,实质就是任务模块的所有动作,如果ansible的模块没有特别编写的action插件,默认情况下是normal或者async(这两个根据模块是否async来选择),normal和async中定义的就是模块的执行步骤。例如,本地创建临时文件,上传临时文件,执行脚本,删除脚本等等,如果想在所有的模块中增加一些特殊步骤,可以通过增加action插件的方式来扩展。

Ansible执行模块流程

  1. ansible命令实质是通过ansible/cli/adhoc.py来运行,同时会收集参数信息
    1. 设置Play信息,然后通过TaskQueueManager进行run,
    2. TaskQueueManager需要Inventory(节点仓库),variable_manager(收集变量),options(命令行中指定的参数),stdout_callback(回调函数)
  2. 在task_queue_manager.py中找到run中
    1. 初始化时会设置队列
    2. 会根据options,,variable_manager,passwords等信息设置成一个PlayContext信息(playbooks/playcontext.py)
    3. 设置插件(plugins)信息callback_loader(回调), strategy_loader(执行策略), module_loader(任务模块)
    4. 通过strategy_loader(strategy插件)的run(默认的strategy类型是linear,线性执行),去按照顺序执行所有的任务(执行一个模块,可能会执行多个任务)
    5. 在strategy_loader插件run之后,会判断action类型。如果是meta类型的话会单独执行(不是具体的ansible模块时),而其他模块时,会加载到队列_queue_task
    6. 在队列中会调用WorkerProcess去处理,在workerproces实际的run之后,会使用TaskExecutor进行执行
    7. 在TaskExecutor中会设置connection插件,并且根据task的类型(模块。或是include等)获取action插件,就是对应的模块,如果模块有自定义的执行,则会执行自定义的action,如果没有的会使用normal或者async,这个是根据是否是任务的async属性来决定
    8. 在Action插件中定义着执行的顺序,及具体操作,例如生成临时目录,生成临时脚本,所以要在统一的模式下,集成一些额外的处理时,可以重写Action的方法
    9. 通过Connection插件来执行Action的各个操作步骤

扩展Ansible实例

执行节点Python环境扩展

实际需求中,我们扩展的一些Ansible模块需要使用三方库,但每个节点中安装这些库有些不易于管理。ansible执行模块的实质就是在节点的python环境下执行生成的脚本,所以我们采取的方案是,指定节点上的Python环境,将局域网内一个python环境作为nfs共享。通过扩展Action插件,增加节点上挂载nfs,待执行结束后再将节点上的nfs卸载。具体实施步骤如下: 扩展代码:

重写ActionBase的execute_module方法

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
187
188
189
190
191
192
193
194
# execute_module

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import json
import pipes

from ansible.compat.six import text_type, iteritems

from ansible import constants as C
from ansible.errors import AnsibleError
from ansible.release import __version__

try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()


class MagicStackBase(object):

def _mount_nfs(self, ansible_nfs_src, ansible_nfs_dest):
cmd = ['mount',ansible_nfs_src, ansible_nfs_dest]
cmd = [pipes.quote(c) for c in cmd]
cmd = ' '.join(cmd)
result = self._low_level_execute_command(cmd=cmd, sudoable=True)
return result

def _umount_nfs(self, ansible_nfs_dest):
cmd = ['umount', ansible_nfs_dest]
cmd = [pipes.quote(c) for c in cmd]
cmd = ' '.join(cmd)
result = self._low_level_execute_command(cmd=cmd, sudoable=True)
return result

def _execute_module(self, module_name=None, module_args=None, tmp=None, task_vars=None, persist_files=False, delete_remote_tmp=True):
'''
Transfer and run a module along with its arguments.
'''

# display.v(task_vars)

if task_vars is None:
task_vars = dict()

# if a module name was not specified for this execution, use
# the action from the task
if module_name is None:
module_name = self._task.action
if module_args is None:
module_args = self._task.args

# set check mode in the module arguments, if required
if self._play_context.check_mode:
if not self._supports_check_mode:
raise AnsibleError("check mode is not supported for this operation")
module_args['_ansible_check_mode'] = True
else:
module_args['_ansible_check_mode'] = False

# Get the connection user for permission checks
remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user

# set no log in the module arguments, if required
module_args['_ansible_no_log'] = self._play_context.no_log or C.DEFAULT_NO_TARGET_SYSLOG

# set debug in the module arguments, if required
module_args['_ansible_debug'] = C.DEFAULT_DEBUG

# let module know we are in diff mode
module_args['_ansible_diff'] = self._play_context.diff

# let module know our verbosity
module_args['_ansible_verbosity'] = display.verbosity

# give the module information about the ansible version
module_args['_ansible_version'] = __version__

# set the syslog facility to be used in the module
module_args['_ansible_syslog_facility'] = task_vars.get('ansible_syslog_facility', C.DEFAULT_SYSLOG_FACILITY)

# let module know about filesystems that selinux treats specially
module_args['_ansible_selinux_special_fs'] = C.DEFAULT_SELINUX_SPECIAL_FS

(module_style, shebang, module_data) = self._configure_module(module_name=module_name, module_args=module_args, task_vars=task_vars)
if not shebang:
raise AnsibleError("module (%s) is missing interpreter line" % module_name)

# get nfs info for mount python packages
ansible_nfs_src = task_vars.get("ansible_nfs_src", None)
ansible_nfs_dest = task_vars.get("ansible_nfs_dest", None)

# a remote tmp path may be necessary and not already created
remote_module_path = None
args_file_path = None
if not tmp and self._late_needs_tmp_path(tmp, module_style):
tmp = self._make_tmp_path(remote_user)

if tmp:
remote_module_filename = self._connection._shell.get_remote_filename(module_name)
remote_module_path = self._connection._shell.join_path(tmp, remote_module_filename)
if module_style in ['old', 'non_native_want_json']:
# we'll also need a temp file to hold our module arguments
args_file_path = self._connection._shell.join_path(tmp, 'args')

if remote_module_path or module_style != 'new':
display.debug("transferring module to remote")
self._transfer_data(remote_module_path, module_data)
if module_style == 'old':
# we need to dump the module args to a k=v string in a file on
# the remote system, which can be read and parsed by the module
args_data = ""
for k,v in iteritems(module_args):
args_data += '%s=%s ' % (k, pipes.quote(text_type(v)))
self._transfer_data(args_file_path, args_data)
elif module_style == 'non_native_want_json':
self._transfer_data(args_file_path, json.dumps(module_args))
display.debug("done transferring module to remote")

environment_string = self._compute_environment_string()

remote_files = None

if args_file_path:
remote_files = tmp, remote_module_path, args_file_path
elif remote_module_path:
remote_files = tmp, remote_module_path

# Fix permissions of the tmp path and tmp files. This should be
# called after all files have been transferred.
if remote_files:
self._fixup_perms2(remote_files, remote_user)


# mount nfs
if ansible_nfs_src and ansible_nfs_dest:
result = self._mount_nfs(ansible_nfs_src, ansible_nfs_dest)
if result['rc'] != 0:
raise AnsibleError("mount nfs failed!!! {0}".format(result['stderr']))

cmd = ""
in_data = None

if self._connection.has_pipelining and self._play_context.pipelining and not C.DEFAULT_KEEP_REMOTE_FILES and module_style == 'new':
in_data = module_data
else:
if remote_module_path:
cmd = remote_module_path

rm_tmp = None
if tmp and "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files and delete_remote_tmp:
if not self._play_context.become or self._play_context.become_user == 'root':
# not sudoing or sudoing to root, so can cleanup files in the same step
rm_tmp = tmp

cmd = self._connection._shell.build_module_command(environment_string, shebang, cmd, arg_path=args_file_path, rm_tmp=rm_tmp)
cmd = cmd.strip()
sudoable = True
if module_name == "accelerate":
# always run the accelerate module as the user
# specified in the play, not the sudo_user
sudoable = False


res = self._low_level_execute_command(cmd, sudoable=sudoable, in_data=in_data)

# umount nfs
if ansible_nfs_src and ansible_nfs_dest:
result = self._umount_nfs(ansible_nfs_dest)
if result['rc'] != 0:
raise AnsibleError("umount nfs failed!!! {0}".format(result['stderr']))

if tmp and "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files and delete_remote_tmp:
if self._play_context.become and self._play_context.become_user != 'root':
# not sudoing to root, so maybe can't delete files as that other user
# have to clean up temp files as original user in a second step
tmp_rm_cmd = self._connection._shell.remove(tmp, recurse=True)
tmp_rm_res = self._low_level_execute_command(tmp_rm_cmd, sudoable=False)
tmp_rm_data = self._parse_returned_data(tmp_rm_res)
if tmp_rm_data.get('rc', 0) != 0:
display.warning('Error deleting remote temporary files (rc: {0}, stderr: {1})'.format(tmp_rm_res.get('rc'), tmp_rm_res.get('stderr', 'No error string available.')))

# parse the main result
data = self._parse_returned_data(res)

# pre-split stdout into lines, if stdout is in the data and there
# isn't already a stdout_lines value there
if 'stdout' in data and 'stdout_lines' not in data:
data['stdout_lines'] = data.get('stdout', u'').splitlines()

display.debug("done with _execute_module (%s, %s)" % (module_name, module_args))
return data

集成到normal.py和async.py中,记住要将这两个插件在ansible.cfg中进行配置

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
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

from ansible.plugins.action import ActionBase
from ansible.utils.vars import merge_hash

from common.ansible_plugins import MagicStackBase


class ActionModule(MagicStackBase, ActionBase):

def run(self, tmp=None, task_vars=None):
if task_vars is None:
task_vars = dict()

results = super(ActionModule, self).run(tmp, task_vars)
# remove as modules might hide due to nolog
del results['invocation']['module_args']
results = merge_hash(results, self._execute_module(tmp=tmp, task_vars=task_vars))
# Remove special fields from the result, which can only be set
# internally by the executor engine. We do this only here in
# the 'normal' action, as other action plugins may set this.
#
# We don't want modules to determine that running the module fires
# notify handlers. That's for the playbook to decide.
for field in ('_ansible_notify',):
if field in results:
results.pop(field)

return results
  • 配置ansible.cfg,将扩展的插件指定为ansible需要的action插件
  • 重写插件方法,重点是execute_module
  • 执行命令中需要指定Python环境,将需要的参数添加进去nfs挂载和卸载的参数
1
ansible 51 -m mysql_db -a "state=dump name=all target=/tmp/test.sql" -i hosts -u root -v -e "ansible_nfs_src=172.16.30.170:/web/proxy_env/lib64/python2.7/site-packages ansible_nfs_dest=/root/.pyenv/versions/2.7.10/lib/python2.7/site-packages ansible_python_interpreter=/root/.pyenv/versions/2.7.10/bin/python"

Python

背景

用 Python 做过爬虫的小伙伴可能接触过 Scrapy,GitHub:https://github.com/scrapy/scrapy。Scrapy 的确是一个非常强大的爬虫框架,爬取效率高,扩展性好,基本上是使用 Python 开发爬虫的必备利器。如果使用 Scrapy 做爬虫,那么在爬取时,我们当然完全可以使用自己的主机来完成爬取,但当爬取量非常大的时候,我们肯定不能在自己的机器上来运行爬虫了,一个好的方法就是将 Scrapy 部署到远程服务器上来执行。 所以,这时候就出现了另一个库 Scrapyd,GitHub:https://github.com/scrapy/scrapyd,有了它我们只需要在远程服务器上安装一个 Scrapyd,启动这个服务,就可以将我们写的 Scrapy 项目部署到远程主机上了,Scrapyd 还提供了各种操作 API,可以自由地控制 Scrapy 项目的运行,API 文档:http://scrapyd.readthedocs.io/en/stable/api.html,例如我们将 Scrapyd 安装在 IP 为 88.88.88.88 的服务器上,然后将 Scrapy 项目部署上去,这时候我们通过请求 API 就可以来控制 Scrapy 项目的运行了,命令如下:

1
curl http://88.88.88.88:6800/schedule.json -d project=myproject -d spider=somespider

这样就相当于启动了 myproject 项目的 somespider 爬虫,而不用我们再用命令行方式去启动爬虫,同时 Scrapyd 还提供了查看爬虫状态、取消爬虫任务、添加爬虫版本、删除爬虫版本等等的一系列 API,所以说,有了 Scrapyd,我们可以通过 API 来控制爬虫的运行,摆脱了命令行的依赖。 另外爬虫部署还是个麻烦事,因为我们需要将爬虫代码上传到远程服务器上,这个过程涉及到打包和上传两个过程,在 Scrapyd 中其实提供了这个部署的 API,叫做 addversion,但是它接受的内容是 egg 包文件,所以说要用这个接口,我们必须要把我们的 Scrapy 项目打包成 egg 文件,然后再利用文件上传的方式请求这个 addversion 接口才可以完成上传,这个过程又比较繁琐了,所以又出现了一个工具叫做 Scrapyd-Client,GitHub:https://github.com/scrapy/scrapyd-client,利用它的 scrapyd-deploy 命令我们便可以完成打包和上传的两个功能,可谓是又方便了一步。 这样我们就已经解决了部署的问题,回过头来,如果我们要想实时查看服务器上 Scrapy 的运行状态,那该怎么办呢?像刚才说的,当然是请求 Scrapyd 的 API 了,如果我们想用 Python 程序来控制一下呢?我们还要用 requests 库一次次地请求这些 API ?这就太麻烦了吧,所以为了解决这个需求,Scrapyd-API 又出现了,GitHub:https://github.com/djm/python-scrapyd-api,有了它我们可以只用简单的 Python 代码就可以实现 Scrapy 项目的监控和运行:

1
2
3
from scrapyd_api import ScrapydAPI
scrapyd = ScrapydAPI('http://88.888.88.88:6800')
scrapyd.list_jobs('project_name')

这样它的返回结果就是各个 Scrapy 项目的运行情况。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
'pending': [
],
'running': [
{
'id': u'14a65...b27ce',
'spider': u'spider_name',
'start_time': u'2018-01-17 22:45:31.975358'
},
],
'finished': [
{
'id': '34c23...b21ba',
'spider': 'spider_name',
'start_time': '2018-01-11 22:45:31.975358',
'end_time': '2018-01-17 14:01:18.209680'
}
]
}

这样我们就可以看到 Scrapy 爬虫的运行状态了。 所以,有了它们,我们可以完成的是:

  • 通过 Scrapyd 完成 Scrapy 项目的部署
  • 通过 Scrapyd 提供的 API 来控制 Scrapy 项目的启动及状态监控
  • 通过 Scrapyd-Client 来简化 Scrapy 项目的部署
  • 通过 Scrapyd-API 来通过 Python 控制 Scrapy 项目

是不是方便多了? 可是?真的达到最方便了吗?肯定没有!如果这一切的一切,从 Scrapy 的部署、启动到监控、日志查看,我们只需要鼠标键盘点几下就可以完成,那岂不是美滋滋?更或者说,连 Scrapy 代码都可以帮你自动生成,那岂不是爽爆了? 有需求就有动力,没错,Gerapy 就是为此而生的,GitHub:https://github.com/Gerapy/Gerapy。 本节我们就来简单了解一下 Gerapy 分布式爬虫管理框架的使用方法。

安装

Gerapy 是一款分布式爬虫管理框架,支持 Python 3,基于 Scrapy、Scrapyd、Scrapyd-Client、Scrapy-Redis、Scrapyd-API、Scrapy-Splash、Jinjia2、Django、Vue.js 开发,Gerapy 可以帮助我们:

  • 更方便地控制爬虫运行
  • 更直观地查看爬虫状态
  • 更实时地查看爬取结果
  • 更简单地实现项目部署
  • 更统一地实现主机管理
  • 更轻松地编写爬虫代码

安装非常简单,只需要运行 pip3 命令即可:

1
$ pip3 install gerapy

安装完成之后我们就可以使用 gerapy 命令了,输入 gerapy 便可以获取它的基本使用方法:

1
2
3
4
5
6
7
$ gerapy
Usage:
gerapy init [--folder=<folder>]
gerapy migrate
gerapy createsuperuser
gerapy runserver [<host:port>]
gerapy makemigrations

如果出现上述结果,就证明 Gerapy 安装成功了。

初始化

接下来我们来开始使用 Gerapy,首先利用如下命令进行一下初始化,在任意路径下均可执行如下命令:

1
$ gerapy init

执行完毕之后,本地便会生成一个名字为 gerapy 的文件夹,接着进入该文件夹,可以看到有一个 projects 文件夹,我们后面会用到。 紧接着执行数据库初始化命令:

1
2
cd gerapy
gerapy migrate

这样它就会在 gerapy 目录下生成一个 SQLite 数据库,同时建立数据库表。 接着我们只需要再运行命令启动服务就好了:

1
gerapy runserver

这样我们就可以看到 Gerapy 已经在 8000 端口上运行了。 全部的操作流程截图如下: 接下来我们在浏览器中打开 http://localhost:8000/,就可以看到 Gerapy 的主界面了: 这里显示了主机、项目的状态,当然由于我们没有添加主机,所以所有的数目都是 0。 如果我们可以正常访问这个页面,那就证明 Gerapy 初始化都成功了。

主机管理

接下来我们可以点击左侧 Clients 选项卡,即主机管理页面,添加我们的 Scrapyd 远程服务,点击右上角的创建按钮即可添加我们需要管理的 Scrapyd 服务: 需要添加 IP、端口,以及名称,点击创建即可完成添加,点击返回即可看到当前添加的 Scrapyd 服务列表,样例如下所示: 这样我们可以在状态一栏看到各个 Scrapyd 服务是否可用,同时可以一目了然当前所有 Scrapyd 服务列表,另外我们还可以自由地进行编辑和删除。

项目管理

Gerapy 的核心功能当然是项目管理,在这里我们可以自由地配置、编辑、部署我们的 Scrapy 项目,点击左侧的 Projects ,即项目管理选项,我们可以看到如下空白的页面: 假设现在我们有一个 Scrapy 项目,如果我们想要进行管理和部署,还记得初始化过程中提到的 projects 文件夹吗?这时我们只需要将项目拖动到刚才 gerapy 运行目录的 projects 文件夹下,例如我这里写好了一个 Scrapy 项目,名字叫做 zhihusite,这时把它拖动到 projects 文件夹下: 这时刷新页面,我们便可以看到 Gerapy 检测到了这个项目,同时它是不可配置、没有打包的: 这时我们可以点击部署按钮进行打包和部署,在右下角我们可以输入打包时的描述信息,类似于 Git 的 commit 信息,然后点击打包按钮,即可发现 Gerapy 会提示打包成功,同时在左侧显示打包的结果和打包名称: 打包成功之后,我们便可以进行部署了,我们可以选择需要部署的主机,点击后方的部署按钮进行部署,同时也可以批量选择主机进行部署,示例如下: 可以发现此方法相比 Scrapyd-Client 的命令行式部署,简直不能方便更多。

监控任务

部署完毕之后就可以回到主机管理页面进行任务调度了,任选一台主机,点击调度按钮即可进入任务管理页面,此页面可以查看当前 Scrapyd 服务的所有项目、所有爬虫及运行状态: 我们可以通过点击新任务、停止等按钮来实现任务的启动和停止等操作,同时也可以通过展开任务条目查看日志详情: 另外我们还可以随时点击停止按钮来取消 Scrapy 任务的运行。 这样我们就可以在此页面方便地管理每个 Scrapyd 服务上的 每个 Scrapy 项目的运行了。

项目编辑

同时 Gerapy 还支持项目编辑功能,有了它我们不再需要 IDE 即可完成项目的编写,我们点击项目的编辑按钮即可进入到编辑页面,如图所示: 这样即使 Gerapy 部署在远程的服务器上,我们不方便用 IDE 打开,也不喜欢用 Vim 等编辑软件,我们可以借助于本功能方便地完成代码的编写。

代码生成

上述的项目主要针对的是我们已经写好的 Scrapy 项目,我们可以借助于 Gerapy 方便地完成编辑、部署、控制、监测等功能,而且这些项目的一些逻辑、配置都是已经写死在代码里面的,如果要修改的话,需要直接修改代码,即这些项目都是不可配置的。 在 Scrapy 中,其实提供了一个可配置化的爬虫 CrawlSpider,它可以利用一些规则来完成爬取规则和解析规则的配置,这样可配置化程度就非常高,这样我们只需要维护爬取规则、提取逻辑就可以了。如果要新增一个爬虫,我们只需要写好对应的规则即可,这类爬虫就叫做可配置化爬虫。 Gerapy 可以做到:我们写好爬虫规则,它帮我们自动生成 Scrapy 项目代码。 我们可以点击项目页面的右上角的创建按钮,增加一个可配置化爬虫,接着我们便可以在此处添加提取实体、爬取规则、抽取规则了,例如这里的解析器,我们可以配置解析成为哪个实体,每个字段使用怎样的解析方式,如 XPath 或 CSS 解析器、直接获取属性、直接添加值等多重方式,另外还可以指定处理器进行数据清洗,或直接指定正则表达式进行解析等等,通过这些流程我们可以做到任何字段的解析。 再比如爬取规则,我们可以指定从哪个链接开始爬取,允许爬取的域名是什么,该链接提取哪些跟进的链接,用什么解析方法来处理等等配置。通过这些配置,我们可以完成爬取规则的设置。 最后点击生成按钮即可完成代码的生成。 生成的代码示例结果如图所示,可见其结构和 Scrapy 代码是完全一致的。 生成代码之后,我们只需要像上述流程一样,把项目进行部署、启动就好了,不需要我们写任何一行代码,即可完成爬虫的编写、部署、控制、监测。

结语

以上便是 Gerapy 分布式爬虫管理框架的基本用法,如需了解更多,可以访问其 GitHub:https://github.com/Gerapy/Gerapy。 如果觉得此框架有不足的地方,欢迎提 Issue,也欢迎发 Pull Request 来贡献代码,如果觉得 Gerapy 有所帮助,还望赐予一个 Star!非常感谢!

Python

本节来介绍一下使用 RNN 的 LSTM 来做 MNIST 分类的方法,RNN 相比 CNN 来说,速度可能会慢,但可以节省更多的内存空间。

初始化

首先我们可以先初始化一些变量,如学习率、节点单元数、RNN 层数等:

1
2
3
4
5
6
7
8
9
10
11
learning_rate = 1e-3
num_units = 256
num_layer = 3
input_size = 28
time_step = 28
total_steps = 2000
category_num = 10
steps_per_validate = 100
steps_per_test = 500
batch_size = tf.placeholder(tf.int32, [])
keep_prob = tf.placeholder(tf.float32, [])

然后还需要声明一下 MNIST 数据生成器:

1
2
3
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data/', one_hot=True)

接下来常规声明一下输入的数据,输入数据用 x 表示,标注数据用 y_label 表示:

1
2
x = tf.placeholder(tf.float32, [None, 784])
y_label = tf.placeholder(tf.float32, [None, 10])

这里输入的 x 维度是 [None, 784],代表 batch_size 不确定,输入维度 784,y_label 同理。 接下来我们需要对输入的 x 进行 reshape 操作,因为我们需要将一张图分为多个 time_step 来输入,这样才能构建一个 RNN 序列,所以这里直接将 time_step 设成 28,这样一来 input_size 就变为了 28,batch_size 不变,所以reshape 的结果是一个三维的矩阵:

1
x_shape = tf.reshape(x, [-1, time_step, input_size])

RNN 层

接下来我们需要构建一个 RNN 模型了,这里我们使用的 RNN Cell 是 LSTMCell,而且要搭建一个三层的 RNN,所以这里还需要用到 MultiRNNCell,它的输入参数是 LSTMCell 的列表。 所以我们可以先声明一个方法用于创建 LSTMCell,方法如下:

1
2
3
def cell(num_units):
cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=num_units)
return DropoutWrapper(cell, output_keep_prob=keep_prob)

这里还加入了 Dropout,来减少训练过程中的过拟合。 接下来我们再利用它来构建多层的 RNN:

1
cells = tf.nn.rnn_cell.MultiRNNCell([cell(num_units) for _ in range(num_layer)])

注意这里使用了 for 循环,每循环一次新生成一个 LSTMCell,而不是直接使用乘法来扩展列表,因为这样会导致 LSTMCell 是同一个对象,导致构建完 MultiRNNCell 之后出现维度不匹配的问题。 接下来我们需要声明一个初始状态:

1
h0 = cells.zero_state(batch_size, dtype=tf.float32)

然后接下来调用 dynamic_rnn() 方法即可完成模型的构建了:

1
output, hs = tf.nn.dynamic_rnn(cells, inputs=x_shape, initial_state=h0)

这里 inputs 的输入就是 x 做了 reshape 之后的结果,初始状态通过 initial_state 传入,其返回结果有两个,一个 output 是所有 time_step 的输出结果,赋值为 output,它是三维的,第一维长度等于 batch_size,第二维长度等于 time_step,第三维长度等于 num_units。另一个 hs 是隐含状态,是元组形式,长度即 RNN 的层数 3,每一个元素都包含了 c 和 h,即 LSTM 的两个隐含状态。 这样的话 output 的最终结果可以取最后一个 time_step 的结果,所以可以使用:

1
output = output[:, -1, :]

或者直接取隐藏状态最后一层的 h 也是相同的:

1
h = hs[-1].h

在此模型中,二者是等价的。但注意如果用于文本处理,可能由于文本长度不一,而 padding,导致二者不同。

输出层

接下来我们再做一次线性变换和 Softmax 输出结果即可:

1
2
3
4
5
6
# Output Layer
w = tf.Variable(tf.truncated_normal([num_units, category_num], stddev=0.1), dtype=tf.float32)
b = tf.Variable(tf.constant(0.1, shape=[category_num]), dtype=tf.float32)
y = tf.matmul(output, w) + b
# Loss
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=y_label, logits=y)

这里的 Loss 直接调用了 softmax_cross_entropy_with_logits 先计算了 Softmax,然后计算了交叉熵。

训练和评估

最后再定义训练和评估的流程即可,在训练过程中每隔一定的 step 就输出 Train Accuracy 和 Test Accuracy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Train
train = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cross_entropy)

# Prediction
correction_prediction = tf.equal(tf.argmax(y, axis=1), tf.argmax(y_label, axis=1))
accuracy = tf.reduce_mean(tf.cast(correction_prediction, tf.float32))

# Train
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for step in range(total_steps + 1):
batch_x, batch_y = mnist.train.next_batch(100)
sess.run(train, feed_dict={x: batch_x, y_label: batch_y, keep_prob: 0.5, batch_size: batch_x.shape[0]})
# Train Accuracy
if step % steps_per_validate == 0:
print('Train', step, sess.run(accuracy, feed_dict={x: batch_x, y_label: batch_y, keep_prob: 0.5,
batch_size: batch_x.shape[0]}))
# Test Accuracy
if step % steps_per_test == 0:
test_x, test_y = mnist.test.images, mnist.test.labels
print('Test', step,
sess.run(accuracy, feed_dict={x: test_x, y_label: test_y, keep_prob: 1, batch_size: test_x.shape[0]}))

运行

直接运行之后,只训练了几轮就可以达到 98% 的准确率:

1
2
3
4
5
6
7
8
9
10
11
Train 0 0.27
Test 0 0.2223
Train 100 0.87
Train 200 0.91
Train 300 0.94
Train 400 0.94
Train 500 0.99
Test 500 0.9595
Train 600 0.95
Train 700 0.97
Train 800 0.98

可以看出来 LSTM 在做 MNIST 字符分类的任务上还是比较有效的。

本节代码

本节代码地址为:https://github.com/AIDeepLearning/LSTMClassification

Python

本文介绍下 RNN 及几种变种的结构和对应的 TensorFlow 源码实现,另外通过简单的实例来实现 TensorFlow RNN 相关类的调用。

RNN

RNN,循环神经网络,Recurrent Neural Networks。人们思考问题往往不是从零开始的,比如阅读时我们对每个词的理解都会依赖于前面看到的一些信息,而不是把前面看的内容全部抛弃再去理解某处的信息。应用到深度学习上面,如果我们想要学习去理解一些依赖上文的信息,RNN 便可以做到,它有一个循环的操作,可以使其可以保留之前学习到的内容。 RNN 的结构如下: 在上图网络结构中,对于矩形块 A 的那部分,通过输入xt(t时刻的特征向量),它会输出一个结果ht(t时刻的状态或者输出)。网络中的循环结构使得某个时刻的状态能够传到下一个时刻。 这些循环的结构让 RNNs 看起来有些难以理解,但我们可以把 RNNs 看成是一个普通的网络做了多次复制后叠加在一起组成的,每一网络会把它的输出传递到下一个网络中。我们可以把 RNNs 在时间步上进行展开,就得到下图这样: 所以最基本的 RNN Cell 输入就是 xt,它还会输出一个隐含内容传递到下一个 Cell,同时还会生成一个结果 ht,其最基本的结构如如下: 仅仅是输入的 xt 和隐藏状态进行 concat,然后经过线性变换后经过一个 tanh 激活函数便输出了,另外隐含内容和输出结果是相同的内容。 我们来分析一下 TensorFlow 里面 RNN Cell 的实现。 TensorFlow 实现 RNN Cell 的位置在 python/ops/rnncellimpl.py,首先其实现了一个 RNNCell 类,继承了 Layer 类,其内部有三个比较重要的方法,state_size()、output_size()、__call() 方法,其中 state_size() 和 output_size() 方法设置为类属性,可以当做属性来调用,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
@property
def state_size(self):
"""size(s) of state(s) used by this cell.
It can be represented by an Integer, a TensorShape or a tuple of Integers
or TensorShapes.
"""
raise NotImplementedError("Abstract method")

@property
def output_size(self):
"""Integer or TensorShape: size of outputs produced by this cell."""
raise NotImplementedError("Abstract method")

分别代表 Cell 的状态和输出维度,和 Cell 中的神经元数量有关,但这里两个方法都没有实现,意思是说我们必须要实现一个子类继承 RNNCell 类并实现这两个方法。 另外对于 call() 方法,实际上就是当初始化的对象直接被调用的时候触发的方法,实现如下:

1
2
3
4
5
6
7
8
9
def __call__(self, inputs, state, scope=None):
if scope is not None:
with vs.variable_scope(scope,
custom_getter=self._rnn_get_variable) as scope:
return super(RNNCell, self).__call__(inputs, state, scope=scope)
else:
with vs.variable_scope(vs.get_variable_scope(),
custom_getter=self._rnn_get_variable):
return super(RNNCell, self).__call__(inputs, state)

实际上是调用了父类 Layer 的 call() 方法,但父类中 call() 方法中又调用了 call() 方法,而 Layer 类的 call() 方法的实现如下:

1
2
def call(self, inputs, **kwargs):
return inputs

父类的 call() 方法实现非常简单,所以要实现其真正的功能,只需要在继承 RNNCell 类的子类中实现 call() 方法即可。 接下来我们看下 RNN Cell 的最基本的实现,叫做 BasicRNNCell,其代码如下:

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
class BasicRNNCell(RNNCell):
"""The most basic RNN cell.
Args:
num_units: int, The number of units in the RNN cell.
activation: Nonlinearity to use. Default: `tanh`.
reuse: (optional) Python boolean describing whether to reuse variables
in an existing scope. If not `True`, and the existing scope already has
the given variables, an error is raised.
"""

def __init__(self, num_units, activation=None, reuse=None):
super(BasicRNNCell, self).__init__(_reuse=reuse)
self._num_units = num_units
self._activation = activation or math_ops.tanh
self._linear = None

@property
def state_size(self):
return self._num_units

@property
def output_size(self):
return self._num_units

def call(self, inputs, state):
"""Most basic RNN: output = new_state = act(W * input + U * state + B)."""
if self._linear is None:
self._linear = _Linear([inputs, state], self._num_units, True)

output = self._activation(self._linear([inputs, state]))
return output, output

可以看到在初始化的时候,最终要的一个参数是 numunits,意思就是这个 Cell 中神经元的个数,另外还有一个参数 activation 即默认使用的激活函数,默认使用的 tanh,reuse 代表该 Cell 是否可以被重新使用。 在 statesize()、output_size() 方法里,其返回的内容都是 num_units,即神经元的个数,接下来 call() 方法中,传入的参数为 inputs 和 state,即输入的 x 和 上一次的隐含状态,首先实例化了一个 _Linear 类,这个类实际上就是做线性变换的类,将二者传递过来,然后直接调用,就实现了 w * [inputs, state] + b 的线性变换,其中 _Linear 类的 __call() 方法实现如下:

1
2
3
4
5
6
7
8
9
10
def __call__(self, args):
if not self._is_sequence:
args = [args]
if len(args) == 1:
res = math_ops.matmul(args[0], self._weights)
else:
res = math_ops.matmul(array_ops.concat(args, 1), self._weights)
if self._build_bias:
res = nn_ops.bias_add(res, self._biases)
return res

很明显这里传递了 [inputs, state] 作为 call() 方法的 args,会执行 concat() 和 matmul() 方法,然后接着再执行 bias_add() 方法,这样就实现了线性变换。 最后回到 BasicRNNCell 的 call() 方法中,在 _linear() 方法外面又包括了一层 _activation() 方法,即对线性变换应用一次 tanh 激活函数处理,作为输出结果。 最后返回的结果是 output 和 output,第一个代表 output,第二个代表隐状态,其值也等于 output。 我们用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
import tensorflow as tf

cell = tf.nn.rnn_cell.BasicRNNCell(num_units=128)
print(cell.state_size)
inputs = tf.placeholder(tf.float32, shape=[32, 100])
h0 = cell.zero_state(32, tf.float32)
output, h1 = cell(inputs=inputs, state=h0)
print(output, output.shape)
print(h1, h1.shape)

这里我们首先初始化了一个神经元个数为 128 的 BasicRNNCell 类,然后构造了一个 shape 为 [32, 100] 的变量作为 inputs,其代表 batch_size 为 32, 维度为 100,随后初始化了初始隐藏状态,调用了 zero_state() 方法,然后直接调用 cell,实际上是最终调用了其 call() 方法,最后得到 output 和 h1,打印输出结果:

1
2
3
128
Tensor("basic_rnn_cell/Tanh:0", shape=(32, 128), dtype=float32) (32, 128)
Tensor("basic_rnn_cell/Tanh:0", shape=(32, 128), dtype=float32) (32, 128)

可以看到,当输入变量维度为 100 的时候,经过一个 128 神经元 Cell 之后,输出维度变成了 128,其输出 shape 变成了 [32, 128],且此时输出结果和隐藏状态是相同的。

LSTM

RNNs 的出现,主要是因为它们能够把以前的信息联系到现在,从而解决现在的问题。比如,利用前面的信息,能够帮助我们理解当前的内容。 有时候,我们在处理当前任务的时候,只需要看一下比较近的一些信息。比如在一个语言模型中,我们要通过上文来预测一下个词可能是什么,那么当我们看到 “the clouds are in the?”时,不需要更多的信息,我们就能够自然而然的想到下一个词应该是“sky”。在这样的情况下,我们所要预测的内容和相关信息之间的间隔很小,这种情况下 RNNs 就能够利用过去的信息, 很容易实现: 但是如果我们想依赖前文距离非常远的信息时,普通的 RNN 就非常难以做到了,随着间隔信息的增大,RNN 难以对其做关联: 但是 LSTM 可以用来解决这个问题。 LSTM,Long Short Term Memory Networks,是 RNN 的一个变种,经试验它可以用来解决更多问题,并取得了非常好的效果。 LSTM Cell 的结构如下: LSTMs 最关键的地方在于 Cell 的状态 和 结构图上面的那条横穿的水平线。 Cell 状态的传输就像一条传送带,向量从整个 Cell 中穿过,只是做了少量的线性操作。这种结构能够很轻松地实现信息从整个 Cell 中穿过而不做改变。 若只有上面的那条水平线是没办法实现添加或者删除信息的,信息的操作是是通过一种叫做门的结构来实现的。 这里我们可以把门分为三个:遗忘门(Forget Gate)、传入门(Input Gate)、输出门(Output Gate)。

遗忘门(Forget Gate)

首先是 LSTM 要决定让那些信息继续通过这个 Cell,这是通过 Forget Gate 的 sigmoid 神经层来实现的。它的输入是ht−1和xt,输出是一个数值都在 0,1 之间的向量,表示让 Ct−1 的各部分信息通过的比重。 0 表示“不让任何信息通过”, 1 表示“让所有信息通过”。

传入门(Input Gate)

下一步是决定让多少新的信息加入到 Cell 中来,一个叫做 Input Gate 的 sigmoid 层决定哪些信息需要更新,一个 New Input 通过 tanh 生成一个向量,也就是备选的用来更新的内容,Ct~ 。在下一步,我们把这两部分联合起来,对 Cell 的状态进行一个更新。 在经过 Forget Gate 和 Input Gate 处理后,我们就可以对输入的 Ct-1 做更新了,即把Ct−1 更新为 Ct,首先我们把旧的状态 Ct−1 和 ft 相乘, 把一些不想保留的信息忘掉。然后加上 it∗Ct~,这部分信息就是我们要添加的新内容,这样就可以完成对 Ct-1 的更新。

输出门 (Output Gate)

最后我们需要来决定输出什么值,输出主要是依赖于 Cell 的状态 Ct,但是又不仅仅依赖于 Ct,而是需要经过一个过滤的处理。首先,我们还是使用一个 sigmoid 层来决定 Ct 中的哪部分信息会被输出。然后我们把 Ct 通过一个 tanh 激活函数处理,然后把其输出和 sigmoid 计算出来的权重相乘,这样就得到了最后输出的结果。 到了最后,其输出结果有三个内容,其中输出结果就是最上面的箭头代指的内容,即最终计算的结果,隐层包括两部分内容,一个是 Ct,一个是最下方的 ht,我们可以将其合并为一个变量来表示。 接下来我们来看下 LSTMCell 的 TensorFlow 代码实现。 首先它的类是 BasicLSTMCell 类,继承了 RNNCell 类,其初始化方法 init() 实现如下:

1
2
3
4
5
6
7
8
9
10
11
def __init__(self, num_units, forget_bias=1.0,
state_is_tuple=True, activation=None, reuse=None):
super(BasicLSTMCell, self).__init__(_reuse=reuse)
if not state_is_tuple:
logging.warn("%s: Using a concatenated state is slower and will soon be "
"deprecated. Use state_is_tuple=True.", self)
self._num_units = num_units
self._forget_bias = forget_bias
self._state_is_tuple = state_is_tuple
self._activation = activation or math_ops.tanh
self._linear = None

这里必须传入的参数仍然是 num_units,即神经元的个数,然后 forget_bias 是初始化 Forget Gate 的偏置大小,state_is_tuple 指的是输出状态类型是元组类型,activation 代表默认激活函数,reuse 代表是否可以被重复使用。 接下来看下 state_size() 方法和 output_size() 方法,实现如下:

1
2
3
4
5
6
7
8
@property
def state_size(self):
return (LSTMStateTuple(self._num_units, self._num_units)
if self._state_is_tuple else 2 * self._num_units)

@property
def output_size(self):
return self._num_units

这里 state_size() 方法变了,因为输出的 state 需要将 Ct 和隐含状态合并,所以它需要包含两部分的内容,如果传入的参数 state_is_tuple 为 True 的话,状态会被表示成一个元组,否则会是 num_units 乘以 2 的数字,默认是元组形式。output_size() 方法则保持不变。 对于 call() 方法,其实现如下:

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
def call(self, inputs, state):
"""Long short-term memory cell (LSTM).

Args:
inputs: `2-D` tensor with shape `[batch_size x input_size]`.
state: An `LSTMStateTuple` of state tensors, each shaped
`[batch_size x self.state_size]`, if `state_is_tuple` has been set to
`True`. Otherwise, a `Tensor` shaped
`[batch_size x 2 * self.state_size]`.

Returns:
A pair containing the new hidden state, and the new state (either a
`LSTMStateTuple` or a concatenated state, depending on
`state_is_tuple`).
"""
sigmoid = math_ops.sigmoid
# Parameters of gates are concatenated into one multiply for efficiency.
if self._state_is_tuple:
c, h = state
else:
c, h = array_ops.split(value=state, num_or_size_splits=2, axis=1)

if self._linear is None:
self._linear = _Linear([inputs, h], 4 * self._num_units, True)
# i = input_gate, j = new_input, f = forget_gate, o = output_gate
i, j, f, o = array_ops.split(
value=self._linear([inputs, h]), num_or_size_splits=4, axis=1)

new_c = (
c * sigmoid(f + self._forget_bias) + sigmoid(i) * self._activation(j))
new_h = self._activation(new_c) * sigmoid(o)

if self._state_is_tuple:
new_state = LSTMStateTuple(new_c, new_h)
else:
new_state = array_ops.concat([new_c, new_h], 1)
return new_h, new_state

首先为了获取 c, h,需要将其从 state 中分离开来,如果传入的 state 是元组的话可以直接分解,否则需要调用 split() 方法来分解:

1
2
3
4
if self._state_is_tuple:
c, h = state
else:
c, h = array_ops.split(value=state, num_or_size_splits=2, axis=1)

接下来定义了几个门的实现:

1
i, j, f, o = array_ops.split(value=self._linear([inputs, h]), num_or_size_splits=4, axis=1)

放到一起来用 Linear 计算然后分成了 4 份,分别代表 Input Gate、New Input、Forget Gate、Output Gate,用 i、j、f、o 来表示,这时候四个变量都经过了线性变换,乘以权重并做了偏置操作。 接下来就是更新 Ct-1 为 Ct 和得到隐含状态输出了,都是遵循 LSTM 内部的公式实现:

1
2
new_c = (c * sigmoid(f + self._forget_bias) + sigmoid(i) * self._activation(j))
new_h = self._activation(new_c) * sigmoid(o)

这里值得注意的是还多加了一个 _forget_bias 变量,即设置了初始化偏置,以免初始输出为 0 的问题。 最后将 new_c 和 new_h 进行合并,如果要输出元组,那么就合并为元组,否则二者进行 concat 操作,返回的结果是 new_h、new_state,前者即 Cell 的输出结果,后者代表隐含状态:

1
2
3
4
5
if self._state_is_tuple:
new_state = LSTMStateTuple(new_c, new_h)
else:
new_state = array_ops.concat([new_c, new_h], 1)
return new_h, new_state

我们再用一个实例来感受一下 BasicLSTMCell 的用法:

1
2
3
4
5
6
7
8
9
10
11
import tensorflow as tf

cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=128)
print(cell.state_size)
inputs = tf.placeholder(tf.float32, shape=(32, 100))
h0 = cell.zero_state(32, tf.float32)
output, h1 = cell(inputs=inputs, state=h0)
print(h1)
print(h1.h, h1.h.shape)
print(h1.c, h1.c.shape)
print(output, output.shape)

这里我们首先初始化了一个神经元个数为 128 的 BasicRNNCell 类,然后构造了一个 shape 为 [32, 100] 的变量作为 inputs,其代表 batch_size 为 32, 维度为 100,随后初始化了初始隐藏状态,调用了 zero_state() 方法,然后直接调用 cell,实际上是最终调用了其 call() 方法,最后得到 output 和 h1,此时 h1 是一个元组,它还可以分离成 h 和 c,分别打印其对象和维度,结果如下:

1
2
3
4
5
LSTMStateTuple(c=128, h=128)
LSTMStateTuple(c=<tf.Tensor 'add_1:0' shape=(32, 128) dtype=float32>, h=<tf.Tensor 'mul_2:0' shape=(32, 128) dtype=float32>)
Tensor("mul_2:0", shape=(32, 128), dtype=float32) (32, 128)
Tensor("add_1:0", shape=(32, 128), dtype=float32) (32, 128)
Tensor("mul_2:0", shape=(32, 128), dtype=float32) (32, 128)

可以看到其维度都是 [32, 128],而且 h1.h 和 output 是相同的。 另外 LSTM 有许多变种,其中一个比较有名的就是 Gers & Schmidhuber (2000) 提出的,它在原来的基础上行添加了 Peephole Connections,使得遗忘门可以受 Ct-1 的影响。 另外还有一个变种就是将 Forget Gate 和 Input Gate 二者联合起来,做到要么遗忘老的输入新的,要么保留老的不输入新的。 但接下来还有一个更常用的变种,俺就是 GRU,它是由 Cho, et al. (2014) 提出的,在提出的同时他还提出了 Seq2Seq 模型,为 Generation Model 做好了铺垫。

GRU

GRU,Gated Recurrent Unit,在 GRU 中,只有两个门:重置门(Reset Gate)和更新门(Update Gate)。同时在这个结构中,把 Ct 和隐藏状态进行了合并,整体结构比标准的 LSTM 结构要简单,而且这个结构后来也非常流行。 接下来我们看下 TensorFlow 中 GRUCell 的实现,代码如下:

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
class GRUCell(RNNCell):
"""Gated Recurrent Unit cell (cf. http://arxiv.org/abs/1406.1078).

Args:
num_units: int, The number of units in the GRU cell.
activation: Nonlinearity to use. Default: `tanh`.
reuse: (optional) Python boolean describing whether to reuse variables
in an existing scope. If not `True`, and the existing scope already has
the given variables, an error is raised.
kernel_initializer: (optional) The initializer to use for the weight and
projection matrices.
bias_initializer: (optional) The initializer to use for the bias.
"""

def __init__(self,
num_units,
activation=None,
reuse=None,
kernel_initializer=None,
bias_initializer=None):
super(GRUCell, self).__init__(_reuse=reuse)
self._num_units = num_units
self._activation = activation or math_ops.tanh
self._kernel_initializer = kernel_initializer
self._bias_initializer = bias_initializer
self._gate_linear = None
self._candidate_linear = None

@property
def state_size(self):
return self._num_units

@property
def output_size(self):
return self._num_units

def call(self, inputs, state):
"""Gated recurrent unit (GRU) with nunits cells."""
if self._gate_linear is None:
bias_ones = self._bias_initializer
if self._bias_initializer is None:
bias_ones = init_ops.constant_initializer(1.0, dtype=inputs.dtype)
with vs.variable_scope("gates"): # Reset gate and update gate.
self._gate_linear = _Linear(
[inputs, state],
2 * self._num_units,
True,
bias_initializer=bias_ones,
kernel_initializer=self._kernel_initializer)

value = math_ops.sigmoid(self._gate_linear([inputs, state]))
r, u = array_ops.split(value=value, num_or_size_splits=2, axis=1)

r_state = r * state
if self._candidate_linear is None:
with vs.variable_scope("candidate"):
self._candidate_linear = _Linear(
[inputs, r_state],
self._num_units,
True,
bias_initializer=self._bias_initializer,
kernel_initializer=self._kernel_initializer)
c = self._activation(self._candidate_linear([inputs, r_state]))
new_h = u * state + (1 - u) * c
return new_h, new_h

在 state_size()、output_size() 方法里,其返回的内容都是 num_units,即神经元的个数。 接下来 call() 方法中,因为 Reset Gate rt 和 Update Gate zt 分别用变量 r、u 表示,它们需要先对 ht-1 即 state 和 xt 做合并,然后再实现线性变换,再调用 sigmod 函数得到:

1
2
value = math_ops.sigmoid(self._gate_linear([inputs, state]))
r, u = array_ops.split(value=value, num_or_size_splits=2, axis=1)

然后需要求解 ht~,首先用 rt 和 ht-1 即 state 相乘:

1
r_state = r * state

然后将其放到线性函数里面,在调用 tanh 激活函数即可:

1
c = self._activation(self._candidate_linear([inputs, r_state]))

最后计算隐含状态和输出结果,二者一致:

1
2
new_h = u * state + (1 - u) * c
return new_h, new_h

这样即可返回得到输出结果和隐藏状态。 我们用一个实例感受一下:

1
2
3
4
5
6
7
8
9
import tensorflow as tf

cell = tf.nn.rnn_cell.GRUCell(num_units=128)
print(cell.state_size)
inputs = tf.placeholder(tf.float32, shape=[32, 100])
h0 = cell.zero_state(32, tf.float32)
output, h1 = cell(inputs=inputs, state=h0)
print(output, output.shape)
print(h1, h1.shape)

运行结果:

1
2
3
128
Tensor("gru_cell/add:0", shape=(32, 128), dtype=float32) (32, 128)
Tensor("gru_cell/add:0", shape=(32, 128), dtype=float32) (32, 128)

这个结果和 BasicRNNCell 并无二致,但 GRUCell 内部的结构使模型的效果更加优化,一般我们也会选取 GRUCell 来代替原生的 BasicRNNCell。

结语

以上便是对 RNN 及一些变种的说明及代码原理分析和实例用法,此部分掌握之后对 Dynamic RNN、多层 RNN 及 RNN Cell 的改写会有很大帮助,需要好好掌握。

Python

上一节使用了最简单的网络来处理了 MNIST 数据集,但只有 92% 的正确率,接下来我们使用卷积神经网络来实现更高的正确率。

权重初始化

在上一节初始化 w 和 b 的时候,我们使用了置零初始化。但在卷积神经网络中,我们需要在初始化的时候权重加入少量噪声来打破对称性和避免零梯度,偏置项直接使用一个较小的正数来避免节点输出恒为零的问题。 所以权重我们可以使用截尾正态分布函数 truncated_normal() 来生成初始化张量,我们可以给它指定均值或标准差,均值默认是 0, 标准差默认是 1,例如我们生成一个 [10] 的张量,代码如下:

1
2
3
4
import tensorflow as tf
initial = tf.truncated_normal([10], stddev=0.1)
with tf.Session() as sess:
print(sess.run(initial))

结果如下:

1
2
[-0.13058113  0.03201858 -0.19349943 -0.06061752 -0.10267895 -0.11079147
0.1881365 -0.01057311 -0.02797078 0.01180232]

另外 constant() 方法是用于生成常量的方法,例如生成一个 [10] 的常量张量,代码如下:

1
2
3
4
import tensorflow as tf
initial = tf.constant(0.2, shape=[10])
with tf.Session() as sess:
print(sess.run(initial))

结果如下:

1
[ 0.2  0.2  0.2  0.2  0.2  0.2  0.2  0.2  0.2  0.2]

所以这里我们可以将这两个方法封装成一个函数来尝试:

1
2
3
4
5
6
7
def weight(shape, stddev=0.1, mean=0):
initial = tf.truncated_normal(shape=shape, mean=mean, stddev=stddev)
return tf.Variable(initial)

def bias(shape, value):
initial = tf.constant(value=value, shape=shape)
return tf.Variable(initial)

卷积

这次我们需要使用卷积神经网络来处理图片,所以这里的核心部分就是卷积和池化了,首先我们来了解一下卷积和池化。 卷积常用的方法为 conv2d() ,它的 API 如下:

1
tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, name=None)

这个方法是 TensorFlow 实现卷积常用的方法,也是搭建卷积神经网络的核心方法,参数介绍如下:

  • input,指需要做卷积的输入图像,它要求是一个 Tensor,具有 [batch_size, in_height, in_width, in_channels] 这样的 shape,具体含义是 [batch_size 的图片数量, 图片高度, 图片宽度, 输入图像通道数],注意这是一个 4 维的 Tensor,要求类型为 float32 和 float64 其中之一。
  • filter,相当于 CNN 中的卷积核,它要求是一个 Tensor,具有 [filter_height, filter_width, in_channels, out_channels] 这样的shape,具体含义是 [卷积核的高度,卷积核的宽度,输入图像通道数,输出通道数(即卷积核个数)],要求类型与参数 input 相同,有一个地方需要注意,第三维 in_channels,就是参数 input 的第四维。
  • strides,卷积时在图像每一维的步长,这是一个一维的向量,长度 4,具有 [stride_batch_size, stride_in_height, stride_in_width, stride_in_channels] 这样的 shape,第一个元素代表在一个样本的特征图上移动,第二三个元素代表在特征图上的高、宽上移动,第四个元素代表在通道上移动。
  • padding,string 类型的量,只能是 SAME、VALID 其中之一,这个值决定了不同的卷积方式。
  • use_cudnn_on_gpu,布尔类型,是否使用 cudnn 加速,默认为true。

返回的结果是 [batch_size, out_height, out_width, out_channels] 维度的结果。 我们这里拿一张 3x3 的图片,单通道(通道为1)的图片,拿一个 1x1 的卷积核进行卷积:

1
2
3
4
input = tf.Variable(tf.random_normal([1, 3, 3, 1]))
filter = tf.Variable(tf.random_normal([1, 1, 1, 1]))
op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')
print(op.shape)

结果如下:

1
(1, 3, 3, 1)

很清晰,一张图片,拿一个 1x1 的核去做卷积,得到的结果输出是 3x3 的,输出通道为 1,batch_size 照旧。 再将卷积核扩大,用一个 3x3 的卷积核:

1
2
3
4
input = tf.Variable(tf.random_normal([1, 3, 3, 1]))
filter = tf.Variable(tf.random_normal([3, 3, 1, 1]))
op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')
print(op.shape)

结果如下:

1
(1, 1, 1, 1)

最后输出的是一个 1x1 的值。 将图片扩大为 7x7,卷积核仍然使用 3x3:

1
2
3
4
input = tf.Variable(tf.random_normal([1, 7, 7, 1]))
filter = tf.Variable(tf.random_normal([3, 3, 1, 1]))
op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')
print(op.shape)

结果如下:

1
(1, 5, 5, 1)

最后输出的是一个 5x5 的值。 这时如果我们把 padding 模式改为 SAME,表示卷积核可以停留在图像边缘:

1
2
3
4
input = tf.Variable(tf.random_normal([1, 7, 7, 1]))
filter = tf.Variable(tf.random_normal([3, 3, 1, 1]))
op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME')
print(op.shape)

结果如下:

1
(1, 7, 7, 1)

则输出的内容和原图像是相同的。 这时如果更改 batch_size 和 out_channels,比如 batch_size 修改为 3,out_channels 修改为 6:

1
2
3
4
input = tf.Variable(tf.random_normal([3, 7, 7, 1]))
filter = tf.Variable(tf.random_normal([3, 3, 1, 6]))
op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME')
print(op.shape)

结果如下:

1
(3, 7, 7, 6)

输出结果的 batch_size 和 out_channels 会随之变化。 当 strides 的步长不为 1 的时候,我们将 stride_in_height 和 stride_in_width 修改为 2,相当于每次移动两步:

1
2
3
4
input = tf.Variable(tf.random_normal([3, 7, 7, 1]))
filter = tf.Variable(tf.random_normal([3, 3, 1, 6]))
op = tf.nn.conv2d(input, filter, strides=[1, 2, 2, 1], padding='VALID')
print(op.shape)

结果如下:

1
(3, 3, 3, 6)

最后我们用一个例子来感受一下:

1
2
3
4
5
6
7
8
9
import tensorflow as tf

input = tf.Variable(tf.random_normal([2, 4, 4, 5]))
filter = tf.Variable(tf.random_normal([2, 2, 5, 2]))
op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')
sess = tf.InteractiveSession()
tf.global_variables_initializer().run()
print(op.shape)
print(sess.run(op))

这里 input、filter 通过指定 shape 的方式调用 random_normal() 方法进行随机初始化,input 的维度为 [2, 4, 4, 5],即 batch_size 为 2,图片是 4x4,输入通道数为 5,卷积核大小为 2x2,输入通道 5,输出通道 2,步长为 1,padding 方式选用 VALID,最后输出得到输出的 shape 和结果。 结果如下:

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
(2, 3, 3, 2)
[[[[ 2.05039382 -8.82934952]
[ -9.77668381 3.63882256]
[ -4.46390772 -5.91670704]]

[[ 8.41201782 -6.72245312]
[ -1.47592044 13.03628349]
[ 5.44015312 2.46059227]]

[[ -3.18967772 1.24733043]
[-10.1108532 -6.44734669]
[ 1.99426246 2.91549349]]]


[[[ -1.66685319 0.32011557]
[ -5.66163826 -0.37670898]
[ -0.74658942 1.31723833]]

[[ -5.85412216 -0.29930949]
[ -0.75974303 -1.84006214]
[ -2.05475235 4.9572196 ]]

[[ -4.09344864 1.39405775]
[ -1.28887582 -2.82365012]
[ 4.87360907 10.8071022 ]]]]

可以看到 input 维度为 [2, 4, 4, 5],filter 维度为 [2, 2, 5, 2] 时,生成的结果维度为 [2, 3, 3, 2]。

池化

池化层往往在卷积层后面,通过池化来降低卷积层输出的特征向量,同时改善结果。 在这里介绍一个常用的最大值池化 max_pool() 方法,其 API 如下:

1
tf.nn.max_pool(value, ksize, strides, padding, name=None)

是CNN当中的最大值池化操作,其实用法和卷积很类似。 参数介绍如下:

  • value,需要池化的输入,一般池化层接在卷积层后面,所以输入通常是 feature map,依然是 [batch_size, height, width, channels] 这样的shape。
  • ksize,池化窗口的大小,取一个四维向量,一般是 [batch_size, height, width, channels],因为我们不想在 batch 和 channels 上做池化,所以这两个维度设为了1。
  • strides,和卷积类似,窗口在每一个维度上滑动的步长,一般也是 [stride_batch_size, stride_height, stride_width, stride_channels]。
  • padding,和卷积类似,可以取 VALID、SAME,返回一个 Tensor,类型不变,shape 仍然是 [batch_size, height, width, channels] 这种形式。

在这里输入为 [3, 7, 7, 2],池化窗口设置为 [1, 2, 2, 1],步长为 [1, 1, 1, 1],padding 模式设置为 VALID。

1
2
3
input = tf.Variable(tf.random_normal([3, 7, 7, 2]))
op = tf.nn.max_pool(input, ksize=[1, 2, 2, 1], strides=[1, 1, 1, 1], padding='VALID')
print(op.shape)

结果如下:

1
(3, 6, 6, 2)

类似的原理,我们可以得到这样的的结果。

卷积和池化

所以了解了以上卷积和池化方法的用法,我们可以定义如下两个工具方法:

1
2
3
4
5
def conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME'):
return tf.nn.conv2d(input, filter, strides=strides, padding=padding)

def max_pool(input, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME'):
return tf.nn.max_pool(input, ksize=ksize, strides=strides, padding=padding)

这两个方法分别实现了卷积和池化,并设置了默认步长和核大小。 接下来就让我们开始神经网络的构建吧。

初始化

首先我们需要初始化一些数据,包括输入的 x 和对一个的标注 y_label:

1
2
x = tf.placeholder(tf.float32, shape=[None, 784])
y_label = tf.placeholder(tf.float32, shape=[None, 10])

第一层卷积

现在我们可以开始实现第一层了。它由一个卷积接一个 max pooling 完成。卷积在每个 5x5 的 patch 中算出 32 个特征。卷积的权重张量形状是 [5, 5, 1, 32],前两个维度是 patch 的大小,接着是输入的通道数目,最后是输出的通道数目,而对于每一个输出通道都有一个对应的偏置量,我们首先初始化 w 和 b

1
2
w_conv1 = weight([5, 5, 1, 32])
b_conv1 = bias([32])

为了用这一层,我们把 x 变成一个四维向量,其第 2、3 维对应图片的宽、高,最后一维代表图片的颜色通道数,因为是灰度图所以这里的通道数为 1,如果是彩色图,则为 3。 随后我们需要对图片做 reshape 操作,将其

1
x_reshape = tf.reshape(x, [-1, 28, 28, 1])

我们把 x_reshape 和权值向量进行卷积,加上偏置项,然后应用 ReLU 激活函数,最后进行 max pooling:

1
2
h_conv1 = tf.nn.relu(conv2d(x_reshape, w_conv1) + b_conv1)
h_pool1 = max_pool(h_conv1)

第二层卷积

现在我们已经实现了一层卷积,为了构建一个更深的网络,我们再继续增加一层卷积,将通道数变成 64,所以这里的初始化权重和偏置为:

1
2
w_conv2 = weight([5, 5, 32, 64])
b_conv2 = bias([64])

随后我们把上一层池化结果 h_pool1 和权值向量进行卷积,加上偏置项,然后应用 ReLU 激活函数,最后进行 max pooling:

1
2
h_conv2 = tf.nn.relu(conv2d(h_pool1, w_conv2) + b_conv2)
h_pool2 = max_pool(h_conv2)

密集连接层

现在,图片尺寸减小到7x7,我们再加入一个有 1024 个神经元的全连接层,用于处理整个图片。我们把池化层输出的张量 reshape 成一些向量,乘上权重矩阵,加上偏置,然后对其使用 ReLU。

1
2
3
4
w_fc1 = weight([7 * 7 * 64, 1024])
b_fc1 = bias([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, w_fc1) + b_fc1)

Dropout

为了减少过拟合,我们在输出层之前加入 dropout。我们用一个 placeholder 来代表一个神经元的输出在 dropout 中保持不变的概率。这样我们可以在训练过程中启用 dropout,在测试过程中关闭 dropout。 TensorFlow 的 tf.nn.dropout 操作除了可以屏蔽神经元的输出外,还会自动处理神经元输出值的 scale,所以用 dropout 的时候可以不用考虑 scale。

1
2
keep_prob = tf.placeholder(tf.float32)
h_fc1_dropout = tf.nn.dropout(h_fc1, keep_prob=keep_prob)

输出层

最后,我们添加一个 Softmax 输出层,这里我们需要将 1024 维转为 10 维,所以需要声明一个 [1024, 10] 的权重和 [10] 的偏置:

1
2
3
w_fc2 = weight([1024, 10])
b_fc1 = bias([10])
y = tf.nn.softmax(tf.matmul(h_fc1_dropout, w_fc2) + b_fc1)

训练和评估模型

为了进行训练和评估,我们使用与之前简单的单层 Softmax 神经网络模型几乎相同的一套代码,只是我们会用更加复杂的 Adam 优化器来做梯度最速下降,在 feed_dict 中加入额外的参数 keep_prob 来控制 dropout 比例,然后每 100次 迭代输出一次日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Loss
cross_entropy = -tf.reduce_sum(y_label * tf.log(y))
train = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

# Prediction
correct_prediction = tf.equal(tf.argmax(y_label, axis=1), tf.argmax(y, axis=1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# Train
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for step in range(total_steps + 1):
batch = mnist.train.next_batch(batch_size)
sess.run(train, feed_dict={x: batch[0], y_label: batch[1], keep_prob: dropout_keep_prob})
# Train accuracy
if step % steps_per_test == 0:
print('Training Accuracy', step,
sess.run(accuracy, feed_dict={x: batch[0], y_label: batch[1], keep_prob: 1}))

# Final Test
print('Test Accuracy', sess.run(accuracy, feed_dict={x: mnist.test.images, y_label: mnist.test.labels, keep_prob: 1}))

运行

以上代码,在最终测试集上的准确率大概是99.2%。 运行结果:

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
Training Accuracy 0 0.05
Training Accuracy 100 0.7
Training Accuracy 200 0.85
Training Accuracy 300 0.9
Training Accuracy 400 0.93
Training Accuracy 500 0.91
Training Accuracy 600 0.94
Training Accuracy 700 0.95
Training Accuracy 800 0.95
Training Accuracy 900 0.95
Training Accuracy 1000 0.97
Training Accuracy 1100 0.95
Training Accuracy 1200 0.96
Training Accuracy 1300 0.99
Training Accuracy 1400 0.98
Training Accuracy 1500 0.95
Training Accuracy 1600 0.97
Training Accuracy 1700 1.0
Training Accuracy 1800 0.95
Training Accuracy 1900 0.95
Training Accuracy 2000 0.95
Training Accuracy 2100 0.96
Training Accuracy 2200 0.96
Training Accuracy 2300 0.98
Training Accuracy 2400 0.97
Training Accuracy 2500 0.96
Training Accuracy 2600 0.99
Training Accuracy 2700 0.96
Training Accuracy 2800 0.98
Training Accuracy 2900 0.95
Training Accuracy 3000 0.99

结语

本节我们实现了卷积神经网络来处理图像相关问题,将准确率大大提高,可见神经网络是非常强大的。

本节代码

本节代码地址为:https://github.com/AIDeepLearning/MNIST

Python

我们本节要用 MNIST 数据集训练一个可以识别数据的深度学习模型来帮助识别手写数字。

MNIST

MNIST 是一个入门级计算机视觉数据集,包含了很多手写数字图片,如图所示: 数据集中包含了图片和对应的标注,在 TensorFlow 中提供了这个数据集,我们可以用如下方法进行导入:

1
2
3
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data/', one_hot=True)
print(mnist)

输出结果如下:

1
2
3
4
5
Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gz
Datasets(train=<tensorflow.contrib.learn.python.learn.datasets.mnist.DataSet object at 0x101707ef0>, validation=<tensorflow.contrib.learn.python.learn.datasets.mnist.DataSet object at 0x1016ae4a8>, test=<tensorflow.contrib.learn.python.learn.datasets.mnist.DataSet object at 0x1016f9358>)

在这里程序会首先下载 MNIST 数据集,然后解压并保存到刚刚制定好的 MNIST_data 文件夹中,然后输出数据集对象。 数据集中包含了 55000 行的训练数据集(mnist.train)、5000 行验证集(mnist.validation)和 10000 行的测试数据集(mnist.test),文件如下所示: 正如前面提到的一样,每一个 MNIST 数据单元有两部分组成:一张包含手写数字的图片和一个对应的标签。我们把这些图片设为 xs,把这些标签设为 ys。训练数据集和测试数据集都包含 xs 和 ys,比如训练数据集的图片是 mnist.train.images ,训练数据集的标签是 mnist.train.labels,每张图片是 28 x 28 像素,即 784 个像素点,我们可以把它展开形成一个向量,即长度为 784 的向量。 所以训练集我们可以转化为 [55000, 784] 的向量,第一维就是训练集中包含的图片个数,第二维是图片的像素点表示的向量。

Softmax

Softmax 可以看成是一个激励(activation)函数或者链接(link)函数,把我们定义的线性函数的输出转换成我们想要的格式,也就是关于 10 个数字类的概率分布。因此,给定一张图片,它对于每一个数字的吻合度可以被 Softmax 函数转换成为一个概率值。Softmax 函数可以定义为: 展开等式右边的子式,可以得到: 比如判断一张图片中的动物是什么,可能的结果有三种,猫、狗、鸡,假如我们可以经过计算得出它们分别的得分为 3.2、5.1、-1.7,Softmax 的过程首先会对各个值进行次幂计算,分别为 24.5、164.0、0.18,然后计算各个次幂结果占总次幂结果的比重,这样就可以得到 0.13、0.87、0.00 这三个数值,所以这样我们就可以实现差别的放缩,即好的更好、差的更差。 如果要进一步求损失值可以进一步求对数然后取负值,这样 Softmax 后的值如果值越接近 1,那么得到的值越小,即损失越小,如果越远离 1,那么得到的值越大。

实现回归模型

首先导入 TensorFlow,命令如下:

1
import tensorflow as tf

接下来我们指定一个输入,在这里输入即为样本数据,如果是训练集那么则是 55000 x 784 的矩阵,如果是验证集则为 5000 x 784 的矩阵,如果是测试集则是 10000 x 784 的矩阵,所以它的行数是不确定的,但是列数是确定的。 所以可以先声明一个 placeholder 对象:

1
x = tf.placeholder(tf.float32, [None, 784])

这里第一个参数指定了矩阵中每个数据的类型,第二个参数指定了数据的维度。 接下来我们需要构建第一层网络,表达式如下: 这里实际上是对输入的 x 乘以 w 权重,然后加上一个偏置项作为输出,而这两个变量实际是在训练的过程中动态调优的,所以我们需要指定它们的类型为 Variable,代码如下:

1
2
w = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))

接下来需要实现的就是上图所述的公式了,我们再进一步调用 Softmax 进行计算,得到 y:

1
y = tf.nn.softmax(tf.matmul(x, w) + b)

通过上面几行代码我们就已经把模型构建完毕了,结构非常简单。

损失函数

为了训练我们的模型,我们首先需要定义一个指标来评估这个模型是好的。其实,在机器学习,我们通常定义指标来表示一个模型是坏的,这个指标称为成本(cost)或损失(loss),然后尽量最小化这个指标。但是这两种方式是相同的。 一个非常常见的,非常漂亮的成本函数是“交叉熵”(cross-entropy)。交叉熵产生于信息论里面的信息压缩编码技术,但是它后来演变成为从博弈论到机器学习等其他领域里的重要技术手段。它的定义如下: y 是我们预测的概率分布, y_label 是实际的分布,比较粗糙的理解是,交叉熵是用来衡量我们的预测用于描述真相的低效性。 我们可以首先定义 y_label,它的表达式是:

1
y_label = tf.placeholder(tf.float32, [None, 10])

接下来我们需要计算它们的交叉熵,代码如下:

1
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_label * tf.log(y), reduction_indices=[1]))

首先用 reduce_sum() 方法针对每一个维度进行求和,reduction_indices 是指定沿哪些维度进行求和。 然后调用 reduce_mean() 则求平均值,将一个向量中的所有元素求算平均值。 这样我们最后只需要优化这个交叉熵就好了。 所以这样我们再定义一个优化方法:

1
train = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

这里使用了 GradientDescentOptimizer,在这里,我们要求 TensorFlow 用梯度下降算法(gradient descent algorithm)以 0.5 的学习速率最小化交叉熵。梯度下降算法(gradient descent algorithm)是一个简单的学习过程,TensorFlow 只需将每个变量一点点地往使成本不断降低的方向移动即可。

运行模型

定义好了以上内容之后,相当于我们已经构建好了一个计算图,即设置好了模型,我们把它放到 Session 里面运行即可:

1
2
3
4
5
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for step in range(total_steps + 1):
batch_x, batch_y = mnist.train.next_batch(batch_size)
sess.run(train, feed_dict={x: batch_x, y_label: batch_y})

该循环的每个步骤中,我们都会随机抓取训练数据中的 batch_size 个批处理数据点,然后我们用这些数据点作为参数替换之前的占位符来运行 train。 这里需要一些变量的定义:

1
2
batch_size = 100
total_steps = 5000

测试模型

那么我们的模型性能如何呢? 首先让我们找出那些预测正确的标签。tf.argmax() 是一个非常有用的函数,它能给出某个 Tensor 对象在某一维上的其数据最大值所在的索引值。由于标签向量是由 0,1 组成,因此最大值 1 所在的索引位置就是类别标签,比如 tf.argmax(y, 1) 返回的是模型对于任一输入 x 预测到的标签值,而 tf.argmax(y_label, 1) 代表正确的标签,我们可以用 tf.equal() 方法来检测我们的预测是否真实标签匹配(索引位置一样表示匹配)。

1
correct_prediction = tf.equal(tf.argmax(y, axis=1), tf.argmax(y_label, axis=1))

这行代码会给我们一组布尔值。为了确定正确预测项的比例,我们可以把布尔值转换成浮点数,然后取平均值。例如,[True, False, True, True] 会变成 [1, 0, 1, 1] ,取平均值后得到 0.75。

1
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

最后,我们计算所学习到的模型在测试数据集上面的正确率,定义如下:

1
2
3
steps_per_test = 100
if step % steps_per_test == 0:
print(step, sess.run(accuracy, feed_dict={x: mnist.test.images, y_label: mnist.test.labels}))

这个最终结果值应该大约是92%。 这样我们就通过完成了训练和测试阶段,实现了一个基本的训练模型,后面我们会继续优化模型来达到更好的效果。 运行结果如下:

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
0 0.453
100 0.8915
200 0.9026
300 0.9081
400 0.9109
500 0.9108
600 0.9175
700 0.9137
800 0.9158
900 0.9176
1000 0.9167
1100 0.9186
1200 0.9206
1300 0.9161
1400 0.9218
1500 0.9179
1600 0.916
1700 0.9196
1800 0.9222
1900 0.921
2000 0.9223
2100 0.9214
2200 0.9191
2300 0.9228
2400 0.9228
2500 0.9218
2600 0.9197
2700 0.9225
2800 0.9238
2900 0.9219
3000 0.9224
3100 0.9184
3200 0.9253
3300 0.9216
3400 0.9218
3500 0.9212
3600 0.9225
3700 0.9224
3800 0.9225
3900 0.9226
4000 0.9201
4100 0.9138
4200 0.9184
4300 0.9222
4400 0.92
4500 0.924
4600 0.9234
4700 0.9219
4800 0.923
4900 0.9254
5000 0.9218

结语

本节通过一个 MNIST 数据集来简单体验了一下真实数据的训练和预测过程,但是准确率还不够高,后面我们会学习用卷积的方式来进行模型训练,准确率会更高。

本节代码

本节代码地址为:https://github.com/AIDeepLearning/MNIST

Python

本篇内容基于 Python3 TensorFlow 1.4 版本。

本节内容

本节通过最简单的示例 —— 平面拟合来说明 TensorFlow 的基本用法。

构造数据

TensorFlow 的引入方式是:

1
import tensorflow as tf

接下来我们构造一些随机的三维数据,然后用 TensorFlow 找到平面去拟合它,首先我们用 Numpy 生成随机三维点,其中变量 x 代表三维点的 (x, y) 坐标,是一个 2x100 的矩阵,即 100 个 (x, y),然后变量 y 代表三位点的 z 坐标,我们用 Numpy 来生成这些随机的点:

1
2
3
4
5
6
import numpy as np
x_data = np.float32(np.random.rand(2, 100))
y_data = np.dot([0.300, 0.200], x_data) + 0.400

print(x_data)
print(y_data)

这里利用 Numpy 的 random 模块的 rand() 方法生成了 2x100 的随机矩阵,这样就生成了 100 个 (x, y) 坐标,然后用了一个 dot() 方法算了矩阵乘法,用了一个长度为 2 的向量跟此矩阵相乘,得到一个长度为 100 的向量,然后再加上一个常量,得到 z 坐标,输出结果样例如下:

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
[[ 0.97232962  0.08897641  0.54844421  0.5877986   0.5121088   0.64716059
0.22353953 0.18406206 0.16782761 0.97569454 0.65686035 0.75569868
0.35698661 0.43332314 0.41185728 0.24801297 0.50098598 0.12025958
0.40650111 0.51486945 0.19292323 0.03679928 0.56501174 0.5321334
0.71044683 0.00318134 0.76611853 0.42602748 0.33002195 0.04414672
0.73208278 0.62182301 0.49471655 0.8116194 0.86148429 0.48835048
0.69902027 0.14901569 0.18737803 0.66826463 0.43462989 0.35768151
0.79315376 0.0400687 0.76952982 0.12236254 0.61519378 0.92795062
0.84952474 0.16663995 0.13729768 0.50603199 0.38752931 0.39529857
0.29228279 0.09773371 0.43220878 0.2603009 0.14576958 0.21881725
0.64888018 0.41048348 0.27641159 0.61700606 0.49728736 0.75936913
0.04028837 0.88986284 0.84112513 0.34227493 0.69162005 0.89058989
0.39744586 0.85080278 0.37685293 0.80529863 0.31220895 0.50500977
0.95800418 0.43696108 0.04143282 0.05169986 0.33503434 0.1671818
0.10234453 0.31241918 0.23630807 0.37890589 0.63020509 0.78184551
0.87924582 0.99288088 0.30762389 0.43499199 0.53140771 0.43461791
0.23833922 0.08681628 0.74615192 0.25835371]
[ 0.8174957 0.26717573 0.23811154 0.02851068 0.9627012 0.36802396
0.50543582 0.29964805 0.44869211 0.23191817 0.77344608 0.36636299
0.56170034 0.37465382 0.00471885 0.19509546 0.49715847 0.15201907
0.5642485 0.70218688 0.6031307 0.4705168 0.98698962 0.865367
0.36558965 0.72073907 0.83386165 0.29963031 0.72276717 0.98171854
0.30932376 0.52615297 0.35522953 0.13186514 0.73437029 0.03887378
0.1208882 0.67004597 0.83422536 0.17487818 0.71460873 0.51926661
0.55297899 0.78169805 0.77547258 0.92139858 0.25020468 0.70916855
0.68722379 0.75378138 0.30182058 0.91982585 0.93160367 0.81539184
0.87977934 0.07394848 0.1004181 0.48765802 0.73601437 0.59894943
0.34601998 0.69065076 0.6768015 0.98533565 0.83803362 0.47194552
0.84103006 0.84892255 0.04474261 0.02038293 0.50802571 0.15178065
0.86116213 0.51097614 0.44155359 0.67713588 0.66439205 0.67885226
0.4243969 0.35731083 0.07878648 0.53950399 0.84162414 0.24412845
0.61285144 0.00316137 0.67407191 0.83218956 0.94473189 0.09813353
0.16728765 0.95433819 0.1416636 0.4220584 0.35413414 0.55999744
0.94829601 0.62568033 0.89808714 0.07021013]]
[ 0.85519803 0.48012807 0.61215557 0.58204171 0.74617288 0.66775297
0.56814902 0.51514823 0.5400867 0.739092 0.75174732 0.6999822
0.61943605 0.60492771 0.52450095 0.51342299 0.64972749 0.46648169
0.63480003 0.69489821 0.57850311 0.50514314 0.76690145 0.73271342
0.68625198 0.54510222 0.79660789 0.58773431 0.64356002 0.60958773
0.68148959 0.6917775 0.61946087 0.66985885 0.80531934 0.5542799
0.63388372 0.5787139 0.62305848 0.63545502 0.67331071 0.61115777
0.74854193 0.56836022 0.78595346 0.62098848 0.63459907 0.8202189
0.79230218 0.60074826 0.50155342 0.73577477 0.70257953 0.68166794
0.6636407 0.44410981 0.54974625 0.57562188 0.59093375 0.58543506
0.66386805 0.6612752 0.61828378 0.78216895 0.71679293 0.72219985
0.58029252 0.83674336 0.66128606 0.50675907 0.70909116 0.6975331
0.69146618 0.75743606 0.6013666 0.77701676 0.6265411 0.68727338
0.77228063 0.60255049 0.42818714 0.52341076 0.66883513 0.49898023
0.55327365 0.49435803 0.6057068 0.68010968 0.77800791 0.65418036
0.69723127 0.8887319 0.52061989 0.61490928 0.63024914 0.64238486
0.66116097 0.55118095 0.80346301 0.49154814]

这样我们就得到了一些三维的点。

构造模型

随后我们用 TensorFlow 来根据这些数据拟合一个平面,拟合的过程实际上就是寻找 (x, y) 和 z 的关系,即变量 x_data 和变量 y_data 的关系,而它们之间的关系刚才我们用了线性变换表示出来了,即 z = w * (x, y) + b,所以拟合的过程实际上就是找 w 和 b 的过程,所以这里我们就首先像设变量一样来设两个变量 w 和 b,代码如下:

1
2
3
4
5
x = tf.placeholder(tf.float32, [2, 100])
y_label = tf.placeholder(tf.float32, [100])
b = tf.Variable(tf.zeros([1]))
w = tf.Variable(tf.random_uniform([2], -1.0, 1.0))
y = tf.matmul(tf.reshape(w, [1, 2]), x) + b

在创建模型的时候,我们首先可以将现有的变量来表示出来,用 placeholder() 方法声明即可,一会我们在运行的时候传递给它真实的数据就好,第一个参数是数据类型,第二个参数是形状,因为 x_data 是 2x100 的矩阵,所以这里形状定义为 [2, 100],而 y_data 是长度为 100 的向量,所以这里形状定义为 [100],当然此处使用元组定义也可以,不过要写成 (100, )。 随后我们用 Variable 初始化了 TensorFlow 中的变量,b 初始化为一个常量,w 是一个随机初始化的 1x2 的向量,范围在 -1 和 1 之间,然后 y 再用 w、x、b 表示出来,其中 matmul() 方法就是 TensorFlow 中提供的矩阵乘法,类似 Numpy 的 dot() 方法。不过不同的是 matmul() 不支持向量和矩阵相乘,即不能 BroadCast,所以在这里做乘法前需要先调用 reshape() 一下转成 1x2 的标准矩阵,最后将结果表示为 y。 这样我们就构造出来了一个线性模型。 这里的 y 是我们模型中输出的值,而真实的数据却是我们输入的 y_data,即 y_label。

损失函数

要拟合这个平面的话,我们需要减小 y_label 和 y 的差距就好了,这个差距越小越好。 所以接下来我们可以定义一个损失函数,来代表模型实际输出值和真实值之间的差距,我们的目的就是来减小这个损失,代码实现如下:

1
loss = tf.reduce_mean(tf.square(y - y_label))

这里调用了 square() 方法,传入 y_label 和 y 的差来求得平方和,然后使用 reduce_mean() 方法得到这个值的平均值,这就是现在模型的损失值,我们的目的就是减小这个损失值,所以接下来我们使用梯度下降的方法来减小这个损失值即可,定义如下代码:

1
2
optimizer = tf.train.GradientDescentOptimizer(0.5)
train = optimizer.minimize(loss)

这里定义了 GradientDescentOptimizer 优化,即使用梯度下降的方法来减小这个损失值,我们训练模型就是来模拟这个过程。

运行模型

最后我们将模型运行起来即可,运行时必须声明一个 Session 对象,然后初始化所有的变量,然后执行一步步的训练即可,实现如下:

1
2
3
4
5
6
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for step in range(201):
sess.run(train, feed_dict={x: x_data, y: y_data})
if step % 10 == 0:
print(step, sess.run(w), sess.run(b))

这里定义了 200 次循环,每一次循环都会执行一次梯度下降优化,每次循环都调用一次 run() 方法,传入的变量就是刚才定义个 train 对象,feed_dict 就把 placeholder 类型的变量赋值即可。随着训练的进行,损失会越来越小,w 和 b 也会被慢慢调整为拟合的值。 在这里每 10 次 循环我们都打印输出一下拟合的 w 和 b 的值,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0 [ 0.31494665  0.33602586] [ 0.84270978]
10 [ 0.19601417 0.17301694] [ 0.47917289]
20 [ 0.23550016 0.18053198] [ 0.44838765]
30 [ 0.26029009 0.18700737] [ 0.43032286]
40 [ 0.27547371 0.19152154] [ 0.41897511]
50 [ 0.28481475 0.19454622] [ 0.41185945]
60 [ 0.29058149 0.19652548] [ 0.40740564]
70 [ 0.2941508 0.19780098] [ 0.40462157]
80 [ 0.29636407 0.1986146 ] [ 0.40288284]
90 [ 0.29773837 0.19913 ] [ 0.40179768]
100 [ 0.29859257 0.19945487] [ 0.40112072]
110 [ 0.29912385 0.199659 ] [ 0.40069857]
120 [ 0.29945445 0.19978693] [ 0.40043539]
130 [ 0.29966027 0.19986697] [ 0.40027133]
140 [ 0.29978839 0.19991697] [ 0.40016907]
150 [ 0.29986817 0.19994824] [ 0.40010536]
160 [ 0.29991791 0.1999677 ] [ 0.40006563]
170 [ 0.29994887 0.19997987] [ 0.40004089]
180 [ 0.29996812 0.19998746] [ 0.40002549]
190 [ 0.29998016 0.19999218] [ 0.40001586]
200 [ 0.29998764 0.19999513] [ 0.40000987]

可以看到,随着训练的进行,w 和 b 也慢慢接近真实的值,拟合越来越精确,接近正确的值。

结语

以上便是通过一个最简单的平面拟合的案例来说明了一下 TensorFlow 的用法,是不是很简单?

代码

本节代码地址:https://github.com/AIDeepLearning/TensorFlowBasis

Linux

部署公司生产环境的 Splash 集群无奈节点太多 差点被搞死·· 还好我有运维神器 Ansible,一次编撰终生可用啊!而且这玩意儿 等幂特性 扩容回滚 So Easy!! 闲话少说开搞!

安装 Ansible:

看官方文档去:http://www.ansible.com.cn/index.html 好像这个主控端不支持 Windows? 大家虚拟机装个 Ubuntu 吧。

闲话少扯直接上干货:

整体目录如下:

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
study@study:~/文档/ansible-examples$ tree Splash_Load_balancing_cluster
Splash_Load_balancing_cluster
├── group_vars
│   └── all
├── roles
│   ├── common
│   │   ├── files
│   │   │   ├── CentOS-Base.repo
│   │   │   ├── docker-ce.repo
│   │   │   ├── epel.repo
│   │   │   ├── ntp.conf
│   │   │   └── RPM-GPG-KEY-EPEL-7
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   ├── docker
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │   └── daemon.json.j2
│   ├── haproxy
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │   └── haproxy.cfg.j2
│   └── splash
│   ├── files
│   │   ├── filters
│   │   │   └── default.txt
│   │   ├── js-profiles
│   │   ├── lua_modules
│   │   └── proxy-profiles
│   │   └── proxy.ini
│   └── tasks
│   └── main.yml
├── site.retry
└── site.yml

Group_vars: 里面定义全局使用的变量 Roles: 存放所有的规则目录 Roles/common :所有服务器初始化配置部署 Roles/common/filters :需要使用的文件或者文件夹 Roles/common/task:部署任务(main.yml 为入口必须要有) Roles/common/templates :配置模板(jinja2 模板语法 用于可变更的配置文件,可获取定义在 Group_vars 中的变量) Roles/Docker :Docker 的安装配置 Roles/HAproxy : HAproxy 的负载均衡配置 Roles/Splash : Splash 的镜像拉取配置部署以及启动 site.yml : 启动入口

使用方法:

在你的 Inventory 文件定义好主机分组:

必须包括 HaProxy、和 Docker 两个分组如下:

1
2
3
4
5
6
7
study@study:~/文档/ansible-examples$ cat /etc/ansible/inventory/splash
[docker]
1.1.1.1
[haproxy]
10.253.20.25

[splash_ports]

主控端新建 SSH 秘钥并发布到你你需要配置的所有主机!!!!(一定要注意如果本机当前工作用户在远程主机不存在额时候,需要指定 remote_user 这个参数):

1
2
3
4
5
study@study:~/文档/ansible-examples$ cat /etc/ansible/ansible.cfg
[defaults]
inventory= /etc/ansible/inventory/

remote_user=root

好了开始执行:

1
study@study:~/文档/ansible-examples/Splash_Load_balancing_cluster$ ansible-playbook site.yml

效果就像这样:

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
PLAY [all] **********************************************************************************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************************************************************************
ok: [10.1.4.101]
ok: [10.1.4.100]

TASK [common : Copy the CentOS repository definition] ***************************************************************************************************************************************
ok: [10.1.4.100]
ok: [10.1.4.101]

TASK [common : Copy the Docker repository definition] ***************************************************************************************************************************************
ok: [10.1.4.100]
ok: [10.1.4.101]

TASK [common : Create the repository for EPEL] **********************************************************************************************************************************************
ok: [10.1.4.100]
ok: [10.1.4.101]

TASK [common : Create the GPG key for EPEL] *************************************************************************************************************************************************
ok: [10.1.4.100]
ok: [10.1.4.101]

TASK [common : Firewalld service stop] ******************************************************************************************************************************************************
ok: [10.1.4.100]
ok: [10.1.4.101]

TASK [common : Chronyd service stop] ********************************************************************************************************************************************************
ok: [10.1.4.100]
ok: [10.1.4.101]

TASK [common : Install Ansible Base package] ************************************************************************************************************************************************
ok: [10.1.4.100] => (item=['libselinux-python', 'libsemanage-python', 'ntp'])
ok: [10.1.4.101] => (item=['libselinux-python', 'libsemanage-python', 'ntp'])

TASK [common : Configure SELinux to disable] ************************************************************************************************************************************************
[WARNING]: SELinux state change will take effect next reboot

ok: [10.1.4.100]
ok: [10.1.4.101]

TASK [common : Change TimeZone] *************************************************************************************************************************************************************
ok: [10.1.4.100]
ok: [10.1.4.101]

TASK [common : Copy NTP conf] ***************************************************************************************************************************************************************
ok: [10.1.4.100]
ok: [10.1.4.101]

TASK [common : NTP Start] *******************************************************************************************************************************************************************
ok: [10.1.4.100]
ok: [10.1.4.101]

PLAY [docker] *******************************************************************************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************************************************************************
ok: [10.1.4.101]

TASK [docker : Install Docker package] ******************************************************************************************************************************************************
ok: [10.1.4.101] => (item=['yum-utils', 'device-mapper-persistent-data', 'lvm2', 'docker-ce'])

TASK [docker : Start Docker] ****************************************************************************************************************************************************************
ok: [10.1.4.101]

TASK [docker : Create Docker Speed Configuration file] **************************************************************************************************************************************
ok: [10.1.4.101]

TASK [docker : Restart Docker] **************************************************************************************************************************************************************
changed: [10.1.4.101]

TASK [splash : pull splash] *****************************************************************************************************************************************************************
changed: [10.1.4.101]

TASK [splash : Copy Splash module] **********************************************************************************************************************************************************
ok: [10.1.4.101] => (item=filters)
ok: [10.1.4.101] => (item=js-profiles)
ok: [10.1.4.101] => (item=lua_modules)
ok: [10.1.4.101] => (item=proxy-profiles)

静静等着跑完 就可以愉快的使用啦 ! 需要增加节点的话直接把 IP 加载 Docker 分组下 重新执行一遍就可以了! 需要注意如果 SSH 非默认的 22 端口还需要指定你的端口号!怎么指定 看看文档去 以上完毕!!! 完整的看这儿:https://github.com/thsheep/ansible-examples

Python

2019 年 01 月 04 日 16:32:17 更新了新的 Chrome 镜像 将 Python 版本升级到了 3.7 Note: 推荐使用结尾提供的 Docker 镜像进行二次打包运行代码 各位小伙伴儿的采集日常是不是被 JavaScript 的各种点击事件折腾的欲仙欲死啊?好不容易找到个 Selenium+Chrome 可以解决问题! 但是另一个 ▄█▀█● 的事实摆在面前,服务器都特么没有 GUI 啊·· 好吧!咱们要知难而上!决不能被这个点小困难打倒······· 然而摆在面前的事实是···· 他丫的各种装不上啊!坑爹啊! 那么我来拯救你们于水火之间了! 服务器如下:

1
2
3
4
5
6
7
8
9
10
11
[root@spider01 ~]# hostnamectl
Static hostname: spider01
Icon name: computer-vm
Chassis: vm
Machine ID: 1c4029c4e7fd42498e25bb75101f85b6
Boot ID: f5a67454b94b454fae3d75ef1ccab69f
Virtualization: kvm
Operating System: CentOS Linux 7 (Core)
CPE OS Name: cpe:/o:centos:centos:7
Kernel: Linux 3.10.0-514.6.2.el7.x86_64
Architecture: x86-64

安装 Chromeium:

1
2
3
4
## 安装yum源
[root@spider01 ~]# sudo yum install -y epel-release
## 安装Chrome
[root@spider01 ~]# yum install -y chromium

去这个地方:https://sites.google.com/a/chromium.org/chromedriver/downloads 下载 ChromeDriver 驱动放在/usr/bin/目录下: 完成结果如下:

1
2
3
[root@spider01 ~]# ll /usr/bin/ | grep chrom
-rwxrwxrwx. 1 root root 7500280 1129 17:32 chromedriver
lrwxrwxrwx. 1 root root 47 1130 09:35 chromium-browser -> /usr/lib64/chromium-browser/chromium-browser.sh

安装 XVFB:

1
2
[root@spider01 ~]# yum install Xvfb -y
[root@spider01 ~]# yum install xorg-x11-fonts* -y

新建在/usr/bin/ 一个名叫 xvfb-chromium 的文件写入以下内容:

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
[root@spider01 ~]# cat /usr/bin/xvfb-chromium
#!/bin/bash

_kill_procs() {
kill -TERM $chromium
wait $chromium
kill -TERM $xvfb
}

# Setup a trap to catch SIGTERM and relay it to child processes
trap _kill_procs SIGTERM

XVFB_WHD=${XVFB_WHD:-1280x720x16}

# Start Xvfb
Xvfb :99 -ac -screen 0 $XVFB_WHD -nolisten tcp &
xvfb=$!

export DISPLAY=:99

chromium --no-sandbox --disable-gpu$@ &
chromium=$!

wait $chromium
wait $xvfb

更改软连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## 更改Chrome启动的软连接
[root@spider01 ~]# ln -s /usr/lib64/chromium-browser/chromium-browser.sh /usr/bin/chromium


[root@spider01 ~]# rm -rf /usr/bin/chromium-browser

[root@spider01 ~]# ln -s /usr/bin/xvfb-chromium /usr/bin/chromium-browser

[root@spider01 ~]# ln -s /usr/bin/xvfb-chromium /usr/bin/google-chrome

[root@spider01 ~]# ll /usr/bin/ | grep chrom*
-rwxrwxrwx. 1 root root 7500280 1129 17:32 chromedriver
lrwxrwxrwx. 1 root root 47 1130 09:47 chromium -> /usr/lib64/chromium-browser/chromium-browser.sh
lrwxrwxrwx. 1 root root 22 1130 09:48 chromium-browser -> /usr/bin/xvfb-chromium
-rwxr-xr-x. 1 root root 73848 127 2016 chronyc
lrwxrwxrwx. 1 root root 22 1130 09:48 google-chrome -> /usr/bin/xvfb-chromium
-rwxrwxrwx. 1 root root 387 1129 18:16 xvfb-chromium

来瞅瞅能不能用哦:

1
2
3
4
5
6
7
8
9
10
11
>>> from selenium import webdriver
>>> options = webdriver.ChromeOptions()
>>> options.add_argument('--headless')
>>> options.add_argument('--no-sandbox')
>>> options.add_argument('--disable-extensions')
>>> options.add_argument('--disable-gpu')
>>> driver = webdriver.Chrome(options=options)
>>> driver.get("http://www.baidu.com")
>>> driver.find_element_by_xpath("./*//input[@id='kw']").send_keys("哎哟卧槽")
>>> driver.find_element_by_xpath("./*//input[@id='su']").click()
>>> driver.page_source

No problem!!!! 好了部署完了!当然 Docker 这么火贼适合懒人了!来来 看这儿 Docker 版的 妥妥滴!

1
docker pull thsheep/python:3.7-debian-chrome

做好了 Python3.7 和 Chrome 集成 需要自己使用 Dockerfile 来重新打包安装你需要的 Python 包。

Note: 请按照以下方式初始化 Webdriver!!!!!!!!

1
2
3
4
5
6
7
8
9
from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-extensions')
options.add_argument('--disable-gpu')

driver = webdriver.Chrome(options=options)

否则会出现无法初始化 Webdriver 的情况

顺便一提!!!!这个玩意儿从事 Web 测试工作的小伙伴可以用!!!!!!!!

下面是 Dockerfile 文件:

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
FROM python:3.7-stretch

ENV DBUS_SESSION_BUS_ADDRESS=/dev/null

#============================================
# Google Chrome
#============================================
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list && \
apt-get update -qqy && \
apt-get -qqy install google-chrome-stable unzip&& \
rm /etc/apt/sources.list.d/google-chrome.list && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*

#==================
# Chrome driver
# CHROME_DRIVER_VERSION 需要根据上面安装的Chrome版本来设置(最好设置成最新版本)
# http://chromedriver.chromium.org/downloads 版本号在这页面上查看
#==================
ARG CHROME_DRIVER_VERSION=2.45
RUN wget -O /tmp/chromedriver.zip https://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip && \
rm -rf /opt/selenium/chromedriver && \
unzip /tmp/chromedriver.zip -d /opt/selenium && \
rm /tmp/chromedriver.zip && \
mv /opt/selenium/chromedriver /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION && \
chmod 755 /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION && \
ln -fs /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION /usr/bin/chromedriver

RUN google-chrome-stable --version

Python

微博登录限制了错误次数···加上 Cookie 大批账号被封需要从 Cookie 池中 剔除被封的账号··· 需要使用代理··· 无赖百度了大半天都是特么的啥玩意儿???结果换成了 Google 手到擒来 分分钟解决(那么问题来了?百度除了卖假药还会干啥?) Selenium+Chrome 认证代理不能通过 options 处理。只能换个方法使用扩展解决 原文地址:https://stackoverflow.com/questions/29983106/how-can-i-set-proxy-with-authentication-in-selenium-chrome-web-driver-using-pyth#answer-30953780 (Stack Overflow 这是个好地方啊) 走你!

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
# -*- coding: utf-8 -*-
# @Time : 2017/11/15 9:50
# @Author : 哎哟卧槽
# @Site :
# @File : pubilc.py
# @Software: PyCharm

import string
import zipfile

def create_proxyauth_extension(proxy_host, proxy_port,
proxy_username, proxy_password,
scheme='http', plugin_path=None):
"""代理认证插件

args:
proxy_host (str): 你的代理地址或者域名(str类型)
proxy_port (int): 代理端口号(int类型)
proxy_username (str):用户名(字符串)
proxy_password (str): 密码 (字符串)
kwargs:
scheme (str): 代理方式 默认http
plugin_path (str): 扩展的绝对路径

return str -> plugin_path
"""


if plugin_path is None:
plugin_path = 'vimm_chrome_proxyauth_plugin.zip'

manifest_json = """
{
"version": "1.0.0",
"manifest_version": 2,
"name": "Chrome Proxy",
"permissions": [
"proxy",
"tabs",
"unlimitedStorage",
"storage",
"<all_urls>",
"webRequest",
"webRequestBlocking"
],
"background": {
"scripts": ["background.js"]
},
"minimum_chrome_version":"22.0.0"
}
"""

background_js = string.Template(
"""
var config = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: "${scheme}",
host: "${host}",
port: parseInt(${port})
},
bypassList: ["foobar.com"]
}
};

chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});

function callbackFn(details) {
return {
authCredentials: {
username: "${username}",
password: "${password}"
}
};
}

chrome.webRequest.onAuthRequired.addListener(
callbackFn,
{urls: ["<all_urls>"]},
['blocking']
);
"""
).substitute(
host=proxy_host,
port=proxy_port,
username=proxy_username,
password=proxy_password,
scheme=scheme,
)
with zipfile.ZipFile(plugin_path, 'w') as zp:
zp.writestr("manifest.json", manifest_json)
zp.writestr("background.js", background_js)

return plugin_path

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from selenium import webdriver
from common.pubilc import create_proxyauth_extension

proxyauth_plugin_path = create_proxyauth_extension(
proxy_host="XXXXX.com",
proxy_port=9020,
proxy_username="XXXXXXX",
proxy_password="XXXXXXX"
)


co = webdriver.ChromeOptions()
# co.add_argument("--start-maximized")
co.add_extension(proxyauth_plugin_path)


driver = webdriver.Chrome(executable_path="C:\chromedriver.exe", chrome_options=co)
driver.get("http://ip138.com/")
print(driver.page_source)

无认证代理:

1
2
3
4
5
options = webdriver.ChromeOptions()
options.add_argument('--proxy-server=http://ip:port')
driver = webdriver.Chrome(executable_path="C:\chromedriver.exe", chrome_options=0ptions)
driver.get("http://ip138.com/")
print(driver.page_source)

以上完毕 So Easy

Java

PS:此文章为小白提供,大佬请绕道!!!! 首先特别感谢大才哥给我提供这个平台,未来我希望把java这个版块的内容补全。 今天要讲的是数据类型,最最最基础的内容~ java标识符、数据类型、关键字 开始我们先看下如何注释java代码。 标识符:类名,方法名,变量。 有三种方式分别为 //表示注释一行代码 / 表示注释一行或者多行代码 (从上面到下面都是注释的代码) / 下面还有一种注释方式叫做文档注释。 / 通常这样表示 */ 文档注释一般写在代码开头用来简述你所做程序的具体内容,在这之前我们首先看一下javadoc命令,我先编写一个简答的代码: package com.briup.chap02; / @author Twinkle @version 1.0 It’s a text file / public class PrimitiveType{ public static void main(String[] args){ byte b = 123; byte b1 = 300; } } 我们javadoc -d 生成目录 编译文件 编译成功后,我们打开刚刚生成doc里打开index.html看一下,大概是这样的: 类概要 类: Student 说明: It’s a text file 这样我们就可以看出文档注释的意义了,他可以显示在你编译出来文档的说明里,但有人会发现为啥我们编写出来的author没有出来呀? 因为他的最前面有一个@,我们需要编写的时候把它加上去才能显示出来,现在我们来试一下, —javadoc -d bin/doc-author -version src/PrimitiveType.java,这样作者和版本信息就出来了 一.类名 这边我们要记住一些代码的基本格式: 类名的写法:Student(前面首字母要大写) 方法和变量的写法:genderItem(前面单词小写,后面单词开头要大写) 常量写法:MAX_PAGE(常量大写,中间一般加下划线) 二.关键字 关键字其实就是电脑里面已经定义好的有特殊意义的标识符,像int,for,double什么的都是关键字。具体意思请百度一下~ 三.数据类型 数据类型是这篇文章的重点,我们来看下这些基本的数据类型 类型 二进制位 例 范围 byte 8位 11111111~01111111 -2^7~2^7-1 short 16位 16个二进制代码 -2^15~2^15-1 int 32位 32个二进制代码 -2^31~2^31-1 long 64位 64个二进制代码 -2^63~2^63-1 浮点型: float 32位 32个二进制代码 double 64位 64个二进制代码 布尔型: boolean 只有false和true两种类型。 具体解释一下为什么会有这么多类型呢?而且二进制位为什么还不一样? 类型多的原因是因为有些数值本身就很小,传递给大的数据类型的话,虽然可以进去,但是有些二进制位就空闲了,占用了多余的内存却没有什么作用,所以才会有这么多的类型。 我们知道编程最终的目的是我们把代码传递给硬件,通过硬件来工作,但是呢,硬件只识别二进制代码,所以java会有一个把它的代码转化为二进制代码的过渡,上面的二进制位就是二进制码的数目,我们要想看他的范围有多大,可以这样算,二进制的第一位为标志符,通俗一点讲就是正负号,后面还有n位的话它的范围就是-|2^n|~|2^n-1| 如果我们定义的类型超出这个范围的话(也就是盆子里已经装满了东西如果再加),java就会报错,超出指定的范围,所以当我们定义数据类型的时候要搞清楚各数据类型的范围。 还有一个特殊的数据类型:char (‘字符’) char的具体位数要结合unicode编码。问题又来了,unicode编码又是什么鬼!unicode编码是一个字符集,里面包含了中,日,韩,三种文字,我们可以通过char的方法来打印出字符:char(‘u\unicode编码’),unicode表具体百度一下哈~ 数据类型转换: 显式转换:也就是强制转换 隐式转换:由JVM虚拟机自行转换 数据类型的强制转换:int a = (强制转换类型)b 转换规则:从存储范围大的类型到存储范围小的类型。 具体规则为:double→float→long→int→short(char)→byte byte b =10; byte a = (int) b; 如果我们把int类型的b转换给byte类型的a的话,会出现溢出现象,所以会报错。 所以正确强制转换的方式为~~: byte b = 10; int(或者更大的类型) a =(int) b; java基本的数据类型就讲到这里啦~ *--可能发布的内容有点混乱,我会尽快把前面的补齐~有疑问的话可以到大才哥的群里找我哈~

未分类

对于 Scrapy 处理 Ajax 处理方式当然是同家兄弟 Splash 比较靠谱! 但是 Splash 有个很坑爹的毛病就是负载承受相对较小·· 一不留神就 GG 了·········· 然后也就没有然后了~~! 所以准备给 Splash 做一个负载均衡;后端放一大堆的 Splash 这样总不会 GG 了吧。 就算其中一个 GG 了还有其它的可替代不是? 废话不多少开整·· 环境是基于: CentOS 7.3 Docker 17.06.2-ce Splash 3.0 HAproxy 1.7.9 (CentOS 大家可以将 yum 切换为阿里云的 yum 源 Docker 同理)

阿里 yum 源: http://mirrors.aliyun.com/help/centos 照葫芦画瓢做一遍(你是 CentOS7 啊!!!!不要选成其他版本了)

注意以下只需要在你需要运行 splash 的机器上安装即可

阿里 Docker 源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# step 1: 安装必要的一些系统工具

sudo yum install -y yum-utils device-mapper-persistent-data lvm2

# Step 2: 添加软件源信息

sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

# Step 3: 更新并安装 Docker-CE

sudo yum makecache fast
sudo yum -y install docker-ce

# Step 4: 开启Docker服务

sudo service docker start

安装 Docker 加速器:

1
curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://8050f360.m.daocloud.io

重启 Docker:

1
systemctl restart docker

这样可以极快的速度拉取镜像。 获取 splash 最新的 docker 镜像:

1
docker pull scrapinghub/splash:master

关闭所有机器防火墙 firewalld(网络安全的环境关闭,不安全的环境请放行端口,自行百度):

1
2
3
systemctl disable firewalld

systemctl stop firewalld

创建 Splash 配置文件目录:

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
# 存放过滤规则文件的目录

[root@localhost ~]# mkdir filters

# 存放JavaScript文件目录

[root@localhost ~]# mkdir js-profiles

# 存放lua模块的目录

[root@localhost ~]# mkdir lua_modules

# 存放代理文件的目录

[root@localhost ~]# mkdir proxy-profiles

# 创建完成如下:

[root@localhost ~]# pwd
/root
[root@localhost ~]# ll
total 4
drwxr-xr-x. 2 root root 25 Sep 26 03:00 filters
drwxr-xr-x. 2 root root 6 Sep 25 21:08 js-profiles
drwxr-xr-x. 2 root root 6 Sep 25 21:08 lua_modules
drwxr-xr-x. 2 root root 32 Sep 25 21:08 proxy-profiles
[root@localhost ~]#

启动 Splash:

1
docker run -d -p 8050:8050 --memory=5.0G --restart=always  --name splash       -v /root/proxy-profiles:/etc/splash/proxy-profiles       -v /root/js-profiles:/etc/splash/js-profiles       -v /root/lua_modules:/etc/splash/lua_modules       -v /root/filters:/etc/splash/filters       scrapinghub/splash:master --maxrss 4500

docker run 启动一个容器 -d 后台启动 -p 8050:8050 将容器的 8050 端口和物理机的 8050 端口绑定(可以从 8050 端口访问容器服务应用) —memory=5.0G 容器最大使用内存为 5.0GB,超出这个限制会被主进程杀死(使用 free -mg 查看并酌情设置你的内存使用) —restart=always 容器退出后无条件重启(满了 5GB 被杀死,然后重启 释放内存) —name splash 容器的名字叫 splash(可以忽略) -v ** 三个-v 参数是将宿主机的目录挂载进容器,便于容器能够直接访问挂载目录中的内容 scrapinghub/splash:master 用于启动容器的镜像 —maxrss 4500 Splash 最大内存使用为 4500MB

查看容器是否启动:

1
2
3
4
[root@localhost ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1b34f7933095 scrapinghub/splash:master "python3 /app/bin/..." 4 hours ago Up 4 hours 5023/tcp, 0.0.0.0:8050->8050/tcp splash
[root@localhost ~]#

访问 Splash 是否正常工作:

请注意:以上操作只需要在你需要运行 splash 的机器上安装即可

安装 HAproxy 实现负载均衡:

安装 zlib-devel(HAproxy 使用 gzip 功能):

1
yum install zlib-devel -y

安装 HAproxy:

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
# 个人喜好 源码放在这个目录
[root@localhost examples]# cd /usr/local/src/

# 安装wget
[root@localhost src]#yum install wget -y

# 下载HAproxy安装包
[root@localhost src]# wget http://www.haproxy.org/download/1.7/src/haproxy-1.7.9.tar.gz

# 解压
[root@localhost src]# tar -zxvf haproxy-1.7.9.tar.gz

# 进入目录
[root@localhost src]# cd haproxy-1.7.9

# 编译
[root@localhost src]# make TARGET=linux2628 PREFIX=/usr/local/haproxy-1.7.9 USE_ZLIB=yes

# 安装
[root@localhost src]# make install

# 拷贝启动文件到目录
[root@localhost src]# cp /usr/local/sbin/haproxy /usr/sbin/

# 测试版本
[root@localhost src]# haproxy -v

# 拷贝启动文件到启动目录
[root@localhost src]# cp examples/haproxy.init /etc/init.d/haproxy

# 赋予可执行权限
[root@localhost src]# chmod 755 /etc/init.d/haproxy

# 创建配置文件目录
[root@localhost src]# mkdir /etc/haproxy

# 创建数据目录
[root@localhost src]# mkdir /var/lib/haproxy

# 创建运行文件目录
[root@localhost src]# mkdir /var/run/haproxy

# 设置日志
[root@localhost src]# vim /etc/rsyslog.conf
# 第15行 $ModLoad imudp #打开注释
# 第16行 $UDPServerRun 514 #打开注释
# 第74行 local3.* /var/log/haproxy.log #local3的路径

# 创建日志文件
[root@localhost src]# touch /var/log/haproxy.log

# 设置权限
[root@localhost src]# chown -R haproxy.haproxy /var/log/haproxy.log

# 启动日志服务
[root@localhost src]# systemctl restart rsyslog.service

配置 HAproxy Conf:

1
[root@localhost src]# vim /etc/haproxy/haproxy.cfg

写入以下内容:

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
# HAProxy 1.7 config for Splash. It assumes Splash instances are executed
# on the same machine and connected to HAProxy using Docker links.
global
# raise it if necessary
maxconn 512
# required for stats page
stats socket /tmp/haproxy

userlist users
user user insecure-password userpass

defaults
log global
mode http

# remove requests from a queue when clients disconnect;
# see https://cbonte.github.io/haproxy-dconv/1.7/configuration.html#4.2-option%20abortonclose
option abortonclose

# gzip can save quite a lot of traffic with json, html or base64 data
# compression algo gzip
compression type text/html text/plain application/json

# increase these values if you want to
# allow longer request queues in HAProxy
timeout connect 3600s
timeout client 3600s
timeout server 3600s


# visit 0.0.0.0:8036 to see HAProxy stats page
listen stats
bind *:8036
mode http
stats enable
stats hide-version
stats show-legends
stats show-desc Splash Cluster
stats uri /
stats refresh 10s
stats realm Haproxy\ Statistics
stats auth admin:adminpass


# Splash Cluster configuration
# 代理服务器监听全局的8050端口
frontend http-in
bind *:8050
# 如果你需要开启Splash的访问认证
# 则注释default_backend splash-cluster
# 并放开其余default_backend splash-cluster 之上的其余注释
# 账号密码为user userpass
# acl auth_ok http_auth(users)
# http-request auth realm Splash if !auth_ok
# http-request allow if auth_ok
# http-request deny

# acl staticfiles path_beg /_harviewer/
# acl misc path / /info /_debug /debug

# use_backend splash-cluster if auth_ok !staticfiles !misc
# use_backend splash-misc if auth_ok staticfiles
# use_backend splash-misc if auth_ok misc
default_backend splash-cluster


backend splash-cluster
option httpchk GET /
balance leastconn

# try another instance when connection is dropped
retries 2
option redispatch
# 将下面IP地址替换为你自己的Splash服务IP和端口
# 按照以下格式一次增加其余的Splash服务器
server splash-0 10.10.1.41:8050 check maxconn 5 inter 2s fall 10 observe layer4
server splash-1 10.10.1.42:8050 check maxconn 5 inter 2s fall 10 observe layer4
server splash-2 10.10.1.32:8050 check maxconn 5 inter 2s fall 10 observe layer4

backend splash-misc
balance roundrobin
# 将下面IP地址替换为你自己的Splash服务IP和端口
# 按照以下格式一次增加其余的Splash服务器
server splash-0 10.10.1.41:8050 check fall 15
server splash-1 10.10.1.42:8050 check fall 15
server splash-2 10.10.1.32:8050 check fall 15

启动 HAproxy:

1
2
3
4
5
6
7
8
# 启动HAproxy
[root@localhost src]# /etc/init.d/haproxy start
Restarting haproxy (via systemctl): [ OK ]

# 如果出现错误则使用:
[root@localhost examples]# systemctl status haproxy.service

# 查看报错

查看 HAproxy 状态: 用户名和密码为: admin adminpass

查看 HAproxy 负载是否生效:

完美!!!收工!! 注意:HAproxy 这台服务器没有安装 Splash 服务,是负载到其余安装有 Splash 的服务器上提供的服务器哦!

Python

大家好,我还是小四毛,不是崔老师!!!!崔老师在隔壁,哈哈哈。

写了一个从网上抓取代理 IP,然后构建代理 IP 池的脚本,放在了这里:https://github.com/xiaosimao/IP_POOL

以后应该还会有很多的改动, 欢迎有兴趣的同学 star,以便及时可以收到改动的通知。

目前是从以下几个网站获取 IP:66ip,xicidaili,data5u,proxydb。

具体的使用方法文档在 readme.md 中, 欢迎交流。

如果你需要从别的网站获得, 那么可以在配置文件中进行相关的配置即可, 如果实在不想自己写,也可以提 issue 给我,我会看情况更新到代码中。

一般情况下,只要配置一下配置项就可以从新的网站获取 IP,最大限度减少写代码的时间。

免费的 ip,质量不敢保证,目前测试的目标网站为百度和https://httpbin.org/get, 还是获得了一些通过测试的 IP,下面是截图。

Net

HTTP 2xx 范围内的状态码表明了“客户端发送的请求已经被服务器接受并且被成功处理了”。 HTTP/1.1 200 OK 是 HTTP 请求成功后的标准响应,当你在浏览器中打开某个网站后,你通常会得到一个 200 状态码。HTTP/1.1 206 状态码表示的是“客户端通过发送范围请求头Range抓取到了资源的部分数据” 这种请求通常用来:

  • 学习http头和状态
  • 解决网路问题
  • 解决大文件下载问题
  • 解决CDN和原始HTTP服务器问题
  • 使用工具例如lftp,wget,telnet测试断电续传
  • 测试将一个大文件分割成多个部分同时下载

测试

查看服务器是否支持 HTTP 206:

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
curl -I https://raw.githubusercontent.com/Germey/LaravelGeetest/master/README.md
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-XSS-Protection: 1; mode=block
ETag: "b29f4639b76cd7f94a4b36b05be6c85acfe478f1"
Content-Type: text/plain; charset=utf-8
Cache-Control: max-age=300
X-Geo-Block-List:
X-GitHub-Request-Id: 850A:16D2:30128BA:3341504:59BBC946
Content-Length: 8709
Accept-Ranges: bytes
Date: Fri, 15 Sep 2017 12:36:31 GMT
Via: 1.1 varnish
Connection: keep-alive
X-Served-By: cache-nrt6123-NRT
X-Cache: HIT
X-Cache-Hits: 1
X-Timer: S1505478991.145862,VS0,VE1
Vary: Authorization,Accept-Encoding
Access-Control-Allow-Origin: *
X-Fastly-Request-ID: ee23d80d2ba507ec0a70c880a075df0d2671aa4d
Expires: Fri, 15 Sep 2017 12:41:31 GMT
Source-Age: 8

其中有两个我们比较关注的请求头: Accept-Ranges: bytes:该响应头表明服务器支持 Range 请求,以及服务器所支持的单位是字节。同时服务器支持断点续传,以及支持同时下载文件的多个部分,也就是说下载工具可以利用范围请求加速下载该文件。Accept-Ranges: none 响应头表示服务器不支持范围请求。 Content-Length: 8709 :Content-Length 响应头表明了响应实体的大小,也就是真实的图片文件的大小是 8709 字节 (8.7K)。

发送

利用 CURL 可以指定请求范围。 获取前 500 字节:

1
curl --header "Range: bytes=0-500" https://raw.githubusercontent.com/Germey/LaravelGeetest/master/README.md

后 500 字节:

1
curl --header "Range: bytes=-500" https://raw.githubusercontent.com/Germey/LaravelGeetest/master/README.md

从 500 - 1000 字节:

1
curl --header "Range: bytes=500-1000" https://raw.githubusercontent.com/Germey/LaravelGeetest/master/README.md

从 500 - 末尾字节:

1
curl --header "Range: bytes=500-" https://raw.githubusercontent.com/Germey/LaravelGeetest/master/README.md

开启

大部分web服务器都原生支持字节范围请求. Apache 2.x用户可以在httpd.conf中尝试 mod_headers:

1
Header set Accept-Ranges bytes

Python

大家好,我是四毛, 不是崔老师。

恩,今天的内容很短, 主要都写在了 README.md 里面。

写了一个将爬虫基本步骤都封装起来的小框架,地址在https://github.com/xiaosimao/AiSpider, 欢迎 Star。

写的很基础,很简单,大道至简(对,其实就是不会写)。

最近也在学一些设计模式的东西。

欢迎有兴趣的同学共同研究,readme.md 中有我的微信(加的时候麻烦注明一下来自静觅),提出存在的问题和你的想法,这样大家可以共同讨论,共同进步。

BE A SPIDERMAN。

Python

Neo4j是一个世界领先的开源图形数据库,由 Java 编写。图形数据库也就意味着它的数据并非保存在表或集合中,而是保存为节点以及节点之间的关系。 Neo4j 的数据由下面几部分构成:

  • 节点
  • 属性

Neo4j 除了顶点(Node)和边(Relationship),还有一种重要的部分——属性。无论是顶点还是边,都可以有任意多的属性。属性的存放类似于一个 HashMap,Key 为一个字符串,而 Value 必须是基本类型或者是基本类型数组。 在Neo4j中,节点以及边都能够包含保存值的属性,此外:

  • 可以为节点设置零或多个标签(例如 Author 或 Book)
  • 每个关系都对应一种类型(例如 WROTE 或 FRIEND_OF)
  • 关系总是从一个节点指向另一个节点(但可以在不考虑指向性的情况下进行查询)

具体介绍可以参考:https://www.w3cschool.cn/neo4j

Neo4j的特点

  • 它拥有简单的查询语言 Neo4j CQL
  • 它遵循属性图数据模型
  • 它通过使用 Apache Lucence 支持索引
  • 它支持 UNIQUE 约束
  • 它包含一个用于执行 CQL 命令的 UI:Neo4j 数据浏览器
  • 它支持完整的 ACID(原子性,一致性,隔离性和持久性)规则
  • 它采用原生图形库与本地 GPE(图形处理引擎)
  • 它支持查询的数据导出到 Json 和 XLS 格式
  • 它提供了 REST API,可以被任何编程语言(如 Java,Spring,Scala 等)访问
  • 它提供了可以通过任何 UI MVC 框架(如 Node JS )访问的 Java 脚本
  • 它支持两种 Java API:Cypher API 和 Native Java API 来开发 Java 应用程序

Neo4j安装

可以到官网直接下载安装包安装即可,链接:https://neo4j.com/download/

Neo4j CQL命令

Neo4j 的 CQL 是非常重要的命令,类似于 SQL 语句,具体的用法可以参考:https://www.w3cschool.cn/neo4j/neo4j_cql_introduction.html

Py2Neo用法

Py2Neo 是用来对接 Neo4j 的 Python 库,接下来对其详细介绍。

相关链接

安装方法

使用 Pip 安装即可:

1
pip3 install py2neo

Node & Relationship

Neo4j 里面最重要的两个数据结构就是节点和关系,即 Node 和 Relationship,可以通过 Node 或 Relationship 对象创建,实例如下:

1
2
3
4
5
6
from py2neo import Node, Relationship

a = Node('Person', name='Alice')
b = Node('Person', name='Bob')
r = Relationship(a, 'KNOWS', b)
print(a, b, r)

运行结果:

1
(alice:Person {name:"Alice"}) (bob:Person {name:"Bob"}) (alice)-[:KNOWS]->(bob)

这样我们就成功创建了两个 Node 和两个 Node 之间的 Relationship。 Node 和 Relationship 都继承了 PropertyDict 类,它可以赋值很多属性,类似于字典的形式,例如可以通过如下方式对 Node 或 Relationship 进行属性赋值,接着上面的代码,实例如下:

1
2
3
4
a['age'] = 20
b['age'] = 21
r['time'] = '2017/08/31'
print(a, b, r)

运行结果:

1
(alice:Person {age:20,name:"Alice"}) (bob:Person {age:21,name:"Bob"}) (alice)-[:KNOWS {time:"2017/08/31"}]->(bob)

可见通过类似字典的操作方法就可以成功实现属性赋值。 另外还可以通过 setdefault() 方法赋值默认属性,例如:

1
2
a.setdefault('location', '北京')
print(a)

运行结果:

1
(alice:Person {age:20,location:"北京",name:"Alice"})

可见没有给 a 对象赋值 location 属性,现在就会使用默认属性。 但如果赋值了 location 属性,则它会覆盖默认属性,例如:

1
2
3
a['location'] = '上海'
a.setdefault('location', '北京')
print(a)

运行结果:

1
(alice:Person {age:20,location:"上海",name:"Alice"})

另外也可以使用 update() 方法对属性批量更新,接着上面的例子实例如下:

1
2
3
4
5
6
data = {
'name': 'Amy',
'age': 21
}
a.update(data)
print(a)

运行结果:

1
(alice:Person {age:21,location:"上海",name:"Amy"})

可以看到这里更新了 a 对象的 name 和 age 属性,没有更新 location 属性,则 name 和 age 属性会更新,location 属性则会保留。

Subgraph

Subgraph,子图,是 Node 和 Relationship 的集合,最简单的构造子图的方式是通过关系运算符,实例如下:

1
2
3
4
5
6
7
from py2neo import Node, Relationship

a = Node('Person', name='Alice')
b = Node('Person', name='Bob')
r = Relationship(a, 'KNOWS', b)
s = a | b | r
print(s)

运行结果:

1
({(alice:Person {name:"Alice"}), (bob:Person {name:"Bob"})}, {(alice)-[:KNOWS]->(bob)})

这样就组成了一个 Subgraph。 另外还可以通过 nodes() 和 relationships() 方法获取所有的 Node 和 Relationship,实例如下:

1
2
print(s.nodes())
print(s.relationships())

运行结果:

1
2
frozenset({(alice:Person {name:"Alice"}), (bob:Person {name:"Bob"})})
frozenset({(alice)-[:KNOWS]->(bob)})

可以看到结果是 frozenset 类型。 另外还可以利用 & 取 Subgraph 的交集,例如:

1
2
3
s1 = a | b | r
s2 = a | b
print(s1 & s2)

运行结果:

1
({(alice:Person {name:"Alice"}), (bob:Person {name:"Bob"})}, {})

可以看到结果是二者的交集。 另外我们还可以分别利用 keys()、labels()、nodes()、relationships()、types() 分别获取 Subgraph 的 Key、Label、Node、Relationship、Relationship Type,实例如下:

1
2
3
4
5
6
s = a | b | r
print(s.keys())
print(s.labels())
print(s.nodes())
print(s.relationships())
print(s.types())

运行结果:

1
2
3
4
5
frozenset({'name'})
frozenset({'Person'})
frozenset({(alice:Person {name:"Alice"}), (bob:Person {name:"Bob"})})
frozenset({(alice)-[:KNOWS]->(bob)})
frozenset({'KNOWS'})

另外还可以用 order() 或 size() 方法来获取 Subgraph 的 Node 数量和 Relationship 数量,实例如下:

1
2
3
4
from py2neo import Node, Relationship, size, order
s = a | b | r
print(order(s))
print(size(s))

运行结果:

1
2
2
1

Walkable

Walkable 是增加了遍历信息的 Subgraph,我们通过 + 号便可以构建一个 Walkable 对象,例如:

1
2
3
4
5
6
7
8
9
from py2neo import Node, Relationship

a = Node('Person', name='Alice')
b = Node('Person', name='Bob')
c = Node('Person', name='Mike')
ab = Relationship(a, "KNOWS", b)
ac = Relationship(a, "KNOWS", c)
w = ab + Relationship(b, "LIKES", c) + ac
print(w)

运行结果:

1
(alice)-[:KNOWS]->(bob)-[:LIKES]->(mike)<-[:KNOWS]-(alice)

这样我们就形成了一个 Walkable 对象。 另外我们可以调用 walk() 方法实现遍历,实例如下:

1
2
3
4
from py2neo import walk

for item in walk(w):
print(item)

运行结果:

1
2
3
4
5
6
7
(alice:Person {name:"Alice"})
(alice)-[:KNOWS]->(bob)
(bob:Person {name:"Bob"})
(bob)-[:LIKES]->(mike)
(mike:Person {name:"Mike"})
(alice)-[:KNOWS]->(mike)
(alice:Person {name:"Alice"})

可以看到它从 a 这个 Node 开始遍历,然后到 b,再到 c,最后重新回到 a。 另外还可以利用 start_node()、end_node()、nodes()、relationships() 方法来获取起始 Node、终止 Node、所有 Node 和 Relationship,例如:

1
2
3
4
print(w.start_node())
print(w.end_node())
print(w.nodes())
print(w.relationships())

运行结果:

1
2
3
4
(alice:Person {name:"Alice"})
(alice:Person {name:"Alice"})
((alice:Person {name:"Alice"}), (bob:Person {name:"Bob"}), (mike:Person {name:"Mike"}), (alice:Person {name:"Alice"}))
((alice)-[:KNOWS]->(bob), (bob)-[:LIKES]->(mike), (alice)-[:KNOWS]->(mike))

可以看到本例中起始和终止 Node 都是同一个,这和 walk() 方法得到的结果是一致的。

Graph

在 database 模块中包含了和 Neo4j 数据交互的 API,最重要的当属 Graph,它代表了 Neo4j 的图数据库,同时 Graph 也提供了许多方法来操作 Neo4j 数据库。 Graph 在初始化的时候需要传入连接的 URI,初始化参数有 bolt、secure、host、http_port、https_port、bolt_port、user、password,详情说明可以参考:http://py2neo.org/v3/database.html#py2neo.database.Graph。 初始化的实例如下:

1
2
3
4
from py2neo import Graph
graph_1 = Graph()
graph_2 = Graph(host="localhost")
graph_3 = Graph("http://localhost:7474/db/data/")

另外我们还可以利用 create() 方法传入 Subgraph 对象来将关系图添加到数据库中,实例如下:

1
2
3
4
5
6
7
8
from py2neo import Node, Relationship, Graph

a = Node('Person', name='Alice')
b = Node('Person', name='Bob')
r = Relationship(a, 'KNOWS', b)
s = a | b | r
graph = Graph(password='123456')
graph.create(s)

这里必须确保 Neo4j 正常运行,其密码为 123456,这里调用 create() 方法即可完成图的创建,结果如下: 另外我们也可以单独添加单个 Node 或 Relationship,实例如下:

1
2
3
4
5
6
7
8
from py2neo import Graph, Node, Relationship

graph = Graph(password='123456')
a = Node('Person', name='Alice')
graph.create(a)
b = Node('Person', name='Bob')
ab = Relationship(a, 'KNOWS', b)
graph.create(ab)

运行结果如下: 另外还可以利用 data() 方法来获取查询结果:

1
2
3
4
5
from py2neo import Graph

graph = Graph(password='123456')
data = graph.data('MATCH (p:Person) return p')
print(data)

运行结果:

1
[{'p': (e0d0f96:Person {name:"Alice"})}, {'p': (cfe57d0:Person {name:"Bob"})}]

这里是通过 CQL 语句实现的查询,输出结果即 CQL 语句的返回结果,是列表形式。 另外输出结果还可以直接转化为 DataFrame 对象,实例如下:

1
2
3
4
5
6
from py2neo import Graph
from pandas import DataFrame
graph = Graph(password='123456')
data = graph.data('MATCH (p:Person) return p')
df = DataFrame(data)
print(df)

运行结果:

1
2
3
                   p
0 {'name': 'Alice'}
1 {'name': 'Bob'}

另外可以使用 find_one() 或 find() 方法进行 Node 的查找,可以利用 match() 或 match_one() 方法对 Relationship 进行查找:

1
2
3
4
5
6
7
from py2neo import Graph

graph = Graph(password='123456')
node = graph.find_one(label='Person')
print(node)
relationship = graph.match_one(rel_type='KNOWS')
print(relationship)

运行结果:

1
2
(c7402c7:Person {age:21,name:"Alice"})
(c7402c7)-[:KNOWS]->(e2c42fc)

如果想要更新 Node 的某个属性可以使用 push() 方法,例如:

1
2
3
4
5
6
7
8
from py2neo import Graph, Node

graph = Graph(password='123456')
a = Node('Person', name='Alice')
node = graph.find_one(label='Person')
node['age'] = 21
graph.push(node)
print(graph.find_one(label='Person'))

运行结果:

1
(a90a763:Person {age:21,name:"Alice"})

如果想要删除某个 Node 可以使用 delete() 方法,例如:

1
2
3
4
5
6
7
from py2neo import Graph

graph = Graph(password='123456')
node = graph.find_one(label='Person')
relationship = graph.match_one(rel_type='KNOWS')
graph.delete(relationship)
graph.delete(node)

在删除 Node 时必须先删除其对应的 Relationship,否则无法删除 Node。 另外我们也可以通过 run() 方法直接执行 CQL 语句,例如:

1
2
3
4
5
from py2neo import Graph

graph = Graph(password='123456')
data = graph.run('MATCH (p:Person) RETURN p LIMIT 5')
print(list(data))

运行结果:

1
[('p': (b6f61ff:Person {age:20,name:"Alice"})), ('p': (cc238b1:Person {age:20,name:"Alice"})), ('p': (b09e672:Person {age:20,name:"Alice"}))]

NodeSelector

Graph 有时候用起来不太方便,比如如果要根据多个条件进行 Node 的查询是做不到的,在这里更方便的查询方法是利用 NodeSelector,我们首先新建如下的 Node 和 Relationship,实例如下:

1
2
3
4
5
6
7
8
9
10
11
from py2neo import Graph, Node, Relationship

graph = Graph(password='123456')
a = Node('Person', name='Alice', age=21, location='广州')
b = Node('Person', name='Bob', age=22, location='上海')
c = Node('Person', name='Mike', age=21, location='北京')
r1 = Relationship(a, 'KNOWS', b)
r2 = Relationship(b, 'KNOWS', c)
graph.create(a)
graph.create(r1)
graph.create(r2)

运行结果: 在这里我们用 NodeSelector 来筛选 age 为 21 的 Person Node,实例如下:

1
2
3
4
5
6
from py2neo import Graph, NodeSelector

graph = Graph(password='123456')
selector = NodeSelector(graph)
persons = selector.select('Person', age=21)
print(list(persons))

运行结果:

1
[(d195b2e:Person {age:21,location:"广州",name:"Alice"}), (eefe475:Person {age:21,location:"北京",name:"Mike"})]

另外也可以使用 where() 进行更复杂的查询,例如查找 name 是 A 开头的 Person Node,实例如下:

1
2
3
4
5
6
from py2neo import Graph, NodeSelector

graph = Graph(password='123456')
selector = NodeSelector(graph)
persons = selector.select('Person').where('_.name =~ "A.*"')
print(list(persons))

运行结果:

1
[(bcd8072:Person {age:21,location:"广州",name:"Alice"})]

在这里用了正则表达式匹配查询。 另外也可以使用 order_by() 进行排序:

1
2
3
4
5
6
from py2neo import Graph, NodeSelector

graph = Graph(password='123456')
selector = NodeSelector(graph)
persons = selector.select('Person').order_by('_.age')
print(list(persons))

运行结果:

1
[(e3fc3d7:Person {age:21,location:"广州",name:"Alice"}), (da0179d:Person {age:21,location:"北京",name:"Mike"}), (cafa16e:Person {age:22,location:"上海",name:"Bob"})]

前面返回的都是列表,如果要查询单个节点的话,可以使用 first() 方法,实例如下:

1
2
3
4
5
6
from py2neo import Graph, NodeSelector

graph = Graph(password='123456')
selector = NodeSelector(graph)
person = selector.select('Person').where('_.name =~ "A.*"').first()
print(person)

运行结果:

1
(ea81c04:Person {age:21,location:"广州",name:"Alice"})

更详细的内容可以查看:http://py2neo.org/v3/database.html#cypher-utilities

OGM

OGM 类似于 ORM,意为 Object Graph Mapping,这样可以实现一个对象和 Node 的关联,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from py2neo.ogm import GraphObject, Property, RelatedTo, RelatedFrom


class Movie(GraphObject):
__primarykey__ = 'title'

title = Property()
released = Property()
actors = RelatedFrom('Person', 'ACTED_IN')
directors = RelatedFrom('Person', 'DIRECTED')
producers = RelatedFrom('Person', 'PRODUCED')

class Person(GraphObject):
__primarykey__ = 'name'

name = Property()
born = Property()
acted_in = RelatedTo('Movie')
directed = RelatedTo('Movie')
produced = RelatedTo('Movie')

我们可以用它来结合 Graph 查询,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from py2neo import Graph
from py2neo.ogm import GraphObject, Property

graph = Graph(password='123456')


class Person(GraphObject):
__primarykey__ = 'name'

name = Property()
age = Property()
location = Property()

person = Person.select(graph).where(age=21).first()
print(person)
print(person.name)
print(person.age)

运行结果:

1
2
3
<Person name='Alice'>
Alice
21

这样我们就成功实现了对象和 Node 的映射。 我们可以用它动态改变 Node 的属性,例如修改某个 Node 的 age 属性,实例如下:

1
2
3
4
5
person = Person.select(graph).where(age=21).first()
print(person.__ogm__.node)
person.age = 22
print(person.__ogm__.node)
graph.push(person)

运行结果:

1
2
(ccf5640:Person {age:21,location:"北京",name:"Mike"})
(ccf5640:Person {age:22,location:"北京",name:"Mike"})

另外我们也可以通过映射关系进行 Relationship 的调整,例如通过 Relationship 添加一个关联 Node,实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from py2neo import Graph
from py2neo.ogm import GraphObject, Property, RelatedTo

graph = Graph(password='123456')

class Person(GraphObject):
__primarykey__ = 'name'

name = Property()
age = Property()
location = Property()
knows = RelatedTo('Person', 'KNOWS')

person = Person.select(graph).where(age=21).first()
print(list(person.knows))
new_person = Person()
new_person.name = 'Durant'
new_person.age = 28
person.knows.add(new_person)
print(list(person.knows))

运行结果:

1
2
[<Person name='Bob'>]
[<Person name='Bob'>, <Person name='Durant'>]

这样我们就完成了 Node 和 Relationship 的添加,同时由于设置了 primarykey 为 name,所以不会重复添加。 但是注意此时数据库并没有更新,只是对象更新了,如果要更新到数据库中还需要调用 Graph 对象的 push() 或 pull() 方法,添加如下代码即可:

1
graph.push(person)

也可以通过 remove() 方法移除某个关联 Node,实例如下:

1
2
3
4
5
person = Person.select(graph).where(name='Alice').first()
target = Person.select(graph).where(name='Durant').first()
person.knows.remove(target)
graph.push(person)
graph.delete(target)

这里 target 是 name 为 Durant 的 Node,代码运行完毕后即可删除关联 Relationship 和删除 Node。 以上便是 OGM 的用法,查询修改非常方便,推荐使用此方法进行 Node 和 Relationship 的修改。 更多内容可以查看:http://py2neo.org/v3/ogm.html#module-py2neo.ogm

结语

以上便是对 Neo4j 的相关介绍。

Python

基本步骤: 1、训练素材分类: 我是参考官方的目录结构: 每个目录中放对应的文本,一个 txt 文件一篇对应的文章:就像下面这样 需要注意的是所有素材比例请保持在相同的比例(根据训练结果酌情调整、不可比例过于悬殊、容易造成过拟合(通俗点就是大部分文章都给你分到素材最多的那个类别去了)) 废话不多说直接上代码吧(测试代码的丑得一逼;将就着看看吧) 需要一个小工具: pip install chinese-tokenizer 这是训练器:

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
import re
import jieba
import json
from io import BytesIO
from chinese_tokenizer.tokenizer import Tokenizer
from sklearn.datasets import load_files
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.externals import joblib

jie_ba_tokenizer = Tokenizer().jie_ba_tokenizer

# 加载数据集
training_data = load_files('./data', encoding='utf-8')
# x_train txt内容 y_train 是类别(正 负 中 )
x_train, _, y_train, _ = train_test_split(training_data.data, training_data.target)
print('开始建模.....')
with open('training_data.target', 'w', encoding='utf-8') as f:
f.write(json.dumps(training_data.target_names))
# tokenizer参数是用来对文本进行分词的函数(就是上面我们结巴分词)
count_vect = CountVectorizer(tokenizer=jieba_tokenizer)

tfidf_transformer = TfidfTransformer()
X_train_counts = count_vect.fit_transform(x_train)

X_train_tfidf = tfidf_transformer.fit_transform(X_train_counts)
print('正在训练分类器.....')
# 多项式贝叶斯分类器训练
clf = MultinomialNB().fit(X_train_tfidf, y_train)
# 保存分类器(好在其它程序中使用)
joblib.dump(clf, 'model.pkl')
# 保存矢量化(坑在这儿!!需要使用和训练器相同的 矢量器 不然会报错!!!!!! 提示 ValueError dimension mismatch··)
joblib.dump(count_vect, 'count_vect')
print("分类器的相关信息:")
print(clf)

下面是是使用训练好的分类器分类文章: 需要分类的文章放在 predict_data 目录中:照样是一篇文章一个 txt 文件

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
# -*- coding: utf-8 -*-
# @Time : 2017/8/23 18:02
# @Author : 哎哟卧槽
# @Site :
# @File : 贝叶斯分类器.py
# @Software: PyCharm

import re
import jieba
import json
from sklearn.datasets import load_files
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.externals import joblib


# 加载分类器
clf = joblib.load('model.pkl')

count_vect = joblib.load('count_vect')
testing_data = load_files('./predict_data', encoding='utf-8')
target_names = json.loads(open('training_data.target', 'r', encoding='utf-8').read())
# # 字符串处理
tfidf_transformer = TfidfTransformer()

X_new_counts = count_vect.transform(testing_data.data)
X_new_tfidf = tfidf_transformer.fit_transform(X_new_counts)
# 进行预测
predicted = clf.predict(X_new_tfidf)
for title, category in zip(testing_data.filenames, predicted):
print('%r => %s' % (title, target_names[category]))

这个样子将训练好的分类器在新的程序中使用时候 就不报错: ValueError dimension mismatch·· 这儿有个 demo 仅供参考:GitHub 地址

Python

估摸着各位小伙伴儿被想使用 CrawlSpider 的 Rule 来抓取 JS,相当受折磨; CrawlSpider Rule 总是不能和 Splash 结合。 废话不多说,手疼····

方法 1:

写一个自定义的函数,使用 Rule 中的 process_request 参数;来替换掉 Rule 本身 Request 的逻辑。 参考官方文档: 1、将请求更换为 SplashRequest 请求: 2、每次请求将本次请求的 URL 使用 Meta 参数传递下去; 3、重写 _requests_to_follow 方法:替换响应 Response 的 URL 为我们传递的 URL(否则会格式为 Splash 的地址) 就像下面这样

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
class MySpider(CrawlSpider):

name = 'innda'

def start_requests(self):
yield SplashRequest(url, dont_process_response=True, args={'wait': 0.5}, meta={'real_url': url})

rules = (
Rule(LinkExtractor(allow=('node_\d+\.htm',)), process_request='splash_request', follow=True),
Rule(LinkExtractor(allow=('content_\d+\.htm',)), callback="one_parse")
)

def splash_request(self, request):
"""
:param request: Request对象(是一个字典;怎么取值就不说了吧!!)
:return: SplashRequest的请求
"""
# dont_process_response=True 参数表示不更改响应对象类型(默认为:HTMLResponse;更改后为:SplashTextResponse)
# args={'wait': 0.5} 表示传递等待参数0.5(Splash会渲染0.5s的时间)
# meta 传递请求的当前请求的URL
return SplashRequest(url=request.url, dont_process_response=True, args={'wait': 0.5}, meta={'real_url': request.url})

def _requests_to_follow(self, response):
"""重写的函数哈!这个函数是Rule的一个方法
:param response: 这货是啥看名字都知道了吧(这货也是个字典,然后你懂的d(・∀・*)♪゚)
:return: 追踪的Request
"""
if not isinstance(response, HtmlResponse):
return
seen = set()
# 将Response的URL更改为我们传递下来的URL
# 需要注意哈! 不能直接直接改!只能通过Response.replace这个魔术方法来改!(当然你改无所谓啦!反正会用报错来报复你 (`皿´) )并且!!!
# 敲黑板!!!!划重点!!!!!注意了!!! 这货只能赋给一个新的对象(你说变量也行,怎么说都行!(*゚∀゚)=3)
newresponse = response.replace(url=response.meta.get('real_url'))
for n, rule in enumerate(self._rules):
# 我要长一点不然有人看不见------------------------------------newresponse 看见没!别忘了改!!!
links = [lnk for lnk in rule.link_extractor.extract_links(newresponse)
if lnk not in seen]
if links and rule.process_links:
links = rule.process_links(links)
for link in links:
seen.add(link)
r = self._build_request(n, link)
yield rule.process_request(r)

def one_parse(self, response):
print(response.url)

方法 2:

这就很简单啦!干掉类型检查就是了(/≧▽≦)/ 就像这样:

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
class MySpider(CrawlSpider):

name = 'innda'

def start_requests(self):
yield SplashRequest(url, args={'wait': 0.5})

rules = (
Rule(LinkExtractor(allow=('node_\d+\.htm',)), process_request='splash_request', follow=True),
Rule(LinkExtractor(allow=('content_\d+\.htm',)), callback="one_parse")
)

def splash_request(self, request):
"""
:param request: Request对象(是一个字典;怎么取值就不说了吧!!)
:return: SplashRequest的请求
"""
# dont_process_response=True 参数表示不更改响应对象类型(默认为:HTMLResponse;更改后为:SplashTextResponse)
# args={'wait': 0.5} 表示传递等待参数0.5(Splash会渲染0.5s的时间)
# meta 传递请求的当前请求的URL
return SplashRequest(url=request.url, args={'wait': 0.5})

def _requests_to_follow(self, response):
"""重写的函数哈!这个函数是Rule的一个方法
:param response: 这货是啥看名字都知道了吧(这货也是个字典,然后你懂的d(・∀・*)♪゚)
:return: 追踪的Request
"""
# *************请注意我就是被注释注释掉的类型检查o(TωT)o 
# if not isinstance(response, HtmlResponse):
# return
# ************************************************
seen = set()
# 将Response的URL更改为我们传递下来的URL
# 需要注意哈! 不能直接直接改!只能通过Response.replace这个魔术方法来改!并且!!!
# 敲黑板!!!!划重点!!!!!注意了!!! 这货只能赋给一个新的对象(你说变量也行,怎么说都行!(*゚∀゚)=3)
# newresponse = response.replace(url=response.meta.get('real_url'))
for n, rule in enumerate(self._rules):
# 我要长一点不然有人看不见------------------------------------newresponse 看见没!别忘了改!!!
links = [lnk for lnk in rule.link_extractor.extract_links(response)
if lnk not in seen]
if links and rule.process_links:
links = rule.process_links(links)
for link in links:
seen.add(link)
r = self._build_request(n, link)
yield rule.process_request(r)

以上完毕@_@!!

Python

各位小伙儿伴儿,一定深受过采集微信公众号之苦吧!特别是!!!!!!公共号历史信息!!!这丫除了通过中间代理采集 APP、还真没什么招数能拿到数据啊! 直到············ 前天晚上微信官方发布了一个文章:点我 大致意思是说以后发布文章的时候可以直接插入其它公众号的文章了。 诶妈呀!这不是一直需要的采集接口嘛!啧啧 天助我也啊!来来·········下面大致的说一下方法。

1、首先你需要一个订阅号! 公众号、和企业号是否可行我不清楚。因为我木有·····

2、其次你需要登录!

微信公众号登录我没仔细看。 这个暂且不说了,我使用的是 selenium 驱动浏览器获取 Cookie 的方法、来达到登录的效果。

3、使用 requests 携带 Cookie、登录获取 URL 的 token(这玩意儿很重要每一次请求都需要带上它)像下面这样:

4、使用获取到的 token、和公众号的微信号(就是数字+字符那种)、获取到公众号的 fakeid(你可以理解公众号的标识)

我们在搜索公众号的时候浏览器带着参数以 GET 方法想红框中的 URL 发起了请求。请求参数如下:

请求相应如下:

代码如下:

好了 我们再继续:

5、点击我们搜索到的公众号之后、又发现一个请求:

请求参数如下:

返回如下:

代码如下:

好了···最后一步、获取所有文章需要处理一下翻页、翻页请求如下:

我大概看了一下、极客学院每一页大概至少有 5 条信息、也就是总文章数/5 就是有多少页。但是有小数、我们取整,然后加 1 就是总页数了。

代码如下:

item.get(‘link’)就是我们需要的公众号文章连接啦!继续请求这个 URL 提取里面的内容就是啦!

以下是完整的测试代码:

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
from selenium import webdriver
import time
import json
from pprint import pprint

post = {}

driver = webdriver.Chrome(executable_path='C:\chromedriver.exe')
driver.get('https://mp.weixin.qq.com/')
time.sleep(2)
driver.find_element_by_xpath("./*//input[@id='account']").clear()
driver.find_element_by_xpath("./*//input[@id='account']").send_keys('你的帐号')
driver.find_element_by_xpath("./*//input[@id='pwd']").clear()
driver.find_element_by_xpath("./*//input[@id='pwd']").send_keys('你的密码')
# 在自动输完密码之后记得点一下记住我
time.sleep(5)
driver.find_element_by_xpath("./*//a[@id='loginBt']").click()
# 拿手机扫二维码!
time.sleep(15)
driver.get('https://mp.weixin.qq.com/')
cookie_items = driver.get_cookies()
for cookie_item in cookie_items:
post[cookie_item['name']] = cookie_item['value']
cookie_str = json.dumps(post)
with open('cookie.txt', 'w+', encoding='utf-8') as f:
f.write(cookie_str)
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
import requests
import redis
import json
import re
import random
import time

gzlist = ['yq_Butler']


url = 'https://mp.weixin.qq.com'
header = {
"HOST": "mp.weixin.qq.com",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0"
}

with open('cookie.txt', 'r', encoding='utf-8') as f:
cookie = f.read()
cookies = json.loads(cookie)
response = requests.get(url=url, cookies=cookies)
token = re.findall(r'token=(\d+)', str(response.url))[0]
for query in gzlist:
query_id = {
'action': 'search_biz',
'token' : token,
'lang': 'zh_CN',
'f': 'json',
'ajax': '1',
'random': random.random(),
'query': query,
'begin': '0',
'count': '5',
}
search_url = 'https://mp.weixin.qq.com/cgi-bin/searchbiz?'
search_response = requests.get(search_url, cookies=cookies, headers=header, params=query_id)
lists = search_response.json().get('list')[0]
fakeid = lists.get('fakeid')
query_id_data = {
'token': token,
'lang': 'zh_CN',
'f': 'json',
'ajax': '1',
'random': random.random(),
'action': 'list_ex',
'begin': '0',
'count': '5',
'query': '',
'fakeid': fakeid,
'type': '9'
}
appmsg_url = 'https://mp.weixin.qq.com/cgi-bin/appmsg?'
appmsg_response = requests.get(appmsg_url, cookies=cookies, headers=header, params=query_id_data)
max_num = appmsg_response.json().get('app_msg_cnt')
num = int(int(max_num) / 5)
begin = 0
while num + 1 > 0 :
query_id_data = {
'token': token,
'lang': 'zh_CN',
'f': 'json',
'ajax': '1',
'random': random.random(),
'action': 'list_ex',
'begin': '{}'.format(str(begin)),
'count': '5',
'query': '',
'fakeid': fakeid,
'type': '9'
}
print('翻页###################',begin)
query_fakeid_response = requests.get(appmsg_url, cookies=cookies, headers=header, params=query_id_data)
fakeid_list = query_fakeid_response.json().get('app_msg_list')
for item in fakeid_list:
print(item.get('link'))
num -= 1
begin = int(begin)
begin+=5
time.sleep(2)

以上完毕!这就是个测试、代码写得奇丑、各位将就着看啊!看不明白?没关系!看这儿:点我看视频

Python

20170609 更新:

感谢一介草民与 ftzz 的反馈

(1) 修复中文路径保存问题

(2) 修复 offset 问题

(3) 修复第一个问题

来个好玩的东西

20170607 更新:

(1) 感谢 Ftzz 提醒, 将图片替换为原图

(2) 将文件保存到本地,解决了最大的缺点问题,不用联网也可以看了

大家好,我是四毛。 写在前面的话 在开始前,给大家分享一个前段时间逛 Github 时看到的某个爬虫脚本中的内容: 所以,大家爬网站的时候,还是友善一点为好,且爬且珍惜啊。 好了,言归正传。 今天主要讲一下如何将某一个知乎问题的所有答案转换为本地 MarkDown 文件。

前期准备

python2.7 html2text markdownpad(这里随意,只要可以支持 md 就行) 会抓包。。。。。 最重要的是你要有代理,因为知乎开始封 IP 了

1.什么是 MarkDown 文件

Markdown 是一种用来写作的轻量级「标记语言」,它用简洁的语法代替排版,而不像一般我们用的字处理软件 WordPages 有大量的排版、字体设置。它使我们专心于码字,用「标记」语法,来代替常见的排版格式。例如此文从内容到格式,甚至插图,键盘就可以通通搞定了。 恩,上面是我抄的,哈哈。想多了解的可以看看这里

2.为什么要将答案转为 MarkDwon

因为。。。。。。懒,哈哈,开个玩笑。最重要的原因还是 markdown 看着比较舒服。平时写脚本的时候,也一直在思考一个问题,如何将一个文字与图片穿插的网页原始的保存下来呢。如果借助工具的话,那就很多了,CTRL+P 打印的时候,选择另存为 PDF,或者搞个印象笔记,直接保存整个网页。那么,我们如何用爬虫实现呢?正好前几天看到了这个项目,仔细研究了一下,大受启发。

3.原理

原理说起来很简单:获取请求到的内容的 BODY 部分,然后重新构建一个 HTML 文件,接着利用 html2text 这个模块将其转换为 markdown 文件,最后对图片及标题按照 markdown 的格式做一些处理就好了。目前应用的场景主要是在知乎。

4.Show Code

4.1 获取知乎答案

写代码的时候,主要考虑了两种使用场景。第一,获取某一特定答案的数据然后进行转换;第二,获取某一个问题的所有答案进行然后挨个进行转换,在这里可以 通过赞同数来对要获取的答案进行质量控制。 4.1.1、某一个特定答案的数据获取

url:https://www.zhihu.com/question/27621722/answer/48658220(前面那个是问题ID,后边的是答案ID)

这一数据的获取我这里分为了两个部分,第一部分请求上述网址,拿到答案主体数据以及赞同数,第二部分请求下面这个接口:

https://www.zhihu.com/api/v4/answers/48658220

为什么会这样?因为这个接口得到的答案正文数据不是完整数据,所以只能分两步了。 4.1.2、某一个特定答案的数据获取 这一个数据就可以通过很简单的方式得到了,接口如下:

https://www.zhihu.com/api/v4/questions/27621722/answers?sort_by=default&include=data%5B%2A%5D.is_normal%2Cis_collapsed%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Cmark_infos%2Ccreated_time%2Cupdated_time%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cupvoted_followees%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=20&offset=3

返回的都是 JSON 数据,很方便获取。但是这里有一个地方需要注意,从这里面取的答案正文数据就是文本数据,不是一个完整的 html 文件,所以需要在构造一下。 4.1.2、保存的字段

author_name 回答用户名 answer_id 答案 ID question_id 问题 ID question_title 问题 vote_up_count 赞同数 create_time 创建时间 答案主体

4.2 Code

主脚本:zhihu.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
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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Created by shimeng on 17-6-5
import os
import re
import json
import requests
import html2text
from parse_content import parse

"""
just for study and fun
Talk is cheap
show me your code
"""

class ZhiHu(object):
def __init__(self):
self.request_content = None

def request(self, url, retry_times=10):
header = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36',
'authorization': 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20',
'Host': 'www.zhihu.com'
}
times = 0
while retry_times>0:
times += 1
print 'request %s, times: %d' %(url, times)
try:
ip = 'your proxy ip'
if ip:
proxy = {
'http': 'http://%s' % ip,
'https': 'http://%s' % ip
}
self.request_content = requests.get(url, headers=header, proxies=proxy, timeout=10).content
except Exception, e:
print e
retry_times -= 1
else:
return self.request_content

def get_all_answer_content(self, question_id, flag=2):
first_url_format = 'https://www.zhihu.com/api/v4/questions/{}/answers?sort_by=default&include=data%5B%2A%5D.is_normal%2Cis_collapsed%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Cmark_infos%2Ccreated_time%2Cupdated_time%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cupvoted_followees%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=20&offset=3'
first_url = first_url_format.format(question_id)
response = self.request(first_url)
if response:
contents = json.loads(response)
print contents.get('paging').get('is_end')
while not contents.get('paging').get('is_end'):
for content in contents.get('data'):
self.parse_content(content, flag)
next_page_url = contents.get('paging').get('next').replace('http', 'https')
contents = json.loads(self.request(next_page_url))
else:
raise ValueError('request failed, quit......')

def get_single_answer_content(self, answer_url, flag=1):
all_content = {}
question_id, answer_id = re.findall('https://www.zhihu.com/question/(\d+)/answer/(\d+)', answer_url)[0]

html_content = self.request(answer_url)
if html_content:
all_content['main_content'] = html_content
else:
raise ValueError('request failed, quit......')

ajax_answer_url = 'https://www.zhihu.com/api/v4/answers/{}'.format(answer_id)
ajax_content = self.request(ajax_answer_url)
if ajax_content:
all_content['ajax_content'] = json.loads(ajax_content)
else:
raise ValueError('request failed, quit......')

self.parse_content(all_content, flag, )

def parse_content(self, content, flag=None):
data = parse(content, flag)
self.transform_to_markdown(data)

def transform_to_markdown(self, data):
content = data['content']
author_name = data['author_name']
answer_id = data['answer_id']
question_id = data['question_id']
question_title = data['question_title']
vote_up_count = data['vote_up_count']
create_time = data['create_time']

file_name = u'%s--%s的回答[%d].md' % (question_title, author_name,answer_id)
folder_name = u'%s' % (question_title)

if not os.path.exists(os.path.join(os.getcwd(),folder_name)):
os.mkdir(folder_name)
os.chdir(folder_name)

f = open(file_name, "wt")
f.write("-" * 40 + "\n")
origin_url = 'https://www.zhihu.com/question/{}/answer/{}'.format(question_id, answer_id)
f.write("## 本答案原始链接: " + origin_url + "\n")
f.write("### question_title: " + question_title.encode('utf-8') + "\n")
f.write("### Author_Name: " + author_name.encode('utf-8') + "\n")
f.write("### Answer_ID: %d" % answer_id + "\n")
f.write("### Question_ID %d: " % question_id + "\n")
f.write("### VoteCount: %s" % vote_up_count + "\n")
f.write("### Create_Time: " + create_time + "\n")
f.write("-" * 40 + "\n")

text = html2text.html2text(content.decode('utf-8')).encode("utf-8")
# 标题
r = re.findall(r'**(.*?)**', text, re.S)
for i in r:
if i != " ":
text = text.replace(i, i.strip())

r = re.findall(r'_(.*)_', text)
for i in r:
if i != " ":
text = text.replace(i, i.strip())
text = text.replace('_ _', '')

# 图片
r = re.findall(r'![]\((?:.*?)\)', text)
for i in r:
text = text.replace(i, i + "\n\n")

f.write(text)

f.close()


if __name__ == '__main__':
zhihu = ZhiHu()
url = 'https://www.zhihu.com/question/27621722/answer/105331078'
zhihu.get_single_answer_content(url)

# question_id = '27621722'
# zhihu.get_all_answer_content(question_id)

zhihu.py 为主脚本,内容很简单,发起请求,调用解析函数进行解析,最后再进行保存。 解析函数脚本:parse_content.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
78
79
80
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Created by shimeng on 17-6-5
import time
from bs4 import BeautifulSoup


def html_template(data):
# api content
html = '''
<html>
<head>
<body>
%s
</body>
</head>
</html>
''' % data
return html


def parse(content, flag=None):
data = {}
if flag == 1:
# single
main_content = content.get('main_content')
ajax_content = content.get('ajax_content')

soup = BeautifulSoup(main_content.decode("utf-8"), "lxml")
answer = soup.find("span", class_="RichText CopyrightRichText-richText")

author_name = ajax_content.get('author').get('name')
answer_id = ajax_content.get('id')
question_id = ajax_content.get('question').get('id')
question_title = ajax_content.get('question').get('title')
vote_up_count = soup.find("meta", itemprop="upvoteCount")["content"]
create_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ajax_content.get('created_time')))


else:
# all
answer_content = content.get('content')

author_name = content.get('author').get('name')
answer_id = content.get('id')
question_id = content.get('question').get('id')
question_title = content.get('question').get('title')
vote_up_count = content.get('voteup_count')
create_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(content.get('created_time')))

content = html_template(answer_content)
soup = BeautifulSoup(content, 'lxml')
answer = soup.find("body")

print author_name,answer_id,question_id,question_title,vote_up_count,create_time
# 这里非原创,看了别人的代码,修改了一下
soup.body.extract()
soup.head.insert_after(soup.new_tag("body", **{'class': 'zhi'}))

soup.body.append(answer)

img_list = soup.find_all("img", class_="content_image lazy")
for img in img_list:
img["src"] = img["data-actualsrc"]
img_list = soup.find_all("img", class_="origin_image zh-lightbox-thumb lazy")
for img in img_list:
img["src"] = img["data-actualsrc"]
noscript_list = soup.find_all("noscript")
for noscript in noscript_list:
noscript.extract()

data['content'] = soup
data['author_name'] = author_name
data['answer_id'] = answer_id
data['question_id'] = question_id
data['question_title'] = question_title
data['vote_up_count'] = vote_up_count
data['create_time'] = create_time

return data

parse_content.py 主要负责构造新的 html,然后对其进行解析,获取数据。

5.测试结果展示

恩,下面还有,就不截图了。

6.缺点与不足

下面聊一聊这种方法的缺点: 这种方法的最大缺点就是:

一定要联网!

一定要联网!

一定要联网!

因为。。。。。。 在 md 文件中我们只是写了个图片的网址,这就意味着 markdown 的编辑器帮我们去存放图片的服务器上对这个图片进行了获取,所以断网也就意味着你看不到图片了;同时也意味着如果用户删除了这张图片,你也就看不到了。 但是,后来我又发现在 markdownpad 中将文件导出为 html 时,即使是断网了,依然可以看到全部的内容,包括图片,所以如果你真的喜欢某一个答案,保存到印象笔记肯定是不错的选择,PDF 直接保存也不错,如果是使用了这个方法,记得转为 html 最好。 还有一个缺点就是 html2text 转换过后的效果其实并不是特别好,还是需要后期在进行处理的。

7.总结

代码还有很多可以改进之处,欢迎大家与我交流:QQ:549411552 (注明来自静觅) 国际惯例:代码在这 收工。

Python

我们尝试维护过一个免费的代理池,但是代理池效果用过就知道了,毕竟里面有大量免费代理,虽然这些代理是可用的,但是既然我们能刷到这个免费代理,别人也能呀,所以就导致这个代理同时被很多人使用来抓取网站,所以当我们兴致勃勃地拿他来抓取某个网站的时候,会发现它还是被网站封禁的状态,所以在某些情况下免费代理池的成功率还是比较低的。 当然我们也可以去购买一些代理,比如几块钱提取几百几千个的代理,然而经过测试后质量也是很一般,也可以去购买专线代理,不过价格也是不菲的。那么目前最稳定而且又保证可用的代理方法就是设置ADSL拨号代理了。 本篇来讲解一下ADSL拨号代理服务器的相关设置。

什么是ADSL

大家可能对ADSL比较陌生,ADSL全称叫做Asymmetric Digital Subscriber Line,非对称数字用户环路,因为它的上行和下行带宽不对称。它采用频分复用技术把普通的电话线分成了电话、上行和下行三个相对独立的信道,从而避免了相互之间的干扰。 有种主机叫做动态拨号VPS主机,这种主机在连接上网的时候是需要拨号的,只有拨号成功后才可以上网,每拨一次号,主机就会获取一个新的IP,也就是它的IP并不是固定的,而且IP量特别大,几乎不会拨到相同的IP,如果我们用它来搭建代理,既能保证高度可用,又可以自由控制拨号切换。 经测试发现这也是最稳定最有效的代理方式,本节详细介绍一下ADSL拨号代理服务器的搭建方法。

购买动态拨号VPS主机

所以在开始之前,我们需要先购买一台动态拨号VPS主机,这样的主机在百度搜索一下,服务商还是相当多的,在这里推荐一家云立方,感觉还是比较良心的,非广告。 配置的话可以自行选择,看下带宽是否可以满足需求就好了。 购买完成之后,就需要安装操作系统了,进入拨号主机的后台,首先预装一个操作系统。 在这里推荐安装CentOS7系统。 然后找到远程管理面板找到远程连接的用户名和密码,也就是SSH远程连接服务器的信息。 比如我这边的IP端口分别是 153.36.65.214:20063,用户名是root。 命令行下输入:

1
ssh root@153.36.65.214 -p 20063

然后输入管理密码,就可以连接上远程服务器了。 进入之后,可以发现有一个可用的脚本文件,叫做ppp.sh,这是拨号初始化的脚本,运行它会让我们输入拨号的用户名和密码,然后它就会开始各种拨号配置,一次配置成功,后面的拨号就不需要重复输入用户名和密码了。 运行ppp.sh脚本,输入用户名密码等待它的配置完成。 都提示成功之后就可以进行拨号了。 在拨号之前如果我们测试ping任何网站都是不通的,因为当前网络还没联通,输入拨号命令:

1
adsl-start

可以发现拨号命令成功运行,没有任何报错信息,这就证明拨号成功完成了,耗时约几秒钟。接下来如果再去ping外网就可以通了。 如果要停止拨号可以输入:

1
adsl-stop

停止之后,可以发现又连不通网络了。

所以只有拨号之后才可以建立网络连接。 所以断线重播的命令就是二者组合起来,先执行adsl-stop再执行adsl-start,每拨一次号,ifocnfig命令观察一下主机的IP,发现主机的IP一直是在变化的,网卡名称叫做ppp0。 所以,到这里我们就可以知道它作为代理服务器的巨大优势了,如果将这台主机作为代理服务器,如果我们一直拨号换IP,就不怕遇到IP被封的情况了,即使某个IP被封了,重新拨一次号就好了。 所以接下来我们要做的就有两件事,一是怎样将主机设置为代理服务器,二是怎样实时获取拨号主机的IP。

设置代理服务器

之前我们经常听说代理服务器,也设置过不少代理了,但是可能没有自己设置吧,自己有一台主机怎样设置为代理服务器呢?接下来我们就亲自试验下怎样搭建HTTP代理服务器。 在Linux下搭建HTTP代理服务器,推荐TinyProxy和Squid,配置都非常简单,在这里我们以TinyProxy为例来讲解一下怎样搭建代理服务器。

安装TinyProxy

当然第一步就是安装TinyProxy这个软件了,在这里我使用的系统是CentOS,所以使用yum来安装,如果是其他系统如Ubuntu可以选择apt-get等命令安装,都是类似的。 命令行执行yum安装指令:

1
2
3
yum install -y epel-release
yum update -y
yum install -y tinyproxy

运行完成之后就可以完成tinyproxy的安装了。

配置TinyProxy

安装完成之后还需要配置一下TinyProxy才可以用作代理服务器,需要编辑配置文件,它一般的路径是/etc/tinyproxy/tinyproxy.conf。 可以看到有一行

1
Port 8888

在这里可以设置代理的端口,默认是8888。 然后继续向下找,有这么一行

1
Allow 127.0.0.1

这是被允许连接的主机的IP,如果想任何主机都可以连接,那就直接将它注释即可,所以在这里我们选择直接注释,也就是任何主机都可以使用这台主机作为代理服务器了。 修改为

1
# Allow 127.0.0.1

设置完成之后重启TinyProxy即可。

1
service tinyproxy start

验证TinyProxy 好了,这样我们就成功搭建好代理服务器了,首先ifconfig查看下当前主机的IP,比如当前我的主机拨号IP为112.84.118.216,在其他的主机运行测试一下。 比如用curl命令设置代理请求一下httpbin,检测下代理是否生效。

1
curl -x 112.84.118.216:8888 httpbin.org/get

如果有正常的结果输出并且origin的值为代理IP的地址,就证明TinyProxy配置成功了。 好,那到现在,我们接下来要做的就是需要动态实时获取主机的IP了。

动态获取IP

真正的好戏才开始呢,我们怎样动态获取主机的IP呢?可能你首先想到的是DDNS也就是动态域名解析服务,我们需要使用一个域名来解析,也就是虽然IP是变的,但域名解析的地址可以随着IP的变化而变化。 它的原理其实是拨号主机向固定的服务器发出请求,服务器获取客户端的IP,然后再将域名解析到这个IP上就可以了。 国内比较有名的服务就是花生壳了,也提供了免费版的动态域名解析,另外DNSPOD也提供了解析接口来动态修改域名解析设置,DNSPOD,但是这样的方式都有一个通病,那就是慢! 原因在于DNS修改后到完全生效是需要一定时间的,所以如果在前一秒拨号了,这一秒的域名解析的可能还是原来的IP,时间长的话可能需要几分钟,也就是说这段时间内,服务器IP已经变了,但是域名还是上一次拨号的IP,所以代理是不能用的,对于爬虫这种秒级响应的需求,是完全不能接受的。 所以根据花生壳的原理,可以完全自己实现一下动态获取IP的方法。 所以本节重点介绍的就是怎样来实现实时获取拨号主机IP的方法。 要实现这个需要两台主机,一台主机就是这台动态拨号VPS主机,另一台是具有固定公网IP的主机。动态VPS主机拨号成功之后就请求远程的固定主机,远程主机获取动态VPS主机的IP,就可以得到这个代理,将代理保存下来,这样拨号主机每拨号一次,远程主机就会及时得到拨号主机的IP,如果有多台拨号VPS,也统一发送到远程主机,这样我们只需要从远程主机取下代理就好了,保准是实时可用,稳定高效的。 整体思路大体是这样子,当然为了更完善一下,我们要做到如下功能: 远程主机:

  • 监听主机请求,获取动态VPS主机IP
  • 将VPS主机IP记录下来存入数据库,支持多个客户端
  • 检测当前接收到的IP可用情况,如果不可用则删除
  • 提供API接口,通过API接口可获取当前可用代理IP

拨号VPS:

  • 定时执行拨号脚本换IP
  • 换IP后立即请求远程主机
  • 拨号后检测是否拨号成功,如果失败立即重新拨号

远程主机实现

说了这么多,那么我们就梳理一下具体的实现吧,整个项目我们用Python3实现。

数据库

远程主机作为一台服务器,动态拨号VPS会定时请求远程主机,远程主机接收到请求后将IP记录下来存入数据库。 因为IP是一直在变化的,IP更新了之后,原来的IP就不能用了,所以对于一个主机来说我们可能需要多次更新一条数据。另外我们不能仅限于维护一台拨号VPS主机,当然是需要支持多台维护的。在这里我们直接选用Key-Value形式的非关系型数据库存储更加方便,所以在此选用Redis数据库。 既然是Key-Value,Key是什么?Value是什么?首先我们能确定Value就是代理的值,比如112.84.119.67:8888,那么Key是什么?我们知道,这个IP是针对一台动态拨号VPS的,而且这个值会不断地变,所以我们需要有一个不变量Key来唯一标识这台主机,所以在这里我们可以把Key当做主机名称。名称怎么来?自己取就好了,只要每台主机的名字不重复,我们就可以区分出是哪台主机了,这个名字可以在拨号主机那边指定,然后传给远程主机就好了。 所以,在这里数据库我们选用Redis,Key就是拨号主机的名称,可以自己指定,Value就是代理的值。 所以可以写一个操作Redis数据库的类,参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class RedisClient(object):
def __init__(self, host=REDIS_HOST, port=REDIS_PORT):
self.db = redis.Redis(host=host, port=port, password=REDIS_PASSWORD)
self.proxy_key = PROXY_KEY

def key(self, name):
return '{key}:{name}'.format(key=self.proxy_key, name=name)

def set(self, name, proxy):
return self.db.set(self.key(name), proxy)

def get(self, name):
return self.db.get(self.key(name)).decode('utf-8')

首先初始化Redis连接,我们可以将Key设计成adsl:vm1这种形式,冒号前面是总的key,冒号后面是主机名称name,这样显得结构更加清晰。 然后指定set()和get()方法,用来存储代理和获取代理。

请求处理

拨号主机会一直向远程主机发送请求,远程主机当然可以获取拨号主机的IP,但是代理端口是无法获得的,我们在拨号主机上设置了TinyProxy或者Squid,但是服务器不知道是在哪个端口开的,所以端口也是需要客户端传给远程主机的。远程主机接收到请求后,将解析得到的IP和端口合并就可以作为完整的代理保存了。 所以现在我们知道拨号主机需要传送给远程主机的信息已经有两个了,一是拨号主机本身的名称,二是代理的端口。

通信秘钥

为了保证远程主机不被恶意的请求干扰,可以设置一个传输秘钥,最简单的方式可以二者共同规定一个秘钥字符串,拨号主机在传送这个字符串,远程主机匹配一下,如果能正确匹配,那就进行下一步的处理,如果不能匹配,那么可能是恶意请求,就忽略这个请求。 当然肯定有更好的加密传输方式,但为了方便起见可以用如上来做。 所以客户机还需要传送一个数据,那就是通信秘钥,一共需要传送三个数据。 所以我们需要架设一个服务器,一直监听客户端的请求,在这里我们用tornado实现。 tornado的安装也非常简单,利用pip安装即可:

1
pip3 install tornado

定义一个处理拨号主机请求的方法,在这里我们使用post请求,参考如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def post(self):
token = self.get_body_argument('token', default=None, strip=False)
port = self.get_body_argument('port', default=None, strip=False)
name = self.get_body_argument('name', default=None, strip=False)
if token == TOKEN and port:
ip = self.request.remote_ip
proxy = ip + ':' + port
print('Receive proxy', proxy)
self.redis.set(name, proxy)
self.test_proxies()
elif token != TOKEN:
self.write('Wrong Token')
elif not port:
self.write('No Client Port')

远程主机获取请求的token,也就是上面我们所说的通信密钥,保证安全。port是拨号机的代理端口,name是拨号主机的名称。然后我们再获取请求的remote_ip,也就是拨号主机的IP。然后将IP和端口拼合就可以得到拨号主机的完整代理信息了,将其存入数据库即可。

代理检测

在远程主机端我们需要做一下代理检测,如果某个代理不可用了,会及时将其去除,以免出现获取到代理后不可用的情况。

注意:在这里在拨号主机端验证是不够的,因为可能突然遇到某个拨号主机宕机的情况,这样拨号主机就不会再向远程主机发送请求,而最后一次得到的代理还会存在于数据库中,所以在远程主机端统一验证比较科学。

验证方式可以定时检测,也可以每收到一次请求检测一次,用获取到的代理来请求某个网站,检测一下是否能访问即可。如果不能,将其从数据库中删除。

API

远程主机已经将拨号主机的IP和端口保存下来了,那也就是说,所有的可用的代理已经在远程主机保存了,我们需要提供一个接口来将代理获取下来。 比如我们可以提供这么几个方法,获取所有代理,获取最新代理,获取随机代理等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def all(self):
keys = self.keys()
proxies = [{'name': key, 'proxy': self.get(key)} for key in keys]
return proxies

def random(self):
items = self.all()
return random.choice(items).get('proxy')

def list(self):
keys = self.keys()
proxies = [self.get(key) for key in keys]
return proxies

def first(self):
return self.get(self.keys()[0])

然后用tornado搭建API服务,如果可以的话还可以绑定一个域名,更加便捷,举例如下: 获取随机代理: 获取最新代理: 获取所有代理: 请求接口获取可用代理即可,比如获取一个随机代理:

1
2
3
4
5
6
7
8
9
import requests

def get_random_proxy():
try:
# 远程主机的服务地址
url = 'http://xxx.xxx.xxx.xxx:8000/random'
return requests.get(url).text
except requests.exceptions.ConnectionError:
return None

这样我们拿到的IP都是稳定可用的,而且过段时间重新请求取到的IP就会变化,是一直动态变化的高可用代理。

拨号VPS实现

定时拨号

拨号VPS需要每隔一段时间就拨号一次,我们可以直接执行命令行来拨号,那在Python里我们只需要调用一下这个拨号命令就好了。利用subprocess模块调用脚本即可,在这里定义一个变量ADSL_BASH为adsl-stop;adsl-start,这就是拨号的脚本。

1
2
import subprocess
(status, output) = subprocess.getstatusoutput(ADSL_BASH)

通过getstatusoutput方法可以获取脚本的执行状态和输出结果,如果status为0,则证明拨号成功,然后检测一下拨号接口是否获取了IP地址。 执行ifconfig命令可以获取当前的IP,我这台主机接口名称叫做ppp0,当然网卡名称可以自己指定,所以将ppp0接口的IP提取出来即可。

1
2
3
4
5
6
7
8
def get_ip(self, ifname=ADSL_IFNAME):
(status, output) = subprocess.getstatusoutput('ifconfig')
if status == 0:
pattern = re.compile(ifname + '.*?inet.*?(\d+\.\d+\.\d+\.\d+).*?netmask', re.S)
result = re.search(pattern, output)
if result:
ip = result.group(1)
return ip

如果方法正常返回IP,则证明IP存在,拨号成功,接下来向远程主机发送请求即可,然后sleep一段时间重新再次拨号。 如果方法返回的值为空,那证明IP不存在,我们需要重新拨号。

请求远程主机

发送的时候需要携带这么几个信息,一个是通信秘钥,一个是代理端口,另一个是主机的标识符,用requests发送即可。

1
requests.post(SERVER_URL, data={'token': TOKEN, 'port': PROXY_PORT, 'name': CLIENT_NAME})

所以整体的思路实现可以写成这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def adsl(self):
while True:
print('ADSL Start, Please wait')
(status, output) = subprocess.getstatusoutput(ADSL_BASH)
if status == 0:
print('ADSL Successfully')
ip = self.get_ip()
if ip:
print('New IP', ip)
try:
requests.post(SERVER_URL, data={'token': TOKEN, 'port': PROXY_PORT, 'name': CLIENT_NAME})
print('Successfully Sent to Server', SERVER_URL)
except ConnectionError:
print('Failed to Connect Server', SERVER_URL)
time.sleep(ADSL_CYCLE)
else:
print('Get IP Failed')
else:
print('ADSL Failed, Please Check')
time.sleep(1)

这样我们就可以做到定时拨号并向远程主机发送请求了。

代码

Talk is cheap, show me the code! 在这里提供一份完整代码实现,其中client模块是在动态VPS主机运行,server模块在远程主机运行,具体的操作使用可以参考README。 ADSLProxyPool

Python

现在维护着一个新浪微博爬虫,爬取量已经5亿+,使用了Scrapyd部署分布式。 Scrapyd运行时会输出日志到本地,导致日志文件会越来越大,这个其实就是Scrapy控制台的输出。但是这个日志其实有用的部分也就是最后那几百行而已,如果出错,去日志查看下出错信息就好了。 所以现在可以写一个脚本,来定时更新日志文件,将最后的100行保存下来就好了。 Scrapyd默认的日志目录是在用户文件夹下的logs目录。 所以在这里我们指定dir=~/logs 新建bash脚本,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh

clean() {
for file in $1/*
do
if [ -d $file ]
then
clean $file
else
echo $file
temp=$(tail -100 $file)
echo "$temp" > $file
fi
done
}

dir=~/logs
clean $dir

新建这样的一个脚本,然后命名为 clean.sh,我的直接放在了用户文件夹下。 然后crontab创建定时任务。 执行

1
crontab -e

我们想要一分钟清理一次日志文件。 输入

1
*/1 * * * * /bin/sh ~/clean.sh

然后退出之后,crontab就可以每隔一分钟执行一次clean.sh,清理日志了。 这样我们就不怕日志文件大量占用主机空间啦~

Python

我的 GITHUB 地址:https://github.com/xiaosimao/weibo_spider 2017.05.04 更新: 感谢哥本哈根小树对于获取 containnerid 的指教,多谢。

大家好,我是新人四毛,大家可以叫我小四毛,至于为什么,在家排行老四,农村人,就是那么任性。

好,自我介绍完毕,开始今天的学(zhuang)习(bi)之路。

说明:本文针对的是有一些爬虫基础的同学,所以看不太懂的同学先补一下基础。

本文的全部代码并没有上传到 GITHUB 中,而且本文的 code 部分给出的代码也是指导性的,大部分还是要靠大家自己动手完成。待后几篇博客出来以后,代码会放到上面。

大家如果有问题交流的话,欢迎在下面进行评论,或者可以加我 QQ:549411552(加的话麻烦注明来自静觅),欢迎大佬拍砖指错,大家共同进步。

前几天,大才发布了一个视频,主要讲的是通过维护一个新浪微博 Cookies 池,抓取新浪微博的相关数据,爬取的站点是 weibo.cn。相关的代码在大才的 Github 里【大才的视频教程真的很用心,视频高清无码,希望大家可以支持大才,毕竟写了那么多精彩的教程真心不易】。

然而,如果你只是想简单的搞点数据,对技术一点兴趣都没有,又或者某宝搜来搜去都没有买到账号,又或者装个模拟登陆需要的模块都想跳楼,有没有除此之外其他的办法呢?你有没有想过在免登陆的情况下就可以获得你想要的数据呢?如果你这么想过而又没有做出来,那么接下来,让我们一起搞(qi)事(fei)吧。

本文重点提供解决问题的思路,会把最关键的点标示出来,代码基本没有。有什么不对或不足之处,还望大家指出,共同进步。

1.前期准备

代理 IP。虽说本文介绍的方法不需要 Cookies,但是代理 IP 还是需要的,要不然也是被新浪分分钟的 403(我测试的时候会出现)。如果你连 403 都不知道是什么,那么还是去看看大才的爬虫基础课程,或者不想看文字的话直接来报大才的视频课程课,哈哈(大才,今晚得加两个菜啊,我这吆喝的)。

2.思路分析

一般做爬虫爬取网站,首选的都是 m 站,其次是 wap 站,最后考虑 PC 站。当然,这不是绝对的,有的时候 PC 站的信息最全,而你又恰好需要全部的信息,那么 PC 站是你的首选。一般 m 站都以 m 开头后接域名,试一下 就好了,实在找不到,上网搜。

所以本文开搞的网址就是 m.weibo.cn。但是当你在浏览器中输入这个网址时,你得到的应该是下面这个页面,如果不是,说明你的浏览器保留了你最近登录微博的 cookie,这个时候,清空浏览器保存的数据,再次打开这个网页,就应该也是这个界面了:

我滴天,是的,你没看错,就是这个登录界面。你不是说不需要登录吗?怎么 TM 的还是这个万恶的界面?怎么破?WTF?

哈哈,其实一开始我也不知道,后来经人指点,才发现只要在后面加入一些东西之后就不会看到这个界面了。那么是什么呢?

当当当当!!!!!!!!!!

http://m.weibo.cn/u/1713926427

当你看到这个网址的时候,憋说话,一定要用心去感受,这个时候说话你的嘴都是咧着的,别问我为什么知道,我就是知道。

用心去感受,真的。

对了,上面网址最后的数字是博主的数字 ID,在 weibo.com 的源码里可以找到,这里不做说明了。

打开上述网址, 界面变成这个样子,是不是很厉害的样子(大手勿喷),拨云见日,对于老手来说,下面的他们就可以不看了,可以去抓包写代码了,但是对于一头雾水的小伙伴请接着往下看:

这就是本文爬虫的入口,没错,就说牛逼的榜姐,入口选一些质量高的,比如你想爬新闻方面信息,那么你就去找澎湃新闻,新浪新闻之类的。

通过该入口,我们可以抓取该博主的所有微博及评论信息,以及该博主关注的人的微博及评论信息,依次往后,循环不断。

在这里谈一点经验:

其实做爬虫,最基础的当然是写代码的能力,抓包什么的都不是什么困难的事,抓包很简单很简单。我觉得最难的是找到入口,找到一个最适合的入口。怎么定义这个最适合呢?就是要去尝试,依照一般的顺序,先找找 M 站,再找找 wap 站,最后再去看 PC 站,找到一个合适的入口,往往会事半功倍。前几天抓取途牛网的相关游记信息,爬 PC 站分分钟的 302,但是爬 M 站,全是接口,全程无阻。

因大多数人都是采集微博信息以及评论信息,所以下面将以这两方面为主。

剧透一下,在这里可以抓到的信息:

(1) 博主信息 (没发现有价值的信息,下面抓包过程不讲)

(2) 博主微博信息(下文抓包讲解)

(3) 微博评论信息(下文抓包讲解)

(4) 热门微博信息(小时榜,日榜,周榜,月榜)(下文抓包未讲解,大家可以摸索一下)

。。。。。。还有很多我没有细看,等待各位细细研究吧。

3. 抓包分析

首先,得会抓包,一般的浏览器的 Network 够用了。

(1) 微博正文抓包

点击 上图中的微博然后往下拉,抓包出现下图:

分析:

可以看到,服务器返回的数据为 json 格式,这个是做爬虫的最喜欢的了。返回的数据包括很多的字段,图中也以及做了标示,相信大家都能看的懂,看不懂那也没办法了。

最后放上抓包的数据:

  1. Request URL:

    http://m.weibo.cn/api/container/getIndex?type=uid&value=1713926427&containerid=1076031713926427&page=2

  2. Request Method:

    GET

  3. Query String Parameters

    type: uid

    value: 1713926427

    containerid: 1076031713926427

    page: 2

(2) 微博评论抓包

单击微博内容,就可以抓包成功,如下图:

分析:

从上面可以看出,这里的数据依然还是很好获取的。

最后放上抓包的数据:

  1. Request URL:

    http://m.weibo.cn/api/comments/show?id=4103388327019042&page=1

  2. Request Method:

    GET

  3. Query String Parameters

    id: 4103388327019042

    page: 1

再次分析:

通过抓包的数据可以发现,获取微博评论必须首先获得这条微博的 ID。所以,目前还是要对微博正文的抓包过程进行分析。

4. 思路解析

在上面的微博正文中发现需要提交以下数据:

type: uid

value: 1713926427

containerid: 1076031713926427

page: 2

其中:type(固定值)、value(博主微博 ID)、containerid(意义不明确,但是带了个 id 在里面,应该代表的是一个唯一性的一个标识)、page(页码)。页码在返回的数据中可以获得。

那么分析到这里,containerid 就是我们要找的最重要的信息。这个字段信息是不会凭空出现的,肯定产生于某一个请求之中,所以这时候,我们再回到开头,回到我们的初始。刷新入口网址,抓包发现了下面 3 个网址,见下图:

分析:

这 3 个网址的格式一模一样,所以点进去看一下里面到底什么情况。

下面的先点开网址 1看看:

分析:

从返回的数据中,可以看到第 1 个网址的主要内容为 user_Info,即博主的个人信息,相关的字段在图中已经标示出来。最令人惊喜的是查找我们需要的 containerid 时,发现数据竟然就在其中,那么可以肯定我们需要的 containerid 就是在这个请求的返回值中,那么问题再次出现,这个请求的网址中又出现了一个 containerid,我们似乎又回到了原点,而且在用户的首页抓包中,在这个请求之前,也没有什么有意义的请求了,到这里是不是就进入死胡同了呢?其实不然,在这里我们就要进行多方面的尝试了,当我们将第一个网址中的 containerid 删掉以后,重新请求一次,发现返回的依然是这些数据,具体见下图:

分析:

而当我将第三个网址,也就是微博正文的网址中的 containerid 去掉后,返回的数据就是博主的个人信息了,而不是我们需要的微博正文,所以可以肯定第一个网址中的 containerid 并不是必须的,而对于网址 3,这个字段则是必须的。

为了让这个爬虫可以顺着一个初始用户爬取到其他用户的相关信息,甚至全网的信息,那么我们就需要让爬虫自己去添加待爬任务。本文选择的初始用户有 3000 多万的粉丝数,就是人们常说的微博大 V。在做这一类的信息爬取时,我们往往关注的是数据的质量,所以我们选择初始用户的关注用户作为下一级的用户。在下一级中,这些用户将被作为初始用户。这样周而复始,最理想的情况当然就是可以把微博全站的质量还不错的博主的微博以及下面的评论都抓取了。但是在实际的操作过程中会发现微博的用户质量真的是参差不齐,所以我们在筛选后面的用户时,可以加一些限制条件,如用户的粉丝数等等。在这里找寻初始用户关注用户信息的这一过程就省略了,留给大家探索一下,很简单。

所以到这里,我们的整个流程就理清了(单个博主,如需循环,则只需要找到下一级用户的 ID 即可,相信这对于聪明的大家肯定不难的):

请求用户主页网址—>得到 containerid,请求微博正文网址—>保存博文相关信息,取出博文 ID,请求评论网址—>得到评论信息

5. CODE TIME

思路已经理清了,那么下面就是 CODE TIME 了,毕竟:

TALK IS CHEAP,SHOW ME YOUR CODE

本文采用 scrapy 编写,重写个 proxy 中间件,即可实现每一个 request 带一个随机 IP,减少被封禁的概率,同时尽量把重试的次数设置大一些。

想要保存哪些信息,根据自身的业务需求而定,具体的信息,能找到的都可以在每一个请求返回的内容中找到,都是 json 格式的,所以这里的代码只是将上面讲的流程实现了一遍,其他的都没有实现。

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
# -*- coding: utf-8 -*-
import scrapy
import json

class SinaSpider(scrapy.Spider):
name = "sina"
allowed_domains = ["m.weibo.cn"]
# root id
first_id = '1713926427'

def start_requests(self):
# to get containerid
url = 'http://m.weibo.cn/api/container/getIndex?type=uid&value={}'.format(self.first_id)
yield scrapy.Request(url=url, callback=self.get_containerid)

def get_containerid(self,response):
content = json.loads(response.body)
# here, we can get containerid
containerid = None
for data in content.get('tabsInfo').get('tabs'):
if data.get('tab_type') == 'weibo':
containerid = data.get('containerid')
print 'weibo request url containerid is %s' % containerid

# construct the wei bo request url
if containerid:
weibo_url = response.url + '&containerid=%s'%containerid
yield scrapy.Request(url=weibo_url, callback=self.get_weibo_id)
else:
print 'sorry, do not get containerid'

def get_weibo_id(self, response):
content = json.loads(response.body)
# get weibo id ,you can also save some other data if you need
for data in content.get('cards'):
if data.get('card_type') == 9:
single_weibo_id = data.get('mblog').get('id')
print single_weibo_id
# here ,if you want to get comment info ,you can construct the comment url just the same as wei bo url

6.总结

本文写到这里就算结束了,我一直信奉授人以鱼不如授人以渔,在这篇文章中,我并没有把全部的代码展示出来,而是通过分析的过程来让大家知道怎么去处理这类问题,在文中也留了好几个可以让大家发挥的地方,如用户关注用户怎么获取?按照关键词搜索的信息怎么抓取?等等。我相信大家通过一步步的抓包以及分析一定可以解决这些问题的。这些问题,在以后的博客中我也会继续更新的。

第一次写这样的博客,感觉还是驾驭不了,还是得多多练习。写博客真的很累,向大才致敬,感谢他无私的为我们奉献了这么多精彩的教程。

Python

PS: 爬虫不进入 img_url 函数的小伙伴儿 请尝试将将代码复制到你新建的 py 文件中。 2017/8/30 更新解决了网站防盗链导致下载图片失败的问题 这几天一直有小伙伴而给我吐槽说,由于妹子图站长把www.mzitu.com/all这个地址取消了。导致原来的那个采集爬虫不能用啦。 正好也有小伙伴儿问 Scrapy 中的图片下载管道是怎么用的。 就凑合在一起把 mzitu.com 给重新写了一下。 首先确保你的 Python 环境已安装 Scrapy!!!!!!!! 命令行下进入你需要存放项目的目录并创建项目: 比如我放在了 D:\PycharmProjects

1
2
3
D:
cd PycharmProjects
scrapy startproject mzitu_scrapy

我是 Windows!其余系统的伙伴儿自己看着办哈。 这都不会的小伙伴儿,快去洗洗睡吧。养足了精神从头看一遍教程哈! 在 PyCharm 中打开我们的项目目录。 在 mzitu_scrapy 目录创建 run.py。写入以下内容:

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

其中的 mzitu 就为待会儿 spider.py 文件中的 name 属性。这点请务必记住哦!不然是跑不起来的。 在 mzitu_scrapy\spider 目录中创建 spider.py。文件作为爬虫文件。 好了!现在我们来想想,怎么来抓 mzitu.com 了。 首先我们的目标是当然是全站的妹子图片!!! 但是问题来了,站长把之前那个 mzitu.com\all 这个 URL 地址给取消了,我们没办法弄到全部的套图地址了! 我们可以去仔细观察一下站点所有套图的地址都是:http://www.mzitu.com/几位数字结尾的。 这种格式地址。 有木有小伙伴儿想到了啥? CrawlSpider !!!就是这玩儿!! 有了它我们就能追踪“http://www.mzitu.com/几位数字结尾的”这种格式的URL了。 Go Go Go Go!开始搞事。 首先在 item.py 中新建我们需要的字段。我们需要啥?我们需要套图的名字和图片地址!! 那我们新建三个字段:

1
2
3
4
5
6
7
8
9
10
import scrapy


class MzituScrapyItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
name = scrapy.Field()
image_urls = scrapy.Field()
url = scrapy.Field()
pass

第一步完成啦!开始写 spider.py 啦! 首先导入我们需要的包:

1
2
3
4
from scrapy import Request
from scrapy.spider import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from mzitu_scrapy.items import MzituScrapyItem

都是干啥的我不说了哈!不知道的小伙伴儿自己去翻翻官方文档。 接下来是:

1
2
3
4
5
6
7
8
class Spider(CrawlSpider):
name = 'mzitu'
allowed_domains = ['mzitu.com']
start_urls = ['http://www.mzitu.com/']
img_urls = []
rules = (
Rule(LinkExtractor(allow=('http://www.mzitu.com/\d{1,6}',), deny=('http://www.mzitu.com/\d{1,6}/\d{1,6}')), callback='parse_item', follow=True),
)

第五行的 img_urls=[] 这个列表是我们之后用来存储每个套图的全部图片的 URL 地址的。 rules 中的语句是:匹配http://www.mzitu.com/1至6位数的的URL(\\d:数字;{1,6}匹配1至6次。就能匹配出1到6位数) 但是我们会发现网页中除了http://www.mzitu.com/XXXXXXX 这种格式的 URL 之外;还有 http://www.mzitu.com/XXXX/XXXX 这个格式的 URL。所以我们需要设置 deny 来不匹配http://www.mzitu.com/XXXX/XXXX这种格式的URL。 然后将匹配到的网页交给 parse_item 来处理。并且持续追踪 看这儿敲黑板!!划重点!!:::

重点说明!!!!不能 parse 函数!!这是 CrawlSpider 进行匹配调用的函数,你要是使用了!rules 就没法进行匹配啦!!!

现在 spider.py 是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from scrapy import Request
from scrapy.spider import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from mzitu_scrapy.items import MzituScrapyItem


class Spider(CrawlSpider):
name = 'mzitu'
allowed_domains = ['mzitu.com']
start_urls = ['http://www.mzitu.com/']
img_urls = []
rules = (
Rule(LinkExtractor(allow=('http://www.mzitu.com/\d{1,6}',), deny=('http://www.mzitu.com/\d{1,6}/\d{1,6}')), callback='parse_item', follow=True),
)


def parse_item(self, response):
print(response.url)

来跑一下试试 别忘了怎么测试的哈!!上面新建的那个 run.py! Good!!真棒!全是我们想要的!!! 现在干啥?啥?你不知道?EXM 你没逗我吧! 当然是解析我们拿到的 response 了!从里面找我们要的套图名称和所有的图片地址了! 我们随便打开一个 URL。 首先用 xpath 取套图名称: 啥?你不知道怎么用 xpath??少年少女 你走吧。出去别说看过我的博文。 ./*//div[@class=’main’]/div[1]/h2/text() 这段 xpath 就是套图名称的 xpath 了!看不懂的少年少女赶快去http://www.w3school.com.cn/看看xpath的教程! 当然你直接用 Chrome 拷贝出来的那个 xpath 也行。(有一定的概率不能使) 现在来找图片地址了,怎么找我在 小白爬虫第一弹中已经写过了哈!这就不详细赘述了! 首先找到每套图有多少张图片: 就是红框中的那个东东。 Xpath 这样写:

1
descendant::div[@class='main']/div[@class='content']/div[@class='pagenavi']/a[last()-1]/span/text()

意思是选取根节点下面所有后代标签,在其中选取出 div[@class=’main’]下面的 div[@class=’content’]下面的/div[@class=’pagenavi’]下面的倒数第二个 a 标签 下面的 span 标签中的文本。(有点长哈哈哈哈哈!其实还可以短一些,我懒就不改了) 然后循环拼接处每张图片的的网页地址,现在 spider.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
from scrapy import Request
from scrapy.spider import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from mzitu_scrapy.items import MzituScrapyItem


class Spider(CrawlSpider):
name = 'mzitu'
allowed_domains = ['mzitu.com']
start_urls = ['http://www.mzitu.com/']
img_urls = []
rules = (
Rule(LinkExtractor(allow=('http://www.mzitu.com/\d{1,6}',), deny=('http://www.mzitu.com/\d{1,6}/\d{1,6}')), callback='parse_item', follow=True),
)


def parse_item(self, response):
"""
:param response: 下载器返回的response
:return:
"""
item = MzituScrapyItem()
# max_num为页面最后一张图片的位置
max_num = response.xpath("descendant::div[@class='main']/div[@class='content']/div[@class='pagenavi']/a[last()-1]/span/text()").extract_first(default="N/A")
item['name'] = response.xpath("./*//div[@class='main']/div[1]/h2/text()").extract_first(default="N/A")
for num in range(1, int(max_num)):
# page_url 为每张图片所在的页面地址
page_url = response.url + '/' + str(num)
yield Request(page_url, callback=self.img_url)

extract_first(default=”N/A”)的意思是:取 xpath 返回值的第一个元素。如果 xpath 没有取到值,则返回 N/A 然后调用函数 img_url 来提取每个网页中的图片地址。img_url 长这样:

1
2
3
4
5
6
7
8
def img_url(self, response,):
"""取出图片URL 并添加进self.img_urls列表中
:param response:
:param img_url 为每张图片的真实地址
"""
img_urls = response.xpath("descendant::div[@class='main-image']/descendant::img/@src").extract()
for img_url in img_urls:
self.img_urls.append(img_url)

descendant::div[@class=’main-image’]/descendant::img/@src 这段 xpath 取出 div[@class=’main-image’]下面所有的 img 标签的 src 属性(有的套图一个页面有好几张图) .extract()不跟上[0]返回的是列表 完整的 spider.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
from scrapy import Request
from scrapy.spider import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from mzitu_scrapy.items import MzituScrapyItem


class Spider(CrawlSpider):
name = 'mzitu'
allowed_domains = ['mzitu.com']
start_urls = ['http://www.mzitu.com/']
img_urls = []
rules = (
Rule(LinkExtractor(allow=('http://www.mzitu.com/\d{1,6}',), deny=('http://www.mzitu.com/\d{1,6}/\d{1,6}')), callback='parse_item', follow=True),
)


def parse_item(self, response):
"""
:param response: 下载器返回的response
:return:
"""
item = MzituScrapyItem()
# max_num为页面最后一张图片的位置
max_num = response.xpath("descendant::div[@class='main']/div[@class='content']/div[@class='pagenavi']/a[last()-1]/span/text()").extract_first(default="N/A")
item['name'] = response.xpath("./*//div[@class='main']/div[1]/h2/text()").extract_first(default="N/A")
item['url'] = response.url
for num in range(1, int(max_num)):
# page_url 为每张图片所在的页面地址
page_url = response.url + '/' + str(num)
yield Request(page_url, callback=self.img_url)
item['image_urls'] = self.img_urls
yield item


def img_url(self, response,):
"""取出图片URL 并添加进self.img_urls列表中
:param response:
:param img_url 为每张图片的真实地址
"""
img_urls = response.xpath("descendant::div[@class='main-image']/descendant::img/@src").extract()
for img_url in img_urls:
self.img_urls.append(img_url)

下面开始把图片弄回本地啦!! 开写我们的 pipelines.py 首先根据官方文档说明我们如果需要使用图片管道 则需要使用 ImagesPipeline: 我们可以依葫芦画瓢写一个。但是这样有一个很麻烦的问题就是,这样下载下来的图片没有分类,很是难看啊! 所以 我们需要重写一下 ImagesPipeline 中的 file_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
# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
from scrapy import Request
from scrapy.pipelines.images import ImagesPipeline
from scrapy.exceptions import DropItem
import re


class MzituScrapyPipeline(ImagesPipeline):

def file_path(self, request, response=None, info=None):
"""
:param request: 每一个图片下载管道请求
:param response:
:param info:
:param strip :清洗Windows系统的文件夹非法字符,避免无法创建目录
:return: 每套图的分类目录
"""
item = request.meta['item']
folder = item['name']
folder_strip = strip(folder)
image_guid = request.url.split('/')[-1]
filename = u'full/{0}/{1}'.format(folder_strip, image_guid)
return filename

def get_media_requests(self, item, info):
"""
:param item: spider.py中返回的item
:param info:
:return:
"""
for img_url in item['image_urls']:
referer = item['url']
yield Request(img_url, meta={'item': item,
'referer': referer})


def item_completed(self, results, item, info):
image_paths = [x['path'] for ok, x in results if ok]
if not image_paths:
raise DropItem("Item contains no images")
return item

# def process_item(self, item, spider):
# return item

def strip(path):
"""
:param path: 需要清洗的文件夹名字
:return: 清洗掉Windows系统非法文件夹名字的字符串
"""
path = re.sub(r'[?\*|“<>:/]', '', str(path))
return path




if __name__ == "__main__":
a = '我是一个?*|“<>:/错误的字符串'
print(strip(a))

写一个中间件来处理图片下载的防盗链:

1
2
3
4
5
6
7
8
9
10
11
class MeiZiTu(object):

def process_request(self, request, spider):
'''设置headers和切换请求头
:param request: 请求体
:param spider: spider对象
:return: None
'''
referer = request.meta.get('referer', None)
if referer:
request.headers['referer'] = referer

最后一步设置 ImagesPipeline 的存储目录! 在 settings.py 中写入:

1
IMAGES_STORE = 'F:\mzitu\\'

则 ImagesPipeline 将所有下载的图片放置在此目录下! 设置图片实效性: 图像管道避免下载最近已经下载的图片。使用 FILES_EXPIRES (或 IMAGES_EXPIRES) 设置可以调整失效期限,可以用天数来指定: 在 settings.py 中写入以下配置。

1
2
# 30 days of delay for images expiration
IMAGES_EXPIRES = 30

settings.py 中开启 item_pipelines:

1
2
3
ITEM_PIPELINES = {
'mzitu_scrapy.pipelines.MzituScrapyPipeline': 300,
}

settings.py 中开启 DOWNLOADER_MIDDLEWARES

1
2
3
DOWNLOADER_MIDDLEWARES = {
'mzitu_scrapy.middlewares.MeiZiTu': 543,
}

如果你需要缩略图之类的请参考官方文档: 将其写入 settings.py 文件中。 至此完毕!!! 来看看效果: 下载速度简直飞起!!友情提示:请务必配置代理哦! 可以参考大才哥的http://cuiqingcai.com/3443.html做一个代理,就不需要重写Scrapy中间件啦!更能避免费代理总是不能用的坑爹行为。 总之省事省时又省心啊! github 地址:https://github.com/thsheep/mzitu_scrapy

Python

本节分享一下爬取知乎用户信息的Scrapy爬虫实战。

本节目标

本节要实现的内容有:

  • 从一个大V用户开始,通过递归抓取粉丝列表和关注列表,实现知乎所有用户的详细信息的抓取。
  • 将抓取到的结果存储到MongoDB,并进行去重操作。

思路分析

我们都知道每个人都有关注列表和粉丝列表,尤其对于大V来说,粉丝和关注尤其更多。 如果我们从一个大V开始,首先可以获取他的个人信息,然后我们获取他的粉丝列表和关注列表,然后遍历列表中的每一个用户,进一步抓取每一个用户的信息还有他们各自的粉丝列表和关注列表,然后再进一步遍历获取到的列表中的每一个用户,进一步抓取他们的信息和关注粉丝列表,循环往复,不断递归,这样就可以做到一爬百,百爬万,万爬百万,通过社交关系自然形成了一个爬取网,这样就可以爬到所有的用户信息了。当然零粉丝零关注的用户就忽略他们吧~ 爬取的信息怎样来获得呢?不用担心,通过分析知乎的请求就可以得到相关接口,通过请求接口就可以拿到用户详细信息和粉丝、关注列表了。 接下来我们开始实战爬取。

环境需求

Python3

本项目使用的Python版本是Python3,项目开始之前请确保你已经安装了Python3。

Scrapy

Scrapy是一个强大的爬虫框架,安装方式如下:

1
pip3 install scrapy

MongoDB

非关系型数据库,项目开始之前请先安装好MongoDB并启动服务。

PyMongo

Python的MongoDB连接库,安装方式如下:

1
pip3 install pymongo

创建项目

安装好以上环境之后,我们便可以开始我们的项目了。 在项目开始之首先我们用命令行创建一个项目:

1
scrapy startproject zhihuuser

创建爬虫

接下来我们需要创建一个spider,同样利用命令行,不过这次命令行需要进入到项目里运行。

1
2
cd zhihuuser
scrapy genspider zhihu www.zhihu.com

禁止ROBOTSTXT_OBEY

接下来你需要打开settings.py文件,将ROBOTSTXT_OBEY修改为False。

1
ROBOTSTXT_OBEY = False

它默认为True,就是要遵守robots.txt 的规则,那么 robots.txt 是个什么东西呢? 通俗来说, robots.txt 是遵循 Robot 协议的一个文件,它保存在网站的服务器中,它的作用是,告诉搜索引擎爬虫,本网站哪些目录下的网页 不希望 你进行爬取收录。在Scrapy启动后,会在第一时间访问网站的 robots.txt 文件,然后决定该网站的爬取范围。 当然,我们并不是在做搜索引擎,而且在某些情况下我们想要获取的内容恰恰是被 robots.txt 所禁止访问的。所以,某些时候,我们就要将此配置项设置为 False ,拒绝遵守 Robot协议 ! 所以在这里设置为False。当然可能本次爬取不一定会被它限制,但是我们一般来说会首先选择禁止它。

尝试最初的爬取

接下来我们什么代码也不修改,执行爬取,运行如下命令:

1
scrapy crawl zhihu

你会发现爬取结果会出现这样的一个错误:

1
500 Internal Server Error

访问知乎得到的状态码是500,这说明爬取并没有成功,其实这是因为我们没有加入请求头,知乎识别User-Agent发现不是浏览器,就返回错误的响应了。 所以接下来的一步我们需要加入请求headers信息,你可以在Request的参数里加,也可以在spider里面的custom_settings里面加,当然最简单的方法莫过于在全局settings里面加了。 我们打开settings.py文件,取消DEFAULT_REQUEST_HEADERS的注释,加入如下的内容:

1
2
3
DEFAULT_REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
}

这个是为你的请求添加请求头,如果你没有设置headers的话,它就会使用这个请求头请求,添加了User-Agent信息,所以这样我们的爬虫就可以伪装浏览器了。 接下来重新运行爬虫。

1
scrapy crawl zhihu

这时你就会发现得到的返回状态码就正常了。 解决了这个问题,我们接下来就可以分析页面逻辑来正式实现爬虫了。

爬取流程

接下来我们需要先探寻获取用户详细信息和获取关注列表的接口。 回到网页,打开浏览器的控制台,切换到Network监听模式。 我们首先要做的是寻找一个大V,以轮子哥为例吧,它的个人信息页面网址是:https://www.zhihu.com/people/excited-vczh 首先打开轮子哥的首页 我们可以看到这里就是他的一些基本信息,我们需要抓取的就是这些,比如名字、签名、职业、关注数、赞同数等等。 接下来我们需要探索一下关注列表接口在哪里,我们点击关注选项卡,然后下拉,点击翻页,我们会在下面的请求中发现出现了 followees开头的Ajax请求。这个就是获取关注列表的接口。 我们观察一下这个请求结构 首先它是一个Get类型的请求,请求的URL是https://www.zhihu.com/api/v4/members/excited-vczh/followees,后面跟了三个参数,一个是include,一个是offset,一个是limit。 观察后可以发现,include是一些获取关注的人的基本信息的查询参数,包括回答数、文章数等等。 offset是偏移量,我们现在分析的是第3页的关注列表内容,offset当前为40。 limit为每一页的数量,这里是20,所以结合上面的offset可以推断,当offset为0时,获取到的是第一页关注列表,当offset为20时,获取到的是第二页关注列表,依次类推。 然后接下来看下返回结果: 可以看到有data和paging两个字段,data就是数据,包含20个内容,这些就是用户的基本信息,也就是关注列表的用户信息。 paging里面又有几个字段,is_end表示当前翻页是否结束,next是下一页的链接,所以在判读分页的时候,我们可以先利用is_end判断翻页是否结束,然后再获取next链接,请求下一页。 这样我们的关注列表就可以通过接口获取到了。 接下来我们再看下用户详情接口在哪里,我们将鼠标放到关注列表任意一个头像上面,观察下网络请求,可以发现又会出现一个Ajax请求。 可以看到这次的请求链接为https://www.zhihu.com/api/v4/members/lu-jun-ya-1 后面又一个参数include,include是一些查询参数,与刚才的接口类似,不过这次参数非常全,几乎可以把所有详情获取下来,另外接口的最后是加了用户的用户名,这个其实是url_token,上面的那个接口其实也是,在返回数据中是可以获得的。 所以综上所述:

理清了如上接口逻辑后,我们就可以开始构造请求了。

生成第一步请求

接下来我们要做的第一步当然是请求轮子哥的基本信息,然后获取轮子哥的关注列表了,我们首先构造一个格式化的url,将一些可变参数提取出来,然后需要重写start_requests方法,生成第一步的请求,接下来我们还需要根据获取到到关注列表做进一步的分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json
from scrapy import Spider, Request
from zhihuuser.items import UserItem

class ZhihuSpider(Spider):
name = "zhihu"
allowed_domains = ["www.zhihu.com"]
user_url = 'https://www.zhihu.com/api/v4/members/{user}?include={include}'
follows_url = 'https://www.zhihu.com/api/v4/members/{user}/followees?include={include}&amp;offset={offset}&amp;limit={limit}'
start_user = 'excited-vczh'
user_query = 'locations,employments,gender,educations,business,voteup_count,thanked_Count,follower_count,following_count,cover_url,following_topic_count,following_question_count,following_favlists_count,following_columns_count,answer_count,articles_count,pins_count,question_count,commercial_question_count,favorite_count,favorited_count,logs_count,marked_answers_count,marked_answers_text,message_thread_token,account_status,is_active,is_force_renamed,is_bind_sina,sina_weibo_url,sina_weibo_name,show_sina_weibo,is_blocking,is_blocked,is_following,is_followed,mutual_followees_count,vote_to_count,vote_from_count,thank_to_count,thank_from_count,thanked_count,description,hosted_live_count,participated_live_count,allow_message,industry_category,org_name,org_homepage,badge[?(type=best_answerer)].topics'
follows_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'

def start_requests(self):
yield Request(self.user_url.format(user=self.start_user, include=self.user_query), self.parse_user)
yield Request(self.follows_url.format(user=self.start_user, include=self.follows_query, limit=20, offset=0),
self.parse_follows)

然后我们实现一下两个解析方法parse_user和parse_follows。

1
2
3
4
def parse_user(self, response):
print(response.text)
def parse_follows(self, response):
print(response.text)

最简单的实现他们的结果输出即可,然后运行观察结果。

1
scrapy crawl zhihu

这时你会发现出现了

1
401 HTTP status code is not handled or not allowed

访问被禁止了,这时我们观察下浏览器请求,发现它相比之前的请求多了一个OAuth请求头。

OAuth

它是Open Authorization的缩写。 OAUTH_token:OAUTH进行到最后一步得到的一个“令牌”,通过此“令牌”请求,就可以去拥有资源的网站抓取任意有权限可以被抓取的资源。 在这里我知乎并没有登陆,这里的OAuth值是

1
oauth c3cef7c66a1843f8b3a9e6a1e3160e20

经过我长久的观察,这个一直不会改变,所以可以长久使用,我们将它配置到DEFAULT_REQUEST_HEADERS里,这样它就变成了:

1
2
3
4
DEFAULT_REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36',
'authorization': 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20',
}

接下来如果我们重新运行爬虫,就可以发现可以正常爬取了。

parse_user

接下来我们处理一下用户基本信息,首先我们查看一下接口信息会返回一些什么数据。 可以看到返回的结果非常全,在这里我们直接声明一个Item全保存下就好了。 在items里新声明一个UserItem

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
from scrapy import Item, Field

class UserItem(Item):
# define the fields for your item here like:
id = Field()
name = Field()
avatar_url = Field()
headline = Field()
description = Field()
url = Field()
url_token = Field()
gender = Field()
cover_url = Field()
type = Field()
badge = Field()

answer_count = Field()
articles_count = Field()
commercial_question_count = Field()
favorite_count = Field()
favorited_count = Field()
follower_count = Field()
following_columns_count = Field()
following_count = Field()
pins_count = Field()
question_count = Field()
thank_from_count = Field()
thank_to_count = Field()
thanked_count = Field()
vote_from_count = Field()
vote_to_count = Field()
voteup_count = Field()
following_favlists_count = Field()
following_question_count = Field()
following_topic_count = Field()
marked_answers_count = Field()
mutual_followees_count = Field()
hosted_live_count = Field()
participated_live_count = Field()

locations = Field()
educations = Field()
employments = Field()

所以在解析方法里面我们解析得到的response内容,然后转为json对象,然后依次判断字段是否存在,赋值就好了。

1
2
3
4
5
6
result = json.loads(response.text)
item = UserItem()
for field in item.fields:
if field in result.keys():
item[field] = result.get(field)
yield item

得到item后通过yield返回就好了。 这样保存用户基本信息就完成了。 接下来我们还需要在这里获取这个用户的关注列表,所以我们需要再重新发起一个获取关注列表的request 在parse_user后面再添加如下代码:

1
2
3
yield Request(
self.follows_url.format(user=result.get('url_token'), include=self.follows_query, limit=20, offset=0),
self.parse_follows)

这样我们又生成了获取该用户关注列表的请求。

parse_follows

接下来我们处理一下关注列表,首先也是解析response的文本,然后要做两件事:

  • 通过关注列表的每一个用户,对每一个用户发起请求,获取其详细信息。
  • 处理分页,判断paging内容,获取下一页关注列表。

所以在这里将parse_follows改写如下:

1
2
3
4
5
6
7
8
9
10
11
results = json.loads(response.text)

if 'data' in results.keys():
for result in results.get('data'):
yield Request(self.user_url.format(user=result.get('url_token'), include=self.user_query),
self.parse_user)

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:
next_page = results.get('paging').get('next')
yield Request(next_page,
self.parse_follows)

这样,整体代码如下:

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 json

from scrapy import Spider, Request
from zhihuuser.items import UserItem


class ZhihuSpider(Spider):
name = "zhihu"
allowed_domains = ["www.zhihu.com"]
user_url = 'https://www.zhihu.com/api/v4/members/{user}?include={include}'
follows_url = 'https://www.zhihu.com/api/v4/members/{user}/followees?include={include}&amp;offset={offset}&amp;limit={limit}'
start_user = 'excited-vczh'
user_query = 'locations,employments,gender,educations,business,voteup_count,thanked_Count,follower_count,following_count,cover_url,following_topic_count,following_question_count,following_favlists_count,following_columns_count,answer_count,articles_count,pins_count,question_count,commercial_question_count,favorite_count,favorited_count,logs_count,marked_answers_count,marked_answers_text,message_thread_token,account_status,is_active,is_force_renamed,is_bind_sina,sina_weibo_url,sina_weibo_name,show_sina_weibo,is_blocking,is_blocked,is_following,is_followed,mutual_followees_count,vote_to_count,vote_from_count,thank_to_count,thank_from_count,thanked_count,description,hosted_live_count,participated_live_count,allow_message,industry_category,org_name,org_homepage,badge[?(type=best_answerer)].topics'
follows_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'

def start_requests(self):
yield Request(self.user_url.format(user=self.start_user, include=self.user_query), self.parse_user)
yield Request(self.follows_url.format(user=self.start_user, include=self.follows_query, limit=20, offset=0),
self.parse_follows)

def parse_user(self, response):
result = json.loads(response.text)
item = UserItem()


for field in item.fields:
if field in result.keys():
item[field] = result.get(field)
yield item

yield Request(
self.follows_url.format(user=result.get('url_token'), include=self.follows_query, limit=20, offset=0),
self.parse_follows)

def parse_follows(self, response):
results = json.loads(response.text)

if 'data' in results.keys():
for result in results.get('data'):
yield Request(self.user_url.format(user=result.get('url_token'), include=self.user_query),
self.parse_user)

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:
next_page = results.get('paging').get('next')
yield Request(next_page,
self.parse_follows)

这样我们就完成了获取用户基本信息,然后递归获取关注列表进一步请求了。 重新运行爬虫,可以发现当前已经可以实现循环递归爬取了。

followers

上面我们实现了通过获取关注列表实现爬取循环,那这里少不了的还有粉丝列表,经过分析后发现粉丝列表的api也类似,只不过把followee换成了follower,其他的完全相同,所以我们按照同样的逻辑添加followers相关信息, 最终spider代码如下:

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
# -*- coding: utf-8 -*-
import json

from scrapy import Spider, Request
from zhihuuser.items import UserItem


class ZhihuSpider(Spider):
name = "zhihu"
allowed_domains = ["www.zhihu.com"]
user_url = 'https://www.zhihu.com/api/v4/members/{user}?include={include}'
follows_url = 'https://www.zhihu.com/api/v4/members/{user}/followees?include={include}&offset={offset}&limit={limit}'
followers_url = 'https://www.zhihu.com/api/v4/members/{user}/followers?include={include}&offset={offset}&limit={limit}'
start_user = 'tianshansoft'
user_query = 'locations,employments,gender,educations,business,voteup_count,thanked_Count,follower_count,following_count,cover_url,following_topic_count,following_question_count,following_favlists_count,following_columns_count,answer_count,articles_count,pins_count,question_count,commercial_question_count,favorite_count,favorited_count,logs_count,marked_answers_count,marked_answers_text,message_thread_token,account_status,is_active,is_force_renamed,is_bind_sina,sina_weibo_url,sina_weibo_name,show_sina_weibo,is_blocking,is_blocked,is_following,is_followed,mutual_followees_count,vote_to_count,vote_from_count,thank_to_count,thank_from_count,thanked_count,description,hosted_live_count,participated_live_count,allow_message,industry_category,org_name,org_homepage,badge[?(type=best_answerer)].topics'
follows_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'
followers_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'

def start_requests(self):
yield Request(self.user_url.format(user=self.start_user, include=self.user_query), self.parse_user)
yield Request(self.follows_url.format(user=self.start_user, include=self.follows_query, limit=20, offset=0),
self.parse_follows)
yield Request(self.followers_url.format(user=self.start_user, include=self.followers_query, limit=20, offset=0),
self.parse_followers)

def parse_user(self, response):
result = json.loads(response.text)
item = UserItem()

for field in item.fields:
if field in result.keys():
item[field] = result.get(field)
yield item

yield Request(
self.follows_url.format(user=result.get('url_token'), include=self.follows_query, limit=20, offset=0),
self.parse_follows)

yield Request(
self.followers_url.format(user=result.get('url_token'), include=self.followers_query, limit=20, offset=0),
self.parse_followers)

def parse_follows(self, response):
results = json.loads(response.text)

if 'data' in results.keys():
for result in results.get('data'):
yield Request(self.user_url.format(user=result.get('url_token'), include=self.user_query),
self.parse_user)

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:
next_page = results.get('paging').get('next')
yield Request(next_page,
self.parse_follows)

def parse_followers(self, response):
results = json.loads(response.text)

if 'data' in results.keys():
for result in results.get('data'):
yield Request(self.user_url.format(user=result.get('url_token'), include=self.user_query),
self.parse_user)

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:
next_page = results.get('paging').get('next')
yield Request(next_page,
self.parse_followers)

需要改变的位置有

  • start_requests里面添加yield followers信息
  • parse_user里面里面添加yield followers信息
  • parse_followers做相应的的抓取详情请求和翻页。

如此一来,spider就完成了,这样我们就可以实现通过社交网络递归的爬取,把用户详情都爬下来。

小结

通过以上的spider,我们实现了如上逻辑:

  • start_requests方法,实现了第一个大V用户的详细信息请求还有他的粉丝和关注列表请求。
  • parse_user方法,实现了详细信息的提取和粉丝关注列表的获取。
  • paese_follows,实现了通过关注列表重新请求用户并进行翻页的功能。
  • paese_followers,实现了通过粉丝列表重新请求用户并进行翻页的功能。

加入pipeline

在这里数据库存储使用MongoDB,所以在这里我们需要借助于Item Pipeline,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MongoPipeline(object):
collection_name = 'users'

def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db

@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE')
)

def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]

def close_spider(self, spider):
self.client.close()

def process_item(self, item, spider):
self.db[self.collection_name].update({'url_token': item['url_token']}, {'$set': dict(item)}, True)
return item

比较重要的一点就在于process_item,在这里使用了update方法,第一个参数传入查询条件,这里使用的是url_token,第二个参数传入字典类型的对象,就是我们的item,第三个参数传入True,这样就可以保证,如果查询数据存在的话就更新,不存在的话就插入。这样就可以保证去重了。 另外记得开启一下Item Pileline

1
2
3
ITEM_PIPELINES = {
'zhihuuser.pipelines.MongoPipeline': 300,
}

然后重新运行爬虫

1
scrapy crawl zhihu

这样就可以发现正常的输出了,会一直不停地运行,用户也一个个被保存到数据库。 看下MongoDB,里面我们爬取的用户详情结果。 到现在为止,整个爬虫就基本完结了,我们主要通过递归的方式实现了这个逻辑。存储结果也通过适当的方法实现了去重。

更高效率

当然我们现在运行的是单机爬虫,只在一台电脑上运行速度是有限的,所以后面我们要想提高抓取效率,需要用到分布式爬虫,在这里需要用到Redis来维护一个公共的爬取队列。 更多的分布式爬虫的实现可以查看自己动手,丰衣足食!Python3网络爬虫实战案例

Python

QQ图片20161021225948 听大才哥说好像我的文章挺难找的,这整理一下。

基础知识篇:

这玩意儿我没写,各位参考大才哥的: Python 爬虫学习系列教程 Python3 爬虫学习视频教程

小白系列教程

小白爬虫第一弹之抓取妹子图 小白爬虫第二弹之健壮的小爬虫 小白爬虫第三弹之去重去重 小白爬虫第四弹之爬虫快跑(多进程+多线程) 小白进阶之 Scrapy 第一篇 小白进阶之 Scrapy 第二篇(登录篇) 小白进阶之Scrapy 分布式的前篇—让 redis 和 MongoDB 安全点 小白进阶之 Scrapy 第三篇(基于 Scrapy-Redis 的分布式以及 cookies 池) 小白进阶之 Scrapy 第四篇(图片下载管道篇) 小白进阶之 Scrapy 第五篇(Scrapy-Splash 配合 CrawlSpider;瞎几把整的) 利用新接口抓取微信公众号的所有文章 小白进阶之Scrapy 第六篇Scrapy-Redis 详解 QQ图片20161021225948 暂时就这些了、最近工作刚入职。上了个新项目,没时间更新文章了(主要是我懒、挤点时间都用来打 LOL 了···············尴尬脸) 等项目第一期结束了,我会把以前许诺的 :JS 异步加载 | 动态爬虫 更新出来。 感谢大才哥的平台(有兴趣的小伙伴一起来更新文章啊! 才不会告诉你们:我扯着大才哥的大旗找了个不错的工作。手动笑哭······) 如果以上网站有更改无法正常采集,请 PM 我一下,我尽量保证 demo 的可用性

Other

公告

大家好,本站于今日(2017.4.11)关闭投稿功能。

原因

由于之前本站开放了投稿注册接口,该接口现在被人利用,每天都会发送垃圾邮件,经常导致邮箱发信过多而被冻结,而WordPress本身没有提供验证码验证,所以自己也不想再去修改,当然最主要的是能发优质文章的又是少之又少,经常会出现一些垃圾草稿,所以博主决定直接将投稿功能关闭,希望大家可以理解。

投稿

如果您有在本站投稿意向,请直接联系我邮件cqc@cuiqingcai.com,我为您注册账号并开通写作权限。

鸣谢

非常感谢在本站投稿的童鞋,尤其是卧槽哥,发表了很多篇高质量爬虫文章。另外还有戴笠兄也是,不过后来戴笠兄的文章因为开车过猛而下架了哈哈,不过还是非常感谢。另外也非常感谢其他在本站投稿的小伙伴,在这不一一点名啦!

结语

最后希望大家可以理解,也非常感谢大家的支持!前一段时间忙着在录制爬虫视频,今天刚刚收尾,现在已经更新完毕,后面我将学习一些数据分析、自然语言处理、Web安全方面的知识分享给大家,希望大家多多支持!感谢!

Python

2022 年 Python3 网络爬虫教程

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

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

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

教程请移步:

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

2018 年 Python3 网络爬虫视频课程链接

以下为 2018 年 Python3 网络爬虫视频课程

天善智能:自己动手,丰衣足食!Python3 网络爬虫实战案例 网易云课堂:自己动手,丰衣足食!Python3 网络爬虫实战案例

课程简介

大家好哈,现在呢静觅博客已经两年多啦,可能大家过来更多看到的是爬虫方面的博文,首先非常感谢大家的支持,希望我的博文对大家有帮助! 之前我写了一些 Python 爬虫方面的文章,Python 爬虫学习系列教程,涉及到了基础和进阶的一些内容,当时更多用到的是 Urllib 还有正则,后来又陆续增加了一些文章,在学习过程中慢慢积累慢慢成型了一套算不上教程的教程,后来有越来越多的小伙伴学习和支持我感到非常开心,再次感谢大家! 不过其实这些教程总的来说有一些问题:

  1. 当时用的 Python2 写的,刚写的时候 Scrapy 这个框架也没有支持 Python3,一些 Python3 爬虫库也不怎么成熟,所以当时选择了 Python2。但到现在,Python3 发展迅速,爬虫库也越来越成熟,而且 Python2 在不久的将来就会停止维护了,所以慢慢地,我的语言重心也慢慢转向了 Python3,我也相信 Python3 会成为主流。所以说之前的一套课程算是有点过时了,相信大家肯定还在寻找 Python3 的一些教程。
  2. 当时学习的时候主要用的 urllib,正则,所以这些文章的较大篇幅也都是 urllib 和正则的一些东西,后来的一些高级库都是在后面慢慢加的,而且一些高级的框架用法也没有做深入讲解,所以感觉整个内容有点头重脚轻,安排不合理。而且现在分布式越来越火,那么分布式爬虫的应用相必也是越来越广泛,之前的课程也没有做系统讲解。
  3. 在介绍一些操作的时候可能介绍不全面,环境的配置也没有兼顾各个平台,所以可能有些小伙伴摸不着头脑,可能卡在某一步不知道接下来是怎么做的了。

那么综合上面的问题呢,最近我花了前前后后将近一个月的时间录制了一套新的 Pyhthon3 爬虫视频教程,将我之前做爬虫的一些经验重新梳理和整合,利用 Python3 编写,从环境配置、基础库讲解到案例实战、框架使用,最后再到分布式爬虫进行了比较系统的讲解。 课程内容是这个样子的:

一、环境篇

  • Python3+Pip 环境配置
  • MongoDB 环境配置
  • Redis 环境配置
  • MySQL 环境配置
  • Python 多版本共存配置
  • Python 爬虫常用库的安装

二、基础篇

  • 爬虫基本原理
  • Urllib 库基本使用
  • Requests 库基本使用
  • 正则表达式基础
  • BeautifulSoup 详解
  • PyQuery 详解
  • Selenium 详解

三、实战篇

  • 使用 Requests+正则表达式爬取猫眼电影
  • 分析 Ajax 请求并抓取今日头条街拍美图
  • 使用 Selenium 模拟浏览器抓取淘宝商品美食信息
  • 使用 Redis+Flask 维护动态代理池
  • 使用代理处理反爬抓取微信文章
  • 使用 Redis+Flask 维护动态 Cookies 池

四、框架篇

  • PySpider 框架基本使用及抓取 TripAdvisor 实战
  • PySpider 架构概述及用法详解
  • Scrapy 框架的安装
  • Scrapy 框架基本使用
  • Scrapy 命令行详解
  • Scrapy 中选择器的用法
  • Scrapy 中 Spiders 的用法
  • Scrapy 中 Item Pipeline 的用法
  • Scrapy 中 Download Middleware 的用法
  • Scrapy 爬取知乎用户信息实战
  • Scrapy+Cookies 池抓取新浪微博
  • Scrapy+Tushare 爬取微博股票数据

五、分布式篇

  • Scrapy 分布式原理及 Scrapy-Redis 源码解析
  • Scrapy 分布式架构搭建抓取知乎
  • Scrapy 分布式的部署详解

整个课程是从小白起点的,从环境配置和基础开始讲起,环境安装部分三大平台都有介绍,实战的部分我是一边写一边讲解,还有一些分布式爬虫的搭建流程也做了介绍。 不过这个课程是收费的,其实里面也包含了我学习爬虫以来的经验和汗水,我在做讲解的时候也会把我学习爬虫的一些思路和想法讲解出来,避免大家走一些弯路,希望大家可以支持一下! 不过在这里有免费的视频,是属于整个课程的一部分,大家可以直接观看 Python3 爬虫三大案例实战分享 整套视频课程放在天善智能这边了,大家如果感兴趣的话可以直接在这里购买,499 元。 课程链接如下: 天善智能:自己动手,丰衣足食!Python3 网络爬虫实战案例 网易云课堂:自己动手,丰衣足食!Python3 网络爬虫实战案例 最后的最后希望大家可以多多支持!非常感谢!知识就是力量!也希望我的课程能为您创造更大的财富!

Python

吃惊表情1 这两天上班接手,别人留下来的爬虫发现一个很好玩的 SQL 脚本拼接。 只要你的 Scrapy Field 字段名字和 数据库字段的名字 一样。那么恭喜你你就可以拷贝这段 SQL 拼接脚本。进行 MySQL 入库处理。 具体拼接代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def process_item(self, item, spider):
if isinstance(item, WhoscoredNewItem):
table_name = item.pop('table_name')
col_str = ''
row_str = ''
for key in item.keys():
col_str = col_str + " " + key + ","
row_str = "{}'{}',".format(row_str, item[key] if "'" not in item[key] else item[key].replace("'", "\\'"))
sql = "insert INTO {} ({}) VALUES ({}) ON DUPLICATE KEY UPDATE ".format(table_name, col_str[1:-1], row_str[:-1])
for (key, value) in six.iteritems(item):
sql += "{} = '{}', ".format(key, value if "'" not in value else value.replace("'", "\\'"))
sql = sql[:-2]
self.cursor.execute(sql) #执行SQL
self.cnx.commit()# 写入操作

这个 SQL 拼接实现了,如果数据库存在相同数据则 更新,不存在则插入 的 SQL 语句 具体实现就是第一个 for 循环,获取 key 作为 MySQL 字段名字、VALUES 做为 SQL 的 VALUES(拼接成一个插入的 SQL 语句) 第二个 for 循环,实现了 字段名 = VALUES 的拼接。 和第一个 for 循环的中的 sql 就组成了 insert into XXXXX on duplicate key update 这个。存在则更新 不存在则插入的 SQL 语句。 QQ图片20161021225948 我只能所 6666666666 写这个拼接的小哥儿有想法。还挺通用。 不知道你们有没有想到这种方法 反正我是没想到。

PHP

今天给大家介绍 WordPress Plugin for UPYUN 插件,专为又拍云和 WordPress 用户准备,主要功能如下:

  1. 可以与 WordPress 无缝结合,通过 WordPress 上传图片和文件到又拍云, 支持大文件上传(需要开启表单 API) 和防盗链功能
  2. 支持同步删除(在 WordPress 后台媒体管理 “删除” 附件后,又拍云服务器中的文件也随之删除)
  3. 增加图片编辑功能
  4. 优化防盗链功能
  5. 增加与水印插件的兼容性,使上传到远程服务器的图片同样可以加上水印等

PS:修复了很多之前版本存在的 bug,具体可访问:github 又拍云是以 CDN 为核心业务,另外提供云存储、云处理、云安全、流量营销等的云服务商,有开放且可扩展的API,以及开放的SDK和第三方插件,还针对开发者启动了 又拍云联盟 活动,可以每月获取免费空间和流量。更多介绍,请访问又拍云安装插件: 进入到你的 WordPress 的 wp-content/plugins 目录下

1
` # pwd/home/wwwroot/blog.v5linux.com/wp-content/plugins`

克隆插件

1
2
3
4
5
6
7
` # git clone https://github.com/ihacklog/hacklog-remote-attachment-upyun.
gitInitialized empty Git repository in /home/wwwroot/blog.v5linux.com/wp-
content/plugins/hacklog-remote-attachment-upyun/.git/remote: Counting 
objects: 387, done.remote: Compressing objects: 100% (31/31), done.
remote: Total 387 (delta 16), reused 0 (delta 0), pack-reused 356Receiving 
objects: 100% (387/387), 399.17 KiB | 106 KiB/s, done.Resolving deltas:
 100% (223/223), done.`

设置权限

1
2
3
4
` # ll总用量 16drwxr-xr-x 4 www  www  4096 1月  12 13:20 akismetdrwxr-xr-x 
8 root root 4096 1月  16 11:34 hacklog-remote-attachment-upyun-rw-r--r-- 1 
www  www  2255 5月  23 2013 hello.php-rw-r--r-- 1 www  www    28 6月   
5 2014 index.php# chown -R www:www hacklog-remote-attachment-upyun/`

注意,如果你是虚拟主机,请下载后打包成 zip 文件上传到 plugins 目录下插件配置 插件设置

主要配置 空间名:后台创建的存储类型服务的名称 操作员和操作员密码:后台获取 表单密钥:又拍云控制台 找到对应的服务 — 高级选项 - 开启表单密钥远程基本 URL:填写你的绑定域名或默认域名(强烈建议使用绑定域名) REST 远程路径和 HTTP 路径:根据需求填写 插件启用和配置详情,请参考:WordPress 远程附件上传插件

Python

啥话都不说了、进入正题。 QQ图片20170205084843 首先我们更新一下 scrapy 版本。最新版为 1.3 再说一遍 Windows 的小伙伴儿 pip 是装不上 Scrapy 的。推荐使用 anaconda 、不然还是老老实实用 Linux 吧

1
2
3
conda install scrapy==1.3
或者
pip install scrapy==1.3

安装 Scrapy-Redis

1
2
3
conda install scrapy-redis
或者
pip install scrapy-redis

需要注意: Python 版本为 2.7,3.4 或者 3.5 。个人使用 3.6 版本也没有问题 Redis>=2.8 Scrapy>=1.0 Redis-py>=2.1 。 3.X 版本的 Python 都是自带 Redis-py 其余小伙伴如果没有的话、自己 pip 安装一下。 开始搞事! 开始之前我们得知道 scrapy-redis 的一些配置:PS 这些配置是写在 Scrapy 项目的 settings.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
#启用Redis调度存储请求队列
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

#确保所有的爬虫通过Redis去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

#默认请求序列化使用的是pickle 但是我们可以更改为其他类似的。PS:这玩意儿2.X的可以用。3.X的不能用
#SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"

#不清除Redis队列、这样可以暂停/恢复 爬取
#SCHEDULER_PERSIST = True

#使用优先级调度请求队列 (默认使用)
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
#可选用的其它队列
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'

#最大空闲时间防止分布式爬虫因为等待而关闭
#这只有当上面设置的队列类是SpiderQueue或SpiderStack时才有效
#并且当您的蜘蛛首次启动时,也可能会阻止同一时间启动(由于队列为空)
#SCHEDULER_IDLE_BEFORE_CLOSE = 10

#将清除的项目在redis进行处理
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300
}

#序列化项目管道作为redis Key存储
#REDIS_ITEMS_KEY = '%(spider)s:items'

#默认使用ScrapyJSONEncoder进行项目序列化
#You can use any importable path to a callable object.
#REDIS_ITEMS_SERIALIZER = 'json.dumps'

#指定连接到redis时使用的端口和地址(可选)
#REDIS_HOST = 'localhost'
#REDIS_PORT = 6379

#指定用于连接redis的URL(可选)
#如果设置此项,则此项优先级高于设置的REDIS_HOST 和 REDIS_PORT
#REDIS_URL = 'redis://user:pass@hostname:9001'

#自定义的redis参数(连接超时之类的)
#REDIS_PARAMS = {}

#自定义redis客户端类
#REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient'

#如果为True,则使用redis的'spop'进行操作。
#如果需要避免起始网址列表出现重复,这个选项非常有用。开启此选项urls必须通过sadd添加,否则会出现类型错误。
#REDIS_START_URLS_AS_SET = False

#RedisSpider和RedisCrawlSpider默认 start_usls 键
#REDIS_START_URLS_KEY = '%(name)s:start_urls'

#设置redis使用utf-8之外的编码
#REDIS_ENCODING = 'latin1'

请各位小伙伴儿自行挑选需要的配置写到项目的 settings.py 文件中 英语渣靠 Google、看不下去的小伙伴儿看这儿:http://scrapy-redis.readthedocs.io/en/stable/readme.html 继续在我们上一篇博文中的爬虫程序修改: 首先把我们需要的 redis 配置文件写入 settings.py 中: 如果你的 redis 数据库按照前一片博文配置过则需要以下至少三项

1
2
3
4
5
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

REDIS_URL = 'redis://root:密码@主机IP:端口'

第三项请按照你的实际情况配置。 Nice 配置文件写到这儿。我们来做一些基本的反爬虫设置 最基本的一个切换 UserAgent! 首先在项目文件中新建一个 useragent.py 用来写一堆 User-Agent(可以去网上找更多,也可以用下面这些现成的)

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
agents = [
"Mozilla/5.0 (Linux; U; Android 2.3.6; en-us; Nexus S Build/GRK39F) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Avant Browser/1.2.789rel1 (http://www.avantbrowser.com)",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5",
"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.310.0 Safari/532.9",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7",
"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.601.0 Safari/534.14",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.27 (KHTML, like Gecko) Chrome/12.0.712.0 Safari/534.27",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.120 Safari/535.2",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7",
"Mozilla/5.0 (Windows; U; Windows NT 6.0 x64; en-US; rv:1.9pre) Gecko/2008072421 Minefield/3.0.2pre",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.10) Gecko/2009042316 Firefox/3.0.10",
"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.0.11) Gecko/2009060215 Firefox/3.0.11 (.NET CLR 3.5.30729)",
"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 GTB5",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0E)",
"Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110622 Firefox/6.0a2",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:7.0.1) Gecko/20100101 Firefox/7.0.1",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b4pre) Gecko/20100815 Minefield/4.0b4pre",
"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0 )",
"Mozilla/4.0 (compatible; MSIE 5.5; Windows 98; Win 9x 4.90)",
"Mozilla/5.0 (Windows; U; Windows XP) Gecko MultiZilla/1.6.1.0a",
"Mozilla/2.02E (Win95; U)",
"Mozilla/3.01Gold (Win95; I)",
"Mozilla/4.8 [en] (Windows NT 5.1; U)",
"Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.4) Gecko Netscape/7.1 (ax)",
"HTC_Dream Mozilla/5.0 (Linux; U; Android 1.5; en-ca; Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.2; U; de-DE) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/234.40.1 Safari/534.6 TouchPad/1.0",
"Mozilla/5.0 (Linux; U; Android 1.5; en-us; sdk Build/CUPCAKE) AppleWebkit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 1.5; en-us; htc_bahamas Build/CRB17) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 2.1-update1; de-de; HTC Desire 1.19.161.5 Build/ERE27) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; Sprint APA9292KT Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 1.5; de-ch; HTC Hero Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; ADR6300 Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 2.1; en-us; HTC Legend Build/cupcake) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 1.5; de-de; HTC Magic Build/PLAT-RC33) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1 FirePHP/0.3",
"Mozilla/5.0 (Linux; U; Android 1.6; en-us; HTC_TATTOO_A3288 Build/DRC79) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 1.0; en-us; dream) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
"Mozilla/5.0 (Linux; U; Android 1.5; en-us; T-Mobile G1 Build/CRB43) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari 525.20.1",
"Mozilla/5.0 (Linux; U; Android 1.5; en-gb; T-Mobile_G2_Touch Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 2.0; en-us; Droid Build/ESD20) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; Droid Build/FRG22D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 2.0; en-us; Milestone Build/ SHOLS_U2_01.03.1) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.0.1; de-de; Milestone Build/SHOLS_U2_01.14.0) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
"Mozilla/5.0 (Linux; U; Android 0.5; en-us) AppleWebKit/522 (KHTML, like Gecko) Safari/419.3",
"Mozilla/5.0 (Linux; U; Android 1.1; en-gb; dream) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
"Mozilla/5.0 (Linux; U; Android 2.0; en-us; Droid Build/ESD20) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; Sprint APA9292KT Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; ADR6300 Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 2.2; en-ca; GT-P1000M Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 3.0.1; fr-fr; A500 Build/HRI66) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13",
"Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
"Mozilla/5.0 (Linux; U; Android 1.6; es-es; SonyEricssonX10i Build/R1FA016) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 1.6; en-us; SonyEricssonX10i Build/R1AA056) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
]

现在我们来重写一下 Scrapy 的下载中间件(哇靠!!重写中间件 好高端啊!!会不会好难!!!放心!!!So Easy!!跟我做!包教包会,毕竟不会你也不能顺着网线来打我啊): 关于重写中间件的详细情况 请参考 官方文档:http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/downloader-middleware.html#scrapy.contrib.downloadermiddleware.DownloaderMiddleware 在项目中新建一个 middlewares.py 的文件(如果你使用的新版本的 Scrapy,在新建的时候会有这么一个文件,直接用就好了) 首先导入 UserAgentMiddleware 毕竟我们要重写它啊!

1
2
3
4
5
6
import json ##处理json的包
import redis #Python操作redis的包
import random #随机选择
from .useragent import agents #导入前面的
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware #UserAegent中间件
from scrapy.downloadermiddlewares.retry import RetryMiddleware #重试中间件

开写:

1
2
3
4
5
class UserAgentmiddleware(UserAgentMiddleware):

def process_request(self, request, spider):
agent = random.choice(agents)
request.headers["User-Agent"] = agent

第一行:定义了一个类 UserAgentmiddleware 继承自 UserAgentMiddleware 第二行:定义了函数process_request(request, spider)为什么定义这个函数,因为 Scrapy 每一个 request 通过中间 件都会调用这个方法。 QQ20170206-223156 第三行:随机选择一个 User-Agent 第四行:设置 request 的 User-Agent 为我们随机的 User-Agent ^_^Y(^o^)Y 一个中间件写完了!哈哈 是不是 So easy! 下面就需要登陆了。这次我们不用上一篇博文的 FromRequest 来实现登陆了。我们来使用 Cookie 登陆。这样的话我们需要重写 Cookie 中间件!分布式爬虫啊!你不能手动的给每个 Spider 写一个 Cookie 吧。而且你还不会知道这个 Cookie 到底有没有失效。所以我们需要维护一个 Cookie 池(这个 cookie 池用 redis)。 好!来理一理思路,维护一个 Cookie 池最基本需要具备些什么功能呢?

  1. 获取 Cookie
  2. 更新 Cookie
  3. 删除 Cookie
  4. 判断 Cookie 是否可用进行相对应的操作(比如重试)

好,我们先做前三个对 Cookie 进行操作。 首先我们在项目中新建一个 cookies.py 的文件用来写我们需要对 Cookie 进行的操作。 haoduofuli/haoduofuli/cookies.py: 首先日常导入我们需要的文件:

1
2
3
4
5
import requests
import json
import redis
import logging
from .settings import REDIS_URL ##获取settings.py中的REDIS_URL

首先我们把登陆用的账号密码 以 Key:value 的形式存入 redis 数据库。不推荐使用 db0(这是 Scrapy-redis 默认使用的,账号密码单独使用一个 db 进行存储。) QQ20170207-221128@2x 就像这个样子。 解决第一个问题:获取 Cookie:

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
import requests
import json
import redis
import logging
from .settings import REDIS_URL

logger = logging.getLogger(__name__)
##使用REDIS_URL链接Redis数据库, deconde_responses=True这个参数必须要,数据会变成byte形式 完全没法用
reds = redis.Redis.from_url(REDIS_URL, db=2, decode_responses=True)
login_url = 'http://haoduofuli.pw/wp-login.php'

##获取Cookie
def get_cookie(account, password):
s = requests.Session()
payload = {
'log': account,
'pwd': password,
'rememberme': "forever",
'wp-submit': "登录",
'redirect_to': "http://http://www.haoduofuli.pw/wp-admin/",
'testcookie': "1"
}
response = s.post(login_url, data=payload)
cookies = response.cookies.get_dict()
logger.warning("获取Cookie成功!(账号为:%s)" % account)
return json.dumps(cookies)

这段很好懂吧。 使用 requests 模块提交表单登陆获得 Cookie,返回一个通过 Json 序列化后的 Cookie(如果不序列化,存入 Redis 后会变成 Plain Text 格式的,后面取出来 Cookie 就没法用啦。) 第二个问题:将 Cookie 写入 Redis 数据库(分布式呀,当然得要其它其它 Spider 也能使用这个 Cookie 了)

1
2
3
4
5
6
7
def init_cookie(red, spidername):
redkeys = reds.keys()
for user in redkeys:
password = reds.get(user)
if red.get("%s:Cookies:%s--%s" % (spidername, user, password)) is None:
cookie = get_cookie(user, password)
red.set("%s:Cookies:%s--%s"% (spidername, user, password), cookie)

使用我们上面建立的 redis 链接获取 redis db2 中的所有 Key(我们设置为账号的哦!),再从 redis 中获取所有的 Value(我设成了密码哦!) 判断这个 spider 和账号的 Cookie 是否存在,不存在 则调用 get_cookie 函数传入从 redis 中获取到的账号密码的 cookie; 保存进 redis,Key 为 spider 名字和账号密码,value 为 cookie。 这儿操作 redis 的不是上面建立的那个 reds 链接哦!而是 red;后面会传进来的(因为要操作两个不同的 db,我在文档中没有看到切换 db 的方法,只好这么用了,知道的小伙伴儿留言一下)。 spidername 获取方式后面也会说的。 还有剩余的更新 Cookie 删除无法使用的账号等,大家伙可以自己试着写写(写不出来也没关系 不影响正常使用) 好啦!搞定!简直 So Easy!!!! 现在开始大业了!重写 cookie 中间件;估摸着吧!聪明的小伙儿看了上面重写 User-Agent 的方法,十之八九也知道怎么重写 Cookie 中间件了。 好啦,现在继续写 middlewares.py 啦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CookieMiddleware(RetryMiddleware):

def __init__(self, settings, crawler):
RetryMiddleware.__init__(self, settings)
self.rconn = redis.from_url(settings['REDIS_URL'], db=1, decode_responses=True)##decode_responses设置取出的编码为str
init_cookie(self.rconn, crawler.spider.name)

@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings, crawler)

def process_request(self, request, spider):
redisKeys = self.rconn.keys()
while len(redisKeys) > 0:
elem = random.choice(redisKeys)
if spider.name + ':Cookies' in elem:
cookie = json.loads(self.rconn.get(elem))
request.cookies = cookie
request.meta["accountText"] = elem.split("Cookies:")[-1]
break

第一行:不说 第二行第三行得说一下 这玩意儿叫重载(我想了大半天都没想起来叫啥,还是问了大才。尴尬)有啥用呢: 也不扯啥子高深问题了,小伙伴儿可能发现,当你继承父类之后;子类是不能用 def init()方法的,不过重载父类之后就能用啦! 第四行:settings[‘REDIS_URL’]是个什么鬼?这是访问 scrapy 的 settings。怎么访问的?下面说 第五行:往 redis 中添加 cookie。第二个参数就是 spidername 的获取方法(其实就是字典啦!)

1
2
3
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings, crawler)

这个貌似不好理解,作用看下面: D9DF3655-F28A-482C-8B02-C53B152958A0 这样是不是一下就知道了?? 至于访问 settings 的方法官方文档给出了详细的方法: http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/settings.html#how-to-access-settings QQ20170207-233701@2x 下面就是完整的 middlewares.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
# -*- coding: utf-8 -*-

# Define here the models for your spider middleware
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/spider-middleware.html

from scrapy import signals
import json
import redis
import random
from .useragent import agents
from .cookies import init_cookie, remove_cookie, update_cookie
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware
from scrapy.downloadermiddlewares.retry import RetryMiddleware
import logging


logger = logging.getLogger(__name__)

class UserAgentmiddleware(UserAgentMiddleware):

def process_request(self, request, spider):
agent = random.choice(agents)
request.headers["User-Agent"] = agent


class CookieMiddleware(RetryMiddleware):

def __init__(self, settings, crawler):
RetryMiddleware.__init__(self, settings)
self.rconn = redis.from_url(settings['REDIS_URL'], db=1, decode_responses=True)##decode_responses设置取出的编码为str
init_cookie(self.rconn, crawler.spider.name)

@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings, crawler)

def process_request(self, request, spider):
redisKeys = self.rconn.keys()
while len(redisKeys) > 0:
elem = random.choice(redisKeys)
if spider.name + ':Cookies' in elem:
cookie = json.loads(self.rconn.get(elem))
request.cookies = cookie
request.meta["accountText"] = elem.split("Cookies:")[-1]
break
#else:
#redisKeys.remove(elem)

#def process_response(self, request, response, spider):

#"""
#下面的我删了,各位小伙伴可以尝试以下完成后面的工作

#你需要在这个位置判断cookie是否失效

#然后进行相应的操作,比如更新cookie 删除不能用的账号

#写不出也没关系,不影响程序正常使用,

#"""

存储我也不写啦!就是这么简单一个分布式的 scrapy 就这么完成啦!!! 我试了下 三台机器 两个小时 就把整个站点全部爬完了。 弄好你的存储 放在不同的机器上就可以跑啦! 完整的代码在 GitHub 上: GitHub:https://github.com/thsheep/haoduofuli Y(^o^)Y 完工 下篇博文来对付爬虫的大敌:Ajax 以后的教程用微博做靶子,那些数据比较有用,可以玩玩分析什么的。

技术杂谈

各位小伙伴 大家好啊!年假结束了··· 也该开始继续我的装逼之旅了。 年前博文的结尾说了 还有一个基于 Scrapy 的分布式版本、 今天这博文就先给大家做些前期工作,其实吧、最主要的是防止你的服务器因为这篇博文被轮········· 博文开始之前 我们先来看篇文章: http://www.youxia.org/daily-news-attack-extortion-does-not-delay-a-week-had-27000-mongodb-database.html 关于年前 MongoDB 由于默认可匿名访问 而导致了一大堆的管理员掉坑里 预估中国有十万数据库被坑。 这是继 Redis 之后又一个小白式的错误······(Redis 也是默认匿名访问) 所以在下一篇博文开始之前,先给一些新手小伙伴做一些准备工作。 因为篇幅较少 先写写 Redis 的一些安全设置: 安装 Redis: 请参考这儿;https://redis.io/download

1
2
3
4
5
6
$ wget http://download.redis.io/releases/redis-3.2.7.tar.gz
$ tar xzf redis-3.2.7.tar.gz
$ cd redis-3.2.7
$ make

$ src/redis-server

ps :如果以上有报错,可能是你的服务器没有安装依赖: CentOS7:

1
yum install -y gcc-c++ tcl

只写关于 Linux 的、Windows 的很简单,配置文件通用: 安装完成后 在目录 redis-3.2.7 中有一个 redis.conf 的配置文件,按照默认习惯我们将其复制到/etc 目录下:

1
[root@MyCloudServer ~]# cp redis-3.2.7/redis.conf /etc

PS:请使用复制(cp)而不要使用移动(mv);毕竟你要弄错了还可以再拷贝一份儿过去用不是? 使用 vim 编辑刚刚拷贝的 redis.conf

1
vim /etc/redis.conf

PS:使用 vim 需要先安装: CentOS7:

1
yum  install vim

我们需要注意以下几项: 1、注释掉 47 行的 bind 127.0.0.1(这个意思是限制为只能 127.0.0.1 也就是本机登录)PS:个人更建议 将你需要连接 Redis 数据库的 IP 地址填写在此处,而不是注释掉。这样做会比直接注释掉更加安全。 2、更改第 84 行 port 6379 为你需要的端口号(这是 Redis 的默认监听端口)PS:个人建议务必更改 3、更改第 128 行 daemonize no 为 daemonize yes(这是让 Redis 后台运行) PS:个人建议更改 4、取消第 480 # requirepass foobared 的#注释符(这是 redis 的访问密码) 并更改 foobared 为你需要的密码 比如 我需们需要密码为 123456 则改为 requirepass 123456。PS:密码不可过长否则 Python 的 redis 客户端无法连接 以上配置文件更改完毕,需要在防火墙放行:

1
firewall-cmd --zone=public --add-port=xxxx/tcp --permanent

请将 xxxx 更改为你自己的 redis 端口。 重启防火墙生效:

1
systemctl restart firewalld.service

指定配置文件启动 redis:

1
[root@MyCloudServer ~]# redis-3.2.7/src/redis-server /etc/redis.conf

加入到开机启动:

1
echo "/root/redis-3.2.6/src/redis-server /etc/redis.conf" >> /etc/rc.local

一个较为安全的 redis 配置完毕。 redis 的桌面客户端我推荐:RedisDesktopManager 去下面这个地址下载就不需要捐助啦! https://github.com/uglide/RedisDesktopManager/releases 当然还有一些其他配置、我们用不到也就不写啦! MongoDB: 这次 MongoDB 挺惨啊!由于默认匿名访问、下面给 MongoDB 配置一点安全措施: 安装 MongoDB: 以 CentOS7 为例其余发行版请参考官方文档:https://docs.mongodb.com/manual/administration/install-on-linux/ 1、建一个 yum 源:

1
[root@MyCloudServer ~]# vim /etc/yum.repos.d/mongodb-org-3.4.repo

写入以下内容:

1
2
3
4
5
6
[mongodb-org-3.4]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/3.4/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-3.4.asc

2、安装 mongoDB 以及相关工具:

1
sudo yum install -y mongodb-org

3、启动 MongoDB:

1
sudo service mongod start

PS:如果你的服务器在使用 SELinux 的话,你需要配置 SElinux 允许 MongoDB 启动,当然更简单的方法是关掉 SElinux。 关闭 SElinux:

1
[root@MyCloudServer ~]# vim /etc/selinux/config

将第 7 行设置为:SELINUX=disabled 4、停止 MongoDB:

1
sudo service mongod stop

上面安装完按成了 MongoDB 下面要步入正题了: 1、备份和更改配置文件:

1
2
[root@MyCloudServer ~]# cp /etc/mongod.conf  /etc/mongod_backup.conf
[root@MyCloudServer ~]# vim /etc/mongod.conf

更改第 28 行 prot 2701 为你需要更改的端口(这是 MongoDB 默认的监听端口) 更改第 29 行 bindIp: 127.0.0.1 为 0.0.0.0(MongoDB 默认只能本地访问)PS:个人建议此处添加你需要连接 MongoDB 服务器的 IP 地址、而不是改成 0.0.0.0。这样做会更安全 启动 MongoDB:

1
mongod --config /etc/mongod.conf

意思是:指定/etc/mongod.conf 为配置文件启动 MongoDB 好了、配置文件更改完毕,现在可以外网访问我们的 MongoDB 了!不需要用户名!匿名的!现在我们进行下一步设置。 因为 MongoDB 默认是匿名访问的、我们需要开启用户认证。 我估摸着很多哥们儿和我一样没补全 啥都不会干、所以直接在服务器上改就不太现实了,需要借助于第三方客户端。我个人推荐:mongobooster 官方地址:https://mongobooster.com/ 收费版免费版功能一样 不用在意: 首先我们需要连上 MongoDB 服务器(别忘了防火墙放行你使用的端口啊!!!) 170203 连上之后大慨是这个样子: 17020301 按下 Ctrl+T 打开 shell 界面输入一下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use admin
db.createUser(
{
user: "你的用户名",
pwd: "你的密码",
roles: [ {role:"userAdminAnyDatabase", db:"admin"} ]
/* All build-in Roles
Database User Roles: read|readWrite
数据库用户角色:读|读写
Database Admion Roles: dbAdmin|dbOwner|userAdmin
数据库管理角色:数据库管理员|数据库所有者|用户管理
Cluster Admin Roles: clusterAdmin|clusterManager|clusterMonitor|hostManager
集群管理角色:
Backup and Restoration Roles: backup|restore
All-Database Roles: readAnyDatabase|readWriteAnyDatabase|userAdminAnyDatabase|dbAdminAnyDatabase
所有数据库角色:读所有数据库|读写所有数据库|所有数据库的用户管理员|所有数据库的管理员
Superuser Roles: root */
}
)

再点击 run 运行即可 会在信息栏中提示 True 现在断开数据库连接、再打开会发现多出一个 admin 的数据库。 QQ截图20170204001502 上面的都做了些什么呢? 首先我们新建了一个 admin 的数据库(MongoDB 的原则哦、有则切换没有就创建) 然后在 admin 数据中创建了一个用户 和 密码 赋予了这个用户管理 admin 数据库 所有数据库用户的权限。 至于有那些权限 在注释中都有写哦!常用的我估摸着写了个对应意思········· OK!搞定这一部分 就可以开启 MongoDB 的用户认证了! 怎么开启呢?首先关闭正在运行的 MongoDB:

1
ps -e | grep mongod

上面的命令会找出 MongoDB 的进程号、然后运行 kill 进程号即可! 开启 MongoDB:

1
mongod --auth --config /etc/mongod.conf

意思是:以认证模式 指定/etc/mongod.conf 启动 MongoDB。 加入开机启动:

1
echo "mongod --auth --config /etc/mongod.conf" >> /etc/rc.local

好了!现在 MongoDB 也配置完成 啦! 现在如果你需要新建一个用户让其使用数据库 你该怎么做呢? 像下面这样;首先你需要连接到 admin 数据库! 在选项 Basic 中照常配置: QQ20170204-004332@2x 需要额外设置的是 Authentication 选项: QQ20170204-004627@2x 连接成功后大概是这个样子: QQ20170204-004930@2x 需要注意的一点是:这个用户只能看到所有的数据库和用户、并不能看到数据!因为我们创建的时候只给了所有数据库用户管理的权限哦! 然后打开 shell 界面按照创建 admin 的模板执行即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use 想要创建的数据库
db.createUser(
{
user: "想要使用的用户名",
pwd: "想要使用的密码",
roles: [ {role:"赋予什么样的权限", db:"创建的数据库"} ]
/* All build-in Roles
Database User Roles: read|readWrite
数据库用户角色:读|读写
Database Admion Roles: dbAdmin|dbOwner|userAdmin
数据库管理角色:数据库管理员|数据库所有者|用户管理
Cluster Admin Roles: clusterAdmin|clusterManager|clusterMonitor|hostManager
集群管理角色:
Backup and Restoration Roles: backup|restore
All-Database Roles: readAnyDatabase|readWriteAnyDatabase|userAdminAnyDatabase|dbAdminAnyDatabase
所有数据库角色:读所有数据库|读写所有数据库|所有数据库的用户管理员|所有数据库的管理员
Superuser Roles: root */
}
)

创建完成后、就可以用创建好的用户名和密码去链接有权限的数据库啦!!是不是 So Easy!!! 其实吧 还是 bindIp 安全 哈哈哈! 以上完毕!! 下一篇就是基于 Scrapy-Redis 的分布式了、真的超级简单!简单得不要不要的

个人日记

没有选择那个二零一六年尾,而是选择了这个二零一六年尾来总结。

毕竟元旦那时候真的被一堆考试烦透,说到考试,可以说我是极其反对这种形式,在我看来,因为有了考试,学一门课反倒成了任务,而不是真正踏实地去学,有了考试,学习的目的不再是单纯学习,而是为了最后的应考。所以很多科目,经验之谈,一旦它成了我的课程,我反倒没有那么多耐心去学它。而又有很多考试,理解性的东西真的不考察理解,你背过,就高分了,背不过,那就没分。做到原题了,就有分了,做不到原题,那就不一定有分。到头来,一门课程的结束伴随着你仅仅在短时间内记忆了一些概念和题目去应考。考试结束,抛掉了,你还记得什么?何况,某些课,你可能这辈子都用不到了。 然而就是这样,或许真的没有比这更合适的考察方式了吧。 果然一扯就停不下来,后面简单点扯。 嗯,就是这样,我来北航读研了,2016级的新生,刚刚渡过了研究生第一个学期。这个学期,基本上把研究生所有的课都上完。我能体会到自己还是偏重于实践性的东西而非理论,一个想法,纯理论都是空谈,实现出来才是最终目标。作为一名程序猿,平时我喜欢瞎捣腾些东西,逛GitHub,搜开源项目,找到有趣的组件来实现自己想要的功能。 二零一六年上半年,毕设的一段时间吧,由于自己对爬虫比较感兴趣,正好毕设也有个选题是关于爬虫的,所以干脆毕设就实现了一个分布式爬虫框架,虽然也是开源项目组合起来的,Scrapy,Redis,Mongo,Splash,Django等等吧,不过这个过程的探索也是受益匪浅。哦对了,也是上半年这个时候吧,换上了自己的第一台Mac,联想也终于寿终正寝了,我也算是真正踏上了程序员的行列。一年下来,不得不说,开发真的太便捷。 那时候正好是大四,也没多少事,期间也接着大大小小的外包,赚点外快,后来又入手了单反,然而到现在我发现自己没有那么狂爱摄影。 每年都有毕业季,今年轮到我们了。毕业行去了云南,还有些意犹未尽的感觉,也感谢一路同行的小伙伴给我拍的绝世美照哈哈。后来忙着毕业照啦,穿上学士服,辗转各大校区,各种奇怪的姿势拍拍拍。现在真的挺想念山大的,那里的人儿,那里的事儿。嗯,毕业快乐。 暑假,我又回到北京。一件重要的事那就是女朋友保研,虽然中间出了点小叉子,不过还是恭喜她能被中科院录取,随后在北京呆了近整个暑假。 随之而来的,便是北航研究生的新学期了。嗯,从山大到了北航。开学时我并没有那么欣喜,或许是已经过来太多次了习惯了。上学期课满满当当,然而你以为我会乖乖听课?我可不是那种学霸。我总是有着自己的学习和项目计划,学习一些我觉得有用的东西,比如Andrew Ng的机器学习、Web相关知识还有在做自己在忙的一些项目。前面说了我不喜欢上课,不喜欢考试,因为我觉得这些时间,可以去做更有意义的事情。最后几个星期突击一下就好了。其实我的大学就是这么过来的,上课都在学习别的和撸代码去了,成绩也还说得过去,不过感觉这样还是挺充实的。然而考前突击的时候是难了点儿,因为大部分我得预习。还好,这学期过去了,后面的时间我终于可以尽情做我想做的事情了,喜欢无拘无束自己探索的感觉。 期间其实还在和同学创业,演艺行业平台,自己负责技术这方面,好玩表演(hwby.com),一年来了吧,网站实现后投入运营,前期还是非常艰难,不过近期也还是有了起色,继续加油。写的过程中也抽离出了自己的一套CMS,以便后期开发应用的时候更加便捷,现在还不成熟,暂未公开。 说一件值得骄傲的事情吧,每天坚持记有道,把每天完成的事情,成功的事情,失败的事情每天做一下总结,这种感觉似乎是记录了自己路途的脚印,自己能感觉出自己走了多远,收获了多少,有一种自我激励的感觉。从14年开始记录到到今天了,希望自己能坚持下去。 哦又想到一个,之前博客上会有很多人加我,后来我想,干脆建一个交流群多好,于是乎在九月份左右,进击的Coder诞生了,三个多月的时间吧,几乎每天都有人加,刚才看了下已经788人啦,在群里跟大家探讨经验,交流技术,没事吐吐槽,扯扯淡,真的很愉快,爱你们。 然而现在还是觉得自己有时候懒癌发作之后就什么也不想干,执行力差,定了一些计划,今天拖明天,明天拖后天,最后就那么不了了之了。半年前定的学习鬼步舞呢,到现在跳的依然那么差。说好的练好腹肌呢,现在似乎没多大效果。 总结了这么多,似乎也没有多么值得骄傲的一件事,算是瞎忙了一整年吧哈哈。 新年计划: 1.写一本爬虫的书并出版,出套算不上教程的经验分享 2.完善好我的CMS,长期维护下去 3.学习数据挖掘和Web安全,向大牛进发 4.懒癌,不敢说改掉,但也能稍微缓解下吧 5.好玩表演,燥起来。 太多太多…. 觉得自己不会的还是太多,想学的也太多,好好提高自己的执行力和自制力吧,新的一年成为更好的自己。 凌晨三点了,安。

PHP

博主在搞Web开发主要采用的是Laravel,然而发现其对PHP版本的要求是越来越高,PHP5.6已经越来受到限制,Laravel 5.5将正式弃用PHP5.6,所以博主决定直接升级到7.1版本。

移除旧版本

由于系统本身已经装了PHP5.6,所以需要先将其移除。 在这里列出目录以及移除需要的命令。

1
2
3
4
5
6
7
8
/private/etc/               sudo rm -rf php-fpm.conf.default php.ini php.ini.default
/usr/bin/ sudo rm -rf php php-config phpdoc phpize
/usr/include sudo rm -rf php
/usr/lib sudo rm -rf php
/usr/sbin sudo rm -rf php-fpm
/usr/share sudo rm -rf php
/usr/share/man/man1 sudo rm -rf php-config.1 php.1 phpize.1
/usr/share/man/man8 sudo rm -rf php-fpm.8

顺次手动删除它们即可。

搞清关系

在卸载过程中你会发现有PHP、FastCGI、php-fpm、spawn-fcgi等等的概念,所以在这里先梳理一下。

CGI

CGI是为了保证web server传递过来的数据是标准格式的,方便CGI程序的编写者。 web server(比如说nginx)只是内容的分发者。比如,如果请求/index.html,那么web server会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。好了,如果现在请求的是/index.php,根据配置文件,nginx知道这个不是静态文件,需要去找PHP解析器来处理,那么他会把这个请求简单处理后交给PHP解析器。Nginx会传哪些数据给PHP解析器呢?url要有吧,查询字符串也得有吧,POST数据也要有,HTTP header不能少吧,好的,CGI就是规定要传哪些数据、以什么样的格式传递给后方处理这个请求的协议。仔细想想,你在PHP代码中使用的用户从哪里来的。 当web server收到/index.php这个请求后,会启动对应的CGI程序,这里就是PHP的解析器。接下来PHP解析器会解析php.ini文件,初始化执行环境,然后处理请求,再以规定CGI规定的格式返回处理后的结果,退出进程。web server再把结果返回给浏览器。

FastCGI

Fastcgi是用来提高CGI程序性能的。 那么CGI程序的性能问题在哪呢?”PHP解析器会解析php.ini文件,初始化执行环境”,就是这里了。标准的CGI对每个请求都会执行这些步骤(不闲累啊!启动进程很累的说!),所以处理每个时间的时间会比较长。这明显不合理嘛!那么Fastcgi是怎么做的呢?首先,Fastcgi会先启一个master,解析配置文件,初始化执行环境,然后再启动多个worker。当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求。这样就避免了重复的劳动,效率自然是高。而且当worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是fastcgi的对进程的管理。

PHP-FPM

是一个实现了Fastcgi的程序,被PHP官方收了。 大家都知道,PHP的解释器是php-cgi。php-cgi只是个CGI程序,他自己本身只能解析请求,返回结果,不会进程管理(皇上,臣妾真的做不到啊!)所以就出现了一些能够调度php-cgi进程的程序,比如说由lighthttpd分离出来的spawn-fcgi。好了PHP-FPM也是这么个东东,在长时间的发展后,逐渐得到了大家的认可(要知道,前几年大家可是抱怨PHP-FPM稳定性太差的),也越来越流行。 php-fpm的管理对象是php-cgi。但不能说php-fpm是fastcgi进程的管理器,因为前面说了fastcgi是个协议,似乎没有这么个进程存在,就算存在php-fpm也管理不了他(至少目前是)。 有的说,php-fpm是php内核的一个补丁 以前是对的。因为最开始的时候php-fpm没有包含在PHP内核里面,要使用这个功能,需要找到与源码版本相同的php-fpm对内核打补丁,然后再编译。后来PHP内核集成了PHP-FPM之后就方便多了,使用\--enalbe-fpm这个编译参数即可。

安装PHP7.1

用brew进行安装。

1
2
brew install homebrew/php/php71
brew install homebrew/php/php71-mcrypt

安装完了之后它会自带PHP-FPM,在 启动PHP-FPM

1
sudo php-fpm

配置文件目录

php.ini

1
/usr/local/etc/php/7.1/php.ini

php-fpm.conf

1
/usr/local/etc/php/7.1/php-fpm.conf

php-fpm

1
/usr/local/opt/php71/sbin/php-fpm

但是执行php-fpm发现没有反应,所以这里需要加一个symlink

1
ln -s /usr/local/opt/php71/sbin/php-fpm /usr/local/bin/php-fpm

然后运行php-fpm

1
sudo php-fpm

启动nginx

1
sudo nginx

关于MySQL和其他的安装在这就不再赘述。 以上便完成了PHP的升级。

Python

QQ图片20161021225948其实拿这个网站当教程刚开始我是拒绝、换其他网站吧,又没什么动力···· 然后就··········· 上一篇 Scrapy 带大家玩了 Spider 今天带带大家玩的东西有两点、第一 CrawlSpider、第二 Scrapy 登录。 目标站点:www.haoduofuli.wang 9555112 Go Go Go!开整! 还记得第一步要干啥? 创建项目文件啊!没有 Scrapy 环境的小伙伴们请参考第一篇安装一下环境哦! 打开你的命令行界面(Windows 是 CMD)使用切换目录的命令到你需要的存放项目文件的磁盘目录

1
2
D:
scrapy startproject haoduofuli

好了 我在 D 盘创建了一个叫做 haoduofuli 的项目。 用 Pycharm 打开这个目录开始我们的爬取之路 Come on! 下一步我们该做什么记得吧?当然是在 items.py 中声明字段了!方便我们在 Spider 中保存获取的内容并通过 Pipline 进行保存(items.py 本质上是一个 dict 字典) 我在 items.py 中声明了以下类容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class HaoduofuliItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()

category = scrapy.Field() #类型
title = scrapy.Field() #标题
imgurl = scrapy.Field() #图片的地址
yunlink = scrapy.Field() #百度云盘的连接
password = scrapy.Field() #百度云盘的密码
url = scrapy.Field() #页面的地址

至于为啥声明的这些类容:各位自己去网站上观察一下、(主要是吧,贴在这儿的话 估计这博文就要被人道主义销毁了) 别忘记上一篇博文教大家的那种在 IDE 中运行 Scrapy 的方法哦! 好上面的我们搞定、开始下一步编写 Spider 啦! QQ图片20161021223818 在 spiders 文件夹中新建一个文件 haoduofuli.py(还不清楚目录和作用的小哥儿快去看看 Scrapy 的第一篇) 首先导入以下包:

1
2
3
4
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpider与Rule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包

详细介绍请参考:http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/spiders.html 中的:CrawlSpider、爬取规则(Crawling rules)、pare_start_url(response)|(此方法重写 start_urls)、以及 Spider 中 start_requests()方法的重写。 下面我带大家简单的玩玩儿顺便获取我们想要的东西。 前面提到了我们需要获取全站的资源、如果使用 Spider 的话就需要写大量的代码(当然只是相对而言的大量代码)!但是我们还有另一个选择那就是今天要说的 CrawlSpider! 吃惊表情1 首先我们新建一个函数 继承 CrawlSpider(上一篇博文是继承 Spider 哦!) 见证奇迹的时刻到了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpider与Rule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包

class myspider(CrawlSpider):

name = 'haoduofuli'
allowed_domains = ['haoduofuli.wang']
start_urls = ['http://www.haoduofuli.wang']

rules = (
Rule(LinkExtractor(allow=('\.html',)), callback='parse_item', follow=True),
)

def parse_item(self, response):
print(response.url)
pass

是不是很厉害!加上中间的空行也就不到二十行代码啊!就把整个网站历遍了!So Easy!! 上面的几行代码的意思 很明了了啊!我只说说 rules 这一块儿 表示所有 response 都会通过这个规则进行过滤匹配、匹配啥?当然是后缀为.html 的 URL 了、callback=’parseitem’表示将获取到的 response 交给 parse_item 函数处理(这儿要注意了、不要使用 parse 函数、因为 CrawlSpider 使用的 parse 来实现逻辑、如果你使用了 parse 函数、CrawlSpider 会运行失败。)、follow=True 表示跟进匹配到的 URL(顺便说一句 allow 的参数支持正则表达式、虽然我也用得不熟、不过超级好使) 至于我这儿的 allow 的参数为啥是’.\html’;大伙儿自己观察一下我们需要获取想要信息的页面的 URL 是不是都是以.html 结束的?明白了吧! 然后 rules 的大概运作方式是下面这样: QQ截图20170122164117 图很清晰明了了(本人也是初学、如有错误 还请各位及时留言 我好纠正。)中间的数据流向是靠引擎来完成的。 好了 我们来看看效果如何: QQ20170122-011812 这是我们返回 response 的 URL、一水儿的 URL 啊!完美!下面就可以进行提取数据了(诶!不对啊怎么没有没什么提取工具啊!还记得上篇博文说的不?下载器返回的 response 是支持 Xpath 的哦!我们直接使用 Xpath 来提取数据就行啦!) 表情2 那么问题来了!Xpath 没用过啊!不会用啊!这可咋整啊!别怕!草鸡简单的!!来不着急! 先大声跟我念:Google 大法好啊! 哈哈哈 没错、我们需要 Chrome(至于为啥不用 Firefox、因为不知道为啥 Firefox 的 Xpath 有时和 Chrome 的结构不一样 有些时候提取不到数据、Chrome 则没什么问题) 来来!跟着我的节奏来!包你五分钟学会使用 Xpath!学不会也没关系、毕竟你也不能顺着网线来打我啊! 第一步:打开你的 Chrome 浏览器 挑选上面任意一个 URL 打开进入我们提取数据的页面(不贴图 容易被 Say GoogBay): 第二步:打开 Chrome 的调试模式找到我们需要提取的内容(如何快速找到呢?还不知道的小哥儿 我只能说你实在是太水了) 点击下面红圈的箭头 然后去网页上点击你需要的内容就 哔!的一下跳过去了! QQ20170122-013435 第三步:在跳转的那一行就是你想要提取内容的一行(背景色完全区别于其它行!!)右键 Copy ——Copy XPath: 就像下面我提取标题: QQ20170122-013823 你会得到这样的内容: //[@id=”postcontent”]/p[1] 意思是:在根节点下面的有一个 id 为 post_content 的标签里面的第一个 p 标签(p[1]) 如果你需要提取的是这个标签的文本你需要在后面加点东西变成下面这样: //[@id=”post_content”]/p[1]/text() 后面加上 text()标签就是提取文本 如果要提取标签里面的属性就把 text()换成@属性比如: //*[@id=”post_content”]/p[1]/@src So Easy!XPath 提取完毕!来看看怎么用的!那就更简单了!!!! response.xpath(‘你 Copy 的 XPath’).extract()[‘要取第几个值’] 注意 XPath 提取出来的默认是 List。 QQ图片20161021224219 看完上面这一段 估计还没有五分钟吧 !好了 XPath 掌握了!我们来开始取我们想要的东西吧!现在我们的代码应该变成这样了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpider与Rule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包

class myspider(CrawlSpider):

name = 'haoduofuli'
allowed_domains = ['haoduofuli.wang']
start_urls = ['http://www.haoduofuli.wang']

rules = (
Rule(LinkExtractor(allow=('\.html',)), callback='parse_item', follow=True),
)

def parse_item(self, response):
item = HaoduofuliItem()
item['url'] = response.url
item['category'] = response.xpath('//*[@id="content"]/div[1]/div[1]/span[2]/a/text()').extract()[0]
item['title'] = response.xpath('//*[@id="content"]/div[1]/h1/text()').extract()[0]
item['imgurl'] = response.xpath('//*[@id="post_content"]/p/img/@src').extract()
return item

我们来跑一下!简直完美! QQ20170122-020745 关于 imgurl 那个 XPath: 你先随便找一找图片的地址 Copy XPath 类似得到这样的: //[@id=”post_content”]/p[2]/img 你瞅瞅网页会发现每一个有几张图片 每张地址都在一个 p 标签下的 img 标签的 src 属性中 把这个 2 去掉变成: //[@id=”post_content”]/p/img 就变成了所有 p 标签下的 img 标签了!加上 /@src 后所有图片就获取到啦!(不加[0]是因为我们要所有的地址、加了 就只能获取一个了!) 关于 XPath 更多的用法与功能详解,建议大家去看看 w3cschool (^o^)/ 第一部分完工、开始第二部分的工作吧!登!录! QQ图片20161022193315 毕竟这些都不是我们要的重点!我们要的是资源 资源啊!能下载东西的地方!如果不是为了资源 那么爬虫将毫无意义(给工钱的另算)。 但是下载资源是隐藏的,需要登录才能看见(别找我要帐号、我也是借的别人的。) 我们先来看看这个网站是怎么登录的,使用 Firefox 打开www.haoduofuli.wang/login.php(为啥是Firefox、因为个人感觉Firefox的表单界面看起来很爽啊!哈哈哈) 打开页面之后开启调试模式(怎么开不说了)—开启持续日志(不然跳转之后没了) QQ截图20170122101749 然后选择网络—选中 html 和 XHR(这样页面类容就会少很多、又不会缺少我们需要的东西) QQ截图20170122103140 现在开始登录(顺手把记住登录也勾上)!调试窗口不要关啊!!!!登录完毕之后你会发现出现一些内容 我们找到其中方法为 post 的请求、然后选择 参数 就能看到我们需要的登录表单啦! QQ截图20170122104241 我划掉的是帐号密码、这个位置应该显示你的帐号密码(这是很简单的一个登录表单、不通用但是思路是一样的。)找到了我们想要的东西我们开始登录吧 首先要知道 Scrapy 登录是如何实现的? 借助于 FromRequests 这个包实现的(前面已经导入过了),下面开整。不需要太大的改动只需增加一些函数 就可以轻而易举的实现的登录。 将我们的 start_urls 中的地址换掉换成我们我们的登陆地址www.haoduofuli.wang/login.php变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpider与Rule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包



account = '你的账号'
password = '你的密码'

class myspider(CrawlSpider):

name = 'haoduofuli'
allowed_domains = ['haoduofuli.wang']
start_urls = ['http://www.haoduofuli.wang/wp-login.php']

那么问题来了!参考上面的流程图你会发现、这丫的没法登录表单没法写啊!start_urls 返回的 responses 就直接给 rules 进行处理了诶!我们需要一个什么方法来截断 start_urls 返回的 responses 方便我们把登录的表单提交上去!那么问题来了 !该用啥? 答案是:parse_start_url(response)这方法;此方法作用是当 start_url 返回 responses 时调用这个方法。官方解释如下: QQ截图20170122105258 然后呢?当然是构造表单并通过 FormRequests 提交了!所以我们的程序现在就应该变成这样子了:

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
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpider与Rule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包



account = '你的帐号'
password = '你的密码'

class myspider(CrawlSpider):

name = 'haoduofuli'
allowed_domains = ['haoduofuli.wang']
start_urls = ['http://www.haoduofuli.wang/wp-login.php']

def parse_start_url(self, response):
###
如果你登录的有验证码之类的,你就可以在此处加入各种处理方法;
比如提交给打码平台,或者自己手动输入、再或者pil处理之类的
###
formdate = {
'log': account,
'pwd': password,
'rememberme': "forever",
'wp-submit': "登录",
'redirect_to': "http://www.haoduofuli.wang/wp-admin/",
'testcookie': "1"
}
return [FormRequest.from_response(response, formdata=formdate, callback=self.after_login)]

最后一句的意思是提交表单 formdate 并将回调 after_login 函数处理后续内容(一般用来判断是否登录成功) 然后开始请求我们需要爬取的页面 现在就变成这样了!

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
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpiderRule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包



account = '你的帐号'
password = '你的密码'

class myspider(CrawlSpider):

name = 'haoduofuli'
allowed_domains = ['haoduofuli.wang']
start_urls = ['http://www.haoduofuli.wang/wp-login.php']

def parse_start_url(self, response):
###
如果你登录的有验证码之类的,你就可以在此处加入各种处理方法;
比如提交给打码平台,或者自己手动输入、再或者pil处理之类的
###
formdate = {
'log': account,
'pwd': password,
'rememberme': "forever",
'wp-submit': "登录",
'redirect_to': "http://www.haoduofuli.wang/wp-admin/",
'testcookie': "1"
}
return [FormRequest.from_response(response, formdata=formdate, callback=self.after_login)]


def after_login(self, response):
###
可以在此处加上判断来确认是否登录成功、进行其他动作。
###
lnk = 'http://www.haoduofuli.wang'
return Request(lnk)

rules = (
Rule(LinkExtractor(allow=('\.html',)), callback='parse_item', follow=True),
)

def parse_item(self, response):
item = HaoduofuliItem()
try:
item['category'] = response.xpath('//*[@id="content"]/div[1]/div[1]/span[2]/a/text()').extract()[0]
item['title'] = response.xpath('//*[@id="content"]/div[1]/h1/text()').extract()[0]
item['imgurl'] = response.xpath('//*[@id="post_content"]/p/img/@src').extract()
item['yunlink'] = response.xpath('//*[@id="post_content"]/blockquote/a/@href').extract()[0]
item['password'] = response.xpath('//*[@id="post_content"]/blockquote/font/text()').extract()[0]
return item
except:
item['category'] = response.xpath('//*[@id="content"]/div[1]/div[1]/span[2]/a/text()').extract()[0]
item['title'] = response.xpath('//*[@id="content"]/div[1]/h1/text()').extract()[0]
item['imgurl'] = response.xpath('//*[@id="post_content"]/p/img/@src').extract()
item['yunlink'] = response.xpath('//*[@id="post_content"]/blockquote/p/a/@href).extract()[0]
item['password'] = response.xpath('//*[@id="post_content"]/blockquote/p/span/text()').extract()[0]
return item

return Request(lnk)就表示我们的开始页面了 至于为啥多了一个 try 判断;完全是因为 这站长不守规矩啊!有些页面不一样·····我能怎么办 我也很无奈啊! 都是被逼的。囧 好了!Spider 写完啦!但是我们的工作还没完!!!网站是靠什么知道这个 request 是否是登录用户发出的?答案是 Cookie! 所以我们需要 下载器 在下载网页之前在 request 中加入 Cookie 来向网站证明我们是登录用户身份;才能获取到需要登录才能查看的信息! 这个该怎么做?现在 Scrapy 的中间件派上用场了! 关于 Cookie 中间件参考:http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/downloader-middleware.html#module-scrapy.contrib.downloadermiddleware.cookies 我们需要做的就是在 settings.py 中的 DOWNLOADER_MIDDLEWARES 开启这个中间件:scrapy.downloadermiddlewares.cookies.CookiesMiddleware 请注意!!!!!! 每一个中间件会对 request 进行操作、你所做的操作可能会依赖于前一个中间件、所以每个中间件的顺序就异常的重要。具体该设置多少请参考: http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/settings.html#std:setting-DOWNLOADER_MIDDLEWARES_BASE QQ截图20170122165743 中的值设置!!这点务必注意···如果不清楚依赖关系 请按照上图的值设置。 从上面可以看出 Cookie 中间件的值为 700 、我们在 settings.py 设置也应该为 700 QQ截图20170122170041 我注释掉的请无视掉!!! 做好这些以后 Scrapy 运作的整个流程大概就变成了下面这样: QQ20170122-232839

1
return Request(lnk) 这一个请求也算作 初始URL 只不过 不是start_urls的返回response 所以不会调用parse_start_url函数哦!

QQ20170122-230207 跑一下!效果杠杠滴!!!至于后面的数据持久化(如何保存数据、大家请自行解决哦!比毕竟上一篇博文讲过了、) 这种更适合使用 MongoDB 存储 超级简单好使。 至此本篇博文结束。 这个还有一个分布式的版本、现在不想写了··· 等年后再写吧。 另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。

Python

使用 python 代码收集主机的系统信息,主要:主机名称、IP、系统版本、服务器厂商、型号、序列号、CPU信息、内存等系统信息。

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
#!/usr/bin/env python
#encoding: utf-8

'''
收集主机的信息:
主机名称、IP、系统版本、服务器厂商、型号、序列号、CPU信息、内存信息
'''

from subprocess import Popen, PIPE
import os,sys

''' 获取 ifconfig 命令的输出 '''
def getIfconfig():
p = Popen(['ifconfig'], stdout = PIPE)
data = p.stdout.read()
return data

''' 获取 dmidecode 命令的输出 '''
def getDmi():
p = Popen(['dmidecode'], stdout = PIPE)
data = p.stdout.read()
return data

''' 根据空行分段落 返回段落列表'''
def parseData(data):
parsed_data = []
new_line = ''
data = [i for i in data.split('\n') if i]
for line in data:
if line[0].strip():
parsed_data.append(new_line)
new_line = line + '\n'
else:
new_line += line + '\n'
parsed_data.append(new_line)
return [i for i in parsed_data if i]

''' 根据输入的段落数据分析出ifconfig的每个网卡ip信息 '''
def parseIfconfig(parsed_data):
dic = {}
parsed_data = [i for i in parsed_data if not i.startswith('lo')]
for lines in parsed_data:
line_list = lines.split('\n')
devname = line_list[0].split()[0]
macaddr = line_list[0].split()[-1]
ipaddr = line_list[1].split()[1].split(':')[1]
break
dic['ip'] = ipaddr
return dic

''' 根据输入的dmi段落数据 分析出指定参数 '''
def parseDmi(parsed_data):
dic = {}
parsed_data = [i for i in parsed_data if i.startswith('System Information')]
parsed_data = [i for i in parsed_data[0].split('\n')[1:] if i]
dmi_dic = dict([i.strip().split(':') for i in parsed_data])
dic['vender'] = dmi_dic['Manufacturer'].strip()
dic['product'] = dmi_dic['Product Name'].strip()
dic['sn'] = dmi_dic['Serial Number'].strip()
return dic

''' 获取Linux系统主机名称 '''
def getHostname():
with open('/etc/sysconfig/network') as fd:
for line in fd:
if line.startswith('HOSTNAME'):
hostname = line.split('=')[1].strip()
break
return {'hostname':hostname}

''' 获取Linux系统的版本信息 '''
def getOsVersion():
with open('/etc/issue') as fd:
for line in fd:
osver = line.strip()
break
return {'osver':osver}

''' 获取CPU的型号和CPU的核心数 '''
def getCpu():
num = 0
with open('/proc/cpuinfo') as fd:
for line in fd:
if line.startswith('processor'):
num += 1
if line.startswith('model name'):
cpu_model = line.split(':')[1].strip().split()
cpu_model = cpu_model[0] + ' ' + cpu_model[2] + ' ' + cpu_model[-1]
return {'cpu_num':num, 'cpu_model':cpu_model}

''' 获取Linux系统的总物理内存 '''
def getMemory():
with open('/proc/meminfo') as fd:
for line in fd:
if line.startswith('MemTotal'):
mem = int(line.split()[1].strip())
break
mem = '%.f' % (mem / 1024.0) + ' MB'
return {'Memory':mem}

if __name__ == '__main__':
dic = {}
data_ip = getIfconfig()
parsed_data_ip = parseData(data_ip)
ip = parseIfconfig(parsed_data_ip)

data_dmi = getDmi()
parsed_data_dmi = parseData(data_dmi)
dmi = parseDmi(parsed_data_dmi)

hostname = getHostname()
osver = getOsVersion()
cpu = getCpu()
mem = getMemory()

dic.update(ip)
dic.update(dmi)
dic.update(hostname)
dic.update(osver)
dic.update(cpu)
dic.update(mem)

''' 将获取到的所有数据信息并按简单格式对齐显示 '''
for k,v in dic.items():
print '%-10s:%s' % (k, v)

实验测试结果:

1
2
3
4
5
6
7
8
9
product   :VMware Virtual Platform
osver :CentOS release 6.4 (Final)
sn :VMware-56 4d b4 6c 05 e5 20 dc-c6 49 0c e1 e0 18 1c 75
Memory :1870 MB
cpu_num :2
ip :192.168.0.8
vender :VMware, Inc.
hostname :vip
cpu_model :Intel(R) i7-4710MQ 2.50GHz