Spark算子總結版

Spark的算子的分類

從大方向來說,Spark 算子大致可以分為以下兩類:

1)Transformation 變換/轉換算子:這種變換并不觸發提交作業,完成作業中間過程處理。

 Transformation 操作是延遲計算的,也就是說從一個RDD 轉換生成另一個 RDD 的轉換操作不是馬上執行,需要等到有 Action 操作的時候才會真正觸發運算。

2)Action 行動算子:這類算子會觸發 SparkContext 提交 Job 作業。

 Action 算子會觸發 Spark 提交作業(Job),并將數據輸出 Spark系統。


從小方向來說,Spark 算子大致可以分為以下三類:

1)Value數據類型的Transformation算子,這種變換并不觸發提交作業,針對處理的數據項是Value型的數據。

2)Key-Value數據類型的Transfromation算子,這種變換并不觸發提交作業,針對處理的數據項是Key-Value型的數據對。

3)Action算子,這類算子會觸發SparkContext提交Job作業。



1)Value數據類型的Transformation算子

  一、輸入分區與輸出分區一對一型

    1、map算子

    2、flatMap算子

    3、mapPartitions算子

    4、glom算子

  二、輸入分區與輸出分區多對一型

    5、union算子

    6、cartesian算子

  三、輸入分區與輸出分區多對多型

    7、grouBy算子

  四、輸出分區為輸入分區子集型

    8、filter算子

    9、distinct算子

    10、subtract算子

    11、sample算子

? ? ?   12、takeSample算子

?  五、Cache型

    13、cache算子  

    14、persist算子


2)Key-Value數據類型的Transfromation算子

  一、輸入分區與輸出分區一對一

    15、mapValues算子

  二、對單個RDD或兩個RDD聚集

   單個RDD聚集

    16、combineByKey算子

    17、reduceByKey算子

    18、partitionBy算子

?  兩個RDD聚集

? ? ? ? ? ? ??19、Cogroup算子

  三、連接

    20、join算子

    21、leftOutJoin和 rightOutJoin算子


?3)Action算子

  一、無輸出

? ? ? ? ? ? ? ?22、foreach算子

  二、HDFS

    23、saveAsTextFile算子

    24、saveAsObjectFile算子

  三、Scala集合和數據類型

    25、collect算子

    26、collectAsMap算子

? ? ? ? ? ? ? 27、reduceByKeyLocally算子

 ? ? ? ? ? 28、lookup算子

    29、count算子

    30、top算子

    31、reduce算子

    32、fold算子

    33、aggregate算子


1. Transformations 算子

(1)?map

將原來 RDD 的每個數據項通過?map 中的用戶自定義函數 f?映射轉變為一個新的元素。源碼中 map 算子相當于初始化一個 RDD, 新 RDD 叫做 MappedRDD(this, sc.clean(f))。

圖 1中每個方框表示一個 RDD 分區,左側的分區經過用戶自定義函數 f:T->U?映射為右側的新 RDD 分區。但是,實際只有等到 Action算子觸發后,這個 f 函數才會和其他函數在一個stage 中對數據進行運算。在圖 1 中的第一個分區,數據記錄 V1 輸入 f,通過 f 轉換輸出為轉換后的分區中的數據記錄 V’1。

  圖1 ? ?map 算子對 RDD 轉換

(2)?flatMap

將原來 RDD 中的每個元素通過函數 f 轉換為新的元素,并將生成的 RDD 的每個集合中的元素合并為一個集合,內部創建 FlatMappedRDD(this,sc.clean(f))。

圖 2 表 示 RDD 的 一 個 分 區 ,進 行 flatMap函 數 操 作, flatMap 中 傳 入 的 函 數 為 f:T->U,?T和 U 可以是任意的數據類型。將分區中的數據通過用戶自定義函數 f 轉換為新的數據。外部大方框可以認為是一個 RDD 分區,小方框代表一個集合。 V1、 V2、 V3 在一個集合作為 RDD 的一個數據項,可能存儲為數組或其他容器,轉換為V’1、 V’2、 V’3 后,將原來的數組或容器結合拆散,拆散的數據形成為 RDD 中的數據項。

圖2 ? ? flapMap 算子對 RDD 轉換

(3)?mapPartitions

mapPartitions 函 數 獲 取 到 每 個 分 區 的 迭 代器,在 函 數 中 通 過 這 個 分 區 整 體 的 迭 代 器 對整 個 分 區 的 元 素 進 行 操 作。 內 部 實 現 是 生 成

MapPartitionsRDD。圖 3 中的方框代表一個 RDD 分區。圖 3 中,用戶通過函數 f (iter)=>iter.f ilter(_>=3) 對分區中所有數據進行過濾,大于和等于 3 的數據保留。一個方塊代表一個 RDD 分區,含有 1、 2、 3 的分區過濾只剩下元素 3。

圖3 ?mapPartitions 算子對 RDD 轉換

(4)glom

glom函數將每個分區形成一個數組,內部實現是返回的GlommedRDD。 圖4中的每個方框代表一個RDD分區。圖4中的方框代表一個分區。 該圖表示含有V1、 V2、 V3的分區通過函數glom形成一數組Array[(V1),(V2),(V3)]。

圖 4 ??glom算子對RDD轉換

(5)?union

使用 union 函數時需要保證兩個 RDD 元素的數據類型相同,返回的 RDD 數據類型和被合并的 RDD 元素數據類型相同,并不進行去重操作,保存所有元素。如果想去重

可以使用 distinct()。同時 Spark 還提供更為簡潔的使用 union 的 API,通過 ++ 符號相當于 union 函數操作。

圖 5 中左側大方框代表兩個 RDD,大方框內的小方框代表 RDD 的分區。右側大方框代表合并后的 RDD,大方框內的小方框代表分區。

含有V1、V2、U1、U2、U3、U4的RDD和含有V1、V8、U5、U6、U7、U8的RDD合并所有元素形成一個RDD。V1、V1、V2、V8形成一個分區,U1、U2、U3、U4、U5、U6、U7、U8形成一個分區。

圖 5 ?union 算子對 RDD 轉換

(6)?cartesian

對 兩 個 RDD 內 的 所 有 元 素?進 行 笛 卡 爾 積 操 作。 操 作 后, 內 部 實 現 返 回CartesianRDD。圖6中左側大方框代表兩個 RDD,大方框內的小方框代表 RDD 的分區。右側大方框代表合并后的 RDD,大方框內的小方框代表分區。圖6中的大方框代表RDD,大方框中的小方框代表RDD分區。

例 如: V1 和 另 一 個 RDD 中 的 W1、 W2、 Q5 進 行 笛 卡 爾 積 運 算 形 成 (V1,W1)、(V1,W2)、 (V1,Q5)。

?圖 6 ?cartesian 算子對 RDD 轉換

(7)?groupBy

groupBy :將元素通過函數生成相應的 Key,數據就轉化為 Key-Value 格式,之后將 Key 相同的元素分為一組。

函數實現如下:

1)將用戶函數預處理:

val cleanF = sc.clean(f)

2)對數據 map 進行函數操作,最后再進行 groupByKey 分組操作。

this.map(t => (cleanF(t), t)).groupByKey(p)

其中, p 確定了分區個數和分區函數,也就決定了并行化的程度。

圖7 中方框代表一個 RDD 分區,相同key 的元素合并到一個組。例如 V1 和 V2 合并為 V, Value 為 V1,V2。形成 V,Seq(V1,V2)。

圖 7?groupBy 算子對 RDD 轉換


(8)?filter

filter 函數功能是對元素進行過濾,對每個 元 素 應 用 f 函 數, 返 回 值 為 true 的 元 素 在RDD 中保留,返回值為 false 的元素將被過濾掉。 內 部 實 現 相 當 于 生 成 FilteredRDD(this,sc.clean(f))。

下面代碼為函數的本質實現:

deffilter(f:T=>Boolean):RDD[T]=newFilteredRDD(this,sc.clean(f))

圖 8 中每個方框代表一個 RDD 分區, T 可以是任意的類型。通過用戶自定義的過濾函數 f,對每個數據項操作,將滿足條件、返回結果為 true 的數據項保留。例如,過濾掉 V2 和 V3 保留了 V1,為區分命名為 V’1。

圖 8 ?filter 算子對 RDD 轉換

(9)distinct

distinct將RDD中的元素進行去重操作。圖9中的每個方框代表一個RDD分區,通過distinct函數,將數據去重。 例如,重復數據V1、 V1去重后只保留一份V1。

    圖9 ?distinct算子對RDD轉換


(10)subtract

subtract相當于進行集合的差操作,RDD 1去除RDD 1和RDD 2交集中的所有元素。圖10中左側的大方框代表兩個RDD,大方框內的小方框代表RDD的分區。 右側大方框

代表合并后的RDD,大方框內的小方框代表分區。 V1在兩個RDD中均有,根據差集運算規則,新RDD不保留,V2在第一個RDD有,第二個RDD沒有,則在新RDD元素中包含V2。

          圖10 ? subtract算子對RDD轉換


(11)?sample

sample 將 RDD 這個集合內的元素進行采樣,獲取所有元素的子集。用戶可以設定是否有放回的抽樣、百分比、隨機種子,進而決定采樣方式。內部實現是生成 SampledRDD(withReplacement, fraction, seed)。

函數參數設置:

‰   withReplacement=true,表示有放回的抽樣。

‰   withReplacement=false,表示無放回的抽樣。

圖 11中 的 每 個 方 框 是 一 個 RDD 分 區。 通 過 sample 函 數, 采 樣 50% 的 數 據。V1、 V2、 U1、 U2、U3、U4 采樣出數據 V1 和 U1、 U2 形成新的 RDD。

       圖11 ?sample 算子對 RDD 轉換


(12)takeSample

takeSample()函數和上面的sample函數是一個原理,但是不使用相對比例采樣,而是按設定的采樣個數進行采樣,同時返回結果不再是RDD,而是相當于對采樣后的數據進行

Collect(),返回結果的集合為單機的數組。

圖12中左側的方框代表分布式的各個節點上的分區,右側方框代表單機上返回的結果數組。 通過takeSample對數據采樣,設置為采樣一份數據,返回結果為V1。

    圖12 ?  takeSample算子對RDD轉換


(13)?cache

cache?將 RDD 元素從磁盤緩存到內存。 相當于 persist(MEMORY_ONLY) 函數的功能。

圖13 中每個方框代表一個 RDD 分區,左側相當于數據分區都存儲在磁盤,通過 cache 算子將數據緩存在內存。

      圖 13 Cache 算子對 RDD 轉換


(14)?persist

persist 函數對?RDD 進行緩存操作。數據緩存在哪里依據 StorageLevel 這個枚舉類型進行確定。 有以下幾種類型的組合(見10), DISK 代表磁盤,MEMORY 代表內存, SER 代表數據是否進行序列化存儲。

下面為函數定義, StorageLevel 是枚舉類型,代表存儲模式,用戶可以通過圖 14-1 按需進行選擇。

persist(newLevel:StorageLevel)

圖 14-1 中列出persist 函數可以進行緩存的模式。例如,MEMORY_AND_DISK_SER 代表數據可以存儲在內存和磁盤,并且以序列化的方式存儲,其他同理。

            圖 14-1 ?persist 算子對 RDD 轉換

  圖 14-2 中方框代表 RDD 分區。 disk 代表存儲在磁盤, mem 代表存儲在內存。數據最初全部存儲在磁盤,通過 persist(MEMORY_AND_DISK) 將數據緩存到內存,但是有的分區無法容納在內存,將含有 V1、 V2、 V3 的RDD存儲到磁盤,將含有U1,U2的RDD仍舊存儲在內存。

? ? ? 圖 14-2 ? Persist 算子對 RDD 轉換


(15)?mapValues

mapValues :針對(Key, Value)型數據中的 Value 進行 Map 操作,而不對 Key 進行處理。

? ? 圖 15 中的方框代表 RDD 分區。 a=>a+2 代表對 (V1,1) 這樣的 Key Value 數據對,數據只對 Value 中的 1 進行加 2 操作,返回結果為 3。

      圖 15 ? mapValues 算子 RDD 對轉換


(16)?combineByKey

下面代碼為 combineByKey 函數的定義:

combineByKey[C](createCombiner:(V) C,

mergeValue:(C, V) C,

mergeCombiners:(C, C) C,

partitioner:Partitioner,

mapSideCombine:Boolean=true,

serializer:Serializer=null):RDD[(K,C)]

說明:

‰   createCombiner: V => C, C 不存在的情況下,比如通過 V 創建 seq C。

‰   mergeValue: (C, V) => C,當 C 已經存在的情況下,需要 merge,比如把 item V

加到 seq C 中,或者疊加。

mergeCombiners: (C, C) => C,合并兩個 C。

‰   partitioner: Partitioner, Shuff le 時需要的 Partitioner。

‰   mapSideCombine : Boolean = true,為了減小傳輸量,很多 combine 可以在 map

端先做,比如疊加,可以先在一個 partition 中把所有相同的 key 的 value 疊加,

再 shuff le。

‰   serializerClass: String = null,傳輸需要序列化,用戶可以自定義序列化類:

例如,相當于將元素為 (Int, Int) 的 RDD 轉變為了 (Int, Seq[Int]) 類型元素的 RDD。圖 16中的方框代表 RDD 分區。如圖,通過 combineByKey, 將 (V1,2), (V1,1)數據合并為( V1,Seq(2,1))。

      圖 16? comBineByKey 算子對 RDD 轉換


(17)?reduceByKey

reduceByKey 是比 combineByKey 更簡單的一種情況,只是兩個值合并成一個值,( Int, Int V)to (Int, Int C),比如疊加。所以 createCombiner reduceBykey 很簡單,就是直接返回 v,而 mergeValue和 mergeCombiners 邏輯是相同的,沒有區別。

函數實現:

def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)]

= {

combineByKey[V]((v: V) => v, func, func, partitioner)

}

圖17中的方框代表 RDD 分區。通過用戶自定義函數 (A,B) => (A + B) 函數,將相同 key 的數據 (V1,2) 和 (V1,1) 的 value 相加運算,結果為( V1,3)。

        圖 17?reduceByKey 算子對 RDD 轉換


(18)partitionBy

partitionBy函數對RDD進行分區操作。

函數定義如下。

partitionBy(partitioner:Partitioner)

如果原有RDD的分區器和現有分區器(partitioner)一致,則不重分區,如果不一致,則相當于根據分區器生成一個新的ShuffledRDD。

圖18中的方框代表RDD分區。 通過新的分區策略將原來在不同分區的V1、 V2數據都合并到了一個分區。


    圖18  partitionBy算子對RDD轉換


(19)Cogroup

cogroup函數將兩個RDD進行協同劃分,cogroup函數的定義如下。

cogroup[W](other: RDD[(K, W)], numPartitions: Int): RDD[(K, (Iterable[V], Iterable[W]))]

對在兩個RDD中的Key-Value類型的元素,每個RDD相同Key的元素分別聚合為一個集合,并且返回兩個RDD中對應Key的元素集合的迭代器。

(K, (Iterable[V], Iterable[W]))

其中,Key和Value,Value是兩個RDD下相同Key的兩個數據集合的迭代器所構成的元組。

圖19中的大方框代表RDD,大方框內的小方框代表RDD中的分區。 將RDD1中的數據(U1,1)、 (U1,2)和RDD2中的數據(U1,2)合并為(U1,((1,2),(2)))。

        圖19 ?Cogroup算子對RDD轉換


(20)?join

join 對兩個需要連接的 RDD 進行 cogroup函數操作,將相同 key 的數據能夠放到一個分區,在 cogroup 操作之后形成的新 RDD 對每個key 下的元素進行笛卡爾積的操作,返回的結果再展平,對應 key 下的所有元組形成一個集合。最后返回 RDD[(K, (V, W))]。

下 面 代 碼 為 join 的 函 數 實 現, 本 質 是通 過 cogroup 算 子 先 進 行 協 同 劃 分, 再 通 過flatMapValues 將合并的數據打散。

this.cogroup(other,partitioner).f latMapValues{case(vs,ws) =>?for(v<-vs;w<-ws)yield(v,w) }

圖 20是對兩個 RDD 的 join 操作示意圖。大方框代表 RDD,小方框代表 RDD 中的分區。函數對相同 key 的元素,如 V1 為 key 做連接后結果為 (V1,(1,1)) 和 (V1,(1,2))。

                    圖 20 ? join 算子對 RDD 轉換


(21)eftOutJoinrightOutJoin

LeftOutJoin(左外連接)和RightOutJoin(右外連接)相當于在join的基礎上先判斷一側的RDD元素是否為空,如果為空,則填充為空。 如果不為空,則將數據進行連接運算,并

返回結果。

下面代碼是leftOutJoin的實現。

if (ws.isEmpty) {

vs.map(v => (v, None))

} else {

for (v <- vs; w <- ws) yield (v, Some(w))

}


2. Actions 算子

本質上在 Action 算子中通過 SparkContext 進行了提交作業的 runJob 操作,觸發了RDD DAG 的執行。

例如, Action 算子 collect 函數的代碼如下,感興趣的讀者可以順著這個入口進行源碼剖析:

/**

* Return an array that contains all of the elements in this RDD.

*/

def collect(): Array[T] = {

/* 提交 Job*/

val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)

Array.concat(results: _*)

}

(22)?foreach

foreach 對 RDD 中的每個元素都應用 f 函數操作,不返回 RDD 和 Array, 而是返回Uint。圖22表示 foreach 算子通過用戶自定義函數對每個數據項進行操作。本例中自定義函數為 println(),控制臺打印所有數據項。

      圖 22 foreach 算子對 RDD 轉換


(23)?saveAsTextFile

函數將數據輸出,存儲到 HDFS 的指定目錄。

下面為 saveAsTextFile 函數的內部實現,其內部

通過調用 saveAsHadoopFile 進行實現:

this.map(x => (NullWritable.get(), new Text(x.toString))).saveAsHadoopFile[TextOutputFormat[NullWritable, Text]](path)

將 RDD 中的每個元素映射轉變為 (null, x.toString),然后再將其寫入 HDFS。

圖 23中左側方框代表 RDD 分區,右側方框代表 HDFS 的 Block。通過函數將RDD 的每個分區存儲為 HDFS 中的一個 Block。

            圖 23 ? saveAsHadoopFile 算子對 RDD 轉換


(24)saveAsObjectFile

saveAsObjectFile將分區中的每10個元素組成一個Array,然后將這個Array序列化,映射為(Null,BytesWritable(Y))的元素,寫入HDFS為SequenceFile的格式。

下面代碼為函數內部實現。

map(x=>(NullWritable.get(),new BytesWritable(Utils.serialize(x))))

圖24中的左側方框代表RDD分區,右側方框代表HDFS的Block。 通過函數將RDD的每個分區存儲為HDFS上的一個Block。

            圖24 saveAsObjectFile算子對RDD轉換


(25)?collect

collect 相當于 toArray, toArray 已經過時不推薦使用, collect 將分布式的 RDD 返回為一個單機的 scala Array 數組。在這個數組上運用 scala 的函數式操作。

圖 25中左側方框代表 RDD 分區,右側方框代表單機內存中的數組。通過函數操作,將結果返回到 Driver 程序所在的節點,以數組形式存儲。

  圖 25 ? Collect 算子對 RDD 轉換


(26)collectAsMap

collectAsMap對(K,V)型的RDD數據返回一個單機HashMap。 對于重復K的RDD元素,后面的元素覆蓋前面的元素。

圖26中的左側方框代表RDD分區,右側方框代表單機數組。 數據通過collectAsMap函數返回給Driver程序計算結果,結果以HashMap形式存儲。


          圖26 CollectAsMap算子對RDD轉換


(27)reduceByKeyLocally

  實現的是先reduce再collectAsMap的功能,先對RDD的整體進行reduce操作,然后再收集所有結果返回為一個HashMap。


(28)lookup

下面代碼為lookup的聲明。

lookup(key:K):Seq[V]

Lookup函數對(Key,Value)型的RDD操作,返回指定Key對應的元素形成的Seq。 這個函數處理優化的部分在于,如果這個RDD包含分區器,則只會對應處理K所在的分區,然后返回由(K,V)形成的Seq。 如果RDD不包含分區器,則需要對全RDD元素進行暴力掃描處理,搜索指定K對應的元素。

圖28中的左側方框代表RDD分區,右側方框代表Seq,最后結果返回到Driver所在節點的應用中。

      圖28 ?lookup對RDD轉換


(29)?count

count 返回整個 RDD 的元素個數。

內部函數實現為:

defcount():Long=sc.runJob(this,Utils.getIteratorSize_).sum

圖 29中,返回數據的個數為 5。一個方塊代表一個 RDD 分區。

    ?圖29 count 對 RDD 算子轉換


(30)top

top可返回最大的k個元素。 函數定義如下。

top(num:Int)(implicit ord:Ordering[T]):Array[T]

相近函數說明如下。

·top返回最大的k個元素。

·take返回最小的k個元素。

·takeOrdered返回最小的k個元素,并且在返回的數組中保持元素的順序。

·first相當于top(1)返回整個RDD中的前k個元素,可以定義排序的方式Ordering[T]。

返回的是一個含前k個元素的數組。


(31)reduce

reduce函數相當于對RDD中的元素進行reduceLeft函數的操作。 函數實現如下。

Some(iter.reduceLeft(cleanF))

reduceLeft先對兩個元素進行reduce函數操作,然后將結果和迭代器取出的下一個元素進行reduce函數操作,直到迭代器遍歷完所有元素,得到最后結果。在RDD中,先對每個分區中的所有元素的集合分別進行reduceLeft。 每個分區形成的結果相當于一個元素,再對這個結果集合進行reduceleft操作。

例如:用戶自定義函數如下。

f:(A,B)=>(A._1+”@”+B._1,A._2+B._2)

圖31中的方框代表一個RDD分區,通過用戶自定函數f將數據進行reduce運算。 示例

最后的返回結果為V1@[1]V2U!@U2@U3@U4,12。


圖31 reduce算子對RDD轉換


(32)fold

fold和reduce的原理相同,但是與reduce不同,相當于每個reduce時,迭代器取的第一個元素是zeroValue。

圖32中通過下面的用戶自定義函數進行fold運算,圖中的一個方框代表一個RDD分區。 讀者可以參照reduce函數理解。

fold((”V0@”,2))( (A,B)=>(A._1+”@”+B._1,A._2+B._2))

          圖32 ?fold算子對RDD轉換


(33)aggregate

aggregate先對每個分區的所有元素進行aggregate操作,再對分區的結果進行fold操作。

aggreagate與fold和reduce的不同之處在于,aggregate相當于采用歸并的方式進行數據聚集,這種聚集是并行化的。 而在fold和reduce函數的運算過程中,每個分區中需要進行串行處理,每個分區串行計算完結果,結果再按之前的方式進行聚集,并返回最終聚集結果。

函數的定義如下。

aggregate[B](z: B)(seqop: (B,A) => B,combop: (B,B) => B): B

圖33通過用戶自定義函數對RDD 進行aggregate的聚集操作,圖中的每個方框代表一個RDD分區。

rdd.aggregate(”V0@”,2)((A,B)=>(A._1+”@”+B._1,A._2+B._2)),(A,B)=>(A._1+”@”+B_1,A._@+B_.2))

最后,介紹兩個計算模型中的兩個特殊變量。

廣播(broadcast)變量:其廣泛用于廣播Map Side Join中的小表,以及廣播大變量等場景。 這些數據集合在單節點內存能夠容納,不需要像RDD那樣在節點之間打散存儲。

Spark運行時把廣播變量數據發到各個節點,并保存下來,后續計算可以復用。 相比Hadoo的distributed cache,廣播的內容可以跨作業共享。 Broadcast的底層實現采用了BT機制。

圖33 ?aggregate算子對RDD轉換

②代表V。

③代表U。

accumulator變量:允許做全局累加操作,如accumulator變量廣泛使用在應用中記錄當前的運行指標的情景。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容