0%

技术杂谈

最近刚遇到个问题,我要给自己做的网站加个无限 debugger 反爬,网站是基于 Vue.js 开发的,比如我就想在 main.js 里面加这么一段:

1
2
3
4
setInterval(() => {
debugger
console.log('debugger')
}, 1000)

当时在 Debug 环境下一切好好的,但是到了 build 之后,再运行发现 debugger 就没了,这就神奇了。

我搜了很久,最后终于找到了解决方案,这里记录下。

开发环境和生产环境

这里首先说下 Vue.js 是有开发环境和生产环境之分的,这里它用了一个关键词,叫做 mode。

我们先来看看两个常用的命令:

1
2
npm run serve
npm run build

这两个命令如果大家开发 Vue.js 的话一定不会陌生,但它们是怎么实现的呢?

顺着一找,其实他们定义在了 package.json 里面,是这样的:

1
2
3
4
5
6
7
8
9
10
{
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
...
}

这里其实就是调用了 vue-cli-service 的几个命令而已。

vue-cli-service 又是哪里来的呢?很简单,在刚开始初始化项目的时候装了个 Vue CLI:

1
2
3
npm install -g @vue/cli
# OR
yarn global add @vue/cli

它提供了 vue-cli-service 这个命令。

然后我们再来详细看看这个 serve 和 build 命令。

serve

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
Usage: vue-cli-service serve [options] [entry]

Options:

--open open browser on server start
--copy copy url to clipboard on server start
--mode specify env mode (default: development)
--host specify host (default: 0.0.0.0)
--port specify port (default: 8080)
--https use https (default: false)
--public specify the public network URL for the HMR client
--skip-plugins comma-separated list of plugin names to skip for this run

看到了吧,这里有个 mode,指的就是运行环境,这里默认为 development,即开发环境。

build

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Usage: vue-cli-service build [options] [entry|pattern]

Options:

--mode specify env mode (default: production)
--dest specify output directory (default: dist)
--modern build app targeting modern browsers with auto fallback
--no-unsafe-inline build app without introducing inline scripts
--target app | lib | wc | wc-async (default: app)
--formats list of output formats for library builds (default: commonjs,umd,umd-min)
--inline-vue include the Vue module in the final bundle of library or web component target
--name name for lib or web-component mode (default: "name" in package.json or entry filename)
--filename file name for output, only usable for 'lib' target (default: value of --name),
--no-clean do not remove the dist directory before building the project
--report generate report.html to help analyze bundle content
--report-json generate report.json to help analyze bundle content
--skip-plugins comma-separated list of plugin names to skip for this run
--watch watch for changes

这里也有一个 mode,默认就是 production 了,即生产环境。

所以,到这里我们就明白了,调用 build 命令之后,实际上是生产环境了,然后生产环境可能做了一些特殊的配置,把一些 debugger 给去除了,所以就没了。

还原

那咋还原呢?

这里我们就需要用到 Vue.js 的另外一个知识了。

Vue.js 同样是基于 Webpack 构建的,利用了 Webpack 的打包技术,不过为了更加方便开发者配置,Vue.js 在 Webpack 的基础上又封装了一层,一些配置我们不需要再实现 webpack.config.js 了,而是可以实现 vue.config.js,配置更加简单。

在 vue.config.js 里面,它为 Webpack 暴露了几个重要的配置入口,一个就是 configureWebpack,一个是 chainWebpack。

具体的教程大家可以参考官方文档:https://cli.vuejs.org/zh/guide/webpack.html。

比如前者可以这么配置:

1
2
3
4
5
6
7
module.exports = {
configureWebpack: {
plugins: [
new MyAwesomeWebpackPlugin()
]
}
}

后者可以这么配置:

1
2
3
4
5
6
7
8
9
10
11
12
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => {
return options
})
}
}

这里,我们如果要修改 Webpack 构建的一些配置的话,可以利用 chainWebpack。

TerserPlugin

然后,这里又需要引入另外一个 Webpack 插件了,叫做 TerserPlugin,官方介绍链接为 https://webpack.js.org/plugins/terser-webpack-plugin/。

而这个库又是依赖 terser 的,官方介绍链接为 https://github.com/terser/terser。

官方介绍为:

A JavaScript parser and mangler/compressor toolkit for ES6+.

OK,反正就是类似一个 JavaScript 压缩转换器,比如它可以将一些 JavaScript 代码转码、混淆、压缩等等。

这里我们就需要借助于它来实现 debugger 的还原。

这里由于我使用的 Webpack 是 4.x 版本,所以 TerserPlugin 也需要是 4.x 版本,5.x 版本我测试过了不行。

安装配置如下,添加到 package.json 的 devDependencies 里面:

1
"terser-webpack-plugin": "^4.2.3",

然后:

1
npm install

接着 vue.config.js 改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
...
productionSourceMap: false,
chainWebpack: config => {
config.optimization.minimizer([
new TerserPlugin({
terserOptions: {
mangle: true,
compress: {
drop_debugger: false
}
}
})
]
)
}
}

这里我就保留了 chainWebpack 的配置,然后这里面通过 config 的 optimization 的 minimizer 方法配置了 plugins,然后这里 TerserPlugin 需要声明一个 terserOptions,然后 compress 里面的 drop_debugger 需要设置为 false。

这样,生产环境的 debugger 语句就不会丢了。

技术杂谈

2019 年,网红王尼玛做了一个五分钟的视频,是有关程序员的综艺节目《创造1024》,视频冲上热搜,被转发了几十万次,而这个视频,通篇是对程序员的刻板印象和标签化再加深。

视频里,程序员是这样子的:

视频里面每个程序员都是标配的黑眼圈,更有甚者那个连续七天七夜不眠不休最后累倒的程序员被推崇为 C 位大佬。「没女朋友」、「宅男」、「呆板」、「理工男」、「格子衫」、「没头发」 等等这些标签。

我不是吐槽视频本身,通过这个视频,站在另外的角度去思考,这里面确实有很多让我感觉不那么舒服的地方。

  • 第一,把女性程序员排除在外;
  • 第二,把整个行业的形象刻画的呆板、邋遢;
  • 第三,为什么程序员就一定会被标签化为这样的形象;

有的时候真的非常伤心,互联网时代真的来临了,程序员这个行业走上台前了。

然而走上台前的,不是曾经想用产品改变世界的梦想,而是秃头、加班、格子衫。

看到一些微博还有这样的:

所以一直到现在,我都对格子衫有着非常深刻的生理性反感,我有条纹衬衫,也有花衬衫,但就是不喜欢格子衫,因为每当我去穿格子衫的时候,我就觉得我走进了公众预设定的无聊框架中。因此,每当女朋友陪我买衣服的时候,路过格子衫的区域,我一定不会去选。

嘿嘿没错,我还有女朋友,谁说程序员没有女朋友的?况且,我女朋友还那么好看。

哦哦跑题了,说回格子衫,我现在选择不穿格子衫,其实也是被框架捆绑的一部分,可我又能怎么办呢?

当然,logo卫衣还是很赞。

当然,在提到程序员这个行业的时候,最大也是最让我讨厌的刻板影响,叫程序员安慰师。

程序员安慰师这个所谓的行业和所谓的热搜热转出来的时候,是我觉得最荒谬的事情,到底是谁觉得我们的智商和情商,我们对工作的热情和严谨,是被女性带动的。每个男性都喜欢漂亮的女孩,这是原始的性,但在这之上,我们如此优秀完成工作的事情,是我们聪明的大脑(掐腰)。

最后我想说,我仍然能看到有非常多的调研报告,有越来越多的小姐姐开始在相亲中去倾向程序员这个行业。

但我想说,所谓高薪,所谓单纯,所谓不会出轨,都是外界给我们的一个个贴下的标签,而在这标签下,我们仍然是独立而特别的个体。希望你最后的选择是因为我这个人,而并不是那些无聊的刻板印象。

我们程序员并不一定是秃头、呆滞、毫无生活情趣的死肥宅,至少说很多都不是,我们也有可爱的一面,也有自己热爱的生活,喜欢的人,喜欢的事,请行业不要再这么标签化我们。

1024,我既不想接受格子衫与人体工学椅的安利,也不想接受任何《程序员图鉴》的调侃。我希望节日能让公司反思过高强度的加班,我希望节日能给更多的人传输目前程序届的开源思想,我希望节日能让人们回忆起,在疫情时候,有那么多个体的程序员不记薪资帮助做统计和维护框架,我希望节日能让更多人意识到我们每一个有灵魂的个体本身。

最后,整篇发言稿都是我的一家之言,是我的三观输出,可能会有很多朋友不习惯或者不开心。用程序码出世界,始终是我的理想。

无论如何,祝大家 1024 节日快乐!

使用Hexo编写博客?

亲爱的伙伴您好,很荣幸能与您在此相遇.

本文主要用于记录,在一台完全的“新”机子上,书写Hexo部署博客。

我已经迫不及待想告诉你的几个容易错误的点,你准备好了么?

Let’s Go

在此之前说明,很多东西都是来自网络,当然无论如何还是建议你查阅官方文档。

Hexo官方文档

Fisrt 环境准备

  • [ ] Git
  • [ ] Node.js (Node.js 版本需不低于 10.13,建议使用 Node.js 12.0 及以上版本)
  • [ ] Hexo

Git:

可以参考此文章 https://cuiqingcai.com/9336.html

Mac 用户

如果在编译时可能会遇到问题,请先到 App Store 安装 Xcode,Xcode 完成后,启动并进入 Preferences -> Download -> Command Line Tools -> Install 安装命令行工具。

Node.JS:

Node.js 为大多数平台提供了官方的 安装程序。对于中国大陆地区用户,可以前往 淘宝 Node.js 镜像 下载。

其它的安装方法:

  • Windows:通过 nvs(推荐)或者nvm 安装。
  • Mac:使用 HomebrewMacPorts 安装。
  • Linux(DEB/RPM-based):从 NodeSource 安装。
  • 其它:使用相应的软件包管理器进行安装,可以参考由 Node.js 提供的 指导

Hexo:

所有必备的应用程序安装完成后,即可使用 npm 安装 Hexo。

1
2
3
4
# 和我一样小白的可使用以下命令来安装
npm install -g hexo-cli
# 对于熟悉 npm 的大牛,可以仅局部安装 hexo 包。
npm install hexo

验证安装

1
2
# 注意请不要在项目中使用,可能会因为限制从而导致验证误差
hexo version

Look

Hexo Version

Second 写作

新建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建一篇新文章或者新的页面
hexo new [layout] <title>
# EG 注意双引号中的是博客标题,不需要加.md等后缀。
hexo new "Your Title"

# hexo new --help
Usage: hexo new [layout] <title>

Description:
Create a new post.

Arguments:
layout Post layout. Use post, page, draft or whatever you want.
title Post title. Wrap it with quotations to escape.

Options:
-p, --path Post path. Customize the path of the post.
-r, --replace Replace the current post if existed.
-s, --slug Post slug. Customize the URL of the post.

Hexo 有三种默认布局:postpagedraft。在创建者三种不同类型的文件时,它们将会被保存到不同的路径;而您自定义的其他布局和 post 相同,都将储存到 source/_posts 文件夹。

布局 路径
post source/_posts
page source
draft source/_drafts

文内设置

用markdown等编辑器写博客,tags的写法如下:

1
2
# 注意冒号与方括号之间有一个空格,方括号中的标签用英文的”,”
tags: [hexo,备忘录]

添加“阅读全文”按钮

方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 在文章任意你想添加的位置添加
<!--more-->
# EG

---
title: How to use hexo to create blog?
date: 2020-10-17 01:48:53
author:Payne
Mail:wuzhipeng1289690157@gamil.com
tags:[Hexo]
---
<!--more-->
后面的内容在首页不显示,只显示到<!--more-->这里

方法二:

1
2
3
4
# 设置首页文章以摘要形式显示,打开主题配置文件,找到auto_excerpt进行修改
auto_excerpt:
enable: true
length: 150

其中length代表显示摘要的截取字符长度。
注:这两种方法,在博客首页显示的效果不一样,根据自己的需要,选择自己喜欢的方法

请开始您的表演😊

Show Time 。。。

Third 自我查阅

1
2
3
4
5
6
7
8
9
10
11
12
# 构建 -> 用于生成博客的html文件
hexo g
# 预览 -> 用于在本地预览博客,打开浏览器,输入 localhost:4000/ 即可查看。
hexo s
# 检查博客格式等符合要求后,用此命令将博客推送到远端。(需要是自己的才行)
hexo d

# 实操
# 构建并查阅
hexo g && hexo s
# 推送(记得清除缓存!!!)
hexo clean && hexo g && hexo d

images

Fourth 发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 详细流程如下
首先clone下来然后,创建分支
git clone "目标地址"
git checkout -b branch_name


# 编写博客



# 构建并查阅
hexo g && hexo s
# 推送(记得清除缓存!!!)
hexo clean && hexo g && hexo d

技术杂谈

Abstract

随着大数据人工智能时代的来临,互联网的快速发展。许许多多以前可能并不那么实际或需要的算法、技术也逐渐进入我们的眼中。例如分布式、集群、负载均衡、也越来越“平民”化。近期重新再一次的对于分布式理念、思想进行了学习。此随笔也因此而来。请多指教 为什么需要分布式? 什么是分布式? 分布式的核心理念是什么? 如何实现分布式、负载均衡、集群?

Why distributed?

为什么需要分布式、集群、负载均衡?

概念的提出或理论的出现,一定是为了解决对应的问题或避免相对应的问题。使其不断的完善化、稳定化。毕竟Human nature is lazy 此时此刻可能更需要什么?(3V3H)

  • Volume 海量
  • Variety 多样
  • velocity 实时
  • 高并发
  • 高可扩
  • 高性能

单机可能将会遇到的问题会有

  • 系统容量
  • 单点故障
  • 性能不足

该如何解决此问题? 有一种比较功利性的思想-缺啥补啥,当然也是比较较为明确的方式。至少我仅仅只需要解决以上中较少问题。 分布式(思想)要解决的问题主要是单机系统中系统容量不足及提高系统可用性。

What‘s Distributed?

什么是分布式?亦或者说满足如何的条件才算分布式?

分布式(系统),简而言之即是由多个处理机通过通信路线互联而构成的松散。一次性近乎解决了所有单点的所有问题。

A distributed system is a system whose components are located on different networked computers, which communicate and coordinate their actions by passing messages to one another.[1] The components interact with one another in order to achieve a common goal. Three significant characteristics of distributed systems are: concurrency of components, lack of a global clock, and independent failure of components.[1] Examples of distributed systems vary from SOA-based systems to massively multiplayer online games to peer-to-peer applications.

个人认为高并发、高可扩、高性能以此为主推到出 并发性:一个大的任务可以划分为若干个子任务,分别在不同的主机上执行 可扩性:可弹性增降容 自治性:分布式系统中的各个节点都包含自己的处理机和内存,各自具有独立的处理数据的功能。通常,彼此在地位上是平等的,无主次之分,既能自治地进行工作,又能利用共享的通信线路来传送信息,协调任务处理 全局唯一性:分布式系统中必须存在一个单一的、全局的进程通信机制,使得任何一个进程都能与其他进程通信,并且不区分本地通信与远程通信。

What is the core idea of distribution?

分布式的核心理念是什么?

较于以上的了解,那么最为重要的为: 一致性:各主从机权限可不同、但需有一致性。建立维持自己的通信 容错性:拥有较为自主的稳定性,可能最大保障系统的正常运行

How to realize distributed?

如何实现分布式?

经过了解与探究,相信你已经抓住的有单机到多机(分布式)的命脉,那就是一致性,信号一致性。如果能想到这里,那么恭喜您,你和我一样还是比较需要去学习的。并没有去否定它的正确与否,仅仅是太过于笼统。 那么既然需要实现分布式,那么仅仅需要建立及维护此通信即可。保障它,你我就从单机的电脑插上了网线。是否有点feel呢?

分布式优缺点剖析

部分先后,仅从不同角度侧重概述。合而为一即可

闪光点: 稳定性:资源共享。若干不同的节点通过通信网络彼此互联,一个节点上的用户可以使用其他节点上的资源。 性能:加快计算速度 机会点 存在通信网络饱和或信息丢失和网络安全问题

RAFT

简介

Raft首先选举出一个server作为Leader,然后赋予它管理日志的全部责任。Leader从客户端接收日志条目,复制给其他server,并告诉他们什么时候可以安全的将日志条目应用到自己的状态机上。拥有一个Leader可以简化replicated log的管理。例如,leader可以决定将新的日志条目放在什么位置,而无需询问其他节点,数据总是简单的从leader流向其他节点。Leader可能失败或者断开连接,这种情况下会选出一个新的leader。 通过leader,Raft将一致性问题分解成三个相当独立的子问题:

  • Leader Election:当集群启动或者leader失效时必须选出一个新的leader。
  • Log Replication:leader必须接收客户端提交的日志,并将其复制到集群中的其他节点,强制其他节点的日志与leader一样。
  • Safety:最关键的安全点就是图3.2中的State Machine Safety Property。如果任何一个server已经在它的状态机apply了一条日志,其他的server不可能在相同的index处apply其他不同的日志条目。后面将会讲述raft如何实现这一点。

基础

一个Raft集群会包含数个server,5是一个典型值,可以容忍两个节点失效。在任何时候每个server都会处于Leader、Candidate、Follower三种状态中的一种。在正常情况下会只有一个leader,其他节点都是follower,follower是消极的,他们不会主动发出请求而仅仅对来自leader和candidate的请求作出回应。leader处理所有来自客户端的请求(如果客户端访问follower,会把请求重定向到follower)。Candidate状态用来选举出一个leader。 多个candidate想要成为leader,如果一个candidate赢得选举,它将会在剩余的term中作为leader。在一些情况下选票可能会被瓜分,导致没有leader产生,这个term将会以没有leader结束,一个新的term将会很快产生。Raft确保每个term至多有一个leader。Term在Raft中起到了逻辑时钟的作用,它可以帮助server检测过期信息比如过期的leader。每一个server都存储有current term字段,会自动随时间增加。当server间通信的时候,会交换current term,如果一个节点的current term比另一个小,它会自动将其更新为较大者。如果candidate或者leader发现了自己的term过期了,它会立刻转为follower状态。如果一个节点收到了一个含有过期的term的请求,它会拒绝该请求。 Raft节点之间通过RPC进行通信,基本的一致性算法仅仅需要两种RPC。RequestVote RPC由candidate在选举过程中发出,AppendEntries RPC由leader发出,用于复制日志和提供心跳。每一个请求类型都有对应的response,Raft假定request和response都可能会丢失,因此要求请求者有超时重试的能力。为了性能,RPC请求会并行发出,而且不保证RPC的到达顺序。

Leader election

Raft使用心跳机制来触发leader选举。当server启动的时候是处于follower状态,当它可以收到来自leader或者candidate的有效RPC请求时就会保持follower的状态。Leader发送周期性的心跳(不含日志的AppendEntries RPC)给所有的follower来确保自己的权威。如果一个follower一段时间(称为election timeout)没有收到消息,它就会假定leader失效并开始新的选举。 为了开始新一轮选举,follower会提高自己当前的term并转为candidate状态。它会先给自己投一票然后并行向集群中的其他server发出RequestVote RPC,candidate会保持这个状态,直到下面三种事情之一发生:

  1. 赢得选举。当candidate收到了集群中相同term的多数节点的赞成票时就会选举成功,每一个server在给定的term内至多只能投票给一个candidate,先到先得。收到多数节点的选票可以确保在一个term内至多只能有一个leader选出。一旦一个candidate赢得选举,它就会成为leader。它之后会发送心跳消息来建立自己的权威,并阻止新的选举。
  2. 另一个server被确定为leader。在等待投票的过程中,candidate可能收到来自其他server的AppendEntries RPC,声明它才是leader。如果RPC中的term大于等于candidate的current term,candidate就会认为这个leader是合法的并转为follower状态。如果RPC中的term比自己当前的小,将会拒绝这个请求并保持candidate状态。
  3. 没有获胜者产生,等待选举超时。candidate没有选举成功或者失败,如果许多follower同时变成candidate,选票就会被瓜分,形不成多数派。这种情况发生时,candidate将会超时并触发新一轮的选举,提高term并发送新的RequestVote RPC。然而如果不采取其他措施的话,选票将可能会被再次瓜分。

Raft使用随机选举超时来确保选票被瓜分的情况很少出现而且出现了也可以被很快解决。election timeout的值会在一个固定区间内随机的选取(比如150-300ms)。这使得在大部分情况下仅有一个server会超时,它将会在其他节点超时前赢得选举并发送心跳。candidate在发起选举前也会重置自己的随机election timeout,也可以帮助减少新的选举轮次内选票瓜分的情况

Log Replication

一旦一个leader被选举出来,它开始为客户端请求服务。每一个客户端请求都包含着一个待状态机执行的命令,leader会将这个命令作为新的一条日志追加到自己的日志中,然后并行向其他server发出AppendEntries RPC来复制日志。当日志被安全的复制之后,leader可以将日志apply到自己的状态机,并将执行结果返回给客户端。如果follower宕机或运行很慢,甚至丢包,leader会无限的重试RPC(即使已经将结果报告给了客户端),直到所有的follower最终都存储了相同的日志。 日志按下图的方式进行组织,每一条日志储存了一条命令和leader接收到该指令时的term序号。日志中的term序号可以用来检测不一致的情况,每一条日志也拥有一个整数索引用于定位。

总结

RAFT算法终究实现了什么?

  • 实现了信号一致性
  • 动态的Leader、Accessible or work 角色及节点管理,最大限度的保障了稳定性。
  • 他的核心为保障大多数都可用,即可正常运行。

寄语: 如果您是一位非常有经验的管理者或您有相对稳健的算法与数据结构基础, 是否与您所存在的了解的结构有所大多相似呢?-有效管理 直接上司管辖直属同事,算法中的BFS 在稍稍加入较为智能的选Leader,若大多数leader停止了工作,worker 将会进行相对应的“休息”,并没有接受到任务,简单的计算机选择做不如不做。毕竟Human nature is lazy

技术杂谈

Docker File 解析:

构建简史

编写一个dockerfile的文件,符合dockerfile的规范 docker build 执行,获得一个自定义的镜像 docker 运行 Docker执行docker file文件的大致流程

docker 从基础镜像运行一个容器 执行一条指令并对容器作出修改 执行类似docker commit的操作提交一个新的镜像层 docker 在基于刚提交的镜像运行一个新容器 执行docker 中的下一条指令知道所有指令都执行完成

Docker File基础知识:

每条保留字指令必须为大写字母且后面要跟随至少一个参数 指令从上到下、从左至右执行 ‘#’ :表示注释 每条指令都会创建一个新的镜像层,并对镜像进行提交

Docker File 体系结构:

保留字指令:

FROM:基础镜像,当前这个新的镜像是基于哪个镜像(scratch) MAINTAINER:镜像作者+邮箱 RUN:容器构建时所需要运行的命令 EXPOSE:当前容器对外暴露的端口号 WORKDIR:指定在创建容器后,终端默认登陆的进来工作目录,一个落脚点 ENV:用于构建镜像过程中设置环境变量 ADD: 拷贝加解压缩:将宿主机目录下的文件拷贝进镜像且add命令会自动处理rul和解压tar压缩包 COPY:将从构建上下文目录中<源路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置

  • COPY src dest
  • COPY [“src”,”dest”]

VOLUME:容器数据卷,用于数据保存和持久化工作 CMD:

  • 指定一个容器启动时要运行的命令
  • DockerFile 中可以有多个CMD指令,但只有最后一个生效,CMD会被docker run 之后的参数替换!!!

ENTRYPOINT:

  • 指定一个容器启动时要运行的命令
  • 目的与CMD一样,都是在指定容器启动程序及参数
  • 不会被替换,被追加

ONBUILD:触发器 当构建一个被继承Docker File时运行的命令,父镜像在被子继承后父镜像的onbuild被触发

关键字详解

Dockerfile 分为四部分:

基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

基础镜像信息

FROM

1
2
# 格式
FROM <image> or FROM <image>:<tag>

如果在同一个Dockerfile中创建多个镜像时,可以使用多个 FROM 指令(每个镜像一次)

注意: Dockerfile每个保留字都会在docker容器中新建一层镜像层, 合理的减少镜像层以达节省资源的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# base
FROM python:3.7
RUN pip3 install requests
RUN pip3 install bs4
RUN pip3 install scrapy
RUN ...

# other
FROM python:3.7
RUN pip3 install requests bs64 scrapy # Python通过空格分隔,达到安装多个包的效果

# batter
# 外部建立所安装包
pip3 freeze >> requirements.txt
# 在Dockerfile 中
FROM python:3.7
RUN pip3 install -r requirements.txt

分析: 达到的效果虽相同,但所用资源却不同, base 所使用的资源约为other,batter的3/5

  • base 三层
  • other 一层
  • batter 一层
  1. 若所需要使用到的第三方库较少,建议使用other
  2. 在实际工程中用到的第三方库大多情况下都大于三个,方法二虽好.但所需导入的包一多,很可能出现单词拼写错误, 格式不符

维护者信息(非必须)

MAINTAINER

1
2
# 格式
MAINTAINR <name>

镜像操作指令(按需择选)

COPY

作用: 复制文件指令,从上下文 目录中复制或目录, 到容器中的指定路径 同等需求下建议使用COPY

1
2
3
4
5
6
# 格式
COPY <src> <dest>
COPY [--chown=<user>:<group>] <源路径1>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
# [--chown=<user>:<group>]:可选参数,用户改变复制到容器内文件的拥有者和属组。
# 复制本地主机的 `<src>`(为 Dockerfile 所在目录的相对路径)到容器中的 `<dest>`

<源路径>:源文件或者源目录,这里可以是通配符表达式,其通配符规则要满足 Go 的 filepath.Match 规则。

1
2
COPY hom* dir/
COPY hom?.txt dir/

<目标路径>:容器内的指定路径,该路径不用事先建好,路径不存在的话,会自动创建。

ADD

作用: 复制文件指令,从上下文 目录中复制或目录, 到容器中的指定路径 同等需求下建议使用COPY

1
2
3
4
5
# 格式 
ADD <src> <dest>`

# 该命令将复制指定的 `<src>` 到容器中的 `<dest>`。
# 其中 `<src>` 可以是Dockerfile所在目录的一个相对路径;也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)。
  • ADD 的优点:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>。
  • ADD 的缺点:在不解压的前提下,无法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。具体是否使用,可以根据是否需要自动解压来决定

小结:

  • ADD 与 COPY 功能无明显差异, 但针对性不同
  • 当使用本地文件为源目录时,建议使用Copy
  • 当需使用压缩包中文件时构建时,建议使用Copy

RUN

作用: 用于在容器内执行命令

1
2
3
# 格式
RUN <command> [option] # 相当于shell格式
RUN ["command", "option1", 'option2'] # 相当于 exec 格式

WORKDIR

作用:指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。(WORKDIR 指定的工作(主)目录,必须是提前创建好)。 docker build 构建镜像过程中的,每一个 RUN 命令都是新建的一层。只有通过 WORKDIR 创建的目录才会一直存在。

1
2
# 格式
WORKDIR <dir>

USE

作用:用于指定将使用命令的用户和用户组 此处只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。 由于docker 无设置,默认需在root权限下运行 正所谓权限越大能力越大,若被入侵则造成损失较于其他权限为最大 安全与灵活性 二者相对斟酌

1
2
# 格式
USER <username>[:<usergroup>]

EXPOSE

作用:仅声明端口

  • 方便配置映射
  • 在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
1
2
# 格式
EXPORT <port1> [<prot2> <prot3> ...]

执行指令

CMD

作用: 类似于 RUN命令,但运行处不同

  • RUN 在Docker build 前运行
  • CMD 在docker run 后运行

注意:

  • 当 Dockerfile 中存在多个 CMD 指令,仅最后一个生效。
  • CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。

当使用自定制镜像时,大致流程为

  • 编写业务代码
  • 构建定制镜像(docker build)
  • docker 中运行业务 (Docker run)
1
2
3
4
5
# 格式
CMD <ShellCommand1 [option]> [&&ShellCommand2[option] ]
CMD ["<可执行命令或文件>", "<option1>", "<option2>", ... ]
# 此写法为保留字 ENTRYPOINT 指令指定的程序提供默认参数
CMD ["<command1>", "<command2>", "<command3>", ...]

ENTRYPOINT

作用: 功能与CMD相似,但不会被docker run 后的指定参数所覆盖, 命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序。 但是当docker run 使用了 —entrypoint 选项此选项的参数,可当作要运行的程序覆盖 ENTRYPOINT 指令指定的程序。 ENTRYPOINT CMD对比 同: 多个 指令,仅最后一个生效。 异: ENTRYPOINT选项的参数可当作要运行的程序覆盖 ENTRYPOINT 指令指定的程序。

1
2
# 格式
ENTRYPOINT ["<可执行命令或文件>","option1", "option2"]

ENV

作用:配置容器内的环境变量,且保存,可以被后续 指令使用

1
2
3
# 格式
ENV <Key> <Values>
ENV <Key1>=<Values1>, <Key2>=<Values2> ...

ARG

作用:配置容器内的环境变量,且保存,可以被后续 指令使用(与ENV功能相似) 不同:作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效 也就是说只有 docker build 的过程中有效,构建好的镜像内不存在此环境变量。

1
构建命令 docker build 中可以用 --build-arg <参数名>=<值> 来覆盖。
1
2
3
# 格式
ARG <key>[=默认值], <Value>
# 若不写Value 则Build时为自己写的默认值

VOLUME

作用:定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

  • 避免重要的数据,因容器重启而丢失
  • 避免容器不断变大
1
2
3
# 格式:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

在启动容器 docker run 的时候,我们可以通过 -v 参数修改挂载点。

HEALTHCHECK

作用: 用于指定某个程序或者指令来监控 docker 容器服务的运行状态。

格式:

1
2
3
HEALTHCHECK [option] CMD <command> #设置检查容器健康状况的命令
HEALTHCHECK NONE # 如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
HEALTHCHECK [option] CMD <command> # CMD 后面跟随的命令使用

ONBUILD

作用: 用于延迟构建命令的执行。 先构建一个父类镜像(ONBUILD 在父类中,但不立即执行),后子类继承此父类镜像(执行父类的ONBUILD 命令) 本次并不执行,当镜像调用它时,将执行父类中ONBUILD命令

1
2
# 格式
ONBUILD <其它指令>

技术杂谈

Docker容器数据卷

Docker理念:

  • 将运用与运行的环境打包形成容器运行,运行可以伴随着容器,但由于对数据要求希望是持久化的
  • 容器之间希望可以共享数据

一、Docker容器数据卷是什么?

  • docker容器产生的数据,如果不通过docker commit生成新的镜像,使数据做为镜像的一部分保存下来,那么删除容器之后,数据也随之被删除。为了能保存数据在docker中,我们使用容器卷。
  • 好比从电脑(docker)中拷贝数据(使用的U盘-容器卷)

二、Docker容器数据卷能干什么?

  • 数据的持久化
  • 容器间继承+共享数据

特点:

  • 容器之间共享过重用数据
  • 卷中更改可之间生效
  • 数据卷中的更改不会在镜像的更新中
  • 数据中的更改不会包含在镜像的更新中
  • 数据卷的生命周期一直持续到没有容器使用止

三、Docker容器数据卷

容器内添加:

1
2
docker run -it -v /[宿主机绝对路径目录]:[/容器内目录] [container_ID]
docker run -it -v /myDataVolume:/dataVolumeContainer [镜像名]

权限报错处理:

1
docker run -it -v /myDataVolume:/dataVolumeContainer --privileged=true [镜像名]

检测是否数据卷是否挂载成功

1
docker inspect [container_ID]

容器停止后,主机修改后数据是否同步 可以!但需为同一个容器!!![The same container_id]

1
2
3
使用docker ps -l 查看运行过的容器信息
# 查看运行容器信息
docker ps -l

命令(带权限):容器中只读,不可修改 docker run -it -v /宿主机绝对路径:/容器内目录:ro 镜像名

dockerfile 添加

javaEE:hello.java —-> hello.class Docker: images ===》 DockerFile

  1. 新建mydocker文件夹并进入
  2. 在dockerfile中使用volume指令来给镜像添加一个或多个数据卷
    • volume[“/dataVolumeContainer”,”/dataVolumeContainer2”,”/dataVolumeContainer3”]
  3. File构建

    1
    2
    3
    4
    5
    # volume test
    FROM centos
    VOLUME ["/dataVolumeContainer1","/dataVolumeContaine2"]
    CMD echo "finished,----sucess1"
    CMD /bin/bash
  4. build后生成镜像

    1
    2
    3
    4
    docker build -f /路径/文件名 -t 容器名:TAG . 
    -f: file-->指定为文件
    -t:为容器重新分配一个为输入终端;
    . : 分布执行file中命令

四、Docker容器数据卷容器

命名的容器挂载数据卷,其他容器通过挂载这个(父容器)实现数据共享,挂载数据卷的容器称之为数据卷容器 容器间传递共享(volumes-from)

dc01(主) 删除后 dc02(子1) dc03(子2)不受影响,dc02与dc3 继续传输

结论:容器之间配置信息的传递,数据卷的生命周期一直持续到没有容器使用为止

技术杂谈

搭建个人Leanote云笔记

Leanote是一款在线的云笔记应用,有如下特点:

  • 支持网页、PC、手机APP客户端和微信版,随时记录,方便分享,支持语音,图片输入。
  • 代码高亮,涵盖所有主流语言的代码高亮,随心所欲在Leanote里写代码,记知识。
  • Markdown 编辑器,实时同步预览。
  • 专业数学公式编辑,像Word和Latex能编辑数学公式。
  • 支持创建思维脑图,将散乱的想法以树状信息分层展示。
  • 详细历史纪录,每次保存都在后端备份,轻松查找,一键恢复。
  • 实时同步云端。

简介

基于Linux + Mongo + Leanote 快速搭建个人云笔记

安装MongoDB

MongoDB是一个基于分布式文件存储的高性能数据库,介于关系数据库和非关系数据库之间,它支持的数据结构非常松散是类似于json和bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。 Leanote云笔记使用MongoDB作为后端数据库,按照以下步骤按照MongoDB数据库。

1
2
3
4
5
6
# install MongoDB
yum -y install mongodb mongodb-server.x86_64 mariadb-devel.i686
# start MongoDB service
systemctl start mongod
# Verify mongodb running state
systemctl status mongod

安装Leanote

1
2
3
4
5
6
7
8
# Download Leanote installation package
wget https://nchc.dl.sourceforge.net/project/leanote-bin/2.6.1/leanote-linux-amd64-v2.6.1.bin.tar.gz
# Unzip
tar -zxvf leanote-linux-amd64-v2.6.1.bin.tar.gz
# setting leanote
vim leanote/conf/app.conf
# change app.secret
app.secret = Self configuration

配置服务

If the following message appears

Failed global initialization: BadValue Invalid or no user locale set. Please ensure LANG and/or LC_*

Please configure the environment variables as follows

1
export LC_ALL=C
1
2
3
4
# init MongoDB
mongorestore -h localhost -d leanote --dir /root/leanote/mongodb_backup/leanote_install_data/
# Start service
nohup bash /root/leanote/bin/run.sh > /root/leanote/run.log 2>&1

访问笔记

在浏览器中访问服务器弹性地址:端口(默认9000)

1
2
3
# 默认账号
username: admin
password: abc123

技术杂谈

Multiple solutions of Fibonacci (Python or Java)

本章是用英文写的,作为或想成为一名优秀的攻城狮,习惯阅读英文文档将使你受益良多。例如更好的查看最新版的官方文档、与国外友人交流、等等 其实英文的生词也并不多,其中90%的英文都在代码里,当然这其中的精华也在代码里,代码相信大部分伙计还是都可以看懂.所以,请不要惊慌。对于English,让我们一起取克服它、习惯它、拥抱它。然后把它锤倒在地,相信你可以的。 GO, Go, GO 如果实在不行,各种页面翻译来一手。莫慌,这都是小场面,啥都不是事儿,好吧

Violence law(Top-down)

It can be solved directly according to the known conditions (f (0) = 0, f (1) = 1 F(N) = F(N - 1) + F(N - 2), for N > 1)

Python Code

1
2
3
4
class Solution:
def fib(self, N: int) -> int:
if N == 1 or N == 2: return N
return self.fib(N - 1) + self.fib(N - 2)

Java Code

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int fib(int N) {
if (N == 1 || N == 2) return 1;
return fib(N - 1) + fib(N - 2);
}
}

class Solution {
public int fib(int N) {
return N < 2 ? N : fib(N - 1) + fib(N - 2);
}
}

Violence law add cache(Pruning)

We know that if we don’t do any processing, we will repeat too many calculations, which is very bad The processing idea will avoid repeated calculation

Python Code

1
2
3
4
5
class Solution2:
@functools.lru_cache()
def fib(self, N: int) -> int:
if N <= 1: return N
else: return self.fib(N - 1) + self.fib(N - 2)

Java Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
private Integer[] cache = new Integer[31];
public int fib(int N) {
if (N <= 1) return N;
cache[0] = 0;
cache[1] = 1;
return memoize(N);
}
public int memoize(int N) {
if (cache[N] != null) return cache[N];
cache[N] = memoize(N-1) + memoize(N-2);
return memoize(N);
}
}

Divide and conquer solution

Recursion, iteration, divide and conquer, backtracking, they do not have a clear distinction Recursion:The core idea is to govern separately and unify the officials

1
2
3
4
5
6
7
class Solution:
def fib(self, N: int) -> int:
memo = {}
if N < 2: return N
if N-1 not in memo: memo[N-1] = self.fib(N-1)
if N-2 not in memo: memo[N-2] = self.fib(N-2)
return memo[N-1] + memo[N-2]

Dynamic recursion(Bottom up)

Basic solutions

More initial value, continuous dynamic recursive

Python Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def fib(self, N: int) -> int:
if N < 2: return N
dp = [0 for _ in range(N + 1)]
dp[0], dp[1] = 0, 1
for i in range(2, N + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[- 1]

class Solution:
def fib(self, N: int) -> int:
if N == 0: return 0
memo = [0,1]
for _ in range(2,N+1):
memo = [memo[-1], memo[-1] + memo[-2]]
return memo[-1]
Java Code
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int fib(int N) {
if (N <= 1) return N;
if (N == 2) return 1;
int curr = 0, prev1 = 1, prev2 = 1;
for (int i = 3; i <= N; i++) {
curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return curr;
}
}

Use better base types (tuples) to improve performance

1
2
3
4
5
6
7
class Solution:
def fib(self, N: int) -> int:
if N == 0: return 0
memo = (0,1)
for _ in range(2,N+1):
memo = (memo[-1], memo[-1] + memo[-2])
return memo[-1]

Better solutions

Python Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def fib(self, N: int) -> int:
curr, prev1, prev2 = 0, 1, 1
for i in range(3, N + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return curr

class Solution5:
def fib(self, N: int) -> int:
prev, now = 0, 1
for i in range(N):
prev, now = now, now + prev
return prev

Java Code

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int fib(int N) {
if (N == 0) return 0;
if (N == 2 || N == 1) return 1;
int prev = 1, curr = 1;
for (int i = 3; i <= N; i++) {
int sum = prev + curr;
prev = curr;
curr = sum;
}
return curr;
}
}

Mathematical conclusion method

Python Code

1
2
3
4
5
class Solution:
def fib(self, N: int) -> int:
sqrt5 = 5 ** 0.5
fun = pow((1 + sqrt5) / 2, n + 1) - pow((1 - sqrt5) / 2, n + 1)
return int(fun / sqrt5)

Java Code

1
2
3
4
5
6
class Solution {
public int fib(int N) {
double sqrt5 = (1 + Math.sqrt(5)) / 2;
return (int)Math.round(Math.pow(sqrt5, N)/ Math.sqrt(5));
}
}

Other

在计算机的世界中由最基本的for loop、while loop、if…else无限衍生,无论多么复杂的逻辑最后大多可归纳为以上三种。当然除非原本逻辑无重复性,无条件分支。

一、循环(重复)

不断的重复、有始有终 循环实现

1
2
3
4
5
6
7
private loop(){
for(start; end; loop termination){
expression1;
expression2;
expression3;
}
}
1
2
3
4
5
def loop():
for start in end/loop_termination:
expression1;
expression2;
expression3;

二、递归


特征:自相似性、有始有终 实现:归去来兮、适可而止 何时想到递归?

子问题与原始问题做同样的事

递归实现:

1
2
3
4
5
6
7
8
9
10
11
12
private void recursion(int level,int param1,int param2...):{
// 终止条件(recursion terminato)
if(level > MAX_LEVEL){
# process_rsult
return
}
// 处理此层过程逻辑(process logic in current level)
process(level, data1, data2...)
// 进入下一层(dill down)
recursion(level: level + 1, newParam):
// 如果需要恢复此层状态
}
1
2
3
4
5
6
7
8
9
10
def recursion(level, param1, param2...):
# 终止条件(recursion terminato)
if level > MAX_LEVEL:
# process_rsult
return
# 处理此层过程逻辑(process logic in current level)
process(level, data1, data2...)
# 进入下一层(dill down)
self.recursion(level + 1, param1, param2...):
# 如果需要恢复此层状态

二、分治


定义:分而治之,群臣归一 何时想到分治?

当复杂的问题可以拆分成简单的子问题

分治实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static int divide_conquer(Problem, Param1, Param2...) {
// 终止条件
if (problem == NULL) {
int res = process_last_result();
return res;
}
// 拆分子问题
subProblems = split_problem(problem)

res0 = divide_conquer(subProblems[0])
res1 = divide_conquer(subProblems[1])
...
// 合并子问题结果
result = process_result(res0, res1);

return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
def divide_conquer(Problem, Param1, Param2...):
# 终止条件
if problem is None:
return
# 拆分子问题
subproblems = split_problem(problem, data)
subresult1 = self.divide_conquer(subproblems[0], p1, ...)
subresult2 = self.divide_conquer(subproblems[1], p1, ...)
subresult3 = self.divide_conquer(subproblems[2], p1, ...)
...
# 合并子问题结果
result = process_result(subresult1, subresult2, subresult3, …)

三:回溯


采用“试错”思想,尝试“分步”去解决问题。在分步的过程中。根据上层结果,尝试此层最优解决此问题,如果此层较于上层不是最优则回溯。

四、DP(Dynamic programming) 动态规划/动态递推


定义

In both contexts it refers to simplifying a complicated problem by breaking it down into simpler sub-problems in a recursive manner. While some decision problems cannot be taken apart this way, decisions that span several points in time do often break apart recursively. 在这两种情况下,它都是指通过递归的方式将复杂问题分解为更简单的子问题来简化它。虽然有些决策问题不能用这种方式分解,但是跨越多个时间点的决策通常会递归地分解。 Simplifying a complicated problem by breaking it down into simpler sub problem(in a recursibe manner) 把一个复杂的问题分解成更简单的子问题简化它(用一种递归的方式)

自低向上 动态规划关键点:

  • 最优子结构
  • 储存中间状态
  • 递推公式(状态转移方程,DP方程)
  • eg
1
2
3
4
5
# 一维
Fib:
opt[i] = opt[n - 1] + opt[n - 2]
# 二维
opt[i][j] = opt[i + 1][j] + opt[i][j + 1]

以斐波那契数列为例:

1
2
3
F(0) = 0, F(1) = 1 

F(N) = F(N - 1) + F(N - 2)(N >= 2

递归(傻递归): 若计算F(4);需计算

1
2
lin1 F(4) = f(3)、f(2), 
lin2 F(3):f(2)、f(1), F(2) = f(1) + f(0)

DP:

1
2
i(0) = 0, i(1) = 1
[0, 1, 1, 2, 3, 5]

总结

动态规划、递归、分治、无本质区别 共性: 重复子问题 异性:最优子结构、中途淘汰次优

技术杂谈

刚装了台新机器,Git 显示总是呈现这样的样子:

1
"\346\265\213\350\257\225.txt"

解决办法:

1
git config --global core.quotepath false

仅此记录,完毕。

技术杂谈

设计模式(Design Patterns),旨在软件设计(可重用的面向对象软件的要素)中,被反复使用的一种代码设计经验。设计模式旨在简化代码量、降低耦合度、高效使用可重用代码,提高代码可拓性和可维护性。

3V3H概念: 3V: Voluem(海量),Variety(多样)、Velocity(实时) 3H: High concurrency(高并发)、High performance(高性能)、High development(高可拓)

设计模式的由来:

设计模式这个术语是由上个世纪90年代Erich Gamma、Richard Helm、Raplh Johnson和Jonhn Vlissides四人总结提炼而出。并编写了Design_Patterns(设计模式)一书,他们四位统称为GOF(俗称四人帮)。 设计模式:即 将常使用的设计思想提炼出一个个模式,然后给每个模式命名,这样在使用的时候更方便交流。GOF把23个常用模式分为创建型模式、结构型模式和行为型模式三类

为何采用设计模式思想?(请注意思想二词!!!)

设计模式并非直接用来完成编码,而是描述在各种不同情况下,要怎么解决问题的一种方案一种思想 面向对象设计模式常以对象来描述其中的关系和相互作用,但不涉用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。 学习设计模式,关键是学习设计思想,不为设计而设计,需合理平衡设计模式的复杂度和灵活性。 须知设计模式并不是万能的!!!

设计模式符合什么原则?

开闭原则(Open Colse Principle):

软件对拓展开发,对修改关闭。

理解:在增加新功能的时候,能不改代码就尽量不要改,如果只增加部分代码便可完成新功能,即是最好。

里氏替换原则(Liskov Substitution Principle):

面向对象设计原则(六大原则)

理解:若调用父类方法可成功,即调用成调用其子类亦可成功

面向对象六大设计原则:

英文名称

缩写

中文名称

Single Responsibility Principle

SRP

单一职责原则

Open Close Principle

OCP

开闭原则

Liskov Substitution Principle

LSP

里氏替换原则

Law of Demeter ( Least Knowledge Principle)

LoD

迪米特法则(最少知道原则)

Interface Segregation Principle

ISP

接口分离原则

Dependency Inversion Principle

DIP

依赖倒置原则

常用设计模式概况:

[gallery columns=”1” size=”full” ids=”9642”]

创建型模式(Creational Patterns):

如何创建对象? 核心思想是要把对象的创建和使用分离,从而使得二者可以相对独立地变换。这些模式更加关注对象之间的创建

  • 单例模式:Singleton Pattern
  • 多例模式:Multiton Pattern
  • 工厂模式:Factory Pattern
  • 静态工厂模式:Static Factory Pattern
  • 抽象工厂模式:Abstract Factory Pattern
  • 原型模式:Prototype Pattern
  • 建造者模式:Builder Pattern
  • 对象池模式:Pool Pattern

结构型模式(Structural Patterns):

如何组合各种对象,以便于更好、更灵活的结构?面向对象的继承机制虽提供了基本的子类继承与拓展父类的功能,但结构型模式却不仅是简单的继承,还有更多的通过组合,使之与运行期的动态组合实现更加灵活的功能。这些模式更加关注对象之间的组合

  • 组合模式(Composite)
  • 桥接模式(Bridge)
  • 适配器模式(Adapter)
  • 过滤器模式(Filter、Criteria Pattern
  • 装饰模式(Decorator)
  • 外观模式(Facade)
  • 门面模式:Facade
  • 享元模式(Flyweight)
  • 代理模式(Proxy)
  • 数据映射模式:Data Mapper
  • 依赖注入模式:Dependency Injection
  • 流接口模式:Fluent Interface
  • 注册模式:Registry

行为模式(Behavioral Patterns):

行为模式主要涉及对象与函数(算法)之间的职责分配,通过对象及函数灵活组合。此种模式更加关注对象之间的通信 责任链模式:Chain of Responsibility Pattern 命令模式:Command Pattern 解释器模式:Interpreter Pattern 迭代器模式:(Iterator Pattern 中介者模式:Mediator Pattern 备忘录模式:Memento Pattern 观察者模式:Observer Pattern 状态模式:State Pattern 空对象模式:Null Object Pattern 策略模式:Strategy Pattern 模板模式:Template Pattern 访问者模式:Visitor Pattern 规格模式:Specification

有不少的人说程序= 算法+ 数据结构,但个人认为程序 = 架构 + 设计模式 + 数据结构与算法 接下来将一起对本文中面向对象原则及设计模式进行详细的学习,同时我也会不断更新算法与数据结构相关的知识,让我们一起学习起来

技术杂谈

官方文档说明

在设置中找到ITEM_PIPELINES并加入以下代码

1
scrapy.pipelines.images.ImagesPipeline: 301

settings配置:

图片存储路径:

1
IMAGES_STORE = “your path”

图片存储天数

1
images_EXPIRES =  30

设置缩略图(固定值):

1
2
3
4
IMAGES_THUMBS = {
'small':(50,50)
'big':(270,270)
}

示例:

1
2
3
# 配置图片管道参数
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IMAGES_STORE = os.path.join(BASE_DIR,'images')
1
2
3
4
5
6
7
8
# 寻找此文件的父级目录
os.path.dirname()
# 当前脚本的绝对路径目录
os.path.abspath(__file__)
# __file__当前脚本的名字

IMAGES_STORE = os.path.join(BASE_DIR,'images')
将BASE_DIR新增IMAGES文件夹路径

设置spider中获取images_url的提取方法

1
2
3
item['image_urls'] = "提取语法"
# item['image_urls'] = response.css(".pic img:attr('src')").extract()
item['images'] = [] # 【】中不需要填写,下载图片之后,保存本地的文件位置

使用ImagesPipeline下载图片时,需要使用images_urls字段,images_urls一般是可迭代的列表或元组类型

如果遇到图片反扒请打开

1
2
3
4
5
# DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
# "referer":"自行配置"
# }

存入MongoDB,示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 import pymongo
from itemadapter import ItemAdapter

class MongoPipeline:

collection_name = 'scrapy_items'

def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db

@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
)

def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]

def close_spider(self, spider):
self.client.close()

def process_item(self, item, spider):
self.db[self.collection_name].insert_one(ItemAdapter(item).asdict())
return item

更多详情请查阅官方文档:https://docs.scrapy.org/en/latest/topics/item-pipeline.html#take-screenshot-of-item

JavaScript

一、先试着英汉翻译一波:

1.按F12打开调试台,再点击Network,再点击Headers,可以找到i=good,这就是我们刚才输入需要翻译的词语good,from Data中的就是请求的参数,分别为:

i: good from: AUTO to: AUTO smartresult: dict client: fanyideskweb salt: 15972332870677 sign: 3a078c10344e67f95822ae9389e1363f lts: 1597233287067 bv: 85c050fb1c0b4d824d801d079db7371a doctype: json version: 2.1 keyfrom: fanyi.web action: FY_BY_CLICKBUTTION

2.在来翻译一个新的词语,看下这些参数有无变化

i: 我是中国人 from: AUTO to: AUTO smartresult: dict client: fanyideskweb salt: 15972365771410 sign: 744ebb7fd625f1d4d9d6a270e98536c2 lts: 1597236577141 bv: 85c050fb1c0b4d824d801d079db7371a doctype: json version: 2.1 keyfrom: fanyi.web action: FY_BY_CLICKBUTTION

发现有5个参数是变化的(实际是4 个),分别为:

i: salt: sign: lts: bv:

3.看看URL,#http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule() 请求连接:http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule 去掉translate_o中的_o,不去的话,轻轻就数据结果:某道词典 ,英语翻译汉语正常,汉语翻译英语时,出现{‘errorCode’: 50} ,post请求方式

4.点击Initiator,可以看到所有的js文件都是@fanyi.min.js:1

5、点击@fanyi.min.js:1进入,

点击中间的{ },格式化一下。

再找到XHR/fetch Breakpoints,添加断点, 你可以针对某一个请求或者请求的关键字设置断点:

再点一下‘+’输入url即可url为http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule

最后再去点击一下翻译按钮

6.翻译:我是中国人,点击t-translate,再点击Scope,再点击Local,再点击r,即可看到我们需要的参数,在吧鼠标放到中间的r上,r = v.generateSaltSign(n),就会弹出Object框。就是我们需要的参数。

7.可以看到8941行,鼠标放到v.gen erateSaltSign(n)上,会弹出f r(e)函数,点击这个函数会进入第8368行,可以看到我们需要的参数

salt: sign: lts: bv:

在8378行打上断点,再次点击翻译按钮,即可看多所有的参数值显示出来。

8.对于第一个 ts: r,相当于pythonzhong 的ts=r赋值, 可以看到r = “” + (new Date).getTime(),知道了r,就知道了ts,r是js中 new Date().getTime()得到的是毫秒数,也就是时间戳,单位为毫秒,13位数字的字符串。

即可用python中的时间戳构造:

13位时间戳获取方法:单位:毫秒 t1 = time.time() ts= int(t1 * 1000) ts=r

第二个参数:salt: i,相当于python中的salt= i赋值,i = r + parseInt(10 * Math.random(), 10),意思是随机产生一个整数 范围是0-9里面的一个随机数

ts=r,转化为字符转,parseInt(10 * Math.random(), 10)产生的随机数也转化为字符串,最后进行字符串拼接,而不是数字相加。请注意一下。跟实际的赋值对比,产生了一个14位数字的字符串,跟实际情况一样。

即可用python中的构造法: salt= str(int(ts))+str(random.randint(0,10)) print(salt)

第三个参数:bv: t,相当于python中的bv=t赋值, var t = n.md5(navigator.appVersion),navigator.appVersion的值竟然是一个 User-Agent,那么重点来了,网上教程都是md5加密相关都是直接用python原生来生成,而我就选择直接扣代码。用python3调用js的库之execjs 来扣,鼠标放到var t = n.md5(navigator.appVersion)中的n.md5()上,将会出现f md5(e)函数,点击进入,来到8196行, md5: function(e){ },把第8196行到8278行扣下来,再应用execjs解析出bv

注意:自调函数调用写法:

var aa=function(e){ }和function aa(e){ }写法都可以,一样的

import execjs f = open(r”text.js”,encoding=’utf-8’).read() ctx1 = execjs.compile(f) bv=ctx1.call(‘md5_1’,ua.random) print(bv)

扣出来的js代码:主体为function md51(e) { } ,应用execjs解析,缺什么参数找什么参数,即可_

var n = function(e, t) { return e << t | e >>> 32 - t } , r = function(e, t) { var n, r, i, a, o; return i = 2147483648 & e, a = 2147483648 & t, n = 1073741824 & e, r = 1073741824 & t, o = (1073741823 & e) + (1073741823 & t), n & r ? 2147483648 ^ o ^ i ^ a : n | r ? 1073741824 & o ? 3221225472 ^ o ^ i ^ a : 1073741824 ^ o ^ i ^ a : o ^ i ^ a } , i = function(e, t, n) { return e & t | ~e & n } , a = function(e, t, n) { return e & n | t & ~n } , o = function(e, t, n) { return e ^ t ^ n } , s = function(e, t, n) { return t ^ (e | ~n) } , l = function(e, t, a, o, s, l, c) { return e = r(e, r(r(i(t, a, o), s), c)), r(n(e, l), t) } , c = function(e, t, i, o, s, l, c) { return e = r(e, r(r(a(t, i, o), s), c)), r(n(e, l), t) } , u = function(e, t, i, a, s, l, c) { return e = r(e, r(r(o(t, i, a), s), c)), r(n(e, l), t) } , d = function(e, t, i, a, o, l, c) { return e = r(e, r(r(s(t, i, a), o), c)), r(n(e, l), t) } , f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } , p = function(e) { var t, n = "", r = ""; for (t = 0; t <= 3; t++) n += (r = "0" + (e >>> 8 * t & 255).toString(16)).substr(r.length - 2, 2); return n }, h = function(e) { e = e.replace(/\\x0d\\x0a/g, "\\n"); for (var t = "", n = 0; n < e.length; n++) { var r = e.charCodeAt(n); if (r < 128) t += String.fromCharCode(r); else if (r > 127 && r < 2048) t += String.fromCharCode(r >> 6 | 192), t += String.fromCharCode(63 & r | 128); else if (r >= 55296 && r <= 56319) { if (n + 1 < e.length) { var i = e.charCodeAt(n + 1); if (i >= 56320 && i <= 57343) { var a = 1024 * (r - 55296) + (i - 56320) + 65536; t += String.fromCharCode(240 | a >> 18 & 7), t += String.fromCharCode(128 | a >> 12 & 63), t += String.fromCharCode(128 | a >> 6 & 63), t += String.fromCharCode(128 | 63 & a), n++ } } } else t += String.fromCharCode(r >> 12 | 224), t += String.fromCharCode(r >> 6 & 63 | 128), t += String.fromCharCode(63 & r | 128) } return t }; f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } function md5_1(e) { var t, n, i, a, o, s, m, g, v, y = Array(); for (e = e, y = f, s = 1732584193, m = 4023233417, g = 2562383102, v = 271733878, t = 0; t < y.length; t += 16) n = s, i = m, a = g, o = v, s = l(s, m, g, v, y[t + 0], 7, 3614090360), v = l(v, s, m, g, y[t + 1], 12, 3905402710), g = l(g, v, s, m, y[t + 2], 17, 606105819), m = l(m, g, v, s, y[t + 3], 22, 3250441966), s = l(s, m, g, v, y[t + 4], 7, 4118548399), v = l(v, s, m, g, y[t + 5], 12, 1200080426), g = l(g, v, s, m, y[t + 6], 17, 2821735955), m = l(m, g, v, s, y[t + 7], 22, 4249261313), s = l(s, m, g, v, y[t + 8], 7, 1770035416), v = l(v, s, m, g, y[t + 9], 12, 2336552879), g = l(g, v, s, m, y[t + 10], 17, 4294925233), m = l(m, g, v, s, y[t + 11], 22, 2304563134), s = l(s, m, g, v, y[t + 12], 7, 1804603682), v = l(v, s, m, g, y[t + 13], 12, 4254626195), g = l(g, v, s, m, y[t + 14], 17, 2792965006), m = l(m, g, v, s, y[t + 15], 22, 1236535329), s = c(s, m, g, v, y[t + 1], 5, 4129170786), v = c(v, s, m, g, y[t + 6], 9, 3225465664), g = c(g, v, s, m, y[t + 11], 14, 643717713), m = c(m, g, v, s, y[t + 0], 20, 3921069994), s = c(s, m, g, v, y[t + 5], 5, 3593408605), v = c(v, s, m, g, y[t + 10], 9, 38016083), g = c(g, v, s, m, y[t + 15], 14, 3634488961), m = c(m, g, v, s, y[t + 4], 20, 3889429448), s = c(s, m, g, v, y[t + 9], 5, 568446438), v = c(v, s, m, g, y[t + 14], 9, 3275163606), g = c(g, v, s, m, y[t + 3], 14, 4107603335), m = c(m, g, v, s, y[t + 8], 20, 1163531501), s = c(s, m, g, v, y[t + 13], 5, 2850285829), v = c(v, s, m, g, y[t + 2], 9, 4243563512), g = c(g, v, s, m, y[t + 7], 14, 1735328473), m = c(m, g, v, s, y[t + 12], 20, 2368359562), s = u(s, m, g, v, y[t + 5], 4, 4294588738), v = u(v, s, m, g, y[t + 8], 11, 2272392833), g = u(g, v, s, m, y[t + 11], 16, 1839030562), m = u(m, g, v, s, y[t + 14], 23, 4259657740), s = u(s, m, g, v, y[t + 1], 4, 2763975236), v = u(v, s, m, g, y[t + 4], 11, 1272893353), g = u(g, v, s, m, y[t + 7], 16, 4139469664), m = u(m, g, v, s, y[t + 10], 23, 3200236656), s = u(s, m, g, v, y[t + 13], 4, 681279174), v = u(v, s, m, g, y[t + 0], 11, 3936430074), g = u(g, v, s, m, y[t + 3], 16, 3572445317), m = u(m, g, v, s, y[t + 6], 23, 76029189), s = u(s, m, g, v, y[t + 9], 4, 3654602809), v = u(v, s, m, g, y[t + 12], 11, 3873151461), g = u(g, v, s, m, y[t + 15], 16, 530742520), m = u(m, g, v, s, y[t + 2], 23, 3299628645), s = d(s, m, g, v, y[t + 0], 6, 4096336452), v = d(v, s, m, g, y[t + 7], 10, 1126891415), g = d(g, v, s, m, y[t + 14], 15, 2878612391), m = d(m, g, v, s, y[t + 5], 21, 4237533241), s = d(s, m, g, v, y[t + 12], 6, 1700485571), v = d(v, s, m, g, y[t + 3], 10, 2399980690), g = d(g, v, s, m, y[t + 10], 15, 4293915773), m = d(m, g, v, s, y[t + 1], 21, 2240044497), s = d(s, m, g, v, y[t + 8], 6, 1873313359), v = d(v, s, m, g, y[t + 15], 10, 4264355552), g = d(g, v, s, m, y[t + 6], 15, 2734768916), m = d(m, g, v, s, y[t + 13], 21, 1309151649), s = d(s, m, g, v, y[t + 4], 6, 4149444226), v = d(v, s, m, g, y[t + 11], 10, 3174756917), g = d(g, v, s, m, y[t + 2], 15, 718787259), m = d(m, g, v, s, y[t + 9], 21, 3951481745), s = r(s, n), m = r(m, i), g = r(g, a), v = r(v, o); return (p(s) + p(m) + p(g) + p(v)).toLowerCase() }

第四个参数:sign: n.md5(“fanyideskweb” + e + i + “]BjuETDhU)zqSxf-=B#7m”),相当于python中的sign=n.md5(“fanyideskweb” + e + i + “]BjuETDhU)zqSxf-=B#7m”), salt=i,e是你输入需要翻译的词语,即可写成sign=n.md5(“fanyideskweb” +str(e) + str(salt) + “]BjuETDhU)zqSxf-=B#7m”),继续选择直接扣代码。用python3调用js的库之execjs 来扣,鼠标放到 sign: n.md5(“fanyideskweb” + e + i + “]BjuETDhU)zqSxf-=B#7m”)中的n.md5()上,将会出现f md5(e)函数,点击进入,来到8196行, md5: function(e){ },把第8196行到8278行扣下来,竟然和刚才的扣bv的方法相同,仅仅是参数不同而已,bv的是 n.md5(navigator.appVersion)中的navigator.appVersion参数,现在的asin的是sign=n.md5(“fanyideskweb” +str(e) + str(salt) + “]BjuETDhU)zqSxf-=B#7m”)中的”fanyideskweb” +str(e) + str(salt) + “]BjuETDhU)zqSxf-=B#7m”参数,再应用execjs解析出sign.

注意:自调函数调用写法:

var aa=function(e){ }和function aa(e){ }写法都可以,一样的

import execjs f = open(r”text.js”,encoding=’utf-8’).read() ctx1 = execjs.compile(f) bv=ctx1.call(‘md5_2’,ua.random) print(bv)

扣出来的js代码:主体为function md52(e) { } ,应用execjs解析,缺什么参数找什么参数,即可_

var n = function(e, t) { return e << t | e >>> 32 - t } , r = function(e, t) { var n, r, i, a, o; return i = 2147483648 & e, a = 2147483648 & t, n = 1073741824 & e, r = 1073741824 & t, o = (1073741823 & e) + (1073741823 & t), n & r ? 2147483648 ^ o ^ i ^ a : n | r ? 1073741824 & o ? 3221225472 ^ o ^ i ^ a : 1073741824 ^ o ^ i ^ a : o ^ i ^ a } , i = function(e, t, n) { return e & t | ~e & n } , a = function(e, t, n) { return e & n | t & ~n } , o = function(e, t, n) { return e ^ t ^ n } , s = function(e, t, n) { return t ^ (e | ~n) } , l = function(e, t, a, o, s, l, c) { return e = r(e, r(r(i(t, a, o), s), c)), r(n(e, l), t) } , c = function(e, t, i, o, s, l, c) { return e = r(e, r(r(a(t, i, o), s), c)), r(n(e, l), t) } , u = function(e, t, i, a, s, l, c) { return e = r(e, r(r(o(t, i, a), s), c)), r(n(e, l), t) } , d = function(e, t, i, a, o, l, c) { return e = r(e, r(r(s(t, i, a), o), c)), r(n(e, l), t) } , f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } , p = function(e) { var t, n = "", r = ""; for (t = 0; t <= 3; t++) n += (r = "0" + (e >>> 8 * t & 255).toString(16)).substr(r.length - 2, 2); return n }, h = function(e) { e = e.replace(/\\x0d\\x0a/g, "\\n"); for (var t = "", n = 0; n < e.length; n++) { var r = e.charCodeAt(n); if (r < 128) t += String.fromCharCode(r); else if (r > 127 && r < 2048) t += String.fromCharCode(r >> 6 | 192), t += String.fromCharCode(63 & r | 128); else if (r >= 55296 && r <= 56319) { if (n + 1 < e.length) { var i = e.charCodeAt(n + 1); if (i >= 56320 && i <= 57343) { var a = 1024 * (r - 55296) + (i - 56320) + 65536; t += String.fromCharCode(240 | a >> 18 & 7), t += String.fromCharCode(128 | a >> 12 & 63), t += String.fromCharCode(128 | a >> 6 & 63), t += String.fromCharCode(128 | 63 & a), n++ } } } else t += String.fromCharCode(r >> 12 | 224), t += String.fromCharCode(r >> 6 & 63 | 128), t += String.fromCharCode(63 & r | 128) } return t }; f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a }

function md5_2(e) { var t, n, i, a, o, s, m, g, v, y = Array(); for (e = e, y = f(e), s = 1732584193, m = 4023233417, g = 2562383102, v = 271733878, t = 0; t < y.length; t += 16) n = s, i = m, a = g, o = v, s = l(s, m, g, v, y[t + 0], 7, 3614090360), v = l(v, s, m, g, y[t + 1], 12, 3905402710), g = l(g, v, s, m, y[t + 2], 17, 606105819), m = l(m, g, v, s, y[t + 3], 22, 3250441966), s = l(s, m, g, v, y[t + 4], 7, 4118548399), v = l(v, s, m, g, y[t + 5], 12, 1200080426), g = l(g, v, s, m, y[t + 6], 17, 2821735955), m = l(m, g, v, s, y[t + 7], 22, 4249261313), s = l(s, m, g, v, y[t + 8], 7, 1770035416), v = l(v, s, m, g, y[t + 9], 12, 2336552879), g = l(g, v, s, m, y[t + 10], 17, 4294925233), m = l(m, g, v, s, y[t + 11], 22, 2304563134), s = l(s, m, g, v, y[t + 12], 7, 1804603682), v = l(v, s, m, g, y[t + 13], 12, 4254626195), g = l(g, v, s, m, y[t + 14], 17, 2792965006), m = l(m, g, v, s, y[t + 15], 22, 1236535329), s = c(s, m, g, v, y[t + 1], 5, 4129170786), v = c(v, s, m, g, y[t + 6], 9, 3225465664), g = c(g, v, s, m, y[t + 11], 14, 643717713), m = c(m, g, v, s, y[t + 0], 20, 3921069994), s = c(s, m, g, v, y[t + 5], 5, 3593408605), v = c(v, s, m, g, y[t + 10], 9, 38016083), g = c(g, v, s, m, y[t + 15], 14, 3634488961), m = c(m, g, v, s, y[t + 4], 20, 3889429448), s = c(s, m, g, v, y[t + 9], 5, 568446438), v = c(v, s, m, g, y[t + 14], 9, 3275163606), g = c(g, v, s, m, y[t + 3], 14, 4107603335), m = c(m, g, v, s, y[t + 8], 20, 1163531501), s = c(s, m, g, v, y[t + 13], 5, 2850285829), v = c(v, s, m, g, y[t + 2], 9, 4243563512), g = c(g, v, s, m, y[t + 7], 14, 1735328473), m = c(m, g, v, s, y[t + 12], 20, 2368359562), s = u(s, m, g, v, y[t + 5], 4, 4294588738), v = u(v, s, m, g, y[t + 8], 11, 2272392833), g = u(g, v, s, m, y[t + 11], 16, 1839030562), m = u(m, g, v, s, y[t + 14], 23, 4259657740), s = u(s, m, g, v, y[t + 1], 4, 2763975236), v = u(v, s, m, g, y[t + 4], 11, 1272893353), g = u(g, v, s, m, y[t + 7], 16, 4139469664), m = u(m, g, v, s, y[t + 10], 23, 3200236656), s = u(s, m, g, v, y[t + 13], 4, 681279174), v = u(v, s, m, g, y[t + 0], 11, 3936430074), g = u(g, v, s, m, y[t + 3], 16, 3572445317), m = u(m, g, v, s, y[t + 6], 23, 76029189), s = u(s, m, g, v, y[t + 9], 4, 3654602809), v = u(v, s, m, g, y[t + 12], 11, 3873151461), g = u(g, v, s, m, y[t + 15], 16, 530742520), m = u(m, g, v, s, y[t + 2], 23, 3299628645), s = d(s, m, g, v, y[t + 0], 6, 4096336452), v = d(v, s, m, g, y[t + 7], 10, 1126891415), g = d(g, v, s, m, y[t + 14], 15, 2878612391), m = d(m, g, v, s, y[t + 5], 21, 4237533241), s = d(s, m, g, v, y[t + 12], 6, 1700485571), v = d(v, s, m, g, y[t + 3], 10, 2399980690), g = d(g, v, s, m, y[t + 10], 15, 4293915773), m = d(m, g, v, s, y[t + 1], 21, 2240044497), s = d(s, m, g, v, y[t + 8], 6, 1873313359), v = d(v, s, m, g, y[t + 15], 10, 4264355552), g = d(g, v, s, m, y[t + 6], 15, 2734768916), m = d(m, g, v, s, y[t + 13], 21, 1309151649), s = d(s, m, g, v, y[t + 4], 6, 4149444226), v = d(v, s, m, g, y[t + 11], 10, 3174756917), g = d(g, v, s, m, y[t + 2], 15, 718787259), m = d(m, g, v, s, y[t + 9], 21, 3951481745), s = r(s, n), m = r(m, i), g = r(g, a), v = r(v, o); return (p(s) + p(m) + p(g) + p(v)).toLowerCase() }

9.大功告成,四个可变参数全部搞定,即可搞定&

附上完整代码 python部分:

import time import execjs import random import requests import json from fake_useragent import UserAgent # var aa=function(e) function aa(e)

class YouDaoTanslate(): def init(self): #类方法调用 self.js_par() def js_par(self): translation_data = input(“请输入要翻译的词或者句子:”) # 13位时间戳获取方法:单位:毫秒 ua = UserAgent() print(ua.random) t1 = time.time() ts = int(t1 * 1000) print(ts) salt = str(int(ts)) + str(random.randint(0, 10)) print(salt)

f = open(r”text.js”, encoding=’utf-8’).read() ctx1 = execjs.compile(f) bv = ctx1.call(‘md5_1’, ua.random) print(bv)

ctx2 = execjs.compile(f) sign_data = ‘fanyideskweb’ + translation_data + salt + ‘mmbP%A-r6U3Nw(n]BjuEU’ sign = ctx2.call(‘md5_2’, sign_data) print(sign)

self.get_translateData(translation_data,ua,ts,salt,bv,sign) def get_translateData(self,translation_data,ua,ts,salt,bv,sign): url = ‘http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule‘ headers = { ‘Host’: ‘fanyi.youdao.com’, ‘Origin’: ‘http://fanyi.youdao.com‘, ‘Referer’: ‘http://fanyi.youdao.com/‘, ‘User-Agent’: ua.random, ‘X-Requested-With’: ‘XMLHttpRequest’, ‘Cookie’: ‘OUTFOX_SEARCH_USER_ID_NCOO=892433278.3204294; OUTFOX_SEARCH_USER_ID=”-1911793285@10.108.160.19”; _ntes_nnid=d2bb7793f13c9a83907e33d40665337a,1597158692753; JSESSIONID=aaamUZK4O580YaKjmjIpx; _rltest__cookies=1597227802599’ } data = { ‘i’: translation_data, ‘from’: ‘AUTO’, ‘to’: ‘AUTO’, ‘smartresult’: ‘dict’, ‘client’: ‘fanyideskweb’, ‘salt’: str(salt), ‘sign’: str(sign), ‘ts’: str(ts), ‘bv’: str(bv), ‘doctype’: ‘json’, ‘version’: ‘2.1’, ‘keyfrom’: ‘fanyi.web’, ‘action’: ‘FY_BY_CLICKBUTTION’ }

response = requests.post(url=url, data=data, headers=headers)

response = requests.post(url=url, data=data, headers=headers) translate_results = json.loads(response.text) # 找到翻译结果 if ‘translateResult’ in translate_results: translate_results = translate_results[‘translateResult’][0][0][‘tgt’] print(“翻译的结果是:%s” % translate_results)

else: print(translate_results)

self.js_par()

YouDaoTanslate()

js部分:

var n = function(e, t) { return e << t | e >>> 32 - t } , r = function(e, t) { var n, r, i, a, o; return i = 2147483648 & e, a = 2147483648 & t, n = 1073741824 & e, r = 1073741824 & t, o = (1073741823 & e) + (1073741823 & t), n & r ? 2147483648 ^ o ^ i ^ a : n | r ? 1073741824 & o ? 3221225472 ^ o ^ i ^ a : 1073741824 ^ o ^ i ^ a : o ^ i ^ a } , i = function(e, t, n) { return e & t | ~e & n } , a = function(e, t, n) { return e & n | t & ~n } , o = function(e, t, n) { return e ^ t ^ n } , s = function(e, t, n) { return t ^ (e | ~n) } , l = function(e, t, a, o, s, l, c) { return e = r(e, r(r(i(t, a, o), s), c)), r(n(e, l), t) } , c = function(e, t, i, o, s, l, c) { return e = r(e, r(r(a(t, i, o), s), c)), r(n(e, l), t) } , u = function(e, t, i, a, s, l, c) { return e = r(e, r(r(o(t, i, a), s), c)), r(n(e, l), t) } , d = function(e, t, i, a, o, l, c) { return e = r(e, r(r(s(t, i, a), o), c)), r(n(e, l), t) } , f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } , p = function(e) { var t, n = "", r = ""; for (t = 0; t <= 3; t++) n += (r = "0" + (e >>> 8 * t & 255).toString(16)).substr(r.length - 2, 2); return n }, h = function(e) { e = e.replace(/\\x0d\\x0a/g, "\\n"); for (var t = "", n = 0; n < e.length; n++) { var r = e.charCodeAt(n); if (r < 128) t += String.fromCharCode(r); else if (r > 127 && r < 2048) t += String.fromCharCode(r >> 6 | 192), t += String.fromCharCode(63 & r | 128); else if (r >= 55296 && r <= 56319) { if (n + 1 < e.length) { var i = e.charCodeAt(n + 1); if (i >= 56320 && i <= 57343) { var a = 1024 * (r - 55296) + (i - 56320) + 65536; t += String.fromCharCode(240 | a >> 18 & 7), t += String.fromCharCode(128 | a >> 12 & 63), t += String.fromCharCode(128 | a >> 6 & 63), t += String.fromCharCode(128 | 63 & a), n++ } } } else t += String.fromCharCode(r >> 12 | 224), t += String.fromCharCode(r >> 6 & 63 | 128), t += String.fromCharCode(63 & r | 128) } return t }; f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 * ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 * 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 * 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } function md5_1(e) { var t, n, i, a, o, s, m, g, v, y = Array(); for (e = e, y = f, s = 1732584193, m = 4023233417, g = 2562383102, v = 271733878, t = 0; t < y.length; t += 16) n = s, i = m, a = g, o = v, s = l(s, m, g, v, y[t + 0], 7, 3614090360), v = l(v, s, m, g, y[t + 1], 12, 3905402710), g = l(g, v, s, m, y[t + 2], 17, 606105819), m = l(m, g, v, s, y[t + 3], 22, 3250441966), s = l(s, m, g, v, y[t + 4], 7, 4118548399), v = l(v, s, m, g, y[t + 5], 12, 1200080426), g = l(g, v, s, m, y[t + 6], 17, 2821735955), m = l(m, g, v, s, y[t + 7], 22, 4249261313), s = l(s, m, g, v, y[t + 8], 7, 1770035416), v = l(v, s, m, g, y[t + 9], 12, 2336552879), g = l(g, v, s, m, y[t + 10], 17, 4294925233), m = l(m, g, v, s, y[t + 11], 22, 2304563134), s = l(s, m, g, v, y[t + 12], 7, 1804603682), v = l(v, s, m, g, y[t + 13], 12, 4254626195), g = l(g, v, s, m, y[t + 14], 17, 2792965006), m = l(m, g, v, s, y[t + 15], 22, 1236535329), s = c(s, m, g, v, y[t + 1], 5, 4129170786), v = c(v, s, m, g, y[t + 6], 9, 3225465664), g = c(g, v, s, m, y[t + 11], 14, 643717713), m = c(m, g, v, s, y[t + 0], 20, 3921069994), s = c(s, m, g, v, y[t + 5], 5, 3593408605), v = c(v, s, m, g, y[t + 10], 9, 38016083), g = c(g, v, s, m, y[t + 15], 14, 3634488961), m = c(m, g, v, s, y[t + 4], 20, 3889429448), s = c(s, m, g, v, y[t + 9], 5, 568446438), v = c(v, s, m, g, y[t + 14], 9, 3275163606), g = c(g, v, s, m, y[t + 3], 14, 4107603335), m = c(m, g, v, s, y[t + 8], 20, 1163531501), s = c(s, m, g, v, y[t + 13], 5, 2850285829), v = c(v, s, m, g, y[t + 2], 9, 4243563512), g = c(g, v, s, m, y[t + 7], 14, 1735328473), m = c(m, g, v, s, y[t + 12], 20, 2368359562), s = u(s, m, g, v, y[t + 5], 4, 4294588738), v = u(v, s, m, g, y[t + 8], 11, 2272392833), g = u(g, v, s, m, y[t + 11], 16, 1839030562), m = u(m, g, v, s, y[t + 14], 23, 4259657740), s = u(s, m, g, v, y[t + 1], 4, 2763975236), v = u(v, s, m, g, y[t + 4], 11, 1272893353), g = u(g, v, s, m, y[t + 7], 16, 4139469664), m = u(m, g, v, s, y[t + 10], 23, 3200236656), s = u(s, m, g, v, y[t + 13], 4, 681279174), v = u(v, s, m, g, y[t + 0], 11, 3936430074), g = u(g, v, s, m, y[t + 3], 16, 3572445317), m = u(m, g, v, s, y[t + 6], 23, 76029189), s = u(s, m, g, v, y[t + 9], 4, 3654602809), v = u(v, s, m, g, y[t + 12], 11, 3873151461), g = u(g, v, s, m, y[t + 15], 16, 530742520), m = u(m, g, v, s, y[t + 2], 23, 3299628645), s = d(s, m, g, v, y[t + 0], 6, 4096336452), v = d(v, s, m, g, y[t + 7], 10, 1126891415), g = d(g, v, s, m, y[t + 14], 15, 2878612391), m = d(m, g, v, s, y[t + 5], 21, 4237533241), s = d(s, m, g, v, y[t + 12], 6, 1700485571), v = d(v, s, m, g, y[t + 3], 10, 2399980690), g = d(g, v, s, m, y[t + 10], 15, 4293915773), m = d(m, g, v, s, y[t + 1], 21, 2240044497), s = d(s, m, g, v, y[t + 8], 6, 1873313359), v = d(v, s, m, g, y[t + 15], 10, 4264355552), g = d(g, v, s, m, y[t + 6], 15, 2734768916), m = d(m, g, v, s, y[t + 13], 21, 1309151649), s = d(s, m, g, v, y[t + 4], 6, 4149444226), v = d(v, s, m, g, y[t + 11], 10, 3174756917), g = d(g, v, s, m, y[t + 2], 15, 718787259), m = d(m, g, v, s, y[t + 9], 21, 3951481745), s = r(s, n), m = r(m, i), g = r(g, a), v = r(v, o); return (p(s) + p(m) + p(g) + p(v)).toLowerCase() }

var n = function(e, t) { return e << t | e >>> 32 - t } , r = function(e, t) { var n, r, i, a, o; return i = 2147483648 & e, a = 2147483648 & t, n = 1073741824 & e, r = 1073741824 & t, o = (1073741823 & e) + (1073741823 & t), n & r ? 2147483648 ^ o ^ i ^ a : n | r ? 1073741824 & o ? 3221225472 ^ o ^ i ^ a : 1073741824 ^ o ^ i ^ a : o ^ i ^ a } , i = function(e, t, n) { return e & t | ~e & n } , a = function(e, t, n) { return e & n | t & ~n } , o = function(e, t, n) { return e ^ t ^ n } , s = function(e, t, n) { return t ^ (e | ~n) } , l = function(e, t, a, o, s, l, c) { return e = r(e, r(r(i(t, a, o), s), c)), r(n(e, l), t) } , c = function(e, t, i, o, s, l, c) { return e = r(e, r(r(a(t, i, o), s), c)), r(n(e, l), t) } , u = function(e, t, i, a, s, l, c) { return e = r(e, r(r(o(t, i, a), s), c)), r(n(e, l), t) } , d = function(e, t, i, a, o, l, c) { return e = r(e, r(r(s(t, i, a), o), c)), r(n(e, l), t) } , f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a } , p = function(e) { var t, n = “”, r = “”; for (t = 0; t <= 3; t++) n += (r = “0” + (e >>> 8 t & 255).toString(16)).substr(r.length - 2, 2); return n }, h = function(e) { e = e.replace(/\x0d\x0a/g, “\n”); for (var t = “”, n = 0; n < e.length; n++) { var r = e.charCodeAt(n); if (r < 128) t += String.fromCharCode(r); else if (r > 127 && r < 2048) t += String.fromCharCode(r >> 6 | 192), t += String.fromCharCode(63 & r | 128); else if (r >= 55296 && r <= 56319) { if (n + 1 < e.length) { var i = e.charCodeAt(n + 1); if (i >= 56320 && i <= 57343) { var a = 1024 (r - 55296) + (i - 56320) + 65536; t += String.fromCharCode(240 | a >> 18 & 7), t += String.fromCharCode(128 | a >> 12 & 63), t += String.fromCharCode(128 | a >> 6 & 63), t += String.fromCharCode(128 | 63 & a), n++ } } } else t += String.fromCharCode(r >> 12 | 224), t += String.fromCharCode(r >> 6 & 63 | 128), t += String.fromCharCode(63 & r | 128) } return t }; f = function(e) { for (var t, n = e.length, r = n + 8, i = 16 ((r - r % 64) / 64 + 1), a = Array(i - 1), o = 0, s = 0; s < n; ) o = s % 4 8, a[t = (s - s % 4) / 4] = a[t] | e.charCodeAt(s) << o, s++; return t = (s - s % 4) / 4, o = s % 4 8, a[t] = a[t] | 128 << o, a[i - 2] = n << 3, a[i - 1] = n >>> 29, a }

function md5_2(e) { var t, n, i, a, o, s, m, g, v, y = Array(); for (e = e, y = f(e), s = 1732584193, m = 4023233417, g = 2562383102, v = 271733878, t = 0; t < y.length; t += 16) n = s, i = m, a = g, o = v, s = l(s, m, g, v, y[t + 0], 7, 3614090360), v = l(v, s, m, g, y[t + 1], 12, 3905402710), g = l(g, v, s, m, y[t + 2], 17, 606105819), m = l(m, g, v, s, y[t + 3], 22, 3250441966), s = l(s, m, g, v, y[t + 4], 7, 4118548399), v = l(v, s, m, g, y[t + 5], 12, 1200080426), g = l(g, v, s, m, y[t + 6], 17, 2821735955), m = l(m, g, v, s, y[t + 7], 22, 4249261313), s = l(s, m, g, v, y[t + 8], 7, 1770035416), v = l(v, s, m, g, y[t + 9], 12, 2336552879), g = l(g, v, s, m, y[t + 10], 17, 4294925233), m = l(m, g, v, s, y[t + 11], 22, 2304563134), s = l(s, m, g, v, y[t + 12], 7, 1804603682), v = l(v, s, m, g, y[t + 13], 12, 4254626195), g = l(g, v, s, m, y[t + 14], 17, 2792965006), m = l(m, g, v, s, y[t + 15], 22, 1236535329), s = c(s, m, g, v, y[t + 1], 5, 4129170786), v = c(v, s, m, g, y[t + 6], 9, 3225465664), g = c(g, v, s, m, y[t + 11], 14, 643717713), m = c(m, g, v, s, y[t + 0], 20, 3921069994), s = c(s, m, g, v, y[t + 5], 5, 3593408605), v = c(v, s, m, g, y[t + 10], 9, 38016083), g = c(g, v, s, m, y[t + 15], 14, 3634488961), m = c(m, g, v, s, y[t + 4], 20, 3889429448), s = c(s, m, g, v, y[t + 9], 5, 568446438), v = c(v, s, m, g, y[t + 14], 9, 3275163606), g = c(g, v, s, m, y[t + 3], 14, 4107603335), m = c(m, g, v, s, y[t + 8], 20, 1163531501), s = c(s, m, g, v, y[t + 13], 5, 2850285829), v = c(v, s, m, g, y[t + 2], 9, 4243563512), g = c(g, v, s, m, y[t + 7], 14, 1735328473), m = c(m, g, v, s, y[t + 12], 20, 2368359562), s = u(s, m, g, v, y[t + 5], 4, 4294588738), v = u(v, s, m, g, y[t + 8], 11, 2272392833), g = u(g, v, s, m, y[t + 11], 16, 1839030562), m = u(m, g, v, s, y[t + 14], 23, 4259657740), s = u(s, m, g, v, y[t + 1], 4, 2763975236), v = u(v, s, m, g, y[t + 4], 11, 1272893353), g = u(g, v, s, m, y[t + 7], 16, 4139469664), m = u(m, g, v, s, y[t + 10], 23, 3200236656), s = u(s, m, g, v, y[t + 13], 4, 681279174), v = u(v, s, m, g, y[t + 0], 11, 3936430074), g = u(g, v, s, m, y[t + 3], 16, 3572445317), m = u(m, g, v, s, y[t + 6], 23, 76029189), s = u(s, m, g, v, y[t + 9], 4, 3654602809), v = u(v, s, m, g, y[t + 12], 11, 3873151461), g = u(g, v, s, m, y[t + 15], 16, 530742520), m = u(m, g, v, s, y[t + 2], 23, 3299628645), s = d(s, m, g, v, y[t + 0], 6, 4096336452), v = d(v, s, m, g, y[t + 7], 10, 1126891415), g = d(g, v, s, m, y[t + 14], 15, 2878612391), m = d(m, g, v, s, y[t + 5], 21, 4237533241), s = d(s, m, g, v, y[t + 12], 6, 1700485571), v = d(v, s, m, g, y[t + 3], 10, 2399980690), g = d(g, v, s, m, y[t + 10], 15, 4293915773), m = d(m, g, v, s, y[t + 1], 21, 2240044497), s = d(s, m, g, v, y[t + 8], 6, 1873313359), v = d(v, s, m, g, y[t + 15], 10, 4264355552), g = d(g, v, s, m, y[t + 6], 15, 2734768916), m = d(m, g, v, s, y[t + 13], 21, 1309151649), s = d(s, m, g, v, y[t + 4], 6, 4149444226), v = d(v, s, m, g, y[t + 11], 10, 3174756917), g = d(g, v, s, m, y[t + 2], 15, 718787259), m = d(m, g, v, s, y[t + 9], 21, 3951481745), s = r(s, n), m = r(m, i), g = r(g, a), v = r(v, o); return (p(s) + p(m) + p(g) + p(v)).toLowerCase() }

Python

本篇目标

  • 了解为什么我们需要直接调用 JavaScript
  • 了解常见的 Python 调用 JavaScript 的库
  • 了解一种性能更高的操作方式
  • 知道什么场景下应该使用什么方式进行调用

通过本文的学习,在你写爬虫时,你应该会对调用 JavaScript 有一个更清晰的了解,并且你还要了解到一些你平时可能见不到的骚操作。

大家如果接触过 JavaScript 逆向的话,应该都知道,通常来说碰到 JS 逆向网站时会有这两种情况:

  • 简单 JS 破解:通过 Python 代码轻松实现
  • 复杂的 JS 破解:代码不容易重写,使用程序直接调用 JS 运行获取结果。

对于简单的 JS 来说,我们可以通过 Python 代码,直接重写,轻轻松松的就能搞定。 而对于复的 JS 代码而言呢,由于代码过于复杂,重写太费时费力,且碰到对方更新这就比较麻烦了。所以,我们一般直接使用程序去调用 JS,在 Python 层面就只是获取一个运行结果,这样做相比于重写而言就方便多了。 那么,接下来我带大家看一下两种比较简单的 JS 代码重写。 本文涉及的所有演示代码请到公众号:AI悦创,后台回复:PJS 。来获取即可!

1. Base64

首先,我们先来看一下 Base64 ,Base64 是我们再写爬虫过程中经常看到的一种编码方式。这边我们来写两个例子。

1
2
3
4
// 原字符
NightTeam
// 编码之后的:
TmlnaHRUZWFt

第一个例子如上,是 NightTeam 经过编码是如上面的结果(TmlnaHRUZWFt),如果我们只是通过这个结果来分析的话,它的特征不是很明显。如果是见的不多或者是新手小白的同学,并不会把它往 Base64 方向去想。 然后,我们来看一下第二个例子:

1
2
3
4
5
6
7
8
9
// 原字符
aiyuechuang
// 编码之后的:
YWl5dWVjaHVhbmc=

// 原字符
Python3
// 编码之后
UHl0aG9uMw==

第二个例子是 aiyuechuang 编码之后的结果,它的末尾有一个等号,Python3 编码之后末尾有两个等号,这个特征相对第一个就比较明显了。一般我们看到尾号有两个等号时应该大概可以猜到这个就是 Base64 了。 然后,直接解码看一看,如果没有什么特别的话,就可以使用 Python 进行重写了。 同学可以使用以下链接 Base64 编码解码的测试学习:http://tool.alixixi.com/base64/ 不过 Base64 也会有一些骚操作,碰到那种情况的时候,我们如果用 Python 重写可能有点麻烦。具体的内容我会在后面的的课程中,单独的跟大家详细的讲解。

2. MD5

第二个的话就是 MD5 ,MD5 在 Javascript 中并没有标准的库,一般我们都是使用开源库去操作。

注意:md5 的话是 哈希 并不是加密。

下面我来看一个 js 实现 md5 的一个例子: md5.js 上面的代码时被混淆过的,但是它的主要一个特征还是比较明显的,有一个入口函数:console.log(hex_md5("aiyuechuang")) 我们可以使用命令行运行一下结果,命令如下:

1
node md5.js

上面的代码自行复制保存为 md5.js 然后运行。

运行结果:

1
2
$ node md5.js
e55babec7f5d5cf7bac7872f0481bec1

我们数一下输出的结果的话,会发现这正好 是 32位,通常我们看到 32 位的一个英文数字混合的字符串,应该马上就能想到时 md5 了,这两个操作的话,因为在 Python 中都有对应的库,分别是:Base64 和 hashlib ,大家应该都知道这个我就不多说了。 例程:Base64 和 hashlib

1
2
3
4
5
6
import base64  
str1 = b'aiyuechuang'
str2 = base64.b64encode(str1)
print(str2)
str3 = base64.b64decode('YWl5dWVjaHVhbmc=')
print(str3)

输出

1
2
3
b'YWl5dWVjaHVhbmc='
b'aiyuechuang'
[Finished in 0.2s]
1
2
3
4
5
import hashlib

data = "aiyuechuang"
result = hashlib.md5(data.encode(encoding = "UTF-8")).hexdigest()
print(result)

输出

1
2
e55babec7f5d5cf7bac7872f0481bec1
[Finished in 0.1s]

像我们前面看到的那些代码,都是比较简单的,他们的算法部分也没有经过修改,所以我们可以使用其他语言和对应的库进行重写。 但是如果对方把算法部分做了一些改变呢? 如果代码量比较大也被混淆到看不出特征了,连操作后产生的字符串都看不出,我们就无法直接使用一个现成的库来复写操作了。 而且这种情况下的代码量太大了,直接对着代码重写成 Python 版本也不太现实,对方一更新你就得再重新看一遍,这样显然时非常麻烦的,也非常耗时。 那么有没有一种更高效的方法呢? 显然是有的,接下来我们来讲如何通过程序来直接调用 JavaScript 代码,也就是碰到复杂的 JS 时候的处理。

  1. 使用 Python 调用 JS
  2. 一种性能更高的调用方式
  3. 到底选择哪种方案比较好

首先,我会分享一些使用 Python 调用 JavaScript 的方式,然后会介绍一种性能更高的调用。以及具体使用哪种调用方式以及怎么选择性的使用,最后我会总结一下这些方案存在的小问题。并且会告诉你如何踩坑。

3. 使用 Python 调用 JS

我们接下来首先讲一下 Python 中调用 JavaScript。

  • PyV8
  • Js2Py
  • PyExecJS
  • PyminiRacer
  • Selenium
  • Pyppeteer

Python 调用 JS 库的话,光是我了解的话,目前就有这么一堆,接下来我们就来依次来介绍这些库。

3.1 PyV8

  • V8 是谷歌开源的 JavaScript 引擎,被使用在了 Chrome 中
  • PyV8 是 V8 引擎的一个 Python 层的包装,可以用来调用 V8 引擎执行 JS 代码
  • 网上有很多使用它来执行 JS 代码的文章
  • 年久失修,最新版本是 2010年的(https://pypi.org/project/PyV8/#history)
  • 存在内存泄漏问题,所以不建议使用

首先来看一下什么是 PyV8,V8 是谷歌开源的 JavaScript 引擎,被使用在了 Chrome 浏览器中,后来因为有人想在 Python 上调用它(V8),于是就有了 PyV8。 那 PyV8 实际上是 V8 引擎的一个 Python 层的包装,可以用来调用 V8 引擎执行 JS 代码,但是这个我不推荐使用它,那我既然不推荐大家使用,我为什么又要讲它呢? 其实,是这样的: 虽然目前网上有很多文章使用它执行 JS 代码,但是这个 PyV8 实际上已经年久失修了,而且它最新的一个正式版本还是 2010年的,可见是有多久远了,链接在上方可以执行访问查看。而且,如果你实际使用过的话,你应该会发现它存在一些内存泄漏的问题。 所以,这边我拿出来说一下,避免有人踩坑。接下来我们来说一下第二个 JS2Py。

3.2 Js2Py

  • Js2Py 是一个纯 Python 实现的 JavaScript 解释器和翻译器
  • 虽然 2019年依然有更新,但那也是 6月份的事情了,而且它的 issues 里面有很多的 bug 没有修复(https://github.com/PiotrDabkowski/Js2Py/issues)。

Js2Py 是一个纯 Python 实现的 JavaScript 解释器和翻译器,它和 PyV8 一样,也是有挺多文章提到这个库,然后来调用 JS 代码。 但是,Js2Py 虽然在2019年仍然更新,但那也是 6月份的事情了,而且它的 issues 里面有很多的 bug 没有修复(https://github.com/PiotrDabkowski/Js2Py/issues)。另外,Js2Py 本身也存在一些问题,就解释器部分来说: 解释器部分:

  • 性能不高
  • 存在一些 BUG

那不仅仅就解释器部分,还有翻译器部分:

  • 对于高度混淆的大型 JS 会转换失败
  • 而且转换出来的代码可读性差、性能不高

总之来讲,它在各个方面来说都不太适合我们的工作场景,所以也是不建议大家使用的。

3.3 PyMinRacer

  • 同样是 V8 引擎的包装,和 PyV8 的效果一样
  • 一个继任 PyExecJS 和 PyramidV8 的库
  • 一个比较新的库

这个库也是一个 PyV8 引擎包装,它的效果和 PyV8 的效果一样的。 而且作者号称这是一个继任 PyExecJS 和 PyramidV8 的库,乍眼一看挺唬人的,不过由于它是一个比较新的库,我这边就没有过多的尝试了,也没有再实际生产环境中使用过,所以不太清楚会有什么坑,感兴趣的朋友,大家可以自己去尝试一下。

3.4 PyExecJS

  • 一个最开始诞生于 Ruby 中的库,后来被移植到了 Python 上
  • 较新的文章一般都会说用它来执行 JS 代码
  • 有多个引擎可选,但一般我们会选择使用 NodeJS 作为引擎来执行代码

接下来我要说的是 PyExecJS ,这个库一个最开始诞生于 Ruby 中的库,后来人被移植到了 Python 上,目前看到一些比较新的文章都是用它来执行 JS 代码的,然后它是有多个引擎可以选择的,我们一般选择 NodeJS 作为它的一个引擎执行代码,毕竟 NodeJS 的速度是比较快的而且配置起来比较简单,那我带大家来看一下 PyExecjs 的使用。

3.4.5 PyExecJS 的使用

  1. 安装 JS 运行环境这里推荐安装 Node.js,安装方便,执行效率也高。

首先我们就是要安装引擎了,这个引擎指的就是 JS 的一个运行环境,这边推荐使用 Node.js。

注意:虽然 Windows 上有个系统自带的 JScript,可以用来作为 PyExecjs 的引擎,但是这个 JScript 很容易与其他的引擎有一个不一样的地方,容易踩到一些奇奇怪怪的坑。所以请大家务必要安装一个其他的引擎。比如说我们这里安装 Node.js 。

那上面装完 Nodejs 之后呢,我们就需要执行安装 PyExecjs 了:

  1. 安装 PyExecJS

    1
    pip install pyexecjs

这边我们使用上面的 pip 就可以进行安装了。 那么我们现在环境就准备好了,可以开始运行了。

  1. 代码示例(检测运行环境)

首先,我们打开 IPython 终端,执行一下一下两行代码,以下也给出了运行结果:

1
2
3
4
In [1]: import execjs

In [2]: execjs.get().name # 查看调用环境
Out[2]: 'Node.js (V8)'

execjs.get() # 查看调用的环境用此来看看我们的库能不能检测到 nodejs,如果不能的话那就需要手动设置一下,不过一般像我上面一样正常输出 node.js 就可以了。 如果,你检测出来的引擎不是 node.js 的话,那你就需要手动设置一下了,这里有两种设置形式,我在下方给你写出来了: 选择不同引擎进行解析

1
2
3
4
5
6
# 长期使用
os.environ["EXECJS_RUNTIME"]="Node"

# 临时使用
import execjs.runtime_names
node=execjs.get(execjs.runtime_names.Node)

由上边可知,我们有两种形式:一种是长期使用的,通过环境变量的形式,通过把环境变量改成大写的 EXECJS_RUNTIME 然后将其值赋值为 Node。 另一种的话,将它改成临时使用的一种方式,这种是直接使用 get,这种做法的话,你在使用的时候就需要使用 node 变量了,不能直接导入 PyExecjs 来直接开始使用,相对麻烦一些。 接下来,就让我们正式使用 PyExecJS 这个包吧。

1
2
3
4
5
6
In [8]: import execjs

In [9]: e = execjs.eval('a = new Array(1, 2, 3)') # 可以直接执行 JS 代码

In [10]: print(e)
[1, 2, 3]

PyExecjs 最简单的用法就是导入包,然后通过 eval 这个方法并传入简单的 JS 代码来执行。但是我们正常情况下肯定不会这么使用,因为我们的 JS 代码是比较复杂的而且 JS 代码内容也是比较多的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding: utf-8 -*-
# @Author: clela
# @Date: 2020-03-24 13:54:27
# @Last Modified by: aiyuechuang
# @Last Modified time: 2020-04-03 08:44:15
# @公众号:AI悦创

In [12]: import execjs

In [13]: jstext = """
...: function hello(str){return str;}
...: """

In [14]: ctx = execjs.compile(jstext) # 编译 JS 代码

In [15]: a = ctx.call("hello", "hello aiyc")

In [16]: print(a)
hello aiyc

这样的话,我们一般通过使用第二种方式,第二种方式是通过使用 compile 对 JS 字符串进行编译,这个编译操作其实就是把参数(jstext)里面的那段 JS 代码给放到一个叫 Context 的上下文中,它并不是我们平时编译程序所说的编译。然后我们 调用 call 方法进行执行。 第一个参数是我们调用 JS 中的的函数名,也就是 hello。然后后面跟着的 hello aiyc 就是参数,也就是我们 JS 中需要传入到 str 的参数。如果 JS 中存在多个参数,我们就直接在后面打个逗号,然后接着写下一个参数就好了。 接下来我们来看一个具体的代码: aes_demo.js 这边我准备了一个 CryptoJS 的一个 JS 文件,CryptoJS 它是一个包含各种加密哈希编码算法的一个开源库,很多网站都会用它提供的函数来生成参数,那么这边我是写了如上面这样的代码,用来调用它里面的 AES 加密参数,来加密一下我提供的字符串。

注意:JS 代码不要放在和 Python 代码同一个文件中,尽量放在单独的 js 文件中,因为我们的 JS 文件内容比较多。然后通过读取文件的方式,

run_aes.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Python 文件:run_aes.py
# -*- coding: utf-8 -*-
# @时间 : 2020-04-06 00:00
# @作者 : AI悦创
# @文件名 : run_aes.py
# @公众号: AI悦创
from pprint import pprint

import execjs
import pathlib
import os

js_path = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
js_path = js_path / "crypto.js"
with js_path.open('r', encoding="utf-8") as f:
script = f.read()

c = "1234"

# 传入python中的变量
add = ('''
aesEncrypt = function() {
result={}
var t = CryptoJS.MD5("login.xxx.com"),
i = CryptoJS.enc.Utf8.parse(t),
r = CryptoJS.enc.Utf8.parse("1234567812345678"),
u = CryptoJS.AES.encrypt(''' + "'{}'".format(c) + ''',i, {
iv: r
});
result.t=t.toString()
result.i =i.toString()
result.r =r.toString()
result.u =u.toString()
return result
};
''')
script = script + add
print("script",script)

x = execjs.compile(script)
result = x.call("aesEncrypt")
print(result)

这里我通过读取文件的方式,将 js 文件读取进来,把代码读取到我们的字符串里面,这样一方面方便我们管理,另一方面也可以直接通过代码检测自动补全功能,使用起来会比较方便。 然后,这里我们有一个小技巧,我们可以通过 format 字符串拼接的形式,将 Python 中的变量,也就是上面的变量 c 然后将这个变量写入到 Js 代码中,从而变相的实现了通过调用 JS 函数,在没有参数的情况下修改 JS 代码中的特定变量的值。最后我们拼接好了我我们的 JS 代码(add 和 script)。 拼完 JS 代码之后,我们这边再常规的进行一个操作,调用 Call 方法执行 aesEncrypt 这样一个函数,需要注意的是,这个代码里面 return 出来的 JS,它是一个 object,JS 中的 object 也就是 Python 中的字典 我们实际使用时,如果需要在 Python 中拿到 object 的话,建议把它转换成一个 json 字符串,而不是直接的把结果 return 出来。 因为,有些时候 PyExecjs 对 object 的转换会出现问题,所以我们可能会拿到一些类似于将字典直接用 str 函数包裹后转为字符串的一个东西,这样的话它是无法通过正常的方式去解析的。 或者说你也可能会遇到其情况的报错,总之大家最好先转一下 json 字符串,然后再 return 避免踩坑。这是我们的一个代码。 接下来我们来说一下,PyExecJS 存在的一些问题主要有以下两点:

  • 执行大型 JS 时会有点慢(这个是因为,每次执行 JS 代码的时候,都是从命令行去调用到的 JS,所以 JS 代码越复杂的话,nodejs 的初始化时间就越长,这个基本上是无解的)
  • 特殊编码的输入或输出参数会出现报错的情况(因为,是从命令行调用的,所以在碰到一些特殊字符输入或输出参数或者 JS 代码本身就有一些特殊字符的情况下,就会直接执行不了,给你抛出一个异常。不过这个跟系统的命令行默认编码有一定关系,具体的话这里就不深究了,直接就说解决方案吧。)
  • 可以把输入或输出的参数使用 Base64 编码一下(如果看报错是 JS 代码部分导致的,那就去看看能不能删除代码中的那部分字符或者你自己 new 一个上下文对象,将那个名叫 tempfile 的参数打开,这样在调用的时候,它就直接去执行那个文件了,不过大量调用的情况下,可能会对磁盘造成一定压力。

而如果参数不充分导致的话,有个很简单的方法:就是把参数使用 Base64 编码一下,因为编码之后出来的字符串,我们知道 Base64 编码之后是生成英文和数字组成的。这样就没有特殊符号了。所以就不会出现问题了。) 关于 PyExecejs 的相关东西就介绍到这里了,我们来看一些其他的内容。

3.5 其他使用 Python 调用 JS 的骚操作

前面说的都是非浏览器环境下直接调用 JS 的操作,但是还有一些市面上根本没人提到的骚操作,其实也挺好用的,接下来我给大家介绍一下:

  1. Selenium
  • 一个 web 自动化测试框架,可以驱动各种浏览器进行模拟人工操作
  • 用于渲染页面以方便提取数据或过验证码
  • 也可以直接驱动浏览器执行 JS

这个大家是比较熟悉的,它是一个外部自动化的测试框架,可以驱动各种浏览器进行模拟人工操作,很多文章或者培训班的课程,都会提到它在爬虫方面的一个使用,比如用它采集一些动态页面,或者用来过一些滑动验证码之类的。 不过我们这里不用它来做这些事,我们要做的是用它来执行 JS 代码,因为这样的话是直接在浏览器环境下执行的 ,所以的话它是省了很多事,那么 Selenium 执行 JS 的核心代码,实际上就下面一行:

1
2
js = "一大段 JS"
result = browser.execute_script(js)

我们来看一下实际的例子: SeleniumDemo 进入项目根目录,输入:python server.py

1
2
3
4
5
6
7
8
9
10
$ python server.py
* Serving Flask app "server" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 262-966-819
* Running on http://0.0.0.0:5002/ (Press CTRL+C to quit)

访问 localhost:5002 我们进入网页之后,有这样的一句话:


每次刷新都会显示不同的内容,查看源代码的话,会发现这个页面中的源代码里面没有对应页面显示的那句话,而是只有一个 input 标签。 我还能观察到,input 标签里面有两个属性,一个是 id、一个是 data,这两个是比较关键的属性,然后我们还发现这里面引用了一个 js 文件,所以这个网页最终结果实际上是通过 JS 文件,然后一系列的操作生成的,那接下来我就来看看 JS 文件,做了什么工作。 js 我们可以看见,这个 JS 文件最后一句,有一个 window.onload =doit 的这样一代码,这个我们知道,当页面加载完成之后,立即执行这个 JS 方法。

1
2
3
4
5
6
7
8
9
function doit() {
let browser_type=BrowserType();
console.log(browser_type)
let supporter =browser_type.supporter
if(supporter==="chrome"){
Base64.run('base64', 'data',supporter)
}

}

然后这个方法里面做了一个这样一个操作:let browser_type=BrowserType(); 首先去判断 supporter 是否等于 Chrome 这个 supporter 实际上有一个 browser_type 这个 browser_type 实际上就是检测浏览器等一系列参数,然后我们获取它里面的 supporter 属性,当 supportersupporter =browser_type.supporter )等于 Chrome 的时候,我们再去执行这个 run 函数。

1
2
3
4
5
6
7
8
run: function (id, attr,supporter) {
let all_str = $(id).getAttribute(attr)
let end_index=supporter.length+58
Base64._keyStr = all_str.substring(0, end_index)
let charset = all_str.substring(64, all_str.length)
let encoded = Base64.decode(charset,supporter);
$(id).value = encoded;
}

也就是 run 函数里面做了一系列操作,然后我传入的 id 可以通过看一下上面的函数 doit 可知传入的是 Base64 也就是说,实际上对 input 这个标签做了一个取值的操作,然后到这边我们就这整体一个过程将会用 JS 去模拟,所以这边我就不细说了。 最终会把这样的一个结果去通过 input.value 属性把值复制到 input 中,也就是我们最终看到的那样一个结果,到目前我就把这个 js 大概做了一件什么样的事情就已经讲的差不多了。接下来我们去看一下 Selenium 这边。 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# -*- coding: utf-8 -*-
# @Time : 2020-04-01 20:56
# @Author : aiyuehcuang
# @File : demo.py
# @Software: PyCharm
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
import time

def get_text(id,attr):
###  拼接字符串注意{}要写出{{}}
script=("""
let bt=BrowserType();
let id='{id}';
let attr='{attr}';
let supporter =bt.supporter;
const run=function(){{
let all_str = $(id).getAttribute(attr)
let end_index=supporter.length+58
Base64._keyStr = all_str.substring(0, end_index)
let charset = all_str.substring(64, all_str.length)
let encoded = Base64.decode(charset,supporter);
return encoded
}}
return run()
""").format(id=id,attr=attr)
return script

chrome_option = Options()
chrome_option.add_argument("--headless")
chrome_option.add_argument("--disable-gpu")
chrome_option.add_argument('--ignore-certificate-errors') # SSL保存
browser = webdriver.Chrome(options=chrome_option)
wait = WebDriverWait(browser, 10)
# 启动浏览器,获取网页源代码
mainUrl = "http://127.0.0.1:5002/"
browser.get(mainUrl)
result=browser.execute_script(get_text("base64","data"))
print(result)
time.sleep(10)
browser.quit()

这边关键的一行代码是:通过 execute_script(get_text("base64","data")) 这样的一句话去执行这个函数,这个函数实际上就是返回一段 JS 代码,这边实际上就是去模拟构造 run 所需要的一些参数,然后把最终的结果返回回去。 这里有两点需要注意:

  1. 如果里面存在拼接字符串的时候,注意花括号实际上要写两个
  2. 如果需要在后面需要获取 JS 返回的值,所以我们上面的代码需要加上 return 来返回 run 函数的结果

我们可以运行一下代码,输出结果如下:

1
2
3
4
5
$ python demo.py

DevTools listening on ws://127.0.0.1:59507/devtools/browser/edbe51d8-744d-447d-9304-e9551a2a6421
[0407/184920.601:INFO:CONSOLE(286)] "[object Object]", source: http://127.0.0.1:5002/static/js/base64.js (286)
生活不是等待暴风雨过去,而是要学会在雨中跳舞。

我们可以看到,我们程够获取到了结果。 这个例子因为它用到了检测浏览器的属性,而且它检测完属性之后会把属性值一直往下传,我们可以从上面的代码中看到它有很多地方使用。 所以,如果我们用 PyExecjs 来写的话,就需要修改很多参数,这样就很不方便了。因为我们需要去模拟这些浏览器参数,我这边写的例子比较简单,像那种更加复杂的。像获取更多的浏览器的一个属性的话,用 PyExecjs 再去写的时候,可能没有浏览器这样的一个环境,所以 PyExecjs 没有 Selenium 有优势。 当然,除了 Selenium 以为,还有一个叫做 Pyppeteer 的库,也是比较常见。 为了控制文章篇幅,咱们下次再续咯,记得关注公众号:AI悦创!

技术杂谈

qimingpian接口加密分析

工具:Chrome + NodeJS + Pycharm 点击获取结果 如果能留下小星星就最好啦

抓包

调出开发者工具,直接到xhr(这里点击改变的时候并未发生网址变更、所以这是Ajax)

参数寻找

一共就两个包,但Preview里面没有数据,but几KB的包没有鬼?反正我不相信1.参数寻找

追根揭底

直接把encrypt_data,拉出来全局搜索(ctrl + shift + F),encrypt_data参数一共六个,但就只有这一个最可疑(我就是不告诉你为什么。。。),其实你看看周围的函数你就会发现,TmD一个个返回啥呀,不是错误就是上传失败。封IP的信息就放了。怕了怕了 2.有猫腻 在console里面打印一下Object(u.a)(e.encrypt_data) 初一看,好像是又好像不是(仅有部分信息)

只有标题,为什么没信息呢? 我告诉你为什么,因为数据被加密了,只给你看标题,充钱就给你看。 不慌,不慌。那个xx说过我离成功就一步了

点击 下一步没错,就是它。老板,求解密一下? ok,感谢老板。再次在console里面打印一下Object(u.a)(e.encrypt_data) 当当当~ 2.3有猫腻-揭开神秘面纱 ok,那它是怎么来的呢?

都晓得它是这里解密出来的,还不就进去搞他呗

3.紧随其后 当当当~,扣它,把这个函数扣出来(快到我怀里来~) 3.紧随其后-加密参数 到这里就基本上把主函数弄完了,但是还没有完 a.a.decode(t)这个鬼我们还不晓得,进去找他,扣它 4.缺啥补啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
decode = function (t) {
var e = (t = String(t).replace(f, "")).length;
e % 4 == 0 && (e = (t = t.replace(/==?$/, "")).length),
(e % 4 == 1 || /[^+a-zA-Z0-9/]/.test(t)) && l("Invalid character: the string to be decoded is not correctly encoded.");
for (var n, r, i = 0, o = "", a = -1; ++a < e;)
r = c.indexOf(t.charAt(a)),
n = i % 4 ? 64 * n + r : r,
i++ % 4 && (o += String.fromCharCode(255 & n >> (-2 * i & 6)));
return o
},

function o(t) {
return JSON.parse(s("5e5062e82f15fe4ca9d24bc5", a.a.decode(t), 0, 0, "012345677890123", 1))
}

这里的参数t,还不晓得,既然是外面传进来的,那么它要么是js生成的,要么就是全局的。全前面找,去console里面测一下,测多次。如果是不变的那么它就是一个全局参数。 拿过来就好,然后在console里面copy(t)。 同理,参数c,和f也是 但是c, 和f 就在decode函数前面,拿了就好 完成!

JavaScript

MiGu登录参数分析

目标:分析咪咕视频登录参数(enpasswordfingerPrintfingerPrintDetail

工具:NodeJs + Chrome 开发者工具

许久没有水文了,闲来无事特来混混脸熟 源码在此,欢迎白嫖,star就更好啦

enpassword

找到登录入口:

查找方式:

点击登录 —> 开启chrome开发者工具 -> 重载框架 —> 抓到登录包 如下:

加密参数寻找

清空之后,使用错误的账号密码登录。一共两个包两张图片。图片开源不看,具体看包,最后在authn包中看到了我们登录所加密过的三个参数,如下

海里捞针-找参数

在搜索框(ctrl + shift + F )下搜索enpassword参数,进入source File 发现link 93,name并未加密;那么就是在它的class 属性 J_RsaPsd中。再次找!

海里捞针-找参数、埋断点

找到三个 J_RsaPsd,每个都上断点,然后在点登录一下 encrypt:加密函数,b.val加密对象(输入的密码) 将其扣出来! 为什么扣这里?因为这里为加密处!由明文转为密文。那我们拿到这些就以为着拿到了加密的函数。就可以自己实现加密

c = new p.RSAKey; c.setPublic(a.result.modulus, a.result.publicExponent); var d = c.encrypt(b.val());

该写如下:(js丫)

1
2
3
4
5
6
function getPwd(pwd) {
c = new p.RSAKey;
c.setPublic(a.result.modulus, a.result.publicExponent);
var d = c.encrypt(b.val());
return d;
}

虽然我们加密的函数已经找到了,but,我们是在自己的环境下并不一定有这个函数(c.encrypt)。所以现在需要去找c.encrypt 新问题:p.RSAKey;没有定义;回到chrome进入p.RSAKey-(选中点击进入f db()) 进入f db()扣出这个方法,然后改写 寻找a.result.modulus, a.result.publicExponent两个参数, 其实是publickey包返回的结果那么至此enpassword加密完成 补两个环境参数

1
2
window = this;
navigator = {};

fingerPrintfingerPrintDetail参数破解

link480 下断点点击下一步,运行 运行一步, 进入RSAfingerPrint函数内,把o.page.RSAfingerPrint方法抠出来 在页面中观察a,b参数 观察发现: 其实a,b,就是我们的a.result.modulus, a.result.publicExponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rsaFingerprint = function () {
a = "00833c4af965ff7a8409f8b5d5a83d87f2f19d7c1eb40dc59a98d2346cbb145046b2c6facc25b5cc363443f0f7ebd9524b7c1e1917bf7d849212339f6c1d3711b115ecb20f0c89fc2182a985ea28cbb4adf6a321ff7e715ba9b8d7261d1c140485df3b705247a70c28c9068caabbedbf9510dada6d13d99e57642b853a73406817";
b = "010001";
var c = $.fingerprint.details
, d = $.fingerprint.result
, e = c.length
, f = ""
, g = new m.RSAKey;
console.log(a, b)
g.setPublic(a, b);
for (var h = g.encrypt(d), i = 0; e > i; i += 117)
f += g.encrypt(c.substr(i, 117));
return {
details: f,
result: h
}
}
rsaFingerprint()

继续寻找;这两个

1
2
c = $.fingerprint.details 
d = $.fingerprint.result

浏览器里面测一下,把他从console拿出来

Other

本文预计阅读需3min

你好,我是你老朋友Payne,大家都或许过我之前写的水文-JS解密入门,没看过的童鞋开源回头看看啊。里面主要讲述了Hash MD5的例子,以及加密与解密,相关的。那么今天我们去搞一下MD5的“父亲”, Hash。主要阐述了什么是哈希,哈希运用方向以及hash碰撞及解决方向,请查阅

Hash算法

哈希

(hash)也叫散列。Hash算法,是将一个不定长的输入,通过散列函数变换成一个定长的输出,即散列值。

应用

Hash主要应用在数据结构以及密码学领域。

数据结构:

使用Hash算法的数据结构叫做哈希表,也叫散列表,主要是为了提高查询的效率。它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数就是hash function,存放记录的数组叫做哈希表。 在数据结构中应用时,有时需要较高的运算速度而弱化考虑抗碰撞性,可以使用自己构建的哈希函数。

密码学:

这种hash散列变换是一种单向运算,具有不可逆性即不能根据散列值还原出输入信息,因此严格意义上讲Hash算法是一种消息摘要算法,不是一种加密算法。常见的hash算法有:SM3、MD5、SHA-1等 。 在不同的应用场景下,hash函数的选择也会有所侧重。比如在管理数据结构时,主要要考虑运算的快速性,并且要保证hash均匀分布;而应用在密码学中就要优先考虑 抗碰撞性 ,避免出现两段不同明文hash值相同的情况发生。

哈希碰撞:

不同的值经过Hash function得到同一值则产生哈希碰撞防止哈希碰撞的最有效方法,就是扩大哈希值的取值空间

1
2
3
16个二进制位的哈希值,产生碰撞的可能性是 65536 分之一。如果有65537个用户,就一定会产生碰撞。哈希值的长度扩大到32个二进制位,碰撞的可能性就会下降到 4,294,967,296 分之一。

更长的哈希值意味着更大的存储空间、更多的计算,将影响性能和成本。开发者s必须做出抉择,在安全与成本之间找到平衡。

hash碰撞解决办法:

1. 开放地址法

  1. 线性探测再散列
  2. 二次探测再散列
  3. 伪随机探测再散列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入 。

    开放寻址法:Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:

    1). di=1,2,3,…,m-1,称线性探测再散列;

    2). di=1^2,(-1)^2,2^2,(-2)^2,(3)^2,…,±(k)^2,(k<=m/2)称二次探测再散列;

    3). di=伪随机数序列,称伪随机探测再散列。

    用开放定址法解决冲突的做法是:当冲突发生时,使用某种探测技术(线性探测法、二次探测法(解决线性探测的堆积问题)、随机探测法(和二次探测原理一致,不一样的是:二次探测以定值跳跃,而随机探测的散列地址跳跃长度是不定值))在散列表中形成一个探测序列。

    沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止插入即可。
  4. 再哈希法

再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数去计算地址,直到无冲突。 虽然不易发生聚集,但是增加了计算时间。

  1. 链地址法(Java hashmap)

链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,将所有关键字为同义词的结点链接在同一个单链表中,

  1. 建立公共溢出区

建立公共溢出区的基本思想是:假设哈希函数的值域是 [1,m-1],则设向量 HashTable[0…m-1] 为基本表, 每个分量存放一个记录,另外设向量 OverTable[0…v] 为溢出表,所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。

Python

你好,我是悦创。 本篇将开启我自己啃代理池的心得,将逐步放送,因为代理池搭建较为复杂,这里我就尽可能把代理池分成几篇来讲,同时也保证,在我其他篇放出来之前,每一篇都是你们的新知识。 学习就像看小说一样,一次一篇就会显得额外的轻松! 当你把学习当作某个娱乐的事情来做,你会发现不一样的世界! 我们无法延长生命长度,但是我们延长生命宽度,学习编程就是扩展生命最有力的武器!

1. 看完之后你会得到什么

  • 返回 yield;
  • eval 的使用;
  • 多个代理网站同时抓取;
  • 使用异步测试代理是否可用;
  • Python 的元类编程简单介绍;
  • 正则表达式、PyQuery 提取数据;
  • 模块化编程;

废话不多说,马上步入正题!

2. 你需要的准备

在学习本篇文章时,希望你已经具备如下技能或者知识点:

  1. Python 环境(推荐 Python 3.7+);
  2. Python 爬虫常用库;
  3. Python 基本语法;
  4. 面向对象编程;
  5. yield、eval 的使用;
  6. 模块化编程;

3. 课前预习知识点

对于代理池的搭建呢,虽然我已经尽可能地照顾到绝大多数地小白,把代理地的搭建呢,进行了拆分,不过对于培训机构或者自学不是特别精进的小伙伴来说还是有些难度的,对于这些呢?我这里也给大家准备了知识点的扫盲。以下内容节选自我个人博客文章,这里为了扫盲就提供必要部分,想看完整的文章可以点击此链接:https://www.aiyc.top/archives/605.html

3.1 Python 的元类编程

你好,我是悦创。 好久不见,最近在啃数学、Java、英语没有来更新公众号,那么今天就来更新一下啦! 又到了每日一啃,啃代码的小悦。今天我遇到了 Python 原类,然而我啥也不懂。只能靠百度和谷歌了,主要还是用谷歌来查啦,百度前几条永远是广告准确度也不行(个人观点),也顺便参考了几个博客:廖雪峰网站,添加了一点自己的观点和理解。

3.1.1 type()

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。 比方说我们要定义一个 Hello 的 class,就写一个 hello.py 模块(这里我实际创建的是:the_test_code_project.py 模块):

1
2
3
class Hello(object):
def hello(self, name='world'):
print('Hello, %s.' % name)

当 Python 解释器载入 hello 模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个 Hello 的 class 对象,测试如下:

1
2
3
4
5
if __name__ == '__main__':
h = Hello()
h.hello()
print(type(Hello))
print(type(h))

运行结果如下:

1
2
3
Hello, world.
<class 'type'>
<class '__main__.Hello'>

其中,上面的输出结果:__main__.Hello 等价于 <class 'the_test_code_project.Hello'> 运行的方式不同显示的方式也不同,但含义是一样的。 type() 函数可以查看一个类型或变量的类型,为了让小白更轻松,我也写了个例子:

1
2
3
4
5
6
7
8
9
10
11
12
number = 12
string = 'Hello AIYC!'
float_number = 12.1
list_data = [1, '2', 3.5, 'AIYC'] # 可变
tuples = (1, '2', 3.5, 'AIYC') # 不可变

if __name__ == '__main__':
print(type(number))
print(type(string))
print(type(float_number))
print(type(list_data))
print(type(tuples))

运行结果:

1
2
3
4
5
<class 'int'>
<class 'str'>
<class 'float'>
<class 'list'>
<class 'tuple'>

Hello 是一个 class,它的类型就是 type ,而 h 是一个实例,它的类型就是class Hello。 我们说 class 的定义是运行时动态创建的,而创建 class 的方法就是使用 type() 函数。 type() 函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过 type() 函数创建出Hello 类,而无需通过 class Hello(object)... 的定义:

1
2
3
4
5
6
def fn(self, name='world'): # 先定义函数
print('Hello, %s.' % name)

Hello = type('Hello', (object,), dict(hello=fn)) # 创建 Hello class
# Hello = type('Class_Name', (object,), dict(hello=fn)) # 创建 Hello class
# type(类名, 父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))

我们接下来来调用一下代码,看输出的结果如何:

1
2
3
4
5
if __name__ == '__main__':
h = Hello()
h.hello()
print(type(Hello))
print(type(h))

这里推荐写成:if __name__ == '__main__': 使代码更加的规范。 运行结果:

1
2
3
Hello, world.
<class 'type'>
<class '__main__.Hello'>

要创建一个 class 对象,type() 函数依次传入 3 个参数:

  1. class 的名称;
  2. 继承的父类集合,注意 Python 支持多重继承,如果只有一个父类,别忘了 tuple 的单元素写法;(这个个 tuple 单元素写法起初本人不太理解,然后一查并认真观察了一下上面的代码就想起来 tuple 单元素写法需要加逗号(,),就是你必须这么写:tuple_1 = (1,) 而不能这么写:tuple_2 = (1)tuple_2 = (1) 的写法,Python 会自动认为是一个整数而不是一个元组)
  3. class 的方法名称与函数绑定,这里我们把函数 fn 绑定到方法名 hello 上。

通过 type() 函数创建的类和直接写 class 是完全一样的,因为 Python 解释器遇到 class 定义时,仅仅是扫描一下 class 定义的语法,然后调用 type() 函数创建出 class。(直接 Class 创建也是) 正常情况下,我们都用 class Xxx... 来定义类,但是,type() 函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。

3.1.2 metaclass

除了使用 type() 动态创建类以外,要控制类的 创建行为 ,还可以使用 metaclass。 metaclass,直译为元类,简单的解释就是:

  • 当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。

image-20200602213149088

  • 但是如果我们想创建出类呢?

那就必须根据 metaclass 创建出类,所以:先定义 metaclass ,然后创建类。连接起来就是:先定义 metaclass ,就可以创建类,最后创建实例。 所以,metaclass 允许你创建类或者修改类 。换句话说,你可以把类看成是 metaclass 创建出来的“实例”。

metaclass 是 Python 面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。(然而还是被我遇见了,而且还是看不懂,但经过大佬的指点就只是知道如何使用,但并不了解其中的原理,所以才有了此篇。)

我们先看一个简单的例子,这个 metaclass 可以给我们自定义的 MyList 增加一个 add 方法:

1
2
3
4
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value) # 加上新的方法
return type.__new__(cls, name, bases, attrs) # 返回修改后的定义

定义 ListMetaclass ,按照默认习惯,metaclass 的类名总是以 Metaclass 结尾,以便清楚地表示这是一个metaclass 。

有了 ListMetaclass ,我们在定义类的时候还要指示使用 ListMetaclass 来定制类,传入关键字参数 metaclass

1
2
class MyList(list, metaclass=ListMetaclass):
pass

当我们传入关键字参数 metaclass 时,魔术就生效了,它指示 Python 解释器在创建 MyList 时,要通过ListMetaclass.__new__() 来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。 __new__() 方法接收到的参数依次是:

  1. 当前准备创建的类的对象;
  2. 类的名字;
  3. 类继承的父类集合;
  4. 类的方法集合。

测试一下 MyList 是否可以调用 add() 方法:

1
2
3
4
5
6
L = MyList()
L.add(1)
print(L)

# 输出
[1]

而普通的 list 没有 add() 方法:

1
2
3
4
5
\>>> L2 = list()
>>> L2.add(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'

这时候,我想你应该和我会有同样的问题,动态修改有什么意义? 直接在 MyList 定义中写上 add() 方法不是更简单吗? 正常情况下,确实应该直接写,我觉得通过 metaclass 修改纯属变态。

3.2 Python eval() 函数

对于 Python 的 eval 呢,大部分人是这么定义的:eval() 函数用来执行一个字符串表达式,并返回表达式的值。 这句话,有可能对于新手来说并不是非常好理解,我们还是用例子来顿悟一下吧。 以下是 eval() 方法的语法:

1
eval(expression[, globals[, locals]])

参数

  • expression — 表达式。
  • globals — 变量作用域,全局命名空间,如果被提供,则必须是一个字典对象。
  • locals — 变量作用域,局部命名空间,如果被提供,可以是任何映射对象。

返回值 返回表达式计算结果。 实际操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [3]: x = 7

In [4]: eval( '3 * x' )
Out[4]: 21

In [5]: eval('pow(2,2)')
Out[5]: 4

In [6]: eval('2 + 2')
Out[6]: 4

In [7]: n=81

In [8]: eval("n + 4")
Out[8]: 85

再来个函数的操作:

1
2
3
4
In [1]: str1 = "print('Hello World')"

In [2]: eval(str1)
Hello World

ok,到此零基础小白关怀文章就完成了,我就不继续赘述啦! 看到这里,你们的身体还行吗? 真叫人头大 我正在的干货要开始了! 进入我们的 show time 环节,起飞了、起飞了,做好准备了!

4. 抓取免费代理

这里呢,我就不再带带大家手摸手的教学如何抓取代理了,因为当你能看这篇文章时,相信你已经对爬虫是有所入门了,如果还没入门的小伙伴可以关注本公众号,往期文章也有零基础教学和基础的课程资源,可以公众号后台回复:Python爬虫。即可获取资源,如果失效也别担心加小悦好友即可!(资源多多噢!)

小白不建议报名培训机构,毕竟现在培训结构收智商税比较多,还是需要多多鉴别噢!

4.1 目标的代理网站

  1. 快代理:https://www.kuaidaili.com/
  2. 西刺代理:https://www.xicidaili.com
  3. 66代理:http://www.66ip.cn/
  4. 无忧代理:http://www.data5u.com
  5. 开心代理-高匿:http://www.kxdaili.com/dailiip/
  6. 云代理:http://www.ip3366.net/free/

对于每个网站的代码呢,具体实现也是比较简单的,这里我就不做过多的赘述啦! 接下来我都直接上代码,不过在上代理的爬虫代码前,我们需要来写个元类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ProxyMetaclass(type):
"""
定义 ProxyMetaclass ,按照默认习惯,metaclass 的类名总是以 Metaclass 结尾,以便清楚地表示这是一个metaclass :
元类,在 FreeProxyGetter 类中加入
CrawlFunc_list 和 CrawlFuncCount
两个参数,分别表示爬虫函数,和爬虫函数的数量。
"""
def __new__(cls, name, bases, attrs):
count = 0
attrs['CrawlFunc_List'] = [] # 添加:CrawlFunc_List 列表方法
for k, v in attrs.items():
if 'crawl_' in k:
# 判断这个函数里面是否携带 crawl_ 也就是利用 value in xxx
attrs['CrawlFunc_List'].append(k)
count += 1
attrs['CrawlFuncCount'] = count # 检测添加的函数的总数量
return type.__new__(cls, name, bases, attrs) # 返回所修改的

详细的代码含义,已经写在上面了,具体的这里 就不赘述了,如果泥有任何不理解的可以去点击阅读原文在我的博客网站下留言,和后台回复数字“3”,加小编好友,拉你入交流群。

4.2 代码编写

4.2.1 请求函数编写

单独保存成一个文件:utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
"""
project = 'Code', file_name = 'utils.py', author = 'AI悦创'
time = '2020/6/1 12:34', product_name = PyCharm, 公众号:AI悦创
code is far away from bugs with the god animal protecting
I love animals. They taste delicious.
"""
import requests
import asyncio
import aiohttp
from requests.exceptions import ConnectionError
from fake_useragent import UserAgent,FakeUserAgentError
import random

def get_page(url, options={}):
"""
构造随机请求头,如果不理解的可以阅读此文章:
两行代码设置 Scrapy UserAgent:https://www.aiyc.top/archives/533.html
"""
try:
ua = UserAgent()
except FakeUserAgentError:
pass
# 生成随机的请求头,加 try...except... 使代码更加健壮
base_headers = {
'User-Agent': ua.random,
'Accept-Encoding': 'gzip, deflate, sdch',
'Accept-Language': 'zh-CN,zh;q=0.8'
}
# 如果使用者有传入请求头,则将此请求头和随机生成的合成在一起
headers = dict(base_headers, **options)
# 当前请求的 url
print('Getting', url)
try:
r = requests.get(url, headers=headers)
print('Getting result', url, r.status_code)
if r.status_code == 200:
return r.text
return None
except ConnectionError:
print('Crawling Failed', url)
return None

class Downloader(object):
"""
一个异步下载器,可以对代理源异步抓取,但是容易被 BAN。
self._htmls: 把请求的 html 放入列表当中
升级版的请求类
"""

def __init__(self, urls):
self.urls = urls
self._htmls = []

async def download_single_page(self, url):
"""
下载单页
"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
self._htmls.append(await response.text())

def download(self):
loop = asyncio.get_event_loop()
tasks = [self.download_single_page(url) for url in self.urls]
loop.run_until_complete(asyncio.wait(tasks))

@property
def htmls(self):
self.download()
return self._htmls

上面请求函数编写完成之后,我们就需要在抓取代理的代码文件中,进行导包,代码如下:

1
2
# files:Spider.py
from utils import get_page

4.2.2 代理抓取代码

导入所需要的库:

1
2
3
4
import requests
from utils import get_page
from pyquery import PyQuery as pq
import re

接下来我们需要创建一个类 FreeProxyGetter 并使用元类创建。

1
2
class FreeProxyGetter(object, metaclass=ProxyMetaclass):
pass
1. 快代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def crawl_kuaidaili(self):
"""
快代理
:return:
"""
for page in range(1, 4):
# 国内高匿代理
start_url = 'https://www.kuaidaili.com/free/inha/{}/'.format(page)
html = get_page(start_url)
# print(html)
pattern = re.compile(
'<td data-title="IP">(.*)</td>s*<td data-title="PORT">(w+)</td>'
)
# s * 匹配空格,起到换行作用
# ip_addres = re.findall(pattern, html) # 写法一
ip_addres = pattern.findall(str(html))
for adress, port in ip_addres:
# print(adress, port)
result = f"{adress}:{port}".strip()
yield result
2. 西刺代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def crawl_xicidaili(self):
"""
西刺代理
:return:
"""
for page in range(1, 4):
start_url = 'https://www.xicidaili.com/wt/{}'.format(page)
html = get_page(start_url)
# print(html)
ip_adress = re.compile(
'<td class="country"><img src="//fs.xicidaili.com/images/flag/cn.png" alt="Cn" /></td>s*<td>(.*?)</td>s*<td>(.*?)</td>'
)
# s* 匹配空格,起到换行作用
re_ip_adress = ip_adress.findall(str(html))
for adress, port in re_ip_adress:
result = f"{adress}:{port}".strip()
yield result
3. 66代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def crawl_daili66(self, page_count=4):
"""
66代理
:param page_count:
:return:
"""
start_url = 'http://www.66ip.cn/{}.html'
urls = [start_url.format(page) for page in range(1, page_count + 1)]
for url in urls:
print('Crawling', url)
html = get_page(url)
if html:
doc = pq(html)
trs = doc('.containerbox table tr:gt(0)').items()
for tr in trs:
ip = tr.find('td:nth-child(1)').text()
port = tr.find('td:nth-child(2)').text()
yield ':'.join([ip, port])
4. 无忧代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def crawl_data5u(self):
"""
无忧代理
:return:
"""
start_url = 'http://www.data5u.com'
html = get_page(start_url)
# print(html)
ip_adress = re.compile(
'<ul class="l2">s*<span><li>(.*?)</li></span>s*<span style="width: 100px;"><li class=".*">(.*?)</li></span>'
)
# s * 匹配空格,起到换行作用
re_ip_adress = ip_adress.findall(str(html))
for adress, port in re_ip_adress:
result = f"{adress}:{port}"
yield result.strip()
5. 开心代理-高匿
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def crawl_kxdaili(self):
"""
开心代理-高匿
:return:
"""
for i in range(1, 4):
start_url = 'http://www.kxdaili.com/dailiip/1/{}.html'.format(i)
try:
html = requests.get(start_url)
if html.status_code == 200:
html.encoding = 'utf-8'
# print(html.text)
ip_adress = re.compile('<tr.*?>s*<td>(.*?)</td>s*<td>(.*?)</td>')
# s* 匹配空格,起到换行作用
re_ip_adress = ip_adress.findall(str(html.text))
for adress, port in re_ip_adress:
result = f"{adress}:{port}"
yield result.strip()
return None
except:
pass
6. 云代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def IP3366Crawler(self):
"""
云代理
parse html file to get proxies
:return:
"""
start_url = 'http://www.ip3366.net/free/?stype=1&page={page}'
urls = [start_url.format(page=i) for i in range(1, 8)]
# s * 匹配空格,起到换行作用
ip_address = re.compile('<tr>s*<td>(.*?)</td>s*<td>(.*?)</td>')
for url in urls:
html = get_page(url)
re_ip_address = ip_address.findall(str(html))
for adress, port in re_ip_address:
result = f"{adress}:{port}"
yield result.strip()

至此,代理网站的代码已经全部编写完成,接下来我们需要了解的是然后调用此类,直接调用吗? 显然不是,我们还需要编写一个运行调用的函数,代码如下:

1
2
3
4
5
6
7
8
9
10
def run(self):
# print(self.__CrawlFunc__)
proxies = []
callback = self.CrawlFunc_List
for i in callback:
print('Callback', i)
for proxy in eval("self.{}()".format(i)):
print('Getting', proxy, 'from', i)
proxies.append(proxy)
return proxies

以上代理的代码中,我并没有抓取每个网站全部页数,如果有需要可以自行调节。 这里我们可以直接写一个调用代码,来测试代码是否正常,写在:Spider.py 也就是代理的代码中,代码如下:

1
2
3
if __name__ == '__main__':
Tester = FreeProxyGetter()
Tester.run()

运行结果如下: 结果较多,省略大部分结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Getting 14.115.70.177:4216 from crawl_xicidaili
Getting 116.22.48.220:4216 from crawl_xicidaili
Getting 223.241.3.120:4216 from crawl_xicidaili
......
Getting 60.188.1.27:8232 from crawl_data5u
Callback crawl_kxdaili
Getting 117.71.165.208:3000 from crawl_kxdaili
Getting 223.99.197.253:63000 from crawl_kxdaili
Getting 60.255.186.169:8888 from crawl_kxdaili
Getting 180.168.13.26:8000 from crawl_kxdaili
Getting 125.124.51.226:808 from crawl_kxdaili
Getting 47.99.145.67:808 from crawl_kxdaili
Getting 222.186.55.41:8080 from crawl_kxdaili
Getting 36.6.224.30:3000 from crawl_kxdaili
Getting 110.43.42.200:8081 from crawl_kxdaili
Getting 39.137.69.8:80 from crawl_kxdaili

ok,至此代理成功抓取,接下来就是我们的异步检测了。

4.2.3 异步检测

这里,我们需要编写一个 error.py 来自定义包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ResourceDepletionError(Exception):

def __init__(self):
Exception.__init__(self)

def __str__(self):
return repr('The proxy source is exhausted')

class PoolEmptyError(Exception):

def __init__(self):
Exception.__init__(self)

def __str__(self):
return repr('The proxy pool is empty')

异步检测代码,这里我就不做任何数据存储,有需求的可以自行添加,下一篇将添加一个存入 redis 数据库的,敬请关注!

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

import aiohttp
from db import RedisClient
from Spider import FreeProxyGetter
from error import ResourceDepletionError
from aiohttp import ServerDisconnectedError, ClientResponseError, ClientConnectorError
from socks import ProxyConnectionError

get_proxy_timeout = 9

class ValidityTester(object):
"""
代理的有效性测试
"""
def __init__(self):
self.raw_proxies = None
self.usable_proxies = [] # 可用代理
self.test_api = 'http://www.baidu.com'

def set_raw_proxies(self, proxies):
self.raw_proxies = proxies # 临时存储一些代理

async def test_single_proxy(self, proxy):
"""
python3.5 之后出现的新特性
text one proxy, if valid, put them to usable_proxies.
"""
try:
async with aiohttp.ClientSession() as session:
try:
if isinstance(proxy, bytes):
proxy = proxy.decode('utf-8')
real_proxy = 'http://' + proxy
print('Testing', proxy)
async with session.get(self.test_api, proxy=real_proxy, timeout=get_proxy_timeout) as response:
if response.status == 200:
print('Valid proxy', proxy)
except (ProxyConnectionError, TimeoutError, ValueError):
print('Invalid proxy', proxy)
except (ServerDisconnectedError, ClientResponseError, ClientConnectorError) as s:
print(s)
pass

def test(self):
"""
aio test all proxies.
"""
print('ValidityTester is working')
try:
loop = asyncio.get_event_loop()
tasks = [self.test_single_proxy(proxy) for proxy in self.raw_proxies]
loop.run_until_complete(asyncio.wait(tasks))
except ValueError:
print('Async Error')

class Schedule(object):
@staticmethod
def valid_proxy():
"""
Get half of proxies which in Spider
"""
tester = ValidityTester()
free_proxy_getter = FreeProxyGetter()
# 获取 Spider 数据库数据
# 调用测试类
tester.set_raw_proxies(free_proxy_getter.run())
# 开始测试
tester.test()

def run(self):
print('Ip processing running')
valid_process = Process(target=Schedule.valid_proxy) # 获取代理并筛选
valid_process.start()
if __name__ == '__main__':
schedule = Schedule().run()

对于上面的部分代理代码,我为了输出结果更加直观,把 print() 输出的,包括 utils.pyprint() 全部注释了, 运行结果(省略部分输出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Testing 182.46.252.84:9999
Testing 183.63.188.250:8808
Testing 39.137.69.10:8080
Testing 119.190.149.226:8060
Testing 219.159.38.201:56210
Testing 58.246.143.32:8118
Testing 114.99.117.13:3000
Testing 114.229.229.248:8118
Testing 119.57.108.53:53281
Testing 223.247.94.164:4216
Testing 219.159.38.208:56210
Testing 59.44.78.30:42335
Testing 112.74.87.172:3128
Testing 182.92.235.109:3128
Testing 58.62.115.3:4216
Testing 183.162.171.37:4216
Testing 223.241.5.46:4216
Testing 223.243.4.231:4216
Testing 223.241.5.228:4216
Testing 183.162.171.56:4216
Testing 223.241.4.146:4216
Testing 223.241.4.200:4216
Testing 223.241.4.182:4216
Testing 118.187.50.114:8080
Testing 223.214.176.170:3000
Testing 125.126.123.208:60004
Testing 163.125.248.39:8088
Testing 119.57.108.89:53281
Testing 222.249.238.138:8080
Testing 125.126.97.77:60004
Testing 222.182.54.210:8118
Valid proxy 58.220.95.90:9401
Valid proxy 60.191.45.92:8080
Valid proxy 222.186.55.41:8080
Valid proxy 60.205.132.71:80
Valid proxy 124.90.49.146:8888

Valid proxy 218.75.109.86:3128
Valid proxy 27.188.64.70:8060

Valid proxy 27.188.62.3:8060
Valid proxy 221.180.170.104:8080
Valid proxy 39.137.69.9:8080

Valid proxy 39.137.69.6:80
Valid proxy 39.137.69.10:8080
Valid proxy 114.229.6.215:8118
Valid proxy 221.180.170.104:8080
Valid proxy 218.89.14.142:8060
Valid proxy 116.196.85.150:3128

Valid proxy 122.224.65.197:3128
Valid proxy 112.253.11.103:8000
Valid proxy 112.253.11.103:8000
Valid proxy 112.253.11.113:8000
Valid proxy 124.90.52.200:8888

Cannot connect to host 113.103.226.99:4216 ssl:default [Connect call failed ('113.103.226.99', 4216)]
Cannot connect to host 36.249.109.59:8221 ssl:default [Connect call failed ('36.249.109.59', 8221)]
Cannot connect to host 125.126.121.66:60004 ssl:default [Connect call failed ('125.126.121.66', 60004)]
Cannot connect to host 27.42.168.46:48919 ssl:default [Connect call failed ('27.42.168.46', 48919)]
Cannot connect to host 163.125.113.220:8118 ssl:default [Connect call failed ('163.125.113.220', 8118)]
Cannot connect to host 114.55.63.34:808 ssl:default [Connect call failed ('114.55.63.34', 808)]
Cannot connect to host 119.133.207.10:4216 ssl:default [Connect call failed ('119.133.207.10', 4216)]
Cannot connect to host 58.62.115.3:4216 ssl:default [Connect call failed ('58.62.115.3', 4216)]
Cannot connect to host 117.71.164.232:3000 ssl:default [Connect call failed ('117.71.164.232', 3000)]

到此,多站抓取异步检测,就已经完全实现,也是实现了模块化编程。我也 顺便把项目结构分享一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
C:.
error.py
info.txt
schedule.py
Spider.py
the_test_code_crawler.py
utils.py

└─__pycache__
db.cpython-37.pyc
error.cpython-37.pyc
Spider.cpython-37.pyc
utils.cpython-37.pyc

本篇我想你主要学习到的目标也就是开头所说的,这里就不赘述了,下一篇我将带大家把抓取到可用的代理进行存储到 redis 数据库中,尽请关注! 源代码文件获取:公众号后台回放:ProxyPool-1 也欢迎加入我的交流群,一起交流!

技术杂谈

之前开发了一个工具包 GerapyPyppeteer,GitHub 地址为 https://github.com/Gerapy/GerapyPyppeteer,这个包实现了 Scrapy 和 Pyppeteer 的对接,利用它我们就可以方便地实现 Scrapy 使用 Pyppeteer 爬取动态渲染的页面了。 另外,很多朋友在运行爬虫的时候可能会使用到 Docker,想把 Scrapy 和 Pyppeteer 打包成 Docker 运行,但是这个打包和测试过程中大家可能会遇到一些问题,在这里对 Pyppeteer 打包 Docker 的坑简单做一下总结。

概述

Pyppeteer 打包 Docker 主要是有这么几个坑点:

  • 依赖没有安装,导致无法正确安装和启动 Pyppeteer。
  • 没有关闭沙盒模式,导致可能出现 Browser closed unexpectedly 错误
  • 没有提前安装好 Pyppeteer,导致每次启动时都要重新安装

下面我们分别对三个问题做下简单的 Troubleshooting。

安装依赖

首先说第一个,安装依赖。 因为 Docker 大部分都是基于 Linux 系统的,比如我常用的基础镜像就是 python:3.7,剩余 Debian 系列,当然还有很多其他的版本,具体可以查看 https://hub.docker.com/_/python 了解下。 但是对于 Pyppeteer 来说,python:3.7 内置的依赖库并不够,我们还需要额外进行安装,安装完毕之后还需要清空下 apt list,一句 Dockerfile 命令如下:

1
2
3
4
5
6
7
RUN apt-get update && 
apt-get -y install libnss3 xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget &&
rm -rf /var/lib/apt/lists/*

关闭沙盒模式

在 Docker 中如果直接启动 Pyppeteer,我们还需要关闭沙盒模式,否则可能会遇到如下错误:

1
2
pyppeteer.errors.BrowserError: Browser closed unexpectedly:
[0924/153706.301300:ERROR:zygote_host_impl_linux.cc(89)] Running as root without --no-sandbox is not supported. See https://crbug.com/638180

这里提示我们要关闭沙盒模式,这里只需要在启动 Pyppeteer 的时候,给 launch 方法的 args 参数多加一个 \--no-sandbox 即可,写法如下:

1
browser = await pyppeteer.launch(options={'args': ['--no-sandbox']})

这样就不会再遇到上面的错误了。

提前安装

另外建议在打包 Docker 的时候就提前把 Pyppeteer 提前安装好,可以单独使用一句 RUN 命令安装即可。

1
RUN pip install -U pip && pip install pyppeteer && pyppeteer-install

这里是提前安装了一下 Pyppteer 这个 Python 库,然后利用 Python 库提供的 pyppeteer-install 命令提前下载了 Chromium 浏览器。 这样后面启动的时候就可以直接唤起 Chromium 浏览器进行爬取了。

总结

最后看下完整 Dockerfile,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM python:3.7

RUN apt-get update &&
apt-get -y install libnss3 xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget &&
rm -rf /var/lib/apt/lists/*

RUN pip install -U pip && pip install pyppeteer && pyppeteer-install

WORKDIR /code
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD python3 run.py

这里首先就是安装了必须的依赖库,然后安装了 Pyppeteer 并下载了 Chromium 浏览器,最后拷贝项目运行即可。 当然最后的一句 CMD 大家可以随意指定入口。 最后大家可以体验一个实例来感受下 Scrapy 和 Pyppeteer 对接后在 Docker 中的运行效果:

1
docker run germey/gerapy-pyppeteer-example

如果大家对 Scrapy 和 Pyppeteer 感兴趣也可以看下我写的这个库 GerapyPyppeteer,GitHub 地址为 https://github.com/Gerapy/GerapyPyppeteer,感谢支持

技术杂谈

解析页面是做爬虫的过程中的重要环节,而且如果站点多了,解析也会变得非常复杂,所以智能化解析就可能是一个不错的解决方案。如果我们能够容忍一定的错误率,那么我们可以利用智能化解析算法帮我们提取一些内容,简单高效。 那有没有办法做到一个网站的全自动化解析呢? 比如来了一个博客网站,我能首先识别出来这是一个列表页还是文章(详情)页,然后提取列表页的每篇文章的链接,然后跳转到每篇文章(详情)页再提取文章相关信息。 那么这里面可能就有四个关键部分:

  • 判断当前所在的页面是列表页还是文章(详情)页
  • 识别出列表页下一页的链接
  • 识别出列表页所有列表链接
  • 识别出文章(详情)页的文章内容和其他信息

如果我们能把这四步都用算法实现出来,那么我们只需要一个网站的主站链接就能轻松地把内容规整地爬取下来了。 那么这篇文章我们就来简单说下第一步,如何判断当前所在的页面的列表页还是文章(详情)页。

注:后文中文章页统一称之为详情页。

示例

列表页和详情页不知道大家有没有基本的概念了,列表页就是导航页,里面带有好多文章或新闻或详情链接,我们选一个链接点进去就是详情页。 比如说这里拿新浪体育来说,首页如图所示: image-20200720193407237 看到这里面有很多链接,就是一些页面导航集合,这个页面就是列表页。 然后我们随便点开一篇新闻报道,如图所示: image-20200720193504042 这里就是一篇新闻报道,带有醒目的标题、发布时间、正文等内容,这就是详情页。 现在我们要做的就是用一个算法来凭借 HTML 代码区分出来哪个是列表页,哪个是详情页。 最后的输入输出如下:

  • 输入:一个页面的 HTML 代码
  • 输出:这个页面是列表页还是详情页,并输出二者的判定概率。

模型选用

首先我们确认下这个问题是个什么问题。 很明显,结果要么是列表页,要么是详情页,就是个二分类问题。 那二分类问题怎么解决呢?实现一个基本的分类模型就好了。大范围就是传统机器学习和现在比较流行的深度学习。总体上来说,深度学习的精度和处理能力会强一点,但是想想我们现在的应用场景,后者要追求精度的话可能需要更多的标注数据,而前者也有比较不错的易用的模型了,比如 SVM。 所以,我们不妨先选用 SVM 模型来实现一个基本的二分类模型来试试看,效果如果已经很好了或者提升空间不大了,那就直接用就好了,如果效果比较差,那我们再选用其他模型来优化。 好,那就定下来了,我们用 SVM 模型来实现一下试试。

数据标注

既然要做分类模型,那么最重要的当然就是数据标注了,我们分两组就好了,一组是列表页,一组是详情页,我们先用手工配合爬虫找一些列表页和详情页的 HTML 代码,然后将其保存下来。 结果类似如下: image-20200720195132784 每个文件夹几百个就行了,数量不用太多,五花八门的页面混起来更好。

特征提取

既然要做 SVM,那么我们得想清楚要分清两个类别需要哪些特征。既然是特征,那我们就要选出二者不同的特征,这样更加有区分度。 比如这里我大体总结了有这么几个特征:

  • 文本密度:正文页通常会包含密集的文字,比如一个 p 节点内部就包含几十上百个文字,如果用单个节点内的文字数目来表示文本密度的话,那么详情页的部分内容文本密度会很高。
  • 超链接节点的数量和比例:一般来说列表页通常会包含多个超链接,而且很大比例都是超链接文本,而详情页却有很多的文字并不是超链接,比如正文。
  • 符号密度:一般来说列表页通常会是一些标题导航,一般可能都不会包含句号,而详情页的正文内容通常就会包含句号等内容,如果按照单位文字所包含的标点符号数量来表示符号密度的话,后者的符号密度也会高一些。
  • 列表簇的数目:一般来说,列表页通常会包含多个具有公共父节点的条目,多个条目构成一个列表簇,虽然说详情页侧栏也会包含一些列表,但至少这个数量也可以成为一个特征来判别。
  • meta 信息:有一些特殊的 meta 信息是列表页独有的,比如只有详情页才会有发布时间,而列表页通常是没有的。
  • 正文标题和 title 标题相似度:一般来说,详情页的正文标题和 title 标题很可能是相同的内容,而列表页通常则是站点的名称。

以上便是我简单列的几个特征,还有很多其他的特征也可以来挖掘,比如视觉特征等等。

模型训练

真正代码实现的过程就是将现有的 HTML 文本进行预处理,把上面的一些特征提取出来,然后直接声明一个 SVM 分类模型即可。 这里声明了一个 feature 名字和对应的处理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
self.feature_funcs = {
'number_of_a_char': number_of_a_char,
'number_of_a_char_log10': self._number_of_a_char_log10,
'number_of_char': number_of_char,
'number_of_char_log10': self._number_of_char_log10,
'rate_of_a_char': self._rate_of_a_char,
'number_of_p_descendants': number_of_p_descendants,
'number_of_a_descendants': number_of_a_descendants,
'number_of_punctuation': number_of_punctuation,
'density_of_punctuation': density_of_punctuation,
'number_of_clusters': self._number_of_clusters,
'density_of_text': density_of_text,
'max_density_of_text': self._max_density_of_text,
'max_number_of_p_children': self._max_number_of_p_children,
'has_datetime_meta': self._has_datetime_mata,
'similarity_of_title': self._similarity_of_title,
}
self.feature_names = self.feature_funcs.keys()

以上方法就是特征和对应的获取方法,具体根据实际情况实现即可。 然后关键的部分就是对数据的处理和模型的训练了,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
list_file_paths = list(glob(f'{DATASETS_LIST_DIR}/*.html'))
detail_file_paths = list(glob(f'{DATASETS_DETAIL_DIR}/*.html'))

x_data, y_data = [], []

for index, list_file_path in enumerate(list_file_paths):
logger.log('inspect', f'list_file_path {list_file_path}')
element = file2element(list_file_path)
if element is None:
continue
preprocess4list_classifier(element)
x = self.features_to_list(self.features(element))
x_data.append(x)
y_data.append(1)

for index, detail_file_path in enumerate(detail_file_paths):
logger.log('inspect', f'detail_file_path {detail_file_path}')
element = file2element(detail_file_path)
if element is None:
continue
preprocess4list_classifier(element)
x = self.features_to_list(self.features(element))
x_data.append(x)
y_data.append(0)

# preprocess data
ss = StandardScaler()
x_data = ss.fit_transform(x_data)
joblib.dump(ss, self.scaler_path)
x_train, x_test, y_train, y_test = train_test_split(x_data, y_data, test_size=0.2, random_state=5)

# set up grid search
c_range = np.logspace(-5, 20, 5, base=2)
gamma_range = np.logspace(-9, 10, 5, base=2)
param_grid = [
{'kernel': ['rbf'], 'C': c_range, 'gamma': gamma_range},
{'kernel': ['linear'], 'C': c_range},
]
grid = GridSearchCV(SVC(probability=True), param_grid, cv=5, verbose=10, n_jobs=-1)
clf = grid.fit(x_train, y_train)
y_true, y_pred = y_test, clf.predict(x_test)
logger.log('inspect', f'n{classification_report(y_true, y_pred)}')
score = grid.score(x_test, y_test)
logger.log('inspect', f'test accuracy {score}')
# save model
joblib.dump(grid.best_estimator_, self.model_path)

这里首先对数据进行预处理,然后将每个 feature 存 存到 x_data 中,标注结果存到 y_data 中。接着我们使用 StandardScaler 对数据进行标准化处理,然后进行随机切分。最后使用 GridSearch 训练了一个 SVM 模型然后保存了下来。 以上便是基本的模型训练过程,具体的代码可以再完善一下。

使用

以上的流程我已经实现了,并且发布了一个开源 Python 包,名字叫做 Gerapy AutoExtractor,GitHub 地址为 https://github.com/Gerapy/GerapyAutoExtractor。 大家如需使用可以使用 pip 安装:

1
pip3 install gerapy-auto-extractor

这个库针对于以上算法提供了四个方法:

  • is_detail:判断是否是详情页
  • is_list:判断是否是列表页
  • probability_of_detail:是详情页的概率,结果是 0-1
  • probability_of_list:是列表页的概率,结果是 0-1

例如,我们随便可以找几个网址存下来,比如把上文的列表页和详情页的 HTML 代码存下来分别保存为 list.html 和 detail.html。 测试代码如下:

1
2
3
4
5
6
7
8
9
10
from gerapy_auto_extractor import is_detail, is_list, probability_of_detail, probability_of_list
from gerapy_auto_extractor.helpers import content, jsonify

html = content('detail.html')
print(probability_of_detail(html), probability_of_list(html))
print(is_detail(html), is_list(html))

html = content('list.html')
print(probability_of_detail(html), probability_of_list(html))
print(is_detail(html), is_list(html))

这里我们就调用了以上四个方法来实现了页面类型和置信度的判断。 类似的输出结果如下:

1
2
3
4
0.9990605314033392 0.0009394685966607814
True False
0.033477426883441685 0.9665225731165583
False True

这样我们就成功得到了两个页面的类别和置信度了。 以上便是判断列表页和详情页的原理和实现,如需了解更多请关注项目 Gerapy Auto Extractor,GitHub 链接为 https://github.com/Gerapy/GerapyAutoExtractor,多谢支持