0%

JS逆向

寻找加密位置

JS hook

  1. 寻找hook的点, 比如设置header设置参数,加密解密
    1. hook最常用的场景是加密解密, cookie, 请求参数等
  2. 编写hook逻辑
  3. 请求调试

应用场景

  1. 过debugger
  2. 拦截请求,注入ws
  3. 主动debugger即寻找参数
  4. 函数替换,函数置空、函数覆盖

JSON.parse

JSON.parse用于将 JSON 格式的字符串解析为 JavaScript 对象, 它通常用于处理从服务器接收到的 JSON 数据。

1
2
3
4
5
6
7
8
(function () {
var parse = JSON.parse;
JSON.parse = function (arg) {
console.log("您猜怎么着?断住了! --> ", arg);
debugger;
return parse(arg); // 不改变原来的执行逻辑
}
})()

XHR断点

1
2
3
4
5
6
7
8
9
10
(function(){
var xhr = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function (method, url, async) {
if (typeof url === 'string' && url.indexOf("comp") !== -1) {
console.log("您猜怎么着?断住了! --> ", method, url, async);
debugger;
}
return xhr.apply(this, arguments);
};
})();

headers

1
2
3
4
5
6
7
8
9
10
(function(){
var sh=window.XMLHttpRequest.prototype.setRequestHeader;
window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
if(key=="enc"){
console.log("您猜怎么着?断住了! --> ", key, value);
debugger;
}
return sh.apply(this, arguments);
};
})()

请求每个页面的cookie都有同一个参数,但是值是加密的而且会发生变化,那么大概率它是反爬参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function(){
var ck='';
Object.defineProperty(document, 'cookie', {
set: function (val) {
if (val.indexOf("v") != -1) {
console.log("您猜怎么着?断住了! --> ", val);
debugger;
}
ck = val;
return val;
},
get: function () {
return ck;
}
})
})()

置空

1
2
3
4
function deg(){
debugger;
}
debugger = function(){}

xhr

使用方法

  1. 从请求网址中选择一小段关键字
  2. Source—>XHR/fetch Breakpoints—>+
  3. 输入关键字
  4. 断住之后跟栈分析

跟启动器和堆栈

适用于任何有启动器的请求

使用方法

  1. 打开chorme开发者工具的network
  2. 选择涉及目标数据的文件
  3. 点击启动器Initiator
  4. 根据加密解密的具体情况分析加密解密的位置,然后在对应的位置添加断点

搜索

document类型的请求时候搜索

使用方法

  1. 点击chorme开发者工具右上角的三个点图标
  2. 选择’Search’
  3. 注意这里使用的是全局的搜索,开发者工具栏左侧的搜索框是局部搜索,只能搜索当前network中的文件

搜索技巧

  1. 目标参数名,例如: total
    1. 给参数名加上双引号, "total"
    2. 给参数名加上单引号, 'total'
    3. 给参数名加上冒号, total:
    4. 加上点,.total
  2. 头部中的参数
    1. 勾选Network-Headers-Response Headers-Raw,不然显示的参数可能和原始参数的大小写存在区别,导致我们搜不到或者hook不到
    2. header[
  3. 加密库
    1. encrypt
    2. AES
    3. Utf8.parse
  4. 解密库
    1. decrypt

js基础

字符串转字典

当接收的数据打印是字典,但实际是字符串,就可以通过json库对其进行转换

可以使用type()查看数据类型

1
2
response = requests.get(url).text
response = json.loads(response)

格式化输出字典

1
pprint.pprint(response)

无限debugger

简单debugger案例

在加载网页时,浏览器运行script会触发debugger

clearInterval()

1
2
3
y=setInterval(function(){
debugger;
},3000)

遇到这种情况可以使用clearInterval(),清除这个定时器

image-20240701101558179

这里的5为interval的编号

一律不在此处暂停

image-20240630155905664

右键点击debugger所在行的左侧,然后点击Never pause here(一律不在此处暂停)

增加条件断点

右键点击debugger所在行的左侧,然后点击Add conditional breakpoint(添加条件断点)

image-20240630160350071

不一定要写false,只要语句等于false都可以

停用断点

image-20240630160616670

大家都别玩了img

构造器

1
2
3
4
5
6
7
8
9
10
11
(() => {
Function.prototype.__constructor = Function;
Function = function () {
if (arguments && typeof arguments[0] === "string") {
if ("debugger" === arguments[0]) {
return;
}
}
return Function.apply(this, arguments);
}
})();

eval

1
2
3
4
5
6
7
8
eval_a = eval
eval = function(a) {
if (a === '(function() {var a = new Date(); debugger; return new Date() - a > 100;}())') {
return null;
} else {
return eval_a(a);
}
}

控制台状态检测

使用debugger卡控制台

原理

1
2
3
4
5
6
7
<script>
function check() {
debugger;
setTimeout(check, 1000);
}
check();
</script>

解决办法

方法一:右键点击debugger所在行的左侧,然后点击Never pause here(一律不在此处暂停)
方法一:hook掉这个检测函数

1
2
3
4
5
(function(){
check=function(){
console.log("前端检测开发者工具是否打开");
};
})();

格式化检测

  1. 检测代码中是否出现\n,因为在格式化代码时,会在代码中添加许多换行。
  2. 检测代码行数,代码格式化之后行数可能会增加

浏览器高度差检测

可以让控制台从页面分离出来

js原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取浏览器的外部高度
const browerOuterHeight = window.outerHeight;
// 获取浏览器的内部高度
const browerInnerHeight = window.innerHeight;
// 取浏览器的外部宽度
const browerOuterWidth = window.outerWidth;
// 获取浏览器的内部宽度
const browerInnerWidth = window.innerWidth;

// 定义高度阈值
const heightThreshold = 150;
// 定义宽度阈值
const widthThreshold = 150;

// 如果外部高度与内部高度的差值大于高度阈值,或者外部宽度与内部宽度的差值大于宽度阈值
if (browerOuterHeight - browerInnerHeight > heightThreshold || browerOuterWidth - browerInnerWidth > widthThreshold) {
// 输出提示信息
console.log('This is a browser with toolbars');
// 重定向到百度
window.location.href = "https://www.baidu.com";
}

运行时间检测

1
2
3
4
5
6
7
8
9
10
11
ort = Date.now()

function a() {
debugger;
current_time = Date.now()
if ((current_time - org) > 2000) {
console.log('检测超时');
} else {
console.log('检测正常');
}
}

禁止控制台输出

原理

1
setInterval('console.clear()', 1000)

解决办法

console—>setting—>Preserve log

原理

console.log被置空

1
console.log = function(){}

解决办法

在网页加载时就用一个变量备份住console.log

宇哥过9ku.com的调试思路

9ku.com
这个网站打开控制台后会跳转到其他页面

  1. 尝试禁用全部断点—》不管用
  2. 搜devtools,搜到了(浏览者开发工具中设置网络为低速3G),但是由于发生页面跳转,结果很快就过去了
  3. 找这个网站最后一个请求,截取url里面的部分字符’aaa’,打一个xhr断点: 没断住
  4. 再找请求,Payload里有个参数key包含”debugger”,十分可疑,再打个xhr断点’key’: 断住了
  5. 有混淆,从断住的地方往下走,边走边搜’devtools’,’console’,没有发现
  6. 重新跟栈,找到跳转的地方
  7. 本地替换代码

反调试

https://www.aqistudy.cn/

image-20240630164215563

这种一般是检测网页窗口,只需要给调试窗口独立出来就可以了

image-20240630164534892

hook

想通过hook查找的变量需要是全局变量

浏览器手动注入

监听cookie中的‘__dfp’的设置

清空浏览器数据

避免跳过加载我们想要的参数

chrome—>F12—>Application—>Clear site data

再将相关全局变量设置为空(这需要后面跟栈时发现)

1
window.name = ''

或者可以开一个无痕窗口

在网页加载时断住

有以下几种方法

  1. 找到第一个js文件的开始,设置一个断点
  2. Event Listener Breakpoints—>Load—>load(第一个文件运行断点,比方法1的断点位置落后一点点)

在控制台运行脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(function () {
'use strict';
let cookieTemp = '';
Object.defineProperty(document, 'cookie', {
set: function (val) {
//监听cookie中的‘__dfp’的设置
if (val.indexOf('__dfp') !== -1) {
debugger;
}
console.log('Hook catch cookie:', val);
cookieTemp = val;
return val;
},
get: function () {
return cookieTemp;
}
})
})();

跟栈

刷新网页,然后运行到脚本debugger处,发现目标参数被赋值,就可以跟栈了。如果是服务器赋的值,就可以在network中搜索这个值,找到返回这个值的请求

油猴脚本注入

  1. 新建脚本,直接给上面的代码复制到这里面
  2. 设置脚本运行的网页// @match *://www.iqiyi.com/*,设置运行脚本的时间setting->General->Run at->document-start

fiddler

编程猫插件(须自行安装)—>注入hook—>填入代码—>删掉地址栏内容—>勾选开启

常用hook代码

JSON.parse

JSON.parse()方法用于将一个JSON字符串转换为对象,在某些站点的加密过程中可能会遇到,以下代码演示了遇到JSON.parse()时,则插入断点:

1
2
3
4
5
6
7
8
(function () {
var parse = JSON.parse;
JSON.parse = function (params) {
console.log("Hook JSON.parse-->", params);
debugger;
return parse(params);
}
})();

JSON.stringify

JSON.stringify()方法用于将JavaScript值转换为JSON字符串,在某些站点的加密过程中可能会遇到,以下代码演示了遇到JSON.stringify()时,则插入断点:

1
2
3
4
5
6
7
8
(function () {
var stringify = JSON.stringify;
JSON.stringify = function (params) {
console.log("Hook JSON.stringify-->", params);
debugger;
return stringify(params);
}
})();

eval

JavaScript eval()函数的作用是计算JavaScript字符串,并把它作为脚本代码来执行。如果参数是一个表达式,eval()函数将执行表达式。如果参数是Javascript语句,eval()将执行javascript语句,经常被用来动态执行JS。以下代码执行后,之后所有的eval()操作都会在控制台打印输出将要执行的JS源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function () {
//保留原始方法
window.__cr_eval = window.eval;
// 重写 eval 方法
var myeval = function (src) {
console.log(src);
console.log("==================eval end=======================");
debugger;
return window.__cr_eval(src);
}
//屏蔽JS中对原生函数native 属性的检测
var _myeval = myeval().bind(null);
_myeval.toString = window.__cr_eval.toString;
Object.defineProperty(window, 'eval', {
value: _myeval
})
})();
1
2
3
4
5
6
7
8
(function () {
var parse_ = JSON.parse;
JSON.parse = function (jp) {
console.log("您猜怎么着?断住了! --> ", jp);
debugger;
return parse_(jp); // 不改变原来的执行逻辑
}
})();

寻找加密入口

搜索

  1. 参数名
  2. 关键字
    1. 发送加密:
      1. encrypt
    2. 接收数据:
      1. decrypt
      2. JSON.parse
      3. success:
  3. 部分文件路径名

搜索文件路径名方法

应对document类型的请求,无法使用跟栈

测试

通过搜索标签的属性(例如id),来找到操作此块标签的函数

定位标签

搜索id

搜索id

添加断点

安排断点

代码,启动!!!

image-20240801162410185

一目了然,找到加密函数

跟栈调试

跟栈调试原理

xhr类型的数据包适合使用跟栈方法

image-20240801154047622

在发包的时候断住,然后就可以选择向前或者向后找

同步调试

  1. 发送数据加密xhr
    1. 在发包处打断点
    2. 通过调用栈观察调用关系
  2. 接收数据解密
    1. 接收数据处打断点:逐语句运行F11
    2. 网页渲染出打DOM断点:观察调用栈的关系

混淆

jsfuck

jsfuck

image-20240822095750128

解决办法

HOOK eval, 变量a就是原代码

1
2
3
eval = function (a) {
debugger;
}

AAencode和JJ混淆

加密工具网址

解决办法

  1. 一般最后一个括号()内是自运行函数,所以只要将最后一个括号和括号里的内容替换成.toString(),即可看到目的代码

  2. HOOK Function

    1
    2
    3
    Function.prototype.constructor = function (params) {
    debugger;
    }

webpack

原理

Webpack 是一个模块打包工具,它可以将多个模块(如 JavaScript、CSS、图片等)打包成一个或多个文件,以便在浏览器中高效加载。通过分析模块间的依赖关系,Webpack 能够优化资源加载,提升网页性能。

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
// 立即执行函数表达式 (IIFE),用于创建一个独立的作用域
!function (t) {
// 定义一个函数 e,用于加载模块
function e(s) {
// 如果模块已经加载,直接返回模块的 exports
if (i[s]) return i[s].exports;
// 创建一个新的模块对象,包含 exports、id 和 loaded 属性
var n = i[s] = {
exports: {}, // 模块的导出内容
id: s, // 模块的唯一标识符
loaded: 1 // 模块的加载状态
};

// 调用模块的工厂函数,传入模块对象、模块的 exports 和加载函数 e
return t[s].call(n.exports, n, n.exports, e),
n.loaded = !0, // 设置模块的加载状态为已完成
n.exports // 返回模块的 exports
}

// 定义一个对象 i,用于存储已加载的模块
var i = {};

// 加载模块 'a'
e('a');

// 模块定义对象,包含两个模块 'a' 和 'b'
}({
a: function () {
console.log('Function 0')
},
b: function () {
console.log('Function 1')
}
})

解决办法

  1. 找到所有webpack文件,复制到一个文件内找到加载器,导出加载器
  2. 删除初始化代码,通过加载器调用加解密函数
  3. 确保在浏览器中运行成功
  4. 确保Node.js中运行成功
  5. 精简代码—修改加载器输出调用代码,替换源函数

基础案例

01天气

利用api获取城市天气

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import json
import pprint

url = "http://api.openweathermap.org/data/2.5/weather?q=wuhan&mode=json&units=metric&lang=zh_cn&APPID=83b30d525bff7db3e396e99abc62bd75"
response = requests.get(url).text
response = json.loads(response)

result = {}
result["city"] = response["name"]
result["temp"] = response["main"]["temp"]
result["temp_max"] = response["main"]["temp_max"]
result["temp_min"] = response["main"]["temp_min"]
result["weather"] = response["weather"][0]["description"]
for i in result:
print(result[i])

爬虫基础

urllib

使用urllib爬取百度的页面

1
2
3
4
5
6
7
8
9
10
11
import urllib.request

url = "http://www.baidu.com"
reponse = urllib.request.urlopen(url=url)

content = reponse.read()
with open(file='./1.txt', mode='w') as fp:
fp.write(content)


# TypeError: write() argument must be str, not bytes

当我试图将content保存到txt文件中,发生了报错。

这里我们得到的content是二进制文件,我们需要将其转化为我们能看懂的格式,也就是进行编码一下。

fp.write(content.decode('utf-8'))

百度页面就成功写入到我们的1.txt文件中了。

response的类型和方法

resposne = urllib.requests.urlopen(url=url)

resposne的类型:<class 'http.client.HTTPResponse'>

方法

  • read默认读取全部,传递数字的话就是指定读取几个字节
  • readline
  • readlines
  • getcode获取状态码
  • geturl获取url
  • getheaders获取请求头

urllib下载(urlretrieve)

urlretrieve(第一参数是文件的网络地址,第二个是本地地址)

下载百度源码

1
2
3
4
import urllib.request

url = "http://www.baidu.com"
response = urllib.request.urlretrieve(url=url, filename='baidu.html')

urlretrive也可用于下载图片和视频等网络资源

1
2
3
4
5
6
7
8
import urllib.request

img_url = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F202007%2F01%2F20200701114519_umgxc.png&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1677571326&t=f80dcf2ac99b58b32bb6e834691a2576"
video_url = "https://vd4.bdstatic.com/mda-ngw9spwz9353bm7c/360p/h264/1659250469860282338/mda-ngw9spwz9353bm7c.mp4?v_from_s=hkapp-haokan-suzhou&auth_key=1674981205-0-0-9b8ff1b4f8accbfc3835e1fda6de811f&bcevod_channel=searchbox_feed&pd=1&cd=0&pt=3&logid=0205360586&vid=131734394297378935&abtest=&klogid=0205360586"

urllib.request.urlretrieve(img_url, 'lisa.jpg')
urllib.request.urlretrieve(video_url, '搞笑.mp4')

quote

用于将字符串转化为unicode编码

使用场景:在我们进行百度的时候要使用unicode编码,而如果我们直接搜索https://www.baidu.com/s?wd=周杰伦,当然这样在浏览器中是被允许的,可是在爬虫的工作中,却无法访问到页面,我们需要将”周杰伦”转换成unicode编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#百度搜索
import urllib.request
import urllib.parse

url = "https://www.baidu.com/s?wd="
name = input('请输入你想搜索的内容:')
name = urllib.parse.quote(name)
url = url + name
headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}

request = urllib.request.Request(url=url, headers=headers)
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')

print(content)

urlencode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import urllib.parse
import urllib.request

data = {'wd': '周杰伦', 'sex': '男', 'location': '台湾'}

data = urllib.parse.urlencode(data)
headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}
url = "https://www.baidu.com/s?" + data
request = urllib.request.Request(url=url, headers=headers)
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')
print(content)

案例:百度翻译

百度翻译和上面的搜索都使用了urlencode,而urlcode将字典转换成了字符串,搜索是将内容放在url里面,而百度翻译则是使用post传data。所以二者所需data的类型也不同,前者不需要编码,而后者需要编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import urllib.request
import urllib.parse
import json

url = 'https://fanyi.baidu.com/sug'
headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}
something = input('请输入你想翻译的内容:')

data = {'kw': something}
data = urllib.parse.urlencode(data).encode('utf-8')
#urllib.request.Request里的data需要字节码,所以需要将上面的字符串进行编码处理即.encode('utf-8')
request = urllib.request.Request(url=url, data=data, headers=headers)
response = urllib.request.urlopen(request).read().decode('utf-8')

obj = json.loads(response)
print(obj)

案例:豆瓣电影前十页分页存储-url+ajax

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
def create_requests(page):
"""create_requests 对传入的页号所对应的页面进行爬取并返回一个该页面的response

_extended_summary_

Args:
page (int): 页号

Returns:
json: 页面的json文件
"""
url = 'https://movie.douban.com/j/chart/top_list?type=24&interval_id=100%3A90&action=&'
data = {'start': (page - 1) * 20, 'end': page * 20}
headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}
import urllib.parse
import urllib.request
data = urllib.parse.urlencode(data)
url += data
request = urllib.request.Request(url=url, headers=headers)
response = urllib.request.urlopen(request).read().decode('utf-8')
return response


def download(response, page):
"""download 将页面的json数据存储到对应文件

_extended_summary_

Args:
response (str): 页面数据
page (int): 用于修改对应文件名

"""
with open('豆瓣电影' + str(page) + '.json', 'w', encoding='utf-8') as fp:
fp.write(response)


if __name__ == '__main__':
start_page = int(input('请输入起始页码:'))
end_page = int(input('请输入终止页码:'))
for page in range(start_page, end_page + 1):
response = create_requests(page)
download(response, page)

案例:kfc餐厅地址-post+ajax

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
def create_requests(page):
"""create_requests 对传入的页号所对应的页面进行爬取并返回一个该页面的response

_extended_summary_

Args:
page (int): 页号

Returns:
json: 页面的json文件
"""
url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=cname'
data = {
'cname': '北京',
'pid': '',
'pageIndex': '2',
'pageSize': '10',
}
headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}
import urllib.parse
import urllib.request
data = urllib.parse.urlencode(data).encode('utf-8')
request = urllib.request.Request(url=url, data=data, headers=headers)
response = urllib.request.urlopen(request).read().decode('utf-8')
return response


def download(response, page):
"""download 将页面的json数据存储到对应文件

_extended_summary_

Args:
response (str): 页面数据
page (int): 用于修改对应文件名

"""
with open('kfc_vme50_' + str(page) + '.json', 'w', encoding='utf-8') as fp:
fp.write(response)


if __name__ == '__main__':
start_page = int(input('请输入起始页码:'))
end_page = int(input('请输入终止页码:'))
for page in range(start_page, end_page + 1):
response = create_requests(page)
download(response, page)

URLError,HTTPError

  1. HTTPError类是URLError类的子类
  2. 导入的包urllib.error.HTTPError urllib.error.URLError
  3. http错误:http错误是针对浏览器无法连接到服务器而增加出来的错误提示。引导并告诉浏览者该页是哪里出了问题。
  4. 通过urllib发送请求的时候,有可能会发送失败,这个时候如果想让你的代码更加的健壮,可以通过try-except进行捕获异常,异常有两类,URLError\HTTPError
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import urllib.request
import urllib.error

headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}
try:
#主机号都错了,URLError
url1 = "https://shabinumberone.com/1123"
request = urllib.request.Request(url=url1, headers=headers)
reponse = urllib.request.urlopen(request).read().decode('utf-8')
except urllib.error.HTTPError:
print('起飞失败')
except urllib.error.URLError:
print('系统正在升级!!!')

try:
#主机号对的,只是文件错了 HTTPError
url2 = "https://www.bilibili.com/video/BV1eA411C7uiii"
request = urllib.request.Request(url=url2, headers=headers)
reponse = urllib.request.urlopen(request).read().decode('utf-8')
except urllib.error.HTTPError:
print('起飞失败')
except urllib.error.URLError:
print('系统正在升级!!!')

Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#需求 使用handler访问百度,并保存源码

import urllib.request

url = 'http://www.baidu.com'
headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}
request = urllib.request.Request(url=url, headers=headers)

#1、获取handler对象
handler = urllib.request.HTTPHandler()

#2、获取opener对象
opener = urllib.request.build_opener(handler)

#3、调用open方法
response = opener.open(request)
content = response.read().decode('utf-8')

print(content)

xpath

获取百度一下的文字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 使用xpath获取百度页面的“百度一下文字”
from lxml import etree
import urllib.request

url = 'http://www.baidu.com'
headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}

request = urllib.request.Request(url=url, headers=headers)
reponse = urllib.request.urlopen(request)
content = reponse.read().decode('utf-8')

tree = etree.HTML(content)
result = tree.xpath('//input[@id="su"]/@value')[0]
#'//'代表子孙节点,上面命令代表搜索所有id为su的子孙input节点的value值
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import urllib.request
from lxml import etree
import time
#需求:下载前十页的图片
#(1)请求对象的定制
#(2)获取网页源码
#(3)下载

#总结:
# 1、有的图片的url后面还有冗余信息没有处理,所以导致有的下载下来的图片无法显示
# 2、避免爬虫测试网站阻止访问,所以在访问每一页后都sleep了1s


def create_request(page):
if (page == 1):
url = 'https://ssr1.scrape.center'
else:
url = 'https://ssr1.scrape.center/page/' + str(page)
headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}
request = urllib.request.Request(url=url, headers=headers)
reponse = urllib.request.urlopen(request)
content = reponse.read().decode('utf-8')
return content


def get_img(content):
tree = etree.HTML(content) # type: ignore
img_list = tree.xpath('//*[@id="index"]/div[1]/div[1]/div//img/@src')
name_list = tree.xpath('//*[@id="index"]/div[1]/div[1]/div//h2/text()')
for i in range(len(name_list)):
name = name_list[i]
src = img_list[i]
url = 'ht' + src[2:]
import os
if (os.path.exists('./movie_img') == False):
os.mkdir('./movie_img')
urllib.request.urlretrieve(url=url,
filename='./movie_img/' + name + '.jpg')
# print(name + "||||" + url)


if __name__ == '__main__':
start_page = int(input("请输入第一页:"))
end_page = int(input("请输入最后一页:"))
for page in range(start_page, end_page + 1):
print('正在下载第' + str(page) + '页')
content = create_request(page)
get_img(content)
print('Done!!!')
time.sleep(1)

jsonpath

jsonpath教程

jsonpath读取网站电影名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import urllib.request
import jsonpath
import json

headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
}
url = 'https://spa1.scrape.center/api/movie/?limit=10&offset=0'
request = urllib.request.Request(url=url, headers=headers)
reponse = urllib.request.urlopen(request)

obj = json.load(reponse)
content = jsonpath.jsonpath(obj, '$..name')
print(content)

BeautifulSoup

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
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('./.vscode/19bs4.html', encoding='utf-8'), 'lxml')

#根据标签查找节点
#1.找到第一个a标签
# print(soup.a)
#2.获取标签的属性值
# print(soup.a.attrs)

# bs4的一些函数
# 1.find 返回第一个符合条件的节点
# print(soup.find('a'))
# print(soup.find('a', id='a2'))
#注意:当查询class属性值的时候,因为和python关键字重合,所以要写成class_
# print(soup.find('a', class_=''))

#2.find_all
# print(soup.find_all('a'))
# print(soup.find_all(['a', 'span']))
#limit作用是限制查询前几个
# print(soup.find_all('li', limit=2))

#3.select
# select方法返回的是一个列表,并且会返回多个数据
# select可以使用类选择器,'.'代表class,'#'代表id
# print(soup.select('#a1'))
#属性选择器
# print(soup.select('a[id]')) #查询所有有id的a标签
# print(soup.select('a[id="a1"]')) #查询id为a1的a标签
#找到所有的a标签和li标签
# print(soup.select('a,li'))
#层级选择器
#--后代选择器
# print(soup.select('div li'))
#--子代选择器
# print(soup.select('div > ul > li'))

# 节点信息
#如果标签对象中,只有内容,那么string和get_text()都可以使用
#如果标签对象中,出了内容还有标签,那么string就获取不到数据 而get_text()是可以获取到数据
# obj = soup.select('#d1')[0]
# print(obj.string)
# print(obj.get_text())

#节点属性
obj = soup.select('#p1')[0]
print(obj.name) #name是标签名字
print(obj.attrs) #attrs将标签所有属性值按字典返回
#获取p的class值
print(obj.attrs.get('class'))
print(obj.get('class'))
print(obj['class'])

正则表达式

常用元字符

元字符:具有固定含义的特殊符号

  • .除换行符之外任意字符
  • \w字母或数字或下划线
  • \s任意的空白符
  • \d数字
  • \n一个换行符
  • \t一个制表符
  • ^字符串的开头
  • $字符串的结尾
  • \W非字符或数字或下划线
  • \D非数字
  • \S非空白符
  • a|b字符a或字符b
  • ()匹配括号内的表达式,也表示一个组
  • [...]匹配字符组中的字符
  • [^...]匹配除了字符组中字符的所有字符

量词

量词:控制前面的元字符出现的次数

  • *重复零次或更多次
  • +重复一次或更多次
  • ?重复零次或一次
  • {n}重复n次
  • {n,}重复n次或更多次
  • {n,m}重复n次到m次

贪婪匹配和惰性匹配

  • .*贪婪匹配
  • .*?惰性匹配

selenium

安装chromedriver和selenium

  1. 下载chromedriver.exe
  2. 将chromedriver.exe复制到chrome安装路径(chrome的exe同级目录),同时复制到python的目录里
  3. 将chrome的安装路径添加到path
  4. pip install slenium

selenium的元素定位

元素定位:自动化要做的就是模拟鼠标和键盘来操作来操作这些元素,点击、输入等等。操作这些元素前首先要找到它们,WebDriver提供很多定位元素的方法:find_element

简单上手:

1
2
3
4
5
6
7
8
from selenium import webdriver
from selenium.webdriver.common.by import By

brower = webdriver.Chrome()
url = 'https://www.baidu.com/'
brower.get(url)

button = brower.find_element(By.XPATH, '//input[@id="su"]')
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
from selenium import webdriver
from selenium.webdriver.common.by import By
import time

brower = webdriver.Chrome()
url = 'https://www.baidu.com/'
brower.get(url)

time.sleep(2)

input = brower.find_element('id', "kw")
input.send_keys('周杰伦')

time.sleep(2)

search = brower.find_element('id', "su")
search.click()

time.sleep(2)

js_bottom = 'document.documentElement.scrollTop=100000'
brower.execute_script(js_bottom)

time.sleep(2)

next_page = brower.find_element(By.XPATH, "//a[@class='n']")
next_page.click()

time.sleep(2)

brower.back()

time.sleep(2)

brower.forward()

time.sleep(3)

brower.quit()

chrome handless

这一段配置是写好的,可以直接照抄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def share_browser():
chrome_options = Options()
chrome_options .add_argument( ' --headless')
chrome_options.add_argument( ' --disable-gpu')
# path是你自己的chrome浏览器的文件路径
path = r'C:\Program Files\Google\Chrome\Application\chrome.exe'
chrome_options. binary_location = path
browser = webdriver.Chrome(chrome_options=chrome_options)
return browser
browser = share_browser()
url = 'https://www.baidu.com'
browser.get(url)

requests

response的属性以及类型

类型 models.Response r.tex
r.text 获取网站源码
r.encoding 访问或定制编码方式
r.url 获取请求的ur
v 响应的字节类型
r.status_code 响应的状态码
r.headers 响应的头信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

url = 'https://fanyi.baidu.com/sug'

headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}

data = {'kw': 'eye'}

response = requests.post(headers=headers, url=url, data=data)
content = response.text
import json

obj = json.loads(content)
print(obj)
  • post请求不需要编解码
  • post请求的参数是data

requests登陆古诗文网

  1. 首先通过输出错误的账号密码,找到登陆接口(在network里面,名字一般带login)

  2. 分析request的data

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    data = {
    '__VIEWSTATE':
    '5RPZ6LJU7KKM7C7VzCFf2pL9LmIteosVELlLLdamdHcTFpGAd6na5T0tne6tzfX6q1THIJeEE0+aUQKJg4vp8UUVXyQqyov9iuZj6ue9cwUv3hkD5alVFu7Gq+QjnGurP+b5enWBUerjStO1NnS+5BlDgjw=',
    '__VIEWSTATEGENERATOR': 'C93BE1AE',
    'from:http': '//so.gushiwen.cn/user/collect.aspx',
    'email': '2982534677@qq.com',
    'pwd': 'EXWRa6NrQq572Ufdsa',
    'code': 'U46S',
    'denglu': '登录',
    }

    可以发现VIEWSTATE,VIEWSTATEGENERATOR,code的是不固定的值,在页面源码中寻找


存储方式

1.文本文件

像是txt文件或者MP3,MP4,json等文件可以使用python的file操作函数

以requests为例子,写入字符文件时需要进行编码处理,而对字节文件则不用。
但是要注意读取response的类型的不同,一个是text,一个是content

1
2
3
4
5
6
7
8
response = requests.get('')
#response是一个txt文件
content = response.text
with open('a.txt', 'w', encoding = 'utf-8')as fp:
fp.write(context)
#response是一个字节文件,json、MP3、img
with open('a.json','wb')as fp:
fp.write(response.content)

使用pandas存储excel

1
2
3
4
5
#img_name,img_score
data = pd.DataFrame()
data['name'] = img_name
data['score'] = img_score
data.to_excel("movie.xlsx", index=False)

将数据存储到mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#连接数据库所需要的信息
mysql_local = {
'host': '127.0.0.1',
'port': 3306,
'user': 'root',
'password': '',
'db': 'data'
}
#img_name,img_grade是两个列表,data是可迭代对象zip
data = zip(img_name, img_grade)
#连接数据库
conn = pymysql.connect(**mysql_local)
cursor = conn.cursor()
#sql查询语句
sql = f'INSERT INTO film_info(film_name,score)VALUES("%s",%s)'
cursor.executemany(sql, data)
conn.commit()

scrapy

scrapy工作流程

流程

  1. 创建爬虫的项目 scrapy startproject projectname,注意:projectname不能数字开头和携带中文
  2. 创建爬虫文件
    1. projectname/spiders文件夹内去创建爬虫文件
    2. 创建爬虫文件scrapy genspider 爬虫文件的名字 要爬取的网页 eg:scrapy genspider baidu www.baidu.com
  3. 运行爬虫代码 scrapy crawl爬虫的名字
    1. 注释掉ROBOTSTXT_OBEY = True,那么我们的爬虫就不遵守robots协议了

scrapy项目结构

项目名字

  • spiders文件夹(存储的是爬虫文件)
    • init
    • 自定义的爬虫文件———scrapy项目的核心功能文件
  • init
  • items(定义数据结构,爬取的数据都包含哪些)
  • middleware(中间件-代理)
  • pipelines(管道-用来处理下载的数据)
  • settings(配置文件-robots协议-ua定义等)

response的属性和方法

  • response.text 获取的是响应的字符串
  • response.body 获取的是二进制数据
  • response.xpath 可以直接通过xpath方法来解析response中的内容
  • response.extract() 提取seletor对象的data属性值
  • response.extract_first() 提取的seletor列表的第一个数据

scrapy工作流程

  1. 引擎向spiders要url
  2. 引擎将要爬取的url给调度器
  3. 调度器会将url生成请求对象放入到指定的队列中
  4. 从队列中出列一个请求
  5. 引擎将请求交给下载器进行处理
  6. 下载器发送请求获取互联网数据
  7. 下载器将数据返回给引擎
  8. 引擎将数据再次给到spiders
  9. spiders通过xpath解析该数据,得到数据或者url
  10. spiders将数据或者url给到引擎
  11. 引擎判断该数据是url还是数据,交给管道(item pipeline)处理,是url交给调度器处理

Feed Exports

scrapy 提供的Feed Exports可以将抓取的所有内容以json,jsonlines,csv, xml, pickle, marshal等格式输出

1
2
3
4
5
6
7
8
scrapy crawl quotes -o quotes.json
scrapy crawl quotes -o quotes.js
scrapy crawl quotes -o quotes.jsonlines
scrapy crawl quotes -o quotes.csv
scrapy crawl quotes -o quotes.xml
scrapy crawl quotes -o quotes.pickle
scrapy crawl quotes -o quotes.marshal
scrapy crawl quotes -o ftp://user:pass@ftp.example.com/path/to/quotes.csv

scrapy案例:quotes

quotes是scrapy的官方示例案例。
网址

每个单位有三个数据:text,author,tags

1、新建项目

scrapy startproject scrapytutorial 新建项目

scrapy genspider quotes quotes.toscrape.com 创建爬虫文件

注释ROBOTSTEXT,不遵守robots协议

2、quotes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import scrapy
from scrapytutorial.items import ScrapytutorialItem

class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ["https://quotes.toscrape.com"]

def parse(self, response):
print("=" * 100)
quotes = response.xpath("//div[@class='quote']")
for quote in quotes:
# 取出text并将text的双引号去掉
text = quote.xpath('span[@class="text"]/text()').extract_first().replace('"', '')
author = quote.xpath('span/small[@class="author"]/text()').extract_first()
tags = quote.xpath('div[@class="tags"]/a/text()').extract()
# 把item传给engine,然后engine把它传给item pipelines
item = ScrapytutorialItem(text=text, author=author, tags=tags)
yield item
next = response.xpath('//li[@class="next"]/a/@href').extract_first()
url = response.urljoin(next)
yield scrapy.Request(url=url, callback=self.parse)
3、items.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy

class ScrapytutorialItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
text = scrapy.Field()
author = scrapy.Field()
tags = scrapy.Field()

4、保存数据

保存数据到一个json文件中

1
scrapy crawl quotes -o quotes.json

每一个item输出到一行json文件

1
2
3
scrapy crawl quotes -o quotes.jsonlines
# 下面是上面的简写
scrapy crawl quotes -o quotes.jl

上面都是保存到一个文件内,第一种是一个[]在外面,里面的每个数据用{}包住,{}之间有’,’分割。下面的方法没有[]和’,’。

5、保存到mongodb
settings.py
1
2
3
4
5
6
7
8
9
# 声明管道和管道的优先级
ITEM_PIPELINES = {
'scrapytutorial.pipelines.TextPipeline': 300,
'scrapytutorial.pipelines.MongoDBPipeline': 400,
}
# mongo数据库
MONGODB_CONNECTION_STRING = 'localhost'
# 数据库名
MONGODB_DATABASE = 'scrapytutorial'

CrawlSpider

  1. 继承自scrapy.Spider

  2. 独门秘笈
    CrawlSpider可以定义规则,再解析html内容的时候,可以根据链接规则提取出指定的链接,然后再向这些链接发送请求
    所以,如果有需要跟进链接的需求,意思就是爬取了网页之后,需要提取链接再次爬取,使用crawlSpider是非常合适的

  3. 提取链接
    链接提取器,在这里就可以写规则提取指定链接

    1
    2
    3
    4
    5
    6
    7
    scrapy.linkextractors.LinkExtractor( 
    allow=(),#正则表达式 提取符合正则的链接
    deny=(),#(不用)正则表达式 不提取符合正则的链接
    allow_domains=(),#(不用)允许的域名
    deny_domains=(),#(不用)不允许的域名
    restrict_xpaths=(),#xpath,提取符合xpath规则的链接
    restrict_css=()#提取符合选择器规则的链接)
  1. 模拟使用
    正则用法:links1 = LinkExtractor(allow=r'list_23_\d+\.html')
    xpath用法:links2 = LinkExtractor(restrict_xpaths=r'//div[@class="x"]')

    css用法:links3 = LinkExtractor(restrict_css='.x')

  2. 提取连接
    link.extract_links(response)

CrawlSpider案例

需求:读书网数据入库

  1. 创建项目:crapy startproject dushuproject
  2. 跳转到spiders路径 cd\dushuproject\dushuproject\spiders
  3. 创建爬虫类:scrapy genspider -t crawl read www.dushu.com
  4. items
  5. spiders
  6. settings
  7. pipelines
    数据保存到本地
    数据保存到mysq1数据库

数据入库

  1. settings配置参数:

    1
    2
    3
    4
    5
    6
    DB_HOST = '192.168.231.128' 
    DB_PORT = 3306
    DB_USER = 'root'
    DB_PASSWORD = '1234'
    DB_NAME = 'test'
    DB_CHARSET = 'utf8'
  1. 管道配置

    1
    2
    3
    from scrapy.utils.project import get_project_settings import pymysql
    class MysqlPipeline(object):
    #_init_方法和open_spider的作用是一样的