2018年1月23日 星期二

【爬蟲】透過Selenium WebDriver 爬網頁,以Instagram為例


常常因為資料分析的需求,會有需要爬網頁資料的時候,而以往爬網頁不外乎將Html拉回來後,依據Tag去拆解資訊。 但現今的網站很大部分都是前端透過API拉版面,以Instagram來說,如果直接透過網址將Html拉回來,會只得到空空的外殼而已,什麼都找不到。 這時候就需要模擬瀏覽器行為來讓Javascript運作,甚至操作瀏覽器去點擊特定按鈕。

Instagram拉回來的網頁就只有一個空殼而已....

透過Selenium.WebDriver,以及Seleium.WebDriver.ChromeDriver套件,可以寫程式操作Chrome的操作行為



接著來一步一步分析如何透過它來爬網頁

開啟Chrome瀏覽器,並且連到想爬的網頁 : https://www.instagram.com/mercci22/
            using (IWebDriver driver = new ChromeDriver())
            {

                driver.Navigate().GoToUrl("https://www.instagram.com/mercci22/");            
            }


接著分析目標網頁,會發現所有PO文資料都放在一個Div且Class為_cmdpi裡面


往下找出每一行、每一格,在div[class='_cmdpi']底下會有div[class='_70iju']每一行


而每一行裡面又有三個Div代表每一格


所以就來透過套件的API找出每一行,並點擊每一格吧
 
            using (IWebDriver driver = new ChromeDriver())
            {
                driver.Navigate().GoToUrl("https://www.instagram.com/mercci22/");

                //找到Post的Container
                var PostContainerElement = driver.FindElement(By.ClassName("_cmdpi"));
                //每一行
                var Rows = PostContainerElement.FindElements(By.ClassName("_70iju"));
                foreach (var row in Rows)
                {
                    var Boxs = row.FindElements(By.XPath("div"));
                    foreach (var box in Boxs)
                    {
                        //點擊每一格讓它展開Dialog
                        box.Click();
                    }
                }
                
                
            }

這時候如果你執行程式,應該會看到它開啟Chrome並且連到網址然後點擊每一格打開視窗


接著來分析彈跳出來的視窗,會發現當視窗開啟時,網頁會出現以下Div[role='dialog']這個元素,關閉後就會移除


所以我們要想辦法拿到這個Div Dialog,才有辦法擷取Po文的文案、日期、圖片,找到Dialog後,後面就重複上述步驟分析Tag,會發現

圖片 : 放在Div[class='_4rbun']底下的Img Tag
文案 : 放在Img Tag的Alt裡面
時間 : 放在Article > div > div > a > time這個Tag裡面


所以目前程式如下
            using (IWebDriver driver = new ChromeDriver())
            {
                driver.Navigate().GoToUrl("https://www.instagram.com/mercci22/");

                //找到Post的Container
                var PostContainerElement = driver.FindElement(By.ClassName("_cmdpi"));
                //每一行
                var Rows = PostContainerElement.FindElements(By.ClassName("_70iju"));
                foreach (var row in Rows)
                {
                    var Boxs = row.FindElements(By.XPath("div"));
                    foreach (var box in Boxs)
                    {
                        //點擊每一格讓它展開Dialog
                        box.Click();

                        //取得Dialog底下的Article元素
                        var article = driver.FindElement(By.XPath("//div[@role='dialog']/div/div/article"));

                        //如果Dialog裡面放的是影片,則_4rbun會不存在
                        if (article.FindElements(By.ClassName("_4rbun")).Count == 0)
                        {
                            //跳過這則,這次目標只抓出圖片
                            continue;
                        }

                        //第一張圖
                        var ImgContainer = article.FindElement(By.ClassName("_4rbun"));
                        var Img = ImgContainer.FindElement(By.TagName("img"));                       
                        var Date = article.FindElement(By.XPath("div/div/a/time"));
                    }
                }
            }

這時候執行的時候可能會發生Exception,原因嘗試取得Dialog底下的Artilce,但Dialog點擊後產生會有時間差導致


優化這段程式,加上Wait的限制,而Selenium提供兩種Wait的方式
implicitly Wait: 預設等待,當元件暫時找不到時,會嘗試等待,直到timeout時間到。
Explicit Wait: 針對特別元件等待。


參考文件:
Selenium 5. Waits 官方文件

我們這邊加上第一種預設等待
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(2);
讀取每個Element時,如果暫時不存在兩秒後TimeOut,之後再執行看看,會發現跑到第二次box.Click()的時候跳Exception。



原因是當我們打開Dialog時,如果爬完不點擊關閉視窗,會點不到第二隔的元素
蓋住了第二格元素,所以要執行關閉視窗按鈕

            using (IWebDriver driver = new ChromeDriver())
            {
                driver.Navigate().GoToUrl("https://www.instagram.com/mercci22/");
                driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(2);
                //找到Post的Container
                var PostContainerElement = driver.FindElement(By.ClassName("_cmdpi"));
                //每一行
                var Rows = PostContainerElement.FindElements(By.ClassName("_70iju"));
                foreach (var row in Rows)
                {
                    var Boxs = row.FindElements(By.XPath("div"));
                    foreach (var box in Boxs)
                    {
                        //點擊每一格讓它展開Dialog
                        box.Click();
                        
                        //取得Dialog底下的Article元素
                        var article = driver.FindElement(By.XPath("//div[@role='dialog']/div/div/article"));

                        //如果Dialog裡面放的是影片,則_4rbun會不存在
                        if (article.FindElements(By.ClassName("_4rbun")).Count == 0)
                        {
                            //關閉Dialog
                            driver.FindElement(By.ClassName("_dcj9f")).Click();
                            //跳過這則,這次目標只抓出圖片
                            continue;
                        }

                        //第一張圖
                        var ImgContainer = article.FindElement(By.ClassName("_4rbun"));
                        var Img = ImgContainer.FindElement(By.TagName("img"));
                        var Date = article.FindElement(By.XPath("div/div/a/time"));

                        //關閉Dialog
                        driver.FindElement(By.ClassName("_dcj9f")).Click();
                    }
                }
            }


這樣就可以順利地走完每一格,並且把圖片、文案、時間資料都讀出來了



多圖片的情境


加下來是應用的第二部分,Instagram是可以分享多圖片的,而多張圖片是在點擊向右按鈕後,才會動態透過JS撈出來

所以必須寫程式判斷是否有這個按鈕,如果有,表示有多張圖片,要求Driver去點擊那個按鈕,並且撈取Img的Src路徑
                        //第一張圖
                        var ImgContainer = article.FindElement(By.ClassName("_4rbun"));
                        var Img = ImgContainer.FindElement(By.TagName("img"));
                        //存放Image的Src List
                        List<string> ImgUrls = new List<string> { Img.GetAttribute("src") };

                        //如果有第二張圖以上,則會出現a[class=''_8kphn _by8kl coreSpriteRightChevron']
                        //直到不再出現表示最後一張圖到了
                        while (article.FindElements(By.CssSelector("a[class='_8kphn _by8kl coreSpriteRightChevron']")).Count > 0)
                        {
                            //點擊按鈕
                            var nextBtn = article.FindElement(By.CssSelector("a[class='_8kphn _by8kl coreSpriteRightChevron']"));
                            nextBtn.Click();

                            //因為Instagram是透過同一個Img Tag動態去換Src,因為程式點擊下一張按鈕太快
                            //會導致有Img Tag存在,但Src還來不及換,導致抓到空白的Src
                            //所以不是元素沒出現的問題,只好要求Thread換下一張圖時先暫停0.5秒再抓
                            Thread.Sleep(500);

                            Img = ImgContainer.FindElement(By.TagName("img"));
                            ImgUrls.Add(Img.GetAttribute("src"));
                        }

這樣就能順利拿到多張圖的路徑了


讀取第二頁的情境

Instagram是滑鼠移到最下方才會動態載入第二頁,所以需要能控制視窗移到最下方來觸發它
            IJavaScriptExecutor js = (IJavaScriptExecutor)driver;
            //如果爬完第一頁還沒爬完,則執行JS讓視窗滾到最下方,觸發讀取第二頁
            js.ExecuteScript("window.scrollTo(0,1000000)");


目前綜合以上所提的應用,應該已經能完全將Instagram的網站資料爬回來,只能說Selenium真的是一個強大的東西阿!!


參考文章:
XML XPath的選擇節點語法
Selenium Documentation

4 則留言: