0%

技术杂谈

搭建个人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,多谢支持

技术杂谈

Airtest

首先需要安装 Airtest,使用 pip3 即可:

1
pip3 install airtest

初始化 device

如果设备没有被初始化的话会进行初始化,并把初始化的设备作为当前设备。 用法如下:

1
2
3
4
5
6
7
8
9
def init_device(platform="Android", uuid=None, **kwargs):
"""
Initialize device if not yet, and set as current device.

:param platform: Android, IOS or Windows
:param uuid: uuid for target device, e.g. serialno for Android, handle for Windows, uuid for iOS
:param kwargs: Optional platform specific keyword args, e.g. `cap_method=JAVACAP` for Android
:return: device instance
"""

示例如下:

1
2
device = init_device('Android')
print(device)

运行结果如下:

1
<airtest.core.android.android.Android object at 0x1018f3a58>

可以发现它返回的是一个 Android 对象。 这个 Android 对象实际上属于 airtest.core.android 这个包,继承自 airtest.core.device.Device 这个类,与之并列的还有 airtest.core.ios.ios.IOSairtest.core.linux.linux.Linuxairtest.core.win.win.Windows 等。这些都有一些针对 Device 操作的 API,下面我们以 airtest.core.android.android.Android 为例来总结一下。

  • get_default_device:获取默认 device
  • uuid:获取当前 Device 的 UUID
  • list_app:列举所有 App
  • path_app:打印输出某个 App 的完整路径
  • check_app:检查某个 App 是否在当前设备上
  • start_app:启动某个 App
  • start_app_timing:启动某个 App,然后计算时间
  • stop_app:停止某个 App
  • clear_app:清空某个 App 的全部数据
  • install_app:安装某个 App
  • install_multiple_app:安装多个 App
  • uninstall_app:卸载某个 App
  • snapshot:屏幕截图
  • shell:获取 Adb Shell 执行的结果
  • keyevent:执行键盘操作
  • wake:唤醒当前设备
  • home:点击 HOME 键
  • text:向设备输入内容
  • touch:点击屏幕某处的位置
  • double_click:双击屏幕某处的位置
  • swipe:滑动屏幕,由一点到另外一点
  • pinch:手指捏和操作
  • logcat:日志记录操作
  • getprop:获取某个特定属性的值
  • get_ip_address:获取 IP 地址
  • get_top_activity:获取当前 Activity
  • get_top_activity_name_and_pid:获取当前 Activity 的名称和进程号
  • get_top_activity_name:获取当前 Activity 的名称
  • is_keyboard_shown:判断当前键盘是否出现了
  • is_locked:设备是否锁定了
  • unlock:解锁设备
  • display_info:获取当前显示信息,如屏幕宽高等
  • get_display_info:同 display_info
  • get_current_resolution:获取当前设备分辨率
  • get_render_resolution:获取当前渲染分辨率
  • start_recording:开始录制
  • stop_recording:结束录制
  • adjust_all_screen:调整屏幕适配分辨率

了解了上面的方法之后,我们可以用一个实例来感受下它们的用法:

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
from airtest.core.android import Android
from airtest.core.api import *
import logging

logging.getLogger("airtest").setLevel(logging.WARNING)

device: Android = init_device('Android')
is_locked = device.is_locked()
print(f'is_locked: {is_locked}')

if is_locked:
device.unlock()

device.wake()

app_list = device.list_app()
print(f'app list {app_list}')

uuid = device.uuid
print(f'uuid {uuid}')

display_info = device.get_display_info()
print(f'display info {display_info}')

resolution = device.get_render_resolution()
print(f'resolution {resolution}')

ip_address = device.get_ip_address()
print(f'ip address {ip_address}')

top_activity = device.get_top_activity()
print(f'top activity {top_activity}')

is_keyboard_shown = device.is_keyboard_shown()
print(f'is keyboard shown {is_keyboard_shown}')

这里我们调用了设备的一些操作方法,获取了一些基本状态,运行结果如下:

1
2
3
4
5
6
7
8
is_locked: False
app list ['com.kimcy929.screenrecorder', 'com.android.providers.telephony', 'io.appium.settings', 'com.android.providers.calendar', 'com.android.providers.media', 'com.goldze.mvvmhabit', 'com.android.wallpapercropper', 'com.android.documentsui', 'com.android.galaxy4', 'com.android.externalstorage', 'com.android.htmlviewer', 'com.android.quicksearchbox', 'com.android.mms.service', 'com.android.providers.downloads', 'mark.qrcode', ..., 'com.google.android.play.games', 'io.kkzs', 'tv.danmaku.bili', 'com.android.captiveportallogin']
uuid emulator-5554
display info {'id': 0, 'width': 1080, 'height': 1920, 'xdpi': 320.0, 'ydpi': 320.0, 'size': 6.88, 'density': 2.0, 'fps': 60.0, 'secure': True, 'rotation': 0, 'orientation': 0.0, 'physical_width': 1080, 'physical_height': 1920}
resolution (0.0, 0.0, 1080.0, 1920.0)
ip address 10.0.2.15
top activity ('com.microsoft.launcher.dev', 'com.microsoft.launcher.Launcher', '16040')
is keyboard shown False

连接 device

连接 device 需要传入设备的 uri,格式类似 android://adbhost:adbport/serialno?param=value&param2=value2,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
def connect_device(uri):
"""
Initialize device with uri, and set as current device.

:param uri: an URI where to connect to device, e.g. `android://adbhost:adbport/serialno?param=value&param2=value2`
:return: device instance
:Example:
* ``android:///`` # local adb device using default params
* ``android://adbhost:adbport/1234566?cap_method=javacap&touch_method=adb`` # remote device using custom params
* ``windows:///`` # local Windows application
* ``ios:///`` # iOS device
"""

示例如下:

1
2
3
4
5
6
from airtest.core.android import Android
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
device: Android = connect_device(uri)
print(device)

运行结果如下:

1
<airtest.core.android.android.Android object at 0x110246940>

其实返回结果和 init_device 是一样的,最后 connect_device 方法就是调用了 init_device 方法。

获取当前 device

就是直接调用 device 方法,定义如下:

1
2
3
4
5
6
7
def device():
"""
Return the current active device.

:return: current device instance
"""
return G.DEVICE

获取所有 device

在 airtest 中有一个全局变量 G,获取所有 device 的方法如下:

1
2
3
4
5
6
7
from airtest.core.android import Android
from airtest.core.api import *

print(G.DEVICE_LIST)
uri = 'Android://127.0.0.1:5037/emulator-5554'
device: Android = connect_device(uri)
print(G.DEVICE_LIST)

运行结果如下:

1
2
[]
[<airtest.core.android.android.Android object at 0x10ba03978>]

这里需要注意的是,在最开始没有调用 connect_device 方法之前,DEVICE_LIST 是空的,在调用之后 DEVICE_LIST 会自动添加已经连接的 device,DEVICE_LIST 就是已经连接的 device 列表

切换 device

我们可以使用 set_current 方法切换当前连接的 device,传入的是 index,定义如下:

1
2
3
4
5
6
7
8
9
def set_current(idx):
"""
Set current active device.

:param idx: uuid or index of initialized device instance
:raise IndexError: raised when device idx is not found
:return: None
:platforms: Android, iOS, Windows
"""

这个方法没有返回值,调用 set_current 方法切换 device 之后,再调用 device 方法就可以获取当前 device 对象了。

执行命令行

可以使用 shell 方法传入 cmd 来执行命令行,定义如下:

1
2
3
4
5
6
7
8
9
10
@logwrap
def shell(cmd):
"""
Start remote shell in the target device and execute the command

:param cmd: command to be run on device, e.g. "ls /data/local/tmp"
:return: the output of the shell cmd
:platforms: Android
"""
return G.DEVICE.shell(cmd)

直接调用 adb 命令就好了,例如获取内存信息就可以使用如下命令:

1
2
3
4
5
6
7
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)

result = shell('cat /proc/meminfo')
print(result)

运行结果如下:

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
MemTotal:        3627908 kB
MemFree: 2655560 kB
MemAvailable: 2725928 kB
Buffers: 3496 kB
Cached: 147472 kB
SwapCached: 0 kB
Active: 744592 kB
Inactive: 126332 kB
Active(anon): 723292 kB
Inactive(anon): 16344 kB
Active(file): 21300 kB
Inactive(file): 109988 kB
Unevictable: 0 kB
Mlocked: 0 kB
HighTotal: 2760648 kB
HighFree: 2073440 kB
LowTotal: 867260 kB
LowFree: 582120 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 720100 kB
Mapped: 127720 kB
Shmem: 19428 kB
Slab: 76196 kB
SReclaimable: 7392 kB
SUnreclaim: 68804 kB
KernelStack: 7896 kB
PageTables: 8544 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 1813952 kB
Committed_AS: 21521776 kB
VmallocTotal: 122880 kB
VmallocUsed: 38876 kB
VmallocChunk: 15068 kB
DirectMap4k: 16376 kB
DirectMap4M: 892928 kB

启动和停止 App

启动和停止 App 直接传入包名即可,其实它们就是调用的 device 的 start_app 和 stop_app 方法,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@logwrap
def start_app(package, activity=None):
"""
Start the target application on device

:param package: name of the package to be started, e.g. "com.netease.my"
:param activity: the activity to start, default is None which means the main activity
:return: None
:platforms: Android, iOS
"""
G.DEVICE.start_app(package, activity)

@logwrap
def stop_app(package):
"""
Stop the target application on device

:param package: name of the package to stop, see also `start_app`
:return: None
:platforms: Android, iOS
"""
G.DEVICE.stop_app(package)

用法示例如下:

1
2
3
4
5
6
7
8
9
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)

package = 'com.tencent.mm'
start_app(package)
sleep(10)
stop_app(package)

这里我指定了微信的包名,然后调用 start_app 启动了微信,然后等待了 10 秒,然后调用了 stop_app 停止了微信。

安装和卸载

安装和卸载也是一样,也是调用了 device 的 install 和 uninstall 方法,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@logwrap
def install(filepath, **kwargs):
"""
Install application on device

:param filepath: the path to file to be installed on target device
:param kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: None
:platforms: Android
"""
return G.DEVICE.install_app(filepath, **kwargs)

@logwrap
def uninstall(package):
"""
Uninstall application on device

:param package: name of the package, see also `start_app`
:return: None
:platforms: Android
"""
return G.DEVICE.uninstall_app(package)

截图

截图使用 snapshot 即可完成,可以设定存储的文件名称,图片质量等。 定义如下:

1
2
3
4
5
6
7
8
9
10
11
def snapshot(filename=None, msg="", quality=ST.SNAPSHOT_QUALITY):
"""
Take the screenshot of the target device and save it to the file.

:param filename: name of the file where to save the screenshot. If the relative path is provided, the default
location is ``ST.LOG_DIR``
:param msg: short description for screenshot, it will be recorded in the report
:param quality: The image quality, integer in range [1, 99]
:return: absolute path of the screenshot
:platforms: Android, iOS, Windows
"""

示例如下:

1
2
3
4
5
6
7
8
9
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)

package = 'com.tencent.mm'
start_app(package)
sleep(3)
snapshot('weixin.png', quality=30)

运行之后在当前目录会生成一个 weixin.png 的图片,如图所示: weixin

唤醒和首页

唤醒和回到首页分别也是调用了 device 的 wake 和 home 方法,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@logwrap
def wake():
"""
Wake up and unlock the target device

:return: None
:platforms: Android

.. note:: Might not work on some models
"""
G.DEVICE.wake()

@logwrap
def home():
"""
Return to the home screen of the target device.

:return: None
:platforms: Android, iOS
"""
G.DEVICE.home()

直接调用即可。

点击屏幕

点击屏幕是 touch 方法,可以传入一张图或者绝对位置,同时可以指定点击次数,定义如下:

1
2
3
4
5
6
7
8
9
10
11
@logwrap
def touch(v, times=1, **kwargs):
"""
Perform the touch action on the device screen

:param v: target to touch, either a Template instance or absolute coordinates (x, y)
:param times: how many touches to be performed
:param kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: finial position to be clicked
:platforms: Android, Windows, iOS
"""

例如我现在的手机屏幕是这样子: image-20200723104955681 这里我截图下来一张图片,如图所示: tpl 然后我们把这个图片声明成一个 Template 传入,示例如下:

1
2
3
4
5
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
touch(Template('tpl.png'))

启动之后它就会识别出这张图片的位置,然后点击。 或者我们可以指定点击的绝对位置,示例如下:

1
2
3
4
5
6
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
touch((400, 600), times=2)

另外上述的 touch 方法还可以完全等同于 click 方法。 如果要双击的话,还可以使用调用 double_click 方法,传入参数也可以是 Template 或者绝对位置。

滑动

滑动可以使用 swipe 方法,可以传入起始和终止位置,两个位置都可以传入绝对位置或者 Template,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@logwrap
def swipe(v1, v2=None, vector=None, **kwargs):
"""
Perform the swipe action on the device screen.

There are two ways of assigning the parameters
* ``swipe(v1, v2=Template(...))`` # swipe from v1 to v2
* ``swipe(v1, vector=(x, y))`` # swipe starts at v1 and moves along the vector.

:param v1: the start point of swipe,
either a Template instance or absolute coordinates (x, y)
:param v2: the end point of swipe,
either a Template instance or absolute coordinates (x, y)
:param vector: a vector coordinates of swipe action, either absolute coordinates (x, y) or percentage of
screen e.g.(0.5, 0.5)
:param **kwargs: platform specific `kwargs`, please refer to corresponding docs
:raise Exception: general exception when not enough parameters to perform swap action have been provided
:return: Origin position and target position
:platforms: Android, Windows, iOS
"""

比如这里我们可以定义手指向右滑动:

1
2
3
4
5
6
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
swipe((200, 300), (900, 300))

放大缩小

放大缩小是使用的 pinch 方法,可以指定放大还是缩小,同时还可以指定中心位置点和放大缩小的比率。 定义如下:

1
2
3
4
5
6
7
8
9
10
11
@logwrap
def pinch(in_or_out='in', center=None, percent=0.5):
"""
Perform the pinch action on the device screen

:param in_or_out: pinch in or pinch out, enum in ["in", "out"]
:param center: center of pinch action, default as None which is the center of the screen
:param percent: percentage of the screen of pinch action, default is 0.5
:return: None
:platforms: Android
"""

示例如下:

1
2
3
4
5
6
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
pinch(in_or_out='out', center=(300, 300), percent=0.4)

键盘事件

可以使用 keyevent 来输入某个键,例如 home、back 等等,keyevent 的定义如下:

1
2
3
4
5
6
7
8
9
def keyevent(keyname, **kwargs):
"""
Perform key event on the device

:param keyname: platform specific key name
:param **kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: None
:platforms: Android, Windows, iOS
"""

示例如下:

1
keyevent("HOME")

这样就表示按了 HOME 键。

输入内容

输入内容需要使用 text 方法,当然前提是这个 widget 需要是 active 状态,text 的定义如下:

1
2
3
4
5
6
7
8
9
10
@logwrap
def text(text, enter=True, **kwargs):
"""
Input text on the target device. Text input widget must be active first.

:param text: text to input, unicode is supported
:param enter: input `Enter` keyevent after text input, default is True
:return: None
:platforms: Android, Windows, iOS
"""

等待和判断

可以使用 wait 方法等待某个内容加载出来,需要传入的是 Template,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@logwrap
def wait(v, timeout=None, interval=0.5, intervalfunc=None):
"""
Wait to match the Template on the device screen

:param v: target object to wait for, Template instance
:param timeout: time interval to wait for the match, default is None which is ``ST.FIND_TIMEOUT``
:param interval: time interval in seconds to attempt to find a match
:param intervalfunc: called after each unsuccessful attempt to find the corresponding match
:raise TargetNotFoundError: raised if target is not found after the time limit expired
:return: coordinates of the matched target
:platforms: Android, Windows, iOS
"""

同时也使用 exists 方法判断某个内容是否存在,定义如下:

1
2
3
4
5
6
7
8
9
@logwrap
def exists(v):
"""
Check whether given target exists on device screen

:param v: target to be checked
:return: False if target is not found, otherwise returns the coordinates of the target
:platforms: Android, Windows, iOS
"""

断言

另外 Airtest 还提供了几个断言语句来判断结果是否存在或者相同,定义如下:

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
@logwrap
def assert_exists(v, msg=""):
"""
Assert target exists on device screen

:param v: target to be checked
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion fails
:return: coordinates of the target
:platforms: Android, Windows, iOS
"""
try:
pos = loop_find(v, timeout=ST.FIND_TIMEOUT, threshold=ST.THRESHOLD_STRICT)
return pos
except TargetNotFoundError:
raise AssertionError("%s does not exist in screen, message: %s" % (v, msg))

@logwrap
def assert_not_exists(v, msg=""):
"""
Assert target does not exist on device screen

:param v: target to be checked
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion fails
:return: None.
:platforms: Android, Windows, iOS
"""
try:
pos = loop_find(v, timeout=ST.FIND_TIMEOUT_TMP)
raise AssertionError("%s exists unexpectedly at pos: %s, message: %s" % (v, pos, msg))
except TargetNotFoundError:
pass

@logwrap
def assert_equal(first, second, msg=""):
"""
Assert two values are equal

:param first: first value
:param second: second value
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion fails
:return: None
:platforms: Android, Windows, iOS
"""
if first != second:
raise AssertionError("%s and %s are not equal, message: %s" % (first, second, msg))

@logwrap
def assert_not_equal(first, second, msg=""):
"""
Assert two values are not equal

:param first: first value
:param second: second value
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion
:return: None
:platforms: Android, Windows, iOS
"""
if first == second:
raise AssertionError("%s and %s are equal, message: %s" % (first, second, msg))

这几个断言比较常用的就是 assert_exists 和 assert_not_exists 判断某个目标是否存在于屏幕上,同时还可以传入 msg,它可以被记录到 report 里面。 以上就是 Airtest 的 API 的用法,它提供了一些便捷的方法封装,同时还对接了图像识别等技术。 但 Airtest 也有一定的局限性,比如不能根据 DOM 树来选择对应的节点,依靠图像识别也有一定的不精确之处,所以还需要另外一个库 —— Poco。

Poco

利用 Poco 我们可以支持 DOM 选择,例如编写 XPath 等来定位某一个节点。 首先需要安装 Poco,使用 pip3 即可:

1
pip3 install pocoui

安装好了之后我们便可以使用它来选择一些节点了,示例如下:

1
2
3
4
5
6
7
8
9
from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco

poco = AndroidUiautomationPoco()

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
poco(text='Weather').click()

比如这里我们就声明了 AndroidUiautomationPoco 这个 Poco 对象,然后调用了 poco 传入一些选择器,选中之后执行 click 方法。 这个选择器非常强大,可以传入 name 和各个属性值,具体的使用方法见:https://poco.readthedocs.io/en/latest/source/poco.pocofw.html

1
2
3
4
5
6
7
8
9
10
11
from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
from poco.proxy import UIObjectProxy

poco = AndroidUiautomationPoco()

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
object: UIObjectProxy = poco("com.microsoft.launcher.dev:id/workspace")
print(object)

poco 返回的是 UIObjectProxy 对象,它提供了其他的操作 API,例如选取子节点,兄弟节点,父节点等等,同时可以调用各个操作方法,如 click、pinch、scroll 等等。 具体的操作文档可以参见:https://poco.readthedocs.io/en/latest/source/poco.proxy.html 下面简单总结:

  • attr:获取节点属性
  • child:获取子节点
  • children:获取所有子节点
  • click:点击
  • double_click:双击
  • drag_to:将某个节点拖拽到另一个节点
  • exists:某个节点是否存在
  • focus:获得焦点
  • get_bounds:获取边界
  • get_name:获取节点名
  • get_position:获取节点位置
  • get_size:获取节点大小
  • get_text:获取文本内容
  • long_click:长按
  • offspring:选出包含直接子代的后代
  • parent:父节点
  • pinch:缩放
  • scroll:滑动
  • set_text:设置文字
  • sibling:兄弟节点
  • swipe:滑动
  • wait:等待
  • wait_for_appearance:等待某个节点出现
  • wait_for_disappearance:等待某个节点消失

以上的这些方法混用的话就可以执行各种节点的选择和相应的操作。

技术杂谈

最近遇到一个问题,那就是需要给别人共享一下 Kubernetes 的某个资源的使用和访问权限,这个仅仅存在于某个 namespace 下,但是我又不能把管理员权限全都给它,我想只给他授予这一个 Namespace 下的权限,那应该怎么办呢? 比如我这边是需要只想授予 postgresql 这个 Namespace 的权限,这里我就需要利用到 Kubernetes 里面的 RBAC 机制来实现了,下面记录了我的操作流程。

创建 Namespace

1
kubectl create namespace postgresql

首先没有 Namespace 的话需要创建一个 Namespace,这里我创建的是 postgresql,大家可以自行修改。

创建 ServiceAccount

接下来需要创建一个 ServiceAccount,yaml 如下:

1
2
3
4
5
apiVersion: v1
kind: ServiceAccount
metadata:
name: postgresql
namespace: postgresql

创建 Role

然后还要创建一个 Role,来控制相应的权限,yaml 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: postgresql
namespace: postgresql
rules:
- apiGroups: ["", "extensions", "apps"]
resources: ["*"]
verbs: ["*"]
- apiGroups: ["batch"]
resources:
- jobs
- cronjobs
verbs: ["*"]

这里由于 Role 是 Namespace 级别的,所以只能在特定 Namespace 下生效,这里我要让授予本 Namespace 下的所有权限,这里 rules 就添加了所有的 API类型、资源类型和操作类型。

创建 RoleBinding

最后需要将 Role 和 ServiceAccount 绑定起来,yaml 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: postgresql
namespace: postgresql
subjects:
- kind: ServiceAccount
name: postgresql
namespace: postgresql
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: postgresql

创建 kubeconfig 文件

现在我们执行上述 yaml 文件之后,ServiceAccount 其实就已经创建好了,它会对应一个 secret,我们来看下详情,执行:

1
kubectl get serviceaccount postgresql -n postgresql -o yaml

好,运行结果类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{},"name":"postgresql","namespace":"postgresql"}}
creationTimestamp: "2020-07-30T16:10:38Z"
name: postgresql
namespace: postgresql
resourceVersion: "17800240"
selfLink: /api/v1/namespaces/postgresql/serviceaccounts/postgresql
uid: 6327db1f-6a93-4f1e-b988-31842989bbbc
secrets:
- name: postgresql-token-v26k7

这里它实际关联了一个 secret,叫做 postgresql-token-v26k7,这里面就隐藏了 ServiceAccount 的 token 和证书。 好,那么我们就可以利用这个 secret 来制作 kubeconfig 文件了,命令如下:

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
server=https://your-server:443
name=postgresql-token-v26k7
namespace=postgresql

ca=$(kubectl get secret/$name -n $namespace -o jsonpath='{.data.ca.crt}')
token=$(kubectl get secret/$name -n $namespace -o jsonpath='{.data.token}' | base64 --decode)

echo "apiVersion: v1
kind: Config
clusters:
- name: test
cluster:
certificate-authority-data: ${ca}
server: ${server}
contexts:
- name: test
context:
cluster: test
user: postgresql
current-context: test
users:
- name: postgresql
user:
token: ${token}
" > postgresql.kubeconfig

这里我们需要指定三个变量:

  • server:就是 Kubernetes Server API 的地址
  • name:就是 ServiceAccount 对应的 secret
  • namespace:就是当前操作的 Namespace

运行之后就会生成一个 portgresql.kubeconfig 文件。

使用

那么怎么使用呢?很简单,设置下环境变量切换下就好了。

1
export KUBECONFIG=postgresql.kubeconfig

这里我们就将 KUBECONFIG 设置了下,这样再执行 kubectl 就会读取到当前的 kubeconfig 文件,就会生效了。 这时候我们来测试下:

1
kubectl get nodes

运行结果如下:

1
Error from server (Forbidden): nodes is forbidden: User "system:serviceaccount:postgresql:postgresql" cannot list resource "nodes" in API group "" at the cluster scope

可以看到这里就提示没有列出节点的权限了。 然后我们操作下 postgresql 下的权限试试:

1
kubectl get svc -n postgresql

运行结果如下:

1
2
3
4
5
NAME                  TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
postgresql ClusterIP 10.0.193.137 <none> 5432/TCP 9d
postgresql-headless ClusterIP None <none> 5432/TCP 9d
postgresql-metrics ClusterIP 10.0.60.88 <none> 9187/TCP 9d
postgresql-read ClusterIP 10.0.236.184 <none> 5432/TCP 9d

这里就可以看到对 postgresql 这个命名空间的操作就成功了。 到此为止我们就成功实现了特定 Namespace 的限制,大功告成。

技术杂谈

之前也写过不少关于爬虫的博客了,比如我拿一个案例来写了一篇博客,当时写的时候好好的,结果过了一段时间这个页面改版了,甚至直接下线了,那这篇案例就废掉了。 另外如果拿别人的站或者 App 来做案例的话,比较容易触犯到对方的利益,风险比较高,比如把某个站的 JavaScript 逆向方案公布出来,比如把某个 App 的逆向方案公布出来。如果此时此刻没有对方联系你的话,一个很大原因可能是规模太小了别人没注意到,但不代表以后不会。我还是选择爱护自己的羽毛,关于逆向实际网站和 App 的案例我都不会发的。在这种情况下比较理想的方案是自建案例,只用这个案例讲明白对应的知识点就可以了。 所以,为此,这段时间我也在写一些爬虫相关的案例,比如:

  • 无反爬的服务端渲染网站
  • 带有参数加密的 SPA 网站
  • 各类形式验证码网站
  • 反 WebDriver 网站
  • 字体反爬等网站
  • 模拟登录网站
  • App 案例,如代理检测,SSL Pining 等

今天发布一下。

案例列表

本案例平台自爬数据、自建页面、自接反爬,案例稳定后永不过期,适合教学与练习。

SSR 网站

  • ssr1:猫眼电影数据网站,数据通过服务端渲染,适合基本爬虫练习。
  • ssr2:HTTPS 无效证书网站,适合做跳过验证 HTTPS 案例。
  • ssr3:HTTP Basic Authentication 网站,适合做 HTTP 认证案例,用户名密码均为 admin。
  • ssr4:每个响应增加了 5 秒延迟,适合测试慢速网站爬取或做爬取速度测试,减少本身网速干扰。

SPA 网站

  • spa1:猫眼电影数据网站,数据通过 Ajax 加载,页面动态渲染,适合 Ajax 分析和动态页面渲染爬取。
  • spa2:猫眼电影数据网站,数据通过 Ajax 加载,数据接口参数加密且有时间限制,适合动态页面渲染爬取或 JavaScript 逆向分析。
  • spa3:猫眼电影数据网站,数据通过 Ajax 加载,无页码翻页,适合 Ajax 分析和动态页面渲染抓取。
  • spa4:新闻网站索引,数据通过 Ajax 加载,无页码翻页,适合 Ajax 分析和动态页面渲染抓取以及智能页面提取分析。
  • spa5:豆瓣图书网站,数据通过 Ajax 加载,有翻页,无反爬,适合大批量动态页面渲染抓取。
  • spa6:电影数据网站,数据通过 Ajax 加载,数据接口参数加密且有时间限制,源码经过混淆,适合 JavaScript 逆向分析。

验证码网站

  • captcha1:对接滑动拼图验证码,适合滑动拼图验证码分析处理。
  • captcha2:对接图标点选验证码,适合图标点选验证码分析处理。
  • captcha3:对接图文点选验证码,适合图文点选验证码分析处理。
  • captcha4:对接语序分析验证码,适合语序分析验证码分析处理。
  • captcha5:对接空间推理验证码,适合空间推理验证码分析处理。
  • captcha6:对接九宫格识图验证码,适合九宫格识图验证码分析处理。

模拟登录网站

  • login1:登录时用户名和密码经过加密处理,适合 JavaScript 逆向分析。
  • login2:对接 Session + Cookies 模拟登录,适合用作 Session + Cookies 模拟登录练习。
  • login3:对接 JWT 模拟登录方式,适合用作 JWT 模拟登录练习。

反爬型网站

  • antispider1:WebDriver 反爬网站,检测到 WebDriver 就不显示页面。
  • antispider2:对接 User-Agent 反爬,检测到常见爬虫 User-Agent 就会拒绝响应,适合用作 User-Agent 反爬练习。
  • antispider3:对接文字偏移反爬,所见顺序并不一定和源码顺序一致,适合用作文字偏移反爬练习。
  • antispider4:对接字体文件反爬,显示的内容并不在 HTML 内,而是隐藏在字体文件,设置了文字映射表,适合用作字体反爬练习。
  • antispider5:限制 IP 访问频率为最多 1 秒一个,如果过多则会封禁 IP。

App

  • app1:最基本的 App 案例,数据通过接口加载,无反爬,无任何加密参数,适合做抓包分析和请求模拟。
  • app2:设置了接口请求不走系统代理,因此无法直接抓包,适合做抓包特殊处理。
  • app3:对系统代理进行了检测,如果设置了代理则无法正常请求数据,适合做抓包特殊处理。
  • app4:设置了 SSL Pining,如果设置了非法证书则无法正常请求数据,适合做反 SSL Pining 处理。
  • app5:接口增加了加密参数,适合做抓包实时处理或可视化爬取或逆向分析。
  • app6:接口增加了加密参数,同时对源码进行了混淆,适合做抓包实时处理或可视化爬取或逆向分析。
  • app7:接口增加了加密参数,同时对安装包进行了加固处理,适合做抓包实时处理或可视化爬取或逆向分析。

暂且是这么多,后续还会继续增加,大家可以试着爬爬看。

汇总链接

为了方便,我专门申请了一个域名,scrape.center,意思名为「爬取中心」,似乎听起来意义上还说的过去? 案例平台首页 URL:https://scrape.center,截图如下: 案例首页 大家可以点击任意一个网站来爬取练习。

案例预览

下面是一些部分案例的截图: SSR1案例截图 SPA4案例截图 SPA5案例截图 SPA6案例截图 上面是一些案例的效果,基本上是使用 Django + Vue.js 开发的,主题使用了红色调,整个案例平台风格比较统一。另外还有一些 App 也是类似的风格,大家可以自行下载体验试试。 当然这里面最主要的还是案例的功能,比如各种加密、反爬、检测等等。

源代码

有朋友可能会问这个案例平台的源代码在哪里。 这里解释一下,由于这个案例平台以后会用于案例的讲解,并且可能会出现在课程、书本中,所以为了避免盗版和抄袭的问题,这里我选择了闭源,也算是对自己的知识成果的另一种保护吧。 不过这并不意味着爬取代码是闭源的,这块还是会开源出来的。

怎么爬

还有朋友会问,这一个个网站这么多类型和反爬,到底怎么爬呢? 其实现在针对这个练习平台一些案例讲解我已经做好了,课程也基本 OK 了,如果感兴趣大家可以点击看看。 https://t.lagou.com/cRC3RGRjSu706 谢谢大家。

技术杂谈

Persistenced:

RDB(Redis Database):

简介:

将时间段间隔内的内存数据以快照的形式写入磁盘,它恢复时是将快照文件直接读到内存里(snapshot)

原理:

Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,在用这个临时文件替换上次持久化号的文件。主进程是不进行任何的IO操作,确保了极高的性能。如果需要进行大规模的数据恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能会丢失

Fork:

作用:创建一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)都与原进程一致。但为一个全新的进程,并作为原进程的子进程

RDB保存的是dump.rdb文件

配置位置:

在redis.conf 中的snapshoting

快照:

save,save只管保存,其他不管全部阻塞 bgsave:redis会在后台异步进行快照操作 快照同时还可以响应客户端请求,可以通过lastsave命令获取最后一次成功执行快照的时间 执行flush命令也会产生dump.rdb文件,但里面是空的,无意义

如何恢复:

将备份文件(dump.rdb)移动到redis安装目录,并启动服务即可 在连接完成之后的终端 使用 config get dir 获取目录 异常恢复: redis-check-rdb —fix {}

优势:

适合大规模的数据备份 对数据完整性和一致性要求不高

劣势:

在一定时间间隔内做一次备份,所以如果redis意外down了就会丢失最后一次快照后的所有更改 Fork的时候,内存中的数据被克隆了一份,内存等将会2倍膨胀性,需考虑!!!

如何停止:

​ 动态所有停止rdb保存规则方法:redis-cli config set save “”

AOF:(Append only File):

导入:

为什么还会出现AOF?(新技术的出现必定弥补老技术的不足,新技术一定会借鉴老技术,是老技术的子类) 如果一个系统里面同时存在RDB是冲突呢还是协作? 为什么AOF会在RDB之后产生 AOF它会有什么优缺点?

原理:以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只需追加文件但不可改写文件,Redis启动之初会读取该文件重新构建数据, 换言之,redis重启就根据日志文件的内容将写指令从前到后执行一次以完成数据恢复工作 配置位置: redis.conf中的APPEND ONLY MODE 配置说明: Appendfsunc:

always: 同步持久化,每次发生更改立即记录到磁盘,性能差但数据完整性比较好 everysec:出厂默认推荐,异步操作,每秒记录,如果一秒内宕机,有数据丢失 No

No-appendfsy-on-rewrite:重写时是否可以运用Appendfsync,默认no即可,保障数据安全 Auto-aof-rewrite-min-size :设定重写基准值 Auto-aof-rewrite-percentage : 设定重写基准值

探讨dump.rdb,与aof的二者是否能共存及选择顺序

当aof损坏时,rdb完全,二者可以和平共存 二者先选择aof

AOF启动/修复/恢复:

正常恢复:

启动:将redis.conf中APPEND ONLY MODE 下appendonly no改为yes 将有数据的aof文件复制一份保存到对于目录(config get dir) 恢复: 重启redis然后重新加载即可

异常恢复:

备份被写坏的AOF文件, 修复: Redistribution-check-aof —fix 进行修复 重启redis,重新加载即可

Rewrite:重写

AOF采用文件追加的方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof

重新原理:

AOF文件持续增长而过大时,会fork出一条新进程来讲文件重写(也是先写零食文件最后在rename),遍历新进程的内存数据,每条记录有一条的set语句.重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件.和快照有点类似

触发机制: REDIS会记录上次重写时的AOF大小,默认配置时当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发 优势:

每秒同步:appendfsync always 同步持久化 每次发送数据变更会被立即记录到磁盘,性能较差完整性比较友好 每修改同步: appendfsync everysec 异步操作,每秒记录,如果一秒内宕机,有数据丢失 不同步:appendfsync no 从不同步

劣势: 相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb AOF运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同

Which one?:

RDB持久化方式:能够在指定的时间间隔内对数据进行快照存储 AOF持久化方式:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重新,使得AOF文件的体积不至于过大 只做缓存:

如果希望数据在服务器运行的时候存在,也可以不使用热河持久化方式.

同时开启两种持久化方式:

当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比rdb文件保存的数据集更完整 RDB的数据不实时,同时使用两者时服务器重启也指挥找AOF文件 那干脆直接使用AOF?不建议 因为RDB更适合用于数据库(AOF在不断变化不好备份),快速重启,而且不担保有AOF可能潜在的BUG,留着作为一个万一的手段

Other

浅谈排序算法与优化(仅部分,Updating)

欢迎查阅与star的源码 写在最前面,此文章少了各排序算法的对比,但多了一份由浅入深的个人理解,以及代码、及算法的优化的思路 阅读文章约 需 5min

列表排序

排序

将一组“无序”的记录序列调整为“有序”的记录序列

列表排序

将无序的列表变为有序列表 输入:列表; 输出:有序列表

升序与降序 内置排序函数:sort(),基于timsort排序算法

Timsort是一种混合稳定排序算法,源自归并排序(merge sort)和插入排序(insertion sort)

有兴趣的伙计可以看看这两篇文章 sort算法运用原理1 sort算法运用原理2 TimSort

常见排序算法

LOW:

冒泡:

列表每两个相邻的数,如果前面比后面大,则交换这两个数 一趟排序完成后,则无序区减少一个数,有序区增加一个数

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
@cal_time
def bubble_sort(li: List[List]) -> List:
print('初始列表', li)
print('初始列表长度:', len(li))
for i in range(len(li) - 1): # 第i躺,总次数
i += 1
for j in range(len(li) - i - 1): # 指针
if li[j] > li[j + 1]: # 如果指针所对应的值大于对比的值,则二者交换位置
li[j], li[j + 1] = li[j + 1], li[j]
else:
break
print(f'冒泡排序第{i}次', li)

li = [np.random.randint(0, 1000) for i in range(5)]
bubble_sort(li)

数据来源于numpy.randmo.randint()随机取数,运行流程:
冒泡排序第1次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第2次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第3次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第4次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第5次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第6次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第7次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第8次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]
冒泡排序第9次 [257, 620, 136, 379, 392, 118, 312, 892, 647, 655]

# 排序次数:列表长度-1,
# 缘由将无序区转化为有序区:(再看一遍以下这段代码, 如果li[j]>li[j+1],则二者交换位置)
if li[j] > li[j + 1]:
li[j], li[j + 1] = li[j + 1], li[j]

# 当一维数组长度为10000的时候所需时间为:

Runtime 优化: 在一次回头看看冒泡排序具体实现思路,将列表每两个相邻的数,如果前面比后面大,则交换这两个数,一趟排序完成后,则无序区减少一个数,有序区增加一个数。 如果无序区已经是有序的呢?按照代码执行流程可知, 如果前者比后者大,那么则两个交换位置。(假设为降序,前面为无序区,后面为有序区) 否则不执行任何交换操作,但会执行便利(也可以理解为此运算”不执行任何交换操作,无实际意义”) 优化目标:(将“不执行任何交换的操作去掉”),咱们在回头看看,具体实际的运算是在第二层for循环里面的,也就说所谓的“无用功”也是在这里产生的 无用功体现为:无论是否进行了位置交换,都会在往有序区在遍历检查一遍 思路如下: 主要优化的地方在跳出循环,在它不交换位置的时候,直接跳出此次的循环 假设它全部都是有序的,并设个标记True, 当如果循环内发生了位置交换,则改变标记为False。 流程控制,if。当if True的时则会执行if内部代码(设立return,或者break)主要跳出循环。避免对有序区进行有一次的排序操作 具体代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
def bubble_sort(li: List):
print("The len of List:", len(li)) # 输入数组长度
for i in range(len(li) - 1): # 保证输入的数组在计算范围类,此处的“-1”是因为索引=长度-1
flag = True
for j in range(len(li) - i - 1):
if li[j] > li[j + 1]:
li[j + 1], li[j] = li[j], li[j + 1]
flag = False
print("Sorting times:%d, Status:%s" % (i + 1, li))
if flag:
break

选择: 插入:

Stronger

快速排序: 堆排序 归并排序

其他排序:

希尔排序 计数排序 基数排序

1
break

选择: 插入:

Stronger

快速排序: 堆排序 归并排序

其他排序:

希尔排序 计数排序 基数排序

技术杂谈

欢迎 Star 的源码

新入手,小白的我,在我眼里 Request 爬虫永远只有四大步,不服来辩?

  1. 确定 URL,构造请求头
  2. 发送请求,获取响应
  3. 解析响应,获取数据
  4. 保存数据

目标:根据视频 BV,获取 B 站视频弹幕

代码地址如下: 抓包确定 URL:

导入:

视频都有一个唯一区分视频:BV 号 那么视频的 URL 规则为:’https://wwww.bilibili.com/video/BV{BVID}’ 找一下弹幕的地址,直接 search,即可!如下

由以上抓包可知,弹幕的 URL:’https://api.bilibili.com/x/v1/dm/list.so?oid=oid‘, 我们获取到 oid 那么这一步就完成了 来,回头去找一下 oid 从何而来呢? 据老夫多年经验指引,他一定在视频 URL 里面。(其实当时也找了挺久的,甚至逆向那一手,断点调试、调用堆栈等等什么都用出来了。最终还是功夫不负有心人,找到了) 其实回头看,oid 是等于 video_URL 页面里面的 cid 参数的(验证了 Payne 式猜想)。过程是难受的

URL,其参数规则也找到了,那么还不就随我为所欲为了。只要拿到视频地址,那不就可以直接拿到弹幕了么。of course!

此处省略 3 万字(请求,解析,网络原理。。。)

其实当时知道两个方法都去试了,JS 那个就不说了,有兴趣的盆友,可以去搞一下 说说这个提取 cid 参数吧,我用的是正则,这种情况最好是用正则,不过也看个人喜好吧。 可以回头看第二张图,初一乍看我好像不会,啊哈哈~

经过优化后(主要是看了其他视频的那啥之后):写出这个神奇的正则

1
cid = re.findall('.*?cid":+(d{9})+', text)[0]

谢谢你认真看到了末尾,那我也写点私活吧。欢迎查阅源码与 star

1
2
其实关于很多网站(普通)的参数就算是有JS加密,以及混淆。并不代表就一定需要去解密,去逆向。有时候真的只需要serch一下惊喜多多,望诸君切记、切记。
咱们解密,逆向,有时候完全即可做一个动态维持即可。例如我源码中的scrape 拉勾

Other

导入:

补充知识:

迭代、递归与循环:迭代与递归都是循环的子集,一个是取值推算,一个是不断的调用自己。 相同点:迭代、递归、循环都是“重复” 相似点:调用逻辑相似 不同点:我简单理解为迭代是根据自身的上一个值推算下一个值,而递归则是由上一个值与“己身”直接运算。循环是自身与外界计算 堆栈关系调用不同 当然也不能说谁好谁坏,只能说三者主针对不同

来,来,来,翠花上栗子:

递归,归去来兮 :

1
2
3
4
5
6
7
8
9
10
11
12
def func(i):
i -= 1
if i == 1:
return 1
else:
print(i)
return func(i)

print(func(8))
# 输出结果:7654321
print(func(4))
# 输出结果:321

迭代,更新换代(单单以数值方面考虑,凸显堆栈方面就。。。)

1
2
3
4
5
6
7
8
9
10
# 迭代
List = ['1','2','3']
i = 0
while True:
printList[i])
i += 1
# 循环、也有点递归的意思。小问题,小问题,这里探究迭代与循环
count = 0
while True
count = count + 1

个人思考及感悟:如何判断生成器与迭代器?:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
区分循环、迭代
循环:与外界计算,重复亦或者

区分迭代器与生成器
迭代器:
可以被next()函数调用并不断返回下一个值的对象称为迭代器
生成器:
不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到出现
StopIteration
可使用isinstance()方法判断是否是Iterator对象

g = (i ** i for i in range(1, 10))
print(next(g))
print(next(g))
print(next(g))
。。。 。。。
for j in g:
print(j)

迭代器是什么?

迭代取值的工具,迭代是个循环,但不是重复的过程。每次的值都基于上次的值而来。迭代是特殊的重复

迭代器能做什么?

迭代取值;程序中用的比较多就是先存后取 用时在释放。释放next()函数,以及遍历

迭代器怎么使用?

​ next(可迭代对象) 或者 可迭代对象. next () 以及在函数中yield关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def foo():
print("starting...")
while True:
res = yield 4
print("res:", res)

g = foo()
print(next(g))
print("*" * 20)
print(next(g))
print(next(g))
print(next(g))

'''
starting...
4
********************
res: None
4
res: None
4
res: None
4
'''

带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数

Python

写在最前面:本人也是个算法小白,如果有什么不对还望大佬指正,谢谢

算法设计评价基本标准

算法是对特定问题求解步骤的一种描述,如果将问题看作函数,那么算法是吧输入转化为输出

算法:

是对特定问题求解步骤的一种描述,是为了解决一个或者一类问题给出的 一个确定的、有限长的操作序列。 算法的设计依赖于数据的存储结构,因此对确定的问题,应该需求子啊适宜的存储结构上设计出一种效率较高的算法

算法的重要特性:

有穷性:

对于任何一组合法的输入值,在执行有穷步骤之后一定能结束,即算法中的操作步骤为有限个,并且每个步骤都能在有限的时间内完成

确定性:

对于每种情况下所应该执行的路径的操作,在算法中都有确切的规定,使算法的执行者或阅读者都能明确其含义及如何执行;并且在确切的条件下只有唯一一条执行流程路径

可行性:

算法中的所有操作都必须足够基本,都可以通过已经实现的基本运算执行有限次实现

有输入:

作为算法加工对象的量值,通常体现为算法中的一组变量。有些输入量需要在算法的执行过程中输入,而有些算法表面上没有输入,但实际上已被嵌入算法之中

有输出:

它是一组与“输入”有确定关系的量值,是算法进行信息加工够得到的结果。这种确定关系即为算法的功能

算法设计目标:

正确性:

算法应满足具体问题的需求,正确反映求解问题对输入、输出加工处理等方面的需求

  1. 程序中不含语法错误,计算的结果却不能满足规格说明要求。
  2. 程序对于特定的几组输入数据能够得出满足要求的结果,而对于其他的输入数据的不出正确的计算结果;
  3. 程序对于精心选择的、经典、苛刻且带有刁难性的几组输入数据能够得到满足需求的结果;(正确的算法)
  4. 程序对于一切合法的输入数据都能得出满足要求的结果

可读性:

算法处理用于编写程序子啊计算机上执行之外,另一个重要用处是阅读和交流。 算法中加入适当的注释,介绍算法的设计思路、各个模块的功能等一些必要性的说明文字来帮助读者理解算法。 要求: 算法中加入适当的注释,介绍算法的设计思路、各个模块的功能等一些必要性的说明文字来帮助读者理解算法 对算法中出现的各种自定义变量和类型能做到“见名知义”,即读者一看到某个变量(或类型名)就能知道其功能

健壮性:

当输入数据非法时,算法能够适当地做出反应或进行处理,输出表示错误性质的信息并终止执行,而不会产生莫名其妙的输出结果。

时间效率与存储占用量:

一般来说,求解同一个问题若有多种算法,则 执行时间短的算法效率高 占用存储空间少的算法较好 算法的执行时间开销和存储空间开销往往相互制约,对高时间效率和低存储占用的要求只能根据问题的性质折中处理

算法复杂度

算法的时间复杂度:

算法的效率指的是算法的执行时间随问题“规模”(通常用整型量 n 表示)的增长而增长的趋势 “规模”在此指的是输入量的数目,比如在排序问题中,问题的规模可以是被排序的元素数目 假如随着问题规模 n 的增长,算法执行时间的增长率和问题规模的增长率相同则可记为: T(n) = O(f(n))

f(n) 为问题规模 n 的某个函数; T(n)被称为算法的(渐进)时间复杂度(Time Complexity) O 表示法不需要给出运行时间的精确值; 选择 f(n),通常选择比较简单的函数形式,并忽略低次项和系数 常用的有 O(1)、O(logn)、O(n)、O(nlogn)、O(n*n)等等 多项式时间复杂度的关系为: O(1) < O(logn) < O(n) < O(N logn) < O(n²) < O(n³) 指数时间算法的关系为: O(2(n 方))< O(n!) <O(n(n 方))

由于估算算法时间复杂度关心的只是算法执行时间的增长率而不是绝对时间,因此可以忽略一些因素。 方法:从算法中选取一种对于所研究的问题来说是“基本操作”的原操作,以该“基本操作”在算法中重复执行的次数作为算法时间复杂度的依据。 EG:

两个 n x n 的矩阵相乘,求其时间复杂度

1
2
for i in range(10):
for j in range(10):

问题规模是矩阵的阶 n,算法的控制结构式三重循环,基本操作是乘法操作 乘法执行次数为 n³,则算法的时间复杂度为 O(n³)

算法的空间复杂度:

算法在执行期间所需要的存储量包括:

  1. 程序代码所占用空间
  2. 输入数据所占用空间
  3. 辅助变量所占用的空间

为了降低复杂度,一个直观的思路是:梳理程序,看其流程中是否有无效的计算或者无效的存储。我们需要从时间复杂度和空间复杂度两个维度来考虑。 常用的降低时间复杂度的方法有递归、二分法、排序算法、动态规划等, 降低空间复杂度的方法,就要围绕数据结构做文章了。 降低空间复杂度的核心思路就是,能用低复杂度的数据结构能解决问题,就千万不要用高复杂度的数据结构。

如何评定一个程序算法的好坏?

简而言之:在符合算法设计标准的前提下,运行的更快、所用计算资源更少。即是更好的算法

请注意这里的比较级别词!!!,对于算法,个人认为就是获得同一结果的同时探究最优解决之道

探究算法优化(自我一点点小体悟-个人能力有限)
  1. 抽象化:将不必要的计算过程去掉

    利用高斯算法解决累计求和问题

  2. 慎选各数据结构,善用第三方算法

    去重: 善用迭代什么的等等 使用字典的特性-不可重复性 布隆过滤器去重(源于哈希算法)

  3. 时空转换:将昂贵的时间转化为廉价的空间

    当‘优无可优时’,取贵舍廉 空间是可以用买的,加个什么内存啊,加个处理器等等什么的 时间是逝去就不在了

    以上仅仅个人的一点点小灵感,望大家不喜勿喷

说了这么多,来道简单的算法题目导入(同一计算机,i5, python) 需求如下:累计求和 1-n 的值(1. 为防止误差,验证 10 次; 2. 验证每次计算次数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 在同一空间复杂下(也就是说没有迭代,探究暴力解法与高斯算法)
# n = 100000
# Method one 代码如下
def func(n):
start = time.time()
count = 0
theSum = 0
for i in range(n + 1):
theSum = theSum + n
count += 1
end = time.time()
return theSum, end - start, count

for i in range(10):
print(f"Sum is %d Required %10.10f seconds count = %d" % func(10000))
# %10.10f 表示取10位小数,运行结果如下

Sum is 100010000 Required 0.0010061264 seconds count = 10001 Sum is 100010000 Required 0.0009891987 seconds count = 10001 Sum is 100010000 Required 0.0000000000 seconds count = 10001 Sum is 100010000 Required 0.0010235310 seconds count = 10001 Sum is 100010000 Required 0.0009710789 seconds count = 10001 Sum is 100010000 Required 0.0000000000 seconds count = 10001 Sum is 100010000 Required 0.0009973049 seconds count = 10001 Sum is 100010000 Required 0.0010013580 seconds count = 10001 Sum is 100010000 Required 0.0000000000 seconds count = 10001 Sum is 100010000 Required 0.0019786358 seconds count = 10001 探究可知:n 扩大 10 倍,运算时间也会扩大 10 倍 高斯算法,从 1 累加至 n,等于(首项+尾项)项数/2 直接引用结论: 1+2+3+…+(n-1) +n={(1+n)+(2+(n-1))…}/2 = (n (n + 1))/2

1
2
3
4
5
6
7
8
9
# Gaussian算法,无迭代算法
def fun1(n):
start = int(time.time())
theSum = (n * (n + 1)) / 2
end = int(time.time())
return theSum, end - start, n / 2

for i in range(1000000):
print(f"Sum is %d Required %10.100f seconds count = %d" % fun1(50**100))

计算结果如下: 循环计算 10000 次,出去 I/O 所需时间几乎可以不计。 Sum is 49999999999999998486656110625518082973725163772751181324120875475173424217777037767098169202353125934013756207986941204091067867184139242319692520523619938935511795533394990905590906653083564427444224 Required 0.0000000000000000000000000000000000000000000000000123123000000000000000000000000000000000000000000000 seconds count = 5000000000000000079514455548799590234180404281972640694890663778873919386085190530406734992928407552 Sum is 49999999999999998486656110625518082973725163772751181324120875475173424217777037767098169202353125934013756207986941204091067867184139242319692520523619938935511795533394990905590906653083564427444224 Required 0.000000000000000000000000000000000000000000000000012332100000000000000000000000000000000000000000000 seconds count = 5000000000000000079514455548799590234180404281972640694890663778873919386085190530406734992928407552 Sum is 49999999999999998486656110625518082973725163772751181324120875475173424217777037767098169202353125934013756207986941204091067867184139242319692520523619938935511795533394990905590906653083564427444224 Required 0.00000000000000000000000000000000000000000000000000000002310000000000000000000000000000000000000 seconds count = 5000000000000000079514455548799590234180404281972640694890663778873919386085190530406734992928407552 Sum is 49999999999999998486656110625518082973725163772751181324120875475173424217777037767098169202353125934013756207986941204091067867184139242319692520523619938935511795533394990905590906653083564427444224 Required 0.000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000 seconds count = 5000000000000000079514455548799590234180404281972640694890663778873919386085190530406734992928407552 Sum is 49999999999999998486656110625518082973725163772751181324120875475173424217777037767098169202353125934013756207986941204091067867184139242319692520523619938935511795533394990905590906653083564427444224 Required 0.000000000000000000000000000000000000000000000000000000010032000000000000000000000000000000000000 seconds count = 5000000000000000079514455548799590234180404281972640694890663778873919386085190530406734992928407552 Sum is 49999999999999998486656110625518082973725163772751181324120875475173424217777037767098169202353125934013756207986941204091067867184139242319692520523619938935511795533394990905590906653083564427444224 Required 0.0000000000000000000000000000000000000000000000012231300000000000000000000000000000000000000000000 seconds count = 5000000000000000079514455548799590234180404281972640694890663778873919386085190530406734992928407552 Sum is 49999999999999998486656110625518082973725163772751181324120875475173424217777037767098169202353125934013756207986941204091067867184139242319692520523619938935511795533394990905590906653083564427444224 Required 0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 seconds count = 5000000000000000079514455548799590234180404281972640694890663778873919386085190530406734992928407552 Sum is 49999999999999998486656110625518082973725163772751181324120875475173424217777037767098169202353125934013756207986941204091067867184139242319692520523619938935511795533394990905590906653083564427444224 Required 0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 seconds count = 5000000000000000079514455548799590234180404281972640694890663778873919386085190530406734992928407552 Sum is 49999999999999998486656110625518082973725163772751181324120875475173424217777037767098169202353125934013756207986941204091067867184139242319692520523619938935511795533394990905590906653083564427444224 Required 0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 seconds count = 5000000000000000079514455548799590234180404281972640694890663778873919386085190530406734992928407552 Sum is 49999999999999998486656110625518082973725163772751181324120875475173424217777037767098169202353125934013756207986941204091067867184139242319692520523619938935511795533394990905590906653083564427444224 Required 0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 seconds count = 5000000000000000079514455548799590234180404281972640694890663778873919386085190530406734992928407552

Python

网络爬虫工程师面试小笔记

————小企业,7K至10k版,面试总结。Payne

面试题之一:Python单例模式

  1. 什么是Python的单例模式?

单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式主要目的是确保某一个类只有一个实例存在。当希望在整个系统中,某个类只能出现一个实例时,单例对象就派上用场了。 面向对象编程单例模式,保证了在程序运行中该类只实例化一次,并提供了一个全局访问点 Python的模块就是天然的单例模式 当模块在第一次导入时,就会生成 .pyc 文件 当第二次导入时就会直接先加载 .pyc 文件,而不会再次执行模块代码。 我们只需把相关函数和数据定义在一个模块中,就可以获得一个单例对象。

  1. 如何实现单例模式?
1.基于类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton(object):
# def __new__(slef):类方法
# pass
# 当我们没写时默认调用object__new__方法

# 然后在执行类的实例化对象:__init__
def __init__(self): # 实例方法
pass

@classmethod
def instance(cls, *args,**kwargs):
if not has attr(Singleton,"_instance"):
Singleton._instance = Singleton(*args,**kwargs)
return Singleton._intance

此时是以完成了一个简单的单例模式案例,But实际开发中随时凉凉 举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Singleton(object):

def __init__(self):
pass

@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance

import threading

def task(arg):
obj = Singleton.instance()
print(obj)

for i in range(10):
t = threading.Thread(target=task,args=[i,])
t.start()

当然此时也并没有什么问题,BUT在’ init ‘方法中加入I/O(input/output)操作就凉凉了 问题出现了,按照以上方式创建的单例无法支持多线程 缘由:Python中实例化对象与初始化对象是分开执行的,又由于多线程之间是通信共享的,故出现线程安全问题。主要体现为,create一个之后kill一个,create一个又被kill一个。所以就。。。 解决思路一:相互独立,分而治之。加锁独立 也就是咱们所了解、知道的线程锁的概念,使得其无序变为相对有序。具体代码便不在此赘述 在看看思路一(相互独立,分而治之。加锁独立) 解决思路二:‘反’实例化,加锁保护独立,确保通用性 在Python3中,调用父类方法是为super(),那么是否可以增加判断: 当类属性不为空时,我们便不在实例化且返回一个已实例化的类属性。这样还是不太完美,带有局限性。进一步加锁保护优化以保障多线程情况下只有一个线程同时访问。这样就保障了单例的安全 基于 new 方法实现!!!

1
2
在回到基于类的第一个代码块,并详细查看其注释。
实例化一个对象是先执行了类的__new__方法(若未写执行object.__new__),实例化对象;然后子啊执行类的__init__方法,对这个对象进行初始化。基于此实现单例。
  1. 基于装饰器

使用装饰器实现,实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def Singleton(cls):
_instance = {}

def _singleton(*args, **kargs):
if cls not in _instance:
_instance[cls] = cls(*args, **kargs)
return _instance[cls]

return _singleton

@Singleton
class A(object):
a = 1

def __init__(self, x=0):
self.x = x

a1 = A(2)
a2 = A(3)
  1. 使用模块的方法

书写代码(并保存在Singleton.py中):

1
2
3
4
class Singleton(object):
def func(self):
pass
singleton = Sinleton()

// from * import singleton 需使用时,直接在其他文件中导入此文件中的对象,那么这个对象即是单例模式对象 还有个基于元类的就没书写了具体请看:https://blog.csdn.net/weixin_44239343/article/details/89376796

面试题之二:Redis有几种数据类型?

如果是单单是Redis那么常用数据类型为五种 他们分别是:String,List,hash,set,zset String:字符串,一个字符串Value最多可以是512M Hash:哈希,是一个String类型的field和Value的映射表 List:列表,时间是链表 Set:集合是一个String类型无序无重复集合,其通过Hash Table实现 Zset(sorted):有序集合 那么应聘时,请注意这个小坑,你Python使用Redis又几种数据类型? 这个是基于语言来回答的,所用语言+Redis数据类型杂糅 Number,String,list,tuple(这个不确定),dict,aggregate 同时又涉语言所拥有的数据类型与redis,一样的就‘合二为一’嘛 以Python为例,稍后继续探究这(6+5)之间的杂糅,dict与aggregate其二者区别为主(其实我也不晓得更深的了)。以及1对1,1对多,多对1。数据结构搞起来,然后哼哼~。

面试题之三:Scrapy框架的运行流程及各模块的作用

如果简历里面写了分布式会拓展scrapy-Redis架构以及其作用。 CAP理论,估计会扯到数据这块。拓展database什么特性啊,之类的。谈优化,谈数据结构。反正数据结构与算法这块,基于此,难于此,也凉于此

面试题之四:scrapy去重所用的几种机制

谨记:先从scrapy本身的去重原理及机制说起来,最基础,优缺点,去重原理等等。一步步来,一上来就BloonFilter,风险不小啊 对于此,自我总结如下:

1、scrapy 基于内存

1
2
3
4
5
scrapy源码中可以找到一个dupefilters.py去重器;

需要将dont_filter设置为False开启去重,默认是True,没有开启去重;

对于每一个url的请求,调度器都会根据请求得相关信息加密得到一个指纹信息,并且将指纹信息和set()集合中的指纹信息进 行 比对,如果set()集合中已经存在这个数据,就不在将这个Request放入队列中;

2、redis 基于内存 更加快捷、速度快、易于管理

1
不说了,前面是叩门砖,Redis就是决胜之地,没啥可讲的,会的基本都会,不会的我也不会,在深挖其原理数据结构,估计得喝上一点,也怕自己一不小心怕给扯飞了

3、布隆过滤器 大 可能存在拥有一定的错误率

1
加分项,是满分还是SSS+。

对于此面试个人总结如下: 源于基础,死于基础(数据结构及类型, 以及算法) 知识点:点串线,线成面。 自己也还有很长一段路走,加油,加油~

技术杂谈

之前我写过几篇文章介绍过有关爬虫的智能解析算法,包括商业化应用 Diffbot、Readability、Newspaper 这些库,另外我有一位朋友之前还专门针对新闻正文的提取算法 GeneralNewsExtractor,这段时间我也参考和研究了一下这些库的算法,同时参考一些论文,也写了一个智能解析库,在这里就做一个非正式的介绍。

引入

那首先说说我想做的是什么。 比如这里有一个网站,网易新闻,https://news.163.com/rank/,这里有个新闻列表,预览图如下image-20200705212920277 任意点开一篇新闻,看到的结果如下: image-20200705213058853 我现在需要做到的是在不编写任何 XPath、Selector 的情况下实现下面信息的提取: 对于列表页来说,我要提取新闻的所有标题列表和对应的链接,它们就是图中的红色区域: image-20200705213401471 这里红色区域分了多个区块,比如这里一共就是 40 个链接,我都需要提取出来,包括标题的名称,标题的 URL。 我们看到页面里面还有很多无用的链接,如上图绿色区域,包括分类、内部导航等,这些需要排除掉。 对于详情页,我主要关心的内容有标题、发布时间、正文内容,它们就是图中红色区域: image-20200705213723198 其中这里也带有一些干扰项,比如绿色区域的侧边栏的内容,无用的分享链接等。 总之,我想实现某种算法,实现如上两大部分的智能化提取。

框架

之前我开发了一个叫做 Gerapy https://github.com/Gerapy/Gerapy 的框架,是一个基于 Scrapy、Scrapyd 的分布式爬虫管理框架,属 1.x 版本。现在正在开发 Gerapy 2.x 版本,其定位转向了 Scrapy 的可视化配置和调试、智能化解析方向,放弃支持 Scraypd,转而支持 Docker、Kubernetes 的部署和监控。 对于智能解析来说,就像刚才说的,我期望的就是上述的功能,在不编写任何 XPath 和 Selector 的情况下实现页面关键内容的提取。 框架现在发布了第一个初步版本,名称叫做 Gerapy Auto Extractor,名字 Gerapy 相关,也会作为 Gerapy 的其中一个模块。 GitHub 链接:https://github.com/Gerapy/GerapyAutoExtractor 现在已经发布了 PyPi,https://pypi.org/project/gerapy-auto-extractor/,可以使用 pip3 来安装,安装方式如下:

1
pip3 install gerapy-auto-extractor

安装完了之后我们就可以导入使用了。

功能

下面简单介绍下它的功能,它能够做到列表页和详情页的解析。 列表页:

  • 标题内容
  • 标题链接

详情页:

  • 标题
  • 正文
  • 发布时间

先暂时实现了如上内容的提取,其他字段的提取暂时还未实现。

使用

要使用 Gerapy Auto Extractor,前提我们必须要先获得 HTML 代码,注意这个 HTML 代码是我们在浏览器里面看到的内容,是整个页面渲染完成之后的代码。在某些情况下如果我们简单用「查看源代码」或 requests 请求获取到的源码并不是真正渲染完成后的 HTML 代码。 要获取完整 HTML 代码可以在浏览器开发者工具,打开 Elements 选项卡,然后复制你所看到的 HTML 内容即可。 先测试下列表页,比如我把 https://news.163.com/rank/ 这个保存为 list.html, image-20200705220428754 然后编写提取代码如下:

1
2
3
4
5
import json
from gerapy_auto_extractor.extractors.list import extract_list

html = open('list.html', encoding='utf-8').read()
print(json.dumps(extract_list(html), indent=2, ensure_ascii=False, default=str))

就是这么简单,核心代码就一行,就是调用了一个 extract_list 方法。 运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[
{
"title": "1家6口5年"结离婚"10次:儿媳""公公岳",
"url": "https://news.163.com/20/0705/05/FGOFE1HJ0001875P.html"
},
{
"title": ""港独"议员泼水阻碍教科书议题林郑月娥深夜斥责",
"url": "https://news.163.com/20/0705/02/FGO66FU90001899O.html"
},
{
"title": "感动中国致敬留德女学生:街头怒怼"港独"有理有",
"url": "https://news.163.com/20/0705/08/FGOPG3AM0001899O.html"
},
{
"title": "香港名医讽刺港警流血少过月经受访时辩称遭盗号",
"url": "https://news.163.com/20/0705/01/FGO42EK90001875O.html"
},
{
"title": "李晨独居北京复式豪宅没想到肌肉男喜欢小花椅子",
"url": "https://home.163.com/20/0705/07/FGOLER1200108GL2.html"
},
{
"title": "不战东京!林丹官宣退役正式结束20年职业生涯",
"url": "https://sports.163.com/20/0704/12/FGML920300058782.html"
},
{
"title": "香港美女搬运工月薪1.6万每月花6千租5平出租",
"url": "https://home.163.com/20/0705/07/FGOLEL1100108GL2.html"
},
{
"title": "杭州第一大P2P"凉了":近百亿未还!被警方立案",
"url": "https://money.163.com/20/0705/07/FGON5T7B00259DLP.html"
},
...
]

可以看到想要的内容就提取出来了,结果是一个列表,包含标题内容和标题链接两个字段,由于内容过长,这里就省略了一部分。 接着我们再测试下正文的提取,随便打开一篇文章,比如 https://news.ifeng.com/c/7xrdz0kysde,保存下 HTML,命名为 detail.html。 image-20200705222433951 编写测试代码如下:

1
2
3
4
import json
from gerapy_auto_extractor.extractors import extract_detail
html = open('detail.html', encoding='utf-8').read()
print(json.dumps(extract_detail(html), indent=2, ensure_ascii=False, default=str))

运行结果如下:

1
2
3
4
5
{
"title": "内蒙古巴彦淖尔发布鼠疫疫情Ⅲ级预警",
"datetime": "2020-07-05 18:54:15",
"content": "2020年7月4日,乌拉特中旗人民医院报告了1例疑似腺鼠疫病例,根据《内蒙古自治区鼠疫疫情预警实施方案》(内鼠防应急发﹝2020﹞7号)和《自治区鼠疫控制应急预案(2020年版)》(内政办发﹝2020﹞17号)的要求,经研究决定,于7月5日发布鼠疫防控Ⅲ级预警信息如下:n一、预警级别及起始时间n预警级别:Ⅲ级。n2020年7月5日起进入预警期,预警时间从本预警通告发布之日持续到2020年底。n二、注意事项n当前我市存在人间鼠疫疫情传播的风险,请广大公众严格按照鼠疫防控“三不三报”的要求,切实做好个人防护,提高自我防护意识和能力。不私自捕猎疫源动物、不剥食疫源动物、不私自携带疫源动物及其产品出疫区;发现病(死)旱獭及其他动物要报告、发现疑似鼠疫病人要报告、发现不明原因的高热病人和急死病人要报告。要谨慎进入鼠疫疫源地,如有鼠疫疫源地的旅居史,出现发热等不适症状时及时赴定点医院就诊。n按照国家、自治区鼠疫控制应急预案的要求,市卫生健康委将根据鼠疫疫情预警的分级,及时发布和调整预警信息。n巴彦淖尔市卫生健康委员会n2020年7月5日n来源:巴彦淖尔市卫生健康委员会"
}

成功输出了标题、正文、发布时间等内容。 这里就演示了基本的列表页、详情页的提取操作。

算法

整个算法的实现比较杂,我看了几篇论文和几个项目的源码,然后经过一些修改实现的。 其中列表页解析的参考论文:

详情页解析的参考论文和项目:

这些都是不完全参考,然后加上自己的一些修改最终才形成了现在的结果。 算法在这里就几句话描述一下思路,暂时先不展开讲了。 列表页解析:

  • 找到具有公共父节点的连续相邻子节点,父节点作为候选节点。
  • 根据节点特征进行聚类融合,将符合条件的父节点融合在一起。
  • 根据节点的特征、文本密度、视觉信息(尚未实现)挑选最优父节点。
  • 从最优父节点内根据标题特征提取标题。

详情页解析:

  • 标题根据 meta、title、h 节点综合提取
  • 时间根据 meta、正则信息综合提取
  • 正文根据文本密度、符号密度、视觉信息(尚未实现)综合提取。

后面等完善了之后再详细介绍算法的具体实现,现在如感兴趣可以去看源码。

说明

本框架仅仅发布了最初测试版本,测试覆盖度比较少,目前仅仅测试了有限的几个网站,尚未大规模测试和添加对比实验,因此准确率现在还没有标准的保证。 参考:关于详情页正文的提取我主要参考了 GeneralNewsExtractor 这个项目,原项目据测试可以达到 90% 以上的准确率。 列表页我测试了腾讯、网易、知乎等都是可以顺利提取的,如: 19841593922229_.pic_hd image-20200705224404571 image-20200705224419759 后面会有大规模测试和修正。 项目初版,肯定存在很多不足,希望大家可以多发 Issue 和提 PR。 另外这里建立了一个 Gerapy 开发交流群,之前在 QQ 群的也欢迎加入,以后交流就在微信群了,大家在使用过程遇到关于 Gerapy、Gerapy Auto Extractor 的问题欢迎交流。 这里放一个临时二维码,后期可能会失效,失效后大家可以到公众号「进击的 Coder」获取加群方式。 image-20200705225922008

待开发功能

  • 视觉信息的融合
  • 文本相似度的融合
  • 分类模型的融合
  • 下一页翻页的信息提取
  • 正文图片、视频的提取
  • 对接 Gerapy

最后感谢大家的支持!

技术杂谈

最近遇到 Mac 的 Git Status 显示中文乱码的问题,类似:

1
"\343\200\220\345\267\245\344\275\234\343\200\221\345\217\221\351\202\256\344\273\266\346\234\215\345\212\241.md"

解决方案:

1
git config --global core.quotepath false

完了之后就显示正常了。

Other

主要亮点为配置和密码找回,安装什么的就。。。

MySQL 基本配置

官网地址:www.mysql.com 安装可参考:https://cuiqingcai.com/5200.html

window:

[gallery columns=”1” size=”full” ids=”9457”] 注意终端 mysqld 开启的不能关闭!

1
2
mysqld //启动服务
mysql //启动客户端

制作服务:

  1. 关闭进程
1
2
3
4
// 查找任务进程
tasklist |findstr mysqld
//终止任务进程
taskkill /F /PID PID
  1. 安装服务与移除服务
1
2
3
4
//安装服务
mysqld --install
//移除服务
mysqld --remove

Linux:

1
2
3
4
centos:
yum -y install mariadb-server mariadb
ubuntu:
yum -y install mysqld-server msyql-client

基本使用: // 查看所有的数据库 show databases; // 进入对应的库: use database(name) 查看表: select * from db;

配置环境变量:

1
vim /etc/profile

在文档最后一行加入:

1
2
3
4
5
PATH = /。。。:$PATH
export PATH
保存退出即可
然后在终端输入
source /etc/profile //生效

管理员密码设置与找回:

管理员账号登录(没有密码,直接回车进入。)

1
mysql -uroot -p

设置管理员密码

1
2
3
mysqladmin -uroot -p {oldPassworld(原始密码,默认为“”,空)} password {“newpassoworld”};
mysqladmin -uroot -p password {“passoworld”};
# passoworld为你锁需要设置的密码哦

MySQL 密码找回:

密码验证思路:mysql 必定将管理员账号密码存储在某个文件夹内,使用时与输入密码验证,成功则能够连接,否则连接失败。 密码找回思路: 跳过 MySQL 密码的验证直接进入

  1. 停止 mysql 服务(注:需要终端的管理员权限运行)

    // windows:

查询进程,并找到 PID:

  1. tasklist | findsrt mysqld
    
    1
    2
    3
    4
    2.  kill 掉 mysql 进程,否则是停止不了服务的,无论如何都需要 kill 掉:

    ![](https://cdn.cuiqingcai.com/wp-content/uploads/2020/06/change_password1.png)

    #此处的为上面查询到的PID,每次都是不一样的,所以就不写具体值了 taskkill /F /PID PID
    1

    net stop mysql
    1
    2
    3
    4

    // Linux/mac:命令不同,基本思路相同 systemctl stop mysql(centos 中默认的 mysql 是 mariadb,可将 mysql 替换成 mariadb 即可)

    若安装了 mysqld(mysql 服务)需停止 MySQL 的进程服务 ,若没有安装 mysql 的服务则此步可省略 重新启动 mysql 服务,且跳过授权表
    Windows: //启动不起来,可能需要net start mysql(这个是特殊情况) mysqld --skip-grant-tables //Linux/mac: mysqld ——safe --skip-grant-tables
    1
    2
    3
    4
    5
    6

    登录,重新修改密码

    > // 登录(此时 MySQL 的 root 权限是没有密码的,直接回车即可进入) mysql -uroot -p // 修改密码(在连接数据库状态中): update mysql.user set password=password("yourpassword") where user="uroot" and host="localhost"
    >
    >
    此语句是告诉数据库,更新密码。密码为yourpassword where 为限制条件 ,限制user为root,host为localhost MySQL的关于用户授权表是存放在mysql库user表中的
    1
    2
    3

    刷新保存设置(这个一定需要,要不之前的功夫都白费了)

    flush privileges;
    1
    2
    3

    退出:

    q # 或者 exit
    1
    2
    3
    4
    5
    6
    7
    8
    9

    重启启动 mysql 服务(终端中): 安装了 MySQL 服务的:net start mysql 若没有安装则在 cmd 中 mysqld 启动一些,用另外的一个 cmd 连接即可

    ## 字符编码:

    查看字符编码

    登录进入 mysql 后

    \\s

进入 mysql 文件夹(这个可不设置,当然设置最好。): 字符编码配置默认文件:my-default.ini 新建后缀名为:’.ini‘的文件

[mysqld] character-set-server=utf8 collation-server=urf8_general_ci [client] default-character=utf8 [mysql] default-character=utf8 user =”root” password =”123456”

技术杂谈

我自己用 Mac 自带的终端很久了,感觉一直还不错。 但美中不足的是终端上面的这个标题实在让人看着太糟心了,看图: image-20200528152435225 上面这行标题,没什么用,又这么难看。 我把偏好设置里面的显示内容都去掉了,设置如下: image-20200528151519064 但是它总是还显示了一个标题,显示成这个样子: image-20200528151017483 上面这个标题看得很难受,我想把它改成无任何内容,简洁清爽,如下图所示: image-20200528150948227 但是现在无论我怎么改偏好设置都不行,总会带上那些信息。 后来搜索了一番发现是 zsh 的问题。 打开 ~/.zshrc 这个文件,找到下面这一行:

1
DISABLE_AUTO_TITLE="true"

把这行取消注释。 另外偏好设置里面把所有的勾选都去掉,如图所示: image-20200528151519064 另外注意这里标题处不能完全为空,需要打上一个空格,否则窗口上方会显示「终端」二字。 最后在 ~/.zshrc 最后还可以加上这一行语句来清屏:

1
clear

这样就不会再显示 Last Login 等相关信息了。 最后看下效果,打开终端就会显示如下样子,简洁清爽,舒服了。 image-20200528151747657 完毕。

Other

SSH 配置:

  • 使用命令在 home 文件夹下新建一个 ssh 文件

配置 SSH 信息,命令:ssh-keygen -t rsa -C e-mail(此时的 C 必须是大写,Email 为绑定了 github 的邮箱),稍后一路回车选择默认值即可

查看 id_rsa.pub 信息,并将其复制到粘贴版中(稍后会用到!)

  • 进入到 github 中点击右上角的头像,选择 setting ——> SSH and GPG keys ——> 将上述复制内容粘贴到编辑面板中——>完成

完成

Other

远程库操作

创建远程库地址别名

git remote add [origin] [Warehouse URI] 创建别名为 origin,github 仓库地址为。。。的 git remote -v 查看相关信息

推送操作:

使用命令:

1
git push 别名(origin) 分支名(master)

注意是将本地库进行推送!!!(此时时事先完成了一些对本地库相关操作)

克隆操作:

git clone [URI]

  1. 首先复制相对应的仓库地址:

这里是克隆下来的结果显示

  • 克隆的作用:

    1. 完整的把远程库克隆下来
    2. 创建 origin 远程地址别名
    3. 初始化本地库

    团队内协作

  • 邀请成员加入仓库工作团队:

    如果没有加入团队是没有权限向所相关的库进行推送的!!! 如何设置(获得)权限呢?(在仓库中点击设置选项进入如下界面)

完成以上操作之后在进行推送即可有权限完成推送。

fetch and merge 操作(二者一起使用相当于 pull)

git remote -v

git fetch [远程库中简称(origin)] [远程库分支名(master)]

git merge [远程库中简称/远程库分支名(origin/master)]

fetch:

  1. 首先是查看了 remote
  2. 然后使用 git fetch 命令下载了远程库内容
  3. 查看本地库中的 project.txt 内容
  4. 切换到新下载的 master 目录(可使用 git reflog 查看分支状态)
  5. 查看远程库中 project.txt 内容

merge:将远程库下载的内容与本地库合并

1
git mergo origin/master

Pull 使用:

pull 冲突处理:

  • 缘由:如果不是基于远程库的最新版本进行修改的
  • 解决:不能推送,必须先拉,拉下来进入冲突状态,进行对内容的修改,删除不相关内容,重新处理即可。更详情请查看前面的 push 冲突解决。

团队外协作:

流程: 具体的流程可以参考上面的流程图:首先是 fork 一个仓库,然后拉去请求 后 clone 到本地,然后编辑更改。后推送到远程仓库(这里的克隆操作就省略了,具体的可以看上面) 发送即可完成。 然后接收者根据流程图步骤 6 开始操作即可

Other

在编辑前首先介绍以下工作流程

本地库与远程库

团队内部协作:

跨团队协作:

添加提交以及查看状态

添加:将工作区的“新建/修改”添加到暂存区

1
git add [file name]

暂存区删除

1
git rm --cached [file name]

查看状态:查看工作区、暂存区状态

1
git status

提交:将暂存区的内容提交到本地库

1
git commit -m "commit message" [file name]

查看历史(日志)

1
2
3
4
5
6
7
8
9
git log:// 最完整的日志形式

// 多屏显示控制方式:空格向下翻页,b向上翻页,q 退出

git log --pretty=oneline:// 简洁的显示

git log --oneline // 更简洁的显示

git reflog :// HEAD@{移动到当前版本需要多少‘步数’}

实际操作

基本思路图: 步骤如下:

  1. 使用 vim 命令新建一个名为 demo1.txt ,并查看状态。提示我们暂存区没有相关的文件,并标红警告

  1. 将新建的文件新增到暂存区:

  1. 将文件添加到本地库:
    • 方式一:
    • 方式二:git commit [file name] 不建议使用

当然直接 commit 也可以,不过。。。

版本前进与后退:(基于索引值对文件进行前进与后退操作)

Vim 多屏显示控制方式:

  • 空格向下翻页
  • b 向上翻页
  • q 退出

查看操作日志:

1
git reflog :// HEAD@{移动到当前版本需要多少‘步数’}

实际操作:查看 reflog

  1. 首先使用 vim 命令新建了一个名为 hard.txt,并在里面写入项目数据‘aaa、bbb’
  2. 使用 git add hard.txt 命令将 hard.txt 添加到暂存区
  3. 使用 git commit hard.txt 命令将 hard.txt 添加到 本地库
  4. 使用命令 git reflog 查看相关的将要操作的参考

给‘项目’版本进行更新:

更新,并将其 commit 到本地库中

基本的已完成了,让我们来试试版本的前进与后退,并查看相对应的内容

git reflog

git reset —hard [索引值] //前进与后退索引命令

注意观察 cat hard.txt 的内容!!!

版本后退:

版本前进

基于符号的版本控制

1
2
3
4
5
// 基于^符号:只能进行版本后退
git reset --hard [file name]^ //回退一个版本
git reset --hard [file name]^^ //回退两个版本
// 基于~符号:
git reset --hard [file name]~n //回退n个版本

reset 命令参数对比:

-- soft

仅仅在本地库中移动 head 指针

  • 本地库下/上移动,相对的暂存区与工作区则被提前/回退了

-- mixed

在本地库移动 Head 指针

  • 不仅会移动本地库还会移动暂存区,不对工作区做修改

重置缓存区

-- hard

在本地库移动 head 指针

重置暂存区,重置工作区;

分支:

同时并行推进多个功能开发,提高效率

各个分支在开发过程中,如果某一个分支,开发失败,不会对其他造成影响

分支操作:

1
2
3
4
5
6
7
8
9
//查看所有的分支
git branch -v
// 创建分支
git branch [file name]
//切换分支
git checkout [file name]
// 合并分支
1.切换到接受修改的分支上(被合并,增加新内容)
2.执行merge命令即可

合并分支注意:首先对其他分支进行编辑更新添加新功能,然后需切换到主分支 master 上,执行 merge 命令,完成合并。便可更新增的功能

// 解决冲突(手动),当不能直接使用 merge 合并时,则需要

何时会出现合并错误?又如何去修改?

在不同分支出现出现相对应的更改时,自动合并不知道以何更新为主,合并将产生冲突;

重新 add,commit 注意 commit 不能带文件名

  1. 编辑文件,删除特殊符号
  2. 把文件修改至满意,保存并退出
  3. git add 【file name】
  4. git commit -m “log message”

    • 此处的 commit 不能带 file name

Other

简介:了解过 git 来源的朋友,应该会晓得 git 与 Linux 系统是同一作者,所以在此操作 Linux 的基础命令几乎都能运行,在此便不在过多赘述: 列举几个在此常用的命令:

  • ls -lA:查看所有的目录(包含隐藏文件夹)
  • ls -l|less 分屏的去查看

安装好了之后如果是默认安装,git 会添加到鼠标右击的快捷栏中(如下所示):

Git 使用前许初始化本地库,具体操作如下:

本地库初始化:(命令行:Git Bash Here)

  • 命令:git add(使用 git bash here)

进入相对应的目录下,新建一个用于学习 Git 的文件夹

1
mkdir xxx

初始化本地库:

1
2
cd xxx  // 进入到相对应的目录下
git init //初始化本地命令

  1. 首先是使用了’ll‘,命令查看当前前目录先文件夹及权限
  2. 使用 mkdir anothergit 命令在当前目录下创建了一个名为 anothergit 的文件夹
  3. 使用 git init ,初始化本地库

初始化成功效果展示:

  1. ll -lA // 显示该目录下全部的文件夹(注:如果使用’ll‘是不能显示出’.git/‘的)
    
    1
    2.
    ll .git/ // 查看.git/文件夹下的可显示的文件夹
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    注:.git 目录中存放的是本地库相关的配置文件,请勿随意删除及修改。(这里凉了就凉凉了,git 的命令也帮不了你)

    ## 设置签名:

    主要用于区分开发者身份,并非会验证验证其邮箱真实性,

    - ​ 形式:
    - 用户名:Payne
    - Email 地址:123123@xxx.com
    - 这里设置的前和登录远程库的账号密码无任何关系
    - 签名:如果都有的话会以项目级别仓库签名为使用,否则会以系统用户级别为使用

    - 项目级别(仓库级别):仅在此库有效
    - 命令:
    //主要用的命令 git config // 实际用的 git config user.name Payne_project git config user.eamil 123123@xxx.com
    1
    2
    3

    - 实际操作:
    - ![](https://cdn.cuiqingcai.com/wp-content/uploads/2020/05/4.项目级别签名设置.png)使用命令,查看项目级别签名设置结果:
    cat ./git/config
    1
    2
    3

    - 系统用户级别:登录当前操作系统的用户范围(包含多个项目级别,仓库级别)
    - 命令:
    //主要用的命令 git config --global // 实际用的 git config --global user.name Payne_global git config --global user.eamil 123123@xxx.com
    • 实际操作系统用户级别的 config 是在系统目录下,如果不想太麻烦的去找直接 cat ~/.git config 即可

Other

这篇文章是我近日学习 git 的笔记,单纯想做个“云备份什么的”,所以的话侧重点就有点偏。

Git 的来源、用处相信大家都或多或少的了解,如果真的想知道的话请自行百度哈~

Git 的安装:

推荐镜像安装:https://npm.taobao.org/mirrors/git-for-windows/ Git 官网地址

https://git-scm.com/

Git 安装相关简介>):

https://git-scm.com/download

各系统的安装(并非唯一方式!)

Window Git 下载地址:

https://git-scm.com/download/win

选择相对应的版本下载即可,下载完成后打开相对应的安装执行,有选项的选择的建议点击第一个默认配置,就不在此过多赘述啦。

MacOS:在命令行中输入以下命令即可

brew install git

以上的网页地址为: https://git-scm.com/download/mac

Linux:(sudo:以管理员权限运行相关的命令,中间的‘-y’:默认同意安装)

在这里以 Ubuntu 为例: Ubuntu:

apt-get install git apt-get -y install git sudo apt-get install git sudo apt-get -y install git

更加具体的可自行查阅 Linux 系统安装 Git 相关:

https://git-scm.com/download/linux

Git 和代码托管中心

代码托管中心的任务:维护远程库

局域网环境下:

  • 可搭建 GitLab 服务器

外网环境下:

  • Github
  • 码云
  • 等等

Python

本节涉及:多线程、多进程、异步的相关概念,希望对你学有所获,学有所成。


基本概念了解:


并发与并行:(偏向于多线/进程方面的原理)

  • 并发: 指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行
  • 并行: 指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的

阻塞与非阻塞:(偏向于协程/异步的原理)


  • 阻塞:阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续处理其他的事情,则称该程序在该操作上是阻塞的。
  • 非阻塞:程序在等待某操作过程中,自身不被阻塞,可以继续处理其他的事情,则称该程序在该操作上是非阻塞的


同步与异步:

  • 同步:不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,我们称这些程序单元是同步执行的。
  • 异步:为完成某个任务,不同程序单元之间过程中无需通信协调,也能完成任务的方式,不相关的程序单元之间可以是异步的。


说了这么多,咱们列举一些他们相关的特点吧:

  • 多线程(英语:multithreading):指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”
  • 多进程(Multiprocessing):每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程.
  • 二者的区别:线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文.多线程主要是为了节约 CPU 时间,发挥利用,根据具体情况而定. 线程的运行中需要使用计算机的内存资源和 CPU。
  • 协程(Coroutine):又称微线程、纤程,协程是一种用户态的轻量级线程。 协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。


    基本的原理都已经了解了 ,那咱们不整一下,咋行?光说不练假把式,走起!!! 本节源码:仓库地址>) 首先先说一下基本的思路:

    • 确定 URL
    • 发起请求,得到响应
    • 解析响应,提取数据、
    • 保存数据

    确定 URL:

    本次请求的 URL(先放地址了!) https://www.guazi.com/cs/buy/o2/#bread

根据以上可知,URL:https://www.guazi.com/{cs}/buy/o{page}/#bread ,更具改变 cs 改变城市,一线城市为前拼音两个字母(例如:长沙/cs 、湘潭/xiangtan),第一页为 o1,第二个为 o2。以此类推 发送请求:

1
2
3
4
5
async def scrape(self, url):
async with self.semaphore:
async with aiohttp.ClientSession(headers=self.header).get(url) as response:
await asyncio.sleep(1)
return await response.text()

注意:再次加入请求头,本网站对 Cookies 有严格的检测。且并不能挂 IP 代理访问 解析响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async def parse(self, html):
with open('car.csv', 'a+', encoding='utf-8') as f:
doc = pq(html)
for message in doc('body > div.list-wrap.js-post > ul > li > a').items():
# 汽车简介
car_name = message('h2.t').text()
# 汽车详情(年限、里程、服务)
car_info = message('div.t-i').text()
year = car_info[:5]
mileage = car_info[6:-5]
service = car_info[13:].replace('|', '')
# 价格
try:
price = message('div.t-price > p').text()
except AttributeError:
price = message('em.line-through').text()
car_pic = message('img').attr('src')
data = f'{car_name}, {year},{mileage}, {service}, {price}n'
logging.info(data)
f.write(data)

我这里是直接一步到位了,解析响应,以及保存数据。 运行之后即可看到类似于这样的东东


如有疑问,咱们评论区见

JavaScript

JS 解密入门——有道翻译

此篇文章省略了很多基础的,例如 json 格式数据的提取啊。试试手,练练感觉。似乎也没啥用。

一 了解加密与解密 :

什么是加密,什么是解密?

  • 加密:数据加密的基本过程,将原为明文的文件或数据经过某种算法进行一次或多次处理。得到的结果常称之为密文的东东。
  • 解密:加密的逆过程,找到加密相同的方式,对其逆向处理,得到原本文件或数据的过程

常用的加密方式:

加密算法分 对称加密非对称加密 其中对称加密算法的加密与解密 密钥相同,非对称加密算法的加密密钥与解密 密钥不同,此外,还有一类 不需要密钥散列算法

本节所涉及的方式:MD5

MD5 用的是 哈希函数,它的典型应用是对一段信息产生 信息摘要,以 防止被篡改。严格来说,MD5 不是一种 加密算法 而是 摘要算法。无论是多长的输入,MD5 都会输出长度为 128bits 的一个串 (通常用 16 进制 表示为 32 个字符)。 更多相关详情请点击此处>)

二 造!点击进入本节源码

这段内容图会比较多,文字叙述会比较少.

确定 URL:

Basic URL : http://fanyi.youdao.com/ 结论缘由,在不刷新全局页面的情况下,在输入框中输入,翻译动态刷新.可知此链接为 Ajax. 经过一系列测试发现,其实际需操作的 URL 为 http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule 在开发者工具中具体观察以下. 基本网站的分析就分析完毕了 注意此处为 POST 请求!!!

观察加密

仔细观察红色方框中,重点观察随着时间改变而改变的参数(图中红色箭头所指之处)

分析加密:

仔细经过上述步骤即可进入本次加密的源码详情页 搜索 sign 参数,得知本页面有 15 个 sign,筛选排查过后可得知以下位置为 sign 等参数,赋值加密过程 为什么会大概确定是此处呢? 理由一:var 声明赋值 理由二:md5() 为什么深信此处呢?

断点一打,debug 一下,啥都出来了.

根据其语法可知,Javascript

  • e 为输入所翻译的内容
  • ts 为七位整数的时间戳
  • salt 为时间戳后加上一位,大于 0 小于 9 的数字
  • bv 为 User-Agent 的值经过md5 加密的 密文
  • sign 为(“fanyideskweb” + e + salt + “Nw(nmmbP%A-r6U3EUn]Aj”)经过 md5 加密的 密文

到这里就基本完成了,那接下来就开始码码吧.

码!!!

看到这里,转而看一下源码。对着上面的注释,仔细看看,相信你一定会有所收获的。

拓展:

可以将此源码打包,并建立用户界面

其实这篇完全就是用来找感觉的,真正的 JS 难度系数成几何倍增长,所以...

加油吧,欧里给~ 如有疑问,那么评论区见

Python

Python 虚拟环境搭建

前言:

什么是虚拟环境?

  • 百度百科>)得知: 以专利的实时动态程序行为修饰与模拟算法,直接利用本机的 OS,模拟出自带与本机相容 OS 的虚拟机(Vista 下可模拟 Vista、XP,Windows 7 下则可模拟 Windows 7、Vista、XP),也称为“虚拟环境”
  • 功能: 每一个环境都相当于一个新的 Python 环境。你可以在这个新的环境里安装库,运行代码等

为什么需要使用虚拟环境?

  • 众所周知 Python 的强大在于其兼容性,其强大的社区等。同时缺也由些许库并不兼容
  • 真实环境与虚拟环境二者相对关联,并非绝对关联,可以在环境里面随便造。

什么时候需要使用虚拟环境?

例如:
  • 需要探究不同版本的 Django 等相互之间的异同
  • 各模块产生冲突时
    • 不知为何,我在 python 环境中后续安装 scrapy,由于库的不兼容会报出安装其中的异步框架(Twisted)的错误
虚拟环境原理:
1
各虚拟环境相当于一个抽屉,在这个抽屉中安装的任何包都不会影响其他抽屉,可以指定项目的虚拟环境来配合使用我们的项目

一、搭建 Python virtualenv

搭建前准备:

  • 请确保 Python 已安装至使用的电脑中(最好已经配置好了环境变量)
  • 请确保 pip 命令能够正常使用,且能正常安装库

如何搭建?

使用 Virtualenv 库

  • 安装 Virtualenv:
    • pip install Virtualenv
      
      1
      2
      3
      4
      - 造起来吧

      - 创建虚拟环境:
      -
      # 后面参数为Virtual environment name 虚拟环境名(可自行定义,我这里以Test为例) Virtualenv Test(Virtual environment name)
      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

      ![](DataBoke虚拟环境搭建微信图片_20200422033005.png)          ![](https://cdn.cuiqingcai.com/wp-content/uploads/2020/04/微信图片_20200422033005.png)

      - 命令行解析:首先创建了一个名为 Virtual environment 的文件夹并且进入(至于为何创建,是因为便于多虚拟环境包管理,这个也是一个 **virtualenv** 的一个缺陷。自己思考后想到较为妥善的解决方法,稍后会阐述明白)
      - 1、 使用 Virtualenv Test 命令创建了一个名为 Test 的虚拟环境包
      - 2、 进入 Test 虚拟文件夹中的 Scripts
      - 3、此时已经进入且使用虚拟环境,后又运行了 deactivate.bat 命令退出了虚拟环境
      - 4、 此时为系统环境(或者说没有使用任何虚拟环境)区分是否为虚拟环境以路径开头是否有“(Virtual environment name)”
      - 删除虚拟环境包
      - 直接删除所对应的文件夹即可
      - 注意点:
      - 需进入 Scripts 目录才可运行
      - 需添加名为. bat 后缀才可运行
      - 阐述一下 Virtualenv 缺陷(不是这个库,而是这个方法!!!),
      - Virtualenv 这个方法是直接在当前目录下创建一个虚拟环境,如果没有单独建立类似于名为 Virtual environment 的文件夹难于管理虚拟环境包,一个两个还好,如果多了的话是十分头疼的。个人建议,如果使用此方法,
      - Virtualenv 这个方法需要进入虚拟环境包中的 Scripts 文件夹才可运行相关的命令,如进入及退出虚拟环境的命令。(当然也可用创建环境变量的方法来解决此缺陷,但如果是单文件还好,那如果是多个虚拟环境包,反倒给自己填麻烦)

      ## 二、 搭建 Python virtualenvwrapper-win

      ### 引言:

      ​ 经过上述的缺陷分析似乎并没有那么方便,就算创建了相关文件夹来放虚拟环境包,但似乎管理起来,却并没有那么简单。(一两个的还好,但是到了三四个,上十个,百个绝对是一件伤脑筋的事情),那么是否有方法能有弥补相关的缺陷呢?答案肯定是有的。Ta 就是 **virtualenvwrapper-win**

      ---

      ## virtualenvwrapper-win:

      - 介绍: Virtaulenvwrapper 是基于 virtualenv 的扩展包
      - 功能: 更方便管理虚拟环境
      - 实现: 它可以将所有虚拟环境整合在一个目录下 ,统一管理(新增,删除,修改,复制,检查),也能够快速在各个虚拟环境间自由切换。

      ###### 提前准备:

      - 请确保 Python 已安装至使用的电脑中(最好已经配置好了环境变量)
      - 请确保 pip 命令能够正常使用,且能正常安装库

      ###### 安装:
      pip install virtualenvwrapper-win

使用:

  • 为了便于使用个人建议,配置系统环境变量,配置如下:​ 找到我的电脑(此电脑),右击属性,点击高级系统设置,后点击环境变量在系统环境变量中添加以下信息,后确认退出 ​

    virtualenvwrapper-win 常用命令如下:

    • 创建虚拟环境: mkvirtualenv (Virtual environment name)
    • 进入虚拟环境:workon (Virtual environment name)
    • 退出当前虚拟环境: deactivate
    • 删除虚拟环境:rmvirtualenv (Virtual environment name)
    • 列出所有虚拟环境列表:workon

      演示如下(此时的 Virtual environment name = Test)箭头代表输入的步骤:
    1. 使用 mkvirtualenv Test 命令创建一个名为 Test 的虚拟环境包(并且创建完成后自动进入此虚拟环境)
    2. 使用 deactivate 命令退出当前虚拟环境
    3. 使用 workon 命令列出虚拟环境表
    4. 使用 workon Test 命令 进入名为 Test 的虚拟环境列表
    5. 使用 rmvirtualenv Test 命令删除了名为 Test 的虚拟环境列表
    6. 再次使用使用 workon Test 命令 进入名为 Test 的虚拟环境列表


      Mac \Linux 同理,就不再这里一一赘述了

三、虚拟环境的使用:

  1. 命令行下,运行虚拟环境 直接 Python (Reptile Engineering.py) 即可

    • 不使用虚拟环境:
    • 使用虚拟环境(因为是一个新的环境,所有的包、库都未安装所以报错<没有 requests 模块错误>:
    • 不使用虚拟不使用虚拟环境:直接运行即可。
  2. Pycharm:使用虚拟环境:以下步骤将虚拟环境 Python 解释器加载到 Pycharm 中来,步骤如下:

写在最后:

首先和大家说句 Sorry 啊,此篇文章中有许多内容来自于百度搜索所得,后根据自己理解改进及编写此篇文章。感谢查看与支持,不喜勿喷。谢谢。 如果有疑问欢迎在评论区留言,我看到后会在第一时间回复,咱们评论区见,加油,欧里给~ 祝学习进步,升官发财,感谢查看与支持,谢谢。 我叫 Payen,某大学在校大二学生,我有 Get 到你么?

声明:

​ 本人 Payen 为本文原著,转载请注明出处,谢谢 ​ ——Payne

技术杂谈

最近我在公司负责的业务已经正式投入上线了,既然是线上环境,那么就需要保证其可用性。 我负责的业务其中就包括一个 Web Service,我需要保证 Service 的每个接口都是可用的,如果某个时间流量大了或者服务器挂掉了,那需要第一时间通知到我。 这时候可能我有这些需求:

  1. 定时测试和监控服务器每个接口是否是可用的,包括返回的数据、状态码是不是正确的。
  2. 我可以随时查看到每个接口的响应时间、可用率等信息,最好是有可视化的图表呈现,一目了然。
  3. 如果接口的错误率超过某一阈值一段时间,及时通知我,包括电话、短信或邮件。
  4. 需要主动去监测接口的可用性,注意这里是主动监测而不是被动监测,不是等用户用的时候报错才提醒我。比如在没有用户用的时候,我也能及时知道每个接口的可用性。

其实,国内的一些服务商已经提供了这些功能,即主动型服务监控,比如「监控宝」,但我并不想用这些服务,一是需要额外花钱,二是数据上并不安全,三是我需要把我的服务集成到公司内部的监控体系下。

有了这些需求,我就结合公司内部现有的一些基础设施先确定一个技术选型,然后实施就好了。 目前公司内部使用的一套监控体系是基于 Kubernetes + Prometheus + Grafana + Alert Manager 的,那么基于我的需求来分析下我怎样利用这一套体系来搭建我想要的监控设施。 解决方案分析:

  1. 首先关于第四个需求比较特殊,现有的监控体系其实已经可以做到服务的被动监测,比如某个 Service 的 API 被调用了,那么相应的调用数据都会被汇总到 Prometheus 上面,Prometheus 里面会计算接口调用的可用率,如果一段时间内如果错误率超过一定阈值,那就报警,追错误的时候去查下 log 就好了。但其实这个不能做到主动监测,比如在凌晨三四点,当没有用户使用的时候,如果这时候服务器出现问题了,我也需要第一时间能知道,所以我需要有一个定时的主动监测程序来实时监测我的所有接口是否是可用的。要做到主动监控,那我一定需要一个接口监测程序定时运行并校验每个接口的结果,这里我选用的就是开源的 JMeter,它大多数情况下是被用来做压测的,但绝对能满足接口调用和检测的需求,只要我定时跑 JMeter 来检测就好了。
  2. 关于第一个需求,我需要监测我的每个接口都是可用的,包括返回的数据也需要是想要的结果。这时候我们可能想到直接跑一些 test case 之类的,但这些其实大多数都是在部署或运行时校验的,如果我要实时跑或者 test case 有 update 了,也不太方便。另外为了写接口测试的时候,如果没有现成的工具,我们可能得写一堆代码,每个接口都写一个,包括 GET 请求的 URL 参数、POST 请求的 Body 信息等等,然后校验接口的返回结果是不是对的,也太麻烦了。所以我们需要找到一个可用的工具来帮助我们快速地完成这些功能。所以,我选择的 JMeter 也提供了可视化界面,我只需要配置一些接口和参数即可,另外它还带有定时器、断言、动态参数、多线程等功能,这样我们也可以做到并发测试、随机等待、动态构造请求参数、返回结果判断等功能了。
  3. 其次再说第二个和第三个需求,其实用现有的 Prometheus + Grafana 就能解决了,这里最关键的则是我的接口监控结果能发给 Prometheus 才行。既然我选用了 JMeter,那么我怎样把 JMeter 的数据发送给 Prometheus 呢?这里需要借助于 JMeter 的一个插件 jmeter-prometheus-plugin,https://github.com/johrstrom/jmeter-prometheus-plugin,利用它就能将 JMeter 变成一个 Data Exporter,Prometheus 来抓取就好了。

所以,综上所述,我利用的一套服务监控体系就是 JMeter + Kubernetes + Prometheus + Grafana + Alert Manager,那么就开干吧。 这里先放一张图,看下最终的监控 Grafana 面板吧: image-20200414152517273 这里一些接口的名称和 URL 我就打码了,这里我可以在 Grafana 中每时每刻都看到每个接口的可用率、响应(包括平均、最快、最慢)时间、状态码等信息,这些信息就是 JMeter 定时检测得到的结果,监控数据转到 Prometheus 里面然后经过 Grafana 可视化出来,并能通过一些指标来实现报警机制。 感兴趣的话,可以继续往下看哈。 为了达成这些功能,我需要解决如下问题:

  • 如何使用 JMeter 来测试每个接口的使用情况。
  • JMeter 如何和 Prometheus 对接起来,即如何集成 jmeter-prometheus-plugin 到 JMeter。
  • JMeter 怎样去部署,部署到哪里。
  • 可视化数据怎样来呈现。
  • 错误状态怎样来快速查看。
  • 出错通知如何实现,比如打电话、发邮件等等。

下面我们就来一个个总结说一下。

由于内容比较多,整个流程我实践下来然后测试通总共花了两天左右的时间,在这里就不完全展开说了,只提关键点了。

JMeter 测试

第一步那就是用 JMeter 来完成接口的测试了,接口的调用形式肯定都有相应的规范的,比如 GET 请求设置啥参数,POST 请求发送什么数据,我们利用 JMeter 都能方便地配置。 JMeter 是有 GUI 的,我们在编写的时候在 GUI 里面设置就好了,界面样例如下所示: JMeter界面(图源:https://www.jianshu.com/p/0349441da3c4) 这里提示几点可能用到的东西:

  • 动态参数,JMeter 里面是支持动态参数设置的,比如循环测试 id 从 1 到 100,或者动态 POST 的数据替换,都是可以做到的,这个可以满足你花样测试接口的需求。
  • 定时器,JMeter 里面有很多 Timer,可以设置各种各样的延时操作,比如每 3 分钟测一次,随机等待多少秒测一次都行。
  • 断言,测试了接口之后,我们不仅要知道是否是可用的,同时也要判断其结果是不是正确的,如果返回状态码是正确的但是结果不对,那也白搭,所以可以使用断言来检查返回结果。

关于 JMeter 的功能这里就不再展开讲了,反正几乎你想实现的任何测试功能都可以实现,具体的用法可以参考 JMeter 的官方文档:https://jmeter.apache.org/usermanual/get-started.html。 嗯,写好了之后,可以用 JMeter 在本地进行测试,测试好了时候,可以把 JMeter 的这个 Test Plan 存成一个 jmx 文件,留作后面备用。

对接 Prometheus

接下来就是如何把数据对接到 Prometheus 里面了。 默认情况下,JMeter 是能导出数据到诸如 InfluxDB 这样的数据库的,借助于它自带的 Listener 即可实现。它并不带导出到 Prometheus 的功能。 这里我们就需要借助于 jmeter-prometheus-plugin 这个插件了,其 GitHub 地址是 https://github.com/johrstrom/jmeter-prometheus-plugin,具体的用法可以参考其官方说明。 这里提示几点:

Listener 的配置示例如图所示: JMeter Listener 这里字段名如 jsr223_rt_as_summary 可以自行修改,比如这里我就统一修改为了 jmeter_test_xxx 这样的字段。 配置完成之后,运行 JMeter 之后,我们就能在 http://localhost:9270 上看到 Exporter 的信息,如图所示: image-20200414155005999 这里面就包含了 JMeter 的一些接口测试结果,包括成功次数、失败次数、状态码等等,另外还有 JVM、处理器等各种环境信息。 部署之后把对应的 URL 交由 Prometheus 就可以把监控数据收集到 Prometheus 里面了。

部署 JMeter

完成上述两步,我们就能成功测试 Service 的每个接口并能生成测试结果的 Exporter 了。 那么 JMeter 写好了,怎么来部署呢?可以用 crontab,放某台服务器上,不过这里最理想的方式当然是部署到 Kubernetes 里面了。 这里就需要把 JMeter 打包成一个镜像了,GitHub 找来找去没找到几个合适的,另外也没有把 jmeter-prometheus-plugin 包括进去,那只有自己来了。 我基于 https://github.com/justb4/docker-jmeter 进行了二次改写,最后打包了一个镜像,已经开源了,地址为:https://github.com/Germey/JMeterMonitor,镜像名称为 germey/jmeter,这里就不再展开讲细节了,有点复杂。 运行所需要的 docker-compose 文件如下:

1
2
3
4
5
6
7
8
9
10
11
version: '3'
services:
jmeter:
restart: always
image: 'germey/jmeter'
volumes:
- ./jmx:/app
command:
- sample.jmx
ports:
- "80:80"

这里我把本地的 jmx 文件夹 mount 到了 Docker 的 app 文件夹,所以这里在运行时需要在项目文件夹下新建 jmx 文件夹,用于存放 jmx 文件,把刚才写好的 jmx 文件放过来就好了。 另外 command 就是 jmx 文件的名称,这里需要修改成你的 jmx 文件。 另外部署到 Kubernetes 的话可以参考这里的 yml 文件:https://github.com/Germey/JMeterMonitor/tree/master/kubernetes

Prometheus 收集数据

在成功部署 JMeter 之后呢,它肯定会提供一个 Web Service 来暴露 JMeter 的测试数据。 如果部署好了 Prometheus 之后,可以把它放在 Prometheus 的 scrape_configs,比如 Service 的 URL 为 jmeter-monitor.com,可以修改 prometheus.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.

# Attach these labels to any time series or alerts when communicating with
# external systems (federation, remote storage, Alertmanager).
external_labels:
monitor: 'codelab-monitor'

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'jmeter-monitor'

# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 5s

static_configs:
- targets: ['jmeter-monitor.com']

其具体的配置字段可以参见:https://prometheus.io/docs/prometheus/latest/configuration/configuration/。 另外呢,这种方式其实并不怎么好,修改 Prometheus 挺麻烦的,推荐使用 Helm + Prometheus-Operator 来安装 Prometheus,然后修改 values.yml 即可修改配置文件了,比如修改 https://github.com/helm/charts/blob/master/stable/prometheus-operator/values.yaml 里面的 additionalScrapeConfigs 即可。

Grafana 可视化

Prometheus 收集完数据之后,我们可以将其可视化出来了。 比如这里有些字段,jmeter_test_can_fail_success 代表成功请求的次数,jmeter_test_can_fail_total 代表总的测试次数。 那么就可以用一个表达式来计算 Error Rate 了:

1
1- jmeter_test_can_fail_success{instance="$instance"} / jmeter_test_can_fail_total{instance="$instance"}

效果如下: image-20200414165846578 这里就能实时可视化展示出来错误率了,更多的一些配置可以自行修改这些表达式进行定制。 最后我配置成的一些监控面板如下所示: image-20200414172832205 这样我要是什么时候想看 Service 接口的情况,随时上来看就好了。

报警

对于报警来说,可以使用两种方式配置,一个是直接使用 Grafana 自带的报警机制,另外是可以通过 Alert Manager,后者功能更加强大,推荐使用后者。 对于 Alert Manager 来说,其监控的规则这里推荐使用 Prometheus-Operator 里面自带的 PrometheusRule 来实现,比如可以定义这么一个 PrometheusRule:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
labels:
app: monitor
name: monitor-rules
spec:
groups:
- name: monitor
rules:
- alert: ServiceErroring
labels:
severity: warning
annotations:
message: Service 连续5分钟错误率过高。
expr: |
avg(1- jmeter_test_can_fail_success{job="service-monitor"} / jmeter_test_can_fail_total{job="service-monitor"}) > 0
for: 5m

这样配置好一个 PrometheusRule 之后,Prometheus 会自动应用这个 Rule 然后监控。 报警方式的话可以通过配置 Alert Manger 的 Receiver 来实现,包括打电话、邮件、短信等等,配置规则可以见:https://prometheus.io/docs/alerting/configuration/。 目前我是利用了组内已经提供的报警机制,组内已经对接好了电话、短信、邮件报警,并可以把每个人的信息进行管理和分组,然后应用到某个报警规则里面,这样一旦有问题,就可以实现报警啦。 另外对于一些规则的管理,我们可以使用一些开源的 Dashboard 来管理,如 Krama,https://github.com/prymitive/karma,利用它我们可以方便配置、禁用和筛选一些报警规则,界面如下image-20200414173101030 不过公司内部已经实现了一套了,对接了公司的员工账号,更加方便,所以我就没有再用这个了。

定时重启

这里另外遇到了一个问题,就是 JMeter 导出的监控数据是不断累积的,而监控的数据则是需要监控最近几分钟的数据,这样一旦发生了 Error,那么 Error Rate 由于历史数据的原因,在服务恢复之后永远不会降为 0,这就导致一些问题。另外如果 JMeter 如果一直运行,其占用的内存会越来越大。 所以一个最好的方式就是定时将 JMeter 重启,这样可以定时清空历史监控数据,保证在新的一段时间内测试获取到最近的监控数据,而不是混杂历史数据。 这里重启就可以利用 Kubernetes 的 Cronjob,比如我们可以每隔 10 分钟让 JMeter 重启一次,类似配置如下:

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
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: jmeter-monitor
spec:
successfulJobsHistoryLimit: 0
failedJobsHistoryLimit: 0
concurrencyPolicy: Replace
schedule: "*/10 * * * *"
jobTemplate:
spec:
template:
metadata:
labels:
service: jmeter-monitor
spec:
containers:
- args:
- jmeter-monitor.jmx
image: germey/jmeter:1
name: jmeter-monitor
volumeMounts:
- mountPath: /app
name: jmeter-storage
ports:
- containerPort: 80
imagePullPolicy: IfNotPresent
resources:
requests:
memory: "4Gi"
cpu: "250m"
limits:
memory: "4Gi"
cpu: "250m"
restartPolicy: OnFailure
volumes:
- name: jmeter-storage
persistentVolumeClaim:
claimName: jmeter

这里有几个地方值得注意:

  • 一个是 concurrencyPolicy,这里配置为 Replace,意思是重启后新建的 Pod 会替换原来的 Pod,保证 JMeter 的 Pod 只有一个。
  • 另外一个是 imagePullPolicy 配置为 IfNotPresent,这样可以每次重启的时候不用重新拉镜像。

这样的话,就能避免发生错误的时候 Error Rate 无法降为 0 的状态了。 好了,到此为止呢,我们就介绍完了使用 JMeter + Kubernetes + Prometheus + Grafana + Alert Manager 进行监控的整体思路了,希望对大家有帮助。 另外由于内容比较多,这里很多地方没有展开讲解,比如 JMeter 的配置、Grafana 的配置、Prometheus-Operator 的配置、Alert Manager 的配置等等,不知道大家敢不敢兴趣,如果感兴趣的话,后面可以继续深入写一个小系列来讲解哈。

个人随笔

刚刚又完成了一项任务。 而这个任务明天就要检查上交了,在这之前,我其实有很多很多的时间去做这件事,而我还是把这个拖到了最后。 怎么说呢?这个就是拖延症,不到最后一刻不去做,总要拖到 Deadline 才去搞。哎,其实吧反思我自己,一直就有这个毛病,不知道大家是不是也有这个毛病,或许大家都有的吧。不然怎么来的 Deadline 是第一生产力的说法呢。 这个毛病体现在太多方面了,比如拿我自己说吧:

  • 快到交稿的时候才去再赶点。
  • 快到明天检查项目的时候才去做点。
  • 快到考前再去复习。

其实也不能说拖延症不好吧,比如拿第三项「快到考前再去复习」,我确实到最后的时候效率会比平时高非常多。我大学和研究生几乎都是翘课翘过来的,除了那些重要的或者老师点名的课会去去,其他的一律翘掉,最后考前突击两星期,然后考个八九十分。悄悄炫耀下吧,当时突击得大学平均绩点最后突击了年级前 5%,最后还保研了,这其实让我有点难以相信的。不过研究生成绩就不行了,因为已经铁了心毕业就工作,所以精力都放在了自己的项目和公司工作上面,所以成绩就没上心了,中游水平吧。 再说回大学吧,由于我平时对考前复习的 “拖延”,我可以平时把精力放在自己更感兴趣的事情上面。比如大学的时候吧,我加了一个实验室,基本上翘课的时间都是泡在实验室里面,我也忘记是在做什么了,也有在摸鱼,反正就是不想上课,当然也有挺多时候在撸代码,学一些技术什么的。所以相比一些「学霸」,我平时还 GET 到了一些编程技能,最后可能绩点还比他们高,而他们可能所有精力都放在功课上了,这可能他们想起来就有点气了哈哈。但其实吧,我虽然当时有点得意,以为自己占到了便宜,但其实当时对某些基础的理解确实不如平时认真学习的同学理解得透彻,比如当时计算机网络、计算机组成原理,当时就是考试突击,考试得了高分,但其实当时对里面的知识理解并不到位,到了真正用的时候,拾起来就没那么容易了。但当然也分科目,比如我到现在工作真的也没有用到计算机组成原理、电子电路的任何一点知识,以后也可能不会用到。所以,如果正在读本文的你,现在如果还在读大学的话,想清楚哪些科目将来对你可能是有用的,好好去学,好好去听讲,不要学我翘这么多课,没准你将来真的可能会用到,到时候就不要后悔自己当时为什么没好好学。 扯远了,这里只是想论证下有些情况拖延症并不一定完全是坏处,也算是给拖延症找了个美丽但似乎不太恰当的借口吧。 不过话说回来,拖延症在多数情况下是不好的,例子就不举了,大家肯定也深有体会。 我曾经也看过关于拖延症的一些科普或 “诊疗” 方案,怎么说呢?每人都会有惰性,每人都喜欢待在自己的 “舒适区”,毕竟躺在被窝里面玩手机多么舒服啊。 还看到一个关于拖延症的解释是说不够热爱,如果你非常热爱你做的事情,你是不会拖延的。我想了想,好像不是这样,比如我热爱 Python、喜欢编程、喜欢去实现某些东西,我想起来为什么有的时候还是不想去做呢?也不是不喜欢不热爱,那就是懒,可能喜欢呆在舒适区就是更合理的解释,因为我也喜欢睡懒觉和躺着玩手机。 所以,有什么方法能缓解我的拖延症呢? 我之前试过一个方法,还是挺有用的。这里分享给大家吧。 比如我现在要写一个过了很久都不维护的项目吧,一想起来要打开它、配置环境、跑起来就各种事,懒得去做。这时候,我通常采取的方法就是强忍着,啥也不管,我就去迎着头皮去做,比如去把项目代码 clone 下来,然后 IDE 打开,然后试着跑起来,出错了稍微调一调,或者试着改几句代码。过这么十几分钟折腾,这时候发现再写,那就能比较顺利地写下去了。 比如我现在要写一篇文章,一想起来我要打开文本编辑器、构思、梳理知识点就又懒得去做。这时候我采取的方案就是强制我自己打开文本编辑器,比如现在写的这篇文章就是这样,我本来并不想写,但是脑子里面又想到了一些事情,还是写写比较好,那我就硬着头皮打开吧,本想写个一两百字就行了,结果你看我现在写到这里了,咦你看我又写了十个字呢,你看我又写了十个字呢,是不是有点文思泉涌啊哈哈哈哈哈。诺,你看到现在,从我写这篇稿子到现在,也就过了不到十分钟吧,我这篇稿子就快收尾了,舒服啊。 俗话说,万事开头难,做事情就是这样,如果大家也遇到了这个问题,不妨也可以试试这个方法,感觉还挺有效的。 但并不是 100% 有效哈,有时候我就是懒到一天啥也不想干也不是没有。 最后再说一个点,那就是其实,你看这件事,你在做之前,你可能觉得这件事好麻烦,不想干,或者好难,我干不了。但在可能大多数情况下,一旦你开始干了,你会发现并没有那么难那么麻烦。 其实,难的就是开始。 好了,我的稿子写完了,你看我在写之前觉得麻烦的一件事,这不就完结了吗。 嗯,就是大晚上随便写写,突然想起来,我似乎今天还有几件觉得比较麻烦的事还没干呢,我去开头去啦,拜拜。

技术杂谈

在前面写过一篇文章介绍深度学习识别滑动验证码缺口的文章,在这篇文章里,我们使用华为云 ModelArts 轻松完成了滑动验证码缺口的识别。但是那种实现方案依赖于 ModelArts,是华为云提供的深度学习平台所搭建的识别模型,其实其内部是用的深度学习的某种目标检测算法实现的,如果利用平台的话,我们无需去申请 GPU、无需去了解其内部的基本原理究竟是怎么回事,它提供了一系列标注、训练、部署的流程。 但用上述方法是有一定的弊端的,比如使用会一直收费,另外不好调优、不好更好地定制自己的一些需求等等。所以这里再发一篇文章来介绍一下直接使用 Python 的深度学习模型来实现滑动验证码缺口识别的方法。

效果

目前可以做到只需要几百张缺口标注图片即可训练出精度高的识别模型,并且可扩展修改为其他任何样式的缺口识别,识别效果样例: 样例 只需要给模型输入一张带缺口的验证码图片,模型就能输出缺口的轮廓和边界信息。 感兴趣的可以继续向下看具体的实现流程。

基础了解

缺口识别属于目标检测问题,关于什么是目标检测这里就不再赘述了,可以参考之前写的这篇文章。 当前做目标检测的算法主要有两种路子,有一阶段式和两阶段式,英文叫做 One stage 和 Two stage,简述如下:

  • Two Stage:算法首先生成一系列目标所在位置的候选框,然后再对这些框选出来的结果进行样本分类,即先找出来在哪,然后再分出来是啥,俗话说叫「看两眼」,这种算法有 R-CNN、Fast R-CNN、Faster R-CNN 等,这些算法架构相对复杂,但准确率上有优势。
  • One Stage:不需要产生候选框,直接将目标定位和分类的问题转化为回归问题,俗话说叫「看一眼」,这种算法有 YOLO、SSD,这些算法虽然准确率上不及 Two stage,但架构相对简单,检测速度更快。

所以这次我们选用 One Stage 的有代表性的目标检测算法 YOLO 来实现滑动验证码缺口的识别。 YOLO,英文全称叫做 You Only Look Once,取了它们的首字母就构成了算法名, 目前 YOLO 算法最新的版本是 V3 版本,这里算法的具体流程我们就不过多介绍了,感兴趣的可以搜一下相关资料了解下,另外也可以了解下 YOLO V1-V3 版本的不同和改进之处,这里列几个参考链接。

数据准备

回归我们本节的主题,我们要做的是缺口的位置识别,那么第一步应该做什么呢? 我们的目标是要训练深度学习模型,训练模型,那我们总得需要让模型知道要学点什么东西吧,这次我们做缺口识别,那么我们需要让模型学的就是这个缺口在哪里。由于一张验证码图片只有一个缺口,要分类就是一类,所以我们只需要找到缺口位置就行了。 好,那模型要学缺口在哪里,那我们就得提供点样本数据让模型来学习才行。数据怎样的呢?那数据就得有带缺口的验证码图片以及我们自己标注的缺口位置。只有把这两部分都告诉模型,模型才能去学习。等模型学好了,等我们再给个新的验证码,那就能检测出缺口在哪里了,这就是一个成功的模型。 OK,那我们就开始准备数据和缺口标注结果吧。 数据这里用的是网易盾的验证码,验证码图片可以自行收集,写个脚本批量保存下来就行。标注的工具可以使用 LabelImg,GitHub 链接为:https://github.com/tzutalin/labelImg,利用它我们可以方便地进行检测目标位置的标注和类别的标注,如这里验证码和标注示例如下标注效果 标注完了会生成一系列 xml 文件,你需要解析 xml 文件把位置的坐标和类别等处理一下,转成训练模型需要的数据。 在这里我先把我整理的数据集放出来吧,完整 GitHub 链接为:https://github.com/Python3WebSpider/DeepLearningSlideCaptcha,我标注了 200 多张图片,然后处理了 xml 文件,变成训练 YOLO 模型需要的数据格式,验证码图片和标注结果见 data/captcha 文件夹。 如果要训练自己的数据,数据格式准备见:https://github.com/eriklindernoren/PyTorch-YOLOv3#train-on-custom-dataset

初始化

上一步我已经把标注好的数据处理好了,可以直接拿来训练了。 由于 YOLO 模型相对比较复杂,所以这个项目我就直接基于开源的 PyTorch-YOLOV3 项目来修改了,模型使用的深度学习框架为 PyTorch,具体的 YOLO V3 模型的实现这里不再阐述了。 另外推荐使用 GPU 训练,不然拿 CPU 直接训练速度很慢。我的 GPU 是 P100,几乎十几秒就训练完一轮。 下面就直接把代码克隆下来吧。 由于本项目我把训练好的模型也放上去了,使用了 Git LFS,所以克隆时间较长,克隆命令如下:

1
git clone https://github.com/Python3WebSpider/DeepLearningSlideCaptcha.git

如果想加速克隆,暂时先跳过大文件模型下载,可以执行命令:

1
GIT_LFS_SKIP_SMUDGE=1 git clone https://github.com/Python3WebSpider/DeepLearningSlideCaptcha.git

环境安装

代码克隆下载之后,我们还需要下载一些预训练模型。 YOLOV3 的训练要加载预训练模型才能有不错的训练效果,预训练模型下载命令如下:

1
bash prepare.sh

执行这个脚本,就能下载 YOLO V3 模型的一些权重文件,包括 yolov3 和 weights 还有 darknet 的 weights,在训练之前我们需要用这些权重文件初始化 YOLO V3 模型。

注意:Windows 下建议使用 Git Bash 来运行上述命令。

另外还需要安装一些必须的库,如 PyTorch、TensorBoard 等,建议使用 Python 虚拟环境,运行命令如下:

1
pip3 install -r requirements.txt

这些库都安装好了之后,就可以开始训练了。

训练

本项目已经提供了标注好的数据集,在 data/captcha,可以直接使用。 当前数据训练脚本:

1
bash train.sh

实测 P100 训练时长约 15 秒一个 epoch,大约几分钟即可训练出较好效果。 训练差不多了,我们可以使用 TensorBoard 来看看 loss 和 mAP 的变化,运行 TensorBoard:

1
tensorboard --logdir='logs' --port=6006 --host 0.0.0.0

loss_1 变化如下: loss 变化 val_mAP 变化如下: mAP 变化 可以看到 loss 从最初的非常高下降到了很低,准确率也逐渐接近 100%。 另外训练过程中还能看到如下的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
\---- [Epoch 99/100, Batch 27/29] ----
+------------+--------------+--------------+--------------+
| Metrics | YOLO Layer 0 | YOLO Layer 1 | YOLO Layer 2 |
+------------+--------------+--------------+--------------+
| grid_size | 14 | 28 | 56 |
| loss | 0.028268 | 0.046053 | 0.043745 |
| x | 0.002108 | 0.005267 | 0.008111 |
| y | 0.004561 | 0.002016 | 0.009047 |
| w | 0.001284 | 0.004618 | 0.000207 |
| h | 0.000594 | 0.000528 | 0.000946 |
| conf | 0.019700 | 0.033624 | 0.025432 |
| cls | 0.000022 | 0.000001 | 0.000002 |
| cls_acc | 100.00% | 100.00% | 100.00% |
| recall50 | 1.000000 | 1.000000 | 1.000000 |
| recall75 | 1.000000 | 1.000000 | 1.000000 |
| precision | 1.000000 | 0.800000 | 0.666667 |
| conf_obj | 0.994271 | 0.999249 | 0.997762 |
| conf_noobj | 0.000126 | 0.000158 | 0.000140 |
+------------+--------------+--------------+--------------+
Total loss 0.11806630343198776

这里显示了训练过程中各个指标的变化情况,如 loss、recall、precision、confidence 等,分别代表训练过程的损失(越小越好)、召回率(能识别出的结果占应该识别出结果的比例,越高越好)、精确率(识别出的结果中正确的比率,越高越好)、置信度(模型有把握识别对的概率,越高越好),可以作为参考。

测试

训练完毕之后会在 checkpoints 文件夹生成 pth 文件,可直接使用模型来预测生成标注结果。 如果你没有训练自己的模型的话,这里我已经把训练好的模型放上去了,可以直接使用我训练好的模型来测试。如之前跳过了 Git LFS 文件下载,则可以使用如下命令下载 Git LFS 文件:

1
git lfs pull

此时 checkpoints 文件夹会生成训练好的 pth 文件。 测试脚本:

1
sh detect.sh

该脚本会读取 captcha 下的 test 文件夹所有图片,并将处理后的结果输出到 result 文件夹。 运行结果样例:

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
Performing object detection:
+ Batch 0, Inference Time: 0:00:00.044223
+ Batch 1, Inference Time: 0:00:00.028566
+ Batch 2, Inference Time: 0:00:00.029764
+ Batch 3, Inference Time: 0:00:00.032430
+ Batch 4, Inference Time: 0:00:00.033373
+ Batch 5, Inference Time: 0:00:00.027861
+ Batch 6, Inference Time: 0:00:00.031444
+ Batch 7, Inference Time: 0:00:00.032110
+ Batch 8, Inference Time: 0:00:00.029131

Saving images:
(0) Image: 'data/captcha/test/captcha_4497.png'
+ Label: target, Conf: 0.99999
(1) Image: 'data/captcha/test/captcha_4498.png'
+ Label: target, Conf: 0.99999
(2) Image: 'data/captcha/test/captcha_4499.png'
+ Label: target, Conf: 0.99997
(3) Image: 'data/captcha/test/captcha_4500.png'
+ Label: target, Conf: 0.99999
(4) Image: 'data/captcha/test/captcha_4501.png'
+ Label: target, Conf: 0.99997
(5) Image: 'data/captcha/test/captcha_4502.png'
+ Label: target, Conf: 0.99999
(6) Image: 'data/captcha/test/captcha_4503.png'
+ Label: target, Conf: 0.99997
(7) Image: 'data/captcha/test/captcha_4504.png'
+ Label: target, Conf: 0.99998
(8) Image: 'data/captcha/test/captcha_4505.png'
+ Label: target, Conf: 0.99998

拿几个样例结果看下: 这里我们可以看到,利用训练好的模型我们就成功识别出缺口的位置了,另外程序还会打印输出这个边框的中心点和宽高信息。 有了这个边界信息,我们再利用某些手段拖动滑块即可通过验证了。本节不再展开讲解。

总结

本篇文章我们介绍了使用深度学习识别滑动验证码缺口的方法,包括标注、训练、测试等环节都进行了阐述。 GitHub 代码:https://github.com/Python3WebSpider/DeepLearningSlideCaptcha。 欢迎 Star、Folk,如果遇到问题,可以在 GitHub Issue 留言。

Python

声明:本文由一位知名的不知名 Payne 原创,转载请注明出处! 首先说一下这个有啥用?要说有用也没啥用,要说没用吧,既然能拿到这些数据,拿来做数据分析。能有效的得到职位信息,薪资信息等。也能为找工作更加简单吧,且能够比较有选择性的相匹配的职位及公司 本章节源码仓库为:https://github.com/Payne-Wu/PythonScrape 一言不合直接上代码!具体教程及思路总代码后! 所用解释器为 Python3.7.1,编辑器为 Pycharm 2018.3.5. 本着虚心求学,孜孜不倦,逼逼赖赖来这里虚心求学,孜孜不倦,逼逼赖赖,不喜勿喷,嘴下手下脚下都请留情。 本节所涉:Request 基本使用、Request 高级使用-会话维持、Cookies、Ajax、JSON 数据格式 Request 更多详情请参考 Request 官方文档: 轻松入门中文版 高级使用中文版 Cookie:有时也用其复数形式 Cookies。类型为“小型文本文件”,是某些网站为了辨别用户身份,进行Session跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息 具体 Cookies 详情请参考:https://baike.baidu.com/item/cookie/1119?fr=aladdin

Ajax 即“Asynchronous Javascript And XML”(异步 JavaScript 和 XML),是指一种创建交互式、快速动态网页应用的网页开发技术,无需重新加载整个网页的情况下,能够更新部分网页的技术。

通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

JSON(JavaScript Object Notation): 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集。 JSON 采用完全独立于语言的文本格式,但是也使用了类似于 C 语言家族的习惯(包括 C, C++, C#, Java, JavaScript, Perl, Python 等)。 这些特性使 JSON 成为理想的数据交换语言。

首先介绍一下关于本章代码的基本思路: 四步走(发起请求、得到响应、解析响应得到数据、保存数据) 四步中准确来说是三步,(发起请求,得到响应、解析响应,提取数据、保存数据)

  • 请求网页(在搜索框中输入所查询的岗位<例如:Python>,得到BASE_URL,)

    • BASE_URL:https://www.lagou.com/jobs/list_Python?labelWords=&fromSearch=true&suginput=
    • 加入请求头(注意加 Cookies),请求 BASE_URL[gallery columns=”1” size=”full” ids=”9164”]
    • 观察响应信息以及本网页源码观察浏览器网页源码,对比发现其中并没有我们所需要的信息:发现 Ajax 的痕迹。
    • 经过一系列操作发现Ajax 网页地址(在这里直接请求此链接并不能访问):
    • 多次请求过后,发现错误。错误的缘由是由于 Cookies 限制,并且网页以动态检测,且时间间隔小。

      • 经过前言的学习,已经学会了。会话维持。动态得到 Cookies,这样不就可以把这个“反爬”彻底绕过了呢?答案肯定是滴
      • 哪让我们做一下会话维持,并动态提取 Cookies 吧。

      • 易混淆点:cookies 的维持为什么是维持 BASE_URL 的而不是 Ajax_URL?下面按照个人理解对于本 Ajax 给出以下解释:结合 Ajax 原理可知,Ajax 其基本原理就是在网页中插入异步触发的。说到底他还是在这个页面,并没有转到其他页面。只是需要特定条件触发即可插入本网页

        • ```
          def Get_cookies(header):
          """
          Get cookies
          @param header:
          @return: cookies
          """
          with requests.Session() as s:
              s.get(cookies_url, headers=header)
              cookies = s.cookies
          # cookies = requests.get(cookies_url, headers=header).cookies
          return cookies
          
    
1
2
3
4
5
6
    - 万事俱备、只欠东风:请求 Ajax_URL 即可得到以下![](https://cdn.cuiqingcai.com/wp-content/uploads/2020/04/DemoPicture2-Ajax-1.png)
- 得到响应:经过以上操作已经请求完成了。并能够保障请求稳定性。(当然在此并没有做异常捕获,如果加上,将会更稳)

- ## 解析响应:如果上述步骤没有错的话,到此已经能得到网页数据了(如上图):

- - 我用的提取代码如下 :
def parse(message): industryField = message['industryField'] # company_message positionName = message['positionName'] companyFullName = message['companyFullName'] companySize = message['companySize'] financeStage = message['financeStage'] # companyLabelList = message['companyLabelList'] companyLabelList = '|'.join(message['companyLabelList']) Type = "|".join([message['firstType'], message['secondType'], message['thirdType']]) Address = ''.join([message['city'], message['district'], ]) salary = message['salary'] positionAdvantage = message['positionAdvantage'] # limitation factor workYear = message['workYear'] jobNature = message['jobNature'] education = message['education'] items = f"{positionName}, {companyFullName}, {companySize}, {financeStage}, {companyLabelList}, {industryField}, " \ f"{Type}, {salary}, {jobNature}, {education}" # items = "".join(str( # [positionName, companyFullName, companySize, financeStage, companyLabelList, industryField, Type, salary, # jobNature, education])) if items: # print(items) logging.info(items) # return items.replace('[', '').replace(']', '') return items.replace('(', '').replace(')', '')
1
2
3
4
5

- 此时只需提取相关数据,即可。得到:![](https://cdn.cuiqingcai.com/wp-content/uploads/2020/04/DemoPicture3-json.png)
- ## 保存数据:

- ## 常规保存:(保存到本地)

def save_message(item):
with open(‘lg3.csv’, ‘a+’, encoding=’gbk’) as f:
f.write(item + ‘\n’)
thread_lock.release()

1
2
3
4
5
6
7
8
9
10
11
12

- ## 数据入库:(保存到数据库)

## 在这里我选择的为 Mongo,接下来,那咱们操作一下吧。Mongo 的安装便不在此处赘述。与 mongo 相关的文章,在这里比较推荐才哥和东哥的几篇文章(以本文来看,比较建议看看这几篇文章。并没说其他不好啊,不,我没有,我没说哦),地址如下:

- ### [如何学好 MongoDB](https://cuiqingcai.com/7121.html)
- ### [[Python3 网络爬虫开发实战] 1.4.2-MongoDB 安装](https://cuiqingcai.com/5205.html)
- ### [[Python3 网络爬虫开发实战] 1.5.2-PyMongo 的安装](https://cuiqingcai.com/5230.html)

##   前方高能预警,造!!!:(此时的你已安装了 Mongo,并能正常使用 mongo。剩下的交给我,我教你好了)

1. ### 安装 pymongo
pip install pymongo
1
2

2. ### 建立连接:在原有的代码基础上改写,添加类似于如下的代码:
MONGO_CONNECTION_STRING = 'mongodb://localhost:27017' # MONGO_DB_NAME = 'Jobs' # MONGO_COLLECTION_NAME = 'Jobs' client = pymongo.MongoClient(MONGO_CONNECTION_STRING) db = client['Jobs'] collection = db['Jobs']
1
2
3
4

![](https://cdn.cuiqingcai.com/wp-content/uploads/2020/04/Annotation-2020-04-23-101932.png)

3. ### 新增存储方法:
def save_data(self, date): """ save to mongodb :param date: :return: """ collection.update_one({ 'name': date.get('companyShortName') }, { '$set': date }, upsert=True)
1
2
3
4

![](https://cdn.cuiqingcai.com/wp-content/uploads/2020/04/微信图片_20200423102245.png)

4. ### 调用此方法:
def main(): p = LaGou() for page in range(1, 31): content = p.scrape(page) data = p.parseResponse(content) download = p.save_data(data) ```

注意:由于 mongo 的存储格式为 key :value 形式,所以咱们提取到的数据返回也必须是 key :value 形式:

看我看我,怎么搞的,我是这样搞的:

左手叉腰,右手摇,Over!

光看文章的话,就算是我自己写的文章单单仅仅看文章也是会云里雾里,建议与源码一起阅读。祝学习进步,心想事成。加油~

写到最后:既然能读到这儿,那么我相信不是白嫖成为习惯的人,说明也或多或少想自己搞一搞。整一整?下次也出来吹吹牛皮,拉钩晓得不,反爬难吧?我会了(虽然对于大佬来说,都可能算不上反扒,和玩似的,这个确实也是的。不过吧,对于新手来说,已经算很难了。)我也是搞过拉勾的男人。找工作就找我,啊哈哈哈。

单一的案例终究只会让你局限于本次案例,如果拉钩反爬又更新了。那么这个就会失效。虽然授之以渔了,但终究是“这条小溪”,更大的海洋还需要更加刻苦努力的学习。个人比较建议学习一下

  • 感谢阅读与支持,谢谢

Python

meizi图会爬么?不会那我教你好了

声明:本文由一位知名的不知名Payne原创,转载请注明出处!后优化代码为52讲轻松搞定网络爬虫第一次实战课程为本节代码思路为基础,后自作聪明,书写。 本章节源码仓库为:https://github.com/Payne-Wu/PythonScrape 写在最前面:本章适用于新手小白,代码规范化思路。 一言不合直接上代码!具体教程及思路总代码后! 所用解释器为Python3.7.1,编辑器为Pycharm 2018.3.5. 本着虚心求学,孜孜不倦,逼逼赖赖来这里虚心求学,孜孜不倦,逼逼赖赖,不喜勿喷,嘴下手下脚下都留情。

基础源码(我刚自学Python时候写的代码)

要爬就爬全站的,一两张一两个还不如另存为来的更加实际。啊哈哈哈

  1. 那咱们先来说说Meizitu的基本思路:

个人总结(四步法):发送请求, 得到响应, 解析数据, 保存数据(比较宏观的概念)

  • 众所周知啊(其实是个人理解):爬虫本质为模拟浏览器发送请求 ,倒推过来思考 既然是模拟请求,请求地址(URI)总得晓得吧。那么本次请求的URI为以下(单个图册):
  • 模拟请求,既然是模拟请求怎么也得,模拟一下吧?被站长大大晓得了那不就被关在门外了。(站长“菇凉”说:“我家是给帅气的小哥哥用户访问的,你个程序来凑一凑不太好吧?”在我脑壳后面敲了敲,后就把我拒之门外,头也不会,扬长而去)经过我苦思冥想,仿佛得到了前所未有的启发,要不我们走走后门,让这个程序做个人?如此甚好。妙哉,妙哉。。。

    • 请求头一带谁都不爱。大叫一声还有谁,于是我就上啊。然而结果确实如此
    • ```
      url = ‘https://www.mzitu.com/
      header =
      {
      “User-Agent”: “Mozilla/5.0 (Windows NT 10.0; WOW64; rv:66.0) Gecko/20100101 Firefox/66.0”
      }

      response = requests.get(url, headers=header) # 请求网页
      print(response)

      1
      2
      3
      4
      5
      6
      7
      8
      9
              
      reponse<200>
      * 经过我请求,他回应,他回应了爱你你(200),最后咱们握手了。菇凉,咱们还不够了解。需要多了解了解。我还没有成为无敌的男ying,我得多学习学习,争取早日娶你过门。(论一个渣男的锻炼与养成)
      * 好不容易进来了,本着虚心求教,好好学习,天天向上,不谈儿女私情的我,怎么也得有所收获吧。这是???渣男的修炼养成计划?这是宝贝啊!!!我得看看,学习学习。
      * [gallery size="full" columns="1" ids="9144"]
      * 这还上锁啦?,这个可不香了。经过研究,一笑  你有你的的张良计,我有我的万能钥匙,Refer钥匙。配置Refer。走着。还不乖乖的。小意思啦。。。。就这样。
      * [gallery size="full" columns="1" ids="9145"]

      虽然不是用的同一张图片,BUT,有无图片是这Refer防盗链作祟的。 **ok,nice。** 各位看官,到这儿咱们也得进入真正的 说了这么多,让我们进入造的环节把,基础四步走,发起请求,得到响应,解析数据,保存!打完收工。 首先咱们是定义了一个自定义的用户代理,优点稳定、简单易于操作,但缺点是比较繁琐。 为什么不用from fake_useragent import UserAgent 这个主要还是因为这个模块不大稳定,我用的时候经常出错。不晓得是不是我。。。 至少我用的不太爽。而且既然自己阔以造轮子,为何不试着造一造呢? 说说User-Agent构造思路,首先是定义了一个User-Agent列表,然后从里面随机取一,作为相对本次User—Agent。 优化后的代码是直接定义了一个请求函数,这样如果需要请求是阔以直接调用这个函数,避免代码臃肿。无脑写requests。既不提升效率,也不简洁。还看着懵。有好的自然就要给好的,有时候对自己得好点。 注意请不要忽略异常捕获哦,这样会使咱们的爬虫小伙计更加健壮。爬虫的Strong在于考虑细致,全方面。这样成为一只成年的爬虫,虽然我也不晓得谁说的,我感觉还是蛮有道理的,如果真的没有人说,那就是我吧。。。。 这里是直接定义了一个请求方法,方便需要请求的时候直接调用。即可获得请求的效果

      def scrape_page(url):
      logging.info(‘scraping %s…’, url)

      header = User_Agent(page)

      print(header)
      try:
      response = requests.get(url, headers=header)
      return response.text
      except TimeoutError as a:
      logging.error(f”Error time is Out:{a}”)
      except ReadTimeout as b:
      logging.error(f”Error ReadTimeout: {b}”)
      except HTTPError as c:
      logging.error(f”Error HTTPError: {c}”)
      except ConnectionError as d:
      logging.error(f”Error ConnectionError: {d}”)

      1
      2


      def scrape_index(page):
      index_url = f”{BASE_URL}/page/{page}/“
      return scrape_page(index_url)

2.1 解析数据(获得图册的URN)

def parseindex(html):
doc = pq(html)
links = doc(‘.postlist #pins > li > span:nth-child(2) > a’)
for link in links.items():
detailurl = link.attr(‘href’)
logging.info(‘Got detailurl %s’, detailurl)
yield detail_url
def scrape_detail(url):
return scrape_page(url)

1
2

做个简单的小结: ```python def main(page): User_Agent(page) index_html = scrape_index(page) detail_urls = parse_index(index_html) # 循环遍历提取出URI for detail_url in detail_urls: 这样就可以得到所有的URI了: ... 2020-04-10 10:07:42,396 - INFO: scraping https://www.mzitu.com/206355... 2020-04-10 10:07:42,703 - INFO: Got detail_url https://www.mzitu.com/212891 2020-04-10 10:07:42,703 - INFO: scraping https://www.mzitu.com/212891... 2020-04-10 10:07:42,863 - INFO: Got detail_url https://www.mzitu.com/225494 2020-04-10 10:07:42,863 - INFO: scraping https://www.mzitu.com/225494... 2020-04-10 10:07:42,992 - INFO: Got detail_url https://www.mzitu.com/226142 2020-04-10 10:07:42,992 - INFO: scraping https://www.mzitu.com/226142... 2020-04-10 10:07:43,172 - INFO: Got detail_url https://www.mzitu.com/226078 2020-04-10 10:07:43,173 - INFO: scraping https://www.mzitu.com/226078... 2020-04-10 10:07:43,661 - INFO: Got detail_url https://www.mzitu.com/210338 2020-04-10 10:07:43,661 - INFO: scraping https://www.mzitu.com/210338... 2020-04-10 10:07:43,838 - INFO: Got detail_url https://www.mzitu.com/225958 2020-04-10 10:07:43,839 - INFO: scraping https://www.mzitu.com/225958... 2020-04-10 10:07:44,003 - INFO: Got detail_url https://www.mzitu.com/225784 2020-04-10 10:07:44,004 - INFO: scraping https://www.mzitu.com/225784... 2020-04-10 10:07:44,163 - INFO: Got detail_url https://www.mzitu.com/218827 2020-04-10 10:07:44,163 - INFO: scraping https://www.mzitu.com/218827... 2020-04-10 10:07:44,402 - INFO: Got detail_url https://www.mzitu.com/213225 ... 然后就没有然后了呗 经过发现每个图集里面是后面加上page就是第几张图片,并且张数是不一样的,那怎么样解决呢? [gallery columns="1" size="full" ids="9146"] [gallery columns="1" size="full" ids="9147"] 多观察几个发现它只显示8个,第七个就是最大的页码数了, 那咱们把他提取出来,作为图集的page,放入循环不就可以拿到任意图集的所有了嘛!经过验证这是可行的。 既然这些又有了,那咱们请求它,存就好啦

if __name
== ‘__main
‘:
for page in range(1, 2):
main(page)
1
2

  最后加入多进程加快速度 改写源码如下

import multiprocessing

TOTAL_PAGE 为所爬取页数

if name == ‘main‘:
pool = multiprocessing.Pool()
pages = range(1, TOTAL_PAGE + 1)
pool.map(main, pages)
pool.close()
```

写到最后: 能看到这里,我相信你是非常想学的,单单看到这里是不可能学会的了,那么如果要去做,阔以尝试着与本章源码一起阅读。希望你能有所收获。 如果想要更好的运用到实际生活中,以爬虫为兴趣,以此为工作。阔以学习崔老师的52讲轻松搞定网络爬虫。祝生活愉快,完事顺心。期待我们下次见面 有问题,有疑问,欢迎在评论区留言,我将知无言,言无不尽。评论区期待你的出现 -Payne