装饰你的手机通讯录-同步微信头像

5月10日 坚果Pro 到手,Smartisan OS 很赞,但我不太喜欢手机联系人的头像,没特色、辨识度不高。
微信用多了,一看头像就能想到具体的人,要是联系人头像能和微信好友头像一样就好了。
phonecontacts

用蛮力,一个一个修改联系人头像当然可行,但这样的纯体力活程序员不应该做。
先网上搜了下,有类似需求,如“QQ头像如何同步到手机联系人”,但没看到解决方案,只有靠自己了。

以下是我的思路,先说明一下,我习惯手机联系人姓名、微信好友备注名都使用真名,也就是说二者能以此关联起来。

  1. 获取微信好友名称和头像,生成 name-photo-map
  2. 遍历手机联系人,根据联系人姓名去 name-photo-map 中查找,为联系人添加头像。

下面来看看具体实现。


1. 工具

  • Chrome浏览器
  • NodeJS

2. 获取微信全部好友的名称和头像

Chrome F12打开开发者工具,登录微信网页版,查看Network。
request
这个请求返回的是JSON,全部好友的信息都在 MemberList

  • 备注 对应 RemarkName
  • 昵称 对应 NickName
  • 头像 对应 HeadImgUrl

注意 RemarkName 我设置的是中文,这里显示的是乱码。这是Chrome没有使用UTF8编码导致的。
在请求上右键 Open in new tab,在新的标签页上 ctrl+s 将请求返回结果保存到文件 wx-contacts.json,我用vscode打开,信息都正常显示了。
rightclick charset
下面这段NodeJS程序用来解析JSON、下载头像图片、并以备注名作为图片的文件名。

var https = require('https');
var fs = require('fs');

var contacts = JSON.parse(fs.readFileSync('./data/wx-contacts.json')).MemberList; // 读取好友数据

var cookie = '登录状态下才能从微信服务器获取头像图片,把wx.qq.com的cookie放在这里';

contacts.forEach((contact) => {
    makeRequest(contact);
});

function makeRequest(contact) {
    var contactName = contact.RemarkName || contact.NickName; // 有备注使用备注,没有备注使用昵称
    if (contactName.indexOf('<') >= 0) return; // 带有表情符号,手机联系人中不可能存在,直接略过
    var options = {
        host: 'wx.qq.com',
        port: 443,
        path: contact.HeadImgUrl,
        method: 'GET',
        headers: {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Encoding': 'gzip, deflate, sdch, br',
            'Accept-Language': 'zh-CN,zh;q=0.8',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Cookie': cookie
        }
    };
    var req = https.get(options, res => {
        res.setEncoding("binary");
        var imgData = '';
        res.on('data', chunk => {
            imgData += chunk;
        });
        res.on('end', () => {
            if (imgData.length > 0) {
                // 发现response header里的content-type都是image/jpeg,为了简化代码,文件后缀名就写死了
                fs.writeFile(`images/${contactName}.jpg`, imgData, "binary");
                if (contact.retry) {
                    console.log(`Download ${contactName} succeeded!`);
                }
            } else {
                // 网络不稳定下载失败,重新下载
                console.log(`Download ${contactName} failed! Retry...`);
                contact.retry = true;
                makeRequest(contact);
            }
        });
    });
    req.end();
}

程序里 cookie 变量的设置
登录微信网页版成功后,通过Chrome Network查看任意一个发往 wx.qq.com 的请求,将请求 header 中的 cookie 内容赋给程序中的变量。
这样NodeJS获取头像的请求才能通过服务器校验,将图片下载下来。

现在我已经下载了所有微信好友的头像图片,并且以好友的备注名作为图片的文件名。
photo


3. 为手机联系人设置头像

步骤:

  1. 导出手机联系人到 xxx.vcf 文件 【务必备份,以免造成麻烦】
  2. 解析 xxx.vcf 文件,为每个联系人添加photo,生成 xxx.new.vcf 文件
  3. 清空手机联系人,导入 xxx.new.vcf 到手机

步骤2的代码如下:

// 程序用到了第三方的库解析vcf文件 https://github.com/jhermsmeier/node-vcf  

// 有两个文件被我修改过,所以删除了package.json  
// node_modules/foldline/foldline.js
// 原来逻辑是一行超过75个字符要截断换行,但发现截断换行后手机导入会失败,所以改为不截断换行

// node_modules/vcf/lib/property.js
// 对比转换前后文件的差异,发现手机导出的文件field为type时候会略去field,并且value都是大写
// 所以修改这里的逻辑,保证程序输出的和手机导出的文件内容一样

var fs = require('fs');

const OUTPUT_VCARD_PATH = 'output/contacts.vcf'; // 程序执行完毕后输出的vcf文件
const INPUT_VCARD_PATH = 'data/contacts.vcf'; // 手机导出的vcf文件
const INPUT_IMG_PATH = 'images/'; // 存放微信好友头像的目录

var StringDecoder = require('string_decoder').StringDecoder;
var decoder = new StringDecoder('utf8');

// 解析vcf文件生成联系人对象数组
var vCard = require('vcf');
var vcfData = fs.readFileSync(INPUT_VCARD_PATH, 'utf-8');
var cards = vCard.parseMultiple(vcfData); // 联系人对象数组

var output = '';
var totalCount = 0;
var hasImgCount = 0;
cards.forEach((card) => {
    totalCount++;
    // 联系人姓名fullName,中文的话是用UTF8 QUOTED-PRINTABLE编码的,需要解码
    var fullName = card.get('fn')._data;
    if (card.get('fn').encoding == 'QUOTED-PRINTABLE') {
        fullName = decodeQuotedPrintable(card.get('fn')._data);
    }

    // 根据联系人姓名查看是否下载过对应的微信头像图片
    var imgPath = INPUT_IMG_PATH + fullName + '.jpg';
    if (fs.existsSync(imgPath)) {
        // 有微信头像图片,设置给联系人的photo
        var imgData = fs.readFileSync(imgPath);
        var dataStr = imgData.toString('base64');
        card.set('photo', dataStr, {
            encoding: 'BASE64',
            type: 'JPEG'
        });
        hasImgCount++;
        console.log(`${fullName} has a head image.`);
    } else {
        console.log(`${fullName} does not have a head image.`);
    }
    output += card.toString('2.1') + '\r\n'; // 2.1表示vcf文件的标准
});

fs.writeFileSync(OUTPUT_VCARD_PATH, output, 'utf-8'); // 输出新的vcf文件
console.log(`\n\nHas Image Count / Total: ${hasImgCount} / ${totalCount}\nSave to "${OUTPUT_VCARD_PATH}" over!!!\n`);

// 解码UTF8 QUOTED-PRINTABLE字符串
function decodeQuotedPrintable(str) {
    var arr = str.split('=').slice(1);
    var byteArr = [];
    arr.forEach(str => {
        byteArr.push(parseInt('0x' + str));
    });
    var cent = Buffer.from(byteArr);
    return decoder.write(cent);
}

将程序输出的vcf文件导入手机通讯录,效果如下:
这里写图片描述


4. 结束

功能实现了,但用起来还是有些麻烦,希望有高手能做个易用的工具。完整的代码在这里

猜你喜欢

转载自blog.csdn.net/github_39212680/article/details/73409960
今日推荐