正則從零到簡單分析html標籤

對於正則以前一直是一個"百度程序員", 也許超過一半甚至更多的程序員也是, 那麼此次來學習一下正則表達式.javascript

事出有因

這部分介紹一下需求的由來, 與主要內容無關.html

工做上有了這樣的需求: java

web端從ueditor來的數據格式是html, 也就是<p>文章內容</p>, 並夾雜着諸多標籤和嵌套. react

然而正在開發的是react-native項目, rn的標籤和html徹底不兼容, 是View, Text, Image等. git

那麼把從web存入的數據讀取到rn上就出了大麻煩, 甚至有些地方要進行跳轉, 有些圖片要顯示, 那麼怎麼辦呢.程序員

經過百度"js如何驗證郵箱"已經沒法知足需求了, 只能學一下了.web

目標

萬事有目標, 咱們要把一下內容轉換成rn的內容:正則表達式

<p>嘿嘿嘿<img class="currentImg" id="currentImg" src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1524647151050&di=d488d0e93e72f13643d843066ef26836&imgtype=0&src=http%3A%2F%2Fimg02.imgcdc.com%2Fgame%2Fzh_cn%2Fpicnews%2F11128819%2F20160518%2F22678501_20160518152946632994008.jpg" width="201.33333333333" height="302" title="點擊查看源網頁"/>拖過來的圖片哦<img class="currentImg" id="currentImg" src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1524647205890&di=5bf77e1d35941def729d2059d91deba8&imgtype=0&src=http%3A%2F%2Fnds.tgbus.com%2FUploadFiles%2F201208%2F20120817142937519.jpg" width="201.17405063291" height="302" title="點擊查看源網頁"/><br/>a<img src=‘test1’ />b<img src=‘test2’ />c<img src=‘test3’ />d</p>

轉換結果是:express

<Text>嘿嘿嘿</Text>
<Image source={{uri: 'https://timgsa.baxxxx'}}></Image>
<Text>拖過來的圖片哦</Text>
<Image source={{uri: 'https://txx'}}></Image>
<Text>a</Text>
<Image source={{uri: 'test1'}}></Image>
<Text>b</Text>
...

本文會從零基礎出發達成這個目標.react-native

講解順序: 正則介紹 => 正則語法系統 => 簡單的例子講解 => 嘗試實現目標以及碰到的問題 => 實現目標

什麼是正則

初中時候學的通配符, 用?表明一個任意字符, 用*表明任意個任意字符來進行搜索, 正則也是如此. 好比:

123[abc]匹配如下哪組字符?

  1. 123c
  2. 123d
  3. 123e
  4. 123f

選了1的朋友你已經知道正則是什麼了. 123[abc]就是正則, 表明匹配內容爲: 前三個字符分別爲123, 第四個字符是abc中的一個, 這個正則遇到123a, 123b, 123c均可以匹配成功, 其餘任何都匹配失敗.

正則語法

百度了正則表達式看到的東西都用了不少術語, 讓人有點犯渾. 我通過學習把正則抽象爲兩個部分: 內容修飾.

看到一長串正則以爲稀里嘩啦, 可是裏面的每一個符號必定都屬於內容或是修飾.

內容

內容的形式有3種:

  1. 直接匹配: 舉例就是剛纔的123[abc]中的123, 這種匹配須要徹底吻合才能匹配, 123就惟一匹配123.
  2. 範圍匹配: 用中括號表示, 也就是剛纔例子中的[abc]. 這種狀況也就是三選一. 任意匹配abc, 而不是匹配abc. 還有兩種形式: 加-來表示範圍, 好比[a-z]; 表示排除範圍內的^, 好比[^abc]
  3. 匹配並選擇緩存到子匹配: 用圓括號表示, 圓括號中的內容語法是"直接匹配"但會被記入緩存做爲子匹配, 我記得我最初接觸正則就是url rewrite, 寫了url正則以後用$1, $2來重寫url.

在範圍匹配中, 咱們常常會用: 數字/字母, 也就是[0-9], [a-zA-Z], 可是常常用到重複地寫麻煩又看不能裝逼了, 因此產生了一些快捷方式: \d表明[0-9], \w表明[0-9a-zA-Z_]這正好是經常使用的用戶名和密碼的規則.

這裏深刻一下圓括號匹配的兩個點. 做爲拓展, 能夠先不看一下的內容直接到下一部分.

由於圓括號中能夠用|符號來表示或的關係, 但有時候又不想被加入緩存. 因而能夠用?:來表示不須要緩存. 例子:hello (?:world|regular expression), 用來匹配hello world或者hello regular expression, 但又不須要把world儲存爲緩存.

若是以前已經用圓括號, 那麼指望以後出現一樣的內容, 能夠用\1這樣\加數字來表示. 舉個例子: 單引號和雙引號, 咱們要匹配'123'或者"123", 可是要保持引號一致. ('|")123\1就能夠解決問題.

修飾

我把修飾部分分爲數量修飾和邊界修飾.

  1. 數量修飾: 符號爲{}, 想到了谷歌: go{2,4}gle這個正則能夠匹配google, gooogle, goooogle, 表明這個o能夠匹配2或者4次. 固然只是爲了舉例能夠枚舉, 由於go{2,}gle表明能夠無限個o, 這樣舉例不方便.

    與以前的範圍匹配同樣, 數量修飾也有快捷符號: ?表明{0,1}, *表明{0,}, +表明{1,}. 都很形象, 不用死記, 就像剛纔的d for digital, w for word. 看過一個例子: colou?r 這裏的?表示無關緊要, 美式和英式的拼寫均可以匹配.

    另外在"無上限"的數量的右邊加?表明不貪婪匹配, 會匹配數量最少的內容. 舉例: a+匹配aaaaa的結果爲aaaaa, a+?匹配aaaaa的結果爲a.

  2. 邊界修飾: ^表示字符串的頭, $表示字符串的尾, \b表示字母與空格間的位置. 用來給匹配定位, 具體用法在實際中操做就會有具體感覺了.

    另外, 正則有一種匹配模式是m, 多行匹配模式, 這個狀況裏^$也能匹配每一行的開頭和結尾.

javascript相關函數

首先明確正則是"正則表達式"與"字符串"發生的匹配關係.

js有個對象是RegExp, 使用方法是new RegExp(pattern, mode), 或者是用/包裹的字面量: /pattern/mode.

這裏發現提到了mode匹配模式, 一共三種:

  1. g: 全局匹配, 匹配到一次不會中止, /a/匹配aaa, 若是沒有g結果是一個a, 有g結果是3個a.
  2. i: 忽略大小寫.
  3. m: 多行模式. 和以前提到的\b有聯動.

三個模式不互斥, 疊加的, 也就是能夠new RegExp(patter, 'gin').

正則的方法有:

  1. .test(): 返回是否匹配成功, true或者false.
  2. .exec(): 失敗返回null, 成功返回數組, 位置0是匹配內容, 以後是圓括號匹配的內容. 要注意的是exec是忽略'g'模式的.

字符串的方法:

  1. .replace(pattern, replacement): replacement能夠字符串或方法, 方法的話參數是匹配到的內容.
  2. .match(pattern): 返回數組, 全部匹配到的內容.

分析一些簡單經常使用的例子

是否小數

function isDecimal(strValue )  {  
   var  objRegExp= /^\d+\.\d+$/;
   return  objRegExp.test(strValue);  
}

\d表明數字, +表明至少有1個數字, \.轉移小數點.

連起來看就是: 至少一個數字(\d+) 小數點(\.) 至少一個數字(\d+) .

^$表明頭尾, 真個字符串是小數的所有, 而不是包含小數.

是否中文名

function ischina(str) {
    var reg=/^[\u4E00-\u9FA5]{2,4}$/;   /*定義驗證表達式*/
    return reg.test(str);     /*進行驗證*/
}

這個範圍是中文的編碼範圍: [\u4E00-\u9FA5], {2,4}匹配2~4個. 也就是匹配2~4箇中文.

是否八位數字

function isStudentNo(str) {
    var reg=/^\d{8}$/;   /*定義驗證表達式*/
    return reg.test(str);     /*進行驗證*/
}

是否電話號碼

function isTelCode(str) {
    var reg= /^((0\d{2,3}-\d{7,8})|(1[3584]\d{9}))$/;
    return reg.test(str);
}

分兩個部分: 座機號和手機號, 用|隔開了.

座機號: 0開頭的三位數或四位數 短槓 7~8位數字.

手機號: 第一位1, 第二位3584的一個, 剩下由9個數字湊滿11位電話.

郵箱地址

function IsEmail(str) {
    var reg=/^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+$/;
    return reg.test(str);
}

邁向目標

這個章節開始整理實現需求的思路.

先回憶一下正則的規則, 其實很簡單, 和加減乘除同樣, 有各類符號: [], (), |, -, {}, +, *, ?. 固然也能夠很複雜, 由於也和加減乘除同樣, 能夠嵌套, 而正則的符號原本就多, 嵌套起來更是暈, 有一些符號在不一樣地方有不一樣做用, 好比\^.(思考題: 分析一下這兩個符號有哪些做用, 在什麼場景).

那麼咱們的目標是: 把一段html分析稱rn的標籤.

由於rn沒有parse的功能, 因此不可使用replace. (replace是代碼高亮的經常使用手段).

因此咱們必須把html分解成js對象, 再從js對象裏去分析輸出rn標籤.

由於html標籤分爲多種, 爲了保證完整性和可維護性, 要把各個標籤的正則分開寫, 也便於以後在分析每一個片斷的時候來取子匹配, 好比img標籤的src, a標籤的href.

通過研究, 正則是不能夠拼接的, 只有字符串能夠拼接. 因此咱們要把不一樣標籤的正則寫成字符串, 再在須要的時候拼接. new RegExp(pattern)的pattern參數是能夠接受字符串的.

匹配text的難題與正則匹配的動做分析

衆所周知, 在html裏的text是能夠光禿禿的(在rn裏必須加上Text標籤). 那麼如何匹配這光禿禿的東西呢, 我開始想了一個辦法: 由於text都在標籤以外, 也就是"夾在>和<中的字符", 或者在開頭(^)和<間的, 或者>和結尾($)間的. 結果標籤全都匹配不到了.

緣由是這樣的, 若是有'g'的模式, 匹配的過程是這樣的:

  1. 進行第一次匹配, 匹配成功後把匹配部分排除待匹配內容.
  2. 進行第二次匹配, 匹配成功後把匹配部分排除待匹配內容.
  3. 直到匹配失敗, 返回全部結果.

舉個例子:

'applebananaapple'.match(/(apple|banana)/g)

結果是["apple", "banana", "apple"]

若是把banana的最後一個字母和apple的第一個字母寫成一個:

'applebananapple'.match(/(apple|banana)/g)

那麼結果就是["apple", "banana"]了.

反而利用了這個特色, 把text的正則寫成: 不包含<>/ ([^<>/]+), 並添加在最後一個匹配, 就能正確地匹配出text啦.

揭曉答案

寫得急促也許有遺漏, 最後貼上完成需求的代碼, 語言是rn, 在map輸出的時候帶着一些項目業務的邏輯請無視.

import React, {Component} from 'react'
import {Text, View, Image, Platform, StyleSheet, TouchableOpacity} from 'react-native'
import {ENVS} from '../../config/apiHost'

/*
    必須props: @html: html內容
    可選props:  @style: 字體style; @magnifyImg: 顯示大圖
 */

const regex = {  // '_' for close tag
    p: `<p[^>]*?>`,
    _p: `<\/p>`,
    span: `<span[^>]*?>`,
    _span: `<\/span>`,
    br: `<br\/>`,
    a: `<a[^>]*?href=(\'|")([^>]+?)\\1[^>]*?>([^<]+?)<\\/a>`, // $1 是標點符號用來處理匹配 $2 href的帶引號的內容 $3 文件名(a標籤的innerText),
    img: `<img[^>]*?src=('|")([^>]+?)\\1[^>]*?\\/>`, // $1 標點符號 $2 src的內容
    text: `[^<>/]+`, // 匹配剩下的, 必定要放在最後
}

const tobeRemoved = new RegExp(`(?:${[regex.p, regex._p, regex.span, regex._span, regex.br].join('|')})`, 'g')

const parseToAst = new RegExp(`(?:${[regex.a, regex.img, regex.text].join('|')})`, 'g')

export default class Parsed extends Component {
    render () {
        let str = this.props.html.trim()
        if (!str) {
            return (
                <Text>html attr not passed to component 'parseHtml'</Text>
            )
        }
        matches = str.replace(tobeRemoved, '').match(parseToAst)
        return (
            <View>
                {matches.map((block, index) => {
                    for (let [key, value] of Object.entries(regex)) {
                        let res = new RegExp(value).exec(block)
                        if (res) {
                            if (key === 'text') {
                                return (
                                    <Text style={this.props.style} key={index}>{block}</Text>
                                )
                            }
                            if (key === 'a') {
                                if (res[2].includes('files')) { // 判斷附件
                                    if (/[jpg|png|jpeg]/i.test(res[3])) { // 判斷圖片
                                        let imgId = res[2].match(/\d+/)[0]
                                        return (
                                            <TouchableOpacity key={index} onPress={() => {this.props.magnifyImg && this.props.magnifyImg(ENVS.production.api_base_url + '/files/' + imgId)}} >
                                                <Image style={{width: 100, height: 100, margin: 10}} source={{uri: ENVS.production.api_base_url + '/files/' + imgId}}></Image>
                                            </TouchableOpacity>
                                        )
                                    }
                                }
                            }
                            if (key === 'img') {
                                return (
                                    <TouchableOpacity key={index} onPress={() => {this.props.magnifyImg && this.props.magnifyImg(res[2])}}>
                                        <Image style={{width: 100, height: 100, margin: 10}} source={{uri: res[2]}}></Image>
                                    </TouchableOpacity>
                                )
                            }
                        }
                    }
                })}
            </View>
        )
    }
}

原文地址

相關文章
相關標籤/搜索