PHP爬虫 网易云音乐歌手和热门歌曲信息抓取

序章
PM最近问我要网易云的歌手的热门歌曲的信息,作为数据分析。说起网络爬虫我们都不陌生,我们分析网站的HTML的格式和URL的通用格式来写相应的算法。然后请求对应的URL来获取HTML字符串,因此总的来说,爬虫的本质就是请求字符解析
一.分析页面布局。
首先我们来分析网易云音乐的HTML构成(多图预警)。我们来看网易云的歌手的网页构成。

图1

图2

图1、图2中分别标注了三个地方,是我们分析一个网页的时候,需要注意的地方。

1.分析URL
2.分析菜单列表。
很容易的能够看到,网易云歌手的分布是按照【类别】来分的。我们在获取每个歌手的信息的时候,又id来表示的,其中图2中地址栏中的地址进行分析:http://music.163.com/discover/artist/cat?id=1001&inital=65

1,有前端知识的同学肯定知道,地址栏中的 “#”是前端的路由。所以我们再实际操作的时候,需要把#去掉。在地址中,http://music.163.com/discover/artist/cat?id=1001&inital=65,id=1001表示的是歌手的类别ID,inital=65表示的是歌手的按照字母排序的歌手名字类别ID。

详细来说
id=1001 表示的是 【华语男歌手】
inital=65 表示的是 【首字母为A的名字】

(读者可以访问http://music.163.com/discover/artist/cat?id=1001&inital=65查看一下网页的源码。)
我们可以通过获取分析HTML源码来获取页面上的所有的【类别ID】和【歌手名字类别ID】,进而获取所有的【歌手列表页面】
例如:
如下图

图3

图中很容一分析的页面上的歌手类别ID,华语男歌手ID为 1001、华语女歌手ID为1002、华语组合/乐队ID为1003。
同理我们很容分析出来歌手的名字类别ID如图4

图4

很容易得知:热门歌手的ID=-1、首字母为A的ID=65、首字母为B的ID=66、首字母为C的ID=67。

由上图的分析很容易知道,假如类别有N个,名字类别有M个。最后组合在一起的URL一共有N*M个。

整体思路我们分析完了。
接下来该分析源码,处理字符串来获取了我们想要的类别ID 和 名字类别ID。

 /**
     * 发送请求
     * @param null $url 请求的URL
     * @return bool|string 返回的信息
     */

   function sendRequest($url=null){
        $handle = null;

        if (empty($url)){
            $handle = fopen($this->url, "rb");
        }else{
            $handle = fopen($url, "rb");
        }

        $contents = stream_get_contents($handle);
        fclose($handle);
        return $contents;
    }
     /**
     * 获取歌手歌曲主页的URL
     * @param $html_str
     * @return array
     */
   function getSingerHomeUrl($html_str){

        $typeMap = [];
        $rltArr = [];
        $urlRsltArr = [];
        $dom =  HtmlDomParser::str_get_html($html_str);
        $elems = $dom->find('li');
        foreach ($elems as $key => $value){
            $aas = $value->find('a');
            foreach ($aas as $k=>$v){
                if (strpos(trim($v->href),'discover/artist/cat') !== false ){
                    if (strpos($v->href,'initial') !== false){
                        $rltArr[] = BASE_URL.$v->href;
                    }else{
                        if (is_numeric(trim($v->href,'/discover/artist/cat?id='))){
                            $typeMap[trim($v->href,'/discover/artist/cat?id=')] = $v->text();
                        }
                    }
                }
            }
        }
        foreach ($typeMap as $id=>$type){
            foreach ($rltArr as $url){
                $urlRsltArr[$type][] =  str_replace('1001',$id,$url);
            }
        }
        return $urlRsltArr;
    }

其中我们用到了一个工具HtmlDomParser,这是一个专门用来解析HTML的SDK,我们可以在github上很容易搜到,在这里我用的是composer来管理这些第三方框架的。

composer require sunra/php-simple-html-dom-parser

既然说到HtmlDomParser,我就多说一句。

传进去html字符串,获取一个DOM对象
$dom =  HtmlDomParser::str_get_html($html_str);

find()方法是以选择器的方式来获取其中的某一个元素的,返回的是一个数组。
$dom->find($selector);

其中有一个坑,我们一般会用var_dump()来打印一个对象。但是对于这个工具来说,这样是无法打印出我们想要的东西的。
这个HTML解析工具为我们封装了一个dump(),我只需要$dom->dump()就可以调试打印了。

其中有一个BASE_URL,这个是我定义的一个常量。

define('BASE_URL','http://music.163.com');
define('MV_BASE_URL','http://music.163.com/mv?id=');
define('SONG_BASE_URL','http://music.163.com/song?id=');
define('APP_PATH',__DIR__);
define('DATA_PATH',APP_PATH.'/data');

这些路径很容易看懂吧。接下来会用到。
通过上述的代码我们封装了好了方法,我们需要一个初始URL来驱动我们整个爬虫的运行。

$url = 'http://music.163.com/discover/artist/cat?id=1001&initial=65';

接下来我们调用我们的方法

$url = 'http://music.163.com/discover/artist/cat?id=1001&initial=65';
$html_str = sendRequest($url);
$arr = getSingerHomeUrl($html_str);

二.分析歌手页面源码

图5

可以从图5看出,整个歌手的数据分为两个部分。

第一部分:带图片的。
第二部分:不带图片。

接下来我们需要做的就是分析这两者的源码。

图6

我们分析两者的源码可以得到如下的结论。
1、a标签的herf的值包含“/artist?id=”
2、并且歌手的id为的值为数字
3、歌手的主页的url为 http://music.163.com/artist?id=xxxx
很容易得到下面的代码

/**
     * 获取歌手的信息,通过解析HTML字符串
     * @param $html_str HTML字符串
     * @param null $type 歌手类型
     * @return array
     */
public function getSingerInfo(){

        $file_path = APP_PATH.'/all/singer.log';
        if (!file_exists($file_path)){
            touch($file_path);
        }

        $html_str = $this->sendRequest();
        $typeUrlsMap = $this->getSingerHomeUrl($html_str);

        foreach ($typeUrlsMap as $type
        =>$urls){

            foreach ($urls as $url){
                //***这个段代码为什么这样写后续在进行讲解***
                //***先立一个flag***
                //-------------FLAG1--------------
                $home_html_str = $this->sendRequest($url);
                do{
                    $home_html_str =  $this->sendRequest($url);
                    if (empty($home_html_str)) {
                        Log::addLog('账号被封,证在等待解封,当前的时间戳'.time(),Log::WARNING);
                        sleep(1);
                    }
                }while(empty($home_html_str));
                //-------------FLAG1--------------
                Log::addLog('正常运行['.$type.']......',Log::WARNING);
                $this->getSingerInfoByHomeHtml($home_html_str,$type,$url);
            }
        }
    }

/**
     * 获取歌手的信息,通过解析HTML字符串
     * @param $html_str HTML字符串
     * @param null $type 歌手类型
     * @return array
     */
   function getSingerInfoByHomeHtml($html_str,$type=null,$url=null){

        $dom =  HtmlDomParser::str_get_html($html_str);
        $elems = $dom->find('li');
        $rltSingerArr = [];
        foreach ($elems as $key => $value){
            $aas = $value->find('a');
            foreach ($aas as $k=>$v){
                //根据上面分析我们很容易得到每个歌手的首页地址
                $id = trim(trim($v->href),"/artist?id=");
                if (strpos(trim($v->href),'/artist?id=') !== false && is_numeric($id) && !empty($v->text())){
                    $name = $v->text();
                    $href  = BASE_URL.trim($v->href);
                    $singerInfoArr = [$id,$name,'',$href,'1090',$type];
                    $rltStr = implode("\t",$singerInfoArr);
                    $rltSingerArr[] = $rltStr;
                    //将获取的歌手信息写入文件
                    file_put_contents(APP_PATH.'/all/singer.log',$rltStr.PHP_EOL,FILE_APPEND) && $this->getSongAndMvInfo($href,$id);
                }
            }
        }
        return $rltSingerArr;
    }
//我们以同样的方法获取到每一首歌的URL构成
//拼接处每一首歌的URL,然后获取他的详细信息
//在上面的函数里
    function getSongAndMvInfo($url=null,$id=null){
        $html_str = null;

        do{
            $html_str =  $this->sendRequest($url);
            if (empty($html_str)) {
                Log::addLog('请求歌手主页账号被封['.$url.'],waitting.....'.time(),Log::WARNING);
                sleep(1);
            }
        }while(empty($html_str));
        Log::addLog('歌手获取URL正常运行['.$url.']',Log::WARNING);

        $dom =  HtmlDomParser::str_get_html($html_str);
        $elems = $dom->find('textarea');
        $jsonArr = [];
        $arr = [];
        foreach ($elems as $elem_a){
            if ($elem_a->style == "display:none;" && !$elem_a->has_child() && empty($elem_a->id) && empty($elem_a->name)){
                $jsonStr = $elem_a->text();
                $jsonArr = $this->jsonToArray($jsonStr);
                $this->formatOutput($jsonArr,APP_PATH.'/all/song.log',$id);
            }
        }
    }
  //格式化输出
   function formatOutput($jsonArr,$fileName,$id){
        if (!file_exists($fileName)){
            touch($fileName);
        }
        foreach ($jsonArr as $song){
            $arr = [$song['id'],$song['name'],$id,'',SONG_BASE_URL.$song['id'],'1090','',MV_BASE_URL.$song['mvid']];
            file_put_contents($fileName,implode("\t",$arr).PHP_EOL,FILE_APPEND) ;
        }
    }

   function jsonToArray($json_str,$isObj=true){
        return json_decode($json_str,$isObj);
    }

解释一下FLAG1
在实际测试中,发现网易云会进行封IP,因为我们请求太频繁,但是在封了账号以后,会在在几秒以后解封账号。

总结一下:
1、分析网页构成。
2、设计字符获取算法。

后期优化
上面存在的问题
1、直接阻塞式的获取歌手和歌曲的信息
2、如果中间出现了问题,还要从头开始爬,无法从断点处继续爬。

针对上述的问题得出解决方案:
1、多进程异步处理。
先获取歌手信息,存入多个文件,或者数据库。然后主进程开启多个子进程进行处理。
2、设置异常函数句柄,在回调函数里面保存状态。

register_shutdown_function(callback $funcName)

还需要进一步完善。

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

推荐阅读更多精彩内容