内容简介:算是第一次接触Go语言,从Runoob看了一中午的基础教程,了解了一些常用语法。最近看到掘金有一位前端大佬发布了两篇 Nodejs定时给女朋友发送暖心消息的教程,决定自己也撸一个差不多的脚本。当然这篇文章很水,只能说是学会了golang的基本使用,通过面相过程的方式做了一个简单的实现。 通过实现这个小玩意儿,算是开始入go坑了吧,请多关照。在本项目中,我使用golang来整理微信聊天记录,然后拼接成HTML,发到自己的邮箱。 这里提供两篇其他的教程,有女朋友的可以学习一下:1.
算是第一次接触 Go 语言,从Runoob看了一中午的基础教程,了解了一些常用语法。最近看到掘金有一位前端大佬发布了两篇 Nodejs定时给女朋友发送暖心消息的教程,决定自己也撸一个差不多的脚本。当然这篇文章很水,只能说是学会了golang的基本使用,通过面相过程的方式做了一个简单的实现。 通过实现这个小玩意儿,算是开始入go坑了吧,请多关照。
写在前面
在本项目中,我使用golang来整理微信聊天记录,然后拼接成HTML,发到自己的邮箱。 这里提供两篇其他的教程,有女朋友的可以学习一下:
1. 用Node+wechaty写一个爬虫脚本每天定时给女朋友发微信暖心话 2. 用Node + EJS写一个爬虫脚本每天定时女朋友发一封暖心邮件
当然,如果你没有女朋友的话,可以自己发到自己的邮箱,本篇采用这种做法。
少废话,先看东西:
技术实现
- Golang操作sqlite,获取聊天记录
- 使用 net/smtp 发送邮件
- 关于获取微信聊天记录内容 (目前在研究一个开源py代码,分析一下微信是如何操作 sqlite 的)
代码部分
Golang操作sqlite,获取聊天记录
微信的聊天记录是存储在sqlite中的,部分设备拿到没有经过任何加密的**.sqlite** 文件
目前在数据库中发现了这些表,还有一些以**ChatExt2_ 为前缀的表,后面与 Chat_ 前缀的表一一对应。 每一个 Chat_ 表代表与一个好友的聊天记录,目前还没有找到高效的方法确定表名与好友的关系,(Friend表居然是空的,不知道是不是我打开的方式不对,这里有待研究), 点开 Chat_**表可以看到和好友的聊天记录,当然这些数据是和手机上看到的数据是一样的,比如你在手机上左滑删除了好友,那么聊天记录在这里面也是找不到的。
这里我用了一个最笨的方法去找到了那个我需要的表,那就是一一遍历表,然后统计出表中的数据条数,最后冒泡 排序 一下,找到这个记录最多的那个表。(我很确定记录最多的那个表就是我想要的那个表)
这里可以看到 Chat_ ,**ChatExt2_ 排名最高,但是实际文本内容在 Chat_**表中。 (93020条数据,其中包括682条'对方已撤回'和'被对方拒收'这样的系统提示)
具体表结构是这样的
- CreateTime: 发消息时间
- Desc: 目前只看到两个值 0表示自己发的,1表示对方
- ImgStatus: 猜测是记录视频,音频,图片和表情包的
- MsgLocalID: 消息本地ID
- Message: 消息主体 (基本都是文本和emoji,图片和表情以xml的形式出现,图片音视频以出现,当然里面都是id,没有实际的媒体URL)
- MsgSvrId: 消息在服务器的ID
- Status: 目前看到的是2表示自己发的,4表示对方发的/系统提示(已撤回,已拒收),5表示被拒收的消息(没有发过去,只存到了本地数据库)
- TableVer: 都是1,没发现什么问题
- Type: 1是文本,47是表情,49是分享卡片,10000是系统消息,别的就不分析了,可以通过Type和Status判断是对方发的还是系统发的
这里我主要是通过传进来两个时间去查询该时间段的消息。
// 记得import "database/sql", _ "github.com/mattn/go-sqlite3" 来操作数据库 // 聊天记录struct type Message struct { create_time int64 content string status int msg_type int } /** * 数据库操作 */ func getMessages(start, end int64) []Message { db, err := sql.Open("sqlite3", DatabasePath) checkErr(err) sql := "SELECT `CreateTime`, `Message`, `Status`, `Type` FROM '" + DatabaseName + "' WHERE `CreateTime` BETWEEN " + strconv.FormatInt(start,10) + " AND " + strconv.FormatInt(end,10) + " ORDER BY `CreateTime` ASC" rows, err := db.Query(sql) checkErr(err) data := make([]Message, 0) for rows.Next() { var message Message err = rows.Scan(&message.create_time, &message.content, &message.status, &message.msg_type) checkErr(err) data = append(data, message) } db.Close() return data } 复制代码
时间的生成上我觉得Go有点麻烦,也可能是我姿势不对
/** * 时间获取封装 */ func getTime() (int64, int64) { start_time := time.Now().Format(TimeFormat) t, _ := time.Parse(TimeFormat, start_time) start_time_unix := (BeignTime - t.Unix() + SoLikeU + 8 * 3600) // 记得减去8个小时的时间戳 end_time_unix := start_time_unix + (26 * 3600) // 到第二天凌晨两点吧,因为你也会失眠睡不着 return start_time_unix, end_time_unix } 复制代码
这里是想通过获取某一天的0点时刻到第二天的2点 (也就是一天的时间+半夜睡不着的那段时间)
中间BeignTime和SoLikeU都是我自己定义的常量,后续在完整代码提供
这里使用了一个方法去获取当天的0点时刻
timeStr := time.Now().Format("2006-01-02") fmt.Println("timeStr:", timeStr) t, _ := time.Parse("2006-01-02", timeStr) timeNumber := t.Unix() fmt.Println("timeNumber:", timeNumber) 复制代码
但是这个方法会返回当天8点而不是0点,所以需要自己手动减去8个小时。
另外go的时间格式化上,并不像其他语言那样用"yyyy-mm-dd hh:mm:ss", 而是用了一个固定的时间: 2006/1/2 15:04:05 , 当初在学习时间格式转化的时候,搜了很多文章发现都是写了这么一个时间,总觉得那些博主不够专业,怎么随随便便的写个时间而不是用ymd,后来索性改成了自己的某个纪念日,结果是有问题的。
所以我觉得时间格式转换对新手很不友好,当然写过之后就记得了 (2006/1/2 15:04:05 分别是1,2,下午3点,4,5)
拼接HTML
当然这里没有什么好讲的,我采用的是最原始的HTML拼接方式,这种方式感觉在前端开发的时候经常用到。
/** * HTML生成 */ func createHTML(data []Message, unix int64) string { html_header := "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\"content=\"width=device-width, initial-scale=1.0\"><meta http-equiv=\"X-UA-Compatible\"content=\"ie=edge\"><title>mango</title></head><style>html,body{height:100%;margin:0;padding:0}.header{width:100%;height:100px}.red-span{font-size:24px;color:rgb(221,73,73)}</style><body>" html_tip := "<div class=\"header\">" + "<div style=\"width:100%; margin: 40px auto;font-size:20px; color:#5f5e5e;text-align:center\">" + "<span class=\"red-span\"> " + time.Unix(unix, 0).Format(TimeFormat) + " </span> <br>" + "<span>你们一共聊了</span>" + "<span class=\"red-span\">" + strconv.Itoa(len(data)) + "</span>" + "<span>句</span>" + "</div>" + "</div>" html_content := "<div class=\"lite-chatbox\">" // 切片遍历是个坑 for index, msg := range data { if index % 10 == 0 { html_content += "<div class=\"tips\">" + "<span>" + time.Unix(msg.create_time, 0).Format("15:04:05") + "</span>" + "</div>" } if msg.status == 4 { // 表示对方发来的消息 html_content += "<div class=\"cleft cmsg\">" + "<img class=\"headIcon radius\" ondragstart=\"return false;\" oncontextmenu=\"return false;\" src=\"" + MangoAvatar + "\" />" + "<span class=\"name\">" + MangoName + "</span>" } else { // 自己发送的消息 html_content += "<div class=\"cright cmsg\">" + "<img class=\"headIcon radius\" ondragstart=\"return false;\" oncontextmenu=\"return false;\" src=\"" + XzkAvatar + "\" />" + "<span class=\"name\">" + XzkName + "</span>" } html_content += "<span class=\"content\">" + msg.content + "</span>" + "</div>" } html_bottom := "<div class=\"header\">" + "<div style=\"width:100%; margin: 40px auto;font-size:20px; color:#5f5e5e;text-align:center\">" + "<span>加油鸭</span><br/>" + "<span>记得背单词,刷算法,减肥</span><br/>" + "</div>" + "</div>" + "</div>" + "</body></html>" html_css := "<style>.lite-chatbox{padding:0;width:100%;position:relative;overflow-y:auto;overflow-x:hidden;font:18px Helvetica,\"PingFang SC\",\"Microsoft YaHei\",sans-serif}.lite-chatbox .cmsg{position:relative;margin:4px 7px;min-height:50px;border:0}.lite-chatbox .cright{text-align:right;margin-left:64px}.lite-chatbox .cleft{text-align:left;margin-right:64px}.lite-chatbox img.headIcon{width:34px;height:34px;top:9px;position:absolute;border:1px solid #c5d4c4}.lite-chatbox .name{color:#8b8b8b;font-size:12px;display:block;line-height:18px}.lite-chatbox .name .htitle{display:inline-block;padding:0 3px;background-color:#ccc;color:#fff;-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;margin-right:4px;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;max-width:50px}.lite-chatbox .content{word-break:break-all;word-wrap:break-word;text-align:left;position:relative;display:inline-block;font-size:15px;padding:10px 15px;line-height:20px;min-width:9px;min-height:18px}.lite-chatbox .content img{width:100%;height:auto}.lite-chatbox .content a{color:#0072C1;margin:0 5px;cursor:hand}.lite-chatbox .tips{margin:12px;text-align:center;font-size:12px}.lite-chatbox .tips span{display:inline-block;padding:4px;background-color:#ccc;color:#fff;-moz-border-radius:6px;-webkit-border-radius:6px;border-radius:6px}.lite-chatbox img.radius{-moz-border-radius:100%;-webkit-border-radius:100%;border-radius:100%}.lite-chatbox .cright img.headIcon{right:0}.lite-chatbox .cleft img.headIcon{left:0}.lite-chatbox .cright .name{margin:0 48px 2px 0}.lite-chatbox .cleft .name{margin:0 0 2px 48px}.lite-chatbox .cright .content{margin:0 48px 0 0;-webkit-border-radius:20px 0 20px 20px;border-radius:20px 0 20px 20px;color:#fff;background:-webkit-linear-gradient(70deg,#3FD1E1 0%,#44D7CD 100%);background:linear-gradient(20deg,#3f8fe1cc 0%,#44d7c9 100%);-webkit-box-shadow:5px 5px 15px 0 rgba(102,102,102,0.15);box-shadow:5px 5px 15px 0 rgba(102,102,102,0.15)}.lite-chatbox .cleft .content{margin:0 0 0 48px;-webkit-border-radius:0 20px 20px 20px;border-radius:0 20px 20px 20px;color:#666;border:1px solid rgba(0,0,0,0.05);background:#FFF;-webkit-box-shadow:5px 5px 15px 0 rgba(102,102,102,0.1);box-shadow:5px 5px 15px 0 rgba(102,102,102,0.1)}.lite-chatbox .cright .content:after{right:-12px;top:8px}.lite-chatbox .cleft .content:after{left:-12px;top:8px}.lite-chatbox .tips .tips-primary{background-color:#3986c8}.lite-chatbox .tips .tips-success{background-color:#49b649}.lite-chatbox .tips .tips-info{background-color:#5bb6d1}.lite-chatbox .tips .tips-warning{background-color:#eea948}.lite-chatbox .tips .tips-danger{background-color:#e24d48}.lite-chatbox .name .admin{background-color:#72D6A0}.lite-chatbox .name .owner{background-color:#F2BF25}</style>" html := html_header + html_css + html_tip + html_content + html_bottom + html_css return html } 复制代码
这里用到了一个前端第三方框架, LiteWebChat_Frame ,可以快速搭建聊天界面。
在封装HTML的时候,一定要注意所有的css必须本地化,不可以采用引入的方式去加载css,但是图片是可以的。这跟邮件的某些安全策略有关吧,所以我在中间拼了一个超级长的style。
发送邮件
发送邮件用到了 "net/smtp" 包。
这里基本配置应该可以看得懂,主要是登录账号,发送邮件
最重要的是设置content_type, 只有设置为 Content-Type: text/html; charset=UTF-8 才可以发送这种html模板的邮件。
**另外有一个大坑。**不知道在什么时候开始,很多邮箱登录都需要使用授权码登录而不是密码。这个在之前某一篇 PHP 笔记中也提到过,大家可以去邮箱的设置中心查看授权码。这里我用163邮箱测试。
最后选择163而不是qq的原因是,QQ可能对资源加载做了更多的限制,我在HTML中插入的超长css代码被过滤掉了,最后显示的是一个乱版的邮件,而163没有任何问题。
// import "net/smtp" /** * 发送邮件 */ func sendMail(html string) { auth := smtp.PlainAuth("", "需要登录的邮箱", "授权码", "smtp.163.com") to := []string{"发送的邮箱"} nickname := "昵称" user := "显示的邮箱" subject := "邮件标题" content_type := "Content-Type: text/html; charset=UTF-8" body := html msg := []byte("To: " + strings.Join(to, ",") + "\r\nFrom: " + nickname + "<" + user + ">\r\nSubject: " + subject + "\r\n" + content_type + "\r\n\r\n" + body) err := smtp.SendMail("smtp.163.com:25", auth, user, to, msg) checkErr(err) fmt.Println("邮件发送成功") } 复制代码
总结
对Golang总结:
第一次写Golang,发现有些地方确实简单,例如 := 定义变量,但是和 Python 无需事先声明变量比,新手可能很容易忘记 : 而只写 = , 其次元组很方便,可以接受多个返回值,第一次接触不借助中间值对ab进行交换也是在go中学到的(a, b = b, a)
go也提供了很多好用的包,这个需要慢慢学习。
时间格式化上可能对新手有些懵逼,后续自己可以手动封装一个时间管理util。
当然语言只是工具,了解了语法之后,简单的上手还是很容易的,接触过Python,Swift后,觉得go的语法也很容易接受
对项目总结
该项目只是通过面相过程的方式去实现了一个小小的功能,没有任何的优化,所以后续我需要去深入了解GO的使用。
同时给自己埋下两个坑吧:
- 学会Go的高级用法
- 既然已经有了聊天记录资源,是否可以通过机器学习等技术训练一个对话机器人。
完整代码 (代码low,请见谅)
package main import ( "fmt" "time" "strings" "strconv" "net/smtp" "io/ioutil" "database/sql" _ "github.com/mattn/go-sqlite3" ) // 聊天记录struct type Message struct { create_time int64 content string status int msg_type int } const ( DatabasePath = "/Users/xzk/Downloads/Applications/com.tencent.xin/Documents/4fa8684bdeae2190a5963a404b0cf007/DB/MM.sqlite" DatabaseName = "Chat_1a6c5c5787382c86b99ba9f72c0732dc" TimeFormat = "2006/1/2" BeignTime = 1551369600 // 19.3.1 00:00:00 SoLikeU = 1525795200 // 2018/5/9 18:18:21 MangoName = ":sparkling_heart:对方昵称,用在HTML" MangoAvatar = "对方的头像,用在HTML" XzkName = "我的昵称,用在HTML" XzkAvatar = "我的头像,用在HTML" // html输出路径,做了存储 FilePath = "/Users/xzk/Desktop/xzk/mango/" ) func main() { now := time.Now().Format("2006/1/2 15:04:05") start_time, end_time := getTime() println("现在是: ", now); println("今天整理的是: [ ", time.Unix(start_time, 0).Format("2006/1/2 15:04:05"), " - " , time.Unix(end_time, 0).Format("2006/1/2 15:04:05"), " ] 的故事") data := getMessages(start_time, end_time) // for _, msg:= range data { // fmt.Println(msg) // } html := createHTML(data, start_time) saveAndSend(html, start_time) fmt.Println("Bye~") } /** * 时间获取封装 */ func getTime() (int64, int64) { start_time := time.Now().Format(TimeFormat) t, _ := time.Parse(TimeFormat, start_time) start_time_unix := (BeignTime - t.Unix() + SoLikeU + 8 * 3600) // 记得减去8个小时的时间戳 end_time_unix := start_time_unix + (26 * 3600) // 到第二天凌晨两点吧,因为你也会失眠睡不着 return start_time_unix, end_time_unix } /** * 数据库操作 */ func getMessages(start, end int64) []Message { db, err := sql.Open("sqlite3", DatabasePath) checkErr(err) sql := "SELECT `CreateTime`, `Message`, `Status`, `Type` FROM '" + DatabaseName + "' WHERE `CreateTime` BETWEEN " + strconv.FormatInt(start,10) + " AND " + strconv.FormatInt(end,10) + " ORDER BY `CreateTime` ASC" rows, err := db.Query(sql) checkErr(err) data := make([]Message, 0) for rows.Next() { var message Message err = rows.Scan(&message.create_time, &message.content, &message.status, &message.msg_type) checkErr(err) data = append(data, message) } db.Close() return data } /** * HTML生成 */ func createHTML(data []Message, unix int64) string { html_header := "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\"content=\"width=device-width, initial-scale=1.0\"><meta http-equiv=\"X-UA-Compatible\"content=\"ie=edge\"><title>mango</title></head><style>html,body{height:100%;margin:0;padding:0}.header{width:100%;height:100px}.red-span{font-size:24px;color:rgb(221,73,73)}</style><body>" html_tip := "<div class=\"header\">" + "<div style=\"width:100%; margin: 40px auto;font-size:20px; color:#5f5e5e;text-align:center\">" + "<span class=\"red-span\"> " + time.Unix(unix, 0).Format(TimeFormat) + " </span> <br>" + "<span>你们一共聊了</span>" + "<span class=\"red-span\">" + strconv.Itoa(len(data)) + "</span>" + "<span>句</span>" + "</div>" + "</div>" html_content := "<div class=\"lite-chatbox\">" for index, msg := range data { if index % 10 == 0 { html_content += "<div class=\"tips\">" + "<span>" + time.Unix(msg.create_time, 0).Format("15:04:05") + "</span>" + "</div>" } if msg.status == 4 { // 表示对方发来的消息 html_content += "<div class=\"cleft cmsg\">" + "<img class=\"headIcon radius\" ondragstart=\"return false;\" oncontextmenu=\"return false;\" src=\"" + MangoAvatar + "\" />" + "<span class=\"name\">" + MangoName + "</span>" } else { // 自己发送的消息 html_content += "<div class=\"cright cmsg\">" + "<img class=\"headIcon radius\" ondragstart=\"return false;\" oncontextmenu=\"return false;\" src=\"" + XzkAvatar + "\" />" + "<span class=\"name\">" + XzkName + "</span>" } html_content += "<span class=\"content\">" + msg.content + "</span>" + "</div>" } html_bottom := "<div class=\"header\">" + "<div style=\"width:100%; margin: 40px auto;font-size:20px; color:#5f5e5e;text-align:center\">" + "<span>加油鸭</span><br/>" + "<span>记得背单词,刷算法,减肥</span><br/>" + "</div>" + "</div>" + "</div>" + "</body></html>" html_css := "<style>.lite-chatbox{padding:0;width:100%;position:relative;overflow-y:auto;overflow-x:hidden;font:18px Helvetica,\"PingFang SC\",\"Microsoft YaHei\",sans-serif}.lite-chatbox .cmsg{position:relative;margin:4px 7px;min-height:50px;border:0}.lite-chatbox .cright{text-align:right;margin-left:64px}.lite-chatbox .cleft{text-align:left;margin-right:64px}.lite-chatbox img.headIcon{width:34px;height:34px;top:9px;position:absolute;border:1px solid #c5d4c4}.lite-chatbox .name{color:#8b8b8b;font-size:12px;display:block;line-height:18px}.lite-chatbox .name .htitle{display:inline-block;padding:0 3px;background-color:#ccc;color:#fff;-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;margin-right:4px;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;max-width:50px}.lite-chatbox .content{word-break:break-all;word-wrap:break-word;text-align:left;position:relative;display:inline-block;font-size:15px;padding:10px 15px;line-height:20px;min-width:9px;min-height:18px}.lite-chatbox .content img{width:100%;height:auto}.lite-chatbox .content a{color:#0072C1;margin:0 5px;cursor:hand}.lite-chatbox .tips{margin:12px;text-align:center;font-size:12px}.lite-chatbox .tips span{display:inline-block;padding:4px;background-color:#ccc;color:#fff;-moz-border-radius:6px;-webkit-border-radius:6px;border-radius:6px}.lite-chatbox img.radius{-moz-border-radius:100%;-webkit-border-radius:100%;border-radius:100%}.lite-chatbox .cright img.headIcon{right:0}.lite-chatbox .cleft img.headIcon{left:0}.lite-chatbox .cright .name{margin:0 48px 2px 0}.lite-chatbox .cleft .name{margin:0 0 2px 48px}.lite-chatbox .cright .content{margin:0 48px 0 0;-webkit-border-radius:20px 0 20px 20px;border-radius:20px 0 20px 20px;color:#fff;background:-webkit-linear-gradient(70deg,#3FD1E1 0%,#44D7CD 100%);background:linear-gradient(20deg,#3f8fe1cc 0%,#44d7c9 100%);-webkit-box-shadow:5px 5px 15px 0 rgba(102,102,102,0.15);box-shadow:5px 5px 15px 0 rgba(102,102,102,0.15)}.lite-chatbox .cleft .content{margin:0 0 0 48px;-webkit-border-radius:0 20px 20px 20px;border-radius:0 20px 20px 20px;color:#666;border:1px solid rgba(0,0,0,0.05);background:#FFF;-webkit-box-shadow:5px 5px 15px 0 rgba(102,102,102,0.1);box-shadow:5px 5px 15px 0 rgba(102,102,102,0.1)}.lite-chatbox .cright .content:after{right:-12px;top:8px}.lite-chatbox .cleft .content:after{left:-12px;top:8px}.lite-chatbox .tips .tips-primary{background-color:#3986c8}.lite-chatbox .tips .tips-success{background-color:#49b649}.lite-chatbox .tips .tips-info{background-color:#5bb6d1}.lite-chatbox .tips .tips-warning{background-color:#eea948}.lite-chatbox .tips .tips-danger{background-color:#e24d48}.lite-chatbox .name .admin{background-color:#72D6A0}.lite-chatbox .name .owner{background-color:#F2BF25}</style>" html := html_header + html_css + html_tip + html_content + html_bottom + html_css return html } /** * 存储生成的网站 */ func saveAndSend(html string, unix int64) { file_path := FilePath + time.Unix(unix, 0).Format("2006-01-02") + ".html" println(file_path) _, err := ioutil.ReadFile(file_path) if err != nil { // 文件不存在, 写入文件存档 bytes := []byte(html) err := ioutil.WriteFile(file_path, bytes, 0644) checkErr(err) fmt.Println("文件已经保存,开始发送邮件") // 发送邮件 sendMail(html) } else { // 文件已经存在,可能已经发送过email fmt.Println(file_path + " 文件已经存在,可能已发送过邮件") } } /** * 发送邮件 */ func sendMail(html string) { auth := smtp.PlainAuth("", "xxx@163.com", "xxx", "smtp.163.com") to := []string{"xxx@163.com"} nickname := "" user := "xxx@163.com" subject := "xxxx" content_type := "Content-Type: text/html; charset=UTF-8" body := html msg := []byte("To: " + strings.Join(to, ",") + "\r\nFrom: " + nickname + "<" + user + ">\r\nSubject: " + subject + "\r\n" + content_type + "\r\n\r\n" + body) err := smtp.SendMail("smtp.163.com:25", auth, user, to, msg) checkErr(err) fmt.Println("邮件发送成功") } /** * 错误检测 */ func checkErr(err error) { if err != nil { panic(err) } } 复制代码
写在尾部: 我想学习如何通过这些聊天记录来训练一个聊天机器人,但是作为开发狗对机器学习或者其他技术并不了解,希望懂的big brother给指条明路。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 小白解密安卓机上微信聊天记录
- python可视化文本分析(1)—分析QQ班群聊天记录宏观
- React 折腾记 - (7) 基于React+Antd封装聊天记录(用到memo,lazy, Suspense)这些
- 手把手教你读取Android版微信和手Q的聊天记录(仅作技术研究学习)
- Telegram 支持删除聊天双方设备中的消息记录
- 3.64亿条中国用户聊天信息泄露到网上,涉及微信支付记录
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HTML 5 与 CSS 3 权威指南
陆凌牛 / 机械工业出版社华章公司 / 2011-4-7 / 69.00
如果你是一位有前瞻性的web前端工作者,那么你一定会从本书中受益,因为它就是专门为你打造的。 《HTML 5与CSS 3权威指南》内容系统而全面,详尽地讲解了html 5和css 3的所有新功能和新特性;技术新颖,所有知识点都紧跟html 5与css 3的最新发展动态(html 5和css 3仍在不断完善之中);实战性强(包含246个示例页面),不仅每个知识点都配有精心设计的小案例(便于动手......一起来看看 《HTML 5 与 CSS 3 权威指南》 这本书的介绍吧!