《Learning Scrapy》(中文版)第5章 快速構建爬蟲


序言
第1章 Scrapy介紹
第2章 理解HTML和XPath
第3章 爬蟲基礎
第4章 從Scrapy到移動應用
第5章 快速構建爬蟲
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy編程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和實時分析


第3章中,我們學習了如何從網頁提取信息并存儲到Items中。大多數情況都可以用這一章的知識處理。本章,我們要進一步學習抓取流程UR2IM中兩個R,Request和Response。

一個具有登錄功能的爬蟲

你常常需要從具有登錄機制的網站抓取數據。多數時候,網站要你提供用戶名和密碼才能登錄。我們的例子,你可以在http://web:9312/dynamichttp://localhost:9312/dynamic找到。用用戶名“user”、密碼“pass”登錄之后,你會進入一個有三條房產鏈接的網頁。現在的問題是,如何用Scrapy登錄?

讓我們使用谷歌Chrome瀏覽器的開發者工具搞清楚登錄的機制。首先,選擇Network標簽(1)。然后,填入用戶名和密碼,點擊Login(2)。如果用戶名和密碼是正確的,你會進入下一頁。如果是錯誤的,會看到一個錯誤頁。

一旦你點擊了Login,在開發者工具的Network標簽欄中,你就會看到一個發往http://localhost:9312/dynamic/login的請求Request Method: POST。

提示:上一章的GET請求,通常用來獲取靜止數據,例如簡單的網頁和圖片。POST請求通常用來獲取的數據,取決于我們發給服務器的數據,例如這個例子中的用戶名和密碼。

點擊這個POST請求,你就可以看到發給服務器的數據,其中包括表單信息,表單信息中有你剛才輸入的用戶名和密碼。所有數據都以文本的形式發給服務器。Chrome開發者工具將它們整理好并展示出來。服務器的響應是302 FOUND(5),然后將我們重定向到新頁面:/dynamic/gated。只有登錄成功時才會出現此頁面。如果沒有正確輸入用戶名和密碼就前往http://localhost:9312/dynamic/gated,服務器會發現你作弊,并將你重定向到錯誤頁面:http://localhost:9312/dynamic/error。服務器怎么知道你和密碼呢?如果你點擊左側的gated(6),你會發現在RequestHeaders(7)下有一個Cookie(8)。

提示:HTTP cookie是通常是一些服務器發送到瀏覽器的短文本或數字片段。反過來,在每一個后續請求中,瀏覽器把它發送回服務器,以確定你、用戶和期限。這讓你可以執行復雜的需要服務器端狀態信息的操作,如你購物車中的商品或你的用戶名和密碼。

總結一下,單單一個操作,如登錄,可能涉及多個服務器往返操作,包括POST請求和HTTP重定向。Scrapy處理大多數這些操作是自動的,我們需要編寫的代碼很簡單。
我們將第3章名為easy的爬蟲重命名為login,并修改里面名字的屬性,如下:

class LoginSpider(CrawlSpider):
    name = 'login'

提示:本章的代碼github的ch05目錄中。這個例子位于ch05/properties。

我們要在http://localhost:9312/dynamic/login上面模擬一個POST請求登錄。我們用Scrapy中的類FormRequest來做。這個類和第3章中的Request很像,但有一個額外的formdata,用來傳遞參數。要使用這個類,首先必須要引入:

from scrapy.http import FormRequest

我們然后將start_URL替換為start_requests()方法。這么做是因為在本例中,比起URL,我們要做一些自定義的工作。更具體地,用下面的函數,我們創建并返回一個FormRequest:

# Start with a login request
def start_requests(self):
  return [
    FormRequest(
      "http://web:9312/dynamic/login",
      formdata={"user": "user", "pass": "pass"}
         )]

就是這樣。CrawlSpider的默認parse()方法,即LoginSpider的基本類,負責處理響應,并如第3章中使用Rules和LinkExtractors。其余的代碼很少,因為Scrapy負責了cookies,當我們登錄時,Scrapy將cookies傳遞給后續請求,與瀏覽器的方式相同。還是用scrapy crawl運行:

$ scrapy crawl login 
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Redirecting (302) to <GET .../gated> from <POST .../login >
DEBUG: Crawled (200) <GET .../data.php>
DEBUG: Crawled (200) <GET .../property_000001.html> (referer: .../data.
php)
DEBUG: Scraped from <200 .../property_000001.html>
  {'address': [u'Plaistow, London'],
   'date': [datetime.datetime(2015, 11, 25, 12, 7, 27, 120119)],
   'description': [u'features'],
   'image_URL': [u'http://web:9312/images/i02.jpg'],
...
INFO: Closing spider (finished)
INFO: Dumping Scrapy stats:
  {...
   'downloader/request_method_count/GET': 4,
   'downloader/request_method_count/POST': 1,
...
   'item_scraped_count': 3,

我們注意到登錄跳轉從dynamic/login到dynamic/gated,然后就可以像之前一樣抓取項目。在統計中,我們看到一個POST請求和四個GET請求;一個是dynamic/gated首頁,三個是房產網頁。

提示:在本例中,我們不保護房產頁,而是是這些網頁的鏈接。代碼在相反的情況下也是相同的。

如果我們使用了錯誤的用戶名和密碼,我們將重定向到一個沒有URL的頁面,進程并將在這里結束,如下所示:

$ scrapy crawl login
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Redirecting (302) to <GET .../dynamic/error > from <POST .../
dynamic/login>
DEBUG: Crawled (200) <GET .../dynamic/error>
...
INFO: Spider closed (closespider_itemcount)

這是一個簡單的登錄示例,演示了基本的登錄機制。大多數網站可能有更復雜的機制,但Scrapy也處理的很好。例如一些網站在執行POST請求時,需要通過從表單頁面到登錄頁面傳遞某種形式的變量以確定cookies的啟用,讓你使用大量用戶名和密碼暴力破解時變得困難。

例如,如果你訪問http://localhost:9312/dynamic/nonce,你會看到一個和之前一樣的網頁,但如果你使用Chrome開發者工具,你會發現這個頁面的表單有一個叫做nonce的隱藏字段。當你提交表單http://localhost:9312/dynamic/nonce-login時,你必須既要提供正確的用戶名密碼,還要提交正確的瀏覽器發給你的nonce值。因為這個值是隨機且只能使用一次,你很難猜到。這意味著,如果要成功登陸,必須要進行兩次請求。你必須訪問表單、登錄頁,然后傳遞數值。和以前一樣,Scrapy有內建的功能可以解決這個問題。

我們創建一個和之前相似的NonceLoginSpider爬蟲。現在,在start_requests()中,我們要向表單頁返回一個簡單的Request,并通過設定callback為名字是parse_welcome()的方法手動處理響應。在parse_welcome()中,我們使用FormRequest對象中的from_response()方法創建FormRequest,并將原始表單中的字段和值導入FormRequest。FormRequest.from_response()可以模擬提交表單。

提示:花時間看from_response()的文檔是十分值得的。他有許多有用的功能如formname和formnumber,它可以幫助你當頁面有多個表單時,選擇特定的表單。

它最大的功能是,一字不差地包含了表單中所有的隱藏字段。我們只需使用formdata參數,填入user和pass字段,并返回FormRequest。代碼如下:

# Start on the welcome page
def start_requests(self):
    return [
        Request(
            "http://web:9312/dynamic/nonce",
            callback=self.parse_welcome)
    ]
# Post welcome page's first form with the given user/pass
def parse_welcome(self, response):
    return FormRequest.from_response(
        response,
        formdata={"user": "user", "pass": "pass"}
    )

像之前一樣運行爬蟲:

$ scrapy crawl noncelogin 
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Crawled (200) <GET .../dynamic/nonce>
DEBUG: Redirecting (302) to <GET .../dynamic/gated > from <POST .../
dynamic/login-nonce>
DEBUG: Crawled (200) <GET .../dynamic/gated>
...
INFO: Dumping Scrapy stats:
  {...
   'downloader/request_method_count/GET': 5,
   'downloader/request_method_count/POST': 1,
...
   'item_scraped_count': 3,

我們看到第一個GET請求先到/dynamic/nonce,然后POST,重定向到/dynamic/nonce-login之后,之后像之前一樣,訪問了/dynamic/gated。登錄過程結束。這個例子的登錄含有兩步。只要有足夠的耐心,無論多少步的登錄過程,都可以完成。

使用JSON APIs和AJAX頁面的爬蟲

有時,你會發現網頁的HTML找不到數據。例如,在http://localhost:9312/static/頁面上右鍵點擊檢查元素(1,2),你就可以在DOM樹種看到所有HTML元素。或者,如果你使用scrapy shell或在Chrome中右鍵點擊查看網頁源代碼(3,4),你會看到這個網頁的HTML代碼不包含任何和值有關的信息。數據都是從何而來呢?

和以前一樣,在開發者工具中打開Network標簽(5)查看發生了什么。左側列表中,可以看到所有的請求。在這個簡單的頁面中,只有三個請求:static/我們已經檢查過了,jquery.min.js是一個流行的JavaScript框架,api.json看起來不同。如果我們點擊它(6),然后在右側點擊Preview標簽(7),我們可以看到它包含我們要找的信息。事實上,http://localhost:9312/properties/api.json包含IDs和名字(8),如下所示:

[{
    "id": 0,
    "title": "better set unique family well"
}, 
... {
    "id": 29,
    "title": "better portered mile"
}]

這是一個很簡單的JSON API例子。更復雜的APIs可能要求你登錄,使用POST請求,或返回某種數據結結構。任何時候,JSON都是最容易解析的格式,因為不需要XPath表達式就可以提取信息。

Python提供了一個強大的JSON解析庫。當我們import json時,我們可以使用json.loads(response.body)解析JSON,并轉換成等價的Python對象,語句、列表和字典。

復制第3章中的manual.py文件。這是最好的方法,因為我們要根據JSON對象中的IDs手動創建URL和Request。將這個文件重命名為api.py,重命名類為ApiSpider、名字是api。新的start_URL變成:

start_URL = (
    'http://web:9312/properties/api.json',
)

如果你要做POST請求或更復雜的操作,你可以使用start_requests()方法和前面幾章介紹的方法。這里,Scrapy會打開這個URL并使用Response作為參數調用parse()方法。我們可以import json,使用下面的代碼解析JSON:

def parse(self, response):
    base_url = "http://web:9312/properties/"
    js = json.loads(response.body)
    for item in js:
        id = item["id"]
        url = base_url + "property_%06d.html" % id
        yield Request(url, callback=self.parse_item)

這段代碼使用了json.loads(response.body)將響應JSON對象轉換為Python列表,然后重復這個過程。對于列表中的每個項,我們設置一個URL,它包含:base_url,property_%06d和.html.base_url,.html.base_url前面定義過的URL前綴。%06d是一個非常有用的Python詞,可以讓我們結合多個Python變量形成一個新的字符串。在本例中,用id變量替換%06d。id被當做數字(%d的意思就是當做數字進行處理),并擴展成6個字符,位數不夠時前面添加0。如果id的值是5,%06d會被替換為000005;id是34322時,%06d會被替換為034322替換。最后的結果是可用的URL。和第3章中的yield一樣,我們用URL做一個新的Request請求。運行爬蟲:

$ scrapy crawl api
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Crawled (200) <GET ...properties/api.json>
DEBUG: Crawled (200) <GET .../property_000029.html>
...
INFO: Closing spider (finished)
INFO: Dumping Scrapy stats:
...
   'downloader/request_count': 31, ...
   'item_scraped_count': 30,

最后一共有31次請求,每個項目一次,api.json一次。

在響應間傳遞參數

許多時候,你想把JSON APIs中的信息存儲到Item中。為了演示,在我們的例子中,對于一個項,JSON API在返回它的名字時,在前面加上“better”。例如,如果一個項的名字時“Covent Garden”,API會返回“Better Covent Garden”。我們要在Items中保存這些含有“bette”的名字。如何將數據從parse()傳遞到parse_item()中呢?

我們要做的就是在parse()方法產生的Request中進行設置。然后,我們可以從parse_item()的的Response中取回。Request有一個名為meta的字典,在Response中可以直接訪問。對于我們的例子,給字典設一個title值以存儲從JSON對象的返回值:

title = item["title"]
yield Request(url, meta={"title": title},callback=self.parse_item)

在parse_item()中,我們可以使用這個值,而不用XPath表達式:

l.add_value('title', response.meta['title'],
      MapCompose(unicode.strip, unicode.title))

你會注意到,我們從調用add_xpath()切換到add_value(),因為對于這個字段不需要使用XPath。我們現在運行爬蟲,就可以在PropertyItems中看到api.json中的標題了。

一個加速30倍的項目爬蟲

當你學習使用一個框架時,這個框架越復雜,你用它做任何事都會很復雜。可能你覺得Scrapy也是這樣。當你就要為XPath和其他方法變得抓狂時,不妨停下來思考一下:我現在抓取網頁的方法是最簡單的嗎?

如果你可以從索引頁中提取相同的信息,就可以避免抓取每一個列表頁,這樣就可以節省大量的工作。

提示:許多網站的索引頁提供的項目數量是不同的。例如,一個網站可以通過調整一個參數,例如&show=50,給每個索引頁面設置10、 50或100個列表項。如果是這樣的話,將其設置為可用的最大值。

例如,對于我們的例子,我們需要的所有信息都存在于索引頁中,包括標題、描述、價格和圖片。這意味著我們抓取單個索引頁,提取30個條目和下一個索引頁的鏈接。通過抓取100個索引頁,我們得到3000個項,但只有100個請求而不是3000個。

在真實的Gumtree網站上,索引頁的描述比列表頁的完整描述要短。這是可行的,或者是更推薦的。

提示:許多情況下,您不得不在數據質量與請求數量間進行折衷。很多網站都限制請求數量(后面章節詳解),所以減少請求可能解決另一個棘手的問題。

在我們的例子中,如果我們查看一個索引頁的HTML,我們會發現,每個列表頁有自己的節點,itemtype="http://schema.org/Product"。節點有每個項的全部信息,如下所示:

讓我們在Scrapy shell中加載索引首頁,并用XPath處理:

$ scrapy shell http://web:9312/properties/index_00000.html
While within the Scrapy shell, let's try to select everything with the Product tag:
>>> p=response.xpath('//*[@itemtype="http://schema.org/Product"]')
>>> len(p)
30
>>> p
[<Selector xpath='//*[@itemtype="http://schema.org/Product"]' data=u'<li 
class="listing-maxi" itemscopeitemt'...]

我們得到了一個包含30個Selector對象的表,每個都指向一個列表。Selector對象和Response對象很像,我們可以用XPath表達式從它們指向的對象中提取信息。不同的是,表達式為有相關性的XPath表達式。相關性XPath表達式與我們之前見過的很像,不同之處是它們前面有一個點“.”。然我們看看如何用.//*[@itemprop="name"][1]/text()提取標題的:

>>> selector = p[3]
>>> selector
<Selector xpath='//*[@itemtype="http://schema.org/Product"]' ... '>
>>> selector.xpath('.//*[@itemprop="name"][1]/text()').extract()
[u'l fun broadband clean people brompton european']

我們可以在Selector對象表中用for循環提取一個索引頁的所有30個項目信息。還是從第3章中的maunal.py文件開始,重命名為fast.py。重復使用大部分代碼,修改parse()和parse_item()方法。更新的方法如下所示:

def parse(self, response):
    # Get the next index URL and yield Requests
    next_sel = response.xpath('//*[contains(@class,"next")]//@href')
    for url in next_sel.extract():
        yield Request(urlparse.urljoin(response.url, url))
    # Iterate through products and create PropertiesItems
    selectors = response.xpath(
        '//*[@itemtype="http://schema.org/Product"]')
    for selector in selectors:
        yield self.parse_item(selector, response)

第一部分中用于產生下一條索引請求的代碼沒有變動。不同的地方是第二部分,我們重復使用選擇器調用parse_item()方法,而不是用yield創建請求。這和原先使用的源代碼很像:

def parse_item(self, selector, response):
    # Create the loader using the selector
    l = ItemLoader(item=PropertiesItem(), selector=selector)
    # Load fields using XPath expressions
l.add_xpath('title', './/*[@itemprop="name"][1]/text()',
                MapCompose(unicode.strip, unicode.title))
    l.add_xpath('price', './/*[@itemprop="price"][1]/text()',
                MapCompose(lambda i: i.replace(',', ''), float),
                re='[,.0-9]+')
    l.add_xpath('description',
                './/*[@itemprop="description"][1]/text()',
                MapCompose(unicode.strip), Join())
    l.add_xpath('address',
                './/*[@itemtype="http://schema.org/Place"]'
                '[1]/*/text()',
                MapCompose(unicode.strip))
    make_url = lambda i: urlparse.urljoin(response.url, i)
    l.add_xpath('image_URL', './/*[@itemprop="image"][1]/@src',
                MapCompose(make_url))
    # Housekeeping fields
    l.add_xpath('url', './/*[@itemprop="url"][1]/@href',
                MapCompose(make_url))
    l.add_value('project', self.settings.get('BOT_NAME'))
    l.add_value('spider', self.name)
    l.add_value('server', socket.gethostname())
    l.add_value('date', datetime.datetime.now())
    return l.load_item()

我們做出的變動是:

  • ItemLoader現在使用selector作為源,不使用Response。這么做可以讓ItemLoader更便捷,可以讓我們從特定的區域而不是整個頁面抓取信息。
  • 通過在前面添加“.”使XPath表達式變為相關XPath。

提示:碰巧的是,在我們的例子中,XPath表達式在索引頁和介紹頁中是相同的。不同的時候,你需要按照索引頁修改XPath表達式。

  • 在response.url給我們列表頁的URL之前,我們必須自己編輯Item的URL。然后,它才能返回我們抓取網頁的URL。我們必須用.//*[@itemprop="url"][1]/@href提取URL,然后將它用MapCompose轉化為URL絕對路徑。

這些小小大量的工作的改動可以節省大量的工作。現在,用以下命令運行爬蟲:

$ scrapy crawl fast -s CLOSESPIDER_PAGECOUNT=3
...
INFO: Dumping Scrapy stats:
   'downloader/request_count': 3, ...
   'item_scraped_count': 90,...

就像之前說的,我們用三個請求,就抓取了90個項目。不從索引開始的話,就要用93個請求。

如果你想用scrapy parse來調試,你需要如下設置spider參數:

$ scrapy parse --spider=fast http://web:9312/properties/index_00000.html
...
>>> STATUS DEPTH LEVEL 1 <<<
# Scraped Items  --------------------------------------------
[{'address': [u'Angel, London'],
... 30 items...
# Requests  ---------------------------------------------------
[<GET http://web:9312/properties/index_00001.html>]

正如所料,parse()返回了30個Items和下一個索引頁的請求。你還可以繼續試驗scrapy parse,例如,設置—depth=2。

可以抓取Excel文件的爬蟲

大多數時候,你每抓取一個網站就使用一個爬蟲,但如果要從多個網站抓取時,不同之處就是使用不同的XPath表達式。為每一個網站配置一個爬蟲工作太大。能不能只使用一個爬蟲呢?答案是可以。

新建一個項目抓取不同的東西。當前我們是在ch05的properties目錄,向上一級:

$ pwd
/root/book/ch05/properties
$ cd ..
$ pwd
/root/book/ch05

新建一個項目,命名為generic,再創建一個名為fromcsv的爬蟲:

$ scrapy startproject generic
$ cd generic
$ scrapy genspider fromcsv example.com

新建一個.csv文件,它是我們抓取的目標。我們可以用Excel表建這個文件。如下表所示,填入URL和XPath表達式,在爬蟲的目錄中(有scrapy.cfg的文件夾)保存為todo.csv。保存格式是csv:

一切正常的話,就可以在終端看見這個文件:

$ cat todo.csv 
url,name,price
a.html,"http://*[@id=""itemTitle""]/text()","http://*[@id=""prcIsum""]/text()"
b.html,//h1/text(),//span/strong/text()
c.html,"http://*[@id=""product-desc""]/span/text()"

Python中有csv文件的內建庫。只需import csv,就可以用后面的代碼一行一行以dict的形式讀取這個csv文件。在當前目錄打開Python命令行,然后輸入:

$ pwd
/root/book/ch05/generic2
$ python
>>> import csv
>>> with open("todo.csv", "rU") as f:
        reader = csv.DictReader(f)
        for line in reader:
            print line

文件的第一行會被自動作為header,從而導出dict的鍵名。對于下面的每一行,我們得到一個包含數據的dict。用for循環執行每一行。前面代碼的結果如下:

{'url': ' http://a.html', 'price': '//*[@id="prcIsum"]/text()', 'name': '//*[@id="itemTitle"]/text()'}
{'url': ' http://b.html', 'price': '//span/strong/text()', 'name': '//h1/text()'}
{'url': ' http://c.html', 'price': '', 'name': '//*[@id="product-desc"]/span/text()'}

很好。現在編輯generic/spiders/fromcsv.py爬蟲。我們使用.csv文件中的URL,并且不希望遇到域名限制的情況。因此第一件事是移除start_URL和allowed_domains。然后再讀.csv文件。

因為從文件中讀取的URL是我們事先不了解的,所以使用一個start_requests()方法。對于每一行,我們都會創建Request。我們還要從request,meta的csv存儲字段名和XPath,以便在我們的parse()函數中使用。然后,我們使用Item和ItemLoader填充Item的字段。下面是所有代碼:

import csv
import scrapy
from scrapy.http import Request
from scrapy.loader import ItemLoader
from scrapy.item import Item, Field
class FromcsvSpider(scrapy.Spider):
    name = "fromcsv"
def start_requests(self):
    with open("todo.csv", "rU") as f:
        reader = csv.DictReader(f)
        for line in reader:
            request = Request(line.pop('url'))
            request.meta['fields'] = line
            yield request
def parse(self, response):
    item = Item()
    l = ItemLoader(item=item, response=response)
    for name, xpath in response.meta['fields'].iteritems():
        if xpath:
      item.fields[name] = Field()
            l.add_xpath(name, xpath)
    return l.load_item()

運行爬蟲,輸出文件保存為csv:

$ scrapy crawl fromcsv -o out.csv
INFO: Scrapy 0.0.3 started (bot: generic)
...
DEBUG: Scraped from <200 a.html>
{'name': [u'My item'], 'price': [u'128']}
DEBUG: Scraped from <200 b.html>
{'name': [u'Getting interesting'], 'price': [u'300']}
DEBUG: Scraped from <200 c.html>
{'name': [u'Buy this now']}
...
INFO: Spider closed (finished)
$ cat out.csv 
price,name
128,My item
300,Getting interesting
,Buy this now

有幾點要注意。項目中沒有定義一個整個項目的Items,我們必須手動向ItemLoader提供一個:

item = Item()
l = ItemLoader(item=item, response=response)

我們還用Item的fields成員變量添加了動態字段。添加一個新的動態字段,并用ItemLoader填充,使用下面的方法:

item.fields[name] = Field()
l.add_xpath(name, xpath)

最后讓代碼再漂亮些。硬編碼todo.csv不是很好。Scrapy提供了一種便捷的向爬蟲傳遞參數的方法。如果我們使用-a參數,例如,-a variable=value,就創建了一個爬蟲項,可以用self.variable取回。為了檢查變量(沒有的話,提供一個默認變量),我們使用Python的getattr()方法:getattr(self, 'variable', 'default')。總之,原來的with open…替換為:

with open(getattr(self, "file", "todo.csv"), "rU") as f:

現在,todo.csv是默認文件,除非使用參數-a,用一個源文件覆蓋它。如果還有一個文件,another_todo.csv,我們可以運行:

$ scrapy crawl fromcsv -a file=another_todo.csv -o out.csv

總結

在本章中,我們進一步學習了Scrapy爬蟲。我們使用FormRequest進行登錄,用請求/響應中的meta傳遞變量,使用了相關的XPath表達式和Selectors,使用.csv文件作為數據源等等。

接下來在第6章學習在Scrapinghub云部署爬蟲,在第7章學習關于Scrapy的設置。


序言
第1章 Scrapy介紹
第2章 理解HTML和XPath
第3章 爬蟲基礎
第4章 從Scrapy到移動應用
第5章 快速構建爬蟲
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy編程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和實時分析


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

推薦閱讀更多精彩內容