0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Googleドキュメント内にあるリンクをGASを使ってすべて取得する方法

Last updated at Posted at 2022-07-17

とある業務でGoogleドキュメント内にあるリンクを取得したかったのですが、これが思いのほか大変でした。GASのドキュメントを読むとRichLinkというElementがあったので、これをfindElementで探してやれば一発かなと安易に考えていたのですが、次の画像のように
スクリーンショット 2022-07-17 16.46.17.png
2つ目のいわゆる一般的なリンクは、RichLinkではなく、別のやり方が必要でした。そして、このやり方がなかなか良い方法がなくて苦労したので、ここに記録として残しておきたいと思います。

やりたいこと

上記の画像の例で言えば、次のようなデータを取得することがゴールです。RichLinkはGASのAPIを普通に使えば取得できるのでここでは省略します。

[{
  anchorText: "普通のリンク",
  url: "https://xxx.com/"
}]

コードの全容

先にコードの全容を紹介して、そこからポイントごとに説明をしていきます。claspを使ってTypeScriptでGASの開発をしていますが、GAS特有のTypeの型ガードを良い感じにコントロールする方法がよくわからず、安易にts-ignoreしてます...。ご容赦ください。

sample.ts
interface referToDocInfoProp {
  anchorText: string;
  url: string;
  startOffset: number;
}

const getAllLinks = (element: Docs.Text | Docs.Body): referToDocInfoProp[] => {
  let links: referToDocInfoProp[] = [];

  if (element.getType() === DocumentApp.ElementType.TEXT) {
    const textObj = element.editAsText();
    const text = element.getText();
    let inUrl = false;

    for (let ch = 0; ch < text.length; ch++) {
      let curUrl: referToDocInfoProp = {
        anchorText: '',
        url: '',
        startOffset: null,
      };
      const url = textObj.getLinkUrl(ch);
      // リングがある
      if (url !== null) {
        if (!inUrl) {
          inUrl = true;
          curUrl.url = String(url);
          curUrl.anchorText = text.slice(ch); // textはelement単位なので、まずはアンカーテキストの冒頭から抜粋
          curUrl.startOffset = ch; // 後でアンカーテキストの後ろ部分を調整する為に使う
        }
      } else {
        // リンクがない、= アンカーテキストの最後の次の文字
        if (inUrl) {
          // アンカーテキストの後ろの部分を調整する
          links[links.length - 1].anchorText = links[links.length - 1].anchorText.substring(
            0,
            ch - links[links.length - 1].startOffset
          );
          inUrl = false;
          curUrl = { anchorText: '', url: '', startOffset: null };
        }
      }
      if (inUrl) {
        // linksへpush
        if (curUrl.url !== '') {
          links.push(curUrl);
        }
      }
    }
  } else {
    // @ts-ignore
    const numChildren = element.getNumChildren();
    for (let i = 0; i < numChildren; i++) {
      // @ts-ignore
      const childElement = element.getChild(i);
      const childElementType = childElement.getType();
      // childを持てないElementはエラーになるのでここで除外する
      if (
        childElementType !== DocumentApp.ElementType.RichText &&
        childElementType !== DocumentApp.ElementType.PAGE_BREAK &&
        childElementType !== DocumentApp.ElementType.UNSUPPORTED
      ) {
        links = links.concat(getAllLinks(childElement));
      }
    }
  }
  return links;
};

コードのポイントごとの説明

リンクをテキスト一文字、一文字ずつ探していく

sample.ts
 if (element.getType() === DocumentApp.ElementType.TEXT) {
    const textObj = element.editAsText();
    const text = element.getText();
    let inUrl = false;

    for (let ch = 0; ch < text.length; ch++) {
      let curUrl: referToDocInfoProp = {
        anchorText: '',
        url: '',
        startOffset: null,
      };
      const url = textObj.getLinkUrl(ch);

Googleドキュメント内のテキストを1文字ずつに対してgetLinkUrlを実行してリンクを探していきます。本当はパラグラフ単位で探すことができれば良いのですが、ParagraphのClassが持つgetLinkUrlメソッドは、複数のリンクを持つ場合にnullを返す仕様になっており、あまり実用的ではありませんでした。

アンカーテキストの文字を取得する

sample.ts
      // リングがある
      if (url !== null) {
        if (!inUrl) {
          inUrl = true;
          curUrl.url = String(url);
          curUrl.anchorText = text.slice(ch); // textはelement単位なので、まずはアンカーテキストの冒頭から抜粋
          curUrl.startOffset = ch; // 後でアンカーテキストの後ろ部分を調整する為に使う
        }
      } else {
        // リンクがない、= アンカーテキストの最後の次の文字
        if (inUrl) {
          // アンカーテキストの後ろの部分を調整する
          links[links.length - 1].anchorText = links[links.length - 1].anchorText.substring(
            0,
            ch - links[links.length - 1].startOffset
          );
          inUrl = false;
          curUrl = { anchorText: '', url: '', startOffset: null };
        }
      }

getLinkUrlの返り値はそのURLだけです。また、例えば"普通のリンク"というアンカーテキストがあった場合に「普」と「通」のいずれも文字で実行しても結果は変わりません。アンカーテキストを取得することは要件の一つのため、上手く文字列を操作して取得していく必要があります。

コード内のコメントでも説明していますが、Element単位のテキストに対して、先頭から順番にgetLinkUrlをしていき、一番初めにURLが取得できた地点でそれまでのテキストをsliceしておきます。その後もgetLinkUrlを続けていき、取得ができなくなった地点をアンカーテキストの終了地点として保存しておいたアンカーテキストの後ろの文字をカットします。

ドキュメントの全てのElementを探索する

sample.ts
    const numChildren = element.getNumChildren();
    for (let i = 0; i < numChildren; i++) {
      // @ts-ignore
      const childElement = element.getChild(i);
      const childElementType = childElement.getType();
      // childを持てないElementはエラーになるのでここで除外する
      if (
        childElementType !== DocumentApp.ElementType.RichText &&
        childElementType !== DocumentApp.ElementType.PAGE_BREAK &&
        childElementType !== DocumentApp.ElementType.UNSUPPORTED
      ) {
        links = links.concat(getAllLinks(childElement));
      }
    }

この関数を呼び出すときは最初にBodyを渡すのが前提となっています。BodyはParagraph等の複数のElementを持っており、その配下にTEXTのElementがあるという構造です。よって、Bodyのchildを引数として再帰的にこの関数を呼び出していきます。

ここの注意点としては、Bodyの中にgetChildメソッドを持たないElementがある場合にエラーになってしまうことです。完全に排除するには該当する全てのElementを条件に加えることですが、実際に使用されているElementが排除できていれば十分だと思います。

終わり

以上、Googleドキュメント内にあるリンクをGASを使ってすべて取得する方法でした。これを特定のフォルダの配下のドキュメントすべてに実行すれば、そこにあるリンクが簡単に取得できて良い感じになります。何かの業務のお役に立てれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?