(这是网安实验室IMiss小组第一轮笔试题的第三题)
请编写简单的爬虫程序,爬取豆瓣电影(https://movie.douban.com/)下“正
在热映”的电影名单,以及对应的导演、编剧、主演、剧情等信息,爬取
结果以数据库方式存储。
PHP+MySQL,利用正则表达式匹配相关信息并存储到数据库中。
首先建表
DROP TABLE IF EXISTS `movie`;
CREATE TABLE `movie` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`moviename` varchar(60) NOT NULL,
`director` varchar(100),
`playwright` varchar(100),
`actor` varchar(200),
`type` varchar(100),
`nation` varchar(100),
`language` varchar(20),
`release` varchar(50),
`duration` int(11),
`othername` varchar(200),
`introduction` text,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
仿照e瞳网2017校运会H5小游戏的项目(https://github.com/eeyes-net/sports_meeting_h5_game-2017-04 ),PHP采用原生代码,index.php作主页,common.php内写功能,config.php设置参数,这里主要用于设置数据库连接参数。
/**
* User: Cantjie
* Date: 2017/5/29
* Time: 17:32
*/
return [
// 服务器地址
'hostname' => '127.0.0.1',
// 数据库名
'database' => 'test_crawler',
// 用户名
'username' => 'root',
// 密码
'password' => '',
// 数据库编码默认采用utf8
'charset' => 'utf8',
];
然后开始一步步写功能。
1、首先是需要一个函数来获取config.php中的参数。(这段代码摘自上文提到的校运会项目);
/**
* get config information
* @param string $key
* @return mixed
*/
function config($key)
{
static $config = null;
if (is_null($config)) {
$config = include dirname(dirname(__FILE__)) . '/config.php';
}
return $config[$key];
}
2、然后还需要一个连接和关闭数据库的函数
/**
* connect or close database
* @param bool $is_close 关闭数据库链接
* @return bool|mysqli 返回MySQLi对象
*/
function connect_db($is_close = false)//如果参数是false,连接数据库,如果参数是true,关闭连接。
{
static $mysqli = null;//php里面用static实现单例
if ($is_close) {
if (!is_null($mysqli)) {
$mysqli->close();
$mysqli = null;
}
return true;//成功关闭返回真
}
if (is_null($mysqli)) {
$mysqli = new mysqli(config('hostname'), config('username'), config('password'), config('database'));
}
if ($mysqli->connect_error) {//如果有错误,返回false
$mysqli = null;
return false;
}
$mysqli->query("SET NAMES 'utf8'");
return $mysqli;
}
3、之后,分析一下豆瓣的页面(https://movie.douban.com/cinema/nowplaying/xian/),可以发现,正在热映的电影html标签中对应这样一个属性:data-category=”nowplaying”;还有另外一个属性:data-subject=”(数字)”。而没出现一次nowplaying,就会出现两个subject编号。因此,利用正则表达式获取所有正在热映的电影的编号 。
/**
* get subjects of movies from "https://movie.douban.com/cinema/nowplaying/xian"
* @return array subjects
*/
function get_subject()
{
$url='https://movie.douban.com/cinema/nowplaying/xian/';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727');
curl_setopt($ch, CURLOPT_REFERER, 'https://movie.douban.com/');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$content=curl_exec($ch);
curl_close($ch);
// $content=file_get_contents($url);
$preg_tag_cate='#data-category="([a-z]+)"#';
preg_match_all($preg_tag_cate,$content,$category);
$preg_tag_subj='#data-subject="([0-9]+)"#';
preg_match_all($preg_tag_subj,$content,$fake_subject);
$n=count($category[1]);
for($i=0;$i<$n;$i++)
{
if($category[1][$i]!="nowplaying")
{
$n=$i;
break;
}
}
for($i=0;$i<$n;$i++)
{
$subject[]=$fake_subject[1][2*$i];//分析页面得到一个nowplaying的subject会出现两次。如此去重
}
return $subject;
}
最开始用file_get_content($url),在多次测试后,首先出现打不开网页的状况,甚至用浏览器也无法打开豆瓣电影的网页,停止访问一段时间后恢复正常,后来出现403forbidden错误。因此放弃file_get_content()函数,改用功能更加强大的cURL,添加上header和referer后可以正常访问。
4、由subject编号得到url,这个函数就比较简单了,简单的拼接而已,无需多说。
/**
* turn subject to urls
* @return string|array urls
*/
function subject_to_url($subjects)
{
if(count($subjects)==1)
{
return 'https://movie.douban.com/subject/'.$subjects;
}
else
{
foreach ($subjects as $subject)
{
$url[]='https://movie.douban.com/subject/'.$subject;
}
return $url;
}
}
5、得到url之后,接下来就是a)先用正则表达式匹配到相关信息,b)将信息放到数据库中。这是整个爬虫最重要的部分了。
/**
* get information from website and store it in mysql
* @param array urls
* @return bool if failed,return false
*/
function get_info($urls)
{
//先get到信息然后再存储
foreach ($urls as $url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727');
curl_setopt($ch, CURLOPT_REFERER, 'https://movie.douban.com/cinema/nowplaying/xian/');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);//不加这句话会出现301错误
$content=curl_exec($ch);
curl_close($ch);
$src=array(" ","
","\t","\n","\r");
$tar=array("","","","","");//删除全部空行、空格
$content=str_replace($src,$tar,$content);
$preg_tag_moviename='#"v:itemreviewed">(.*?)<\/span>#';
preg_match_all($preg_tag_moviename,$content,$tar_moviename);
if(movie_validate($tar_moviename[1][0]))
{
echo '《'.$tar_moviename[1][0]."》在数据库中已经存在
";
continue;
}
$preg_tag_director='#"v:directedBy">(.*?)<\/a>#';
preg_match_all($preg_tag_director,$content,$tar_director);
$preg_tag_playwright='#"/celebrity/[0-9]+/">(.*?)<\/a>#';
preg_match_all($preg_tag_playwright,$content,$tar_playwright);
$preg_tag_actor='#rel="v:starring">(.*?)<\/a>#';
preg_match_all($preg_tag_actor,$content,$tar_actor);
$preg_tag_type='#"v:genre">(.*?)<\/span>#';
preg_match_all($preg_tag_type,$content,$tar_type);
$pre_tag_nation='#制片国家/地区:<\/span>(.*?)
#';
preg_match_all($pre_tag_nation,$content,$tar_nation);
$pre_tag_language='#语言:<\/span>(.*?)
#';
preg_match_all($pre_tag_language,$content,$tar_language);
$pre_tag_release='#"v:initialReleaseDate"content="(.*?)"#';
preg_match_all($pre_tag_release,$content,$tar_release);
$pre_tag_duration='#"v:runtime"content="([0-9]+)"#';
preg_match_all($pre_tag_duration,$content,$tar_duration);
$pre_tag_othername='#又名:<\/span>(.*?)
#';
preg_match_all($pre_tag_othername,$content,$tar_othername);
$pre_tag_introduction='#class="allhidden">(.*?)<\/span>#';
if(preg_match_all($pre_tag_introduction,$content,$tar_introduction)==0)
{
$pre_tag_introduction='#"v:summary"class="">(.*?)<\/span>#';
preg_match_all($pre_tag_introduction,$content,$tar_introduction);
}
//对获取的数组进行处理
$moviename=$tar_moviename[1][0];
$director="";
foreach ($tar_director[1] as $tar)
{
$director=$director.$tar.'/';
}
$playwright="";
foreach ($tar_playwright[1] as $tar)
{
$playwright=$playwright.$tar.'/';
}
$actor="";
foreach ($tar_actor[1] as $tar)
{
$actor=$actor.$tar.'/';
}
$type="";
foreach ($tar_type[1] as $tar) {
$type=$type.$tar.'/';
}
$nation=$tar_nation[1][0];
$language=$tar_language[1][0];
$release="";
foreach ($tar_release[1] as $tar)
{
$release=$release.$tar.'/';
}
$duration=(int) $tar_duration[1][0];
$othername=$tar_othername[1][0];
$introduction=$tar_introduction[1][0];
//连接数据库并储存数据
$mysqli=connect_db();
if($mysqli==null)
return false;
$stmt = $mysqli->prepare("INSERT INTO `movie` (`moviename`, `director`, `playwright`, `actor`, `type`,`nation`,`language`,`release`,`duration`,`othername`,`introduction`) VALUES (?,?,?,?,?,?,?,?,?,?,?)");
$stmt->bind_param('ssssssssdss',$moviename,$director,$playwright,$actor,$type,$nation,$language,$release,$duration,$othername,$introduction);
$stmt->execute();
$stmt->close();
echo '导入《'.$moviename.'》成功
';
sleep(3);//为了防止被屏蔽,暂停三秒
}
}
a)前几次测试中,发现网页提示301错误,将CURLOPT_FOLLOWLOCATION设置为true后问题解决。
b)在利用正则表达式匹配introduction的时候,即使利用\s*也常常出现问题,因此选择在获取到网页后就把网页的全部不可见字符删除。由此解决了introduction匹配不上的问题。但是也带来了最下方的问题2。
1、connect_db(false)之后忘记断开连接了,即缺少connect_db(true);
2、删除源码中全部空格等不可见字符的处理不妥,或许可以放到获取到影片名之后,因为影片名中是含有英文的,这下英文都连到一起了。
3、导入成功与否无法实时显示。虽然echo ‘导入成功’放在循环里面,但结果依然是这若干行‘导入成功’会在最后一下子冒出来,而不是一个一个出现。
4、效率太低,对于大量内容单进程无法胜任,因此需要尝试多进程。
5、每次运行都要重新下载网页,或许对于不严格要求时效性的网页可以在第一次爬取后便下载到本地。
6、缺少代理,应当再爬取一些代理IP,进一步利用代理IP来爬取豆瓣上的内容。