nextflow中文文档(3):Nextflow脚本(Nextflow scripting)

Nextflow scripting

Nextflow脚本语言是Groovy编程语言的扩展。Groovy是用于Java虚拟机的强大编程语言。Nextflow语法专门用于以声明的方式简化计算管道的编写。

Nextflow可以执行任何Groovy代码段或使用JVM平台的任何库。

详细了解Groovy编程语言,请参考这些链接:
Groovy User Guide
Groovy Cheat sheet
Groovy in Action

下面是关于Nextflow脚本语言中使用的最重要的语言结构的速成课程。

!Warning

Nextflow使用UTF-8作为源文件和应用程序文件的默认文件字符编码。在使用您最喜欢的文本编辑器编辑Nextflow脚本时,请确保使用UTF-8编码。

Language basics

Hello world

打印内容就像使用printprintln方法一样简单。

println "Hello, World!"

两者之间的唯一区别是println方法隐式地向打印字符串追加一个 new line character(在输出结果末尾增加一行)。

Variables(变量)

要定义一个变量,只需给它赋一个值:

x = 1
println x

x = new java.util.Date()
println x

x = -3.1499392
println x

x = false
println x

x = "Hi"
println x

Lists(列表)

List对象可以通过将列表项放在方括号中来定义:

myList = [1776, -1, 33, 99, 0, 928734928763]

您可以使用方括号符号访问列表中的给定项(索引从0开始):

println myList[0]

为了获得列表的长度,使用size方法:

println myList.size()

了解更多关于列表的信息:
Groovy Lists tutorial
Groovy List SDK
Java List SDK

Maps

Maps用于存储associative arrays or dictionaries。它们是异构的、命名的数据的无序集合:

scores = [ "Brett":100, "Pete":"Did not finish", "Andrew":86.87934 ]

注意,存储在Map中的每个值可以是不同的类型。Brett是整数,Pete是字符串,Andrew是浮点数。
可以通过两种方式访问Map中的值:

println scores["Pete"]
println scores.Pete

要向Map添加数据或修改映射,其语法类似于向列表添加值:

scores["Pete"] = 3
scores["Cedric"] = 120

了解更多关于Map的信息:
Groovy Maps tutorial
Groovy Map SDK
Java Map SDK

多重赋值(Multiple assignment)

数组或列表对象可以用于一次给多个变量赋值:

(a, b, c) = [10, 20, 'foo']
assert a == 10 && b == 20 && c == 'foo'

赋值操作符左边的三个变量由列表中相应的项传入。
更多关于多重赋值(Multiple assignment )的信息,请参阅Groovy文档。

条件执行(Conditional Execution)

任何编程语言最重要的特性之一就是能够在不同的条件下执行不同的代码。最简单的方法是使用if构造:

x = Math.random()
if( x < 0.5 ) {
println "You lost."
}
else {
println "You won!"
}

字符串(Strings)

字符串可以用单引号或双引号('")来定义:

println "he said 'cheese' once"
println 'he said "cheese!" again'

字符串可以用+连接:

a = "world"
print "hello " + a + "\n"

字符串嵌入值(String interpolation)

单引号字符串和双引号字符串之间有一个重要的区别:双引号字符串支持变量嵌入值,而单引号字符串不支持。
在实践中,双引号字符串可以包含任意变量的值(在其名称前加上$字符前缀),也可以包含任何表达式的值(使用${expression}语法),类似于Bash/shell脚本:

foxtype = 'quick'
foxcolor = ['b', 'r', 'o', 'w', 'n']
println "The $foxtype ${foxcolor.join()} fox"

x = 'Hello'
println '$x + $y'

这段代码打印:

The quick brown fox
$x + $y

多行字符串(Multi-line strings)

跨越多行的文本块可以通过使用三个单引号或双引号来定义:

text = """
hello there James
how are you today?
"""

!Note

和之前一样,双引号内的多行字符串支持变量嵌入值,而单引号内的多行字符串不支持。
在Bash/shell脚本中,用\字符结束多行字符串中的一行,可以防止用一个新行字符将该行与下一行分隔开:

myLongCmdline = """ blastp \
-in $input_query \
-out $output_file \
-db $blast_database \
-html
"""

result = myLongCmdline.execute().text

在前面的例子中,blastp和它的-in-out-db-html参数实际上是一行。

内置变量(Implicit variables)

脚本内置变量

以下变量是在脚本全局执行范围内内置的:

Name Description
baseDir The directory where the main workflow script is located (deprecated in favour of projectDir since 20.04.0).
launchDir The directory where the workflow is run (requires version 20.04.0 or later).
moduleDir The directory where a module script is located for DSL2 modules or the same as projectDir for a non-module script (requires version 20.04.0 or later).
nextflow Dictionary like object representing nextflow runtime information (see Nextflow metadata).
params Dictionary like object holding workflow parameters specifing in the config file or as command line options.
projectDir The directory where the main script is located (requires version 20.04.0 or later).
workDir The directory where tasks temporary files are created.
workflow Dictionary like object representing workflow runtime information (see Runtime metadata).

配置内置变量(Configuration implicit variables)

在Nextflow配置文件中内置了以下变量::

Name Description
baseDir The directory where the main workflow script is located (deprecated in favour of projectDir since 20.04.0).
launchDir The directory where the workflow is run (requires version 20.04.0 or later).
projectDir The directory where the main script is located (requires version 20.04.0 or later).

流程内置变量(Process implicit variables)

在流程定义范围内,task内置变量是可用的,它允许访问当前任务配置指令。例如

process foo {
script:
"""
some_tool --cpus $task.cpus --mem $task.memory
"""
}

在上面的代码中,task.cpu报告 cpus directive
task.memorymemory directive
的当前值取决于工作流配置文件中给出的实际设置。
详情请参阅Process directives

Closures

简单地说,closure是可以作为参数传递给函数的代码块。因此,您可以定义一段代码,然后像传递字符串或整数那样传递它。准确说,您可以创建定义为first class objects的函数。

square = { it * it }

表达式it * it周围的花括号告诉脚本解释器将该表达式视为代码。it标识符是一个内置变量,表示在调用函数时传递给函数的值。
一旦编译,函数对象将被赋值给变量square,就像前面显示的其他变量赋值一样。eg:

println square(9)
and get the value 81.

这并不是很有趣,直到我们发现可以将函数square作为参数传递给其他函数或方法。一些内置函数接受这样的函数作为参数。列表上的collect方法就是一个例子:

[ 1, 2, 3, 4 ].collect(square)

这个表达式的意思是:创建一个值为1、2、3和4的数组,然后调用它的collect方法,传入上面定义的closure 。collect方法遍历数组中的每个项,调用该项上的closure,然后将结果放入新数组中,结果为:

[ 1, 4, 9, 16 ]

要了解更多可以用closure作为参数调用的方法,请参阅Groovy GDK
默认情况下,closures 接受一个名为it的参数,但您也可以使用多个自定义命名的参数创建closures。例如,Map.each()方法可以接受一个带两个参数的closure,它将Map中每个键-值对的和关联绑定到这个closure上。这里,我们在closure中使用了明显的变量名keyvalue

printMapClosure = { key, value ->
println "$key = $value"
}

[ "Yue" : "Wu", "Mark" : "Williams", "Sudha" : "Kumari" ].each(printMapClosure)
Prints:
Yue=Wu
Mark=Williams
Sudha=Kumari

closure还有另外两个重要特征。首先,它可以访问定义它的作用域中的变量,以便与它们进行交互。
其次,closure可以以anonymous的方式定义,这意味着closure没有指定名称,而是在需要使用它的地方定义。
作为展示这两个特性的示例,请参见下面的代码:

myMap = ["China": 1 , "India" : 2, "USA" : 3]

result = 0
myMap.keySet().each( { result+= myMap[it] } )
println result

Groovy documentation中了解关于closure的更多信息。

正则表达式(Regular expressions)

正则表达式是文本处理的Swiss Army knife。它们为程序员提供了从字符串中匹配和提取patterns的能力。
正则表达式可以通过~/pattern/语法和=~==~操作符使用。
使用=~来检查给定的pattern是否出现在字符串中的任意位置:

assert 'foo' =~ /foo/ // return TRUE
assert 'foobar' =~ /foo/ // return TRUE

使用==~检查字符串是否完全匹配给定的正则表达式pattern。

assert 'foo' ==~ /foo/ // return TRUE
assert 'foobar' ==~ /foo/ // return FALSE

值得注意的是,~操作符从给定的字符串创建Java Pattern对象,而=~操作符创建Java Matcher对象。

x = ~/abc/
println x.class
// prints java.util.regex.Pattern

y = 'some string' =~ /abc/
println y.class
// prints java.util.regex.Matcher

正则表达式support是从Java导入的。Java的正则表达式语言和API记录在Pattern Java documentation
您可能还对这篇文章感兴趣:Groovy: Don’t Fear the RegExp

String replacement

在给定的字符串中替换出现的pattern,请使用replaceFirstreplaceAll方法:

x = "colour".replaceFirst(/ou/, "o")
println x
// prints: color

y = "cheesecheese".replaceAll(/cheese/, "nice")
println y
// prints: nicenice

Capturing groups

您可以匹配包含groups的pattern。首先使用=~操作符创建一个匹配器对象。然后,您可以索引 matcher object 来查找matches:matcher[0]返回一个列表,表示字符串中正则表达式的第一个匹配。第一个列表元素是匹配整个正则表达式的字符串,其余元素是匹配每个组的字符串。
下面是它的工作原理:

programVersion = '2.7.3-beta'
m = programVersion =~ /(\d+).(\d+).(\d+)-?(.+)/

assert m[0] == ['2.7.3-beta', '2', '7', '3', 'beta']
assert m[0][1] == '2'
assert m[0][2] == '7'
assert m[0][3] == '3'
assert m[0][4] == 'beta'

应用一些syntactic sugar, 您可以在一行代码中完成相同的工作:

programVersion = '2.7.3-beta'
(full, major, minor, patch, flavor) = (programVersion =~ /(\d+).(\d+).(\d+)-?(.+)/)[0]

println full // 2.7.3-beta
println major // 2
println minor // 7
println patch // 3
println flavor // beta

删除字符串中的一部分(Removing part of a string)

可以使用正则表达式模式删除部分String值。找到的第一个匹配将被一个空字符串替换:

// define the regexp pattern
wordStartsWithGr = ~/(?i)\s+Gr\w+/

// apply and verify the result
('Hello Groovy world!' - wordStartsWithGr) == 'Hello world!'
('Hi Grails users' - wordStartsWithGr) == 'Hi users'

从字符串中删除第一个是5个字符的单词:

assert ('Remove first match of 5 letter word' - ~/\b\w{5}\b/) == 'Remove match of 5 letter word'

从字符串中删除第一个数字及其末尾的空格:

assert ('Line contains 20 characters' - ~/\d+\s+/) == 'Line contains characters'

Files and I/O

要访问和处理文件,请使用file方法,该方法返回给定文件路径字符串的文件系统对象:

myFile = file('some/path/to/my_file.file')

file方法可以引用文件或目录,具体取决于文件系统中的字符串路径所引用的内容。
当使用通配符 *, ?, []和{}时,参数被解释为一个glob path matcher,file方法返回一个列表对象,其中包含文件名与指定模式匹配的文件的路径,如果没有找到匹配,则返回一个空列表:

listOfFiles = file('some/path/*.fa')

!Note

glob模式中的两个星号(**)类似于*,但可以匹配文件系统路径中的任意数量的目录组件。
默认情况下,通配符不匹配目录或隐藏文件。例如,如果你想在结果列表中包含隐藏文件,可以添加可选参数hidden:

listWithHidden = file('some/path/*.fa', hidden: true)

这里是file的可用选项:

Name Description
glob When true interprets characters *, ?, [] and {} as glob wildcards, otherwise handles them as normal characters (default: true)
type Type of paths returned, either file, dir or any (default: file)
hidden When true includes hidden files in the resulting paths (default: false)
maxDepth Maximum number of directory levels to visit (default: no limit)
followLinks When true follows symbolic links during directory tree traversal, otherwise treats them as files (default: true)
checkIfExists When true throws an exception of the specified path do not exist in the file system (default: false)

!Tip

如果你是一个Java geek,您会有兴趣知道file方法返回一个 Path对象,它允许你使用在Java程序中常见的方法。
See also: Channel.fromPath.

Basic read/write

给定一个file变量(使用前面示例中所示的file方法声明),读取文件就像获取文件的text属性的值一样容易,该属性以字符串值的形式返回文件内容:

print myFile.text

类似地,您可以通过将字符串值分配给文件的text属性来将其保存到文件中:

myFile.text = 'Hello world!'

!Note

现有的文件内容被赋值操作覆盖,它也可以创建不存在的文件。

为了在不删除现有内容的情况下将字符串值追加到文件中,可以使用append方法:

myFile.append('Add this line\n')

或者使用left shift,这是将文本内容追加到文件的更常用的方法:

myFile << 'Add a line more\n'

二进制数据也可以以同样的方式管理,只是使用文件属性bytes而不是text。因此,下面的示例读取文件并以字节数组的形式返回其内容:

binaryContent = myFile.bytes

或者,您可以通过写入操作将字节数组数据缓冲区保存到文件中:

myFile.bytes = binaryBuffer

!Warning

上述方法在单个变量或缓冲区中一次读取和写入所有文件内容。因此,在处理大文件时不建议使用它们,因为大文件需要更有效的内存方法,例如逐行读取文件或使用固定大小的缓冲区。

Read a file line by line

为了逐行读取文本文件,你可以使用file对象提供的readLines()方法,它以字符串列表的形式返回文件内容:

myFile = file('some/my_file.txt')
allLines = myFile.readLines()
for( line : allLines ) {
println line
}

常用写法:

file('some/my_file.txt')
.readLines()
.each { println it }

!Note

readLines()方法会一次读取所有文件内容,并返回包含所有行的列表。因此,不要使用它来读取大文件。
要处理大文件,可以使用方法eachLine,它每次只向内存中读入一行:

count = 0
myFile.eachLine { str ->
println "line ${count++}: $str"
}

高级文件读取操作(Advanced file reading operations)

Reader类和InputStream类分别为读取文本和二进制文件提供了很好的方法。
newReader方法为给定的文件创建一个Reader对象,允许您以单个字符、行或字符数组的形式读取内容:

myReader = myFile.newReader()
String line
while( line = myReader.readLine() ) {
println line
}
myReader.close()

withReader方法的工作原理与此类似,但当您完成文件处理后,它会自动为您调用close方法。因此,前面的例子可以更简单地写成如下:

myFile.withReader {
String line
while( line = it.readLine() ) {
println line
}
}

方法newInputStreamwithInputStream的工作方式类似。主要的区别是它们创建了一个用于写入二进制数据的InputStream对象。
下面是最重要的从文件中读取的方法:

Name Description
getText Returns the file content as a string value
getBytes Returns the file content as byte array
readLines Reads the file line by line and returns the content as a list of strings
eachLine Iterates over the file line by line, applying the specified closure
eachByte Iterates over the file byte by byte, applying the specified closure
withReader Opens a file for reading and lets you access it with a Reader object
withInputStream Opens a file for reading and lets you access it with an InputStream object
newReader Returns a Reader object to read a text file
newInputStream Returns an InputStream object to read a binary file

阅读ReaderInputStream类的Java文档,了解更多关于从文件中读取数据的方法。

高级文件写入操作(Advanced file writing operations)

WriterOutputStream类分别为文本和二进制文件的编写提供了很好的方式,包括对单个字符或字节的简单操作,以及对大文件的支持。

例如,给定两个文件对象sourceFiletargetFile,下面的代码将第一个文件的内容复制到第二个文件中,用X替换所有U字符:

sourceFile.withReader { source ->
targetFile.withWriter { target ->
String line
while( line=source.readLine() ) {
target << line.replaceAll('U','X')
}
}
}

下面是最重要的写入文件的方法:

Name Description
setText Writes a string value to a file
setBytes Writes a byte array to a file
write Writes a string to a file, replacing any existing content
append Appends a string value to a file without replacing existing content
newWriter Creates a Writer object that allows you to save text data to a file
newPrintWriter Creates a PrintWriter object that allows you to write formatted text to a file
newOutputStream Creates an OutputStream object that allows you to write binary data to a file
withWriter Applies the specified closure to a Writer object, closing it when finished
withPrintWriter Applies the specified closure to a PrintWriter object, closing it when finished
withOutputStream Applies the specified closure to an OutputStream object, closing it when finished

请阅读Writer
PrintWriter
OutputStream类的Java文档,了解更多关于将数据写入文件的方法。

列出目录内容(List directory content)

假设您需要遍历您选择的目录。您可以定义指向它的myDir变量:

myDir = file('any/path')

获取目录列表的最简单方法是使用listlistFiles方法,它们返回目录的一级元素(文件和目录)的集合:

allFiles = myDir.list()
for( def file : allFiles ) {
println file
}

!Note

listlistFiles之间的唯一区别是前者返回一个字符串列表,而后者返回一个允许您访问文件元数据的文件对象列表,例如大小、最后修改时间等。

eachFile方法只允许迭代第一级元素(就像listFiles)。与其他each- methods一样,each files接受一个closure作为参数:

myDir.eachFile { item ->
if( item.isFile() ) {
println "{item.getName()} - size:{item.size()}"
}
else if( item.isDirectory() ) {
println "${item.getName()} - DIR"
}
}

上述方法有几种变体可用。完整列表见下表。

Name Description
eachFile Iterates through first-level elements (files and directories). Read more
eachDir Iterates through first-level directories only. Read more
eachFileMatch Iterates through files and dirs whose names match the given filter. Read more
eachDirMatch Iterates through directories whose names match the given filter. Read more
eachFileRecurse Iterates through directory elements depth-first. Read more
eachDirRecurse Iterates through directories depth-first (regular files are ignored). Read more

参见:Channel fromPath方法。

创建目录(Create directories)

给定一个表示不存在目录的文件变量,如下所示:

myDir = file('any/path')

mkdir方法在给定路径上创建一个目录,如果目录创建成功则返回true,否则返回false:

result = myDir.mkdir()
println result ? "OK" : "Cannot create directory: $myDir"

!Note

如果父目录不存在,上述方法将失败并返回false

mkdirs方法创建以文件对象命名的目录,包括任何不存在的父目录:

myDir.mkdirs()

创建链接(Create links)

对于一个文件,mklink方法为使用指定的路径作为参数的文件创建* file system link *:

myFile = file('/some/path/file.txt')
myFile.mklink('/user/name/link-to-file.txt')

可选参数表:

Name Description
hard When truecreates a hard link, otherwise creates a soft (aka symbolic) link. (default: false)
overwrite When true overwrites any existing file with the same name, otherwise throws a FileAlreadyExistsException (default: false)

复制文件(Copy files)

copyTo方法将文件复制到新文件或目录中,或将目录复制到新目录中:

myFile.copyTo('new_name.txt')

!Note

如果目标文件已经存在,它将被新的文件替换。还要注意,如果目标是一个目录,源文件将被复制到该目录中,并保持文件的原始名称。

当源文件是一个目录时,它的所有内容都被复制到目标目录:

myDir = file('/some/path')
myDir.copyTo('/some/new/path')

If the target path does not exist, it will be created automatically.

!Tip

copyTo方法模仿了Linux命令cp -r <source> <target>,注意:Linux工具通常将以斜杠结尾的路径(e.g. /some/path/name/)视为目录,而将不以斜杠结尾的路径(e.g. /some/path/name)视为常规文件,Nextflow(由于使用了Java文件API)将这两个路径视为相同的文件系统对象。如果路径存在,它将根据它的实际类型(即作为一个常规文件或目录)进行处理。如果该路径不存在,则将其视为常规文件,并自动创建任何缺失的父目录。

移动文件(Move files)

您可以使用moveTo方法移动文件:

myFile = file('/some/path/file.txt')
myFile.moveTo('/another/path/new_file.txt')

!Note

当与目标同名的文件已经存在时,它将被源文件替换。还要注意,当目标是一个目录时,文件将被移动到(或移动到)该目录中,并保持文件的原始名称。

当源目录是一个目录时,所有目录内容都被移动到目标目录:

myDir = file('/any/dir_a')
myDir.moveTo('/any/dir_b')

请注意,上述示例的结果取决于目标目录是否存在。如果目标目录存在,则将源文件移动到目标目录,从而产生路径:

/any/dir_b/dir_a

如果目标目录不存在,则源文件将被重命名为目标名称,从而产生路径:

/any/dir_b

!Tip

moveTo方法模拟了Linux命令mv <source> <target>,与上面给copyTo的警告相同。

重命名文件(Rename files)

您可以使用rename to file方法重命名文件或目录:

myFile = file('my_file.txt')
myFile.renameTo('new_file_name.txt')

删除文件(Delete files)

file方法delete删除给定路径下的文件或目录,如果操作成功返回true,否则返回false:

myFile = file('some/file.txt')
result = myFile.delete()
println result ? "OK" : "Can delete: $myFile"

!Note

此方法只删除不包含任何文件或子目录的目录。要删除一个目录及其所有内容(即删除它可能包含的所有文件和子目录),使用deleteDir方法。

查看文件属性

以下方法可用于使用file方法创建的文件变量:

Name Description
getName Gets the file name e.g. /some/path/file.txt -> file.txt
getBaseName Gets the file name without its extension e.g. /some/path/file.tar.gz-> file.tar
getSimpleName Gets the file name without any extension e.g. /some/path/file.tar.gz -> file
getExtension Gets the file extension e.g. /some/path/file.txt -> txt
getParent Gets the file parent path e.g. /some/path/file.txt -> /some/path
size Gets the file size in bytes
exists Returns true if the file exists, or false otherwise
isEmpty Returns true if the file is zero length or does not exist, false otherwise
isFile Returns true if it is a regular file e.g. not a directory
isDirectory Returns true if the file is a directory
isHidden Returns true if the file is hidden
lastModified Returns the file last modified timestamp i.e. a long as Linux epoch time

例如,下面的行打印文件名和大小:

println "File {myFile.getName() size:{myFile.size()}"

!Tip

任何以get前缀开始的方法名称的调用都可以省略get前缀和ending()圆括号。因此写入myFile. getname()myFile.name是完全相同的,而myFile. getbasename()myFile.baseName是完全相同的,等等。

获取和修改文件权限(Get and modify file permissions)

给定一个表示文件(或目录)的文件变量,getPermissions方法返回一个9个字符的字符串,使用Linux symbolic notation表示文件的权限,例如rw-rw-r--:

permissions = myFile.getPermissions()

类似地,setPermissions方法使用相同的表示法设置文件的权限:

myFile.setPermissions('rwxr-xr-x')

setPermissions方法的第二个版本用三个数字分别表示所有者、组和其他权限,设置文件的权限:

myFile.setPermissions(7,5,5)

Learn more about File permissions numeric notation.

HTTP/FTP files

Nextflow提供了HTTP/S和FTP协议的透明集成,将远程资源作为本地文件系统对象处理。只需将资源URL指定为文件对象的参数:

pdb = file('http://files.rcsb.org/header/5FID.pdb')

然后,您可以像前面几节所描述的那样将其作为本地文件访问:

println pdb.text

上面的一行代码打印远程PDB文件的内容。前面的部分提供了一些代码示例,展示了如何流式传输或复制文件的内容。

!Note

HTTP/S和FTP文件不支持写入和列表操作。

Counting records

countLines

countLines方法计算文本文件中的行数。

def sample = file('/data/sample.txt')
println sample.countLines()

.gz后缀结尾的文件将被GZIP压缩并自动解压缩。

countFastq

countFastq方法计算FASTQ格式文件中的records 数。

def sample = file('/data/sample.fastq')
println sample.countFastq()

.gz后缀结尾的文件将被GZIP压缩并自动解压缩。

REFERENCE
https://www.nextflow.io/docs/latest/script.html#files-and-i-o

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容