手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)

栏目: IOS · Android · 发布时间: 6年前

内容简介:我司有关部门为了获取黑产群的动态,有同事潜伏在大量的黑产群(QQ群、微信群)中,干起了无间道的工作。随着黑产群数量的激增,同事希望能自动获取黑产群的聊天信息,并交付风控引擎进行风险评估。于是,这个工作就交给我了,是时候表现一波了……

1、引言

特别说明: 本文内容仅用于即时通讯技术研究和学习之用,请勿用于非法用途。如本文内容有不妥之处,请联系 JackJiang 进行处理!

手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)

我司有关部门为了获取黑产群的动态,有同事潜伏在大量的黑产群(QQ群、微信群)中,干起了无间道的工作。随着黑产群数量的激增,同事希望能自动获取黑产群的聊天信息,并交付风控引擎进行风险评估。于是,这个工作就交给我了,是时候表现一波了……

针对同事的需求,分析了一通,总结一下:

1)能够自动获取微信和 QQ群的聊天记录;

2)只要文字记录,图片和表情包,语音之类的不要;

3)后台自动运行,非实时获取记录。

注: 本文读取聊天记录的方法只适用于监控自己拥有的微信或者QQ ,无法监控或者盗取其他人的聊天记录。本文只写了如何获取聊天记录,服务器落地程序并不复杂,不做赘述。写的仓促,有错别字还请见谅。)

学习交流:

- 即时通讯开发交流3群: 185926912 [推荐]

- 移动端IM开发入门文章:《 新手入门一篇就够:从零开发移动端IM

(本文同步发布于: http://www.52im.net/thread-1992-1-1.html

2、相关文章

即时通讯网之前整理过微信本地数据库的读取和样本,如有兴趣可请往阅读:

微信本地数据库破解版(含iOS、Android),仅供学习研究 [附件下载]

3、准备工作

参阅很多相关的文章之后,对这个需求有了大致的想法,开始着手准备:

1)需要一个有root权限的Android手机,我用的是红米5(强调必须已被ROOT);

2)android的开发环境(就是Android Studio那一套啦);

3)android相关的开发经验(我是个PHP,第一次写Android程序,踩了不少坑)。

4、获取微信聊天记录过程分享

4.1 着手准备

微信的聊天记录保存在Android系统的:"/data/data/com.tencent.mm/MicroMsg/c5fb89d4729f72c345711cb*/EnMicroMsg.db" 目录和文件下。

该文件是加密的数据库文件,需要用到sqlcipher来打开。密码为:MD5(手机的IMEI+微信UIN)的前七位。文件所在的那个乱码文件夹的名称也是一段加密MD5值:MD5('mm'+微信UIN)。微信的UIN存放在微信文件夹“/data/data/com.tencent.mmshared_prefs/system_config_prefs.xml”中。(这个减号一定要带着!)

手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)

另外: 即时通讯网之前整理过微信本地数据库的样本,如有兴趣可请往下载:《 微信本地数据库破解版(含iOS、Android),仅供学习研究 [附件下载] 》。

注意: 如果手机是双卡双待,那么会有两个IMEI号,默认选择 IMEI1,如果不行,可以尝试一下字符串‘1234567890ABCDEF’。早期的微信会去判定你的IMEI,如果为空 默认选择这个字符串。

手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)

拿到密码,就可以打开EnMicroMsg.db了。微信聊天记录,包括个人、群组的所有记录全部存在message这张表里(如下图所示),就像下面这两张截图里展示的一样。

手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)

(为了方便截图,此图截自《 微信本地数据库破解版(含iOS、Android),仅供学习研究 [附件下载] 》中的样本)

手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)

(为了方便截图,此图截自《 微信本地数据库破解版(含iOS、Android),仅供学习研究 [附件下载] 》中的样本)

4.2 代码实现

第一步, 不可能直接去访问EnMicroMsg.db。因为没有权限,还要避免和微信本身产生冲突,所以选择把这个文件拷贝到自己的项目下:

oldPath ="/data/data/com.tencent.mm/MicroMsg/c5fb89d4729f72c345711cb**\***/EnMicroMsg.db";
newPath ="/data/data/com.你的项目/EnMicroMsg.db";
copyFile(oldPath,newPath);//代码见 部分源码

第二步, 拿到文件的密码:

String password = (MD5Until.md5("IMEI+微信UIN").substring(0, 7).toLowerCase());

第三步, 打开文件,执行SQL:

SQLiteDatabase.loadLibs(context);
SQLiteDatabaseHook hook = newSQLiteDatabaseHook() {
    publicvoidpreKey(SQLiteDatabase database) {
    }
    publicvoidpostKey(SQLiteDatabase database) {
        database.rawExecSQL("PRAGMA cipher_migrate;");//很重要
    }
};
SQLiteDatabase db = openDatabase(newPath, password, null, NO_LOCALIZED_COLLATORS, hook);
    longnow = System.currentTimeMillis();
    Log.e("readWxDatabases", "读取微信数据库:"+ now);
    intcount = 0;
    if(msgId != "0") {
        String sql = "select * from message";
        Log.e("sql", sql);
        Cursor c = db.rawQuery(sql, null);
        while(c.moveToNext()) {
            long_id = c.getLong(c.getColumnIndex("msgId"));
            String content = c.getString(c.getColumnIndex("content"));
            inttype = c.getInt(c.getColumnIndex("type"));
            String talker = c.getString(c.getColumnIndex("talker"));
            longtime = c.getLong(c.getColumnIndex("createTime"));
            JSONObject tmpJson = handleJson(_id, content, type, talker, time);
            returnJson.put("data"+ count, tmpJson);
            count++;
        }
        c.close();
        db.close();
        Log.e("readWxDatanases", "读取结束:"+ System.currentTimeMillis() + ",count:"+ count);
    }

到此,我们就可以通过自已写的代码拿到微信的聊天记录了,之后可以直接将整理好的JSON通过POST请求发到服务器就可以了。( 忍不住吐槽: 写服务器落地程序用了30分钟,写上面这一坨花了三四天,还不包括搭建开发环境、下载SDK、折腾ADB什么的)。

5、获取QQ聊天记录过程分享

5.1 说明

QQ的聊天记录有点麻烦,他的文件保存在:“/data/data/com.tencent.mobileqq/databases/你的QQ号码.db”。

这个文件是不加密的,可以直接打开。QQ中群组的聊天记录是单独建表存放的,所有的QQ群信息存放在TroopInfoV2表里,需要对字段troopuin求MD5,然后找到他的聊天记录表:mr_troop_" + troopuinMD5 +"_New。

但是!(看到“但是”就没好事。。。)

问题来了,它的内容是加密的,而且加密方法还很复杂:根据手机IMEI循环逐位异或。具体的我不举例子了,太麻烦,直接看文章最后的解密方法。

5.2 代码实现

第一步, 还是拷贝数据库文件:

final String QQ_old_path = "/data/data/com.tencent.mobileqq/databases/QQ号.db";
final String QQ_new_path = "/data/data/com.android.saurfang/QQ号.db";
DataHelp.copyFile(QQ_old_path,QQ_new_path);

第二步, 打开并读取内容:

SQLiteDatabase.loadLibs(context);
String password = "";
SQLiteDatabaseHook hook = newSQLiteDatabaseHook() {
    publicvoidpreKey(SQLiteDatabase database) {}
    publicvoidpostKey(SQLiteDatabase database) {
        database.rawExecSQL("PRAGMA cipher_migrate;");
    }
};
 MessageDecode mDecode = newMessageDecode(imid);
HashMap<String, String> troopInfo = newHashMap<String, String>();
try{
    SQLiteDatabase db = openDatabase(newPath,password,null, NO_LOCALIZED_COLLATORS,hook);
    longnow = System.currentTimeMillis();
    Log.e("readQQDatabases","读取QQ数据库:"+now);
    //读取所有的群信息
    String sql = "select troopuin,troopname from TroopInfoV2 where _id";
    Log.e("sql",sql);
    Cursor c = db.rawQuery(sql,null);
    while(c.moveToNext()){
        String troopuin = c.getString(c.getColumnIndex("troopuin"));
        String troopname = c.getString(c.getColumnIndex("troopname"));
        String name = mDecode.nameDecode(troopname);
        String uin = mDecode.uinDecode(troopuin);
        Log.e("readQQDatanases","读取结束:"+name);
        troopInfo.put(uin, name);
    }
    c.close();
 
    inttroopCount = troopInfo.size();
    Iterator<String> it = troopInfo.keySet().iterator();
    JSONObject json = newJSONObject();
    //遍历所有的表
    while(troopCount > 0) {
        try{
            while(it.hasNext()) {
                String troopuin = (String)it.next();
                String troopname = troopInfo.get(troopuin);
                if(troopuin.length() < 8)
                    continue;
                String troopuinMD5 = getMD5(troopuin);
                String troopMsgSql = "select _id,msgData, senderuin, time from mr_troop_"+ troopuinMD5 +"_New";
                Log.e("sql",troopMsgSql);
                Cursor  cc = db.rawQuery(troopMsgSql,null);
                JSONObject tmp = newJSONObject();
                while(cc.moveToNext()) {
                    long_id = cc.getLong(cc.getColumnIndex("_id"));
                    byte[] msgByte = cc.getBlob(cc.getColumnIndex("msgData"));
                    String ss = mDecode.msgDecode(msgByte);
                    //图片不保留
                    if(ss.indexOf("jpg") != -1|| ss.indexOf("gif") != -1
                            || ss.indexOf("png") != -1)
                        continue;
                    String time = cc.getString(cc.getColumnIndex("time"));
                    String senderuin = cc.getString(cc.getColumnIndex("senderuin"));
                    senderuin  = mDecode.uinDecode(senderuin);
                    JSONObject tmpJson = handleQQJson(_id,ss,senderuin,time);
                    tmp.put(String.valueOf(_id),tmpJson);
                }
                troopCount--;
                cc.close();
            }
        } catch(Exception e) {
            Log.e("e","readWxDatabases"+e.toString());
        }
    }
    db.close();
}catch(Exception e){
    Log.e("e","readWxDatabases"+e.toString());
}

然后你就可以把信息发到服务器落地了(同样跟微信的记录上传一样,通过你自已写的代码发送到你的服务端就可以了)。

6、题外话:一些注意点

这里还有几个需要注意的地方。

1) 最新安卓系统很难写个死循环直接跑了,所以我们需要使用Intent,来开始Service,再通过Service调用AlarmManager,就像下面的代码这样:

publicclassMainActivity extendsAppCompatActivity {
    privateIntent intent;
    @Override
    protectedvoidonCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity\_main);
        intent = newIntent(this, LongRunningService.class);
        startService(intent);
    }
 
    @Override
    protectedvoidonDestroy() {
        super.onDestroy();
        stopService(intent);
    }
}

然后再创建一个LongRunningService,在其中调用AlarmManager:

AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
intMinutes = 60*1000; //此处规定执行的间隔时间
longtriggerAtTime = SystemClock.elapsedRealtime() + Minutes;
Intent intent1 = newIntent(this, AlarmReceiver.class);//注入要执行的类
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent1, 0);
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent);
returnsuper.onStartCommand(intent, flags, startId);

在AlarmReceiver中调用我们的方法:

//微信部分
postWXMsg.readWXDatabase();
//QQ部分
postQQMsg.readQQDatabase();
//再次开启LongRunningService这个服务,即可实现定时循环。
Intent intentNext = newIntent(context, LongRunningService.class);
context.startService(intentNext);

2) 安卓不允许在主线程里进行网络连接,可以直接用 retrofit2 来发送数据(或者最简单的方法就是用AsyncTask了)。

3) 项目需要授权网络连接(就是在AndroidManifast.xml里加上网络权限申请就是了);

4) 项目需要引入的包:

implementation files('libs/sqlcipher.jar')
implementation files('libs/sqlcipher-javadoc.jar')
implementation 'com.squareup.retrofit2:retrofit:2.0.0'
implementation 'com.squareup.retrofit2:converter-gson:2.0.0'

5) 如果复制文件时失败,校验文件路径不存在,多半是因为授权问题。需要对数据库文件授权 全用户rwx权限;

6) 如果服务端使用 MySql 数据库的话,数据库编码请用utf8mb4编码,用来支持Emoji表情。。

7、我的部分源码

(因为种种原因,我不太好直接把源码贴上来,现把几个实用方法分享出来,可以直接使用。)

复制文件的方法:

/**
  * 复制单个文件
  *
  * @param oldPath String 原文件路径 如:c:/fqf.txt
  * @param newPath String 复制后路径 如:f:/fqf.txt
  * @return boolean
  */
 publicstaticbooleancopyFile(String oldPath, String newPath) {
     deleteFolderFile(newPath, true);
     Log.e("copyFile", "time_1:"+ System.currentTimeMillis());
     InputStream inStream = null;
     FileOutputStream fs = null;
     try{
         intbytesum = 0;
         intbyteread = 0;
         File oldfile = newFile(oldPath);
         Boolean flag = oldfile.exists();
         Log.e("copyFile", "flag:"+flag );
         if(oldfile.exists()) { //文件存在时
             inStream = newFileInputStream(oldPath); //读入原文件
             fs = newFileOutputStream(newPath);
             byte[] buffer = newbyte[2048];
             while((byteread = inStream.read(buffer)) != -1) {
                 bytesum += byteread; //字节数 文件大小
                 fs.write(buffer, 0, byteread);
             }
             Log.e("copyFile", "time_2:"+ System.currentTimeMillis());
         }
     } catch(Exception e) {
         System.out.println("复制单个文件操作出错");
         e.printStackTrace();
     } finally{
         try{
             if(inStream != null) {
                 inStream.close();
             }
             if(fs != null) {
                 fs.close();
             }
         } catch(IOException e) {
             e.printStackTrace();
         }
     }
     returntrue;
 }
 
 /**
  * 删除单个文件
  *
  * @param filepath
  * @param deleteThisPath
  */
 publicstaticvoiddeleteFolderFile(String filepath, booleandeleteThisPath) {
     if(!TextUtils.isEmpty(filepath)) {
         try{
             File file = newFile(filepath);
             if(file.isDirectory()) {
                 //处理目录
                 File files[] = file.listFiles();
                 for(inti = 0; i < file.length(); i++) {
                     deleteFolderFile(files[i].getAbsolutePath(), true);
                 }
             }
             if(deleteThisPath) {
                 if(!file.isDirectory()) {
                     //删除文件
                     file.delete();
                 } else{
                     //删除目录
                     if(file.listFiles().length == 0) {
                         file.delete();
                     }
                 }
             }
         } catch(Exception e) {
             e.printStackTrace();
         }
     }
 }

MD5方法:

publicclassMD5Until {
    publicstaticcharHEX_DIGITS[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            'A', 'B', 'C', 'D', 'E', 'F'};
    //将字符串转化为位
    publicstaticString toHexString(byte[] b){
        StringBuilder stringBuilder = newStringBuilder(b.length * 2);
        for(inti = 0; i < b.length; i++) {
            stringBuilder.append(HEX_DIGITS[(b[i] & 0xf0) >>> 4]);
            stringBuilder.append(HEX_DIGITS[b[i] & 0x0f]);
        }
        returnstringBuilder.toString();
    }
    publicstaticString md5(String string){
        try{
            MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
            digest.update(string.getBytes());
            bytemessageDigest[] = digest.digest();
            returntoHexString(messageDigest);
        }catch(NoSuchAlgorithmException e){
            e.printStackTrace();
        }
        return"";
    }
}

QQ信息解密方法:

public class MessageDecode {
    public String imeiID;
    public intimeiLen;
    public MessageDecode(String imeiID)
    {
        this.imeiID = imeiID;
        this.imeiLen = imeiID.length();
    }
    public boolean isChinese(bytech) {
        intres = ch & 0x80;
        if(res != 0)
            returntrue;
        returnfalse;
    }
    public String timeDecode(String time)
    {
        String datetime = "1970-01-01 08:00:00";
        SimpleDateFormat sdFormat = newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try{
            longsecond = Long.parseLong(time);
            Date dt = newDate(second * 1000);
            datetime = sdFormat.format(dt);
        } catch(NumberFormatException e) {
            e.printStackTrace();
        }
        returndatetime;
    }
    public String nameDecode(String name)
    {
        bytenbyte[] = name.getBytes();
        byteibyte[] = imeiID.getBytes();
        bytexorName[] = newbyte[nbyte.length];
 
        intindex = 0;
        for(inti = 0; i < nbyte.length; i++) {
            if(isChinese(nbyte[i])){
                xorName[i] = nbyte[i];
                i++;
                xorName[i] = nbyte[i];
                i++;
                xorName[i] = (byte)(nbyte[i] ^ ibyte[index % imeiLen]);
                index++;
            } else{
                xorName[i] = (byte)(nbyte[i] ^ ibyte[index % imeiLen]);
                index++;
            }
        }
        return new String(xorName);
    }
    public String uinDecode(String uin)
    {
        byteubyte[] = uin.getBytes();
        byteibyte[] = imeiID.getBytes();
        bytexorMsg[] = newbyte[ubyte.length];
        intindex = 0;
        for(inti = 0; i < ubyte.length; i++) {
            xorMsg[i] = (byte)(ubyte[i] ^ ibyte[index % imeiLen]);
            index++;
        }
        returnnewString(xorMsg);
    }
 
    public String msgDecode(byte[] msg)
    {
        byteibyte[] = imeiID.getBytes();
        bytexorMsg[] = newbyte[msg.length];
        intindex = 0;
        for(int i = 0; i < msg.length; i++) {
            xorMsg[i] = (byte)(msg[i] ^ ibyte[index % imeiLen]);
            index++;
        }
        return new String(xorMsg);
    }
}

附录:有关微信、QQ的技术文章汇总

微信朋友圈千亿访问量背后的技术挑战和实践总结

腾讯技术分享:腾讯是如何大幅降低带宽和网络流量的(图片压缩篇)

腾讯技术分享:腾讯是如何大幅降低带宽和网络流量的(音视频技术篇)

微信团队分享:微信移动端的全文检索多音字问题解决方案

腾讯技术分享:Android版手机QQ的缓存监控与优化实践

微信团队分享:iOS版微信的高性能通用key-value组件技术实践

微信团队分享:iOS版微信是如何防止特殊字符导致的炸群、APP崩溃的?

腾讯技术分享:Android手Q的线程死锁监控系统技术实践

微信团队原创分享:iOS版微信的内存监控系统技术实践

让互联网更快:新一代QUIC协议在腾讯的技术实践分享

iOS后台唤醒实战:微信收款到账语音提醒技术总结

腾讯技术分享:社交网络图片的带宽压缩技术演进之路

微信团队分享:视频图像的超分辨率技术原理和应用场景

微信团队分享:微信每日亿次实时音视频聊天背后的技术解密

QQ音乐团队分享:Android中的图片压缩技术详解(上篇)

QQ音乐团队分享:Android中的图片压缩技术详解(下篇)

腾讯团队分享:手机QQ中的人脸识别酷炫动画效果实现详解

腾讯团队分享 :一次手Q聊天界面中图片显示bug的追踪过程分享

微信团队分享:微信Android版小视频编码填过的那些坑 》 

微信手机端的本地数据全文检索优化之路 》 

企业微信客户端中组织架构数据的同步更新方案优化实战

微信团队披露:微信界面卡死超级bug“15。。。。”的来龙去脉

QQ 18年:解密8亿月活的QQ后台服务接口隔离技术

月活8.89亿的超级IM微信是如何进行Android端兼容测试的

以手机QQ为例探讨移动端IM中的“轻应用”

一篇文章get微信开源移动端数据库组件WCDB的一切!

微信客户端团队负责人技术访谈:如何着手客户端性能监控和优化

微信后台基于时间序的海量数据冷热分级架构设计实践

微信团队原创分享:Android版微信的臃肿之困与模块化实践之路

微信后台团队:微信后台异步消息队列的优化升级实践分享

微信团队原创分享:微信客户端 SQLite 数据库损坏修复实践 》 

腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率 》 

腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇) 》 

腾讯原创分享(三):如何大幅压缩移动网络下APP的流量消耗(上篇) 》 

微信Mars:微信内部正在使用的网络层封装库,即将开源 》 

如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源 》 

开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石 [源码下载] 》 

微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解 》 

微信团队原创分享:Android版微信后台保活实战分享(进程保活篇) 》 

微信团队原创分享:Android版微信后台保活实战分享(网络保活篇) 》 

Android版微信从300KB到30MB的技术演进(PPT讲稿) [附件下载] 》 

微信团队原创分享:Android版微信从300KB到30MB的技术演进 》 

微信技术总监谈架构:微信之道——大道至简(演讲全文)

微信技术总监谈架构:微信之道——大道至简(PPT讲稿) [附件下载] 》 

如何解读《微信技术总监谈架构:微信之道——大道至简》

微信海量用户背后的后台系统存储架构(视频+PPT) [附件下载]

微信异步化改造实践:8亿月活、单机千万连接背后的后台解决方案 》 

微信朋友圈海量技术之道PPT [附件下载] 》 

微信对网络影响的技术试验及分析(论文全文) 》 

一份微信后台技术架构的总结性笔记 》 

架构之道:3个 程序员 成就微信朋友圈日均10亿发布量[有视频] 》 

快速裂变:见证微信强大后台架构从0到1的演进历程(一)

快速裂变:见证微信强大后台架构从0到1的演进历程(二) 》 

微信团队原创分享:Android内存泄漏监控和优化技巧总结 》 

全面总结iOS版微信升级iOS9遇到的各种“坑” 》 

微信团队原创资源混淆工具:让你的APK立减1M 》 

微信团队原创Android资源混淆工具:AndResGuard [有源码] 》 

Android版微信安装包“减肥”实战记录 》 

iOS版微信安装包“减肥”实战记录 》 

移动端IM实践:iOS版微信界面卡顿监测方案 》 

微信“红包照片”背后的技术难题 》 

移动端IM实践:iOS版微信小视频功能技术方案实录 》 

移动端IM实践:Android版微信如何大幅提升交互性能(一)

移动端IM实践:Android版微信如何大幅提升交互性能(二)

移动端IM实践:实现Android版微信的智能心跳机制 》 

移动端IM实践:WhatsApp、Line、微信的心跳策略分析 》 

移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)

移动端IM实践:iOS版微信的多设备字体适配方案探讨 》 

信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑

腾讯信鸽技术分享:百亿级实时消息推送的实战经验

IPv6技术详解:基本概念、应用现状、技术实践(上篇)

IPv6技术详解:基本概念、应用现状、技术实践(下篇)

腾讯TEG团队原创:基于MySQL的分布式数据库TDSQL十年锻造经验分享

微信多媒体团队访谈:音视频开发的学习、微信的音视频技术和挑战等

了解iOS消息推送一文就够:史上最全iOS Push技术详解

腾讯技术分享:微信小程序音视频技术背后的故事

腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面

微信多媒体团队梁俊斌访谈:聊一聊我所了解的音视频技术

腾讯音视频实验室:使用AI黑科技实现超低码率的高清实时视频聊天

腾讯技术分享:微信小程序音视频与WebRTC互通的技术思路和实践

手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)

>> 更多同类文章 ……

(本文同步发布于: http://www.52im.net/thread-1992-1-1.html


以上所述就是小编给大家介绍的《手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First Web Design

Head First Web Design

Ethan Watrall、Jeff Siarto / O’Reilly Media, Inc. / 2009-01-02 / USD 49.99

Want to know how to make your pages look beautiful, communicate your message effectively, guide visitors through your website with ease, and get everything approved by the accessibility and usability ......一起来看看 《Head First Web Design》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试