爬虫

第一章 :爬虫基础简介

什么是爬虫:通过编写程序,模拟浏览器上网,然后使其去互联网上抓取数据的过程。

爬虫在使用场景中的分类:

  • 通用爬虫:抓取系统重要组成部分。抓取的是一整张页面数据。
  • 聚焦爬虫:是建立在通用爬虫的基础之上。抓取的是页面中特定的局部内容。
  • 增量式爬虫:检测网站中数据更新的情况。只抓取网站中最新更新出来的数据。

反爬机制:门户网站可以通过制定相应的策略或技术手段,防止爬虫程序进行网站数据的爬取。

反反爬虫策略:爬虫程序可以通过制定相应的策略或技术手段,破解门户网站中具有的反爬虫机制,从而获取门户网站的数据。

robots.txt协议:规定了网站中哪些数据允许爬取以及哪些数据不允许爬取。

http协议:HyperText Transfer Protocol,超文本传输协议。

http协议的特点(重点):

  • 应用层协议。(最顶层也是和用户交互的层)
  • 无连接:http协议每次发送请求都是独立的。http 1.1以后有一个头:connection:keep_alive。
  • 无状态:http协议不记录状态,进而产生了两种记录http状态的技术:cookie和session。

常用请求头信息:

  • User-Agent:请求载体的身份标识。
  • Connection:请求完毕后,是断开连接还是保持连接。
  • cookie:请求的状态信息。
  • Referer:表示产生请求的网页来源于哪里。
  • accept:允许传入的文件类型。

常用响应头信息:

  • Content-Type:服务器响应给客户端的数据类型。

https协议:HTTPS (Hypertext Transfer Protocol over Secure Socket Layer)简单讲是http的安全版,在http下加入SSL层。

SSL(Secure Sockets Layer 安全套接层)主要用于Web的安全传输协议,在传输层对网络连接进行加密,保障在Internet上数据传输的安全。

数据加密方式:

  • 对称密钥加密
  • 非对称密钥加密
  • 证书密钥加密

第二章:requests模块基础

requests安装:pip install requests

python中基于网络请求的模块:

  • urllib模块
  • requests模块

requests模块介绍:python中原生的一款基于网络请求的模块,功能非常强大,简单便捷,效率极高。

  • 作用:模拟浏览器给服务器端发送请求。
  • 使用(requests模块的编码流程):
    • 指定url(UA伪装、请求参数处理)
    • 发送请求
    • 获取响应数据
    • 持久化存储

requests常用方法和属性:

  • response.encoding:获取页面响应数据的编码格式。(response.encoding = ‘utf-8’:设置编码格式)
  • response.status_code:响应状态码。
  • response.headers:响应头信息。

User-Agent检测:门户网站会检测对应请求的载体身份标识,如果检测到请求的载体身份标识不是基于某一款浏览器,则服务器端可能拒绝该次请求。

User-Agent伪装:让爬虫程序对应的请求载体伪装成某一刻浏览器。

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

requests发送get请求:requests.get(url=url, params=param, headers=headers)

发送get请求:获取请求的网页HTML文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests

if __name__ == '__main__':
# 指定url
url = 'https://www.sogou.com/web'
kw = input('enter a word:')
# 对参数进行处理,封装到字典
param = {
'query': kw
}
# User-Agent伪装
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55'
}
# 发送get请求
resp = requests.get(url=url, params=param, headers=headers)
# 打印请求地址
print(resp.url)
page_text = resp.text
# 持久化存储
with open(kw + '.html', 'w', encoding='utf-8') as f:
f.write(page_text)

requests发送post请求:requests.post(url=url, data=data, headers=headers)

发送post请求:爬取百度翻译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import json

if __name__ == '__main__':
url = 'https://fanyi.baidu.com/sug'
kw = input("enter a word:")
# post请求参数处理
data = {
'kw': kw
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55'
}
# 发送post请求
resp = requests.post(url=url, data=data, headers=headers)
# 获取json响应数据(必须确认响应数据是json类型,才可以使用json方法)
baidu_json = resp.json()
# 持久化存储(json数据)
f = open('./'+kw+".json", 'w', encoding='utf-8')
json.dump(baidu_json, fp=f, ensure_ascii=False)

看网站发送请求的方式:看地址信息有没有改变再抓包

  • 整张页面
  • ajax局部刷新/动态加载数据

看网站响应的数据格式:Content-Type

  • txt/html或text/plain(字符串):txt属性
  • application/json(对象):json方法
  • 图片/音频等(二进制):content属性

持久化存储json数据:json.dump(json数据, fp=文件, ensure_ascii=False)

综合练习:爬取药监总局详情数据

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
import requests

if __name__ == '__main__':
# http://scxk.nmpa.gov.cn:81/xk/
# 批量获取不同企业的id值
url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsList'
page = input("请输入爬取的页数:")
data = {
'on': 'true',
'page': page,
'pageSize': '15',
'productName': '',
'conditionType': '1',
'applyname': '',
'applysn': ''
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55'
}
resp = requests.post(url=url, data=data, headers=headers)
json_ids = resp.json()
# 从json数据中取出id值存放到列表中
id_list = []
for dic in json_ids['list']:
id_list.append(dic['ID'])
print(id_list)

# 通过获取的id值拼接成url请求企业详情数据
url = 'http://scxk.nmpa.gov.cn:81/xk/itownet/portalAction.do?method=getXkzsById'
# 存储详情数据
all_detail_data = []
# 将每个id放入到参数并发送post请求
for i in id_list:
data = {
'id': i
}
resp = requests.post(url=url, data=data, headers=headers)
detail_json = resp.json()
print(detail_json)
all_detail_data.append(detail_json)

第三章:数据解析

聚焦爬虫:爬取页面中指定的页面内容。

编码流程:1、指定url 2、发起请求 3、获取响应数据 4、数据解析 5、持久化存储

数据解析分类:1、正则表达式 2、bs4 3、xpath

数据解析原理概念:在网页的标签之间或者标签对应的属性中解析局部的文本内容进行存储。

  • 进行指定标签的定位。
  • 提取标签中或标签对应的属性中的数据。

正则表达式数据解析

正则表达式数据解析步骤:

  • import re
  • re.findall(正则表达式, 文本数据, re.S):返回符合条件数据的列表

正则表达式练习:分页爬取图片数据

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
import requests
import re
import os

if __name__ == '__main__':
# 创建图片文件
if not os.path.exists('./imgLibs'):
os.mkdir('./imgLibs')
# 定义存放图片地址的列表
img_url_list = []
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55'
}
url = 'https://pic.netbian.com/4kdongman/index_%d.html'
for pageNum in range(2, 5):
print('正在爬取第' + str(pageNum) + '页图片')
new_url = format(url % pageNum)
resp = requests.get(url=new_url, headers=headers)
# 给响应数据设置编码格式
resp.encoding = 'gbk'
# 通用处理中文乱码解决方案
# str.encode('iso-8859-1').decode('gbk')
page_text = resp.text
# 正则表达式解析页面的图片地址
ex = '.*?.html" target="_blank"><img src="(.*?)" alt=".*?'
img_url_list = re.findall(ex, page_text, re.S)
for img_url in img_url_list:
# 拼接完整图片地址
img_url = 'https://pic.netbian.com' + img_url
# 发送get请求获取图片
img_data = requests.get(url=img_url, headers=headers).content
# 截取图片名称
img_name = img_url.split('/')[-1]
# 图片存储路径
img_path = './imgLibs/' + img_name
# 持久化存储图片
with open(img_path, 'wb') as f:
f.write(img_data)
print(img_name, '下载成功!!')

bs4数据解析

bs4数据解析(css选择器):

  • 实例化一个BeautifulSoup对象,并且将页面源码数据加载到该对象中
  • 通过调用BeautifulSoup对象中相关的属性或方法进行标签定位和数据提取

环境安装:

  • pip install bs4
  • pip install lxml:解析器

实例化BeautifulSoup对象:

  • from bs4 import BeautifulSoup
  • soup = BeautifulSoup(html文件数据, ‘lxml’)

BeautifulSoup对象中的方法和属性(css选择器):

  • soup.tagName/soup.find(‘tagName’):返回页面第一次出现的该标签
  • soup.find(‘tagName’,class_=’value’):返回页面中有class=’value’的该标签
  • soup.find_all(‘tagName’):返回页面中所有该标签的一个列表
  • soup.select(‘css选择器’):返回符合要求的所有标签的一个列表

获取标签之间的文本数据:

  • soup.tagName.text/get_text():返回标签之间的所有文本数据
  • soup.tagName.string:只可以返回该标签下面直系的文本内容

获取标签的属性值:

  • soup.tageName[‘attributeName’]:获取标签指定的属性值

bs4练习:爬取三国演义小说

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

if __name__ == '__main__':
url = 'https://www.shicimingju.com/book/sanguoyanyi.html'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55'
}
# 获取首页页面数据
resp = requests.get(url=url, headers=headers)
resp.encoding = 'utf-8'
page_text = resp.text
# 实例化BeautifulSoup对象,加载html文本
soup = BeautifulSoup(page_text, 'lxml')
# 获取所有小说标题文本所在的a标签
a_list = soup.select('.book-mulu > ul > li > a')
f = open('三国演义.txt', 'w', encoding='utf-8')
# 在a标签解析出章节的标题和详情页的url
for a in a_list:
# 解析出小说的章节标题
title = a.string
# 正则提取a标签的链接地址
# ex = '<a href="(.*?)">.*?</a>'
# detail_url = re.findall(ex, str(a), re.S)
# bs4提取a标签的链接地址(获取a标签中href属性值)
detail_url = a['href']
detail_url = 'https://www.shicimingju.com' + detail_url
# 请求小说内容的页面数据
resp = requests.get(url=detail_url, headers=headers)
resp.encoding = 'utf-8'
detail_page_text = resp.text
# 解析详情页的文本内容
detail_soup = BeautifulSoup(detail_page_text, 'lxml')
content = detail_soup.find('div', class_='chapter_content').text
# 持久化存储
f.write(title+":"+content+"\n")
print(title, "爬取成功")

xpath数据解析

xpath解析:最常用且最便捷高效的一种解析方式。

  • 实例化一个etree对象,并且将页面源码数据加载到该对象中。
  • 调用etree对象中的xpath方法结合着xpath表达式实现标签的定位和内容的捕获。

实例化BeautifulSoup对象:

  • from lxml import etree
  • 本地html文档中的源码数据加载到etree对象中:
    • etree.parse(filePath)
  • 互联网上获取的源码数据加载到etree对象中:
    • etree.HTML(‘page_text’)

调用xpath方法进行标签定位:

  • xpath(‘xpath表达式’):返回Element对象
  • 重点:xpath表达式——用于定位

获取标签之间的文本:

  • xpath表达式/text():返回该标签下面直系的文本内容列表
  • xpath表达式//text():返回该标签之间的所有文本内容列表

获取标签的属性值:

  • xpath表达式/@attributeName:获取标签指定的属性值

xpath练习:爬取城市名称

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
import requests
from lxml import etree

if __name__ == '__main__':
url = 'https://www.aqistudy.cn/historydata/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55'
}
# 用于存放热门城市的名称
hot_city_names = []
# 用于存放所有城市的名称
all_city_names = []
page_text = requests.get(url=url, headers=headers).text
# 加载请求的html文本
tree = etree.HTML(page_text)
# xpath解析热门城市名称
hot_city_list = tree.xpath("//div[@class='hot']//ul/li")
for li in hot_city_list:
# 获取热门城市名称
hot_city_name = li.xpath("./a/text()")
hot_city_names.append(hot_city_name[0])
print(hot_city_names, len(hot_city_names))
# xpath解析所有城市名称
city_initial_list = tree.xpath("//div[@class='all']/div[2]/ul")
for ul in city_initial_list:
city_name_list = ul.xpath("./div[2]/li")
for li in city_name_list:
# 获取城市名称
city_name = li.xpath("./a/text()")
all_city_names.append(city_name[0])
print(all_city_names, len(all_city_names))

# 通用xpath'//div[@class='hot']//ul/li/a/text() | //div[@class='all']/div[2]/ul/div[2]/li/a'

xpath/bs4结合练习:音效爬取

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
import requests
from lxml import etree
from bs4 import BeautifulSoup
import os

if __name__ == '__main__':
if not os.path.exists('./musicFile'):
os.mkdir('./musicFile')
url = 'https://sc.chinaz.com/yinxiao/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55'
}
page_text = requests.get(url=url, headers=headers).text
tree = etree.HTML(page_text)
music_List = tree.xpath("//div[@id='AudioList']//div[@class='right-head']/a")
# 只有前两个免费
for a in music_List[0], music_List[1]:
# 解析music链接
music_url = a.xpath("./@href")[0]
music_url = 'https://sc.chinaz.com' + music_url
music_page_text = requests.get(url=music_url, headers=headers).text
soup = BeautifulSoup(music_page_text, 'lxml')
# 解析音乐名称
music_name = soup.select('.play-box > div >h1')[0].string
# 设置编码格式
music_name = music_name.encode('iso-8859-1').decode('utf-8')
# 解析下载地址
mp3_url = soup.select('#mp3box > div:nth-of-type(2) > a:nth-of-type(1)')[0]['href']
mp3_url = 'https:'+mp3_url
mp3_download = requests.get(url=mp3_url, headers=headers).content
music_path = './musicFile/'+music_name+".mp3"
with open(music_path, 'wb') as f:
f.write(mp3_download)
print(music_name+'.mp3', '下载成功!')

第四章:验证码识别

门户网站反爬机制:使用验证码。使用验证码可以防止应用或者网站被恶意注册、攻击。

识别验证码操作:

  • 人工肉眼识别。(不推荐)
  • 第三方自动识别。
  • python验证码识别库

第三方超级鹰

第三方超级鹰使用流程:

使用超级鹰识别验证码:

  • 请求验证码图片地址,进行本地下载
  • 调用超级鹰接口对验证码图片进行识别
  • 使用返回的验证码识别结果

超级鹰识别验证:古诗文网验证码识别

  • 超级鹰功能代码
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
import requests
from hashlib import md5


class Chaojiying_Client(object):

def __init__(self, username, password, soft_id):
self.username = username
password = password.encode('utf8')
self.password = md5(password).hexdigest()
self.soft_id = soft_id
self.base_params = {
'user': self.username,
'pass2': self.password,
'softid': self.soft_id,
}
self.headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}

def PostPic(self, im, codetype):
"""
im: 图片字节
codetype: 题目类型 参考 http://www.chaojiying.com/price.html
"""
params = {
'codetype': codetype,
}
params.update(self.base_params)
files = {'userfile': ('ccc.jpg', im)}
r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files,
headers=self.headers)
return r.json()

def ReportError(self, im_id):
"""
im_id:报错题目的图片ID
"""
params = {
'id': im_id,
}
params.update(self.base_params)
r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
return r.json()
  • 调用超级鹰识别验证码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
from fake_useragent import UserAgent
import os
from pcstudy.验证码处理.超级鹰.chaojiying import Chaojiying_Client

if __name__ == '__main__':
if not os.path.exists('./codeImg'):
os.mkdir('./codeImg')
url = 'https://so.gushiwen.cn/RandCode.ashx'
headers = {
'User-Agent': UserAgent().chrome
}
# 获取验证码图片,存储到本地
code_img = requests.get(url=url, headers=headers).content
with open('./codeImg/code.jpg', 'wb') as f:
f.write(code_img)
# 调用超级鹰识别验证码
chaojiying = Chaojiying_Client('lrw5243', '1274604930.qq', '931400') # 用户名、密码、软件ID
im = open('./codeImg/code.jpg', 'rb').read() # 本地图片文件路径
# 打印验证码文本
print(chaojiying.PostPic(im, 1902)['pic_str']) # 1902 验证码类型

OCR技术验证码识别

OCR技术简介:光学字符识别(Optical Character Recognition, OCR)是指对文本资料的图像文件进行分析识别处理,获取文字及版面信息的过程。亦即将图像中的文字进行识别,并以文本的形式返回。

ddddocr库的使用:

ocr技术识别验证:学习通登录验证码识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
from fake_useragent import UserAgent
import os
import ddddocr

if __name__ == '__main__':
if not os.path.exists('./codeImg'):
os.mkdir('./codeImg')
url = 'https://passport2.chaoxing.com/num/code'
headers = {
'User-Agent': UserAgent().chrome
}
# 获取验证码图片,存储到本地
code_img = requests.get(url=url, headers=headers).content
with open('./codeImg/xxt.jpg', 'wb') as f:
f.write(code_img)
# 使用ddddocr识别验证码
ocr = ddddocr.DdddOcr()
with open('./codeImg/xxt.jpg', 'rb') as f:
img_bytes = f.read()
res = ocr.classification(img_bytes)
print(res)

第五章:requests模块高级

Cookie处理

cookie和session理解:

  • cookie是网站用来辨别用户身份,进行会话跟踪,存储在本地终端上的数据。
  • session在web中主要用来在服务器端存储特定用户对象会话所需要的信息。

cookie和session产生的原因:

  • http协议是一个无状态协议,在特定操作的时候,需要保存信息,进而产生了cookie和session。

cookie原理:

  • 由服务器来产生,浏览器第一次请求,服务器发送给客户端进而保存。
  • 浏览器继续访问时,就会在请求头的cookie字段上附带cookie信息,这样服务器就可以识别是谁在访问了。

cookie处理方式:

  • 手动处理:通过抓包工具获取cookie值,将该值封装到headers中。(不建议)
  • 自动处理:requests创建session会话对象,使用session对象发送请求。
    • 创建session会话对象:session = requests.session()
    • 使用session会话对象发送请求:session.post(url=url, data=data, headers=headers)

作用:使用session会话对象进行请求的发送,如果请求过程中产生了cookie,则该cookie会被自动存储,再次使用该session会话对象发送请求就会携带cookie信息。

防止登录请求重定向:浏览器调试 Network ,勾选Preserve log,可以Console 保留历史日志和网络请求

需求:模拟登录,爬取门户网站用户登录后的个人信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
from fake_useragent import UserAgent

if __name__ == '__main__':
# 创建session会话对象
session = requests.session()
url = 'https://xsgz.hufe.edu.cn/website/login'
data = {
'uname': '202108210651',
'pd_mm': 'e11c0a39f9b24b9ebb2513c199efe684'
}
headers = {
'User-Agent': UserAgent().chrome
}
# 使用session会话对象发送登录请求,此时session对象携带了cookie信息
resp = session.post(url=url, data=data, headers=headers)
print(resp.json())
welcome_url = 'https://xsgz.hufe.edu.cn/wap/main/welcome'
# 再次使用session会话对象请求个人信息主页
welcome_page_text = session.get(url=welcome_url, headers=headers).text
with open('./welcome.html', 'w', encoding='utf-8') as f:
f.write(welcome_page_text)

设置代理

代理实际上是代理服务器(proxy server),它的功能是代理网络用户获取网络信息。

IP检测:当我们用同一个IP多次频繁访问服务器时,服务器会检测到该请求可能是爬虫操作,服务器会封禁该IP,因此就不能正常的响应页面的信息了。

代理的作用:

  • 突破自身IP访问的限制。
  • 隐藏自身IP。

HTTP 代理服务器:主要用于访问网页, 一般有内容过滤和缓存功能, 端口一般为80 、8080 、3128 等。

根据代理的匿名程度分类:

  • 透明代理:服务端上知道该次请求是代理服务器,也能追踪到真实的IP。
  • 普通匿名代理: 服务端上知道该次请求是代理服务器, 但追踪不到真实的IP。
  • 高度匿名代理:可以完整的将IP变成代理服务器的IP,对方追踪不到真实的IP。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
from fake_useragent import UserAgent
from lxml import etree

if __name__ == '__main__':
url = 'http://www.baidu.com/s?wd=ip&usm=3&rsv_idx=2&rsv_page=1'
headers = {
'User-Agent': UserAgent().chrome
}
# 设置代理
proxies = {
'http': 'http://127.0.0.1',
'https': 'https://127.0.0.1'
}
resp = requests.get(url=url, headers=headers, proxies=proxies)
resp.encoding = 'utf-8'
page_text = resp.text
with open('./ip.html', 'w', encoding='utf-8') as f:
f.write(page_text)

第六章:高性能异步爬虫

目的:在爬虫中使用异步实现高性能的数据爬取操作。

使用线程池

异步爬虫方式:使用线程池Pool

默认单线程串行方式执行:用时8秒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time
# 默认单线程串行方式执行
url_list = ['http://www.baidu.com', 'http://www.baidu.com', 'http://www.baidu.com', 'http://www.baidu.com']


def get_page(url):
print("正在爬取:" + url)
time.sleep(2)
print("爬取成功!")


start_time = time.time()
for url in url_list:
get_page(url)
end_time = time.time()

print(end_time - start_time) # 8秒

使用线程池方式执行:2秒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time
# 导入线程池模块对应的类
from multiprocessing.dummy import Pool
# 使用线程池方式执行
url_list = ['http://www.baidu.com', 'http://www.baidu.com', 'http://www.baidu.com', 'http://www.baidu.com']


def get_page(url):
print("正在爬取:" + url)
time.sleep(2)
print("爬取成功!")


start_time = time.time()
# 实力化一个线程池对象,池的大小为4
pool = Pool(4)
pool.map(get_page, url_list)
end_time = time.time()
# 关闭进程池
pool.close()
# 主进程阻塞等待子进程的退出
pool.join()
print(end_time - start_time) # 2秒

爬取梨视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import time
import requests
from fake_useragent import UserAgent
from lxml import etree
from selenium import webdriver
from random import random
from multiprocessing.dummy import Pool
import os
import re


class PearVideo:
def __init__(self):
# 创建视频文件夹
if not os.path.exists('./videoSrc'):
os.mkdir('./videoSrc')
self.headers = {
'User-Agent': UserAgent().chrome
}
options = webdriver.ChromeOptions()
# 无头浏览器
options.add_argument('--headless')
self.driver = webdriver.Chrome(options=options)

def get_page_source(self, num, pear_url):
self.driver.get(pear_url)
flag = '加载更多'
sum = 9
# 通过循环加载视频
while flag == '加载更多' and sum < num:
self.driver.find_element_by_id('listLoadMore').click()
flag = self.driver.find_element_by_id('listLoadMore').get_attribute('innerHTML')
sum += 12
time.sleep(1)
# 获取页面源码
page_text = self.driver.page_source
self.driver.quit()
# 解析源码数据
tree = etree.HTML(page_text)
# 下载指定数量列表
li_list = tree.xpath('//ul[@id="categoryList"]/li')[:num]
return li_list

def get_video_url(self, li_list):
data = []
for li in li_list:
# 视频内容url
href = li.xpath('./div/a/@href')[0]
cont_id = href.split('_')[-1]
# 视频名称
video_name = li.xpath('./div/a/div[2]/text()')[0] + '.mp4'
content_url = 'https://www.pearvideo.com/videoStatus.jsp'
params = {
'contId': cont_id,
'mrd': random()
}
content_headers = {
'User-Agent': UserAgent().chrome,
'Referer': 'https://www.pearvideo.com/' + href
}
# 得到视频下载地址
content_json = requests.get(url=content_url, params=params, headers=content_headers).json()
download_url = content_json['videoInfo']['videos']['srcUrl']
# 校正下载地址
download_url = re.sub('/[0-9]+-', '/cont-{}-'.format(cont_id), download_url)
name_url = {
'name': video_name,
'url': download_url
}
data.append(name_url)
return data

def download_video(self, data):
# 下载视频
print(data['name'], '正在下载.............')
video_data = requests.get(url=data['url'], headers=self.headers).content
video_path = './videoSrc/' + data['name']
with open(video_path, 'wb') as f:
f.write(video_data)
print(data['name'], '下载成功!!')


if __name__ == '__main__':
pear_url = 'https://www.pearvideo.com/category_59'
num = int(input("输入要爬取的视频数量:"))
pear = PearVideo()
# 获取页面源码,解析出包含视频的li标签列表
li_list = pear.get_page_source(num, pear_url)
# 获取视频的名称和链接
data = pear.get_video_url(li_list)
# 创建线程池
pool = Pool(10)
# 使用线程池原则:线程池处理的是阻塞且较为耗时的操作
pool.map(pear.download_video, data)
# 关闭线程池
pool.close()
# 主进程阻塞,等待子进程的退出
pool.join()

协程异步爬虫

第六章:selenium加持

selenium模块和爬虫之间的关联:

  • 便捷的获取网站中动态加载的数据
    • driver.page_source:获取页面源码,包括JS动态加载的数据。
  • 便捷实现模拟登录
    • 实现模拟登录,爬取用户登录数据。

处理简单滑块验证码:模拟登录12306

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
from selenium import webdriver
from time import sleep
from selenium.webdriver import ActionChains
options = webdriver.ChromeOptions()

# 设置修改selenium的特征值,防止被检测到
options.add_experimental_option('excludeSwitches', ['enable-automation'])
# 隐藏selenium的自动控制功能显示防止被检测
options.add_argument("--disable-blink-features=AutomationControlled")
# 启动时最大化
options.add_argument('start-maximized')
# 不自动关闭浏览器
options.add_experimental_option('detach', True)

driver = webdriver.Chrome(options=options)

driver.get('https://kyfw.12306.cn/otn/resources/login.html')
# 进行登录操作
driver.find_element_by_id('J-userName').send_keys('17674737693')
driver.find_element_by_id('J-password').send_keys('1274604930qq')
driver.find_element_by_id('J-login').click()
sleep(1)
# 处理滑块验证码
slip_block = driver.find_element_by_id('nc_1_n1z')
action = ActionChains(driver)
# 动作链实现拖拽移动
action.click_and_hold(slip_block).move_by_offset(300, 0).perform()
sleep(1)
driver.find_element_by_xpath('//a[@class="btn btn-primary ok"]').click()
# 获取页面源码数据
print(driver.page_source)
sleep(5)
driver.quit()

处理图片验证码:模拟登录学习通

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from selenium import webdriver
from time import sleep
import ddddocr

options = webdriver.ChromeOptions()
# 设置修改selenium的特征值,防止被检测到
options.add_experimental_option('excludeSwitches', ['enable-automation'])
# 隐藏selenium的自动控制功能显示防止被检测
options.add_argument("--disable-blink-features=AutomationControlled")
# 启动时最大化
options.add_argument('start-maximized')

driver = webdriver.Chrome(options=options)

driver.get('https://passport2.chaoxing.com/login')

# 获取页面标题
title = driver.title
# 解决验证识别容错率
while title == '用户登录':
# 进行登录操作
driver.find_element_by_id('unameId').send_keys('19373140107')
driver.find_element_by_id('passwordId').send_keys('1274604930.qq')
# 定位验证码图片,进行截图
code = driver.find_element_by_id('numVerCode')
code.screenshot('./code.png')

# 使用ddddocr识别验证码
ocr = ddddocr.DdddOcr()
with open('./code.png', 'rb') as f:
img_bytes = f.read()
res = ocr.classification(img_bytes)
print(res)
# 输入识别的验证码
driver.find_element_by_id('numcode').send_keys(res)
# 点击登录按钮
driver.find_element_by_xpath('//input[@type="button"]').click()
# 刷新页面
driver.refresh()
title = driver.title
sleep(2)
driver.quit()

第七章:scrapy框架

scrapy介绍:scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。

安装:pip install scrapy

  • 可能报错,需要安装scrapy依赖库twisted

scrapy框架基本使用

scrapy框架的使用:

  • 创建爬虫项目:scrapy startproject 项目名
  • 创建爬虫文件:
    • 在项目的spiders文件下创建
    • scrapy genspider 爬虫文件名 爬取的网址url
  • 运行爬虫代码:
    • scrapy crawl 爬虫的名字
    • 需要在项目的settings.py文件注释君子协议(ROBOTSTXT_OBEY = True)

项目结构:

初始爬虫文件:创建百度文件

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

class BaiduSpider(scrapy.Spider):
# 爬虫的名字 运行爬虫的时候使用的值
name = 'baidu'
# 允许访问的域名
allowed_domains = ['www.baidu.com']
# 起始的url
start_urls = ['http://www.baidu.com/']

# 实行start_urls后执行的方法,response参数相当于requests.get()的结果
def parse(self, response):
pass

scrapy常用属性方法:

scrapy架构组成:

scrapy工作原理:

scrapy案例应用:爬取汽车之家信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import scrapy


class CarSpider(scrapy.Spider):
name = 'car'
allowed_domains = ['car.autohome.com.cn']
# 注意如果请求接口为html结尾,最后不需要/
start_urls = ['http://car.autohome.com.cn/price/brand-15.html']

def parse(self, response):
# xpath解析数据
name_list = response.xpath('//*[@class="list-cont-main"]/div/a/text()')
price_list = response.xpath('//div[@class="main-lever-right"]/div/span/span/text()')

for i in range(len(name_list)):
# 获取selector对象的值
name = name_list[i].extract()
price = price_list[i].extract()
print(name, price)

scrapy shell

scrapy shell:Scrapy终端,是一个交互终端,供您在未启动spider的情况下尝试及调试您的爬取代码。

ipython使用:自动补全,高亮输出,及其他特性。

  • 安装:pip install ipython

scrapy shell应用:scrapy shell 网址url

response对象:

  • response.body:二进制数据
  • response.text:页面源码数据
  • response.json:处理json数据
  • response.url:请求的url
  • response.status:响应状态码

response的解析:

  • response.xpath():xpath定位解析,返回一个selector列表对象
  • response.css():css属性定位解析,,返回一个selector列表对象
    • 获取内容:response.css(‘#su::text’).extract_first()
    • 获取属性:response.css(‘#su::attr(“value”)’).extract_first()

yield

yield 是一个类似 return 的关键字,迭代一次遇到yield时就返回yield后面(右边)的值。重点是:下一次迭代 时,从上一次迭代遇到的yield后面的代码(下一行)开始执行。

带有 yield 的函数不再是一个普通函数,而是一个生成器generator,可用于迭代。

分页爬取

案例:爬取当当图书网

一、cmd:创建爬虫项目

  • 创建项目:scrapy startproject dangdang

  • 创建爬虫文件:进入spider文件夹 scrapy genspider dang http://category.dangdang.com

  • 执行爬虫代码:scrapy crawl dang

二、item.py:确定items

1
2
3
4
5
6
7
8
9
import scrapy

class DangdangItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 定义需要下载的数据
title = scrapy.Field()
img = scrapy.Field()
price = scrapy.Field()

三、dang.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
import scrapy
from dangdang.items import DangdangItem


class DangSpider(scrapy.Spider):
name = 'dang'
allowed_domains = ['category.dangdang.com']
start_urls = ['http://category.dangdang.com/cp01.01.02.00.00.00.html']

base_url = 'http://category.dangdang.com/pg'
page = 1

def parse(self, response):
# pipelines:下载数据
# items:定义数据结构
li_list = response.xpath('//ul[@id="component_59"]/li')
for li in li_list:
# 解析书名
title = li.xpath('./a/@title').extract_first()
img = li.xpath('./a/img/@data-original').extract_first()

# 解析图片
if img:
img = img
else:
img = li.xpath('./a/img/@src').extract_first()
# 解析价格
price = li.xpath('./p[3]/span[1]/text()').extract_first()
print(title, img, price)

book = DangdangItem(title=title, img=img, price=price)

# 获取的book返回给pipelines
yield book

if self.page < 100:
self.page += 1
url = self.base_url + str(self.page) + '-cp01.01.02.00.00.00.html'

# scrapy.Request:就是scrapy的get请求
# url发起请求的地址,callback执行的函数
# 类似递归进行分页爬取操作,也可以调用其他方法
yield scrapy.Request(url=url, callback=self.parse)

四、pipelines.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
from itemadapter import ItemAdapter


# 需要在settings中开启管道
class DangdangPipeline:

# 在爬虫文件开始之前执行
def open_spider(self, spider):
self.fp = open('./books.json', 'w', encoding='utf-8')

# item就是yield返回的book对象
def process_item(self, item, spider):
self.fp.write(str(item))
return item

# 在爬虫文件执行之后执行
def close_spider(self, spider):
self.fp.close()


import urllib.request

# 开启多条管道:定义管道类,在settings中开启管道
class DangdangDownloadPipeline:

def open_spider(self, spider):
if not os.path.exists('./imgSrc'):
os.mkdir('./imgSrc')

def process_item(self, item, spider):
url = 'http:' + item.get('img')
img_path = './imgSrc/' + item.get('title') + '.jpg'
# 下载图片
urllib.request.urlretrieve(url=url, filename=img_path)
return item

五、settings.py:配置信息

1
2
3
4
5
6
7
8
# 修改robots协议
ROBOTSTXT_OBEY = False
# 开启管道下载
ITEM_PIPELINES = {
# 管道可以有多个,优先级范围1~1000,值越小优先级越高
'dangdang.pipelines.DangdangPipeline': 300,
'dangdang.pipelines.DangdangDownloadPipeline': 301
}

CrawlSpider

CrawlSpider简介:

  • CrawlSpider继承自scrapy.Spider

  • CrawlSpider可以定义规则,再解析html内容的时候,可以根据链接规则提取出指定的链接,然后再向这些链接发送请求。

  • 如果有需要跟进链接的需求,意思就是爬取了网页之后,需要提取链接再次爬取,使用CrawlSpider是非常合适的。

  • 链接提取器:定义规则提取指定链接

运行原理:

案例:爬取读书网

一、cmd:创建CrawlSpider爬虫项目

  • 创建项目:scrapy startproject readbook

  • 创建爬虫文件:进入spider文件夹 scrapy genspider -t read http://www.dushu.com/book/1188_1.html

  • 执行爬虫代码:scrapy crawl dang

二、item.py:确定items

1
2
3
4
5
6
7
import scrapy

class ReadbookItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
name = scrapy.Field()
src = scrapy.Field()

三、dang.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
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from readbook.items import ReadbookItem

class ReadSpider(CrawlSpider):
name = 'read'
allowed_domains = ['www.dushu.com']
start_urls = ['http://www.dushu.com/book/1188_1.html']

rules = (
# allow 正则表达式定义分页链接规则
Rule(LinkExtractor(allow=r'/book/1188_\d+\.html'),
# callback只能写函数名字符串
callback='parse_item',
# 是否跟进
follow=True),
)

def parse_item(self, response):
img_list = response.xpath('//div[@class="bookslist"]//img')
for img in img_list:
src = img.xpath('./@data-original').extract_first()
name = img.xpath('./@alt').extract_first()
book = ReadbookItem(src=src, name=name)
yield book

四、mysql:数据入库

  • 创建spider数据库,建立book表
1
2
3
4
5
6
7
create database spider default character set utf8 collate utf8_general_ci;
use spider;
create table book(
id int primary key auto_increment,
name varchar(128),
src varchar(128)
)
  • 安装pymysql:pip install pymysql
    • pymysql.connect(host,port,user,password,db,charset):建立连接
    • conn.cursor():获取游标
    • .cursor.execute(sql):执行sql

五、settings.py:配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ROBOTSTXT_OBEY = False

# 数据库配置信息
# 注意:1、字符集utf8不能有杠 2、端口号为整数
DB_HOST = '127.0.0.1'
DB_PORT = 3306
DB_USER = 'root'
DB_PASSWORD = '123456'
DB_NAME = 'spider'
DB_CHARSET = 'utf8'

ITEM_PIPELINES = {
'readbook.pipelines.ReadbookPipeline': 300,
'readbook.pipelines.MysqlPipeline': 301
}

# 指定日志的级别
# LOG_LEVEL = 'WARNING'
LOG_FILE = 'readbook.log'

六、pipelines.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
from itemadapter import ItemAdapter

class ReadbookPipeline:
def open_spider(self, spider):
self.fp = open('./books.json', 'w', encoding='utf-8')

def process_item(self, item, spider):
self.fp.write(str(item))
return item

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


# 加载settings文件
from scrapy.utils.project import get_project_settings
# 导入pymysql
import pymysql

class MysqlPipeline:
def open_spider(self, spider):
settings = get_project_settings()
# 获取settings中配置的数据库信息
self.host = settings['DB_HOST']
self.port = settings['DB_PORT']
self.user = settings['DB_USER']
self.password = settings['DB_PASSWORD']
self.name = settings['DB_NAME']
self.charset = settings['DB_CHARSET']
self.connect()

def connect(self):
# 建立连接
self.conn = pymysql.connect(
user=self.user,
password=self.password,
host=self.host,
db=self.name,
port=self.port,
charset=self.charset
)
self.cursor = self.conn.cursor()

def process_item(self, item, spider):
sql = 'insert into book(name, src) values("{0}", "{1}")'.format(item['name'], item['src'])
# 执行sql
self.cursor.execute(sql)
# 提交
self.conn.commit()
return item

def close_spider(self, spider):
# 关闭游标
self.cursor.close()
# 关闭连接
self.conn.close()

日志处理

日志级别:默认的日志等级是DEBUG,只要出现了DEBUG或者DEBUG以上等级的日志那么这些日志将会打印。

  • CRITICAL:严重错误
  • ERROR: 一般错误
  • WARNING: 警告
  • INFO: 一般信息
  • DEBUG: 调试信息

settings.py文件设置:

  • LOG_FILE:将屏幕显示的信息全部记录到文件中,屏幕不再显示,注意文件后缀一定是.log。
  • LOG_LEVEL:设置日志显示的等级,就是显示哪些,不显示哪些。

post请求

重写start_requests方法:def start_requests(self)

  • 定义post请求url,data参数

  • yield scrapy.FormRequest(url=url, headers=headers, callback=self.parse_item, formdata=data)

start_requests的返回值:

  • url: 要发送的post地址
  • headers:可以定制头信息
  • callback: 回调函数
  • formdata: post所携带的数据,这是一个字典
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
import scrapy


class TranslateSpider(scrapy.Spider):
name = 'translate'
allowed_domains = ['fanyi.baidu.com']
# start_urls = ['http://fanyi.baidu.com/sug']
#
# def parse(self, response):
# pass

# post请求处理
def start_requests(self):
url = 'https://fanyi.baidu.com/sug'
data = {
'kw': 'translate'
}

# FromRequest:发送post请求
yield scrapy.FormRequest(url=url, formdata=data, callback=self.translate_parse)

def translate_parse(self, response):
# response为post请求的返回数据
content = response.json()
print(content)

scrapy代理

settings.py中,打开一个选项:

1
2
3
DOWNLOADER_MIDDLEWARES = { 
'postproject.middlewares.Proxy': 543,
}

middlewares.py中写代码:

1
2
3
def process_request(self, request, spider): 
request.meta['proxy'] = 'https://113.68.202.10:9999'
return None

scrapy中间件

selenium加持scarpy

分布式爬虫

增量式爬虫

js逆向

exe打包

python虚拟环境

通过虚拟环境工具为项目创建纯净的依赖环境,并且实现项目之间相互隔离的 Python 环境,也可以方便的切换环境中的 Python 版本。

python环境目录:

  • Lib:标准库和site-pakages
  • Script:pip安装的可执行文件
  • python.exe

venv 模块使用:

  • python -m venv myvenv:在当前目录创建虚拟环境
  • 使用:在创建项目时引用当前创建的虚拟环境

虚拟环境激活:虚拟环境创建好后,需要激活才能在当前命令行中使用,可以理解成将当前命令行环境中 PATH 变量的值替换掉。

  • 激活脚本路径: \Scripts\activate
  • 退出虚拟环境:\Scripts\deactivate

为了减少环境变量的配置,虚拟环境会把python.exe和pip安装的可执行文件放在同一Scripts目录下。

pycharm创建虚拟环境:在项目创建对话框中,可以创建或者选择已经已有的解析器。

exe打包步骤

1、创建虚拟环境和开发项目

  • 查看虚拟环境安装的第三方依赖包:pip list
  • 导出文件形式查看虚拟环境安装的第三方依赖包:pip freeze > requirements.txt
  • 通过 requirements.txt 导入包:pip install -r requirements.txt
  • 安装指定库的版本:pip install selenium==3.14.0

2、进行项目开发

3、安装pyinstaller:pip install pyinstaller

4、对项目进行打包:

  • 多文件打包:pyinstaller -D 项目名.py
  • 单文件打包:pyinstaller -F 项目名.py
  • 会根据打包的.py文件命名exe文件
  • 打包并命名:pyinstaller -F 项目名.py -n 新名字

常用指令:

  • -F:打包一个单个文件,如果你的代码都写在一个.py文件的话,可以用这个,如果是多个.py文件建议别用

  • -D:打包多个文件,在dist中生成很多依赖文件,适合以框架形式编写工具代码,我个人比较推荐这样,代码易于维护

  • -i [icon path]:可以设置exe文件图标

  • -n [name]:指定可执行文件名称

  • –workpath:指定打包时临时文件存放目录

  • –distpath:指定打包好的可执行程序存放目录

  • –clean:在构建打包之前清理缓存并删除临时文件

打包后的文件目录:

  • dist:打包好的可执行程序默认存放目录

  • build:打包时临时文件默认存放目录

  • spec文件:打包配置文件

注意:打包好后拖动exe文件在cmd 里面运行,可以查看程序报错信息。

路径问题

当项目引用其他文件时,打包后运行会因为路径问题而找不到目标文件。

1、多文件打包,运行不会报错

2、单文件打包,运行会报错

  • 因为单文件打包的exe文件运行时会解压到临时目录,代码获取的是临时目录。

解决路径问题:

  • 方式一:sys.argv[0]
1
2
3
4
5
6
7
8
9
10
11
12
13
import os
import sys
# os.path.dirname(path):返回文件路径
# os.path.realpath():获取当前执行脚本的绝对路径
# sys.argv[0]:指向程序运行的全路径名
BASE_DIR1 = os.path.dirname(os.path.realpath(sys.argv[0]))
DIR = os.path.join(BASE_DIR1, 'name.txt')
# os.path.abspath():获得绝对路径
BASE_DIR2 = os.path.dirname(os.path.abspath(__file__))
# os.path.join(path, name):连接目录和文件名
print(BASE_DIR1)
print(BASE_DIR2)
print(DIR)
  • 方式二:frozen
1
2
3
4
5
6
7
import sys
import os
# 通过反射去sys里面找frozen,找不到就是打包后的程序
if getattr(sys, 'frozen', False):
BASE_DIR = os.path.dirname(sys.excutable)
else:
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

导入模块问题

py文件打包时会默认将引入的模块一并打包,并且引入的模块中也引入了其他模块,pyinstaller也会一并打包。

如果遇到动态导入模块的代码时,打包程序不会将动态导入的模块一并打包。

  • 需要在.spec文件的hiddenimport中配置动态导入的模块
  • 自定义打包:pyinstaller -F name.spec
1
2
3
4
5
import importlib
# 动态导入的模块
key_word = importlib.import_module('key_word')
js = key_word.removeAttribute('aa')
print(js)