0%

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

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

[![](https://2.bp.blogspot.com/-6yR04AI5phg/Wmf7S3BE3TI/AAAAAAAAITs/gELt5or7tQkL60jmDRYwb1-YGuNU8cKggCLcBGAs/s400/1.png)](https://2.bp.blogspot.com/-6yR04AI5phg/Wmf7S3BE3TI/AAAAAAAAITs/gELt5or7tQkL60jmDRYwb1-YGuNU8cKggCLcBGAs/s1600/1.png)
Instagram拉回來的網頁就只有一個空殼而已....
透過Selenium.WebDriver,以及Seleium.WebDriver.ChromeDriver套件,可以寫程式操作Chrome的操作行為
[![](https://1.bp.blogspot.com/-DkheSsyDReI/Wmf8FTKy0VI/AAAAAAAAIT0/raZdEEh40xgGJQbTFwhZ2A3aQbOOh4VWACLcBGAs/s640/1.png)](https://1.bp.blogspot.com/-DkheSsyDReI/Wmf8FTKy0VI/AAAAAAAAIT0/raZdEEh40xgGJQbTFwhZ2A3aQbOOh4VWACLcBGAs/s1600/1.png)
[![](https://3.bp.blogspot.com/-vnkv6jsDImY/Wmf8OnewkhI/AAAAAAAAIT4/FnlCPUMVsH05noZ1RVbhxFbWqMIYKZJ5gCLcBGAs/s640/1.png)](https://3.bp.blogspot.com/-vnkv6jsDImY/Wmf8OnewkhI/AAAAAAAAIT4/FnlCPUMVsH05noZ1RVbhxFbWqMIYKZJ5gCLcBGAs/s1600/1.png)

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

開啟Chrome瀏覽器,並且連到想爬的網頁 : https://www.instagram.com/mercci22/

1
2
3
4
5
6
using (IWebDriver driver = new ChromeDriver())
{

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

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

[![](https://1.bp.blogspot.com/-sbreT5cUils/Wmf99JegYUI/AAAAAAAAIUI/oIXZxzMGX7c9LU4uVQ9H0BSJUI67UgdGwCLcBGAs/s640/1.png)](https://1.bp.blogspot.com/-sbreT5cUils/Wmf99JegYUI/AAAAAAAAIUI/oIXZxzMGX7c9LU4uVQ9H0BSJUI67UgdGwCLcBGAs/s1600/1.png)

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

[![](https://4.bp.blogspot.com/-yluY12_h7T0/Wmf_JJsdiBI/AAAAAAAAIUQ/zLIRDslBbZQEYGRvu0qXp4JoSdObu-lPwCLcBGAs/s640/1.png)](https://4.bp.blogspot.com/-yluY12_h7T0/Wmf_JJsdiBI/AAAAAAAAIUQ/zLIRDslBbZQEYGRvu0qXp4JoSdObu-lPwCLcBGAs/s1600/1.png)

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

[![](https://4.bp.blogspot.com/-jnEHEexjkeM/Wmf_roHtA4I/AAAAAAAAIUY/4t6RR_UwEP8-1I7EipD7guWU4Ddyk3d5QCLcBGAs/s640/1.png)](https://4.bp.blogspot.com/-jnEHEexjkeM/Wmf_roHtA4I/AAAAAAAAIUY/4t6RR_UwEP8-1I7EipD7guWU4Ddyk3d5QCLcBGAs/s1600/1.png)

所以就來透過套件的API找出每一行,並點擊每一格吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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並且連到網址然後點擊每一格打開視窗

[![](https://3.bp.blogspot.com/-UIjpEynpLcA/WmgBKT9R10I/AAAAAAAAIUk/WA64zbd-g-YoQdf33ZVlrfVbXDLsYJkhACLcBGAs/s640/1.png)](https://3.bp.blogspot.com/-UIjpEynpLcA/WmgBKT9R10I/AAAAAAAAIUk/WA64zbd-g-YoQdf33ZVlrfVbXDLsYJkhACLcBGAs/s1600/1.png)

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

[![](https://4.bp.blogspot.com/-clgYAUdg4y0/WmgBoMCshoI/AAAAAAAAIUo/E3NwCF5n5i8Cpilg7m-OChlEQEgGHOGAwCLcBGAs/s640/1.png)](https://4.bp.blogspot.com/-clgYAUdg4y0/WmgBoMCshoI/AAAAAAAAIUo/E3NwCF5n5i8Cpilg7m-OChlEQEgGHOGAwCLcBGAs/s1600/1.png)

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

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

所以目前程式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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點擊後產生會有時間差導致

[![](https://4.bp.blogspot.com/-TUSU5eY99xU/WmgWjs_9uBI/AAAAAAAAIVE/QnOtvHvoW38zNLWZQ7m6NpICJluVxyc2QCLcBGAs/s640/1.png)](https://4.bp.blogspot.com/-TUSU5eY99xU/WmgWjs_9uBI/AAAAAAAAIVE/QnOtvHvoW38zNLWZQ7m6NpICJluVxyc2QCLcBGAs/s1600/1.png)

優化這段程式,加上Wait的限制,而Selenium提供兩種Wait的方式

implicitly Wait: 預設等待,當元件暫時找不到時,會嘗試等待,直到timeout時間到。
Explicit Wait: 針對特別元件等待。

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

我們這邊加上第一種預設等待

1
2
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(2);

讀取每個Element時,如果暫時不存在兩秒後TimeOut,之後再執行看看,會發現跑到第二次box.Click()的時候跳Exception。

[![](https://4.bp.blogspot.com/-y0clUXejLl4/WmgZwH5ZmeI/AAAAAAAAIVQ/bDTSCWKn9jkNDem1AxJ-TAQPwPQpW-7hACLcBGAs/s640/1.png)](https://4.bp.blogspot.com/-y0clUXejLl4/WmgZwH5ZmeI/AAAAAAAAIVQ/bDTSCWKn9jkNDem1AxJ-TAQPwPQpW-7hACLcBGAs/s1600/1.png)

原因是當我們打開Dialog時,如果爬完不點擊關閉視窗,會點不到第二隔的元素

[![](https://1.bp.blogspot.com/-gPgFjISg5XI/WmgaJ1qWtUI/AAAAAAAAIVU/ii1yaeglfDgqvpH05tnhPa8Soxe3OszjACLcBGAs/s640/1.png)](https://1.bp.blogspot.com/-gPgFjISg5XI/WmgaJ1qWtUI/AAAAAAAAIVU/ii1yaeglfDgqvpH05tnhPa8Soxe3OszjACLcBGAs/s1600/1.png)
蓋住了第二格元素,所以要執行關閉視窗按鈕
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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撈出來

[![](https://4.bp.blogspot.com/-S8AFeQpjcHk/WmgbnCX9SgI/AAAAAAAAIVk/2jcAH-RsJ4cg7IN6X2_UwCWVmbNX1BJeACLcBGAs/s400/1.png)](https://4.bp.blogspot.com/-S8AFeQpjcHk/WmgbnCX9SgI/AAAAAAAAIVk/2jcAH-RsJ4cg7IN6X2_UwCWVmbNX1BJeACLcBGAs/s1600/1.png)
所以必須寫程式判斷是否有這個按鈕,如果有,表示有多張圖片,要求Driver去點擊那個按鈕,並且撈取Img的Src路徑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//第一張圖
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是滑鼠移到最下方才會動態載入第二頁,所以需要能控制視窗移到最下方來觸發它
```csharp IJavaScriptExecutor js = (IJavaScriptExecutor)driver; //如果爬完第一頁還沒爬完,則執行JS讓視窗滾到最下方,觸發讀取第二頁 js.ExecuteScript("window.scrollTo(0,1000000)");

```

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

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