【问题标题】:Get All Links in a Document获取文档中的所有链接
【发布时间】:2013-09-10 19:32:08
【问题描述】:

假设 Google Docs/Drive 中的“普通文档”(例如段落、列表、表格)包含分散在整个内容中的外部链接,您如何使用 Google Apps 脚本编译存在的链接列表?

具体来说,我想通过在每个 url 中搜索 oldText 并在每个 url 中将其替换为 newText 来更新文档中所有损坏的链接,但不是文本。

我认为开发文档的replacing text 部分不是我需要的——我需要扫描文档的每个元素吗?我可以只使用editAsText 并使用 html 正则表达式吗?示例将不胜感激。

【问题讨论】:

    标签: google-apps-script google-docs


    【解决方案1】:

    这只是最痛苦的! 代码可用as part of a gist

    是的,我不会拼写。

    getAllLinks

    这是一个实用函数,用于扫描文档中的所有 LinkUrls,并以数组的形式返回它们。

    /**
     * Get an array of all LinkUrls in the document. The function is
     * recursive, and if no element is provided, it will default to
     * the active document's Body element.
     *
     * @param {Element} element The document element to operate on. 
     * .
     * @returns {Array}         Array of objects, vis
     *                              {element,
     *                               startOffset,
     *                               endOffsetInclusive, 
     *                               url}
     */
    function getAllLinks(element) {
      var links = [];
      element = element || DocumentApp.getActiveDocument().getBody();
      
      if (element.getType() === DocumentApp.ElementType.TEXT) {
        var textObj = element.editAsText();
        var text = element.getText();
        var inUrl = false;
        for (var ch=0; ch < text.length; ch++) {
          var url = textObj.getLinkUrl(ch);
          if (url != null) {
            if (!inUrl) {
              // We are now!
              inUrl = true;
              var curUrl = {};
              curUrl.element = element;
              curUrl.url = String( url ); // grab a copy
              curUrl.startOffset = ch;
            }
            else {
              curUrl.endOffsetInclusive = ch;
            }          
          }
          else {
            if (inUrl) {
              // Not any more, we're not.
              inUrl = false;
              links.push(curUrl);  // add to links
              curUrl = {};
            }
          }
        }
        if (inUrl) {
          // in case the link ends on the same char that the element does
          links.push(curUrl); 
        }
      }
      else {
        var numChildren = element.getNumChildren();
        for (var i=0; i<numChildren; i++) {
          links = links.concat(getAllLinks(element.getChild(i)));
        }
      }
    
      return links;
    }
    

    findAndReplaceLinks

    此实用程序基于 getAllLinks 进行查找和替换功能。

    /**
     * Replace all or part of UrlLinks in the document.
     *
     * @param {String} searchPattern    the regex pattern to search for 
     * @param {String} replacement      the text to use as replacement
     *
     * @returns {Number}                number of Urls changed 
     */
    function findAndReplaceLinks(searchPattern,replacement) {
      var links = getAllLinks();
      var numChanged = 0;
      
      for (var l=0; l<links.length; l++) {
        var link = links[l];
        if (link.url.match(searchPattern)) {
          // This link needs to be changed
          var newUrl = link.url.replace(searchPattern,replacement);
          link.element.setLinkUrl(link.startOffset, link.endOffsetInclusive, newUrl);
          numChanged++
        }
      }
      return numChanged;
    }
    

    演示用户界面

    为了演示这些实用程序的使用,这里有几个 UI 扩展:

    function onOpen() {
      // Add a menu with some items, some separators, and a sub-menu.
      DocumentApp.getUi().createMenu('Utils')
          .addItem('List Links', 'sidebarLinks')
          .addItem('Replace Link Text', 'searchReplaceLinks')
          .addToUi();
    }
    
    function searchReplaceLinks() {
      var ui = DocumentApp.getUi();
      var app = UiApp.createApplication()
                     .setWidth(250)
                     .setHeight(100)
                     .setTitle('Change Url text');
      var form = app.createFormPanel();
      var flow = app.createFlowPanel();
      flow.add(app.createLabel("Find: "));
      flow.add(app.createTextBox().setName("searchPattern"));
      flow.add(app.createLabel("Replace: "));
      flow.add(app.createTextBox().setName("replacement"));
      var handler = app.createServerHandler('myClickHandler');
      flow.add(app.createSubmitButton("Submit").addClickHandler(handler));
      form.add(flow);
      app.add(form);
      ui.showDialog(app);
    }
    
    // ClickHandler to close dialog
    function myClickHandler(e) {
      var app = UiApp.getActiveApplication();
    
      app.close();
      return app;
    }
    
    function doPost(e) {
      var numChanged = findAndReplaceLinks(e.parameter.searchPattern,e.parameter.replacement);
      var ui = DocumentApp.getUi();
      var app = UiApp.createApplication();
      
      sidebarLinks(); // Update list
    
      var result = DocumentApp.getUi().alert(
          'Results',
          "Changed "+numChanged+" urls.",
          DocumentApp.getUi().ButtonSet.OK);
    }
    
    
    /**
     * Shows a custom HTML user interface in a sidebar in the Google Docs editor.
     */
    function sidebarLinks() {
      var links = getAllLinks();
      var sidebar = HtmlService
              .createHtmlOutput()
              .setTitle('URL Links')
              .setWidth(350 /* pixels */);
    
      // Display list of links, url only.
      for (var l=0; l<links.length; l++) {
        var link = links[l];
        sidebar.append('<p>'+link.url);
      }
      
      DocumentApp.getUi().showSidebar(sidebar);
    }
    

    【讨论】:

    • 呃,寻找链接太难看了——不过这不是你的错:)
    • 我想我在getAllLinks 中发现了一个错误——如果链接是文本行中的最后一件事(即在段落中),那么它不会被添加到列表中,因为它是在 for 循环之后不检查 if(inUrl)。它在您的屏幕截图示例中有效,因为链接后有一段时间。
    • stackoverflow.com/a/18795330/1037948 下方查看我的疯狂回答——因为你启发了我的解决方案,我将把你作为官方回答
    • 你在哪里运行这段代码?在 chrome 开发者控制台中?如果是这样,当运行getAllLinks 时,我收到错误Uncaught SyntaxError: Unexpected token **。此外,在谷歌文档中,我看不到上图中的“Utils”标签
    • @JoannaMarietti 哦...我第一次听说脚本编辑器。凉爽的!谢谢
    【解决方案2】:

    我为您的第一个问题提供了另一个更简短的答案,关于迭代文档正文中的 all 链接。这个指导性代码返回当前文档正文中的一个平面链接数组,其中每个链接由一个对象表示,其条目指向文本元素 (text)、包含它的段落元素或列表项元素 (@987654322 @)、链接出现的文本中的偏移索引 (startOffset) 和 URL 本身 (url)。希望您会发现它很容易满足您自己的需求。

    它使用getTextAttributeIndices() 方法而不是遍历文本的每个字符,因此预计比以前写的答案要快得多。

    编辑:自从最初发布这个答案以来,我修改了几次函数。它现在还 (1) 包括每个链接的 endOffsetInclusive 属性(请注意,对于延伸到文本元素末尾的链接,它可以是 null - 在这种情况下,可以使用 link.text.length-1 代替); (2) 在文档的所有部分中查找链接,不仅是正文,并且 (3) 包括 sectionisFirstPageSection 属性以指示链接所在的位置; (4) 接受参数mergeAdjacent,当设置为真时,将只返回一个链接条目,用于链接到同一 URL 的连续文本段(例如,如果部分文本是样式与另一部分不同)。

    为了在所有部分下包含链接,引入了一个新的实用函数 iterateSections()

    /**
     * Returns a flat array of links which appear in the active document's body. 
     * Each link is represented by a simple Javascript object with the following 
     * keys:
     *   - "section": {ContainerElement} the document section in which the link is
     *     found. 
     *   - "isFirstPageSection": {Boolean} whether the given section is a first-page
     *     header/footer section.
     *   - "paragraph": {ContainerElement} contains a reference to the Paragraph 
     *     or ListItem element in which the link is found.
     *   - "text": the Text element in which the link is found.
     *   - "startOffset": {Number} the position (offset) in the link text begins.
     *   - "endOffsetInclusive": the position of the last character of the link
     *      text, or null if the link extends to the end of the text element.
     *   - "url": the URL of the link.
     *
     * @param {boolean} mergeAdjacent Whether consecutive links which carry 
     *     different attributes (for any reason) should be returned as a single 
     *     entry.
     * 
     * @returns {Array} the aforementioned flat array of links.
     */
    function getAllLinks(mergeAdjacent) {
      var links = [];
    
      var doc = DocumentApp.getActiveDocument();
    
    
      iterateSections(doc, function(section, sectionIndex, isFirstPageSection) {
        if (!("getParagraphs" in section)) {
          // as we're using some undocumented API, adding this to avoid cryptic
          // messages upon possible API changes.
          throw new Error("An API change has caused this script to stop " + 
                          "working.\n" +
                          "Section #" + sectionIndex + " of type " + 
                          section.getType() + " has no .getParagraphs() method. " +
            "Stopping script.");
        }
    
        section.getParagraphs().forEach(function(par) { 
          // skip empty paragraphs
          if (par.getNumChildren() == 0) {
            return;
          }
    
          // go over all text elements in paragraph / list-item
          for (var el=par.getChild(0); el!=null; el=el.getNextSibling()) {
            if (el.getType() != DocumentApp.ElementType.TEXT) {
              continue;
            }
    
            // go over all styling segments in text element
            var attributeIndices = el.getTextAttributeIndices();
            var lastLink = null;
            attributeIndices.forEach(function(startOffset, i, attributeIndices) { 
              var url = el.getLinkUrl(startOffset);
    
              if (url != null) {
                // we hit a link
                var endOffsetInclusive = (i+1 < attributeIndices.length? 
                                          attributeIndices[i+1]-1 : null);
    
                // check if this and the last found link are continuous
                if (mergeAdjacent && lastLink != null && lastLink.url == url && 
                      lastLink.endOffsetInclusive == startOffset - 1) {
                  // this and the previous style segment are continuous
                  lastLink.endOffsetInclusive = endOffsetInclusive;
                  return;
                }
    
                lastLink = {
                  "section": section,
                  "isFirstPageSection": isFirstPageSection,
                  "paragraph": par,
                  "textEl": el,
                  "startOffset": startOffset,
                  "endOffsetInclusive": endOffsetInclusive,
                  "url": url
                };
    
                links.push(lastLink);
              }        
            });
          }
        });
      });
    
    
      return links;
    }
    
    /**
     * Calls the given function for each section of the document (body, header, 
     * etc.). Sections are children of the DocumentElement object.
     *
     * @param {Document} doc The Document object (such as the one obtained via
     *     a call to DocumentApp.getActiveDocument()) with the sections to iterate
     *     over.
     * @param {Function} func A callback function which will be called, for each
     *     section, with the following arguments (in order):
     *       - {ContainerElement} section - the section element
     *       - {Number} sectionIndex - the child index of the section, such that
     *         doc.getBody().getParent().getChild(sectionIndex) == section.
     *       - {Boolean} isFirstPageSection - whether the section is a first-page
     *         header/footer section.
     */
    function iterateSections(doc, func) {
      // get the DocumentElement interface to iterate over all sections
      // this bit is undocumented API
      var docEl = doc.getBody().getParent();
    
      var regularHeaderSectionIndex = (doc.getHeader() == null? -1 : 
                                       docEl.getChildIndex(doc.getHeader()));
      var regularFooterSectionIndex = (doc.getFooter() == null? -1 : 
                                       docEl.getChildIndex(doc.getFooter()));
    
      for (var i=0; i<docEl.getNumChildren(); ++i) {
        var section = docEl.getChild(i);
    
        var sectionType = section.getType();
        var uniqueSectionName;
        var isFirstPageSection = (
          i != regularHeaderSectionIndex &&
          i != regularFooterSectionIndex && 
          (sectionType == DocumentApp.ElementType.HEADER_SECTION ||
           sectionType == DocumentApp.ElementType.FOOTER_SECTION));
    
        func(section, i, isFirstPageSection);
      }
    }
    

    【讨论】:

    • Will - 感谢您的关注和编辑 - 但是请注意,“text”属性是指指向 Text 元素,这在我的代码中很有用 - 仅返回文本内容在我的场景中还不够。我相应地重命名了该属性以更好地反映这一点。
    • 您的getAllLinks() 函数比@Mogsdad 中的函数工作得更好,如果您的脚本中的textEl 重命名为@,则可以用作他的getAllLinks() 函数的替代品987654338@ 匹配他的。也就是说,setLinkUrl() 似乎不再接受null 作为endOffsetInclusive 的值。它必须是整数,因此需要为此更新代码。
    • 是否可以在 Google Docs Python SDK 中执行相同的操作?
    【解决方案3】:

    我在玩耍并合并了@Mogsdad 的answer——这是真正复杂的版本:

    var _ = Underscorejs.load(); // loaded via http://googleappsdeveloper.blogspot.com/2012/11/using-open-source-libraries-in-apps.html, rolled my own
    var ui = DocumentApp.getUi();
    
    // #region --------------------- Utilities -----------------------------
    
    var gDocsHelper = (function(P, un) {
      // heavily based on answer https://stackoverflow.com/a/18731628/1037948
    
      var updatedLinkText = function(link, offset) {
        return function() { return 'Text: ' + link.getText().substring(offset,100) + ((link.getText().length-offset) > 100 ? '...' : ''); }
      }
    
      P.updateLink = function updateLink(link, oldText, newText, start, end) {
        var oldLink = link.getLinkUrl(start);
    
        if(0 > oldLink.indexOf(oldText)) return false;
    
        var newLink = oldLink.replace(new RegExp(oldText, 'g'), newText);
        link.setLinkUrl(start || 0, (end || oldLink.length), newLink);
        log(true, "Updating Link: ", oldLink, newLink, start, end, updatedLinkText(link, start) );
    
        return { old: oldLink, "new": newLink, getText: updatedLinkText(link, start) };
      };
    
      // moving this reused block out to 'private' fn
      var updateLinkResult = function(text, oldText, newText, link, urls, sidebar, updateResult) {
        // and may as well update the link while we're here
        if(false !== (updateResult = P.updateLink(text, oldText, newText, link.start, link.end))) {
           sidebar.append('<li>' + updateResult['old'] + ' &rarr; ' + updateResult['new'] + ' at ' + updateResult['getText']() + '</li>'); 
        }
    
        urls.push(link.url); // so multiple links get added to list
      };
    
      P.updateLinksMenu = function() {
        // https://developers.google.com/apps-script/reference/base/prompt-response
        var oldText = ui.prompt('Old link text to replace').getResponseText();
        var newText = ui.prompt('New link text to replace with').getResponseText();
    
        log('Replacing: ' + oldText + ', ' + newText);
        var sidebar = gDocUiHelper.createSidebar('Update All Links', '<h3>Replacing</h3><p><code>' + oldText + '</code> &rarr; <code>' + newText + '</code></p><hr /><ol>');
    
        // current doc available to script
        var doc = DocumentApp.getActiveDocument().getBody();//.getActiveSection();
    
        // Search until a link is found
        var links = P.findAllElementsFor(doc, function(text) {
          var i = -1, n = text.getText().length, link = false, url, urls = [], updateResult;
    
          // note: the following only gets the FIRST link in the text -- while(i < n && !(url = text.getLinkUrl(i++)));
    
          // scan the text element for links
          while(++i < n) {
    
            // getLinkUrl will continue to get a link while INSIDE the stupid link, so only do this once
            if(url = text.getLinkUrl(i)) {
              if(false === link) {
                link = { start: i, end: -1, url: url };
                // log(true, 'Type: ' + text.getType(), 'Link: ' + url, function() { return 'Text: ' + text.getText().substring(i,100) + ((n-i) > 100 ? '...' : '')});
              }
              else {
                link.end = i; // keep updating the end position until we leave
              }
            }
            // just left the link -- reset link tracking
            else if(false !== link) {
              // and may as well update the link while we're here
              updateLinkResult(text, oldText, newText, link, urls, sidebar);
              link = false; // reset "counter"
            }
    
          }
    
          // once we've reached the end of the text, must also check to see if the last thing we found was a link
          if(false !== link) updateLinkResult(text, oldText, newText, link, urls, sidebar);
    
          return urls;
        });
    
        sidebar.append('</ol><p><strong>' + links.length + ' links reviewed</strong></p>');
        gDocUiHelper.attachSidebar(sidebar);
    
        log(links);
      };
    
      P.findAllElementsFor = function(el, test) {
        // generic utility function to recursively find all elements; heavily based on https://stackoverflow.com/a/18731628/1037948
    
        var results = [], searchResult = null, i, result;
        // https://developers.google.com/apps-script/reference/document/body#findElement(ElementType)
        while (searchResult = el.findElement(DocumentApp.ElementType.TEXT, searchResult)) {
          var t = searchResult.getElement().editAsText(); // .asParagraph()
    
          // check to add to list
          if(test && (result = test(t))) {
            if( _.isArray(result) ) results = results.concat(result); // could be big? http://jsperf.com/self-concatenation/
            else results.push(result);
          }
        }
        // recurse children if not plain text item
        if(el.getType() !== DocumentApp.ElementType.TEXT) {
          i = el.getNumChildren();
    
          var result;
          while(--i > 0) {
            result = P.findAllElementsFor(el.getChild(i));
            if(result && result.length > 0) results = results.concat(result);
          }
        }
    
        return results;
      };
    
      return P;  
    })({});
    
    // really? it can't handle object properties?
    function gDocsUpdateLinksMenu() {
      gDocsHelper.updateLinksMenu();
    }
    
    gDocUiHelper.addMenu('Zaus', [ ['Update links', 'gDocsUpdateLinksMenu'] ]);
    
    // #endregion --------------------- Utilities -----------------------------
    

    为了完整起见,我在下面包含了用于创建菜单、侧边栏等的“额外”实用程序类:

    var log = function() {
      // return false;
    
      var args = Array.prototype.slice.call(arguments);
    
      // allowing functions delegates execution so we can save some non-debug cycles if code left in?
    
      if(args[0] === true) Logger.log(_.map(args, function(v) { return _.isFunction(v) ? v() : v; }).join('; '));
      else
        _.each(args, function(v) {
          Logger.log(_.isFunction(v) ? v() : v);
        });
    }
    
    // #region --------------------- Menu -----------------------------
    
    var gDocUiHelper = (function(P, un) {
    
      P.addMenuToSheet = function addMenu(spreadsheet, title, items) {
        var menu = ui.createMenu(title);
        // make sure menu items are correct format
        _.each(items, function(v,k) {
          var err = [];
    
          // provided in format [ [name, fn],... ] instead
          if( _.isArray(v) ) {
            if ( v.length === 2 ) {
              menu.addItem(v[0], v[1]);
            }
            else {
              err.push('Menu item ' + k + ' missing name or function: ' + v.join(';'))
            }
          }
          else {
            if( !v.name ) err.push('Menu item ' + k + ' lacks name');
            if( !v.functionName ) err.push('Menu item ' + k + ' lacks function');
    
            if(!err.length) menu.addItem(v.name, v.functionName);
          }
    
          if(err.length) {
            log(err);
            ui.alert(err.join('; '));
          }
    
        });
    
        menu.addToUi();
      };
    
      // list of things to hook into
      var initializers = {};
    
      P.addMenu = function(menuTitle, menuItems) {
        if(initializers[menuTitle] === un) {
          initializers[menuTitle] = [];
        }
        initializers[menuTitle] = initializers[menuTitle].concat(menuItems);
      };
    
      P.createSidebar = function(title, content, options) {
        var sidebar = HtmlService
        .createHtmlOutput()
        .setTitle(title)
        .setWidth( (options && options.width) ? width : 350 /* pixels */);
    
        sidebar.append(content);
    
        if(options && options.on) DocumentApp.getUi().showSidebar(sidebar);
        // else { sidebar.attach = function() { DocumentApp.getUi().showSidebar(this); }; } // should really attach to prototype...
    
        return sidebar;
      };
    
      P.attachSidebar = function(sidebar) {
        DocumentApp.getUi().showSidebar(sidebar);
      };
    
    
      P.onOpen = function() {
        var spreadsheet = SpreadsheetApp.getActive();
        log(initializers);
        _.each(initializers, function(v,k) {
          P.addMenuToSheet(spreadsheet, k, v);
        });
      };
    
      return P;
    })({});
    
    // #endregion --------------------- Menu -----------------------------
    
    /**
     * A special function that runs when the spreadsheet is open, used to add a
     * custom menu to the spreadsheet.
     */
    function onOpen() {
      gDocUiHelper.onOpen();
    }
    

    【讨论】:

      【解决方案4】:

      在让 Mogsdad 的解决方案发挥作用时遇到了一些麻烦。具体来说,它错过了结束其父元素的链接,因此没有尾随的非链接字符来终止它。我已经实现了一些解决这个问题并返回标准范围元素的东西。在这里分享,以防有人觉得有用。

      function getAllLinks(element) {
        var rangeBuilder = DocumentApp.getActiveDocument().newRange();
      
        // Parse the text iteratively to find the start and end indices for each link
        if (element.getType() === DocumentApp.ElementType.TEXT) {
          var links = [];
          var string = element.getText();
          var previousUrl = null; // The URL of the previous character 
          var currentLink = null; // The latest link being built
          for (var charIndex = 0; charIndex < string.length; charIndex++) {
            var currentUrl = element.getLinkUrl(charIndex);
            // New URL means create a new link
            if (currentUrl !== null && previousUrl !== currentUrl) {
              if (currentLink !== null) links.push(currentLink);
              currentLink = {};
              currentLink.url = String(currentUrl);
              currentLink.startOffset = charIndex;
            }
            // In a URL means extend the end of the current link
            if (currentUrl !== null) {
              currentLink.endOffsetInclusive = charIndex;
            }
            // Not in a URL means close and push the link if ready
            if (currentUrl === null) {
              if (currentLink !== null) links.push(currentLink);
              currentLink = null;
            }
            // End the loop and go again
            previousUrl = currentUrl;
          }
          // Handle the end case when final character is a link
          if (currentLink !== null) links.push(currentLink);
          // Convert the links into a range before returning
          links.forEach(function(link) {
            rangeBuilder.addElement(element, link.startOffset, link.endOffsetInclusive);
          });
        }
      
        // If not a text element then recursively get links from child elements
        else if (element.getNumChildren) {
          for (var i = 0; i < element.getNumChildren(); i++) {
            rangeBuilder.addRange(getAllLinks(element.getChild(i)));
          }
        }
      
        return rangeBuilder.build();
      }
      

      【讨论】:

        【解决方案5】:

        此 Excel 宏列出了 Word 文档中的链接。您需要先将数据复制到 Word 文档中。

        Sub getLinks()
        Dim wApp As Word.Application, wDoc As Word.Document
        Dim i As Integer, r As Range
        Const filePath = "C:\test\test.docx"
        Set wApp = CreateObject("Word.Application")
        'wApp.Visible = True
        Set wDoc = wApp.Documents.Open(filePath)
        Set r = Range("A1")
        For i = 1 To wDoc.Hyperlinks.Count
            r = wDoc.Hyperlinks(i).Address
            Set r = r.Offset(1, 0)
        Next i
        wApp.Quit
        Set wDoc = Nothing
        Set wApp = Nothing
        End Sub
        

        【讨论】:

        • Microsoft word 和 VBA 与 Google Docs 和 JavaScript 衍生 Google Apps 脚本有何关系?
        • 在 Goggle Docs 中输入/输出内容很容易,而且我的方法似乎需要更少的代码。问题标题表明目标是“获取文档中的所有链接”。但是,如果目标是使用某个工具,那么您是对的,它是不相关的。
        • @TonyM ,好的,但问题是字面意思,“您如何使用 Google Apps 脚本编译存在的链接列表?”
        • 我同意你和 tehhowch 都有优点。我发布答案的原因是(1)我的代码回答了标题中指定的目标,(2)与此处的其他解决方案相比,它非常简单。这可能对那些不依赖于特定工具的人很有吸引力。
        【解决方案6】:

        你是对的......搜索和替换在这里不适用。 使用 setLinkUrl() https://developers.google.com/apps-script/reference/document/container-element#setLinkUrl(String)

        基本上你必须递归地遍历元素(元素可以包含元素)并且对于每个 使用 getLinkUrl() 获取 oldText 如果不是 null , setLinkUrl(newText) .... 保持显示的文本不变

        【讨论】:

        • 是的,我认为会是这样;那时我的问题是弄清楚如何递归元素
        【解决方案7】:

        这是一种无需脚本即可实现相同目标的快速而肮脏的方法:

        1. 从 Google 文档中,将文档保存为 RTF 格式。
        2. 在您选择的编辑器中,编辑 RTF 文件中的链接(在我的例子中,我想修改所有超链接,所以我使用了 Emacs 和 regexp-replace)。完成后保存文件。
        3. 创建一个全新的 Google Doc,然后从菜单中选择 File>Open 并打开 RTF 文件。 Docs 会将您编辑的 RTF 文件转换回正确的 Google Doc,从而恢复所有格式。

        Google Docs 的 RTF 格式非常完整——我没有注意到在往返过程中有任何保真度损失,它的优点是可以在一个易于编辑和应用正则表达式工具的表单。

        【讨论】:

          猜你喜欢
          • 2013-10-02
          • 2022-09-28
          • 2012-05-25
          • 2021-06-06
          • 1970-01-01
          • 1970-01-01
          • 2012-11-06
          • 2021-11-26
          • 2023-03-28
          相关资源
          最近更新 更多