Skip to content

Instantly share code, notes, and snippets.

@spersico
Created October 22, 2022 15:56
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save spersico/19b92f2c37f01118c19f2ef9c113f0d7 to your computer and use it in GitHub Desktop.
Save spersico/19b92f2c37f01118c19f2ef9c113f0d7 to your computer and use it in GitHub Desktop.
Huawei Notes HTML to TXT
import { writeFile, readdir, readFile } from 'node:fs/promises';
import path from 'path';
/*
Steps to get/export your notes from Huawei Notes:
1. Login into a Huawei Account in the phone.
2. Activate in your phone, the Notes Syncing, which is inside of Huawei Account > Cloud
3. Log in into https://cloud.huawei.com
4. Go to https://cloud.huawei.com/home#/account/gdpr, and click on Download Notes
5. This will give you a zip file with a password. Extract the zip file into a folder.
6. Copy this file into the folder as index.mjs
7. You'll need NodeJs installed for this: https://nodejs.org/en/
I made and tested this on v18+, but v19 and v16 should also work
8. open a console/terminal (see how to do that in your OS), and run in it "node index.mjs"
9. Your notes should be in the notes.txt file inside of the same folder.
Extra:
The script only copies the text content, as simple text,
and it doesn't copy the title or other stuff that your notes might contain.
I left a comment in the portion of code that might be helpful to modify if
you want more information, such as the creation time, or other info.
*/
async function readNotes() {
console.log(`๐Ÿ“ | Huawei Notes HTML -> TXT`);
const __dirname = path.resolve(path.dirname(''));
console.log(`๐Ÿ“ | > Reading Directory`, __dirname);
const folders = [];
const files = await readdir(__dirname, {
withFileTypes: true,
});
files.forEach((file) => {
if (file.isDirectory()) folders.push(file.name);
});
console.log(`๐Ÿ“ | > Notes found: `, folders.length);
const notes = await Promise.all(
folders.map(async (folder) => {
const route = `${__dirname}/${folder}/json.js`;
return readFile(route, 'utf8')
.then((weirdJs) => {
const noteData = JSON.parse(
weirdJs.replace('var data = ', '')
).content;
/*
noteData has all the notes information, AFAICS.
If you want to have more info, such as timestamps,
this is the place to look into to add more info.
I only needed the text content in my case
*/
return noteData.content.split('|')[1].trim();
})
.catch((reason) => {
console.error(`๐Ÿ› | > Error: `, route, reason);
return '';
});
})
);
const cleanedUpNotes = notes.filter(Boolean);
console.log(
`๐Ÿ“ | > Total after removing empty or errored: `,
cleanedUpNotes.length
);
await writeFile('notes.txt', cleanedUpNotes.join('\n'), 'utf8');
console.log(`๐Ÿ“ | Notes succesfully exported into notes.txt file! ๐ŸŽ‰`);
}
readNotes();
/*
MIT License
Copyright ยฉ 2022 Santiago Persico
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the โ€œSoftwareโ€), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED โ€œAS ISโ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
@CmdrMaylee
Copy link

Excellent script! Worked a charm and was easy to adapt for the Huawei notepad todo-section as well! Great big props to you :)

@cbreezier
Copy link

This is great, thanks @spersico

I was tearing my hair out trying to dump the notes data in any form at all, but your method of using the GDPR data request was perfect (thanks EU).

I noticed a lot of weird characters in my data, specifically the string <>><><<<. I figured that this was some kind of delimiter (naughty naughty on the part of the Huawei devs - don't mix control characters and data). The Text|... and Bullet|... and Attachment|... were also definitely data type blocks.

I made a few small improvements:

  1. Split by the weird <>><><<< delimiter
  2. Understand the different data types in a basic way
  3. Import into Google Keep

I'll just copy my updates here - they're nowhere near as polished as @spersico 's work and it's all just a rough "this only has to run once" script, but it may help someone else in future.

For the Google Keep import, I tried using https://github.com/kiwiz/gkeepapi but ran into unresolvable authentication errors. I also looked into the official Google Keep API, but it's only applicable to enterprise environments. So I did the only hacky alternative left which was driving the web UI with Puppeteer.

import { writeFile, readdir, readFile } from 'node:fs/promises';
import path from 'path';
import puppeteer from "puppeteer";
/*
Steps to get/export your notes from Huawei Notes:
1. Login into a Huawei Account in the phone.
2. Activate in your phone, the Notes Syncing, which is inside of Huawei Account > Cloud
3. Log in into https://cloud.huawei.com
4. Go to https://cloud.huawei.com/home#/account/gdpr, and click on Download Notes
5. This will give you a zip file with a password. Extract the zip file into a folder.
6. Copy this file into the folder as index.mjs
7. You'll need NodeJs installed for this: https://nodejs.org/en/
   I made and tested this on v18+, but v19 and v16 should also work
8. open a console/terminal (see how to do that in your OS), and run in it "node index.mjs"
9. Your notes should be in the notes.txt file inside of the same folder.
Extra:
The script only copies the text content, as simple text,
and it doesn't copy the title or other stuff that your notes might contain.
I left a comment in the portion of code that might be helpful to modify if
you want more information, such as the creation time, or other info.
*/

async function readNotes() {
  console.log(`๐Ÿ“ | Huawei Notes HTML -> TXT`);
  const __dirname = path.resolve(path.dirname(''));
  console.log(`๐Ÿ“ | > Reading Directory`, __dirname);
  const folders = [];

  const files = await readdir(__dirname, {
    withFileTypes: true,
  });
  files.forEach((file) => {
    if (file.isDirectory()) folders.push(file.name);
  });
  console.log(`๐Ÿ“ | > Notes found: `, folders.length);

  const notes = await Promise.all(
    folders.map(async (folder) => {
      const route = `${__dirname}/${folder}/json.js`;
      return readFile(route, 'utf8')
        .then((weirdJs) => {
          /*
          noteData has all the notes information, AFAICS.
          If you want to have more info, such as timestamps,
          this is the place to look into to add more info.
          I only needed the text content in my case
          */
          const noteData = JSON.parse(
            weirdJs.replace('var data = ', '')
          ).content;
          
          const title = noteData.title.split('\n')[0].trim();
          const contentBlocks = noteData.content.split('<>><><<<');
          const contents = contentBlocks.map((block) => {
            const type = block.split('|')[0];
            const data = block.split('|')[1].trim();
            
            if (type === 'Text') {
              return data;
            } else if (type === 'Bullet') {
              return ` - ${data}`;
            } else if (type === 'Attachment') {
              const fileName = data.split('/').reduce((_, cur) => cur);
              return `Attachment(${folder}/${fileName})`;
            } else {
              console.warn(`Unknown block type: ${type}`);
            }
          });
          
          return {
            title,
            contents: contents.join('\n'),
            hasAttachment: Boolean(noteData.has_attachment),
          };
        })
        .catch((reason) => {
          console.error(`๐Ÿ› | > Error: `, route, reason);
          return '';
        });
    })
  );
  const cleanedUpNotes = notes.filter(Boolean);
  console.log(
    `๐Ÿ“ | > Total after removing empty or errored: `,
    cleanedUpNotes.length
  );
  
  const browser = await puppeteer.connect({
    browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser/47075516-0e1b-40df-b7f8-3f28f973ad63',
  });

  const page = await browser.newPage();
  await page.goto('https://keep.google.com/u/0/');

  async function createNote(title, contents) {
    console.log('Creating note...');
    console.log('About to click the input box to expand into title and contents');
    // "Take a note" input - expands into title and contents fields after clicking
    await page.click('.h1U9Be-YPqjbf');

    // "Title" input
    console.log('Waiting for title input');
    await page.waitForSelector('.r4nke-YPqjbf');
    await page.click('.r4nke-YPqjbf');
    await page.type('.r4nke-YPqjbf', title);

    // "Take a note" input (contents)
    console.log('Waiting for contents input');
    await page.waitForSelector('.h1U9Be-YPqjbf');
    await page.click('.h1U9Be-YPqjbf');
    await page.type('.h1U9Be-YPqjbf', contents);

    // "Close" button
    console.log('Waiting for close button');
    await page.waitForSelector('.VIpgJd-LgbsSe');
    await page.click('.VIpgJd-LgbsSe');

    // Scroll back to the top so that the inputs are visible again for the next note
    await page.evaluate(() => window.scroll(0, 0));
  }

  for (const note of cleanedUpNotes.slice(36)) {
    await createNote(note.title, note.contents);
  }

  await browser.disconnect();

  await writeFile('notes.jsonl', cleanedUpNotes.map((it) => JSON.stringify(it)).join('\n'), 'utf8');
  console.log(`๐Ÿ“ | Notes succesfully exported into notes.jsonl file! ๐ŸŽ‰`);
}

readNotes();

/* 
MIT License
Copyright ยฉ 2022 Santiago Persico
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the โ€œSoftwareโ€), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED โ€œAS ISโ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
*/

To run this successfully, some prerequisites:

  1. npm install puppeteer
  2. Close any running instances of Chrome
  3. /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 or the equivalent Chrome binary on your OS/installation. Pick whatever port you want. Note down the output of this command, something like DevTools listening on ws://127.0.0.1:9222/devtools/browser/47075516-0e1b-40df-b7f8-3f28f973ad63
  4. Log into your Google account and make sure you can access your notes at https://keep.google.com/u/0/
  5. node index.mjs

Some notes and caveats about my updated script:

  • I try and read the title from the note data, but Huawei's format is pretty busted and the title seems to often (but not always) be duplicated with the first line of the contents
  • I convert bullet lists into lines prefixed with -. I don't do anything fancy to turn them into proper Google Keep bullet points
  • I don't handle attachments well - I don't automatically insert the image into the new Google Keep notes. You'll have to manually insert these in yourself, but I do leave references in the note like Attachment(<where you'll find the file>)
  • I output in JSONL format instead of a text file
  • The CSS selectors I use to interact with the Google Keep UI are very brittle. They're simple classnames (which aren't unique and it only works because the "create a note" stuff is at the top of the page), and they're autogenerated classnames that are liable to change whenever Google redeploys their frontend. You'll have to modify these most likely

@spersico
Copy link
Author

Cool addition @cbreezier!
Sadly, I don't have the data I used anymore (because I was under the same mindset, it was a โ€œrun onceโ€ project), but I would have loved to have your addition at the time I ran my script, 'cause my original plan also was to add the output to google notes ๐Ÿ˜….

@pat-exe
Copy link

pat-exe commented Nov 16, 2023

Thanks for the script Spersico, I've been trying to find something to export my huawei notes into some sort of readable format. For this script to run properly, which directory should I move the extracted notes (step 5 in your instructions) to? Leave it in user > downloads or move it to the root directory?

Also my knowledge in terms of coding is very very small, how would I add more note information under noteData? I would like to be able to retrieve the title and date data from the notes if possible.

For context I'm using nodejs v20.9

Update: I decided to just manually copy and paste the notes into notepad txt files, which took some time but worked in the end. All my data has now been saved from my old phone. Even though I didn't use the script, thanks for providing it :)

@macs-massimopiazza
Copy link

macs-massimopiazza commented Apr 20, 2024

Yeah thanks man. It was super useful!

I also needed to save timestamps and other informations. So i made a custom template that prints each note in a prettier way and with more informations in the notes.txt. Not the best, but is was a run once thing for me eheh.
I'm sharing it here in case anyone needs it. (it's just the notes generation part, rows from 38 to 59 of the original script)

 const notes = await Promise.all(
    folders.map(async (folder, i) => {
      const route = `${__dirname}/${folder}/json.js`;
      return readFile(route, 'utf8')
        .then((weirdJs) => {
          const noteData = JSON.parse(
            weirdJs.replace('var data = ', '')
          ).content;
          return `---------START-----------\ncount: ${i + 1}/${folders.length}\ncreated: ${new Date(noteData.created).toLocaleDateString()}\nmodified: ${new Date(noteData.modified).toLocaleDateString()}\nfavorite: ${noteData.favorite == 0 ? 'no' : 'yes'}\ntag_id: ${noteData.tag_id}\n--------------------\n${noteData.content.split('|')[1].trim()}\n----------END----------`;;
        })
        .catch((reason) => {
          console.error(`๐Ÿ› | > Error: `, route, reason);
          return '';
        });
    })
  );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment