とある業務でGoogleドキュメント内にあるリンクを取得したかったのですが、これが思いのほか大変でした。GASのドキュメントを読むとRichLinkというElementがあったので、これをfindElementで探してやれば一発かなと安易に考えていたのですが、次の画像のように
2つ目のいわゆる一般的なリンクは、RichLinkではなく、別のやり方が必要でした。そして、このやり方がなかなか良い方法がなくて苦労したので、ここに記録として残しておきたいと思います。
やりたいこと
上記の画像の例で言えば、次のようなデータを取得することがゴールです。RichLinkはGASのAPIを普通に使えば取得できるのでここでは省略します。
[{
anchorText: "普通のリンク",
url: "https://xxx.com/"
}]
コードの全容
先にコードの全容を紹介して、そこからポイントごとに説明をしていきます。claspを使ってTypeScriptでGASの開発をしていますが、GAS特有のTypeの型ガードを良い感じにコントロールする方法がよくわからず、安易にts-ignoreしてます...。ご容赦ください。
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;
};
コードのポイントごとの説明
リンクをテキスト一文字、一文字ずつ探していく
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を返す仕様になっており、あまり実用的ではありませんでした。
アンカーテキストの文字を取得する
// リングがある
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を探索する
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を使ってすべて取得する方法でした。これを特定のフォルダの配下のドキュメントすべてに実行すれば、そこにあるリンクが簡単に取得できて良い感じになります。何かの業務のお役に立てれば幸いです。
- 参考文献
-
Get All Links in a Document - Stack Overflow
※ コードの多くはこちらを参考にさせて頂きました。ありがとうございました。
-
Get All Links in a Document - Stack Overflow