C++中全排列函式next_permutation(2)
爬蟲基本流程
1.指定url
url = "https://www.aqistudy.cn/historydata/"
2.UA偽裝、防盜鏈
模擬瀏覽器
headers = {
'user-agent': 'Mozilla / 5.0(Windows NT 10.0;WOW64) AppleWebKit / 537.36(KHTML, likeGecko) Chrome / 94.0.4606.61Safari / 537.36' # UA 偽裝
"Referer": "....." # 防盜鏈
}
3.請求引數的處理
data = { "page":"", "query": "" }
4.發起請求
response = requests.post(url=url, data=data, headers=headers)
# 模擬登入傳送請求
session = reuqests.Session()
session.post(headers=headers,url=url)
5.獲取響應資料
# 網頁原始碼
data =response.text
#圖片資料 二進位制資料
data =response.content
#json資料
data =response.json()
6.持久化儲存
with open("filename","w",enconding="utf-8") as f: f.write(data) # 將資料寫到filename 檔案中 # 二進位制檔案 with open("filename","wb") as f: f.write(data) # 將二進位制資料data寫入filename中
中文亂碼解決
1.檢視原始碼格式:
開啟開發者工具 2、在console 中輸入“document.charset”檢視頁面編碼
2.解決:
如果編碼格式是utf-8:
response = requests.get(url=url, headers=headers)
response.encoding = "utf-8" # 最重要
tree = etree.HTML(response.text)
如果編碼格式是GBK:
response = requests.get(url=url, headers=headers) response.encoding = "GBK" tree = etree.HTML(response.text)
bs4
資料解析的原理
1.例項化一個BeautifulSoup物件,並且將頁面原始碼資料載入到該物件中
2.通過呼叫BeautifulSoup物件中相關的屬性或者方法進行標籤定位和資料提取
環境安裝
pip install bs4
pip install lxml
資料解析的方法和屬性
標籤定位
soup.tagName # 返回的是文件中第一次出現的tagName對應的標籤
soup.find():
find('tagName') # 等同於soup.div
屬性定位:
soup.find('div',class_/id/attr='song')
soup.find_all('tagName'):返回符合要求的所有標籤(列表)
select:
select('某種選擇器(id,class,標籤...選擇器)'),返回的是一個列表。
層級選擇器:
soup.select('.tang > ul > li > a') # >表示的是一個層級
oup.select('.tang > ul a') # 空格表示的多個層級
獲取標籤之間的文字資料:
soup.a.text/string/get_text()
text/get_text() # 可以獲取某一個標籤中所有的文字內容
string:# 只可以獲取該標籤下面直系的文字內容
獲取標籤中屬性值:
soup.a['href']
使用
例項化BeautifulSoup物件
-
將本地的html文件中的資料載入到該物件中
fp = open('./test.html','r',encoding='utf-8') soup = BeautifulSoup(fp,'lxml')
-
將網際網路上獲取的頁面原始碼載入到該物件中
page_text = response.text soup = BeatifulSoup(page_text,'lxml')
案列
import requests
from bs4 import BeautifulSoup
if __name__ == "__main__":
url = "http://www.shicimingju.com/book/sanguoyanyi.html"
headers = {
'user - agent': 'Mozilla / 5.0(Windows NT 10.0;WOW64) AppleWebKit / 537.36(KHTML, likeGecko) Chrome / 94.0.4606.61Safari / 537.36'
}
data = requests.get(url=url, headers=headers)
data.encoding = "utf-8"
soup = BeautifulSoup(data.text, "lxml")
all_title = soup.select(".book-mulu>ul a")
for title in all_title:
url2 = "https://www.shicimingju.com" + title["href"]
res = requests.get(url=url2, headers=headers)
res.encoding = "utf-8"
soup = BeautifulSoup(res.text, "lxml")
title = title.text.split("·")[1]
with open(f"三國/{title}", "w", encoding="utf-8") as f:
f.write(soup.select(".chapter_content")[0].text)
xpath
解析原理
1.例項化一個etree的物件,且需要將被解析的頁面原始碼資料載入到該物件中。
2.呼叫etree物件中的xpath方法結合著xpath表示式實現標籤的定位和內容的捕獲。
環境的安裝
- pip install lxml
例項化一個etree物件
from lxml import etree
1.將本地的html文件中的原始碼資料載入到etree物件中:
etree.parse(filePath)
2.可以將從網際網路上獲取的原始碼資料載入到該物件中
etree.HTML('page_text')
xpath表示式
/:表示的是從根節點開始定位。表示的是一個層級。
//:表示的是多個層級。可以表示從任意位置開始定位。
# 屬性定位
//div[@class='song'] /tag[@attrName="attrValue"]
# 索引定位
//div[@class="song"]/p[3] 索引是從1開始的。
# 取文字
/text() 獲取的是標籤中直系的文字內容
//text() 標籤中非直系的文字內容(所有的文字內容)
# 取屬性
/@attrName ==>img/src
案例
彼岸圖片爬取
import requests
from lxml import etree
if __name__ == "__main__":
url = 'https://pic.netbian.com/4k/index_61.html'
headers = {
'user-agent': 'Mozilla / 5.0(Windows NT 10.0;WOW64) AppleWebKit / 537.36(KHTML, likeGecko) Chrome / 94.0.4606.61Safari / 537.36'
}
response = requests.get(url=url, headers=headers)
response.encoding = "GBK"
tree = etree.HTML(response.text)
img_all_url = tree.xpath('//ul[@class="clearfix"]//img/@src')
for img_url in img_all_url:
url = "https://pic.netbian.com" + img_url
img_content = requests.get(url=url, headers=headers).content
img_name = url.split("/")[-1]
with open(f"彼岸圖/{img_name}","wb") as f:
f.write(img_content)
print("over")
模擬登陸
1.建立session物件:
session = reuqests.Session()
2.使用session物件進行模擬登入post請求的傳送
session.post(headers=headers,url=url)
代理ip
破解封ip這種反爬機制
代理伺服器
作用:
1. 突破自身IP訪問的限制
2.隱藏自身真實的IP
代理ip型別:
http:應用到http協議對應的url中
https:應用到https協議對應的url中
代理ip的匿名度;
透明:伺服器知道該次請求使用代理,也知道請求對應的真實ip
匿名:知道使用了代理,不知道真實ip
高匿:不知道使用了代理,更不知道真實的ip
使用
proxies = {
"http": '49.85.112.173:7890'
}
requests.get(url=url, headers=headers, proxies=proxies, params=params)
例項:
import requests
url = 'https://www.baidu.com/s'
headers = {
"user-agent": "Mozilla / 5.0(Windows NT 10.0;WOW64) AppleWebKit / 537.36(KHTML, likeGecko) Chrome / 94.0.4606.61Safari / 537.36"
}
proxies = {
"http": '49.85.112.173:7890'
}
wd = input("請輸入搜尋的內容:")
params = {
'ie': 'utf-8',
'f': '8',
'rsv_bp': 1,
'rsv_idx': 1,
'tn': 'baidu',
'wd': wd,
}
response = requests.get(url=url, headers=headers, proxies=proxies, params=params).text
print(response)
print("over")
高效能非同步爬蟲
在爬蟲中使用非同步實現高效能的資料爬取操作
多執行緒,多程序(不建議)
好處:可以為相關堵塞的操作單獨開啟執行緒或程序,堵塞操作就可以非同步執行。
弊端:無法無限制的開啟多執行緒或者多程序。
執行緒池、程序池(適當使用)
好處:我們可以降低系統對程序或者 執行緒和銷燬一個頻率,從而更好的降低系統的開銷。
弊端:池中執行緒或程序的數量是有上限。
案例執行緒池
# 梨視訊爬取
import requests
from multiprocessing.dummy import Pool
import time
from lxml import etree
start_time = time.time()
headers = {
"user-agent": "Mozilla / 5.0(Windows NT 10.0;WOW64) AppleWebKit / 537.36(KHTML, likeGecko) Chrome / 94.0.4606.61Safari / 537.36"
}
url = "https://www.pearvideo.com/category_5"
response = requests.get(url=url, headers=headers).text
tree = etree.HTML(response)
all_href = tree.xpath('//ul[@id="categoryList"]/li/div/a/@href')
video_all_list = []
for href in all_href:
video_url = "https://www.pearvideo.com/" + href
video_dic = {"contId": href.split("_")[1], "url": video_url}
video_all_list.append(video_dic)
def down_img(data_list):
json_headers = {
"user-agent": "Mozilla / 5.0(Windows NT 10.0;WOW64) AppleWebKit / 537.36(KHTML, likeGecko) Chrome / 94.0.4606.61Safari / 537.36",
"Referer": data_list["url"] # 防盜鏈
}
contId = data_list["contId"]
video_json_url = f"https://www.pearvideo.com/videoStatus.jsp?contId={contId}"
response_json = requests.get(url=video_json_url, headers=json_headers).json()
mp4_url = response_json["videoInfo"]["videos"]["srcUrl"]
cont = f"cont-{contId}"
real_url = mp4_url.replace(mp4_url.split("-")[0].split("/")[-1], cont)
print(f"正在下載{contId}")
mp4_content = requests.get(url=real_url, headers=json_headers).content
with open(f"梨視訊/{contId}.mp4", "wb") as f:
f.write(mp4_content)
print(f"下載完成{contId}")
pool = Pool(8) # 建立8個執行緒池
pool.map(down_img, video_all_list)
pool.close() # 關閉執行緒池
pool.join() # 主程序堵塞後 讓子程序繼續進行完成,子程序完成後,再把主程序全部關閉
now = time.time()
print(f"用時{now - start_time}")
"""
json_headers = {
"user-agent": "Mozilla / 5.0(Windows NT 10.0;WOW64) AppleWebKit / 537.36(KHTML, likeGecko) Chrome / 94.0.4606.61Safari / 537.36",
"Referer": data_list["url"] # 防盜鏈
}
data_list是一個列表
pool.map(down_img, video_all_list)
當執行到這段程式碼時
相當於
for data_list in data_list
所有能從data_list通過key取值value
"""
非同步協程
1.用async修飾一個函式,呼叫之後返回一個協程物件
2.將協程封裝到Task物件中並新增到事件迴圈的任務列表中,等待事件迴圈去執行
3.建立一個事件迴圈物件
4.將協程物件註冊到loop中,啟動loop
import requests
import asyncio
import time
import aiohttp
urls = []
start = time.time()
for i in range(10):
urls.append('http://127.0.0.1:5000/bobo')
print(urls)
async def get_page(url): # async修飾一個函式
async with aiohttp.ClientSession() as session:
# get()、post():
# headers,params/data,proxy='http://ip:port'
async with await session.get(url) as response:
# text()返回字串形式的響應資料
# read()返回的二進位制形式的響應資料
# json()返回的就是json物件
# 注意:獲取響應資料操作之前一定要使用await進行手動掛起
page_text = await response.text()
print(page_text)
tasks = []
for url in urls:
c = get_page(url) # 返回一個協程物件
task = asyncio.ensure_future(c)
tasks.append(task) # 將協程封裝到Task物件中並新增到事件迴圈的任務列表中
方法一:
'''
loop = asyncio.get_event_loop() # 建立一個事件迴圈物件
result = asyncio.wait(tasks)
loop.run_until_complete(result) # 將協程物件註冊到loop中,啟動loop
'''
方法二:
'''
本質上方式一是一樣的,內部先 建立事件迴圈 然後執行 run_until_complete,一個簡便的寫法。
python 3.7
result = asyncio.wait(tasks)
asyncio.run(result)
'''
end = time.time()
print('總耗時:', end - start)
標籤定位
#執行一組js程式
bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
#點選搜尋按鈕
bro.find_element_by_css_selector('.btn-search').click()
#回退
bro.back()
sleep(2)
#前進
bro.forward()
# 關閉瀏覽器
bro.quit()
bro.close()
#標籤互動
search_input = bro.find_element_by_id('q')
search_input.send_keys('Iphone') # 如果是input標籤將在標籤中輸入Iphone
#匯入動作鏈對應的類
from selenium.webdriver import ActionChains
#動作鏈
action = ActionChains(bro)
#點選長按指定的標籤
action.click_and_hold(div)
# 移動動作鏈 x軸17 y軸0
action.move_by_offset(17,0).perform()
#釋放動作鏈
action.release()
#如果定位的標籤是存在於iframe標籤之中的則必須通過如下操作在進行標籤定位
bro.switch_to.frame('iframeResult') # 切換瀏覽器標籤定位的作用域
bro.switch_to.frame(定位ifrme元素)
selenium
from selenium import webdriver
from time import sleep
#實現無視覺化介面
from selenium.webdriver.chrome.options import Options
#實現規避檢測
from selenium.webdriver import ChromeOptions
#實現無視覺化介面的操作
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
#實現規避檢測
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
#如何實現讓selenium規避被檢測到的風險
bro = webdriver.Chrome(executable_path='./chromedriver',chrome_options=chrome_options,options=option)
#無視覺化介面(無頭瀏覽器 不彈出瀏覽器) phantomJs
bro.get('https://www.baidu.com')
print(bro.page_source)
sleep(2)
bro.quit()
12306模擬登入
from selenium import webdriver
import time
from selenium.webdriver.support import wait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
driver = webdriver.Chrome(r"chromedriver_win32/chromedriver.exe")
driver.get("https://kyfw.12306.cn/otn/resources/login.html")
driver.find_element_by_xpath('//*[@id="toolbar_Div"]/div[2]/div[2]/ul/li[2]/a').click()
driver.find_element_by_id("J-userName").send_keys(15237674912)
time.sleep(0.5)
driver.find_element_by_id('J-password').send_keys(""*******")
driver.find_element_by_id('J-login').click()
# 關閉selenium不可使用滑塊的限制
script = 'Object.defineProperty(navigator,"webdriver",{get:()=>undefined,});'
driver.execute_script(script)
# 等待5秒時間家長出類 nc_iconfont.btn_slide
slide_btn = wait.WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.CLASS_NAME, 'nc_iconfont.btn_slide')))
# click_and_hold按住滑鼠左鍵在源元素上,點選並且不釋放
ActionChains(driver).click_and_hold(on_element=slide_btn).perform()
# move_by_offset向右移動 x軸300 y軸0
ActionChains(driver).move_by_offset(xoffset=300, yoffset=0).perform()
12306登入老版 超級鷹驗證
# 下述程式碼為超級鷹提供的示例程式碼
import requests
from hashlib import md5
class Chaojiying_Client(object):
def __init__(self, username, password, soft_id):
self.username = username
password = password.encode('utf8')
self.password = md5(password).hexdigest()
self.soft_id = soft_id
self.base_params = {
'user': self.username,
'pass2': self.password,
'softid': self.soft_id,
}
self.headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}
def PostPic(self, im, codetype):
"""
im: 圖片位元組
codetype: 題目型別 參考 http://www.chaojiying.com/price.html
"""
params = {
'codetype': codetype,
}
params.update(self.base_params)
files = {'userfile': ('ccc.jpg', im)}
r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files,
headers=self.headers)
return r.json()
def ReportError(self, im_id):
"""
im_id:報錯題目的圖片ID
"""
params = {
'id': im_id,
}
params.update(self.base_params)
r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
return r.json()
# chaojiying = Chaojiying_Client('bobo328410948', 'bobo328410948', '899370') #使用者中心>>軟體ID 生成一個替換 96001
# im = open('12306.jpg', 'rb').read() #本地圖片檔案路徑 來替換 a.jpg 有時WIN系統須要//
# print(chaojiying.PostPic(im, 9004)['pic_str'])
# 上述程式碼為超級鷹提供的示例程式碼
# 使用selenium開啟登入頁面
from selenium import webdriver
import time
from PIL import Image
from selenium.webdriver import ActionChains
bro = webdriver.Chrome(executable_path='./chromedriver')
bro.get('https://kyfw.12306.cn/otn/login/init')
time.sleep(1)
# save_screenshot就是將當前頁面進行截圖且儲存
bro.save_screenshot('aa.png')
# 確定驗證碼圖片對應的左上角和右下角的座標(裁剪的區域就確定)
code_img_ele = bro.find_element_by_xpath('//*[@id="loginForm"]/div/ul[2]/li[4]/div/div/div[3]/img')
location = code_img_ele.location # 驗證碼圖片左上角的座標 x,y
print('location:', location)
size = code_img_ele.size # 驗證碼標籤對應的長和寬
print('size:', size)
# 左上角和右下角座標
rangle = (
int(location['x']), int(location['y']), int(location['x'] + size['width']), int(location['y'] + size['height']))
# 至此驗證碼圖片區域就確定下來了
i = Image.open('./aa.png')
code_img_name = './code.png'
# crop根據指定區域進行圖片裁剪
frame = i.crop(rangle)
frame.save(code_img_name)
# 將驗證碼圖片提交給超級鷹進行識別
chaojiying = Chaojiying_Client('bobo328410948', 'bobo328410948', '899370') # 使用者中心>>軟體ID 生成一個替換 96001
im = open('code.png', 'rb').read() # 本地圖片檔案路徑 來替換 a.jpg 有時WIN系統須要//
print(chaojiying.PostPic(im, 9004)['pic_str'])
result = chaojiying.PostPic(im, 9004)['pic_str']
all_list = [] # 要儲存即將被點選的點的座標 [[x1,y1],[x2,y2]]
if '|' in result:
list_1 = result.split('|')
count_1 = len(list_1)
for i in range(count_1):
xy_list = []
x = int(list_1[i].split(',')[0])
y = int(list_1[i].split(',')[1])
xy_list.append(x)
xy_list.append(y)
all_list.append(xy_list)
else:
x = int(result.split(',')[0])
y = int(result.split(',')[1])
xy_list = []
xy_list.append(x)
xy_list.append(y)
all_list.append(xy_list)
print(all_list)
# 遍歷列表,使用動作鏈對每一個列表元素對應的x,y指定的位置進行點選操作
for l in all_list:
x = l[0]
y = l[1]
ActionChains(bro).move_to_element_with_offset(code_img_ele, x, y).click().perform()
time.sleep(0.5)
bro.find_element_by_id('username').send_keys('[email protected]')
time.sleep(2)
bro.find_element_by_id('password').send_keys('bobo_15027900535')
time.sleep(2)
bro.find_element_by_id('loginSub').click()
time.sleep(30)
bro.quit()
scrapy框架
環境的安裝
python
- mac or linux:
pip install scrapy
- windows:
pip install wheel
# 下載twisted,下載地址為http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
# 安裝twisted:pip install Twisted‑17.1.0‑cp36‑cp36m‑win_amd64.whl
pip install pywin32
pip install scrapy
```
基礎指令
建立一個工程:
scrapy startproject xxxPro
cd xxxPro # 切換到這個工程
在spiders子目錄中建立一個爬蟲檔案
scrapy genspider spiderName www.xxx.com
執行工程:
scrapy crawl spiderName
持久化儲存
基於終端
基於終端指令:
- 要求:只可以將parse方法的返回值儲存到本地的文字檔案中
- 注意:持久化儲存對應的文字檔案的型別只可以為:'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle
- 指令:scrapy crawl xxx -o filePath
- 好處:簡介高效便捷
- 缺點:侷限性比較強(資料只可以儲存到指定字尾的文字檔案中)
基於管道
-
在items.py 中封裝屬性物件
import scrapy # 封裝了 content time 2個物件 class DuanziproItem(scrapy.Item): # define the fields for your item here like: content = scrapy.Field() time = scrapy.Field()
-
將解析到資料值儲存到items物件
duanzi.py
item = DuanziproItem() item['time'] = time item['content'] = content
-
在管道檔案中編寫程式碼完成資料儲存的操作
pipelines.py
class DuanziproPipeline: fp = None # 重寫父類的一個方法:該方法只在開始爬蟲的時候被呼叫一次 def open_spider(self, spider): print('開始爬蟲......') self.fp = open('./duanzi.txt', 'w', encoding='utf-8') # 專門用來處理item型別物件 # 該方法可以接收爬蟲檔案提交過來的item物件 # 該方法沒接收到一個item就會被呼叫一次 def process_item(self, item, spider): time = item['time'] content = item['content'] self.fp.write(time + ':' + content + '\n') return item # 就會傳遞給下一個即將被執行的管道類 def close_spider(self, spider): print('結束爬蟲!') self.fp.close()
-
在配置檔案settings.py中開啟管道操作
ITEM_PIPELINES = {
'duanziPro.pipelines.DuanziproPipeline': 300,
}
儲存到資料庫
duanzi.py
import scrapy
from ..items import DuanziproItem
class DuanziSpider(scrapy.Spider):
name = 'duanzi'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://duanzixing.com/']
def parse(self, response):
all_content = response.xpath('//article[@class="excerpt"]//h2/a/text()').extract()
all_time = response.xpath('//p[@class="meta"]/time/text()').extract()
for i in range(0, len(all_time)):
time = all_time[i]
content = all_content[i]
item = DuanziproItem()
item['time'] = time
item['content'] = content
# 2.將item物件提交給管道
yield item
在item.py中封裝物件
import scrapy
# 封裝了 content time 2個物件
class DuanziproItem(scrapy.Item):
# define the fields for your item here like:
content = scrapy.Field()
time = scrapy.Field()
pipelines.py 增加一個類
import pymysql
class QiubaiproPipeline(object):
conn = None # mysql的連線物件宣告
cursor = None # mysql遊標物件宣告
def open_spider(self,spider):
print('開始爬蟲')
# 連結資料庫
# host 本機的ip地址
# 在命令列輸入 ipconfig檢視
self.conn = pymysql.Connect(host='127.0.0.1',port=3306,user='root',password='',db='duanzi',charset='utf8')
# 該方法可以接受爬蟲檔案中提交過來的item物件,並且對item物件的頁面資料進行持久化處理
# 引數:item表示的就是接受到的item物件
def process_item(self, item, spider):
# 1.連結資料庫
# 執行sql語句
# 插入資料
sql = 'insert into db1(time,content) values("%s","%s")'%(item['author'], item['content'])
# 獲取遊標
self.cursor = self.conn.cursor()
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
# 提交事務
return item
# 該方法只會在爬蟲結束的時候被呼叫一次
def close_spider(self,spider):
print('爬蟲結束')
self.cursor.close()
self.conn.close()
Spider的全站資料爬取
通過定製一個url模板
使用模板自定義修改獲取一個新的url 回撥 parse()
yield scrapy.Request(url=new_url, callback=self.parse)
# 彼岸圖爬取
import scrapy
class BiantuSpider(scrapy.Spider):
name = 'biantu'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://pic.netbian.com/index_1.html']
# 定製模板
url = "https://pic.netbian.com/index_%d.html"
page_num = 2
def parse(self, response):
alt_list = response.xpath('//ul[@class="clearfix"]/li/a/img/@alt').extract()
for title in alt_list:
print(title)
if self.page_num <= 5:
new_url = format(self.url % self.page_num)
self.page_num += 1
# 回撥 parse函式
yield scrapy.Request(url=new_url, callback=self.parse)
五大核心
-
引擎(Scrapy)
用來處理整個系統的資料流處理, 觸發事務(框架核心) -
排程器(Scheduler)
用來接受引擎發過來的請求, 壓入佇列中, 並在引擎再次請求的時候返回. 可以想像成一個URL(抓取網頁的址 或者說是連結)的優先佇列, 由它來決定下一個要抓取的網址是什麼, 同時去除重複的網址
下載器(Downloader)
用於下載網頁內容, 並將網頁內容返回給蜘蛛(Scrapy下載器是建立在twisted這個高效的非同步模型上的) -
爬蟲(Spiders)
爬蟲是主要幹活的, 用於從特定的網頁中提取自己需要的資訊, 即所謂的實體(Item)。使用者也可以從中提取出連結,讓Scrapy繼續抓取下一個頁面 -
專案管道(Pipeline)
負責處理爬蟲從網頁中抽取的實體,主要的功能是持久化實體、驗證實體的有效性、清除不需要的資訊。當頁面被爬蟲解析後,將被髮送到專案管道,並經過幾個特定的次序處理資料。