python360百科_python抓取360百科踩过的坑!
學習python一周,學著寫了一個爬蟲,用來抓取360百科的詞條,在這個過程中。因為一個小小的修改,程序出現一些問題,又花了幾天時間研究,問了各路高手,都沒解決,終于還是自己攻克了,事實上就是對list列表理解不夠深入導致的。這個bug非常有借鑒意義,分享出現。
先看看終于抓取出的結果:
以下進入正題。先來看看文件結構。這里有5個模塊:
spider_main.py是入口函數
url_manager.py是管理器,管理須要抓取的url和已經抓取的url
html_downloader.py是下載器,下載相應url的網頁
html_parser.py是解析器,解析出新的url列表和當前的詞條信息
html_outputer.py是輸出器。將抓取的詞條title和解釋summary輸出成一個html表格
本程序使用的是最新的python3.4.4。使用的類庫有:
官方的urllib
第三方的BeautifulSoup(自行下載安裝)
一、spider_main.py
這個函數是入口函數,全部方法都是字面意思,能夠到相應的類中查看
from baike360_spider import url_manager, html_downloader, html_parser, html_outputer
class SpiderMain(object):
def __init__(self):
self.urls = url_manager.UrlManager()
self.downloader = html_downloader.HtmlDownloader()
self.parser = html_parser.HtmlParser()
self.outputer = html_outputer.HtmlOutputer()
def craw(self):
count = 1
self.urls.add_new_url(root_url)
while self.urls.has_new_url():
try:
new_url = self.urls.get_new_url()
print(‘craw %d: %s‘ % (count, new_url))
html_cont = self.downloader.download(new_url)
new_urls, new_data = self.parser.parse(new_url, html_cont)
self.urls.add_new_urls(new_urls)
self.outputer.collect_data(new_data)
if count == 5:
break
count += 1
except Exception as e:
print(‘craw failed‘)
print(e)
self.outputer.output_html()
if __name__ == ‘__main__‘:
root_url = ‘http://baike.so.com/doc/1790119-1892991.html‘
obj_spider = SpiderMain()
obj_spider.craw()
二、url_manager.py
管理器的作用是處理新的須要抓取的url和已經抓取的url,因此在構造函數里面初始化兩個變量,相應有4個方法,功能都非常easy,看看就懂。
class UrlManager(object):
def __init__(self):
self.new_urls = set()
self.old_urls = set()
def add_new_url(self, url):
if url is None:
return
if url not in self.new_urls and url not in self.old_urls:
self.new_urls.add(url)
def add_new_urls(self, urls):
if urls is None or len(urls) == 0:
return
for url in urls:
self.add_new_url(url)
def has_new_url(self):
return len(self.new_urls) != 0
def get_new_url(self):
new_url = self.new_urls.pop()
self.old_urls.add(new_url)
return new_url
三、html_downloader.py
把相應的url網頁下載下來,準備給解析器解析須要的數據
import urllib.request
class HtmlDownloader(object):
def download(self, url):
if url is None:
return
res = urllib.request.urlopen(url)
if res.getcode() != 200:
return
return res.read()
四、html_parser.py
解析器的作用是對傳入的url和html網頁進行解析。使用到BeautifulSoup第三方庫,相應有兩個私有方法,一個解析出該網頁下全部的鏈接。存到list中,方便下次隨機抽取。還有一個解析出當前網頁詞條的title和summary,放到一個字典中。里面有3個key:url、title和summary。
在這個類里面踩了一個大坑,最后再講!
import re
from urllib.parse import urljoin
from bs4 import BeautifulSoup
class HtmlParser(object):
def __init__(self):
self.new_urls = set()
# self.res_data = dict()
def _get_new_urls(self, page_url, soup):
# new_urls = set()
# /doc/5912108-6125016.html或/doc/3745498.html
links = soup.find_all(‘a‘, href=re.compile(r‘/doc/[\d-]+\.html‘))
for link in links:
new_url = link[‘href‘]
new_full_url = urljoin(page_url, new_url)
# print(new_full_url)
self.new_urls.add(new_full_url)
return self.new_urls
def _get_new_data(self, page_url, soup):
res_data = dict()
# url
res_data[‘url‘] = page_url
# Python
title_node = soup.find(‘span‘, class_=‘title‘)
res_data[‘title‘] = title_node.get_text()
#
summary_node = soup.find(‘div‘, class_=‘card_content‘).find(‘p‘)
res_data[‘summary‘] = summary_node.get_text()
# print("dd: ", self.res_data)
return res_data
def parse(self, page_url, html_cont):
if page_url is None or html_cont is None:
return
soup = BeautifulSoup(html_cont, ‘html.parser‘, from_encoding=‘utf-8‘)
new_urls = self._get_new_urls(page_url, soup)
new_data = self._get_new_data(page_url, soup)
return new_urls, new_data
五、html_outputer.py
輸出器的作用是把解析器解析出的網頁字典存入列表。然后循環輸出一個html表格
class HtmlOutputer(object):
def __init__(self):
self.datas = []
def collect_data(self, data):
if data is None:
return
self.datas.append(data)
def output_html(self):
fout = open(‘output.html‘, ‘w‘, encoding=‘utf-8‘)
fout.write(‘‘)
fout.write("
")fout.write(‘
‘)fout.write(‘
for data in self.datas:
fout.write(‘
‘)fout.write(‘
%s‘ % (data[‘url‘], data[‘title‘]))fout.write(‘
%s‘ % data[‘summary‘])fout.write(‘
‘)fout.write(‘
‘)fout.write(‘‘)
fout.write(‘‘)
fout.close()
六、*****關鍵。我踩過的坑!
!!!
我把解析器里面的res_data字典的初始化放到了構造函數里面,造成的結果是輸出的表格是5個反復的數據,都是和最后一組一樣。!
喜歡思考的同學能夠想想是為什么?
原因分析:
這要涉及到list列表和字典的性質了
首先,字典的key相應的value是不可變的,假設強行又一次賦值,會覆蓋上次的數據!
①第1次抓取后,res_data指向了字典{‘a‘: 1}
②res_data傳入輸出器后,self.datas列表內容也指向了字典{‘a‘: 1}
③第2次抓取后,解析器里面對a又一次賦新值2了,此時事實上是res_data指向了新的字典{‘a‘:
2}。因為原指向的key一樣,也一起變成了字典{‘a‘: 2},此時事實上self.datas列表的指向也變成了原字典{‘a‘:
2}(假設你在入口函數斷點查看,會出現一個奇葩的事。就是執行完解析器,可是還沒執行輸出器時,輸出器的self.datas已經變成和新解析出的值一樣了。!。當時我糾結了幾天)
④當res_data再次傳入輸出器后,會被加入后self.datas列表后面,因為原來里面的字典已經變成了{‘a‘:
2},全部此時會出現2個{‘a‘: 2},即
?
?self.datas = [{‘a‘: 2},?{‘a‘: 2}]
⑤如此循環抓取5次,終于self.datas里面是5個一樣的字典。并且都和最后一次抓取的一樣!
那么你可能會問,為什么把res_data初始化放在私有方法_get_new_data里面就沒問題呢?因為初始化在這里,每實例化一次解析器,就又一次執行一次這個函數。也會清空res_data字典,因此和原來指向的字典斷開了,又一次賦值也不會影響到原來的字典了。
總結
以上是生活随笔為你收集整理的python360百科_python抓取360百科踩过的坑!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python找与7相关的数字_C++和p
- 下一篇: 远视储备是什么意思