├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── app-release.apk ├── build.gradle ├── libs │ └── XposedBridgeApi.jar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── chiontang │ │ └── wechatmomentexport │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── xposed_init │ ├── java │ │ └── me │ │ │ └── chiontang │ │ │ └── wechatmomentexport │ │ │ ├── Config.java │ │ │ ├── IntervalThread.java │ │ │ ├── Main.java │ │ │ ├── PreferenceActivity.java │ │ │ ├── Utils.java │ │ │ └── models │ │ │ ├── Comment.java │ │ │ ├── Like.java │ │ │ ├── Tweet.java │ │ │ └── User.java │ └── res │ │ ├── layout │ │ └── activity_preference.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-en │ │ └── strings.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values-zh │ │ └── strings.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── me │ └── chiontang │ └── wechatmomentexport │ └── ExampleUnitTest.java ├── build.gradle ├── demo_1.jpg ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── weixin6313android740.apk /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | WeChatMomentExport -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WeChat Moment Export 2 | -------------------- 3 | Deprecated. Try [WeChatMomentStat-Android](https://github.com/Chion82/WeChatMomentStat-Android) instead! 4 | 5 | 本项目已不再维护。请移步至无需依赖Xposed的[WeChatMomentStat-Android](https://github.com/Chion82/WeChatMomentStat-Android) 6 | 7 | WeChatModuleExport is an Xposed module which helps you export WeChat moment data to JSON files. 8 | 9 | 本工具为Xposed模块,用于导出微信朋友圈数据。 10 | 支持导出朋友圈文字内容、图片、段视频、点赞、评论等数据。 11 | 12 | Supported WeChat Versions(目前支持的微信版本): 13 | 14 | [WeChat 6.3.13](https://github.com/Chion82/WeChatMomentExport/raw/master/weixin6313android740.apk) 15 | 16 | 17 | 18 | Download 19 | -------- 20 | 21 | [WeChatMomentExport Releases](https://github.com/Chion82/WeChatMomentExport/releases) 22 | 23 | Change Log 24 | ---------- 25 | 26 | * 2016-2-6 支持微信版本6.3.13.64_r4488992,修复抓取数据不齐的问题。因为需要清空微信朋友圈缓存,本APP将需要获得root权限。 27 | 28 | * 2016-2-4 支持微信版本6.3.13,支持朋友圈图片视频等完整数据的导出,逆向APK时成功分析朋友圈数据模型,增加GUI配置面板 29 | 30 | Usage 31 | ----- 32 | 33 | ##EN 34 | 35 | * Make sure your Android device is rooted with Xposed Framework installed. 36 | 37 | * Install and enable this Xposed module on your device. Reboot. 38 | 39 | * Grant root access for the app. 40 | 41 | * In the GUI preference panel, click "START INTERCEPT". 42 | 43 | * Open WeChat and enter the Moments page. 44 | 45 | * Scroll down to load more moments. Meanwhile the module should be capturing the loaded data automatically. 46 | 47 | * Click "STOP INTERCEPT" when done. 48 | 49 | * By default, the generated JSON file is ```moments_output.json``` located in the root directory of the external storage. 50 | 51 | ##ZH 52 | 53 | * 确认安卓设备已root并安装好Xposed框架。 54 | 55 | * 安装并启用本Xposed模块,然后重启手机。 56 | 57 | * 允许本app获得root权限。 58 | 59 | * 打开本模块的GUI设置页并点击“开始抓取”。 60 | 61 | * 打开微信并进入朋友圈页面。 62 | 63 | * 向下滚动页面来加载更多朋友圈。同时本插件会自动抓取已加载的朋友圈数据。 64 | 65 | * 完成后,点“停止抓取”。 66 | 67 | * 默认情况下,抓取并经过转换的JSON数据存放在外部存储根目录下的 ```moments_output.json``` 文件中。 68 | 69 | * 如需抓取指定某个好友的朋友圈数据,在“开始抓取”之后直接从微信联系人进入TA的个人相册即可,在停止抓取之前不要打开朋友圈或者其他人的相册。 70 | 71 | Known Issues 72 | ------------ 73 | 74 | * 个人相册中无法抓取纯文本朋友圈。 75 | 76 | Build Requirements 77 | ------------------ 78 | 79 | * IDE: Android Studio 80 | 81 | License 82 | ------- 83 | 84 | GPLv3 85 | 86 | Author 87 | ------ 88 | 89 | [Chion Tang的归宅开发部活动记录](https://blog.chionlab.moe) 90 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chion82/WeChatMomentExport/ecd7f52d8a9af6a5c3084d10ef1dce8198b3c4c0/app/app-release.apk -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "me.chiontang.wechatmomentexport" 9 | minSdkVersion 15 10 | targetSdkVersion 23 11 | versionCode 3 12 | versionName "2.1 beta" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | provided fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.1.1' 26 | } 27 | -------------------------------------------------------------------------------- /app/libs/XposedBridgeApi.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chion82/WeChatMomentExport/ecd7f52d8a9af6a5c3084d10ef1dce8198b3c4c0/app/libs/XposedBridgeApi.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/chiontang/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/me/chiontang/wechatmomentexport/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package me.chiontang.wechatmomentexport; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 19 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | me.chiontang.wechatmomentexport.Main -------------------------------------------------------------------------------- /app/src/main/java/me/chiontang/wechatmomentexport/Config.java: -------------------------------------------------------------------------------- 1 | package me.chiontang.wechatmomentexport; 2 | 3 | import android.os.Environment; 4 | 5 | /** 6 | * Created by chiontang on 2/4/16. 7 | */ 8 | public class Config { 9 | static boolean enabled = false; 10 | 11 | static boolean ready = false; 12 | 13 | static String outputFile = Environment.getExternalStorageDirectory() + "/moments_output.json"; 14 | 15 | final static String[] VERSIONS = {"6.3.13.49_r4080b63", "6.3.13.64_r4488992"}; 16 | 17 | final static String[] PROTOCAL_SNS_DETAIL_CLASSES = {"com.tencent.mm.protocal.b.atp", "com.tencent.mm.protocal.b.atp"}; 18 | 19 | final static String[] PROTOCAL_SNS_DETAIL_METHODS = {"a", "a"}; 20 | 21 | final static String[] SNS_XML_GENERATOR_CLASSES = {"com.tencent.mm.plugin.sns.f.i", "com.tencent.mm.plugin.sns.f.i"}; 22 | 23 | final static String[] SNS_XML_GENERATOR_METHODS = {"a", "a"}; 24 | 25 | final static String[] PROTOCAL_SNS_OBJECT_CLASSES = {"com.tencent.mm.protocal.b.aqi", "com.tencent.mm.protocal.b.aqi"}; 26 | 27 | final static String[] PROTOCAL_SNS_OBJECT_METHODS = {"a", "a"}; 28 | 29 | final static String[] PROTOCAL_SNS_OBJECT_USERID_FIELDS = {"iYA", "iYA"}; 30 | 31 | final static String[] PROTOCAL_SNS_OBJECT_NICKNAME_FIELDS = {"jyd", "jyd"}; 32 | 33 | final static String[] PROTOCAL_SNS_OBJECT_TIMESTAMP_FIELDS = {"fpL", "fpL"}; 34 | 35 | final static String[] PROTOCAL_SNS_OBJECT_COMMENTS_FIELDS = {"jJX", "jJX"}; 36 | 37 | final static String[] PROTOCAL_SNS_OBJECT_LIKES_FIELDS = {"jJU", "jJU"}; 38 | 39 | final static String[] SNS_OBJECT_EXT_AUTHOR_NAME_FIELDS = {"jyd", "jyd"}; 40 | 41 | final static String[] SNS_OBJECT_EXT_REPLY_TO_FIELDS = {"jJM", "jJM"}; 42 | 43 | final static String[] SNS_OBJECT_EXT_COMMENT_FIELDS = {"fsI", "fsI"}; 44 | 45 | final static String[] SNS_OBJECT_EXT_AUTHOR_ID_FIELDS = {"iYA", "iYA"}; 46 | 47 | static String PROTOCAL_SNS_DETAIL_CLASS; 48 | 49 | static String PROTOCAL_SNS_DETAIL_METHOD; 50 | 51 | static String SNS_XML_GENERATOR_CLASS; 52 | 53 | static String SNS_XML_GENERATOR_METHOD; 54 | 55 | static String PROTOCAL_SNS_OBJECT_CLASS; 56 | 57 | static String PROTOCAL_SNS_OBJECT_METHOD; 58 | 59 | static String PROTOCAL_SNS_OBJECT_USERID_FIELD; 60 | 61 | static String PROTOCAL_SNS_OBJECT_NICKNAME_FIELD; 62 | 63 | static String PROTOCAL_SNS_OBJECT_TIMESTAMP_FIELD; 64 | 65 | static String PROTOCAL_SNS_OBJECT_COMMENTS_FIELD; 66 | 67 | static String PROTOCAL_SNS_OBJECT_LIKES_FIELD; 68 | 69 | static String SNS_OBJECT_EXT_AUTHOR_NAME_FIELD; 70 | 71 | static String SNS_OBJECT_EXT_REPLY_TO_FIELD; 72 | 73 | static String SNS_OBJECT_EXT_COMMENT_FIELD; 74 | 75 | static String SNS_OBJECT_EXT_AUTHOR_ID_FIELD; 76 | 77 | static void checkWeChatVersion(String version) { 78 | for (int i=0;i tweetList; 21 | 22 | IntervalThread(ArrayList tweetList) { 23 | this.tweetList = tweetList; 24 | } 25 | 26 | @Override 27 | public void run() { 28 | super.run(); 29 | while (true) { 30 | try { 31 | Thread.sleep(500); 32 | } catch (InterruptedException e) { 33 | e.printStackTrace(); 34 | } 35 | if (!Config.enabled || !Config.ready) { 36 | return; 37 | } 38 | saveToFile(); 39 | } 40 | } 41 | 42 | private void saveToFile() { 43 | JSONArray tweetListJSON = new JSONArray(); 44 | 45 | for (int tweetIndex=0; tweetIndex tweetList = new ArrayList(); 34 | String lastTimelineId = ""; 35 | Thread intervalSaveThread = null; 36 | Context appContext = null; 37 | String wechatVersion = ""; 38 | 39 | @Override 40 | public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable { 41 | if (!lpparam.packageName.equals("com.tencent.mm")) 42 | return; 43 | 44 | loadFromSharedPreference(); 45 | 46 | findAndHookMethod("com.tencent.mm.ui.LauncherUI", lpparam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() { 47 | @Override 48 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 49 | super.afterHookedMethod(param); 50 | if (appContext != null) { 51 | return; 52 | } 53 | XposedBridge.log("LauncherUI hooked."); 54 | appContext = ((Activity)param.thisObject).getApplicationContext(); 55 | PackageInfo pInfo = appContext.getPackageManager().getPackageInfo(lpparam.packageName, 0); 56 | if (pInfo != null) 57 | wechatVersion = pInfo.versionName; 58 | XposedBridge.log("WeChat version=" + wechatVersion); 59 | Config.checkWeChatVersion(wechatVersion); 60 | 61 | if (!Config.ready) { 62 | return; 63 | } 64 | 65 | hookMethods(lpparam); 66 | } 67 | }); 68 | 69 | } 70 | 71 | private void hookMethods(final LoadPackageParam lpparam) { 72 | findAndHookMethod(Config.PROTOCAL_SNS_DETAIL_CLASS, lpparam.classLoader, Config.PROTOCAL_SNS_DETAIL_METHOD, int.class, Object[].class, new XC_MethodHook() { 73 | @Override 74 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 75 | super.afterHookedMethod(param); 76 | loadFromSharedPreference(); 77 | if (!Config.enabled || !Config.ready) { 78 | intervalSaveThread = null; 79 | tweetList.clear(); 80 | return; 81 | } 82 | Class atp = XposedHelpers.findClass(Config.PROTOCAL_SNS_DETAIL_CLASS, lpparam.classLoader); 83 | Class Parser = XposedHelpers.findClass(Config.SNS_XML_GENERATOR_CLASS, lpparam.classLoader); 84 | Method parseMethod = Parser.getMethod(Config.SNS_XML_GENERATOR_METHOD, atp); 85 | try { 86 | String result = (String) parseMethod.invoke(this, param.thisObject); 87 | if (!getTimelineId(result).equals(lastTimelineId)) 88 | currentTweet.clear(); 89 | parseTimelineXML(result); 90 | addTweetToListNoRepeat(); 91 | lastTimelineId = getTimelineId(result); 92 | } catch (Exception e) { 93 | 94 | } 95 | if (intervalSaveThread == null) { 96 | intervalSaveThread = new IntervalThread(tweetList); 97 | intervalSaveThread.start(); 98 | } 99 | } 100 | }); 101 | 102 | findAndHookMethod(Config.PROTOCAL_SNS_OBJECT_CLASS, lpparam.classLoader, Config.PROTOCAL_SNS_OBJECT_METHOD, int.class, Object[].class, new XC_MethodHook() { 103 | @Override 104 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 105 | super.afterHookedMethod(param); 106 | loadFromSharedPreference(); 107 | if (!Config.enabled || !Config.ready) { 108 | tweetList.clear(); 109 | return; 110 | } 111 | Object aqiObject = param.thisObject; 112 | parseSnsObject(aqiObject); 113 | } 114 | }); 115 | } 116 | 117 | private void loadFromSharedPreference() { 118 | XSharedPreferences pref = new XSharedPreferences(Main.class.getPackage().getName(), "config"); 119 | pref.makeWorldReadable(); 120 | pref.reload(); 121 | Config.enabled = pref.getBoolean("enabled", false); 122 | Config.outputFile = pref.getString("outputFile", Environment.getExternalStorageDirectory() + "/moments_output.json"); 123 | } 124 | 125 | private String getTimelineId(String xmlResult) { 126 | Pattern idPattern = Pattern.compile(""); 127 | Matcher idMatcher = idPattern.matcher(xmlResult); 128 | if (idMatcher.find()) { 129 | return idMatcher.group(1); 130 | } else { 131 | return ""; 132 | } 133 | } 134 | 135 | private void parseTimelineXML(String xmlResult) throws Throwable { 136 | Pattern userIdPattern = Pattern.compile(""); 137 | Pattern contentPattern = Pattern.compile("", Pattern.DOTALL); 138 | Pattern mediaPattern = Pattern.compile(".*?.*?"); 139 | Pattern timestampPattern = Pattern.compile(""); 140 | 141 | Matcher userIdMatcher = userIdPattern.matcher(xmlResult); 142 | Matcher contentMatcher = contentPattern.matcher(xmlResult); 143 | Matcher mediaMatcher = mediaPattern.matcher(xmlResult); 144 | Matcher timestampMatcher = timestampPattern.matcher(xmlResult); 145 | 146 | currentTweet.id = getTimelineId(xmlResult); 147 | 148 | currentTweet.rawXML = xmlResult; 149 | 150 | if (timestampMatcher.find()) { 151 | currentTweet.timestamp = Integer.parseInt(timestampMatcher.group(1)); 152 | } 153 | 154 | if (userIdMatcher.find()) { 155 | currentTweet.authorId = userIdMatcher.group(1); 156 | } 157 | 158 | if (contentMatcher.find()) { 159 | currentTweet.content = contentMatcher.group(1); 160 | } 161 | 162 | while (mediaMatcher.find()) { 163 | boolean flag = true; 164 | for (int i=0;i likes = new ArrayList(); 16 | public ArrayList comments = new ArrayList(); 17 | public ArrayList mediaList = new ArrayList(); 18 | public String rawXML = ""; 19 | public long timestamp = 0; 20 | public boolean ready = false; 21 | 22 | public void print() { 23 | XposedBridge.log("================================"); 24 | XposedBridge.log("id: " + this.id); 25 | XposedBridge.log("Author: " + this.author); 26 | XposedBridge.log("Content: " + this.content); 27 | XposedBridge.log("Likes:"); 28 | for (int i=0; i(this.likes); 49 | newTweet.comments = new ArrayList(this.comments); 50 | newTweet.mediaList = new ArrayList(this.mediaList); 51 | newTweet.rawXML = this.rawXML; 52 | newTweet.timestamp = this.timestamp; 53 | return newTweet; 54 | } 55 | 56 | public void clear() { 57 | id = ""; 58 | author = ""; 59 | content = ""; 60 | authorId = ""; 61 | likes.clear(); 62 | comments.clear(); 63 | mediaList.clear(); 64 | rawXML = ""; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/me/chiontang/wechatmomentexport/models/User.java: -------------------------------------------------------------------------------- 1 | package me.chiontang.wechatmomentexport.models; 2 | 3 | /** 4 | * Created by chiontang on 2/5/16. 5 | */ 6 | public class User { 7 | public String userId; 8 | public String userName; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_preference.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | 25 | 26 | 35 | 36 | 45 | 46 | 53 | 54 |