Caffeine是一种高性能的缓存库,是基于Java 8的最佳(最优)缓存框架。
基于Google的Guava Cache,Caffeine提供一个性能卓越的本地缓存(local cache) 实现, 也是SpringBoot内>置的本地缓存实现。(Caffeine性能是Guava Cache的6倍)
Caffeine提供灵活的结构来创建缓存,并且有以下特性:
1、自动加载条目到缓存中,可选异步方式
2、可以基于大小剔除
3、可以设置过期时间,时间可以从上次访问或上次写入开始计算
4、异步刷新
5、keys自动包装在弱引用中
6、values自动包装在弱引用或软引用中
7、条目剔除通知
8、缓存访问统计
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.2</version>
</dependency>
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springcenter.basemgt.center.wx.module.common.vo.WxDictVo;
import org.springcenter.basemgt.center.wx.module.menu.vo.WxBaseInfoVo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class CacheConfig {
@Bean(name = "caffeineCache")
public Cache<String, DataTokenVo> caffeineCache() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(60, TimeUnit.DAYS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(500)
.build();
}
@Bean(name = "caffeinemManagerDictCache")
public Cache<String, DataTokenVo> caffeineAppidDictCache() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(60, TimeUnit.HOURS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(500)
.build();
}
}
@Resource(name = "caffeinemManagerDictCache")
private Cache<String, WxBaseInfoVo> caffeineCache;
// 手动添加数据
cache.put("java-study", "java-study");
System.out.println(cache.getIfPresent("java-study"));
// 1.如果缓存中能查到,则直接返回
// 2.如果查不到,则从我们自定义的getValue方法获取数据,并加入到缓存中
String val = cache.get("java-study", k -> getValue(k));
System.out.println(val);
/**
* 缓存中找不到,则会进入这个方法。一般是从数据库获取内容
* @param k
* @return
*/
private static String getValue(String k) {
return k + ":value";
}
@Component
@Slf4j
public class WxDataCache {
private final WxDataService wxDataService;
@Autowired
public WxDataCache (WxDataService wxDataService) {
this.wxDataService= wxDataService;
}
/**
* 时间差值标准
*/
private static final Integer MINUTES_STANDARD = 15;
/**
* 缓存最大数量
*/
private final Integer MAX_SIZE = 50;
/**
* 缓存有效期值10
*/
private final Long DURATION = 15L;
private static final String TOKEN_PREFIX = "APPID_ACCESS_TOKEN_PREFIX_";
/**
* 定义及配置Caffeine本地缓存
*/
private final LoadingCache<String, DataTokenVo > cache = Caffeine.newBuilder()
// 配置淘汰最大条数策略
.maximumSize(MAX_SIZE)
// 配置有效期时间/单位
.expireAfterWrite(DURATION, TimeUnit.MINUTES)
// 加载缓存,当在本地缓存中找不到的时候,通过load方法将数据加载到缓存中,并返回给需要的地方
.build(new CacheLoader<String, DataTokenVo >() {
@Override
public DataTokenVo load(@NonNull String appid) {
log.info("=== 执行load将Caffeine缓存查询新数据appid:{} === ", appid);
return getDataTokenVo(appid);
}
private DataTokenVo getDataTokenVo (String appid) {
// 查询数据库
final QueryDataTokenParam param = new QueryDataTokenParam ();
param.setAppId(appid.replace(TOKEN_PREFIX , ""));
final DataTokenVo tokenInfo = wxDataService.findDataTokenInfo(param);
if (tokenInfo == null) {
throw new RuntimeException(String.format("Caffeine本地缓存Token获取失败,APPID:%s", param.getAppId()));
}
log.info("=== 新Token入缓存 appid:{} ===", appid);
// 新Token入缓存
final Date expireTime = tokenInfo.getExpireTime();
long minutes = (System.currentTimeMillis() - expireTime.getTime()) / (60 * 1000);
if (minutes > MINUTES_STANDARD) {
return tokenInfo;
}
log.info("=== 当前新Token入缓存插值不满足{},返回Null ===", DURATION);
return null;
}
});
/**
* 获取缓存数据
*
* @param appid 参数
* @return 结果
*/
public DataTokenVo getData(String appid) {
return cache.get(TOKEN_PREFIX + appid);
}
/**
* 往缓存中存值,存入后可通过getData获取缓存数据
*
* @param wxAccessTokenVo 对象数据
*/
public void putDataTokenVo(DataTokenVo dataTokenVo ) {
cache.put(TOKEN_PREFIX + dataTokenVo .getAppId(), dataTokenVo );
}
}
Caffeine 配置说明:
initialCapacity: 初始的缓存空间大小。
maximumSize: 缓存的最大数量。
maximumWeight: 缓存的最大权重。
expireAfterAccess: 最后一次读或写操作后经过指定时间过期。
expireAfterWrite: 最后一次写操作后经过指定时间过期。
refreshAfterWrite: 创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存。
weakKeys: 打开key的弱引用。弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
weakValues:打开value的弱引用。
softValues:打开value的软引用。软引用: 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
recordStats:开发统计功能。
weakValues 和 softValues 不可以同时使用
maximumSize 和 maximumWeight 不可以同时使用
expireAfterWrite 和 expireAfterAccess 同事存在时,以 expireAfterWrite 为准
public static void demo(){
Cache<String,String> cache = Caffeine.newBuilder()
.expireAfterWrite(20, TimeUnit.SECONDS)
.maximumSize(5000)
.build();
// 1.Insert or update an entry
cache.put("hello","world");
// 2. Lookup an entry, or null if not found
String val1 = cache.getIfPresent("hello");
// 3. Lookup and compute an entry if absent, or null if not computable
cache.get("msg", k -> createExpensiveGraph(k));
// 4. Remove an entry
cache.invalidate("hello");
}
private static String createExpensiveGraph(String key){
System.out.println("begin to query db..."+Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("success to query db...");
return UUID.randomUUID().toString();
}
private static void demo() {
LoadingCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.maximumSize(500)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return createExpensiveGraph(key);
}
@Override
public Map<String, String> loadAll(Iterable<? extends String> keys) {
System.out.println("build keys");
Map<String,String> map = new HashMap<>();
for(String k : keys){
map.put(k,k+"-val");
}
return map;
}
});
String val1 = cache.get("hello");
Map<String,String> values = cache.getAll(Lists.newArrayList("key1", "key2"));
}
private static String createExpensiveGraph(String key){
System.out.println("begin to query db..."+Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("success to query db...");
return UUID.randomUUID().toString();
}
还可以通过LoadingCache的getAll方法批量查询, 当CacheLoader未实现loadAll方法时,
会批量调用load方法聚合会返回
当CacheLoader实现loadAll方法时, 则直接调用loadAll返回
public interface CacheLoader<K, V>{
V load(@NonNull K var1) throws Exception;
Map<K, V> loadAll(@NonNull Iterable<? extends K> keys);
}
AsyncCache是另一种Cache,它基于Executor计算Entry,并返回一个CompletableFuture
和Cache的区别是, AsyncCache计算Entry的线程是ForkJoinPool线程池. 手动Cache缓存是调用线程进行计算
private static void demo() throws ExecutionException, InterruptedException {
AsyncCache<String,String> cache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(10, TimeUnit.SECONDS)
.buildAsync();
// Lookup and asynchronously compute an entry if absent
CompletableFuture<String> future = cache.get("hello", k -> createExpensiveGraph(k));
System.out.println(future.get());
}
private static String createExpensiveGraph(String key){
System.out.println("begin to query db..."+Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("success to query db...");
return UUID.randomUUID().toString();
}
public static void demo() throws ExecutionException, InterruptedException {
AsyncLoadingCache<String,String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.maximumSize(500)
.buildAsync(k -> createExpensiveGraph(k));
CompletableFuture<String> future = cache.get("hello");
System.out.println(future.get());
}
private static String createExpensiveGraph(String key){
System.out.println("begin to query db..."+Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("success to query db...");
return UUID.randomUUID().toString();
}
如果缓存的条目数量不应该超过某个值,那么可以使用Caffeine.maximumSize(long)。如果超过这个值,则会剔除很久没有被访问过或者不经常使用的那个条目。
上述测试并不是i=500时, 而是稍微延迟于i的增加, 说明驱逐是另外一个线程异步进行的
LoadingCache<String,String> cache = Caffeine.newBuilder()
.maximumSize(500)
.recordStats()
.build( k -> UUID.randomUUID().toString());
for (int i = 0; i < 600; i++) {
cache.get(String.valueOf(i));
if(i> 500){
CacheStats stats = cache.stats();
System.out.println("evictionCount:"+stats.evictionCount());
System.out.println("stats:"+stats.toString());
}
}
如果,不同的条目有不同的权重值的话(不同的实例占用空间大小不一样),那么你可以用Caffeine.weigher(Weigher)来指定一个权重函数,并且使用Caffeine.maximumWeight(long)来设定最大的权重值。
上述测试并不是i=200时, 而是稍微延迟于i的增加, 说明驱逐是另外一个线程异步进行的
简单的来说,要么限制缓存条目的数量,要么限制缓存条目的权重值,二者取其一。
LoadingCache<Integer,String> cache = Caffeine.newBuilder()
.maximumWeight(300)
.recordStats()
.weigher((Weigher<Integer, String>) (key, value) -> {
if(key % 2 == 0){
return 2;
}
return 1;
})
.build( k -> UUID.randomUUID().toString());
for (int i = 0; i < 300; i++) {
cache.get(i);
if(i> 200){
System.out.println(cache.stats().toString());
}
}
基于时间又分为四种:
expireAfterAccess、expireAfterWrite、refreshAfterWrite、expireAfter
private static LoadingCache<String,String> cache = Caffeine.newBuilder()
.expireAfterAccess(1, TimeUnit.SECONDS)
.build(key -> UUID.randomUUID().toString());
1、访问包括读和写入
2、数据失效后不会主动重新加载, 必须依赖下一次访问. (言外之意: 失效和回源是两个动作)
3、key超时失效或不存在,若多个线程并发访问, 只有1个线程回源数据,其他线程阻塞等待数据返回
4、对同一数据一直访问, 且间隔小于失效时间, 则不会去load数据, 一直读到的是脏数据
private static LoadingCache<String,String> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.build(key -> UUID.randomUUID().toString());
1、数据失效后不会主动重新加载, 必须依赖下一次访问. (言外之意: 失效和回源是两个动作)
2、key超时失效或不存在,若多个线程并发访问, 只有1个线程回源数据,其他线程阻塞等待数据返回
3、expire后来访问一定能保证拿到最新的数据
1、数据失效后不会主动重新加载, 必须依赖下一次访问. (言外之意: 失效和回源是两个动作)
2、当cache命中未命中时, 若多个线程并发访问时, 只有1个线程回源数据,其他线程阻塞等待数据返回
3、当cache命中失效数据时, 若多个线程并发访问时, 第一个访问的线程提交一个load数据的任务到公共线程>池,然后和所有其他访问线程一样直接返回旧值
public static void demo(){
MyTicker ticker = new MyTicker();
LoadingCache<String,String> cache = Caffeine.newBuilder()
.maximumSize(500)
.ticker(ticker)
//此时的效果为expireAfterWrite(5,TimeUnit.SECONDS)
.expireAfter(new Expiry<String, String>() {
//1.如果写入key时是第一次创建,则调用该方法返回key剩余的超时时间, 单位纳秒ns
//currentTime为当前put时Ticket的时间,单位ns
@Override
public long expireAfterCreate(String key,String value, long currentTime) {
System.out.println("write first currentTime:"+currentTime/1_000_000_000L);
return 5_000_000_000L;//5s
}
//2.如果写入key时已经存在即更新key时,则调用该方法返回key剩余的超时时间, 单位纳秒ns
//currentTime为当前put时Ticket的时间,单位ns,durationTime为旧值(上次设置)剩余的存活时间,单位是ns
@Override
public long expireAfterUpdate(String key,String value, long currentTime,long durationTime) {
System.out.println("update currentTime:"+currentTime/1_000_000_000L+",leftTime:"+durationTime/1_000_000_000L);
return 5_000_000_000L;//5s
}
//3.如果key被访问时,则调用该方法返回key剩余的超时时间, 单位纳秒ns
//currentTime为read时Ticket的时间,单位ns,durationTime为旧值(上次设置)剩余的存活时间,单位是ns
@Override
public long expireAfterRead(String key,String value, long currentTime,long durationTime) {
System.out.println("read currentTime:"+currentTime/1_000_000_000L+",leftTime:"+durationTime/1_000_000_000L);
return durationTime;
}
})
.build(k -> UUID.randomUUID().toString());
cache.get("key1");//触发expireAfterCreate
ticker.advance(1, TimeUnit.SECONDS);//模拟时间消逝
cache.get("key1");//触发expireAfterRead,剩余生存时间4s
ticker.advance(2, TimeUnit.SECONDS);//模拟时间消逝
cache.put("key1","value1");//触发expireAfterUpdate,重置生存时间为5s
ticker.advance(3, TimeUnit.SECONDS);//模拟时间消逝
cache.get("key1");//触发expireAfterCreate,剩余生存时间为2s
}
public class MyTicker implements Ticker {
private final AtomicLong nanos = new AtomicLong();
//模拟时间消逝
public void advance(long time, TimeUnit unit) {
this.nanos.getAndAdd(unit.toNanos(time));
}
@Override
public long read() {
return this.nanos.get();
}
}
1、以上基于时间驱逐, 数据超时失效和回源是两个动作, 必须依赖下一次访问. 为了避免服务启动时大量缓存穿透, 可以通过提前项目启动时手动预热
2、一般expireAfterWrite和refreshAfterWrite结合使用, expire的时间t1大于refresh的时间t2, 在t2~t1内数据更新允许脏数据, t1之后必须要重新同步加载新数据
/**
* 允许GC时回收keys或values
*/
public static void demo(){
LoadingCache<String,String> cache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(10, TimeUnit.SECONDS)
.weakKeys()
.weakValues()
.build(k -> UUID.randomUUID().toString());
}
Caffeine.weakKeys() 使用弱引用存储key。如果没有强引用这个key,则GC时允许回收该条目
Caffeine.weakValues() 使用弱引用存储value。如果没有强引用这个value,则GC时允许回收该条目
Caffeine.softValues() 使用软引用存储value, 如果没有强引用这个value,则GC内存不足时允许回收该条目
public static void demo(){
/**
* 使用软引用存储value,GC内存不够时会回收
*/
LoadingCache<String,String> cache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(10, TimeUnit.SECONDS)
.softValues()//注意没有softKeys方法
.build(k -> UUID.randomUUID().toString());
}
Java4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用
1、eviction 指受策略影响而被删除
2、invalidation 值被调用者手动删除
3、removal 值因eviction或invalidation而发生的一种行为
// individual key
cache.invalidate(key)
// bulk keys
cache.invalidateAll(keys)
// all keys
cache.invalidateAll()
1、EXPLICIT:如果原因是这个,那么意味着数据被我们手动的remove掉了
2、REPLACED:就是替换了,也就是put数据的时候旧的数据被覆盖导致的移除
3、COLLECTED:这个有歧义点,其实就是收集,也就是垃圾回收导致的,
一般是用弱引用或者软引用会导致这个情况
4、EXPIRED:数据过期,无需解释的原因。
5、SIZE:个数超过限制导致的移除
public static void demo(){
LoadingCache<String,String> cache = Caffeine.newBuilder()
.maximumSize(5)
.recordStats()
.expireAfterWrite(2, TimeUnit.SECONDS)
.removalListener((String key, String value, RemovalCause cause) -> {
System.out.printf("Key %s was removed (%s)%n", key, cause);
})
.build(key -> UUID.randomUUID().toString());
for (int i = 0; i < 15; i++) {
cache.get(i+"");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
}
//因为evict是异步线程去执行,为了看到效果稍微停顿一下
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
Key 0 was removed (SIZE)
Key 1 was removed (SIZE)
Key 6 was removed (SIZE)
Key 7 was removed (SIZE)
Key 8 was removed (SIZE)
Key 9 was removed (SIZE)
Key 10 was removed (SIZE)
Key 2 was removed (EXPIRED)
Key 3 was removed (EXPIRED)
Key 4 was removed (EXPIRED)
public static void demo(){
LoadingCache<Integer,String> cache = Caffeine.newBuilder()
.maximumSize(10)
.expireAfterWrite(10, TimeUnit.SECONDS)
.recordStats()
.build(key -> {
if(key % 6 == 0 ){
return null;
}
return UUID.randomUUID().toString();
});
for (int i = 0; i < 20; i++) {
cache.get(i);
printStats(cache.stats());
}
for (int i = 0; i < 10; i++) {
cache.get(i);
printStats(cache.stats());
}
}
private static void printStats(CacheStats stats){
System.out.println("---------------------");
System.out.println("stats.hitCount():"+stats.hitCount());//命中次数
System.out.println("stats.hitRate():"+stats.hitRate());//缓存命中率
System.out.println("stats.missCount():"+stats.missCount());//未命中次数
System.out.println("stats.missRate():"+stats.missRate());//未命中率
System.out.println("stats.loadSuccessCount():"+stats.loadSuccessCount());//加载成功的次数
System.out.println("stats.loadFailureCount():"+stats.loadFailureCount());//加载失败的次数,返回null
System.out.println("stats.loadFailureRate():"+stats.loadFailureRate());//加载失败的百分比
System.out.println("stats.totalLoadTime():"+stats.totalLoadTime());//总加载时间,单位ns
System.out.println("stats.evictionCount():"+stats.evictionCount());//驱逐次数
System.out.println("stats.evictionWeight():"+stats.evictionWeight());//驱逐的weight值总和
System.out.println("stats.requestCount():"+stats.requestCount());//请求次数
System.out.println("stats.averageLoadPenalty():"+stats.averageLoadPenalty());//单次load平均耗时
}
public static void demo(){
MyTicker ticker = new MyTicker();
LoadingCache<String,String> cache = Caffeine.newBuilder()
.maximumSize(500)
.ticker(ticker)
.expireAfterWrite(1, TimeUnit.SECONDS)
.build(k -> UUID.randomUUID().toString());
cache.get("key1");//触发expireAfterCreate
ticker.advance(1, TimeUnit.SECONDS);//模拟时间消逝
cache.get("key1");//触发expireAfterRead,剩余生存时间4s
ticker.advance(2, TimeUnit.SECONDS);//模拟时间消逝
cache.put("key1","value1");//触发expireAfterUpdate,重置生存时间为5s
}
public class MyTicker implements Ticker {
private final AtomicLong nanos = new AtomicLong();
//模拟时间消逝
public void advance(long time, TimeUnit unit) {
this.nanos.getAndAdd(unit.toNanos(time));
}
@Override
public long read() {
return this.nanos.get();
}
}
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h