精华内容
下载资源
问答
  • 对从字进行解析
    万次阅读
    2020-02-22 12:15:05

    1.1 PDFBOX介绍

    Apache PDFBox是一个开源Java库,支持PDF文档的开发和转换。 我们可以使用PDFBox开发可以创建,转换和操作PDF文档的Java程序。PDFBox的主要功能:

    • Extract Text - 使用PDFBox,您可以从PDF文件中提取Unicode文本。
    • Split & Merge - 使用PDFBox,您可以将单个PDF文件分成多个文件,并将它们合并为一个文件。
    • Fill Forms - 使用PDFBox,您可以在文档中填写表单数据。
    • Print - 使用PDFBox,您可以使用标准Java打印API打印PDF文件。
    • Save as Image - 使用PDFBox,您可以将PDF保存为图像文件,如PNG或JPEG。
    • Create PDFs - 使用PDFBox,您可以通过创建Java程序创建新的PDF文件,还可以包含图像和字体。
    • Signing - 使用PDFBox,您可以将数字签名添加到PDF文件。

    PDFBox的四个主要组成部分:

    • PDFBox - 这是PDFBox的主要部分。 它包含与内容提取和操作相关的类和接口。
    • FontBox - 它包含与font相关的类和接口,使用这些类我们可以修改PDF文档的文本字体。
    • XmpBox - 包含处理XMP元数据的类和接口。
    • Preflight - 此组件用于根据PDF/A-1b标准验证PDF文件。

    PDFBOX操作PDF文档基本实现:

    • 创建PDF文档( Creating a PDF Document)
    • 添加页面( Adding Pages)
    • 删除页面( Removing Pages)
    • 读写文档属性( Document Properties)
    • 添加文本( Adding Text)
    • 添加多行( Adding Multiple Lines)
    • 读取文本( Reading Text)
    • 插入图像( Inserting Image)
    • 加密PDF文档(Encrypting a PDF Document)
    • 拆分PDF文档( Splitting a PDF Document)
    • 合并多个PDF文档(Merging Multiple PDF Documents)
    • 从PDF文档生成图像(Save Images)
    • 添加矩形( Adding Rectangles)

    详细参考PDFBOX官网

    1.2 项目需求分析

    项目中需要根据既有的电子图书进行信息化的归档,方便分类目查询。因为图书主要是以如下排版存在:
    在这里插入图片描述

    相对来说页面分为左右两部分,右侧是知识点的描述,比较规范。左侧是图文的说明,为了生动,各种形式,大小都有,基本上没有规律,所以无法进行解析。最终敲定方案如下:

    右侧描述根据章节来生成知识点描述,可以按章节或者关键字检索。对应描述的左侧部分,需要整体生成辅助说明一个图片。辅助说明相应的知识点内容。

    拆分成技术实现为:

    • 指定区域进行PDF文字内容提取
    • 指定区域进行PDF图片内容提取

    1.3 代码实现

    1.3.1 SpringBoot工程引入PDFBox依赖

    POM.xml

        <!-- https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox -->
    	<dependency>
    	    <groupId>org.apache.pdfbox</groupId>
    	    <artifactId>pdfbox</artifactId>
    	    <version>2.0.18</version>
    	</dependency>
    

    PdfboxUtils.java

    package com.gavinbj.common.utils;
    
    import java.awt.Rectangle;
    import java.awt.image.BufferedImage;
    import java.io.File;
    
    import org.apache.pdfbox.pdmodel.PDDocument;
    import org.apache.pdfbox.pdmodel.PDPage;
    import org.apache.pdfbox.rendering.PDFRenderer;
    import org.apache.pdfbox.text.PDFTextStripperByArea;
    
    public class PdfboxUtils {
    
    	public static final String REGION_NAME = "content";
    
    	
    	/**
    	 * 根据指定文件页码的指定区域读取文字
    	 * 
    	 * @param filePath PDF文件路径
    	 * @param iPage PDF页码
    	 * @param textRrect 读取文字的区域
    	 * @return 文字内容
    	 */
    	public static String readRectangelText(String filePath, int iPage, Rectangle textRrect) {
    
    		String textContent = "";
    		
    		try(PDDocument pdfDoc = PDDocument.load(new File(filePath))) {
    			// 获取指定的PDF页
    			PDPage pdfPage = pdfDoc.getPage(iPage);
    
    			// 获取指定位置的文字(文字剥离器)
    			PDFTextStripperByArea textStripper = new PDFTextStripperByArea();
    			textStripper.setSortByPosition(true);
    			textStripper.addRegion(REGION_NAME, textRrect);
    			textStripper.extractRegions(pdfPage);
    
    			textContent = textStripper.getTextForRegion(REGION_NAME);
    
    			// 释放资源
    			pdfDoc.close();
    		} catch (Exception ex) {
    			ex.printStackTrace();
    		} 
    
    		return textContent;
    
    	}
    	
    	/**
    	 * 根据指定文件页码的指定区域读取图片
    	 * 
    	 * @param filePath PDF文件路径
    	 * @param iPage PDF页码
    	 * @param imgRrect 读取图片的区域
    	 * @return 图片内容
    	 */
    	public static BufferedImage readRectangelImage(String filePath, int iPage, Rectangle imgRrect) {
    
    		BufferedImage bufImage = null;
    		try(PDDocument pdfDoc = PDDocument.load(new File(filePath))) {
    			// 获取渲染器,主要用来后面获取BufferedImage
    			PDFRenderer pdfRenderer = new PDFRenderer(pdfDoc);
    			// 截取指定位置生产图片
    			bufImage = pdfRenderer.renderImage(iPage).getSubimage(imgRrect.x,imgRrect.y,imgRrect.width,imgRrect.height); 
    
                // 释放资源
    			pdfDoc.close();
    		} catch (Exception ex) {
    			ex.printStackTrace();
    		} 
    
    		return bufImage;
    	}
    	
    }
    
    

    PdfTest.java:测试类

    package com.gavinbj.common.utils;
    
    import java.awt.Rectangle;
    import java.awt.image.BufferedImage;
    import java.io.File;
    import java.io.IOException;
    
    import javax.imageio.ImageIO;
    
    public class PdfTest {
    	
    	public static void main(String[] args) throws IOException {
    		
    	    String pdfPath="C:\\PDFDemos\\pdf\\9.pdf";
    	    
    	    Rectangle textRrect = new Rectangle(420, 80, 130, 245);
    	    
    	    String strContent = PdfboxUtils.readRectangelText(pdfPath, 0, textRrect);
    	    
    		System.out.println(strContent);
    		
    		// 保存图片
    		Rectangle imgRrect = new Rectangle(0, 76, 420, 240);
    		BufferedImage bufImage = PdfboxUtils.readRectangelImage(pdfPath, 0, imgRrect);
    		
    		File outputfile = new File("D:\\pdfImage2.png");
            ImageIO.write(bufImage, "png", outputfile);
    		
    	}
    	
    }
    

    执行测试结果:

    一、太阳辐射
    1 …概念:太阳源源不断地
    以电磁波的形式向四周放射能
    量,被称为太阳辐射。
    太阳辐射携带巨大的能量。
    地球所接收到的太阳辐射能量仅
    为太阳向宇宙空间放射的总辐射
    能量的二十亿分之一。
    在日地平均距离处与太
    阳光垂直的地球大气上界单位
    面积上在单位时间内所接收到
    的太阳辐射的所有波长的总能
    量,称为太阳常数。
    

    在这里插入图片描述

    结论:从上面例子可以看出使用PDFBOX相应的方法可以读取指定位置的文字。

    并可以从指定位置提取图片并保存,不同于直接读取PDF中内嵌的对象,可以根据你的指定区域直接截取相应的内容作为图片保存,但是处理速度不敢恭维,特别慢。暂时没有想到优化的办法。

    更多相关内容
  • 看到本文,相信以后你任何形式额字体反爬都能见招拆招。 文章目录深度剖析自定义字体解析自定义字体的介绍Python加载页面解析顶部导航栏分类和地点列表解析字体对应css的下载URL解析css获取自定义字体的URL

    大家好,我的小小明。
    前面我在《Python处理超强反爬(TSec防火墙+CSS图片背景偏移定位)》一文中讲解如何解析css图片背景偏移的数据,并通过图像识别提取文字。

    本文将带你解析各种形式自定义字体,绘制点阵图,并通过图像识别提取出关系列表,最终校对后构建正确的对应关系,最终获取到正确的数据。

    看到本文,相信以后你对任何形式额字体反爬都能见招拆招。

    深度剖析自定义字体解析

    自定义字体的介绍

    首先,我们必须要清楚自定义字体与普通字体的区别,自定义字体定义了一些特殊的Unicode编码对应的点阵图数据,而普通字体只是定义标准编码的显示形式,所以普通字体渲染的数据可以直接复制出正确的文本,而自定义字体只能复制到对应的Unicode编码。

    那么游览器如何显示出对应的字符呢?那是因为游览器会根据自定义字体的对应关系,渲染对应的点阵图进行显示。

    下面我们以某团购网站为例进行演示。

    这次我分析的页面是深圳休闲娱乐

    image-20210802161659847

    image-20210802162026699

    可以看到自定义字体都存在于svgmtsi标签中,不同的class属性也对应了不同自定义字体文件。

    如果我们取消所有的自定义字体的加载,可以看到网页上对应的位置都会出现乱码:

    image-20210802162640117

    从上图也可以看到,产生自定义字体的位置完全是随机的。

    对于这种情况,我们最好使用可以修改HTML DOM树的库来维持节点的相对顺序,我选择了BeautifulSoup这个库,可惜只支持css选择器。

    不过也好,早期我学编程用Java玩小爬虫的时候就更喜欢css选择器,正好可以找回久违的感觉。

    接下来我们一步步分析页面,首先用python读取页面数据:

    Python加载页面

    import requests
    
    headers = {
        "Connection": "keep-alive",
        "Cache-Control": "max-age=0",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
        "Accept-Language": "zh-CN,zh;q=0.9"
    }
    session = requests.Session()
    session.headers = headers
    res = session.get("http://www.dianping.com/shenzhen/ch30")
    

    下面我们使用BeautifulSoup解析下载的页面,构建DOM树:

    from bs4 import BeautifulSoup
    
    soup = BeautifulSoup(res.text, 'html5lib')
    

    关于BeautifulSoup可以查看官方文档:

    (上面两个链接内容一样,目录形式有区别)

    解析顶部导航栏分类和地点列表

    由于现在该团购网站翻第二页就要求登录,咱们也没有打算真的要爬它。所以我通过多下载几个分类链接,来模拟批量下载的效果。

    下面准备解析出下面这些对应的标题:

    image-20210802165248267

    通过xpath查询工具获取到xpath后,就可以转换为css选择器。

    分类列表:

    # //div[@id='classfy']/a/span
    type_list = []
    for a_tag in soup.select("div#classfy > a"):
        type_list.append((a_tag.span.text, a_tag['href']))
    type_list
    
    [('按摩/足疗', 'http://www.dianping.com/shenzhen/ch30/g141'),
     ('KTV', 'http://www.dianping.com/shenzhen/ch30/g135'),
     ('洗浴/汗蒸', 'http://www.dianping.com/shenzhen/ch30/g140'),
     ('酒吧', 'http://www.dianping.com/shenzhen/ch30/g133'),
     ('运动健身', 'http://www.dianping.com/shenzhen/ch30/g2636'),
     ('茶馆', 'http://www.dianping.com/shenzhen/ch30/g134'),
     ('密室', 'http://www.dianping.com/shenzhen/ch30/g2754'),
     ('团建拓展', 'http://www.dianping.com/shenzhen/ch30/g34089'),
     ('采摘/农家乐', 'http://www.dianping.com/shenzhen/ch30/g20038'),
     ('剧本杀', 'http://www.dianping.com/shenzhen/ch30/g50035'),
     ('游戏厅', 'http://www.dianping.com/shenzhen/ch30/g137'),
     ('DIY手工坊', 'http://www.dianping.com/shenzhen/ch30/g144'),
     ('私人影院', 'http://www.dianping.com/shenzhen/ch30/g20041'),
     ('轰趴馆', 'http://www.dianping.com/shenzhen/ch30/g20040'),
     ('网吧/电竞', 'http://www.dianping.com/shenzhen/ch30/g20042'),
     ('VR', 'http://www.dianping.com/shenzhen/ch30/g33857'),
     ('桌面游戏', 'http://www.dianping.com/shenzhen/ch30/g6694'),
     ('棋牌室', 'http://www.dianping.com/shenzhen/ch30/g32732'),
     ('文化艺术', 'http://www.dianping.com/shenzhen/ch30/g142'),
     ('新奇体验', 'http://www.dianping.com/shenzhen/ch30/g34090')]
    

    地点列表:

    # //div[@id='region-nav']/a/span
    area_list = []
    for a_tag in soup.select("div#region-nav > a"):
        area_list.append((a_tag.span.text, a_tag['href']))
    area_list
    
    [('福田区', 'http://www.dianping.com/shenzhen/ch30/r29'),
     ('南山区', 'http://www.dianping.com/shenzhen/ch30/r31'),
     ('罗湖区', 'http://www.dianping.com/shenzhen/ch30/r30'),
     ('盐田区', 'http://www.dianping.com/shenzhen/ch30/r32'),
     ('龙华区', 'http://www.dianping.com/shenzhen/ch30/r12033'),
     ('龙岗区', 'http://www.dianping.com/shenzhen/ch30/r34'),
     ('宝安区', 'http://www.dianping.com/shenzhen/ch30/r33'),
     ('坪山区', 'http://www.dianping.com/shenzhen/ch30/r12035'),
     ('光明区', 'http://www.dianping.com/shenzhen/ch30/r89951'),
     ('南澳大鹏新区', 'http://www.dianping.com/shenzhen/ch30/r12036')]
    

    解析字体对应css的下载URL

    经观察可以发现,定义自定义字体的css文件在链接带有svgtextcss关键字的url中:

    image-20210802170838134

    我们可以从所有的定义css样式的链接中找到含有svgtextcss关键字的链接:

    from urllib import parse
    
    def getUrlFromNode(nodes, tag):
        for node in nodes:
            url = node['href']
            if url.find(tag) != -1:
                return parse.urljoin(base_url, url)
    
    
    def get_css_url(soup):
        css_url = getUrlFromNode(soup.select(
            "head > link[rel=stylesheet]"), "svgtextcss")
        return css_url
    
    
    css_url = get_css_url(soup)
    css_url
    
    'http://s3plus.meituan.net/v1/mss_0a06a471f9514fc79c981b5466f56b91/svgtextcss/18379bbeb1f5bf54c52bb1d8b71d4fb1.css'
    

    解析css获取自定义字体的URL

    格式化定义字体的css文件:

    image-20210802172030864

    可以看到,class定义了使用的字体名称,font-face定义了每个字体名称对应的字体文件。

    虽然现在我们可以看到规律每个class就是加了一个PingFangSC-Regular-的前缀作为字体名称,但是我们无法保证以后该网站依然会这样设计,为了保证以后在这个点上面不需要改代码,我们依然还是解析出每个class对应的font-family,再解析出每个font-family对应的多个字体URL,最终多个字体URL取后缀为.woff格式的URL,建立class属性到woff字体的映射关系。

    下面是完整代码:

    import re
    
    
    def get_url(urls, tag, only_First=True):
        urls = [parse.urljoin(base_url, url)
                for url in urls if tag is None or url.find(tag) != -1]
        if urls and only_First:
            return urls[0]
        return urls
    
    
    def parseCssFontUrl(css_url, tag=None, only_First=True):
        res = session.get(css_url)
        rule = {}
        font_face = {}
        for name, value in re.findall("([^{}]+){([^{}]+)}", res.text):
            name = name.strip()
            for row in value.split(";"):
                if row.find(":") == -1:
                    continue
                k, v = row.split(":")
                k, v = k.strip(), v.strip(' "\'')
                if name == "@font-face":
                    if k == "font-family":
                        font_name = v
                    elif k == "src":
                        font_face.setdefault(font_name, []).extend(
                            re.findall("url\(\"([^()]+)\"\)", v))
                else:
                    rule[name[1:]] = v
        font_urls = {}
        for class_name, tag_name in rule.items():
            font_urls[class_name] = get_url(font_face[tag_name], tag)
        return font_urls
    
    
    font_urls = parseCssFontUrl(css_url, ".woff", only_First=False)
    font_urls
    
    {'shopNum': 'http://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/89e46c52.woff',
     'tagName': 'http://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/f8536a55.woff',
     'reviewTag': 'http://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/0373a060.woff',
     'address': 'http://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/f8536a55.woff'}
    

    下载字体

    我们可以将上述四个字体都下载下来看看:

    def download_file(url, out_name=None):
        if out_name is None:
            out_name = url[url.rfind("/")+1:]
        with open(out_name, "wb") as f:
            f.write(session.get(url).content)
    
    for class_name, url in font_urls.items():
        download_file(url, f"{class_name}.woff")
    

    下载后得到4个字体文件:

    image-20210802173222464

    想要本地查看字体,我们可以通过FontCreator字体设计工具,百度一下可以直接搜索到下载链接。

    打开后:

    image-20210802174851524

    经过对比发现四个文件的点阵图顺序完全一致,不同的只是编码与点阵图的关系。

    建立自定义字体映射关系

    下面我们需要分析对于指定字体每个被定义的Unicode字符对应的真实字符。由于字体文件中存储的字符的点阵图,本质是图片而不是文本,所以我们无法复制出来。但我们可以考虑通过PIL加载自定义字体,然后将每个被定义的Unicode字符画出相应的点阵图,再进行图像识别,就可以获取相应的文本数据了。

    这里需要使用fontTools工具,可以直接使用pip安装。

    详见:https://github.com/fonttools/fonttools

    以class等于tagName的字体为例,先获取其被定义的Unicode字符列表:

    from fontTools.ttLib import TTFont
    
    tfont = TTFont("tagName.woff")
    # 去掉前2个扩展字符
    uni_list = tfont.getGlyphOrder()[2:]
    print(uni_list[:10], len(uni_list))
    
    ['uniec3e', 'unif3fc', 'uniea1f', 'unie7f7', 'unie258', 'unif5aa', 'unif48c', 'unif088', 'unif588', 'unif82e'] 601
    

    这里打印了前10个Unicode代码点,共有601个自定义字符。

    打印结果也与上面的截图中FontCreator字体设计工具查看的结果一致。

    使用PIL绘图工具,先绘制前5个代码点测试一下:

    from PIL import ImageFont, Image, ImageDraw
    
    font = ImageFont.truetype("tagName.woff", 20)
    for uchar in uni_list[:5]:
        unknown_char = f"\\u{uchar[3:]}".encode().decode("unicode_escape")
        im = Image.new(mode='RGB', size=(22, 20), color="white")
        draw = ImageDraw.Draw(im=im)
        draw.text(xy=(5, -5), text=unknown_char, fill=0, font=font)
        display(im)
    

    绘制结果:

    image-20210802181849772

    可以看到能够正确绘制出相应的点阵图。

    下面再测试每n个代码点为一组一起绘制,减少后面图像识别的次数(这里设置n=25,绘制5组):

    n = 25
    font = ImageFont.truetype("tagName.woff", 20)
    for i in range(0, 5*n, n):
        im = Image.new(mode='RGB', size=(20*n+10, 22), color="white")
        draw = ImageDraw.Draw(im=im)
        unknown_chars = "".join(uni_list[i:i + n]).replace("uni", "\\u")
        unknown_chars = unknown_chars.encode().decode("unicode_escape")
        draw.text(xy=(5, -4), text=unknown_chars, fill=0, font=font)
        display(im)
    

    绘制结果:

    image-20210802181925813

    封装一下,批量获取一个字体文件的全部图片对象:

    from fontTools.ttLib import TTFont
    from PIL import ImageFont, Image, ImageDraw
    
    
    def getCustomFontGroupImgs(font_file, uni_list=None, group_num=25):
        if uni_list is None:
            tfont = TTFont(font_file)
            uni_list = tfont.getGlyphOrder()[2:]
        imgs = []
        font = ImageFont.truetype(font_file, 20)
        for i in range(0, len(uni_list), group_num):
            im = Image.new(mode='RGB', size=(20*group_num+10, 22), color="white")
            draw = ImageDraw.Draw(im=im)
            unknown_chars = "".join(uni_list[i:i + group_num]).replace("uni", "\\u")
            unknown_chars = unknown_chars.encode().decode("unicode_escape")
            draw.text(xy=(5, -4), text=unknown_chars, fill=0, font=font)
            imgs.append(im)
        return imgs
    

    pytesseract默认不支持对中文的识别,需要较多的配置。这次我们直接使用一个最近比较流行的库叫带带弟弟orc来进行图像识别,一行命令即可安装:

    pip install ddddocr
    

    使用示例和参数可以查看:https://pypi.org/project/ddddocr/

    不过该库只支持传图片字节和base64编码,不支持直接传入图片对象,需要二次转换。

    可以定义一个将图片转字节的方法:

    from io import BytesIO
    
    def get_img_bytes(img):
        img_byte = BytesIO()
        im.save(img_byte, format='JPEG')  # format: PNG or JPEG
        return img_byte.getvalue()  # im对象转为二进制流
    

    然后就可以以如下形式进行批量识别:

    from ddddocr import DdddOcr
    
    imgs = getCustomFontGroupImgs('shopNum.woff', group_num=50)
    ocr = DdddOcr()
    result = []
    for im in imgs:
        display(im)
        text = ocr.classification(get_img_bytes(im))
        print(text)
        result.append(text)
    

    效果如下:

    image-20210802193638811

    整体来说准确率还是非常高的。

    我最终还是决定直接继承DdddOcr类,重写识别方法优化算法(以后再考虑自行开发图像识别类):

    from ddddocr import DdddOcr, np
    
    
    class OCR(DdddOcr):
        def __init__(self):
            super().__init__()
    
        def ocr(self, image):
            image = image.resize(
                (int(image.size[0] * (64 / image.size[1])), 64), Image.ANTIALIAS).convert('L')
            image = np.array(image).astype(np.float32)
            image = np.expand_dims(image, axis=0) / 255.
            image = (image - 0.5) / 0.5
            ort_inputs = {'input1': np.array([image])}
            ort_outs = self._DdddOcr__ort_session.run(None, ort_inputs)
            result = []
            last_item = 0
            for item in ort_outs[0][0]:
                if item == 0 or item == last_item:
                    continue
                result.append(self._DdddOcr__charset[item])
                last_item = item
            return ''.join(result)
    

    然后这样调用:

    imgs = getCustomFontGroupImgs('shopNum.woff', group_num=42)
    ocr = OCR()
    result = []
    for im in imgs:
        display(im)
        text = ocr.ocr(im)
        print(text)
        result.append(text)
    

    image-20210803100946648

    可以看到经过继承调整后的代码,识别准确率更高了些。

    最终我们人工校对修改后,得到如下字符集:

    words = '1234567890店中美家馆小车大市公酒行国品发电金心业商司超生装园场食有新限天面工服海华水房饰城乐汽香部利子老艺花专东肉菜学福饭人百餐茶务通味所山区门药银农龙停尚安广鑫一容动南具源兴鲜记时机烤文康信果阳理锅宝达地儿衣特产西批坊州牛佳化五米修爱北养卖建材三会鸡室红站德王光名丽油院堂烧江社合星货型村自科快便日民营和活童明器烟育宾精屋经居庄石顺林尔县手厅销用好客火雅盛体旅之鞋辣作粉包楼校鱼平彩上吧保永万物教吃设医正造丰健点汤网庆技斯洗料配汇木缘加麻联卫川泰色世方寓风幼羊烫来高厂兰阿贝皮全女拉成云维贸道术运都口博河瑞宏京际路祥青镇厨培力惠连马鸿钢训影甲助窗布富牌头四多妆吉苑沙恒隆春干饼氏里二管诚制售嘉长轩杂副清计黄讯太鸭号街交与叉附近层旁对巷栋环省桥湖段乡厦府铺内侧元购前幢滨处向座下澩凤港开关景泉塘放昌线湾政步宁解白田町溪十八古双胜本单同九迎第台玉锦底后七斜期武岭松角纪朝峰六振珠局岗洲横边济井办汉代临弄团外塔杨铁浦字年岛陵原梅进荣友虹央桂沿事津凯莲丁秀柳集紫旗张谷的是不了很还个也这我就在以可到错没去过感次要比觉看得说常真们但最喜哈么别位能较境非为欢然他挺着价那意种想出员两推做排实分间甜度起满给热完格荐喝等其再几只现朋候样直而买于般豆量选奶打每评少算又因情找些份置适什蛋师气你姐棒试总定啊足级整带虾如态且尝主话强当更板知己无酸让入啦式笑赞片酱差像提队走嫩才刚午接重串回晚微周值费性桌拍跟块调糕'
    

    字体文件中的Unicode代码点则与上述字符集字符一一对应。

    由于该网站所有的自定义字体的点阵图都是这个顺序,所以我们不再需要解析其他的字体文件获取这个字符列表。当然这个团购网站以后还打算变态到每个字体文件的点阵图顺序也随机,那我只能说,真狠。那到时候我再考虑升级自己的代码,因为我个人的目标就是没有我解析不了的数据。

    有了点阵图对应的字符集,咱们就可以轻松建立字体文件的映射关系:

    from fontTools.ttLib import TTFont
    
    font_data = TTFont("tagName.woff")
    uni_list = font_data.getGlyphOrder()[2:]
    font_map = dict(zip(map(lambda x: x[3:], uni_list), words))
    

    字体缓存器

    针对该团购网站,由于我们无法保证所有页面用这一个相同的css文件,所以我们需要建立一个css的URL到字体文件URL和字体文件URL到对应字体映射关系的二级缓存:

    from io import BytesIO
    
    url2FontMapCache = {}
    css2FontCache = {}
    
    
    def getFontMapFromURL(font_url):
        "缓存字体URL对应字体映射关系"
        if font_url not in url2FontMapCache:
            font_bytes = BytesIO(session.get(font_url).content)
            font_data = TTFont(font_bytes)
            uni_list = font_data.getGlyphOrder()[2:]
            url2FontMapCache[font_url] = dict(
                zip(map(lambda x: x[3:], uni_list), words))
        return url2FontMapCache[font_url]
    
    
    def getFontMapFromClassName(class_name, css_url):
        "缓存指定css文件对应字体URL"
        if css_url not in css2FontCache:
            css2FontCache[css_url] = parseCssFontUrl(css_url, ".woff")
        font_url = css2FontCache[css_url].get(class_name)
        return getFontMapFromURL(font_url)
    

    可以获取当前页面下,每个自定义字体的映射关系:

    for class_name in font_urls.keys():
        font_map = getFontMapFromClassName(class_name, css_url)
        print(list(font_map.items())[:12])
    

    结果:

    [('e0a7', '1'), ('ebf3', '2'), ('ee9b', '3'), ('e7e4', '4'), ('f5f8', '5'), ('e7a1', '6'), ('ef49', '7'), ('eef7', '8'), ('f7e0', '9'), ('e633', '0'), ('e5de', '店'), ('e67f', '中')]
    [('ec3e', '1'), ('f3fc', '2'), ('ea1f', '3'), ('e7f7', '4'), ('e258', '5'), ('f5aa', '6'), ('f48c', '7'), ('f088', '8'), ('f588', '9'), ('f82e', '0'), ('e7c5', '店'), ('e137', '中')]
    [('e3e0', '1'), ('e85f', '2'), ('f3c8', '3'), ('f3d5', '4'), ('e771', '5'), ('f251', '6'), ('f6f6', '7'), ('e8da', '8'), ('ea58', '9'), ('f8fb', '0'), ('ef9b', '店'), ('f3dd', '中')]
    [('ec3e', '1'), ('f3fc', '2'), ('ea1f', '3'), ('e7f7', '4'), ('e258', '5'), ('f5aa', '6'), ('f48c', '7'), ('f088', '8'), ('f588', '9'), ('f82e', '0'), ('e7c5', '店'), ('e137', '中')]
    

    将所有自定义字体全部替换为正常文字

    有了字体映射关系,我们就可以对页面的自定义字体替换成我们解析好的文本数据。

    首先获取被替换的父节点列表,方便对比:

    b_tags = [svgmtsi.parent for svgmtsi in soup.find_all('svgmtsi')]
    b_tags
    

    image-20210803101837065

    虽然我们现在看到该网站每个svgmtsi标签只存放一个字符,但无法确保以后也依然如此,所以我们的代码现在就考虑一个svgmtsi标签内部存在多个字符的情况。

    执行替换:

    for svgmtsi in soup.find_all('svgmtsi'):
        class_name = svgmtsi['class'][0]
        font_map = getFontMapFromClassName(class_name, css_url)
        chars = []
        for c in svgmtsi.text:
            char = c.encode("unicode_escape").decode()[2:]
            chars.append(font_map[char])
        svgmtsi.replaceWith("".join(chars))
    

    替换后,再查看之前保存的节点:

    b_tags
    

    image-20210803101902441

    提取数据

    将自定义字体替换之后,我们就可以非常丝滑的提取需要的数据了:

    num_rule = re.compile("\d+")
    
    for li_tag in soup.select("div#shop-all-list div.txt"):
        title = li_tag.select_one("div.tit>a>h4").text
        url = li_tag.select_one("div.tit>a")["href"]
        star_class = li_tag.select_one(
            "div.comment>div.nebula_star>div.star_icon>span")["class"]
        star = int(num_rule.findall(" ".join(star_class))[0])//10
    
        comment_tag = li_tag.select_one("div.comment>a.review-num>b")
        comment_num = comment_tag.text if comment_tag else None
    
        mean_price_tag = li_tag.select_one("div.comment>a.mean-price>b")
        mean_price = mean_price_tag.text if mean_price_tag else None
    
        fun_type = li_tag.select_one("div.tag-addr>a:nth-of-type(1)>span.tag").text
        area = li_tag.select_one("div.tag-addr>a:nth-of-type(2)>span.tag").text
        print(title, url, star, comment_num, mean_price, fun_type, area)
    
    轰趴天台·大白之家(南山店) http://www.dianping.com/shop/k1lqueFI6sIOfjnI 5 129 ¥196 轰趴馆 华侨城
    巨鹿搏击俱乐部(车公庙店) http://www.dianping.com/shop/k4tmabQaordrq6Tm 5 238 ¥246 拳击 车公庙
    SWING CAGE 棒球击球笼&冲浪滑板碗池 http://www.dianping.com/shop/l2lUP0rvcLPy4ebm 5 1088 ¥85 体育场馆 科技园
    微醺云深处沉浸式剧场 http://www.dianping.com/shop/HaLzYuXfvUWmPLSz 0 8 None 剧本杀 科技园
    逐见有光Chandelle http://www.dianping.com/shop/H2CSpvtn70y12wNh 5 138 ¥281 DIY手工坊 市中心/会展中心
    cozy cozy银饰DIY手作室(万象城店) http://www.dianping.com/shop/l3mCIs6dSrdL9jo7 5 1270 ¥321 DIY手工坊 万象城
    博哥的小剧场沉浸推理体验馆(南山万象天地店) http://www.dianping.com/shop/H3S4zD55e1gmfb8E 3 18 None 剧本杀 科技园
    FlowLife拓极滑板冲浪俱乐部(蛇口旗舰店) http://www.dianping.com/shop/G9sHgWISxYtBXz79 5 481 ¥217 新奇体验 蛇口
    Doors秘道·独立剧情密室(车公庙分店) http://www.dianping.com/shop/k4O3oDj6BwLtbgD4 5 878 ¥101 密室 车公庙
    御隆茶馆 http://www.dianping.com/shop/H6HEuBttJKlMkaAn 0 3 None 棋牌室 南头
    【十万伏特】手创空间 自由DIY http://www.dianping.com/shop/k5OURy1bNIs7ed7v 5 271 ¥152 DIY手工坊 梅林
    八町桑BATTING SOUND 棒球体验馆 http://www.dianping.com/shop/k9yQRAmYoa3o8cLI 5 734 ¥114 新奇体验 车公庙
    星美棋牌 http://www.dianping.com/shop/l4JRIjqLWi2zeFQd 3 9 None 棋牌室 国贸
    ZUO STUDIO烘焙课程· 茶歇蛋糕订购(南山京基百纳广场... http://www.dianping.com/shop/ER0EyDpjx36ekF0G 5 687 ¥224 DIY手工坊 白石洲
    cozy cozy银饰DIY手作室(南山店) http://www.dianping.com/shop/G7MbwkosLSvS3X1I 5 431 ¥338 DIY手工坊 南头
    

    批量下载

    经过以上测试,我们可以将所有相关方法都封装一下,下面我们下载深圳华南城的所有娱乐相关的团购信息:

    import re
    from bs4 import BeautifulSoup
    import requests
    import pandas as pd
    import random
    import time
    from urllib import parse
    from io import BytesIO
    from fontTools.ttLib import TTFont
    
    url2FontMapCache = {}
    css2FontCache = {}
    words = '1234567890店中美家馆小车大市公酒行国品发电金心业商司超生装园场食有新限天面工服海华水房饰城乐汽香部利子老艺花专东肉菜学福饭人百餐茶务通味所山区门药银农龙停尚安广鑫一容动南具源兴鲜记时机烤文康信果阳理锅宝达地儿衣特产西批坊州牛佳化五米修爱北养卖建材三会鸡室红站德王光名丽油院堂烧江社合星货型村自科快便日民营和活童明器烟育宾精屋经居庄石顺林尔县手厅销用好客火雅盛体旅之鞋辣作粉包楼校鱼平彩上吧保永万物教吃设医正造丰健点汤网庆技斯洗料配汇木缘加麻联卫川泰色世方寓风幼羊烫来高厂兰阿贝皮全女拉成云维贸道术运都口博河瑞宏京际路祥青镇厨培力惠连马鸿钢训影甲助窗布富牌头四多妆吉苑沙恒隆春干饼氏里二管诚制售嘉长轩杂副清计黄讯太鸭号街交与叉附近层旁对巷栋环省桥湖段乡厦府铺内侧元购前幢滨处向座下澩凤港开关景泉塘放昌线湾政步宁解白田町溪十八古双胜本单同九迎第台玉锦底后七斜期武岭松角纪朝峰六振珠局岗洲横边济井办汉代临弄团外塔杨铁浦字年岛陵原梅进荣友虹央桂沿事津凯莲丁秀柳集紫旗张谷的是不了很还个也这我就在以可到错没去过感次要比觉看得说常真们但最喜哈么别位能较境非为欢然他挺着价那意种想出员两推做排实分间甜度起满给热完格荐喝等其再几只现朋候样直而买于般豆量选奶打每评少算又因情找些份置适什蛋师气你姐棒试总定啊足级整带虾如态且尝主话强当更板知己无酸让入啦式笑赞片酱差像提队走嫩才刚午接重串回晚微周值费性桌拍跟块调糕'
    num_rule = re.compile("\d+")
    
    def get_url(urls, tag, only_First=True):
        urls = [parse.urljoin(base_url, url)
                for url in urls if tag is None or url.find(tag) != -1]
        if urls and only_First:
            return urls[0]
        return urls
    
    
    def parseCssFontUrl(css_url, tag=None, only_First=True):
        res = session.get(css_url)
        rule = {}
        font_face = {}
        for name, value in re.findall("([^{}]+){([^{}]+)}", res.text):
            name = name.strip()
            for row in value.split(";"):
                if row.find(":") == -1:
                    continue
                k, v = row.split(":")
                k, v = k.strip(), v.strip(' "\'')
                if name == "@font-face":
                    if k == "font-family":
                        font_name = v
                    elif k == "src":
                        font_face.setdefault(font_name, []).extend(
                            re.findall("url\(\"([^()]+)\"\)", v))
                else:
                    rule[name[1:]] = v
        font_urls = {}
        for class_name, tag_name in rule.items():
            font_urls[class_name] = get_url(font_face[tag_name], tag)
        return font_urls
    
    
    def getFontMapFromURL(font_url):
        "缓存字体URL对应字体映射关系"
        if font_url not in url2FontMapCache:
            font_bytes = BytesIO(session.get(font_url).content)
            font_data = TTFont(font_bytes)
            uni_list = font_data.getGlyphOrder()[2:]
            url2FontMapCache[font_url] = dict(
                zip(map(lambda x: x[3:], uni_list), words))
        return url2FontMapCache[font_url]
    
    
    def getFontMapFromClassName(class_name, css_url):
        "缓存指定css文件对应字体URL"
        if css_url not in css2FontCache:
            css2FontCache[css_url] = parseCssFontUrl(css_url, ".woff")
        font_url = css2FontCache[css_url].get(class_name)
        return getFontMapFromURL(font_url)
    
    
    def parse_data(soup):
        result = []
        for li_tag in soup.select("div#shop-all-list div.txt"):
            title = li_tag.select_one("div.tit>a>h4").text
            url = li_tag.select_one("div.tit>a")["href"]
            star_class = li_tag.select_one(
                "div.comment>div.nebula_star>div.star_icon>span")["class"]
            star = int(num_rule.findall(" ".join(star_class))[0])//10
    
            comment_tag = li_tag.select_one("div.comment>a.review-num>b")
            comment_num = comment_tag.text if comment_tag else None
    
            mean_price_tag = li_tag.select_one("div.comment>a.mean-price>b")
            mean_price = mean_price_tag.text if mean_price_tag else None
    
            fun_type = li_tag.select_one(
                "div.tag-addr>a:nth-of-type(1)>span.tag").text
            area = li_tag.select_one("div.tag-addr>a:nth-of-type(2)>span.tag").text
            result.append((title, star, comment_num,
                          mean_price, fun_type, area, url))
        return result
    
    
    def getUrlFromNode(nodes, tag):
        for node in nodes:
            url = node['href']
            if url.find(tag) != -1:
                return parse.urljoin(base_url, url)
    
    
    def get_css_url(soup):
        css_url = getUrlFromNode(soup.select(
            "head > link[rel=stylesheet]"), "svgtextcss")
        return css_url
    
    
    def fix_text(soup):
        css_url = get_css_url(soup)
        for svgmtsi in soup.find_all('svgmtsi'):
            class_name = svgmtsi['class'][0]
            font_map = getFontMapFromClassName(class_name, css_url)
            chars = []
            for c in svgmtsi.text:
                char = c.encode("unicode_escape").decode()[2:]
                chars.append(font_map[char])
            svgmtsi.replaceWith("".join(chars))
    
    
    headers = {
        "Connection": "keep-alive",
        "Cache-Control": "max-age=0",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
        "Accept-Language": "zh-CN,zh;q=0.9"
    }
    session = requests.Session()
    session.headers = headers
    base_url = "http://www.dianping.com/shenzhen/ch30"
    res = session.get(base_url)
    
    soup = BeautifulSoup(res.text, 'html5lib')
    type_list = []
    for a_tag in soup.select("div#classfy > a"):
        type_list.append((a_tag.span.text, a_tag['href']+'r91172'))
    
    result = []
    for type_name, url in type_list:
        print(type_name, url)
        res = session.get(url)
        soup = BeautifulSoup(res.text, 'html5lib')
        fix_text(soup)
        result.extend(parse_data(soup))
        time.sleep(random.randint(2, 4))
    
    df = pd.DataFrame(result, columns=["标题", "星级", "评论数", "均价", "娱乐类型", "区域", "链接"])
    df.评论数 = df.评论数.apply(lambda x: int(x) if x else pd.NA)
    df.均价 = df.均价.str[1:].apply(lambda x: int(x) if x else pd.NA)
    df.drop_duplicates(inplace=True)
    df.to_excel("华南城娱乐.xlsx", index=False)
    

    爬取结果(有一定的二次编辑):

    image-20210803104317180

    总结

    整体来说,该团购网站的反爬机制还是挺猛的,费了九牛二虎之力也就只能每个栏目爬一页数据,还没有地址,推荐各位不要去爬了。

    不过本文的目的就是演示把最难的字体反爬给解决掉,希望本文已经达到这个目标,如果后面还有更难的字体反爬网站出现,再继续更深的剖析,见招拆招。

    希望各位小伙伴,在研究完本文后,能够应对任何字体反爬问题。

    展开全文
  • 编写程序,获取网络中的IP数据包内容 程序的输出内容应包括IP包的版本、头长度、服务类型、数据包长度等IP数据包格式中的相应字段 为了获取网络中的IP数据包,必须网卡进行编程,在这里使用套接(socket)进行编程
  • 随着电子信息技术的发展与成熟,加上国家的大力推广,电子发票已经开始慢慢取代纸质发票。相比传统的纸质发票,电子发票除了绿色...下文将慢慢分析电子发票文件的内部结构,并尝试电子发票中数字证书及签名进行解析

    前言

    随着电子信息技术的发展与成熟,加上国家的大力推广,电子发票已经开始慢慢取代纸质发票。相比传统的纸质发票,电子发票除了绿色环保,节约成本之外,更重要的是电子发票采取电子签章实现发票签名、电子盖章,具有唯一性、不可抵赖性、防篡改等优点,而且更加容易税务管理。那么,我们平常拿到一张电子发票,应该如何验证它的真伪呢?如何保证它是合法且没有被别人篡改呢?这就需要对电子发票的原理有所了解了。下文将慢慢分析电子发票文件的内部结构,并尝试对电子发票中数字证书及签名进行解析。

    电子发票的结构

    我们收到的电子发票文件后缀名都是.ofd,它的载体就是OFD版式文件,OFD文件我们可以简单认为它就是我们国家自主研发与定义的文件格式,类似于PDF,我们通过winzip或者7zip等解压工具打开,就可以看到它的内部结构。
    在这里插入图片描述
    从图中可以看到,电子发票主要分为两个体系,一个是内容体系,主要描述发票各个要素承载的信息以及样式;另外一个是签名体系,用于校验发票的正确性。
    这里和验证相关的的文件主要是Signature.xml(签名/签章描述文件)、Seal.esl(电子印章文件)和SignedValue.dat(签名值文件)。

    电子发票的验证步骤

    我们拿到这三个文件需要怎么做呢?最权威的国标文件给出了验证的具体步骤,如图所示:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    好家伙,光验证步骤就有a~h步,其中第d步是验证电子印章的有效性,而这里又是涉及到多个步骤:
    在这里插入图片描述
    怎么理解这一堆复杂的步骤呢?电子签章和电子印章的区别是什么?
    想象一下我们收到一张发票,应该怎样去验证它的真伪呢?

    1. 需要保证电子发票是开票者开具的,文件并没有被篡改过。
    2. 需要验证发票上的印章是真实有效的。

    上面提到的第1点就是电子签章的验证,第2点就是电子印章的验证。两者缺一不可。
    而无论是电子签章还是电子印章,核心又是对数字签名进行验证,因此,提取文件中的证书文件是关键。下面以电子印章(Seal.esl)为例,介绍一下如何解析里面的内容。

    电子印章的文件组成

    电子印章文件是一个二进制文件,通过文本工具是无法得知里面的内容。实际上,电子印章是以ASN.1格式来存储的。
    ASN.1是什么东西呢?ASN.1是国际电信标准(ITU-T)定义的标准,用来结构化描述证书,ASN.1类似于JSON或者XML这样的数据结构。一般的证书都是通过ASN.1来定义的。
    https://lapo.it/asn1js/ 是一个神奇的在线ASN.1解析网站,把Seal.esl丢上去,可以看到电子印章的内容基本都被解析出来了:
    在这里插入图片描述
    可以看到电子印章文件的大致结构,里面包含了电子印章的一些基础信息,例如印章信息、制章者、签名算法、签名值等。唯一有点遗憾的是无法通过结果得知各个元素的名称与作用,这是由于网站无法知道我们的数据结构是怎样定义的。我们还是要更加深入研究国标中对电子印章的数据结构定义。
    在这里插入图片描述
    上图是电子印章中印章属性的结构定义,我们可以理解电子印章是在原有数字证书的基础上封装了一些信息,核心还是里面的数字证书Certificate(如下图红框所示),而这个证书与我们平时浏览器上信任的证书是同一个东西。
    在这里插入图片描述
    通过Java编写程序,我们可以更加定制化地专门解析电子印章的内容,并且能提取出证书的内容,为后面的数字签名验证打下基础。

    电子印章的解析

    目前最主流的java解析ASN.1内容工具是:bcprov,使用方法也相当简单。

    <dependency>
       <groupId>org.bouncycastle</groupId>
       <artifactId>bcprov-jdk15on</artifactId>
       <version>1.68</version>
    </dependency>
    
    @Slf4j
    public class TestAsn1Parser {
    
        @Test
        public void testData() throws Exception{
            // 1. 读取电子印章(Seal.esl)
            ASN1InputStream bin = new ASN1InputStream(new ByteArrayInputStream(IOUtils.toByteArray(AsnParser.class.getResourceAsStream("Seal.esl"))));
            ASN1Primitive obj = bin.readObject();
            DLSequence app = (DLSequence) obj;
    
            // 2. 根据国标定义,找到证书二进制内容的位置
            DEROctetString cert = (DEROctetString) app.getObjectAt(1);
            ASN1InputStream bin2 = new ASN1InputStream(cert.getOctets());
            DLSequence seq = (DLSequence) bin2.readObject();
    
            // 3. 解析证书内容
            Certificate certificate = Certificate.getInstance(seq);
            TBSCertificate tbsCertificate = certificate.getTBSCertificate();
    
            
            // 示例:获取证书内数字签名的主要元素
            log.info("签名算法:{}", certificate.getSignatureAlgorithm().getAlgorithm());
            log.info("签名值:{}", certificate.getSignature());
            TBSCertificate tbsCertificate = certificate.getTBSCertificate();
            log.info("公钥:{}", tbsCertificate.getSubjectPublicKeyInfo().getPublicKeyData());
        }
    }
    

    在这里插入图片描述
    例子中只对电子印章中的证书部分进行解析,实际应用中我们可以创建对应的实现来完全映射国标中的数据结构。
    通过上面代码得到电子印章的数字签名信息,包括:

    1. 签名算法是1.2.156.10197.1.501(国标中对应的是国密算法:基于SM3的SM2签名)
    2. 签名值
    3. 公钥

    有了上面的信息,接下来要做的事情就简单了,只需对该数字签名进行验证即可。

    后记

    上面介绍的仅仅是电子发票验证的一小部分,由于篇幅有限,除了要对数字签名验证以外,还需要验证证书的有效性,这里又涉及到证书链与CRL相关的验证。而电子印章验证通过后还需进行电子签章的验证。有时间的话将进一步补充介绍后面的步骤。
    电子发票涉及相关的内容较多,包括了OFD、ASN.1和信息安全、密码学等相关知识,特别是相关国家标准较多,需要仔细参考研究其中的描述说明。上面的示例仅作参考,如有错漏,还请见谅。
    By Ryan.ou

    参考资料

    [1] GB/T 33190-2016 电子文件存储与交换格式 版式文档
    [2] GB/T 38540-2020 信息安全技术 安全电子签章密码 技术规范
    [3] GB/T 20518 2018 信息安全技术 公钥基础设施 数字证书格式
    [4] OFD开源读写组件
    [5] ASN.1在线解析网站
    [6] https://blog.csdn.net/weixin_42497593/article/details/112151171

    展开全文
  • PDF.js 解析PDF文件demo

    千次下载 热门讨论 2013-09-05 10:11:24
    pdf.js 解析PDF文件DEMO 下载这个DEMO 轻松搞定在线解析PDF文件 支持翻页浏览 pdf.js 是一个技术原型主要用于在 HTML5 平台上展示 PDF 文档,无需任何本地技术支持 注意:只兼容支持HTML5的浏览器
  • 这道题没有一个标准的答案,它涉及很多的知识点,面试官会通过这道题了解你哪一方面的知识比较擅长,然后继续追问看看你的掌握程度。当然本篇博客的分析也只是我的一些个人理解,前端的角度出发,具体地分析...

    一、前言

    • 从输入一个 URL,然后按下回车到显示页面,中间发生了什么?这是一道经典的面试题,不光前端面试会问到,后端面试也会被问到。这道题没有一个标准的答案,它涉及很多的知识点,面试官会通过这道题了解你对哪一方面的知识比较擅长,然后继续追问看看你的掌握程度。当然本篇博客的分析也只是我的一些个人理解,从前端的角度出发,具体地分析从底层到高层、从硬件到软件的原理,以及在同一个层次讨论浏览器、操作系统、服务器是如何交互的。
    • 其实,这个问题可以拆解成两个过程:
      • 用户输入 URL,客户端(浏览器)拿到服务端的数据;
      • 浏览器获取到数据,呈现页面(也就是浏览器工作过程)。

    二、输入网址到浏览器获得资源的过程

    ① 按下 URL 地址首字母

    • 以输入“google.com”的地址为例,当按下“g”键,浏览器接收到消息之后,会触发自动完成机制:
      • 浏览器根据自己的算法,以及是否处于隐私浏览模式,会在浏览器的地址框下方给出输入建议,大部分算法会优先考虑根据你的搜索历史和书签等内容给出建议。
      • 你打算输入 “google.com”,因此给出的建议并不匹配,但是输入过程中仍然有大量的代码在后台运行,每一次按键都会使得给出的建议更加准确;
      • 甚至有可能在你输入之前,浏览器就将 “google.com” 建议给你。

    ② 按下回车键

    • 为了从零开始,我们选择键盘上的回车键被按到最低处作为起点,在这个时刻,一个专用于回车键的电流回路被直接地或者通过电容器间接地闭合,使得少量的电流进入键盘的逻辑电路系统,这个系统会扫描每个键的状态,对于按键开关的电位弹跳变化进行噪音消除(debounce),并将其转化为键盘码值。
    • 在这里,回车的码值是13,键盘控制器在得到码值之后,将其编码,用于之后的传输。现在这个传输过程几乎都是通过通用串行总线(USB)或者蓝牙(Bluetooth)来进行的,以前是通过 PS/2 或者 ADB 连接进行。
    • USB 键盘:
      • 键盘的 USB 元件通过计算机上的 USB 接口与 USB 控制器相连接,USB 接口中的第一号针为它提供了 5V 的电压;
      • 键码值存储在键盘内部电路的寄存器 endpoint 内;
      • USB 控制器大概每隔 10ms 便查询一次 endpoint 以得到存储的键码值数据,这个最短时间间隔由键盘提供
        键值码值通过 USB 串行接口引擎被转换成一个或者多个遵循低层 USB 协议的 USB 数据包;
      • 这些数据包通过 D+ 针或者 D- 针(中间的两个针),以最高 1.5Mb/s 的速度从键盘传输至计算机,速度限制是因为人机交互设备总是被声明成"低速设备"(USB 2.0 compliance);
      • 这个串行信号在计算机的 USB 控制器处被解码,然后被人机交互设备通用键盘驱动进行进一步解释,之后按键的码值被传输到操作系统的硬件抽象层。
    • 虚拟键盘(触屏设备):
      • 在现代电容屏上,当用户把手指放在屏幕上时,一小部分电流从传导层的静电域经过手指传导,形成了一个回路,使得屏幕上触控的那一点电压下降,屏幕控制器产生一个中断,报告这次“点击”的坐标;
      • 然后移动操作系统通知当前活跃的应用,有一个点击事件发生在它的某个 GUI 部件上了,现在这个部件是虚拟键盘的按钮;
      • 虚拟键盘引发一个软中断,返回给 OS 一个“按键按下”消息;
      • 这个消息又返回来向当前活跃的应用通知一个“按键按下”事件。
    • 键盘在它的中断请求线(IRQ)上发送信号,信号会被中断控制器映射到一个中断向量,实际上就是一个整型数。CPU 使用中断描述符表(IDT)把中断向量映射到对应函数,这些函数被称为中断处理器,它们由操作系统内核提供。当一个中断到达时,CPU 根据 IDT 和中断向量索引到对应的中断处理器,然后操作系统内核出场。

    ③ 一个 WM_KEYDOWN 消息被发往应用程序(Windows)

    • HID 把键盘按下的事件传送给 KBDHID.sys 驱动,把 HID 的信号转换成一个扫描码(Scancode),这里回车的扫描码是 VK_RETURN(0x0d)。 KBDHID.sys 驱动和 KBDCLASS.sys (键盘类驱动 keyboard class driver)进行交互,这个驱动负责安全地处理所有键盘和小键盘的输入事件。之后它又去调用 Win32K.sys,在这之前有可能把消息传递给安装的第三方键盘过滤器,这些都是发生在内核模式。
    • Win32K.sys 通过 GetForegroundWindow() API 函数找到当前哪个窗口是活跃的,这个 API 函数提供当前浏览器的地址栏的句柄。Windows 系统的"message pump"机制调用 SendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam) 函数, lParam 是一个用来指示这个按键的更多信息的掩码,这些信息包括按键重复次数(这里是0),实际扫描码(可能依赖于 OEM 厂商,不过通常不会是 VK_RETURN ),功能键(alt, shift, ctrl)是否被按下(在这里没有),以及一些其他状态。
    • Windows 的 SendMessage API 直接将消息添加到特定窗口句柄 hWnd 的消息队列中,之后赋给 hWnd 的主要消息处理函数 WindowProc 将会被调用,用于处理队列中的消息。
    • 当前活跃的句柄 hWnd 实际上是一个 edit control 控件,这种情况下,WindowProc 有一个用于处理 WM_KEYDOWN 消息的处理器,这段代码会查看 SendMessage 传入的第三个参数 wParam,因为这个参数是 VK_RETURN,于是它知道用户按下了回车键。

    ④ 一个 KeyDown NSEvent 被发往应用程序(Mac OS X)

    • 中断信号引发了 I/O Kit Kext 键盘驱动的中断处理事件,驱动把信号翻译成键码值,然后传给 OS X 的 WindowServer 进程。
    • 然后, WindowServer 将这个事件通过 Mach 端口分发给合适的(活跃的,或者正在监听的)应用程序,这个信号会被放到应用程序的消息队列里。
    • 队列中的消息可以被拥有足够高权限的线程使用 mach_ipc_dispatch 函数读取到,这个过程通常是由 NSApplication 主事件循环产生并且处理的,通过 NSEventType 为 KeyDown 的 NSEvent 。

    ⑤ Xorg 服务器监听键码值(GNU/Linux)

    • 当使用图形化的 X Server 时,X Server 会按照特定的规则把键码值再一次映射,映射成扫描码。
    • 当这个映射过程完成之后, X Server 把这个按键字符发送给窗口管理器(DWM、metacity、i3 等),窗口管理器再把字符发送给当前窗口。
    • 当前窗口使用有关图形 API 把文字打印在输入框内。

    ⑥ 解析 URL

    • 浏览器通过 URL 能够知道下面的信息:
    Protocol "http"
    使用HTTP协议
    Resource "/"
    请求的资源是主页(index)
    
    • 那么输入的是 URL 还是搜索的关键字?当协议或主机名不合法时,浏览器会将地址栏中输入的文字传给默认的搜索引擎。大部分情况下,在把文字传递给搜索引擎的时候,URL 会带有特定的一串字符,用来告诉搜索引擎这次搜索来自这个特定浏览器。
    • 转换非 ASCII 的 Unicode 字符:
      • 浏览器检查输入是否含有不是 a-z, A-Z,0-9, - 或者 . 的字符;
      • 这里主机名是 google.com ,所以没有非 ASCII 的字符;如果有的话,浏览器会对主机名部分使用 Punycode 编码。

    ⑦ 检查 HSTS 列表

    • 浏览器检查自带的“预加载 HSTS(HTTP 严格传输安全)”列表,这个列表里包含那些请求浏览器只使用 HTTPS 进行连接的网站。
    • 如果网站在这个列表里,浏览器会使用 HTTPS 而不是 HTTP 协议,否则,最初的请求会使用 HTTP 协议发送。
    • 注意,一个网站哪怕不在 HSTS 列表里,也可以要求浏览器对自己使用 HSTS 政策进行访问。浏览器向网站发出第一个 HTTP 请求之后,网站会返回浏览器一个响应,请求浏览器只使用 HTTPS 发送请求。然而,就是这第一个 HTTP 请求,却可能会使用户受到 添downgrade attack 的威胁,这也是为什么现代浏览器都预置了 HSTS 列表。

    ⑧ DNS 查询

    • 浏览器检查域名是否在缓存当中(要查看 Chrome 当中的缓存, 打开 chrome://net-internals/#dns)。
    • 如果缓存中没有,就去调用 gethostbyname 库函数(操作系统不同函数也不同)进行查询。
    • gethostbyname 函数在试图进行DNS解析之前首先检查域名是否在本地 Hosts 里,Hosts 的位置 不同的操作系统有所不同
    • 如果 gethostbyname 没有这个域名的缓存记录,也没有在 hosts 里找到,它将会向 DNS 服务器发送一条 DNS 查询请求,DNS 服务器是由网络通信栈提供的,通常是本地路由器或者 ISP 的缓存 DNS 服务器。
    • 查询本地 DNS 服务器:
      • 如果 DNS 服务器和我们的主机在同一个子网内,系统会按照下面的 ARP 过程对 DNS 服务器进行 ARP查询;
      • 如果 DNS 服务器和我们的主机在不同的子网,系统会按照下面的 ARP 过程对默认网关进行查询。

    ⑨ ARP 过程

    • 要想发送 ARP(地址解析协议)广播,需要有一个目标 IP 地址,同时还需要知道用于发送 ARP 广播的接口的 MAC 地址:
      • 首先查询 ARP 缓存,如果缓存命中,返回结果:目标 IP = MAC;
      • 如果缓存没有命中:
        • 查看路由表,看看目标 IP 地址是不是在本地路由表中的某个子网内,如果是,使用跟那个子网相连的接口,否则使用与默认网关相连的接口;
        • 查询选择的网络接口的 MAC 地址;
        • 发送一个二层( OSI 模型中的数据链路层)ARP 请求:
    Sender MAC: interface:mac:address:here
    Sender IP: interface.ip.goes.here
    Target MAC: FF:FF:FF:FF:FF:FF (Broadcast)
    Target IP: target.ip.goes.here
    
    • 根据连接主机和路由器的硬件类型不同,可以分为以下几种情况:
      • 直连:如果和路由器是直接连接的,路由器会返回一个 ARP Reply:
    Sender MAC: target:mac:address:here
    Sender IP: target.ip.goes.here
    Target MAC: interface:mac:address:here
    Target IP: interface.ip.goes.here
    
      • 集线器:如果连接到一个集线器,集线器会把 ARP 请求向所有其它端口广播,如果路由器也“连接”在其中,它会返回一个 ARP Reply 。
      • 交换机:
        • 如果连接到了一个交换机,交换机会检查本地 CAM/MAC 表,查看哪个端口有要找的那个 MAC 地址,如果没有找到,交换机会向所有其它端口广播这个 ARP 请求。
        • 如果交换机的 MAC/CAM 表中有对应的条目,交换机会向想要查询的 MAC 地址的那个端口发送 ARP 请求;
        • 如果路由器也“连接”在其中,它会返回一个 ARP Reply。
    • 有 DNS 服务器或者默认网关的 IP 地址,就可以继续 DNS 请求:
      • 使用 53 端口向 DNS 服务器发送 UDP 请求包,如果响应包太大,会使用 TCP 协议;
      • 如果本地/ISP DNS 服务器没有找到结果,它会发送一个递归查询请求,一层一层向高层 DNS 服务器做查询,直到查询到起始授权机构,如果找到会把结果返回。

    ⑩ 使用套接字

    • 当浏览器得到目标服务器的 IP 地址,以及 URL 中给出来端口号(http 协议默认端口号是 80, https 默认端口号是 443),它会调用系统库函数 socket ,请求一个 TCP流套接字,对应的参数是 AF_INET/AF_INET6 和 SOCK_STREAM 。
    • 该请求首先被交给传输层,在传输层请求被封装成 TCP segment,目标端口会被加入头部,源端口会在系统内核的动态端口范围内选取(Linux 下是 ip_local_port_range)。
    • TCP segment 被送往网络层,网络层会在其中再加入一个 IP 头部,里面包含目标服务器的IP地址以及本机的 IP 地址,把它封装成一个 IP packet。
    • TCP packet 接下来会进入链路层,链路层会在封包中加入 frame 头部,里面包含本地内置网卡的 MAC 地址以及网关(本地路由器)的 MAC 地址。像前面说的一样,如果内核不知道网关的 MAC 地址,它必须进行 ARP 广播来查询其地址。
    • 到现在,TCP 封包已经准备好,可以使用下面的方式进行传输:
    • 对于大部分家庭网络和小型企业网络来说,封包会从本地计算机出发,经过本地网络,再通过调制解调器把数字信号转换成模拟信号,使其适于在电话线路,有线电视光缆和无线电话线路上传输。在传输线路的另一端,是另外一个调制解调器,它把模拟信号转换回数字信号,交由下一个网络节点处理。
    • 最终封包会到达管理本地子网的路由器。在那里出发,它会继续经过自治区域(autonomous system, 缩写 AS)的边界路由器,其他自治区域,最终到达目标服务器。一路上经过的这些路由器会从IP数据报头部里提取出目标地址,并将封包正确地路由到下一个目的地。IP数据报头部 time to live (TTL) 域的值每经过一个路由器就减1,如果封包的TTL变为0,或者路由器由于网络拥堵等原因封包队列满了,那么这个包会被路由器丢弃。
    • 上面的发送和接受过程在 TCP 连接期间会发生很多次:
      • 客户端选择一个初始序列号(ISN),将设置 SYN 位的封包发送给服务器端,表明自己要建立连接并设置了初始序列号;
      • 服务器端接收到 SYN 包,如果它可以建立连接:
        • 服务器端选择它自己的初始序列号;
        • 服务器端设置 SYN 位,表明自己选择一个初始序列号;
        • 服务器端把 (客户端ISN + 1) 复制到 ACK 域,并且设置 ACK 位,表明自己接收到客户端的第一个封包。
      • 客户端通过发送下面一个封包来确认这次连接:
        • 自己的序列号 +1;
        • 接收端 ACK+1
        • 设置 ACK 位;
      • 数据通过下面的方式传输:
        • 当一方发送了 N 个 Bytes 的数据之后,将自己的 SEQ 序列号也增加 N;
        • 另一方确认接收到这个数据包(或者一系列数据包)之后,它发送一个 ACK 包,ACK 的值设置为接收到的数据包的最后一个序列号;
      • 关闭连接时:
        • 要关闭连接的一方发送一个 FIN 包;
        • 另一方确认这个 FIN 包,并且发送自己的 FIN 包;
        • 要关闭的一方使用 ACK 包来确认接收到了 FIN。

    ⑪ TLS 握手

    • 客户端发送一个 ClientHello 消息到服务器端,消息中同时包含它的 Transport Layer Security (TLS) 版本,可用的加密算法和压缩算法。
    • 服务器端向客户端返回一个 ServerHello 消息,消息中包含服务器端的TLS版本,服务器所选择的加密和压缩算法,以及数字证书认证机构(Certificate Authority,缩写 CA)签发的服务器公开证书,证书中包含了公钥。客户端会使用这个公钥加密接下来的握手过程,直到协商生成一个新的对称密钥。
    • 客户端根据自己的信任 CA 列表,验证服务器端的证书是否可信,如果认为可信,客户端会生成一串伪随机数,使用服务器的公钥加密它,这串随机数会被用于生成新的对称密钥。
    • 服务器端使用自己的私钥解密上面提到的随机数,然后使用这串随机数生成自己的对称主密钥。
    • 客户端发送一个 Finished 消息给服务器端,使用对称密钥加密这次通讯的一个散列值。
    • 服务器端生成自己的 hash 值,然后解密客户端发送来的信息,检查这两个值是否对应,如果对应,就向客户端发送一个 Finished 消息,也使用协商好的对称密钥加密。
    • 从现在开始,接下来整个 TLS 会话都使用对称秘钥进行加密,传输应用层(HTTP)内容。

    ⑫ HTTP 协议

    • 如果浏览器是 Google 出品的,它不会使用 HTTP 协议来获取页面信息,而是会与服务器端发送请求,商讨使用 SPDY 协议。
    • 如果浏览器使用 HTTP 协议而不支持 SPDY 协议,它会向服务器发送这样的一个请求:
    GET / HTTP/1.1
    Host: google.com
    Connection: close
    [其他头部]
    
    • “其它头部”包含一系列的由冒号分割开的键值对,它们的格式符合 HTTP 协议标准,它们之间由一个换行符分割开来(假设浏览器没有违反 HTTP 协议标准的 bug,同时假设浏览器使用 HTTP/1.1 协议,不然的话头部可能不包含 Host 字段,同时 GET 请求中的版本号会变成 HTTP/1.0 或者 HTTP/0.9)。
    • HTTP/1.1 定义“关闭连接”的选项 “close”,发送者使用这个选项指示这次连接在响应结束之后会断开。例如:
    Connection:close
    
    • 不支持持久连接的 HTTP/1.1 应用必须在每条消息中都包含 “close” 选项。
    • 在发送完这些请求和头部之后,浏览器发送一个换行符,表示要发送的内容已经结束。
    • 服务器端返回一个响应码,指示这次请求的状态,响应的形式是这样的:
    200 OK
    [响应头部]
    
    • 然后是一个换行,接下来有效载荷(payload),也就是 www.google.com 的HTML内容,服务器下面可能会关闭连接,如果客户端请求保持连接的话,服务器端会保持连接打开,以供之后的请求重用。
    • 如果浏览器发送的HTTP头部包含了足够多的信息(例如包含了 Etag 头部),以至于服务器可以判断出,浏览器缓存的文件版本自从上次获取之后没有再更改过,服务器可能会返回这样的响应:
    304 Not Modified
    [响应头部]
    
    • 这个响应没有有效载荷,浏览器会从自己的缓存中取出想要的内容。在解析完 HTML 之后,浏览器和客户端会重复上面的过程,直到HTML页面引入的所有资源(图片,CSS,favicon.ico等等)全部都获取完毕,区别只是头部的 GET / HTTP/1.1 会变成 GET /$(相对www.google.com的URL) HTTP/1.1 。
    • 如果 HTML 引入 www.google.com 域名之外的资源,浏览器会回到上面解析域名那一步,按照下面的步骤往下一步一步执行,请求中的 Host 头部会变成另外的域名。

    ⑬ HTTP 服务器请求处理

    • HTTPD(HTTP Daemon)在服务器端处理请求/响应,最常见的 HTTPD 有 Linux 上常用的 Apache 和 nginx,以及 Windows 上的 IIS。
    • HTTPD 接收请求,服务器把请求拆分为以下几个参数:
      • HTTP 请求方法(GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, 或者 TRACE),直接在地址栏中输入 URL 这种情况下,使用的是 GET 方法;
      • 域名:google.com;
      • 请求路径/页面:/ (没有请求 google.com 下的指定的页面,因此 / 是默认的路径)。
    • 服务器验证其上已经配置 google.com 的虚拟主机。
    • 服务器验证 google.com 接受 GET 方法。
    • 服务器验证该用户可以使用 GET 方法(根据 IP 地址,身份信息等)。
    • 如果服务器安装了 URL 重写模块(例如 Apache 的 mod_rewrite 和 IIS 的 URL Rewrite),服务器会尝试匹配重写规则,如果匹配上的话,服务器会按照规则重写这个请求。
    • 服务器根据请求信息获取相应的响应内容,这种情况下由于访问路径是 “/”,会访问首页文件(可以重写这个规则,但是这个是最常用的)。
    • 服务器会使用指定的处理程序分析处理这个文件,假如 Google 使用 PHP,服务器会使用 PHP 解析 index 文件,并捕获输出,把 PHP 的输出结果返回给请求者。

    ⑭ 浏览器

    • 当服务器提供资源之后(HTML,CSS,JS,图片等),浏览器会执行下面的操作:
      • 解析:HTML,CSS,JS;
      • 渲染:构建 DOM 树 -> 渲染 -> 布局 -> 绘制。
    • 浏览器的功能是从服务器上取回想要的资源,然后展示在浏览器窗口当中,资源通常是 HTML 文件,也可能是 PDF、图片或者其他类型的内容,资源的位置通过用户提供的 URI(Uniform Resource Identifier) 来确定。
    • 浏览器解释和展示 HTML 文件的方法,这些标准由 Web 标准组织 W3C(World Wide Web Consortium) 维护。不同浏览器的用户界面大都十分接近,有很多共同的 UI 元素:
      • 一个地址栏
      • 后退和前进按钮
      • 书签选项
      • 刷新和停止按钮
      • 主页按钮
    • 组成浏览器的组件有:
      • 用户界面包含地址栏,前进后退按钮,书签菜单等,除了请求页面之外所有你看到的内容都是用户界面的一部分;
      • 浏览器引擎负责让 UI 和渲染引擎协调工作;
      • 渲染引擎渲染引擎负责展示请求内容,如果请求的内容是 HTML,渲染引擎会解析 HTML 和 CSS,然后将内容展示在屏幕上;
      • 网络组件负责网络调用,例如 HTTP 请求等,使用一个平台无关接口,下层是针对不同平台的具体实现;
      • UI 后端用于绘制基本 UI 组件,如下拉列表框和窗口;UI 后端暴露一个统一的平台无关的接口,下层使用操作系统的 UI 方法实现;
      • Javascript 引擎用于解析和执行 Javascript 代码;
      • 数据存储组件是一个持久层,浏览器可能需要在本地存储各种各样的数据,例如 Cookie 等;浏览器也需要支持诸如 localStorage,IndexedDB,WebSQL 和 FileSystem 之类的存储机制。

    ⑮ HTML 解析

    • 浏览器渲染引擎从网络层取得请求的文档,一般情况下文档会分成 8 KB 大小的分块传输。
    • HTML 解析器的主要工作是对 HTML 文档进行解析,生成解析树,解析树是以 DOM 元素以及属性为节点的树
    • DOM 是文档对象模型(Document Object Model)的缩写,它是 HTML 文档的对象表示,同时也是 HTML 元素面向外部(如 Javascript)的接口。
    • 树的根部是"Document"对象,整个 DOM 和 HTML 文档几乎是一对一的关系。
    • HTML 不能使用常见的自顶向下或自底向上方法来进行分析,主要原因有以下几点:
      • 语言本身的“宽容”特性;
      • HTML 本身可能是残缺的,对于常见的残缺,浏览器需要有传统的容错机制来支持它们;
      • 解析过程需要反复,对于其它语言来说,源码不会在解析过程中发生变化,但是对于 HTML 来说,动态代码,例如脚本元素中包含的 document.write() 方法会在源码中添加内容,也就是说,解析过程实际上会改变输入的内容。
    • 由于不能使用常用的解析技术,浏览器创造了专门用于解析 HTML 的解析器。解析算法主要包含了两个阶段:标记化(tokenization)和树的构建。
    • 解析结束之后:
      • 浏览器开始加载网页的外部资源(CSS,图像,Javascript 文件等);
      • 此时浏览器把文档标记为可交互的(interactive),浏览器开始解析处于“推迟(deferred)”模式的脚本,也就是那些需要在文档解析完毕之后再执行的脚本,之后文档的状态会变为“完成(complete)”,浏览器会触发“加载(load)”事件。
      • 注意解析 HTML 网页时永远不会出现“无效语法(Invalid Syntax)”错误,浏览器会修复所有错误内容,然后继续解析。

    ⑯ CSS 解析

    • 根据 CSS词法和句法 分析 CSS 文件和 <style> 标签包含的内容以及 style 属性的值;
    • 每个 CSS 文件都被解析成一个样式表对象(StyleSheet object),这个对象里包含带有选择器的 CSS 规则和对应 CSS 语法的对象。
    • CSS 解析器可能是自顶向下的,也可能是使用解析器生成器生成的自底向上的解析器。

    ⑰ 页面渲染

    • 通过遍历 DOM 节点树创建一个“Frame 树”或“渲染树”,并计算每个节点的各个 CSS 样式值;
    • 通过累加子节点的宽度,该节点的水平内边距(padding)、边框(border)和外边距(margin),自底向上的计算"Frame 树"中每个节点的首选(preferred)宽度;
    • 通过自顶向下的给每个节点的子节点分配可行宽度,计算每个节点的实际宽度;
    • 通过应用文字折行、累加子节点的高度和此节点的内边距(padding)、边框(border)和外边距(margin),自底向上的计算每个节点的高度;
    • 使用上面的计算结果构建每个节点的坐标;
    • 当存在元素使用 floated,位置有 absolutely 或 relatively 属性的时候,会有更多复杂的计算,详见css2current-work
    • 创建 layer 层来表示页面中的哪些部分可以成组的被绘制,而不用被重新栅格化处理,每个帧对象都被分配给一个层;
    • 页面上的每个层都被分配了纹理(?);
    • 每个层的帧对象都会被遍历,计算机执行绘图命令绘制各个层,此过程可能由 CPU 执行栅格化处理,或者直接通过 D2D/SkiaGL 在 GPU 上绘制;
    • 上面所有步骤都可能利用到最近一次页面渲染时计算出来的各个值,这样可以减少不少计算量;
    • 计算出各个层的最终位置,一组命令由 Direct3D/OpenGL 发出,GPU 命令缓冲区清空,命令传至 GPU 并异步渲染,帧被送到Window Server。

    ⑱ GPU 渲染

    • 在渲染过程中,图形处理层可能使用通用用途的 CPU,也可能使用图形处理器 GPU;
    • 当使用 GPU 用于图形渲染时,图形驱动软件会把任务分成多个部分,这样可以充分利用 GPU 强大的并行计算能力,用于在渲染过程中进行大量的浮点计算。

    ⑲ 整体流程

    在这里插入图片描述

    • 总结如下:
      • 用户在某个标签页输入 URL 并回车后,浏览器主进程会新开一个网络线程,发起 HTTP 请求;
      • 浏览器会进行 DNS 查询,将域名解析为 IP 地址;
      • 浏览器获得 IP 地址后,向服务器请求建立 TCP 连接;
      • 浏览器向服务器发起 HTTP 请求;
      • 服务器处理请求,返回 HTTP 响应;
      • 浏览器的渲染进程解析并绘制页面;
      • 如果遇到 JS/CSS/图片 等静态资源的引用链接,重复上述过程,向服务器请求这些资源。

    三、底层原理分析

    ① 浏览器

    • 浏览器是多进程的,只有一个主进程,又称 Browser 进程,负责创建/管理其他进程、显示浏览器主窗口、下载网络资源等。
    • 每打开一个标签页,就创建一个独立的浏览器渲染进程,浏览器渲染进程又称为浏览器内核、Renderer 进程,其内部是多线程的,负责页面渲染、脚本执行等。
    • 浏览器的每一个网络请求,都需要主进程开辟一个单独的网络线程去处理。输入 URL 相当于一个网络请求,解析 HTML 时遇到 JS/CSS/图片等静态资源的引用链接,也需要分别请求。
    • 浏览器的渲染流程:
      • 解析 HTML 文件,构建 DOM 树,同时下载 CSS 等静态资源;
      • CSS 文件下载完成后,解析 CSS 文件,形成 CSSOM 树;
      • DOM 与 CSSOM 合并得到渲染树 Render Tree;
      • 计算渲染树中各个元素的尺寸、位置,这个过程称为回流 Reflow;
      • 浏览器将各个图层的信息发送给 GPU,GPU 绘制页面。
    • 浏览器的渲染进程是多线程的:
      • GUI 渲染线程:
        • 负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等;
        • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行;
        • GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
      • JS 引擎线程:
        • 也称为 JS 内核,负责处理 Javascript 脚本程序(例如 V8 引擎);
        • JS 引擎线程负责解析 Javascript 脚本,运行代码;
        • JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序;
        • GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
      • 事件触发线程:
        • 归属于浏览器而不是 JS 引擎,用来控制事件循环(可以理解,JS 引擎自己都忙不过来,需要浏览器另开线程协助);
        • 当 JS 引擎执行代码块如 setTimeOut 时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件线程中;
        • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理;
        • 由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)。
      • 定时触发线程:
        • 传说中的 setInterval 与 setTimeout 所在线程;
        • 浏览器定时计数器并不是由 JavaScript 引擎计数的(因为 JavaScript 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确);
        • 通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行);
        • W3C 在 HTML 标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms。
      • 异步 http 请求线程:
        • 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求;
        • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,再由 JavaScript 引擎执行。
    • 浏览器运行机制:
      • 所有同步任务都在主线程执行,形成一个执行栈;
      • 主线程之外还存在一个任务队列,只要异步任务有了结果,就在任务队列放置一个事件(比如回调函数);
      • 一旦执行栈中所有同步任务执行完毕,系统就会读取任务队列中的事件,该事件结束等待状态,进入执行栈,开始执行;
      • 主线程不断重复以上步骤,也就是说任务队列这个线程是浏览器开辟的,浏览器将 js 解析器解析的回调函数扔进任务队列中,等浏览器主线程 js 解析器解析完同步任务后,浏览器将任务队列中的任务拿回主线程扔给 js 解析器来继续执行。
    • JS 引擎的执行过程分为三个阶段:
      • 语法分析阶段:分别对加载完成的代码块进行语法检验,语法正确则进入预编译阶段;不正确则停止该代码块的执行,查找下一个代码块并进行加载,加载完成再次进入该代码块的语法分析阶段;
      • 预编译阶段:通过语法分析阶段后,进入预编译阶段,则创建变量对象(创建 arguments 对象(函数运行环境下),函数声明提前解析,变量声明提升),确定作用域链以及 this 指向;
      • 执行阶段。

    ② 服务器

    • 大型的项目,往往是由多台服务器组成一个集群,以此应对庞大的并发访问量。这种情况下,用户的请求都会指向调度服务器,由调度服务器将请求分发给集群中的某台服务器 A,然后调度服务器会将 A 服务器的 HTTP 响应发送给用户。

    在这里插入图片描述

    • 此外,后端还会经过安全认证、参数校验、中间件、执行业务代码、访问数据库等一系列操作,才能向用户返回结果。
    • http 重定向协议实现负载均衡:根据用户的 http 请求计算出一个真实的 web 服务器地址,并将该 web 服务器地址写入 http 重定向响应中返回给浏览器,由浏览器重新进行访问:

    在这里插入图片描述

    • dns 域名解析负载均衡:在 DNS 服务器上配置多个域名对应 IP 的记录,例如一个域名 www.baidu.com 对应一组 web 服务器 IP 地址,域名解析时经过 DNS 服务器的算法将一个域名请求分配到合适的真实服务器上。DNS 负载均衡的控制权在域名服务商手里,网站可能无法做出过多的改善和管理。不能够按服务器的处理能力来分配负载,DNS 负载均衡采用的是简单的轮询算法,不能区分服务器之间的差异,不能反映服务器当前运行状态,所以其的负载均衡效果并不是太好。

    在这里插入图片描述

    • 反向代理负载均衡:反向代理处于 web 服务器这边,反向代理服务器提供负载均衡的功能,同时管理一组 web 服务器,它根据负载均衡算法将请求的浏览器访问转发到不同的 web 服务器处理,处理结果经过反向服务器返回给浏览器。如:浏览器访问请求的地址是反向代理服务器的地址 114.100.80.10,反向代理服务器收到请求,经过负载均衡算法后得到一个真实物理地址 10.0.03,并将请求结果发给真实无服务,真实服务器处理完后通过反向代理服务器返回给请求用户。

    在这里插入图片描述

    • IP 负载均衡:在网络层通过修改目标地址进行负载均衡,用户访问请求到达负载均衡服务器,负载均衡服务器在操作系统内核进程获取网络数据包,根据算法得到一台真实服务器地址,然后将用户请求的目标地址修改成该真实服务器地址,数据处理完后返回给负载均衡服务器,负载均衡服务器收到响应后将自身的地址修改成原用户访问地址后再讲数据返回回去:

    在这里插入图片描述

    • 数据链路层负载均衡:在数据链路层修改 Mac 地址进行负载均衡:
      • 负载均衡服务器的 IP 和它所管理的 web 服务群的虚拟 IP 一致;
      • 负载均衡数据分发过程中不修改访问地址的 IP 地址,而是修改 Mac 地址;
      • 通过这两点达到不修改数据包的原地址和目标地址就可以进行正常的访问。

    在这里插入图片描述

    ③ 网络协议

    • 网络协议栈:OSI 七层模型与 TCP/IP 协议栈各层功能与协议:

    在这里插入图片描述

    • DNS 查询过程
      • DNS 是应用层的协议,作用是将域名转化为 IP,以供传输层建立 TCP 连接;
      • 整体流程:浏览器搜索自身的 DNS 缓存、搜索操作系统的 DNS 缓存、读取本地的 Host 文件和向本地 DNS 服务器进行查询等。
    • DNS 的递归查询与迭代查询
      • 主机向本地域名服务器的查询一般都是采用递归查询,所谓递归查询就是:如果主机所询问的本地域名服务器不知道被查询的域名的 IP 地址,那么本地域名服务器就以 DNS 客户的身份,向其它根域名服务器继续发出查询请求报文(即替主机继续查询),而不是让主机自己进行下一步查询。因此,递归查询返回的查询结果或者是所要查询的 IP 地址,或者是报错,表示无法查询到所需的 IP 地址。
      • 本地域名服务器向根域名服务器的查询的迭代查询,迭代查询的特点:当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的 IP 地址,要么告诉本地服务器:“你下一步应当向哪一个域名服务器进行查询”;然后让本地服务器进行后续的查询。根域名服务器通常是把自己知道的顶级域名服务器的IP地址告诉本地域名服务器,让本地域名服务器再向顶级域名服务器查询;顶级域名服务器在收到本地域名服务器的查询请求后,要么给出所要查询的IP地址,要么告诉本地服务器下一步应当向哪一个权限域名服务器进行查询;最后,知道了所要解析的IP地址或报错,然后把这个结果返回给发起查询的主机。

    在这里插入图片描述
    在这里插入图片描述

      • 递归:客户端只发一次请求,要求对方给出最终结果。
      • 迭代:客户端发出一次请求,对方如果没有授权回答,它就会返回一个能解答这个查询的其它名称服务器列表,客户端会再向返回的列表中发出请求,直到找到最终负责所查域名的名称服务器,从它得到最终结果。
      • 授权回答:向 dns 服务器查询一个域名,刚好这个域名是本服务器负责,返回的结果就是授权回答。
      • 从递归和迭代查询可以看出:
        • 客户端-本地 dns 服务端:这部分属于递归查询;
        • 本地 dns 服务端—外网:这部分属于迭代查询;
        • 递归查询时,返回的结果只有两种:查询成功或查询失败;
        • 迭代查询,又称作重指引,返回的是最佳的查询点或者主机地址。
    • TCP 三次握手、四次挥手
      • TCP 是传输层协议,HTTP 协议通过 TCP 协议发送数据,之所以选择 TCP,是因为 HTTP 对数据的准确性要求高,TCP 能够提供可靠的交付。
    • SSL 四次握手
      • SSL 位于传输层和应用层之间,TCP 和 HTTP 之间,SSL 协议的目的是,为通信双方提供一种在不安全的网络环境中,安全地协商一个密钥的方法。
      • SSL 实现的核心是非对称式密码学,非对称密码包含两个部分:公钥和私钥,一个用作加密,另一个则用作解密。使用其中一个密钥把明文加密后所得的密文,只能用相对应的另一个密钥才能解密得到原本的明文;甚至连最初用来加密的密钥也不能用作解密。
      • HTTPS 本质上就是 HTTPS + SSL,通过 SSL 四次握手过程,客户端和服务端会商定一个共同的随机密钥,用来对称加密,握手结束后,双方都会使用这个约定的随机密钥来加密、解密会话内容,使用的完全是普通的 HTTP 协议。
    • HTTP 的长连接与短连接
      • HTTP/1.0 默认使用的是短连接,也就是说,浏览器每请求一个静态资源,就建立一次连接,任务结束就中断连接。
      • HTTP/1.1 默认使用的是长连接,长连接是指在一个网页打开期间,所有网络请求都使用同一条已经建立的连接;当没有数据发送时,双方需要发检测包以维持此连接;长连接不会永久保持连接,而是有一个保持时间;实现长连接要客户端和服务端都支持长连接。
      • 长连接的优点:TCP 三次握手时会有 1.5 RTT 的延迟,以及建立连接后慢启动(slow-start)特性,当请求频繁时,建立和关闭 TCP 连接会浪费时间和带宽,而重用一条已有的连接性能更好。
      • 长连接的缺点:长连接会占用服务器的资源。
    • HTTP 缓存
      • 浏览器可以将已经请求过的资源(如图片、JS 文件)缓存下来,下次再次请求相同的资源时,直接从缓存读取。
      • 浏览器采用的缓存策略有两种:强制缓存、协商缓存。浏览器根据第一次请求资源时返回的 HTTP 响应头来选择缓存策略。
      • 强制缓存:为资源设置一个过期时间,只要资源没有过期,就读取浏览器的缓存。强制缓存不需要向服务器发起请求,浏览器直接返回 200(from cache)。
      • 协商缓存:浏览器携带缓存资源的元信息,向服务器发起请求,由服务器决定是否使用缓存。如果协商缓存生效,服务器返回 304 和 Not Modified;如果协商缓存失效,服务器返回 200 和请求结果。
      • 协商缓存的原理是:服务端根据资源的元信息,判断浏览器缓存的资源在服务器上是否有改动。元信息有两种,一种是资源的上次修改时间,另一种是资源 Hash 值。前者实现简单,但是精确度低,文件修改时间只能以秒记;后者精确度高,但是性能低,需要计算哈希值。
    展开全文
  • Pythonhtml解析(BeautifulSoup)

    万次阅读 2020-11-05 16:57:54
    BeautifulSoup支持不同的解析器,比如,HTML解析XML解析HTML5解析。一般情况下,我们用的比较多的是 lxml 解析器。 BeautifulSoup安装  BeautifulSoup3 目前已经停止更新,推荐在现在的项目中使用...
  • 前段时间在LeetCode上刷题,遇到了很多涉及字符串进行解析的题目。可能是出于这个原因,最近迷恋上了字符串的解析问题。数学基本运算表达式的解析就涉及这类问题。所谓数学基本运算表达式的解析就是指给定一个...
  • JavaScript详细解析

    万次阅读 多人点赞 2021-09-20 14:28:18
    文章目录1、JavaScript详细解析1.1、JavaScript介绍1.2、快速入门引入js的方式一:内部方式引入js的方式一:外部方式1.3、开发环境搭建1.4、快速入门总结2、JavaScript基本语法2.1、注释2.2、输入输出语句2.3、变量...
  • Wireshark中的名字解析

    千次阅读 2019-12-18 21:12:17
    那么本文就来聊一聊wiresark提供了哪些名字的解析以及其依据,作为我的专栏《wireshark入门到精通》中的其中一篇。 在wireshark解析报文的时候,会显示物理地址的名称。我们知道物理地址是按段分配给各个硬件...
  • 文章目录一、DNS系统1、DNS的作用2、DNS概述3、DNS的定义4、域名结构:二、DNS域名解析方式:三、DNS服务器类型四、构建DNS域名正反向解析服务器步骤1、安装bind软件包2、配置正向解析3、反向解析 一、DNS系统 1、...
  • 数字信封的生成及解析

    千次阅读 2017-07-07 10:37:20
    2.将p10文件拿去ca进行申请,会得到:sig cert、enc cert、enc key(存在于数字信封中) 3.随机生成一个对称密钥symmetric key(这个对称密钥是16位的,可能是AES\SM4\SM1\等) 4.symmetric key + enc key 形成 ...
  • 好久都没更新博客了,最近是真的很忙,每天抽出1小时写博客,有的时候更本没时间,今天写一个解析PE的一个软件,过程和内容很干,干货干货 之前有很多人加我要资料和软件,我从来没说过要钱什么的,只要给个关注和...
  • 报文解析

    万次阅读 2019-01-14 09:38:00
    HTTP报文的流动方向:一次HTTP请求,HTTP报文会“客户端”流到“代理”再流到“服务器”,在服务器工作完成之后,报文又会“服务器”流到“代理”再流到“客户端” 报文的语法:所有的HT...
  • 经过前几篇文章,已经成功把excel文件成功上传成功了,接下来就是要在后台中拿到excel的每一行和每一列进行操作。 怎样解析excel文件拿到里面的内容就变得重中之重了,请看我下面的“神来之笔"。 1、pom.xml ...
  • 域名解析过程详解

    千次阅读 2022-02-24 16:50:30
    不管是哪一个本地域名服务器,若要因特网上任何一个域名进行解析,只要自己无法解析,就首先求助根域名服务器。所以根域名服务器是最重要的域名服务器。假定所有的根域名服务器都瘫痪了,那么整个DNS系统就无法...
  • 学习笔记 - 关于postgres软解析

    万次阅读 2020-09-09 01:02:19
    http://blog.itpub.net/21778816/viewspace-631631/ postgres软解析的例子,比较少。 PREPARE select_mapid(varchar) as select * from bl_rpshanghai where mapid=$1; EXECUTE select_mapid('466163');
  • DNS域名解析

    千次阅读 2022-04-13 19:28:47
    文章目录一.DNS工作原理与类型1.1DNS简介1.2DNS协议及作用1.3DNS域名体系结构1.4DNS查询方式1.4.1递归查询1.4.2迭代查询1.5DNS服务器类型二.DNS域名解析服务配置2.1安装bind软件2.2正向解析配置2.3反向解析配置 ...
  • java解析Excel文件的方法

    千次阅读 2022-04-25 14:38:03
    使用org.apache.poi依赖,我自己写了两个类来实现Excel文件的解析,重要的写前边,后边附加Controller与前端Element-ui的标签实现的文件上传与接收
  • HTTP协议解析

    千次阅读 2022-04-17 22:42:10
    一、HTTP协议基础1.定义2....Get与POST的区别。...用于万维网 (www:World Wide Web)服务器传输超文本到本地浏览器的传送协议 HTTP基于TCP/IP通信协议来传递数据(HTML文件,图片文件,查询结果等) 2.工作原理
  • 语法解析及Antlr

    千次阅读 2020-02-11 19:28:00
    1 语法解析 1.1 语法解析器 1.1.1 执行流程 1.1.2 语法树好处 1.1.3 解析方法LL与LR 1.1.4 抽象语法树(AST) 1.2 语法规则文件 2 Antlr 2.1 解析方法 2.1.1 递归下降语法分析器 2.1.2 ALL(*)解析器 2.2 ...
  • winpcap抓包并进行协议解析

    热门讨论 2010-07-15 23:12:06
    一般网上的抓包程序都是利用原始套接,而基于原始套接的抓包程序是无法抓到网络层一下的包的,如ARP包。本程序实现的任意类型的抓包。 资源中含有1.程序源码 2.winpcap安装程序3.课程设计文档4.VC++设置说明。...
  • CAN报文解析—案例

    万次阅读 多人点赞 2021-02-06 18:51:35
    通常接收到的CAN报文由很多部分(图1)组成,解析报文时用到的主要是帧ID和数据两部分。 3.1 帧ID的组成 接收到的十六进制的ID实际上是由 29 位标识符转换而来,目前大多数的通信协议中都直接给出
  • poi解析excel读取日期为数字的问题

    万次阅读 2017-04-20 09:17:55
    今天在用poi解析excel文件时,碰到一个蛋疼的问题。 在我的excel文件中有一列是日期类型,例如有以下这么一行数据(日期中月份前面的0会自动去掉): 在读取注册日期这个数据时,返回了一串数字,变成了 42149, ...
  • OFD文件解析流程

    千次阅读 多人点赞 2020-08-21 17:54:44
    OFD解析数据流程分享 最近接到公司任务,应客户要求要做ofd格式文件的电票识别。刚接到任务的时候我对于ofd格式文件知道的少之甚少。之前只是听到同事提起过这样的一种格式。在我当时的认知里就单纯的知道他是"中国...
  • Java实现二维码的生成与解析

    千次阅读 2021-11-29 18:41:46
    一.在pom文件中加入Google提供的依赖: <!--google生成二维码--> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId>.../depe
  • dns域名解析原理及过程

    千次阅读 2021-06-03 11:57:22
    域名的层次结构1.DNS域名解析 1.DNS协议 域名解析:就是域名到IP地址的转换过程。(也可以将IP地址转换为相应的域名地址,叫做反向解析) 人们通过注册的域名可以方便地访问到网站。 IP地址是网络上标识站点的数字...
  • 大多数现代应用程序的共同点是,它们需要各种形式的数据进行编码或解码。无论是通过网络下载的 Json 数据,还是存储在本地的模型的某种形式的序列化表示形式,对于几乎任何 Swift 代码库而言,能够可靠地编码和...
  • 解析包时出现问题如何解决?安卓APP在安装的时候如果出现意味的操作,极容易出现“解析包时出现错误”的提示,当然这种情况极其少见,更多的原因是该安卓APP是经过第三方修改过的,建议在官方或应用平台下载APP。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 680,978
精华内容 272,391
关键字:

对从字进行解析