Android Html解析

47132 ワード

前編のAndroid SpannablesStringの浅い分析ではhtmlを用いてテキスト処理の効果を実現した.当時の設定部分のコードは以下の通りです.
private void setText() {
    String originText = "#    #      Android N          (Developer Preview)";

    String effect1 = "<font color='#FF0000'>#    #</font> <br>       Android " +
            "N          <a href='http://developer.android.com/index.html'>(Developer Preview)</a>";

    String effect2 = "<font color='#303F9F'>#    #</font>       Android " +
            "N          <a href='http://developer.android.com/index.html'>(Developer Preview)</a>";
    StringBuilder sb = new StringBuilder(originText);
    sb.append("<br><br><br><br>");
    sb.append(effect1);
    sb.append("<br><br><br><br>");
    sb.append(effect2);
    textView.setText(Html.fromHtml(sb.toString()));
    textView.setMovementMethod(LinkMovementMethod.getInstance());
}

ここでは,一部の文字の表示色を変更するとともに,他の一部の内容にクリックイベントの処理を追加し,下線を加えた.今日はHtmlコードの解析を見てみましょう.
かいせきてつづき
上のコードで設定時にHtmlを呼び出しました.fromHtml()関数は,この関数からhtmlコンテンツをTextViewに解析して示すことができるコンテンツであることが分かる.ここから、このようなことをしたかどうかを見てみましょう.解析できる内容はどんなものですか.
この関数を呼び出す前に、まずHtmlのコンストラクション関数private Html()を見てみましょう.privateによって修飾されているのが見えます.外部ではHtmlインスタンスを構築できないことを示しています.コードにはインスタンスを返す場所もないので、ここでの使用方法は静的な方法で呼び出されています.
fromHtml()関数を見てみましょう.
public static Spanned fromHtml(String source) {
    return fromHtml(source, null, null);
}

public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) {
    Parser parser = new Parser();
    try {
        parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
    } catch (org.xml.sax.SAXNotRecognizedException e) {
        // Should not happen.
        throw new RuntimeException(e);
    } catch (org.xml.sax.SAXNotSupportedException e) {
        // Should not happen.
        throw new RuntimeException(e);
    }

    HtmlToSpannedConverter converter =
        new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser);
    return converter.convert();
}

       Html.fromHtml関数は、3つのパラメータのromHtml(String source,ImageGetter imageGetter,TagHandler tagHandler)を呼び出し続け、パラメータはそれぞれデータソース、ピクチャ処理、tag処理である.まず、orgにあるParserの例を構築する.ccil.cowan.tagsoupパッケージの下で、私の地元のコードはナビゲートできません.ここでは、このコードの接続Parserソースコードをあげます.
ここではTagSoupについて追加します.彼の公式紹介を見てみましょう.
This is the home page of TagSoup, a SAX-compliant parser written in Java that, instead of parsing well-formed or valid XML, parses HTML as it is found in the wild: poor, nasty and brutish, though quite often far from short. TagSoup is designed for people who have to process this stuff using some semblance of a rational application design. By providing a SAX interface, it allows standard XML tools to be applied to even the worst HTML. TagSoup also includes a command-line processor that reads HTML files and can generate either clean HTML or well-formed XML that is a close approximation to XHTML.
大体の意味は次の通りです.
TagSoupはJava言語でHtmlを解析するツールを作成し、SAXエンジンを通じて構造が悪く、狂ったHTMLドキュメントを解析します.TagSoupは、HTMLドキュメントを構造の良いXMLドキュメントに変換し、開発者が取得したHTMLドキュメントを解析するなどの操作を容易にすることができます.同時にTagSoupはコマンドラインプログラムを提供し、TagSoupを実行してHTMLドキュメントを解析することができます.
Parserを構築してParserを呼び出す.setProperty関数は、schemaPropertyに伝わります.ここは文字列で、HTML Schemaオブジェクトにも伝わります.HTML SchemaオブジェクトにはHTMLのすべての属性ノードが羅列されています.HTML SchemaもTagSoupに属しています.ここではTagSoupについてあまり紹介しません.彼が何をしているのか知っていればいいです.
最後にHtmlToSpannedConverterインスタンスを構築し、上に渡されたパラメータ、データソース、imageGetter、tagHandler、parserを入力し、最後にHtmlToSpannedConverterのconvert関数を呼び出した.ここではこのクラス名を見て,主にHtmlコンテンツをSpanオブジェクトに変換する操作を大まかに知ることができる.ここでは前編に戻りますが、最終的にはSpanタイプが扱われています.
ここではまずHtmlToSpannedConverterの構造関数を見てみましょう.
public HtmlToSpannedConverter(String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
        Parser parser) {
    mSource = source;
    mSpannableStringBuilder = new SpannableStringBuilder();
    mImageGetter = imageGetter;
    mTagHandler = tagHandler;
    mReader = parser;
}

入力されたパラメータを対応する変数に割り当てると同時に、StringBuilderタイプのSpannablesStringBuilderオブジェクトが構築されます.StringBuilderは主に文字列を接続し、不要なスペースの浪費を減らします.SpannablesStringBuilderはもちろんSpannablesStringに接続します.SpannablesStringの主な内容は前のAndroid SpannablesStringの浅い分析を見て、convert関数が何をしたかを見続けましょう.
public Spanned convert() {
    mReader.setContentHandler(this);
    try {
        mReader.parse(new InputSource(new StringReader(mSource)));
    } catch (IOException e) {
        // We are reading from a string. There should not be IO problems.
        throw new RuntimeException(e);
    } catch (SAXException e) {
        // TagSoup doesn't throw parse exceptions.
        throw new RuntimeException(e);
    }

    // Fix flags and range for paragraph-type markup.
    Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
    for (int i = 0; i < obj.length; i++) {
        int start = mSpannableStringBuilder.getSpanStart(obj[i]);
        int end = mSpannableStringBuilder.getSpanEnd(obj[i]);

        // If the last line of the range is blank, back off by one.
        if (end - 2 >= 0) {
            if (mSpannableStringBuilder.charAt(end - 1) == '
'
&& mSpannableStringBuilder.charAt(end - 2) == '
'
) { end--; } } if (end == start) { mSpannableStringBuilder.removeSpan(obj[i]); } else { mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH); } } return mSpannableStringBuilder; }

ここでまずmReaderを呼び出します.setContentHandler関数、mReaderは前述のParserインスタンスであり、その後mReaderを呼び出す.parse関数は、入力されたsourceをInputSourceオブジェクトとして構築します.私たちはまずparseオブジェクトを見に行って、それから下を見ます.
public void parse(InputSource input) throws IOException, SAXException {
    setup();
    Reader r = getReader(input);
    theContentHandler.startDocument();
    theScanner.resetDocumentLocator(input.getPublicId(), input.getSystemId());
    if (theScanner instanceof Locator) {
        theContentHandler.setDocumentLocator((Locator) theScanner);
    }
    if (!(theSchema.getURI().equals("")))
        theContentHandler.startPrefixMapping(theSchema.getPrefix(), theSchema.getURI());
    theScanner.scan(r, this);
}

まずsetUpを呼び出し、ここでは主にいくつかの付与、初期化操作を行います.次に入力されたInputSourceオブジェクトをReaderオブジェクトに変換し、ContentHanderのstartDocumentを呼び出します.ここではHtmlToSpannedConverterのstartDocumentが呼び出されます.空の関数で何もしていないことがわかります.ここでは主に最も主要な部分を見てみましょうscan(r,this)、ここではコードを見ないで、彼は主に以下の操作をしました.
  • まずtheScannerはHTML Scannerタイプであるため、このscanは呼び出されたHTML Scannerのscan関数であり、ReaderとScanHanderに伝わり、ScanHanderはParserで実現される.
  • scanでは各文字を読み出し、特殊文字を除去し、statetableテーブルに従って各文字を処理する
  • .
  • は、いくつかの文字に遭遇したときにsaveメソッド処理を呼び出し、ここでは主に入力bufferを表示するたびに、20文字以上が処理される.ScanHanderのpcdata関数を呼び出します.
  • pcdata関数は空白文字を処理し、その後rectify関数を呼び出し、関数名から修正の意味であることがわかる.
  • rectify関数ではElementチェーンテーブルが1つ処理されるため、複数回処理され、最後にrestartまたはpush関数が呼び出され、restartでもpush関数が呼び出され続けます.
  • push関数でやっと属性のtheContentHandlerを見て、theContentHandlerを呼び出しました.startElement(namespace, localName, name, e.atts());ここではHtmlToSpannedConverterで実装されているので、ここで実際に呼び出されているのはHtmlToSpannedConverterのstartElement関数であり、startElement関数はhandleStartTag関数を呼び出し続けている.

  • handleStartTag関数を見てみましょう.
    private void handleStartTag(String tag, Attributes attributes) {
        if (tag.equalsIgnoreCase("br")) {
            // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
            // so we can safely emite the linebreaks when we handle the close tag.
        } else if (tag.equalsIgnoreCase("p")) {
            handleP(mSpannableStringBuilder);
        } else if (tag.equalsIgnoreCase("div")) {
            handleP(mSpannableStringBuilder);
        } else if (tag.equalsIgnoreCase("strong")) {
            start(mSpannableStringBuilder, new Bold());
        } else if (tag.equalsIgnoreCase("b")) {
            start(mSpannableStringBuilder, new Bold());
        } else if (tag.equalsIgnoreCase("em")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("cite")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("dfn")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("i")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("big")) {
            start(mSpannableStringBuilder, new Big());
        } else if (tag.equalsIgnoreCase("small")) {
            start(mSpannableStringBuilder, new Small());
        } else if (tag.equalsIgnoreCase("font")) {
            startFont(mSpannableStringBuilder, attributes);
        } else if (tag.equalsIgnoreCase("blockquote")) {
            handleP(mSpannableStringBuilder);
            start(mSpannableStringBuilder, new Blockquote());
        } else if (tag.equalsIgnoreCase("tt")) {
            start(mSpannableStringBuilder, new Monospace());
        } else if (tag.equalsIgnoreCase("a")) {
            startA(mSpannableStringBuilder, attributes);
        } else if (tag.equalsIgnoreCase("u")) {
            start(mSpannableStringBuilder, new Underline());
        } else if (tag.equalsIgnoreCase("sup")) {
            start(mSpannableStringBuilder, new Super());
        } else if (tag.equalsIgnoreCase("sub")) {
            start(mSpannableStringBuilder, new Sub());
        } else if (tag.length() == 2 &&
                   Character.toLowerCase(tag.charAt(0)) == 'h' &&
                   tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
            handleP(mSpannableStringBuilder);
            start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
        } else if (tag.equalsIgnoreCase("img")) {
            startImg(mSpannableStringBuilder, attributes, mImageGetter);
        } else if (mTagHandler != null) {
            mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
        }
    }

    上記のコードからもhtmlのラベルを解析できることが分かるが,ここではすべてのElementのstartラベルのみを処理し,endラベルはまだ処理されていないが,startは主にtextにSpanを設定し,初期と終了までは同じで,空のクラスを設定し,識別対象としたが,ラベルp,divについては,imgなどは異なる設定をしており、特にimgでは主に設定置換アイコンが呼び出されており、この機能は前のImageGetterで実現されている.
    private static void start(SpannableStringBuilder text, Object mark) {
       int len = text.length();
       text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
    }

    上はstartラベルを作っただけで、endラベルはまた上を作って、前にrectifyがpushを呼び出してスタックを押さえたと言っていましたが、当時は他のコードを省略していました.ここではもう一つの処理をして、一つのElementスキャンが終わった後にpop出スタックを行って、popの中でElendement関数を呼び出して、ここで実際にHtmlToSpannedConverterのElendement関数を呼び出しました.endElement関数ではhandleEndTag関数の呼び出しが続行されます.
    handleEndTag関数を見てみましょう.
    private void handleEndTag(String tag) {
            if (tag.equalsIgnoreCase("br")) {
                handleBr(mSpannableStringBuilder);
            } else if (tag.equalsIgnoreCase("p")) {
                handleP(mSpannableStringBuilder);
            } else if (tag.equalsIgnoreCase("div")) {
                handleP(mSpannableStringBuilder);
            } else if (tag.equalsIgnoreCase("strong")) {
                end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
            } else if (tag.equalsIgnoreCase("b")) {
                end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
            } else if (tag.equalsIgnoreCase("em")) {
                end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
            } else if (tag.equalsIgnoreCase("cite")) {
                end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
            } else if (tag.equalsIgnoreCase("dfn")) {
                end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
            } else if (tag.equalsIgnoreCase("i")) {
                end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
            } else if (tag.equalsIgnoreCase("big")) {
                end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
            } else if (tag.equalsIgnoreCase("small")) {
                end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
            } else if (tag.equalsIgnoreCase("font")) {
                endFont(mSpannableStringBuilder);
            } else if (tag.equalsIgnoreCase("blockquote")) {
                handleP(mSpannableStringBuilder);
                end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
            } else if (tag.equalsIgnoreCase("tt")) {
                end(mSpannableStringBuilder, Monospace.class,
                        new TypefaceSpan("monospace"));
            } else if (tag.equalsIgnoreCase("a")) {
                endA(mSpannableStringBuilder);
            } else if (tag.equalsIgnoreCase("u")) {
                end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
            } else if (tag.equalsIgnoreCase("sup")) {
                end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
            } else if (tag.equalsIgnoreCase("sub")) {
                end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
            } else if (tag.length() == 2 &&
                    Character.toLowerCase(tag.charAt(0)) == 'h' &&
                    tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
                handleP(mSpannableStringBuilder);
                endHeader(mSpannableStringBuilder);
            } else if (mTagHandler != null) {
                mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
            }
        }

    ここではhandleStartTagとペアになって処理し,主に置換処理を行い,statに入力されたクラス識別に基づいてspanオブジェクトを再設定する.
    private static void end(SpannableStringBuilder text, Class kind, Object repl) {
        int len = text.length();
        Object obj = getLast(text, kind);
        int where = text.getSpanStart(obj);
    
        text.removeSpan(obj);
    
        if (where != len) {
           text.setSpan(repl, where, len,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }

    ここでkindは前のmark,replが置き換えられたspanオブジェクトであり,ここでは主に以下のspanオブジェクトを用いることができる:StyleSpan,RelativeSizeSpan,QuoteSpan,UnderlineSpan,SuperscriptSpan.彼がSpannablesStringより処理できるものが少ないのが見えますが、ここではheaderの中で2つのspan処理が行われているのが見えます.最後に、すべてのノードが一致していない場合、自分でmTagHandlerを実現した場合は、mTagHandlerを使用して処理します.
    ここでconvert関数に戻り、parse処理が完了した後、mSpannablesStringBuilderの処理を継続し、設定されたspanをループ処理し、''改行を無視し、spanのstartがendと同じ位置にある場合、ノードにコンテンツ処理がないことを示し、span removeを削除します.最後に、処理が完了したSpannablesStringBuilderをTextViewに戻して表示します.
    ぎゃくかい
    上でhtmlをSpannablesStringBuilderに変換し、HtmlはSpannablesStringBuilderコンテンツをhtmlコンテンツに変換することもできます.ここでは主にtoHtml関数を呼び出します.
    public static String toHtml(Spanned text) {
        StringBuilder out = new StringBuilder();
        withinHtml(out, text);
        return out.toString();
    }
    
    private static void withinHtml(StringBuilder out, Spanned text) {
        int len = text.length();
    
        int next;
        for (int i = 0; i < text.length(); i = next) {
            next = text.nextSpanTransition(i, len, ParagraphStyle.class);
            ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
            String elements = " ";
            boolean needDiv = false;
    
            for(int j = 0; j < style.length; j++) {
                if (style[j] instanceof AlignmentSpan) {
                    Layout.Alignment align =
                        ((AlignmentSpan) style[j]).getAlignment();
                    needDiv = true;
                    if (align == Layout.Alignment.ALIGN_CENTER) {
                        elements = "align=\"center\" " + elements;
                    } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
                        elements = "align=\"right\" " + elements;
                    } else {
                        elements = "align=\"left\" " + elements;
                    }
                }
            }
            if (needDiv) {
                out.append("<div ").append(elements).append(">");
            }
    
            withinDiv(out, text, i, next);
    
            if (needDiv) {
                out.append("</div>");
            }
        }
    }

    toHtml関数ではwithinHtml関数が呼び出され、withinHtml関数ではspanオブジェクトがループ処理されます.AlignmentSpanオブジェクトの場合、外側のレイヤdivがネストされ、その後withinDivが呼び出されて処理が続行されます.
    private static void withinDiv(StringBuilder out, Spanned text, int start, int end) {
        int next;
        for (int i = start; i < end; i = next) {
            next = text.nextSpanTransition(i, end, QuoteSpan.class);
            QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
    
            for (QuoteSpan quote : quotes) {
                out.append("<blockquote>");
            }
    
            withinBlockquote(out, text, i, next);
    
            for (QuoteSpan quote : quotes) {
                out.append("</blockquote>
    "
    ); } } }

    withinDivでテキストの処理を続行します.最終的にはwithinParagraphを呼び出してテキストを処理します.withinParagraphは、対応するspanオブジェクトを処理します.
    private static boolean withinParagraph(StringBuilder out, Spanned text, int start, int end, int nl, boolean last) {
        int next;
        for (int i = start; i < end; i = next) {
            next = text.nextSpanTransition(i, end, CharacterStyle.class);
            CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);
    
            for (int j = 0; j < style.length; j++) {
                if (style[j] instanceof StyleSpan) {
                    int s = ((StyleSpan) style[j]).getStyle();
    
                    if ((s & Typeface.BOLD) != 0) {
                        out.append("<b>");
                    }
                    if ((s & Typeface.ITALIC) != 0) {
                        out.append("<i>");
                    }
                }
                if (style[j] instanceof TypefaceSpan) {
                    String s = ((TypefaceSpan) style[j]).getFamily();
    
                    if ("monospace".equals(s)) {
                        out.append("<tt>");
                    }
                }
                if (style[j] instanceof SuperscriptSpan) {
                    out.append("<sup>");
                }
                if (style[j] instanceof SubscriptSpan) {
                    out.append("<sub>");
                }
                if (style[j] instanceof UnderlineSpan) {
                    out.append("<u>");
                }
                if (style[j] instanceof StrikethroughSpan) {
                    out.append("<strike>");
                }
                if (style[j] instanceof URLSpan) {
                    out.append("<a href=\"");
                    out.append(((URLSpan) style[j]).getURL());
                    out.append("\">");
                }
                if (style[j] instanceof ImageSpan) {
                    out.append("<img src=\"");
                    out.append(((ImageSpan) style[j]).getSource());
                    out.append("\">");
                    i = next;
                }
                if (style[j] instanceof AbsoluteSizeSpan) {
                    out.append("<font size =\"");
                    out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6);
                    out.append("\">");
                }
                if (style[j] instanceof ForegroundColorSpan) {
                    out.append("<font color =\"#");
                    String color = Integer.toHexString(((ForegroundColorSpan)style[j]).getForegroundColor() + 0x01000000);
                    while (color.length() < 6) {
                        color = "0" + color;
                    }
                    out.append(color);
                    out.append("\">");
                }
            }
    
            withinStyle(out, text, i, next);
    
            for (int j = style.length - 1; j >= 0; j--) {
                if (style[j] instanceof ForegroundColorSpan) {
                    out.append("</font>");
                }
                if (style[j] instanceof AbsoluteSizeSpan) {
                    out.append("</font>");
                }
                if (style[j] instanceof URLSpan) {
                    out.append("</a>");
                }
                if (style[j] instanceof StrikethroughSpan) {
                    out.append("</strike>");
                }
                if (style[j] instanceof UnderlineSpan) {
                    out.append("</u>");
                }
                if (style[j] instanceof SubscriptSpan) {
                    out.append("</sub>");
                }
                if (style[j] instanceof SuperscriptSpan) {
                    out.append("</sup>");
                }
                if (style[j] instanceof TypefaceSpan) {
                    String s = ((TypefaceSpan) style[j]).getFamily();
    
                    if (s.equals("monospace")) {
                        out.append("</tt>");
                    }
                }
                if (style[j] instanceof StyleSpan) {
                    int s = ((StyleSpan) style[j]).getStyle();
    
                    if ((s & Typeface.BOLD) != 0) {
                        out.append("</b>");
                    }
                    if ((s & Typeface.ITALIC) != 0) {
                        out.append("</i>");
                    }
                }
            }
        }
    
        if (nl == 1) {
            out.append("<br>
    "
    ); return false; } else { for (int i = 2; i < nl; i++) { out.append("<br>"); } return !last; } } private static void withinStyle(StringBuilder out, CharSequence text, int start, int end) { for (int i = start; i < end; i++) { char c = text.charAt(i); if (c == '<') { out.append("&lt;"); } else if (c == '>') { out.append("&gt;"); } else if (c == '&') { out.append("&amp;"); } else if (c >= 0xD800 && c <= 0xDFFF) { if (c < 0xDC00 && i + 1 < end) { char d = text.charAt(i + 1); if (d >= 0xDC00 && d <= 0xDFFF) { i++; int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00; out.append("&#").append(codepoint).append(";"); } } } else if (c > 0x7E || c < ' ') { out.append("&#").append((int) c).append(";"); } else if (c == ' ') { while (i + 1 < end && text.charAt(i + 1) == ' ') { out.append("&nbsp;"); i++; } out.append(' '); } else { out.append(c); } } }

    ここでは異なるspanに基づいて対応するhtmlコンテンツを生成し,最後にhtml戻りを生成する.
    まとめ
    ここではプロセス全体を初歩的に解析しただけで、TagSoupのように、自分でコードを見ることができる内容がたくさんあります.TagSoupが狂ったhtmlの内容をどのように解析しているかを見てみましょう.
    一般的に簡単な効果は、htmlを使えばいいのですが、相対的にコード量が少なく、htmlを知っている人はどのような効果を実現する必要があるのか、複雑な効果や機能があれば、あるいは同じテキストに複数の効果と機能が必要であれば、SpannablesStringを採用して実現する必要があります.実際の開発では需要に応じて対応する効果を実現する必要がある.