字段表示

1
2
3
4
5
6
7
8
9
本地列表:local_list;
单项:local
本地修改时间: localTime
上次快照时间: snapTime

远程列表:remote_list;
单项:remote
上次同步时间: syncTime

状态判断

  1. 本地删除的(还没提交到远程,就删除的,针对这种情况可以直接清理掉)

    (local.snapTime == 0L && local.isDisabled)

  2. 无变化的

    (local.localTime == local.snapTime) && (local.snapTime == remote.syncTime)

  3. 本地新增的

    ((local.snapTime == 0L) && (local.localTime > 0))

  4. 远程新增的

    (remote_list.subtract(local_list))

  5. 本地修改的

    ((local.snapTime == remote.syncTime) && (local.localTime > local.snapTime))

  6. 远程修改的

    ((local.localTime == local.snapTime) && (remote.syncTime > local.snapTime))

  7. 远程和本地都有修改的

    (local.localTime > local.snapTime) && (remote.syncTime > local.snapTime)

冲突解决

对于前 6 个状态,是不需要处理冲突的, 简单的合并处理即可。

对于第 7 个,是需要手动来解决的。

上传同步

  1. 取当前时间:
1
val now = Date().time
  1. 设置修改时间:
1
2
sync_list.map { it.syncTime=now }
local_list.map { it.localTime=now; it.snapTime=now }
  1. 上传 sync_list 到服务器

参考:

从客户端的角度来看,文件同步的本质是本地文件集合与云端文件集合的对比。从实现角度来讲,客户端会保存一份云端文件集合的快照,通过将快照和云端集合对比可以计算出云端文件变更,通过将快照和本地集合对比则可以计算出本地文件变更。对于本地文件的变更,需要将文件提交至云端;对于云端文件的变更,需要将文件同步至本地。对于文件同步在云端和本地都有修改的情况下,就需要进行冲突处理。

为减少内存占用,不采取云端快照到本地的方式;这里换了种方式:在本地数据库中只加了个 snapTime 快照时间 ,来作为参考点;

更多思考

  • 在处理冲突期间,远程数据库已经被另一端上传覆盖了。
  • 本地的修改时间比快照时间还小?

返回的 json 信息

  • 错误情况
1
2
3
4
{
"err_code": 1003,
"err_msg": "参数错误,超出最大数量限制"
}
  • 正常情况
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"err_code": 0,
"err_msg": "ok",
"data": {
"id": 1,
"name": "lyloou",
"email": "lyloou@qq.com",
"personal_signature": "多么美好的太阳",
"gmt_create": "2020-01-16T09:38:18.000+0000",
"gmt_modified": "2020-03-14T11:27:58.000+0000",
"is_disabled": false
}
}

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Result {

@JsonProperty("err_code")
private final int status;

@JsonProperty("err_msg")
private String msg;

@JsonProperty("data")
private Object data;

public Result(int status, String msg) {
this.status = status;
this.msg = msg;
}

public Result data(Object data) {
this.data = data;
return this;
}

public Result msg(String msg) {
this.msg = msg;
return this;
}

public Result appendMsg(String msg) {
return appendMsg(msg, ", ");
}

public Result appendMsg(String msg, String sep) {
if (!Strings.isNullOrEmpty(msg)) {
this.msg = Joiner.on(sep).join(this.msg, msg);
} else {
this.msg = msg;
}
return this;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 处理 Result 的接口,具体实现看 ResultHandlerImpl
*/
public interface ResultHandler {

/**
* 只需要返回 code 和 msg
*
* @param code 状态码
* @return 结果
*/
Result msgResult(StatusCode code);

/**
* 正常返回时,才使用这个;
* 错误的时候,尽量不要用这个,除非前端有作处理。填写不当的 data,有可能会导致解析不对;
*
* @param code 错误码
* @param data 返回给调用者的数据
* @return 结果
*/
Result dataResult(StatusCode code, Object data);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class ResultHandlerImpl implements ResultHandler {

@Override
public Result msgResult(StatusCode code) {
StatusCodeDict status = code.get();
return new Result(status.code(), status.msg());
}


@Override
public Result dataResult(StatusCode code, Object data) {
StatusCodeDict status = code.get();
Result result = new Result(status.code(), status.msg());
result.data(data);
return result;
}

}
1
2
3
4
5
6
7
8
9
10
/**
* ResultHandle 用此类作为参数。
* 通过此接口,可以通过 lambda 的方式来获取 StatusCodeDict 里的信息
* (<code>resultHandler.msgResult(() -> PARAM_LOGIN_ERROR);</code>)
*/
@FunctionalInterface
public interface StatusCode extends Supplier<StatusCodeDict> {
@Override
StatusCodeDict get();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* 错误码字典
*/
public enum StatusCodeDict {


// 通用码
COMMON_OK(0, "ok"),
COMMON_UNKNOWN(9999, "未知的异常"),
COMMON_INVALID_REQUEST(9998, "无效的请求"),

// 系统
SYSTEM_404(404, "404你懂的"),
SYSTEM_502(502, "服务器打洋了"),
SYSTEM_500(500, "哎哟,这里有个锅"),

PARAM(1001, "参数错误"),
PARAM_BEYOND_QUANTITY_NUMBER(1003, "参数错误,超出最大数量限制"),

// 登录
PARAM_LOGIN_ERROR(1102, "用户名不存在或密码错误"),

// 商品
GOODS_IS_NOT_EXISTED(1301, "商品不存在"),
GOODS_IS_NOT_MARKETABLE(1302, "商品已经下架"),

DB(1401, "数据库异常"),

UNDEFINED(1999, "未定义的业务异常"),
;


private int code;
private String msg;
private static final Map<Integer, StatusCodeDict> dict = new HashMap<>();

static {
Arrays.stream(StatusCodeDict.values())
.forEach(p -> dict.put(p.code, p));
}

public static StatusCodeDict of(int code) {
return Optional.of(dict.get(code)).orElse(COMMON_UNKNOWN);
}

StatusCodeDict(int code, String msg) {
this.code = code;
this.msg = msg;
}

public int code() {
return code;
}

public String msg() {
return msg;
}
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping(path = "${apiVersion}/user")
public class UserController {

@Autowired
private ResultHandler resultHandler;

@Autowired
UserMapper userMapper;

@PostMapping("login")
public Result login(
@RequestParam("name") String name,
@RequestParam("password") String password
) {
UserPassword userPassword = userMapper.getUserPasswordByNamePassword(name, password);
if (userPassword == null) {
return resultHandler.msgResult(() -> PARAM_LOGIN_ERROR);
}
User user = userMapper.getUser(userPassword.getUserId());
return resultHandler.dataResult(() -> COMMON_OK, user);
}
}

用到的库

  • Retrofit
  • RxJava2
1
2
3
4
5
6
7
8
9
10
11
def rxandroidVersion = '2.0.1'
implementation "io.reactivex.rxjava2:rxandroid:$rxandroidVersion"
def retrofitVersion = '2.8.0'
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation 'com.google.code.gson:gson:2.8.5'

implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

服务器统一返回格式

error_code 不为 0 时,就不用解析 data(其值为 null),根据相应的错误码提示给用户;

1
2
3
4
{
"err_code": 1101,
"err_msg": "参数name不能为空"
}

error_code0 时,就可以解析 data,这个返回值通过实体类或列表来映射得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"err_code": 0,
"err_msg": "ok",
"data": {
"id": 1,
"name": "lyloou",
"email": "lyloou@qq.com",
"personal_signature": "多么美好的太阳",
"gmt_create": "2020-01-16T09:38:18.000+0000",
"gmt_modified": "2020-03-14T11:27:58.000+0000",
"is_disabled": false
}
}

定义返回类

通过 CResult 来接收返回的数据,data 的通过泛型来映射成对应实体类(用泛型的好处,是不用为每一个返回都建立一个类);

1
2
3
// https://medium.com/@stustirling/responses-errors-with-retrofit-2-rxjava2-6d55eafecf5a
data class CResult<T>(var err_code: Int, var err_msg: String, var data: T?)

Retrofit 简单封装

具体用法参考:Retrofit 官网

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Network.kt
val gson: Gson = GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.setLenient()
.create()
fun Any.toJsonString(): String {
return gson.toJson(this)
}

object Network {
private var headerPairs: (() -> List<Pair<String, String>>)? = null
private val builder = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create());

fun withHeader(pairList: (() -> List<Pair<String, String>>)): Network {
this.headerPairs = pairList
return this
}

fun <T> get(baseUrl: String, clazz: Class<T>): T {
val okHttpBuilder = OkHttpClient.Builder()
headerPairs?.invoke()?.let {
okHttpBuilder.addInterceptor(interceptor(it))
}
return builder.baseUrl(baseUrl)
.client(okHttpBuilder.build())
.build().create(clazz)
}

// 如果是需要授权才能获取信息,统一用这个。
// 把用户信息和授权信息通过 header 的方式发到服务器
// [Retrofit — Add Custom Request Header](https://futurestud.io/tutorials/retrofit-add-custom-request-header)
fun auth(userPassword: UserPassword?): List<Pair<String, String>> {
userPassword?.let {
return listOf(
"Content-Type" to "application/json",
"Authorization" to it.password,
"UserId" to it.userId.toString()
)
}
return emptyList()
}
}

fun interceptor(headers: List<Pair<String, String>>): (Interceptor.Chain) -> Response {
return {
val newBuilder = it.request().newBuilder()
headers.forEach { header ->
newBuilder.addHeader(header.first, header.second)
}
it.proceed(newBuilder.build())
}
}

fun <T> Observable<T>.defaultScheduler(): Observable<T> {
return this.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}

准备用户的类和 retrofit 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// user.kt
data class User(
val id: Long,
val name: String,
val email: String,
@SerializedName("personal_signature")
val personalSignature: String,
@SerializedName("gmt_create")
val gmtCreate: Date
)

data class UserPassword(
val userId: Long,
val name: String,
val password: String
)

enum class Url(val url: String) : Str {
UserApi("http://127.0.0.1:8888/api/v1/user/"),
;
}


fun Network.userApi(): UserApi {
return get(Url.UserApi.url, UserApi::class.java)
}

// 创建带授权的 userApi
fun Network.userWithAuthApi(userPassword: UserPassword? = UserPasswordHelper.getUserPassword()): UserApi {
return withHeader { auth(userPassword) }
.get(Url.UserApi.url, UserApi::class.java);
}

使用方法 1 ,基于 Observable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

interface UserApi {
@POST("login")
fun login(@Query("name") name: String, @Query("password") password: String): Observable<CResult<User?>>

@POST("update")
fun update(@Body user: User): Observable<CResult<String?>>
}

// 用户登录
Network.userApi()
.login(name, encodedPassword)
.defaultScheduler()
.subscribe({
if (it.err_code == 0) {
// save user info to localStorage
} else {
toast("错误代码:${it.err_code},错误信息:${it.err_msg}")
}
}, {
toast("网络异常:${it.message}")
})

// 更新用户
Network.userApi()
.update(user)
.defaultScheduler()
.subscribe({
if (it.err_code == 0) {
// doSuccess
} else {
// do error
}
}, {
toast("网络异常:${it.message}")
})

使用方法 2,基于 kotlin 的 coroutine

首先给 ViewModel 添加一个 apiForCResult 扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun <T> ViewModel.apiForCResult(
block: suspend CoroutineScope.() -> CResult<T?>,
okFun: (T?) -> Unit = {},
failFun: (String) -> Unit = {}
) {
viewModelScope.launch {
try {
val result = withContext(Dispatchers.IO, block)
if (result.err_code == 0) {
okFun(result.data)
} else {
failFun(result.err_msg)
}
} catch (e: Exception) {
failFun("${e.message}")
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
interface UserApi {
@POST("login")
suspend fun login(@Query("name") name: String, @Query("password") password: String): CResult<User?>

@POST("update")
suspend fun update(@Body user: User): CResult<String?>
}

// 在 viewModel 中
class LoginViewModel : ViewModel() {
fun login(
name: String,
password: String,
okFun: (User?) -> Unit,
failFun: (String) -> Unit
) {
apiForCResult(
{ Network.userApi().login(name, password) },
okFun,
failFun
)
}

fun update(
user: User,
okFun: (String?) -> Unit,
failFun: (String) -> Unit
) {
apiForCResult(
{ Network.userWithAuthApi().update(user) },
okFun,
failFun
)
}
}

class UserActivity{
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.login -> {
viewModel.login(name, password, ::doSuccess, ::toast)
}
R.id.updaete -> {
viewModel.update(
user,
{toast("更新成功")},
{toast(it)}
)
}
}
return super.onOptionsItemSelected(item)
}
// 在 activity 中
}

总结

结合使用 kotlin 的扩展功能,可以简化许多冗余的代码。

原理

订单号 = 前缀 + 日期 + (一个 1000-9999 之间的四位数字)

后面的四位数字,可以通过 jedis.incrBy() 来实现。

效能:最多可以在 1 秒钟创建 9000 个不重复单号

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* ID 生成器
*/
@Component
public class IdGenerator {
/**
* 生成器类型
*/
public enum Type {
NORMAL(""),
ORDER("OR"),
RETURN("RT"),
REFUND("RF");

private String prefix;

Type(String prefix) {
this.prefix = prefix;
}
}

private static String GENERATOR = "GENERATOR";
private static String FORMAT = "yyyyMMddHHmmss";

public String generate(Type type) {
String now = DateFormatUtils.format(new Date(), FORMAT);
String key = GENERATOR + ":" + type.name();
int counter = getCounter(key);
return String.format("%s%s%s", type.prefix, now, counter);
}

@Autowired
Jedis jedis;

/**
* 通过 redis 来获取累加器
* 根据 key 类型来获取累加数字
*
* @param key 类型
* @return 累加的数字
*/
private int getCounter(String key) {
int num;
String serial = jedis.get(key);
num = 1000;
if (serial != null && Integer.parseInt(serial) < 9999) {
num = jedis.incrBy(key, 1).intValue();
} else {
jedis.set(key, String.valueOf(num));
}
return num;
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@RunWith(SpringRunner.class)
@ComponentScan(basePackages = {"com.lyloou.order"})
@SpringBootTest
public class FlowApplicationTests {
@Autowired
IdGenerator idGenerator;

@Test
public void testGenerator() {
List<String> ids = Lists.newArrayList();
for (int i = 0; i < 10; i++) {
double delay = Math.random() * 1000;
Thread.sleep((long) delay);
ids.add(idGenerator.generate(IdGenerator.Type.ORDER));
}
System.out.println(Joiner.on(",\n").join(ids));
}
}

/** 运行结果:
OR202003261127151030,
OR202003261127161031,
OR202003261127171032,
OR202003261127181033,
OR202003261127181034,
OR202003261127191035,
OR202003261127191036,
OR202003261127191037,
OR202003261127201038,
OR202003261127211039
*/

Quartz 在某一天的某一秒执行一个任务

配置

1
2
3
4
5
6
7
<!-- pom.xml -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.2</version>
<type>jar</type>
</dependency>
1
2
3
4
; job.properties
a.job=com.lyloou.FeeJob
a.cron=0 0/1 * * * ?
a.enable=true

更多 cron 配置,看 链接

Quartz 在某一天的某一秒执行一个任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// TimeUtils.java
public class TimeUtils {
// 1月中的15号,0点整
public static Timestamp getMonthDay15(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DAY_OF_MONTH, 15);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
return new Timestamp(calendar.getTimeInMillis());
}
}

// FeeJob.java
/**
* 入账任务
*
* @author lyloou
*/
public class FeeJob implements Job {

private final Log logger = Logger.get(FeeJob.class);

@Override
public void execute(JobExecutionContext jec) {
// 1分钟执行一次
Date date = new Date();
if (TimeUtils.getMonthDay15(date).getTime() != date.getTime()) {
return;
}

long t1 = System.currentTimeMillis();
// do your job here
long spend = System.currentTimeMillis() - t1;
logger.info(String.format("%s|执行xxx任务,耗时%s:", getClass().getName(), spend));
}
}

取整

1
2
3
4
// 向上取整(天花板)
Math.ceil();
// 向下取整(地板)
Math.floor();

判断两个金额是否相等

注意不能直接使用equal== , 例如: 0.010.010 实际上是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 比较两个数值是否相等
*/
public static boolean isEqual(Object x, Object y) {
return compareTo(x, y) == 0;
}

/**
* 比较数字大小,x > y返回-1,x = y返回0,x < y返回1
*/
public static int compareTo(Object x, Object y) {
return new BigDecimal(x + "").compareTo(new BigDecimal(y + ""));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) throws Exception {
BigDecimal a = divide(1, 7, 2, RoundingMode.HALF_EVEN);
BigDecimal b = divide(6, 7, 2, RoundingMode.HALF_EVEN);
System.out.println(a);
System.out.println(b);
System.out.println(CalcUtils.add(a, b));

}

public static BigDecimal divide(Object x, Object y, int bit, RoundingMode mode) {
return new BigDecimal(x + "").divide(new BigDecimal(y + ""), bit, mode);
}
}

shell 脚本修改 json 中某个字段的值

  • 思路:通过 awk 来找到旧数据,然后用 sed 来替换旧数据

源码

config.json

1
2
3
4
5
6
7
{
"name": "the_name",
"id": "132869",
"content_url": "https://hot.example.com/",
"enable_feature1": "true",
"enable_feature2": "false"
}

config/mode1.sh

1
2
3
4
#!bin/bash
content_url_new="https://hot1.example.com/"
enable_feature1_new="true"
enable_feature2_new="true"

config/mode2.sh

1
2
3
4
#!bin/bash
content_url_new="https://hot2.example.com/"
enable_feature1_new="false"
enable_feature2_new="false"

main.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!bin/bash

if [ "$1" != "mode1" -a "$1" != "mode2" ];then
echo "tip:─┬─────── 您输入参数不对,请重试:"
echo " │─────── 'mode1', 使用config/mode1.sh的配置"
echo " └─────── 'mode2', 使用config/mode2.sh的配置"
exit 0
fi

case $1 in
mode1)
. config/mode1.sh
;;
mode2)
. config/mode2.sh
;;
*)
echo "Usage: sh main.sh [mode1|mode2]"
exit;
esac

# 如果要修改的内容在文档中唯一,可以做全局修改
content_url_old=$(awk -F"\"" '/content_url/{print $4}' example.json)
sed -e "s@$content_url_old@$content_url_new@g" -i example.json


# 如果要修改的内容在文档中不唯一,就需要针对那一行做修改。(例如,这个例子中有两个布尔类型的值)
enable_feature1_line=$(awk -F"\"" '/enable_feature1/{print NR}' example.json) # 记住行号
enable_feature1_old=$(awk -F"\"" '/enable_feature1/{print $4}' example.json) # 获取旧数据
sed -e "$enable_feature1_line s@$enable_feature1_old@$enable_feature1_new@" -i example.json # 替换所在行的老数据

enable_feature2_line=$(awk -F"\"" '/enable_feature2/{print NR}' example.json) # 记住行号
enable_feature2_old=$(awk -F"\"" '/enable_feature2/{print $4}' example.json) # 获取旧数据
sed -e "$enable_feature2_line s@$enable_feature2_old@$enable_feature2_new@" -i example.json # 替换所在行的老数据

运行

1
2
sh main.sh mode1
sh main.sh mode2

Jsoup 解析 html 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Test {

// http://www.runoob.com/java/java-regular-expressions.html
@Test
public void testRegex() {
String input = "【丝路亚心】250gX4<font color=\"red\">核桃仁</font> 原味生<font color=\"red\">核桃核桃仁</font> 新疆特产, <font color=\"red\">123445</font>bcdef";
String regex = "<font[^>]+?>(.*?)<\\\\/font>";
Pattern compile = Pattern.compile(regex);
Matcher matcher = compile.matcher(input);
List<String> output = new ArrayList<>();
int count = 0;
while (matcher.find()) {
System.out.println("--" + count);
output.add(matcher.group(count));
count++;
}

System.out.println(output);
}

@Test
public void testHtml() {

String html = "<span style=\"color:red;border-radius:10px;background-color:blue;\">抢购</span> 我是标题我是标题我是标题<font color=\"red\">高亮</font>我是标题我是标题我是标题我是标题<font color=\"red\">高亮</font>我是标题";

Document doc = Jsoup.parse(html);
System.out.println(doc); // 输出带标签的html文档
System.out.println("\n---->" + doc.text()); // 输出内容
System.out.println("\n---->" + doc.getElementsByTag("span").get(0).html());
System.out.println("\n---->" + doc.getElementsByTag("font").get(0).html());

}

@Test
public void testStyle() {
String style = "position: absolute; width: 500px; height: 552px; color: red; background-color: blue;";
String width = "width";
String color = "color";
String backgroundColor = "background-color";
System.out.println(getFloat(style, width));
System.out.println(getString(style, color));
System.out.println(getString(style, backgroundColor));
}

// https://www.cnblogs.com/qlqwjy/p/7531579.html
public static double getFloat(String value, String property) {
try {
if (value.contains(property)) {
value = value.substring(value.indexOf(property));
value = value.substring(0, value.contains(";") ? value.indexOf(";") : value.length());
String attr = value.substring(value.indexOf(":") + 1).trim();
return Double.parseDouble(attr.substring(0, attr.indexOf("px")));
}
} catch (Exception e) {
// ignore all exception
}

return 0;
}

public static String getString(String value, String property) {
try {
if (value.contains(property)) {
System.out.println(value);
value = value.substring(value.indexOf(property));
value = value.substring(0, value.contains(";") ? value.indexOf(";") : value.length());
return value.substring(value.indexOf(":") + 1).trim();
}
} catch (Exception e) {
// ignore all exception
}

return "";
}
}

0%