├── LICENSE ├── README.md ├── [Worker] GDN Placement Cleaner ├── account.gs ├── common.gs ├── config.gs ├── main.gs └── readme.md ├── [Worker] GPT Labeler ├── account.gs ├── common.gs ├── config.gs └── main.gs ├── _lib ├── README.md ├── account.gs ├── common.gs ├── config.gs └── index.gs ├── dev ├── Eval.js ├── TelegramAPI_sample.js └── [Search]_SKAG_Campaign.js ├── n-gram ├── README.md ├── account.gs ├── calc.gs ├── common.gs ├── config.gs ├── index.gs └── negatives.gs ├── release ├── Budget_Control.js ├── GDN_Excluded_Placement.js ├── GDN_Excluded_Placement_ByName.js ├── Search_AdGroup_CrossKeys.js ├── Search_Ads_Optimizer.js ├── Search_Bidder.js ├── Search_Google_Keywords_Mining.js ├── Video_Auto_Placement_Builder.js ├── Video_Youtube_parser.js └── ahrefs │ └── GDN_DR_parser.js └── search_keyword_builder ├── Code.gs ├── account.gs ├── buildNewKeywords.gs ├── common.gs ├── config.gs └── readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Ads Scripts 2 | 3 | + `[Worker] GDN Placement Cleaner` - Excludes placements with bad CPA 4 | + `[Worker] GPT Labeler` - Keyword relevance analysis using GPT-4 5 | 6 | `/_lib` 7 | + Basic Google Ads script template 8 | 9 | `/dev/` 10 | + `Eval.js` - Исполняет код внешней библиотеки 11 | + `TelegramAPI_sample.js` - Шлёт сообщения в Telegram 12 | 13 | `/release/` 14 | 15 | + `Budget_Control.js` - Контролирует точный расход бюджета для борьбы с https://support.google.com/adwords/answer/1704443 16 | + `GDN_Excluded_Placement.js` - Исключает площадки с плохими финасовыми показателями 17 | + `GDN_Excluded_Placement_ByName.js` - Исключает площадки по вхождению строки 18 | + `Search_AdGroup_CrossKeys.js` - Кросс-минусовка между группами объявлений 19 | + `Search_Ads_Optimizer.js` - Тестирует объявления, выбирая то у кого ниже Cost Per Conversion 20 | + `Search_Bidder.js` - Контролирует позицию ключевых слов 21 | + `Search_Google_Keywords_Mining.js` - Создаёт новые ключевые слова на основании сервиса подсказок Google 22 | + `Video_Youtube_parser.js` - Ищет места размещения на YouTube с помощью поиска по ключевым словам 23 | 24 | `/search_keyword_builder` 25 | + Создает новые ключевые слова на основании отчета о поисковых запросах 26 | 27 | `/n-gram` 28 | + Создаёт детальный отчет по поисковым запросам 29 | -------------------------------------------------------------------------------- /[Worker] GDN Placement Cleaner/account.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves statistics on campaigns and identifies bad placements based on cost per acquisition (CPA) threshold. 3 | * Creates an excluded placement list for each campaign with bad placements. 4 | */ 5 | function account() { 6 | const campaigns_stats = getCampagnsStats(); 7 | for (let campaign_id in campaigns_stats) { 8 | const bad_placements = getBadPlacements( 9 | campaign_id, 10 | campaigns_stats[campaign_id] 11 | ); 12 | if (bad_placements.length !== 0) { 13 | const list_name = `${config().blacklistName} - cid ${campaign_id}`; 14 | makeAndPopulateExcludedPlacementList( 15 | list_name, 16 | bad_placements, 17 | campaign_id 18 | ); 19 | } 20 | } 21 | } 22 | 23 | /** 24 | * Creates or updates an excluded placement list with bad placements for a specific campaign. 25 | * @param {string} list_name - Name of the excluded placement list to create or update. 26 | * @param {Array.} bad_placements - An array of bad placements to add to the excluded placement list. 27 | * @param {string} campaign_id - ID of the campaign to add the excluded placement list to. 28 | */ 29 | function makeAndPopulateExcludedPlacementList(list_name, bad_placements, campaign_id) { 30 | const excludedPlacementListIterator = AdsApp.excludedPlacementLists() 31 | .withCondition(`shared_set.name = '${list_name}'`) 32 | .get(); 33 | const campaign = AdsApp.campaigns().withIds([campaign_id]).get().next(); 34 | 35 | let excludedPlacementList; 36 | 37 | if (excludedPlacementListIterator.hasNext()) { 38 | // If the list exists, use it 39 | excludedPlacementList = excludedPlacementListIterator.next(); 40 | } else { 41 | // If the list does not exist, create it 42 | excludedPlacementList = AdsApp.newExcludedPlacementListBuilder() 43 | .withName(list_name) 44 | .build() 45 | .getResult(); 46 | } 47 | 48 | excludedPlacementList.addExcludedPlacements(bad_placements); 49 | campaign.addExcludedPlacementList(excludedPlacementList); 50 | } 51 | 52 | /** 53 | * Identifies bad placements based on CPA threshold. 54 | * @param {string} campaign_id - Campaign ID to search for bad placements. 55 | * @param {number} avg_cpa - Average CPA of the campaign. 56 | * @return {Array.} An array of bad placements. 57 | */ 58 | function getBadPlacements(campaign_id, avg_cpa) { 59 | const arr = []; 60 | const threshold = parseInt(Number(avg_cpa) * 1000000 * 5); 61 | const startDate = daysToGaqlDate( 62 | config().customDaysInDateRange + config().customDateRangeShift 63 | ); 64 | const endDate = daysToGaqlDate(config().customDateRangeShift); 65 | 66 | const query = `SELECT 67 | detail_placement_view.target_url, 68 | detail_placement_view.resource_name, 69 | detail_placement_view.placement_type, 70 | detail_placement_view.placement, 71 | detail_placement_view.group_placement_target_url, 72 | metrics.clicks, 73 | metrics.conversions, 74 | metrics.cost_micros 75 | FROM 76 | detail_placement_view 77 | WHERE 78 | segments.date BETWEEN "${startDate}" AND "${endDate}" 79 | AND metrics.cost_micros > ${threshold} 80 | AND campaign.id = ${campaign_id}`; 81 | const result = AdsApp.search(query, { 82 | apiVersion: "v13", 83 | }); 84 | while (result.hasNext()) { 85 | const row = result.next(); 86 | if (row) { 87 | try { 88 | let to_bad_list = false; 89 | const placement = row.detailPlacementView.groupPlacementTargetUrl; 90 | const conversions = parseFloat(row.metrics.conversions).toFixed(2); 91 | const cost_micros = parseFloat(row.metrics.costMicros).toFixed(2); 92 | const cost = cost_micros / 1000000; 93 | let placement_cpa = Number(cost); 94 | if (conversions !== 0) { 95 | placement_cpa = (Number(cost) / Number(conversions)).toFixed(2); 96 | if (+cost > +avg_cpa * +config().badRate) { 97 | if (+placement_cpa > +avg_cpa * +config().badRate) { 98 | to_bad_list = true; 99 | } 100 | } 101 | } else { 102 | if (cost > avg_cpa * +config().badRate) { 103 | to_bad_list = true; 104 | } 105 | } 106 | if (to_bad_list) { 107 | arr.push(placement); 108 | Logger.log(`placement - ${placement} > placement_cpa - ${placement_cpa}`); 109 | } 110 | } catch (e) { 111 | Logger.log(e); 112 | // Add more error handling here if necessary 113 | } 114 | } 115 | } 116 | return arr; 117 | } 118 | 119 | /** 120 | * Retrieves statistics on campaigns. 121 | * @return {Object} An object with campaign ID as keys and average CPA as values. 122 | */ 123 | function getCampagnsStats() { 124 | const report = {}; 125 | const startDate = daysToGaqlDate( 126 | config().customDaysInDateRange + config().customDateRangeShift 127 | ); 128 | const endDate = daysToGaqlDate(config().customDateRangeShift); 129 | 130 | const query = `SELECT 131 | campaign.id, 132 | campaign.name, 133 | campaign.serving_status, 134 | campaign.status, 135 | metrics.clicks, 136 | metrics.conversions, 137 | metrics.cost_micros, 138 | metrics.conversions_value 139 | FROM 140 | campaign 141 | WHERE 142 | metrics.conversions > 30 143 | AND campaign.serving_status = 'SERVING' 144 | AND campaign.status = 'ENABLED' 145 | AND campaign.advertising_channel_type = 'DISPLAY' 146 | AND segments.date BETWEEN "${startDate}" AND "${endDate}" 147 | ORDER BY 148 | metrics.cost_micros DESC`; 149 | 150 | const result = AdsApp.search(query, { 151 | apiVersion: "v13", 152 | }); 153 | 154 | while (result.hasNext()) { 155 | let row = result.next(); 156 | if (row) { 157 | try { 158 | const campaign_id = row.campaign.id; 159 | const campaign_name = row.campaign.name; 160 | const conversions = parseFloat(row.metrics.conversions).toFixed(2); 161 | const cost_micros = parseFloat(row.metrics.costMicros).toFixed(2); 162 | const cost = cost_micros / 1000000; 163 | const avg_cpa = (Number(cost) / Number(conversions)).toFixed(2); 164 | 165 | Logger.log( 166 | `campaign_name - ${campaign_name} > avg_cpa - ${avg_cpa}` 167 | ); 168 | 169 | report[campaign_id] = avg_cpa; 170 | } catch (e) { 171 | Logger.log(e); 172 | } 173 | } 174 | } 175 | 176 | return report; 177 | } -------------------------------------------------------------------------------- /[Worker] GDN Placement Cleaner/common.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a number of days to a GAQL-formatted date string. 3 | * @param {number} days - The number of days to subtract from the current date. 4 | * @returns {string} - A string in the format "yyyy-MM-dd", representing the number of days ago from the current date. 5 | */ 6 | function daysToGaqlDate(days) { 7 | const now = new Date(); 8 | const targetDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000); 9 | return Utilities.formatDate(targetDate, "UTC", "yyyy-MM-dd"); 10 | } -------------------------------------------------------------------------------- /[Worker] GDN Placement Cleaner/config.gs: -------------------------------------------------------------------------------- 1 | function config() { 2 | return { 3 | // Prefix of the list where we put bad CPA placements 4 | blacklistName: "BadCPA", 5 | 6 | // Specify the number of days to select 7 | // If we want to use conversion or profitability data, then we should specify a value 8 | // greater than the conversion window. 9 | customDaysInDateRange: 180, 10 | 11 | // Specify how many days we shift the selection from today. 12 | // It is needed so as not to take those days when statistics are delayed. 13 | // If we want to use conversion or profitability data, then we should specify a value equal to 14 | // the days in the conversion window. 15 | customDateRangeShift: 0, 16 | 17 | // The threshold ratio of CPA to the campaign average, after which the placement starts to be considered "bad" 18 | badRate: 2, 19 | }; 20 | } -------------------------------------------------------------------------------- /[Worker] GDN Placement Cleaner/main.gs: -------------------------------------------------------------------------------- 1 | // Copyright 2023, https://github.com/pamnard 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* ======================================================================================== 16 | 17 | Before start use the script, you need to configure it in the file\functions - config 18 | 19 | ======================================================================================== */ 20 | 21 | /** 22 | * Retrieves all child accounts of the MCC account with at least 30 conversions in the last 30 days 23 | * and proceeds them in parallel using the `proceedAccounts()` function. 24 | */ 25 | function main() { 26 | const account_to_proceed = []; 27 | const accountSelector = AdsManagerApp.accounts(); 28 | const accountIterator = accountSelector.get(); 29 | while (accountIterator.hasNext()) { 30 | const account = accountIterator.next(); 31 | const stats = account.getStatsFor("LAST_30_DAYS"); 32 | if (stats.getConversions() > 30) { 33 | account_to_proceed.push(account.getCustomerId()); 34 | } 35 | if (account_to_proceed.length >= 50) { 36 | proceedAccounts(account_to_proceed); 37 | account_to_proceed.length = 0; 38 | } 39 | } 40 | if (account_to_proceed.length !== 0) { 41 | proceedAccounts(account_to_proceed); 42 | } 43 | 44 | function proceedAccounts(ids_array) { 45 | const selector = AdsManagerApp.accounts().withIds(ids_array); 46 | selector.executeInParallel("account"); 47 | } 48 | } 49 | 50 | function all_finished() { 51 | Logger.log("All done!"); 52 | } 53 | -------------------------------------------------------------------------------- /[Worker] GDN Placement Cleaner/readme.md: -------------------------------------------------------------------------------- 1 | # [Worker] GDN Placement 2 | 3 | Checks for exceeding CPA placements for campaigns and excludes placements with CPA N-times worse than average. Works specifically at the campaign level so that the comparison is based on the conversion event individually set for a particular campaign, and not on the account average. 4 | -------------------------------------------------------------------------------- /[Worker] GPT Labeler/account.gs: -------------------------------------------------------------------------------- 1 | function account() { 2 | 3 | Logger.log(`${get_account_name()} - Start`); 4 | 5 | ensureAccountLabels(); 6 | 7 | let labels_arr = []; 8 | let labelSelector = AdsApp.labels() 9 | .withCondition("label.name LIKE 'GPT_%'"); 10 | let labelIterator = labelSelector.get(); 11 | while (labelIterator.hasNext()) { 12 | let label = labelIterator.next(); 13 | labels_arr.push(`'${label.getResourceName()}'`); 14 | } 15 | let labels_str = labels_arr.join(','); 16 | Logger.log(labels_str); 17 | var campaignSelector = AdsApp 18 | .campaigns() 19 | .withCondition("campaign.name LIKE '%Generic%'"); 20 | let campaignIterator = campaignSelector.get(); 21 | while (campaignIterator.hasNext()) { 22 | let campaign = campaignIterator.next(); 23 | let keywordSelector = campaign 24 | .keywords() 25 | .withCondition(`ad_group_criterion.labels CONTAINS NONE (${labels_str})`) 26 | .withCondition("ad_group_criterion.status != REMOVED") 27 | .withLimit(50) 28 | .orderBy("metrics.cost_micros DESC"); 29 | let keywordIterator = keywordSelector.get(); 30 | while (keywordIterator.hasNext()) { 31 | let keyword = keywordIterator.next(); 32 | let text = keyword.getText(); 33 | let prompt = create_prompt(text); 34 | let response = gpt4(prompt); 35 | if (response.indexOf('Yes') > -1) { 36 | keyword.applyLabel('GPT_YES') 37 | } 38 | if (response.indexOf('No') > -1) { 39 | keyword.applyLabel('GPT_NO') 40 | } 41 | Logger.log(`${text} => ${response}`); 42 | } 43 | } 44 | 45 | Logger.log(`${get_account_name()} - Finish`); 46 | } 47 | 48 | function create_prompt(key) { 49 | let str = `My company is a %%%online store%%%. 50 | We advertise in Google Ads to attract people interested in %%%buy shirts%%%. 51 | Do you think the "${key}" is a good target keyword for us? 52 | Answer only “Yes” or “No”.`; 53 | return str; 54 | } 55 | /** 56 | * This function sends a prompt to the OpenAI GPT-3 API and returns a response. 57 | * 58 | * @param {string} prompt - The prompt to send to the GPT-3 API. 59 | * @param {string} username - The username of the user sending the prompt. 60 | * @returns {string} The response from the GPT-3 API. 61 | */ 62 | function gpt4(prompt) { 63 | 64 | let messages = [{ 65 | "role": "user", 66 | "content": prompt 67 | }]; 68 | 69 | // Call the OpenAI GPT-3 API with the messages array to get a response. 70 | let response = callAPI(messages); 71 | 72 | // Return the GPT-3 response. 73 | return response; 74 | } 75 | 76 | /** 77 | * Call OpenAI API to get a response to a given message 78 | * 79 | * @param {string[]} messages - An array of messages to send to the API 80 | * @returns {string} - The response from the API 81 | */ 82 | function callAPI(messages) { 83 | Utilities.sleep(2000); 84 | // Create data object to send to API 85 | let data = { 86 | 'model': 'gpt-4', 87 | 'messages': messages, 88 | }; 89 | 90 | // Set options for UrlFetchApp 91 | let options = { 92 | 'method': 'post', 93 | 'contentType': 'application/json', 94 | 'payload': JSON.stringify(data), 95 | 'headers': { 96 | Authorization: 'Bearer ' + config().openai_apikey, 97 | }, 98 | muteHttpExceptions: true 99 | }; 100 | 101 | // Fetch response from API using UrlFetchApp 102 | let response = UrlFetchApp.fetch( 103 | 'https://api.openai.com/v1/chat/completions', 104 | options, 105 | ); 106 | 107 | // Log the response content 108 | // Logger.log(response.getContentText()); 109 | 110 | // Check if there is an error in the response 111 | if (JSON.parse(response.getContentText()).error?.message != undefined) { 112 | // Return the error message 113 | return JSON.parse(response.getContentText())['error']['message']; 114 | } else { 115 | // Return the response from the API 116 | return JSON.parse(response.getContentText())['choices'][0]['message']['content'].replace(/^\n\n/, ''); 117 | } 118 | } -------------------------------------------------------------------------------- /[Worker] GPT Labeler/common.gs: -------------------------------------------------------------------------------- 1 | function unique(arr) { 2 | var tmp = {}; 3 | return arr.filter(function (a) { 4 | return a in tmp ? 0 : tmp[a] = 1; 5 | }); 6 | } 7 | 8 | function handleErrors(errors) { 9 | Logger.log(errors); 10 | } 11 | 12 | function get_account_name() { 13 | return AdsApp.currentAccount().getName(); 14 | } 15 | 16 | function days_ago(days) { 17 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24, 18 | now = new Date(), 19 | fromDate = new Date(now.getTime() - days * MILLIS_PER_DAY), 20 | timeZone = AdsApp.currentAccount().getTimeZone(), 21 | formatDate = Utilities.formatDate(fromDate, timeZone, 'yyyy-MM-dd'); 22 | return formatDate; 23 | } 24 | 25 | function ensureAccountLabels() { 26 | function getAccountLabelNames() { 27 | var labelNames = []; 28 | var iterator = AdWordsApp.labels().get(); 29 | while (iterator.hasNext()) { 30 | labelNames.push(iterator.next().getName()); 31 | } 32 | return labelNames; 33 | } 34 | var labelNames = getAccountLabelNames(); 35 | if (labelNames.indexOf('GPT_YES') == -1) { 36 | AdWordsApp.createLabel('GPT_YES'); 37 | } 38 | if (labelNames.indexOf('GPT_NO') == -1) { 39 | AdWordsApp.createLabel('GPT_NO'); 40 | } 41 | } -------------------------------------------------------------------------------- /[Worker] GPT Labeler/config.gs: -------------------------------------------------------------------------------- 1 | function config() { 2 | return { 3 | openai_apikey: "000000000000000000000000000000000000000000000", 4 | } 5 | } -------------------------------------------------------------------------------- /[Worker] GPT Labeler/main.gs: -------------------------------------------------------------------------------- 1 | /* ======================================================================================== 2 | 3 | Before start use the script, you need to configure it in the file\functions - config 4 | 5 | ======================================================================================== */ 6 | 7 | function main() { 8 | var account_ids = [], 9 | accountSelector = AdsManagerApp.accounts(), 10 | accountIterator = accountSelector.get(); 11 | while (accountIterator.hasNext()) { 12 | var account = accountIterator.next(), 13 | account_name = account.getName().toLowerCase(), 14 | cost = account.getStatsFor('LAST_7_DAYS').getCost(); 15 | if ((cost > +0) && 16 | // (account_name.indexOf('indonesia') > -1) && 17 | (account_name.indexOf('web') > -1)) { 18 | if (account_ids.length < 50) { 19 | account_ids.push(account.getCustomerId()); 20 | } else { 21 | execute_ids(account_ids); 22 | Utilities.sleep(1000); 23 | account_ids = []; 24 | } 25 | } 26 | } 27 | 28 | execute_ids(account_ids); 29 | 30 | function execute_ids(ids) { 31 | var accountSelector = AdsManagerApp.accounts() 32 | .withIds(ids) 33 | .withLimit(50); 34 | accountSelector.executeInParallel('account', 'all_finished'); 35 | } 36 | } 37 | 38 | function all_finished() { 39 | Logger.log('All done!'); 40 | } -------------------------------------------------------------------------------- /_lib/README.md: -------------------------------------------------------------------------------- 1 | # Basic Google Ads script template 2 | 3 | Helps to build the architecture of the script, and also contains a small set of useful functions. 4 | -------------------------------------------------------------------------------- /_lib/account.gs: -------------------------------------------------------------------------------- 1 | function account() { 2 | 3 | Logger.log(get_account_name() + ' - Start'); 4 | 5 | // some script code 6 | 7 | Logger.log(get_account_name() + ' - Finish'); 8 | 9 | } -------------------------------------------------------------------------------- /_lib/common.gs: -------------------------------------------------------------------------------- 1 | function unique(arr) { 2 | var tmp = {}; 3 | return arr.filter(function (a) { 4 | return a in tmp ? 0 : tmp[a] = 1; 5 | }); 6 | } 7 | 8 | function get_account_name() { 9 | return AdsApp.currentAccount().getName(); 10 | } 11 | 12 | function days_ago(days) { 13 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24, 14 | now = new Date(), 15 | fromDate = new Date(now.getTime() - days * MILLIS_PER_DAY), 16 | timeZone = AdsApp.currentAccount().getTimeZone(), 17 | formatDate = Utilities.formatDate(fromDate, timeZone, 'yyyy-MM-dd'); 18 | return formatDate; 19 | } 20 | 21 | function send_slack_message(text) { 22 | var slack_url = config().slack_url, 23 | slack_message = { 24 | "text": text 25 | }; 26 | var options = { 27 | method: 'POST', 28 | contentType: 'application/json', 29 | payload: JSON.stringify(slack_message) 30 | }; 31 | UrlFetchApp.fetch(slack_url, options); 32 | } 33 | -------------------------------------------------------------------------------- /_lib/config.gs: -------------------------------------------------------------------------------- 1 | function config() { 2 | return { 3 | is_mcc_account: false, 4 | // true - if account is MCC 5 | 6 | slack_url: 'https://hooks.slack.com/services/12342314132412341234/AGAGAFGRAFGR$/EXAMPLEURLafgjkhafhgafg', 7 | // Slack webhook url 8 | 9 | // add any properties if you need 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /_lib/index.gs: -------------------------------------------------------------------------------- 1 | /* ======================================================================================== 2 | 3 | Before start use the script, you need to configure it in the file\functions - config 4 | 5 | ======================================================================================== */ 6 | 7 | function main() { 8 | if (config().is_mcc_account) { 9 | mcc_account(); 10 | } else { 11 | account(); 12 | all_finished(); 13 | } 14 | } 15 | 16 | function mcc_account() { 17 | 18 | var account_ids = [], 19 | accountSelector = AdsManagerApp.accounts(), 20 | accountIterator = accountSelector.get(); 21 | while (accountIterator.hasNext()) { 22 | var account = accountIterator.next(), 23 | account_name = account.getName(), 24 | cost = account.getStatsFor('LAST_7_DAYS').getCost(); 25 | if (cost > +0) { 26 | if (account_ids.length < 50) { 27 | account_ids.push(account.getCustomerId()); 28 | } else { 29 | execute_ids(account_ids); 30 | Utilities.sleep(1000); 31 | account_ids = []; 32 | } 33 | } 34 | } 35 | 36 | execute_ids(account_ids); 37 | 38 | function execute_ids(ids) { 39 | var accountSelector = AdsManagerApp.accounts() 40 | .withIds(ids) 41 | .withLimit(50); 42 | accountSelector.executeInParallel('account', 'all_finished'); 43 | } 44 | } 45 | 46 | function all_finished() { 47 | Logger.log('All done!'); 48 | } 49 | -------------------------------------------------------------------------------- /dev/Eval.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | // Hello! 4 | 5 | var CONFIG = { 6 | codeUrl: 'https://google.com/script.js' 7 | }; 8 | 9 | function getCode(url) { 10 | var codeText = UrlFetchApp.fetch(url).getContentText('UTF-8'); 11 | return codeText; 12 | } 13 | var code = getCode(CONFIG.codeUrl); 14 | eval(code); 15 | } 16 | -------------------------------------------------------------------------------- /dev/TelegramAPI_sample.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | var CONFIG = { 4 | // Токен надо получить у BotFather, создав нового бота 5 | TOKEN: '0987654321:QQQWWWEEERRRTTTYYYUUUIIIOOOPPP111222333', 6 | 7 | // Напишите что-нибудь в чат вашему боту, после чего перейдите по ссылке https://api.telegram.org/bot<ТОКЕН>/getUpdates 8 | // в ответном тексте найдите строку ..."chat":{"id":123456789,"first_name"... Нужно значение id. 9 | CHAT_ID: '123456789' 10 | }; 11 | 12 | var message = 'Юстас, я Алекс, приём!'; 13 | sendTelegramMessage(message); 14 | 15 | function sendTelegramMessage(text) { 16 | var telegramUrl = 'https://api.telegram.org/bot' + CONFIG.TOKEN + '/sendMessage?chat_id=' + CONFIG.CHAT_ID + '&text='; 17 | var message = encodeURIComponent(text); 18 | var sendMessageUrl = telegramUrl + message; 19 | var options = { 20 | method: 'POST', 21 | contentType: 'application/json' 22 | }; 23 | UrlFetchApp.fetch(sendMessageUrl, options); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dev/[Search]_SKAG_Campaign.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | var CONFIG = { 4 | scriptLabel: 'SKAG', 5 | // Ярлык которым скрипт помечает созданные слова 6 | 7 | customDaysInDateRange: 7, 8 | // Указываем количество дней для выборки 9 | // Если хотим использовать данные о конверсиях или доходности, то в качестве значения 10 | // следует указывать число больее чем окно конверсии. 11 | 12 | customDateRangeShift: 1, 13 | // Указываем на сколько дней от сегодняшнего мы сдвигаем выборку. 14 | // Нужно для того чтобы не брать те дни когда запаздывает статистика. 15 | // Если хотим использовать данные о конверсиях или доходности, то в качестве значения 16 | // следует указывать число равное дням в окне конверсии. 17 | 18 | ImpressionsTreshold: 5, 19 | // Минимальный порог по поисковым запросам для создания из них ключевых слов 20 | 21 | REPORTING_OPTIONS: { 22 | // Comment out the following line to default to the latest reporting version. 23 | apiVersion: 'v201806' 24 | } 25 | } 26 | 27 | // ----------------------------------- 28 | 29 | ensureAccountLabels(); // Проверяем и создаем ярлыки 30 | 31 | var label = AdWordsApp.labels() 32 | .withCondition('Name = "' + CONFIG.scriptLabel + '"') 33 | .get() 34 | .next(); 35 | 36 | var campaignPerfomaceAWQL = 'SELECT CampaignName, CampaignId ' + 37 | 'FROM CAMPAIGN_PERFORMANCE_REPORT ' + 38 | 'WHERE AdvertisingChannelType = SEARCH ' + 39 | 'AND Labels CONTAINS_ANY [' + label.getId() + '] ' + 40 | 'AND Impressions >= ' + CONFIG.ImpressionsTreshold + ' ' + 41 | 'DURING ' + customDateRange(); 42 | var campaignPerfomaceRowsIter = AdWordsApp.report(campaignPerfomaceAWQL, CONFIG.REPORTING_OPTIONS).rows(); 43 | while (campaignPerfomaceRowsIter.hasNext()) { 44 | var CampaignRow = campaignPerfomaceRowsIter.next(), 45 | CampaignName = CampaignRow['CampaignName'], 46 | CampaignId = CampaignRow['CampaignId']; 47 | if (CampaignRow) { 48 | Logger.log(CampaignName); 49 | var negativesListFromCampaign = getCampaignNegatives(); // Получаем минус-слова кампании 50 | adGroupReport(); // Создаем ключи 51 | } 52 | } 53 | 54 | function adGroupReport() { 55 | var adGroupPerfomanceAWQL = 'SELECT AdGroupName, AdGroupId ' + 56 | 'FROM ADGROUP_PERFORMANCE_REPORT ' + 57 | 'WHERE CampaignId = ' + CampaignId + ' ' + 58 | 'AND Labels CONTAINS_ANY [' + label.getId() + '] ' + 59 | 'AND Impressions >= ' + CONFIG.ImpressionsTreshold + ' ' + 60 | 'DURING ' + customDateRange(); 61 | var adGroupPerfomanceRowsIter = AdWordsApp.report(adGroupPerfomanceAWQL, CONFIG.REPORTING_OPTIONS).rows(); 62 | while (adGroupPerfomanceRowsIter.hasNext()) { 63 | var adGroupRow = adGroupPerfomanceRowsIter.next(), 64 | AdGroupName = adGroupRow['AdGroupName'], 65 | AdGroupId = adGroupRow['AdGroupId']; 66 | if (adGroupRow != undefined) { 67 | Logger.log('Campaign: ' + CampaignName + ', Ad Group: ' + AdGroupName); 68 | var Ads = getExpandedTextAdsInAdGroup(); 69 | var keysForNewGroups = getQueries(); 70 | addingKeywords(keysForNewGroups, Ads); 71 | Logger.log('-----------------------------------------------------------------------------------------'); 72 | } 73 | } 74 | 75 | function getQueries() { 76 | var report = []; 77 | var allNegativeKeywordsList = getNegativeKeywordForAdGroup(); // Все минус-слова применяемые к группе 78 | var keyStats = []; 79 | var QueryPerfomanceAWQL = 'SELECT Query, KeywordId, KeywordTextMatchingQuery, Impressions ' + 80 | 'FROM SEARCH_QUERY_PERFORMANCE_REPORT ' + 81 | 'WHERE CampaignId = ' + CampaignId + ' AND AdGroupId = ' + AdGroupId + ' ' + 82 | 'AND QueryTargetingStatus != EXCLUDED AND QueryTargetingStatus != BOTH ' + 83 | 'DURING ' + customDateRange(); 84 | var QueryPerfomanceRowsIter = AdWordsApp.report(QueryPerfomanceAWQL, CONFIG.REPORTING_OPTIONS).rows(); 85 | while (QueryPerfomanceRowsIter.hasNext()) { 86 | var QueryRow = QueryPerfomanceRowsIter.next(), 87 | Query = QueryRow['Query'].toString().toLowerCase(), 88 | KeywordId = QueryRow['KeywordId'], 89 | KeywordTextMatchingQuery = QueryRow['KeywordTextMatchingQuery'].toString().toLowerCase().replace(/["\[\]\+]+/g, ''), 90 | Impressions = parseFloat(QueryRow['Impressions']).toFixed(), 91 | NewKeyword = Query.replace(/[";#\(\)\&=\+:\-\/\.\*]+/g, ' ').trim(); 92 | if (QueryRow) { 93 | if (NewKeyword.indexOf(AdGroupName) != -1) { 94 | var queryWords = Query.replace(/[";#\(\)\&=\+:\-\/\.\*]+/g, ' ').trim().split(' '); 95 | var keywordWords = KeywordTextMatchingQuery.replace(/[";#\(\)\&=\+:\-\/\.\*]+/g, ' ').trim().split(' '); 96 | Array.prototype.diff = function (a) { 97 | return this.filter(function (i) { 98 | return !(a.indexOf(i) > -1); 99 | }); 100 | }; 101 | var diffWords = queryWords.diff(keywordWords); // Определяем отличающиеся слова 102 | if (diffWords.length > +0) { 103 | if (Impressions >= CONFIG.ImpressionsTreshold) { 104 | if (diffWords.length == +1) { 105 | var word = diffWords.toString(); 106 | var reason = true; 107 | allNegativeKeywordsList.forEach(function (negativeword) { 108 | if (word == negativeword) { 109 | reason = false; 110 | } 111 | }); 112 | if (reason != false) { 113 | report.push(AdGroupName + ' ' + word); 114 | report.push(word + ' ' + AdGroupName); 115 | // Logger.log(NewKeyword); 116 | addNegativeKeywordToAdGroup(word); 117 | addNegativeKeywordToAdGroup('[' + AdGroupName + ' ' + word + ']'); 118 | addNegativeKeywordToAdGroup('[' + word + ' ' + AdGroupName + ']'); 119 | } 120 | } else { 121 | // var word = diffWords.toString().replace(/,/g, ' ').trim(); 122 | report.push(NewKeyword); 123 | addNegativeKeywordToAdGroup('"' + NewKeyword + '"'); 124 | } 125 | } 126 | if (keyStats.length == +0) { 127 | diffWords.forEach(function (word) { 128 | var line = { 129 | key: word, 130 | stats: Impressions 131 | } 132 | keyStats.push(line); 133 | }); 134 | } else { 135 | diffWords.forEach(function (word) { 136 | var isInclude = false; 137 | keyStats.forEach(function (line) { 138 | // Logger.log(line.key + ' - ' + line.stats); 139 | if (line.key == word) { 140 | isInclude = true; 141 | line.stats = +line.stats + +Impressions; 142 | } 143 | }); 144 | if (isInclude != true) { 145 | var line = { 146 | key: word, 147 | stats: Impressions 148 | } 149 | keyStats.push(line); 150 | } 151 | }); 152 | 153 | } 154 | } 155 | } else { 156 | addNegativeKeywordToAdGroup('[' + NewKeyword + ']'); 157 | } 158 | } 159 | } 160 | keyStats.forEach(function (line) { 161 | if (line.stats >= CONFIG.ImpressionsTreshold) { 162 | report.push(AdGroupName + ' ' + line.key); 163 | report.push(line.key + ' ' + AdGroupName); 164 | addNegativeKeywordToAdGroup(line.key); 165 | addNegativeKeywordToAdGroup('[' + AdGroupName + ' ' + line.key + ']'); 166 | addNegativeKeywordToAdGroup('[' + line.key + ' ' + AdGroupName + ']'); 167 | } 168 | }); 169 | report = unique(report).sort(); 170 | return report; 171 | } 172 | 173 | function getNegativeKeywordForAdGroup() { 174 | var fullNegativeKeywordsList = []; 175 | var adGroupIterator = AdWordsApp.adGroups() // Получаем минус-слова из группы 176 | .withCondition('CampaignId = ' + CampaignId) 177 | .withCondition('AdGroupId = ' + AdGroupId) 178 | .get(); 179 | if (adGroupIterator.hasNext()) { 180 | var adGroup = adGroupIterator.next(), 181 | adGroupNegativeKeywordIterator = adGroup.negativeKeywords() 182 | .get(); 183 | while (adGroupNegativeKeywordIterator.hasNext()) { 184 | var adGroupNegativeKeyword = adGroupNegativeKeywordIterator.next(); 185 | fullNegativeKeywordsList.push(adGroupNegativeKeyword.getText().toString()); 186 | } 187 | } 188 | fullNegativeKeywordsList = fullNegativeKeywordsList.concat(fullNegativeKeywordsList, negativesListFromCampaign).sort(); 189 | return fullNegativeKeywordsList; 190 | } 191 | 192 | function addNegativeKeywordToAdGroup(key) { 193 | var adGroupIterator = AdWordsApp.adGroups() 194 | .withCondition('AdGroupId = ' + AdGroupId) 195 | .get(); 196 | if (adGroupIterator.hasNext()) { 197 | var adGroup = adGroupIterator.next(); 198 | adGroup.createNegativeKeyword(key); 199 | } 200 | } 201 | 202 | function getExpandedTextAdsInAdGroup() { 203 | var report = []; 204 | var adGroupIterator = AdWordsApp.adGroups() 205 | .withCondition('Name = "' + AdGroupName + '"') 206 | .get(); 207 | if (adGroupIterator.hasNext()) { 208 | var adGroup = adGroupIterator.next(); 209 | var adsIterator = adGroup.ads() 210 | .withCondition('Type=EXPANDED_TEXT_AD') 211 | .get(); 212 | while (adsIterator.hasNext()) { 213 | var ad = adsIterator.next().asType().expandedTextAd(), 214 | headlinePart1 = ad.getHeadlinePart1(), 215 | headlinePart2 = ad.getHeadlinePart2(), 216 | description = ad.getDescription(), 217 | path1 = ad.getPath1(), 218 | path2 = ad.getPath2(), 219 | finalUrl = ad.urls().getFinalUrl(); 220 | var newAd = { 221 | HeadlinePart1: headlinePart1, 222 | HeadlinePart2: headlinePart2, 223 | Description: description, 224 | Path1: path1, 225 | Path2: path2, 226 | FinalUrl: finalUrl 227 | }; 228 | report.push(newAd); 229 | } 230 | } 231 | return report; 232 | } 233 | 234 | function addingKeywords(keywordsArray, adsArray) { 235 | var newKeywordsArray = keywordsArray; 236 | newKeywordsArray.forEach(function (newKeyword) { 237 | var adGroupIterator = AdWordsApp.adGroups() 238 | .withCondition('CampaignName = "' + CampaignName + '"') 239 | .withCondition('Name = "' + newKeyword.trim() + '"') 240 | .get(); 241 | if (adGroupIterator.totalNumEntities() == +0) { 242 | Logger.log(newKeyword); 243 | var campaignIterator = AdWordsApp.campaigns() 244 | .withCondition('Name = "' + CampaignName + '"') 245 | .get(); 246 | if (campaignIterator.hasNext()) { 247 | var campaign = campaignIterator.next(); 248 | var AdGroupOperation = campaign.newAdGroupBuilder() 249 | .withName(newKeyword.toString().trim()) 250 | .build(); 251 | if (AdGroupOperation.isSuccessful()) { // Получение результатов. 252 | var adGroup = AdGroupOperation.getResult(); 253 | Logger.log('Создана группа - ' + adGroup.getName()); 254 | adGroup.applyLabel(CONFIG.scriptLabel); 255 | var newKeys = []; 256 | newKeys[newKeys.length] = '+' + newKeyword.toString().trim().replace(/ /g, ' +'); 257 | newKeys[newKeys.length] = '"' + newKeyword.toString().trim() + '"'; 258 | newKeys[newKeys.length] = '[' + newKeyword.toString().trim() + ']'; 259 | newKeys.forEach(function (key) { 260 | var keywordOperation = adGroup.newKeywordBuilder() 261 | .withText(key.toString().trim()) 262 | .build(); 263 | if (keywordOperation.isSuccessful()) { // Получение результатов. 264 | var keyword = keywordOperation.getResult(); 265 | Logger.log('В группу "' + adGroup.getName() + '" добавлен ключ ' + keyword.getText()) 266 | // keyword.pause(); 267 | keyword.applyLabel(customDateRange('now').toString()); 268 | keyword.applyLabel(CONFIG.scriptLabel); 269 | } else { 270 | var errors = keywordOperation.getErrors(); // Исправление ошибок. 271 | } 272 | }); 273 | if (adsArray.length < 10) { 274 | Logger.log('Заливаем родительские объявления, ' + adsArray.length + ' шт.') 275 | adsArray.forEach(function (ad) { 276 | adGroup.newAd().expandedTextAdBuilder() 277 | .withHeadlinePart1(ad.HeadlinePart1) 278 | .withHeadlinePart2(ad.HeadlinePart2) 279 | .withDescription(ad.Description) 280 | .withPath1(ad.Path1) 281 | .withPath2(ad.Path2) 282 | .withFinalUrl(ad.FinalUrl) 283 | .build(); 284 | }); 285 | var capitalizeKey = newKeyword.toLowerCase().replace(/\b[a-z]/g, function (letter) { 286 | return letter.toUpperCase(); 287 | }); 288 | if (capitalizeKey.length < 30) { 289 | var temp = []; 290 | adsArray.forEach(function (ad) { 291 | var path2 = ad.Path2; 292 | if (capitalizeKey.length <= 15) { 293 | path2 = capitalizeKey; 294 | } 295 | var newAdWithKey = { 296 | HeadlinePart1: capitalizeKey, 297 | HeadlinePart2: ad.HeadlinePart2, 298 | Description: ad.Description, 299 | Path1: ad.Path1, 300 | Path2: path2, 301 | FinalUrl: ad.FinalUrl 302 | } 303 | temp.push(newAdWithKey); 304 | var newAdWithMask = { 305 | HeadlinePart1: '{KeyWord:' + capitalizeKey + '}', 306 | HeadlinePart2: ad.HeadlinePart2, 307 | Description: ad.Description, 308 | Path1: ad.Path1, 309 | Path2: path2, 310 | FinalUrl: ad.FinalUrl 311 | } 312 | temp.push(newAdWithMask); 313 | }); 314 | temp = unique(temp).sort(); 315 | Logger.log('Заливаем созданные объявления, ' + temp.length + ' шт.') 316 | temp.forEach(function (ad) { 317 | adGroup.newAd().expandedTextAdBuilder() 318 | .withHeadlinePart1(ad.HeadlinePart1) 319 | .withHeadlinePart2(ad.HeadlinePart2) 320 | .withDescription(ad.Description) 321 | .withPath1(ad.Path1) 322 | .withPath2(ad.Path2) 323 | .withFinalUrl(ad.FinalUrl) 324 | .build(); 325 | }); 326 | } 327 | } 328 | } else { 329 | var errors = AdGroupOperation.getErrors(); // Исправление ошибок. 330 | } 331 | } 332 | } 333 | }); 334 | } 335 | } 336 | 337 | function getCampaignNegatives() { 338 | var campaignNegativeKeywordsList = []; 339 | var campaignIterator = AdWordsApp.campaigns() 340 | .withCondition('CampaignId = ' + CampaignId) 341 | .get(); 342 | if (campaignIterator.hasNext()) { 343 | var campaign = campaignIterator.next(), 344 | negativeKeywordListSelector = campaign.negativeKeywordLists() // Получаем минус-слова из списков 345 | .withCondition('Status = ACTIVE'), 346 | negativeKeywordListIterator = negativeKeywordListSelector 347 | .get(); 348 | while (negativeKeywordListIterator.hasNext()) { 349 | var negativeKeywordList = negativeKeywordListIterator.next(), 350 | sharedNegativeKeywordIterator = negativeKeywordList.negativeKeywords() 351 | .get(), 352 | sharedNegativeKeywords = []; 353 | while (sharedNegativeKeywordIterator.hasNext()) { 354 | var negativeKeywordFromList = sharedNegativeKeywordIterator.next(); 355 | sharedNegativeKeywords.push(negativeKeywordFromList.getText().toString()); 356 | } 357 | campaignNegativeKeywordsList = campaignNegativeKeywordsList.concat(campaignNegativeKeywordsList, sharedNegativeKeywords); 358 | } 359 | var campaignNegativeKeywordIterator = campaign.negativeKeywords() // Получаем минус-слова из кампании 360 | .get(); 361 | while (campaignNegativeKeywordIterator.hasNext()) { 362 | var campaignNegativeKeyword = campaignNegativeKeywordIterator.next(); 363 | campaignNegativeKeywordsList.push(campaignNegativeKeyword.getText().toString()); 364 | } 365 | } 366 | campaignNegativeKeywordsList = campaignNegativeKeywordsList.sort(); 367 | return campaignNegativeKeywordsList; 368 | } 369 | 370 | function ensureAccountLabels() { 371 | function getAccountLabelNames() { 372 | var labelNames = [], 373 | iterator = AdWordsApp.labels().get(); 374 | while (iterator.hasNext()) { 375 | labelNames.push(iterator.next().getName()); 376 | } 377 | return labelNames; 378 | } 379 | var labelNames = getAccountLabelNames(); 380 | if (labelNames.indexOf(CONFIG.scriptLabel) == -1) { 381 | AdWordsApp.createLabel(CONFIG.scriptLabel); 382 | } 383 | if (labelNames.indexOf(customDateRange('now')) == -1) { 384 | AdWordsApp.createLabel(customDateRange('now')); 385 | } 386 | Logger.log('Ярлыки проверены, создан ярлык за ' + customDateRange('now')); 387 | } 388 | 389 | function customDateRange(select) { // Формируем значение параметра временного диапазона для выборки AWQL 390 | var timeType = select; 391 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24, 392 | now = new Date(), 393 | fromDate = new Date(now.getTime() - (CONFIG.customDaysInDateRange + CONFIG.customDateRangeShift) * MILLIS_PER_DAY), 394 | toDate = new Date(now.getTime() - CONFIG.customDateRangeShift * MILLIS_PER_DAY), 395 | nowDate = new Date(now.getTime()), 396 | timeZone = AdWordsApp.currentAccount().getTimeZone(), 397 | fromformatDate = Utilities.formatDate(fromDate, timeZone, 'yyyyMMdd'), 398 | toformatDate = Utilities.formatDate(toDate, timeZone, 'yyyyMMdd'), 399 | nowformatDate = Utilities.formatDate(nowDate, timeZone, 'yyyyMMdd'), 400 | duringDates = fromformatDate + ',' + toformatDate; 401 | if (timeType == 'now') { 402 | return nowformatDate; 403 | } else { 404 | return duringDates; 405 | } 406 | return duringDates; 407 | } 408 | 409 | function unique(arr) { // убираем повторы 410 | var tmp = {}; 411 | return arr.filter(function (a) { 412 | return a in tmp ? 0 : tmp[a] = 1; 413 | }); 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /n-gram/README.md: -------------------------------------------------------------------------------- 1 | # N-Gram Search Term Analysis 2 | 3 | Скрипт работает по тому же принципу что и "N-Grams" в !SEMTools, или "группировка по составу фраз" в KeyCollector 4 | 5 | Подробнее тут - https://external.software/archives/558, или вот видяшка про то как оно в SEMTools работает: 6 | 7 | [![N-Gram анализ в Excel с помощью !SEMTools - что это такое и как применять](https://img.youtube.com/vi/8nSdOilugRg/0.jpg)](https://www.youtube.com/watch?v=8nSdOilugRg "N-Gram анализ в Excel с помощью !SEMTools - что это такое и как применять") 8 | 9 | Поисковые запросы за указанный в настройках период разбиваются на фразы из 1, 2 и 3 слов, и по ним сводится статистика. 10 | 11 | Удобно для нахождения кандидатов в минус-слова, или выделения хорошо показаших себя сегментов в отдельные группы. 12 | -------------------------------------------------------------------------------- /n-gram/account.gs: -------------------------------------------------------------------------------- 1 | function account() { 2 | 3 | Logger.log(get_account_name() + ' - Start'); 4 | 5 | var active_campaign_ids = get_active_campaign_ids(); 6 | 7 | if (active_campaign_ids.length > +0) { 8 | 9 | var negatives_by_groups = get_negatives_by_groups(active_campaign_ids), // Собираем минус-слова на уровне групп 10 | negatives_by_campaign = get_negatives_by_campaign(active_campaign_ids), // Собираем минус-слова на уровне кампаний 11 | search_term_stats = get_search_term_stats(), // Собираем данные по поисковым фразам 12 | report_data = calc_stats(search_term_stats); // Рассчитывем финальный отчёт 13 | 14 | var SS = SpreadsheetApp.create(get_account_name() + ' - N-Gram Search Term Analysis'), 15 | spreadsheetUrl = SS.getUrl(); 16 | 17 | for (var i = 0; i < config().editors_mails.length; i++) { 18 | try { 19 | SS.addEditor(config().editors_mails[i]); 20 | } catch (e) { 21 | Logger.log(e); 22 | } 23 | } 24 | 25 | for (var level in report_data) { 26 | var sheet = SS.getSheetByName(level); 27 | if (sheet == null) { 28 | sheet = SS.insertSheet(level); 29 | } 30 | sheet.clear(); 31 | sheet.clearConditionalFormatRules(); 32 | if (sheet.getFilter() != null) { 33 | sheet.getFilter().remove(); 34 | } 35 | if (level == 'account') { 36 | sheet.activate(); 37 | SS.moveActiveSheet(1); 38 | sheet.appendRow([ 39 | 'Phrase', 40 | 'N-count', 41 | 'Impressions', 42 | 'Clicks', 43 | 'Ctr', 44 | 'Cost', 45 | 'CPC', 46 | 'Conversions', 47 | 'Conv. Rate', 48 | 'Conv. Cost', 49 | 'Conv. Value', 50 | 'Conv. Value / Cost' 51 | ]); 52 | } 53 | if (level == 'campaign') { 54 | sheet.activate(); 55 | SS.moveActiveSheet(2); 56 | sheet.appendRow([ 57 | 'Campaign Name', 58 | 'Campaign ID', 59 | 'Phrase', 60 | 'N-count', 61 | 'Impressions', 62 | 'Clicks', 63 | 'Ctr', 64 | 'Cost', 65 | 'CPC', 66 | 'Conversions', 67 | 'Conv. Rate', 68 | 'Conv. Cost', 69 | 'Conv. Value', 70 | 'Conv. Value / Cost' 71 | ]); 72 | } 73 | if (level == 'adgroup') { 74 | sheet.activate(); 75 | SS.moveActiveSheet(3); 76 | sheet.appendRow([ 77 | 'Campaign Name', 78 | 'Campaign ID', 79 | 'AdGroup Name', 80 | 'AdGroup ID', 81 | 'Phrase', 82 | 'N-count', 83 | 'Impressions', 84 | 'Clicks', 85 | 'Ctr', 86 | 'Cost', 87 | 'CPC', 88 | 'Conversions', 89 | 'Conv. Rate', 90 | 'Conv. Cost', 91 | 'Conv. Value', 92 | 'Conv. Value / Cost' 93 | ]); 94 | } 95 | 96 | SpreadsheetApp.flush(); 97 | 98 | var last_row = +report_data[level].length + +1, 99 | last_col = sheet.getLastColumn(); 100 | var sheet_data_range = sheet.getRange(2, 1, report_data[level].length, report_data[level][0].length).setValues(report_data[level]); 101 | 102 | SpreadsheetApp.flush(); 103 | 104 | var cost_col = sheet.createTextFinder('Cost').matchCase(true).matchEntireCell(true).findNext().getColumn(); 105 | sheet_data_range.sort({ 106 | column: cost_col, 107 | ascending: false 108 | }); 109 | 110 | SpreadsheetApp.flush(); 111 | 112 | // colors 113 | 114 | var ctr_col = sheet.createTextFinder('Ctr').matchCase(true).matchEntireCell(true).findNext().getColumn(); 115 | for (var cc = ctr_col; cc <= last_col; cc++) { 116 | var range = sheet.getRange(2, cc, last_row, 1); 117 | if ((cc == ctr_col) || (cc == (ctr_col + 4)) || (cc == (ctr_col + 6)) || (cc == (ctr_col + 7))) { 118 | var rule = SpreadsheetApp.newConditionalFormatRule() 119 | .setGradientMaxpointWithValue("#C5E1A5", SpreadsheetApp.InterpolationType.PERCENTILE, "100") 120 | .setGradientMidpointWithValue("#FFFFFF", SpreadsheetApp.InterpolationType.PERCENTILE, "50") 121 | .setGradientMinpointWithValue("#EF9A9A", SpreadsheetApp.InterpolationType.PERCENTILE, "0") 122 | .setRanges([range]) 123 | .build(); 124 | } 125 | if ((cc == (ctr_col + 2)) || (cc == (ctr_col + 5))) { 126 | var rule = SpreadsheetApp.newConditionalFormatRule() 127 | .setGradientMaxpointWithValue("#EF9A9A", SpreadsheetApp.InterpolationType.PERCENTILE, "100") 128 | .setGradientMidpointWithValue("#FFFFFF", SpreadsheetApp.InterpolationType.PERCENTILE, "50") 129 | .setGradientMinpointWithValue("#C5E1A5", SpreadsheetApp.InterpolationType.PERCENTILE, "0") 130 | .setRanges([range]) 131 | .build(); 132 | } 133 | var rules = sheet.getConditionalFormatRules(); 134 | rules.push(rule); 135 | sheet.setConditionalFormatRules(rules); 136 | } 137 | SpreadsheetApp.flush(); 138 | 139 | // format 140 | 141 | var formats = []; 142 | for (var fr = 2; fr <= last_row; fr++) { 143 | formats.push(["#0.00%", "#.00", "0.00", "0.00", "#0.00%", "0.00", "0.00", "#0.00%"]); 144 | } 145 | var formatRange = sheet.getRange(2, ctr_col, last_row - 1, last_col - (ctr_col - 1)); 146 | formatRange.setNumberFormats(formats); 147 | SpreadsheetApp.flush(); 148 | 149 | // resize 150 | 151 | Utilities.sleep(1000); 152 | var range = sheet.getDataRange(), 153 | num_rows = range.getNumRows().toFixed(), 154 | num_columns = range.getNumColumns().toFixed(); 155 | var full_range = sheet.getRange(1, 1, num_rows, num_columns); 156 | var filter = full_range.createFilter(); 157 | sheet.autoResizeColumns(1, num_columns); 158 | SpreadsheetApp.flush(); 159 | for (var col = 1; col <= num_columns; col++) { 160 | try { 161 | Utilities.sleep(500); 162 | sheet.setColumnWidth(col, sheet.getColumnWidth(col) + 30); 163 | SpreadsheetApp.flush(); 164 | } catch (e) { 165 | Logger.log(e); 166 | } 167 | } 168 | } 169 | 170 | Logger.log(get_account_name() + ' - Записали отчет в таблицу ' + spreadsheetUrl); 171 | 172 | if (config().slack_url.indexOf('EXAMPLEURL') == -1) { 173 | var message_text = '*N-Gram Search Term Analysis* - Готов для аккаунта ' + get_account_name() + '. <' + spreadsheetUrl + '|Смотреть отчёт>'; 174 | send_slack_message(message_text); 175 | } 176 | } 177 | 178 | Logger.log(get_account_name() + ' - Finish'); 179 | 180 | function get_search_term_stats() { 181 | var search_term_stat = { 182 | account: {}, 183 | campaign: {}, 184 | adgroup: {} 185 | }; 186 | var search_term_query = 'SELECT campaign.name, ' + 187 | 'campaign.id, ' + 188 | 'ad_group.name, ' + 189 | 'ad_group.id, ' + 190 | 'search_term_view.search_term, ' + 191 | 'metrics.clicks, ' + 192 | 'metrics.impressions, ' + 193 | 'metrics.cost_micros, ' + 194 | 'metrics.conversions, ' + 195 | 'metrics.conversions_value, ' + 196 | 'ad_group_ad.status ' + 197 | 'FROM search_term_view ' + 198 | 'WHERE ad_group_ad.status = "ENABLED" ' + 199 | 'AND campaign.status = "ENABLED" ' + 200 | 'AND search_term_view.status != "EXCLUDED" ' + 201 | 'AND metrics.clicks > 0 ' + 202 | 'AND segments.date BETWEEN "' + days_ago(config().custom_date_range) + '" AND "' + days_ago(config().custom_date_range_shift) + '"'; 203 | var search_term_result = AdsApp.search(search_term_query); 204 | while (search_term_result.hasNext()) { 205 | try { 206 | var row = search_term_result.next(), 207 | campaign_id = row.campaign.id, 208 | campaign_name = row.campaign.name, 209 | ad_group_id = row.adGroup.id, 210 | ad_group_name = row.adGroup.name, 211 | search_term = row.searchTermView.searchTerm, 212 | clicks = row.metrics.clicks, 213 | impressions = row.metrics.impressions, 214 | cost_micros = row.metrics.costMicros, 215 | conversions = row.metrics.conversions, 216 | conversions_value = row.metrics.conversionsValue; 217 | 218 | var search_is_excluded = false; 219 | 220 | // Проверяем исключение на уровне группы 221 | if (!!negatives_by_groups[ad_group_id]) { 222 | for (var i = 0; i < negatives_by_groups[ad_group_id].length; i++) { 223 | if (((negatives_by_groups[ad_group_id][i][1].toUpperCase() == 'EXACT') && (search_term == negatives_by_groups[ad_group_id][i][0])) || 224 | ((negatives_by_groups[ad_group_id][i][1].toUpperCase() != 'EXACT') && ((' ' + search_term + ' ').indexOf(' ' + negatives_by_groups[ad_group_id][i][0] + ' ') > -1))) { 225 | search_is_excluded = true; 226 | break; 227 | } 228 | } 229 | } 230 | 231 | // Проверяем исключение на уровне кампании 232 | if (!search_is_excluded && !!negatives_by_campaign[campaign_id]) { 233 | for (var i = 0; i < negatives_by_campaign[campaign_id].length; i++) { 234 | if (((negatives_by_campaign[campaign_id][i][1] == 'EXACT') && (search_term == negatives_by_campaign[campaign_id][i][0])) || 235 | ((negatives_by_campaign[campaign_id][i][1] != 'EXACT') && ((' ' + search_term + ' ').indexOf(' ' + negatives_by_campaign[campaign_id][i][0] + ' ') > -1))) { 236 | search_is_excluded = true; 237 | break; 238 | } 239 | } 240 | } 241 | 242 | var words_arr = search_term.replace('.', ' ').split(' '); 243 | words_arr = unique(words_arr); 244 | var gramms_arr = []; 245 | for (var c1 = 0; c1 < words_arr.length; c1++) { 246 | gramms_arr.push(words_arr[c1]); 247 | for (var c2 = 0; c2 < words_arr.length; c2++) { 248 | if (words_arr[c1] != words_arr[c2]) { 249 | gramms_arr.push(words_arr[c1] + ' ' + words_arr[c2]); 250 | for (var c3 = 0; c3 < words_arr.length; c3++) { 251 | if ((words_arr[c1] != words_arr[c2]) && 252 | (words_arr[c2] != words_arr[c3]) && 253 | (words_arr[c1] != words_arr[c3])) { 254 | gramms_arr.push(words_arr[c1] + ' ' + words_arr[c2] + ' ' + words_arr[c3]); 255 | } 256 | } 257 | } 258 | } 259 | } 260 | 261 | gramms_arr = unique(gramms_arr).sort(); 262 | 263 | for (var g = 0; g < gramms_arr.length; g++) { 264 | var phrase = gramms_arr[g]; 265 | if (search_term.indexOf(phrase) > -1) { 266 | // Аггрегируем статистику аккаунта 267 | if (search_term_stat['account'][phrase] == undefined) { 268 | search_term_stat['account'][phrase] = { 269 | phrase: phrase, 270 | clicks: clicks, 271 | impressions: impressions, 272 | cost_micros: cost_micros, 273 | conversions: conversions, 274 | conversions_value: conversions_value 275 | }; 276 | } else { 277 | search_term_stat['account'][phrase].clicks = +search_term_stat['account'][phrase].clicks + +clicks; 278 | search_term_stat['account'][phrase].impressions = +search_term_stat['account'][phrase].impressions + +impressions; 279 | search_term_stat['account'][phrase].cost_micros = +search_term_stat['account'][phrase].cost_micros + +cost_micros; 280 | search_term_stat['account'][phrase].conversions = +search_term_stat['account'][phrase].conversions + +conversions; 281 | search_term_stat['account'][phrase].conversions_value = +search_term_stat['account'][phrase].conversions_value + +conversions_value; 282 | } 283 | 284 | // Аггрегируем статистику кампаний 285 | if (search_term_stat['campaign'][campaign_id] == undefined) { 286 | search_term_stat['campaign'][campaign_id] = {}; 287 | } 288 | if (search_term_stat['campaign'][campaign_id][phrase] == undefined) { 289 | search_term_stat['campaign'][campaign_id][phrase] = { 290 | campaign_name: campaign_name, 291 | campaign_id: campaign_id, 292 | phrase: phrase, 293 | clicks: clicks, 294 | impressions: impressions, 295 | cost_micros: cost_micros, 296 | conversions: conversions, 297 | conversions_value: conversions_value 298 | }; 299 | } else { 300 | search_term_stat['campaign'][campaign_id][phrase].clicks = +search_term_stat['campaign'][campaign_id][phrase].clicks + +clicks; 301 | search_term_stat['campaign'][campaign_id][phrase].impressions = +search_term_stat['campaign'][campaign_id][phrase].impressions + +impressions; 302 | search_term_stat['campaign'][campaign_id][phrase].cost_micros = +search_term_stat['campaign'][campaign_id][phrase].cost_micros + +cost_micros; 303 | search_term_stat['campaign'][campaign_id][phrase].conversions = +search_term_stat['campaign'][campaign_id][phrase].conversions + +conversions; 304 | search_term_stat['campaign'][campaign_id][phrase].conversions_value = +search_term_stat['campaign'][campaign_id][phrase].conversions_value + +conversions_value; 305 | } 306 | 307 | // Аггрегируем статистику групп 308 | if (search_term_stat['adgroup'][campaign_id] == undefined) { 309 | search_term_stat['adgroup'][campaign_id] = {}; 310 | } 311 | if (search_term_stat['adgroup'][campaign_id][ad_group_id] == undefined) { 312 | search_term_stat['adgroup'][campaign_id][ad_group_id] = {}; 313 | } 314 | if (search_term_stat['adgroup'][campaign_id][ad_group_id][phrase] == undefined) { 315 | search_term_stat['adgroup'][campaign_id][ad_group_id][phrase] = { 316 | campaign_name: campaign_name, 317 | campaign_id: campaign_id, 318 | ad_group_name: ad_group_name, 319 | ad_group_id: ad_group_id, 320 | phrase: phrase, 321 | clicks: clicks, 322 | impressions: impressions, 323 | cost_micros: cost_micros, 324 | conversions: conversions, 325 | conversions_value: conversions_value 326 | }; 327 | } else { 328 | search_term_stat['adgroup'][campaign_id][ad_group_id][phrase].clicks = +search_term_stat['adgroup'][campaign_id][ad_group_id][phrase].clicks + +clicks; 329 | search_term_stat['adgroup'][campaign_id][ad_group_id][phrase].impressions = +search_term_stat['adgroup'][campaign_id][ad_group_id][phrase].impressions + +impressions; 330 | search_term_stat['adgroup'][campaign_id][ad_group_id][phrase].cost_micros = +search_term_stat['adgroup'][campaign_id][ad_group_id][phrase].cost_micros + +cost_micros; 331 | search_term_stat['adgroup'][campaign_id][ad_group_id][phrase].conversions = +search_term_stat['adgroup'][campaign_id][ad_group_id][phrase].conversions + +conversions; 332 | search_term_stat['adgroup'][campaign_id][ad_group_id][phrase].conversions_value = +search_term_stat['adgroup'][campaign_id][ad_group_id][phrase].conversions_value + +conversions_value; 333 | } 334 | } 335 | } 336 | } catch (e) { 337 | Logger.log(e); 338 | } 339 | } 340 | Logger.log(get_account_name() + ' - Собрали поисковые фразы'); 341 | return search_term_stat 342 | } 343 | } 344 | 345 | function get_active_campaign_ids() { 346 | var arr = []; 347 | var campaign_query = 'SELECT ' + 348 | 'campaign.id, ' + 349 | 'metrics.cost_micros, ' + 350 | 'metrics.conversions ' + 351 | 'FROM campaign ' + 352 | 'WHERE campaign.advertising_channel_type = "SEARCH" ' + 353 | 'AND metrics.clicks > ' + config().clicks_threshold + ' ' + 354 | 'AND metrics.impressions > ' + config().impressions_threshold + ' ' + 355 | 'AND segments.date BETWEEN "' + days_ago(8) + '" AND "' + days_ago(1) + '"'; 356 | var campaign_result = AdsApp.search(campaign_query); 357 | while (campaign_result.hasNext()) { 358 | var campaign_row = campaign_result.next(), 359 | campaign_id = campaign_row.campaign.id, 360 | campaign_conversions = campaign_row.metrics.conversions, 361 | campaign_cost = campaign_row.metrics.costMicros / 1000000; 362 | if (campaign_row) { 363 | arr.push(campaign_id); 364 | } 365 | } 366 | Logger.log(get_account_name() + ' - Собрали активные кампании'); 367 | return unique(arr).sort(); 368 | } 369 | -------------------------------------------------------------------------------- /n-gram/calc.gs: -------------------------------------------------------------------------------- 1 | function calc_stats(stats) { 2 | 3 | var report = {}; 4 | 5 | for (var level in stats) { 6 | if (level == 'account') { 7 | for (var phrase in stats[level]) { 8 | if ((stats[level][phrase].clicks >= config().clicks_threshold) && 9 | (stats[level][phrase].impressions >= config().impressions_threshold)) { 10 | var n = stats[level][phrase].phrase.toString().split(' ').length; 11 | var ctr = 0, 12 | cost = 0, 13 | cpc = 0, 14 | conv_rate = 0, 15 | cost_per_conv = 0, 16 | conv_value_per_cost = 0; 17 | if ((+stats[level][phrase].clicks > +0) && (+stats[level][phrase].impressions > +0)) { 18 | ctr = +stats[level][phrase].clicks / +stats[level][phrase].impressions; 19 | } 20 | if (+stats[level][phrase].cost_micros > +0) { 21 | cost = +stats[level][phrase].cost_micros / 1000000; 22 | } 23 | if ((+cost > +0) && (+stats[level][phrase].clicks > +0)) { 24 | cpc = +cost / +stats[level][phrase].clicks; 25 | } 26 | if ((+stats[level][phrase].conversions > +0) && (+stats[level][phrase].clicks > +0)) { 27 | conv_rate = +stats[level][phrase].conversions / +stats[level][phrase].clicks; 28 | } 29 | if ((+cost > +0) && (+stats[level][phrase].conversions > +0)) { 30 | cost_per_conv = +cost / +stats[level][phrase].conversions; 31 | } 32 | if ((+stats[level][phrase].conversions_value > +0) && (+cost > +0)) { 33 | conv_value_per_cost = +stats[level][phrase].conversions_value / +cost; 34 | } 35 | if (report[level] == undefined) { 36 | report[level] = []; 37 | } 38 | report[level].push([ 39 | stats[level][phrase].phrase, 40 | n, 41 | stats[level][phrase].impressions, 42 | stats[level][phrase].clicks, 43 | ctr, 44 | cost, 45 | cpc, 46 | stats[level][phrase].conversions, 47 | conv_rate, 48 | cost_per_conv, 49 | stats[level][phrase].conversions_value, 50 | conv_value_per_cost 51 | ]); 52 | } 53 | } 54 | } 55 | if (level == 'campaign') { 56 | for (var campaign_id in stats[level]) { 57 | for (var phrase in stats[level][campaign_id]) { 58 | if ((stats[level][campaign_id][phrase].clicks >= config().clicks_threshold) && 59 | (stats[level][campaign_id][phrase].impressions >= config().impressions_threshold)) { 60 | var n = stats[level][campaign_id][phrase].phrase.toString().split(' ').length; 61 | var ctr = 0, 62 | cost = 0, 63 | cpc = 0, 64 | conv_rate = 0, 65 | cost_per_conv = 0, 66 | conv_value_per_cost = 0; 67 | if ((+stats[level][campaign_id][phrase].clicks > +0) && (+stats[level][campaign_id][phrase].impressions > +0)) { 68 | ctr = +stats[level][campaign_id][phrase].clicks / +stats[level][campaign_id][phrase].impressions; 69 | } 70 | if (+stats[level][campaign_id][phrase].cost_micros > +0) { 71 | cost = +stats[level][campaign_id][phrase].cost_micros / 1000000; 72 | } 73 | if ((+cost > +0) && (+stats[level][campaign_id][phrase].clicks > +0)) { 74 | cpc = +cost / +stats[level][campaign_id][phrase].clicks; 75 | } 76 | if ((+stats[level][campaign_id][phrase].conversions > +0) && (+stats[level][campaign_id][phrase].clicks > +0)) { 77 | conv_rate = +stats[level][campaign_id][phrase].conversions / +stats[level][campaign_id][phrase].clicks; 78 | } 79 | if ((+cost > +0) && (+stats[level][campaign_id][phrase].conversions > +0)) { 80 | cost_per_conv = +cost / +stats[level][campaign_id][phrase].conversions; 81 | } 82 | if ((+stats[level][campaign_id][phrase].conversions_value > +0) && (+cost > +0)) { 83 | conv_value_per_cost = +stats[level][campaign_id][phrase].conversions_value / +cost; 84 | } 85 | if (report[level] == undefined) { 86 | report[level] = []; 87 | } 88 | report[level].push([ 89 | stats[level][campaign_id][phrase].campaign_name, 90 | stats[level][campaign_id][phrase].campaign_id, 91 | stats[level][campaign_id][phrase].phrase, 92 | n, 93 | stats[level][campaign_id][phrase].impressions, 94 | stats[level][campaign_id][phrase].clicks, 95 | ctr, 96 | cost, 97 | cpc, 98 | stats[level][campaign_id][phrase].conversions, 99 | conv_rate, 100 | cost_per_conv, 101 | stats[level][campaign_id][phrase].conversions_value, 102 | conv_value_per_cost 103 | ]); 104 | } 105 | } 106 | } 107 | } 108 | if (level == 'adgroup') { 109 | for (var campaign_id in stats[level]) { 110 | for (var ad_group_id in stats[level][campaign_id]) { 111 | for (var phrase in stats[level][campaign_id][ad_group_id]) { 112 | if ((stats[level][campaign_id][ad_group_id][phrase].clicks >= config().clicks_threshold) && 113 | (stats[level][campaign_id][ad_group_id][phrase].impressions >= config().impressions_threshold)) { 114 | var n = stats[level][campaign_id][ad_group_id][phrase].phrase.toString().split(' ').length; 115 | var ctr = 0, 116 | cost = 0, 117 | cpc = 0, 118 | conv_rate = 0, 119 | cost_per_conv = 0, 120 | conv_value_per_cost = 0; 121 | if ((+stats[level][campaign_id][ad_group_id][phrase].clicks > +0) && (+stats[level][campaign_id][ad_group_id][phrase].impressions > +0)) { 122 | ctr = +stats[level][campaign_id][ad_group_id][phrase].clicks / +stats[level][campaign_id][ad_group_id][phrase].impressions; 123 | } 124 | if (+stats[level][campaign_id][ad_group_id][phrase].cost_micros > +0) { 125 | cost = +stats[level][campaign_id][ad_group_id][phrase].cost_micros / 1000000; 126 | } 127 | if ((+cost > +0) && (+stats[level][campaign_id][ad_group_id][phrase].clicks > +0)) { 128 | cpc = +cost / +stats[level][campaign_id][ad_group_id][phrase].clicks; 129 | } 130 | if ((+stats[level][campaign_id][ad_group_id][phrase].conversions > +0) && (+stats[level][campaign_id][ad_group_id][phrase].clicks > +0)) { 131 | conv_rate = +stats[level][campaign_id][ad_group_id][phrase].conversions / +stats[level][campaign_id][ad_group_id][phrase].clicks; 132 | } 133 | if ((+cost > +0) && (+stats[level][campaign_id][ad_group_id][phrase].conversions > +0)) { 134 | cost_per_conv = +cost / +stats[level][campaign_id][ad_group_id][phrase].conversions; 135 | } 136 | if ((+stats[level][campaign_id][ad_group_id][phrase].conversions_value > +0) && (+cost > +0)) { 137 | conv_value_per_cost = +stats[level][campaign_id][ad_group_id][phrase].conversions_value / +cost; 138 | } 139 | if (report[level] == undefined) { 140 | report[level] = []; 141 | } 142 | report[level].push([ 143 | stats[level][campaign_id][ad_group_id][phrase].campaign_name, 144 | stats[level][campaign_id][ad_group_id][phrase].campaign_id, 145 | stats[level][campaign_id][ad_group_id][phrase].ad_group_name, 146 | stats[level][campaign_id][ad_group_id][phrase].ad_group_id, 147 | stats[level][campaign_id][ad_group_id][phrase].phrase, 148 | n, 149 | stats[level][campaign_id][ad_group_id][phrase].impressions, 150 | stats[level][campaign_id][ad_group_id][phrase].clicks, 151 | ctr, 152 | cost, 153 | cpc, 154 | stats[level][campaign_id][ad_group_id][phrase].conversions, 155 | conv_rate, 156 | cost_per_conv, 157 | stats[level][campaign_id][ad_group_id][phrase].conversions_value, 158 | conv_value_per_cost 159 | ]); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | Logger.log(get_account_name() + ' - Рассчитали финальный отчёт'); 167 | return report 168 | } 169 | -------------------------------------------------------------------------------- /n-gram/common.gs: -------------------------------------------------------------------------------- 1 | function unique(arr) { 2 | var tmp = {}; 3 | return arr.filter(function (a) { 4 | return a in tmp ? 0 : tmp[a] = 1; 5 | }); 6 | } 7 | 8 | function get_account_name() { 9 | return AdsApp.currentAccount().getName(); 10 | } 11 | 12 | function days_ago(days) { 13 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24, 14 | now = new Date(), 15 | fromDate = new Date(now.getTime() - days * MILLIS_PER_DAY), 16 | timeZone = AdsApp.currentAccount().getTimeZone(), 17 | formatDate = Utilities.formatDate(fromDate, timeZone, 'yyyy-MM-dd'); 18 | return formatDate; 19 | } 20 | 21 | function send_slack_message(text) { 22 | var slack_url = config().slack_url, 23 | slack_message = { 24 | "text": text 25 | }; 26 | var options = { 27 | method: 'POST', 28 | contentType: 'application/json', 29 | payload: JSON.stringify(slack_message) 30 | }; 31 | UrlFetchApp.fetch(slack_url, options); 32 | } 33 | -------------------------------------------------------------------------------- /n-gram/config.gs: -------------------------------------------------------------------------------- 1 | function config() { 2 | return { 3 | is_mcc_account: false, 4 | // true - if account is MCC 5 | 6 | editors_mails: [ 7 | 'account_one@gmail.com', 8 | 'account_two@gmail.com' 9 | ], 10 | // Список аккаунтов которые получат доступ к отчёту 11 | 12 | slack_url: 'https://hooks.slack.com/services/12342314132412341234/AGAGAFGRAFGR$/EXAMPLEURLafgjkhafhgafg', 13 | // Url вебхука для отправки сообщения в слак о готовности отчета 14 | 15 | custom_date_range: 180, 16 | // Указываем количество дней для выборки 17 | 18 | custom_date_range_shift: 1, 19 | // Указываем на сколько дней от сегодняшнего мы сдвигаем выборку. Нужно для того чтобы не брать те дни когда запаздывает статистика. 20 | 21 | impressions_threshold: 100, 22 | // Минимальный порог показов для отчета 23 | 24 | clicks_threshold: 100 25 | // Минимальный порог кликов для отчета 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /n-gram/index.gs: -------------------------------------------------------------------------------- 1 | /* =================================================================================== 2 | 3 | Перед началом работы со скриптом его необходимо настроить в файле\функции - config 4 | 5 | =================================================================================== */ 6 | 7 | function main() { 8 | if (config().is_mcc_account) { 9 | mcc_account(); 10 | } else { 11 | account(); 12 | all_finished(); 13 | } 14 | } 15 | 16 | function mcc_account() { 17 | 18 | var account_ids = [], 19 | accountSelector = AdsManagerApp.accounts(), 20 | accountIterator = accountSelector.get(); 21 | while (accountIterator.hasNext()) { 22 | var account = accountIterator.next(), 23 | account_name = account.getName(), 24 | cost = account.getStatsFor('LAST_7_DAYS').getCost(); 25 | if (cost > +0) { 26 | if (account_ids.length < 50) { 27 | account_ids.push(account.getCustomerId()); 28 | } else { 29 | execute_ids(account_ids); 30 | Utilities.sleep(1000); 31 | account_ids = []; 32 | } 33 | } 34 | } 35 | 36 | execute_ids(account_ids); 37 | 38 | function execute_ids(ids) { 39 | var accountSelector = AdsManagerApp.accounts() 40 | .withIds(ids) 41 | .withLimit(50); 42 | accountSelector.executeInParallel('account', 'all_finished'); 43 | } 44 | } 45 | 46 | function all_finished() { 47 | Logger.log('All done!'); 48 | } 49 | -------------------------------------------------------------------------------- /n-gram/negatives.gs: -------------------------------------------------------------------------------- 1 | function get_negatives_by_groups(campaign_ids) { 2 | 3 | var arr = []; 4 | 5 | var group_negatives_query = 'SELECT campaign.id, ' + 6 | 'ad_group.id, ' + 7 | 'ad_group_criterion.keyword.text, ' + 8 | 'ad_group_criterion.keyword.match_type ' + 9 | 'FROM keyword_view ' + 10 | 'WHERE ad_group_criterion.negative = TRUE ' + 11 | 'AND campaign.status = "ENABLED" ' + 12 | 'AND campaign.id IN (' + campaign_ids.join(', ') + ')'; 13 | var group_negatives_result = AdsApp.search(group_negatives_query); 14 | while (group_negatives_result.hasNext()) { 15 | try { 16 | var row = group_negatives_result.next(), 17 | ad_group_id = row.adGroup.id, 18 | campaign_id = row.campaign.id, 19 | keyword_text = row.adGroupCriterion.keyword.text, 20 | keyword_matchtype = row.adGroupCriterion.keyword.matchType; 21 | if (arr[ad_group_id] == undefined) { 22 | arr[ad_group_id] = [[ 23 | keyword_text.toLowerCase(), 24 | keyword_matchtype 25 | ]]; 26 | } else { 27 | arr[ad_group_id].push([ 28 | keyword_text.toLowerCase(), 29 | keyword_matchtype 30 | ]); 31 | } 32 | } catch (e) { 33 | Logger.log(e); 34 | } 35 | } 36 | Logger.log(get_account_name() + ' - Собрали минус-слова для групп'); 37 | return arr 38 | } 39 | 40 | function get_negatives_by_campaign(campaign_ids) { 41 | 42 | var arr = []; 43 | 44 | var campaign_negatives_query = 'SELECT campaign.id, ' + 45 | 'campaign_criterion.keyword.text, ' + 46 | 'campaign_criterion.keyword.match_type ' + 47 | 'FROM campaign_criterion ' + 48 | 'WHERE campaign_criterion.negative = TRUE ' + 49 | 'AND campaign_criterion.type = "KEYWORD" ' + 50 | 'AND campaign.status = "ENABLED" ' + 51 | 'AND campaign.id IN (' + campaign_ids.join(', ') + ')'; 52 | var campaign_negatives_result = AdsApp.search(campaign_negatives_query); 53 | while (campaign_negatives_result.hasNext()) { 54 | try { 55 | var row = campaign_negatives_result.next(), 56 | campaign_id = row.campaign.id, 57 | keyword_text = row.campaignCriterion.keyword.text, 58 | keyword_matchtype = row.campaignCriterion.keyword.matchType; 59 | if (arr[campaign_id] == undefined) { 60 | arr[campaign_id] = [[ 61 | keyword_text.toLowerCase(), 62 | keyword_matchtype 63 | ]]; 64 | } else { 65 | arr[campaign_id].push([ 66 | keyword_text.toLowerCase(), 67 | keyword_matchtype 68 | ]); 69 | } 70 | } catch (e) { 71 | Logger.log(e); 72 | } 73 | } 74 | 75 | // Ищем кампании используюшие общие списки минус-слов 76 | var shared_set_data = [], 77 | shared_set_names = [], 78 | shared_set_campaigns = []; 79 | 80 | var campaign_shared_set_negatives_query = 'SELECT campaign.name, ' + 81 | 'campaign.id, ' + 82 | 'shared_set.name ' + 83 | 'FROM campaign_shared_set ' + 84 | 'WHERE shared_set.type = "NEGATIVE_KEYWORDS" ' + 85 | 'AND shared_set.status = "ENABLED"'; 86 | var campaign_shared_set_negatives_result = AdsApp.search(campaign_shared_set_negatives_query); 87 | while (campaign_shared_set_negatives_result.hasNext()) { 88 | try { 89 | var campaign_shared_set_row = campaign_shared_set_negatives_result.next(), 90 | campaign_shared_set_negatives_campaign_id = campaign_shared_set_row.campaign.id, 91 | campaign_shared_set_negatives_shared_set_name = campaign_shared_set_row.sharedSet.name; 92 | if (shared_set_campaigns[campaign_shared_set_negatives_shared_set_name] == undefined) { 93 | shared_set_campaigns[campaign_shared_set_negatives_shared_set_name] = [campaign_shared_set_negatives_campaign_id]; 94 | } else { 95 | shared_set_campaigns[campaign_shared_set_negatives_shared_set_name].push(campaign_shared_set_negatives_campaign_id); 96 | } 97 | } catch (e) { 98 | Logger.log(e); 99 | } 100 | } 101 | 102 | // Мапим айдишники и имена по общим спискам 103 | 104 | var shared_set_negatives_query = 'SELECT shared_set.name, ' + 105 | 'shared_set.id, ' + 106 | 'shared_set.member_count, ' + 107 | 'shared_set.reference_count, ' + 108 | 'shared_set.type ' + 109 | 'FROM shared_set ' + 110 | 'WHERE shared_set.type = "NEGATIVE_KEYWORDS" ' + 111 | 'AND shared_set.reference_count > 0'; 112 | var shared_set_negatives_result = AdsApp.search(shared_set_negatives_query); 113 | while (shared_set_negatives_result.hasNext()) { 114 | try { 115 | var shared_set_row = shared_set_negatives_result.next(), 116 | shared_set_negatives_campaign_id = shared_set_row.sharedSet.id, 117 | shared_set_negatives_shared_set_name = shared_set_row.sharedSet.name; 118 | shared_set_names[shared_set_negatives_campaign_id] = shared_set_negatives_shared_set_name; 119 | } catch (e) { 120 | Logger.log(e); 121 | } 122 | } 123 | 124 | // Склеиваем минус слова из сетов и кампаний 125 | 126 | var shared_criterion_negatives_query = 'SELECT shared_set.id, ' + 127 | 'shared_criterion.keyword.match_type, ' + 128 | 'shared_criterion.keyword.text, ' + 129 | 'shared_set.name ' + 130 | 'FROM shared_criterion ' + 131 | 'WHERE shared_criterion.type = "KEYWORD" ' + 132 | 'AND shared_set.type = "NEGATIVE_KEYWORDS"'; 133 | var shared_criterion_negatives_result = AdsApp.search(shared_criterion_negatives_query); 134 | while (shared_criterion_negatives_result.hasNext()) { 135 | try { 136 | var shared_criterion_negatives_row = shared_criterion_negatives_result.next(), 137 | shared_criterion_negatives_set_name = shared_criterion_negatives_row.sharedSet.name, 138 | shared_criterion_negatives_keyword_text = shared_criterion_negatives_row.sharedCriterion.keyword.text, 139 | shared_criterion_negatives_keyword_match_type = shared_criterion_negatives_row.sharedCriterion.keyword.matchType; 140 | if (shared_set_campaigns[shared_criterion_negatives_set_name] !== undefined) { 141 | for (var i = 0; i < shared_set_campaigns[shared_criterion_negatives_set_name].length; i++) { 142 | var campaignId = shared_set_campaigns[shared_criterion_negatives_set_name][i]; 143 | if (arr[campaignId] == undefined) { 144 | arr[campaignId] = [[ 145 | shared_criterion_negatives_keyword_text.toLowerCase(), 146 | shared_criterion_negatives_keyword_match_type 147 | ]]; 148 | } else { 149 | arr[campaignId].push([ 150 | shared_criterion_negatives_keyword_text.toLowerCase(), 151 | shared_criterion_negatives_keyword_match_type 152 | ]); 153 | } 154 | } 155 | } 156 | } catch (e) { 157 | Logger.log(e); 158 | } 159 | } 160 | Logger.log(get_account_name() + ' - Собрали минус-слова для кампаний'); 161 | return arr 162 | } 163 | -------------------------------------------------------------------------------- /release/Budget_Control.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | var CONFIG = { 4 | SLACK_URL: 'https://hooks.slack.com/services/xxxxxxxx/zzzzzzzz/yyyyyyyy', 5 | // Вебхук для Слака 6 | 7 | SCRIPT_LABEL: 'Budget_Control' 8 | // Следим за кампаниями помеченными этим ярлыком 9 | }; 10 | 11 | // =================================================================== 12 | 13 | ensureAccountLabels(); // Проверяем и создаем ярлык 14 | 15 | var campaignSelector = AdWordsApp.campaigns() 16 | .withCondition('LabelNames CONTAINS_ANY ["' + CONFIG.SCRIPT_LABEL + '"]'); 17 | var campaignIterator = campaignSelector.get(); 18 | while (campaignIterator.hasNext()) { 19 | var campaign = campaignIterator.next(); 20 | var budget = campaign.getBudget(); 21 | var stats = campaign.getStatsFor('TODAY'); 22 | var cost = parseFloat(stats.getCost()).toFixed(2); 23 | if (budget.isExplicitlyShared() == true) { 24 | var budgetCampaignIterator = budget.campaigns().get(); 25 | var allAssociatedCost = +0; 26 | while (budgetCampaignIterator.hasNext()) { 27 | var associatedCampaign = budgetCampaignIterator.next(); 28 | var associatedCampaignStats = associatedCampaign.getStatsFor('TODAY'); 29 | var associatedCost = associatedCampaignStats.getCost(); 30 | allAssociatedCost = allAssociatedCost + +associatedCost; 31 | } 32 | allAssociatedCost = parseFloat(allAssociatedCost).toFixed(2) 33 | if (allAssociatedCost > budget.getAmount()) { 34 | if (campaign.isEnabled() == true) { 35 | campaign.pause(); 36 | Logger.log('Campaign ' + campaign.getName() + ' paused'); 37 | Logger.log('Budget amount: ' + budget.getAmount()); 38 | Logger.log('Budget spend: ' + allAssociatedCost); 39 | Logger.log('-------------------------------------------------'); 40 | var message = ':double_vertical_bar: В кампании ' + campaign.getName() + ', сегодня расход ' + allAssociatedCost + ' при бюджете ' + budget.getAmount() + '. Кампания остановлена. \n\n'; 41 | sendSlackMessage(message); 42 | } 43 | } else { 44 | if (campaign.isPaused() == true) { 45 | campaign.enable(); 46 | Logger.log('Campaign ' + campaign.getName() + ' enabled'); 47 | Logger.log('Budget amount: ' + budget.getAmount()); 48 | Logger.log('Budget spend: ' + allAssociatedCost); 49 | Logger.log('-------------------------------------------------'); 50 | } 51 | } 52 | } else { 53 | if (cost > budget.getAmount()) { 54 | if (campaign.isEnabled() == true) { 55 | campaign.pause(); 56 | Logger.log('Campaign ' + campaign.getName() + ' paused'); 57 | Logger.log('Budget amount: ' + budget.getAmount()); 58 | Logger.log('Budget spend: ' + cost); 59 | Logger.log('-------------------------------------------------'); 60 | var message = ':double_vertical_bar: В кампании ' + campaign.getName() + ', сегодня расход ' + cost + ' при бюджете ' + budget.getAmount() + '. Кампания остановлена. \n\n'; 61 | sendSlackMessage(message); 62 | } 63 | } else { 64 | if (campaign.isPaused() == true) { 65 | campaign.enable(); 66 | Logger.log('Campaign ' + campaign.getName() + ' enabled'); 67 | Logger.log('Budget amount: ' + budget.getAmount()); 68 | Logger.log('Budget spend: ' + cost); 69 | Logger.log('-------------------------------------------------'); 70 | } 71 | } 72 | } 73 | } 74 | 75 | function ensureAccountLabels() { 76 | function getAccountLabelNames() { 77 | var labelNames = []; 78 | var iterator = AdWordsApp.labels().get(); 79 | while (iterator.hasNext()) { 80 | labelNames.push(iterator.next().getName()); 81 | } 82 | return labelNames; 83 | } 84 | var labelNames = getAccountLabelNames(); 85 | if (labelNames.indexOf(CONFIG.SCRIPT_LABEL) == -1) { 86 | AdWordsApp.createLabel(CONFIG.SCRIPT_LABEL); 87 | } 88 | } 89 | 90 | function sendSlackMessage(text, opt_channel) { 91 | var slackMessage = { 92 | text: text, 93 | icon_url: 'https://www.gstatic.com/images/icons/material/product/1x/adwords_64dp.png', 94 | username: 'AdWords Scripts', 95 | channel: opt_channel || '#adwords' 96 | }; 97 | 98 | var options = { 99 | method: 'POST', 100 | contentType: 'application/json', 101 | payload: JSON.stringify(slackMessage) 102 | }; 103 | UrlFetchApp.fetch(CONFIG.SLACK_URL, options); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /release/GDN_Excluded_Placement.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | // Скрипт добавляет площадки в список исключений по заданным параметрам 4 | 5 | var CONFIG = { 6 | ARPU: 15, 7 | // Средняя выручка на конверсию 8 | 9 | AverageCheck: 190, 10 | // Средняя выручка на конверсию с Value 11 | 12 | customDaysInDateRange: 180, 13 | // Указываем количество дней для выборки 14 | // Если хотим использовать данные о конверсиях или доходности, то в качестве значения следует указывать число больее чем окно конверсии. 15 | 16 | customDateRangeShift: 0 17 | // Указываем на сколько дней от сегодняшнего мы сдвигаем выборку. Нужно для того чтобы не брать те дни когда запаздывает статистика. 18 | // Если хотим использовать данные о конверсиях или доходности, то в качестве значения следует указывать число равное дням в окне конверсии. 19 | }; 20 | 21 | //====================================================================== 22 | 23 | var REPORTING_OPTIONS = { 24 | // Comment out the following line to default to the latest reporting version. 25 | apiVersion: 'v201705' 26 | }; 27 | 28 | var campaignPerfomaceAWQL = 'SELECT CampaignName, CampaignId ' + 29 | 'FROM CAMPAIGN_PERFORMANCE_REPORT ' + 30 | 'WHERE CampaignStatus = ENABLED AND AdvertisingChannelType = DISPLAY ' + 31 | 'AND Cost > ' + (CONFIG.ARPU * 1000000) + ' ' + 32 | 'DURING ' + customDateRange(); 33 | var campaignPerfomaceRowsIter = AdWordsApp.report(campaignPerfomaceAWQL, REPORTING_OPTIONS).rows(); 34 | while (campaignPerfomaceRowsIter.hasNext()) { 35 | var CampaignRow = campaignPerfomaceRowsIter.next(); 36 | var CampaignName = CampaignRow['CampaignName']; 37 | var CampaignId = CampaignRow['CampaignId']; 38 | if (CampaignRow) { 39 | getAdGroups(); 40 | } 41 | } 42 | 43 | function getAdGroups() { 44 | var AdGroupPerfomanceAWQL = 'SELECT AdGroupName, AdGroupId ' + 45 | 'FROM ADGROUP_PERFORMANCE_REPORT ' + 46 | 'WHERE CampaignId = ' + CampaignId + ' AND AdGroupStatus = ENABLED ' + 47 | 'AND Cost > ' + (CONFIG.ARPU * 1000000) + ' ' + 48 | 'DURING ' + customDateRange(); 49 | var AdGroupPerfomancerowsIter = AdWordsApp.report(AdGroupPerfomanceAWQL, REPORTING_OPTIONS).rows(); 50 | while (AdGroupPerfomancerowsIter.hasNext()) { 51 | var AdGroupRow = AdGroupPerfomancerowsIter.next(); 52 | var AdGroupName = AdGroupRow['AdGroupName']; 53 | var AdGroupId = AdGroupRow['AdGroupId']; 54 | if (AdGroupRow) { 55 | Logger.log('CampaignName: ' + CampaignName + ' AdGroupName: ' + AdGroupName); 56 | placemetsBlock(); 57 | } 58 | } 59 | 60 | function placemetsBlock() { 61 | var AWQL = 'SELECT Criteria, Clicks, Conversions, Cost, ConversionValue, CostPerConversion, IsNegative ' + 62 | 'FROM PLACEMENT_PERFORMANCE_REPORT ' + 63 | 'WHERE CampaignId = ' + CampaignId + ' AND AdGroupId = ' + AdGroupId + ' AND IsNegative = FALSE ' + 64 | 'AND Cost > ' + (CONFIG.ARPU * 1000000) + ' ' + 65 | 'DURING ' + customDateRange(); // 66 | var rowsIter = AdWordsApp.report(AWQL).rows(); 67 | while (rowsIter.hasNext()) { // определяем исключаемые площадки 68 | var row = rowsIter.next(), 69 | Domain = row['Criteria'].toString(), 70 | Clicks = parseFloat(row['Clicks']).toFixed(), 71 | Conversions = parseFloat(row['Conversions']).toFixed(2), 72 | ConversionValue = parseFloat(row['ConversionValue']).toFixed(), 73 | CostPerConversion = parseFloat(row['CostPerConversion']).toFixed(2), 74 | Cost = parseFloat(row['Cost']).toFixed(2), 75 | IsNegative = row['IsNegative'].toString(), 76 | CustomConversionRate = ((Conversions / Clicks) * 100).toFixed(2); 77 | if (ConversionValue == +0) { 78 | if ((Conversions < 0.01) && (Cost > (CONFIG.ARPU * 3))) { // на 2 ARPU нет конверсий 79 | addNegativeKeywordToAdGroup(Domain); 80 | Logger.log('Исключаем ' + Domain + ' - нет конверсий при большом расходе (больше 2-х ARPU).'); 81 | Logger.log('Клики - ' + Clicks + 82 | ' Конверсии - ' + Conversions + 83 | ' Конверсия(%) - ' + CustomConversionRate + 84 | ' Стоимость конверсии - ' + CostPerConversion + 85 | ' Расход - ' + Cost + 86 | ' Доход с площадки - ' + ConversionValue); 87 | Logger.log('------------------------------------'); 88 | } 89 | if ((Conversions > +0) && (Cost > (CONFIG.ARPU * 3)) && (Conversions < 3) && (CostPerConversion > (CONFIG.ARPU * 2))) { // 1-2 сильно дорогие конверсии 90 | addNegativeKeywordToAdGroup(Domain); 91 | Logger.log('Исключаем ' + Domain + ' - конверсий мало, а те что есть очень дорогие.'); 92 | Logger.log('Клики - ' + Clicks + 93 | ' Конверсии - ' + Conversions + 94 | ' Конверсия(%) - ' + CustomConversionRate + 95 | ' Стоимость конверсии - ' + CostPerConversion + 96 | ' Расход - ' + Cost + 97 | ' Доход с площадки - ' + ConversionValue); 98 | Logger.log('------------------------------------'); 99 | } 100 | 101 | if ((Conversions > 2) && (Cost > (CONFIG.ARPU * 2)) && (CostPerConversion > CONFIG.ARPU)) { // конверсий много, но они дороже ARPU 102 | addNegativeKeywordToAdGroup(Domain); 103 | Logger.log('Исключаем ' + Domain + ' - конверсий много, но они дороже ARPU.'); 104 | Logger.log('Клики - ' + Clicks + 105 | ' Конверсии - ' + Conversions + 106 | ' Конверсия(%) - ' + CustomConversionRate + 107 | ' Стоимость конверсии - ' + CostPerConversion + 108 | ' Расход - ' + Cost + 109 | ' Доход с площадки - ' + ConversionValue); 110 | Logger.log('------------------------------------'); 111 | } 112 | if ((Cost > CONFIG.AverageCheck)) { // ROI = 0 113 | addNegativeKeywordToAdGroup(Domain); 114 | Logger.log('Исключаем ' + Domain + ' - ROI = 0.'); 115 | Logger.log('Клики - ' + Clicks + 116 | ' Конверсии - ' + Conversions + 117 | ' Конверсия(%) - ' + CustomConversionRate + 118 | ' Стоимость конверсии - ' + CostPerConversion + 119 | ' Расход - ' + Cost + 120 | ' Доход с площадки - ' + ConversionValue); 121 | Logger.log('------------------------------------'); 122 | } 123 | } 124 | } 125 | } 126 | 127 | function addNegativeKeywordToAdGroup(negativePlacement) { // исключаем площадки 128 | Logger.log(negativePlacement); 129 | var adGroupIterator = AdWordsApp.adGroups() 130 | .withCondition('CampaignId = ' + CampaignId) 131 | .withCondition('AdGroupId = ' + AdGroupId) 132 | .get(); 133 | if (adGroupIterator.hasNext()) { 134 | var adGroup = adGroupIterator.next(); 135 | var placementIterator = AdWordsApp.display().placements() 136 | .withCondition('CampaignId = ' + CampaignId) 137 | .withCondition('AdGroupId = ' + AdGroupId) 138 | .withCondition('Status = ENABLED') 139 | .withCondition('PlacementUrl = ' + negativePlacement) 140 | .get(); 141 | while (placementIterator.hasNext()) { 142 | var placement = placementIterator.next(); 143 | placement.remove(); 144 | } 145 | var placementBuilder = adGroup.display().newPlacementBuilder() 146 | .withUrl(negativePlacement) // required 147 | .exclude(); // create the placement 148 | } 149 | } 150 | } 151 | 152 | function customDateRange(select) { // Формируем значение параметра временного диапазона для выборки AWQL 153 | var timeType = select, 154 | MILLIS_PER_DAY = 1000 * 60 * 60 * 24, 155 | now = new Date(), 156 | fromDate = new Date(now.getTime() - (CONFIG.customDaysInDateRange + CONFIG.customDateRangeShift) * MILLIS_PER_DAY), 157 | toDate = new Date(now.getTime() - CONFIG.customDateRangeShift * MILLIS_PER_DAY), 158 | nowDate = new Date(now.getTime()), 159 | timeZone = AdWordsApp.currentAccount().getTimeZone(), 160 | fromformatDate = Utilities.formatDate(fromDate, timeZone, 'yyyyMMdd'), 161 | toformatDate = Utilities.formatDate(toDate, timeZone, 'yyyyMMdd'), 162 | nowformatDate = Utilities.formatDate(nowDate, timeZone, 'yyyyMMdd'), 163 | duringDates = fromformatDate + ',' + toformatDate; 164 | return duringDates; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /release/GDN_Excluded_Placement_ByName.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | var CONFIG = { 4 | exclude: ['job', 'gta', 'game', 'blogspot', 'faucet', 'satoshi', 'dota', 'minecraft', 'flash', 'apk', 'android', 'mp3', 'fb2', 'farm', 'dating', 'astro', 'film', 'video', 'movie', 'book', 'download', 'torrent', 'kino', 'radio', 'weather', 'chords', 'zodiak', 'recept', 'recipe', 'spongebob', 'barbie', 'skyrim', 'ferma', 'mafia', 'mario', 'epub', '2048', 'dendy', 'sega', 'zuma', 'pdf', 'simulat', 'mods', 'play', 'spintires', 'spin-tires'], 5 | // Площадки содержаище любое из этих значений должны быть исключены 6 | 7 | period: 'LAST_7_DAYS', 8 | // Анализируем площадки у которых были показы за указанный период 9 | // ALL_TIME, LAST_7_DAYS, LAST_WEEK, LAST_MONTH, LAST_14_DAYS, LAST_30_DAYS, LAST_BUSINESS_WEEK, THIS_WEEK_SUN_TODAY, THIS_WEEK_MON_TODAY, LAST_WEEK_SUN_SAT, THIS_MONTH 10 | 11 | EXCLUDED_PLACEMENT_LIST_NAME: 'Trash' 12 | // В какой список будут складываться исключенные площадки 13 | } 14 | 15 | var placementArray = []; 16 | 17 | var AWQL = 'SELECT Domain, CampaignId ' + 18 | 'FROM AUTOMATIC_PLACEMENTS_PERFORMANCE_REPORT ' + 19 | 'WHERE Impressions > 0 AND Conversions < 1 ' + 20 | 'DURING ' + CONFIG.period; 21 | 22 | var report = AdWordsApp.report(AWQL); 23 | var rows = report.rows(); 24 | while (rows.hasNext()) { 25 | var row = rows.next(); 26 | var Domain = row['Domain'].toString(); 27 | var CampaignId = row['CampaignId']; 28 | if (containsAny(Domain, CONFIG.exclude)) { 29 | if (Domain.indexOf('mobileapp::') != -1) { 30 | Domain = Domain.replace(/mobileapp::/, '').replace(/2\-com/, 'com').replace(/1\-com/, 'com') + '.adsenseformobileapps.com'; 31 | } 32 | placementArray.push(Domain); 33 | } 34 | } 35 | addNegativeKeywordToList(placementArray); 36 | 37 | function addNegativeKeywordToList(negativePlacements) { // исключаем площадки 38 | var excludedPlacementListIterator = AdWordsApp.excludedPlacementLists() 39 | .withCondition('Name = ' + CONFIG.EXCLUDED_PLACEMENT_LIST_NAME) 40 | .get(); 41 | if (excludedPlacementListIterator.totalNumEntities() == 1) { 42 | var excludedPlacementList = excludedPlacementListIterator.next() 43 | .addExcludedPlacements(negativePlacements); 44 | } else { 45 | AdWordsApp.newExcludedPlacementListBuilder() 46 | .withName(CONFIG.EXCLUDED_PLACEMENT_LIST_NAME) 47 | .build() 48 | .getResult() 49 | .addExcludedPlacements(negativePlacements); 50 | } 51 | } 52 | 53 | function containsAny(str, arr) { 54 | for (var i = 0; i != arr.length; i++) { 55 | var substring = arr[i]; 56 | if (str.indexOf(substring) != -1) { 57 | return true; 58 | } 59 | } 60 | return false; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /release/Search_AdGroup_CrossKeys.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | var CONFIG = { 4 | ScriptLabel: 'Сross_Keys' 5 | // Ярлык для обрабатываемых кампаний 6 | } 7 | 8 | //-------------------------------------------------------------- 9 | 10 | var adGroupIdsList = []; 11 | Logger.log('Собираем все ключевые слова'); 12 | var keywordsData = getData(); // Собираем все ключевые слова 13 | 14 | Logger.log('Выделяем минус-слова из пересекающихся'); 15 | var negativeKeys = keysCross(keywordsData); // Выделяем минус-слова из пересекающихся 16 | 17 | Logger.log('Группируем минус-слова по группам'); 18 | var groupedNegatives = keysByGroups(negativeKeys); // Группируем минус-слова по группам 19 | 20 | Logger.log('Исключаем минус-слова из групп'); 21 | excludeKeys(groupedNegatives); // Исключаем минус-слова из групп 22 | 23 | function getData() { 24 | var result = []; 25 | var campaignSelector = AdWordsApp.campaigns() 26 | .withCondition('LabelNames CONTAINS_ANY ["' + CONFIG.ScriptLabel + '"]') 27 | .withCondition('Status = ENABLED'); 28 | var campaignIterator = campaignSelector.get(); 29 | while (campaignIterator.hasNext()) { 30 | var campaign = campaignIterator.next(); 31 | var adGroupSelector = campaign.adGroups() 32 | .withCondition('AdGroupStatus = ENABLED'); 33 | var adGroupIterator = adGroupSelector.get(); 34 | while (adGroupIterator.hasNext()) { 35 | var adGroup = adGroupIterator.next(); 36 | var adGroupId = adGroup.getId(); 37 | adGroupIdsList.push(adGroupId); 38 | var keywordSelector = adGroup.keywords() 39 | .withCondition('KeywordMatchType = BROAD'); 40 | var keywordIterator = keywordSelector.get(); 41 | while (keywordIterator.hasNext()) { 42 | var keyWord = keywordIterator.next(); 43 | var keyWordText = keyWord.getText().toString().replace(/\+/g, ''); 44 | result.push({ 45 | adGroupId: adGroupId, 46 | keyWordText: keyWordText 47 | }); 48 | } 49 | }; 50 | } 51 | return result; 52 | } 53 | 54 | function keysCross(arr) { 55 | var result = []; 56 | arr.forEach(function (keyOne) { 57 | var wordsOne = keyOne.keyWordText.split(' '); 58 | arr.forEach(function (keyTwo) { 59 | var wordsTwo = keyTwo.keyWordText.split(' '); 60 | if (keyOne.adGroupId != keyTwo.adGroupId) { 61 | if ((keyOne.keyWordText.indexOf(keyTwo.keyWordText) != -1) && (keyOne.keyWordText != keyTwo.keyWordText)) { 62 | Array.prototype.diff = function (a) { 63 | return this.filter(function (i) { 64 | return !(a.indexOf(i) > -1); 65 | }); 66 | }; 67 | var diffWords = wordsOne.diff(wordsTwo); 68 | if (diffWords.length > +0) { 69 | diffWords.forEach(function (negativekey) { 70 | result.push({ 71 | adGroupId: keyTwo.adGroupId, 72 | negativeKey: negativekey 73 | }); 74 | }); 75 | } 76 | } 77 | } 78 | }); 79 | }); 80 | result = unique(result); 81 | return result; 82 | } 83 | 84 | function keysByGroups(arr) { 85 | var result = []; 86 | adGroupIdsList.forEach(function (id) { 87 | var tmp = { 88 | adGroupId: id, 89 | negativeKeys: [] 90 | } 91 | arr.forEach(function (line) { 92 | if (line.adGroupId == id) { 93 | tmp.negativeKeys.push(line.negativeKey) 94 | } 95 | }); 96 | result.push(tmp); 97 | }); 98 | return result; 99 | } 100 | 101 | function excludeKeys(arr) { 102 | arr.forEach(function (line) { 103 | var adGroupSelector = AdWordsApp.adGroups() 104 | .withCondition('Id = ' + line.adGroupId); 105 | var adGroupIterator = adGroupSelector.get(); 106 | if (adGroupIterator.hasNext()) { 107 | var adGroup = adGroupIterator.next(); 108 | var campaign = adGroup.getCampaign(); 109 | Logger.log('Кампания: ' + campaign.getName() + ', Группа объявлений: ' + adGroup.getName()); 110 | line.negativeKeys.forEach(function(negativeKey) { 111 | adGroup.createNegativeKeyword(negativeKey); 112 | Logger.log('Добавлено минус-слово: ' + negativeKey); 113 | }); 114 | } 115 | Logger.log('--------------------------------------------------------------'); 116 | }); 117 | } 118 | 119 | function unique(arr) { // убираем повторы 120 | var tmp = {}; 121 | return arr.filter(function (a) { 122 | return a in tmp ? 0 : tmp[a] = 1; 123 | }); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /release/Search_Ads_Optimizer.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | var CONFIG = { 4 | customDaysInDateRange: 365, 5 | // Указываем количество дней для выборки 6 | // Если хотим использовать данные о конверсиях или доходности, то в качестве значения следует указывать число больее чем окно конверсии. 7 | 8 | customDateRangeShift: 0 9 | // Указываем на сколько дней от сегодняшнего мы сдвигаем выборку. Нужно для того чтобы не брать те дни когда запаздывает статистика. 10 | // Если хотим использовать данные о конверсиях или доходности, то в качестве значения следует указывать число равное дням в окне конверсии. 11 | }; 12 | 13 | //=========================================================== 14 | 15 | var REPORTING_OPTIONS = { 16 | // Comment out the following line to default to the latest reporting version. 17 | apiVersion: 'V201806' 18 | }; 19 | 20 | var campaignPerfomaceAWQL = 'SELECT CampaignName, CampaignId ' + 21 | 'FROM CAMPAIGN_PERFORMANCE_REPORT ' + 22 | 'WHERE CampaignStatus = ENABLED AND AdvertisingChannelType = SEARCH AND Clicks > 100 ' + 23 | 'DURING ' + customDateRange(); 24 | var campaignPerfomaceRowsIter = AdWordsApp.report(campaignPerfomaceAWQL, REPORTING_OPTIONS).rows(); 25 | while (campaignPerfomaceRowsIter.hasNext()) { 26 | var CampaignRow = campaignPerfomaceRowsIter.next(), 27 | CampaignName = CampaignRow['CampaignName'], 28 | CampaignId = CampaignRow['CampaignId']; 29 | if (CampaignRow) { 30 | getAdGroups(); 31 | } 32 | } 33 | 34 | function getAdGroups() { 35 | var AdGroupPerfomanceAWQL = 'SELECT AdGroupName, AdGroupId ' + 36 | 'FROM ADGROUP_PERFORMANCE_REPORT ' + 37 | 'WHERE CampaignId = ' + CampaignId + ' AND AdGroupStatus = ENABLED AND Clicks > 100 ' + 38 | 'DURING ' + customDateRange(); 39 | var AdGroupPerfomancerowsIter = AdWordsApp.report(AdGroupPerfomanceAWQL, REPORTING_OPTIONS).rows(); 40 | while (AdGroupPerfomancerowsIter.hasNext()) { 41 | var AdGroupRow = AdGroupPerfomancerowsIter.next(), 42 | AdGroupName = AdGroupRow['AdGroupName'], 43 | AdGroupId = AdGroupRow['AdGroupId']; 44 | if (AdGroupRow) { 45 | Logger.log('CampaignName: ' + CampaignName + ' AdGroupName: ' + AdGroupName); 46 | var statsForAdsInGroup = getAds(); 47 | shuffle(statsForAdsInGroup); 48 | } 49 | } 50 | 51 | function getAds() { 52 | var adsStats = []; 53 | var adsAWQL = 'SELECT Id, Status, Clicks, Conversions, Cost ' + 54 | 'FROM AD_PERFORMANCE_REPORT ' + 55 | 'WHERE CampaignId = ' + CampaignId + ' AND AdGroupId = ' + AdGroupId + ' AND Status != DISABLED ' + 56 | 'DURING ' + customDateRange(); 57 | var adsRowsIter = AdWordsApp.report(adsAWQL, REPORTING_OPTIONS).rows(); 58 | while (adsRowsIter.hasNext()) { 59 | var adsRow = adsRowsIter.next(), 60 | adsId = adsRow['Id'].toString(), 61 | adsStatus = adsRow['Status'].toString(), 62 | adsClicks = parseFloat(adsRow['Clicks']).toFixed(), 63 | adsConversions = parseFloat(adsRow['Conversions']).toFixed(2), 64 | adsCost = parseFloat(adsRow['Cost']).toFixed(2) * 1000000; 65 | var adsCostPerConversion = +0; 66 | if (adsConversions > 0) { 67 | adsCostPerConversion = parseFloat(adsCost / adsConversions).toFixed(2); 68 | } 69 | if (adsRow) { 70 | var statsRow = { 71 | IdCol: adsId, 72 | StatusCol: adsStatus, 73 | ClicksCol: adsClicks, 74 | ConversionsCol: adsConversions, 75 | CostCol: adsCost, 76 | CostPerConversionCol: adsCostPerConversion, 77 | }; 78 | adsStats[adsStats.length] = statsRow; 79 | if (adsStatus == 'enabled') { 80 | toggleAdsInAdGroup(adsId, 'pause'); 81 | Logger.log('Выключаем объявление: ' + adsId); 82 | } 83 | } 84 | } 85 | return adsStats; 86 | } 87 | 88 | function shuffle(array) { 89 | var stats = array, 90 | clicksStat = [], 91 | conversionCostStat = [], 92 | lowestClicks = +0, 93 | bestConversionCost = +0; 94 | for (var i = 0; i < stats.length; i++) { 95 | var statsRow = stats[i], 96 | clicks = statsRow['ClicksCol'], 97 | conversionCost = statsRow['CostPerConversionCol']; 98 | clicksStat[clicksStat.length] = clicks; 99 | conversionCostStat[conversionCostStat.length] = conversionCost; 100 | } 101 | lowestClicks = getMinOfArray(clicksStat); 102 | bestConversionCost = getMinOfArray(conversionCostStat); 103 | 104 | if (lowestClicks < 100) { 105 | for (var i = 0; i < stats.length; i++) { 106 | var statsRow = stats[i], 107 | clicks = statsRow['ClicksCol']; 108 | if (clicks == lowestClicks) { 109 | toggleAdsInAdGroup(statsRow['IdCol'], 'enable'); 110 | Logger.log('Включаем объявление: ' + statsRow['IdCol']); 111 | } 112 | } 113 | } 114 | if (lowestClicks > 100) { 115 | for (var i = 0; i < stats.length; i++) { 116 | var statsRow = stats[i], 117 | conversionCost = statsRow['CostPerConversionCol']; 118 | if (conversionCost == bestConversionCost) { 119 | toggleAdsInAdGroup(statsRow['IdCol'], 'enable'); 120 | Logger.log('Включаем лучшее объявление: ' + statsRow['IdCol']); 121 | } 122 | } 123 | } 124 | } 125 | 126 | function toggleAdsInAdGroup(adId, status) { 127 | var adGroupIterator = AdWordsApp.adGroups() 128 | .withCondition('AdGroupId = ' + AdGroupId) 129 | .get(); 130 | if (adGroupIterator.hasNext()) { 131 | var adGroup = adGroupIterator.next(), 132 | adsIterator = adGroup.ads() 133 | .withCondition('Id = ' + adId) 134 | .get(); 135 | while (adsIterator.hasNext()) { 136 | var ad = adsIterator.next(); 137 | if (status == 'pause') { 138 | ad.pause(); 139 | } 140 | if (status == 'enable') { 141 | ad.enable(); 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | function getMinOfArray(numArray) { 149 | return Math.min.apply(null, numArray); 150 | } 151 | 152 | function customDateRange() { // Формируем значение параметра временного диапазона для выборки AWQL 153 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24, 154 | now = new Date(), 155 | fromDate = new Date(now.getTime() - (CONFIG.customDaysInDateRange + CONFIG.customDateRangeShift) * MILLIS_PER_DAY), 156 | toDate = new Date(now.getTime() - CONFIG.customDateRangeShift * MILLIS_PER_DAY), 157 | nowDate = new Date(now.getTime()), 158 | timeZone = AdWordsApp.currentAccount().getTimeZone(), 159 | fromformatDate = Utilities.formatDate(fromDate, timeZone, 'yyyyMMdd'), 160 | toformatDate = Utilities.formatDate(toDate, timeZone, 'yyyyMMdd'), 161 | nowformatDate = Utilities.formatDate(nowDate, timeZone, 'yyyyMMdd'), 162 | duringDates = fromformatDate + ',' + toformatDate; 163 | return duringDates; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /release/Search_Bidder.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | var CONFIG = { 4 | ARPU: 9, 5 | // APRU на пользователя 6 | 7 | AverageCheck: 180, 8 | // Средний чек на пользователя 9 | 10 | minPosition: 2, 11 | // Минимально удерживаемая позиция 12 | }; 13 | 14 | ARPU = (CONFIG.ARPU/3).toFixed(2); 15 | AverageCheck = (CONFIG.AverageCheck/3).toFixed(2); 16 | 17 | var campaignPerfomaceAWQL = 'SELECT CampaignName, CampaignId ' + 18 | 'FROM CAMPAIGN_PERFORMANCE_REPORT ' + 19 | 'WHERE CampaignStatus = ENABLED AND AdvertisingChannelType = SEARCH AND BiddingStrategyType = MANUAL_CPC ' + 20 | 'AND CampaignName DOES_NOT_CONTAIN_IGNORE_CASE DSA AND CampaignName DOES_NOT_CONTAIN "[" ' + 21 | 'DURING TODAY'; 22 | var campaignPerfomaceRowsIter = AdWordsApp.report(campaignPerfomaceAWQL).rows(); 23 | while (campaignPerfomaceRowsIter.hasNext()) { 24 | var CampaignRow = campaignPerfomaceRowsIter.next(), 25 | CampaignName = CampaignRow['CampaignName'], 26 | CampaignId = CampaignRow['CampaignId']; 27 | if (CampaignRow) { 28 | getAdGroups(); 29 | } 30 | } 31 | 32 | function getAdGroups() { 33 | var adGroupPerfomanceAWQL = 'SELECT AdGroupName, AdGroupId ' + 34 | 'FROM ADGROUP_PERFORMANCE_REPORT ' + 35 | 'WHERE CampaignId = ' + CampaignId + ' AND AdGroupStatus = ENABLED ' + 36 | 'DURING TODAY'; 37 | var adGroupPerfomanceRowsIter = AdWordsApp.report(adGroupPerfomanceAWQL).rows(); 38 | while (adGroupPerfomanceRowsIter.hasNext()) { 39 | var AdGroupRow = adGroupPerfomanceRowsIter.next(), 40 | AdGroupName = AdGroupRow['AdGroupName'], 41 | AdGroupId = AdGroupRow['AdGroupId']; 42 | if (AdGroupRow != undefined) { 43 | Logger.log('Campaign: ' + CampaignName + ', Ad Group: ' + AdGroupName); 44 | lowPosition(); 45 | firstPage(); 46 | Logger.log('-----------------------------------------------------------------------------------------'); 47 | } 48 | } 49 | 50 | function lowPosition() { 51 | var keywordIterator = AdWordsApp.keywords() 52 | .withCondition('CampaignId = ' + CampaignId) 53 | .withCondition('AdGroupId = ' + AdGroupId) 54 | .withCondition('AveragePosition > ' + CONFIG.minPosition) 55 | .withCondition('Impressions > ' + nowDateFormatted) 56 | .withCondition('Status = ENABLED') 57 | .forDateRange('TODAY') 58 | .get(); 59 | if (keywordIterator.hasNext()) { 60 | while (keywordIterator.hasNext()) { 61 | var keyword = keywordIterator.next(), 62 | keyStrategy = keyword.bidding().getStrategyType().toString(), 63 | keywordCpc = parseFloat(keyword.bidding().getCpc()).toFixed(2); 64 | if (keyStrategy == 'MANUAL_CPC') { 65 | keyword.bidding().setCpc(bidCpc(keywordCpc)); 66 | Logger.log('Повышаем позицию'); 67 | Logger.log('Keyword: ' + keyword.getText() + ' OldCPC: ' + keywordCpc + ' NewCPC: ' + bidCpc(keywordCpc)); 68 | } 69 | } 70 | } 71 | } 72 | 73 | function firstPage() { 74 | var keywordIterator = AdWordsApp.keywords() 75 | .withCondition('CampaignId = ' + CampaignId) 76 | .withCondition('AdGroupId = ' + AdGroupId) 77 | .withCondition('Status = ENABLED') 78 | .get(); 79 | if (keywordIterator.hasNext()) { 80 | while (keywordIterator.hasNext()) { 81 | var keyword = keywordIterator.next(), 82 | keyStrategy = keyword.bidding().getStrategyType().toString(), 83 | keywordFirstPageCpc = parseFloat(keyword.getFirstPageCpc()).toFixed(2), 84 | keywordCpc = parseFloat(keyword.bidding().getCpc()).toFixed(2); 85 | if (keyStrategy == 'MANUAL_CPC') { 86 | if (keywordFirstPageCpc > keywordCpc) { 87 | keyword.bidding().setCpc(bidCpc(keywordCpc)); 88 | Logger.log('Выводим на 1-ю страницу'); 89 | Logger.log('Keyword: ' + keyword.getText() + ' OldCPC: ' + keywordCpc + ' NewCPC: ' + bidCpc(keywordCpc)); 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | function bidCpc(cpc) { 98 | var oldBid = cpc; 99 | if (oldBid > 0) { 100 | // do nothing 101 | } else { 102 | oldBid = 1; 103 | } 104 | var newBid = oldBid * 1.1; 105 | if (newBid > ARPU) { 106 | newBid = ARPU; 107 | } 108 | return newBid.toFixed(2); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /release/Search_Google_Keywords_Mining.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | // Settings 4 | 5 | var CONFIG = { 6 | targetCampaign: 'Search_New_Mining', 7 | // Целевая кампания 8 | 9 | scriptLabel: 'Key_Parser', 10 | // Ярлык которым скрипт помечает созданные слова 11 | 12 | impressionsThreshold: '10' 13 | // Минимальный порог показов для исходных ключевых слов 14 | }; 15 | 16 | var REPORTING_OPTIONS = { 17 | // Comment out the following line to default to the latest reporting version. 18 | apiVersion: 'v201705' 19 | }; 20 | 21 | //--------------------------------------------------------------------------------------------------------- 22 | 23 | ensureAccountLabels(); // Проверяем и создаем ярлыки 24 | 25 | var campaignPerfomaceAWQL = 'SELECT CampaignName, CampaignId ' + 26 | 'FROM CAMPAIGN_PERFORMANCE_REPORT ' + 27 | 'WHERE AdvertisingChannelType = SEARCH ' + 28 | 'AND CampaignName = ' + CONFIG.targetCampaign + ' ' + 29 | 'DURING TODAY'; 30 | var campaignPerfomaceRowsIter = AdWordsApp.report(campaignPerfomaceAWQL, REPORTING_OPTIONS).rows(); 31 | Logger.log(campaignPerfomaceAWQL); 32 | while (campaignPerfomaceRowsIter.hasNext()) { 33 | var CampaignRow = campaignPerfomaceRowsIter.next(), 34 | CampaignName = CampaignRow['CampaignName'], 35 | CampaignId = CampaignRow['CampaignId']; 36 | if (CampaignRow) { 37 | var negativesListFromCampaign = getCampaignNegatives(); 38 | var campaignSettings = getCampaignSettings(); 39 | var googleSettings = setGoogleSettings(campaignSettings); 40 | var domainsList = allDomains(); 41 | var langsList = allLangs(); 42 | getAdGroups(); 43 | } 44 | } 45 | 46 | function getAdGroups() { 47 | var adGroupPerfomanceAWQL = 'SELECT AdGroupName, AdGroupId ' + 48 | 'FROM ADGROUP_PERFORMANCE_REPORT ' + 49 | 'WHERE CampaignId = ' + CampaignId + ' AND AdGroupStatus = ENABLED ' + 50 | 'DURING TODAY'; 51 | var adGroupPerfomanceRowsIter = AdWordsApp.report(adGroupPerfomanceAWQL, REPORTING_OPTIONS).rows(); 52 | while (adGroupPerfomanceRowsIter.hasNext()) { 53 | var AdGroupRow = adGroupPerfomanceRowsIter.next(); 54 | var AdGroupName = AdGroupRow['AdGroupName']; 55 | var AdGroupId = AdGroupRow['AdGroupId']; 56 | if (AdGroupRow) { 57 | var negativeKeywords = getNegativeKeywordForAdGroup(); 58 | Logger.log('Минус-слов: ' + negativeKeywords.length); 59 | getKeywords(); 60 | } 61 | } 62 | 63 | function getNegativeKeywordForAdGroup() { 64 | var result = []; 65 | var adGroupIterator = AdWordsApp.adGroups() 66 | .withCondition('AdGroupId = ' + AdGroupId) 67 | .get(); 68 | if (adGroupIterator.hasNext()) { 69 | var adGroup = adGroupIterator.next(); 70 | var negativeKeywordIterator = adGroup.negativeKeywords() 71 | .get(); 72 | while (negativeKeywordIterator.hasNext()) { 73 | var negativeKeyword = negativeKeywordIterator.next(); 74 | if (negativeKeyword.getMatchType() == 'BROAD') { 75 | result.push(negativeKeyword.getText().toString()); 76 | } 77 | } 78 | } 79 | result = result.concat(negativesListFromCampaign, result); 80 | return result; 81 | } 82 | 83 | function getKeywords() { // Получаем ключи для обработки 84 | var keywordSelector = AdWordsApp.keywords() 85 | .withCondition('CampaignId = ' + CampaignId) 86 | .withCondition('AdGroupId = ' + AdGroupId) 87 | .withCondition('Status != REMOVED') 88 | .withCondition('LabelNames CONTAINS_NONE ["' + CONFIG.scriptLabel + '"]') 89 | .withCondition('KeywordMatchType = BROAD') 90 | .withCondition('Impressions >= ' + CONFIG.impressionsThreshold) 91 | .orderBy('Impressions DESC') 92 | .forDateRange('LAST_30_DAYS'); 93 | var keywordIterator = keywordSelector.get(); 94 | while (keywordIterator.hasNext()) { 95 | var keyword = keywordIterator.next(); 96 | var keywordtext = keyword.getText().toString().replace(/\+/g, ''); 97 | Logger.log(keywordtext); 98 | for (var q = 0; q < domainsList.length; q++) { 99 | var domain = domainsList[q]; 100 | Logger.log(domain); 101 | for (var z = 0; z < langsList.length; z++) { 102 | var alphabet = langsList[z]; 103 | var serp = queryKeyword(keywordtext, domain, alphabet); // Собираем ключи 104 | } 105 | } 106 | keyword.applyLabel(CONFIG.scriptLabel); 107 | } 108 | } 109 | 110 | function queryKeyword(keyword, url, letters) { 111 | var alphabet = letters; 112 | var primary = []; 113 | primary.push(keyword); 114 | alphabet.forEach(function (letter) { 115 | var wordPlusOneLetter = keyword + ' ' + letter; 116 | primary.push(wordPlusOneLetter); 117 | }); 118 | var secondary = []; 119 | primary.forEach(function (line) { 120 | if (line != keyword) { 121 | var querykeyword = encodeURIComponent(line); 122 | var clearedphrases = keysFetch(querykeyword); 123 | addingKeywords(clearedphrases); // Добавляем новые ключевые слова 124 | if (clearedphrases.length > +9) { 125 | alphabet.forEach(function (letter) { 126 | var wordPlusTwoLetter = line + letter; 127 | secondary.push(wordPlusTwoLetter); 128 | }); 129 | } 130 | } 131 | }); 132 | secondary.forEach(function (line) { 133 | if (line != keyword) { 134 | var querykeyword = encodeURIComponent(line); 135 | var clearedphrases = keysFetch(querykeyword); 136 | addingKeywords(clearedphrases); // Добавляем новые ключевые слова 137 | } 138 | }); 139 | 140 | function addingKeywords(keywordsArray) { 141 | var newKeywordsArray = keywordsArray; 142 | newKeywordsArray.forEach( 143 | function (newKeyword) { 144 | var newKey = '+' + newKeyword.toString().replace(/ /g, ' +'); 145 | var adGroupIterator = AdWordsApp.adGroups() 146 | .withCondition('CampaignName = "' + CampaignName + '"') 147 | .withCondition('AdGroupName = "' + AdGroupName + '"') 148 | .get(); 149 | while (adGroupIterator.hasNext()) { 150 | var adGroup = adGroupIterator.next(); 151 | var keywordOperation = adGroup.newKeywordBuilder() 152 | .withText(newKey) 153 | .build(); 154 | } 155 | } 156 | ); 157 | } 158 | 159 | function keysFetch(key) { 160 | Utilities.sleep(100); 161 | var googleUrl = 'https://www.' + url + '/s?gs_rn=18&gs_ri=psy-ab&cp=7&gs_id=d7&xhr=t&q=', 162 | response = UrlFetchApp.fetch(googleUrl + key), 163 | text = response.getContentText('UTF-8'), 164 | phrases = JSON.parse(text), 165 | arr = []; 166 | 167 | phrases[1].forEach(function (line) { 168 | var words = line[0].toString().replace(/[\.;#\(\)=\+:\-\/]+/g, ' ').split(' '); 169 | if (words.length < 6) { 170 | var reason = true; 171 | words.forEach(function (word) { 172 | negativeKeywords.forEach(function (negativeWord) { 173 | if (word == negativeWord) { 174 | reason = false; 175 | } 176 | }); 177 | }); 178 | if (reason != false) { 179 | arr.push(line[0]); 180 | } 181 | } 182 | }); 183 | return arr; 184 | } 185 | } 186 | } 187 | 188 | function getCampaignNegatives() { 189 | var campaignNegativeKeywordsList = []; 190 | var campaignIterator = AdWordsApp.campaigns() 191 | .withCondition('CampaignId = ' + CampaignId) 192 | .get(); 193 | if (campaignIterator.hasNext()) { 194 | var campaign = campaignIterator.next(); 195 | var negativeKeywordListSelector = campaign.negativeKeywordLists() // Получаем минус-слова из списков 196 | .withCondition('Status = ACTIVE'); 197 | var negativeKeywordListIterator = negativeKeywordListSelector 198 | .get(); 199 | while (negativeKeywordListIterator.hasNext()) { 200 | var negativeKeywordList = negativeKeywordListIterator.next(); 201 | var sharedNegativeKeywordIterator = negativeKeywordList.negativeKeywords() 202 | .get(); 203 | var sharedNegativeKeywords = []; 204 | while (sharedNegativeKeywordIterator.hasNext()) { 205 | var negativeKeywordFromList = sharedNegativeKeywordIterator.next(); 206 | sharedNegativeKeywords.push(negativeKeywordFromList.getText()); 207 | } 208 | campaignNegativeKeywordsList = campaignNegativeKeywordsList.concat(campaignNegativeKeywordsList, sharedNegativeKeywords); 209 | } 210 | var campaignNegativeKeywordIterator = campaign.negativeKeywords() // Получаем минус-слова из кампании 211 | .get(); 212 | while (campaignNegativeKeywordIterator.hasNext()) { 213 | var campaignNegativeKeyword = campaignNegativeKeywordIterator.next(); 214 | campaignNegativeKeywordsList.push(campaignNegativeKeyword.getText()); 215 | } 216 | } 217 | campaignNegativeKeywordsList = campaignNegativeKeywordsList.sort(); 218 | return campaignNegativeKeywordsList; 219 | } 220 | 221 | function allDomains() { 222 | var arr = []; 223 | for (var i = 0; i < googleSettings.length; i++) { 224 | var row = googleSettings[i]; 225 | if (JSON.stringify(row).indexOf('domain') != -1) { 226 | var domain = googleSettings[i].location.domain; 227 | arr.push(domain); 228 | } 229 | } 230 | return arr; 231 | } 232 | 233 | function allLangs() { 234 | var arr = []; 235 | for (var i = 0; i < googleSettings.length; i++) { 236 | var row = googleSettings[i]; 237 | if (JSON.stringify(row).indexOf('alphabet') != -1) { 238 | var alphabet = googleSettings[i].language.alphabet; 239 | arr.push(alphabet); 240 | } 241 | } 242 | return arr; 243 | } 244 | 245 | function getCampaignSettings() { 246 | var settings = [], 247 | regionIds = allRegionsIds(); 248 | 249 | var campaignIterator = AdWordsApp.campaigns() 250 | .withCondition('Name = ' + CONFIG.targetCampaign) 251 | .get(); 252 | if (campaignIterator.hasNext()) { 253 | var campaign = campaignIterator.next(); 254 | var languageIterator = campaign.targeting().languages() 255 | .get(); 256 | while (languageIterator.hasNext()) { 257 | var campaignlanguage = languageIterator.next(); 258 | var row = { 259 | languageId: parseFloat(campaignlanguage.getId()).toFixed(), 260 | languageName: campaignlanguage.getName() 261 | }; 262 | settings.push(row); 263 | } 264 | var targetedLocationIterator = campaign.targeting().targetedLocations().get(); 265 | while (targetedLocationIterator.hasNext()) { 266 | var campaigntargetedLocation = targetedLocationIterator.next(); 267 | var row = { 268 | locationId: parseFloat(campaigntargetedLocation.getId()).toFixed(), 269 | locationName: campaigntargetedLocation.getName() 270 | }; 271 | regionIds.forEach( 272 | function (id) { 273 | if (id == parseFloat(campaigntargetedLocation.getId().toString()).toFixed()) { 274 | settings.push(row); 275 | } 276 | } 277 | ) 278 | } 279 | } 280 | return settings; 281 | } 282 | 283 | function setGoogleSettings(arr) { 284 | var settings = arr; 285 | var result = []; 286 | for (var i = 0; i < settings.length; i++) { 287 | var row = settings[i]; 288 | if (row.languageId) { 289 | var row = { 290 | language: { 291 | id: row.languageId, 292 | alphabet: alphabet(row.languageId) 293 | } 294 | } 295 | result.push(row); 296 | } 297 | if (row.locationId) { 298 | var googleDomain = regionGoogle(row.locationId); 299 | var row = { 300 | location: { 301 | id: row.languageId, 302 | domain: googleDomain 303 | } 304 | } 305 | result.push(row); 306 | } 307 | } 308 | return result; 309 | } 310 | 311 | function allRegionsIds() { 312 | var regionSettings = regionGoogle(); 313 | var idsList = []; 314 | for (var i = 0; i < regionSettings.length; i++) { 315 | var row = regionSettings[i]; 316 | var idCol = row[0]; 317 | idsList.push(idCol); 318 | } 319 | return idsList; 320 | } 321 | 322 | function regionGoogle(id) { 323 | var arr = [ 324 | ['2004', 'AF', 'Afghanistan', 'google.com.af'], 325 | ['2008', 'AL', 'Albania', '--'], 326 | ['2012', 'DZ', 'Algeria', 'google.dz'], 327 | ['2016', 'AS', 'American Samoa', 'google.as'], 328 | ['2020', 'AD', 'Andorra', 'gooagle.ad'], 329 | ['2024', 'AO', 'Angola', 'google.co.ao'], 330 | ['2010', 'AQ', 'Antarctica', '--'], 331 | ['2028', 'AG', 'Antigua and Barbuda', 'google.com.ag'], 332 | ['2032', 'AR', 'Argentina', 'google.com.ar'], 333 | ['2051', 'AM', 'Armenia', 'google.am'], 334 | ['2036', 'AU', 'Australia', 'google.com.au'], 335 | ['2040', 'AT', 'Austria', 'google.at'], 336 | ['2031', 'AZ', 'Azerbaijan', 'google.az'], 337 | ['2048', 'BH', 'Bahrain', 'google.com.bh'], 338 | ['2050', 'BD', 'Bangladesh', 'google.com.bd'], 339 | ['2052', 'BB', 'Barbados', '--'], 340 | ['2112', 'BY', 'Belarus', 'google.by'], 341 | ['2056', 'BE', 'Belgium', 'google.be'], 342 | ['2084', 'BZ', 'Belize', 'google.com.bz'], 343 | ['2204', 'BJ', 'Benin', 'google.bj'], 344 | ['2064', 'BT', 'Bhutan', '--'], 345 | ['2068', 'BO', 'Bolivia', 'google.com.bo'], 346 | ['2070', 'BA', 'Bosnia and Herzegovina', 'google.ba'], 347 | ['2072', 'BW', 'Botswana', 'google.co.bw'], 348 | ['2076', 'BR', 'Brazil', 'google.com.br'], 349 | ['2096', 'BN', 'Brunei', 'google.com.bn'], 350 | ['2100', 'BG', 'Bulgaria', 'google.bg'], 351 | ['2854', 'BF', 'Burkina Faso', 'google.bf'], 352 | ['2108', 'BI', 'Burundi', 'google.bi'], 353 | ['2116', 'KH', 'Cambodia', 'google.com.kh'], 354 | ['2120', 'CM', 'Cameroon', 'google.cm'], 355 | ['2124', 'CA', 'Canada', 'google.ca'], 356 | ['2132', 'CV', 'Cape Verde', 'google.cv'], 357 | ['2535', 'BQ', 'Caribbean Netherlands', '--'], 358 | ['2140', 'CF', 'Central African Republic', '--'], 359 | ['2148', 'TD', 'Chad', '--'], 360 | ['2152', 'CL', 'Chile', '--'], 361 | ['2156', 'CN', 'China', 'google.com.hk'], 362 | ['2162', 'CX', 'Christmas Island', '--'], 363 | ['2166', 'CC', 'Cocos (Keeling) Islands', '--'], 364 | ['2170', 'CO', 'Colombia', 'google.com.co'], 365 | ['2174', 'KM', 'Comoros', '--'], 366 | ['2184', 'CK', 'Cook Islands', 'google.co.ck'], 367 | ['2188', 'CR', 'Costa Rica', 'google.co.cr'], 368 | ['2384', 'CI', 'Cote d`Ivoire ', 'google.ci '], 369 | ['2191', 'HR', 'Croatia', 'google.hr'], 370 | ['2531', 'CW', 'Curacao', '--'], 371 | ['2196', 'CY', 'Cyprus', 'google.com.cy'], 372 | ['2203', 'CZ', 'Czechia', 'google.cz'], 373 | ['2180', 'CD', 'Democratic Republic of the Congo', 'google.cg'], 374 | ['2208', 'DK', 'Denmark', 'google.dk'], 375 | ['2262', 'DJ', 'Djibouti', 'google.dj'], 376 | ['2212', 'DM', 'Dominica', 'google.dm'], 377 | ['2214', 'DO', 'Dominican Republic', 'google.com.do'], 378 | ['2218', 'EC', 'Ecuador', 'google.com.ec'], 379 | ['2818', 'EG', 'Egypt', 'google.com.eg'], 380 | ['2222', 'SV', 'El Salvador', 'google.com.sv'], 381 | ['2226', 'GQ', 'Equatorial Guinea', '--'], 382 | ['2232', 'ER', 'Eritrea', '--'], 383 | ['2233', 'EE', 'Estonia', 'google.ee'], 384 | ['2231', 'ET', 'Ethiopia', 'google.com.et'], 385 | ['2583', 'FM', 'Federated States of Micronesia', 'google.fm'], 386 | ['2242', 'FJ', 'Fiji', 'google.com.fj'], 387 | ['2246', 'FI', 'Finland', 'google.fi'], 388 | ['2250', 'FR', 'France', 'google.fr'], 389 | ['2258', 'PF', 'French Polynesia', '--'], 390 | ['2260', 'TF', 'French Southern and Antarctic Lands', '--'], 391 | ['2266', 'GA', 'Gabon', 'google.ga'], 392 | ['2268', 'GE', 'Georgia', 'google.ge'], 393 | ['2276', 'DE', 'Germany', 'google.de'], 394 | ['2288', 'GH', 'Ghana', 'google.com.gh'], 395 | ['2300', 'GR', 'Greece', 'google.gr'], 396 | ['2308', 'GD', 'Grenada', '--'], 397 | ['2316', 'GU', 'Guam', '--'], 398 | ['2320', 'GT', 'Guatemala', 'google.com.gt'], 399 | ['2831', 'GG', 'Guernsey', 'google.gg'], 400 | ['2324', 'GN', 'Guinea', '--'], 401 | ['2624', 'GW', 'Guinea-Bissau', '--'], 402 | ['2328', 'GY', 'Guyana', 'google.gy'], 403 | ['2332', 'HT', 'Haiti', 'google.ht'], 404 | ['2334', 'HM', 'Heard Island and McDonald Islands', '--'], 405 | ['2340', 'HN', 'Honduras', 'google.hn'], 406 | ['2348', 'HU', 'Hungary', 'google.hu'], 407 | ['2352', 'IS', 'Iceland', 'google.is'], 408 | ['2356', 'IN', 'India', 'google.co.in'], 409 | ['2360', 'ID', 'Indonesia', 'google.co.id'], 410 | ['2368', 'IQ', 'Iraq', 'google.iq'], 411 | ['2372', 'IE', 'Ireland', 'google.ie'], 412 | ['2376', 'IL', 'Israel', 'google.co.il'], 413 | ['2380', 'IT', 'Italy', 'google.it'], 414 | ['2388', 'JM', 'Jamaica', 'google.com.jm'], 415 | ['2392', 'JP', 'Japan', 'google.co.jp'], 416 | ['2832', 'JE', 'Jersey', 'google.je'], 417 | ['2400', 'JO', 'Jordan', 'google.jo'], 418 | ['2398', 'KZ', 'Kazakhstan', 'google.kz'], 419 | ['2404', 'KE', 'Kenya', 'google.co.ke'], 420 | ['2296', 'KI', 'Kiribati', 'google.ki'], 421 | ['2414', 'KW', 'Kuwait', 'google.com.kw'], 422 | ['2417', 'KG', 'Kyrgyzstan', 'google.kg'], 423 | ['2418', 'LA', 'Laos', 'google.la'], 424 | ['2428', 'LV', 'Latvia', 'google.lv'], 425 | ['2422', 'LB', 'Lebanon', 'google.com.lb'], 426 | ['2426', 'LS', 'Lesotho', 'google.co.ls'], 427 | ['2430', 'LR', 'Liberia', '--'], 428 | ['2434', 'LY', 'Libya', 'google.com.ly'], 429 | ['2438', 'LI', 'Liechtenstein', 'google.li'], 430 | ['2440', 'LT', 'Lithuania', 'google.lt'], 431 | ['2442', 'LU', 'Luxembourg', 'google.lu'], 432 | ['2807', 'MK', 'Macedonia (FYROM)', 'google.mk'], 433 | ['2450', 'MG', 'Madagascar', 'google.mg'], 434 | ['2454', 'MW', 'Malawi', 'google.mw'], 435 | ['2458', 'MY', 'Malaysia', 'google.com.my'], 436 | ['2462', 'MV', 'Maldives', 'google.mv'], 437 | ['2466', 'ML', 'Mali', 'google.ml'], 438 | ['2470', 'MT', 'Malta', 'google.com.mt'], 439 | ['2584', 'MH', 'Marshall Islands', '--'], 440 | ['2478', 'MR', 'Mauritania', '--'], 441 | ['2480', 'MU', 'Mauritius', 'google.mu'], 442 | ['2484', 'MX', 'Mexico', 'google.com.mx'], 443 | ['2498', 'MD', 'Moldova', 'google.md'], 444 | ['2492', 'MC', 'Monaco', '--'], 445 | ['2496', 'MN', 'Mongolia', 'google.mn'], 446 | ['2499', 'ME', 'Montenegro', 'google.me'], 447 | ['2504', 'MA', 'Morocco', 'google.co.ma'], 448 | ['2508', 'MZ', 'Mozambique', 'google.co.mz'], 449 | ['2104', 'MM', 'Myanmar (Burma)', 'google.com.mm'], 450 | ['2516', 'NA', 'Namibia', 'google.com.na'], 451 | ['2520', 'NR', 'Nauru', 'google.nr'], 452 | ['2524', 'NP', 'Nepal', 'google.com.np'], 453 | ['2528', 'NL', 'Netherlands', 'google.nl'], 454 | ['2540', 'NC', 'New Caledonia', '--'], 455 | ['2554', 'NZ', 'New Zealand', 'google.co.nz'], 456 | ['2558', 'NI', 'Nicaragua', 'google.com.ni'], 457 | ['2562', 'NE', 'Niger', 'google.ne'], 458 | ['2566', 'NG', 'Nigeria', 'google.com.ng'], 459 | ['2570', 'NU', 'Niue', 'google.nu'], 460 | ['2574', 'NF', 'Norfolk Island', 'google.com.nf'], 461 | ['2580', 'MP', 'Northern Mariana Islands', '--'], 462 | ['2578', 'NO', 'Norway', 'google.no'], 463 | ['2512', 'OM', 'Oman', 'google.com.om'], 464 | ['2586', 'PK', 'Pakistan', 'google.com.pk'], 465 | ['2585', 'PW', 'Palau', '--'], 466 | ['2591', 'PA', 'Panama', 'google.com.pa'], 467 | ['2598', 'PG', 'Papua New Guinea', '--'], 468 | ['2600', 'PY', 'Paraguay', 'google.com.py'], 469 | ['2604', 'PE', 'Peru', 'google.com.pe'], 470 | ['2608', 'PH', 'Philippines', 'google.com.ph'], 471 | ['2612', 'PN', 'Pitcairn Islands', 'google.pn'], 472 | ['2616', 'PL', 'Poland', 'google.pl'], 473 | ['2620', 'PT', 'Portugal', 'google.pt'], 474 | ['2634', 'QA', 'Qatar', 'google.com.qa'], 475 | ['2178', 'CG', 'Republic of the Congo', '--'], 476 | ['2642', 'RO', 'Romania', 'google.ro'], 477 | ['2643', 'RU', 'Russia', 'google.ru'], 478 | ['2646', 'RW', 'Rwanda', 'google.rw'], 479 | ['2654', 'SH', 'Saint Helena, Ascension and Tristan da Cunha', 'google.sh'], 480 | ['2659', 'KN', 'Saint Kitts and Nevis', '--'], 481 | ['2662', 'LC', 'Saint Lucia', '--'], 482 | ['2666', 'PM', 'Saint Pierre and Miquelon', '--'], 483 | ['2670', 'VC', 'Saint Vincent and the Grenadines', 'google.com.vc'], 484 | ['2882', 'WS', 'Samoa', 'google.ws'], 485 | ['2674', 'SM', 'San Marino', 'google.sm'], 486 | ['2678', 'ST', 'Sao Tome and Principe', 'google.st'], 487 | ['2682', 'SA', 'Saudi Arabia', 'google.com.sa'], 488 | ['2686', 'SN', 'Senegal', 'google.sn'], 489 | ['2688', 'RS', 'Serbia', 'google.rs'], 490 | ['2690', 'SC', 'Seychelles', 'google.sc'], 491 | ['2694', 'SL', 'Sierra Leone', 'google.com.sl'], 492 | ['2702', 'SG', 'Singapore', 'google.com.sg'], 493 | ['2534', 'SX', 'Sint Maarten', '--'], 494 | ['2703', 'SK', 'Slovakia', 'google.sk'], 495 | ['2705', 'SI', 'Slovenia', 'google.si'], 496 | ['2090', 'SB', 'Solomon Islands', 'google.com.sb'], 497 | ['2706', 'SO', 'Somalia', 'google.so'], 498 | ['2710', 'ZA', 'South Africa', 'google.co.za'], 499 | ['2239', 'GS', 'South Georgia and the South Sandwich Islands', '--'], 500 | ['2410', 'KR', 'South Korea', '--'], 501 | ['2724', 'ES', 'Spain', 'google.es'], 502 | ['2144', 'LK', 'Sri Lanka', 'google.lk'], 503 | ['2740', 'SR', 'Suriname', '--'], 504 | ['2748', 'SZ', 'Swaziland', '--'], 505 | ['2752', 'SE', 'Sweden', 'google.se'], 506 | ['2756', 'CH', 'Switzerland', 'google.ch'], 507 | ['2762', 'TJ', 'Tajikistan', 'google.com.tj'], 508 | ['2834', 'TZ', 'Tanzania', 'google.co.tz'], 509 | ['2764', 'TH', 'Thailand', 'google.co.th'], 510 | ['2044', 'BS', 'The Bahamas', 'google.bs'], 511 | ['2270', 'GM', 'The Gambia', 'google.gm'], 512 | ['2626', 'TL', 'Timor-Leste', 'google.tl'], 513 | ['2768', 'TG', 'Togo', 'google.tg'], 514 | ['2772', 'TK', 'Tokelau', 'google.tk'], 515 | ['2776', 'TO', 'Tonga', 'google.to'], 516 | ['2780', 'TT', 'Trinidad and Tobago', 'google.tt'], 517 | ['2788', 'TN', 'Tunisia', 'google.tn'], 518 | ['2792', 'TR', 'Turkey', 'google.com.tr'], 519 | ['2795', 'TM', 'Turkmenistan', 'google.tm'], 520 | ['2798', 'TV', 'Tuvalu', '--'], 521 | ['2800', 'UG', 'Uganda', 'google.co.ug'], 522 | ['2804', 'UA', 'Ukraine', 'google.com.ua'], 523 | ['2784', 'AE', 'United Arab Emirates', 'google.ae'], 524 | ['2826', 'GB', 'United Kingdom', 'google.co.uk'], 525 | ['2840', 'US', 'United States', 'google.com'], 526 | ['2581', 'UM', 'United States Minor Outlying Islands', '--'], 527 | ['2858', 'UY', 'Uruguay', 'google.com.uy'], 528 | ['2860', 'UZ', 'Uzbekistan', 'google.co.uz'], 529 | ['2548', 'VU', 'Vanuatu', 'google.vu'], 530 | ['2336', 'VA', 'Vatican City', '--'], 531 | ['2862', 'VE', 'Venezuela', 'google.co.ve'], 532 | ['2704', 'VN', 'Vietnam', 'google.com.vn'], 533 | ['2876', 'WF', 'Wallis and Futuna', '--'], 534 | ['2887', 'YE', 'Yemen', '--'], 535 | ['2894', 'ZM', 'Zambia', 'google.co.zm'], 536 | ['2716', 'ZW', 'Zimbabwe', 'google.co.zw'], 537 | ]; 538 | if (id) { 539 | var domain; 540 | arr.forEach(function (row) { 541 | if (parseFloat(row[0].toString()).toFixed() == id) { 542 | domain = row[3]; 543 | if (domain == '--') { 544 | domain = 'google.com'; 545 | } 546 | } 547 | }); 548 | return domain; 549 | } else { 550 | return arr; 551 | } 552 | } 553 | 554 | function alphabet(id) { 555 | var arr = [ 556 | ['Arabic', 'ar', 1019, ['أ', 'ب', 'ت', 'ث', 'ج', 'ح', 'خ', 'د', 'ذ', 'ر', 'ز', 'س', 'ش', 'ص', 'ض', 'ط', 'ظ', 'ع', 'ف', 'ق', 'ك', 'ل', 'م', 'ن', 'و', 'ي', 'ﻩ', 'غ']], // Arabic 557 | ['Bulgarian', 'bg', 1020, ['а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ь', 'ю', 'я']], // Bulgarian 558 | ['Catalan', 'ca', 1038, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'à', 'ç', 'è', 'é', 'í', 'ï', 'ò', 'ó', 'ú', 'ü']], // Catalan 559 | ['Chinese (simplified)', 'zh_CN', 1017, []], // Chinese(simplified), reserved 560 | ['Chinese (traditional)', 'zh_TW', 1018, []], // Chinese(traditional), reserved 561 | ['Croatian', 'hr', 1039, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'z', 'ć', 'č', 'đ', 'š', 'ž', 'dž', 'lj', 'nj']], // Croatian 562 | ['Czech', 'cs', 1021, ['a', 'b', 'c', 'ch', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'á', 'é', 'í', 'ó', 'ú', 'ý', 'č', 'ď', 'ě', 'ň', 'ř', 'š', 'ť', 'ů', 'ž']], // Czech 563 | ['Danish', 'da', 1009, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'æ', 'ø', 'å']], // Danish 564 | ['Dutch', 'nl', 1010, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']], // Dutch 565 | ['English', 'en', 1000, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']], // English 566 | ['Estonian', 'et', 1043, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ä', 'õ', 'ö', 'ü', 'š', 'ž']], // Estonian 567 | ['Filipino', 'tl', 1042, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'ng', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ñ']], // Filipino 568 | ['Finnish', 'fi', 1011, ['a', 'ä', 'å', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'ö', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']], // Finnish 569 | ['French', 'fr', 1002, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'à', 'â', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'î', 'ï', 'ô', 'ù', 'û', 'ü', 'ÿ', 'œ']], // French 570 | ['German', 'de', 1001, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ß', 'ä', 'ö', 'ü']], // German 571 | ['Greek', 'el', 1022, ['α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'ς', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω']], // Greek 572 | ['Hebrew', 'iw', 1027, ['א', 'אֽ', 'אֿ', 'ב', 'ג', 'ד', 'ה', 'ו', 'וֹ', 'וּ', 'ז', 'ח', 'ט', 'י', 'ך', 'כ', 'ל', 'ם', 'מ', 'ן', 'נ', 'ס', 'ע', 'ף', 'פ', 'פּ', 'ץ', 'צ', 'ק', 'ר', 'ש', 'שׁ', 'שׂ', 'ת', 'תּ']], // Hebrew 573 | ['Hindi', 'hi', 1023, ['अ', 'आ', 'इ', 'ई', 'उ', 'ऊ', 'ऋ', 'ऌ', 'ऍ', 'ऎ', 'ए', 'ऐ', 'ऑ', 'ऒ', 'ओ', 'औ', 'क', 'ख', 'ग', 'घ', 'ङ', 'च', 'छ', 'ज', 'झ', 'ञ', 'ट', 'ठ', 'ड', 'ढ', 'ण', 'त', 'थ', 'द', 'ध', 'न', 'ऩ', 'प', 'फ', 'ब', 'भ', 'म', 'य', 'र', 'ऱ', 'ल', 'ळ', 'ऴ', 'व', 'श', 'ष', 'स', 'ह', 'ॐ', 'क़', 'ख़', 'ग़', 'ज़', 'ड़', 'ढ़', 'फ़', 'य़', 'ॠ', 'ॡ']], // Hindi 574 | ['Hungarian', 'hu', 1024, ['A', 'B', 'C', 'Cs', 'D', 'Dz', 'Dzs', 'E', 'F', 'G', 'Gy', 'H', 'I', 'J', 'K', 'L', 'Ly', 'M', 'N', 'Ny', 'O', 'P', 'Q', 'R', 'S', 'Sz', 'T', 'Ty', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Zs', 'Á', 'É', 'Ë', 'Í', 'Ó', 'Ö', 'Ú', 'Ü', 'Ő', 'Ű']], // Hungarian 575 | ['Icelandic', 'is', 1026, ['a', 'b', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'x', 'y', 'z', 'á', 'æ', 'é', 'í', 'ð', 'ó', 'ö', 'ú', 'ý', 'þ']], // Icelandic 576 | ['Indonesian', 'id', 1025, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']], // Indonesian 577 | ['Italian', 'it', 1004, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'z', 'à', 'è', 'é', 'ì', 'í', 'î', 'ò', 'ó', 'ù', 'ú']], // Italian 578 | ['Japanese', 'ja', 1005, ['あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', 'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む', 'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'り', 'る', 'れ', 'ろ', 'わ', 'ゐ', 'ゑ', 'を', 'ん']], // Japanese 579 | ['Korean', 'ko', 1012, ['ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ', 'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ']], // Korean 580 | ['Latvian', 'lv', 1028, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'z', 'ā', 'č', 'ē', 'ģ', 'ī', 'ķ', 'ļ', 'ņ', 'š', 'ū', 'ž']], // Latvian 581 | ['Lithuanian', 'lt', 1029, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'y', 'z', 'ą', 'č', 'ė', 'ę', 'į', 'š', 'ū', 'ų', 'ž']], // Lithuanian 582 | ['Malay', 'ms', 1102, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']], // Malay 583 | ['Norwegian', 'no', 1013, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'å', 'æ', 'ø']], // Norwegian 584 | ['Persian', 'fa', 1064, ['ء', 'آ', 'أ', 'ئـ', 'ا', 'ب', 'بـ', 'ت', 'تـ', 'ث', 'ثـ', 'ج', 'جـ', 'ح', 'حـ', 'خ', 'خـ', 'د', 'ذ', 'ر', 'ز', 'س', 'سـ', 'ش', 'شـ', 'ص', 'صـ', 'ض', 'ضـ', 'ط', 'طـ', 'ظ', 'ظـ', 'ع', 'عـ', 'غ', 'غـ', 'ـأ', 'ـؤ', 'ـئ', 'ـئـ', 'ـا', 'ـب', 'ـبـ', 'ـت', 'ـتـ', 'ـث', 'ـثـ', 'ـج', 'ـجـ', 'ـح', 'ـحـ', 'ـخ', 'ـخـ', 'ـد', 'ـذ', 'ـر', 'ـز', 'ـس', 'ـسـ', 'ـش', 'ـشـ', 'ـص', 'ـصـ', 'ـض', 'ـضـ', 'ـط', 'ـطـ', 'ـظ', 'ـظـ', 'ـع', 'ـعـ', 'ـغ', 'ـغـ', 'ـف', 'ـفـ', 'ـق', 'ـقـ', 'ـل', 'ـلـ', 'ـم', 'ـمـ', 'ـن', 'ـنـ', 'ه', 'ـهـ', 'ـو', 'ـپ', 'ـپـ', 'ـچ', 'ـچـ', 'ـژ', 'ـک', 'ـکـ', 'ـگ', 'ـگـ', 'ـی', 'ـیـ', 'ف', 'فـ', 'ق', 'قـ', 'ل', 'لـ', 'م', 'مـ', 'ن', 'نـ', 'ه', 'هـ', 'و', 'پ', 'پـ', 'چ', 'چـ', 'ژ', 'ک', 'کـ', 'گ', 'گـ', 'ی', 'یـ']], // Persian 585 | ['Polish', 'pl', 1030, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ó', 'ą', 'ć', 'ę', 'ł', 'ń', 'ś', 'ź', 'ż']], // Polish 586 | ['Portuguese', 'pt', 1014, ['a', 'b', 'c', 'ch', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ç']], // Portuguese 587 | ['Romanian', 'ro', 1032, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'â', 'î', 'ă', 'ș', 'ț']], // Romanian 588 | ['Russian', 'ru', 1031, ['а', 'б', 'в', 'г', 'д', 'е', 'ё', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я']], // Russian 589 | ['Serbian', 'sr', 1035, ['а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'ђ', 'ј', 'љ', 'њ', 'ћ', 'џ']], // Serbian 590 | ['Slovak', 'sk', 1033, ['a', 'b', 'c', 'ch', 'd', 'dz', 'dž', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'á', 'ä', 'é', 'í', 'ó', 'ô', 'ú', 'ý', 'č', 'ď', 'ĺ', 'ľ', 'ň', 'ŕ', 'š', 'ť', 'ž']], // Slovak 591 | ['Slovenian', 'sl', 1034, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'z', 'č', 'š', 'ž']], // Slovenian 592 | ['Spanish', 'es', 1003, ['a', 'b', 'c', 'ch', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'll', 'm', 'n', 'o', 'p', 'q', 'r', 'rr', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ñ']], // Spanish 593 | ['Swedish', 'sv', 1015, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ä', 'å', 'ö', 'š', 'ž']], // Swedish 594 | ['Thai', 'th', 1044, ['ก', 'ข', 'ฃ', 'ค', 'ฅ', 'ฆ', 'ง', 'จ', 'ฉ', 'ช', 'ซ', 'ฌ', 'ญ', 'ฎ', 'ฏ', 'ฐ', 'ฑ', 'ฒ', 'ณ', 'ด', 'ต', 'ถ', 'ท', 'ธ', 'น', 'บ', 'ป', 'ผ', 'ฝ', 'พ', 'ฟ', 'ภ', 'ม', 'ย', 'ร', 'ฤ', 'ฤๅ', 'ล', 'ฦ', 'ว', 'ศ', 'ษ', 'ส', 'ห', 'ฬ', 'อ', 'อิ', 'อี', 'อุ', 'อู', 'ฮ', 'ะ', 'า', 'ึๅ', 'เ', 'โ']], // Thai 595 | ['Turkish', 'tr', 1037, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'y', 'z', 'ç', 'ö', 'ü', 'ğ', 'ı', 'ş']], // Turkish 596 | ['Ukrainian', 'uk', 1036, ['є', 'і', 'ї', 'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ь', 'ю', 'я', 'ґ']], // Ukrainian 597 | ['Urdu', 'ur', 1041, ['ء', 'ا', 'ب', 'ت', 'ث', 'ج', 'ح', 'خ', 'د', 'ذ', 'ر', 'ز', 'س', 'ش', 'ص', 'ض', 'ط', 'ظ', 'ع', 'غ', 'ف', 'ق', 'ل', 'م', 'ن', 'و', 'ٹ', 'پ', 'چ', 'ڈ', 'ڑ', 'ژ', 'ک', 'گ', 'ھ', 'ہ', 'ی', 'ے', 'ﮩ']], // Urdu 598 | ['Vietnamese', 'vi', 1040, ['a', 'ă', 'â', 'b', 'c', 'd', 'đ', 'e', 'ê', 'g', 'h', 'i', 'k', 'l', 'm', 'n', 'o', 'ô', 'ơ', 'p', 'q', 'r', 's', 't', 'u', 'ư', 'v', 'x', 'y']] // Vietnamese 599 | ]; 600 | var alphabet = []; 601 | arr.forEach(function (row) { 602 | if (row[2].toFixed() == id) { 603 | alphabet = row[3]; 604 | } 605 | }); 606 | return alphabet; 607 | } 608 | 609 | function ensureAccountLabels() { 610 | function getAccountLabelNames() { 611 | var labelNames = []; 612 | var iterator = AdWordsApp.labels() 613 | .get(); 614 | while (iterator.hasNext()) { 615 | labelNames.push(iterator.next().getName()); 616 | } 617 | return labelNames; 618 | } 619 | var labelNames = getAccountLabelNames(); 620 | if (labelNames.indexOf(CONFIG.scriptLabel) == -1) { 621 | AdWordsApp.createLabel(CONFIG.scriptLabel); 622 | } 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /release/Video_Auto_Placement_Builder.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | var CONFIG = { 4 | SpreadsheetUrl: 'https://docs.google.com/spreadsheets/d/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/edit', 5 | // Таблица где в первой колонке содержится список ключевых слов для поиска 6 | 7 | Lang: ['en', 'ru'], 8 | // Коды языков для результатов поиска - язык инерфейса Ютуба - http://www.loc.gov/standards/iso639-2/php/code_list.php 9 | 10 | TargetLabel: 'YT_PlacementBuilder' 11 | // Этим ярлыком надо пометить целевую видео-кампанию и группу объявлений в ней 12 | }; 13 | 14 | // ----------------------------------- 15 | 16 | var Spreadsheet = SpreadsheetApp.openByUrl(CONFIG.SpreadsheetUrl), 17 | sheet = Spreadsheet.getSheets()[0], 18 | sheetName = Spreadsheet.getName(); 19 | 20 | var keyWords = getSpreadSheedData(); // Получили исходные ключи 21 | 22 | saveResult(); // Запросили Ютуб и записали результат 23 | 24 | function saveResult() { 25 | keyWords.forEach(function (keyword) { 26 | Logger.log(keyword); 27 | var arr = searchVideosByKeyword(keyword[0].toString()); 28 | arr.forEach(function (id) { 29 | addChannelPlacement(id); 30 | }) 31 | }); 32 | } 33 | 34 | function getSpreadSheedData() { 35 | var range = sheet.getDataRange(), 36 | numRows = range.getNumRows().toFixed(), 37 | numColumns = range.getNumColumns().toFixed(), 38 | rangeWithData = sheet.getRange(1, 1, numRows, numColumns), 39 | dataValues = rangeWithData.getValues(); 40 | return dataValues; 41 | } 42 | 43 | function searchVideosByKeyword(key) { 44 | var result = []; 45 | CONFIG.Lang.forEach(function (lang) { 46 | var videoResults = YouTube.Search.list( 47 | 'id, snippet', { 48 | 'order': 'viewCount', 49 | 'q': key.toString().replace(/\"/g, ' ').replace(/\'/g, ' '), 50 | 'relevanceLanguage': lang, 51 | 'type': 'video', 52 | 'maxResults': 50 53 | } 54 | ); 55 | if (videoResults.items[0] !== undefined) { 56 | for (var i in videoResults.items) { 57 | var line = []; 58 | var item = videoResults.items[i]; 59 | var videoId = item.id.videoId.toString(); 60 | var results = videosListById('snippet', { 61 | 'id': videoId 62 | }); 63 | if (results.items[0] !== undefined) { 64 | var channelId = results.items[0].snippet.channelId; 65 | if (channelId != undefined) { 66 | var id = channelId; 67 | result.push(id); 68 | // Logger.log(id); 69 | } 70 | } 71 | } 72 | } 73 | var channelResults = YouTube.Search.list( 74 | 'id,snippet', { 75 | 'order': 'videoCount', 76 | 'q': key.toString().replace(/\"/g, ' ').replace(/\'/g, ' '), 77 | 'relevanceLanguage': lang, 78 | 'type': 'channel', 79 | 'maxResults': 50 80 | } 81 | ); 82 | if (channelResults.items[0] !== undefined) { 83 | for (var i in channelResults.items) { 84 | var item = channelResults.items[i]; 85 | var id = item.id.channelId.toString(); 86 | result.push(id); 87 | // Logger.log(id); 88 | } 89 | } 90 | }); 91 | result = unique(result); 92 | return result; 93 | } 94 | 95 | function videosListById(part, params) { 96 | var response = YouTube.Videos.list(part, params); 97 | return (response); 98 | } 99 | 100 | function addChannelPlacement(channelId) { 101 | var videoCampaignSelector = AdWordsApp.videoCampaigns() 102 | .withCondition('LabelNames CONTAINS_ANY [' + CONFIG.TargetLabel + ']'); 103 | var videoCampaignIterator = videoCampaignSelector.get(); 104 | if (videoCampaignIterator.totalNumEntities() == +1) { 105 | while (videoCampaignIterator.hasNext()) { 106 | var videoCampaign = videoCampaignIterator.next(); 107 | var videoNetwork = videoCampaign.getNetworks(); 108 | // Logger.log(videoNetwork); 109 | if (videoNetwork == 'YOUTUBE_VIDEO') { 110 | var videoAdGroupSelector = videoCampaign.videoAdGroups() 111 | .withCondition('LabelNames CONTAINS_ANY [' + CONFIG.TargetLabel + ']'); 112 | var videoAdGroupIterator = videoAdGroupSelector.get(); 113 | if (videoAdGroupIterator.totalNumEntities() == +1) { 114 | while (videoAdGroupIterator.hasNext()) { 115 | var videoAdGroup = videoAdGroupIterator.next(); 116 | var videoYouTubeChannelBuilder = videoAdGroup.videoTargeting().newYouTubeChannelBuilder(); 117 | var videoYouTubeChannelOperation = videoYouTubeChannelBuilder 118 | .withChannelId(channelId) // required 119 | .build(); // create the YouTube channel 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | function unique(arr) { // убираем повторы 128 | var tmp = {}; 129 | return arr.filter(function (a) { 130 | return a in tmp ? 0 : tmp[a] = 1; 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /release/Video_Youtube_parser.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | var CONFIG = { 4 | SpreadsheetUrl: 'https://docs.google.com/spreadsheets/d/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/edit', 5 | // Таблица где в первой колонке содержится список ключевых слов для поиска 6 | 7 | SearchType: 'video', 8 | // video/channel - Будем искать каналы содержащие видео, или сразу каналы? 9 | 10 | Lang: 'en' 11 | // Коды языков для результатов поиска - http://www.loc.gov/standards/iso639-2/php/code_list.php 12 | }; 13 | 14 | // ----------------------------------- 15 | 16 | var Spreadsheet = SpreadsheetApp.openByUrl(CONFIG.SpreadsheetUrl), 17 | sheet = Spreadsheet.getSheets()[0], 18 | sheetName = Spreadsheet.getName(); 19 | 20 | var keyWords = getSpreadSheedData(); 21 | 22 | saveResult(); 23 | 24 | function saveResult() { 25 | var ssNew = SpreadsheetApp.create('Youtube (' + CONFIG.SearchType + ') - ' + sheetName); 26 | Logger.log(ssNew.getUrl()); 27 | Logger.log('================================================'); 28 | Utilities.sleep(100); 29 | var sheetNew = ssNew.getSheets()[0]; 30 | 31 | sheetNew.appendRow([ 32 | 'ID', 33 | 'Keyword' 34 | ]); 35 | 36 | keyWords.forEach(function (keyword) { 37 | var arr = searchVideosByKeyword(keyword.toString()); 38 | arr.forEach(function (line) { 39 | sheetNew.appendRow(line); 40 | }) 41 | }); 42 | } 43 | 44 | function getSpreadSheedData() { 45 | var range = sheet.getDataRange(), 46 | numRows = range.getNumRows().toFixed(), 47 | numColumns = range.getNumColumns().toFixed(), 48 | rangeWithData = sheet.getRange(1, 1, numRows, numColumns), 49 | dataValues = rangeWithData.getValues(); 50 | return dataValues; 51 | } 52 | 53 | function searchVideosByKeyword(key) { 54 | var result = []; 55 | if (CONFIG.SearchType == 'video') { 56 | var resultsOne = YouTube.Search.list( 57 | 'id,snippet', { 58 | 'order': 'viewCount', 59 | 'q': key.toString().replace(/\"/g, ' ').replace(/\'/g, ' '), 60 | 'type': CONFIG.SearchType, 61 | 'maxResults': 50 62 | } 63 | ); 64 | if (resultsOne.items[0] !== undefined) { 65 | for (var i in resultsOne.items) { 66 | var line = []; 67 | var item = resultsOne.items[i], 68 | videoId = item.id.videoId.toString(); 69 | var results = videosListById('snippet', { 70 | 'id': videoId 71 | }); 72 | if (results.items[0] !== undefined) { 73 | var channelId = results.items[0].snippet.channelId; 74 | if (channelId != undefined) { 75 | line[0] = 'youtube.com/channel/' + channelId; 76 | line[1] = key.toString().replace(/\"/g, ' ').replace(/\'/g, ' '); 77 | result.push(line); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | if (CONFIG.SearchType == 'channel') { 84 | var results = YouTube.Search.list( 85 | 'id,snippet', { 86 | 'order': 'videoCount', 87 | 'q': key.toString().replace(/\"/g, ' ').replace(/\'/g, ' '), 88 | 'type': CONFIG.SearchType, 89 | 'maxResults': 50 90 | } 91 | ); 92 | for (var i in results.items) { 93 | var item = results.items[i]; 94 | var line = []; 95 | line[0] = 'youtube.com/channel/' + item.id.channelId.toString(); 96 | line[1] = key.toString().replace(/\"/g, ' ').replace(/\'/g, ' '); 97 | result.push(line); 98 | } 99 | } 100 | Utilities.sleep(100); 101 | return result; 102 | } 103 | 104 | function videosListById(part, params) { 105 | var response = YouTube.Videos.list(part, params); 106 | return (response); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /release/ahrefs/GDN_DR_parser.js: -------------------------------------------------------------------------------- 1 | function main() { 2 | 3 | var ahrefsToken = 'xyxyxyxyxyxyxyxyxyxyxyxyxy'; 4 | 5 | // Указываем количество дней для выборки 6 | // Если хотим использовать данные о конверсиях или доходности, то в качестве значения следует указывать число больее чем окно конверсии. 7 | var customDaysInDateRange = 365; 8 | 9 | // Указываем на сколько дней от сегодняшнего мы сдвигаем выборку. Нужно для того чтобы не брать те дни когда запаздывает статистика. 10 | // Если хотим использовать данные о конверсиях или доходности, то в качестве значения следует указывать число равное дням в окне конверсии. 11 | var customDateRangeShift = 45; 12 | 13 | //--------------------------------------------------------------------------------------------------------- 14 | 15 | var allDomains = []; 16 | var allData = []; 17 | 18 | var PerfomaceAWQL = 'SELECT Domain, Clicks, Conversions, ConversionValue, Cost ' + 19 | 'FROM AUTOMATIC_PLACEMENTS_PERFORMANCE_REPORT ' + 20 | 'WHERE Cost > 10000000 ' + 21 | 'DURING ' + customDateRange(); 22 | var PerfomaceRowsIter = AdWordsApp.report(PerfomaceAWQL).rows(); 23 | Logger.log(PerfomaceAWQL); 24 | while (PerfomaceRowsIter.hasNext()) { 25 | var row = PerfomaceRowsIter.next(); 26 | var Criteria = row['Domain'].toString(); 27 | var Clicks = parseFloat(row['Clicks']).toFixed(); 28 | var Conversions = parseFloat(row['Conversions']).toFixed(2); 29 | var ConversionValue = parseFloat(row['ConversionValue']).toFixed(2); 30 | var Cost = parseFloat(row['Cost']).toFixed(2); 31 | 32 | if (Criteria.indexOf('::') == -1) { 33 | allDomains.push(Criteria); 34 | var line = { 35 | CriteriaRow: Criteria, 36 | ClicksRow: Clicks, 37 | ConversionsRow: Conversions, 38 | ConversionValueRow: ConversionValue, 39 | CostRow: Cost 40 | }; 41 | allData.push(line); 42 | } 43 | } 44 | 45 | allDomains = unique(allDomains); 46 | 47 | var uniqueDomains = []; 48 | 49 | allDomains.forEach(function (uniquedomain) { 50 | var domainStats = {}; 51 | domainStats.Criteria = uniquedomain; 52 | domainStats.Clicks = +0; 53 | domainStats.Conversions = +0; 54 | domainStats.ConversionValue = +0; 55 | domainStats.Cost = +0; 56 | domainStats.Rating = getDomainRating(uniquedomain); 57 | allData.forEach(function (line) { 58 | if (line.CriteriaRow == uniquedomain) { 59 | domainStats.Clicks = domainStats.Clicks + +line.ClicksRow; 60 | domainStats.Conversions = domainStats.Conversions + +line.ConversionsRow; 61 | domainStats.ConversionValue = domainStats.ConversionValue + +line.ConversionValueRow; 62 | domainStats.Cost = domainStats.Cost + +line.CostRow; 63 | } 64 | }); 65 | uniqueDomains.push(domainStats); 66 | }); 67 | 68 | var statByRates = []; 69 | 70 | var rateCount = 0; 71 | while (rateCount < 101) { 72 | var rateStats = {}; 73 | rateStats.Rating = rateCount; 74 | rateStats.Clicks = +0; 75 | rateStats.Conversions = +0; 76 | rateStats.ConversionValue = +0; 77 | rateStats.Cost = +0; 78 | uniqueDomains.forEach(function (line) { 79 | if (line.Rating == rateCount) { 80 | rateStats.Clicks = rateStats.Clicks + +line.Clicks; 81 | rateStats.Conversions = rateStats.Conversions + +line.Conversions; 82 | rateStats.ConversionValue = rateStats.ConversionValue + +line.ConversionValue; 83 | rateStats.Cost = rateStats.Cost + +line.Cost; 84 | } 85 | }); 86 | statByRates.push(rateStats); 87 | rateCount++; 88 | } 89 | 90 | var ssNew = SpreadsheetApp.create('Stats By DR'); 91 | var sheet = ssNew.getSheets()[0]; 92 | sheet.appendRow([ 93 | 'Domain Rating', 94 | 'Clicks', 95 | 'Conversions', 96 | 'ConversionValue', 97 | 'Cost', 98 | ]); 99 | statByRates.forEach(function (line) { 100 | sheet.appendRow([ 101 | line.Rating, 102 | line.Clicks, 103 | line.Conversions, 104 | line.ConversionValue, 105 | line.Cost 106 | ]); 107 | }) 108 | Logger.log(ssNew.getUrl()); 109 | 110 | function getDomainRating(url) { 111 | var ahrefsUrl = 'http://apiv2.ahrefs.com/?from=domain_rating&target=' + url + '&mode=domain&output=json&token=' + ahrefsToken; 112 | var response = UrlFetchApp.fetch(ahrefsUrl); 113 | Utilities.sleep(100); 114 | var ahrefs = JSON.parse(response.getContentText('UTF-8')); 115 | var rating = ahrefs.domain.domain_rating; 116 | return rating; 117 | } 118 | 119 | function unique(arr) { // убираем повторы 120 | var result = []; 121 | nextInput: 122 | for (var i = 0; i < arr.length; i++) { 123 | var str = arr[i]; // для каждого элемента 124 | for (var j = 0; j < result.length; j++) { // ищем, был ли он уже? 125 | if (result[j] == str) continue nextInput; // если да, то следующий 126 | } 127 | result.push(str); 128 | } 129 | return result; 130 | } 131 | 132 | function customDateRange(select) { // Формируем значение параметра временного диапазона для выборки AWQL 133 | var timeType = select; 134 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24; 135 | var now = new Date(); 136 | var fromDate = new Date(now.getTime() - (customDaysInDateRange + customDateRangeShift) * MILLIS_PER_DAY); 137 | var toDate = new Date(now.getTime() - customDateRangeShift * MILLIS_PER_DAY); 138 | var nowDate = new Date(now.getTime()); 139 | var timeZone = AdWordsApp.currentAccount().getTimeZone(); 140 | var fromformatDate = Utilities.formatDate(fromDate, timeZone, 'yyyyMMdd'); 141 | var toformatDate = Utilities.formatDate(toDate, timeZone, 'yyyyMMdd'); 142 | var nowformatDate = Utilities.formatDate(nowDate, timeZone, 'yyyyMMdd'); 143 | var duringDates = fromformatDate + ',' + toformatDate; 144 | 145 | if (timeType == 'from') { 146 | return fromformatDate; 147 | } else if (timeType == 'to') { 148 | return toformatDate; 149 | } else if (timeType == 'now') { 150 | return nowformatDate; 151 | } else { 152 | return duringDates; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /search_keyword_builder/Code.gs: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////// 2 | // Search Keyword Builder 2.0 3 | // Настройки в config.js 4 | ////////////////////////////////////////////////////////////////////////////// 5 | 6 | function main() { 7 | 8 | // Select the accounts to be processed. You can process up to 50 accounts. 9 | var accountSelector = MccApp.accounts() 10 | .withCondition('Cost > 0') 11 | .forDateRange('LAST_7_DAYS') 12 | .orderBy('Cost DESC') 13 | .withLimit(50); 14 | // Process the account in parallel. The callback method is optional. 15 | accountSelector.executeInParallel('account_main', 'allFinished'); 16 | } 17 | 18 | function allFinished(results) { 19 | if (!AdsApp.getExecutionInfo().isPreview()) { 20 | Logger.log('Работа закончена'); 21 | } else { 22 | Logger.log('Отработали превью'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /search_keyword_builder/account.gs: -------------------------------------------------------------------------------- 1 | function account_main() { 2 | 3 | ensureAccountLabels(); // Проверяем и создаем ярлыки 4 | 5 | var campaignQuery = 'SELECT ' + 6 | 'campaign.name ' + 7 | 'FROM campaign ' + 8 | 'WHERE campaign.advertising_channel_type = "SEARCH" ' + 9 | 'AND campaign.status != "REMOVED" ' + 10 | 'AND metrics.impressions > ' + CONFIG().customDaysInDateRange + ' ' + 11 | 'AND segments.date BETWEEN "' + customDateRange('from') + '" AND "' + customDateRange('to') + '"'; 12 | var campaignResult = AdsApp.search(campaignQuery, { 13 | apiVersion: 'v8' 14 | }); 15 | while (campaignResult.hasNext()) { 16 | var campaign_row = campaignResult.next(), 17 | campaign_name = campaign_row.campaign.name; 18 | if (campaign_row) { 19 | adGroupReport(campaign_name); // Создаем ключи 20 | } 21 | } 22 | } 23 | 24 | function adGroupReport(campaign_name) { 25 | var adGroupSelector = AdsApp.adGroups() 26 | .withCondition('Impressions > ' + CONFIG().customDaysInDateRange) 27 | .withCondition('CampaignName = "' + campaign_name + '"') 28 | .withCondition('Status != REMOVED') 29 | .forDateRange('LAST_30_DAYS') 30 | .orderBy('Cost DESC'); 31 | var adGroupIterator = adGroupSelector.get(); 32 | while (adGroupIterator.hasNext()) { 33 | var ad_group = adGroupIterator.next(), 34 | ad_group_id = ad_group.getId(), 35 | ad_group_name = ad_group.getName(), 36 | campaign_name = ad_group.getCampaign().getName(), 37 | campaign_id = ad_group.getCampaign().getId(); 38 | Logger.log('Campaign: ' + campaign_name + ', Ad Group: ' + ad_group_name); 39 | buildNewKeywords(ad_group_id, campaign_id); 40 | Logger.log('-----------------------------------------------------------------------------------------'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /search_keyword_builder/buildNewKeywords.gs: -------------------------------------------------------------------------------- 1 | function buildNewKeywords(ad_group_id, campaign_id) { 2 | 3 | var allNegativeKeywordsList = getNegativeKeywordForAdGroup(), // Минус-слова собранные со всех уровней (группа, кампания, списки) 4 | google_ads_queries = getSearchQweries(), // поисковые запросы по данным Google Ads 5 | google_analytics_queries = getGaReport(AdWordsApp.currentAccount().getCustomerId().replace(/\-/gm, ''), CONFIG().gaProfileId); // поисковые запросы по данным Google Aanalytics 6 | 7 | var full_queries_list = google_ads_queries.concat(google_analytics_queries); 8 | full_queries_list = unique(full_queries_list).sort(); 9 | 10 | addingKeywords(full_queries_list); // Добавляем новые ключевые слова 11 | 12 | function addingKeywords(arr) { 13 | var adGroupIterator = AdWordsApp.adGroups() 14 | .withCondition('CampaignId = ' + campaign_id) 15 | .withCondition('AdGroupId = ' + ad_group_id) 16 | .get(); 17 | while (adGroupIterator.hasNext()) { 18 | var adGroup = adGroupIterator.next(); 19 | for (var k = 0; k < arr.length; k++) { 20 | if (checkNegativeKeywords(arr[k]) != false) { // проверяем пересечение с минус-словами 21 | var match_types = CONFIG().targetMatchType; 22 | for (var m = 0; m < match_types.length; m++) { 23 | if (match_types[m] == 'BROAD') { 24 | var new_key = '+' + arr[k].replace(/ /gm, ' +'); 25 | } 26 | if (match_types[m] == 'PHRASE') { 27 | var new_key = '"' + arr[k] + '"'; 28 | } 29 | if (match_types[m] == 'EXACT') { 30 | var new_key = '[' + arr[k] + ']'; 31 | } 32 | var keywordOperation = adGroup.newKeywordBuilder() 33 | .withText(new_key) 34 | .build(); 35 | if (keywordOperation.isSuccessful()) { // Получение результатов. 36 | var keyword = keywordOperation.getResult(); 37 | var stats = keyword.getStatsFor('LAST_30_DAYS'); 38 | if (stats.getImpressions() == +0) { // если по добавленному ключевому слову вернулись показы, значит оно склеилось с существующим ключём 39 | keyword.pause(); 40 | keyword.applyLabel(customDateRange('now').toString()); 41 | keyword.applyLabel(CONFIG().scriptLabel); 42 | Logger.log('Добавили: ' + new_key.toString()); 43 | } 44 | } else { 45 | var errors = keywordOperation.getErrors(); // Исправление ошибок. 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | function getSearchQweries() { 54 | var report = []; 55 | var search_term_query = 'SELECT ' + 56 | 'search_term_view.search_term, ' + 57 | 'metrics.impressions ' + 58 | 'FROM search_term_view ' + 59 | 'WHERE search_term_view.status NOT IN ("ADDED", "ADDED_EXCLUDED", "EXCLUDED") ' + 60 | 'AND campaign.id = ' + campaign_id + ' ' + 61 | 'AND ad_group.id = ' + ad_group_id + ' ' + 62 | 'AND metrics.impressions >= ' + CONFIG().customDaysInDateRange + ' ' + 63 | 'AND segments.date BETWEEN "' + customDateRange('from') + '" AND "' + customDateRange('to') + '"'; 64 | var search_term_result = AdsApp.search(search_term_query, { 65 | apiVersion: 'v8' 66 | }); 67 | while (search_term_result.hasNext()) { 68 | var search_term_row = search_term_result.next(); 69 | var search_term = search_term_row.searchTermView.searchTerm.toLowerCase().trim(); 70 | var impressions = search_term_row.metrics.impressions; 71 | if (search_term.split(' ').length <= 7) { 72 | report.push(search_term); 73 | } 74 | } 75 | report = unique(report).sort(); 76 | return report; 77 | } 78 | 79 | function getGaReport(id, profile_id) { 80 | var report = []; 81 | var today = new Date(), 82 | start_date = new Date(today.getTime() - (CONFIG().customDaysInDateRange + CONFIG().customDateRangeShift) * 24 * 60 * 60 * 1000), 83 | end_date = new Date(today.getTime() - CONFIG().customDateRangeShift * 24 * 60 * 60 * 1000), 84 | start_formatted_date = Utilities.formatDate(start_date, 'UTC', 'yyyy-MM-dd'), 85 | end_formatted_date = Utilities.formatDate(end_date, 'UTC', 'yyyy-MM-dd'); 86 | var table_id = 'ga:' + profile_id; 87 | var metric = 'ga:impressions'; 88 | var options = { 89 | 'samplingLevel': 'HIGHER_PRECISION', 90 | 'dimensions': 'ga:keyword,ga:adMatchedQuery', 91 | 'sort': '-ga:impressions', 92 | 'filters': 'ga:adwordsCustomerID==' + id + ';ga:adKeywordMatchType!=Exact;ga:impressions>' + CONFIG().customDaysInDateRange + ';ga:adwordsAdGroupID==' + ad_group_id + ';ga:adwordsCampaignID==' + campaign_id, 93 | 'max-results': 10000 94 | }; 95 | var ga_report = Analytics.Data.Ga.get(table_id, start_formatted_date, end_formatted_date, metric, options); 96 | if (ga_report.rows) { 97 | for (var i = 0; i < ga_report.rows.length; i++) { 98 | var ga_row = ga_report.rows[i]; 99 | var keyword = ga_row[0].replace(/\+/gm, '').toLowerCase().trim(), 100 | ad_matched_query = ga_row[1].toLowerCase().trim(); 101 | if (keyword != ad_matched_query) { 102 | if (ad_matched_query.split(' ').length <= 7) { 103 | report.push(ad_matched_query); 104 | } 105 | } 106 | } 107 | } else { 108 | Logger.log('No rows returned.'); 109 | } 110 | report = unique(report).sort(); 111 | return report; 112 | } 113 | 114 | function getNegativeKeywordForAdGroup() { // Получаем минус-слова из группы 115 | var fullNegativeKeywordsList = []; 116 | 117 | var adGroupIterator = AdWordsApp.adGroups() 118 | .withCondition('CampaignId = ' + campaign_id) 119 | .withCondition('AdGroupId = ' + ad_group_id) 120 | .get(); 121 | if (adGroupIterator.hasNext()) { 122 | var adGroup = adGroupIterator.next(); 123 | var adGroupNegativeKeywordIterator = adGroup.negativeKeywords() 124 | .get(); 125 | while (adGroupNegativeKeywordIterator.hasNext()) { 126 | var adGroupNegativeKeyword = adGroupNegativeKeywordIterator.next(); 127 | fullNegativeKeywordsList[fullNegativeKeywordsList.length] = adGroupNegativeKeyword.getText(); 128 | } 129 | } 130 | var negativesListFromCampaign = getCampaignNegatives(campaign_id); 131 | fullNegativeKeywordsList = fullNegativeKeywordsList.concat(fullNegativeKeywordsList, negativesListFromCampaign).sort(); 132 | 133 | return fullNegativeKeywordsList; 134 | 135 | function getCampaignNegatives(campaign_id) { // Получаем минус-слова из кампании 136 | var campaignNegativeKeywordsList = []; 137 | var campaignIterator = AdWordsApp.campaigns() 138 | .withCondition('CampaignId = ' + campaign_id) 139 | .get(); 140 | if (campaignIterator.hasNext()) { 141 | var campaign = campaignIterator.next(); 142 | var negativeKeywordListSelector = campaign.negativeKeywordLists() // Получаем минус-слова из списков 143 | .withCondition('Status = ACTIVE'); 144 | var negativeKeywordListIterator = negativeKeywordListSelector.get(); 145 | while (negativeKeywordListIterator.hasNext()) { 146 | var negativeKeywordList = negativeKeywordListIterator.next(); 147 | var sharedNegativeKeywordIterator = negativeKeywordList.negativeKeywords().get(); 148 | var sharedNegativeKeywords = []; 149 | while (sharedNegativeKeywordIterator.hasNext()) { 150 | var negativeKeywordFromList = sharedNegativeKeywordIterator.next(); 151 | sharedNegativeKeywords[sharedNegativeKeywords.length] = negativeKeywordFromList.getText(); 152 | } 153 | campaignNegativeKeywordsList = campaignNegativeKeywordsList.concat(campaignNegativeKeywordsList, sharedNegativeKeywords); 154 | } 155 | var campaignNegativeKeywordIterator = campaign.negativeKeywords().get(); 156 | while (campaignNegativeKeywordIterator.hasNext()) { 157 | var campaignNegativeKeyword = campaignNegativeKeywordIterator.next(); 158 | campaignNegativeKeywordsList[campaignNegativeKeywordsList.length] = campaignNegativeKeyword.getText(); 159 | } 160 | } 161 | campaignNegativeKeywordsList = campaignNegativeKeywordsList.sort(); 162 | return campaignNegativeKeywordsList; 163 | } 164 | } 165 | 166 | function checkNegativeKeywords(keywordForCheck) { // это какой-то древний кусок, не буду его трогать 167 | function checkingNegativeKeywords() { 168 | var result = true; 169 | 170 | function checkResult(check) { 171 | if (check != true) { 172 | result = false; 173 | } 174 | } 175 | allNegativeKeywordsList.forEach( 176 | function (negativeKeyword) { 177 | var negativeWord = negativeKeyword.toString().toLowerCase(); 178 | var clearedNegativeKeyword = negativeWord.replace(/\+/g, '').replace(/\"/g, ''); 179 | if ((keywordForCheck.indexOf('[') != -1) || (keywordForCheck.indexOf('"') != -1)) { // минус-фраза с точным или фразовым соответствием 180 | if (negativeWord == keywordForCheck) { 181 | checkResult(false); 182 | // Logger.log('(1) Фраза: ' + keywordForCheck + ' Минус-фраза: ' + negativeWord); 183 | } else if (negativeWord.indexOf('[') == -1) { 184 | if (clearedNegativeKeyword.indexOf(' ') != -1) { 185 | if (keywordForCheck.indexOf(clearedNegativeKeyword) != -1) { 186 | // очищеная минус-фраза есть в ключевой фразе, но минус-фраза не в точном и не в широком соответствии 187 | checkResult(false); 188 | // Logger.log('(2) Фраза: ' + keywordForCheck + ' Минус-фраза: ' + negativeWord); 189 | } 190 | } else { 191 | var words = []; 192 | words = keywordForCheck.toLowerCase().replace(/\+/g, '').replace(/\"/g, '').replace(/\[/g, '').replace(/\]/g, '').split(' '); // разбиваем ключевую фразу на слова 193 | // Logger.log(words); 194 | words.forEach( 195 | function (word) { 196 | if (negativeWord == word) { // проверяем совпадение минус-фразы(слова), со словами в ключевой фразе 197 | checkResult(false); 198 | // Logger.log('(3) Фраза: ' + keywordForCheck + ' Минус-фраза: ' + negativeWord); 199 | } 200 | } 201 | ); 202 | } 203 | } 204 | } else { // минус-фраза с широким соответствием 205 | if (negativeWord.indexOf(' ') != -1) { 206 | var negativeWords = []; 207 | negativeWords = negativeWord.replace(/\+/g, '').replace(/\"/g, '').replace(/\[/g, '').replace(/\]/g, '').split(' '); 208 | var words = []; 209 | words = keywordForCheck.toLowerCase().replace(/\+/g, '').replace(/\"/g, '').replace(/\[/g, '').replace(/\]/g, '').split(' '); // разбиваем ключевую фразу на слова 210 | // Logger.log(words); 211 | Array.prototype.diff = function (a) { 212 | return this.filter(function (i) { 213 | return !(a.indexOf(i) > -1); 214 | }); 215 | }; 216 | var diffWords = negativeWords.diff(words); 217 | if (diffWords.length == 0) { 218 | checkResult(false); 219 | // Logger.log('(4) Фраза: ' + keywordForCheck + ' Минус-фраза: ' + negativeWord); 220 | } 221 | } else { 222 | var words = []; 223 | words = keywordForCheck.toLowerCase().replace(/\+/g, '').replace(/\"/g, '').replace(/\[/g, '').replace(/\]/g, '').split(' '); // разбиваем ключевую фразу на слова 224 | // Logger.log(words); 225 | words.forEach( 226 | function (word) { 227 | if (negativeWord == word) { // проверяем совпадение минус-фразы(слова), со словами в ключевой фразе 228 | checkResult(false); 229 | // Logger.log('(5) Фраза: ' + keywordForCheck + ' Минус-фраза: ' + negativeWord); 230 | } 231 | } 232 | ); 233 | } 234 | } 235 | } 236 | ); 237 | return result; 238 | } 239 | return checkingNegativeKeywords(); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /search_keyword_builder/common.gs: -------------------------------------------------------------------------------- 1 | function ensureAccountLabels() { 2 | function getAccountLabelNames() { 3 | var labelNames = []; 4 | var iterator = AdWordsApp.labels().get(); 5 | while (iterator.hasNext()) { 6 | labelNames.push(iterator.next().getName()); 7 | } 8 | return labelNames; 9 | } 10 | var labelNames = getAccountLabelNames(); 11 | if (labelNames.indexOf(CONFIG().scriptLabel) == -1) { 12 | AdWordsApp.createLabel(CONFIG().scriptLabel); 13 | } 14 | if (labelNames.indexOf(customDateRange('now')) == -1) { 15 | AdWordsApp.createLabel(customDateRange('now')); 16 | } 17 | Logger.log('Ярлыки проверены, создан ярлык за ' + customDateRange('now')); 18 | } 19 | 20 | function customDateRange(select) { // Формируем значение параметра временного диапазона для выборки AWQL 21 | var timeType = select; 22 | var MILLIS_PER_DAY = 1000 * 60 * 60 * 24; 23 | var now = new Date(); 24 | var fromDate = new Date(now.getTime() - (CONFIG().customDaysInDateRange + CONFIG().customDateRangeShift) * MILLIS_PER_DAY); 25 | var toDate = new Date(now.getTime() - CONFIG().customDateRangeShift * MILLIS_PER_DAY); 26 | var nowDate = new Date(now.getTime()); 27 | var timeZone = AdWordsApp.currentAccount().getTimeZone(); 28 | var fromformatDate = Utilities.formatDate(fromDate, timeZone, 'yyyyMMdd'); 29 | var toformatDate = Utilities.formatDate(toDate, timeZone, 'yyyyMMdd'); 30 | var nowformatDate = Utilities.formatDate(nowDate, timeZone, 'yyyyMMdd'); 31 | var duringDates = fromformatDate + ',' + toformatDate; 32 | if (timeType == 'from') { 33 | return fromformatDate; 34 | } else if (timeType == 'to') { 35 | return toformatDate; 36 | } else if (timeType == 'now') { 37 | return nowformatDate; 38 | } else { 39 | return duringDates; 40 | } 41 | } 42 | 43 | function unique(arr) { // убираем повторы 44 | var tmp = {}; 45 | return arr.filter(function (a) { 46 | return a in tmp ? 0 : tmp[a] = 1; 47 | }); 48 | } 49 | 50 | function getCurrentAccountDetails() { 51 | var currentAccount = AdWordsApp.currentAccount(); 52 | return currentAccount.getName(); 53 | } 54 | -------------------------------------------------------------------------------- /search_keyword_builder/config.gs: -------------------------------------------------------------------------------- 1 | function CONFIG() { 2 | return { 3 | // ID профиля GA с которым связан рекламный аккаунт 4 | gaProfileId: '1234567890', 5 | 6 | // Ярлык которым скрипт помечает созданные слова 7 | scriptLabel: 'Keyword Builder', 8 | 9 | // Указываем количество дней для выборки 10 | // Если хотим использовать данные о конверсиях или доходности, то в качестве значения 11 | // следует указывать число большее чем окно конверсии. 12 | customDaysInDateRange: 30, 13 | 14 | // Указываем на сколько дней от сегодняшнего мы сдвигаем выборку. 15 | // Нужно для того чтобы не брать те дни когда запаздывает статистика. 16 | // Если хотим использовать данные о конверсиях или доходности, то в качестве значения 17 | // следует указывать число равное дням в окне конверсии. 18 | customDateRangeShift: 0, 19 | 20 | // Добавляемые типы соответствий (BROAD, PHRASE, EXACT). Оставьте в списке только нужные 21 | // Широкове соответсвие добавляется с модификатором 22 | targetMatchType: [ 23 | 'BROAD', 24 | 'PHRASE', 25 | 'EXACT' 26 | ], 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /search_keyword_builder/readme.md: -------------------------------------------------------------------------------- 1 | Скрипт создающий новые ключевые слова на основании отчетов о поисковых запросах из Google Ads и Google Analytics 2 | --------------------------------------------------------------------------------