python爬虫实例-爬取北邮研究生成绩

做了许多爬虫的小练习,都是比较简单的爬虫程序,像下小说啊,爬取一个网站内所有的页面之类的。基本上都是看着别人的需求,按照别人的思路,来写程序。

心想,能不能自己构思,弄一个自己的小程序,解决自己的实际需求。楼主还是个学生,恰好这学期期末了,可以在学校的网站上查询自己的成绩了,于是就想着能不能自己做一个爬虫,从自己学校的网站上爬取一些自己的信息呢?

说干就干,于是开始琢磨网站的结构、网页的URL、请求数据包一类的。首先先来查询成绩的整个流程吧!

需求:

这是学校成绩查询的网址 http://yjxt.bupt.edu.cn

《python爬虫实例-爬取北邮研究生成绩》

结构比较简单,输入学号,密码,验证码,选择身份之后点击登录就会出现主页面了:

《python爬虫实例-爬取北邮研究生成绩》

《python爬虫实例-爬取北邮研究生成绩》

点击课程成绩信息查询,在右侧就显示出所有的成绩信息了:

《python爬虫实例-爬取北邮研究生成绩》

《python爬虫实例-爬取北邮研究生成绩》

我们要做的就是爬取这个表格中所有的数据了。

1、准备工作:

熟话说,磨刀不误砍材工,我们首先要先准备好开发和调试需要的工具。
开发环境:楼主用的是WIN7 64位系统,但是安装的python2.7 32bit的,安装了最新的Eclipse+pydev。
浏览器:用到了UC浏览器(这个也是刚使用没多久的浏览器,调试以及插件仓库都是用的chrome的,个人感觉上比chrome还要快一点,所以就一直用着),主要用来分析网页结构,浏览器请求和响应报文的解析

文档比较器:BCompare。Beyond Compare 是一个综合的比对工具。 可比对的对象包括纯文字档、资料夹、zip 压缩案、FTP 站,等等。 您可以使用它管理您的原始档、保持比对资料夹同步、比对程式的输出结果、以及检查所烧入光碟备份的精确度。Beyond Compare 的主要目标还是帮助您详尽的分析差异之处,并且对它们进行详尽的处理。 软体内部包含了许多档案和资料夹命令动作。如果网页中有一些动态生成的序列id什么的元素,可以对比网页源代码,找出这些不同之处,很是方便。
验证码解析:这个可以参考另外一篇关于验证码识别的一篇文章,介绍了如何用pytesser来识别验证码。http://blog.csdn.net/zq602316498/article/details/37817341

抓包工具: wireshark,大名鼎鼎的抓包工具。因为要使用python发数据包的话,不像chrome浏览器那样能将报文头部,post数据结构啥的都能分析出来,python发包如果出了bug的话,没有抓包软件是挺麻烦的一件事情。当然也可以通过设置urllib2打开debug模式,方法如下:

httpHandler = urllib2.HTTPHandler(debuglevel=1)
httpsHandler = urllib2.HTTPSHandler(debuglevel=1)
opener = urllib2.build_opener(httpHandler, httpsHandler)
urllib2.install_opener(opener)
response = urllib2.urlopen('http://www.google.com')

这样就能看到传输内容了。如下图:
《python爬虫实例-爬取北邮研究生成绩》

运行的时候,发送的请求包以及响应都列了出来。我们可以运行多次,然后将多次的值日志通过前面提到BComare软件对比日志记录,来分析程序可能出现的问题。如下图是两次运行日志的对比界面。
《python爬虫实例-爬取北邮研究生成绩》

2、登录页面

首选我们来看一下登录页面的组成结构:

打开登录页面-按F12调出调试工具,看一下表单的结构:

《python爬虫实例-爬取北邮研究生成绩》

我们可以看到用户名和密码的 name属性分别为 UserName,PassWord。

我们先打开Network调试视图,然后填好用户名和密码以及验证码尝试登录一下(当然输入的用户和密码是错误的),在Network视图中可以看到我们向服务器发出的请求报文结构以及服务器返回的数据结构:

《python爬虫实例-爬取北邮研究生成绩》

在这里我们可以看到,从我们点击登录直到登录失败,一共产生了3次数据交换,如上图:

第一个数据包UserLogin.aspx?exit=1 这个是登录的请求,我们点开它可以看到如下图所示的数据结构:

《python爬虫实例-爬取北邮研究生成绩》

我们可以看到,上图中,我们请求的
地址为:http://yjxt.bupt.edu.cn/UserLogin.aspx?exit=1 ,
请求的方式为:post,
请求的sessionid为:ASP.NET_SessionId=ussshjtm2yp2whflmzbysvuj; 
请求的方式为:XMLHttpRequest方式,即ajax异步请求的方式。
参数中包含了我们的:用户名(21359841) 密码(453453)以及验证码(7031)。


因此我们需要要发起这个请求就有两个难点:
1、我们要构造包含有sessionID号的request头部
2、我们要构造含有用户名,密码和验证码的表数据

我们知道服务器是通过session来维护和浏览器之间关系的,通过session来判断这个请求时那个浏览器发起的,那么这个sessionID是怎么来的呢?
其实当我们打开登录页面,服务器会通过cookie将session的id号记录到浏览器本地,每当浏览器向服务器发请求时,它都会读取这个cookie中的sesisonid。我们可以在【Cookies】视图中看到当前有多少cookie值。如下图显示了当前的所有cookie数据。
《python爬虫实例-爬取北邮研究生成绩》

讲到这里,顿时就想到了可以在python代码中通过读取cookie,找到这个sessionid不就OK了吗?事实上我们也是这么干的!

self.cookieJar = cookielib.CookieJar()        
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookieJar))
myRequest = urllib2.Request(url = self.loginUrl)   # 自定义一个请求 
for item in self.cookieJar:
    if item.name =='ASP.NET_SessionId':
        self.sessionId = item.value
    print '    Name = '+item.name
    print '    Value = '+item.value  

这样就将这个sessionid保存了下来,以供以后发登录请求的时候使用。(因为我们之前使用了装载了HTTPCookieProcessor的opener,在以后发包的时候会自动将sessionID包裹在请求包中,就省略了我们手动添加sessionID的麻烦。而且有时候由于手动添加多个sessionid会导致请求失败 -_-)

那怎么解决第二个问题,解析验证码呢? 请继续看第二个数据包的分析。


第二个数据包ValidateCode.aspx?image=167742089 咋一看好像和验证码有关系,不过为啥不是图片格式呢?我们点开它看一下:


《python爬虫实例-爬取北邮研究生成绩》


咦、这里为啥没有返回数据呢?
其实这个是有返回数据的,只不过它返回的不是一般的文本数据,而是服务器动态生成验证码图片以后,将这个图片的数据流返回给了浏览器,而浏览器把这个流显示成了图片了!
我们可以把这个地址复制到浏览器的地址栏里面,访问一下试试看。
《python爬虫实例-爬取北邮研究生成绩》







再刷新一下试试!

《python爬虫实例-爬取北邮研究生成绩》

都不一样吧!其实这些图片都是服务器先生成了验证码,然后将它转换成图片返回给了浏览器了!

所以这就给了们一个思路:我们只要将这个图片再转换成文字不就解决了验证码的问题吗?

说起来简单,做起来难!
第一个是获取图片的问题
刚开始的时候我本来是想,首先open(‘http://yjxt.bupt.edu.cn/UserLogin.aspx?exit=1’)得到登录界面,然后再open(‘http://yjxt.bupt.edu.cn/Public/ValidateCode.aspx?image=1480423392‘)(这里的image=1480423392其实没什么用 –)
其实这是不好的!!
假设用户A第一个open获取页面之后,它会得到一个sessionID,假设为s1,同时得到一个验证码,假设为abc。
当用户A再进行第二次open的时候,因为没有使用第一次的sesson s1
,这时候服务器会再分配给他一个新的session s2 ,以及一个新的验证码def。由于验证码解析这一块一般是分别单独放在一个模块中解析的,这时候不仅要考虑s1,还要考虑s2,造成了逻辑上不清晰。而且
我们有了两个session和两个验证码。这个时候我们就必须要谨慎的选择读取cookies的时机。
由于之前认识浅显,认为第一个open请求页面会产生session,第二个open只是请求了一个图片流而已,不会产生session,所以一直认为第一个open是十分有必要的,纠结了一大会儿。
后来想想其实是很没必要的

直接进行第二个open获取图片其实就足够了!服务器对浏览器验证码进行验证的时候,如果没有旧的sessionID的话,sessionid总是将其和最后一次请求验证码图片时候的数据进行比对,因此第二个open获取的图片直接可以用来解析验证码。
第二个问题是识别图片解析出验证码的问题
关于这个,http://blog.csdn.net/zq602316498/article/details/37817341 这里有较详细介绍,就不多说了。
这里没有将验证码图片下载到本地,因为觉得没有必要。用了StringIO模块,直接送给pytesser解析了。


            im = self.opener.open(self.imageUrl).read()       #获取验证码和cookies值
            img_buffer = StringIO.StringIO(im)
            img = Image.open(img_buffer)
            textcode = pytesser.image_to_string(img)
            

第三个数据alert.gif就特别简单了,仅仅是一个错误警告弹窗的图片而已。


好了,到这里我们就分析完了登录的流程了,因此我们可以写出登录的代码来:

def login(self):
        postdata ={
                   'UserName':self.username,
                   'PassWord':self.password,
                   'drpLoginType':1,
                   'ScriptManager1':'UpdatePanel2|btLogin',
                    '__EVENTTARGET':'btLogin',
                    '__VIEWSTATE':'''/wEPDwULLTE3MzIzNjYwNjMPZBYCAgMPZBYGAg0PZBYCZg9kFgICAQ8PFgIeCEltYWdlVXJsBSp+L1B1YmxpYy9WYWxpZGF0ZUNvZGUuYXNweD9pbWFnZT0xOTg4NjIyMjlkZAIRD2QWAmYPZBYCAgEPEGRkFgFmZAIVD2QWAmYPZBYCAgEPDxYCHgtOYXZpZ2F0ZVVybAUtfi9QdWJsaWMvRW1haWxHZXRQYXNzd2QuYXNweD9FSUQ9VHVyOHZadXVYa3M9ZGQYAQUeX19Db250cm9sc1JlcXVpcmVQb3N0QmFja0tleV9fFgEFDVZhbGlkYXRlSW1hZ2U=''',
                    '__EVENTVALIDATION':'/wEdAApk+MhPeRcW7LbXAbrLM7jrR1LBKX1P1xh290RQyTesRQa+ROBMEf7egV772v+RsRJUvPovksJgUuQnp+WD/+4LQKymBEaZgVw9rfDiAaM1opWKhJheoUmouOqQCzlwTSNWlQTw3DcvmMLY3PAqFoA+uFSTy5ozCEG4XBxL/Ykep0cgC/Irwlr9d8VObb8MnYO0GRqRfbdgDIW2dtIsr6rb',
                    '__ASYNCPOST':'true'
                   }
        code = self.getImageCodeAndCookie()
        postdata['ValidateCode']=code[:4]
        print 'postdata:',postdata
        postdata=urllib.urlencode(postdata)     # POST的数据
        myRequest = urllib2.Request(url = self.loginUrl,data = postdata)
        myRequest.add_header('Accept','*/*')
#       myRequest.add_header('Content-Length','792')
        myRequest.add_header('Accept-Encoding','gzip,deflate,sdch')
        myRequest.add_header('Accept-Language','zh-CN,zh;q=0.8')
        myRequest.add_header('Cache-Control','no-cache')
        myRequest.add_header('Connection','keep-alive')
        myRequest.add_header('Content-Type','application/x-www-form-urlencoded; charset=UTF-8')
#       myRequest.add_header('Cookie:LoginType','LoginType=1; ASP.NET_SessionId='+ self.sessionId)
        myRequest.add_header('Host','yjxt.bupt.edu.cn')
        myRequest.add_header('Origin','http://yjxt.bupt.edu.cn')
        myRequest.add_header('Referer','http://yjxt.bupt.edu.cn/UserLogin.aspx?exit=1')
        myRequest.add_header('User-Agent','Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 UBrowser/1.0.739.0 Safari/537.36')
        myRequest.add_header('X-MicrosoftAjax','Delta=true')
        myRequest.add_header('X-Requested-With','XMLHttpRequest')
        result = self.opener.open(myRequest).read()
        print result

注意在登录代码中注释了两条语句: 

myRequest.add_header('Content-Length','792')

Content-Length如果存在并且有效的话,则必须和消息内容的传输长度完全一致
。经过测试,如果过短则会截断,过长则会导致超时。如果我们设置的长度和我们发的消息内容传输长度不一致,就会请求失败。因此我们将它注释掉,交给python自动计算。

<pre name="code" class="python" style="font-size: 18px;">myRequest.add_header('Cookie:LoginType','LoginType=1; ASP.NET_SessionId='+ self.sessionId)

楼主当时是加上了这条语句,结果一直请求失败。后来注释掉以后就OK了。猜测是因为和opener自动填充的sessionID冗余了。


运行会发现出现如下图所示:

《python爬虫实例-爬取北邮研究生成绩》

我们这个登录请求是异步方式提交的,当返回‘pageRedirect’命令后,浏览器会自动跳转到成功页面。而python不是浏览器,不能跳转。事实上我们已经登陆成功了!直接open成绩查询的地址就可以了。

接下来我们就要看如何在登录后找到成绩单页面!

3、抓取成绩

我们登陆成功后会出现下图显示:

《python爬虫实例-爬取北邮研究生成绩》

注意到这是地址显示的为:http://yjxt.bupt.edu.cn/Gstudent/Default.aspx?UID=*******

我们点开成绩管理后:《python爬虫实例-爬取北邮研究生成绩》

地址还是:http://yjxt.bupt.edu.cn/Gstudent/Default.aspx?UID=******* (这个UID是我们登录时输入的用户名)

为啥没有变化呢?

其实是由于这个网页使用了iframe框架。一个浏览器窗体可以通过几个页面的组合来显示。iframe框架可以把HTML文档分为多个页面。这样重载页面时不需要重载整个页面,只需要重载页面中的一个框架页(减少了数据的传输,增加了网页下载速度)

在网页上右键-【查看网页源代码】(不要选择【查看框架源代码】)

《python爬虫实例-爬取北邮研究生成绩》

找到如下iframe标签:

《python爬虫实例-爬取北邮研究生成绩》


这个就是盛放整个成绩表格的框架。我们也可以通过【Elements】视图来获取他的地址:

《python爬虫实例-爬取北邮研究生成绩》

解决了这个问题,那我们需要的成绩呢?

别急,我们再用【Network】视图来找找这个成绩到底在哪里。 切换到【Network】视图,然后我们重新点击‘课程成绩查询’ ,然后看究竟发生什么。

《python爬虫实例-爬取北邮研究生成绩》

注意到,这里有一个请求:StudentScoreQuery.aspx?EID=l0RCAjrC!60Alnrcjky12 (除此之外的其他请求都是一些js,css文件我们就不分析了),点开这个请求就水落石出了:

《python爬虫实例-爬取北邮研究生成绩》

这个就是我们需要的数据。而他的完整请求是:http://yjxt.bupt.edu.cn/Gstudent/Course/StudentScoreQuery.aspx?EID=l0RCAjrC!60Alnrcjky12Ad6vU4OJDrqYylAGKDjRFO3OCFxhesOvg==&UID=*******(这个EID看着像是动态生成的数据,其实是静态的,不信可以拿不同的浏览器试一下。楼主是验证了好多次发现都没有变过,包括我们在构造表数据的时候,好多数据都是看着像动态,其实是静态的 –)

接下来就是从返回值中取出我们需要的数据来了。通过正则表达式,我们可以很容易的解决这个问题。(正则表达式写的有点丑,将就着用吧–)

def getScores(self):
        scorePage = self.opener.open(self.scoreUrl).read()
        print scorePage
        
        #统一编码格式
        charset = chardet.detect(scorePage)
        charset = charset['encoding']
        if charset !='utf-8' and charset !='UTF-8':
            scorePage = scorePage.decode('gb2312' , 'ignore').encode("utf-8")
        unicodePage = scorePage.decode('utf-8')
            
        pattern = re.compile('<td align="center">(\d*?)</td><td align="center">(.*?)</td><td align="center">(\d*?)</td><td align="center">(\d*?)</td><td align="center">(\d*?)</td><td align="center">(\d*?)</td><td align="center">(\d*?)</td><td align="center">(\d*?)</td><td align="center" nowrap="nowrap">(.*?)</td><td align="center" nowrap="nowrap">(.*?)</td><td align="center"> </td>')
        scores = pattern.findall(unicodePage)
        
        for eachScore in scores:
            score={}
            score['classNo']=eachScore[0]
            score['className']=eachScore[1]
            score['classHours']=eachScore[2]
            score['credit']=eachScore[3]
            score['term']=eachScore[4]
            score['final']=eachScore[5]
            score['complex']=eachScore[6]
            score['rank']=eachScore[7]
            score['family']=eachScore[8]
            score['attribute']=eachScore[9]
            self.scores.append(score)

取出的结果如下:

《python爬虫实例-爬取北邮研究生成绩》

自此,我们的程序就算完成了。

最后有个小插曲,在将解析好的验证码提交给服务器时,不小心将参数名写错了

postdata['ValidoteCode']=code[:4]

结果竟然登录成功了 。。。。。。。应该是因为服务器取’ValidateCode’参数的时候,如果能取到该值,则会将其进行验证。而如果取出为NULL的话,直接跳过了验证。忽略了该值为空的情况!

小小吐槽一下网站做得还是不够严谨。 —

程序写的不是很规范,刚接触python ,能力有限哎。。好多都是个人网上找的资料,加上一些跟人的理解,揣测啥的,有不对的地方欢迎批评指正,嘿!

完整代码

#!/usr/bin/env python
#coding=utf-8
import urllib2
import urllib
import re
import pytesser
import StringIO
import Image
import cookielib
import chardet
import time
class ScoreCrawl(object):
    def __init__(self , username = "*******" ,password='******'):
        self.username = username
        self.password = password
        self.loginUrl ='http://yjxt.bupt.edu.cn/UserLogin.aspx?exit=1'
        self.imageUrl ='http://yjxt.bupt.edu.cn/Public/ValidateCode.aspx?image=1052561647'
        self.scoreUrl ='http://yjxt.bupt.edu.cn/Gstudent/Course/StudentScoreQuery.aspx?EID=l0RCAjrC!60Alnrcjky12Ad6vU4OJDrqYylAGKDjRFO3OCFxhesOvg==&UID='+username
        self.cookieJar = cookielib.CookieJar()
        httpHandler = urllib2.HTTPHandler(debuglevel=1)  
        httpsHandler = urllib2.HTTPSHandler(debuglevel=1)  
        self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookieJar),httpHandler, httpsHandler)
        self.scores=[]
    
    def getImageCodeAndCookie(self):
        try:
            im = self.opener.open(self.imageUrl).read()       #获取验证码和cookies值
            img_buffer = StringIO.StringIO(im)
            img = Image.open(img_buffer)
            textcode = pytesser.image_to_string(img)
            print 'Cookies:'            
            for item in self.cookieJar:
                if item.name =='ASP.NET_SessionId':
                    self.sessionId = item.value
                print '    Name = '+item.name
                print '    Value = '+item.value  
            print 'ImageCode=',textcode
            return textcode
        except Exception as e:
            print 'Failed to get imagecode!', e
            return ''
    def login(self):
        postdata ={
                   'UserName':self.username,
                   'PassWord':self.password,
                   'drpLoginType':1,
                   'ScriptManager1':'UpdatePanel2|btLogin',
                    '__EVENTTARGET':'btLogin',
                    '__VIEWSTATE':'''/wEPDwULLTE3MzIzNjYwNjMPZBYCAgMPZBYGAg0PZBYCZg9kFgICAQ8PFgIeCEltYWdlVXJsBSp+L1B1YmxpYy9WYWxpZGF0ZUNvZGUuYXNweD9pbWFnZT0xOTg4NjIyMjlkZAIRD2QWAmYPZBYCAgEPEGRkFgFmZAIVD2QWAmYPZBYCAgEPDxYCHgtOYXZpZ2F0ZVVybAUtfi9QdWJsaWMvRW1haWxHZXRQYXNzd2QuYXNweD9FSUQ9VHVyOHZadXVYa3M9ZGQYAQUeX19Db250cm9sc1JlcXVpcmVQb3N0QmFja0tleV9fFgEFDVZhbGlkYXRlSW1hZ2U=''',
                    '__EVENTVALIDATION':'/wEdAApk+MhPeRcW7LbXAbrLM7jrR1LBKX1P1xh290RQyTesRQa+ROBMEf7egV772v+RsRJUvPovksJgUuQnp+WD/+4LQKymBEaZgVw9rfDiAaM1opWKhJheoUmouOqQCzlwTSNWlQTw3DcvmMLY3PAqFoA+uFSTy5ozCEG4XBxL/Ykep0cgC/Irwlr9d8VObb8MnYO0GRqRfbdgDIW2dtIsr6rb',
                    '__ASYNCPOST':'true'
                   }
        code = self.getImageCodeAndCookie()
        postdata['ValidateCode']=code[:4]
        print 'postdata:',postdata
        postdata=urllib.urlencode(postdata)     # POST的数据
        myRequest = urllib2.Request(url = self.loginUrl,data = postdata)
        myRequest.add_header('Accept','*/*')
#         myRequest.add_header('Content-Length','792')
        myRequest.add_header('Accept-Encoding','gzip,deflate,sdch')
        myRequest.add_header('Accept-Language','zh-CN,zh;q=0.8')
        myRequest.add_header('Cache-Control','no-cache')
        myRequest.add_header('Connection','keep-alive')
        myRequest.add_header('Content-Type','application/x-www-form-urlencoded; charset=UTF-8')
#         myRequest.add_header('Cookie:LoginType','LoginType=1; ASP.NET_SessionId='+ self.sessionId)
        myRequest.add_header('Host','yjxt.bupt.edu.cn')
        myRequest.add_header('Origin','http://yjxt.bupt.edu.cn')
        myRequest.add_header('Referer','http://yjxt.bupt.edu.cn/UserLogin.aspx?exit=1')
        myRequest.add_header('User-Agent','Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 UBrowser/1.0.739.0 Safari/537.36')
        myRequest.add_header('X-MicrosoftAjax','Delta=true')
        myRequest.add_header('X-Requested-With','XMLHttpRequest')
        result = self.opener.open(myRequest).read()
        print result
        
    def getScores(self):
        scorePage = self.opener.open(self.scoreUrl).read()
        
        #统一编码格式
        charset = chardet.detect(scorePage)
        charset = charset['encoding']
        if charset !='utf-8' and charset !='UTF-8':
            scorePage = scorePage.decode('gb2312' , 'ignore').encode("utf-8")
        unicodePage = scorePage.decode('utf-8')
            
        pattern = re.compile('<td align="center">(\d*?)</td><td align="center">(.*?)</td><td align="center">(\d*?)</td><td align="center">(\d*?)</td><td align="center">(\d*?)</td><td align="center">(\d*?)</td><td align="center">(\d*?)</td><td align="center">(\d*?)</td><td align="center" nowrap="nowrap">(.*?)</td><td align="center" nowrap="nowrap">(.*?)</td><td align="center"> </td>')
        scores = pattern.findall(unicodePage)
        
        for eachScore in scores:
            score={}
            score['classNo']=eachScore[0]
            score['className']=eachScore[1]
            score['classHours']=eachScore[2]
            score['credit']=eachScore[3]
            score['term']=eachScore[4]
            score['final']=eachScore[5]
            score['complex']=eachScore[6]
            score['rank']=eachScore[7]
            score['family']=eachScore[8]
            score['attribute']=eachScore[9]
            self.scores.append(score)
    
    def showScores(self):
        print '*'*200
        print '%30s%35s%32s%32s%32s%32s%32s%32s%30s%30s' % ('课程编号','课程名称','学时','学分','学期','期末','综合成绩','班级排名','类别','属性')
        for e in self.scores:
            print '%-30s%-35s%-32s%-32s%-32s%-32s%-32s%-32s%-30s%-30s' % (e['classNo'],e['className'],e['classHours'],e['credit'],e['term'],e['final'],e['complex'],e['rank'],e['family'],e['attribute'])
             
        
def main():
    scoreCrawl = ScoreCrawl()
    scoreCrawl.login()
    scoreCrawl.getScores()
    scoreCrawl.showScores()
    print '------'*20

if __name__ == '__main__':
    main()



    原文作者:张小琦
    原文地址: https://blog.csdn.net/zq602316498/article/details/37834103
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞