1.需求方:
1.已知账户,获取该账户下每天的发视频数据,同时获取一个视频连续30天的点赞数,分享数,评论数。
2.需求方确定在标题中附带来源和作者相关信息,从标题中提取该部分信息,作为原创和作者绩效考核。
2.实现方:
1.在douyin开放平台中注册,并绑定该抖音账户,获取APPID和APP_SECRET。
3.实现过程:
3.1 授权步骤一 通过用户授权获取授权码code
该接口只适用于抖音获取授权临时票据(code)。
注意:
抖音的 OAuth API 以https://open.douyin.com/开头。 该 URL 不是用来请求的,
需要展示给用户用于扫码,在抖音 APP 支持端内唤醒的版本内打开的话会弹出客户端原生授权页面。 获取的 code
可以用来调用 oauth/access_token/ 换取用户 acccess_token。
若需要授权多个 scope 需要把多个 scope 使用英文 “,” 拼接,例如 scope1,scope2,scope3 。
使用本接口前提:首先你需要去官网申请,使你的应用可以使用特定的 Scope,具体需要哪些 Scope,请查看各接口定义。 其次你需要在本 URL 的
scope 字段中填上用户需要授权给你的 Scope。 用户授权通过后,你才可以调用相应的接口。
/**
* 抖音授权
*
* @param request
* @param response
*/
@GetMapping("/videoConfig/douYin/getCode")
@ResponseBody
public R getDouYinCode(HttpServletRequest request, HttpServletResponse response) {
VideoConfig dyInfo = videoConfigMapper.selectByType("dy");
if (dyInfo == null) {
logger.error("=============无抖音平台数据==============");
throw new RuntimeException("无抖音平台数据");
}
// https://open.douyin.com/platform/oauth/connect/?client_key=xxxxxxx&response_type=code&scope=video.list&redirect_uri=https://baidu.com/login
//需要用户开放的权限
String scope = "trial.whitelist,video.list,renew_refresh_token";
//回调地址 https://baidu.com/login 为临时回调地址的,正式上线要用线上的域名
//VIDEO_AUTH_CALLBACK_URL 为网站的回调域名
String redirect_uri = VIDEO_AUTH_CALLBACK_URL + "/douYin/authCallback";
String requestUrl = "https://open.douyin.com/platform/oauth/connect/?client_key=" + dyInfo.getAppId()
+ "&response_type=code" + "&scope=" + scope + "&redirect_uri=" + redirect_uri;
return R.ok().data("url", requestUrl);
}
}
3.2 授权步骤二 获取access_token
该接口用于获取用户授权第三方接口调用的凭证 access_token;该接口适用于抖音/头条授权。
注意:
抖音的 OAuth API 以https://open.douyin.com/开头。 头条的 OAuth API
以https://open.snssdk.com/开头。 西瓜的 OAuth API
以https://open-api.ixigua.com/开头。 access_token
为用户授权第三方接口调用的凭证,存储在客户端,可能会被窃取,泄漏后可能会发生用户隐私数据泄漏的风险,建议存储在服务端。 获取到
access_token 后授权临时票据 (code) 不要再授权刷新,否则会导致上一次获取的 code 过期。
上面/videoConfig/douYin/getCode请求将回调下面请求地址,并在request中返回code,然后请求获取token,并记录access_token,refresh_token ,expires_in,refresh_expires_in,open_id
@GetMapping("/douYin/authCallback")
@ResponseBody
public String dyAuthCallback(HttpServletRequest request) throws ParseException {
// 请求参数
VideoConfig dyInfo = videoConfigMapper.selectByType("dy");
//请求参数
String code = request.getParameter("code");
// String code = "xxxxxxxxxxxxxxxxxx";
Map<String, String> map = new HashMap<>();
map.put("client_key", dyInfo.getAppId());
map.put("client_secret", dyInfo.getAppSecret());
map.put("code", code);
map.put("grant_type", "authorization_code");
//请求地址
String url = dyInfo.getGetTokenUrl();
HttpRequest httpRequest = HttpRequest.get(url, map, Boolean.TRUE);
String result = httpRequest.body();
JSONObject jasonObject = JSONObject.parseObject(result);
JSONObject data = (JSONObject) jasonObject.get("data");
System.out.println("jasonObject = " + jasonObject);
int errorCode = Integer.parseInt(data.get("error_code").toString());
if (errorCode == 0) {
//查询数据库里面的内容
String open_id = data.get("open_id").toString();
String access_token = data.get("access_token").toString();
String refresh_token = data.get("refresh_token").toString();
//记录token,将他的过期时间也记录下来,查询时过期,就去更新
long expires_in = Long.parseLong(data.get("expires_in").toString());
long refresh_expires_in = Long.parseLong(data.get("refresh_expires_in").toString());
long currentTimeMillis = System.currentTimeMillis();
long tokenExpiresInLong = currentTimeMillis + expires_in * 1000;
long refreshExpiresInLong = currentTimeMillis + refresh_expires_in * 1000;
String tokenExpiresIn = TimeUtil.transferLongToDate("yyyy-MM-dd HH:mm:ss", tokenExpiresInLong);
String refreshExpiresIn = TimeUtil.transferLongToDate("yyyy-MM-dd HH:mm:ss", refreshExpiresInLong);
dyInfo.setOpenId(open_id);
dyInfo.setToken(access_token);
dyInfo.setRefreshToken(refresh_token);
dyInfo.setTokenExpiresIn(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(tokenExpiresIn));
dyInfo.setRefreshExpiresIn(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(refreshExpiresIn));
videoConfigMapper.updateByPrimaryKeySelective(dyInfo);
logger.info("accessToken记录成功");
return "授权成功,请关闭当前窗口!";
}
return "授权失败,请联系管理人员";
}
3.3 授权步骤三 刷新access_token
/**
* 刷新 access_token 的有效期
*/
private void refresh_token() throws ParseException {
logger.info("=================抖音刷新 access_token 的有效期=================");
VideoConfig dyInfo = videoConfigMapper.selectByType("dy");
String client_key = dyInfo.getAppId();
Map<String, String> map = new HashMap<>();
map.put("client_key", client_key);
map.put("grant_type", "refresh_token");
map.put("refresh_token", dyInfo.getRefreshToken());
//请求地址
String url = dyInfo.getGetRefreshTokenUrl();
HttpRequest httpRequest = HttpRequest.post(url, map, Boolean.TRUE).header("Content-Type", "multipart/form-data");
String result = httpRequest.body();
JSONObject jasonObject = JSONObject.parseObject(result);
JSONObject data = (JSONObject) jasonObject.get("data");
logger.info("=================抖音刷新刷新access_token 的有效期:{}=================", jasonObject);
String errorCode = data.get("error_code").toString();
if (errorCode.equals("10008") || errorCode.equals("2190008")) {
//这表示access_token过期 需要重新获取refresh_token 后会获取一个新的 access_token 以及新的超时时间。
//说明要刷新重新获取refresh_token
logger.info("=================抖音刷新刷新重新获取refresh_token=================");
renew_refresh_token();
} else {
//未过期,提取并记录刷新后的access_token
if (errorCode.equals("0")) {
logger.info("=================抖音 未过期,提取并记录刷新后的access_token=================");
String open_id = data.get("open_id").toString();
String access_token = data.get("access_token").toString();
//记录token,将他的过期时间也记录下来,查询时过期,就去更新
long expires_in = Long.parseLong(data.get("expires_in").toString());
long refresh_expires_in = Long.parseLong(data.get("refresh_expires_in").toString());
long currentTimeMillis = System.currentTimeMillis();
long tokenExpiresInLong = currentTimeMillis + expires_in * 1000;
long refreshExpiresInLong = currentTimeMillis + refresh_expires_in * 1000;
String tokenExpiresIn = TimeUtil.transferLongToDate("yyyy-MM-dd HH:mm:ss", tokenExpiresInLong);
String refreshExpiresIn = TimeUtil.transferLongToDate("yyyy-MM-dd HH:mm:ss", refreshExpiresInLong);
dyInfo.setOpenId(open_id);
dyInfo.setToken(access_token);
dyInfo.setRefreshToken(dyInfo.getRefreshToken());
dyInfo.setTokenExpiresIn(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(tokenExpiresIn));
dyInfo.setRefreshExpiresIn(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(refreshExpiresIn));
videoConfigMapper.updateByPrimaryKeySelective(dyInfo);
logger.info("accessToken记录成功");
}
}
// 提取并记录刷新后的access_token 获取成功后再执行一次获取视频列表数据
// 如果access_token刷新后则进行renew_refresh_token操作
}
3.4 授权步骤四 刷新refresh_token
/**
* 该接口用于刷新refresh_token的有效期
*/
private void renew_refresh_token() throws ParseException {
VideoConfig dyInfo = videoConfigMapper.selectByType("dy");
String client_key = dyInfo.getAppId();
Map<String, String> map = new HashMap<>();
map.put("client_key", client_key);
map.put("refresh_token", dyInfo.getRefreshToken());
//请求地址
String url = dyInfo.getGetRenewRefreshTokenUrl();
HttpRequest httpRequest = HttpRequest.post(url, map, Boolean.TRUE).header("Content-Type", "multipart/form-data");
String result = httpRequest.body();
logger.info("=================刷新refresh_token的有效期,数据获取result:{}=================", result);
JSONObject jasonObject = JSONObject.parseObject(result);
JSONObject data = (JSONObject) jasonObject.get("data");
logger.info("=================刷新refresh_token的有效期,数据获取jasonObject:{}=================", jasonObject);
String errorCode = data.get("error_code").toString();
//未过期,提取并记录刷新后的access_token
if (errorCode.equals("0")) {
//过期时间 30天
long expires_in = Long.parseLong(data.get("expires_in").toString());
String refresh_token = data.get("refresh_token").toString();
long currentTimeMillis = System.currentTimeMillis();
System.out.println("currentTimeMillis = " + currentTimeMillis);
long refreshExpiresInLong = currentTimeMillis + expires_in * 1000;
String refreshExpiresIn = TimeUtil.transferLongToDate("yyyy-MM-dd HH:mm:ss", refreshExpiresInLong);
dyInfo.setRefreshToken(refresh_token);
dyInfo.setRefreshExpiresIn(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(refreshExpiresIn));
videoConfigMapper.updateByPrimaryKeySelective(dyInfo);
logger.info("刷新refresh_token记录成功");
}else{
//TODO 给管理员发一个短信需要他认证。
}
}
3.5 获取视频列表
1.执行一个定时器任务,进行数据抓取,并记录数据库中。
2.抖音每次只支持十条数据返回,每次不能全部抓取,分批次读取数据,找到30天内的数据,30天内的数据是需要更新总点击数和总分享数等。
3.列表是有规律的,除指定的几个其余是按照当前时间逆序排序的。
作者可能有多个人,因此每个数据插入前要进行人员匹配哦
/**
* 获取视频列表并插入数据库 每天凌晨2点30分抓取
*/
@Scheduled(cron = "${dy.task.cron}")
public void getVideoList() throws ParseException {
/**
* open_id =
* access_token =
*/
VideoConfig dyInfo = videoConfigMapper.selectByType("dy");
if (dyInfo == null) {
throw new RuntimeException("无数据抖音平台数据");
}
//检查是否需要刷新access_token
if (dyInfo.getTokenExpiresIn().getTime() < System.currentTimeMillis()) {
//为空则说明需要去刷新 获取老版本然后更新
refresh_token();
//刷新后再次获取最新的access_token相关信息
dyInfo = videoConfigMapper.selectByType("dy");
}
//获取列表
String has_more = null;
String cursor = "0";
int count = 0;
JSONArray jsonArray = new JSONArray();
do {
JSONObject list10 = getList10(dyInfo, cursor);
if (list10 == null) return;
has_more = list10.get("has_more").toString();
cursor = list10.get("cursor").toString();
JSONArray list = (JSONArray) list10.get("list");
for (int i = 0; i < list.size(); i++) {
JSONObject json = (JSONObject) list.get(i);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String date = json.get("create_time").toString();
String createTime = sdf.format(new Date(Long.parseLong(String.valueOf(Long.parseLong(date) * 1000)))); //时间戳转化成时间
boolean flag = TimeUtil.isPastNDays(createTime, 30);
if (!flag) {
//不在该时间范围就移除数据.
list.remove(i);
System.out.println("移除时间为:" + TimeUtil.TimeStampToTime(date));
System.out.println("移除的标题为:" + json.get("title").toString());
//动态调整结束请求页数,当从第二页开始,如果有连续移除时,说明后面的页数据已经不是要取的了.
if (count >= 2) {
has_more = "false";
}
}
}
jsonArray.addAll(list);
//标记页数
count++;
} while (has_more.equals("true"));
logger.info("本次翻页{}次,共采集到有效数据{}条", count, jsonArray.size());
dataDeal(jsonArray);
logger.info("=================抖音数据采集结束=================");
}
//数据处理过程
private void dataDeal(JSONArray jsonArray) throws ParseException {
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
//视频创建时间戳
String date = jsonObject.get("create_time").toString();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String createTime = sdf.format(new Date(Long.parseLong(String.valueOf(Long.parseLong(date) * 1000)))); //时间戳转化成时间
//网页链接
String share_url = jsonObject.get("share_url").toString();
//标题
String title = jsonObject.get("title").toString();
String layout = "";
String authors = "";
String editor = "";
String editor2 = "";
String editor3 = "";
String editor4 = "";
if (title.indexOf("来源:") > 0||title.indexOf("来源:") > 0) { //判断是否有这个
String str1 = "";
if(title.indexOf("来源:")>0){
str1 = title.substring(0, title.indexOf("来源:"));
}else{
str1 = title.substring(0, title.indexOf("来源:"));
}
//获取作者相关信息,处理格式问题
String contactInfo = title.substring(str1.length()).replace(" ", "").replace("\n", "");
logger.info("=================抖音数据获取contactInfo:{}=================", contactInfo);
Pattern from = Pattern.compile("来源:.+作者|编辑");
Matcher fromM = from.matcher(contactInfo);
while (fromM.find()) {
//获取作者信息然后去匹配字段
layout = contactInfo.substring(fromM.start(), fromM.end()).replace("来源:", "").replace("编辑", "");
if (layout.contains("xxxxx")) { //xxxx表示来来源后的一段文字
//自己的媒体有作者信息
// 以 编辑:开头,以 编辑: 结束的字符串
Pattern p = Pattern.compile("作者:.+编辑:");
Matcher m = p.matcher(contactInfo);
while (m.find()) {
//获取作者信息然后去匹配字段
authors = contactInfo.substring(m.start(), m.end()).replace("作者:", "").replace("编辑", "");
logger.info("作者:" + authors);
}
}
}
//获取编辑
Pattern ed = Pattern.compile("编辑:.+责编");
Matcher em = ed.matcher(contactInfo);
while (em.find()) {
//获取作者信息然后去匹配字段
editor = contactInfo.substring(em.start(), em.end()).replace("编辑:", "").replace("责编", "");
logger.info("编辑:" + editor);
}
//获取责编
Pattern ed2 = Pattern.compile("责编:.+编审");
Matcher em2 = ed2.matcher(contactInfo);
while (em2.find()) {
//获取作者信息然后去匹配字段
editor2 = contactInfo.substring(em2.start(), em2.end()).replace("责编:", "").replace("编审", "");
logger.info("责编:" + editor2);
}
//获取编审
Pattern ed3 = Pattern.compile("编审:.+监制");
Matcher em3 = ed3.matcher(contactInfo);
while (em3.find()) {
//获取作者信息然后去匹配字段
editor3 = contactInfo.substring(em3.start(), em3.end()).replace("编审:", "").replace("监制", "");
logger.info("编审:" + editor3);
}
//获取监制
Pattern ed4 = Pattern.compile("监制:.+");
Matcher em4 = ed4.matcher(contactInfo);
while (em4.find()) {
//获取监制信息然后去匹配字段
editor4 = contactInfo.substring(em4.start(), em4.end()).replace("监制:", "");
logger.info("监制:" + editor4);
}
}
//统计数据
JSONObject statistics = (JSONObject) jsonObject.get("statistics"); //分享数
//播放数
int play_count = (int) statistics.get("play_count");
//点赞数
int digg_count = (int) statistics.get("digg_count");
//分享数
int share_count = (int) statistics.get("share_count");
//首先根据时间和标题看是否已经插入了
title = title.replace("\n", "");
Audit audi = auditMapper.selectByTitleAndTime(title, createTime);
//如果已经入库则更新总点击和总分享量
if (audi == null) {
Audit audit = new Audit();
audit.setCreateTime(createTime);
audit.setMediaName("抖音");
audit.setType("video");
audit.setTitle(title);
audit.setLayout(layout);
audit.setLink(share_url);
audit.setReadCount(play_count);
audit.setClickTotal(digg_count);
audit.setShareCount(share_count);
audit.setReadTotal(play_count);
audit.setClickTotal(digg_count);
audit.setShareTotal(share_count);
insertAudit(authors, editor, editor2, editor3, editor4, audit);
} else {
logger.info("================= 更新数据 =================");
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String currentDate = df.format(new Date());
int dayNum = TimeUtil.daysBetween(createTime, currentDate);
//未审核并且时间再30天内
if (audi.getIsAudit() < 2 && dayNum <= 30) {
audi.setReadTotal(audi.getReadTotal() + play_count);
audi.setClickTotal(audi.getClickTotal() + digg_count);
audi.setShareTotal(audi.getShareTotal() + share_count);
//更新
auditMapper.updateByPrimaryKeySelective(audi);
logger.info("更新一条数据成功,{},{}", audi.getTitle(), audi.getCreateTime());
}
}
}
}
//获取数据
private JSONObject getList10(VideoConfig dyInfo, String cursor) {
//数据读取
String open_id = dyInfo.getOpenId();
String access_token = dyInfo.getToken();
Map<String, String> map = new HashMap<>();
map.put("open_id", open_id);
map.put("cursor", cursor);
map.put("count", "10");
logger.info("=================抖音数据采集开始=================");
//请求地址
String url = dyInfo.getGetVideoListUrl();
HttpRequest httpRequest = HttpRequest.get(url, map, Boolean.TRUE).header("access-token", access_token);
String result = httpRequest.body();
JSONObject jasonObject = JSONObject.parseObject(result);
JSONObject data = (JSONObject) jasonObject.get("data");
if (!data.get("error_code").toString().equals("0")) {
logger.error("抖音数据获取失败:{}", jasonObject);
return null;
}
JSONArray jsonArray = (JSONArray) data.get("list");
logger.info("=================抖音数据获取,接口响应数据:{}=================", jasonObject);
logger.info("数据条数为:{}", jsonArray.size());
return data;
}
//开启一个事务,要么都成功插入数据库,要么都失败会滚
@Transactional(rollbackFor = Exception.class)
public void insertAudit(String authors, String editor, String editor2, String editor3, String editor4, Audit audit) {
try {
auditMapper.insertSelective(audit);
logger.info("新加一条数据成功,{},{}", audit.getTitle(), audit.getCreateTime());
//执行插入作者信息
authorDoUtil.insertAuthorInfoPlus(audit.getAuditId(), authors, AuthorType.AUTHOR);
//执行编辑作者信息
authorDoUtil.insertAuthorInfoPlus(audit.getAuditId(), editor, AuthorType.EDITOR);
//执行插入责编信息
authorDoUtil.insertAuthorInfoPlus(audit.getAuditId(), editor2, AuthorType.EX_EDITOR);
//执行插入编审信息
authorDoUtil.insertAuthorInfoPlus(audit.getAuditId(), editor3, AuthorType.EDITORIAL);
//执行插入监制信息
authorDoUtil.insertAuthorInfoPlus(audit.getAuditId(), editor4, AuthorType.SUPERVISOR);
} catch (Exception e) {
logger.error("执行抖音数据采集失败,回滚成功!标题为:{},{},{}", audit.getTitle(), audit.getCreateTime(), e.getMessage());
//手动回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(),使得事务生效,出现异常回滚。
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}