【Python】爬虫-搭配“一木记账”实现半自动记录校园卡消费记录

实现功能:

  1. 自动登入校园VPN
  2. 自动完成校园身份认证
  3. 进入校园卡消费记录页面自动爬取消费数据
  4. 对数据进行基本处理,标注消费类型等配合后续导入
  5. 将Excel数据自动发送至邮箱

思路

没啥思路,脚踩西瓜皮,滑到哪里算哪里

具体代码

导入库

import os
import re
import json
import xlwings as xw
import pandas as pd
from time import sleep
from math import ceil
from pathlib import Path  # 处理后缀名
from datetime import datetime  # 处理日期
from selenium import webdriver as wd  # 浏览器
from selenium.webdriver.common.by import By  # 浏览器
from selenium.webdriver.edge.options import Options  # 浏览器
from selenium.webdriver.support.wait import WebDriverWait  #侦测等待

#发送邮件相关
import smtplib
from email.header import Header
from email.utils import formataddr
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart

需要用到的一些全局变量

write_path = "import.xlsx"
new_format = "import.xls"
host = 'smtp.qq.com'
global driver, name, password, sender, passwd, receiver

获取个人信息

包括:

name 校园认证名
password 校园认证密码
sender 发送邮箱
passwd 邮箱密码
receiver 接收邮箱

首次运行需要手动输入,并保存在本地json文件中,以后运行直接读取json文件中的信息。

def get_info():
    """
    获得个人登录信息
    """
    global name, password, sender, passwd, receiver
    try:
        with open("info.json") as f:
            ls = json.load(f)
            name = ls[0]
            password = ls[1]
            sender = ls[2]
            passwd = ls[3]
            receiver = ls[4]

    except:
        name = input("请输入学号:")
        password = input("请输入密码:")
        sender = input("请输入发件人:")
        passwd = input("请输入邮箱授权码:")
        receiver = input("请输入收件人:")
        with open("info.json", "w+") as f:
            json.dump([name, password, sender, passwd, receiver], f)

打开浏览器

借鉴的网上的资料,具体原理我也不清楚

def edge():
    """
    打开Edge浏览器
    """
    global driver
    options = Options()  # 消除无用提示-“连到系统上的设备没有发挥作用。 (0x1F)”
    options.add_experimental_option("excludeSwitches",
                                    ["enable-automation", "enable-logging"])
    driver = wd.Edge(options=options)

登录校园VPN

通过网页认证实现登入VPN

还需要实现在完成登入后等待页面跳转结束再进入下一个步骤,否则可能出现登录VPN失败的情况。没有想到什么好方法,只能退而求其次强制等待5秒了。

def LoginVPN():
    """
    登录校园VPN
    """
    url = "https://***.*****.edu.cn/por/login_psw.csp?rnd=0."
    driver.get(url)
    sleep(0.3)
    try:
        driver.find_element(By.ID, "svpn_name").send_keys(name)
        sleep(0.1)
        driver.find_element(By.ID, "svpn_password").send_keys(password)
        sleep(0.1)
        driver.find_element(By.ID, "logButton").click()
        sleep(5)
        """ 此处应该实现监测直到登录成功后再跳转页面 亟需改进以降低不必要的等待时间"""
    except:
        sleep(0.5)

 统一身份认证

def LoginXXXX():
    """
    统一身份认证
    """
    url = "https://ids.*****.edu.cn/authserver/login"
    driver.get(url)
    sleep(0.3)
    try:
        driver.find_element(By.ID, "username").send_keys(name)
        sleep(0.1)
        driver.find_element(By.ID, "password").send_keys(password)
        sleep(0.1)
        driver.find_element(
            By.XPATH,
            '// *[@id="casLoginForm"]/p[4]/div/ins').click()  # 一周内免密登录
        sleep(0.1)
        driver.find_element(By.XPATH,
                            '//*[@id="casLoginForm"]/p[5]/button').click()
        sleep(0.5)
    except:
        sleep(0.5)

进入消费数据页面

def Ecard():
    """
    登录一卡通,并进入数据页面
    """
    url = "http://ecard.*****.edu.cn"
    driver.get(url)
    sleep(1)
    driver.switch_to.frame("leftFrame")
    driver.find_element(
        By.XPATH, "/html/body/table/tbody/tr/th/table/tbody/tr[3]/th[2]/a"
    ).click()  # 历史流水查询
    driver.switch_to.default_content()
    driver.switch_to.frame("middlemainFrame")  # 切换框架
    driver.find_element(
        By.XPATH,
        '//*[@id="accounthisTrjn1"]/table/tbody/tr/td/table[2]/tbody/tr/th/table/tbody/tr[3]/td/div/input',
    ).click()  # 确定
    sleep(0.5)
    driver.find_element(
        By.XPATH,
        '//*[@id="accounthisTrjn2"]/table[2]/tbody/tr/th/table/tbody/tr[1]/th[3]/div',
    ).click()  # 一月内
    sleep(2.5)

获取交易总次数

方便后续计算翻页次数,感觉应该可以合并到其他函数中

def get_transaction_num():
    """
    获取交易总次数
    """
    bottom_data = driver.find_element(
        By.XPATH, '//*[@id="tables"]/tbody/tr[19]/td/div').text  # 表格底部信息
    num = re.compile(":(.+)次交易").findall(bottom_data)  # 正则取值
    return int(num[0])

 主函数

感觉我注释里写的挺清楚的,有几个用到的函数看后面的介绍,实际是写在main函数前的

另外说一下这行里的19和17,是肉眼观察手动数出来的,没什么特别来历

row = 19 if i < n else num + 19 - 17 * n
def main(num):
    """
    主函数:
    num为总交易次数
    导出关键消费数据
    并同时写入数据表
    """
    count = 0  # 序号
    col = [1, 5, 6]  # 易知,只需要采集这4列的数据即可,并按类型、时间、名称即地点、交易额的顺序采集
    n = ceil(num / 17)  # 总的大循环次数

    for i in range(1, n + 1):
        page_record = []  # 储存每页的记录
        row = 19 if i < n else num + 19 - 17 * n  # 控制每页数据条数,前n-1页均为17条/页,特殊处理最后一页
        for j in range(2, row):
            record = [0] * 10  # 储存一条记录,即一行。易知,第5678个元素暂时无用,可用于临时储存数据
            count += 1
            print(f"{i}-{j-1}-{count}\t", end="")
            data = driver.find_elements(
                By.XPATH, f'//*[@id="tables"]/tbody/tr[{j}]/td[{4}]')[0].text
            record[5] = data
            if data == "持卡人消费" or data == "代扣代缴":
                # 只收录这两种类型的消费,一般来说即为伙食费与水费
                print(f"{data}\t", end="")
                for k, l in zip(col, [6, 7, 8]):
                    data = driver.find_elements(
                        By.XPATH,
                        f'//*[@id="tables"]/tbody/tr[{j}]/td[{k}]')[0].text
                    record[l] = data  # 获取单元格的值
                    print(f"{data}\t", end="")
                    sleep(0.05)
                record_process(record)
                page_record.append(record)
            else:
                print("")
                continue
            print("")
        print("\n")
        write_data(page_record)  # 以页为单位写入数据

        if i == 1:  # 第一页时
            driver.find_elements(
                By.XPATH,
                '//*[@id="tables"]/tbody/tr[19]/td/div/a[1]')[0].click()  # 下一页
        elif i == n:  # 最后一页时不点击
            continue
        else:  # 除过第一页与最后一页
            driver.find_elements(
                By.XPATH,
                '//*[@id="tables"]/tbody/tr[19]/td/div/a[3]')[0].click()  # 下一页
        sleep(0.5)

数据处理

主要是水费和一日三餐的费用

def record_process(record):
    """
    处理每行的数据
    """
    record[0] = record[6]  # 时间
    record[1] = "支出" if float(record[8]) < 0 else "收入"  # 支出/收入
    record[2] = float(record[8])  # 金额
    record[3] = "食品餐饮" if record[5] == "持卡人消费" else "居家生活"  # 类别
    if record[3] == "居家生活":  # 子类
        record[4] = "水费"
    else:
        # 2023/02/12 07:50:50
        hour = int(record[6][11:13])
        if 6 <= hour <= 10:
            record[4] = "早餐"
        elif 16 <= hour <= 20:
            record[4] = "晚餐"
        else:
            record[4] = "午餐"
    record[5] = "日常账本"  # 所属账本
    record[6] = "一卡通"  # 收支账户
    record[9] = record[7]  # 地址
    record[7] = ""  # 备注
    record[8] = ""  # 标签
    return record

写入数据

def write_data(record):
    """
    写入数据
    """
    app = xw.App(visible=False, add_book=False)
    try:
        work_book = app.books.open(write_path)
        work_sheet = work_book.sheets["Sheet1"]
        nrow, ncol = work_sheet.used_range.shape
        work_sheet.range(nrow + 1, 1).value = record
        work_book.save()
        sleep(1)
        work_book.close()
        app.kill()
    except:
        title = [[
            "日期", "收支类型", "金额", "类别", "子类", "所属账本", "收支账户", "备注", "标签", "地址"
        ]]
        new_work_book = app.books.add()
        work_sheet = new_work_book.sheets["Sheet1"]
        work_sheet.range(1, 1).value = title
        new_work_book.save(write_path)
        new_work_book.close()
        app.quit()
        write_data(record)

删除重复数据

每次在主函数运行完成后执行一次

def delete_repeat():
    """
    删除重复数据
    """
    data = pd.read_excel(write_path, sheet_name=0)
    data.drop_duplicates().to_excel(write_path, header=True, index=None)

更改文件类型

因为“一木记账”导入支持xls文件导入,但是使用xlwings库又只能操作xlsx文件,所以需要把xlsx转换成xls格式,在删除完重复数据后执行

def change_format():
    '''
    更改文件类型
    '''
    app = xw.App(visible=False, add_book=False)
    work_book = app.books.open(write_path)
    work_book.save(new_format)
    sleep(1)
    work_book.close()
    app.kill()

发送邮件

直接抄的其他人的代码,没有深究原理

def send_email():
    '''
    发送邮件
    '''
    msg = MIMEMultipart('mixed')
    msg['From'] = formataddr(['发送账本专用', sender])
    msg['Subject'] = "账本导入专用"  # 邮件主题

    # 添加附件 - excel
    att_excel = MIMEText(open(r'import.xls', 'rb').read(), 'base64', 'utf-8')
    att_excel["Content-Type"] = 'application/octet-stream'
    att_excel[
        "Content-Disposition"] = 'attachment; filename="AccountBookImport.xls"'
    msg.attach(att_excel)

    try:
        # 创建并登录SMTP服务器
        server = smtplib.SMTP_SSL(host, 465)
        server.login(sender, passwd)
        server.sendmail(sender, receiver, msg.as_string())
        server.quit()
        print('邮件发送成功!')
    except smtplib.SMTPException as e:
        print('Error: 邮件发送失败!', e)

运行

if __name__ == "__main__":
    get_info()
    edge()
    LoginVPN()
    Login****()
    Ecard()
    num = get_transaction_num()
    main(num)
    delete_repeat()
    change_format()
    send_email()

最后

一开始写的挺顺利的,应该不到一周就较完美的实现了把数据输出在屏幕。之后开始一路踩坑。照着两本书断断续续写了两周基本实现能把数据写入到表格里了,虽然有几个挺大的bug,所以也因为还有其他事情要忙就没继续处理。之后过了好像有快一个月继续写,终于实现了除了自动发邮件外的所有功能。然后又过了几周直接白嫖前人资料写完自动发邮件。完结撒花(虽然还有几个地方需要改进

运行过程截图以后慢慢再补吧

另外记录一下自己像个大傻子一样把自己的账号密码写进代码里面然后扔到码云里面开源了,仓库被一个人复制两天后才发现

posted @ 2023-05-18 23:33  闲登小阁看新晴  阅读(128)  评论(0)    收藏  举报