5月10日 坚果Pro
到手,Smartisan OS
很赞,但我不太喜欢手机联系人的头像,没特色、辨识度不高。
微信用多了,一看头像就能想到具体的人,要是联系人头像能和微信好友头像一样就好了。
用蛮力,一个一个修改联系人头像当然可行,但这样的纯体力活程序员不应该做。
先网上搜了下,有类似需求,如“QQ头像如何同步到手机联系人”,但没看到解决方案,只有靠自己了。
以下是我的思路,先说明一下,我习惯手机联系人姓名、微信好友备注名都使用真名,也就是说二者能以此关联起来。
- 获取微信好友名称和头像,生成
name-photo-map
- 遍历手机联系人,根据联系人姓名去
name-photo-map
中查找,为联系人添加头像。
下面来看看具体实现。
1. 工具
- Chrome浏览器
- NodeJS
2. 获取微信全部好友的名称和头像
Chrome F12打开开发者工具,登录微信网页版,查看Network。
这个请求返回的是JSON,全部好友的信息都在 MemberList
里
- 备注 对应
RemarkName
- 昵称 对应
NickName
- 头像 对应
HeadImgUrl
注意 RemarkName
我设置的是中文,这里显示的是乱码。这是Chrome没有使用UTF8编码导致的。
在请求上右键 Open in new tab
,在新的标签页上 ctrl+s
将请求返回结果保存到文件 wx-contacts.json
,我用vscode打开,信息都正常显示了。
下面这段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获取头像的请求才能通过服务器校验,将图片下载下来。
现在我已经下载了所有微信好友的头像图片,并且以好友的备注名作为图片的文件名。
3. 为手机联系人设置头像
步骤:
- 导出手机联系人到
xxx.vcf
文件 【务必备份,以免造成麻烦】 - 解析
xxx.vcf
文件,为每个联系人添加photo,生成xxx.new.vcf
文件 - 清空手机联系人,导入
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. 结束
功能实现了,但用起来还是有些麻烦,希望有高手能做个易用的工具。完整的代码在这里。