爬取12306总结

Posted by 404player on March 4, 2020

最近没怎么学习新东西,就想做一些能验证所学的东西。恰巧前段时间学了python中一个基于浏览器自动化的模块selenium,就想着利用它来爬取一些比较难爬的数据玩一下,然后就选择了12306这个网站。接下来我要就我爬取过程中用到的一些技巧和遇到的一些难点作个总结。


1.12306网站分析

12306的网址是https://kyfw.12306.cn/otn/login/init

下面我们来看看12306的页面:

good

从上面的页面中我们可以看出,要爬取12306的数据,首先要做的就是一个模拟登录。要做模拟登录,脑海里首先浮现出的是在请求页面中传入登录参数。这种做法在验证码是数字英文的时候当然可以做到,但是12306引入的验证码识别是一种图像识别点击的验证方法,我们没有办法通过简单的参数传入去做到点击操作,这个时候我们就需要用到python中一个叫selenium的模块。

下面我们来简单地介绍一下selenium模块。

2.selenium模块简介

2.1 selenium基本操作

selenium是一个基于浏览器自动化的python模块。

什么叫浏览器自动化呢?所谓自动化,就是不需要我们手动运行,通过程序就可以让浏览器自动地完成一些复杂的操作。下面我们写一个程序,让其自动地访问京东,并在输入框输入Python,点击确认输入,我们来看看这个程序该怎么写:

from selenium import webdriver
from time import sleep
bro = webdriver.Chrome(executable_path='chromedriver.exe')
bro.get('https://www.jd.com/')
sleep(1)
#进行标签定位
search_input = bro.find_element_by_id('key')
search_input.send_keys('Python')

btn = bro.find_element_by_xpath('//*[@id="search"]/div/div[2]/button')
btn.click()
sleep(2)

首先要使用selenium实现浏览器的自动化,我们必须先要下载对应浏览器的驱动程序。

我用的是Chrome浏览器,它的驱动程序的下载地址是http://chromedriver.storage.googleapis.com/index.html。下载完成之后,要记住把chromedriver.exe放在你在写的这个程序的同一个目录下。

安装完浏览器驱动以后,先实例化了一个bro浏览器驱动对象,然后可使用find_element_by_id方法通过id定位到对应的输入框,接下来用send_keys进行表单输入。

除了find_element_by_id定位以外,我们更习惯用的是find_element_by_xpath进行定位的方法,这个方法传入的参数就是我们极其熟悉的xpath路径,一般只要打开开发者工具复制黏贴即可。

所有操作做完以后,因为请求发送是需要时间的,所以sleep等待回复。

接下来我们看一看运行的效果,动态的没办法贴出来,直接看最后的结果:

可以看到最终浏览器是显示了京东官网的页面,并搜索了Python相关内容。

这里拓展一下,假如我们要自动化地将浏览器页面往下滑应该怎么操作呢?我们知道前端语言有一门语言Javascript是专门负责页面跳转与动态加载的,我们也可以通过在浏览器中嵌入javascript代码来实现动态操作。看一下下面这一段代码:(在前面代码的基础上加的)

# 执行js
bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
sleep(2)
bro.quit()

execute_script就是一个执行javascript代码的方法,相当于在网页控制台Console中写入代码。

最后一个bro.quit()跟文件关闭一样,都是要释放内存的。

2.2 selenium与爬虫的关系

前文说了那么多,都是在说selenium实现浏览器自动化的功能,那这个模块究竟跟爬虫有什么关系呢?

还是上面的例子,我们来写一段代码来看看效果:

from selenium import webdriver
from time import sleep
bro = webdriver.Chrome(executable_path='chromedriver.exe')
bro.get('https://www.jd.com/')
sleep(1)  
page_text = bro.page_source
print(page_text)  
sleep(2)
bro.quit()

结果就不贴出来了,最终结果程序将网页源代码打印了出来。

这跟爬虫的关系就紧密起来了,因为无论是什么样的爬虫,获取源码永远是它最先要做的事情。那我们现在最主要的问题就回到了selenium本身,我们都知道selenium是一个基于浏览器去请求的模块,用浏览器去请求的速度肯定是远远不及用requests模块直接去发出请求快的,那为什么要用selenium模块去请求呢?

原因很简单!!虽然说两者请求回来都是页面源代码,但这两者还是有很大区别的。使用requests模块请求回来的代码就是对应url主页的源代码,而用selenium可以实现可见即可得的功效。简单地说,就是使用selenium请求回来的数据就是F12开发者工具中Elements里的源码。

这就意味着无论是主页中静态的数据还是动态的数据,都可以通过selenium获得,这就给我们爬取动态加载的数据提供了便利。

下面是一个我爬取药监总局数据的代码,可以参考着领悟一番:

# Author:Geekboy
from selenium import webdriver
from time import sleep
from lxml import etree
bro = webdriver.Chrome('chromedriver.exe')

bro.get('http://125.35.6.84:81/xk/')
sleep(1)
page_text = bro.page_source
page_text_list = [page_text]

for i in range(3):
    bro.find_element_by_id('pageIto_next').click() #点击下一页
    sleep(1)
    page_text_list.append(bro.page_source)

for page_text in page_text_list:
    tree = etree.HTML(page_text)
    li_list = tree.xpath('//ul[@id="gzlist"]/li')
    for li in li_list:
        title = li.xpath('./dl/@title')[0]
        num = li.xpath('./ol/@title')[0]
        print(title+':'+num)

sleep(2)
bro.quit()

使用selenium模块爬取动态数据,我们不用在浏览器开发者工具中花时间仔细搜寻动态数据所在的页面,只要page_source把数据爬下来,就可以直接用xpath解析数据,十分方便,当然在获取多页面数据的速度上肯定比requests要慢,有得必有失嘛

3.动作链

解决了获取数据的问题,我们就要回到最初的问题上,也就是模拟登录的问题,这也是我们必须要用selenium的原因

在动手解决一个问题之前,我们首先要了解一个概念,什么叫动作链?

顾名思义,其实动态链就是一系列连续的动作,就像点击拖动、把光标放到某个地方进行点击等等,可以说,只有我们清楚了动态链的操作,才能很好地去处理现在网络上大部分的复杂验证码。

动态链的操作需要用到一个ActionChains模块,下面我们结合代码来对动作链进行讲解:

# Author:Geekboy
from selenium import webdriver
from time import sleep
from lxml import etree
from selenium.webdriver import ActionChains
bro = webdriver.Chrome('chromedriver.exe')
bro.get('https:///www.runoob.com/try/try.php?filename=jqueryui-api-droppable')

bro.switch_to.frame('iframeResult')
div_tag = bro.find_element_by_id('draggable')

# 拖动=点击+滑动
action = ActionChains(bro)
action.click_and_hold(div_tag)

for i in range(5):
    #perform让动作链立即执行
    action.move_by_offset(17,5).perform()
    sleep(0.5)

action.release()

sleep(3)

bro.quit()

我们这次访问的网站本来就是一个仿照拖动验证码的实验网站,我们来看看它的页面:

验证

这段代码有几个要注意的点:

第一点,bro.switch_to.frame('iframeResult')。在刚开始写这段代码的时候,我是 直接获取源代码后就开始我模拟拖动的操作,发现并不成功,回过头去看看页面源码,发现在div_tag对应元素上面有这么一段代码<iframe frameborder="0" id="iframeResult" style="height: 579.36px;"></iframe>,意思就是它在网页中嵌入了一个内联框架,你可以简单地理解为另一个网页,这个时候你可以通过bro.switch_to.frame('id_name')切换到这个框架中。

第二点,动作链在模块导入后要实例化一次action = ActionChains(bro),且参数为浏览器驱动实例,才能生效。动作链有很多自动化的操作方法,需要用的时候可以自行百度。

第三点,跟filebro一样,动作链实例一样需要释放内存,这一步在一个优秀的程序中是必须要做到的,否则在程序规模越来越大的情况下,内存的占用对程序性能的影响也会越来越大。

第四点,动态链实例action可调用move_by_offset进行横向和纵向的移动。

4.爬取12306程序解析

因为我自己是没有12306的密码的,而且整个爬取的过程难点在于模拟登录,这里只记录整个模拟登录的过程。

前文已经说过,模拟登录的重点在于验证码的识别,而要识别验证码,我们必须依靠第三方平台的一些基于机器学习的验证码识别接口,这些接口可以通过我们传入图片给我们返回验证码的数字英文或者需要点击位置的页面坐标等等。我习惯用的验证码识别平台是一个叫超级鹰的网站。

要获取这个接口,你首先要在网站中注册一个账户,并生成一个软件ID,这个软件ID在识别函数中要以参数的形式传入

chaojiy

接下来,你要在网站的开发文档内选择下载Python语言识别接口的源码
kaifa

下载下来后源码如下:

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()


#这上面的代码就是类的定义


if __name__ == '__main__':
	chaojiying = Chaojiying_Client('超级鹰用户名', '超级鹰用户名的密码', '96001')	
	im = open('a.jpg', 'rb').read()													
	print chaojiying.PostPic(im, 1902)												

具体的源码内容不需要读懂,我们只需要知道这个接口中定义了识别验证码的类,并提供调用这个类的函数。在写相关函数的时候,我们只需要将定义类的相关代码复制在我们要写的程序中,并将调用函数修改一下,就可以实现验证码的识别。

解决了识别问题,所谓巧妇难为无米之炊。我们还要将验证码的图片获取,正常情况下,我们是可以通过xpath方法去将图片数据获取的,但是经过多次错误后,我发现,当我对这张验证码的图片发起请求时,返回的是一张新的验证码,而我们要识别的是已经显示在主页的验证码,这就代表着我们无法通过爬取的手段获取验证码图片

这该怎么办呢?PIL模块为我们提供了很好的方法。 (其实就是最新版的pillow)

PIL模块可以将截取的图片进行再处理,下面开始写这段程序,并进行说明

from selenium import webdriver
from time import sleep
from lxml import etree
from selenium.webdriver import ActionChains
from  PIL import Image
from Cjy import Chaojiying_Client

bro = webdriver.Chrome(executable_path='chromedriver.exe')
bro.get('https://kyfw.12306.cn/otn/login/init')

sleep(1)
bro.save_screenshot('main.png')

code_image_tag = bro.find_element_by_xpath('//*[@id="loginForm"]/div/ul[2]/li[4]/div/div/div[3]/img')
location = code_image_tag.location
size = code_image_tag.size
rangle = (int(location['x']),int(location['y']),int(location['x']+size['width']),int(location['y']+size['height']))
#裁剪的区域范围

i = Image.open('./main.png')
frame = i.crop(rangle)
frame.save('code.png')

首先就像前文所说的那样,先将验证码识别的类存入当前目录下一个名为Cjy.py的文件中,然后在程序开头导入,为后面验证做出准备。

在用selenium模块获取网页后,可使用save_screenshot将整个页面截图,并保存起来,这个时候我们获取的页面其中就包括了验证码的图片。

随后我们用xpath标签定位到了图片,利用locationsize方法定位了图片在主页的位置并识别了图片的大小,值得注意的一点是,这里的位置,指的是图片最左下角处的点在主页中的位置坐标,返回的是一个有两个元素的字典,我们下面可以看到通过location['x']location['y']可以轻松地获取到它的数据。

rangle中的值包括四个数,分别是图片最左下角的坐标与最右上角的坐标,通过这两个坐标,我们可以使用PIL模块进行精确截图并保存起来。

完成截图以后,我们就可以开始下一部分的编程,使用识别API进行验证码的识别

def get_text(imgPath,imgType):
    if __name__ == '__main__':
        chaojiying = Chaojiying_Client('baomi', 'baomi', '903627')
        im = open(imgPath, 'rb').read()
        return chaojiying.PostPic(im,imgType)['pic_str']

result = get_text('./code.png',9004)


all_list = []
if '|' in result:
    list_1 = result.split('|')
    count_1 = len(list_1)
    for i in range(count_1):
        xy_list = []
        x = int(list_1[i].split(',')[0])
        y = int(list_1[i].split(',')[1])
        xy_list.append(x)
        xy_list.append(y)
        all_list.append(xy_list)
else:
    xy_list = []
    x = int(result.split(',')[0])
    y = int(result.split(',')[1])
    xy_list.append(x)
    xy_list.append(y)
    all_list.append(xy_list)

print(all_list)
for a in all_list:
    x = a[0]
    y = a[1]
    ActionChains(bro).move_to_element_with_offset(code_image_tag,x,y).click().perform()
    sleep(1)

上面代码框的第一段代码,其实就是刚刚从超级鹰下载下来的源码中提供的调用函数,我们可以截取下来看一下:

if __name__ == '__main__':
	chaojiying = Chaojiying_Client('超级鹰用户名', '超级鹰用户名的密码', '96001')	
	im = open('a.jpg', 'rb').read()													
	print chaojiying.PostPic(im, 1902)												

这段函数中需要传入的参数就是验证码的类型和验证码图片所在的相对路径,验证码图片的类型可以去超级鹰平台的价格体系中获取。 用户名和密码是写死在函数内部的。
值得注意的一点是,函数内部还有一个参数(就是调用函数中96001 所在位置的参数)。这个参数其实就是我们刚刚生成的一个软件ID,也是可以写死在函数中的。(当然识别是要收费的,要在平台上面购买积分,大概1块钱1000个积分,识别一次30个积分左右。)

我们需要识别的验证码返回的应该是一个坐标,根据价格体系,类型编号应该是9004

mam

返回的坐标用|分隔(因为根据文字识别图片验证码识别出来的图片不一定只有一个),需要写一段代码对这些坐标进行分离,并以数组的形式存储在数组中。 (数组中包含数组)

获取坐标后,我们就可以直接根据坐标进行点击了吗?

其实还不行,因为我们现在获取的坐标是在验证码图片的基础上获取的,而点击操作则是在整个页面中进行的,如果直接将xy参数传入move_by_offset中的话,程序会让光标在页面以页面最左下角的坐标为零点,进行横向及纵向的移动,那有什么好办法呢?

Python无疑是十分强大的,除了move_by_offset函数以外,它还提供了一个叫move_to_element_with_offset的函数,只要我们在提供x,y参数之前,把之前定位的图片也作为参数传入,程序就会将验证码图片所在位置的最左下角当成零点,进行移动,问题解决。

下面我们来看一看这段代码运行的结果:

baomi

验证码成功识别,只要用selenium模块将用户名密码参数在表单中输入并提交,便可成功进行模拟登录。

4.总结

爬取12306的过程中,遇到了不少麻烦,不过总算都成功解决了。

其实爬取数据的过程中,除了验证码这一道大难关,还有很多很NB的反爬机制,包括将数据加密甚至将加密方法也进行加密的操作,所以为了获取数据,难度实在不小。

接下来的时间我也开始上课了,花在python和安全领域的时间可能会有所缩减,不过还是要坚持学习,争取早日成为技术大牛!!!