Quantcast
Channel: 技术杂谈 –静觅
Viewing all 277 articles
Browse latest View live

Attention原理及TensorFlow AttentionWrapper源码解析

$
0
0

本节来详细说明一下 Seq2Seq 模型中一个非常有用的 Attention 的机制,并结合 TensorFlow 中的 AttentionWrapper 来剖析一下其代码实现。

Seq2Seq

首先来简单说明一下 Seq2Seq 模型,如果搞过深度学习,想必一定听说过 Seq2Seq 模型,Seq2Seq 其实就是 Sequence to Sequence,也简称 S2S,也可以称之为 Encoder-Decoder 模型,这个模型的核心就是编码器(Encoder)和解码器(Decoder)组成的,架构雏形是在 2014 年由论文 Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation, Cho et al 提出的,后来 Sequence to Sequence Learning with Neural Networks, Sutskever et al 算是比较正式地提出了 Sequence to Sequence 的架构,后来 Neural Machine Translation by Jointly Learning to Align and Translate, Bahdanau et al 又提出了 Attention 机制,将 Seq2Seq 模型推上神坛,并横扫了非常多的任务,现在也非常广泛地用于机器翻译、对话生成、文本摘要生成等各种任务上,并取得了非常好的效果。

下面的图示意了 Seq2Seq 模型的基本架构:

可以看到图中有一个中间状态 $ c $ 向量,在 $ c $ 向量左侧的我们可以称之为编码器(Encoder),编码器这里示意的是 RNN 序列,另外 RNN 单元还可以使用 LSTM、GRU 等变体, 在编码器下方输入了 $ x_1 $、$ x_2 $、$ x_3 $、$ x_4 $,代表模型的输入内容,例如在翻译模型中可以分别代表“我爱中国”这四个字,这样经过序列处理,它就会得到最后的输出,我们将其表示为 $ c $ 向量,这样编码器的工作就完成了。在图中 $ c $ 向量的右侧部分我们可以称之为解码器(Decoder),它拿到编码器生成的 $ c $ 向量,然后再进行序列解码,得到输出结果 $ y_1 $、$ y_2 $、$ y_3 $,例如刚才输入的“我爱中国”四个字便被解码成了 “I love China”,这样就实现了翻译任务,以上就是最基本的 Seq2Seq 模型原理。

另外还有一种变体,$ c $ 向量在每次解码的时候都会作为解码器的输入,其实原理都是类似的,如图所示:

这种模型架构是通用的,所以它的适用场景也非常广泛。如机器翻译、对话生成、文本摘要、阅读理解、语音识别,也可以用在一些趣味场景中,如诗词生成、对联生成、代码生成、评论生成等等,效果都很不错。

Attention

通过上图我们可以发现,Encoder 把所有的输入序列编码成了一个 $ c $ 向量,然后使用 $ c $ 向量来进行解码,因此,$ c $ 向量中必须包含了原始序列中的所有信息,所以它的压力其实是很大的,而且由于 RNN 容易把前面的信息“忘记”掉,所以基本的 Seq2Seq 模型,对于较短的输入来说,效果还是可以接受的,但是在输入序列比较长的时候,$ c $ 向量存不下那么多信息,就会导致生成效果大大折扣。

Attention 机制解决了这个问题,它可以使得在输入文本长的时候精确率也不会有明显下降,它是怎么做的呢?既然一个 $ c $ 向量存不了,那么就引入多个 $ c $ 向量,称之为 $ c_1 $、$ c_2 $、…、$ c_i $,在解码的时候,这里的 $ i $ 对应着 Decoder 的解码位次,每次解码就利用对应的 $ c_i $ 向量来解码,如图所示:

这里的每个 $ c_i $ 向量其实包含了当前所输出与输入序列各个部分重要性的相关的信息。不同的 $ c_i $ 向量里面包含的输入信息各部分的权重是不同的,先放一个示意图:

还是上面的例子,例如输入信息是“我爱中国”,输出的的理想结果应该是“I love China”,在解码的时候,应该首先需要解码出 “I” 这个字符,这时候会用到 $ c_1 $ 向量,而 $ c_1 $ 向量包含的信息中,“我”这个字的重要性更大,因此它便倾向解码输出 “I”,当解码第二个字的时候,会用到 $ c_2 $ 向量,而 $ c_2 $ 向量包含的信息中,“爱” 这个字的重要性更大,因此会解码输出 “love”,在解码第三个字的时候,会用到 $ c_3 $ 向量,而 $ c_3 $向量包含的信息中,”中国” 这两个字的权重都比较大,因此会解码输出 “China”。所以其实,Attention 注意力机制中的 $ c_i $ 向量记录了不同解码时刻应该更关注于哪部分输入数据,也实现了编码解码过程的对齐。经过实验发现,这种机制可以有效解决输入信息过长时导致信息解码效果不理想的问题,另外解码生成效果同时也有提升。

下面我们以 Bahdanau 提出的 Attention 为例来详细剖析一下 Attention 机制。

在没有引入 Attention 之前,Decoder 在某个时刻解码的时候实际上是依赖于三个部分的,首先我们知道 RNN 中,每次输出结果会依赖于隐层和输入,在 Seq2Seq 模型中,还需要依赖于 $ c $ 向量,所以这里我们设在 $ i $ 时刻,解码器解码的内容是 $ y_i $,上一次解码结果是 $ y_{i-1} $,隐层输出是 $ s_t $,所以它们满足这样的关系:

$$ y_i = g(y_{i-1}, s_i, c) $$

同时 $ s_i $ 和 $ c $ 还满足这样的关系:

$$ s_i = f(s_{i-1}, y_{i-1}, c) $$

即每次的隐层输出是上一个隐层和上一个输出结果和 $ c $ 向量共同计算得出的。

但是刚才说了,这样会带来一些问题,$ c $ 向量不足以包含输入内容的所有信息,尤其是在输入序列特别长的情况下,所以这里我们不再使用一个 $ c $ 向量,而是每一个解码过程对应一个 $ c_i $ 向量,所以公式改写如下:

$$ y_i = g(y_{i-1}, s_i, c_i) $$

同时 $ s_i $ 的计算方式也变为如下公式:

$$ s_i = f(s_{i-1}, y_{i-1}, c_i) $$

所以,这里每次解码得出 $ y_i $ 时,都有与之对应的 $ c_i $ 向量。那么这个 $ c_i $ 向量又是怎么来的呢?实际上它是由编码器端每个时刻的隐含状态加权平均得到的,这里假设编码器端的的序列长度为 $ T_x $,序列位次用 $ j $ 来表示,编码器段每个时刻的隐含状态即为 $ h_1 $、$ h_2 $、…、$ h_j $、…、$ h_{T_x} $,对于解码器的第 $ i $ 时刻,对应的 $ c_i $ 表示如下:

$$ c_i = \sum_{j=1}^{T_x} \alpha_{ij}h_j $$

编码器输出的结果中,$ h_j $ 中包含了输入序列中的第 $ j $ 个词及前面的一些信息,如果是用了双向 RNN 的话,则包含的是第 $ j $ 个词即前后的一些词的信息,这里 $ \alpha_{ij} $ 代表了分配的权重,这代表在生成第 i 个结果的时候,对于输入信息的各个阶段的 $ hj $ 的注意力分配是不同的。 当 $ a_{ij} $ 的值越高,表示第 $ i $ 个输出在第 $ j $ 个输入上分配的注意力越多,这样就会导致在生成第 $ i $ 个输出的时候,受第 $ j $ 个输入的影响也就越大。

那么 $ a_{ij} $ 又是怎么得来的呢?其实它就又关系到第 $ i-1 $ 个输出隐藏状态 $ s_{i-1} $ 以及输入中的各个隐含状态 $ h_j $,公式表示如下:

$$ \alpha_{ij} = \frac {exp(e_{ij})} {\sum_{k=1}^{T_x} exp(e_{ik})} $$

同时 $ e_{ij} $ 又表示为:

$$ e_{ij} = a(s_{i-1}, h_j) = {v_a}^Ttanh(W_as_{i-1} + U_ah_j) $$

这也就是说,这个权重就是 $ s_{i-1} $ 和 $ h_j $ 分别计算得到一个数值,然后再过一个 softmax 函数得到的,结果就是 $ \alpha_{ij} $。

因此 $ c_i $ 就可以表示为:

$$ c_i = \sum_{j=1}^{T_x} softmax(a(s_{i-1}, h_j)) \cdot h_j $$

以上便是整个 Attention 机制的推导过程。

TensorFlow AttentionWrapper

我们了解了基本原理,但真正离程序实现出来其实还是有很大差距的,接下来我们就结合 TensorFlow 框架来了解一下 Attention 的实现机制。

在 TensorFlow 中,Attention 的相关实现代码是在 tensorflow/contrib/seq2seq/python/ops/attention_wrapper.py 文件中,这里面实现了两种 Attention 机制,分别是 BahdanauAttention 和 LuongAttention,其实现论文分别如下:

整个 attention_wrapper.py 文件中主要包含几个类,我们主要关注其中几个:

  • AttentionMechanism、_BaseAttentionMechanism、LuongAttention、BahdanauAttention 实现了 Attention 机制的逻辑。
    • AttentionMechanism 是 Attention 类的父类,继承了 object 类,内部没有任何实现。
    • _BaseAttentionMechanism 继承自 AttentionMechanism 类,定义了 Attention 机制的一些公共方法实现和属性。
    • LuongAttention、BahdanauAttention 均继承 _BaseAttentionMechanism 类,分别实现了上面两篇论文的 Attention 机制。
  • AttentionWrapperState 用来存储整个计算过程中的 state,和 RNN 中的 state 类似,只不过这里额外还存储了 attention、time 等信息。
  • AttentionWrapper 主要用于对封装 RNNCell,继承自 RNNCell,封装后依然是 RNNCell 的实例,可以构建一个带有 Attention 机制的 Decoder。
  • 另外还有一些公共方法,例如 hardmax、safe_cumpord 等。

下面我们以 BahdanauAttention 为例来说明 Attention 机制及 AttentionWrapper 的实现。

BahdanauAttention

首先我们来介绍 BahdanauAttention 类的具体原理。

首先我们来看下它的初始化方法:

def __init__(self,
    num_units,
    memory,
    memory_sequence_length=None,
    normalize=False,
    probability_fn=None,
    score_mask_value=None,
    dtype=None,
    name="BahdanauAttention"):

这里一共接受八个参数,下面一一进行说明:

  • num_units:神经元节点数,我们知道在计算 $ e_{ij} $ 的时候,需要使用 $ s_{i-1} $ 和 $ h_j $ 来进行计算,而二者的维度可能并不是统一的,需要进行变换和统一,所以这里就有了 $ W_a $ 和 $ U_a $ 这两个系数,所以在代码中就是用 num_units 来声明了一个全连接 Dense 网络,用于统一二者的维度,以便于下一步的计算:
query_layer=layers_core.Dense(num_units, name="query_layer", use_bias=False, dtype=dtype)
memory_layer=layers_core.Dense(num_units, name="memory_layer", use_bias=False, dtype=dtype)

这里我们可以看到声明了一个 query_layer 和 memory_layer,分别和 $ s_{i-1} $ 及 $ h_j $ 做全连接变换,统一维度。

  • memory:The memory to query; usually the output of an RNN encoder. 即解码时用到的上文信息,维度需要是 [batch_size, max_time, context_dim]。这时我们观察一下父类 _BaseAttentionMechanism 的初始化方法,实现如下:
with ops.name_scope(
    name, "BaseAttentionMechanismInit", nest.flatten(memory)):
  self._values = _prepare_memory(
      memory, memory_sequence_length,
      check_inner_dims_defined=check_inner_dims_defined)
  self._keys = (
      self.memory_layer(self._values) if self.memory_layer
      else self._values)

这里通过 _prepare_memory() 方法对 memory 进行处理,然后调用 memory_layer 对 memory 进行全连接维度变换,变换成 [batch_size, max_time, num_units]。

  • memory_sequence_length:Sequence lengths for the batch entries in memory. 即 memory 变量的长度信息,类似于 dynamic_rnn 中的 sequence_length,被 _prepare_memory() 方法调用处理 memory 变量,进行 mask 操作:
seq_len_mask = array_ops.sequence_mask(
    memory_sequence_length,
    maxlen=array_ops.shape(nest.flatten(memory)[0])[1],
    dtype=nest.flatten(memory)[0].dtype)
seq_len_batch_size = (
    memory_sequence_length.shape[0].value
    or array_ops.shape(memory_sequence_length)[0])
  • normalize:Whether to normalize the energy term. 即是否要实现标准化,方法出自论文:Weight Normalization: A Simple Reparameterization to Accelerate Training of Deep Neural Networks, Salimans, et al
  • probability_fn:A callable function which converts the score to probabilities. 计算概率时的函数,必须是一个可调用的函数,默认使用 softmax(),还可以指定 hardmax() 等函数。
  • score_mask_value:The mask value for score before passing into probability_fn. The default is -inf. Only used if memory_sequence_length is not None. 在使用 probability_fn 计算概率之前,对 score 预先进行 mask 使用的值,默认是负无穷。但这个只有在 memory_sequence_length 参数定义的时候有效。
  • dtype:The data type for the query and memory layers of the attention mechanism. 数据类型,默认是 float32。
  • name:Name to use when creating ops,自定义名称。

接下来类里面定义了一个 __call__() 方法:

def __call__(self, query, previous_alignments):
    with variable_scope.variable_scope(None, "bahdanau_attention", [query]):
      processed_query = self.query_layer(query) if self.query_layer else query
      score = _bahdanau_score(processed_query, self._keys, self._normalize)
    alignments = self._probability_fn(score, previous_alignments)
    return alignments

这里首先定义了 processed_query,这里也是通过 query_layer 过了一个全连接网络,将最后一维统一成 num_units,然后调用了 bahdanau_score() 方法,这个方法是比较重要的,主要用来计算公式中的 $ e{ij} $,传入的参数是 processed_query 以及上文中提及的 keys 变量,二者一个代表了 $ s{i-1} $,一个代表了 $ h_j $,_bahdanau_score() 方法实现如下:

def _bahdanau_score(processed_query, keys, normalize):
    dtype = processed_query.dtype
    # Get the number of hidden units from the trailing dimension of keys
    num_units = keys.shape[2].value or array_ops.shape(keys)[2]
    # Reshape from [batch_size, ...] to [batch_size, 1, ...] for broadcasting.
    processed_query = array_ops.expand_dims(processed_query, 1)
    v = variable_scope.get_variable(
      "attention_v", [num_units], dtype=dtype)
    if normalize:
        # Scalar used in weight normalization
        g = variable_scope.get_variable(
            "attention_g", dtype=dtype,
            initializer=math.sqrt((1. / num_units)))
        # Bias added prior to the nonlinearity
        b = variable_scope.get_variable(
            "attention_b", [num_units], dtype=dtype,
            initializer=init_ops.zeros_initializer())
        # normed_v = g * v / ||v||
        normed_v = g * v * math_ops.rsqrt(
            math_ops.reduce_sum(math_ops.square(v)))
        return math_ops.reduce_sum(normed_v * math_ops.tanh(keys + processed_query + b), [2])
    else:
        return math_ops.reduce_sum(v * math_ops.tanh(keys + processed_query), [2])

这里其实就是实现了 keys 和 processed_query 的加和,如果指定了 normalize 的话还需要进行额外的 normalize,结果就是公式中的 $ e_{ij} $,在 TensorFlow 中常用 score 变量表示。

接下来再回到 __call__() 方法中,这里得到了 score 变量,接下来可以对齐求 softmax() 操作,得到 $ \alpha_{ij} $:

alignments = self._probability_fn(score, previous_alignments)

这就代表了在 $ i $ 时刻,Decoder 的时候对 Encoder 得到的每个 $ hj $ 的权重大小比例,在 TensorFlow 中常用 alignments 变量表示。

所以综上所述,BahdanauAttention 就是初始化时传入 num_units 以及 Encoder Outputs,然后调时传入 query 用即可得到权重变量 alignments。

AttentionWrapperState

接下来我们再看下 AttentionWrapperState 这个类,这个类其实比较简单,就是定义了 Attention 过程中可能需要保存的变量,如 cell_state、attention、time、alignments 等内容,同时也便于后期的可视化呈现,代码实现如下:

class AttentionWrapperState(
    collections.namedtuple("AttentionWrapperState",
                           ("cell_state", "attention", "time", "alignments",
                            "alignment_history"))):

可见它就是继承了 namedtuple 这个数据结构,其实整个 AttentionWrapperState 就像声明了一个结构体,可以传入需要的字段生成这个对象。

AttentionWrapper

了解了 Attention 机制及 BahdanauAttention 的原理之后,最后我们再来了解一下 AttentionWrapper,可能你用过很多其他的 Wrapper,如 DropoutWrapper、ResidualWrapper 等等,它们其实都是 RNNCell 的实例,其实 AttentionWrapper 也不例外,它对 RNNCell 进行了封装,封装后依然还是 RNNCell 的实例。一个普通的 RNN 模型,你要加入 Attention,只需要在 RNNCell 外面套一层 AttentionWrapper 并指定 AttentionMechanism 的实例就好了。而且如果要更换 AttentionMechanism,只需要改变 AttentionWrapper 的参数就好了,这可谓对 Attention 的实现架构完全解耦,配置非常灵活,TF 大法好!

接下来我们首先来看下它的初始化方法,其参数是这样的:

def __init__(self,
    cell,
    attention_mechanism,
    attention_layer_size=None,
    alignment_history=False,
    cell_input_fn=None,
    output_attention=True,
    initial_cell_state=None,
    name=None):

下面对参数进行一一说明:

  • cell:An instance of RNNCell. RNNCell 的实例,这里可以是单个的 RNNCell,也可以是多个 RNNCell 组成的 MultiRNNCell。
  • attention_mechanism:即 AttentionMechanism 的实例,如 BahdanauAttention 对象,另外可以是多个 AttentionMechanism 组成的列表。
  • attention_layer_size:是数字或者数字做成的列表,如果是 None(默认),直接使用加权计算后得到的 Attention 作为输出,如果不是 None,那么 Attention 结果还会和 Output 进行拼接并做线性变换再输出。其代码实现如下:
if attention_layer_size is not None:
    attention_layer_sizes = tuple(attention_layer_size if isinstance(attention_layer_size, (list, tuple)) else (attention_layer_size,))
    if len(attention_layer_sizes) != len(attention_mechanisms):
        raise ValueError("If provided, attention_layer_size must contain exactly one integer per attention_mechanism, saw: %d vs %d" % (len(attention_layer_sizes), len(attention_mechanisms)))
    self._attention_layers = tuple(layers_core.Dense(attention_layer_size, name="attention_layer", use_bias=False, dtype=attention_mechanisms[i].dtype) for i, attention_layer_size in enumerate(attention_layer_sizes))
    self._attention_layer_size = sum(attention_layer_sizes)
else:
    self._attention_layers = None
    self._attention_layer_size = sum(attention_mechanism.values.get_shape()[-1].value for attention_mechanism in attention_mechanisms) 
    
for i, attention_mechanism in enumerate(self._attention_mechanisms):
    attention, alignments = _compute_attention(attention_mechanism, cell_output, previous_alignments[i], self._attention_layers[i] if self._attention_layers else None)
    alignment_history = previous_alignment_history[i].write(state.time, alignments) if self._alignment_history else ()
  • alignment_history:即是否将之前的 alignments 存储到 state 中,以便于后期进行可视化展示。
  • cell_input_fn:将 Input 进行处理的方式,默认会将上一步的 Attention 进行 拼接操作,以免造成重复关注同样的内容。代码调用如下:
cell_inputs = self._cell_input_fn(inputs, state.attention)
  • output_attention:是否将 Attention 返回,如果是 False 则返回 Output,否则返回 Attention,默认是 True。
  • initial_cell_state:计算时的初始状态。
  • name:自定义名称。

AttentionWrapper 的核心方法在它的 call() 方法,即类似于 RNNCell 的 call() 方法,AttentionWrapper 类对其进行了重载,代码实现如下:

def call(self, inputs, state):
    # Step 1
    cell_inputs = self._cell_input_fn(inputs, state.attention)
    # Step 2
    cell_state = state.cell_state
    cell_output, next_cell_state = self._cell(cell_inputs, cell_state)
    # Step 3
    if self._is_multi:
        previous_alignments = state.alignments
        previous_alignment_history = state.alignment_history
    else:
        previous_alignments = [state.alignments]
        previous_alignment_history = [state.alignment_history]
    all_alignments = []
    all_attentions = []
    all_histories = []
    for i, attention_mechanism in enumerate(self._attention_mechanisms):
        attention, alignments = _compute_attention(attention_mechanism, cell_output, previous_alignments[i], self._attention_layers[i] if self._attention_layers else None)
        alignment_history = previous_alignment_history[i].write(state.time, alignments) if self._alignment_history else ()
        all_alignments.append(alignments)
        all_histories.append(alignment_history)
        all_attentions.append(attention)
    # Step 4
    attention = array_ops.concat(all_attentions, 1)
    # Step 5
    next_state = AttentionWrapperState(
        time=state.time + 1,
        cell_state=next_cell_state,
        attention=attention,
        alignments=self._item_or_tuple(all_alignments),
        alignment_history=self._item_or_tuple(all_histories))
    # Step 6
    if self._output_attention:
        return attention, next_state
    else:
        return cell_output, next_state

在这里将一些异常判断代码去除了,以便于结构看得更清晰。

首先在第一步中,调用了 _cell_input_fn() 方法,对 inputs 和 state.attention 变量进行处理,默认是使用 concat() 函数拼接,作为当前时间步的输入。因为可能前一步的 Attention 可能对当前 Attention 有帮助,以免让模型连续两次将注意力放在同一个地方。

在第二步中,其实就是调用了普通的 RNNCell 的 call() 方法,得到输出和下一步的状态。

第三步中,这时得到的输出其实并没有用上 AttentionMechanism 中的 alignments 信息,所以当前的输出信息中我们并没有跟 Encoder 的信息做 Attention,所以这里还需要调用 _compute_attention() 方法进行权重的计算,其方法实现如下:

def _compute_attention(attention_mechanism, cell_output, previous_alignments, attention_layer):
    alignments = attention_mechanism(cell_output, previous_alignments=previous_alignments)
    expanded_alignments = array_ops.expand_dims(alignments, 1)
    context = math_ops.matmul(expanded_alignments, attention_mechanism.values)
    context = array_ops.squeeze(context, [1])
    if attention_layer is not None:
        attention = attention_layer(array_ops.concat([cell_output, context], 1))
    else:
        attention = context
    return attention, alignments

这个方法接收四个参数,其中 attention_mechanism 就是 AttentionMechanism 的实例,cell_output 就是当前 Output,previous_alignments 是上步的 alignments 信息,调用 attention_mechanism 计算之后就会得到当前步的 alignments 信息了,即 $ \alpha_{ij} $。接下来再利用 alignments 信息进行加权运算,得到 attention 信息,即 $ c_{i} $,最后将二者返回。

在第四步中,就是将 attention 结果每个时间步进行 concat,得到 attention vector。

第五步中,声明 AttentionWrapperState 作为下一步的状态。

第六步,判断是否要输出 Attention,如果是,输出 Attention 及下一步状态,否则输出 Outputs 及下一步状态。

好,以上便是整个 AttentionWrapper 源码解析过程,了解了源码之后,再做模型优化的话就非常得心应手了。

参考来源

转载请注明:静觅 » Attention原理及TensorFlow AttentionWrapper源码解析


SSH反向隧道搭建过程

$
0
0

现在我们有一台内网主机 A,在局域网内是可以访问的,但是如果我们现在不处在局域网内,可以选择 VPN 连接,但这样其实并不太方便,所以本节我们来说明一下利用 SSH 反向隧道来实现访问内网主机的方法。

准备

首先我们需要有一台公网主机作为跳板,这台主机是可以公网访问的,我们将其命名为 B,它的 IP 假设为 10.10.10.10。

所以两台机器网络配置如下:

A 内网机器

  • IP:192.168.1.2
  • SSH端口: 22
  • 用户名:usera
  • 密码:passworda
  • 内网配置端口:22(即配置 SSH 端口的反向隧道)

B 公网机器

  • IP:10.10.10.10
  • SSH端口: 22
  • 用户名:userb
  • 密码:passwordb
  • 公网端口:22001(即用 B 的 22001 端口连到 A 的 SSH 22 端口)

配置SSH秘钥

首先我们需要在 A 主机上生成 SSH 秘钥,和 B 用 SSH 建立认证。

首先在主机 A 上执行如下命令生成 SSH 秘钥:

ssh-keygen -t rsa -C "your@email.com"

命令里面的邮箱需要自行更换。

然后利用如下命令将 A 的 SSH 秘钥添加到 B 的 authorized_keys 里面:

ssh-copy-id userb@10.10.10.10

执行后会提示输入主机 B 的密码,执行完毕之后,我们登录到 B,就发现 authorized_keys 里面就多了 A 的 SSH 公钥了,成功建立 SSH 认证。

B 主机配置

B 主机需要更改 /etc/ssh/sshd_config 文件,修改如下一行:

GatewayPorts yes

这样可以把监听的端口绑定到任意IP 0.0.0.0上,否则只有本机 127.0.0.1 可以访问。

然后重启 sshd 服务:

sudo service sshd restart

A 主机配置

主机 A 再安装一个 AutoSSH,以 Ubuntu 为例,命令如下:

sudo apt-get install autossh

然后执行如下命令即可完成反向 SSH 配置:

autossh -M 55555 -NfR 0.0.0.0:22001:localhost:22 userb@10.10.10.10

这里 -M 后面任意填写一个可用端口即可,-N 代表只建立连接,不打开shell ,-f 代表建立成功后在后台运行,-R 代表指定端口映射。

这里是将 A 主机的 22 端口映射到 B 主机的 22001 端口,这样就完成了配置。

主要我们再访问 B 主机的 22001 端口,就会自动转发到 A 主机的 22 端口了,即可以公网访问了。

连接测试

接下来 SSH 测试连接 A 主机即可:

ssh usera@10.10.10.10 -p 22001

输入密码,完成连接。

转载请注明:静觅 » SSH反向隧道搭建过程

【Python搞搞轻量Blog】第一发 Flask入门

$
0
0

 大家好,我是仙宇(其实就是咸鱼)

我发现很多小伙伴一直想着有自己的一个博客,而且还想自己写一个。

你们都这么爱折腾,我就给你们搞一个轻量级级别的Blog.

准备

我们要用Python来写一套轻量级的博客,那么必须要有Python方面的基础.

如果有HTML和CSS的基础食用更佳…

 

介绍 AND 框架选择

Python有很多Web框架,可谓是百家争鸣,我这里列出几个比较叼的几个框架

  • Django      市场占有率最高,官方文档几近完美,但是适合比较大的项目,小项目会显得累赘。
  • Tornado    可异步,性能高,提供更多的底层细节,还能进行Web Socket,但是和数据库…..(大坑)
  • Web.py      小巧,精炼,扩展不是很多,而且问题是作者太牛掰,被上帝请去喝茶了。
  • Flask          年轻,2010年出现、可扩展、小巧、内置开发服务器和调试器、使用jinja2模板、完全兼容WSGI 1.0

 

框架十分多,但是我们用哪个框架来搞我们的轻量级博客呢?

答案是: Flask

 

理论概念 MVC

啥事MVC呢?

很多小伙伴一脸懵逼,这Tm又是个神马?

M:Model ==> 数据库模型

V:Views ==> 可以理解为定义网页的地址,以及渲染网页等

C:Controller ==> 可以理解为 网页功能的逻辑,实现

 

安装

Flask 库的名字就叫 flask

我们使用pip进行安装

pip install flask --index http://pypi.douban.com/simple --trusted-host pypi.douban.com

pip: 专门负责对python库的管理

install: 安装

选用豆瓣的Pypi库安装flask会很快(国内)

 

测试是否成功安装

打开Python

import flask

只要没报错,就是成功安装 Flask库了

裸跑Web应用

库已经安装了,那就该讲讲如何利用Flask来写一个简单的Web应用。

from flask import Flask # 导入包

app = Flask(__name__) # 创建一个Web应用

@app.route('/') # 定义路由(Views),可以理解为定义页面的URL
def index(): 
    return "这是用Python + Flask 搞出来的。" # 渲染页面

if __name__ == "__main__":
    app.run(host='127.0.0.1',port=8080) # 运行,指定监听地址为 127.0.0.1:8080

我们来运行试试。

然后我们去用浏览器访问页面

开启Debug调试

如果我们修改了代码,我们需要将开启的Web程序关闭再开启,这样会影响开发效率,也很烦。

Flask内部有一个功能,专门针对这样的事情发生。

修改代码

if __name__ == "__main__":
    app.run(host='127.0.0.1',port=8080,debug=True)

我们添加了一个参数,就是debug,这个参数默认是False

开启这个功能后,我们每次修改完代码,Web应用对自动进行调整。

现在修改代码并且保存,应用就会自动更新你的代码。

分析

现在我们来分析一下代码

from flask import Flask # 导入包

app = Flask(__name__) # 创建一个Web应用

app.config['DEBUG'] = True

@app.route('/') # 定义路由(Views),可以理解为定义页面的URL
def index(): 
    return "这是用Python + Flask 搞出来的。" # 渲染页面

if __name__ == "__main__":
    app.run(host='127.0.0.1',port=8080)

所有Flask程序都需要进行创建一个实例。Web服务器会使用WSGI协议,将接受到的客户端请求转交给这个对象处理,可以理解为代码中的app

第3行:Flask需要一个参数,这个参数通常是主模块或是包的名字。所以通常会传入 __name__

Flask用这个参数来决定程序的根目录,以便以后找到资源文件,比如网页中的图片,视频,音频等

 

第5行:可以通过使用app.config类来修改配置,开启调试模式。当然,这里所采用的方法只能适用于设置很少的时候,以后会介绍另外几种修改配置的方法。

第7行: 还记得我之前提到了MVC吗?这里就是其中V,就是路由。这里的代码的目的就是为我们来指定一个路由,也就是页面的地址。

第8 – 9行: 使用app.route()装饰器会将URL和执行的视图函数(函数 index )的关系保存在app.url_map属性上。当你访问指定的URL时,就会调用这个函数。当遇到第一个return时,就会结束。其中的return就是你的response

第12行: 执行app.run来启动服务器。默认的Flask会监听的地址是127.0.0.1:5000。我们指定host和port参数,就修改了监听地址。 服务启动后,会先判断参数host以及port是否为None,如果为None,就会将host和port修改为默认值。然后会判断debug。然后就会调用werkzeug.serving.run_simple来启动Web服务,默认会使用单进程的werkzeug.serving_BaseWSGIServer来处理客户端的请求。

(这里的Werkzeug其实就是WSGI的实现和应用,从中可以发现,Flask是基于Werkzeug开发的。你或许可以去查查Werkzeug的文档,来自己实现一个Web服务框架)

深入

我贴出Flask源码我们来瞅瞅

 

从827行代码可以发现 Flask从 werkzeug.serving引入了run_simple,之后的一部分代码就是进行调整和检验参数(host和port再有就是debug),最后把参数传给了run_simple来启动服务。

所以大家不要以为框架都是高大上,有时候看看源码,就会发现没有想象的那么困难。

再来看看 run_simple可以看到,processes就是进程,processes默认值为1。

注意

还要注意的就是,这里的app.run的启动方法,只适合在码代码的时候调试。

千万不要在产品上线的时候也用这种方法来启动服务!!!

以后我会给大家介绍在生产环境下如何配置。

或许大家可以去看看关于uWSGI的文章。

最后,贴上我微信二维码,有问题的小伙伴请留言,我也在学习,希望大家有什么坑可以抛出来交流。

 

转载请注明:静觅 » 【Python搞搞轻量Blog】第一发 Flask入门

【Python搞搞轻量Blog】第二发 Flask入门(2)

$
0
0

Hi,大家好。我是仙宇

上一篇文章 Flask入门 中创建了一个非常简单的Web应用。

但从最简单中又稍微深入的说了一下Flask 基于Werkzeug。

使用模板 (利用HTML)

如何快速利用写好的HTML呢?

 

第一步: 在和主应用的同级目录下,创建一个名为 templates 文件夹。

这个就是存放模板的文件夹,模板就是html文件

第二步:在templates 文件下新建一个index.html 文件

 

然后看下我们的主程序代码,如何去利用模板。

 

from flask import Flask,render_template
app = Flask(__name__)

@app.route('/')
def index():
    return render_template("index.html",name='Stronger') # 模板名称为 index.html

if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8080,debug=True)

第一行: 新导入了一个 render_template 函数

第六行代码: 调用render_template,传入的第一个参数是模板的文件名称,它会在主程序的同级目录下去寻找一个名为templates的文件夹,在这个文件下去寻找模板。第二个传入的参数,会在模板中显示,看上面的html代码图片,发现在第 10 行中有 {{ name }} 这样的一个写法。这里的 name就是你在主程序中传入的参数。

如果我将 {{ name }} 改为 {{ user }} 呢?

那我就需要在主程序中,将render_template(“index.html”,name=’Strong’) 改为 render_template(“index.html”,user=’Strong’)

我们看下效果

 

有人会问了,如果不传入后面的参数,模板中会显示什么呢?

答: 当不传入参数,模板中的变量就是None,不会显示。

 

可不可以改存放模板的文件夹呢?

答:可以,在 Flask(__name__) 修改代码为 Flask(__name__,template_folder=”存放模板文件夹名称”)

在模板中编程

在 Flask入门 中我就说过可以使用jinjia2模板引擎。

使用jinjia2模板引擎的好处在于: 渲染速度快、方便、可进行模板继承,让模板重复使用、可在模板中进行宏、可进行模板直接的导入

我就不举例了,直接列出一些语法。(偷偷懒)

  • {# … #} 模板中的注释
  • {% … %} 用于执行for循环,if条件判断登语句
  • {{ …  }} 用于把表达式的结果输出到模板上,不止可以传数值,还能传列表,数组,字典等

URL 规则

一个关于指定路由的坑:

很多人会这样指定路由

@app.route('/user')

但是这样写路由,当访问 /user/的时候,就会产生一个 “Not Found”的错误,而且会被搜索引擎引用同一个页面两次,第二次则是 /user/ 这样的。

所以,最好的写法应该这样写。

@app.route('/user/')

在结尾加上 / ,既不失优雅,又不失B格

 

现在,我需要写三个相似的路由,/user/1/   /user/2/   /user/3/ 这样的三个路由。

难道需要我这样写三次吗?

@app.route('/user/1/')
@app.route('/user/2/')
@app.route('/user/3/')
def user():
        pass

这样写就显得太Low了。

 

应该这样写:

@app.route('/user/<uid>')
def user(uid):
        pass

第一行: 尖括号里面的内容是动态的,凡是匹配到/user/前缀的URL都会映射到这个路由上,在内部将uid作为参数。默认的数值类型是字符串,如果需要指定其他类型,则需要其他写法

通常格式是 <converter:variable_name> 其中,冒号左边的 converter 有这样几种

  • string 接受任何没有/的文本,这也是默认
  • int 整数型
  • float: 浮点型
  • path: 路径,接受/

当然,可以自己去写一个匹配方法,也可以使用正则表达式去匹配,后面如果需要,我就多BB。

第二行:函数中形参uid 接收的值就是装饰器中尖括号的值

比如我现在访问 /user/100,那么uid的值就为100,函数中uid的值也是100

构造URL

Flask用url_for()函数构造url,用起来很方便。它的第一个参数是函数名.

举例:现在我要利用url_for()去构造一个有参数id=10,name=‘XeanYu’,age=16的一个URL

from flask import Flask,url_for
app = Flask(__name__) 

@app.route('/user/')
def index():
    print(url_for('index',id=10,name='XeanYu',age=16))
    return 'Test'

if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8080,debug=True)

可以看到,我们首先需要先导入 url_for

重点在于第6行

第6行: 第一个参数是函数名,第二个参数id=10,第三个参数name=XeanYu,第四个参数age=16

这个函数可用与jinjia2的模板中。

跳转和重定向

跳转(状态码通常301) => 通常用于旧网址转移到了新网址

重定向(状态码通常302)  => 表示页面是暂时性的转移

在Flask中,重定向是 redirect

举例:

from flask import Flask,url_for,redirect

app = Flask(__name__)

@app.route('/')
def index():
    return redirect(url_for('user'))

@app.route('/user/')
def user():
    return 'This is User'

if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8080,debug=True)

我们在第一行中导入了redirect

当我们访问 index 路由时,就会自动重定向到user路由,状态码为302。你也可以在redirect中指定状态码,比如我要指定状态码为301,我就会这样写: redirect(url_for(‘user’),code=301)

方法 Method

前端通常少不了GET或者POST这两种方法。

我们如何在应用中搞定GET或者POST呢?

首先,我们需要在指定路由的时候就去允许页面去使用GET和POST等其他方法。

@app.route('/',methods=['GET','POST'])

这样就允许了GET和POST方法了。

GET方法默认就存在。

abort错误

你可以用 redirect() 函数把用户重定向到其它地方。放弃请求并返回错误代码,用 abort() 函数。这里是一个它们如何使用的例子:

from flask import abort, redirect, url_for,Flask

app = Flask(__name__)
@app.route('/')
def index():
    return redirect(url_for('login'))

@app.route('/login')
def login():
    abort(401)
    return "Test"

可以看到,没有给我们渲染 Test,而是返回401放弃请求的错误。

表示禁止访问,之后的语句用于不会执行。

 

有什么问题的小伙伴,可以与我交流。

转载请注明:静觅 » 【Python搞搞轻量Blog】第二发 Flask入门(2)

【Python搞搞轻量博客】第三发 开发无数据库留言板

$
0
0

Hi,大家好,我又回来了。

今天我要写一个无需数据,来实现一个简易的留言板。

准备

  • 留言有用户名,标题,内容,时间
  • 多用户

前端与后端数据提交

通常Web应用在前端和后端的数据交互少不了Get和Post这两种HTTP方法。

那么,我们如何通过传递参数的方式来获取数据呢?

比如,/get/?name=xeanyu&age=10 其中的数据为name=xeanyu 和 age = 10

from flask import Flask,request
app = Flask(__name__)

@app.route('/get/',methods=['GET'])
def index():
    name = request.args.get('name')
    age = request.args.get('age')
    print("name: %s \nage: %s " % (name,age))
    return 'OK'

if __name__ == '__main__':
    app.run(debug=True)

第一行: 我们导入了 request,利用它来获取前端传递给后端的数据

第六行: 获取参数,name的值,如果name没有传递参数,不会报错,而是返回None

第七行:获取参数,age的值,同上

我们看下控制台

成功获取get方法所传递的值。

 

然后我再来使用Post方法来获取数据。

如果使用Post来获取参数,我们就需要使用html来传递到后端。

from flask import Flask,request,render_template
app = Flask(__name__)

@app.route('/get/',methods=['GET','POST'])
def index():
    if request.method == 'GET':
        return render_template('index.html')

    if request.method == 'POST':
        name = request.form.get('name')
        age = request.form.get('age')

        print("POST 方法 \nname: %s \nage: %s" % (name,age))

        return '已经获取数据'

if __name__ == '__main__':
    app.run(debug=True)

然后我们来看HTML代码

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>POST方法</title>
</head>
<body>

        <center><h1>Post方法</h1></center>

        <center>
            <form action="{{ url_for('index') }}" method="POST">

                姓名: <input type="text" name="name"><br>

                年龄: <input type="text" name="age"><br>

                <button>提交</button>
            </form>
        </center>

</body>
</html>

可以看到,在第十二行中,form表单中的action的值是我们用 url_for()函数获取路由index的URL,然后method所使用的http方法为 POST

然后来看app.py主应用的代码

第4行: 其中methods添加POST方法

第6 – 15行中:利用if判断所访问的方法,如果是get,就是直接用浏览器访问,则渲染一个模板,如果是POST方法,就使用 request.form.get来获取 模板中表单的两个输入的值。

可以看到,这是GET方法的访问,所给我们渲染的模板。很简陋蛤

然后我们输入年龄和姓名,再来看控制台。

可以看到,成功获取了表单数据。现在,我们学到的知识,已经够我们写一个无需数据库的留言板了。

 

开始写留言板

from flask import Flask,request,render_template,redirect,url_for
import time
app = Flask(__name__)

users = [] # 这里存放所有的留言

@app.route('/say/',methods=['GET','POST'])
def index():
    if request.method == 'GET':
        return render_template('index.html',says=users)

    else:
        title = request.form.get('say_title')
        text = request.form.get('say')
        user = request.form.get('say_user')
        date = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime())

        users.append({"title": title,
                      "text":text,
                      "user":user,
                      "date": date})

        return redirect(url_for('index'))

if __name__ == '__main__':
    app.run(debug=True,)

还有模板代码

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>留言板</title>
</head>
<body>
        <center><h1>留言</h1></center>

        <center>
            <form action="{{ url_for('index') }}" method="POST">
                用户名: <input type="text" name="say_user"><br>
                留言标题: <input type="text" name="say_title"><br>
                留言内容: <input type="text" name="say"><br>
                <button>提交留言</button>
            </form>
        </center>

    <center>
        <table border="2">
            <thead>
            <tr>
                <th>标题</th>
                <th>内容</th>
                <th>用户</th>
                <th>留言时间</th>
            </tr>
            </thead>

            <tbody>

            {% for say in says %}
                <tr>
                    <td>{{ say['title'] }}</td>
                    <td>{{ say['text'] }}</td>
                    <td>{{ say['user'] }}</td>
                    <td>{{ say['date'] }}</td>
                </tr>
            {% endfor %}

            </tbody>
        </table>
    </center>
</body>
</html>

主应用app.py中

大致的思路是,创建一个留言列表。然后有Get方法去渲染模板,在模板中输入值,再去用POST提交给路由,路由将数据定义成为一个字典。

字典是这样的:

  • title => 用户输入的标题
  • text => 用户输入的内容
  • user => 用户名
  • date => 时间,使用time模块,实时获取

多说无益,我们来运行测试一下。

每次提交POST表单数据,视图函数添加到users列表以后,就会又跳转到get方式的页面。

代码可见主应用app.py中 12 – 23行代码中,第23行代码使用redirect又跳转到了留言页面。

 

这个留言板弊端很多,比如一旦重启应用,就会丢失所有的用户留言,你可以使用json,进行序列化储存。或者其他方式写入到文件。

弊端如下:

  • 数据无法持久化储存
  • 没有进行数据审核
  • html如果内容较长,显示就会很难看。
  • 还有非常多的弊端,自己去脑补吧大家。

Ok,如果大家有什么问题请加微信互相交流。

转载请注明:静觅 » 【Python搞搞轻量博客】第三发 开发无数据库留言板

【Python搞搞轻量博客】必要的知识

$
0
0

大家好,我又回来啦!

今天准备下数据库的,但是我想了想,那玩意讲的东西太多,怕大家一下接受不了,所以数据库那玩意我需要分几篇写,毕竟是重点嘛。

So ~ 我来说说今天我要讲的东西:

  • 更多的修改配置方法
  • request
  • session 讲解
  • 自定义错误页面
  • 静态文件管理

推荐

jinja2中文文档

推荐几部书和电影。

电影:

  • 《阿甘正传》
  • 《肖生克的救赎》
  • 《这个杀手不太冷》
  • 《教父》
  • 《辛德勒的名单》
  • 《霸王别姬》
  • 《美丽人生》
  • 《V字仇杀队》

书籍:

  • 《三体》
  • 《消费者行为学》
  • 《未来简史》
  • 《浪潮之巅》
  • 《腾讯传》
  • 《数学之美》
  • 《只是为了好玩》 Linus的半生自传
  • 《黑客与画家》
  • 《失控》 必读!

好书好电影很多啦。对了,还有一部美剧《硅谷》都很好。

不管是看书还是看电影,都不要被艳丽的封面和宣传片骗了!

配置方法

第一种:

如果设置的东西很少的话,就直接硬编码进来。

比如这样:

app = Flask(__name__)
app.config['DEBUG'] = True

其中app.configflask.config.Config的一个实例,继承了Python的dict类型。

 

第二种:

既然app.config是一个dict。那么我们就能这么写:

app = Flask(__name__)
app.config.update(
    DEBUG = True,
    ......
)

 

第三种:(通常就用这种)

通过对象引入配置.

 

我们在主应用目录下创建一个了 config.py 文件然后在里面写一个类,类名叫啥都可以,创建之后,写了一个DEBUG = True

blog.py 下的第四行: 使用 app.config.from_object引入了类。

 

第四种:

使用文件导入

 

可以看到,也可以这样引入。

 

更多:

有非常多的配置引入方法。

比如也可以用 json ,可以用 configparser  来写配置…等等…一大坨啦。以上是推荐的几种方式。

 

请求上下文之 request:  封装了客户端发出的HTTP请求中的内容

from flask import Flask,request

app = Flask(__name__) # 实例化

第一行: 首先我们引入request

 

request 的方法很多。

我们来演示几个。

request.url 

当我们访问视图函数时,会返回我们一个 当前的 url

request.method

之前我们用过,返回 客户端用的什么方法访问的,通常是GET或者是POST

request.host

返回主机(或是域名)

request.file

当你使用表单上传文件的时候,需要用到它。

使用它和使用request.form 和 request.args 一样,使用 get 去获取

request.headers

返回一个 http 头 的 dict类型

可以使用 request.hreaders.get(‘Host’) 等来获取数据

 

request.environ

返回一个列表,列表是 HTTP头的详情和 WSGI的详情,可用 request.environ.get()  来获取数据

{
'wsgi.version': (1, 0), 
'wsgi.url_scheme': 'http', 
'wsgi.input': <_io.BufferedReader name=6>,
'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>,
'wsgi.multithread': False,
'wsgi.multiprocess': False,
'wsgi.run_once': False,
'werkzeug.server.shutdown': <function WSGIRequestHandler.make_environ.<locals>.shutdown_server at 0x10c734b70>,
'SERVER_SOFTWARE': 'Werkzeug/0.14.1', 
'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '',
'PATH_INFO': '/', 'QUERY_STRING': '',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': 51042, 
'SERVER_NAME': '127.0.0.1', 
'SERVER_PORT': '5000', 
'SERVER_PROTOCOL': 'HTTP/1.1', 
'HTTP_HOST': '127.0.0.1:5000', 
'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 
'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 
'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6', 
'HTTP_ACCEPT_LANGUAGE': 'zh-cn', 
'HTTP_ACCEPT_ENCODING': 'gzip, deflate', 
'HTTP_CONNECTION': 'keep-alive', 'werkzeug.request': <Request 'http://127.0.0.1:5000/' [GET]>
}

这是 environ 的所有元素。

 

request.remote_addr

返回客户端地址

request.user_agent

返回客户端的 User_Agent

 

还有一些,列个表:

  • form:  获取html表单提交的POST数据
  • args: 获取GET数据
  • values: 是form和args的结合,既包括表单数据,也包括args数据
  • json: 如果客户端提交的是json数据,则用本方法去解析
  • is_xhr: 返回True或是False。用于判断客户端是否是通过JavaScrpt的XMLHttpRequest提交的,其实就是Ajax提交。

请求上下文之一  Session: 存储用户回话

在web中,会话是一种服务器客户端保持连通状态的方法。在Flask中,这种方法通过Cookie实现。

举例说明:

比如一个web站定有一个注册功能,注册的时候有一个验证码。每次刷新验证码就会变。

如果验证码有一个变量,如果你正在注册(输入验证码),但是这个时候,别人访问了一次,这个验证码变量就变了。你输入好以后,点击确定,但是这时候的验证码已经是新验证码了,你输入的是之前的验证码。

这个问题怎么解决呢?

这个时候就是session出场了,session会对每一个客户端有独立的变量(形象举例),然后这个时候别人输入验证码就不会影响你的验证码了。

session就是针对每一个客户端有专门的session。

这就像局部变量,在每一个函数里面有相同名字的变量,但是互相不影响,除非是全局变量才会影响。

这里的session和操作字典是没什么区别的。

默认情况下,用户回话保存在客户端的cookie中,使用设置的 SECRET_KEY 进行加密签名,如果篡改了cookie的内容,签名就会失效,回话也会消失。

常见的场景:

如一些登录页面,点击登录后,会有一个记住登录,或是浏览器弹出的记住密码等。

说了一堆,我们来试试怎么用。

from flask import Flask,session,request,render_template
import os
app = Flask(__name__) ##  实例化

# 配置
app.config.update(
    DEBUG = True,
    SECRET_KEY = os.urandom(24) # 使用os.urandom生成随机,进行加密
)

@app.route('/',methods=['GET','POST'])
def index():
    # 判断是否是 GET
    if request.method == 'GET':
        return render_template("index.html",id=session.get('id')) # 用get,如果id不存在就不会返回异常,而是None

    # 如果不是GET,就是 POST
    # Linus: 好代码是很少使用if等判断的.
    session['id'] = request.form.get('id')
    return render_template('index.html',id=session.get('id'))

if __name__ == '__main__':
    app.run()

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>学习session</title>
</head>
<body>
<center>
    <div>
        <form action="{{ url_for('index') }}" method="POST">

        输入ID: <input type="text" name="id">

        </form>
    </div>

    <h2>{% if id %} ID是: {{ id }}{% else %}Hello Strong{% endif %}</h2>
    <!--
    这里分开其实就是这样的:
        {% if id %}
            ID是: {{ id }}
        {% else %}
            Hello Strong
       {% endif %}
    -->
</center>

</body>
</html>

blog.py 中:

第八行: 使用session的话 SECRET_KEY 这项配置必须给True给session配置一个秘钥,使用 os.uranger()  进行生成,官方推荐的

这样看的话,感觉session没起到啥效果,但是!

我们用两个浏览器测试!

可以看到, session针对不同的客户端,会分配一个不同的回话。

如果你使用一个变量,而不是 session ,就完成不了这样的效果。

自定义错误

from flask import Flask
app = Flask(__name__) ##  实例化
# 配置
app.config['DEBUT'] = True

@app.route('/')
def index():
    return "Index"

@app.errorhandler(404)
def error_404(error):
    return '404 Not Found',404 

if __name__ == '__main__':
    app.run()

第10行: 利用装饰器来捕获404错误,当发生404时候,就会触发这个 error_404 这个视图函数,其中 errorhandler 中的参数 404是状态码

第12行: 可以看到,return两个值,一个是页面,一个是状态码。如果不给第二个值(状态码)的话,就会只返回页面,不会返回状态码,就像这样。

所以,我们要在 return 后带上状态码,就像这样。

当然,你也可以捕获其他页面错误,比如403之类的

静态文件

每个页面如果没有js和css,有时候显的会很Low,而且很多功能是需要js的。那么,我们如何去用这些资源呢?

难道我们要写到html里面吗?那请问我们有上万行css,你也要写到html里面吗?所以我们要去引用css或者js再或者是图片等等。

通常,像图片,css,视频,js等等这些文件,通常为 “静态文件”

在Flask中,有专门去存放这类文件的,在项目目录下去新建一个名为 static 的文件夹.

然后我们在 static 下新建一个名为 message.txt 文本。

然后用浏览器去访问这个静态资源

你只能去 /static/下获取。

当然,你可以指定哪个是静态文件夹,app = Flask(__name__,static_folder="你指定的文件夹")

我们来看下Flask方法的源码

如果我们在模板中去用静态文件呢?难道我们要去一个一个去写完整路径吗?

答案是:不用!

大家还记得 url_for() 函数吗?

没错,我们可以使用 url_for() 去写完整路径。

比如我现在有一个名为 bootstrap.min.css 的文件在 static 目录下的 css 文件下。

我们来获取这个完整路径

url_for('static',filename="css/bootstrap.min.css")

在html模板中,用 link 标签来引入的话可以这样写。

<link rel="stylesheet" href="{{ url_for("static",filename="css/bootstrap.min.css") }}" />

有问题的小伙伴可以来咨询!

微信二维码

转载请注明:静觅 » 【Python搞搞轻量博客】必要的知识

Flask 静态文件缓存问题

$
0
0

大家好,今天才发现很多学习Flask的小伙伴都有这么一个问题,清理缓存好麻烦啊,今天就教大家怎么解决。

大家在使用Flask静态文件的时候,每次更新,发现CSS或是Js或者其他的文件不会更新。

这是因为浏览器的缓存问题。

普遍大家是这几步解决办法。

  • 清理浏览器缓存
  • 设置浏览器不缓存
  • 也有以下这么写的

@app.context_processor
def override_url_for():
    return dict(url_for=dated_url_for)

def dated_url_for(endpoint, **values):
    if endpoint == 'static':
        filename = values.get('filename', None)
    if filename:
        file_path = os.path.join(app.root_path, endpoint, filename)
        values['q'] = int(os.stat(file_path).st_mtime)
        return url_for(endpoint, **values)

如果是我,我不会这么做,效率很低。

这是  Flask的 config 的源码,里面可以看到,有设置缓存最大时间

SEND_FILE_MAX_AGE_DEFAULT 可以看到,它是一个 temedelta 的值

我们去更改配置。

第2行: 我们引入了datetimetimedelta对象

第6行: 我们配置缓存最大时间

 

这样就解决了缓存问题,不用去写多余的代码,不用去清理浏览器的缓存。

一定要学着去看官方文档和框架的源代码!!

有什么问题请联系

微信二维码

转载请注明:静觅 » Flask 静态文件缓存问题

《Python3网络爬虫开发实战》来了!文末另有抽奖送书及优惠券福利!

$
0
0

嗨~ 给大家重磅推荐一本新书!还未上市前就已经重印 3 次的 Python 爬虫书!那么它就是由静觅博客博主崔庆才所作的《Python3网络爬虫开发实战》!!!

书籍介绍

本书《Python3网络爬虫开发实战》全面介绍了利用 Python3 开发网络爬虫的知识,书中首先详细介绍了各种类型的环境配置过程和爬虫基础知识,还讨论了 urllib、requests 等请求库和 Beautiful Soup、XPath、pyquery 等解析库以及文本和各类数据库的存储方法,另外本书通过多个真实新鲜案例介绍了分析 Ajax 进行数据爬取,Selenium 和 Splash 进行动态网站爬取的过程,接着又分享了一些切实可行的爬虫技巧,比如使用代理爬取和维护动态代理池的方法、ADSL 拨号代理的使用、各类验证码(图形、极验、点触、宫格等)的破解方法、模拟登录网站爬取的方法及 Cookies 池的维护等等。

此外,本书的内容还远远不止这些,作者还结合移动互联网的特点探讨了使用 Charles、mitmdump、Appium 等多种工具实现 App 抓包分析、加密参数接口爬取、微信朋友圈爬取的方法。此外本书还详细介绍了 pyspider 框架、Scrapy 框架的使用和分布式爬虫的知识,另外对于优化及部署工作,本书还包括 Bloom Filter 效率优化、Docker 和 Scrapyd 爬虫部署、分布式爬虫管理框架Gerapy 的分享。

全书共 604 页,足足两斤重呢~ 定价为 99 元!

作者介绍

看书就先看看谁写的嘛,我们来了解一下~

崔庆才,静觅博客博主(https://cuiqingcai.com),博客 Python 爬虫博文已过百万,北京航空航天大学硕士,微软小冰大数据工程师,有多个大型分布式爬虫项目经验,乐于技术分享,文章通俗易懂 ^_^

附皂片一张 ~(@^_^@)~

图文介绍

呕心沥血设计的宣传图也得放一下~

专家评论

书是好是坏,得让专家看评一评呀,那么下面就是几位专家的精彩评论,快来看看吧~

在互联网软件开发工程师的分类中,爬虫工程师是非常重要的。爬虫工作往往是一个公司核心业务开展的基础,数据抓取下来,才有后续的加工处理和最终展现。此时数据的抓取规模、稳定性、实时性、准确性就显得非常重要。早期的互联网充分开放互联,数据获取的难度很小。随着各大公司对数据资产日益看重,反爬水平也在不断提高,各种新技术不断给爬虫软件提出新的课题。本书作者对爬虫的各个领域都有深刻研究,书中探讨了Ajax数据的抓取、动态渲染页面的抓取、验证码识别、模拟登录等高级话题,同时也结合移动互联网的特点探讨了App的抓取等。更重要的是,本书提供了大量源码,可以帮助读者更好地理解相关内容。强烈推荐给各位技术爱好者阅读!

——梁斌,八友科技总经理

数据既是当今大数据分析的前提,也是各种人工智能应用场景的基础。得数据者得天下,会爬虫者走遍天下也不怕!一册在手,让小白到老司机都能有所收获!

——李舟军,北京航空航天大学教授,博士生导师

本书从爬虫入门到分布式抓取,详细介绍了爬虫技术的各个要点,并针对不同的场景提出了对应的解决方案。另外,书中通过大量的实例来帮助读者更好地学习爬虫技术,通俗易懂,干货满满。强烈推荐给大家!

——宋睿华,微软小冰首席科学家

有人说中国互联网的带宽全给各种爬虫占据了,这说明网络爬虫的重要性以及中国互联网数据封闭垄断的现状。爬是一种能力,爬是为了不爬。

——施水才,北京拓尔思信息技术股份有限公司总裁

全书目录

书的目录也有~ 看这里!

  • 1-开发环境配置
  • 1.1-Python3的安装
  • 1.2-请求库的安装
  • 1.3-解析库的安装
  • 1.4-数据库的安装
  • 1.5-存储库的安装
  • 1.6-Web库的安装
  • 1.7-App爬取相关库的安装
  • 1.8-爬虫框架的安装
  • 1.9-部署相关库的安装
  • 2-爬虫基础
  • 2.1-HTTP基本原理
  • 2.2-网页基础
  • 2.3-爬虫的基本原理
  • 2.4-会话和Cookies
  • 2.5-代理的基本原理
  • 3-基本库的使用
  • 3.1-使用urllib
  • 3.1.1-发送请求
  • 3.1.2-处理异常
  • 3.1.3-解析链接
  • 3.1.4-分析Robots协议
  • 3.2-使用requests
  • 3.2.1-基本用法
  • 3.2.2-高级用法
  • 3.3-正则表达式
  • 3.4-抓取猫眼电影排行
  • 4-解析库的使用
  • 4.1-使用XPath
  • 4.2-使用Beautiful Soup
  • 4.3-使用pyquery
  • 5-数据存储
  • 5.1-文件存储
  • 5.1.1-TXT文本存储
  • 5.1.2-JSON文件存储
  • 5.1.3-CSV文件存储
  • 5.2-关系型数据库存储
  • 5.2.1-MySQL存储
  • 5.3-非关系型数据库存储
  • 5.3.1-MongoDB存储
  • 5.3.2-Redis存储
  • 6-Ajax数据爬取
  • 6.1-什么是Ajax
  • 6.2-Ajax分析方法
  • 6.3-Ajax结果提取
  • 6.4-分析Ajax爬取今日头条街拍美图
  • 7-动态渲染页面爬取
  • 7.1-Selenium的使用
  • 7.2-Splash的使用
  • 7.3-Splash负载均衡配置
  • 7.4-使用Selenium爬取淘宝商品
  • 8-验证码的识别
  • 8.1-图形验证码的识别
  • 8.2-极验滑动验证码的识别
  • 8.3-点触验证码的识别
  • 8.4-微博宫格验证码的识别
  • 9-代理的使用
  • 9.1-代理的设置
  • 9.2-代理池的维护
  • 9.3-付费代理的使用
  • 9.4-ADSL拨号代理
  • 9.5-使用代理爬取微信公众号文章
  • 10-模拟登录
  • 10.1-模拟登录并爬取GitHub
  • 10.2-Cookies池的搭建
  • 11-App的爬取
  • 11.1-Charles的使用
  • 11.2-mitmproxy的使用
  • 11.3-mitmdump爬取“得到”App电子书信息
  • 11.4-Appium的基本使用
  • 11.5-Appium爬取微信朋友圈
  • 11.6-Appium+mitmdump爬取京东商品
  • 12-pyspider框架的使用
  • 12.1-pyspider框架介绍
  • 12.2-pyspider的基本使用
  • 12.3-pyspider用法详解
  • 13-Scrapy框架的使用
  • 13.1-Scrapy框架介绍
  • 13.2-Scrapy入门
  • 13.3-Selector的用法
  • 13.4-Spider的用法
  • 13.5-Downloader Middleware的用法
  • 13.6-Spider Middleware的用法
  • 13.7-Item Pipeline的用法
  • 13.8-Scrapy对接Selenium
  • 13.9-Scrapy对接Splash
  • 13.10-Scrapy通用爬虫
  • 13.11-Scrapyrt的使用
  • 13.12-Scrapy对接Docker
  • 13.13-Scrapy爬取新浪微博
  • 14-分布式爬虫
  • 14.1-分布式爬虫原理
  • 14.2-Scrapy-Redis源码解析
  • 14.3-Scrapy分布式实现
  • 14.4-Bloom Filter的对接
  • 15-分布式爬虫的部署
  • 15.1-Scrapyd分布式部署
  • 15.2-Scrapyd-Client的使用
  • 15.3-Scrapyd对接Docker
  • 15.4-Scrapyd批量部署
  • 15.5-Gerapy分布式管理

购买链接

想必很多小伙伴已经等了很久了,之前预售那么久也一直迟迟没有货,发售就有不少网店又售空了,不过现在起不用担心了!

书籍现已在京东、天猫、当当等网店上架并全面供应啦,复制链接到浏览器打开或扫描二维码打开即可购买了!

 京东商城

https://item.jd.com/12333540.html

 天猫商城

https://detail.tmall.com/item.htm?id=566699703917

 当当网

http://product.dangdang.com/25249602.html

欢迎大家购买,O(∩_∩)O

免费预览

不放心?想先看看有些啥,没问题!看这里:

免费章节试读(复制粘贴至浏览器打开):

将一直免费开放前7章节,欢迎大家试读!

好了,接下来就是我们的福利环节啦~

福利一:抽奖送书!!!

恭喜你看到这里了!那么接下来的福利时间就到了!后面还有两个福利不容错过哦~

抽奖送书活动第一波来袭(后面还有很多波哦),公众号抽奖送 30 本作者亲笔签名书籍!!!

活动流程(重要,请一定认真阅读这两条):

  1. 公众号进击的Coder回复 “抽奖” 获取抽奖码,请记住您的抽奖码,2018.5.15 22:00 截止,逾期参与无效,活动结束后会从参与活动的小伙伴中随机抽取 30 位并在微信公众号公布,届时请关注公众号消息!获奖的小伙伴会获得作者亲笔签名的《Python3网络爬虫开发实战》一本。
  2. 参加活动的小伙伴可以提前填写好收货地址,以免中奖之后联系不上无法发货,地址填写入口在公众号回复“抽奖”后返回图文结果的第三个,本活动绝对会保证您的地址隐私安全,如不放心可在得知中奖结果后填写,如果公布结果后七天内未填写收货地址,则视为放弃。

福利二:独家优惠!!!

等等,你以为这就是全部福利吗?当然不是!除了抽奖送书,我们还拿到了拨号VPS知名品牌云立方的独家优惠,在公众号(进击的Coder )中回复:“优惠券”,即可免费领取云立方50元主机优惠券,数量有限,先到先得!优惠券可在云立方官网(www.yunlifang.cn)购买动态IP拨号VPS时抵扣现金,有了它,爬虫代理易如反掌!

你问我动态拨号VPS能做什么?应该怎么用在爬虫里?来这里了解一下:

轻松获得海量稳定代理!ADSL拨号代理的搭建

福利三:视频课程!!!

当然除了书籍,也有配套的视频课程,作者同样是崔庆才,二者结合学习效果更佳!限时优惠折扣中!扫描下图中二维码即可了解详情!

最后也是最重要的就是参与活动的地址了!!!快来扫码回复领取属于你的福利吧!!!

特别致谢

最后特别感谢云立方、天善智能对本活动的大力支持!

 

转载请注明:静觅 » 《Python3网络爬虫开发实战》来了!文末另有抽奖送书及优惠券福利!


Python中logging模块的基本用法

$
0
0

在 PyCon 2018 上,Mario Corchero 介绍了在开发过程中如何更方便轻松地记录日志的流程。

整个演讲的内容包括:

  • 为什么日志记录非常重要
  • 日志记录的流程是怎样的
  • 怎样来进行日志记录
  • 怎样进行日志记录相关配置
  • 日志记录使用常见误区

下面我们来梳理一下整个演讲的过程,其实其核心就是介绍了 logging 模块的使用方法和一些配置。

日志记录的重要性

在开发过程中,如果程序运行出现了问题,我们是可以使用我们自己的 Debug 工具来检测到到底是哪一步出现了问题,如果出现了问题的话,是很容易排查的。但程序开发完成之后,我们会将它部署到生产环境中去,这时候代码相当于是在一个黑盒环境下运行的,我们只能看到其运行的效果,是不能直接看到代码运行过程中每一步的状态的。在这个环境下,运行过程中难免会在某个地方出现问题,甚至这个问题可能是我们开发过程中未曾遇到的问题,碰到这种情况应该怎么办?

如果我们现在只能得知当前问题的现象,而没有其他任何信息的话,如果我们想要解决掉这个问题的话,那么只能根据问题的现象来试图复现一下,然后再一步步去调试,这恐怕是很难的,很大的概率上我们是无法精准地复现这个问题的,而且 Debug 的过程也会耗费巨多的时间,这样一旦生产环境上出现了问题,修复就会变得非常棘手。但这如果我们当时有做日志记录的话,不论是正常运行还是出现报错,都有相关的时间记录,状态记录,错误记录等,那么这样我们就可以方便地追踪到在当时的运行过程中出现了怎样的状况,从而可以快速排查问题。

因此,日志记录是非常有必要的,任何一款软件如果没有标准的日志记录,都不能算作一个合格的软件。作为开发者,我们需要重视并做好日志记录过程。

日志记录的流程框架

那么在 Python 中,怎样才能算作一个比较标准的日志记录过程呢?或许很多人会使用 print 语句输出一些运行信息,然后再在控制台观察,运行的时候再将输出重定向到文件输出流保存到文件中,这样其实是非常不规范的,在 Python 中有一个标准的 logging 模块,我们可以使用它来进行标注的日志记录,利用它我们可以更方便地进行日志记录,同时还可以做更方便的级别区分以及一些额外日志信息的记录,如时间、运行模块信息等。

接下来我们先了解一下日志记录流程的整体框架。

如图所示,整个日志记录的框架可以分为这么几个部分:

  • Logger:即 Logger Main Class,是我们进行日志记录时创建的对象,我们可以调用它的方法传入日志模板和信息,来生成一条条日志记录,称作 Log Record。
  • Log Record:就代指生成的一条条日志记录。
  • Handler:即用来处理日志记录的类,它可以将 Log Record 输出到我们指定的日志位置和存储形式等,如我们可以指定将日志通过 FTP 协议记录到远程的服务器上,Handler 就会帮我们完成这些事情。
  • Formatter:实际上生成的 Log Record 也是一个个对象,那么我们想要把它们保存成一条条我们想要的日志文本的话,就需要有一个格式化的过程,那么这个过程就由 Formatter 来完成,返回的就是日志字符串,然后传回给 Handler 来处理。
  • Filter:另外保存日志的时候我们可能不需要全部保存,我们可能只需要保存我们想要的部分就可以了,所以保存前还需要进行一下过滤,留下我们想要的日志,如只保存某个级别的日志,或只保存包含某个关键字的日志等,那么这个过滤过程就交给 Filter 来完成。
  • Parent Handler:Handler 之间可以存在分层关系,以使得不同 Handler 之间共享相同功能的代码。

以上就是整个 logging 模块的基本架构和对象功能,了解了之后我们详细来了解一下 logging 模块的用法。

日志记录的相关用法

总的来说 logging 模块相比 print 有这么几个优点:

  • 可以在 logging 模块中设置日志等级,在不同的版本(如开发环境、生产环境)上通过设置不同的输出等级来记录对应的日志,非常灵活。
  • print 的输出信息都会输出到标准输出流中,而 logging 模块就更加灵活,可以设置输出到任意位置,如写入文件、写入远程服务器等。
  • logging 模块具有灵活的配置和格式化功能,如配置输出当前模块信息、运行时间等,相比 print 的字符串格式化更加方便易用。

下面我们初步来了解下 logging 模块的基本用法,先用一个实例来感受一下:

import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

在这里我们首先引入了 logging 模块,然后进行了一下基本的配置,这里通过 basicConfig 配置了 level 信息和 format 信息,这里 level 配置为 INFO 信息,即只输出 INFO 级别的信息,另外这里指定了 format 格式的字符串,包括 asctime、name、levelname、message 四个内容,分别代表运行时间、模块名称、日志级别、日志内容,这样输出内容便是这四者组合而成的内容了,这就是 logging 的全局配置。

接下来声明了一个 Logger 对象,它就是日志输出的主类,调用对象的 info() 方法就可以输出 INFO 级别的日志信息,调用 debug() 方法就可以输出 DEBUG 级别的日志信息,非常方便。在初始化的时候我们传入了模块的名称,这里直接使用 __name__ 来代替了,就是模块的名称,如果直接运行这个脚本的话就是 __main__,如果是 import 的模块的话就是被引入模块的名称,这个变量在不同的模块中的名字是不同的,所以一般使用 __name__ 来表示就好了,再接下来输出了四条日志信息,其中有两条 INFO、一条 WARNING、一条 DEBUG 信息,我们看下输出结果:

2018-06-03 13:42:43,526 - __main__ - INFO - This is a log info
2018-06-03 13:42:43,526 - __main__ - WARNING - Warning exists
2018-06-03 13:42:43,526 - __main__ - INFO - Finish

可以看到输出结果一共有三条日志信息,每条日志都是对应了指定的格式化内容,另外我们发现 DEBUG 的信息是没有输出的,这是因为我们在全局配置的时候设置了输出为 INFO 级别,所以 DEBUG 级别的信息就被过滤掉了。

这时如果我们将输出的日志级别设置为 DEBUG,就可以看到 DEBUG 级别的日志输出了:

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

输出结果:

2018-06-03 13:49:22,770 - __main__ - INFO - This is a log info
2018-06-03 13:49:22,770 - __main__ - DEBUG - Debugging
2018-06-03 13:49:22,770 - __main__ - WARNING - Warning exists
2018-06-03 13:49:22,770 - __main__ - INFO - Finish

由此可见,相比 print 来说,通过刚才的代码,我们既可以输出时间、模块名称,又可以输出不同级别的日志信息作区分并加以过滤,是不是灵活多了?

当然这只是 logging 模块的一小部分功能,接下来我们首先来全面了解一下 basicConfig 的参数都有哪些:

  • filename:即日志输出的文件名,如果指定了这个信息之后,实际上会启用 FileHandler,而不再是 StreamHandler,这样日志信息便会输出到文件中了。
  • filemode:这个是指定日志文件的写入方式,有两种形式,一种是 w,一种是 a,分别代表清除后写入和追加写入。
  • format:指定日志信息的输出格式,即上文示例所示的参数,详细参数可以参考:docs.python.org/3/library/l…,部分参数如下所示:
    • %(levelno)s:打印日志级别的数值。
    • %(levelname)s:打印日志级别的名称。
    • %(pathname)s:打印当前执行程序的路径,其实就是sys.argv[0]。
    • %(filename)s:打印当前执行程序名。
    • %(funcName)s:打印日志的当前函数。
    • %(lineno)d:打印日志的当前行号。
    • %(asctime)s:打印日志的时间。
    • %(thread)d:打印线程ID。
    • %(threadName)s:打印线程名称。
    • %(process)d:打印进程ID。
    • %(processName)s:打印线程名称。
    • %(module)s:打印模块名称。
    • %(message)s:打印日志信息。
  • datefmt:指定时间的输出格式。
  • style:如果 format 参数指定了,这个参数就可以指定格式化时的占位符风格,如 %、{、$ 等。
  • level:指定日志输出的类别,程序会输出大于等于此级别的信息。
  • stream:在没有指定 filename 的时候会默认使用 StreamHandler,这时 stream 可以指定初始化的文件流。
  • handlers:可以指定日志处理时所使用的 Handlers,必须是可迭代的。

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

import logging

logging.basicConfig(level=logging.DEBUG,
                    filename='output.log',
                    datefmt='%Y/%m/%d %H:%M:%S',
                    format='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(module)s - %(message)s')
logger = logging.getLogger(__name__)

logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

这里我们指定了输出文件的名称为 output.log,另外指定了日期的输出格式,其中年月日的格式变成了 %Y/%m/%d,另外输出的 format 格式增加了 lineno、module 这两个信息,运行之后便会生成一个 output.log 的文件,内容如下:

2018/06/03 14:43:26 - __main__ - INFO - 9 - demo3 - This is a log info
2018/06/03 14:43:26 - __main__ - DEBUG - 10 - demo3 - Debugging
2018/06/03 14:43:26 - __main__ - WARNING - 11 - demo3 - Warning exists
2018/06/03 14:43:26 - __main__ - INFO - 12 - demo3 - Finish

可以看到日志便会输出到文件中,同时输出了行号、模块名称等信息。

以上我们通过 basicConfig 来进行了一些全局的配置,我们同样可以使用 Formatter、Handler 进行更灵活的处理,下面我们来了解一下。

Level

首先我们来了解一下输出日志的等级信息,logging 模块共提供了如下等级,每个等级其实都对应了一个数值,列表如下:

等级 数值
CRITICAL 50
FATAL 50
ERROR 40
WARNING 30
WARN 30
INFO 20
DEBUG 10
NOTSET 0

这里最高的等级是 CRITICAL 和 FATAL,两个对应的数值都是 50,另外对于 WARNING 还提供了简写形式 WARN,两个对应的数值都是 30。

我们设置了输出 level,系统便只会输出 level 数值大于或等于该 level 的的日志结果,例如我们设置了输出日志 level 为 INFO,那么输出级别大于等于 INFO 的日志,如 WARNING、ERROR 等,DEBUG 和 NOSET 级别的不会输出。

import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.WARN)

# Log
logger.debug('Debugging')
logger.critical('Critical Something')
logger.error('Error Occurred')
logger.warning('Warning exists')
logger.info('Finished')

这里我们设置了输出级别为 WARN,然后对应输出了五种不同级别的日志信息,运行结果如下:

Critical Something
Error Occurred
Warning exists

可以看到只有 CRITICAL、ERROR、WARNING 信息输出了,DEBUG、INFO 信息没有输出。

Handler

下面我们先来了解一下 Handler 的用法,看下面的实例:

import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)
handler = logging.FileHandler('output.log')
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

这里我们没有再使用 basicConfig 全局配置,而是先声明了一个 Logger 对象,然后指定了其对应的 Handler 为 FileHandler 对象,然后 Handler 对象还单独指定了 Formatter 对象单独配置输出格式,最后给 Logger 对象添加对应的 Handler 即可,最后可以发现日志就会被输出到 output.log 中,内容如下:

2018-06-03 14:53:36,467 - __main__ - INFO - This is a log info
2018-06-03 14:53:36,468 - __main__ - WARNING - Warning exists
2018-06-03 14:53:36,468 - __main__ - INFO - Finish

另外我们还可以使用其他的 Handler 进行日志的输出,logging 模块提供的 Handler 有:

  • StreamHandler:logging.StreamHandler;日志输出到流,可以是 sys.stderr,sys.stdout 或者文件。
  • FileHandler:logging.FileHandler;日志输出到文件。
  • BaseRotatingHandler:logging.handlers.BaseRotatingHandler;基本的日志回滚方式。
  • RotatingHandler:logging.handlers.RotatingHandler;日志回滚方式,支持日志文件最大数量和日志文件回滚。
  • TimeRotatingHandler:logging.handlers.TimeRotatingHandler;日志回滚方式,在一定时间区域内回滚日志文件。
  • SocketHandler:logging.handlers.SocketHandler;远程输出日志到TCP/IP sockets。
  • DatagramHandler:logging.handlers.DatagramHandler;远程输出日志到UDP sockets。
  • SMTPHandler:logging.handlers.SMTPHandler;远程输出日志到邮件地址。
  • SysLogHandler:logging.handlers.SysLogHandler;日志输出到syslog。
  • NTEventLogHandler:logging.handlers.NTEventLogHandler;远程输出日志到Windows NT/2000/XP的事件日志。
  • MemoryHandler:logging.handlers.MemoryHandler;日志输出到内存中的指定buffer。
  • HTTPHandler:logging.handlers.HTTPHandler;通过”GET”或者”POST”远程输出到HTTP服务器。

下面我们使用三个 Handler 来实现日志同时输出到控制台、文件、HTTP 服务器:

import logging
from logging.handlers import HTTPHandler
import sys

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)

# StreamHandler
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(level=logging.DEBUG)
logger.addHandler(stream_handler)

# FileHandler
file_handler = logging.FileHandler('output.log')
file_handler.setLevel(level=logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

# HTTPHandler
http_handler = HTTPHandler(host='localhost:8001', url='log', method='POST')
logger.addHandler(http_handler)

# Log
logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

运行之前我们需要先启动 HTTP Server,并运行在 8001 端口,其中 log 接口是用来接收日志的接口。

运行之后控制台输出会输出如下内容:

This is a log info
Debugging
Warning exists
Finish

output.log 文件会写入如下内容:

2018-06-03 15:13:44,895 - __main__ - INFO - This is a log info
2018-06-03 15:13:44,947 - __main__ - WARNING - Warning exists
2018-06-03 15:13:44,949 - __main__ - INFO - Finish

HTTP Server 会收到控制台输出的信息。

这样一来,我们就通过设置多个 Handler 来控制了日志的多目标输出。

另外值得注意的是,在这里 StreamHandler 对象我们没有设置 Formatter,因此控制台只输出了日志的内容,而没有包含时间、模块等信息,而 FileHandler 我们通过 setFormatter() 方法设置了一个 Formatter 对象,因此输出的内容便是格式化后的日志信息。

另外每个 Handler 还可以设置 level 信息,最终输出结果的 level 信息会取 Logger 对象的 level 和 Handler 对象的 level 的交集。

Formatter

在进行日志格式化输出的时候,我们可以不借助于 basicConfig 来全局配置格式化输出内容,可以借助于 Formatter 来完成,下面我们再来单独看下 Formatter 的用法:

import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.WARN)
formatter = logging.Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y/%m/%d %H:%M:%S')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)

# Log
logger.debug('Debugging')
logger.critical('Critical Something')
logger.error('Error Occurred')
logger.warning('Warning exists')
logger.info('Finished')

在这里我们指定了一个 Formatter,并传入了 fmt 和 datefmt 参数,这样就指定了日志结果的输出格式和时间格式,然后 handler 通过 setFormatter() 方法设置此 Formatter 对象即可,输出结果如下:

2018/06/03 15:47:15 - __main__ - CRITICAL - Critical Something
2018/06/03 15:47:15 - __main__ - ERROR - Error Occurred
2018/06/03 15:47:15 - __main__ - WARNING - Warning exists

这样我们可以每个 Handler 单独配置输出的格式,非常灵活。

捕获 Traceback

如果遇到错误,我们更希望报错时出现的详细 Traceback 信息,便于调试,利用 logging 模块我们可以非常方便地实现这个记录,我们用一个实例来感受一下:

import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)

# Formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# FileHandler
file_handler = logging.FileHandler('result.log')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

# StreamHandler
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)

# Log
logger.info('Start')
logger.warning('Something maybe fail.')
try:
    result = 10 / 0
except Exception:
    logger.error('Faild to get result', exc_info=True)
logger.info('Finished')

这里我们在 error() 方法中添加了一个参数,将 exc_info 设置为了 True,这样我们就可以输出执行过程中的信息了,即完整的 Traceback 信息。

运行结果如下:

2018-06-03 16:00:15,382 - __main__ - INFO - Start print log
2018-06-03 16:00:15,382 - __main__ - DEBUG - Do something
2018-06-03 16:00:15,382 - __main__ - WARNING - Something maybe fail.
2018-06-03 16:00:15,382 - __main__ - ERROR - Faild to get result
Traceback (most recent call last):
  File "/private/var/books/aicodes/loggingtest/demo8.py", line 23, in <module>
    result = 10 / 0
ZeroDivisionError: division by zero
2018-06-03 16:00:15,383 - __main__ - INFO - Finished

可以看到这样我们就非常方便地记录下来了报错的信息,一旦出现了错误,我们也能非常方便地排查。

配置共享

在写项目的时候,我们肯定会将许多配置放置在许多模块下面,这时如果我们每个文件都来配置 logging 配置那就太繁琐了,logging 模块提供了父子模块共享配置的机制,会根据 Logger 的名称来自动加载父模块的配置。

例如我们这里首先定义一个 main.py 文件:

import logging
import core

logger = logging.getLogger('main')
logger.setLevel(level=logging.DEBUG)

# Handler
handler = logging.FileHandler('result.log')
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

logger.info('Main Info')
logger.debug('Main Debug')
logger.error('Main Error')
core.run()

这里我们配置了日志的输出格式和文件路径,同时定义了 Logger 的名称为 main,然后引入了另外一个模块 core,最后调用了 core 的 run() 方法。

接下来我们定义 core.py,内容如下:

import logging

logger = logging.getLogger('main.core')

def run():
    logger.info('Core Info')
    logger.debug('Core Debug')
    logger.error('Core Error')

这里我们定义了 Logger 的名称为 main.core,注意这里开头是 main,即刚才我们在 main.py 里面的 Logger 的名称,这样 core.py 里面的 Logger 就会复用 main.py 里面的 Logger 配置,而不用再去配置一次了。

运行之后会生成一个 result.log 文件,内容如下:

2018-06-03 16:55:56,259 - main - INFO - Main Info
2018-06-03 16:55:56,259 - main - ERROR - Main Error
2018-06-03 16:55:56,259 - main.core - INFO - Core Info
2018-06-03 16:55:56,259 - main.core - ERROR - Core Error

可以看到父子模块都使用了同样的输出配置。

如此一来,我们只要在入口文件里面定义好 logging 模块的输出配置,子模块只需要在定义 Logger 对象时名称使用父模块的名称开头即可共享配置,非常方便。

文件配置

在开发过程中,将配置在代码里面写死并不是一个好的习惯,更好的做法是将配置写在配置文件里面,我们可以将配置写入到配置文件,然后运行时读取配置文件里面的配置,这样是更方便管理和维护的,下面我们以一个实例来说明一下,首先我们定义一个 yaml 配置文件:

version: 1
formatters:
  brief:
    format: "%(asctime)s - %(message)s"
  simple:
    format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers:
  console:
    class : logging.StreamHandler
    formatter: brief
    level   : INFO
    stream  : ext://sys.stdout
  file:
    class : logging.FileHandler
    formatter: simple
    level: DEBUG
    filename: debug.log
  error:
    class: logging.handlers.RotatingFileHandler
    level: ERROR
    formatter: simple
    filename: error.log
    maxBytes: 10485760
    backupCount: 20
    encoding: utf8
loggers:
  main.core:
    level: DEBUG
    handlers: [console, file, error]
root:
  level: DEBUG
  handlers: [console]

这里我们定义了 formatters、handlers、loggers、root 等模块,实际上对应的就是各个 Formatter、Handler、Logger 的配置,参数和它们的构造方法都是相同的。

接下来我们定义一个主入口文件,main.py,内容如下:

import logging
import core
import yaml
import logging.config
import os


def setup_logging(default_path='config.yaml', default_level=logging.INFO):
    path = default_path
    if os.path.exists(path):
        with open(path, 'r', encoding='utf-8') as f:
            config = yaml.load(f)
            logging.config.dictConfig(config)
    else:
        logging.basicConfig(level=default_level)


def log():
    logging.debug('Start')
    logging.info('Exec')
    logging.info('Finished')


if __name__ == '__main__':
    yaml_path = 'config.yaml'
    setup_logging(yaml_path)
    log()
    core.run()

这里我们定义了一个 setup_logging() 方法,里面读取了 yaml 文件的配置,然后通过 dictConfig() 方法将配置项传给了 logging 模块进行全局初始化。

另外这个模块还引入了另外一个模块 core,所以我们定义 core.py 如下:

import logging

logger = logging.getLogger('main.core')

def run():
    logger.info('Core Info')
    logger.debug('Core Debug')
    logger.error('Core Error')

这个文件的内容和上文是没有什么变化的。

观察配置文件,主入口文件 main.py 实际上对应的是 root 一项配置,它指定了 handlers 是 console,即只输出到控制台。另外在 loggers 一项配置里面,我们定义了 main.core 模块,handlers 是 console、file、error 三项,即输出到控制台、输出到普通文件和回滚文件。

这样运行之后,我们便可以看到所有的运行结果输出到了控制台:

2018-06-03 17:07:12,727 - Exec
2018-06-03 17:07:12,727 - Finished
2018-06-03 17:07:12,727 - Core Info
2018-06-03 17:07:12,727 - Core Info
2018-06-03 17:07:12,728 - Core Error
2018-06-03 17:07:12,728 - Core Error

在 debug.log 文件中则包含了 core.py 的运行结果:

2018-06-03 17:07:12,727 - main.core - INFO - Core Info
2018-06-03 17:07:12,727 - main.core - DEBUG - Core Debug
2018-06-03 17:07:12,728 - main.core - ERROR - Core Error

可以看到,通过配置文件,我们可以非常灵活地定义 Handler、Formatter、Logger 等配置,同时也显得非常直观,也非常容易维护,在实际项目中,推荐使用此种方式进行配置。

以上便是 logging 模块的基本使用方法,有了它,我们可以方便地进行日志管理和维护,会给我们的工作带来极大的方便。

日志记录使用常见误区

在日志输出的时候经常我们会用到字符串拼接的形式,很多情况下我们可能会使用字符串的 format() 来构造一个字符串,但这其实并不是一个好的方法,因为还有更好的方法,下面我们对比两个例子:

import logging

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# bad
logging.debug('Hello {0}, {1}!'.format('World', 'Congratulations'))
# good
logging.debug('Hello %s, %s!', 'World', 'Congratulations')

这里有两种打印 Log 的方法,第一种使用了字符串的 format() 的方法进行构造,传给 logging 的只用到了第一个参数,实际上 logging 模块提供了字符串格式化的方法,我们只需要在第一个参数写上要打印输出的模板,占位符用 %s、%d 等表示即可,然后在后续参数添加对应的值就可以了,推荐使用这种方法。

运行结果如下:

2018-06-03 22:27:51,220 - root - DEBUG - Hello World, Congratulations!
2018-06-03 22:27:51,220 - root - DEBUG - Hello World, Congratulations!

另外在进行异常处理的时候,通常我们会直接将异常进行字符串格式化,但其实可以直接指定一个参数将 traceback 打印出来,示例如下:

import logging

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

try:
    result = 5 / 0
except Exception as e:
    # bad
    logging.error('Error: %s', e)
    # good
    logging.error('Error', exc_info=True)
    # good
    logging.exception('Error')

如果我们直接使用字符串格式化的方法将错误输出的话,是不会包含 Traceback 信息的,但如果我们加上 exc_info 参数或者直接使用 exception() 方法打印的话,那就会输出 Traceback 信息了。

运行结果如下:

2018-06-03 22:24:31,927 - root - ERROR - Error: division by zero
2018-06-03 22:24:31,927 - root - ERROR - Error
Traceback (most recent call last):
  File "/private/var/books/aicodes/loggingtest/demo9.py", line 6, in <module>
    result = 5 / 0
ZeroDivisionError: division by zero
2018-06-03 22:24:31,928 - root - ERROR - Error
Traceback (most recent call last):
  File "/private/var/books/aicodes/loggingtest/demo9.py", line 6, in <module>
    result = 5 / 0
ZeroDivisionError: division by zero

以上便是整个对 logging 模块的介绍。嗯,是时候抛弃 print 了,开始体验下 logging 的便利吧!

参考内容

https://docs.python.org/3/library/logging.html

http://www.cnblogs.com/dahu-daqing/p/7040764.html

转载请注明:静觅 » Python中logging模块的基本用法

小白进阶之Scrapy第六篇Scrapy-Redis详解

$
0
0

Scrapy-Redis 详解

通常我们在一个站站点进行采集的时候,如果是小站的话 我们使用scrapy本身就可以满足。

但是如果在面对一些比较大型的站点的时候,单个scrapy就显得力不从心了。

要是我们能够多个Scrapy一起采集该多好啊 人多力量大。

很遗憾Scrapy官方并不支持多个同时采集一个站点,虽然官方给出一个方法:

**将一个站点的分割成几部分 交给不同的scrapy去采集**

似乎是个解决办法,但是很麻烦诶!毕竟分割很麻烦的哇

下面就改轮到我们的额主角Scrapy-Redis登场了!

什么??你这么就登场了?还没说为什么呢?

好吧 为了简单起见 就用官方图来简单说明一下:

这张图大家相信大家都很熟悉了。重点看一下SCHEDULER

1. 先来看看官方对于SCHEDULER的定义:

**SCHEDULER接受来自Engine的Requests,并将它们放入队列(可以按顺序优先级),以便在之后将其提供给Engine**

点我看文档

2. 现在我们来看看SCHEDULER都提供了些什么功能:

根据官方文档说明 在我们没有没有指定 SCHEDULER 参数时,默认使用:’scrapy.core.scheduler.Scheduler’ 作为SCHEDULER(调度器)

scrapy.core.scheduler.py:

 

class Scheduler(object):

    def __init__(self, dupefilter, jobdir=None, dqclass=None, mqclass=None,
                 logunser=False, stats=None, pqclass=None):
        self.df = dupefilter
        self.dqdir = self._dqdir(jobdir)
        self.pqclass = pqclass
        self.dqclass = dqclass
        self.mqclass = mqclass
        self.logunser = logunser
        self.stats = stats
        # 注意在scrpy中优先注意这个方法,此方法是一个钩子 用于访问当前爬虫的配置
    @classmethod
    def from_crawler(cls, crawler):
        settings = crawler.settings
        # 获取去重用的类 默认:scrapy.dupefilters.RFPDupeFilter
        dupefilter_cls = load_object(settings['DUPEFILTER_CLASS'])
        # 对去重类进行配置from_settings 在 scrapy.dupefilters.RFPDupeFilter 43行
        # 这种调用方式对于IDE跳转不是很好  所以需要自己去找
        # @classmethod
        # def from_settings(cls, settings):
        #     debug = settings.getbool('DUPEFILTER_DEBUG')
        #     return cls(job_dir(settings), debug)
        # 上面就是from_settings方法 其实就是设置工作目录 和是否开启debug
        dupefilter = dupefilter_cls.from_settings(settings)
        # 获取优先级队列 类对象 默认:queuelib.pqueue.PriorityQueue
        pqclass = load_object(settings['SCHEDULER_PRIORITY_QUEUE'])
        # 获取磁盘队列 类对象(SCHEDULER使用磁盘存储 重启不会丢失)
        dqclass = load_object(settings['SCHEDULER_DISK_QUEUE'])
        # 获取内存队列 类对象(SCHEDULER使用内存存储 重启会丢失)
        mqclass = load_object(settings['SCHEDULER_MEMORY_QUEUE'])
        # 是否开启debug
        logunser = settings.getbool('LOG_UNSERIALIZABLE_REQUESTS', settings.getbool('SCHEDULER_DEBUG'))
        # 将这些参数传递给 __init__方法
        return cls(dupefilter, jobdir=job_dir(settings), logunser=logunser,
                   stats=crawler.stats, pqclass=pqclass, dqclass=dqclass, mqclass=mqclass)


    def has_pending_requests(self):
      """检查是否有没处理的请求"""
        return len(self) > 0

    def open(self, spider):
      """Engine创建完毕之后会调用这个方法"""
        self.spider = spider
        # 创建一个有优先级的内存队列 实例化对象
        # self.pqclass 默认是:queuelib.pqueue.PriorityQueue
        # self._newmq 会返回一个内存队列的 实例化对象 在110  111 行
        self.mqs = self.pqclass(self._newmq)
        # 如果self.dqdir 有设置 就创建一个磁盘队列 否则self.dqs 为空
        self.dqs = self._dq() if self.dqdir else None
        # 获得一个去重实例对象 open 方法是从BaseDupeFilter继承的
        # 现在我们可以用self.df来去重啦
        return self.df.open()

    def close(self, reason):
      """当然Engine关闭时"""
          # 如果有磁盘队列 则对其进行dump后保存到active.json文件中
        if self.dqs:
            prios = self.dqs.close()
            with open(join(self.dqdir, 'active.json'), 'w') as f:
                json.dump(prios, f)
        # 然后关闭去重
        return self.df.close(reason)

    def enqueue_request(self, request):
      """添加一个Requests进调度队列"""
          # self.df.request_seen是检查这个Request是否已经请求过了 如果有会返回True
        if not request.dont_filter and self.df.request_seen(request):
              # 如果Request的dont_filter属性没有设置(默认为False)和 已经存在则去重
            # 不push进队列
            self.df.log(request, self.spider)
            return False
        # 先尝试将Request push进磁盘队列
        dqok = self._dqpush(request)
        if dqok:
              # 如果成功 则在记录一次状态
            self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider)
        else:
              # 不能添加进磁盘队列则会添加进内存队列
            self._mqpush(request)
            self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider)
        self.stats.inc_value('scheduler/enqueued', spider=self.spider)
        return True

    def next_request(self):
      """从队列中获取一个Request"""
          # 优先从内存队列中获取
        request = self.mqs.pop()
        if request:
            self.stats.inc_value('scheduler/dequeued/memory', spider=self.spider)
        else:
              # 不能获取的时候从磁盘队列队里获取
            request = self._dqpop()
            if request:
                self.stats.inc_value('scheduler/dequeued/disk', spider=self.spider)
        if request:
            self.stats.inc_value('scheduler/dequeued', spider=self.spider)
        # 将获取的到Request返回给Engine
        return request

    def __len__(self):
        return len(self.dqs) + len(self.mqs) if self.dqs else len(self.mqs)

    def _dqpush(self, request):
        if self.dqs is None:
            return
        try:
            reqd = request_to_dict(request, self.spider)
            self.dqs.push(reqd, -request.priority)
        except ValueError as e:  # non serializable request
            if self.logunser:
                msg = ("Unable to serialize request: %(request)s - reason:"
                       " %(reason)s - no more unserializable requests will be"
                       " logged (stats being collected)")
                logger.warning(msg, {'request': request, 'reason': e},
                               exc_info=True, extra={'spider': self.spider})
                self.logunser = False
            self.stats.inc_value('scheduler/unserializable',
                                 spider=self.spider)
            return
        else:
            return True

    def _mqpush(self, request):
        self.mqs.push(request, -request.priority)

    def _dqpop(self):
        if self.dqs:
            d = self.dqs.pop()
            if d:
                return request_from_dict(d, self.spider)

    def _newmq(self, priority):
        return self.mqclass()

    def _newdq(self, priority):
        return self.dqclass(join(self.dqdir, 'p%s' % priority))

    def _dq(self):
        activef = join(self.dqdir, 'active.json')
        if exists(activef):
            with open(activef) as f:
                prios = json.load(f)
        else:
            prios = ()
        q = self.pqclass(self._newdq, startprios=prios)
        if q:
            logger.info("Resuming crawl (%(queuesize)d requests scheduled)",
                        {'queuesize': len(q)}, extra={'spider': self.spider})
        return q

    def _dqdir(self, jobdir):
        if jobdir:
            dqdir = join(jobdir, 'requests.queue')
            if not exists(dqdir):
                os.makedirs(dqdir)
            return dqdir

 

只挑了一些重点的写了一些注释剩下大家自己领会(才不是我懒哦 )

从上面的代码 我们可以很清楚的知道 SCHEDULER的主要是完成了 push Request pop Request 和 去重的操作。

而且queue 操作是在内存队列中完成的。

大家看queuelib.queue就会发现基于内存的(deque)

那么去重呢?

class RFPDupeFilter(BaseDupeFilter):
    """Request Fingerprint duplicates filter"""

    def __init__(self, path=None, debug=False):
        self.file = None
        self.fingerprints = set()
        self.logdupes = True
        self.debug = debug
        self.logger = logging.getLogger(__name__)
        if path:
              # 此处可以看到去重其实打开了一个名叫 requests.seen的文件
            # 如果是使用的磁盘的话
            self.file = open(os.path.join(path, 'requests.seen'), 'a+')
            self.file.seek(0)
            self.fingerprints.update(x.rstrip() for x in self.file)

    @classmethod
    def from_settings(cls, settings):
        debug = settings.getbool('DUPEFILTER_DEBUG')
        return cls(job_dir(settings), debug)

    def request_seen(self, request):
        fp = self.request_fingerprint(request)
        if fp in self.fingerprints:
              # 判断我们的请求是否在这个在集合中
            return True
        # 没有在集合就添加进去
        self.fingerprints.add(fp)
        # 如果用的磁盘队列就写进去记录一下
        if self.file:
            self.file.write(fp + os.linesep)

 

 

 

按照正常流程就是大家都会进行重复的采集;我们都知道进程之间内存中的数据不可共享的,那么你在开启多个Scrapy的时候,它们相互之间并不知道对方采集了些什么那些没有没采集。那就大家伙儿自己玩自己的了。完全没没有效率的提升啊!

怎么解决呢?

这就是我们Scrapy-Redis解决的问题了,不能协作不就是因为Request 和 去重这两个 不能共享吗?

那我把这两个独立出来好了。

将Scrapy中的SCHEDULER组件独立放到大家都能访问的地方不就OK啦!加上scrapy-redis后流程图就应该变成这样了👇

So············· 这样是不是看起来就清楚多了???

下面我们来看看Scrapy-Redis是怎么处理的👇

scrapy_redis.scheduler.py:

class Scheduler(object):
    """Redis-based scheduler

    Settings
    --------
    SCHEDULER_PERSIST : bool (default: False)
        Whether to persist or clear redis queue.
    SCHEDULER_FLUSH_ON_START : bool (default: False)
        Whether to flush redis queue on start.
    SCHEDULER_IDLE_BEFORE_CLOSE : int (default: 0)
        How many seconds to wait before closing if no message is received.
    SCHEDULER_QUEUE_KEY : str
        Scheduler redis key.
    SCHEDULER_QUEUE_CLASS : str
        Scheduler queue class.
    SCHEDULER_DUPEFILTER_KEY : str
        Scheduler dupefilter redis key.
    SCHEDULER_DUPEFILTER_CLASS : str
        Scheduler dupefilter class.
    SCHEDULER_SERIALIZER : str
        Scheduler serializer.

    """

    def __init__(self, server,
                 persist=False,
                 flush_on_start=False,
                 queue_key=defaults.SCHEDULER_QUEUE_KEY,
                 queue_cls=defaults.SCHEDULER_QUEUE_CLASS,
                 dupefilter_key=defaults.SCHEDULER_DUPEFILTER_KEY,
                 dupefilter_cls=defaults.SCHEDULER_DUPEFILTER_CLASS,
                 idle_before_close=0,
                 serializer=None):
        """Initialize scheduler.

        Parameters
        ----------
        server : Redis
            这是Redis实例
        persist : bool
            是否在关闭时清空Requests.默认值是False。
        flush_on_start : bool
            是否在启动时清空Requests。 默认值是False。
        queue_key : str
            Request队列的Key名字
        queue_cls : str
            队列的可导入路径(就是使用什么队列)
        dupefilter_key : str
            去重队列的Key
        dupefilter_cls : str
            去重类的可导入路径。
        idle_before_close : int
            等待多久关闭

        """
        if idle_before_close < 0:
            raise TypeError("idle_before_close cannot be negative")

        self.server = server
        self.persist = persist
        self.flush_on_start = flush_on_start
        self.queue_key = queue_key
        self.queue_cls = queue_cls
        self.dupefilter_cls = dupefilter_cls
        self.dupefilter_key = dupefilter_key
        self.idle_before_close = idle_before_close
        self.serializer = serializer
        self.stats = None

    def __len__(self):
        return len(self.queue)

    @classmethod
    def from_settings(cls, settings):
        kwargs = {
            'persist': settings.getbool('SCHEDULER_PERSIST'),
            'flush_on_start': settings.getbool('SCHEDULER_FLUSH_ON_START'),
            'idle_before_close': settings.getint('SCHEDULER_IDLE_BEFORE_CLOSE'),
        }

        # If these values are missing, it means we want to use the defaults.
        optional = {
            # TODO: Use custom prefixes for this settings to note that are
            # specific to scrapy-redis.
            'queue_key': 'SCHEDULER_QUEUE_KEY',
            'queue_cls': 'SCHEDULER_QUEUE_CLASS',
            'dupefilter_key': 'SCHEDULER_DUPEFILTER_KEY',
            # We use the default setting name to keep compatibility.
            'dupefilter_cls': 'DUPEFILTER_CLASS',
            'serializer': 'SCHEDULER_SERIALIZER',
        }
        # 从setting中获取配置组装成dict(具体获取那些配置是optional字典中key)
        for name, setting_name in optional.items():
            val = settings.get(setting_name)
            if val:
                kwargs[name] = val

        # Support serializer as a path to a module.
        if isinstance(kwargs.get('serializer'), six.string_types):
            kwargs['serializer'] = importlib.import_module(kwargs['serializer'])
                # 或得一个Redis连接
        server = connection.from_settings(settings)
        # Ensure the connection is working.
        server.ping()

        return cls(server=server, **kwargs)

    @classmethod
    def from_crawler(cls, crawler):
        instance = cls.from_settings(crawler.settings)
        # FIXME: for now, stats are only supported from this constructor
        instance.stats = crawler.stats
        return instance

    def open(self, spider):
        self.spider = spider

        try:
              # 根据self.queue_cls这个可以导入的类 实例化一个队列
            self.queue = load_object(self.queue_cls)(
                server=self.server,
                spider=spider,
                key=self.queue_key % {'spider': spider.name},
                serializer=self.serializer,
            )
        except TypeError as e:
            raise ValueError("Failed to instantiate queue class '%s': %s",
                             self.queue_cls, e)

        try:
              # 根据self.dupefilter_cls这个可以导入的类 实例一个去重集合
            # 默认是集合 可以实现自己的去重方式 比如 bool 去重
            self.df = load_object(self.dupefilter_cls)(
                server=self.server,
                key=self.dupefilter_key % {'spider': spider.name},
                debug=spider.settings.getbool('DUPEFILTER_DEBUG'),
            )
        except TypeError as e:
            raise ValueError("Failed to instantiate dupefilter class '%s': %s",
                             self.dupefilter_cls, e)

        if self.flush_on_start:
            self.flush()
        # notice if there are requests already in the queue to resume the crawl
        if len(self.queue):
            spider.log("Resuming crawl (%d requests scheduled)" % len(self.queue))

    def close(self, reason):
        if not self.persist:
            self.flush()

    def flush(self):
        self.df.clear()
        self.queue.clear()

    def enqueue_request(self, request):
      """这个和Scrapy本身的一样"""
        if not request.dont_filter and self.df.request_seen(request):
            self.df.log(request, self.spider)
            return False
        if self.stats:
            self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
        # 向队列里面添加一个Request
        self.queue.push(request)
        return True

    def next_request(self):
      """获取一个Request"""
        block_pop_timeout = self.idle_before_close
        # block_pop_timeout 是一个等待参数 队列没有东西会等待这个时间  超时就会关闭
        request = self.queue.pop(block_pop_timeout)
        if request and self.stats:
            self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
        return request

    def has_pending_requests(self):
        return len(self) > 0

 

来先来看看

 

以上就是Scrapy-Redis中的SCHEDULER模块。下面我们来看看queue和本身的什么不同:

scrapy_redis.queue.py

以最常用的优先级队列 PriorityQueue 举例:

 

class PriorityQueue(Base):
    """Per-spider priority queue abstraction using redis' sorted set"""
        """其实就是使用Redis的有序集合 来对Request进行排序,这样就可以优先级高的在有序集合的顶层 我们只需要"""
    """从上往下依次获取Request即可"""
    def __len__(self):
        """Return the length of the queue"""
        return self.server.zcard(self.key)

    def push(self, request):
        """Push a request"""
        """添加一个Request进队列"""
        # self._encode_request 将Request请求进行序列化
        data = self._encode_request(request)
        """
        d = {
        'url': to_unicode(request.url),  # urls should be safe (safe_string_url)
        'callback': cb,
        'errback': eb,
        'method': request.method,
        'headers': dict(request.headers),
        'body': request.body,
        'cookies': request.cookies,
        'meta': request.meta,
        '_encoding': request._encoding,
        'priority': request.priority,
        'dont_filter': request.dont_filter,
        'flags': request.flags,
        '_class': request.__module__ + '.' + request.__class__.__name__
            }

        data就是上面这个字典的序列化
        在Scrapy.utils.reqser.py 中的request_to_dict方法中处理
        """

        # 在Redis有序集合中数值越小优先级越高(就是会被放在顶层)所以这个位置是取得 相反数
        score = -request.priority
        # We don't use zadd method as the order of arguments change depending on
        # whether the class is Redis or StrictRedis, and the option of using
        # kwargs only accepts strings, not bytes.
        # ZADD 是添加进有序集合
        self.server.execute_command('ZADD', self.key, score, data)

    def pop(self, timeout=0):
        """
        Pop a request
        timeout not support in this queue class
        有序集合不支持超时所以就木有使用timeout了  这个timeout就是挂羊头卖狗肉
        """
        """从有序集合中取出一个Request"""
        # use atomic range/remove using multi/exec
        """使用multi的原因是为了将获取Request和删除Request合并成一个操作(原子性的)在获取到一个元素之后 删除它,因为有序集合 不像list 有pop 这种方式啊"""
        pipe = self.server.pipeline()
        pipe.multi()
        # 取出 顶层第一个
        # zrange :返回有序集 key 中,指定区间内的成员。0,0 就是第一个了
        # zremrangebyrank:移除有序集 key 中,指定排名(rank)区间内的所有成员 0,0也就是第一个了
        # 更多请参考Redis官方文档
        pipe.zrange(self.key, 0, 0).zremrangebyrank(self.key, 0, 0)
        results, count = pipe.execute()
        if results:
            return self._decode_request(results[0])

 

以上就是SCHEDULER在处理Request的时候做的操作了。

是时候来看看SCHEDULER是怎么处理去重的了!

只需要注意这个👇方法即可:

def request_seen(self, request):
  """Returns True if request was already seen.

        Parameters
        ----------
        request : scrapy.http.Request

        Returns
        -------
        bool

        """
  # 通过self.request_fingerprint 会生一个sha1的指纹
  fp = self.request_fingerprint(request)
  # This returns the number of values added, zero if already exists.
  # 添加进一个Redis集合如果self.key这个集合中存在fp这个指纹会返回1  不存在返回0
  added = self.server.sadd(self.key, fp)
  return added == 0

 

 

这样大家就都可以访问同一个Redis 获取同一个spider的Request 在同一个位置去重,就不用担心重复啦

大概就像这样:

  1. spider1:检查一下这个Request是否在Redis去重,如果在就证明其它的spider采集过啦!如果不在就添加进调度队列,等待别 人获取。自己继续干活抓取网页 产生新的Request了 重复之前步骤。
  2. spider2:以相同的逻辑执行

可能有些小伙儿会产生疑问了~~!spider2拿到了别人的Request了 怎么能正确的执行呢?逻辑不会错吗?

这个不用担心啦 因为整Request当中包含了,所有的逻辑,回去看看上面那个序列化的字典。

总结一下:

  1. 1. Scrapy-Reids 就是将Scrapy原本在内存中处理的 调度(就是一个队列Queue)、去重、这两个操作通过Redis来实现
  2. 多个Scrapy在采集同一个站点时会使用相同的redis key(可以理解为队列)添加Request 获取Request 去重Request,这样所有的spider不会进行重复采集。效率自然就嗖嗖的上去了。
  3. 3. Redis是原子性的,好处不言而喻(一个Request要么被处理 要么没被处理,不存在第三可能)

另外Scrapy-Redis本身不支持Redis-Cluster,大量网站去重的话会给单机很大的压力(就算使用boolfilter 内存也不够整啊!)

改造方式很简单:

  1.  使用 **rediscluster** 这个包替换掉本身的Redis连接
  2. Redis-Cluster 不支持事务,可以使用lua脚本进行代替(lua脚本是原子性的哦)
  3. **注意使用lua脚本 不能写占用时间很长的操作**(毕竟一大群人等着操作Redis 你总不能让人家等着吧)

以上!完毕

对于懒人小伙伴儿 看看这个我改好的: 集群版Scrapy-Redis **PS: 支持Python3.6+ 哦 ! 其余的版本没测试过**

转载请注明:静觅 » 小白进阶之Scrapy第六篇Scrapy-Redis详解

自然语言处理中句子相似度计算的几种方法

$
0
0

在做自然语言处理的过程中,我们经常会遇到需要找出相似语句的场景,或者找出句子的近似表达,这时候我们就需要把类似的句子归到一起,这里面就涉及到句子相似度计算的问题,那么本节就来了解一下怎么样来用 Python 实现句子相似度的计算。

基本方法

句子相似度计算我们一共归类了以下几种方法:

  • 编辑距离计算
  • 杰卡德系数计算
  • TF 计算
  • TFIDF 计算
  • Word2Vec 计算

下面我们来一一了解一下这几种算法的原理和 Python 实现。

编辑距离计算

编辑距离,英文叫做 Edit Distance,又称 Levenshtein 距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数,如果它们的距离越大,说明它们越是不同。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

例如我们有两个字符串:string 和 setting,如果我们想要把 string 转化为 setting,需要这么两步:

  • 第一步,在 s 和 t 之间加入字符 e。
  • 第二步,把 r 替换成 t。

所以它们的编辑距离差就是 2,这就对应着二者要进行转化所要改变(添加、替换、删除)的最小步数。

那么用 Python 怎样来实现呢,我们可以直接使用 distance 库:

import distance

def edit_distance(s1, s2):
    return distance.levenshtein(s1, s2)

s1 = 'string'
s2 = 'setting'
print(edit_distance(s1, s2))

这里我们直接使用 distance 库的 levenshtein() 方法,传入两个字符串,即可获取两个字符串的编辑距离了。

运行结果如下:

2

这里的 distance 库我们可以直接使用 pip3 来安装:

pip3 install distance

这样如果我们想要获取相似的文本的话可以直接设定一个编辑距离的阈值来实现,如设置编辑距离为 2,下面是一个样例:

import distance

def edit_distance(s1, s2):
    return distance.levenshtein(s1, s2)

strings = [
    '你在干什么',
    '你在干啥子',
    '你在做什么',
    '你好啊',
    '我喜欢吃香蕉'
]

target = '你在干啥'
results = list(filter(lambda x: edit_distance(x, target) <= 2, strings))
print(results)

这里我们定义了一些字符串,然后定义了一个目标字符串,然后用编辑距离 2 的阈值进行设定,最后得到的结果就是编辑距离在 2 及以内的结果,运行结果如下:

['你在干什么', '你在干啥子']

通过这种方式我们可以大致筛选出类似的句子,但是发现一些句子例如“你在做什么” 就没有被识别出来,但他们的意义确实是相差不大的,因此,编辑距离并不是一个好的方式,但是简单易用。

杰卡德系数计算

杰卡德系数,英文叫做 Jaccard index, 又称为 Jaccard 相似系数,用于比较有限样本集之间的相似性与差异性。Jaccard 系数值越大,样本相似度越高。

实际上它的计算方式非常简单,就是两个样本的交集除以并集得到的数值,当两个样本完全一致时,结果为 1,当两个样本完全不同时,结果为 0。

算法非常简单,就是交集除以并集,下面我们用 Python 代码来实现一下:

from sklearn.feature_extraction.text import CountVectorizer
import numpy as np


def jaccard_similarity(s1, s2):
    def add_space(s):
        return ' '.join(list(s))
    
    # 将字中间加入空格
    s1, s2 = add_space(s1), add_space(s2)
    # 转化为TF矩阵
    cv = CountVectorizer(tokenizer=lambda s: s.split())
    corpus = [s1, s2]
    vectors = cv.fit_transform(corpus).toarray()
    # 求交集
    numerator = np.sum(np.min(vectors, axis=0))
    # 求并集
    denominator = np.sum(np.max(vectors, axis=0))
    # 计算杰卡德系数
    return 1.0 * numerator / denominator


s1 = '你在干嘛呢'
s2 = '你在干什么呢'
print(jaccard_similarity(s1, s2))

这里我们使用了 Sklearn 库中的 CountVectorizer 来计算句子的 TF 矩阵,然后利用 Numpy 来计算二者的交集和并集,随后计算杰卡德系数。

这里值得学习的有 CountVectorizer 的用法,通过它的 fit_transform() 方法我们可以将字符串转化为词频矩阵,例如这里有两句话“你在干嘛呢”和“你在干什么呢”,首先 CountVectorizer 会计算出不重复的有哪些字,会得到一个字的列表,结果为:

['么', '什', '你', '呢', '嘛', '在', '干']

这个其实可以通过如下代码来获取,就是获取词表内容:

cv.get_feature_names()

接下来通过转化之后,vectors 变量就变成了:

[[0 0 1 1 1 1 1]
 [1 1 1 1 0 1 1]]

它对应的是两个句子对应词表的词频统计,这里是两个句子,所以结果是一个长度为 2 的二维数组,比如第一句话“你在干嘛呢”中不包含“么”字,那么第一个“么”字对应的结果就是0,即数量为 0,依次类推。

后面我们使用了 np.min() 方法并传入了 axis 为 0,实际上就是获取了每一列的最小值,这样实际上就是取了交集,np.max() 方法是获取了每一列的最大值,实际上就是取了并集。

二者分别取和即是交集大小和并集大小,然后作商即可,结果如下:

0.5714285714285714

这个数值越大,代表两个字符串越接近,否则反之,因此我们也可以使用这个方法,并通过设置一个相似度阈值来进行筛选。

TF 计算

第三种方案就是直接计算 TF 矩阵中两个向量的相似度了,实际上就是求解两个向量夹角的余弦值,就是点乘积除以二者的模长,公式如下:

cosθ=a·b/|a|*|b|

上面我们已经获得了 TF 矩阵,下面我们只需要求解两个向量夹角的余弦值就好了,代码如下:

from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
from scipy.linalg import norm

def tf_similarity(s1, s2):
    def add_space(s):
        return ' '.join(list(s))
    
    # 将字中间加入空格
    s1, s2 = add_space(s1), add_space(s2)
    # 转化为TF矩阵
    cv = CountVectorizer(tokenizer=lambda s: s.split())
    corpus = [s1, s2]
    vectors = cv.fit_transform(corpus).toarray()
    # 计算TF系数
    return np.dot(vectors[0], vectors[1]) / (norm(vectors[0]) * norm(vectors[1]))


s1 = '你在干嘛呢'
s2 = '你在干什么呢'
print(tf_similarity(s1, s2))

在在这里我们使用了 np.dot() 方法获取了向量的点乘积,然后通过 norm() 方法获取了向量的模长,经过计算得到二者的 TF 系数,结果如下:

0.7302967433402214

TFIDF 计算

另外除了计算 TF 系数我们还可以计算 TFIDF 系数,TFIDF 实际上就是在词频 TF 的基础上再加入 IDF 的信息,IDF 称为逆文档频率,不了解的可以看下阮一峰老师的讲解:http://www.ruanyifeng.com/blog/2013/03/tf-idf.html,里面对 TFIDF 的讲解也是十分透彻的。

下面我们还是借助于 Sklearn 中的模块 TfidfVectorizer 来实现,代码如下:

from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np
from scipy.linalg import norm


def tfidf_similarity(s1, s2):
    def add_space(s):
        return ' '.join(list(s))
    
    # 将字中间加入空格
    s1, s2 = add_space(s1), add_space(s2)
    # 转化为TF矩阵
    cv = TfidfVectorizer(tokenizer=lambda s: s.split())
    corpus = [s1, s2]
    vectors = cv.fit_transform(corpus).toarray()
    # 计算TF系数
    return np.dot(vectors[0], vectors[1]) / (norm(vectors[0]) * norm(vectors[1]))


s1 = '你在干嘛呢'
s2 = '你在干什么呢'
print(tfidf_similarity(s1, s2))

这里的 vectors 变量实际上就对应着 TFIDF 值,内容如下:

[[0.         0.         0.4090901  0.4090901  0.57496187 0.4090901 0.4090901 ]
 [0.49844628 0.49844628 0.35464863 0.35464863 0.  0.35464863 0.35464863]]

运行结果如下:

0.5803329846765686

所以通过 TFIDF 系数我们也可以进行相似度的计算。

Word2Vec 计算

Word2Vec,顾名思义,其实就是将每一个词转换为向量的过程。如果不了解的话可以参考:https://blog.csdn.net/itplus/article/details/37969519

这里我们可以直接下载训练好的 Word2Vec 模型,模型的链接地址为:https://pan.baidu.com/s/1TZ8GII0CEX32ydjsfMc0zw,是使用新闻、百度百科、小说数据来训练的 64 维的 Word2Vec 模型,数据量很大,整体效果还不错,我们可以直接下载下来使用,这里我们使用的是 news_12g_baidubaike_20g_novel_90g_embedding_64.bin 数据,然后实现 Sentence2Vec,代码如下:

import gensim
import jieba
import numpy as np
from scipy.linalg import norm

model_file = './word2vec/news_12g_baidubaike_20g_novel_90g_embedding_64.bin'
model = gensim.models.KeyedVectors.load_word2vec_format(model_file, binary=True)

def vector_similarity(s1, s2):
    def sentence_vector(s):
        words = jieba.lcut(s)
        v = np.zeros(64)
        for word in words:
            v += model[word]
        v /= len(words)
        return v
    
    v1, v2 = sentence_vector(s1), sentence_vector(s2)
    return np.dot(v1, v2) / (norm(v1) * norm(v2))

在获取 Sentence Vector 的时候,我们首先对句子进行分词,然后对分好的每一个词获取其对应的 Vector,然后将所有 Vector 相加并求平均,这样就可得到 Sentence Vector 了,然后再计算其夹角余弦值即可。

调用示例如下:

s1 = '你在干嘛'
s2 = '你正做什么'
vector_similarity(s1, s2)

结果如下:

0.6701133967824016

这时如果我们再回到最初的例子看下效果:

strings = [
    '你在干什么',
    '你在干啥子',
    '你在做什么',
    '你好啊',
    '我喜欢吃香蕉'
]

target = '你在干啥'

for string in strings:
    print(string, vector_similarity(string, target))

依然是前面的例子,我们看下它们的匹配度结果是多少,运行结果如下:

你在干什么 0.8785495016487204
你在干啥子 0.9789649689827049
你在做什么 0.8781992402695274
你好啊 0.5174225914249863
我喜欢吃香蕉 0.582990841450621

可以看到相近的语句相似度都能到 0.8 以上,而不同的句子相似度都不足 0.6,这个区分度就非常大了,可以说有了 Word2Vec 我们可以结合一些语义信息来进行一些判断,效果明显也好很多。

所以总体来说,Word2Vec 计算的方式是非常好的。

另外学术界还有一些可能更好的研究成果,这个可以参考知乎上的一些回答:https://www.zhihu.com/question/29978268/answer/54399062

以上便是进行句子相似度计算的基本方法和 Python 实现,本节代码地址:https://github.com/AIDeepLearning/SentenceDistance

转载请注明:静觅 » 自然语言处理中句子相似度计算的几种方法

《Python3网络爬虫开发实战》第二波抽奖赠书活动来了!

$
0
0

嗨~ 给大家重磅推荐一本书!上市两月就已经重印 4 次的 Python 爬虫书!它就是由静觅博客博主崔庆才所作的《Python3网络爬虫开发实战》!!!同时文末还有抽奖赠书活动,不容错过!!!

书籍介绍

本书《Python3网络爬虫开发实战》全面介绍了利用 Python3 开发网络爬虫的知识,书中首先详细介绍了各种类型的环境配置过程和爬虫基础知识,还讨论了 urllib、requests 等请求库和 Beautiful Soup、XPath、pyquery 等解析库以及文本和各类数据库的存储方法,另外本书通过多个真实新鲜案例介绍了分析 Ajax 进行数据爬取,Selenium 和 Splash 进行动态网站爬取的过程,接着又分享了一些切实可行的爬虫技巧,比如使用代理爬取和维护动态代理池的方法、ADSL 拨号代理的使用、各类验证码(图形、极验、点触、宫格等)的破解方法、模拟登录网站爬取的方法及 Cookies 池的维护等等。

此外,本书的内容还远远不止这些,作者还结合移动互联网的特点探讨了使用 Charles、mitmdump、Appium 等多种工具实现 App 抓包分析、加密参数接口爬取、微信朋友圈爬取的方法。此外本书还详细介绍了 pyspider 框架、Scrapy 框架的使用和分布式爬虫的知识,另外对于优化及部署工作,本书还包括 Bloom Filter 效率优化、Docker 和 Scrapyd 爬虫部署、分布式爬虫管理框架Gerapy 的分享。

全书共 604 页,足足两斤重呢~ 定价为 99 元!

作者介绍

看书就先看看谁写的嘛,我们来了解一下~

崔庆才,静觅博客博主(https://cuiqingcai.com),博客 Python 爬虫博文阅读量已过百万,北京航空航天大学硕士,天善智能、网易云课堂讲师,微软小冰大数据工程师,有多个大型分布式爬虫项目经验,乐于技术分享,文章通俗易懂 ^_^

附皂片一张 ~(@^_^@)~

图文介绍

呕心沥血设计的宣传图也得放一下~

专家评论

书是好是坏,得让专家看评一评呀,那么下面就是几位专家的精彩评论,快来看看吧~

在互联网软件开发工程师的分类中,爬虫工程师是非常重要的。爬虫工作往往是一个公司核心业务开展的基础,数据抓取下来,才有后续的加工处理和最终展现。此时数据的抓取规模、稳定性、实时性、准确性就显得非常重要。早期的互联网充分开放互联,数据获取的难度很小。随着各大公司对数据资产日益看重,反爬水平也在不断提高,各种新技术不断给爬虫软件提出新的课题。本书作者对爬虫的各个领域都有深刻研究,书中探讨了Ajax数据的抓取、动态渲染页面的抓取、验证码识别、模拟登录等高级话题,同时也结合移动互联网的特点探讨了App的抓取等。更重要的是,本书提供了大量源码,可以帮助读者更好地理解相关内容。强烈推荐给各位技术爱好者阅读!

——梁斌,八友科技总经理

数据既是当今大数据分析的前提,也是各种人工智能应用场景的基础。得数据者得天下,会爬虫者走遍天下也不怕!一册在手,让小白到老司机都能有所收获!

——李舟军,北京航空航天大学教授,博士生导师

本书从爬虫入门到分布式抓取,详细介绍了爬虫技术的各个要点,并针对不同的场景提出了对应的解决方案。另外,书中通过大量的实例来帮助读者更好地学习爬虫技术,通俗易懂,干货满满。强烈推荐给大家!

——宋睿华,微软小冰首席科学家

有人说中国互联网的带宽全给各种爬虫占据了,这说明网络爬虫的重要性以及中国互联网数据封闭垄断的现状。爬是一种能力,爬是为了不爬。

——施水才,北京拓尔思信息技术股份有限公司总裁

全书目录

书的目录也有~ 看这里!

  • 1-开发环境配置
  • 1.1-Python3的安装
  • 1.2-请求库的安装
  • 1.3-解析库的安装
  • 1.4-数据库的安装
  • 1.5-存储库的安装
  • 1.6-Web库的安装
  • 1.7-App爬取相关库的安装
  • 1.8-爬虫框架的安装
  • 1.9-部署相关库的安装
  • 2-爬虫基础
  • 2.1-HTTP基本原理
  • 2.2-网页基础
  • 2.3-爬虫的基本原理
  • 2.4-会话和Cookies
  • 2.5-代理的基本原理
  • 3-基本库的使用
  • 3.1-使用urllib
  • 3.1.1-发送请求
  • 3.1.2-处理异常
  • 3.1.3-解析链接
  • 3.1.4-分析Robots协议
  • 3.2-使用requests
  • 3.2.1-基本用法
  • 3.2.2-高级用法
  • 3.3-正则表达式
  • 3.4-抓取猫眼电影排行
  • 4-解析库的使用
  • 4.1-使用XPath
  • 4.2-使用Beautiful Soup
  • 4.3-使用pyquery
  • 5-数据存储
  • 5.1-文件存储
  • 5.1.1-TXT文本存储
  • 5.1.2-JSON文件存储
  • 5.1.3-CSV文件存储
  • 5.2-关系型数据库存储
  • 5.2.1-MySQL存储
  • 5.3-非关系型数据库存储
  • 5.3.1-MongoDB存储
  • 5.3.2-Redis存储
  • 6-Ajax数据爬取
  • 6.1-什么是Ajax
  • 6.2-Ajax分析方法
  • 6.3-Ajax结果提取
  • 6.4-分析Ajax爬取今日头条街拍美图
  • 7-动态渲染页面爬取
  • 7.1-Selenium的使用
  • 7.2-Splash的使用
  • 7.3-Splash负载均衡配置
  • 7.4-使用Selenium爬取淘宝商品
  • 8-验证码的识别
  • 8.1-图形验证码的识别
  • 8.2-极验滑动验证码的识别
  • 8.3-点触验证码的识别
  • 8.4-微博宫格验证码的识别
  • 9-代理的使用
  • 9.1-代理的设置
  • 9.2-代理池的维护
  • 9.3-付费代理的使用
  • 9.4-ADSL拨号代理
  • 9.5-使用代理爬取微信公众号文章
  • 10-模拟登录
  • 10.1-模拟登录并爬取GitHub
  • 10.2-Cookies池的搭建
  • 11-App的爬取
  • 11.1-Charles的使用
  • 11.2-mitmproxy的使用
  • 11.3-mitmdump爬取“得到”App电子书信息
  • 11.4-Appium的基本使用
  • 11.5-Appium爬取微信朋友圈
  • 11.6-Appium+mitmdump爬取京东商品
  • 12-pyspider框架的使用
  • 12.1-pyspider框架介绍
  • 12.2-pyspider的基本使用
  • 12.3-pyspider用法详解
  • 13-Scrapy框架的使用
  • 13.1-Scrapy框架介绍
  • 13.2-Scrapy入门
  • 13.3-Selector的用法
  • 13.4-Spider的用法
  • 13.5-Downloader Middleware的用法
  • 13.6-Spider Middleware的用法
  • 13.7-Item Pipeline的用法
  • 13.8-Scrapy对接Selenium
  • 13.9-Scrapy对接Splash
  • 13.10-Scrapy通用爬虫
  • 13.11-Scrapyrt的使用
  • 13.12-Scrapy对接Docker
  • 13.13-Scrapy爬取新浪微博
  • 14-分布式爬虫
  • 14.1-分布式爬虫原理
  • 14.2-Scrapy-Redis源码解析
  • 14.3-Scrapy分布式实现
  • 14.4-Bloom Filter的对接
  • 15-分布式爬虫的部署
  • 15.1-Scrapyd分布式部署
  • 15.2-Scrapyd-Client的使用
  • 15.3-Scrapyd对接Docker
  • 15.4-Scrapyd批量部署
  • 15.5-Gerapy分布式管理

购买链接

想必很多小伙伴已经等了很久了,之前预售那么久也一直迟迟没有货,发售就有不少网店又售空了,不过现在起不用担心了!

书籍现已在京东、天猫、当当等网店上架并全面供应啦,复制链接到浏览器打开或扫描二维码打开即可购买了!

 京东商城

https://item.jd.com/12333540.html

 天猫商城

https://detail.tmall.com/item.htm?id=566699703917

 当当网

http://product.dangdang.com/25249602.html

欢迎大家购买,O(∩_∩)O

免费预览

不放心?想先看看有些啥,没问题!看这里:

免费章节试读(复制粘贴至浏览器打开):

将一直免费开放前7章节,欢迎大家试读!

好了,接下来就是我们的福利环节啦~

福利一:抽奖送书!!!

恭喜你看到这里了!那么接下来的福利时间就到了!后面还有两个福利不容错过哦~

抽奖送书活动第二波来袭(后面还有很多波哦),公众号抽奖送 30 本作者亲笔签名书籍!!!

活动流程(重要,请一定认真阅读):

公众号进击的Coder回复 “抽奖” 获取抽奖码,2018.6.24 22:00 截止,逾期参与无效,请记住您的抽奖码,活动结束后会从参与活动的小伙伴中根据幸运值按照权重比例抽取 30 位并在微信公众号公布,届时请关注公众号抽奖结果的公布!获奖的小伙伴会获得作者亲笔签名的《Python3网络爬虫开发实战》一本。

福利二:独家优惠!!!

等等,你以为这就是全部福利吗?当然不是!除了抽奖送书,我们还拿到了拨号VPS知名品牌云立方的独家优惠,在公众号(进击的Coder )中回复:“优惠券”,即可免费领取云立方50元主机优惠券,数量有限,先到先得!优惠券可在云立方官网(www.yunlifang.cn)购买动态IP拨号VPS时抵扣现金,有了它,爬虫代理易如反掌!

你问我动态拨号VPS能做什么?应该怎么用在爬虫里?来这里了解一下:

轻松获得海量稳定代理!ADSL拨号代理的搭建

福利三:视频课程!!!

当然除了书籍,也有配套的视频课程,作者同样是崔庆才,二者结合学习效果更佳!限时优惠折扣中!扫描下图中二维码即可了解详情!

最后也是最重要的就是参与活动的地址了!!!快来扫码回复领取属于你的福利吧!!!

特别致谢

最后特别感谢云立方、天善智能对本活动的大力支持!

 

转载请注明:静觅 » 《Python3网络爬虫开发实战》第二波抽奖赠书活动来了!

推荐一些Mac上比较好用的软件

$
0
0

最近有一个朋友刚入手了 Mac,准备专门搞开发用,让我给他推荐几款软件,然后我就把我的 Launchpad 截图发给了他,他看到这密密麻麻的软件完全不知所措。

于是乎,我就大略整理了一些我比较推荐的几款软件,同时分享给大家,希望对大家有所帮助!

下面的一些软件都是我个人比较喜欢的,其实还有很多其他的恕不能一一列举了,如果大家有其他推荐的欢迎留言给我,谢谢!

日常工具

一些日常工具在这里我就不一一列举了,大部分使用 Mac 的小伙伴都会安装,比如 QQ、微信、Chrome 浏览器、网易云音乐、迅雷等等,这些在 Windows 上也几乎都是必备软件,这里就不再展开说明了。

效率工具

效率工具顾名思义,可以方便和简化 Mac 的操作,提高生产工作效率的工具,下面推荐几款我比较常用的。

Alfred

首推 Alfred,可以说是 Mac 必备软件,利用它我们可以快速地进行各种操作,大幅提高工作效率,如快速打开某个软件、快速打开某个链接、快速搜索某个文档,快速定位某个文件,快速查看本机 IP,快速定义某个色值,几乎你能想到的都能对接实现。

这些快速功能是怎么实现的呢?实际上是 Alfred 对接了很多 Workflow,我们可以使用 Workflow 方便地进行功能扩展,一些比较优秀的 Workflow 已经有人专门做过整理了,可以参见:https://github.com/zenorocha/alfred-workflows

推荐指数:★★★★★

Todoist

大家肯定也在使用各种 Todo List 的软件,这种软件其实也是五花八门,经过我本人试用,我觉得 Todoist 这款软件是最方便的。

它支持各种类型的任务定制,还可以设置分组、优先级、Deadline、执行人员、提醒、协作、效率统计等功能。另外它的各个平台支持真是异常地全啊,网页、PC、移动端就不用说了,都必须有的,另外它还有浏览器插件版、电邮版、可穿戴设备(如 Apple Watch、Google Wear)版,另外他还可以和 Mac 的日历事件进行同步,日历添加的事件也会自动添加到 Todoist 里面,非常方便,是目前我体验过的最好用的一款。

这款软件个人推荐购买专业版解锁全部功能,一个月 3 刀,但个人觉得确实非常值。

推荐指数:★★★★☆

Paste

Mac 上默认只有一个粘贴板,当我们新复制了一段文字之后,如果我们想再找寻之前复制的历史记录就找不到了,这其实是很反人类的。

好在 Paste 这款软件帮我们解决了这个问题,它可以保存我们粘贴板的历史记录,等需要粘贴某个内容的时候只需要呼出 Paste 历史粘贴板,然后选择某个特定的内容粘贴就好了,另外它还支持文本格式调整粘贴板分类和搜索,还可以支持快速便捷粘贴。有了它,妈妈再也不用担心我的粘贴板丢失了!

推荐指数:★★★★★

Synergy

工作时我会使用公司的台式机,是 Windows 系统,另外自己的个人笔记本 Mac 也会放在旁边,两台 PC 有时候会交替使用,但是我总不能配两套键盘和鼠标吧,这样就显得累赘了,而且也没那么多地方放啊。

有了 Synergy,我们可以将两台 PC 关联,实现键盘鼠标共享。我们可以使用一套键盘和鼠标来操作两台 PC,注意这是两个完全独立的 PC,各自有各自的屏幕和系统,使用 Synergy 我们可以做到一套键鼠同时控制两台电脑,鼠标可以直接从一台电脑的屏幕滑动到另一台电脑屏幕上,同时键盘、粘贴板也都是共享的。

设想这么个情景,我在我的台式机 Windows 上打开了一个页面,需要让我输入一个很长的序列号,而这个序列号又恰巧存在 Mac 上,这时如果有了 Synergy 将二者关联,我们只需要把鼠标从 Windows 的屏幕上直接滑动到 Mac 的屏幕上,选中序列号,然后键盘按下复制的快捷键,然后再把鼠标移回 Windows,粘贴即可,一气呵成。而不必再想办法发消息传输了,大大提高效率。

推荐指数:★★★★

Feedly、Reeder

博客现在已经越来越多了,越来越多的人开始在博客上发表文章,而当我们遇到优质的博客时,我们还想随时知道博客的发表动态,一旦有新文章发表我们想立马得到相关动态,这样可以实现吗?

肯定是可行的,现在绝大多数博客都有 RSS 订阅功能,有了它我们可以订阅自己喜欢的博客,这里我使用的 RSS 订阅工具就是 Feedly,利用它我可以很轻松地添加自己喜欢的博客或论坛到自己的 Feed 流里面,一旦有文章更新,我就会收到相应提示。

但是 Feedly 有个小问题,就是在国内速度太慢了,所以我又使用 Reeder 将 Feedly 里面的 Feed 流做了转接,它可以添加 Feedly 源,并带有灵活的分类、标记等管理功能,还支持各种预览方式,还支持存储到 Pocket,还有各种分享方式,功能十分齐全。

总之,推荐 Feedly 来添加自己喜欢的博客,用 Reeder 来阅读订阅的内容,双剑合璧,另外 Reeder 对移动版的支持也很不错,可以体验一下。

推荐指数:★★★★

Mindnode

有时候在思考问题的时候我们想要把一些思路记录下来,另外在做一些概要设计的时候需要把概要图大体描述出来,这时候画一个思维导图再合适不过了,比如你现在读的这篇文章就很适合用一个思维导图画一下。

画思维导图我个人比较喜欢的一款软件是 Mindnode,觉得比较简洁好用,当然也有不少人使用 XMind,也很不错。可能是先入为主,也可能是界面设计风格,我个人更加偏向于使用 Mindnode。

推荐指数:★★★★

1Password

随着年龄的增长,我们可能变得越来越忘事了。另外还有些反人类的网站密码必须要至少大写、小写、数字、特殊符号,有的还要求不少于多少位,有的还要求我么能定时更换密码,还不能与之前用过的相同!这会使得我们之前预想设计的很多密码都没法用了。另外网站又这么多,谁又能把网站的密码都记下来啊?

这时候我们就需要一款专门管理密码的软件,我个人推荐一款叫做 1Password,有了它我们可以将各个平台的密码保存起来,同时它还可以根据我们的要求帮我们随机生成一些密码并保存,这对注册一些新网站非常有用,同时使用随机的密码还降低了撞库的风险,不然一个平台的密码被盗了,其他平台用的同样的密码的话,就很不安全了。

1Password 还支持各种平台,如网页、PC、移动版都通通完美支持,实现密码云同步,妈妈再也不用担心我忘记密码了!

推荐指数:★★★★

系统工具

下面介绍的两款系统工具软件几乎是装机必备的。

Tuxera NTFS For Mac

用了 Mac,我们在使用移动硬盘的时候可能会遇到一个无法传输数据(如拷贝文件)的问题,这是因为部分移动硬盘是 NTFS 格式的,而 Mac 的磁盘不是这个格式,因此就会导致二者之间无法拷贝文件。有一个解决方法就是使用 Tuxera NTFS For Mac,有了它,我们就可以比较顺利地拷贝文件了。

另外还有其他品牌的 NTFS For Mac 软件,也可以尝试使用一下。

推荐指数:★★★★☆

VMware、Parallels Desktop

用了 Mac 之后,难免会有些情况下也还会不得不使用 Windows,毕竟很多软件可能只有 Windows 版本,但用 Mac 我就不推荐装双系统了,直接装虚拟机就好了,Mac 上虚拟机软件有两款比较好用,一个就是著名的 VMware,另一个就是 Parallels Desktop,这两款我都使用过,觉得都非常不错,现在用的是 VMware。

推荐指数:★★★★☆

CleanMyMac

很多时候用着用着磁盘就不够用了,如果你的 Mac 硬盘是 512GB 的倒还好,256GB 的你就得多注意一下了,另外 1T 定制版土豪请绕道,这款软件不适合你。

CleanMyMac 可以非常方便地帮助我们扫描缓存、大文件、废纸篓、残留项等内容,清理这些内容之后我们可以节省很多硬盘空间,另外它还支持软件卸载和残留清扫功能,可以帮我们非常干净地移除 Mac 中的软件,目前应该是出到第三版了,非常推荐。

推荐指数:★★★★☆

编辑器

既然做程序开发嘛,不配置好自己的开发环境怎么行,下面推荐一下我平常使用的开发软件。

JetBrains

我目前使用的 IDE 是 JetBrains 全家桶,目前我编写 Python 比较多,所以主要使用 PyCharm,另外写前端的时候也会使用 WebStorm,写 Java 就用 IntelliJ IDEA,C、C++ 用 CLion,PHP 的话就用 PhpStorm,Ruby 的话就用 RubyMine,其他的语言用的就少了,就没有装了。

当然有的小伙伴会说 JetBrains 系列的 IDE 需要购买啊?我只想说,国人的力量是无穷的,在网上其实可以搜到各种破解方法,如 License Server 验证,你能搜到各种五花八门的 License Server。另外 JetBrains 还有专门的 Educational Programs,可以来这里申请:https://www.jetbrains.com/education/programs/?fromMenu,学生、老师或教育工作者可以使用学校的 edu 邮箱申请免费的 License,如果你还是学生的话,那么申请是十分方便的,因为我还是个学生,我目前就在使用学生套餐,当然如果你已经工作的话也可以向正在上学的弟弟妹妹们借一下嘛。

总之我个人比较喜欢 JetBrains 全家桶,不论是页面风格还是开发习惯我都比较喜欢,推荐使用。

推荐指数:★★★★☆

Sublime

有时候我们可能下载了或接收了一些单个的文本文件,我们只想看看文本文件内容是什么,或者对其再做一些简单的修改操作,这时候就没必要单独用 JetBrains 的 IDE 打开了,显得有点重了。或者有时候需要修改某个配置文件,这时候也需要一个比较好用的编辑器。我使用的就是 Sublime,对于一些日常的文本编辑是足够了,另外 Sublime 还可以扩展好多插件,配置好了功能上基本不输 JetBrains IDE,非常推荐。

推荐指数:★★★★

MarkEditor

现在越来越多的写作平台开始支持 MarkDown,不得不说这确实是一门提高文字生产效率的语言,写 MarkDown 我强烈推荐 MarkEditor,我之前尝试过各种 MarkDown 写作软件,觉得都不如这款好用,如 Typora、MWeb、GitBook 等等。

MarkEditor 支持写作及预览模式,更重要的是支持文件管理,很多软件如 Typora 只能打开单个的 Makrdown 文件,不能打开整个文件夹,这就很鸡肋了。另外 MarkEditor 支持直接插入图片,如我们截了一张图或者刚从网上复制了一张图,在 MarkEditor 里面直接粘贴就可以了,它会自动把这张图保存到当前目录下,同时生成 Makrdown 格式的的图片链接,不能更方便了!另外还支持主题自定义、样式自定义,还可以快速插入某些 Makrdown 元素,还支持 Latex 公式,还可以快速导出电子书,快速生成文稿网页,快速局域网共享,功能应有尽有,强烈推荐!

这个软件我购买了 Pro 版,解锁了全部功能,订购地址:https://www.markeditor.com/,个人觉得物超所值!

推荐指数:★★★★★

SnippetLab

在写代码的时候,我们经常会有一些常用代码或者精华代码,或者一些常用的配置,想要单独保存下来复用,这时我们可能会把它保存到某个文本文件里面,更高级点可以使用云笔记,如有道云笔记或者印象笔记,用过 GitHub Gists 的小伙伴可能会选择 GitHub Gists,但我觉得这些都不是最佳的。

首先文本文件、云笔记里面其实并不是专门为了保存代码使用的,另外 GitHub Gists 保存操作并没有那么便捷,而且打开速度也很慢,影响体验。在这里推荐一款专门用来保存代码的软件叫做 SnippetLab,涉设计初衷就是为了保存短代码片的,它支持几乎所有编程语言,另外支持分类、分级、加标签、加描述等,另外它还可以和 Alfred 对接实现快速搜索查找,另外还支持备份、导出、云同步等各种功能,非常适合做代码片的管理。

推荐指数:★★★★

Beyond Compare

有时候我们需要比较两个文件的不同之处,以便于快速得知两个版本的修改内容,我使用的软件是 Beyond Compare,个人觉得比较简洁好用,同时删除和添加的内容有对应的红绿颜色标识,推荐给大家使用。

推荐指数:★★★☆

管理工具

有时候我们需要管理很多文件,或者还需要远程管理很多终端设备,在这里推荐几款比较好用的工具。

Filezlla

有时候我们需要管理一些远程的服务器,比如 Linux 服务器。那么如何和这些服务器之间传递数据和文件呢?这里推荐一个轻便简洁的软件 Filezlla,它支持 FTP、SFTP 等协议类型,使用它我们可以方便地进行文件传输和远程文件管理。

推荐指数:★★★

ForkLift

Mac 上的 Finder 你是不是已经受够了?在一些方面做得相当不友好,例如在当前打开的目录下新建一个空白文件,在当前的目录下打开命令行工具等等,有了 ForkLift 这些都是小意思了。另外 ForkLift 还集成了 Filezlla 的功能,利用它我们还可以像普通文件管理器一样管理远程的主机内容,它还支持 FTP、SFTP、SMB、WebDAV、NFS 等等各种协议。同时界面也非常美观,有了它,几乎可以抛弃 Finder 和 Filezlla 了,强烈推荐!

推荐指数:★★★★☆

SSH Shell

我们经常会和各种服务器打交道,例如我们经常使用 SSH 来远程连接某台 Linux 服务器,原生 Terminal 是支持 SSH 的,但你会发现原生带的这个太难用了。可能很多小伙伴使用 iTerm,不得不说这确实是个神器,大大方便了远程管理流程。但我在这里还要推荐一个我经常使用的 SSH Shell,没错,它的名字就是 SSH Shell,它的页面操作简洁,同时管理和记录远程主机十分方便,另外还支持秘钥管理、自动重连、自定义主题等等功能,个人用起来十分顺手,强烈推荐!

推荐指数:★★★★☆

HomeBrew、CakeBrew

对于开发者来说,这个软件几乎是 Mac 上必备的一个软件,它的官方简介就是 “The missing package manager for macOS”,算是 Mac 上的一个软件包平台,它里面包含着非常多的 Mac 开发软件包,比如 Python、PHP、Redis、MySQL、RabbitMQ、HBase 等等,几乎你能想到的开发软件都集成在里面了,堪称神器!

它的安装也非常简单,参见这里:https://brew.sh/,另外 HomeBrew 也有对应的图形界面,叫做 CakeBrew,如果不喜欢命令行操作的话可以使用 CakeBrew 来代替。

推荐指数:★★★★★

影音图像

IINA

这个必须要赞一下,非常强大简洁好用的视频播放器,是 GitHub 上的一个开源软件,链接是:https://lhc70000.github.io/iina/,播放控制、视频设置、音频设置、字幕设置、文件操作,几乎你能想到的应有尽有,而且无广告,简洁清爽,支持的视频格式也十分广泛,推荐使用!

推荐指数:★★★★

ScreenFlow

之前我曾录制过一些 Python 的视频课程,本来尝试过 QuickTime 录制,可是实在是太难用了,另外视频剪辑、音频剪辑等又是个麻烦事。后来我就使用了 ScreenFlow,它集录制、剪辑、配音、字幕、特效等功能于一体,另外录制质量,渲染质量也是一流,大大提高了我的效率,堪称神器!

推荐指数:★★★★☆

iPic

有时候我们在写 MarkDown 的时候,可能突然需要一张插入一张图片,比如我们想插入一张屏幕截图,我们就需要把这张图片先存下来,然后加上图片的路径,如果转发给别人还需要连着图片一并发给对方,这其实是不怎么方便的,倘若这张图片是一张来自网络的图片,我们直接用 HTTP 访问的话,那岂不是方便太多了?

要将图片传到网上分几步?三步。第一步,把上传页面打开,第二步,把图片传到网上并把传后链接拷贝下来,第三步,把上传页面关闭。简直是太麻烦了对不对?另外找个合适的图床也是个麻烦事啊,七牛?又拍?你不得又得申请和注册。那么有了 iPic,一切就不是难事了,它可以监听 Mac 的粘贴板,一旦我们复制了一张图或者新截了一张图,它就能显示到待上传队列里面,我们点一下它就会把图片上传到网络上,然后生成上传后的链接,默认使用的是新浪的图床,网速也非常快。有了它,传图什么的都不是事了!另外付费版还支持各种自定义图床,如七牛云、又拍云、阿里云、腾讯云等等。

推荐指数:★★★★☆

PixelMator

在 Windows 上我们常用 PS 来修改和处理图片,Mac 上我是没有使用 PS,使用了 PixelMator,个人觉得使用这款软件能完全胜任 PS 的工作,一般的图片设计、排版、抠图、特效、蒙版等操作都支持,我个人比较喜欢使用这款软件做设计。

推荐指数:★★★★

Polarr Photo Editor

这个软件又名“泼辣修图”,类似 Mac 上的美图秀秀,它自带了各种后期滤镜,还带有 Lightroom 的很多调光调色的工具,能够帮我们快速对照片进行后期处理,效果也还不错,当然比不上 Photoshop 和 Lightroom 那么专业,但对于快速进行后处理的小伙伴来说不失为一个好的选择。

推荐指数:★★★★

Boom2

我有边工作边听歌的习惯,所以音乐几乎离不开我的生活,入了个好耳机,那当然就得配上好音乐。大家肯定也听说过音效均衡器,我们可以调整不同的音效参数来达到不同的声音效果,如电子音、人声、环绕、重低音等等,在 Mac 上我觉得最好用的就是 Boom2 了,它内置了各种音效均衡器,还有一些高保真效果的渲染,效果非常给力。我一般听歌的时候就会把 Boom2 开起来,享受不一样的音效感觉,美哉。

推荐指数:★★★★

趣味扩展

另外还有几个比较有意思的工具推荐下。

Tickeys

使用过机械键盘吗?按键感觉和声音很爽吧,但是用了 Mac,你如果不使用外接键盘的话,想必手感就差上不少,但这款软件或许可以拯救一下,它可以模拟机械键盘的按键声,每次按键都有有机械键盘清脆的声音,我平时戴耳机撸代码的时候就会开着这个软件,感觉体验还是不错的,建议尝试一下。

推荐指数:★★★☆

Duet

Duet 这款软件可以将 iPad 或 iPhone 变成电脑的扩展屏幕,如果你有一个大屏的比如 12.9 寸的 iPad 的话,非常建议你尝试一下这款软件,这样如果正你在用 Mac 不用 iPad 的话,完全可以用 Duet 把 iPad 和电脑屏幕连接起来来扩展显示,充分利用资源。

推荐指数:★★★☆

好了,暂时推荐这么多,其实还有很多很多,尤其是专门针对于开发者的一些工具,这些就太偏极客化了,后面再为大家整理一些好用的开发者工具,敬请期待。

还不尽兴的小伙伴可以关注 GitHub 上的一个仓库叫 awesome-mac,里面列出来了 Mac 上推荐的非常多的软件,总结得非常非常详细,链接是:https://github.com/jaywcjlove/awesome-mac,大家可以去看下。

Tips

可能有的小伙伴好奇我的 Launchpad 为啥能放那么多图标,是怎么做到的?其实很简单,几行代码就搞定了。

调整每列显示图标数量,这里以 7 为例:

defaults write com.apple.dock springboard-rows -int 7

调整每行显示图标的数量,这里以 8 为例:

defaults write com.apple.dock springboard-columns -int 8

上面两行代码最后的数字可以自行修改。

修改完了之后还需要重置一下 Launchpad,代码如下:

defaults write com.apple.dock ResetLaunchPad -bool TRUE;killall Dock

好了,这样我们就可以自由定制我们的 Launchpad 图标数量啦!

另外,还有的小伙伴会说,很多软件都需要花钱购买啊,咋办?告诉你个网址:http://xclient.info/,几乎你想找的破解版都有,别说别的了,雷锋也别叫了,省下的钱打赏给我一点就行哈哈。

以上就是我的一些 Mac 常用软件分享及 Tips,希望对大家有帮助!

另外大家如有还有推荐的软件,欢迎留言给我,非常感谢!

转载请注明:静觅 » 推荐一些Mac上比较好用的软件

【Python搞搞轻量博客】第四发 数据库入门

$
0
0

大家好,我咸鱼又顺利回来啦!2333333…

最近小伙伴们催的紧,还有人说不更新要锤我的。

吓到我前来更新了。

今天给大家带来的是数据库的操作。

说到数据库啊,大家别怕,虽然要学的很多!

但是,有我在!

  • 数据库介绍

推荐环节:

书籍:

  • 《傲慢与偏见》
  • 《了不起的盖茨比》
  • 《金雀花王朝》
  • 《从0到1》
  • 《灰犀牛》
  • 《查理·芒格传》
  • 《天意》

 

数据库介绍

之前,我们写了一个非常简单的无需数据库的篇章,是为了巩固前几章的知识。

:“既然可以不用数据库就可以写,那我们还为什么学数据库?”

答:“如果数据非常大,那么我们怎么去获取我们需要的数据呢?难道我们要去遍历吗?上万的数据,是非常容易累计的。而且我们的数据存在内存上,一旦断电,数据就会消失,而且不易维护这些数据。”

 

问:“难道不可以存在一个文件里面吗?”

答: “如果直接将数据放在一个文件里,那如果我们要去在百万数据中去获取一个数据,请问怎么办呢?而且这样非常浪费资源。”

……总之这样的问有很多。

现在,我们要讲讲数据库。

数据库,你可以理解为一个专门存放数据的一个工具。

那存放什么数据呢?
比如:

  • 你的账户密码
  • 你的浏览记录
  • 你的操作记录
  • 银行卡的余额和交易记录、转帐信息
  • 各个不同的网站、QQ、网上购物、贴吧、喜欢听的音乐、电影的收藏信息
  • 手机电话机录、余额、公交卡余额、水费、电费、彩票的购买记录
  • 以及你的开房记录…

总之,你在网上的一切信息,基本都是存放在各大公司或者个人的数据库中。

 

数据库的基本单位

在一个数据库应用中,会有很多

在一个中,又有很多表单

在一个表单中,又有很多字段

在一个字段中,又有很多

所以,它可以这样排序。

数据库应用(mysql) > 库(db) > 表单(table) > 字段(field) >

数据库应用: 通常存放数据只要一个数据库应用。

: 进行数据的大分类。

表单: 进一步分类,比如  UsersVpis 等这样的分类

字段: 比如在 Users中会有这样的分类,比如 username password age等这样的分类。

: 每个数据就是一行

扔个图片大家可能会更清楚

表单

字段

数据展示

 

这样的话,大家应该心里就有个底了。

然后我们开始正式介绍数据库。

这是近年流行的数据库

2018年StackOverflow数据库调查报告

所以,我们使用mysql数据库。

关于安装的事情,大家可以去找相关的文章。因为每个人的情况不同,比如操作系统,我没办法把三种操作系统(Linux,Windows,Mac)都去做一遍图文教程。而且有时候坑不断….

 

转载请注明:静觅 » 【Python搞搞轻量博客】第四发 数据库入门

Python中异步协程的使用方法介绍

$
0
0

1. 前言

在执行一些 IO 密集型任务的时候,程序常常会因为等待 IO 而阻塞。比如在网络爬虫中,如果我们使用 requests 库来进行请求的话,如果网站响应速度过慢,程序一直在等待网站响应,最后导致其爬取效率是非常非常低的。

为了解决这类问题,本文就来探讨一下 Python 中异步协程来加速的方法,此种方法对于 IO 密集型任务非常有效。如将其应用到网络爬虫中,爬取效率甚至可以成百倍地提升。

注:本文协程使用 async/await 来实现,需要 Python 3.5 及以上版本。

2. 基本了解

在了解异步协程之前,我们首先得了解一些基础概念,如阻塞和非阻塞、同步和异步、多进程和协程。

2.1 阻塞

阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。

常见的阻塞形式有:网络 I/O 阻塞、磁盘 I/O 阻塞、用户输入阻塞等。阻塞是无处不在的,包括 CPU 切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。如果是多核 CPU 则正在执行上下文切换操作的核不可被利用。

2.2 非阻塞

程序在等待某操作过程中,自身不被阻塞,可以继续运行干别的事情,则称该程序在该操作上是非阻塞的。

非阻塞并不是在任何程序级别、任何情况下都可以存在的。
仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。

非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。

2.3 同步

不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,称这些程序单元是同步执行的。

例如购物系统中更新商品库存,需要用“行锁”作为通信信号,让不同的更新请求强制排队顺序执行,那更新库存的操作是同步的。

简言之,同步意味着有序。

2.4 异步

为完成某个任务,不同程序单元之间过程中无需通信协调,也能完成任务的方式,不相关的程序单元之间可以是异步的。

例如,爬虫下载网页。调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是无关的,也无需相互通知协调。这些异步操作的完成时刻并不确定。

简言之,异步意味着无序。

2.5 多进程

多进程就是利用 CPU 的多核优势,在同一时间并行地执行多个任务,可以大大提高执行效率。

2.6 协程

协程,英文叫做 Coroutine,又称微线程,纤程,协程是一种用户态的轻量级线程。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重入时,就相当于进入上一次调用的状态。

协程本质上是个单进程,协程相对于多进程来说,无需线程上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单。

我们可以使用协程来实现异步操作,比如在网络爬虫场景下,我们发出一个请求之后,需要等待一定的时间才能得到响应,但其实在这个等待过程中,程序可以干许多其他的事情,等到响应得到之后才切换回来继续处理,这样可以充分利用 CPU 和其他资源,这就是异步协程的优势。

3. 异步协程用法

接下来让我们来了解下协程的实现,从 Python 3.4 开始,Python 中加入了协程的概念,但这个版本的协程还是以生成器对象为基础的,在 Python 3.5 则增加了 async/await,使得协程的实现更加方便。

Python 中使用协程最常用的库莫过于 asyncio,所以本文会以 asyncio 为基础来介绍协程的使用。

首先我们需要了解下面几个概念:

  • event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件发生的时候,就会调用对应的处理方法。
  • coroutine:中文翻译叫协程,在 Python 中常指代为协程对象类型,我们可以将协程对象注册到时间循环中,它会被事件循环调用。我们可以使用 async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象。
  • task:任务,它是对协程对象的进一步封装,包含了任务的各个状态。
  • future:代表将来执行或没有执行的任务的结果,实际上和 task 没有本质区别。

另外我们还需要了解 async/await 关键字,它是从 Python 3.5 才出现的,专门用于定义协程。其中,async 定义一个协程,await 用来挂起阻塞方法的执行。

3.1 定义协程

首先我们来定义一个协程,体验一下它和普通进程在实现上的不同之处,代码如下:

import asyncio

async def execute(x):
    print('Number:', x)

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine)
print('After calling loop')

运行结果:

Coroutine: <coroutine object execute at 0x1034cf830>
After calling execute
Number: 1
After calling loop

首先我们引入了 asyncio 这个包,这样我们才可以使用 async 和 await,然后我们使用 async 定义了一个 execute() 方法,方法接收一个数字参数,方法执行之后会打印这个数字。

随后我们直接调用了这个方法,然而这个方法并没有执行,而是返回了一个 coroutine 协程对象。随后我们使用 get_event_loop() 方法创建了一个事件循环 loop,并调用了 loop 对象的 run_until_complete() 方法将协程注册到事件循环 loop 中,然后启动。最后我们才看到了 execute() 方法打印了输出结果。

可见,async 定义的方法就会变成一个无法直接执行的 coroutine 对象,必须将其注册到事件循环中才可以执行。

上文我们还提到了 task,它是对 coroutine 对象的进一步封装,它里面相比 coroutine 对象多了运行状态,比如 running、finished 等,我们可以用这些状态来获取协程对象的执行情况。

在上面的例子中,当我们将 coroutine 对象传递给 run_until_complete() 方法的时候,实际上它进行了一个操作就是将 coroutine 封装成了 task 对象,我们也可以显式地进行声明,如下所示:

import asyncio

async def execute(x):
    print('Number:', x)
    return x

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
print('Task:', task)
loop.run_until_complete(task)
print('Task:', task)
print('After calling loop')

运行结果:

Coroutine: <coroutine object execute at 0x10e0f7830>
After calling execute
Task: <Task pending coro=<execute() running at demo.py:4>>
Number: 1
Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1>
After calling loop

这里我们定义了 loop 对象之后,接着调用了它的 create_task() 方法将 coroutine 对象转化为了 task 对象,随后我们打印输出一下,发现它是 pending 状态。接着我们将 task 对象添加到事件循环中得到执行,随后我们再打印输出一下 task 对象,发现它的状态就变成了 finished,同时还可以看到其 result 变成了 1,也就是我们定义的 execute() 方法的返回结果。

另外定义 task 对象还有一种方式,就是直接通过 asyncio 的 ensure_future() 方法,返回结果也是 task 对象,这样的话我们就可以不借助于 loop 来定义,即使我们还没有声明 loop 也可以提前定义好 task 对象,写法如下:

import asyncio

async def execute(x):
    print('Number:', x)
    return x

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

task = asyncio.ensure_future(coroutine)
print('Task:', task)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)
print('After calling loop')

运行结果:

Coroutine: <coroutine object execute at 0x10aa33830>
After calling execute
Task: <Task pending coro=<execute() running at demo.py:4>>
Number: 1
Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1>
After calling loop

发现其效果都是一样的。

3.2 绑定回调

另外我们也可以为某个 task 绑定一个回调方法,来看下面的例子:

import asyncio
import requests

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

def callback(task):
    print('Status:', task.result())

coroutine = request()
task = asyncio.ensure_future(coroutine)
task.add_done_callback(callback)
print('Task:', task)

loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)

在这里我们定义了一个 request() 方法,请求了百度,返回状态码,但是这个方法里面我们没有任何 print() 语句。随后我们定义了一个 callback() 方法,这个方法接收一个参数,是 task 对象,然后调用 print() 方法打印了 task 对象的结果。这样我们就定义好了一个 coroutine 对象和一个回调方法,我们现在希望的效果是,当 coroutine 对象执行完毕之后,就去执行声明的 callback() 方法。

那么它们二者怎样关联起来呢?很简单,只需要调用 add_done_callback() 方法即可,我们将 callback() 方法传递给了封装好的 task 对象,这样当 task 执行完毕之后就可以调用 callback() 方法了,同时 task 对象还会作为参数传递给 callback() 方法,调用 task 对象的 result() 方法就可以获取返回结果了。

运行结果:

Task: <Task pending coro=<request() running at demo.py:5> cb=[callback() at demo.py:11]>
Status: <Response [200]>
Task: <Task finished coro=<request() done, defined at demo.py:5> result=<Response [200]>>

实际上不用回调方法,直接在 task 运行完毕之后也可以直接调用 result() 方法获取结果,如下所示:

import asyncio
import requests

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

coroutine = request()
task = asyncio.ensure_future(coroutine)
print('Task:', task)

loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)
print('Task Result:', task.result())

运行结果是一样的:

Task: <Task pending coro=<request() running at demo.py:4>>
Task: <Task finished coro=<request() done, defined at demo.py:4> result=<Response [200]>>
Task Result: <Response [200]>

3.3 多任务协程

上面的例子我们只执行了一次请求,如果我们想执行多次请求应该怎么办呢?我们可以定义一个 task 列表,然后使用 asyncio 的 wait() 方法即可执行,看下面的例子:

import asyncio
import requests

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
print('Tasks:', tasks)

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

for task in tasks:
    print('Task Result:', task.result())

这里我们使用一个 for 循环创建了五个 task,组成了一个列表,然后把这个列表首先传递给了 asyncio 的 wait() 方法,然后再将其注册到时间循环中,就可以发起五个任务了。最后我们再将任务的运行结果输出出来,运行结果如下:

Tasks: [<Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>]
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>

可以看到五个任务被顺次执行了,并得到了运行结果。

3.4 协程实现

前面说了这么一通,又是 async,又是 coroutine,又是 task,又是 callback,但似乎并没有看出协程的优势啊?反而写法上更加奇怪和麻烦了,别急,上面的案例只是为后面的使用作铺垫,接下来我们正式来看下协程在解决 IO 密集型任务上有怎样的优势吧!

上面的代码中,我们用一个网络请求作为示例,这就是一个耗时等待的操作,因为我们请求网页之后需要等待页面响应并返回结果。耗时等待的操作一般都是 IO 操作,比如文件读取、网络请求等等。协程对于处理这种操作是有很大优势的,当遇到需要等待的情况的时候,程序可以暂时挂起,转而去执行其他的操作,从而避免一直等待一个程序而耗费过多的时间,充分利用资源。

为了表现出协程的优势,我们需要先创建一个合适的实验环境,最好的方法就是模拟一个需要等待一定时间才可以获取返回结果的网页,上面的代码中使用了百度,但百度的响应太快了,而且响应速度也会受本机网速影响,所以最好的方式是自己在本地模拟一个慢速服务器,这里我们选用 Flask。

如果没有安装 Flask 的话可以执行如下命令安装:

pip3 install flask

然后编写服务器代码如下:

from flask import Flask
import time

app = Flask(__name__)

@app.route('/')
def index():
    time.sleep(3)
    return 'Hello!'

if __name__ == '__main__':
    app.run(threaded=True)

这里我们定义了一个 Flask 服务,主入口是 index() 方法,方法里面先调用了 sleep() 方法休眠 3 秒,然后接着再返回结果,也就是说,每次请求这个接口至少要耗时 3 秒,这样我们就模拟了一个慢速的服务接口。

注意这里服务启动的时候,run() 方法加了一个参数 threaded,这表明 Flask 启动了多线程模式,不然默认是只有一个线程的。如果不开启多线程模式,同一时刻遇到多个请求的时候,只能顺次处理,这样即使我们使用协程异步请求了这个服务,也只能一个一个排队等待,瓶颈就会出现在服务端。所以,多线程模式是有必要打开的。

启动之后,Flask 应该默认会在 127.0.0.1:5000 上运行,运行之后控制台输出结果如下:

 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

接下来我们再重新使用上面的方法请求一遍:

import asyncio
import requests
import time

start = time.time()

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = requests.get(url)
    print('Get response from', url, 'Result:', response.text)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:', end - start)

在这里我们还是创建了五个 task,然后将 task 列表传给 wait() 方法并注册到时间循环中执行。

运行结果如下:

Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 15.049368143081665

可以发现和正常的请求并没有什么两样,依然还是顺次执行的,耗时 15 秒,平均一个请求耗时 3 秒,说好的异步处理呢?

其实,要实现异步处理,我们得先要有挂起的操作,当一个任务需要等待 IO 结果的时候,可以挂起当前任务,转而去执行其他任务,这样我们才能充分利用好资源,上面方法都是一本正经的串行走下来,连个挂起都没有,怎么可能实现异步?想太多了。

要实现异步,接下来我们再了解一下 await 的用法,使用 await 可以将耗时等待的操作挂起,让出控制权。当协程执行的时候遇到 await,时间循环就会将本协程挂起,转而去执行别的协程,直到其他的协程挂起或执行完毕。

所以,我们可能会将代码中的 request() 方法改成如下的样子:

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = await requests.get(url)
    print('Get response from', url, 'Result:', response.text)

仅仅是在 requests 前面加了一个 await,然而执行以下代码,会得到如下报错:

Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Cost time: 15.048935890197754
Task exception was never retrieved
future: <Task finished coro=<request() done, defined at demo.py:7> exception=TypeError("object Response can't be used in 'await' expression",)>
Traceback (most recent call last):
  File "demo.py", line 10, in request
    status = await requests.get(url)
TypeError: object Response can't be used in 'await' expression

这次它遇到 await 方法确实挂起了,也等待了,但是最后却报了这么个错,这个错误的意思是 requests 返回的 Response 对象不能和 await 一起使用,为什么呢?因为根据官方文档说明,await 后面的对象必须是如下格式之一:

  • A native coroutine object returned from a native coroutine function,一个原生 coroutine 对象。
  • A generator-based coroutine object returned from a function decorated with types.coroutine(),一个由 types.coroutine() 修饰的生成器,这个生成器可以返回 coroutine 对象。
  • An object with an await__ method returning an iterator,一个包含 __await 方法的对象返回的一个迭代器。

可以参见:https://www.python.org/dev/peps/pep-0492/#await-expression

reqeusts 返回的 Response 不符合上面任一条件,因此就会报上面的错误了。

那么有的小伙伴就发现了,既然 await 后面可以跟一个 coroutine 对象,那么我用 async 把请求的方法改成 coroutine 对象不就可以了吗?所以就改写成如下的样子:

import asyncio
import requests
import time

start = time.time()

async def get(url):
    return requests.get(url)

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = await get(url)
    print('Get response from', url, 'Result:', response.text)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:', end - start)

这里我们将请求页面的方法独立出来,并用 async 修饰,这样就得到了一个 coroutine 对象,我们运行一下看看:

Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 15.134317874908447

还是不行,它还不是异步执行,也就是说我们仅仅将涉及 IO 操作的代码封装到 async 修饰的方法里面是不可行的!我们必须要使用支持异步操作的请求方式才可以实现真正的异步,所以这里就需要 aiohttp 派上用场了。

3.5 使用 aiohttp

aiohttp 是一个支持异步请求的库,利用它和 asyncio 配合我们可以非常方便地实现异步请求操作。

安装方式如下:

pip3 install aiohttp

官方文档链接为:https://aiohttp.readthedocs.io/,它分为两部分,一部分是 Client,一部分是 Server,详细的内容可以参考官方文档。

下面我们将 aiohttp 用上来,将代码改成如下样子:

import asyncio
import aiohttp
import time

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = await get(url)
    print('Get response from', url, 'Result:', result)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:', end - start)

在这里我们将请求库由 requests 改成了 aiohttp,通过 aiohttp 的 ClientSession 类的 get() 方法进行请求,结果如下:

Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 3.0199508666992188

成功了!我们发现这次请求的耗时由 15 秒变成了 3 秒,耗时直接变成了原来的 1/5。

代码里面我们使用了 await,后面跟了 get() 方法,在执行这五个协程的时候,如果遇到了 await,那么就会将当前协程挂起,转而去执行其他的协程,直到其他的协程也挂起或执行完毕,再进行下一个协程的执行。

开始运行时,时间循环会运行第一个 task,针对第一个 task 来说,当执行到第一个 await 跟着的 get() 方法时,它被挂起,但这个 get() 方法第一步的执行是非阻塞的,挂起之后立马被唤醒,所以立即又进入执行,创建了 ClientSession 对象,接着遇到了第二个 await,调用了 session.get() 请求方法,然后就被挂起了,由于请求需要耗时很久,所以一直没有被唤醒,好第一个 task 被挂起了,那接下来该怎么办呢?事件循环会寻找当前未被挂起的协程继续执行,于是就转而执行第二个 task 了,也是一样的流程操作,直到执行了第五个 task 的 session.get() 方法之后,全部的 task 都被挂起了。所有 task 都已经处于挂起状态,那咋办?只好等待了。3 秒之后,几个请求几乎同时都有了响应,然后几个 task 也被唤醒接着执行,输出请求结果,最后耗时,3 秒!

怎么样?这就是异步操作的便捷之处,当遇到阻塞式操作时,任务被挂起,程序接着去执行其他的任务,而不是傻傻地等着,这样可以充分利用 CPU 时间,而不必把时间浪费在等待 IO 上。

有人就会说了,既然这样的话,在上面的例子中,在发出网络请求后,既然接下来的 3 秒都是在等待的,在 3 秒之内,CPU 可以处理的 task 数量远不止这些,那么岂不是我们放 10 个、20 个、50 个、100 个、1000 个 task 一起执行,最后得到所有结果的耗时不都是 3 秒左右吗?因为这几个任务被挂起后都是一起等待的。

理论来说确实是这样的,不过有个前提,那就是服务器在同一时刻接受无限次请求都能保证正常返回结果,也就是服务器无限抗压,另外还要忽略 IO 传输时延,确实可以做到无限 task 一起执行且在预想时间内得到结果。

我们这里将 task 数量设置成 100,再试一下:

tasks = [asyncio.ensure_future(request()) for _ in range(100)]

耗时结果如下:

Cost time: 3.106252670288086

最后运行时间也是在 3 秒左右,当然多出来的时间就是 IO 时延了。

可见,使用了异步协程之后,我们几乎可以在相同的时间内实现成百上千倍次的网络请求,把这个运用在爬虫中,速度提升可谓是非常可观了。

3.6 与单进程、多进程对比

可能有的小伙伴非常想知道上面的例子中,如果 100 次请求,不是用异步协程的话,使用单进程和多进程会耗费多少时间,我们来测试一下:

首先来测试一下单进程的时间:

import requests
import time

start = time.time()

def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = requests.get(url).text
    print('Get response from', url, 'Result:', result)

for _ in range(100):
    request()

end = time.time()
print('Cost time:', end - start)

最后耗时:

Cost time: 305.16639709472656

接下来我们使用多进程来测试下,使用 multiprocessing 库:

import requests
import time
import multiprocessing

start = time.time()

def request(_):
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = requests.get(url).text
    print('Get response from', url, 'Result:', result)

cpu_count = multiprocessing.cpu_count()
print('Cpu count:', cpu_count)
pool = multiprocessing.Pool(cpu_count)
pool.map(request, range(100))

end = time.time()
print('Cost time:', end - start)

这里我使用了multiprocessing 里面的 Pool 类,即进程池。我的电脑的 CPU 个数是 8 个,这里的进程池的大小就是 8。

运行时间:

Cost time: 48.17306900024414

可见 multiprocessing 相比单线程来说,还是可以大大提高效率的。

3.7 与多进程的结合

既然异步协程和多进程对网络请求都有提升,那么为什么不把二者结合起来呢?在最新的 PyCon 2018 上,来自 Facebook 的 John Reese 介绍了 asyncio 和 multiprocessing 各自的特点,并开发了一个新的库,叫做 aiomultiprocess,感兴趣的可以了解下:https://www.youtube.com/watch?v=0kXaLh8Fz3k

这个库的安装方式是:

pip3 install aiomultiprocess

需要 Python 3.6 及更高版本才可使用。

使用这个库,我们可以将上面的例子改写如下:

import asyncio
import aiohttp
import time
from aiomultiprocess import Pool

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result

async def request():
    url = 'http://127.0.0.1:5000'
    urls = [url for _ in range(100)]
    async with Pool() as pool:
        result = await pool.map(get, urls)
        return result

coroutine = request()
task = asyncio.ensure_future(coroutine)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)

end = time.time()
print('Cost time:', end - start)

这样就会同时使用多进程和异步协程进行请求,当然最后的结果其实和异步是差不多的:

Cost time: 3.1156570434570312

因为我的测试接口的原因,最快的响应也是 3 秒,所以这部分多余的时间基本都是 IO 传输时延。但在真实情况下,我们在做爬取的时候遇到的情况千变万化,一方面我们使用异步协程来防止阻塞,另一方面我们使用 multiprocessing 来利用多核成倍加速,节省时间其实还是非常可观的。

以上便是 Python 中协程的基本用法,希望对大家有帮助。

4. 参考来源

转载请注明:静觅 » Python中异步协程的使用方法介绍


【Python搞搞轻量博客】第五发 关系型数据库Mysql的Py交易

$
0
0

嘿嘿,我想高产,但是我懒…
我们今天继续讲数据库。

咱开始说啊!

什么是关系型数据库?

关系型数据库又称为关系型数据库管理系统(RDBMS),它是利用数据概念实现对数据处理的算法,达到对数据及其快速的增删改查操作。

既然被称为关系型数据库,那么它的关系在哪里体现呢?

举一个🌰吧。

比如我现在有表单A 和 表单B

其中:

表单A 中有一个名为user_id的字段

表单B 中也有一个名为user_id的字段

现在我把他们建立一种联系,当我去修改表单Auser_id的值时,表单B 中的user_id的值也会自动进行修改,因为他们建立的一种关系,因为这种关系,使得数据具有一致性。 千万数据中,获取有数条直接,在运维或者开发哥哥的神操作下,他们冥冥中被安排的明明白白。

 

非关系型数据库 正如它的名字,每条数据间都是独立存在的,没撒子关系哩。

RDBMS 术语

在上一篇文章 第四发 数据库入门 中,我提到的数据表单都是RDBMS中的一些术语。现在我继续补充一些。

冗余: 不考虑数据大小,去提高查询数据的速度。举一个🌰比如我现在有两个表单,一个叫A,一个叫B,而在AB中,有好几个字段是相关联的,如果这时候我们数据量很大,还的要进行极其频繁的数据查询,数据库查询的速度一定会慢下来。在极其频繁的查询中,因为很多字段都是相关联的,每次都要进行很多次跨表查询,所以速度会慢下来。如果我们这时候取消几个字段关系,把B中的字段写到A里边,把A里边的写到B里边。这样虽然取消了关系,但是两个表中数据重复了,所以数据库大小就会变大,但是这时候就会减少跨表查询,数据库查询速度就很上来。所以,在数据库中,有时候查询慢的原因未必是数据量太大,而是拥有极其复杂的字段关系。

 

主键: 你可以理解为主要关键字.主键在当前表单的当前字段是唯一的 比如数据库通常都是在第一个字段是 ID,这个通常就是一个主键,它默认会自增长。它在名为ID的字段下是不会重复的,每行的值与其他行的值不会重复。

 

外键: 主要用于两个表直接的关联. 强行举🌰,比如我现在有一个名为A 和 B 的表单,在A 中有一个名为 username 的字段,在B中有一个名为user_email的字段,这时username去关联user_email的字段,这时的username字段就叫做外键.

 

索引: 利用一定的算法方法,对专门的字段进行优化,使其加快查询速率。

接下来,来看Mysql的使用。

为了后期我们方便使用,我先在这里教大家创建一个mysql账户

我们使用默认的root 超级管理员用户登录后,是这样的。

这时,我们要创建一个专门针对一个名为test的库创建一个用户,让这个用户只对test库有增删改查的权限。

我决定这个用户的用户名和密码都设置为 xeanyu

我们来解析一下 grant all on test.* to 'xeanyu'@'localhost' identified by "xeanyu";

其中

grant是Mysql一个专门控制权限的命令

all 指的是所有权限

test.* test是数据库名字,然后后边的 .*是指当前所有表

'xeanyu'@'localhost' 其中前面的xeanyu指的是用户名,而localhost指的是这个用户名能在哪里进行登录,这里的localhost是本地。

identified by "xeanyu" 指的是设置密码为xeanyu

请根据自己的情况做决定,记得命令后边有一个分号!!

然后我们退出数据库,使用xeanyu的数据库账户登录一下。

可以看到,我们使用新创建的用户名成功登录了。

这是胜利的第一步!

 

Mysql数据库简单的一些命令

  • show databases;  显示当前用户下能操作的所有数据库。
  • use [数据库名称];  切换数据库
  • show tables;    显示当前数据库下的所有表单
  • create database [数据库名称] charset utf8mb4;    创建一个名为[数据库名称]的数据库,且编码为utf8.如果不指定编码,可能只能支持拉丁文。
  • drop database [数据库名称];    删除数据库[名称]
  • truncate table [表名];    清空表单所有数据
  • delete table [表名];    删除表单
  • desc [表名];    查看表结构
  • select [字段名或者用*代替所有] from [表单名];     查看[表单名]中的[字段名或者用*代替所有]的数据。

Mysql 创建一个表单

我们先来手撸一个表结构(以后就不用了,放💕)

可以看到,重点在于最后的创建表结构。

我们分析一下那个Sql代码。

create table User(
id int auto_increment,
user_name char(32) not null,
user_pass char(64) not null,
user_age int null,
primary key(id));

其中id user_name user_pass user_age 字段,其中在代码的最后一行,指明了字段id 是主键,在代码第二行,从auto_increment看出指明了字段id 自增长。而其中多次出现 not null,它的意思是不容许空,而null的意思是容许为空。

通常字段格式就是: 字段名 字段类型 字段属性

我们使用 desc User 看出User表单中的表单结构.

向User表单插入第一条数据

我们来分析一下插入语句。

insert into User (user_name,user_pass,user_age) values("XeanYu","XeanYu",18);

我们分看下

其中insert 和 into 就不用说了,固定的。

其中User 是表单名,(user_name,user_pass,user_age) 其中是三个对应User中的三个字段,有人问,为什么不给id字段赋值?因为id 字段是自增长的主键。

然后values("XeanYu","XeanYu",18) 其中三个值对应三个user_name,user_pass,user_age三个字段。

这时候有人估计看咸鱼不爽了,想要找事。如果我把id 字段的值强行写进去呢,比如我就写个1,重复的话,数据库能拿我怎么样呢?咸鱼我准备实施反击计划。

看到了吧。Mysql都看你不爽了,人家主键在对应字段下面是数据唯一的!

 

有人可能还要问啦,如果我把id改成其他数字行吗?我要回答,当然可以,只要主键的值不重复,一切都好说。

可以看到,其实主键的值就是在上一行主键的值 +1

Mysql简单数据查询

我们已经了解了简单的语句查询。

我们稍微生个级别。

比如我要user_age 为18的几行数据怎么办?也就是说年龄18的用户怎么筛选出来?

可以看到,我们用where user_age = 18 过滤出了我们需要的数据。

继续升级,如果我不要看到id 这个字段的内容咋办?

我们可以去更改select 所选择的字段进行输出。

由于有三个完全相同资料的用户,我想直接跳过前两个进行输出怎么搞?

这个时候我们就要清楚偏移量大哥了.

可以看到,我们多了两个东西limit offset 这两个东西

limit: 显示条数

offset: 跳过几条开始查询

Mysql简单的数据修改更新

假如我想修改麻花藤的user_pass字段的值怎么办呢?

直接上图

update [表名] set [字段修改语句] where 条件;

Mysql 简单的降序升序

比如我现在有一堆新日志,但是现在数据库的从小到大的排列方法,我想把顺序倒转过来,我该怎么搞呢?

好了,Mysql的基本知识就写到这里,等咸鱼哪天突然发疯给大家疯狂讲Mysql吧。

接下来就讲Python和Mysql直接的操作。

Python操作Mysql

有一个这样的库,专门针对关系型数据库而有的模块。

他就是Sqlalchemy 它是一个ORM框架,是由Mako的作者创建的。

我们先使用pip3来安装一下。

pip3 install Sqlalchemy --index http://pypi.douban.com/simple --trusted-host pypi.douban.com # 我们需要的主要模块
pip3 install pymysql --index http://pypi.douban.com/simple --trusted-host pypi.douban.com  # 用于Sqlalchemy与mysql直接的驱动问题
# 考虑到Python官方的PYPI仓库安装很慢,所以我们使用豆瓣的PYPI仓库。当然,如果你有梯子,可以直接pip,不用带参数。
# 其实这里的pymysql也可以进行Python操作Mysql,但是后边我们要用Sqlalchemy做大事情!

在上边的内容里边,我们已经建立了一个针对数据库 test的用户,现在这个用户就要派上用场了。

 

现在,我如果要用Python获取数据库test下的 User 的所有数据。

废话不多说,直接上代码。

__author__ = 'XeanYu'
from sqlalchemy import create_engine
from pymysql import install_as_MySQLdb
install_as_MySQLdb()

engine = create_engine("mysql://xeanyu:xeanyu@127.0.0.1/test")

con = engine.connect() # 去连接数据库,返回一个连接后的实例
raw = con.execute("select * from User") # 注意,使用Python去操作数据库,写Sql命令时可以不带分号。

for i in raw: # 这里会返回一个raw,raw中每个元素是每行值所组成的的元组(tuple)
    print(i)

第2~4行:其中create_engine 是用于连接数据库的,它会返回一个实例,但是这个时候并未连接。而第二行中的install_as_MySQLdb是一个处理包的函数,原本Mysqldb是不支持Python3的,后来有了Pymysql,但是还有很多模块需要Mysqldb,所以就在这里进行了包上的处理,第三行所执行的函数,就是可以让那些需要Mysqldb的模块可以获取到Mysqldb包。

 

第6行: 这里需要重点讲下,这里的create_engine 连接数据库的格式是

dialect+driver://username:password@host:port/database_name

其中 dialect 指的是数据库程序,比如我用mysql,或者其他数据库名称,比如sqllite,postgersql等等。

其中driver 是数据库程序的驱动,如果不指定,Sqlalchemy默认会是Mysqldb,这也是我为什么要用 install_as_MySQLdb 的原因。

其中username 是数据库用户名,比如我们创建的 xeanyu 这个用户,xeanyu就是用户名

其中 password 是用户密码

其中hostport 是数据库地址和端口,其中port不指定则默认根据dialect 去默认。

其中database_name 是数据库名称

所以我们根据以上格式,我们去连接数据库。

 

第8行: 我们根据create_engine给我们返回给我们的实例去连接数据库,进行connect()

 

第9行: 我们又根据connect() 返回给我们的连接实例,去进行数据库的操作, select * from User 其中不必带上分号,它会返回一个迭代器,我们把这个迭代器赋值给raw

 

第11~12行: 我们输出我们查询的东西。

OK了,今天就说到这里。下节我们细说。

转载请注明:静觅 » 【Python搞搞轻量博客】第五发 关系型数据库Mysql的Py交易

《Python3网络爬虫开发实战》第三波赠书活动来了!

$
0
0

开门见山

话不多说了!第三波送书活动来了!这次送 20 本签名版《Python3网络爬虫开发实战》。

本书目前上市三个月已经重印 6 次,上市三个月以来长期位居京东计算机类新书榜第一位(现已不算新书),目前在豆瓣的评分是 9.2 分。

书籍介绍

本书《Python3网络爬虫开发实战》全面介绍了利用 Python3 开发网络爬虫的知识,书中首先详细介绍了各种类型的环境配置过程和爬虫基础知识,还讨论了 urllib、requests 等请求库和 Beautiful Soup、XPath、pyquery 等解析库以及文本和各类数据库的存储方法,另外本书通过多个真实新鲜案例介绍了分析 Ajax 进行数据爬取,Selenium 和 Splash 进行动态网站爬取的过程,接着又分享了一些切实可行的爬虫技巧,比如使用代理爬取和维护动态代理池的方法、ADSL 拨号代理的使用、各类验证码(图形、极验、点触、宫格等)的破解方法、模拟登录网站爬取的方法及 Cookies 池的维护等等。

此外,本书的内容还远远不止这些,作者还结合移动互联网的特点探讨了使用 Charles、mitmdump、Appium 等多种工具实现 App 抓包分析、加密参数接口爬取、微信朋友圈爬取的方法。此外本书还详细介绍了 pyspider 框架、Scrapy 框架的使用和分布式爬虫的知识,另外对于优化及部署工作,本书还包括 Bloom Filter 效率优化、Docker 和 Scrapyd 爬虫部署、分布式爬虫管理框架Gerapy 的分享。

全书共 604 页,足足两斤重呢~ 定价为 99 元!

作者介绍

看书就先看看谁写的嘛,我们来了解一下~

崔庆才,静觅博客博主(https://cuiqingcai.com),博客 Python 爬虫博文阅读量已过千万,北京航空航天大学硕士,天善智能、网易云课堂讲师,微软小冰大数据工程师,有多个大型分布式爬虫项目经验,乐于技术分享,文章通俗易懂 ^_^

附皂片一张 ~(@^_^@)~

图文介绍

呕心沥血设计的宣传图也得放一下~

专家评论

书是好是坏,得让专家看评一评呀,那么下面就是几位专家的精彩评论,快来看看吧~

在互联网软件开发工程师的分类中,爬虫工程师是非常重要的。爬虫工作往往是一个公司核心业务开展的基础,数据抓取下来,才有后续的加工处理和最终展现。此时数据的抓取规模、稳定性、实时性、准确性就显得非常重要。早期的互联网充分开放互联,数据获取的难度很小。随着各大公司对数据资产日益看重,反爬水平也在不断提高,各种新技术不断给爬虫软件提出新的课题。本书作者对爬虫的各个领域都有深刻研究,书中探讨了Ajax数据的抓取、动态渲染页面的抓取、验证码识别、模拟登录等高级话题,同时也结合移动互联网的特点探讨了App的抓取等。更重要的是,本书提供了大量源码,可以帮助读者更好地理解相关内容。强烈推荐给各位技术爱好者阅读!

——梁斌,八友科技总经理

数据既是当今大数据分析的前提,也是各种人工智能应用场景的基础。得数据者得天下,会爬虫者走遍天下也不怕!一册在手,让小白到老司机都能有所收获!

——李舟军,北京航空航天大学教授,博士生导师

本书从爬虫入门到分布式抓取,详细介绍了爬虫技术的各个要点,并针对不同的场景提出了对应的解决方案。另外,书中通过大量的实例来帮助读者更好地学习爬虫技术,通俗易懂,干货满满。强烈推荐给大家!

——宋睿华,微软小冰首席科学家

有人说中国互联网的带宽全给各种爬虫占据了,这说明网络爬虫的重要性以及中国互联网数据封闭垄断的现状。爬是一种能力,爬是为了不爬。

——施水才,北京拓尔思信息技术股份有限公司总裁

全书目录

书的目录也有~ 看这里!

  • 1-开发环境配置
  • 1.1-Python3的安装
  • 1.2-请求库的安装
  • 1.3-解析库的安装
  • 1.4-数据库的安装
  • 1.5-存储库的安装
  • 1.6-Web库的安装
  • 1.7-App爬取相关库的安装
  • 1.8-爬虫框架的安装
  • 1.9-部署相关库的安装
  • 2-爬虫基础
  • 2.1-HTTP基本原理
  • 2.2-网页基础
  • 2.3-爬虫的基本原理
  • 2.4-会话和Cookies
  • 2.5-代理的基本原理
  • 3-基本库的使用
  • 3.1-使用urllib
  • 3.1.1-发送请求
  • 3.1.2-处理异常
  • 3.1.3-解析链接
  • 3.1.4-分析Robots协议
  • 3.2-使用requests
  • 3.2.1-基本用法
  • 3.2.2-高级用法
  • 3.3-正则表达式
  • 3.4-抓取猫眼电影排行
  • 4-解析库的使用
  • 4.1-使用XPath
  • 4.2-使用Beautiful Soup
  • 4.3-使用pyquery
  • 5-数据存储
  • 5.1-文件存储
  • 5.1.1-TXT文本存储
  • 5.1.2-JSON文件存储
  • 5.1.3-CSV文件存储
  • 5.2-关系型数据库存储
  • 5.2.1-MySQL存储
  • 5.3-非关系型数据库存储
  • 5.3.1-MongoDB存储
  • 5.3.2-Redis存储
  • 6-Ajax数据爬取
  • 6.1-什么是Ajax
  • 6.2-Ajax分析方法
  • 6.3-Ajax结果提取
  • 6.4-分析Ajax爬取今日头条街拍美图
  • 7-动态渲染页面爬取
  • 7.1-Selenium的使用
  • 7.2-Splash的使用
  • 7.3-Splash负载均衡配置
  • 7.4-使用Selenium爬取淘宝商品
  • 8-验证码的识别
  • 8.1-图形验证码的识别
  • 8.2-极验滑动验证码的识别
  • 8.3-点触验证码的识别
  • 8.4-微博宫格验证码的识别
  • 9-代理的使用
  • 9.1-代理的设置
  • 9.2-代理池的维护
  • 9.3-付费代理的使用
  • 9.4-ADSL拨号代理
  • 9.5-使用代理爬取微信公众号文章
  • 10-模拟登录
  • 10.1-模拟登录并爬取GitHub
  • 10.2-Cookies池的搭建
  • 11-App的爬取
  • 11.1-Charles的使用
  • 11.2-mitmproxy的使用
  • 11.3-mitmdump爬取“得到”App电子书信息
  • 11.4-Appium的基本使用
  • 11.5-Appium爬取微信朋友圈
  • 11.6-Appium+mitmdump爬取京东商品
  • 12-pyspider框架的使用
  • 12.1-pyspider框架介绍
  • 12.2-pyspider的基本使用
  • 12.3-pyspider用法详解
  • 13-Scrapy框架的使用
  • 13.1-Scrapy框架介绍
  • 13.2-Scrapy入门
  • 13.3-Selector的用法
  • 13.4-Spider的用法
  • 13.5-Downloader Middleware的用法
  • 13.6-Spider Middleware的用法
  • 13.7-Item Pipeline的用法
  • 13.8-Scrapy对接Selenium
  • 13.9-Scrapy对接Splash
  • 13.10-Scrapy通用爬虫
  • 13.11-Scrapyrt的使用
  • 13.12-Scrapy对接Docker
  • 13.13-Scrapy爬取新浪微博
  • 14-分布式爬虫
  • 14.1-分布式爬虫原理
  • 14.2-Scrapy-Redis源码解析
  • 14.3-Scrapy分布式实现
  • 14.4-Bloom Filter的对接
  • 15-分布式爬虫的部署
  • 15.1-Scrapyd分布式部署
  • 15.2-Scrapyd-Client的使用
  • 15.3-Scrapyd对接Docker
  • 15.4-Scrapyd批量部署
  • 15.5-Gerapy分布式管理

购买链接

想必很多小伙伴已经等了很久了,之前预售那么久也一直迟迟没有货,发售就有不少网店又售空了,不过现在起不用担心了!

书籍现已在京东、天猫、当当等网店上架并全面供应啦,复制链接到浏览器打开或扫描二维码打开即可购买了!

 京东商城

https://item.jd.com/12333540.html

 天猫商城

https://detail.tmall.com/item.htm?id=566699703917

当当网

http://product.dangdang.com/25249602.html

欢迎大家购买,谢谢支持!O(∩_∩)O

免费预览

不放心?想先看看有些啥,没问题!看这里:

免费章节试读:

将一直免费开放前7章节,欢迎大家试读!

好了,接下来就是我们的福利环节啦~

福利一:签名书!!!

恭喜你看到这里了!那么接下来的福利时间就到了!后面还有两个福利不容错过~

赠书活动第三波来袭,送 20 本作者亲笔签名书籍!!!

活动流程(重要,请一定认真阅读):

公众号进击的Coder回复 “赠书” 获取序列码参与活动,2018.7.24 22:00 截止,逾期参与无效,请记住您的序列码,这是您的唯一标识。

您可以转发活动页面邀请好友帮忙积攒人气值,最终取人气值前 20 位赠书,截止日期 2018.7.24 22:00,该时刻人气值前 20 位的朋友每人会获得签名书一本。

最终赠书名单会在微信公众号进击的Coder公布,届时请关注公众号消息!

福利二:独家优惠!!!

等等,你以为这就是全部福利吗?当然不是!除了抽奖送书,我们还拿到了拨号VPS知名品牌云立方的独家优惠,在公众号(进击的Coder )中回复:“优惠券”,即可免费领取云立方50元主机优惠券,数量有限,先到先得!优惠券可在云立方官网(www.yunlifang.cn)购买动态IP拨号VPS时抵扣现金,有了它,爬虫代理易如反掌!

你问我动态拨号VPS能做什么?应该怎么用在爬虫里?来这里了解一下:

轻松获得海量稳定代理!ADSL拨号代理的搭建

福利三:视频课程!!!

当然除了书籍,也有配套的视频课程,目前半价促销中,作者同样是崔庆才,二者结合学习效果更佳!限时优惠折扣中!扫描下图中二维码即可了解详情!

最后也是最重要的就是参与活动的地址了!!!快来扫码回复领取属于你的福利吧!!!

特别致谢

最后特别感谢云立方、天善智能对本活动的大力支持!

转载请注明:静觅 » 《Python3网络爬虫开发实战》第三波赠书活动来了!

Python glom包初探

$
0
0

大家好, 我不是崔老师,我是四毛,下面是我的个人公众号,欢迎大家关注。

好久没有写东西了,一直都记录在了自己的笔记上,这一篇是关于glom的一个介绍与初步使用,后期会将里面的各种API再给大家介绍下,同时,最近在搞爬虫的实时数据监控,也挺有意思,后面会和大家分享,敬请期待。

猛然发现,英语水平巅峰就在高考那一天。

因为是边看,边练习,然后翻译,所以个人理解可能有偏差,有错误的地方,请大家指正。

首先,这个库是用来处理一些嵌套的数据的,作者也在PyCon 2018上做了个分享,老美的PyCon还是有点质量的,不像国内的,搞的什么玩意。

视频地址:https://www.youtube.com/watch?v=bTAFl8P2DkE&t=18m07s

 

更新: 2018年7月28日10:32:08

经过咨询库的作者,在最后留的那个问题的准确解法如下:

import glom

target = {
    'data': {
        'name': 'just_test',
        'likes': [{'ball': 'basketball'},
                  {'ball': 'football'},
                  {'water': 'swim'}]
    }
}

spec = {
    'name' : ('data.name'),
    'likes' : ('data', 'likes', [glom.Coalesce('ball', 'water')])
}

print glom.glom(target, spec)
####
{'name': 'just_test', 'likes': ['basketball', 'football', 'swim']}

非常棒,准确来说就是得灵活运用Coalesce方法啊,不能太死板。非常Pythonic。

另附网址,作者有个很搞笑little four hair ,哈哈哈哈

Issue地址

 

1. 官方文档地址

文档地址

2. 安装方法

pip install glom

3. 正式开始

glom,官方的说法是用PYTHONIC的方式来处理内嵌的数据。对于现实世界中的数据处理更加给力,现实世界中的数据,我的理解就是AJAX越来越流行了,处理这类数据会越来越频繁。有如下特点:

  • 对于嵌套数据结构的基于路径式的访问
  • 可读,有意义的错误消息
  • 声明性数据转换,使用轻量级,Pythonic规范
  • 内置数据探索和调试功能

     3.1 原始处理嵌套数据

下面的脚本包导入

from glom import glom

下面的data就是个简单的嵌套数据,一般都可以用下面几种方法进行处理

data = {'a': {'b': {'c': 'd'}}}
data['a']['b']['c']
data.get('a').get('b').get('c')
data.get('a', {}).get('b',{}).get('c')

但是当我们的数据改变成下面的这样时:

data2 = {'a': {'b': None}}
data2['a']['b']['c']
Traceback (most recent call last):
...
TypeError: 'NoneType' object has no attribute '__getitem__'

会报错,而且由于是嵌套数据,从错误信息里我们只知道有个None值,但是到底谁是呢,是a,是b呢,反正肯定不是我们的朋友小哪吒。

    3.2 glom出场

那么glom怎么处理上面的数据呢?

如其所言,路径式:

data = {'a': {'b': {'c': 'd'}}}
print glom(data, 'a.b.c')  # d

看起来还是很优雅, 很Pythonic。

data2 = {'a': {'b': None}}
glom(data2, 'a.b.c')

错误信息如下:

glom.core.PathAccessError: could not access 'c', part 2 of Path('a', 'b', 'c'), got error: AttributeError("'NoneType' object has no attribute 'c'",)

很明显,这个错误就很直观。

难道仅仅只有这个?当然不是

     3.2.1  Going Beyond Access

上面的是原标题,我的理解是不仅仅获取数据,还有别的呢。

首先,介绍两个基本的术语

target 目标数据,可以是字典,列表,或其他任意的对象
spec  我们想要的输出格式 【specifications】, 定义你自己所需要的格式

现在让我们跟随宇航员的脚步,探索太阳系吧。

  • 获取某个行星的名字:

target = {'galaxy': {'system': {'planet': 'jupiter'}}}
# 这个格式就是需要个字段值,所以输出的就是个字段值
spec = 'galaxy.system.planet'
glom(target, spec)
# 'jupyter'

  • 现在,宇航员们想把行星的名字放进一个列表中,数据是这样:

target = {'system': {'planets': [{'name': 'earth'}, {'name': 'jupiter'}]}}

  • 通常,处理这样的话,都要写个循环,或者搞个列表解析式,那么glom怎么处理呢?

glom(target, ('system.planets', ['name']))
print glom(target, spec)
# ['earth', 'jupiter']

是不是很简单。那么现在新需求又来了,宇航员想得到下面这个数据里面的行星的卫星的数:

target = {'system': {'planets': [{'name': 'earth', 'moons': 1},
                                  {'name': 'jupiter', 'moons': 69}]}}

  • glom解决方法:

# 自定义的格式
spec = {'names': ('system.planets', ['name']),
        'moons': ('system.planets', ['moons'])}
print glom(target, spec)
# {'moons': [1, 69], 'names': ['earth', 'jupiter']}

     3.2.2  Changing Requirements

Coalesce 是glom定义的一种结构,允许我们对于spec中的子spec进行进一步的处理,你只要在子spec中将可能存在的值定义好就行了,听起来有点绕,现在来梳理一下。

  • 首先,子spec是什么?

spec = {'names': ('system.planets', ['name']),
         'moons': ('system.planets', ['moons'])}
# 以这个为例,这里面的system.planets就是个子spec

  • 然后,使用其解析数据:

target = {'system': {
    'planets': [{'name': 'earth', 'moons': 1}, {'name': 'jupiter', 'moons': 69}],
}
}
spec = {'names': (Coalesce('system.planets', 'system.dwarf_planets'), ['name']),
         'moons': (Coalesce('system.planets', 'system.dwarf_planets'), ['moons'])}

print glom(target, spec)
# {'moons': [1, 69], 'names': ['earth', 'jupiter']}

  • 接着当我们的数据变成了这个以后

target = {'system': {'dwarf_planets': [{'name': 'pluto', 'moons': 5},
                                        {'name': 'ceres', 'moons': 0}]}}
spec = {'names': (Coalesce('system.planets', 'system.dwarf_planets'), ['name']),
         'moons': (Coalesce('system.planets', 'system.dwarf_planets'), ['moons'])}
print glom(target, spec)
# {'moons': [5, 0], 'names': ['pluto', 'ceres']}

可以看到,依然可以使用相同的spec来解析不同的目标数据。

有意思的是,你可以在target里面同时写入plantes和dwarf_plants数据试试看,会返回什么数据。

【这里应该是个惰性的匹配,只要匹配到一个,后面的就不再去匹配了】

     3.2.3  True Python Native

真正的原生python

在glom里面,你可以传值给python里面的任意的函数

举例:

  • 求和

target = {'system': {'planets': [{'name': 'earth', 'moons': 1},
                                  {'name': 'jupiter', 'moons': 69}]}}

print glom(target, {'moon_count': ('system.planets', ['moons'], sum)})

# {'moon_count': 70}

原教程这里还有个案例,但是我还没有理解好,就不写出来了,大家可以点击链接自己看一下。

 

4. 结论

下一节,为大家带来其中一些重要的函数。

最后,在用的过程中,一直有个疑问,数据如下:

target = {
    'data': {
        'name': 'just_test',
        'likes': [{'ball': 'basketball'},
                  {'ball': 'football'},
                  {'water': 'swim'}]
    }
}

现在,我想返回的数据格式为:

{'name': 'just_for_test', 'likes': ['basketball', 'football', 'water']}

一开始我以为可以这么用:

spec = {
    'name': ('data.name'),
    'likes': ('data.likes', ['ball', 'water'] ),
}

但是不行,这样会报错。后来用了另外的方法:

spec = {
    'name': ('data.name'),
    'likes': ('data.likes', [lambda x: x.values()[0] if 'ball' or 'water' in x.keys() else ''] ),
}

print glom(target, spec)
# {'name': 'just_test', 'likes': ['basketball', 'football', 'swim']}

这样感觉很不爽啊,还望会的同学不吝赐教啊。

转载请注明:静觅 » Python glom包初探

Elasticsearch 基本介绍及其与 Python 的对接实现

$
0
0

什么是 Elasticsearch

想查数据就免不了搜索,搜索就离不开搜索引擎,百度、谷歌都是一个非常庞大复杂的搜索引擎,他们几乎索引了互联网上开放的所有网页和数据。然而对于我们自己的业务数据来说,肯定就没必要用这么复杂的技术了,如果我们想实现自己的搜索引擎,方便存储和检索,Elasticsearch 就是不二选择,它是一个全文搜索引擎,可以快速地储存、搜索和分析海量数据。

为什么要用 Elasticsearch

Elasticsearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。

那 Lucene 又是什么?Lucene 可能是目前存在的,不论开源还是私有的,拥有最先进,高性能和全功能搜索引擎功能的库,但也仅仅只是一个库。要用上 Lucene,我们需要编写 Java 并引用 Lucene 包才可以,而且我们需要对信息检索有一定程度的理解才能明白 Lucene 是怎么工作的,反正用起来没那么简单。

那么为了解决这个问题,Elasticsearch 就诞生了。Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目标是使全文检索变得简单,相当于 Lucene 的一层封装,它提供了一套简单一致的 RESTful API 来帮助我们实现存储和检索。

所以 Elasticsearch 仅仅就是一个简易版的 Lucene 封装吗?那就大错特错了,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确的形容:

  • 一个分布式的实时文档存储,每个字段可以被索引与搜索
  • 一个分布式实时分析搜索引擎
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据

总之,是一个相当牛逼的搜索引擎,维基百科、Stack Overflow、GitHub 都纷纷采用它来做搜索。

Elasticsearch 的安装

我们可以到 Elasticsearch 的官方网站下载 Elasticsearch:https://www.elastic.co/downloads/elasticsearch,同时官网也附有安装说明。

首先把安装包下载下来并解压,然后运行 bin/elasticsearch(Mac 或 Linux)或者 bin\elasticsearch.bat (Windows) 即可启动 Elasticsearch 了。

我使用的是 Mac,Mac 下个人推荐使用 Homebrew 安装:

brew install elasticsearch

Elasticsearch 默认会在 9200 端口上运行,我们打开浏览器访问
http://localhost:9200/ 就可以看到类似内容:

{
  "name" : "atntrTf",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "e64hkjGtTp6_G2h1Xxdv5g",
  "version" : {
    "number": "6.2.4",
    "build_hash": "ccec39f",
    "build_date": "2018-04-12T20:37:28.497551Z",
    "build_snapshot": false,
    "lucene_version": "7.2.1",
    "minimum_wire_compatibility_version": "5.6.0",
    "minimum_index_compatibility_version": "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

如果看到这个内容,就说明 Elasticsearch 安装并启动成功了,这里显示我的 Elasticsearch 版本是 6.2.4 版本,版本很重要,以后安装一些插件都要做到版本对应才可以。

接下来我们来了解一下 Elasticsearch 的基本概念以及和 Python 的对接。

Elasticsearch 相关概念

在 Elasticsearch 中有几个基本的概念,如节点、索引、文档等等,下面来分别说明一下,理解了这些概念对熟悉 Elasticsearch 是非常有帮助的。

Node 和 Cluster

Elasticsearch 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elasticsearch 实例。

单个 Elasticsearch 实例称为一个节点(Node)。一组节点构成一个集群(Cluster)。

Index

Elasticsearch 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。

所以,Elasticsearch 数据管理的顶层单位就叫做 Index(索引),其实就相当于 MySQL、MongoDB 等里面的数据库的概念。另外值得注意的是,每个 Index (即数据库)的名字必须是小写。

Document

Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。

Document 使用 JSON 格式表示,下面是一个例子。

同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。

Type

Document 可以分组,比如 weather 这个 Index 里面,可以按城市分组(北京和上海),也可以按气候分组(晴天和雨天)。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document,类似 MySQL 中的数据表,MongoDB 中的 Collection。

不同的 Type 应该有相似的结构(Schema),举例来说,id 字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的一个区别。性质完全不同的数据(比如 products 和 logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。

根据规划,Elastic 6.x 版只允许每个 Index 包含一个 Type,7.x 版将会彻底移除 Type。

Fields

即字段,每个 Document 都类似一个 JSON 结构,它包含了许多字段,每个字段都有其对应的值,多个字段组成了一个 Document,其实就可以类比 MySQL 数据表中的字段。

在 Elasticsearch 中,文档归属于一种类型(Type),而这些类型存在于索引(Index)中,我们可以画一些简单的对比图来类比传统关系型数据库:

Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices   -> Types  -> Documents -> Fields

以上就是 Elasticsearch 里面的一些基本概念,通过和关系性数据库的对比更加有助于理解。

Python 对接 Elasticsearch

Elasticsearch 实际上提供了一系列 Restful API 来进行存取和查询操作,我们可以使用 curl 等命令来进行操作,但毕竟命令行模式没那么方便,所以这里我们就直接介绍利用 Python 来对接 Elasticsearch 的相关方法。

Python 中对接 Elasticsearch 使用的就是一个同名的库,安装方式非常简单:

pip3 install elasticsearch

官方文档是:https://elasticsearch-py.readthedocs.io/,所有的用法都可以在里面查到,文章后面的内容也是基于官方文档来的。

创建 Index

我们先来看下怎样创建一个索引(Index),这里我们创建一个名为 news 的索引:

from elasticsearch import Elasticsearch

es = Elasticsearch()
result = es.indices.create(index='news', ignore=400)
print(result)

如果创建成功,会返回如下结果:

{'acknowledged': True, 'shards_acknowledged': True, 'index': 'news'}

返回结果是 JSON 格式,其中的 acknowledged 字段表示创建操作执行成功。

但这时如果我们再把代码执行一次的话,就会返回如下结果:

{'error': {'root_cause': [{'type': 'resource_already_exists_exception', 'reason': 'index [news/QM6yz2W8QE-bflKhc5oThw] already exists', 'index_uuid': 'QM6yz2W8QE-bflKhc5oThw', 'index': 'news'}], 'type': 'resource_already_exists_exception', 'reason': 'index [news/QM6yz2W8QE-bflKhc5oThw] already exists', 'index_uuid': 'QM6yz2W8QE-bflKhc5oThw', 'index': 'news'}, 'status': 400}

它提示创建失败,status 状态码是 400,错误原因是 Index 已经存在了。

注意这里我们的代码里面使用了 ignore 参数为 400,这说明如果返回结果是 400 的话,就忽略这个错误不会报错,程序不会执行抛出异常。

假如我们不加 ignore 这个参数的话:

es = Elasticsearch()
result = es.indices.create(index='news')
print(result)

再次执行就会报错了:

raise HTTP_EXCEPTIONS.get(status_code, TransportError)(status_code, error_message, additional_info)
elasticsearch.exceptions.RequestError: TransportError(400, 'resource_already_exists_exception', 'index [news/QM6yz2W8QE-bflKhc5oThw] already exists')

这样程序的执行就会出现问题,所以说,我们需要善用 ignore 参数,把一些意外情况排除,这样可以保证程序的正常执行而不会中断。

删除 Index

删除 Index 也是类似的,代码如下:

from elasticsearch import Elasticsearch

es = Elasticsearch()
result = es.indices.delete(index='news', ignore=[400, 404])
print(result)

这里也是使用了 ignore 参数,来忽略 Index 不存在而删除失败导致程序中断的问题。

如果删除成功,会输出如下结果:

{'acknowledged': True}

如果 Index 已经被删除,再执行删除则会输出如下结果:

{'error': {'root_cause': [{'type': 'index_not_found_exception', 'reason': 'no such index', 'resource.type': 'index_or_alias', 'resource.id': 'news', 'index_uuid': '_na_', 'index': 'news'}], 'type': 'index_not_found_exception', 'reason': 'no such index', 'resource.type': 'index_or_alias', 'resource.id': 'news', 'index_uuid': '_na_', 'index': 'news'}, 'status': 404}

这个结果表明当前 Index 不存在,删除失败,返回的结果同样是 JSON,状态码是 400,但是由于我们添加了 ignore 参数,忽略了 400 状态码,因此程序正常执行输出 JSON 结果,而不是抛出异常。

插入数据

Elasticsearch 就像 MongoDB 一样,在插入数据的时候可以直接插入结构化字典数据,插入数据可以调用 create() 方法,例如这里我们插入一条新闻数据:

from elasticsearch import Elasticsearch

es = Elasticsearch()
es.indices.create(index='news', ignore=400)

data = {'title': '美国留给伊拉克的是个烂摊子吗', 'url': 'http://view.news.qq.com/zt2011/usa_iraq/index.htm'}
result = es.create(index='news', doc_type='politics', id=1, body=data)
print(result)

这里我们首先声明了一条新闻数据,包括标题和链接,然后通过调用 create() 方法插入了这条数据,在调用 create() 方法时,我们传入了四个参数,index 参数代表了索引名称,doc_type 代表了文档类型,body 则代表了文档具体内容,id 则是数据的唯一标识 ID。

运行结果如下:

{'_index': 'news', '_type': 'politics', '_id': '1', '_version': 1, 'result': 'created', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 0, '_primary_term': 1}

结果中 result 字段为 created,代表该数据插入成功。

另外其实我们也可以使用 index() 方法来插入数据,但与 create() 不同的是,create() 方法需要我们指定 id 字段来唯一标识该条数据,而 index() 方法则不需要,如果不指定 id,会自动生成一个 id,调用 index() 方法的写法如下:

es.index(index='news', doc_type='politics', body=data)

create() 方法内部其实也是调用了 index() 方法,是对 index() 方法的封装。

更新数据

更新数据也非常简单,我们同样需要指定数据的 id 和内容,调用 update() 方法即可,代码如下:

from elasticsearch import Elasticsearch

es = Elasticsearch()
data = {
    'title': '美国留给伊拉克的是个烂摊子吗',
    'url': 'http://view.news.qq.com/zt2011/usa_iraq/index.htm',
    'date': '2011-12-16'
}
result = es.update(index='news', doc_type='politics', body=data, id=1)
print(result)

这里我们为数据增加了一个日期字段,然后调用了 update() 方法,结果如下:

{'_index': 'news', '_type': 'politics', '_id': '1', '_version': 2, 'result': 'updated', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 1, '_primary_term': 1}

可以看到返回结果中,result 字段为 updated,即表示更新成功,另外我们还注意到有一个字段 _version,这代表更新后的版本号数,2 代表这是第二个版本,因为之前已经插入过一次数据,所以第一次插入的数据是版本 1,可以参见上例的运行结果,这次更新之后版本号就变成了 2,以后每更新一次,版本号都会加 1。

另外更新操作其实利用 index() 方法同样可以做到,写法如下:

es.index(index='news', doc_type='politics', body=data, id=1)

可以看到,index() 方法可以代替我们完成两个操作,如果数据不存在,那就执行插入操作,如果已经存在,那就执行更新操作,非常方便。

删除数据

如果想删除一条数据可以调用 delete() 方法,指定需要删除的数据 id 即可,写法如下:

from elasticsearch import Elasticsearch

es = Elasticsearch()
result = es.delete(index='news', doc_type='politics', id=1)
print(result)

运行结果如下:

{'_index': 'news', '_type': 'politics', '_id': '1', '_version': 3, 'result': 'deleted', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 2, '_primary_term': 1}

可以看到运行结果中 result 字段为 deleted,代表删除成功,_version 变成了 3,又增加了 1。

查询数据

上面的几个操作都是非常简单的操作,普通的数据库如 MongoDB 都是可以完成的,看起来并没有什么了不起的,Elasticsearch 更特殊的地方在于其异常强大的检索功能。

对于中文来说,我们需要安装一个分词插件,这里使用的是 elasticsearch-analysis-ik,GitHub 链接为:https://github.com/medcl/elasticsearch-analysis-ik,这里我们使用 Elasticsearch 的另一个命令行工具 elasticsearch-plugin 来安装,这里安装的版本是 6.2.4,请确保和 Elasticsearch 的版本对应起来,命令如下:

elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.2.4/elasticsearch-analysis-ik-6.2.4.zip

这里的版本号请替换成你的 Elasticsearch 的版本号。

安装之后重新启动 Elasticsearch 就可以了,它会自动加载安装好的插件。

首先我们新建一个索引并指定需要分词的字段,代码如下:

from elasticsearch import Elasticsearch

es = Elasticsearch()
mapping = {
    'properties': {
        'title': {
            'type': 'text',
            'analyzer': 'ik_max_word',
            'search_analyzer': 'ik_max_word'
        }
    }
}
es.indices.delete(index='news', ignore=[400, 404])
es.indices.create(index='news', ignore=400)
result = es.indices.put_mapping(index='news', doc_type='politics', body=mapping)
print(result)

这里我们先将之前的索引删除了,然后新建了一个索引,然后更新了它的 mapping 信息,mapping 信息中指定了分词的字段,指定了字段的类型 type 为 text,分词器 analyzer 和 搜索分词器 search_analyzer 为 ik_max_word,即使用我们刚才安装的中文分词插件。如果不指定的话则使用默认的英文分词器。

接下来我们插入几条新的数据:

datas = [
    {
        'title': '美国留给伊拉克的是个烂摊子吗',
        'url': 'http://view.news.qq.com/zt2011/usa_iraq/index.htm',
        'date': '2011-12-16'
    },
    {
        'title': '公安部:各地校车将享最高路权',
        'url': 'http://www.chinanews.com/gn/2011/12-16/3536077.shtml',
        'date': '2011-12-16'
    },
    {
        'title': '中韩渔警冲突调查:韩警平均每天扣1艘中国渔船',
        'url': 'https://news.qq.com/a/20111216/001044.htm',
        'date': '2011-12-17'
    },
    {
        'title': '中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首',
        'url': 'http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml',
        'date': '2011-12-18'
    }
]

for data in datas:
    es.index(index='news', doc_type='politics', body=data)

这里我们指定了四条数据,都带有 title、url、date 字段,然后通过 index() 方法将其插入 Elasticsearch 中,索引名称为 news,类型为 politics。

接下来我们根据关键词查询一下相关内容:

result = es.search(index='news', doc_type='politics')
print(result)

可以看到查询出了所有插入的四条数据:

{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 1.0,
    "hits": [
      {
        "_index": "news",
        "_type": "politics",
        "_id": "c05G9mQBD9BuE5fdHOUT",
        "_score": 1.0,
        "_source": {
          "title": "美国留给伊拉克的是个烂摊子吗",
          "url": "http://view.news.qq.com/zt2011/usa_iraq/index.htm",
          "date": "2011-12-16"
        }
      },
      {
        "_index": "news",
        "_type": "politics",
        "_id": "dk5G9mQBD9BuE5fdHOUm",
        "_score": 1.0,
        "_source": {
          "title": "中国驻洛杉矶领事馆遭亚裔男子枪击,嫌犯已自首",
          "url": "http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml",
          "date": "2011-12-18"
        }
      },
      {
        "_index": "news",
        "_type": "politics",
        "_id": "dU5G9mQBD9BuE5fdHOUj",
        "_score": 1.0,
        "_source": {
          "title": "中韩渔警冲突调查:韩警平均每天扣1艘中国渔船",
          "url": "https://news.qq.com/a/20111216/001044.htm",
          "date": "2011-12-17"
        }
      },
      {
        "_index": "news",
        "_type": "politics",
        "_id": "dE5G9mQBD9BuE5fdHOUf",
        "_score": 1.0,
        "_source": {
          "title": "公安部:各地校车将享最高路权",
          "url": "http://www.chinanews.com/gn/2011/12-16/3536077.shtml",
          "date": "2011-12-16"
        }
      }
    ]
  }
}

可以看到返回结果会出现在 hits 字段里面,然后其中有 total 字段标明了查询的结果条目数,还有 max_score 代表了最大匹配分数。

另外我们还可以进行全文检索,这才是体现 Elasticsearch 搜索引擎特性的地方:

dsl = {
    'query': {
        'match': {
            'title': '中国 领事馆'
        }
    }
}

es = Elasticsearch()
result = es.search(index='news', doc_type='politics', body=dsl)
print(json.dumps(result, indent=2, ensure_ascii=False))

这里我们使用 Elasticsearch 支持的 DSL 语句来进行查询,使用 match 指定全文检索,检索的字段是 title,内容是“中国领事馆”,搜索结果如下:

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 2.546152,
    "hits": [
      {
        "_index": "news",
        "_type": "politics",
        "_id": "dk5G9mQBD9BuE5fdHOUm",
        "_score": 2.546152,
        "_source": {
          "title": "中国驻洛杉矶领事馆遭亚裔男子枪击,嫌犯已自首",
          "url": "http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml",
          "date": "2011-12-18"
        }
      },
      {
        "_index": "news",
        "_type": "politics",
        "_id": "dU5G9mQBD9BuE5fdHOUj",
        "_score": 0.2876821,
        "_source": {
          "title": "中韩渔警冲突调查:韩警平均每天扣1艘中国渔船",
          "url": "https://news.qq.com/a/20111216/001044.htm",
          "date": "2011-12-17"
        }
      }
    ]
  }
}

这里我们看到匹配的结果有两条,第一条的分数为 2.54,第二条的分数为 0.28,这是因为第一条匹配的数据中含有“中国”和“领事馆”两个词,第二条匹配的数据中不包含“领事馆”,但是包含了“中国”这个词,所以也被检索出来了,但是分数比较低。

因此可以看出,检索时会对对应的字段全文检索,结果还会按照检索关键词的相关性进行排序,这就是一个基本的搜索引擎雏形。

另外 Elasticsearch 还支持非常多的查询方式,详情可以参考官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/6.3/query-dsl.html

以上便是对 Elasticsearch 的基本介绍以及 Python 操作 Elasticsearch 的基本用法,但这仅仅是 Elasticsearch 的基本功能,它还有更多强大的功能等待着我们的探索,后面会继续更新,敬请期待。

本节代码:https://github.com/Germey/ElasticSearch

资料推荐

另外推荐几个不错的学习站点:

参考资料

转载请注明:静觅 » Elasticsearch 基本介绍及其与 Python 的对接实现

快来学习怎么可视化监控你的爬虫

$
0
0

大家好,我是四毛,下面是我的个人公众号,欢迎关注。有问题的可以私信我,看到就会回复。

 

更新 2018年08月03日14:39:32

其实可以利用scrapy的扩展展示更多的数据,立个flag,后面更新上来

 

好,开始今天的文章。

今天主要是来说一下怎么可视化来监控你的爬虫的状态

相信大家在跑爬虫的过程中,也会好奇自己养的爬虫一分钟可以爬多少页面多大的数据量,当然查询的方式多种多样。今天我来讲一种可视化的方法。

关于爬虫数据在mongodb里的版本我写了一个可以热更新配置的版本,即添加了新的爬虫配置以后,不用重启程序,即可获取刚刚添加的爬虫的状态数据,大家可以通过关注我的公众号以后, 回复“可视化”即可获取脚本地址

1.成品图

 

这个是监控服务器网速的最后成果,显示的是下载与上传的网速,单位为M。爬虫的原理都是一样的,只不过将数据存到InfluxDB的方式不一样而已, 如下图。

可以实现对爬虫数量,增量,大小,大小增量的实时监控。

 

2. 环境

  • InfluxDb,是目前比较流行的时间序列数据库;
  • Grafana,一个可视化面板(Dashboard),有着非常漂亮的图表和布局展示,功能齐全的度量仪表盘和图形编辑器,支持Graphite、zabbix、InfluxDB、Prometheus和OpenTSDB作为数据源
  • Ubuntu
  • influxdb(pip install influxdb)
  • Python 2.7

 

3. 原理

获取要展示的数据,包含当前的时间数据,存到InfluxDb里面,然后再到Grafana里面进行相应的配置即可展示;

4. 安装

    4.1 Grafana安装

             官方安装指导

安装好以后,打开本地的3000端口,即可进入管理界面,用户名与密码都是admin

     4.2 InfulxDb安装

这个安装就网上自己找吧,有很多的配置我都没有配置,就不在这里误人子弟了。

5. InfluxDb简单操作

碰到了数据库,肯定要把增删改查学会了啊, 和sql几乎一样,只有一丝丝的区别,具体操作,大家可以参考官方的文档。

  • influx 进入命令行
  • CREATE DATABASE test 创建数据库
  • show databases 查看数据库
  • use test 使用数据库
  • show series 看表
  • select * from table_test 选择数据
  • DROP MEASUREMENT table_test 删表

6. 存数据

InfluxDb数据库的数据有一定的格式,因为我都是利用python库进行相关操作,所以下面将在python中的格式展示一下:

json_body = [
    {
        "measurement": "crawler",
        "time": current_time,
        "tags": {
            "spider_name": collection_name
        },
        "fields": {
            "count": current_count,
            "increase_count": increase_amount,
            "size": co_size,
            "increase_size": increase_co_size

        }
    }
]

其中:

  • measurement, 表名
  • time,时间
  • tags,标签
  • fields,字段

可以看到,就是个列表里面,嵌套了一个字典。其中,对于时间字段,有特殊要求,可以参考这里, 下面是python实现方法:

from datetime import datetime
current_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')

所以,到这里,如何将爬虫的相关属性存进去呢?以MongoDB为例

mongodb_client = pymongo.MongoClient(uri)
    for db_name, collection_name in dbs_and_cos.iteritems():
        # 数据库操作
        db = mongodb_client[db_name]
        co = db[collection_name]
        # 集合大小
        co_size = round(float(db.command("collstats", collection_name).get('size')) / 1024 / 1024, 2)
        # 集合内数据条数
        current_count = co.count()

        # 初始化,当程序刚执行时,初始量就设置为第一次执行时获取的数据
        init_count = _count_dict.get(collection_name, current_count)
        # 初始化,当程序刚执行时,初始量就设置为第一次执行时获取的数据大小
        init_size = _size_dict.get(collection_name, co_size)

        # 条数增长量
        increase_amount = current_count - init_count
        # 集合大小增长量
        increase_co_size = co_size - init_size

        current_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')

        # 赋值
        _size_dict[collection_name] = co_size
        _count_dict[collection_name] = current_count

        json_body = [
            {
                "measurement": "crawler",
                "time": current_time,
                "tags": {
                    "spider_name": collection_name
                },
                "fields": {
                    "count": current_count,
                    "increase_count": increase_amount,
                    "size": co_size,
                    "increase_size": increase_co_size

                }
            }
        ]
        print json_body
        client.write_points(json_body)

完整代码,关注上面的公众号,发送“”可视化“”即可获取。

那么现在我们已经往数据里存了数据了,那么接下来要做的就是把存的数据展示出来。

7.展示数据

7.1 配置数据源

以admin登录到Grafana的后台后,我们首先需要配置一下数据源。点击左边栏的最下面的按钮,然后点击DATA SOURCES,这样就可以进入下面的页面:

点击ADD DATA SOURCE,进行配置即可,如下图:

其中,name自行设定;Type 选择InfluxDB;url为默认的http://localhost:8086, 其他的因为我前面没有进行配置,所以默认的即可。然后在InfluxDB Details里的填入Database名,最后点击测试,如果没有报错的话,则可以进入下一步的展示数据了;

7.2 展示数据

点击左边栏的+号,然后点击GRAPH

接着点击下图中的edit进入编辑页面:

从上图中可以发现:

  •   中间板块是最后的数据展示
  • 下面是数据的设置项
  • 右上角是展示时间的设置板块,在这里可以选择要展示多久的数据

7.2.1 配置数据

  1. 在Data Source中选择刚刚在配置数据源的时候配置的NAME字段,而不是database名。
  2. 接着在下面选择要展示的数据。看着就很熟悉是不是,完全是sql语句的可视化。同时,当我们的数据放到相关的字段上的时候,双击,就会把可以选择的项展示出来了,我们要做的就是直接选择即可;
  3. 设置右上角的时间,则可以让数据实时进行更新与展示

因为下面的配置实质就是sql查询语句,所以大家按照自己的需求,进行选择配置即可,当配置完以后,就可以在中间的面板里面看到数据了。

 

 

8. 总结

到这里,本篇文章就结束了。其中,对于Grafana的操作我没有介绍的很详细,因为本篇主要讲的是怎么利用这几个工具完成我们的任务。

同时,里面的功能确实很多,还有可以安装的插件。我自己目前还是仅仅对于用到的部分比较了解,所以大家可以查询官方的或者别的教程资料来对Grafana进行更深入的了解,制作出更加好看的可视化作品来。

最后,关注公众号,回复“可视化” 即可获取本文代码哦

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

转载请注明:静觅 » 快来学习怎么可视化监控你的爬虫

Viewing all 277 articles
Browse latest View live