『No15: Go 实现 python 库 fake-useragent』

go-15.png
15.png

大家好,我叫谢伟,是一名程序员。

在未来人人都是手艺人。

传统的手艺人在圈子内创造影响力,在互联网时代,个人影响力不仅仅限于圈子内,互联网创造无限可能性。

好,今天的主题:作品意识

1、作品意识

作品很好理解,比如歌手发行唱片、发行单曲,作家写书等,前端工程师可能更容易出作品,比如,写一个优雅的网站,比如写一个优雅的工具,写一个实用的小程序,开发一个个人的APP等,这些都是作品。

后端人员,可以写库,虽然在和真实用户交互层面,后端工程师开发的工具大概只能在程序员内使用,或者有一定编程基础的人才能使用。

尽管不是每个人写的工具都能得到广泛的认可、使用。作为程序员,一定要有作品意识,什么意思?

  • 写用户交互友好的工具
  • 写符合人性的工具
  • 写简洁的工具

对于后端开发者来说最终体现出来的作品便是“不断的创造轮子、或者重复造轮子”。

关于是否重复造轮子,知乎有广泛的讨论,我的观点是:想要提高个人的技能水平,需要不断的造轮子,水平低,造简单的轮子,水平渐渐提升,造更高级点的轮子。在过程参考优秀开源工具的实现、思想。

模仿是最简单的学习方式

2、如何产出作品

在工作之余,我较长时间放在 Github 上。去发现一些好的项目,去参考一些好的效果。当然作为后端开发者,我不太会去看前端的项目,同样,作为Gopher ,我倾向于 关注 Go 项目,但如果是火热的 Python 项目,那我也会关注下。

随着关注点的越来越精细,我倾向于从我熟悉的东西入手,什么意思,为什么从熟悉的东西入手,因为我越来越发现,自信心是很重要的,如果你不能第一时间对一个项目提起兴趣和自信心,你可能没什么机会和这个项目产生化学反应。

近期在阅读 requests-html

  • 我比较熟悉爬虫
  • 这是一个网页信息解析的库
  • 代码量不是很大,阅读起来简便
import sys
import asyncio
from urllib.parse import urlparse, urlunparse, urljoin
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures._base import TimeoutError
from functools import partial
from typing import Set, Union, List, MutableMapping, Optional

import pyppeteer
import requests
from pyquery import PyQuery

from fake_useragent import UserAgent
from lxml.html.clean import Cleaner
import lxml
from lxml import etree
from lxml.html import HtmlElement
from lxml.html import tostring as lxml_html_tostring
from lxml.html.soupparser import fromstring as soup_parse
from parse import search as parse_search
from parse import findall, Result
from w3lib.encoding import html_to_unicode

从导入的库来看,这个网页解析的库主要是整合了各种第三方库和内置库,这意味着,很少有直接从零开始造轮子,可以借助已经比较优秀的第三方库。

好,关于这个库的分析,我下次讲。

取其中的一个库来分析:fake_useragent

这是一个生成UserAgent 的库,可以指定浏览器的类型,也可以随机生成UserAgent。

我一不小心对它产生了兴趣。

网上一般的讲解如何随机生存UserAgent 的处理方法是,在本地缓存一个大的文件,随机从文件内取一个。当然这看上去不够极客唉。

1、使用

from fake_useragent import UserAgent
ua = UserAgent()

ua.ie
# Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US);
ua.msie
# Mozilla/5.0 (compatible; MSIE 10.0; Macintosh; Intel Mac OS X 10_7_3; Trident/6.0)'
ua['Internet Explorer']
# Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.2; SV1; .NET CLR 3.3.69573; WOW64; en-US)
ua.opera
# Opera/9.80 (X11; Linux i686; U; ru) Presto/2.8.131 Version/11.11
ua.chrome
# Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2'
ua.google
# Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13
ua['google chrome']
# Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11
ua.firefox
# Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1
ua.ff
# Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1
ua.safari
# Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25

# and the best one, random via real world browser usage statistic
ua.random

用法十分简单,毕竟完成的功能就不是很复杂。

  • 随机生成
  • 指定浏览器生成

2、阅读 fake_useragent 文档

fake_useragent

可以看出,其实是从网上抓取到的信息,在随机生成的。

那作者如何选择浏览器?只选择这几个浏览器的理由是?

image.png

可以查看当前最主流的浏览器的一些数据。

由于一些大家都知道的原因,这个网站的访问需要借助一些手段。

Sometimes, useragentstring.com or w3schools.com changes their html, or down, in such case fake-useragent uses CDN cloudfront fallback

意味着还可以从这个网站上获取UserAgent

3、梳理

  • 这是一个获取 UserAgent 的库
  • 主要的数据来自两个网站
  • 根据统计数据得出主流的浏览器

本质是一个爬虫

4、源代码

    def load(self):
        try:
            with self.load.lock:
                if self.cache:
                    self.data = load_cached(
                        self.path,
                        use_cache_server=self.use_cache_server,
                        verify_ssl=self.verify_ssl,
                    )
                else:
                    self.data = load(
                        use_cache_server=self.use_cache_server,
                        verify_ssl=self.verify_ssl,
                    )

                # TODO: change source file format
                # version 0.1.4+ migration tool
                self.data_randomize = list(self.data['randomize'].values())
                self.data_browsers = self.data['browsers']
        except FakeUserAgentError:
            if self.fallback is None:
                raise
            else:
                logger.warning(
                    'Error occurred during fetching data, '
                    'but was suppressed with fallback.',
                )
    def __getattr__(self, attr):
        if attr in self.safe_attrs:
            return super(UserAgent, self).__getattr__(attr)

        try:
            for value, replacement in settings.REPLACEMENTS.items():
                attr = attr.replace(value, replacement)

            attr = attr.lower()

            if attr == 'random':
                browser = random.choice(self.data_randomize)
            else:
                browser = settings.SHORTCUTS.get(attr, attr)

            return random.choice(self.data_browsers[browser])
        except (KeyError, IndexError):
            if self.fallback is None:
                raise FakeUserAgentError('Error occurred during getting browser')  # noqa
            else:
                logger.warning(
                    'Error occurred during getting browser, '
                    'but was suppressed with fallback.',
                )

                return self.fallback

核心代码是这两个函数。

数据来源:


CACHE_SERVER = 'http://d2g6u4gh6d9rq0.cloudfront.net/browsers/fake_useragent_{version}.json'.format(  # noqa
    version=__version__,
)

BROWSERS_STATS_PAGE = 'https://www.w3schools.com/browsers/default.asp'

BROWSER_BASE_PAGE = 'http://useragentstring.com/pages/useragentstring.php?name={browser}'  # noqa

5、实现

你已经知道了这是个Python 库。好了,你也了解了这个库的核心代码和思想。

你下一步怎么做?

重新实现。

你可以选择 Python 实现,但是在你看源代码的过程中,你的思维应该已经受这个库的具体处理方式影响了。

所以,你可以用其他语言进行处理,核心思想还在,但规则变了。所以你会花费一点点的思考。这样其实对你自己有好处。

5.1、项目组织

按照领域驱动的思想,将项目区分为四层:UI、Infra、Domain、Application

├─application
├─domain
│  ├─global
│  └─parse
├─infra
│  └─download
├─main
├─ui

infra层:


package download

import (
    "net/http"

    "errors"

    "github.com/PuerkitoBio/goquery"
)

var (
    ErrRequest  = errors.New("request err")
    ErrResponse = errors.New("response err")
)

func ResponseDownload(url string) (*goquery.Document, error) {
    request, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, ErrRequest
    }

    request.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36")
    client := http.DefaultClient

    response, err := client.Do(request)
    if err != nil {
        return nil, ErrResponse
    }

    defer response.Body.Close()
    return goquery.NewDocumentFromReader(response.Body)
}

本质是一个爬虫,获取网页信息少不了这个函数。

domain 层:

package global

import "fmt"

const Version = "0.1.10"

var (
    BROWSERS_STATS_PAGE = "https://www.w3schools.com/browsers/default.asp"
    BROWSER_BASE_PAGE   = "http://useragentstring.com/pages/useragentstring.php?name=%s"
    CACHE_SERVER        = "http://d2g6u4gh6d9rq0.cloudfront.net/browsers/fake_useragent_%s.json"
)

var OVERRIDES = make(map[string]string)
var LOCALUSERAGENT = make(map[string][]string)

func init() {
    OVERRIDES = map[string]string{
        "Edge/IE": "Internet Explorer",
        "IE/Edge": "Internet Explorer",
    }
    CACHE_SERVER = fmt.Sprintf(CACHE_SERVER, Version)
}

全局参数,主要是上面提到的网站信息。

  • BROWSERS_STATS_PAGE
  • BROWSER_BASE_PAGE
  • CACHE_SERVER
package parse

import (
    "errors"

    "github.com/PuerkitoBio/goquery"
    "github.com/tidwall/gjson"
)

var (
    ErrArray   = errors.New("array err")
    ErrInvalid = errors.New("invalid json")
)

func CloudFront(doc *goquery.Document, browserType string) ([]gjson.Result, error) {

    if !gjson.Valid(doc.Text()) {
        return nil, ErrInvalid
    }

    jsonResult := gjson.Parse(doc.Text())
    browserUserAgent := jsonResult.Get("browsers." + browserType)
    browserUserAgentOk := browserUserAgent.IsArray()
    if !browserUserAgentOk {
        return nil, ErrArray
    }
    return browserUserAgent.Array(), nil

}

cloudfront 网站获取UserAgent

package parse

import (
    "github.com/PuerkitoBio/goquery"
)

var browserList = []string{
    "Internet Explorer",
    "Opera",
    "Chrome",
    "Safari",
    "FireFox",
}

func UserAgentCom(doc *goquery.Document) ([]string, error) {

    var newBrowserList = make([]string, 1)

    doc.Find("div#liste ul li").Each(func(i int, selection *goquery.Selection) {
        userAgent := selection.Find("a").Text()
        //fmt.Println(userAgent)
        newBrowserList = append(newBrowserList, userAgent)
    })
    return newBrowserList, nil

}

useragentstring.com 网站获取 UserAgent

Application 层:

package application

import (
    "errors"
    "fake-user-agent-go-ng/domain/global"
    "fake-user-agent-go-ng/infra/download"
    "fmt"

    "fake-user-agent-go-ng/domain/parse"

    "math/rand"
    "time"

    "github.com/PuerkitoBio/goquery"
    "github.com/tidwall/gjson"
)

type FakeUserAgent struct {
    UserAgentStringOk bool
    CloudFrontNetOk   bool
    Cache             bool
}

var (
    ErrUserAgent = errors.New("user agent err")
)

func NewFakeUserAgent(UserAgentStringOk bool, CloudFrontNetOk bool, CacheOK bool) *FakeUserAgent {
    return &FakeUserAgent{
        UserAgentStringOk: UserAgentStringOk,
        CloudFrontNetOk:   CloudFrontNetOk,
        Cache:             CacheOK,
    }
}

func (F *FakeUserAgent) IE() string {
    return F.common("Internet+Explorer")

}

func (F *FakeUserAgent) InternetExplorer() string {
    return F.IE()
}

func (F *FakeUserAgent) Msie() string {
    return F.IE()
}

func (F *FakeUserAgent) Chrome() string {
    return F.common("Chrome")
}

func (F *FakeUserAgent) Google() string {
    return F.Chrome()
}

func (F *FakeUserAgent) Opera() string {
    return F.common("Opera")
}

func (F *FakeUserAgent) Safari() string {
    return F.common("Safari")
}

func (F *FakeUserAgent) FireFox() string {
    return F.common("Firefox")
}

func (F *FakeUserAgent) FF() string {
    return F.FireFox()
}

func (F *FakeUserAgent) Random() string {
    randomChoice := []string{
        "Chrome",
        "Firefox",
        "Safari",
        "Opera",
        "Internet+Explorer",
    }
    r := rand.NewSource(time.Now().UnixNano())
    random := rand.New(r)

    browserType := randomChoice[random.Intn(len(randomChoice))]
    return F.common(browserType)
}

func (F *FakeUserAgent) common(browserType string) string {
    r := rand.NewSource(time.Now().Unix())
    randomChoice := rand.New(r)
    if F.Cache {
        index := randomChoice.Intn(len(global.LOCALUSERAGENT[browserType]))
        return global.LOCALUSERAGENT[browserType][index]

    }

    var url string
    if F.UserAgentStringOk {
        url = fmt.Sprintf(global.BROWSER_BASE_PAGE, browserType)
    } else {
        url = global.CACHE_SERVER
    }

    var (
        doc *goquery.Document
        err error
    )

    doc, err = download.ResponseDownload(url)

    if err != nil {
        fmt.Println(ErrUserAgent)
        panic(ErrUserAgent)
    }
    var (
        userAgentList []string
    )

    if F.UserAgentStringOk {
        userAgentList, err = parse.UserAgentCom(doc)
        if err != nil {
            fmt.Println(ErrUserAgent)
            panic(ErrUserAgent)
        }
        return userAgentList[randomChoice.Intn(len(userAgentList))]
    }

    if F.CloudFrontNetOk {
        var userAgentResult []gjson.Result
        userAgentResult, err = parse.CloudFront(doc, browserType)
        if err != nil {
            fmt.Println(ErrUserAgent)
            panic(ErrUserAgent)
        }
        return userAgentResult[randomChoice.Intn(len(userAgentResult))].String()
    }
    return ""

}

主要是实现这几个函数:

  • Random 随机得到一个 UserAgent
  • IE/Msie/InternetExplorer 返回IE 浏览器UserAgent
  • FF/FireFox 返回 FireFox 浏览器UserAgent
  • Google/Chrome 返回 Chrome 浏览器UserAgent
  • Opera 返回 Opera 浏览器UserAgent

同时使用下面几个参数,决定从哪个网站抓取,还是使用本地缓存。

6. 开源

[图片上传失败...(image-cb2b7f-1531152156667)]

欢迎使用唉。

本节完,再会,我是谢伟。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 周末,六点三十分,许是习惯唤醒了我,许是米色半遮光窗帘外的晨曦唤醒了我,总之,再不复入眠。 起身蹑手...
    梦1212阅读 539评论 0 1
  • 亲爱的儿子,今天周五,你依然没有写作业,明明多说好了,为什么不做呢?也许是妈妈没有说明白!晚上我们一起上了课,认识...
    Cindycindycindy阅读 119评论 0 0
  • 你如今稳重利落温婉可人的模样你年少时曾设想过无数次,你只是不曾想真的有一天你成长成了这般模样,你总是唏嘘着过去说就...
    M丶八月阅读 951评论 3 10