Python爬蟲 - 記一次字型反爬

2h0n9發表於2019-04-21

前言

最近一直在為找工作煩惱,剛好遇到一家公司要求我先做幾道反爬蟲的題,看了之後覺得自己還挺菜的,不過也過了幾關,剛好遇到一個之前沒遇到過的反爬蟲手段 — 字型反爬

正文

一、站點分析

題目要求:這裡有一個網站,分了1000頁,求所有數字的和。注意,是人看到的數字,不是網頁原始碼中的數字哦~

頁面

就這,從圖裡能看出數字的字型有些不同,看看原始碼是什麼樣的

網頁原始碼

可以看到,原始碼裡的內容和網頁上顯示的內容根本不一樣,當然,題目也說了;那麼這是怎麼回事呢,切換到 Network 欄,重新整理網頁看看請求

network內容

可以看到,這裡有兩個字型請求,選擇後可以預覽字型

字型預覽

很明顯,數字有點問題,被改過了,上面那一個請求的字型檔案是正常的字型(下圖),可以拿來做比較,以便於我們分析

正常字型

一般來說字型檔案的數字就是這樣的順序 1 2 3 4 5 6 7 8 9 0 ,以這個為模板,被修改後的字型中的數字 2 處與 正常字型9 的位置。回到網頁原始碼和內容,網頁上顯示 274 ,實際原始碼中是 920(下圖),用上面的字型做替換我們會發現,2 在被 修改過的字型 中的位置是 8 ,而 8正常字型 中就是 8,由此可得結論:我們只要把這 修改過的字型 搞到手,然後把網頁上顯示的內容逐個拆分為單個數字,然後從字型中匹配出正常字型就行了,不過,根據題目,我們需要反著來做,也就是從原始碼入手,獲取到內容後拆分為單個字型,接著從字型中獲取網頁上顯示的內容。

對比

我自己寫的時候都覺得頭暈,直接寫程式碼,這樣能更好的表達我要說什麼,不過,這裡要說一點,據我分析,這個網頁有1000頁,每一頁的字型都是不同的,就需要每獲取一個網頁就得重新獲取被修改的字型。我這裡用的是 scrapy 框架。

二、程式碼階段

首先新建一個scrapy專案

➜  ~ scrapy startproject glidedsky
New Scrapy project 'glidedsky', using template directory '/usr/local/lib/python3.7/site-packages/scrapy/templates/project', created in:
    /Users/zhonglizhen/glidedsky

You can start your first spider with:
    cd glidedsky
    scrapy genspider example example.com
➜  ~
複製程式碼

接著建立一個Spider

➜  ~ cd glidedsky 
➜  ~ glidedsky scrapy genspider glidedsky glidesky.com
Cannot create a spider with the same name as your project
➜  ~ glidedsky
複製程式碼
scrapy 怎麼用我就不說了,直接看程式碼
# glidedsky.py
import scrapy
import requests
import re

from glidedsky.items import GlidedskyItem
from glidedsky.spiders.config import *


class GlidedskySpider(scrapy.Spider):
    name = 'glidedsky'
    start_urls = ['http://glidedsky.com/level/web/crawler-font-puzzle-1']

    def __int__(self):
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36',
        }

    def request(self, url, callback):
        request = scrapy.Request(url=url, callback=callback)
        # 新增 cookies
        request.cookies['XSRF-TOKEN'] = XSRF_TOKEN
        request.cookies['glidedsky_session'] = glidedsky_session
        # 新增 headers
        request.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36'
        return request

    def start_requests(self):
        for i, url in enumerate(self.start_urls):
            yield self.request(url, self.parse_item)

    def parse_item(self, response):
        """
        解析numbers
        :param response:
        :return:
        """
        body = response.css('html').get()
        self.save_font(body)
        col_md_nums = response.css('.col-md-1::text').extract()
        items = GlidedskyItem()
        for col_md_num in col_md_nums:
          	# 這裡獲取到的是原始碼中的內容,並不是我們在網頁上看到的內容,需要去資料管道進一步處理
            items['numbers'] = col_md_num.replace('\n', '').replace(' ', '')
            yield items
        # 獲取下一頁
        next = response.xpath('//li/a[@rel="next"]')
        # 判斷是否有下一頁
        if len(next) > 0:
            next_page = next[0].attrib['href']
            # response.urljoin 可以幫我們構造下一頁的連結
            url = response.urljoin(next_page)
            yield self.request(url=url, callback=self.parse_item)

    def save_font(self, body):
        """
        儲存字型到本地
        :param response: 網頁原始碼
        :return:
        """
        pattern = r'src:.url\("(.*?)"\).format\("woff"\)'
        woff_font_url = re.findall(pattern, body, re.S)
        print(woff_font_url)
        resp = requests.get(woff_font_url[0], headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36'})
        with open(WOFF_FONT_FILENAME, 'wb') as f:
            f.write(resp.content)
複製程式碼

在解析字型之前先分析一下字型檔案的內容,因為這裡面有坑(起碼我這個站點是這樣),下載好字型後,用python的 fontTools 庫把 woff格式 轉成 xml檔案,然後開啟;或者用 font-creator 直接開啟,但是這個工具只有windows上有,所以這裡就用第一種方法。

1、先把 woff格式 轉成 xml格式 檔案

import requests
from fontTools.ttLib import TTFont

# 先把字型檔案下載下來
url = "https://guyujiezi.com/fonts/LQ1K9/1A7s3D.woff"
filename = url.split('/')[-1]
resp = requests.get(url)
with open(filename, 'wb') as f:
    f.write(resp.content)
# 接著用 TTFont 開啟檔案
font = TTFont(filename)
# TTFont 中有一個 saveXML 的方法
font.saveXML(filename.replace(filename.split('.')[-1], 'xml'))
複製程式碼

2、用文字編輯器開啟

只需要看 GlyphOrder 項就行了,其實直接看 GlyphOrder 一個屁都看不出來,完全和之前做的分析不一樣,不過仔細觀察後發現這裡面也被人做了手腳,1703589624 這跟電話號碼一樣的就是上面看到的 修改後的字型 預覽到的,可能這樣還是看不出什麼;其中 id 屬性的值為 修改後的字型 中的數字,name 屬性為 正常字型,但是根本不對,之前算過,網頁中的 274,正常內容是 920,而下面,2 明顯對應著 zero ,其實我在這裡被坑了,如果把 2+1=33 不就是對應著 nine 了嗎,然後發現後面 74 也是對應著 20,有 12GlyphID 的目的就是坑我們的(我猜的),不過這確實挺坑的。分析過後可以開始寫程式碼了

GlyphOrder

3、程式碼如下,這是 pipelines.py 檔案

# pipelines.py
# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
from scrapy.exceptions import DropItem

from fontTools.ttLib import TTFont
from glidedsky.spiders.config import *


class GlidedskyPipeline(object):

    result = 0

    def process_item(self, item, spider):
        if item['numbers']:
            numbers = item['numbers']
            #print("@@@@@ 假數字: %s \n" % numbers)
            font = TTFont(WOFF_FONT_FILENAME) # 首先建立一個TTFont物件,引數為字型檔案的路徑
            true_number = "" 
            for num in range(len(numbers)):
                fn = NUMBER_TEMP[numbers[num]] # 從模版中獲取數字對應著的英語單詞
                glyph_id = int(font.getGlyphID(fn)) - 1 # font.getGlyphID 方法是根據GlyphID name屬性獲取id屬性的值,引數傳入name值,最後減一
                true_number += str(glyph_id)
            self.result += int(true_number)
            print("@@@@@ 計算結果: %d" % self.result)

        else:
            return DropItem('Missing Number.')

複製程式碼

config.py

DATA_PATH = '/Volumes/HDD500G/Documents/Python/Scrapy/glidedsky/glidedsky/data' # 這是我為了儲存字型檔案新建的資料夾
WOFF_FONT_FILENAME = DATA_PATH + '/woff-font.woff'
XSRF_TOKEN = ''
glidedsky_session = ''
NUMBER_TEMP = {'1': 'one', '2': 'two', '3': 'three', '4': 'four', '5': 'five', '6': 'six', '7': 'seven', '8': 'eight', '9': 'nine', '0': 'zero'} # 這個模版是為了方便我計算,題目需要
複製程式碼

items.py

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class GlidedskyItem(scrapy.Item):
    # define the fields for your item here like:
    numbers = scrapy.Field()
複製程式碼

settings.py,設定我就不全部貼了,只貼需要改的部分

# 這本來是註釋掉了的
ITEM_PIPELINES = {
   'glidedsky.pipelines.GlidedskyPipeline': 300,
}
複製程式碼

接著直接執行即可

➜ cd /你專案儲存地址/glidedsky/
➜ scrapy startpoject glidedsky
複製程式碼

輸出結果就不展示了,賊雞兒多

結論

這種反爬蟲手段是我第一次遇到,以前遇到的也就驗證碼和ip限制,不過也算是漲了知識,最後結果是我解決了

相關文章