mr88fang 发表于 2024-11-4 15:11

AutoX 脚本导出微信账单



微信是支持账单导出的(**钱包**->**账单**->**下载账单**->**用于个人对账**),有时间和格式限制。使用脚本没有时间和格式限制,需要指定账单截止日期即可。

## 前提条件

1. 你需要一个安卓手机
2. 你需要安装 (https://github.com/kkevsekk1/AutoX/releases)
3. 你需要一点动手能力
4. 手机需要打开无障碍功能才能执行脚本

如果以上条件不能满足也没关系,网上冲浪也是可以的。

## 功能列表

- 导出格式
- csv (默认)
- json
- txt (微信默认格式)
- md
- 截止时间(该时间段停止)
- 示例:2024年10月,和账单筛选时间范围一致


## 使用说明

1. 执行脚本前先打开**微信账单**页面
2. 再返回 `autox` 页面执行脚本
3. 执行结束会返回到 `autox` 页面
4. 默认保存文件在 `/sdcard/Download/` 目录

## 实现思路

使用 `autox` 获取页面布局信息一般常用 `id` 或 `className` 获取页面布局信息,但是微信账单页面都没有 `id` 和 `className`,只能获取最外面的容器 `android.webkit.WebView`,一层一层剥洋葱了。下面是页面布局的大概结构

**布局结构**

```html
<view classNam="android.webkit.WebView">
    <view >
      <!-- 筛选条件 -->
      <view >
            <view >
                <button>全部账单</button>
                <button>统计</button>
            <view />
      <view />
      <!-- 账单列表 -->
      <view >
            <view >
                <!-- 两行 view 是一个整体,第一个月份统计,第二月份收入明细 -->
                <view >
                  <view >
                        <view >
                            <button>2024-11</button>
                            <view class="android.widget.TextView">支出<view />
                            <view class="android.widget.TextView">¥200<view />
                            <view class="android.widget.TextView">收入<view />
                            <view class="android.widget.TextView">¥200<view />
                        <view />
                  <view />
                <view />
                <view >
                  <!-- 明细 -->
                  <view >
                        二维码收款-来自计xxxx,11月00日00点00分,收入0.00元,按钮。点按两次并按住可长按
                  <view />
                  <view >
                        二维码收款-来自计xxxx,11月00日00点00分,收入0.00元,按钮。点按两次并按住可长按
                  <view />
                  <view >
                        二维码收款-来自计xxxx,11月00日00点00分,收入0.00元,按钮。点按两次并按住可长按
                  <view />
                  <!-- ......更多 -->
                <view />
            <view />
      <view />
    <view />
    <view classNam="android.app.Dialog" ><view />
<view />
```

[**脚本代码**](https://864000.lanzouj.com/iQcKA2e8afuf "下载脚本")

```js

/**
* @description wx 账单导出脚本
* 1、执行脚本前先打开微信账单页面
* 2、返回 autox 页面执行脚本
* 3、执行结束会返回到 autox 页面
* 4、默认保存文件在 Download 目录
* @AuThor Mr.Fang
* @time 2024年11月4日12:01:39
*/

// 等待打开无障碍继续执行
auto.waitFor();

let endFlag = false; // 结束标识
let format = "csv"; // 默认导出格式
let endCondition = '2024年7月'; // 默认截止日期
const savePath = '/sdcard/Download/';// 本地路径
const options = ["json", "md", "txt", "csv"]; // 导出格式
const listData = []; // 账单数据
const keyMap = new Map(); // 存放日期


/**
* 字符串日期转标准日期
* @Param {*} inputDate 2024年10月22日7时8分
* @returns
*/
function convertDateTime(inputDate) {
    // 解析输入的日期字符串
    const match = inputDate.match(/(\d+)年(\d+)月(\d+)日(\d+)点(\d+)分/);
    if (!match) {
      return '输入的日期格式不正确';
    }
    if (match) {
      const year = match;
      const month = String(match).padStart(2, '0'); // 确保月份为两位
      const day = String(match).padStart(2, '0'); // 确保日期为两位
      const hour = String(match).padStart(2, '0'); // 转换为24小时制
      const minute = String(match).padStart(2, '0'); // 确保分钟为两位

      // 构建标准时间格式
      const standardTime = `${year}-${month}-${day} ${hour}:${minute}:00`;
      return standardTime;
    } else {
      console.log('输入格式不正确');
    }
    return "";
}

/**
* 验证截止日期
* @param {*} inputDate 2024年10月
* @returns
*/
function verifyDateTime(inputDate) {
    // 解析输入的日期字符串
    const dateParts = inputDate.match(/(\d+)年(\d+)月/);
    if (!dateParts) {
      return '输入的截止日期不正确';
    }
    return '';
}

/**
*
* @returns 返回最大长度
*/
function calcMaxLength() {
    const mergedArray = listData.reduce((acc, item) => { return acc.concat(item.list) }, []);
    return mergedArray.reduce((max, item) => Math.max(max, (item.type ? item.type.length : 0) + item.user.length), 10);
}


// 转 csv
function textConvertCSV() {
    // 输出 csv 格式
    let text = '时间,用户名,类型,资金\n';
    const mergedArray = listData.reduce((acc, item) => { return acc.concat(item.list) }, []);
    for (let item of mergedArray) {
      let { time, user, type, amount } = item;
      text += `${time},${user},${type},${amount}\n`;
    }
    return text;
}

// 转 md
function textConvertMD() {
    let text = "| 时间               | 用户名 | 类型 | 金额 |\n";
    text += "| ------------------ | ------ | ---- | ---- |\n";
    const mergedArray = listData.reduce((acc, item) => { return acc.concat(item.list) }, []);
    for (let item of mergedArray) {
      let { time, user, type, amount } = item;
      if (user.includes("*")) {
            user = user.replace("*", "\\*");
      }
      text += `|${time}|${user}|${type}|${amount}|\n`;
    }
    return text;
}

// 转 txt
function textConvertTxt() {
    let text = "";
    const maxlength = calcMaxLength();
    for (let item of listData) {
      let { key, list } = item;
      let keys = key.split('|');
      text += `${keys}\t\t支出¥${keys} 收入¥${keys}\n`;
      list.forEach(detail => {
            // 构造空字符
            let padding = ' '.repeat(maxlength - (detail.type ? detail.type.length : 0) - detail.user.length);
            text += `${detail.type}-${detail.user}${padding}${detail.amount}\n`;
            text += `${detail.time}\n`;
      })
      text += '\n\n';
    }
    return text;
}

/**
* 保存到本地
*/
function saveLocal() {
    // 输出 csv 格式
    let content = '';
    switch (format) {
      case 'csv': content = textConvertCSV(); break;
      case 'md': content = textConvertMD(); break;
      case 'txt': content = textConvertTxt(); break;
      default: content = JSON.stringify(listData);
    }
    const fullPath = savePath + endCondition + '.' + format
    files.write(fullPath, content)
    console.log('文件写入成功:', fullPath);
    return fullPath;
}

/**
* 字符串金额转金额
* @param {*} transaction
* @returns
*/
function convertAmount(transaction) {
    // 使用正则表达式提取金额
    const amountMatch = transaction.match(/(\d+\.?\d*)元/);
    if (!amountMatch) return '无效的交易格式';

    // 提取金额并转换为浮点数
    const amount = parseFloat(amountMatch);

    // 根据交易类型添加正负号
    if (transaction.includes('收入')) {
      return `+${amount.toFixed(2)}`;
    } else if (transaction.includes('支出')) {
      return `-${amount.toFixed(2)}`;
    } else {
      return '无效的交易类型';
    }
}

// 定义一个函数来比较两个对象是否相等
function isSameEntry(entryA, entryB) {
    return entryA.user === entryB.user && entryA.time === entryB.time && entryA.amount === entryB.amount;
}

/**
* 添加账单数据
* @param {*} key 日期
* @param {*} data 明细
*/
function addBill(key, data) {
    if (keyMap.has(key)) {
      let index = keyMap.get(key);
      let { list } = listData;
      // 过滤掉arrayA中存在于arrayB的项
      let filtered = list.filter(a => !data.some(b => isSameEntry(a, b)));
      // 合并 filtered 和 data
      listData.list = filtered.concat(data);
    } else {
      let index = listData.length;
      listData.push({ key: key, list: data });
      keyMap.set(key, index);
    }
}

function loadData() {
    // 获取页面根账单节点
    let webView = className("android.webkit.WebView").findOne(1000);
    // 账单列表节点
    let elements = webView.children().children().children().children();
    // 节点数量
    let length = elements.length;
    console.log("账单数量:", length);
    for (let i = 0; i < length; i += 2) {
      console.log(i);
      if (i + 1 >= length) {
            console.log('提前结束')
            break;
      }
      // 月份统计数据 示例:2024年11月 支出$00.00收入$00.00
      let firstChildrens = elements.children();
      // 账单列表 示例:二维码收款-来自*M,10月18日7点44分,+6.00,……
      let lastChildrens = elements.children();
      if (firstChildrens.length === 0) {
            console.log('跳过空节点')
            continue;
      }
      console.log('firstChildrens:', firstChildrens.length);
      console.log('lastChildrens:', lastChildrens.length);

      let months = firstChildrens.children().length === 1 ? firstChildrens.children().children() : firstChildrens.children();
      console.log('months', months.length)
      if (months.length === 0) {
            continue;
      }
      let year = months.text();
      if (year === endCondition) {
            endFlag = true;
            console.log("提前结束")
            return true;
      }
      let output = months.text();
      let input = months.text();
      console.log(`时间:${year} 支出:${output} 收入:${input}`)

      let listTemp = [];
      lastChildrens.forEach(item => {
            // 二维码收款-来自计*xxx,10月30日8点48分,收入1.00元,按钮。点按两次并按住可长按
            let text = item.text();
            if (text) {
                let list = text.split(',');
                list.pop();
                // 收入类型-用户 时间 收入 支出
                let user = list, type = "";
                let firstIndex = user.indexOf('-');
                if (firstIndex != -1) {
                  type = user.substring(0, firstIndex);
                  user = user.substring(firstIndex + 1);
                }
                let time = convertDateTime(year.substring(0, 5) + list);
                let amount = convertAmount(list);
                console.log(time, '\t', type, '\t', user, '\t', amount);
                listTemp.push({ time, type, user, amount })
            }
      })
      addBill(year + "|" + output + "|" + input, listTemp);
    }
    // 结束标识
    if (length >= 2) { // 倒数第二个结束标识
      const end = elements;
      const entText = end.children().text();
      console.log('entText', entText);
      if (entText === "暂无更多记录") {
            return true;
      }
    }
    // 滚动页面
    webView.scrollForward();
    return false;
}


/**
* 定时器,每隔 5 秒执行一次
*/

function startInterval() {
    launch("com.tencent.mm");
    sleep(2000);
    const button = text("全部账单").findOne(1000);
    if (button) {
      const interval = setInterval(() => {
            if (endFlag) {
                clearInterval(interval);
                const local = saveLocal();
                launch("org.autojs.autoxjs.v7");
                sleep(1000);
                alert("保存路径" + local)
            } else {
                endFlag = loadData();
            }
      }, 1000)
    } else {
      launch("org.autojs.autoxjs.v7");
      sleep(1000);
      toast("请打开账单页面")
    }
}

/**
* 开始方法
*/
function start() {
    const input = dialogs.rawInput("请输入截止年月", endCondition);
    console.log('input:', input);
    const result = verifyDateTime(input);
    if (result) {
      toastLog(result);
    } else {
      endCondition = input
      const selectIndex = dialogs.singleChoice("请选择导出格式", options, 3);
      format = options;
      console.log(format);
      toastLog("开始执行");
      startInterval();
    }
}

start();
```

## 导出示例

**txt**

```text
2024年10月        支出¥200.00元 收入¥245.89元
二维码收款-来自计*g   +10.00
2024-10-30 08:48:00
二维码收款-来自B*m   +1.00
2024-10-24 10:53:00
二维码收款-来自L*S   +5.00
2024-10-22 16:56:00
二维码收款-来自*鹿      +6.00
2024-10-18 07:44:00
转账-来来xxxx- yo      +100.00
2024-10-17 16:00:00
二维码收款-来xxxx- yo+18.88
2024-10-17 14:46:00
…………省略
```

**md**

```text
| 时间                | 用户名       | 类型       | 金额    |
| ------------------- | ------------ | ---------- | ------- |
| 2024-10-30 08:48:00 | 来自计\*g    | 二维码收款 | +10.00|
| 2024-10-24 10:53:00 | 来自B\*m   | 二维码收款 | +1.00   |
| 2024-10-22 16:56:00 | 来自L\*S   | 二维码收款 | +5.00   |
| 2024-10-18 07:44:00 | 来自\*鹿   | 二维码收款 | +6.00   |
| 2024-10-17 16:00:00 | 来自xxxx- yo | 转账       | +100.00 |
| 2024-10-17 14:46:00 | 来自xxxx- yo | 二维码收款 | +18.88|
…………省略
```

**csv**

```text
时间,用户名,类型,资金
2024-10-30 08:48:00,来自计*g,二维码收款,+10.00
2024-10-24 10:53:00,来自B*m,二维码收款,+1.00
2024-10-22 16:56:00,来自L*S,二维码收款,+5.00
2024-10-18 07:44:00,来自*鹿,二维码收款,+6.00
2024-10-17 16:00:00,来自xxxx- yo,转账,+100.00
2024-10-17 14:46:00,来自xxxx- yo,二维码收款,+18.88
…………省略
```

mr88fang 发表于 2024-11-4 17:12

kangta520 发表于 2024-11-4 16:43
鸿蒙不行


华为手机好像都不行,估计是 `dialogs`弹框运行不了,你手动把最后一行 start() 改成 startInterval() 应该就可以了

```js
// 修改前
start();
// 修改后
startInterval();
```

脚本前面定义的文件格式和截止日期自行修改

```js
let format = "csv"; // 默认导出格式
let endCondition = '2024年7月'; // 默认截止日期
````

mr88fang 发表于 2024-11-5 16:48

91264676 发表于 2024-11-5 16:46
请教一下大佬一般是如何写autox代码的,在手机上写还是电脑上写完传到手机上,手机写代码总感觉很不顺手(刚刚 ...

开发文档:http://doc.autoxjs.com/#/documentation
VSCode 插件:https://marketplace.visualstudio.com/items?itemName=aaroncheng.auto-js-vsce-fixed

用VSCode 可以远程调试运行脚本

NOOB 发表于 2024-11-4 15:19

大佬牛逼,回头试试

mmjj0025 发表于 2024-11-4 15:28

可以导出微信通讯录吗?

mr88fang 发表于 2024-11-4 15:30

mmjj0025 发表于 2024-11-4 15:28
可以导出微信通讯录吗?

原则上应该可以,无障碍嘛,就是模拟人为操作

kings0b 发表于 2024-11-4 15:30

大佬牛逼,学习了

ddfzl 发表于 2024-11-4 16:10

这个不错,试试看,感谢大佬

kangta520 发表于 2024-11-4 16:43

鸿蒙不行

changyufeichang 发表于 2024-11-4 17:50

真的太牛了,支持~~

wine2024 发表于 2024-11-4 17:57

非常好   技术细节丰富
页: [1] 2 3
查看完整版本: AutoX 脚本导出微信账单