目標
多線程數據抓取-58同城轉轉網的二手產品
實作
1. 建立一個項目
新建一個項目58tongcheng1
2. 觀察頁面特征
不同頁面的不同規則問題
分頁問題
3. 設計工作流程
首先,在主列表頁爬取所有商品的URL,存儲在mongodb
中,在數據庫中建立對應的URL_list
(爬蟲1)
然后,詳情頁具體產品的信息,存儲在數據庫item_info
中 (爬蟲2)
爬蟲2應把 爬蟲1抓取的并已存在數據庫中的URL取出來,依次讀取詳情頁,獲得所要信息,再把這些信息存儲在item_info
這個表單中
4. 創建channel_extract.py
獲取每個分類產品的鏈接
from bs4 import BeautifulSoup
import requests
start_url = 'http://bj.58.com/sale.shtml'
url_host = 'http://bj.58.com'
def get_channel_urls(url):
wb_data = requests.get(start_url)
soup = BeautifulSoup(wb_data.text, 'lxml')
links = soup.select('ul.ym-submnu > li > b > a') # 尋找該標簽時比較麻煩,因為它是hover顯示
print(links)
for link in links:
page_url = url_host + str(link.get('href'))
print(page_url)
get_channel_urls(start_url) # 通過它打印出所有的URL
把所有的URL集中起來建立一個新的長字符串
channel_list = '''
http://bj.58.com/shouji/
http://bj.58.com/tongxunyw/
http://bj.58.com/danche/
http://bj.58.com/diandongche/
http://bj.58.com/fzixingche/
http://bj.58.com/sanlunche/
http://bj.58.com/peijianzhuangbei/
5. 創建page_parsing.py
獲取產品詳情
from bs4 import BeautifulSoup
import requests
import time
import pymongo
client = pymongo.MongoClient('localhost', 27017)
chengxu = client['chengxu']
url_list = chengxu['url_list3']
item_info = url_list['item_info3']
# spider 1 爬取首頁中顯示的類目中,一個類目下的所有商品的鏈接
def get_links_from(channel,pages,who_sells=0): # who_sells = 0表示個人,1表示商家
#http://bj.58.com/shouji/1/pn2/
list_view = '{}{}/pn{}/'.format(channel,str(who_sells),str(pages)) # 找網頁規律的時候,剛刷新和點擊后的相同頁面的網址會有變化,但頁面相同,它們是等價的,所以找頁面規律時要多點擊或刷新來找
wb_data = requests.get(list_view)
time.sleep(1)
soup = BeautifulSoup(wb_data.text, 'lxml')
if soup.find('td','t'): # 一個類目的頁碼是有限的,通過尋找td.t來判斷系統是否爬過頭了
for link in soup.select('td.t a.t'): # 這里的td.t a.t 是點擊某個分類后的新網頁的每個具體商品的鏈接的selector
#for link in soup.select( ('td.t a.t') if not soup.find_all('zhiding', 'huishou') else None ): #修改失敗,計劃排除被抓取的幾排廣告
# 注意!!!上面代碼后面,若是('td.t >a.t')即無法顯示結果,必須空格!這樣才對('td.t > a.t')
item_link = link.get('href').split('?')[0] # 這里的0是對切片后的字符串形成的列表list進行篩選,選第一段,即0(for in 就是對列表的)
url_list.insert_one({'url': item_link }) # insert是數據庫函數,注意區分
print(item_link)
else:
pass
#get_links_from('http://bj.58.com/shuma/', 2)
# spider 2 爬詳情頁的數據
def get_item_info(url):
wb_data = requests.get(url)
soup = BeautifulSoup(wb_data.text, 'lxml')
no_longer_exist = '商品已下架' in soup.find('div', "button_li").get_text() # 從下方 AAA 處移過來的代碼,理解時先忽略它。
# find()里面的代碼實際是完整的div="button_li",而且要保證該段代碼在正常網頁和已下架網頁中都存在,否則正常網頁報錯。
if no_longer_exist:
pass
else:
title = soup.title.text
price = soup.select('span.price_now i')[0].text
# 后面必須加[0].text,因為數據庫要是str才能存進去,soup.select返回的對象是list,就算list里面只有一個元素,也不能用.text方法,所以才選擇用[0],把元素從list調出來,再進行.text方法
area = soup.select('.palce_li i')[0].text if soup.find_all('i') else None
item_info.insert_one({'title':title, 'price':price, 'area':area })
print({'title': title, 'price': price, 'area':area})
#get_item_info('http://zhuanzhuan.58.com/detail/919823388320399372z.shtml')
#======= AAA 爬取的商品鏈接中有失效的,剔除它(商品已交易則該網址會失效),測試完該段代碼備注掉==========#
# url = 'http://zhuanzhuan.58.com/detail/922439089107222541z.shtml' # 網址上的商品已下架
# wb_data = requests.get(url)
# soup = BeautifulSoup(wb_data.text, 'lxml')
#print(soup.prettify())
# 上面的步驟查詢了失效網址的結構。
#no_longer_exist = '商品已下架' in soup.find('span', "soldout_btn").get_text() # 搬到上方get_item_info
#print (no_longer_exist) # 查看no_longer_exist是True False。上面的find里代碼必須是完整的<xxx>內容,形成一個list,否則系統報錯屬性錯誤或者無法迭代
注意事項均備注在代碼中。。。
6. 多進程數據抓取
建立主程序 main.py
from multiprocessing import Pool #
from channel_extract import channel_list
from page_parsing import get_links_from
def get_all_links_from(channel):
for num in range(1,51):
get_links_from(channel,num)
if __name__=='__main__': # 一種類似作文開頭的感謝領導的套話格式,防止上下程序串混亂了,沒特別的意思
pool = Pool() # 創建進程池
pool.map(get_all_links_from, channel_list.split())
# map函數的特點是把括號內的后一個參數放到前一個參數(函數)里去依次執行。約定俗成map第一個參數為不帶 () 的函數。
# channel_list 是引用過來的,我們之前定義過它是一個長字符串,將它分成一段段,split()函數會將一個字符串自動變成分割好的一個大list
監控程序 counts.py
import time
from page_parsing import url_list # url_list 是數據庫的第一張表的名稱
while True:
print(url_list.find().count())
# find()展示url_list中所有的元素,count()計數,這兩種函數是數據庫函數,不能用于字典和列表
time.sleep(5)
# 該段程序用來監控用,當它和主程序一起開的時候,它可以計算數量進程,方便管理
7. 運行
打開終端,開啟3個窗口,切換到程序文件夾中,第一個窗口輸入mongod
,輸入mongo
,好了,mongo已開啟
第二個窗口輸入 python3 counts.py
第三個窗口輸入python3 main.py
好了,開始抓取數據了,成功
8. 斷點續傳
from page_parsing import get_links_from, get_item_info, url_list, item_info # 該條為更改的,下面代碼全部是新建的
# 斷點續傳
db_urls = [ item['url'] for item in url_list.find() ] # 用列表解析式裝入所要爬取的鏈接
index_urls = [ item['url'] for item in item_info.find() ] # 所引出詳情信息數據庫中所有的現存的 url 字段
x = set(db_urls) # 轉換成集合的數據結構
y = set(index_urls)
rest_of_urls = x - y
設計思路:
- 分兩個數據庫,第一個用于只用于存放抓取下來的
url
(ulr_list)
;第二個則儲存 url 對應的物品詳情信息(item_info)
- 在抓取過程中在第二個數據庫中寫入數據的同時,新增一個字段(key)
'index_url'
即該詳情對應的鏈接 - 若抓取中斷,在第二個存放詳情頁信息的數據庫中的 url 字段應該是第一個數據庫中 url 集合的子集
- 兩個集合的 url 相減得出圣賢應該抓取的 url 還有哪些
備注
(1) find()
的參數依次為(標簽名,標簽屬性),返回一個標簽(可多重嵌套)或None;
(2)find_all()
的參數依次為(標簽名,標簽屬性),返回一個標簽列表或者空列表
(3) python的find()
是字符串對象的方法,用于查找子字符串,返回第一個字串出現的位置或-1(字串不存在);mongodb的find()
是列表對象的方法,接收字典參數,鍵值對為所要查找條目鍵值對,用于查找條目,返回True
(4) mongodb
的查詢方法find()
與find_one()
find()
方法成功找到符合條件的記錄則返回一個生成器(實質是停留在符合條件記錄的集合的第一條記錄位置的cursor),用list方法轉化為列表后,如果該存在符合條件的記錄,則生成一個列表,否則生成一個空列表。
find_one
({查詢鍵值對},{顯示字段:0表示不顯示or1表示顯示,其余默認不顯示,'_id'默認顯示})返回查詢到的第一條。