Exemple #1
0
    def login(self, account, password):
        """
        登录校园卡网站
        :param str account: 校园卡卡号
        :param str password: 校园卡查询密码
        :return: {'code': 1, "msg": "登录失败,请重试"} 或 {'code': 0, 'msg': '登录成功'}
        """
        captcha_code = CardCaptcha(self.get_image(URL.card_captcha())).crack()
        data = {
            "sno": account,
            "pwd": base64.b64encode(str(password).encode("utf8")),
            "ValiCode": captcha_code,
            "remember": 1,
            "uclass": 1,
            "zqcode": "",
            "json": True,
        }

        result = self._login_execute(url=URL.card_login(), data=data)
        if result['code'] == 2:
            # 如果验证码错误,尝试递归重复登录
            return self.login(account, password)
        result['success'] = not result['code']
        if result['success']:
            self.verify = True
        else:
            raise AuthenticationException(result['msg'])
        return result
Exemple #2
0
 def login(self, account, password):
     """
     登录教务系统 jwxt.njupt.edu.cn
     :param account: 南邮学号
     :param password: 密码
     :return: {'code': 1, "msg": "登录失败"} 或 {'code': 0, 'msg': '登录成功'}
     """
     captcha_code = ZhengfangCaptcha(self.get_image(
         URL.zhengfang_captcha())).crack()
     data = {
         "__VIEWSTATE": self._get_viewstate(URL.zhengfang_login()),
         'txtUserName': account,
         'TextBox2': password,
         'RadioButtonList1': "%D1%A7%C9%FA",
         "txtSecretCode": captcha_code,
         "Button1": "",
         "hidPdrs": "",
         "hidsc": ""
     }
     result = self._login_execute(url=URL.zhengfang_login(), data=data)
     if result['code'] == 2:
         # 如果验证码错误,尝试递归重复登录
         return self.login(account, password)
     result['success'] = not result['code']
     if result['success']:
         self.verify = True
     else:
         raise AuthenticationException(result['msg'])
     return result
Exemple #3
0
 def _get_captcha(self, url=URL.jwxt_captcha()):
     r = self.get(url)
     im = Image.open(BytesIO(r.content))
     if url == URL.aolan_captcha():
         return str(AolanCaptcha(im))
     if url == URL.jwxt_captcha():
         return str(ZhengfangCaptcha(im))
Exemple #4
0
 def get_score(self):
     """
     获取课程成绩和绩点等信息
     :return: dict 
             {'gpa': 4.99,
                 'coursers': [{
                     'year': '2015-2016',
                     'semester': '1',
                     'code': '00wk00003',
                     'name': '从"愚昧"到"科学"-科学技术简史',
                     'attribute': '任选',
                     'belong': '全校任选课',
                     'credit': '2.0',
                     'point': '',
                     'score': '81',
                     'minorMark': '0',
                     'makeUpScore': '',
                     'retakeScore': '',
                     'college': '网络课程',
                     'note': '',
                     'retakeMark': '0',
                     'englishName': ''
                     }, 
                     ]
                 }
     """
     viewstate = self._get_viewstate(url=URL.jwxt_score(self.account))
     data = {
         'ddlXN': '',
         'ddlXQ': '',
         '__VIEWSTATE': viewstate,
         'Button2': '%D4%DA%D0%A3%D1%A7%CF%B0%B3%C9%BC%A8%B2%E9%D1%AF'
     }
     soup = self._url2soup(method='post',
                           url=URL.jwxt_score(self.account),
                           data=data)
     result = {'gpa': float(soup.select_one('#pjxfjd > b').text[7:])}
     cols = [
         'year', 'semester', 'code', 'name', 'attribute', 'belong',
         'credit', 'point', 'score', 'minorMark', 'makeUpScore',
         'retakeScore', 'college', 'note', 'retakeMark', 'englishName'
     ]
     #  学年 学期 课程代码 课程名称 课程性质 课程归属 学分 绩点 成绩 辅修标记 补考成绩 重修成绩 开课学院 备注 重修标记 英文名称
     coursers = []
     for tr in soup.select('#Datagrid1  > tr')[1:]:
         courser = {}
         for col, td in zip(cols, tr.select('td')):
             value = td.text.strip()
             courser[col] = value
         coursers.append(courser)
     result['coursers'] = coursers
     return result
Exemple #5
0
 def get_score(self):
     """
     获取课程成绩和绩点等信息
     :return: dict 
             {'gpa': 4.99,  # GPA
                 'courses': [{
                     'year': '2015-2016',  # 修读学年
                     'semester': '1',  # 修读学期
                     'code': '00wk00003',  # 课程编号
                     'name': '从"愚昧"到"科学"-科学技术简史',  # 课程中文名
                     'attribute': '任选',  # 课程性质
                     'belong': '全校任选课',  # 课程归属
                     'credit': '2.0',  # 学分
                     'point': '',  # 绩点
                     'score': '81',  # 成绩
                     'minorMark': '0',  # 重修标记
                     'make_up_score': '',  # 补考成绩
                     'retake_score': '',  # 重修成绩
                     'college': '网络课程',  # 开课学院
                     'note': '',  # 备注
                     'retake_mark': '0', # 重修标记
                     'english_name': ''  # 课程英文名
                     }, 
                 ]
             }
     """
     viewstate = self._get_viewstate(url=URL.zhengfang_score(self.account))
     data = {
         'ddlXN': '',
         'ddlXQ': '',
         '__VIEWSTATE': viewstate,
         'Button2': '%D4%DA%D0%A3%D1%A7%CF%B0%B3%C9%BC%A8%B2%E9%D1%AF'
     }
     soup = self.get_soup(method='post',
                          url=URL.zhengfang_score(self.account),
                          data=data)
     result = {'gpa': float(soup.select_one('#pjxfjd').text[7:])}
     cols = [
         'year', 'semester', 'code', 'name', 'attribute', 'belong',
         'credit', 'point', 'score', 'minor_mark', 'make_up_score',
         'retake_score', 'college', 'note', 'retake_mark', 'english_name'
     ]
     courses = []
     for tr in soup.select('#Datagrid1  > tr')[1:]:
         courser = {}
         for col, td in zip(cols, tr.select('td')):
             value = td.text.strip()
             courser[col] = value
         courses.append(courser)
     result['courses'] = courses
     return result
Exemple #6
0
 def login(self, account, password):
     """
     登录南邮图书馆 jwxt.njupt.edu.cn
     :param account: 南邮学号
     :param password: 密码
     :return: {'r': 1, "msg": "登录失败"} 或 {'r': 0, 'msg': '登录成功'}
     """
     data = {
         "number": account,
         'passwd': password,
         'captcha': self._get_captcha(URL.lib_captcha()),
         'select': "cert_no",
         "returnUrl": "",
     }
     return self._login_execute(url=URL.jwxt_login(), data=data)
Exemple #7
0
 def recharge_net(self, amount=0.01):
     """
     充值网费(从校园卡余额中)
     :param amount: 金额 如 2.33
     :return: dict
         {
             'success':True, 
             'code' : 0,
             'Msg' : '充值成功'
         }
     """
     data = {
         'paytype': 1,
         'aid': AIDS['net'],
         'account': self._get_inner_account(),
         'tran': int(amount * 100),
         'netacc': '{"netacc": "", "bal": null, "pkgid": null, "lostflag": null, "freezeflag": null, '
                   '"expflag": null,"statustime": null, "duration": null, "starttime": null, "pkgtab": []}',
         'pkgflag': 'none',
         'pkg': '{}',
         'acctype': '###',
         'qpwd': '',
         'json': True
     }
     result = self._url2json(url=URL.card_net_pay(), method='post', data=data)
     result = json.loads(result['Msg'])
     return {
         'success': not int(result['pay_net_gdc']['retcode']),
         'code': int(result['pay_net_gdc']['retcode']),
         'msg': result['pay_net_gdc']['errmsg']
     }
Exemple #8
0
    def _recharge_electricity(self, amount, elec_aid, building_id, building,
                              room_id):
        data = {
            "acctype": "###",
            "paytype": 1,
            "aid": elec_aid,
            "account": self._get_inner_account(),
            "tran": int(amount * 100),
            "roomid": room_id,
            "room": "",
            "floorid": "",
            "floor": "",
            "buildingid": building_id,
            "building": building,
            "areaid": "",
            "areaname": "",
            "json": True
        }

        r = json.loads(
            self.get_json(URL.card_elec_pay(), 'post', data=data)['Msg'])
        return {
            'success': not bool(int(r['pay_elec_gdc']['retcode'])),
            'code': int(r['pay_elec_gdc']['retcode']),
            'msg': r['pay_elec_gdc']['errmsg'],
        }
Exemple #9
0
 def _get_viewstate(self, url=None):
     response = self.get(url or URL.jwxt_login())
     soup = BeautifulSoup(response.content, "lxml")
     viewstate = soup.find('input', attrs={
         "name": "__VIEWSTATE"
     }).get("value")
     return viewstate
Exemple #10
0
 def recharge(self, amount=0.01):
     """
     从绑定的银行卡中扣款充值余额
     :param amount: 充值金额,默认为0.01元
     :return: dict
         {   
             'success':True  # 转账是否成功
             'code': 0,  # 状态码
             'msg': '转账成功'  # 附加信息
         }
     """
     data = {
         'account': self._get_inner_account(),
         'acctype': '###',
         'tranamt': int(100 * amount),
         'qpwd': '',
         'paymethod': '2',
         'paytype': '使用绑定的默认账号',
         'client_type': 'web',
         'json': True,
     }
     info = self.get_json(URL.card_recharge(), 'post', data)
     info = json.loads(info['Msg'])['transfer']
     result = {
         'code': int(info['retcode']),
         'msg': info['errmsg'],
         'success': not bool(int(info['retcode']))
     }
     return result
Exemple #11
0
 def get_bill(self,
              start_date=(datetime.datetime.now() -
                          datetime.timedelta(days=30)).strftime("%Y-%m-%d"),
              end_date=datetime.datetime.now().strftime("%Y-%m-%d"),
              rows=100,
              page=1):
     """
     查询校园卡消费记录,默认为最近一个月的消费记录
     :param start_date: 查询区间的开始时间 "2017-12-27"
     :param end_date:  查询区间的结束时间 "2018-01-26"
     :param rows: 一次查询返回的最大记录数量,默认为100条记录
     :param page: 如果结果数量有多页,决定返回第几页。
     :return: list 
         {'recodes': 
             [{'balance': 39.71,  # 余额
               'change': -5, # 变动
               'comment': '未知系统,交电费', # 注释
               'department': '仙林售电处', # 操作部门
               'time': '2018-01-26 20:55:40', # 时间
               'type': '代扣代缴', # 类型
               'week': '星期五'}, # 星期
              {'balances': 39.71,
               'change': -7.5,
               'comment': '',
               'department': '一餐厅二楼清真食堂',
               'time': '2018-01-24 17:09:36',
               'type': '持卡人消费',
               'week': '星期三'}
                ... ],
         'total': 52 # 总的记录数
         'total_pages':2 # 总页数
         'page':1  # 当前的页码
         }
     """
     data = {
         "sdate": start_date,
         "edate": end_date,
         "account": self._get_inner_account(),
         "page": page,
         "rows": rows
     }
     temp = self.get_json(url=URL.card_bill(), method="post", data=data)
     result = {
         'total': temp['total'],
         'recodes': [],
         'total_pages': temp['total'] // rows + 1,
         'page': page
     }
     if temp['rows']:
         for row in temp['rows']:
             result['recodes'].append({
                 'balance': row['CARDBAL'],
                 'department': row['MERCNAME'].strip(),
                 'comment': row['JDESC'].strip(),
                 'change': row['TRANAMT'],
                 'week': row['XQ'],
                 'time': row['OCCTIME'],
                 'type': row['TRANNAME'].strip(),
             })
     return result
Exemple #12
0
 def login(self, account, password):
     """
     登录奥兰系统 jwxt.njupt.edu.cn
     :param account: 南邮学号、考生号、身份证号
     :param password: 密码
     :return: {'r': 1, "msg": "登录失败"} 或 {'r': 0, 'msg': '登录成功'}
     """
     data = {
         "__VIEWSTATE": self._get_viewstate(URL.aolan_login()),
         '__VIEWSTATEGENERATOR':
         self._get_viewstategenerator(URL.aolan_login()),
         'userbh': account,
         'pas2s': hashlib.md5(password.upper().encode('utf8')).hexdigest(),
         "vcode": self._get_captcha(URL.aolan_captcha()),
         "cw": "",
         "xzbz": "1",
     }
     return self._login_execute(url=URL.aolan_login(), data=data)
Exemple #13
0
 def login(self, account, password):
     """
     登录教务系统 jwxt.njupt.edu.cn
     :param account: 南邮学号
     :param password: 密码
     :return: {'r': 1, "msg": "登录失败"} 或 {'r': 0, 'msg': '登录成功'}
     """
     data = {
         "__VIEWSTATE": self._get_viewstate(),
         'txtUserName': account,
         'TextBox2': password,
         'RadioButtonList1': "%D1%A7%C9%FA",
         "txtSecretCode": self._get_captcha(URL.jwxt_captcha()),
         "Button1": "",
         "hidPdrs": "",
         "hidsc": ""
     }
     return self._login_execute(url=URL.jwxt_login(), data=data)
Exemple #14
0
 def _get_inner_account(self):
     """
     获取对应的内部账号,部分接口参数需要
     :return: 校园卡号对应的系统内部账号 
     """
     if not self.inner_account:
         info = self._url2json(URL.card_info(), method='post', data={'json': True})
         result = json.loads(info['Msg'])
         self.inner_account = result['query_card']['card'][0]['account']
     return self.inner_account
Exemple #15
0
 def get_courses(self):
     """
     获取这学期的选课情况
     :return: 
     
     """
     soup = self.get_soup(method='get',
                          url=URL.zhengfang_courses(self.account))
     trs = soup.select('#DBGrid > tr')[1:]
     courses = []
     for tr in trs:
         tds = [node.text.strip() for node in tr.select('td')]
         name = tds[2]
         teacher = tds[5]
         all_time = tds[8].split(';')
         all_room = tds[9].split(';')
         for time, room in zip(all_time, all_room):
             if time and room:
                 week_start, week_end = map(int,
                                            week_re.search(time).groups())
                 courser_index = list(
                     map(
                         int,
                         courser_indexs_re.search(time).groups()[0].split(
                             ',')))
                 week = re.search('{(.*)}', time).groups()[0]
                 if '双周' in week and week_start % 2 == 1:
                     week_start += 1
                 if '单周' in week and week_start % 2 == 0:
                     week_start += 1
                 courses.append({
                     'day':
                     chinese_rome[time[1]],
                     'name':
                     name,
                     'week':
                     week,
                     'week_start':
                     week_start,
                     'week_end':
                     week_end,
                     'class_start':
                     courser_index[0],
                     'class_end':
                     courser_index[-1],
                     'teacher':
                     teacher,
                     'room':
                     room,
                     'interval':
                     2 if '单周' in week or '双周' in week else 1,
                 })
     return courses
Exemple #16
0
 def wrapper(self, *args, **kwargs):
     if self.verify:  # 利用状态量进行状态的判定
         return func(self, *args, **kwargs)
     else:
         # 先判断有没有cookie文件, 再判断cookie是否有效
         if 'WEB' in requests.utils.dict_from_cookiejar(self.cookies):
             # 判断教务系统是否登录成功,根据正方的一个地址无需参数的地址
             r = self.get_soup(method="get", url=URL.zhengfang_logintest())
             if 'Object moved to ' not in r.text:
                 self.verify = True
                 return func(self, *args, **kwargs)
         raise UnauthorizedError('未登录正方系统,请先登录')
Exemple #17
0
 def get_schedule(self, week, year=None, semester=None):
     """
     获取指定学期指定周的课表(不传入年份和学期则默认当前学期)
     :param year: 学年 格式为 "2017-2018"
     :param semester: 学期 数字1或2
     :param week: 周次 数字 1-20
     :return: 二维列表schedule,schedule[i][j]代表周i第j节课的课程。 为了方便,i或j为零0的单元均不使用。
             列表的元素为None,代表没有课程,或描述课程信息的dict,dict例如
             {
                 'classroom': '教4-202', 
                 'name': '技术经济学', 
                 'teacher': '储成祥'
             }
     """
     schedule = [[None for col in range(13)] for row in range(8)]
     if year and semester:
         pass
     else:
         r = self.get(url=URL.zhengfang_class_schedule(self.account))
         soup = BeautifulSoup(r.text.replace('<br>', '\n'), 'lxml')
         trs = soup.select("#Table1 > tr")
         for index, tr in enumerate(trs):
             tds = tr.select('td')
             for td in tds:
                 if len(td.text) > 4:  # 筛选出包含课程信息的单元格
                     for courser in td.text.split('\n\n'):
                         info = courser.split()
                         start_week, end_week = map(
                             int,
                             week_re.search(info[1]).groups())
                         courser_index = map(
                             int,
                             courser_indexs_re.search(
                                 info[1]).groups()[0].split(','))
                         is_odd_week_only = "单周" in info[1]
                         is_even_week_only = "双周" in info[1]
                         week_day = chinese_rome[info[1][1]]
                         courser = {
                             'name': info[0],
                             'teacher': info[2],
                             'classroom': info[3],
                         }
                         if start_week <= week <= end_week:
                             if (week % 2 == 0 and is_odd_week_only) or (
                                     week % 2 == 1 and is_even_week_only):
                                 pass
                             else:
                                 for courser_index in courser_index:
                                     schedule[week_day][
                                         courser_index] = courser
     return schedule
Exemple #18
0
 def get_net_balance(self):
     """
     获取Dr.com的上网费用余额
     :return: float  2.33
     """
     data = {
         'jsondata': '{"query_net_info": {"aid": "%s", "account": "%s", "payacc": ""}}'  # 无法使用format格式化
                     % (AIDS['net'], self._get_inner_account()),
         'funname': 'synjones.onecard.query.net.info',
         'json': True,
     }
     result = self._url2json(url=URL.card_common(), data=data, method='post')
     result = json.loads(result['Msg'])
     return float(re.search(r'余额(\d*\.\d*)元', result['query_net_info']['errmsg']).groups()[0])
Exemple #19
0
 def _get_build_ids(self, aid):
     """获取建筑的id"""
     data = {
         'jsondata': '{"query_elec_building": {"aid": "%s", "account": "%s", "area": {"area": "", "areaname": ""}}}'
                     % (aid, self._get_inner_account()),
         'funname': 'synjones.onecard.query.elec.building',
         'json': True,
     }
     result = self._url2json(data=data, url=URL.card_common(), method='post')
     result = json.loads(result['Msg'])
     # - -!
     building_id = {}
     for build in result['query_elec_building']['buildingtab']:
         building_id[build['building']] = build['buildingid']
     return building_id
 def wrapper(self, *args, **kwargs):
     if self.verify:  # 利用状态量进行状态的判定
         return func(self, *args, **kwargs)
     else:
         # 先判断有没有cookie文件, 再判断cookie是否有效
         if 'WEB' in requests.utils.dict_from_cookiejar(self.cookies):
             # 判断教务系统是否登录成功,根据正方的一个地址无需参数的地址
             r = self._url2soup(method="get", url=URL.jwxt_logintest())
             if 'Object moved to ' not in r.text:
                 self.verify = True
                 return func(self, *args, **kwargs)
         while True:  # 运行到此处代表cookies无效
             data = self.login(self.account, input('输入密码:'))
             if data.get('r') == 0:
                 self.verify = True
                 return func(self, *args, **kwargs)
             else:
                 print(data.get('msg'))
Exemple #21
0
 def wrapper(self, *args, **kwargs):
     success = False
     # 先判断有没有cookie文件, 再判断cookie是否有效
     if 'WEB' in requests.utils.dict_from_cookiejar(self.cookies):
         # 判断教务系统是否登录成功,根据正方的一个地址无需参数的地址
         r = self._url2soup(method="get", url=URL.jwxt_logintest())
         if 'Object moved to ' not in r.text:
             success = True
     while not success:
         account = input("请输入教务系统账号:")
         self.account = account
         password = input("请输入账号密码:")
         data = self.login(account, password)
         if data.get("r") == 0:
             success = True
         else:
             print(data.get("msg"))
     else:
         return func(self, *args, **kwargs)
Exemple #22
0
 def get_class_schedule(self, week, year=None, semester=None):
     """
     获取指定学期指定周的课表(不传入年份和学期则默认当前学期)
     :param year: 学年 格式为 "2017-2018"
     :param semester: 学期 数字1或2
     :param week: 周次 数字 1-20
     :return: 
     """
     if year and semester:
         pass
     else:
         r = self.get(url=URL.jwxt_class_schedule(self.account))
         soup = BeautifulSoup(r.text.replace('<br>', '\n'), 'lxml')
         trs = soup.select("#Table1 > tr")
         schedule = {}
         for tr in trs:
             tds = tr.select('td')
             for td in tds:
                 print(td.text.split())
Exemple #23
0
 def get_balance(self):
     """
     查询余额
     :return: dict
             {
                 'balance': 10.01,  # 到账余额
                 'unsettle_balance': 0.01  # 过渡余额
                 'total': 10.02  # 总余额
             }
     """
     info = self._url2json(URL.card_info(), method='post', data={'json': True})
     result = json.loads(info['Msg'])
     balance = int(result['query_card']['card'][0]['db_balance']) / 100  # 到账余额
     unsettle_balance = int(result['query_card']['card'][0]['unsettle_amount']) / 100  # 过渡余额
     return {
         'balance': balance,  # 到账余额
         'unsettle_balance': unsettle_balance,  # 过渡余额
         'total': balance + unsettle_balance,  # 总余额
     }
Exemple #24
0
 def get_grade(self):
     """
     获取等级考试成绩信息
     :return: list
             [
                 {'date': '20151219',  # 考试日期
                  'name': '全国大学英语四级考试',  # 考试名称
                  'number': '320082152113313',  # 准考证号
                  'score': '547',  # 成绩
                  'semester': '1',  # 学期
                  'year': '2015-2016'  # 学年
                  },
             ]
     """
     soup = self.get_soup(method='get',
                          url=URL.zhengfang_grade(self.account))
     results = []
     for tr in soup.select("#DataGrid1 > tr")[1:]:
         names = ['year', 'semester', 'name', 'number', 'date', 'score']
         result = {}
         for name, td in zip(names, tr.select('td')[:6]):
             result[name] = td.text
         results.append(result)
     return results
Exemple #25
0
        :param data_type:    提交的数据格式(可能是表单类型,也可能是json格式的字符串)
        :param kwargs:  requests支持的参数,比如可以设置代理参数
        :return: BeautifulSoup对象
        """
        # r = getattr(self, method)(url, json=json, data=data, params=params, **kwargs)
        # if r.ok:
        #     soup = BeautifulSoup(r.text, 'lxml')
        #     return soup
        # else:
        #     raise ConnectionError("检查网络连接")

        try:  # 出现网络连接问题,直接在该处抛出错误
            r = getattr(self, method)(url,
                                      json=json,
                                      data=data,
                                      params=params,
                                      **kwargs)
        except ConnectionError:
            raise ConnectionError("请检查网络连接")
        else:
            if r.ok:  # 状态码小于400为True
                soup = BeautifulSoup(r.text, 'lxml')
                return soup
            else:  # 处理其他状态码
                raise Exception('请确保能够正常访问当前页面: {}'.format(url))


if __name__ == "__main__":
    test_model = Model()
    test_model._get_captcha(URL.aolan_captcha())
Exemple #26
0
手动获取奥兰系统验证码的训练集
"""
import hashlib
import os
import time
from io import BytesIO

import requests
from PIL import Image

from njupt.settings import BASE_DIR
from njupt.urls import URL
from njupt.utils.aolan.aolan_captcha import letters

for i in range(100):
    r = requests.get(URL.aolan_captcha())
    im = Image.open(BytesIO(r.content))
    # 灰度处理
    imgry = im.convert('L')

    # 二值处理
    threshold = 220
    table = []
    for i in range(256):
        if i < threshold:
            table.append(0)
        else:
            table.append(1)
    out = imgry.point(table, '1')
    out.show()
    numbers = input("输入数值")
Exemple #27
0
 def _get_captcha(self, url=URL.jwxt_captcha()):
     r = self.get(url)
     im = Image.open(BytesIO(r.content))
     im.show()
     captcha = input("输入验证码:")
     return captcha
    def crack(self):
        result = []
        v = VectorTools()
        # 装载训练数据集
        with open(os.path.join(current_dir, 'imageset.dat'), 'rb+') as f:
            imageset = pickle.load(f)
        for letter in self.handle_split_image():
            guess = []
            for image in imageset:
                for x, y in image.items():
                    if len(y) != 0:
                        guess.append((v.relation(y[0], self.buildvector(letter)), x))
            guess.sort(reverse=True)
            neighbors = guess[:10]  # 距离最近的十个向量
            class_votes = {}  # 投票
            for neighbor in neighbors:
                class_votes.setdefault(neighbor[-1], 0)
                class_votes[neighbor[-1]] += 1
            sorted_votes = sorted(class_votes.items(), key=lambda x: x[1], reverse=True)
            result.append(sorted_votes[0][0])
        return ''.join(result)

    def __str__(self):
        return self.crack()


if __name__ == "__main__":
    r = requests.get(URL.zhengfang_captcha())
    im = Image.open(BytesIO(r.content))
    print(ZhengfangCaptcha(im))