Python简单爬虫流程
互联网就像是一张蜘蛛网,爬虫就是在网上爬行的蜘蛛,当蜘蛛爬到蜘蛛网的每个节点,就相当于访问并获取了当前网页的信息,随着网页链接之间的关系,蜘蛛可以顺着节点逐渐访问到其他的网页,获取自己需要的信息并保存下来。
一、相关知识
1.1 爬虫原理
网络爬虫抓取过程可以理解为模拟浏览器操作的过程。
模拟浏览器发送请求->获取网页代码->提取有用的数据->存放于数据库或文件中
1.2 HTTP协议
浏览器的主要功能是向服务器发出请求,在浏览器窗口中展示所选择的网络资源,HTTP是一套计算机通过网络进行通信的规则。
HTTP协议是基于请求/回应机制的。客户端与服务器端建立连接后,以请求方法、URI、协议版本等方式向服务器端发出请求,该请求可跟随包含请求修饰符、客户信息、及可能的请求体(body)内容的MIME类型消息。服务器端通过状态队列(status line)来回应,内容包括消息的协议版本、成功或错误代码,也跟随着包含服务器信息、实体元信息及实体内容的MIME类型消息。
1.2.1 请求(Request)
1) 请求方式 常用的请求方式:GET,POST 其他请求方式:HEAD,PUT,DELETE,OPTHONS
post请求的参数放在请求体内,get请求的参数直接放在url后
2) 请求url
url全称统一资源定位符,如一个网页文档,一张图片都可以用url唯一来确定
3) 请求头
| 参数 | 解释 |
|---|---|
| cookies | cookie用来保存登录信息,HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。 |
| User-agent | 告诉服务器这是浏览器发过来的请求(请求头中如果没有user-agent客户端配置,服务端可能将你当做一个非法用户)务必加上 |
| host | 指定请求的服务器的域名和端口号 |
| Referer | 上一次的跳转路径 |
| Accept | 指定客户端能够接收的内容类型 |
| Accept-Charset | 浏览器可以接受的字符编码集。 |
| Content-Type | 用于规定请求体的编码格式,服务端代码需要使用它对接收到的消息主体进行解析。 |
4) 请求体 如果是get方式,请求体没有内容 如果是post方式,请求体是format data,登录信息,文件上传等,信息都会被附加到请求体内
请求体的格式种类:
-
application/x-www-form-urlencoded(默认格式)
post请求最常见也是默认的数据提交格式。要求数据名称(name)和数据值(value)之间以等号相连,与另一组name/value值之间用&相连。例如username=12345&password=23456。
# 以form表单形式提交数据 data = {'UNAME': self.p['username'], 'PASSWORD': self.p['password']} login_response = self.session.post(self.p['login_url'], data=data, headers=self.header) -
application/json
这种数据提交格式告诉服务端消息主体是序列化后的 JSON 字符串
# 可以将dict类型的数据转换成json格式的字符串提交 data = {'UNAME': self.p['username'], 'PASSWORD': self.p['password']} login_response = self.session.post(self.p['login_url'], data=json.dumps(data), headers=self.header) # 也可以用json参数提交,json参数会自动将字典类型的对象转换为json格式 login_response = self.session.post(self.p['login_url'], json=data, headers=self.header) -
text/xml
使用 HTTP 作为传输协议,XML 作为编码方式的远程调用规范
# 请求体为xml格式的字符串 data = f""" <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body> <UserLogin xmlns="http://tempuri.org/"> <loginName>{self.p['username']}</loginName> <pwd>{self.p['password']}</pwd> <type>2</type> </UserLogin> </s:Body> </s:Envelope> """ login_response = self.session.post(self.p['login_url'], data=data, headers=self.header) -
multipart/form-data
multipart/form-data主要用于文件上传,当我们使用它时,必须让 form表单的enctype 等于 multipart/form-data。
files = {"file": open("../test.txt", "rb")} response = self.session.post(url, files=files)
1.2.2 响应(Response)
1) 响应状态
- 200:服务器已成功处理了请求
- 3xx:重定向
- 404:服务器找不到请求的网页
- 403:服务器拒绝请求
- 5xx:服务器错误
2) 响应头
| 参数 | 解释 |
|---|---|
| Location | 跳转 |
| set-cookie | 可能有多个,是来告诉浏览器,把cookie保存下来 |
3) 网页源码
包括html、css、js、图片等
二、 爬虫
爬虫所需工具:
请求库: requests,selenium
解析库: re,BeautifulSoup, json
session会话:
requests库的session会话对象可以跨请求保持某些参数, 使用session成功的登录了某个网站,则在再次使用该session对象求求该网站的其他网页都会默认使用该session之前使用的cookie等参数。
self.session = requests.session()
2.1 登陆
2.1.1 requests方式
传入请求需要的参数(data或params),进行登陆
params是用来发送查询字符串,而data是用来发送正文的。post方法和get方法的特性是:这两种参数post方法都可以用,get方法只能发查询字符串,不能发送正文。
params = {'action': 'login'}
data = {
'username': self.p['username'],
'userPassword': self.p['password'],
'Submit': '(unable to decode value)',
}
login_response = self.session.post(self.p['login_url'], data=data, params=params, headers=self.header)
2.1.2 selenium方式
对必要的元素(用户名、密码及其它必填值)定位并赋值,然后通过.click()点击进行登陆
使用selenium方式通常需要一定的等待时间,保证浏览器加载完成
option = webdriver.ChromeOptions()
option.add_argument('headless')
self.browser = webdriver.Chrome(options=option)
self.browser.get(self.p['login_url'])
self.browser.find_element_by_name('txtUserName').clear()
self.browser.find_element_by_name('txtUserName').send_keys(self.p['username'])
self.browser.find_element_by_name('txtPwd').clear()
self.browser.find_element_by_name('txtPwd').send_keys(self.p['password'])
self.browser.find_element_by_id('submit').click()
一般我们使用requests请求进行登陆,但是如果请求参数过于复杂,可以先用selenium登陆,将获取到的cookies转化成requests请求可用的cookies形式,再进行后续操作
for cookie in self.browser.get_cookies():
self.cookies[cookie['name']] = cookie['value']
#将cookies放在参数中进行请求
export_response = self.session.get(export_url, params=params, headers=self.header, cookies=self.cookies)
常用的定位方法有以下几种:
id定位:findelementby_id()
name定位:findelementby_name()
class定位:findelementbyclassname()
link定位:findelementbylinktext()
partial link定位:findelementbypartiallink_text()
tag定位:findelementbytagname()
xpath定位:findelementby_xpath()
css定位:findelementbycssselector()
2.1.3 验证码
1) 图片提取
将验证码图片保存在本地,使用打码平台或其他识别验证码的方式获取验证码
imagecode_url = 'http://eip.crks.cn/Confirmation.aspx'
image_response = self.session.get(url=imagecode_url)
write_file(self.p['image_dir'], f"{self.p['dps_id']}_image.jpg", image_response.content)
# cid, imagecode = ymdhttp.ymd(f"{self.p['image_dir']}/{self.p['dps_id']}_image.jpg", 1004)
imagecode = parse_imagecode(f"{self.p['image_dir']}/{self.p['dps_id']}_image.jpg")
2) 页面提取
有些验证码是通过js随机生成,刷新网页之后可以在页面上提取到验证码
self.base_url = re.search(r'http://(.*)/index.asp', self.p['login_url']).group(1)
imagecode_url = f'http://{self.base_url}/index.asp?Do=Gyslogin'
image_response = self.session.get(url=imagecode_url)
imagecode = re.search('<FONT COLOR="#FF3300" style="font-size:12pt;" ><B>(.*)</B></FONT>',
image_response.text).group(1)
2.2 获取数据
2.2.1 requests方式
获取数据首先需要得到请求url及请求参数。
有时请求数据的headers可能和登陆时的headers不同,需要对照网页将必要的参数加入到请求头。
特殊请求参数
1) __VIEWSTATE, __EVENTVALIDATION 等参数用来保存网页的一些特殊信息,需要从页面中动态获取。
2) Token 是指计算机中的临时身份证,使用用户名/密码向服务端请求认证,服务端认证成功,在服务端会返回 Token 给客户端,需要从response中获取。
3) 时间 有时请求参数中需要带有当前的时间,根据网页上的时间格式加入该参数。
通常有 str(round(time.time() * 1000))、int(round(time.time()))等格式。
4) json格式参数,需要用 json.dumps()将参数转化为json格式。
5) 多个同名参数, 如果参数中包含多个同名参数,只保留一个参数名,将所有的值都放在一个列表中。
2.2.2 selenium方式
添加cookie
如果用requests请求登陆,需要给selenium添加cookie
self.browser = webdriver.Chrome(chrome_options=option)
for key, value in cookie_dict.items():
self.browser.add_cookie({'name': key, 'value': value})
等待
如果请求的数据量较大,需要设置较长的等待时间,避免未请求到数据发生错误
调用js
有时点击某个元素,会弹出新窗口,但是程序还在原来的窗口定位元素,这样就会定位不到元素,所以需要先执行js方法,再去执行点击事件
dianji = self.browser.find_element_by_xpath('//span[@class="ksdh_icon_01 ksdh_icon_05"]')
self.browser.execute_script("arguments[0].click();", dianji)
time.sleep(2)
2.3 解析
2.3.1 正则表达式
正则表达式通常被用来检索、替换那些符合某个规则的文本。比如获取得到数据的页码、导出数据时的文件名、截取url等。
# 获取页码
page_num = re.search(r'<span id="lbSumPage">(.*)</span>页', data_response.text).group(1)
# 获取文件名
filename = re.search(r'filename=(.*)', data_response.headers['Content-Disposition']).group(1)
# 截取部分url
self.base_url = re.search("http://(.*)/efjsx/", self.p['login_url']).group(1)
# 获取导出url
export_url = re.search(r'"ExportUrlBase":"(.*?)"', page_source).group(1)
2.3.2 BeautifulSoup
BeautifulSoup 是一个HTML/XML的解析器, 主要的功能是解析和提取 HTML/XML 数据。
BeautifulSoup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳为4种:
- Tag
- NavigableString
- BeautifulSoup
- Comment
Python爬虫获取html中的文本方法多种多样,目前会用到的主要有以下几种:
- stripped_strings:用来获取目标路径下所有的子孙非标签字符串,会自动去掉空白字符串,返回的是一个生成器
- strings:用来获取目标路径下所有的子孙非标签字符串,返回的是个生成器
- get_text:用来获取目标路径下的子孙字符串,返回的是字符串(包含HTML的格式内容)
- text:用来获取目标路径下的子孙非标签字符串,返回的是字符串
# 创建beautifulsoup对象
soup = BeautifulSoup(page_source, 'lxml')
soup = BeautifulSoup(response.text, 'lxml')
# 得到数据列表
title_data_tr_list = soup.find('table', id="GVdingdan").contents
# 得到一个title_iter生成器
title_iter = title_data_tr_list[1].stripped_strings
for title in title_iter:
titles.append(title)
for data_tr in title_data_tr_list[2: -1]:
if type(data_tr.string) == bs4.element.NavigableString:
continue
temp_data = []
data_iter = data_tr.stripped_strings
for data in data_iter:
temp_data.append(data)
2.3.3 json
如果返回的为json格式的数据,可通过json()提取数据
# 获取data
rows = response.json()['data']
titles.extend(list(rows[0].keys()))
# 获取token
self.token = login_resopnse.json()['token']
2.4 存储
爬取的数据可存储在txt文件、excel文件或数据库等
2.5 反爬
因为很多网站都有反爬虫措施,同一个ip访问次数过多很容易被封IP,解决这个问题有以下应对措施
伪造User-Agent 可以收集多种浏览器的User-Agent,在请求头中把User-Agent设置成浏览器中的User-Agent,来伪造浏览器访问。
使用代理 可以换着用多个代理IP来进行访问,防止同一个IP发起过多请求而被封IP
降低访问速度 在每次重复爬取之间设置一个时间间隔,避免过于频繁的使用同一IP访问同一网站
2.6 数据处理
获取到需要的数据之后有时还需要做某些处理
根据实际需要进行处理