写在前面网站是在宝塔里维护的，网站的ssl证书每个季度就会过期还得手动申请太麻烦了，就想着搞个自动申请，本以为十几分就搞定结果搞了两个多小时。以下是踩坑记录，其余能在官方文档中找到的操作不再赘述。certd在宝塔里的安装过程省略，着重说一下在certd中配置宝塔-网站证书部署，也就是下面的的第二个阶段这个阶段的配置里需要填一个宝塔面板的地址，正常理解的就是应该把宝塔页面的浏览器地址填上去，结果一直报错，卡在这里很久查了半天，找到这句话：在绝大多数 Linux 环境（CentOS/Ubuntu）的 Docker 中，容器访问宿主机都使用这个固定 IP：http://172.17.0.1:8888所以我就试着把http://172.17.0.1:8888这个地址当作certd访问宝塔的地址填上去测了一下，结果真的通了然后按照测试按钮下面的结果把172.20.0.2加入宝塔api的白名单就搞定了
一些好用的AI工具[tabs] [tab name="AI画图" active="true"] [link] [item name="Midjourney" link="https://www.midjourney.com/home" desc="AI画图Top1" /] [/link] [/tab][tab name="Prompt"] [link] [item name="Ask Prompts" link="https://www.askprompts.com/" desc="ChatGPT快捷指令" /] [/link] [/tab] [tab name="AI导航"] [link] [item name="AIVESA" link="https://www.naviai.cn/" desc="AI工具聚合网站" /] [/link] [/tab][tab name="图片"] [link] [item name="免扣图片网" link="https://miankoutupian.com/" desc="AI免费抠图网站" /] [/link] [/tab] [tab name="Github"] [link] [item name="一些实用工具集合" link="https://www.kdocs.cn/l/cdH3liGF360p" desc=" " /] [/link] [/tab][tab name="自媒体"] [link] [item name="小红书文案AI生成" link="https://ai.xiaohongshu.live/zh?f=wz.aidaohang.com.cn" desc=" " /] [/link] [/tab][/tabs]
翻译论文来源：bitcask-intro.pdf (riak.com)背景介绍Bitcask的起源与Riak分布式数据库的历史紧密相连。在Riak的K/V集群中，每个节点都使用了可插拔的本地存储；几乎任何结构的K/V存储都可以用作每个主机的存储引擎。这种可插拔性使得Riak的处理能够并行化，从而可以在不影响代码库其他部分的情况下改进和测试存储引擎。有很多类似的本地K/V存储系统，包括但不限于Berkeley DB、Tokyo Cabinet和Innostore。在评估此类存储引擎时，我们想实现的目标包括：读取或写入每个项目的低延迟高吞吐量，尤其是在写入随机项目的传入流时处理比RAM大得多的数据集的能力，无退化故障友好性，在快速恢复和不丢失数据方面都很好易于备份和恢复相对简单、可理解（因而可支持）的代码结构和数据格式访问负载大或容量大时的可预测行为允许在Riak中轻松默认使用的许可证实现其中一些目标并不困难，但是想实现所有目标就不那么容易了。现有的本地K/V存储系统（包括但不限于作者编写的系统）均未达到上述所有目标。当我们在与Eric Brewer讨论这个问题时，他关于哈希表日志合并的关键见解是：这样做可能会比LSM树更快或更快。这导致我们以新的视角探索了20世纪80年代和90年代首次开发的日志结构化文件系统所使用的一些技术。这次探索导致了Bitcask的诞生，它是一个能够完全实现上述所有目标的存储系统。虽然Bitcask最初是为了给Riak使用而诞生，但是它的设计很通用，因此也可以作为其他应用程序的本地K/V存储。模型描述active data file最终采用的模型在概念上非常简单。Bitcask实例是一个目录，我们强制规定在给定时间内，只有一个操作系统进程可以打开该Bitcask进行写入。您可以将该进程有效地视为“数据库服务器”。在任何时候，该目录中都有一个文件由服务器进行写入操作。当该文件达到一定大小时，它将被关闭，并创建一个新的活动文件。[font color="#FFA500"]一旦文件被关闭，无论是出于有意还是由于服务器退出，它都被视为不可变的，并且永远不会被再次打开进行写入。[/font]活动文件，也就是上文提到的active data file，只能以追加的方式写入，这意味着顺序写入的同时不需要磁盘寻址。文件中的每个键值对entry的格式如下：每次写入时，都只是向active data file追加一个新的entry。删除操作只是写入一个特殊的墓碑值（可以理解为是一个特殊标记），它将在下一次合并时被删除。因此，Bitcask数据文件无非是这些entry的线性序列：keydir在active data file中完成追加操作后，接着去内存中更新一个名为keydir的数据结构。keydir是一个哈希表（在本论文中它是一个哈希表，也可以是其他数据结构），它将Bitcask中的每个key映射到一个固定大小的结构，这个结构记录了这个key写在哪个文件、该键在该文件中的偏移量以及大小。一开始我觉得上面这张图就是对bitcask中哈希表存储内容的正确理解，但是后来觉得下面这个图才是，因为哈希表的value存储的应该是entry的信息，而不是entry中value的信息。原论文中的图有比较大的迷惑性。数据写入与读取数据的写入其实在上面两节已经介绍过了，为了方便理解记忆就再总结一下。写入很简单，就是往bitcask中追加一条entry，然后更新keydir（原子操作），将刚刚新增的entry的信息存储起来，就像下面这样：数据的读取流程则是先拿着key去keydir中取出相应的entry信息，然后根据entry中提供的信息去data file中取出key对应的value，就像下面这样：数据合并因为bitcask删除的数据的方式是通过追加一条相同key的entry实现的，所以文件的size会越来越大，就需要定期的合并文件，合并的过程是这样的：先遍历所有的old data file，将所有的有效数据进行合并，如果有多个entry含有相同的key，则只保留最新的entry，有点像Redis中的AOF，合并完成后，old data file会变成merge data file，且数量也会减少，例如10个old data file 合并成5个merge data file。因为bitcask是在内存中构建索引，也就是之前提到的keydir，构建keydir需要在启动的时候扫描所有的data file，如果数据量很大，那么构建索引的过程就会很耗时，为了解决这个问题，bitcask在合并数据的时候还会为每个merge data file生成一个hint file，这个hint file中存储的也是一堆entry，这些entry的格式和data file中的entry保持一致，唯一的区别就是data file中的entry存储的value是真实数据，而hint file中entry的value存储的是数据的位置。结束目前对bitcask的理解也就是这些了，肯定有不准确的地方，想要彻底弄明白也只能自己手搓一个kv存储才行。有任何问题都可以在评论区交流。
前置知识本节主要是对标准文件IO进行一些简单的封装。代码实现项目结构|-bitcask |--fio//新增 |---file_io.go //新增 |---io_manager.go//新增 |--data |---log_record.go |--index |---btree.go |---index.goIO接口定义新增文件：fio/io_manager.gopackage fio const FilePermCode = 0644 // IOManager 文件IO管理器 type IOManager interface { // Read 从文件指定位置读取数据 Read(b []byte, offset int64) (int, error) // Write 写入数据到文件指定位置 Write(b []byte) (int, error) // Sync 将存放在内存缓冲区的数据持久化到磁盘中 Sync() error // Close 关闭文件 Close() error }IO接口实现以下代码就是对标准文件IO进行简单的封装，方法的相应注释也都一并复制了，虽然是英文单但都很简单，应该不难看懂。新增：fio/file_io.gopackage fio import "os" // FileIO 对标准文件系统的一些接口进行简单封装 type FileIO struct { //系统文件描述符 fd *os.File } // NewFileIO 初始化标准文件IO func NewFileIO(filename string) (*FileIO, error) { fd, err := os.OpenFile( filename, // 以读写模式的模式打开文件，如果文件不存在则创建， // 如果文件已存在则以追加的方式进行写入 os.O_RDWR|os.O_CREATE|os.O_APPEND, FilePermCode) if err != nil { return nil, err } return &FileIO{fd: fd}, nil } // Read 从文件指定位置读取数据 func (fio *FileIO) Read(b []byte, offset int64) (int, error) { // ReadAt reads len(b) bytes from the File starting at byte offset off. // It returns the number of bytes read and the error, if any. // ReadAt always returns a non-nil error when n < len(b). // At end of file, that error is io.EOF. return fio.fd.ReadAt(b, offset) } // Write 写入数据到文件指定位置 func (fio *FileIO) Write(b []byte) (int, error) { // Write writes len(b) bytes to the File. // It returns the number of bytes written and an error, if any. // Write returns a non-nil error when n != len(b). return fio.fd.Write(b) } // Sync 将存放在内存缓冲区的数据持久化到磁盘中 func (fio *FileIO) Sync() error { // Sync commits the current contents of the file to stable storage. // Typically, this means flushing the file system's in-memory copy // of recently written data to disk. return fio.fd.Sync() } // Close 关闭文件 func (fio *FileIO) Close() error { return fio.fd.Close() }单元测试工具函数，用于删除测试文件func destroyFile(path string) { if err := os.RemoveAll(path); err != nil{ panic(err) } }FileIOfunc TestNewFileIOManager(t *testing.T) { path := filepath.Join("/tmp", "test.data") io, err := NewFileIO(path) cerr := io.Close() assert.Nil(t, cerr) defer destroyFile(path) assert.Nil(t, err) assert.NotNil(t, io) }Writefunc TestFileIO_Writer(t *testing.T) { path := filepath.Join("/tmp", "test.data") io, err := NewFileIO(path) defer destroyFile(path) assert.Nil(t, err) assert.NotNil(t, io) n, err := io.Write([]byte("hello")) assert.Nil(t, err) assert.Equal(t, 5, n) n, err = io.Write([]byte("world")) assert.Nil(t, err) assert.Equal(t, 5, n) cerr := io.Close() assert.Nil(t, cerr) }Readfunc TestFileIO_Read(t *testing.T) { path := filepath.Join("/tmp", "test.data") io, err := NewFileIO(path) defer destroyFile(path) assert.Nil(t, err) assert.NotNil(t, io) n, err := io.Write([]byte("hello")) assert.Nil(t, err) assert.Equal(t, 5, n) n, err = io.Write([]byte("world")) assert.Nil(t, err) assert.Equal(t, 5, n) b := make([]byte, 5) n, err = io.Read(b, 0) assert.Nil(t, err) assert.Equal(t, 5, n) assert.Equal(t, b, []byte("hello")) b = make([]byte, 5) n, err = io.Read(b, 5) assert.Nil(t, err) assert.Equal(t, 5, n) assert.Equal(t, b, []byte("world")) cerr := io.Close() assert.Nil(t, cerr) }Syncfunc TestFileIO_Sync(t *testing.T) { path := filepath.Join("/tmp", "test.data") io, err := NewFileIO(path) defer destroyFile(path) assert.Nil(t, err) assert.NotNil(t, io) err = io.Sync() assert.Nil(t, err) cerr := io.Close() assert.Nil(t, cerr) }Closefunc TestFileIO_Close(t *testing.T) { path := filepath.Join("/tmp", "test.data") io, err := NewFileIO(path) defer destroyFile(path) assert.Nil(t, err) assert.NotNil(t, io) err = io.Close() assert.Nil(t, err) }
前置知识TODOBitcaskTODO代码实现项目结构|-bitcask |--data |---log_record.go |--index |---btree.go |---index.go数据存放要把数据存放到磁盘中，就需要通过一个数据结构来记录这个位置信息。data/log_record.go// LogRecordPos 数据内存索引，记录数据在磁盘中的位置 type LogRecordPos struct { Fid uint32 // 文件id，表示数据存放在哪个文件中 Offset int64 // 表示将数据存放在了文件中的哪个位置 }索引接口实现索引接口是为了方便以后使用不同的数据结构作为索引，只要该数据结构实现此接口即可生效，不需要额外改动。index/index.go// Indexer 抽象索引接口，如果要实现不同的索引结构，只需要实现该接口即可 type Indexer interface { // Put 往索引中 添加key对应的索引位置信息 Put(key []byte, pos *data.LogRecordPos) bool // Get 根据key获取索引位置信息 Get(key []byte) *data.LogRecordPos // Delete 根据key删除索引位置信息 Delete(key []byte) bool }索引实现我们使用BTree作为索引结构，google提供了一个BTree库，直接导入即可：github.com/google/btree。在实现索引接口之前，还需要对导入的BTree进行简单的封装：data/btree.go// BTree 索引数据结构，封装了google的btree库 type BTree struct { tree *btree.BTree lock *sync.RWMutex } // NewBTree 初始化一个BTree对象 func NewBTree() *BTree { return &BTree{ //这里的degree指的是BTree的度，也就是每个节点最多有多少个子节点 tree: btree.New(32), //lock: &sync.RWMutex{}, lock: new(sync.RWMutex), } }接口实现：index/btree.gofunc (bt *BTree) Put(key []byte, pos *data.LogRecordPos) bool { it := &Item{key: key, pos: pos} bt.lock.Lock() bt.tree.ReplaceOrInsert(it) bt.lock.Unlock() return true } func (bt *BTree) Get(key []byte) *data.LogRecordPos { it := &Item{key: key} bt.lock.RLock() value := bt.tree.Get(it) if value == nil { return nil } bt.lock.RUnlock() return value.(*Item).pos } func (bt *BTree) Delete(key []byte) bool { it := &Item{key: key} //会返回被删除的值，如果btree中没有这个值，则返回nil oldItem := bt.tree.Delete(it) if oldItem == nil { return false } return true }在Put方法中，我们调用了BTree中自带的ReplaceOrInsert方法：// ReplaceOrInsert adds the given item to the tree. If an item in the tree // already equals the given one, it is removed from the tree and returned. // Otherwise, nil is returned. // // nil cannot be added to the tree (will panic). func (t *BTree) ReplaceOrInsert(item Item) Item { if item == nil { panic("nil item being added to BTree") } if t.root == nil { t.root = t.cow.newNode() t.root.items = append(t.root.items, item) t.length++ return nil } else { t.root = t.root.mutableFor(t.cow) if len(t.root.items) >= t.maxItems() { item2, second := t.root.split(t.maxItems() / 2) oldroot := t.root t.root = t.cow.newNode() t.root.items = append(t.root.items, item2) t.root.children = append(t.root.children, oldroot, second) } } out := t.root.insert(item, t.maxItems()) if out == nil { t.length++ } return out }从上面可以看出，这个方法要求入参和出参均是一个Item类型，所以我们还需要定义一个Item结构体，同时还要求这个结构体实现一个Less方法：// Item represents a single object in the tree. type Item interface { // Less tests whether the current item is less than the given argument. // // This must provide a strict weak ordering. // If !a.Less(b) && !b.Less(a), we treat this to mean a == b (i.e. we can only // hold one of either a or b in the tree). Less(than Item) bool }添加Item结构体：index/index.gotype Item struct { key []byte pos *data.LogRecordPos } // Less 实现btree.Item接口的Less方法,因为BTree自带的ReplaceOrInsert方法需要比较key的大小 func (itemA *Item) Less(itemB btree.Item) bool { //.(*Item) 是一个类型断言，它尝试将 itemB 断言为 *Item 类型。 //如果 itemB 确实是 *Item 类型，那么这个断言就会成功，我们就可以访问其 key 字段。 //如果 itemB 不是 *Item 类型，那么这个断言就会失败，程序会抛出一个运行时错误 return bytes.Compare(itemA.key, itemB.(*Item).key) == -1 }单元测试PUTfunc TestBTree_Put(t *testing.T) { bTree := NewBTree() rep1 := bTree.Put(nil, &data.LogRecordPos{Fid: 1, Offset: 10}) assert.True(t, rep1) rep2 := bTree.Put([]byte("a"), &data.LogRecordPos{Fid: 1, Offset: 20}) assert.True(t, rep2) }GETfunc TestBTree_Get(t *testing.T) { bTree := NewBTree() rep1 := bTree.Put(nil, &data.LogRecordPos{Fid: 1, Offset: 10}) assert.True(t, rep1) pos1 := bTree.Get(nil) assert.Equal(t, uint32(1),pos1.Fid) assert.Equal(t, int64(10),pos1.Offset) rep2 := bTree.Put([]byte("a"), &data.LogRecordPos{Fid: 1, Offset: 20}) assert.True(t, rep2) rep3 := bTree.Put([]byte("a"), &data.LogRecordPos{Fid: 1, Offset: 21}) assert.True(t, rep3) pos2 := bTree.Get([]byte("a")) assert.Equal(t, uint32(1), pos2.Fid) assert.Equal(t, int64(21), pos2.Offset) }DELETEfunc TestBTree_Delete(t *testing.T) { bTree := NewBTree() //part1 rep1 := bTree.Put(nil, &data.LogRecordPos{Fid: 1, Offset: 10}) assert.True(t, rep1) pos1 := bTree.Get(nil) assert.Equal(t, uint32(1), pos1.Fid) assert.Equal(t, int64(10), pos1.Offset) del1 := bTree.Delete(nil) assert.True(t, del1) pos2 := bTree.Get(nil) assert.Nil(t,pos2) //part2 rep2 := bTree.Put([]byte("aa"), &data.LogRecordPos{Fid: 2, Offset: 20}) assert.True(t, rep2) pos3 := bTree.Get([]byte("aa")) assert.Equal(t, uint32(2), pos3.Fid) del2 := bTree.Delete([]byte("aa")) assert.True(t, del2) pos4 := bTree.Get([]byte("aa")) assert.Nil(t, pos4) }踩坑如果完全按照上面的代码执行，会发现delete的单元测试无法通过，更奇怪的是，当单独执行part1或part2的测试时，又会发现没有问题。解决方法：在Get方法中，在执行完btree.Get方法后应该立刻释放读锁：func (bt *BTree) Get(key []byte) *data.LogRecordPos { it := &Item{key: key} bt.lock.RLock() value := bt.tree.Get(it) bt.lock.RUnlock() if value == nil { return nil } return value.(*Item).pos }
Joern安装之全网最详细教程写在前面网上很多关于jorn的安装教程都是几年前的了，而且joern对包的版本要求非常严格，就导致joern的安装异常麻烦。本人异常“幸运”的碰到了，历时三天终于安装成功。文章记录了本人安装joern的全过程，其中包括在安装过程中遇到的问题以及解决问题的方法，希望可以帮助到大家。所需要的各种包已经整理好放在最后。环境要求这里的环境要求尽量保持一致，否则会导致莫名其妙的问题。java 1.7python 2.7neo4j-community-2.1.8(2.1.5也可)，文末提供的安装包自带gremlin插件。关于java和neo4j的安装我这里就不细说了，网上教程一大堆，也不难，只给出安装neo4j的链接。开始安装确保jdk安装成功且环境已经配置完成；确保neo4j可以正常启动。安装joernwget https://github.com/fabsx00/joern/archive/0.3.1.tar.gztar xfzv 0.3.1.tar.gz这一步就是去那个链接下载joern的压缩包，然后解压。如果你有WinScp类似的工具，你可以直接复制这个链接去浏览器里下载，然后把解压后的文件夹放到linux就行。下载joern需要的依赖包cd joern-0.3.1wget http://mlsec.org/joern/lib/lib.tar.gztar xfzv lib.tar.gz这一步跟上一步一样，也是去那个链接里下载依赖包的压缩包，然后解压。注意要确保解压后的lib文件夹位于joern的目录下。lib文件夹中是各种各样的jar包，也就是说jar包的位置是：joern/lib/jar包下载antsudo apt-get install antsudo ant倒数第二行出现BUILD SUCCESSFUL即表示安装成功，如下图所示，其中红框中的路径下一步要用。建立连接alias joern = 'java -jar 红框中的路径'就是把第三步红框中的路径拼接在alias joern = 'java -jar后面，别忘了单引号。安装py2neo 2.0sudo pip install py2nep==2.0执行上面的代码大概率汇报错，因为2.0的包实在是过于久远，以至于pip已经抛弃了它。此时只能去github上下载，一定要下载2.0的包。https://github.com/py2neo-org/py2neo/tags?after=py2neo-2.0.7把下载好的安装包放在linux任意位置，进入py2neo的根目录，执行下面的代码：sudo python2 setup.py install安装python-joernsudo apt-get install python-setuptools python-devwget https://github.com/fabsx00/python-joern/archive/0.3.1.tar.gztar xfzv 0.3.1.tar.gzcd python-joern-0.3.1sudo python2 setup.py install这一步安装了两个东西，一定要按照上面的顺序安装。第一个的安装基本不会出错。安装的第二个是python-joern本体，这一步如果出错了，可能是因为py2neo的版本不对，也可能是某个包下载出错，譬如我这里的错误：最后一行这里提示出现了invalid syntax错误，然后很多同学就是去google或者百度invalid syntax关键字，去找解决方法，一顿操作猛如虎，结果哼哧哼哧搜了半天都是驴唇不对马嘴的答案。咱们要善于发现发现问题的本质，发挥主观能动性，不能是Terminal它说啥就是啥，怎么办呢？找到出现错误的第一行：上面的代码一看真的头皮发麻，我一开始也是这样，不想去读代码，反正就是赖。但是你稍微读一下就会发现下面的信息第一个红框肯定是正常信息，因为执行的时候没报错。咱们找错误肯定先找出错的第一行，首先定位关键字Traceback，用来跟踪错误出处的。一看到Traceback这个关键字就知道错误的起点在这部分，然后往上看，发现Reading.... Downloading...啥的，最后发现pyparsing这一看就是一个python包呀，也就是说它在下载这个包的时候出错了。那怎么办？去github上下载下来然后自己手动安装pyparsing版本那么多，咱们要下载哪一个版本呢？看上图箭头指出来的信息：searching for pyparsing != 3.0.5 ,>=2.0.2这里的!=和>=这个两个符号就很有意思，尤其是这个>=，打个比方：你给你女朋友买了个火腿肠(3.0.5)，然后她说：这>=棒棒糖(2.0.2)啥意思？肯定是你女朋友想吃棒棒糖呀，那你肯定得满足她不是？(我一个单身狗怎么能会想出这么生动的比喻？)所以它大概率想要的版本是2.0.2，实在不行把3.0.5，3.0.7也都下载下来一个个排除。怎么操作？github 上搜pyparsing通过tag找到对应的版本2.0.2，下载下来并解压把文件放到linux任意位置，进入pyparsing的根目录执行 sudo python2 setup.py install完事在安装过程中遇到的很多问题都可以通过上面的方法解决，就看你愿不愿意去找Traceback如果这一步没问题，接下来的安装基本上不会出错安装joern-tools，执行下面的代码之前请确保git已经安装成功，并成功配置。（如果没有git也没关系，无git方法下面会细说）sudo apt-get install graphviz libgraphviz-devgit clone https://github.com/fabsx00/joern-toolscd joern-toolssudo python2 setup.py install这一步跟第6步一样，安装了两个东西，前一个安装基本不会出错，主要是第二个。首先说没装git怎么下载：首先把连接复制到浏览器中，去下载这个文件夹并解压进入文件夹根目录，执行 sudo python2 setup.py install完事你也可能遇到和第6步一样的问题，在安装某个包的时候出错。但是每个人的遇到的出错的包可能不一样用，所以请按照第六步说的方法排除问题。这里还是详细的说一下我遇到的奇葩问题。乍一看，又是万恶的invalid syntax，先不慌，按照第六步的说法定位Traceback关键字，于是找到下面的信息：看图说话：这肯定是在安装pygraphviz这个包的时候出错了，还是1.9版本的pygraphviz。接下来就是直接去github上下载这个包，然后进入它的根目录执行 sudo python2 setup.py install进行安装就行了。但是我遇到个很奇葩的问题：安装joern的大环境是python2.7，但是在安装pygraphviz的时候他却给我报了这样的错误：invalid syntax它又出现了！而且这次的报错信息只有这两行！是不是慌了？别慌，再分享一招，出现无效语法这个错误还有一种可能：python版本不对，怎么知道的呢？去pygraphviz的文件夹中找readme或者install文件，这两个文件都是用来说明安装这个包的注意事项以及需要的依赖。果不其然，发现了两个说明文件，readme.rst我打不开，没关系，直接看INSTALL.txt：PyGraphviz requires:Python (version 3.8, 3.9, or 3.10)可以看到它要求的环境是python3.8及以上，那咱们的python2.7环境又不能改，咋办？发挥主观能动性的时候到了！既然python不能改，但是PyGraphviz包的版本可以改呀。老办法，去github上找，从1.9版本一个一个往下试。我已经帮同学们试过了：1.8，1.7，1.6都不行，最高只有1.5版本的PyGraphviz可以适配python2.7。问题解决！到这里，joern-tools终于装好了。革命尚未成功，咱们仍需努力。离成功还有最后一步！别骄躁，做个深呼吸，平复一下激动的心情。检查安装是否成功在joern-tools的根目录执行下面的代码：joern-lookup执行完上面的代码后，如果出现下面的信息，就说明joern安装成功了。但是我TM又报错了！麻了……提示我py2neo中没找到gremlin这个模块，但是这个模块我在安装neo4j的时候就装进去了(因为gremlin是neo4j的一个插件，是装在neo4j中的)。吻住，别慌。既然gremlin模块没问题，那就是py2neo的问题，因为一开始我使用pip命令装了一个python3的py2neo，可能是识别py2neo的时候没有用上2.0的py2neo，所以我把python3的py2neo卸载了：注意我这里卸载的是2021.2.3版本的py2neo，如果你也遇到了这个麻烦请一定要注意自己删除的版本是否是多余的版本，别删错了。果然，删除了多余的py2neo后就成功了。守得云开见月明，类目，完结撒花~写在最后一个简简单单的安装竟然也写了2000多字，除了记录一下第一次遇见环境这么复杂的而且教程也极少的软件安装，也同样想和大家分享自己遇见问题时解决问题的思路，我觉得解决问题的思路比解决问题的方法更让人收益。如果本文有帮到你，记得点个赞，拜~[button color="success" icon="" url="https://pan.baidu.com/s/1mYqNcdB5PhCKIhQFkQ03Yw " type=""]百度网盘[/button]密码：
[scode type="green" size=""]尝试了很多教程，有些美化操作因为主题版本不适配而失效，所以这里只记录适用于handsome8.3版本的美化操作有本文未提到的且适用于8.3版本的美化教程欢迎在评论区中留言。本文会持续更新。[/scode][tabs][tab name="外观" active="true"]此栏美化教程需要在主题设置/开发者设置中自行添加js或者css代码，以及对主题源代码进行改动，若日后更新主题，可能需要重复美化过程，比较麻烦。个人更推荐使用插件。[collapse title="一、 添加额外表情包" status="false"]用压缩包内的0w0.js文件替换usr/themes/handsome/usr下的同名文件将压缩包中另外两个文件夹：weibo和paopao放入/usr/themes/handsome/assets/img/emotion目录下若感觉表情包尺寸太大或太小，可将下面的代码填入开发者设置/自定义css中自行调节大小，个人推荐25px左右.emotion-weibo { width: 100px; } .emotion-tieba { width: 100px; } .comment-content-true img.emotion-weibo { max-width: 100%!important; } .comment-content-true img.emotion-tieba { max-width: 100%!important; }[button color="info" icon="glyphicon glyphicon-download-alt" url="https://pan.baidu.com/s/1rm7RSUhrV5yRO0EHK1csvg" type=""]百度云[/button]提取码：[/collapse][collapse title="二、 给左上角头像添加特效" status="false"]在开发者设置/css中添加以下代码即可旋转特效，可通过totate字段调节旋转角度img-full {width: 100px;border-radius: 50%;animation: light 4s ease-in-out infinite;transition: 0.5s;}.img-full:hover {transform: scale(1.15) rotate(720deg);}礼花特效@keyframes light { 0% { box-shadow: 0 0 4px #f00; } 25% { box-shadow: 0 0 16px #0f0; } 50% { box-shadow: 0 0 4px #00f; } 75% { box-shadow: 0 0 16px #0f0; } 100% { box-shadow: 0 0 4px #f00; } }[/collapse][collapse title="三、 去除首页顶部博客名称" status="false"]进入/usr/themes/handsome/index.php文件，删除以下代码 <?php $this->options->title(); ?> [/collapse][collapse title="四、 彩色标签云" status="false"]在开发者设置/java script中添加以下代码 let tags = document.querySelectorAll("#tag_cloud-2 a"); let colorArr = ["#000000", "#000000", "#000000", "#000000", "#000000", "#000000"]; tags.forEach(tag => { tagsColor = colorArr[Math.floor(Math.random() * colorArr.length)]; tag.style.backgroundColor = tagsColor; }); let tags = document.querySelectorAll("#tag_cloud-2 a"); let colorArr = ["#ADD8E6", "#ADD8E6", "#ADD8E6", "#ADD8E6", "#ADD8E6", "#ADD8E6"]; tags.forEach(tag => { tagsColor = colorArr[Math.floor(Math.random() * colorArr.length)]; tag.style.backgroundColor = tagsColor; }); let tags = document.querySelectorAll("#tag_cloud-2 a"); let colorArr = ["#428BCA", "#AEDCAE", "#ECA9A7", "#DA99FF", "#FFB380", "#D9B999"]; tags.forEach(tag => { tagsColor = colorArr[Math.floor(Math.random() * colorArr.length)]; tag.style.backgroundColor = tagsColor; }); let tags = document.querySelectorAll("#tag_cloud-2 a"); let colorArr = ["#00BFFF", "#00BFFF", "#00BFFF", "#00BFFF", "#00BFFF", "#00BFFF"]; tags.forEach(tag => { tagsColor = colorArr[Math.floor(Math.random() * colorArr.length)]; tag.style.backgroundColor = tagsColor; });[/collapse][collapse title="五、 底部栏框高度调节" status="false"]进入开发者设置/css，添加以下代码 .wrapper { padding: 10px } [/collapse][collapse title="六、 文章底部添加版权信息" status="false"]此操作达到的美化效果和CopyRight插件功能类似，但插件可以控制特定页面是否显示版权信息。个人觉得插件显示的版权信息的行间距太大。首先在usr/themes/handsome/post.php添加以下代码 本文作者：<?php $this->author(); ?> 本文链接：<a href="<?php $this->permalink() ?>" title="<?php $this->title() ?>"><?php $this->permalink() ?> 版权声明：本博客所有文章除特别声明外，均默认采用 CC BY-NC-SA 4.0 许可协议。 然后在开发者设置/css中添加以下代码.post-copyright { font-size: 13px; margin: 8px 0; padding: 10px; border-left: 4px solid #7fff00; background-color: rgba(220, 220, 220, 0.1); list-style: none; word-break: break-all; position: relative; overflow: hidden; } .post-copyright li { display: list-item; text-align: -webkit-match-parent; } .post-copyright a { color: rgba(0, 120, 231, 1); text-decoration: none; transition: color .1s; }[/collapse][collapse title="七、 给文章卡片周围添加灰色阴影" status="false"]在开发者设置/css中添加以下代码.panel{ box-shadow: 1px 1px 5px 5px rgba(169,169,169,2); -moz-box-shadow: 1px 1px 5px 5px rgba(169,169,169,2); } .panel:hover{ box-shadow: 1px 1px 5px 5px rgba(169,169,169,2); -moz-box-shadow: 1px 1px 5px 5px rgba(169,169,169,2); } .panel-small{ box-shadow: 1px 1px 5px 5px rgba(169,169,169,2); -moz-box-shadow: 1px 1px 5px 5px rgba(169,169,169,2); } .panel-small:hover{ box-shadow: 1px 1px 5px 5px rgba(169,169,169,2); -moz-box-shadow: 1px 1px 5px 5px rgba(169,169,169,2); } #如果也想使盒子四周也有阴影，加上以下代码 .app.container { box-shadow: 0 0 30px rgba(169,169,169,2); }[/collapse][collapse title="持续更新中...." status="false"]暂无[/collapse][/tab][tab name="插件"]此栏目所介绍的插件都可以通过typecho官网下载，这里只介绍我本人使用过且觉得不错的插件。[collapse title="一、 添加评论回复提醒" status="false"]添加评论提醒有很多插件，这里只介绍4种：CommentToMail、CommentToWechat、WeChatNotice、LoveKKComment。[scode type="green" size=""]CommentToMail比较老了，我当时安装的时候测试显示没问题，但是在实际环境中始终收不到提醒，捣鼓了很久也没弄好；[/scode][scode type="green" size=""]CommentToWechat、WeChatNotice这两个插件可以让博主在微信上收到读者的评论提醒，但是博主如果回复了评论，读者是收不到提醒的；[/scode][scode type="green" size=""]LoveKKComment是目前功能比较玩完善的评论提醒插件了，支持阿里云的邮件服务，读者和博主双方都能收到彼此的评论提醒，响应速度也比较快。具体的使用方法下面这个链接里的教程非常详细，不再重复赘述，这里只说我自己踩到的坑：进行验证回复地址这个步骤时，尽量不要在移动端进行验证，否则可能导致验证无法通过。[/scode][button color="success" icon="glyphicon glyphicon-link" url="https://www.wuqintai.com/archives/2157.html" type=""]插件使用教程[/button][/collapse][collapse title="持续更新中...." status="false"]暂无[/collapse][/tab][/tabs]
[timeline title="每个人都是一本书" type="large" start="梦开始的地方" end=""][item date="2022-2-21"] 网站诞生啦 ::aru:proud:: [/item][item date="2022-2-28"]第一次骑电瓶车上路就被罚款了，没戴头盔、没上牌照 ::aru:crying:: [/item][/timeline]
添加贴吧表情包在开发者/自定义css中添加一下代码.emotion-weibo { width: 24px; } .emotion-tieba { width: 25px; } .comment-content-true img.emotion-weibo { max-width: 100%!important; } .comment-content-true img.emotion-tieba { max-width: 100%!important; } 2添加头像旋转特效.img-full { width: 100px; border-radius: 50%; animation: light 4s ease-in-out infinite; transition: 0.5s;}.img-full:hover {transform: scale(1.15) rotate(720deg);}@keyframes light {0% { box-shadow: 0 0 4px #f00; } 25% { box-shadow: 0 0 16px #0f0; } 50% { box-shadow: 0 0 4px #00f; } 75% { box-shadow: 0 0 16px #0f0; } 100% { box-shadow: 0 0 4px #f00; }}3文章左侧图标和评论头像悬停特效没搞懂啥意思css.img-circle { transition: all 0.3s;}.img-circle:hover {transform: rotate(360deg);}3首页文章卡片悬浮特效css.blog-post .panel:not(article) {transition: all 0.3s;}.blog-post .panel:not(article):hover {transform: translateY(-10px); box-shadow: 0 8px 10px rgba(73, 90, 47, 0.47);}4彩色标签云js let tags = document.querySelectorAll("#tag_cloud-2 a");let colorArr = ["#000000", "#000000", "#000000", "#000000", "#000000", "#000000"];tags.forEach(tag => {tagsColor = colorArr[Math.floor(Math.random() * colorArr.length)]; tag.style.backgroundColor = tagsColor;}); let tags = document.querySelectorAll("#tag_cloud-2 a");let colorArr = ["#C0C0C0", "#C0C0C0", "#C0C0C0", "#C0C0C0", "#C0C0C0", "#C0C0C0"];tags.forEach(tag => {tagsColor = colorArr[Math.floor(Math.random() * colorArr.length)]; tag.style.backgroundColor = tagsColor;}); let tags = document.querySelectorAll("#tag_cloud-2 a");let colorArr = ["#ADD8E6", "#ADD8E6", "#ADD8E6", "#ADD8E6", "#ADD8E6", "#ADD8E6"];tags.forEach(tag => {tagsColor = colorArr[Math.floor(Math.random() * colorArr.length)]; tag.style.backgroundColor = tagsColor;}); let tags = document.querySelectorAll("#tag_cloud-2 a");let colorArr = ["#428BCA", "#AEDCAE", "#ECA9A7", "#DA99FF", "#FFB380", "#D9B999"];tags.forEach(tag => {tagsColor = colorArr[Math.floor(Math.random() * colorArr.length)]; tag.style.backgroundColor = tagsColor;}); let tags = document.querySelectorAll("#tag_cloud-2 a");let colorArr = ["#00BFFF", "#00BFFF", "#00BFFF", "#00BFFF", "#00BFFF", "#00BFFF"];tags.forEach(tag => {tagsColor = colorArr[Math.floor(Math.random() * colorArr.length)]; tag.style.backgroundColor = tagsColor;});