PHP cURL库函数抓取页面内容

原文地址:PHPcURL库函数抓取页面内容(转)作者:巴克

cURL是一个利用URL语法规定来传输文件和数据的工具,支持很多协议和选项,如HTTP、FTP、TELNET等,能提供URL 请求相关的各种细节信息。最爽的是,PHP 也支持cURL 库

本文将介绍 cURL 的一些高级特性,以及在 PHP 中如何运用它。

1 为什么要用cURL?

是的,我们可以通过其他办法获取网页内容。大多数时候,我因为想偷懒,都直接用简单的 PHP的file_get_contents()函数:

$content = file_get_contents("http://www.awaimai.com");$lines = file("http://www.awaimai.com");readfile(http://www.awaimai.com);

不过,这种做法缺乏灵活性和有效的错误处理。而且,你也不能用它完成一些高难度任务,比如处理:coockies、验证、表单提交、文件上传等等。

2 启用cURL

首先,我们得先要确定 PHP 是否开启了这个库,你可以通过使用phpinfo()函数来得到这一信息。如果在网页上看到下面的输出,那么表示 cURL 库已开启。

如果 curl没有开启,那么就需要开启这个库。如果是在Windows平台下,那么非常简单,你需要改一改php.ini文件的设置,找到php_curl.dll,并取消前面的分号注释就行了。如下所示:

# 取消下面的注释extension=php_curl.dll

如果是 Linux 服务器,需要重新编译 PHP ,编译时在configure命令上加上--with-curl参数。

3 基本结构

在学习更为复杂的功能之前,先来看一下在 PHP 中建立 cURL 请求的基本步骤:

初始化

设置选项

执行并获取结果

释放cURL句柄

实现代码如下:

// 1. 初始化$ch = curl_init();// 2. 设置选项curl_setopt($ch, CURLOPT_URL,"http://www.awaimai.com");// 设置要抓取的页面地址curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);// 抓取结果直接返回(如果为0,则直接输出内容到页面)curl_setopt($ch, CURLOPT_HEADER,0);// 不需要页面的HTTP头// 3. 执行并获取HTML文档内容,可用echo输出内容$output = curl_exec($ch);// 4. 释放curl句柄curl_close($ch);

第二步(也就是curl_setopt())最为重要,一切玄妙均在此。有一长串cURL参数可供设置,它们能指定 URL请求的各个细节。要一次性全部看完并理解可能比较困难,所以今天我们只试一下那些更常用也更有用的选项。

4 检查错误

你可以在cur_exec()后加一段检查错误的语句(虽然这并不是必需的):

$output = curl_exec($ch);if($output ===FALSE) {echo"cURL Error: ". curl_error($ch);}

请注意,比较的时候我们用的是=== FALSE,而非==FALSE。因为我们得区分空输出和布尔值FALSE,后者才是真正的错误。

5 获取信息

利用curl_getinfo()能够在 cURL执行后获取请求的有关信息,当然,这也是一个可选的设置项,:

curl_exec($ch);$info= curl_getinfo($ch);echo'获取'.$info['url'] .'耗时'.$info['total_time'] .'秒';

返回的数组中包括了以下信息:

url//资源网络地址

content_type//内容编码

http_code//HTTP状态码

header_size//header的大小

request_size//请求的大小

filetime//文件创建时间

ssl_verify_result//SSL验证结果

redirect_count//跳转技术

total_time//总耗时

namelookup_time//DNS查询耗时

connect_time//等待连接耗时

pretransfer_time//传输前准备耗时

size_upload//上传数据的大小

size_download//下载数据的大小

speed_download//下载速度

speed_upload//上传速度

download_content_length//下载内容的长度

upload_content_length//上传内容的长度

starttransfer_time//开始传输的时间

redirect_time//重定向耗时

6 基于浏览器的重定向

在第一个例子中,我们将提供一段用于侦测服务器是否有基于浏览器的重定向的代码。例如,有些网站会根据是否是手机浏览器甚至用户来自哪个国家来重定向网页。

我们利用CURLOPT_HTTPHEADER选项来设定发送出的HTTP请求头信息(http headers),包括useragent信息和默认语言。然后看看这些特定网站是否会把我们重定向到不同的URL。

// 测试用的URL$urls =array("http://www.bbc.com","http://www.baidu.com","http://www.ubuntu.com");// 测试用的浏览器信息$browsers =array("standard"=>array("user_agent"=>"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 (.NET CLR 3.5.30729)","language"=>"en-us,en;q=0.5"),"iphone"=>array("user_agent"=>"Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A537a Safari/419.3","language"=>"en"),"french"=>array("user_agent"=>"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; GTB6; .NET CLR 2.0.50727)","language"=>"fr,fr-FR;q=0.5"));foreach($urlsas$url) {echo"URL: $urln

";foreach($browsersas$test_name => $browser) {                $ch = curl_init();// 设置 urlcurl_setopt($ch, CURLOPT_URL, $url);// 设置浏览器的特定headercurl_setopt($ch, CURLOPT_HTTPHEADER,array("User-Agent: {$browser['user_agent']}","Accept-Language: {$browser['language']}"));// 页面内容我们并不需要curl_setopt($ch, CURLOPT_NOBODY,1);// 只需返回HTTP headercurl_setopt($ch, CURLOPT_HEADER,1);// 返回结果,而不是输出它curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);                $output = curl_exec($ch);                curl_close($ch);// 有重定向的HTTP头信息吗?if(preg_match("!Location: (.*)!", $output, $matches)) {echo"$test_name: redirects to $matches[1]n

";                }else{echo"$test_name: no redirectionn

";                }        }echo"nn

";}

首先,我们建立一组需要测试的URL,接着指定一组需要测试的浏览器信息。最后通过循环测试各种URL和浏览器匹配可能产生的情况。

因为我们指定了CURLOPT_NOBODY选项,所以返回的输出内容则只包括HTTP头信息(被存放于$output中)。利用一个简单的正则,我们检查这个头信息中是否包含了Location:字样。

运行这段代码应该会返回如下结果:

URL: http://www.bbc.comstandard: no redirectioniphone: no redirectionfrench: no redirectionURL: http://www.baidu.comstandard: redirects to https://www.baidu.com/iphone: no redirectionfrench: redirects to https://www.baidu.com/URL: http://www.ubuntu.comstandard: redirects to http://www.ubuntu.com/index_kyliniphone: redirects to http://www.ubuntu.com/index_kylinfrench: redirects to http://www.ubuntu.com/index_kylin

7

用POST方法发送数据

当发起 GET 请求时,数据可以通过“查询字串”(Query String)传递给一个URL。例如,在必应(鉴于

Google 需要翻墙,用 Bing 代替)中搜索时,搜索关键即为 URL 的查询字串的一部分:

http://www.bing.com?q=awaimai.com

这种情况下你可能并不需要 cURL 来模拟。把这个URL丢给file_get_contents()就能得到相同结果。

不过有一些HTML表单是用 POST 方法提交的。这种表单提交时,数据是通过 HTTP请求体(requestbody) 发送,而不是查询字串。例如,当使用ThinkPHP网站的搜索功能时,无论输入什么关键字,总是被 POST到如下页面:

http://www.thinkphp.cn/Search/

你可以用 PHP 脚本来模拟这种 URL 请求。首先,新建一个可以接受并显示 POST数据的文件,我们给它命名为post_output.php,脚本内容为:

print_r($_POST);

接下来,写一段 PHP 脚本来执行 cURL 请求:

$url ="http://localhost/post_output.php";$post_data =array("foo"=>"bar","query"=>"Nettuts","action"=>"Submit");$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);// 我们在POST数据哦!curl_setopt($ch, CURLOPT_POST,1);// 加上POST变量curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);$output = curl_exec($ch);curl_close($ch);echo$output;

执行代码后应该会得到以下结果:

Array(    [foo] => bar    [query] => Nettuts    [action] => Submit)

这段脚本发送一个 POST 请求给post_output.php,这个页面$_POST变量并返回,我们利用cURL 捕捉了这个输出。

8 文件上传

上传文件和前面的 POST十分相似。因为所有的文件上传表单都是通过POST方法提交的。首先新建一个接收文件的页面,命名为upload_output.php,页面内容:

print_r($_FILES);

以下是真正执行文件上传任务的脚本,命名为upload.php,内容:

$url ="http://localhost/upload_output.php";$post_data =array("foo"=>"bar",// 要上传的本地文件地址"upload"=>"@C:/wamp/www/test.zip");$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);curl_setopt($ch, CURLOPT_POST,1);curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);$output = curl_exec($ch);curl_close($ch);echo$output;

如果你需要上传一个文件,只需要把文件路径赋给upload,作为POST变量传过去,不过记得在前面加上@符号。执行这段脚本应该会得到类似如下输出:

Array(    [upload] =>Array([name] => test.zip[type] => application/octet-stream[tmp_name] => C:Windowsphp1BB4.tmp[error] =>0[size] =>487235))

9 cURL批处理(multi

cURL)

cURL还有一个高级特性:批处理句柄(handle)。这一特性允许你同时或异步地打开多个URL连接。下面是来自来自php.net的示例代码

// 创建两个cURL资源$ch1 = curl_init();$ch2 = curl_init();// 指定URL和适当的参数curl_setopt($ch1, CURLOPT_URL,"http://lxr.php.net/");curl_setopt($ch1, CURLOPT_HEADER,0);curl_setopt($ch2, CURLOPT_URL,"http://www.php.net/");curl_setopt($ch2, CURLOPT_HEADER,0);// 创建cURL批处理句柄$mh = curl_multi_init();// 加上前面两个资源句柄curl_multi_add_handle($mh, $ch1);curl_multi_add_handle($mh, $ch2);// 预定义一个状态变量$active =null;// 执行批处理do{    $mrc = curl_multi_exec($mh, $active);}while($mrc == CURLM_CALL_MULTI_PERFORM);while($active && $mrc == CURLM_OK) {if(curl_multi_select($mh) !=-1) {do{            $mrc = curl_multi_exec($mh, $active);        }while($mrc == CURLM_CALL_MULTI_PERFORM);    }}// 关闭各个句柄curl_multi_remove_handle($mh, $ch1);curl_multi_remove_handle($mh, $ch2);curl_multi_close($mh);

这里要做的就是打开多个 cURL 句柄并指派给一个批处理句柄。然后你就只需在一个while循环里等它执行完毕。

这个示例中有两个主要循环。第一个do-while循环重复调用curl_multi_exec()。这个函数是无隔断(non-blocking)的,但会尽可能少地执行。它返回一个状态值,只要这个值等于常量CURLM_CALL_MULTI_PERFORM,就代表还有一些刻不容缓的工作要做(例如,把对应URL的http头信息发送出去)。也就是说,我们需要不断调用该函数,直到返回值发生改变。

而接下来的while循环,只在$active变量为true时继续。这一变量之前作为第二个参数传给了curl_multi_exec(),代表只要批处理句柄中是否还有活动连接。接着,我们调用curl_multi_select(),在活动连接(例如接受服务器响应)出现之前,它都是被“屏蔽”的。这个函数成功执行后,我们又会进入另一个do-while循环,继续下一条URL。

还是来看一看怎么把这一功能用到实处吧:

9.1 WordPress 链接检查器

想象一下你有一个文章数目庞大的博客,这些文章中包含了大量外部网站链接。一段时间之后,因为这样那样的原因,这些链接中相当数量都失效了。要么是被和谐了,要么是整个站点都被功夫网了…

我们下面建立一个脚本,分析所有这些链接,找出打不开或者404的网站/网页,并生成一个报告。

请注意,以下并不是一个真正可用的WordPress插件,仅仅是一段独立功能的脚本而已,仅供演示,谢谢。

好,开始吧。首先,从数据库中读取所有这些链接:

// 配置 MySQL 数据库$db_host ='localhost';$db_user ='root';$db_pass ='';$db_name ='wordpress';$excluded_domains =array('localhost','www.mydomain.com');$max_connections =10;// 初始化一些变量$url_list =array();$working_urls =array();$dead_urls =array();$not_found_urls =array();$active =null;// 连到 MySQLif(!mysql_connect($db_host, $db_user, $db_pass)) {die('Could not connect: '. mysql_error());}if(!mysql_select_db($db_name)) {die('Could not select db: '. mysql_error());}// 找出所有含有链接的文章$sql ="SELECT post_content FROM wp_posts

WHERE post_content LIKE '%href=%'

AND post_status = 'publish'

AND post_type = 'post'";$res = mysql_query($sql)ordie(mysql_error());while($d = mysql_fetch_assoc($res)) {// 用正则匹配链接if(preg_match_all("!href="(.*?)"!", $d['post_content'], $matches)) {foreach($matches[1]as$url) {// 剔除排除的域名$tmp = parse_url($url);if(in_array($tmp['host'], $excluded_domains)) {continue;                        }// 保存 URL$url_list []= $url;                }        }}// 移除重复链接$url_list = array_values(array_unique($url_list));if(!$url_list) {die('No URL to check');}

我们首先配置好数据库,一系列要排除的域名($excluded_domains),以及最大同时连接数量($max_connections)。然后,连接数据库,获取文章和包含的链接,把它们收集到一个数组中($url_list)。

下面的代码有点复杂了,因此我将一小步一小步地详细解释:

// 1. 批处理器$mh = curl_multi_init();// 2. 加入需批量处理的URLfor($i =0; $i < $max_connections; $i++) {        add_url_to_multi_handle($mh, $url_list);}// 3. 初始处理do{        $mrc = curl_multi_exec($mh, $active);}while($mrc == CURLM_CALL_MULTI_PERFORM);// 4. 主循环while($active && $mrc == CURLM_OK) {// 5. 有活动连接if(curl_multi_select($mh) !=-1) {// 6. 干活do{                        $mrc = curl_multi_exec($mh, $active);                }while($mrc == CURLM_CALL_MULTI_PERFORM);// 7. 有信息否?if($mhinfo = curl_multi_info_read($mh)) {// 意味着该连接正常结束// 8. 从curl句柄获取信息$chinfo = curl_getinfo($mhinfo['handle']);// 9. 死链么?if(!$chinfo['http_code']) {                                $dead_urls []= $chinfo['url'];// 10. 404了?}elseif($chinfo['http_code'] ==404) {                                $not_found_urls []= $chinfo['url'];// 11. 还能用}else{                                $working_urls []= $chinfo['url'];                        }// 12. 移除句柄curl_multi_remove_handle($mh, $mhinfo['handle']);                        curl_close($mhinfo['handle']);// 13. 加入新URL,干活if(add_url_to_multi_handle($mh, $url_list)) {do{                                        $mrc = curl_multi_exec($mh, $active);                                }while($mrc == CURLM_CALL_MULTI_PERFORM);                        }                }        }}// 14. 完了curl_multi_close($mh);echo"==Dead URLs==n";echoimplode("n",$dead_urls) ."nn";echo"==404 URLs==n";echoimplode("n",$not_found_urls) ."nn";echo"==Working URLs==n";echoimplode("n",$working_urls);// 15. 向批处理器添加urlfunctionadd_url_to_multi_handle($mh, $url_list){static$index =0;// 如果还剩url没用if($url_list[$index]) {// 新建curl句柄$ch = curl_init();// 配置urlcurl_setopt($ch, CURLOPT_URL, $url_list[$index]);// 不想输出返回的内容curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);// 重定向到哪儿我们就去哪儿curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);// 不需要内容体,能够节约带宽和时间curl_setopt($ch, CURLOPT_NOBODY,1);// 加入到批处理器中curl_multi_add_handle($mh, $ch);// 拨一下计数器,下次调用该函数就能添加下一个url了$index++;returntrue;        }else{// 没有新的URL需要处理了returnfalse;        }}

下面解释一下以上代码。列表的序号对应着代码注释中的顺序数字。

新建一个批处理句柄。

稍后我们将创建一个把URL加入批处理器的函数add_url_to_multi_handle()。每当这个函数被调用,就有一个新url被加入批处理器。一开始,我们给批处理器添加了10个URL(这一数字由$max_connections所决定)。

运行curl_multi_exec()进行初始化工作是必须的,只要它返回CURLM_CALL_MULTI_PERFORM就还有事情要做。这么做主要是为了创建连接,它不会等待完整的URL响应。

只要批处理中还有活动连接主循环就会一直持续。

curl_multi_select()会一直等待,直到某个URL查询产生活动连接。

cURL的活儿又来了,主要是获取响应数据。

检查各种信息。当一个URL请求完成时,会返回一个数组。

在返回的数组中有一个 cURL 句柄。我们利用其获取单个 cURL 请求的相应信息。

如果这是一个死链或者请求超时,不会返回http状态码。

如果这个页面找不到了,会返回404状态码。

其他情况我们都认为这个链接是可用的(当然,你也可以再检查一下500错误之类…)。

从该批次移除这个 cURL 句柄,因为它已经没有利用价值了,关了它!

很好,现在可以另外加一个URL进来了。再一次地,初始化工作又开始进行…

嗯,该干的都干了。关闭批处理器,生成报告。

回过头来看给批处理器添加新 URL 的函数。这个函数每调用一次,静态变量$index就递增一次,这样我们才能知道还剩多少URL 没处理。

我把这个脚本在我的博客上跑了一遍(测试需要,有一些错误链接是故意加上的),共检查约40个URL,只耗费两秒不到。当需要检查更加大量的URL时,其省心省力的效果可想而知!如果你同时打开10个连接,还能再快上10倍!另外,你还可以利用cURL批处理的无隔断特性,来处理大量URL请求,而不会阻塞你的Web脚本。

10 另一些有用的cURL

选项

10.1 HTTP 认证

如果某个URL请求需要基于 HTTP 的身份验证,你可以使用下面的代码:

$url ="http://www.somesite.com/members/";$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);// 发送用户名和密码curl_setopt($ch, CURLOPT_USERPWD,"myusername:mypassword");// 你可以允许其重定向curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);// 下面的选项让 cURL 在重定向后// 也能发送用户名和密码curl_setopt($ch, CURLOPT_UNRESTRICTED_AUTH,1);$output = curl_exec($ch);curl_close($ch);

10.2 FTP 上传

PHP 自带有FTP 类库, 但你也能用 cURL:

// 打开一个文件指针$file = fopen("/path/to/file","r");$size = filesize("/path/to/file");// url里包含了大部分所需信息$url ="ftp://username:password@mydomain.com:21/path/to/new/file";$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);// 上传相关的选项curl_setopt($ch, CURLOPT_UPLOAD,1);curl_setopt($ch, CURLOPT_INFILE, $file);curl_setopt($ch, CURLOPT_INFILESIZE, $size);// 是否开启ASCII模式 (上传文本文件时有用)curl_setopt($ch, CURLOPT_FTPASCII,1);$output = curl_exec($ch);curl_close($ch);

10.3 代理/翻墙请求

你可以用代理发起 cURL 请求:

$ch = curl_init();curl_setopt($ch, CURLOPT_URL,'http://www.example.com');curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);// 指定代理地址curl_setopt($ch, CURLOPT_PROXY,'11.11.11.11:8080');// 如果需要的话,提供用户名和密码curl_setopt($ch, CURLOPT_PROXYUSERPWD,'username:password');$output = curl_exec($ch);curl_close ($ch);

10.4 回调函数

可以在一个URL请求过程中,让 cURL

调用某指定的回调函数。例如,在内容或者响应下载的过程中,立刻开始利用数据,而不用等到完全下载完。

$ch = curl_init();curl_setopt($ch, CURLOPT_URL,'http://net.tutsplus.com');curl_setopt($ch, CURLOPT_WRITEFUNCTION,"progress_function");curl_exec($ch);curl_close ($ch);functionprogress_function($ch, $str){echo$str;returnstrlen($str);}

这个回调函数必须返回字串的长度,不然此功能将无法正常使用。在URL响应接收的过程中,只要收到一个数据包,这个函数就会被调用。

更多信息请关注千锋PHP,千锋论坛

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

推荐阅读更多精彩内容