1. 程式人生 > 實用技巧 >Python爬蟲開發手冊

Python爬蟲開發手冊

python爬蟲開發手冊

第一章 requests模組

1.1 指定URL

  • UA偽裝

    headers = {
    		'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36'
    }
    
    

1.2 請求

  • 請求引數

    # get請求
    params = {
        '引數': '值',
    }
    
    # post請求
    data = {
        '引數': '值',
    }
    
    
  • get請求

    response = requests.get(url=url, params=params, headers=headers)
    
  • post請求

    response = requests.post(url=url, data=data, headers=headers)
    
  • 關於字元編碼

    # 方法一:手動設定響應資料的編碼格式
    response.encoding = 'gbk'
    
    # 方法二:字串統一編碼(通用)
    str_name = str_name.encode('iso-8859-1').decode('gbk')
    
    
  • 關於請求失敗

    def get_response(url):
        try:
            response = requests.get(url=url, headers=headers, timeout=30)
            if response.status_code == 200:
                return response
        except Exception:
            for i in range(1, 10):
                print(f'請求{url}超時,第{i}次重複請求')
                response = requests.get(url, headers=headers, timeout=30)
                if response.status_code == 200:
                    return response
    

1.3 響應

  • 響應型別

    # 字串
    response.text
    
    # 二進位制
    response.content
    
    # json物件
    json_obj = response.json()
    # json序列化時對中文預設使用的ascii編碼.想輸出真正的中文需要指定ensure_ascii=False
    json.dump(json_obj, fp=fp, ensure_ascii=False)  
    
    

1.4 解析:正則、bs4、XPath

  • 正則

    page_text = requests.get(url=url, headers=headers).text
    '''
    <div class="thumb">
      	<a href="/article/121721100" target="_blank">
      			<img src="//pic.qiushibaike.com/system/pictures/12172/121721100/medium/DNXDX9TZ8SDU6OK2.jpg" alt="指引我有前進的方向">
      	</a>
    </div>
    '''
    ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'
    
    # 如果不使用re.S引數,則只在每一行內進行匹配,如果一行沒有,就換下一行重新開始。
    # re.S:單行匹配,將整個字串作為一個整體,在整體中進行匹配;讓.匹配包括換行在內的所有字元
    # re.M:多行匹配,將字串按\n分開;讓^匹配每行的開頭,$匹配每行的結尾
    re.findall(ex, page_text, re.S)
    
    
  • bs4

    from bs4 import BeautifulSoup
    
    # 物件例項化:1.本地html文件
    with open('./test.html','r',encoding='utf-8') as fp:
    		soup = BeautifulSoup(fp,'lxml')
    # 物件例項化:2.網際網路上獲取的頁面原始碼
    page_text = response.text
    soup = BeatifulSoup(page_text,'lxml')
    
    # 方法和屬性:
    soup.div  # 首個div標籤
    soup.find('div')  # 首個div標籤
    soup.find('div', class_='song')  # 首個class='song'的div標籤
    soup.find_all('div')  # 所有div標籤(列表)
    
    # select選擇器
    soup.select('div')  # 所有div標籤(列表)
    soup.select('.tang>ul>li>a')  # >表示一個層級
    soup.select('.tang>ul a')  # 空格表示多個層級
    
    # 獲取標籤內文字資料
    soup.a.text  # 標籤內所有文字
    soup.a.get_text()  # 標籤內所有文字
    soup.a.string  # 標籤下直系文字
    
    # 獲取標籤中屬性值
    soup.a['href']  # a標籤的href屬性值
    
    
  • XPath

    from lxml import etree
    
    # 物件例項化:1.本地html文件
    etree.parse('./test.html')
    # 物件例項化:2.網際網路上獲取的頁面原始碼
    page_text = response.text
    tree = etree.HTML(page_text)
    
    # /是一個層級,從根節點開始定位。
    tree.xpath('/html/body/div')
    # //是多個層級,從任意位置開始定位。
    tree.xpath('/html//div')
    tree.xpath('//div')
    # ./,從當前層級開始定位。
    tree.xpath('./div')
    
    # 屬性定位
    tree.xpath('//div[@class="song"]')
    
    # 索引定位(索引從1開始)
    tree.xpath('//div[@class="song"]/p[3]')
    
    # 取文字:/text() 標籤中直系文字
    tree.xpath('//div[@class="tang"]//li[5]/a/text()')
    # 取文字://text() 標籤中所有文字
    tree.xpath('//li[7]//text()')
    tree.xpath('//div[@class="tang"]//text()')
    
    # 取屬性:@attrName
    r = tree.xpath('//div[@class="song"]/img/@src')
    
    # xpath表示式
    tree.xpath('//div[@class="bottom"]/ul/li/')  # 熱門城市a標籤的層級關係
    tree.xpath('//div[@class="bottom"]/ul/div[2]/li/a')  # 全部城市a標籤的層級關係
    # 全部城市a標籤的層級關係
    tree.xpath('//div[@class="bottom"]/ul/li/a | //div[@class="bottom"]/ul/div[2]/li/a')
    
    

1.5 儲存

  • 文字

    page_text = response.text
    
    # 文字
    with open('./a.txt', 'w', encoding='utf-8') as fp:
    		fp.write(page_text)
        
    
  • 圖片

    img_data = response.content
    
    # 二進位制格式
    with open('./qiutu.jpg','wb') as fp:
      	fp.write(img_data)
        
    
  • json物件

    list_data = response.json()
    
    # json物件
    with open('./douban.json','w',encoding='utf-8') as fp:
    		json.dump(list_data, fp=fp, ensure_ascii=False)
    

第二章 驗證碼

2.1 滑動驗證碼

第三章 代理

3.1 代理相關的網站

  • 快代理

  • 西祠代理

  • 全網代理IP

    # 網址
    www.goubanjia.com
    
    # 代理ip的匿名度
    透明:伺服器知道該次請求使用了代理,也知道請求對應的真實ip
    匿名:知道使用了代理,不知道真實ip
    高匿:不知道使用了代理,更不知道真實的ip
    
    

3.2 設定代理

  • 設定代理

    import requests
    
    # 先獲取代理IP
    response = requests.get(url=url, headers=headers, proxies={"https": '36.25.40.214:8888'})
    
    

第四章 session

4.1 模擬登入

  • 模擬登入

    import requests
    
    # 建立一個session物件
    session = requests.Session()
    
    
    # session物件對url傳送get請求,攜帶了cookie
    page_text = session.get(url=url, headers=headers).text
    
    # 使用session物件進行模擬登入post請求的傳送,cookie就會被儲存在session中
    response = session.post(url=post_url, data=data, headers=headers)
    print(response.status_code)
    
    

第五章 高效能非同步

5.1 多程序、多執行緒

  • 優點:可以為相關阻塞的操作單獨開啟執行緒或者程序,阻塞操作可以非同步執行。
  • 缺點:無法無限制的開啟多執行緒或者多程序。

5.2 程序池、執行緒池

  • 優點:可以降低系統對程序或者執行緒建立和銷燬的頻率,從而很好的降低系統的開銷。

  • 缺點:池中執行緒或程序的數量是有上限。

    from multiprocessing.dummy import Pool
    
    
    def get_video_data(dic):
        url = dic['url']
        name = dic['name']
        data = requests.get(url=url, headers=headers).content
        with open(name, 'wb') as fp:
            fp.write(data)
        
    
    dic = {
        'name': name,
        'url': video_url
    }
    urls.append(dic)
    
    # 使用執行緒池對視訊資料進行請求(較為耗時的阻塞操作)
    pool = Pool(4)
    pool.map(get_video_data, urls)
    
    pool.close()
    pool.join()
    
    
    from concurrent.futures import ThreadPoolExecutor
    
    
    def get_video_data(dic):
        url = dic['url']
        name = dic['name']
        data = requests.get(url=url, headers=headers).content
        with open(name, 'wb') as fp:
            fp.write(data)
        
    
    dic = {
        'name': name,
        'url': video_url
    }
    urls.append(dic)
    
    # 例項化一個執行緒池物件
    pool = ThreadPoolExecutor(4)  
    for dic in urls:
    		pool.submit(get_video_data, dic) 
        
    # 等所有任務完成,再關閉執行緒池
    pool.shutdown(wait=True)
    
    

5.3 單執行緒+非同步協程

  • 詳情見第六章

第六章 協程爬蟲

6.1 gevent模組

  • gevent模組屬於第三方模組

    from gevent import monkey;monkey.patch_all()
    
    import gevent
    import time
    
    def eat(name):
        print('%s eat 1' %name)
        gevent.sleep(2)  # gevent可以識別的io阻塞
        print('%s eat 2' %name)
    
    def play(name):
        print('%s play 1' %name)
        time.sleep(1)  # gevent不能直接識別,需要首行打補丁
        print('%s play 2' %name)
    
    
    g1=gevent.spawn(eat,'egon')
    g2=gevent.spawn(play,name='egon')
    
    # 或者gevent.joinall([g1,g2])
    g1.join()
    g2.join()  
    
    print('主')
    
    

6.2 asyncio模組

  • asyncio模組是python內建的模組

    import asyncio
    """
    直接呼叫
    """
    
    # 用async定義一個協程
    async def request(url):
        print('正在請求的url是', url)
        print('請求成功,', url)
        return url
    
    
    # 被async修飾的函式,呼叫之後返回一個 協程物件c
    c = request('www.baidu.com')
    
    # 1. 基於asyncio 建立一個 事件迴圈物件loop
    loop = asyncio.get_event_loop()
    
    # 2. 將 協程物件c 註冊到 事件迴圈物件loop 中,然後啟動loop
    loop.run_until_complete(c)
    
    
    import asyncio
    """
    通過task物件呼叫
    """
    
    # 用async定義一個協程
    async def request(url):
        print('正在請求的url是', url)
        print('請求成功,', url)
        return url
    
    
    # 被async修飾的函式,呼叫之後返回一個協程物件c
    c = request('www.baidu.com')
    
    # 1. 基於asyncio 建立一個 事件迴圈物件loop
    loop = asyncio.get_event_loop()
    
    # 2. 基於loop 建立一個 task物件
    task = loop.create_task(c)
    
    # 3. 將 task物件 註冊到 事件迴圈物件loop 中,然後啟動loop
    print(task)
    loop.run_until_complete(task)
    print(task)
    
    
    import asyncio
    """
    通過future物件呼叫
    """
    
    # 用async定義一個協程
    async def request(url):
        print('正在請求的url是', url)
        print('請求成功,', url)
        return url
    
    
    # 被async修飾的函式,呼叫之後返回一個協程物件c
    c = request('www.baidu.com')
    
    # 1. 基於asyncio 建立一個 事件迴圈物件loop
    loop = asyncio.get_event_loop()
    
    # 2. 基於asyncio 建立一個 future物件
    future = asyncio.ensure_future(c)
    
    # 3. 將 future物件 註冊到 事件迴圈物件loop 中,然後啟動loop
    print(future)
    loop.run_until_complete(future)
    print(future)
    
    
    import asyncio
    """
    呼叫回撥函式
    """
    
    # 用async定義一個協程
    async def request(url):
        print('正在請求的url是', url)
        print('請求成功,', url)
        return url
      
      
    def callback_func(future):
        # result()返回的是任務物件中封裝的協程物件對應函式的返回值
        print(future.result())
    
    
    # 被async修飾的函式,呼叫之後返回一個 協程物件c
    c = request('www.baidu.com')
    
    # 1. 基於asyncio 建立一個 事件迴圈物件loop
    loop = asyncio.get_event_loop()
    
    # 2. 基於asyncio 建立一個 future物件
    future = asyncio.ensure_future(c)
    
    # 3. 將 回撥函式 繫結到 future物件 中
    future.add_done_callback(callback_func)
    
    # 4. 將 future物件 註冊到 事件迴圈物件loop 中,然後啟動loop
    loop.run_until_complete(future)
    
  • 示例一:模擬

    import asyncio
    
    
    # 用async定義一個協程
    async def request(url):
        print('正在下載', url)
        
        # 非同步協程中出現同步模組相關的程式碼,無法實現非同步。
        # time.sleep(2)
        
        # 在asyncio中遇到阻塞操作,用await進行手動掛起
        await asyncio.sleep(2)
        
        print('下載完畢', url)
    
    
    urls = [
        'www.baidu.com',
        'www.sogou.com',
        'www.goubanjia.com'
    ]
    
    # 任務列表:存放多個任務物件
    tasks = []
    for url in urls:
        c = request(url)
        task = asyncio.ensure_future(c)
        tasks.append(task)
    
    loop = asyncio.get_event_loop()
    # 將任務列表封裝到wait中
    loop.run_until_complete(asyncio.wait(tasks))
    
    
  • 示例二:訪問flask伺服器

    import requests
    import asyncio
    import aiohttp
    
    
    async def get_page(url):
        async with aiohttp.ClientSession() as session:
            # get()、post():
            # headers,params/data,proxy='http://ip:port'
            async with await session.get(url=url) as response:
                # text()返回字串形式的響應資料
                # read()返回的二進位制形式的響應資料
                # json()返回的就是json物件
                # 注意:獲取響應資料前,要用await進行手動掛起
                page_text = await response.text()
                print(page_text)
    
                
    urls = [
        'www.baidu.com',
        'www.sogou.com',
        'www.goubanjia.com'
    ]
    
    # 任務列表:存放多個任務物件
    tasks = []
    for url in urls:
        c = get_page(url)
        task = asyncio.ensure_future(c)
        tasks.append(task)
    
    loop = asyncio.get_event_loop()
    # 將任務列表封裝到wait中
    loop.run_until_complete(asyncio.wait(tasks))
    
    
  • 示例三:訪問網頁

    import asyncio
    import aiohttp
    
    
    class GetArticle(object):
    
        def __init__(self, url, file):
            pass
    
        async def article2dict(self, menu_dict):
            pass
          
            # 非同步協程:請求文章URL的text內容
            async with aiohttp.ClientSession() as session:
                try:
                    async with await session.get(url=article_url, headers=self.headers, timeout=5) as response:
                        # 使用gbk進行解碼
                        article_text = await response.text(encoding='gbk')
                except Exception:
                    for i in range(1, 10):
                        print(f'請求 {article_url} 超時,第{i}次重複請求')
                        async with await session.get(url=article_url, headers=self.headers, timeout=5) as response:
                            article_text = await response.text(encoding='gbk')
                            print(f'第{i}次重複請求 {article_url} 成功')
                            # 訪問成功,跳出迴圈
                            break
                            
            pass
    
        def run(self):
            """
            主函式
            :return:
            """
            pass
    
            # 任務列表:存放多個任務物件
            tasks = []
            for menu_dict in menu_list:
                c = self.article2dict(menu_dict)
                task = asyncio.ensure_future(c)
                tasks.append(task)
    
            loop = asyncio.get_event_loop()
    				# 將任務列表封裝到wait中
            loop.run_until_complete(asyncio.wait(tasks))
            
            pass
    
    

第七章 selenium模組

7.1 瀏覽器驅動

7.2 基本使用方法

  • 示例一:

    from selenium import webdriver
    
    # 例項化一個瀏覽器物件(傳入瀏覽器的驅動程式)
    bro = webdriver.Chrome(executable_path='./chromedriver')
    """
    browser = webdriver.Firefox()
    browser = webdriver.Edge()
    browser = webdriver.PhantomJS()
    browser = webdriver.Safari()
    """
    
    # 瀏覽器發起請求:get
    bro.get('https://www.taobao.com/')
    
    # 獲取當前頁面的原始碼資料:page_source
    page_text = bro.page_source
    
    # 標籤定位:find_element...,返回標籤
    search_input = bro.find_element_by_id('q')
    # 標籤定位:find_elements...,返回標籤列表
    search_input = bro.find_elements_by_id('q')
    
    # 標籤互動:send_keys、clear
    search_input.send_keys('iPhone')
    search_input.clear()
    
    # 執行一組js程式:execute_script
    bro.execute_script('window.scrollTo(0, document.body.scrollHeight)')
    
    # 點選搜尋按鈕
    btn = bro.find_element_by_css_selector('.btn-search')
    btn.click()
    
    # 瀏覽器發起新的請求:get
    bro.get('https://www.baidu.com')
    
    # 回退
    bro.back()
    
    # 前進
    bro.forward()
    
    # 關閉瀏覽器
    bro.quit()
    
    

7.3 動作鏈 + iframe處理

  • 示例一:

    from selenium import webdriver
    
    # 匯入動作鏈對應的類
    from selenium.webdriver import ActionChains
    
    bro = webdriver.Chrome(executable_path='./chromedriver')
    bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
    
    # 定位的標籤存在於iframe標籤之中,必須切換瀏覽器標籤定位的作用域(通過id查詢iframe)
    bro.switch_to.frame('iframeResult')  
    div = bro.find_element_by_id('draggable')
    
    # 動作鏈
    action = ActionChains(bro)
    
    # 點選並長按指定的標籤
    action.click_and_hold(div)
    
    for i in range(5):
        # move_by_offset(x,y):x水平方向 y豎直方向;perform()立即執行動作鏈操作
        action.move_by_offset(17, 0).perform()
    
    # 釋放動作鏈
    action.release()
    
    # 關閉瀏覽器
    bro.quit()
    
    
  • 示例二:模擬登入QQ空間

    from selenium import webdriver
    from time import sleep
    
    bro = webdriver.Chrome(executable_path='./chromedriver')
    bro.get('https://qzone.qq.com/')
    
    # 切換到iframe作用域
    bro.switch_to.frame('login_frame')
    
    # 切換到賬號密碼登入
    a_tag = bro.find_element_by_id("switcher_plogin")
    a_tag.click()
    
    # 賬號密碼
    userName_tag = bro.find_element_by_id('u')
    password_tag = bro.find_element_by_id('p')
    sleep(1)
    userName_tag.send_keys('*******')
    sleep(1)
    password_tag.send_keys('*******')
    sleep(1)
    btn = bro.find_element_by_id('login_button')
    btn.click()
    
    sleep(5)
    
    # 關閉瀏覽器
    bro.quit()
    
    

7.4 無視覺化頁面 + 規避檢測

  • 示例一:

    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)
    
    bro.get('https://www.baidu.com')
    
    print(bro.page_source)
    
    bro.quit()
    
    

7.5 12306登入示例

  • 示例一:

    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)
    
    # 找到驗證碼圖片標籤
    code_img_ele = bro.find_element_by_xpath('//*[@id="loginForm"]/div/ul[2]/li[4]/div/div/div[3]/img')
    # 圖片標籤左上角座標:{'x': 366, 'y': 274}
    location = code_img_ele.location
    # 圖片標籤尺寸:{'height': 190, 'width': 293}
    size = code_img_ele.size
    # 圖片標籤左上角和右下角座標
    rangle = (
        int(location['x']), int(location['y']),
        int(location['x'] + size['width']), int(location['y'] + size['height']),
    )
    
    # 將當前頁面截圖並儲存
    bro.save_screenshot('aa.png')
    i = Image.open('./aa.png')
    # 根據指定區域對圖片進行裁剪
    frame = i.crop(rangle)
    frame.save('./code.png')
    
    # 將驗證碼圖片提交給超級鷹進行識別
    chaojiying = Chaojiying_Client('賬號', '密碼', '軟體ID')  
    im = open('code.png', 'rb').read() 
    result = chaojiying.PostPic(im, 9004)['pic_str']
    
    # 要儲存即將被點選的點的座標:[[x1, y1], [x2, y2]]
    all_list = []  
    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)
        
    # 遍歷列表,使用動作鏈對每一個列表元素對應的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(1)
    
    # 賬號密碼
    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()
    
    

7.6 phantomJS模組

  • PhantomJS是一款無介面的瀏覽器,其自動化操作流程和上述操作谷歌瀏覽器是一致的。由於是無介面的,為了能夠展示自動化操作流程,PhantomJS為使用者提供了一個截圖的功能,使用save_screenshot函式實現。

    from selenium import webdriver
    
    # phantomjs路徑
    bro = webdriver.PhantomJS('PhantomJS驅動路徑')
    # 開啟百度
    bro.get('http://www.baidu.com/')
    # 截圖
    bro.save_screenshot(r'phantomjs\baidu.png')
    
    

7.7 Pyppeteer模組

  • 示例一:

    import asyncio
    from pyppeteer import launch
    from lxml import etree
    
    async def main():
        # 新建一個 bro 瀏覽器物件:headless無頭模式、args關閉提示條:”Chrome 正受到自動測試軟體的控制”
        bro = await launch(headless=False, args=['--disable-infobars'])
        # 在瀏覽器中新建了一個選項卡
        page = await bro.newPage()
        # page 物件呼叫了 goto 方法就相當於在瀏覽器中輸入了這個 URL
        await page.goto('http://quotes.toscrape.com/js/')
        # 當前瀏覽器頁面的原始碼
        page_text = await page.content()
        tree = etree.HTML(page_text)
        div_list = tree.xpath('//div[@class="quote"]')
        print(len(div_list))
        await bro.close()
    asyncio.get_event_loop().run_until_complete(main())
    

第八章 scrapy框架

8.1 基本使用方法

  • 建立一個專案

    scrapy startproject '專案名'
    
  • 建立一個爬蟲檔案

    cd '專案名'
    scrapy genspider '爬蟲檔名' www.xxx.com
    
  • 啟動專案

    scrapy crawl '爬蟲檔名'
    

8.2 基本配置

  • settings.py

    # UA偽裝
    USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36'
    
    # robots協議
    ROBOTSTXT_OBEY = False
    
    # 日誌級別
      LOG_LEVEL = 'ERROR'
    
    
  • 爬蟲檔案.py

    class TxtSpider(scrapy.Spider):
        name = 'txt'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['http://xxx.com']  # 起始爬取的url
    
    

8.3 儲存

  • 指令

    # 執行輸出指定格式進行儲存:將爬取到的資料寫入不同格式的檔案中進行儲存
    scrapy crawl '爬蟲名稱' -o 'xxx.json'
    scrapy crawl '爬蟲名稱' -o 'xxx.xml'
    scrapy crawl '爬蟲名稱' -o 'xxx.csv'
    
  • 管道

    # items.py:
    		# 1、定義資料屬性。
    import scrapy
    class XxxItem(scrapy.Item):
    		name = scrapy.Field()
    
        
    # 爬蟲檔案.py:
    		# 1、將爬取到的資料封裝到items物件中。
      	# 2、使用yield將items物件提交給pipelines管道。
    def parse(self, response):
        item = XxxItem()
        item['name'] = name
        yield item
    
        
    # pipelines.py:
    		# 1、process_item方法中接收item物件。
      	# 2、將item物件中儲存的資料進行持久化儲存。
        
    class XxxPipeline(object):
        def __init__(self):
            self.fp = None  
        # 重寫父類方法:開始爬蟲時,執行一次
        def open_spider(self,spider):
            print('爬蟲開始')
            self.fp = open('./data.txt', 'w')
        # 重寫父類方法:該方法會被執行呼叫多次
        def process_item(self, item, spider):
            #將爬蟲程式提交的item進行持久化儲存
            self.fp.write(item['author'] + ':' + item['content'] + '\n')
            return item
        # 重寫父類方法:結束爬蟲時,執行一次
        def close_spider(self,spider):
            self.fp.close()
            print('爬蟲結束')
    
    class XxxPipeline_2(object):
    	  pass
            
    # settings.py:
    		# 1、開啟管道
    ITEM_PIPELINES = {
    		'xxx.pipelines.XxxPipeline': 300, # 300表示為優先順序,值越小優先順序越高
     		'xxx.pipelines.XxxPipeline_2': 200, 
    }
    
    

8.4 分頁

  • 爬蟲檔案.py

    class TxtSpider(scrapy.Spider):
        name = 'txt'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['http://xxx.com']  # 起始爬取的url
        
        # 分頁URL模版
        url = 'https://www.qiushibaike.com/text/page/%s/'
        pageNumMax = 100 # 最大頁碼
        pageNum = 1 # 起始頁碼
        
        def parse(self, response):
            pass
          	# 爬取所有分頁資料
            if self.pageNum <= self.pageNumMax:
                self.pageNum += 1
                url = format(self.url % self.pageNum)
                # 遞迴爬取資料:callback引數的值為回撥函式
                yield scrapy.Request(url=url, callback=self.parse)
       
    

8.5 詳情頁

  • 爬蟲檔案.py

    class TxtSpider(scrapy.Spider):
        name = 'txt'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['http://xxx.com']  # 起始爬取的url
        
        # 詳情頁
        def parse_detail(self, response):
          	# 呼叫引數
          	item = response.meta['item']
            pass
            yield item
    		
        # 當前頁面
        def parse(self, response, **kwargs):
            article_url_list = response.xpath('//div[@class="Volume"]//dd/a/@href').extract()
            for article_url in article_url_list:
                pass
              	# 請求傳參:meta={},可以將meta字典傳遞給請求對應的回撥函式
                yield scrapy.Request(article_url, callback=self.parse_detail, meta={'item': item})
    
    

8.6 提高效率

  • 增加併發

    # 預設scrapy開啟的併發執行緒為32個
    CONCURRENT_REQUESTS = 100
    
  • 降低日誌級別

    # 在執行scrapy時,會有大量日誌資訊的輸出,為了減少CPU的使用率。可以設定log輸出資訊為INFO或者ERROR即可
    LOG_LEVEL = 'INFO'
    LOG_LEVEL = 'ERROR'
    
  • 禁止cookie

    # 如果不是真的需要cookie,則在scrapy爬取資料時可以禁止cookie從而減少CPU的使用率,提升爬取效率
    COOKIES_ENABLED = False
    
  • 禁止重試

    # 對失敗的HTTP進行重新請求(重試)會減慢爬取速度,因此可以禁止重試
    RETRY_ENABLED = False
    
  • 減少下載超時

    # 如果對一個非常慢的連結進行爬取,減少下載超時可以讓卡住的連結快速被放棄,從而提升效率
    DOWNLOAD_TIMEOUT = 10 # 超時時間為10s
    

8.7 圖片

  • settinss.py

    # 指定圖片儲存的目錄
    IMAGES_STORE = './img'
    
  • pipelines.py

    from scrapy.pipelines.images import ImagesPipeline
    import scrapy
    
    
    class imgsPileLine(ImagesPipeline):
    
        # 根據圖片地址進行圖片資料的請求
        def get_media_requests(self, item, info):
            yield scrapy.Request(item['img_url'])
    
        # 指定圖片完整路徑 + 圖片名
        def file_path(self, request, response=None, info=None, *, item=None):
            imgName = request.url.split('/')[-1].split('?')[0]
            print('下載完成!')
            return imgName
    
        def item_completed(self, results, item, info):
          	# 返回給下一個即將被執行的管道類
            return item  
          
    

8.8 中介軟體(代理IP)

  • settings.py

    DOWNLOADER_MIDDLEWARES = {
       'xxx.middlewares.XxxDownloaderMiddleware': 543,
    }
    
  • middlewares.py

    import random
    
    class XxxDownloaderMiddleware(object):
      
        # 對請求設定隨機的User-Agent
        user_agent_list = [
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 "
            "(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
            "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 "
            "(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 "
            "(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 "
            "(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
            "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 "
            "(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
            "(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
            "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 "
            "(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
            "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
            "(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 "
            "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
            "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 "
            "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
        ]
        
        # 對請求設定隨機的代理
        PROXY_http = [
            '153.180.102.104:80',
            '195.208.131.189:56055',
        ]
        PROXY_https = [
            '120.83.49.90:9000',
            '95.189.112.214:35508',
        ]
    
        # 攔截請求
        def process_request(self, request, spider):
            # UA偽裝
            request.headers['User-Agent'] = random.choice(self.user_agent_list)
            # 為了驗證代理的操作是否生效
            request.meta['proxy'] = 'http://183.146.213.198:80'
            return None
    
        # 攔截所有的響應
        def process_response(self, request, response, spider):
          	pass
            return response
    
        # 攔截髮生異常的請求
        def process_exception(self, request, exception, spider):
            if request.url.split(':')[0] == 'http':
                # 代理
                request.meta['proxy'] = 'http://' + random.choice(self.PROXY_http)
            else:
                request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)
            return request  # 將修正之後的請求物件進行重新的請求傳送
    
    

8.9 瀏覽器物件selenium

  • 爬蟲檔案.py

    from selenium import webdriver
    
    class TxtSpider(scrapy.Spider):
        name = 'txt'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['http://xxx.com']  # 起始爬取的url
        
        models_urls = []  # 儲存需要動態載入的URL
        
        # 例項化一個瀏覽器物件
        def __init__(self):
            self.bro = webdriver.Chrome(executable_path='./瀏覽器驅動路徑')
            
        pass
      
      	# 重寫父類方法:爬蟲結束時被呼叫,關閉瀏覽器物件
        def closed(self, spider):
          	self.bro.quit()
    
    
  • middlewares.py

    from scrapy.http import HtmlResponse
    from time import sleep
    
    class XxxDownloaderMiddleware(object):
      
        # 攔截請求
        def process_request(self, request, spider):
            pass
            return None
    
        # 攔截所有的響應
        def process_response(self, request, response, spider): 
          	# 獲取爬蟲類中定義的瀏覽器物件
            bro = spider.bro  
    
            # 挑選出指定的響應物件進行篡改
            if request.url in spider.models_urls:
                bro.get(request.url) 
                sleep(3)
                # 包含了動態載入的新聞資料
                page_text = bro.page_source  
    						# 篡改後的相應物件
                new_response = HtmlResponse(url=request.url, body=page_text, encoding='utf-8', request=request)
                return new_response
            else:
                # 其他請求對應的響應物件
                return response
    
        # 攔截髮生異常的請求
        def process_exception(self, request, exception, spider):
            pass
            return request
    
    

8.10 CrawlSpider類

  • 建立一個專案

    scrapy startproject '專案名'
    
  • 建立一個CrawlSpider爬蟲檔案

    cd '專案名'
    scrapy genspider -t crawl '爬蟲檔名' www.xxx.com
    
  • 啟動專案

    scrapy crawl '爬蟲檔名'
    
  • 爬蟲檔案.py

    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    from sunPro.items import SunproItem, DetailItem
    
    
    # 需求:爬取sun網站中的編號,新聞標題,新聞內容,標號
    class SunSpider(CrawlSpider):
        name = 'sun'
        # allowed_domains = ['www.xxx.com']
        start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=']
    
        # 連結提取器:根據指定規則(allow="正則")進行指定連結的提取
        # 分頁
        link = LinkExtractor(allow=r'type=4&page=\d+')
        # 詳情頁
        link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml')
        
        # 規則解析器:將連結提取器提取到的連結進行指定規則(callback)的解析操作
        rules = (
          	# follow=True:可以將連結提取器 繼續作用到 連線提取器提取到的連結 所對應的頁面中,即找到所有分頁
            Rule(link, callback='parse_item', follow=True),
            Rule(link_detail, callback='parse_detail')
        )
    
        # 如下兩個解析方法不可以實現請求傳參,可以依次儲存到兩個item
        # 解析新聞編號和新聞的標題
        def parse_item(self, response):
            # 注意:xpath表示式中不可以出現tbody標籤
            tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
            for tr in tr_list:
                new_num = tr.xpath('./td[1]/text()').extract_first()
                new_title = tr.xpath('./td[2]/a[2]/@title').extract_first()
                item = SunproItem()
                item['title'] = new_title
                item['new_num'] = new_num
    
                yield item
    
        # 解析新聞內容和新聞編號
        def parse_detail(self, response):
            new_id = response.xpath('/html/body/div[9]/table[1]//tr/td[2]/span[2]/text()').extract_first()
            new_content = response.xpath('/html/body/div[9]/table[2]//tr[1]//text()').extract()
            new_content = ''.join(new_content)
            item = DetailItem()
            item['content'] = new_content
            item['new_id'] = new_id
    
            yield item
            
    

第九章 分散式

9.1 scrapy

  • 排程器不可以被分散式機群共享
  • 管道不可以被分散式機群共享

9.1 scrapy-redis

  • 可以給原生的scrapy框架提供可以被共享的管道和排程器

9.2 基本使用方法

  • 建立一個專案

    scrapy startproject '專案名'
    
  • 建立一個CrawlSpider爬蟲檔案

    cd '專案名'
    scrapy genspider -t crawl '爬蟲檔名' www.xxx.com
    
  • 爬蟲檔案.py

    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    from fbsPro.items import FbsproItem
    from scrapy_redis.spiders import RedisCrawlSpider
    
    
    class FbsSpider(RedisCrawlSpider):
        name = 'fbs'
    
        # 將start_urls和allowed_domains進行註釋
        # allowed_domains = ['www.xxx.com']
        # start_urls = ['http://www.xxx.com/']
    
        # 新增一個新屬性,可以被共享的排程器佇列的名稱
        redis_key = 'sun'
    
        # 和CrawlSpider類似
        rules = (
            Rule(LinkExtractor(allow=r'type=4&page=\d+'), callback='parse_item', follow=True),
        )
    
        def parse_item(self, response):
            tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
            for tr in tr_list:
                new_num = tr.xpath('./td[1]/text()').extract_first()
                new_title = tr.xpath('./td[2]/a[2]/@title').extract_first()
    
                item = FbsproItem()
                item['title'] = new_title
                item['new_num'] = new_num
    
                yield item       
    
  • settings.py

    # 指定scrapy-redis的管道
    ITEM_PIPELINES = {
        'scrapy_redis.pipelines.RedisPipeline': 400
    }
    
    # 指定排程器
    # 增加了一個去重容器類的配置, 作用是用Redis的set集合來儲存請求的指紋資料, 從而實現請求去重的持久化
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    # 使用scrapy-redis元件自己的排程器
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    # 配置排程器是否要持久化, 也就是當爬蟲結束了, 要不要清空Redis中請求佇列和去重指紋的set。如果是True, 就表示要持久化儲存, 就不清空資料, 否則清空資料
    SCHEDULER_PERSIST = True
    
    # 指定redis資料庫
    REDIS_HOST = '127.0.0.1'  # redis遠端伺服器的ip(修改)
    REDIS_PORT = 6379
    
    
  • 修改redis.conf

    # 註釋掉bind 127.0.0.1
    # IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
    # JUST COMMENT THE FOLLOWING LINE.
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # bind 127.0.0.1
    
    # 關閉保護模式
    protected-mode no
    
    
  • 結合著配置檔案開啟redis服務

    ./redis-server ../redis.conf
    
  • 啟動客戶端

    ./redis-cli 
    
  • 執行工程

    cd 專案名/spiders
    scrapy runspider '爬蟲檔案.py'
    
  • 在客戶端的終端,向排程器的佇列中放入一個起始的URL

    lpush '可以被共享的排程器佇列的名稱' 'www.xxx.com'
    
  • 爬取到的資料儲存在了redis的proName:items這個資料結構中

9.3 redis基本指令

  • 客戶端

    keys *  # 檢視所有表
    lrange proName:items 0 -1  # 檢視所有記錄 
    llen proName:items  # 查看錶中的記錄個數
    smembers urls  # 檢視所有記錄 
    
    # 關閉資料庫
    redis-cli -p 埠號 shutdown
    

第十章 增量式

10.1 基於redis的set資料結構

  • 爬蟲檔案.py

    import scrapy
    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    
    from redis import Redis
    from moviePro.items import MovieproItem
    
    
    class MovieSpider(CrawlSpider):
        name = 'movie'
        # allowed_domains = ['www.ccc.com']
        start_urls = ['https://www.4567tv.tv/frim/index1.html']
    
        rules = (
            Rule(LinkExtractor(allow=r'/frim/index1-\d+\.html'), callback='parse_item', follow=True),
        )
    
        # 1、建立redis連結物件
        conn = Redis(host='127.0.0.1', port=6379)
    
        # 用於解析每一個頁碼對應頁面中的電影詳情頁的url
        def parse_item(self, response):
            li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li')
            for li in li_list:
                # 獲取詳情頁的url
                detail_url = 'https://www.4567tv.tv' + li.xpath('./div/a/@href').extract_first()
    
                # 2、將詳情頁的url存入redis的set中,實現去重
                ex = self.conn.sadd('urls', detail_url)
                if ex == 1:
                    print('該url沒有被爬取過,可以進行資料的爬取')
                    yield scrapy.Request(url=detail_url, callback=self.parst_detail)
                else:
                    print('資料爬取過!')
    
        # 解析詳情頁中的電影名稱和型別,進行持久化儲存
        def parst_detail(self, response):
            item = MovieproItem()
            pass
            yield item