Python简单爬虫流程

99 阅读9分钟

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) 请求头

参数解释
cookiescookie用来保存登录信息,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 数据处理

获取到需要的数据之后有时还需要做某些处理

根据实际需要进行处理