Web Scraping & Data Cleaning
Last Updated: 2022/11/10
王柳鋐
深度學習與自然語言理解
資料收集
資料分析與結果呈現
資料爬蟲
資料爬蟲
資料清理
前處理
文字分類
視覺化
資料分析
資料爬蟲
資料清理
文章
有哪些可能的任務?
情感分析 (Sentiment Analysis)
新聞分類 (News Categorization)
主題分析 (Topic Analysis)
問答 (Question Answering)
自然語言推論 (Natural Language Inference )
自動摘要 (automatic Abstracting)
機器翻譯 (Machine Translation)
自然語言處理 (Natural Language Processing)
新近崛起技術:BERT
Bidirectional Encoder Representations from Transformers
預訓練
特定任務訓練
簡介
爬取網頁(連結) ➠ 製作網頁索引
Web crawling
應用:搜尋引擎
爬取網頁內容 ➠ 轉成結構化資料
Web scraping
目的:Make smarter decisions
價格追蹤
商品評價
潛在客戶
圖片來源1
圖片來源2
2.1 遍訪連結
建立待訪堆疊
1. 從一個網頁開始
2.2 擷取內容
3. 輸出結果(至檔案)
圖片來源1
圖片來源2
取得HTML回應碼
解析超連結
解析其他HTML元素
資料輸出
資料清理
能力1: 取得HTML回應碼?了解HTTP協定
網址(http request)
瀏覽器
Web 伺服器
HTML回應碼(http response)
能力2: 解析超連結、其他HTML元素、網頁內容 ➠
HTML Parser(解析器)
符號化
建立DOM樹
以p元素為例
元素
內容
開始標籤
結束標籤
屬性名稱
屬性值
找出超連結元素A,屬性href的值 ➠ 待訪網址
【任務】建立待訪網址串列:
1.找到所有超連結元素A,讀取屬性href的值
2.讀取該A元素的內容(顯示於網頁的文字)
<div id="mw-pages">
<h2>類別”台灣球員“中的頁面</h2>
<p>這個分類中有以下的 200 個頁面,共有 15,436 個頁面。</p>(先前200) (
<a href="/wiki/index.php?title=%E5%88%86%E9%A1%9E:%E5%8F%B0%E7%81%A3%E7%90%83%E5%93%A1"
title="分類:台灣球員">之後200</a>)
<div lang="zh-tw" dir="ltr" class="mw-content-ltr">
<table style="width: 100%;">
<tr style="vertical-align: top;">
<td style="width: 33.3%;">
<h3>丁</h3>
<ul>
<li><a href="/wiki/index.php/%E4%B8%81%E4%B8%96%E5%81%89" title="丁世偉">丁世偉</a>
</li>
<li><a href="/wiki/index.php/%E4%B8%81%E4%B8%96%E6%9D%B0" title="丁世杰">丁世杰</a>
</li>
</ul>
<h3>丘</h3>
<ul>
<li><a href="/wiki/index.php/%E4%B8%98%E6%98%8C%E6%A6%AE" title="丘昌榮">丘昌榮</a>
</li>
</ul>
<h3>中</h3>
</td>
</tr>
</table>
</div>
</div>
內文:之後200 網址:http://twbsball.dils.tku.edu.tw/wiki/index.php/%E5%88%86%E9%A1%9E:....
球員:丁世偉 網址:http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%B8%81%E4%B8%96%E5%81%89
球員:丁世杰 網址:http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%B8%81%E4%B8%96%E6%9D%B0
球員:丘昌榮 網址:http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%B8%98%E6%98%8C%E6%A6%AE
【任務】建立待訪網址串列:
1.找到所有超連結元素A,讀取屬性href的值
2.讀取該A元素的內容(顯示於網頁的文字)
...
<a href="/wiki/index.php/%E4%B8%98%E6%98%8C%E6%A6%AE" title="丘昌榮">丘昌榮</a>
...
球員:丘昌榮 網址:http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%B8%98%E6%98%8C%E6%A6%AE
【問題】
1. 超連結內文:語意不同
2. href有「絕對」、「相對」路徑之分
相對路徑, 不完整
補成絕對路徑
能力2: 解析超連結、其他HTML元素、網頁內容
Parser(解析器)
能力1: 取得HTML回應碼?了解HTTP協定
http連線功能
➠
圖片來源1
能力3: 資料清理、資料輸出 ➠
資料處理工具
2.1 遍訪連結
建立待訪堆疊
1. 從一個網頁開始
2.2 擷取內容
3. 輸出結果(至檔案)
圖片來源1
圖片來源2
取得HTML回應碼
解析超連結
解析其他HTML元素
資料輸出
資料清理
☛ 單一頁面版:使用 Requests + BeautilfulSoup4
無限循環版:使用Scraper
pip install requests
pip install beautifulsoup4
pip install lxml
安裝Requests, BeautilfulSoup套件(使用pip 或 pip3)
parser部分使用lxml(速度快,也可解析XML檔)
若系統找不到pip,改用pip3
import requests
from bs4 import BeautifulSoup
# Step 1: 發出Http Request, 解析(parse)Http Response
## url: 要擷取的網址
base_url = "http://twbsball.dils.tku.edu.tw" # 網站基礎網址,補絕對網址用
url = "http://twbsball.dils.tku.edu.tw/wiki/index.php/%E5%88%86%E9%A1%9E:%E5%8F%B0%E7%81%A3%E7%90%83%E5%93%A1"
resp = requests.get(url) # 發出HTTP request
if resp.status_code == 200: # 代碼200表示連線正常
soup = BeautifulSoup(resp.content, "lxml") # parsing網頁內容
else:
exit(1) # 非200代碼,程式直接結束
### 維基百科頁面標題特徵: id="firstHeading"
title = soup.find(id="firstHeading") # 根據id值進行搜尋
print(f'頁面標題{title.text}')
### 維基百科頁面內容:假設只對包含在id="mw-pages"內的超連結感興趣
### 注意!!!每個網頁內容的id可能都不一樣!!!必須先詳細了解網頁格式
all_links = soup.find(id="mw-pages").find_all("a") # 找出所有<A>標籤
print(f"超連結數量:{len(all_links):10}")
# 走訪過濾後的<A>標籤:
#### 1. 只對此Wiki站內連結感興趣
#### 2. 只處理球員連結
num_of_link = 0
for link in all_links:
if link['href'].find("/wiki/") == -1: # 站外連結跳過不處理
continue
if link.text == '之後200': # 網頁特定內容,跳過不處理
print(f'type of link:{type(link)}')
continue
print(f"球員:{link.text} 網址:{base_url + link['href']}")
num_of_link += 1
print(f'球員總數{num_of_link:10}')
webscraping-link.py
重要:必須先了解該網頁的HTML結構
重點觀察
前往目標網址後,開啟「開發人員工具」(F12)
任務 1: 爬取「球員連結」
但不是「所有超連結」都是球員
移動游標尋找目標區域
div標籤, id值 "mw-pages"
# 以'lxml' parser解析 resp.content的內容
soup = BeautifulSoup(resp.content, "lxml")
...
# 找出mw-pages段落裡面所有<A>標籤
all_links = soup.find(id="mw-pages").find_all("a")
任務 2: 擷取「頁面內容」
有興趣的段落沒有 id, class等屬性值可用
dl, dd, ul, li等標籤無法區分是否為感興趣的段落
url = "擷取的網址"
resp = requests.get(url) # (1)連線
if resp.status_code == 200: # 代碼200表示連線正常
soup = BeautifulSoup(resp.content, "lxml") # (2)分析
else:
exit(1) # 非200代碼,程式直接結束
import requests
from bs4 import BeautifulSoup
import需要的模組
連線與分析(parsing)
### 維基百科頁面標題特徵: id="firstHeading"
title = soup.find(id="firstHeading") # 根據id值進行搜尋
print(f'頁面標題{title.text}')
擷取任務1: 取得標題
### 維基百科頁面內容:假設只對包含在id="mw-pages"內的超連結感興趣
### 注意!!!每個網頁內容的id可能都不一樣!!!必須先詳細了解網頁格式
all_links = soup.find(id="mw-pages").find_all("a") #找出所有<A>標籤
# 走訪過濾後的<A>標籤:
for link in all_links:
if link['href'].find("/wiki/") == -1: # 站外連結跳過不處理
continue
if link.text == '之後200': # 網頁特定內容,跳過不處理
continue
print(f"球員:{link.text} 網址:{link['href']}") # 印出網址
擷取任務2: 取出段落中的超連結
webscraping-player.py
from bs4 import BeautifulSoup
import requests
import pandas
sections = [ # data frame的column name,共9個
'簡介', # 同義詞段落'生平簡介'
'基本資料',
'經歷',
'個人年表',
'特殊事蹟',
'職棒生涯成績',
'備註',
'註釋或參考文獻',
'外部連結',
]
sid = { # 台灣棒球維基館的section id
'基本資料': '.E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99',
'經歷': '.E7.B6.93.E6.AD.B7',
'個人年表': '.E5.80.8B.E4.BA.BA.E5.B9.B4.E8.A1.A8',
'特殊事蹟': '.E7.89.B9.E6.AE.8A.E4.BA.8B.E8.B9.9F',
'職棒生涯成績':'.E8.81.B7.E6.A3.92.E7.94.9F.E6.B6.AF.E6.88.90.E7.B8.BE',
'備註':'.E5.82.99.E8.A8.BB',
'註釋或參考文獻': '.E8.A8.BB.E9.87.8B.E6.88.96.E5.8F.83.E8.80.83.E6.96.87.E7.8D.BB',
'外部連結': '.E5.A4.96.E9.83.A8.E9.80.A3.E7.B5.90',
'結尾': 'stub'
}
intro = {
'簡介': '.E7.B0.A1.E4.BB.8B',
'生平簡介': '.E7.94.9F.E5.B9.B3.E7.B0.A1.E4.BB.8B'
}
def extract_introduction(content, key, value):
pos = str(content).find(f'id="{value}"') # 簡介 / 生平簡介
if pos == -1:
return None
else:
next_pos = str(content).find('<span class="mw-headline" id=".E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99">基本資料</span>')
intro_str = str(content)[pos:next_pos] # 擷取 「簡介/生平簡介」 段落
# print('*****',key, len(key))
intro_str = intro_str[intro_str.find(key):] # 去掉最前面未切乾淨的html碼
# print(intro_str)
intro_soup = BeautifulSoup(intro_str, 'lxml') # parsing
texts = intro_soup.find_all(text=True) # 取出所有元素的文字,不含標籤屬性
# 資料格式為串列
return u"".join(t.strip() for t in texts) # 將串列元素接在一起形單一字串,u""為unicode
####使用字串.find()和取得子字串的功能,切出每個段落大致的內容
# 參數:
# content: http response回應的內容部份
# start: 開始字串(不含),要裁切內容的開始位置
# end: 結尾字串(不含), 裁切內容的結束位置
# 回傳:
# 裁切好的子字串
def get_sub_content(content, start, end):
start_pos = str(content).find(f'id="{start}"')
print(f'sp:{start_pos}')
if start_pos == -1: # 找不到start字串
return None
else: # 找到 start字串
end_pos = str(content).find(f'id="{end}"') # 尋找end字串
if end_pos == -1:
return None # 找不到end字串
else:
print(f'ep:{end_pos}')
return str(content)[start_pos: end_pos] # 回傳裁切子字串
#### 使用BeatuifulSoup的功能,取出以<dd></dd>包起來的段落內容
# 參數:
# html_str: 要分析的段落內容字串
# 回傳:
# 段落內容串列,每一個li為一個串列元素
def extract_dlist(html_str):
soup = BeautifulSoup(html_str, 'lxml') # parsing
li = soup.find_all('li') # 找到<li>
li_texts = [] # 空字串, 將存入所有li的文字
for item in li:
item_text = item.find_all(text=True)
if u"".join(item_text).strip() != '':
li_texts.append(u"".join(item_text).strip())
return li_texts
# Step 1: 發出Http Request, 解析(parse)Http Response
## url: 要擷取的網址
base_url = "http://twbsball.dils.tku.edu.tw"
url = "http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%BD%95%E9%9C%87%E7%83%8A"
url = "http://twbsball.dils.tku.edu.tw/wiki/index.php/%E7%8E%8B%E5%BB%BA%E6%B0%91(1980)"
resp = requests.get(url) # 發出HTTP request
if resp.status_code == 200:
soup = BeautifulSoup(resp.content, "lxml") # parsing網頁內容
else:
exit(1)
final_result = {} # 最後擷取的全文結果
all_content = soup.find(id="bodyContent") # 取得全部HTML內文
for key, value in intro.items():
pp = extract_introduction(all_content, key, value) # 取得 簡介
if pp != None:
print(f'找到{key}')
final_result[key] = pp # 簡介
break
else:
print(f'找不到{key}')
start_str = '' # 擷取段落用的開始字串
end_str = '' # 擷取段落用的結束字串
title = '' # 目前擷取的段落名稱:對應至sid的key
next_title = '' # 下一個擷取的段落名稱:對應至sid的key
for key, value in sid.items(): # 遍訪sid串列,取出定位字串
if start_str == '': # 1. 設定開始字串
start_str = value # 1.1開始字串
title = key # 1.2段落名稱
continue
else:
if end_str == '': # 2. 設定結束字串
end_str = value # 2.1 結束字串
next_title = key # 2.2 下一個段落名稱
print(title)
# 使用開始字串,結束字串取得中間的html內容
sub_content_str = get_sub_content(all_content, start_str, end_str)
if sub_content_str == None: # 如果擷取回來沒有內容
end_str = '' # 清除結束字串,繼續處理下一個sid項目
continue
# 處理擷取回來的段落內容
li_list = extract_dlist(sub_content_str) # 取出當中的li
final_result[title] = ''.join(li_list) # 紀錄起來
start_str = end_str # 設定下一段的開始字串
end_str = '' # 清除結束字串
title =next_title # 設定下一段的段落名稱
# print(final_result) # 印出擷取的資料
fr_array = [] # 準備空白表格資料
fr_array.append(final_result) # 加入一筆資料至表格
index=[i for i in range(len(fr_array))] # data frame的索引
### 建立dataframe
# 1. fr_array: 表格資料(目前只有一筆)
# 2. index=index: 前者是參數名稱,後者是第9行建立的索引串列
# 3. columns=sections: 前者是參數名稱, 後者是先前定義的欄位串列
df = pandas.DataFrame(fr_array,index=index, columns=sections)
df.to_csv('out_file.csv') # 寫出檔案(utf-8編碼)
裁切子字串的函式:指定開始、結束字串,切出中間的內容
####使用字串.find()和取得子字串的功能,切出每個段落大致的內容
# 參數:
# content: http response回應的內容部份
# start: 開始字串(不含),要裁切內容的開始位置
# end: 結尾字串(不含), 裁切內容的結束位置
# 回傳:
# 裁切好的子字串
def get_sub_content(content, start, end):
start_pos = str(content).find(f'id="{start}"')
print(f'sp:{start_pos}')
if start_pos == -1: # 找不到start字串
return None
else: # 找到 start字串
end_pos = str(content).find(f'id="{end}"') # 尋找end字串
if end_pos == -1:
return None # 找不到end字串
else:
print(f'ep:{end_pos}')
return str(content)[start_pos: end_pos] # 回傳裁切子字串
開始字串、結束字串包括:
sid = { # 台灣棒球維基館的section id
'基本資料': '.E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99',
'經歷': '.E7.B6.93.E6.AD.B7',
'個人年表': '.E5.80.8B.E4.BA.BA.E5.B9.B4.E8.A1.A8',
'特殊事蹟': '.E7.89.B9.E6.AE.8A.E4.BA.8B.E8.B9.9F',
'職棒生涯成績':'.E8.81.B7.E6.A3.92.E7.94.9F.E6.B6.AF.E6.88.90.E7.B8.BE',
'備註':'.E5.82.99.E8.A8.BB',
'註釋或參考文獻': '.E8.A8.BB.E9.87.8B.E6.88.96.E5.8F.83.E8.80.83.E6.96.87.E7.8D.BB',
'外部連結': '.E5.A4.96.E9.83.A8.E9.80.A3.E7.B5.90',
'結尾': 'stub'
}
開始字串、結束字串段落內都是<dd></dd>, 找出當中的<li>
#### 使用BeatuifulSoup的功能,取出以<dd></dd>包起來的段落內容
# 參數:
# html_str: 要分析的段落內容字串
# 回傳:
# 段落內容串列,每一個li為一個串列元素
def extract_dlist(html_str):
soup = BeautifulSoup(html_str, 'lxml') # parsing
li = soup.find_all('li') # 找到<li>
li_texts = [] # 空字串, 將存入所有li的文字
for item in li:
item_text = item.find_all(text=True) # 只要內文
if u"".join(item_text).strip() != '':
li_texts.append(u"".join(item_text).strip())
return li_texts
li的內文才是要擷取的主體內容
球員簡介段落的格式與其他段落不同
def extract_introduction(content, value):
pos = str(content).find(f'id="{value}"') # 簡介 / 生平簡介
if pos == -1:
return None
else:
next_pos = str(content).find('<span class="mw-headline" id=".E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99">基本資料</span>')
intro_str = str(content)[pos:next_pos] # 擷取 「簡介/生平簡介」 段落
# print('*****',key, len(key))
intro_str = intro_str[intro_str.find(key):] # 去掉最前面未切乾淨的html碼
# print(intro_str)
intro_soup = BeautifulSoup(intro_str, 'lxml') # parsing
texts = intro_soup.find_all(text=True) # 取出所有元素的文字,不含標籤屬性
# 資料格式為串列
return u"".join(t.strip() for t in texts) # 將串列元素接在一起形單一字串,u""為unicode
intro = {
'簡介': '.E7.B0.A1.E4.BB.8B',
'生平簡介': '.E7.94.9F.E5.B9.B3.E7.B0.A1.E4.BB.8B'
}
簡介段落有下列兩種網頁格式
soup = BeautifulSoup(resp.content, "lxml")
all_content = soup.find(id="bodyContent") # 取得全部HTML內文
start_str = '' # 擷取段落用的開始字串
end_str = '' # 擷取段落用的結束字串
title = '' # 目前擷取的段落名稱:對應至sid的key
next_title = '' # 下一個擷取的段落名稱:對應至sid的key
final_result = {}
for key, value in sid.items(): # 遍訪sid串列,取出定位字串
if start_str == '': # 1. 設定開始字串
start_str = value # 1.1開始字串
title = key # 1.2段落名稱
continue
else:
if end_str == '': # 2. 設定結束字串
end_str = value # 2.1 結束字串
next_title = key # 2.2 下一個段落名稱
print(title)
# 使用開始字串,結束字串取得中間的html內容
sub_content_str = get_sub_content(all_content, start_str, end_str)
if sub_content_str == None: # 如果擷取回來沒有內容
end_str = '' # 清除結束字串,繼續處理下一個sid項目
continue
# 處理擷取回來的段落內容
li_list = extract_dlist(sub_content_str) # 取出當中的li
final_result[title] = li_list # 紀錄起來
start_str = end_str # 設定下一段的開始字串
end_str = '' # 清除結束字串
title =next_title # 設定下一段的段落名稱
print(final_result) # 印出擷取的資料
取得各段落內文
單一頁面版:使用 Requests + BeautilfulSoup4
☛ 無限循環版:使用Scrapy
安裝Scrapy, BeautilfulSoup套件(使用pip 或 pip3)
parser部分使用lxml(速度快,也可解析XML檔)
pip install scrapy
pip install beautifulsoup4
pip install lxml
若系統找不到pip,改用pip3
此範例使用
效能較佳!
建立Scrapy專案,例如:
scrapy startproject tutorial
你的程式加在這個資料夾內
scrapy startproject mycrawler
在這個資料夾內新增
twbaseball_spider.py(任何檔名皆可)
爬蟲主程式#1
資料夾內可以手動新增多隻爬蟲主程式
import scrapy
class TwBaseballSpider(scrapy.Spider): # 1. 繼承scrapy.Spider
name = "quotes" # 2. 為spider取名
start_urls = [ # 3. 頁面擷取串列: 數量不限
'https://quotes.toscrape.com/page/1/', # 爬取網址1
'https://quotes.toscrape.com/page/2/', # 爬取網址2
]
def parse(self, response): # 4. 頁面解析
page = response.url.split("/")[-2] # 4-1 內容解析
# 4-2 資料儲存
filename = f'quotes-{page}.html' # 設定檔名
with open(filename, 'wb') as f:
f.write(response.body) # 寫入網頁<body>內容
self.log(f'Saved file {filename}')
twbaseball_spider.py
爬取兩個網頁,將網頁內容寫入檔案
import scrapy
class TwBaseballSpider(scrapy.Spider): # 1. 繼承scrapy.Spider
name = "quotes" # 2. 為spider取名
start_urls = [ # 3. 頁面擷取串列: 數量不限
'https://quotes.toscrape.com/page/1/', # 爬取網址1
'https://quotes.toscrape.com/page/2/', # 爬取網址2
]
def parse(self, response): # 4. 頁面解析
page = response.url.split("/")[-2] # 4-1 內容解析
# 4-2 資料儲存
filename = f'quotes-{page}.html' # 設定檔名
with open(filename, 'wb') as f:
f.write(response.body) # 寫入網頁<body>內容
self.log(f'Saved file {filename}')
twbaseball_spider.py
class名稱自取
import scrapy
class TwBaseballSpider(scrapy.Spider): # 1. 繼承scrapy.Spider
name = "quotes" # 2. 為spider取名
start_urls = [ # 3. 頁面擷取串列: 數量不限
'https://quotes.toscrape.com/page/1/', # 爬取網址1
'https://quotes.toscrape.com/page/2/', # 爬取網址2
]
def parse(self, response): # 4. 頁面解析
page = response.url.split("/")[-2] # 4-1 內容解析
# 4-2 資料儲存
filename = f'quotes-{page}.html' # 設定檔名
with open(filename, 'wb') as f:
f.write(response.body) # 寫入網頁<body>內容
self.log(f'Saved file {filename}')
twbaseball_spider.py
為spider取名
執行程式時,會指定此名稱
import scrapy
class TwBaseballSpider(scrapy.Spider): # 1. 繼承scrapy.Spider
name = "quotes" # 2. 為spider取名
start_urls = [ # 3. 頁面擷取串列: 數量不限
'https://quotes.toscrape.com/page/1/', # 爬取網址1
'https://quotes.toscrape.com/page/2/', # 爬取網址2
]
def parse(self, response): # 4. 頁面解析
page = response.url.split("/")[-2] # 4-1 內容解析
# 4-2 資料儲存
filename = f'quotes-{page}.html' # 設定檔名
with open(filename, 'wb') as f:
f.write(response.body) # 寫入網頁<body>內容
self.log(f'Saved file {filename}')
twbaseball_spider.py
執行時,依序爬取網頁
但不限於只爬取串列中指定的
網頁
可遞迴呼叫「回呼函式」parse()
達成無限循環
import scrapy
class TwBaseballSpider(scrapy.Spider): # 1. 繼承scrapy.Spider
name = "quotes" # 2. 為spider取名
start_urls = [ # 3. 頁面擷取串列: 數量不限
'https://quotes.toscrape.com/page/1/', # 爬取網址1
'https://quotes.toscrape.com/page/2/', # 爬取網址2
]
def parse(self, response): # 4. 頁面解析
page = response.url.split("/")[-2] # 4-1 內容解析
# 4-2 資料儲存
filename = f'quotes-{page}.html' # 設定檔名
with open(filename, 'wb') as f:
f.write(response.body) # 寫入網頁<body>內容
self.log(f'Saved file {filename}')
twbaseball_spider.py
回呼函式
透過response參數解析內容
繼續跟進處理其他網址
回傳擷取的資料、儲存...
cd mycrawler
從「終端機」執行
1. 切換到專案資料最上層!
scrapy crawl quotes
scrapy crawl twbaseball
2. 執行scrapy crawl指令
spider名稱,自訂於程式碼中
import scrapy
class TwBaseballSpider(scrapy.Spider): # 1. 繼承scrapy.Spider
name = "quotes" # 2. 為spider取名
start_urls = [ # 3. 頁面擷取串列: 數量不限
'https://quotes.toscrape.com/page/1/', # 爬取網址1
'https://quotes.toscrape.com/page/2/', # 爬取網址2
]
def parse(self, response): # 4. 頁面解析
page = response.url.split("/")[-2] # 4-1 內容解析
# 4-2 資料儲存
filename = f'quotes-{page}.html' # 設定檔名
with open(filename, 'wb') as f:
f.write(response.body) # 寫入網頁<body>內容
self.log(f'Saved file {filename}')
import scrapy
from bs4 import BeautifulSoup
import random
import mycrawler.items as items
class TwBaseballSpider(scrapy.Spider): # 1. 繼承scrapy.Spider
name = "twbaseball" # 2. 為spider命名
allowed_domains = ['dils.tku.edu.tw']
start_urls = [ # 3. 頁面擷取串列
'http://twbsball.dils.tku.edu.tw/wiki/index.php/%E5%88%86%E9%A1%9E:%E5%8F%B0%E7%81%A3%E7%90%83%E5%93%A1'
]
total_page = len(start_urls) # 分頁總數
num_of_player = 0 # 抓取的球員總數
player = items.Player() # 球員資料欄位->自行定義在items.py
sid = { # 台灣棒球維基館的section id
'簡介': '.E7.B0.A1.E4.BB.8B',
'基本資料': '.E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99',
'經歷': '.E7.B6.93.E6.AD.B7',
'個人年表': '.E5.80.8B.E4.BA.BA.E5.B9.B4.E8.A1.A8',
'特殊事蹟': '.E7.89.B9.E6.AE.8A.E4.BA.8B.E8.B9.9F',
'職棒生涯成績': '.E8.81.B7.E6.A3.92.E7.94.9F.E6.B6.AF.E6.88.90.E7.B8.BE',
'外部連結': '.E5.A4.96.E9.83.A8.E9.80.A3.E7.B5.90'
}
def parse(self, response): # 4. 頁面解析
next200 = False # pagination link processing flag
player_links = [] # 球員超連結串列
for link in response.css('#mw-pages').css("a"):
text = link.css('a::text').get() # 超連結的文字部分
if text == '先前200': # 先前200已經處理過了
continue
if text == '之後200': # 之後200 下一頁球員
if next200: # 已經處理過了
continue
else: # 尚未處理
next200 = True # 標示為已處理
self.total_page += 1
# yield response.follow(link, self.parse) # 連續循環版
print(f"^^^current total pages:{self.total_page}")
if random.randint(1,100)<=2:
player_links.append(link)
self.num_of_player += len(player_links)
yield from response.follow_all(player_links, self.parse_player)
print(f"***current total number of players:{self.num_of_player}")
def parse_player(self, response):
player = items.Player() # 新球員資料
soup = BeautifulSoup(response.body, 'lxml')
# 0. 球員姓名
player['name'] = soup.find(id="firstHeading").text
yield {
'title': player['name'],
}
twbaseball_spider.py
items.py:可以定義「想要擷取的欄位」
# Define here the models for your scraped items
from re import S
import scrapy
class MycrawlerItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
pass
class Player(scrapy.Item): # 球員資料
url = scrapy.Field() # 網址
name = scrapy.Field() # 球員姓名
nickname = scrapy.Field() # 綽號
birthday = scrapy.Field() # 生日
height = scrapy.Field() # 身高
weight = scrapy.Field() # 體重
position = scrapy.Field() # 守備位置
intro = scrapy.Field() # 簡介全文
experience = scrapy.Field() # 經歷
event = scrapy.Field() # 年表
performance = scrapy.Field() # 特殊事蹟
record = scrapy.Field() # 成績紀錄
links = scrapy.Field() # 外部連結
items.py
參考這邊的寫法
import scrapy
from bs4 import BeautifulSoup
import random
import mycrawler.items as items
class TwBaseballSpider(scrapy.Spider): # 1. 繼承scrapy.Spider
name = "twbaseball" # 2. 為spider命名
allowed_domains = ['dils.tku.edu.tw']
start_urls = [ # 3. 頁面擷取串列
'http://twbsball.dils.tku.edu.tw/wiki/index.php/%E5%88%86%E9%A1%9E:%E5%8F%B0%E7%81%A3%E7%90%83%E5%93%A1'
]
....
twbaseball_spider.py(部份)
所有spider都需要這三部份
twbaseball_spider.py(部份)
...
class TwBaseballSpider(scrapy.Spider): # 1. 繼承scrapy.Spider
...
def parse(self, response): # 4. 頁面解析
next200 = False # pagination link processing flag
player_links = [] # 球員超連結串列
for link in response.css('#mw-pages').css("a"):
text = link.css('a::text').get() # 超連結的文字部分
if text == '先前200': # 先前200已經處理過了
continue
if text == '之後200': # 之後200 下一頁球員
if next200: # 已經處理過了
continue
else: # 尚未處理
next200 = True # 標示為已處理
self.total_page += 1
# yield response.follow(link, self.parse) # 連續循環版
print(f"^^^current total pages:{self.total_page}")
if random.randint(1,100)<=2:
player_links.append(link)
self.num_of_player += len(player_links)
yield from response.follow_all(player_links, self.parse_player)
print(f"***current total number of players:{self.num_of_player}")
...
主要的回呼函式
無限循環? 透過遞迴達成!
...
yield from response.follow_all(player_links, self.parse_player)
...
....
yield response.follow(link, self.parse) # 連續循環版
....
連往下一個超連結link,並呼叫parse()回呼函式
yield: 生成器,類似return,但可節省許多記憶體
連往player_links內所有超連結,並呼叫parse_player()回呼函式
yield from:產生許多生成器
➊ 用不到、不相關
➋ 資料闕漏
刪除一筆、刪除一欄、補遺
刪除欄位
➌ 資料型別錯誤
重填正確型別資料
讀取檔案
Web
●●●
建立資料結構
資料清理
輸出
●●●
資料庫
一張表格
欄位名稱
有無索引
資料闕漏
1. 讀寫檔案
2. 資料清理
pip install pandas
pip install numpy
from bs4 import BeautifulSoup
import requests
import pandas
sections = [ # data frame的column name,共9個
'簡介', # 同義詞段落'生平簡介'
'基本資料',
'經歷',
'個人年表',
'特殊事蹟',
'職棒生涯成績',
'備註',
'註釋或參考文獻',
'外部連結',
]
sid = { # 台灣棒球維基館的section id
'基本資料': '.E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99',
'經歷': '.E7.B6.93.E6.AD.B7',
'個人年表': '.E5.80.8B.E4.BA.BA.E5.B9.B4.E8.A1.A8',
'特殊事蹟': '.E7.89.B9.E6.AE.8A.E4.BA.8B.E8.B9.9F',
'職棒生涯成績':'.E8.81.B7.E6.A3.92.E7.94.9F.E6.B6.AF.E6.88.90.E7.B8.BE',
'備註':'.E5.82.99.E8.A8.BB',
'註釋或參考文獻': '.E8.A8.BB.E9.87.8B.E6.88.96.E5.8F.83.E8.80.83.E6.96.87.E7.8D.BB',
'外部連結': '.E5.A4.96.E9.83.A8.E9.80.A3.E7.B5.90',
'結尾': 'stub'
}
intro = {
'簡介': '.E7.B0.A1.E4.BB.8B',
'生平簡介': '.E7.94.9F.E5.B9.B3.E7.B0.A1.E4.BB.8B'
}
def extract_introduction(content, key, value):
pos = str(content).find(f'id="{value}"') # 簡介 / 生平簡介
if pos == -1:
return None
else:
next_pos = str(content).find('<span class="mw-headline" id=".E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99">基本資料</span>')
intro_str = str(content)[pos:next_pos] # 擷取 「簡介/生平簡介」 段落
# print('*****',key, len(key))
intro_str = intro_str[intro_str.find(key):] # 去掉最前面未切乾淨的html碼
# print(intro_str)
intro_soup = BeautifulSoup(intro_str, 'lxml') # parsing
texts = intro_soup.find_all(text=True) # 取出所有元素的文字,不含標籤屬性
# 資料格式為串列
return u"".join(t.strip() for t in texts) # 將串列元素接在一起形單一字串,u""為unicode
####使用字串.find()和取得子字串的功能,切出每個段落大致的內容
# 參數:
# content: http response回應的內容部份
# start: 開始字串(不含),要裁切內容的開始位置
# end: 結尾字串(不含), 裁切內容的結束位置
# 回傳:
# 裁切好的子字串
def get_sub_content(content, start, end):
start_pos = str(content).find(f'id="{start}"')
print(f'sp:{start_pos}')
if start_pos == -1: # 找不到start字串
return None
else: # 找到 start字串
end_pos = str(content).find(f'id="{end}"') # 尋找end字串
if end_pos == -1:
return None # 找不到end字串
else:
print(f'ep:{end_pos}')
return str(content)[start_pos: end_pos] # 回傳裁切子字串
#### 使用BeatuifulSoup的功能,取出以<dd></dd>包起來的段落內容
# 參數:
# html_str: 要分析的段落內容字串
# 回傳:
# 段落內容串列,每一個li為一個串列元素
def extract_dlist(html_str):
soup = BeautifulSoup(html_str, 'lxml') # parsing
li = soup.find_all('li') # 找到<li>
li_texts = [] # 空字串, 將存入所有li的文字
for item in li:
item_text = item.find_all(text=True)
if u"".join(item_text).strip() != '':
li_texts.append(u"".join(item_text).strip())
return li_texts
# Step 1: 發出Http Request, 解析(parse)Http Response
## url: 要擷取的網址
base_url = "http://twbsball.dils.tku.edu.tw"
url = "http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%BD%95%E9%9C%87%E7%83%8A"
url = "http://twbsball.dils.tku.edu.tw/wiki/index.php/%E7%8E%8B%E5%BB%BA%E6%B0%91(1980)"
resp = requests.get(url) # 發出HTTP request
if resp.status_code == 200:
soup = BeautifulSoup(resp.content, "lxml") # parsing網頁內容
else:
exit(1)
final_result = {} # 最後擷取的全文結果
all_content = soup.find(id="bodyContent") # 取得全部HTML內文
for key, value in intro.items():
pp = extract_introduction(all_content, key, value) # 取得 簡介
if pp != None:
print(f'找到{key}')
final_result[key] = pp # 簡介
break
else:
print(f'找不到{key}')
start_str = '' # 擷取段落用的開始字串
end_str = '' # 擷取段落用的結束字串
title = '' # 目前擷取的段落名稱:對應至sid的key
next_title = '' # 下一個擷取的段落名稱:對應至sid的key
for key, value in sid.items(): # 遍訪sid串列,取出定位字串
if start_str == '': # 1. 設定開始字串
start_str = value # 1.1開始字串
title = key # 1.2段落名稱
continue
else:
if end_str == '': # 2. 設定結束字串
end_str = value # 2.1 結束字串
next_title = key # 2.2 下一個段落名稱
print(title)
# 使用開始字串,結束字串取得中間的html內容
sub_content_str = get_sub_content(all_content, start_str, end_str)
if sub_content_str == None: # 如果擷取回來沒有內容
end_str = '' # 清除結束字串,繼續處理下一個sid項目
continue
# 處理擷取回來的段落內容
li_list = extract_dlist(sub_content_str) # 取出當中的li
final_result[title] = ''.join(li_list) # 紀錄起來
start_str = end_str # 設定下一段的開始字串
end_str = '' # 清除結束字串
title =next_title # 設定下一段的段落名稱
# print(final_result) # 印出擷取的資料
fr_array = [] # 準備空白表格資料
fr_array.append(final_result) # 加入一筆資料至表格
index=[i for i in range(len(fr_array))] # data frame的索引
### 建立dataframe
# 1. fr_array: 表格資料(目前只有一筆)
# 2. index=index: 前者是參數名稱,後者是第9行建立的索引串列
# 3. columns=sections: 前者是參數名稱, 後者是先前定義的欄位串列
df = pandas.DataFrame(fr_array,index=index, columns=sections)
df.to_csv('out_file.csv') # 寫出檔案(utf-8編碼)
webscraping-player.py
from bs4 import BeautifulSoup
import requests
import pandas
....
# print(final_result) # 印出擷取的資料
fr_array = [] # 準備空白表格資料
fr_array.append(final_result) # 加入一筆資料至表格
index=[i for i in range(len(fr_array))] # data frame的索引
### 建立dataframe
# 1. fr_array: 表格資料(目前只有一筆)
# 2. index=index: 前者是參數名稱,後者是第9行建立的索引串列
# 3. columns=sections: 前者是參數名稱, 後者是先前定義的欄位串列
df = pandas.DataFrame(fr_array,index=index, columns=sections)
df.to_csv('out_file.csv') # 寫出檔案(utf-8編碼)
webscraping-player.py(部份)
#-*-coding:UTF-8 -*-
# 讀取馬偕日記csv檔,使用CKIP斷詞、實體辨識進行處理, 並作統計
# Import Ckip Transformers module
from ckip_transformers.nlp import CkipWordSegmenter, CkipPosTagger, CkipNerChunker
import pandas as pd
def read_file(fn):
df = pd.read_csv(fn,names=['date','content']) # 日記無header,自訂header為'date','content'
df['content'] = df['content'].str.strip() # 去除日記內容頭尾的空白字元
print(df.info())
return df
# 步驟一: 讀取馬偕日記
data_file = './data/MackayFull-202209-utf8.csv'
out_file = './data/MackayFull-202209-Output-utf8.csv'
df = read_file(data_file)
# print(df)
# 步驟二: load models
ws_driver = CkipWordSegmenter(model="bert-base")
print('word segmenter loaded...')
pos_driver = CkipPosTagger(model="bert-base")
print('Pos Tagger loaded...')
ner_driver = CkipNerChunker(model="bert-base")
df['word_segment'] = ws_driver(df['content']) # 斷詞
df['part_of_speech'] = pos_driver(df['content']) # 詞性標記
df['ner_chunker'] = ner_driver(df['content']) # 實體辨識
# 合併 斷詞 於 詞性標記 的結果
sub_df = df[['word_segment','part_of_speech']] # 取出斷詞、詞性標記
word_with_pos = list()
for index in range(sub_df.shape[0]): # 每一筆
print('處理第 %d 筆' % index)
ws = sub_df['word_segment'][index] # 斷詞結果
pos = sub_df['part_of_speech'][index] # 詞性標記結果
count = 0 # 指標 for part_of_speech
words = list()
for word in ws: # 每一筆
# print(word, pos[count], sep=' ')
words.append([word, pos[count]])
count += len(word)
word_with_pos.append(words)
df['word_with_pos'] = word_with_pos
print(df['word_with_pos'])
print(df['ner_chunker'])
'''
# 版本1: 只取部分欄位
out_field = ['date','content','word_with_pos','ner_chunker']
out_df=df[out_field]
out_df.to_csv(out_file, sep='\t', encoding='utf-8')
'''
# 版本2: 輸出所有欄位
df.to_csv(out_file, sep='\t', encoding='utf-8')
02Mackay.py
有使用BERT處理文字(需另外安裝)
pip install -U transformers
...
import pandas as pd
def read_file(fn):
# 日記無header,自訂header為'date','content'
df = pd.read_csv(fn,names=['date','content'])
df['content'] = df['content'].str.strip() # 去除頭尾空白字元
print(df.info())
return df
# 步驟一: 讀取馬偕日記
data_file = './data/MackayFull-202209-utf8.csv'
out_file = './data/MackayFull-202209-Output-utf8.csv'
df = read_file(data_file)
# print(df)
...
02Mackay.py(部份)
02Mackay.py(部份)
...
out_file = './data/MackayFull-202209-Output-utf8.csv'
df = read_file(data_file)
...
#...處理日記內容...
...
# 版本1: 只取部分欄位
out_field = ['date','content','word_with_pos','ner_chunker']
out_df=df[out_field]
out_df.to_csv(out_file, sep='\t', encoding='utf-8')
# 版本2: 輸出所有欄位
df.to_csv(out_file, sep='\t', encoding='utf-8')
1. 讀寫檔案
2. 資料清理
columns_to_drop = ['PassengerId', 'SibSp',
'Parch', 'WikiId',
'Name_wiki', 'Age_wiki']
df.drop(columns_to_drop, inplace=True, axis=1)
df.head()
移除這些欄位
.drop() 移除, axis=1 代表columns
.head() 印出前面5筆
import pandas as pd
df = pd.read_csv('data.csv')
df.fillna(130, inplace = True)
print(df.to_string())
以130取代空白值
Q. 該以什麼值取代空白值?
import pandas as pd
df = pd.read_csv('data.csv')
df["Calories"].fillna(130, inplace = True)
import pandas as pd
df = pd.read_csv('data.csv')
df['Date'] = pd.to_datetime(df['Date'])
print(df.to_string())
import pandas as pd
df = pd.read_csv('data.csv')
df.loc[7,'Duration'] = 45
print(df.to_string())
import pandas as pd
df = pd.read_csv('data.csv')
for x in df.index:
if df.loc[x, "Duration"] > 120:
df.loc[x, "Duration"] = 120
print(df.to_string())