# Python爬蟲使用Beautiful Soup ###### tags: `python` `crawler` `beautifulsoup` ## 介紹 Beautiful Soup是Python中用來解析HTML、XML標籤文件的模組,並能修復含有未閉合標籤等錯誤的文件(此種文件常被稱為tag soup);解析後會為這個頁面建立一個`BeautifulSoup`物件,這個物件中包含了整個頁面的結構樹,透過這個`BeautifulSoup`物件的結構樹,就可以輕鬆的提取頁面內任何有興趣的資料了。 #### 找出資料在網頁哪個元素的三種方式 1. 土法煉鋼:在瀏覽器點右鍵,檢視原始碼後直接用網頁內容來搜尋資料被什麼元素包住。 2. 先排版:有些網站原始碼較混亂,可以使用網路上的HTML排版工具先排版後再進行資料定位。 3. 使用Chrome的開發工具,在想要定位的元素上點滑鼠右鍵->檢查(Inspect) ## BeautifulSoup支援的網頁元素定位方式 1. [基本find系列方法](#基本find系列方法) 2. [CSS Selector(選擇器)](#CSS選擇器) 3. XPath ## 安裝Beautiful Soup模組 Python 3.x版本可以使用 `pip3` 安裝: ``` pip3 install bs4 ``` 如果是Python 2.x版本,可以這樣安裝: ``` pip install bs4 ``` > 參考資料:https://pypi.org/project/beautifulsoup4/ ## 解析HTML文件 ### 建立BeautifulSoup物件 下面範例為建立一個解析HTML文件的`BeautifulSoup`物件,解析完成後會產生一個`soup`物件,這個就是該HTML的結構樹物件,之後所有資料的搜尋、提取等操作都是透過這個物件來進行。 ```python= # 引入Beautiful Soup模組 from bs4 import BeautifulSoup # 原始HTML原始碼 html_doc = """ <html> <head> <title>這是HTML文件標題</title> </head> <body> <h1 id="article" class="banner">網頁標題</h1> <p data-author='aaron'>文章段落</p> <a href="https://www.aaronlife.com/ref1">參考資料連結1</a> <a href="https://www.aaronlife.com/ref2">參考資料連結2</a> <p>這是一份<b class="boldtext">HTML文件</b>。</p> <h2 id="article">網頁標題2</h2> </body> </html> """ # 建立BeautifulSoup物件解析HTML文件 soup = BeautifulSoup(html_doc, 'html.parser') ``` ##### BeautifulSoup文件解析器有以下幾種: | 解析器名稱 | 使用範例 | 說明 | | -------------------- | -------- | ---- | | Python’s html.parser | `BeautifulSoup(markup, "html.parser")` | Python內建 | | lxml’s HTML parser | `BeautifulSoup(markup, "lxml")` | 速度較快的HTML解析器 | | lxml’s XML parser | `BeautifulSoup(markup, "lxml-xml")`<br/>`BeautifulSoup(markup, "xml")` | 速度較快的XML解析器 | | html5lib | `BeautifulSoup(markup, "html5lib")` | 解析後建立HTML 5物件 | > 官方文件:https://www.crummy.com/software/BeautifulSoup/bs4/doc/ > **補充** > > lxml解析器需要另外安裝: > > ``` > pip3 install lxml > ``` ##### 輸出排版後的HTML文件:`prettify()` 呼叫`BeautifulSoup`物件的`prettify`方法可以輸出排版過後的HTML文件,在開發時可以用來觀察其文件的整個輪廓: ```python= print(soup.prettify()) ``` 會輸出: ```html= <html> <head> <title> 這是HTML文件標題 </title> </head> <body> <h1 id="article" class="banner"> 網頁標題 </h1> <p data-author='aaron'> 文章段落 </p> <a href="https://www.aaronlife.com/ref1"> 參考資料連結1 </a> <a href="https://www.aaronlife.com/ref2"> 參考資料連結2 </a> <p> 這是一份 <b class="boldtext"> HTML文件 </b> 。 </p> </body> </html> ``` ### 取得指定的標籤及內容 直接指定網頁標題標籤的名稱(例如:`title`),即可將該標籤的節點抓出來: ```python= print(soup.title) # 取得第一個title標籤 print(soup.html.body.h1) # 取得指定位置的第一個h1標籤 print(soup.a) # 取得文件內第一個a標籤 ``` 會輸出包含標籤的文字: ``` <title>這是HTML文件標題</title> <h1 id="article" class="banner">網頁標題</h1> <a href="https://www.aaronlife.com/ref1">參考資料連結1</a> ``` 透過 `string` 屬性,可以取得標籤內的純文字: ```python= print(soup.title.string) ``` 會輸出: ``` 這是HTML文件標題 ``` ## 基本find系列方法 ### 搜尋標籤 - `find_all()` 透過 `find_all` 可以在文件內找出指定的所有標籤,`find_all`會以`list`傳回所有找到的標籤: ```python= tag_p = soup.find_all('p') for p in tag_p: print(p.getText()) ``` 會輸出: ``` 文章段落 這是一份HTML文件。 ``` > 注意: > > 因為第二個p標籤內含有子標籤,所以如果直接使用`soup.string`只會得到`None`,這時就必須使用`getText()`方法來取得文字。 ### 取出節點屬性 ##### `get()`方法 透過 `get()`方法,可以取得標籤內的屬性值,下面透過`get()`方法來取得`a`標籤內的`href`屬性: ```python= tag_a = soup.find_all('a') for a in tag_a: print(a.get('href')) ``` 會輸出: ``` https://www.aaronlife.com/ref1 https://www.aaronlife.com/ref2 ``` ##### `attrs` 也可以透過`attrs`屬性可以拿到標籤內全部的屬性,該屬性為一個`list`,例: ```python print(tag.attrs['data-table']) ``` 會得到`data-table`的屬性值 ### 同時搜尋多種標籤 如果要同時搜尋多種HTML標籤,可以使用 list 來指定所有要搜尋的標籤名稱: ```python= tags = soup.find_all(['a', 'p']) for tag in tags: print(tag) ``` 會輸出: ``` <p data-author='aaron'>文章段落</p> <a href="https://www.aaronlife.com/ref1">參考資料連結1</a> <a href="https://www.aaronlife.com/ref2">參考資料連結2</a> <p>這是一份<b class="boldtext">HTML文件</b>。</p> ``` ### 限制搜尋的標籤數量 `find_all` 預設會搜尋所有符合條件的節點,如果遇到文件較大或是標籤數量很多的時候,就會執行比較多的時間,如果在應用上不需要用到全部標籤,可以用 `limit` 參數來限制搜尋的標籤上限值,這樣一來,就只會找出前幾個符合條件的標籤: ``` tags = soup.find_all(['a', 'p'], limit=2) for tag in tags: print(tag) ``` 會輸出: ``` <p data-author='aaron'>文章段落</p> <a href="https://www.aaronlife.com/ref1">參考資料連結1</a> ``` 如果只需要抓出第一個符合條件的節點,可以直接使用 `find()`方法即可: ```python= tag = soup.find('a') # 搜尋第一個a標籤 print("第一個搜尋結果:", tag) tag = soup.find(['a', 'p']) # 在多個標籤名稱下搜尋第一個符合條件的標籤 print("第二個搜尋結果:", tag) ``` 會輸出: ``` 第一個搜尋結果: <a href="https://www.aaronlife.com/ref1">參考資料連結1</a> 第二個搜尋結果: <p data-author='aaron'>文章段落</p> ``` ### 遞迴搜尋 在預設的狀況下,`find_all()`方法會以遞迴的方式 往下尋找所有的子標籤,例如搜尋`html`標籤下所有h1標籤,包含子標籤下的標籤: ``` tags = soup.html.find_all('h1') ``` 會輸出: ```python= print(tags) ``` 上面程式碼,加上`recursive=False`後,表示只會搜尋在`html`標籤下第一層的標籤: ```python= tags = soup.html.find_all('h1', recursive=False) print(tags) ``` 會輸出: ``` [] ``` > 因為`h1`標籤是在`html`標籤下的`body`標籤內,也就是底下兩層,所以不會被搜尋到。 ### 以屬性名稱搜尋 透過指定標籤內的屬性名稱,可以將所有符合屬性值的所有標籤找出來,例如搜尋 `id` 屬性為 `article` 的節點: ```python= tags = soup.find_all(id='article') print(tags) ``` 會輸出: ``` [<h1 id="article" class="banner">網頁標題</h1>, <h2 id="article">網頁標題2</h2>] ``` 可以透過結合標籤名稱名稱與屬性名稱進行更精確的搜尋,例如搜尋 `id` 屬性為 `article` 的 `h2` 節點: ```python= tag = soup.find_all('h2', id='article') print(tag) ``` 會輸出: ``` [<h2 id="article">網頁標題2</h2>] ``` > 補充: > > 以屬性做為條件來搜尋時,也可以一次給多個屬性條件來篩選,例如: > > `tags = soup.find_all(id='article', name='title') 在HTML5中有些屬性名稱中含有符號,例如 `data-`開頭的屬性名稱如果直接寫在Python的方法中的話會出現錯誤: ```python= tag = soup.find(data-attr='aaron') ``` 會出現下面錯誤: ``` SyntaxError: expression cannot contain assignment ``` 解決方法為,將屬性名稱與值放進一個字典(dict)中,再將此字典設定給 `attrs` 參數即可,例如: ```python= condition = {'data-author': 'aaron'} tag = soup.find_all(attrs=condition) ``` 以正規表示法來搜尋: ```python= import re links = soup.find_all(href=re.compile('^https://www.aaronlife.com')) print(links) ``` > 說明: > > 只要href屬性開頭為`https://www.aaronlife.com`都會被搜尋出來。 ### 以CSS屬性搜尋 因為`class` 是Python程式語言的保留字,所以在Beautiful Soup內改用 `class_`這個名稱來代表HTML標籤內的class屬性,例如要搜尋class為`banner`的標籤: ```python= tags = soup.find_all(class_='banner') print(tags) ``` 會輸出: ``` [<h1 class="banner" id="article">網頁標題</h1>] ``` > 補充: > > 1. 一個class屬性可以有多個屬性值,在做`class_`屬性執比對時,只要有一個符合就算成功。 > 2. 也可以直接拿完整的class屬性值字串比對,但如果順序不對就會失敗,例如:`"banner primary"`和`"primary banner"`會是不一樣的屬性值,這個情況下會建議用CSS選擇器來進行比對,例如:`soup.select('h1.banner.primary')`或`soup.select('*.banner.primary')` 搜尋多個屬性值: ```python tags = soup.find_all(class_=['banner', 'primary']) ``` 搜尋多個屬性值需要使用list將所有要搜尋的值放在一起。 ### 以文字內容搜尋 使用 `find_all` 配合 `string` 參數可以根據文字進行來搜尋特定的標籤內容,例如: ```python= contents = soup.find_all(string='網頁標題') print(contents) ``` 會輸出: ``` ['網頁標題'] ``` > 注意: > > 輸出結果只會有內容,而不是完整的標籤。 ### 搜尋上層標籤 `find_all`和`find`方法都是從外層向下搜尋子標籤,但我們也可以由下往上來搜尋上層標籤(或稱父標籤),方法`find_parents`(如同`find_all`網上搜尋全部符合條件的標籤)和`find_parent`(如同`find`指搜尋第一個符合的標籤)例如: ```python= tag_b = soup.find('b') parent_p = tag_a.find_parent('p') print(parent_p) ``` 會輸出: ``` <p>這是一份<b class="boldtext">HTML文件</b>。</p> ``` ### 往前、往後搜尋標籤 如果想要搜尋的標籤在同一層,則可以使用: - `find_previous_siblings()`(全部符合的)和 `find_previous_sibling()`(第一個符合的):往前搜尋。 - `find_next_siblings()`(全部符合的)和 `find_next_sibling()`(第一個符合的):往後搜尋。 例如: ```python= tag.find_previous_siblings('a') # 往前搜尋同一層的全部a標籤 tag.find_previous_sibling('a') # 搜尋前一個a標籤 tag.find_next_siblings('a') # 往後搜尋全部的a標籤 tag.find_next_sibling('a') # 搜尋後面第一個a標籤 ``` ## CSS選擇器 | 方法名稱 | 說明 | | -------------- | ------------------------------------------- | | `select()` | 以CSS選擇器的方式來找出所有符合條件的元素 | | `select_one()` | 以CSS選擇器的方式來定位第一筆符合條件的元素 | #### `select()` ```python= from bs4 import BeautifulSoup # 原始HTML原始碼 html_doc = ''' <html> <head> <title>這是HTML文件標題</title> </head> <body> <h1 id="article" class="banner">網頁標題1</h1> <p data-author='aaron' class="reqular text-normal">文章段落1</p> <a class="link no btn" href="https://www.aaronlife.com/ref1">參考<b>資料</b>連結1</a> <a class="link btn" href="http://www.aaronlife.org/ref2">參考<b>資料</b>連結2</a> <a class="link btn" href="http://www.aaronlife.edu/ref2">參考資料連結3</a> <a class="link btn" href="https://www.aaronlife.com/ref2">參考資料連結4</a> <p>這是一份<b class="boldtext">HTML文件</b>。</p> <h2 id="article1" class="banner">網頁標題2</h2> <p data-author='andy' class="reqular text-normal">文章段落2</p> <h2 id="article2" class="title normal">網頁標題3</h2> <p data-author='william' class="reqular text-normal">文章段落3</p> </body> </html> ''' # 建立BeautifulSoup物件解析HTML文件 soup = BeautifulSoup(html_doc, 'html.parser') # 透過標籤來定位元素 result = soup.select('a') print('select a:', result) result = soup.select('body p b') print('select p b:', result) # 透過id來定位元素 result = soup.select('#article') print('select #article:', result) # 多重選擇(只要一個符合即可) result = soup.select('#article, p, b') print('select #article, p, b:', result) # 全部class都要符合才會被定位到 result = soup.select('.no.link.btn') print('select .no.link.btn:', result) # 透過class來定位元素 result = soup.select('.banner') print('select .banner:', result) # 透過是否存在某個屬性來定位元素 result = soup.select('a[href]') print('select a[href]:', result) # 透過指定的屬性值來定位元素 result = soup.select('a[href="http://wwww.aaronlife.com"]') print('select a[href=http://wwww.aaronlife.com]:', result) # 透過指定的屬性值「開頭字串」來定位元素 result = soup.select('a[href^="http"]') print('select a[href^=http]:', result) # 透過指定的屬性值「結束字串」來定位元素 result = soup.select('a[href$="com"]') print('select a[href$=com]:', result) # 透過指定的屬性值「有包含的字串」來定位元素 result = soup.select('a[href*="com"]') print('select a[href*=com]:', result) ``` #### `select_one()` 使用方式與select()一樣,直接回傳符合條件的元素,而非list。 ## 從其它的HTML文件來源建立BeautifulSoup物件 ### 從檔案建立 如果我們想要用 Beautiful Soup 解析已經下載的 HTML 檔案,可以直接將開啟的檔案交給 `BeautifulSoup` 處理: ```python= from bs4 import BeautifulSoup # 從檔案讀取HTML原始碼來進行解析 with open("index.html") as html_file: soup = BeautifulSoup(html_doc) print(html_doc) ``` 會輸出: ```html= <html> <head> <title>這是HTML文件標題</title> </head> <body> <h1 id="article" class="banner">網頁標題</h1> <p data-author='aaron'>文章段落</p> <a href="https://www.aaronlife.com/ref1">參考資料連結1</a> <a href="https://www.aaronlife.com/ref2">參考資料連結2</a> <p>這是一份<b class="boldtext">HTML文件</b>。</p> </body> </html> ``` > 備註: > 須先將最上面的HTML文件範例存成index.html檔案,並和這個範例的程式碼檔案放在同一層目錄下。 ### 從網路建立(URL) 要直接從網路抓取html文件,可以使用Python內建的`request`模組: ```python= import requests from bs4 import BeautifulSoup url = "https://www.ptt.cc/bbs/Python/index.html" # PTT Python看板 response = requests.get(url) # 使用requests的get方法把網頁抓下來 html_doc = response.text # text屬性就是html文件原始碼 soup = BeautifulSoup(response.text, "lxml") # 指定lxml作為解析器來建立Beautiful物件 ```